github.com/haalcala/mattermost-server-change-repo@v0.0.0-20210713015153-16753fbeee5f/plugin/helpers_bots.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package plugin 5 6 import ( 7 "io/ioutil" 8 "path/filepath" 9 10 "github.com/pkg/errors" 11 12 "github.com/mattermost/mattermost-server/v5/model" 13 "github.com/mattermost/mattermost-server/v5/utils" 14 ) 15 16 type ensureBotOptions struct { 17 ProfileImagePath string 18 IconImagePath string 19 } 20 21 type EnsureBotOption func(*ensureBotOptions) 22 23 func ProfileImagePath(path string) EnsureBotOption { 24 return func(args *ensureBotOptions) { 25 args.ProfileImagePath = path 26 } 27 } 28 29 func IconImagePath(path string) EnsureBotOption { 30 return func(args *ensureBotOptions) { 31 args.IconImagePath = path 32 } 33 } 34 35 // EnsureBot implements Helpers.EnsureBot 36 func (p *HelpersImpl) EnsureBot(bot *model.Bot, options ...EnsureBotOption) (retBotID string, retErr error) { 37 err := p.ensureServerVersion("5.10.0") 38 if err != nil { 39 return "", errors.Wrap(err, "failed to ensure bot") 40 } 41 42 // Default options 43 o := &ensureBotOptions{ 44 ProfileImagePath: "", 45 IconImagePath: "", 46 } 47 48 for _, setter := range options { 49 setter(o) 50 } 51 52 botID, err := p.ensureBot(bot) 53 if err != nil { 54 return "", err 55 } 56 57 err = p.setBotImages(botID, o.ProfileImagePath, o.IconImagePath) 58 if err != nil { 59 return "", err 60 } 61 return botID, nil 62 } 63 64 type ShouldProcessMessageOption func(*shouldProcessMessageOptions) 65 66 type shouldProcessMessageOptions struct { 67 AllowSystemMessages bool 68 AllowBots bool 69 AllowWebhook bool 70 FilterChannelIDs []string 71 FilterUserIDs []string 72 OnlyBotDMs bool 73 } 74 75 // AllowSystemMessages configures a call to ShouldProcessMessage to return true for system messages. 76 // 77 // As it is typically desirable only to consume messages from users of the system, ShouldProcessMessage ignores system messages by default. 78 func AllowSystemMessages() ShouldProcessMessageOption { 79 return func(options *shouldProcessMessageOptions) { 80 options.AllowSystemMessages = true 81 } 82 } 83 84 // AllowBots configures a call to ShouldProcessMessage to return true for bot posts. 85 // 86 // As it is typically desirable only to consume messages from human users of the system, ShouldProcessMessage ignores bot messages by default. When allowing bots, take care to avoid a loop where two plugins respond to each others posts repeatedly. 87 func AllowBots() ShouldProcessMessageOption { 88 return func(options *shouldProcessMessageOptions) { 89 options.AllowBots = true 90 } 91 } 92 93 // AllowWebhook configures a call to ShouldProcessMessage to return true for posts from webhook. 94 // 95 // As it is typically desirable only to consume messages from human users of the system, ShouldProcessMessage ignores webhook messages by default. 96 func AllowWebhook() ShouldProcessMessageOption { 97 return func(options *shouldProcessMessageOptions) { 98 options.AllowWebhook = true 99 } 100 } 101 102 // FilterChannelIDs configures a call to ShouldProcessMessage to return true only for the given channels. 103 // 104 // By default, posts from all channels are allowed to be processed. 105 func FilterChannelIDs(filterChannelIDs []string) ShouldProcessMessageOption { 106 return func(options *shouldProcessMessageOptions) { 107 options.FilterChannelIDs = filterChannelIDs 108 } 109 } 110 111 // FilterUserIDs configures a call to ShouldProcessMessage to return true only for the given users. 112 // 113 // By default, posts from all non-bot users are allowed. 114 func FilterUserIDs(filterUserIDs []string) ShouldProcessMessageOption { 115 return func(options *shouldProcessMessageOptions) { 116 options.FilterUserIDs = filterUserIDs 117 } 118 } 119 120 // OnlyBotDMs configures a call to ShouldProcessMessage to return true only for direct messages sent to the bot created by EnsureBot. 121 // 122 // By default, posts from all channels are allowed. 123 func OnlyBotDMs() ShouldProcessMessageOption { 124 return func(options *shouldProcessMessageOptions) { 125 options.OnlyBotDMs = true 126 } 127 } 128 129 // ShouldProcessMessage implements Helpers.ShouldProcessMessage 130 func (p *HelpersImpl) ShouldProcessMessage(post *model.Post, options ...ShouldProcessMessageOption) (bool, error) { 131 messageProcessOptions := &shouldProcessMessageOptions{} 132 for _, option := range options { 133 option(messageProcessOptions) 134 } 135 136 botIDBytes, kvGetErr := p.API.KVGet(BotUserKey) 137 if kvGetErr != nil { 138 return false, errors.Wrap(kvGetErr, "failed to get bot") 139 } 140 141 if botIDBytes != nil { 142 if post.UserId == string(botIDBytes) { 143 return false, nil 144 } 145 } 146 147 if post.IsSystemMessage() && !messageProcessOptions.AllowSystemMessages { 148 return false, nil 149 } 150 151 if !messageProcessOptions.AllowWebhook && post.GetProp("from_webhook") == "true" { 152 return false, nil 153 } 154 155 if !messageProcessOptions.AllowBots { 156 user, appErr := p.API.GetUser(post.UserId) 157 if appErr != nil { 158 return false, errors.Wrap(appErr, "unable to get user") 159 } 160 161 if user.IsBot { 162 return false, nil 163 } 164 } 165 166 if len(messageProcessOptions.FilterChannelIDs) != 0 && !utils.StringInSlice(post.ChannelId, messageProcessOptions.FilterChannelIDs) { 167 return false, nil 168 } 169 170 if len(messageProcessOptions.FilterUserIDs) != 0 && !utils.StringInSlice(post.UserId, messageProcessOptions.FilterUserIDs) { 171 return false, nil 172 } 173 174 if botIDBytes != nil && messageProcessOptions.OnlyBotDMs { 175 channel, appErr := p.API.GetChannel(post.ChannelId) 176 if appErr != nil { 177 return false, errors.Wrap(appErr, "unable to get channel") 178 } 179 180 if !model.IsBotDMChannel(channel, string(botIDBytes)) { 181 return false, nil 182 } 183 } 184 185 return true, nil 186 } 187 188 func (p *HelpersImpl) readFile(path string) ([]byte, error) { 189 bundlePath, err := p.API.GetBundlePath() 190 if err != nil { 191 return nil, errors.Wrap(err, "failed to get bundle path") 192 } 193 194 imageBytes, err := ioutil.ReadFile(filepath.Join(bundlePath, path)) 195 if err != nil { 196 return nil, errors.Wrap(err, "failed to read image") 197 } 198 return imageBytes, nil 199 } 200 201 func (p *HelpersImpl) ensureBot(bot *model.Bot) (retBotID string, retErr error) { 202 // Must provide a bot with a username 203 if bot == nil || len(bot.Username) < 1 { 204 return "", errors.New("passed a bad bot, nil or no username") 205 } 206 207 // If we fail for any reason, this could be a race between creation of bot and 208 // retrieval from another EnsureBot. Just try the basic retrieve existing again. 209 defer func() { 210 if retBotID == "" || retErr != nil { 211 var err error 212 var botIDBytes []byte 213 214 err = utils.ProgressiveRetry(func() error { 215 botIDBytes, err = p.API.KVGet(BotUserKey) 216 if err != nil { 217 return err 218 } 219 return nil 220 }) 221 222 if err == nil && botIDBytes != nil { 223 retBotID = string(botIDBytes) 224 retErr = nil 225 } 226 } 227 }() 228 229 botIDBytes, kvGetErr := p.API.KVGet(BotUserKey) 230 if kvGetErr != nil { 231 return "", errors.Wrap(kvGetErr, "failed to get bot") 232 } 233 234 // If the bot has already been created, use it 235 if botIDBytes != nil { 236 botID := string(botIDBytes) 237 238 // ensure existing bot is synced with what is being created 239 botPatch := &model.BotPatch{ 240 Username: &bot.Username, 241 DisplayName: &bot.DisplayName, 242 Description: &bot.Description, 243 } 244 245 if _, err := p.API.PatchBot(botID, botPatch); err != nil { 246 return "", errors.Wrap(err, "failed to patch bot") 247 } 248 249 return botID, nil 250 } 251 252 // Check for an existing bot user with that username. If one exists, then use that. 253 if user, userGetErr := p.API.GetUserByUsername(bot.Username); userGetErr == nil && user != nil { 254 if user.IsBot { 255 if kvSetErr := p.API.KVSet(BotUserKey, []byte(user.Id)); kvSetErr != nil { 256 p.API.LogWarn("Failed to set claimed bot user id.", "userid", user.Id, "err", kvSetErr) 257 } 258 } else { 259 p.API.LogError("Plugin attempted to use an account that already exists. Convert user to a bot account in the CLI by running 'mattermost user convert <username> --bot'. If the user is an existing user account you want to preserve, change its username and restart the Mattermost server, after which the plugin will create a bot account with that name. For more information about bot accounts, see https://mattermost.com/pl/default-bot-accounts", "username", bot.Username, "user_id", user.Id) 260 } 261 return user.Id, nil 262 } 263 264 // Create a new bot user for the plugin 265 createdBot, createBotErr := p.API.CreateBot(bot) 266 if createBotErr != nil { 267 return "", errors.Wrap(createBotErr, "failed to create bot") 268 } 269 270 if kvSetErr := p.API.KVSet(BotUserKey, []byte(createdBot.UserId)); kvSetErr != nil { 271 p.API.LogWarn("Failed to set created bot user id.", "userid", createdBot.UserId, "err", kvSetErr) 272 } 273 274 return createdBot.UserId, nil 275 } 276 277 func (p *HelpersImpl) setBotImages(botID, profileImagePath, iconImagePath string) error { 278 if profileImagePath != "" { 279 imageBytes, err := p.readFile(profileImagePath) 280 if err != nil { 281 return errors.Wrap(err, "failed to read profile image") 282 } 283 appErr := p.API.SetProfileImage(botID, imageBytes) 284 if appErr != nil { 285 return errors.Wrap(appErr, "failed to set profile image") 286 } 287 } 288 if iconImagePath != "" { 289 imageBytes, err := p.readFile(iconImagePath) 290 if err != nil { 291 return errors.Wrap(err, "failed to read icon image") 292 } 293 appErr := p.API.SetBotIconImage(botID, imageBytes) 294 if appErr != nil { 295 return errors.Wrap(appErr, "failed to set icon image") 296 } 297 } 298 return nil 299 }