From 6f8542aa491a964e72d0b40e3bfe30c92e8604a9 Mon Sep 17 00:00:00 2001 From: Adrien Date: Fri, 21 Apr 2023 22:27:05 +0200 Subject: [PATCH] Updating dependencies (#22) * Started to updated dependencies * Pytest, requests * Added pre-commit, ran pre-commit, updated chat_id -> user_chat_id in chatjoinrequests, added constructors to extract chat data and chat request data --- .github/FUNDING.yml | 2 +- .github/workflows/deploy.yml | 58 ++++++++-------- .github/workflows/precommit.yaml | 25 +++++++ .github/workflows/publish.yml | 2 +- .pre-commit-config.yaml | 29 ++++++++ Dockerfile | 12 ---- Procfile | 1 - README.md | 10 +-- app/__init__.py | 12 ++-- app/__main__.py | 22 +++--- app/db.py | 23 +++---- app/handlers.py | 115 ++++++++++++++++--------------- app/types.py | 48 ++++++++++++- app/utils.py | 4 +- requirements.txt | 55 +++++++-------- strings.toml | 2 +- test/test_async.py | 5 +- 17 files changed, 251 insertions(+), 174 deletions(-) create mode 100644 .github/workflows/precommit.yaml create mode 100644 .pre-commit-config.yaml delete mode 100644 Procfile diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 9b5e2d9..5a778ff 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -10,4 +10,4 @@ # issuehunt: # Replace with a single IssueHunt username # otechie: # Replace with a single Otechie username # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -custom: ['https://www.buymeacoffee.com/WhyNotTryCalmer'] \ No newline at end of file +custom: ['https://www.buymeacoffee.com/WhyNotTryCalmer'] diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 94e0417..16db5d5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,29 +1,29 @@ -name: Deployment - -on: - workflow_run: - workflows: ["Publish"] - branches: [master] - types: - - completed - - workflow_dispatch: - -jobs: - deploy_to_production: - name: Connect and deploy - runs-on: ubuntu-latest - steps: - - name: Check out the repo - uses: actions/checkout@v3 - - - name: Connect And Pull - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.PROD_HOST }} - username: root - key: ${{ secrets.PROD_KEY }} - passphrase: ${{ secrets.PROD_PASSWD }} - script: | - cd /opt/app - ./refresh ringo +name: Deployment + +on: + workflow_run: + workflows: ["Publish"] + branches: [master] + types: + - completed + + workflow_dispatch: + +jobs: + deploy_to_production: + name: Connect and deploy + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v3 + + - name: Connect And Pull + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.PROD_HOST }} + username: root + key: ${{ secrets.PROD_KEY }} + passphrase: ${{ secrets.PROD_PASSWD }} + script: | + cd /opt/app + ./refresh ringo diff --git a/.github/workflows/precommit.yaml b/.github/workflows/precommit.yaml new file mode 100644 index 0000000..33a9fd5 --- /dev/null +++ b/.github/workflows/precommit.yaml @@ -0,0 +1,25 @@ +name: Check pre-commit + +on: + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run pre-commit + run: pre-commit run --all-files --show-diff-on-failure --verbose + + - name: Pre-commit failed ! Read this + if: failure() + run: | + echo 'To ensure your code is properly formatted, run: pip install pre-commit; pre-commit install;' + echo 'Fix your current code with: pre-commit run --all-files' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 014739f..54d0f15 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,7 +4,7 @@ on: workflow_run: workflows: ["Test"] branches: [master] - types: + types: - completed workflow_dispatch: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a2bab82 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: mixed-line-ending + args: + - "--fix=lf" + + - repo: https://github.com/myint/autoflake + rev: v2.1.1 + hooks: + - id: autoflake + args: + - "--in-place" + - "--remove-all-unused-imports" + - "--remove-unused-variables" + + - repo: https://github.com/pycqa/isort + rev: "5.12.0" + hooks: + - id: isort + args: ["--profile", "black"] + + - repo: https://github.com/psf/black + rev: "23.3.0" + hooks: + - id: black diff --git a/Dockerfile b/Dockerfile index 5abfa7c..e63b073 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,22 +8,10 @@ COPY ./requirements.txt . ENV PIPENV_VENV_IN_PROJECT=1 RUN pipenv install -# Azure handles TLS -# RUN openssl req -subj \ -# "/C=CH/ST=Bern/L=Bern/O=WhoCares /OU=Again/CN=SoNosey" \ -# -newkey rsa:2048 -sha256 -nodes \ -# -keyout private.key -x509 -days 3650 -out cert.pem - -# runner FROM python:3.10-slim-bullseye WORKDIR /opt/app -# runnable code COPY . . COPY --from=builder /opt/app/.venv/ .venv/ -# Azure handles TLS -# COPY --from=builder /opt/app/cert.pem cert.pem -# COPY --from=builder /opt/app/private.key private.key - CMD . .venv/bin/activate && python -m app diff --git a/Procfile b/Procfile deleted file mode 100644 index 82a4195..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: python -m app \ No newline at end of file diff --git a/README.md b/README.md index bd0137a..eb2bc62 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ ## Overview -This bot provides a simple click-based verification workflow for your Telegram users. It requires that you have enabled the options `Only members can send messages` as well as `Use chat join requests`; only the latter is strictly necessary but goes hand in hand with the former. +This bot provides a simple click-based verification workflow for your Telegram users. It requires that you have enabled the options `Only members can send messages` as well as `Use chat join requests`; only the latter is strictly necessary but goes hand in hand with the former. You can see [the bot](https://t.me/alert_me_and_my_chat_bot) in action [here](https://t.me/PopOS_en). -If you like the bot, feel free to use it in your own chats, fork this repository or even pay a coffee or a beer to the developer. At any rate please mind the LICENSE. +If you like the bot, feel free to use it in your own chats, fork this repository or even pay a coffee or a beer to the developer. At any rate please mind the LICENSE. ## How it works @@ -34,7 +34,7 @@ The bot uses exactly two commands in addition to `/help` (which aliases to `/sta - `chat_id`: the chat_id of the chat where the bot should listen for join requests (you cannot manually set this value) - `chat_url`: the full url (https://t.me/...) of the chat where the bot should listen for join requests (you can and __should__ set this value) - `mode `: see the previous section for explanations - - `helper_chat_id_`: chat_id of the chat to which the both should forward join requests notifications (only used in __manual__ mode) + - `helper_chat_id_`: chat_id of the chat to which the both should forward join requests notifications (only used in __manual__ mode) - `verification_msg`: the message the bot should send to users trying to verify after landing a join requests. Naturall it's not convenient to set a long verification message in this way, so for that key it might be preferable to use a line break, as in: ``` /set mode auto verification_message @@ -48,7 +48,7 @@ The bot uses exactly two commands in addition to `/help` (which aliases to `/sta Question 2: ... Question 3: ... Thanks very much! Admins will now accept or decline your join request. - ``` + ``` - `paused `: _off_ pauses the bot for this chat, _off_ makes it active - `show_join_time `: _off_ does not show the time it took for a user to join the target chat after being sent an invite, _on_ shows it - `changelog < on | off>`: _off_ lets you opt-out of changelog notification messages @@ -81,4 +81,4 @@ If you want to have the bot listen to a custom port, there is the option to add 4. Deploy: `docker run -p : --env-file .env localhost/ringo` -Finally to start the bot run `python -m app` if you want to register a webhook hook and receive updates with the built-in server. Otherwise start with `python -m app --polling`. \ No newline at end of file +Finally to start the bot run `python -m app` if you want to register a webhook hook and receive updates with the built-in server. Otherwise start with `python -m app --polling`. diff --git a/app/__init__.py b/app/__init__.py index 1d89246..4d7a930 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,11 +3,7 @@ from os import environ from sys import stdout -from motor.motor_asyncio import ( - AsyncIOMotorClient, - AsyncIOMotorCollection, - AsyncIOMotorDatabase, -) +from motor.motor_asyncio import AsyncIOMotorClient from telegram.warnings import PTBUserWarning from toml import loads @@ -23,9 +19,9 @@ """ Database """ client = AsyncIOMotorClient(environ["MONGO_CONN_STRING"]) -db: AsyncIOMotorDatabase = client["alert-me"] -chats: AsyncIOMotorCollection = db["chats"] -logs: AsyncIOMotorCollection = db["logs"] +db = client["alert-me"] +chats = db["chats"] +logs = db["logs"] clean_up_db = True if environ.get("CLEAN_UP_DB", False) == "true" else False """ Setup strings """ diff --git a/app/__main__.py b/app/__main__.py index c0bfb69..c91790d 100644 --- a/app/__main__.py +++ b/app/__main__.py @@ -1,29 +1,30 @@ -from sys import argv from os import environ +from sys import argv + +from telegram import Message from telegram.ext import ( Application, + CallbackQueryHandler, ChatJoinRequestHandler, - MessageHandler, CommandHandler, - CallbackQueryHandler, + MessageHandler, filters, ) from telegram.ext.filters import MessageFilter -from telegram import Message +from app import dialog_manager from app.handlers import ( admin_op, + answering_help, expected_dialog, getting_status, - replying_to_bot, - wants_to_join, + has_joined, processing_cbq, - answering_help, - setting_bot, + replying_to_bot, resetting, - has_joined, + setting_bot, + wants_to_join, ) -from app import dialog_manager class Dialog(MessageFilter): @@ -68,6 +69,7 @@ def registerHandlers(app: Application): """ from os import path + import uvloop TOKEN = environ["TOKEN"] diff --git a/app/db.py b/app/db.py index 35c0fd6..75d3d1c 100644 --- a/app/db.py +++ b/app/db.py @@ -1,20 +1,19 @@ -from functools import reduce +from asyncio import as_completed, gather, sleep +from datetime import datetime, timedelta from os import environ from typing import Optional -from telegram.ext import ContextTypes + from pymongo.collection import ReturnDocument -from pymongo.results import DeleteResult, UpdateResult, InsertOneResult -from datetime import datetime, timedelta -from asyncio import as_completed, gather, sleep +from pymongo.results import DeleteResult, InsertOneResult, UpdateResult +from telegram.ext import ContextTypes -from app import chats, logs +from app import chats, clean_up_db, logs from app.types import ( ChatId, Log, - Questionnaire, - Operation, - ServiceLog, MessageId, + Operation, + Questionnaire, ServiceLog, Settings, Status, @@ -24,7 +23,6 @@ UserWithName, ) from app.utils import mark_successful_coroutines, run_coroutines_masked -from app import clean_up_db """ Settings """ @@ -73,7 +71,6 @@ async def remove_chats(chats_ids: list[ChatId]) -> DeleteResult: async def add_pending( chat_id: ChatId, user_id: UserId, message_id: MessageId ) -> UpdateResult: - user_key = f"pending_{user_id}" payload = {"message_id": message_id, "at": datetime.now()} return await chats.find_one_and_update( @@ -230,7 +227,6 @@ async def get_users_at(chat_id: ChatId, user_ids: list[UserId]) -> list[datetime async def log(to_log: Log) -> InsertOneResult | UpdateResult: match to_log: - case ServiceLog(): return await logs.insert_one(to_log.as_dict()) @@ -308,7 +304,6 @@ async def background_task(context: ContextTypes.DEFAULT_TYPE | None) -> None | i # Collecting results async for u in cursor: - if not "user_id" in u or not "chat_id" in u: continue @@ -322,7 +317,6 @@ async def background_task(context: ContextTypes.DEFAULT_TYPE | None) -> None | i to_notify.append(User(u["user_id"], u["chat_id"])) if tried_6h_ago_and_got_alert(u): - if u["chat_id"] in banners: if not hh(uid, cid) in banned: to_ban.append(User(u["user_id"], u["chat_id"])) @@ -341,6 +335,7 @@ async def background_task(context: ContextTypes.DEFAULT_TYPE | None) -> None | i "chat_id": {"$in": [u.chat_id for u in to_deny_and_remove]}, } ) + # Declining pending join requests with exceptions masked # as there is no way to determine with certainty if the target join request was taken back or not async def deny_notify(user: User): diff --git a/app/handlers.py b/app/handlers.py index 5d1ed57..17e3d9a 100644 --- a/app/handlers.py +++ b/app/handlers.py @@ -23,7 +23,9 @@ upsert_settings, ) from app.types import ( + ChatData, ChatId, + ChatJoinRequestData, Dialog, Mode, Questionnaire, @@ -47,27 +49,33 @@ async def answering_help(update: Update, context: ContextTypes.DEFAULT_TYPE): - chat_id = update.message.chat_id + chat_data = ChatData.from_update(update) + if not chat_data: + return reply = strings["commands"]["help"] await context.bot.send_message( - chat_id, reply, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True + chat_data.chat_id, + reply, + parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=True, ) @withAuth async def setting_bot(update: Update, context: ContextTypes.DEFAULT_TYPE): - received = update.message.text - chat_id = update.message.chat_id - s = received.split(" ") + chat_data = ChatData.from_update(update) + if not chat_data: + return + s = chat_data.message_text.split(" ") reply = "" if len(s) == 1 or s[1] == "": # Get - if fetched := await fetch_settings(chat_id): + if fetched := await fetch_settings(chat_data.chat_id): reply = strings["settings"]["settings"] + fetched.render(with_alert=True) else: reply = strings["settings"]["none_found"] - elif settings := Settings(received, chat_id): + elif settings := Settings(chat_data.message_text, chat_data.chat_id): # Set if updated := await upsert_settings(settings): questionnaire: Mode = "questionnaire" @@ -82,21 +90,20 @@ async def extractor_closure( rep = "" if q := Questionnaire.parse(answers): - await upsert_questionnaire(chat_id, q) + await upsert_questionnaire(chat_data.chat_id, q) rep = "Thanks, the questionnaire reads:\n" + q.render() else: rep = "Failed to parse your message into a valid questionnaire. Please start over." - await context.bot.send_message(chat_id, rep) + await context.bot.send_message(chat_data.chat_id, rep) # Setting up state to detect the reply - user_id = update.message.from_user.id dialog_manager.add( - user_id, + chat_data.user_id, Reply( - user_id, - chat_id, + chat_data.user_id, + chat_data.chat_id, extractor_closure, ), ) @@ -109,53 +116,50 @@ async def extractor_closure( # No parse reply = strings["settings"]["failed_to_parse"] await context.bot.send_message( - chat_id, reply, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True + chat_data.chat_id, + reply, + parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=True, ) @withAuth async def resetting(update: Update, context: ContextTypes.DEFAULT_TYPE): - chat_id = update.message.chat_id + chat_data = ChatData.from_update(update) + if not chat_data: + return reply = strings["settings"]["reset"] await gather( - reset(chat_id), - context.bot.send_message(chat_id, reply, disable_web_page_preview=True), + reset(chat_data.chat_id), + context.bot.send_message( + chat_data.chat_id, reply, disable_web_page_preview=True + ), ) async def wants_to_join(update: Update, context: ContextTypes.DEFAULT_TYPE): - request = update.chat_join_request - user_id, user_name, chat_id, chat_name = ( - request.from_user.id, - request.from_user.username or request.from_user.first_name, - request.chat.id, - request.chat.username, - ) - + req = ChatJoinRequestData.from_update(update) + if not req: + return admins, settings = await gather( - context.bot.get_chat_administrators(chat_id), fetch_settings(chat_id) + context.bot.get_chat_administrators(req.chat_id), fetch_settings(req.chat_id) ) alert = admins_ids_mkup(admins) if admins else "" - # Missing settings if not settings: return await context.bot.send_message( - chat_id, strings["settings"]["missing"], disable_web_page_preview=True + req.chat_id, strings["settings"]["missing"], disable_web_page_preview=True ) - # Paused if hasattr(settings, "paused") and settings.paused: return - if hasattr(settings, "mode"): - match settings.mode: - case "auto": await gather( context.bot.send_message( - user_id, + req.user_chat_id, settings.verification_msg if settings.verification_msg and len(settings.verification_msg) >= 10 @@ -163,13 +167,20 @@ async def wants_to_join(update: Update, context: ContextTypes.DEFAULT_TYPE): disable_web_page_preview=True, reply_markup=agree_btn( strings["wants_to_join"]["ok"], - chat_id, + req.chat_id, strings["chat"]["url"] if not settings.chat_url else settings.chat_url, ), ), - log(UserLog("wants_to_join", chat_id, user_id, user_name)), + log( + UserLog( + "wants_to_join", + req.chat_id, + req.from_user_id, + req.from_user_name, + ) + ), ) case "manual": @@ -181,27 +192,27 @@ async def closure_send(destination: ChatId, text: str): parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True, reply_markup=accept_or_reject_btns( - user_id, - user_name, - chat_id, + req.from_user_id, + req.from_user_name, + req.chat_id, strings["chat"]["url"] if not settings.chat_url else settings.chat_url, ), ) - await add_pending(chat_id, user_id, response.message_id) + await add_pending( + req.chat_id, req.from_user_id, response.message_id + ) if hasattr(settings, "helper_chat_id") and settings.helper_chat_id: - body = f"{mention_markdown(user_id, user_name)} has just asked to join your chat {mention_markdown(chat_id, chat_name)}, you might want to accept them." + body = f"{mention_markdown(req.from_user_id, req.from_user_name)} has just asked to join your chat {mention_markdown(req.chat_id, req.chat_name)}, you might want to accept them." await closure_send(settings.helper_chat_id, alert + "\n" + body) else: - body = f"{mention_markdown(user_id, user_name)} just joined, but I couldn't find any chat to notify." - await closure_send(chat_id, alert + "\n" + body) + body = f"{mention_markdown(req.from_user_id, req.from_user_name)} just joined, but I couldn't find any chat to notify." + await closure_send(req.chat_id, alert + "\n" + body) case "questionnaire": - if q := settings.questionnaire: - questions = q.questions async def extractor_closure(answers: list[str]) -> None: @@ -211,23 +222,23 @@ async def extractor_closure(answers: list[str]) -> None: for q, a in zip(questions, answers) ] ) - reply = f"@{mention_markdown(user_id, user_name)} has just requested to join this chat. Their answers to the questionnaire are as follows:\n{escape_markdown(q_a)}" + reply = f"@{mention_markdown(req.from_user_id, req.from_user_name)} has just requested to join this chat. Their answers to the questionnaire are as follows:\n{escape_markdown(q_a)}" keyboard = accept_or_reject_btns( - user_id, user_name, chat_id, "" + req.from_user_id, req.from_user_name, req.chat_id, "" ) await context.bot.send_message( - chat_id, + req.chat_id, reply, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN, ) - dialog = Dialog(user_id, chat_id, q, extractor_closure) + dialog = Dialog(req.from_user_id, req.chat_id, q, extractor_closure) dialog.start() reply = dialog.take_reply() - dialog_manager.add(user_id, dialog) + dialog_manager.add(req.from_user_id, dialog) await context.bot.send_message( - user_id, dialog.intro + ("\n" + reply) if reply else "" + req.user_chat_id, dialog.intro + ("\n" + reply) if reply else "" ) case _: @@ -322,7 +333,6 @@ async def processing_cbq(update: Update, context: ContextTypes.DEFAULT_TYPE): reply = "" match operation: - case "accept": response = await context.bot.approve_chat_join_request(chat_id, user_id) if response: @@ -475,16 +485,13 @@ async def expected_dialog(update: Update, context: ContextTypes.DEFAULT_TYPE): text = update.message.text if "/cancel" in text: - dialog_manager.cancel(user_id) user_id = update.message.from_user.id reply = "Okay, starting over" return await context.bot.send_message(user_id, reply) if dialog := dialog_manager[user_id]: - match dialog: - case Dialog(): if reply := dialog.take_reply(text): await context.bot.send_message(user_id, reply) diff --git a/app/types.py b/app/types.py index aef3e0b..2064e5d 100644 --- a/app/types.py +++ b/app/types.py @@ -1,7 +1,9 @@ from __future__ import annotations + +from asyncio import create_task, get_running_loop from datetime import datetime -from itertools import pairwise from functools import reduce +from itertools import pairwise from typing import ( Any, Callable, @@ -13,8 +15,8 @@ OrderedDict, TypeAlias, ) -from asyncio import create_task, get_running_loop +from telegram import Update from telegram.helpers import escape_markdown ChatId: TypeAlias = int | str @@ -175,6 +177,48 @@ def __len__(self) -> int: return len(self.as_dict()) +class ChatData(NamedTuple): + chat_id: int + user_id: int + message_text: str + + @classmethod + def from_update(cls, update: Update) -> ChatData | None: + try: + return cls( + user_id=update.message.from_user.id, + chat_id=update.message.chat_id, + message_text=update.message.text, + ) + except Exception: + return None + + +class ChatJoinRequestData(NamedTuple): + chat_id: int + chat_name: str + from_user_id: int + user_chat_id: int + from_user_name: str + + @classmethod + def from_update(cls, update: Update) -> ChatJoinRequestData | None: + request = update.chat_join_request + if not request: + return + try: + return cls( + chat_id=request.chat.id, + chat_name=request.chat.username, + from_user_id=request.from_user.id, + user_chat_id=request.user_chat_id, + from_user_name=request.from_user.username + or request.from_user.first_name, + ) + except Exception: + return None + + Operation = Literal[ "wants_to_join", "has_verified", diff --git a/app/utils.py b/app/utils.py index 0269105..316cb0f 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,5 +1,5 @@ from asyncio import as_completed -from datetime import datetime, timedelta, date +from datetime import date, datetime, timedelta from functools import wraps from typing import Any, Callable, Coroutine, Generator, Iterable @@ -31,7 +31,7 @@ def mention_markdown(user_id: UserId, username: str) -> str: return f"[{username}](tg://user?id={user_id})" -def admins_ids_mkup(admins: list[ChatMember]) -> str: +def admins_ids_mkup(admins: Iterable[ChatMember]) -> str: return ", ".join( [ mention_markdown( diff --git a/requirements.txt b/requirements.txt index 3a84dde..7584935 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,33 +1,24 @@ -anyio==3.6.1 -APScheduler==3.9.1 -attrs==21.4.0 -cachetools==5.2.0 -certifi==2022.6.15 -dnspython==2.2.1 -h11==0.12.0 -httpcore==0.15.0 -httpx==0.23.0 -idna==3.3 -iniconfig==1.1.1 -install==1.3.5 -motor==3.0.0 -packaging==21.3 -pluggy==1.0.0 -py==1.11.0 -pymongo==4.1.1 -pyparsing==3.0.9 -pytest==7.1.2 -pytest-asyncio==0.19.0 -python-telegram-bot==20.0a2 -pytz==2022.1 -pytz-deprecation-shim==0.1.0.post0 -requests==2.28.1 -rfc3986==1.5.0 -six==1.16.0 -sniffio==1.2.0 +anyio==3.6.2; python_full_version >= '3.6.2' +certifi==2022.12.7; python_version >= '3.6' +charset-normalizer==3.1.0; python_version >= '3.7' +dnspython==2.3.0; python_version >= '3.7' and python_version < '4.0' +exceptiongroup==1.1.1; python_version < '3.11' +h11==0.14.0; python_version >= '3.7' +httpcore==0.16.3; python_version >= '3.7' +httpx==0.23.3; python_version >= '3.7' +idna==3.4; python_version >= '3.5' +iniconfig==2.0.0; python_version >= '3.7' +motor==3.1.2 +packaging==23.1; python_version >= '3.7' +pluggy==1.0.0; python_version >= '3.6' +pymongo[motor]==4.3.3 +pytest-asyncio==0.21.0 +pytest==7.3.1 +python-telegram-bot==20.2 +requests==2.28.2 +rfc3986[idna2008]==1.5.0 +sniffio==1.3.0; python_version >= '3.7' toml==0.10.2 -tomli==2.0.1 -tornado==6.2 -tzdata==2022.1 -tzlocal==4.2 -uvloop==0.16.0 +tomli==2.0.1; python_version < '3.11' +urllib3==1.26.15; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' +uvloop==0.17.0 diff --git a/strings.toml b/strings.toml index e296b02..a1f305e 100644 --- a/strings.toml +++ b/strings.toml @@ -35,7 +35,7 @@ zn = "歡迎" [settings] settings = "Your settings read:\n" -updated = "Thanks, your new settings read:\n---\n" +updated = "Thanks, your new settings read:\n---\n" failed_to_parse = "Unable to parse! You need to pass pairs of key-values separated by spaces, as in:\n `key1 val1 key2 val2`... The list of keys is available [here](https://github.com/why-not-try-calmer/ringo#commands)." failed_to_update = "Unable to update! There might be an issue with the database." reset = "All settings reset. Get started with `/set mode auto`. Call `/help` if you need." diff --git a/test/test_async.py b/test/test_async.py index bdba849..722ce80 100644 --- a/test/test_async.py +++ b/test/test_async.py @@ -1,6 +1,7 @@ -import pytest -from typing import Any, Coroutine from asyncio import as_completed, gather, get_event_loop_policy +from typing import Any, Coroutine + +import pytest from app.db import background_task, fetch_chat_ids, fetch_settings, get_status