PaiGram/core/plugin.py

485 lines
16 KiB
Python
Raw Normal View History

import copy
import datetime
import re
from importlib import import_module
from re import Pattern
from types import MethodType
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar, Union
# noinspection PyProtectedMember
from telegram._utils.defaultvalue import DEFAULT_TRUE
# noinspection PyProtectedMember
from telegram._utils.types import DVInput, JSONDict
from telegram.ext import BaseHandler, ConversationHandler, Job
# noinspection PyProtectedMember
from telegram.ext._utils.types import JobCallback
from telegram.ext.filters import BaseFilter
from typing_extensions import ParamSpec
__all__ = ["Plugin", "handler", "conversation", "job", "error_handler"]
P = ParamSpec("P")
T = TypeVar("T")
HandlerType = TypeVar("HandlerType", bound=BaseHandler)
TimeType = Union[float, datetime.timedelta, datetime.datetime, datetime.time]
_Module = import_module("telegram.ext")
_NORMAL_HANDLER_ATTR_NAME = "_handler_data"
_CONVERSATION_HANDLER_ATTR_NAME = "_conversation_data"
_JOB_ATTR_NAME = "_job_data"
_EXCLUDE_ATTRS = ["handlers", "jobs", "error_handlers"]
class _Plugin:
def _make_handler(self, datas: Union[List[Dict], Dict]) -> List[HandlerType]:
result = []
if isinstance(datas, list):
for data in filter(lambda x: x, datas):
func = getattr(self, data.pop("func"))
result.append(data.pop("type")(callback=func, **data.pop("kwargs")))
else:
func = getattr(self, datas.pop("func"))
result.append(datas.pop("type")(callback=func, **datas.pop("kwargs")))
return result
@property
def handlers(self) -> List[HandlerType]:
result = []
for attr in dir(self):
# noinspection PyUnboundLocalVariable
if (
not (attr.startswith("_") or attr in _EXCLUDE_ATTRS)
and isinstance(func := getattr(self, attr), MethodType)
and (datas := getattr(func, _NORMAL_HANDLER_ATTR_NAME, None))
):
for data in datas:
if data["type"] not in ["error", "new_chat_member"]:
result.extend(self._make_handler(data))
return result
def _new_chat_members_handler_funcs(self) -> List[Tuple[int, Callable]]:
result = []
for attr in dir(self):
# noinspection PyUnboundLocalVariable
if (
not (attr.startswith("_") or attr in _EXCLUDE_ATTRS)
and isinstance(func := getattr(self, attr), MethodType)
and (datas := getattr(func, _NORMAL_HANDLER_ATTR_NAME, None))
):
for data in datas:
if data and data["type"] == "new_chat_member":
result.append((data["priority"], func))
return result
@property
def error_handlers(self) -> Dict[Callable, bool]:
result = {}
for attr in dir(self):
# noinspection PyUnboundLocalVariable
if (
not (attr.startswith("_") or attr in _EXCLUDE_ATTRS)
and isinstance(func := getattr(self, attr), MethodType)
and (datas := getattr(func, _NORMAL_HANDLER_ATTR_NAME, None))
):
for data in datas:
if data and data["type"] == "error":
result.update({func: data["block"]})
return result
@property
def jobs(self) -> List[Job]:
from core.bot import bot
result = []
for attr in dir(self):
# noinspection PyUnboundLocalVariable
if (
not (attr.startswith("_") or attr in _EXCLUDE_ATTRS)
and isinstance(func := getattr(self, attr), MethodType)
and (datas := getattr(func, _JOB_ATTR_NAME, None))
):
for data in datas:
_job = getattr(bot.job_queue, data.pop("type"))(
callback=func, **data.pop("kwargs"), **{key: data.pop(key) for key in list(data.keys())}
)
result.append(_job)
return result
class _Conversation(_Plugin):
_conversation_kwargs: Dict
def __init_subclass__(cls, **kwargs):
cls._conversation_kwargs = kwargs
super(_Conversation, cls).__init_subclass__()
return cls
@property
def handlers(self) -> List[HandlerType]:
result: List[HandlerType] = []
entry_points: List[HandlerType] = []
states: Dict[Any, List[HandlerType]] = {}
fallbacks: List[HandlerType] = []
for attr in dir(self):
# noinspection PyUnboundLocalVariable
if (
not (attr.startswith("_") or attr == "handlers")
and isinstance(func := getattr(self, attr), Callable)
and (handler_datas := getattr(func, _NORMAL_HANDLER_ATTR_NAME, None))
):
conversation_data = getattr(func, _CONVERSATION_HANDLER_ATTR_NAME, None)
if attr == "cancel":
handler_datas = copy.deepcopy(handler_datas)
conversation_data = copy.deepcopy(conversation_data)
_handlers = self._make_handler(handler_datas)
if conversation_data:
if (_type := conversation_data.pop("type")) == "entry":
entry_points.extend(_handlers)
elif _type == "state":
if (key := conversation_data.pop("state")) in states:
states[key].extend(_handlers)
else:
states[key] = _handlers
elif _type == "fallback":
fallbacks.extend(_handlers)
else:
result.extend(_handlers)
if entry_points or states or fallbacks:
result.append(
ConversationHandler(
entry_points, states, fallbacks, **self.__class__._conversation_kwargs # pylint: disable=W0212
)
)
return result
class Plugin(_Plugin):
Conversation = _Conversation
class _Handler:
def __init__(self, **kwargs):
self.kwargs = kwargs
@property
def _type(self) -> Type[BaseHandler]:
return getattr(_Module, f"{self.__class__.__name__.strip('_')}Handler")
def __call__(self, func: Callable[P, T]) -> Callable[P, T]:
data = {"type": self._type, "func": func.__name__, "kwargs": self.kwargs}
if hasattr(func, _NORMAL_HANDLER_ATTR_NAME):
handler_datas = getattr(func, _NORMAL_HANDLER_ATTR_NAME)
handler_datas.append(data)
setattr(func, _NORMAL_HANDLER_ATTR_NAME, handler_datas)
else:
setattr(func, _NORMAL_HANDLER_ATTR_NAME, [data])
return func
class _CallbackQuery(_Handler):
def __init__(
self,
pattern: Union[str, Pattern, type, Callable[[object], Optional[bool]]] = None,
block: DVInput[bool] = DEFAULT_TRUE,
):
super(_CallbackQuery, self).__init__(pattern=pattern, block=block)
class _ChatJoinRequest(_Handler):
def __init__(self, block: DVInput[bool] = DEFAULT_TRUE):
super(_ChatJoinRequest, self).__init__(block=block)
class _ChatMember(_Handler):
def __init__(self, chat_member_types: int = -1):
super().__init__(chat_member_types=chat_member_types)
class _ChosenInlineResult(_Handler):
def __init__(self, block: DVInput[bool] = DEFAULT_TRUE, pattern: Union[str, Pattern] = None):
super().__init__(block=block, pattern=pattern)
class _Command(_Handler):
def __init__(self, command: str, filters: "BaseFilter" = None, block: DVInput[bool] = DEFAULT_TRUE):
super(_Command, self).__init__(command=command, filters=filters, block=block)
class _InlineQuery(_Handler):
def __init__(
self, pattern: Union[str, Pattern] = None, block: DVInput[bool] = DEFAULT_TRUE, chat_types: List[str] = None
):
super().__init__(pattern=pattern, block=block, chat_types=chat_types)
class _MessageNewChatMembers(_Handler):
def __init__(self, func: Callable[P, T] = None, *, priority: int = 5):
super().__init__()
self.func = func
self.priority = priority
def __call__(self, func: Callable[P, T] = None) -> Callable[P, T]:
self.func = self.func or func
data = {"type": "new_chat_member", "priority": self.priority}
if hasattr(func, _NORMAL_HANDLER_ATTR_NAME):
handler_datas = getattr(func, _NORMAL_HANDLER_ATTR_NAME)
handler_datas.append(data)
setattr(func, _NORMAL_HANDLER_ATTR_NAME, handler_datas)
else:
setattr(func, _NORMAL_HANDLER_ATTR_NAME, [data])
return func
class _Message(_Handler):
def __init__(
self,
filters: "BaseFilter",
block: DVInput[bool] = DEFAULT_TRUE,
):
super(_Message, self).__init__(filters=filters, block=block)
new_chat_members = _MessageNewChatMembers
class _PollAnswer(_Handler):
def __init__(self, block: DVInput[bool] = DEFAULT_TRUE):
super(_PollAnswer, self).__init__(block=block)
class _Poll(_Handler):
def __init__(self, block: DVInput[bool] = DEFAULT_TRUE):
super(_Poll, self).__init__(block=block)
class _PreCheckoutQuery(_Handler):
def __init__(self, block: DVInput[bool] = DEFAULT_TRUE):
super(_PreCheckoutQuery, self).__init__(block=block)
class _Prefix(_Handler):
def __init__(
self,
prefix: str,
command: str,
filters: BaseFilter = None,
block: DVInput[bool] = DEFAULT_TRUE,
):
super(_Prefix, self).__init__(prefix=prefix, command=command, filters=filters, block=block)
class _ShippingQuery(_Handler):
def __init__(self, block: DVInput[bool] = DEFAULT_TRUE):
super(_ShippingQuery, self).__init__(block=block)
class _StringCommand(_Handler):
def __init__(self, command: str):
super(_StringCommand, self).__init__(command=command)
class _StringRegex(_Handler):
def __init__(self, pattern: Union[str, Pattern], block: DVInput[bool] = DEFAULT_TRUE):
super(_StringRegex, self).__init__(pattern=pattern, block=block)
class _Type(_Handler):
# noinspection PyShadowingBuiltins
def __init__(
self, type: Type, strict: bool = False, block: DVInput[bool] = DEFAULT_TRUE # pylint: disable=redefined-builtin
):
super(_Type, self).__init__(type=type, strict=strict, block=block)
# noinspection PyPep8Naming
class handler(_Handler):
def __init__(self, handler_type: Callable[P, HandlerType], **kwargs: P.kwargs):
self._type_ = handler_type
super(handler, self).__init__(**kwargs)
@property
def _type(self) -> Type[BaseHandler]:
# noinspection PyTypeChecker
return self._type_
callback_query = _CallbackQuery
chat_join_request = _ChatJoinRequest
chat_member = _ChatMember
chosen_inline_result = _ChosenInlineResult
command = _Command
inline_query = _InlineQuery
message = _Message
poll_answer = _PollAnswer
pool = _Poll
pre_checkout_query = _PreCheckoutQuery
prefix = _Prefix
shipping_query = _ShippingQuery
string_command = _StringCommand
string_regex = _StringRegex
type = _Type
# noinspection PyPep8Naming
class error_handler:
def __init__(self, func: Callable[P, T] = None, *, block: bool = DEFAULT_TRUE):
self._func = func
self._block = block
def __call__(self, func: Callable[P, T] = None) -> Callable[P, T]:
self._func = func or self._func
data = {"type": "error", "block": self._block}
if hasattr(func, _NORMAL_HANDLER_ATTR_NAME):
handler_datas = getattr(func, _NORMAL_HANDLER_ATTR_NAME)
handler_datas.append(data)
setattr(func, _NORMAL_HANDLER_ATTR_NAME, handler_datas)
else:
setattr(func, _NORMAL_HANDLER_ATTR_NAME, [data])
return func
def _entry(func: Callable[P, T]) -> Callable[P, T]:
setattr(func, _CONVERSATION_HANDLER_ATTR_NAME, {"type": "entry"})
return func
class _State:
def __init__(self, state: Any):
self.state = state
def __call__(self, func: Callable[P, T] = None) -> Callable[P, T]:
setattr(func, _CONVERSATION_HANDLER_ATTR_NAME, {"type": "state", "state": self.state})
return func
def _fallback(func: Callable[P, T]) -> Callable[P, T]:
setattr(func, _CONVERSATION_HANDLER_ATTR_NAME, {"type": "fallback"})
return func
# noinspection PyPep8Naming
class conversation(_Handler):
entry_point = _entry
state = _State
fallback = _fallback
class _Job:
kwargs: Dict = {}
def __init__(
self,
name: str = None,
data: object = None,
chat_id: int = None,
user_id: int = None,
job_kwargs: JSONDict = None,
**kwargs,
):
self.name = name
self.data = data
self.chat_id = chat_id
self.user_id = user_id
self.job_kwargs = {} if job_kwargs is None else job_kwargs
self.kwargs = kwargs
def __call__(self, func: JobCallback) -> JobCallback:
data = {
"name": self.name,
"data": self.data,
"chat_id": self.chat_id,
"user_id": self.user_id,
"job_kwargs": self.job_kwargs,
"kwargs": self.kwargs,
"type": re.sub(r"([A-Z])", lambda x: "_" + x.group().lower(), self.__class__.__name__).lstrip("_"),
}
if hasattr(func, _JOB_ATTR_NAME):
job_datas = getattr(func, _JOB_ATTR_NAME)
job_datas.append(data)
setattr(func, _JOB_ATTR_NAME, job_datas)
else:
setattr(func, _JOB_ATTR_NAME, [data])
return func
class _RunOnce(_Job):
def __init__(
self,
when: TimeType,
data: object = None,
name: str = None,
chat_id: int = None,
user_id: int = None,
job_kwargs: JSONDict = None,
):
super().__init__(name, data, chat_id, user_id, job_kwargs, when=when)
class _RunRepeating(_Job):
def __init__(
self,
interval: Union[float, datetime.timedelta],
first: TimeType = None,
last: TimeType = None,
data: object = None,
name: str = None,
chat_id: int = None,
user_id: int = None,
job_kwargs: JSONDict = None,
):
super().__init__(name, data, chat_id, user_id, job_kwargs, interval=interval, first=first, last=last)
class _RunMonthly(_Job):
def __init__(
self,
when: datetime.time,
day: int,
data: object = None,
name: str = None,
chat_id: int = None,
user_id: int = None,
job_kwargs: JSONDict = None,
):
super().__init__(name, data, chat_id, user_id, job_kwargs, when=when, day=day)
class _RunDaily(_Job):
def __init__(
self,
time: datetime.time,
days: Tuple[int, ...] = tuple(range(7)),
data: object = None,
name: str = None,
chat_id: int = None,
user_id: int = None,
job_kwargs: JSONDict = None,
):
super().__init__(name, data, chat_id, user_id, job_kwargs, time=time, days=days)
class _RunCustom(_Job):
def __init__(
self,
data: object = None,
name: str = None,
chat_id: int = None,
user_id: int = None,
job_kwargs: JSONDict = None,
):
super().__init__(name, data, chat_id, user_id, job_kwargs)
# noinspection PyPep8Naming
class job:
run_once = _RunOnce
run_repeating = _RunRepeating
run_monthly = _RunMonthly
run_daily = _RunDaily
run_custom = _RunCustom