You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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