replaced sqlalchemy with peewee for orm and revamped schema

Also covers issue #62
This commit is contained in:
Morgan McMillian 2024-12-22 10:31:30 -08:00
parent 734be1edda
commit 3d96f910ee
5 changed files with 263 additions and 404 deletions

View file

@ -17,11 +17,11 @@ dependencies = [
"requests", "requests",
"Flask[async]", "Flask[async]",
"pnutpy>=0.5.0", "pnutpy>=0.5.0",
"sqlalchemy",
"filemagic", "filemagic",
"mautrix>=0.20.6,<0.21", "mautrix>=0.20.6,<0.21",
"websockets", "websockets",
"asyncclick", "asyncclick",
"peewee",
] ]
[project.urls] [project.urls]

View file

@ -11,10 +11,7 @@ import os
from mautrix.client import ClientAPI from mautrix.client import ClientAPI
from mautrix.types import * from mautrix.types import *
from pnut_matrix.models import *
from pnut_matrix.models import Avatars, Rooms, Events, Users, DirectRooms, ControlRooms
from pnut_matrix.database import db_session
from sqlalchemy import and_
from flask import Flask, jsonify, request, abort from flask import Flask, jsonify, request, abort
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,10 +26,6 @@ def not_found(error):
def forbidden(error): def forbidden(error):
return jsonify({'errcode':'PNUT_FORBIDDEN'}), 403 return jsonify({'errcode':'PNUT_FORBIDDEN'}), 403
@app.teardown_appcontext
def shutdown_session(exception=None):
db_session.remove()
@app.route("/_matrix/app/v1/rooms/<alias>") @app.route("/_matrix/app/v1/rooms/<alias>")
@app.route("/rooms/<alias>") @app.route("/rooms/<alias>")
async def query_alias(alias): async def query_alias(alias):
@ -40,7 +33,8 @@ async def query_alias(alias):
alias_localpart = alias.split(":")[0][1:] alias_localpart = alias.split(":")[0][1:]
channel_id = int(alias_localpart.split('_')[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: if room is not None:
abort(404) abort(404)
@ -58,9 +52,9 @@ async def query_alias(alias):
if 'io.pnut.core.chat-settings' in channel.raw: if 'io.pnut.core.chat-settings' in channel.raw:
for setting in channel.raw['io.pnut.core.chat-settings']: for setting in channel.raw['io.pnut.core.chat-settings']:
if 'name' in setting: if 'name' in setting:
name = setting['name'] name = f"🥜 {setting['name']}"
else: else:
name = None name = f"🥜 channel {channel.id}"
if 'description' in setting: if 'description' in setting:
topic = setting['description']['text'] topic = setting['description']['text']
else: else:
@ -84,15 +78,11 @@ async def query_alias(alias):
if not channel.you_subscribed: if not channel.you_subscribed:
pnutpy.api.subscribe_channel(channel_id) pnutpy.api.subscribe_channel(channel_id)
rr = Rooms(
room_id=room_id, room = PnutChannels(room_id=room_id, pnut_chan=channel_id)
pnut_chan=channel_id, room.save()
portal=True logging.debug(f'-created new room for channel {room.pnut_chan}-')
) logging.debug(room.room_id)
logging.debug(rr.room_id)
logging.debug(rr.pnut_chan)
db_session.add(rr)
db_session.commit()
except pnutpy.errors.PnutPermissionDenied: except pnutpy.errors.PnutPermissionDenied:
logging.debug("-permission denied-") logging.debug("-permission denied-")
@ -114,23 +104,25 @@ async def on_receive_events(transaction):
events = request.get_json()["events"] events = request.get_json()["events"]
for event in events: for event in events:
logging.debug('-----event-----')
logging.debug(event) logging.debug(event)
logging.debug('~~~~~~~~~~~~~~~')
if (app.config['MATRIX_ADMIN_ROOM'] and if (app.config['MATRIX_ADMIN_ROOM'] and
app.config['MATRIX_ADMIN_ROOM'] == event['room_id']): app.config['MATRIX_ADMIN_ROOM'] == event['room_id']):
logging.debug('>----on_admin_event----<')
await on_admin_event(event) await on_admin_event(event)
return jsonify({}) return jsonify({})
user = Users.query.filter(Users.matrix_id == user = PnutUsers.select().where(PnutUsers.matrix_id ==
event['sender']).one_or_none() event['sender']).first()
if event['type'] == 'm.room.message': if event['type'] == 'm.room.message':
logging.debug('>----new_message----<')
await new_message(event, user) await new_message(event, user)
# elif event['type'] == 'm.sticker':
# new_sticker(event, user)
elif event['type'] == 'm.room.redaction': elif event['type'] == 'm.room.redaction':
logging.debug('>----delete_message----<')
delete_message(event, user) delete_message(event, user)
elif event['type'] == 'm.room.member': elif event['type'] == 'm.room.member':
@ -138,59 +130,55 @@ async def on_receive_events(transaction):
'membership' in event['content']): 'membership' in event['content']):
if (event['content']['membership'] == "invite" and if (event['content']['membership'] == "invite" and
event['content']['is_direct']): event['content']['is_direct']):
logging.debug('----> direct invite <----') logging.debug('>----on_direct_invite----<')
await on_direct_invite(event) await on_direct_invite(event)
return jsonify({}) return jsonify({})
if 'membership' in event['content']: if 'membership' in event['content']:
if event['content']['membership'] == "leave": if event['content']['membership'] == "leave":
logging.debug('----> leave event <----') logging.debug('>----on_leave_event----<')
await on_leave_event(event) await on_leave_event(event)
return jsonify({}) return jsonify({})
logging.debug("----room member event----")
logging.debug(user)
logging.debug(event)
return jsonify({}) return jsonify({})
async def new_message(event, user): async def new_message(event, user):
if (app.config['MATRIX_PNUT_PREFIX'] in event['sender'] or
'pnut-bridge' in event['sender']): if event['sender'] == app.config['MATRIX_AS_ID']:
logging.debug('-skipping dup event-')
return return
if 'msgtype' not in event['content']: if app.config['MATRIX_PNUT_PREFIX'] in event['sender']:
logging.debug('-unknown message type-')
return return
control = ControlRooms.query.filter(ControlRooms.room_id == if user.room_id == event['room_id']:
event['room_id']).one_or_none() await on_control_message(event, user)
if control is not None:
await on_control_message(event)
return return
direct = DirectRooms.query.filter(DirectRooms.room_id == room = PnutChannels.select().where(PnutChannels.room_id ==
event['room_id']).one_or_none() event['room_id']).first()
if direct is not None: logging.debug(f'room: {room}')
return on_direct_message(event, user, direct)
room = Rooms.query.filter(Rooms.room_id == event['room_id']).one_or_none()
if room is None: if room is None:
logging.debug('-room not mapped-') logging.debug('-room not mapped-')
return return
if room.is_direct:
logging.debug('>----on_direct_message----<')
return on_direct_message(event, user, room)
if user is not None: if user is not None:
token = user.pnut_user_token token = user.pnut_user_token
prefix = "" prefix = ""
else: else:
token = app.config['MATRIX_PNUT_TOKEN'] token = app.config['MATRIX_PNUT_TOKEN']
matrix_profile = get_profile(event['sender']) matrix_profile = get_profile(event['sender'])
if "displayname" in matrix_profile: if ('displayname' in matrix_profile):
prefix = (f"[{matrix_profile['displayname']}]" prefix = (f"[{matrix_profile['displayname']}]"
f" ({event['sender']})\n") f" ({event['sender']})\n")
else: else:
prefix = "(" + event['sender'] + ")\n" prefix = "(" + event['sender'] + ")\n"
pnutpy.api.add_authorization_token(token) pnutpy.api.add_authorization_token(token)
raw = {} raw = {}
@ -204,16 +192,13 @@ async def new_message(event, user):
try: try:
msg, meta = pnutpy.api.create_message(room.pnut_chan, msg, meta = pnutpy.api.create_message(room.pnut_chan,
data={'text': text, 'raw': raw}) data={'text': text, 'raw': raw})
revent = Events( bridge_event = Events(
event_id=event['event_id'], event_id=event['event_id'],
room_id=event['room_id'], room_id=event['room_id'],
pnut_msg_id=msg.id, pnut_id=msg.id,
pnut_user_id=msg.user.id, pnut_channel=room.pnut_chan
pnut_chan_id=room.pnut_chan,
deleted=False
) )
db_session.add(revent) bridge_event.save()
db_session.commit()
# TODO: need to redo this for global message # TODO: need to redo this for global message
# if user is not None: # if user is not None:
@ -362,8 +347,8 @@ def delete_message(event, user):
token = app.config['MATRIX_PNUT_TOKEN'] token = app.config['MATRIX_PNUT_TOKEN']
pnutpy.api.add_authorization_token(token) pnutpy.api.add_authorization_token(token)
e = Events.query.filter(and_(Events.event_id == event['redacts'], e = Events.select().where((Events.event_id == events['redacts']) &
Events.deleted == False)).one_or_none() (Events.deleted == False)).first()
if e is None: if e is None:
logging.debug("- can't find the event to remove -") logging.debug("- can't find the event to remove -")
return return
@ -371,7 +356,7 @@ def delete_message(event, user):
try: try:
r, meta = pnutpy.api.delete_message(e.pnut_chan_id, e.pnut_msg_id) r, meta = pnutpy.api.delete_message(e.pnut_chan_id, e.pnut_msg_id)
e.deleted = True e.deleted = True
db_session.commit() e.save()
except pnutpy.errors.PnutPermissionDenied as e: except pnutpy.errors.PnutPermissionDenied as e:
pass pass
@ -406,9 +391,11 @@ async def create_pnut_matrix_room(channel, user):
if channel.acl.read.public: if channel.acl.read.public:
visibility = RoomDirectoryVisibility.PUBLIC visibility = RoomDirectoryVisibility.PUBLIC
preset = RoomCreatePreset.PUBLIC preset = RoomCreatePreset.PUBLIC
is_private = False
else: else:
visibility = RoomDirectoryVisibility.PRIVATE visibility = RoomDirectoryVisibility.PRIVATE
preset = RoomCreatePreset.PRIVATE preset = RoomCreatePreset.PRIVATE
is_private = True
if 'io.pnut.core.chat-settings' in channel.raw: if 'io.pnut.core.chat-settings' in channel.raw:
for setting in channel.raw['io.pnut.core.chat-settings']: for setting in channel.raw['io.pnut.core.chat-settings']:
@ -428,9 +415,20 @@ async def create_pnut_matrix_room(channel, user):
name=name, name=name,
topic=topic) topic=topic)
rr = Rooms(room_id=room_id, pnut_chan=channel.id, portal=True) room = PnutChannels(room_id=room_id, pnut_chan=channel.id,
db_session.add(rr) is_private=is_private)
db_session.commit() 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): def new_matrix_user(username):
endpoint = "/_matrix/client/v3/register" endpoint = "/_matrix/client/v3/register"
@ -479,22 +477,6 @@ async def on_admin_event(event):
elif msg[0] == 'list': elif msg[0] == 'list':
await matrix_api.send_message(event['room_id'], cmd_admin_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: except Exception:
errmsg = "- on_admin_event -" errmsg = "- on_admin_event -"
logging.exception(errmsg) logging.exception(errmsg)
@ -504,10 +486,6 @@ def cmd_admin_help(cmd=None):
help_desc = "Show information about available commands." help_desc = "Show information about available commands."
list_usage = "list" list_usage = "list"
list_desc = "List the rooms currently linked with pnut.io." list_desc = "List the rooms currently linked with pnut.io."
unlink_usage = "unlink <room_id> | <pnut_channel_id>"
unlink_desc = "Unlink a room between Matrix and pnut.io."
link_usage = "link <room_id> <pnut_channel_id>"
link_desc = "Link a room between Matrix and pnut.io."
if cmd == 'help': if cmd == 'help':
text = "usage: " + help_usage + "\n\n" text = "usage: " + help_usage + "\n\n"
@ -532,7 +510,7 @@ def cmd_admin_help(cmd=None):
def cmd_admin_list(): def cmd_admin_list():
text = "" text = ""
rooms = Rooms.query.all() rooms = PnutChannels.select()
if len(rooms) > 0: if len(rooms) > 0:
text = "ID\tMATRIX ID\tPNUT CHANNEL\n" text = "ID\tMATRIX ID\tPNUT CHANNEL\n"
@ -543,108 +521,21 @@ def cmd_admin_list():
text += str(room.id) + '\t' text += str(room.id) + '\t'
text += room.room_id + '\t\t\t\t\t' text += room.room_id + '\t\t\t\t\t'
text += str(room.pnut_chan) + '\t' text += str(room.pnut_chan) + '\t'
if room.portal:
text += "(portal)"
text += '\n' text += '\n'
return TextMessageEventContent(msgtype='m.text', body=text) 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): async def on_direct_invite(event):
# direct chat with the appservice user
if event['state_key'] == app.config['MATRIX_AS_ID']: if event['state_key'] == app.config['MATRIX_AS_ID']:
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
base_url=app.config['MATRIX_HOST'], base_url=app.config['MATRIX_HOST'],
token=app.config['MATRIX_AS_TOKEN']) 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'], matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
base_url=app.config['MATRIX_HOST'], base_url=app.config['MATRIX_HOST'],
token=app.config['MATRIX_AS_TOKEN'], 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'], pnut_user = bridge_user.replace(app.config['MATRIX_PNUT_PREFIX'],
'').split(':')[0] '').split(':')[0]
user = Users.query.filter(Users.matrix_id == user = PnutUsers.select().where(PnutUsers.matrix_id ==
event['sender']).one_or_none() event['sender']).first()
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)
dm = DirectRooms(room_id=event['room_id'], if user is not None:
bridge_user=bridge_user, pnut_chan=channel.id) 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: try:
logging.debug('--> trying to join room <--') logging.debug('--> trying to join room <--')
await matrix_api.join_room_by_id(event['room_id']) await matrix_api.join_room_by_id(event['room_id'])
dm.save()
db_session.add(dm)
db_session.commit()
except Exception: except Exception:
errmsg = "- on_direct_invite -" errmsg = "- on_direct_invite -"
logging.exception(errmsg) logging.exception(errmsg)
async def on_leave_event(event): async def on_leave_event(event):
direct = DirectRooms.query.filter(DirectRooms.room_id == direct_room = PnutChannels.select().where(PnutChannels.room_id ==
event['room_id']).one_or_none() event['room_id']).first()
if direct is not None:
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'], matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
base_url=app.config['MATRIX_HOST'], base_url=app.config['MATRIX_HOST'],
token=app.config['MATRIX_AS_TOKEN'], token=app.config['MATRIX_AS_TOKEN'],
as_user_id=direct.bridge_user.lower()) as_user_id=direct_room.direct_pnut_user.lower())
try: try:
await matrix_api.leave_room(event['room_id']) await matrix_api.leave_room(event['room_id'])
db_session.delete(direct) direct_room.delete_instance()
db_session.commit()
except Exception: except Exception:
errmsg = "- on_leave_event -" errmsg = "- on_leave_event -"
logging.exception(errmsg) logging.exception(errmsg)
control = ControlRooms.query.filter(ControlRooms.room_id == if user is not None:
event['room_id']).one_or_none()
if control is not None:
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
base_url=app.config['MATRIX_HOST'], base_url=app.config['MATRIX_HOST'],
token=app.config['MATRIX_AS_TOKEN']) token=app.config['MATRIX_AS_TOKEN'])
try: try:
await matrix_api.leave_room(event['room_id']) await matrix_api.leave_room(event['room_id'])
db_session.delete(control) user.room_id = None
db_session.commit() user.save()
except Exception: except Exception:
errmsg = "- on_leave_event -" errmsg = "- on_leave_event -"
logging.exception(errmsg) logging.exception(errmsg)
def on_direct_message(event, user, room): def on_direct_message(event, user, room):
if user is not None: if user is not None:
token = user.pnut_user_token token = user.pnut_user_token
prefix = "" prefix = ""
@ -727,19 +632,17 @@ def on_direct_message(event, user, room):
raw['io.pnut.core.crosspost'] = [crosspost_raw(event)] raw['io.pnut.core.crosspost'] = [crosspost_raw(event)]
evtext, evraw = msg_from_event(event) evtext, evraw = msg_from_event(event)
text = prefix + evtext text = prefix + evtext
try: try:
msg, meta = pnutpy.api.create_message(room.pnut_chan, msg, meta = pnutpy.api.create_message(room.pnut_chan,
data={'text': text, 'raw': raw}) data={'text': text, 'raw': raw})
revent = Events( bridge_event = Events(
event_id=event['event_id'], event_id=event['event_id'],
room_id=event['room_id'], room_id=event['room_id'],
pnut_msg_id=msg.id, pnut_id=msg.id,
pnut_user_id=msg.user.id, pnut_channel=room.pnut_chan
pnut_chan_id=room.pnut_chan,
deleted=False
) )
db_session.add(revent) bridge_event.save()
db_session.commit()
except pnutpy.errors.PnutAuthAPIException: except pnutpy.errors.PnutAuthAPIException:
logging.exception('-unable to post to pnut channel-') logging.exception('-unable to post to pnut channel-')
@ -749,11 +652,10 @@ def on_direct_message(event, user, room):
return jsonify({}) return jsonify({})
async def on_control_message(event): async def on_control_message(event, user):
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
base_url=app.config['MATRIX_HOST'], base_url=app.config['MATRIX_HOST'],
token=app.config['MATRIX_AS_TOKEN']) token=app.config['MATRIX_AS_TOKEN'])
logging.debug("- direct room event received -")
if event['type'] != 'm.room.message': if event['type'] != 'm.room.message':
return jsonify({}) return jsonify({})
@ -775,26 +677,25 @@ async def on_control_message(event):
elif msg[0] == '!save': elif msg[0] == '!save':
if len(msg) > 1: if len(msg) > 1:
await matrix_api.send_message(event['room_id'], await matrix_api.send_message(event['room_id'],
cmd_user_save(event['sender'], cmd_user_save(user, msg[1]))
msg[1]))
else: else:
await matrix_api.send_message(event['room_id'], await matrix_api.send_message(event['room_id'],
cmd_user_save()) cmd_user_save())
elif msg[0] == '!drop': elif msg[0] == '!drop':
await matrix_api.send_message(event['room_id'], r = await cmd_user_drop(user)
cmd_user_drop(event['sender'])) await matrix_api.send_message(event['room_id'], r)
elif msg[0] == '!status': elif msg[0] == '!status':
await matrix_api.send_message(event['room_id'], await matrix_api.send_message(event['room_id'],
cmd_user_status(event['sender'])) cmd_user_status(user))
elif msg[0] == '!join': elif msg[0] == '!join':
if len(msg) > 1: 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) await matrix_api.send_message(event['room_id'], r)
else: else:
r = await cmd_user_join(event['sender']) r = await cmd_user_join(user)
await matrix_api.send_message(event['room_id'], r) await matrix_api.send_message(event['room_id'], r)
except Exception: except Exception:
@ -822,7 +723,7 @@ def cmd_user_auth():
return TextMessageEventContent(msgtype='m.text', body=reply) 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: if token is None:
reply = "You must provide a token with this command.\n" reply = "You must provide a token with this command.\n"
reply += "!save <token>" reply += "!save <token>"
@ -832,13 +733,9 @@ def cmd_user_save(sender=None, token=None):
try: try:
response, meta = pnutpy.api.get_user('me') response, meta = pnutpy.api.get_user('me')
user = Users( user.pnut_user_id = response.id
matrix_id=sender, user.pnut_user_token = token
pnut_user_id=response.id, user.save()
pnut_user_token=token
)
db_session.add(user)
db_session.commit()
reply = "Success! You are now authorized as " + response['username'] 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) return TextMessageEventContent(msgtype='m.text', body=reply)
def cmd_user_drop(sender=None): async def cmd_user_drop(user):
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."
except Exception as e: direct_rooms = PnutChannels.select().where(PnutChannels.direct_mtrx_user ==
logging.exception('!drop') user.matrix_id)
reply = "Error! Problem removing your token." 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) return TextMessageEventContent(msgtype='m.text', body=reply)
def cmd_user_status(sender=None): def cmd_user_status(user):
try: try:
user = Users.query.filter(Users.matrix_id == sender).one_or_none()
if user is None: if user is None:
reply = "You are currently not authorized on pnut.io" reply = "You are currently not authorized on pnut.io"
else: else:
@ -886,28 +795,27 @@ def cmd_user_status(sender=None):
return TextMessageEventContent(msgtype='m.text', body=reply) 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: if channel_id is None:
reply = "You must provide a channel id number with this command.\n" reply = "You must provide a channel id number with this command.\n"
reply += "!join <channel #>" reply += "!join <channel #>"
return TextMessageEventContent(msgtype='m.text', body=reply) return TextMessageEventContent(msgtype='m.text', body=reply)
try: try:
user = Users.query.filter(Users.matrix_id == sender).one_or_none()
if user is None: if user is None:
reply = "You are currently not authorized on pnut.io" reply = "You are currently not authorized on pnut.io"
else: else:
pnutpy.api.add_authorization_token(user.pnut_user_token) pnutpy.api.add_authorization_token(user.pnut_user_token)
channel, meta = pnutpy.api.get_channel(channel_id, include_raw=1) channel, meta = pnutpy.api.get_channel(channel_id, include_raw=1)
room = Rooms.query.filter(Rooms.pnut_chan == room = PnutChannels.select().where(PnutChannels.pnut_chan ==
channel_id).one_or_none() channel_id).first()
if room is None: if room is None:
await create_pnut_matrix_room(channel, user) await create_pnut_matrix_room(channel, user)
else: else:
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'], matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
base_url=app.config['MATRIX_HOST'], base_url=app.config['MATRIX_HOST'],
token=app.config['MATRIX_AS_TOKEN']) 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" reply = "ok"
except pnutpy.errors.PnutAuthAPIException as e: except pnutpy.errors.PnutAuthAPIException as e:
@ -952,6 +860,10 @@ def main():
app.config.update(config) app.config.update(config)
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
db.init(config['SERVICE_DB'])
db_create_tables()
app.run(host=config['LISTEN_HOST'], port=config['LISTEN_PORT']) app.run(host=config['LISTEN_HOST'], port=config['LISTEN_PORT'])
if __name__ == '__main__': if __name__ == '__main__':

