From 3059abdd88eaf74968d0249bde405c941d0538fd Mon Sep 17 00:00:00 2001 From: Konstantinos Pittas Date: Mon, 5 Dec 2022 16:04:44 +0200 Subject: [PATCH] [MM-44765] Add API method for getting post's information from permalink (#21518) * add api for getting post's information * add information about current user state Co-authored-by: Mattermod --- api4/post.go | 22 +++ api4/post_test.go | 256 +++++++++++++++++++++++++++ app/app_iface.go | 1 + app/opentracing/opentracing_layer.go | 22 +++ app/post.go | 68 +++++++ model/client4.go | 14 ++ model/post_info.go | 15 ++ 7 files changed, 398 insertions(+) create mode 100644 model/post_info.go diff --git a/api4/post.go b/api4/post.go index 6aa7049563..0d9f369731 100644 --- a/api4/post.go +++ b/api4/post.go @@ -23,6 +23,7 @@ func (api *API) InitPost() { api.BaseRoutes.Posts.Handle("/ids", api.APISessionRequired(getPostsByIds)).Methods("POST") api.BaseRoutes.Posts.Handle("/ephemeral", api.APISessionRequired(createEphemeralPost)).Methods("POST") api.BaseRoutes.Post.Handle("/thread", api.APISessionRequired(getPostThread)).Methods("GET") + api.BaseRoutes.Post.Handle("/info", api.APISessionRequired(getPostInfo)).Methods("GET") api.BaseRoutes.Post.Handle("/files/info", api.APISessionRequired(getFileInfosForPost)).Methods("GET") api.BaseRoutes.PostsForChannel.Handle("", api.APISessionRequired(getPostsForChannel)).Methods("GET") api.BaseRoutes.PostsForUser.Handle("/flagged", api.APISessionRequired(getFlaggedPostsForUser)).Methods("GET") @@ -1055,3 +1056,24 @@ func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) { w.Header().Set(model.HeaderEtagServer, model.GetEtagForFileInfos(infos)) w.Write(js) } + +func getPostInfo(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequirePostId() + if c.Err != nil { + return + } + + info, appErr := c.App.GetPostInfo(c.AppContext, c.Params.PostId) + if appErr != nil { + c.Err = appErr + return + } + + js, err := json.Marshal(info) + if err != nil { + c.Err = model.NewAppError("getPostInfo", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + return + } + + w.Write(js) +} diff --git a/api4/post_test.go b/api4/post_test.go index 3240347e1e..8594fae136 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -3343,6 +3343,262 @@ func TestPostReminder(t *testing.T) { require.Truef(t, caught, "User should have received %s event", model.WebsocketEventEphemeralMessage) } +func TestPostGetInfo(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + client := th.Client + sysadminClient := th.SystemAdminClient + sysadminClient.AddTeamMember(th.BasicTeam.Id, th.SystemAdminUser.Id) + + openChannel, _, err := client.CreateChannel(&model.Channel{TeamId: th.BasicTeam.Id, Type: model.ChannelTypeOpen, Name: "open-channel", DisplayName: "Open Channel"}) + require.NoError(t, err) + sysadminClient.AddChannelMember(openChannel.Id, th.SystemAdminUser.Id) + openPost, _, err := client.CreatePost(&model.Post{ChannelId: openChannel.Id}) + require.NoError(t, err) + + privateChannel, _, err := sysadminClient.CreateChannel(&model.Channel{TeamId: th.BasicTeam.Id, Type: model.ChannelTypePrivate, Name: "private-channel", DisplayName: "Private Channel"}) + require.NoError(t, err) + privatePost, _, err := sysadminClient.CreatePost(&model.Post{ChannelId: privateChannel.Id}) + require.NoError(t, err) + + privateChannelBasicUser, _, err := client.CreateChannel(&model.Channel{TeamId: th.BasicTeam.Id, Type: model.ChannelTypePrivate, Name: "private-channel-basic-user", DisplayName: "Private Channel - Basic User"}) + require.NoError(t, err) + privatePostBasicUser, _, err := client.CreatePost(&model.Post{ChannelId: privateChannelBasicUser.Id}) + require.NoError(t, err) + + user3 := th.CreateUser() + gmChannel, _, err := client.CreateGroupChannel([]string{th.BasicUser.Id, th.BasicUser2.Id, user3.Id}) + require.NoError(t, err) + gmPost, _, err := client.CreatePost(&model.Post{ChannelId: gmChannel.Id}) + require.NoError(t, err) + + dmChannel, _, err := client.CreateDirectChannel(th.BasicUser.Id, th.BasicUser2.Id) + require.NoError(t, err) + dmPost, _, err := client.CreatePost(&model.Post{ChannelId: dmChannel.Id}) + require.NoError(t, err) + + openTeam, _, err := sysadminClient.CreateTeam(&model.Team{Type: model.TeamOpen, Name: "open-team", DisplayName: "Open Team"}) + require.NoError(t, err) + openTeamOpenChannel, _, err := sysadminClient.CreateChannel(&model.Channel{TeamId: openTeam.Id, Type: model.ChannelTypeOpen, Name: "open-team-open-channel", DisplayName: "Open Team - Open Channel"}) + require.NoError(t, err) + openTeamOpenPost, _, err := sysadminClient.CreatePost(&model.Post{ChannelId: openTeamOpenChannel.Id}) + require.NoError(t, err) + + // Alt team is a team without the sysadmin in it. + altOpenTeam, _, err := client.CreateTeam(&model.Team{Type: model.TeamOpen, Name: "alt-open-team", DisplayName: "Alt Open Team"}) + require.NoError(t, err) + altOpenTeamOpenChannel, _, err := client.CreateChannel(&model.Channel{TeamId: altOpenTeam.Id, Type: model.ChannelTypeOpen, Name: "alt-open-team-open-channel", DisplayName: "Open Team - Open Channel"}) + require.NoError(t, err) + altOpenTeamOpenPost, _, err := client.CreatePost(&model.Post{ChannelId: altOpenTeamOpenChannel.Id}) + require.NoError(t, err) + + inviteTeam, _, err := sysadminClient.CreateTeam(&model.Team{Type: model.TeamInvite, Name: "invite-team", DisplayName: "Invite Team"}) + require.NoError(t, err) + inviteTeamOpenChannel, _, err := sysadminClient.CreateChannel(&model.Channel{TeamId: inviteTeam.Id, Type: model.ChannelTypeOpen, Name: "invite-team-open-channel", DisplayName: "Invite Team - Open Channel"}) + require.NoError(t, err) + inviteTeamOpenPost, _, err := sysadminClient.CreatePost(&model.Post{ChannelId: inviteTeamOpenChannel.Id}) + require.NoError(t, err) + + testCases := []struct { + name string + team *model.Team + hasJoinedTeam bool + channel *model.Channel + hasJoinedChannel bool + post *model.Post + client *model.Client4 + hasAccess bool + }{ + // Open channel - Current Team + { + name: "Open post - Current team - Basic user", + team: th.BasicTeam, + hasJoinedTeam: true, + channel: openChannel, + hasJoinedChannel: true, + post: openPost, + client: client, + hasAccess: true, + }, + { + name: "Open post - Current team - Sysadmin user", + team: th.BasicTeam, + hasJoinedTeam: true, + channel: openChannel, + hasJoinedChannel: true, + post: openPost, + client: sysadminClient, + hasAccess: true, + }, + + // Private channel - Current Team + { + name: "Private post by sysadmin - Current team - Basic user", + team: th.BasicTeam, + channel: privateChannel, + post: privatePost, + client: client, + hasAccess: false, + }, + { + name: "Private post by sysadmin - Current team - Sysadmin user", + team: th.BasicTeam, + hasJoinedTeam: true, + channel: privateChannel, + hasJoinedChannel: true, + post: privatePost, + client: sysadminClient, + hasAccess: true, + }, + { + name: "Private post by basic user - Current team - Basic user", + team: th.BasicTeam, + hasJoinedTeam: true, + channel: privateChannelBasicUser, + hasJoinedChannel: true, + post: privatePostBasicUser, + client: client, + hasAccess: true, + }, + { + name: "Private post by basic user - Current team - Sysadmin user", + team: th.BasicTeam, + hasJoinedTeam: true, + channel: privateChannelBasicUser, + hasJoinedChannel: false, + post: privatePostBasicUser, + client: sysadminClient, + hasAccess: true, + }, + + // GM channel + { + name: "GM post - Current team - Basic user", + team: nil, + channel: gmChannel, + hasJoinedChannel: true, + post: gmPost, + client: client, + hasAccess: true, + }, + { + name: "GM post - Current team - Sysadmin user", + team: nil, + channel: gmChannel, + post: gmPost, + client: sysadminClient, + hasAccess: false, + }, + + // DM channel + { + name: "DM post - Current team - Basic user", + team: nil, + channel: dmChannel, + hasJoinedChannel: true, + post: dmPost, + client: client, + hasAccess: true, + }, + { + name: "DM post - Current team - Sysadmin user", + team: nil, + channel: dmChannel, + post: dmPost, + client: sysadminClient, + hasAccess: false, + }, + + // Open channel - Open Team + { + name: "Open post - Open team - Basic user", + team: openTeam, + hasJoinedTeam: false, + channel: openTeamOpenChannel, + hasJoinedChannel: false, + post: openTeamOpenPost, + client: client, + hasAccess: true, + }, + { + name: "Open post - Open team - Sysadmin user", + team: openTeam, + hasJoinedTeam: true, + channel: openTeamOpenChannel, + hasJoinedChannel: true, + post: openTeamOpenPost, + client: sysadminClient, + hasAccess: true, + }, + + // Open channel - Alt Open Team + { + name: "Open post - Alt open team - Basic user", + team: altOpenTeam, + hasJoinedTeam: true, + channel: altOpenTeamOpenChannel, + hasJoinedChannel: true, + post: altOpenTeamOpenPost, + client: client, + hasAccess: true, + }, + { + name: "Open post - Alt open team - Sysadmin user", + team: altOpenTeam, + hasJoinedTeam: false, + channel: altOpenTeamOpenChannel, + hasJoinedChannel: false, + post: altOpenTeamOpenPost, + client: sysadminClient, + hasAccess: true, + }, + + // Open channel - Invite Team + { + name: "Open post - Invite team - Basic user", + team: inviteTeam, + channel: inviteTeamOpenChannel, + post: inviteTeamOpenPost, + client: client, + hasAccess: false, + }, + { + name: "Open post - Invite team - Sysadmin user", + team: inviteTeam, + hasJoinedTeam: true, + channel: inviteTeamOpenChannel, + hasJoinedChannel: true, + post: inviteTeamOpenPost, + client: sysadminClient, + hasAccess: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + info, resp, err := tc.client.GetPostInfo(tc.post.Id) + if !tc.hasAccess { + require.Error(t, err) + CheckNotFoundStatus(t, resp) + return + } + + require.NoError(t, err) + CheckOKStatus(t, resp) + require.Equal(t, tc.channel.Id, info.ChannelId) + require.Equal(t, tc.channel.Type, info.ChannelType) + require.Equal(t, tc.channel.DisplayName, info.ChannelDisplayName) + require.Equal(t, tc.hasJoinedChannel, info.HasJoinedChannel) + if tc.team != nil { + require.Equal(t, tc.team.Id, info.TeamId) + require.Equal(t, tc.team.Type, info.TeamType) + require.Equal(t, tc.team.DisplayName, info.TeamDisplayName) + require.Equal(t, tc.hasJoinedTeam, info.HasJoinedTeam) + } + }) + } +} + func TestAcknowledgePost(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/app/app_iface.go b/app/app_iface.go index aca9f1e795..948060d0f6 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -712,6 +712,7 @@ type AppIface interface { GetPostIdAfterTime(channelID string, time int64, collapsedThreads bool) (string, *model.AppError) GetPostIdBeforeTime(channelID string, time int64, collapsedThreads bool) (string, *model.AppError) GetPostIfAuthorized(c request.CTX, postID string, session *model.Session, includeDeleted bool) (*model.Post, *model.AppError) + GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model.AppError) GetPostThread(postID string, opts model.GetPostsOptions, userID string) (*model.PostList, *model.AppError) GetPosts(channelID string, offset int, limit int) (*model.PostList, *model.AppError) GetPostsAfterPost(options model.GetPostsOptions) (*model.PostList, *model.AppError) diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 35664d638c..498451174b 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -7952,6 +7952,28 @@ func (a *OpenTracingAppLayer) GetPostIfAuthorized(c request.CTX, postID string, return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostInfo") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetPostInfo(c, postID) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetPostThread(postID string, opts model.GetPostsOptions, userID string) (*model.PostList, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostThread") diff --git a/app/post.go b/app/post.go index b9f1bf0ed3..9fafd61f9b 100644 --- a/app/post.go +++ b/app/post.go @@ -2151,6 +2151,74 @@ func (a *App) CheckPostReminders() { } +func (a *App) GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model.AppError) { + userID := c.Session().UserId + post, appErr := a.GetSinglePost(postID, false) + if appErr != nil { + return nil, appErr + } + + channel, appErr := a.GetChannel(c, post.ChannelId) + if appErr != nil { + return nil, appErr + } + + notFoundError := model.NewAppError("GetPostInfo", "app.post.get.app_error", nil, "", http.StatusNotFound) + + var team *model.Team + hasPermissionToAccessTeam := false + if channel.TeamId != "" { + team, appErr = a.GetTeam(channel.TeamId) + if appErr != nil { + return nil, appErr + } + + if team.Type == model.TeamOpen { + hasPermissionToAccessTeam = a.HasPermissionToTeam(userID, team.Id, model.PermissionJoinPublicTeams) + } else if team.Type == model.TeamInvite { + hasPermissionToAccessTeam = a.HasPermissionToTeam(userID, team.Id, model.PermissionJoinPrivateTeams) + } + } else { + // This happens in case of DMs and GMs. + hasPermissionToAccessTeam = true + } + + if !hasPermissionToAccessTeam { + return nil, notFoundError + } + + hasPermissionToAccessChannel := false + if channel.Type == model.ChannelTypeOpen { + hasPermissionToAccessChannel = true + } else if channel.Type == model.ChannelTypePrivate { + hasPermissionToAccessChannel = a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionManagePrivateChannelMembers) + } else if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup { + hasPermissionToAccessChannel = a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionReadChannel) + } + + if !hasPermissionToAccessChannel { + return nil, notFoundError + } + + _, channelMemberErr := a.GetChannelMember(c, channel.Id, userID) + + info := model.PostInfo{ + ChannelId: channel.Id, + ChannelType: channel.Type, + ChannelDisplayName: channel.DisplayName, + HasJoinedChannel: channelMemberErr == nil, + } + if team != nil { + _, teamMemberErr := a.GetTeamMember(team.Id, userID) + + info.TeamId = team.Id + info.TeamType = team.Type + info.TeamDisplayName = team.DisplayName + info.HasJoinedTeam = teamMemberErr == nil + } + return &info, nil +} + func includeEmbedsAndImages(a *App, c request.CTX, topThreadList *model.TopThreadList, userID string) (*model.TopThreadList, error) { for _, topThread := range topThreadList.Items { topThread.Post = a.PreparePostForClientWithEmbedsAndImages(c, topThread.Post, false, false, true) diff --git a/model/client4.go b/model/client4.go index fded08a23d..5347048f17 100644 --- a/model/client4.go +++ b/model/client4.go @@ -8540,6 +8540,20 @@ func (c *Client4) GetNewTeamMembersSince(teamID string, timeRange string, page i return newTeamMembersList, BuildResponse(r), nil } +func (c *Client4) GetPostInfo(postId string) (*PostInfo, *Response, error) { + r, err := c.DoAPIGet(c.postRoute(postId)+"/info", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var info *PostInfo + if err = json.NewDecoder(r.Body).Decode(&info); err != nil { + return nil, nil, NewAppError("GetPostInfo", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + return info, BuildResponse(r), nil +} + func (c *Client4) AcknowledgePost(postId, userId string) (*PostAcknowledgement, *Response, error) { r, err := c.DoAPIPost(c.userRoute(userId)+c.postRoute(postId)+"/ack", "") if err != nil { diff --git a/model/post_info.go b/model/post_info.go new file mode 100644 index 0000000000..0a48ae9a36 --- /dev/null +++ b/model/post_info.go @@ -0,0 +1,15 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type PostInfo struct { + ChannelId string `json:"channel_id"` + ChannelType ChannelType `json:"channel_type"` + ChannelDisplayName string `json:"channel_display_name"` + HasJoinedChannel bool `json:"has_joined_channel"` + TeamId string `json:"team_id"` + TeamType string `json:"team_type"` + TeamDisplayName string `json:"team_display_name"` + HasJoinedTeam bool `json:"has_joined_team"` +}