Skip to content

Commit b59b0c5

Browse files
authored
Merge pull request #9 from jamietsao/remove-users
Add ability to remove users from channels
2 parents 1fd4901 + 98b1254 commit b59b0c5

2 files changed

Lines changed: 107 additions & 8 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ The users with emails `steph@warriors.com` and `klay@warriors.com` should be inv
2626

2727
_* Set `private` flag to `true` if you want to invite users to private channels. As noted above, this will require the additional permission scopes of `groups:read` and `groups:write`_
2828

29+
#### Want to remove users from channels?
30+
Simply set the optional `action` flag to `remove` (`add` is the default):
31+
32+
`slack-multi-channel-invite -api_token=<user-oauth-token> -action=remove -emails=kd@warriors.com -channels=dubnation,warriors -private=<true|false>`
33+
34+
2935
## Implementation
3036
Initially, I figured this script would be a simple loop that invoked some API to invite users to a channel. It turns out this API endpoint ([`conversations.invite`](https://api.slack.com/methods/conversations.invite)) expects the user ID (instead of username) and channel ID (instead of channel name). Problem is, it's not very straightforward to get user and channel IDs. There isn't a way to lookup a user by username (only by email). And there's no way to look up a single channel, unless you have the channel ID already (chicken and egg).
3137

main.go

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ import (
1313

1414
const (
1515
conversationsInviteURL = "https://slack.com/api/conversations.invite"
16+
conversationsKickURL = "https://slack.com/api/conversations.kick"
1617
conversationsListURL = "https://slack.com/api/conversations.list"
1718
usersLookupByEmailURL = "https://slack.com/api/users.lookupByEmail"
19+
20+
actionAdd = "add"
21+
actionRemove = "remove"
1822
)
1923

2024
type (
@@ -46,6 +50,16 @@ type (
4650
Error string `json:"error"`
4751
}
4852

53+
conversationsKickRequest struct {
54+
ChannelID string `json:"channel"`
55+
UserID string `json:"user"`
56+
}
57+
58+
conversationsKickResponse struct {
59+
Ok bool `json:"ok"`
60+
Error string `json:"error"`
61+
}
62+
4963
usersLookupByEmailResponse struct {
5064
Ok bool `json:"ok"`
5165
User user `json:"user"`
@@ -65,20 +79,22 @@ type (
6579
// 3) For each of the given channels, invite the users (user IDs) to the channel (channel ID)
6680
func main() {
6781
var apiToken string
82+
var action string
6883
var emails string
6984
var channelsArg string
7085
var private bool
7186
var debug bool
7287

7388
// parse flags
7489
flag.StringVar(&apiToken, "api_token", "", "Slack OAuth Access Token")
90+
flag.StringVar(&action, "action", "add", "'add' to invite users, 'remove' to remove users")
7591
flag.StringVar(&emails, "emails", "", "Comma separated list of Slack user emails to invite")
7692
flag.StringVar(&channelsArg, "channels", "", "Comma separated list of channels to invite users to")
7793
flag.BoolVar(&private, "private", false, "Boolean flag to enable private channel invitations (requires OAuth scopes 'groups:read' and 'groups:write')")
7894
flag.BoolVar(&debug, "debug", false, "Enables debug logging when set to true")
7995
flag.Parse()
8096

81-
if apiToken == "" || emails == "" || channelsArg == "" {
97+
if apiToken == "" || emails == "" || channelsArg == "" || (action != actionAdd && action != actionRemove) {
8298
flag.Usage()
8399
os.Exit(1)
84100
}
@@ -112,8 +128,12 @@ func main() {
112128
fmt.Printf("DEBUG: Total # of channels retrieved: %d\n", len(channelNameToIDMap))
113129
}
114130

115-
// invite users to each channel
116-
fmt.Printf("\nInviting users to channels ...\n")
131+
// invite/remove users to each channel
132+
if action == actionAdd {
133+
fmt.Printf("\nInviting users to channels ...\n")
134+
} else {
135+
fmt.Printf("\nRemoving users from channels ...\n")
136+
}
117137
channels := strings.Split(channelsArg, ",")
118138
for _, channel := range channels {
119139
channelID := channelNameToIDMap[channel]
@@ -122,13 +142,25 @@ func main() {
122142
continue
123143
}
124144

125-
err := inviteUsersToChannel(apiToken, userIDs, channelID)
126-
if err != nil {
127-
fmt.Printf("Error while inviting users to %s (%s): %s\n", channel, channelID, err)
128-
continue
145+
if action == actionAdd {
146+
err := inviteUsersToChannel(apiToken, userIDs, channelID)
147+
if err != nil {
148+
fmt.Printf("Error while inviting users to %s (%s): %s\n", channel, channelID, err)
149+
continue
150+
}
151+
} else {
152+
err := removeUsersFromChannel(apiToken, userIDs, channelID, debug)
153+
if err != nil {
154+
fmt.Printf("Error while removing users from %s (%s): %s\n", channel, channelID, err)
155+
continue
156+
}
129157
}
130158

131-
fmt.Printf("Users invited to '%s'\n", channel)
159+
if action == actionAdd {
160+
fmt.Printf("Users invited to '%s'\n", channel)
161+
} else {
162+
fmt.Printf("Users removed from '%s'\n", channel)
163+
}
132164
}
133165

134166
fmt.Println("\nAll done! You're welcome =)")
@@ -287,6 +319,67 @@ func inviteUsersToChannel(apiToken string, userIDs []string, channelID string) e
287319
return nil
288320
}
289321

322+
func removeUsersFromChannel(apiToken string, userIDs []string, channelID string, debug bool) error {
323+
// API only supports removing users one at a time ...
324+
for _, userID := range userIDs {
325+
err := removeUserFromChannel(apiToken, userID, channelID)
326+
if err != nil {
327+
if debug {
328+
fmt.Printf("DEBUG: Error while removing user %s from channel %s: %s\n", userID, channelID, err)
329+
}
330+
return err
331+
}
332+
}
333+
return nil
334+
}
335+
336+
func removeUserFromChannel(apiToken string, userID string, channelID string) error {
337+
httpClient := &http.Client{}
338+
339+
reqBody, err := json.Marshal(conversationsKickRequest{
340+
ChannelID: channelID,
341+
UserID: userID,
342+
})
343+
if err != nil {
344+
return err
345+
}
346+
347+
req, err := http.NewRequest(http.MethodPost, conversationsKickURL, bytes.NewReader(reqBody))
348+
if err != nil {
349+
return err
350+
}
351+
352+
req.Header.Add("Content-Type", "application/json")
353+
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken))
354+
355+
resp, err := httpClient.Do(req)
356+
if err != nil {
357+
return err
358+
}
359+
defer resp.Body.Close()
360+
361+
if resp.StatusCode != http.StatusOK {
362+
err := printErrorResponseBody(resp)
363+
if err != nil {
364+
return err
365+
}
366+
return fmt.Errorf("Non-200 status code: (%d)", resp.StatusCode)
367+
}
368+
369+
var data conversationsKickResponse
370+
err = json.NewDecoder(resp.Body).Decode(&data)
371+
if err != nil {
372+
return err
373+
}
374+
375+
if !data.Ok {
376+
fmt.Printf("conversationsKickResponse: %+v\n", data)
377+
return fmt.Errorf("Non-ok response while removing user from channel")
378+
}
379+
380+
return nil
381+
}
382+
290383
func printErrorResponseBody(resp *http.Response) error {
291384
bodyBytes, err := ioutil.ReadAll(resp.Body)
292385
if err != nil {

0 commit comments

Comments
 (0)