diff --git a/README.md b/README.md index bf7a293..3521cf1 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,72 @@ The core of this chatbot is based on [github.com/shawnanastasio/python-matrix-bot-api](https://github.com/shawnanastasio/python-matrix-bot-api/) which was published under GPL-3.0. It is heavily modified to create an API to easily add plugins. This bot is intended to operate on the [Matrix network](https://matrix.org). -This code has two purposes. Firstly, and mainly, it serves as virtual assistent in the Matrix based communication platform of members of the Dutch student association [Alcuinus](https://alcuinus.nl). Secondly, the bot's easy to use API makes the development of a simple plugin an approachable first project for new members of Alcuinus' IT working groups during their training. +This bot was orginally created to serve two purposes. Firstly, and mainly, it serves as virtual assistent in the Matrix based communication platform of members of the Dutch student association [Alcuinus](https://alcuinus.nl). Secondly, the bot's easy to use API makes the development of a simple plugin an approachable first project for new members of Alcuinus' IT working groups during their training. ## Content * [Installation & Requirements](#installation-requirements) + * [Automatic installation (Linux)](#automatic-installation-linux) + * [Manual installation](#manual-installation) * [Plugins](#plugins) * [List of available plugins](#list-of-available-plugins) * [API](#api) - * [Mandatory files](#mandatory-files) + * [Mandatory/recommended files](#mandatory-recommended-files) * [Help template](#help-template) * [Plugin class template](#plugin-class-template) * [Additional tips](#additional-tips) ## Installation & Requirements: -... +First, clone this repository: +``` +git clone https://git.dennispotter.eu/Dennis/matrix-chatbot +``` +In order for the bot framework to work, Python 3, [virtualenv](https://virtualenv.pypa.io/en/latest/), and [pip](https://pypi.org/project/pip/) must be installed on your computer or server. + +### Automatic installation (Linux) +On Linux, the bash script `install.sh` can be used to install the bot. +``` +install.sh : Creates a virtualenv, an initial config.json, and installs + all required packages +install.sh service: Runs ./install and additionally adds a systemd service. + This command will ask you for root credentials! +install.sh help : Prints this message. +``` +### Manual installation +Create a virtual environment by entering the directory and running: +``` +virtualenv . +``` +Enter the virtual environment +``` +source bin/activate +``` +and install all required packages +``` +pip3 install -r requirements.txt +``` +Now, you need to create a configuration file `config.json`. The template `config.json.template` can be used for this purpose. + +Finally, the bot can be started by executing the following command within the virtual environment: + +``` +./run.py +``` ## Plugins The bot has different plugins that can be activated by sending `[Keyword in bot's config.json] [Keyword(s) in plugin's config.json]` to a room in which the bot is present. Below, under [List of available plugins](list-of-available-plugins), you can first find a list of already available plugins. Then, under [API](#api), you can find a description on how to develop a new plugin. ### List of available plugins: -* [Admidio events plugin](src/branch/master/plugins/events) +* [Hello plugin](src/branch/master/plugins/events): An example plugin that interacts with users that send "Hello bot" to the bot. +* [Admidio events plugin](src/branch/master/plugins/events): a plugin that interacts with an [Admidio](https://admidio.org) installation. ### API To interact with rooms, the [Matrix Python SDK](http://matrix-org.github.io/matrix-python-sdk/) can be used. -#### Mandatory files -To create a plugin, a few mandatory files must be present. The tree below shows the general structure of a plugin. A new plugin must be placed in a directory with the same name. The main class (more on that later) of the plugin must be defined in a Python file with the same name within that directory. The event may not use bot's main configuration file. All configuration must be placed in a separate `config.json`. +#### Mandatory/recommended files +To create a plugin, a few mandatory or recommended files must be present. The tree below shows the general structure of a plugin. A new plugin must be placed in a directory with the same name. The main class (more on that later) of the plugin must be defined in a Python file with the same name within that directory. The event may not use bot's main configuration file. All configuration must be placed in a separate `config.json`. -A subdirectory `messages` is used to store messages. Within this directory, a file `help` must be present. The content of this file must be returned with a method of the plugin class (more on that later). Furthermore, for every language a file `messages..json` with all messages should exist. +A subdirectory `messages` is used to store messages. Within this directory, a file `help` may be present. The content of this file must be returned with a method of the plugin class (more on that later). Furthermore, for every language a file `messages..json` with all messages should exist. + +The help file is not mandatory. If no help message for a plugin should appear when sending a help request to the bot, this file can be ommitted. The Plugin's `help()` method should return 0 in that case. ``` . └── plugins diff --git a/config.json.example b/config.json.example deleted file mode 100644 index 3c9526f..0000000 --- a/config.json.example +++ /dev/null @@ -1,17 +0,0 @@ -{ - "bot_credentials": { - "username": "", - "password": "", - "server": "" - }, - - "character": { - "name": "Peter", - "avatar": "images/profilepic.jpg", - "avatar_mime": "image/jpeg" - }, - - "plugins": [ - "events" - ] -} diff --git a/config.json.template b/config.json.template new file mode 100644 index 0000000..765f116 --- /dev/null +++ b/config.json.template @@ -0,0 +1,17 @@ +{ + "bot_credentials": { + "username": "${uservar}", + "password": "${passvar}", + "server": "${servervar}" + }, + + "character": { + "name": "Bot", + "avatar": "images/default_avatar.png", + "avatar_mime": "image/png" + }, + + "plugins": [ + "hello" + ] +} diff --git a/images/default_avatar.png b/images/default_avatar.png new file mode 100644 index 0000000..26916c1 Binary files /dev/null and b/images/default_avatar.png differ diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..ab1e782 --- /dev/null +++ b/install.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# +# Installation script for matrix-chatbot +# +# @author Dennis Potter +# @copyright 2019, Dennis Potter +# @license GNU General Public License (version 3) +# +# matrix-chatbot +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +################################################################################## + +# This script does the following: +# - create virtualenv +# - install requirements of main bot +# - create initial config.json +# - (optional) create systemd service + +if [[ $1 == help ]]; then + echo "Help:" + echo "install.sh : Creates a virtualenv, an initial config.json, and installs" + echo " all required packages" + echo "install.sh service: Runs ./install and additionally adds a systemd service." + echo " This command will ask you for root credentials!" + echo "install.sh help : Prints this message." + + exit 0 +fi + +# Define directory of chatbot +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +# Check if user is superuser +if [[ "$EUID" -eq 0 ]]; then + echo "ERR: Please do not run this script as super user!" + exit 1 +fi + +################################################################# +# Create function to check availability of software +################################################################# +function availability { + if [[ ! $(command -v $1) ]]; then + echo "ERR: '$1' is not available but required. Please install it!" + exit 1 + fi +} + +################################################################# +# Create virtualenv +################################################################# +# Check whether virtualenv is present +availability virtualenv + +# Create virtualenv +virtualenv $DIR + +# Enter virtualenv +source $DIR/bin/activate + +################################################################# +# Install requirements +################################################################# +# Check whether pip3 is present +availability pip3 + +# Install requirements +pip3 install -r requirements.txt + +################################################################# +# Create config.json +################################################################# +echo "" +echo "############################################################################" +echo "This step will generate a configuration file. The resulting bot will be very" +echo "simple and is only able to respond to the message "Hello bot". However, it" +echo "forms the perfect base for further development." +echo "" +echo "In the following steps, an _existing_ user must be provided. If you did not" +echo "yet create a user for your bot, please do so now!" +echo "############################################################################" +echo "" + +if [[ -f $DIR/config.json ]]; then + read -p "$DIR/config.json already exists. Should I overwrite it? [y/n]: " ow + + if [[ $ow != y ]]; then + echo "ERR: Cannot write to $DIR/config.json!" + exit 1 + fi +fi + +read -p 'Matrix homeserver: ' servervar +read -p 'Matrix username: ' uservar +read -p 'Matrix password: ' passvar + +sed -e "s/\${servervar}/${servervar}/"\ + -e "s/\${uservar}/${uservar}/"\ + -e "s/\${passvar}/${passvar}/"\ + $DIR/config.json.template > $DIR/config.json + +################################################################# +# Configure service +################################################################# +if [[ $1 != service ]]; then + exit 0 +fi + +sudo useradd -r mchatbot + +sed -e "s/\${DIR}/${DIR}/"\ + $DIR/matrix-chatbot.service.template > $DIR/matrix-chatbot.service + +sudo mv $DIR/matrix-chatbot.service /etc/systemd/system/matrix-chatbot.service + +sudo systemctl enable matrix-chatbot.service +sudo systemctl start matrix-chatbot.service diff --git a/matrix-chatbot.service.template b/matrix-chatbot.service.template new file mode 100644 index 0000000..8a54bd8 --- /dev/null +++ b/matrix-chatbot.service.template @@ -0,0 +1,14 @@ +[Unit] +Description=Matrix chatbot +After=multi-user.target + +[Service] +Type=simple +Restart=always +RestartSec=3 +User=mchatbot +WorkingDirectory=${DIR} +ExecStart=${DIR}/bin/python run.py + +[Install] +WantedBy=multi-user.target diff --git a/matrix_bot_api/matrix_bot_api.py b/matrix_bot_api/matrix_bot_api.py index 1bec33f..ef51e96 100644 --- a/matrix_bot_api/matrix_bot_api.py +++ b/matrix_bot_api/matrix_bot_api.py @@ -15,6 +15,7 @@ HELP_LOCATION = os.path.join(os.path.dirname(__file__), 'help') private_message = "Hey {}! Ik heb je even een privébericht gestuurd 🙂" def eprint(*args, **kwargs): + '''Print error messages to stderr''' print(*args, file=sys.stderr, **kwargs) class MatrixBotAPI: @@ -159,7 +160,11 @@ class MatrixBotAPI: help_text = open(HELP_LOCATION, mode="r").read() for plugin in self.plugins: - help_text += plugin.help() + try: + help_text += plugin.help() + except TypeError: + # The plugin probably returned 0, ignore it + pass self.send_message_private_public(room, event, help_text) diff --git a/plugins/admidio_events/events.py b/plugins/admidio_events/admidio_events.py similarity index 95% rename from plugins/admidio_events/events.py rename to plugins/admidio_events/admidio_events.py index 8436958..2d139be 100644 --- a/plugins/admidio_events/events.py +++ b/plugins/admidio_events/admidio_events.py @@ -2,7 +2,7 @@ __author__ = "Dennis Potter" __copyright__ = "Copyright 2019, Dennis Potter" __credits__ = ["Dennis Potter"] __license__ = "GPL-3.0" -__version__ = "1.0.1" +__version__ = "0.5.0" __maintainer__ = "Dennis Potter" __email__ = "dennis@dennispotter.eu" @@ -16,14 +16,19 @@ import MySQLdb as mysql from matrix_bot_api.mregex_handler import MRegexHandler +EVENTS_DATA_DIR = os.path.join(os.path.dirname(__file__), + '../../data/events') +DATA_LOCATION = os.path.join(os.path.dirname(__file__), + '../../data/events/data.db') CONFIG_LOCATION = os.path.join(os.path.dirname(__file__), 'config.json') HELP_LOCATION = os.path.join(os.path.dirname(__file__), 'messages/help') MESSAGES_LOCATION = os.path.join(os.path.dirname(__file__), 'messages/messages.dutch.json') class Plugin: - """ Description of event plugin """ - + """ This plugin grabs events from Admidio (https://admidio.org) + and lets users interact with the data + """ def __init__(self, bot): # Load the configuration with open(CONFIG_LOCATION) as json_data: @@ -61,7 +66,7 @@ class Plugin: try: # Try to make a new directory - os.mkdir("data/events") + os.mkdir(EVENTS_DATA_DIR) # Define query to INSERT event table to SQLite DB sql = """CREATE TABLE 'events' ( @@ -69,7 +74,7 @@ class Plugin: 'datetime_posted' datetime);""" # Open, execute, commit, and close SQLite database - sqlite_db = sqlite3.connect('data/events/data.db') + sqlite_db = sqlite3.connect(DATA_LOCATION) sqlite_cursor = sqlite_db.cursor() sqlite_cursor.execute(sql) sqlite_db.commit() @@ -120,7 +125,7 @@ class Plugin: results = mysql_cursor.fetchall() # Open SQLite database - sqlite_db = sqlite3.connect('data/events/data.db') + sqlite_db = sqlite3.connect(DATA_LOCATION) sqlite_cursor = sqlite_db.cursor() diff --git a/plugins/hello/README.md b/plugins/hello/README.md new file mode 100644 index 0000000..3a84af2 --- /dev/null +++ b/plugins/hello/README.md @@ -0,0 +1 @@ +This is an example plugin with only a single callback. When a user says "Hello bot" in a room in which te bot is present, the user replies with "Hello \!". diff --git a/plugins/hello/hello.py b/plugins/hello/hello.py new file mode 100644 index 0000000..f2f323f --- /dev/null +++ b/plugins/hello/hello.py @@ -0,0 +1,25 @@ +from matrix_bot_api.mregex_handler import MRegexHandler +import os + +HELP_LOCATION = os.path.join(os.path.dirname(__file__), 'messages/help') + +class Plugin: + """ This is an example plugin with only a single callback. When + a user says "Hello bot" in a room in which te bot is present, + the user replies with "Hello !". + """ + + def __init__(self, bot): + # Define sensitivity + self.handler = [] + + self.handler.append(MRegexHandler("Hello bot", self.info_callback)) + + # Save parent bot + self.bot = bot + + def info_callback(self, room, event): + room.send_text(f"Hello {event['sender']}!") + + def help(self): + return open(HELP_LOCATION, mode="r").read() diff --git a/plugins/hello/messages/help b/plugins/hello/messages/help new file mode 100644 index 0000000..c01c7f6 --- /dev/null +++ b/plugins/hello/messages/help @@ -0,0 +1,5 @@ +
Hello
+This is an example help message of an example plugin. This message is made up in HTML. +
    +
  • bot: greet me with "Hello bot". I will greet you back.
  • +
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c5f69d1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +matrix-client>=0.3.2 diff --git a/run.py b/run.py index 99cff11..989dc96 100755 --- a/run.py +++ b/run.py @@ -3,14 +3,27 @@ This is Peter. Peter is using the Python Matrix Bot API """ +__author__ = "Dennis Potter" +__copyright__ = "Copyright 2019, Dennis Potter" +__credits__ = ["Dennis Potter"] +__license__ = "GPL-3.0" +__version__ = "0.5.0" +__maintainer__ = "Dennis Potter" +__email__ = "dennis@dennispotter.eu" + import json +import os +import time +import threading # Bot API import from matrix_bot_api.matrix_bot_api import MatrixBotAPI +CONFIG_LOCATION = os.path.join(os.path.dirname(__file__), 'config.json') + def main(): # Load the configuration - with open('config.json') as json_data: + with open(CONFIG_LOCATION) as json_data: config = json.load(json_data) # Create an instance of the MatrixBotAPI @@ -19,11 +32,11 @@ def main(): # Start polling bot.start_polling() - # Infinitely read stdin to stall main thread while the bot runs in other - # threads. + # Stall this thread while the bot runs in other threads. On ^C, this will + # indicate to all threads that they should cancel try: - while True: - input() + forever = threading.Event(); + forever.wait() except: bot.cancel = True