lita-slack 0.1.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+