diff --git a/pagermaid/__init__.py b/pagermaid/__init__.py index 7e6b0a6..6236219 100644 --- a/pagermaid/__init__.py +++ b/pagermaid/__init__.py @@ -14,7 +14,7 @@ import pyromod.listen from pyrogram import Client import sys -pgm_version = "1.2.3" +pgm_version = "1.2.4" CMD_LIST = {} module_dir = __path__[0] working_dir = getcwd() diff --git a/pagermaid/__main__.py b/pagermaid/__main__.py index 13efb47..d9cd89d 100644 --- a/pagermaid/__main__.py +++ b/pagermaid/__main__.py @@ -7,6 +7,7 @@ from pyrogram import idle from pagermaid import bot, logs, working_dir from pagermaid.hook import Hook from pagermaid.modules import module_list, plugin_list +from pagermaid.single_utils import safe_remove from pagermaid.utils import lang, process_exit path.insert(1, f"{working_dir}{sep}plugins") @@ -18,6 +19,9 @@ async def main(): await bot.start() me = await bot.get_me() + if me.is_bot: + safe_remove("pagermaid.session") + exit() logs.info(f"{lang('save_id')} {me.first_name}({me.id})") for module_name in module_list: diff --git a/pagermaid/config.py b/pagermaid/config.py index 9e93e87..8e7b214 100644 --- a/pagermaid/config.py +++ b/pagermaid/config.py @@ -5,7 +5,7 @@ from yaml import load, FullLoader, safe_load from shutil import copyfile -def strtobool(val): +def strtobool(val, default=False): """Convert a string representation of truth to true (1) or false (0). True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values @@ -18,7 +18,8 @@ def strtobool(val): elif val in ('n', 'no', 'f', 'false', 'off', '0'): return 0 else: - raise ValueError("invalid truth value %r" % (val,)) + print("[Degrade] invalid truth value %r" % (val,)) + return default try: @@ -35,17 +36,20 @@ class Config: API_HASH = os.environ.get("API_HASH", config["api_hash"]) STRING_SESSION = os.environ.get("STRING_SESSION") DEBUG = strtobool(os.environ.get("PGM_DEBUG", config["debug"])) - ERROR_REPORT = strtobool(os.environ.get("PGM_ERROR_REPORT", config["error_report"])) + ERROR_REPORT = strtobool(os.environ.get("PGM_ERROR_REPORT", config["error_report"]), True) LANGUAGE = os.environ.get("PGM_LANGUAGE", config["application_language"]) REGION = os.environ.get("PGM_REGION", config["application_region"]) TTS = os.environ.get("PGM_TTS", config["application_tts"]) LOG = strtobool(os.environ.get("PGM_LOG", config["log"])) LOG_ID = int(os.environ.get("PGM_LOG_ID", config["log_chatid"])) IPV6 = strtobool(os.environ.get("PGM_IPV6", config["ipv6"])) + ALLOW_ANALYTIC = strtobool(os.environ.get("PGM_ALLOW_ANALYTIC", config["allow_analytic"]), True) + SENTRY_API = "https://0785960e63e04279a694d0486d47d9ea@o1342815.ingest.sentry.io/6617119" + MIXPANEL_API = "c79162511383b0fa1e9c062a2a86c855" TIME_FORM = os.environ.get("PGM_TIME_FORM", config["time_form"]) DATE_FORM = os.environ.get("PGM_DATE_FORM", config["date_form"]) START_FORM = os.environ.get("PGM_START_FORM", config["start_form"]) - SILENT = strtobool(os.environ.get("PGM_PGM_SILENT", config["silent"])) + SILENT = strtobool(os.environ.get("PGM_PGM_SILENT", config["silent"]), True) PROXY_ADDRESS = os.environ.get("PGM_PROXY_ADDRESS", config["proxy_addr"]) PROXY_PORT = os.environ.get("PGM_PROXY_PORT", config["proxy_port"]) PROXY = None diff --git a/pagermaid/hook.py b/pagermaid/hook.py index 089a699..da4a821 100644 --- a/pagermaid/hook.py +++ b/pagermaid/hook.py @@ -1,4 +1,5 @@ import asyncio +import sys from pyrogram import StopPropagation @@ -81,31 +82,64 @@ class Hook: logs.info(f"[shutdown]: {type(exception)}: {exception}") @staticmethod - async def command_pre(message: Message): - if cors := [pre(**inject(message, pre)) for pre in hook_functions["command_pre"]]: # noqa - try: + async def command_pre(message: Message, command): + cors = [] + try: + for pre in hook_functions["command_pre"]: + try: + data = inject(message, pre, command=command) + except Exception as exception: + logs.info(f"[process_error]: {type(exception)}: {exception}") + continue + cors.append(pre(**data)) # noqa + if cors: await asyncio.gather(*cors) - except StopPropagation as e: - raise StopPropagation from e - except Exception as exception: - logs.info(f"[command_pre]: {type(exception)}: {exception}") + except SystemExit: + await Hook.shutdown() + sys.exit(0) + except StopPropagation as e: + raise StopPropagation from e + except Exception as exception: + logs.info(f"[command_pre]: {type(exception)}: {exception}") @staticmethod - async def command_post(message: Message): - if cors := [post(**inject(message, post)) for post in hook_functions["command_post"]]: # noqa - try: + async def command_post(message: Message, command): + cors = [] + try: + for post in hook_functions["command_post"]: + try: + data = inject(message, post, command=command) + except Exception as exception: + logs.info(f"[process_error]: {type(exception)}: {exception}") + continue + cors.append(post(**data)) # noqa + if cors: await asyncio.gather(*cors) - except StopPropagation as e: - raise StopPropagation from e - except Exception as exception: - logs.info(f"[command_post]: {type(exception)}: {exception}") + except SystemExit: + await Hook.shutdown() + sys.exit(0) + except StopPropagation as e: + raise StopPropagation from e + except Exception as exception: + logs.info(f"[command_post]: {type(exception)}: {exception}") @staticmethod - async def process_error_exec(message: Message, exc_info: BaseException, exc_format: str): - if cors := [error(**inject(message, error, exc_info=exc_info, exc_format=exc_format)) for error in hook_functions["process_error"]]: # noqa - try: + async def process_error_exec(message: Message, command, exc_info: BaseException, exc_format: str): + cors = [] + try: + for error in hook_functions["process_error"]: + try: + data = inject(message, error, command=command, exc_info=exc_info, exc_format=exc_format) + except Exception as exception: + logs.info(f"[process_error]: {type(exception)}: {exception}") + continue + cors.append(error(**data)) # noqa + if cors: await asyncio.gather(*cors) - except StopPropagation as e: - raise StopPropagation from e - except Exception as exception: - logs.info(f"[process_error]: {type(exception)}: {exception}") + except SystemExit: + await Hook.shutdown() + sys.exit(0) + except StopPropagation as e: + raise StopPropagation from e + except Exception as exception: + logs.info(f"[process_error]: {type(exception)}: {exception}") diff --git a/pagermaid/listener.py b/pagermaid/listener.py index d6d33d2..2337d0d 100644 --- a/pagermaid/listener.py +++ b/pagermaid/listener.py @@ -6,6 +6,7 @@ from time import strftime, gmtime, time from traceback import format_exc from pyrogram import ContinuePropagation, StopPropagation, filters, Client +from pyrogram.errors import Flood, Forbidden from pyrogram.errors.exceptions.bad_request_400 import ( MessageIdInvalid, MessageNotModified, @@ -139,7 +140,7 @@ def listener(**args): read_context[(message.chat.id, message.id)] = True if command: - await Hook.command_pre(message) + await Hook.command_pre(message, command) if data := inject(message, function): await function(**data) else: @@ -150,12 +151,12 @@ def listener(**args): elif function.__code__.co_argcount == 2: await function(client, message) if command: - await Hook.command_post(message) + await Hook.command_post(message, command) except StopPropagation as e: raise StopPropagation from e except KeyboardInterrupt as e: raise KeyboardInterrupt from e - except MessageNotModified: + except (UserNotParticipant, MessageNotModified, MessageEmpty, Flood, Forbidden): pass except MessageIdInvalid: logs.warning( @@ -179,8 +180,6 @@ def listener(**args): ) with contextlib.suppress(BaseException): await message.edit(lang("reload_des")) - except UserNotParticipant: - pass except ContinuePropagation as e: if block_process: raise StopPropagation from e @@ -196,12 +195,12 @@ def listener(**args): await message.edit(lang("run_error"), no_reply=True) # noqa if not diagnostics: return - await Hook.process_error_exec(message, exc_info, exc_format) if Config.ERROR_REPORT: report = f"""# Generated: {strftime('%H:%M %d/%m/%Y', gmtime())}. \n# ChatID: {message.chat.id}. \n# UserID: {message.from_user.id if message.from_user else message.sender_chat.id}. \n# Message: \n-----BEGIN TARGET MESSAGE-----\n{message.text or message.caption}\n-----END TARGET MESSAGE-----\n# Traceback: \n-----BEGIN TRACEBACK-----\n{str(exc_format)}\n-----END TRACEBACK-----\n# Error: "{str(exc_info)}". \n""" await attach_report(report, f"exception.{time()}.pgp.txt", None, "PGP Error report generated.") + await Hook.process_error_exec(message, command, exc_info, exc_format) if (message.chat.id, message.id) in read_context: del read_context[(message.chat.id, message.id)] if block_process: @@ -274,9 +273,7 @@ def raw_listener(filter_s): await process_exit(start=False, _client=client, message=message) await Hook.shutdown() sys.exit(0) - except UserNotParticipant: - pass - except MessageEmpty: + except (UserNotParticipant, MessageNotModified, MessageEmpty, Flood, Forbidden): pass except BaseException: exc_info = sys.exc_info()[1] diff --git a/pagermaid/modules/mixpanel.py b/pagermaid/modules/mixpanel.py new file mode 100644 index 0000000..25fa9be --- /dev/null +++ b/pagermaid/modules/mixpanel.py @@ -0,0 +1,29 @@ +from pagermaid import Config +from pagermaid.enums import Client, Message +from pagermaid.hook import Hook + +from mixpanel import Mixpanel + + +mp = Mixpanel(Config.MIXPANEL_API) + + +@Hook.on_startup() +async def mixpanel_init_id(bot: Client): + me = await bot.get_me() + if me.username: + mp.people_set(str(me.id), {'$first_name': me.first_name, "username": me.username}) + else: + mp.people_set(str(me.id), {'$first_name': me.first_name}) + + +@Hook.command_postprocessor() +async def mixpanel_report(bot: Client, message: Message, command): + if not Config.ALLOW_ANALYTIC: + return + me = await bot.get_me() + sender_id = message.from_user.id if message.from_user else "" + sender_id = message.sender_chat.id if message.sender_chat else sender_id + if sender_id < 0 and message.outgoing: + sender_id = me.id + mp.track(str(sender_id), f'Function {command}', {'command': command, "bot_id": me.id}) diff --git a/pagermaid/modules/prune.py b/pagermaid/modules/prune.py index 81d217f..aaa6f2f 100644 --- a/pagermaid/modules/prune.py +++ b/pagermaid/modules/prune.py @@ -4,7 +4,8 @@ from asyncio import sleep from pagermaid import log from pagermaid.listener import listener -from pagermaid.utils import lang, Message +from pagermaid.enums import Client, Message +from pagermaid.utils import lang import contextlib @@ -44,40 +45,31 @@ async def prune(message: Message): need_admin=True, description=lang('sp_des'), parameters=lang('sp_parameters')) -async def self_prune(message: Message): +async def self_prune(bot: Client, message: Message): """ Deletes specific amount of messages you sent. """ msgs = [] count_buffer = 0 + offset = 0 if len(message.parameter) != 1: if not message.reply_to_message: return await message.edit(lang('arg_error')) - async for msg in message.bot.search_messages( - message.chat.id, - from_user="me", - offset=message.reply_to_message.id, - ): - msgs.append(msg.id) - count_buffer += 1 - if len(msgs) == 100: - await message.bot.delete_messages(message.chat.id, msgs) - msgs = [] - if msgs: - await message.bot.delete_messages(message.chat.id, msgs) - if count_buffer == 0: - await message.delete() - count_buffer += 1 - await log(f"{lang('prune_hint1')}{lang('sp_hint')} {str(count_buffer)} {lang('prune_hint2')}") - notification = await send_prune_notify(message, count_buffer, count_buffer) - await sleep(1) - await notification.delete() - return + offset = message.reply_to_message.id try: count = int(message.parameter[0]) await message.delete() except ValueError: await message.edit(lang('arg_error')) return - async for msg in message.bot.search_messages(message.chat.id, from_user="me"): + async for msg in bot.get_chat_history(message.chat.id, limit=100): + if count_buffer == count: + break + if msg.from_user and msg.from_user.is_self: + msgs.append(msg.id) + count_buffer += 1 + if len(msgs) == 100: + await message.bot.delete_messages(message.chat.id, msgs) + msgs = [] + async for msg in bot.search_messages(message.chat.id, from_user="me", offset=offset): if count_buffer == count: break msgs.append(msg.id) @@ -101,7 +93,7 @@ async def self_prune(message: Message): need_admin=True, description=lang('yp_des'), parameters=lang('sp_parameters')) -async def your_prune(message: Message): +async def your_prune(bot: Client, message: Message): """ Deletes specific amount of messages someone sent. """ if not message.reply_to_message: return await message.edit(lang('not_reply')) @@ -119,18 +111,34 @@ async def your_prune(message: Message): except Exception: # noqa pass count_buffer = 0 - async for msg in message.bot.search_messages(message.chat.id, from_user=target.from_user.id): + msgs = [] + async for msg in bot.get_chat_history(message.chat.id, limit=100): + if count_buffer == count: + break + if msg.from_user and msg.from_user.id == target.from_user.id: + msgs.append(msg.id) + count_buffer += 1 + if len(msgs) == 100: + await message.bot.delete_messages(message.chat.id, msgs) + msgs = [] + async for msg in bot.search_messages(message.chat.id, from_user=target.from_user.id): if count_buffer == count: break - await msg.delete() count_buffer += 1 + msgs.append(msg.id) + if len(msgs) == 100: + await message.bot.delete_messages(message.chat.id, msgs) + msgs = [] + if msgs: + await message.bot.delete_messages(message.chat.id, msgs) await log( f"{lang('prune_hint1')}{lang('yp_hint')} {str(count_buffer)} / {count} {lang('prune_hint2')}" ) - notification = await send_prune_notify(message, count_buffer, count) - await sleep(1) - await notification.delete() + with contextlib.suppress(ValueError): + notification = await send_prune_notify(message, count_buffer, count) + await sleep(1) + await notification.delete() @listener(is_plugin=False, outgoing=True, command="del", diff --git a/pagermaid/modules/sentry.py b/pagermaid/modules/sentry.py new file mode 100644 index 0000000..dc82549 --- /dev/null +++ b/pagermaid/modules/sentry.py @@ -0,0 +1,58 @@ +import sentry_sdk + +from subprocess import run, PIPE +from time import time + +from pyrogram.errors import Unauthorized + +from pagermaid import Config +from pagermaid.enums import Client, Message +from pagermaid.hook import Hook +from pagermaid.single_utils import safe_remove + + +def sentry_before_send(event, hint): + global sentry_sdk_report_time + exc_info = hint.get("exc_info") + if exc_info and isinstance(exc_info[1], Unauthorized): + # The user has been deleted/deactivated or session revoked + safe_remove('pagermaid.session') + exit(1) + if time() <= sentry_sdk_report_time + 30: + sentry_sdk_report_time = time() + return None + else: + sentry_sdk_report_time = time() + return event + + +sentry_sdk_report_time = time() +sentry_sdk_git_hash = run("git rev-parse HEAD", stdout=PIPE, shell=True).stdout.decode().strip() +sentry_sdk.init( + Config.SENTRY_API, + traces_sample_rate=1.0, + release=sentry_sdk_git_hash, + before_send=sentry_before_send, + environment="production", +) + + +@Hook.on_startup() +async def sentry_init_id(bot: Client): + me = await bot.get_me() + if me.username: + sentry_sdk.set_user({"id": me.id, "name": me.first_name, "username": me.username, "ip_address": "{{auto}}"}) + else: + sentry_sdk.set_user({"id": me.id, "name": me.first_name, "ip_address": "{{auto}}"}) + + +@Hook.process_error() +async def sentry_report(message: Message, command, exc_info, **_): + sender_id = message.from_user.id if message.from_user else "" + sender_id = message.sender_chat.id if message.sender_chat else sender_id + sentry_sdk.set_context("Target", {"ChatID": str(message.chat.id), + "UserID": str(sender_id), + "Msg": message.text or ""}) + if command: + sentry_sdk.set_tag("com", command) + sentry_sdk.capture_exception(exc_info) diff --git a/requirements.txt b/requirements.txt index 2cc6811..852e3fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,6 @@ psutil>=5.8.0 httpx apscheduler sqlitedict -casbin==1.16.9 +casbin==1.16.11 +mixpanel +sentry-sdk==1.9.0