"""Recognize contact restrictions from system notices, never ordinary chat topics.""" import re import unicodedata import math CONTACT_REASON = '对方开启联系人验证,当前不是联系人,已跳过自动回复' SYSTEM_TYPES = {11, 38, 101, 132, 1002, 1011, 1012, 1017, 1022, 1025, 1043, 1988} def normalized_notice(text): return re.sub(r'[\W_]+', '', unicodedata.normalize('NFKC', str(text or ''))) def contact_verification_reason(text): value = normalized_notice(text) if ('开启了联系人验证' in value or '开启了朋友验证' in value) and ( '请先发送联系人验证请求' in value or '请先发送朋友验证请求' in value ): return CONTACT_REASON if re.search(r'你(?:还|尚)?不是.{0,5}的(?:联系人|好友)', value) and ( '验证通过后' in value or '验证请求' in value ): return CONTACT_REASON return '' def rejected_contact_reason(text): # Used only for explicit transport failures, never user-written message text. return contact_verification_reason(text) or (CONTACT_REASON if any( phrase in normalized_notice(text) for phrase in ('对方不是你的联系人', '该用户不是你的联系人', '你不是对方的联系人', '对方拒收了你的消息') ) else '') def system_contact_status(message): try: system = message.get('is_system') is True or int(message.get('content_type') or 0) in SYSTEM_TYPES except (ValueError, TypeError): system = False if not system: return '' text = message.get('content', '') if contact_verification_reason(text): return 'blocked' value = normalized_notice(text) if ('通过了' in value and ('联系人验证' in value or '朋友验证' in value)) or ( '你已添加了' in value and '现在可以开始聊天了' in value ): return 'restored' return '' def conversation_contact_reason(context): for message in reversed((context or {}).get('messages') or []): status = system_contact_status(message) if status: return CONTACT_REASON if status == 'blocked' else '' return '' def contact_event_order(message): """A source message anchor, never the time an old notice was rediscovered.""" try: stamp = float(message.get('send_time') or 0) row = int(message.get('rowid') or 0) return (stamp, row) if math.isfinite(stamp) and stamp > 0 and row >= 0 else (0.0, 0) except (TypeError, ValueError, OverflowError): return (0.0, 0) def contact_peer(account, conv_id): account, conv_id = str(account or ''), str(conv_id or '') if not re.fullmatch(r'[0-9]{1,24}', account): return '' if re.fullmatch(r'M:[0-9]{1,24}', conv_id): return conv_id[2:] if conv_id[2:] != account else '' if re.fullmatch(r'S:[0-9]{1,24}_[0-9]{1,24}', conv_id): peers = conv_id[2:].split('_') if peers.count(account) == 1: return next(peer for peer in peers if peer != account) return '' def latest_contact_notice(context): """Only notices belonging to the requested exact account/conversation.""" if not isinstance(context, dict): return None account, conv_id = context.get('account'), context.get('conv_id') for message in reversed(context.get('messages') or []): if (message.get('account') == account and message.get('conv_id') == conv_id and system_contact_status(message)): return message return None def current_customer_relation(context): """An active local relation permits rechecking a NEW question, not old sends.""" if not isinstance(context, dict): return False account, conv_id = context.get('account'), context.get('conv_id') peer = contact_peer(account, conv_id) relation = context.get('contact_relation') or {} return bool(peer and isinstance(relation, dict) and relation.get('state') == 'active' and relation.get('account') == account and relation.get('conv_id') == conv_id and relation.get('peer_id') == peer) def real_customer_event(message, account, conv_id): """Require a positive persisted server identity, peer sender and direct chat.""" peer = contact_peer(account, conv_id) if not isinstance(message, dict) or not peer: return False try: kind = int(message.get('content_type')) server = str(message.get('server_id') or '') stamp, row = contact_event_order(message) return bool(message.get('account') == account and message.get('conv_id') == conv_id and message.get('is_self') is False and str(message.get('sender_id') or '') == peer and message.get('dedup_key') and server.isdecimal() and int(server) > 0 and stamp > 0 and row > 0 and message.get('is_system') is not True and kind in {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 14, 16, 17, 20, 111, 123}) except (TypeError, ValueError, OverflowError): return False def visual_contact_reason(region, items): """Require centered grey notice text inside the already-cropped chat pane.""" import numpy as np if region is None or getattr(region, 'ndim', 0) != 3 or not region.size: return '' height, width = region.shape[:2] candidates = [] for item in items: if float(item.get('score') or 0) < .70: continue x1, y1 = max(0, int(item.get('x1', 0))), max(0, int(item.get('y1', 0))) x2, y2 = min(width, int(item.get('x2', 0))), min(height, int(item.get('y2', 0))) if x2 <= x1 or y2 <= y1 or not .25 * width < (x1+x2)/2 < .75 * width: continue pixels = region[y1:y2, x1:x2, :3].astype(np.int16) low, high = pixels.min(axis=2), pixels.max(axis=2) neutral_ink = (high-low < 40) & (low < 215) grey_ink = neutral_ink & (low >= 90) # Black text in white customer bubbles and blue outgoing bubbles is not a system banner. count = int(neutral_ink.sum()) if count < 15 or float(grey_ink.sum()) / count < .65: continue candidates.append((y1, y2, x1, str(item.get('text') or ''))) candidates.sort() group, bottom, line_height = [], -1000, 0 for y1, y2, x1, text in candidates: if y1-bottom > max(12, line_height * 1.5): if contact_verification_reason(''.join(group)): return CONTACT_REASON group = [] group.append(text) bottom, line_height = max(bottom, y2), max(1, y2-y1) return contact_verification_reason(''.join(group))