mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
178 lines
6.6 KiB
Python
178 lines
6.6 KiB
Python
"""QQ Official keyboard rendering and interaction callback conversion."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
import typing
|
|
|
|
from langbot_plugin.api.entities.builtin.platform import events as platform_events
|
|
|
|
|
|
def interaction_delivery_capabilities() -> dict[str, typing.Any]:
|
|
return {
|
|
'field_types': ['select'],
|
|
'action_styles': ['default', 'primary', 'danger'],
|
|
'supports_updates': True,
|
|
'max_fields': 1,
|
|
}
|
|
|
|
|
|
def _buttons(request: dict[str, typing.Any], callback_token: str) -> list[tuple[str, str, int]]:
|
|
fields = request.get('fields') if isinstance(request.get('fields'), list) else []
|
|
actions = request.get('actions') if isinstance(request.get('actions'), list) else []
|
|
if actions and not fields:
|
|
return [
|
|
(
|
|
str(action.get('label') or action.get('id') or index + 1),
|
|
f'lbi:{callback_token}:a:{index}',
|
|
1 if action.get('style') == 'primary' else 0,
|
|
)
|
|
for index, action in enumerate(actions[:25])
|
|
if isinstance(action, dict)
|
|
]
|
|
if len(fields) == 1 and not actions and isinstance(fields[0], dict):
|
|
field = fields[0]
|
|
if field.get('type') != 'select':
|
|
return []
|
|
options = field.get('options') if isinstance(field.get('options'), list) else []
|
|
return [
|
|
(
|
|
str(option.get('label') or option.get('value') or index + 1),
|
|
f'lbi:{callback_token}:f:0:{index}',
|
|
0,
|
|
)
|
|
for index, option in enumerate(options[:25])
|
|
if isinstance(option, dict)
|
|
]
|
|
return []
|
|
|
|
|
|
def build_interaction_keyboard(request: dict[str, typing.Any], callback_token: str) -> dict[str, typing.Any] | None:
|
|
buttons = _buttons(request, callback_token)
|
|
if not buttons:
|
|
return None
|
|
rows = []
|
|
for start in range(0, len(buttons), 2):
|
|
rows.append(
|
|
{
|
|
'buttons': [
|
|
{
|
|
'id': str(start + offset + 1),
|
|
'render_data': {
|
|
'label': label,
|
|
'visited_label': f'✓ {label}',
|
|
'style': style,
|
|
},
|
|
'action': {
|
|
'type': 1,
|
|
'permission': {'type': 2},
|
|
'data': callback_data,
|
|
'unsupport_tips': 'Please update QQ to use this action.',
|
|
},
|
|
}
|
|
for offset, (label, callback_data, style) in enumerate(buttons[start : start + 2])
|
|
]
|
|
}
|
|
)
|
|
return {'content': {'rows': rows}}
|
|
|
|
|
|
def _text(request: dict[str, typing.Any], rich: bool) -> str:
|
|
parts = [str(request.get('title') or '').strip()]
|
|
description = str(request.get('description') or '').strip()
|
|
if description:
|
|
parts.append(description)
|
|
if not rich:
|
|
fallback = str(request.get('fallback_text') or '').strip()
|
|
if fallback:
|
|
parts.append(fallback)
|
|
return '\n\n'.join(part for part in parts if part)
|
|
|
|
|
|
async def send_interaction(adapter: typing.Any, params: dict[str, typing.Any]) -> dict[str, typing.Any]:
|
|
request = params.get('request')
|
|
reply_target = params.get('reply_target')
|
|
callback_token = str(params.get('callback_token') or '')
|
|
if not isinstance(request, dict) or not isinstance(reply_target, dict) or not callback_token:
|
|
raise ValueError('interaction.request requires request, reply_target, and callback_token')
|
|
target_type = str(reply_target.get('target_type') or '')
|
|
target_id = str(reply_target.get('target_id') or '')
|
|
keyboard = build_interaction_keyboard(request, callback_token)
|
|
if keyboard is None or target_type not in {'person', 'group'}:
|
|
result = await adapter.send_message(
|
|
target_type,
|
|
target_id,
|
|
adapter._plain_message(_text(request, False)),
|
|
)
|
|
return {'ok': True, 'message_id': result.message_id, 'rich': False}
|
|
raw = await adapter.bot.send_markdown_keyboard(
|
|
target_type='c2c' if target_type == 'person' else 'group',
|
|
target_id=target_id,
|
|
markdown_content=_text(request, True),
|
|
keyboard=keyboard,
|
|
msg_id=reply_target.get('message_id'),
|
|
)
|
|
return {'ok': True, 'message_id': raw.get('id') if isinstance(raw, dict) else None, 'rich': True}
|
|
|
|
|
|
def parse_callback_data(value: str) -> dict[str, typing.Any]:
|
|
parts = value.split(':')
|
|
if len(parts) == 4 and parts[0] == 'lbi' and parts[1] and parts[2] == 'a' and parts[3].isdigit():
|
|
return {'callback_token': parts[1], 'action_ref': int(parts[3])}
|
|
if (
|
|
len(parts) == 5
|
|
and parts[0] == 'lbi'
|
|
and parts[1]
|
|
and parts[2] == 'f'
|
|
and parts[3].isdigit()
|
|
and parts[4].isdigit()
|
|
):
|
|
return {
|
|
'callback_token': parts[1],
|
|
'field_ref': int(parts[3]),
|
|
'option_ref': int(parts[4]),
|
|
}
|
|
raise ValueError('invalid QQ interaction callback data')
|
|
|
|
|
|
def interaction_event_from_payload(
|
|
event_data: dict[str, typing.Any],
|
|
) -> platform_events.PlatformSpecificEvent | None:
|
|
resolved = (event_data.get('data') or {}).get('resolved') or {}
|
|
callback_data = str(resolved.get('button_data') or '')
|
|
if not callback_data.startswith('lbi:'):
|
|
return None
|
|
parsed = parse_callback_data(callback_data)
|
|
chat_type = event_data.get('chat_type')
|
|
if chat_type == 2 or event_data.get('user_openid'):
|
|
target_type = 'person'
|
|
target_id = str(event_data.get('user_openid') or '')
|
|
elif chat_type == 1 or event_data.get('group_openid'):
|
|
target_type = 'group'
|
|
target_id = str(event_data.get('group_openid') or '')
|
|
elif chat_type == 0 or event_data.get('channel_id'):
|
|
target_type = 'group'
|
|
target_id = str(event_data.get('channel_id') or '')
|
|
else:
|
|
raise ValueError('QQ interaction callback has no target')
|
|
actor_id = str(
|
|
event_data.get('member_openid')
|
|
or event_data.get('user_openid')
|
|
or ((event_data.get('member') or {}).get('user') or {}).get('id')
|
|
or ''
|
|
)
|
|
return platform_events.PlatformSpecificEvent(
|
|
type='platform.specific',
|
|
adapter_name='qqofficial-omni',
|
|
action='interaction.submitted',
|
|
data={
|
|
**parsed,
|
|
'actor_id': actor_id,
|
|
'target_type': target_type,
|
|
'target_id': target_id,
|
|
'display_text': 'submitted',
|
|
},
|
|
timestamp=time.time(),
|
|
source_platform_object=event_data,
|
|
)
|