qbot 0.1.3 → 0.1.10

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
- SHA1:
3
- metadata.gz: 96175e44e35b00efdc98a08f7dc482c75358a2fa
4
- data.tar.gz: f56a2d9a7e9d7634d8c8f1dcbe8a443cad455b05
2
+ SHA256:
3
+ metadata.gz: 4c0dd3c3d9a34d8eae7415eb10ddefa307ebca4bd200321e18fee452de9de157
4
+ data.tar.gz: 3a4f94dda22cd3aa76a83efd0c6c664ab365ed18946bec7b3e3bd7862728a421
5
5
  SHA512:
6
- metadata.gz: b03726403afaad4534f48644c27184b7a7df7fa562bbcdae9bd87a67004b816594f5c15d6726e204438793bff9cf0d3b6a27dfa715465168a4194a6bab6f6600
7
- data.tar.gz: 0a33dbf259131511157139fefbe1faf4d790a40fc9962a77f8f4ddd962ae5b0a71de90f97748d29cc31219c2168c6f8c7b4d9d77e3b725c72d008f4070c668c6
6
+ metadata.gz: cf435067c01baa076842a9ce672291897668eeef96b4e33fb216c7149feb6481139b79944725ba0c4816223925648521c7ebedd64ad909601e9b3a89e2d8c502
7
+ data.tar.gz: b61c2f73b417f4d6eb08dbef13386385d7a221847ef9a7c52607f1f29fb519d11eb7e227fe91b0beb35d3055de81c0082664eee0a42080b3fcce7308c6e7892a
@@ -1,4 +1,5 @@
1
1
  require 'qbot/base'
2
+ require 'qbot/embed/help'
2
3
  require 'qbot/autorun'
3
4
 
4
5
  module Qbot
@@ -1,6 +1,6 @@
1
- module Qbot
1
+ require 'qbot/message'
2
2
 
3
- Message = Struct.new(:text, :data, :matched)
3
+ module Qbot
4
4
 
5
5
  module Adapter
6
6
 
@@ -20,7 +20,7 @@ module Qbot
20
20
 
21
21
  def run(bots)
22
22
  listen do |message|
23
- bots.each { |bot| bot.call(message.dup) }
23
+ bots.each { |bot| bot.listen(message.dup) }
24
24
  end
25
25
  end
26
26
 
@@ -32,10 +32,6 @@ module Qbot
32
32
  raise 'Not implemented'
33
33
  end
34
34
 
35
- def reply_to(message, text, **options)
36
- raise 'Not implemented'
37
- end
38
-
39
35
  end
40
36
 
41
37
  end
@@ -20,6 +20,8 @@ module Qbot
20
20
 
21
21
  access_token(resp.headers['token'])
22
22
  raise 'Login failed' unless @token
23
+
24
+ @bot_id = me['username']
23
25
  end
24
26
 
25
27
  def access_token(token)
@@ -35,22 +37,19 @@ module Qbot
35
37
  end
36
38
 
37
39
  def post(text, **options)
38
- resp = api_call(:post, "/posts", body: options.merge(message: text))
39
- Qbot.app.logger.info("#{self.class} - Post message: #{resp.status} - '#{text}'")
40
- end
41
-
42
- def reply_to(message, text, **options)
43
40
  if options[:channel_id]
44
- channel_id = options[:channel_id]
41
+ # do nothing
45
42
  elsif options[:channel_name]
46
- channel = channel(options[:channel_name])
47
- channel_id = channel['id'] if channel
43
+ channel = channel(options.delete(:channel_name))
44
+ options[:channel_id] = channel['id'] if channel
45
+ elsif options[:reply_to]
46
+ message = options.delete(:reply_to)
47
+ options[:channel_id] = message.data['channel_id']
48
48
  end
49
49
 
50
- channel_id ||= message.data['channel_id'] if message
51
- return unless channel_id
52
-
53
- post(text, **options.merge(channel_id: channel_id))
50
+ return unless options[:channel_id]
51
+ Qbot.app.logger.info("#{self.class} - Post message: '#{text}'")
52
+ api_call(:post, "/posts", body: options.merge(message: text))
54
53
  end
55
54
 
56
55
  private
