From 6bcaf782885b3fc9556b5be8a4ccc815f9a82a2f Mon Sep 17 00:00:00 2001 From: Akim Date: Fri, 26 Apr 2024 16:36:26 +0100 Subject: [PATCH 01/18] fixed incs2 channel handler issue fixed issue when message was longer than discord webhook limit --- plugins/incs2chat.py | 61 +++++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/plugins/incs2chat.py b/plugins/incs2chat.py index 99c2613..092384f 100644 --- a/plugins/incs2chat.py +++ b/plugins/incs2chat.py @@ -80,7 +80,7 @@ def convert_message_to_markdown_text(message: Message) -> str: return ''.join(text) -def translate_text(text: str, source_lang: str = 'RU', target_lang: str = 'EN'): +def translate_text(text: str, source_lang: str = 'RU', target_lang: str = 'EN') -> str | None: headers = {'Authorization': f'DeepL-Auth-Key {config.DEEPL_TOKEN}', 'Content-Type': 'application/json'} data = {'text': [text], 'source_lang': source_lang, 'target_lang': target_lang} @@ -104,6 +104,44 @@ def post_to_discord_webhook(url: str, text: str): # todo: attachments support? logging.error(f'{r.status_code=}, {r.reason=}') +def process_ds_text(message: Message) -> list[str] | None: + logging.info(message) + + text_list = [convert_message_to_markdown_text(message)] + + # DS webhook limit 2000 symbols (ahui), hope tg limit is <4000 if more pls fix + if text_list[0]: + if len(text_list[0]) > 2000: + # splitting message into 2 similar-sized messages to save structure and beauty + last_newline_index = text_list[0][:len(text_list[0]) // 2].rfind('\n') + if last_newline_index != -1: + second_part = text_list[0][last_newline_index + 1:] + text_list.append(second_part) + text_list[0] = text_list[0][:last_newline_index] + + return text_list + + +def forward_to_discord(message: Message): + texts = process_ds_text(message) + logging.info(texts) + + if texts: + for text in texts: + post_to_discord_webhook(config.DS_WEBHOOK_URL, text) + post_to_discord_webhook(config.DS_WEBHOOK_URL_EN, translate_text(text, 'RU', 'EN')) + + message.continue_propagation() + + +async def cs_l10n_update(message: Message): + has_the_l10n_line = ((message.text and "Обновлены файлы локализации" in message.text) + or (message.caption and "Обновлены файлы локализации" in message.caption)) + + if has_the_l10n_line: + await message.reply_sticker('CAACAgIAAxkBAAID-l_9tlLJhZQSgqsMUAvLv0r8qhxSAAIKAwAC-p_xGJ-m4XRqvoOzHgQ') + + @Client.on_message(filters.chat(config.INCS2CHAT) & filters.command("ban")) async def ban(client: Client, message: Message): chat = await client.get_chat(config.INCS2CHAT) @@ -207,27 +245,14 @@ async def echo(client: Client, message: Message): return await reply_to.reply_voice(voice, quote=should_reply, caption=caption, caption_entities=entities) -@Client.on_message(filters.channel & filters.chat(config.INCS2CHANNEL)) -async def forward_to_discord(_, message: Message): - text = convert_message_to_markdown_text(message) - logging.info(text) - - if text: - post_to_discord_webhook(config.DS_WEBHOOK_URL, text) - post_to_discord_webhook(config.DS_WEBHOOK_URL_EN, translate_text(text, 'RU', 'EN')) - - message.continue_propagation() - - @Client.on_message(filters.linked_channel & filters.chat(config.INCS2CHAT)) -async def cs_l10n_update(_, message: Message): +async def handle_new_post(_, message: Message): is_sent_by_correct_chat = (message.sender_chat and message.sender_chat.id == config.INCS2CHANNEL) is_forwarded_from_correct_chat = (message.forward_from_chat and message.forward_from_chat.id == config.INCS2CHANNEL) - has_the_l10n_line = ((message.text and "Обновлены файлы локализации" in message.text) - or (message.caption and "Обновлены файлы локализации" in message.caption)) - if is_sent_by_correct_chat and is_forwarded_from_correct_chat and has_the_l10n_line: - await message.reply_sticker('CAACAgIAAxkBAAID-l_9tlLJhZQSgqsMUAvLv0r8qhxSAAIKAwAC-p_xGJ-m4XRqvoOzHgQ') + if is_sent_by_correct_chat and is_forwarded_from_correct_chat: + await cs_l10n_update(message) + forward_to_discord(message) @Client.on_message(filters.chat(config.INCS2CHAT) & filters.forwarded) From b376f3e9c56a4c82ef9c035f5825b1e072933094 Mon Sep 17 00:00:00 2001 From: Akim Date: Fri, 26 Apr 2024 16:42:45 +0100 Subject: [PATCH 02/18] fix --- plugins/incs2chat.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/incs2chat.py b/plugins/incs2chat.py index 092384f..6c0e041 100644 --- a/plugins/incs2chat.py +++ b/plugins/incs2chat.py @@ -131,8 +131,6 @@ def forward_to_discord(message: Message): post_to_discord_webhook(config.DS_WEBHOOK_URL, text) post_to_discord_webhook(config.DS_WEBHOOK_URL_EN, translate_text(text, 'RU', 'EN')) - message.continue_propagation() - async def cs_l10n_update(message: Message): has_the_l10n_line = ((message.text and "Обновлены файлы локализации" in message.text) From da222728a70c30d59a5960b52399ae4012f15cd4 Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Fri, 26 Apr 2024 22:07:56 +0500 Subject: [PATCH 03/18] Use tgenity for formatting conversion --- plugins/incs2chat.py | 88 ++++++++++++------------------------------- requirements.txt | Bin 2946 -> 2980 bytes 2 files changed, 25 insertions(+), 63 deletions(-) diff --git a/plugins/incs2chat.py b/plugins/incs2chat.py index 6c0e041..2fa1209 100644 --- a/plugins/incs2chat.py +++ b/plugins/incs2chat.py @@ -2,10 +2,11 @@ import logging import random -import requests from pyrogram import Client, filters -from pyrogram.enums import ChatMembersFilter, MessageEntityType +from pyrogram.enums import ChatMembersFilter from pyrogram.types import Message, MessageEntity +import requests +from tgentity import to_md # noinspection PyUnresolvedReferences import env @@ -34,50 +35,13 @@ def correct_message_entities(entities: list[MessageEntity] | None, return entities -def convert_message_to_markdown_text(message: Message) -> str: - """Convert Telegram formatting to Markdown (primarily for Discord).""" - - entity_type_to_markdown = { # only includes simple closing entities - MessageEntityType.BOLD: '**', - MessageEntityType.ITALIC: '*', - MessageEntityType.UNDERLINE: '__', - MessageEntityType.STRIKETHROUGH: '~~', - MessageEntityType.SPOILER: '||', - MessageEntityType.CODE: '`' - } - - text = message.text or message.caption or '' - - if message.entities is None: - return text - - text = list(text) - text_shift = 0 - for entity in message.entities: - # special cases first - if entity.type is MessageEntityType.TEXT_LINK: - text.insert(entity.offset + text_shift, '[') - text.insert(entity.offset + entity.length + text_shift + 1, f']({entity.url})') - text_shift += 2 - if entity.type is MessageEntityType.PRE: - text.insert(entity.offset + text_shift, f'```{entity.language}\n') - text.insert(entity.offset + entity.length + text_shift + 1, f'\n```') - text_shift += 2 - if entity.type is MessageEntityType.BLOCKQUOTE: - text.insert(entity.offset + text_shift, '> ') - text_shift += 1 - - symbol = entity_type_to_markdown.get(entity.type) - if symbol: - previous_symbol = text[entity.offset + text_shift - 2] - if previous_symbol in entity_type_to_markdown.values(): # todo: still fails on 3+ styles in the same chunk - text.insert(entity.offset + text_shift - 1, symbol) - text.insert(entity.offset + entity.length + text_shift, symbol) - else: - text.insert(entity.offset + text_shift, symbol) - text.insert(entity.offset + entity.length + text_shift + 1, symbol) - text_shift += 2 - return ''.join(text) +def to_discord_markdown(message: Message) -> str: + text = (to_md(message) + .replace('~', '~~') + .replace(r'\(', '(') # god bless this feels awful + .replace(r'\)', ')')) + + return text def translate_text(text: str, source_lang: str = 'RU', target_lang: str = 'EN') -> str | None: @@ -104,32 +68,30 @@ def post_to_discord_webhook(url: str, text: str): # todo: attachments support? logging.error(f'{r.status_code=}, {r.reason=}') -def process_ds_text(message: Message) -> list[str] | None: +def process_discord_text(message: Message) -> list[str]: logging.info(message) - text_list = [convert_message_to_markdown_text(message)] + text_list = [to_discord_markdown(message) if message.entities else message.text] - # DS webhook limit 2000 symbols (ahui), hope tg limit is <4000 if more pls fix - if text_list[0]: - if len(text_list[0]) > 2000: - # splitting message into 2 similar-sized messages to save structure and beauty - last_newline_index = text_list[0][:len(text_list[0]) // 2].rfind('\n') - if last_newline_index != -1: - second_part = text_list[0][last_newline_index + 1:] - text_list.append(second_part) - text_list[0] = text_list[0][:last_newline_index] + # Discord webhook limit is 2000 symbols (@akimerslys: ahui), hoping Telegram limit is <4000 + if len(text_list[0]) > 2000: + # splitting message into 2 similar-sized messages to save structure and beauty + last_newline_index = text_list[0][:len(text_list[0]) // 2].rfind('\n') + if last_newline_index != -1: + second_part = text_list[0][last_newline_index + 1:] + text_list.append(second_part) + text_list[0] = text_list[0][:last_newline_index] - return text_list + return text_list def forward_to_discord(message: Message): - texts = process_ds_text(message) + texts = process_discord_text(message) logging.info(texts) - if texts: - for text in texts: - post_to_discord_webhook(config.DS_WEBHOOK_URL, text) - post_to_discord_webhook(config.DS_WEBHOOK_URL_EN, translate_text(text, 'RU', 'EN')) + for text in texts: + post_to_discord_webhook(config.DS_WEBHOOK_URL, text) + post_to_discord_webhook(config.DS_WEBHOOK_URL_EN, translate_text(text, 'RU', 'EN')) async def cs_l10n_update(message: Message): diff --git a/requirements.txt b/requirements.txt index d1b95854b8af70e54f98821e9c933be656e1e4a1..3d68268075e89a7a598780e5450533d0dfb2c558 100644 GIT binary patch delta 42 tcmZn?Un0IiiAy$wA)UdMA&;SiArpuz8Ek>jfI*MJ2#5_f%W|bM0szJ<2rd8s delta 12 TcmZ1?-Xy+3iEFb9R{|pd8ngqG From e0b7719c5c5d6175bb6fc149bb84a78b35b1e0ab Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Thu, 2 May 2024 15:14:53 +0500 Subject: [PATCH 04/18] Fix getting wrong cached data, `States.sget` -> `States.get_or_unknown` --- utypes/game_data.py | 27 +++++++++------------------ utypes/states.py | 19 +++++++------------ 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/utypes/game_data.py b/utypes/game_data.py index 0bd11eb..536e14b 100644 --- a/utypes/game_data.py +++ b/utypes/game_data.py @@ -180,8 +180,7 @@ def request(cls): cs2_client_version = int(options['ClientVersion']) - 2000000 cs2_server_version = int(options['ServerVersion']) - 2000000 cs2_patch_version = options['PatchVersion'] - cs2_version_timestamp = version_datetime.timestamp() # todo: maybe should use unix timestamp - # todo: instead of this piece of crap? + cs2_version_timestamp = version_datetime.timestamp() return GameVersionData(cs2_client_version, cs2_server_version, @@ -199,11 +198,7 @@ def cached_data(filename: str | Path): cs2_server_version = cache_file.get('cs2_client_version', 'unknown') cs2_patch_version = cache_file.get('cs2_patch_version', 'unknown') - # Timestamp could be saved as ISO-8601, so we need to address that cs2_version_timestamp = cache_file.get('cs2_version_timestamp', 0) - if isinstance(cs2_version_timestamp, str): - cs2_version_timestamp = dt.datetime.fromisoformat(cs2_version_timestamp).timestamp() - return GameVersionData(cs2_client_version, cs2_server_version, cs2_patch_version, @@ -296,15 +291,11 @@ def cached_server_status(filename: str | Path): if game_server_dt == States.UNKNOWN: return States.UNKNOWN - gc_state = States.sget(cache_file.get('game_coordinator')) - sl_state = States.sget(cache_file.get('sessions_logon')) - ms_state = States.sget(cache_file.get('matchmaking_scheduler')) - sc_state = States.sget(cache_file.get('steam_community')) - webapi_state = States.sget(cache_file.get('webapi')) - - now = utime.utcnow() - is_maintenance = ((now.weekday() == 1 and now.hour > 21) or (now.weekday() == 2 and now.hour < 4)) \ - and not (gc_state == States.NORMAL and sl_state == States.NORMAL) + gc_state = States.get_or_unknown(cache_file.get('game_coordinator_state')) + sl_state = States.get_or_unknown(cache_file.get('sessions_logon_state')) + ms_state = States.get_or_unknown(cache_file.get('matchmaking_scheduler_state')) + sc_state = States.get_or_unknown(cache_file.get('steam_community_state')) + webapi_state = States.get_or_unknown(cache_file.get('webapi_state')) return ServerStatusData(game_server_dt, gc_state, sl_state, ms_state, sc_state, webapi_state) @@ -318,8 +309,8 @@ def cached_matchmaking_stats(filename: str | Path): if game_server_dt is States.UNKNOWN: return States.UNKNOWN - gc_state = States.sget(cache_file.get('game_coordinator')) - sl_state = States.sget(cache_file.get('sessions_logon')) + gc_state = States.get_or_unknown(cache_file.get('game_coordinator_state')) + sl_state = States.get_or_unknown(cache_file.get('sessions_logon_state')) graph_url = cache_file.get('graph_url', '') online_players = cache_file.get('online_players', 0) @@ -347,7 +338,7 @@ def latest_info_update(filename: str | Path): if cache_file.get('api_timestamp', 'unknown') == 'unknown': return States.UNKNOWN - return dt.datetime.fromtimestamp(cache_file.get('api_timestamp', 0), dt.UTC) + return dt.datetime.fromtimestamp(cache_file['api_timestamp'], dt.UTC) class LeaderboardStats(NamedTuple): diff --git a/utypes/states.py b/utypes/states.py index b570634..a337a5a 100644 --- a/utypes/states.py +++ b/utypes/states.py @@ -32,18 +32,13 @@ class States: UNKNOWN = State('unknown', LK.states_unknown) @classmethod - def get(cls, data: str) -> State | None: - data = data.replace(' ', '_').upper() - try: - return getattr(cls, data) - except KeyError: - return + def get(cls, data, default=None) -> State | None: + data = str(data).replace(' ', '_').upper() + + return getattr(cls, data, default) @classmethod - def sget(cls, data: str | None) -> State: - """Same as States.get(), but returns States.UNKNOWN if there's no such state or `data is None`.""" - if data is None: - return States.UNKNOWN - st = cls.get(data) + def get_or_unknown(cls, data: str | None) -> State: + """Same as ``States.get()``, but returns ``States.UNKNOWN`` if there's no such state.""" - return st if st is not None else States.UNKNOWN + return cls.get(data, States.UNKNOWN) From ef4e7ec7d524d738eb7fc7e5777de39be8fcfdd4 Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Thu, 2 May 2024 15:15:20 +0500 Subject: [PATCH 05/18] Add new India DC, update localization --- collectors/core.py | 1 + l10n/data/be.json | 3 ++- l10n/data/en.json | 1 + l10n/data/fa.json | 1 + l10n/data/it.json | 1 + l10n/data/ru.json | 3 ++- l10n/data/tr.json | 3 ++- l10n/data/uk.json | 3 ++- l10n/data/uz.json | 1 + l10n/l10n.py | 1 + utypes/datacenters.py | 14 +++++++++----- 11 files changed, 23 insertions(+), 9 deletions(-) diff --git a/collectors/core.py b/collectors/core.py index 2cee155..9367838 100644 --- a/collectors/core.py +++ b/collectors/core.py @@ -48,6 +48,7 @@ ('india', 'mumbai'): 'India Mumbai', ('india', 'chennai'): 'India Chennai', ('india', 'bombay'): 'India Bombay', + ('india', 'madras'): 'India Madras', ('china', 'shanghai'): 'China Shanghai', ('china', 'tianjin'): 'China Tianjin', ('china', 'guangzhou'): 'China Guangzhou', diff --git a/l10n/data/be.json b/l10n/data/be.json index dc375a5..cea038d 100644 --- a/l10n/data/be.json +++ b/l10n/data/be.json @@ -158,8 +158,9 @@ "dc_india_title": "дата-цэнтраў Індыі", "dc_india_inline_title": "Індыйскія ДЦ", "dc_india_mumbai": "Мумбаі", - "dc_india_bombay": "Bombay", + "dc_india_bombay": "Бамбей", "dc_india_chennai": "Чэннай", + "dc_india_madras": "Мадрас", "dc_japan": "Японія", "dc_japan_title": "дата-цэнтра Японіі", "dc_japan_inline_title": "Японскі ДЦ", diff --git a/l10n/data/en.json b/l10n/data/en.json index 07685b7..bcd9eaf 100644 --- a/l10n/data/en.json +++ b/l10n/data/en.json @@ -160,6 +160,7 @@ "dc_india_mumbai": "Mumbai", "dc_india_bombay": "Bombay", "dc_india_chennai": "Chennai", + "dc_india_madras": "Madras", "dc_japan": "Japan", "dc_japan_title": "Japanʼs DC", "dc_japan_inline_title": "Japanese DC", diff --git a/l10n/data/fa.json b/l10n/data/fa.json index 2a9df9f..266a9b8 100644 --- a/l10n/data/fa.json +++ b/l10n/data/fa.json @@ -160,6 +160,7 @@ "dc_india_mumbai": "مامبی", "dc_india_bombay": "Bombay", "dc_india_chennai": "چنای", + "dc_india_madras": "Madras", "dc_japan": "ژاپن", "dc_japan_title": "ژاپن", "dc_japan_inline_title": "دیتا سنتر ژاپنی", diff --git a/l10n/data/it.json b/l10n/data/it.json index 32ed6b3..ef31e81 100644 --- a/l10n/data/it.json +++ b/l10n/data/it.json @@ -160,6 +160,7 @@ "dc_india_mumbai": "Mumbai", "dc_india_bombay": "Bombay", "dc_india_chennai": "Chennai", + "dc_india_madras": "Madras", "dc_japan": "Giappone", "dc_japan_title": "del CED del Giappone", "dc_japan_inline_title": "CED Giapponese", diff --git a/l10n/data/ru.json b/l10n/data/ru.json index 81e67cb..195f1ca 100644 --- a/l10n/data/ru.json +++ b/l10n/data/ru.json @@ -158,8 +158,9 @@ "dc_india_title": "дата-центров Индии", "dc_india_inline_title": "Индийские ДЦ", "dc_india_mumbai": "Мумбаи", - "dc_india_bombay": "Bombay", + "dc_india_bombay": "Бомбей", "dc_india_chennai": "Ченнай", + "dc_india_madras": "Мадрас", "dc_japan": "Япония", "dc_japan_title": "дата-центра Японии", "dc_japan_inline_title": "Японский ДЦ", diff --git a/l10n/data/tr.json b/l10n/data/tr.json index c19d390..6706c95 100644 --- a/l10n/data/tr.json +++ b/l10n/data/tr.json @@ -157,9 +157,10 @@ "dc_india": "Hindistan", "dc_india_title": "Hindistan'daki veri merkezleri", "dc_india_inline_title": "Hint DC'leri", - "dc_india_mumbai": "Bombay", + "dc_india_mumbai": "Mumbai", "dc_india_bombay": "Bombay", "dc_india_chennai": "Chennai", + "dc_india_madras": "Madras", "dc_japan": "Japonya", "dc_japan_title": "Japonya'daki veri merkezi", "dc_japan_inline_title": "Japon DC", diff --git a/l10n/data/uk.json b/l10n/data/uk.json index a6ae700..c1db88d 100644 --- a/l10n/data/uk.json +++ b/l10n/data/uk.json @@ -158,8 +158,9 @@ "dc_india_title": "дата-центрів Індії", "dc_india_inline_title": "Індійські ДЦ", "dc_india_mumbai": "Мумбаї", - "dc_india_bombay": "Bombay", + "dc_india_bombay": "Бомбей", "dc_india_chennai": "Ченнай", + "dc_india_madras": "Мадрас", "dc_japan": "Японія", "dc_japan_title": "дата-центру Японії", "dc_japan_inline_title": "Японський ДЦ", diff --git a/l10n/data/uz.json b/l10n/data/uz.json index 565bfbe..acbba88 100644 --- a/l10n/data/uz.json +++ b/l10n/data/uz.json @@ -160,6 +160,7 @@ "dc_india_mumbai": "Mumbay", "dc_india_bombay": "Bombay", "dc_india_chennai": "Chennai", + "dc_india_madras": "Madras", "dc_japan": "Yaponiya", "dc_japan_title": "Yaponiyadagi data-centre", "dc_japan_inline_title": "Yaponiya DC", diff --git a/l10n/l10n.py b/l10n/l10n.py index ae7f03d..1dc7094 100644 --- a/l10n/l10n.py +++ b/l10n/l10n.py @@ -172,6 +172,7 @@ class Locale(SLocale): dc_india_mumbai: str # Mumbai dc_india_bombay: str # Bombay dc_india_chennai: str # Chennai + dc_india_madras: str # Madras dc_japan: str # Japan dc_japan_title: str # Japanʼs DC dc_japan_inline_title: str # Japanese DC diff --git a/utypes/datacenters.py b/utypes/datacenters.py index 90c74f2..f705df2 100644 --- a/utypes/datacenters.py +++ b/utypes/datacenters.py @@ -278,17 +278,21 @@ class DatacenterAtlas: "india", [ Datacenter( - "mumbai", - l10n_key_title=LK.dc_india_mumbai + "bombay", + l10n_key_title=LK.dc_india_bombay ), Datacenter( "chennai", l10n_key_title=LK.dc_india_chennai ), Datacenter( - "bombay", - l10n_key_title=LK.dc_india_bombay - ) + "madras", + l10n_key_title=LK.dc_india_madras + ), + Datacenter( + "mumbai", + l10n_key_title=LK.dc_india_mumbai + ), ], "🇮🇳", LK.dc_india, From ee729d304fa1e39d9975d162a65172780b7f0293 Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Fri, 3 May 2024 16:08:07 +0500 Subject: [PATCH 06/18] Fixed some formatting issues --- plugins/incs2chat.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/incs2chat.py b/plugins/incs2chat.py index 2fa1209..8e28533 100644 --- a/plugins/incs2chat.py +++ b/plugins/incs2chat.py @@ -1,6 +1,7 @@ import asyncio import logging import random +import re from pyrogram import Client, filters from pyrogram.enums import ChatMembersFilter @@ -37,9 +38,10 @@ def correct_message_entities(entities: list[MessageEntity] | None, def to_discord_markdown(message: Message) -> str: text = (to_md(message) - .replace('~', '~~') + .replace(r'\.', '.') .replace(r'\(', '(') # god bless this feels awful .replace(r'\)', ')')) + text = re.sub(r'~([^~]+)~', r'~~\1~~', text) return text From 2aef3b3f96d32ec2c700c58cd3f36e7e79f1f21c Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Fri, 10 May 2024 19:33:33 +0500 Subject: [PATCH 07/18] Potential fix of corrupted cache, added "dprp_build_sync_id" tracker alert --- collectors/core.py | 25 ++++++++++++++++--------- collectors/game_coordinator.py | 2 +- collectors/gc_alerter.py | 21 +++++++++++++-------- collectors/online_players_graph.py | 2 +- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/collectors/core.py b/collectors/core.py index 9367838..fc42eb1 100644 --- a/collectors/core.py +++ b/collectors/core.py @@ -60,6 +60,13 @@ } execution_start_dt = dt.datetime.now() + +execution_cron_hour = execution_start_dt.hour +execution_cron_minute = execution_start_dt.minute + 1 +if execution_cron_minute >= 60: + execution_cron_hour += 1 + execution_cron_minute %= 60 + loc = locale('ru') logging.basicConfig(level=logging.INFO, @@ -153,12 +160,12 @@ async def update_cache_info(): cache['player_24h_peak'] = player_24h_peak with open(config.CACHE_FILE_PATH, 'w', encoding='utf-8') as f: - json.dump(cache, f, indent=4) + json.dump(cache, f, indent=4, ensure_ascii=False) except Exception: logging.exception('Caught exception in the main thread!') -@scheduler.scheduled_job('cron', hour=execution_start_dt.hour, minute=execution_start_dt.minute + 1) +@scheduler.scheduled_job('cron', hour=execution_cron_hour, minute=execution_cron_minute) async def unique_monthly(): # noinspection PyBroadException try: @@ -176,14 +183,14 @@ async def unique_monthly(): cache['monthly_unique_players'] = data with open(config.CACHE_FILE_PATH, 'w', encoding='utf-8') as f: - json.dump(cache, f, indent=4) + json.dump(cache, f, indent=4, ensure_ascii=False) except Exception: logging.exception('Caught exception while gathering monthly players!') time.sleep(45) return await unique_monthly() -@scheduler.scheduled_job('cron', hour=execution_start_dt.hour, minute=execution_start_dt.minute + 1) +@scheduler.scheduled_job('cron', hour=execution_cron_hour, minute=execution_cron_minute) async def check_currency(): # noinspection PyBroadException try: @@ -196,14 +203,14 @@ async def check_currency(): cache['key_price'] = new_prices with open(config.CACHE_FILE_PATH, 'w', encoding='utf-8') as f: - json.dump(cache, f, indent=4) + json.dump(cache, f, indent=4, ensure_ascii=False) except Exception: logging.exception('Caught exception while gathering key price!') time.sleep(45) return await check_currency() -@scheduler.scheduled_job('cron', hour=execution_start_dt.hour, minute=execution_start_dt.minute + 2) +@scheduler.scheduled_job('cron', hour=execution_cron_hour, minute=execution_cron_minute, second=30) async def fetch_leaderboard(): # noinspection PyBroadException try: @@ -251,10 +258,10 @@ async def send_alert(key, new_value): logging.warning(f'Got wrong key to send alert: {key}') return - if not config.TEST_MODE: - chat_list = [config.INCS2CHAT, config.CSTRACKER] - else: + if config.TEST_MODE: chat_list = [config.AQ] + else: + chat_list = [config.INCS2CHAT, config.CSTRACKER] for chat_id in chat_list: msg = await bot.send_message(chat_id, text) diff --git a/collectors/game_coordinator.py b/collectors/game_coordinator.py index cc21327..35c5257 100644 --- a/collectors/game_coordinator.py +++ b/collectors/game_coordinator.py @@ -174,7 +174,7 @@ def dump_cache(self): """Dumps ``self.cache`` to the cache file.""" with open(self.cache_file_path, 'w', encoding='utf-8') as f: - json.dump(self.cache, f, indent=4) + json.dump(self.cache, f, indent=4, ensure_ascii=False) def update_cache(self, new_info: dict[str, ...]): """Loads the cache into ``self.cache``, updates it with new info and dumps back to the cache file.""" diff --git a/collectors/gc_alerter.py b/collectors/gc_alerter.py index 4a699cc..59db9a6 100644 --- a/collectors/gc_alerter.py +++ b/collectors/gc_alerter.py @@ -24,6 +24,7 @@ 'dpr_build_id': loc.notifs_build_dpr, 'dprp_build_id': loc.notifs_build_dprp, 'dpr_build_sync_id': f'{loc.notifs_build_dpr} 🔃', + 'dprp_build_sync_id': f'{loc.notifs_build_dprp} 🔃', 'cs2_app_changenumber': loc.notifs_build_cs2_client, 'cs2_server_changenumber': loc.notifs_build_cs2_server} @@ -40,7 +41,7 @@ workdir=config.SESS_FOLDER) -@scheduler.scheduled_job('interval', seconds=45) +@scheduler.scheduled_job('interval', seconds=30) async def scan_for_gc_update(): # noinspection PyBroadException try: @@ -53,14 +54,18 @@ async def scan_for_gc_update(): new_value = cache[_id] if prev_cache.get(_id) != new_value: prev_cache[_id] = new_value - if _id == 'dpr_build_id' and new_value == cache['public_build_id']: - await send_alert('dpr_build_sync_id', new_value) - continue + if new_value == cache['public_build_id']: + if _id == 'dpr_build_id': + await send_alert('dpr_build_sync_id', new_value) + continue + if _id == 'dprp_build_id': + await send_alert('dprp_build_sync_id', new_value) + continue await send_alert(_id, new_value) with open(config.GC_PREV_CACHE_FILE_PATH, 'w', encoding='utf-8') as f: - json.dump(prev_cache, f, indent=4) + json.dump(prev_cache, f, indent=4, ensure_ascii=False) except Exception: logging.exception('Caught an exception while scanning GC info!') @@ -76,10 +81,10 @@ async def send_alert(key: str, new_value: int): text = alert_sample.format(new_value) - if not config.TEST_MODE: - chat_list = [config.INCS2CHAT, config.CSTRACKER] - else: + if config.TEST_MODE: chat_list = [config.AQ] + else: + chat_list = [config.INCS2CHAT, config.CSTRACKER] for chat_id in chat_list: msg = await bot.send_message(chat_id, text, disable_web_page_preview=True) diff --git a/collectors/online_players_graph.py b/collectors/online_players_graph.py index ce0ec9f..ee8e065 100644 --- a/collectors/online_players_graph.py +++ b/collectors/online_players_graph.py @@ -117,7 +117,7 @@ def graph_maker(): cache['graph_url'] = image_url with open(config.CACHE_FILE_PATH, 'w', encoding='utf-8') as f: - json.dump(cache, f, indent=4) + json.dump(cache, f, indent=4, ensure_ascii=False) except Exception: logging.exception('Caught exception in graph maker!') time.sleep(MINUTE) From 1c13d171ffd3f57d1e19d47b1d43ed38ce115811 Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Sun, 12 May 2024 01:52:13 +0500 Subject: [PATCH 08/18] Logs unifying, exceptions now send notifications --- bottypes/botclient.py | 14 ++--- bottypes/logger.py | 129 ++++++++++++++++++++++++------------------ main.py | 2 +- 3 files changed, 81 insertions(+), 64 deletions(-) diff --git a/bottypes/botclient.py b/bottypes/botclient.py index 885d0e7..4e24f63 100644 --- a/bottypes/botclient.py +++ b/bottypes/botclient.py @@ -86,9 +86,7 @@ def signal_handler(signum, __): task = asyncio.create_task(asyncio.sleep(self.MAINLOOP_TIMEOUT.total_seconds())) try: await task - if not self.logger.is_queue_empty(): - await self.logger.process_queue() - + await self.logger.process_queue() except asyncio.CancelledError: self.is_in_mainloop = False @@ -458,10 +456,10 @@ async def log(self, text: str, *, disable_notification: bool = True, """Sends log to the log channel.""" if instant: # made for specific log messages (e.g. "Bot is shutting down...") - await self.logger.log_instantly(self, text, disable_notification, reply_markup, parse_mode) + await self.logger.send_log(self, text, disable_notification, reply_markup, parse_mode) return - await self.logger.log(self, text, disable_notification, reply_markup, parse_mode) + await self.logger.schedule_system_log(self, text, disable_notification, reply_markup, parse_mode) async def log_message(self, session: UserSession, message: Message): """Sends message log to the log channel.""" @@ -469,7 +467,7 @@ async def log_message(self, session: UserSession, message: Message): if self.test_mode: return - await self.logger.log_message(self, session, message) + await self.logger.schedule_message_log(self, session, message) async def log_callback(self, session: UserSession, callback_query: CallbackQuery): """Sends callback query log to the log channel.""" @@ -477,7 +475,7 @@ async def log_callback(self, session: UserSession, callback_query: CallbackQuery if self.test_mode: return - await self.logger.log_callback(self, session, callback_query) + await self.logger.schedule_callback_log(self, session, callback_query) async def log_inline(self, session: UserSession, inline_query: InlineQuery): """Sends an inline query log to the log channel.""" @@ -485,4 +483,4 @@ async def log_inline(self, session: UserSession, inline_query: InlineQuery): if self.test_mode: return - await self.logger.log_inline(self, session, inline_query) + await self.logger.schedule_inline_log(self, session, inline_query) diff --git a/bottypes/logger.py b/bottypes/logger.py index 9068c12..9fd6d65 100644 --- a/bottypes/logger.py +++ b/bottypes/logger.py @@ -1,95 +1,114 @@ from __future__ import annotations -import asyncio import typing if typing.TYPE_CHECKING: - from typing import Coroutine from pyrogram.enums import ParseMode from pyrogram.types import (CallbackQuery, InlineQuery, Message, - ReplyKeyboardMarkup) + ReplyKeyboardMarkup, User) from .botclient import BotClient from .sessions import UserSession +class SystemLogPayload(typing.NamedTuple): + client: BotClient + text: str + disable_notification: bool + reply_markup: ReplyKeyboardMarkup + parse_mode: ParseMode + + +class EventLogPayload(typing.NamedTuple): + client: BotClient + user: User + session: UserSession + result_text: str + + +SYSTEM = 'system' + + class BotLogger: """Made to work in a pair with BotClient handling logging stuff.""" - def __init__(self, log_channel_id: int, ): + def __init__(self, log_channel_id: int): self.log_channel_id = log_channel_id - self._logs_queue = asyncio.Queue() + self._logs_queue: dict[str, list[SystemLogPayload | EventLogPayload]] = {} def is_queue_empty(self): - return self._logs_queue.empty() + return not bool(self._logs_queue) + + def put_into_queue(self, _id: str, payload: SystemLogPayload | EventLogPayload): + if self._logs_queue.get(_id) is None: + self._logs_queue[_id] = [] - async def put_into_queue(self, coroutine: Coroutine): - await self._logs_queue.put(coroutine) + self._logs_queue[_id].append(payload) async def process_queue(self): if not self.is_queue_empty(): - coroutine = await self._logs_queue.get() - await coroutine - - async def log(self, client: BotClient, text: str, - disable_notification: bool = True, - reply_markup: ReplyKeyboardMarkup = None, - parse_mode: ParseMode = None): - """Put sending a log into the queue.""" - - await self._logs_queue.put(self.log_instantly(client, text, - disable_notification, - reply_markup, - parse_mode)) - - async def log_instantly(self, client: BotClient, text: str, - disable_notification: bool = True, - reply_markup: ReplyKeyboardMarkup = None, - parse_mode: ParseMode = None): - """Sends log to the log channel instantly.""" + userid = tuple(self._logs_queue)[0] + logged_events = self._logs_queue[userid] + + if userid == SYSTEM: # invoked by system, not user + system_log = logged_events.pop(0) + return await self.send_log(system_log.client, + system_log.text, + system_log.disable_notification, + system_log.reply_markup, + system_log.parse_mode) + + del self._logs_queue[userid] + client = logged_events[-1].client + user = logged_events[-1].user + session = logged_events[-1].session + display_name = f'@{user.username}' if user.username is not None else f'{user.mention} (username hidden)' + + text = [f'👤: {display_name}', + f'ℹ️: {userid}', + f'✈️: {user.language_code}', + f'⚙️: {session.locale.lang_code}', + f'━━━━━━━━━━━━━━━━━━━━━━━━'] + [event.result_text for event in logged_events] + return await self.send_log(client, '\n'.join(text), disable_notification=True) + + async def schedule_system_log(self, client: BotClient, text: str, + disable_notification: bool = True, + reply_markup: ReplyKeyboardMarkup = None, + parse_mode: ParseMode = None): + """Put sending a system log into the queue.""" + + self.put_into_queue(SYSTEM, SystemLogPayload(client, text, disable_notification, reply_markup, parse_mode)) + + async def send_log(self, client: BotClient, text: str, + disable_notification: bool = True, + reply_markup: ReplyKeyboardMarkup = None, + parse_mode: ParseMode = None): + """Sends log to the log channel immediately, avoiding the queue.""" await client.send_message(self.log_channel_id, text, disable_notification=disable_notification, reply_markup=reply_markup, parse_mode=parse_mode) - async def log_message(self, client: BotClient, session: UserSession, message: Message): + async def schedule_message_log(self, client: BotClient, session: UserSession, message: Message): """Put sending a message log into the queue.""" - username = message.from_user.username - display_name = f'@{username}' if username is not None else f'{message.from_user.mention} (username hidden)' + user = message.from_user + message_text = message.text if message.text is not None else "" - text = (f'✍️ User: {display_name}\n' - f'ID: {message.from_user.id}\n' - f'Telegram language: {message.from_user.language_code}\n' - f'Chosen language: {session.locale.lang_code}\n' - f'Private message: "{message.text if message.text is not None else ""}"') - await self._logs_queue.put(client.send_message(self.log_channel_id, text, disable_notification=True)) + self.put_into_queue(str(message.from_user.id), EventLogPayload(client, user, session, f'✍️: "{message_text}"')) - async def log_callback(self, client: BotClient, session: UserSession, callback_query: CallbackQuery): + async def schedule_callback_log(self, client: BotClient, session: UserSession, callback_query: CallbackQuery): """Put sending a callback query log into the queue""" - username = callback_query.from_user.username - display_name = f'@{username}' if username is not None \ - else f'{callback_query.from_user.mention} (username hidden)' + user = callback_query.from_user - text = (f'🔀 User: {display_name}\n' - f'ID: {callback_query.from_user.id}\n' - f'Telegram language: {callback_query.from_user.language_code}\n' - f'Chosen language: {session.locale.lang_code}\n' - f'Callback query: {callback_query.data}') - await self._logs_queue.put(client.send_message(self.log_channel_id, text, disable_notification=True)) + self.put_into_queue(str(user.id), EventLogPayload(client, user, session, f'🔀: {callback_query.data}')) - async def log_inline(self, client: BotClient, session: UserSession, inline_query: InlineQuery): + async def schedule_inline_log(self, client: BotClient, session: UserSession, inline_query: InlineQuery): """Put sending an inline query log into the queue.""" - username = inline_query.from_user.username - display_name = f'@{username}' if username is not None else f'{inline_query.from_user.mention} (username hidden)' + user = inline_query.from_user - text = (f'🛰 User: {display_name}\n' - f'ID: {inline_query.from_user.id}\n' - f'Telegram language: {inline_query.from_user.language_code}\n' - f'Chosen language: {session.locale.lang_code}\n' - f'Inline query: "{inline_query.query}"') - await self._logs_queue.put(client.send_message(self.log_channel_id, text, disable_notification=True)) + self.put_into_queue(str(user.id), EventLogPayload(client, user, session, f'🛰: "{inline_query.query}"')) diff --git a/main.py b/main.py index 651174d..26b778d 100644 --- a/main.py +++ b/main.py @@ -59,7 +59,7 @@ async def handle_exceptions_in_callback(client: BotClient, session: UserSession, bot_message: Message, exc: Exception): logging.exception('Caught exception!', exc_info=exc) await client.log(f'❗️ {"".join(traceback.format_exception(exc))}', - disable_notification=True, parse_mode=ParseMode.DISABLED) + disable_notification=False, parse_mode=ParseMode.DISABLED) return await something_went_wrong(client, session, bot_message) From ea30da3e07937cdd1d802d50ddcc353eb2c6df5d Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Sun, 12 May 2024 02:55:11 +0500 Subject: [PATCH 09/18] Fix conflicted requirement --- requirements.txt | Bin 2980 -> 2982 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3d68268075e89a7a598780e5450533d0dfb2c558..2ce0d8423d392b38217c6492976370a1cd675ce5 100644 GIT binary patch delta 16 XcmZ1?zD#^W9v8DAgTdx}E>lJTE6oIF delta 14 VcmZ1`zC?UO9v7qK=0Yx0MgS!-1U~=( From ac37c24404afeffe76d7dc53ec738e78f9c7fd20 Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Sun, 12 May 2024 03:24:25 +0500 Subject: [PATCH 10/18] Another requirements fix --- requirements.txt | Bin 2982 -> 3058 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2ce0d8423d392b38217c6492976370a1cd675ce5..4eeda5b8d22343833c469aacfd7ddebe99bfccf7 100644 GIT binary patch delta 101 zcmZ1`{z-hpGA3qa28GQ_nY7vcO&F3GOc~68*nlCC!Gs|N$WLTQV@L+F4Hyi8EJHBg k9LP@vs!IWi7y;Fo0Cl7QRat^zDo`#FD3ZMSI_G&t00TS|g8%>k delta 25 hcmew)zD#_>GN#R|n6%j0a~TpDG8ytVD|4M^1ORpt2nzrJ From 52a9ecdfa7035d49030367740e6334db5c8ee318 Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Mon, 13 May 2024 22:17:20 +0500 Subject: [PATCH 11/18] Bye env.py, better Discord text wrapping, some incs2chat.py cleanup --- collectors/core.py | 2 - collectors/env.py | 15 ----- collectors/game_coordinator.py | 2 - collectors/gc_alerter.py | 2 - collectors/online_players_graph.py | 2 - plugins/env.py | 15 ----- plugins/incs2chat.py | 98 ++++++++++++++++++------------ plugins/inline.py | 2 - 8 files changed, 58 insertions(+), 80 deletions(-) delete mode 100644 collectors/env.py delete mode 100644 plugins/env.py diff --git a/collectors/core.py b/collectors/core.py index fc42eb1..bb61a39 100644 --- a/collectors/core.py +++ b/collectors/core.py @@ -13,8 +13,6 @@ uvloop.install() -# noinspection PyUnresolvedReferences -import env import config from functions import utime from l10n import locale diff --git a/collectors/env.py b/collectors/env.py deleted file mode 100644 index 5e3f142..0000000 --- a/collectors/env.py +++ /dev/null @@ -1,15 +0,0 @@ -from pathlib import Path -import sys - - -project_dir = Path(__file__).parent.parent.absolute() -functions_dir = project_dir / 'functions' -l10n_dir = project_dir / 'l10n' -plugins_dir = project_dir / 'plugins' -scrapers_dir = project_dir / 'scrapers' -utypes_dir = project_dir / 'utypes' - -all_dirs = (project_dir, functions_dir, l10n_dir, plugins_dir, scrapers_dir, utypes_dir) - -for i, path in enumerate(all_dirs): - sys.path.insert(i + 1, str(path)) diff --git a/collectors/game_coordinator.py b/collectors/game_coordinator.py index 35c5257..6060d1c 100644 --- a/collectors/game_coordinator.py +++ b/collectors/game_coordinator.py @@ -18,8 +18,6 @@ uvloop.install() -# noinspection PyUnresolvedReferences -import env import config from functions import utime from utypes import GameVersion, States, SteamWebAPI diff --git a/collectors/gc_alerter.py b/collectors/gc_alerter.py index 59db9a6..9d0bf81 100644 --- a/collectors/gc_alerter.py +++ b/collectors/gc_alerter.py @@ -10,8 +10,6 @@ uvloop.install() -# noinspection PyUnresolvedReferences -import env import config from functions import locale diff --git a/collectors/online_players_graph.py b/collectors/online_players_graph.py index ee8e065..8e0589d 100644 --- a/collectors/online_players_graph.py +++ b/collectors/online_players_graph.py @@ -13,8 +13,6 @@ import seaborn as sns from telegraph import Telegraph -# noinspection PyUnresolvedReferences -import env import config from functions import utime diff --git a/plugins/env.py b/plugins/env.py deleted file mode 100644 index 5e3f142..0000000 --- a/plugins/env.py +++ /dev/null @@ -1,15 +0,0 @@ -from pathlib import Path -import sys - - -project_dir = Path(__file__).parent.parent.absolute() -functions_dir = project_dir / 'functions' -l10n_dir = project_dir / 'l10n' -plugins_dir = project_dir / 'plugins' -scrapers_dir = project_dir / 'scrapers' -utypes_dir = project_dir / 'utypes' - -all_dirs = (project_dir, functions_dir, l10n_dir, plugins_dir, scrapers_dir, utypes_dir) - -for i, path in enumerate(all_dirs): - sys.path.insert(i + 1, str(path)) diff --git a/plugins/incs2chat.py b/plugins/incs2chat.py index 8e28533..21fa3a6 100644 --- a/plugins/incs2chat.py +++ b/plugins/incs2chat.py @@ -5,15 +5,22 @@ from pyrogram import Client, filters from pyrogram.enums import ChatMembersFilter -from pyrogram.types import Message, MessageEntity +from pyrogram.types import Chat, Message, MessageEntity, User import requests from tgentity import to_md -# noinspection PyUnresolvedReferences -import env import config +DISCORD_MESSAGE_LENGTH_LIMIT = 2000 + + +async def is_administrator(chat: Chat, user: User) -> bool: + admins = {admin.user async for admin in chat.get_members(filter=ChatMembersFilter.ADMINISTRATORS)} + + return user in admins + + def correct_message_entities(entities: list[MessageEntity] | None, original_text: str, new_text: str) -> list[MessageEntity] | None: """Correct message entities (a.k.a. Markdown formatting) for edited text.""" @@ -46,6 +53,45 @@ def to_discord_markdown(message: Message) -> str: return text +def wrap_text(text: str, max_length: int) -> list[str]: + """ + Wraps the given text into multiple sections with a length <= ``max_length``. + + Prioritises wrapping by newlines, then spaces. + """ + + if len(text) <= max_length: + return [text] + + text_parts = [] + while len(text) > max_length: + longest_possible_part = text[:max_length] + last_char_index = longest_possible_part.rfind('\n') + if last_char_index == -1: + last_char_index = longest_possible_part.rfind(' ') + if last_char_index == -1: + last_char_index = max_length + + new_part = text[:last_char_index] + text_parts.append(new_part) + if text[last_char_index].isspace(): + last_char_index += 1 + text = text[last_char_index:] + + text_parts.append(text) + + return text_parts + + +def process_discord_text(message: Message) -> list[str]: + text = to_discord_markdown(message) if message.entities else message.text + + # fixme: can break formatting if wrapping happens in the middle of formatted section + # fixme: (e.g. "**some [split] wise words**") + # fixme severity: low + return wrap_text(text, DISCORD_MESSAGE_LENGTH_LIMIT) + + def translate_text(text: str, source_lang: str = 'RU', target_lang: str = 'EN') -> str | None: headers = {'Authorization': f'DeepL-Auth-Key {config.DEEPL_TOKEN}', 'Content-Type': 'application/json'} data = {'text': [text], 'source_lang': source_lang, 'target_lang': target_lang} @@ -70,23 +116,6 @@ def post_to_discord_webhook(url: str, text: str): # todo: attachments support? logging.error(f'{r.status_code=}, {r.reason=}') -def process_discord_text(message: Message) -> list[str]: - logging.info(message) - - text_list = [to_discord_markdown(message) if message.entities else message.text] - - # Discord webhook limit is 2000 symbols (@akimerslys: ahui), hoping Telegram limit is <4000 - if len(text_list[0]) > 2000: - # splitting message into 2 similar-sized messages to save structure and beauty - last_newline_index = text_list[0][:len(text_list[0]) // 2].rfind('\n') - if last_newline_index != -1: - second_part = text_list[0][last_newline_index + 1:] - text_list.append(second_part) - text_list[0] = text_list[0][:last_newline_index] - - return text_list - - def forward_to_discord(message: Message): texts = process_discord_text(message) logging.info(texts) @@ -115,33 +144,29 @@ async def ban(client: Client, message: Message): return await message.reply("Эта команда недоступна, Вы не являетесь разработчиком Valve.") if message.reply_to_message: - og_msg = message.reply_to_message - await chat.ban_member(og_msg.from_user.id) - await message.reply(f"{og_msg.from_user.first_name} получил(а) VAC бан.") + original_msg = message.reply_to_message + await chat.ban_member(original_msg.from_user.id) + await message.reply(f"{original_msg.from_user.first_name} получил(а) VAC бан.") @Client.on_message(filters.chat(config.INCS2CHAT) & filters.command("unban")) async def unban(client: Client, message: Message): chat = await client.get_chat(config.INCS2CHAT) - admins = chat.get_members(filter=ChatMembersFilter.ADMINISTRATORS) - admins = {admin.user.id async for admin in admins} - if message.from_user.id not in admins: + if not await is_administrator(chat, message.from_user): return await message.reply("Эта команда недоступна, Вы не являетесь разработчиком Valve.") if message.reply_to_message: - og_msg = message.reply_to_message - await chat.unban_member(og_msg.from_user.id) - await message.reply(f"VAC бан у {og_msg.from_user.first_name} был удалён.") + original_msg = message.reply_to_message + await chat.unban_member(original_msg.from_user.id) + await message.reply(f"VAC бан у {original_msg.from_user.first_name} был удалён.") @Client.on_message(filters.chat(config.INCS2CHAT) & filters.command("warn")) async def warn(client: Client, message: Message): chat = await client.get_chat(config.INCS2CHAT) - admins = chat.get_members(filter=ChatMembersFilter.ADMINISTRATORS) - admins = {admin.user.id async for admin in admins} - if message.from_user.id not in admins: + if not await is_administrator(chat, message.from_user): return await message.reply("Эта команда недоступна, Вы не являетесь разработчиком Valve.") if message.reply_to_message: @@ -153,11 +178,9 @@ async def warn(client: Client, message: Message): @Client.on_message(filters.chat(config.INCS2CHAT) & filters.command('echo')) async def echo(client: Client, message: Message): chat = await client.get_chat(config.INCS2CHAT) - admins = chat.get_members(filter=ChatMembersFilter.ADMINISTRATORS) - admins = {admin.user.id async for admin in admins} await message.delete() - if message.from_user.id not in admins: + if not await is_administrator(chat, message.from_user): return if message.reply_to_message: @@ -229,8 +252,3 @@ async def meow_meow_meow_meow(_, message: Message): if message.sticker.file_unique_id == 'AgADtD0AAu4r4Ug' and chance < 5: await message.reply('мяу мяу мяу мяу') - - -# @Client.on_message(filters.chat(config.INCS2CHAT) & filters.animation) -# async def debugging_gifs(_, message: Message): -# print(message.animation) diff --git a/plugins/inline.py b/plugins/inline.py index dbd6ae6..ad558c0 100644 --- a/plugins/inline.py +++ b/plugins/inline.py @@ -5,8 +5,6 @@ from pyrogram.enums import ParseMode from pyrogram.types import InlineQuery, InlineQueryResultArticle, InputTextMessageContent -# noinspection PyUnresolvedReferences -import env from bottypes import BotClient, UserSession import config from functions import datacenter_handlers, info_formatters From 1c61736ccfd09456b8055db81aae31c842e3d64f Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Mon, 13 May 2024 22:22:03 +0500 Subject: [PATCH 12/18] Removed undefined ARS and TRY from currencies list --- l10n/data/be.json | 8 +- l10n/data/en.json | 8 +- l10n/data/fa.json | 2 - l10n/data/it.json | 8 +- l10n/data/ru.json | 4 +- l10n/data/tags.json | 16 +- l10n/data/tr.json | 8 +- l10n/data/uk.json | 8 +- l10n/data/uz.json | 8 +- l10n/tags.py | 418 ++++++++++++++++++++++---------------------- utypes/game_data.py | 28 +-- 11 files changed, 244 insertions(+), 272 deletions(-) diff --git a/l10n/data/be.json b/l10n/data/be.json index cea038d..a6efa95 100644 --- a/l10n/data/be.json +++ b/l10n/data/be.json @@ -207,7 +207,6 @@ "🇹🇭 THB: ฿ {}", "🇻🇳 VND: {} ₫", "🇰🇷 KRW: ₩ {}", - "🇹🇷 TRY: ₺ {}", "🇺🇦 UAH: {} ₴", "🇲🇽 MXN: Mex$ {}", "🇨🇦 CAD: CDN$ {}", @@ -226,7 +225,6 @@ "🇿🇦 ZAR: R {}", "🇮🇳 INR: ₹ {}", "🇨🇷 CRC: ₡ {}", - "🇦🇷 ARS: ARS$ {}", "🇮🇱 ILS: ₪ {}", "🇰🇼 KWD: {} KD", "🇶🇦 QAR: {} QR", @@ -245,10 +243,10 @@ "(напрыклад, `@INCS2bot price rub` або `@INCS2bot price рубель`)" ], "exchangerate_inline_title_selected": "Паглядзець кошт ключа ў {}", - "exchangerate_inline_description_selected": "1x Ключ ад кейса ў Counter-Strike каштуе {} {}", - "exchangerate_inline_text_selected": "1x Ключ ад кейса ў Counter-Strike каштуе {} {}", + "exchangerate_inline_description_selected": "1x Ключ ад кейса ў Counter-Strike каштуе {} {}.", + "exchangerate_inline_text_selected": "1x Ключ ад кейса ў Counter-Strike каштуе {} {}.", "exchangerate_inline_title_notfound": "Нічога не знойдзена!", - "exchangerate_inline_description_notfound": "Націсніце сюды, каб адправіць усе даступныя валюты", + "exchangerate_inline_description_notfound": "Націсніце сюды, каб адправіць усе даступныя валюты.", "game_status_button_title": "Стан сервераў", "game_status_inline_title": "Стан сервераў", "game_status_inline_description": "Праверыць даступнасць сервераў", diff --git a/l10n/data/en.json b/l10n/data/en.json index bcd9eaf..823043c 100644 --- a/l10n/data/en.json +++ b/l10n/data/en.json @@ -207,7 +207,6 @@ "🇹🇭 THB: ฿ {}", "🇻🇳 VND: {} ₫", "🇰🇷 KRW: ₩ {}", - "🇹🇷 TRY: ₺ {}", "🇺🇦 UAH: {} ₴", "🇲🇽 MXN: Mex$ {}", "🇨🇦 CAD: CDN$ {}", @@ -225,7 +224,6 @@ "🇭🇰 HKD: HK$ {}", "🇿🇦 ZAR: R {}", "🇮🇳 INR: ₹ {}", - "🇦🇷 ARS: ARS$ {}", "🇨🇷 CRC: ₡ {}", "🇮🇱 ILS: ₪ {}", "🇰🇼 KWD: {} KD", @@ -244,10 +242,10 @@ "(for example, `@INCS2bot price eur` or `@INCS2bot price dollar`)" ], "exchangerate_inline_title_selected": "Check key price in {}", - "exchangerate_inline_description_selected": "1x Counter-Strike case key is {} {}", - "exchangerate_inline_text_selected": "1x Counter-Strike case key is {} {}", + "exchangerate_inline_description_selected": "1x Counter-Strike case key is {} {}.", + "exchangerate_inline_text_selected": "1x Counter-Strike case key is {} {}.", "exchangerate_inline_title_notfound": "Nothing found!", - "exchangerate_inline_description_notfound": "Tap here to send all the available currencies", + "exchangerate_inline_description_notfound": "Tap here to send all the available currencies.", "game_status_button_title": "Server status", "game_status_inline_title": "Server status", "game_status_inline_description": "Check the availability of the servers", diff --git a/l10n/data/fa.json b/l10n/data/fa.json index 266a9b8..bc9c913 100644 --- a/l10n/data/fa.json +++ b/l10n/data/fa.json @@ -207,7 +207,6 @@ "🇹🇭 THB: ฿ {}", "🇻🇳 VND: {} ₫", "🇰🇷 KRW: ₩ {}", - "🇹🇷 TRY: ₺ {}", "🇺🇦 UAH: {} ₴", "🇲🇽 MXN: Mex$ {}", "🇨🇦 CAD: CDN$ {}", @@ -225,7 +224,6 @@ "🇭🇰 HKD: HK$ {}", "🇿🇦 ZAR: R {}", "🇮🇳 INR: ₹ {}", - "🇦🇷 ARS: ARS$ {}", "🇨🇷 CRC: ₡ {}", "🇮🇱 ILS: ₪ {}", "🇰🇼 KWD: {} KD", diff --git a/l10n/data/it.json b/l10n/data/it.json index ef31e81..27076eb 100644 --- a/l10n/data/it.json +++ b/l10n/data/it.json @@ -207,7 +207,6 @@ "🇹🇭 THB: ฿ {}", "🇻🇳 VND: {} ₫", "🇰🇷 KRW: ₩ {}", - "🇹🇷 TRY: ₺ {}", "🇺🇦 UAH: {} ₴", "🇲🇽 MXN: Mex$ {}", "🇨🇦 CAD: CDN$ {}", @@ -225,7 +224,6 @@ "🇭🇰 HKD: HK$ {}", "🇿🇦 ZAR: R {}", "🇮🇳 INR: ₹ {}", - "🇦🇷 ARS: ARS$ {}", "🇨🇷 CRC: ₡ {}", "🇮🇱 ILS: ₪ {}", "🇰🇼 KWD: {} KD", @@ -244,10 +242,10 @@ "(ad esempio, `@INCS2bot price eur` o `@INCS2bot price dollaro`)" ], "exchangerate_inline_title_selected": "Controlla il prezzo della chiave in {}", - "exchangerate_inline_description_selected": "1x chiave di una cassa di Counter-Strike equivale a {} {}", - "exchangerate_inline_text_selected": "1x chiave di una cassa di Counter-Strike equivale a {} {}", + "exchangerate_inline_description_selected": "1x chiave di una cassa di Counter-Strike equivale a {} {}.", + "exchangerate_inline_text_selected": "1x chiave di una cassa di Counter-Strike equivale a {} {}.", "exchangerate_inline_title_notfound": "Nulla è stato trovato!", - "exchangerate_inline_description_notfound": "Tocca qui per ricevere tutte le valute disponibili", + "exchangerate_inline_description_notfound": "Tocca qui per ricevere tutte le valute disponibili.", "game_status_button_title": "Stato dei server", "game_status_inline_title": "Stato dei server", "game_status_inline_description": "Controlla la disponibilità dei server", diff --git a/l10n/data/ru.json b/l10n/data/ru.json index 195f1ca..305c162 100644 --- a/l10n/data/ru.json +++ b/l10n/data/ru.json @@ -207,7 +207,6 @@ "🇹🇭 THB: ฿ {}", "🇻🇳 VND: {} ₫", "🇰🇷 KRW: ₩ {}", - "🇹🇷 TRY: ₺ {}", "🇺🇦 UAH: {} ₴", "🇲🇽 MXN: Mex$ {}", "🇨🇦 CAD: CDN$ {}", @@ -226,7 +225,6 @@ "🇿🇦 ZAR: R {}", "🇮🇳 INR: ₹ {}", "🇨🇷 CRC: ₡ {}", - "🇦🇷 ARS: ARS$ {}", "🇮🇱 ILS: ₪ {}", "🇰🇼 KWD: {} KD", "🇶🇦 QAR: {} QR", @@ -248,7 +246,7 @@ "exchangerate_inline_description_selected": "1x Ключ от кейса в Counter-Strike стоит {} {}", "exchangerate_inline_text_selected": "1x Ключ от кейса в Counter-Strike стоит {} {}", "exchangerate_inline_title_notfound": "Ничего не найдено!", - "exchangerate_inline_description_notfound": "Нажмите сюда, чтобы отправить все доступные валюты", + "exchangerate_inline_description_notfound": "Нажмите сюда, чтобы отправить все доступные валюты.", "game_status_button_title": "Состояние серверов", "game_status_inline_title": "Состояние серверов", "game_status_inline_description": "Проверить доступность серверов", diff --git a/l10n/data/tags.json b/l10n/data/tags.json index 5e57b51..8845d5b 100644 --- a/l10n/data/tags.json +++ b/l10n/data/tags.json @@ -489,13 +489,6 @@ "von", "وان" ], - "currencies_try": [ - "try", - "lira", - "лира", - "ліра", - "لیر" - ], "currencies_uah": [ "uah", "hrywna", @@ -629,13 +622,6 @@ "روپیه", "rupia" ], - "currencies_ars": [ - "ars", - "peso", - "песо", - "песа", - "نکو" - ], "currencies_crc": [ "crc", "colon", @@ -684,4 +670,4 @@ "тэнге", "قزاق" ] -} +} \ No newline at end of file diff --git a/l10n/data/tr.json b/l10n/data/tr.json index 6706c95..688bd79 100644 --- a/l10n/data/tr.json +++ b/l10n/data/tr.json @@ -207,7 +207,6 @@ "🇹🇭 THB: ฿ {}", "🇻🇳 VND: {} ₫", "🇰🇷 KRW: ₩ {}", - "🇹🇷 TRY: ₺ {}", "🇺🇦 UAH: {} ₴", "🇲🇽 MXN: Mex$ {}", "🇨🇦 CAD: CDN$ {}", @@ -226,7 +225,6 @@ "🇿🇦 ZAR: R {}", "🇮🇳 INR: ₹ {}", "🇨🇷 CRC: ₡ {}", - "🇦🇷 ARS: ARS$ {}", "🇮🇱 ILS: ₪ {}", "🇰🇼 KWD: {} KD", "🇶🇦 QAR: {} QR", @@ -245,10 +243,10 @@ "(ör., `@INCS2bot price rub` veya `@INCS2bot price ruble`)" ], "exchangerate_inline_title_selected": "anahtarın {} maliyetini öğrenin", - "exchangerate_inline_description_selected": "1x Counter-Strike Case Anahtar maliyeti {} {}", - "exchangerate_inline_text_selected": "1x Counter-Strike Case Anahtar maliyeti {} {}", + "exchangerate_inline_description_selected": "1x Counter-Strike Case Anahtar maliyeti {} {}.", + "exchangerate_inline_text_selected": "1x Counter-Strike Case Anahtar maliyeti {} {}.", "exchangerate_inline_title_notfound": "Hiçbirşey bulunamadı!", - "exchangerate_inline_description_notfound": "Mevcut tüm para birimlerini göndermek için burayı basın", + "exchangerate_inline_description_notfound": "Mevcut tüm para birimlerini göndermek için burayı basın.", "game_status_button_title": "Sunucu durumu", "game_status_inline_title": "Sunucu durumu", "game_status_inline_description": "Sunucu kullanılabilirliğini kontrol etmek", diff --git a/l10n/data/uk.json b/l10n/data/uk.json index c1db88d..38c00d8 100644 --- a/l10n/data/uk.json +++ b/l10n/data/uk.json @@ -207,7 +207,6 @@ "🇹🇭 THB: ฿ {}", "🇻🇳 VND: {} ₫", "🇰🇷 KRW: ₩ {}", - "🇹🇷 TRY: ₺ {}", "🇺🇦 UAH: {} ₴", "🇲🇽 MXN: Mex$ {}", "🇨🇦 CAD: CDN$ {}", @@ -226,7 +225,6 @@ "🇿🇦 ZAR: R {}", "🇮🇳 INR: ₹ {}", "🇨🇷 CRC: ₡ {}", - "🇦🇷 ARS: ARS$ {}", "🇮🇱 ILS: ₪ {}", "🇰🇼 KWD: {} KD", "🇶🇦 QAR: {} QR", @@ -245,10 +243,10 @@ "(наприклад, `@INCS2bot price uah гривня` чи `@INCS2bot price гривня`)" ], "exchangerate_inline_title_selected": "Дізнатись ціну ключа в {}", - "exchangerate_inline_description_selected": "1x Ключ від кейсу в Counter-Strike коштує {} {}", - "exchangerate_inline_text_selected": "1x Ключ від кейса в Counter-Strike коштує {} {}", + "exchangerate_inline_description_selected": "1x Ключ від кейсу в Counter-Strike коштує {} {}.", + "exchangerate_inline_text_selected": "1x Ключ від кейса в Counter-Strike коштує {} {}.", "exchangerate_inline_title_notfound": "Нічого не знайдено!", - "exchangerate_inline_description_notfound": "Натисніть сюди, щоб дізнатись усі доступні валюти", + "exchangerate_inline_description_notfound": "Натисніть сюди, щоб дізнатись усі доступні валюти.", "game_status_button_title": "Стан серверів", "game_status_inline_title": "Стан серверів", "game_status_inline_description": "Перевірити доступність серверів", diff --git a/l10n/data/uz.json b/l10n/data/uz.json index acbba88..676708a 100644 --- a/l10n/data/uz.json +++ b/l10n/data/uz.json @@ -207,7 +207,6 @@ "🇹🇭 THB: ฿ {}", "🇻🇳 VND: {} ₫", "🇰🇷 KRW: ₩ {}", - "🇹🇷 TRY: ₺ {}", "🇺🇦 UAH: {} ₴", "🇲🇽 MXN: Mex$ {}", "🇨🇦 CAD: CDN$ {}", @@ -226,7 +225,6 @@ "🇿🇦 ZAR: R {}", "🇮🇳 INR: ₹ {}", "🇨🇷 CRC: ₡ {}", - "🇦🇷 ARS: ARS$ {}", "🇮🇱 ILS: ₪ {}", "🇰🇼 KWD: {} KD", "🇶🇦 QAR: {} QR", @@ -245,10 +243,10 @@ "(masalan, `@INCS2bot price rub` или `@INCS2bot price ruble`)" ], "exchangerate_inline_title_selected": "Kalit narxini ko'ring {}", - "exchangerate_inline_description_selected": "1x Counter-Strike ko'krak kaliti xarajatlari {} {}", - "exchangerate_inline_text_selected": "1x Counter-Strike ko'krak kaliti xarajatlari {} {}", + "exchangerate_inline_description_selected": "1x Counter-Strike ko'krak kaliti xarajatlari {} {}.", + "exchangerate_inline_text_selected": "1x Counter-Strike ko'krak kaliti xarajatlari {} {}.", "exchangerate_inline_title_notfound": "Hech narsa topilmadi!", - "exchangerate_inline_description_notfound": "Barcha mavjud valyutalarni yuborish uchun shu yerni bosing", + "exchangerate_inline_description_notfound": "Barcha mavjud valyutalarni yuborish uchun shu yerni bosing.", "game_status_button_title": "Server holati", "game_status_inline_title": "Server holati", "game_status_inline_description": "Server mavjudligini tekshiring", diff --git a/l10n/tags.py b/l10n/tags.py index e89eb3e..6d1dec1 100644 --- a/l10n/tags.py +++ b/l10n/tags.py @@ -1,210 +1,208 @@ -from __future__ import annotations -import json -from pathlib import Path -from typing import NamedTuple -import warnings - - -__all__ = ('Tags', 'TagsKeys', 'dump_tags') - -# todo: probably rewrite it cuz it's basically a L10n clone - - -class UnexpectedTagKey(UserWarning): - pass - - -class UndefinedTagKey(UserWarning): - pass - - -class PrimaryTagsFileNotFound(UserWarning): - pass - - -class Tags(NamedTuple): - """Object containing all the tags required by Telegram API. Tags - can be accessed as object attributes or by string keys using - get(key) method. Can be converted to dict using to_dict() method.""" - - dc_africa: list # South Africa - dc_australia: list # Australia - - dc_southamerica: list # South America - dc_southamerica_argentina: list # Argentina - dc_southamerica_brazil: list # Brazil - dc_southamerica_chile: list # Chile - dc_southamerica_peru: list # Peru - - dc_europe: list # Europe - dc_europe_austria: list # Austria - dc_europe_finland: list # Finland - dc_europe_germany: list # Germany - dc_europe_netherlands: list # Netherlands - dc_europe_poland: list # Poland - dc_europe_spain: list # Spain - dc_europe_sweden: list # Sweden - - dc_us: list # USA - dc_us_east: list # East - dc_us_west: list # West - - dc_asia: list # Asia - dc_asia_india: list # India - dc_asia_japan: list # Japan - dc_asia_china: list # China - dc_asia_emirates: list # Emirates - dc_asia_singapore: list # Singapore - dc_asia_hongkong: list # Hong Kong - dc_asia_southkorea: list # South Korea - - currencies_usd: list # U.S. Dollar - currencies_gbp: list # British Pound - currencies_eur: list # Euro - currencies_rub: list # Russian Ruble - currencies_brl: list # Brazilian Real - currencies_jpy: list # Japanese Yen - currencies_nok: list # Norwegian Krone - currencies_idr: list # Indonesian Rupiah - currencies_myr: list # Malaysian Ringgit - currencies_php: list # Philippine Peso - currencies_sgd: list # Singapore Dollar - currencies_thb: list # Thai Baht - currencies_vnd: list # Vietnamese Dong - currencies_krw: list # South Korean Won - currencies_try: list # Turkish Lira - currencies_uah: list # Ukrainian Hryvnia - currencies_mxn: list # Mexican Peso - currencies_cad: list # Canadian Dollar - currencies_aud: list # Australian Dollar - currencies_nzd: list # New Zealand Dollar - currencies_pln: list # Polish Zloty - currencies_chf: list # Swiss Franc - currencies_aed: list # U.A.E. Dirham - currencies_clp: list # Chilean Peso - currencies_cny: list # Chinese Yuan - currencies_cop: list # Colombian Peso - currencies_pen: list # Peruvian Sol - currencies_sar: list # Saudi Riyal - currencies_twd: list # Taiwan Dollar - currencies_hkd: list # Hong Kong Dollar - currencies_zar: list # South African Rand - currencies_inr: list # Indian Rupee - currencies_ars: list # Argentine Peso - currencies_crc: list # Costa Rican Colon - currencies_ils: list # Israeli Shekel - currencies_kwd: list # Kuwaiti Dinar - currencies_qar: list # Qatari Riyal - currencies_uyu: list # Uruguayan Peso - currencies_kzt: list # Kazakhstani Tenge - - def to_dict(self) -> dict[str, list]: - """Returns a dict converted from a Tags object.""" - - return self._asdict() - - def to_set(self) -> set: - """Returns a set converted from a Tags object.""" - - result = set() - for tags in self._asdict().values(): - result.update(tags) - return set(result) - - def dcs_to_set(self) -> set: - """Returns a datacenters set of converted from a Tags object.""" - - result = set() - for k, tags in self._asdict().items(): - if k.startswith('dc'): - result.update(tags) - return set(result) - - def to_list(self) -> list: - """Returns a list converted from a Tags object.""" - - result = [] - for tags in self._asdict().values(): - result.extend(tags) - return result - - def currencies_to_list(self) -> list: - """Returns a currency list converted from a Tags object.""" - - result = [] - for k, tags in self._asdict().items(): - if k.startswith('currencies'): - result.extend(tags) - return result - - def currencies_to_dict(self) -> dict: - """Returns a set converted from a Tags object.""" - - result = {} - for k, tags in self._asdict().items(): - if k.startswith('currencies'): - result[tags[0]] = tags - return result - - def get(self, key: str) -> str: - """Returns tags list associated with the given key (if such - key exists, otherwise returns the key itself).""" - - if key not in self._fields: - warnings.warn(f'Got unexpected key "{key}", returned the key', UnexpectedTagKey, stacklevel=2) - return key - return getattr(self, key) - - @classmethod - def sample(cls) -> Tags: - """Returns a sample Tags object with key names as values""" - return cls(**{field: field for field in cls._fields}) - - -TagsKeys = Tags.sample() - - -def dump_tags() -> Tags: - """Dumps "tags.json" and returns Tags object, containing all defined tags lists. - """ - path = Path(__file__).parent / 'data' / 'tags.json' - if not path.exists(): - warnings.warn(f"Can't find tags.json, generating a file...", PrimaryTagsFileNotFound) - sample = Tags.sample() - with open(path, 'w', encoding='utf-8') as f: - json.dump({k: [v] for k, v in sample.to_dict().items()}, f, indent=4, ensure_ascii=False) - - with open(path, encoding='utf-8') as f: - data = json.load(f) - - # Add undefined fields - found_undefined_fields = False - for field in Tags._fields: - if field not in data: - warnings.warn(f'Found undefined tags field "{field}" in "tags.json"', UndefinedTagKey, stacklevel=2) - data[field] = [field] - found_undefined_fields = True - - # Find unexpected fields - unexpected_fields = [] - for field in tuple(data): - if field not in Tags._fields: - warnings.warn(f'Got unexpected tags field "{field}" in "tags.json"', UnexpectedTagKey, stacklevel=2) - unexpected_fields.append(field) - - # Dump data with undefined and unexpected fields - if found_undefined_fields or unexpected_fields: - with open(path, 'w', encoding='utf-8') as f: - data = {field: data[field] for field in Tags._fields + tuple(unexpected_fields)} # fixing pairs order - json.dump(data, f, indent=4, ensure_ascii=False) - - # Remove unexpected fields - for field in unexpected_fields: - del data[field] - - for og_field, og_tags in data.items(): - for field, tags in data.items(): - if og_field != field and field.startswith(og_field): - data[field].extend(og_tags) - - return Tags(**data) +from __future__ import annotations +import json +from pathlib import Path +from typing import NamedTuple +import warnings + + +__all__ = ('Tags', 'TagsKeys', 'dump_tags') + +# todo: probably rewrite it cuz it's basically a L10n clone + + +class UnexpectedTagKey(UserWarning): + pass + + +class UndefinedTagKey(UserWarning): + pass + + +class PrimaryTagsFileNotFound(UserWarning): + pass + + +class Tags(NamedTuple): + """Object containing all the tags required by Telegram API. Tags + can be accessed as object attributes or by string keys using + get(key) method. Can be converted to dict using to_dict() method.""" + + dc_africa: list # South Africa + dc_australia: list # Australia + + dc_southamerica: list # South America + dc_southamerica_argentina: list # Argentina + dc_southamerica_brazil: list # Brazil + dc_southamerica_chile: list # Chile + dc_southamerica_peru: list # Peru + + dc_europe: list # Europe + dc_europe_austria: list # Austria + dc_europe_finland: list # Finland + dc_europe_germany: list # Germany + dc_europe_netherlands: list # Netherlands + dc_europe_poland: list # Poland + dc_europe_spain: list # Spain + dc_europe_sweden: list # Sweden + + dc_us: list # USA + dc_us_east: list # East + dc_us_west: list # West + + dc_asia: list # Asia + dc_asia_india: list # India + dc_asia_japan: list # Japan + dc_asia_china: list # China + dc_asia_emirates: list # Emirates + dc_asia_singapore: list # Singapore + dc_asia_hongkong: list # Hong Kong + dc_asia_southkorea: list # South Korea + + currencies_usd: list # U.S. Dollar + currencies_gbp: list # British Pound + currencies_eur: list # Euro + currencies_rub: list # Russian Ruble + currencies_brl: list # Brazilian Real + currencies_jpy: list # Japanese Yen + currencies_nok: list # Norwegian Krone + currencies_idr: list # Indonesian Rupiah + currencies_myr: list # Malaysian Ringgit + currencies_php: list # Philippine Peso + currencies_sgd: list # Singapore Dollar + currencies_thb: list # Thai Baht + currencies_vnd: list # Vietnamese Dong + currencies_krw: list # South Korean Won + currencies_uah: list # Ukrainian Hryvnia + currencies_mxn: list # Mexican Peso + currencies_cad: list # Canadian Dollar + currencies_aud: list # Australian Dollar + currencies_nzd: list # New Zealand Dollar + currencies_pln: list # Polish Zloty + currencies_chf: list # Swiss Franc + currencies_aed: list # U.A.E. Dirham + currencies_clp: list # Chilean Peso + currencies_cny: list # Chinese Yuan + currencies_cop: list # Colombian Peso + currencies_pen: list # Peruvian Sol + currencies_sar: list # Saudi Riyal + currencies_twd: list # Taiwan Dollar + currencies_hkd: list # Hong Kong Dollar + currencies_zar: list # South African Rand + currencies_inr: list # Indian Rupee + currencies_crc: list # Costa Rican Colon + currencies_ils: list # Israeli Shekel + currencies_kwd: list # Kuwaiti Dinar + currencies_qar: list # Qatari Riyal + currencies_uyu: list # Uruguayan Peso + currencies_kzt: list # Kazakhstani Tenge + + def to_dict(self) -> dict[str, list]: + """Returns a dict converted from a Tags object.""" + + return self._asdict() + + def to_set(self) -> set: + """Returns a set converted from a Tags object.""" + + result = set() + for tags in self._asdict().values(): + result.update(tags) + return set(result) + + def dcs_to_set(self) -> set: + """Returns a datacenters set of converted from a Tags object.""" + + result = set() + for k, tags in self._asdict().items(): + if k.startswith('dc'): + result.update(tags) + return set(result) + + def to_list(self) -> list: + """Returns a list converted from a Tags object.""" + + result = [] + for tags in self._asdict().values(): + result.extend(tags) + return result + + def currencies_to_list(self) -> list: + """Returns a currency list converted from a Tags object.""" + + result = [] + for k, tags in self._asdict().items(): + if k.startswith('currencies'): + result.extend(tags) + return result + + def currencies_to_dict(self) -> dict: + """Returns a set converted from a Tags object.""" + + result = {} + for k, tags in self._asdict().items(): + if k.startswith('currencies'): + result[tags[0]] = tags + return result + + def get(self, key: str) -> str: + """Returns tags list associated with the given key (if such + key exists, otherwise returns the key itself).""" + + if key not in self._fields: + warnings.warn(f'Got unexpected key "{key}", returned the key', UnexpectedTagKey, stacklevel=2) + return key + return getattr(self, key) + + @classmethod + def sample(cls) -> Tags: + """Returns a sample Tags object with key names as values""" + return cls(**{field: field for field in cls._fields}) + + +TagsKeys = Tags.sample() + + +def dump_tags() -> Tags: + """Dumps "tags.json" and returns Tags object, containing all defined tags lists. + """ + path = Path(__file__).parent / 'data' / 'tags.json' + if not path.exists(): + warnings.warn(f"Can't find tags.json, generating a file...", PrimaryTagsFileNotFound) + sample = Tags.sample() + with open(path, 'w', encoding='utf-8') as f: + json.dump({k: [v] for k, v in sample.to_dict().items()}, f, indent=4, ensure_ascii=False) + + with open(path, encoding='utf-8') as f: + data = json.load(f) + + # Add undefined fields + found_undefined_fields = False + for field in Tags._fields: + if field not in data: + warnings.warn(f'Found undefined tags field "{field}" in "tags.json"', UndefinedTagKey, stacklevel=2) + data[field] = [field] + found_undefined_fields = True + + # Find unexpected fields + unexpected_fields = [] + for field in tuple(data): + if field not in Tags._fields: + warnings.warn(f'Got unexpected tags field "{field}" in "tags.json"', UnexpectedTagKey, stacklevel=2) + unexpected_fields.append(field) + + # Dump data with undefined and unexpected fields + if found_undefined_fields or unexpected_fields: + with open(path, 'w', encoding='utf-8') as f: + data = {field: data[field] for field in Tags._fields + tuple(unexpected_fields)} # fixing pairs order + json.dump(data, f, indent=4, ensure_ascii=False) + + # Remove unexpected fields + for field in unexpected_fields: + del data[field] + + for og_field, og_tags in data.items(): + for field, tags in data.items(): + if og_field != field and field.startswith(og_field): + data[field].extend(og_tags) + + return Tags(**data) diff --git a/utypes/game_data.py b/utypes/game_data.py index 536e14b..8c9c420 100644 --- a/utypes/game_data.py +++ b/utypes/game_data.py @@ -77,7 +77,6 @@ class ExchangeRateData(NamedTuple): THB: float VND: float KRW: float - TRY: float UAH: float MXN: float CAD: float @@ -95,7 +94,6 @@ class ExchangeRateData(NamedTuple): HKD: float ZAR: float INR: float - ARS: float CRC: float ILS: float KWD: float @@ -206,25 +204,27 @@ def cached_data(filename: str | Path): class ExchangeRate: - __slots__ = () GET_KEY_PRICES_API = f'https://api.steampowered.com/ISteamEconomy/GetAssetPrices/v1/' \ f'?appid=730&key={config.STEAM_API_KEY}' CURRENCIES_SYMBOLS = {"USD": "$", "GBP": "£", "EUR": "€", "RUB": "₽", "BRL": "R$", "JPY": "¥", "NOK": "kr", "IDR": "Rp", "MYR": "RM", "PHP": "₱", "SGD": "S$", "THB": "฿", - "VND": "₫", "KRW": "₩", "TRY": "₺", "UAH": "₴", - "MXN": "Mex$", "CAD": "CDN$", "AUD": "A$", - "NZD": "NZ$", "PLN": "zł", "CHF": "CHF", "AED": "AED", - "CLP": "CLP$", "CNY": "¥", "COP": "COL$", "PEN": "S/.", - "SAR": "SR", "TWD": "NT$", "HKD": "HK$", "ZAR": "R", - "INR": "₹", "ARS": "ARS$", "CRC": "₡", "ILS": "₪", - "KWD": "KD", "QAR": "QR", "UYU": "$U", "KZT": "₸"} + "VND": "₫", "KRW": "₩", "UAH": "₴", "MXN": "Mex$", + "CAD": "CDN$", "AUD": "A$", "NZD": "NZ$", "PLN": "zł", + "CHF": "CHF", "AED": "AED", "CLP": "CLP$", "CNY": "¥", + "COP": "COL$", "PEN": "S/.", "SAR": "SR", "TWD": "NT$", + "HKD": "HK$", "ZAR": "R", "INR": "₹", "CRC": "₡", + "ILS": "₪", "KWD": "KD", "QAR": "QR", "UYU": "$U", + "KZT": "₸"} + UNDEFINED_CURRENCIES = ('Unknown', 'ARS', 'BYN', 'TRY') @classmethod def request(cls): r = requests.get(cls.GET_KEY_PRICES_API, timeout=15).json()['result']['assets'] key_price = [item for item in r if item['classid'] == '1544098059'][0]['prices'] - del key_price['Unknown'], key_price['BYN'] #, key_price['ARS'], key_price['TRY'] # undefined values + + for currency in cls.UNDEFINED_CURRENCIES: + del key_price[currency] prices = {k: v / 100 for k, v in key_price.items()} formatted_prices = {k: f'{v:.0f}' if v % 1 == 0 else f'{v:.2f}' @@ -242,6 +242,10 @@ def cached_data(filename: str | Path): if key_prices is None: return {} + for cur in ('ARS', 'TRY'): # these values could be left in the cache + if key_prices.get(cur): # todo: remove later + del key_prices[cur] + return ExchangeRateData(**key_prices) @@ -377,7 +381,7 @@ def from_json(cls, data): losses = entry.val elif entry.tag == 19: for map_id, map_name in MAPS.items(): - last_wins[map_name] = ((entry.val << (4 * map_id)) & 0xF0000000) >> 4*7 + last_wins[map_name] = ((entry.val << (4 * map_id)) & 0xF0000000) >> 4 * 7 elif entry.tag == 20: timestamp = entry.val elif entry.tag == 21: From e4a28d3b4797ab97b3c2f022f32ab630a869475e Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Mon, 13 May 2024 22:22:30 +0500 Subject: [PATCH 13/18] Added test for tags --- l10n/tags.py | 8 ++++++-- l10n/test.py | 14 +++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/l10n/tags.py b/l10n/tags.py index 6d1dec1..8ba3841 100644 --- a/l10n/tags.py +++ b/l10n/tags.py @@ -163,8 +163,8 @@ def sample(cls) -> Tags: def dump_tags() -> Tags: - """Dumps "tags.json" and returns Tags object, containing all defined tags lists. - """ + """Dumps "tags.json" and returns Tags object, containing all defined tags lists.""" + path = Path(__file__).parent / 'data' / 'tags.json' if not path.exists(): warnings.warn(f"Can't find tags.json, generating a file...", PrimaryTagsFileNotFound) @@ -206,3 +206,7 @@ def dump_tags() -> Tags: data[field].extend(og_tags) return Tags(**data) + + +if __name__ == '__main__': + dump_tags() diff --git a/l10n/test.py b/l10n/test.py index ca8bfa3..5d2b9f7 100644 --- a/l10n/test.py +++ b/l10n/test.py @@ -2,7 +2,7 @@ from sl10n.pimpl import JSONImpl -from l10n import SL10n, Locale +from l10n import SL10n, Locale, dump_tags def test_l10n(recwarn): @@ -16,3 +16,15 @@ def test_l10n(recwarn): for r in recwarn: print(f'{r.category.__name__}: {r.message}') assert len(recwarn) == 0, 'SL10n process raised some warnings.' + + +def test_tags(recwarn): + """ + Test to check any missing or unexpected tags keys. + """ + + dump_tags() + + for r in recwarn: + print(f'{r.category.__name__}: {r.message}') + assert len(recwarn) == 0, 'Tags process raised some warnings.' From 1c6fdb31bd73337bbdc3a5175f0ea8ceec4cb137 Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Mon, 13 May 2024 22:28:13 +0500 Subject: [PATCH 14/18] Fixed wrong cache data being read, some cleanup --- bottypes/logger.py | 2 +- bottypes/stats.py | 2 +- collectors/core.py | 20 ++++++++++++++++++++ main.py | 7 +++---- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/bottypes/logger.py b/bottypes/logger.py index 9fd6d65..9856b68 100644 --- a/bottypes/logger.py +++ b/bottypes/logger.py @@ -69,7 +69,7 @@ async def process_queue(self): f'ℹ️: {userid}', f'✈️: {user.language_code}', f'⚙️: {session.locale.lang_code}', - f'━━━━━━━━━━━━━━━━━━━━━━━━'] + [event.result_text for event in logged_events] + f'━━━━━━━━━━━━━━━━━━━━━━━'] + [event.result_text for event in logged_events] return await self.send_log(client, '\n'.join(text), disable_notification=True) async def schedule_system_log(self, client: BotClient, text: str, diff --git a/bottypes/stats.py b/bottypes/stats.py index 4469f14..c24ba60 100644 --- a/bottypes/stats.py +++ b/bottypes/stats.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -@dataclass +@dataclass(slots=True) class BotRegularStats: callback_queries_handled = 0 inline_queries_handled = 0 diff --git a/collectors/core.py b/collectors/core.py index bb61a39..56e70b2 100644 --- a/collectors/core.py +++ b/collectors/core.py @@ -57,6 +57,18 @@ ('japan', 'tokyo'): 'Japan', } +UNUSED_FIELDS = ['csgo_client_version', + 'csgo_server_version', + 'csgo_patch_version', + 'csgo_version_timestamp', + 'sdk_build_id', + 'ds_build_id', + 'valve_ds_changenumber', + 'webapi', + 'sessions_logon', + 'steam_community', + 'matchmaking_scheduler'] + execution_start_dt = dt.datetime.now() execution_cron_hour = execution_start_dt.hour @@ -80,6 +92,12 @@ workdir=config.SESS_FOLDER) +def clear_from_unused_fields(cache: dict): + for field in UNUSED_FIELDS: + if cache.get(field): + del cache[field] + + def remap_dc(info: dict, dc: Datacenter): api_info_field = DATACENTER_API_FIELDS[dc.id] return info[api_info_field] @@ -130,6 +148,8 @@ async def update_cache_info(): with open(config.CACHE_FILE_PATH, encoding='utf-8') as f: cache = json.load(f) + clear_from_unused_fields(cache) + overall_data = GameServers.request() for key, value in overall_data.asdict().items(): diff --git a/main.py b/main.py index 26b778d..7ba2f77 100644 --- a/main.py +++ b/main.py @@ -305,7 +305,7 @@ async def profile_info(client: BotClient, session: UserSession, bot_message: Mes with open(config.CACHE_FILE_PATH, encoding='utf-8') as f: cache_file = json.load(f) - if cache_file.get('webapi') != 'normal': + if cache_file.get('webapi_state') != 'normal': return await send_about_maintenance(client, session, bot_message) await bot_message.edit(session.locale.bot_choose_cmd, @@ -932,18 +932,17 @@ async def main(): scheduler.add_job(regular_stats_report, 'interval', hours=8, args=(bot,)) - scheduler.start() - try: await db_session.init(config.USER_DB_FILE_PATH) await bot.start() + scheduler.start() await bot.log('Bot started.', instant=True) await bot.mainloop() except Exception as e: logging.exception('The bot got terminated because of exception!') await bot.log(f'Bot got terminated because of exception!\n' f'\n' - f'❗️ {e.__traceback__}', disable_notification=False) + f'❗️ {e.__traceback__}', disable_notification=True, instant=True) finally: logging.info('Shutting down the bot...') await bot.log('Bot is shutting down...', instant=True) From 247d4a8904b5a34e54a6b5daa44251cddb5cf623 Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Mon, 13 May 2024 22:52:58 +0500 Subject: [PATCH 15/18] Hotfix stats.py --- bottypes/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bottypes/stats.py b/bottypes/stats.py index c24ba60..4469f14 100644 --- a/bottypes/stats.py +++ b/bottypes/stats.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -@dataclass(slots=True) +@dataclass class BotRegularStats: callback_queries_handled = 0 inline_queries_handled = 0 From cc9239d021cef27022360aec771f1f502404cbb6 Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Tue, 14 May 2024 13:47:04 +0500 Subject: [PATCH 16/18] Forwards filtering improved, "via bot" filtering --- plugins/incs2chat.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/incs2chat.py b/plugins/incs2chat.py index 21fa3a6..9e114bb 100644 --- a/plugins/incs2chat.py +++ b/plugins/incs2chat.py @@ -133,6 +133,11 @@ async def cs_l10n_update(message: Message): await message.reply_sticker('CAACAgIAAxkBAAID-l_9tlLJhZQSgqsMUAvLv0r8qhxSAAIKAwAC-p_xGJ-m4XRqvoOzHgQ') +async def filter_message(sender: Chat | User, message: Message): + if sender and sender.id in config.FILTERED_SENDERS: + await message.delete() + + @Client.on_message(filters.chat(config.INCS2CHAT) & filters.command("ban")) async def ban(client: Client, message: Message): chat = await client.get_chat(config.INCS2CHAT) @@ -242,8 +247,13 @@ async def handle_new_post(_, message: Message): @Client.on_message(filters.chat(config.INCS2CHAT) & filters.forwarded) async def filter_forwards(_, message: Message): - if message.forward_from_chat and message.forward_from_chat.id in config.FILTER_FORWARDS: - await message.delete() + forward_from = message.forward_from_chat if message.forward_from_chat else message.forward_from + await filter_message(forward_from, message) + + +@Client.on_message(filters.chat(config.INCS2CHAT) & filters.via_bot) +async def filter_via_bot(_, message: Message): + await filter_message(message.via_bot, message) @Client.on_message(filters.chat(config.INCS2CHAT) & filters.sticker) From b0caf6c92c2eb9f997a4eb11629520e94b7cbc7f Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Thu, 16 May 2024 19:44:43 +0500 Subject: [PATCH 17/18] Moved collectors to the root to avoid using env --- collectors/core.py => core.py | 0 ...game_coordinator.py => game_coordinator.py | 0 collectors/gc_alerter.py => gc_alerter.py | 204 +++++++------- ...layers_graph.py => online_players_graph.py | 260 +++++++++--------- 4 files changed, 232 insertions(+), 232 deletions(-) rename collectors/core.py => core.py (100%) rename collectors/game_coordinator.py => game_coordinator.py (100%) rename collectors/gc_alerter.py => gc_alerter.py (97%) rename collectors/online_players_graph.py => online_players_graph.py (97%) diff --git a/collectors/core.py b/core.py similarity index 100% rename from collectors/core.py rename to core.py diff --git a/collectors/game_coordinator.py b/game_coordinator.py similarity index 100% rename from collectors/game_coordinator.py rename to game_coordinator.py diff --git a/collectors/gc_alerter.py b/gc_alerter.py similarity index 97% rename from collectors/gc_alerter.py rename to gc_alerter.py index 9d0bf81..cd5ac89 100644 --- a/collectors/gc_alerter.py +++ b/gc_alerter.py @@ -1,102 +1,102 @@ -import json -import logging -import platform - -from apscheduler.schedulers.asyncio import AsyncIOScheduler -from pyrogram import Client -if platform.system() == 'Linux': - # noinspection PyPackageRequirements - import uvloop - - uvloop.install() - -import config -from functions import locale - -loc = locale('ru') - -MONITORING_IDS = ('cs2_app_changenumber', 'cs2_server_changenumber', - 'dprp_build_id', 'dpr_build_id', 'public_build_id') - -available_alerts = {'public_build_id': loc.notifs_build_public, - 'dpr_build_id': loc.notifs_build_dpr, - 'dprp_build_id': loc.notifs_build_dprp, - 'dpr_build_sync_id': f'{loc.notifs_build_dpr} 🔃', - 'dprp_build_sync_id': f'{loc.notifs_build_dprp} 🔃', - 'cs2_app_changenumber': loc.notifs_build_cs2_client, - 'cs2_server_changenumber': loc.notifs_build_cs2_server} - -logging.basicConfig(level=logging.INFO, - format="%(asctime)s | %(message)s", - datefmt="%H:%M:%S — %d/%m/%Y") - -scheduler = AsyncIOScheduler() -bot = Client(config.BOT_GC_MODULE_NAME, - api_id=config.API_ID, - api_hash=config.API_HASH, - bot_token=config.BOT_TOKEN, - no_updates=True, - workdir=config.SESS_FOLDER) - - -@scheduler.scheduled_job('interval', seconds=30) -async def scan_for_gc_update(): - # noinspection PyBroadException - try: - with open(config.GC_PREV_CACHE_FILE_PATH, encoding='utf-8') as f: - prev_cache = json.load(f) - with open(config.CACHE_FILE_PATH, encoding='utf-8') as f: - cache = json.load(f) - - for _id in MONITORING_IDS: - new_value = cache[_id] - if prev_cache.get(_id) != new_value: - prev_cache[_id] = new_value - if new_value == cache['public_build_id']: - if _id == 'dpr_build_id': - await send_alert('dpr_build_sync_id', new_value) - continue - if _id == 'dprp_build_id': - await send_alert('dprp_build_sync_id', new_value) - continue - - await send_alert(_id, new_value) - - with open(config.GC_PREV_CACHE_FILE_PATH, 'w', encoding='utf-8') as f: - json.dump(prev_cache, f, indent=4, ensure_ascii=False) - except Exception: - logging.exception('Caught an exception while scanning GC info!') - - -async def send_alert(key: str, new_value: int): - logging.info(f'Found new change: {key}, sending alert...') - - alert_sample = available_alerts.get(key) - - if alert_sample is None: - logging.warning(f'Got wrong key to send alert: {key}') - return - - text = alert_sample.format(new_value) - - if config.TEST_MODE: - chat_list = [config.AQ] - else: - chat_list = [config.INCS2CHAT, config.CSTRACKER] - - for chat_id in chat_list: - msg = await bot.send_message(chat_id, text, disable_web_page_preview=True) - if chat_id == config.INCS2CHAT: - await msg.pin(disable_notification=True) - - -def main(): - try: - scheduler.start() - bot.run() - except TypeError: # catching TypeError because Pyrogram propogates it at stop for some reason - logging.info('Shutting down the bot...') - - -if __name__ == '__main__': - main() +import json +import logging +import platform + +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from pyrogram import Client +if platform.system() == 'Linux': + # noinspection PyPackageRequirements + import uvloop + + uvloop.install() + +import config +from functions import locale + +loc = locale('ru') + +MONITORING_IDS = ('cs2_app_changenumber', 'cs2_server_changenumber', + 'dprp_build_id', 'dpr_build_id', 'public_build_id') + +available_alerts = {'public_build_id': loc.notifs_build_public, + 'dpr_build_id': loc.notifs_build_dpr, + 'dprp_build_id': loc.notifs_build_dprp, + 'dpr_build_sync_id': f'{loc.notifs_build_dpr} 🔃', + 'dprp_build_sync_id': f'{loc.notifs_build_dprp} 🔃', + 'cs2_app_changenumber': loc.notifs_build_cs2_client, + 'cs2_server_changenumber': loc.notifs_build_cs2_server} + +logging.basicConfig(level=logging.INFO, + format="%(asctime)s | %(message)s", + datefmt="%H:%M:%S — %d/%m/%Y") + +scheduler = AsyncIOScheduler() +bot = Client(config.BOT_GC_MODULE_NAME, + api_id=config.API_ID, + api_hash=config.API_HASH, + bot_token=config.BOT_TOKEN, + no_updates=True, + workdir=config.SESS_FOLDER) + + +@scheduler.scheduled_job('interval', seconds=30) +async def scan_for_gc_update(): + # noinspection PyBroadException + try: + with open(config.GC_PREV_CACHE_FILE_PATH, encoding='utf-8') as f: + prev_cache = json.load(f) + with open(config.CACHE_FILE_PATH, encoding='utf-8') as f: + cache = json.load(f) + + for _id in MONITORING_IDS: + new_value = cache[_id] + if prev_cache.get(_id) != new_value: + prev_cache[_id] = new_value + if new_value == cache['public_build_id']: + if _id == 'dpr_build_id': + await send_alert('dpr_build_sync_id', new_value) + continue + if _id == 'dprp_build_id': + await send_alert('dprp_build_sync_id', new_value) + continue + + await send_alert(_id, new_value) + + with open(config.GC_PREV_CACHE_FILE_PATH, 'w', encoding='utf-8') as f: + json.dump(prev_cache, f, indent=4, ensure_ascii=False) + except Exception: + logging.exception('Caught an exception while scanning GC info!') + + +async def send_alert(key: str, new_value: int): + logging.info(f'Found new change: {key}, sending alert...') + + alert_sample = available_alerts.get(key) + + if alert_sample is None: + logging.warning(f'Got wrong key to send alert: {key}') + return + + text = alert_sample.format(new_value) + + if config.TEST_MODE: + chat_list = [config.AQ] + else: + chat_list = [config.INCS2CHAT, config.CSTRACKER] + + for chat_id in chat_list: + msg = await bot.send_message(chat_id, text, disable_web_page_preview=True) + if chat_id == config.INCS2CHAT: + await msg.pin(disable_notification=True) + + +def main(): + try: + scheduler.start() + bot.run() + except TypeError: # catching TypeError because Pyrogram propogates it at stop for some reason + logging.info('Shutting down the bot...') + + +if __name__ == '__main__': + main() diff --git a/collectors/online_players_graph.py b/online_players_graph.py similarity index 97% rename from collectors/online_players_graph.py rename to online_players_graph.py index 8e0589d..75d7e55 100644 --- a/collectors/online_players_graph.py +++ b/online_players_graph.py @@ -1,130 +1,130 @@ -import json -import logging -import time - -from apscheduler.schedulers.blocking import BlockingScheduler -from matplotlib.colors import LinearSegmentedColormap -from matplotlib.cm import ScalarMappable -import matplotlib.dates as mdates -import matplotlib.pyplot as plt -from matplotlib.ticker import FixedFormatter -import pandas as pd -from requests import JSONDecodeError -import seaborn as sns -from telegraph import Telegraph - -import config -from functions import utime - -MINUTE = 60 -MAX_ONLINE_MARKS = (MINUTE // 10) * 24 * 7 * 2 # = 2016 marks - every 10 minutes for the last two weeks - -logging.basicConfig(level=logging.INFO, - format="%(asctime)s | %(name)s: %(message)s", - datefmt="%H:%M:%S — %d/%m/%Y") - -scheduler = BlockingScheduler() -telegraph = Telegraph(access_token=config.TELEGRAPH_ACCESS_TOKEN) - -cmap = LinearSegmentedColormap.from_list('custom', [(1, 1, 0), (1, 0, 0)], N=100) -norm = plt.Normalize(0, 2_000_000) -mappable = ScalarMappable(norm=norm, cmap=cmap) - -ticks = [0, 250000, 500000, 750000, 1000000, 1250000, 1500000, 1750000, 2000000] -colorbar_ticks_format = FixedFormatter(['0', '250K', '500K', '750K', '1M', '1.25M', '1.5M', '1.75M', '2M+']) -fig_ticks_format = ['' for _ in ticks] - -x_major_locator = mdates.DayLocator() -x_major_formatter = mdates.DateFormatter("%b %d") - - -@scheduler.scheduled_job('cron', hour='*', minute='0,10,20,30,40,50', second='0') -def graph_maker(): - # noinspection PyBroadException - try: - with open(config.CACHE_FILE_PATH, encoding='utf-8') as f: - cache = json.load(f) - - old_player_data = pd.read_csv(config.PLAYER_CHART_FILE_PATH, parse_dates=['DateTime']) - - marks_count = len(old_player_data.index) - if marks_count >= MAX_ONLINE_MARKS: - remove_marks = marks_count - MAX_ONLINE_MARKS - old_player_data.drop(range(remove_marks + 1), axis=0, inplace=True) - - player_count = cache.get('online_players', 0) - if player_count < 50_000: # potentially Steam maintenance - player_count = old_player_data.iloc[-1]['Players'] - - temp_player_data = pd.DataFrame( - [[f'{utime.utcnow():%Y-%m-%d %H:%M:%S}', player_count]], - columns=["DateTime", "Players"], - ) - - new_player_data = pd.concat([old_player_data, temp_player_data]) - - new_player_data.to_csv(config.PLAYER_CHART_FILE_PATH, index=False) - - fig: plt.Figure - ax: plt.Axes - - sns.set_style('whitegrid') - - fig, ax = plt.subplots(figsize=(10, 2.5)) - ax.scatter('DateTime', 'Players', - data=new_player_data, - c='Players', cmap=cmap, s=10, norm=norm, linewidths=0.7) - ax.fill_between(new_player_data['DateTime'], - new_player_data['Players'] - 20_000, - color=cmap(0.5), alpha=0.4) - ax.margins(x=0) - - ax.grid(visible=True, axis='y', linestyle='--', alpha=0.3) - ax.grid(visible=False, axis='x') - ax.spines['bottom'].set_position('zero') - ax.spines['bottom'].set_color('black') - ax.set(xlabel='', ylabel='') - ax.xaxis.set_ticks_position('bottom') - ax.xaxis.set_major_locator(x_major_locator) - ax.xaxis.set_major_formatter(x_major_formatter) - ax.legend(loc='upper left') - ax.text(0.20, 0.88, - 'Made by @INCS2\n' - 'updates every 10 min', - ha='center', transform=ax.transAxes, color='black', size='8') - ax.set_yticks(ticks, fig_ticks_format) - - fig.colorbar(mappable, ax=ax, - ticks=ticks, - format=colorbar_ticks_format, - pad=0.01) - - fig.subplots_adjust(top=0.933, bottom=0.077, left=0.03, right=1.07) - - fig.savefig(config.GRAPH_IMG_FILE_PATH, dpi=200) - plt.close() - - try: - image_path = telegraph.upload_file(str(config.GRAPH_IMG_FILE_PATH))[0]['src'] - except JSONDecodeError: # SCREW YOU - time.sleep(1) - image_path = telegraph.upload_file(str(config.GRAPH_IMG_FILE_PATH))[0]['src'] - image_url = f'https://telegra.ph{image_path}' - - if image_url != cache.get('graph_url'): - cache['graph_url'] = image_url - - with open(config.CACHE_FILE_PATH, 'w', encoding='utf-8') as f: - json.dump(cache, f, indent=4, ensure_ascii=False) - except Exception: - logging.exception('Caught exception in graph maker!') - time.sleep(MINUTE) - return graph_maker() - - -def main(): - scheduler.start() - - -if __name__ == "__main__": - main() +import json +import logging +import time + +from apscheduler.schedulers.blocking import BlockingScheduler +from matplotlib.colors import LinearSegmentedColormap +from matplotlib.cm import ScalarMappable +import matplotlib.dates as mdates +import matplotlib.pyplot as plt +from matplotlib.ticker import FixedFormatter +import pandas as pd +from requests import JSONDecodeError +import seaborn as sns +from telegraph import Telegraph + +import config +from functions import utime + +MINUTE = 60 +MAX_ONLINE_MARKS = (MINUTE // 10) * 24 * 7 * 2 # = 2016 marks - every 10 minutes for the last two weeks + +logging.basicConfig(level=logging.INFO, + format="%(asctime)s | %(name)s: %(message)s", + datefmt="%H:%M:%S — %d/%m/%Y") + +scheduler = BlockingScheduler() +telegraph = Telegraph(access_token=config.TELEGRAPH_ACCESS_TOKEN) + +cmap = LinearSegmentedColormap.from_list('custom', [(1, 1, 0), (1, 0, 0)], N=100) +norm = plt.Normalize(0, 2_000_000) +mappable = ScalarMappable(norm=norm, cmap=cmap) + +ticks = [0, 250000, 500000, 750000, 1000000, 1250000, 1500000, 1750000, 2000000] +colorbar_ticks_format = FixedFormatter(['0', '250K', '500K', '750K', '1M', '1.25M', '1.5M', '1.75M', '2M+']) +fig_ticks_format = ['' for _ in ticks] + +x_major_locator = mdates.DayLocator() +x_major_formatter = mdates.DateFormatter("%b %d") + + +@scheduler.scheduled_job('cron', hour='*', minute='0,10,20,30,40,50', second='0') +def graph_maker(): + # noinspection PyBroadException + try: + with open(config.CACHE_FILE_PATH, encoding='utf-8') as f: + cache = json.load(f) + + old_player_data = pd.read_csv(config.PLAYER_CHART_FILE_PATH, parse_dates=['DateTime']) + + marks_count = len(old_player_data.index) + if marks_count >= MAX_ONLINE_MARKS: + remove_marks = marks_count - MAX_ONLINE_MARKS + old_player_data.drop(range(remove_marks + 1), axis=0, inplace=True) + + player_count = cache.get('online_players', 0) + if player_count < 50_000: # potentially Steam maintenance + player_count = old_player_data.iloc[-1]['Players'] + + temp_player_data = pd.DataFrame( + [[f'{utime.utcnow():%Y-%m-%d %H:%M:%S}', player_count]], + columns=["DateTime", "Players"], + ) + + new_player_data = pd.concat([old_player_data, temp_player_data]) + + new_player_data.to_csv(config.PLAYER_CHART_FILE_PATH, index=False) + + fig: plt.Figure + ax: plt.Axes + + sns.set_style('whitegrid') + + fig, ax = plt.subplots(figsize=(10, 2.5)) + ax.scatter('DateTime', 'Players', + data=new_player_data, + c='Players', cmap=cmap, s=10, norm=norm, linewidths=0.7) + ax.fill_between(new_player_data['DateTime'], + new_player_data['Players'] - 20_000, + color=cmap(0.5), alpha=0.4) + ax.margins(x=0) + + ax.grid(visible=True, axis='y', linestyle='--', alpha=0.3) + ax.grid(visible=False, axis='x') + ax.spines['bottom'].set_position('zero') + ax.spines['bottom'].set_color('black') + ax.set(xlabel='', ylabel='') + ax.xaxis.set_ticks_position('bottom') + ax.xaxis.set_major_locator(x_major_locator) + ax.xaxis.set_major_formatter(x_major_formatter) + ax.legend(loc='upper left') + ax.text(0.20, 0.88, + 'Made by @INCS2\n' + 'updates every 10 min', + ha='center', transform=ax.transAxes, color='black', size='8') + ax.set_yticks(ticks, fig_ticks_format) + + fig.colorbar(mappable, ax=ax, + ticks=ticks, + format=colorbar_ticks_format, + pad=0.01) + + fig.subplots_adjust(top=0.933, bottom=0.077, left=0.03, right=1.07) + + fig.savefig(config.GRAPH_IMG_FILE_PATH, dpi=200) + plt.close() + + try: + image_path = telegraph.upload_file(str(config.GRAPH_IMG_FILE_PATH))[0]['src'] + except JSONDecodeError: # SCREW YOU + time.sleep(1) + image_path = telegraph.upload_file(str(config.GRAPH_IMG_FILE_PATH))[0]['src'] + image_url = f'https://telegra.ph{image_path}' + + if image_url != cache.get('graph_url'): + cache['graph_url'] = image_url + + with open(config.CACHE_FILE_PATH, 'w', encoding='utf-8') as f: + json.dump(cache, f, indent=4, ensure_ascii=False) + except Exception: + logging.exception('Caught exception in graph maker!') + time.sleep(MINUTE) + return graph_maker() + + +def main(): + scheduler.start() + + +if __name__ == "__main__": + main() From 8c7a500b17b7cdf4aeacbdbd4d859b39c586a3db Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Thu, 16 May 2024 21:37:31 +0500 Subject: [PATCH 18/18] Experimental attachments support --- plugins/incs2chat.py | 47 +++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/plugins/incs2chat.py b/plugins/incs2chat.py index 9e114bb..3ecc178 100644 --- a/plugins/incs2chat.py +++ b/plugins/incs2chat.py @@ -1,7 +1,9 @@ import asyncio +import json import logging import random import re +from io import BytesIO from pyrogram import Client, filters from pyrogram.enums import ChatMembersFilter @@ -84,7 +86,10 @@ def wrap_text(text: str, max_length: int) -> list[str]: def process_discord_text(message: Message) -> list[str]: - text = to_discord_markdown(message) if message.entities else message.text + text = (to_discord_markdown(message) if message.entities + else message.caption if message.caption + else message.text if message.text + else '') # fixme: can break formatting if wrapping happens in the middle of formatted section # fixme: (e.g. "**some [split] wise words**") @@ -105,24 +110,42 @@ def translate_text(text: str, source_lang: str = 'RU', target_lang: str = 'EN') logging.error(f'{r.status_code=}, {r.reason=}') -def post_to_discord_webhook(url: str, text: str): # todo: attachments support? - headers = {'Content-Type': 'application/json'} +def post_to_discord_webhook(url: str, text: str, attachment: BytesIO = None): # todo: attachments support? payload = {'content': text} - r = requests.post(url, json=payload, headers=headers) - if r.status_code != 204: # Discord uses 204 as a success code (yikes) + if attachment: + payload = {'payload_json': (None, json.dumps(payload)), 'files[0]': (attachment.name, attachment.getbuffer())} + r = requests.post(url, files=payload) + else: + headers = {'Content-Type': 'application/json'} + r = requests.post(url, payload, headers=headers) + + if r.status_code not in [200, 204]: # Discord uses 204 as a success code (yikes) logging.error('Failed to post to Discord webhook.') - logging.error(f'{text=}') - logging.error(f'{r.status_code=}, {r.reason=}') + logging.error(f'{payload=}') + logging.error(f'{r.status_code=}, {r.reason=}, {r.text=}') -def forward_to_discord(message: Message): +async def forward_to_discord(client: Client, message: Message): texts = process_discord_text(message) logging.info(texts) + attachment: BytesIO | None + try: + # fixme: for every attachment this ^ function will be called multiple times + # fixme: this could be fixed if we keep track of media groups but too lazy to implement it properly + attachment = await client.download_media(message, in_memory=True) + except ValueError: + attachment = None + for text in texts: - post_to_discord_webhook(config.DS_WEBHOOK_URL, text) - post_to_discord_webhook(config.DS_WEBHOOK_URL_EN, translate_text(text, 'RU', 'EN')) + if attachment: + post_to_discord_webhook(config.DS_WEBHOOK_URL, text, attachment) + post_to_discord_webhook(config.DS_WEBHOOK_URL_EN, translate_text(text, 'RU', 'EN'), attachment) + attachment = None + else: + post_to_discord_webhook(config.DS_WEBHOOK_URL, text) + post_to_discord_webhook(config.DS_WEBHOOK_URL_EN, translate_text(text, 'RU', 'EN')) async def cs_l10n_update(message: Message): @@ -236,13 +259,13 @@ async def echo(client: Client, message: Message): @Client.on_message(filters.linked_channel & filters.chat(config.INCS2CHAT)) -async def handle_new_post(_, message: Message): +async def handle_new_post(client: Client, message: Message): is_sent_by_correct_chat = (message.sender_chat and message.sender_chat.id == config.INCS2CHANNEL) is_forwarded_from_correct_chat = (message.forward_from_chat and message.forward_from_chat.id == config.INCS2CHANNEL) if is_sent_by_correct_chat and is_forwarded_from_correct_chat: await cs_l10n_update(message) - forward_to_discord(message) + await forward_to_discord(client, message) @Client.on_message(filters.chat(config.INCS2CHAT) & filters.forwarded)