chatrix-bot 1.0.0.pre
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.
- checksums.yaml +7 -0
- data/.editorconfig +12 -0
- data/.gitignore +170 -0
- data/.rspec +3 -0
- data/.rubocop.yml +2 -0
- data/.travis.yml +11 -0
- data/.yardopts +5 -0
- data/Gemfile +31 -0
- data/Guardfile +67 -0
- data/LICENSE +21 -0
- data/README.md +67 -0
- data/Rakefile +11 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/chatrix-bot.gemspec +32 -0
- data/exe/chatrix-bot +53 -0
- data/lib/chatrix/bot.rb +97 -0
- data/lib/chatrix/bot/command.rb +99 -0
- data/lib/chatrix/bot/config.rb +60 -0
- data/lib/chatrix/bot/errors.rb +16 -0
- data/lib/chatrix/bot/parameter.rb +57 -0
- data/lib/chatrix/bot/pattern.rb +25 -0
- data/lib/chatrix/bot/plugin.rb +149 -0
- data/lib/chatrix/bot/plugin_manager.rb +83 -0
- data/lib/chatrix/bot/plugins.rb +16 -0
- data/lib/chatrix/bot/plugins/echo.rb +36 -0
- data/lib/chatrix/bot/plugins/help.rb +50 -0
- data/lib/chatrix/bot/plugins/hug.rb +22 -0
- data/lib/chatrix/bot/version.rb +8 -0
- metadata +102 -0
data/chatrix-bot.gemspec
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
6
|
+
require 'chatrix/bot/version'
|
|
7
|
+
|
|
8
|
+
Gem::Specification.new do |spec|
|
|
9
|
+
spec.name = 'chatrix-bot'
|
|
10
|
+
spec.version = Chatrix::Bot::VERSION
|
|
11
|
+
spec.authors = ['Adam Hellberg']
|
|
12
|
+
spec.email = ['sharparam@sharparam.com']
|
|
13
|
+
|
|
14
|
+
spec.summary = 'A Ruby chatbot for Matrix with plugin support'
|
|
15
|
+
# spec.description = %q{}
|
|
16
|
+
spec.homepage = 'https://github.com/Sharparam/chatrix-bot'
|
|
17
|
+
spec.license = 'MIT'
|
|
18
|
+
|
|
19
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
20
|
+
f.match(%r{^(test|spec|features)/})
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
spec.bindir = 'exe'
|
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
25
|
+
spec.require_paths = ['lib']
|
|
26
|
+
|
|
27
|
+
spec.required_ruby_version = '>= 2.3.0'
|
|
28
|
+
|
|
29
|
+
spec.add_runtime_dependency 'chatrix', '~> 1.0'
|
|
30
|
+
|
|
31
|
+
spec.add_development_dependency 'bundler', '~> 1.12'
|
|
32
|
+
end
|
data/exe/chatrix-bot
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require 'optparse'
|
|
6
|
+
|
|
7
|
+
require 'chatrix'
|
|
8
|
+
require 'chatrix/bot'
|
|
9
|
+
|
|
10
|
+
require 'pp'
|
|
11
|
+
|
|
12
|
+
defaults = {
|
|
13
|
+
config: Chatrix::Bot::Config::DEFAULT_CONFIG_PATH
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
options = {}
|
|
17
|
+
|
|
18
|
+
OptionParser.new do |opts|
|
|
19
|
+
opts.banner = 'Usage: chatrix-bot [options]'
|
|
20
|
+
|
|
21
|
+
opts.on(
|
|
22
|
+
'-g',
|
|
23
|
+
'--generate [FILE]',
|
|
24
|
+
'Generate config to specified file and exit.',
|
|
25
|
+
" FILE defaults to '#{defaults[:config]}'"
|
|
26
|
+
) do |file|
|
|
27
|
+
file ||= defaults[:config]
|
|
28
|
+
data = Chatrix::Bot::Config.defaults
|
|
29
|
+
Chatrix::Bot::Config.new(file, data).save
|
|
30
|
+
exit
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
opts.on(
|
|
34
|
+
'-c',
|
|
35
|
+
'--config FILE',
|
|
36
|
+
'Specify the config file to use',
|
|
37
|
+
" (uses '#{defaults[:config]}' if not specified)"
|
|
38
|
+
) do |c|
|
|
39
|
+
options[:config] = c
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
opts.on_tail('-v', '--version', 'Print version and exit') do
|
|
43
|
+
puts "chatrix-bot v#{Chatrix::Bot::VERSION}, chatrix v#{Chatrix::VERSION}"
|
|
44
|
+
exit
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
opts.on_tail('-h', '--help', 'Shows this help message and exits') do
|
|
48
|
+
puts opts
|
|
49
|
+
exit
|
|
50
|
+
end
|
|
51
|
+
end.parse!
|
|
52
|
+
|
|
53
|
+
pp options
|
data/lib/chatrix/bot.rb
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
require 'chatrix/bot/errors'
|
|
6
|
+
require 'chatrix/bot/version'
|
|
7
|
+
require 'chatrix/bot/config'
|
|
8
|
+
require 'chatrix/bot/plugin_manager'
|
|
9
|
+
|
|
10
|
+
require 'chatrix'
|
|
11
|
+
|
|
12
|
+
module Chatrix
|
|
13
|
+
# The Chatrix bot class.
|
|
14
|
+
class Bot
|
|
15
|
+
# Config object containing the bot config.
|
|
16
|
+
attr_reader :config
|
|
17
|
+
|
|
18
|
+
# The Logger instance for the bot.
|
|
19
|
+
attr_reader :log
|
|
20
|
+
|
|
21
|
+
# The PluginManager for this bot.
|
|
22
|
+
attr_reader :plugin_manager
|
|
23
|
+
|
|
24
|
+
# Initializes a new Bot instance.
|
|
25
|
+
# @param file [String] File to load config from.
|
|
26
|
+
def initialize(file = Config::DEFAULT_CONFIG_PATH)
|
|
27
|
+
@started_at = (Time.now.to_f * 1e3).round
|
|
28
|
+
|
|
29
|
+
@config = Config.load file
|
|
30
|
+
|
|
31
|
+
init_logger
|
|
32
|
+
|
|
33
|
+
log.debug 'Initializing plugin manager'
|
|
34
|
+
@plugin_manager = PluginManager.new self
|
|
35
|
+
|
|
36
|
+
log.debug 'bot finished initializing'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def admin?(user)
|
|
40
|
+
@config[:admins].member? user.id
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Starts the bot (starts syncing with the homeserver).
|
|
44
|
+
def start
|
|
45
|
+
init_client unless @client
|
|
46
|
+
|
|
47
|
+
log.info 'Bot starting to sync'
|
|
48
|
+
|
|
49
|
+
@client.start_syncing
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Stops the bot (stops syncing with the homeserver).
|
|
53
|
+
def stop
|
|
54
|
+
log.info 'Bot stopping sync'
|
|
55
|
+
@client.stop_syncing
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def on_room_message(room, message)
|
|
59
|
+
# Do not process messages sent before we joined, or if we sent
|
|
60
|
+
# them ourselves
|
|
61
|
+
return if message.sender == @client.me || message.timestamp < @started_at
|
|
62
|
+
plugin_manager.parse_message room, message
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def on_sync_error(error)
|
|
66
|
+
log.error "SYNC ERROR: #{error.inspect}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def init_logger
|
|
72
|
+
if @config[:debug]
|
|
73
|
+
@log = Logger.new $stdout
|
|
74
|
+
@log.level = Logger::DEBUG
|
|
75
|
+
else
|
|
76
|
+
@log = Logger.new @config[:log_file], 'daily'
|
|
77
|
+
@log.level = @config[:log_level]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
@log.progname = 'chatrix-bot'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def init_client
|
|
84
|
+
log.debug 'Client initialization'
|
|
85
|
+
|
|
86
|
+
@client = Chatrix::Client.new(
|
|
87
|
+
@config[:access_token],
|
|
88
|
+
@config[:user_id],
|
|
89
|
+
homeserver: @config[:homeserver]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
log.debug 'Client event registrations'
|
|
93
|
+
|
|
94
|
+
@client.subscribe(self, prefix: :on)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'chatrix/bot/parameter'
|
|
4
|
+
|
|
5
|
+
module Chatrix
|
|
6
|
+
class Bot
|
|
7
|
+
# Describes a plugin command.
|
|
8
|
+
class Command
|
|
9
|
+
PREFIX = '!'
|
|
10
|
+
|
|
11
|
+
attr_reader :name
|
|
12
|
+
|
|
13
|
+
attr_reader :syntax
|
|
14
|
+
|
|
15
|
+
attr_reader :help
|
|
16
|
+
|
|
17
|
+
attr_reader :handler
|
|
18
|
+
|
|
19
|
+
attr_reader :required_power
|
|
20
|
+
|
|
21
|
+
def initialize(name, syntax, help, opts = {})
|
|
22
|
+
@name = name
|
|
23
|
+
@syntax = syntax
|
|
24
|
+
@help = help
|
|
25
|
+
@aliases = opts[:aliases] || []
|
|
26
|
+
@handler = opts[:handler]
|
|
27
|
+
@required_power = opts[:power] || 0
|
|
28
|
+
|
|
29
|
+
configure_parameters syntax, opts
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.command?(message)
|
|
33
|
+
!message.match(/^#{PREFIX}[^\s]/).nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.extract_name(message)
|
|
37
|
+
message.match(/^#{PREFIX}([^\s]+)/)[1]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.stylize(name)
|
|
41
|
+
"#{PREFIX}#{name}" unless command? name
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.parse(message)
|
|
45
|
+
return nil unless command? message
|
|
46
|
+
|
|
47
|
+
name = message.match(/^#{PREFIX}([^\s]+)/)[1]
|
|
48
|
+
rest = message.match(/ (.+)$/)
|
|
49
|
+
body = rest.is_a?(MatchData) ? rest[1].to_s : ''
|
|
50
|
+
|
|
51
|
+
{ name: name, body: body }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def name_or_alias?(name)
|
|
55
|
+
@name == name || @aliases.member?(name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def parse(message)
|
|
59
|
+
data = {}
|
|
60
|
+
|
|
61
|
+
@parameters.each_with_index do |param, index|
|
|
62
|
+
last = index == @parameters.count - 1
|
|
63
|
+
parsed = param.parse message, last ? Parameter::MATCHERS[:rest] : nil
|
|
64
|
+
|
|
65
|
+
break if parsed.nil?
|
|
66
|
+
|
|
67
|
+
data[param.name] = parsed[:content]
|
|
68
|
+
message = parsed[:rest]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
data
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def usage
|
|
75
|
+
<<~EOF
|
|
76
|
+
Usage: #{self.class.stylize(name)} #{syntax}
|
|
77
|
+
#{help}
|
|
78
|
+
Required power level to use: #{required_power}
|
|
79
|
+
EOF
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test(user, room)
|
|
83
|
+
raise PermissionError unless user.power_in(room) >= required_power
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def configure_parameters(syntax, options = {})
|
|
89
|
+
@parameters = []
|
|
90
|
+
syntax.scan(/([<\[])([\w\ ]+)[>\]]/) do |match|
|
|
91
|
+
param = match[1].gsub(/\s+/, '_').to_sym
|
|
92
|
+
required = match[0] == '<'
|
|
93
|
+
matcher = options[:matchers][param] if options.key? :matchers
|
|
94
|
+
@parameters.push Parameter.new param, required, matcher
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
module Chatrix
|
|
7
|
+
class Bot
|
|
8
|
+
# Manages configuration for the bot.
|
|
9
|
+
#
|
|
10
|
+
# Storage location for other data is based on the directory
|
|
11
|
+
# containing the file for the config. I.E: If the config file is stored
|
|
12
|
+
# as `~/.chatrix-bot/config.yaml`, then `~/.chatrix-bot` will be used
|
|
13
|
+
# as the data directory for the bot.
|
|
14
|
+
class Config
|
|
15
|
+
DEFAULT_CONFIG_PATH = '~/.config/chatrix-bot/config.yaml'
|
|
16
|
+
|
|
17
|
+
attr_reader :file
|
|
18
|
+
|
|
19
|
+
def initialize(file = DEFAULT_CONFIG_PATH, data = nil)
|
|
20
|
+
@file = File.expand_path file
|
|
21
|
+
@dir = File.dirname @file
|
|
22
|
+
@data = data || {}
|
|
23
|
+
|
|
24
|
+
FileUtils.mkpath @dir unless File.exist? @dir
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def [](key)
|
|
28
|
+
@data[key]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def []=(key, value)
|
|
32
|
+
@data[key] = value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.load(file)
|
|
36
|
+
YAML.load_file File.expand_path(file)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.defaults
|
|
40
|
+
{
|
|
41
|
+
access_token: '<ACCESS TOKEN>',
|
|
42
|
+
user_id: '<MY USER ID>',
|
|
43
|
+
admins: ['GLOBAL ADMINS', '@user:example.com'],
|
|
44
|
+
homeserver: 'https://server.example.com:1234',
|
|
45
|
+
log_file: 'chatrix-bot.log',
|
|
46
|
+
log_level: Logger::INFO
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def get(key, default)
|
|
51
|
+
self[key] = default if self[key].nil?
|
|
52
|
+
self[key]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def save(file = @file)
|
|
56
|
+
File.open(file, 'w') { |f| f.write to_yaml }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chatrix
|
|
4
|
+
class Bot
|
|
5
|
+
# General bot error.
|
|
6
|
+
class BotError < RuntimeError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Error raised while parsing a command.
|
|
10
|
+
class CommandError < BotError
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class PermissionError < CommandError
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chatrix
|
|
4
|
+
class Bot
|
|
5
|
+
# Describes a parameter in a command.
|
|
6
|
+
class Parameter
|
|
7
|
+
MATCHERS = {
|
|
8
|
+
normal: /[^\s]+/,
|
|
9
|
+
quoted: /"(?:\\"|[^"])*?"/,
|
|
10
|
+
rest: /.+$/
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :name
|
|
14
|
+
|
|
15
|
+
attr_reader :required
|
|
16
|
+
|
|
17
|
+
def initialize(name, required, matcher = nil)
|
|
18
|
+
@name = name
|
|
19
|
+
@required = required
|
|
20
|
+
@matcher = matcher || MATCHERS[:normal]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def required?
|
|
24
|
+
required
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def match(text, matcher = nil)
|
|
28
|
+
(matcher || @matcher).match text
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse(text, matcher = nil)
|
|
32
|
+
if required && text.empty?
|
|
33
|
+
raise CommandError, "Missing required parameter #{name}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
return nil if text.empty?
|
|
37
|
+
|
|
38
|
+
match = match text.gsub(/^\s+/, '').gsub(/\s+$/, ''), matcher
|
|
39
|
+
|
|
40
|
+
return nil unless valid_match? match
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
content: match.to_s,
|
|
44
|
+
rest: match.post_match.gsub(/^\s+/, '').gsub(/\s+$/, '')
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def valid_match?(match)
|
|
51
|
+
return true unless match.nil?
|
|
52
|
+
raise CommandError, "Missing required parameter #{name}" if required
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chatrix
|
|
4
|
+
class Bot
|
|
5
|
+
# Describes a pattern used for matching messages.
|
|
6
|
+
class Pattern
|
|
7
|
+
attr_reader :pattern
|
|
8
|
+
|
|
9
|
+
attr_reader :handler
|
|
10
|
+
|
|
11
|
+
def initialize(pattern, handler = nil)
|
|
12
|
+
@pattern = pattern
|
|
13
|
+
@handler = handler
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def match?(text)
|
|
17
|
+
!match(text).nil?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def match(text)
|
|
21
|
+
@pattern.match text
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|