diff --git a/pyproject.toml b/pyproject.toml index 75628a0..a8f1fb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "asyncclick", "peewee", "tomlkit", + "beautifulsoup4", ] [project.urls] diff --git a/src/pnut_matrix/appservice.py b/src/pnut_matrix/appservice.py index 5ff6d14..b4f3713 100644 --- a/src/pnut_matrix/appservice.py +++ b/src/pnut_matrix/appservice.py @@ -11,6 +11,7 @@ import time import os import argparse +from bs4 import BeautifulSoup from mautrix.client import ClientAPI from mautrix.types import * from pnut_matrix.models import * @@ -110,12 +111,6 @@ 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({}) - user = PnutUsers.select().where(PnutUsers.matrix_id == event['sender']).first() @@ -127,6 +122,10 @@ async def on_receive_events(transaction): logging.debug('>----delete_message----<') delete_message(event, user) + elif event['type'] == 'm.reaction': + logging.debug('>----react_to_message----<') + await react_message(event, user) + elif event['type'] == 'm.room.member': if ('is_direct' in event['content'] and 'membership' in event['content']): @@ -166,6 +165,12 @@ async def new_message(event, user): if event['room_id'] == app.config['pnut']['global_room']: room = PnutChannels(pnut_chan=0, room_id=app.config['pnut']['global_room']) + elif event['room_id'] == app.config['pnut']['news_room']: + room = PnutChannels(pnut_chan=0, + room_id=app.config['pnut']['news_room']) + elif event['room_id'] == app.config['pnut']['bot_room']: + room = PnutChannels(pnut_chan=0, + room_id=app.config['pnut']['bot_room']) else: logging.debug('-room not mapped-') @@ -191,69 +196,32 @@ async def new_message(event, user): pnutpy.api.add_authorization_token(token) raw = {} - raw['io.pnut.core.crosspost'] = [crosspost_raw(event)] - text, oembed = await msg_from_event(event, user) + # raw['io.pnut.core.crosspost'] = [crosspost_raw(event)] + text, oembed, reply_to = await msg_from_event(event, user, room.pnut_chan) if text is None: return + text = prefix + text if oembed: raw['io.pnut.core.oembed'] = [oembed] logging.debug(oembed) - reply_to_id = None - ev_content = event['content'] - if 'm.relates_to' in ev_content: - m_relates_to = ev_content['m.relates_to'] - if 'm.in_reply_to' in m_relates_to: - reply_event_id = m_relates_to['m.in_reply_to']['event_id'] - e = Events.select().where(Events.event_id == - reply_event_id).first() - if e is not None: - reply_to_id = e.pnut_id - try: payload = {'raw': raw} - if room.pnut_chan == 0: - if reply_to_id is not None: - orig, meta = pnutpy.api.get_post(reply_to_id) - if orig.user.id != user.pnut_user_id: - author = orig.user.username - text = f"@{author} {text}" - if 'content' in orig: - cc_list = [] - for m in orig.content.entities.mentions: - if m.text == author: - continue - cc_list.append(f"@{m.text}") - if len(cc_list) > 0: - copy = " ".join(cc_list) - text = f"{text} /{copy}" - payload['reply_to'] = reply_to_id + if room.pnut_chan == 0: payload['text'] = text + if reply_to: + payload['reply_to'] = reply_to data, meta = pnutpy.api.create_post(data=payload) else: - if reply_to_id is not None: - orig, meta = pnutpy.api.get_message(room.pnut_chan, reply_to_id) - if orig.user.id != user.pnut_user_id: - author = orig.user.username - text = f"@{author} {text}" - if 'content' in orig: - cc_list = [] - for m in orig.content.entities.mentions: - if m.text == author: - continue - cc_list.append(f"@{m.text}") - if len(cc_list) > 0: - copy = " ".join(cc_list) - text = f"{text} /{copy}" - payload['reply_to'] = reply_to_id - payload['text'] = text - data, meta = pnutpy.api.create_message(room.pnut_chan, - data=payload) + if reply_to: + payload['reply_to'] = reply_to + data, meta = pnutpy.api.create_message(room.pnut_chan, data=payload) + bridge_event = Events( event_id=event['event_id'], room_id=event['room_id'], @@ -276,11 +244,11 @@ async def new_message(event, user): logging.exception('-something bad happened here-') return -async def msg_from_event(event, user): +async def msg_from_event(event, user, pnut_chan): text = None oembed = None - if (event['content']['msgtype'] == 'm.text' or - event['content']['msgtype'] == 'm.notice'): + reply_to = None + if event['content']['msgtype'] == 'm.text': text = event['content']['body'] fregex = re.compile(r'!file\s(\d+)') @@ -298,6 +266,36 @@ async def msg_from_event(event, user): 'file_token': pnut_file.pnut_file_token, 'format': 'oembed'}} + if text.startswith('!') and pnut_chan == 0: + logging.debug(">>>> stream command <<<<") + text, reply_to = await on_stream_command(event, user, pnut_chan) + + elif 'm.relates_to' in event['content']: + m_relates_to = event['content']['m.relates_to'] + if 'm.in_reply_to' in m_relates_to: + reply_event_id = m_relates_to['m.in_reply_to']['event_id'] + e = Events.select().where(Events.event_id == + reply_event_id).first() + if e is not None: + reply_to = e.pnut_id + if 'formatted_body' in event['content']: + formatted_body = event['content']['formatted_body'] + soup = BeautifulSoup(formatted_body, 'html.parser') + mx_reply = soup.findAll('mx-reply') + for item in mx_reply: + item.decompose() + body = soup.decode() + + m = fregex.search(body) + if m is not None: + file_id = m.group(1) + body = fregex.sub('', body) + + else: + body = text + + text = cmd_stream_reply(user, reply_to, body, pnut_chan) + elif event['content']['msgtype'] == 'm.emote': text = "* " + event['content']['body'] @@ -306,7 +304,7 @@ async def msg_from_event(event, user): event['content']['msgtype'] == 'm.audio') and user is not None): await media_from_event(event, user) - return None, None + return None, None, None elif (event['content']['msgtype'] == 'm.image' or event['content']['msgtype'] == 'm.video' or @@ -329,7 +327,7 @@ async def msg_from_event(event, user): logging.debug('-unknown msg type- ' + event['content']['msgtype']) return - return text, oembed + return text, oembed, reply_to def crosspost_raw(event): cross_profile = {'username': event['sender']} @@ -463,6 +461,50 @@ def delete_message(event, user): except pnutpy.errors.PnutPermissionDenied: pass +async def react_message(event, user): + 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['sender'] == as_id: + return + + if app.config['matrix']['namespace'] in event['sender']: + return + + if user is None: + return + + pnutpy.api.add_authorization_token(user.pnut_user_token) + + m_relates_to = event['content']['m.relates_to'] + react_event_id = m_relates_to['event_id'] + e = Events.select().where(Events.event_id == react_event_id).first() + if e is None: + logging.debug("- can't find the event to react to -") + return + + if e.pnut_channel == 0: + + # repost + if m_relates_to['key'] == '👀': + reply = cmd_stream_repost(e.pnut_id) + + # bookmark + elif m_relates_to['key'] in ['❤️', '⭐', '👍', '🔖']: + reply = cmd_stream_bookmark(e.pnut_id) + + else: + return + + reply = f'{event['sender']} {reply}' + reply_msg = TextMessageEventContent(msgtype=MessageType.NOTICE, + body=reply) + await matrix_api.send_message(event['room_id'], reply_msg) + def get_profile(userid): url = app.config['matrix']['homeserver'] + "/_matrix/client/r0/profile/" + userid r = requests.get(url) @@ -559,78 +601,6 @@ 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']) -# -# 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) - -# 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) - async def on_direct_invite(event): as_id = (f"@{app.config['matrix']['sender_local']}:" f"{app.config['matrix']['domain']}") @@ -730,7 +700,7 @@ async def on_direct_message(event, user, room): raw = {} raw['io.pnut.core.crosspost'] = [crosspost_raw(event)] - evtext, evraw = await msg_from_event(event, user) + evtext, evraw, noop = await msg_from_event(event, user, room.pnut_chan) text = prefix + evtext try: @@ -752,6 +722,180 @@ async def on_direct_message(event, user, room): return jsonify({}) +async def on_stream_command(event, user, pnut_chan): + 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 None, None + + args = event['content']['body'].split(' ') + + stream_commands = { + '!help': '\t\t\t\tdisplay help text', + '!reply': ' (post_id) (text)\treply to post', + '!replyg': ' (post_id) (text)\treply globally to post', + '!repost': ' (post_id)\t\trepost a post to your followers', + '!bookmark': ' (post_id)\t\tbookmark a post', + } + + if args[0] == '!help': + reply = f'{event['sender']}\n' + reply += '
Commands:\n'
+        for key in stream_commands:
+            reply += f"{key} {stream_commands[key]}\n"
+        reply += '
' + reply_msg = TextMessageEventContent(msgtype=MessageType.TEXT, + format=Format.HTML, + formatted_body=reply) + await matrix_api.send_message(event['room_id'], reply_msg) + return None, None + + elif args[0] == '!reply': + if len(args) < 3: + reply = f'{event['sender']} invalid command!\n' + reply += f"
Usage: !reply {stream_commands['!reply']}
" + reply_msg = TextMessageEventContent(msgtype=MessageType.TEXT, + format=Format.HTML, + formatted_body=reply) + await matrix_api.send_message(event['room_id'], reply_msg) + return None, None + text = cmd_stream_reply(user, args[1], ' '.join(args[2:]), pnut_chan) + reply_to = args[1] + return text, reply_to + + elif args[0] == '!replyg': + if len(args) < 3: + reply = f'{event['sender']} invalid command!\n' + reply += f"
Usage: !replyg {stream_commands['!replyg']}
" + reply_msg = TextMessageEventContent(msgtype=MessageType.TEXT, + format=Format.HTML, + formatted_body=reply) + await matrix_api.send_message(event['room_id'], reply_msg) + return None, None + text = ' '.join(args[2:]) + reply_to = args[1] + return text, reply_to + + elif args[0] == '!repost': + if len(args) < 2: + reply = f'{event['sender']} invalid command!\n' + reply += f"
Usage: !repost {stream_commands['!repost']}
" + reply_msg = TextMessageEventContent(msgtype=MessageType.TEXT, + format=Format.HTML, + formatted_body=reply) + reply = await matrix_api.send_message(event['room_id'], reply_msg) + return None, None + reply = cmd_stream_repost(args[1]) + reply = f'{event['sender']} {reply}' + reply_msg = TextMessageEventContent(msgtype=MessageType.NOTICE, + body=reply) + await matrix_api.send_message(event['room_id'], reply_msg) + return None, None + + elif args[0] == '!bookmark': + if len(args) < 2: + reply = f'{event['sender']} invalid command!\n' + reply += f"
Usage: !bookmark {stream_commands['!bookmark']}
" + reply_msg = TextMessageEventContent(msgtype=MessageType.TEXT, + format=Format.HTML, + formatted_body=reply) + reply = await matrix_api.send_message(event['room_id'], reply_msg) + return None, None + reply = cmd_stream_bookmark(args[1]) + reply = f'{event['sender']} {reply}' + reply_msg = TextMessageEventContent(msgtype=MessageType.NOTICE, + body=reply) + await matrix_api.send_message(event['room_id'], reply_msg) + return None, None + + else: + logging.debug('>>> unknown command') + logging.debug(text) + logging.debug(user) + return None, None + +def cmd_stream_reply(user, post_id, text, pnut_chan): + logging.debug(f'>>> reply to {post_id}') + logging.debug(text) + + try: + pnut_user, meta = pnutpy.api.get_user(user.pnut_user_id) + + if pnut_chan == 0: + orig, meta = pnutpy.api.get_post(post_id) + + else: + orig, meta = pnutpy.api.get_message(pnut_chan, post_id) + + if orig.user.id != user.pnut_user_id: + author = orig.user.username + text = f"@{author} {text}" + if 'content' in orig: + cc_list = [] + for m in orig.content.entities.mentions: + if m.text == author: + continue + if m.text == pnut_user.username: + continue + cc_list.append(f"@{m.text}") + if len(cc_list) > 0: + copy = " ".join(cc_list) + text = f"{text} /{copy}" + + return text + + except pnutpy.errors.PnutAuthAPIException: + logging.error('>>> PNUT AUTH ERROR!!!') + + except Exception: + logging.exception('!!!!!!!!!!!!!!!!!!!!!') + +def cmd_stream_bookmark(post_id): + logging.debug(f'>>> bookmark {post_id}') + + try: + orig, meta = pnutpy.api.get_post(post_id) + logging.debug(f'>>> {orig.content.text}') + + if orig.you_bookmarked: + post, meta = pnutpy.api.unbookmark_post(post_id) + return f'unbookmarked post {post.id}' + + else: + post, meta = pnutpy.api.bookmark_post(post_id) + return f'bookmarked post {post.id}' + + except pnutpy.errors.PnutAuthAPIException: + logging.error('>>> PNUT AUTH ERROR!!!') + + except Exception: + logging.exception('!!!!!!!!!!!!!!!!!!!!!') + +def cmd_stream_repost(post_id): + logging.debug(f'>>> repost {post_id}') + + try: + orig, meta = pnutpy.api.get_post(post_id) + logging.debug(f'>>> {orig.content.text}') + + if orig.you_reposted: + post, meta = pnutpy.api.unrepost_post(post_id) + return f'unreposted post {post_id}' + + else: + post, meta = pnutpy.api.repost_post(post_id) + return f'reposted post {post_id}' + + except pnutpy.errors.PnutAuthAPIException: + logging.error('>>> PNUT AUTH ERROR!!!') + + except Exception: + logging.exception('!!!!!!!!!!!!!!!!!!!!!') + async def on_control_message(event, user): as_id = (f"@{app.config['matrix']['sender_local']}:" f"{app.config['matrix']['domain']}") diff --git a/src/pnut_matrix/pnutservice.py b/src/pnut_matrix/pnutservice.py index 8c244c7..63c8169 100644 --- a/src/pnut_matrix/pnutservice.py +++ b/src/pnut_matrix/pnutservice.py @@ -196,7 +196,7 @@ async def new_pnut_post(post, meta): postlink = f"https://posts.pnut.io/{post.id}" plaintext = f"{post.content.text}\n{postlink}" htmltext = (f"{post.content.html}" - f"  [🔗]") + f"  [🔗] - {post.id}") eventtext = TextMessageEventContent(msgtype=MessageType.TEXT, format=Format.HTML, body=plaintext,