diff options
author | Joram Wilander <jwawilander@gmail.com> | 2017-03-16 14:58:33 -0400 |
---|---|---|
committer | Corey Hulen <corey@hulen.com> | 2017-03-16 11:58:33 -0700 |
commit | d757645c2490dd4f0de16cc32e05551b1476d0a0 (patch) | |
tree | 5d6e4049aa296b7629a34ca5b02d0aba25dd27dc | |
parent | 04c0223c6402b12e67c61474ae310b0a56af6482 (diff) | |
download | chat-d757645c2490dd4f0de16cc32e05551b1476d0a0.tar.gz chat-d757645c2490dd4f0de16cc32e05551b1476d0a0.tar.bz2 chat-d757645c2490dd4f0de16cc32e05551b1476d0a0.zip |
Implement some channel endpoints for APIv4 (#5767)
-rw-r--r-- | api4/api.go | 2 | ||||
-rw-r--r-- | api4/channel.go | 79 | ||||
-rw-r--r-- | api4/channel_test.go | 125 | ||||
-rw-r--r-- | app/channel.go | 14 | ||||
-rw-r--r-- | i18n/en.json | 4 | ||||
-rw-r--r-- | model/channel_member.go | 20 | ||||
-rw-r--r-- | model/channel_member_test.go | 14 | ||||
-rw-r--r-- | model/client4.go | 32 | ||||
-rw-r--r-- | store/sql_channel_store.go | 35 | ||||
-rw-r--r-- | store/sql_channel_store_test.go | 74 | ||||
-rw-r--r-- | store/store.go | 1 |
11 files changed, 399 insertions, 1 deletions
diff --git a/api4/api.go b/api4/api.go index 422af7b7b..75f2e0254 100644 --- a/api4/api.go +++ b/api4/api.go @@ -35,6 +35,7 @@ type Routes struct { Channels *mux.Router // 'api/v4/channels' Channel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}' + ChannelForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}' ChannelByName *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}' ChannelByNameForTeamName *mux.Router // 'api/v4/teams/name/{team_name:[A-Za-z0-9_-]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}' ChannelsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels' @@ -113,6 +114,7 @@ func InitApi(full bool) { BaseRoutes.Channels = BaseRoutes.ApiRoot.PathPrefix("/channels").Subrouter() BaseRoutes.Channel = BaseRoutes.Channels.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter() + BaseRoutes.ChannelForUser = BaseRoutes.User.PathPrefix("/channels/{channel_id:[A-Za-z0-9]+}").Subrouter() BaseRoutes.ChannelByName = BaseRoutes.Team.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter() BaseRoutes.ChannelByNameForTeamName = BaseRoutes.TeamByName.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter() BaseRoutes.ChannelsForTeam = BaseRoutes.Team.PathPrefix("/channels").Subrouter() diff --git a/api4/channel.go b/api4/channel.go index acf14846a..5ed63320b 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -17,21 +17,26 @@ func InitChannel() { BaseRoutes.Channels.Handle("", ApiSessionRequired(createChannel)).Methods("POST") BaseRoutes.Channels.Handle("/direct", ApiSessionRequired(createDirectChannel)).Methods("POST") + BaseRoutes.Channels.Handle("/members/{user_id:[A-Za-z0-9]+}/view", ApiSessionRequired(viewChannel)).Methods("POST") BaseRoutes.Team.Handle("/channels", ApiSessionRequired(getPublicChannelsForTeam)).Methods("GET") BaseRoutes.Channel.Handle("", ApiSessionRequired(getChannel)).Methods("GET") BaseRoutes.Channel.Handle("", ApiSessionRequired(updateChannel)).Methods("PUT") BaseRoutes.Channel.Handle("", ApiSessionRequired(deleteChannel)).Methods("DELETE") + BaseRoutes.Channel.Handle("/stats", ApiSessionRequired(getChannelStats)).Methods("GET") + + BaseRoutes.ChannelForUser.Handle("/unread", ApiSessionRequired(getChannelUnread)).Methods("GET") + BaseRoutes.ChannelByName.Handle("", ApiSessionRequired(getChannelByName)).Methods("GET") BaseRoutes.ChannelByNameForTeamName.Handle("", ApiSessionRequired(getChannelByNameForTeamName)).Methods("GET") BaseRoutes.ChannelMembers.Handle("", ApiSessionRequired(getChannelMembers)).Methods("GET") + BaseRoutes.ChannelMembers.Handle("/ids", ApiSessionRequired(getChannelMembersByIds)).Methods("POST") BaseRoutes.ChannelMembersForUser.Handle("", ApiSessionRequired(getChannelMembersForUser)).Methods("GET") BaseRoutes.ChannelMember.Handle("", ApiSessionRequired(getChannelMember)).Methods("GET") BaseRoutes.ChannelMember.Handle("", ApiSessionRequired(removeChannelMember)).Methods("DELETE") BaseRoutes.ChannelMember.Handle("/roles", ApiSessionRequired(updateChannelMemberRoles)).Methods("PUT") - BaseRoutes.Channels.Handle("/members/{user_id:[A-Za-z0-9]+}/view", ApiSessionRequired(viewChannel)).Methods("POST") } func createChannel(c *Context, w http.ResponseWriter, r *http.Request) { @@ -207,6 +212,53 @@ func getChannel(c *Context, w http.ResponseWriter, r *http.Request) { } } +func getChannelUnread(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireChannelId().RequireUserId() + if c.Err != nil { + return + } + + if !app.SessionHasPermissionToUser(c.Session, c.Params.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + if !app.SessionHasPermissionToChannel(c.Session, c.Params.ChannelId, model.PERMISSION_READ_CHANNEL) { + c.SetPermissionError(model.PERMISSION_READ_CHANNEL) + return + } + + channelUnread, err := app.GetChannelUnread(c.Params.ChannelId, c.Params.UserId) + if err != nil { + c.Err = err + return + } + + w.Write([]byte(channelUnread.ToJson())) +} + +func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireChannelId() + if c.Err != nil { + return + } + + if !app.SessionHasPermissionToChannel(c.Session, c.Params.ChannelId, model.PERMISSION_READ_CHANNEL) { + c.SetPermissionError(model.PERMISSION_READ_CHANNEL) + return + } + + memberCount, err := app.GetChannelMemberCount(c.Params.ChannelId) + + if err != nil { + c.Err = err + return + } + + stats := model.ChannelStats{ChannelId: c.Params.ChannelId, MemberCount: memberCount} + w.Write([]byte(stats.ToJson())) +} + func getPublicChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) { c.RequireTeamId() if c.Err != nil { @@ -326,6 +378,31 @@ func getChannelMembers(c *Context, w http.ResponseWriter, r *http.Request) { } } +func getChannelMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireChannelId() + if c.Err != nil { + return + } + + userIds := model.ArrayFromJson(r.Body) + if len(userIds) == 0 { + c.SetInvalidParam("user_ids") + return + } + + if !app.SessionHasPermissionToChannel(c.Session, c.Params.ChannelId, model.PERMISSION_READ_CHANNEL) { + c.SetPermissionError(model.PERMISSION_READ_CHANNEL) + return + } + + if members, err := app.GetChannelMembersByIds(c.Params.ChannelId, userIds); err != nil { + c.Err = err + return + } else { + w.Write([]byte(members.ToJson())) + } +} + func getChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { c.RequireChannelId().RequireUserId() if c.Err != nil { diff --git a/api4/channel_test.go b/api4/channel_test.go index 97364a73e..bf7218f0b 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -759,6 +759,53 @@ func TestGetChannelMembers(t *testing.T) { CheckNoError(t, resp) } +func TestGetChannelMembersByIds(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + + cm, resp := Client.GetChannelMembersByIds(th.BasicChannel.Id, []string{th.BasicUser.Id}) + CheckNoError(t, resp) + + if (*cm)[0].UserId != th.BasicUser.Id { + t.Fatal("returned wrong user") + } + + _, resp = Client.GetChannelMembersByIds(th.BasicChannel.Id, []string{}) + CheckBadRequestStatus(t, resp) + + cm1, resp := Client.GetChannelMembersByIds(th.BasicChannel.Id, []string{"junk"}) + CheckNoError(t, resp) + if len(*cm1) > 0 { + t.Fatal("no users should be returned") + } + + cm1, resp = Client.GetChannelMembersByIds(th.BasicChannel.Id, []string{"junk", th.BasicUser.Id}) + CheckNoError(t, resp) + if len(*cm1) != 1 { + t.Fatal("1 member should be returned") + } + + cm1, resp = Client.GetChannelMembersByIds(th.BasicChannel.Id, []string{th.BasicUser2.Id, th.BasicUser.Id}) + CheckNoError(t, resp) + if len(*cm1) != 2 { + t.Fatal("2 members should be returned") + } + + _, resp = Client.GetChannelMembersByIds("junk", []string{th.BasicUser.Id}) + CheckBadRequestStatus(t, resp) + + _, resp = Client.GetChannelMembersByIds(model.NewId(), []string{th.BasicUser.Id}) + CheckForbiddenStatus(t, resp) + + Client.Logout() + _, resp = Client.GetChannelMembersByIds(th.BasicChannel.Id, []string{th.BasicUser.Id}) + CheckUnauthorizedStatus(t, resp) + + _, resp = th.SystemAdminClient.GetChannelMembersByIds(th.BasicChannel.Id, []string{th.BasicUser2.Id, th.BasicUser.Id}) + CheckNoError(t, resp) +} + func TestGetChannelMember(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer TearDown() @@ -914,6 +961,84 @@ func TestViewChannel(t *testing.T) { CheckNoError(t, resp) } +func TestGetChannelUnread(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + user := th.BasicUser + channel := th.BasicChannel + + channelUnread, resp := Client.GetChannelUnread(channel.Id, user.Id) + CheckNoError(t, resp) + if channelUnread.TeamId != th.BasicTeam.Id { + t.Fatal("wrong team id returned for a regular user call") + } else if channelUnread.ChannelId != channel.Id { + t.Fatal("wrong team id returned for a regular user call") + } + + _, resp = Client.GetChannelUnread("junk", user.Id) + CheckBadRequestStatus(t, resp) + + _, resp = Client.GetChannelUnread(channel.Id, "junk") + CheckBadRequestStatus(t, resp) + + _, resp = Client.GetChannelUnread(channel.Id, model.NewId()) + CheckForbiddenStatus(t, resp) + + _, resp = Client.GetChannelUnread(model.NewId(), user.Id) + CheckForbiddenStatus(t, resp) + + newUser := th.CreateUser() + Client.Login(newUser.Email, newUser.Password) + _, resp = Client.GetChannelUnread(th.BasicChannel.Id, user.Id) + CheckForbiddenStatus(t, resp) + + Client.Logout() + + _, resp = th.SystemAdminClient.GetChannelUnread(channel.Id, user.Id) + CheckNoError(t, resp) + + _, resp = th.SystemAdminClient.GetChannelUnread(model.NewId(), user.Id) + CheckNotFoundStatus(t, resp) + + _, resp = th.SystemAdminClient.GetChannelUnread(channel.Id, model.NewId()) + CheckNotFoundStatus(t, resp) +} + +func TestGetChannelStats(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + channel := th.CreatePrivateChannel() + + stats, resp := Client.GetChannelStats(channel.Id, "") + CheckNoError(t, resp) + + if stats.ChannelId != channel.Id { + t.Fatal("couldnt't get extra info") + } else if stats.MemberCount != 1 { + t.Fatal("got incorrect member count") + } + + _, resp = Client.GetChannelStats("junk", "") + CheckBadRequestStatus(t, resp) + + _, resp = Client.GetChannelStats(model.NewId(), "") + CheckForbiddenStatus(t, resp) + + Client.Logout() + _, resp = Client.GetChannelStats(channel.Id, "") + CheckUnauthorizedStatus(t, resp) + + th.LoginBasic2() + + _, resp = Client.GetChannelStats(channel.Id, "") + CheckForbiddenStatus(t, resp) + + _, resp = th.SystemAdminClient.GetChannelStats(channel.Id, "") + CheckNoError(t, resp) +} + func TestUpdateChannelRoles(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer TearDown() diff --git a/app/channel.go b/app/channel.go index 49d55e52f..f63592000 100644 --- a/app/channel.go +++ b/app/channel.go @@ -702,6 +702,20 @@ func GetChannelCounts(teamId string, userId string) (*model.ChannelCounts, *mode } } +func GetChannelUnread(channelId, userId string) (*model.ChannelUnread, *model.AppError) { + result := <-Srv.Store.Channel().GetChannelUnread(channelId, userId) + if result.Err != nil { + return nil, result.Err + } + channelUnread := result.Data.(*model.ChannelUnread) + + if channelUnread.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.CHANNEL_MARK_UNREAD_MENTION { + channelUnread.MsgCount = 0 + } + + return channelUnread, nil +} + func JoinChannel(channel *model.Channel, userId string) *model.AppError { if channel.DeleteAt > 0 { return model.NewLocAppError("JoinChannel", "api.channel.join_channel.already_deleted.app_error", nil, "") diff --git a/i18n/en.json b/i18n/en.json index 8e63fe567..55c3bb7ed 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -36,6 +36,10 @@ "translation": "June" }, { + "id": "store.sql_channel.get_unread.app_error", + "translation": "We couldn't get the channel unread messages" + }, + { "id": "March", "translation": "March" }, diff --git a/model/channel_member.go b/model/channel_member.go index b9a08c769..32ffaf606 100644 --- a/model/channel_member.go +++ b/model/channel_member.go @@ -47,6 +47,15 @@ func (o *ChannelMembers) ToJson() string { } } +func (o *ChannelUnread) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + func ChannelMembersFromJson(data io.Reader) *ChannelMembers { decoder := json.NewDecoder(data) var o ChannelMembers @@ -58,6 +67,17 @@ func ChannelMembersFromJson(data io.Reader) *ChannelMembers { } } +func ChannelUnreadFromJson(data io.Reader) *ChannelUnread { + decoder := json.NewDecoder(data) + var o ChannelUnread + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + func (o *ChannelMember) ToJson() string { b, err := json.Marshal(o) if err != nil { diff --git a/model/channel_member_test.go b/model/channel_member_test.go index e43560cee..6fb666300 100644 --- a/model/channel_member_test.go +++ b/model/channel_member_test.go @@ -69,3 +69,17 @@ func TestChannelMemberIsValid(t *testing.T) { t.Fatal(err) } } + +func TestChannelUnreadJson(t *testing.T) { + o := ChannelUnread{ChannelId: NewId(), TeamId: NewId(), MsgCount: 5, MentionCount: 3} + json := o.ToJson() + ro := ChannelUnreadFromJson(strings.NewReader(json)) + + if o.TeamId != ro.TeamId { + t.Fatal("Team Ids do not match") + } + + if o.MentionCount != ro.MentionCount { + t.Fatal("MentionCount do not match") + } +} diff --git a/model/client4.go b/model/client4.go index 2da69b6b4..6e862a1db 100644 --- a/model/client4.go +++ b/model/client4.go @@ -842,6 +842,16 @@ func (c *Client4) GetChannel(channelId, etag string) (*Channel, *Response) { } } +// GetChannelStats returns statistics for a channel. +func (c *Client4) GetChannelStats(channelId string, etag string) (*ChannelStats, *Response) { + if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/stats", etag); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return ChannelStatsFromJson(r.Body), BuildResponse(r) + } +} + // GetPublicChannelsForTeam returns a channel based on the provided team id string. func (c *Client4) GetPublicChannelsForTeam(teamId string, page int, perPage int, etag string) (*ChannelList, *Response) { query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) @@ -894,6 +904,17 @@ func (c *Client4) GetChannelMembers(channelId string, page, perPage int, etag st } } +// GetChannelMembersByIds gets the channel members in a channel for a list of user ids. +func (c *Client4) GetChannelMembersByIds(channelId string, userIds []string) (*ChannelMembers, *Response) { + if r, err := c.DoApiPost(c.GetChannelMembersRoute(channelId)+"/ids", ArrayToJson(userIds)); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return ChannelMembersFromJson(r.Body), BuildResponse(r) + + } +} + // GetChannelMember gets a channel member. func (c *Client4) GetChannelMember(channelId, userId, etag string) (*ChannelMember, *Response) { if r, err := c.DoApiGet(c.GetChannelMemberRoute(channelId, userId), etag); err != nil { @@ -925,6 +946,17 @@ func (c *Client4) ViewChannel(userId string, view *ChannelView) (bool, *Response } } +// GetChannelUnread will return a ChannelUnread object that contains the number of +// unread messages and mentions for a user. +func (c *Client4) GetChannelUnread(channelId, userId string) (*ChannelUnread, *Response) { + if r, err := c.DoApiGet(c.GetUserRoute(userId)+c.GetChannelRoute(channelId)+"/unread", ""); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return ChannelUnreadFromJson(r.Body), BuildResponse(r) + } +} + // UpdateChannelRoles will update the roles on a channel for a user. func (c *Client4) UpdateChannelRoles(channelId, userId, roles string) (bool, *Response) { requestBody := map[string]string{"roles": roles} diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go index a85bb0656..d72722f7c 100644 --- a/store/sql_channel_store.go +++ b/store/sql_channel_store.go @@ -309,6 +309,41 @@ func (s SqlChannelStore) extraUpdated(channel *model.Channel) StoreChannel { return storeChannel } +func (s SqlChannelStore) GetChannelUnread(channelId, userId string) StoreChannel { + storeChannel := make(StoreChannel, 1) + + go func() { + result := StoreResult{} + + var unreadChannel model.ChannelUnread + err := s.GetReplica().SelectOne(&unreadChannel, + `SELECT + Channels.TeamId TeamId, Channels.Id ChannelId, (Channels.TotalMsgCount - ChannelMembers.MsgCount) MsgCount, ChannelMembers.MentionCount MentionCount, ChannelMembers.NotifyProps NotifyProps + FROM + Channels, ChannelMembers + WHERE + Id = ChannelId + AND Id = :ChannelId + AND UserId = :UserId + AND DeleteAt = 0`, + map[string]interface{}{"ChannelId": channelId, "UserId": userId}) + + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.GetChannelUnread", "store.sql_channel.get_unread.app_error", nil, "channelId="+channelId+" "+err.Error(), http.StatusInternalServerError) + if err == sql.ErrNoRows { + result.Err.StatusCode = http.StatusNotFound + } + } else { + result.Data = &unreadChannel + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (us SqlChannelStore) InvalidateChannel(id string) { channelCache.Remove(id) } diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go index 0e726eab7..f347fa438 100644 --- a/store/sql_channel_store_test.go +++ b/store/sql_channel_store_test.go @@ -171,6 +171,80 @@ func TestChannelStoreUpdate(t *testing.T) { } } +func TestGetChannelUnread(t *testing.T) { + Setup() + + teamId1 := model.NewId() + teamId2 := model.NewId() + + uid := model.NewId() + m1 := &model.TeamMember{TeamId: teamId1, UserId: uid} + m2 := &model.TeamMember{TeamId: teamId2, UserId: uid} + Must(store.Team().SaveMember(m1)) + Must(store.Team().SaveMember(m2)) + notifyPropsModel := model.GetDefaultChannelNotifyProps() + + // Setup Channel 1 + c1 := &model.Channel{TeamId: m1.TeamId, Name: model.NewId(), DisplayName: "Downtown", Type: model.CHANNEL_OPEN, TotalMsgCount: 100} + Must(store.Channel().Save(c1)) + cm1 := &model.ChannelMember{ChannelId: c1.Id, UserId: m1.UserId, NotifyProps: notifyPropsModel, MsgCount: 90} + Must(store.Channel().SaveMember(cm1)) + + // Setup Channel 2 + c2 := &model.Channel{TeamId: m2.TeamId, Name: model.NewId(), DisplayName: "Cultural", Type: model.CHANNEL_OPEN, TotalMsgCount: 100} + Must(store.Channel().Save(c2)) + cm2 := &model.ChannelMember{ChannelId: c2.Id, UserId: m2.UserId, NotifyProps: notifyPropsModel, MsgCount: 90, MentionCount: 5} + Must(store.Channel().SaveMember(cm2)) + + // Check for Channel 1 + if resp := <-store.Channel().GetChannelUnread(c1.Id, uid); resp.Err != nil { + t.Fatal(resp.Err) + } else { + ch := resp.Data.(*model.ChannelUnread) + if c1.Id != ch.ChannelId { + t.Fatal("wrong channel id") + } + + if teamId1 != ch.TeamId { + t.Fatal("wrong team id for channel 1") + } + + if ch.NotifyProps == nil { + t.Fatal("wrong props for channel 1") + } + + if ch.MentionCount != 0 { + t.Fatal("wrong MentionCount for channel 1") + } + + if ch.MsgCount != 10 { + t.Fatal("wrong MsgCount for channel 1") + } + } + + // Check for Channel 2 + if resp2 := <-store.Channel().GetChannelUnread(c2.Id, uid); resp2.Err != nil { + t.Fatal(resp2.Err) + } else { + ch2 := resp2.Data.(*model.ChannelUnread) + if c2.Id != ch2.ChannelId { + t.Fatal("wrong channel id") + } + + if teamId2 != ch2.TeamId { + t.Fatal("wrong team id") + } + + if ch2.MentionCount != 5 { + t.Fatal("wrong MentionCount for channel 2") + } + + if ch2.MsgCount != 10 { + t.Fatal("wrong MsgCount for channel 2") + } + } +} + func TestChannelStoreGet(t *testing.T) { Setup() diff --git a/store/store.go b/store/store.go index 497f613da..72572b1e0 100644 --- a/store/store.go +++ b/store/store.go @@ -134,6 +134,7 @@ type ChannelStore interface { SearchMore(userId string, teamId string, term string) StoreChannel GetMembersByIds(channelId string, userIds []string) StoreChannel AnalyticsDeletedTypeCount(teamId string, channelType string) StoreChannel + GetChannelUnread(channelId, userId string) StoreChannel } type PostStore interface { |