Skip to content

Commit

Permalink
Join public channels automatically
Browse files Browse the repository at this point in the history
  • Loading branch information
timoreimann committed Feb 12, 2023
1 parent 9df9d65 commit 9c64644
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 36 deletions.
27 changes: 14 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ Right now, the only supported target is Slack: given a list of PageDuty schedule

## How-to

You will need to create a Slack app with the following scopes

| Scope | Optional | Used for |
|--------------------|----------|------------------------------------|
| `users:read` | no | |
| `channels:read` | yes | managing topics (public channels) |
| `channels:manage` | yes | managing topics (public channels) |
| `groups:read` | yes | managing topics (private channels) |
| `groups:write` | yes | managing topics (private channels) |
| `usergroups:read` | yes | managing user groups |
| `usergroups:write` | yes | managing user groups |

and invite it to the target channel. (One easy to do this is to select the app from a channel where it already exists and use the context menu to add it to another channel.)
You will need to create a Slack app with the following scopes:

| Scope | Optional | Used for |
|--------------------|----------|---------------------------------------|
| `users:read` | no | |
| `channels:join` | yes | joining public channels automatically |
| `channels:read` | yes | managing topics (public channels) |
| `channels:manage` | yes | managing topics (public channels) |
| `groups:read` | yes | managing topics (private channels) |
| `groups:write` | yes | managing topics (private channels) |
| `usergroups:read` | yes | managing user groups |
| `usergroups:write` | yes | managing user groups |

For private channels and when the `channels:join` scope is not assigned, the Slack app needs to be joined to the target channel manually. (One easy to do this is to select the app from a channel where it already exists and use the context menu to add it to another channel.)

