├── app_service.py ├── requirements.txt ├── .travis.yml ├── asconfig.yaml.example ├── config.json.example ├── LICENSE ├── .gitignore ├── telematrix ├── database.py └── __init__.py └── README.md /app_service.py: -------------------------------------------------------------------------------- 1 | import telematrix 2 | 3 | if __name__ == '__main__': 4 | telematrix.main() 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==1.0.5 2 | aiotg==0.7.11 3 | beautifulsoup4==4.5.1 4 | sqlalchemy==1.1.3 5 | Pillow==4.0.0 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: ["3.5"] 3 | install: 4 | - "pip install -r requirements.txt" 5 | - "pip install pylint" 6 | script: 7 | - "pylint app_service.py telematrix/*.py -E" 8 | -------------------------------------------------------------------------------- /asconfig.yaml.example: -------------------------------------------------------------------------------- 1 | url: "http://localhost:5000" 2 | 3 | as_token: "AS_TOKEN_HERE" 4 | hs_token: "HS_TOKEN_HERE" 5 | id: "telematrix" 6 | 7 | sender_localpart: telegram 8 | namespaces: 9 | users: 10 | - exclusive: true 11 | regex: '@telegram_.*' 12 | rooms: [] 13 | aliases: 14 | - exclusive: false 15 | regex: '#telegram_.*' 16 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "tokens": { 3 | "hs": "HS_KEY", 4 | "as": "AS_KEY", 5 | "telegram": "TELEGRAM_BOT_API_KEY", 6 | "google": "GOOGLE_API_KEY" 7 | }, 8 | 9 | "hosts": { 10 | "internal": "http://127.0.0.1:PORT/", 11 | "external": "https://DOMAIN.TLD/", 12 | "bare": "DOMAIN.TLD" 13 | }, 14 | 15 | "user_id_format": "@telegram_{}:DOMAIN.TLD", 16 | "db_url": "sqlite:///database.db", 17 | 18 | "as_port": 5000 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Sijmen Schoon 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python,virtualenv,vim,macos 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # IPython Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv/ 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | 96 | 97 | ### VirtualEnv ### 98 | # Virtualenv 99 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 100 | .Python 101 | [Bb]in 102 | [Ii]nclude 103 | [Ll]ib 104 | [Ll]ib64 105 | [Ll]ocal 106 | [Ss]cripts 107 | pyvenv.cfg 108 | .venv 109 | pip-selfcheck.json 110 | 111 | 112 | ### Vim ### 113 | # swap 114 | [._]*.s[a-w][a-z] 115 | [._]s[a-w][a-z] 116 | # session 117 | Session.vim 118 | # temporary 119 | .netrwhist 120 | *~ 121 | # auto-generated tag files 122 | tags 123 | 124 | 125 | ### macOS ### 126 | *.DS_Store 127 | .AppleDouble 128 | .LSOverride 129 | 130 | # Icon must end with two \r 131 | Icon 132 | 133 | 134 | # Thumbnails 135 | ._* 136 | 137 | # Files that might appear in the root of a volume 138 | .DocumentRevisions-V100 139 | .fseventsd 140 | .Spotlight-V100 141 | .TemporaryItems 142 | .Trashes 143 | .VolumeIcon.icns 144 | .com.apple.timemachine.donotpresent 145 | 146 | # Directories potentially created on remote AFP share 147 | .AppleDB 148 | .AppleDesktop 149 | Network Trash Folder 150 | Temporary Items 151 | .apdisk 152 | 153 | # Config 154 | config.json 155 | database.db 156 | -------------------------------------------------------------------------------- /telematrix/database.py: -------------------------------------------------------------------------------- 1 | """ 2 | Defines all database models and provides necessary functions to manage it. 3 | """ 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy.orm import sessionmaker 6 | import sqlalchemy as sa 7 | 8 | engine = None 9 | Base = declarative_base() 10 | Session = sessionmaker() 11 | session = None 12 | 13 | class ChatLink(Base): 14 | """Describes a link between the Telegram and Matrix side of the bridge.""" 15 | __tablename__ = 'chat_link' 16 | 17 | id = sa.Column(sa.Integer, primary_key=True) 18 | matrix_room = sa.Column(sa.String) 19 | tg_room = sa.Column(sa.BigInteger) 20 | active = sa.Column(sa.Boolean) 21 | 22 | def __init__(self, matrix_room, tg_room, active): 23 | self.matrix_room = matrix_room 24 | self.tg_room = tg_room 25 | self.active = active 26 | 27 | 28 | class TgUser(Base): 29 | """Describes a user on the Telegram side of the bridge.""" 30 | __tablename__ = 'tg_user' 31 | 32 | id = sa.Column(sa.Integer, primary_key=True) 33 | tg_id = sa.Column(sa.BigInteger) 34 | name = sa.Column(sa.String) 35 | profile_pic_id = sa.Column(sa.String, nullable=True) 36 | 37 | def __init__(self, tg_id, name, profile_pic_id=None): 38 | self.tg_id = tg_id 39 | self.name = name 40 | self.profile_pic_id = profile_pic_id 41 | 42 | 43 | class MatrixUser(Base): 44 | """Describes a user on the Matrix side of the bridge.""" 45 | __tablename__ = 'matrix_user' 46 | 47 | id = sa.Column(sa.Integer, primary_key=True) 48 | matrix_id = sa.Column(sa.String) 49 | name = sa.Column(sa.String) 50 | 51 | def __init__(self, matrix_id, name): 52 | self.matrix_id = matrix_id 53 | self.name = name 54 | 55 | class Message(Base): 56 | """Describes a message in a room bridged between Telegram and Matrix""" 57 | __tablename__ = "message" 58 | 59 | id = sa.Column(sa.Integer, primary_key=True) 60 | tg_group_id = sa.Column(sa.BigInteger) 61 | tg_message_id = sa.Column(sa.BigInteger) 62 | 63 | matrix_room_id = sa.Column(sa.String) 64 | matrix_event_id = sa.Column(sa.String) 65 | 66 | displayname = sa.Column(sa.String) 67 | 68 | def __init__(self, tg_group_id, tg_message_id, matrix_room_id, matrix_event_id, displayname): 69 | self.tg_group_id = tg_group_id 70 | self.tg_message_id = tg_message_id 71 | 72 | self.matrix_room_id = matrix_room_id 73 | self.matrix_event_id = matrix_event_id 74 | 75 | self.displayname = displayname 76 | 77 | def initialize(*args, **kwargs): 78 | """Initializes the database and creates tables if necessary.""" 79 | global engine, Base, Session, session 80 | engine = sa.create_engine(*args, **kwargs) 81 | Session.configure(bind=engine) 82 | session = Session() 83 | Base.metadata.bind = engine 84 | Base.metadata.create_all() 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # *DEPRECATED* 2 | 3 | Use [tulir/mautrix-telegram](https://github.com/tulir/mautrix-telegram) instead. It's much better. 4 | 5 | *Old README.md for reference:* 6 | 7 | # telematrix 8 | 9 | A bridge between Telegram and [Matrix](http://matrix.org/). Currently under development — this project isn't considered to be in a usable state right now. 10 | 11 | ## Usage 12 | After the [installation](#installation), follow these steps to bridge a matrix room to a telegram group chat or channel: 13 | - Invite the bot to the telegram chat. 14 | - Send `/alias` in the telegram chat. 15 | - The bot will answer with an alias, something like `#telegram_-XXXXXXXXX:yourserver.example`. Add that as an alias to the matrix room you want to bridge. 16 | 17 | In case it doesn't work make sure that all these are true: 18 | - You are on the same server as the bridge. If that is not the case, you can't set the alias, because you can only set aliases on the server you are on. 19 | - The matrix room is not set to invite only. The bridge currently doesn't support invite only rooms, so the rooms must be set to be open for all. Guests access is not required though. 20 | 21 | ## Installation 22 | ### Dependencies 23 | 24 | First, create a virtualenv and activate it: 25 | 26 | ```bash 27 | virtualenv venv -p $(which python3) 28 | . venv/bin/activate 29 | ``` 30 | 31 | Then install the requirements using pip: 32 | 33 | ```bash 34 | pip install -r requirements.txt 35 | ``` 36 | 37 | ### Configuration 38 | **telematrix configuration** 39 | 40 | First, copy config.json.example to config.json. Then fill in the fields: 41 | 42 | * `tokens.hs`: A randomly generated token 43 | * `tokens.as`: Another randomly generated token 44 | * `tokens.telegram`: The Telegram bot API token, as generated by @BotFather 45 | * `tokens.google`: A Google API key, used for URL shortening. Can be left out to disable. 46 | * `hosts.internal`: The homeserver host to connect to internally. 47 | * `hosts.external`: The external domain of the homeserver, used for generating URLs. 48 | * `hosts.bare`: Just the (sub)domain of the server. 49 | * `user_id_format`: A Python `str.format`-style string to format user IDs as 50 | * `db_url`: A SQLAlchemy URL for the database. See the [SQLAlchemy docs](http://docs.sqlalchemy.org/en/latest/core/engines.html). 51 | 52 | **Synapse configuration** 53 | 54 | Copy asconfig.yaml.example to asconfig.yaml, then fill in the fields: 55 | 56 | * `url`: The host and port of telematrix. Most likely `http://localhost:5000`. 57 | * `as_token`: `token.as` from telematrix config. 58 | * `hs_token`: `token.hs` from telematrix config. 59 | 60 | The rest of the config can be left as is. Finally, add a relative path to this config file in the Synapse's homeserver.yaml: 61 | 62 | ```yaml 63 | app_service_config_files: 64 | - "../telematrix/asconfig.yaml" 65 | ``` 66 | 67 | ## Contributions 68 | 69 | Want to help? Awesome! This bridge still needs a lot of work, so any help is welcome. 70 | 71 | A great start is reporting bugs — if you find it doesn't work like it's supposed to, do submit an issue on Github. Or, if you're a programmer (which you probably are, considering you are on this website), feel free to try to fix it yourself. Just make sure Pylint approves of your code! 72 | -------------------------------------------------------------------------------- /telematrix/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Telematrix 3 | 4 | App service for Matrix to bridge a room with a Telegram group. 5 | """ 6 | import asyncio 7 | import html 8 | import json 9 | import logging 10 | import mimetypes 11 | from datetime import datetime 12 | from time import time 13 | from urllib.parse import unquote, quote, urlparse, parse_qs 14 | from io import BytesIO 15 | import re 16 | 17 | from PIL import Image 18 | from aiohttp import web, ClientSession 19 | from aiotg import Bot 20 | from bs4 import BeautifulSoup 21 | 22 | import telematrix.database as db 23 | 24 | # Read the configuration file 25 | try: 26 | with open('config.json', 'r') as config_file: 27 | CONFIG = json.load(config_file) 28 | 29 | HS_TOKEN = CONFIG['tokens']['hs'] 30 | AS_TOKEN = CONFIG['tokens']['as'] 31 | TG_TOKEN = CONFIG['tokens']['telegram'] 32 | 33 | try: 34 | GOOGLE_TOKEN = CONFIG['tokens']['google'] 35 | except KeyError: 36 | GOOGLE_TOKEN = None 37 | 38 | MATRIX_HOST = CONFIG['hosts']['internal'] 39 | MATRIX_HOST_EXT = CONFIG['hosts']['external'] 40 | MATRIX_HOST_BARE = CONFIG['hosts']['bare'] 41 | 42 | MATRIX_PREFIX = MATRIX_HOST + '_matrix/client/r0/' 43 | MATRIX_MEDIA_PREFIX = MATRIX_HOST + '_matrix/media/r0/' 44 | 45 | USER_ID_FORMAT = CONFIG['user_id_format'] 46 | DATABASE_URL = CONFIG['db_url'] 47 | 48 | AS_PORT = CONFIG['as_port'] if 'as_port' in CONFIG else 5000 49 | except (OSError, IOError) as exception: 50 | print('Error opening config file:') 51 | print(exception) 52 | exit(1) 53 | 54 | GOO_GL_URL = 'https://www.googleapis.com/urlshortener/v1/url' 55 | 56 | TG_BOT = Bot(api_token=TG_TOKEN) 57 | MATRIX_SESS = ClientSession() 58 | SHORTEN_SESS = ClientSession() 59 | 60 | 61 | def create_response(code, obj): 62 | """ 63 | Create an HTTP response with a JSON body. 64 | :param code: The status code of the response. 65 | :param obj: The object to serialize and include in the response. 66 | :return: A web.Response. 67 | """ 68 | return web.Response(text=json.dumps(obj), status=code, 69 | content_type='application/json', charset='utf-8') 70 | 71 | 72 | VALID_TAGS = ['b', 'strong', 'i', 'em', 'a', 'pre'] 73 | 74 | 75 | def sanitize_html(string): 76 | """ 77 | Sanitize an HTML string for the Telegram bot API. 78 | :param string: The HTML string to sanitized. 79 | :return: The sanitized HTML string. 80 | """ 81 | string = string.replace('
', '\n').replace('
', '\n') \ 82 | .replace('
', '\n') 83 | soup = BeautifulSoup(string, 'html.parser') 84 | for tag in soup.find_all(True): 85 | if tag.name == 'blockquote': 86 | tag.string = ('\n' + tag.text).replace('\n', '\n> ')[3:-3] 87 | if tag.name not in VALID_TAGS: 88 | tag.hidden = True 89 | return soup.renderContents().decode('utf-8') 90 | 91 | 92 | def format_matrix_msg(form, content): 93 | """ 94 | Formats a matrix message for sending to Telegram 95 | :param form: The format string of the message, where the first parameter 96 | is the username and the second one the message. 97 | :param content: The content to be sent. 98 | :return: The formatted string. 99 | """ 100 | if 'format' in content and content['format'] == 'org.matrix.custom.html': 101 | sanitized = re.sub("(.+?) \(Telegram\)".format(MATRIX_HOST_BARE), "\\2", content['formatted_body']) 102 | sanitized = sanitize_html(sanitized) 103 | return html.escape(form).format(sanitized), 'HTML' 104 | else: 105 | return form.format(html.escape(content['body'])), None 106 | 107 | 108 | async def download_matrix_file(url, filename): 109 | """ 110 | Download a file from an MXC URL to /tmp/{filename} 111 | :param url: The MXC URL to download from. 112 | :param filename: The filename in /tmp/ to download into. 113 | """ 114 | m_url = MATRIX_MEDIA_PREFIX + 'download/{}{}'.format(url.netloc, url.path) 115 | async with MATRIX_SESS.get(m_url) as response: 116 | data = await response.read() 117 | with open('/tmp/{}'.format(filename), 'wb') as file: 118 | file.write(data) 119 | 120 | 121 | async def shorten_url(url): 122 | """ 123 | Shorten an URL using goo.gl. Returns the original URL if it fails. 124 | :param url: The URL to shorten. 125 | :return: The shortened URL. 126 | """ 127 | if not GOOGLE_TOKEN: 128 | return url 129 | 130 | headers = {'Content-Type': 'application/json'} 131 | async with SHORTEN_SESS.post(GOO_GL_URL, params={'key': GOOGLE_TOKEN}, 132 | data=json.dumps({'longUrl': url}), 133 | headers=headers) \ 134 | as response: 135 | obj = await response.json() 136 | 137 | if 'id' in obj: 138 | return obj['id'] 139 | else: 140 | return url 141 | 142 | def matrix_is_telegram(user_id): 143 | username = user_id.split(':')[0][1:] 144 | return username.startswith('telegram_') 145 | 146 | def get_username(user_id): 147 | return user_id.split(':')[0][1:] 148 | 149 | mime_extensions = { 150 | 'image/jpeg': 'jpg', 151 | 'image/gif': 'gif', 152 | 'image/png': 'png', 153 | 'image/tiff': 'tif', 154 | 'image/x-tiff': 'tif', 155 | 'image/bmp': 'bmp', 156 | 'image/x-windows-bmp': 'bmp' 157 | } 158 | 159 | async def matrix_transaction(request): 160 | """ 161 | Handle a transaction sent by the homeserver. 162 | :param request: The request containing the transaction. 163 | :return: The response to send. 164 | """ 165 | body = await request.json() 166 | events = body['events'] 167 | for event in events: 168 | if 'age' in event and event['age'] > 600000: 169 | print('discarded event of age', event['age']) 170 | continue 171 | try: 172 | print('{}: <{}> {}'.format(event['room_id'], event['user_id'], event['type'])) 173 | except KeyError: 174 | pass 175 | 176 | if event['type'] == 'm.room.aliases' and event['state_key'] == MATRIX_HOST_BARE: 177 | aliases = event['content']['aliases'] 178 | 179 | links = db.session.query(db.ChatLink)\ 180 | .filter_by(matrix_room=event['room_id']).all() 181 | for link in links: 182 | db.session.delete(link) 183 | 184 | for alias in aliases: 185 | print(alias) 186 | if alias.split('_')[0] != '#telegram' \ 187 | or alias.split(':')[-1] != MATRIX_HOST_BARE: 188 | continue 189 | 190 | tg_id = alias.split('_')[1].split(':')[0] 191 | link = db.ChatLink(event['room_id'], tg_id, True) 192 | db.session.add(link) 193 | db.session.commit() 194 | 195 | continue 196 | 197 | link = db.session.query(db.ChatLink)\ 198 | .filter_by(matrix_room=event['room_id']).first() 199 | if not link: 200 | print('{} isn\'t linked!'.format(event['room_id'])) 201 | continue 202 | group = TG_BOT.group(link.tg_room) 203 | 204 | try: 205 | response = None 206 | 207 | if event['type'] == 'm.room.message': 208 | user_id = event['user_id'] 209 | if matrix_is_telegram(user_id): 210 | continue 211 | 212 | 213 | sender = db.session.query(db.MatrixUser)\ 214 | .filter_by(matrix_id=user_id).first() 215 | 216 | if not sender: 217 | response = await matrix_get('client', 'profile/{}/displayname' 218 | .format(user_id), None) 219 | try: 220 | displayname = response['displayname'] 221 | except KeyError: 222 | displayname = get_username(user_id) 223 | sender = db.MatrixUser(user_id, displayname) 224 | db.session.add(sender) 225 | else: 226 | displayname = sender.name or get_username(user_id) 227 | content = event['content'] 228 | 229 | if 'msgtype' not in content: 230 | continue 231 | 232 | if content['msgtype'] == 'm.text': 233 | msg, mode = format_matrix_msg('{}', content) 234 | response = await group.send_text("{}: {}".format(displayname, msg), parse_mode='HTML') 235 | elif content['msgtype'] == 'm.notice': 236 | msg, mode = format_matrix_msg('{}', content) 237 | response = await group.send_text("[{}] {}".format(displayname, msg), parse_mode=mode) 238 | elif content['msgtype'] == 'm.emote': 239 | msg, mode = format_matrix_msg('{}', content) 240 | response = await group.send_text("* {} {}".format(displayname, msg), parse_mode=mode) 241 | elif content['msgtype'] == 'm.image': 242 | try: 243 | url = urlparse(content['url']) 244 | 245 | # Append the correct extension if it's missing or wrong 246 | ext = mime_extensions[content['info']['mimetype']] 247 | if not content['body'].endswith(ext): 248 | content['body'] += '.' + ext 249 | 250 | # Download the file 251 | await download_matrix_file(url, content['body']) 252 | with open('/tmp/{}'.format(content['body']), 'rb') as img_file: 253 | # Create the URL and shorten it 254 | url_str = MATRIX_HOST_EXT + \ 255 | '_matrix/media/r0/download/{}{}' \ 256 | .format(url.netloc, quote(url.path)) 257 | url_str = await shorten_url(url_str) 258 | 259 | caption = '{} sent an image'.format(displayname) 260 | response = await group.send_photo(img_file, caption=caption) 261 | except: 262 | pass 263 | else: 264 | print('Unsupported message type {}'.format(content['msgtype'])) 265 | print(json.dumps(content, indent=4)) 266 | 267 | elif event['type'] == 'm.room.member': 268 | if matrix_is_telegram(event['state_key']): 269 | continue 270 | 271 | user_id = event['state_key'] 272 | content = event['content'] 273 | 274 | sender = db.session.query(db.MatrixUser)\ 275 | .filter_by(matrix_id=user_id).first() 276 | if sender: 277 | displayname = sender.name 278 | else: 279 | displayname = get_username(user_id) 280 | 281 | if content['membership'] == 'join': 282 | oldname = sender.name if sender else get_username(user_id) 283 | try: 284 | displayname = content['displayname'] or get_username(user_id) 285 | except KeyError: 286 | displayname = get_username(user_id) 287 | 288 | if not sender: 289 | sender = db.MatrixUser(user_id, displayname) 290 | else: 291 | sender.name = displayname 292 | db.session.add(sender) 293 | 294 | msg = None 295 | if 'unsigned' in event and 'prev_content' in event['unsigned']: 296 | prev = event['unsigned']['prev_content'] 297 | if prev['membership'] == 'join': 298 | if 'displayname' in prev and prev['displayname']: 299 | oldname = prev['displayname'] 300 | 301 | msg = '> {} changed their display name to {}'\ 302 | .format(oldname, displayname) 303 | else: 304 | msg = '> {} has joined the room'.format(displayname) 305 | 306 | if msg: 307 | response = await group.send_text(msg) 308 | elif content['membership'] == 'leave': 309 | msg = '< {} has left the room'.format(displayname) 310 | response = await group.send_text(msg) 311 | elif content['membership'] == 'ban': 312 | msg = ' joining room <{}>. This is likely because guests are not allowed to join the room." 455 | .format(user_id, room_id)) 456 | 457 | async def update_matrix_displayname_avatar(tg_user): 458 | name = tg_user['first_name'] 459 | if 'last_name' in tg_user: 460 | name += ' ' + tg_user['last_name'] 461 | name += ' (Telegram)' 462 | user_id = USER_ID_FORMAT.format(tg_user['id']) 463 | 464 | db_user = db.session.query(db.TgUser).filter_by(tg_id=tg_user['id']).first() 465 | 466 | profile_photos = await TG_BOT.get_user_profile_photos(tg_user['id']) 467 | pp_file_id = None 468 | try: 469 | pp_file_id = profile_photos['result']['photos'][0][-1]['file_id'] 470 | except: 471 | pp_file_id = None 472 | 473 | if db_user: 474 | if db_user.name != name: 475 | await matrix_put('client', 'profile/{}/displayname'.format(user_id), user_id, {'displayname': name}) 476 | db_user.name = name 477 | if db_user.profile_pic_id != pp_file_id: 478 | if pp_file_id: 479 | pp_uri, _ = await upload_tgfile_to_matrix(pp_file_id, user_id) 480 | await matrix_put('client', 'profile/{}/avatar_url'.format(user_id), user_id, {'avatar_url':pp_uri}) 481 | else: 482 | await matrix_put('client', 'profile/{}/avatar_url'.format(user_id), user_id, {'avatar_url':None}) 483 | db_user.profile_pic_id = pp_file_id 484 | else: 485 | db_user = db.TgUser(tg_user['id'], name, pp_file_id) 486 | await matrix_put('client', 'profile/{}/displayname'.format(user_id), user_id, {'displayname': name}) 487 | if pp_file_id: 488 | pp_uri, _ = await upload_tgfile_to_matrix(pp_file_id, user_id) 489 | await matrix_put('client', 'profile/{}/avatar_url'.format(user_id), user_id, {'avatar_url':pp_uri}) 490 | else: 491 | await matrix_put('client', 'profile/{}/avatar_url'.format(user_id), user_id, {'avatar_url':None}) 492 | db.session.add(db_user) 493 | db.session.commit() 494 | 495 | 496 | @TG_BOT.handle('sticker') 497 | async def aiotg_sticker(chat, sticker): 498 | link = db.session.query(db.ChatLink).filter_by(tg_room=chat.id).first() 499 | if not link: 500 | print('Unknown telegram chat {}: {}'.format(chat, chat.id)) 501 | return 502 | 503 | await update_matrix_displayname_avatar(chat.sender); 504 | 505 | room_id = link.matrix_room 506 | user_id = USER_ID_FORMAT.format(chat.sender['id']) 507 | txn_id = quote('{}{}'.format(chat.message['message_id'], chat.id)) 508 | 509 | file_id = sticker['file_id'] 510 | uri, length = await upload_tgfile_to_matrix(file_id, user_id, 'image/png', 'PNG') 511 | 512 | info = {'mimetype': 'image/png', 'size': length, 'h': sticker['height'], 513 | 'w': sticker['width']} 514 | body = 'Sticker_{}.png'.format(int(time() * 1000)) 515 | 516 | if uri: 517 | j = await send_matrix_message(room_id, user_id, txn_id, body=body, 518 | url=uri, info=info, msgtype='m.image') 519 | 520 | if 'errcode' in j and j['errcode'] == 'M_FORBIDDEN': 521 | await register_join_matrix(chat, room_id, user_id) 522 | await send_matrix_message(room_id, user_id, txn_id + 'join', 523 | body=body, url=uri, info=info, 524 | msgtype='m.image') 525 | 526 | if 'caption' in chat.message: 527 | await send_matrix_message(room_id, user_id, txn_id + 'caption', 528 | body=chat.message['caption'], 529 | msgtype='m.text') 530 | if 'event_id' in j: 531 | name = chat.sender['first_name'] 532 | if 'last_name' in chat.sender: 533 | name += " " + chat.sender['last_name'] 534 | name += " (Telegram)" 535 | message = db.Message( 536 | chat.message['chat']['id'], 537 | chat.message['message_id'], 538 | room_id, 539 | j['event_id'], 540 | name) 541 | db.session.add(message) 542 | db.session.commit() 543 | 544 | @TG_BOT.handle('photo') 545 | async def aiotg_photo(chat, photo): 546 | link = db.session.query(db.ChatLink).filter_by(tg_room=chat.id).first() 547 | if not link: 548 | print('Unknown telegram chat {}: {}'.format(chat, chat.id)) 549 | return 550 | 551 | await update_matrix_displayname_avatar(chat.sender); 552 | room_id = link.matrix_room 553 | user_id = USER_ID_FORMAT.format(chat.sender['id']) 554 | txn_id = quote('{}{}'.format(chat.message['message_id'], chat.id)) 555 | 556 | file_id = photo[-1]['file_id'] 557 | uri, length = await upload_tgfile_to_matrix(file_id, user_id) 558 | info = {'mimetype': 'image/jpeg', 'size': length, 'h': photo[-1]['height'], 559 | 'w': photo[-1]['width']} 560 | body = 'Image_{}.jpg'.format(int(time() * 1000)) 561 | 562 | if uri: 563 | j = await send_matrix_message(room_id, user_id, txn_id, body=body, 564 | url=uri, info=info, msgtype='m.image') 565 | 566 | if 'errcode' in j and j['errcode'] == 'M_FORBIDDEN': 567 | await register_join_matrix(chat, room_id, user_id) 568 | await send_matrix_message(room_id, user_id, txn_id + 'join', 569 | body=body, url=uri, info=info, 570 | msgtype='m.image') 571 | 572 | if 'caption' in chat.message: 573 | await send_matrix_message(room_id, user_id, txn_id + 'caption', 574 | body=chat.message['caption'], 575 | msgtype='m.text') 576 | 577 | if 'event_id' in j: 578 | name = chat.sender['first_name'] 579 | if 'last_name' in chat.sender: 580 | name += " " + chat.sender['last_name'] 581 | name += " (Telegram)" 582 | message = db.Message( 583 | chat.message['chat']['id'], 584 | chat.message['message_id'], 585 | room_id, 586 | j['event_id'], 587 | name) 588 | db.session.add(message) 589 | db.session.commit() 590 | 591 | @TG_BOT.command(r'/alias') 592 | async def aiotg_alias(chat, match): 593 | await chat.reply('The Matrix alias for this chat is #telegram_{}:{}' 594 | .format(chat.id, MATRIX_HOST_BARE)) 595 | 596 | 597 | @TG_BOT.command(r'(?s)(.*)') 598 | async def aiotg_message(chat, match): 599 | link = db.session.query(db.ChatLink).filter_by(tg_room=chat.id).first() 600 | if link: 601 | room_id = link.matrix_room 602 | else: 603 | print('Unknown telegram chat {}: {}'.format(chat, chat.id)) 604 | return 605 | 606 | await update_matrix_displayname_avatar(chat.sender); 607 | user_id = USER_ID_FORMAT.format(chat.sender['id']) 608 | txn_id = quote('{}:{}'.format(chat.message['message_id'], chat.id)) 609 | 610 | message = match.group(0) 611 | 612 | if 'forward_from' in chat.message: 613 | fw_from = chat.message['forward_from'] 614 | if 'last_name' in fw_from: 615 | msg_from = '{} {} (Telegram)'.format(fw_from['first_name'], 616 | fw_from['last_name']) 617 | else: 618 | msg_from = '{} (Telegram)'.format(fw_from['first_name']) 619 | 620 | quoted_msg = '\n'.join(['>{}'.format(x) for x in message.split('\n')]) 621 | quoted_msg = 'Forwarded from {}:\n{}' \ 622 | .format(msg_from, quoted_msg) 623 | 624 | quoted_html = '
{}
' \ 625 | .format(html.escape(message).replace('\n', '
')) 626 | quoted_html = 'Forwarded from {}:\n{}' \ 627 | .format(html.escape(msg_from), quoted_html) 628 | j = await send_matrix_message(room_id, user_id, txn_id, 629 | body=quoted_msg, 630 | formatted_body=quoted_html, 631 | format='org.matrix.custom.html', 632 | msgtype='m.text') 633 | 634 | elif 'reply_to_message' in chat.message: 635 | re_msg = chat.message['reply_to_message'] 636 | if not 'text' in re_msg and not 'photo' in re_msg and not 'sticker' in re_msg: 637 | return 638 | if 'last_name' in re_msg['from']: 639 | msg_from = '{} {} (Telegram)'.format(re_msg['from']['first_name'], 640 | re_msg['from']['last_name']) 641 | else: 642 | msg_from = '{} (Telegram)'.format(re_msg['from']['first_name']) 643 | date = datetime.fromtimestamp(re_msg['date']) \ 644 | .strftime('%Y-%m-%d %H:%M:%S') 645 | 646 | reply_mx_id = db.session.query(db.Message)\ 647 | .filter_by(tg_group_id=chat.message['chat']['id'], tg_message_id=chat.message['reply_to_message']['message_id']).first() 648 | 649 | html_message = html.escape(message).replace('\n', '
') 650 | if 'text' in re_msg: 651 | quoted_msg = '\n'.join(['>{}'.format(x) 652 | for x in re_msg['text'].split('\n')]) 653 | quoted_html = '
{}
' \ 654 | .format(html.escape(re_msg['text']) 655 | .replace('\n', '
')) 656 | else: 657 | quoted_msg = '' 658 | quoted_html = '' 659 | 660 | if reply_mx_id: 661 | quoted_msg = 'Reply to {}:\n{}\n\n{}' \ 662 | .format(reply_mx_id.displayname, quoted_msg, message) 663 | quoted_html = 'Reply to {}:
{}

