├── .gitignore ├── README.md ├── config.ini ├── filters ├── ContentTypeFilter.py └── __init__.py ├── handlers ├── __init__.py ├── deleting.py ├── edit.py └── receive.py ├── locales ├── en │ └── LC_MESSAGES │ │ ├── messages.mo │ │ └── messages.po ├── messages.pot ├── ru │ └── LC_MESSAGES │ │ ├── messages.mo │ │ └── messages.po └── uk │ └── LC_MESSAGES │ └── messages.po ├── main.py ├── middlewares ├── __init__.py └── user_check.py ├── repo ├── __init__.py ├── modules │ ├── __init__.py │ ├── base.py │ ├── messages.py │ └── users.py └── repo.py ├── requirements.txt └── utils ├── __init__.py ├── config.py └── encryptor.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### JetBrains template 2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 4 | 5 | # User-specific stuff 6 | .idea/**/workspace.xml 7 | .idea/**/tasks.xml 8 | .idea/**/usage.statistics.xml 9 | .idea/**/dictionaries 10 | .idea/**/shelf 11 | 12 | # AWS User-specific 13 | .idea/**/aws.xml 14 | 15 | # Generated files 16 | .idea/**/contentModel.xml 17 | 18 | # Sensitive or high-churn files 19 | .idea/**/dataSources/ 20 | .idea/**/dataSources.ids 21 | .idea/**/dataSources.local.xml 22 | .idea/**/sqlDataSources.xml 23 | .idea/**/dynamic.xml 24 | .idea/**/uiDesigner.xml 25 | .idea/**/dbnavigator.xml 26 | 27 | # Gradle 28 | .idea/**/gradle.xml 29 | .idea/**/libraries 30 | 31 | # Gradle and Maven with auto-import 32 | # When using Gradle or Maven with auto-import, you should exclude module files, 33 | # since they will be recreated, and may cause churn. Uncomment if using 34 | # auto-import. 35 | # .idea/artifacts 36 | # .idea/compiler.xml 37 | # .idea/jarRepositories.xml 38 | # .idea/modules.xml 39 | # .idea/*.iml 40 | # .idea/modules 41 | # *.iml 42 | # *.ipr 43 | 44 | # CMake 45 | cmake-build-*/ 46 | 47 | # Mongo Explorer plugin 48 | .idea/**/mongoSettings.xml 49 | 50 | # File-based project format 51 | *.iws 52 | 53 | # IntelliJ 54 | out/ 55 | 56 | # mpeltonen/sbt-idea plugin 57 | .idea_modules/ 58 | 59 | # JIRA plugin 60 | atlassian-ide-plugin.xml 61 | 62 | # Cursive Clojure plugin 63 | .idea/replstate.xml 64 | 65 | # SonarLint plugin 66 | .idea/sonarlint/ 67 | 68 | # Crashlytics plugin (for Android Studio and IntelliJ) 69 | com_crashlytics_export_strings.xml 70 | crashlytics.properties 71 | crashlytics-build.properties 72 | fabric.properties 73 | 74 | # Editor-based Rest Client 75 | .idea/httpRequests 76 | 77 | # Android studio 3.1+ serialized cache file 78 | .idea/caches/build_file_checksums.ser 79 | 80 | ### Example user template template 81 | ### Example user template 82 | 83 | # IntelliJ project files 84 | .idea 85 | *.iml 86 | out 87 | gen 88 | ### Python template 89 | # Byte-compiled / optimized / DLL files 90 | __pycache__/ 91 | *.py[cod] 92 | *$py.class 93 | 94 | # C extensions 95 | *.so 96 | 97 | # Distribution / packaging 98 | .Python 99 | build/ 100 | develop-eggs/ 101 | dist/ 102 | downloads/ 103 | eggs/ 104 | .eggs/ 105 | lib/ 106 | lib64/ 107 | parts/ 108 | sdist/ 109 | var/ 110 | wheels/ 111 | share/python-wheels/ 112 | *.egg-info/ 113 | .installed.cfg 114 | *.egg 115 | MANIFEST 116 | 117 | # PyInstaller 118 | # Usually these files are written by a python script from a template 119 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 120 | *.manifest 121 | *.spec 122 | 123 | # Installer logs 124 | pip-log.txt 125 | pip-delete-this-directory.txt 126 | 127 | # Unit test / coverage reports 128 | htmlcov/ 129 | .tox/ 130 | .nox/ 131 | .coverage 132 | .coverage.* 133 | .cache 134 | nosetests.xml 135 | coverage.xml 136 | *.cover 137 | *.py,cover 138 | .hypothesis/ 139 | .pytest_cache/ 140 | cover/ 141 | 142 | # Translations 143 | *.mo 144 | *.pot 145 | 146 | # Django stuff: 147 | *.log 148 | local_settings.py 149 | db.sqlite3 150 | db.sqlite3-journal 151 | 152 | # Flask stuff: 153 | instance/ 154 | .webassets-cache 155 | 156 | # Scrapy stuff: 157 | .scrapy 158 | 159 | # Sphinx documentation 160 | docs/_build/ 161 | 162 | # PyBuilder 163 | .pybuilder/ 164 | target/ 165 | 166 | # Jupyter Notebook 167 | .ipynb_checkpoints 168 | 169 | # IPython 170 | profile_default/ 171 | ipython_config.py 172 | 173 | # pyenv 174 | # For a library or package, you might want to ignore these files since the code is 175 | # intended to run in multiple environments; otherwise, check them in: 176 | # .python-version 177 | 178 | # pipenv 179 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 180 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 181 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 182 | # install all needed dependencies. 183 | #Pipfile.lock 184 | 185 | # poetry 186 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 187 | # This is especially recommended for binary packages to ensure reproducibility, and is more 188 | # commonly ignored for libraries. 189 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 190 | #poetry.lock 191 | 192 | # pdm 193 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 194 | #pdm.lock 195 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 196 | # in version control. 197 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 198 | .pdm.toml 199 | .pdm-python 200 | .pdm-build/ 201 | 202 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 203 | __pypackages__/ 204 | 205 | # Celery stuff 206 | celerybeat-schedule 207 | celerybeat.pid 208 | 209 | # SageMath parsed files 210 | *.sage.py 211 | 212 | # Environments 213 | .env 214 | .venv 215 | env/ 216 | venv/ 217 | ENV/ 218 | env.bak/ 219 | venv.bak/ 220 | 221 | # Spyder project settings 222 | .spyderproject 223 | .spyproject 224 | 225 | # Rope project settings 226 | .ropeproject 227 | 228 | # mkdocs documentation 229 | /site 230 | 231 | # mypy 232 | .mypy_cache/ 233 | .dmypy.json 234 | dmypy.json 235 | 236 | # Pyre type checker 237 | .pyre/ 238 | 239 | # pytype static type analyzer 240 | .pytype/ 241 | 242 | # Cython debug symbols 243 | cython_debug/ 244 | 245 | # PyCharm 246 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 247 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 248 | # and can be added to the global gitignore or merged into this file. For a more nuclear 249 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 250 | #.idea/ 251 | 252 | /config.ini 253 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Official bot](https://t.me/MessageLoggingBot) 2 | 3 | ## Telegram private message logging based on Telegram Business features 4 | 5 | This bot was designed for those who would like to receive some kind of notification when someone edits or 6 | deletes messages in a personal dialogue with them. 7 | 8 | To ensure the security of message storage, an encryption system is used based on the `connection_id` parameter 9 | which Telegram transmits when connecting a bot to a profile and which is unique for each user, which 10 | allows you to guarantee complete safety of messages. 11 | 12 | ### List of supported message types: 13 | - Text message 14 | - Media message with caption (Photo/Video/GIF/Voice/Audio) 15 | - Media message without caption (Photo/Video/GIF/Voice/Audio) 16 | - Stickers 17 | - Video note 18 | - Location (It is not saved in the database, so when changed, only the current coordinates are displayed) 19 | 20 | ### Known Issues: 21 | - Messages you received before connecting to the bot cannot be tracked. 22 | - Unfortunately, due to the fact that the `connection_id` is unique for each connection - when disconnecting the bot from the profile 23 | all your messages will be deleted from the database because they will never be able to be decrypted in the future, which means further 24 | storage makes no sense. 25 | 26 | ### List of supported UI languages: 27 | - English 28 | - Ukrainian 29 | - Russian -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [BOT] 2 | token = 3 | 4 | [DATABASE] 5 | username = 6 | password = 7 | ip = 8 | port = 9 | db = -------------------------------------------------------------------------------- /filters/ContentTypeFilter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from aiogram.enums import ContentType 4 | from aiogram.filters import Filter 5 | from aiogram.types import Message, BusinessMessagesDeleted 6 | 7 | from repo import Repo 8 | from utils.encryptor import get_text_hash 9 | 10 | 11 | class ContentTypeFilter(Filter): 12 | def __init__(self, *args) -> None: 13 | self.type = args 14 | 15 | async def __call__(self, message: Message, repo: Repo) -> bool: 16 | if isinstance(message, BusinessMessagesDeleted): 17 | saved_message = repo.messages.get(message_id=message.message_ids[0], 18 | connection_id=get_text_hash(message.business_connection_id)) 19 | if saved_message is None: 20 | logging.warning(f"Received update for message with id={message.message_ids[0]} in chat={message.chat.id} that was not found") 21 | return False 22 | 23 | if saved_message.is_media: 24 | return saved_message.media_type in self.type 25 | elif saved_message.is_sticker: return ContentType.STICKER in self.type 26 | else: return ContentType.TEXT in self.type 27 | 28 | return message.content_type in self.type -------------------------------------------------------------------------------- /filters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyperAI/tg-message-logger-bot/263ce2b20c79251078622192ad896f204b576fcb/filters/__init__.py -------------------------------------------------------------------------------- /handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyperAI/tg-message-logger-bot/263ce2b20c79251078622192ad896f204b576fcb/handlers/__init__.py -------------------------------------------------------------------------------- /handlers/deleting.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from aiogram import Router, Bot 5 | from aiogram.enums import ContentType 6 | from aiogram.exceptions import TelegramBadRequest 7 | from aiogram.types import BusinessMessagesDeleted 8 | from aiogram.utils.i18n import gettext as _ 9 | 10 | from filters.ContentTypeFilter import ContentTypeFilter 11 | from repo import Repo 12 | from utils.encryptor import get_text_hash, TextEncryptor 13 | 14 | message_delete_route = Router() 15 | 16 | 17 | # Text 18 | @message_delete_route.deleted_business_messages(ContentTypeFilter(ContentType.TEXT, )) 19 | async def text_delete(bdm: BusinessMessagesDeleted, bot: Bot, repo: Repo) -> None: 20 | user = repo.users.get_by_connection(get_text_hash(bdm.business_connection_id)) 21 | 22 | if user is None: 23 | return 24 | connection_id = get_text_hash(bdm.business_connection_id) 25 | 26 | for m in bdm.message_ids: 27 | message = repo.messages.get(message_id=m, connection_id=connection_id) 28 | if message is None: 29 | return 30 | 31 | repo.messages.delete(message) 32 | 33 | user_link = "" 34 | if bdm.chat.has_private_forwards: 35 | user_link = f"tg://user?id={bdm.chat.id}" 36 | elif bdm.chat.username is not None: 37 | user_link = "https://t.me/" + bdm.chat.username 38 | 39 | text = _("🗑 Deletion noticed!" 40 | "\n\nMessage by {name}:" 41 | "
{msg}", 42 | locale=user.language).format(user_link=user_link, 43 | name=bdm.chat.full_name, 44 | msg=TextEncryptor(key=bdm.business_connection_id).decrypt( 45 | message.message)) 46 | 47 | await bot.send_message(chat_id=user.id, text=text) 48 | 49 | await asyncio.sleep(0.4) # To avoid limit 50 | 51 | 52 | # Media (Photo, Video, Voice, Animation, Audio) 53 | @message_delete_route.deleted_business_messages(ContentTypeFilter(ContentType.PHOTO, 54 | ContentType.VIDEO, 55 | ContentType.VOICE, 56 | ContentType.ANIMATION, 57 | ContentType.AUDIO, 58 | ContentType.DOCUMENT, )) 59 | async def media_delete(bdm: BusinessMessagesDeleted, bot: Bot, repo: Repo) -> None: 60 | user = repo.users.get_by_connection(get_text_hash(bdm.business_connection_id)) 61 | 62 | if user is None: 63 | return 64 | connection_id = get_text_hash(bdm.business_connection_id) 65 | 66 | for m in bdm.message_ids: 67 | message = repo.messages.get(message_id=m, connection_id=connection_id) 68 | if message is None: 69 | return 70 | 71 | repo.messages.delete(message) 72 | 73 | user_link = "" 74 | if bdm.chat.has_private_forwards: 75 | user_link = f"tg://user?id={bdm.chat.id}" 76 | elif bdm.chat.username is not None: 77 | user_link = "https://t.me/" + bdm.chat.username 78 | 79 | text = _("🗑 Deletion noticed!" 80 | "\n\nMessage by {name}:", 81 | locale=user.language).format(user_link=user_link, 82 | name=bdm.chat.full_name) 83 | 84 | if message.message is None: 85 | pass 86 | else: 87 | text += "
{msg}".format( 88 | msg=TextEncryptor(key=bdm.business_connection_id).decrypt(message.message)) 89 | 90 | if message.media_type == ContentType.PHOTO: 91 | await bot.send_photo(chat_id=user.id, 92 | photo=TextEncryptor(key=bdm.business_connection_id).decrypt(message.media), 93 | caption=text) 94 | elif message.media_type == ContentType.VIDEO: 95 | await bot.send_video(chat_id=user.id, 96 | video=TextEncryptor(key=bdm.business_connection_id).decrypt(message.media), 97 | caption=text) 98 | elif message.media_type == ContentType.VOICE: 99 | try: 100 | await bot.send_voice(chat_id=user.id, 101 | voice=TextEncryptor(key=bdm.business_connection_id).decrypt(message.media), 102 | caption=text) 103 | except TelegramBadRequest: 104 | t = await bot.send_message(chat_id=user.id, 105 | text=text) 106 | await bot.send_message(reply_to_message_id=t.message_id, 107 | chat_id=user.id, 108 | text=_("Voice message can't be sent because of your privacy settings!")) 109 | elif message.media_type == ContentType.ANIMATION: 110 | await bot.send_animation(chat_id=user.id, 111 | animation=TextEncryptor(key=bdm.business_connection_id).decrypt(message.media), 112 | caption=text) 113 | elif message.media_type == ContentType.DOCUMENT: 114 | await bot.send_document(chat_id=user.id, 115 | document=TextEncryptor(key=bdm.business_connection_id).decrypt(message.media), 116 | caption=text) 117 | else: 118 | return 119 | 120 | await asyncio.sleep(0.4) # To avoid limit 121 | 122 | 123 | # No caption media (Stickers, Video note) 124 | @message_delete_route.deleted_business_messages(ContentTypeFilter(ContentType.STICKER, ContentType.VIDEO_NOTE)) 125 | async def nocap_media_delete(bdm: BusinessMessagesDeleted, bot: Bot, repo: Repo) -> None: 126 | user = repo.users.get_by_connection(get_text_hash(bdm.business_connection_id)) 127 | 128 | if user is None: 129 | return 130 | connection_id = get_text_hash(bdm.business_connection_id) 131 | 132 | for m in bdm.message_ids: 133 | message = repo.messages.get(message_id=m, connection_id=connection_id) 134 | if message is None: 135 | return 136 | 137 | repo.messages.delete(message) 138 | 139 | user_link = "" 140 | if bdm.chat.has_private_forwards: 141 | user_link = f"tg://user?id={bdm.chat.id}" 142 | elif bdm.chat.username is not None: 143 | user_link = "https://t.me/" + bdm.chat.username 144 | 145 | text = _("🗑 Deletion noticed!" 146 | "\n\nMessage by {name}:", 147 | locale=user.language).format(user_link=user_link, 148 | name=bdm.chat.full_name) 149 | 150 | t = await bot.send_message(chat_id=user.id, text=text) 151 | 152 | if message.is_sticker: 153 | await bot.send_sticker(chat_id=user.id, 154 | reply_to_message_id=t.message_id, 155 | sticker=TextEncryptor(key=bdm.business_connection_id).decrypt(message.sticker)) 156 | elif message.media_type == ContentType.VIDEO_NOTE: 157 | await bot.send_video_note(chat_id=user.id, 158 | reply_to_message_id=t.message_id, 159 | video_note=TextEncryptor(key=bdm.business_connection_id).decrypt(message.media)) 160 | else: 161 | return 162 | 163 | await asyncio.sleep(0.4) # To avoid limit 164 | 165 | 166 | @message_delete_route.edited_business_message() 167 | async def not_handled(msg: BusinessMessagesDeleted): 168 | logging.warning("Some deleting update is not handled!") 169 | -------------------------------------------------------------------------------- /handlers/edit.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from aiogram import Router, Bot 4 | from aiogram.enums import ContentType 5 | from aiogram.types import Message 6 | from aiogram.utils.i18n import gettext as _ 7 | from aiogram.utils.media_group import MediaGroupBuilder 8 | 9 | from filters.ContentTypeFilter import ContentTypeFilter 10 | from repo import Repo 11 | from repo.modules.messages import MessageData 12 | from repo.modules.users import UserData 13 | from utils.encryptor import get_text_hash, TextEncryptor 14 | 15 | message_edit_route = Router() 16 | 17 | 18 | async def get_data(repo: Repo, business_connection_id, 19 | from_user_id, message_id) -> typing.Optional[tuple[UserData, MessageData]]: 20 | user = repo.users.get_by_connection(get_text_hash(business_connection_id)) 21 | 22 | if user is None: 23 | return 24 | 25 | if user.id == from_user_id: 26 | return 27 | 28 | connection_id = get_text_hash(business_connection_id) 29 | 30 | message = repo.messages.get(message_id=message_id, connection_id=connection_id) 31 | 32 | if message is None: 33 | return 34 | 35 | return user, message 36 | 37 | 38 | # Text edit handler 39 | @message_edit_route.edited_business_message(ContentTypeFilter(ContentType.TEXT, )) 40 | async def text_edit(bm: Message, bot: Bot, repo: Repo) -> None: 41 | data = await get_data(repo, bm.business_connection_id, bm.from_user.id, bm.message_id) 42 | if data is None: return 43 | 44 | message = data[1] 45 | user = data[0] 46 | 47 | user_link = "" 48 | if bm.chat.has_private_forwards: 49 | user_link = f"tg://user?id={bm.chat.id}" 50 | elif bm.chat.username is not None: 51 | user_link = "https://t.me/" + bm.chat.username 52 | 53 | text = _("✏ Editing noticed!" 54 | "\n\nOld message by {name}:" 55 | "
{old_msg}" 56 | "\nNew message:" 57 | "
{new_msg}", 58 | locale=user.language).format(user_link=user_link, 59 | name=bm.chat.full_name, 60 | new_msg=bm.html_text, 61 | old_msg=TextEncryptor(key=bm.business_connection_id).decrypt( 62 | message.message)) 63 | 64 | message.message = TextEncryptor(key=bm.business_connection_id).encrypt(bm.html_text) 65 | repo.save() 66 | 67 | await bot.send_message(chat_id=user.id, text=text) 68 | 69 | 70 | # Video, Photo, Animation, Voice, Audio caption edit handler 71 | @message_edit_route.edited_business_message(ContentTypeFilter(ContentType.PHOTO, 72 | ContentType.VIDEO, 73 | ContentType.ANIMATION, 74 | ContentType.VOICE, 75 | ContentType.AUDIO, 76 | ContentType.DOCUMENT)) 77 | async def media_edit(bm: Message, bot: Bot, repo: Repo) -> None: 78 | data = await get_data(repo, bm.business_connection_id, bm.from_user.id, bm.message_id) 79 | if data is None: return 80 | 81 | message = data[1] 82 | user = data[0] 83 | 84 | user_link = "" 85 | if bm.chat.has_private_forwards: 86 | user_link = f"tg://user?id={bm.chat.id}" 87 | elif bm.chat.username is not None: 88 | user_link = "https://t.me/" + bm.chat.username 89 | 90 | text = _("✏ Editing noticed!" 91 | "\n\nOld message by {name}:" 92 | "
{old_msg}" 93 | "\nNew message:" 94 | "
{new_msg}", 95 | locale=user.language).format(user_link=user_link, 96 | name=bm.chat.full_name, 97 | new_msg=bm.html_text, 98 | old_msg=TextEncryptor(key=bm.business_connection_id).decrypt( 99 | message.message) if message.message is not None else "") 100 | 101 | message.message = TextEncryptor(key=bm.business_connection_id).encrypt(bm.html_text) 102 | repo.save() 103 | 104 | if bm.content_type == ContentType.PHOTO: 105 | media = bm.photo[-1].file_id 106 | else: 107 | media = bm.model_dump()[bm.content_type]['file_id'] 108 | 109 | if bm.content_type not in [ContentType.ANIMATION]: 110 | media_group = MediaGroupBuilder(caption=text) 111 | media_group.add(type=message.media_type if message.media_type not in [ContentType.VOICE] else ContentType.AUDIO, 112 | media=TextEncryptor(key=bm.business_connection_id).decrypt(message.media)) 113 | 114 | if media != TextEncryptor(key=bm.business_connection_id).decrypt(message.media): 115 | media_group.add(type=bm.content_type if bm.content_type not in [ContentType.VOICE] else ContentType.AUDIO, 116 | media=media) 117 | 118 | message.media = TextEncryptor(key=bm.business_connection_id).encrypt(media) 119 | repo.save() 120 | 121 | await bot.send_media_group(chat_id=user.id, media=media_group.build()) 122 | else: 123 | if bm.content_type == ContentType.ANIMATION: 124 | message.media = TextEncryptor(key=bm.business_connection_id).encrypt(media) 125 | repo.save() 126 | 127 | await bot.send_animation(chat_id=user.id, animation=media, caption=text) 128 | 129 | 130 | # Location change handler 131 | @message_edit_route.edited_business_message(ContentTypeFilter(ContentType.LOCATION, )) 132 | async def location_edit(bm: Message, bot: Bot, repo: Repo) -> None: 133 | user = repo.users.get_by_connection(connection_id=get_text_hash(bm.business_connection_id)) 134 | 135 | user_link = "" 136 | if bm.chat.has_private_forwards: 137 | user_link = f"tg://user?id={bm.chat.id}" 138 | elif bm.chat.username is not None: 139 | user_link = "https://t.me/" + bm.chat.username 140 | 141 | text = _( 142 | "📍 Location change detected!" 143 | "\n\nby {name}" 144 | "\n\nP.S. The bot does not store information about the location and sees it only at the time of update. Therefore, it is impossible to find out what exactly has changed", 145 | locale=user.language 146 | ).format( 147 | user_link=user_link, 148 | name=bm.chat.full_name 149 | ) 150 | 151 | await bot.send_message(chat_id=user.id, text=text) 152 | 153 | 154 | @message_edit_route.edited_business_message() 155 | async def not_handled(msg: Message): 156 | print(msg.content_type) -------------------------------------------------------------------------------- /handlers/receive.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from aiogram import Router, Bot 4 | from aiogram.enums import ContentType 5 | from aiogram.types import Message 6 | from aiogram.utils.i18n import gettext as _ 7 | 8 | from filters.ContentTypeFilter import ContentTypeFilter 9 | from repo import Repo 10 | from repo.modules.messages import MessageData 11 | from utils.encryptor import get_text_hash, TextEncryptor 12 | 13 | message_receive_route = Router() 14 | bad_users = [] # Used to prevent spam if user is not found in db 15 | 16 | 17 | # Text 18 | @message_receive_route.business_message(ContentTypeFilter(ContentType.TEXT,)) 19 | async def text_handle(msg: Message, repo: Repo, bot: Bot) -> None: 20 | user = repo.users.get_by_connection(get_text_hash(msg.business_connection_id)) 21 | 22 | if user is None: 23 | if get_text_hash(msg.business_connection_id) not in bad_users: 24 | bad_users.append(get_text_hash(msg.business_connection_id)) 25 | bot_info = await bot.me() 26 | await msg.answer( 27 | _("An error has occurred! Please add the bot to your profile again!\n\nvia @{bot_username}").format( 28 | bot_username=bot_info.username)) 29 | return 30 | 31 | if user.id == msg.from_user.id: 32 | return 33 | 34 | msg_data = MessageData(connection_id=get_text_hash(msg.business_connection_id), 35 | message_id=msg.message_id, 36 | message=TextEncryptor(key=msg.business_connection_id).encrypt(msg.html_text)) 37 | 38 | repo.messages.add(message=msg_data) 39 | 40 | 41 | # Sticker 42 | @message_receive_route.business_message(ContentTypeFilter(ContentType.STICKER, )) 43 | async def stick_handle(msg: Message, repo: Repo, bot: Bot) -> None: 44 | user = repo.users.get_by_connection(get_text_hash(msg.business_connection_id)) 45 | 46 | if user is None: 47 | if get_text_hash(msg.business_connection_id) not in bad_users: 48 | bad_users.append(get_text_hash(msg.business_connection_id)) 49 | bot_info = await bot.me() 50 | await msg.answer( 51 | _("An error has occurred! Please add the bot to your profile again!\n\nvia @{bot_username}").format( 52 | bot_username=bot_info.username)) 53 | return 54 | 55 | if user.id == msg.from_user.id: 56 | return 57 | 58 | msg_data = MessageData(connection_id=get_text_hash(msg.business_connection_id), 59 | message_id=msg.message_id, 60 | is_sticker=True, 61 | sticker=TextEncryptor(key=msg.business_connection_id).encrypt(msg.sticker.file_id)) 62 | 63 | repo.messages.add(message=msg_data) 64 | 65 | 66 | # Media (Video, Animation, Voice, Photo, Audio) 67 | @message_receive_route.business_message( 68 | ContentTypeFilter( 69 | ContentType.VIDEO, 70 | ContentType.VOICE, 71 | ContentType.ANIMATION, 72 | ContentType.PHOTO, 73 | ContentType.VIDEO_NOTE, 74 | ContentType.AUDIO, 75 | ContentType.DOCUMENT, 76 | ) 77 | ) 78 | async def media_handle(msg: Message, repo: Repo, bot: Bot) -> None: 79 | user = repo.users.get_by_connection(get_text_hash(msg.business_connection_id)) 80 | 81 | if user is None: 82 | if get_text_hash(msg.business_connection_id) not in bad_users: 83 | bad_users.append(get_text_hash(msg.business_connection_id)) 84 | bot_info = await bot.me() 85 | await msg.answer( 86 | _("An error has occurred! Please add the bot to your profile again!\n\nvia @{bot_username}").format( 87 | bot_username=bot_info.username)) 88 | return 89 | 90 | if user.id == msg.from_user.id: 91 | return 92 | 93 | msg_data = MessageData(connection_id=get_text_hash(msg.business_connection_id), 94 | message_id=msg.message_id, 95 | is_media=True, 96 | media_type=msg.content_type, 97 | media=TextEncryptor(key=msg.business_connection_id).encrypt( 98 | msg.model_dump()[msg.content_type][ 99 | 'file_id'] if msg.content_type != ContentType.PHOTO else msg.photo[-1].file_id), 100 | message=TextEncryptor(key=msg.business_connection_id).encrypt( 101 | msg.caption) if msg.caption is not None else None) 102 | 103 | repo.messages.add(message=msg_data) 104 | 105 | 106 | @message_receive_route.business_message() 107 | async def not_handled(msg: Message): 108 | logging.warning(f"Message of type {msg.content_type} is not handled!") 109 | -------------------------------------------------------------------------------- /locales/en/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyperAI/tg-message-logger-bot/263ce2b20c79251078622192ad896f204b576fcb/locales/en/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /locales/en/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # English translations for PROJECT. 2 | # Copyright (C) 2024 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR
{msg}" 41 | msgstr "" 42 | 43 | #: handlers/deleting.py:72 handlers/deleting.py:130 44 | msgid "" 45 | "🗑 Deletion noticed!\n" 46 | "\n" 47 | "Message by {name}:" 48 | msgstr "" 49 | 50 | #: handlers/deleting.py:99 51 | msgid "Voice message can't be sent because of your privacy settings!" 52 | msgstr "" 53 | 54 | #: handlers/edit.py:53 handlers/edit.py:89 55 | msgid "" 56 | "✏ Editing noticed!\n" 57 | "\n" 58 | "Old message by {name}:
{old_msg}\n" 60 | "New message:
{new_msg}" 61 | msgstr "" 62 | 63 | #: handlers/edit.py:140 64 | msgid "" 65 | "📍 Location change detected!\n" 66 | "\n" 67 | "by {name}\n" 68 | "\n" 69 | "P.S. The bot does not store information about the location and sees it " 70 | "only at the time of update. Therefore, it is impossible to find out what " 71 | "exactly has changed" 72 | msgstr "" 73 | 74 | #: handlers/receive.py:32 75 | msgid "" 76 | "An error has occurred! Please add the bot to your profile again!\n" 77 | "\n" 78 | "via @{bot_username}" 79 | msgstr "" 80 | 81 | #~ msgid "" 82 | #~ "Hello, {name}!\n" 83 | #~ "I will help you to log editing " 84 | #~ "and deleting messages done by another" 85 | #~ " users!\n" 86 | #~ "\n" 87 | #~ "You just need to send me ID " 88 | #~ "of the channel in which you prefer" 89 | #~ " to see the logs. By default I" 90 | #~ " will save them in your 'Saved' " 91 | #~ "chat" 92 | #~ msgstr "" 93 | 94 | #~ msgid "" 95 | #~ "🗑 Deletion noticed!\n" 96 | #~ "\n" 97 | #~ "Message by {name}:
{msg}" 99 | #~ msgstr "" 100 | 101 | #~ msgid "" 102 | #~ "✏ Editing noticed!\n" 103 | #~ "\n" 104 | #~ "Old message by {name}:
{old_msg}" 106 | #~ "\n" 107 | #~ "New message:
{new_msg}" 108 | #~ msgstr "" 109 | 110 | #~ msgid "" 111 | #~ "Hello, {name}!\n" 112 | #~ "I will help you to log editing " 113 | #~ "and deleting messages done by another" 114 | #~ " users!\n" 115 | #~ "\n" 116 | #~ "You just need to send me ID " 117 | #~ "of the channel in which you prefer" 118 | #~ " to see the logs. By default I" 119 | #~ " will save them here" 120 | #~ msgstr "" 121 | 122 | #~ msgid "" 123 | #~ "Sorry but I can't found this chat.\n" 124 | #~ "\n" 125 | #~ "Maybe you forgot to add me to it?" 126 | #~ msgstr "" 127 | 128 | #~ msgid "" 129 | #~ "Hello, {name}!\n" 130 | #~ "I will help you to log editing " 131 | #~ "and deleting messages done by another" 132 | #~ " users!\n" 133 | #~ "\n" 134 | #~ "Source code" 136 | #~ msgstr "" 137 | 138 | #~ msgid "" 139 | #~ "🗑 Deletion noticed!\n" 140 | #~ "\n" 141 | #~ "Message by {name}:
{msg}" 144 | #~ msgstr "" 145 | 146 | #~ msgid "" 147 | #~ "🗑 Deletion noticed!\n" 148 | #~ "\n" 149 | #~ "Message by {name}:" 150 | #~ msgstr "" 151 | 152 | #~ msgid "" 153 | #~ "🗑 Deletion noticed!\n" 154 | #~ "\n" 155 | #~ "Message by {name}" 156 | #~ msgstr "" 157 | 158 | #~ msgid "" 159 | #~ "✏ Editing noticed!\n" 160 | #~ "\n" 161 | #~ "Old message by {name}:
{old_msg}\n" 164 | #~ "New message:
{new_msg}" 165 | #~ msgstr "" 166 | 167 | #~ msgid "" 168 | #~ "✏ Editing noticed!\n" 169 | #~ "\n" 170 | #~ "Old message by {name}:
{old_msg}\n" 173 | #~ "New message:
{new_msg}" 174 | #~ msgstr "" 175 | 176 | -------------------------------------------------------------------------------- /locales/messages.pot: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2025 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR
{msg}" 40 | msgstr "" 41 | 42 | #: handlers/deleting.py:72 handlers/deleting.py:130 43 | msgid "" 44 | "🗑 Deletion noticed!\n" 45 | "\n" 46 | "Message by {name}:" 47 | msgstr "" 48 | 49 | #: handlers/deleting.py:99 50 | msgid "Voice message can't be sent because of your privacy settings!" 51 | msgstr "" 52 | 53 | #: handlers/edit.py:53 handlers/edit.py:89 54 | msgid "" 55 | "✏ Editing noticed!\n" 56 | "\n" 57 | "Old message by {name}:
{old_msg}\n" 59 | "New message:
{new_msg}" 60 | msgstr "" 61 | 62 | #: handlers/edit.py:140 63 | msgid "" 64 | "📍 Location change detected!\n" 65 | "\n" 66 | "by {name}\n" 67 | "\n" 68 | "P.S. The bot does not store information about the location and sees it " 69 | "only at the time of update. Therefore, it is impossible to find out what " 70 | "exactly has changed" 71 | msgstr "" 72 | 73 | #: handlers/receive.py:32 74 | msgid "" 75 | "An error has occurred! Please add the bot to your profile again!\n" 76 | "\n" 77 | "via @{bot_username}" 78 | msgstr "" 79 | 80 | -------------------------------------------------------------------------------- /locales/ru/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyperAI/tg-message-logger-bot/263ce2b20c79251078622192ad896f204b576fcb/locales/ru/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /locales/ru/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # Russian translations for PROJECT. 2 | # Copyright (C) 2024 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR
{msg}" 47 | "" 48 | msgstr "" 49 | "🗑 Замечено удаление!\n" 50 | "\n" 51 | "Сообщение от {name}:
{msg}" 52 | "blockquote>" 53 | 54 | #: handlers/deleting.py:72 handlers/deleting.py:130 55 | msgid "" 56 | "🗑 Deletion noticed!\n" 57 | "\n" 58 | "Message by {name}:" 59 | msgstr "" 60 | "🗑 Замечено удаление!\n" 61 | "\n" 62 | "Сообщение от {name}:" 63 | 64 | #: handlers/deleting.py:99 65 | msgid "Voice message can't be sent because of your privacy settings!" 66 | msgstr "" 67 | "Голосовые сообщения не могут быть отправлены из-за ваших настроек " 68 | "приватности!" 69 | 70 | #: handlers/edit.py:53 handlers/edit.py:89 71 | msgid "" 72 | "✏ Editing noticed!\n" 73 | "\n" 74 | "Old message by {name}:{old_msg}\n" 76 | "New message:{new_msg}" 77 | msgstr "" 78 | "✏ Замечено редактирование!\n" 79 | "\n" 80 | "Старое сообщение от {name}:" 81 | "{old_msg}\n" 82 | "Новое сообщение:{new_msg}" 83 | 84 | #: handlers/edit.py:140 85 | msgid "" 86 | "📍 Location change detected!\n" 87 | "\n" 88 | "by {name}\n" 89 | "\n" 90 | "P.S. The bot does not store information about the location and sees it only at " 91 | "the time of update. Therefore, it is impossible to find out what exactly has " 92 | "changed" 93 | msgstr "" 94 | "📍 Обнаружено изменение местоположения!\n" 95 | "\n" 96 | "от {name}\n" 97 | "\n" 98 | "P.S. Бот не хранит информацию о местоположении и видит ее только в момент " 99 | "обновления. Поэтому узнать, что именно изменилось, невозможно" 100 | 101 | #: handlers/receive.py:32 102 | msgid "" 103 | "An error has occurred! Please add the bot to your profile again!\n" 104 | "\n" 105 | "via @{bot_username}" 106 | msgstr "" 107 | "Произошла ошибка! Пожалуйста, добавьте бота в свой профиль еще раз!\n" 108 | "\n" 109 | "via @{bot_username}" 110 | 111 | #~ msgid "" 112 | #~ "Hello, {name}!\n" 113 | #~ "I will help you to log editing and deleting messages done by another " 114 | #~ "users!\n" 115 | #~ "\n" 116 | #~ "You just need to send me ID of the channel in which you prefer to see the " 117 | #~ "logs. By default I will save them here" 118 | #~ msgstr "" 119 | #~ "Привет, {name}!\n" 120 | #~ "Я помогу тебе отслеживать редактирование и удаление сообщений в личных " 121 | #~ "сообщениях!\n" 122 | #~ "\n" 123 | #~ "Ты можешь отправить мне ID канала в котором хочешь видеть логи. Изначально " 124 | #~ "я отправляю все логи сюда" 125 | 126 | #~ msgid "" 127 | #~ "🗑 Deletion noticed!\n" 128 | #~ "\n" 129 | #~ "Message by {name}" 130 | #~ msgstr "" 131 | #~ "🗑 Замечено удаление!\n" 132 | #~ "\n" 133 | #~ "Сообщение от {name}" 134 | 135 | #~ msgid "" 136 | #~ "✏ Editing noticed!\n" 137 | #~ "\n" 138 | #~ "Old message by {name}:{old_msg}\n" 140 | #~ "New message:{new_msg}" 141 | #~ msgstr "" 142 | #~ "✏ Замечено редактирование!\n" 143 | #~ "\n" 144 | #~ "Старое сообщение от {name}:" 145 | #~ "{old_msg}\n" 146 | #~ "Новое сообщение:{new_msg}" 147 | -------------------------------------------------------------------------------- /locales/uk/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # Ukrainian translations for PROJECT. 2 | # Copyright (C) 2024 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR, 2024. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2025-01-12 19:15+0100\n" 11 | "PO-Revision-Date: 2025-01-12 19:17+0100\n" 12 | "Last-Translator: \n" 13 | "Language-Team: uk \n" 14 | "Language: uk\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " 19 | "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 20 | "Generated-By: Babel 2.13.1\n" 21 | "X-Generator: Poedit 3.5\n" 22 | 23 | #: main.py:51 24 | msgid "⚠ All your data was cleared because of disconnecting" 25 | msgstr "⚠ Всі ваші дані було видалено через відключення" 26 | 27 | #: main.py:67 main.py:80 28 | msgid "" 29 | "Hello, {name}!\n" 30 | "I will help you to log editing and deleting messages done by another " 31 | "users!\n" 32 | "\n" 33 | "Source " 34 | "code" 35 | msgstr "" 36 | "Привіт, {name}!\n" 37 | "Я допоможу вам реєструвати редагування та видалення повідомлень, " 38 | "зроблених іншими користувачами!\n" 39 | "\n" 40 | "Source " 41 | "code" 42 | 43 | #: handlers/deleting.py:36 44 | msgid "" 45 | "🗑 Deletion noticed!\n" 46 | "\n" 47 | "Message by {name}: {msg}" 49 | msgstr "" 50 | "🗑 Помічено видалення!\n" 51 | "\n" 52 | "Повідомлення від {name}:{msg}" 54 | 55 | #: handlers/deleting.py:72 handlers/deleting.py:130 56 | msgid "" 57 | "🗑 Deletion noticed!\n" 58 | "\n" 59 | "Message by {name}:" 60 | msgstr "" 61 | "🗑 Помічено видалення!\n" 62 | "\n" 63 | "Повідомлення від {name}:" 64 | 65 | #: handlers/deleting.py:99 66 | msgid "Voice message can't be sent because of your privacy settings!" 67 | msgstr "" 68 | "Голосове повідомлення не може бути надіслано через ваші налаштування " 69 | "конфіденційності!" 70 | 71 | #: handlers/edit.py:53 handlers/edit.py:89 72 | msgid "" 73 | "✏ Editing noticed!\n" 74 | "\n" 75 | "Old message by {name}:{old_msg}\n" 77 | "New message:{new_msg}" 78 | msgstr "" 79 | "✏ Помічено редагування!\n" 80 | "\n" 81 | "Старе повідомлення від {name}:" 82 | "{old_msg}\n" 83 | "Нове повідомлення:{new_msg}" 84 | 85 | #: handlers/edit.py:140 86 | msgid "" 87 | "📍 Location change detected!\n" 88 | "\n" 89 | "by {name}\n" 90 | "\n" 91 | "P.S. The bot does not store information about the location and sees it " 92 | "only at the time of update. Therefore, it is impossible to find out what " 93 | "exactly has changed" 94 | msgstr "" 95 | "📍 Помічено зміну геолокації!\n" 96 | "\n" 97 | "від {name}\n" 98 | "\n" 99 | "P.S. Бот не зберігає інформацію про місцезнаходження і бачить її тільки в " 100 | "момент оновлення. Тому дізнатися, що саме змінилося, неможливо" 101 | 102 | #: handlers/receive.py:32 103 | msgid "" 104 | "An error has occurred! Please add the bot to your profile again!\n" 105 | "\n" 106 | "via @{bot_username}" 107 | msgstr "" 108 | "Виникла помилка! Будь ласка, додайте бота до свого профілю ще раз!\n" 109 | "\n" 110 | "via @{bot_username}" 111 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from aiogram import Bot, Dispatcher 5 | from aiogram.enums import ParseMode, ContentType 6 | from aiogram.types import BusinessConnection, Message, BotCommand 7 | from aiogram.utils.i18n import gettext as _, I18n, SimpleI18nMiddleware 8 | 9 | from filters.ContentTypeFilter import ContentTypeFilter 10 | from handlers.deleting import message_delete_route 11 | from handlers.edit import message_edit_route 12 | from handlers.receive import message_receive_route 13 | from middlewares.user_check import UsersMiddleware 14 | from repo import Repo 15 | from repo.modules.users import UserData 16 | from utils.config import config 17 | from utils.encryptor import get_text_hash 18 | 19 | logging.basicConfig(level=logging.INFO, 20 | format="[%(asctime)s][%(levelname)s][%(funcName)s][%(module)s][%(lineno)d] - %(message)s") 21 | 22 | dp = Dispatcher() 23 | i18n = I18n(path="locales", default_locale="en", domain="messages") 24 | 25 | dp.update.middleware(SimpleI18nMiddleware(i18n=i18n)) 26 | dp.update.middleware(UsersMiddleware()) 27 | 28 | dp.include_routers(message_receive_route, message_edit_route, message_delete_route) 29 | 30 | repo = Repo( 31 | username=config.DATABASE.username, 32 | password=config.DATABASE.password, 33 | ip=config.DATABASE.ip, 34 | port=config.DATABASE.port, 35 | db=config.DATABASE.db, 36 | ) 37 | 38 | dp["repo"] = repo 39 | 40 | 41 | @dp.business_connection() 42 | async def connection_handler(bc: BusinessConnection, bot: Bot) -> None: 43 | connection_id = get_text_hash(bc.id) 44 | 45 | user = repo.users.get(bc.user.id) 46 | 47 | if not bc.is_enabled: 48 | repo.messages.delete_by_cid(connection_id=connection_id) 49 | repo.users.delete(user=user) 50 | 51 | text = _("⚠ All your data was cleared because of disconnecting") 52 | 53 | await bot.send_message(chat_id=bc.user.id, text=text) 54 | return 55 | 56 | if user is not None: 57 | if user.connection_id != connection_id: 58 | user.connection_id = connection_id 59 | repo.save() 60 | 61 | repo.messages.delete_by_cid(connection_id=connection_id) 62 | s = repo.users.add(UserData(id=bc.user.id, 63 | connection_id=get_text_hash(bc.id), 64 | language=bc.user.language_code)) 65 | 66 | if s: 67 | text = _("Hello, {name}!" 68 | "\nI will help you to log editing and deleting messages done by another users!" 69 | "\n\nSource code").format( 70 | name=bc.user.full_name, 71 | user_id=bc.user.id) 72 | 73 | await bot.send_photo(chat_id=bc.user.id, 74 | caption=text, 75 | photo="https://omeba-work.com/screenshoot/fc36db79d525fb040fbad9c8039c8dca.jpg") 76 | 77 | 78 | @dp.message(ContentTypeFilter(ContentType.TEXT,)) 79 | async def get_chat_id(msg: Message, bot: Bot) -> None: 80 | text = _("Hello, {name}!" 81 | "\nI will help you to log editing and deleting messages done by another users!" 82 | "\n\nSource code").format( 83 | name=msg.from_user.full_name, 84 | user_id=msg.from_user.id) 85 | 86 | await bot.send_photo(chat_id=msg.from_user.id, 87 | caption=text, 88 | photo="https://omeba-work.com/screenshoot/fc36db79d525fb040fbad9c8039c8dca.jpg") 89 | 90 | 91 | async def main() -> None: 92 | bot = Bot( 93 | token=config.BOT.token, 94 | parse_mode=ParseMode.HTML, 95 | disable_web_page_preview=True, 96 | ) 97 | 98 | # English commands translation 99 | await bot.set_my_commands( 100 | commands=[ 101 | BotCommand(command="start", description="Short information about bot capabilities"), 102 | ], 103 | language_code="en" 104 | ) 105 | # Russian commands translation 106 | await bot.set_my_commands( 107 | commands=[ 108 | BotCommand(command="start", description="Краткая информация о возможностях бота") 109 | ], 110 | language_code="ru" 111 | ) 112 | # Ukrainian commands translation 113 | await bot.set_my_commands( 114 | commands=[ 115 | BotCommand(command="start", description="Швидка довідка о можливостях бота") 116 | ], 117 | language_code="uk" 118 | ) 119 | 120 | await dp.start_polling(bot) 121 | 122 | 123 | if __name__ == "__main__": 124 | try: 125 | asyncio.run(main()) 126 | except KeyboardInterrupt: 127 | pass 128 | -------------------------------------------------------------------------------- /middlewares/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyperAI/tg-message-logger-bot/263ce2b20c79251078622192ad896f204b576fcb/middlewares/__init__.py -------------------------------------------------------------------------------- /middlewares/user_check.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Callable, Dict, Any, Awaitable 3 | 4 | from aiogram import BaseMiddleware 5 | from aiogram.types import TelegramObject, User, Message 6 | 7 | from repo import Repo 8 | from repo.modules.users import UserData 9 | from utils.encryptor import get_text_hash 10 | 11 | 12 | class UsersMiddleware(BaseMiddleware): 13 | async def __call__(self, handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], event: TelegramObject, 14 | data: Dict[str, Any]) -> Any: 15 | repo: Repo = data["repo"] 16 | user: User = data.get("event_from_user", None) 17 | 18 | if user is None: 19 | return await handler(event, data) 20 | 21 | user_data = repo.users.get(user.id) 22 | 23 | if user_data is None or data.get('business_connection_id') is None: 24 | return await handler(event, data) 25 | 26 | connection_id = get_text_hash(data['business_connection_id']) 27 | if user_data.connection_id != connection_id: 28 | user_data.connection_id = connection_id 29 | repo.save() 30 | 31 | return await handler(event, data) 32 | -------------------------------------------------------------------------------- /repo/__init__.py: -------------------------------------------------------------------------------- 1 | from .repo import Repo -------------------------------------------------------------------------------- /repo/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyperAI/tg-message-logger-bot/263ce2b20c79251078622192ad896f204b576fcb/repo/modules/__init__.py -------------------------------------------------------------------------------- /repo/modules/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import typing 3 | 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy.orm import Session 6 | 7 | Base = declarative_base() 8 | 9 | 10 | class BaseRepo(abc.ABC): 11 | def __init__(self, s) -> None: 12 | self._s: Session = s 13 | 14 | @abc.abstractmethod 15 | def get(self, **kwargs) -> typing.Optional[Base]: 16 | pass 17 | 18 | @abc.abstractmethod 19 | def add(self, obj: Base) -> bool: 20 | pass 21 | -------------------------------------------------------------------------------- /repo/modules/messages.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import typing 3 | 4 | from sqlalchemy import Column, Integer, String, BigInteger, Boolean, Text 5 | 6 | from repo.modules.base import Base, BaseRepo 7 | 8 | 9 | class MessageData(Base): 10 | __tablename__ = 'messages' 11 | 12 | id = Column(Integer, primary_key=True, autoincrement=True) 13 | 14 | connection_id = Column(String(4112)) 15 | message_id = Column(BigInteger) 16 | message = Column(Text, default=None) 17 | 18 | is_sticker = Column(Boolean, default=False) 19 | is_media = Column(Boolean, default=False) 20 | 21 | sticker = Column(Text, default=None) 22 | 23 | media = Column(Text, default=None) 24 | media_type = Column(Text, default=None) 25 | 26 | 27 | class MessagesRepo(BaseRepo): 28 | def __init__(self, s): 29 | super().__init__(s) 30 | 31 | def add(self, message: MessageData) -> bool: 32 | try: 33 | self._s.add(message) 34 | self._s.commit() 35 | return True 36 | except Exception as e: 37 | logging.error(e) 38 | return False 39 | 40 | def get(self, message_id: int, connection_id: str) -> typing.Optional[MessageData]: 41 | return self._s.query(MessageData).filter_by(message_id=message_id, connection_id=connection_id).first() 42 | 43 | def delete(self, message: MessageData) -> bool: 44 | self._s.delete(message) 45 | self._s.commit() 46 | 47 | def delete_by_cid(self, connection_id) -> bool: 48 | self._s.query(MessageData).filter(MessageData.connection_id == connection_id).delete() 49 | self._s.commit() 50 | -------------------------------------------------------------------------------- /repo/modules/users.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import typing 3 | 4 | from sqlalchemy import Column, String, BigInteger 5 | 6 | from repo.modules.base import Base, BaseRepo 7 | 8 | 9 | class UserData(Base): 10 | __tablename__ = 'users' 11 | 12 | id = Column(BigInteger, primary_key=True) 13 | connection_id = Column(String(1028), unique=True) 14 | channel_id = Column(BigInteger) 15 | language = Column(String(4), default="en") 16 | 17 | 18 | class UsersRepo(BaseRepo): 19 | def __init__(self, s) -> None: 20 | super().__init__(s) 21 | 22 | def add(self, user: UserData) -> bool: 23 | s = self.get(user.id) 24 | if s is not None: 25 | return False 26 | 27 | try: 28 | self._s.add(user) 29 | self._s.commit() 30 | return True 31 | except Exception as e: 32 | logging.error(e) 33 | return False 34 | 35 | def get(self, id: int) -> typing.Optional[UserData]: 36 | return self._s.query(UserData).get({"id": id}) 37 | 38 | def get_by_connection(self, connection_id: str) -> typing.Optional[UserData]: 39 | return self._s.query(UserData).filter_by(connection_id=connection_id).first() 40 | 41 | def update_connection_id(self, user: UserData, connection_id: str) -> bool: 42 | user.connection_id = connection_id 43 | 44 | self._s.commit() 45 | 46 | def delete(self, user: UserData) -> bool: 47 | self._s.delete(user) 48 | self._s.commit() 49 | -------------------------------------------------------------------------------- /repo/repo.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | from repo.modules.base import Base 5 | from repo.modules.messages import MessagesRepo 6 | from repo.modules.users import UsersRepo 7 | 8 | 9 | class Repo: 10 | def __init__(self, username: str, password: str, ip: str, port: int, db: str) -> None: 11 | self.engine = create_engine(f'mysql+pymysql://{username}:{password}@{ip}:{port}/{db}') 12 | 13 | self._Session = sessionmaker(bind=self.engine) 14 | self.session = self._Session() 15 | 16 | Base.metadata.create_all(self.engine) 17 | 18 | def save(self) -> None: 19 | self.session.commit() 20 | 21 | @property 22 | def users(self) -> UsersRepo: 23 | return UsersRepo(s=self.session) 24 | 25 | @property 26 | def messages(self) -> MessagesRepo: 27 | return MessagesRepo(s=self.session) 28 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyperAI/tg-message-logger-bot/263ce2b20c79251078622192ad896f204b576fcb/requirements.txt -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyperAI/tg-message-logger-bot/263ce2b20c79251078622192ad896f204b576fcb/utils/__init__.py -------------------------------------------------------------------------------- /utils/config.py: -------------------------------------------------------------------------------- 1 | from configparser import ConfigParser 2 | from typing import Dict, Any 3 | 4 | from pydantic import BaseModel, SecretStr 5 | 6 | 7 | def parse_config_file(config_file: str) -> Dict[str, Dict[str, Any]]: 8 | config = ConfigParser() 9 | config.read(config_file) 10 | 11 | config_data = {} 12 | 13 | for section in config.sections(): 14 | main_section = section.split('.')[0] 15 | 16 | if section != main_section: 17 | secondary_section = section.split('.')[1] 18 | if main_section in config_data.keys(): 19 | config_data[main_section][secondary_section] = dict(config.items(section)) 20 | else: 21 | config_data[main_section] = {} 22 | config_data[main_section][secondary_section] = dict(config.items(section)) 23 | else: 24 | config_data[section] = dict(config.items(section)) 25 | 26 | return config_data 27 | 28 | 29 | class BotConfig(BaseModel): 30 | token: str 31 | 32 | 33 | class DatabaseConfig(BaseModel): 34 | username: str 35 | password: str 36 | ip: str 37 | port: int 38 | db: str 39 | 40 | 41 | class Config(BaseModel): 42 | DATABASE: DatabaseConfig 43 | BOT: BotConfig 44 | 45 | 46 | config = Config(**parse_config_file("config.ini")) 47 | -------------------------------------------------------------------------------- /utils/encryptor.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | 4 | from cryptography.fernet import Fernet 5 | 6 | 7 | class TextEncryptor: 8 | def __init__(self, key): 9 | key_bytes = str(key).encode() 10 | key_bytes += b'\x00' * (32 - len(key_bytes)) 11 | 12 | self.cipher_suite = Fernet(base64.urlsafe_b64encode(key_bytes)) 13 | 14 | def encrypt(self, plaintext): 15 | plaintext_bytes = plaintext.encode('utf-8') 16 | encrypted_bytes = self.cipher_suite.encrypt(plaintext_bytes) 17 | return encrypted_bytes.decode('utf-8') 18 | 19 | def decrypt(self, ciphertext): 20 | ciphertext_bytes = ciphertext.encode('utf-8') 21 | decrypted_bytes = self.cipher_suite.decrypt(ciphertext_bytes) 22 | return decrypted_bytes.decode('utf-8') 23 | 24 | 25 | def get_text_hash(text: str) -> str: 26 | hash = hashlib.sha256() 27 | hash.update(text.encode()) 28 | return hash.hexdigest() 29 | --------------------------------------------------------------------------------