slacks 0.0.1

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.
@@ -0,0 +1,34 @@
1
+ require "slacks/conversation"
2
+
3
+ module Slacks
4
+ class Event
5
+ attr_reader :message, :channel, :sender
6
+
7
+ def initialize(session: nil, message: nil, channel: nil, sender: nil)
8
+ @session = session
9
+ @message = message
10
+ @channel = channel
11
+ @sender = sender
12
+ end
13
+
14
+ def user
15
+ return @user if defined?(@user)
16
+ @user = sender && ::User.find_by_slack_username(sender.username)
17
+ end
18
+
19
+ def reply(*args)
20
+ channel.reply(*args)
21
+ end
22
+
23
+ def random_reply(*args)
24
+ channel.random_reply(*args)
25
+ end
26
+
27
+ def start_conversation!
28
+ Conversation.new(session, channel, sender)
29
+ end
30
+
31
+ private
32
+ attr_reader :session
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ # This is a channel that the bot is aware of
2
+ # but is not a member of — and cannot reply to.
3
+ #
4
+ # It should expose the same API as channel,
5
+ # but you will not be able to reply on this
6
+ # channel or start a conversation on it.
7
+
8
+ module Slacks
9
+ class GuestChannel < Channel
10
+
11
+ def initialize(attributes)
12
+ @id = attributes["channel_id"]
13
+ @name = attributes["channel_name"]
14
+
15
+ @type = :channel
16
+ @type = :group if id.start_with?("G")
17
+ @type = :direct_message if id.start_with?("D")
18
+ end
19
+
20
+ def reply(*args)
21
+ raise NotInChannelError, self
22
+ end
23
+ alias :say :reply
24
+
25
+ def random_reply(*args)
26
+ raise NotInChannelError, self
27
+ end
28
+
29
+ def guest?
30
+ true
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,44 @@
1
+ module Slacks
2
+ class Listener
3
+ attr_reader :matcher, :flags
4
+ attr_accessor :conversation
5
+
6
+ def initialize(listeners, matcher, direct, flags, callback)
7
+ # flags.each do |flag|
8
+ # unless Slacks::Message.can_apply?(flag)
9
+ # raise ArgumentError, "#{flag.inspect} is not a recognized flag"
10
+ # end
11
+ # end
12
+
13
+ @listeners = listeners
14
+ @matcher = matcher.freeze
15
+ @flags = flags.sort.freeze
16
+ @direct = direct
17
+ @callback = callback
18
+ end
19
+
20
+ def match(message)
21
+ matcher.match message.to_s(flags)
22
+ end
23
+
24
+ def direct?
25
+ @direct
26
+ end
27
+
28
+ def indirect?
29
+ !direct?
30
+ end
31
+
32
+ def stop_listening!
33
+ listeners.delete self
34
+ self
35
+ end
36
+
37
+ def call(e)
38
+ @callback.call(e)
39
+ end
40
+
41
+ private
42
+ attr_reader :listeners
43
+ end
44
+ end
@@ -0,0 +1,31 @@
1
+ require "thread_safe"
2
+
3
+ module Slacks
4
+ class ListenerCollection
5
+
6
+ def initialize
7
+ @listeners = ThreadSafe::Array.new
8
+ end
9
+
10
+ def listen_for(matcher, flags=[], &block)
11
+ Listener.new(self, matcher, true, flags, block).tap do |listener|
12
+ @listeners.push listener
13
+ end
14
+ end
15
+
16
+ def overhear(matcher, flags=[], &block)
17
+ Listener.new(self, matcher, false, flags, block).tap do |listener|
18
+ @listeners.push listener
19
+ end
20
+ end
21
+
22
+ def each(&block)
23
+ @listeners.each(&block)
24
+ end
25
+
26
+ def delete(listener)
27
+ @listeners.delete listener
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,71 @@
1
+ module Slacks
2
+ class Message
3
+
4
+ def initialize(session, data)
5
+ @session = session
6
+ @data = data
7
+ @processed_text = Hash.new do |hash, flags|
8
+ hash[flags] = flags.inject(text) { |text, flag| session.apply(flag, text) }.strip
9
+ end
10
+ end
11
+
12
+
13
+ def channel
14
+ return @channel if defined?(@channel)
15
+ @channel = session.slack.find_channel data["channel"]
16
+ end
17
+
18
+ def sender
19
+ return @sender if defined?(@sender)
20
+ @sender = session.slack.find_user data["user"]
21
+ end
22
+
23
+ def timestamp
24
+ data["ts"]
25
+ end
26
+
27
+ def type
28
+ data.fetch("subtype", "message")
29
+ end
30
+
31
+ def text
32
+ return @text if defined?(@text)
33
+ @text = self.class.normalize(data["text"])
34
+ end
35
+ alias :to_str :text
36
+
37
+ def to_s(flags=[])
38
+ processed_text[flags]
39
+ end
40
+
41
+ def inspect
42
+ "#{text.inspect} (from: #{sender}, channel: #{channel})"
43
+ end
44
+
45
+ def add_reaction(emoji)
46
+ session.slack.add_reaction(emoji, self)
47
+ end
48
+
49
+
50
+ def respond_to_missing?(method, include_all)
51
+ return true if text.respond_to?(method)
52
+ super
53
+ end
54
+
55
+ def method_missing(method, *args, &block)
56
+ return text.public_send(method, *args, &block) if text.respond_to?(method)
57
+ super
58
+ end
59
+
60
+
61
+ def self.normalize(text)
62
+ text
63
+ .gsub(/[“”]/, "\"")
64
+ .gsub(/[‘’]/, "'")
65
+ .strip
66
+ end
67
+
68
+ private
69
+ attr_reader :session, :data, :processed_text
70
+ end
71
+ end
@@ -0,0 +1,30 @@
1
+ require "slacks/event"
2
+
3
+ module Slacks
4
+ class RtmEvent < Event
5
+ attr_reader :match, :message_object
6
+
7
+ def initialize(session: nil, message: nil, match_data: nil, listener: nil)
8
+ super(session: session, message: message.text, channel: message.channel, sender: message.sender)
9
+ @message_object = message
10
+ @match = match_data
11
+ @listener = listener
12
+ end
13
+
14
+ def matched?(key)
15
+ match[key].present?
16
+ end
17
+
18
+ def stop_listening!
19
+ listener.stop_listening!
20
+ end
21
+
22
+ def react(emoji)
23
+ message_object.add_reaction(emoji)
24
+ end
25
+
26
+ private
27
+ attr_reader :listener
28
+
29
+ end
30
+ end
@@ -0,0 +1,147 @@
1
+ require "slacks/connection"
2
+ require "slacks/listener_collection"
3
+
4
+ module Slacks
5
+ class Session
6
+ attr_reader :listeners, :typing_speed, :token, :slack
7
+
8
+ def initialize(token, &block)
9
+ @typing_speed = 100.0
10
+ @token = token
11
+ @listeners = Slacks::ListenerCollection.new
12
+
13
+ if block_given?
14
+ listeners.instance_eval(&block)
15
+ start!
16
+ end
17
+ end
18
+
19
+ def listen_for(matcher, flags=[], &block)
20
+ listeners.listen_for(matcher, flags, &block)
21
+ end
22
+
23
+ def overhear(matcher, flags=[], &block)
24
+ listeners.overhear(matcher, flags, &block)
25
+ end
26
+
27
+ def start!
28
+ @slack = Slacks::Connection.new(self, token)
29
+
30
+ slack.listen! do |message|
31
+ process_message(message)
32
+ end
33
+ end
34
+
35
+ def error(message)
36
+ # TODO
37
+ puts "\e[33m[slack:error] #{message}\e[0m"
38
+ end
39
+
40
+ def apply(flag, text)
41
+ send :"_apply_#{flag}", text
42
+ end
43
+
44
+ def can_apply?(flag)
45
+ respond_to? :"_apply_#{flag}", true
46
+ end
47
+
48
+ protected
49
+
50
+ def process_message(data)
51
+
52
+ # Don't respond to things that another bot said
53
+ return if data.fetch("subtype", "message") == "bot_message"
54
+
55
+ # Normalize mentions of users
56
+ data["text"].gsub!(/<@U[^|]+\|([^>]*)>/, "@\\1")
57
+
58
+ # Normalize mentions of channels
59
+ data["text"].gsub!(/<[@#]?([UC][^>]+)>/) do |match|
60
+ begin
61
+ slack.find_channel($1)
62
+ rescue ArgumentError
63
+ match
64
+ end
65
+ end
66
+
67
+ message = Slacks::Message.new(self, data)
68
+
69
+ # Is someone talking directly to the bot?
70
+ direct_mention = message.channel.direct_message? || message[slack.bot.name]
71
+
72
+ listeners.each do |listener|
73
+ # Listeners come in two flavors: direct and indirect
74
+ #
75
+ # To trigger a direct listener, the but must be directly
76
+ # spoken to: as when the bot is mentioned or it is in
77
+ # a conversation with someone.
78
+ #
79
+ # An indirect listener is triggered in any context
80
+ # when it matches.
81
+ #
82
+ # We can ignore any listener that definitely doesn't
83
+ # meet these criteria.
84
+ next unless listener.indirect? or direct_mention or listener.conversation
85
+
86
+ message = Slacks::Message.new(self, data)
87
+
88
+ # Does the message match one of our listeners?
89
+ match_data = listener.match message
90
+ next unless match_data
91
+
92
+ # TODO: Want event.message to be the processed text
93
+ event = Slacks::RtmEvent.new(
94
+ session: self,
95
+ message: message,
96
+ match_data: match_data,
97
+ listener: listener)
98
+
99
+ # Skip listeners if they are not part of this conversation
100
+ next unless listener.indirect? or direct_mention or listener.conversation.includes?(event)
101
+
102
+ invoke! listener, event
103
+ end
104
+ # rescue Exception
105
+ # # TODO
106
+ # # Houston.report_exception $!
107
+ # puts "\e[31m[slack:exception] (#{$!.class}) #{$!.message}\n #{$!.backtrace.join("\n ")}\e[0m"
108
+ end
109
+
110
+ def invoke!(listener, event)
111
+ # # TODO
112
+ # puts "\e[35m[slack:hear:#{event.message_object.type}] #{event.message_object.inspect}\e[0m"
113
+
114
+ listener.call(event)
115
+ # Thread.new do
116
+ # begin
117
+ # @callback.call(e)
118
+ # rescue Exception # rescues StandardError by default; but we want to rescue and report all errors
119
+ # # TODO
120
+ # # Houston.report_exception $!, parameters: {channel: e.channel, message: e.message, sender: e.sender}
121
+ # puts "\e[31m[slack:exception] (#{$!.class}) #{$!.message}\n #{$!.backtrace.join("\n ")}\e[0m"
122
+ # e.reply "An error occurred when I was trying to answer you"
123
+ # ensure
124
+ # ActiveRecord::Base.clear_active_connections! if defined?(ActiveRecord)
125
+ # end
126
+ # end
127
+ end
128
+
129
+ def _apply_downcase(text)
130
+ text.downcase
131
+ end
132
+
133
+ def _apply_no_punctuation(text)
134
+ # Need to leave @ and # in @mentions and #channels
135
+ text.gsub(/[^\w\s@#]/, "")
136
+ end
137
+
138
+ def _apply_no_mentions(text)
139
+ text.gsub(/(?:^|\W+)#{slack.bot}\b/, "")
140
+ end
141
+
142
+ def _apply_no_emoji(text)
143
+ text.gsub(/(?::[^:]+:)/, "")
144
+ end
145
+
146
+ end
147
+ end
@@ -0,0 +1,12 @@
1
+ module Slacks
2
+ class Team
3
+ attr_reader :id, :name, :domain
4
+
5
+ def initialize(data)
6
+ @id = data.fetch("id")
7
+ @name = data.fetch("name")
8
+ @domain = data.fetch("domain")
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ module Slacks
2
+ class User
3
+ attr_reader :id, :username, :email, :first_name, :last_name
4
+
5
+ def initialize(session, attributes={})
6
+ @session = session
7
+
8
+ profile = attributes["profile"]
9
+ @id = attributes["id"]
10
+ @username = attributes["name"]
11
+ @email = profile["email"]
12
+ @first_name = profile["first_name"]
13
+ @last_name = profile["last_name"]
14
+ end
15
+
16
+ def name
17
+ "#{first_name} #{last_name}"
18
+ end
19
+
20
+ def inspect
21
+ "<Slacks::User id=\"#{id}\" name=\"#{name}\">"
22
+ end
23
+
24
+ def to_s
25
+ "@#{username}"
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module Slacks
2
+ VERSION = "0.0.1"
3
+ end
data/lib/slacks.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "slacks/version"
2
+ require "slacks/core_ext/exception"
3
+ require "slacks/session"
data/slacks.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "slacks/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "slacks"
8
+ spec.version = Slacks::VERSION
9
+ spec.authors = ["Bob Lail"]
10
+ spec.email = ["bob.lailfamily@gmail.com"]
11
+
12
+ spec.summary = %q{A library for communicating via Slack}
13
+ spec.homepage = "https://github.com/houston/slacks"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ # https://blog.jcoglan.com/2013/05/06/websocket-driver-an-io-agnostic-websocket-module-or-why-most-protocol-libraries-arent/
22
+ spec.add_dependency "websocket-driver"
23
+ spec.add_dependency "multi_json"
24
+ spec.add_dependency "faraday"
25
+ spec.add_dependency "faraday-raise-errors"
26
+ spec.add_dependency "thread_safe"
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.10"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "minitest", "~> 5.0"
31
+ spec.add_development_dependency "pry"
32
+ end
metadata ADDED
@@ -0,0 +1,197 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: slacks
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Bob Lail
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-04-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: websocket-driver
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: multi_json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: faraday-raise-errors
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: thread_safe
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.10'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.10'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '10.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '10.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: minitest
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '5.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '5.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description:
140
+ email:
141
+ - bob.lailfamily@gmail.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - ".gitignore"
147
+ - ".travis.yml"
148
+ - Gemfile
149
+ - LICENSE.txt
150
+ - README.md
151
+ - Rakefile
152
+ - bin/console
153
+ - bin/setup
154
+ - lib/slacks.rb
155
+ - lib/slacks/bot_user.rb
156
+ - lib/slacks/channel.rb
157
+ - lib/slacks/connection.rb
158
+ - lib/slacks/conversation.rb
159
+ - lib/slacks/core_ext/exception.rb
160
+ - lib/slacks/driver.rb
161
+ - lib/slacks/errors.rb
162
+ - lib/slacks/event.rb
163
+ - lib/slacks/guest_channel.rb
164
+ - lib/slacks/listener.rb
165
+ - lib/slacks/listener_collection.rb
166
+ - lib/slacks/message.rb
167
+ - lib/slacks/rtm_event.rb
168
+ - lib/slacks/session.rb
169
+ - lib/slacks/team.rb
170
+ - lib/slacks/user.rb
171
+ - lib/slacks/version.rb
172
+ - slacks.gemspec
173
+ homepage: https://github.com/houston/slacks
174
+ licenses:
175
+ - MIT
176
+ metadata: {}
177
+ post_install_message:
178
+ rdoc_options: []
179
+ require_paths:
180
+ - lib
181
+ required_ruby_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
186
+ required_rubygems_version: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: '0'
191
+ requirements: []
192
+ rubyforge_project:
193
+ rubygems_version: 2.2.2
194
+ signing_key:
195
+ specification_version: 4
196
+ summary: A library for communicating via Slack
197
+ test_files: []