Skip to content

Commit 5a19c3b

Browse files
authored
Merge pull request #2 from jamietsao/add-private-channel-support
Added private channel support
2 parents 4770a03 + bd5a684 commit 5a19c3b

2 files changed

Lines changed: 86 additions & 38 deletions

File tree

README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,28 @@ Enjoy!
88
## Instructions
99
1. [Create](https://api.slack.com/apps) a Slack App for your workspace.
1010
2. Add the following permission scopes to your app:
11-
- `users.read`
12-
- `users.read.email`
13-
- `channels.read`
14-
- `channels.write`
11+
- `users:read`
12+
- `users:read.email`
13+
- `channels:read`
14+
- `channels:write`
15+
- `groups:read` (only if inviting to private channels)
16+
- `groups:write` (only if inviting to private channels)
1517
3. Install app to your workspace which will generate a new OAuth Access Token
1618
4. Download script:
1719
- If you have Go installed: `go get github.com/jamietsao/slack-multi-channel-invite`
1820
- Else download the binary directly: https://github.com/jamietsao/slack-multi-channel-invite/releases
1921
5. Run script:
2022

21-
`slack-multi-channel-invite -api_token=<oauth-access-token> -channels=foo,bar,baz -user_email=steph@curry.com`
23+
`slack-multi-channel-invite -api_token=<oauth-access-token> -channels=foo,bar,baz -user_email=steph@curry.com -private=<true|false>`
2224

2325
The user with email `steph@curry.com` should be invited to channels `foo`, `bar`, and `baz`!
2426

27+
_* 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`_
28+
2529
## Implementation
2630
Initially, I figured this script would be a simple loop that invoked some API to invite a user 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).
2731

2832
For these reasons, I wrote the script like so:
2933
1. [Look up](https://api.slack.com/methods/users.lookupByEmail) the Slack user ID by email.
30-
2. [Query](https://api.slack.com/methods/conversations.list) all public channels in the workspace and create a name -> ID mapping.
34+
2. [Query](https://api.slack.com/methods/conversations.list) all public (or private) channels in the workspace and create a name -> ID mapping.
3135
3. For each of the given channels, [invite](https://api.slack.com/methods/conversations.invite) the user to the channel using the user ID and channel ID from steps 1 & 2.

main.go

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ type (
2222
Ok bool `json:"ok"`
2323
Channels []channel `json:"channels"`
2424
ResponseMetadata responseMetadata `json:"response_metadata"`
25+
Error string `json:error`
26+
Needed string `json:needed`
27+
Provided string `json:provided`
2528
}
2629

2730
channel struct {
@@ -58,17 +61,21 @@ type (
5861
// This script invites the given user to the given list of channels on Slack.
5962
// Due to the oddness of the Slack API, this is accomplished via these steps:
6063
// 1) Look up the Slack user ID by email
61-
// 2) Query all public channels in the workspace and create a name -> ID mapping
64+
// 2) Query all public (private if 'private' flag is set to true) channels in the workspace and create a name -> ID mapping
6265
// 3) For each of the given channels, invite the user (user ID) to the channel (channel ID)
6366
func main() {
6467
var apiToken string
6568
var userEmail string
6669
var channelsArg string
70+
var private bool
71+
var debug bool
6772

6873
// parse flags
6974
flag.StringVar(&apiToken, "api_token", "", "Slack OAuth Access Token")
7075
flag.StringVar(&userEmail, "user_email", "", "Email of Slack user to invite")
7176
flag.StringVar(&channelsArg, "channels", "", "Comma separated list of channels to invite user to")
77+
flag.BoolVar(&private, "private", false, "Boolean flag to enable private channel invitations (requires OAuth scopes 'groups:read' and 'groups:write')")
78+
flag.BoolVar(&debug, "debug", false, "Enables debug logging when set to true")
7279
flag.Parse()
7380

7481
if apiToken == "" || userEmail == "" || channelsArg == "" {
@@ -82,24 +89,43 @@ func main() {
8289
panic(err)
8390
}
8491

85-
// get all public channels
86-
channelNameToIDMap, err := getPublicChannels(apiToken)
92+
if debug {
93+
fmt.Printf("User ID for '%s': %s\n", userEmail, userID)
94+
}
95+
96+
// get all channels
97+
channelNameToIDMap, err := getChannels(apiToken, private, debug)
8798
if err != nil {
8899
panic(err)
89100
}
90101

102+
if debug {
103+
fmt.Printf("Total # of channels retrieved: %d\n", len(channelNameToIDMap))
104+
}
105+
91106
// invite user to each channel
92107
channels := strings.Split(channelsArg, ",")
93108
for _, channel := range channels {
94109
channelID := channelNameToIDMap[channel]
110+
if channelID == "" {
111+
fmt.Printf("Channel '%s' not found -- skipping\n", channel)
112+
continue
113+
}
114+
115+
if debug {
116+
fmt.Printf("Inviting user %s (%s) to channel %s (%s)\n", userEmail, userID, channel, channelID)
117+
}
95118

96-
err := inviteUserToChannel(apiToken, userID, userEmail, channelID, channel)
119+
err := inviteUserToChannel(apiToken, userID, channelID)
97120
if err != nil {
98-
fmt.Printf("Error while inviting %s to %s: %s\n", userEmail, channel, err)
121+
fmt.Printf("Error while inviting %s (%s) to %s (%s): %s\n", userEmail, userID, channel, channelID, err)
122+
continue
99123
}
124+
125+
fmt.Printf("User %s invited to '%s'\n", userEmail, channel)
100126
}
101127

102-
fmt.Println("All done! You're welcome =)")
128+
fmt.Println("\nAll done! You're welcome =)")
103129
}
104130

105131
func getUserID(apiToken, userEmail string) (string, error) {
@@ -134,44 +160,63 @@ func getUserID(apiToken, userEmail string) (string, error) {
134160
return data.User.ID, nil
135161
}
136162

137-
// TODO: add proper paging to ensure all public channels are retrieved
138-
func getPublicChannels(apiToken string) (map[string]string, error) {
139-
// query list of public channels
140-
resp, err := http.Get(conversationsListURL + fmt.Sprintf("?token=%s&exclude_archived=true&limit=1000", apiToken))
141-
if err != nil {
142-
panic(err)
163+
func getChannels(apiToken string, private bool, debug bool) (map[string]string, error) {
164+
165+
channelType := "public_channel"
166+
if private {
167+
channelType = "private_channel"
143168
}
144-
defer resp.Body.Close()
145169

146-
if resp.StatusCode != http.StatusOK {
147-
err := printErrorResponseBody(resp)
170+
nameToID := make(map[string]string)
171+
172+
var nextCursor string
173+
for {
174+
// query list of channels
175+
resp, err := http.Get(conversationsListURL + fmt.Sprintf("?token=%s&cursor=%s&exclude_archived=true&limit=200&types=%s", apiToken, nextCursor, channelType))
176+
if err != nil {
177+
panic(err)
178+
}
179+
defer resp.Body.Close()
180+
181+
if resp.StatusCode != http.StatusOK {
182+
err := printErrorResponseBody(resp)
183+
if err != nil {
184+
return nil, err
185+
}
186+
return nil, fmt.Errorf("Non-200 status code (%d)", resp.StatusCode)
187+
}
188+
189+
var data conversationsListResponse
190+
err = json.NewDecoder(resp.Body).Decode(&data)
148191
if err != nil {
149192
return nil, err
150193
}
151-
return nil, fmt.Errorf("Non-200 status code (%d)", resp.StatusCode)
152-
}
153194

154-
var data conversationsListResponse
155-
err = json.NewDecoder(resp.Body).Decode(&data)
156-
if err != nil {
157-
return nil, err
158-
}
195+
if !data.Ok {
196+
fmt.Printf("conversationsListResponse: %+v", data)
197+
return nil, fmt.Errorf("Non-ok response while querying list of channels")
198+
}
159199

160-
if !data.Ok {
161-
fmt.Printf("conversationsListResponse: %+v", data)
162-
return nil, fmt.Errorf("Non-ok response while querying list of public channels")
163-
}
200+
if debug {
201+
fmt.Printf("# of channels returned in page: %d\n", len(data.Channels))
202+
}
164203

165-
// create map of channel names to IDs
166-
nameToID := make(map[string]string)
167-
for _, channel := range data.Channels {
168-
nameToID[channel.Name] = channel.ID
204+
// map of channel names to IDs
205+
for _, channel := range data.Channels {
206+
nameToID[channel.Name] = channel.ID
207+
}
208+
209+
// paginate if necessary
210+
nextCursor = data.ResponseMetadata.NextCursor
211+
if nextCursor == "" {
212+
break
213+
}
169214
}
170215

171216
return nameToID, nil
172217
}
173218

174-
func inviteUserToChannel(apiToken, userID, userEmail, channelID, channelName string) error {
219+
func inviteUserToChannel(apiToken, userID, channelID string) error {
175220
httpClient := &http.Client{}
176221

177222
reqBody, err := json.Marshal(conversationsInviteRequest{
@@ -215,7 +260,6 @@ func inviteUserToChannel(apiToken, userID, userEmail, channelID, channelName str
215260
return fmt.Errorf("Non-ok response while inviting user to channel")
216261
}
217262

218-
fmt.Printf("User %s invited to %s\n", userEmail, channelName)
219263
return nil
220264
}
221265

0 commit comments

Comments
 (0)