Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

340 linhas
16KB

  1. # encoding:utf-8
  2. """
  3. wechaty channel
  4. Python Wechaty - https://github.com/wechaty/python-wechaty
  5. """
  6. import os
  7. import time
  8. import asyncio
  9. from typing import Optional, Union
  10. from bridge.context import Context, ContextType
  11. from wechaty_puppet import MessageType, FileBox, ScanStatus # type: ignore
  12. from wechaty import Wechaty, Contact
  13. from wechaty.user import Message, MiniProgram, UrlLink
  14. from channel.channel import Channel
  15. from common.log import logger
  16. from common.tmp_dir import TmpDir
  17. from config import conf
  18. from voice.audio_convert import sil_to_wav, mp3_to_sil
  19. class WechatyChannel(Channel):
  20. def __init__(self):
  21. pass
  22. def startup(self):
  23. asyncio.run(self.main())
  24. async def main(self):
  25. config = conf()
  26. # 使用PadLocal协议 比较稳定(免费web协议 os.environ['WECHATY_PUPPET_SERVICE_ENDPOINT'] = '127.0.0.1:8080')
  27. token = config.get('wechaty_puppet_service_token')
  28. os.environ['WECHATY_PUPPET_SERVICE_TOKEN'] = token
  29. global bot
  30. bot = Wechaty()
  31. bot.on('scan', self.on_scan)
  32. bot.on('login', self.on_login)
  33. bot.on('message', self.on_message)
  34. await bot.start()
  35. async def on_login(self, contact: Contact):
  36. logger.info('[WX] login user={}'.format(contact))
  37. async def on_scan(self, status: ScanStatus, qr_code: Optional[str] = None,
  38. data: Optional[str] = None):
  39. pass
  40. # contact = self.Contact.load(self.contact_id)
  41. # logger.info('[WX] scan user={}, scan status={}, scan qr_code={}'.format(contact, status.name, qr_code))
  42. # print(f'user <{contact}> scan status: {status.name} , 'f'qr_code: {qr_code}')
  43. async def on_message(self, msg: Message):
  44. """
  45. listen for message event
  46. """
  47. from_contact = msg.talker() # 获取消息的发送者
  48. to_contact = msg.to() # 接收人
  49. room = msg.room() # 获取消息来自的群聊. 如果消息不是来自群聊, 则返回None
  50. from_user_id = from_contact.contact_id
  51. to_user_id = to_contact.contact_id # 接收人id
  52. # other_user_id = msg['User']['UserName'] # 对手方id
  53. content = msg.text()
  54. mention_content = await msg.mention_text() # 返回过滤掉@name后的消息
  55. match_prefix = self.check_prefix(content, conf().get('single_chat_prefix'))
  56. # conversation: Union[Room, Contact] = from_contact if room is None else room
  57. if room is None and msg.type() == MessageType.MESSAGE_TYPE_TEXT:
  58. if not msg.is_self() and match_prefix is not None:
  59. # 好友向自己发送消息
  60. if match_prefix != '':
  61. str_list = content.split(match_prefix, 1)
  62. if len(str_list) == 2:
  63. content = str_list[1].strip()
  64. img_match_prefix = self.check_prefix(content, conf().get('image_create_prefix'))
  65. if img_match_prefix:
  66. content = content.split(img_match_prefix, 1)[1].strip()
  67. await self._do_send_img(content, from_user_id)
  68. else:
  69. await self._do_send(content, from_user_id)
  70. elif msg.is_self() and match_prefix:
  71. # 自己给好友发送消息
  72. str_list = content.split(match_prefix, 1)
  73. if len(str_list) == 2:
  74. content = str_list[1].strip()
  75. img_match_prefix = self.check_prefix(content, conf().get('image_create_prefix'))
  76. if img_match_prefix:
  77. content = content.split(img_match_prefix, 1)[1].strip()
  78. await self._do_send_img(content, to_user_id)
  79. else:
  80. await self._do_send(content, to_user_id)
  81. elif room is None and msg.type() == MessageType.MESSAGE_TYPE_AUDIO:
  82. if not msg.is_self(): # 接收语音消息
  83. # 下载语音文件
  84. voice_file = await msg.to_file_box()
  85. silk_file = TmpDir().path() + voice_file.name
  86. await voice_file.to_file(silk_file)
  87. logger.info("[WX]receive voice file: " + silk_file)
  88. # 将文件转成wav格式音频
  89. wav_file = os.path.splitext(silk_file)[0] + '.wav'
  90. sil_to_wav(silk_file, wav_file)
  91. # 语音识别为文本
  92. query = super().build_voice_to_text(wav_file).content
  93. # 交验关键字
  94. match_prefix = self.check_prefix(query, conf().get('single_chat_prefix'))
  95. if match_prefix is not None:
  96. if match_prefix != '':
  97. str_list = query.split(match_prefix, 1)
  98. if len(str_list) == 2:
  99. query = str_list[1].strip()
  100. # 返回消息
  101. if conf().get('voice_reply_voice'):
  102. await self._do_send_voice(query, from_user_id)
  103. else:
  104. await self._do_send(query, from_user_id)
  105. else:
  106. logger.info("[WX]receive voice check prefix: " + 'False')
  107. # 清除缓存文件
  108. os.remove(wav_file)
  109. os.remove(silk_file)
  110. elif room and msg.type() == MessageType.MESSAGE_TYPE_TEXT:
  111. # 群组&文本消息
  112. room_id = room.room_id
  113. room_name = await room.topic()
  114. from_user_id = from_contact.contact_id
  115. from_user_name = from_contact.name
  116. is_at = await msg.mention_self()
  117. content = mention_content
  118. config = conf()
  119. match_prefix = (is_at and not config.get("group_at_off", False)) \
  120. or self.check_prefix(content, config.get('group_chat_prefix')) \
  121. or self.check_contain(content, config.get('group_chat_keyword'))
  122. # Wechaty判断is_at为True,返回的内容是过滤掉@之后的内容;而is_at为False,则会返回完整的内容
  123. # 故判断如果匹配到自定义前缀,则返回过滤掉前缀+空格后的内容,用于实现类似自定义+前缀触发生成AI图片的功能
  124. prefixes = config.get('group_chat_prefix')
  125. for prefix in prefixes:
  126. if content.startswith(prefix):
  127. content = content.replace(prefix, '', 1).strip()
  128. break
  129. if ('ALL_GROUP' in config.get('group_name_white_list') or room_name in config.get(
  130. 'group_name_white_list') or self.check_contain(room_name, config.get(
  131. 'group_name_keyword_white_list'))) and match_prefix:
  132. img_match_prefix = self.check_prefix(content, conf().get('image_create_prefix'))
  133. if img_match_prefix:
  134. content = content.split(img_match_prefix, 1)[1].strip()
  135. await self._do_send_group_img(content, room_id)
  136. else:
  137. await self._do_send_group(content, room_id, room_name, from_user_id, from_user_name)
  138. elif room and msg.type() == MessageType.MESSAGE_TYPE_AUDIO:
  139. # 群组&语音消息
  140. room_id = room.room_id
  141. room_name = await room.topic()
  142. from_user_id = from_contact.contact_id
  143. from_user_name = from_contact.name
  144. is_at = await msg.mention_self()
  145. config = conf()
  146. # 是否开启语音识别、群消息响应功能、群名白名单符合等条件
  147. if config.get('group_speech_recognition') and (
  148. 'ALL_GROUP' in config.get('group_name_white_list') or room_name in config.get(
  149. 'group_name_white_list') or self.check_contain(room_name, config.get(
  150. 'group_name_keyword_white_list'))):
  151. # 下载语音文件
  152. voice_file = await msg.to_file_box()
  153. silk_file = TmpDir().path() + voice_file.name
  154. await voice_file.to_file(silk_file)
  155. logger.info("[WX]receive voice file: " + silk_file)
  156. # 将文件转成wav格式音频
  157. wav_file = os.path.splitext(silk_file)[0] + '.wav'
  158. sil_to_wav(silk_file, wav_file)
  159. # 语音识别为文本
  160. query = super().build_voice_to_text(wav_file).content
  161. # 校验关键字
  162. match_prefix = self.check_prefix(query, config.get('group_chat_prefix')) \
  163. or self.check_contain(query, config.get('group_chat_keyword'))
  164. # Wechaty判断is_at为True,返回的内容是过滤掉@之后的内容;而is_at为False,则会返回完整的内容
  165. if match_prefix is not None:
  166. # 故判断如果匹配到自定义前缀,则返回过滤掉前缀+空格后的内容,用于实现类似自定义+前缀触发生成AI图片的功能
  167. prefixes = config.get('group_chat_prefix')
  168. for prefix in prefixes:
  169. if query.startswith(prefix):
  170. query = query.replace(prefix, '', 1).strip()
  171. break
  172. # 返回消息
  173. img_match_prefix = self.check_prefix(query, conf().get('image_create_prefix'))
  174. if img_match_prefix:
  175. query = query.split(img_match_prefix, 1)[1].strip()
  176. await self._do_send_group_img(query, room_id)
  177. elif config.get('voice_reply_voice'):
  178. await self._do_send_group_voice(query, room_id, room_name, from_user_id, from_user_name)
  179. else:
  180. await self._do_send_group(query, room_id, room_name, from_user_id, from_user_name)
  181. else:
  182. logger.info("[WX]receive voice check prefix: " + 'False')
  183. # 清除缓存文件
  184. os.remove(wav_file)
  185. os.remove(silk_file)
  186. async def send(self, message: Union[str, Message, FileBox, Contact, UrlLink, MiniProgram], receiver):
  187. logger.info('[WX] sendMsg={}, receiver={}'.format(message, receiver))
  188. if receiver:
  189. contact = await bot.Contact.find(receiver)
  190. await contact.say(message)
  191. async def send_group(self, message: Union[str, Message, FileBox, Contact, UrlLink, MiniProgram], receiver):
  192. logger.info('[WX] sendMsg={}, receiver={}'.format(message, receiver))
  193. if receiver:
  194. room = await bot.Room.find(receiver)
  195. await room.say(message)
  196. async def _do_send(self, query, reply_user_id):
  197. try:
  198. if not query:
  199. return
  200. context = Context(ContextType.TEXT, query)
  201. context['session_id'] = reply_user_id
  202. reply_text = super().build_reply_content(query, context).content
  203. if reply_text:
  204. await self.send(conf().get("single_chat_reply_prefix") + reply_text, reply_user_id)
  205. except Exception as e:
  206. logger.exception(e)
  207. async def _do_send_voice(self, query, reply_user_id):
  208. try:
  209. if not query:
  210. return
  211. context = Context(ContextType.TEXT, query)
  212. context['session_id'] = reply_user_id
  213. reply_text = super().build_reply_content(query, context).content
  214. if reply_text:
  215. # 转换 mp3 文件为 silk 格式
  216. mp3_file = super().build_text_to_voice(reply_text).content
  217. silk_file = os.path.splitext(mp3_file)[0] + '.sil'
  218. voiceLength = mp3_to_sil(mp3_file, silk_file)
  219. # 发送语音
  220. t = int(time.time())
  221. file_box = FileBox.from_file(silk_file, name=str(t) + '.sil')
  222. file_box.metadata = {'voiceLength': voiceLength}
  223. await self.send(file_box, reply_user_id)
  224. # 清除缓存文件
  225. os.remove(mp3_file)
  226. os.remove(silk_file)
  227. except Exception as e:
  228. logger.exception(e)
  229. async def _do_send_img(self, query, reply_user_id):
  230. try:
  231. if not query:
  232. return
  233. context = Context(ContextType.IMAGE_CREATE, query)
  234. img_url = super().build_reply_content(query, context).content
  235. if not img_url:
  236. return
  237. # 图片下载
  238. # pic_res = requests.get(img_url, stream=True)
  239. # image_storage = io.BytesIO()
  240. # for block in pic_res.iter_content(1024):
  241. # image_storage.write(block)
  242. # image_storage.seek(0)
  243. # 图片发送
  244. logger.info('[WX] sendImage, receiver={}'.format(reply_user_id))
  245. t = int(time.time())
  246. file_box = FileBox.from_url(url=img_url, name=str(t) + '.png')
  247. await self.send(file_box, reply_user_id)
  248. except Exception as e:
  249. logger.exception(e)
  250. async def _do_send_group(self, query, group_id, group_name, group_user_id, group_user_name):
  251. if not query:
  252. return
  253. context = Context(ContextType.TEXT, query)
  254. group_chat_in_one_session = conf().get('group_chat_in_one_session', [])
  255. if ('ALL_GROUP' in group_chat_in_one_session or \
  256. group_name in group_chat_in_one_session or \
  257. self.check_contain(group_name, group_chat_in_one_session)):
  258. context['session_id'] = str(group_id)
  259. else:
  260. context['session_id'] = str(group_id) + '-' + str(group_user_id)
  261. reply_text = super().build_reply_content(query, context).content
  262. if reply_text:
  263. reply_text = '@' + group_user_name + ' ' + reply_text.strip()
  264. await self.send_group(conf().get("group_chat_reply_prefix", "") + reply_text, group_id)
  265. async def _do_send_group_voice(self, query, group_id, group_name, group_user_id, group_user_name):
  266. if not query:
  267. return
  268. context = Context(ContextType.TEXT, query)
  269. group_chat_in_one_session = conf().get('group_chat_in_one_session', [])
  270. if ('ALL_GROUP' in group_chat_in_one_session or \
  271. group_name in group_chat_in_one_session or \
  272. self.check_contain(group_name, group_chat_in_one_session)):
  273. context['session_id'] = str(group_id)
  274. else:
  275. context['session_id'] = str(group_id) + '-' + str(group_user_id)
  276. reply_text = super().build_reply_content(query, context).content
  277. if reply_text:
  278. reply_text = '@' + group_user_name + ' ' + reply_text.strip()
  279. # 转换 mp3 文件为 silk 格式
  280. mp3_file = super().build_text_to_voice(reply_text).content
  281. silk_file = os.path.splitext(mp3_file)[0] + '.sil'
  282. voiceLength = mp3_to_sil(mp3_file, silk_file)
  283. # 发送语音
  284. t = int(time.time())
  285. file_box = FileBox.from_file(silk_file, name=str(t) + '.silk')
  286. file_box.metadata = {'voiceLength': voiceLength}
  287. await self.send_group(file_box, group_id)
  288. # 清除缓存文件
  289. os.remove(mp3_file)
  290. os.remove(silk_file)
  291. async def _do_send_group_img(self, query, reply_room_id):
  292. try:
  293. if not query:
  294. return
  295. context = Context(ContextType.IMAGE_CREATE, query)
  296. img_url = super().build_reply_content(query, context).content
  297. if not img_url:
  298. return
  299. # 图片发送
  300. logger.info('[WX] sendImage, receiver={}'.format(reply_room_id))
  301. t = int(time.time())
  302. file_box = FileBox.from_url(url=img_url, name=str(t) + '.png')
  303. await self.send_group(file_box, reply_room_id)
  304. except Exception as e:
  305. logger.exception(e)
  306. def check_prefix(self, content, prefix_list):
  307. for prefix in prefix_list:
  308. if content.startswith(prefix):
  309. return prefix
  310. return None
  311. def check_contain(self, content, keyword_list):
  312. if not keyword_list:
  313. return None
  314. for ky in keyword_list:
  315. if content.find(ky) != -1:
  316. return True
  317. return None