├── 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', '
{}' \ 654 | .format(html.escape(re_msg['text']) 655 | .replace('\n', '
{}
' \ 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 | --------------------------------------------------------------------------------