159 lines
6.5 KiB
Python
159 lines
6.5 KiB
Python
"""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))
|