Refactor for ActiveRecord and Config Files

Update Custom Commands to leverage ActiveRecord and save commands to a
database. By default, it will use SQLite. Configuration for this (and
potential configuration for PostgreSQL and MySQL) live in
`config/db.yml`. Inculde a Rakefile for handling DB creation and
migrations.

Rakefile: Add Rakefile to handle running the bot, and DB management
Gemfile: Update with new gem dependencies
db/migrate/*: ActiveRecord migrations for Custom Command
custom_commands.rb: Update to leverage ActiveRecord

Leverage the Rakefile to start the bot, removing the binary file. Update
the Dockerfile to also leverage the Rakefile.

Dockerfile: Update to use Rakefile, and install new dependencies
chronicle: Remove unnecessary start file

Refactor the `chronicle_bot` file into `chronicle` and `matrix`

chronicle.rb: General Chronicle setup
matrix.rb: Start a Matrix-specific bot

Update the bot to read configuration from files, instead of either the
environment, or hard-coded values.

config/db.yml: Database configuration
config/bot.yml: General bot configuration

Update the README to reflect the above change with regards to running
the bot either using the Rakefile, or using a Docker container.
This commit is contained in:
Bill Niblock 2021-02-27 19:40:54 -05:00
parent b919de11f5
commit ce06aa568b
11 changed files with 312 additions and 136 deletions

View file

@ -5,6 +5,7 @@
# For example, "!hello" might print:
# "Welcome to the channel! You can follow [this link](link) to visit
# our website!"
require 'active_record'
require 'json'
module Chronicle
@ -19,10 +20,8 @@ module Chronicle
def initialize(bot)
@bot = bot
@msgid = 'tmp_until_per-room'
@custom_commands = read_commands(@msgid)
@bot.available_commands(self, @custom_commands.keys)
@bot.available_commands(self, list_commands)
end
# Provide help for the commands of this addon
@ -49,6 +48,7 @@ module Chronicle
# @param message [Message object] The relevant message object
def matrix_command(message)
pfx = @bot.cmd_prefix
roomid = message.room_id
cmd = message.content[:body].split(/\s+/)[0].gsub(/#{pfx}/, '')
msgstr = message.content[:body]
.gsub(/#{pfx}\w+\s*/, '')
@ -58,43 +58,34 @@ module Chronicle
case cmd
when "addcommand"
res = handle_addcommand(msgstr)
res = handle_addcommand(roomid, msgstr)
when "modcommand"
res = handle_modcommand(msgstr)
res = handle_modcommand(roomid, msgstr)
when "remcommand"
res = handle_remcommand(msgstr)
res = handle_remcommand(roomid, msgstr)
else
res = runcmd(cmd)
res = handle_runcommand(roomid, cmd)
end
room = @bot.client.ensure_room(message.room_id)
room = @bot.client.ensure_room(roomid)
room.send_notice(res)
end
# Add a new custom command
def addcmd(message)
command = message.slice!(/\w+\s+/).strip
return cmd_add_error(command) if verify_commands(command)
return cmd_addon_error if addon_command(command)
@custom_commands[command] = message
@bot.available_commands(self, [command])
save_commands(@msgid)
"New command saved: !#{command}"
end
# Adds a new custom command
#
# @param roomid [string] The Matrix Room ID
# @param message [String] The command plus response
# @return
def handle_addcommand(message)
res = 'Usage: !addcommand NAME RESPONSE'
# @return A response message
def handle_addcommand(roomid, message)
res = cmd_add_usage
if message.split(/\s+/).count > 1
res = addcmd(message)
command = message.slice!(/\w+\s+/).strip
res = save_command(roomid, command, message)
@bot.available_commands(self, [command])
end
res
@ -102,12 +93,18 @@ module Chronicle
# Modify an existing custom command
#
# @param roomid [string] The Matrix Room ID
# @param message [hash] The message data from Matrix
def handle_modcommand(message)
res = 'Usage: !modcommand NAME NEW-RESPONSE'
# @return A response message
def handle_modcommand(roomid, message)
res = cmd_add_usage
if message.split(/\s+/).count > 1
res = modcmd(message)
command = message.slice!(/\w+\s+/).strip
res = mod_command(roomid, command, message)
@bot.available_commands(self, [command])
end
res
@ -115,47 +112,37 @@ module Chronicle
# Remove an existing custom command
#
# @param roomid [string] The Matrix Room ID
# @param message [hash] The message data from Matrix
def handle_remcommand(message)
res = 'Usage: !remcommand NAME'
# @return A response message
def handle_remcommand(roomid, message)
res = cmd_rem_usage
if message.split(/\s+/).count == 1
res = remcmd(message)
command = message.strip
res = remove_command(roomid, command)
@bot.disable_commands(command)
end
res
end
# Modify an existing custom command
def modcmd(message)
command = message.slice!(/\w+\s+/).strip
# Return the response for a custom command
#
# @param roomid [string] The Matrix Room ID
# @param message [hash] The message data from Matrix
# @return A response message
def handle_runcommand(roomid, message)
res = cmd_rem_usage
return cmd_mod_error(command) unless verify_commands(command)
res = CustomCommands.find_by(
roomid: roomid,
command: message.strip
).response
@custom_commands[command] = message
save_commands(@msgid)
"!#{command} modified."
end
# Delete an existing custom command
def remcmd(message)
command = message.strip
return cmd_rem_error unless verify_commands(command)
@custom_commands.delete(command)
@bot.disable_commands(command)
save_commands(@msgid)
"!#{command} removed."
end
# Execute a custom command
def runcmd(command)
return cmd_mod_error(command) unless verify_commands(command)
@custom_commands[command]
res
end
private
@ -165,12 +152,6 @@ module Chronicle
@bot.all_commands.keys.include?(command)
end
# Error message when trying to add an existing command
def cmd_add_error(command)
'This custom command already exists. '\
"You can modify it by typing `!modcommand #{command}`"
end
# Help message for addcommand
def cmd_add_usage
'Add a custom command. '\
@ -212,33 +193,84 @@ module Chronicle
"\nUsage: !remcommand EXISTING-COMMAND"
end
# Read the existing saved commands into memory
def read_commands(msgid)
cmds = {}
cmds_file = "#{msgid}_custom_commands.json"
if File.exist?(cmds_file) && !File.empty?(cmds_file)
File.open(cmds_file, 'r') do |f|
cmds = JSON.parse(f.read)
end
# List all available commands from the DB
def list_commands
commands = CustomCommands.select(:command).map do |c|
c.command
end
cmds
commands
end
# Save the existing commands to a local file
def save_commands(msgid)
cmds_file = "#{msgid}_custom_commands.json"
# Modify an existing command in the DB
def mod_command(roomid, command, response)
res = "Command updated: !#{command}"
File.open(cmds_file, 'w') do |f|
f.write(@custom_commands.to_json)
cc = CustomCommands.find_by(:command => command)
cc.response = response
unless cc.save
@bot.scribe.info('CustomCommander') {
"Problem modifying: #{command}. Not saved."
}
return cc.errors.objects.first.full_message
end
@bot.scribe.info('CustomCommander') {
"Custom command updated: #{command}"
}
res
end
# Check if a command already exists
def verify_commands(command)
@custom_commands.keys.include?(command)
# Remove an existing command from the DB
def remove_command(roomid, command)
res = "Command removed: !#{command}"
CustomCommands.find_by(
roomid: roomid,
command: command
).delete
res
end
# Save a new command to the DB
def save_command(roomid, command, response)
res = "Command saved: !#{command}"
cc = CustomCommands.new do |c|
c.roomid = roomid
c.command = command
c.response = response
end
unless cc.save
@bot.scribe.info('CustomCommander') {
"Duplicate command: #{command}. Not saved."
}
return cc.errors.objects.first.full_message
end
@bot.scribe.info('CustomCommander') {
"Custom command saved: #{command}"
}
res
end
end
# The ActiveRecord model for handling the custom commands
class CustomCommands < ActiveRecord::Base
validates_presence_of :roomid, :command, :response
validates_length_of :command, { minimum: 1 }
validates_length_of :response, { minimum: 1 }
validates :command, uniqueness: { message:
"already exists. You can modify it by typing `!modcommand %{value}`"
}
end
end
end

