#!/usr/bin/ruby require "pathname" require "yaml" require "fileutils" gem "slack-api", "~> 1.1", ">= 1.1.3" require "slack" class Slackup Error = Class.new(StandardError) RUN_ROOT = Pathname Dir.pwd def self.run_root RUN_ROOT end SEMAPHORE = Mutex.new attr_reader :name def initialize(name, token) @name = name @token = token FileUtils.mkdir_p(name) end def self.team_token_pairs_file Pathname.glob(run_root.join("slack_teams.{json,yml,yaml}")).first end def self.team_token_pairs if team_token_pairs_file.readable? YAML.load(team_token_pairs_file.read) else fail Error, "No team token pairs file found. See README for instructions." end end def self.backup(team_token_pairs = team_token_pairs()) team_token_pairs.each do |name, token| new(name, token).execute end end def execute SEMAPHORE.synchronize do authorize! && Dir.chdir(name) do channels.each do |channel| write_channel_messages(channel) end write_stars write_users Dir.chdir(ims_dir) do im_list.each do |im| write_im_messages(im) end end end end end private def authorize! Slack.configure do |config| config.token = @token end auth_test = Slack.auth_test if auth_test["ok"] === true p [name] true else p [name, auth_test] false end end User = Struct.new(:user_hash) do def id; user_hash["id"]; end def name; user_hash["name"]; end def deleted; user_hash["deleted"]; end def color; user_hash["color"]; end def profile; user_hash["profile"]; end def admin?; user_hash["is_admin"]; end def owner?; user_hash["is_owner"]; end def has_2fa?; user_hash["has_2fa"]; end def has_files?; user_hash["has_files"]; end def to_hash; user_hash; end end # { # "ok": true, # "members": [ # { # "id": "U023BECGF", # "name": "bobby", # "deleted": false, # "color": "9f69e7", # "profile": { # "first_name": "Bobby", # "last_name": "Tables", # "real_name": "Bobby Tables", # "email": "bobby@slack.com", # "skype": "my-skype-name", # "phone": "+1 (123) 456 7890", # "image_24": "https:\/\/...", # "image_32": "https:\/\/...", # "image_48": "https:\/\/...", # "image_72": "https:\/\/...", # "image_192": "https:\/\/..." # }, # "is_admin": true, # "is_owner": true, # "has_2fa": false, # "has_files": true # }, # ] # } def users @users ||= Slack.users_list["members"].map { |member| User.new(member) } end def channels @channels ||= Slack.channels_list["channels"] end Im = Struct.new(:im_hash) do def id; im_hash["id"]; end def user; im_hash["user"]; end end # @return [Hash] # @example # { # "ok"=>true, # "ims"=>[ # {"id"=>"D1234567890", "is_im"=>true, "user"=>"USLACKBOT", "created"=>1372105335, "is_user_deleted"=>false}, # ] # } def im_list @im_list ||= Slack.im_list["ims"].map { |im| Im.new(im) } end # @param im_id [String] is the 'channel' of the im, e.g. "D1234567890" # @return [Hash] # @example return # { # "ok": true, # "latest": "1358547726.000003", # "messages": [ # { # "type": "message", # "ts": "1358546515.000008", # "user": "U2147483896", # "text": "<@U0453RHGQ> has some thoughts on that kind of stuff" # }, # ] # "has_more": false def im_history(im_id) Slack.im_history(channel: im_id) end def write_channel_messages(channel) messages = Slack.channels_history(channel: channel["id"], count: "1000")["messages"] File.open(backup_filename(channel["name"]), "w") do |f| formatted_messages = format_channel_messages(messages) f.write serialize(formatted_messages) end end def format_messages(messages) messages.reverse.map { |msg| if msg.has_key?("text") && msg.has_key?("user") msg["user"] = user_name(msg["user"]) msg["text"].gsub!(/<@(?<userid>U[A-Z0-9]+)>/) { userid = $~[:userid] # MatchData "<@#{user_name(userid)}>" } msg else nil end }.compact end alias_method :format_channel_messages, :format_messages alias_method :format_im_messages, :format_messages # gets user name for an id, if mapping is known, else returns the input def user_name(user_id) @user_names ||= users.each_with_object({}) {|user, lookup| lookup[user.id] = user.name } @user_names.fetch(user_id) { user_id } end def write_stars File.open(backup_filename("stars"), "w") do |f| stars = Slack.stars_list(count: "1000", page: "1") f.write(serialize(stars)) end end def write_users File.open(backup_filename("users"), "w") do |f| f.write(serialize(users.map(&:to_hash))) end end def serialize(obj) obj.to_yaml end def write_im_messages(im) messages = im_history(im.id)["messages"] im_username = user_name(im.user).downcase.gsub(/\s+/, "-") formatted_messages = format_im_messages(messages) return if formatted_messages.empty? File.open(backup_filename(im_username), "w") do |f| f.write serialize(formatted_messages) end end def ims_dir @ims_dir ||= "ims" FileUtils.mkdir_p(@ims_dir) @ims_dir end def backup_filename(name) "#{name}.yml" end end if $0 == __FILE__ Slackup.backup end