diff --git a/.gitignore b/.gitignore
index 6a18ad4..2d05c5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
+# Config
+config.json
+*.db
+
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
diff --git a/README.md b/README.md
index 4735f07..18b4cbd 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,12 @@
-# matrix-chatbot
+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 works with on the [Matrix network](https://matrix.org).
-An extensible Python chatbot that is compatible with the Matrix protocol
\ No newline at end of file
+
+For now, this README only contains notes.
+
+## Requirements:
+
+MySQLdb
+
+## Write about:
+* File structure of plugins and structure of the class
+* Create help function for bot and for all plugins
diff --git a/config.json.example b/config.json.example
new file mode 100644
index 0000000..3c9526f
--- /dev/null
+++ b/config.json.example
@@ -0,0 +1,17 @@
+{
+ "bot_credentials": {
+ "username": "",
+ "password": "",
+ "server": ""
+ },
+
+ "character": {
+ "name": "Peter",
+ "avatar": "images/profilepic.jpg",
+ "avatar_mime": "image/jpeg"
+ },
+
+ "plugins": [
+ "events"
+ ]
+}
diff --git a/images/profilepic.jpg b/images/profilepic.jpg
new file mode 100644
index 0000000..3a93b70
Binary files /dev/null and b/images/profilepic.jpg differ
diff --git a/matrix_bot_api/__init__.py b/matrix_bot_api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/matrix_bot_api/help b/matrix_bot_api/help
new file mode 100644
index 0000000..451d951
--- /dev/null
+++ b/matrix_bot_api/help
@@ -0,0 +1,13 @@
+
Peter's help
+Als je een zin start met Peter word ik geactiveerd!
+
+
+
+Ik heb verschillende submodules die je kunt gebruiken. Start een submodule door Peter <submodule naam> <commando>. Neem als voorbeeld de evenementen module. Om een lijst van de komende 10 evenementen te verkrijgen, type Peter evenementen lijst.
+
+
+
+
+ - help: laat dit bericht zien.
+
+
diff --git a/matrix_bot_api/matrix_bot_api.py b/matrix_bot_api/matrix_bot_api.py
new file mode 100644
index 0000000..853ad8a
--- /dev/null
+++ b/matrix_bot_api/matrix_bot_api.py
@@ -0,0 +1,197 @@
+import traceback
+import re
+import os
+import sys
+import importlib
+
+from matrix_client.client import MatrixClient
+from matrix_client.api import MatrixRequestError, MatrixHttpApi
+from matrix_client.user import User
+
+from matrix_bot_api.mregex_handler import MRegexHandler
+
+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(*args, file=sys.stderr, **kwargs)
+
+class MatrixBotAPI:
+ # rooms - List of rooms ids to operate in, or None to accept all rooms
+ def __init__(self, config, rooms=None):
+ self.username = config['bot_credentials']['username']
+
+ # Authenticate with given credentials
+ self.client = MatrixClient(config['bot_credentials']['server'])
+ try:
+ self.token = self.client.login_with_password(
+ config['bot_credentials']['username'],
+ config['bot_credentials']['password'])
+ except MatrixRequestError as e:
+ print(e)
+ if e.code == 403:
+ print("Bad username/password")
+ except Exception as e:
+ print("Invalid server URL")
+ traceback.print_exc()
+
+ self.api = MatrixHttpApi(
+ config['bot_credentials']['server'],
+ token=self.token)
+
+ # 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
+ for room_id, room in self.client.get_rooms().items():
+ 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
+
+ # Store empty list of plugins
+ self.plugins = []
+
+
+ # Try to import plugins. All plugins must be located in the
+ # ./plugins directory
+ modules = []
+
+ try:
+ for plugin in config['plugins']:
+ modules.append(
+ importlib.import_module(
+ "plugins.{0}.{0}".format(plugin), package = None))
+ except:
+ eprint("Importing one or more of the plugins did not go well!")
+ sys.exit()
+
+ # Loop through the available modules and add their handler variables
+ for i, module in enumerate(modules):
+ # Create new instance of plugin and append to plugins array
+ self.plugins.append(module.Plugin(self))
+
+ # Add handler of newly created instance to bot
+ self.add_handler(self.plugins[i].handler)
+
+ # Run setup
+ self.setup(config)
+
+ # Add callback for help function
+ self.help_handler = MRegexHandler("Peter help", self.help)
+ self.add_handler(self.help_handler)
+
+ def setup(self, config):
+ """This function only runs once, ever. It initializes an SQLite
+ database, sets the bot's avatar, and name"""
+
+ try:
+ # Try to make new directory
+ os.mkdir("data")
+
+ # Get user instance
+ self.user = self.client.get_user(self.username)
+
+ # Set username
+ self.user.set_display_name(config['character']['name'])
+
+ # Open, upload, and set avatar
+ f = open(config['character']['avatar'], mode="rb")
+
+ avatar = self.client.upload(f, config['charactar']['avatar_mime'])
+ self.user.set_avatar_url(avatar)
+
+ except FileExistsError:
+ # Do nothing, data directory already exists
+ pass
+
+ def send_message_private_public(self, room, event, message):
+ orig_room = room
+ found_room = False
+
+ for room_id, room in self.client.get_rooms().items():
+ joined_members = room.get_joined_members()
+
+ # Check for rooms with only two members
+ if len(joined_members) == 2:
+ # Check if sender is in that room
+ for member in joined_members:
+ if event['sender'] == member.user_id:
+ found_room = True
+
+ if found_room:
+ # If the flag is set, we do not need to check further rooms
+ break
+
+ # Send help message to an existing room or to a new room
+ if found_room:
+ room.send_html(message)
+ else:
+ room = self.client.create_room(invitees=[event['sender']]);
+ room.send_html(message)
+
+ if room != orig_room:
+ display_name_sender = self.api.get_display_name(event['sender'])
+ orig_room.send_text(private_message.format(display_name_sender))
+
+ 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()
+
+ for plugin in self.plugins:
+ help_text += plugin.help()
+
+ self.send_message_private_public(room, event, help_text)
+
+ def add_handler(self, handler):
+ self.handlers.append(handler)
+
+ def handle_message(self, room, event):
+ # Make sure we didn't send this message
+ if re.match("@" + self.username, event['sender']):
+ 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()
+ return self.client.sync_thread
diff --git a/matrix_bot_api/mcommand_handler.py b/matrix_bot_api/mcommand_handler.py
new file mode 100644
index 0000000..bc56789
--- /dev/null
+++ b/matrix_bot_api/mcommand_handler.py
@@ -0,0 +1,25 @@
+"""
+Defines a matrix bot handler for commands
+"""
+import re
+
+from matrix_bot_api.mhandler import MHandler
+
+
+class MCommandHandler(MHandler):
+
+ # command - String of command to handle
+ # handle_callback - Function to call if message contains command
+ # cmd_char - Character that denotes a command. '!' by default
+ def __init__(self, command, handle_callback, cmd_char='!'):
+ MHandler.__init__(self, self.test_command, handle_callback)
+ self.command = command
+ self.cmd_char = cmd_char
+
+ # Function called by Matrix bot api to determine whether or not to handle this message
+ def test_command(self, room, event):
+ # Test the message to see if it has our command
+ if event['type'] == "m.room.message":
+ if re.match(self.cmd_char + self.command, event['content']['body']):
+ return True
+ return False
diff --git a/matrix_bot_api/mhandler.py b/matrix_bot_api/mhandler.py
new file mode 100644
index 0000000..9701720
--- /dev/null
+++ b/matrix_bot_api/mhandler.py
@@ -0,0 +1,13 @@
+"""
+Defines a Matrix bot message handler
+"""
+
+
+class MHandler(object):
+ # test_callback - function that takes a room and event and returns a boolean
+ # indicating whether we should pass the message on to handle_callback
+ #
+ # handle_callback - function that takes a room and event and handles them
+ def __init__(self, test_callback, handle_callback):
+ self.test_callback = test_callback
+ self.handle_callback = handle_callback
diff --git a/matrix_bot_api/mregex_handler.py b/matrix_bot_api/mregex_handler.py
new file mode 100644
index 0000000..32aa684
--- /dev/null
+++ b/matrix_bot_api/mregex_handler.py
@@ -0,0 +1,25 @@
+"""
+Defines a matrix bot handler that uses regex to determine if message should be handled
+"""
+import re
+
+from matrix_bot_api.mhandler import MHandler
+
+
+class MRegexHandler(MHandler):
+
+ # regex_str - Regular expression to test message against
+ #
+ # handle_callback - Function to call if messages matches regex
+ def __init__(self, regex_str, handle_callback):
+ MHandler.__init__(self, self.test_regex, handle_callback)
+ self.regex_str = regex_str
+
+ def test_regex(self, room, event):
+ # Test the message and see if it matches the regex
+ if event['type'] == "m.room.message":
+ if re.search(self.regex_str, event['content']['body']):
+ # The message matches the regex, return true
+ return True
+
+ return False
diff --git a/plugins/__init__.py b/plugins/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/events/README.md b/plugins/events/README.md
new file mode 100644
index 0000000..abaf2fa
--- /dev/null
+++ b/plugins/events/README.md
@@ -0,0 +1 @@
+Insert description of event plugin.
diff --git a/plugins/events/__init__.py b/plugins/events/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/events/config.json.example b/plugins/events/config.json.example
new file mode 100644
index 0000000..942ef89
--- /dev/null
+++ b/plugins/events/config.json.example
@@ -0,0 +1,13 @@
+{
+ "database_credentials": {
+ "username": "",
+ "password": "",
+ "database": ""
+ },
+ "cat_id_room_mapping": [
+ [[], ""],
+ [[], ""]
+ ],
+ "update_time_span": 5,
+ "base_url_adm": ""
+}
diff --git a/plugins/events/events.py b/plugins/events/events.py
new file mode 100644
index 0000000..22c0340
--- /dev/null
+++ b/plugins/events/events.py
@@ -0,0 +1,170 @@
+import os
+import json
+import time
+import sqlite3
+import threading
+import datetime as dt
+import MySQLdb as mysql
+
+from matrix_bot_api.mregex_handler import MRegexHandler
+
+CONFIG_LOCATION = os.path.join(os.path.dirname(__file__), 'config.json')
+HELP_LOCATION = os.path.join(os.path.dirname(__file__), 'help')
+NEW_EVENT_LOCATION = os.path.join(os.path.dirname(__file__), 'new_event')
+
+class Plugin:
+ """ Description of event plugin """
+
+ def __init__(self, bot):
+ # Load the configuration
+ with open(CONFIG_LOCATION) as json_data:
+ self.config = json.load(json_data)
+
+ # Define sensitivity
+ self.handler = MRegexHandler("Peter evenementen", self.callback)
+
+ # Save parent bot
+ self.bot = bot
+
+ # Start thread to check events
+ self.event_thread = threading.Thread(target=self.check_new_event_thread)
+ self.event_thread.start()
+
+ self.setup()
+
+ def setup(self):
+ """This function only runs once, ever. It initializes the necessary
+ SQLite tables."""
+
+ try:
+ # Define query to INSERT event table to SQLite DB
+ sql = """CREATE TABLE 'events' (
+ 'dat_id' integer,
+ 'datetime_posted' datetime);"""
+
+ # Open, execute, commit, and close SQLite database
+ sqlite_db = sqlite3.connect('data/bot.db')
+ sqlite_cursor = sqlite_db.cursor()
+ sqlite_cursor.execute(sql)
+ sqlite_db.commit()
+ sqlite_db.close()
+
+ except sqlite3.OperationalError:
+ # Table already exists, do nothing
+ pass
+
+ def check_new_event_thread(self):
+ while not self.bot.cancel:
+ self.check_new_event()
+
+ def check_new_event(self):
+ """ Check every minutes for new events in the database.
+
+ Since the check will only be performed every minutes, the
+ connection to the database will not be kept open.
+
+ As soon as an event is posted to the defined room, its ID is
+ saved in an SQLite database.
+ """
+
+ # Connect to database
+ mysql_db = mysql.connect(
+ user = self.config['database_credentials']['username'],
+ password = self.config['database_credentials']['password'],
+ database = self.config['database_credentials']['database'])
+
+ mysql_cursor = mysql_db.cursor()
+
+ # Grab all events from database that start in the future
+ select_sql = """SELECT dat_id, dat_cat_id,
+ dat_headline, dat_begin, dat_end
+ FROM adm_dates
+ WHERE dat_begin >'{}'""".format(
+ dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
+
+ mysql_cursor.execute(select_sql)
+
+ # Fetch events
+ results = mysql_cursor.fetchall()
+
+ mysql_db.close()
+
+ # Open SQLite database
+ sqlite_db = sqlite3.connect('data/bot.db')
+
+ sqlite_cursor = sqlite_db.cursor()
+
+ # Define query to SELECT event from SQLite DB
+ select_sql = """SELECT dat_id
+ FROM events
+ WHERE dat_id = {}"""
+
+ insert_sql = """INSERT INTO events(dat_id, datetime_posted)
+ VALUES(:dat_id, :datetime_posted)"""
+
+ # Loop through event
+ for row in results:
+ # First, check if a message on this event was already sent
+ sqlite_cursor.execute(select_sql.format(row[0]))
+
+ if sqlite_cursor.fetchall():
+ # Do nothing. This event was already processed
+ pass
+ else:
+ # This appears to be a new event. Process it!
+
+ # Generate links
+ base_date_url = "{}{}".format(
+ self.config['base_url_adm'],
+ "/adm_program/modules/dates/")
+
+ view_link = "{}dates.php?id={}&view_mode=html&"
+ view_link += "view=detail&headline={}"
+ view_link = view_link.format(base_date_url, row[0], row[2])
+
+ function_link = "{}dates_function.php?mode={}&dat_id={}&"
+
+ attend_link = function_link.format(base_date_url, '3', row[0])
+ maybe_link = function_link.format(base_date_url, '7', row[0])
+ cancel_link = function_link.format(base_date_url, '4', row[0])
+
+ new_event = open(NEW_EVENT_LOCATION, mode="r").read()
+
+ # First, write to SQLite database that it is processed
+ sqlite_cursor.execute(
+ insert_sql, {
+ 'dat_id':row[0],
+ 'datetime_posted':
+ dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")})
+
+ # Check role ID of event and check if config defines
+ # where it should be shared.
+ for mapping in self.config['cat_id_room_mapping']:
+ if row[1] in mapping[0]:
+ # We got a winner!
+
+ for i, room in self.bot.client.get_rooms().items():
+ if room.display_name in mapping[1]:
+ room.send_html(
+ new_event.format(
+ row[3].strftime("%d-%m-%Y"),
+ row[3].strftime("%H:%M"),
+ row[2],
+ view_link,
+ attend_link,
+ maybe_link,
+ cancel_link))
+
+ # Commit and close queries
+ sqlite_db.commit()
+ sqlite_db.close()
+
+ # Sleep
+ #time.sleep(self.config['update_time_span'] * 60)
+ time.sleep(1)
+
+ def callback(self, room, event):
+ room.send_text("Information ")
+
+ def help(self):
+ return open(HELP_LOCATION, mode="r").read()
diff --git a/plugins/events/help b/plugins/events/help
new file mode 100644
index 0000000..4ab9dd6
--- /dev/null
+++ b/plugins/events/help
@@ -0,0 +1,7 @@
+evenementen
+
+ - lijst: creëer een lijst met de komende 10 evenementen. Als meer of minder evenementen nodig zijn, definieer het getal als lijst <natuurlijk getal>. Dit commando laat ook het identificatienummer van evenementen zien.
+ - info <ID>: laat informatie zien over het evenement met het identificatienummer <ID>. Titel, tijd, locatie, beschrijving en aan- en afmeldlinks worden getoond.
+ - chat <ID>: maak een chatgroep aan voor alle leden die op dit moment op aanwezig of misschien staan in een evenement met het identificatienummer <ID>.
+ - deelnemers <ID>: dit laat alle deelnemers zien die op dit moment op aanwezig of misschien staan voor een bepaald evenement met het identificatienummer <ID>.
+
diff --git a/plugins/events/new_event b/plugins/events/new_event
new file mode 100644
index 0000000..10918a0
--- /dev/null
+++ b/plugins/events/new_event
@@ -0,0 +1,11 @@
+@room er is zojuist een nieuw evenement aangemaakt! Op {} om {} zal "{}" plaatsvinden.
+
+
+
+Ben je aangemeld op de leden database in je favoriete browser? Klik dan hier:
+
diff --git a/run.py b/run.py
new file mode 100755
index 0000000..99cff11
--- /dev/null
+++ b/run.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+"""
+This is Peter. Peter is using the Python Matrix Bot API
+"""
+
+import json
+
+# Bot API import
+from matrix_bot_api.matrix_bot_api import MatrixBotAPI
+
+def main():
+ # Load the configuration
+ with open('config.json') as json_data:
+ config = json.load(json_data)
+
+ # Create an instance of the MatrixBotAPI
+ bot = MatrixBotAPI(config)
+
+ # Start polling
+ bot.start_polling()
+
+ # Infinitely read stdin to stall main thread while the bot runs in other
+ # threads.
+ try:
+ while True:
+ input()
+ except:
+ bot.cancel = True
+
+if __name__ == "__main__":
+ main()