@@ -59,10 +58,9 @@ module Qbot
59
58
  end
60
59
 
61
60
  def start_connection(&block)
62
- running = true
63
61
  ws_url = URI.join(@server.gsub(/^http(s?):/, 'ws\1:'), endpoint('/websocket')).to_s
64
62
 
65
- ws = Faye::WebSocket::Client.new(ws_url)
63
+ ws = Faye::WebSocket::Client.new(ws_url, nil, {ping: 60})
66
64
  ws.send({seq: 1, action: 'authentication_challenge', data: {token: @token}}.to_json)
67
65
 
68
66
  ws.on :message do |e|
@@ -76,12 +74,12 @@ module Qbot
76
74
 
77
75
  ws.on :close do |e|
78
76
  Qbot.app.logger.info("#{self.class} - Websocket connection closed: #{e.code} #{e.reason}")
79
- if running then start_connection(&block) else Qbot.app.stop end
77
+ sleep 30
78
+ start_connection(&block)
80
79
  end
81
80
 
82
81
  ws.on :error do |e|
83
82
  Qbot.app.logger.error("#{self.class} - Websocket encountered error: #{e.message}")
84
- running = false
85
83
  end
86
84
  end
87
85
 
@@ -91,16 +89,16 @@ module Qbot
91
89
 
92
90
  case type
93
91
  when 'posted'
94
- post = JSON.parse(data['data']['post'])
92
+ data = JSON.parse(data['data']['post'])
95
93
 
