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{}' \