Next up, one or more _slack syncs_ must be configured, preferrably through a YAML configuration file. Here is an [example file](config.example.yaml):

Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func realMain(p params) error {

sp := syncerParams{
pdClient: newPagerDutyClient(pdToken),
slClient: newSlackClient(slToken),
slClient: newSlackMetaClient(slToken),
}

ctx := context.Background()
Expand Down
69 changes: 50 additions & 19 deletions slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,20 @@ func (cl channelList) find(id, name string) *slack.Channel {
return nil
}

type slackClient struct {
*slack.Client
type slackMetaClient struct {
slackClient *slack.Client
}

func newSlackClient(token string) *slackClient {
return &slackClient{
Client: slack.New(token),
func newSlackMetaClient(token string) *slackMetaClient {
return &slackMetaClient{
slackClient: slack.New(token),
}
}

func (cl *slackClient) getSlackUsers(ctx context.Context) (slackUsers, error) {
func (metaClient *slackMetaClient) getSlackUsers(ctx context.Context) (slackUsers, error) {
// GetUsersContext retries on rate-limit errors, so no need to wrap it around
// retryOnSlackRateLimit.
apiUsers, err := cl.GetUsersContext(ctx)
apiUsers, err := metaClient.slackClient.GetUsersContext(ctx)
if err != nil {
return nil, err
}
Expand All @@ -83,7 +83,7 @@ func (cl *slackClient) getSlackUsers(ctx context.Context) (slackUsers, error) {
return slUsers, nil
}

func (cl *slackClient) getChannels(ctx context.Context) (channelList, error) {
func (metaClient *slackMetaClient) getChannels(ctx context.Context) (channelList, error) {
var (
list channelList
cursor string
Expand All @@ -96,7 +96,7 @@ func (cl *slackClient) getChannels(ctx context.Context) (channelList, error) {
)
retErr := retryOnSlackRateLimit(ctx, func(ctx context.Context) error {
var err error
channels, nextCursor, err = cl.GetConversationsContext(ctx, &slack.GetConversationsParameters{
channels, nextCursor, err = metaClient.slackClient.GetConversationsContext(ctx, &slack.GetConversationsParameters{
Cursor: cursor,
ExcludeArchived: "true",
Limit: 200,
Expand All @@ -121,15 +121,15 @@ func (cl *slackClient) getChannels(ctx context.Context) (channelList, error) {
return list, nil
}

func (cl *slackClient) getChannelByID(ctx context.Context, id string) (*slack.Channel, error) {
return cl.GetConversationInfoContext(ctx, id, false)
func (metaClient *slackMetaClient) getChannelByID(ctx context.Context, id string) (*slack.Channel, error) {
return metaClient.slackClient.GetConversationInfoContext(ctx, id, false)
}

func (cl *slackClient) getUserGroups(ctx context.Context) ([]UserGroup, error) {
func (metaClient *slackMetaClient) getUserGroups(ctx context.Context) ([]UserGroup, error) {
var groups []slack.UserGroup
retErr := retryOnSlackRateLimit(ctx, func(ctx context.Context) error {
var err error
groups, err = cl.GetUserGroupsContext(ctx, []slack.GetUserGroupsOption(nil)...)
groups, err = metaClient.slackClient.GetUserGroupsContext(ctx, []slack.GetUserGroupsOption(nil)...)
return err
})
if retErr != nil {
Expand Down Expand Up @@ -180,9 +180,40 @@ func (ocg *oncallGroup) ensureMember(m string) {
ocg.members = append(ocg.members, m)
}

func (cl *slackClient) updateOncallGroupMembers(ctx context.Context, oncallGroups oncallGroups, dryRun bool) error {
func (metaClient *slackMetaClient) joinChannel(ctx context.Context, channelID string) (joined bool, err error) {
_, warn, respWarnings, err := metaClient.slackClient.JoinConversationContext(ctx, channelID)
if err != nil {
if strings.Contains(err.Error(), "method_not_supported_for_channel_type") {
// This likely means it is a private channel that we cannot join.
return false, nil
}
return false, err
}

const alreadyInChannelWarning = "already_in_channel"

joined = true
var warnings []string
for _, w := range append([]string{warn}, respWarnings...) {
if w != "" {
if w == alreadyInChannelWarning {
joined = false
continue
}
warnings = append(warnings, warn)
}
}

if len(warnings) > 0 {
return false, fmt.Errorf("joined channel with warnings: %s", strings.Join(warnings, "; "))
}

return joined, nil
}

func (metaClient *slackMetaClient) updateOncallGroupMembers(ctx context.Context, oncallGroups oncallGroups, dryRun bool) error {
for _, group := range oncallGroups {
currentMembers, err := cl.GetUserGroupMembersContext(ctx, group.userGroupID)
currentMembers, err := metaClient.slackClient.GetUserGroupMembersContext(ctx, group.userGroupID)
if err != nil {
return fmt.Errorf("failed to get user group members for %q: %s", group.userGroupName, err)
}
Expand All @@ -197,7 +228,7 @@ func (cl *slackClient) updateOncallGroupMembers(ctx context.Context, oncallGroup
fmt.Printf("[DRY RUN] Not updating user group %s with member(s): %s\n", group.userGroupName, concatMembers)
continue
}
_, err = cl.UpdateUserGroupMembersContext(ctx, group.userGroupID, concatMembers)
_, err = metaClient.slackClient.UpdateUserGroupMembersContext(ctx, group.userGroupID, concatMembers)
if err != nil {
return fmt.Errorf("failed to update user group members for %q: %s", group.userGroupName, err)
}
Expand All @@ -207,8 +238,8 @@ func (cl *slackClient) updateOncallGroupMembers(ctx context.Context, oncallGroup
return nil
}

func (cl *slackClient) updateTopic(ctx context.Context, channelID string, topic string, dryRun bool) error {
channel, err := cl.getChannelByID(ctx, channelID)
func (metaClient *slackMetaClient) updateTopic(ctx context.Context, channelID string, topic string, dryRun bool) error {
channel, err := metaClient.getChannelByID(ctx, channelID)
if err != nil {
return err
}
Expand All @@ -221,7 +252,7 @@ func (cl *slackClient) updateTopic(ctx context.Context, channelID string, topic
fmt.Println("[DRY RUN] Not updating topic")
return nil
}
_, err := cl.SetTopicOfConversationContext(ctx, channel.ID, topic)
_, err := metaClient.slackClient.SetTopicOfConversationContext(ctx, channel.ID, topic)
if err != nil {
return err
}
Expand Down
21 changes: 18 additions & 3 deletions syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"fmt"
"os"
"strings"
"text/template"
)

Expand All @@ -17,9 +18,9 @@ type runSlackSync struct {
}

type syncerParams struct {
pdClient *pagerDutyClient
slClient *slackClient
slackUsers slackUsers
pdClient *pagerDutyClient
slClient *slackMetaClient
slackUsers slackUsers
slackUserGroups UserGroups
}

Expand Down Expand Up @@ -120,6 +121,20 @@ func (s *syncer) Run(ctx context.Context, slackSyncs []runSlackSync) error {
}

func (s *syncer) runSlackSync(ctx context.Context, slackSync runSlackSync) error {
if !slackSync.dryRun {
joined, err := s.slClient.joinChannel(ctx, slackSync.slackChannelID)
if err != nil {
if strings.Contains(err.Error(), "missing_scope") {
fmt.Printf(`cannot automatically join channel with ID %s because of missing scope "channels:join" -- please add the scope or join pdsync manually`, slackSync.slackChannelID)
} else {
return fmt.Errorf("failed to join channel with ID %s: %s", slackSync.slackChannelID, err)
}
}
if joined {
fmt.Printf("joined channel with ID %s\n", slackSync.slackChannelID)
}
}

ocgs := oncallGroups{}
slackUserIDByScheduleName := map[string]string{}
for _, schedule := range slackSync.pdSchedules {
Expand Down

0 comments on commit 9c64644

Please sign in to comment.