Added installation files

This commit adds a convenient installation script for Linux and also
adds a description on how to install the bot.

Furthermore, a very simple example plugin is added. This way, new users
can easily discover the possibilities of this plugin.

Finally, the default character "Peter"---which is used within
Alcuinus---is removed, and a more generic character is added.
This commit is contained in:
Dennis Potter 2019-01-12 15:13:21 +01:00
parent 3a85bd368f
commit 0d32c727fa
13 changed files with 272 additions and 36 deletions

View File

@ -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). 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 ## Content
* [Installation & Requirements](#installation-requirements) * [Installation & Requirements](#installation-requirements)
* [Automatic installation (Linux)](#automatic-installation-linux)
* [Manual installation](#manual-installation)
* [Plugins](#plugins) * [Plugins](#plugins)
* [List of available plugins](#list-of-available-plugins) * [List of available plugins](#list-of-available-plugins)
* [API](#api) * [API](#api)
* [Mandatory files](#mandatory-files) * [Mandatory/recommended files](#mandatory-recommended-files)
* [Help template](#help-template) * [Help template](#help-template)
* [Plugin class template](#plugin-class-template) * [Plugin class template](#plugin-class-template)
* [Additional tips](#additional-tips) * [Additional tips](#additional-tips)
## Installation & Requirements: ## 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 ## 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. 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: ### 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 ### API
To interact with rooms, the [Matrix Python SDK](http://matrix-org.github.io/matrix-python-sdk/<Paste>) can be used. To interact with rooms, the [Matrix Python SDK](http://matrix-org.github.io/matrix-python-sdk/<Paste>) can be used.
#### Mandatory files #### Mandatory/recommended 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`. 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.<language>.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.<language>.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 └── plugins

View File

@ -1,17 +0,0 @@
{
"bot_credentials": {
"username": "",
"password": "",
"server": ""
},
"character": {
"name": "Peter",
"avatar": "images/profilepic.jpg",
"avatar_mime": "image/jpeg"
},
"plugins": [
"events"
]
}

17
config.json.template Normal file
View File

@ -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"
]
}

BIN
images/default_avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

128
install.sh Executable file
View File

@ -0,0 +1,128 @@
#!/bin/bash
#
# Installation script for matrix-chatbot
#
# @author Dennis Potter <dennis@dennispotter.eu>
# @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 <http://www.gnu.org/licenses/>.
##################################################################################
# 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

View File

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

View File

@ -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 🙂" private_message = "Hey {}! Ik heb je even een privébericht gestuurd 🙂"
def eprint(*args, **kwargs): def eprint(*args, **kwargs):
'''Print error messages to stderr'''
print(*args, file=sys.stderr, **kwargs) print(*args, file=sys.stderr, **kwargs)
class MatrixBotAPI: class MatrixBotAPI:
@ -159,7 +160,11 @@ class MatrixBotAPI:
help_text = open(HELP_LOCATION, mode="r").read() help_text = open(HELP_LOCATION, mode="r").read()
for plugin in self.plugins: for plugin in self.plugins:
try:
help_text += plugin.help() help_text += plugin.help()
except TypeError:
# The plugin probably returned 0, ignore it
pass
self.send_message_private_public(room, event, help_text) self.send_message_private_public(room, event, help_text)

View File

@ -2,7 +2,7 @@ __author__ = "Dennis Potter"
__copyright__ = "Copyright 2019, Dennis Potter" __copyright__ = "Copyright 2019, Dennis Potter"
__credits__ = ["Dennis Potter"] __credits__ = ["Dennis Potter"]
__license__ = "GPL-3.0" __license__ = "GPL-3.0"
__version__ = "1.0.1" __version__ = "0.5.0"
__maintainer__ = "Dennis Potter" __maintainer__ = "Dennis Potter"
__email__ = "dennis@dennispotter.eu" __email__ = "dennis@dennispotter.eu"
@ -16,14 +16,19 @@ import MySQLdb as mysql
from matrix_bot_api.mregex_handler import MRegexHandler 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') CONFIG_LOCATION = os.path.join(os.path.dirname(__file__), 'config.json')
HELP_LOCATION = os.path.join(os.path.dirname(__file__), 'messages/help') HELP_LOCATION = os.path.join(os.path.dirname(__file__), 'messages/help')
MESSAGES_LOCATION = os.path.join(os.path.dirname(__file__), MESSAGES_LOCATION = os.path.join(os.path.dirname(__file__),
'messages/messages.dutch.json') 'messages/messages.dutch.json')
class Plugin: 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): def __init__(self, bot):
# Load the configuration # Load the configuration
with open(CONFIG_LOCATION) as json_data: with open(CONFIG_LOCATION) as json_data:
@ -61,7 +66,7 @@ class Plugin:
try: try:
# Try to make a new directory # Try to make a new directory
os.mkdir("data/events") os.mkdir(EVENTS_DATA_DIR)
# Define query to INSERT event table to SQLite DB # Define query to INSERT event table to SQLite DB
sql = """CREATE TABLE 'events' ( sql = """CREATE TABLE 'events' (
@ -69,7 +74,7 @@ class Plugin:
'datetime_posted' datetime);""" 'datetime_posted' datetime);"""
# Open, execute, commit, and close SQLite database # 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 = sqlite_db.cursor()
sqlite_cursor.execute(sql) sqlite_cursor.execute(sql)
sqlite_db.commit() sqlite_db.commit()
@ -120,7 +125,7 @@ class Plugin:
results = mysql_cursor.fetchall() results = mysql_cursor.fetchall()
# Open SQLite database # Open SQLite database
sqlite_db = sqlite3.connect('data/events/data.db') sqlite_db = sqlite3.connect(DATA_LOCATION)
sqlite_cursor = sqlite_db.cursor() sqlite_cursor = sqlite_db.cursor()

1
plugins/hello/README.md Normal file
View File

@ -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 \<username\>!".

25
plugins/hello/hello.py Normal file
View File

@ -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 <username>!".
"""
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()

View File

@ -0,0 +1,5 @@
<h5><i>Hello</i></h5>
This is an example help message of an example plugin. This message is made up in HTML.
<ul>
<li><i>bot</i>:&emsp;greet me with "Hello bot". I will greet you back.</li>
</ul>

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
matrix-client>=0.3.2

23
run.py
View File

@ -3,14 +3,27 @@
This is Peter. Peter is using the Python Matrix Bot API 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 json
import os
import time
import threading
# Bot API import # Bot API import
from matrix_bot_api.matrix_bot_api import MatrixBotAPI from matrix_bot_api.matrix_bot_api import MatrixBotAPI
CONFIG_LOCATION = os.path.join(os.path.dirname(__file__), 'config.json')
def main(): def main():
# Load the configuration # Load the configuration
with open('config.json') as json_data: with open(CONFIG_LOCATION) as json_data:
config = json.load(json_data) config = json.load(json_data)
# Create an instance of the MatrixBotAPI # Create an instance of the MatrixBotAPI
@ -19,11 +32,11 @@ def main():
# Start polling # Start polling
bot.start_polling() bot.start_polling()
# Infinitely read stdin to stall main thread while the bot runs in other # Stall this thread while the bot runs in other threads. On ^C, this will
# threads. # indicate to all threads that they should cancel
try: try:
while True: forever = threading.Event();
input() forever.wait()
except: except:
bot.cancel = True bot.cancel = True