Use a database for links, allowing for automatic configuration

Fixes #6
This commit is contained in:
Sijmen Schoon 2016-11-15 00:52:00 +01:00
parent 08c8a88721
commit 935fada1c5
4 changed files with 180 additions and 21 deletions

View File

@ -16,6 +16,8 @@ from aiohttp import web, ClientSession
from aiotg import Bot
from bs4 import BeautifulSoup
import database as db
# Read the configuration file
try:
with open('config.json', 'r') as config_file:
@ -28,6 +30,7 @@ try:
MATRIX_HOST = CONFIG['hosts']['internal']
MATRIX_HOST_EXT = CONFIG['hosts']['external']
MATRIX_HOST_BARE = CONFIG['hosts']['bare']
MATRIX_PREFIX = MATRIX_HOST + '_matrix/client/r0/'
MATRIX_MEDIA_PREFIX = MATRIX_HOST + '_matrix/media/r0/'
@ -36,6 +39,8 @@ try:
TELEGRAM_CHATS = CONFIG['chats']
MATRIX_ROOMS = {v: k for k, v in TELEGRAM_CHATS.items()}
DATABASE_URL = CONFIG['db_url']
except (OSError, IOError) as exception:
print('Error opening config file:')
print(exception)
@ -126,6 +131,12 @@ async def shorten_url(url):
else:
return url
def matrix_is_telegram(user_id):
username = user_id.split(':')[0][1:]
return username.startswith('telegram_')
def get_username(user_id):
return user_id.split(':')[0][1:]
async def matrix_transaction(request):
"""
@ -136,24 +147,65 @@ async def matrix_transaction(request):
body = await request.json()
events = body['events']
for event in events:
if event['room_id'] not in MATRIX_ROOMS:
print('{} not in matrix_rooms!'.format(event['room_id']))
elif event['type'] == 'm.room.message':
group = TG_BOT.group(MATRIX_ROOMS[event['room_id']])
print(event)
username = event['user_id'].split(':')[0][1:]
if username.startswith('telegram_'):
return create_response(200, {})
if event['type'] == 'm.room.aliases':
aliases = event['content']['aliases']
links = db.session.query(db.ChatLink)\
.filter_by(matrix_room=event['room_id']).all()
for link in links:
db.session.delete(link)
for alias in aliases:
print(alias)
if alias.split('_')[0] != '#telegram' \
or alias.split(':')[-1] != MATRIX_HOST_BARE:
continue
tg_id = alias.split('_')[1].split(':')[0]
link = db.ChatLink(event['room_id'], tg_id, True)
db.session.add(link)
db.session.commit()
continue
link = db.session.query(db.ChatLink)\
.filter_by(matrix_room=event['room_id']).first()
if not link:
print('{} isn\'t linked!'.format(event['room_id']))
continue
group = TG_BOT.group(link.tg_room)
if event['type'] == 'm.room.message':
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']
if content['msgtype'] == 'm.text':
msg, mode = format_matrix_msg('<{}> {}', username, content)
msg, mode = format_matrix_msg('<{}> {}', displayname, content)
await group.send_text(msg, parse_mode=mode)
elif content['msgtype'] == 'm.notice':
msg, mode = format_matrix_msg('[{}] {}', username, content)
msg, mode = format_matrix_msg('[{}] {}', displayname, content)
await group.send_text(msg, parse_mode=mode)
elif content['msgtype'] == 'm.emote':
msg, mode = format_matrix_msg('* {} {}', username, content)
msg, mode = format_matrix_msg('* {} {}', displayname, content)
await group.send_text(msg, parse_mode=mode)
elif content['msgtype'] == 'm.image':
url = urlparse(content['url'])
@ -164,13 +216,45 @@ async def matrix_transaction(request):
.format(url.netloc, quote(url.path))
url_str = await shorten_url(url_str)
caption = '<{}> {} ({})'.format(username, content['body'],
url_str)
caption = '<{}> {} ({})'.format(displayname,
content['body'], url_str)
await group.send_photo(img_file, caption=caption)
else:
print('Unsupported message type {}'.format(content['msgtype']))
print(json.dumps(content, indent=4))
elif event['type'] == 'm.room.member':
if matrix_is_telegram(event['state_key']):
continue
user_id = event['state_key']
content = event['content']
sender = db.session.query(db.MatrixUser)\
.filter_by(matrix_id=user_id).first()
if sender:
displayname = sender.name
else:
displayname = get_username(user_id)
if content['membership'] == 'join':
displayname = content['displayname'] or get_username(user_id)
if not sender:
sender = db.MatrixUser(user_id, displayname)
else:
sender.name = displayname
db.session.add(sender)
msg = '> {} has joined the room'.format(displayname)
await group.send_text(msg)
elif content['membership'] == 'leave':
msg = '< {} has left the room'.format(displayname)
await group.send_text(msg)
elif content['membership'] == 'ban':
msg = '<! {} was banned from the room'.format(displayname)
await group.send_text(msg)
db.session.commit()
return create_response(200, {})
@ -178,12 +262,12 @@ async def _matrix_request(method_fun, category, path, user_id, data=None,
content_type=None):
# pylint: disable=too-many-arguments
# Due to this being a helper function, the argument count acceptable
if content_type is None:
content_type = 'application/octet-stream'
if data is not None:
if isinstance(data, dict):
data = json.dumps(data)
content_type = 'application/json; charset=utf-8'
elif content_type is None:
content_type = 'application/octet-stream'
params = {'access_token': AS_TOKEN}
if user_id is not None:
@ -235,6 +319,7 @@ async def matrix_room(request):
localpart = room_alias.split(':')[0]
chat = '_'.join(localpart.split('_')[1:])
# Look up the chat in the database
if chat in TELEGRAM_CHATS:
await matrix_post('client', 'createRoom', None,
{'room_alias_name': localpart[1:]})
@ -314,11 +399,18 @@ async def aiotg_photo(chat, photo):
url=uri, info=info, msgtype='m.image')
@TG_BOT.command(r'/alias')
async def aiotg_alias(chat, match):
await chat.reply('The Matrix alias for this chat is #telegram_{}:{}'
.format(chat.id, MATRIX_HOST_BARE))
@TG_BOT.command(r'(?s)(.*)')
async def aiotg_message(chat, match):
try:
room_id = TELEGRAM_CHATS[str(chat.id)]
except KeyError:
link = db.session.query(db.ChatLink).filter_by(tg_room=chat.id).first()
if link:
room_id = link.matrix_room
else:
print('Unknown telegram chat {}: {}'.format(chat, chat.id))
return
@ -384,9 +476,9 @@ async def aiotg_message(chat, match):
msgtype='m.text')
if 'errcode' in j and j['errcode'] == 'M_FORBIDDEN':
await asyncio.sleep(1)
await asyncio.sleep(0.1)
await register_join_matrix(chat, room_id, user_id)
await asyncio.sleep(1)
await asyncio.sleep(0.1)
await send_matrix_message(room_id, user_id, txn_id, body=message,
msgtype='m.text')
@ -396,6 +488,7 @@ def main():
Main function to get the entire ball rolling.
"""
logging.basicConfig(level=logging.WARNING)
db.initialize(DATABASE_URL)
loop = asyncio.get_event_loop()
asyncio.ensure_future(TG_BOT.loop())

