diff --git a/lib/lita-slack.rb b/lib/lita-slack.rb index 85426dd..bc6bf15 100644 --- a/lib/lita-slack.rb +++ b/lib/lita-slack.rb @@ -4,4 +4,4 @@ File.join("..", "..", "locales", "*.yml"), __FILE__ )] -require "lita/adapters/slack" +require_relative "lita/adapters/slack" diff --git a/lib/lita/adapters/slack.rb b/lib/lita/adapters/slack.rb index 6107cb6..f98eb36 100644 --- a/lib/lita/adapters/slack.rb +++ b/lib/lita/adapters/slack.rb @@ -1,5 +1,5 @@ -require 'lita/adapters/slack/chat_service' -require 'lita/adapters/slack/rtm_connection' +require_relative 'slack/chat_service' +require_relative 'slack/rtm_connection' module Lita module Adapters @@ -40,7 +40,11 @@ def roster(target) def send_messages(target, strings) api = API.new(config) - api.send_messages(channel_for(target), strings) + if target.respond_to?(:thread) + api.send_messages(target.room || target.user&.id, strings, thread_ts: target.thread) + else + api.send_messages(target.room || target.user&.id, strings) + end end def set_topic(target, topic) @@ -60,14 +64,6 @@ def shut_down attr_reader :rtm_connection - def channel_for(target) - if target.private_message? - rtm_connection.im_for(target.user.id) - else - target.room - end - end - def channel_roster(room_id, api) response = api.channels_info room_id response['channel']['members'] diff --git a/lib/lita/adapters/slack/api.rb b/lib/lita/adapters/slack/api.rb index 3bb437c..8a2ca9f 100644 --- a/lib/lita/adapters/slack/api.rb +++ b/lib/lita/adapters/slack/api.rb @@ -1,9 +1,9 @@ require 'faraday' -require 'lita/adapters/slack/team_data' -require 'lita/adapters/slack/slack_im' -require 'lita/adapters/slack/slack_user' -require 'lita/adapters/slack/slack_channel' +require_relative 'team_data' +require_relative 'slack_im' +require_relative 'slack_user' +require_relative 'slack_channel' module Lita module Adapters @@ -46,6 +46,10 @@ def im_list call_api("im.list") end + def user_info( user_id ) + call_api("users.info", user: user_id) + end + def send_attachments(room_or_user, attachments) call_api( "chat.postMessage", @@ -55,29 +59,31 @@ def send_attachments(room_or_user, attachments) ) end - def send_messages(channel_id, messages) - call_api( - "chat.postMessage", - **post_message_config, - as_user: true, + def send_messages(channel_id, messages, additional_payload = {}) + data = { channel: channel_id, + as_user: true, text: messages.join("\n"), - ) + }.merge(post_message_config) + .merge(additional_payload) + + call_api( "chat.postMessage", **data ) end def set_topic(channel, topic) call_api("channels.setTopic", channel: channel, topic: topic) end - def rtm_start - response_data = call_api("rtm.start") + def rtm_connect + response_data = call_api("rtm.connect") + + raise RuntimeError, response_data["error"] if response_data["ok"] != true TeamData.new( - SlackIM.from_data_array(response_data["ims"]), + response_data["team"]["id"], + response_data["team"]["name"], + response_data["team"]["domain"], 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"], ) end diff --git a/lib/lita/adapters/slack/chat_service.rb b/lib/lita/adapters/slack/chat_service.rb index 317997b..48b7475 100644 --- a/lib/lita/adapters/slack/chat_service.rb +++ b/lib/lita/adapters/slack/chat_service.rb @@ -1,4 +1,4 @@ -require "lita/adapters/slack/attachment" +require_relative "attachment" module Lita module Adapters diff --git a/lib/lita/adapters/slack/message_handler.rb b/lib/lita/adapters/slack/message_handler.rb index b1572b2..891880e 100644 --- a/lib/lita/adapters/slack/message_handler.rb +++ b/lib/lita/adapters/slack/message_handler.rb @@ -1,3 +1,5 @@ +require_relative 'slack_source' + module Lita module Adapters class Slack < Adapter @@ -112,9 +114,20 @@ def channel data["channel"] end + def thread + data["thread_ts"] + end + + def user_lookup( user_id ) + user = User.find_by_id( user_id ) + return if user + + User.create( user_id ) + end + def dispatch_message(user) room = Lita::Room.find_by_id(channel) - source = Source.new(user: user, room: room || channel) + source = SlackSource.new(user: user, room: room || channel, thread: thread) source.private_message! if channel && channel[0] == "D" message = Message.new(robot, body, source) message.command! if source.private_message? @@ -123,8 +136,8 @@ def dispatch_message(user) robot.receive(message) end - def from_self?(user) - user.id == robot_id + def from_self?(user_id) + user_id == robot_id end def handle_bot_change @@ -153,9 +166,13 @@ def handle_message return unless supported_subtype? return if data["user"] == 'USLACKBOT' - user = User.find_by_id(data["user"]) || User.create(data["user"]) + return if from_self?(data["user"]) - return if from_self?(user) + user = User.find_by_id(data["user"]) + if user.nil? + user_data = API.new(robot.config.adapters.slack).user_info(data["user"])['user'] + user = User.create( user_id, user_data ) + end dispatch_message(user) end @@ -163,11 +180,12 @@ def handle_message def handle_reaction log.debug "#{type} event received from Slack" + # avoid processing reactions added/removed by self + return if from_self?(data["user"]) + # find or create user user = User.find_by_id(data["user"]) || User.create(data["user"]) - # avoid processing reactions added/removed by self - return if from_self?(user) # find or create item_user item_user = User.find_by_id(data["item_user"]) || User.create(data["item_user"]) diff --git a/lib/lita/adapters/slack/rtm_connection.rb b/lib/lita/adapters/slack/rtm_connection.rb index 90bae4d..cb9f661 100644 --- a/lib/lita/adapters/slack/rtm_connection.rb +++ b/lib/lita/adapters/slack/rtm_connection.rb @@ -1,12 +1,12 @@ require 'faye/websocket' require 'multi_json' -require 'lita/adapters/slack/api' -require 'lita/adapters/slack/event_loop' -require 'lita/adapters/slack/im_mapping' -require 'lita/adapters/slack/message_handler' -require 'lita/adapters/slack/room_creator' -require 'lita/adapters/slack/user_creator' +require_relative 'api' +require_relative 'event_loop' +require_relative 'im_mapping' +require_relative 'message_handler' +require_relative 'room_creator' +require_relative 'user_creator' module Lita module Adapters @@ -17,23 +17,15 @@ class RTMConnection class << self def build(robot, config) - new(robot, config, API.new(config).rtm_start) + new(robot, config, API.new(config).rtm_connect) end end def initialize(robot, config, team_data) @robot = robot @config = config - @im_mapping = IMMapping.new(API.new(config), team_data.ims) @websocket_url = team_data.websocket_url @robot_id = team_data.self.id - - UserCreator.create_users(team_data.users, robot, robot_id) - RoomCreator.create_rooms(team_data.channels, robot) - end - - def im_for(user_id) - im_mapping.im_for(user_id) end def run(queue = nil, options = {}) diff --git a/lib/lita/adapters/slack/slack_source.rb b/lib/lita/adapters/slack/slack_source.rb new file mode 100644 index 0000000..611bb26 --- /dev/null +++ b/lib/lita/adapters/slack/slack_source.rb @@ -0,0 +1,15 @@ +module Lita + module Adapters + class Slack < Adapter + # @api private + class SlackSource < Lita::Source + attr_reader :thread + + def initialize( user: nil, room: nil, private_message: nil, thread: nil ) + super( user: user, room: room, private_message: private_message ) + @thread = thread + end + end + end + end +end diff --git a/lib/lita/adapters/slack/team_data.rb b/lib/lita/adapters/slack/team_data.rb index e77b3b3..8cdb326 100644 --- a/lib/lita/adapters/slack/team_data.rb +++ b/lib/lita/adapters/slack/team_data.rb @@ -2,7 +2,7 @@ module Lita module Adapters # @api private class Slack < Adapter - TeamData = Struct.new(:ims, :self, :users, :channels, :websocket_url) + TeamData = Struct.new(:id, :name, :domain, :self, :websocket_url) end end end diff --git a/lita-slack.gemspec b/lita-slack.gemspec index 59a929f..0c0aa55 100644 --- a/lita-slack.gemspec +++ b/lita-slack.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency "lita", ">= 4.7.1" spec.add_runtime_dependency "multi_json" - spec.add_development_dependency "bundler", "~> 1.3" + spec.add_development_dependency "bundler" spec.add_development_dependency "pry-byebug" spec.add_development_dependency "rack-test" spec.add_development_dependency "rake" diff --git a/spec/lita/adapters/slack/api_spec.rb b/spec/lita/adapters/slack/api_spec.rb index 771df2e..a012369 100644 --- a/spec/lita/adapters/slack/api_spec.rb +++ b/spec/lita/adapters/slack/api_spec.rb @@ -637,11 +637,11 @@ def stubs(postMessage_options = {}) end end - describe "#rtm_start" do + describe "#rtm_connect" do let(:http_status) { 200 } 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/rtm.connect', token: token) do [http_status, {}, http_response] end end @@ -652,34 +652,19 @@ 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' }], + team: { id: 'T0987654321' }, }) end it "has data on the bot user" do - response = subject.rtm_start + response = subject.rtm_connect expect(response.self.id).to eq('U12345678') end - it "has an array of IMs" do - response = subject.rtm_start - - expect(response.ims[0].id).to eq('D024BFF1M') - end - - it "has an array of users" do - response = subject.rtm_start - - expect(response.users[0].id).to eq('U023BECGF') - end - it "has a WebSocket URL" do - response = subject.rtm_start + response = subject.rtm_connect expect(response.websocket_url).to eq('wss://example.com/') end diff --git a/spec/lita/adapters/slack/message_handler_spec.rb b/spec/lita/adapters/slack/message_handler_spec.rb index c449b0c..59ddecf 100644 --- a/spec/lita/adapters/slack/message_handler_spec.rb +++ b/spec/lita/adapters/slack/message_handler_spec.rb @@ -1,10 +1,13 @@ require "spec_helper" +# require_relative '../../../../lib/lita/adapters/slack/slack_source' describe Lita::Adapters::Slack::MessageHandler, lita: true do subject { described_class.new(robot, robot_id, data) } before do allow(robot).to receive(:trigger) + allow(robot).to receive(:alias) + allow(robot).to receive(:receive) Lita::Adapters::Slack::RoomCreator.create_room(channel, robot) end @@ -35,16 +38,17 @@ } end let(:message) { instance_double('Lita::Message', command!: false, extensions: {}) } - let(:source) { instance_double('Lita::Source', private_message?: false) } + let(:source) { instance_double('Lita::Adapters::Slack::SlackSource', private_message?: false) } let(:user) { instance_double('Lita::User', id: 'U023BECGF') } let(:room) { instance_double('Lita::Room', id: "C2147483705", name: "general") } before do allow(Lita::User).to receive(:find_by_id).and_return(user) allow(Lita::Room).to receive(:find_by_id).and_return(room) - allow(Lita::Source).to receive(:new).with( + allow(Lita::Adapters::Slack::SlackSource).to receive(:new).with( user: user, - room: room + room: room, + thread: nil, ).and_return(source) allow(Lita::Message).to receive(:new).with(robot, "Hello", source).and_return(message) allow(robot).to receive(:receive).with(message) @@ -72,9 +76,10 @@ before do allow(Lita::Room).to receive(:find_by_id).and_return(nil) - allow(Lita::Source).to receive(:new).with( + allow(Lita::Adapters::Slack::SlackSource).to receive(:new).with( user: user, - room: "D2147483705" + room: "D2147483705", + thread: nil, ).and_return(source) allow(source).to receive(:private_message!).and_return(true) allow(source).to receive(:private_message?).and_return(true) @@ -156,6 +161,32 @@ end end + context "when the message is in a thread" do + let(:data) do + { + "type" => "message", + "channel" => "C2147483705", + "user" => "U023BECGF", + "text" => "Hello", + "ts" => "1234.5678", + "thread_ts" => "1234.5678", + } + end + let(:source) { instance_double('Lita::Adapters::Slack::SlackSource', private_message?: false, thread: "1234.5678") } + + before do + allow(Lita::Adapters::Slack::SlackSource).to receive(:new).with( + user: user, + room: room, + thread: "1234.5678", + ).and_return(source) + end + + it "sets the source thread option" do + subject.handle + end + end + describe "Removing message formatting" do let(:user) { instance_double('Lita::User', id: 'U123',name: 'name', mention_name: 'label') } diff --git a/spec/lita/adapters/slack/rtm_connection_spec.rb b/spec/lita/adapters/slack/rtm_connection_spec.rb index 32188f3..6219fd7 100644 --- a/spec/lita/adapters/slack/rtm_connection_spec.rb +++ b/spec/lita/adapters/slack/rtm_connection_spec.rb @@ -9,21 +9,18 @@ def with_websocket(subject, queue) thread.join end - subject { described_class.new(robot, config, rtm_start_response) } + subject { described_class.new(robot, config, rtm_connect_response) } let(:api) { instance_double("Lita::Adapters::Slack::API") } let(:registry) { Lita::Registry.new } let(:robot) { Lita::Robot.new(registry) } - let(:raw_user_data) { Hash.new } - let(:channel) { Lita::Adapters::Slack::SlackChannel.new('C2147483705', 'general', 1360782804, 'U023BECGF', metadata) } - let(:metadata) { Hash.new } - let(:rtm_start_response) do + let(:rtm_connect_response) do Lita::Adapters::Slack::TeamData.new( - [], - Lita::Adapters::Slack::SlackUser.new('U12345678', 'carl', nil, raw_user_data), - [Lita::Adapters::Slack::SlackUser.new('U12345678', 'carl', '', raw_user_data)], - [channel], + 'T2U81E2FP', + 'SlackDemo', + 'slackdemo', + Lita::Adapters::Slack::SlackUser.new('U12345678', 'carl', nil, {}), "wss://example.com/" ) end @@ -39,42 +36,12 @@ def with_websocket(subject, queue) describe ".build" do before do allow(Lita::Adapters::Slack::API).to receive(:new).with(config).and_return(api) - allow(api).to receive(:rtm_start).and_return(rtm_start_response) + allow(api).to receive(:rtm_connect).and_return(rtm_connect_response) end - it "constructs a new RTMConnection with the results of rtm.start data" do + it "constructs a new RTMConnection with the results of rtm.connect data" do expect(described_class.build(robot, config)).to be_an_instance_of(described_class) end - - it "creates users with the results of rtm.start data" do - expect(Lita::Adapters::Slack::UserCreator).to receive(:create_users) - - described_class.build(robot, config) - end - - it "creates rooms with the results of rtm.start data" do - expect(Lita::Adapters::Slack::RoomCreator).to receive(:create_rooms) - - described_class.build(robot, config) - end - end - - describe "#im_for" do - before do - allow(Lita::Adapters::Slack::API).to receive(:new).with(config).and_return(api) - allow( - Lita::Adapters::Slack::IMMapping - ).to receive(:new).with(api, []).and_return(im_mapping) - allow(im_mapping).to receive(:im_for).with('U12345678').and_return('D024BFF1M') - end - - let(:im_mapping) { instance_double('Lita::Adapters::Slack::IMMapping') } - - it "delegates to the IMMapping" do - with_websocket(subject, queue) do |websocket| - expect(subject.im_for('U12345678')).to eq('D024BFF1M') - end - end end describe "#run" do @@ -118,6 +85,7 @@ def with_websocket(subject, queue) context "when the WebSocket is closed from outside" do it "shuts down the reactor" do with_websocket(subject, queue) do |websocket| + sleep 0.1 # Since this code is run in a thread, we need to wait for it to finish websocket.close expect(EM.stopping?).to be_truthy end diff --git a/spec/lita/adapters/slack_spec.rb b/spec/lita/adapters/slack_spec.rb index fbbbd23..818758e 100644 --- a/spec/lita/adapters/slack_spec.rb +++ b/spec/lita/adapters/slack_spec.rb @@ -73,7 +73,7 @@ end describe "via the Web API, retrieving the roster for a group/mpim channel" do - let(:room_source) { Lita::Source.new(room: 'G024BE91L') } + let(:room_source) { Lita::Adapters::Slack::SlackSource.new(room: 'G024BE91L') } let(:response) do { ok: true, @@ -95,7 +95,7 @@ end describe "via the Web API, retrieving the roster for an im channel" do - let(:room_source) { Lita::Source.new(room: 'D024BFF1M') } + let(:room_source) { Lita::Adapters::Slack::SlackSource.new(room: 'D024BFF1M') } let(:response) do { ok: true, @@ -117,7 +117,7 @@ end describe "#send_messages" do - let(:room_source) { Lita::Source.new(room: 'C024BE91L') } + let(:room_source) { Lita::Adapters::Slack::SlackSource.new(room: 'C024BE91L') } let(:user) { Lita::User.new('U023BECGF') } let(:user_source) { Lita::Source.new(user: user) } let(:private_message_source) do @@ -133,10 +133,36 @@ 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']) + expect(api).to receive(:send_messages).with(room_source.room, ['foo'], { thread_ts: nil }) subject.send_messages(room_source, ['foo']) end + + context "when thread is set" do + let(:room_source) { Lita::Adapters::Slack::SlackSource.new(room: 'C024BE91L', thread: '12345.67890') } + + it "sends the message to the Web API with thread_ts" do + expect(api).to receive(:send_messages).with(room_source.room, ['foo'], { thread_ts: '12345.67890' }) + + subject.send_messages(room_source, ['foo']) + end + end + + context "with optional thread" do + it "sends the message to the Web API without thread_ts" do + expect(api).to receive(:send_messages).with(private_message_source.room, ['foo']) + + subject.send_messages(private_message_source, ['foo']) + end + end + + context "with user source" do + it "sends direct message to the Web API" do + expect(api).to receive(:send_messages).with(user_source.user.id, ['foo']) + + subject.send_messages(user_source, ['foo']) + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6632cfb..d52bf12 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,7 +6,7 @@ ] SimpleCov.start { add_filter "/spec/" } -require "lita-slack" +require_relative "../lib/lita-slack" require "lita/rspec" require "pry"