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

@@ -0,0 +1,164 @@
import { protobuf_decode } from '@snowluma/proton';
import { createLogger } from '@snowluma/common/logger';
import type { GroupMemberJoin, GroupMemberLeave } from '../../events';
import type { GroupChange, SelfJoinInGroup } from '@snowluma/proto-defs/notify';
import {
decodeGroupChangeOperatorUid,
decodeNestedOperatorUid,
decodeRawOperatorUid,
resolveUidToUin,
} from '../helpers';
import type { MsgPushDecoder } from '../registry';
const log = createLogger('MsgPush.GroupJoin');
function joinTypeFromOperationType(raw: number): NonNullable<GroupMemberJoin['joinType']> {
// QQ uses bit 0x80 as a flag on the PkgType=33 operation code. Clear only
// that bit: other high bits remain significant in the native handler. The
// normalized code 3 means an invitation; all other values mean approval.
// Do not use the nested `join_type`: that is an invitation-source enum.
const normalized = raw - (raw & 0x80);
return normalized === 3 ? 'invite' : 'approve';
}
export const decodeGroupMemberJoin: MsgPushDecoder = (ctx) => {
const change = protobuf_decode<GroupChange>(ctx.content);
if (!change) {
return [];
}
const groupId = change.groupUin ?? 0;
const userUid = change.memberUid ?? '';
const operatorUid = decodeGroupChangeOperatorUid(
change.operatorBytes ?? new Uint8Array(0),
'group member increase',
);
const operation = (change.decreaseType ?? 0) - ((change.decreaseType ?? 0) & 0x80);
const origin = ctx.responseHead?.grp?.messageKey?.joinExtra?.origin;
// QQ consumes this independent origin only for operation 2. Type 1
// identifies a QR-code sharer; neither that person nor the approving
// operator is evidence of an invitation.
const invitorUid =
operation === 2 && origin?.type !== 1 && origin?.uid?.length
? decodeRawOperatorUid(origin.uid, 'group member invitation origin')
: '';
log.trace(
'join identity: group=%d operation=%d originType=%s inviterProvided=%s',
groupId,
operation,
origin?.type === undefined ? 'absent' : String(origin.type),
Boolean(invitorUid),
);
const ev: GroupMemberJoin = {
kind: 'group_member_join',
time: ctx.head.timestamp,
selfUin: ctx.selfUin,
groupId,
userUin: resolveUidToUin(ctx.identity, groupId, userUid),
operatorUin: resolveUidToUin(ctx.identity, groupId, operatorUid),
userUid,
operatorUid,
...(invitorUid
? { invitorUid, invitorUin: resolveUidToUin(ctx.identity, groupId, invitorUid) }
: {}),
joinType: joinTypeFromOperationType(change.decreaseType ?? 0),
};
return [ev];
};
// QQ's GroupChange.decreaseType (proto field 4) on a member-decrease push:
// 3 → the bot itself was kicked
// 129 → the group was disbanded
// 130 → voluntary leave
// 131 → another member was kicked
// 0 / absent → treat as voluntary leave (defensive default)
// The kick → kick_me distinction (was it us?) is decided downstream in the
// OneBot converter via selfId; here we only resolve the protocol-level reason.
function leaveTypeFromDecreaseType(dt: number): GroupMemberLeave['leaveType'] {
switch (dt) {
case 129:
return 'disband';
case 0:
case 130:
return 'leave';
default:
return 'kick';
}
}
export const decodeGroupMemberLeave: MsgPushDecoder = (ctx) => {
const change = protobuf_decode<GroupChange>(ctx.content);
if (!change) {
return [];
}
const dt = change.decreaseType ?? 0;
const groupId = change.groupUin ?? 0;
const userUid = change.memberUid ?? '';
const userUin = resolveUidToUin(ctx.identity, groupId, userUid);
let operatorUid: string;
let operatorUin: number;
if (dt === 130) {
// A voluntary leave has no independent operator. QQ may place structured
// metadata in field 5, so it must not be decoded as a UID. OneBot models
// the leaving member as the operator for this event.
operatorUid = userUid;
operatorUin = userUin;
} else {
const operatorBytes = change.operatorBytes ?? new Uint8Array(0);
// Self-kick always uses OperatorInfo. Ordinary removals vary between a raw
// UTF-8 UID and the same envelope, so discriminate by its field-1 wire tag.
operatorUid =
dt === 3
? decodeNestedOperatorUid(operatorBytes, 'group member decrease type=3')
: decodeGroupChangeOperatorUid(operatorBytes, `group member decrease type=${dt}`);
operatorUin = resolveUidToUin(ctx.identity, groupId, operatorUid);
}
const ev: GroupMemberLeave = {
kind: 'group_member_leave',
time: ctx.head.timestamp,
selfUin: ctx.selfUin,
groupId,
userUin,
operatorUin,
userUid,
operatorUid,
leaveType: leaveTypeFromDecreaseType(dt),
};
return [ev];
};
export const decodeGroupSelfJoined: MsgPushDecoder = (ctx) => {
const joined = protobuf_decode<SelfJoinInGroup>(ctx.content);
if (!joined) {
return [];
}
// groupUin is uint_64 on the wire (bigint). Real group IDs fit in
// uint32 (≤ 10 digits in practice), so Number() conversion is safe
// — saturate to 0 on the unlikely overflow rather than letting
// BigInt propagate downstream where consumers expect a number.
const groupUinBig = joined.groupUin ?? 0n;
const groupId = groupUinBig > 0n && groupUinBig <= 0x7fffffffn ? Number(groupUinBig) : 0;
const operatorUid = joined.operatorUid ?? '';
// We're the one joining — surface the bot's own identity as userUin
// so the existing converter's `notice.group_increase` shape works
// unchanged.
const ev: GroupMemberJoin = {
kind: 'group_member_join',
time: ctx.head.timestamp,
selfUin: ctx.selfUin,
groupId,
userUin: ctx.selfUin,
userUid: ctx.identity.selfUid ?? '',
operatorUin: resolveUidToUin(ctx.identity, groupId, operatorUid),
operatorUid,
};
return [ev];
};