├── Rakefile ├── .rspec ├── Gemfile ├── lib ├── slack_export │ ├── version.rb │ ├── slack_client.rb │ └── exporter.rb └── slack_export.rb ├── .gitignore ├── LICENSE.txt ├── bin └── slack_export ├── slack_export.gemspec ├── README.md └── spec └── spec_helper.rb /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/slack_export/version.rb: -------------------------------------------------------------------------------- 1 | module SlackExport 2 | VERSION = "0.0.3" 3 | end 4 | -------------------------------------------------------------------------------- /lib/slack_export.rb: -------------------------------------------------------------------------------- 1 | module SlackExport 2 | end 3 | 4 | require "slack_export/version" 5 | require "slack_export/slack_client" 6 | require "slack_export/exporter" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .idea 3 | /.bundle/ 4 | /.yardoc 5 | /Gemfile.lock 6 | /_yardoc/ 7 | /coverage/ 8 | /doc/ 9 | /pkg/ 10 | /spec/reports/ 11 | /tmp/ 12 | *.bundle 13 | *.so 14 | *.o 15 | *.a 16 | mkmf.log 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Michael Tucker 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /bin/slack_export: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "optparse" 4 | require "slack_export" 5 | 6 | options = {} 7 | 8 | optparse = OptionParser.new do |opts| 9 | opts.banner = "Usage: slack_export --key your-slack-api-key --channel private-channel-name --path folder-to-create-export" 10 | 11 | opts.on("-h", "--help", "Display this screen") do 12 | puts opts 13 | exit 14 | end 15 | 16 | opts.on("-k", "--key KEY", "Slack API Key (required)") do |key| 17 | options[:key] = key 18 | end 19 | 20 | opts.on("-c", "--channel CHANNEL", "Private Channel name (required). ONLY Private channels are supported") do |channel| 21 | options[:channel] = channel 22 | end 23 | 24 | opts.on("-p", "--path PATH", "Local folder path to write export file (required). Path must already exist") do |path| 25 | options[:path] = path 26 | end 27 | end 28 | optparse.parse! 29 | 30 | [:key, :channel, :path].each do |arg| 31 | unless options[arg] 32 | puts optparse.help 33 | exit -1 34 | end 35 | end 36 | 37 | begin 38 | exporter = SlackExport::Exporter.new(options[:key], options[:channel], options[:path]) 39 | exporter.logger = -> (message) { puts message } 40 | 41 | exporter.export 42 | rescue => e 43 | puts e.message 44 | exit -1 45 | end 46 | -------------------------------------------------------------------------------- /slack_export.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "slack_export/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "slack_export" 8 | spec.version = SlackExport::VERSION 9 | spec.authors = ["Michael Tucker"] 10 | spec.email = ["mtucker@godaddy.com"] 11 | spec.homepage = "https://github.com/mtuckergd/slack_export" 12 | spec.summary = %q{Export private Slack channels} 13 | spec.description = %q{Export private Slack channels in the standard Slack export format} 14 | spec.license = "MIT" 15 | 16 | spec.files = Dir[File.join("lib", "**", "*")] 17 | spec.executables = Dir[File.join("bin", "**", "*")].map! { |f| f.gsub(/bin\//, "") } 18 | spec.test_files = Dir[File.join("test", "**", "*"), File.join("spec", "**", "*"), File.join("features", "**", "*")] 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_runtime_dependency "rest-client", "~> 2.0" 22 | spec.add_runtime_dependency "rubyzip", "~> 1.0" 23 | 24 | spec.add_development_dependency "bundler", "~> 1.7" 25 | spec.add_development_dependency "rake", "~> 10.0" 26 | spec.add_development_dependency "rspec", "~> 3.0" 27 | end 28 | -------------------------------------------------------------------------------- /lib/slack_export/slack_client.rb: -------------------------------------------------------------------------------- 1 | require "rest-client" 2 | 3 | module SlackExport 4 | class SlackClient 5 | 6 | BASE_URL = "https://slack.com/api/" 7 | LIST_USERS = "users.list" 8 | LIST_CHANNELS = "groups.list" 9 | LIST_MESSAGES = "groups.history" 10 | CHANNEL_INFO = "groups.info" 11 | 12 | attr_accessor :token, :channel 13 | 14 | def initialize(api_key, channel_name) 15 | self.token = api_key 16 | self.channel = get_channel_id(channel_name) 17 | end 18 | 19 | def get_users 20 | response = post_form(LIST_USERS) 21 | response["members"] 22 | end 23 | 24 | def get_channels 25 | response = post_form(LIST_CHANNELS) 26 | response["groups"] 27 | end 28 | 29 | def get_messages() 30 | responses = [] 31 | latest = latest.to_i #pages backward, so keep track of most recent message pulled 32 | oldest = oldest.to_i 33 | loop do 34 | responses << post_form(LIST_MESSAGES, { channel: channel, latest: latest, oldest: oldest, count: 1000}) 35 | latest = responses.last["messages"].last["ts"] 36 | break unless responses.last["has_more"] 37 | end 38 | 39 | responses.map {|r| r["messages"]}.flatten 40 | end 41 | 42 | private 43 | 44 | def get_channel_id(channel_name) 45 | get_channels.select {|g| g["name"] == channel_name}.first["id"] 46 | end 47 | 48 | def post_form(action, form_values={}) 49 | response = RestClient::Request.execute( 50 | url: "#{BASE_URL}#{action}", 51 | method: :post, 52 | payload: form_values.merge(token: self.token) 53 | ) 54 | 55 | JSON.parse(response) 56 | end 57 | 58 | end 59 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SlackExport 2 | 3 | Slack does not support export of private channels, for privacy reasons. This is a good thing. 4 | However, your data is stil yours and you should be able to export it. 5 | This Gem provides a utility to export your private channels in a standard Slack export format. 6 | The export can even be imported to another Slack account as a public or private channel and 7 | includes user accounts. 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem 'slack_export' 15 | ``` 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install slack_export 24 | 25 | ## Usage 26 | 27 | This export utility is intended for use via command line. As such, it comes with an executable 28 | script. After installing the Gem, run the following command to see usage: 29 | 30 | ```bash 31 | slack_export -h 32 | ``` 33 | 34 | It is recommended to provide a clean output directory, to avoid any file conflicts, as this utility 35 | generates some temporary files and a single output zip file in the specified directory. 36 | 37 | For simplicity of development (read: dev laziness), this tool still uses [Legacy Slack Tokens](https://api.slack.com/custom-integrations/legacy-tokens). 38 | You will need to generate your own API key for the Slack account being exported and provide it to this utility. 39 | 40 | Once you have your export, you can import back into Slack directly from your Slack admin at 41 | https://{your-account-name}.slack.com/services/import/slack. Follow the instructions in that flow to import 42 | to your new Slack account. 43 | 44 | ## Future Updates 45 | 46 | - Support export of multiple channels at a time 47 | - Support full channel export 48 | - Support listing of channels and interactive selection for export 49 | 50 | ## Contributing 51 | 52 | 1. Fork it ( https://github.com/mtuckergd/slack_export/fork ) 53 | 2. Create your feature branch (`git checkout -b my-new-feature`) 54 | 3. Commit your changes (`git commit -am 'Add some feature'`) 55 | 4. Push to the branch (`git push origin my-new-feature`) 56 | 5. Create a new Pull Request 57 | 6. Profit 58 | -------------------------------------------------------------------------------- /lib/slack_export/exporter.rb: -------------------------------------------------------------------------------- 1 | require 'slack_export/slack_client' 2 | require 'json' 3 | require 'zip' 4 | 5 | module SlackExport 6 | class Exporter 7 | 8 | attr_reader :client, :channel, :base_path 9 | attr_accessor :logger 10 | 11 | def initialize(api_key, channel, base_path, logger=nil) 12 | @client = SlackClient.new(api_key, channel) 13 | @channel = channel 14 | @base_path = base_path 15 | @logger = logger || self.logger 16 | end 17 | 18 | def export 19 | raise StandardError, "Directory #{base_path} does not exist" unless Dir.exist?(base_path) 20 | log "Exporting #{channel} to folder #{base_path}" 21 | 22 | # CHANNELS 23 | channels = client.get_channels.select {|c| c["name"] == channel} 24 | log "Exporting #{channels.count} channels" 25 | File.open(channels_path, "w") {|f| f.write(channels.to_json)} 26 | 27 | # MESSAGES 28 | messages = client.get_messages 29 | log "Exporting #{messages.count} messages" 30 | Dir.mkdir(messages_base_path) unless Dir.exist?(messages_base_path) 31 | File.open(messages_path, "w") do |file| 32 | file.write(messages.to_json) 33 | end 34 | 35 | # USERS 36 | users = client.get_users 37 | log "Exporting #{users.count} users" 38 | users = users.select do |u| 39 | messages.any? {|m| m["user"] == u["id"]} 40 | end 41 | File.open(users_path, "w") {|f| f.write(users.to_json)} 42 | 43 | # BUNDLE TO ZIP 44 | log "Bundling export file to #{export_path}" 45 | Zip::File.open(export_path, Zip::File::CREATE) do |zip| 46 | zip.add(users_filename, users_path) 47 | zip.add(channels_filename, channels_path) 48 | zip.add(messages_sub_path, messages_path) 49 | end 50 | 51 | # cleanup 52 | File.delete(users_path, messages_path, channels_path) 53 | Dir.delete(messages_base_path) 54 | end 55 | 56 | def export_path 57 | File.join(base_path, "#{channel}.zip") 58 | end 59 | 60 | private 61 | 62 | def users_filename 63 | "users.json" 64 | end 65 | 66 | def users_path 67 | File.join(base_path, users_filename) 68 | end 69 | 70 | def channels_filename 71 | "channels.json" 72 | end 73 | 74 | def channels_path 75 | File.join(base_path, channels_filename) 76 | end 77 | 78 | def messages_filename 79 | "messages.json" 80 | end 81 | 82 | def messages_base_path 83 | File.join(base_path, channel) 84 | end 85 | 86 | def messages_sub_path 87 | File.join(channel, messages_filename) 88 | end 89 | 90 | def messages_path 91 | File.join(base_path, messages_sub_path) 92 | end 93 | 94 | def log(message) 95 | @logger.call message if @logger 96 | end 97 | 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # The `.rspec` file also contains a few flags that are not defaults but that 16 | # users commonly want. 17 | # 18 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 19 | RSpec.configure do |config| 20 | # rspec-expectations config goes here. You can use an alternate 21 | # assertion/expectation library such as wrong or the stdlib/minitest 22 | # assertions if you prefer. 23 | config.expect_with :rspec do |expectations| 24 | # This option will default to `true` in RSpec 4. It makes the `description` 25 | # and `failure_message` of custom matchers include text for helper methods 26 | # defined using `chain`, e.g.: 27 | # be_bigger_than(2).and_smaller_than(4).description 28 | # # => "be bigger than 2 and smaller than 4" 29 | # ...rather than: 30 | # # => "be bigger than 2" 31 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 32 | end 33 | 34 | # rspec-mocks config goes here. You can use an alternate test double 35 | # library (such as bogus or mocha) by changing the `mock_with` option here. 36 | config.mock_with :rspec do |mocks| 37 | # Prevents you from mocking or stubbing a method that does not exist on 38 | # a real object. This is generally recommended, and will default to 39 | # `true` in RSpec 4. 40 | mocks.verify_partial_doubles = true 41 | end 42 | 43 | # The settings below are suggested to provide a good initial experience 44 | # with RSpec, but feel free to customize to your heart's content. 45 | =begin 46 | # These two settings work together to allow you to limit a spec run 47 | # to individual examples or groups you care about by tagging them with 48 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 49 | # get run. 50 | config.filter_run :focus 51 | config.run_all_when_everything_filtered = true 52 | 53 | # Allows RSpec to persist some state between runs in order to support 54 | # the `--only-failures` and `--next-failure` CLI options. We recommend 55 | # you configure your source control system to ignore this file. 56 | config.example_status_persistence_file_path = "spec/examples.txt" 57 | 58 | # Limits the available syntax to the non-monkey patched syntax that is 59 | # recommended. For more details, see: 60 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 61 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 62 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 63 | config.disable_monkey_patching! 64 | 65 | # This setting enables warnings. It's recommended, but in some cases may 66 | # be too noisy due to issues in dependencies. 67 | config.warnings = true 68 | 69 | # Many RSpec users commonly either run the entire suite or an individual 70 | # file, and it's useful to allow more verbose output when running an 71 | # individual spec file. 72 | if config.files_to_run.one? 73 | # Use the documentation formatter for detailed output, 74 | # unless a formatter has already been configured 75 | # (e.g. via a command-line flag). 76 | config.default_formatter = 'doc' 77 | end 78 | 79 | # Print the 10 slowest examples and example groups at the 80 | # end of the spec run, to help surface which specs are running 81 | # particularly slow. 82 | config.profile_examples = 10 83 | 84 | # Run specs in random order to surface order dependencies. If you find an 85 | # order dependency and want to debug it, you can fix the order by providing 86 | # the seed, which is printed after each run. 87 | # --seed 1234 88 | config.order = :random 89 | 90 | # Seed global randomization in this process using the `--seed` CLI option. 91 | # Setting this allows you to use `--seed` to deterministically reproduce 92 | # test failures related to randomization by passing the same `--seed` value 93 | # as the one that triggered the failure. 94 | Kernel.srand config.seed 95 | =end 96 | end 97 | --------------------------------------------------------------------------------