View file

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

View file

@ -1,44 +1,48 @@
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean import logging
from pnut_matrix.database import Base
class Avatars(Base): from peewee import *
__tablename__ = 'avatars' from playhouse.migrate import *
id = Column(Integer, primary_key=True)
pnut_user = Column(String(250), unique=True)
avatar = Column(String(250))
class Rooms(Base): db = SqliteDatabase(None)
__tablename__ = 'rooms' migrator = SqliteMigrator(db)
id = Column(Integer, primary_key=True)
room_id = Column(String(250), unique=True)
pnut_chan = Column(Integer, unique=True)
portal = Column(Boolean)
class DirectRooms(Base): class BaseModel(Model):
__tablename__ = 'direct' class Meta:
id = Column(Integer, primary_key=True) database = db
room_id = Column(String(250), unique=True)
pnut_chan = Column(Integer, unique=True)
bridge_user = Column(String(250))
class ControlRooms(Base): class PnutAvatars(BaseModel):
__tablename__ = 'control' pnut_user = CharField(unique=True)
id = Column(Integer, primary_key=True) avatar_url = CharField()
room_id = Column(String(250), unique=True)
class Events(Base): class PnutChannels(BaseModel):
__tablename__ = 'events' pnut_chan = IntegerField(unique=True)
id = Column(Integer, primary_key=True) room_id = CharField()
event_id = Column(String(250)) is_private = BooleanField(default=False)
room_id = Column(String(250)) is_direct = BooleanField(default=False)
pnut_msg_id = Column(Integer) direct_pnut_user = CharField(null=True)
pnut_user_id = Column(Integer) direct_mtrx_user = CharField(null=True)
pnut_chan_id = Column(Integer)
deleted = Column(Boolean)
class Users(Base): class PnutPrivateChanMembers(BaseModel):
__tablename__ = 'users' pnut_chan = IntegerField()
id = Column(Integer, primary_key=True) room_id = CharField()
matrix_id = Column(String(250)) pnut_user_id = IntegerField()
pnut_user_id = Column(Integer) matrix_id = CharField()
pnut_user_token = Column(String(250))
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])

