From 884820dc486767c50f741783143287aada4b0058 Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Thu, 2 May 2024 14:33:53 +0500 Subject: [PATCH 1/5] Swap to steam-py-lib (ValvePython/steam fork with fixes) and fixed ValvePython/csgo fork --- collectors/game_coordinator.py | 264 +++++++++++++++++---------------- requirements.txt | Bin 2946 -> 2912 bytes utypes/profiles.py | 71 +++------ 3 files changed, 161 insertions(+), 174 deletions(-) diff --git a/collectors/game_coordinator.py b/collectors/game_coordinator.py index cc21327..32a4557 100644 --- a/collectors/game_coordinator.py +++ b/collectors/game_coordinator.py @@ -1,16 +1,20 @@ -import asyncio +import steam.monkey +steam.monkey.patch_minimal() + import datetime as dt import json import logging -from pathlib import Path import platform +import sys import time from zoneinfo import ZoneInfo -from apscheduler.schedulers.asyncio import AsyncIOScheduler -from steam import App -from steam.ext.csgo import Client as CS2GCClient -from steam.ext.csgo.protobufs.sdk import GcConnectionStatus +from apscheduler.schedulers.gevent import GeventScheduler +from csgo.client import CSGOClient +import gevent +from steam.client import SteamClient +from steam.enums import EResult + if platform.system() == 'Linux': # noinspection PyPackageRequirements @@ -22,174 +26,184 @@ import env import config from functions import utime -from utypes import GameVersion, States, SteamWebAPI +from utypes import GameVersion, States VALVE_TIMEZONE = ZoneInfo('America/Los_Angeles') -logging.basicConfig(format='%(asctime)s | %(name)s: %(message)s', - datefmt='%H:%M:%S — %d/%m/%Y', - force=True) +logging.basicConfig(level=logging.INFO, + format='%(asctime)s | GC: %(message)s', + datefmt='%H:%M:%S — %d/%m/%Y') -logger = logging.getLogger(f'{config.BOT_NAME}.GCCollector') -logger.setLevel(logging.INFO) +client = SteamClient() +client.set_credential_location(config.STEAM_CREDS_PATH) +cs = CSGOClient(client) +scheduler = GeventScheduler() -api = SteamWebAPI(config.STEAM_API_KEY) +@client.on('error') +def handle_error(result): + logging.info(f'Logon result: {result!r}') -class GCCollector(CS2GCClient): - APPS_TO_FETCH = App(id=730), App(id=2275500), App(id=2275530) # the last two apps don't get fetched +@client.on('channel_secured') +def send_relogin(): + if client.relogin_available: + client.relogin() - cache: dict[str, ...] - def __init__(self, cache_file_path: Path, **kwargs): - super().__init__(**kwargs) +@client.on('connected') +def log_connect(): + logging.info(f'Connected to {client.current_server_addr}') - self.cache_file_path = cache_file_path - self.load_cache() - self.scheduler = AsyncIOScheduler() - self.scheduler.add_job(self.update_depots, 'interval', seconds=45) - # self.scheduler.add_job(self.update_players_count, 'interval', seconds=45) # for GC requesting which doesn't work for now - self.scheduler.add_job(self.update_players_count_alter, 'interval', minutes=2) # currently use WebAPI as an alternative +@client.on('reconnect') +def handle_reconnect(delay): + logging.info(f'Reconnect in {delay}s...') - async def login(self, *args, **kwargs): - logger.info('Logging in...') - await super().login(*args, **kwargs) - async def on_ready(self): - logger.info('Logged in successfully.') +@client.on('disconnected') +def handle_disconnect(): + logging.info('Disconnected.') - async def on_disconnect(self): - logger.info('Disconnected.') - self.scheduler.pause() + if client.relogin_available: + logging.info('Reconnecting...') + client.reconnect(maxdelay=30) # todo: could be broken - needs to be tested somehow - logger.info('Reconnecting...') - await self.login(username=config.STEAM_USERNAME, password=config.STEAM_PASS) - result = self.is_ready() + # sys.exit() - logger.info('Reconnected successfully.' if result else 'Failed to reconnect.') - if result: - self.scheduler.resume() - async def on_gc_ready(self): - logger.info('CS launched.') - self.scheduler.start() +@client.on('logged_on') +def handle_after_logon(): + cs.launch() + scheduler.start() - async def on_gc_status_change(self, status: GcConnectionStatus): # currently doesn't get called - logger.info(f'{status.name!r} (on_gc_status_change)') - statuses = {0: States.NORMAL, 1: States.INTERNAL_SERVER_ERROR, 2: States.OFFLINE, - 3: States.RELOADING, 4: States.INTERNAL_STEAM_ERROR} - game_coordinator = statuses.get(status.value, States.UNKNOWN) +@cs.on('ready') +def cs_launched(): + logging.info('CS launched.') - if game_coordinator != self.cache.get('game_coordinator'): - self.update_cache({'game_coordinator': game_coordinator.literal}) - logger.info(f'Successfully dumped game coordinator status: {game_coordinator.literal}') +@cs.on('connection_status') +def update_gc_status(status): + statuses = {0: States.NORMAL, 1: States.INTERNAL_SERVER_ERROR, 2: States.OFFLINE, + 3: States.RELOADING, 4: States.INTERNAL_STEAM_ERROR} + game_coordinator = statuses.get(status, States.UNKNOWN) - async def update_depots(self): - try: - data = await self.fetch_product_info(apps=self.APPS_TO_FETCH) - data = {int(app.id): app for app in data} - logging.info(data) - - main_data = data[730] - public_build_id = main_data.public_branch.build_id - dpr_build_id = main_data.get_branch('dpr').build_id - dprp_build_id = main_data.get_branch('dprp').build_id - - # currently can't track todo: investigate, is it steam.py's issue or valve's - # cs2_app_change_number = data[2275500].change_number - # cs2_server_change_number = data[2275530].change_number - except Exception: - logger.exception('Caught an exception while trying to fetch depots!') - return + with open(config.CACHE_FILE_PATH, encoding='utf-8') as f: + cache = json.load(f) - if public_build_id != self.cache.get('public_build_id'): - _ = asyncio.create_task(self.update_game_version()) + if game_coordinator != cache.get('game_coordinator'): + cache['game_coordinator'] = game_coordinator.literal - self.update_cache({ - 'public_build_id': public_build_id, - 'dpr_build_id': dpr_build_id, - 'dprp_build_id': dprp_build_id, - # 'cs2_app_changenumber': cs2_app_change_number, - # 'cs2_server_changenumber': cs2_server_change_number - }) + with open(config.CACHE_FILE_PATH, 'w', encoding='utf-8') as f: + json.dump(cache, f, indent=4) - logger.info('Successfully dumped game build IDs.') + logging.info(f'Successfully dumped game coordinator status: {game_coordinator.literal}') - async def update_game_version(self): - timeout = 30 * 60 - timeout_start = time.time() - while time.time() < timeout_start + timeout: - try: - data = GameVersion.request() - no_version_data_cached = (data.cs2_client_version is None) - version_has_changed = (data.cs2_client_version != self.cache.get('cs2_client_version')) +@scheduler.scheduled_job('interval', seconds=45) +def update_depots(): + # noinspection PyBroadException + try: + data = client.get_product_info(apps=[730, 2275500, 2275530], timeout=15)['apps'] + main_data = data[730] - # We also want the data to be up-to-date, so we check datetime - new_version_datetime = (dt.datetime.fromtimestamp(data.cs2_version_timestamp) - .replace(tzinfo=VALVE_TIMEZONE).astimezone(dt.UTC)) + public_build_id = int(main_data['depots']['branches']['public']['buildid']) + dpr_build_id = int(main_data['depots']['branches']['dpr']['buildid']) + dprp_build_id = int(main_data['depots']['branches']['dprp']['buildid']) - is_up_to_date = utime.utcnow() - new_version_datetime < dt.timedelta(hours=12) + cs2_app_change_number = data[2275500]['_change_number'] + cs2_server_change_number = data[2275530]['_change_number'] + except Exception: + logging.exception('Caught an exception while trying to fetch depots!') + return - if no_version_data_cached or (version_has_changed and is_up_to_date): - self.update_cache(data.asdict()) - return - except Exception: - logging.exception('Caught an exception while trying to get new version!') - await asyncio.sleep(45) + with open(config.CACHE_FILE_PATH, encoding='utf-8') as f: + cache = json.load(f) - # sometimes SteamDB updates the info much later (xPaw: Zzz...) - # because of this, we retry in an hour - await asyncio.sleep(60 * 60) - await self.update_game_version() + cache['cs2_app_changenumber'] = cs2_app_change_number + cache['cs2_server_changenumber'] = cs2_server_change_number + cache['dprp_build_id'] = dprp_build_id + cache['dpr_build_id'] = dpr_build_id - async def update_players_count(self): - player_count = await self.get_app(730).player_count() # currently doesn't work - freezes the function entirely - self.update_cache({'online_players': player_count}) + if public_build_id != cache.get('public_build_id'): + cache['public_build_id'] = public_build_id + gevent.spawn(update_game_version) - logger.info(f'Successfully dumped player count: {player_count}') + with open(config.CACHE_FILE_PATH, 'w', encoding='utf-8') as f: + json.dump(cache, f, indent=4) - async def update_players_count_alter(self): - response = api.get_number_of_current_players(appid=730).get('response') # getting this value from gc is more accurate + logging.info('Successfully dumped game build IDs.') - player_count = 0 - if response.get('result') == 1 and response.get('player_count'): - player_count = response['player_count'] - self.update_cache({'online_players': player_count}) +def update_game_version(): + timeout = 30 * 60 + timeout_start = time.time() + while time.time() < timeout_start + timeout: + # noinspection PyBroadException + try: + data = GameVersion.request() + + with open(config.CACHE_FILE_PATH, encoding='utf-8') as f: + cache = json.load(f) - logger.info(f'Successfully dumped player count: {player_count}') + # Made to ensure we will grab the latest public data if we *somehow* don't have anything cached + no_cached_data = (cache.get('cs2_client_version') is None) - def load_cache(self): - """Loads cache into ``self.cache``.""" + # We also want to ensure that the data is up-to-date, so we check datetime + new_data_datetime = (dt.datetime.fromtimestamp(data.cs2_version_timestamp) + .replace(tzinfo=VALVE_TIMEZONE).astimezone(dt.UTC)) + is_up_to_date = utime.utcnow() - new_data_datetime < dt.timedelta(hours=12) - with open(self.cache_file_path, encoding='utf-8') as f: - self.cache = json.load(f) + if no_cached_data or (is_up_to_date and data.cs2_client_version != cache.get('cs2_client_version')): + for key, value in data.asdict().items(): + cache[key] = value + + with open(config.CACHE_FILE_PATH, 'w', encoding='utf-8') as f: + json.dump(cache, f, indent=4) + sys.exit() + except Exception: + logging.exception('Caught an exception while trying to get new version!') + gevent.sleep(45) + continue + gevent.sleep(45) + # xPaw: Zzz... + # because of this, we retry in an hour + gevent.sleep(60 * 60) + update_game_version() - 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) +@scheduler.scheduled_job('interval', seconds=45) +def online_players(): + value = client.get_player_count(730) - 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.""" + with open(config.CACHE_FILE_PATH, 'r', encoding='utf-8') as f: + cache = json.load(f) - self.load_cache() + if value != cache.get('online_players'): + cache['online_players'] = value - for k, v in new_info.items(): - self.cache[k] = v + with open(config.CACHE_FILE_PATH, 'w', encoding='utf-8') as f: + json.dump(cache, f, indent=4) - self.dump_cache() + logging.info(f'Successfully dumped player count: {value}') def main(): - collector = GCCollector(cache_file_path=config.CACHE_FILE_PATH) - collector.run(username=config.STEAM_USERNAME, password=config.STEAM_PASS, debug=True) + try: + logging.info('Logging in...') + result = client.login(username=config.STEAM_USERNAME, password=config.STEAM_PASS) + + if result != EResult.OK: + logging.error(f"Failed to login: {result!r}") + sys.exit(1) + + logging.info('Logged in successfully.') + client.run_forever() + except KeyboardInterrupt: + if client.connected: + logging.info('Logout...') + client.logout() if __name__ == '__main__': diff --git a/requirements.txt b/requirements.txt index d1b95854b8af70e54f98821e9c933be656e1e4a1..d86ee4a2d09f8be98cc0f8822adc3c65b270bde7 100644 GIT binary patch delta 889 zcmYLIJ&O}z5T4B@n`95Ga30|d;q+37B-!um7D5oBMTjD2BUYPavj%e}_o-F~HX>pX zrn0uO_D6_VS{|a7{t4qV?_N$`hIe*8pLyop*IU1?)>nUHn$nz(=zwO>jwzwrI2N_Y zA!|9>DY#JW<=dX-@wQgI;o#GvD`e6gifM@WnaU8mO`dr9jdoP*Ky%39mG-1MLx)2| z??cwrB{JZLxqF*g#TIg<>Ihg;n)6S?Kii{Ms>30W#nh+v+Kb#&q3t3orT~b13ekUp zZy&df-V@kuh-fj5p@l%}Q$#N87ObHlgdU=c3tt3TV|YnV`Ae>%*?gXB@UDKXAw*=8 zr-(a+JO^JgkE7;R4DdUBP|hM-*atij=**;f{_c}JpAB|5B zEKD4rzXLWVm|7SSmLkXqa|u5YVr=T*9%8zT$MBM=0QthYisHw`ekH-)GB&~sv=)CT zp76MI9}|5pH923tu0^~mRk>T<-Eb5il9TYc5uM$|yfW7wEr7eNswJ@PQ&U-*@J)fG z3v^?(uMyBDz~JE?DujYQg2+x~vmVYEmH;-Hc&Oe+0BeLiUs*F8$NZz*t33i|J+)Q& z6EgO)KS6d=Gut{|77xsNL0+%T2j;fs@Y3Ac$u<;-6`kY_ouczZ@h_Y4*xu+HuJZf? D0>_xM delta 943 zcmY*YOKTHh6upy}G!rRVNDXa-Nl^(XY3GrU2Js0+Fa!h_F1nC5P1*_5nKqNurc1Zt zB3wlL39iM32trrl+LgNTZ>ab<_xomi@NqLU+;i_a-?=wm&OR#le*N4epCa0&G0r|+ z#qnr`)(}mary}Wm4Ac&V_zD!pL0)jqf0zSgee~%ojIHwRnYEmA@oLZ3*Sje`rYXAh zs1CGGd#H$Lf_LF>2pHidy;H2{O!W!In>}8vt|6ICMd8x` z&wye@xevOORO8**yExZGd5dov8~oO|c3zXDih3yBNwlOHn_p)v!{(2fi`F)%ddQDJ zI95*~kdA_O_-kgl_E+`*xQLEX6Ds%O#05b#xRR}y;&TgO^Yv`kR9524<)>N8X!5J< zd#j-gxC19c8iLjR82aAb^-X+}6fG6+3Seb;nMF}&GD#kLxP-W08J zqj2AF`DLMQrK6Fp>(e1Z9F&`ja)&<`YW%6N#OKTlB^j=;%Xs(5=TfFszYPsrh_PA$ z4^^xGU{~_dM%nlEWmy9+)D_S{m+v7LIN?Gr(^q*ix;*gK79-rjSbSF?H zjz!6TMq|~K=n7TAHbp!@Hc(qI0CEJzs=7&)YN}g^&PP<^@5RN1DR9CfHx)l*J}#F| Jck+6@e*+pjsz?9; diff --git a/utypes/profiles.py b/utypes/profiles.py index b59d952..4fdb3a8 100644 --- a/utypes/profiles.py +++ b/utypes/profiles.py @@ -1,10 +1,10 @@ from dataclasses import astuple, dataclass from enum import StrEnum -import hashlib import re from typing import NamedTuple, Self -from steam import ID, InvalidID +from steam import steamid +from steam.steamid import SteamID import requests import config @@ -242,9 +242,9 @@ def from_dict(cls, stats: dict) -> Self: @classmethod async def get(cls, data) -> Self: try: - steamid = await parse_steamid(data) + _id = parse_steamid(data) - response = api.get_user_game_stats(steamid=steamid.id64, appid=730) + response = api.get_user_game_stats(steamid=_id.as_64, appid=730) if not response: raise ParseUserStatsError(ErrorCode.PROFILE_IS_PRIVATE) @@ -252,7 +252,7 @@ async def get(cls, data) -> Self: raise ParseUserStatsError(ErrorCode.NO_STATS_AVAILABLE) stats_dict = {stat['name']: stat['value'] for stat in response['playerstats']['stats']} - stats_dict['steamid'] = steamid.id64 + stats_dict['steamid'] = _id.as_64 return cls.from_dict(stats_dict) except requests.exceptions.HTTPError as e: @@ -312,10 +312,10 @@ def _extract_faceit_data(data: dict): @classmethod async def get(cls, data) -> Self: try: - steamid = await parse_steamid(data) + _id = parse_steamid(data) - bans = api.get_player_bans(steamids=str(steamid.id64)) - user_data = api.get_player_summaries(steamids=str(steamid.id64))["response"]["players"][0] + bans = api.get_player_bans(steamids=str(_id.as_64)) + user_data = api.get_player_summaries(steamids=str(_id.as_64))["response"]["players"][0] vanity = user_data['profileurl'] @@ -325,10 +325,10 @@ async def get(cls, data) -> Self: account_created = user_data.get('timecreated') vanity_url = vanity.split('/')[-2] - if vanity_url == str(steamid.id64): + if vanity_url == str(_id.as_64): vanity_url = None - faceit_api_link = f'https://api.faceit.com/search/v2/players?query={steamid.id64}' + faceit_api_link = f'https://api.faceit.com/search/v2/players?query={_id.as_64}' faceit_api_response = requests.get(faceit_api_link, timeout=15).json()['payload']['results'] faceit_elo, faceit_lvl, faceit_url, faceit_ban = cls._extract_faceit_data(faceit_api_response) @@ -345,12 +345,12 @@ async def get(cls, data) -> Self: trade_ban = (bans_data['EconomyBan'] == 'banned') return cls(vanity_url, - steamid.id64, - steamid.id, + _id.as_64, + _id.id, account_created, - steamid.invite_url, - steamid.invite_code, - cls.get_csgo_friend_code(steamid), + _id.invite_url, + _id.as_invite_code, + _id.as_csgo_friend_code, faceit_url, faceit_elo, faceit_lvl, @@ -369,54 +369,27 @@ async def get(cls, data) -> Self: raise ParseUserStatsError(ErrorCode.PROFILE_IS_PRIVATE) raise e - @staticmethod - def get_csgo_friend_code(steamid: ID) -> str | None: - hashed = int.from_bytes( - hashlib.md5( - (b"CSGO" + steamid.id.to_bytes(4, "big"))[::-1], - ).digest()[:4], - "little", - ) - result = 0 - for i in range(8): - id_nib = (steamid.id64 >> (i * 4)) & 0xF - hash_nib = (hashed >> i) & 0x1 - a = (result << 4) | id_nib - result = ((result >> 28) << 32) | a - result = ((result >> 31) << 32) | ((a << 1) | hash_nib) - result = int.from_bytes(result.to_bytes(8, "big"), "little") - code: list[str] = [] - for i in range(13): - if i in {4, 9}: - code.append("-") - code.append(_csgofrcode_chars[result & 31]) - result >>= 5 - return "".join(code[5:]) - def to_tuple(self) -> tuple: return astuple(self) -async def parse_steamid(data: str) -> ID: +def parse_steamid(data: str) -> SteamID: data = data.strip() if STEAM_PROFILE_LINK_PATTERN.match(data): if not data.startswith('http'): data = 'https://' + data - if (steamid := await ID.from_url(data)) is None: + if (_id := steamid.from_url(data)) is None: raise ParseUserStatsError(ErrorCode.INVALID_LINK) - return steamid + return _id - try: - if (steamid := ID(data)).is_valid(): - return steamid - except InvalidID: - pass + if (_id := SteamID(data)).is_valid(): + return _id data = f'https://steamcommunity.com/id/{data}' - if (steamid := await ID.from_url(data)) is None: + if (_id := steamid.from_url(data)) is None: raise ParseUserStatsError(ErrorCode.INVALID_REQUEST) - return steamid + return _id From d719f00d94cdb30270a537ed373c1f72b76b0964 Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Fri, 17 May 2024 19:04:47 +0500 Subject: [PATCH 2/5] cstracker now works from one script --- collectors/game_coordinator.py | 97 ++++++++++++++++++++++++--------- collectors/gc_alerter.py | 99 ---------------------------------- 2 files changed, 73 insertions(+), 123 deletions(-) delete mode 100644 collectors/gc_alerter.py diff --git a/collectors/game_coordinator.py b/collectors/game_coordinator.py index 32a4557..a769410 100644 --- a/collectors/game_coordinator.py +++ b/collectors/game_coordinator.py @@ -1,6 +1,4 @@ -import steam.monkey -steam.monkey.patch_minimal() - +import asyncio import datetime as dt import json import logging @@ -9,9 +7,10 @@ import time from zoneinfo import ZoneInfo +from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.gevent import GeventScheduler from csgo.client import CSGOClient -import gevent +from pyrogram import Client, idle from steam.client import SteamClient from steam.enums import EResult @@ -25,19 +24,35 @@ # noinspection PyUnresolvedReferences import env import config -from functions import utime +from functions import locale, utime from utypes import GameVersion, States VALVE_TIMEZONE = ZoneInfo('America/Los_Angeles') +loc = locale('ru') + +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 | GC: %(message)s', datefmt='%H:%M:%S — %d/%m/%Y') +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) client = SteamClient() client.set_credential_location(config.STEAM_CREDS_PATH) cs = CSGOClient(client) -scheduler = GeventScheduler() +gevent_scheduler = GeventScheduler() +async_scheduler = AsyncIOScheduler() @client.on('error') @@ -75,7 +90,8 @@ def handle_disconnect(): @client.on('logged_on') def handle_after_logon(): cs.launch() - scheduler.start() + async_scheduler.start() + gevent_scheduler.start() @cs.on('ready') @@ -101,8 +117,8 @@ def update_gc_status(status): logging.info(f'Successfully dumped game coordinator status: {game_coordinator.literal}') -@scheduler.scheduled_job('interval', seconds=45) -def update_depots(): +@async_scheduler.scheduled_job('interval', seconds=45) +async def update_depots(): # noinspection PyBroadException try: data = client.get_product_info(apps=[730, 2275500, 2275530], timeout=15)['apps'] @@ -121,14 +137,24 @@ def update_depots(): with open(config.CACHE_FILE_PATH, encoding='utf-8') as f: cache = json.load(f) - cache['cs2_app_changenumber'] = cs2_app_change_number - cache['cs2_server_changenumber'] = cs2_server_change_number - cache['dprp_build_id'] = dprp_build_id - cache['dpr_build_id'] = dpr_build_id - - if public_build_id != cache.get('public_build_id'): - cache['public_build_id'] = public_build_id - gevent.spawn(update_game_version) + new_data = {'cs2_app_changenumber': cs2_app_change_number, + 'cs2_server_changenumber': cs2_server_change_number, + 'dprp_build_id': dprp_build_id, + 'dpr_build_id': dpr_build_id, + 'public_build_id': public_build_id} + + for _id, new_value in new_data.items(): + if cache.get(_id) != new_value: + 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 _id == 'dprp_build_id' and new_value == cache['public_build_id']: + await send_alert('dprp_build_sync_id', new_value) + continue + if _id == 'public_build_id': + _ = asyncio.create_task(update_game_version) + await send_alert(_id, new_value) with open(config.CACHE_FILE_PATH, 'w', encoding='utf-8') as f: json.dump(cache, f, indent=4) @@ -164,16 +190,16 @@ def update_game_version(): sys.exit() except Exception: logging.exception('Caught an exception while trying to get new version!') - gevent.sleep(45) + asyncio.sleep(45) continue - gevent.sleep(45) + asyncio.sleep(45) # xPaw: Zzz... # because of this, we retry in an hour - gevent.sleep(60 * 60) + asyncio.sleep(60 * 60) update_game_version() -@scheduler.scheduled_job('interval', seconds=45) +@gevent_scheduler.scheduled_job('interval', seconds=45) def online_players(): value = client.get_player_count(730) @@ -189,7 +215,29 @@ def online_players(): logging.info(f'Successfully dumped player count: {value}') -def main(): +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 not config.TEST_MODE: + chat_list = [config.INCS2CHAT, config.CSTRACKER] + else: + chat_list = [config.AQ] + + 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) + + +async def main(): try: logging.info('Logging in...') result = client.login(username=config.STEAM_USERNAME, password=config.STEAM_PASS) @@ -199,7 +247,8 @@ def main(): sys.exit(1) logging.info('Logged in successfully.') - client.run_forever() + await bot.start() + await idle() except KeyboardInterrupt: if client.connected: logging.info('Logout...') @@ -207,4 +256,4 @@ def main(): if __name__ == '__main__': - main() + asyncio.run(main()) diff --git a/collectors/gc_alerter.py b/collectors/gc_alerter.py deleted file mode 100644 index 4a699cc..0000000 --- a/collectors/gc_alerter.py +++ /dev/null @@ -1,99 +0,0 @@ -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() - -# noinspection PyUnresolvedReferences -import env -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} 🔃', - '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=45) -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 _id == 'dpr_build_id' and new_value == cache['public_build_id']: - await send_alert('dpr_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) - 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 not config.TEST_MODE: - chat_list = [config.INCS2CHAT, config.CSTRACKER] - else: - chat_list = [config.AQ] - - 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() From 04edadb375a05bb7d5ba5f372eb340926f9316e3 Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Sun, 19 May 2024 21:28:46 +0500 Subject: [PATCH 3/5] Now we can --- functions/info_formatters.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/functions/info_formatters.py b/functions/info_formatters.py index 8094177..215703f 100644 --- a/functions/info_formatters.py +++ b/functions/info_formatters.py @@ -75,10 +75,6 @@ def format_server_status(data: ServerStatusData, locale: Locale) -> str: game_servers_dt = f'{format_datetime(game_servers_dt, "HH:mm:ss, dd MMM", locale=lang_code).title()} (UTC)' text = ( - f'
Currently we\'re unable to detect CS2\'s game coordinator status, ' - f'because one of our dependencies is broken. Other trackers are working fine. ' - f'Please accept our apologies.
' - f'\n\n' f'{locale.game_status_text.format(tick, *states)}' f'\n\n' f'{locale.latest_data_update.format(game_servers_dt)}' From 5f84eae2f561a90e53dcb0ef27a26b9251698e19 Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Sun, 19 May 2024 21:29:33 +0500 Subject: [PATCH 4/5] Removed env and moved collectors to the root for easier merge --- collectors/env.py | 15 - collectors/core.py => core.py | 0 ...game_coordinator.py => game_coordinator.py | 0 ...layers_graph.py => online_players_graph.py | 264 +++++++++--------- plugins/env.py | 15 - plugins/incs2chat.py | 2 - plugins/inline.py | 2 - 7 files changed, 132 insertions(+), 166 deletions(-) delete mode 100644 collectors/env.py rename collectors/core.py => core.py (100%) rename collectors/game_coordinator.py => game_coordinator.py (100%) rename collectors/online_players_graph.py => online_players_graph.py (97%) delete mode 100644 plugins/env.py 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/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/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 ce0ec9f..6084a76 100644 --- a/collectors/online_players_graph.py +++ b/online_players_graph.py @@ -1,132 +1,132 @@ -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 - -# noinspection PyUnresolvedReferences -import env -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) - 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 + +# noinspection PyUnresolvedReferences +import env +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) + except Exception: + logging.exception('Caught exception in graph maker!') + time.sleep(MINUTE) + return graph_maker() + + +def main(): + scheduler.start() + + +if __name__ == "__main__": + main() 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 99c2613..c01b1d0 100644 --- a/plugins/incs2chat.py +++ b/plugins/incs2chat.py @@ -7,8 +7,6 @@ from pyrogram.enums import ChatMembersFilter, MessageEntityType from pyrogram.types import Message, MessageEntity -# noinspection PyUnresolvedReferences -import env import config 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 d0afc9a523592472fdc5e2bb2ccad240a01c5edd Mon Sep 17 00:00:00 2001 From: SyberiaK Date: Sun, 19 May 2024 21:29:33 +0500 Subject: [PATCH 5/5] Removed env and moved collectors to the root for easier merge --- collectors/env.py | 15 - collectors/core.py => core.py | 2 - ...game_coordinator.py => game_coordinator.py | 2 - ...layers_graph.py => online_players_graph.py | 262 +++++++++--------- plugins/env.py | 15 - plugins/incs2chat.py | 2 - plugins/inline.py | 2 - 7 files changed, 130 insertions(+), 170 deletions(-) delete mode 100644 collectors/env.py rename collectors/core.py => core.py (99%) rename collectors/game_coordinator.py => game_coordinator.py (99%) rename collectors/online_players_graph.py => online_players_graph.py (96%) delete mode 100644 plugins/env.py 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/core.py b/core.py similarity index 99% rename from collectors/core.py rename to core.py index 2cee155..5e83d5c 100644 --- a/collectors/core.py +++ b/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/game_coordinator.py b/game_coordinator.py similarity index 99% rename from collectors/game_coordinator.py rename to game_coordinator.py index a769410..97827c3 100644 --- a/collectors/game_coordinator.py +++ b/game_coordinator.py @@ -21,8 +21,6 @@ uvloop.install() -# noinspection PyUnresolvedReferences -import env import config from functions import locale, utime from utypes import GameVersion, States diff --git a/collectors/online_players_graph.py b/online_players_graph.py similarity index 96% rename from collectors/online_players_graph.py rename to online_players_graph.py index ce0ec9f..6521b38 100644 --- a/collectors/online_players_graph.py +++ b/online_players_graph.py @@ -1,132 +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 - -# noinspection PyUnresolvedReferences -import env -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) - 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) + except Exception: + logging.exception('Caught exception in graph maker!') + time.sleep(MINUTE) + return graph_maker() + + +def main(): + scheduler.start() + + +if __name__ == "__main__": + main() 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 99c2613..c01b1d0 100644 --- a/plugins/incs2chat.py +++ b/plugins/incs2chat.py @@ -7,8 +7,6 @@ from pyrogram.enums import ChatMembersFilter, MessageEntityType from pyrogram.types import Message, MessageEntity -# noinspection PyUnresolvedReferences -import env import config 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