Skip to content

Commit b24433b

Browse files
vespianPawel Rozlach
and
Pawel Rozlach
authored
[GH-24] Implement channel welcome messages (#31)
* Implement channel welcome messages * DRY up the command.go code by moving commands to separate functions * Review fixes #1 * Change keys prefix ordering * Switch back to sending ephemeral posts * Send an opportunistic ephemeral post and a DM Co-authored-by: Pawel Rozlach <[email protected]>
1 parent d4de6d5 commit b24433b

File tree

5 files changed

+260
-76
lines changed

5 files changed

+260
-76
lines changed

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ where
6161
- **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
6262
- **DelayInSeconds**: The number of seconds after joining a team that the user receives a welcome message.
6363
- **Message**: The message posted to the user.
64-
- (Optional) **AttachmentMessage**: Message text in attachment containing user action buttons.
64+
- (Optional) **AttachmentMessage**: Message text in attachment containing user action buttons.
6565
- (Optional) **Actions**: Use this to add new team members to channels automatically or based on which action button they pressed.
6666
- **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.
6767
- **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:
7373
* `/welcomebot help` - show a short usage information
7474
* `/welcomebot list` - lists the teams for which greetings were defined
7575
* `/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
76+
* `/welcomebot set_channel_welcome` - sets the given text as current's channel welcome message
77+
* `/welcomebot get_channel_welcome` - gets the current's channel welcome message
78+
* `/welcomebot delete_channel_welcome` - deletes the current's channel welcome message
7679

7780
## Example
7881

server/command.go

+164-54
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,28 @@ import (
1010
)
1111

1212
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.
13-
* |/welcomebot list| - list the teams for which welcome messages were defined`
13+
* |/welcomebot list| - list the teams for which welcome messages were defined
14+
* |/welcomebot set_channel_welcome [welcome-message]| - set the welcome message for the given channel. Direct channels are not supported.
15+
* |/welcomebot get_channel_welcome| - print the welcome message set for the given channel (if any)
16+
* |/welcomebot delete_channel_welcome| - delete the welcome message for the given channel (if any)
17+
`
1418

1519
func getCommand() *model.Command {
1620
return &model.Command{
1721
Trigger: "welcomebot",
1822
DisplayName: "welcomebot",
1923
Description: "Welcome Bot helps add new team members to channels.",
2024
AutoComplete: true,
21-
AutoCompleteDesc: "Available commands: preview, help",
25+
AutoCompleteDesc: "Available commands: preview, help, list, set_channel_welcome, get_channel_welcome, delete_channel_welcome",
2226
AutoCompleteHint: "[command]",
2327
}
2428
}
2529

26-
func (p *Plugin) postCommandResponse(args *model.CommandArgs, text string) {
30+
func (p *Plugin) postCommandResponse(args *model.CommandArgs, text string, textArgs ...interface{}) {
2731
post := &model.Post{
2832
UserId: p.botUserID,
2933
ChannelId: args.ChannelId,
30-
Message: text,
34+
Message: fmt.Sprintf(text, textArgs...),
3135
}
3236
_ = p.API.SendEphemeralPost(args.UserId, post)
3337
}
@@ -43,7 +47,144 @@ func (p *Plugin) hasSysadminRole(userId string) (bool, error) {
4347
return true, nil
4448
}
4549

46-
func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
50+
func (p *Plugin) validateCommand(action string, parameters []string) string {
51+
switch action {
52+
case "preview":
53+
if len(parameters) != 1 {
54+
return "Please specify a team, for which preview should be made."
55+
}
56+
case "list":
57+
if len(parameters) > 0 {
58+
return "List command does not accept any extra parameters"
59+
}
60+
case "set_channel_welcome":
61+
if len(parameters) == 0 {
62+
return "`set_channel_welcome` command requires the message to be provided"
63+
}
64+
case "get_channel_welcome":
65+
if len(parameters) > 0 {
66+
return "`get_channel_welcome` command does not accept any extra parameters"
67+
}
68+
case "delete_channel_welcome":
69+
if len(parameters) > 0 {
70+
return "`delete_channel_welcome` command does not accept any extra parameters"
71+
}
72+
}
73+
74+
return ""
75+
}
76+
77+
func (p *Plugin) executeCommandPreview(teamName string, args *model.CommandArgs) {
78+
found := false
79+
for _, message := range p.getWelcomeMessages() {
80+
if message.TeamName == teamName {
81+
if err := p.previewWelcomeMessage(teamName, args, *message); err != nil {
82+
p.postCommandResponse(args, "error occured while processing greeting for team `%s`: `%s`", teamName, err)
83+
return
84+
}
85+
86+
found = true
87+
}
88+
}
89+
90+
if !found {
91+
p.postCommandResponse(args, "team `%s` has not been found", teamName)
92+
}
93+
94+
return
95+
}
96+
97+
func (p *Plugin) executeCommandList(args *model.CommandArgs) {
98+
wecomeMessages := p.getWelcomeMessages()
99+
100+
if len(wecomeMessages) == 0 {
101+
p.postCommandResponse(args, "There are no welcome messages defined")
102+
return
103+
}
104+
105+
// Deduplicate entries
106+
teams := make(map[string]struct{})
107+
for _, message := range wecomeMessages {
108+
teams[message.TeamName] = struct{}{}
109+
}
110+
111+
var str strings.Builder
112+
str.WriteString("Teams for which welcome messages are defined:")
113+
for team := range teams {
114+
str.WriteString(fmt.Sprintf("\n * %s", team))
115+
}
116+
p.postCommandResponse(args, str.String())
117+
return
118+
}
119+
120+
func (p *Plugin) executeCommandSetWelcome(args *model.CommandArgs) {
121+
channelInfo, appErr := p.API.GetChannel(args.ChannelId)
122+
if appErr != nil {
123+
p.postCommandResponse(args, "error occured while checking the type of the chanelId `%s`: `%s`", args.ChannelId, appErr)
124+
return
125+
}
126+
127+
if channelInfo.Type == model.CHANNEL_PRIVATE {
128+
p.postCommandResponse(args, "welcome messages are not supported for direct channels")
129+
return
130+
}
131+
132+
// strings.Fields will consume ALL whitespace, so plain re-joining of the
133+
// parameters slice will not produce the same message
134+
message := strings.SplitN(args.Command, "set_channel_welcome", 2)[1]
135+
message = strings.TrimSpace(message)
136+
137+
key := fmt.Sprintf("%s%s", WELCOMEBOT_CHANNEL_WELCOME_KEY, args.ChannelId)
138+
if appErr := p.API.KVSet(key, []byte(message)); appErr != nil {
139+
p.postCommandResponse(args, "error occured while storing the welcome message for the chanel: `%s`", appErr)
140+
return
141+
}
142+
143+
p.postCommandResponse(args, "stored the welcome message:\n%s", message)
144+
return
145+
}
146+
147+
func (p *Plugin) executeCommandGetWelcome(args *model.CommandArgs) {
148+
key := fmt.Sprintf("%s%s", WELCOMEBOT_CHANNEL_WELCOME_KEY, args.ChannelId)
149+
data, appErr := p.API.KVGet(key)
150+
if appErr != nil {
151+
p.postCommandResponse(args, "error occured while retrieving the welcome message for the chanel: `%s`", appErr)
152+
return
153+
}
154+
155+
if data == nil {
156+
p.postCommandResponse(args, "welcome message has not been set yet")
157+
return
158+
}
159+
160+
p.postCommandResponse(args, "Welcome message is:\n%s", string(data))
161+
return
162+
}
163+
164+
func (p *Plugin) executeCommandDeleteWelcome(args *model.CommandArgs) {
165+
key := fmt.Sprintf("%s%s", WELCOMEBOT_CHANNEL_WELCOME_KEY, args.ChannelId)
166+
data, appErr := p.API.KVGet(key)
167+
168+
if appErr != nil {
169+
p.postCommandResponse(args, "error occured while retrieving the welcome message for the chanel: `%s`", appErr)
170+
return
171+
}
172+
173+
if data == nil {
174+
p.postCommandResponse(args, "welcome message has not been set yet")
175+
return
176+
}
177+
178+
if appErr := p.API.KVDelete(key); appErr != nil {
179+
p.postCommandResponse(args, "error occured while deleting the welcome message for the chanel: `%s`", appErr)
180+
return
181+
}
182+
183+
p.postCommandResponse(args, "welcome message has been deleted")
184+
return
185+
}
186+
187+
func (p *Plugin) ExecuteCommand(_ *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
47188
split := strings.Fields(args.Command)
48189
command := split[0]
49190
parameters := []string{}
@@ -59,9 +200,14 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo
59200
return &model.CommandResponse{}, nil
60201
}
61202

203+
if response := p.validateCommand(action, parameters); response != "" {
204+
p.postCommandResponse(args, response)
205+
return &model.CommandResponse{}, nil
206+
}
207+
62208
isSysadmin, err := p.hasSysadminRole(args.UserId)
63209
if err != nil {
64-
p.postCommandResponse(args, fmt.Sprintf("authorization failed: %v", err))
210+
p.postCommandResponse(args, "authorization failed: %s", err)
65211
return &model.CommandResponse{}, nil
66212
}
67213
if !isSysadmin {
@@ -71,55 +217,20 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo
71217

72218
switch action {
73219
case "preview":
74-
if len(parameters) != 1 {
75-
p.postCommandResponse(args, "Please specify a team, for which preview should be made.")
76-
return &model.CommandResponse{}, nil
77-
}
78-
79220
teamName := parameters[0]
80-
81-
found := false
82-
for _, message := range p.getWelcomeMessages() {
83-
if message.TeamName == teamName {
84-
if err := p.previewWelcomeMessage(teamName, args, *message); err != nil {
85-
errMsg := fmt.Sprintf("error occured while processing greeting for team `%s`: `%s`", teamName, err)
86-
p.postCommandResponse(args, errMsg)
87-
return &model.CommandResponse{}, nil
88-
}
89-
90-
found = true
91-
}
92-
}
93-
94-
if !found {
95-
p.postCommandResponse(args, fmt.Sprintf("team `%s` has not been found", teamName))
96-
}
221+
p.executeCommandPreview(teamName, args)
97222
return &model.CommandResponse{}, nil
98223
case "list":
99-
if len(parameters) > 0 {
100-
p.postCommandResponse(args, "List command does not accept any extra parameters")
101-
return &model.CommandResponse{}, nil
102-
}
103-
104-
wecomeMessages := p.getWelcomeMessages()
105-
106-
if len(wecomeMessages) == 0 {
107-
p.postCommandResponse(args, "There are no welcome messages defined")
108-
return &model.CommandResponse{}, nil
109-
}
110-
111-
// Deduplicate entries
112-
teams := make(map[string]struct{})
113-
for _, message := range wecomeMessages {
114-
teams[message.TeamName] = struct{}{}
115-
}
116-
117-
var str strings.Builder
118-
str.WriteString("Teams for which welcome messages are defined:")
119-
for team := range teams {
120-
str.WriteString(fmt.Sprintf("\n * %s", team))
121-
}
122-
p.postCommandResponse(args, str.String())
224+
p.executeCommandList(args)
225+
return &model.CommandResponse{}, nil
226+
case "set_channel_welcome":
227+
p.executeCommandSetWelcome(args)
228+
return &model.CommandResponse{}, nil
229+
case "get_channel_welcome":
230+
p.executeCommandGetWelcome(args)
231+
return &model.CommandResponse{}, nil
232+
case "delete_channel_welcome":
233+
p.executeCommandDeleteWelcome(args)
123234
return &model.CommandResponse{}, nil
124235
case "help":
125236
fallthrough
@@ -129,7 +240,6 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo
129240
return &model.CommandResponse{}, nil
130241
}
131242

132-
p.postCommandResponse(args, fmt.Sprintf("Unknown action %v", action))
133-
243+
p.postCommandResponse(args, "Unknown action %v", action)
134244
return &model.CommandResponse{}, nil
135245
}

server/hooks.go

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/mattermost/mattermost-server/v5/mlog"
8+
"github.com/mattermost/mattermost-server/v5/model"
9+
"github.com/mattermost/mattermost-server/v5/plugin"
10+
)
11+
12+
// UserHasJoinedTeam is invoked after the membership has been committed to the database. If
13+
// actor is not nil, the user was added to the team by the actor.
14+
func (p *Plugin) UserHasJoinedTeam(c *plugin.Context, teamMember *model.TeamMember, actor *model.User) {
15+
data := p.constructMessageTemplate(teamMember.UserId, teamMember.TeamId)
16+
if data == nil {
17+
return
18+
}
19+
20+
for _, message := range p.getWelcomeMessages() {
21+
if message.TeamName == data.Team.Name {
22+
go p.processWelcomeMessage(*data, *message)
23+
}
24+
}
25+
}
26+
27+
// UserHasJoinedChannel is invoked after the membership has been committed to
28+
// the database. If actor is not nil, the user was invited to the channel by
29+
// the actor.
30+
func (p *Plugin) UserHasJoinedChannel(c *plugin.Context, channelMember *model.ChannelMember, _ *model.User) {
31+
if channelInfo, appErr := p.API.GetChannel(channelMember.ChannelId); appErr != nil {
32+
mlog.Error(
33+
"error occured while checking the type of the chanel",
34+
mlog.String("channelId", channelMember.ChannelId),
35+
mlog.Err(appErr),
36+
)
37+
return
38+
} else if channelInfo.Type == model.CHANNEL_PRIVATE {
39+
return
40+
}
41+
42+
key := fmt.Sprintf("%s%s", WELCOMEBOT_CHANNEL_WELCOME_KEY, channelMember.ChannelId)
43+
data, appErr := p.API.KVGet(key)
44+
if appErr != nil {
45+
mlog.Error(
46+
"error occured while retrieving the welcome message",
47+
mlog.String("channelId", channelMember.ChannelId),
48+
mlog.Err(appErr),
49+
)
50+
return
51+
}
52+
53+
if data == nil {
54+
// No welcome message for the given channel
55+
return
56+
}
57+
58+
dmChannel, err := p.API.GetDirectChannel(channelMember.UserId, p.botUserID)
59+
if err != nil {
60+
mlog.Error(
61+
"error occured while creating direct channel to the user",
62+
mlog.String("UserId", channelMember.UserId),
63+
mlog.Err(err),
64+
)
65+
return
66+
}
67+
68+
// We send a DM and an opportunistic ephemeral message to the channel. See
69+
// the discussion at the link below for more details:
70+
// https://github.com/mattermost/mattermost-plugin-welcomebot/pull/31#issuecomment-611691023
71+
postDM := &model.Post{
72+
UserId: p.botUserID,
73+
ChannelId: dmChannel.Id,
74+
Message: string(data),
75+
}
76+
if _, appErr := p.API.CreatePost(postDM); appErr != nil {
77+
mlog.Error("failed to post welcome message to the channel",
78+
mlog.String("channelId", dmChannel.Id),
79+
mlog.Err(appErr),
80+
)
81+
}
82+
83+
postChannel := &model.Post{
84+
UserId: p.botUserID,
85+
ChannelId: channelMember.ChannelId,
86+
Message: string(data),
87+
}
88+
time.Sleep(1 * time.Second)
89+
_ = p.API.SendEphemeralPost(channelMember.UserId, postChannel)
90+
}

server/plugin.go

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const (
1212
botUsername = "welcomebot"
1313
botDisplayName = "Welcomebot"
1414
botDescription = "A bot account created by the Welcomebot plugin."
15+
16+
WELCOMEBOT_CHANNEL_WELCOME_KEY = "chanmsg_"
1517
)
1618

1719
// Plugin represents the welcome bot plugin

0 commit comments

Comments
 (0)