View file

@ -19,13 +19,13 @@ from mautrix.errors.request import MNotFound, MForbidden
from websockets.asyncio.client import connect from websockets.asyncio.client import connect
from websockets.exceptions import ConnectionClosed from websockets.exceptions import ConnectionClosed
from pnut_matrix.models import Avatars, Rooms, Events, DirectRooms, Users from pnut_matrix.models import *
from pnut_matrix.database import db_session, init_db
from sqlalchemy import and_
logger = logging.getLogger() logger = logging.getLogger()
config = None config = None
matrix_url = None
class MLogFilter(logging.Filter): class MLogFilter(logging.Filter):
@ -64,12 +64,11 @@ async def new_pnut_message(msg, meta):
token=config['MATRIX_AS_TOKEN'], token=config['MATRIX_AS_TOKEN'],
as_user_id=matrix_id.lower()) as_user_id=matrix_id.lower())
if meta['channel_type'] == 'io.pnut.core.chat': channel_id = int(msg.channel_id)
room = Rooms.query.filter(Rooms.pnut_chan == room = PnutChannels.select().where(PnutChannels.pnut_chan ==
msg.channel_id).one_or_none() channel_id).first()
elif meta['channel_type'] == 'io.pnut.core.pm':
room = DirectRooms.query.filter(DirectRooms.pnut_chan == if meta['channel_type'] == 'io.pnut.core.pm':
msg.channel_id).one_or_none()
if room is None: if room is None:
# Do do an invite from the bridge user? # Do do an invite from the bridge user?
logger.debug('new invite?') logger.debug('new invite?')
@ -77,21 +76,22 @@ async def new_pnut_message(msg, meta):
# subscribed_user_ids from meta # subscribed_user_ids from meta
logger.debug(meta['subscribed_user_ids']) logger.debug(meta['subscribed_user_ids'])
pnut_user = matrix_id_from_pnut(msg.user.username) pnut_user = matrix_id_from_pnut(msg.user.username)
profile = await matrix_api.get_profile(matrix_id.lower()) try:
if not profile: profile = await matrix_api.get_profile(matrix_id.lower())
except MNotFound:
new_matrix_user(msg.user.username) new_matrix_user(msg.user.username)
invitees=[] invitees=[]
for pm_user in meta['subscribed_user_ids']: for pm_user in meta['subscribed_user_ids']:
user = Users.query.filter(Users.pnut_user_id == user = PnutUsers.select().where(PnutUsers.pnut_user_id ==
pm_user).one_or_none() pm_user).first()
if int(pm_user) == msg.user.id: if int(pm_user) == msg.user.id:
continue continue
if user is not None: if user is not None:
invitees.append(user.matrix_id) invitees.append(user.matrix_id)
if len(invitees) > 0: if len(invitees) > 0:
room = new_room(pnut_user, invitees, msg.channel_id) room = new_room(pnut_user, invitees, msg.channel_id)
else:
room = None
logger.debug(room) logger.debug(room)
if room is None: if room is None:
@ -111,17 +111,19 @@ async def new_pnut_message(msg, meta):
await set_matrix_display(msg.user) await set_matrix_display(msg.user)
logger.debug('-set_display-') logger.debug('-set_display-')
avatar = Avatars.query.filter(Avatars.pnut_user == avatar = PnutAvatars.select().where(PnutAvatars.pnut_user ==
msg.user.username).one_or_none() msg.user.username).first()
if avatar is None or avatar.avatar != msg.user.content.avatar_image.url: if avatar is None or avatar.avatar_url != msg.user.content.avatar_image.url:
await set_matrix_avatar(msg.user) await set_matrix_avatar(msg.user)
logger.debug('-set_avatar-') logger.debug('-set_avatar-')
# members = matrix_api.get_room_members(room.room_id) if room.is_private:
# logger.debug(members) matrix_api_as = ClientAPI(config['MATRIX_AS_ID'],
# join_room(room.room_id, config['MATRIX_AS_ID']) base_url=config['MATRIX_HOST'],
# TODO: sort out room invite and join logic token=config['MATRIX_AS_TOKEN'])
await join_room(room.room_id, matrix_id) await matrix_api_as.invite_user(room.room_id, matrix_id.lower())
await matrix_api.join_room(room.room_id)
if 'content' in msg: if 'content' in msg:
eventtext = TextMessageEventContent(msgtype=MessageType.TEXT, eventtext = TextMessageEventContent(msgtype=MessageType.TEXT,
@ -129,15 +131,13 @@ async def new_pnut_message(msg, meta):
body=msg.content.text, body=msg.content.text,
formatted_body=msg.content.html) formatted_body=msg.content.html)
rid = await matrix_api.send_message(room.room_id, eventtext) rid = await matrix_api.send_message(room.room_id, eventtext)
event = Events( bridge_event = Events(
event_id=rid, event_id=rid,
room_id=room.room_id, room_id=room.room_id,
pnut_msg_id=msg.id, pnut_id=msg.id,
pnut_user_id=msg.user.id, pnut_channel=msg.channel_id
pnut_chan_id=msg.channel_id, )
deleted=False) bridge_event.save()
db_session.add(event)
db_session.commit()
if 'raw' in msg: if 'raw' in msg:
logger.debug('-handle media uploads-') logger.debug('-handle media uploads-')
@ -177,15 +177,15 @@ async def new_pnut_post(post, meta):
await set_matrix_display(post.user) await set_matrix_display(post.user)
logger.debug('-set_display-') logger.debug('-set_display-')
avatar = Avatars.query.filter(Avatars.pnut_user == avatar = PnutAvatars.select().where(PnutAvatars.pnut_user ==
post.user.username).one_or_none() post.user.username).first()
if (avatar is None or 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) await set_matrix_avatar(post.user)
logger.debug('-set_avatar-') logger.debug('-set_avatar-')
room_id = config['MATRIX_GLOBAL_ROOM'] 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}" postlink = f"https://posts.pnut.io/{post.id}"
plaintext = f"{post.content.text}\n{postlink}" plaintext = f"{post.content.text}\n{postlink}"
htmltext = (f"{post.content.html}" htmltext = (f"{post.content.html}"
@ -195,15 +195,13 @@ async def new_pnut_post(post, meta):
body=plaintext, body=plaintext,
formatted_body=htmltext) formatted_body=htmltext)
rid = await matrix_api.send_message(room_id, eventtext) rid = await matrix_api.send_message(room_id, eventtext)
event = Events( bridge_event = Events(
event_id=rid, event_id=rid,
room_id=room_id, room_id=room_id,
pnut_msg_id=post.id, pnut_id=post.id,
pnut_user_id=post.user.id, pnut_channel=0
pnut_chan_id=0, )
deleted=False) bridge_event.save()
db_session.add(event)
db_session.commit()
if 'raw' in post: if 'raw' in post:
logger.debug('-handle media uploads-') logger.debug('-handle media uploads-')
@ -231,7 +229,7 @@ async def new_media(room_id, msg):
dl_url = oembed.url dl_url = oembed.url
elif oembed.type == 'video': elif oembed.type == 'video':
msgtype = 'm.video' msgtype = 'm.video'
dl_url = oembed.url dl_url = oembed.embeddable_url
info['h'] = oembed.height info['h'] = oembed.height
info['w'] = oembed.width info['w'] = oembed.width
elif oembed.type == 'html5video': elif oembed.type == 'html5video':
@ -267,15 +265,13 @@ async def new_media(room_id, msg):
channel_id = msg.channel_id channel_id = msg.channel_id
else: else:
channel_id = 0 channel_id = 0
event = Events( bridge_event = Events(
event_id=rid, event_id=rid,
room_id=room_id, room_id=room_id,
pnut_msg_id=msg.id, pnut_id=msg.id,
pnut_user_id=msg.user.id, pnut_channel=channel_id
pnut_chan_id=channel_id, )
deleted=False) bridge_event.save()
db_session.add(event)
db_session.commit()
async def delete_message(msg): async def delete_message(msg):
matrix_id = matrix_id_from_pnut(msg.user.username) matrix_id = matrix_id_from_pnut(msg.user.username)
@ -284,12 +280,12 @@ async def delete_message(msg):
token=config['MATRIX_AS_TOKEN'], token=config['MATRIX_AS_TOKEN'],
as_user_id=matrix_id.lower()) as_user_id=matrix_id.lower())
events = Events.query.filter(and_(Events.pnut_msg_id == events = Events.select().where((Events.pnut_id == msg.id) &
msg.id, Events.deleted == False)).all() (Events.deleted == False))
for event in events: for event in events:
await matrix_api.redact(event.room_id, event.event_id) await matrix_api.redact(event.room_id, event.event_id)
event.deleted = True event.deleted = True
db_session.commit() event.save()
def matrix_id_from_pnut(username): def matrix_id_from_pnut(username):
matrix_id = (f"@{config['MATRIX_PNUT_PREFIX']}{username}" matrix_id = (f"@{config['MATRIX_PNUT_PREFIX']}{username}"
@ -334,15 +330,17 @@ async def set_matrix_avatar(user):
try: try:
await matrix_api.set_avatar_url(ul) 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: if avatar is None:
avatar = Avatars(pnut_user=user.username, avatar = PnutAvatars(pnut_user=user.username,
avatar=user.content.avatar_image.url) avatar_url=user.content.avatar_image.url)
db_session.add(avatar)
else: else:
avatar.avatar = user.content.avatar_image.url avatar.avatar_url = user.content.avatar_image.url
db_session.commit()
avatar.save()
except Exception: except Exception:
logger.exception('failed to set user avatar') logger.exception('failed to set user avatar')
@ -371,39 +369,6 @@ def new_matrix_user(username):
logger.debug(r.text) logger.debug(r.text)
return 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): def new_room(pnut_user, invitees, chan):
dr = None dr = None
url = matrix_url + '/createRoom' url = matrix_url + '/createRoom'
@ -424,13 +389,11 @@ def new_room(pnut_user, invitees, chan):
logger.debug(r.text) logger.debug(r.text)
logger.debug(response) logger.debug(response)
for bridge_user in invitees: for bridge_user in invitees:
dr = DirectRooms(room_id=response['room_id'], direct_room = PnutChannels(pnut_chan=chan, room_id=response['room_id'],
bridge_user=pnut_user, pnut_chan=chan) is_direct=True, direct_pnut_user=pnut_user,
logger.debug(dr) direct_mtrx_user=bridge_user)
db_session.add(dr) direct_room.save()
db_session.commit() return direct_room
return dr
async def on_message(message): async def on_message(message):
logger.debug("on_message: " + message) logger.debug("on_message: " + message)
@ -473,7 +436,10 @@ async def on_message(message):
async def asmain(): async def asmain():
if config['MATRIX_ADMIN_ROOM']: if config['MATRIX_ADMIN_ROOM']:
logger.debug("- sould join 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 = 'wss://stream.pnut.io/v1/app?access_token='
ws_url += config['PNUT_APPTOKEN'] + '&key=' + config['PNUT_APPKEY'] ws_url += config['PNUT_APPTOKEN'] + '&key=' + config['PNUT_APPKEY']
@ -490,6 +456,7 @@ async def asmain():
def main(): def main():
global config global config
global matrix_url
a_parser = argparse.ArgumentParser() a_parser = argparse.ArgumentParser()
a_parser.add_argument( a_parser.add_argument(
'-d', action='store_true', dest='debug', '-d', action='store_true', dest='debug',
@ -504,6 +471,9 @@ def main():
with open(configyaml, "rb") as config_file: with open(configyaml, "rb") as config_file:
config = yaml.load(config_file, Loader=yaml.SafeLoader) config = yaml.load(config_file, Loader=yaml.SafeLoader)
db.init(config['SERVICE_DB'])
db_create_tables()
logging.config.dictConfig(config['logging']) logging.config.dictConfig(config['logging'])
redact_filter = MLogFilter() redact_filter = MLogFilter()
logging.getLogger("werkzeug").addFilter(redact_filter) logging.getLogger("werkzeug").addFilter(redact_filter)
@ -511,9 +481,6 @@ def main():
matrix_url = config['MATRIX_HOST'] + '/_matrix/client/v3' matrix_url = config['MATRIX_HOST'] + '/_matrix/client/v3'
# setup the database connection
init_db()
asyncio.run(asmain()) asyncio.run(asmain())
logger.info('!! shutdown initiated !!') logger.info('!! shutdown initiated !!')