1313 PlatformMetadata ,
1414 register_platform_adapter ,
1515)
16- from astrbot .core .message .components import File , Record , Video
16+ from astrbot .core .message .components import BaseMessageComponent , File , Record , Video
1717from astrbot .core .platform .astr_message_event import MessageSesion
1818
1919from .kook_client import KookClient
2020from .kook_config import KookConfig
2121from .kook_event import KookEvent
22+ from .kook_roles_record import KookRolesRecord
2223from .kook_types import (
2324 ContainerModule ,
2425 FileModule ,
2728 KmarkdownElement ,
2829 KookCardMessageContainer ,
2930 KookChannelType ,
31+ KookMarkdownMentionRolePart ,
32+ KookMentionTagName ,
3033 KookMessageEventData ,
3134 KookMessageType ,
3235 KookModuleType ,
36+ KookRoleExtraType ,
3337 PlainTextElement ,
3438 SectionModule ,
3539)
3640
37- KOOK_AT_SELECTOR_REGEX = re .compile (r"\(met\)([^()]+)\(met\)" )
41+ KOOK_AT_SELECTOR_REGEX = re .compile (r"\((met|rol)\)([^()]+)\(\1\)" )
42+ AT_MENTION_PREFIX_REGEX = re .compile (r"^@[^\s]+(\s*-\s*[^\s]+)?\s*" )
3843
3944
4045@register_platform_adapter (
@@ -53,6 +58,7 @@ def __init__(
5358 self ._reconnect_task = None
5459 self .running = False
5560 self ._main_task = None
61+ self ._roles_cache = KookRolesRecord ("" , self .client .http_client )
5662
5763 async def send_by_session (
5864 self , session : MessageSesion , message_chain : MessageChain
@@ -84,16 +90,26 @@ async def _on_received(self, event: KookMessageEventData):
8490 event_type = event .type
8591 if event_type in (KookMessageType .KMARKDOWN , KookMessageType .CARD ):
8692 if self ._should_ignore_event_by_bot_nickname (event .author_id ):
87- logger .debug ("[KOOK] 收到来自机器人自身的消息 , 忽略此消息" )
93+ logger .debug ("[KOOK] 判断此消息为来自机器人自身的消息 , 忽略此消息" )
8894 return
8995 try :
9096 abm = await self .convert_message (event )
9197 await self .handle_msg (abm )
9298 except Exception as e :
9399 logger .error (f"[KOOK] 消息处理异常: { e } " )
94100 elif event_type == KookMessageType .SYSTEM :
95- logger .debug (f'[KOOK] 消息为系统通知, 通知类型为: "{ event .extra .type } "' )
96- logger .debug (f"[KOOK] 原始消息数据: { event .to_json ()} " )
101+ match event .extra .type :
102+ case KookRoleExtraType ():
103+ # 此时 target_id 就是频道id(guild_id)
104+ guild_id = event .target_id
105+ logger .info (
106+ f'[KOOK] 收到频道"{ guild_id } "的角色更新通知, 类型为"{ event .extra .type .value } ", 刷新角色id缓存'
107+ )
108+ self ._roles_cache .clear_guild_roles_cache (int (guild_id ))
109+ case _:
110+ logger .debug (
111+ f'[KOOK] 判断此消息为"{ event .extra .type } "类型的系统通知, 因未实现此消息的处理流程而忽略此消息, 原始消息数据: { event .to_json ()} '
112+ )
97113
98114 async def run (self ):
99115 """主运行循环"""
@@ -124,6 +140,8 @@ async def _main_loop(self):
124140 logger .info ("[KOOK] 尝试连接KOOK服务器..." )
125141
126142 # 尝试连接
143+ await self .client .get_bot_info ()
144+ self ._roles_cache .set_bot_id (self .client .bot_id )
127145 success = await self .client .connect ()
128146
129147 if success :
@@ -191,47 +209,86 @@ async def _cleanup(self):
191209
192210 logger .info ("[KOOK] 资源清理完成" )
193211
194- def _parse_kmarkdown_text_message (
195- self , data : KookMessageEventData , self_id : str
196- ) -> tuple [list , str ]:
197- kmarkdown = data .extra .kmarkdown
198- content = data .content or ""
199- if kmarkdown is None :
200- logger .error (
201- f'[KOOK] 无法转换"{ KookMessageType .KMARKDOWN .name } "消息, 消息中找不到kmarkdown字段'
202- )
203- logger .error (f"[KOOK] 原始消息内容: { data .to_json ()} " )
204- return [], ""
212+ async def _convert_text_message_to_component (
213+ self ,
214+ content : str ,
215+ raw_content : str ,
216+ mention_role_part : list [KookMarkdownMentionRolePart ] | None = None ,
217+ guild_id : str | None = None ,
218+ mention_name_map : dict [str , str ] | None = None ,
219+ ) -> tuple [list [BaseMessageComponent ], str ]:
220+ # kook平台有一个角色(role)的概念,他表示拥有某一类权限的许多用户
221+ # 且角色本身也有一个自己的id,与正常用户id不同
222+ # 而在频道中是可以`@`角色的,而想要知道bot是否属于某个角色
223+ # 需要通过 `/user/view` 接口获取当前bot账号的某个频道下所属角色的id
224+ # 为了解决 https://github.com/AstrBotDevs/AstrBot/issues/7539
225+ # 在确定机器人需要响应某个`(rol)xxx(rol)`时,需要将角色id替换装当前的bot id
226+ # 包装成`At`机器人自己,而`At`的name就保留角色名称
227+ # 如果没有查询到角色id或者bot不属于某类角色, 则不处理此`(rol)xxx(rol)`
228+ # 暂时想不到能在不修改原有消息内容的情况下处理这个角色mention的方案
205229
206- raw_content = kmarkdown .raw_content or content
207- if not isinstance (content , str ):
208- content = str (content )
209- if not isinstance (raw_content , str ):
210- raw_content = str (raw_content )
230+ message_str = raw_content
231+ bot_id = self .client .bot_id
232+ bot_nickname = self .client .bot_nickname
233+ bot_username = self .client .bot_username
234+ components : list [BaseMessageComponent ] = []
235+ if mention_name_map is None :
236+ mention_name_map = {}
237+ cursor = 0
211238
212- # TODO 后面的pydantic类型替换,以后再来探索吧 :(
213- mention_name_map : dict [str , str ] = {}
214- mention_part = kmarkdown .mention_part
215- if isinstance (mention_part , list ):
216- for item in mention_part :
217- if not isinstance (item , dict ):
218- continue
219- mention_id = item .get ("id" )
220- if mention_id is None :
221- continue
222- mention_name_map [str (mention_id )] = str (item .get ("username" , "" ))
239+ role_mention_counter = - 1
223240
224- components = []
225- cursor = 0
226241 for match in KOOK_AT_SELECTOR_REGEX .finditer (content ):
227242 if match .start () > cursor :
228- plain_text = content [cursor : match .start ()]
243+ plain_text = content [cursor : match .start ()]. strip ( " " )
229244 if plain_text :
230245 components .append (Plain (text = plain_text ))
231246
232- mention_target = match .group (1 ).strip ()
233- if mention_target == "all" :
247+ tag_name = match .group (1 )
248+ mention_target = match .group (2 ).strip ()
249+ if tag_name == KookMentionTagName .MENTION and mention_target == "all" :
234250 components .append (AtAll ())
251+ elif tag_name == KookMentionTagName .ROLE :
252+ role_mention_counter += 1
253+ role_id = 0
254+ role_mention_name = mention_target
255+ if mention_role_part is not None :
256+ if len (mention_role_part ) > role_mention_counter :
257+ role_mention_name = mention_role_part [role_mention_counter ].name
258+ role_id = mention_role_part [role_mention_counter ].role_id
259+ if (
260+ bot_nickname == role_mention_name
261+ or bot_username == role_mention_name
262+ ):
263+ components .append (
264+ At (
265+ qq = bot_id ,
266+ name = role_mention_name , # 保留角色名称
267+ )
268+ )
269+ continue
270+ if not mention_target .isdigit () and role_id == 0 :
271+ continue
272+
273+ role_id = role_id or int (mention_target )
274+ if not guild_id :
275+ continue
276+
277+ if not guild_id .isdigit ():
278+ continue
279+
280+ if not await self ._roles_cache .has_role_in_channel (
281+ role_id , int (guild_id )
282+ ):
283+ continue
284+
285+ components .append (
286+ At (
287+ qq = bot_id ,
288+ name = role_mention_name , # 保留角色名称
289+ )
290+ )
291+
235292 elif mention_target :
236293 components .append (
237294 At (
@@ -242,21 +299,20 @@ def _parse_kmarkdown_text_message(
242299 cursor = match .end ()
243300
244301 if cursor < len (content ):
245- tail_text = content [cursor :]
302+ tail_text = content [cursor :]. strip ( " " )
246303 if tail_text :
247304 components .append (Plain (text = tail_text ))
248305
249- message_str = raw_content
306+ message_str = raw_content . strip ()
250307 if components :
251308 for comp in components :
252309 if isinstance (comp , Plain ):
253310 if not comp .text .strip ():
254311 continue
255312 break
256313 if isinstance (comp , At ):
257- if str (comp .qq ) == str (self_id ):
258- message_str = re .sub (
259- r"^@[^\s]+(\s*-\s*[^\s]+)?\s*" ,
314+ if str (comp .qq ) == str (self .client .bot_id ):
315+ message_str = AT_MENTION_PREFIX_REGEX .sub (
260316 "" ,
261317 message_str ,
262318 count = 1 ,
@@ -270,10 +326,44 @@ def _parse_kmarkdown_text_message(
270326
271327 return components , message_str
272328
273- def _parse_card_message (self , data : KookMessageEventData ) -> tuple [list , str ]:
329+ async def _parse_kmarkdown_message (
330+ self , data : KookMessageEventData
331+ ) -> tuple [list [BaseMessageComponent ], str ]:
332+ kmarkdown = data .extra .kmarkdown
333+ guild_id = data .extra .guild_id
334+ mention_role_part = None
335+ if kmarkdown :
336+ mention_role_part = kmarkdown .mention_role_part
337+ # 无法处理可能会收到的道具消息content,只能保留原样
338+ content = str (data .content ) or ""
339+ if kmarkdown is None :
340+ logger .error (
341+ f'[KOOK] 无法转换"{ KookMessageType .KMARKDOWN .name } "消息, 消息中找不到kmarkdown字段'
342+ )
343+ logger .error (f"[KOOK] 原始消息内容: { data .to_json ()} " )
344+ return [], ""
345+
346+ raw_content = kmarkdown .raw_content or content
347+
348+ mention_name_map : dict [str , str ] = {}
349+ mention_part = kmarkdown .mention_part
350+ for item in mention_part :
351+ mention_id = item .id
352+ if mention_id is None :
353+ continue
354+ mention_name_map [str (mention_id )] = str (item .username )
355+
356+ return await self ._convert_text_message_to_component (
357+ content , raw_content , mention_role_part , guild_id , mention_name_map
358+ )
359+
360+ async def _parse_card_message (
361+ self , data : KookMessageEventData
362+ ) -> tuple [list [BaseMessageComponent ], str ]:
274363 content = data .content
275364 if not isinstance (content , str ):
276365 content = str (content )
366+ guild_id = data .extra .guild_id
277367
278368 card_list = KookCardMessageContainer .from_dict (json .loads (content ))
279369
@@ -304,18 +394,13 @@ def _parse_card_message(self, data: KookMessageEventData) -> tuple[list, str]:
304394 logger .debug (f"[KOOK] 跳过或未处理模块: { module .type } " )
305395
306396 text = "" .join (text_parts )
307- message = []
397+ message : list [ BaseMessageComponent ] = []
308398
309399 if text :
310- for search in KOOK_AT_SELECTOR_REGEX .finditer (text ):
311- search_text = search .group (1 ).strip ()
312- if search_text == "all" :
313- message .append (AtAll ())
314- continue
315- message .append (At (qq = search_text ))
316- text = text .replace (f"(met){ search_text } (met)" , "" )
317-
318- message .append (Plain (text = text ))
400+ component_parts , text = await self ._convert_text_message_to_component (
401+ text , text , guild_id = guild_id
402+ )
403+ message .extend (component_parts )
319404
320405 for img_url in images :
321406 message .append (Image (file = img_url ))
@@ -387,12 +472,10 @@ async def convert_message(self, data: KookMessageEventData) -> AstrBotMessage:
387472 abm .message_id = data .msg_id or "unknown"
388473
389474 if data .type == KookMessageType .KMARKDOWN :
390- message , message_str = self ._parse_kmarkdown_text_message (data , abm .self_id )
391- abm .message = message
392- abm .message_str = message_str
475+ abm .message , abm .message_str = await self ._parse_kmarkdown_message (data )
393476 elif data .type == KookMessageType .CARD :
394477 try :
395- abm .message , abm .message_str = self ._parse_card_message (data )
478+ abm .message , abm .message_str = await self ._parse_card_message (data )
396479 except Exception as exp :
397480 logger .error (f"[KOOK] 卡片消息解析失败: { exp } " )
398481 logger .error (f"[KOOK] 原始消息内容: { data .to_json ()} " )
0 commit comments