SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

+906
View File
@@ -0,0 +1,906 @@
import type { PacketInfo } from '@snowluma/common/protocol-types';
import { formatEvent } from './format';
import { createLogger, type Logger } from '@snowluma/common/logger';
import type { BridgeEventBus } from './event-bus';
import type { QQEventVariant } from './events';
import type { GroupInviteTracker } from './group-invite-tracker';
import type { IdentityService } from './identity-service';
import { formatGroupRequestFlag, isSelfGroupInvitation, type GroupRequestInfo } from './qq-info';
const moduleLog = createLogger('Bridge');
const moduleEventLog = createLogger('Event');
const modulePacketLog = createLogger('Protocol.Packet');
// Notice kinds that get logged as a warning (operationally important
// state changes that an operator probably wants to see at default
// info level). Everything else falls through to info.
const WARN_EVENT_KINDS = new Set([
'group_recall',
'friend_recall',
'group_member_leave',
'group_mute',
'friend_request',
'group_invite',
]);
type GroupMemberIdentityEvent = Extract<
QQEventVariant,
{ kind: 'group_member_join' | 'group_member_leave' }
>;
export type CmdParser = (pkt: PacketInfo, identity: IdentityService) => QQEventVariant[];
class EnrichedDispatchError extends Error {
constructor(readonly originalError: unknown) {
super(originalError instanceof Error ? originalError.message : String(originalError));
this.name = 'EnrichedDispatchError';
}
}
export interface PacketPipelineDeps {
identity: IdentityService;
events: BridgeEventBus;
/** Account group-list fetch. Roster side-effect only; the pipeline
* swallows failures and does not read the returned roster. */
fetchGroupList(): Promise<void>;
/** One group's member-list fetch (adapter TTL applies). Roster
* side-effect only; the pipeline does not pass `force`. */
fetchGroupMemberList(groupId: number): Promise<void>;
/** Resolve a pending request's comment and approval tuple after a push. */
resolveGroupJoinRequest(
groupId: number,
uid: string,
subType: 'add' | 'invite',
invitedUid?: string,
): Promise<GroupRequestInfo | null>;
groupInvites: GroupInviteTracker;
}
export class IncomingPacketPipeline {
private cmdHandlers_ = new Map<string, CmdParser[]>();
private memberRefreshTasks_ = new Map<number, Promise<void>>();
private readonly log: Logger;
private readonly eventLog: Logger;
private readonly packetLog: Logger;
constructor(private readonly deps: PacketPipelineDeps) {
// Tag every line we emit with this Bridge's UIN so per-account file
// routing works. Unparseable uin (shouldn't happen) falls back to
// the module-level logger so we still log, just without the slot.
const uinNum = Number.parseInt(deps.identity.uin, 10);
const bind = Number.isFinite(uinNum) && uinNum > 0 ? { uin: uinNum } : null;
this.log = bind ? moduleLog.child(bind) : moduleLog;
this.eventLog = bind ? moduleEventLog.child(bind) : moduleEventLog;
this.packetLog = bind ? modulePacketLog.child(bind) : modulePacketLog;
}
registerCmd(cmd: string, parser: CmdParser): void {
const arr = this.cmdHandlers_.get(cmd) ?? [];
arr.push(parser);
this.cmdHandlers_.set(cmd, arr);
}
handlesCmd(cmd: string): boolean {
return this.cmdHandlers_.has(cmd);
}
process(pkt: PacketInfo): Promise<void> {
return this.processWithHandlers(pkt, this.cmdHandlers_.get(pkt.serviceCmd));
}
processEvents(pkt: PacketInfo, events: readonly QQEventVariant[]): Promise<void> {
return this.processWithHandlers(pkt, [() => [...events]]);
}
private processWithHandlers(
pkt: PacketInfo,
handlers: readonly CmdParser[] | undefined,
): Promise<void> {
const startedAt = Date.now();
if (!handlers) {
this.packetLog.trace(() => [
'packet_branch serviceCmd=%j seqId=%d branch=parser_unregistered',
pkt.serviceCmd,
pkt.seqId,
]);
this.packetLog.trace(() => [
'packet_terminal serviceCmd=%j seqId=%d outcome=dropped reason=parser_unregistered events=0 dispatched=0 elapsedMs=%d',
pkt.serviceCmd,
pkt.seqId,
Date.now() - startedAt,
]);
return Promise.resolve();
}
let eventCount = 0;
let dispatched = 0;
let parserErrors = 0;
let dispatchErrors = 0;
let enrichmentFailures = 0;
const enrichmentTasks: Promise<void>[] = [];
handlers.forEach((handler, index) => {
const parser = index + 1;
try {
const events = handler(pkt, this.deps.identity);
if (events.length === 0) {
this.packetLog.trace(() => [
'packet_branch serviceCmd=%j seqId=%d parser=%d branch=parser_zero_events',
pkt.serviceCmd,
pkt.seqId,
parser,
]);
return;
}
eventCount += events.length;
this.packetLog.trace(() => [
'packet_branch serviceCmd=%j seqId=%d parser=%d branch=parser_events events=%d',
pkt.serviceCmd,
pkt.seqId,
parser,
events.length,
]);
for (const event of events) {
if (this.needsPreDispatchIdentityRefresh(event)) {
this.traceEnrichmentStarted(pkt, event, 'identity_refresh');
const task = this.dispatchAfterIdentityRefresh(pkt, event)
.then((count) => {
dispatched += count;
})
.catch((error) => {
if (error instanceof EnrichedDispatchError) {
dispatchErrors += 1;
this.traceDispatchFailure(pkt, event, error.originalError);
} else {
enrichmentFailures += 1;
this.traceEnrichmentFailure(
pkt,
event,
'identity_refresh',
error,
);
}
this.log.warn(
'dispatchAfterIdentityRefresh failed: %s',
error instanceof Error
? (error.stack ?? error.message)
: String(error),
);
});
enrichmentTasks.push(task);
} else if (event.kind === 'group_invite') {
this.traceEnrichmentStarted(pkt, event, 'group_invite');
const task = this.dispatchGroupInvite(pkt, event)
.then((count) => {
dispatched += count;
})
.catch((error) => {
if (error instanceof EnrichedDispatchError) {
dispatchErrors += 1;
this.traceDispatchFailure(pkt, event, error.originalError);
} else {
enrichmentFailures += 1;
this.traceEnrichmentFailure(pkt, event, 'group_invite', error);
}
this.log.warn(
'dispatchGroupInvite failed: %s',
error instanceof Error
? (error.stack ?? error.message)
: String(error),
);
});
enrichmentTasks.push(task);
} else {
try {
dispatched += this.dispatchEvent(pkt, event, 'sync');
} catch (error) {
dispatchErrors += 1;
this.traceDispatchFailure(pkt, event, error);
this.log.error(
'dispatch error for %s event=%s: %s',
pkt.serviceCmd,
event.kind,
error instanceof Error
? (error.stack ?? error.message)
: String(error),
);
break;
}
}
}
} catch (error) {
parserErrors += 1;
this.packetLog.trace(() => [
'packet_branch serviceCmd=%j seqId=%d parser=%d branch=parser_exception error=%j',
pkt.serviceCmd,
pkt.seqId,
parser,
error instanceof Error ? error.message : String(error),
]);
this.log.error(
'handler error for %s: %s',
pkt.serviceCmd,
error instanceof Error ? (error.stack ?? error.message) : String(error),
);
}
});
const finish = (): void => {
if (enrichmentFailures > 0 || dispatchErrors > 0) {
this.packetLog.trace(() => [
'packet_terminal serviceCmd=%j seqId=%d outcome=failed reason=%s events=%d dispatched=%d parserErrors=%d dispatchErrors=%d elapsedMs=%d',
pkt.serviceCmd,
pkt.seqId,
enrichmentFailures > 0 ? 'enrichment_failed' : 'dispatch_exception',
eventCount,
dispatched,
parserErrors,
dispatchErrors,
Date.now() - startedAt,
]);
return;
}
if (parserErrors > 0) {
this.packetLog.trace(() => [
'packet_terminal serviceCmd=%j seqId=%d outcome=failed reason=parser_exception events=%d dispatched=%d parserErrors=%d elapsedMs=%d',
pkt.serviceCmd,
pkt.seqId,
eventCount,
dispatched,
parserErrors,
Date.now() - startedAt,
]);
return;
}
if (dispatched > 0) {
this.packetLog.trace(() => [
'packet_terminal serviceCmd=%j seqId=%d outcome=completed reason=dispatch_complete events=%d dispatched=%d parserErrors=%d elapsedMs=%d',
pkt.serviceCmd,
pkt.seqId,
eventCount,
dispatched,
parserErrors,
Date.now() - startedAt,
]);
return;
}
this.packetLog.trace(() => [
'packet_terminal serviceCmd=%j seqId=%d outcome=%s reason=%s events=%d dispatched=0 parserErrors=%d elapsedMs=%d',
pkt.serviceCmd,
pkt.seqId,
parserErrors > 0 ? 'failed' : 'dropped',
parserErrors > 0 ? 'parser_exception' : 'no_events',
eventCount,
parserErrors,
Date.now() - startedAt,
]);
};
if (enrichmentTasks.length === 0) {
finish();
return Promise.resolve();
}
return Promise.all(enrichmentTasks).then(finish);
}
private traceEnrichmentStarted(
pkt: PacketInfo,
event: QQEventVariant,
enrichment: 'identity_refresh' | 'group_invite',
): void {
this.packetLog.trace(() => [
'packet_branch serviceCmd=%j seqId=%d branch=enrichment_started eventKind=%j enrichment=%j',
pkt.serviceCmd,
pkt.seqId,
event.kind,
enrichment,
]);
}
private traceEnrichmentFailure(
pkt: PacketInfo,
event: QQEventVariant,
enrichment: 'identity_refresh' | 'group_invite',
error: unknown,
): void {
this.packetLog.trace(() => [
'packet_branch serviceCmd=%j seqId=%d branch=enrichment_failed eventKind=%j enrichment=%j error=%j',
pkt.serviceCmd,
pkt.seqId,
event.kind,
enrichment,
error instanceof Error ? error.message : String(error),
]);
}
private traceDispatchFailure(pkt: PacketInfo, event: QQEventVariant, error: unknown): void {
this.packetLog.trace(() => [
'packet_branch serviceCmd=%j seqId=%d branch=dispatch_exception eventKind=%j error=%j',
pkt.serviceCmd,
pkt.seqId,
event.kind,
error instanceof Error ? error.message : String(error),
]);
}
private traceDispatch(
pkt: PacketInfo,
eventKind: QQEventVariant['kind'],
mode: 'sync' | 'enriched' | 'derived',
): void {
this.packetLog.trace(() => [
'packet_branch serviceCmd=%j seqId=%d branch=dispatch eventKind=%j mode=%s',
pkt.serviceCmd,
pkt.seqId,
eventKind,
mode,
]);
}
private dispatchEvent(
pkt: PacketInfo,
event: QQEventVariant,
mode: 'sync' | 'enriched',
): number {
// Snapshot the sender's cached group card BEFORE dispatch — the side-effects
// inside finishDispatch self-heal it, so the old value must be read first.
const cardBefore = this.groupCardBefore(event);
this.finishDispatch(event);
this.traceDispatch(pkt, event.kind, mode);
let dispatched = 1;
if (this.emitGroupCardChange(event, cardBefore)) {
this.traceDispatch(pkt, 'group_card_change', 'derived');
dispatched++;
}
if (this.emitGroupInviteCard(event)) {
this.traceDispatch(pkt, 'group_invite', 'derived');
dispatched++;
}
return dispatched;
}
private emitGroupInviteCard(event: QQEventVariant): boolean {
if (event.kind !== 'friend_message' || event.senderUin === event.selfUin) {
return false;
}
const groupId = event.inviteCardGroupUin;
const sequence = event.inviteCardSequence;
if (
!groupId ||
!sequence ||
!this.deps.groupInvites.receiveCard(groupId, sequence, event.senderUid)
) {
return false;
}
this.finishDispatch({
kind: 'group_invite',
time: event.time,
selfUin: event.selfUin,
groupId,
fromUin: event.senderUin,
fromUid: event.senderUid,
invitedUin: event.selfUin,
subType: 'invite',
message: '',
flag: formatGroupRequestFlag({ groupId, sequence, eventType: 2, filtered: false }),
});
return true;
}
private emit(event: QQEventVariant): void {
// Fire-and-forget: errors inside subscribers are surfaced via the bus's
// own onError hook so one bad listener never blocks the others.
void this.deps.events.emit(event);
}
/** The sender's cached group card just before this event is dispatched (its
* side-effects will overwrite the cache). Only meaningful for group_message. */
private groupCardBefore(event: QQEventVariant): string | undefined {
if (event.kind !== 'group_message') {
return undefined;
}
return this.deps.identity.findGroupMember(event.groupId, event.senderUin)?.card;
}
/** Surface a `group_card_change` when a KNOWN member's card actually changed —
* mirrors NapCat's `parseCardChangedEvent`. Requires a non-empty prior card
* (`cardBefore`) that differs from a non-empty new one, so a cold/unknown
* cache never fabricates a change on first contact. */
private emitGroupCardChange(event: QQEventVariant, cardBefore: string | undefined): boolean {
if (event.kind !== 'group_message') {
return false;
}
const cardNew = event.senderCard ?? '';
if (!cardBefore || !cardNew || cardBefore === cardNew) {
return false;
}
this.finishDispatch({
kind: 'group_card_change',
time: event.time,
selfUin: event.selfUin,
groupId: event.groupId,
userUin: event.senderUin,
cardNew,
cardOld: cardBefore,
});
return true;
}
/**
* Common dispatch tail shared by the sync path and the two async enrichment
* paths: run side effects, log the event, then emit it.
*/
private finishDispatch(event: QQEventVariant): void {
this.handleSideEffects(event);
printEvent(this.eventLog, this.deps.identity, event);
this.emit(event);
}
private needsPreDispatchIdentityRefresh(
event: QQEventVariant,
): event is Extract<QQEventVariant, { kind: 'group_member_join' }> {
return (
event.kind === 'group_member_join' &&
event.groupId > 0 &&
((event.userUin <= 0 && Boolean(event.userUid)) ||
((event.invitorUin ?? 0) <= 0 && Boolean(event.invitorUid)))
);
}
private async dispatchAfterIdentityRefresh(
pkt: PacketInfo,
event: Extract<QQEventVariant, { kind: 'group_member_join' }>,
): Promise<number> {
try {
await this.prepareGroupMemberJoinIdentity(event);
} catch (e) {
this.packetLog.trace(() => [
'packet_branch serviceCmd=%j seqId=%d branch=enrichment_degraded eventKind=%j enrichment="identity_refresh" error=%j',
pkt.serviceCmd,
pkt.seqId,
event.kind,
e instanceof Error ? e.message : String(e),
]);
this.log.warn(
'failed to resolve group member join identity: group=%d uid=%s err=%s',
event.groupId,
event.userUid ?? '',
e instanceof Error ? e.message : String(e),
);
}
try {
return this.dispatchEvent(pkt, event, 'enriched');
} catch (error) {
throw new EnrichedDispatchError(error);
}
}
private async dispatchGroupInvite(
pkt: PacketInfo,
event: Extract<QQEventVariant, { kind: 'group_invite' }>,
): Promise<number> {
// A self-invite notice carries an inviter UID but no approval sequence.
if (
event.subType === 'invite' &&
this.isSelfMemberIdentity(event.invitedUin ?? 0, event.invitedUid)
) {
if (!(event.groupId > 0) || !event.fromUid?.trim() || !event.flag) {
this.log.warn('self invitation missing group or inviter: group=%d', event.groupId);
return 0;
}
if (!this.deps.groupInvites.receiveNotice(event.groupId, event.fromUid)) {
this.log.debug(
'self invitation already dispatched from the card: group=%d',
event.groupId,
);
return 0;
}
if (!event.fromUin) {
event.fromUin =
(await this.deps.identity.resolveUin(event.fromUid, event.groupId)) ?? 0;
if (!event.fromUin) {
this.log.warn(
'self invitation sender unavailable: group=%d uid=%s',
event.groupId,
event.fromUid,
);
}
}
try {
return this.dispatchEvent(pkt, event, 'sync');
} catch (error) {
throw new EnrichedDispatchError(error);
}
}
const uid = event.fromUid;
if (!uid) {
throw new Error('group request omitted requester UID');
}
this.deps.identity.rememberRequestIdentity({
groupId: event.groupId,
uid,
uin: event.fromUin,
source: 'group_request',
});
const request = await this.deps.resolveGroupJoinRequest(
event.groupId,
uid,
event.subType,
event.invitedUid,
);
if (!request) {
this.log.warn('pending group request not found: group=%d uid=%s', event.groupId, uid);
return 0;
}
if (isSelfGroupInvitation(request)) {
return 0;
}
event.message = request.comment;
event.flag = formatGroupRequestFlag(request);
const requesterUin = event.subType === 'invite' ? request.invitorUin : request.targetUin;
event.fromUin =
event.fromUin ||
requesterUin ||
(await this.deps.identity.resolveUin(uid, event.groupId)) ||
0;
if (event.fromUin === 0) {
this.log.warn('group request sender unavailable: group=%d uid=%s', event.groupId, uid);
}
if (event.subType === 'invite') {
event.invitedUid = request.targetUid;
event.invitedUin =
request.targetUin ||
(await this.deps.identity.resolveUin(request.targetUid, event.groupId)) ||
0;
}
try {
return this.dispatchEvent(pkt, event, 'enriched');
} catch (error) {
throw new EnrichedDispatchError(error);
}
}
private async prepareGroupMemberJoinIdentity(
event: Extract<QQEventVariant, { kind: 'group_member_join' }>,
): Promise<void> {
this.resolveMemberIdentityFromCache(event);
const results = await Promise.allSettled(
(
[
['userUid', 'userUin'],
['invitorUid', 'invitorUin'],
] as const
).map(async ([uidKey, uinKey]) => {
const uid = event[uidKey];
if (!uid || (event[uinKey] ?? 0) > 0) {
return;
}
const uin = await this.deps.identity.resolveUin(uid, event.groupId);
if (uin !== null) {
event[uinKey] = uin;
} else {
this.log.warn(
'group join identity unavailable: group=%d role=%s',
event.groupId,
uidKey,
);
}
}),
);
this.resolveMemberIdentityFromCache(event);
const failures = results.filter((result) => result.status === 'rejected');
if (failures.length) {
throw new AggregateError(
failures.map((result) => result.reason),
`group join identity lookup failed: ${failures
.map(({ reason }) =>
reason instanceof Error ? reason.message : String(reason),
)
.join('; ')}`,
);
}
}
private resolveMemberIdentityFromCache(event: GroupMemberIdentityEvent): void {
if (event.groupId <= 0) {
return;
}
if (event.userUin <= 0 && event.userUid) {
const uin = this.deps.identity.findUinByUid(event.userUid, event.groupId);
if (uin !== null) {
event.userUin = uin;
}
}
if (event.operatorUin <= 0 && event.operatorUid) {
const uin = this.deps.identity.findUinByUid(event.operatorUid, event.groupId);
if (uin !== null) {
event.operatorUin = uin;
}
}
if (
event.kind === 'group_member_join' &&
(event.invitorUin ?? 0) <= 0 &&
event.invitorUid
) {
const uin = this.deps.identity.findUinByUid(event.invitorUid, event.groupId);
if (uin !== null) {
event.invitorUin = uin;
}
}
}
private isSelfMemberIdentity(uin: number, uid?: string): boolean {
const selfUin = Number(this.deps.identity.uin);
return (uin > 0 && uin === selfUin) || (Boolean(uid) && uid === this.deps.identity.selfUid);
}
private handleSideEffects(event: QQEventVariant): void {
this.rememberEventIdentity(event);
let groupId = 0;
let reason = '';
let refreshGroupList = false;
switch (event.kind) {
case 'group_member_join':
groupId = event.groupId;
reason = 'group_member_join';
refreshGroupList = this.isSelfMemberIdentity(event.userUin, event.userUid);
break;
case 'group_member_leave':
groupId = event.groupId;
reason = 'group_member_leave';
break;
case 'group_admin':
groupId = event.groupId;
reason = 'group_admin';
break;
default:
return;
}
if (groupId <= 0) {
return;
}
if (this.memberRefreshTasks_.has(groupId)) {
return;
}
if (event.kind === 'group_member_join' && !this.deps.identity.findGroup(groupId)) {
refreshGroupList = true;
}
const task = (async () => {
try {
if (refreshGroupList) {
try {
await this.deps.fetchGroupList();
} catch {
/* ignore */
}
}
if (!this.deps.identity.findGroup(groupId)) {
this.log.debug('member cache refreshed: group=%d reason=%s', groupId, reason);
return;
}
await this.deps.fetchGroupMemberList(groupId);
this.log.debug('member cache refreshed: group=%d reason=%s', groupId, reason);
} catch (e) {
this.log.warn(
'failed to refresh member cache: group=%d reason=%s err=%s',
groupId,
reason,
e instanceof Error ? e.message : String(e),
);
} finally {
this.memberRefreshTasks_.delete(groupId);
}
})();
this.memberRefreshTasks_.set(groupId, task);
}
private rememberEventIdentity(event: QQEventVariant): void {
switch (event.kind) {
case 'friend_message':
// The message already carries the authoritative UID/UIN pair. Keep it
// at the receive boundary so later C2C operations never have to infer
// a peer UID from a stale message row or a fallible profile lookup.
this.deps.identity.rememberRequestIdentity({
uid: event.senderUid,
uin: event.senderUin,
source: 'friend_message',
});
break;
case 'group_message': {
// [#1] Self-heal a member's cached group card from message traffic. The
// member cache only refreshes via a member-list refetch, which nothing
// triggers on a card change and a quiet, months-long bot may never fire.
// The decoder resolved the current card from field 4 into senderCard;
// when it differs from the cache, update it — gated so we write only on
// an actual change, not on every message.
const cached = this.deps.identity.findGroupMember(event.groupId, event.senderUin);
if (cached && event.senderCard !== (cached.card ?? '')) {
this.deps.identity.updateGroupMember(event.groupId, {
...cached,
card: event.senderCard,
});
}
break;
}
case 'group_member_join':
if (event.invitorUid) {
this.deps.identity.rememberRequestIdentity({
groupId: event.groupId,
uid: event.invitorUid,
uin: event.invitorUin ?? 0,
source: 'group_request',
});
}
this.deps.identity.rememberGroupMemberIdentity(event.groupId, {
uid: event.userUid,
uin: event.userUin,
});
this.deps.identity.rememberGroupMemberIdentity(event.groupId, {
uid: event.operatorUid,
uin: event.operatorUin,
});
this.deps.identity.rememberGroupMemberJoined(event.groupId, {
uid: event.userUid,
uin: event.userUin,
});
break;
case 'group_member_leave':
this.deps.identity.markGroupMemberInactive(event.groupId, {
uid: event.userUid,
uin: event.userUin,
});
this.deps.identity.rememberGroupMemberIdentity(event.groupId, {
uid: event.operatorUid,
uin: event.operatorUin,
});
break;
case 'group_admin': {
this.deps.identity.rememberGroupMemberIdentity(event.groupId, {
uin: event.userUin,
});
// #93: get_group_member_info serves the cache and must not refetch
// per message. Patch a known member's role here — never invent a
// phantom, never downgrade the owner.
const cached = this.deps.identity.findGroupMember(event.groupId, event.userUin);
if (cached && cached.role !== 'owner') {
this.deps.identity.updateGroupMember(event.groupId, {
...cached,
role: event.set ? 'admin' : 'member',
});
}
break;
}
case 'friend_request':
this.deps.identity.rememberRequestIdentity({
uid: event.fromUid,
uin: event.fromUin,
source: 'friend_request',
});
break;
case 'friend_remark_changed': {
const rosterUpdated = this.deps.identity.updateFriendRemark(
event.userUid,
event.userUin,
event.remark,
);
this.log.debug(
'friend remark synchronized (uid=%s uin=%d length=%d rosterUpdated=%s)',
event.userUid,
event.userUin,
event.remark.length,
rosterUpdated,
);
break;
}
case 'group_invite': {
this.deps.identity.rememberRequestIdentity({
groupId: event.groupId,
uid: event.fromUid,
uin: event.fromUin,
source: 'group_request',
});
if (event.invitedUid) {
this.deps.identity.rememberRequestIdentity({
groupId: event.groupId,
uid: event.invitedUid,
uin: event.invitedUin ?? 0,
source: 'group_request',
});
}
break;
}
default:
break;
}
}
}
function printEvent(log: Logger, identity: IdentityService, event: QQEventVariant): void {
// Message-class events (group/friend/temp message) get rendered by the
// OneBot layer's logReceivedMessage — its output already includes the
// assigned message ID, which the raw packet doesn't have. Returning
// null here is the formatter's signal to skip.
const message = formatEvent(identity, event);
if (!message) {
return;
}
if (WARN_EVENT_KINDS.has(event.kind)) {
log.warn('%s', message);
} else {
log.info('%s', message);
}
}