slacky 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ .bundle
2
+ .idea
3
+ .DS_Store
4
+ pkg
@@ -0,0 +1 @@
1
+ 1.9.3-p448
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
@@ -0,0 +1,70 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ slacky (0.1)
5
+ dotenv
6
+ em-cron
7
+ eventmachine
8
+ faye-websocket
9
+ pg
10
+ slack-ruby-client (>= 0.6)
11
+ tzinfo
12
+ tzinfo-data
13
+
14
+ GEM
15
+ remote: http://rubygems.org/
16
+ specs:
17
+ activesupport (4.2.6)
18
+ i18n (~> 0.7)
19
+ json (~> 1.7, >= 1.7.7)
20
+ minitest (~> 5.1)
21
+ thread_safe (~> 0.3, >= 0.3.4)
22
+ tzinfo (~> 1.1)
23
+ dotenv (2.1.1)
24
+ em-cron (0.2.0)
25
+ eventmachine
26
+ parse-cron (~> 0.1)
27
+ eventmachine (1.2.0.1)
28
+ factory_girl (4.7.0)
29
+ activesupport (>= 3.0.0)
30
+ faraday (0.9.2)
31
+ multipart-post (>= 1.2, < 3)
32
+ faraday_middleware (0.10.0)
33
+ faraday (>= 0.7.4, < 0.10)
34
+ faye-websocket (0.10.3)
35
+ eventmachine (>= 0.12.0)
36
+ websocket-driver (>= 0.5.1)
37
+ gli (2.13.4)
38
+ hashie (3.4.4)
39
+ i18n (0.7.0)
40
+ json (1.8.3)
41
+ minitest (5.8.4)
42
+ multipart-post (2.0.0)
43
+ parse-cron (0.1.4)
44
+ pg (0.18.4)
45
+ rake (11.1.2)
46
+ slack-ruby-client (0.7.2)
47
+ activesupport
48
+ faraday
49
+ faraday_middleware
50
+ gli
51
+ hashie
52
+ json
53
+ websocket-driver
54
+ thread_safe (0.3.5)
55
+ tzinfo (1.2.2)
56
+ thread_safe (~> 0.1)
57
+ tzinfo-data (1.2016.4)
58
+ tzinfo (>= 1.0.0)
59
+ websocket-driver (0.6.3)
60
+ websocket-extensions (>= 0.1.0)
61
+ websocket-extensions (0.1.2)
62
+
63
+ PLATFORMS
64
+ ruby
65
+
66
+ DEPENDENCIES
67
+ factory_girl
68
+ minitest
69
+ rake
70
+ slacky!
@@ -0,0 +1,15 @@
1
+ # Slacky
2
+
3
+ ### Carbon Five Slack bot starter kit
4
+
5
+ This gem is an out of the box Slack bot running in Ruby. It runs as a
6
+ daemon, has a CLI, and in the future will integrate with other Carbon
7
+ Five services (like Timesheet).
8
+
9
+ ## Installation
10
+
11
+ ...
12
+
13
+ ## Usage
14
+
15
+ ...
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.libs << 'lib' << 'test'
6
+ test.pattern = 'test/**/*_test.rb'
7
+ test.verbose = true
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,12 @@
1
+ module Slacky
2
+ require 'slacky/version'
3
+ require 'slacky/cli'
4
+ require 'slacky/config'
5
+ require 'slacky/daemon'
6
+ require 'slacky/service'
7
+ require 'slacky/bot'
8
+ require 'slacky/bookkeeper'
9
+ require 'slacky/user'
10
+ require 'slacky/channel'
11
+ require 'slacky/message'
12
+ end
@@ -0,0 +1,55 @@
1
+ module Slacky
2
+ class Bookkeeper
3
+ def initialize(client)
4
+ @client = client
5
+ end
6
+
7
+ def web_client
8
+ @client.web_client
9
+ end
10
+
11
+ def keep_the_books
12
+ @client.on :presence_change do |data|
13
+ next unless ( user = User.find data.user )
14
+ user.presence = data['presence']
15
+ user.save
16
+ end
17
+
18
+ @client.on :channel_created do |data|
19
+ web_client.channels_info(channel: data.channel.id).tap do |resp|
20
+ if resp.ok
21
+ channel = Channel.channel resp.channel
22
+ puts "Channel ##{channel.name}: channel_created"
23
+ end
24
+ end
25
+ end
26
+
27
+ handle_channel(:channel_deleted) { |c| c.delete }
28
+ handle_channel(:channel_archive) { |c| c.archive }
29
+ handle_channel(:channel_unarchive) { |c| c.unarchive }
30
+ handle_channel(:channel_rename) { |c, data| c.rename data.channel.name }
31
+
32
+ @client.on :group_joined do |data|
33
+ next unless data.channel.is_group
34
+ channel = Channel.group data.channel
35
+ puts "Channel ##{channel.name}: group_joined"
36
+ end
37
+
38
+ handle_channel(:group_left) { |g| g.delete }
39
+ handle_channel(:group_archive) { |g| g.archive }
40
+ handle_channel(:group_unarchive) { |g| g.unarchive }
41
+ handle_channel(:group_rename) { |g, data| g.rename data.channel.name }
42
+ end
43
+
44
+ def handle_channel(event)
45
+ @client.on event do |data|
46
+ channel_id = data.channel.is_a?(String) ? data.channel : data.channel.id
47
+ Channel.find(channel_id).tap do |channel|
48
+ yield channel, data
49
+ puts "Channel ##{channel.name}: #{event}"
50
+ end
51
+ end
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,229 @@
1
+ require 'slack-ruby-client'
2
+ require 'set'
3
+ require 'tzinfo'
4
+ require 'em/cron'
5
+
6
+ module Slacky
7
+ class Bot
8
+ attr_reader :client, :config, :slack_id
9
+
10
+ def initialize(config)
11
+ puts "#{config.name} is starting up..."
12
+
13
+ @config = config
14
+ @restarts = []
15
+ @command_handlers = []
16
+ @channel_handlers = []
17
+ @im_handlers = []
18
+ @raw_handlers = []
19
+ @cron_handlers = []
20
+
21
+ unless @config.slack_api_token
22
+ @config.log "No Slack API token found. Use environment variable SLACK_API_TOKEN."
23
+ return
24
+ end
25
+
26
+ Slack.configure do |slack_cfg|
27
+ slack_cfg.token = @config.slack_api_token
28
+ end
29
+
30
+ @client = Slack::RealTime::Client.new
31
+
32
+ auth = web_client.auth_test
33
+ if auth.ok
34
+ @slack_id = auth.user_id
35
+ @config.log "Slackbot is active!"
36
+ else
37
+ @config.log "Slackbot is doomed :-("
38
+ return
39
+ end
40
+
41
+ @bookkeeper = Bookkeeper.new @client
42
+
43
+ Channel.bot = self
44
+ Message.bot = self
45
+
46
+ populate_users
47
+ populate_channels
48
+ end
49
+
50
+ def web_client
51
+ @client.web_client
52
+ end
53
+
54
+ def name
55
+ @config.down_name
56
+ end
57
+
58
+ def known_commands
59
+ @command_handlers.map { |ch| ch[:command] }
60
+ end
61
+
62
+ def on(type, &block)
63
+ @raw_handlers << { type: type, handler: block }
64
+ end
65
+
66
+ def on_command(command, &block)
67
+ @command_handlers << { command: command.downcase, handler: block }
68
+ end
69
+
70
+ def on_message(attrs, &block)
71
+ attrs ||= {}
72
+ @channel_handlers << { match: attrs[:match], channels: attrs[:channels], handler: block }
73
+ end
74
+
75
+ def on_im(attrs, &block)
76
+ attrs ||= {}
77
+ @im_handlers << { match: attrs[:match], handler: block }
78
+ end
79
+
80
+ def at(cron, &block)
81
+ @cron_handlers << { cron: cron, handler: block }
82
+ end
83
+
84
+ def handle_channel(message)
85
+ handled = false
86
+
87
+ if message.command?
88
+ @command_handlers.each do |h|
89
+ command, handler = h.values_at :command, :handler
90
+ next unless command == message.command
91
+ @client.typing channel: message.channel.slack_id
92
+ handler.call message
93
+ handled = true
94
+ end
95
+ end
96
+
97
+ return if handled
98
+
99
+ @channel_handlers.each do |h|
100
+ match, channels, handler = h.values_at :match, :channels, :handler
101
+ accept = Channel.find channels
102
+ next if accept && accept.include?(message.channel)
103
+ next if match && ! match === message
104
+ @client.typing channel: message.channel.slack_id
105
+ handler.call message
106
+ end
107
+ end
108
+
109
+ def handle_im(message)
110
+ unless message.user.slack_im_id == message.channel.slack_id
111
+ message.user.slack_im_id = message.channel.slack_id
112
+ message.user.save
113
+ end
114
+
115
+ handled = false
116
+
117
+ @command_handlers.each do |h|
118
+ command, handler = h.values_at :command, :handler
119
+ next unless command == message.command
120
+ @client.typing channel: message.channel.slack_id
121
+ handler.call message
122
+ handled = true
123
+ end
124
+
125
+ return if handled
126
+
127
+ @im_handlers.each do |h|
128
+ match, handler = h.values_at :match, :handler
129
+ next if match && ! match === message
130
+ @client.typing channel: message.channel.slack_id
131
+ handler.call message
132
+ end
133
+ end
134
+
135
+ def run
136
+ @bookkeeper.keep_the_books
137
+
138
+ @client.on :message do |data|
139
+ next unless ( user = User.find data.user )
140
+
141
+ channel = Channel.find data.channel
142
+ channel = Channel.im data.channel, user if data.channel =~ /^D/ && ! channel
143
+ next unless channel
144
+
145
+ reject = Channel.find @config.slack_reject_channels
146
+ next if reject && reject.find { |c| c.slack_id == data.channel }
147
+
148
+ accept = Channel.find @config.slack_accept_channels
149
+ next if accept && ! accept.find { |c| c.slack_id == data.channel }
150
+
151
+ message = Message.new(user, channel, data)
152
+ handle_channel message if [ :group, :channel ].include? channel.type
153
+ handle_im message if [ :im ].include? channel.type
154
+ end
155
+
156
+ @raw_handlers.each do |h|
157
+ type, handler = h.values_at :type, :handler
158
+ @client.on type do |data|
159
+ handler.call data
160
+ end
161
+ end
162
+
163
+ @cron_thread ||= Thread.new do
164
+ EM.run do
165
+ @cron_handlers.each do |h|
166
+ cron, handler = h.values_at :cron, :handler
167
+ EM::Cron.schedule cron do |time|
168
+ begin
169
+ handler.call
170
+ rescue => e
171
+ @config.log "An error ocurred inside the Slackbot (in a scheduled block)", e
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ puts "#{@config.name} is listening to: #{@config.slack_accept_channels}"
179
+
180
+ @client.start!
181
+ rescue => e
182
+ @config.log "An error ocurred inside the Slackbot", e
183
+ @restarts << Time.new
184
+ @restarts.shift while (@restarts.length > 3)
185
+ if @restarts.length == 3 and ( Time.new - @restarts.first < 30 )
186
+ @config.log "Too many errors. Not restarting anymore."
187
+ else
188
+ run
189
+ end
190
+ end
191
+
192
+ def populate_users
193
+ print "Getting users from Slack..."
194
+ resp = web_client.users_list presence: 1
195
+ throw resp unless resp.ok
196
+ resp.members.map do |member|
197
+ next unless member.profile.email # no bots
198
+ next if member.deleted # no ghosts
199
+ next if member.is_ultra_restricted # no single channel guests
200
+ user = User.find(member.id) || User.new(slack_id: member.id)
201
+ user.populate(member).save
202
+ end
203
+ puts " done!"
204
+ end
205
+
206
+ def populate_channels
207
+ print "Getting channels from Slack..."
208
+ resp = web_client.channels_list
209
+ throw resp unless resp.ok
210
+ resp.channels.map do |channel|
211
+ Channel.channel channel
212
+ end
213
+
214
+ resp = web_client.groups_list
215
+ throw resp unless resp.ok
216
+ resp.groups.map do |group|
217
+ Channel.group group
218
+ end
219
+ puts " done!"
220
+ end
221
+
222
+ def blowup(user, data, args, &respond)
223
+ respond.call "Tick... tick... tick... BOOM! Goodbye."
224
+ EM.next_tick do
225
+ raise "kablammo!"
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,103 @@
1
+ module Slacky
2
+ class Channel
3
+ attr_reader :slack_id, :topic, :purpose, :members, :type
4
+ attr_accessor :name
5
+
6
+ @@channels = {}
7
+ @@bot = nil
8
+
9
+ def self.bot=(bot)
10
+ @@bot = bot
11
+ end
12
+
13
+ def self.find(channel)
14
+ return nil unless channel
15
+ return channel.map { |c| Channel.find c }.compact if channel.is_a? Array
16
+ return channel if channel.is_a? Channel
17
+ @@channels[channel]
18
+ end
19
+
20
+ def self.im(channel_id, user)
21
+ Channel.new.populate_im(channel_id, user).save
22
+ end
23
+
24
+ def self.channel(channel_data)
25
+ Channel.new.populate_channel(channel_data).save
26
+ end
27
+
28
+ def self.group(group_data)
29
+ Channel.new.populate_group(group_data).save
30
+ end
31
+
32
+ def archived?
33
+ @archived
34
+ end
35
+
36
+ def archive
37
+ @archived = true
38
+ end
39
+
40
+ def unarchive
41
+ @archived = false
42
+ end
43
+
44
+ def delete
45
+ @@channels.delete @slack_id
46
+ @@channels.delete "##{@name}"
47
+ @@channels.delete "@#{@user.username}" if @user
48
+ end
49
+
50
+ def rename(name)
51
+ @@channels.delete "##{@name}"
52
+ @name = name
53
+ @@channels["##{@name}"] = self
54
+ end
55
+
56
+ def member?
57
+ case @type
58
+ when :channel ; @member
59
+ when :group ; @members[@@bot.slack_id]
60
+ when :im ; true
61
+ else throw "Unknown channel type: #{@type}"
62
+ end
63
+ end
64
+
65
+ def save
66
+ @@channels[@slack_id] = self
67
+ @@channels["##{@name}"] = self if @name
68
+ @@channels["@#{@user.username}"] = self if @user
69
+ self
70
+ end
71
+
72
+ def populate_channel(channel)
73
+ populate channel
74
+ @type = :channel
75
+ @member = channel.is_member
76
+ self
77
+ end
78
+
79
+ def populate_group(group)
80
+ populate group
81
+ @type = :group
82
+ @members = User.find group.members
83
+ self
84
+ end
85
+
86
+ def populate_im(slack_id, user)
87
+ @type = :im
88
+ @slack_id = slack_id
89
+ @user = user
90
+ self
91
+ end
92
+
93
+ private
94
+
95
+ def populate(channel)
96
+ @slack_id = channel.id
97
+ @name = channel.name
98
+ @archived = channel.is_archived
99
+ @topic = channel.topic.value
100
+ @purpose = channel.purpose.value
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,35 @@
1
+ module Slacky
2
+ class CLI
3
+ attr_reader :bot
4
+
5
+ def initialize(name, opts)
6
+ throw "CLI must be passed a name" unless name
7
+ @options = { :verbose => false }.merge opts
8
+ config = Config.new name
9
+ @bot = Bot.new config
10
+ daemon = Daemon.new config, bot
11
+ @service = Service.new config, daemon
12
+ end
13
+
14
+ def run(params)
15
+ @service.run
16
+ end
17
+
18
+ def start(params)
19
+ @service.start
20
+ end
21
+
22
+ def stop(params)
23
+ @service.stop
24
+ end
25
+
26
+ def restart(params)
27
+ @service.restart
28
+ end
29
+
30
+ def status(params)
31
+ @service.status
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,66 @@
1
+ require 'yaml'
2
+ require 'time'
3
+ require 'pg'
4
+ require 'dotenv'
5
+
6
+ module Slacky
7
+ class Config
8
+ attr_reader :pid_file, :name, :db
9
+
10
+ def initialize(name, opts = {})
11
+ @name = name
12
+ Dotenv.load ".env", "#{config_dir}/.env"
13
+ FileUtils.mkdir config_dir unless File.directory? config_dir
14
+ @pid_file = "#{config_dir}/pid"
15
+ @db = PG.connect(db_connect_params)
16
+ User.db = @db
17
+ User.initialize_table
18
+
19
+ @timestamps = {}
20
+ end
21
+
22
+ def down_name
23
+ @name.downcase
24
+ end
25
+
26
+ def slack_api_token
27
+ ENV['SLACK_API_TOKEN']
28
+ end
29
+
30
+ def config_dir
31
+ ENV['CONFIG_DIR'] || "#{ENV['HOME']}/.#{down_name}"
32
+ end
33
+
34
+ def slack_reject_channels
35
+ return nil unless ENV['REJECT_CHANNELS']
36
+ ENV['REJECT_CHANNELS'].split(',').map {|c| c.strip}
37
+ end
38
+
39
+ def slack_accept_channels
40
+ return nil unless ENV['ACCEPT_CHANNELS']
41
+ ENV['ACCEPT_CHANNELS'].split(',').map {|c| c.strip}
42
+ end
43
+
44
+ def log(msg, ex = nil)
45
+ log = File.new(log_file, 'a')
46
+ timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
47
+ type = ex ? 'ERROR' : ' INFO'
48
+ log.puts "#{type} #{timestamp} #{msg}"
49
+ if ex
50
+ log.puts ex.message
51
+ log.puts("Stacktrace:\n" + ex.backtrace.join("\n"))
52
+ end
53
+ log.flush
54
+ end
55
+
56
+ private
57
+
58
+ def db_connect_params
59
+ ENV['DATABASE_URL'] || { dbname: "slacky_#{down_name}" }
60
+ end
61
+
62
+ def log_file
63
+ "#{config_dir}/#{down_name}.log"
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,67 @@
1
+ module Slacky
2
+ class Daemon
3
+
4
+ def initialize(config, bot)
5
+ @config = config
6
+ @bot = bot
7
+ @active = true
8
+ @running = false
9
+ end
10
+
11
+ def start(daemonize = true)
12
+ Process.daemon if daemonize
13
+ write_pid
14
+
15
+ [ 'HUP', 'INT', 'QUIT', 'TERM' ].each do |sig|
16
+ Signal.trap(sig) do
17
+ @config.log "Interrupted with signal: #{sig}"
18
+ kill
19
+ end
20
+ end
21
+
22
+ begin
23
+ @slackthread = Thread.new { @bot.run }
24
+ run @bot
25
+ rescue => e
26
+ @config.log "Unexpected error", e
27
+ ensure
28
+ cleanup
29
+ end
30
+ end
31
+
32
+ def cleanup
33
+ delete_pid
34
+ end
35
+
36
+ private
37
+
38
+ def run(slackbot)
39
+ @config.log "#{@config.name} is running."
40
+ while active? do
41
+ # TODO: handle timed tasks
42
+ #slackbot.ask_all if time.min % 10 == 0 # every 10 minutes
43
+ sleep 0.5
44
+ end
45
+ @config.log "#{@config.name} got killed"
46
+ @slackthread.kill
47
+ end
48
+
49
+ def active?
50
+ @active
51
+ end
52
+
53
+ def kill
54
+ @active = false
55
+ end
56
+
57
+ def write_pid
58
+ File.open @config.pid_file, 'w' do |f|
59
+ f.write Process.pid.to_s
60
+ end
61
+ end
62
+
63
+ def delete_pid
64
+ File.delete @config.pid_file if File.exists? @config.pid_file
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,61 @@
1
+ module Slacky
2
+ class Message
3
+ attr_reader :raw, :text, :user, :channel
4
+
5
+ @@decorator = @@bot = nil
6
+
7
+ def self.decorator=(decorator)
8
+ @@decorator = decorator
9
+ end
10
+
11
+ def self.bot=(bot)
12
+ @@bot = bot
13
+ end
14
+
15
+ def initialize(user, channel, raw)
16
+ @raw = raw
17
+ @user = user
18
+ @channel = channel
19
+ @text = raw.text.strip
20
+ @pieces = @text.split ' '
21
+ self.extend @@decorator if @@decorator
22
+ end
23
+
24
+ def reply(msg)
25
+ @@bot.client.message channel: @channel.slack_id, reply_to: @raw.id, text: msg
26
+ end
27
+
28
+ def command?
29
+ first = word 0
30
+ return false unless first
31
+ first == @@bot.name || first == "<@#{@@bot.slack_id}>"
32
+ end
33
+
34
+ def command
35
+ if @channel.type == :im
36
+ command? ? downword(1) : downword(0)
37
+ else
38
+ command? ? downword(1) : nil
39
+ end
40
+ end
41
+
42
+ def yes?
43
+ [ 'y', 'yes' ].include? @text
44
+ end
45
+
46
+ def no?
47
+ [ 'n', 'no' ].include? @text
48
+ end
49
+
50
+ private
51
+
52
+ def word(n)
53
+ return nil unless @pieces.length > n
54
+ @pieces[n]
55
+ end
56
+
57
+ def downword(n)
58
+ word(n) && word(n).downcase
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,73 @@
1
+ require 'fileutils'
2
+
3
+ class Slacky::Service
4
+ def initialize(config, daemon)
5
+ @config = config
6
+ @daemon = daemon
7
+ end
8
+
9
+ def run
10
+ @daemon.start false
11
+ end
12
+
13
+ def start(persist = false)
14
+ pid = get_pid
15
+ if pid
16
+ puts "#{@config.name} is already running with PID #{pid}"
17
+ return
18
+ end
19
+
20
+ print "Starting #{@config.name}... "
21
+ new_pid = Process.fork { @daemon.start }
22
+ Process.detach new_pid
23
+ puts "started"
24
+ end
25
+
26
+ def stop(persist = false)
27
+ pid = get_pid
28
+ unless pid
29
+ puts "#{@config.name} is not running"
30
+ return
31
+ end
32
+
33
+ print "Stopping #{@config.name}..."
34
+
35
+ begin
36
+ Process.kill 'HUP', pid
37
+ rescue
38
+ @daemon.cleanup
39
+ end
40
+
41
+ ticks = 0
42
+ while pid = get_pid && ticks < 40
43
+ sleep 0.5
44
+ ticks += 1
45
+ print '.' if ticks % 4 == 0
46
+ end
47
+ puts " #{pid.nil? ? 'stopped' : 'failed'}"
48
+ end
49
+
50
+ def restart
51
+ stop
52
+ start
53
+ end
54
+
55
+ def status
56
+ pid = get_pid
57
+ if pid
58
+ puts "#{@config.name} is running with PID #{pid}"
59
+ true
60
+ else
61
+ puts "#{@config.name} is not running"
62
+ false
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def get_pid
69
+ return nil unless File.exists? @config.pid_file
70
+ IO.read(@config.pid_file).to_i
71
+ end
72
+
73
+ end
@@ -0,0 +1,91 @@
1
+ require 'json'
2
+
3
+ module Slacky
4
+ class User
5
+ attr_accessor :username, :slack_id, :slack_im_id, :first_name, :last_name, :email, :timezone, :presence, :data
6
+ attr_reader :tz
7
+
8
+ @@decorator = @@db = nil
9
+
10
+ def self.decorator=(decorator)
11
+ @@decorator = decorator
12
+ end
13
+
14
+ def self.db=(db)
15
+ @@db = db
16
+ end
17
+
18
+ def self.initialize_table
19
+ @@db.exec <<-SQL
20
+ create table if not exists users (
21
+ username varchar(64) not null,
22
+ slack_id varchar(20) not null,
23
+ slack_im_id varchar(20),
24
+ first_name varchar(64),
25
+ last_name varchar(64),
26
+ email varchar(128) not null,
27
+ timezone varchar(256),
28
+ presence varchar(64),
29
+ data jsonb not null
30
+ );
31
+ SQL
32
+ end
33
+
34
+ def self.find(user)
35
+ return user.map { |u| User.find u }.compact if user.is_a? Array
36
+ result = @@db.exec_params "select * from users where slack_id = $1", [ user ]
37
+ if result.ntuples == 0
38
+ result = @@db.exec_params "select * from users where username = $1", [ user ]
39
+ end
40
+ return nil if result.ntuples == 0
41
+
42
+ row = result[0]
43
+ user = self.new username: row['username'],
44
+ slack_id: row['slack_id'],
45
+ slack_im_id: row['slack_im_id'],
46
+ first_name: row['first_name'],
47
+ last_name: row['last_name'],
48
+ email: row['email'],
49
+ timezone: row['timezone'],
50
+ presence: row['presence'],
51
+ data: JSON.parse(row['data'])
52
+ user.extend @@decorator if @@decorator
53
+ user
54
+ end
55
+
56
+ def initialize(attrs={})
57
+ @username = attrs[:username]
58
+ @slack_id = attrs[:slack_id]
59
+ @slack_im_id = attrs[:slack_im_id]
60
+ @first_name = attrs[:first_name]
61
+ @last_name = attrs[:last_name]
62
+ @email = attrs[:email]
63
+ @timezone = attrs[:timezone] || "America/Los_Angeles"
64
+ @presence = attrs[:presence]
65
+ @data = attrs[:data] || {}
66
+ end
67
+
68
+ def populate(member)
69
+ @username = member.name
70
+ @first_name = member.profile.first_name
71
+ @last_name = member.profile.last_name
72
+ @email = member.profile.email
73
+ @timezone = member.tz
74
+ @presence = member.presence
75
+ @data = {} unless @data
76
+ self
77
+ end
78
+
79
+ def save
80
+ @@db.exec_params "delete from users where slack_id = $1", [ @slack_id ]
81
+ @@db.exec_params "insert into users (username, slack_id, slack_im_id, first_name, last_name, email, timezone, presence, data)
82
+ values ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
83
+ [ @username, @slack_id, @slack_im_id, @first_name, @last_name, @email, @timezone, @presence, JSON.dump(@data) ]
84
+ self
85
+ end
86
+
87
+ def reset
88
+ @data = {}
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ module Slacky
2
+ VERSION = "0.1"
3
+ end
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH << File.dirname(__FILE__) + "/lib"
3
+ require 'slacky/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "slacky"
7
+ s.version = Slacky::VERSION
8
+ s.authors = ["Michael Wynholds"]
9
+ s.email = ["mike@carbonfive.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Carbon Five Slack bot gem}
12
+ s.description = %q{Carbon Five Slack bot gem}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_runtime_dependency 'slack-ruby-client', ">= 0.6"
20
+ s.add_runtime_dependency 'pg'
21
+ s.add_runtime_dependency 'eventmachine'
22
+ s.add_runtime_dependency 'faye-websocket'
23
+ s.add_runtime_dependency 'em-cron'
24
+ s.add_runtime_dependency 'tzinfo'
25
+ s.add_runtime_dependency 'tzinfo-data'
26
+ s.add_runtime_dependency 'dotenv'
27
+
28
+ s.add_development_dependency 'rake'
29
+ s.add_development_dependency 'minitest'
30
+ s.add_development_dependency 'factory_girl'
31
+ end
metadata ADDED
@@ -0,0 +1,245 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: slacky
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Michael Wynholds
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-05-24 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: slack-ruby-client
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0.6'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0.6'
30
+ - !ruby/object:Gem::Dependency
31
+ name: pg
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: eventmachine
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: faye-websocket
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: em-cron
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: tzinfo
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: tzinfo-data
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: dotenv
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :runtime
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ - !ruby/object:Gem::Dependency
143
+ name: rake
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ - !ruby/object:Gem::Dependency
159
+ name: minitest
160
+ requirement: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ! '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ - !ruby/object:Gem::Dependency
175
+ name: factory_girl
176
+ requirement: !ruby/object:Gem::Requirement
177
+ none: false
178
+ requirements:
179
+ - - ! '>='
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ type: :development
183
+ prerelease: false
184
+ version_requirements: !ruby/object:Gem::Requirement
185
+ none: false
186
+ requirements:
187
+ - - ! '>='
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ description: Carbon Five Slack bot gem
191
+ email:
192
+ - mike@carbonfive.com
193
+ executables: []
194
+ extensions: []
195
+ extra_rdoc_files: []
196
+ files:
197
+ - .gitignore
198
+ - .ruby-version
199
+ - Gemfile
200
+ - Gemfile.lock
201
+ - README.md
202
+ - Rakefile
203
+ - lib/slacky.rb
204
+ - lib/slacky/bookkeeper.rb
205
+ - lib/slacky/bot.rb
206
+ - lib/slacky/channel.rb
207
+ - lib/slacky/cli.rb
208
+ - lib/slacky/config.rb
209
+ - lib/slacky/daemon.rb
210
+ - lib/slacky/message.rb
211
+ - lib/slacky/service.rb
212
+ - lib/slacky/user.rb
213
+ - lib/slacky/version.rb
214
+ - slacky.gemspec
215
+ homepage: ''
216
+ licenses: []
217
+ post_install_message:
218
+ rdoc_options: []
219
+ require_paths:
220
+ - lib
221
+ required_ruby_version: !ruby/object:Gem::Requirement
222
+ none: false
223
+ requirements:
224
+ - - ! '>='
225
+ - !ruby/object:Gem::Version
226
+ version: '0'
227
+ segments:
228
+ - 0
229
+ hash: -3373873093616992315
230
+ required_rubygems_version: !ruby/object:Gem::Requirement
231
+ none: false
232
+ requirements:
233
+ - - ! '>='
234
+ - !ruby/object:Gem::Version
235
+ version: '0'
236
+ segments:
237
+ - 0
238
+ hash: -3373873093616992315
239
+ requirements: []
240
+ rubyforge_project:
241
+ rubygems_version: 1.8.23
242
+ signing_key:
243
+ specification_version: 3
244
+ summary: Carbon Five Slack bot gem
245
+ test_files: []