61
lib/chronicle.rb Executable file
View file

@ -0,0 +1,61 @@
# frozen_string_literal: true
require 'active_record'
require 'faraday'
require 'json'
require 'logger'
require 'matrix_sdk'
require 'yaml'
require_relative './matrix'
# Require any addons
Dir[File.join(__dir__, 'addons', '*.rb')].each do |file|
require file
end
# Chronicle Bot
module Chronicle
# A filter to simplify syncs
BOT_FILTER = {
presence: { types: [] },
account_data: { types: [] },
room: {
ephemeral: { types: [] },
state: {
types: ['m.room.*'],
lazy_load_members: true
},
timeline: {
types: ['m.room.message']
},
account_data: { types: [] }
}
}.freeze
# Establish configuration for Chronicle
module Config
class << self
# Matrix connection configuration attributes
attr_accessor :matrix_homeserver, :matrix_access_token
# Logging configuration attributes
attr_accessor :log_file, :log_verbose
end
# Load a configuration Hash, and store in "module variables"
#
# @param config [Hash] a configuration hash
def self.load_config(config)
@matrix_homeserver = config["matrix"]["homeserver"]
@matrix_access_token = config["matrix"]["token"]
@log_file = config["log"]["file"]
@log_verbose = config["log"]["debug"]
end
end
def self.start()
Chronicle::Matrix.start
end
end

