2018-12-16 15:38:47 +00:00
|
|
|
import traceback
|
|
|
|
import re
|
|
|
|
import os
|
|
|
|
import sys
|
2019-01-14 22:47:38 +00:00
|
|
|
import hjson
|
2018-12-16 15:38:47 +00:00
|
|
|
import importlib
|
2019-01-12 17:02:34 +00:00
|
|
|
import sqlite3
|
|
|
|
import datetime as dt
|
|
|
|
|
|
|
|
from pip._internal import main as pipmain
|
2018-12-16 15:38:47 +00:00
|
|
|
|
2019-01-19 23:41:02 +00:00
|
|
|
from matrix_client.api import MatrixRequestError
|
2018-12-16 15:38:47 +00:00
|
|
|
from matrix_bot_api.mregex_handler import MRegexHandler
|
|
|
|
|
2019-01-19 23:41:02 +00:00
|
|
|
from .custom_matrix_client.api import CustomMatrixHttpApi
|
|
|
|
from .custom_matrix_client.client import CustomMatrixClient
|
|
|
|
|
2019-01-14 22:47:38 +00:00
|
|
|
MESSAGES_DIR = os.path.join(os.path.dirname(__file__), 'messages')
|
2019-01-12 17:02:34 +00:00
|
|
|
DATA_LOCATION = os.path.join(os.path.dirname(__file__), '../data/bot.db')
|
2018-12-16 15:38:47 +00:00
|
|
|
|
2019-01-14 22:47:38 +00:00
|
|
|
HELP_LOCATION = MESSAGES_DIR + '/help'
|
2018-12-16 15:38:47 +00:00
|
|
|
|
|
|
|
def eprint(*args, **kwargs):
|
2019-01-13 11:50:15 +00:00
|
|
|
"""Print error messages to stderr"""
|
2018-12-16 15:38:47 +00:00
|
|
|
print(*args, file=sys.stderr, **kwargs)
|
|
|
|
|
|
|
|
class MatrixBotAPI:
|
2019-01-12 17:02:34 +00:00
|
|
|
# rooms - List of rooms ids to operate in, or None to accept all rooms
|
2018-12-16 15:38:47 +00:00
|
|
|
def __init__(self, config, rooms=None):
|
2019-01-12 17:02:34 +00:00
|
|
|
self.config = config
|
|
|
|
self.username = self.config['bot_credentials']['username']
|
2018-12-16 15:38:47 +00:00
|
|
|
|
|
|
|
# Authenticate with given credentials
|
2019-01-19 23:41:02 +00:00
|
|
|
self.client = CustomMatrixClient(
|
|
|
|
self.config['bot_credentials']['server'])
|
2018-12-16 15:38:47 +00:00
|
|
|
try:
|
|
|
|
self.token = self.client.login_with_password(
|
2019-01-12 17:02:34 +00:00
|
|
|
self.config['bot_credentials']['username'],
|
|
|
|
self.config['bot_credentials']['password'])
|
2018-12-16 15:38:47 +00:00
|
|
|
except MatrixRequestError as e:
|
|
|
|
print(e)
|
|
|
|
if e.code == 403:
|
|
|
|
print("Bad username/password")
|
2019-01-19 23:41:02 +00:00
|
|
|
sys.exit()
|
2018-12-16 15:38:47 +00:00
|
|
|
except Exception as e:
|
|
|
|
print("Invalid server URL")
|
|
|
|
traceback.print_exc()
|
2019-01-19 23:41:02 +00:00
|
|
|
sys.exit()
|
2018-12-16 15:38:47 +00:00
|
|
|
|
|
|
|
# Store allowed rooms
|
|
|
|
self.rooms = rooms
|
|
|
|
|
|
|
|
# Store empty list of handlers
|
|
|
|
self.handlers = []
|
|
|
|
|
|
|
|
# If rooms is None, we should listen for invites and automatically
|
|
|
|
# accept them
|
|
|
|
if rooms is None:
|
|
|
|
self.client.add_invite_listener(self.handle_invite)
|
|
|
|
self.rooms = []
|
|
|
|
|
|
|
|
# Add all rooms we're currently in to self.rooms and add their
|
|
|
|
# callbacks
|
2019-01-19 23:41:02 +00:00
|
|
|
for room_id, room in self.client.rooms.items():
|
2018-12-16 15:38:47 +00:00
|
|
|
room.add_listener(self.handle_message)
|
|
|
|
self.rooms.append(room_id)
|
|
|
|
else:
|
|
|
|
# Add the message callback for all specified rooms
|
|
|
|
for room in self.rooms:
|
|
|
|
room.add_listener(self.handle_message)
|
|
|
|
|
|
|
|
# This flag can be set by the calling function to cancel all threads
|
|
|
|
# of all plugins
|
|
|
|
self.cancel = False
|
|
|
|
|
2019-01-12 17:02:34 +00:00
|
|
|
# Run setup
|
|
|
|
self.setup()
|
|
|
|
|
|
|
|
# Add plugins
|
|
|
|
self.add_plugins()
|
2018-12-16 15:38:47 +00:00
|
|
|
|
2019-01-12 17:02:34 +00:00
|
|
|
# Add callback for help function
|
|
|
|
self.help_handler = MRegexHandler(
|
|
|
|
self.config['triggers']['help'], self.help)
|
|
|
|
self.add_handler(self.help_handler)
|
|
|
|
|
2019-01-14 22:47:38 +00:00
|
|
|
|
2019-01-12 17:02:34 +00:00
|
|
|
def add_plugins(self):
|
2019-01-13 11:50:15 +00:00
|
|
|
"""Acquire list of plugins from configuration, load them,
|
|
|
|
initialize them, and add them to a list of object
|
|
|
|
"""
|
|
|
|
|
2019-01-12 17:02:34 +00:00
|
|
|
# Store empty list of plugins
|
|
|
|
self.plugin_objects = []
|
2018-12-16 15:38:47 +00:00
|
|
|
|
|
|
|
# Try to import plugins. All plugins must be located in the
|
|
|
|
# ./plugins directory
|
|
|
|
modules = []
|
|
|
|
|
2019-01-19 23:41:02 +00:00
|
|
|
try:
|
2019-01-13 11:50:15 +00:00
|
|
|
# Loop through the available plugins, install their requirements,
|
|
|
|
# load them as module, run their setup, append them to a list,
|
|
|
|
# and add their handler variables
|
2019-01-19 23:41:02 +00:00
|
|
|
for i, plugin in enumerate(self.config['plugins']):
|
|
|
|
# Install requirements
|
|
|
|
self.install_requirements(plugin)
|
2019-01-13 11:50:15 +00:00
|
|
|
|
2019-01-19 23:41:02 +00:00
|
|
|
# Dynamically load the module
|
|
|
|
modules.append(
|
|
|
|
importlib.import_module(
|
|
|
|
"plugins.{0}.{0}".format(plugin), package = None))
|
2018-12-16 15:38:47 +00:00
|
|
|
|
2019-01-19 23:41:02 +00:00
|
|
|
# Run the module's setup function and save that it got installed
|
|
|
|
self.setup_plugin(modules[i])
|
2019-01-12 17:02:34 +00:00
|
|
|
|
2019-01-19 23:41:02 +00:00
|
|
|
# Create new instance of plugin and append to plugin_objects array
|
|
|
|
self.plugin_objects.append(modules[i].Plugin(self))
|
2018-12-16 15:38:47 +00:00
|
|
|
|
2019-01-19 23:41:02 +00:00
|
|
|
# Add handler of newly created instance to bot
|
|
|
|
self.add_handler(self.plugin_objects[i].handler)
|
|
|
|
except:
|
|
|
|
eprint("Importing one or more of the plugins did not go well!")
|
|
|
|
traceback.print_exc()
|
|
|
|
sys.exit()
|
2018-12-16 15:38:47 +00:00
|
|
|
|
2019-01-13 11:50:15 +00:00
|
|
|
def check_installation_plugin(self, module_name):
|
|
|
|
"""Function returns 0 if a plugin with that name is not
|
|
|
|
currently installed
|
2019-01-12 17:02:34 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
# Define query to SELECT event from SQLite DB
|
|
|
|
select_sql = f"""SELECT module_name
|
|
|
|
FROM plugins
|
|
|
|
WHERE module_name = "{module_name}" """
|
2018-12-16 15:38:47 +00:00
|
|
|
|
2019-01-13 11:50:15 +00:00
|
|
|
# Open SQLite database and run query
|
2019-01-12 17:02:34 +00:00
|
|
|
sqlite_db = sqlite3.connect(DATA_LOCATION)
|
|
|
|
sqlite_cursor = sqlite_db.cursor()
|
|
|
|
sqlite_cursor.execute(select_sql)
|
|
|
|
|
2019-01-13 11:50:15 +00:00
|
|
|
number_fetched = sqlite_cursor.fetchall()
|
|
|
|
|
|
|
|
sqlite_db.close()
|
|
|
|
|
|
|
|
return number_fetched
|
|
|
|
|
|
|
|
def install_requirements(self, module_name):
|
|
|
|
"""Install requirements for a given plugin."""
|
|
|
|
if self.check_installation_plugin(module_name):
|
2019-01-12 17:02:34 +00:00
|
|
|
# Do nothing, this was already installed
|
|
|
|
print(f"Check: {module_name} already installed.")
|
|
|
|
else:
|
|
|
|
# Install requirements
|
|
|
|
pipmain(
|
|
|
|
['install',
|
|
|
|
'-r',
|
|
|
|
f'plugins/{module_name}/requirements.txt'])
|
|
|
|
|
2019-01-13 11:50:15 +00:00
|
|
|
|
|
|
|
def setup_plugin(self, module):
|
|
|
|
"""Run the setup for a given plugin and subsequently save that
|
|
|
|
this plugin was installed.
|
|
|
|
"""
|
|
|
|
|
|
|
|
module_name = module.__name__.split('.')[-1]
|
|
|
|
|
|
|
|
if not self.check_installation_plugin(module_name):
|
|
|
|
# This appears to be new, insert it, process requirements.txt
|
|
|
|
# and run the plugin's setup file.
|
|
|
|
print(f"Running installation of {module_name}.")
|
|
|
|
|
2019-01-12 17:02:34 +00:00
|
|
|
# Run module's install method
|
2019-01-19 23:41:02 +00:00
|
|
|
try:
|
|
|
|
module.setup()
|
|
|
|
except:
|
|
|
|
print(f"{module_name} did not specify setup(). Skipping...")
|
2019-01-12 17:02:34 +00:00
|
|
|
|
|
|
|
# Save in database that we installed this plugin
|
|
|
|
datetime_added = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
2019-01-13 11:50:15 +00:00
|
|
|
# Open SQLite database
|
2019-01-12 17:02:34 +00:00
|
|
|
insert_sql = f"""INSERT INTO plugins(module_name, datetime_added)
|
|
|
|
VALUES('{module_name}', '{datetime_added}')"""
|
|
|
|
|
2019-01-13 11:50:15 +00:00
|
|
|
sqlite_db = sqlite3.connect(DATA_LOCATION)
|
|
|
|
sqlite_cursor = sqlite_db.cursor()
|
2019-01-12 17:02:34 +00:00
|
|
|
sqlite_cursor.execute(insert_sql)
|
|
|
|
sqlite_db.commit()
|
|
|
|
sqlite_db.close()
|
|
|
|
|
|
|
|
# Tell the user how awesome we are
|
|
|
|
print(f"Successfully installed {module_name}.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setup(self):
|
2018-12-16 15:38:47 +00:00
|
|
|
"""This function only runs once, ever. It initializes an SQLite
|
2019-01-12 17:02:34 +00:00
|
|
|
database, sets the bot's avatar, and name
|
|
|
|
"""
|
2018-12-16 15:38:47 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
# Try to make new directory
|
|
|
|
os.mkdir("data")
|
|
|
|
|
2019-01-12 17:02:34 +00:00
|
|
|
# Create SQLite database
|
|
|
|
# Define query to INSERT event table to SQLite DB
|
|
|
|
sql = """CREATE TABLE 'plugins' (
|
|
|
|
'id' INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
'datetime_added' DATTIME,
|
|
|
|
'module_name' TEXT);"""
|
|
|
|
|
|
|
|
# Open, execute, commit, and close SQLite database
|
|
|
|
sqlite_db = sqlite3.connect(DATA_LOCATION)
|
|
|
|
sqlite_cursor = sqlite_db.cursor()
|
|
|
|
sqlite_cursor.execute(sql)
|
|
|
|
sqlite_db.commit()
|
|
|
|
sqlite_db.close()
|
|
|
|
|
2018-12-16 15:38:47 +00:00
|
|
|
# Get user instance
|
|
|
|
self.user = self.client.get_user(self.username)
|
|
|
|
|
2019-01-12 17:02:34 +00:00
|
|
|
## Set username
|
|
|
|
self.user.set_display_name(self.config['character']['name'])
|
2018-12-16 15:38:47 +00:00
|
|
|
|
2019-01-12 17:02:34 +00:00
|
|
|
## Open, upload, and set avatar
|
|
|
|
f = open(self.config['character']['avatar'], mode="rb")
|
2018-12-16 15:38:47 +00:00
|
|
|
|
2019-01-12 17:02:34 +00:00
|
|
|
avatar = self.client.upload(
|
2019-01-12 21:35:16 +00:00
|
|
|
f, self.config['character']['avatar_mime'])
|
2018-12-16 15:38:47 +00:00
|
|
|
self.user.set_avatar_url(avatar)
|
|
|
|
|
|
|
|
except FileExistsError:
|
|
|
|
# Do nothing, data directory already exists
|
|
|
|
pass
|
|
|
|
|
|
|
|
def help(self, room, event):
|
|
|
|
"""Prints a general help message and then grabs all help
|
|
|
|
messages from the different plugins
|
|
|
|
"""
|
|
|
|
|
|
|
|
help_text = open(HELP_LOCATION, mode="r").read()
|
|
|
|
|
2019-01-12 17:02:34 +00:00
|
|
|
for plugin in self.plugin_objects:
|
2019-01-12 14:13:21 +00:00
|
|
|
try:
|
|
|
|
help_text += plugin.help()
|
|
|
|
except TypeError:
|
|
|
|
# The plugin probably returned 0, ignore it
|
|
|
|
pass
|
2018-12-16 15:38:47 +00:00
|
|
|
|
2019-01-19 23:41:02 +00:00
|
|
|
self.client.send_message_private_public(room, event, help_text)
|
2018-12-16 15:38:47 +00:00
|
|
|
|
|
|
|
def add_handler(self, handler):
|
2019-01-06 15:01:01 +00:00
|
|
|
try:
|
|
|
|
# Assume it's a list and not a single handler
|
|
|
|
for handler_obj in handler:
|
|
|
|
self.handlers.append(handler_obj)
|
|
|
|
except TypeError:
|
|
|
|
# If it is not a list, TypeError: not iterable will occur
|
|
|
|
self.handlers.append(handler)
|
2018-12-16 15:38:47 +00:00
|
|
|
|
|
|
|
def handle_message(self, room, event):
|
|
|
|
# Make sure we didn't send this message
|
2019-01-06 15:01:01 +00:00
|
|
|
if re.match(self.username, event['sender']):
|
2018-12-16 15:38:47 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
# Loop through all installed handlers and see if they need to be called
|
|
|
|
for handler in self.handlers:
|
|
|
|
if handler.test_callback(room, event):
|
|
|
|
# This handler needs to be called
|
|
|
|
try:
|
|
|
|
handler.handle_callback(room, event)
|
|
|
|
except:
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
def handle_invite(self, room_id, state):
|
|
|
|
print("Got invite to room: " + str(room_id))
|
|
|
|
print("Joining...")
|
|
|
|
room = self.client.join_room(room_id)
|
|
|
|
|
|
|
|
# Add message callback for this room
|
|
|
|
room.add_listener(self.handle_message)
|
|
|
|
|
|
|
|
# Add room to list
|
|
|
|
self.rooms.append(room)
|
|
|
|
|
|
|
|
def start_polling(self):
|
|
|
|
# Starts polling for messages
|
|
|
|
self.client.start_listener_thread()
|
2019-01-19 23:41:02 +00:00
|
|
|
|
2018-12-16 15:38:47 +00:00
|
|
|
return self.client.sync_thread
|