Implement permissions specs for mentions. (#2758)

* Implement permissions specs for mentions.
- public boards: admin, editor, commenter can mention team members and auto-add them to board; guests can mention board members
- private boards:  admin, editor, commenter, guest can mention board members
- viewers cannot mention
This commit is contained in:
Doug Lauder 2022-04-13 18:09:55 -04:00 committed by GitHub
parent 4c61ae9623
commit 89cc947a21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 205 additions and 73 deletions

View file

@ -30,35 +30,43 @@ type notifyBackendParams struct {
cfg *config.Configuration cfg *config.Configuration
client *pluginapi.Client client *pluginapi.Client
permissions permissions.PermissionsService permissions permissions.PermissionsService
store store.Store
wsAdapter ws.Adapter
serverRoot string serverRoot string
logger *mlog.Logger logger *mlog.Logger
} }
func createMentionsNotifyBackend(params notifyBackendParams) (*notifymentions.Backend, error) { func createMentionsNotifyBackend(params notifyBackendParams) (*notifymentions.Backend, error) {
delivery, err := createDelivery(params.client, params.serverRoot, params.permissions) delivery, err := createDelivery(params.client, params.serverRoot)
if err != nil { if err != nil {
return nil, err return nil, err
} }
backend := notifymentions.New(delivery, params.permissions, params.logger) backendParams := notifymentions.BackendParams{
Store: params.store,
Permissions: params.permissions,
Delivery: delivery,
WSAdapter: params.wsAdapter,
Logger: params.logger,
}
backend := notifymentions.New(backendParams)
return backend, nil return backend, nil
} }
func createSubscriptionsNotifyBackend(params notifyBackendParams, store store.Store, func createSubscriptionsNotifyBackend(params notifyBackendParams) (*notifysubscriptions.Backend, error) {
wsPluginAdapter ws.PluginAdapterInterface) (*notifysubscriptions.Backend, error) { delivery, err := createDelivery(params.client, params.serverRoot)
//
delivery, err := createDelivery(params.client, params.serverRoot, params.permissions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
backendParams := notifysubscriptions.BackendParams{ backendParams := notifysubscriptions.BackendParams{
ServerRoot: params.serverRoot, ServerRoot: params.serverRoot,
Store: store, Store: params.store,
Permissions: params.permissions, Permissions: params.permissions,
Delivery: delivery, Delivery: delivery,
WSAdapter: wsPluginAdapter, WSAdapter: params.wsAdapter,
Logger: params.logger, Logger: params.logger,
NotifyFreqCardSeconds: params.cfg.NotifyFreqCardSeconds, NotifyFreqCardSeconds: params.cfg.NotifyFreqCardSeconds,
NotifyFreqBoardSeconds: params.cfg.NotifyFreqBoardSeconds, NotifyFreqBoardSeconds: params.cfg.NotifyFreqBoardSeconds,
@ -68,7 +76,7 @@ func createSubscriptionsNotifyBackend(params notifyBackendParams, store store.St
return backend, nil return backend, nil
} }
func createDelivery(client *pluginapi.Client, serverRoot string, permissions permissions.PermissionsService) (*plugindelivery.PluginDelivery, error) { func createDelivery(client *pluginapi.Client, serverRoot string) (*plugindelivery.PluginDelivery, error) {
bot := &model.Bot{ bot := &model.Bot{
Username: botUsername, Username: botUsername,
DisplayName: botDisplayname, DisplayName: botDisplayname,
@ -81,7 +89,7 @@ func createDelivery(client *pluginapi.Client, serverRoot string, permissions per
pluginAPI := &pluginAPIAdapter{client: client} pluginAPI := &pluginAPIAdapter{client: client}
return plugindelivery.New(botID, serverRoot, pluginAPI, permissions), nil return plugindelivery.New(botID, serverRoot, pluginAPI), nil
} }
type pluginAPIAdapter struct { type pluginAPIAdapter struct {

View file

@ -122,7 +122,9 @@ func (p *Plugin) OnActivate() error {
backendParams := notifyBackendParams{ backendParams := notifyBackendParams{
cfg: cfg, cfg: cfg,
client: client, client: client,
store: db,
permissions: permissionsService, permissions: permissionsService,
wsAdapter: p.wsPluginAdapter,
serverRoot: baseURL + "/boards", serverRoot: baseURL + "/boards",
logger: logger, logger: logger,
} }
@ -135,7 +137,7 @@ func (p *Plugin) OnActivate() error {
} }
notifyBackends = append(notifyBackends, mentionsBackend) notifyBackends = append(notifyBackends, mentionsBackend)
subscriptionsBackend, err2 := createSubscriptionsNotifyBackend(backendParams, db, p.wsPluginAdapter) subscriptionsBackend, err2 := createSubscriptionsNotifyBackend(backendParams)
if err2 != nil { if err2 != nil {
return fmt.Errorf("error creating subscription notifications backend: %w", err2) return fmt.Errorf("error creating subscription notifications backend: %w", err2)
} }
@ -209,7 +211,7 @@ func (p *Plugin) createBoardsConfig(mmconfig mmModel.Config, baseURL string, ser
featureFlags := parseFeatureFlags(mmconfig.FeatureFlags.ToMap()) featureFlags := parseFeatureFlags(mmconfig.FeatureFlags.ToMap())
return &config.Configuration{ return &config.Configuration{
ServerRoot: baseURL + "/plugins/focalboard", ServerRoot: baseURL,
Port: -1, Port: -1,
DBType: *mmconfig.SqlSettings.DriverName, DBType: *mmconfig.SqlSettings.DriverName,
DBConfigString: *mmconfig.SqlSettings.DataSource, DBConfigString: *mmconfig.SqlSettings.DataSource,

View file

@ -356,6 +356,15 @@ func (a *App) notifyBlockChanged(action notify.Action, block *model.Block, oldBl
return return
} }
boardMember, _ := a.GetMemberForBoard(board.ID, modifiedByID)
if boardMember == nil {
// create temporary guest board member
boardMember = &model.BoardMember{
BoardID: board.ID,
UserID: modifiedByID,
}
}
evt := notify.BlockChangeEvent{ evt := notify.BlockChangeEvent{
Action: action, Action: action,
TeamID: board.TeamID, TeamID: board.TeamID,
@ -363,7 +372,7 @@ func (a *App) notifyBlockChanged(action notify.Action, block *model.Block, oldBl
Card: card, Card: card,
BlockChanged: block, BlockChanged: block,
BlockOld: oldBlock, BlockOld: oldBlock,
ModifiedByID: modifiedByID, ModifiedBy: boardMember,
} }
a.notifications.BlockChanged(evt) a.notifications.BlockChanged(evt)
} }

View file

@ -240,6 +240,10 @@ func (a *App) GetMembersForUser(userID string) ([]*model.BoardMember, error) {
return a.store.GetMembersForUser(userID) return a.store.GetMembersForUser(userID)
} }
func (a *App) GetMemberForBoard(boardID string, userID string) (*model.BoardMember, error) {
return a.store.GetMemberForBoard(boardID, userID)
}
func (a *App) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error) { func (a *App) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error) {
board, err := a.store.GetBoard(member.BoardID) board, err := a.store.GetBoard(member.BoardID)
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {

View file

@ -3,11 +3,17 @@
package notifymentions package notifymentions
import "github.com/mattermost/focalboard/server/services/notify" import (
"github.com/mattermost/focalboard/server/services/notify"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
// MentionDelivery provides an interface for delivering @mention notifications to other systems, such as // MentionDelivery provides an interface for delivering @mention notifications to other systems, such as
// channels server via plugin API. // channels server via plugin API.
// On success the user id of the user mentioned is returned. // On success the user id of the user mentioned is returned.
type MentionDelivery interface { type MentionDelivery interface {
MentionDeliver(mentionUsername string, extract string, evt notify.BlockChangeEvent) (string, error) MentionDeliver(mentionedUser *mm_model.User, extract string, evt notify.BlockChangeEvent) (string, error)
UserByUsername(mentionUsername string) (*mm_model.User, error)
IsErrNotFound(err error) bool
} }

View file

@ -4,12 +4,14 @@
package notifymentions package notifymentions
import ( import (
"errors"
"fmt" "fmt"
"sync" "sync"
"github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/notify" "github.com/mattermost/focalboard/server/services/notify"
"github.com/mattermost/focalboard/server/services/permissions" "github.com/mattermost/focalboard/server/services/permissions"
"github.com/mattermost/focalboard/server/ws"
"github.com/wiggin77/merror" "github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/shared/mlog"
@ -19,25 +21,41 @@ const (
backendName = "notifyMentions" backendName = "notifyMentions"
) )
var (
ErrMentionPermission = errors.New("mention not permitted")
)
type MentionListener interface { type MentionListener interface {
OnMention(userID string, evt notify.BlockChangeEvent) OnMention(userID string, evt notify.BlockChangeEvent)
} }
type BackendParams struct {
Store Store
Permissions permissions.PermissionsService
Delivery MentionDelivery
WSAdapter ws.Adapter
Logger *mlog.Logger
}
// Backend provides the notification backend for @mentions. // Backend provides the notification backend for @mentions.
type Backend struct { type Backend struct {
delivery MentionDelivery store Store
permissions permissions.PermissionsService permissions permissions.PermissionsService
delivery MentionDelivery
wsAdapter ws.Adapter
logger *mlog.Logger logger *mlog.Logger
mux sync.RWMutex mux sync.RWMutex
listeners []MentionListener listeners []MentionListener
} }
func New(delivery MentionDelivery, permissions permissions.PermissionsService, logger *mlog.Logger) *Backend { func New(params BackendParams) *Backend {
return &Backend{ return &Backend{
delivery: delivery, store: params.Store,
permissions: permissions, permissions: params.Permissions,
logger: logger, delivery: params.Delivery,
wsAdapter: params.WSAdapter,
logger: params.Logger,
} }
} }
@ -110,7 +128,7 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
extract := extractText(evt.BlockChanged.Title, username, newLimits()) extract := extractText(evt.BlockChanged.Title, username, newLimits())
userID, err := b.delivery.MentionDeliver(username, extract, evt) userID, err := b.deliverMentionNotification(username, extract, evt)
if err != nil { if err != nil {
merr.Append(fmt.Errorf("cannot deliver notification for @%s: %w", username, err)) merr.Append(fmt.Errorf("cannot deliver notification for @%s: %w", username, err))
} }
@ -141,3 +159,79 @@ func safeCallListener(listener MentionListener, userID string, evt notify.BlockC
}() }()
listener.OnMention(userID, evt) listener.OnMention(userID, evt)
} }
func (b *Backend) deliverMentionNotification(username string, extract string, evt notify.BlockChangeEvent) (string, error) {
mentionedUser, err := b.delivery.UserByUsername(username)
if err != nil {
if b.delivery.IsErrNotFound(err) {
// not really an error; could just be someone typed "@sometext"
return "", nil
} else {
return "", fmt.Errorf("cannot lookup mentioned user: %w", err)
}
}
if evt.ModifiedBy == nil {
return "", fmt.Errorf("invalid user cannot mention: %w", ErrMentionPermission)
}
if evt.Board.Type == model.BoardTypeOpen {
// public board rules:
// - admin, editor, commenter: can mention anyone on team (mentioned users are automatically added to board)
// - guest: can mention board members
switch {
case evt.ModifiedBy.SchemeAdmin, evt.ModifiedBy.SchemeEditor, evt.ModifiedBy.SchemeCommenter:
if !b.permissions.HasPermissionToTeam(mentionedUser.Id, evt.TeamID, model.PermissionViewTeam) {
return "", fmt.Errorf("%s cannot mention non-team member %s : %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission)
}
// add mentioned user to board (if not already a member)
member, err := b.store.GetMemberForBoard(evt.Board.ID, mentionedUser.Id)
if member == nil || b.store.IsErrNotFound(err) {
// currently all memberships are created as editors by default
newBoardMember := &model.BoardMember{
UserID: mentionedUser.Id,
BoardID: evt.Board.ID,
SchemeEditor: true,
}
if member, err = b.store.SaveMember(newBoardMember); err != nil {
return "", fmt.Errorf("cannot add mentioned user %s to board %s: %w", mentionedUser.Id, evt.Board.ID, err)
}
b.logger.Debug("auto-added mentioned user to board",
mlog.String("user_id", mentionedUser.Id),
mlog.String("board_id", evt.Board.ID),
mlog.String("board_type", string(evt.Board.Type)),
)
b.wsAdapter.BroadcastMemberChange(evt.TeamID, evt.Board.ID, member)
} else {
b.logger.Debug("skipping auto-add mentioned user to board; already a member",
mlog.String("user_id", mentionedUser.Id),
mlog.String("board_id", evt.Board.ID),
mlog.String("board_type", string(evt.Board.Type)),
)
}
case evt.ModifiedBy.SchemeViewer:
// viewer should not have gotten this far since they cannot add text to a card
return "", fmt.Errorf("%s (viewer) cannot mention user %s: %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission)
default:
// this is a guest
if !b.permissions.HasPermissionToBoard(mentionedUser.Id, evt.Board.ID, model.PermissionViewBoard) {
return "", fmt.Errorf("%s cannot mention non-board member %s : %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission)
}
}
} else {
// private board rules:
// - admin, editor, commenter, guest: can mention board members
switch {
case evt.ModifiedBy.SchemeViewer:
// viewer should not have gotten this far since they cannot add text to a card
return "", fmt.Errorf("%s (viewer) cannot mention user %s: %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission)
default:
// everyone else can mention board members
if !b.permissions.HasPermissionToBoard(mentionedUser.Id, evt.Board.ID, model.PermissionViewBoard) {
return "", fmt.Errorf("%s cannot mention non-board member %s : %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission)
}
}
}
return b.delivery.MentionDeliver(mentionedUser, extract, evt)
}

View file

@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notifymentions
import "github.com/mattermost/focalboard/server/model"
type Store interface {
GetUserByID(userID string) (*model.User, error)
GetMemberForBoard(boardID, userID string) (*model.BoardMember, error)
SaveMember(bm *model.BoardMember) (*model.BoardMember, error)
CreateSubscription(sub *model.Subscription) (*model.Subscription, error)
IsErrNotFound(err error) bool
}

View file

@ -18,6 +18,7 @@ type Store interface {
GetUserByID(userID string) (*model.User, error) GetUserByID(userID string) (*model.User, error)
GetMemberForBoard(boardID, userID string) (*model.BoardMember, error) GetMemberForBoard(boardID, userID string) (*model.BoardMember, error)
SaveMember(bm *model.BoardMember) (*model.BoardMember, error)
CreateSubscription(sub *model.Subscription) (*model.Subscription, error) CreateSubscription(sub *model.Subscription) (*model.Subscription, error)
GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error) GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error)

View file

@ -102,7 +102,7 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
BlockType: model.TypeCard, BlockType: model.TypeCard,
BlockID: evt.BlockChanged.ID, BlockID: evt.BlockChanged.ID,
SubscriberType: model.SubTypeUser, SubscriberType: model.SubTypeUser,
SubscriberID: evt.ModifiedByID, SubscriberID: evt.ModifiedBy.UserID,
} }
if _, err = b.store.CreateSubscription(sub); err != nil { if _, err = b.store.CreateSubscription(sub); err != nil {
@ -119,7 +119,7 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
if err != nil { if err != nil {
merr.Append(fmt.Errorf("cannot fetch subscribers for board %s: %w", evt.Board.ID, err)) merr.Append(fmt.Errorf("cannot fetch subscribers for board %s: %w", evt.Board.ID, err))
} }
if err = b.notifySubscribers(subs, evt.Board.ID, model.TypeBoard, evt.ModifiedByID); err != nil { if err = b.notifySubscribers(subs, evt.Board.ID, model.TypeBoard, evt.ModifiedBy.UserID); err != nil {
merr.Append(fmt.Errorf("cannot notify board subscribers for board %s: %w", evt.Board.ID, err)) merr.Append(fmt.Errorf("cannot notify board subscribers for board %s: %w", evt.Board.ID, err))
} }
@ -132,7 +132,7 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
if err != nil { if err != nil {
merr.Append(fmt.Errorf("cannot fetch subscribers for card %s: %w", evt.Card.ID, err)) merr.Append(fmt.Errorf("cannot fetch subscribers for card %s: %w", evt.Card.ID, err))
} }
if err = b.notifySubscribers(subs, evt.Card.ID, model.TypeCard, evt.ModifiedByID); err != nil { if err = b.notifySubscribers(subs, evt.Card.ID, model.TypeCard, evt.ModifiedBy.UserID); err != nil {
merr.Append(fmt.Errorf("cannot notify card subscribers for card %s: %w", evt.Card.ID, err)) merr.Append(fmt.Errorf("cannot notify card subscribers for card %s: %w", evt.Card.ID, err))
} }
@ -142,7 +142,7 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
if err != nil { if err != nil {
merr.Append(fmt.Errorf("cannot fetch subscribers for block %s: %w", evt.BlockChanged.ID, err)) merr.Append(fmt.Errorf("cannot fetch subscribers for block %s: %w", evt.BlockChanged.ID, err))
} }
if err := b.notifySubscribers(subs, evt.BlockChanged.ID, evt.BlockChanged.Type, evt.ModifiedByID); err != nil { if err := b.notifySubscribers(subs, evt.BlockChanged.ID, evt.BlockChanged.Type, evt.ModifiedBy.UserID); err != nil {
merr.Append(fmt.Errorf("cannot notify block subscribers for block %s: %w", evt.BlockChanged.ID, err)) merr.Append(fmt.Errorf("cannot notify block subscribers for block %s: %w", evt.BlockChanged.ID, err))
} }
} }
@ -183,9 +183,14 @@ func (b *Backend) OnMention(userID string, evt notify.BlockChangeEvent) {
return return
} }
// TODO: Automatically add user to board? Fail and show UI? Waiting for PM decision. // user mentioned must be a board member to subscribe to card.
// Currently the subscription created below will only notify if the user is already if !b.permissions.HasPermissionToBoard(userID, evt.Board.ID, model.PermissionViewBoard) {
// a member of the board. b.logger.Debug("Not subscribing mentioned non-board member to card",
mlog.String("user_id", userID),
mlog.String("block_id", evt.BlockChanged.ID),
)
return
}
sub := &model.Subscription{ sub := &model.Subscription{
BlockType: model.TypeCard, BlockType: model.TypeCard,
@ -195,7 +200,6 @@ func (b *Backend) OnMention(userID string, evt notify.BlockChangeEvent) {
} }
var err error var err error
if sub, err = b.store.CreateSubscription(sub); err != nil { if sub, err = b.store.CreateSubscription(sub); err != nil {
b.logger.Warn("Cannot subscribe mentioned user to card", b.logger.Warn("Cannot subscribe mentioned user to card",
mlog.String("user_id", userID), mlog.String("user_id", userID),

View file

@ -4,51 +4,31 @@
package plugindelivery package plugindelivery
import ( import (
"errors"
"fmt" "fmt"
"github.com/mattermost/focalboard/server/services/notify" "github.com/mattermost/focalboard/server/services/notify"
"github.com/mattermost/focalboard/server/utils" "github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v6/model" mm_model "github.com/mattermost/mattermost-server/v6/model"
) )
var ( // MentionDeliver notifies a user they have been mentioned in a blockv ia the plugin API.
ErrMentionPermission = errors.New("mention not permitted") func (pd *PluginDelivery) MentionDeliver(mentionedUser *mm_model.User, extract string, evt notify.BlockChangeEvent) (string, error) {
) author, err := pd.api.GetUserByID(evt.ModifiedBy.UserID)
// MentionDeliver notifies a user they have been mentioned in a block.
func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string, evt notify.BlockChangeEvent) (string, error) {
user, err := userByUsername(pd.api, mentionUsername)
if err != nil {
if isErrNotFound(err) {
// not really an error; could just be someone typed "@sometext"
return "", nil
} else {
return "", fmt.Errorf("cannot lookup mentioned user: %w", err)
}
}
// make sure mentioned user has permissions to team.
if !pd.permissions.HasPermissionToTeam(user.Id, evt.TeamID, model.PermissionViewTeam) {
return "", fmt.Errorf("mentioned user %s not member of team %s: %w", user.Id, evt.TeamID, ErrMentionPermission)
}
author, err := pd.api.GetUserByID(evt.ModifiedByID)
if err != nil { if err != nil {
return "", fmt.Errorf("cannot find user: %w", err) return "", fmt.Errorf("cannot find user: %w", err)
} }
channel, err := pd.api.GetDirectChannel(user.Id, pd.botID) channel, err := pd.api.GetDirectChannel(mentionedUser.Id, pd.botID)
if err != nil { if err != nil {
return "", fmt.Errorf("cannot get direct channel: %w", err) return "", fmt.Errorf("cannot get direct channel: %w", err)
} }
link := utils.MakeCardLink(pd.serverRoot, evt.Board.TeamID, evt.Board.ID, evt.Card.ID) link := utils.MakeCardLink(pd.serverRoot, evt.Board.TeamID, evt.Board.ID, evt.Card.ID)
post := &model.Post{ post := &mm_model.Post{
UserId: pd.botID, UserId: pd.botID,
ChannelId: channel.Id, ChannelId: channel.Id,
Message: formatMessage(author.Username, extract, evt.Card.Title, link, evt.BlockChanged), Message: formatMessage(author.Username, extract, evt.Card.Title, link, evt.BlockChanged),
} }
return user.Id, pd.api.CreatePost(post) return mentionedUser.Id, pd.api.CreatePost(post)
} }

View file

@ -4,8 +4,6 @@
package plugindelivery package plugindelivery
import ( import (
"github.com/mattermost/focalboard/server/services/permissions"
mm_model "github.com/mattermost/mattermost-server/v6/model" mm_model "github.com/mattermost/mattermost-server/v6/model"
) )
@ -39,17 +37,21 @@ type PluginAPI interface {
// PluginDelivery provides ability to send notifications to direct message channels via Mattermost plugin API. // PluginDelivery provides ability to send notifications to direct message channels via Mattermost plugin API.
type PluginDelivery struct { type PluginDelivery struct {
botID string botID string
serverRoot string serverRoot string
api PluginAPI api PluginAPI
permissions permissions.PermissionsService
} }
func New(botID string, serverRoot string, api PluginAPI, permissions permissions.PermissionsService) *PluginDelivery { func New(botID string, serverRoot string, api PluginAPI) *PluginDelivery {
return &PluginDelivery{ return &PluginDelivery{
botID: botID, botID: botID,
serverRoot: serverRoot, serverRoot: serverRoot,
api: api, api: api,
permissions: permissions,
} }
} }
// IsErrNotFound returns true if `err` or one of its wrapped children are the `ErrNotFound`
// as defined in the plugin API.
func (pd *PluginDelivery) IsErrNotFound(err error) bool {
return pd.api.IsErrNotFound(err)
}

View file

@ -13,14 +13,14 @@ const (
usernameSpecialChars = ".-_ " usernameSpecialChars = ".-_ "
) )
func userByUsername(api PluginAPI, username string) (*mm_model.User, error) { func (pd *PluginDelivery) UserByUsername(username string) (*mm_model.User, error) {
// check for usernames that might have trailing punctuation // check for usernames that might have trailing punctuation
var user *mm_model.User var user *mm_model.User
var err error var err error
ok := true ok := true
trimmed := username trimmed := username
for ok { for ok {
user, err = api.GetUserByUsername(trimmed) user, err = pd.api.GetUserByUsername(trimmed)
if err != nil && !isErrNotFound(err) { if err != nil && !isErrNotFound(err) {
return nil, err return nil, err
} }

View file

@ -43,7 +43,8 @@ var (
) )
func Test_userByUsername(t *testing.T) { func Test_userByUsername(t *testing.T) {
delivery := newPlugAPIMock(mockUsers) pluginAPI := newPlugAPIMock(mockUsers)
delivery := New("bot_id", "server_root", pluginAPI)
tests := []struct { tests := []struct {
name string name string
@ -64,7 +65,7 @@ func Test_userByUsername(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := userByUsername(delivery, tt.uname) got, err := delivery.UserByUsername(tt.uname)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("userByUsername() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("userByUsername() error = %v, wantErr %v", err, tt.wantErr)
return return

View file

@ -27,7 +27,7 @@ type BlockChangeEvent struct {
Card *model.Block Card *model.Block
BlockChanged *model.Block BlockChanged *model.Block
BlockOld *model.Block BlockOld *model.Block
ModifiedByID string ModifiedBy *model.BoardMember
} }
type SubscriptionChangeNotifier interface { type SubscriptionChangeNotifier interface {

View file

@ -2,6 +2,7 @@ package utils
import ( import (
"context" "context"
"runtime/debug"
"sync/atomic" "sync/atomic"
"time" "time"
@ -119,7 +120,11 @@ func (cn *CallbackQueue) exec(f CallbackFunc) {
// don't let a panic in the callback exit the thread. // don't let a panic in the callback exit the thread.
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
cn.logger.Error("CallbackQueue callback panic", mlog.String("name", cn.name), mlog.Any("panic", r)) cn.logger.Error("CallbackQueue callback panic",
mlog.String("name", cn.name),
mlog.Any("panic", r),
mlog.String("stack", string(debug.Stack())),
)
} }
}() }()