diff --git a/.flake8 b/.flake8 deleted file mode 100644 index f4e781d..0000000 --- a/.flake8 +++ /dev/null @@ -1,7 +0,0 @@ -[flake8] -ignore = E501, E265, F811, B010, PT001, VNE003, DJ01, PIE783, PIE785, SIM113, SIM102, FS003, W504, PIE801 -max-line-length = 160 -exclude = - .git, - venv, - __pycache__ diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index bc3cd63..0000000 --- a/.isort.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[settings] -line_length = 160 -skip=migrations/*.py, node_modules, venv -known_standard_library=typing -multi_line_output=4 diff --git a/Makefile b/Makefile index ada31f2..54f0277 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,10 @@ install-deps: deps pip-sync requirements.txt deps: - pip-compile requirements.in + pip-compile --resolver=backtracking --output-file=requirements.txt pyproject.toml dev-deps: deps - pip-compile dev-requirements.in + pip-compile --resolver=backtracking --extra=dev --output-file=dev-requirements.txt pyproject.toml lint: flake8 *.py diff --git a/bot.py b/bot.py index 7f5b3c3..d18c9cf 100644 --- a/bot.py +++ b/bot.py @@ -1,28 +1,17 @@ -from typing import Optional - import os from telegram import Message, Update -from telegram.ext import CallbackContext, Dispatcher, MessageHandler, Updater -from telegram.ext.filters import BaseFilter, Filters +from telegram.ext import Application, ContextTypes, MessageHandler +from telegram.ext.filters import TEXT, BaseFilter -import rekognition import text from filters import ContainsLink, ContainsTelegramContact, ContainsThreeOrMoreEmojies, IsMedia, IsMessageOnBehalfOfChat, with_default_filters from helpers import DB_ENABLED, enable_logging, in_production, init_sentry -def get_profile_picture(message: Message) -> Optional[str]: - photos = message.from_user.get_profile_photos() - - if photos is not None and photos.total_count > 0: - profile_picture = photos.photos[0][0].get_file() - return profile_picture.file_path - - -def log_message(message: Message, action: Optional[str] = ''): +async def log_message(message: Message | None, action: str | None = ''): """Create a log entry for telegram message""" - if message is None or not DB_ENABLED(): + if message is None or not DB_ENABLED() or message.from_user is None: return from models import LogEntry @@ -32,24 +21,19 @@ def log_message(message: Message, action: Optional[str] = ''): message_id=message.message_id, text=message.text or '', meta={ - 'tags': [ - *rekognition.get_labels(image_url=get_profile_picture(message)), - *text.Labels(message.text)(), - ], + 'tags': text.Labels(message.text)(), }, raw=message.to_dict(), action=action, ) -def delete(update: Update, context: CallbackContext): +async def delete(update: Update, context: ContextTypes.DEFAULT_TYPE): message = update.message or update.edited_message - log_message(message, action='delete') - message.bot.delete_message( - message_id=message.message_id, - chat_id=message.chat_id, - ) + if message is not None: + await log_message(message, action='delete') + await message.delete() def delete_messages_that_match(*filters: BaseFilter) -> MessageHandler: @@ -59,6 +43,7 @@ def delete_messages_that_match(*filters: BaseFilter) -> MessageHandler: if __name__ == '__main__': from dotenv import load_dotenv + load_dotenv() bot_token = os.getenv('BOT_TOKEN') @@ -66,31 +51,31 @@ def delete_messages_that_match(*filters: BaseFilter) -> MessageHandler: raise RuntimeError('Please set BOT_TOKEN environment variable') app_name = os.getenv('BOT_NAME') - bot = Updater(token=bot_token) - dispatcher: Dispatcher = bot.dispatcher # type: ignore + bot = Application.builder().token(bot_token).build() - dispatcher.add_handler(delete_messages_that_match(ContainsTelegramContact())) - dispatcher.add_handler(delete_messages_that_match(ContainsLink())) - dispatcher.add_handler(delete_messages_that_match(IsMessageOnBehalfOfChat())) - dispatcher.add_handler(delete_messages_that_match(ContainsThreeOrMoreEmojies())) - dispatcher.add_handler(delete_messages_that_match(IsMedia())) + bot.add_handler(delete_messages_that_match(ContainsTelegramContact())) + bot.add_handler(delete_messages_that_match(ContainsLink())) + bot.add_handler(delete_messages_that_match(IsMessageOnBehalfOfChat())) + bot.add_handler(delete_messages_that_match(ContainsThreeOrMoreEmojies())) + bot.add_handler(delete_messages_that_match(IsMedia())) if DB_ENABLED(): # log all not handled messages from models import create_tables + create_tables() # type: ignore - dispatcher.add_handler( - MessageHandler(filters=Filters.text, callback=lambda update, context: log_message(update.message or update.edited_message)), + bot.add_handler( + MessageHandler(filters=TEXT, + callback=lambda update, context: log_message(update.message or update.edited_message)), ) if in_production(): init_sentry() - bot.start_webhook( + bot.run_webhook( listen='0.0.0.0', port=8000, url_path=bot_token, webhook_url=f'https://{app_name}.tough-dev.school/' + bot_token, ) - bot.idle() else: # bot is running on the dev machine enable_logging() - bot.start_polling() + bot.run_polling() diff --git a/dev-requirements.in b/dev-requirements.in deleted file mode 100644 index 1ed10f8..0000000 --- a/dev-requirements.in +++ /dev/null @@ -1,33 +0,0 @@ --c requirements.txt -ipython -watchdog[watchmedo] - -mypy -types-emoji -types-boto3 - - -isort -autopep8<1.6.0 -flake8-bugbear -flake8-cognitive-complexity -flake8-commas -flake8-django -flake8-eradicate -flake8-isort>=4.0.0 -flake8-fixme -flake8-mock -flake8-multiline-containers -flake8-mutable -flake8-pep3101 -flake8-pie -flake8-print -flake8-printf-formatting -flake8-quotes -flake8-simplify -flake8-todo -flake8-use-fstring -flake8-variables-names -flake8-walrus - - diff --git a/dev-requirements.txt b/dev-requirements.txt index 9add27b..b7c5d86 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,36 +2,50 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile dev-requirements.in +# pip-compile --extra=dev --output-file=dev-requirements.txt --resolver=backtracking pyproject.toml # -appnope==0.1.2 +anyio==3.7.1 + # via httpcore +appnope==0.1.3 # via ipython astor==0.8.1 # via flake8-simplify -attrs==21.2.0 +asttokens==2.2.1 + # via stack-data +attrs==23.1.0 # via # flake8-bugbear # flake8-eradicate # flake8-multiline-containers autopep8==1.5.7 - # via -r dev-requirements.in + # via channel-discussion-antispam-bot (pyproject.toml) backcall==0.2.0 # via ipython -boto3-stubs==1.20.28 +boto3-stubs==1.27.1 # via types-boto3 -botocore-stubs==1.23.28 +botocore-stubs==1.29.165 # via boto3-stubs -cognitive-complexity==1.2.0 +certifi==2023.5.7 + # via + # httpcore + # httpx + # sentry-sdk +cognitive-complexity==1.3.0 # via flake8-cognitive-complexity -decorator==5.0.9 +decorator==5.1.1 # via ipython -eradicate==2.0.0 +emoji==1.7.0 + # via channel-discussion-antispam-bot (pyproject.toml) +eradicate==2.3.0 # via flake8-eradicate -flake8==3.9.2 +exceptiongroup==1.1.2 + # via anyio +executing==1.2.0 + # via stack-data +flake8==6.0.0 # via # flake8-bugbear # flake8-commas - # flake8-django # flake8-eradicate # flake8-isort # flake8-multiline-containers @@ -39,115 +53,149 @@ flake8==3.9.2 # flake8-pep3101 # flake8-print # flake8-printf-formatting + # flake8-pyproject # flake8-quotes # flake8-simplify # flake8-use-fstring # flake8-walrus -flake8-bugbear==21.4.3 - # via -r dev-requirements.in +flake8-bugbear==23.6.5 + # via channel-discussion-antispam-bot (pyproject.toml) flake8-cognitive-complexity==0.1.0 - # via -r dev-requirements.in -flake8-commas==2.0.0 - # via -r dev-requirements.in -flake8-django==1.1.2 - # via -r dev-requirements.in -flake8-eradicate==1.0.0 - # via -r dev-requirements.in + # via channel-discussion-antispam-bot (pyproject.toml) +flake8-commas==2.1.0 + # via channel-discussion-antispam-bot (pyproject.toml) +flake8-eradicate==1.5.0 + # via channel-discussion-antispam-bot (pyproject.toml) flake8-fixme==1.1.1 - # via -r dev-requirements.in -flake8-isort==4.0.0 - # via -r dev-requirements.in -flake8-mock==0.3 - # via -r dev-requirements.in -flake8-multiline-containers==0.0.18 - # via -r dev-requirements.in + # via channel-discussion-antispam-bot (pyproject.toml) +flake8-isort==6.0.0 + # via channel-discussion-antispam-bot (pyproject.toml) +flake8-multiline-containers==0.0.19 + # via channel-discussion-antispam-bot (pyproject.toml) flake8-mutable==1.2.0 - # via -r dev-requirements.in -flake8-pep3101==1.3.0 - # via -r dev-requirements.in -flake8-pie==0.13.0 - # via -r dev-requirements.in -flake8-print==4.0.0 - # via -r dev-requirements.in + # via channel-discussion-antispam-bot (pyproject.toml) +flake8-pep3101==2.0.0 + # via channel-discussion-antispam-bot (pyproject.toml) +flake8-pie==0.16.0 + # via channel-discussion-antispam-bot (pyproject.toml) +flake8-print==5.0.0 + # via channel-discussion-antispam-bot (pyproject.toml) flake8-printf-formatting==1.1.2 - # via -r dev-requirements.in -flake8-quotes==3.2.0 - # via -r dev-requirements.in -flake8-simplify==0.14.1 - # via -r dev-requirements.in + # via channel-discussion-antispam-bot (pyproject.toml) +flake8-pyproject==1.2.3 + # via channel-discussion-antispam-bot (pyproject.toml) +flake8-quotes==3.3.2 + # via channel-discussion-antispam-bot (pyproject.toml) +flake8-simplify==0.20.0 + # via channel-discussion-antispam-bot (pyproject.toml) flake8-todo==0.7 - # via -r dev-requirements.in -flake8-use-fstring==1.1 - # via -r dev-requirements.in -flake8-variables-names==0.0.4 - # via -r dev-requirements.in -flake8-walrus==1.1.0 - # via -r dev-requirements.in -ipython==7.24.1 - # via -r dev-requirements.in -ipython-genutils==0.2.0 - # via traitlets -isort==5.8.0 + # via channel-discussion-antispam-bot (pyproject.toml) +flake8-use-fstring==1.4 + # via channel-discussion-antispam-bot (pyproject.toml) +flake8-variables-names==0.0.6 + # via channel-discussion-antispam-bot (pyproject.toml) +flake8-walrus==1.2.0 + # via channel-discussion-antispam-bot (pyproject.toml) +h11==0.14.0 + # via httpcore +httpcore==0.17.3 + # via httpx +httpx==0.24.1 + # via python-telegram-bot +idna==3.4 + # via + # anyio + # httpx +ipython==8.14.0 + # via channel-discussion-antispam-bot (pyproject.toml) +isort==5.12.0 # via - # -r dev-requirements.in + # channel-discussion-antispam-bot (pyproject.toml) # flake8-isort -jedi==0.18.0 +jedi==0.18.2 # via ipython -matplotlib-inline==0.1.2 +matplotlib-inline==0.1.6 # via ipython -mccabe==0.6.1 +mccabe==0.7.0 # via flake8 -mypy==0.930 - # via -r dev-requirements.in -mypy-extensions==0.4.3 +mypy==1.4.1 + # via channel-discussion-antispam-bot (pyproject.toml) +mypy-extensions==1.0.0 # via mypy -parso==0.8.2 +parso==0.8.3 # via jedi +peewee==3.16.2 + # via channel-discussion-antispam-bot (pyproject.toml) pexpect==4.8.0 # via ipython pickleshare==0.7.5 # via ipython -prompt-toolkit==3.0.18 +prompt-toolkit==3.0.39 # via ipython +psycopg2-binary==2.9.6 + # via channel-discussion-antispam-bot (pyproject.toml) ptyprocess==0.7.0 # via pexpect -pycodestyle==2.7.0 +pure-eval==0.2.2 + # via stack-data +pycodestyle==2.10.0 # via # autopep8 # flake8 # flake8-print # flake8-todo -pyflakes==2.3.1 +pyflakes==3.0.1 # via flake8 -pygments==2.9.0 +pygments==2.15.1 # via ipython +python-dotenv==1.0.0 + # via channel-discussion-antispam-bot (pyproject.toml) +python-telegram-bot[webhooks]==20.3 + # via channel-discussion-antispam-bot (pyproject.toml) pyyaml==6.0 # via watchdog +sentry-sdk==1.27.0 + # via channel-discussion-antispam-bot (pyproject.toml) six==1.16.0 + # via asttokens +sniffio==1.3.0 # via - # -c requirements.txt - # flake8-print -testfixtures==6.17.1 - # via flake8-isort + # anyio + # httpcore + # httpx +stack-data==0.6.2 + # via ipython toml==0.10.2 # via autopep8 -tomli==2.0.0 - # via mypy -traitlets==5.0.5 +tomli==2.0.1 + # via + # flake8-pyproject + # mypy +tornado==6.3.2 + # via python-telegram-bot +traitlets==5.9.0 # via # ipython # matplotlib-inline -types-boto3==1.0.1 - # via -r dev-requirements.in -types-emoji==1.2.6 - # via -r dev-requirements.in -typing-extensions==3.10.0.0 +types-awscrt==0.16.21 + # via + # botocore-stubs + # types-s3transfer +types-boto3==1.0.2 + # via channel-discussion-antispam-bot (pyproject.toml) +types-emoji==2.1.0.3 + # via channel-discussion-antispam-bot (pyproject.toml) +types-s3transfer==0.6.1 + # via boto3-stubs +typing-extensions==4.7.1 # via # flake8-pie # mypy -watchdog[watchmedo]==2.1.6 - # via -r dev-requirements.in -wcwidth==0.2.5 +urllib3==1.26.16 + # via sentry-sdk +watchdog[watchmedo]==3.0.0 + # via channel-discussion-antispam-bot (pyproject.toml) +wcwidth==0.2.6 # via prompt-toolkit # The following packages are considered to be unsafe in a requirements file: diff --git a/filters.py b/filters.py index 47dbe87..957ec51 100644 --- a/filters.py +++ b/filters.py @@ -1,7 +1,7 @@ import operator from functools import reduce from telegram import Message -from telegram.ext import BaseFilter, MessageFilter +from telegram.ext.filters import BaseFilter, MessageFilter import text from helpers import DB_ENABLED @@ -11,7 +11,7 @@ class HasNoValidPreviousMessages(MessageFilter): MIN_PREVIOUS_MESSAGES_COUNT = 3 def filter(self, message: Message) -> bool: - if not DB_ENABLED(): + if not DB_ENABLED() or message.from_user is None: return True return self.has_no_valid_previous_messages(user_id=message.from_user.id, chat_id=message.chat_id) @@ -50,7 +50,7 @@ def filter(self, message: Message) -> bool: class ContainsTelegramContact(MessageFilter): def filter(self, message: Message) -> bool: if message.text is None: - return False # type: ignore + return False return ' @' in message.text or message.text.startswith('@') @@ -59,7 +59,7 @@ class ContainsLink(MessageFilter): def filter(self, message: Message) -> bool: if message.text is None: - return False # type: ignore + return False entities_types = set([entity.type for entity in message.entities]) return len(entities_types.intersection({'url', 'text_link'})) != 0 diff --git a/helpers.py b/helpers.py index 088af64..4ab6554 100644 --- a/helpers.py +++ b/helpers.py @@ -1,6 +1,7 @@ import logging import os import sentry_sdk +from sentry_sdk.integrations.asyncio import AsyncioIntegration def DB_ENABLED() -> bool: @@ -22,7 +23,12 @@ def init_sentry() -> None: sentry_dsn = os.getenv('SENTRY_DSN', None) if sentry_dsn: - sentry_sdk.init(sentry_dsn) + sentry_sdk.init( + dsn=sentry_dsn, + integrations=[ + AsyncioIntegration(), + ], + ) __all__ = [ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e01c8ac --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,77 @@ +[project] +name = "channel-discussion-antispam-bot" +version = "0.0.1" +dependencies = [ + "python-telegram-bot[webhooks]>20", + "python-dotenv", + "sentry-sdk", + "peewee", + "psycopg2-binary", + "emoji", +] + + +[project.optional-dependencies] +dev = [ + "ipython", + "watchdog[watchmedo]", + "mypy", + "types-emoji", + "types-boto3", + "isort", + "autopep8<1.6.0", + "flake8-bugbear", + "flake8-cognitive-complexity", + "flake8-commas", + "flake8-eradicate", + "flake8-isort>=4.0.0", + "flake8-fixme", + "flake8-multiline-containers", + "flake8-mutable", + "flake8-pep3101", + "flake8-pie", + "flake8-print", + "flake8-printf-formatting", + "flake8-quotes", + "flake8-simplify", + "flake8-todo", + "flake8-use-fstring", + "flake8-variables-names", + "flake8-walrus", + "flake8-pyproject", +] + + + +[tool.setuptools] +packages = [] + + +[tool.flake8] +max-line-length = 160 +ignore = [ + "E501", + "E265", + "F811", + "B010", + "PT001", + "VNE003", + "PIE783", + "PIE785", + "SIM113", + "SIM102", + "FS003", + "W504", + "PIE801", +] +exclude = [ + "venv", + ".git", + "__pycache__", +] + + +[tool.isort] +line_length = 160 +known_standard_library = ["typing"] +multi_line_output = 4 diff --git a/rekognition.py b/rekognition.py deleted file mode 100644 index 1ddd424..0000000 --- a/rekognition.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import List, Optional - -import boto3 -import httpx -import os -from dotenv import load_dotenv - -load_dotenv() - -client = boto3.client( - 'rekognition', - aws_access_key_id=os.getenv('AWS_ACCESS_KEY_ID'), - aws_secret_access_key=os.getenv('AWS_SECRET_ACCESS_KEY'), - region_name='us-east-1', -) - - -def get_labels(image_url: Optional[str]) -> List[str]: - if image_url is None: - return list() - - response = client.detect_labels( - Image={ - 'Bytes': download(image_url), - }, - MaxLabels=5, - ) - - return [label['Name'] for label in response['Labels'] if label['Confidence'] > 80] - - -def download(url: str) -> bytes: - response = httpx.get(url) - - return response.content diff --git a/requirements.in b/requirements.in deleted file mode 100644 index a029df8..0000000 --- a/requirements.in +++ /dev/null @@ -1,8 +0,0 @@ -python-telegram-bot<20 -python-dotenv -sentry-sdk -peewee -psycopg2-binary -boto3 -httpx -emoji diff --git a/requirements.txt b/requirements.txt index 0bf935d..0bf56d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,77 +2,45 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile requirements.in +# pip-compile --output-file=requirements.txt --resolver=backtracking pyproject.toml # -anyio==3.3.4 +anyio==3.7.1 # via httpcore -apscheduler==3.6.3 - # via python-telegram-bot -boto3==1.20.5 - # via -r requirements.in -botocore==1.23.5 - # via - # boto3 - # s3transfer -cachetools==4.2.2 - # via python-telegram-bot -certifi==2021.5.30 +certifi==2023.5.7 # via # httpcore # httpx - # python-telegram-bot # sentry-sdk -emoji==1.6.1 - # via -r requirements.in +emoji==1.7.0 + # via channel-discussion-antispam-bot (pyproject.toml) +exceptiongroup==1.1.2 + # via anyio h11==0.14.0 # via httpcore -httpcore==0.17.2 +httpcore==0.17.3 # via httpx httpx==0.24.1 - # via -r requirements.in -idna==3.2 + # via python-telegram-bot +idna==3.4 # via # anyio # httpx -jmespath==0.10.0 - # via - # boto3 - # botocore -peewee==3.14.8 - # via -r requirements.in -psycopg2-binary==2.9.2 - # via -r requirements.in -python-dateutil==2.8.2 - # via botocore -python-dotenv==0.17.1 - # via -r requirements.in -python-telegram-bot==13.15 - # via -r requirements.in -pytz==2023.3 - # via - # apscheduler - # python-telegram-bot -s3transfer==0.5.0 - # via boto3 -sentry-sdk==1.1.0 - # via -r requirements.in -six==1.16.0 - # via - # apscheduler - # python-dateutil -sniffio==1.2.0 +peewee==3.16.2 + # via channel-discussion-antispam-bot (pyproject.toml) +psycopg2-binary==2.9.6 + # via channel-discussion-antispam-bot (pyproject.toml) +python-dotenv==1.0.0 + # via channel-discussion-antispam-bot (pyproject.toml) +python-telegram-bot[webhooks]==20.3 + # via channel-discussion-antispam-bot (pyproject.toml) +sentry-sdk==1.27.0 + # via channel-discussion-antispam-bot (pyproject.toml) +sniffio==1.3.0 # via # anyio # httpcore # httpx -tornado==6.1 +tornado==6.3.2 # via python-telegram-bot -tzlocal==5.0.1 - # via apscheduler -urllib3==1.26.5 - # via - # botocore - # sentry-sdk - -# The following packages are considered to be unsafe in a requirements file: -# setuptools +urllib3==1.26.16 + # via sentry-sdk diff --git a/text.py b/text.py index 63867c7..3e058be 100644 --- a/text.py +++ b/text.py @@ -1,20 +1,19 @@ -from typing import List, Optional import emoji class Labels: - def __init__(self, text: Optional[str]) -> None: + def __init__(self, text: str | None) -> None: self.text = text - def __call__(self) -> List[str]: + def __call__(self) -> list[str]: return self.get_emoji_label() - def get_emoji_label(self) -> List[str]: + def get_emoji_label(self) -> list[str]: if self.text is None: return [] - emoji_count = len(emoji.emoji_lis(self.text)) + emoji_count = len(emoji.emoji_list(self.text)) if emoji_count == 0: return []