View File

@ -8,13 +8,15 @@
"hosts": {
"internal": "http://127.0.0.1:PORT/",
"external": "https://DOMAIN.TLD/"
"external": "https://DOMAIN.TLD/",
"bare": "DOMAIN.TLD"
},
"chats": {
"-TELEGRAM_ID": "!INTERNAL_ID:DOMAIN.TLD"
},
"user_id_format": "@telegram_{}:DOMAIN.TLD"
"user_id_format": "@telegram_{}:DOMAIN.TLD",
"db_url": "sqlite:///database.db"
}

63
database.py Normal file
View File

@ -0,0 +1,63 @@
"""
Defines all database models and provides necessary functions to manage it.
"""
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import sqlalchemy as sa
engine = None
Base = declarative_base()
Session = sessionmaker()
session = None
class ChatLink(Base):
"""Describes a link between the Telegram and Matrix side of the bridge."""
__tablename__ = 'chat_link'
id = sa.Column(sa.Integer, primary_key=True)
matrix_room = sa.Column(sa.String)
tg_room = sa.Column(sa.BigInteger)
active = sa.Column(sa.Boolean)
def __init__(self, matrix_room, tg_room, active):
self.matrix_room = matrix_room
self.tg_room = tg_room
self.active = active
class TgUser(Base):
"""Describes a user on the Telegram side of the bridge."""
__tablename__ = 'tg_user'
id = sa.Column(sa.Integer, primary_key=True)
tg_id = sa.Column(sa.BigInteger)
name = sa.Column(sa.String)
profile_pic_id = sa.Column(sa.String)
def __init__(self, tg_id, name, profile_pic_id):
self.tg_id = tg_id
self.name = name
self.profile_pic_id = profile_pic_id
class MatrixUser(Base):
"""Describes a user on the Matrix side of the bridge."""
__tablename__ = 'matrix_user'
id = sa.Column(sa.Integer, primary_key=True)
matrix_id = sa.Column(sa.String)
name = sa.Column(sa.String)
def __init__(self, matrix_id, name):
self.matrix_id = matrix_id
self.name = name
def initialize(*args, **kwargs):
"""Initializes the database and creates tables if necessary."""
global engine, Base, Session, session
engine = sa.create_engine(*args, **kwargs)
Session.configure(bind=engine)
session = Session()
Base.metadata.bind = engine
Base.metadata.create_all()

View File

@ -1,3 +1,4 @@
aiohttp==1.0.5
aiotg==0.7.11
beautifulsoup4==4.5.1
sqlalchemy==1.1.3