├── .gitignore ├── Gemfile ├── lib ├── kappa-slack.rb └── kappa-slack │ ├── cli.rb │ └── uploader.rb ├── bin └── kappa-slack ├── README.md ├── LICENSE.md └── Gemfile.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env.* 3 | tmp 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'activesupport' 4 | gem 'dotenv' 5 | gem 'thor' 6 | gem 'mechanize' 7 | gem 'httpclient' 8 | -------------------------------------------------------------------------------- /lib/kappa-slack.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | require 'active_support/core_ext' 3 | 4 | module KappaSlack 5 | autoload :CLI, 'kappa-slack/cli' 6 | autoload :Uploader, 'kappa-slack/uploader' 7 | 8 | def self.logger 9 | @logger ||= Logger.new(STDOUT) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /bin/kappa-slack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | require 'bundler/setup' 4 | 5 | APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..')) 6 | LIB_PATH = File.join(APP_ROOT, 'lib') 7 | $LOAD_PATH.unshift(LIB_PATH) 8 | 9 | require 'dotenv' 10 | Dotenv.load 11 | 12 | require 'kappa-slack' 13 | KappaSlack::CLI.start(ARGV) 14 | -------------------------------------------------------------------------------- /lib/kappa-slack/cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | 3 | module KappaSlack 4 | class CLI < Thor::Group 5 | class_option :slack_email, default: ENV['SLACK_EMAIL'], required: true, type: :string 6 | class_option :slack_password, default: ENV['SLACK_PASSWORD'], required: true, type: :string 7 | class_option :slack_team_name, default: ENV['SLACK_TEAM_NAME'], required: true, type: :string 8 | class_option :skip_bttv_emotes, type: :boolean, default: false 9 | class_option :skip_one_letter_emotes, type: :boolean, default: true 10 | 11 | def self.banner 12 | 'kappa-slack [options]' 13 | end 14 | 15 | def upload 16 | Uploader.new(**options.to_hash.symbolize_keys).upload 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kappa Slack 2 | Add Kappa to your Slack. Kappa Slack is a script that will add all Twitch and BTTV emotes to your Slack workspace. 3 | 4 | ## Installation 5 | 6 | Setup your local copy by running: 7 | 8 | ```sh 9 | git clone git@github.com:calderalabs/kappa-slack.git 10 | cd kappa-slack 11 | bundle install 12 | ``` 13 | 14 | ## Usage 15 | 16 | Example `.env` file: 17 | 18 | ```sh 19 | SLACK_TEAM_NAME=kappa 20 | SLACK_EMAIL=kappa@twitch.tv 21 | SLACK_PASSWORD=password123 22 | ``` 23 | 24 | If you have the `.env` file setup correctly, you can just run `bin/kappa-slack` to start uploading emotes. 25 | Without an `.env` file, you can still run the script, but you need to provide options as follows: 26 | 27 | ```sh 28 | bin/kappa-slack --slack-team-name=kappa --slack-email=kappa@twitch.tv --slack-password=password123 29 | ``` 30 | 31 | Optionally, you can pass these options to skip certain emotes: 32 | 33 | * `--skip-bttv-emotes` (default: `false`) Skips emotes from BetterTTV 34 | * `--skip-one-letter-emotes` (default: `true`) Skips single letter emotes, like `D:` 35 | 36 | Enjoy! 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Caldera Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (4.2.5.1) 5 | i18n (~> 0.7) 6 | json (~> 1.7, >= 1.7.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | domain_name (0.5.20160128) 11 | unf (>= 0.0.5, < 1.0.0) 12 | dotenv (2.1.0) 13 | http-cookie (1.0.2) 14 | domain_name (~> 0.5) 15 | httpclient (2.7.1) 16 | i18n (0.7.0) 17 | json (1.8.3) 18 | mechanize (2.7.4) 19 | domain_name (~> 0.5, >= 0.5.1) 20 | http-cookie (~> 1.0) 21 | mime-types (>= 1.17.2, < 3) 22 | net-http-digest_auth (~> 1.1, >= 1.1.1) 23 | net-http-persistent (~> 2.5, >= 2.5.2) 24 | nokogiri (~> 1.6) 25 | ntlm-http (~> 0.1, >= 0.1.1) 26 | webrobots (>= 0.0.9, < 0.2) 27 | mime-types (2.99) 28 | mini_portile2 (2.3.0) 29 | minitest (5.8.4) 30 | net-http-digest_auth (1.4) 31 | net-http-persistent (2.9.4) 32 | nokogiri (1.8.2) 33 | mini_portile2 (~> 2.3.0) 34 | ntlm-http (0.1.1) 35 | thor (0.19.1) 36 | thread_safe (0.3.5) 37 | tzinfo (1.2.2) 38 | thread_safe (~> 0.1) 39 | unf (0.1.4) 40 | unf_ext 41 | unf_ext (0.0.7.2) 42 | webrobots (0.1.2) 43 | 44 | PLATFORMS 45 | ruby 46 | 47 | DEPENDENCIES 48 | activesupport 49 | dotenv 50 | httpclient 51 | mechanize 52 | thor 53 | 54 | BUNDLED WITH 55 | 1.14.1 56 | -------------------------------------------------------------------------------- /lib/kappa-slack/uploader.rb: -------------------------------------------------------------------------------- 1 | require 'mechanize' 2 | require 'httpclient' 3 | require 'httpclient/webagent-cookie' 4 | require 'json' 5 | require 'fileutils' 6 | require 'digest/sha1' 7 | 8 | module KappaSlack 9 | class Uploader 10 | def initialize( 11 | slack_team_name:, 12 | slack_email:, 13 | slack_password:, 14 | skip_bttv_emotes:, 15 | skip_one_letter_emotes:) 16 | @slack_team_name = slack_team_name 17 | @slack_email = slack_email 18 | @slack_password = slack_password 19 | @skip_bttv_emotes = skip_bttv_emotes 20 | @skip_one_letter_emotes = skip_one_letter_emotes 21 | end 22 | 23 | def upload 24 | visit('/') do |login_page| 25 | login_page.form_with(:id => 'signin_form') do |form| 26 | form.email = slack_email 27 | form.password = slack_password 28 | end.submit 29 | 30 | visit('/admin/emoji') do |emoji_page| 31 | uploaded_page = emoji_page 32 | tmp_dir_path = File.join(APP_ROOT, 'tmp') 33 | FileUtils.mkdir_p(tmp_dir_path) 34 | 35 | emotes.each do |emote| 36 | existing_emote = uploaded_page.search(".emoji_row:contains(':#{emote[:name]}:')") 37 | next if existing_emote.present? 38 | file_path = File.join(tmp_dir_path, Digest::SHA1.hexdigest(emote[:name])) 39 | 40 | File.open(file_path, 'w') do |file| 41 | http.get_content(emote[:url]) do |chunk| 42 | file.write(chunk) 43 | end 44 | end 45 | 46 | next if File.size(file_path) > 64 * 1024 47 | KappaSlack.logger.info "Uploading #{emote[:name]}" 48 | 49 | uploaded_page = uploaded_page.form_with(:id => 'addemoji') do |form| 50 | form.field_with(:name => 'name').value = emote[:name] 51 | form.file_upload_with(:name => 'img').file_name = file_path 52 | end.submit 53 | end 54 | 55 | FileUtils.rm_rf(tmp_dir_path) 56 | end 57 | end 58 | end 59 | 60 | private 61 | 62 | attr_reader :slack_team_name, :slack_email, :slack_password 63 | 64 | def skip_bttv_emotes? 65 | @skip_bttv_emotes 66 | end 67 | 68 | def skip_one_letter_emotes? 69 | @skip_one_letter_emotes 70 | end 71 | 72 | def browser 73 | @browser ||= Mechanize.new 74 | end 75 | 76 | def http 77 | @http ||= HTTPClient.new 78 | end 79 | 80 | def visit(path, &block) 81 | browser.get(URI.join("https://#{slack_team_name}.slack.com", path), &block) 82 | end 83 | 84 | def bttv_emotes 85 | response = JSON.parse(http.get_content('https://api.betterttv.net/2/emotes')) 86 | url_template = "https:#{response['urlTemplate'].gsub('{{image}}', '1x')}" 87 | 88 | response['emotes'].map do |emote| 89 | { 90 | name: emote['code'].parameterize, 91 | url: url_template.gsub('{{id}}', emote['id']) 92 | } 93 | end 94 | end 95 | 96 | def twitch_emotes 97 | response = JSON.parse(http.get_content('https://twitchemotes.com/api_cache/v3/global.json')) 98 | url_template = 'https://static-cdn.jtvnw.net/emoticons/v1/{id}/1.0' 99 | 100 | response.map do |name, emote| 101 | { 102 | name: name.parameterize, 103 | url: url_template.gsub('{id}', emote['id'].to_s) 104 | } 105 | end 106 | end 107 | 108 | def emotes 109 | all_emotes = twitch_emotes 110 | all_emotes += bttv_emotes unless skip_bttv_emotes? 111 | 112 | if skip_one_letter_emotes? 113 | all_emotes.select { |e| e[:name].length > 1 } 114 | else 115 | all_emotes 116 | end 117 | end 118 | end 119 | end 120 | --------------------------------------------------------------------------------