lita-slack 0.1.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 69ca07c3550da36b31e59b943598fe2393f0f07f
4
- data.tar.gz: 3638c4841bd58cf6ae53b33626449d6f9122a4e1
3
+ metadata.gz: d8487603a85d7972122eb6826b21534e31484a63
4
+ data.tar.gz: ec5c0068bdf7b935299f099eff9b1cecefad4b2b
5
5
  SHA512:
6
- metadata.gz: 31299e3c3e8501f540c323a7a4e8978fc0e1d8ec40519899519505d8e1bfc42a7cf5088cb8c542be5cc03351906fa79b651f84ec6e4fe79836167828f18820d2
7
- data.tar.gz: 8580bd596871b84509bddb9c00ef0432b035eb0438dc597f7de8393e7d40b93ff98537a2c67460ef87e7e5d7f1dab7dd615c367236966308268ec1d10f32ffb4
6
+ metadata.gz: b1b9000c60ee73995c0cfce1f36499d3b6756a0e4558bf0c96eff7bc2ceaa24e11fbe57952232291c89439850b5039739d1f93960d796081009cbeb76471a714
7
+ data.tar.gz: 3c8b5d80b99919f23eb2f0fce811b8f8f551cfc316e8f2ed51c73c9834e33bb904d8a75f4e5a00540cd664e5d41c9e15ec91fb6e23a3eb4e83d9afbb9e9b4a44
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ script: bundle exec rake
5
+ before_install:
6
+ - gem update --system
7
+ services:
8
+ - redis-server
9
+
data/README.md CHANGED
@@ -1,50 +1,37 @@
1
1
  # lita-slack
2
2
 
