* create ChannelBookmarks table

* ChannelBookmark model

* channel bookamrks Store layer

* add GetBookmarksForAllChannelByIdSince

* add channel bookmarks to test store

* Add channel bookmarks to app layer

* remove index for createAt in channel bookmarks migrations

* remove createAt from select channel bookmark query and enable store delete bookmark test

* update reponse of UpdateBookmark

* rename db migration files

* channel bookmarks store update sort order

* channel bookmarks app layer update sort order

* fix lint & tests

* Fix lint and introduce util functions to insert / remove from slice

* remove model etag

* i18n

* defer remove file info after test run

* Fix tests passing the request context

* fix migrations

* fix TestRetry

* Add bookmark permissions (#25560)

* Adds channel bookmarks permissions

* Fix linter

* Remove unnecessary empty lines

* Remove scss change as it's not necessary anymore

* Fix mock store

* Fix mock store and add role entry

* Fix test

* Adds cypress test and update permissions migration to update admin roles

* Adds channel bookmarks roles to default admin roles

* Adds bookmark permissions to default role permissions constant in webapp

* Update mmctl test

* Update permission test after normalising the roles

* fix store tests

* fix app layer tests

* Add new bookmark endpoint (#25624)

* Adds channel bookmarks api scaffold and create endpoint

* Applies review comments to the API docs

* Adds websocket test to create channel bookmark

---------

Co-authored-by: Mattermost Build <build@mattermost.com>

* MM-54426 exclude Channel Bookmarks files from data retention (#25656)

* Augment channel APIs to include bookmarks (#25567)

* update files docs for server 9.4

* Adds update channel bookmark endpoint (#25653)

* Adds update channel bookmark sort order endpoint (#25686)

* Adds update channel bookmark endpoint

* Updates edit app method to return the right deleted bookmark and adds tests

* Adds the update channel bookmark sort order endpoint

* Fix repeated test after merge

* Assign right permissions to each test

* Update store and app layer to return specific errors and add tests

* Adds delete channel bookmark endpoint (#25693)

* Updates edit app method to return the right deleted bookmark and adds tests

* Fix repeated test after merge

* Updates edit app method to return the right deleted bookmark and adds tests

* Adds delete channel bookmark endpoint

* Adds list channel bookmarks endpoint (#25700)

* Add channel moderation to bookmarks (#25716)

* fix migrations index

* fix getChannelsForTeamForUser

* fix getChannelsForTeamForUser

* fix bad merge client4

* fix file api with bookmark permission

* add ChannelBookmarks feature flag

* add missing translations

* Set DB column for type as enum

* use custom type for bookmark query using sqlx

* use transaction when saving bookmark

* return NewErrNotFound instead of Sql.ErrNoRows

* use squirrel for IN query

* add a limit of 1K for records in GetBookmarksForAllChannelByIdSince

* UpdateSortOrder with one single query instead of multiple updates

* fix shadow declaration

* fix channel bookmarks permission string definition in admin console

* fix another shadow declaration

* Fix model conversion

* add SplitSliceInChunks

* remove include bookmarks in channels api

* Cap amount of bookmarks per channel

* add etag back to get channels

* feedback review

* update file info when replacing a bookmark file

* return 501 not implemented when the license is not available

* add detail message when getting channel member on bookmark api

* start audit before permission check on create bookmark api

* use require.Eventuallyf for testing WS events

* remove unnecessary log in app layer

* use require instead of assert to avoid panics

* enforce limit when querying bookmarks since

* prevent to create/update bookmark if file is already attached

* fix lint

* delete file when a bookmark is deleted

* Dot allow to set a fileId and a url at the same time to a bookmark

* fix query to delete a file that belongs to a bookmark

* do not patch the bookmark type

* Server side FeatureFlag check (#26145)

* use ff in server, set ff to false

* turn on FF for unit tests

* defer unset FF for unit tests

* turn ff on for testing

* only allow attaching files that were uploaded for bookmark

* Set feature flag off as default

* fix lint

* update email templates as PR failed

* revert templates

* force the assignment of ID when creating a bookmark

* Fix unit tests

---------

Co-authored-by: Miguel de la Cruz <miguel@mcrx.me>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Caleb Roseland <caleb@calebroseland.com>
Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
Этот коммит содержится в:
Elias Nahum
2024-03-12 22:36:05 +08:00
коммит произвёл GitHub
родитель 2480a6c646
Коммит 7e9cd04a8b
66 изменённых файлов: 6578 добавлений и 57 удалений

Просмотреть файл

@@ -49,6 +49,8 @@ type Routes struct {
ChannelMembersForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/members'
ChannelModerations *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/moderations'
ChannelCategories *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/categories'
ChannelBookmarks *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/bookmarks'
ChannelBookmark *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/bookmarks/{bookmark_id:[A-Za-z0-9]+}'
Posts *mux.Router // 'api/v4/posts'
Post *mux.Router // 'api/v4/posts/{post_id:[A-Za-z0-9]+}'
@@ -193,6 +195,8 @@ func Init(srv *app.Server) (*API, error) {
api.BaseRoutes.ChannelMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/members").Subrouter()
api.BaseRoutes.ChannelModerations = api.BaseRoutes.Channel.PathPrefix("/moderations").Subrouter()
api.BaseRoutes.ChannelCategories = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/categories").Subrouter()
api.BaseRoutes.ChannelBookmarks = api.BaseRoutes.Channel.PathPrefix("/bookmarks").Subrouter()
api.BaseRoutes.ChannelBookmark = api.BaseRoutes.ChannelBookmarks.PathPrefix("/{bookmark_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Posts = api.BaseRoutes.APIRoot.PathPrefix("/posts").Subrouter()
api.BaseRoutes.Post = api.BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
@@ -323,6 +327,7 @@ func Init(srv *app.Server) (*API, error) {
api.InitHostedCustomer()
api.InitDrafts()
api.InitIPFiltering()
api.InitChannelBookmarks()
api.InitReports()
api.InitLimits()
api.InitOutgoingOAuthConnection()

Просмотреть файл

@@ -623,6 +623,41 @@ func (th *TestHelper) CreateUserWithAuth(authService string) *model.User {
return user
}
// CreateGuestAndClient creates a guest user, adds them to the basic
// team, basic channel and basic private channel, and generates an API
// client ready to use
func (th *TestHelper) CreateGuestAndClient() (*model.User, *model.Client4) {
id := model.NewId()
// create a guest user and add it to the basic team and public/private channels
guest, cgErr := th.App.CreateGuest(th.Context, &model.User{
Email: "test_guest" + id + "@sample.com",
Username: "guest_" + id,
Nickname: "guest_" + id,
Password: "Password1",
EmailVerified: true,
})
if cgErr != nil {
panic(cgErr)
}
_, _, tErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, guest.Id, th.SystemAdminUser.Id)
if tErr != nil {
panic(tErr)
}
th.AddUserToChannel(guest, th.BasicChannel)
th.AddUserToChannel(guest, th.BasicPrivateChannel)
// create a client and login the guest
guestClient := th.CreateClient()
_, _, lErr := guestClient.Login(context.Background(), guest.Username, "Password1")
if lErr != nil {
panic(lErr)
}
return guest, guestClient
}
func (th *TestHelper) SetupLdapConfig() {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableMultifactorAuthentication = true
@@ -809,6 +844,23 @@ func (th *TestHelper) CreateDmChannel(user *model.User) *model.Channel {
return channel
}
func (th *TestHelper) PatchChannelModerationsForMembers(channelId, name string, val bool) {
patch := []*model.ChannelModerationPatch{{
Name: &name,
Roles: &model.ChannelModeratedRolesPatch{Members: model.NewBool(val)},
}}
channel, err := th.App.GetChannel(th.Context, channelId)
if err != nil {
panic(err)
}
_, err = th.App.PatchChannelModerationsForChannel(th.Context, channel, patch)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginBasic() {
th.LoginBasicWithClient(th.Client)
}

408
server/channels/api4/channel_bookmark.go Обычный файл
Просмотреть файл

@@ -0,0 +1,408 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/audit"
)
func (api *API) InitChannelBookmarks() {
if api.srv.Config().FeatureFlags.ChannelBookmarks {
api.BaseRoutes.ChannelBookmarks.Handle("", api.APISessionRequired(createChannelBookmark)).Methods("POST")
api.BaseRoutes.ChannelBookmark.Handle("", api.APISessionRequired(updateChannelBookmark)).Methods("PATCH")
api.BaseRoutes.ChannelBookmark.Handle("/sort_order", api.APISessionRequired(updateChannelBookmarkSortOrder)).Methods("POST")
api.BaseRoutes.ChannelBookmark.Handle("", api.APISessionRequired(deleteChannelBookmark)).Methods("DELETE")
api.BaseRoutes.ChannelBookmarks.Handle("", api.APISessionRequired(listChannelBookmarksForChannel)).Methods("GET")
}
}
func createChannelBookmark(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("createChannelBookmark", "api.channel.bookmark.channel_bookmark.license.error", nil, "", http.StatusNotImplemented)
return
}
connectionID := r.Header.Get(model.ConnectionId)
c.RequireChannelId()
if c.Err != nil {
return
}
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
var channelBookmark *model.ChannelBookmark
err := json.NewDecoder(r.Body).Decode(&channelBookmark)
if err != nil || channelBookmark == nil {
c.SetInvalidParamWithErr("channelBookmark", err)
return
}
channelBookmark.ChannelId = c.Params.ChannelId
auditRec := c.MakeAuditRecord("createChannelBookmark", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "channelBookmark", channelBookmark)
switch channel.Type {
case model.ChannelTypeOpen:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionAddBookmarkPublicChannel) {
c.SetPermissionError(model.PermissionAddBookmarkPublicChannel)
return
}
case model.ChannelTypePrivate:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionAddBookmarkPrivateChannel) {
c.SetPermissionError(model.PermissionAddBookmarkPrivateChannel)
return
}
case model.ChannelTypeGroup, model.ChannelTypeDirect:
// Any member of DM/GMs but guests can manage channel bookmarks
if _, errGet := c.App.GetChannelMember(c.AppContext, channel.Id, c.AppContext.Session().UserId); errGet != nil {
c.Err = model.NewAppError("createChannelBookmark", "api.channel.bookmark.create_channel_bookmark.direct_or_group_channels.forbidden.app_error", nil, errGet.Message, http.StatusForbidden)
return
}
user, gAppErr := c.App.GetUser(c.AppContext.Session().UserId)
if gAppErr != nil {
c.Err = gAppErr
return
}
if user.IsGuest() {
c.Err = model.NewAppError("createChannelBookmark", "api.channel.bookmark.create_channel_bookmark.direct_or_group_channels_by_guests.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
default:
c.Err = model.NewAppError("createChannelBookmark", "api.channel.bookmark.create_channel_bookmark.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
newChannelBookmark, appErr := c.App.CreateChannelBookmark(c.AppContext, channelBookmark, connectionID)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(newChannelBookmark)
auditRec.AddEventObjectType("channelBookmarkWithFileInfo")
c.LogAudit("display_name=" + newChannelBookmark.DisplayName)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(newChannelBookmark); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateChannelBookmark(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("updateChannelBookmark", "api.channel.bookmark.channel_bookmark.license.error", nil, "", http.StatusNotImplemented)
return
}
connectionID := r.Header.Get(model.ConnectionId)
c.RequireChannelId()
if c.Err != nil {
return
}
var patch *model.ChannelBookmarkPatch
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil || patch == nil {
c.SetInvalidParamWithErr("channelBookmarkPatch", err)
return
}
originalChannelBookmark, appErr := c.App.GetBookmark(c.Params.ChannelBookmarkId, false)
if appErr != nil {
c.Err = appErr
return
}
patchedBookmark := originalChannelBookmark.Clone()
auditRec := c.MakeAuditRecord("updateChannelBookmark", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "channelBookmark", patch)
// The channel bookmark should belong to the same channel specified in the URL
if patchedBookmark.ChannelId != c.Params.ChannelId {
c.SetInvalidParam("channel_id")
return
}
auditRec.AddEventPriorState(originalChannelBookmark)
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
switch channel.Type {
case model.ChannelTypeOpen:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionEditBookmarkPublicChannel) {
c.SetPermissionError(model.PermissionEditBookmarkPublicChannel)
return
}
case model.ChannelTypePrivate:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionEditBookmarkPrivateChannel) {
c.SetPermissionError(model.PermissionEditBookmarkPrivateChannel)
return
}
case model.ChannelTypeGroup, model.ChannelTypeDirect:
// Any member of DM/GMs but guests can manage channel bookmarks
if _, errGet := c.App.GetChannelMember(c.AppContext, channel.Id, c.AppContext.Session().UserId); errGet != nil {
c.Err = model.NewAppError("updateChannelBookmark", "api.channel.bookmark.update_channel_bookmark.direct_or_group_channels.forbidden.app_error", nil, errGet.Message, http.StatusForbidden)
return
}
user, gAppErr := c.App.GetUser(c.AppContext.Session().UserId)
if gAppErr != nil {
c.Err = gAppErr
return
}
if user.IsGuest() {
c.Err = model.NewAppError("updateChannelBookmark", "api.channel.bookmark.update_channel_bookmark.direct_or_group_channels_by_guests.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
default:
c.Err = model.NewAppError("updateChannelBookmark", "api.channel.bookmark.update_channel_bookmark.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
patchedBookmark.Patch(patch)
updateChannelBookmarkResponse, appErr := c.App.UpdateChannelBookmark(c.AppContext, patchedBookmark, connectionID)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(updateChannelBookmarkResponse)
auditRec.AddEventObjectType("updateChannelBookmarkResponse")
c.LogAudit("")
if err := json.NewEncoder(w).Encode(updateChannelBookmarkResponse); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateChannelBookmarkSortOrder(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("updateChannelBookmarkSortOrder", "api.channel.bookmark.channel_bookmark.license.error", nil, "", http.StatusNotImplemented)
return
}
connectionID := r.Header.Get(model.ConnectionId)
c.RequireChannelId()
if c.Err != nil {
return
}
var newSortOrder int64
if err := json.NewDecoder(r.Body).Decode(&newSortOrder); err != nil {
c.SetInvalidParamWithErr("channelBookmarkSortOrder", err)
return
}
if newSortOrder < 0 {
c.SetInvalidParam("channelBookmarkSortOrder")
return
}
auditRec := c.MakeAuditRecord("updateChannelBookmarkSortOrder", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", c.Params.ChannelBookmarkId)
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
switch channel.Type {
case model.ChannelTypeOpen:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionOrderBookmarkPublicChannel) {
c.SetPermissionError(model.PermissionOrderBookmarkPublicChannel)
return
}
case model.ChannelTypePrivate:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionOrderBookmarkPrivateChannel) {
c.SetPermissionError(model.PermissionOrderBookmarkPrivateChannel)
return
}
case model.ChannelTypeGroup, model.ChannelTypeDirect:
// Any member of DM/GMs but guests can manage channel bookmarks
if _, errGet := c.App.GetChannelMember(c.AppContext, channel.Id, c.AppContext.Session().UserId); errGet != nil {
c.Err = model.NewAppError("updateChannelBookmarkSortOrder", "api.channel.bookmark.update_channel_bookmark_sort_order.direct_or_group_channels.forbidden.app_error", nil, errGet.Message, http.StatusForbidden)
return
}
user, gAppErr := c.App.GetUser(c.AppContext.Session().UserId)
if gAppErr != nil {
c.Err = gAppErr
return
}
if user.IsGuest() {
c.Err = model.NewAppError("updateChannelBookmarkSortOrder", "api.channel.bookmark.update_channel_bookmark_sort_order.direct_or_group_channels_by_guests.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
default:
c.Err = model.NewAppError("updateChannelBookmarkSortOrder", "api.channel.bookmark.update_channel_bookmark_sort_order.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
bookmarks, appErr := c.App.UpdateChannelBookmarkSortOrder(c.Params.ChannelBookmarkId, c.Params.ChannelId, newSortOrder, connectionID)
if appErr != nil {
c.Err = appErr
return
}
for _, b := range bookmarks {
if b.Id == c.Params.ChannelBookmarkId {
auditRec.AddEventResultState(b)
auditRec.AddEventObjectType("channelBookmarkWithFileInfo")
break
}
}
auditRec.Success()
c.LogAudit("")
if err := json.NewEncoder(w).Encode(bookmarks); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteChannelBookmark(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("deleteChannelBookmark", "api.channel.bookmark.channel_bookmark.license.error", nil, "", http.StatusNotImplemented)
return
}
connectionID := r.Header.Get(model.ConnectionId)
c.RequireChannelId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deleteChannelBookmark", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", c.Params.ChannelBookmarkId)
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
switch channel.Type {
case model.ChannelTypeOpen:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionDeleteBookmarkPublicChannel) {
c.SetPermissionError(model.PermissionDeleteBookmarkPublicChannel)
return
}
case model.ChannelTypePrivate:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionDeleteBookmarkPrivateChannel) {
c.SetPermissionError(model.PermissionDeleteBookmarkPrivateChannel)
return
}
case model.ChannelTypeGroup, model.ChannelTypeDirect:
// Any member of DM/GMs but guests can manage channel bookmarks
if _, errGet := c.App.GetChannelMember(c.AppContext, channel.Id, c.AppContext.Session().UserId); errGet != nil {
c.Err = model.NewAppError("deleteChannelBookmark", "api.channel.bookmark.delete_channel_bookmark.direct_or_group_channels.forbidden.app_error", nil, errGet.Message, http.StatusForbidden)
return
}
user, gAppErr := c.App.GetUser(c.AppContext.Session().UserId)
if gAppErr != nil {
c.Err = gAppErr
return
}
if user.IsGuest() {
c.Err = model.NewAppError("deleteChannelBookmark", "api.channel.bookmark.delete_channel_bookmark.direct_or_group_channels_by_guests.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
default:
c.Err = model.NewAppError("deleteChannelBookmark", "api.channel.bookmark.delete_channel_bookmark.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
oldBookmark, obErr := c.App.GetBookmark(c.Params.ChannelBookmarkId, false)
if obErr != nil {
c.Err = obErr
return
}
// The channel bookmark should belong to the same channel specified in the URL
if oldBookmark.ChannelId != c.Params.ChannelId {
c.SetInvalidParam("channel_id")
return
}
auditRec.AddEventPriorState(oldBookmark)
bookmark, appErr := c.App.DeleteChannelBookmark(c.Params.ChannelBookmarkId, connectionID)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(bookmark)
c.LogAudit("bookmark=" + bookmark.DisplayName)
if err := json.NewEncoder(w).Encode(bookmark); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func listChannelBookmarksForChannel(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("listChannelBookmarksForChannel", "api.channel.bookmark.channel_bookmark.license.error", nil, "", http.StatusNotImplemented)
return
}
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannelContent) {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
bookmarks, appErr := c.App.GetChannelBookmarks(c.Params.ChannelId, c.Params.BookmarksSince)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(bookmarks); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}

1551
server/channels/api4/channel_bookmark_test.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -4230,9 +4230,9 @@ func TestGetChannelModerations(t *testing.T) {
t.Run("Returns default moderations with default roles", func(t *testing.T) {
moderations, _, err := th.SystemAdminClient.GetChannelModerations(context.Background(), channel.Id, "")
require.NoError(t, err)
require.Equal(t, len(moderations), 4)
require.Equal(t, len(moderations), 5)
for _, moderation := range moderations {
if moderation.Name == "manage_members" {
if moderation.Name == "manage_members" || moderation.Name == "manage_bookmarks" {
require.Empty(t, moderation.Roles.Guests)
} else {
require.Equal(t, moderation.Roles.Guests.Value, true)
@@ -4320,7 +4320,7 @@ func TestGetChannelModerations(t *testing.T) {
require.Nil(t, appErr)
th.RemovePermissionFromRole(model.PermissionManagePublicChannelMembers.Id, scheme.DefaultChannelUserRole)
defer th.AddPermissionToRole(model.PermissionCreatePost.Id, scheme.DefaultChannelUserRole)
defer th.AddPermissionToRole(model.PermissionManagePublicChannelMembers.Id, scheme.DefaultChannelUserRole)
// public channel does not have the permission
moderations, _, err := th.SystemAdminClient.GetChannelModerations(context.Background(), channel.Id, "")
@@ -4341,6 +4341,48 @@ func TestGetChannelModerations(t *testing.T) {
}
})
t.Run("Returns the correct value for manage_bookmarks depending on whether the channel is public or private", func(t *testing.T) {
scheme := th.SetupTeamScheme()
team.SchemeId = &scheme.Id
_, appErr := th.App.UpdateTeamScheme(team)
require.Nil(t, appErr)
bookmarkPublicPermissions := []string{
model.PermissionAddBookmarkPublicChannel.Id,
model.PermissionEditBookmarkPublicChannel.Id,
model.PermissionDeleteBookmarkPublicChannel.Id,
model.PermissionOrderBookmarkPublicChannel.Id,
}
for _, p := range bookmarkPublicPermissions {
th.RemovePermissionFromRole(p, scheme.DefaultChannelUserRole)
}
defer func() {
for _, p := range bookmarkPublicPermissions {
th.AddPermissionToRole(p, scheme.DefaultChannelUserRole)
}
}()
// public channel does not have the permissions
moderations, _, err := th.SystemAdminClient.GetChannelModerations(context.Background(), channel.Id, "")
require.NoError(t, err)
for _, moderation := range moderations {
if moderation.Name == "manage_bookmarks" {
require.Equal(t, moderation.Roles.Members.Value, false)
}
}
// private channel does have the permissions
moderations, _, err = th.SystemAdminClient.GetChannelModerations(context.Background(), th.BasicPrivateChannel.Id, "")
require.NoError(t, err)
for _, moderation := range moderations {
if moderation.Name == "manage_bookmarks" {
require.Equal(t, moderation.Roles.Members.Value, true)
}
}
})
t.Run("Does not return an error if the team scheme has a blank DefaultChannelGuestRole field", func(t *testing.T) {
scheme := th.SetupTeamScheme()
scheme.DefaultChannelGuestRole = ""
@@ -4405,9 +4447,9 @@ func TestPatchChannelModerations(t *testing.T) {
t.Run("Returns default moderations with empty patch", func(t *testing.T) {
moderations, _, err := th.SystemAdminClient.PatchChannelModerations(context.Background(), channel.Id, emptyPatch)
require.NoError(t, err)
require.Equal(t, len(moderations), 4)
require.Equal(t, len(moderations), 5)
for _, moderation := range moderations {
if moderation.Name == "manage_members" {
if moderation.Name == "manage_members" || moderation.Name == "manage_bookmarks" {
require.Empty(t, moderation.Roles.Guests)
} else {
require.Equal(t, moderation.Roles.Guests.Value, true)
@@ -4431,9 +4473,9 @@ func TestPatchChannelModerations(t *testing.T) {
moderations, _, err := th.SystemAdminClient.PatchChannelModerations(context.Background(), channel.Id, patch)
require.NoError(t, err)
require.Equal(t, len(moderations), 4)
require.Equal(t, len(moderations), 5)
for _, moderation := range moderations {
if moderation.Name == "manage_members" {
if moderation.Name == "manage_members" || moderation.Name == "manage_bookmarks" {
require.Empty(t, moderation.Roles.Guests)
} else {
require.Equal(t, moderation.Roles.Guests.Value, true)
@@ -4468,9 +4510,9 @@ func TestPatchChannelModerations(t *testing.T) {
moderations, _, err := th.SystemAdminClient.PatchChannelModerations(context.Background(), channel.Id, patch)
require.NoError(t, err)
require.Equal(t, len(moderations), 4)
require.Equal(t, len(moderations), 5)
for _, moderation := range moderations {
if moderation.Name == "manage_members" {
if moderation.Name == "manage_members" || moderation.Name == "manage_bookmarks" {
require.Empty(t, moderation.Roles.Guests)
} else {
require.Equal(t, moderation.Roles.Guests.Value, true)
@@ -4523,9 +4565,9 @@ func TestPatchChannelModerations(t *testing.T) {
moderations, _, err := th.SystemAdminClient.PatchChannelModerations(context.Background(), channel.Id, emptyPatch)
require.NoError(t, err)
require.Equal(t, len(moderations), 4)
require.Equal(t, len(moderations), 5)
for _, moderation := range moderations {
if moderation.Name == "manage_members" {
if moderation.Name == "manage_members" || moderation.Name == "manage_bookmarks" {
require.Empty(t, moderation.Roles.Guests)
} else {
require.Equal(t, moderation.Roles.Guests.Value, false)
@@ -4545,9 +4587,9 @@ func TestPatchChannelModerations(t *testing.T) {
moderations, _, err = th.SystemAdminClient.PatchChannelModerations(context.Background(), channel.Id, patch)
require.NoError(t, err)
require.Equal(t, len(moderations), 4)
require.Equal(t, len(moderations), 5)
for _, moderation := range moderations {
if moderation.Name == "manage_members" {
if moderation.Name == "manage_members" || moderation.Name == "manage_bookmarks" {
require.Empty(t, moderation.Roles.Guests)
} else {
require.Equal(t, moderation.Roles.Guests.Value, false)

Просмотреть файл

@@ -150,9 +150,15 @@ func uploadFileSimple(c *Context, r *http.Request, timestamp time.Time) *model.F
clientId := r.Form.Get("client_id")
audit.AddEventParameter(auditRec, "client_id", clientId)
creatorId := c.AppContext.Session().UserId
if isBookmark, err := strconv.ParseBool(r.URL.Query().Get(model.BookmarkFileOwner)); err == nil && isBookmark {
creatorId = model.BookmarkFileOwner
audit.AddEventParameter(auditRec, model.BookmarkFileOwner, true)
}
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, c.Params.Filename, r.Body,
app.UploadFileSetTeamId(FileTeamId),
app.UploadFileSetUserId(c.AppContext.Session().UserId),
app.UploadFileSetUserId(creatorId),
app.UploadFileSetTimestamp(timestamp),
app.UploadFileSetContentLength(r.ContentLength),
app.UploadFileSetClientId(clientId))
@@ -267,6 +273,11 @@ NextPart:
continue NextPart
}
isBookmark := false
if val, queryErr := strconv.ParseBool(r.URL.Query().Get(model.BookmarkFileOwner)); queryErr == nil {
isBookmark = val
}
// A file part.
if c.Params.ChannelId == "" && asStream == nil {
@@ -279,7 +290,7 @@ NextPart:
return nil
}
return uploadFileMultipartLegacy(c, mr, timestamp)
return uploadFileMultipartLegacy(c, mr, timestamp, isBookmark)
}
c.RequireChannelId()
@@ -312,9 +323,15 @@ NextPart:
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameter(auditRec, "client_id", clientId)
creatorId := c.AppContext.Session().UserId
if isBookmark {
creatorId = model.BookmarkFileOwner
audit.AddEventParameter(auditRec, model.BookmarkFileOwner, true)
}
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, filename, part,
app.UploadFileSetTeamId(FileTeamId),
app.UploadFileSetUserId(c.AppContext.Session().UserId),
app.UploadFileSetUserId(creatorId),
app.UploadFileSetTimestamp(timestamp),
app.UploadFileSetContentLength(-1),
app.UploadFileSetClientId(clientId))
@@ -353,7 +370,7 @@ NextPart:
// borrowing from http.ParseMultipartForm. If successful it returns a
// *model.FileUploadResponse filled in with the individual model.FileInfo's.
func uploadFileMultipartLegacy(c *Context, mr *multipart.Reader,
timestamp time.Time) *model.FileUploadResponse {
timestamp time.Time, isBookmark bool) *model.FileUploadResponse {
// Parse the entire form.
form, err := mr.ReadForm(*c.App.Config().FileSettings.MaxFileSize)
if err != nil {
@@ -414,9 +431,15 @@ func uploadFileMultipartLegacy(c *Context, mr *multipart.Reader,
audit.AddEventParameter(auditRec, "channel_id", channelId)
audit.AddEventParameter(auditRec, "client_id", clientId)
creatorId := c.AppContext.Session().UserId
if isBookmark {
creatorId = model.BookmarkFileOwner
audit.AddEventParameter(auditRec, model.BookmarkFileOwner, true)
}
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, fileHeader.Filename, f,
app.UploadFileSetTeamId(FileTeamId),
app.UploadFileSetUserId(c.AppContext.Session().UserId),
app.UploadFileSetUserId(creatorId),
app.UploadFileSetTimestamp(timestamp),
app.UploadFileSetContentLength(-1),
app.UploadFileSetClientId(clientId))
@@ -461,8 +484,12 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
audit.AddEventParameterAuditable(auditRec, "file", info)
perm := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), info.ChannelId, model.PermissionReadChannelContent)
if info.CreatorId != c.AppContext.Session().UserId && !perm {
if info.CreatorId == model.BookmarkFileOwner {
if !perm {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
} else if info.CreatorId != c.AppContext.Session().UserId && !perm {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
@@ -495,7 +522,12 @@ func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
}
perm := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), info.ChannelId, model.PermissionReadChannelContent)
if info.CreatorId != c.AppContext.Session().UserId && !perm {
if info.CreatorId == model.BookmarkFileOwner {
if !perm {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
} else if info.CreatorId != c.AppContext.Session().UserId && !perm {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
@@ -539,12 +571,17 @@ func getFileLink(c *Context, w http.ResponseWriter, r *http.Request) {
audit.AddEventParameterAuditable(auditRec, "file", info)
perm := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), info.ChannelId, model.PermissionReadChannelContent)
if info.CreatorId != c.AppContext.Session().UserId && !perm {
if info.CreatorId == model.BookmarkFileOwner {
if !perm {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
} else if info.CreatorId != c.AppContext.Session().UserId && !perm {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
if info.PostId == "" {
if info.PostId == "" && info.CreatorId != model.BookmarkFileOwner {
c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.no_post.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
return
}
@@ -573,7 +610,12 @@ func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) {
}
perm := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), info.ChannelId, model.PermissionReadChannelContent)
if info.CreatorId != c.AppContext.Session().UserId && !perm {
if info.CreatorId == model.BookmarkFileOwner {
if !perm {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
} else if info.CreatorId != c.AppContext.Session().UserId && !perm {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
@@ -608,8 +650,12 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
}
perm := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), info.ChannelId, model.PermissionReadChannelContent)
if info.CreatorId != c.AppContext.Session().UserId && !perm {
if info.CreatorId == model.BookmarkFileOwner {
if !perm {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
} else if info.CreatorId != c.AppContext.Session().UserId && !perm {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}

Просмотреть файл

@@ -96,6 +96,7 @@ func testUploadFilesPost(
blobs [][]byte,
clientIds []string,
useChunked bool,
isBookmark bool,
) (*model.FileUploadResponse, *model.Response, error) {
// Do not check len(clientIds), leave it entirely to the user to
// provide. The server will error out if it does not match the number
@@ -120,6 +121,10 @@ func testUploadFilesPost(
postURL += fmt.Sprintf("&client_id=%v", url.QueryEscape(clientIds[i]))
}
if isBookmark {
postURL += "&bookmark=true"
}
fur, resp, err := testDoUploadFileRequest(t, c, postURL, blob, ct, cl)
if err != nil {
return nil, resp, err
@@ -145,6 +150,7 @@ func testUploadFilesMultipart(
names []string,
blobs [][]byte,
clientIds []string,
isBookmark bool,
) (
*model.FileUploadResponse,
*model.Response,
@@ -185,7 +191,11 @@ func testUploadFilesMultipart(
}
require.NoError(t, mw.Close())
fur, resp, err := testDoUploadFileRequest(t, c, "", mwBody.Bytes(), mw.FormDataContentType(), -1)
url := ""
if isBookmark {
url += "?bookmark=true"
}
fur, resp, err := testDoUploadFileRequest(t, c, url, mwBody.Bytes(), mw.FormDataContentType(), -1)
if err != nil {
return nil, resp, err
}
@@ -230,6 +240,7 @@ func TestUploadFiles(t *testing.T) {
expectedImageMiniPreview []bool
setupConfig func(a *app.App) func(a *app.App)
checkResponse func(t testing.TB, resp *model.Response)
uploadAsBookmark bool
}{
// Upload a bunch of files, mixed images and non-images
{
@@ -578,6 +589,20 @@ func TestUploadFiles(t *testing.T) {
}
},
},
{
title: "Bookmark images",
names: []string{"orientation_test_5.jpeg"},
expectedImageThumbnailNames: []string{"orientation_test_5_expected_thumb.jpeg"},
expectedImagePreviewNames: []string{"orientation_test_5_expected_preview.jpeg"},
channelId: channel.Id,
expectImage: true,
expectedCreatorId: model.BookmarkFileOwner,
expectedImageWidths: []int{2860},
expectedImageHeights: []int{1578},
expectedImageHasPreview: []bool{true},
expectedImageMiniPreview: []bool{true},
uploadAsBookmark: true,
},
}
for _, useMultipart := range []bool{true, false} {
@@ -627,9 +652,9 @@ func TestUploadFiles(t *testing.T) {
var resp *model.Response
var err error
if useMultipart {
fileResp, resp, err = testUploadFilesMultipart(t, client, channelId, tc.names, blobs, tc.clientIds)
fileResp, resp, err = testUploadFilesMultipart(t, client, channelId, tc.names, blobs, tc.clientIds, tc.uploadAsBookmark)
} else {
fileResp, resp, err = testUploadFilesPost(t, client, channelId, tc.names, blobs, tc.clientIds, tc.useChunkedInSimplePost)
fileResp, resp, err = testUploadFilesPost(t, client, channelId, tc.names, blobs, tc.clientIds, tc.useChunkedInSimplePost, tc.uploadAsBookmark)
}
if tc.checkResponse != nil {
@@ -672,6 +697,9 @@ func TestUploadFiles(t *testing.T) {
ext := filepath.Ext(fname)
name := fname[:len(fname)-len(ext)]
expectedDir := fmt.Sprintf("%v/teams/%v/channels/%v/users/%s/%s", date, FileTeamId, channel.Id, ri.CreatorId, ri.Id)
if tc.uploadAsBookmark {
expectedDir = fmt.Sprintf("%v/teams/%v/channels/%v/%s", model.BookmarkFileOwner, FileTeamId, channel.Id, ri.Id)
}
expectedPath := fmt.Sprintf("%s/%s", expectedDir, fname)
assert.Equal(t, dbInfo.Path, expectedPath,
fmt.Sprintf("File %v saved to:%q, expected:%q", dbInfo.Name, dbInfo.Path, expectedPath))
@@ -774,7 +802,7 @@ func TestGetFile(t *testing.T) {
CheckUnauthorizedStatus(t, resp)
_, _, err = th.SystemAdminClient.GetFile(context.Background(), fileId)
require.Error(t, err)
require.NoError(t, err)
CheckUnauthorizedStatus(t, resp)
}
@@ -889,7 +917,7 @@ func TestGetFileThumbnail(t *testing.T) {
client.Logout(context.Background())
_, _, err = th.SystemAdminClient.GetFileThumbnail(context.Background(), fileId)
require.Error(t, err)
require.NoError(t, err)
CheckForbiddenStatus(t, resp)
}
@@ -1002,7 +1030,7 @@ func TestGetFilePreview(t *testing.T) {
client.Logout(context.Background())
_, _, err = th.SystemAdminClient.GetFilePreview(context.Background(), fileId)
require.Error(t, err)
require.NoError(t, err)
CheckForbiddenStatus(t, resp)
}
@@ -1027,7 +1055,6 @@ func TestGetFileInfo(t *testing.T) {
info, _, err := client.GetFileInfo(context.Background(), fileId)
require.NoError(t, err)
require.NoError(t, err)
require.Equal(t, fileId, info.Id, "got incorrect file")
require.Equal(t, user.Id, info.CreatorId, "file should be assigned to user")
require.Equal(t, "", info.PostId, "file shouldn't have a post")
@@ -1057,7 +1084,7 @@ func TestGetFileInfo(t *testing.T) {
client.Logout(context.Background())
_, _, err = th.SystemAdminClient.GetFileInfo(context.Background(), fileId)
require.Error(t, err)
require.NoError(t, err)
CheckForbiddenStatus(t, resp)
}

Просмотреть файл

@@ -506,6 +506,7 @@ type AppIface interface {
CopyFileInfos(rctx request.CTX, userID string, fileIDs []string) ([]string, *model.AppError)
CopyWranglerPostlist(c request.CTX, wpl *model.WranglerPostList, targetChannel *model.Channel) (*model.Post, *model.AppError)
CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError)
CreateChannelBookmark(c request.CTX, newBookmark *model.ChannelBookmark, connectionId string) (*model.ChannelBookmarkWithFileInfo, *model.AppError)
CreateChannelWithUser(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError)
CreateCommand(cmd *model.Command) (*model.Command, *model.AppError)
CreateCommandWebhook(commandID string, args *model.CommandArgs) (*model.CommandWebhook, *model.AppError)
@@ -550,6 +551,7 @@ type AppIface interface {
DeleteAllKeysForPlugin(pluginID string) *model.AppError
DeleteBrandImage(rctx request.CTX) *model.AppError
DeleteChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError
DeleteChannelBookmark(bookmarkId, connectionId string) (*model.ChannelBookmarkWithFileInfo, *model.AppError)
DeleteCommand(commandID string) *model.AppError
DeleteDraft(rctx request.CTX, draft *model.Draft, connectionID string) *model.AppError
DeleteEmoji(c request.CTX, emoji *model.Emoji) *model.AppError
@@ -627,9 +629,11 @@ type AppIface interface {
GetAuditsPage(rctx request.CTX, userID string, page int, perPage int) (model.Audits, *model.AppError)
GetAuthorizationCode(c request.CTX, w http.ResponseWriter, r *http.Request, service string, props map[string]string, loginHint string) (string, *model.AppError)
GetAuthorizedAppsForUser(userID string, page, perPage int) ([]*model.OAuthApp, *model.AppError)
GetBookmark(bookmarkId string, includeDeleted bool) (*model.ChannelBookmarkWithFileInfo, *model.AppError)
GetBrandImage(rctx request.CTX) ([]byte, *model.AppError)
GetBulkReactionsForPosts(postIDs []string) (map[string][]*model.Reaction, *model.AppError)
GetChannel(c request.CTX, channelID string) (*model.Channel, *model.AppError)
GetChannelBookmarks(channelId string, since int64) ([]*model.ChannelBookmarkWithFileInfo, *model.AppError)
GetChannelByName(c request.CTX, channelName, teamID string, includeDeleted bool) (*model.Channel, *model.AppError)
GetChannelByNameForTeamName(c request.CTX, channelName, teamName string, includeDeleted bool) (*model.Channel, *model.AppError)
GetChannelCounts(c request.CTX, teamID string, userID string) (*model.ChannelCounts, *model.AppError)
@@ -1147,6 +1151,8 @@ type AppIface interface {
UnregisterPluginForSharedChannels(pluginID string) error
UnshareChannel(channelID string) (bool, error)
UpdateActive(c request.CTX, user *model.User, active bool) (*model.User, *model.AppError)
UpdateChannelBookmark(c request.CTX, updateBookmark *model.ChannelBookmarkWithFileInfo, connectionId string) (*model.UpdateChannelBookmarkResponse, *model.AppError)
UpdateChannelBookmarkSortOrder(bookmarkId, channelId string, newIndex int64, connectionId string) ([]*model.ChannelBookmarkWithFileInfo, *model.AppError)
UpdateChannelMemberNotifyProps(c request.CTX, data map[string]string, channelID string, userID string) (*model.ChannelMember, *model.AppError)
UpdateChannelMemberRoles(c request.CTX, channelID string, userID string, newRoles string) (*model.ChannelMember, *model.AppError)
UpdateChannelMemberSchemeRoles(c request.CTX, channelID string, userID string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.ChannelMember, *model.AppError)

Просмотреть файл

@@ -127,10 +127,26 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) {
model.PermissionManagePrivateChannelMembers.Id,
model.PermissionDeletePost.Id,
model.PermissionEditPost.Id,
model.PermissionAddBookmarkPublicChannel.Id,
model.PermissionEditBookmarkPublicChannel.Id,
model.PermissionDeleteBookmarkPublicChannel.Id,
model.PermissionOrderBookmarkPublicChannel.Id,
model.PermissionAddBookmarkPrivateChannel.Id,
model.PermissionEditBookmarkPrivateChannel.Id,
model.PermissionDeleteBookmarkPrivateChannel.Id,
model.PermissionOrderBookmarkPrivateChannel.Id,
},
"channel_admin": {
model.PermissionManageChannelRoles.Id,
model.PermissionUseGroupMentions.Id,
model.PermissionAddBookmarkPublicChannel.Id,
model.PermissionEditBookmarkPublicChannel.Id,
model.PermissionDeleteBookmarkPublicChannel.Id,
model.PermissionOrderBookmarkPublicChannel.Id,
model.PermissionAddBookmarkPrivateChannel.Id,
model.PermissionEditBookmarkPrivateChannel.Id,
model.PermissionDeleteBookmarkPrivateChannel.Id,
model.PermissionOrderBookmarkPrivateChannel.Id,
},
"team_user": {
model.PermissionListTeamChannels.Id,
@@ -166,6 +182,14 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) {
model.PermissionConvertPrivateChannelToPublic.Id,
model.PermissionDeletePost.Id,
model.PermissionDeleteOthersPosts.Id,
model.PermissionAddBookmarkPublicChannel.Id,
model.PermissionEditBookmarkPublicChannel.Id,
model.PermissionDeleteBookmarkPublicChannel.Id,
model.PermissionOrderBookmarkPublicChannel.Id,
model.PermissionAddBookmarkPrivateChannel.Id,
model.PermissionEditBookmarkPrivateChannel.Id,
model.PermissionDeleteBookmarkPrivateChannel.Id,
model.PermissionOrderBookmarkPrivateChannel.Id,
},
"system_user": {
model.PermissionListPublicTeams.Id,

Просмотреть файл

@@ -1122,7 +1122,7 @@ func buildChannelModerations(c request.CTX, channelType model.ChannelType, membe
Enabled: higherScopedMemberPermissions[permissionKey],
}
if permissionKey == "manage_members" {
if permissionKey == "manage_members" || permissionKey == "manage_bookmarks" {
roles.Guests = nil
} else {
roles.Guests = &model.ChannelModeratedRole{

152
server/channels/app/channel_bookmark.go Обычный файл
Просмотреть файл

@@ -0,0 +1,152 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
func (a *App) GetChannelBookmarks(channelId string, since int64) ([]*model.ChannelBookmarkWithFileInfo, *model.AppError) {
bookmarks, err := a.Srv().Store().ChannelBookmark().GetBookmarksForChannelSince(channelId, since)
if err != nil {
return nil, model.NewAppError("GetChannelBookmarks", "app.channel.bookmark.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
}
return bookmarks, nil
}
func (a *App) GetBookmark(bookmarkId string, includeDeleted bool) (*model.ChannelBookmarkWithFileInfo, *model.AppError) {
bookmark, err := a.Srv().Store().ChannelBookmark().Get(bookmarkId, includeDeleted)
if err != nil {
return nil, model.NewAppError("GetBookmark", "app.channel.bookmark.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
}
return bookmark, nil
}
func (a *App) CreateChannelBookmark(c request.CTX, newBookmark *model.ChannelBookmark, connectionId string) (*model.ChannelBookmarkWithFileInfo, *model.AppError) {
newBookmark.OwnerId = c.Session().UserId //ensure that the bookmark is being created by the user who owns the session
newBookmark.Id = "" // ensure that creating a new bookmark generates a new ID
bookmark, err := a.Srv().Store().ChannelBookmark().Save(newBookmark, true)
if err != nil {
return nil, model.NewAppError("CreateChannelBookmark", "app.channel.bookmark.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
message := model.NewWebSocketEvent(model.WebsocketEventChannelBookmarkCreated, "", bookmark.ChannelId, "", nil, connectionId)
bookmarkJSON, jsonErr := json.Marshal(bookmark)
if jsonErr != nil {
return nil, model.NewAppError("CreateChannelBookmark", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("bookmark", string(bookmarkJSON))
a.Publish(message)
return bookmark, nil
}
func (a *App) UpdateChannelBookmark(c request.CTX, updateBookmark *model.ChannelBookmarkWithFileInfo, connectionId string) (*model.UpdateChannelBookmarkResponse, *model.AppError) {
response := &model.UpdateChannelBookmarkResponse{}
if updateBookmark.OwnerId == c.Session().UserId {
isAnotherFile := updateBookmark.FileInfo != nil && updateBookmark.FileId != "" && updateBookmark.FileId != updateBookmark.FileInfo.Id
if isAnotherFile {
if fileAlreadyAttachedErr := a.Srv().Store().ChannelBookmark().ErrorIfBookmarkFileInfoAlreadyAttached(updateBookmark.FileId); fileAlreadyAttachedErr != nil {
return nil, model.NewAppError("UpdateChannelBookmark", "app.channel.bookmark.update.app_error", nil, "", http.StatusInternalServerError).Wrap(fileAlreadyAttachedErr)
}
}
if err := a.Srv().Store().ChannelBookmark().Update(updateBookmark.ChannelBookmark); err != nil {
return nil, model.NewAppError("UpdateChannelBookmark", "app.channel.bookmark.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if isAnotherFile {
fileInfo, fileErr := a.Srv().Store().FileInfo().Get(updateBookmark.FileId)
if fileErr != nil {
return nil, model.NewAppError("UpdateChannelBookmark", "app.channel.bookmark.get_existing.app_err", nil, "", http.StatusNotFound).Wrap(fileErr)
}
response.Updated = updateBookmark.ToBookmarkWithFileInfo(fileInfo)
} else {
response.Updated = updateBookmark.ToBookmarkWithFileInfo(updateBookmark.FileInfo)
}
} else {
existingBookmark, ebErr := a.Srv().Store().ChannelBookmark().Get(updateBookmark.Id, false)
if ebErr != nil {
return nil, model.NewAppError("UpdateChannelBookmark", "app.channel.bookmark.get_existing.app_err", nil, "", http.StatusNotFound).Wrap(ebErr)
}
existingBookmark.DeleteAt = model.GetMillis()
if err := a.Srv().Store().ChannelBookmark().Delete(updateBookmark.Id, false); err != nil {
return nil, model.NewAppError("UpdateChannelBookmark", "app.channel.bookmark.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
newBookmark := updateBookmark.SetOriginal(c.Session().UserId)
bookmark, err := a.Srv().Store().ChannelBookmark().Save(newBookmark, false)
if err != nil {
return nil, model.NewAppError("UpdateChannelBookmark", "app.channel.bookmark.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
response.Updated = bookmark
response.Deleted = existingBookmark.ToBookmarkWithFileInfo(nil)
}
message := model.NewWebSocketEvent(model.WebsocketEventChannelBookmarkUpdated, "", updateBookmark.ChannelId, "", nil, connectionId)
bookmarkJSON, jsonErr := json.Marshal(response)
if jsonErr != nil {
return nil, model.NewAppError("UpdateChannelBookmark", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("bookmarks", string(bookmarkJSON))
a.Publish(message)
return response, nil
}
func (a *App) DeleteChannelBookmark(bookmarkId, connectionId string) (*model.ChannelBookmarkWithFileInfo, *model.AppError) {
if err := a.Srv().Store().ChannelBookmark().Delete(bookmarkId, true); err != nil {
return nil, model.NewAppError("DeleteChannelBookmark", "app.channel.bookmark.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
bookmark, err := a.GetBookmark(bookmarkId, true)
if err != nil {
return nil, model.NewAppError("DeleteChannelBookmark", "app.channel.bookmark.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
}
message := model.NewWebSocketEvent(model.WebsocketEventChannelBookmarkDeleted, "", bookmark.ChannelId, "", nil, connectionId)
bookmarkJSON, jsonErr := json.Marshal(bookmark)
if jsonErr != nil {
return nil, model.NewAppError("DeleteChannelBookmark", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("bookmark", string(bookmarkJSON))
a.Publish(message)
return bookmark, nil
}
func (a *App) UpdateChannelBookmarkSortOrder(bookmarkId, channelId string, newIndex int64, connectionId string) ([]*model.ChannelBookmarkWithFileInfo, *model.AppError) {
bookmarks, err := a.Srv().Store().ChannelBookmark().UpdateSortOrder(bookmarkId, channelId, newIndex)
if err != nil {
var iiErr *store.ErrInvalidInput
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &iiErr):
return nil, model.NewAppError("UpdateSortOrder", "app.channel.bookmark.update_sort.invalid_input.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpdateSortOrder", "app.channel.bookmark.update_sort.missing_bookmark.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("UpdateSortOrder", "app.channel.bookmark.update_sort.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
message := model.NewWebSocketEvent(model.WebsocketEventChannelBookmarkSorted, "", channelId, "", nil, connectionId)
bookmarkJSON, jsonErr := json.Marshal(bookmarks)
if jsonErr != nil {
return nil, model.NewAppError("UpdateSortOrder", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("bookmarks", string(bookmarkJSON))
a.Publish(message)
return bookmarks, nil
}

498
server/channels/app/channel_bookmark_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,498 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"testing"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func find_bookmark(slice []*model.ChannelBookmarkWithFileInfo, id string) *model.ChannelBookmarkWithFileInfo {
for _, element := range slice {
if element.Id == id {
return element
}
}
return nil
}
func createBookmark(name string, bookmarkType model.ChannelBookmarkType, channelId string, fileId string) *model.ChannelBookmark {
bookmark := &model.ChannelBookmark{
ChannelId: channelId,
DisplayName: name,
LinkUrl: "https://mattermost.com",
Type: bookmarkType,
Emoji: ":smile:",
FileId: fileId,
}
return bookmark
}
func TestCreateBookmark(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("create a channel bookmark", func(t *testing.T) {
th.Context.Session().UserId = th.BasicUser.Id // set the user for the session
bookmark1 := createBookmark("Link bookmark test", model.ChannelBookmarkLink, th.BasicChannel.Id, "")
bookmarkResp, err := th.App.CreateChannelBookmark(th.Context, bookmark1, "")
require.Nil(t, err)
require.NotNil(t, bookmarkResp)
assert.Equal(t, bookmarkResp.ChannelId, th.BasicChannel.Id)
assert.NotEmpty(t, bookmarkResp.Id)
bookmark2 := createBookmark("File bookmark test", model.ChannelBookmarkFile, th.BasicChannel.Id, "")
bookmarkResp, err = th.App.CreateChannelBookmark(th.Context, bookmark2, "")
assert.Nil(t, bookmarkResp)
assert.NotNil(t, err)
})
t.Run("Cannot create more than MaxBookmarksPerChannel", func(t *testing.T) {
th.Context.Session().UserId = th.BasicUser.Id // set the user for the session
for i := 1; i < model.MaxBookmarksPerChannel; i++ {
bookmark := createBookmark(fmt.Sprintf("Link bookmark test %d", i), model.ChannelBookmarkLink, th.BasicChannel.Id, "")
bookmarkResp, err := th.App.CreateChannelBookmark(th.Context, bookmark, "")
require.Nil(t, err)
require.NotNil(t, bookmarkResp)
assert.Equal(t, bookmarkResp.ChannelId, th.BasicChannel.Id)
assert.NotEmpty(t, bookmarkResp.Id)
}
bookmark := createBookmark("Bookmark that should not be added", model.ChannelBookmarkLink, th.BasicChannel.Id, "")
bookmarkResp, err := th.App.CreateChannelBookmark(th.Context, bookmark, "")
assert.Nil(t, bookmarkResp)
assert.NotNil(t, err)
})
}
func TestUpdateBookmark(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
var updateBookmark *model.ChannelBookmarkWithFileInfo
var testUpdateAnotherFile = func(th *TestHelper, t *testing.T) {
file := &model.FileInfo{
Id: model.NewId(),
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
th.App.Srv().Store().FileInfo().Save(th.Context, file)
defer th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, file.Id)
bookmark2 := createBookmark("File to be updated", model.ChannelBookmarkFile, th.BasicChannel.Id, file.Id)
bookmarkResp, err := th.App.CreateChannelBookmark(th.Context, bookmark2, "")
require.Nil(t, err)
require.NotNil(t, bookmarkResp)
file2 := &model.FileInfo{
Id: model.NewId(),
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
th.App.Srv().Store().FileInfo().Save(th.Context, file2)
th.App.Srv().Store().FileInfo().AttachToPost(th.Context, file2.Id, model.NewId(), th.BasicChannel.Id, model.BookmarkFileOwner)
defer th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, file2.Id)
bookmark2.FileId = file2.Id
bookmarkResp, err = th.App.CreateChannelBookmark(th.Context, bookmark2, "")
require.NotNil(t, err)
require.Nil(t, bookmarkResp)
}
t.Run("same user update a channel bookmark", func(t *testing.T) {
bookmark1 := &model.ChannelBookmark{
ChannelId: th.BasicChannel.Id,
DisplayName: "Link bookmark test",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
th.Context.Session().UserId = th.BasicUser.Id // set the user for the session
bookmarkResp, err := th.App.CreateChannelBookmark(th.Context, bookmark1, "")
require.Nil(t, err)
require.NotNil(t, bookmarkResp)
updateBookmark = bookmarkResp.Clone()
updateBookmark.DisplayName = "New name"
time.Sleep(1 * time.Millisecond) // to avoid collisions
response, _ := th.App.UpdateChannelBookmark(th.Context, updateBookmark, "")
require.NotNil(t, response)
assert.Greater(t, response.Updated.UpdateAt, response.Updated.CreateAt)
testUpdateAnotherFile(th, t)
})
t.Run("another user update a channel bookmark", func(t *testing.T) {
updateBookmark2 := updateBookmark.Clone()
updateBookmark2.DisplayName = "Another new name"
th.Context.Session().UserId = th.BasicUser2.Id
response, _ := th.App.UpdateChannelBookmark(th.Context, updateBookmark2, "")
require.NotNil(t, response)
assert.Equal(t, response.Updated.OriginalId, response.Deleted.Id)
assert.Equal(t, response.Updated.DeleteAt, int64(0))
assert.Greater(t, response.Deleted.DeleteAt, int64(0))
assert.Equal(t, "Another new name", response.Updated.DisplayName)
assert.Equal(t, "New name", response.Deleted.DisplayName)
testUpdateAnotherFile(th, t)
})
t.Run("update an already deleted channel bookmark", func(t *testing.T) {
bookmark1 := &model.ChannelBookmark{
ChannelId: th.BasicChannel.Id,
DisplayName: "Link bookmark test",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
th.Context.Session().UserId = th.BasicUser.Id // set the user for the session
bookmarkResp, err := th.App.CreateChannelBookmark(th.Context, bookmark1, "")
require.Nil(t, err)
require.NotNil(t, bookmarkResp)
updateBookmark = bookmarkResp.Clone()
_, err = th.App.DeleteChannelBookmark(updateBookmark.Id, "")
assert.Nil(t, err)
updateBookmark.DisplayName = "New name"
_, err = th.App.UpdateChannelBookmark(th.Context, updateBookmark, "")
assert.NotNil(t, err)
})
t.Run("update a nonexisting channel bookmark", func(t *testing.T) {
updateBookmark := &model.ChannelBookmark{
Id: model.NewId(),
ChannelId: th.BasicChannel.Id,
DisplayName: "Link bookmark test",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
_, err := th.App.UpdateChannelBookmark(th.Context, updateBookmark.ToBookmarkWithFileInfo(nil), "")
assert.NotNil(t, err)
assert.Equal(t, "app.channel.bookmark.get_existing.app_err", err.Id)
})
}
func TestDeleteBookmark(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("delete a channel bookmark", func(t *testing.T) {
bookmark1 := &model.ChannelBookmark{
ChannelId: th.BasicChannel.Id,
DisplayName: "Link bookmark test",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
th.Context.Session().UserId = th.BasicUser.Id // set the user for the session
bookmarkResp, err := th.App.CreateChannelBookmark(th.Context, bookmark1, "")
require.Nil(t, err)
require.NotNil(t, bookmarkResp)
bookmarkResp, err = th.App.DeleteChannelBookmark(bookmarkResp.Id, "")
require.Nil(t, err)
require.NotNil(t, bookmarkResp)
assert.Greater(t, bookmarkResp.DeleteAt, int64(0))
})
}
func TestGetChannelBookmarks(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Context.Session().UserId = th.BasicUser.Id // set the user for the session
bookmark1 := &model.ChannelBookmark{
ChannelId: th.BasicChannel.Id,
DisplayName: "Bookmark 1",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
th.App.CreateChannelBookmark(th.Context, bookmark1, "")
file := &model.FileInfo{
Id: model.NewId(),
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
th.App.Srv().Store().FileInfo().Save(th.Context, file)
defer th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, file.Id)
bookmark2 := &model.ChannelBookmark{
ChannelId: th.BasicChannel.Id,
DisplayName: "Bookmark 2",
FileId: file.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
th.App.CreateChannelBookmark(th.Context, bookmark2, "")
t.Run("get bookmarks of a channel", func(t *testing.T) {
bookmarks, err := th.App.GetChannelBookmarks(th.BasicChannel.Id, 0)
require.Nil(t, err)
require.NotNil(t, bookmarks)
assert.Len(t, bookmarks, 2)
})
t.Run("get bookmarks of a channel after one is deleted (aka only return the changed bookmarks)", func(t *testing.T) {
now := model.GetMillis()
th.App.DeleteChannelBookmark(bookmark1.Id, "")
bookmarks, err := th.App.GetChannelBookmarks(th.BasicChannel.Id, 0)
require.Nil(t, err)
require.NotNil(t, bookmarks)
assert.Len(t, bookmarks, 1)
bookmarks, err = th.App.GetChannelBookmarks(th.BasicChannel.Id, now)
require.Nil(t, err)
require.NotNil(t, bookmarks)
assert.Len(t, bookmarks, 1)
deleted := false
for _, b := range bookmarks {
if b.DeleteAt > 0 {
deleted = true
break
}
}
assert.Equal(t, deleted, true)
})
}
func TestUpdateChannelBookmarkSortOrder(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
channelId := th.BasicChannel.Id
th.Context.Session().UserId = th.BasicUser.Id // set the user for the session
bookmark0 := &model.ChannelBookmark{
ChannelId: channelId,
DisplayName: "Bookmark 0",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
file := &model.FileInfo{
Id: model.NewId(),
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
bookmark1 := &model.ChannelBookmark{
ChannelId: channelId,
DisplayName: "Bookmark 1",
FileId: file.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
_, err := th.App.Srv().Store().FileInfo().Save(th.Context, file)
require.NoError(t, err)
defer th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, file.Id)
bookmark2 := &model.ChannelBookmark{
ChannelId: channelId,
DisplayName: "Bookmark 2",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
}
bookmark3 := &model.ChannelBookmark{
ChannelId: channelId,
DisplayName: "Bookmark 3",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
}
bookmark4 := &model.ChannelBookmark{
ChannelId: channelId,
DisplayName: "Bookmark 4",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
}
bookmarkResp, appErr := th.App.CreateChannelBookmark(th.Context, bookmark0, "")
require.Nil(t, appErr)
require.NotNil(t, bookmarkResp)
bookmark0 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, appErr = th.App.CreateChannelBookmark(th.Context, bookmark1, "")
require.Nil(t, appErr)
require.NotNil(t, bookmarkResp)
bookmark1 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, appErr = th.App.CreateChannelBookmark(th.Context, bookmark2, "")
require.Nil(t, appErr)
require.NotNil(t, bookmarkResp)
bookmark2 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, appErr = th.App.CreateChannelBookmark(th.Context, bookmark3, "")
require.Nil(t, appErr)
require.NotNil(t, bookmarkResp)
bookmark3 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, appErr = th.App.CreateChannelBookmark(th.Context, bookmark4, "")
require.Nil(t, appErr)
require.NotNil(t, bookmarkResp)
bookmark4 = bookmarkResp.ChannelBookmark.Clone()
t.Run("change order of bookmarks first to last", func(t *testing.T) {
bookmarks, sortErr := th.App.UpdateChannelBookmarkSortOrder(bookmark0.Id, channelId, int64(4), "")
require.Nil(t, sortErr)
require.NotNil(t, bookmarks)
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks last to first", func(t *testing.T) {
bookmarks, sortErr := th.App.UpdateChannelBookmarkSortOrder(bookmark0.Id, channelId, int64(0), "")
require.Nil(t, sortErr)
require.NotNil(t, bookmarks)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks first to third", func(t *testing.T) {
bookmarks, sortErr := th.App.UpdateChannelBookmarkSortOrder(bookmark0.Id, channelId, int64(2), "")
require.Nil(t, sortErr)
require.NotNil(t, bookmarks)
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
// now reset order
th.App.UpdateChannelBookmarkSortOrder(bookmark0.Id, channelId, int64(0), "")
})
t.Run("change order of bookmarks second to third", func(t *testing.T) {
bookmarks, sortErr := th.App.UpdateChannelBookmarkSortOrder(bookmark1.Id, channelId, int64(2), "")
require.Nil(t, sortErr)
require.NotNil(t, bookmarks)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks third to second", func(t *testing.T) {
bookmarks, sortErr := th.App.UpdateChannelBookmarkSortOrder(bookmark1.Id, channelId, int64(1), "")
require.Nil(t, sortErr)
require.NotNil(t, bookmarks)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks last to previous last", func(t *testing.T) {
bookmarks, sortErr := th.App.UpdateChannelBookmarkSortOrder(bookmark4.Id, channelId, int64(3), "")
require.Nil(t, sortErr)
require.NotNil(t, bookmarks)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks last to second", func(t *testing.T) {
bookmarks, sortErr := th.App.UpdateChannelBookmarkSortOrder(bookmark3.Id, channelId, int64(1), "")
require.Nil(t, sortErr)
require.NotNil(t, bookmarks)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks error when new index is out of bounds", func(t *testing.T) {
_, appErr = th.App.UpdateChannelBookmarkSortOrder(bookmark3.Id, channelId, int64(-1), "")
assert.Error(t, appErr)
_, appErr = th.App.UpdateChannelBookmarkSortOrder(bookmark3.Id, channelId, int64(5), "")
assert.Error(t, appErr)
})
t.Run("change order of bookmarks error when bookmark not found", func(t *testing.T) {
_, appErr = th.App.UpdateChannelBookmarkSortOrder(model.NewId(), channelId, int64(0), "")
assert.Error(t, appErr)
})
}

Просмотреть файл

@@ -1716,6 +1716,7 @@ func TestPatchChannelModerationsForChannel(t *testing.T) {
createReactions := model.ChannelModeratedPermissions[1]
manageMembers := model.ChannelModeratedPermissions[2]
channelMentions := model.ChannelModeratedPermissions[3]
manageBookmarks := model.ChannelModeratedPermissions[4]
nonChannelModeratedPermission := model.PermissionCreateBot.Id
@@ -1809,6 +1810,26 @@ func TestPatchChannelModerationsForChannel(t *testing.T) {
},
},
},
{
Name: "Removing manage bookmarks from members role",
ChannelModerationsPatch: []*model.ChannelModerationPatch{
{
Name: &manageBookmarks,
Roles: &model.ChannelModeratedRolesPatch{Members: model.NewBool(false)},
},
},
PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{
manageBookmarks: {
Members: &model.ChannelModeratedRole{Value: false, Enabled: true},
},
},
RevertChannelModerationsPatch: []*model.ChannelModerationPatch{
{
Name: &manageBookmarks,
Roles: &model.ChannelModeratedRolesPatch{Members: model.NewBool(true)},
},
},
},
{
Name: "Removing create posts from guests role",
ChannelModerationsPatch: []*model.ChannelModerationPatch{
@@ -1881,6 +1902,18 @@ func TestPatchChannelModerationsForChannel(t *testing.T) {
ShouldError: false,
ShouldHaveNoChannelScheme: true,
},
{
Name: "Removing manage bookmarks from guests role should not error",
ChannelModerationsPatch: []*model.ChannelModerationPatch{
{
Name: &manageBookmarks,
Roles: &model.ChannelModeratedRolesPatch{Guests: model.NewBool(false)},
},
},
PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{},
ShouldError: false,
ShouldHaveNoChannelScheme: true,
},
{
Name: "Removing a permission that is not channel moderated should not error",
ChannelModerationsPatch: []*model.ChannelModerationPatch{
@@ -1981,6 +2014,12 @@ func TestPatchChannelModerationsForChannel(t *testing.T) {
Members: model.NewBool(true),
},
},
{
Name: &manageBookmarks,
Roles: &model.ChannelModeratedRolesPatch{
Members: model.NewBool(true),
},
},
},
PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{},
ShouldHaveNoChannelScheme: true,
@@ -2039,7 +2078,7 @@ func TestPatchChannelModerationsForChannel(t *testing.T) {
if permission, found := tc.PermissionsModeratedByPatch[moderation.Name]; found && permission.Guests != nil {
require.Equal(t, moderation.Roles.Guests.Value, permission.Guests.Value)
require.Equal(t, moderation.Roles.Guests.Enabled, permission.Guests.Enabled)
} else if moderation.Name == manageMembers {
} else if moderation.Name == manageMembers || moderation.Name == "manage_bookmarks" {
require.Empty(t, moderation.Roles.Guests)
} else {
require.Equal(t, moderation.Roles.Guests.Value, true)

Просмотреть файл

@@ -704,6 +704,7 @@ func (t *UploadFileTask) init(a *App) {
t.fileinfo.CreatorId = t.UserId
t.fileinfo.CreateAt = t.Timestamp.UnixNano() / int64(time.Millisecond)
t.fileinfo.Path = t.pathPrefix() + t.Name
t.fileinfo.ChannelId = t.ChannelId
t.limitedInput = &io.LimitedReader{
R: t.Input,
@@ -950,6 +951,12 @@ func (t *UploadFileTask) postprocessImage(file io.Reader) {
}
func (t UploadFileTask) pathPrefix() string {
if t.UserId == model.BookmarkFileOwner {
return model.BookmarkFileOwner +
"/teams/" + t.TeamId +
"/channels/" + t.ChannelId +
"/" + t.fileinfo.Id + "/"
}
return t.Timestamp.Format("20060102") +
"/teams/" + t.TeamId +
"/channels/" + t.ChannelId +
@@ -1003,6 +1010,9 @@ func (a *App) DoUploadFileExpectModification(c request.CTX, now time.Time, rawTe
info.CreateAt = now.UnixNano() / int64(time.Millisecond)
pathPrefix := now.Format("20060102") + "/teams/" + teamID + "/channels/" + channelID + "/users/" + userID + "/" + info.Id + "/"
if userID == model.BookmarkFileOwner {
pathPrefix = model.BookmarkFileOwner + "/teams/" + teamID + "/channels/" + channelID + "/" + info.Id + "/"
}
info.Path = pathPrefix + filename
if info.IsImage() && !info.IsSvg() {

Просмотреть файл

@@ -96,6 +96,16 @@ func TestDoUploadFile(t *testing.T) {
value = fmt.Sprintf("20090305/teams/%v/channels/%v/users/%v/%v/%v", teamID, channelID, userID, info4.Id, filename)
assert.Equal(t, value, info4.Path, "stored file at incorrect path")
info5, err := th.App.DoUploadFile(th.Context, time.Date(2008, 3, 5, 1, 2, 3, 4, time.Local), teamID, channelID, model.BookmarkFileOwner, filename, data)
require.Nil(t, err, "DoUploadFile should succeed with valid data")
defer func() {
th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info5.Id)
th.App.RemoveFile(info3.Path)
}()
value = fmt.Sprintf("%v/teams/%v/channels/%v/%v/%v", model.BookmarkFileOwner, teamID, channelID, info5.Id, filename)
assert.Equal(t, value, info5.Path, "stored file at incorrect path")
}
func TestUploadFile(t *testing.T) {

Просмотреть файл

@@ -1994,6 +1994,28 @@ func (a *OpenTracingAppLayer) CreateChannel(c request.CTX, channel *model.Channe
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateChannelBookmark(c request.CTX, newBookmark *model.ChannelBookmark, connectionId string) (*model.ChannelBookmarkWithFileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateChannelBookmark")
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.CreateChannelBookmark(c, newBookmark, connectionId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateChannelScheme(c request.CTX, channel *model.Channel) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateChannelScheme")
@@ -3093,6 +3115,28 @@ func (a *OpenTracingAppLayer) DeleteChannel(c request.CTX, channel *model.Channe
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteChannelBookmark(bookmarkId string, connectionId string) (*model.ChannelBookmarkWithFileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteChannelBookmark")
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.DeleteChannelBookmark(bookmarkId, connectionId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteChannelScheme(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteChannelScheme")
@@ -5138,6 +5182,28 @@ func (a *OpenTracingAppLayer) GetAuthorizedAppsForUser(userID string, page int,
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetBookmark(bookmarkId string, includeDeleted bool) (*model.ChannelBookmarkWithFileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetBookmark")
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.GetBookmark(bookmarkId, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetBot(rctx request.CTX, botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetBot")
@@ -5248,6 +5314,28 @@ func (a *OpenTracingAppLayer) GetChannel(c request.CTX, channelID string) (*mode
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelBookmarks(channelId string, since int64) ([]*model.ChannelBookmarkWithFileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelBookmarks")
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.GetChannelBookmarks(channelId, since)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelByName(c request.CTX, channelName string, teamID string, includeDeleted bool) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelByName")
@@ -17562,6 +17650,50 @@ func (a *OpenTracingAppLayer) UpdateChannel(c request.CTX, channel *model.Channe
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannelBookmark(c request.CTX, updateBookmark *model.ChannelBookmarkWithFileInfo, connectionId string) (*model.UpdateChannelBookmarkResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannelBookmark")
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.UpdateChannelBookmark(c, updateBookmark, connectionId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannelBookmarkSortOrder(bookmarkId string, channelId string, newIndex int64, connectionId string) ([]*model.ChannelBookmarkWithFileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannelBookmarkSortOrder")
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.UpdateChannelBookmarkSortOrder(bookmarkId, channelId, newIndex, connectionId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannelMemberNotifyProps(c request.CTX, data map[string]string, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannelMemberNotifyProps")

Просмотреть файл

@@ -1156,6 +1156,31 @@ func (a *App) getAddOutgoingOAuthConnectionsPermissions() (permissionsMap, error
return t, nil
}
func (a *App) getAddChannelBookmarksPermissionsMigration() (permissionsMap, error) {
transformations := []permissionTransformation{}
transformations = append(transformations, permissionTransformation{
On: permissionOr(
isRole(model.ChannelUserRoleId),
isRole(model.ChannelAdminRoleId),
isRole(model.TeamAdminRoleId),
isRole(model.SystemAdminRoleId),
),
Add: []string{
model.PermissionAddBookmarkPublicChannel.Id,
model.PermissionEditBookmarkPublicChannel.Id,
model.PermissionDeleteBookmarkPublicChannel.Id,
model.PermissionOrderBookmarkPublicChannel.Id,
model.PermissionAddBookmarkPrivateChannel.Id,
model.PermissionEditBookmarkPrivateChannel.Id,
model.PermissionDeleteBookmarkPrivateChannel.Id,
model.PermissionOrderBookmarkPrivateChannel.Id,
},
})
return transformations, nil
}
// DoPermissionsMigrations execute all the permissions migrations need by the current version.
func (a *App) DoPermissionsMigrations() error {
return a.Srv().doPermissionsMigrations()
@@ -1202,6 +1227,7 @@ func (s *Server) doPermissionsMigrations() error {
{Key: model.MigrationKeyAddReadChannelContentPermissions, Migration: a.getAddChannelReadContentPermissions},
{Key: model.MigrationKeyAddIPFilteringPermissions, Migration: a.getAddIPFilterPermissionsMigration},
{Key: model.MigrationKeyAddOutgoingOAuthConnectionsPermissions, Migration: a.getAddOutgoingOAuthConnectionsPermissions},
{Key: model.MigrationKeyAddChannelBookmarksPermissions, Migration: a.getAddChannelBookmarksPermissionsMigration},
}
roles, err := s.Store().Role().GetAll()

Просмотреть файл

@@ -236,6 +236,8 @@ channels/db/migrations/mysql/000118_create_index_poststats.down.sql
channels/db/migrations/mysql/000118_create_index_poststats.up.sql
channels/db/migrations/mysql/000119_msteams_shared_channels_opts.down.sql
channels/db/migrations/mysql/000119_msteams_shared_channels_opts.up.sql
channels/db/migrations/mysql/000120_create_channelbookmarks_table.down.sql
channels/db/migrations/mysql/000120_create_channelbookmarks_table.up.sql
channels/db/migrations/postgres/000001_create_teams.down.sql
channels/db/migrations/postgres/000001_create_teams.up.sql
channels/db/migrations/postgres/000002_create_team_members.down.sql
@@ -472,3 +474,5 @@ channels/db/migrations/postgres/000118_create_index_poststats.down.sql
channels/db/migrations/postgres/000118_create_index_poststats.up.sql
channels/db/migrations/postgres/000119_msteams_shared_channels_opts.down.sql
channels/db/migrations/postgres/000119_msteams_shared_channels_opts.up.sql
channels/db/migrations/postgres/000120_create_channelbookmarks_table.down.sql
channels/db/migrations/postgres/000120_create_channelbookmarks_table.up.sql

Просмотреть файл

@@ -0,0 +1 @@
DROP TABLE IF EXISTS ChannelBookmarks;

Просмотреть файл

@@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS ChannelBookmarks (
Id varchar(26) NOT NULL,
OwnerId varchar(26) NOT NULL,
ChannelId varchar(26) NOT NULL,
FileInfoId varchar(26) DEFAULT NULL,
CreateAt bigint(20) DEFAULT 0,
UpdateAt bigint(20) DEFAULT 0,
DeleteAt bigint(20) DEFAULT 0,
DisplayName text,
SortOrder bigint(20) DEFAULT 0,
LinkUrl text DEFAULT NULL,
ImageUrl text DEFAULT NULL,
Emoji varchar(64) DEFAULT NULL,
Type ENUM('link', 'file'),
OriginalId varchar(26) DEFAULT NULL,
ParentId varchar(26) DEFAULT NULL,
PRIMARY KEY (Id),
KEY idx_channelbookmarks_channelid (ChannelId),
KEY idx_channelbookmarks_update_at (UpdateAt),
KEY idx_channelbookmarks_delete_at (DeleteAt)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Просмотреть файл

@@ -0,0 +1,18 @@
DROP INDEX IF EXISTS idx_channelbookmarks_channelid;
DROP INDEX IF EXISTS idx_channelbookmarks_update_at;
DROP INDEX IF EXISTS idx_channelbookmarks_delete_at;
DROP TABLE IF EXISTS channelbookmarks;
DO
$$
BEGIN
IF EXISTS (SELECT * FROM pg_type typ
INNER JOIN pg_namespace nsp ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = current_schema()
AND typ.typname = 'channel_bookmark_type') THEN
DROP TYPE channel_bookmark_type;
END IF;
END;
$$
LANGUAGE plpgsql;

Просмотреть файл

@@ -0,0 +1,34 @@
DO
$$
BEGIN
IF NOT EXISTS (SELECT * FROM pg_type typ
INNER JOIN pg_namespace nsp ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = current_schema()
AND typ.typname = 'channel_bookmark_type') THEN
CREATE TYPE channel_bookmark_type AS ENUM ('link', 'file');
END IF;
END;
$$
LANGUAGE plpgsql;
CREATE TABLE IF NOT EXISTS channelbookmarks (
id varchar(26) PRIMARY KEY,
ownerid varchar(26) NOT NULL,
channelid varchar(26) NOT NULL,
fileinfoid varchar(26) DEFAULT NULL,
createat bigint DEFAULT 0,
updateat bigint DEFAULT 0,
deleteat bigint DEFAULT 0,
displayname text DEFAULT '',
sortorder integer DEFAULT 0,
linkurl text DEFAULT NULL,
imageurl text DEFAULT NULL,
emoji varchar(64) DEFAULT NULL,
type channel_bookmark_type DEFAULT 'link',
originalid varchar(26) DEFAULT NULL,
parentid varchar(26) DEFAULT NULL
);
CREATE INDEX IF NOT EXISTS idx_channelbookmarks_channelid ON channelbookmarks (channelid);
CREATE INDEX IF NOT EXISTS idx_channelbookmarks_update_at ON channelbookmarks (updateat);
CREATE INDEX IF NOT EXISTS idx_channelbookmarks_delete_at ON channelbookmarks (deleteat);

Просмотреть файл

@@ -22,6 +22,7 @@ type OpenTracingLayer struct {
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelBookmarkStore store.ChannelBookmarkStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
@@ -78,6 +79,10 @@ func (s *OpenTracingLayer) Channel() store.ChannelStore {
return s.ChannelStore
}
func (s *OpenTracingLayer) ChannelBookmark() store.ChannelBookmarkStore {
return s.ChannelBookmarkStore
}
func (s *OpenTracingLayer) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return s.ChannelMemberHistoryStore
}
@@ -261,6 +266,11 @@ type OpenTracingLayerChannelStore struct {
Root *OpenTracingLayer
}
type OpenTracingLayerChannelBookmarkStore struct {
store.ChannelBookmarkStore
Root *OpenTracingLayer
}
type OpenTracingLayerChannelMemberHistoryStore struct {
store.ChannelMemberHistoryStore
Root *OpenTracingLayer
@@ -2689,6 +2699,132 @@ func (s *OpenTracingLayerChannelStore) UserBelongsToChannels(userID string, chan
return result, err
}
func (s *OpenTracingLayerChannelBookmarkStore) Delete(bookmarkId string, deleteFile bool) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelBookmarkStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelBookmarkStore.Delete(bookmarkId, deleteFile)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileId string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelBookmarkStore.ErrorIfBookmarkFileInfoAlreadyAttached")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelBookmarkStore.ErrorIfBookmarkFileInfoAlreadyAttached(fileId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelBookmarkStore) Get(Id string, includeDeleted bool) (*model.ChannelBookmarkWithFileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelBookmarkStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelBookmarkStore.Get(Id, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelBookmarkStore) GetBookmarksForChannelSince(channelId string, since int64) ([]*model.ChannelBookmarkWithFileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelBookmarkStore.GetBookmarksForChannelSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelBookmarkStore.GetBookmarksForChannelSince(channelId, since)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelBookmarkStore) Save(bookmark *model.ChannelBookmark, increaseSortOrder bool) (*model.ChannelBookmarkWithFileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelBookmarkStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelBookmarkStore.Save(bookmark, increaseSortOrder)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelBookmarkStore) Update(bookmark *model.ChannelBookmark) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelBookmarkStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelBookmarkStore.Update(bookmark)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelBookmarkStore) UpdateSortOrder(bookmarkId string, channelId string, newIndex int64) ([]*model.ChannelBookmarkWithFileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelBookmarkStore.UpdateSortOrder")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelBookmarkStore.UpdateSortOrder(bookmarkId, channelId, newIndex)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.DeleteOrphanedRows")
@@ -13284,6 +13420,7 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer {
newStore.AuditStore = &OpenTracingLayerAuditStore{AuditStore: childStore.Audit(), Root: &newStore}
newStore.BotStore = &OpenTracingLayerBotStore{BotStore: childStore.Bot(), Root: &newStore}
newStore.ChannelStore = &OpenTracingLayerChannelStore{ChannelStore: childStore.Channel(), Root: &newStore}
newStore.ChannelBookmarkStore = &OpenTracingLayerChannelBookmarkStore{ChannelBookmarkStore: childStore.ChannelBookmark(), Root: &newStore}
newStore.ChannelMemberHistoryStore = &OpenTracingLayerChannelMemberHistoryStore{ChannelMemberHistoryStore: childStore.ChannelMemberHistory(), Root: &newStore}
newStore.ClusterDiscoveryStore = &OpenTracingLayerClusterDiscoveryStore{ClusterDiscoveryStore: childStore.ClusterDiscovery(), Root: &newStore}
newStore.CommandStore = &OpenTracingLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}

Просмотреть файл

@@ -26,6 +26,7 @@ type RetryLayer struct {
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelBookmarkStore store.ChannelBookmarkStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
@@ -82,6 +83,10 @@ func (s *RetryLayer) Channel() store.ChannelStore {
return s.ChannelStore
}
func (s *RetryLayer) ChannelBookmark() store.ChannelBookmarkStore {
return s.ChannelBookmarkStore
}
func (s *RetryLayer) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return s.ChannelMemberHistoryStore
}
@@ -265,6 +270,11 @@ type RetryLayerChannelStore struct {
Root *RetryLayer
}
type RetryLayerChannelBookmarkStore struct {
store.ChannelBookmarkStore
Root *RetryLayer
}
type RetryLayerChannelMemberHistoryStore struct {
store.ChannelMemberHistoryStore
Root *RetryLayer
@@ -2987,6 +2997,153 @@ func (s *RetryLayerChannelStore) UserBelongsToChannels(userID string, channelIds
}
func (s *RetryLayerChannelBookmarkStore) Delete(bookmarkId string, deleteFile bool) error {
tries := 0
for {
err := s.ChannelBookmarkStore.Delete(bookmarkId, deleteFile)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileId string) error {
tries := 0
for {
err := s.ChannelBookmarkStore.ErrorIfBookmarkFileInfoAlreadyAttached(fileId)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelBookmarkStore) Get(Id string, includeDeleted bool) (*model.ChannelBookmarkWithFileInfo, error) {
tries := 0
for {
result, err := s.ChannelBookmarkStore.Get(Id, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelBookmarkStore) GetBookmarksForChannelSince(channelId string, since int64) ([]*model.ChannelBookmarkWithFileInfo, error) {
tries := 0
for {
result, err := s.ChannelBookmarkStore.GetBookmarksForChannelSince(channelId, since)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelBookmarkStore) Save(bookmark *model.ChannelBookmark, increaseSortOrder bool) (*model.ChannelBookmarkWithFileInfo, error) {
tries := 0
for {
result, err := s.ChannelBookmarkStore.Save(bookmark, increaseSortOrder)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelBookmarkStore) Update(bookmark *model.ChannelBookmark) error {
tries := 0
for {
err := s.ChannelBookmarkStore.Update(bookmark)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelBookmarkStore) UpdateSortOrder(bookmarkId string, channelId string, newIndex int64) ([]*model.ChannelBookmarkWithFileInfo, error) {
tries := 0
for {
result, err := s.ChannelBookmarkStore.UpdateSortOrder(bookmarkId, channelId, newIndex)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0
@@ -15157,6 +15314,7 @@ func New(childStore store.Store) *RetryLayer {
newStore.AuditStore = &RetryLayerAuditStore{AuditStore: childStore.Audit(), Root: &newStore}
newStore.BotStore = &RetryLayerBotStore{BotStore: childStore.Bot(), Root: &newStore}
newStore.ChannelStore = &RetryLayerChannelStore{ChannelStore: childStore.Channel(), Root: &newStore}
newStore.ChannelBookmarkStore = &RetryLayerChannelBookmarkStore{ChannelBookmarkStore: childStore.ChannelBookmark(), Root: &newStore}
newStore.ChannelMemberHistoryStore = &RetryLayerChannelMemberHistoryStore{ChannelMemberHistoryStore: childStore.ChannelMemberHistory(), Root: &newStore}
newStore.ClusterDiscoveryStore = &RetryLayerClusterDiscoveryStore{ClusterDiscoveryStore: childStore.ClusterDiscovery(), Root: &newStore}
newStore.CommandStore = &RetryLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}

Просмотреть файл

@@ -20,6 +20,7 @@ func genStore() *mocks.Store {
mock.On("Bot").Return(&mocks.BotStore{})
mock.On("Channel").Return(&mocks.ChannelStore{})
mock.On("ChannelMemberHistory").Return(&mocks.ChannelMemberHistoryStore{})
mock.On("ChannelBookmark").Return(&mocks.ChannelBookmarkStore{})
mock.On("ClusterDiscovery").Return(&mocks.ClusterDiscoveryStore{})
mock.On("RemoteCluster").Return(&mocks.RemoteClusterStore{})
mock.On("Command").Return(&mocks.CommandStore{})
@@ -61,6 +62,7 @@ func genStore() *mocks.Store {
mock.On("PostPersistentNotification").Return(&mocks.PostPersistentNotificationStore{})
mock.On("TrueUpReview").Return(&mocks.TrueUpReviewStore{})
mock.On("DesktopTokens").Return(&mocks.DesktopTokensStore{})
mock.On("ChannelBookmark").Return(&mocks.ChannelBookmarkStore{})
return mock
}

Просмотреть файл

@@ -0,0 +1,380 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"strconv"
sq "github.com/mattermost/squirrel"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/utils"
"github.com/pkg/errors"
)
type SqlChannelBookmarkStore struct {
*SqlStore
}
func newSqlChannelBookmarkStore(sqlStore *SqlStore) store.ChannelBookmarkStore {
return &SqlChannelBookmarkStore{sqlStore}
}
func bookmarkWithFileInfoSliceColumns() []string {
return []string{
"cb.Id",
"cb.OwnerId",
"cb.ChannelId",
"cb.FileInfoId",
"cb.CreateAt",
"cb.UpdateAt",
"cb.DeleteAt",
"cb.DisplayName",
"cb.SortOrder",
"cb.LinkUrl",
"cb.ImageUrl",
"cb.Emoji",
"cb.Type",
"COALESCE(cb.OriginalId, '') as OriginalId",
"COALESCE(fi.Id, '') as FileId",
"COALESCE(fi.Name, '') as FileName",
"COALESCE(fi.Extension, '') as Extension",
"COALESCE(fi.Size, 0) as Size",
"COALESCE(fi.MimeType, '') as MimeType",
"COALESCE(fi.Width, 0) as Width",
"COALESCE(fi.Height, 0) as Height",
"COALESCE(fi.HasPreviewImage, false) as HasPreviewImage",
"COALESCE(fi.MiniPreview, '') as MiniPreview",
}
}
func (s *SqlChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileId string) error {
existingQuery := s.getSubQueryBuilder().
Select("FileInfoId").
From("ChannelBookmarks").
Where(sq.And{
sq.Eq{"FileInfoId": fileId},
sq.Eq{"DeleteAt": 0},
})
alreadyAttachedQuery := s.getQueryBuilder().
Select("COUNT(*)").
From("FileInfo").
Where(sq.Or{
sq.Expr("Id IN (?)", existingQuery),
sq.And{
sq.Or{
sq.NotEq{"PostId": ""},
sq.NotEq{"CreatorId": model.BookmarkFileOwner},
},
sq.Eq{"Id": fileId},
},
})
var attached int64
err := s.GetReplicaX().GetBuilder(&attached, alreadyAttachedQuery)
if err != nil {
return errors.Wrap(err, "unable_to_save_channel_bookmark")
}
if attached > 0 {
return store.NewErrInvalidInput("ChannelBookmarks", "FileInfoId", fileId)
}
return nil
}
func (s *SqlChannelBookmarkStore) Get(Id string, includeDeleted bool) (*model.ChannelBookmarkWithFileInfo, error) {
query := s.getQueryBuilder().
Select(bookmarkWithFileInfoSliceColumns()...).
From("ChannelBookmarks cb").
LeftJoin("FileInfo fi ON cb.FileInfoId = fi.Id").
Where(sq.Eq{"cb.Id": Id})
if !includeDeleted {
query = query.Where(sq.Eq{"cb.DeleteAt": 0})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_bookmark_getforchanneltsince_tosql")
}
bookmark := model.ChannelBookmarkAndFileInfo{}
if err := s.GetReplicaX().Get(&bookmark, queryString, args...); err != nil {
return nil, store.NewErrNotFound("ChannelBookmark", Id)
}
return bookmark.ToChannelBookmarkWithFileInfo(), nil
}
func (s *SqlChannelBookmarkStore) Save(bookmark *model.ChannelBookmark, increaseSortOrder bool) (b *model.ChannelBookmarkWithFileInfo, err error) {
bookmark.PreSave()
if err := bookmark.IsValid(); err != nil {
return nil, err
}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(transaction, &err)
var currentBookmarksCount int64
query := s.getQueryBuilder().
Select("COUNT(*) as count").
From("ChannelBookmarks").
Where(sq.Eq{"ChannelId": bookmark.ChannelId, "DeleteAt": 0})
err = transaction.GetBuilder(&currentBookmarksCount, query)
if err != nil {
return nil, errors.Wrap(err, "failed while getting the count of ChannelBookmarks")
}
if currentBookmarksCount >= model.MaxBookmarksPerChannel {
return nil, store.NewErrLimitExceeded("bookmarks_per_channel", int(currentBookmarksCount), "channelId="+bookmark.ChannelId)
}
if bookmark.FileId != "" {
err = s.ErrorIfBookmarkFileInfoAlreadyAttached(bookmark.FileId)
if err != nil {
return nil, errors.Wrap(err, "unable_to_save_channel_bookmark")
}
}
if increaseSortOrder {
var sortOrder int64
query := s.getQueryBuilder().
Select("COALESCE(MAX(SortOrder), -1) as SortOrder").
From("ChannelBookmarks").
Where(sq.Eq{"ChannelId": bookmark.ChannelId, "DeleteAt": 0})
err = transaction.GetBuilder(&sortOrder, query)
if err != nil {
return nil, errors.Wrap(err, "failed while getting the sortOrder from ChannelBookmarks")
}
bookmark.SortOrder = sortOrder + 1
}
sql, args, sqlErr := s.getQueryBuilder().
Insert("ChannelBookmarks").
Columns("Id", "CreateAt", "UpdateAt", "DeleteAt", "ChannelId", "OwnerId", "FileInfoId", "DisplayName", "SortOrder", "LinkUrl", "ImageUrl", "Emoji", "Type").
Values(bookmark.Id, bookmark.CreateAt, bookmark.UpdateAt, bookmark.DeleteAt, bookmark.ChannelId, bookmark.OwnerId, bookmark.FileId, bookmark.DisplayName, bookmark.SortOrder, bookmark.LinkUrl, bookmark.ImageUrl, bookmark.Emoji, bookmark.Type).
ToSql()
if sqlErr != nil {
return nil, errors.Wrap(err, "insert_channel_bookmark_to_sql")
}
if _, insertErr := transaction.Exec(sql, args...); insertErr != nil {
return nil, errors.Wrap(insertErr, "unable_to_save_channel_bookmark")
}
var fileInfo model.FileInfo
if bookmark.FileId != "" {
query, args, queryErr := s.getQueryBuilder().
Select("Id, Name, Extension, Size, MimeType, Width, Height, HasPreviewImage, MiniPreview").
From("FileInfo").
Where(sq.Eq{"Id": bookmark.FileId}).
ToSql()
if queryErr != nil {
return nil, errors.Wrap(queryErr, "channel_bookmark_get_file_info_to_sql")
}
if queryErr = transaction.Get(&fileInfo, query, args...); queryErr != nil {
return nil, errors.Wrap(queryErr, "unable_to_get_channel_bookmark_file_info")
}
}
err = transaction.Commit()
return bookmark.ToBookmarkWithFileInfo(&fileInfo), err
}
func (s *SqlChannelBookmarkStore) Update(bookmark *model.ChannelBookmark) error {
bookmark.PreUpdate()
if err := bookmark.IsValid(); err != nil {
return err
}
query, args, err := s.getQueryBuilder().
Update("ChannelBookmarks").
Set("DisplayName", bookmark.DisplayName).
Set("SortOrder", bookmark.SortOrder).
Set("LinkUrl", bookmark.LinkUrl).
Set("ImageUrl", bookmark.ImageUrl).
Set("Emoji", bookmark.Emoji).
Set("FileInfoId", bookmark.FileId).
Set("UpdateAt", bookmark.UpdateAt).
Where(sq.Eq{
"Id": bookmark.Id,
"DeleteAt": 0,
}).
ToSql()
if err != nil {
return errors.Wrap(err, "channel_bookmark_update_tosql")
}
res, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to update channel bookmark with id=%s", bookmark.Id)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return errors.Wrapf(err, "failed to get affected rows after updating bookmark with id=%s", bookmark.Id)
}
if rowsAffected == 0 {
return store.NewErrNotFound("ChannelBookmark", bookmark.Id)
}
return nil
}
func (s *SqlChannelBookmarkStore) UpdateSortOrder(bookmarkId, channelId string, newIndex int64) ([]*model.ChannelBookmarkWithFileInfo, error) {
now := model.GetMillis()
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(transaction, &err)
bookmarks, err := s.GetBookmarksForChannelSince(channelId, 0)
if err != nil {
return nil, err
}
if (int(newIndex) > len(bookmarks)-1) || newIndex < 0 {
return nil, store.NewErrInvalidInput("ChannelBookmark", "SortOrder", newIndex)
}
currentIndex := -1
var current *model.ChannelBookmarkWithFileInfo
for index, b := range bookmarks {
if b.Id == bookmarkId {
currentIndex = index
current = b
break
}
}
if currentIndex == -1 {
return nil, store.NewErrNotFound("ChannelBookmark", bookmarkId)
}
bookmarks = utils.RemoveElementFromSliceAtIndex(bookmarks, currentIndex)
bookmarks = utils.InsertElementToSliceAtIndex(bookmarks, current, int(newIndex))
caseStmt := sq.Case()
query := s.getQueryBuilder().
Update("ChannelBookmarks")
ids := []string{}
for index, b := range bookmarks {
b.SortOrder = int64(index)
caseStmt = caseStmt.When(sq.Eq{"Id": b.Id}, strconv.FormatInt(int64(index), 10))
ids = append(ids, b.Id)
}
query = query.Set("SortOrder", caseStmt)
query = query.Set("UpdateAt", now)
query = query.Where(sq.Eq{"Id": ids})
queryStr, args, queryErr := query.ToSql()
if queryErr != nil {
return nil, queryErr
}
if _, updateSortOrderErr := transaction.Exec(queryStr, args...); updateSortOrderErr != nil {
return nil, updateSortOrderErr
}
err = transaction.Commit()
return bookmarks, err
}
func (s *SqlChannelBookmarkStore) Delete(bookmarkId string, deleteFile bool) error {
now := model.GetMillis()
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return err
}
defer finalizeTransactionX(transaction, &err)
query, args, err := s.getQueryBuilder().
Update("ChannelBookmarks").
Set("DeleteAt", now).
Set("UpdateAt", now).
Where(sq.Eq{"Id": bookmarkId}).
ToSql()
if err != nil {
return errors.Wrap(err, "channel_bookmark_delete_tosql")
}
_, err = transaction.Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to delete channel bookmark with id=%s", bookmarkId)
}
if deleteFile {
fileIdQuery := s.getSubQueryBuilder().
Select("FileInfoId").
From("ChannelBookmarks").
Where(sq.And{
sq.Eq{"Id": bookmarkId},
sq.Eq{"DeleteAt": 0},
})
fileQuery, fileArgs, fileErr := s.getQueryBuilder().
Update("FileInfo").
Set("DeleteAt", now).
Set("UpdateAt", now).
Where(sq.Expr("Id IN (?)", fileIdQuery)).
ToSql()
if fileErr != nil {
return errors.Wrap(err, "channel_bookmark_delete_tosql")
}
_, err = transaction.Exec(fileQuery, fileArgs...)
if err != nil {
return errors.Wrapf(err, "failed to delete channel bookmark with id=%s", bookmarkId)
}
}
return transaction.Commit()
}
func (s *SqlChannelBookmarkStore) GetBookmarksForChannelSince(channelId string, since int64) ([]*model.ChannelBookmarkWithFileInfo, error) {
query := s.getQueryBuilder().
Select(bookmarkWithFileInfoSliceColumns()...).
From("ChannelBookmarks cb").
LeftJoin("FileInfo fi ON cb.FileInfoId = fi.Id").
Where(sq.Eq{"cb.ChannelId": channelId})
if since > 0 {
query = query.Where(sq.Or{
sq.GtOrEq{"cb.UpdateAt": since},
sq.GtOrEq{"cb.DeleteAt": since},
})
} else {
query = query.Where(sq.Eq{"cb.DeleteAt": 0})
}
query = query.
OrderBy("cb.SortOrder ASC").
OrderBy("cb.DeleteAt ASC").
Limit(model.MaxBookmarksPerChannel * 2) // limit to the double of the cap as an edge case
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_bookmark_getforchanneltsince_tosql")
}
bookmarkRows := []model.ChannelBookmarkAndFileInfo{}
bookmarks := []*model.ChannelBookmarkWithFileInfo{}
if err := s.GetReplicaX().Select(&bookmarkRows, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find bookmarks")
}
for _, bookmark := range bookmarkRows {
bookmarks = append(bookmarks, bookmark.ToChannelBookmarkWithFileInfo())
}
return bookmarks, nil
}

Просмотреть файл

@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"testing"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
)
func TestChannelBookmarkStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestChannelBookmarkStore)
}

Просмотреть файл

@@ -465,12 +465,12 @@ func (fs SqlFileInfoStore) PermanentDelete(rctx request.CTX, fileId string) erro
func (fs SqlFileInfoStore) PermanentDeleteBatch(rctx request.CTX, endTime int64, limit int64) (int64, error) {
var query string
if fs.DriverName() == "postgres" {
query = "DELETE from FileInfo WHERE Id = any (array (SELECT Id FROM FileInfo WHERE CreateAt < ? LIMIT ?))"
query = "DELETE from FileInfo WHERE Id = any (array (SELECT Id FROM FileInfo WHERE CreateAt < ? AND CreatorId != ? LIMIT ?))"
} else {
query = "DELETE from FileInfo WHERE CreateAt < ? LIMIT ?"
query = "DELETE from FileInfo WHERE CreateAt < ? AND CreatorId != ? LIMIT ?"
}
sqlResult, err := fs.GetMasterX().Exec(query, endTime, limit)
sqlResult, err := fs.GetMasterX().Exec(query, endTime, model.BookmarkFileOwner, limit)
if err != nil {
return 0, errors.Wrap(err, "failed to delete FileInfos in batch")
}

Просмотреть файл

@@ -111,6 +111,7 @@ type SqlStoreStores struct {
postPersistentNotification store.PostPersistentNotificationStore
trueUpReview store.TrueUpReviewStore
desktopTokens store.DesktopTokensStore
channelBookmarks store.ChannelBookmarkStore
}
type SqlStore struct {
@@ -236,6 +237,7 @@ func New(settings model.SqlSettings, logger mlog.LoggerIFace, metrics einterface
store.stores.postPersistentNotification = newSqlPostPersistentNotificationStore(store)
store.stores.trueUpReview = newSqlTrueUpReviewStore(store)
store.stores.desktopTokens = newSqlDesktopTokensStore(store, metrics)
store.stores.channelBookmarks = newSqlChannelBookmarkStore(store)
store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures()
@@ -1043,6 +1045,10 @@ func (ss *SqlStore) DesktopTokens() store.DesktopTokensStore {
return ss.stores.desktopTokens
}
func (ss *SqlStore) ChannelBookmark() store.ChannelBookmarkStore {
return ss.stores.channelBookmarks
}
func (ss *SqlStore) DropAllTables() {
if ss.DriverName() == model.DatabaseDriverPostgres {
ss.masterX.Exec(`DO

Просмотреть файл

@@ -91,6 +91,7 @@ type Store interface {
PostPersistentNotification() PostPersistentNotificationStore
TrueUpReview() TrueUpReviewStore
DesktopTokens() DesktopTokensStore
ChannelBookmark() ChannelBookmarkStore
}
type RetentionPolicyStore interface {
@@ -1033,6 +1034,16 @@ type TrueUpReviewStore interface {
Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error)
}
type ChannelBookmarkStore interface {
ErrorIfBookmarkFileInfoAlreadyAttached(fileId string) error
Get(Id string, includeDeleted bool) (b *model.ChannelBookmarkWithFileInfo, err error)
Save(bookmark *model.ChannelBookmark, increaseSortOrder bool) (b *model.ChannelBookmarkWithFileInfo, err error)
Update(bookmark *model.ChannelBookmark) error
UpdateSortOrder(bookmarkId, channelId string, newIndex int64) ([]*model.ChannelBookmarkWithFileInfo, error)
Delete(bookmarkId string, deleteFile bool) error
GetBookmarksForChannelSince(channelId string, since int64) ([]*model.ChannelBookmarkWithFileInfo, error)
}
// ChannelSearchOpts contains options for searching channels.
//
// NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records.

Просмотреть файл

@@ -0,0 +1,483 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
func find_bookmark(slice []*model.ChannelBookmarkWithFileInfo, id string) *model.ChannelBookmarkWithFileInfo {
for _, element := range slice {
if element.Id == id {
return element
}
}
return nil
}
func TestChannelBookmarkStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("SaveChannelBookmark", func(t *testing.T) { testSaveChannelBookmark(t, rctx, ss) })
t.Run("UpdateChannelBookmark", func(t *testing.T) { testUpdateChannelBookmark(t, rctx, ss) })
t.Run("UpdateSortOrderChannelBookmark", func(t *testing.T) { testUpdateSortOrderChannelBookmark(t, rctx, ss) })
t.Run("DeleteChannelBookmark", func(t *testing.T) { testDeleteChannelBookmark(t, rctx, ss) })
t.Run("GetChannelBookmark", func(t *testing.T) { testGetChannelBookmark(t, rctx, ss) })
}
func testSaveChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelId := model.NewId()
userId := model.NewId()
bookmark1 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Link bookmark test",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
file := &model.FileInfo{
Id: model.NewId(),
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
bookmark2 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "file bookmark test",
FileId: file.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
bookmark3 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "file already attached",
FileId: file.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
file2 := &model.FileInfo{
Id: model.NewId(),
CreatorId: userId,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
bookmark4 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "file already attached to a post",
FileId: file2.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
_, err := ss.FileInfo().Save(rctx, file)
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(rctx, file.Id)
_, err = ss.FileInfo().Save(rctx, file2)
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(rctx, file2.Id)
err = ss.FileInfo().AttachToPost(rctx, file2.Id, model.NewId(), channelId, userId)
require.NoError(t, err)
t.Run("save bookmarks", func(t *testing.T) {
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark1.ChannelId, bookmarkResp.ChannelId)
assert.Nil(t, bookmarkResp.FileInfo)
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark2.Clone(), true)
assert.NoError(t, err)
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark2.ChannelId, bookmarkResp.ChannelId)
assert.NotNil(t, bookmarkResp.FileInfo)
bookmarks, err := ss.ChannelBookmark().GetBookmarksForChannelSince(channelId, 0)
assert.NoError(t, err)
assert.Len(t, bookmarks, 2)
_, err = ss.ChannelBookmark().Save(bookmark3.Clone(), true)
assert.Error(t, err) // Error as the file is attached to a bookmark
_, err = ss.ChannelBookmark().Save(bookmark4.Clone(), true)
assert.Error(t, err) // Error as the file is attached to a post
})
}
func testUpdateChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelId := model.NewId()
userId := model.NewId()
bookmark1 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Link bookmark test",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
}
t.Run("update bookmark", func(t *testing.T) {
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
now := model.GetMillis()
bookmark2 := bookmarkResp.ChannelBookmark.Clone()
bookmark2.DisplayName = "Updated display name"
bookmark2.Emoji = ":smile:"
bookmark2.LinkUrl = "https://mattermost.com/about"
time.Sleep(time.Millisecond * 250)
err = ss.ChannelBookmark().Update(bookmark2.Clone())
assert.NoError(t, err)
bookmarks, err := ss.ChannelBookmark().GetBookmarksForChannelSince(channelId, now)
assert.NoError(t, err)
assert.Len(t, bookmarks, 1)
b := find_bookmark(bookmarks, bookmark2.Id)
assert.NotNil(t, b)
assert.Equal(t, b.DisplayName, bookmark2.DisplayName)
assert.Equal(t, b.Type, model.ChannelBookmarkLink)
assert.NotEmpty(t, b.Emoji)
assert.Equal(t, b.CreateAt, bookmark2.CreateAt)
assert.Greater(t, b.UpdateAt, bookmark2.UpdateAt)
err = ss.ChannelBookmark().Update(bookmark1.Clone())
assert.Error(t, err)
bookmark3 := bookmark2.Clone()
bookmark3.Type = model.ChannelBookmarkFile
err = ss.ChannelBookmark().Update(bookmark3)
assert.Error(t, err)
})
}
func testUpdateSortOrderChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelId := model.NewId()
userId := model.NewId()
bookmark0 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Bookmark 0",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
file := &model.FileInfo{
Id: model.NewId(),
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
bookmark1 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Bookmark 1",
FileId: file.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
_, err := ss.FileInfo().Save(rctx, file)
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(rctx, file.Id)
bookmark2 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Bookmark 2",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
}
bookmark3 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Bookmark 3",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
}
bookmark4 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Bookmark 4",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
}
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark0.Clone(), true)
assert.NoError(t, err)
bookmark0 = bookmarkResp.ChannelBookmark.Clone()
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark0.ChannelId, bookmarkResp.ChannelId)
assert.Nil(t, bookmarkResp.FileInfo)
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
bookmark1 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark2.Clone(), true)
assert.NoError(t, err)
bookmark2 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark3.Clone(), true)
assert.NoError(t, err)
bookmark3 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark4.Clone(), true)
assert.NoError(t, err)
bookmark4 = bookmarkResp.ChannelBookmark.Clone()
t.Run("change order of bookmarks first to last", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark0.Id, channelId, 4)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks last to first", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark0.Id, channelId, 0)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks first to third", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark0.Id, channelId, 2)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
// now reset order
ss.ChannelBookmark().UpdateSortOrder(bookmark0.Id, channelId, 0)
})
t.Run("change order of bookmarks second to third", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark1.Id, channelId, 2)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks third to second", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark1.Id, channelId, 1)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks last to previous last", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark4.Id, channelId, 3)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks last to second", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark3.Id, channelId, 1)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks error when new index is out of bounds", func(t *testing.T) {
var iiErr *store.ErrInvalidInput
_, err = ss.ChannelBookmark().UpdateSortOrder(bookmark3.Id, channelId, -1)
assert.Error(t, err)
assert.ErrorAs(t, err, &iiErr)
_, err = ss.ChannelBookmark().UpdateSortOrder(bookmark3.Id, channelId, 5)
assert.Error(t, err)
assert.ErrorAs(t, err, &iiErr)
})
t.Run("change order of bookmarks error when bookmark not found", func(t *testing.T) {
_, err = ss.ChannelBookmark().UpdateSortOrder(model.NewId(), channelId, 0)
assert.Error(t, err)
var nfErr *store.ErrNotFound
assert.ErrorAs(t, err, &nfErr)
})
}
func testDeleteChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelId := model.NewId()
userId := model.NewId()
bookmark1 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Link bookmark test",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
file := &model.FileInfo{
Id: model.NewId(),
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
bookmark2 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "file bookmark test",
FileId: file.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
_, err := ss.FileInfo().Save(rctx, file)
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(rctx, file.Id)
t.Run("delete bookmark", func(t *testing.T) {
now := model.GetMillis()
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
bookmark1 = bookmarkResp.ChannelBookmark.Clone()
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark1.ChannelId, bookmarkResp.ChannelId)
assert.Nil(t, bookmarkResp.FileInfo)
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark2.Clone(), true)
assert.NoError(t, err)
bookmark2 = bookmarkResp.ChannelBookmark.Clone()
err = ss.ChannelBookmark().Delete(bookmark2.Id, true)
assert.NoError(t, err)
bookmarks, err := ss.ChannelBookmark().GetBookmarksForChannelSince(channelId, now)
assert.NoError(t, err)
assert.Len(t, bookmarks, 2) // we have two as the deleted record also gets returned for sync'ing purposes
b := find_bookmark(bookmarks, bookmark2.Id)
assert.NotNil(t, b)
assert.Equal(t, bookmarks[0].Type, model.ChannelBookmarkLink)
})
}
func testGetChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelId := model.NewId()
userId := model.NewId()
bookmark1 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Link bookmark test",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
t.Run("get bookmark", func(t *testing.T) {
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
bookmark1 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, err = ss.ChannelBookmark().Get(bookmark1.Id, false)
assert.NoError(t, err)
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark1.ChannelId, bookmarkResp.ChannelId)
assert.Nil(t, bookmarkResp.FileInfo)
err = ss.ChannelBookmark().Delete(bookmark1.Id, true)
assert.NoError(t, err)
bookmarkResp, err = ss.ChannelBookmark().Get(bookmark1.Id, false)
assert.Error(t, err)
assert.Nil(t, bookmarkResp)
bookmarkResp, err = ss.ChannelBookmark().Get(bookmark1.Id, true)
assert.NoError(t, err)
assert.NotNil(t, bookmarkResp)
})
}

Просмотреть файл

@@ -615,16 +615,26 @@ func testFileInfoPermanentDeleteBatch(t *testing.T, rctx request.CTX, ss store.S
})
require.NoError(t, err)
bookmarkFile, err := ss.FileInfo().Save(rctx, &model.FileInfo{ // should not be deleted
PostId: postId,
ChannelId: channelId,
CreatorId: model.BookmarkFileOwner,
Path: "file.txt",
CreateAt: 1000,
})
defer ss.FileInfo().PermanentDelete(rctx, bookmarkFile.Id)
require.NoError(t, err)
postFiles, err := ss.FileInfo().GetForPost(postId, true, false, false)
require.NoError(t, err)
assert.Len(t, postFiles, 3)
assert.Len(t, postFiles, 4)
_, err = ss.FileInfo().PermanentDeleteBatch(rctx, 1500, 1000)
require.NoError(t, err)
postFiles, err = ss.FileInfo().GetForPost(postId, true, false, false)
require.NoError(t, err)
assert.Len(t, postFiles, 1)
assert.Len(t, postFiles, 2)
}
func testFileInfoPermanentDeleteByUser(t *testing.T, rctx request.CTX, ss store.Store) {

Просмотреть файл

@@ -0,0 +1,176 @@
// Code generated by mockery v2.23.2. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost/server/public/model"
mock "github.com/stretchr/testify/mock"
)
// ChannelBookmarkStore is an autogenerated mock type for the ChannelBookmarkStore type
type ChannelBookmarkStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: bookmarkId, deleteFile
func (_m *ChannelBookmarkStore) Delete(bookmarkId string, deleteFile bool) error {
ret := _m.Called(bookmarkId, deleteFile)
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(bookmarkId, deleteFile)
} else {
r0 = ret.Error(0)
}
return r0
}
// ErrorIfBookmarkFileInfoAlreadyAttached provides a mock function with given fields: fileId
func (_m *ChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileId string) error {
ret := _m.Called(fileId)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(fileId)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: Id, includeDeleted
func (_m *ChannelBookmarkStore) Get(Id string, includeDeleted bool) (*model.ChannelBookmarkWithFileInfo, error) {
ret := _m.Called(Id, includeDeleted)
var r0 *model.ChannelBookmarkWithFileInfo
var r1 error
if rf, ok := ret.Get(0).(func(string, bool) (*model.ChannelBookmarkWithFileInfo, error)); ok {
return rf(Id, includeDeleted)
}
if rf, ok := ret.Get(0).(func(string, bool) *model.ChannelBookmarkWithFileInfo); ok {
r0 = rf(Id, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelBookmarkWithFileInfo)
}
}
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(Id, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetBookmarksForChannelSince provides a mock function with given fields: channelId, since
func (_m *ChannelBookmarkStore) GetBookmarksForChannelSince(channelId string, since int64) ([]*model.ChannelBookmarkWithFileInfo, error) {
ret := _m.Called(channelId, since)
var r0 []*model.ChannelBookmarkWithFileInfo
var r1 error
if rf, ok := ret.Get(0).(func(string, int64) ([]*model.ChannelBookmarkWithFileInfo, error)); ok {
return rf(channelId, since)
}
if rf, ok := ret.Get(0).(func(string, int64) []*model.ChannelBookmarkWithFileInfo); ok {
r0 = rf(channelId, since)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelBookmarkWithFileInfo)
}
}
if rf, ok := ret.Get(1).(func(string, int64) error); ok {
r1 = rf(channelId, since)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: bookmark, increaseSortOrder
func (_m *ChannelBookmarkStore) Save(bookmark *model.ChannelBookmark, increaseSortOrder bool) (*model.ChannelBookmarkWithFileInfo, error) {
ret := _m.Called(bookmark, increaseSortOrder)
var r0 *model.ChannelBookmarkWithFileInfo
var r1 error
if rf, ok := ret.Get(0).(func(*model.ChannelBookmark, bool) (*model.ChannelBookmarkWithFileInfo, error)); ok {
return rf(bookmark, increaseSortOrder)
}
if rf, ok := ret.Get(0).(func(*model.ChannelBookmark, bool) *model.ChannelBookmarkWithFileInfo); ok {
r0 = rf(bookmark, increaseSortOrder)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelBookmarkWithFileInfo)
}
}
if rf, ok := ret.Get(1).(func(*model.ChannelBookmark, bool) error); ok {
r1 = rf(bookmark, increaseSortOrder)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: bookmark
func (_m *ChannelBookmarkStore) Update(bookmark *model.ChannelBookmark) error {
ret := _m.Called(bookmark)
var r0 error
if rf, ok := ret.Get(0).(func(*model.ChannelBookmark) error); ok {
r0 = rf(bookmark)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateSortOrder provides a mock function with given fields: bookmarkId, channelId, newIndex
func (_m *ChannelBookmarkStore) UpdateSortOrder(bookmarkId string, channelId string, newIndex int64) ([]*model.ChannelBookmarkWithFileInfo, error) {
ret := _m.Called(bookmarkId, channelId, newIndex)
var r0 []*model.ChannelBookmarkWithFileInfo
var r1 error
if rf, ok := ret.Get(0).(func(string, string, int64) ([]*model.ChannelBookmarkWithFileInfo, error)); ok {
return rf(bookmarkId, channelId, newIndex)
}
if rf, ok := ret.Get(0).(func(string, string, int64) []*model.ChannelBookmarkWithFileInfo); ok {
r0 = rf(bookmarkId, channelId, newIndex)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelBookmarkWithFileInfo)
}
}
if rf, ok := ret.Get(1).(func(string, string, int64) error); ok {
r1 = rf(bookmarkId, channelId, newIndex)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewChannelBookmarkStore interface {
mock.TestingT
Cleanup(func())
}
// NewChannelBookmarkStore creates a new instance of ChannelBookmarkStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewChannelBookmarkStore(t mockConstructorTestingTNewChannelBookmarkStore) *ChannelBookmarkStore {
mock := &ChannelBookmarkStore{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

Просмотреть файл

@@ -72,6 +72,22 @@ func (_m *Store) Channel() store.ChannelStore {
return r0
}
// ChannelBookmark provides a mock function with given fields:
func (_m *Store) ChannelBookmark() store.ChannelBookmarkStore {
ret := _m.Called()
var r0 store.ChannelBookmarkStore
if rf, ok := ret.Get(0).(func() store.ChannelBookmarkStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ChannelBookmarkStore)
}
}
return r0
}
// ChannelMemberHistory provides a mock function with given fields:
func (_m *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore {
ret := _m.Called()

Просмотреть файл

@@ -65,6 +65,7 @@ type Store struct {
PostPersistentNotificationStore mocks.PostPersistentNotificationStore
TrueUpReviewStore mocks.TrueUpReviewStore
DesktopTokensStore mocks.DesktopTokensStore
ChannelBookmarkStore mocks.ChannelBookmarkStore
}
func (s *Store) SetContext(context context.Context) { s.context = context }
@@ -110,13 +111,14 @@ func (s *Store) Draft() store.DraftStore { return &s.D
func (s *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return &s.ChannelMemberHistoryStore
}
func (s *Store) TrueUpReview() store.TrueUpReviewStore { return &s.TrueUpReviewStore }
func (s *Store) DesktopTokens() store.DesktopTokensStore { return &s.DesktopTokensStore }
func (s *Store) NotifyAdmin() store.NotifyAdminStore { return &s.NotifyAdminStore }
func (s *Store) Group() store.GroupStore { return &s.GroupStore }
func (s *Store) LinkMetadata() store.LinkMetadataStore { return &s.LinkMetadataStore }
func (s *Store) SharedChannel() store.SharedChannelStore { return &s.SharedChannelStore }
func (s *Store) PostPriority() store.PostPriorityStore { return &s.PostPriorityStore }
func (s *Store) ChannelBookmark() store.ChannelBookmarkStore { return &s.ChannelBookmarkStore }
func (s *Store) TrueUpReview() store.TrueUpReviewStore { return &s.TrueUpReviewStore }
func (s *Store) DesktopTokens() store.DesktopTokensStore { return &s.DesktopTokensStore }
func (s *Store) NotifyAdmin() store.NotifyAdminStore { return &s.NotifyAdminStore }
func (s *Store) Group() store.GroupStore { return &s.GroupStore }
func (s *Store) LinkMetadata() store.LinkMetadataStore { return &s.LinkMetadataStore }
func (s *Store) SharedChannel() store.SharedChannelStore { return &s.SharedChannelStore }
func (s *Store) PostPriority() store.PostPriorityStore { return &s.PostPriorityStore }
func (s *Store) PostAcknowledgement() store.PostAcknowledgementStore {
return &s.PostAcknowledgementStore
}
@@ -187,5 +189,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool {
&s.PostAcknowledgementStore,
&s.PostPersistentNotificationStore,
&s.DesktopTokensStore,
&s.ChannelBookmarkStore,
)
}

Просмотреть файл

@@ -22,6 +22,7 @@ type TimerLayer struct {
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelBookmarkStore store.ChannelBookmarkStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
@@ -78,6 +79,10 @@ func (s *TimerLayer) Channel() store.ChannelStore {
return s.ChannelStore
}
func (s *TimerLayer) ChannelBookmark() store.ChannelBookmarkStore {
return s.ChannelBookmarkStore
}
func (s *TimerLayer) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return s.ChannelMemberHistoryStore
}
@@ -261,6 +266,11 @@ type TimerLayerChannelStore struct {
Root *TimerLayer
}
type TimerLayerChannelBookmarkStore struct {
store.ChannelBookmarkStore
Root *TimerLayer
}
type TimerLayerChannelMemberHistoryStore struct {
store.ChannelMemberHistoryStore
Root *TimerLayer
@@ -2478,6 +2488,118 @@ func (s *TimerLayerChannelStore) UserBelongsToChannels(userID string, channelIds
return result, err
}
func (s *TimerLayerChannelBookmarkStore) Delete(bookmarkId string, deleteFile bool) error {
start := time.Now()
err := s.ChannelBookmarkStore.Delete(bookmarkId, deleteFile)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelBookmarkStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileId string) error {
start := time.Now()
err := s.ChannelBookmarkStore.ErrorIfBookmarkFileInfoAlreadyAttached(fileId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelBookmarkStore.ErrorIfBookmarkFileInfoAlreadyAttached", success, elapsed)
}
return err
}
func (s *TimerLayerChannelBookmarkStore) Get(Id string, includeDeleted bool) (*model.ChannelBookmarkWithFileInfo, error) {
start := time.Now()
result, err := s.ChannelBookmarkStore.Get(Id, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelBookmarkStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelBookmarkStore) GetBookmarksForChannelSince(channelId string, since int64) ([]*model.ChannelBookmarkWithFileInfo, error) {
start := time.Now()
result, err := s.ChannelBookmarkStore.GetBookmarksForChannelSince(channelId, since)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelBookmarkStore.GetBookmarksForChannelSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelBookmarkStore) Save(bookmark *model.ChannelBookmark, increaseSortOrder bool) (*model.ChannelBookmarkWithFileInfo, error) {
start := time.Now()
result, err := s.ChannelBookmarkStore.Save(bookmark, increaseSortOrder)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelBookmarkStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelBookmarkStore) Update(bookmark *model.ChannelBookmark) error {
start := time.Now()
err := s.ChannelBookmarkStore.Update(bookmark)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelBookmarkStore.Update", success, elapsed)
}
return err
}
func (s *TimerLayerChannelBookmarkStore) UpdateSortOrder(bookmarkId string, channelId string, newIndex int64) ([]*model.ChannelBookmarkWithFileInfo, error) {
start := time.Now()
result, err := s.ChannelBookmarkStore.UpdateSortOrder(bookmarkId, channelId, newIndex)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelBookmarkStore.UpdateSortOrder", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()
@@ -11965,6 +12087,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay
newStore.AuditStore = &TimerLayerAuditStore{AuditStore: childStore.Audit(), Root: &newStore}
newStore.BotStore = &TimerLayerBotStore{BotStore: childStore.Bot(), Root: &newStore}
newStore.ChannelStore = &TimerLayerChannelStore{ChannelStore: childStore.Channel(), Root: &newStore}
newStore.ChannelBookmarkStore = &TimerLayerChannelBookmarkStore{ChannelBookmarkStore: childStore.ChannelBookmark(), Root: &newStore}
newStore.ChannelMemberHistoryStore = &TimerLayerChannelMemberHistoryStore{ChannelMemberHistoryStore: childStore.ChannelMemberHistory(), Root: &newStore}
newStore.ClusterDiscoveryStore = &TimerLayerClusterDiscoveryStore{ClusterDiscoveryStore: childStore.ClusterDiscovery(), Root: &newStore}
newStore.CommandStore = &TimerLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}

Просмотреть файл

@@ -75,6 +75,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store {
systemStore.On("GetByName", model.MigrationKeyDeleteOrphanDrafts).Return(&model.System{Name: model.MigrationKeyDeleteOrphanDrafts, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddIPFilteringPermissions).Return(&model.System{Name: model.MigrationKeyAddIPFilteringPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddOutgoingOAuthConnectionsPermissions).Return(&model.System{Name: model.MigrationKeyAddOutgoingOAuthConnectionsPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddChannelBookmarksPermissions).Return(&model.System{Name: model.MigrationKeyAddChannelBookmarksPermissions, Value: "true"}, nil)
systemStore.On("GetByName", "CustomGroupAdminRoleCreationMigrationComplete").Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil)
systemStore.On("GetByName", "products_boards").Return(&model.System{Name: "products_boards", Value: "true"}, nil)
systemStore.On("GetByName", "elasticsearch_fix_channel_index_migration").Return(&model.System{Name: "elasticsearch_fix_channel_index_migration", Value: "true"}, nil)

Просмотреть файл

@@ -87,6 +87,19 @@ func StringSliceDiff(a, b []string) []string {
return result
}
func InsertElementToSliceAtIndex[T comparable](slice []T, element T, index int) []T {
if len(slice) == index {
return append(slice, element)
}
slice = append(slice[:index+1], slice[index:]...)
slice[index] = element
return slice
}
func RemoveElementFromSliceAtIndex[T comparable](slice []T, index int) []T {
return append(slice[:index], slice[index+1:]...)
}
func GetIPAddress(r *http.Request, trustedProxyIPHeader []string) string {
address := ""

Просмотреть файл

@@ -94,6 +94,10 @@ type Params struct {
IncludeChannelMemberCount string
OutgoingOAuthConnectionID string
//Bookmarks
ChannelBookmarkId string
BookmarksSince int64
// Cloud
InvoiceId string
}
@@ -146,6 +150,7 @@ func ParamsFromRequest(r *http.Request) *Params {
params.RemoteId = props["remote_id"]
params.InvoiceId = props["invoice_id"]
params.OutgoingOAuthConnectionID = props["outgoing_oauth_connection_id"]
params.ChannelBookmarkId = props["bookmark_id"]
params.Scope = query.Get("scope")
if val, err := strconv.Atoi(query.Get("page")); err != nil || val < 0 {
@@ -240,6 +245,12 @@ func ParamsFromRequest(r *http.Request) *Params {
params.FilterHasMember = query.Get("filter_has_member")
if val, err := strconv.ParseInt(query.Get("bookmarks_since"), 10, 64); err != nil || val < 0 {
params.BookmarksSince = 0
} else {
params.BookmarksSince = val
}
return params
}

Просмотреть файл

@@ -459,6 +459,48 @@ func TestParamsFromRequest(t *testing.T) {
LimitAfter: LimitDefault,
},
},
{
"include channel bookmarks",
mustURL("/?include_bookmarks=true"),
nil,
&Params{
BookmarksSince: 0,
LimitAfter: LimitDefault,
PerPage: PerPageDefault,
LogsPerPage: LogsPerPageDefault,
LimitBefore: LimitDefault,
},
},
{
"include channel bookmarks with negative bookmark since",
mustURL("/?include_bookmarks=true&bookmarks_since=-1"),
nil,
&Params{
BookmarksSince: 0,
LimitAfter: LimitDefault,
PerPage: PerPageDefault,
LogsPerPage: LogsPerPageDefault,
LimitBefore: LimitDefault,
},
},
{
"include channel bookmarks with bookmark since",
mustURL("/?include_bookmarks=true&bookmarks_since=123456789"),
nil,
&Params{
BookmarksSince: 123456789,
LimitAfter: LimitDefault,
PerPage: PerPageDefault,
LogsPerPage: LogsPerPageDefault,
LimitBefore: LimitDefault,
},
},
}
for _, testCase := range testCases {

Просмотреть файл

@@ -235,7 +235,18 @@ func (s *MmctlUnitTestSuite) TestResetPermissionsCmd() {
Permissions: []string{"view_foos", "delete_bars"},
}
expectedPermissions := []string{"manage_channel_roles", "use_group_mentions"}
expectedPermissions := []string{
"manage_channel_roles",
"use_group_mentions",
"add_bookmark_public_channel",
"edit_bookmark_public_channel",
"delete_bookmark_public_channel",
"order_bookmark_public_channel",
"add_bookmark_private_channel",
"edit_bookmark_private_channel",
"delete_bookmark_private_channel",
"order_bookmark_private_channel",
}
expectedPatch := &model.RolePatch{
Permissions: &expectedPermissions,
}

Просмотреть файл

@@ -231,6 +231,58 @@
"id": "api.channel.add_user_to_channel.type.app_error",
"translation": "Can not add user to this channel type."
},
{
"id": "api.channel.bookmark.channel_bookmark.license.error",
"translation": "Your license does not support channel bookmarks."
},
{
"id": "api.channel.bookmark.create_channel_bookmark.direct_or_group_channels.forbidden.app_error",
"translation": "User is not allowed to create a channel bookmark."
},
{
"id": "api.channel.bookmark.create_channel_bookmark.direct_or_group_channels_by_guests.forbidden.app_error",
"translation": "Failed to create the channel bookmark."
},
{
"id": "api.channel.bookmark.create_channel_bookmark.forbidden.app_error",
"translation": "Failed to create the channel bookmark."
},
{
"id": "api.channel.bookmark.delete_channel_bookmark.direct_or_group_channels.forbidden.app_error",
"translation": "Failed to delete the channel bookmark."
},
{
"id": "api.channel.bookmark.delete_channel_bookmark.direct_or_group_channels_by_guests.forbidden.app_error",
"translation": "Failed to delete the channel bookmark."
},
{
"id": "api.channel.bookmark.delete_channel_bookmark.forbidden.app_error",
"translation": "Failed to delete the channel bookmark."
},
{
"id": "api.channel.bookmark.update_channel_bookmark.direct_or_group_channels.forbidden.app_error",
"translation": "Failed to update the channel bookmark."
},
{
"id": "api.channel.bookmark.update_channel_bookmark.direct_or_group_channels_by_guests.forbidden.app_error",
"translation": "Failed to update the channel bookmark."
},
{
"id": "api.channel.bookmark.update_channel_bookmark.forbidden.app_error",
"translation": "Failed to update the channel bookmark."
},
{
"id": "api.channel.bookmark.update_channel_bookmark_sort_order.direct_or_group_channels.forbidden.app_error",
"translation": "Failed to update the channel bookmark's sort order."
},
{
"id": "api.channel.bookmark.update_channel_bookmark_sort_order.direct_or_group_channels_by_guests.forbidden.app_error",
"translation": "Failed to update the channel bookmark's sort order."
},
{
"id": "api.channel.bookmark.update_channel_bookmark_sort_order.forbidden.app_error",
"translation": "Failed to update the channel bookmark's sort order."
},
{
"id": "api.channel.change_channel_privacy.private_to_public",
"translation": "This channel has been converted to a Public Channel and can be joined by any team member."
@@ -4638,6 +4690,38 @@
"id": "app.channel.autofollow.app_error",
"translation": "Failed to update thread membership for mentioned user"
},
{
"id": "app.channel.bookmark.delete.app_error",
"translation": "Could not delete bookmark."
},
{
"id": "app.channel.bookmark.get.app_error",
"translation": "Could not get bookmark."
},
{
"id": "app.channel.bookmark.get_existing.app_err",
"translation": "Could not get existing bookmark to update."
},
{
"id": "app.channel.bookmark.save.app_error",
"translation": "Could not save bookmark."
},
{
"id": "app.channel.bookmark.update.app_error",
"translation": "Could not update bookmark."
},
{
"id": "app.channel.bookmark.update_sort.app_error",
"translation": "Could not sort the bookmark."
},
{
"id": "app.channel.bookmark.update_sort.invalid_input.app_error",
"translation": "Could not sort the bookmark. Invalid input."
},
{
"id": "app.channel.bookmark.update_sort.missing_bookmark.app_error",
"translation": "Could not sort the bookmark. Not found."
},
{
"id": "app.channel.clear_all_custom_role_assignments.select.app_error",
"translation": "Failed to retrieve the channel members."
@@ -8438,6 +8522,58 @@
"id": "model.channel.is_valid.update_at.app_error",
"translation": "Update at must be a valid time."
},
{
"id": "model.channel_bookmark.is_valid.channel_id.app_error",
"translation": "Invalid channel id."
},
{
"id": "model.channel_bookmark.is_valid.create_at.app_error",
"translation": "Create at must be a valid time."
},
{
"id": "model.channel_bookmark.is_valid.display_name.app_error",
"translation": "Display name missing."
},
{
"id": "model.channel_bookmark.is_valid.file_id.missing_or_invalid.app_error",
"translation": "File id is missing or invalid."
},
{
"id": "model.channel_bookmark.is_valid.id.app_error",
"translation": "Invalid Id."
},
{
"id": "model.channel_bookmark.is_valid.image_url.app_error",
"translation": "Invalid image url."
},
{
"id": "model.channel_bookmark.is_valid.link_file.app_error",
"translation": "Cannot set a link and a file in the same bookmark."
},
{
"id": "model.channel_bookmark.is_valid.link_url.missing_or_invalid.app_error",
"translation": "Link url is missing or invalid."
},
{
"id": "model.channel_bookmark.is_valid.original_id.app_error",
"translation": "Invalid original id."
},
{
"id": "model.channel_bookmark.is_valid.owner_id.app_error",
"translation": "Invalid owner id."
},
{
"id": "model.channel_bookmark.is_valid.parent_id.app_error",
"translation": "Invalid parent id."
},
{
"id": "model.channel_bookmark.is_valid.type.app_error",
"translation": "Invalid type."
},
{
"id": "model.channel_bookmark.is_valid.update_at.app_error",
"translation": "Update at must be a valid time."
},
{
"id": "model.channel_member.is_valid.channel_auto_follow_threads_value.app_error",
"translation": "Invalid channel-auto-follow-threads value."

322
server/public/model/channel_bookmark.go Обычный файл
Просмотреть файл

@@ -0,0 +1,322 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
)
type ChannelBookmarkType string
const (
ChannelBookmarkLink ChannelBookmarkType = "link"
ChannelBookmarkFile ChannelBookmarkType = "file"
BookmarkFileOwner = "bookmark"
MaxBookmarksPerChannel = 50
)
type ChannelBookmark struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
ChannelId string `json:"channel_id"`
OwnerId string `json:"owner_id"`
FileId string `json:"file_id"`
DisplayName string `json:"display_name"`
SortOrder int64 `json:"sort_order"`
LinkUrl string `json:"link_url,omitempty"`
ImageUrl string `json:"image_url,omitempty"`
Emoji string `json:"emoji,omitempty"`
Type ChannelBookmarkType `json:"type"`
OriginalId string `json:"original_id,omitempty"`
ParentId string `json:"parent_id,omitempty"`
}
func (o *ChannelBookmark) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": o.Id,
"create_at": o.CreateAt,
"update_at": o.UpdateAt,
"delete_at": o.DeleteAt,
"channel_id": o.ChannelId,
"owner_id": o.OwnerId,
"file_id": o.FileId,
"type": o.Type,
"original_id": o.OriginalId,
"parent_id": o.ParentId,
}
}
// Clone returns a shallow copy of the channel bookmark.
func (o *ChannelBookmark) Clone() *ChannelBookmark {
bCopy := *o
return &bCopy
}
// SetOriginal generates a new bookmark copying the data of the
// receiver bookmark, resets its timestamps and main ID, updates its
// OriginalId and sets the owner to the ID passed as a parameter
func (o *ChannelBookmark) SetOriginal(newOwnerId string) *ChannelBookmark {
bCopy := *o
bCopy.Id = ""
bCopy.CreateAt = 0
bCopy.DeleteAt = 0
bCopy.UpdateAt = 0
bCopy.OriginalId = o.Id
bCopy.OwnerId = newOwnerId
return &bCopy
}
func (o *ChannelBookmark) IsValid() *AppError {
if !IsValidId(o.Id) {
return NewAppError("ChannelBookmark.IsValid", "model.channel_bookmark.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if o.CreateAt == 0 {
return NewAppError("ChannelBookmark.IsValid", "model.channel_bookmark.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.UpdateAt == 0 {
return NewAppError("ChannelBookmark.IsValid", "model.channel_bookmark.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if !IsValidId(o.ChannelId) {
return NewAppError("ChannelBookmark.IsValid", "model.channel_bookmark.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(o.OwnerId) {
return NewAppError("ChannelBookmark.IsValid", "model.channel_bookmark.is_valid.owner_id.app_error", nil, "", http.StatusBadRequest)
}
if o.DisplayName == "" {
return NewAppError("ChannelBookmark.IsValid", "model.channel_bookmark.is_valid.display_name.app_error", nil, "", http.StatusBadRequest)
}
if !(o.Type == ChannelBookmarkFile || o.Type == ChannelBookmarkLink) {
return NewAppError("ChannelBookmark.IsValid", "model.channel_bookmark.is_valid.type.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.Type == ChannelBookmarkLink && (o.LinkUrl == "" || !IsValidHTTPURL(o.LinkUrl)) {
return NewAppError("ChannelBookmark.IsValid", "model.channel_bookmark.is_valid.link_url.missing_or_invalid.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.Type == ChannelBookmarkLink && o.ImageUrl != "" && !IsValidHTTPURL(o.ImageUrl) {
return NewAppError("ChannelBookmark.IsValid", "model.channel_bookmark.is_valid.image_url.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.Type == ChannelBookmarkFile && (o.FileId == "" || !IsValidId(o.FileId)) {
return NewAppError("ChannelBookmark.IsValid", "model.channel_bookmark.is_valid.file_id.missing_or_invalid.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.ImageUrl != "" && o.FileId != "" {
return NewAppError("ChannelBookmark.IsValid", "model.channel_bookmark.is_valid.link_file.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.OriginalId != "" && !IsValidId(o.OriginalId) {
return NewAppError("ChannelBookmark.IsValid", "model.channel_bookmark.is_valid.original_id.app_error", nil, "", http.StatusBadRequest)
}
if o.ParentId != "" && !IsValidId(o.ParentId) {
return NewAppError("ChannelBookmark.IsValid", "model.channel_bookmark.is_valid.parent_id.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (o *ChannelBookmark) PreSave() {
if o.Id == "" {
o.Id = NewId()
}
o.DisplayName = SanitizeUnicode(o.DisplayName)
if o.CreateAt == 0 {
o.CreateAt = GetMillis()
}
o.UpdateAt = o.CreateAt
}
func (o *ChannelBookmark) PreUpdate() {
o.UpdateAt = GetMillis()
o.DisplayName = SanitizeUnicode(o.DisplayName)
}
func (o *ChannelBookmark) ToBookmarkWithFileInfo(f *FileInfo) *ChannelBookmarkWithFileInfo {
bwf := ChannelBookmarkWithFileInfo{
ChannelBookmark: &ChannelBookmark{
Id: o.Id,
CreateAt: o.CreateAt,
UpdateAt: o.UpdateAt,
DeleteAt: o.DeleteAt,
ChannelId: o.ChannelId,
OwnerId: o.OwnerId,
FileId: o.FileId,
DisplayName: o.DisplayName,
SortOrder: o.SortOrder,
LinkUrl: o.LinkUrl,
ImageUrl: o.ImageUrl,
Emoji: o.Emoji,
Type: o.Type,
OriginalId: o.OriginalId,
ParentId: o.ParentId,
},
}
if f != nil && f.Id != "" {
bwf.FileInfo = f
}
return &bwf
}
type ChannelBookmarkPatch struct {
FileId *string `json:"file_id"`
DisplayName *string `json:"display_name"`
SortOrder *int64 `json:"sort_order"`
LinkUrl *string `json:"link_url,omitempty"`
ImageUrl *string `json:"image_url,omitempty"`
Emoji *string `json:"emoji,omitempty"`
}
func (o *ChannelBookmarkPatch) Auditable() map[string]interface{} {
return map[string]interface{}{
"file_id": o.FileId,
}
}
func (o *ChannelBookmark) Patch(patch *ChannelBookmarkPatch) {
if patch.FileId != nil {
o.FileId = *patch.FileId
}
if patch.DisplayName != nil {
o.DisplayName = *patch.DisplayName
}
if patch.SortOrder != nil {
o.SortOrder = *patch.SortOrder
}
if patch.LinkUrl != nil {
o.LinkUrl = *patch.LinkUrl
}
if patch.ImageUrl != nil {
o.ImageUrl = *patch.ImageUrl
}
if patch.Emoji != nil {
o.Emoji = *patch.Emoji
}
}
type ChannelBookmarkWithFileInfo struct {
*ChannelBookmark
FileInfo *FileInfo `json:"file,omitempty"`
}
func (o *ChannelBookmarkWithFileInfo) Auditable() map[string]interface{} {
a := o.ChannelBookmark.Auditable()
if o.FileInfo != nil {
a["file"] = o.FileInfo.Auditable()
}
return a
}
// Clone returns a shallow copy of the channel bookmark with file info.
func (o *ChannelBookmarkWithFileInfo) Clone() *ChannelBookmarkWithFileInfo {
bCopy := *o
return &bCopy
}
type ChannelWithBookmarks struct {
*Channel
Bookmarks []*ChannelBookmarkWithFileInfo `json:"bookmarks,omitempty"`
}
type ChannelWithTeamDataAndBookmarks struct {
*ChannelWithTeamData
Bookmarks []*ChannelBookmarkWithFileInfo `json:"bookmarks,omitempty"`
}
type UpdateChannelBookmarkResponse struct {
Updated *ChannelBookmarkWithFileInfo `json:"updated,omitempty"`
Deleted *ChannelBookmarkWithFileInfo `json:"deleted,omitempty"`
}
func (o *UpdateChannelBookmarkResponse) Auditable() map[string]any {
a := map[string]any{}
if o.Updated != nil {
a["updated"] = o.Updated.Auditable()
}
if o.Deleted != nil {
a["updated"] = o.Deleted.Auditable()
}
return a
}
type ChannelBookmarkAndFileInfo struct {
Id string
CreateAt int64
UpdateAt int64
DeleteAt int64
ChannelId string
OwnerId string
FileInfoId string
DisplayName string
SortOrder int64
LinkUrl string
ImageUrl string
Emoji string
Type ChannelBookmarkType
OriginalId string
ParentId string
FileId string
FileName string
Extension string
Size int64
MimeType string
Width int
Height int
HasPreviewImage bool
MiniPreview *[]byte
}
func (o *ChannelBookmarkAndFileInfo) ToChannelBookmarkWithFileInfo() *ChannelBookmarkWithFileInfo {
bwf := &ChannelBookmarkWithFileInfo{
ChannelBookmark: &ChannelBookmark{
Id: o.Id,
CreateAt: o.CreateAt,
UpdateAt: o.UpdateAt,
DeleteAt: o.DeleteAt,
ChannelId: o.ChannelId,
OwnerId: o.OwnerId,
FileId: o.FileInfoId,
DisplayName: o.DisplayName,
SortOrder: o.SortOrder,
LinkUrl: o.LinkUrl,
ImageUrl: o.ImageUrl,
Emoji: o.Emoji,
Type: o.Type,
OriginalId: o.OriginalId,
ParentId: o.ParentId,
},
}
if o.FileInfoId != "" && o.FileId != "" {
miniPreview := o.MiniPreview
if len(*miniPreview) == 0 {
miniPreview = nil
}
bwf.FileInfo = &FileInfo{
Id: o.FileId,
Name: o.FileName,
Extension: o.Extension,
Size: o.Size,
MimeType: o.MimeType,
Width: o.Width,
Height: o.Height,
HasPreviewImage: o.HasPreviewImage,
MiniPreview: miniPreview,
}
}
return bwf
}

544
server/public/model/channel_bookmark_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,544 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestChannelBookmarkIsValid(t *testing.T) {
testCases := []struct {
Description string
Bookmark *ChannelBookmark
ExpectedIsValid bool
}{
{
"nil bookmark",
&ChannelBookmark{},
false,
},
{
"bookmark without create at timestamp",
&ChannelBookmark{
Id: NewId(),
OwnerId: NewId(),
ChannelId: "",
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkLink,
CreateAt: 0,
UpdateAt: 3,
DeleteAt: 4,
},
false,
},
{
"bookmark without update at timestamp",
&ChannelBookmark{
Id: NewId(),
OwnerId: NewId(),
ChannelId: "",
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkLink,
CreateAt: 2,
UpdateAt: 0,
DeleteAt: 4,
},
false,
},
{
"bookmark with missing channel id",
&ChannelBookmark{
Id: NewId(),
OwnerId: NewId(),
ChannelId: "",
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkLink,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
false,
},
{
"bookmark with invalid channel id",
&ChannelBookmark{
Id: NewId(),
OwnerId: NewId(),
ChannelId: "invalid",
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkLink,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
false,
},
{
"bookmark with missing owner id",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: "",
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkLink,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
false,
},
{
"bookmark with invalid user id",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: "invalid",
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkLink,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
false,
},
{
"bookmark with missing displayname",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
FileId: "",
DisplayName: "",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkLink,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
false,
},
{
"bookmark with missing type",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: "",
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
false,
},
{
"bookmark with invalid type",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: "invalid",
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
false,
},
{
"bookmark of type link with missing link url",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkLink,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
false,
},
{
"bookmark of type link with invalid link url",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "invalid",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkLink,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
false,
},
{
"bookmark of type link with valid link url",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "https://mattermost.com",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkLink,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
true,
},
{
"bookmark of type link with empty image url",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "https://mattermost.com",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkLink,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
true,
},
{
"bookmark of type link with invalid image url",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "https://mattermost.com",
ImageUrl: "invalid",
Emoji: "",
Type: ChannelBookmarkLink,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
false,
},
{
"bookmark of type link with invalid image url",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "https://mattermost.com",
ImageUrl: "https://mattermost.com/some-image-without-extension", // we don't care if the URL is an actual image as the client should handle the error
Emoji: "",
Type: ChannelBookmarkLink,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
true,
},
{
"bookmark of type file with missing file id",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
FileId: "",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkFile,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
false,
},
{
"bookmark of type file with invalid file id",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
FileId: "invalid",
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkFile,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
false,
},
{
"bookmark of type file with valid file id",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
FileId: NewId(),
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkFile,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
},
true,
},
{
"bookmark of type file with invalid original id",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
FileId: NewId(),
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkFile,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
OriginalId: "invalid",
},
false,
},
{
"bookmark of type file with invalid parent id",
&ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
FileId: NewId(),
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkFile,
CreateAt: 2,
UpdateAt: 3,
DeleteAt: 4,
ParentId: "invalid",
},
false,
},
{
"bookmark of type link with a file Id attached",
&ChannelBookmark{
Id: NewId(),
OwnerId: NewId(),
ChannelId: "",
FileId: NewId(),
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "http://somelink",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkLink,
CreateAt: 0,
UpdateAt: 3,
DeleteAt: 0,
},
false,
},
{
"bookmark of type file with a url",
&ChannelBookmark{
Id: NewId(),
OwnerId: NewId(),
ChannelId: "",
FileId: NewId(),
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "http://somelink",
ImageUrl: "",
Emoji: "",
Type: ChannelBookmarkFile,
CreateAt: 0,
UpdateAt: 3,
DeleteAt: 0,
},
false,
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
if testCase.ExpectedIsValid {
require.Nil(t, testCase.Bookmark.IsValid())
} else {
require.NotNil(t, testCase.Bookmark.IsValid())
}
})
}
}
func TestChannelBookmarkPreSave(t *testing.T) {
bookmark := &ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "https://mattermost.com",
Type: ChannelBookmarkLink,
DeleteAt: 0,
}
originalBookmark := &ChannelBookmark{
Id: bookmark.Id,
ChannelId: bookmark.ChannelId,
OwnerId: bookmark.OwnerId,
DisplayName: bookmark.DisplayName,
SortOrder: bookmark.SortOrder,
LinkUrl: bookmark.LinkUrl,
Type: bookmark.Type,
DeleteAt: bookmark.DeleteAt,
}
bookmark.PreSave()
assert.NotEqual(t, 0, bookmark.CreateAt)
assert.NotEqual(t, 0, bookmark.UpdateAt)
originalBookmark.CreateAt = bookmark.CreateAt
originalBookmark.UpdateAt = bookmark.UpdateAt
assert.Equal(t, originalBookmark, bookmark)
}
func TestChannelBookmarkPreUpdate(t *testing.T) {
bookmark := &ChannelBookmark{
Id: NewId(),
ChannelId: NewId(),
OwnerId: NewId(),
DisplayName: "display name",
SortOrder: 0,
LinkUrl: "https://mattermost.com",
Type: ChannelBookmarkLink,
CreateAt: 2,
DeleteAt: 0,
}
originalBookmark := &ChannelBookmark{
Id: bookmark.Id,
ChannelId: bookmark.ChannelId,
OwnerId: bookmark.OwnerId,
DisplayName: bookmark.DisplayName,
SortOrder: bookmark.SortOrder,
LinkUrl: bookmark.LinkUrl,
Type: bookmark.Type,
DeleteAt: bookmark.DeleteAt,
}
bookmark.PreSave()
assert.NotEqual(t, 0, bookmark.UpdateAt)
originalBookmark.CreateAt = bookmark.CreateAt
originalBookmark.UpdateAt = bookmark.UpdateAt
assert.Equal(t, originalBookmark, bookmark)
bookmark.PreUpdate()
assert.Greater(t, bookmark.UpdateAt, originalBookmark.UpdateAt)
}
func TestChannelBookmarkPatch(t *testing.T) {
p := &ChannelBookmarkPatch{
DisplayName: NewString(NewId()),
SortOrder: NewInt64(1),
LinkUrl: NewString(NewId()),
}
b := ChannelBookmark{
Id: NewId(),
DisplayName: NewId(),
Type: ChannelBookmarkLink, // should not update
LinkUrl: NewId(),
}
b.Patch(p)
require.Empty(t, b.FileId)
require.Equal(t, *p.DisplayName, b.DisplayName)
require.Equal(t, *p.SortOrder, b.SortOrder)
require.Equal(t, *p.LinkUrl, b.LinkUrl)
require.Equal(t, ChannelBookmarkLink, b.Type)
}

Просмотреть файл

@@ -588,6 +588,14 @@ func (c *Client4) limitsRoute() string {
return "/limits"
}
func (c *Client4) bookmarksRoute(channelId string) string {
return c.channelRoute(channelId) + "/bookmarks"
}
func (c *Client4) bookmarkRoute(channelId, bookmarkId string) string {
return fmt.Sprintf(c.bookmarksRoute(channelId)+"/%v", bookmarkId)
}
func (c *Client4) DoAPIGet(ctx context.Context, url string, etag string) (*http.Response, error) {
return c.DoAPIRequest(ctx, http.MethodGet, c.APIURL+url, "", etag)
}
@@ -8917,3 +8925,85 @@ func (c *Client4) GetUserLimits(ctx context.Context) (*UserLimits, *Response, er
}
return &userLimits, BuildResponse(r), nil
}
// CreateChannelBookmark creates a channel bookmark based on the provided struct.
func (c *Client4) CreateChannelBookmark(ctx context.Context, channelBookmark *ChannelBookmark) (*ChannelBookmark, *Response, error) {
channelBookmarkJSON, err := json.Marshal(channelBookmark)
if err != nil {
return nil, nil, NewAppError("CreateChannelBookmark", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(ctx, c.bookmarksRoute(channelBookmark.ChannelId), channelBookmarkJSON)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var cb ChannelBookmark
if err := json.NewDecoder(r.Body).Decode(&cb); err != nil {
return nil, nil, NewAppError("CreateChannelBookmark", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &cb, BuildResponse(r), nil
}
// UpdateChannelBookmark updates a channel bookmark based on the provided struct.
func (c *Client4) UpdateChannelBookmark(ctx context.Context, channelId, bookmarkId string, patch *ChannelBookmarkPatch) (*UpdateChannelBookmarkResponse, *Response, error) {
buf, err := json.Marshal(patch)
if err != nil {
return nil, nil, NewAppError("UpdateChannelBookmark", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPatchBytes(ctx, c.bookmarkRoute(channelId, bookmarkId), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ucb UpdateChannelBookmarkResponse
if err := json.NewDecoder(r.Body).Decode(&ucb); err != nil {
return nil, nil, NewAppError("UpdateChannelBookmark", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &ucb, BuildResponse(r), nil
}
// UpdateChannelBookmarkSortOrder updates a channel bookmark's sort order based on the provided new index.
func (c *Client4) UpdateChannelBookmarkSortOrder(ctx context.Context, channelId, bookmarkId string, sortOrder int64) ([]*ChannelBookmarkWithFileInfo, *Response, error) {
buf, err := json.Marshal(sortOrder)
if err != nil {
return nil, nil, NewAppError("UpdateChannelBookmarkSortOrder", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(ctx, c.bookmarkRoute(channelId, bookmarkId)+"/sort_order", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var b []*ChannelBookmarkWithFileInfo
if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
return nil, nil, NewAppError("UpdateChannelBookmarkSortOrder", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return b, BuildResponse(r), nil
}
// DeleteChannelBookmark deletes a channel bookmark.
func (c *Client4) DeleteChannelBookmark(ctx context.Context, channelId, bookmarkId string) (*ChannelBookmarkWithFileInfo, *Response, error) {
r, err := c.DoAPIDelete(ctx, c.bookmarkRoute(channelId, bookmarkId))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var b *ChannelBookmarkWithFileInfo
if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
return nil, nil, NewAppError("DeleteChannelBookmark", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return b, BuildResponse(r), nil
}
func (c *Client4) ListChannelBookmarksForChannel(ctx context.Context, channelId string, since int64) ([]*ChannelBookmarkWithFileInfo, *Response, error) {
query := fmt.Sprintf("?bookmarks_since=%v", since)
r, err := c.DoAPIGet(ctx, c.bookmarksRoute(channelId)+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var b []*ChannelBookmarkWithFileInfo
if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
return nil, nil, NewAppError("ListChannelBookmarksForChannel", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return b, BuildResponse(r), nil
}

Просмотреть файл

@@ -49,10 +49,11 @@ type FeatureFlags struct {
CloudIPFiltering bool
ConsumePostHook bool
CloudAnnualRenewals bool
CloudAnnualRenewals bool
CloudDedicatedExportUI bool
ChannelBookmarks bool
WebSocketEventScope bool
}
@@ -74,6 +75,7 @@ func (f *FeatureFlags) SetDefaults() {
f.ConsumePostHook = false
f.CloudAnnualRenewals = false
f.CloudDedicatedExportUI = false
f.ChannelBookmarks = false
f.WebSocketEventScope = false
}

Просмотреть файл

@@ -97,7 +97,7 @@ func (fi *FileInfo) IsValid() *AppError {
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(fi.CreatorId) && fi.CreatorId != "nouser" {
if !IsValidId(fi.CreatorId) && (fi.CreatorId != "nouser" && fi.CreatorId != BookmarkFileOwner) {
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.user_id.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
}

Просмотреть файл

@@ -53,6 +53,13 @@ func TestFileInfoIsValid(t *testing.T) {
assert.NotNil(t, info.IsValid(), "empty Path isn't valid")
info.Path = "fake/path.png"
})
t.Run("Creator ID for bookmarks is valid", func(t *testing.T) {
creatorId := info.CreatorId
info.CreatorId = BookmarkFileOwner
assert.Nil(t, info.IsValid(), "creatorId isn't valid")
info.CreatorId = creatorId
})
}
func TestFileInfoIsImage(t *testing.T) {

Просмотреть файл

@@ -47,4 +47,5 @@ const (
MigrationKeyDeleteOrphanDrafts = "delete_orphan_drafts_migration"
MigrationKeyAddIPFilteringPermissions = "add_ip_filtering_permissions"
MigrationKeyAddOutgoingOAuthConnectionsPermissions = "add_outgoing_oauth_connections_permissions"
MigrationKeyAddChannelBookmarksPermissions = "add_channel_bookmarks_permissions"
)

Просмотреть файл

@@ -109,6 +109,14 @@ var PermissionPromoteGuest *Permission
var PermissionDemoteToGuest *Permission
var PermissionUseChannelMentions *Permission
var PermissionUseGroupMentions *Permission
var PermissionAddBookmarkPublicChannel *Permission
var PermissionEditBookmarkPublicChannel *Permission
var PermissionDeleteBookmarkPublicChannel *Permission
var PermissionOrderBookmarkPublicChannel *Permission
var PermissionAddBookmarkPrivateChannel *Permission
var PermissionEditBookmarkPrivateChannel *Permission
var PermissionDeleteBookmarkPrivateChannel *Permission
var PermissionOrderBookmarkPrivateChannel *Permission
var PermissionReadOtherUsersTeams *Permission
var PermissionEditBrand *Permission
var PermissionManageSharedChannels *Permission
@@ -389,6 +397,7 @@ var SysconsoleReadPermissions []*Permission
var SysconsoleWritePermissions []*Permission
var PermissionManageOutgoingOAuthConnections *Permission
var ModeratedBookmarkPermissions []*Permission
func initializePermissions() {
PermissionInviteUser = &Permission{
@@ -1177,6 +1186,57 @@ func initializePermissions() {
"authentication.permissions.use_group_mentions.description",
PermissionScopeChannel,
}
// Channel bookmarks
PermissionAddBookmarkPublicChannel = &Permission{
"add_bookmark_public_channel",
"",
"",
PermissionScopeChannel,
}
PermissionEditBookmarkPublicChannel = &Permission{
"edit_bookmark_public_channel",
"",
"",
PermissionScopeChannel,
}
PermissionDeleteBookmarkPublicChannel = &Permission{
"delete_bookmark_public_channel",
"",
"",
PermissionScopeChannel,
}
PermissionOrderBookmarkPublicChannel = &Permission{
"order_bookmark_public_channel",
"",
"",
PermissionScopeChannel,
}
PermissionAddBookmarkPrivateChannel = &Permission{
"add_bookmark_private_channel",
"",
"",
PermissionScopeChannel,
}
PermissionEditBookmarkPrivateChannel = &Permission{
"edit_bookmark_private_channel",
"",
"",
PermissionScopeChannel,
}
PermissionDeleteBookmarkPrivateChannel = &Permission{
"delete_bookmark_private_channel",
"",
"",
PermissionScopeChannel,
}
PermissionOrderBookmarkPrivateChannel = &Permission{
"order_bookmark_private_channel",
"",
"",
PermissionScopeChannel,
}
PermissionReadOtherUsersTeams = &Permission{
"read_other_users_teams",
"authentication.permissions.read_other_users_teams.name",
@@ -2386,6 +2446,14 @@ func initializePermissions() {
PermissionDeleteOthersPosts,
PermissionUseChannelMentions,
PermissionUseGroupMentions,
PermissionAddBookmarkPublicChannel,
PermissionEditBookmarkPublicChannel,
PermissionDeleteBookmarkPublicChannel,
PermissionOrderBookmarkPublicChannel,
PermissionAddBookmarkPrivateChannel,
PermissionEditBookmarkPrivateChannel,
PermissionDeleteBookmarkPrivateChannel,
PermissionOrderBookmarkPrivateChannel,
}
GroupScopedPermissions := []*Permission{
@@ -2454,6 +2522,7 @@ func initializePermissions() {
"create_reactions",
"manage_members",
PermissionUseChannelMentions.Id,
"manage_bookmarks",
}
ChannelModeratedPermissionsMap = map[string]string{
@@ -2464,6 +2533,21 @@ func initializePermissions() {
PermissionManagePrivateChannelMembers.Id: ChannelModeratedPermissions[2],
PermissionUseChannelMentions.Id: ChannelModeratedPermissions[3],
}
ModeratedBookmarkPermissions = []*Permission{
PermissionAddBookmarkPublicChannel,
PermissionEditBookmarkPublicChannel,
PermissionDeleteBookmarkPublicChannel,
PermissionOrderBookmarkPublicChannel,
PermissionAddBookmarkPrivateChannel,
PermissionEditBookmarkPrivateChannel,
PermissionDeleteBookmarkPrivateChannel,
PermissionOrderBookmarkPrivateChannel,
}
for _, mbp := range ModeratedBookmarkPermissions {
ChannelModeratedPermissionsMap[mbp.Id] = ChannelModeratedPermissions[4]
}
}
func init() {

Просмотреть файл

@@ -582,6 +582,15 @@ func ChannelModeratedPermissionsChangedByPatch(role *Role, patch *RolePatch) []s
return result
}
func isModeratedBookmarkPermission(permission string) bool {
for _, mbp := range ModeratedBookmarkPermissions {
if mbp.Id == permission {
return true
}
}
return false
}
// GetChannelModeratedPermissions returns a map of channel moderated permissions that the role has access to
func (r *Role) GetChannelModeratedPermissions(channelType ChannelType) map[string]bool {
moderatedPermissions := make(map[string]bool)
@@ -597,11 +606,22 @@ func (r *Role) GetChannelModeratedPermissions(channelType ChannelType) map[strin
}
if moderated == permission {
// Special case where the channel moderated permission for `manage_members` is different depending on whether the channel is private or public
// Special case where the channel moderated permission for `manage_members` is different depending
// on whether the channel is private or public
if moderated == PermissionManagePublicChannelMembers.Id || moderated == PermissionManagePrivateChannelMembers.Id {
canManagePublic := channelType == ChannelTypeOpen && moderated == PermissionManagePublicChannelMembers.Id
canManagePrivate := channelType == ChannelTypePrivate && moderated == PermissionManagePrivateChannelMembers.Id
moderatedPermissions[moderatedPermissionValue] = canManagePublic || canManagePrivate
// Special case where the channel moderated permission for `manage_bookmarks` is different
// depending on whether the channel is private or public.
//
// Only AddBookmark is checked even if the permission includes four (add, delete, edit and
// order) as all of them are enabled or disabled in together
} else if isModeratedBookmarkPermission(moderated) {
canManagePublic := channelType == ChannelTypeOpen && moderated == PermissionAddBookmarkPublicChannel.Id
canManagePrivate := channelType == ChannelTypePrivate && moderated == PermissionAddBookmarkPrivateChannel.Id
moderatedPermissions[moderatedPermissionValue] = canManagePublic || canManagePrivate
} else {
moderatedPermissions[moderatedPermissionValue] = true
}
@@ -783,6 +803,14 @@ func MakeDefaultRoles() map[string]*Role {
PermissionManagePrivateChannelMembers.Id,
PermissionDeletePost.Id,
PermissionEditPost.Id,
PermissionAddBookmarkPublicChannel.Id,
PermissionEditBookmarkPublicChannel.Id,
PermissionDeleteBookmarkPublicChannel.Id,
PermissionOrderBookmarkPublicChannel.Id,
PermissionAddBookmarkPrivateChannel.Id,
PermissionEditBookmarkPrivateChannel.Id,
PermissionDeleteBookmarkPrivateChannel.Id,
PermissionOrderBookmarkPrivateChannel.Id,
},
SchemeManaged: true,
BuiltIn: true,
@@ -795,6 +823,14 @@ func MakeDefaultRoles() map[string]*Role {
Permissions: []string{
PermissionManageChannelRoles.Id,
PermissionUseGroupMentions.Id,
PermissionAddBookmarkPublicChannel.Id,
PermissionEditBookmarkPublicChannel.Id,
PermissionDeleteBookmarkPublicChannel.Id,
PermissionOrderBookmarkPublicChannel.Id,
PermissionAddBookmarkPrivateChannel.Id,
PermissionEditBookmarkPrivateChannel.Id,
PermissionDeleteBookmarkPrivateChannel.Id,
PermissionOrderBookmarkPrivateChannel.Id,
},
SchemeManaged: true,
BuiltIn: true,
@@ -873,6 +909,14 @@ func MakeDefaultRoles() map[string]*Role {
PermissionConvertPrivateChannelToPublic.Id,
PermissionDeletePost.Id,
PermissionDeleteOthersPosts.Id,
PermissionAddBookmarkPublicChannel.Id,
PermissionEditBookmarkPublicChannel.Id,
PermissionDeleteBookmarkPublicChannel.Id,
PermissionOrderBookmarkPublicChannel.Id,
PermissionAddBookmarkPrivateChannel.Id,
PermissionEditBookmarkPrivateChannel.Id,
PermissionDeleteBookmarkPrivateChannel.Id,
PermissionOrderBookmarkPrivateChannel.Id,
},
SchemeManaged: true,
BuiltIn: true,

Просмотреть файл

@@ -86,6 +86,10 @@ const (
WebsocketEventAcknowledgementRemoved WebsocketEventType = "post_acknowledgement_removed"
WebsocketEventPersistentNotificationTriggered WebsocketEventType = "persistent_notification_triggered"
WebsocketEventHostedCustomerSignupProgressUpdated WebsocketEventType = "hosted_customer_signup_progress_updated"
WebsocketEventChannelBookmarkCreated = "channel_bookmark_created"
WebsocketEventChannelBookmarkUpdated = "channel_bookmark_updated"
WebsocketEventChannelBookmarkDeleted = "channel_bookmark_deleted"
WebsocketEventChannelBookmarkSorted = "channel_bookmark_sorted"
WebsocketPresenceIndicator WebsocketEventType = "presence"
)