Slack Multi-Teams Overview

Provides an overview of unread channels, unread messages and channel histories.

Image preview of Slack Multi-Teams Overview plugin.

slack-multi-teams.1m.rb

Edit
Open on GitHub
#!/usr/bin/env ruby
#
# Slack Mutli-Teams Overview.
#
# by Harry Löwen
#
# Provides an overview of unread channels, unread messages and channel histories.
# Features: multiple teams (workspaces) handling,
# 'mark as read' and 'open in slack' buttons,
# display all channels or only unread ones.
#
# Uses Slack Legacy Token (from now)
# https://api.slack.com/custom-integrations/legacy-tokens
#
# Refresh rate is set to every minute.
# Because: 180+ lines of code and 4+ requests running per team (one channel, one user)
# For a simple unread indicator check out: https://github.com/matryer/bitbar-plugins/blob/master/Messenger/slack-unread.1s.py
#
# Feel free to customize settings, colors, all-done-messages, etc.
#
# metadata
# <xbar.title>Slack Multi-Teams Overview</xbar.title>
# <xbar.version>v1.0</xbar.version>
# <xbar.author>Harry Löwen</xbar.author>
# <xbar.author.github>harryloewen</xbar.author.github>
# <xbar.desc>Provides an overview of unread channels, unread messages and channel histories.</xbar.desc>
# <xbar.image>https://drive.google.com/uc?export=preview&id=1vxQ5qr8opWaHhiqFlJZmi0oCOG3ik0uJ</xbar.image>
# <xbar.dependencies>ruby<xbar.dependencies>
# <xbar.abouturl>https://github.com/harryloewen/bitbar-slack-multi-teams/</xbar.abouturl>

require 'net/http'
require 'open-uri'
require 'json'

# your token(s) please
TOKENS = [
  'xoxp-your-slack-token',
  'xoxp-another-slack-token',
].freeze

# display all channels or only those with unread messages
ALL_CHANNELS = false

# your default color
COLOR = '#696969'.freeze # '#696969' works in darkmode as well

# your random messages if there're no unreads (and if ALL_CHANNELS is set to false)
ALL_DONE_MESSAGES = [
  ":v: All caught up. | color=#{COLOR}
    What’s next? | color=#{COLOR}",
  ":octopus: All done. | color=#{COLOR}
    The world is your oyster. | color=#{COLOR}",
  ":clap: Everything unread is now read. | color=#{COLOR}
    You’ve done it. | color=#{COLOR}",
  ":boom: Boom. | color=#{COLOR}
    You’re up to date. | color=#{COLOR}",
  ":seedling: Everything’s sorted! | color=#{COLOR}
    Let’s start something new. | color=#{COLOR}",
  ":car: There. | color=#{COLOR}
    All caught up. | color=#{COLOR}",
  ":balloon: There! Caught up. | color=#{COLOR}
    Set your mind to something new. | color=#{COLOR}",
  ":rocket: All done. | color=#{COLOR}
    The future is yours. | color=#{COLOR}",
  ":raised_hands: That’s everything! | color=#{COLOR}",
  ":tractor: You’re all read. | color=#{COLOR}
    Here’s a tractor. | color=#{COLOR}"
].freeze

# some helpful methods
def load_content(api_method, options = nil)
  url = "https://slack.com/api/#{api_method}?token=#{@team[:token]}#{options}"
  @content = JSON.parse(open(url).read)
  return if @content['ok']
  @output += "🚫\n"
  @output += "#{api_method}: #{@content['error']} | color=red"
end

def load_team
  load_content('team.info')
  return unless @content['ok']
  @team[:id] = @content['team']['id']
  @team[:name] = @content['team']['name']
  @team.merge!(unreads: 0, users: [], channels: [])
  @teams << @team
end

def load_channels
  load_content('users.conversations', '&types=public_channel%2Cprivate_channel%2Cmpim%2Cim')
  @content['channels'].each do |channel|
    @team[:channels] <<
      { id: channel['id'], name: channel['name'], user: channel['user'],
        is_channel: channel['is_channel'], is_im: channel['is_im'] }
  end
