diff --git a/README.md b/README.md index f239ad3d7..56d38ffd4 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ where - **TeamName**: The team for which the Welcome Bot sends a message for. Must be the team handle used in the URL, in lowercase. For example, in the following URL the **TeamName** value is `my-team`: https://example.com/my-team/channels/my-channel - **DelayInSeconds**: The number of seconds after joining a team that the user receives a welcome message. - **Message**: The message posted to the user. -- (Optional) **AttachmentMessage**: Message text in attachment containing user action buttons. +- (Optional) **AttachmentMessage**: Message text in attachment containing user action buttons. - (Optional) **Actions**: Use this to add new team members to channels automatically or based on which action button they pressed. - **ActionType**: One of `button` or `automatic`. When `button`: enables uses to select which types of channels they want to join. When `automatic`: the user is automatically added to the specified channels. - **ActionDisplayName**: Sets the display name for the user action buttons. @@ -73,6 +73,9 @@ The preview of the configured messages can be done via bot commands: * `/welcomebot help` - show a short usage information * `/welcomebot list` - lists the teams for which greetings were defined * `/welcomebot preview [team-name]` - sends ephemeral messages to the user calling the command, with the preview of the welcome message[s] for the given team name and the user that requested the preview +* `/welcomebot set_channel_welcome` - sets the given text as current's channel welcome message +* `/welcomebot get_channel_welcome` - gets the current's channel welcome message +* `/welcomebot delete_channel_welcome` - deletes the current's channel welcome message ## Example diff --git a/server/command.go b/server/command.go index e72773f0e..1d073055c 100644 --- a/server/command.go +++ b/server/command.go @@ -10,7 +10,11 @@ import ( ) const COMMAND_HELP = `* |/welcomebot preview [team-name] [user-name]| - preview the welcome message for the given team name. The current user's username will be used to render the template. -* |/welcomebot list| - list the teams for which welcome messages were defined` +* |/welcomebot list| - list the teams for which welcome messages were defined +* |/welcomebot set_channel_welcome [welcome-message]| - set the welcome message for the given channel. Direct channels are not supported. +* |/welcomebot get_channel_welcome| - print the welcome message set for the given channel (if any) +* |/welcomebot delete_channel_welcome| - delete the welcome message for the given channel (if any) +` func getCommand() *model.Command { return &model.Command{ @@ -18,16 +22,16 @@ func getCommand() *model.Command { DisplayName: "welcomebot", Description: "Welcome Bot helps add new team members to channels.", AutoComplete: true, - AutoCompleteDesc: "Available commands: preview, help", + AutoCompleteDesc: "Available commands: preview, help, list, set_channel_welcome, get_channel_welcome, delete_channel_welcome", AutoCompleteHint: "[command]", } } -func (p *Plugin) postCommandResponse(args *model.CommandArgs, text string) { +func (p *Plugin) postCommandResponse(args *model.CommandArgs, text string, textArgs ...interface{}) { post := &model.Post{ UserId: p.botUserID, ChannelId: args.ChannelId, - Message: text, + Message: fmt.Sprintf(text, textArgs...), } _ = p.API.SendEphemeralPost(args.UserId, post) } @@ -43,7 +47,144 @@ func (p *Plugin) hasSysadminRole(userId string) (bool, error) { return true, nil } -func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { +func (p *Plugin) validateCommand(action string, parameters []string) string { + switch action { + case "preview": + if len(parameters) != 1 { + return "Please specify a team, for which preview should be made." + } + case "list": + if len(parameters) > 0 { + return "List command does not accept any extra parameters" + } + case "set_channel_welcome": + if len(parameters) == 0 { + return "`set_channel_welcome` command requires the message to be provided" + } + case "get_channel_welcome": + if len(parameters) > 0 { + return "`get_channel_welcome` command does not accept any extra parameters" + } + case "delete_channel_welcome": + if len(parameters) > 0 { + return "`delete_channel_welcome` command does not accept any extra parameters" + } + } + + return "" +} + +func (p *Plugin) executeCommandPreview(teamName string, args *model.CommandArgs) { + found := false + for _, message := range p.getWelcomeMessages() { + if message.TeamName == teamName { + if err := p.previewWelcomeMessage(teamName, args, *message); err != nil { + p.postCommandResponse(args, "error occured while processing greeting for team `%s`: `%s`", teamName, err) + return + } + + found = true + } + } + + if !found { + p.postCommandResponse(args, "team `%s` has not been found", teamName) + } + + return +} + +func (p *Plugin) executeCommandList(args *model.CommandArgs) { + wecomeMessages := p.getWelcomeMessages() + + if len(wecomeMessages) == 0 { + p.postCommandResponse(args, "There are no welcome messages defined") + return + } + + // Deduplicate entries + teams := make(map[string]struct{}) + for _, message := range wecomeMessages { + teams[message.TeamName] = struct{}{} + } + + var str strings.Builder + str.WriteString("Teams for which welcome messages are defined:") + for team := range teams { + str.WriteString(fmt.Sprintf("\n * %s", team)) + } + p.postCommandResponse(args, str.String()) + return +} + +func (p *Plugin) executeCommandSetWelcome(args *model.CommandArgs) { + channelInfo, appErr := p.API.GetChannel(args.ChannelId) + if appErr != nil { + p.postCommandResponse(args, "error occured while checking the type of the chanelId `%s`: `%s`", args.ChannelId, appErr) + return + } + + if channelInfo.Type == model.CHANNEL_PRIVATE { + p.postCommandResponse(args, "welcome messages are not supported for direct channels") + return + } + + // strings.Fields will consume ALL whitespace, so plain re-joining of the + // parameters slice will not produce the same message + message := strings.SplitN(args.Command, "set_channel_welcome", 2)[1] + message = strings.TrimSpace(message) + + key := fmt.Sprintf("%s%s", WELCOMEBOT_CHANNEL_WELCOME_KEY, args.ChannelId) + if appErr := p.API.KVSet(key, []byte(message)); appErr != nil { + p.postCommandResponse(args, "error occured while storing the welcome message for the chanel: `%s`", appErr) + return + } + + p.postCommandResponse(args, "stored the welcome message:\n%s", message) + return +} + +func (p *Plugin) executeCommandGetWelcome(args *model.CommandArgs) { + key := fmt.Sprintf("%s%s", WELCOMEBOT_CHANNEL_WELCOME_KEY, args.ChannelId) + data, appErr := p.API.KVGet(key) + if appErr != nil { + p.postCommandResponse(args, "error occured while retrieving the welcome message for the chanel: `%s`", appErr) + return + } + + if data == nil { + p.postCommandResponse(args, "welcome message has not been set yet") + return + } + + p.postCommandResponse(args, "Welcome message is:\n%s", string(data)) + return +} + +func (p *Plugin) executeCommandDeleteWelcome(args *model.CommandArgs) { + key := fmt.Sprintf("%s%s", WELCOMEBOT_CHANNEL_WELCOME_KEY, args.ChannelId) + data, appErr := p.API.KVGet(key) + + if appErr != nil { + p.postCommandResponse(args, "error occured while retrieving the welcome message for the chanel: `%s`", appErr) + return + } + + if data == nil { + p.postCommandResponse(args, "welcome message has not been set yet") + return + } + + if appErr := p.API.KVDelete(key); appErr != nil { + p.postCommandResponse(args, "error occured while deleting the welcome message for the chanel: `%s`", appErr) + return + } + + p.postCommandResponse(args, "welcome message has been deleted") + return +} + +func (p *Plugin) ExecuteCommand(_ *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { split := strings.Fields(args.Command) command := split[0] parameters := []string{} @@ -59,9 +200,14 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo return &model.CommandResponse{}, nil } + if response := p.validateCommand(action, parameters); response != "" { + p.postCommandResponse(args, response) + return &model.CommandResponse{}, nil + } + isSysadmin, err := p.hasSysadminRole(args.UserId) if err != nil { - p.postCommandResponse(args, fmt.Sprintf("authorization failed: %v", err)) + p.postCommandResponse(args, "authorization failed: %s", err) return &model.CommandResponse{}, nil } if !isSysadmin { @@ -71,55 +217,20 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo switch action { case "preview": - if len(parameters) != 1 { - p.postCommandResponse(args, "Please specify a team, for which preview should be made.") - return &model.CommandResponse{}, nil - } - teamName := parameters[0] - - found := false - for _, message := range p.getWelcomeMessages() { - if message.TeamName == teamName { - if err := p.previewWelcomeMessage(teamName, args, *message); err != nil { - errMsg := fmt.Sprintf("error occured while processing greeting for team `%s`: `%s`", teamName, err) - p.postCommandResponse(args, errMsg) - return &model.CommandResponse{}, nil - } - - found = true - } - } - - if !found { - p.postCommandResponse(args, fmt.Sprintf("team `%s` has not been found", teamName)) - } + p.executeCommandPreview(teamName, args) return &model.CommandResponse{}, nil case "list": - if len(parameters) > 0 { - p.postCommandResponse(args, "List command does not accept any extra parameters") - return &model.CommandResponse{}, nil - } - - wecomeMessages := p.getWelcomeMessages() - - if len(wecomeMessages) == 0 { - p.postCommandResponse(args, "There are no welcome messages defined") - return &model.CommandResponse{}, nil - } - - // Deduplicate entries - teams := make(map[string]struct{}) - for _, message := range wecomeMessages { - teams[message.TeamName] = struct{}{} - } - - var str strings.Builder - str.WriteString("Teams for which welcome messages are defined:") - for team := range teams { - str.WriteString(fmt.Sprintf("\n * %s", team)) - } - p.postCommandResponse(args, str.String()) + p.executeCommandList(args) + return &model.CommandResponse{}, nil + case "set_channel_welcome": + p.executeCommandSetWelcome(args) + return &model.CommandResponse{}, nil + case "get_channel_welcome": + p.executeCommandGetWelcome(args) + return &model.CommandResponse{}, nil + case "delete_channel_welcome": + p.executeCommandDeleteWelcome(args) return &model.CommandResponse{}, nil case "help": fallthrough @@ -129,7 +240,6 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo return &model.CommandResponse{}, nil } - p.postCommandResponse(args, fmt.Sprintf("Unknown action %v", action)) - + p.postCommandResponse(args, "Unknown action %v", action) return &model.CommandResponse{}, nil } diff --git a/server/hooks.go b/server/hooks.go new file mode 100644 index 000000000..48809a7e2 --- /dev/null +++ b/server/hooks.go @@ -0,0 +1,90 @@ +package main + +import ( + "fmt" + "time" + + "github.com/mattermost/mattermost-server/v5/mlog" + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/plugin" +) + +// UserHasJoinedTeam is invoked after the membership has been committed to the database. If +// actor is not nil, the user was added to the team by the actor. +func (p *Plugin) UserHasJoinedTeam(c *plugin.Context, teamMember *model.TeamMember, actor *model.User) { + data := p.constructMessageTemplate(teamMember.UserId, teamMember.TeamId) + if data == nil { + return + } + + for _, message := range p.getWelcomeMessages() { + if message.TeamName == data.Team.Name { + go p.processWelcomeMessage(*data, *message) + } + } +} + +// UserHasJoinedChannel is invoked after the membership has been committed to +// the database. If actor is not nil, the user was invited to the channel by +// the actor. +func (p *Plugin) UserHasJoinedChannel(c *plugin.Context, channelMember *model.ChannelMember, _ *model.User) { + if channelInfo, appErr := p.API.GetChannel(channelMember.ChannelId); appErr != nil { + mlog.Error( + "error occured while checking the type of the chanel", + mlog.String("channelId", channelMember.ChannelId), + mlog.Err(appErr), + ) + return + } else if channelInfo.Type == model.CHANNEL_PRIVATE { + return + } + + key := fmt.Sprintf("%s%s", WELCOMEBOT_CHANNEL_WELCOME_KEY, channelMember.ChannelId) + data, appErr := p.API.KVGet(key) + if appErr != nil { + mlog.Error( + "error occured while retrieving the welcome message", + mlog.String("channelId", channelMember.ChannelId), + mlog.Err(appErr), + ) + return + } + + if data == nil { + // No welcome message for the given channel + return + } + + dmChannel, err := p.API.GetDirectChannel(channelMember.UserId, p.botUserID) + if err != nil { + mlog.Error( + "error occured while creating direct channel to the user", + mlog.String("UserId", channelMember.UserId), + mlog.Err(err), + ) + return + } + + // We send a DM and an opportunistic ephemeral message to the channel. See + // the discussion at the link below for more details: + // https://github.com/mattermost/mattermost-plugin-welcomebot/pull/31#issuecomment-611691023 + postDM := &model.Post{ + UserId: p.botUserID, + ChannelId: dmChannel.Id, + Message: string(data), + } + if _, appErr := p.API.CreatePost(postDM); appErr != nil { + mlog.Error("failed to post welcome message to the channel", + mlog.String("channelId", dmChannel.Id), + mlog.Err(appErr), + ) + } + + postChannel := &model.Post{ + UserId: p.botUserID, + ChannelId: channelMember.ChannelId, + Message: string(data), + } + time.Sleep(1 * time.Second) + _ = p.API.SendEphemeralPost(channelMember.UserId, postChannel) +} diff --git a/server/plugin.go b/server/plugin.go index 3186b19e4..1463e555a 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -12,6 +12,8 @@ const ( botUsername = "welcomebot" botDisplayName = "Welcomebot" botDescription = "A bot account created by the Welcomebot plugin." + + WELCOMEBOT_CHANNEL_WELCOME_KEY = "chanmsg_" ) // Plugin represents the welcome bot plugin diff --git a/server/team_hooks.go b/server/team_hooks.go deleted file mode 100644 index 743e92e28..000000000 --- a/server/team_hooks.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "github.com/mattermost/mattermost-server/v5/model" - "github.com/mattermost/mattermost-server/v5/plugin" -) - -// UserHasJoinedTeam is invoked after the membership has been committed to the database. If -// actor is not nil, the user was added to the team by the actor. -func (p *Plugin) UserHasJoinedTeam(c *plugin.Context, teamMember *model.TeamMember, actor *model.User) { - data := p.constructMessageTemplate(teamMember.UserId, teamMember.TeamId) - if data == nil { - return - } - - for _, message := range p.getWelcomeMessages() { - if message.TeamName == data.Team.Name { - go p.processWelcomeMessage(*data, *message) - } - } -}