Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GH-24] Implement channel welcome messages #31

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down
218 changes: 164 additions & 54 deletions server/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,28 @@ 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{
vespian marked this conversation as resolved.
Show resolved Hide resolved
Trigger: "welcomebot",
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)
}
Expand All @@ -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{}
Expand All @@ -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 {
Expand All @@ -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":
vespian marked this conversation as resolved.
Show resolved Hide resolved
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
Expand All @@ -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
}
90 changes: 90 additions & 0 deletions server/hooks.go
Original file line number Diff line number Diff line change
@@ -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),
vespian marked this conversation as resolved.
Show resolved Hide resolved
}
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)
}
2 changes: 2 additions & 0 deletions server/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading