feat(onebot): add group_name to group message events (#212)

Group message events (message + message_sent) now carry group_name — a
non-standard but common OneBot extension. The name comes from the
message's own wire field (ResponseGrp field 7, accurate even right after
a rename), falling back to the identity group cache, else '' when neither
is available. It's resolved in the group-message decoder, where both the
wire head and the identity cache are already at hand, so the converter
just passes it through (no ConverterContext change).

Verified live (tempserver): real inbound group messages from a non-self
account carry group_name set to the actual group name.
This commit is contained in:
DrSmoothl
2026-07-09 18:50:54 +08:00
parent 7ab91c81e9
commit 9b470d0412
5 changed files with 15 additions and 3 deletions
@@ -73,6 +73,7 @@ export async function convertGroupMessage(ctx: ConverterContext, event: GroupMes
message_id: messageId,
message_seq: event.msgSeq,
group_id: event.groupId,
group_name: event.groupName,
user_id: event.senderUin,
message: segments,
raw_message: segmentsToRawMessage(segments),
@@ -54,6 +54,7 @@ function makeGroupMessage(senderUin: number): GroupMessage {
time: 1700000000,
selfUin: SELF_ID,
groupId: 99999,
groupName: 'Test Group 测试群',
senderUin,
senderNick: senderUin === SELF_ID ? 'me' : 'peer',
senderCard: '',
@@ -100,6 +101,7 @@ describe('convertEvent — message kinds', () => {
expect(out!.message_type).toBe('group');
expect(out!.sub_type).toBe('normal');
expect(out!.group_id).toBe(GROUP_ID);
expect(out!.group_name).toBe('Test Group 测试群');
});
it('group_message self → post_type "message_sent"', async () => {
+4
View File
@@ -133,6 +133,10 @@ export interface FriendMessage extends QQEvent {
export interface GroupMessage extends QQEvent {
kind: 'group_message';
groupId: number;
/** Group display name. From the message's own wire field (ResponseGrp
* field 7 — accurate even right after a rename), falling back to the
* identity group cache, else '' when neither is available. */
groupName: string;
senderUin: number;
senderNick: string;
senderCard: string;
@@ -12,6 +12,7 @@ export const decodeGroupMessage: MsgPushDecoder = (ctx) => {
msgId: ctx.head.msgId & 0x7FFFFFFF,
elements: decodeRichBody(ctx.body, true),
groupId: 0,
groupName: '',
senderNick: '',
senderCard: '',
senderRole: '',
@@ -43,5 +44,9 @@ export const decodeGroupMessage: MsgPushDecoder = (ctx) => {
} else if (!ev.senderNick && grp?.memberCard) {
ev.senderNick = grp.memberCard;
}
// Group name: prefer the message's own wire field (ResponseGrp field 7 —
// fresh even right after a rename), fall back to the cached group list, else
// leave it empty. `ev.groupId` is set above from grp.groupUin.
ev.groupName = grp?.groupName || ctx.identity.findGroup(ev.groupId)?.groupName || '';
return [ev];
};
@@ -9,7 +9,7 @@ import type { SsoGetGroupMsg, SsoGetGroupMsgResponse } from '@snowluma/proto-def
import type { IdentityService } from '../../src/identity-service';
import { SSO_GET_GROUP_MSG_CMD, fetchGroupMessageRange } from '../../src/msg-push';
const identity = { findGroupMember: () => undefined } as unknown as IdentityService;
const identity = { findGroupMember: () => undefined, findGroup: () => null } as unknown as IdentityService;
function okResult(data: Uint8Array): SendPacketResult {
return { success: true, gotResponse: true, responseData: data } as SendPacketResult;
@@ -69,7 +69,7 @@ describe('fetchGroupMessageRange / SsoGetGroupMsg', () => {
it('[#1] prefers the fresher group card in grp.memberCard (field 4) over a stale cache card', async () => {
const stale = { uin: 222, uid: '', nickname: 'BaseNick', card: 'StaleCard', role: 'member', level: 0, title: '', joinTime: 0, lastSentTime: 0, shutUpTime: 0 };
const idWithMember = { findGroupMember: (_g: number, u: number) => (u === 222 ? stale : undefined) } as unknown as IdentityService;
const idWithMember = { findGroupMember: (_g: number, u: number) => (u === 222 ? stale : undefined), findGroup: () => null } as unknown as IdentityService;
const resp = protobuf_encode<SsoGetGroupMsgResponse>({
body: { groupUin: 9999, messages: [
{ responseHead: { fromUin: 222, grp: { groupUin: 9999, memberCard: 'FreshCard' } }, // field2 empty, field4 = fresh card
@@ -84,7 +84,7 @@ describe('fetchGroupMessageRange / SsoGetGroupMsg', () => {
it('[#1] keeps the cache card when field 4 equals the base nickname (no active group card)', async () => {
const m = { uin: 222, uid: '', nickname: 'BaseNick', card: 'CachedCard', role: 'member', level: 0, title: '', joinTime: 0, lastSentTime: 0, shutUpTime: 0 };
const id = { findGroupMember: () => m } as unknown as IdentityService;
const id = { findGroupMember: () => m, findGroup: () => null } as unknown as IdentityService;
const resp = protobuf_encode<SsoGetGroupMsgResponse>({
body: { groupUin: 9999, messages: [
{ responseHead: { fromUin: 222, grp: { groupUin: 9999, memberCard: 'BaseNick' } }, // field4 == base nick → not a card