{}

' \ 664 | .format(html.escape(room_id), html.escape(reply_mx_id.matrix_event_id), html.escape(reply_mx_id.displayname), 665 | quoted_html, html_message) 666 | else: 667 | quoted_msg = 'Reply to {}:\n{}\n\n{}' \ 668 | .format(msg_from, quoted_msg, message) 669 | quoted_html = 'Reply to {}:
{}

{}

' \ 670 | .format(html.escape(msg_from), 671 | quoted_html, html_message) 672 | 673 | j = await send_matrix_message(room_id, user_id, txn_id, 674 | body=quoted_msg, 675 | formatted_body=quoted_html, 676 | format='org.matrix.custom.html', 677 | msgtype='m.text') 678 | else: 679 | j = await send_matrix_message(room_id, user_id, txn_id, body=message, 680 | msgtype='m.text') 681 | 682 | if 'errcode' in j and j['errcode'] == 'M_FORBIDDEN': 683 | await register_join_matrix(chat, room_id, user_id) 684 | await asyncio.sleep(0.5) 685 | j = await send_matrix_message(room_id, user_id, txn_id + 'join', 686 | body=message, msgtype='m.text') 687 | elif 'event_id' in j: 688 | name = chat.sender['first_name'] 689 | if 'last_name' in chat.sender: 690 | name += " " + chat.sender['last_name'] 691 | name += " (Telegram)" 692 | message = db.Message( 693 | chat.message['chat']['id'], 694 | chat.message['message_id'], 695 | room_id, 696 | j['event_id'], 697 | name) 698 | db.session.add(message) 699 | db.session.commit() 700 | 701 | 702 | def main(): 703 | """ 704 | Main function to get the entire ball rolling. 705 | """ 706 | logging.basicConfig(level=logging.WARNING) 707 | db.initialize(DATABASE_URL) 708 | 709 | loop = asyncio.get_event_loop() 710 | asyncio.ensure_future(TG_BOT.loop()) 711 | 712 | app = web.Application(loop=loop) 713 | app.router.add_route('GET', '/rooms/{room_alias}', matrix_room) 714 | app.router.add_route('PUT', '/transactions/{transaction}', 715 | matrix_transaction) 716 | web.run_app(app, port=AS_PORT) 717 | 718 | 719 | if __name__ == "__main__": 720 | main() 721 | --------------------------------------------------------------------------------