View file

@ -2,6 +2,7 @@
require 'faraday'
require 'json'
require 'logger'
require 'matrix_sdk'
# Require any addons
@ -11,51 +12,24 @@ end
# Chronicle Bot
module Chronicle
# A filter to simplify syncs
BOT_FILTER = {
presence: { types: [] },
account_data: { types: [] },
room: {
ephemeral: { types: [] },
state: {
types: ['m.room.*'],
lazy_load_members: true
},
timeline: {
types: ['m.room.message']
},
account_data: { types: [] }
}
}.freeze
# Chronicle Bot for Matrix
module Matrix
# Begin the beast
def self.start
unless ENV["CHRONICLE_HOMESERVER"]
raise "Export your homeserver URL to CHRONICLE_HOMESERVER"
end
unless ENV["CHRONICLE_ACCESS_TOKEN"]
raise "Export your access token to CHRONICLE_ACCESS_TOKEN"
end
if ENV["CHRONICLE_DEBUG"]
if Chronicle::Config.log_verbose >= 1
Thread.abort_on_exception = true
MatrixSdk.debug!
end
bot = ChronicleBot.new(
ENV["CHRONICLE_HOMESERVER"],
ENV["CHRONICLE_ACCESS_TOKEN"]
)
bot.run
ChronicleBot.new(
Chronicle::Config.matrix_homeserver,
Chronicle::Config.matrix_access_token
).run
end
# The bot
class ChronicleBot
attr_reader :all_commands, :cmd_prefix
attr_reader :all_commands, :cmd_prefix, :scribe
def initialize(hs_url, access_token)
@hs_url = hs_url
@ -65,6 +39,9 @@ module Chronicle
@all_commands = {}
@allowed_commands = {}
@scribe = Logger.new('chronicle.log')
@scribe.info('ChronicleBot') {'Initializing a new instance of Chronicle'}
register_commands
available_commands(self, %w[listcommands help])
end
@ -73,6 +50,7 @@ module Chronicle
def available_commands(addon, commands)
commands.each do |command|
@all_commands[command] = addon
@scribe.info('ChronicleBot') {"Adding available command: #{command}"}
end
end
@ -143,6 +121,10 @@ module Chronicle
return unless msgstr =~ /^#{@cmd_prefix}#{cmds}\s*/
msgstr.match(/^#{@cmd_prefix}(#{cmds})\s*/) do |m|
@scribe.info('ChronicleBot') {
"Running command: #{msgstr.split(' ')[0].strip}"
}
@all_commands[m.to_s[1..-1].strip].matrix_command(message)
end
end