message_cleanup.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. from __future__ import annotations
  2. import asyncio
  3. import logging
  4. from functools import wraps
  5. from typing import Any
  6. from weakref import WeakKeyDictionary
  7. GROUP_INTERACTION_TTL_SECONDS = 60
  8. _LOGGER = logging.getLogger(__name__)
  9. _scheduled_messages: set[tuple[int, int]] = set()
  10. _active_interactions: WeakKeyDictionary[asyncio.Task[Any], int] = WeakKeyDictionary()
  11. _TRACKED_SEND_METHODS = (
  12. "send_message",
  13. "send_photo",
  14. "send_document",
  15. "send_video",
  16. "send_animation",
  17. "send_audio",
  18. "send_voice",
  19. "send_video_note",
  20. "send_sticker",
  21. "send_contact",
  22. "send_location",
  23. "send_venue",
  24. "send_poll",
  25. "send_dice",
  26. "send_media_group",
  27. "send_cached_media",
  28. "copy_message",
  29. "copy_media_group",
  30. "forward_messages",
  31. )
  32. def _message_key(message: Any) -> tuple[int, int] | None:
  33. chat = getattr(message, "chat", None)
  34. message_id = getattr(message, "id", None)
  35. if not chat or not message_id:
  36. return None
  37. try:
  38. return int(chat.id), int(message_id)
  39. except (TypeError, ValueError):
  40. return None
  41. def _is_group_message(message: Any) -> bool:
  42. chat = getattr(message, "chat", None)
  43. if not chat:
  44. return False
  45. chat_type = getattr(chat, "type", None)
  46. chat_type_value = str(getattr(chat_type, "value", chat_type) or "").lower()
  47. if chat_type_value:
  48. return chat_type_value in {"group", "supergroup"}
  49. try:
  50. return int(chat.id) < 0
  51. except (TypeError, ValueError):
  52. return False
  53. def _current_task() -> asyncio.Task[Any] | None:
  54. try:
  55. return asyncio.current_task()
  56. except RuntimeError:
  57. return None
  58. async def _delete_later(message: Any, delay_seconds: int) -> None:
  59. key = _message_key(message)
  60. if key is None:
  61. return
  62. try:
  63. await asyncio.sleep(max(1, int(delay_seconds)))
  64. try:
  65. await message.delete(revoke=True)
  66. except asyncio.CancelledError:
  67. raise
  68. except Exception as exc:
  69. _LOGGER.warning(
  70. "群交互消息自动删除失败 chat_id=%s message_id=%s: %s",
  71. key[0],
  72. key[1],
  73. exc,
  74. )
  75. finally:
  76. _scheduled_messages.discard(key)
  77. def schedule_group_cleanup(
  78. *messages: Any,
  79. delay_seconds: int = GROUP_INTERACTION_TTL_SECONDS,
  80. ) -> None:
  81. for message in messages:
  82. key = _message_key(message)
  83. if key is None or not _is_group_message(message):
  84. continue
  85. if key in _scheduled_messages:
  86. continue
  87. _scheduled_messages.add(key)
  88. asyncio.create_task(
  89. _delete_later(message, delay_seconds),
  90. name=f"group-interaction-cleanup:{key[0]}:{key[1]}",
  91. )
  92. def is_group_cleanup_scheduled(message: Any) -> bool:
  93. key = _message_key(message)
  94. return bool(key and key in _scheduled_messages)
  95. def clear_current_group_interaction() -> None:
  96. task = _current_task()
  97. if task is not None:
  98. _active_interactions.pop(task, None)
  99. def begin_group_message_interaction(
  100. message: Any,
  101. *,
  102. menu_texts: set[str] | frozenset[str] = frozenset(),
  103. ) -> bool:
  104. clear_current_group_interaction()
  105. if not _is_group_message(message):
  106. return False
  107. text = str(
  108. getattr(message, "text", None) or getattr(message, "caption", None) or ""
  109. ).strip()
  110. folded = text.casefold()
  111. is_command = text.startswith("/") or folded in {"@admin", "@admins"}
  112. is_command = is_command or folded.startswith(("@admin ", "@admins "))
  113. if not is_command and text not in menu_texts:
  114. return False
  115. task = _current_task()
  116. if task is None:
  117. return False
  118. _active_interactions[task] = int(message.chat.id)
  119. schedule_group_cleanup(message)
  120. return True
  121. def begin_group_callback_interaction(query: Any) -> bool:
  122. clear_current_group_interaction()
  123. message = getattr(query, "message", None)
  124. if (
  125. not message
  126. or not _is_group_message(message)
  127. or not is_group_cleanup_scheduled(message)
  128. ):
  129. return False
  130. task = _current_task()
  131. if task is None:
  132. return False
  133. _active_interactions[task] = int(message.chat.id)
  134. return True
  135. def track_current_group_interaction(
  136. result: Any,
  137. *,
  138. reply_to_message_id: Any = None,
  139. ) -> None:
  140. if reply_to_message_id is None:
  141. return
  142. task = _current_task()
  143. if task is None:
  144. return
  145. chat_id = _active_interactions.get(task)
  146. if chat_id is None:
  147. return
  148. messages = result if isinstance(result, (list, tuple)) else (result,)
  149. for message in messages:
  150. key = _message_key(message)
  151. if key and key[0] == chat_id and _is_group_message(message):
  152. schedule_group_cleanup(message)
  153. def install_group_interaction_tracking(client: Any) -> None:
  154. if getattr(client, "_wbb_group_interaction_tracking", False):
  155. return
  156. for method_name in _TRACKED_SEND_METHODS:
  157. original = getattr(client, method_name, None)
  158. if not callable(original):
  159. continue
  160. @wraps(original)
  161. async def tracked_send(*args: Any, _original=original, **kwargs: Any):
  162. result = await _original(*args, **kwargs)
  163. track_current_group_interaction(
  164. result,
  165. reply_to_message_id=kwargs.get("reply_to_message_id"),
  166. )
  167. return result
  168. setattr(client, method_name, tracked_send)
  169. client._wbb_group_interaction_tracking = True