diff --git a/pyproject.toml b/pyproject.toml index 1756174..8fcd659 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,11 +17,11 @@ dependencies = [ "requests", "Flask[async]", "pnutpy>=0.5.0", - "sqlalchemy", "filemagic", "mautrix>=0.20.6,<0.21", "websockets", "asyncclick", + "peewee", ] [project.urls] diff --git a/src/pnut_matrix/appservice.py b/src/pnut_matrix/appservice.py index 5e82f86..806175a 100644 --- a/src/pnut_matrix/appservice.py +++ b/src/pnut_matrix/appservice.py @@ -11,10 +11,7 @@ import os from mautrix.client import ClientAPI from mautrix.types import * - -from pnut_matrix.models import Avatars, Rooms, Events, Users, DirectRooms, ControlRooms -from pnut_matrix.database import db_session -from sqlalchemy import and_ +from pnut_matrix.models import * from flask import Flask, jsonify, request, abort logger = logging.getLogger(__name__) @@ -29,10 +26,6 @@ def not_found(error): def forbidden(error): return jsonify({'errcode':'PNUT_FORBIDDEN'}), 403 -@app.teardown_appcontext -def shutdown_session(exception=None): - db_session.remove() - @app.route("/_matrix/app/v1/rooms/") @app.route("/rooms/") async def query_alias(alias): @@ -40,7 +33,8 @@ async def query_alias(alias): alias_localpart = alias.split(":")[0][1:] channel_id = int(alias_localpart.split('_')[1]) - room = Rooms.query.filter(Rooms.pnut_chan == channel_id).one_or_none() + room = PnutChannels.select().where(PnutChannels.pnut_chan == + channel_id).first() if room is not None: abort(404) @@ -58,9 +52,9 @@ async def query_alias(alias): if 'io.pnut.core.chat-settings' in channel.raw: for setting in channel.raw['io.pnut.core.chat-settings']: if 'name' in setting: - name = setting['name'] + name = f"🥜 {setting['name']}" else: - name = None + name = f"🥜 channel {channel.id}" if 'description' in setting: topic = setting['description']['text'] else: @@ -84,15 +78,11 @@ async def query_alias(alias): if not channel.you_subscribed: pnutpy.api.subscribe_channel(channel_id) - rr = Rooms( - room_id=room_id, - pnut_chan=channel_id, - portal=True - ) - logging.debug(rr.room_id) - logging.debug(rr.pnut_chan) - db_session.add(rr) - db_session.commit() + + room = PnutChannels(room_id=room_id, pnut_chan=channel_id) + room.save() + logging.debug(f'-created new room for channel {room.pnut_chan}-') + logging.debug(room.room_id) except pnutpy.errors.PnutPermissionDenied: logging.debug("-permission denied-") @@ -114,23 +104,25 @@ async def on_receive_events(transaction): events = request.get_json()["events"] for event in events: + logging.debug('-----event-----') 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({}) - user = Users.query.filter(Users.matrix_id == - event['sender']).one_or_none() + user = PnutUsers.select().where(PnutUsers.matrix_id == + event['sender']).first() if event['type'] == 'm.room.message': + logging.debug('>----new_message----<') await new_message(event, user) - # elif event['type'] == 'm.sticker': - # new_sticker(event, user) - elif event['type'] == 'm.room.redaction': + logging.debug('>----delete_message----<') delete_message(event, user) elif event['type'] == 'm.room.member': @@ -138,59 +130,55 @@ async def on_receive_events(transaction): 'membership' in event['content']): if (event['content']['membership'] == "invite" and event['content']['is_direct']): - logging.debug('----> direct invite <----') + logging.debug('>----on_direct_invite----<') await on_direct_invite(event) return jsonify({}) if 'membership' in event['content']: if event['content']['membership'] == "leave": - logging.debug('----> leave event <----') + logging.debug('>----on_leave_event----<') await on_leave_event(event) return jsonify({}) - logging.debug("----room member event----") - logging.debug(user) - logging.debug(event) - return jsonify({}) async def new_message(event, user): - if (app.config['MATRIX_PNUT_PREFIX'] in event['sender'] or - 'pnut-bridge' in event['sender']): - logging.debug('-skipping dup event-') + + if event['sender'] == app.config['MATRIX_AS_ID']: return - if 'msgtype' not in event['content']: - logging.debug('-unknown message type-') + if app.config['MATRIX_PNUT_PREFIX'] in event['sender']: return - control = ControlRooms.query.filter(ControlRooms.room_id == - event['room_id']).one_or_none() - if control is not None: - await on_control_message(event) + if user.room_id == event['room_id']: + await on_control_message(event, user) return - direct = DirectRooms.query.filter(DirectRooms.room_id == - event['room_id']).one_or_none() - if direct is not None: - return on_direct_message(event, user, direct) + room = PnutChannels.select().where(PnutChannels.room_id == + event['room_id']).first() + logging.debug(f'room: {room}') - room = Rooms.query.filter(Rooms.room_id == event['room_id']).one_or_none() if room is None: logging.debug('-room not mapped-') return + if room.is_direct: + logging.debug('>----on_direct_message----<') + return on_direct_message(event, user, room) + if user is not None: token = user.pnut_user_token prefix = "" + else: token = app.config['MATRIX_PNUT_TOKEN'] matrix_profile = get_profile(event['sender']) - if "displayname" in matrix_profile: + if ('displayname' in matrix_profile): prefix = (f"[{matrix_profile['displayname']}]" f" ({event['sender']})\n") else: prefix = "(" + event['sender'] + ")\n" + pnutpy.api.add_authorization_token(token) raw = {} @@ -204,16 +192,13 @@ async def new_message(event, user): try: msg, meta = pnutpy.api.create_message(room.pnut_chan, data={'text': text, 'raw': raw}) - revent = Events( + bridge_event = Events( event_id=event['event_id'], room_id=event['room_id'], - pnut_msg_id=msg.id, - pnut_user_id=msg.user.id, - pnut_chan_id=room.pnut_chan, - deleted=False + pnut_id=msg.id, + pnut_channel=room.pnut_chan ) - db_session.add(revent) - db_session.commit() + bridge_event.save() # TODO: need to redo this for global message # if user is not None: @@ -362,8 +347,8 @@ def delete_message(event, user): token = app.config['MATRIX_PNUT_TOKEN'] pnutpy.api.add_authorization_token(token) - e = Events.query.filter(and_(Events.event_id == event['redacts'], - Events.deleted == False)).one_or_none() + e = Events.select().where((Events.event_id == events['redacts']) & + (Events.deleted == False)).first() if e is None: logging.debug("- can't find the event to remove -") return @@ -371,7 +356,7 @@ def delete_message(event, user): try: r, meta = pnutpy.api.delete_message(e.pnut_chan_id, e.pnut_msg_id) e.deleted = True - db_session.commit() + e.save() except pnutpy.errors.PnutPermissionDenied as e: pass @@ -406,9 +391,11 @@ async def create_pnut_matrix_room(channel, user): if channel.acl.read.public: visibility = RoomDirectoryVisibility.PUBLIC preset = RoomCreatePreset.PUBLIC + is_private = False else: visibility = RoomDirectoryVisibility.PRIVATE preset = RoomCreatePreset.PRIVATE + is_private = True if 'io.pnut.core.chat-settings' in channel.raw: for setting in channel.raw['io.pnut.core.chat-settings']: @@ -428,9 +415,20 @@ async def create_pnut_matrix_room(channel, user): name=name, topic=topic) - rr = Rooms(room_id=room_id, pnut_chan=channel.id, portal=True) - db_session.add(rr) - db_session.commit() + room = PnutChannels(room_id=room_id, pnut_chan=channel.id, + is_private=is_private) + room.save() + + if is_private: + chan_member = PnutPrivateChanMembers(pnut_chan=channel.id, + room_id=room_id, + pnut_user_id=user.pnut_user_id, + matrix_id=user.matrix_id) + chan_member.save() + + logging.debug('-create_pnut_matrix_room-') + logging.debug(f'-created new room for channel {room.pnut_chan}-') + logging.debug(room.room_id) def new_matrix_user(username): endpoint = "/_matrix/client/v3/register" @@ -479,22 +477,6 @@ async def on_admin_event(event): elif msg[0] == 'list': await matrix_api.send_message(event['room_id'], cmd_admin_list()) - elif msg[0] == 'unlink': - if len(msg) > 1: - await matrix_api.send_message(event['room_id'], - cmd_admin_unlink(msg[1])) - else: - await matrix_api.send_message(event['room_id'], - cmd_admin_help('unlink')) - - elif msg[0] == 'link': - if len(msg) > 2: - await matrix_api.send_message(event['room_id'], - cmd_admin_link(msg[1], msg[2])) - else: - await matrix_api.send_message(event['room_id'], - cmd_admin_help('link')) - except Exception: errmsg = "- on_admin_event -" logging.exception(errmsg) @@ -504,10 +486,6 @@ def cmd_admin_help(cmd=None): help_desc = "Show information about available commands." list_usage = "list" list_desc = "List the rooms currently linked with pnut.io." - unlink_usage = "unlink | " - unlink_desc = "Unlink a room between Matrix and pnut.io." - link_usage = "link " - link_desc = "Link a room between Matrix and pnut.io." if cmd == 'help': text = "usage: " + help_usage + "\n\n" @@ -532,7 +510,7 @@ def cmd_admin_help(cmd=None): def cmd_admin_list(): text = "" - rooms = Rooms.query.all() + rooms = PnutChannels.select() if len(rooms) > 0: text = "ID\tMATRIX ID\tPNUT CHANNEL\n" @@ -543,108 +521,21 @@ def cmd_admin_list(): text += str(room.id) + '\t' text += room.room_id + '\t\t\t\t\t' text += str(room.pnut_chan) + '\t' - if room.portal: - text += "(portal)" text += '\n' return TextMessageEventContent(msgtype='m.text', body=text) -async def cmd_admin_link(room_id, pnut_chan_id): - matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], - base_url=app.config['MATRIX_HOST'], - token=app.config['MATRIX_AS_TOKEN']) - pnutpy.api.add_authorization_token(app.config['MATRIX_PNUT_TOKEN']) - - mrcheck = Rooms.query.filter(Rooms.room_id == room_id).one_or_none() - pncheck = Rooms.query.filter(Rooms.pnut_chan == pnut_chan_id).one_or_none() - - if mrcheck is not None or pncheck is not None: - text = "- room may already be linked -" - return TextMessageEventContent(msgtype='m.text', body=text) - - try: - channel, meta = pnutpy.api.subscribe_channel(pnut_chan_id) - await matrix_api.join_room(room_id) - - rec = Rooms( - room_id=room_id, - pnut_chan=channel.id, - portal=False - ) - db_session.add(rec) - db_session.commit() - - except pnutpy.errors.PnutAuthAPIException: - errmsg = "- unable to subscribe to channel -" - logging.exception(errmsg) - return TextMessageEventContent(msgtype='m.text', body=errmsg) - - except Exception: - errmsg = "- unable to link room for some reason -" - logging.exception(errmsg) - return TextMessageEventContent(msgtype='m.text', body=errmsg) - -async def cmd_admin_unlink(rid): - matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], - base_url=app.config['MATRIX_HOST'], - token=app.config['MATRIX_AS_TOKEN']) - pnutpy.api.add_authorization_token(app.config['MATRIX_PNUT_TOKEN']) - - if rid.startswith('!'): - room = Rooms.query.filter(Rooms.room_id == rid).one_or_none() - else: - room = Rooms.query.filter(Rooms.pnut_chan == rid).one_or_none() - - if hasattr(room, 'portal'): - if room.portal: - alias = "#" + app.config['MATRIX_PNUT_PREFIX'] - alias += str(room.pnut_chan) + ":" - alias += app.config['MATRIX_DOMAIN'] - await matrix_api.remove_room_alias(alias) - - # Kicking users needs at least moderator privs - members = await matrix_api.get_members(room.room_id) - reason = "Portal room has been unlinked by administrator" - for m in members['chunk']: - if (m['content']['membership'] == 'join' and - m['sender'] != app.config['MATRIX_AS_ID']): - if room.portal: - await matrix_api.kick_user(room.room_id, - m['sender'], - reason=reason) - else: - prefix = f"@{app.config['MATRIX_PNUT_PREFIX']}" - if m['sender'].startswith(prefix): - await matrix_api.kick_user(room.room_id, - m['sender'], - reason=reason) - - try: - channel, meta = pnutpy.api.unsubscribe_channel(room.pnut_chan) - await matrix_api.leave_room(room.room_id) - - if room is not None: - db_session.delete(room) - db_session.commit() - text = "- room has been unlinked -" - else: - text = "- unable to locate room to unlink -" - - return TextMessageEventContent(msgtype='m.text', body=text) - - except Exception: - errmsg = "- error while unlinking room -" - logging.exception(errmsg) - return TextMessageEventContent(msgtype='m.text', body=errmsg) - async def on_direct_invite(event): + # 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']) - dm = ControlRooms(room_id=event['room_id']) - else: + 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'], @@ -654,62 +545,76 @@ async def on_direct_invite(event): pnut_user = bridge_user.replace(app.config['MATRIX_PNUT_PREFIX'], '').split(':')[0] - user = Users.query.filter(Users.matrix_id == - event['sender']).one_or_none() - if user is not None: - # TODO: need to handle if the user isn't registered - pnutpy.api.add_authorization_token(user.pnut_user_token) - channel, meta = pnutpy.api.existing_pm(ids=pnut_user) - new_matrix_user(pnut_user) + user = PnutUsers.select().where(PnutUsers.matrix_id == + event['sender']).first() - dm = DirectRooms(room_id=event['room_id'], - bridge_user=bridge_user, pnut_chan=channel.id) + if user is not None: + pnutpy.api.add_authorization_token(user.pnut_user_token) + + try: + channel, meta = pnutpy.api.existing_pm(ids=pnut_user) + new_matrix_user(pnut_user) + + dm = PnutChannels(pnut_chan=channel.id, + room_id=event['room_id'], + is_direct=True, + direct_pnut_user=bridge_user, + direct_mtrx_user=user.matrix_id) + + except pnutpy.errors.PnutAuthAPIException: + abort(403) + + else: + abort(403) + + else: + return try: logging.debug('--> trying to join room <--') await matrix_api.join_room_by_id(event['room_id']) - - db_session.add(dm) - db_session.commit() + dm.save() except Exception: errmsg = "- on_direct_invite -" logging.exception(errmsg) async def on_leave_event(event): - direct = DirectRooms.query.filter(DirectRooms.room_id == - event['room_id']).one_or_none() - if direct is not None: + direct_room = PnutChannels.select().where(PnutChannels.room_id == + event['room_id']).first() + + user = PnutUsers.select().where(PnutUsers.room_id == + event['room_id']).first() + + 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'], - as_user_id=direct.bridge_user.lower()) + as_user_id=direct_room.direct_pnut_user.lower()) try: await matrix_api.leave_room(event['room_id']) - db_session.delete(direct) - db_session.commit() + direct_room.delete_instance() except Exception: errmsg = "- on_leave_event -" logging.exception(errmsg) - control = ControlRooms.query.filter(ControlRooms.room_id == - event['room_id']).one_or_none() - if control is not None: + if user is not None: matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], base_url=app.config['MATRIX_HOST'], token=app.config['MATRIX_AS_TOKEN']) try: await matrix_api.leave_room(event['room_id']) - db_session.delete(control) - db_session.commit() + user.room_id = None + user.save() except Exception: errmsg = "- on_leave_event -" logging.exception(errmsg) def on_direct_message(event, user, room): + if user is not None: token = user.pnut_user_token prefix = "" @@ -727,19 +632,17 @@ def on_direct_message(event, user, room): raw['io.pnut.core.crosspost'] = [crosspost_raw(event)] evtext, evraw = msg_from_event(event) text = prefix + evtext + try: msg, meta = pnutpy.api.create_message(room.pnut_chan, data={'text': text, 'raw': raw}) - revent = Events( + bridge_event = Events( event_id=event['event_id'], room_id=event['room_id'], - pnut_msg_id=msg.id, - pnut_user_id=msg.user.id, - pnut_chan_id=room.pnut_chan, - deleted=False + pnut_id=msg.id, + pnut_channel=room.pnut_chan ) - db_session.add(revent) - db_session.commit() + bridge_event.save() except pnutpy.errors.PnutAuthAPIException: logging.exception('-unable to post to pnut channel-') @@ -749,11 +652,10 @@ def on_direct_message(event, user, room): return jsonify({}) -async def on_control_message(event): +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']) - logging.debug("- direct room event received -") if event['type'] != 'm.room.message': return jsonify({}) @@ -775,26 +677,25 @@ async def on_control_message(event): elif msg[0] == '!save': if len(msg) > 1: await matrix_api.send_message(event['room_id'], - cmd_user_save(event['sender'], - msg[1])) + cmd_user_save(user, msg[1])) else: await matrix_api.send_message(event['room_id'], cmd_user_save()) elif msg[0] == '!drop': - await matrix_api.send_message(event['room_id'], - cmd_user_drop(event['sender'])) + r = await cmd_user_drop(user) + await matrix_api.send_message(event['room_id'], r) elif msg[0] == '!status': await matrix_api.send_message(event['room_id'], - cmd_user_status(event['sender'])) + cmd_user_status(user)) elif msg[0] == '!join': if len(msg) > 1: - r = await cmd_user_join(event['sender'], msg[1]) + r = await cmd_user_join(user, msg[1]) await matrix_api.send_message(event['room_id'], r) else: - r = await cmd_user_join(event['sender']) + r = await cmd_user_join(user) await matrix_api.send_message(event['room_id'], r) except Exception: @@ -822,7 +723,7 @@ def cmd_user_auth(): return TextMessageEventContent(msgtype='m.text', body=reply) -def cmd_user_save(sender=None, token=None): +def cmd_user_save(user, token=None): if token is None: reply = "You must provide a token with this command.\n" reply += "!save " @@ -832,13 +733,9 @@ def cmd_user_save(sender=None, token=None): try: response, meta = pnutpy.api.get_user('me') - user = Users( - matrix_id=sender, - pnut_user_id=response.id, - pnut_user_token=token - ) - db_session.add(user) - db_session.commit() + user.pnut_user_id = response.id + user.pnut_user_token = token + user.save() reply = "Success! You are now authorized as " + response['username'] @@ -851,25 +748,37 @@ def cmd_user_save(sender=None, token=None): return TextMessageEventContent(msgtype='m.text', body=reply) -def cmd_user_drop(sender=None): - try: - user = Users.query.filter(Users.matrix_id == sender).one_or_none() - if user is not None: - db_session.delete(user) - db_session.commit() - reply = "Success! Your auth token has been removed." - else: - reply = "You do not appear to be registered." +async def cmd_user_drop(user): - except Exception as e: - logging.exception('!drop') - reply = "Error! Problem removing your token." + 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'], + as_user_id=dir_room.direct_pnut_user.lower()) + await matrix_api.leave_room(dir_room.room_id) + dir_room.delete_instance() + + 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']) + await matrix_api.kick_user(priv_room.room_id, user.matrix_id, + reason='user left from bridge') + priv_room.delete_instance() + + user.pnut_user_id = None + user.pnut_user_token = None + user.save() + reply = "Success! Your auth token has been removed." return TextMessageEventContent(msgtype='m.text', body=reply) -def cmd_user_status(sender=None): +def cmd_user_status(user): try: - user = Users.query.filter(Users.matrix_id == sender).one_or_none() if user is None: reply = "You are currently not authorized on pnut.io" else: @@ -886,28 +795,27 @@ def cmd_user_status(sender=None): return TextMessageEventContent(msgtype='m.text', body=reply) -async def cmd_user_join(sender=None, channel_id=None): +async def cmd_user_join(user, channel_id=None): if channel_id is None: reply = "You must provide a channel id number with this command.\n" reply += "!join " return TextMessageEventContent(msgtype='m.text', body=reply) try: - user = Users.query.filter(Users.matrix_id == sender).one_or_none() if user is None: reply = "You are currently not authorized on pnut.io" else: pnutpy.api.add_authorization_token(user.pnut_user_token) channel, meta = pnutpy.api.get_channel(channel_id, include_raw=1) - room = Rooms.query.filter(Rooms.pnut_chan == - channel_id).one_or_none() + room = PnutChannels.select().where(PnutChannels.pnut_chan == + channel_id).first() 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']) - await matrix_api.invite_user(room.room_id, sender) + await matrix_api.invite_user(room.room_id, user.matrix_id) reply = "ok" except pnutpy.errors.PnutAuthAPIException as e: @@ -952,6 +860,10 @@ def main(): app.config.update(config) logging.basicConfig(level=logging.DEBUG) + + db.init(config['SERVICE_DB']) + db_create_tables() + app.run(host=config['LISTEN_HOST'], port=config['LISTEN_PORT']) if __name__ == '__main__': diff --git a/src/pnut_matrix/database.py b/src/pnut_matrix/database.py deleted file mode 100644 index 2260a33..0000000 --- a/src/pnut_matrix/database.py +++ /dev/null @@ -1,24 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import scoped_session, sessionmaker -from sqlalchemy.ext.declarative import declarative_base -import yaml -import os - -configyaml = os.environ.get("CONFIG_FILE") - -with open(configyaml, "rb") as config_file: - config = yaml.load(config_file, Loader=yaml.SafeLoader) - -engine = create_engine(config['SERVICE_DB']) -db_session = scoped_session(sessionmaker(bind=engine)) - -Base = declarative_base() - -Base.query = db_session.query_property() - -def init_db(): - # import all modules here that might define models so that - # they will be registered properly on the metadata. Otherwise - # you will have to import them first before calling init_db() - import pnut_matrix.models - Base.metadata.create_all(bind=engine) diff --git a/src/pnut_matrix/models.py b/src/pnut_matrix/models.py index 2e422f6..80ce5a0 100644 --- a/src/pnut_matrix/models.py +++ b/src/pnut_matrix/models.py @@ -1,44 +1,48 @@ -from sqlalchemy import Column, ForeignKey, Integer, String, Boolean -from pnut_matrix.database import Base +import logging -class Avatars(Base): - __tablename__ = 'avatars' - id = Column(Integer, primary_key=True) - pnut_user = Column(String(250), unique=True) - avatar = Column(String(250)) +from peewee import * +from playhouse.migrate import * -class Rooms(Base): - __tablename__ = 'rooms' - id = Column(Integer, primary_key=True) - room_id = Column(String(250), unique=True) - pnut_chan = Column(Integer, unique=True) - portal = Column(Boolean) +db = SqliteDatabase(None) +migrator = SqliteMigrator(db) -class DirectRooms(Base): - __tablename__ = 'direct' - id = Column(Integer, primary_key=True) - room_id = Column(String(250), unique=True) - pnut_chan = Column(Integer, unique=True) - bridge_user = Column(String(250)) +class BaseModel(Model): + class Meta: + database = db -class ControlRooms(Base): - __tablename__ = 'control' - id = Column(Integer, primary_key=True) - room_id = Column(String(250), unique=True) +class PnutAvatars(BaseModel): + pnut_user = CharField(unique=True) + avatar_url = CharField() -class Events(Base): - __tablename__ = 'events' - id = Column(Integer, primary_key=True) - event_id = Column(String(250)) - room_id = Column(String(250)) - pnut_msg_id = Column(Integer) - pnut_user_id = Column(Integer) - pnut_chan_id = Column(Integer) - deleted = Column(Boolean) +class PnutChannels(BaseModel): + pnut_chan = IntegerField(unique=True) + room_id = CharField() + is_private = BooleanField(default=False) + is_direct = BooleanField(default=False) + direct_pnut_user = CharField(null=True) + direct_mtrx_user = CharField(null=True) -class Users(Base): - __tablename__ = 'users' - id = Column(Integer, primary_key=True) - matrix_id = Column(String(250)) - pnut_user_id = Column(Integer) - pnut_user_token = Column(String(250)) +class PnutPrivateChanMembers(BaseModel): + pnut_chan = IntegerField() + room_id = CharField() + pnut_user_id = IntegerField() + matrix_id = CharField() + +class Events(BaseModel): + event_id = CharField(unique=True) + room_id = CharField() + pnut_id = IntegerField() + pnut_channel = IntegerField() + revised = BooleanField(default=False) + deleted = BooleanField(default=False) + +class PnutUsers(BaseModel): + matrix_id = CharField(unique=True) + room_id = CharField() + pnut_user_id = IntegerField(unique=True, null=True) + pnut_user_token = CharField(null=True) + +def db_create_tables(): + with db: + db.create_tables([PnutUsers, Events, PnutChannels, PnutAvatars, + PnutPrivateChanMembers]) diff --git a/src/pnut_matrix/pnutservice.py b/src/pnut_matrix/pnutservice.py index 475ba6d..bbbb424 100644 --- a/src/pnut_matrix/pnutservice.py +++ b/src/pnut_matrix/pnutservice.py @@ -19,13 +19,13 @@ from mautrix.errors.request import MNotFound, MForbidden from websockets.asyncio.client import connect from websockets.exceptions import ConnectionClosed -from pnut_matrix.models import Avatars, Rooms, Events, DirectRooms, Users -from pnut_matrix.database import db_session, init_db -from sqlalchemy import and_ +from pnut_matrix.models import * + logger = logging.getLogger() config = None +matrix_url = None class MLogFilter(logging.Filter): @@ -64,12 +64,11 @@ async def new_pnut_message(msg, meta): token=config['MATRIX_AS_TOKEN'], as_user_id=matrix_id.lower()) - if meta['channel_type'] == 'io.pnut.core.chat': - room = Rooms.query.filter(Rooms.pnut_chan == - msg.channel_id).one_or_none() - elif meta['channel_type'] == 'io.pnut.core.pm': - room = DirectRooms.query.filter(DirectRooms.pnut_chan == - msg.channel_id).one_or_none() + channel_id = int(msg.channel_id) + room = PnutChannels.select().where(PnutChannels.pnut_chan == + channel_id).first() + + if meta['channel_type'] == 'io.pnut.core.pm': if room is None: # Do do an invite from the bridge user? logger.debug('new invite?') @@ -77,21 +76,22 @@ async def new_pnut_message(msg, meta): # subscribed_user_ids from meta logger.debug(meta['subscribed_user_ids']) pnut_user = matrix_id_from_pnut(msg.user.username) - profile = await matrix_api.get_profile(matrix_id.lower()) - if not profile: + try: + profile = await matrix_api.get_profile(matrix_id.lower()) + + except MNotFound: new_matrix_user(msg.user.username) + invitees=[] for pm_user in meta['subscribed_user_ids']: - user = Users.query.filter(Users.pnut_user_id == - pm_user).one_or_none() + user = PnutUsers.select().where(PnutUsers.pnut_user_id == + pm_user).first() if int(pm_user) == msg.user.id: continue if user is not None: invitees.append(user.matrix_id) if len(invitees) > 0: room = new_room(pnut_user, invitees, msg.channel_id) - else: - room = None logger.debug(room) if room is None: @@ -111,17 +111,19 @@ async def new_pnut_message(msg, meta): await set_matrix_display(msg.user) logger.debug('-set_display-') - avatar = Avatars.query.filter(Avatars.pnut_user == - msg.user.username).one_or_none() - if avatar is None or avatar.avatar != msg.user.content.avatar_image.url: + avatar = PnutAvatars.select().where(PnutAvatars.pnut_user == + msg.user.username).first() + if avatar is None or avatar.avatar_url != msg.user.content.avatar_image.url: await set_matrix_avatar(msg.user) logger.debug('-set_avatar-') - # members = matrix_api.get_room_members(room.room_id) - # logger.debug(members) - # join_room(room.room_id, config['MATRIX_AS_ID']) - # TODO: sort out room invite and join logic - await join_room(room.room_id, matrix_id) + if room.is_private: + matrix_api_as = ClientAPI(config['MATRIX_AS_ID'], + base_url=config['MATRIX_HOST'], + 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) if 'content' in msg: eventtext = TextMessageEventContent(msgtype=MessageType.TEXT, @@ -129,15 +131,13 @@ async def new_pnut_message(msg, meta): body=msg.content.text, formatted_body=msg.content.html) rid = await matrix_api.send_message(room.room_id, eventtext) - event = Events( + bridge_event = Events( event_id=rid, room_id=room.room_id, - pnut_msg_id=msg.id, - pnut_user_id=msg.user.id, - pnut_chan_id=msg.channel_id, - deleted=False) - db_session.add(event) - db_session.commit() + pnut_id=msg.id, + pnut_channel=msg.channel_id + ) + bridge_event.save() if 'raw' in msg: logger.debug('-handle media uploads-') @@ -177,15 +177,15 @@ async def new_pnut_post(post, meta): await set_matrix_display(post.user) logger.debug('-set_display-') - avatar = Avatars.query.filter(Avatars.pnut_user == - post.user.username).one_or_none() + avatar = PnutAvatars.select().where(PnutAvatars.pnut_user == + post.user.username).first() if (avatar is None or - avatar.avatar != post.user.content.avatar_image.url): + avatar.avatar_url != post.user.content.avatar_image.url): await set_matrix_avatar(post.user) logger.debug('-set_avatar-') room_id = config['MATRIX_GLOBAL_ROOM'] - await join_room(room_id, matrix_id) + await matrix_api.join_room(room_id) postlink = f"https://posts.pnut.io/{post.id}" plaintext = f"{post.content.text}\n{postlink}" htmltext = (f"{post.content.html}" @@ -195,15 +195,13 @@ async def new_pnut_post(post, meta): body=plaintext, formatted_body=htmltext) rid = await matrix_api.send_message(room_id, eventtext) - event = Events( + bridge_event = Events( event_id=rid, room_id=room_id, - pnut_msg_id=post.id, - pnut_user_id=post.user.id, - pnut_chan_id=0, - deleted=False) - db_session.add(event) - db_session.commit() + pnut_id=post.id, + pnut_channel=0 + ) + bridge_event.save() if 'raw' in post: logger.debug('-handle media uploads-') @@ -231,7 +229,7 @@ async def new_media(room_id, msg): dl_url = oembed.url elif oembed.type == 'video': msgtype = 'm.video' - dl_url = oembed.url + dl_url = oembed.embeddable_url info['h'] = oembed.height info['w'] = oembed.width elif oembed.type == 'html5video': @@ -267,15 +265,13 @@ async def new_media(room_id, msg): channel_id = msg.channel_id else: channel_id = 0 - event = Events( + bridge_event = Events( event_id=rid, room_id=room_id, - pnut_msg_id=msg.id, - pnut_user_id=msg.user.id, - pnut_chan_id=channel_id, - deleted=False) - db_session.add(event) - db_session.commit() + pnut_id=msg.id, + pnut_channel=channel_id + ) + bridge_event.save() async def delete_message(msg): matrix_id = matrix_id_from_pnut(msg.user.username) @@ -284,12 +280,12 @@ async def delete_message(msg): token=config['MATRIX_AS_TOKEN'], as_user_id=matrix_id.lower()) - events = Events.query.filter(and_(Events.pnut_msg_id == - msg.id, Events.deleted == False)).all() + events = Events.select().where((Events.pnut_id == msg.id) & + (Events.deleted == False)) for event in events: await matrix_api.redact(event.room_id, event.event_id) event.deleted = True - db_session.commit() + event.save() def matrix_id_from_pnut(username): matrix_id = (f"@{config['MATRIX_PNUT_PREFIX']}{username}" @@ -334,15 +330,17 @@ async def set_matrix_avatar(user): try: await matrix_api.set_avatar_url(ul) - avatar = Avatars.query.filter(Avatars.pnut_user == - user.username).one_or_none() + + avatar = PnutAvatars.select().where(PnutAvatars.pnut_user == + user.username).first() if avatar is None: - avatar = Avatars(pnut_user=user.username, - avatar=user.content.avatar_image.url) - db_session.add(avatar) + avatar = PnutAvatars(pnut_user=user.username, + avatar_url=user.content.avatar_image.url) + else: - avatar.avatar = user.content.avatar_image.url - db_session.commit() + avatar.avatar_url = user.content.avatar_image.url + + avatar.save() except Exception: logger.exception('failed to set user avatar') @@ -371,39 +369,6 @@ def new_matrix_user(username): logger.debug(r.text) return -async def join_room(room_id, matrix_id): - logging.debug('----- trying to join room -----') - - matrix_api_as = ClientAPI(config['MATRIX_AS_ID'], - base_url=config['MATRIX_HOST'], - token=config['MATRIX_AS_TOKEN']) - - matrix_api = ClientAPI(config['MATRIX_AS_ID'], - base_url=config['MATRIX_HOST'], - token=config['MATRIX_AS_TOKEN'], - as_user_id=matrix_id.lower()) - - try: - await matrix_api.join_room(room_id) - # logging.debug('----- should be joined -----') - - except MForbidden: - # logging.debug('------ got a forbidden ------') - await matrix_api_as.invite_user(room_id, matrix_id.lower()) - await matrix_api.join_room(room_id) - - except MatrixConnectionError: - # logger.debug(e) - # if 'code' in e and e.code == 403: - # await matrix_api_as.invite_user(room_id, matrix_id) - # await matrix_api.join_room(room_id) - # else: - logging.debug('------- moar join errors -------') - logger.exception('failed to join room') - logger.debug(f"{room_id}") - - logger.debug('-room_join-') - def new_room(pnut_user, invitees, chan): dr = None url = matrix_url + '/createRoom' @@ -424,13 +389,11 @@ def new_room(pnut_user, invitees, chan): logger.debug(r.text) logger.debug(response) for bridge_user in invitees: - dr = DirectRooms(room_id=response['room_id'], - bridge_user=pnut_user, pnut_chan=chan) - logger.debug(dr) - db_session.add(dr) - db_session.commit() - - return dr + direct_room = PnutChannels(pnut_chan=chan, room_id=response['room_id'], + is_direct=True, direct_pnut_user=pnut_user, + direct_mtrx_user=bridge_user) + direct_room.save() + return direct_room async def on_message(message): logger.debug("on_message: " + message) @@ -473,7 +436,10 @@ async def on_message(message): async def asmain(): if config['MATRIX_ADMIN_ROOM']: logger.debug("- sould join admin room -") - await join_room(config['MATRIX_ADMIN_ROOM'], config['MATRIX_AS_ID']) + 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'] @@ -490,6 +456,7 @@ async def asmain(): def main(): global config + global matrix_url a_parser = argparse.ArgumentParser() a_parser.add_argument( '-d', action='store_true', dest='debug', @@ -504,6 +471,9 @@ def main(): with open(configyaml, "rb") as config_file: config = yaml.load(config_file, Loader=yaml.SafeLoader) + db.init(config['SERVICE_DB']) + db_create_tables() + logging.config.dictConfig(config['logging']) redact_filter = MLogFilter() logging.getLogger("werkzeug").addFilter(redact_filter) @@ -511,9 +481,6 @@ def main(): matrix_url = config['MATRIX_HOST'] + '/_matrix/client/v3' - # setup the database connection - init_db() - asyncio.run(asmain()) logger.info('!! shutdown initiated !!')