forwarder.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. """事件处理 + 转发核心:白名单过滤、防死循环、去重、调用 Forward API。
  2. lark.ws.Client 在独立线程中运行事件循环并回调 handler。
  3. 本模块的事件回调把任务投递到 asyncio.Queue(跨线程安全),
  4. 由 ratelimit.Worker 在主事件循环中匀速消费。
  5. """
  6. from __future__ import annotations
  7. import asyncio
  8. import logging
  9. import threading
  10. from typing import Optional
  11. import lark_oapi as lark
  12. from lark_oapi.api.im.v1 import (
  13. ForwardMessageRequest,
  14. ForwardMessageRequestBody,
  15. )
  16. from config import Config
  17. from dedup import MessageDedup
  18. from ratelimit import ForwardTask, RateLimitedWorker
  19. logger = logging.getLogger(__name__)
  20. class Forwarder:
  21. """绑定配置、去重器、限流 Worker,并提供事件回调。"""
  22. def __init__(
  23. self,
  24. cfg: Config,
  25. client: lark.Client,
  26. dedup: MessageDedup,
  27. worker: RateLimitedWorker,
  28. loop: asyncio.AbstractEventLoop,
  29. ) -> None:
  30. self._cfg = cfg
  31. self._client = client
  32. self._dedup = dedup
  33. self._worker = worker
  34. self._loop = loop
  35. self._source_set = set(cfg.source_chat_ids)
  36. # ---------- 事件回调(lark SDK 在子线程调用) ----------
  37. def on_message_receive(self, data: lark.im.v1.P2ImMessageReceiveV1) -> None:
  38. try:
  39. self._handle_event(data)
  40. except Exception as e:
  41. logger.exception("事件处理异常: %s", e)
  42. def _handle_event(self, data: lark.im.v1.P2ImMessageReceiveV1) -> None:
  43. event = data.event
  44. if event is None:
  45. return
  46. message = getattr(event, "message", None)
  47. sender = getattr(event, "sender", None)
  48. if message is None:
  49. return
  50. message_id: str = message.message_id or ""
  51. chat_id: str = message.chat_id or ""
  52. sender_type: str = (sender.sender_type if sender else "") or ""
  53. # 1) 防死循环:丢弃机器人自己发出的消息
  54. if sender_type == "bot":
  55. logger.debug("跳过 bot 消息 message_id=%s", message_id)
  56. return
  57. # 2) 白名单:只处理源群
  58. if chat_id not in self._source_set:
  59. logger.debug("跳过非白名单群 chat_id=%s", chat_id)
  60. return
  61. # 3) 去重
  62. if not self._dedup.check_and_mark(message_id):
  63. logger.debug("重复消息已丢弃 message_id=%s", message_id)
  64. return
  65. logger.info("接收消息 message_id=%s chat_id=%s type=%s",
  66. message_id, chat_id, message.message_type)
  67. # 4) 投递转发任务(每个目标群一个)
  68. for target in self._cfg.target_chat_ids:
  69. if target == chat_id:
  70. # 源==目标,跳过避免无效转发
  71. continue
  72. task = ForwardTask(
  73. message_id=message_id,
  74. target_chat_id=target,
  75. source_chat_id=chat_id,
  76. )
  77. # 跨线程安全投递到主事件循环的队列
  78. asyncio.run_coroutine_threadsafe(
  79. self._worker.enqueue(task), self._loop
  80. )
  81. # ---------- 实际转发(Worker 调用,运行在主事件循环) ----------
  82. async def forward(self, task: ForwardTask) -> bool:
  83. """调用飞书 Forward API。返回 True 表示成功。"""
  84. req = (
  85. ForwardMessageRequest.builder()
  86. .message_id(task.message_id)
  87. .receive_id_type("chat_id")
  88. .request_body(
  89. ForwardMessageRequestBody.builder()
  90. .receive_id(task.target_chat_id)
  91. .build()
  92. )
  93. .build()
  94. )
  95. def _call() -> bool:
  96. try:
  97. resp = self._client.im.v1.message.forward(req)
  98. except Exception as e:
  99. logger.error("Forward API 异常 message_id=%s -> %s: %s",
  100. task.message_id, task.target_chat_id, e)
  101. return False
  102. if not resp.success():
  103. logger.error("Forward API 失败 message_id=%s -> %s code=%s msg=%s",
  104. task.message_id, task.target_chat_id,
  105. resp.code, resp.msg)
  106. return False
  107. logger.info("转发成功 message_id=%s -> %s",
  108. task.message_id, task.target_chat_id)
  109. return True
  110. # lark-oapi 的 Client 是同步阻塞调用,放到默认 executor 执行
  111. return await asyncio.get_event_loop().run_in_executor(None, _call)
  112. def build_event_handler(forwarder: Forwarder):
  113. """构造 lark EventDispatcherHandler,注册 im.message.receive_v1。"""
  114. return (
  115. lark.EventDispatcherHandler.builder("", "")
  116. .register_p2_im_message_receive_v1(forwarder.on_message_receive)
  117. .build()
  118. )