end

def load_users
  load_content('users.list')
  @content['members'].each do |user|
    @team[:users] << { id: user['id'], name: user['name'] }
  end
end

def find_user(message)
  return '...' unless message['type'] == 'message'
  if message['user']
    '@' + message['user']
  elsif message['bot_id']
    if message['attachments']
      message['attachments'][0]['service_name']
    else
      'Bot'
    end
  end
end

def find_text(message)
  return '...' unless message['type'] == 'message'
  if !message['text'].nil? && !message['text'].empty?
    message['text'].tr("\n", ' ').tr("\r", ' ')
  elsif message['attachments']
    message['attachments'].first['text'].tr("\n", ' ').tr("\r", ' ')
  end
end

def handle_messages(channel, red_messages)
  history = []
  @content['messages'].each do |message|
    color = red_messages > 0 ? 'red' : COLOR
    history << "--#{find_user(message)}: #{find_text(message)}|length=90 color=#{color}\n"
    red_messages -= 1
  end
  history << "\n-----\n"
  history << "--🔗 open in Slack | href=slack://channel?id=#{channel}&team=#{@team[:id]}\n"
end

def marking_url(channel)
  timestamp = @content['messages'].first['ts']
  "https://slack.com/api/channels.mark?token=#{@team[:token]}&channel=#{channel[:id]}&ts=#{timestamp}"
end

def load_history
  @team[:channels].each do |channel|
    method = channel[:is_im] ? 'im.history' : 'channels.history'
    load_content(method, "&channel=#{channel[:id]}&count=6&unreads=true")
    channel[:unread] = @content['unread_count_display'].to_i

    if channel[:unread] > 0
      @team[:unreads] += 1
      channel[:history] = handle_messages(channel[:id], channel[:unread])
      channel[:history] << "--✅ mark as read | bash='/usr/bin/curl' param1='#{marking_url(channel)}' refresh=true terminal=false\n"
    elsif ALL_CHANNELS
      channel[:history] = handle_messages(channel[:id], channel[:unread])
    end
  end
end

# everything starts here
@teams = []
@output = ''

TOKENS.each do |token|
  @team = { token: token }
  load_team
end

@teams.each do |team|
  @team = team
  load_channels
  load_history

  @output += "\n---\n#{@team[:name]}\n"

  if @team[:unreads] > 0 || ALL_CHANNELS
    @team[:channels].each do |channel|
      next unless channel[:unread] > 0 || ALL_CHANNELS
      @output += channel[:is_im] ? "<@#{channel[:user]}>" : "##{channel[:name]}"
      @output += channel[:unread] > 0 ? " (#{channel[:unread]})|color=red\n" : "\n"
      channel[:history].each { |message| @output += message.to_s }
    end

    load_users
    @team[:users].each do |user|
      @output = @output.gsub(user[:id], user[:name]).gsub("<@#{user[:name]}>", "@#{user[:name]}")
      @output = @output.gsub('<!channel>', '@channel').gsub('<!here>','@here')
    end
  else
    @output += ALL_DONE_MESSAGES.sample
  end
end

@unread_channels = @teams.map { |t| t[:unreads] }.inject(0, :+)
@unread_channels = '' if @unread_channels.zero?

ICON='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'.freeze

# and finally print it!
puts "
#{@unread_channels} | image=#{ICON}
---
#{@output}
---
Settings
--Tokens: #{TOKENS.count}
----Total: #{TOKENS.count}
----Valid: #{@teams.count} | color=#{@teams.count < TOKENS.count ? 'red' : 'green'}
----Generate tokens | href=https://api.slack.com/custom-integrations/legacy-tokens
--Show all channels: #{ALL_CHANNELS} | color=#{ALL_CHANNELS ? 'green' : 'red'}
---
Refresh ⟳| refresh=true
"