├── .gitignore ├── Procfile ├── timezone.png ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md └── slack-timezone-converter.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.sw* 3 | *~ 4 | run.sh 5 | slack-rtmapi 6 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: bundle exec ruby slack-timezone-converter.rb $SLACK_TOKEN 2 | -------------------------------------------------------------------------------- /timezone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiosba/slack-timezone-converter/HEAD/timezone.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gem 'activesupport' 3 | gem 'slack-rtmapi', git: 'https://github.com/caiosba/slack-rtmapi', ref: '58c209f' 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/caiosba/slack-rtmapi 3 | revision: 58c209f40f02f0328c1a9e3df6ca4980a4d1c510 4 | ref: 58c209f 5 | specs: 6 | slack-rtmapi (1.0.0.rc4) 7 | websocket-driver 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | activesupport (4.2.2) 13 | i18n (~> 0.7) 14 | json (~> 1.7, >= 1.7.7) 15 | minitest (~> 5.1) 16 | thread_safe (~> 0.3, >= 0.3.4) 17 | tzinfo (~> 1.1) 18 | concurrent-ruby (1.1.6) 19 | i18n (0.9.5) 20 | concurrent-ruby (~> 1.0) 21 | json (1.8.6) 22 | minitest (5.14.0) 23 | thread_safe (0.3.6) 24 | tzinfo (1.2.6) 25 | thread_safe (~> 0.1) 26 | websocket-driver (0.5.1) 27 | websocket-extensions (>= 0.1.0) 28 | websocket-extensions (0.1.5) 29 | 30 | PLATFORMS 31 | ruby 32 | 33 | DEPENDENCIES 34 | activesupport 35 | slack-rtmapi! 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Caio Almeida 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Slack Timezone Converter 2 | 3 | An integration for Slack that converts any time string in a message to all timezones where the team is. 4 | 5 | ![Screenshot](timezone.png?raw=true "Screenshot") 6 | 7 | ## Usage 8 | 9 | Just invite the @bot to your channel. Then, any message that mentions @time and contains a time will be converted, for example: `Let's meet at 8am PDT please @time`. 10 | 11 | ## Description 12 | 13 | Currently supports any format parsable by [ActiveSupport](http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html). 14 | 15 | Any time a time string is found in a message that mentions `@time`, the integration, running in a server, converts it to all timezones 16 | where the team has at least one member. A message is sent back to the Slack channel with all the conversions and a fancy clock icon 17 | that represents the time. It supports any channel joined by the user whose token is passed as parameter. This user's timezone is used 18 | as the default when a timezone is not present on the parsed message. 19 | 20 | Example message that would be parsed: `Hey, our meeting is at 12h30 PDT @time`. 21 | 22 | ## Installation 23 | 24 | In order to use this integration, the following Ruby libraries are needed: 25 | 26 | * slack-rtmapi 27 | * active\_support 28 | * json 29 | 30 | But they can be installed by using `bundle`: 31 | 32 | `bundle install` 33 | 34 | After all requirements are met, it's just necessary to run the code, passing the Slack token as parameter: 35 | 36 | `ruby slack-timezone-converter.rb ` 37 | 38 | This program runs indefinitely and listens for new messages on the Slack channels. It can be stopped by just stopping the process. 39 | 40 | ## TODO 41 | 42 | * Correctly identify the format where a dot is the separator between hour and minutes (e.g., "8.30am") 43 | 44 | ## References 45 | 46 | * https://api.slack.com/web#basics 47 | * https://api.slack.com/rtm 48 | * https://github.com/caiosba/slack-rtmapi 49 | -------------------------------------------------------------------------------- /slack-timezone-converter.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'active_support/all' 4 | require 'json' 5 | Bundler.require 6 | 7 | TOKEN = ARGV[0] # Get one at https://api.slack.com/web#basics 8 | PER_LINE = ARGV[1] || 1 # Number of times per line 9 | MESSAGE = ARGV[2].to_s # Additional message to be appended 10 | 11 | # Get a Slack clock emoji from a time object 12 | 13 | def slack_clock_emoji_from_time(time) 14 | hour = time.hour % 12 15 | hour = 12 if hour == 0 16 | time.min==30? ":clock#{hour}30:" : ":clock#{hour}:" 17 | end 18 | 19 | # Normalize times 20 | 21 | def normalize(text) 22 | text.gsub(/([0-9]{1,2})([0-9]{2})( ?([aA]|[pP])[mM])/, '\1:\2\3') 23 | end 24 | 25 | # Get the current user from token 26 | 27 | uri = URI.parse("https://slack.com/api/auth.test?token=#{TOKEN}") 28 | http = Net::HTTP.new(uri.host, uri.port) 29 | http.use_ssl = true 30 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 31 | response = http.get(uri.request_uri) 32 | CURRENT_USER = JSON.parse(response.body)['user_id'] 33 | 34 | # Get users list and all available timezones and set default timezone 35 | 36 | uri = URI.parse("https://slack.com/api/users.list?token=#{TOKEN}") 37 | http = Net::HTTP.new(uri.host, uri.port) 38 | http.use_ssl = true 39 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 40 | response = http.get(uri.request_uri) 41 | timezones = {} 42 | users = {} 43 | JSON.parse(response.body)['members'].each do |user| 44 | offset, label = user['tz_offset'], user['tz'] 45 | next if offset.nil? or offset == 0 or label.nil? or user['deleted'] 46 | label = ActiveSupport::TimeZone.find_tzinfo(label).current_period.abbreviation.to_s 47 | if key = timezones.key(offset) and !key.split(' / ').include?(label) 48 | timezones.delete(key) 49 | label = key + ' / ' + label 50 | end 51 | timezones[label] = offset unless timezones.has_value?(offset) 52 | users[user['id']] = { offset: offset, tz: ActiveSupport::TimeZone[offset].tzinfo.name } 53 | end 54 | 55 | timezones = timezones.sort_by{ |key, value| value } 56 | 57 | #Time.zone = users[CURRENT_USER][:tz] 58 | 59 | # Connect to Slack 60 | 61 | url = SlackRTM.get_url token: TOKEN 62 | client = SlackRTM::Client.new websocket_url: url 63 | 64 | # Listen for new messages (events of type "message") 65 | 66 | puts "[#{Time.now}] Connected to Slack!" 67 | 68 | client.on :message do |data| 69 | if data['type'] === 'message' and !data['text'].nil? and data['subtype'].nil? and data['reply_to'].nil? and (data['text'].include?("@time") or data['text'].include?("@U03N0KXMD") or data['text'].include?("@timebot") or data['text'].include?("@U5D0ARDSS")) and 70 | !data['text'].gsub(/<[^>]+>/, '').match(/[0-9](([hH]([0123456789 ?:,;.]|$))|( ?[aA][mM])|( ?[pP][mM])|(:[0-9]{2}))/).nil? 71 | 72 | # Identify time patterns 73 | begin 74 | Time.zone = begin users[data['user']][:tz] rescue users[CURRENT_USER][:tz] end 75 | text = normalize data['text'] 76 | time = Time.zone.parse(text).utc 77 | puts "[#{Time.now}] Got time #{time}" 78 | 79 | text = [] 80 | i = 0 81 | timezones.each do |label, offset| 82 | i += 1 83 | localtime = time + offset 84 | emoji = slack_clock_emoji_from_time(localtime) 85 | message = "#{emoji} #{localtime.strftime('%H:%M')} #{label}" 86 | message += (i % PER_LINE.to_i == 0) ? "\n" : " " 87 | text << (users[data['user']] && offset == users[data['user']][:offset] ? "#{message}" : message) 88 | end 89 | 90 | text << (MESSAGE % time.to_i.to_s) 91 | 92 | puts "[#{Time.now}] Sending message..." 93 | client.send({ type: 'message', channel: data['channel'], text: text.join }) 94 | rescue Exception => e 95 | # Just ignore the message 96 | puts "Exception: #{e.message}" 97 | end 98 | end 99 | end 100 | 101 | # Runs forever until an exception happens or the process is stopped/killed 102 | 103 | client.main_loop 104 | assert false 105 | --------------------------------------------------------------------------------