message_cleanup.py 5.6 KB

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