3
- **lita-slack** is an adapter for [Lita](https://github.com/jimmycuadra/lita) that allows you to use the robot with [Slack](https://slack.com/). As **lita-slack** is one way, robot messages to Slack, it depends on [lita-slack-handler](https://github.com/kenjij/lita-slack-handler) gem to receive messages from Slack.
3
+ **lita-slack** is an adapter for [Lita](https://www.lita.io/) that allows you to use the robot with [Slack](https://slack.com/). The current adapter is not compatible with pre-1.0.0 versions, as it now uses Slack's [Real Time Messaging API](https://api.slack.com/rtm).
4
4
 
5
5
  ## Installation
6
6
 
7
- Add **lita-slack** and **lita-slack-handler** to your Lita instance's Gemfile:
7
+ Add **lita-slack** to your Lita instance's Gemfile:
8
8
 
9
9
  ``` ruby
10
10
  gem "lita-slack"
11
- gem "lita-slack-handler"
12
11
  ```
13
12
 
14
13
  ## Configuration
15
14
 
16
- **First, you need to make sure your Slack team has [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook) integration setup. For configuration regarding lita-slack-handler, see its [README](https://github.com/kenjij/lita-slack-handler).**
17
-
18
- Then, define the following attributes:
19
-
20
15
  ### Required attributes
21
16
 
22
- * `incoming_token` (String) – Slack integration token.
23
- * `team_domain` (String) – Slack team domain; subdomain of slack.com.
24
-
25
- ### Optional attributes
17
+ * `token` (String) – The bot's Slack API token. Create a bot and get its token at https://my.slack.com/services/new/bot.
26
18
 
27
- * `incoming_url` (String) Default: https://<team_domain>.slack.com/services/hooks/incoming-webhook
28
- * `username` (String) – Display name of the robot; default: whatever is set in Slack integration
29
- * `add_mention` (Bool) – Always prefix message with mention of the user which it's directed to; this triggers a notification.
19
+ **Note**: When using lita-slack, the adapter will overwrite the bot's name and mention name with the values set on the server, so `config.robot.name` and `config.robot.mention_name` will have no effect.
30
20
 
31
- ### Example lita_config.rb
21
+ ### Example
32
22
 
33
23
  ``` ruby
34
24
  Lita.configure do |config|
35
- config.robot.name = "Lita"
36
- config.robot.mention_name = "@lita"
37
- # Select the Slack adapter
38
25
  config.robot.adapter = :slack
39
- # lita-slack adapter config
40
- config.adapter.incoming_token = "aN1NvAlIdDuMmYt0k3n"
41
- config.adapter.team_domain = "example"
42
- config.adapter.username = "lita"
43
- # Some more handlers and other config
44
- # .....
26
+ config.adapters.slack.token = "abcd-1234567890-hWYd21AmMH2UHAkx29vb5c1Y"
45
27
  end
46
28
  ```
47
29
 
30
+ ## Events
31
+
32
+ * `:connected` - When the robot has connected to Slack. No payload.
33
+ * `:disconnected` - When the robot has disconnected from Slack. No payload.
34
+
48
35
  ## License
49
36
 
50
37
  [MIT](http://opensource.org/licenses/MIT)
@@ -0,0 +1,69 @@
1
+ require 'faraday'
2
+
3
+ require 'lita/adapters/slack/team_data'
4
+ require 'lita/adapters/slack/slack_im'
5
+ require 'lita/adapters/slack/slack_user'
6
+
7
+ module Lita
8
+ module Adapters
9
+ class Slack < Adapter
10
+ class API
11
+ def initialize(token, stubs = nil)
12
+ @token = token
13
+ @stubs = stubs
14
+ end
15
+
16
+ def im_open(user_id)
17
+ response_data = call_api("im.open", user: user_id)
18
+
19
+ SlackIM.new(response_data["channel"]["id"], user_id)
20
+ end
21
+
22
+ def rtm_start
23
+ response_data = call_api("rtm.start")
24
+
25
+ TeamData.new(
26
+ SlackIM.from_data_array(response_data["ims"]),
27
+ SlackUser.from_data(response_data["self"]),
28
+ SlackUser.from_data_array(response_data["users"]),
29
+ response_data["url"]
30
+ )
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :stubs
36
+ attr_reader :token
37
+
38
+ def call_api(method, post_data = {})
39
+ response = connection.post(
40
+ "https://slack.com/api/#{method}",
41
+ { token: token }.merge(post_data)
42
+ )
43
+
44
+ data = parse_response(response, method)
45
+
46
+ raise "Slack API call to #{method} returned an error: #{data["error"]}." if data["error"]
47
+
48
+ data
49
+ end
50
+
51
+ def connection
52
+ if stubs
53
+ Faraday.new { |faraday| faraday.adapter(:test, stubs) }
54
+ else
55
+ Faraday.new
56
+ end
57
+ end
58
+
59
+ def parse_response(response, method)
60
+ unless response.success?
61
+ raise "Slack API call to #{method} failed with status code #{response.status}."
62
+ end
63
+
64
+ MultiJson.load(response.body)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,34 @@
1
+ module Lita
2
+ module Adapters
3
+ class Slack < Adapter
4
+ class IMMapping
5
+ def initialize(api, ims)
6
+ @api = api
7
+ @mapping = {}
8
+
9
+ add_mappings(ims)
10
+ end
11
+
12
+ def add_mapping(im)
13
+ mapping[im.user_id] = im.id
14
+ end
15
+
16
+ def add_mappings(ims)
17
+ ims.each { |im| add_mapping(im) }
18
+ end
19
+
20
+ def im_for(user_id)
21
+ mapping.fetch(user_id) do
22
+ im = api.im_open(user_id)
23
+ mapping[user_id] = im.id
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :api
30
+ attr_reader :mapping
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,118 @@
1
+ module Lita
2
+ module Adapters
3
+ class Slack < Adapter
4
+ class MessageHandler
5
+ def initialize(robot, robot_id, data)
6
+ @robot = robot
7
+ @robot_id = robot_id
8
+ @data = data
9
+ @type = data["type"]
10
+ end
11
+
12
+ def handle
13
+ case type
14
+ when "hello"
15
+ handle_hello
16
+ when "message"
17
+ handle_message
18
+ when "user_change", "team_join"
19
+ handle_user_change
20
+ when "bot_added", "bot_changed"
21
+ handle_bot_change
22
+ when "error"
23
+ handle_error
24
+ else
25
+ handle_unknown
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :data
32
+ attr_reader :robot
33
+ attr_reader :robot_id
34
+ attr_reader :type
35
+
36
+ def body
37
+ data["text"].sub(/^\s*<@#{robot_id}>/, "@#{robot.mention_name}")
38
+ end
39
+
40
+ def channel
41
+ data["channel"]
42
+ end
43
+
44
+ def dispatch_message(user)
45
+ source = Source.new(user: user, room: channel)
46
+ message = Message.new(robot, body, source)
47
+ log.debug("Dispatching message to Lita from #{user.id}.")
48
+ robot.receive(message)
49
+ end
50
+
51
+ def from_self?(user)
52
+ if data["subtype"] == "bot_message"
53
+ robot_user = User.find_by_name(robot.name)
54
+
55
+ robot_user && robot_user.id == user.id
56
+ end
57
+ end
58
+
59
+ def handle_bot_change
60
+ log.debug("Updating user data for bot.")
61
+ UserCreator.create_user(data["bot"], robot, robot_id)
62
+ end
63
+
64
+ def handle_error
65
+ error = data["error"]
66
+ code = error["code"]
67
+ message = error["msg"]
68
+ log.error("Error with code #{code} received from Slack: #{message}")
69
+ end
70
+
71
+ def handle_hello
72
+ log.info("Connected to Slack.")
73
+ robot.trigger(:connected)
74
+ end
75
+
76
+ def handle_message
77
+ return unless supported_subtype?
78
+
79
+ user = User.find_by_id(data["user"]) || User.create(data["user"])
80
+
81
+ return if from_self?(user)
82
+
83
+ dispatch_message(user)
84
+ end
85
+
86
+ def handle_unknown
87
+ unless data["reply_to"]
88
+ log.debug("#{type} event received from Slack and will be ignored.")
89
+ end
90
+ end
91
+
92
+ def handle_user_change
93
+ log.debug("Updating user data.")
94
+ UserCreator.create_user(data["user"], robot, robot_id)
95
+ end
96
+
97
+ def log
98
+ Lita.logger
99
+ end
100
+
101
+ # Types of messages Lita should dispatch to handlers.
102
+ def supported_message_subtypes
103
+ %w(bot_message me_message)
104
+ end
105
+
106
+ def supported_subtype?
107
+ subtype = data["subtype"]
108
+
109
+ if subtype
110
+ supported_message_subtypes.include?(subtype)
111
+ else
112
+ true
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,103 @@
1
+ require 'eventmachine'
2
+ require 'faye/websocket'
3
+ require 'multi_json'
4
+
5
+ require 'lita/adapters/slack/api'
6
+ require 'lita/adapters/slack/im_mapping'
7
+ require 'lita/adapters/slack/message_handler'
8
+ require 'lita/adapters/slack/user_creator'
9
+
10
+ module Lita
11
+ module Adapters
12
+ class Slack < Adapter
13
+ class RTMConnection
14
+ MAX_MESSAGE_BYTES = 16_000
15
+
16
+ class << self
17
+ def build(robot, token)
18
+ new(robot, token, API.new(token).rtm_start)
19
+ end
20
+ end
21
+
22
+ def initialize(robot, token, team_data)
23
+ @robot = robot
24
+ @im_mapping = IMMapping.new(token, team_data.ims)
25
+ @websocket_url = team_data.websocket_url
26
+ @robot_id = team_data.self.id
27
+
28
+ UserCreator.create_users(team_data.users, robot, robot_id)
29
+ end
30
+
31
+ def im_for(user_id)
32
+ im_mapping.im_for(user_id)
33
+ end
34
+
35
+ def run(queue = nil)
36
+ EM.run do
37
+ log.debug("Connecting to the Slack Real Time Messaging API.")
38
+ @websocket = Faye::WebSocket::Client.new(websocket_url, nil, ping: 10)
39
+
40
+ websocket.on(:open) { log.debug("Connected to the Slack Real Time Messaging API.") }
41
+ websocket.on(:message) { |event| receive_message(event) }
42
+ websocket.on(:close) { log.info("Disconnected from Slack.") }
43
+ websocket.on(:error) { |event| log.debug("WebSocket error: #{event.message}") }
44
+
45
+ queue << websocket if queue
46
+ end
47
+ end
48
+
49
+ def send_messages(channel, strings)
50
+ strings.each do |string|
51
+ websocket.send(safe_payload_for(channel, string))
52
+ end
53
+ end
54
+
55
+ def shut_down
56
+ if websocket
57
+ log.debug("Closing connection to the Slack Real Time Messaging API.")
58
+ websocket.close
59
+ end
60
+
61
+ EM.stop if EM.reactor_running?
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :im_mapping
67
+ attr_reader :robot
68
+ attr_reader :robot_id
69
+ attr_reader :websocket
70
+ attr_reader :websocket_url
71
+
72
+ def log
73
+ Lita.logger
74
+ end
75
+
76
+ def payload_for(channel, string)
77
+ MultiJson.dump({
78
+ id: 1,
79
+ type: 'message',
80
+ text: string,
81
+ channel: channel
82
+ })
83
+ end
84
+
85
+ def receive_message(event)
86
+ data = MultiJson.load(event.data)
87
+
88
+ MessageHandler.new(robot, robot_id, data).handle
89
+ end
90
+
91
+ def safe_payload_for(channel, string)
92
+ payload = payload_for(channel, string)
93
+
94
+ if payload.size > MAX_MESSAGE_BYTES
95
+ raise ArgumentError, "Cannot send payload greater than #{MAX_MESSAGE_BYTES} bytes."
96
+ end
97
+
98
+ payload
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,26 @@
1
+ module Lita
2
+ module Adapters
3
+ class Slack < Adapter
4
+ class SlackIM
5
+ class << self
6
+ def from_data_array(ims_data)
7
+ ims_data.map { |im_data| from_data(im_data) }
8
+ end
9
+
10
+ def from_data(im_data)
11
+ new(im_data['id'], im_data['user'])
12
+ end
13
+ end
14
+
15
+ attr_reader :id
16
+ attr_reader :user_id
17
+
18
+ def initialize(id, user_id)
19
+ @id = id
20
+ @user_id = user_id
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,31 @@
1
+ module Lita
2
+ module Adapters
3
+ class Slack < Adapter
4
+ class SlackUser
5
+ class << self
6
+ def from_data(user_data)
7
+ new(
8
+ user_data['id'],
9
+ user_data['name'],
10
+ user_data['real_name']
11
+ )
12
+ end
13
+
14
+ def from_data_array(users_data)
15
+ users_data.map { |user_data| from_data(user_data) }
16
+ end
17
+ end
18
+
19
+ attr_reader :id
20
+ attr_reader :name
21
+ attr_reader :real_name
22
+
23
+ def initialize(id, name, real_name)
24
+ @id = id
25
+ @name = name
26
+ @real_name = real_name
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ module Lita
2
+ module Adapters
3
+ class Slack < Adapter
4
+ TeamData = Struct.new(:ims, :self, :users, :websocket_url)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,34 @@
1
+ module Lita
2
+ module Adapters
3
+ class Slack < Adapter
4
+ class UserCreator
5
+ class << self
6
+ def create_user(slack_user, robot, robot_id)
7
+ User.create(
8
+ slack_user.id,
9
+ name: real_name(slack_user),
10
+ mention_name: slack_user.name
11
+ )
12
+
13
+ update_robot(robot, slack_user) if slack_user.id == robot_id
14
+ end
15
+
16
+ def create_users(slack_users, robot, robot_id)
17
+ slack_users.each { |slack_user| create_user(slack_user, robot, robot_id) }
18
+ end
19
+
20
+ private
21
+
22
+ def real_name(slack_user)
23
+ slack_user.real_name.size > 0 ? slack_user.real_name : slack_user.name
24
+ end
25
+
26
+ def update_robot(robot, slack_user)
27
+ robot.name = slack_user.real_name
28
+ robot.mention_name = slack_user.name
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,91 +1,46 @@
1
- require 'lita'
2
- require 'faraday'
3
- require 'json'
1
+ require 'lita/adapters/slack/rtm_connection'
4
2
 
5
3
  module Lita
6
- module Adapters
7
- class Slack < Adapter
8
- # Required Lita config keys (via lita_config.rb)
9
- require_configs :incoming_token, :team_domain
4
+ module Adapters
5
+ class Slack < Adapter
6
+ # Required configuration attributes.
7
+ config :token, type: String, required: true
10
8
 
11
- # Adapter main run loop
12
- def run
13
- log.debug 'Slack::run started'
14
- sleep
15
- rescue Interrupt
16
- shut_down
17
- end
9
+ # Starts the connection.
10
+ def run
11
+ return if rtm_connection
18
12
 
19
- def send_messages(target, strings)
20
- log.debug 'Slack::send_messages started'
21
- status = http_post prepare_payload(target, strings)
22
- log.error "Slack::send_messages failed to send (#{status})" if status != 200
23
- log.debug 'Slack::send_messages ending'
24
- end
25
-
26
- def set_topic(target, topic)
27
- # Slack currently provides no method
28
- log.info 'Slack::set_topic no implementation'
29
- end
30
-
31
- def shut_down
32
- end
33
-
34
- private
35
-
36
- def prepare_payload(target, strings)
37
- if not defined?(target.room)
38
- channel_id = nil
39
- log.warn "Slack::prepare_payload proceeding without channel designation"
40
- else
41
- channel_id = target.room
42
- end
43
- payload = {'channel' => channel_id, 'username' => username}
44
- payload['text'] = strings.join('\n')
45
- if add_mention? and defined?(target.user.id)
46
- payload['text'] = payload['text'].prepend("<@#{target.user.id}> ")
47
- end
48
- return payload
49
- end
13
+ @rtm_connection = RTMConnection.build(robot, config.token)
14
+ rtm_connection.run
15
+ end
50
16
 
51
- def http_post(payload)
52
- res = Faraday.post do |req|
53
- log.debug "Slack::http_post sending payload to #{incoming_url}; length: #{payload.to_json.size}"
54
- req.url incoming_url, :token => config.incoming_token
55
- req.headers['Content-Type'] = 'application/json'
56
- req.body = payload.to_json
57
- end
58
- log.info "Slack::http_post sent payload with response status #{res.status}"
59
- log.debug "Slack::http_post response body: #{res.body}"
60
- return res.status
61
- end
17
+ def send_messages(target, strings)
18
+ return unless rtm_connection
62
19
 
63
- #
64
- # Accessor shortcuts
65
- #
66
- def config
67
- Lita.config.adapter
68
- end
20
+ rtm_connection.send_messages(channel_for(target), strings)
21
+ end
69
22
 
70
- def log
71
- Lita.logger
72
- end
23
+ def shut_down
24
+ return unless rtm_connection
73
25
 
74
- def incoming_url
75
- config.incoming_url ||
76
- "https://#{config.team_domain}.slack.com/services/hooks/incoming-webhook"
26
+ rtm_connection.shut_down
27
+ robot.trigger(:disconnected)
77
28
  end
78
29
 
79
- def username
80
- config.username
81
- end
30
+ private
31
+
32
+ attr_reader :rtm_connection
82
33
 
83
- def add_mention?
84
- config.add_mention
34
+ def channel_for(target)
35
+ if target.room
36
+ target.room
37
+ else
38
+ rtm_connection.im_for(target.user.id)
39
+ end
85
40
  end
86
- end
41
+ end
87
42
 
88
- # Register Slack adapter to Lita
89
- Lita.register_adapter(:slack, Slack)
90
- end
43
+ # Register Slack adapter to Lita
44
+ Lita.register_adapter(:slack, Slack)
45
+ end
91
46
  end
data/lib/lita-slack.rb CHANGED
@@ -1 +1,7 @@
1
+ require "lita"
2
+
3
+ Lita.load_locales Dir[File.expand_path(
4
+ File.join("..", "..", "locales", "*.yml"), __FILE__
5
+ )]
6
+
1
7
  require "lita/adapters/slack"
data/lita-slack.gemspec CHANGED
@@ -1,8 +1,8 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "lita-slack"
3
- spec.version = "0.1.2"
4
- spec.authors = ["Ken J."]
5
- spec.email = ["kenjij@gmail.com"]
3
+ spec.version = "1.0.0"
4
+ spec.authors = ["Ken J.", "Jimmy Cuadra"]
5
+ spec.email = ["kenjij@gmail.com", "jimmy@jimmycuadra.com"]
6
6
  spec.description = %q{Lita adapter for Slack.}
7
7
  spec.summary = %q{Lita adapter for Slack.}
8
8
  spec.homepage = "https://github.com/kenjij/lita-slack"
@@ -14,10 +14,16 @@ Gem::Specification.new do |spec|
14
14
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
15
15
  spec.require_paths = ["lib"]
16
16
 
17
- spec.add_runtime_dependency "lita", ">= 2.7"
18
- spec.add_runtime_dependency "lita-slack-handler", ">= 0.2.1"
17
+ spec.add_runtime_dependency "eventmachine"
18
+ spec.add_runtime_dependency "faraday"
19
+ spec.add_runtime_dependency "faye-websocket", ">= 0.8.0"
20
+ spec.add_runtime_dependency "lita", ">= 4.0.4"
21
+ spec.add_runtime_dependency "multi_json"
19
22
 
20
23
  spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rack-test"
21
25
  spec.add_development_dependency "rake"
22
26
  spec.add_development_dependency "rspec", ">= 3.0.0"
27
+ spec.add_development_dependency "simplecov"
28
+ spec.add_development_dependency "coveralls"
23
29
  end
data/locales/en.yml ADDED
@@ -0,0 +1,5 @@
1
+ en:
2
+ lita:
3
+ adapters:
4
+ slack:
5
+