Archiving Empty Channels

Life tends towards entropy, and our Slack workspaces aren’t immune. Every so often, an admin (or kind volunteer) will want to go through and clean up their team’s workspace, whether that’s old channels, unused members, or what have you. A case we’ve recently run into is having to clean out all the channels that have 0 remaining members in them - maybe the people have left the company, or the channel itself isn’t of value going forward. For whatever reason, feel free to borrow and expand on this script to take all sorts of bulk actions against the channels of your workspace.

Setup

This script will use the endpoints conversations.list and conversations.archive.

⚠️ If you are planning to run this for Private Channels, know that the script will run conversations.join as well, because the App needs to be a part of the channel before it can archive it.

  • You’ll need to create a new Slack app, so that the script has permissions to interact with your workspace
  • Request the bot scopes you will need to request channels:read, channels:manage. (+ groups:read, groups:write, and channels:join if you plan to run against private channels, too)
  • Set your token as an environment variable called SLACK_TOKEN
  • Run the script using Python 3.6+

The script

import os
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError

CONVERSATION_TYPES_TO_CHECK = ['public_channel'] # ['public_channel', 'private_channel']

def handle_channel(client: WebClient, channel: dict):
    cname = channel["name"] 
    channel_id = channel["id"]
    num_members = channel["num_members"]
    is_private = channel.get("is_private", False)
    ignore_list = ['general']
    
    if cname in ignore_list:
        return None

    if num_members == 0:
        print(f'Archiving {cname} for having 0 members..')
        if is_private:
            # have to join the channel before we can archive it
            client.conversations_join(channel_id=channel_id)

        try:
            # rate limiting for this endpoint handled by the client level handler we added
            client.conversations_archive(channel_id=channel_id)
        except SlackApiError as e:
            if e.response["error"] == "cant_archive_required":
                print(f'[!] unable to archive {cname} as it is required')

def search_conversations_and_archive_0_member_channels(client):
    cursor = None
    while True:
        response = client.conversations_list(
            limit=300,
            cursor=cursor,
            exclude_archived=True,
            types=','.join(CONVERSATION_TYPES_TO_CHECK),
        )
        for channel in response["channels"]:
            handle_channel(client, channel)
        cursor = response.get("response_metadata", {}).get("next_cursor")
        if not cursor:
            break

if __name__ == "__main__":
    client = WebClient(token=os.environ["SLACK_TOKEN"])
    # This handler does retries when HTTP status 429 is returned, https://slack.dev/python-slack-sdk/web/index.html
    from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
    rate_limit_handler = RateLimitErrorRetryHandler(max_retry_count=1)
    client.retry_handlers.append(rate_limit_handler)

    search_conversations_and_archive_0_member_channels(client)