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
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:
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];
|
||||
};
|
||||
Reference in new issue
Block a user