import json import requests import logging import re import pnutpy from matrix_client.api import MatrixHttpApi from matrix_client.api import MatrixError, MatrixRequestError from models import Avatars, Rooms, Events, Users from database import db_session from sqlalchemy import and_ from flask import Flask, jsonify, request, abort logger = logging.getLogger(__name__) app = Flask(__name__) @app.errorhandler(404) def not_found(error): return jsonify({'errcode':'COM.MONKEYSTEW.PNUT_NOT_FOUND'}), 404 @app.errorhandler(403) def forbidden(error): return jsonify({'errcode':'COM.MONKEYSTEW.PNUT_FORBIDDEN'}), 403 @app.teardown_appcontext def shutdown_session(exception=None): db_session.remove() @app.route("/rooms/") 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() if room is not None: abort(404) token = app.config['MATRIX_PNUT_TOKEN'] pnutpy.api.add_authorization_token(token) try: channel, meta = pnutpy.api.get_channel(channel_id, include_raw=1) if 'is_active' in channel and channel.is_active == False: logger.debug("-channel isn't active-") abort(404) channel_settings = {} for item in channel.raw: if item.type == 'io.pnut.core.chat-settings': channel_settings = item.value # Matrix sdk doesn't include all details in a single call room = {'room_alias_name': alias_localpart} if 'name' in channel_settings: room['name'] = channel_settings['name'] if 'description' in channel_settings: room['topic'] = channel_settings['description'] if channel.acl.read.any_user: room['preset'] = 'public_chat' room['visibility'] = 'public' url = app.config['MATRIX_HOST'] + '/_matrix/client/api/v1/createRoom?access_token=' url += app.config['MATRIX_AS_TOKEN'] headers = {"Content-Type":"application/json"} r = requests.post(url, headers=headers, data=json.dumps(room)) if r.status_code == 200: pnutpy.api.subscribe_channel(channel_id) rdata = r.json() rr = Rooms( room_id=rdata['room_id'], pnut_chan=channel_id, portal=True ) db_session.add(rr) db_session.commit() except Exception: logger.exception("-couldn't get the pnut channel-") abort(404) return jsonify({}) @app.route("/transactions/", methods=["PUT"]) def on_receive_events(transaction): access_token = request.args.get('access_token', '') if access_token != app.config['MATRIX_HS_TOKEN']: abort(403) events = request.get_json()["events"] for event in events: logger.debug(event) # TODO: route event if it's in the control room if app.config['MATRIX_ADMIN_ROOM'] and app.config['MATRIX_ADMIN_ROOM'] == event['room_id']: return on_admin_event(event) user = Users.query.filter(Users.matrix_id == event['user_id']).one_or_none() if event['type'] == 'm.room.message': new_message(event, user) elif event['type'] == 'm.room.redaction': delete_message(event, user) elif event['type'] == 'm.room.member': logger.debug("-room member event") return jsonify({}) def new_message(event, user): if app.config['MATRIX_PNUT_PREFIX'] in event['user_id'] or 'pnut-bridge' in event['user_id']: logger.debug('-skipping dup event-') return room = Rooms.query.filter(Rooms.room_id == event['room_id']).one_or_none() if room is None: logger.debug('-room not mapped-') return if 'msgtype' not in event['content']: logger.debug('-unknown message type-') return if user is not None: token = user.pnut_user_token prefix = "" else: token = app.config['MATRIX_PNUT_TOKEN'] prefix = "[" + get_displayname(event['user_id']) + "] (" + event['user_id'] + ")\n" prefix = prefix.replace('@', '@\v') pnutpy.api.add_authorization_token(token) text = None embed = None if event['content']['msgtype'] == 'm.text' or event['content']['msgtype'] == 'm.notice': text = event['content']['body'] elif event['content']['msgtype'] == 'm.emote': text = "* " + event['content']['body'] elif event['content']['msgtype'] == 'm.image': text = event['content']['body'] + "\n" text += app.config['MATRIX_HOST'] + '/_matrix/media/r0/download/' + event['content']['url'][6:] embed = [raw_from_event(event)] elif event['content']['msgtype'] == 'm.video': text = event['content']['body'] + "\n" text += app.config['MATRIX_HOST'] + '/_matrix/media/r0/download/' + event['content']['url'][6:] elif event['content']['msgtype'] == 'm.audio': text = event['content']['body'] + "\n" text += app.config['MATRIX_HOST'] + '/_matrix/media/r0/download/' + event['content']['url'][6:] elif event['content']['msgtype'] == 'm.file': text = event['content']['body'] + "\n" text += app.config['MATRIX_HOST'] + '/_matrix/media/r0/download/' + event['content']['url'][6:] else: logger.debug('-unknown msg type- ' + event['content']['msgtype']) return text = prefix + text try: msg, meta = pnutpy.api.create_message(room.pnut_chan, data={'text': text, 'raw': embed}) revent = 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 ) db_session.add(revent) db_session.commit() if user is not None: cctag = re.search('##$', text) if cctag: cname = get_channel_settings(room.pnut_chan)['name'] text = text[:-2] text += '\n\n[' + cname + "](https://patter.chat/room.html?channel=" + str(room.pnut_chan) + ")" r, meta = pnutpy.api.create_post(data={'text': text}) except pnutpy.errors.PnutAuthAPIException: logger.exception('-unable to post to pnut channel-') return except Exception: logger.exception('-something bad happened here-') return def raw_from_event(event): url = app.config['MATRIX_HOST'] + '/_matrix/media/r0/download/' + event['content']['url'][6:] if event['content']['msgtype'] == 'm.image': value = {'type': "photo", 'version': "1.0"} value['url'] = url value['title'] = event['content']['body'] if 'info' in event['content']: value['width'] = event['content']['info']['w'] value['height'] = event['content']['info']['h'] if 'thumbnail_info' in event['content']['info']: value['thumbnail_url'] = app.config['MATRIX_HOST'] + '/_matrix/media/r0/download/' + event['content']['info']['thumbnail_url'][6:] value['thumbnail_width'] = event['content']['info']['thumbnail_info']['w'] value['thumbnail_height'] = event['content']['info']['thumbnail_info']['h'] elif event['content']['msgtype'] == 'm.video': # TODO: Need to sort out the oembed for this media type value = {'type': "html5video", 'version': "1.0"} source = {'url': url} value['title'] = event['content']['body'] if 'info' in event['content']: value['width'] = event['content']['info']['w'] value['height'] = event['content']['info']['h'] source['type'] = event['content']['info']['mimetype'] else: return None value['sources'] = [source] elif event['content']['msgtype'] == 'm.audio': # TODO: Need to sort out the oembed for this media type value = {'type': "audio", 'version': "1.0"} return None else: return None return {'type': "io.pnut.core.oembed", 'value': value} def delete_message(event, user): # TODO: should there be moderator handled redactions? if user is not None: token = user.pnut_user_token else: 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() if e is None: logger.debug("- can't find the event to remove -") return r, meta = pnutpy.api.delete_message(e.pnut_chan_id, e.pnut_msg_id) e.deleted = True db_session.commit() def get_displayname(userid): url = "http://localhost:8008/_matrix/client/r0/profile/" + userid r = requests.get(url) if r.status_code == 200: data = json.loads(r.text) if 'displayname' in data: return data["displayname"] return userid def get_channel_settings(channel_id): channel_settings = {} try: channel, meta = pnutpy.api.get_channel(channel_id, include_raw=1) for item in channel.raw: if item.type == 'io.pnut.core.chat-settings': channel_settings = item.value except Exception: logger.exception('-unable to get channel settings-') return channel_settings def on_admin_event(event): matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'], token=app.config['MATRIX_AS_TOKEN']) logger.debug("- admin room event recieved -") if event['type'] != 'm.room.message': return jsonify({}) msg = event['content']['body'].split(' ') if msg[0] == 'help': if len(msg) > 1: matrix_api.send_message(event['room_id'], cmd_admin_help(msg[1])) else: matrix_api.send_message(event['room_id'], cmd_admin_help()) elif msg[0] == 'list': matrix_api.send_message(event['room_id'], cmd_admin_list()) elif msg[0] == 'unlink': if len(msg) > 1: matrix_api.send_message(event['room_id'], cmd_admin_unlink(msg[1])) else: matrix_api.send_message(event['room_id'], cmd_admin_help('unlink')) elif msg[0] == 'link': if len(msg) > 2: matrix_api.send_message(event['room_id'], cmd_admin_link(msg[1], msg[2])) else: matrix_api.send_message(event['room_id'], cmd_admin_help('link')) return jsonify({}) 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." 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" 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 text def cmd_admin_list(): text = "" rooms = Rooms.query.all() 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' if room.portal: text += "(portal)" text += '\n' return text def cmd_admin_link(room_id, pnut_chan_id): matrix_api = MatrixHttpApi(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: return "- room may already be linked -" try: channel, meta = pnutpy.api.subscribe_channel(pnut_chan_id) r = 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 -" logger.exception(errmsg) return errmsg except Exception: errmsg = "- unable to link room for some reason -" logger.exception(errmsg) return errmsg def cmd_admin_unlink(id): matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'], token=app.config['MATRIX_AS_TOKEN']) pnutpy.api.add_authorization_token(app.config['MATRIX_PNUT_TOKEN']) if id.startswith('!'): room = Rooms.query.filter(Rooms.room_id == id).one_or_none() else: room = Rooms.query.filter(Rooms.pnut_chan == id).one_or_none() if room.portal: alias = "#" + app.config['MATRIX_PNUT_PREFIX'] alias += str(room.pnut_chan) + ":" alias += app.config['MATRIX_DOMAIN'] matrix_api.remove_room_alias(alias) # Kicking users needs at least moderator privs members = matrix_api.get_room_members(room.room_id) reason = "Portal room has been unlinked by administrator" for m in members['chunk']: if m['membership'] == 'join' and m['sender'] != app.config['MATRIX_AS_ID']: if room.portal: matrix_api.kick_user(room.room_id, m['sender'], reason=reason) else: if m['sender'].startswith("@" + app.config['MATRIX_PNUT_PREFIX']): matrix_api.kick_user(room.room_id, m['sender'], reason=reason) try: channel, meta = pnutpy.api.unsubscribe_channel(room.pnut_chan) matrix_api.leave_room(room.room_id) if room is not None: db_session.delete(room) db_session.commit() return "- room has been unlinked -" else: return "- unable to locate room to unlink -" except Exception: errmsg = "- error while unlinking room -" logger.exception(errmsg) return errmsg