diff --git a/config.json.example b/config.json.example index 67378e0..624d166 100644 --- a/config.json.example +++ b/config.json.example @@ -15,5 +15,7 @@ "user_id_format": "@telegram_{}:DOMAIN.TLD", "db_url": "sqlite:///database.db", + "hide_membership_changes": "True", + "as_port": 5000 } diff --git a/telematrix/__init__.py b/telematrix/__init__.py index 75ef826..96c4257 100644 --- a/telematrix/__init__.py +++ b/telematrix/__init__.py @@ -44,6 +44,7 @@ USER_ID_FORMAT = CONFIG['user_id_format'] DATABASE_URL = CONFIG['db_url'] + HIDE_MEMBERSHIP_CHANGES = CONFIG['hide_membership_changes'] AS_PORT = CONFIG['as_port'] if 'as_port' in CONFIG else 5000 except (OSError, IOError) as exception: @@ -57,6 +58,8 @@ MATRIX_SESS = ClientSession() SHORTEN_SESS = ClientSession() +MT = mimetypes.MimeTypes() + def create_response(code, obj): """ @@ -139,22 +142,21 @@ async def shorten_url(url): else: return url + def matrix_is_telegram(user_id): + """ + Check if the user is a virtual telegram user or a real matrix user. Returns True if it is a fake + user (telegram virtual user). + :param user_id: The matrix id of the user. + :return: True if a virtual telegram user. + """ username = user_id.split(':')[0][1:] return username.startswith('telegram_') + def get_username(user_id): return user_id.split(':')[0][1:] -mime_extensions = { - 'image/jpeg': 'jpg', - 'image/gif': 'gif', - 'image/png': 'png', - 'image/tiff': 'tif', - 'image/x-tiff': 'tif', - 'image/bmp': 'bmp', - 'image/x-windows-bmp': 'bmp' -} async def matrix_transaction(request): """ @@ -209,7 +211,6 @@ async def matrix_transaction(request): if matrix_is_telegram(user_id): continue - sender = db.session.query(db.MatrixUser)\ .filter_by(matrix_id=user_id).first() @@ -231,40 +232,102 @@ async def matrix_transaction(request): if content['msgtype'] == 'm.text': msg, mode = format_matrix_msg('{}', content) - response = await group.send_text("{}: {}".format(displayname, msg), parse_mode='HTML') + response = await group.send_text("{}: {}".format(displayname, msg), + parse_mode='HTML') elif content['msgtype'] == 'm.notice': msg, mode = format_matrix_msg('{}', content) - response = await group.send_text("[{}] {}".format(displayname, msg), parse_mode=mode) + response = await group.send_text("[{}] {}".format(displayname, msg), + parse_mode=mode) elif content['msgtype'] == 'm.emote': msg, mode = format_matrix_msg('{}', content) - response = await group.send_text("* {} {}".format(displayname, msg), parse_mode=mode) - elif content['msgtype'] == 'm.image': + response = await group.send_text("* {} {}".format(displayname, msg), + parse_mode=mode) + elif content['msgtype'] in ['m.image', 'm.audio', 'm.video', 'm.file']: try: url = urlparse(content['url']) # Append the correct extension if it's missing or wrong - ext = mime_extensions[content['info']['mimetype']] - if not content['body'].endswith(ext): - content['body'] += '.' + ext + try: + exts = MT.types_map_inv[1][content['info']['mimetype']] + if not content['body'].endswith(tuple(exts)): + content['body'] += '.' + exts[0] + except KeyError: + pass # Download the file await download_matrix_file(url, content['body']) - with open('/tmp/{}'.format(content['body']), 'rb') as img_file: + with open('/tmp/{}'.format(content['body']), 'rb') as file: # Create the URL and shorten it url_str = MATRIX_HOST_EXT + \ '_matrix/media/r0/download/{}{}' \ .format(url.netloc, quote(url.path)) url_str = await shorten_url(url_str) - caption = '{} sent an image'.format(displayname) - response = await group.send_photo(img_file, caption=caption) + if content['msgtype'] == 'm.image': + if content['info']['mimetype'] == 'image/gif': + # Send gif as a video, so telegram can display it animated + caption = '{} sent a gif'.format(displayname) + await group.send_chat_action('upload_video') + response = await group.send_video(file, caption=caption) + else: + caption = '{} sent an image'.format(displayname) + await group.send_chat_action('upload_photo') + response = await group.send_photo(file, caption=caption) + elif content['msgtype'] == 'm.video': + caption = '{} sent a video'.format(displayname) + await group.send_chat_action('upload_video') + response = await group.send_video(file, caption=caption) + elif content['msgtype'] == 'm.audio': + caption = '{} sent an audio file'.format(displayname) + await group.send_chat_action('upload_audio') + response = await group.send_audio(file, caption=caption) + elif content['msgtype'] == 'm.file': + caption = '{} sent a file'.format(displayname) + await group.send_chat_action('upload_document') + response = await group.send_document(file, caption=caption) except: pass else: print('Unsupported message type {}'.format(content['msgtype'])) print(json.dumps(content, indent=4)) + elif event['type'] == 'm.sticker': + user_id = event['user_id'] + if matrix_is_telegram(user_id): + continue + + sender = db.session.query(db.MatrixUser) \ + .filter_by(matrix_id=user_id).first() + + if not sender: + response = await matrix_get('client', 'profile/{}/displayname' + .format(user_id), None) + try: + displayname = response['displayname'] + except KeyError: + displayname = get_username(user_id) + sender = db.MatrixUser(user_id, displayname) + db.session.add(sender) + else: + displayname = sender.name or get_username(user_id) + content = event['content'] + + try: + url = urlparse(content['url']) + await download_matrix_file(url, content['body']) + + png_image = Image.open('/tmp/{}'.format(content['body'])) + png_image.save('/tmp/{}.webp'.format(content['body']), 'WEBP') + with open('/tmp/{}.webp'.format(content['body']), 'rb') as file: + response = await group.send_document(file) + + except: + pass + elif event['type'] == 'm.room.member': + if HIDE_MEMBERSHIP_CHANGES: # Hide everything, could be improved to be + # more specific + continue if matrix_is_telegram(event['state_key']): continue @@ -428,6 +491,27 @@ async def upload_tgfile_to_matrix(file_id, user_id, mime='image/jpeg', convert_t return None, 0 +async def upload_file_to_matrix(file_id, user_id, mime): + """ + Upload to the matrix homeserver all kinds of files based on mime informations. + :param file_id: Telegram file id + :param user_id: Matrix user id + :param mime: mime type of the file + :return: Tuple (JSON response, data length) if success, None else + """ + file_path = (await TG_BOT.get_file(file_id))['file_path'] + request = await TG_BOT.download_file(file_path) + data = await request.read() + + j = await matrix_post('media', 'upload', user_id, data, mime) + length = len(data) + + if 'content_uri' in j: + return j['content_uri'], length + else: + return None, 0 + + async def register_join_matrix(chat, room_id, user_id): name = chat.sender['first_name'] if 'last_name' in chat.sender: @@ -451,6 +535,7 @@ async def register_join_matrix(chat, room_id, user_id): user_id, {'displayname': name}) await matrix_post('client', 'join/{}'.format(room_id), user_id, {}) + async def update_matrix_displayname_avatar(tg_user): name = tg_user['first_name'] if 'last_name' in tg_user: @@ -488,7 +573,47 @@ async def update_matrix_displayname_avatar(tg_user): await matrix_put('client', 'profile/{}/avatar_url'.format(user_id), user_id, {'avatar_url':None}) db.session.add(db_user) db.session.commit() - + + +def create_file_name(obj_type, mime): + try: + ext = MT.types_map_inv[1][mime][0] + except KeyError: + try: + ext = MT.types_map_inv[0][mime][0] + except KeyError: + ext = '' + name = '{}_{}{}'.format(obj_type, int(time() * 1000), ext) + return name + + +async def send_file_to_matrix(chat, room_id, user_id, txn_id, body, uri, info, msgtype): + j = await send_matrix_message(room_id, user_id, txn_id, body=body, + url=uri, info=info, msgtype=msgtype) + + if 'errcode' in j and j['errcode'] == 'M_FORBIDDEN': + await register_join_matrix(chat, room_id, user_id) + await send_matrix_message(room_id, user_id, txn_id + 'join', + body=body, url=uri, info=info, msgtype=msgtype) + + if 'caption' in chat.message: + await send_matrix_message(room_id, user_id, txn_id + 'caption', + body=chat.message['caption'], msgtype='m.text') + + if 'event_id' in j: + name = chat.sender['first_name'] + if 'last_name' in chat.sender: + name += " " + chat.sender['last_name'] + name += " (Telegram)" + message = db.Message( + chat.message['chat']['id'], + chat.message['message_id'], + room_id, + j['event_id'], + name) + db.session.add(message) + db.session.commit() + @TG_BOT.handle('sticker') async def aiotg_sticker(chat, sticker): @@ -497,7 +622,7 @@ async def aiotg_sticker(chat, sticker): print('Unknown telegram chat {}: {}'.format(chat, chat.id)) return - await update_matrix_displayname_avatar(chat.sender); + await update_matrix_displayname_avatar(chat.sender) room_id = link.matrix_room user_id = USER_ID_FORMAT.format(chat.sender['id']) @@ -511,32 +636,8 @@ async def aiotg_sticker(chat, sticker): body = 'Sticker_{}.png'.format(int(time() * 1000)) if uri: - j = await send_matrix_message(room_id, user_id, txn_id, body=body, - url=uri, info=info, msgtype='m.image') - - if 'errcode' in j and j['errcode'] == 'M_FORBIDDEN': - await register_join_matrix(chat, room_id, user_id) - await send_matrix_message(room_id, user_id, txn_id + 'join', - body=body, url=uri, info=info, - msgtype='m.image') - - if 'caption' in chat.message: - await send_matrix_message(room_id, user_id, txn_id + 'caption', - body=chat.message['caption'], - msgtype='m.text') - if 'event_id' in j: - name = chat.sender['first_name'] - if 'last_name' in chat.sender: - name += " " + chat.sender['last_name'] - name += " (Telegram)" - message = db.Message( - chat.message['chat']['id'], - chat.message['message_id'], - room_id, - j['event_id'], - name) - db.session.add(message) - db.session.commit() + await send_file_to_matrix(chat, room_id, user_id, txn_id, body, uri, info, 'm.image') + @TG_BOT.handle('photo') async def aiotg_photo(chat, photo): @@ -551,39 +652,107 @@ async def aiotg_photo(chat, photo): txn_id = quote('{}{}'.format(chat.message['message_id'], chat.id)) file_id = photo[-1]['file_id'] + uri, length = await upload_tgfile_to_matrix(file_id, user_id) info = {'mimetype': 'image/jpeg', 'size': length, 'h': photo[-1]['height'], 'w': photo[-1]['width']} body = 'Image_{}.jpg'.format(int(time() * 1000)) if uri: - j = await send_matrix_message(room_id, user_id, txn_id, body=body, - url=uri, info=info, msgtype='m.image') - - if 'errcode' in j and j['errcode'] == 'M_FORBIDDEN': - await register_join_matrix(chat, room_id, user_id) - await send_matrix_message(room_id, user_id, txn_id + 'join', - body=body, url=uri, info=info, - msgtype='m.image') - - if 'caption' in chat.message: - await send_matrix_message(room_id, user_id, txn_id + 'caption', - body=chat.message['caption'], - msgtype='m.text') + await send_file_to_matrix(chat, room_id, user_id, txn_id, body, uri, info, 'm.image') + + +@TG_BOT.handle('audio') +async def aiotg_audio(chat, audio): + link = db.session.query(db.ChatLink).filter_by(tg_room=chat.id).first() + if not link: + print('Unknown telegram chat {}: {}'.format(chat, chat.id)) + return + + await update_matrix_displayname_avatar(chat.sender); + room_id = link.matrix_room + user_id = USER_ID_FORMAT.format(chat.sender['id']) + txn_id = quote('{}{}'.format(chat.message['message_id'], chat.id)) + + file_id = audio['file_id'] + try: + mime = audio['mime_type'] + except KeyError: + mime = 'audio/mp3' + uri, length = await upload_file_to_matrix(file_id, user_id, mime) + info = {'mimetype': mime, 'size': length} + body = create_file_name('Audio', mime) + + if uri: + await send_file_to_matrix(chat, room_id, user_id, txn_id, body, uri, info, 'm.audio') + + +@TG_BOT.handle('document') +async def aiotg_document(chat, document): + link = db.session.query(db.ChatLink).filter_by(tg_room=chat.id).first() + if not link: + print('Unknown telegram chat {}: {}'.format(chat, chat.id)) + return + + await update_matrix_displayname_avatar(chat.sender); + room_id = link.matrix_room + user_id = USER_ID_FORMAT.format(chat.sender['id']) + txn_id = quote('{}{}'.format(chat.message['message_id'], chat.id)) + + file_id = document['file_id'] + try: + mime = document['mime_type'] + except KeyError: + mime = '' + uri, length = await upload_file_to_matrix(file_id, user_id, mime) + info = {'mimetype': mime, 'size': length} + + if uri: + # We check if the document can be sent in a better way (for example a photo or a gif) + # For gif, that's still not perfect : it's sent as a video to matrix instead of a real + # gif image + if 'image' in mime: + msgtype = 'm.image' + body = create_file_name('Image', mime) + elif 'video' in mime: + msgtype = 'm.video' + body = create_file_name('Video', mime) + elif 'audio' in mime: + msgtype = 'm.audio' + body = create_file_name('Audio', mime) + else: + msgtype = 'm.file' + body = create_file_name('File', mime) + await send_file_to_matrix(chat, room_id, user_id, txn_id, body, uri, info, msgtype) + + +# This doesn't catch video from telegram, I don't know why +# The handler is never called +@TG_BOT.handle('video') +async def aiotg_video(chat, video): + link = db.session.query(db.ChatLink).filter_by(tg_room=chat.id).first() + if not link: + print('Unknown telegram chat {}: {}'.format(chat, chat.id)) + return + + await update_matrix_displayname_avatar(chat.sender); + room_id = link.matrix_room + user_id = USER_ID_FORMAT.format(chat.sender['id']) + txn_id = quote('{}{}'.format(chat.message['message_id'], chat.id)) + + file_id = video['file_id'] + try: + mime = video['mime_type'] + except KeyError: + mime = 'video/mp4' + uri, length = await upload_file_to_matrix(file_id, user_id, mime) + info = {'mimetype': mime, 'size': length, 'h': video['height'], + 'w': video['width']} + body = create_file_name('Video', mime) + + if uri: + await send_file_to_matrix(chat, room_id, user_id, txn_id, body, uri, info, 'm.video') - if 'event_id' in j: - name = chat.sender['first_name'] - if 'last_name' in chat.sender: - name += " " + chat.sender['last_name'] - name += " (Telegram)" - message = db.Message( - chat.message['chat']['id'], - chat.message['message_id'], - room_id, - j['event_id'], - name) - db.session.add(message) - db.session.commit() @TG_BOT.command(r'/alias') async def aiotg_alias(chat, match): @@ -637,11 +806,11 @@ async def aiotg_message(chat, match): re_msg['from']['last_name']) else: msg_from = '{} (Telegram)'.format(re_msg['from']['first_name']) - date = datetime.fromtimestamp(re_msg['date']) \ - .strftime('%Y-%m-%d %H:%M:%S') + date = datetime.fromtimestamp(re_msg['date']).strftime('%Y-%m-%d %H:%M:%S') reply_mx_id = db.session.query(db.Message)\ - .filter_by(tg_group_id=chat.message['chat']['id'], tg_message_id=chat.message['reply_to_message']['message_id']).first() + .filter_by(tg_group_id=chat.message['chat']['id'], + tg_message_id=chat.message['reply_to_message']['message_id']).first() html_message = html.escape(message).replace('\n', '
') if 'text' in re_msg: @@ -658,7 +827,9 @@ async def aiotg_message(chat, match): quoted_msg = 'Reply to {}:\n{}\n\n{}' \ .format(reply_mx_id.displayname, quoted_msg, message) quoted_html = 'Reply to {}:
{}

{}

' \ - .format(html.escape(room_id), html.escape(reply_mx_id.matrix_event_id), html.escape(reply_mx_id.displayname), + .format(html.escape(room_id), + html.escape(reply_mx_id.matrix_event_id), + html.escape(reply_mx_id.displayname), quoted_html, html_message) else: quoted_msg = 'Reply to {}:\n{}\n\n{}' \