From 03e895e87b3701fec4d324b5a6a6a237f1945235 Mon Sep 17 00:00:00 2001 From: Morgan McMillian Date: Sun, 12 Jan 2025 08:15:57 -0800 Subject: [PATCH] replaced config file format with toml --- pyproject.toml | 1 + src/pnut_matrix/appservice.py | 306 ++++++++++++++++++--------------- src/pnut_matrix/cmd.py | 269 +++++++++++++++++++++++++---- src/pnut_matrix/pnutservice.py | 115 +++++++------ 4 files changed, 468 insertions(+), 223 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9e8933e..75628a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "websockets", "asyncclick", "peewee", + "tomlkit", ] [project.urls] diff --git a/src/pnut_matrix/appservice.py b/src/pnut_matrix/appservice.py index ea71808..5ff6d14 100644 --- a/src/pnut_matrix/appservice.py +++ b/src/pnut_matrix/appservice.py @@ -1,5 +1,6 @@ import json import yaml +import tomlkit import requests import logging import logging.config @@ -30,14 +31,14 @@ def forbidden(error): async def query_alias(alias): logging.debug("--- query alias ---") alias_localpart = alias.split(":")[0][1:] - channel_id = int(alias_localpart.split('_')[1]) + channel_id = int(alias_localpart.lstrip(app.config['matrix']['namespace'])) room = PnutChannels.select().where(PnutChannels.pnut_chan == channel_id).first() if room is not None: abort(404) - token = app.config['MATRIX_PNUT_TOKEN'] + token = app.config['pnut']['bot_token'] pnutpy.api.add_authorization_token(token) try: logging.debug("---- getting the channel ----") @@ -59,9 +60,11 @@ async def query_alias(alias): else: topic = None - matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], - base_url=app.config['MATRIX_HOST'], - token=app.config['MATRIX_AS_TOKEN']) + as_id = (f"@{app.config['matrix']['sender_local']}:" + f"{app.config['matrix']['domain']}") + matrix_api = ClientAPI(as_id, + base_url=app.config['matrix']['homeserver'], + token=app.config['matrix']['as_token']) if channel.acl.read.public: visibility = RoomDirectoryVisibility.PUBLIC @@ -98,7 +101,7 @@ async def query_alias(alias): async def on_receive_events(transaction): access_token = request.args.get('access_token', '') - if access_token != app.config['MATRIX_HS_TOKEN']: + if access_token != app.config['matrix']['hs_token']: abort(403) events = request.get_json()["events"] @@ -107,11 +110,11 @@ async def on_receive_events(transaction): logging.debug(event) logging.debug('~~~~~~~~~~~~~~~') - if (app.config['MATRIX_ADMIN_ROOM'] and - app.config['MATRIX_ADMIN_ROOM'] == event['room_id']): - logging.debug('>----on_admin_event----<') - await on_admin_event(event) - return jsonify({}) + # if (app.config['MATRIX_ADMIN_ROOM'] and + # app.config['MATRIX_ADMIN_ROOM'] == event['room_id']): + # logging.debug('>----on_admin_event----<') + # await on_admin_event(event) + # return jsonify({}) user = PnutUsers.select().where(PnutUsers.matrix_id == event['sender']).first() @@ -142,11 +145,13 @@ async def on_receive_events(transaction): return jsonify({}) async def new_message(event, user): + as_id = (f"@{app.config['matrix']['sender_local']}:" + f"{app.config['matrix']['domain']}") - if event['sender'] == app.config['MATRIX_AS_ID']: + if event['sender'] == as_id: return - if app.config['MATRIX_PNUT_PREFIX'] in event['sender']: + if app.config['matrix']['namespace'] in event['sender']: return if user.room_id == event['room_id']: @@ -158,9 +163,9 @@ async def new_message(event, user): logging.debug(f'room: {room}') if room is None: - if event['room_id'] == app.config['MATRIX_GLOBAL_ROOM']: + if event['room_id'] == app.config['pnut']['global_room']: room = PnutChannels(pnut_chan=0, - room_id=app.config['MATRIX_GLOBAL_ROOM']) + room_id=app.config['pnut']['global_room']) else: logging.debug('-room not mapped-') @@ -168,14 +173,14 @@ async def new_message(event, user): if room.is_direct: logging.debug('>----on_direct_message----<') - return on_direct_message(event, user, room) + return await on_direct_message(event, user, room) if user is not None: token = user.pnut_user_token prefix = "" else: - token = app.config['MATRIX_PNUT_TOKEN'] + token = app.config['pnut']['bot_token'] matrix_profile = get_profile(event['sender']) if ('displayname' in matrix_profile): prefix = (f"[{matrix_profile['displayname']}]" @@ -261,9 +266,9 @@ async def new_message(event, user): if room.pnut_chan != 0: logging.exception('-unable to post to pnut channel-') else: - matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], - base_url=app.config['MATRIX_HOST'], - token=app.config['MATRIX_AS_TOKEN']) + matrix_api = ClientAPI(as_id, + base_url=app.config['matrix']['homeserver'], + token=app.config['matrix']['as_token']) await matrix_api.redact(event['room_id'], event['event_id'], reason='user not authenticated') @@ -314,7 +319,7 @@ async def msg_from_event(event, user): elif event['content']['msgtype'] == 'm.file': file_url = event['content']['url'][6:] file_name = event['content']['body'] - dl_url = (f"{app.config['MATRIX_URL']}" + dl_url = (f"{app.config['matrix']['homeserver']}" f"/_matrix/client/v1/media/download/{file_url}" f"/{file_name}") text = (f"[{file_name}]" @@ -330,20 +335,22 @@ def crosspost_raw(event): cross_profile = {'username': event['sender']} matrix_profile = get_profile(event['sender']) if "avatar_url" in matrix_profile: - cross_profile['avatar_image'] = (f"{app.config['MATRIX_URL']}" + cross_profile['avatar_image'] = (f"{app.config['matrix']['homeserver']}" f"/_matrix/media/r0/download/" f"{matrix_profile['avatar_url'][6:]}") crosspost = {} crosspost['canonical_url'] = (f"https://matrix.to/#/{event['room_id']}" f"/{event['event_id']}" - f":{app.config['MATRIX_DOMAIN']}") + f":{app.config['matrix']['domain']}") crosspost['source'] = {'name': "matrix.", 'url': "https://matrix.org"} crosspost['user'] = cross_profile return crosspost async def media_from_event(event, user): + as_id = (f"@{app.config['matrix']['sender_local']}:" + f"{app.config['matrix']['domain']}") mxc_url = event['content']['url'] if event['content']['msgtype'] == 'm.image': kind = 'image' @@ -359,9 +366,9 @@ async def media_from_event(event, user): 'name': file_name, 'kind': kind, 'mimetype': mime_type, 'is_public': True} - matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], - base_url=app.config['MATRIX_HOST'], - token=app.config['MATRIX_AS_TOKEN']) + matrix_api = ClientAPI(as_id, + base_url=app.config['matrix']['homeserver'], + token=app.config['matrix']['as_token']) media_file = await matrix_api.download_media(mxc_url) pnutpy.api.add_authorization_token(user.pnut_user_token) @@ -391,7 +398,7 @@ async def media_from_event(event, user): def oembed_from_event(event): media_url = event['content']['url'][6:] file_name = event['content']['body'] - dl_url = (f"{app.config['MATRIX_URL']}" + dl_url = (f"{app.config['matrix']['homeserver']}" f"/_matrix/client/v1/media/download/{media_url}" f"/{file_name}") @@ -439,7 +446,7 @@ def delete_message(event, user): if user is not None: token = user.pnut_user_token else: - token = app.config['MATRIX_PNUT_TOKEN'] + token = app.config['pnut']['bot_token'] pnutpy.api.add_authorization_token(token) e = Events.select().where((Events.event_id == event['redacts']) & @@ -457,7 +464,7 @@ def delete_message(event, user): pass def get_profile(userid): - url = app.config['MATRIX_HOST'] + "/_matrix/client/r0/profile/" + userid + url = app.config['matrix']['homeserver'] + "/_matrix/client/r0/profile/" + userid r = requests.get(url) if r.status_code == 200: return json.loads(r.text) @@ -480,7 +487,7 @@ def get_channel_settings(channel_id): async def create_pnut_matrix_room(channel, user): name = None topic = None - alias_localpart = f"{app.config['MATRIX_PNUT_PREFIX']}{channel.id}" + alias_localpart = f"{app.config['matrix']['namespace']}{channel.id}" invitees = [user.matrix_id] if channel.acl.read.public: @@ -501,9 +508,11 @@ async def create_pnut_matrix_room(channel, user): if 'description' in setting: topic = setting['description']['text'] - matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], - base_url=app.config['MATRIX_HOST'], - token=app.config['MATRIX_AS_TOKEN']) + as_id = (f"@{app.config['matrix']['sender_local']}:" + f"{app.config['matrix']['domain']}") + matrix_api = ClientAPI(as_id, + base_url=app.config['matrix']['homeserver'], + token=app.config['matrix']['as_token']) room_id = await matrix_api.create_room(alias_localpart, invitees=invitees, @@ -529,15 +538,15 @@ async def create_pnut_matrix_room(channel, user): def new_matrix_user(username): endpoint = "/_matrix/client/v3/register" - url = app.config['MATRIX_HOST'] + endpoint + url = app.config['matrix']['homeserver'] + endpoint params = {'kind': 'user'} data = { 'type': 'm.login.application_service', - 'username': app.config['MATRIX_PNUT_PREFIX'] + username + 'username': app.config['matrix']['namespace'] + username } headers = { "Content-Type": "application/json", - "Authorization": "Bearer " + app.config['MATRIX_AS_TOKEN'] + "Authorization": "Bearer " + app.config['matrix']['as_token'] } r = requests.post(url, headers=headers, json=data, params=params) if r.status_code == 200: @@ -550,96 +559,99 @@ def new_matrix_user(username): logging.debug(r.text) return -async def on_admin_event(event): - matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], - base_url=app.config['MATRIX_HOST'], - token=app.config['MATRIX_AS_TOKEN']) +# async def on_admin_event(event): +# matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], +# base_url=app.config['MATRIX_HOST'], +# token=app.config['MATRIX_AS_TOKEN']) +# +# logging.debug("- admin room event recieved -") +# +# if event['type'] != 'm.room.message': +# return jsonify({}) +# +# msg = event['content']['body'].split(' ') +# +# try: +# if msg[0] == 'help': +# if len(msg) > 1: +# await matrix_api.send_message(event['room_id'], +# cmd_admin_help(msg[1])) +# else: +# await matrix_api.send_message(event['room_id'], +# cmd_admin_help()) +# +# elif msg[0] == 'list': +# await matrix_api.send_message(event['room_id'], cmd_admin_list()) +# +# except Exception: +# errmsg = "- on_admin_event -" +# logging.exception(errmsg) - logging.debug("- admin room event recieved -") +# def cmd_admin_help(cmd=None): +# help_usage = "help [command]" +# help_desc = "Show information about available commands." +# list_usage = "list" +# list_desc = "List the rooms currently linked with pnut.io." +# +# if cmd == 'help': +# text = "usage: " + help_usage + "\n\n" +# text += help_desc +# if cmd == 'list': +# text = "usage: " + list_usage + "\n\n" +# text += list_desc +# elif cmd == 'unlink': +# text = "usage: " + unlink_usage + "\n\n" +# text += unlink_desc +# elif cmd == 'link': +# text = "usage: " + link_usage + "\n\n" +# text += link_desc +# else: +# text = "The following commands are available:\n\n" +# text += help_usage + "\n" +# text += list_usage + "\n" +# text += unlink_usage + "\n" +# text += link_usage + "\n" +# +# return TextMessageEventContent(msgtype='m.text', body=text) - if event['type'] != 'm.room.message': - return jsonify({}) - - msg = event['content']['body'].split(' ') - - try: - if msg[0] == 'help': - if len(msg) > 1: - await matrix_api.send_message(event['room_id'], - cmd_admin_help(msg[1])) - else: - await matrix_api.send_message(event['room_id'], - cmd_admin_help()) - - elif msg[0] == 'list': - await matrix_api.send_message(event['room_id'], cmd_admin_list()) - - except Exception: - errmsg = "- on_admin_event -" - logging.exception(errmsg) - -def cmd_admin_help(cmd=None): - help_usage = "help [command]" - help_desc = "Show information about available commands." - list_usage = "list" - list_desc = "List the rooms currently linked with pnut.io." - - if cmd == 'help': - text = "usage: " + help_usage + "\n\n" - text += help_desc - if cmd == 'list': - text = "usage: " + list_usage + "\n\n" - text += list_desc - elif cmd == 'unlink': - text = "usage: " + unlink_usage + "\n\n" - text += unlink_desc - elif cmd == 'link': - text = "usage: " + link_usage + "\n\n" - text += link_desc - else: - text = "The following commands are available:\n\n" - text += help_usage + "\n" - text += list_usage + "\n" - text += unlink_usage + "\n" - text += link_usage + "\n" - - return TextMessageEventContent(msgtype='m.text', body=text) - -def cmd_admin_list(): - text = "" - rooms = PnutChannels.select() - - if len(rooms) > 0: - text = "ID\tMATRIX ID\tPNUT CHANNEL\n" - else: - text = " - no rooms are currently linked - \n" - - for room in rooms: - text += str(room.id) + '\t' - text += room.room_id + '\t\t\t\t\t' - text += str(room.pnut_chan) + '\t' - text += '\n' - - return TextMessageEventContent(msgtype='m.text', body=text) +# def cmd_admin_list(): +# text = "" +# rooms = PnutChannels.select() +# +# if len(rooms) > 0: +# text = "ID\tMATRIX ID\tPNUT CHANNEL\n" +# else: +# text = " - no rooms are currently linked - \n" +# +# for room in rooms: +# text += str(room.id) + '\t' +# text += room.room_id + '\t\t\t\t\t' +# text += str(room.pnut_chan) + '\t' +# text += '\n' +# +# return TextMessageEventContent(msgtype='m.text', body=text) async def on_direct_invite(event): + as_id = (f"@{app.config['matrix']['sender_local']}:" + f"{app.config['matrix']['domain']}") + # direct chat with the appservice user - if event['state_key'] == app.config['MATRIX_AS_ID']: - matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], - base_url=app.config['MATRIX_HOST'], - token=app.config['MATRIX_AS_TOKEN']) + if event['state_key'] == as_id: + matrix_api = ClientAPI(as_id, + base_url=app.config['matrix']['homeserver'], + token=app.config['matrix']['as_token']) dm = PnutUsers(matrix_id=event['sender'], room_id=event['room_id']) # direct chat with another pnut user - elif app.config['MATRIX_PNUT_PREFIX'] in event['state_key']: - matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], - base_url=app.config['MATRIX_HOST'], - token=app.config['MATRIX_AS_TOKEN'], + elif app.config['matrix']['namespace'] in event['state_key']: + matrix_api = ClientAPI(as_id, + base_url=app.config['matrix']['homeserver'], + token=app.config['matrix']['as_token'], as_user_id=event['state_key']) bridge_user = event['state_key'] - pnut_user = bridge_user.replace(app.config['MATRIX_PNUT_PREFIX'], + pnut_user = bridge_user.replace(app.config['matrix']['namespace'], '').split(':')[0] user = PnutUsers.select().where(PnutUsers.matrix_id == @@ -685,10 +697,12 @@ async def on_leave_event(event): user = PnutUsers.select().where(PnutUsers.room_id == event['room_id']).first() + as_id = (f"@{app.config['matrix']['sender_local']}:" + f"{app.config['matrix']['domain']}") if direct_room is not None: - matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], - base_url=app.config['MATRIX_HOST'], - token=app.config['MATRIX_AS_TOKEN'], + matrix_api = ClientAPI(as_id, + base_url=app.config['matrix']['homeserver'], + token=app.config['matrix']['as_token'], as_user_id=direct_room.direct_pnut_user.lower()) try: @@ -699,13 +713,13 @@ async def on_leave_event(event): errmsg = "- on_leave_event -" logging.exception(errmsg) -def on_direct_message(event, user, room): +async def on_direct_message(event, user, room): if user is not None: token = user.pnut_user_token prefix = "" else: - token = app.config['MATRIX_PNUT_TOKEN'] + token = app.config['pnut']['bot_token'] matrix_profile = get_profile(event['sender']) if "displayname" in matrix_profile: prefix = (f"[{matrix_profile['displayname']}]" @@ -716,7 +730,7 @@ def on_direct_message(event, user, room): raw = {} raw['io.pnut.core.crosspost'] = [crosspost_raw(event)] - evtext, evraw = msg_from_event(event) + evtext, evraw = await msg_from_event(event, user) text = prefix + evtext try: @@ -739,9 +753,11 @@ def on_direct_message(event, user, room): return jsonify({}) async def on_control_message(event, user): - matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], - base_url=app.config['MATRIX_HOST'], - token=app.config['MATRIX_AS_TOKEN']) + as_id = (f"@{app.config['matrix']['sender_local']}:" + f"{app.config['matrix']['domain']}") + matrix_api = ClientAPI(as_id, + base_url=app.config['matrix']['homeserver'], + token=app.config['matrix']['as_token']) if event['type'] != 'm.room.message': return jsonify({}) @@ -835,13 +851,14 @@ def cmd_user_save(user, token=None): return TextMessageEventContent(msgtype='m.text', body=reply) async def cmd_user_drop(user): - + as_id = (f"@{app.config['matrix']['sender_local']}:" + f"{app.config['matrix']['domain']}") direct_rooms = PnutChannels.select().where(PnutChannels.direct_mtrx_user == user.matrix_id) for dir_room in direct_rooms: - matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], - base_url=app.config['MATRIX_HOST'], - token=app.config['MATRIX_AS_TOKEN'], + matrix_api = ClientAPI(as_id, + base_url=app.config['matrix']['homeserver'], + token=app.config['matrix']['as_token'], as_user_id=dir_room.direct_pnut_user.lower()) await matrix_api.leave_room(dir_room.room_id) dir_room.delete_instance() @@ -849,9 +866,9 @@ async def cmd_user_drop(user): private_rooms = PnutPrivateChanMembers.select().where( PnutPrivateChanMembers.pnut_user_id == user.pnut_user_id) for priv_room in private_rooms: - matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], - base_url=app.config['MATRIX_HOST'], - token=app.config['MATRIX_AS_TOKEN']) + matrix_api = ClientAPI(as_id, + base_url=app.config['matrix']['homeserver'], + token=app.config['matrix']['as_token']) await matrix_api.kick_user(priv_room.room_id, user.matrix_id, reason='user left from bridge') priv_room.delete_instance() @@ -882,6 +899,8 @@ def cmd_user_status(user): return TextMessageEventContent(msgtype='m.text', body=reply) async def cmd_user_join(user, channel_id=None): + as_id = (f"@{app.config['matrix']['sender_local']}:" + f"{app.config['matrix']['domain']}") if channel_id is None: reply = "You must provide a channel id number with this command.\n" reply += "!join " @@ -898,9 +917,9 @@ async def cmd_user_join(user, channel_id=None): if room is None: await create_pnut_matrix_room(channel, user) else: - matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], - base_url=app.config['MATRIX_HOST'], - token=app.config['MATRIX_AS_TOKEN']) + matrix_api = ClientAPI(as_id, + base_url=app.config['matrix']['homeserver'], + token=app.config['matrix']['as_token']) await matrix_api.invite_user(room.room_id, user.matrix_id) reply = "ok" @@ -935,14 +954,20 @@ class MLogFilter(logging.Filter): def main(): a_parser = argparse.ArgumentParser() - a_parser.add_argument('-c', '--config', dest='configyaml', - default="config.yaml", help="configuration file") + a_parser.add_argument('-c', '--config', dest='config', + default="config.toml", help="configuration file") + a_parser.add_argument('-l', '--log_config', dest='log_config', + default='logging-config.yaml', + help='logging configuration') args = a_parser.parse_args() - with open(args.configyaml, "rb") as config_file: - config = yaml.load(config_file, Loader=yaml.SafeLoader) + with open(args.config, "rb") as config_file: + config = tomlkit.load(config_file) - logging.config.dictConfig(config['logging']) + with open(args.log_config, 'rb') as log_config_file: + log_config = yaml.load(log_config_file, Loader=yaml.SafeLoader) + + logging.config.dictConfig(log_config) redact_filter = MLogFilter() logging.getLogger("werkzeug").addFilter(redact_filter) logging.getLogger("urllib3.connectionpool").addFilter(redact_filter) @@ -950,10 +975,11 @@ def main(): app.config.update(config) logging.basicConfig(level=logging.DEBUG) - db.init(config['SERVICE_DB']) + db.init(config['database']) db_create_tables() - app.run(host=config['LISTEN_HOST'], port=config['LISTEN_PORT']) + host_addr, host_port = config['listen_addr'].split(':') + app.run(host=host_addr, port=host_port) if __name__ == '__main__': main() diff --git a/src/pnut_matrix/cmd.py b/src/pnut_matrix/cmd.py index bd40a1a..ecccda2 100755 --- a/src/pnut_matrix/cmd.py +++ b/src/pnut_matrix/cmd.py @@ -7,6 +7,9 @@ import requests import pnutpy import json import yaml +import tomlkit +import secrets +import string from mautrix.client import ClientAPI from mautrix.types import TextMessageEventContent, Format, MessageType, EventType @@ -17,25 +20,132 @@ PNUT_API="https://api.pnut.io/v1" @click.group() @click.option('--debug', '-d', is_flag=True) -@click.option('--config', '-c', required=True) +@click.option('--config', '-c') @click.pass_context async def cmd(ctx, debug, config): if debug: logging.basicConfig(level=logging.DEBUG) ctx.ensure_object(dict) - with open(config, "rb") as config_file: - ctx.obj['config'] = yaml.load(config_file, Loader=yaml.SafeLoader) + if config is None: + ctx.obj['config'] = tomlkit.document() + else: + with open(config, "rb") as config_file: + ctx.obj['config'] = tomlkit.load(config_file) + click.echo(ctx.obj['config']) + +@cmd.command() +@click.option('--listen_addr', prompt='Host address and port to listen on', + default='127.0.0.1:5000') +@click.option('--homeserver', prompt='Matrix homeserver URL', + default='http://127.0.0.1:8008') +@click.option('--domain', prompt='Matrix domain') +@click.option('--namespace', prompt='Namespace for bridge)', + default='_pnut_') +@click.option('--sender', prompt='Sender localpart for bridge', + default='_pnut_bot') +@click.option('--client_id', prompt='Pnut Client ID') +@click.option('--client_secret', prompt='Pnut Client Secret') +@click.option('--bot_user', prompt='Pnut bot username') +@click.option('--bot_token', prompt='Pnut bot user token') +@click.option('--app_token', prompt='Pnut app token') +@click.option('--app_key', prompt='Pnut app stream key') +@click.pass_context +def generate_config(ctx, listen_addr, homeserver, domain, namespace, sender, + client_id, client_secret, bot_user, bot_token, app_token, + app_key): + + alpha = string.ascii_letters + string.digits + as_token = ''.join(secrets.choice(alpha) for i in range(33)) + hs_token = ''.join(secrets.choice(alpha) for i in range(33)) + + config = tomlkit.document() + config.add("listen_addr", listen_addr) + config.add("database", "store.db") + + matrix = tomlkit.table() + matrix.add("homeserver", homeserver) + matrix.add("domain", domain) + matrix.add("as_token", as_token) + matrix.add("hs_token", hs_token) + matrix.add("namespace", namespace) + matrix.add("sender_local", sender) + config.add("matrix", matrix) + + pnut = tomlkit.table() + pnut.add("client_id", client_id) + pnut.add("client_secret", client_secret) + pnut.add("bot_user", bot_user) + pnut.add("bot_token", bot_token) + pnut.add("app_token", app_token) + pnut.add("app_key", app_key) + pnut.add("global_stream", False) + pnut.add("global_room", "") + pnut.add("global_humans_only", True) + config.add("pnut", pnut) + + click.echo() + write_conf = click.confirm("Write pnut-matrix configuration to config.toml?") + if write_conf: + with open('config.toml', 'w') as write_file: + tomlkit.dump(config, write_file) + + else: + click.echo() + click.echo('##################################################' + + '####################') + click.echo('# The following is the generated config.toml file.' + + ' with pnut-matrix. #') + click.echo('##################################################' + + '####################') + click.echo(tomlkit.dumps(config)) + click.echo('##################################################' + + '####################') + + appsrv = {} + appsrv['id'] = 'pnut' + appsrv['url'] = f'http://127.0.0.1:5000' + appsrv['as_token'] = as_token + appsrv['hs_token'] = hs_token + appsrv['sender_localpart'] = sender + appsrv['namespaces'] = { + 'users': [{'exclusive': True, 'regex': f'@{namespace}.*'}], + 'rooms': [], + 'aliases': [{'exclusive': True, 'regex': f'#{namespace}.*'}]} + + write_asconf = click.confirm("Write matrix configuration to appservice.yaml?") + if write_asconf: + with open('appservice.yaml', 'w') as write_file: + yaml.dump(appsrv, write_file) + + else: + click.echo() + click.echo('############################################################' + + '###########################') + click.echo('# The following is the generated appservice.yaml file to use' + + ' with your matrix server. #') + click.echo('############################################################' + + '###########################') + click.echo(yaml.dump(appsrv)) + click.echo('############################################################' + + '###########################') @cmd.command() @click.pass_context async def get_streams(ctx): config = ctx.obj['config'] - logging.debug(config) + if 'pnut' not in config: + click.echo("Pnut configuration missing!") + exit(1) + + if 'app_token' not in config['pnut']: + click.echo("Pnut app token missing from configuration!") + exit(1) + endpoint = "/streams" headers = { "Content-Type": "application/json", - "Authorization": "Bearer " + config['PNUT_APPTOKEN'] + "Authorization": "Bearer " + config['pnut']['app_token'] } url = f"{PNUT_API}{endpoint}" @@ -53,11 +163,18 @@ async def get_streams(ctx): @click.pass_context def rm_stream(ctx, key): config = ctx.obj['config'] - logging.debug(config) + if 'pnut' not in config: + click.echo("Pnut configuration missing!") + exit(1) + + if 'app_token' not in config['pnut']: + click.echo("Pnut app token missing from configuration!") + exit(1) + endpoint = f"/streams/{key}" headers = { "Content-Type": "application/json", - "Authorization": "Bearer " + config['PNUT_APPTOKEN'] + "Authorization": "Bearer " + config['pnut']['app_token'] } url = f"{PNUT_API}{endpoint}" @@ -74,11 +191,18 @@ def rm_stream(ctx, key): @click.pass_context def rm_streams(ctx): config = ctx.obj['config'] - logging.debug(config) + if 'pnut' not in config: + click.echo("Pnut configuration missing!") + exit(1) + + if 'app_token' not in config['pnut']: + click.echo("Pnut app token missing from configuration!") + exit(1) + endpoint = f"/streams" headers = { "Content-Type": "application/json", - "Authorization": "Bearer " + config['PNUT_APPTOKEN'] + "Authorization": "Bearer " + config['pnut']['app_token'] } url = f"{PNUT_API}{endpoint}" @@ -97,10 +221,18 @@ def rm_streams(ctx): @click.pass_context def new_stream(ctx, key, object_types): config = ctx.obj['config'] + if 'pnut' not in config: + click.echo("Pnut configuration missing!") + exit(1) + + if 'app_token' not in config['pnut']: + click.echo("Pnut app token missing from configuration!") + exit(1) + endpoint = f"/streams" headers = { "Content-Type": "application/json", - "Authorization": "Bearer " + config['PNUT_APPTOKEN'] + "Authorization": "Bearer " + config['pnut']['app_token'] } otypes = [x.strip() for x in object_types.split(',')] data = { @@ -124,10 +256,18 @@ def new_stream(ctx, key, object_types): @click.pass_context def update_stream(ctx, key, object_types): config = ctx.obj['config'] + if 'pnut' not in config: + click.echo("Pnut configuration missing!") + exit(1) + + if 'app_token' not in config['pnut']: + click.echo("Pnut app token missing from configuration!") + exit(1) + endpoint = f"/streams/{key}" headers = { "Content-Type": "application/json", - "Authorization": "Bearer " + config['PNUT_APPTOKEN'] + "Authorization": "Bearer " + config['pnut']['app_token'] } otypes = [x.strip() for x in object_types.split(',')] data = { @@ -171,18 +311,29 @@ def pnut_app_token(ctx): # click.echo(r.text) @cmd.command() -@click.argument('username') @click.pass_context -def new_matrix_asuser(ctx, username): +def create_matrix_asuser(ctx): config = ctx.obj['config'] + if 'matrix' not in config: + click.echo("Matrix configuration missing!") + exit(1) + + if 'as_token' not in config['matrix']: + click.echo("matrix appservice token missing from configuration!") + exit(1) + + if 'sender_local' not in config['matrix']: + click.echo("matrix local sender missing from configuration!") + exit(1) + endpoint = "/_matrix/client/v3/register" - url = config['MATRIX_HOST'] + endpoint + url = config['matrix']['homeserver'] + endpoint headers = { "Content-Type": "application/json", - "Authorization": "Bearer " + config['MATRIX_AS_TOKEN'] + "Authorization": "Bearer " + config['matrix']['as_token'] } params = {'kind': 'user'} - data = {'type': 'm.login.application_service','username': username} + data = {'type': 'm.login.application_service','username': config['matrix']['sender_local']} r = requests.post(url, headers=headers, json=data, params=params) if r.status_code == 200: @@ -195,23 +346,36 @@ def new_matrix_asuser(ctx, username): @cmd.command() @click.argument('invitee') @click.pass_context -def new_pnut_admin_room(ctx, invitee): +def create_pnut_admin_room(ctx, invitee): config = ctx.obj['config'] + if 'matrix' not in config: + click.echo("Matrix configuration missing!") + exit(1) + + if 'as_token' not in config['matrix']: + click.echo("matrix appservice token missing from configuration!") + exit(1) + + if 'namespace' not in config['matrix']: + click.echo("matrix namespace missing from configuration!") + exit(1) + + as_id = f"@{config['matrix']['sender_local']}:{config['matrix']['domain']}" endpoint = "/_matrix/client/v3/createRoom" - url = config['MATRIX_HOST'] + endpoint + url = config['matrix']['homeserver'] + endpoint headers = { "Content-Type": "application/json", - "Authorization": "Bearer " + config['MATRIX_AS_TOKEN'] + "Authorization": "Bearer " + config['matrix']['as_token'] } data = { 'visibility': "private", 'is_direct': False, 'name': "Pnut Bridge Admin Room", - 'room_alias_name': f"{config['MATRIX_PNUT_PREFIX']}admin", + 'room_alias_name': f"{config['matrix']['namespace']}admin", 'invite': [invitee], 'power_level_content_override': { 'users': { - f"{config['MATRIX_AS_ID']}": 100, + f"{as_id}": 100, f"{invitee}": 100 } } @@ -228,13 +392,25 @@ def new_pnut_admin_room(ctx, invitee): @cmd.command() @click.pass_context -def new_pnut_global_room(ctx): +def create_pnut_global_room(ctx): config = ctx.obj['config'] + if 'matrix' not in config: + click.echo("Matrix configuration missing!") + exit(1) + + if 'as_token' not in config['matrix']: + click.echo("matrix appservice token missing from configuration!") + exit(1) + + if 'namespace' not in config['matrix']: + click.echo("matrix namespace missing from configuration!") + exit(1) + endpoint = "/_matrix/client/v3/createRoom" - url = config['MATRIX_HOST'] + endpoint + url = config['matrix']['homeserver'] + endpoint headers = { "Content-Type": "application/json", - "Authorization": "Bearer " + config['MATRIX_AS_TOKEN'] + "Authorization": "Bearer " + config['matrix']['as_token'] } # data = { # 'visibility': "public", @@ -248,7 +424,7 @@ def new_pnut_global_room(ctx): data = { 'visibility': "public", 'name': "Pnut Global Stream", - 'room_alias_name': f"{config['MATRIX_PNUT_PREFIX']}global" + 'room_alias_name': f"{config['matrix']['namespace']}global" } logging.debug(data) r = requests.post(url, headers=headers, json=data) @@ -267,15 +443,24 @@ def new_pnut_global_room(ctx): @click.pass_context def elevate_matrix_user(ctx, room_id, matrix_id, power_level): config = ctx.obj['config'] + if 'matrix' not in config: + click.echo("Matrix configuration missing!") + exit(1) + + if 'as_token' not in config['matrix']: + click.echo("matrix appservice token missing from configuration!") + exit(1) + + as_id = f"@{config['matrix']['sender_local']}:{config['matrix']['domain']}" endpoint = f"/_matrix/client/v3/rooms/{room_id}/state/" url = config['MATRIX_HOST'] + endpoint + "m.room.power_levels" headers = { "Content-Type": "application/json", - "Authorization": "Bearer " + config['MATRIX_AS_TOKEN'] + "Authorization": "Bearer " + config['matrix']['as_token'] } data = { 'users': { - f"{config['MATRIX_AS_ID']}": 100, + f"{as_id}": 100, f"{matrix_id}": int(power_level) } } @@ -293,9 +478,18 @@ def elevate_matrix_user(ctx, room_id, matrix_id, power_level): @click.pass_context async def list_joined_rooms(ctx): config = ctx.obj['config'] - matrix_api = ClientAPI(config['MATRIX_AS_ID'], - base_url=config['MATRIX_HOST'], - token=config['MATRIX_AS_TOKEN']) + if 'matrix' not in config: + click.echo("Matrix configuration missing!") + exit(1) + + if 'as_token' not in config['matrix']: + click.echo("matrix appservice token missing from configuration!") + exit(1) + + as_id = f"@{config['matrix']['sender_local']}:{config['matrix']['domain']}" + matrix_api = ClientAPI(as_id, + base_url=config['matrix']['homeserver'], + token=config['matrix']['as_token']) room_list = await matrix_api.get_joined_rooms() for room_id in room_list: click.echo(room_id) @@ -323,9 +517,18 @@ async def list_joined_rooms(ctx): @click.pass_context async def part_room(ctx, room_id): config = ctx.obj['config'] - matrix_api = ClientAPI(config['MATRIX_AS_ID'], - base_url=config['MATRIX_HOST'], - token=config['MATRIX_AS_TOKEN']) + if 'matrix' not in config: + click.echo("Matrix configuration missing!") + exit(1) + + if 'as_token' not in config['matrix']: + click.echo("matrix appservice token missing from configuration!") + exit(1) + + as_id = f"@{config['matrix']['sender_local']}:{config['matrix']['domain']}" + matrix_api = ClientAPI(as_id, + base_url=config['matrix']['homeserver'], + token=config['matrix']['as_token']) # TODO: need to clear alias await matrix_api.leave_room(room_id) diff --git a/src/pnut_matrix/pnutservice.py b/src/pnut_matrix/pnutservice.py index 9de0002..652c5a2 100644 --- a/src/pnut_matrix/pnutservice.py +++ b/src/pnut_matrix/pnutservice.py @@ -2,6 +2,7 @@ import time import logging import logging.config import yaml +import tomlkit import json import pnutpy import requests @@ -25,7 +26,6 @@ from pnut_matrix.models import * logger = logging.getLogger() config = None -matrix_url = None class MLogFilter(logging.Filter): @@ -52,16 +52,18 @@ async def new_pnut_message(msg, meta): logger.debug("text: " + msg.content.text) # ignore messages posted by the bridge - if msg.user.username == config['MATRIX_PNUT_USER']: + if msg.user.username == config['pnut']['bot_user']: return - if msg.source.id == config['PNUTCLIENT_ID']: + if msg.source.id == config['pnut']['client_id']: return + as_id = (f"@{config['matrix']['sender_local']}:" + f"{config['matrix']['domain']}") matrix_id = matrix_id_from_pnut(msg.user.username) - matrix_api = ClientAPI(config['MATRIX_AS_ID'], - base_url=config['MATRIX_HOST'], - token=config['MATRIX_AS_TOKEN'], + matrix_api = ClientAPI(as_id, + base_url=config['matrix']['homeserver'], + token=config['matrix']['as_token'], as_user_id=matrix_id.lower()) channel_id = int(msg.channel_id) @@ -118,9 +120,9 @@ async def new_pnut_message(msg, meta): logger.debug('-set_avatar-') if room.is_private: - matrix_api_as = ClientAPI(config['MATRIX_AS_ID'], - base_url=config['MATRIX_HOST'], - token=config['MATRIX_AS_TOKEN']) + matrix_api_as = ClientAPI(as_id, + base_url=config['matrix']['homeserver'], + token=config['matrix']['as_token']) await matrix_api_as.invite_user(room.room_id, matrix_id.lower()) await matrix_api.join_room(room.room_id) @@ -145,10 +147,10 @@ async def new_pnut_message(msg, meta): async def new_pnut_post(post, meta): - if not config['PNUT_GLOBAL']: + if not config['pnut']['global_stream']: return - if (config['PNUT_GLOBAL_HUMAN_ONLY'] and + if (config['pnut']['global_humans_only'] and post.user.type in ['feed', 'bot']): logging.debug('-skipping non human post-') return @@ -160,10 +162,12 @@ async def new_pnut_post(post, meta): text += f"<{post.user.username}> reposted >> " post = post.repost_of + as_id = (f"@{config['matrix']['sender_local']}:" + f"{config['matrix']['domain']}") matrix_id = matrix_id_from_pnut(post.user.username) - matrix_api = ClientAPI(config['MATRIX_AS_ID'], - base_url=config['MATRIX_HOST'], - token=config['MATRIX_AS_TOKEN'], + matrix_api = ClientAPI(as_id, + base_url=config['matrix']['homeserver'], + token=config['matrix']['as_token'], as_user_id=matrix_id.lower()) try: profile = await matrix_api.get_profile(matrix_id.lower()) @@ -184,7 +188,7 @@ async def new_pnut_post(post, meta): await set_matrix_avatar(post.user) logger.debug('-set_avatar-') - room_id = config['MATRIX_GLOBAL_ROOM'] + room_id = config['pnut']['global_room'] await matrix_api.join_room(room_id) postlink = f"https://posts.pnut.io/{post.id}" plaintext = f"{post.content.text}\n{postlink}" @@ -209,9 +213,11 @@ async def new_pnut_post(post, meta): async def new_media(room_id, msg): matrix_id = matrix_id_from_pnut(msg.user.username) - matrix_api = ClientAPI(config['MATRIX_AS_ID'], - base_url=config['MATRIX_HOST'], - token=config['MATRIX_AS_TOKEN'], + as_id = (f"@{config['matrix']['sender_local']}:" + f"{config['matrix']['domain']}") + matrix_api = ClientAPI(as_id, + base_url=config['matrix']['homeserver'], + token=config['matrix']['as_token'], as_user_id=matrix_id.lower()) if 'io.pnut.core.oembed' in msg.raw: @@ -275,9 +281,11 @@ async def new_media(room_id, msg): async def delete_message(msg): matrix_id = matrix_id_from_pnut(msg.user.username) - matrix_api = ClientAPI(config['MATRIX_AS_ID'], - base_url=config['MATRIX_HOST'], - token=config['MATRIX_AS_TOKEN'], + as_id = (f"@{config['matrix']['sender_local']}:" + f"{config['matrix']['domain']}") + matrix_api = ClientAPI(as_id, + base_url=config['matrix']['homeserver'], + token=config['matrix']['as_token'], as_user_id=matrix_id.lower()) events = Events.select().where((Events.pnut_id == msg.id) & @@ -288,8 +296,8 @@ async def delete_message(msg): event.save() def matrix_id_from_pnut(username): - matrix_id = (f"@{config['MATRIX_PNUT_PREFIX']}{username}" - f":{config['MATRIX_DOMAIN']}") + matrix_id = (f"@{config['matrix']['namespace']}{username}" + f":{config['matrix']['domain']}") return matrix_id def matrix_display_from_pnut(user): @@ -308,17 +316,21 @@ def matrix_display_from_pnut(user): async def set_matrix_display(user): matrix_id = matrix_id_from_pnut(user.username) - matrix_api = ClientAPI(config['MATRIX_AS_ID'], - base_url=config['MATRIX_HOST'], - token=config['MATRIX_AS_TOKEN'], + as_id = (f"@{config['matrix']['sender_local']}:" + f"{config['matrix']['domain']}") + matrix_api = ClientAPI(as_id, + base_url=config['matrix']['homeserver'], + token=config['matrix']['as_token'], as_user_id=matrix_id.lower()) await matrix_api.set_displayname(matrix_display_from_pnut(user)) async def set_matrix_avatar(user): matrix_id = matrix_id_from_pnut(user.username) - matrix_api = ClientAPI(config['MATRIX_AS_ID'], - base_url=config['MATRIX_HOST'], - token=config['MATRIX_AS_TOKEN'], + as_id = (f"@{config['matrix']['sender_local']}:" + f"{config['matrix']['domain']}") + matrix_api = ClientAPI(as_id, + base_url=config['matrix']['homeserver'], + token=config['matrix']['as_token'], as_user_id=matrix_id.lower()) dl = requests.get(user.content.avatar_image.url, stream=True) @@ -347,15 +359,15 @@ async def set_matrix_avatar(user): def new_matrix_user(username): endpoint = "/_matrix/client/v3/register" - url = config['MATRIX_HOST'] + endpoint + url = config['matrix']['homeserver'] + endpoint params = {'kind': 'user'} data = { 'type': 'm.login.application_service', - 'username': config['MATRIX_PNUT_PREFIX'] + username + 'username': config['matrix']['namespace'] + username } headers = { "Content-Type": "application/json", - "Authorization": "Bearer " + config['MATRIX_AS_TOKEN'] + "Authorization": "Bearer " + config['matrix']['as_token'] } logger.debug(data) r = requests.post(url, headers=headers, json=data, params=params) @@ -371,9 +383,9 @@ def new_matrix_user(username): def new_room(pnut_user, invitees, chan): dr = None - url = matrix_url + '/createRoom' + url = config['matrix']['homeserver'] + '/_matrix/client/v3/createRoom' params = { - "access_token": config['MATRIX_AS_TOKEN'], + "access_token": config['matrix']['as_token'], "user_id": pnut_user.lower() } content = { @@ -434,15 +446,15 @@ async def on_message(message): await new_pnut_post(pnut_post, meta) async def asmain(): - if config['MATRIX_ADMIN_ROOM']: - logger.debug("- sould join admin room -") - matrix_api_as = ClientAPI(config['MATRIX_AS_ID'], - base_url=config['MATRIX_HOST'], - token=config['MATRIX_AS_TOKEN']) - await matrix_api_as.join_room(config['MATRIX_ADMIN_ROOM']) + # if config['MATRIX_ADMIN_ROOM']: + # logger.debug("- sould join admin room -") + # matrix_api_as = ClientAPI(config['MATRIX_AS_ID'], + # base_url=config['MATRIX_HOST'], + # token=config['MATRIX_AS_TOKEN']) + # await matrix_api_as.join_room(config['MATRIX_ADMIN_ROOM']) ws_url = 'wss://stream.pnut.io/v1/app?access_token=' - ws_url += config['PNUT_APPTOKEN'] + '&key=' + config['PNUT_APPKEY'] + ws_url += config['pnut']['app_token'] + '&key=' + config['pnut']['app_key'] ws_url += '&include_raw=1' async for websocket in connect(uri=ws_url): try: @@ -456,25 +468,28 @@ async def asmain(): def main(): global config - global matrix_url a_parser = argparse.ArgumentParser() - a_parser.add_argument('-c', '--config', dest='configyaml', - default="config.yaml", help="configuration file") + a_parser.add_argument('-c', '--config', dest='config', + default="config.toml", help="configuration file") + a_parser.add_argument('-l', '--log_config', dest='log_config', + default='logging-config.yaml', + help='logging configuration') args = a_parser.parse_args() - with open(args.configyaml, "rb") as config_file: - config = yaml.load(config_file, Loader=yaml.SafeLoader) + with open(args.config, 'rb') as config_file: + config = tomlkit.load(config_file) - db.init(config['SERVICE_DB']) + with open(args.log_config, 'rb') as log_config_file: + log_config = yaml.load(log_config_file, Loader=yaml.SafeLoader) + + db.init(config['database']) db_create_tables() - logging.config.dictConfig(config['logging']) + logging.config.dictConfig(log_config) redact_filter = MLogFilter() logging.getLogger("werkzeug").addFilter(redact_filter) logging.getLogger("urllib3.connectionpool").addFilter(redact_filter) - matrix_url = config['MATRIX_HOST'] + '/_matrix/client/v3' - asyncio.run(asmain()) logger.info('!! shutdown initiated !!')