96
- message = Qbot::Message.new
97
- message.data = post
98
- message.text = post['message']
94
+ message = Qbot::Message.new(data)
95
+ message.text = data['message']
96
+ message.mention(/^\s*@#{@bot_id}\b/)
99
97
 
100
98
  callback.call(message)
101
99
  end
102
100
  rescue => err
103
- Qbot.app.logger.error("#{self.class} - ERROR! #{err}")
101
+ Qbot.app.logger.error("#{self.class} - ERROR: #{err}")
104
102
  end
105
103
 
106
104
  def api_call(method, path, **options, &block)
@@ -18,9 +18,9 @@ module Qbot
18
18
  def receive_line(text)
19
19
  exit 0 if text.strip == 'exit'
20
20
 
21
- message = Qbot::Message.new
22
- message.data = text
21
+ message = Qbot::Message.new(text)
23
22
  message.text = text
23
+ message.mention(/^\s*bot\b/)
24
24
 
25
25
  @callback.call(message)
26
26
  $stdout.print ">> "
@@ -39,10 +39,6 @@ module Qbot
39
39
  $stdout.puts text
40
40
  end
41
41
 
42
- def reply_to(message, text, **options)
43
- post(text, **options)
44
- end
45
-
46
42
  end
47
43
 
48
44
  end
@@ -12,9 +12,9 @@ module Qbot
12
12
  SLACK_API_URL = 'https://slack.com/api'
13
13
 
14
14
  def initialize(api_token: nil)
15
- @server = URI.join(SLACK_API_URL, '/').to_s
16
-
17
15
  access_token(api_token || ENV['QBOT_SLACK_API_TOKEN'])
16
+ @server = URI.join(SLACK_API_URL, '/').to_s
17
+ @bot_id = me['user_id']
18
18
  end
19
19
 
20
20
  def access_token(token)
@@ -26,26 +26,23 @@ module Qbot
26
26
  end
27
27
 
28
28
  def close
29
- EM.stop
29
+ EM.stop_event_loop
30
30
  end
31
31
 
32
32
  def post(text, **options)
33
- resp = api_call(:post, "/chat.postMessage", options.merge(text: text))
34
- Qbot.app.logger.info("#{self.class} - Post message: #{resp.status} - '#{text}'")
35
- end
36
-
37
- def reply_to(message, text, **options)
38
33
  if options[:channel_id]
39
- channel_id = options[:channel_id]
34
+ options[:channel] = options.delete(:channel_id)
40
35
  elsif options[:channel_name]
41
- channel = channel(options[:channel_name])
42
- channel_id = channel['id'] if channel
36
+ channel = channel(options.delete(:channel_name))
37
+ options[:channel] = channel['id'] if channel
38
+ elsif options[:reply_to]
39
+ message = options.delete(:reply_to)
40
+ options[:channel] = message.data['channel']
43
41
  end
44
42
 
45
- channel_id ||= message.data['channel'] if message
46
- return unless channel_id
47
-
48
- post(text, **options.merge(channel: channel_id))
43
+ return unless options[:channel]
44
+ Qbot.app.logger.info("#{self.class} - Post message: '#{text}'")
45
+ api_call(:post, "/chat.postMessage", options.merge(text: text))
49
46
  end
50
47
 
51
48
  private
@@ -57,7 +54,6 @@ module Qbot
57
54
  resp = api_call(:get, '/rtm.start')
58
55
  data = JSON.parse(resp.body)
59
56
 
60
- running = true
61
57
  ws_url = data['url']
62
58
  ws = Faye::WebSocket::Client.new(ws_url, nil, {ping: 60})
63
59
 
@@ -72,26 +68,26 @@ module Qbot
72
68
 
73
69
  ws.on :close do |e|
74
70
  Qbot.app.logger.info("#{self.class} - Websocket connection closed: #{e.code} #{e.reason}")
75
- if running then start_connection(&block) else Qbot.app.stop end
71
+ sleep 30
72
+ start_connection(&block)
76
73
  end
77
74
 
78
75
  ws.on :error do |e|
79
76
  Qbot.app.logger.error("#{self.class} - Websocket encountered error: #{e.message}")
80
- running = false
81
77
  end
82
78
  end
83
79
 
84
80
  def emit_event(data, callback)
85
- return unless type = data['event']
81
+ return unless type = data['type']
86
82
  Qbot.app.logger.debug("#{self.class} - Event '#{type}' recieved")
87
83
 
88
84
  case type
89
85
  when 'message'
90
86
  return if data['subtype']
91
87
 
92
- message = Qbot::Message.new
93
- message.data = data
88
+ message = Qbot::Message.new(data)
94
89
  message.text = data['text']
90
+ message.mention(/^\s*<@#{@bot_id}>/)
95
91
 
96
92
  callback.call(message)
97
93
  end
@@ -26,24 +26,25 @@ module Qbot
26
26
 
27
27
  def initialize
28
28
  @bots = []
29
+ @help = {}
29
30
  @threads = []
30
31
  @timers = Timers::Group.new
31
32
  @logger = Logger.new(STDOUT)
32
33
  end
33
34
 
34
- def add(bot)
35
+ def add_bot(bot)
35
36
  @bots << bot
36
37
  end
37
38
 
38
- def bots
39
- @bots.map { |bot| bot.class.name }.uniq
39
+ def help_text(usages = {})
40
+ @help.update(usages)
40
41
  end
41
42
 
42
43
  def start
43
44
  @logger.info("Booting #{self.class}.")
44
45
  @logger.info("#{storage.class} - Storage driver loaded.")
45
46
  @logger.info("#{adapter.class} - Adapter driver loaded.")
46
- bots.each { |bot| @logger.info("Loading #{bot}.") }
47
+ @bots.map { |bot| bot.class.name }.uniq.each { |bot| @logger.info("#{bot} loaded.") }
47
48
 
48
49
  Thread.abort_on_exception = true
49
50
  @threads << Thread.start { loop { @timers.wait } }
@@ -5,7 +5,10 @@ module Qbot
5
5
  exit if $!
6
6
 
7
7
  %i{INT TERM}.each do |signal|
8
- Signal.trap(signal) { exit }
8
+ Signal.trap(signal) do
9
+ Qbot.app.stop
10
+ exit
11
+ end
9
12
  end
10
13
 
11
14
  Qbot.run!(ARGV)
@@ -13,15 +13,20 @@ module Qbot
13
13
 
14
14
  class << self
15
15
 
16
- def on(pattern, &block)
17
- pattern = Regexp.new(pattern.to_s) unless Regexp === pattern
18
- Qbot.app.add(new(pattern, &block))
16
+ def on(pattern, **options, &block)
17
+ pattern = Regexp.new("\b#{pattern}\b") unless Regexp === pattern
18
+ instance = new(pattern, **options, &block)
19
+ Qbot.app.add_bot(instance)
19
20
  end
20
21
 
21
22
  def cron(pattern, &block)
22
23
  schedule(pattern, &block)
23
24
  end
24
25
 
26
+ def help(usages)
27
+ Qbot.app.help_text(usages)
28
+ end
29
+
25
30
  private
26
31
  def schedule(pattern, &block)
27
32
  parser = CronParser.new(pattern)
@@ -29,32 +34,43 @@ module Qbot
29
34
  delay = parser.next(current) - current
30
35
 
31
36
  Qbot.app.timers.after(delay) do
32
- new(pattern).instance_eval(&block)
37
+ begin
38
+ instance = new(pattern)
39
+ instance.instance_eval(&block)
40
+ rescue => e
41
+ Qbot.app.logger.error("#{instance.class} - Error: #{e}")
42
+ end
43
+
33
44
  schedule(pattern, &block)
34
45
  end
35
46
  end
36
47
 
37
48
  end
38
49
 
39
- def initialize(pattern, &block)
50
+ def initialize(pattern, **options, &block)
40
51
  @pattern = pattern
52
+ @options = options
41
53
  @callback = block
42
54
  end
43
55
 
44
- def call(message)
45
- @message = message
46
- return unless @pattern =~ @message.text.to_s.strip
47
-
48
- begin
49
- Qbot.app.logger.debug("#{self.class} - Recieve message: '#{message.text}'")
50
- instance_exec($~, &@callback)
51
- rescue => e
52
- Qbot.app.logger.error("#{self.class} - Error: #{e}")
56
+ def listen(message)
57
+ if @options[:global]
58
+ return unless message.match(@pattern)
59
+ else
60
+ return unless message.mentioned?
61
+ return unless message.match(@pattern, prefix: message.mention)
53
62
  end
63
+
64
+ Qbot.app.logger.debug("#{self.class} - Recieve message: '#{message.text}'")
65
+
66
+ @message = message
67
+ instance_exec(@message, &@callback)
68
+ rescue => e
69
+ Qbot.app.logger.error("#{self.class} - Error: #{e}")
54
70
  end
55
71
 
56
72
  def post(text, **options)
57
- Qbot.app.adapter.reply_to(@message, text, **options)
73
+ Qbot.app.adapter.post(text, reply_to: @message, **options)
58
74
  end
59
75
 
60
76
  def cache
@@ -2,25 +2,22 @@ module Qbot
2
2
 
3
3
  class Cron < Qbot::Base
4
4
 
5
+ help "cron add <pattern> <message>" => "Add a cron task."
5
6
  on /^cron add (\S+ \S+ \S+ \S+ \S+) (.+)$/ do |msg|
6
- post start(unique_id, msg[1], msg[2])
7
+ post start(unique_id, msg.captures[1], msg.captures[2])
7
8
  end
8
9
 
10
+ help "cron del <cron-id>" => "Remove a cron task."
9
11
  on /^cron del (\d+)/ do |msg|
10
- post stop(msg[1].to_i)
12
+ post stop(msg.captures[1].to_i)
11
13
  end
12
14
 
15
+ help "cron list" => "List all cron tasks."
13
16
  on /^cron list\b/ do |msg|
14
17
  post list_all
15
18
  end
16
19
 
17
- usage <<~EOL
18
- Usage:
19
- `cron add <pattern> <message>` - add a cron task.
20
- `cron del <cron-id>` - remove a cron task.
21
- `cron list` - list all cron tasks.
22
- EOL
23
-
20
+ private
24
21
  def self.timers
25
22
  @timers ||= {}
26
23
  end
@@ -29,26 +26,25 @@ module Qbot
29
26
  schedule(id, cron, text) rescue return
30
27
 
31
28
  cache[id] = [cron, text]
32
- "ADD " + dump_line(id, cron, text)
29
+ "ADD #{format(id, cron, text)}"
33
30
  end
34
31
 
35
32
  def stop(id)
36
33
  Cron.timers[id].cancel if Cron.timers[id]
37
34
  cron, text = cache[id]
38
- return unless cron && text
39
35
 
40
- cache[id] = nil
41
- "DEL " + dump_line(id, cron, text)
36
+ cache[id] = nil
37
+ "DEL #{format(id, cron, text)}"
42
38
  end
43
39
 
44
40
  def list_all
45
41
  resp = StringIO.new
46
42
  cache.each do |id, (cron, text)|
47
43
  next unless cron && text
48
- resp.puts dump_line(id, cron, text)
44
+ resp.puts format(id, cron, text)
49
45
  end
50
46
 
51
- resp.string
47
+ resp.string.chomp
52
48
  end
53
49
 
54
50
  private
@@ -63,7 +59,7 @@ module Qbot
63
59
  end
64
60
  end
65
61
 
66
- def dump_line(id, cron, text)
62
+ def format(id, cron, text)
67
63
  "#{id.to_s.rjust(3, '0')}: #{cron} #{text}"
68
64
  end
69
65
 
@@ -2,15 +2,11 @@ module Qbot
2
2
 
3
3
  class Echo < Qbot::Base
4
4
 
5
+ help "echo <text>" => "Echo back the given <text>."
5
6
  on /^echo( .+)$/ do |msg|
6
- post msg[1].strip
7
+ post msg.captures[1].strip
7
8
  end
8
9
 
9
- usage <<~EOL
10
- Usage:
11
- `echo <text>` - Reflect the given <text>.
12
- EOL
13
-
14
10
  end
15
11
 
16
12
  end
@@ -0,0 +1,18 @@
1
+ require 'stringio'
2
+
3
+ module Qbot
4
+
5
+ class Help < Qbot::Base
6
+
7
+ on /^help\b/i do |msg|
8
+ usage = Qbot.app.help_text
9
+
10
+ text = StringIO.new
11
+ text.puts 'Usage:'
12
+ text.puts usage.map { |key, val| "`#{key}` - #{usage[key]}" }
13
+ post text.string.chomp
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -2,8 +2,8 @@ module Qbot
2
2
 
3
3
  class Ping < Qbot::Base
4
4
 
5
- on /^ping\b/i do |msg|
6
- post msg[0].tr('iI', 'oO')
5
+ on /\bping\b/i, global: true do |msg|
6
+ post msg.captures[0].tr('iI', 'oO')
7
7
  end
8
8
 
9
9
  end
@@ -0,0 +1,29 @@
1
+ module Qbot
2
+
3
+ class Message
4
+
5
+ attr_accessor :text
6
+ attr_reader :data, :captures
7
+
8
+ def initialize(data, text = '')
9
+ @data = data
10
+ @text = text
11
+ end
12
+
13
+ def match(regexp, prefix: nil)
14
+ text = @text.dup
15
+ text.sub!(/^#{prefix}/, '') if prefix
16
+ @captures = text.strip.match(regexp)
17
+ end
18
+
19
+ def mention(regexp = nil)
20
+ @mention = text.slice(regexp) if regexp
21
+ @mention
22
+ end
23
+
24
+ def mentioned?
25
+ !!mention
26
+ end
27
+ end
28
+
29
+ end
@@ -9,7 +9,7 @@ module Qbot
9
9
  end
10
10
 
11
11
  def namespace(ns)
12
- @db[ns]
12
+ @db[ns] ||= {}
13
13
  end
14
14
 
15
15
  end
@@ -1,3 +1,3 @@
1
1
  module Qbot
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.10"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qbot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - haccht
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-09-14 00:00:00.000000000 Z
11
+ date: 2019-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -175,7 +175,9 @@ files:
175
175
  - lib/qbot/base.rb
176
176
  - lib/qbot/embed/cron.rb
177
177
  - lib/qbot/embed/echo.rb
178
+ - lib/qbot/embed/help.rb
178
179
  - lib/qbot/embed/ping.rb
180
+ - lib/qbot/message.rb
179
181
  - lib/qbot/storage.rb
180
182
  - lib/qbot/storage/leveldb.rb
181
183
  - lib/qbot/storage/memory.rb
@@ -201,7 +203,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
201
203
  version: '0'
202
204
  requirements: []
203
205
  rubyforge_project:
204
- rubygems_version: 2.6.14.1
206
+ rubygems_version: 2.7.6
205
207
  signing_key:
206
208
  specification_version: 4
207
209
  summary: Tiny chatbot flamework.