Files
DrSmoothl 744f25fd9b fix(protocol): integrate styled group card normalization
Keep the full control-character range limited to decoded styled-card text. Preserve member-level error isolation and cover whitespace, Unicode text, cached output, and restart behavior.

Refs #680
2026-11-09 14:57:41 +08:00

3.9 KiB
Raw Permalink Blame History

Contact profiles

get_friend_list and get_friends_with_category preserve their existing result shapes and add optional public profile fields to each friend. By default they only read the paginated roster: they do not issue a profile request for every friend. Both accept with_profile: true to explicitly query additional details. Large lists may require a longer client timeout.

Explicit enrichment uses a five-minute cache belonging to the current account, deduplicates simultaneous queries and limits aggregate profile concurrency to four. Values already supplied by the roster take precedence, including empty strings, zero and false. An enrichment failure fails the action after draining the current batch; it is logged, is not cached and is not automatically retried. A later call may retry it. The ordinary flat list retains its existing logged fallback to cached friends when the roster cannot be refreshed.

Friend rows contain user_id, nickname, remark, and category_id when available. Public fields also appear in get_stranger_info when QQ supplies them:

Fields Meaning
long_nick, sex, age, level Signature, gender, age and QQ level
birthday_year, birthday_month, birthday_day Birthday; a hidden year is omitted
phone_num, email Contact details made available by the profile
country, province, city, college Location and school
is_qq_vip, is_vip, is_years_vip, vip_level Ordinary membership, super membership (existing meaning of is_vip), annual membership and level
privilege_icons jump_url if supplied, plus open and closed icon lists

Each privilege entry may contain type, level, flag, is_big, jump_url, icon_url and deluxe_icon_url. The two icon URLs can coexist. Flags retain their server values; they are not guessed to mean annual or premium membership. Local downloaded icon paths are not part of the response.

Missing data is omitted rather than replaced with false, zero or an empty list. An explicitly returned empty icon-list body produces open: [], closed: []; unavailable icon data omits privilege_icons. Existing required stranger fields and aliases retain their earlier defaults. Malformed optional birthdays are logged without recording their values; malformed text or privilege payloads fail decoding.

SDK calls keep the existing options argument:

const friends = await client.getFriendList({ withProfile: true, timeoutMs: 120_000 });
const categories = await client.call('get_friends_with_category', { with_profile: true });

The exported FriendInfo, StrangerInfo, PublicProfileFields and ProfilePrivilege types describe these fields. Returned availability depends on QQ and the account's access; requesting enrichment does not guarantee every field is present.

Group member cards

Styled cards are decoded before their text is normalized. Within successfully decoded UTF-8 text, SnowLuma removes C0 control characters (U+0000–U+001F) except tab, line feed and carriage return, and removes DEL (U+007F). Spaces, Unicode formatting characters and emoji sequences are preserved. Raw plain text and explicitly literal display names retain strict control-character validation; invalid UTF-8 is rejected in both formats.

get_group_member_list and get_group_member_info keep returning member data when an individual group card cannot be decoded. The last valid card for that member in the same group and account is retained; if none is known, the OneBot card field is an empty string. An explicitly empty card from QQ clears a previous value.

Unreadable cards are logged with the group, member, byte length and decoding reason. The persistent cache distinguishes an unknown card from a confirmed empty one, so later roster refreshes can recover it after restart. The existing 60-second roster cache and no_cache: true refresh option still apply.