diff --git a/.env.example b/.env.example index d311fec..c3a41f8 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,4 @@ -DB_PATH=data/database.sqlite3 -MATRIX_BOT_ID=@fckidiots:matrix.org MATRIX_HOMESERVER_URL=https://matrix.org -MATRIX_OWNER_ID=@kotovalexarian:matrix.org -MATRIX_PASSWORD=... +MATRIX_FULL_BOT_ID=@fckidiots:matrix.org +MATRIX_BOT_PASSWORD=... TELEGRAM_BOT_TOKEN=5890667880:... diff --git a/Dockerfile b/Dockerfile index 5914a9f..9f29f75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,4 +6,5 @@ RUN apt-get install --yes python3 python3-pip COPY requirements.txt /app/ RUN pip3 install -r requirements.txt COPY mirrortea/* /app/mirrortea/ +COPY config.yaml /app/ ENTRYPOINT ["/usr/bin/python3", "/app/mirrortea"] diff --git a/README.md b/README.md index 1805e99..a86bd34 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,54 @@ -MirrorTea +MirrorTea 🐦 ========= -New cool Matrix <-> Telegram bridge for personal use, replacement of -**mautrix-telegram**. +**New lite Matrix <-> Telegram bridge for personal use, replacement of +mautrix-telegram**. + + +For the matrix server is represented by a single and regular user. To simulate chats with different users, creates rooms in matrix, and sets /roomnick and /roomavatar corresponding to the simulated user in telegram. + +For telegram, it can be represented as a single bot. + +#### Advantages over the current mautrix bridge: + +- Supports the bot's private message bridge +- Requires no permanent host (server) +- Can run from a phone (termux) or user computer +- Does not require a domain +- Doesn't require a statistical IP +- No synapse server required +- Easy to deploy, unlike synapse +- Doesn't require any computing power like synapse +- Not tied to a specific matrix server + +#### Disadvantages: + +- Doesn't support papits +- No support for groups + +## Install 🌺 + +#### Clone repository + + +```git clone https://inex.dev/def/MirrorTea && cd MirrorTea``` + + +#### Then edit ```.env``` secrets config + +```mv .env.example .env``` + +```nano .env``` + + +#### Run on *unix-like systems: + +```pip install -r requirements.txt``` + +```python3 mirrortea``` + +#### Run with docker: + +```docker-compose up --build``` + +kotov isprav eto diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..6206344 --- /dev/null +++ b/config.yaml @@ -0,0 +1,7 @@ +matrix_owner_id: "@dettlaff:inex.rocks" +db_path: "data/database.sqlite3" + +matrix_homeserver_url: "{{ MATRIX_HOMESERVER_URL }}" +matrix_full_bot_id: "{{ MATRIX_FULL_BOT_ID }}" +matrix_bot_password: "{{ MATRIX_BOT_PASSWORD }}" +telegram_bot_token: "{{ TELEGRAM_BOT_TOKEN }}" diff --git a/mirrortea/__main__.py b/mirrortea/__main__.py index 5d076cd..2dc2aba 100644 --- a/mirrortea/__main__.py +++ b/mirrortea/__main__.py @@ -1,115 +1,36 @@ import asyncio -import os -import sqlite3 -import sys -import aiogram as telegram -import nio as matrix +from matrix import MatrixLoop +from abstract_source_platform.telegram import Telegram -TELEGRAM_USER_MATRIX_CHATS_SQL = ''' -CREATE TABLE IF NOT EXISTS telegram_user_matrix_chats ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - telegram_user_id INTEGER NOT NULL, - matrix_chat_id INTEGER NOT NULL, - FOREIGN KEY(telegram_user_id) REFERENCES telegram_users(id), - FOREIGN KEY(matrix_chat_id) REFERENCES matrix_chats(id) -); -''' +from config_dataclass import Config -TELEGRAM_USERS_SQL = ''' -CREATE TABLE IF NOT EXISTS telegram_users -( - id INTEGER PRIMARY KEY NOT NULL (15), - first_name TEXT NOT NULL (50), - last_name TEXT (50), - username TEXT (50), -); -''' +CONFIG_FILE_NAME = "config.yaml" def main(): - config = Config( - db_path=os.environ['DB_PATH'], - matrix_bot_id=os.environ['MATRIX_BOT_ID'], - matrix_homeserver_url=os.environ['MATRIX_HOMESERVER_URL'], - matrix_owner_id=os.environ['MATRIX_OWNER_ID'], - matrix_password=os.environ['MATRIX_PASSWORD'], - telegram_bot_token=os.environ['TELEGRAM_BOT_TOKEN'], - ) + config = Config.from_yaml_config(CONFIG_FILE_NAME) asyncio.run(Application(config).run()) -class Config: - def __init__(self, **kwargs): - self.db_path = kwargs['db_path'] - self.matrix_bot_id = kwargs['matrix_bot_id'] - self.matrix_homeserver_url = kwargs['matrix_homeserver_url'] - self.matrix_owner_id = kwargs['matrix_owner_id'] - self.matrix_password = kwargs['matrix_password'] - self.telegram_bot_token = kwargs['telegram_bot_token'] class Application: def __init__(self, config): self.config = config - self.sqlite_adapter = SqliteAdapter(self, config.db_path) self.matrix_loop = MatrixLoop(self) - self.telegram_loop = TelegramLoop(self) + self.telegram = Telegram(self) async def run(self): try: await self.matrix_loop.prepare() await asyncio.gather( self.matrix_loop.run(), - self.telegram_loop.run(), + self.telegram.run(), ) finally: if self.matrix_loop: await self.matrix_loop.finish() -class MatrixLoop: - def __init__(self, app): - self.app = app - self.client = matrix.AsyncClient( - app.config.matrix_homeserver_url, - app.config.matrix_bot_id, - ) - self.client.add_event_callback(self.on_message, matrix.RoomMessage) - async def prepare(self): - await self.client.login(self.app.config.matrix_password) - - async def finish(self): - await self.client.close() - - async def run(self): - await self.client.sync_forever(timeout=30000) - - async def on_message(self, room, event): - print(room, event, file=sys.stderr) - -class TelegramLoop: - def __init__(self, app): - self.app = app - self.bot = telegram.Bot(token=app.config.telegram_bot_token) - self.dispatcher = telegram.Dispatcher(bot=self.bot) - self.dispatcher.register_message_handler(self.on_message) - - async def run(self): - await self.dispatcher.start_polling() - - async def on_message(self, msg): - print(msg, file=sys.stderr) - -class SqliteAdapter: - def __init__(self, app, path): - self.app = app - self.path = path - self.conn = sqlite3.connect(path) - self._create_tables() - - def _create_tables(self): - self.conn.execute(TELEGRAM_USER_MATRIX_CHATS_SQL) - self.conn.commit() - -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/mirrortea/abstract_source_platform/__init__.py b/mirrortea/abstract_source_platform/__init__.py new file mode 100644 index 0000000..815fa79 --- /dev/null +++ b/mirrortea/abstract_source_platform/__init__.py @@ -0,0 +1,3 @@ +from abstract_source_platform.telegram import Telegram + +platforms = [Telegram] diff --git a/mirrortea/abstract_source_platform/abstact_source_platform.py b/mirrortea/abstract_source_platform/abstact_source_platform.py new file mode 100644 index 0000000..f12d8b3 --- /dev/null +++ b/mirrortea/abstract_source_platform/abstact_source_platform.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod + +from models.user import User + + +class AbstractSourcePlatform(ABC): + @abstractmethod + def get_user_information(self) -> User: + """Init tables in database""" diff --git a/mirrortea/abstract_source_platform/telegram.py b/mirrortea/abstract_source_platform/telegram.py new file mode 100644 index 0000000..0cb3916 --- /dev/null +++ b/mirrortea/abstract_source_platform/telegram.py @@ -0,0 +1,24 @@ +import sys +import aiogram as telegram + +from abstract_source_platform.abstact_source_platform import ( + AbstractSourcePlatform, +) +from models.user import User + + +class Telegram(AbstractSourcePlatform): + def __init__(self, app): + self.app = app + self.bot = telegram.Bot(token=app.config.telegram_bot_token) + self.dispatcher = telegram.Dispatcher(bot=self.bot) + self.dispatcher.register_message_handler(self.on_message) + + async def run(self): + await self.dispatcher.start_polling() + + async def on_message(self, msg): + print(msg, file=sys.stderr) + + async def get_user_information(self) -> User: + pass diff --git a/mirrortea/config_dataclass.py b/mirrortea/config_dataclass.py new file mode 100644 index 0000000..a636c4e --- /dev/null +++ b/mirrortea/config_dataclass.py @@ -0,0 +1,34 @@ +import os +import yaml +from pathlib import Path +from dataclasses import dataclass + +from jinja2 import BaseLoader, Environment + + +def render_env_template(raw_config: str) -> dict: + template = Environment(loader=BaseLoader).from_string(raw_config) + conf = template.render(**os.environ) + return yaml.safe_load(conf) + + +@dataclass +class Config: + db_path: Path + + matrix_homeserver_url: str + matrix_full_bot_id: str + matrix_bot_password: str + + telegram_bot_token: str + + matrix_owner_id: str + + @classmethod + def from_dict(config_class, dict): + return config_class(**dict) + + @classmethod + def from_yaml_config(config_class, path: Path): + with open(path) as raw: + return config_class.from_dict(render_env_template(raw.read())) diff --git a/mirrortea/matrix.py b/mirrortea/matrix.py new file mode 100644 index 0000000..061b4a3 --- /dev/null +++ b/mirrortea/matrix.py @@ -0,0 +1,35 @@ +import nio as matrix +import sys + + +class MatrixLoop: + def __init__(self, app): + self.app = app + self.client = matrix.AsyncClient( + app.config.matrix_homeserver_url, + app.config.matrix_full_bot_id, + ) + self.client.add_event_callback(self.on_message, matrix.RoomMessage) + + async def prepare(self): + await self.client.login(self.app.config.matrix_bot_password) + + async def finish(self): + await self.client.close() + + async def run(self): + await self.client.sync_forever(timeout=30000) + + async def on_message(self, room, event): + print(room, event, file=sys.stderr) + + def upgrade_room(self, room, telegram_nickname): + event_dict = matrix.event_builders.event_builder.EventBuilder( + name=telegram_nickname + ).as_dict() + client.room_send( + room_id=room, + message_type=event_dict["type"], + content=event_dict["content"], + ) # предположу что оно так работает + # https://matrix-nio.readthedocs.io/en/latest/nio.html#module-nio.event_builders.state_events diff --git a/mirrortea/models/user.py b/mirrortea/models/user.py new file mode 100644 index 0000000..e8315e5 --- /dev/null +++ b/mirrortea/models/user.py @@ -0,0 +1,15 @@ +from typing import Optional +from pydantic import BaseModel + + +class User(BaseModel): + """ + Bridge user from platform source + """ + + first_name: str + last_named: Optional[str] + + id: str + username: Optional[str] + avatar_hash: Optional[str] diff --git a/mirrortea/repository/__init__.py b/mirrortea/repository/__init__.py new file mode 100644 index 0000000..53fd20a --- /dev/null +++ b/mirrortea/repository/__init__.py @@ -0,0 +1,5 @@ +from repository.sqlite_repository import ( + SQLiteDatabaseRepository, +) + +repository = SQLiteDatabaseRepository() diff --git a/mirrortea/repository/abstract_db_repository.py b/mirrortea/repository/abstract_db_repository.py new file mode 100644 index 0000000..a0fd9fc --- /dev/null +++ b/mirrortea/repository/abstract_db_repository.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + + +class AbstractDatabaseRepository(ABC): + @abstractmethod + def create_tables(self) -> None: + """Init tables in database""" diff --git a/mirrortea/repository/sqlite_init_tables.py b/mirrortea/repository/sqlite_init_tables.py new file mode 100644 index 0000000..be530c6 --- /dev/null +++ b/mirrortea/repository/sqlite_init_tables.py @@ -0,0 +1,27 @@ +TELEGRAM_USER_MATRIX_CHATS_SQL = """ +CREATE TABLE IF NOT EXISTS telegram_user_matrix_chats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + telegram_user_id INTEGER NOT NULL, + matrix_chat_id INTEGER NOT NULL, + FOREIGN KEY(telegram_user_id) REFERENCES telegram_users(id), + FOREIGN KEY(matrix_chat_id) REFERENCES matrix_chats(id) +); +""" + +TELEGRAM_USERS_SQL = """ +CREATE TABLE IF NOT EXISTS telegram_users +( + id INTEGER PRIMARY KEY NOT NULL (15), + first_name TEXT NOT NULL (50), + last_name TEXT (50), + username TEXT (50), + avatar_hash BLOB, +); +""" + +MATRIX_ROOMS_SQL = """ +CREATE TABLE IF NOT EXISTS telegram_users +( + id TEXT PRIMARY KEY NOT NULL +); +""" diff --git a/mirrortea/repository/sqlite_repository.py b/mirrortea/repository/sqlite_repository.py new file mode 100644 index 0000000..65ce56c --- /dev/null +++ b/mirrortea/repository/sqlite_repository.py @@ -0,0 +1,31 @@ +import sqlite3 +from pathlib import Path +from typing import Optional + +from repository.abstract_db_repository import AbstractDatabaseRepository + +from repository.sqlite_init_tables import ( + TELEGRAM_USER_MATRIX_CHATS_SQL, + TELEGRAM_USERS_SQL, + MATRIX_ROOMS_SQL, +) + +TABLES_LIST = [ + TELEGRAM_USER_MATRIX_CHATS_SQL, + TELEGRAM_USERS_SQL, + MATRIX_ROOMS_SQL, +] + + +class SQLiteDatabaseRepository(AbstractDatabaseRepository): + def __init__(self, app, path: Path): + self.path = path + self.conn = sqlite3.connect(path) + + @staticmethod + def create_tables(self) -> None: + """Init tables in database""" + + for table in TABLES_LIST: + self.conn.execute(table) + self.conn.commit() diff --git a/requirements.txt b/requirements.txt index 3f3c37f..495b865 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ aiogram==2.24 matrix-nio==0.20.1 +jinja2==3.1.2 +pydantic==1.10.2 +pyyaml==6.0 \ No newline at end of file