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.
@@ -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
@@ -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
@@ -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