├── .gitignore ├── .gitmodules ├── README.md ├── alembic.ini ├── bridge.py ├── config.json.dist ├── data └── .gitkeep ├── lib ├── commands │ └── commands.py ├── common_libs │ └── msgqueue.py ├── database_mappings │ ├── rooms.py │ └── roomstypes.py ├── flaskviews │ ├── shutdown.py │ └── transactions.py ├── protocols │ ├── matrix.py │ ├── xmpp.py │ └── xmpp_conn │ │ └── connection.py └── rooms │ └── rooms.py ├── migrations ├── README ├── env.py ├── script.py.mako └── versions │ ├── 7d6a7a0c6c1d_add_rooms_types_table.py │ ├── 960957b4f2ec_add_commander_to_rooms.py │ └── de731ab04076_add_rooms_table.py ├── plugins ├── bridge │ └── bridge.py └── help │ └── help.py ├── registration.yaml.example ├── requirements.txt └── schema.sql /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *swp 3 | registration.yaml 4 | .python-version 5 | mxbridge.conf 6 | registration.yaml 7 | data/* 8 | config.json 9 | logs/* 10 | config/config.json 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/regius"] 2 | path = vendor/regius 3 | url = https://dev.pztrn.name/regius/regius.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Development stopped due to lack of interest in XMPP for me.** 2 | 3 | # Matrix-XMPP Bridge 4 | This project creates a bridge between a Matrix room and an XMPP MUC. It is currently very early in development. 5 | 6 | Originally forked from https://github.com/jfrederickson/matrix-xmpp-bridge it was heavily refactored and adopted for Python 3. Missing functionality was added (like be a bidirectional, users aliases in XMPP and Matrix, etc). 7 | 8 | **WARNING**: this bridge isn't a "bot like" one, it REQUIRES that you have a possibility to add AS registration file to synapse's configuration file! 9 | It is still possible to bridge XMPP MUC on other's homeserver room. Just join it and use Room ID from settings! 10 | 11 | ## Dependencies 12 | - python3 13 | - sleekxmpp 14 | - requests 15 | - flask 16 | - flask-classy 17 | 18 | ... or just ``pip install -r requirements.txt`` :) 19 | 20 | ## Installation 21 | 22 | Make sure you have Python 3 installed. I recommend [PyENV](https://github.com/yyuu/pyenv) for managing python versions and do not rely on system one. This guide will assume that you're installed Python 3 with PyENV. 23 | 24 | After that clone this repo, go to source's root directory and execute: 25 | 26 | ``` 27 | pyenv local ${VERSION} 28 | ``` 29 | 30 | Where `${VERSION}` is a Python version you installed previously. 31 | 32 | After setting local python's version install dependencies: 33 | 34 | ``` 35 | pip install -r requirements.txt 36 | ``` 37 | 38 | This could take some time. 39 | 40 | After all don't forget to update submodules: 41 | 42 | ``` 43 | git submodule init && git submodule update 44 | ``` 45 | 46 | ## Configuration 47 | 48 | - Add an AS and HS token to registration.yaml and reference it in your homeserver config as described [here](http://matrix.org/blog/2015/03/02/introduction-to-application-services/) 49 | - Edit mxbridge.conf.example with user and room details for the Matrix/XMPP rooms you would like to bridge and save as mxbridge.conf in bot's source directory 50 | - Start bridge.py 51 | 52 | ### Custom configuration file 53 | 54 | There is a possibility to specify configuration file name while launching bridge. It is useful to run several independent instances using one code base. Just pass configuration file name after "bridge.py", like this: 55 | 56 | ``` 57 | ./bridge.py my_custom_mxbridge.conf 58 | ``` 59 | 60 | ### Synapse 61 | 62 | Due to some (temporary) things, it is better to set `rc_messages_per_second` to 5, otherwise bridge could eat messages when it detects new XMPP user to map to Matrix. 63 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = migrations 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # max length of characters to apply to the 11 | # "slug" field 12 | #truncate_slug_length = 40 13 | 14 | # set to 'true' to run the environment during 15 | # the 'revision' command, regardless of autogenerate 16 | # revision_environment = false 17 | 18 | # set to 'true' to allow .pyc and .pyo files without 19 | # a source .py file to be detected as revisions in the 20 | # versions/ directory 21 | # sourceless = false 22 | 23 | # version location specification; this defaults 24 | # to migrations/versions. When using multiple version 25 | # directories, initial revisions must be specified with --version-path 26 | # version_locations = %(here)s/bar %(here)s/bat migrations/versions 27 | 28 | # the output encoding used when revision files 29 | # are written from script.py.mako 30 | # output_encoding = utf-8 31 | 32 | sqlalchemy.url = driver://user:pass@localhost/dbname 33 | 34 | 35 | # Logging configuration 36 | [loggers] 37 | keys = root,sqlalchemy,alembic 38 | 39 | [handlers] 40 | keys = console 41 | 42 | [formatters] 43 | keys = generic 44 | 45 | [logger_root] 46 | level = WARN 47 | handlers = console 48 | qualname = 49 | 50 | [logger_sqlalchemy] 51 | level = WARN 52 | handlers = 53 | qualname = sqlalchemy.engine 54 | 55 | [logger_alembic] 56 | level = INFO 57 | handlers = 58 | qualname = alembic 59 | 60 | [handler_console] 61 | class = StreamHandler 62 | args = (sys.stderr,) 63 | level = NOTSET 64 | formatter = generic 65 | 66 | [formatter_generic] 67 | format = %(levelname)-5.5s [%(name)s] %(message)s 68 | datefmt = %H:%M:%S 69 | -------------------------------------------------------------------------------- /bridge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import os 5 | from sqlalchemy.orm import exc 6 | import sys 7 | import time 8 | import threading 9 | 10 | # Load Regius. 11 | system_regius = 0 12 | try: 13 | from regius.regius import Regius 14 | system_regius = 1 15 | except: 16 | pass 17 | 18 | if not system_regius: 19 | if os.path.exists("config.json"): 20 | preseed = json.loads(open("config.json", "r").read()) 21 | if "paths" in preseed and "regius" in preseed["paths"]: 22 | print("Using regius from {0}".format(preseed["paths"]["regius"].replace("CURPATH", sys.path[0]))) 23 | sys.path.insert(1, preseed["paths"]["regius"].replace("CURPATH", sys.path[0])) 24 | print(sys.path) 25 | 26 | try: 27 | import regius 28 | except ImportError: 29 | print("Failed to import Regius!") 30 | print("Paths:") 31 | print(sys.path[0], sys.path[1]) 32 | 33 | class Bridge: 34 | """ 35 | Main class for bridge. 36 | """ 37 | def __init__(self, regius_instance): 38 | # App service for communication with Matrix. 39 | self.__appservice = None 40 | # Commands. 41 | self.__commands = None 42 | # Configuration. 43 | self.__config = None 44 | # Database. 45 | self.__database = None 46 | # Message queue. 47 | self.__queue = None 48 | # XMPP's connections manager. 49 | self.__xmpp = None 50 | 51 | # Framework initialization. 52 | self.__regius = regius_instance 53 | self.loader = self.__regius.get_loader() 54 | self.config = self.loader.request_library("common_libs", "config") 55 | 56 | self.log = self.loader.request_library("common_libs", "logger").log 57 | self.log(0, "Framework initialization complete.") 58 | 59 | def go(self): 60 | self.__queue = self.loader.request_library("common_libs", "msgqueue") 61 | self.initialize_database() 62 | self.initialize_rooms() 63 | self.load_plugins() 64 | self.initialize_commands() 65 | self.launch_webservice() 66 | self.launch_xmpp_connection() 67 | 68 | while True: 69 | try: 70 | if self.__queue.is_empty(): 71 | time.sleep(1) 72 | else: 73 | print("Items in queue: {0}".format(str(self.__queue.items_in_queue()))) 74 | print("Processing item from queue...") 75 | item = self.__queue.get_message() 76 | if item["from_component"] == "xmpp": 77 | # Messages from XMPP. 78 | self.__appservice.process_message(item) 79 | if item["from_component"] == "appservice": 80 | if item["type"] == "invite": 81 | # Process room invite. 82 | self.__appservice.process_invite(item) 83 | elif item["type"] == "command_room_message": 84 | # Invites and command room's messages should be 85 | # processed by appservice. 86 | self.__appservice.process_message(item) 87 | #else: 88 | # Messages to XMPP. 89 | # self.__xmpp.send_message(item["from"], item["to"], item["body"]) 90 | 91 | time.sleep(1) 92 | except KeyboardInterrupt: 93 | self.log(0, "Keyboard interrupt!") 94 | 95 | def initialize_commands(self): 96 | """ 97 | Initializes command room's commands. 98 | """ 99 | self.__commands = self.loader.request_library("commands", "commands") 100 | 101 | def initialize_database(self): 102 | """ 103 | Initializes database connection, as well as loads all neccessary 104 | data into RAM. 105 | """ 106 | self.database = self.loader.request_library("common_libs", "database") 107 | self.database.create_connection("production") 108 | self.database.load_mappings() 109 | 110 | self.migrator = self.loader.request_library("database_tools", "migrator") 111 | self.migrator.migrate() 112 | 113 | def initialize_rooms(self): 114 | """ 115 | Initialize rooms handler. 116 | """ 117 | self.rooms = self.loader.request_library("rooms", "rooms") 118 | 119 | def launch_webservice(self): 120 | self.log(0, "Launching Matrix protocol listener...") 121 | 122 | self.__appservice = self.loader.request_library("protocols", "matrix") 123 | 124 | as_view = self.loader.request_library("flaskviews", "transactions") 125 | self.__appservice.register_app(as_view, "/transactions/", "transactions") 126 | 127 | shutdown_view = self.loader.request_library("flaskviews", "shutdown") 128 | self.__appservice.register_app(shutdown_view, "/shutdown/", "shutdown") 129 | 130 | self.__appservice.start() 131 | 132 | def launch_xmpp_connection(self): 133 | self.__xmpp = self.loader.request_library("protocols", "xmpp") 134 | self.__xmpp.start() 135 | 136 | def load_plugins(self): 137 | """ 138 | """ 139 | self.log(0, "Loading plugins...") 140 | for plugin in os.listdir(os.path.join(self.config.get_temp_value("SCRIPT_PATH"), "plugins")): 141 | self.log(1, "Loading plugin '{CYAN}{plugin_name}{RESET}'...", {"plugin_name": plugin}) 142 | self.loader.request_plugin(plugin) 143 | 144 | def read_config(self): 145 | print("Initializing configuration...") 146 | self.__config = Config() 147 | 148 | # Bridge path. 149 | PATH = sys.modules["config.config"].__file__ 150 | PATH = os.path.sep.join(PATH.split(os.path.sep)[:-2]) 151 | self.__config.set_temp_value("BRIDGE_PATH", PATH) 152 | 153 | self.__config.parse_config() 154 | 155 | if __name__ == "__main__": 156 | print("Starting Matrix-XMPP bridge...") 157 | app_path = os.path.abspath(__file__).split(os.path.sep)[:-1] 158 | app_path = os.path.sep.join(app_path) 159 | sys.path.insert(0, app_path) 160 | r = regius.init(preseed, app_path) 161 | b = Bridge(r) 162 | exit(b.go()) 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /config.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "preseed": { 3 | "app_name": "matrix-xmpp-bridge", 4 | "version": "0.0.1", 5 | "url": "http://github.com/pztrn/matrix-xmpp-bridge", 6 | "auth": 0, 7 | "ui": "cli" 8 | }, 9 | "paths": { 10 | "regius": "CURPATH/vendor/regius", 11 | "config": "config" 12 | }, 13 | "logger": { 14 | "runtime_logs_access": 0, 15 | "default_debug_level": 2 16 | }, 17 | "eventer": { 18 | "suppress_fire_messages": 0 19 | }, 20 | "matrix": { 21 | "api_url": "http://localhost:8008/_matrix/client/r0", 22 | "token": "supersekret", 23 | "users_prefix": "xmpp", 24 | "domain": "localhost", 25 | "room_id": "!asdfkjaskdjas:localhost", 26 | "listen_address": "0.0.0.0", 27 | "listen_port": "5000" 28 | }, 29 | "xmpp": { 30 | "muc_room": "name@conference.domain.tld", 31 | "nick": "MatrixBridge", 32 | "username": "user@domain.tld", 33 | "password": "xmpppass", 34 | "server_address": "xmpp.domain.tld" 35 | }, 36 | "database": { 37 | "production_type": "postgresql+psycopg2", 38 | "production_host": "localhost", 39 | "production_user": "mxbridge", 40 | "production_dbname": "mxbridge", 41 | "production_pass": "mxbridge" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pztrn/matrix-xmpp-bridge/335f576f202169e537c01f1f745c3cf7c6f5d053/data/.gitkeep -------------------------------------------------------------------------------- /lib/commands/commands.py: -------------------------------------------------------------------------------- 1 | from lib.common_libs.library import Library 2 | 3 | class Commands(Library): 4 | 5 | _info = { 6 | "name" : "Commands handling library", 7 | "shortname" : "commands_library", 8 | "description" : "Library responsible for handling commands." 9 | } 10 | 11 | def __init__(self): 12 | Library.__init__(self) 13 | self.__commands = {} 14 | 15 | def get_commands(self): 16 | """ 17 | Returns self.__commands dict. Used by help plugin. 18 | """ 19 | return self.__commands 20 | 21 | def init_library(self): 22 | """ 23 | """ 24 | self.log(0, "Initializing commands library...") 25 | 26 | # Get all plugins. 27 | plugins = self.loader.get_loaded_plugins() 28 | 29 | for plugin in plugins: 30 | cmds = plugins[plugin].get_commands() 31 | for cmd in cmds: 32 | if not cmd in self.__commands: 33 | self.log(2, "Adding command {YELLOW}{cmd}{RESET}...", {"cmd": cmd}) 34 | self.__commands[cmd] = cmds[cmd] 35 | self.__commands[cmd]["plugin_name"] = cmd 36 | else: 37 | self.log(0, "{RED}ERROR:{RESET} command {YELLOW}{cmd}{RESET} already exists for plugin {BLUE}{plugin_name}{RESET}", {"cmd": cmd, "plugin_name": plugin}) 38 | 39 | self.log(0, "We're accepting {CYAN}{commands_count}{RESET} commands", {"commands_count": len(self.__commands)}) 40 | 41 | def process_command(self, command, message): 42 | """ 43 | Processes command. 44 | """ 45 | self.log(0, "Processing command {YELLOW}{command}{RESET} with data '{CYAN}{data}{RESET}'", {"command": command, "data": message}) 46 | data = "" 47 | for cmd in self.__commands: 48 | if command in self.__commands[cmd]["keywords"]: 49 | data = self.__commands[cmd]["method"](message) 50 | return True, data 51 | 52 | return False, data 53 | -------------------------------------------------------------------------------- /lib/common_libs/msgqueue.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from lib.common_libs.library import Library 4 | 5 | class Msgqueue(Library): 6 | 7 | _info = { 8 | "name" : "Messages queue library", 9 | "shortname" : "msgqueue_library", 10 | "description" : "Library responsible for handling messages queue." 11 | } 12 | 13 | def __init__(self): 14 | Library.__init__(self) 15 | self.__queue = [] 16 | self.__lock = threading.Lock() 17 | 18 | def add_message(self, message): 19 | self.__lock.acquire(blocking = True) 20 | self.__queue.append(message) 21 | self.__lock.release() 22 | 23 | def get_message(self): 24 | self.__lock.acquire(blocking = True) 25 | msg = self.__queue.pop(0) 26 | self.__lock.release() 27 | return msg 28 | 29 | def init_library(self): 30 | """ 31 | """ 32 | pass 33 | 34 | def is_empty(self): 35 | return len(self.__queue) == 0 36 | 37 | def items_in_queue(self): 38 | return len(self.__queue) 39 | -------------------------------------------------------------------------------- /lib/database_mappings/rooms.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, VARCHAR 2 | 3 | from lib.common_libs import common 4 | DBMap = common.TEMP_SETTINGS["DBMap"] 5 | 6 | class Rooms(DBMap): 7 | __tablename__ = "rooms" 8 | 9 | room_id = Column("room_id", String, primary_key = True, unique = True) 10 | room_type = Column("room_type", Integer, nullable = False) 11 | commander = Column("commander", String, nullable = False) 12 | -------------------------------------------------------------------------------- /lib/database_mappings/roomstypes.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, VARCHAR 2 | 3 | from lib.common_libs import common 4 | DBMap = common.TEMP_SETTINGS["DBMap"] 5 | 6 | class Roomstypes(DBMap): 7 | __tablename__ = "rooms_types" 8 | 9 | id = Column("id", Integer, primary_key = True, unique = True, autoincrement = True) 10 | name = Column("name", String(255), nullable = False) 11 | description = Column("description", String(255), nullable = False) 12 | -------------------------------------------------------------------------------- /lib/flaskviews/shutdown.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from flask import request 3 | from flask.views import MethodView 4 | 5 | from lib.common_libs.library import Library 6 | 7 | class Shutdown(MethodView, Library): 8 | 9 | _info = { 10 | "name" : "Flash shutdown handler library", 11 | "shortname" : "flash_shutdown_library", 12 | "description" : "Library responsible for handling Flask shutdown requests." 13 | } 14 | 15 | def __init__(self, loader = None): 16 | Library.__init__(self) 17 | MethodView.__init__(self) 18 | 19 | self.loader = loader 20 | self.log = None 21 | 22 | def get(self): 23 | if not self.log: 24 | self.log = self.loader.request_library("common_libs", "logger").log 25 | 26 | func = request.environ.get('werkzeug.server.shutdown') 27 | func() 28 | 29 | self.log(0, "Werkzeug web server shutdown call issued.") 30 | 31 | return jsonify({}) 32 | -------------------------------------------------------------------------------- /lib/flaskviews/transactions.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from flask import request 3 | from flask.views import MethodView 4 | 5 | from lib.common_libs.library import Library 6 | 7 | class Transactions(MethodView, Library): 8 | 9 | _info = { 10 | "name" : "Incoming transactions handler library", 11 | "shortname" : "matrix_transactions_library", 12 | "description" : "Library responsible for handling incoming Matrix transactions." 13 | } 14 | 15 | def __init__(self, loader = None): 16 | Library.__init__(self) 17 | MethodView.__init__(self) 18 | 19 | self.__loader = loader 20 | self.__bridge_username = None 21 | self.log = None 22 | self.__queue = None 23 | self.__rooms = None 24 | self.__config = None 25 | self.__rooms_types = None 26 | self.__rooms = None 27 | self.config = None 28 | self.__bridge_username = None 29 | 30 | def put(self, txid): 31 | if not self.__config: 32 | self.__config = self.__loader.request_library("common_libs", "config") 33 | 34 | if not self.__rooms_types: 35 | self.__rooms_types = self.__config.get_temp_value("/rooms/types") 36 | 37 | if not self.__rooms: 38 | self.__rooms = self.__config.get_temp_value("/rooms/rooms") 39 | 40 | if not self.config: 41 | self.config = self.__config.get_temp_value("app_config") 42 | 43 | if not self.__queue: 44 | self.__queue = self.__loader.request_library("common_libs", "msgqueue") 45 | 46 | if not self.log: 47 | self.log = self.__loader.request_library("common_libs", "logger").log 48 | 49 | if not self.__bridge_username: 50 | self.__bridge_username = self.__config.get_temp_value("/matrix/bridge_username") 51 | 52 | if not self.__rooms: 53 | self.__rooms = self.__loader.request_library("rooms", "rooms") 54 | 55 | events = request.get_json()["events"] 56 | for event in events: 57 | self.log(2, "Event: {event}", {"event": event}) 58 | 59 | # Check room type, if any. 60 | # None if room wasn't found. 61 | room_type = self.__rooms.check_room_type(event["room_id"]) 62 | self.log(2, "Detected room type: {type}", {"type": room_type}) 63 | 64 | # Room message. 65 | if event['type'] == 'm.room.message' and event["age"] < 10000 and room_type: 66 | # If we have message's body. 67 | if "content" in event and "body" in event["content"]: 68 | # If we have this message in bridged room. 69 | if room_type == "Bridged room" and not self.config["matrix"]["users_prefix"] in event["user_id"]: 70 | data = {"type": "bridged_room_message", "from_component": "appservice", "from": event["user_id"], "from_room": event["room_id"]} 71 | if event["content"]["msgtype"] in ["m.image", "m.file", "m.video"]: 72 | # Craft image URL. 73 | domain = event["content"]["url"].split("/")[2] 74 | media_id = event["content"]["url"].split("/")[3] 75 | body = "http://" + domain + "/_matrix/media/r0/download/" + domain + "/" + media_id 76 | data["body"] = body 77 | elif event["content"]["msgtype"] == "m.emote": 78 | data["body"] = "/me" + event["content"]["body"] 79 | else: 80 | data["body"] = event["content"]["body"] 81 | self.log(2, "Adding message to queue: {msg}", {"msg": data}) 82 | self.__queue.add_message(data) 83 | 84 | self.log(1, "User: {user}, Room: {room}", {"user": event["user_id"], "room": event["room_id"]}) 85 | self.log(1, "Event Type: {event_type}", {"event_type": event["type"]}) 86 | self.log(1, "Content: {content}", {"content": event["content"]}) 87 | 88 | # If we have command room message. 89 | if room_type == "Command room" and not event["user_id"] == "@" + self.__bridge_username: 90 | self.log(0, "Received command room message from {user}", {"user": event["user_id"]}) 91 | data = {"type": "command_room_message", "from_component": "appservice", "from": event["user_id"], "room_id": event["room_id"], "body": event["content"]["body"], "msgtype": event["content"]["msgtype"]} 92 | self.log(2, "Adding message to queue: {msg}", {"msg": data}) 93 | self.__queue.add_message(data) 94 | 95 | # Inviting. 96 | if event["type"] == "m.room.member" and "content" in event and event["content"]["membership"] == "invite": 97 | self.log(0, "Received invite into: {room} from: {user}", {"room": event["room_id"], "user": event["user_id"]}) 98 | data = {"type": "invite", "from_component": "appservice", "user_id": event["user_id"], "room_id": event["room_id"]} 99 | self.log(1, "Adding message to queue: {msg}", {"msg": data}) 100 | self.__queue.add_message(data) 101 | 102 | return jsonify({}) 103 | -------------------------------------------------------------------------------- /lib/protocols/matrix.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | import json 3 | import markdown2 4 | import os 5 | import random 6 | import requests 7 | import string 8 | import threading 9 | import time 10 | 11 | from lib.common_libs.library import Library 12 | 13 | CONFIG = None 14 | QUEUE = None 15 | BRIDGE_USERNAME = None 16 | COMMAND_ROOMS = [] 17 | BRIDGED_ROOMS = [] 18 | 19 | class Matrix(threading.Thread, Library): 20 | 21 | _info = { 22 | "name" : "Matrix connection library", 23 | "shortname" : "matrix_library", 24 | "description" : "Library responsible for handling connection to Matrix homeserver." 25 | } 26 | 27 | def __init__(self): 28 | Library.__init__(self) 29 | threading.Thread.__init__(self) 30 | 31 | # Flask App. 32 | self.__app = Flask("matrix-xmpp-bridge") 33 | 34 | # Joined rooms list. 35 | self.__joined = {} 36 | 37 | # Markdown formatter. 38 | self.__markdowner = None 39 | 40 | # Shutdown marker. 41 | self.__shutdown = False 42 | 43 | 44 | def command(self, message): 45 | """ 46 | """ 47 | self.log(0, "Processing command room message...") 48 | command = message["body"].split(" ")[0] 49 | body = " ".join(message["body"].split(" ")[1:]) 50 | status, data = self.__commands.process_command(command, body) 51 | if not status: 52 | msg = "Unknown command. Type 'help' to get list of available commands." 53 | else: 54 | msg = data 55 | 56 | self.log(2, "Reply: {reply}", {"reply": msg}) 57 | 58 | # Try to format Markdown message. 59 | msg_markdown = self.__markdowner.convert(msg) 60 | if msg_markdown != msg: 61 | # [3:-4] to get rid of

. 62 | rdata = self.__send_message(message["room_id"], "@" + self.__bridge_username, "m.text", msg, msg_markdown[:-1][3:-4]) 63 | else: 64 | rdata = self.__send_message(message["room_id"], "@" + self.__bridge_username, "m.text", msg) 65 | 66 | self.log(2, rdata) 67 | 68 | def init_library(self): 69 | """ 70 | """ 71 | self.log(0, "Initializing Matrix protocol handler...") 72 | self.__rooms = self.loader.request_library("rooms", "rooms") 73 | self.__commands = self.loader.request_library("commands", "commands") 74 | 75 | self.__matrix_cfg = self.config.get_temp_value("app_config")["matrix"] 76 | 77 | # API URL to which messages will be pushed. 78 | self.__msg_api_url = self.__matrix_cfg["api_url"] + "/rooms/{0}/send/m.room.message" 79 | # Authentication parameters. 80 | self.__params = { 81 | "access_token": self.__matrix_cfg["token"] 82 | } 83 | # Bridge's username 84 | self.__bridge_username = "{0}_bridge:{1}".format(self.__matrix_cfg["users_prefix"], self.__matrix_cfg["domain"]) 85 | self.config.set_temp_value("/matrix/bridge_username", self.__bridge_username) 86 | 87 | # Markdown. 88 | self.__markdowner = markdown2.Markdown() 89 | 90 | def join_room(self, room_id, full_username): 91 | """ 92 | Joins to Matrix room. 93 | WARNING: full_username should start with "@"! 94 | """ 95 | if len(full_username) < 4: 96 | self.log(0, "{RED}Error:{RESET} User ID not specified!") 97 | return 98 | 99 | if len(room_id) < 4: 100 | self.log(0, "{RED}Error:{RESET} Room ID not specified!") 101 | return 102 | 103 | self.log(1, "Trying to join {room_id} as {user}...", {"room_id": room_id, "user": full_username}) 104 | 105 | # Join room. 106 | if not room_id in self.__joined: 107 | self.__joined[room_id] = {} 108 | 109 | if not full_username in self.__joined[room_id]: 110 | data = { 111 | "user_id": full_username, 112 | "access_token": self.__params["access_token"] 113 | } 114 | r = requests.post(self.__matrix_cfg["api_url"] + '/join/' + room_id, params = data, data = json.dumps(data)) 115 | self.log(2, "Join request output from Matrix: {output}", {"output": r.json()}) 116 | else: 117 | self.log(1, "User already in room, will not re-join.") 118 | 119 | self.__update_users_list_in_room(room_id) 120 | 121 | def on_shutdown(self): 122 | """ 123 | Executes on bridge shutdown. 124 | """ 125 | # Execute HTTP GET request to Flask to shut it down. 126 | # Compose URL first. 127 | url = "http://{0}:{1}/shutdown/".format(self.__matrix_cfg["listen_address"], self.__matrix_cfg["listen_port"]) 128 | requests.get(url) 129 | 130 | self.__shutdown = True 131 | 132 | def process_invite(self, message): 133 | """ 134 | Process invite into room. 135 | """ 136 | self.log(0, "Processing invite into Matrix room...") 137 | # All invites should be done only to command room. So we should: 138 | # * Join. 139 | # * Check for users count in room. 140 | # * If users count == 2: say "hello", otherwise send error to 141 | # chat and leave room. 142 | # Join room. 143 | self.join_room(message["room_id"], "@" + self.__bridge_username) 144 | # Check how much users we have in chat room. Leave if > 2. 145 | if len(self.__joined[message["room_id"]]) > 2: 146 | self.log(0, "{YELLOW}Warning:{RESET} more than 2 users in room, including me. Leaving...") 147 | self.__send_message(message["room_id"], "@" + self.__bridge_username, "m.text", "Can't use this room as command room. Please, invite me to a room where we will be alone!") 148 | self.__leave_room(message["room_id"], "@" + self.__bridge_username) 149 | return 150 | self.__send_message(message["room_id"], "@" + self.__bridge_username, "m.text", "Hello there! Type 'help' to get help :)") 151 | self.__rooms.add_command_room(message["room_id"], message["user_id"]) 152 | 153 | def process_message(self, message): 154 | """ 155 | Processes passed message. 156 | """ 157 | if message["type"] == "command_room_message": 158 | # Here we do a check if room ID in command rooms list. This 159 | # is needed because we might do something not so good and 160 | # it will disappear from commands rooms list. And this 161 | # check if required for re-adding room to command rooms 162 | # list. 163 | room_type = self.__rooms.check_room_type(message["room_id"]) 164 | # For now if room ID is unknown - Command room type will 165 | # be forced. 166 | if room_type == "Command room": 167 | self.__rooms.add_command_room(message["room_id"], message["from"]) 168 | self.command(message) 169 | elif message["type"] == "invite": 170 | self.join_room(message["room_id"], "@" + self.__bridge_username) 171 | elif message["type"] == "message_to_room": 172 | self.send_message_to_matrix(message) 173 | 174 | def register_app(self, instance, base, view_name): 175 | """ 176 | Registers Flask view with Flask app. 177 | """ 178 | #instance.register(self.__app, route_base = base, trailing_slash = False) 179 | self.__app.add_url_rule(base, view_func = instance.as_view(view_name, loader = self.loader)) 180 | 181 | def run(self): 182 | """ 183 | Run App Service thread. 184 | """ 185 | # Try to register our master user. 186 | self.__register_user("@" + self.__bridge_username, self.__bridge_username.split(":")[0], "XMPP bridge") 187 | #self.join_room(self.__matrix_cfg["room_id"], "@" + self.__bridge_username) 188 | self.__app.config['TRAP_BAD_REQUEST_ERRORS'] = True 189 | self.__app.run(host = self.__matrix_cfg["listen_address"], port = int(self.__matrix_cfg["listen_port"])) 190 | 191 | return 192 | 193 | def send_message_to_matrix(self, queue_item): 194 | """ 195 | Tries to send message to Matrix. 196 | """ 197 | self.log(0, "Sending message to Matrix...") 198 | # Splitting "@Matrix" from user highlight, if any. 199 | if "@Matrix" in queue_item["body"]: 200 | idx = 0 201 | body_s = queue_item["body"].split(" ") 202 | for item in body_s: 203 | if "@Matrix" in item: 204 | idx = body_s.index(item) 205 | 206 | body_s[idx] = "@" + body_s[idx].split("@")[0] 207 | body = " ".join(body_s) 208 | else: 209 | body = queue_item["body"] 210 | 211 | # Get username for Matrix. 212 | full_username, username, nickname = self.__compose_matrix_username(queue_item["conference"], queue_item["from"]) 213 | self.log(2, "Full username: {full_username}, username: {username}, nickname: {nickname}", {"full_username": full_username, "username": username, "nickname": nickname}) 214 | 215 | # Check if we have this user in room. And then if user already 216 | # registered. And register user if not. Same for joining room. 217 | if not full_username[1:] in self.__joined[queue_item["room_id"]]: 218 | self.__register_user(full_username, username, nickname) 219 | 220 | # Join room. 221 | self.join_room(queue_item["room_id"], full_username) 222 | 223 | data = self.__send_message(queue_item["room_id"], full_username, "m.text", body) 224 | 225 | if "error" in data and data["errcode"] == "M_FORBIDDEN" and "not in room" in data["error"]: 226 | self.log(0, "{RED}Error:{RESET} user {user} not in room, joining and re-sending message...", {"user": full_username}) 227 | self.join_room(queue_item["room_id"], full_username) 228 | 229 | self.__send_message(queue_item["room_id"], full_username, "m.text", body) 230 | 231 | def __compose_matrix_username(self, conference, username): 232 | """ 233 | This method composes a username for Matrix, which will be used 234 | for pseudouser. 235 | """ 236 | # Check if passed nickname is in ASCII range. 237 | is_ascii = True 238 | try: 239 | username.encode("ascii") 240 | except UnicodeEncodeError: 241 | is_ascii = False 242 | 243 | if is_ascii: 244 | matrix_username = "{0}_{1}_{2}".format(self.__matrix_cfg["users_prefix"], 245 | username, conference) 246 | matrix_username_full = "@{0}:{1}".format(matrix_username, self.__matrix_cfg["domain"]) 247 | else: 248 | matrix_username = "{0}_{1}_{2}".format(self.__matrix_cfg["users_prefix"], 249 | username.encode("punycode").decode("utf-8"), conference) 250 | matrix_username = "@{0}_{1}_{2}:{3}".format(self.__matrix_cfg["users_prefix"], 251 | username.encode("punycode").decode("utf-8"), conference, self.__matrix_cfg["domain"]) 252 | 253 | matrix_nickname = "{0} (XMPP MUC)".format(username) 254 | return matrix_username_full, matrix_username, matrix_nickname 255 | 256 | def __leave_room(self, room_id, full_username): 257 | """ 258 | Leave specified room. 259 | """ 260 | self.log(0, "User {YELLOW}{user}{RESET} leaving room {CYAN}{room_id}{RESET}", {"user": full_username, "room_id": room_id}) 261 | data = { 262 | "user_id": full_username, 263 | "access_token": self.__params["access_token"] 264 | } 265 | leave_url = self.__matrix_cfg["api_url"] + "/rooms/{0}/leave".format(room_id) 266 | d = requests.post(leave_url, params = data, data = json.dumps(data)) 267 | self.log(2, "Leave request result: {result}", {"result": d.json()}) 268 | 269 | def __register_user(self, full_username, username, nickname): 270 | """ 271 | Registers user with Matrix server for later usage with bridge. 272 | """ 273 | # Checking for profile existing. 274 | d = requests.get(self.__matrix_cfg["api_url"] + "/profile/" + full_username) 275 | rdata = d.json() 276 | if "displayname" in rdata: 277 | self.log(1, "User {user} already registered with nickname '{nickname}'", {"user": username, "nickname": rdata["displayname"]}) 278 | 279 | if "error" in rdata and rdata["error"] == "No row found": 280 | self.log(0, "Registering user...") 281 | # Register user! 282 | # Try to register user and join the room. 283 | data = { 284 | "type": "m.login.application_service", 285 | "user": username 286 | } 287 | 288 | # Register user. 289 | url = self.__matrix_cfg["api_url"] + "/register" 290 | r = requests.post(url, params = self.__params, data = json.dumps(data)) 291 | 292 | # Set display name. 293 | data_to_put = { 294 | "displayname": nickname 295 | } 296 | 297 | data = { 298 | "user_id": full_username 299 | } 300 | data.update(self.__params) 301 | url = self.__matrix_cfg["api_url"] + "/profile/{0}/displayname".format(full_username) 302 | d = requests.put(url, params = data, data = json.dumps(data_to_put), headers = {"Content-Type": "application/json"}) 303 | 304 | def __send_message(self, room_id, user_id, msgtype, message, formatted_message = None): 305 | """ 306 | Really sends message to room. 307 | """ 308 | self.log(0, "Sending message...") 309 | 310 | tx = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(20)) 311 | 312 | if len(room_id) < 4: 313 | self.log(0, "{RED}Error:{RESET} Can't send message to undefined room ID!") 314 | return 315 | 316 | body = { 317 | "msgtype": msgtype, 318 | "body": message, 319 | } 320 | 321 | if formatted_message != None: 322 | body["formatted_body"] = formatted_message 323 | body["format"] = "org.matrix.custom.html" 324 | 325 | body_compiled = json.dumps(body) 326 | 327 | url = self.__msg_api_url.format(room_id) + "/" + tx 328 | 329 | data = { 330 | "user_id": user_id 331 | } 332 | data.update(self.__params) 333 | d = requests.put(url, data = body_compiled, params = data, headers = {"Content-Type": "application/json"}) 334 | self.log(2, "URL: {url}, homeserver's output: {output}", {"url": d.url, "output": d.text}) 335 | 336 | return d.json() 337 | 338 | def __update_users_list_in_room(self, room_id): 339 | """ 340 | Updates users list in room in internal storage. 341 | """ 342 | # Get members list. 343 | d = requests.get(self.__matrix_cfg["api_url"] + "/rooms/" + room_id + "/members", params = self.__params) 344 | data = d.json() 345 | 346 | if "errcode" in data and data["errcode"] == "M_GUEST_ACCESS_FORBIDDEN": 347 | self.log(0, "{RED}Error:{RESET} Failed to join Matrix room: permissions error!") 348 | return 349 | 350 | if not room_id in self.__joined: 351 | self.__joined[room_id] = {} 352 | 353 | for item in data["chunk"]: 354 | if item["content"]["membership"] == "join": 355 | self.__joined[room_id][item["state_key"][1:]] = { 356 | "username": item["state_key"] 357 | } 358 | 359 | self.log(2, "All joined users: {users}", {"users": self.__joined}) 360 | -------------------------------------------------------------------------------- /lib/protocols/xmpp.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | 4 | from lib.common_libs.library import Library 5 | 6 | from lib.protocols.xmpp_conn.connection import XMPPConnection, XMPPConnectionWrapper 7 | 8 | class Xmpp(threading.Thread, Library): 9 | """ 10 | This is a connection manager - thing that ruling all XMPP connections 11 | we create. 12 | """ 13 | 14 | _info = { 15 | "name" : "XMPP connections manager library", 16 | "shortname" : "xmpp_library", 17 | "description" : "Library responsible for handling XMPP connections." 18 | } 19 | 20 | def __init__(self): 21 | Library.__init__(self) 22 | threading.Thread.__init__(self) 23 | 24 | # XMPP mapped nicks. 25 | # Format: 26 | # {"nickname": XMPPClient connection} 27 | self.__xmpp_nicks = {} 28 | 29 | # Shutdown marker. 30 | self.__shutdown = False 31 | 32 | def connect_to_server(self): 33 | print("Connecting to XMPP server...") 34 | # This will connect our "master client", which will watch 35 | # for new messages. 36 | conn = XMPPConnectionWrapper(self.__config, self.__matrix_cfg, self.__queue, self.__config["nick"], True) 37 | self.__xmpp_nicks[self.__config["nick"]] = conn 38 | conn.start() 39 | 40 | def init_library(self): 41 | self.__queue = self.loader.request_library("common_libs", "msgqueue") 42 | self.__config = self.config.get_temp_value("xmpp") 43 | self.__matrix_cfg = self.config.get_temp_value("matrix") 44 | 45 | def run(self): 46 | self.connect_to_server() 47 | 48 | return 49 | 50 | def send_message(self, from_name, to, raw_message): 51 | # Check if we have "from_name" in XMPP mapped nicks. 52 | xmpp_nick = from_name[1:].split(":")[0] + "@Matrix" 53 | if not xmpp_nick in self.__xmpp_nicks or not self.__xmpp_nicks[xmpp_nick].status(): 54 | print("Creating mapped connection for '{0}'...".format(xmpp_nick)) 55 | 56 | conn = XMPPConnectionWrapper(self.__config, self.__matrix_cfg, self.__queue, xmpp_nick, False) 57 | self.__xmpp_nicks[xmpp_nick] = conn 58 | conn.start() 59 | 60 | # Check if we have "(XMPP MUC)" in body. If so - remove it. 61 | if "(XMPP MUC)" in raw_message: 62 | message = raw_message.replace(" (XMPP MUC)", "") 63 | else: 64 | message = raw_message 65 | 66 | print("Sending message to MUC: from '{0}' - {1}".format(xmpp_nick, message)) 67 | msg = { 68 | "mucnick": xmpp_nick, 69 | "body": message 70 | } 71 | 72 | self.__xmpp_nicks[xmpp_nick].send_message(to, message) 73 | 74 | def on_shutdown(self): 75 | """ 76 | Disconnecting from XMPP. 77 | """ 78 | for nick in self.__xmpp_nicks: 79 | self.__xmpp_nicks[nick].shutdown() 80 | 81 | self.__shutdown = True 82 | -------------------------------------------------------------------------------- /lib/protocols/xmpp_conn/connection.py: -------------------------------------------------------------------------------- 1 | import json 2 | #import logging 3 | import sleekxmpp 4 | import threading 5 | import uuid 6 | 7 | #logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') 8 | 9 | class XMPPConnection(sleekxmpp.ClientXMPP): 10 | """ 11 | This class responsible for single XMPP connection and run in 12 | separate thread than ConnectionManager. 13 | 14 | It connects to XMPP with desired login, password and nickname, 15 | joins XMPP room. 16 | """ 17 | def __init__(self, jid, password, room, nick): 18 | sleekxmpp.ClientXMPP.__init__(self, jid, password) 19 | self.room = room 20 | self.nick = nick 21 | # Are we connected? 22 | self.__connected = False 23 | # Should we process messages received by this connection? 24 | # We should process (for now) only messages that received by 25 | # master user. 26 | self.__should_process = False 27 | 28 | self.add_event_handler("session_start", self.start_session) 29 | self.add_event_handler("groupchat_message", self.muc_message) 30 | 31 | def connected(self): 32 | return self.__connected 33 | 34 | def set_config(self, config): 35 | self.__config = config 36 | 37 | def set_matrixconfig(self, config): 38 | self.__matrix_cfg = config 39 | 40 | def set_queue(self, queue): 41 | self.__queue = queue 42 | 43 | def set_should_process(self): 44 | self.__should_process = True 45 | 46 | def start_session(self, event): 47 | self.get_roster() 48 | self.send_presence() 49 | self.plugin["xep_0045"].joinMUC(self.room, self.nick, wait=True) 50 | self.__connected = True 51 | 52 | def muc_message(self, msg): 53 | # We should not send messages from XMPP which have "@Matrix" in 54 | # username. 55 | if self.__should_process and not "@Matrix" in msg["mucnick"]: 56 | print("Received message: {0}".format(msg)) 57 | conference = str(msg["from"]).split("/")[0] 58 | data = {"type": "message_to_room", "from_component": "xmpp", "from": msg["mucnick"], "to": self.__matrix_cfg["room_id"], "body": msg["body"], "id": msg["id"], "conference": conference} 59 | print("Adding item to queue: {0}".format(data)) 60 | self.__queue.add_message(data) 61 | print("Queue len: " + str(self.__queue.items_in_queue())) 62 | 63 | class XMPPConnectionWrapper(threading.Thread): 64 | """ 65 | This is a wrapper around XMPPConnection for launching later in 66 | separate thread. 67 | """ 68 | def __init__(self, config, matrix_config, queue, muc_nick, should_process): 69 | threading.Thread.__init__(self) 70 | self.__config = config 71 | self.__matrix_cfg = matrix_config 72 | self.__queue = queue 73 | # MUC nick 74 | self.__muc_nick = muc_nick 75 | # XMPP connection. 76 | self.__xmpp = None 77 | # Should messages be processed? 78 | self.__should_process = should_process 79 | # Shutdown marker. 80 | self.__shutdown = False 81 | 82 | def connect_to_server(self): 83 | jid = self.__config["username"] 84 | room = self.__config["muc_room"] 85 | nick = self.__config["nick"] 86 | password = self.__config["password"] 87 | resource = uuid.uuid4().urn[9:] 88 | 89 | self.__xmpp = XMPPConnection(jid + "/" + resource, password, room, self.__muc_nick) 90 | self.__xmpp.set_config(self.__config) 91 | self.__xmpp.set_matrixconfig(self.__matrix_cfg) 92 | self.__xmpp.set_queue(self.__queue) 93 | self.__xmpp.register_plugin("xep_0045") 94 | 95 | if self.__should_process: 96 | self.__xmpp.set_should_process() 97 | 98 | print("Connecting...") 99 | if self.__xmpp.connect(address = (self.__config["server_address"], 5222), reattempt = True): 100 | try: 101 | self.__xmpp.process(block=True) 102 | except TypeError: 103 | self.__xmpp.process(threaded=False) # Used for older versions of SleekXMPP 104 | print("Done") 105 | else: 106 | print("Unable to connect.") 107 | 108 | def run(self): 109 | self.connect_to_server() 110 | 111 | while True: 112 | if self.__shutdown: 113 | self.__xmpp.disconnect(wait = True) 114 | break 115 | 116 | time.sleep(1) 117 | 118 | return 119 | 120 | def send_message(self, to, message): 121 | while True: 122 | if self.__xmpp and self.__xmpp.connected(): 123 | self.__xmpp.send_message(mtype = "groupchat", mto = to, mbody = message) 124 | break 125 | 126 | def shutdown(self): 127 | """ 128 | Shuts down connection. 129 | """ 130 | self.__shutdown = True 131 | 132 | def status(self): 133 | if self.__xmpp and self.__xmpp.connected(): 134 | return True 135 | else: 136 | return False 137 | -------------------------------------------------------------------------------- /lib/rooms/rooms.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | # Regius's database.get_database_mapping() thing do something wrong, 4 | # so we should instantiate mapping normally. 5 | from lib.database_mappings.rooms import Rooms as RoomsMap 6 | from lib.common_libs.library import Library 7 | 8 | class Rooms(Library): 9 | 10 | _info = { 11 | "name" : "Rooms handling library", 12 | "shortname" : "rooms_library", 13 | "description" : "Library responsible for handling rooms." 14 | } 15 | 16 | def __init__(self): 17 | Library.__init__(self) 18 | # Rooms list. 19 | self.__rooms = {} 20 | # Rooms types we accept. 21 | self.__rooms_types = {} 22 | 23 | # Threading lock :). 24 | self.__lock = threading.Lock() 25 | 26 | def add_command_room(self, room_id, user_id): 27 | self.__lock.acquire(blocking = True) 28 | if not room_id in self.__rooms.keys(): 29 | self.log(1, "Adding command room {CYAN}{room_id}{RESET} for user {YELLOW}{user_id}{RESET}", {"room_id": room_id, "user_id": user_id}) 30 | self.__rooms[room_id] = { 31 | "id": room_id, 32 | "type": "Command room", 33 | "commander": user_id, 34 | "in_database": True 35 | } 36 | room = RoomsMap() 37 | room.room_id = room_id 38 | room.room_type = self.__rooms_types["Command room"]["id"] 39 | room.commander = user_id 40 | sess = self.__database.get_session() 41 | sess.add(room) 42 | sess.commit() 43 | self.__lock.release() 44 | 45 | def check_room_type(self, room_id): 46 | """ 47 | Checks for room type and returns approriate type in string 48 | representation (i.e. "Bridged room" or "Command room"). 49 | """ 50 | if room_id in self.__rooms.keys(): 51 | return self.__rooms[room_id]["type"] 52 | else: 53 | # If room ID is unknown - assuming command room. 54 | return "Command room" 55 | 56 | def init_library(self): 57 | """ 58 | """ 59 | self.log(0, "Initializing rooms handling library...") 60 | 61 | self.__database = self.loader.request_library("common_libs", "database") 62 | 63 | # Get rooms types. 64 | sess = self.__database.get_session() 65 | rooms_types_mapping = self.__database.get_database_mapping("roomstypes") 66 | rooms_types = sess.query(rooms_types_mapping).order_by(rooms_types_mapping.id).all() 67 | for room_type in rooms_types: 68 | self.__rooms_types[room_type.name] = { 69 | "id": room_type.id, 70 | "name": room_type.name, 71 | "description": room_type.description 72 | } 73 | 74 | # Get rooms. 75 | rooms_mapping = self.__database.get_database_mapping("rooms") 76 | data = sess.query(rooms_mapping).all() 77 | # Convert rooms list into dictionary. 78 | for item in data: 79 | self.__rooms[item.room_id] = { 80 | "id": item.room_id, 81 | "commander": item.commander, 82 | "in_database": True 83 | } 84 | # Room type. 85 | for rt in self.__rooms_types: 86 | if self.__rooms_types[rt]["id"] == item.room_type: 87 | self.__rooms[item.room_id]["type"] = self.__rooms_types[rt]["name"] 88 | 89 | def on_shutdown(self): 90 | """ 91 | """ 92 | self.log(0, "Updating rooms list in database...") 93 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | 6 | # this is the Alembic Config object, which provides 7 | # access to the values within the .ini file in use. 8 | config = context.config 9 | 10 | # Interpret the config file for Python logging. 11 | # This line sets up loggers basically. 12 | fileConfig(config.config_file_name) 13 | 14 | # add your model's MetaData object here 15 | # for 'autogenerate' support 16 | # from myapp import mymodel 17 | # target_metadata = mymodel.Base.metadata 18 | target_metadata = None 19 | 20 | # other values from the config, defined by the needs of env.py, 21 | # can be acquired: 22 | # my_important_option = config.get_main_option("my_important_option") 23 | # ... etc. 24 | 25 | 26 | def run_migrations_offline(): 27 | """Run migrations in 'offline' mode. 28 | 29 | This configures the context with just a URL 30 | and not an Engine, though an Engine is acceptable 31 | here as well. By skipping the Engine creation 32 | we don't even need a DBAPI to be available. 33 | 34 | Calls to context.execute() here emit the given string to the 35 | script output. 36 | 37 | """ 38 | url = config.get_main_option("sqlalchemy.url") 39 | context.configure( 40 | url=url, target_metadata=target_metadata, literal_binds=True) 41 | 42 | with context.begin_transaction(): 43 | context.run_migrations() 44 | 45 | 46 | def run_migrations_online(): 47 | """Run migrations in 'online' mode. 48 | 49 | In this scenario we need to create an Engine 50 | and associate a connection with the context. 51 | 52 | """ 53 | connectable = engine_from_config( 54 | config.get_section(config.config_ini_section), 55 | prefix='sqlalchemy.', 56 | poolclass=pool.NullPool) 57 | 58 | with connectable.connect() as connection: 59 | context.configure( 60 | connection=connection, 61 | target_metadata=target_metadata 62 | ) 63 | 64 | with context.begin_transaction(): 65 | context.run_migrations() 66 | 67 | if context.is_offline_mode(): 68 | run_migrations_offline() 69 | else: 70 | run_migrations_online() 71 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/7d6a7a0c6c1d_add_rooms_types_table.py: -------------------------------------------------------------------------------- 1 | """Add rooms_types table 2 | 3 | Revision ID: 7d6a7a0c6c1d 4 | Revises: de731ab04076 5 | Create Date: 2016-12-26 22:19:25.156107 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '7d6a7a0c6c1d' 14 | down_revision = 'de731ab04076' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table("rooms_types", 21 | sa.Column("id", sa.Integer, primary_key = True, unique = True, autoincrement = True), 22 | sa.Column("name", sa.String(255), nullable = False), 23 | sa.Column("description", sa.String(255), nullable = False) 24 | ) 25 | 26 | op.execute("INSERT INTO rooms_types (name, description) VALUES ('Command room', 'Command room is a private room for setting up a bridge between other room and XMPP MUC')") 27 | op.execute("INSERT INTO rooms_types (name, description) VALUES ('Bridged room', 'Bridged room is a public room which have operating bridge between it and XMPP MUC. Most of commands unavailable here.')") 28 | 29 | 30 | def downgrade(): 31 | op.drop_table("rooms_types") 32 | -------------------------------------------------------------------------------- /migrations/versions/960957b4f2ec_add_commander_to_rooms.py: -------------------------------------------------------------------------------- 1 | """Add commander to rooms 2 | 3 | Revision ID: 960957b4f2ec 4 | Revises: 7d6a7a0c6c1d 5 | Create Date: 2016-12-31 08:01:13.453088 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '960957b4f2ec' 14 | down_revision = '7d6a7a0c6c1d' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column("rooms", sa.Column("commander", sa.String(255), nullable = False)) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column("rooms", "commander") 25 | -------------------------------------------------------------------------------- /migrations/versions/de731ab04076_add_rooms_table.py: -------------------------------------------------------------------------------- 1 | """Add rooms table 2 | 3 | Revision ID: de731ab04076 4 | Revises: 5 | Create Date: 2016-12-25 15:36:51.183693 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'de731ab04076' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table("rooms", 21 | sa.Column("room_id", sa.String(255), primary_key = True, unique = True), 22 | sa.Column("room_type", sa.Integer, nullable = False) 23 | ) 24 | 25 | op.create_index("rooms_room_ids", "rooms", ["room_id"]) 26 | op.create_index("rooms_room_type", "rooms", ["room_type"]) 27 | 28 | 29 | def downgrade(): 30 | op.drop_index("rooms_room_ids", "rooms") 31 | op.drop_index("rooms_room_type", "rooms") 32 | 33 | op.drop_table("rooms") 34 | 35 | -------------------------------------------------------------------------------- /plugins/bridge/bridge.py: -------------------------------------------------------------------------------- 1 | from lib.common_libs.plugin import Plugin 2 | 3 | HELP_MESSAGE = """**Creating a bridge** 4 | To properly configure bridge you should issue command with following syntax: 5 | 6 | ``` 7 | bridge [matrix_room_id] [xmpp_user@domain] [xmpp_user_password] [muc@address.tld] 8 | ``` 9 | 10 | Where: 11 | 12 | * `matrix_room_id` is room ID in Matrix. Check it out by pressing cogwheel near room's topic. 13 | * `xmpp_user@domain` is XMPP JID which bot will use. 14 | * `xmpp_user_password` is a password for XMPP JID. 15 | * `muc@address.tld` is MUC address we will bridge. 16 | 17 | Sit back. wait for couple of minutes and room will be bridged :) 18 | 19 | **Listing all bridges you created** 20 | 21 | Issue `bridge list` to get list of bridges you've created. 22 | """ 23 | 24 | class Bridge_Plugin(Plugin): 25 | """ 26 | This plugin responsible for configuring of bridges. 27 | """ 28 | 29 | _info = { 30 | "name" : "Bridge plugin", 31 | "shortname" : "help_command_plugin", 32 | "description" : "This plugin responsible for configuring of bridges." 33 | } 34 | 35 | def __init__(self): 36 | Plugin.__init__(self) 37 | 38 | # Chat commands we will answer for. 39 | self.__chat_commands = { 40 | "bridge": { 41 | "keywords" : ["bridge"], 42 | "description" : "Manage bridges. Type 'bridge list' to get list of bridges configured by you.", 43 | "method" : self.process 44 | } 45 | } 46 | 47 | def get_commands(self): 48 | """ 49 | Returns commands list we accept to caller. 50 | """ 51 | return self.__chat_commands 52 | 53 | def initialize(self): 54 | """ 55 | Help plugin initialization. 56 | """ 57 | self.log(0, "Initializing plugin...") 58 | 59 | def process(self, message): 60 | """ 61 | Processes message. 62 | """ 63 | if len(message) == 0: 64 | return self.show_help() 65 | else: 66 | return "Bridge configuration received message: " + message 67 | 68 | def show_help(self): 69 | """ 70 | Returns help string. 71 | """ 72 | return HELP_MESSAGE 73 | -------------------------------------------------------------------------------- /plugins/help/help.py: -------------------------------------------------------------------------------- 1 | from lib.common_libs.plugin import Plugin 2 | 3 | class Help_Plugin(Plugin): 4 | """ 5 | This plugin responsible for printing help to chat. 6 | """ 7 | 8 | _info = { 9 | "name" : "Help plugin", 10 | "shortname" : "help_command_plugin", 11 | "description" : "This plugin responsible for printing help to chat." 12 | } 13 | 14 | def __init__(self): 15 | Plugin.__init__(self) 16 | 17 | # Chat commands we will answer for. 18 | self.__chat_commands = { 19 | "help": { 20 | "keywords" : ["help"], 21 | "description" : "Show help message.", 22 | "method" : self.show_help 23 | } 24 | } 25 | 26 | def get_commands(self): 27 | """ 28 | Returns commands list we accept to caller. 29 | """ 30 | return self.__chat_commands 31 | 32 | def initialize(self): 33 | """ 34 | Help plugin initialization. 35 | """ 36 | self.log(0, "Initializing plugin...") 37 | 38 | def show_help(self, message): 39 | """ 40 | Returns help string. 41 | """ 42 | help_string = "" 43 | cmds = self.loader.request_library("commands", "commands").get_commands() 44 | 45 | for cmd in cmds: 46 | # Format keywords in string. 47 | if len(cmds[cmd]["keywords"]) > 1: 48 | kwds = "`, `".join(cmds[cmd]["keywords"]) 49 | help_string += kwds 50 | else: 51 | help_string += "`" + cmds[cmd]["keywords"][0] + "`" 52 | 53 | # Add description. 54 | help_string += " - " + cmds[cmd]["description"] 55 | 56 | # Add newline. 57 | help_string += "\n\n" 58 | 59 | return help_string 60 | -------------------------------------------------------------------------------- /registration.yaml.example: -------------------------------------------------------------------------------- 1 | # registration.yaml 2 | 3 | # this is the base URL of the application service 4 | url: "http://localhost:5000" 5 | 6 | # This is the token that the AS should use as its access_token when using the Client-Server API 7 | # This can be anything you want. 8 | as_token: supersekret 9 | 10 | # This is the token that the HS will use when sending requests to the AS. 11 | # This can be anything you want. 12 | hs_token: supersekret 13 | 14 | id: xmpp-matrix-bridge 15 | 16 | # this is the local part of the desired user ID for this AS (in this case @logging:localhost) 17 | sender_localpart: mxbridge 18 | namespaces: 19 | users: 20 | - exclusive: true 21 | regex: "@xmpp_.*" 22 | rooms: [] 23 | aliases: 24 | - exclusive: false 25 | regex: "#xmpp.*" 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic 2 | flask 3 | markdown2 4 | psycopg2 5 | requests 6 | sleekxmpp 7 | sqlalchemy 8 | -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | drop table if exists virtual_users; 2 | create table usermap ( 3 | id integer primary key autoincrement, 4 | xmpp_user text not null, 5 | matrix_user text not null 6 | ); 7 | create table roommap ( 8 | id integer primary key autoincrement, 9 | xmpp_room text not null, 10 | matrix_room text not null 11 | ); 12 | create table room_membership ( 13 | id integer primary key autoincrement, 14 | room text not null, 15 | room integer not null 16 | ) 17 | --------------------------------------------------------------------------------