Compare commits

...

16 Commits

Author SHA1 Message Date
Tulir Asokan 005daa9ee2 Bump version to 0.2.0 2018-06-08 11:55:59 +03:00
Tulir Asokan 7029102c0f Bump version to 0.2.0rc6 2018-06-06 13:39:09 +03:00
Tulir Asokan 708110eb08 Make cascade rules alembic upgrade optional to un-break sqlite 2018-06-03 14:30:19 +03:00
Tulir Asokan c0da861562 Add warning about delete-portal kicking all room members 2018-06-01 18:05:35 +03:00
Tulir Asokan 844cf14bcd Bump version to 0.2.0rc5 2018-06-01 13:27:20 +03:00
Tulir Asokan fe32475e10 Fix kicking Telegram puppets and fix error message when bridging chats you've left 2018-05-31 11:19:24 +03:00
Tulir Asokan f28f5915a4 Don't create portal in response to relaybot events. Fixes #145 2018-05-31 11:18:48 +03:00
Tulir Asokan 1aa80c1a8f Fix user_portal delete cascade when deleting portals 2018-05-31 11:18:20 +03:00
Tulir Asokan 5d9b94fa5f Bump version to 0.2.0rc4 2018-05-29 22:26:26 +03:00
Tulir Asokan 6ef31599e9 Read database path from config in alembic env.py
Slightly related to #135
2018-05-29 18:37:08 +03:00
Tulir Asokan e961e0bcc6 Fix manual bridging using the relay bot 2018-05-29 15:26:40 +03:00
Tulir Asokan dc85754b1e Fix postgres compatibility 2018-05-29 15:17:08 +03:00
Tulir Asokan 04e2c03dae Allow inviting relaybot-whitelisted Matrix users to portal from telegram 2018-05-29 15:15:42 +03:00
Tulir Asokan 42d54dac5b Bump version to 0.2.0rc3 2018-05-25 00:08:46 +03:00
Tulir Asokan 767a51f994 Merge pull request #142 from jcgruenhage/master
Rework Dockerfile to remove virtualenv
2018-05-25 00:07:57 +03:00
Jan Christian Grünhage 313b5e5d07 rework Dockerfile to remove virtualenv 2018-05-24 00:59:26 +02:00
15 changed files with 128 additions and 57 deletions
+8 -8
View File
@@ -7,12 +7,14 @@ COPY . /opt/mautrixtelegram
RUN apk add --no-cache \ RUN apk add --no-cache \
python3-dev \ python3-dev \
py3-virtualenv \ py3-virtualenv \
py3-pillow \
py3-aiohttp \
py3-lxml \
py3-magic \
py3-numpy \
py3-asn1crypto \
py3-sqlalchemy \
build-base \ build-base \
zlib-dev \
jpeg-dev \
libxslt-dev \
libxml2-dev \
libmagic \
ffmpeg \ ffmpeg \
bash \ bash \
ca-certificates \ ca-certificates \
@@ -21,9 +23,7 @@ RUN apk add --no-cache \
&& cd /opt/mautrixtelegram \ && cd /opt/mautrixtelegram \
&& cp -r docker/root/* / \ && cp -r docker/root/* / \
&& rm docker -rf \ && rm docker -rf \
&& virtualenv -p /usr/bin/python3 .venv \ && pip3 install -r requirements.txt -r optional-requirements.txt
&& source .venv/bin/activate \
&& pip install -r requirements.txt -r optional-requirements.txt
VOLUME /data VOLUME /data
-3
View File
@@ -35,9 +35,6 @@ script_location = alembic
# are written from script.py.mako # are written from script.py.mako
# output_encoding = utf-8 # output_encoding = utf-8
sqlalchemy.url = sqlite:///mautrix-telegram.db
# Logging configuration # Logging configuration
[loggers] [loggers]
keys = root,sqlalchemy,alembic keys = root,sqlalchemy,alembic
+7 -1
View File
@@ -1,4 +1,3 @@
from __future__ import with_statement
from alembic import context from alembic import context
from sqlalchemy import engine_from_config, pool from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig from logging.config import fileConfig
@@ -8,12 +7,19 @@ from os.path import abspath, dirname
sys.path.insert(0, dirname(dirname(abspath(__file__)))) sys.path.insert(0, dirname(dirname(abspath(__file__))))
from mautrix_telegram.base import Base from mautrix_telegram.base import Base
from mautrix_telegram.config import Config
import mautrix_telegram.db import mautrix_telegram.db
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
config = context.config config = context.config
mxtg_config_path = context.get_x_argument(as_dictionary=True).get("config", "config.yaml")
mxtg_config = Config(mxtg_config_path, None, None)
mxtg_config.load()
config.set_main_option("sqlalchemy.url",
mxtg_config.get("appservice.database", "sqlite:///mautrix-telegram.db"))
# Interpret the config file for Python logging. # Interpret the config file for Python logging.
# This line sets up loggers basically. # This line sets up loggers basically.
fileConfig(config.config_file_name) fileConfig(config.config_file_name)
@@ -0,0 +1,41 @@
"""Add cascade rules to UserPortal
Revision ID: 2228d49c383f
Revises: bcfefa1f1299
Create Date: 2018-05-31 11:11:59.482112
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2228d49c383f'
down_revision = 'bcfefa1f1299'
branch_labels = None
depends_on = None
def upgrade():
try:
with op.batch_alter_table("user_portal") as batch_op:
batch_op.drop_constraint("user_portal_user_fkey", type_="foreignkey")
batch_op.drop_constraint("user_portal_portal_fkey", type_="foreignkey")
batch_op.create_foreign_key("user_portal_user_fkey", "user", ["user"], ["tgid"],
onupdate="CASCADE", ondelete="CASCADE")
batch_op.create_foreign_key("user_portal_portal_fkey", "portal",
["portal", "portal_receiver"], ["tgid", "tg_receiver"],
onupdate="CASCADE", ondelete="CASCADE")
except ValueError:
return
def downgrade():
try:
with op.batch_alter_table("user_portal") as batch_op:
batch_op.drop_constraint("user_portal_user_fkey", type_="foreignkey")
batch_op.drop_constraint("user_portal_portal_fkey", type_="foreignkey")
batch_op.create_foreign_key("user_portal_user_fkey", "portal",
["portal", "portal_receiver"], ["tgid", "tg_receiver"])
batch_op.create_foreign_key("user_portal_portal_fkey", "user", ["user"], ["tgid"])
except ValueError:
return
@@ -19,31 +19,31 @@ depends_on = None
def upgrade(): def upgrade():
Session = op.create_table('telethon_sessions', Session = op.create_table('telethon_sessions',
sa.Column('session_id', sa.VARCHAR(), nullable=False), sa.Column('session_id', sa.String, nullable=False),
sa.Column('dc_id', sa.INTEGER(), nullable=False), sa.Column('dc_id', sa.Integer, nullable=False),
sa.Column('server_address', sa.VARCHAR(), nullable=True), sa.Column('server_address', sa.String, nullable=True),
sa.Column('port', sa.INTEGER(), nullable=True), sa.Column('port', sa.Integer, nullable=True),
sa.Column('auth_key', sa.BLOB(), nullable=True), sa.Column('auth_key', sa.LargeBinary, nullable=True),
sa.PrimaryKeyConstraint('session_id', 'dc_id')) sa.PrimaryKeyConstraint('session_id', 'dc_id'))
SentFile = op.create_table('telethon_sent_files', SentFile = op.create_table('telethon_sent_files',
sa.Column('session_id', sa.VARCHAR(), nullable=False), sa.Column('session_id', sa.String, nullable=False),
sa.Column('md5_digest', sa.BLOB(), nullable=False), sa.Column('md5_digest', sa.LargeBinary, nullable=False),
sa.Column('file_size', sa.INTEGER(), nullable=False), sa.Column('file_size', sa.Integer, nullable=False),
sa.Column('type', sa.INTEGER(), nullable=False), sa.Column('type', sa.Integer, nullable=False),
sa.Column('id', sa.INTEGER(), nullable=True), sa.Column('id', sa.BigInteger, nullable=True),
sa.Column('hash', sa.INTEGER(), nullable=True), sa.Column('hash', sa.BigInteger, nullable=True),
sa.PrimaryKeyConstraint('session_id', 'md5_digest', 'file_size', sa.PrimaryKeyConstraint('session_id', 'md5_digest', 'file_size',
'type')) 'type'))
Entity = op.create_table('telethon_entities', Entity = op.create_table('telethon_entities',
sa.Column('session_id', sa.VARCHAR(), nullable=False), sa.Column('session_id', sa.String, nullable=False),
sa.Column('id', sa.INTEGER(), nullable=False), sa.Column('id', sa.Integer, nullable=False),
sa.Column('hash', sa.INTEGER(), nullable=False), sa.Column('hash', sa.Integer, nullable=False),
sa.Column('username', sa.VARCHAR(), nullable=True), sa.Column('username', sa.String, nullable=True),
sa.Column('phone', sa.INTEGER(), nullable=True), sa.Column('phone', sa.Integer, nullable=True),
sa.Column('name', sa.VARCHAR(), nullable=True), sa.Column('name', sa.String, nullable=True),
sa.PrimaryKeyConstraint('session_id', 'id')) sa.PrimaryKeyConstraint('session_id', 'id'))
Version = op.create_table('telethon_version', Version = op.create_table('telethon_version',
sa.Column('version', sa.INTEGER(), nullable=False), sa.Column('version', sa.Integer, nullable=False),
sa.PrimaryKeyConstraint('version')) sa.PrimaryKeyConstraint('version'))
conn = op.get_bind() conn = op.get_bind()
sessions = [os.path.basename(f) for f in os.listdir(".") if f.endswith(".session")] sessions = [os.path.basename(f) for f in os.listdir(".") if f.endswith(".session")]
@@ -18,7 +18,7 @@ depends_on = None
def upgrade(): def upgrade():
op.add_column('telegram_file', op.add_column('telegram_file',
sa.Column('timestamp', sa.BigInteger(), nullable=False, default=0, sa.Column('timestamp', sa.BigInteger(), nullable=False, default=0,
server_default="true")) server_default="0"))
def downgrade(): def downgrade():
@@ -29,7 +29,7 @@ def upgrade():
sa.UniqueConstraint('mxid')) sa.UniqueConstraint('mxid'))
op.create_table('user', op.create_table('user',
sa.Column('mxid', sa.String), sa.Column('mxid', sa.String),
sa.Column('tgid', sa.Integer, nullable=True), sa.Column('tgid', sa.Integer, nullable=True, unique=True),
sa.Column('tg_username', sa.String, nullable=True), sa.Column('tg_username', sa.String, nullable=True),
sa.Column('saved_contacts', sa.Integer, nullable=False, default=0), sa.Column('saved_contacts', sa.Integer, nullable=False, default=0),
sa.PrimaryKeyConstraint('mxid')) sa.PrimaryKeyConstraint('mxid'))
@@ -50,9 +50,13 @@ def upgrade():
sa.Column('portal', sa.Integer), sa.Column('portal', sa.Integer),
sa.Column('portal_receiver', sa.Integer), sa.Column('portal_receiver', sa.Integer),
sa.PrimaryKeyConstraint('user', 'portal', 'portal_receiver'), sa.PrimaryKeyConstraint('user', 'portal', 'portal_receiver'),
sa.ForeignKeyConstraint(("user",), ("user.tgid",)), sa.ForeignKeyConstraint(("user",), ("user.tgid",),
name="user_portal_user_fkey",
onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(("portal", "portal_receiver"), sa.ForeignKeyConstraint(("portal", "portal_receiver"),
("portal.tgid", "portal.tg_receiver"))) ("portal.tgid", "portal.tg_receiver"),
name="user_portal_portal_fkey",
onupdate="CASCADE", ondelete="CASCADE"))
op.create_table('message', op.create_table('message',
sa.Column('mxid', sa.String), sa.Column('mxid', sa.String),
sa.Column('mx_room', sa.String), sa.Column('mx_room', sa.String),
+5 -8
View File
@@ -8,14 +8,13 @@ function fixperms {
# Go into env # Go into env
cd /opt/mautrixtelegram cd /opt/mautrixtelegram
source .venv/bin/activate export FFMPEG_BINARY=/usr/bin/ffmpeg
# Replace database path in alembic.ini # Replace database path in config.
sed -i "s#sqlite:///mautrix-telegram.db#sqlite:////data/mautrix-telegram.db#" alembic.ini
sed -i "s#sqlite:///mautrix-telegram.db#sqlite:////data/mautrix-telegram.db#" /data/config.yaml sed -i "s#sqlite:///mautrix-telegram.db#sqlite:////data/mautrix-telegram.db#" /data/config.yaml
# Check that database is in the right state # Check that database is in the right state
alembic upgrade head alembic -x config=/data/config.yaml upgrade head
if [[ ! -f /data/config.yaml ]]; then if [[ ! -f /data/config.yaml ]]; then
cp example-config.yaml /data/config.yaml cp example-config.yaml /data/config.yaml
@@ -28,7 +27,7 @@ if [[ ! -f /data/config.yaml ]]; then
fi fi
if [[ ! -f /data/registration.yaml ]]; then if [[ ! -f /data/registration.yaml ]]; then
python -m mautrix_telegram -g -c /data/config.yaml -r /data/registration.yaml python3 -m mautrix_telegram -g -c /data/config.yaml -r /data/registration.yaml
echo "Didn't find a registration file." echo "Didn't find a registration file."
echo "Generated ode for you." echo "Generated ode for you."
echo "Copy that over to synapses app service directory." echo "Copy that over to synapses app service directory."
@@ -36,7 +35,5 @@ if [[ ! -f /data/registration.yaml ]]; then
exit exit
fi fi
export FFMPEG_BINARY=/usr/bin/ffmpeg
fixperms fixperms
exec su-exec ${UID}:${GID} python -m mautrix_telegram -c /data/config.yaml exec su-exec ${UID}:${GID} python3 -m mautrix_telegram -c /data/config.yaml
+1 -1
View File
@@ -1,2 +1,2 @@
__version__ = "0.2.0rc2" __version__ = "0.2.0"
__author__ = "Tulir Asokan <tulir@maunium.net>" __author__ = "Tulir Asokan <tulir@maunium.net>"
+1 -1
View File
@@ -26,7 +26,7 @@ from alchemysession import AlchemySessionContainer
from mautrix_appservice import AppService from mautrix_appservice import AppService
from .base import Base from .base import Base
from .config import Config, DictWithRecursion from .config import Config
from .matrix import MatrixHandler from .matrix import MatrixHandler
from .db import init as init_db from .db import init as init_db
+1 -1
View File
@@ -166,7 +166,7 @@ class Bot(AbstractUser):
if not self.mxid_regex.match(mxid): if not self.mxid_regex.match(mxid):
return await reply("That doesn't look like a Matrix ID.") return await reply("That doesn't look like a Matrix ID.")
user = await u.User.get_by_mxid(mxid).ensure_started() user = await u.User.get_by_mxid(mxid).ensure_started()
if not user.whitelisted: if not user.relaybot_whitelisted:
return await reply("That user is not whitelisted to use the bridge.") return await reply("That user is not whitelisted to use the bridge.")
elif user.logged_in: elif user.logged_in:
displayname = f"@{user.username}" if user.username else user.displayname displayname = f"@{user.username}" if user.username else user.displayname
+29 -7
View File
@@ -17,6 +17,7 @@
import asyncio import asyncio
from telethon.errors import * from telethon.errors import *
from telethon.tl.types import ChatForbidden, ChannelForbidden
from mautrix_appservice import MatrixRequestError from mautrix_appservice import MatrixRequestError
from .. import portal as po from .. import portal as po
@@ -102,7 +103,7 @@ def _get_portal_murder_function(action, room_id, function, command, completed_me
} }
@command_handler() @command_handler(needs_auth=False)
async def delete_portal(evt: CommandEvent): async def delete_portal(evt: CommandEvent):
portal, ok = await _get_portal_and_check_permission(evt, "delete_portal") portal, ok = await _get_portal_and_check_permission(evt, "delete_portal")
if not ok: if not ok:
@@ -114,10 +115,14 @@ async def delete_portal(evt: CommandEvent):
return await evt.reply("Please confirm deletion of portal " return await evt.reply("Please confirm deletion of portal "
f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}) " f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}) "
f"to Telegram chat \"{portal.title}\" " f"to Telegram chat \"{portal.title}\" "
"by typing `$cmdprefix+sp confirm-delete`") "by typing `$cmdprefix+sp confirm-delete`"
"\n\n"
"**WARNING:** If the bridge bot has the power level to do so, **this "
"will kick ALL users** in the room. If you just want to remove the "
"bridge, use `$cmdprefix+sp unbridge` instead.")
@command_handler() @command_handler(needs_auth=False)
async def unbridge(evt: CommandEvent): async def unbridge(evt: CommandEvent):
portal, ok = await _get_portal_and_check_permission(evt, "unbridge_room") portal, ok = await _get_portal_and_check_permission(evt, "unbridge_room")
if not ok: if not ok:
@@ -131,7 +136,7 @@ async def unbridge(evt: CommandEvent):
"by typing `$cmdprefix+sp confirm-unbridge`") "by typing `$cmdprefix+sp confirm-unbridge`")
@command_handler() @command_handler(needs_auth=False)
async def bridge(evt: CommandEvent): async def bridge(evt: CommandEvent):
if len(evt.args) == 0: if len(evt.args) == 0:
return await evt.reply("**Usage:** " return await evt.reply("**Usage:** "
@@ -247,14 +252,31 @@ async def confirm_bridge(evt: CommandEvent):
return await evt.reply("Please use `$cmdprefix+sp continue` to confirm the bridging or " return await evt.reply("Please use `$cmdprefix+sp continue` to confirm the bridging or "
"`$cmdprefix+sp cancel` to cancel.") "`$cmdprefix+sp cancel` to cancel.")
user = evt.sender if evt.sender.logged_in else evt.tgbot
try:
entity = await user.client.get_entity(portal.peer)
except Exception:
evt.log.exception("Failed to get_entity(%s) for manual bridging.", portal.peer)
if evt.sender.logged_in:
return await evt.reply("Failed to get info of telegram chat. "
"You are logged in, are you in that chat?")
else:
return await evt.reply("Failed to get info of telegram chat. "
"You're not logged in, is the relay bot in the chat?")
if isinstance(entity, (ChatForbidden, ChannelForbidden)):
if evt.sender.logged_in:
return await evt.reply("You don't seem to be in that chat.")
else:
return await evt.reply("The bot doesn't seem to be in that chat.")
direct = False
portal.mxid = bridge_to_mxid portal.mxid = bridge_to_mxid
portal.title, portal.about, levels = await _get_initial_state(evt) portal.title, portal.about, levels = await _get_initial_state(evt)
portal.photo_id = "" portal.photo_id = ""
portal.save() portal.save()
entity = await evt.sender.client.get_entity(portal.peer) asyncio.ensure_future(portal.update_matrix_room(user, entity, direct, levels=levels),
direct = False
asyncio.ensure_future(portal.update_matrix_room(evt.sender, entity, direct, levels=levels),
loop=evt.loop) loop=evt.loop)
return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.") return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.")
+5 -3
View File
@@ -57,12 +57,14 @@ class UserPortal(Base):
query = None query = None
__tablename__ = "user_portal" __tablename__ = "user_portal"
user = Column(Integer, ForeignKey("user.tgid"), primary_key=True) user = Column(Integer, ForeignKey("user.tgid", onupdate="CASCADE", ondelete="CASCADE"),
primary_key=True)
portal = Column(Integer, primary_key=True) portal = Column(Integer, primary_key=True)
portal_receiver = Column(Integer, primary_key=True) portal_receiver = Column(Integer, primary_key=True)
__table_args__ = (ForeignKeyConstraint(("portal", "portal_receiver"), __table_args__ = (ForeignKeyConstraint(("portal", "portal_receiver"),
("portal.tgid", "portal.tg_receiver")),) ("portal.tgid", "portal.tg_receiver"),
onupdate="CASCADE", ondelete="CASCADE"),)
class User(Base): class User(Base):
@@ -70,7 +72,7 @@ class User(Base):
__tablename__ = "user" __tablename__ = "user"
mxid = Column(String, primary_key=True) mxid = Column(String, primary_key=True)
tgid = Column(Integer, nullable=True) tgid = Column(Integer, nullable=True, unique=True)
tg_username = Column(String, nullable=True) tg_username = Column(String, nullable=True)
saved_contacts = Column(Integer, default=0) saved_contacts = Column(Integer, default=0)
contacts = relationship("Contact", uselist=True, contacts = relationship("Contact", uselist=True,
+1 -1
View File
@@ -154,7 +154,7 @@ class MatrixHandler:
puppet = Puppet.get_by_mxid(user) puppet = Puppet.get_by_mxid(user)
if sender and puppet: if sender and puppet:
await portal.leave_matrix(puppet, sender) await portal.leave_matrix(puppet, sender, event_id)
user = User.get_by_mxid(user, create=False) user = User.get_by_mxid(user, create=False)
if not user: if not user:
+3 -1
View File
@@ -1318,6 +1318,8 @@ class Portal:
await intent.redact(self.mxid, mxid) await intent.redact(self.mxid, mxid)
async def _create_room_on_action(self, source, action): async def _create_room_on_action(self, source, action):
if source.is_relaybot:
return False
create_and_exit = (MessageActionChatCreate, MessageActionChannelCreate) create_and_exit = (MessageActionChatCreate, MessageActionChannelCreate)
create_and_continue = (MessageActionChatAddUser, MessageActionChatJoinedByLink) create_and_continue = (MessageActionChatAddUser, MessageActionChatJoinedByLink)
if isinstance(action, create_and_exit + create_and_continue): if isinstance(action, create_and_exit + create_and_continue):
@@ -1331,7 +1333,7 @@ class Portal:
action = update.action action = update.action
should_ignore = ((not self.mxid and not await self._create_room_on_action(source, action)) should_ignore = ((not self.mxid and not await self._create_room_on_action(source, action))
or self.is_duplicate_action(update)) or self.is_duplicate_action(update))
if should_ignore: if should_ignore or not self.mxid:
return return
# TODO figure out how to see changes to about text / channel username # TODO figure out how to see changes to about text / channel username
if isinstance(action, MessageActionChatEditTitle): if isinstance(action, MessageActionChatEditTitle):