Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supporting Socket Mode #1

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Lita.configure do |config|
config.robot.admins = ["U012A3BCD"]

config.adapters.slack.token = "abcd-1234567890-hWYd21AmMH2UHAkx29vb5c1Y"
config.adapters.slack.app_token = "xapp-1234567890-hWYd21AmMH2UHAkx29vb5c1Y"

config.adapters.slack.link_names = true
config.adapters.slack.parse = "full"
Expand Down
1 change: 1 addition & 0 deletions lib/lita/adapters/slack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module Adapters
class Slack < Adapter
# Required configuration attributes.
config :token, type: String, required: true
config :app_token, type: String, required: true
config :proxy, type: String
config :parse, type: [String]
config :link_names, type: [true, false]
Expand Down
73 changes: 61 additions & 12 deletions lib/lita/adapters/slack/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ def im_list(params: {})
response
end

def users_list(params: {})
call_paginated_api(method: 'users.list', params: params, result_field: 'members')
end

def users_profile_get(user)
call_api("users.profile.get", user: user)
end

def auth_test
call_api("auth.test")
end

def conversations_list(types: ["public_channel"], params: {})
params.merge!({
types: types.join(',')
Expand Down Expand Up @@ -145,16 +157,20 @@ def set_topic(channel, topic)

def rtm_start
Lita.logger.debug("Starting `rtm_start` method")
response_data = call_api("rtm.start")
response_data = call_app_api("apps.connections.open")
Lita.logger.debug("Started building TeamData")

ws_url = response_data["url"]

team_data = TeamData.new(
SlackIM.from_data_array(response_data["ims"]),
SlackUser.from_data(response_data["self"]),
SlackUser.from_data_array(response_data["users"]),
SlackChannel.from_data_array(response_data["channels"]) +
SlackChannel.from_data_array(response_data["groups"]),
response_data["url"],
SlackIM.from_data_array(im_list["ims"]),
SlackUser.from_data(get_identity),
SlackUser.from_data_array(users_list["members"]),
SlackChannel.from_data_array(channels_list["channels"]) +
SlackChannel.from_data_array(groups_list["groups"]),
ws_url,
)

Lita.logger.debug("Finished building TeamData")
Lita.logger.debug("Finishing method `rtm_start`")
team_data
Expand All @@ -166,29 +182,62 @@ def rtm_start
attr_reader :config
attr_reader :post_message_config

def get_identity
user_id = auth_test["user_id"]
profile = users_profile_get(user_id)["profile"]
profile["id"] = user_id
profile
end

def call_api(method, post_data = {})
Lita.logger.debug("Starting request to Slack API with rtm.start")
Lita.logger.debug("Starting request to Slack API")
response = connection.post(
"https://slack.com/api/#{method}",
{ token: config.token }.merge(post_data)
)
Lita.logger.debug("Finished request to Slack API rtm.start")
Lita.logger.debug("Finished request to Slack API")
data = parse_response(response, method)
Lita.logger.debug("Finished parsing rtm.start response")
Lita.logger.debug("Finished parsing response")
raise "Slack API call to #{method} returned an error: #{data["error"]}." if data["error"]

data
end

def call_app_api(method, post_data = {})
Lita.logger.debug("Starting request to Slack API with App Token")
response = connection.post(
"https://slack.com/api/#{method}",
post_data,
{"Authorization": "Bearer #{config.app_token}"}
)
Lita.logger.debug("Finished request to Slack API App Token")
data = parse_response(response, method)
Lita.logger.debug("Finished parsing response")
raise "Slack API call to #{method} returned an error: #{data["error"]}." if data["error"]

data
end

def connection
retry_options = {
retry_statuses: [429],
methods: %i[get post]
}
if stubs
Faraday.new { |faraday| faraday.adapter(:test, stubs) }
Faraday.new do |faraday|
faraday.request :url_encoded
faraday.request :retry, retry_options
faraday.adapter(:test, stubs)
end
else
options = {}
unless config.proxy.nil?
options = { proxy: config.proxy }
end
Faraday.new(options)
Faraday.new(options) do |faraday|
faraday.request :url_encoded
faraday.request :retry, retry_options
end
end
end

Expand Down
20 changes: 14 additions & 6 deletions lib/lita/adapters/slack/rtm_connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,9 @@ def run(queue = nil, options = {}, &block)
end
end

def send_messages(channel, strings)
strings.each do |string|
EventLoop.defer { websocket.send(safe_payload_for(channel, string)) }
end
def ack(envelope_id)
payload = MultiJson.dump({envelope_id: envelope_id, payload: {}})
EventLoop.defer { websocket.send(payload) }
end

def shut_down
Expand Down Expand Up @@ -125,8 +124,17 @@ def payload_for(channel, string)

def receive_message(event)
data = MultiJson.load(event.data)

EventLoop.defer { MessageHandler.new(robot, robot_id, data).handle }
return if data["retry_attempt"].to_i > 0

EventLoop.defer do
case data["type"]
when "events_api"
log.debug("Acknowledging #{data["envelope_id"]}")
ack(data["envelope_id"])
next unless data["payload"]["event"]["bot_id"].nil?
MessageHandler.new(robot, robot_id, data["payload"]["event"]).handle
end
end
end

def safe_payload_for(channel, string)
Expand Down
73 changes: 65 additions & 8 deletions spec/lita/adapters/slack/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@

let(:http_status) { 200 }
let(:token) { 'abcd-1234567890-hWYd21AmMH2UHAkx29vb5c1Y' }
let(:app_token) { 'apptoken' }
let(:config) { Lita::Adapters::Slack.configuration_builder.build }
let(:self_id) { "self_id" }

before do
config.token = token
config.app_token = app_token
end

describe "#im_open" do
Expand Down Expand Up @@ -796,9 +799,20 @@ def stubs(postMessage_options = {})

describe "#rtm_start" do
let(:http_status) { 200 }

before do
allow(subject).to receive(:auth_test) { {"user_id" => self_id} }
allow(subject).to receive(:im_list) { {"ims" => [{ "id" => 'D024BFF1M' }]} }
allow(subject).to receive(:users_profile_get).with(self_id) { {"profile" => { "id" => self_id }} }
allow(subject).to receive(:users_list) { {"members" => [{ "id" => 'U023BECGF' }]} }
allow(subject).to receive(:channels_list) { {"channels" => [{ "id" => 'D024BFF1M' }]} }
allow(subject).to receive(:groups_list) { {"groups" => [{ "id" => 'G024BFF1M' }]} }
end


let(:stubs) do
Faraday::Adapter::Test::Stubs.new do |stub|
stub.post('https://slack.com/api/rtm.start', token: token) do
stub.post('https://slack.com/api/apps.connections.open', {}) do
[http_status, {}, http_response]
end
end
Expand All @@ -809,18 +823,13 @@ def stubs(postMessage_options = {})
MultiJson.dump({
ok: true,
url: 'wss://example.com/',
users: [{ id: 'U023BECGF' }],
ims: [{ id: 'D024BFF1M' }],
self: { id: 'U12345678' },
channels: [{ id: 'C1234567890' }],
groups: [{ id: 'G0987654321' }],
})
end

it "has data on the bot user" do
response = subject.rtm_start

expect(response.self.id).to eq('U12345678')
expect(response.self.id).to eq(self_id)
end

it "has an array of IMs" do
Expand All @@ -843,6 +852,32 @@ def stubs(postMessage_options = {})
end
end

describe "#users_list" do
let(:http_status) { 200 }
let(:stubs) do
Faraday::Adapter::Test::Stubs.new do |stub|
stub.post('https://slack.com/api/users.list', token: token) do
[http_status, {}, http_response]
end
end
end

describe "with a successful response" do
let(:http_response) do
MultiJson.dump({
ok: true,
members: [{ id: 'U023BECGF', name: 'spengler', real_name: 'Egon Spengler' }],
})
end

it "has an array of members" do
response = subject.users_list

expect(response["members"][0]["name"]).to eq 'spengler'
end
end
end

describe "#conversations_list" do
describe "#conversations_list" do
let(:channel_id) { 'C024G4BGW' }
Expand Down Expand Up @@ -969,7 +1004,7 @@ def stubs(postMessage_options = {})
expect(response['channels'][1]['id']).to eq(channel_id_2)
end
end

describe "with a Slack error" do
let(:http_response) do
MultiJson.dump({
Expand All @@ -983,6 +1018,28 @@ def stubs(postMessage_options = {})
"Slack API call to conversations.list returned an error: invalid_auth."
)
end

context "ratelimited" do
let(:calls) { [] }
let(:http_status) { 429 }
let(:http_response) { MultiJson.dump({ok: false, error: "ratelimited"}) }
let(:stubs) do
Faraday::Adapter::Test::Stubs.new do |stub|
stub.post('https://slack.com/api/conversations.list', token: token, limit: 1, types: "public_channel") do |env|
calls << env.dup
[http_status, {}, http_response]
end
end
end

# Faraday retries max 2 times
it "raises RuntimeError after retry" do
expect { subject.conversations_list(params: { limit: 1 })}.to raise_error(
"Slack API call to conversations.list returned an error: ratelimited."
)
expect(calls.size).to eq(3)
end
end
end

describe "with an HTTP error" do
Expand Down
34 changes: 3 additions & 31 deletions spec/lita/adapters/slack/rtm_connection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def with_websocket(subject, queue)
end

describe "#run" do
let(:event) { double('Event', data: '{}') }
let(:event) { double('Event', data: '{"type": "events_api", "payload": {"event": {}}, "envelope_id": ""}') }
let(:message_handler) { instance_double('Lita::Adapters::Slack::MessageHandler') }

it "creates the WebSocket" do
Expand Down Expand Up @@ -114,6 +114,8 @@ def with_websocket(subject, queue)
{},
).and_return(message_handler)

allow(subject).to receive(:ack)

expect(message_handler).to receive(:handle)

# Testing private methods directly is bad, but it's difficult to get
Expand All @@ -133,34 +135,4 @@ def with_websocket(subject, queue)
end

end

describe "#send_messages" do
let(:message_json) { MultiJson.dump(id: 1, type: 'message', text: 'hi', channel: channel_id) }
let(:channel_id) { 'C024BE91L' }
let(:websocket) { instance_double("Faye::WebSocket::Client") }

before do
# TODO: Don't stub what you don't own!
allow(Faye::WebSocket::Client).to receive(:new).and_return(websocket)
allow(websocket).to receive(:on)
allow(websocket).to receive(:close)
allow(Lita::Adapters::Slack::EventLoop).to receive(:defer).and_yield
end

it "writes messages to the WebSocket" do
with_websocket(subject, queue) do |websocket|
expect(websocket).to receive(:send).with(message_json)

subject.send_messages(channel_id, ['hi'])
end
end

it "raises an ArgumentError if the payload is too large" do
with_websocket(subject, queue) do |websocket|
expect do
subject.send_messages(channel_id, ['x' * 16_001])
end.to raise_error(ArgumentError)
end
end
end
end
15 changes: 0 additions & 15 deletions spec/lita/adapters/slack_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,21 +112,6 @@
Lita::Adapters::Slack::SlackSource.new(room: 'C024BE91L', user: user, private_message: true)
end

describe "via the Web API" do
let(:api) { instance_double('Lita::Adapters::Slack::API') }

before do
allow(Lita::Adapters::Slack::API).to receive(:new).with(subject.config).and_return(api)
end

it "does not send via the RTM api" do
expect(rtm_connection).to_not receive(:send_messages)
expect(api).to receive(:send_messages).with(room_source.room, ['foo'])

subject.send_messages(room_source, ['foo'])
end
end

describe "with an ellipsis" do
let(:room_source) { Lita::Adapters::Slack::SlackSource.new(room: 'C024BE91L', extensions: { timestamp: "12345" } ) }
let(:api) { instance_double('Lita::Adapters::Slack::API') }
Expand Down