补全一些onebot action实现 2 (#24)

* feat: implement get profile like

* feat: implement get at all remain

* feat: implement get undirectional friends

* feat: implement set self long nick

* feat: implement set input status

* feat: implement EN 2 ZH

* feat: implement get mini app ark

* feat: implement click inline button

* feat: implement set and send group sign

* feat: implement set qq avatar

* feat: group notice with pic and delete notice implement

* fix(group-notice): POST api and fix new bkn logic
This commit is contained in:
H3CoF6
2026-05-10 10:14:41 +08:00
committed by GitHub
parent bec23c4dbb
commit 6f6ce1144b
8 changed files with 1022 additions and 74 deletions
+374 -1
View File
@@ -47,6 +47,24 @@ import {
SetStatusReqSchema,
SetStatusRespSchema,
OidbSetProfileSchema,
Oidb0x7edReqSchema,
Oidb0x7edRespSchema,
Oidb0x8a7RespSchema,
Oidb0x8a7ReqSchema,
Oidb0xe17RespSchema,
Oidb0xe17ReqSchema,
Oidb0x112aReqSchema,
Oidb0x112aRespSchema,
Oidb0xcd4ReqSchema,
Oidb0xcd4RespSchema,
Oidb0x990ReqSchema,
Oidb0x990RespSchema,
MiniAppShareReqSchema,
MiniAppShareRespSchema,
Oidb0x112eReqSchema,
Oidb0x112eRespSchema,
Oidb0xeb7ReqSchema,
Oidb0xeb7RespSchema,
} from './proto/oidb-action';
import { FileUploadExtSchema } from './proto/highway';
import {
@@ -1358,4 +1376,359 @@ export async function setProfile(
req,
OidbSetProfileSchema
);
}
}
export async function getProfileLike(
bridge: Bridge,
userId?: number,
start: number = 0,
limit: number = 10
) {
// const isSelf = !userId || userId === bridge.qqInfo.uin;
// const targetUid = isSelf
// ? bridge.qqInfo.uid
// : await bridge.getUidByUinV2(userId!);
const isSelf = !userId
const targetUid = isSelf
? await resolveSelfUid(bridge)
: await resolveUserUid(bridge, userId)
if (!targetUid) {
throw new Error('target uid not found');
}
const req = {
targetUid: targetUid,
basic: 1,
vote: 1,
favorite: 1,
start: start,
limit: limit,
};
const result = await sendOidbAndDecode<any>(
bridge,
'OidbSvcTrpcTcp.0x7ed_12',
0x7ED,
12,
req,
Oidb0x7edReqSchema,
Oidb0x7edRespSchema
);
const data = result?.userLikeInfos?.[0];
if (!data) {
throw new Error('get profile like info empty');
}
return {
uid: data.uid,
time: Number(data.time),
favoriteInfo: {
total_count: data.favoriteInfo?.totalCount || 0,
last_time: Number(data.favoriteInfo?.lastTime || 0),
today_count: data.favoriteInfo?.newCount || 0,
userInfos: []
},
voteInfo: {
total_count: data.voteInfo?.totalCount || 0,
new_count: data.voteInfo?.newCount || 0,
new_nearby_count: 0,
last_visit_time: Number(data.voteInfo?.lastTime || 0),
userInfos: []
}
};
}
export async function getGroupAtAllRemain(
bridge: Bridge,
groupId: number
) {
const req = {
basic1: 1,
basic2: 2,
basic3: 1,
uin: BigInt(bridge.qqInfo.uin),
groupId: BigInt(groupId),
type: 0,
};
const result = await sendOidbAndDecode<any>(
bridge,
'OidbSvcTrpcTcp.0x8a7_0',
0x8A7,
0,
req,
Oidb0x8a7ReqSchema,
Oidb0x8a7RespSchema
);
if (!result) {
throw new Error('get group at all remain result empty');
}
return {
can_at_all: !!result.canAtAll,
remain_at_all_count_for_group: Number(result.groupRemain || 0), // 【修改点】:防止底层库将 uint32 也返回成 BigInt 导致无法被 JSON 序列化
remain_at_all_count_for_uin: Number(result.uinRemain || 0) // 【修改点】:同上
};
}
export async function getUnidirectionalFriendList(
bridge: Bridge
) {
const reqObj = {
uint64_uin: String(bridge.qqInfo.uin),
uint64_top: 0,
uint32_req_num: 99,
bytes_cookies: ""
};
const req = {
jsonBody: JSON.stringify(reqObj)
};
const result = await sendOidbAndDecode<any>(
bridge,
'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0',
0xE17,
0,
req,
Oidb0xe17ReqSchema,
Oidb0xe17RespSchema
);
if (!result || !result.jsonBody) {
throw new Error('get unidirectional friend list empty');
}
const parsed = JSON.parse(result.jsonBody);
return parsed.rpt_block_list || [];
}
export async function setSelfLongNick(
bridge: Bridge,
longNick: string
) {
const req = {
uin: BigInt(bridge.qqInfo.uin),
profile: {
tag: 102,
value: String(longNick)
}
};
await sendOidbAndDecode<any>(
bridge,
'OidbSvcTrpcTcp.0x112a_2',
0x112A,
2,
req,
Oidb0x112aReqSchema,
Oidb0x112aRespSchema
);
}
export async function setInputStatus(
bridge: Bridge,
userId: number,
eventType: number
) {
const targetUid = await resolveUserUid(bridge, userId);
if (!targetUid) {
throw new Error('target uid not found');
}
const req = {
reqBody: {
uid: targetUid,
chatType: 0,
eventType: eventType
}
};
await sendOidbAndDecode<any>(
bridge,
'OidbSvcTrpcTcp.0xcd4_1',
0xCD4,
1,
req,
Oidb0xcd4ReqSchema,
Oidb0xcd4RespSchema
);
}
export async function translateEn2Zh(
bridge: Bridge,
words: string[]
) {
const req = {
translateReq: {
srcLang: 'en',
dstLang: 'zh',
words: words
},
tag10: 1,
tag12: 1
};
const result = await sendOidbAndDecode<any>(
bridge,
'OidbSvcTrpcTcp.0x990_2',
0x990,
2,
req,
Oidb0x990ReqSchema,
Oidb0x990RespSchema
);
const resp = result?.translateResp;
if (!resp) {
throw new Error('translate response empty');
}
return resp.dstWords || [];
}
export async function getMiniAppArk(
bridge: Bridge,
type: string,
title: string,
desc: string,
picUrl: string,
jumpUrl: string
) {
let appid = "1109937557"; // 默认 bili
let iconUrl = "http://miniapp.gtimg.cn/public/appicon/51f90239b78a2e4994c11215f4c4ba15_200.jpg";
if (type === 'weibo') {
appid = "1109224783";
iconUrl = "http://miniapp.gtimg.cn/public/appicon/35bbb44dc68e65194cfacfb206b8f1f7_200.jpg";
} else if (type !== 'bili') {
throw new Error(`unsupported type: ${type}, only support bili and weibo`);
}
const request = protoEncode({
sdkVersion: "V1_PC_MINISDK_99.99.99_1_APP_A",
body: {
appid,
title,
desc,
picUrl,
jumpUrl,
iconUrl
}
}, MiniAppShareReqSchema);
const result = await bridge.sendRawPacket('LightAppSvc.mini_app_share.AdaptShareInfo', request);
if (!result.success || !result.responseData) {
throw new Error(result.errorMessage || 'get mini app ark failed');
}
const decoded = protoDecode(result.responseData, MiniAppShareRespSchema);
const jsonStr = decoded?.body?.jsonStr;
if (!jsonStr) {
throw new Error('mini app share json empty');
}
const parsed = JSON.parse(jsonStr);
return {
data: {
ver: parsed.ver,
prompt: parsed.prompt,
config: parsed.config,
app: parsed.appName,
view: parsed.appView,
meta: parsed.metaData,
miniappShareOrigin: 3,
miniappOpenRefer: "10002"
}
};
}
export async function clickInlineKeyboardButton(
bridge: Bridge,
groupId: number,
botAppid: number,
buttonId: string,
callbackData: string,
msgSeq: number
) {
const req = {
botAppid: BigInt(botAppid),
msgSeq: BigInt(msgSeq),
buttonId: String(buttonId),
callbackData: String(callbackData || ''),
unknown7: 0,
groupId: BigInt(groupId),
unknown9: 1,
};
const result = await sendOidbAndDecode<any>(
bridge,
'OidbSvcTrpcTcp.0x112e_1',
0x112E,
1,
req,
Oidb0x112eReqSchema,
Oidb0x112eRespSchema
);
if (!result) {
throw new Error('click inline keyboard button result empty');
}
return {
result: Number(result.result || 0),
errMsg: result.errMsg || '',
status: 0,
promptText: result.promptText || '',
promptType: 0,
promptIcon: 0
};
}
export async function sendGroupSign(
bridge: Bridge,
groupId: number
) {
const req = {
signInInfo: {
uin: String(bridge.qqInfo.uin),
groupId: String(groupId),
version: "9.0.90"
}
};
await sendOidbAndDecode<any>(
bridge,
'OidbSvcTrpcTcp.0xEB7_1',
0xEB7,
1,
req,
Oidb0xeb7ReqSchema,
Oidb0xeb7RespSchema
);
}
export async function setAvatar(
bridge: Bridge,
source: string,
): Promise<void> {
const loaded = await loadBinarySource(source, 'avatar');
if (!loaded.bytes.length) throw new Error('avatar file is empty');
const hashes = computeHashes(loaded.bytes);
const session = await fetchHighwaySession(bridge);
await uploadHighwayHttp(bridge, session, 90, loaded.bytes, hashes.md5, new Uint8Array(0));
}
+45 -1
View File
@@ -67,6 +67,16 @@ import {
fetchGroupFileCount as fetchGroupFileCount_,
setOnlineStatus as setOnlineStatus_,
setProfile as setProfile_,
getProfileLike as getProfileLike_,
getGroupAtAllRemain as getGroupAtAllRemain_,
getUnidirectionalFriendList as getUnidirectionalFriendList_,
setSelfLongNick as setSelfLongNick_,
setInputStatus as setInputStatus_,
translateEn2Zh as translateEn2Zh_,
getMiniAppArk as getMiniAppArk_,
clickInlineKeyboardButton as clickInlineKeyboardButton_,
sendGroupSign as sendGroupSign_,
setAvatar as setAvatar_,
} from './bridge-actions';
import {
getGroupHonorInfo as getGroupHonorInfo_,
@@ -75,6 +85,7 @@ import {
getGroupEssenceAll as getGroupEssenceAll_,
sendGroupNotice as sendGroupNotice_,
getGroupNotice as getGroupNotice_,
deleteGroupNoticeByFid as deleteGroupNotice_,
getCookiesStr as getCookiesStr_,
getCsrfToken as getCsrfToken_,
getCredentials as getCredentials_,
@@ -492,8 +503,15 @@ export class Bridge {
return getGroupNotice_(this, groupId);
}
async deleteGroupNotice(groupId: number, fid: string): Promise<boolean> {
return deleteGroupNotice_(this, groupId, fid);
}
async fetchGroupFileCount(groupId: number): Promise<{ fileCount: number; maxCount: number }> { return fetchGroupFileCount_(this, groupId); }
async getGroupAtAllRemain(groupId: number) {
return getGroupAtAllRemain_(this, groupId);
}
// extend
async setOnlineStatus(status: number, extStatus: number = 0, batteryStatus: number = 100): Promise<void> {
return setOnlineStatus_(this, status, extStatus, batteryStatus);
@@ -504,7 +522,33 @@ export class Bridge {
async getCookiesStr(domain: string): Promise<string> { return getCookiesStr_(this, domain); }
async getCsrfToken(): Promise<number> { return getCsrfToken_(this); }
async getCredentials(domain: string) { return getCredentials_(this, domain); }
async getProfileLike(userId?: number, start?: number, limit?: number) {
return getProfileLike_(this, userId, start, limit);
}
async getUnidirectionalFriendList() {
return getUnidirectionalFriendList_(this);
}
async setSelfLongNick(longNick: string) {
return setSelfLongNick_(this, longNick);
}
async setInputStatus(userId: number, eventType: number) {
return setInputStatus_(this, userId, eventType);
}
async translateEn2Zh(words: string[]) {
return translateEn2Zh_(this, words);
}
async getMiniAppArk(type: string, title: string, desc: string, picUrl: string, jumpUrl: string) {
return getMiniAppArk_(this, type, title, desc, picUrl, jumpUrl);
}
async clickInlineKeyboardButton(groupId: number, botAppid: number, buttonId: string, callbackData: string, msgSeq: number) {
return clickInlineKeyboardButton_(this, groupId, botAppid, buttonId, callbackData, msgSeq);
}
async sendGroupSign(groupId: number) {
return sendGroupSign_(this, groupId);
}
async setAvatar(source: string): Promise<void> {
return setAvatar_(this, source);
}
}
// --- Module-level helper functions ---
+157 -1
View File
@@ -862,4 +862,160 @@ export const OidbSetProfileSchema = {
uin: { field: 1, type: 'uint64' as const },
stringProfiles: { field: 2, type: 'repeated_message' as const, schema: OidbProfileStringItemSchema },
intProfiles: { field: 3, type: 'repeated_message' as const, schema: OidbProfileIntItemSchema },
} satisfies ProtoSchema;
} satisfies ProtoSchema;
export const Oidb0x7edInteractionSchema = {
totalCount: { field: 1, type: 'uint32' as const },
newCount: { field: 2, type: 'uint32' as const },
todayCount: { field: 3, type: 'uint32' as const },
lastTime: { field: 4, type: 'uint64' as const },
// userInfos: { field: 7, type: 'repeated_message' ... } // 我没有这个字段,期待未来补全
} satisfies ProtoSchema;
export const Oidb0x7edUserLikeInfoSchema = {
uid: { field: 1, type: 'string' as const },
time: { field: 2, type: 'uint64' as const },
favoriteInfo: { field: 3, type: 'message' as const, schema: Oidb0x7edInteractionSchema },
voteInfo: { field: 4, type: 'message' as const, schema: Oidb0x7edInteractionSchema },
} satisfies ProtoSchema;
export const Oidb0x7edReqSchema = {
targetUid: { field: 1, type: 'string' as const },
basic: { field: 2, type: 'uint32' as const },
vote: { field: 3, type: 'uint32' as const },
favorite: { field: 4, type: 'uint32' as const },
start: { field: 12, type: 'uint32' as const },
limit: { field: 103, type: 'uint32' as const }, // 0xB8 0x06
} satisfies ProtoSchema;
export const Oidb0x7edRespSchema = {
userLikeInfos: { field: 1, type: 'repeated_message' as const, schema: Oidb0x7edUserLikeInfoSchema },
} satisfies ProtoSchema;
export const Oidb0x8a7ReqSchema = {
basic1: { field: 1, type: 'uint32' as const },
basic2: { field: 2, type: 'uint32' as const },
basic3: { field: 3, type: 'uint32' as const },
uin: { field: 4, type: 'uint64' as const },
groupId: { field: 5, type: 'uint64' as const },
type: { field: 12, type: 'uint32' as const },
} satisfies ProtoSchema;
export const Oidb0x8a7RespSchema = {
uinRemain: { field: 2, type: 'uint32' as const },
groupRemain: { field: 3, type: 'uint32' as const },
msg: { field: 4, type: 'string' as const },
canAtAll: { field: 6, type: 'bool' as const },
} satisfies ProtoSchema;
export const Oidb0xe17ReqSchema = {
jsonBody: { field: 3, type: 'string' as const },
} satisfies ProtoSchema;
export const Oidb0xe17RespSchema = {
jsonBody: { field: 4, type: 'string' as const },
} satisfies ProtoSchema;
export const Oidb0x112aProfileInfoSchema = {
tag: { field: 1, type: 'uint32' as const },
value: { field: 2, type: 'string' as const },
} satisfies ProtoSchema;
export const Oidb0x112aReqSchema = {
uin: { field: 1, type: 'uint64' as const },
profile: { field: 2, type: 'message' as const, schema: Oidb0x112aProfileInfoSchema },
} satisfies ProtoSchema;
export const Oidb0x112aRespSchema = {} satisfies ProtoSchema;
export const Oidb0xcd4ReqBodySchema = {
uid: { field: 1, type: 'string' as const },
chatType: { field: 2, type: 'uint32' as const }, // 默认为 0
eventType: { field: 3, type: 'uint32' as const }, // 输入状态 1 等
} satisfies ProtoSchema;
export const Oidb0xcd4ReqSchema = {
reqBody: { field: 1, type: 'message' as const, schema: Oidb0xcd4ReqBodySchema },
} satisfies ProtoSchema;
export const Oidb0xcd4RespSchema = {} satisfies ProtoSchema;
export const Oidb0x990TranslateReqSchema = {
srcLang: { field: 1, type: 'string' as const },
dstLang: { field: 2, type: 'string' as const },
words: { field: 3, type: 'repeated_string' as const },
} satisfies ProtoSchema;
export const Oidb0x990ReqSchema = {
translateReq: { field: 2, type: 'message' as const, schema: Oidb0x990TranslateReqSchema },
tag10: { field: 10, type: 'uint32' as const },
tag12: { field: 12, type: 'uint32' as const },
} satisfies ProtoSchema;
export const Oidb0x990TranslateRespSchema = {
errorCode: { field: 1, type: 'uint32' as const },
errorMsg: { field: 2, type: 'string' as const },
srcLang: { field: 3, type: 'string' as const },
dstLang: { field: 4, type: 'string' as const },
srcWords: { field: 5, type: 'repeated_string' as const },
dstWords: { field: 6, type: 'repeated_string' as const },
} satisfies ProtoSchema;
export const Oidb0x990RespSchema = {
translateResp: { field: 2, type: 'message' as const, schema: Oidb0x990TranslateRespSchema }
} satisfies ProtoSchema;
export const MiniAppShareReqBodySchema = {
appid: { field: 2, type: 'string' as const },
title: { field: 3, type: 'string' as const },
desc: { field: 4, type: 'string' as const },
picUrl: { field: 9, type: 'string' as const },
jumpUrl: { field: 11, type: 'string' as const },
iconUrl: { field: 12, type: 'string' as const },
} satisfies ProtoSchema;
export const MiniAppShareReqSchema = {
sdkVersion: { field: 2, type: 'string' as const },
body: { field: 4, type: 'message' as const, schema: MiniAppShareReqBodySchema },
} satisfies ProtoSchema;
export const MiniAppShareRespBodySchema = {
jsonStr: { field: 2, type: 'string' as const },
} satisfies ProtoSchema;
export const MiniAppShareRespSchema = {
status: { field: 2, type: 'uint32' as const },
msg: { field: 3, type: 'string' as const },
body: { field: 4, type: 'message' as const, schema: MiniAppShareRespBodySchema },
} satisfies ProtoSchema;
export const Oidb0x112eReqSchema = {
botAppid: { field: 3, type: 'uint64' as const },
msgSeq: { field: 4, type: 'uint64' as const },
buttonId: { field: 5, type: 'string' as const },
callbackData: { field: 6, type: 'string' as const },
unknown7: { field: 7, type: 'uint32' as const },
groupId: { field: 8, type: 'uint64' as const },
unknown9: { field: 9, type: 'uint32' as const },
} satisfies ProtoSchema;
export const Oidb0x112eRespSchema = {
result: { field: 3, type: 'uint32' as const },
promptText: { field: 4, type: 'string' as const },
errMsg: { field: 5, type: 'string' as const },
} satisfies ProtoSchema;
export const Oidb0xeb7SignInInfoSchema = {
uin: { field: 1, type: 'string' as const },
groupId: { field: 2, type: 'string' as const },
version: { field: 3, type: 'string' as const },
} satisfies ProtoSchema;
export const Oidb0xeb7ReqSchema = {
signInInfo: { field: 2, type: 'message' as const, schema: Oidb0xeb7SignInInfoSchema },
} satisfies ProtoSchema;
export const Oidb0xeb7RespSchema = {} satisfies ProtoSchema;
+24 -8
View File
@@ -12,7 +12,7 @@ import {
import { RequestUtil } from './web/request-util';
import { getHonorListWebAPI, WebHonorType } from './web/group-honor';
import { getGroupEssenceMsg, getGroupEssenceMsgAll } from './web/group-essence';
import { setGroupNoticeWebAPI, getGroupNoticeWebAPI } from './web/group-notice';
import { setGroupNoticeWebAPI, getGroupNoticeWebAPI, uploadGroupNoticeImage, deleteGroupNotice } from './web/group-notice';
export async function forceFetchClientKey(bridge: Bridge) {
const resp = await sendOidbAndDecode<any>(
@@ -189,15 +189,25 @@ export async function sendGroupNotice(
let imgWidth = 540;
let imgHeight = 300;
if (options?.image) {
let imageBuffer: Buffer;
// 暂时不知道怎么实现,后面有空再看看
if (options.image.startsWith('http://') || options.image.startsWith('https://')) {
const response = await fetch(options.image);
if (!response.ok) throw new Error(`Failed to download image: ${response.status}`);
imageBuffer = Buffer.from(await response.arrayBuffer());
} else {
const { readFileSync } = await import('fs');
imageBuffer = readFileSync(options.image);
}
// if (options?.image) {
// const picInfo = await bridge.uploadGroupBulletinPic(groupCode, options.image);
// picId = picInfo.id;
// imgWidth = picInfo.width;
// imgHeight = picInfo.height;
// }
const picInfo = await uploadGroupNoticeImage(cookieObject, imageBuffer);
if (picInfo) {
picId = picInfo.id;
imgWidth = picInfo.width;
imgHeight = picInfo.height;
}
}
const ret = await setGroupNoticeWebAPI(
cookieObject,
@@ -261,6 +271,12 @@ export async function getGroupNotice(bridge: Bridge, groupId: number) {
return retNotices;
}
export async function deleteGroupNoticeByFid(bridge: Bridge, groupId: number, fid: string): Promise<boolean> {
const groupCode = groupId.toString();
const cookieObject = await getCookies(bridge, 'qun.qq.com');
return await deleteGroupNotice(cookieObject, groupCode, fid);
}
/**
* 提取的公共算法:根据 skey/p_skey 计算 bkn (也就是 token / csrf_token)
*/
+188 -34
View File
@@ -1,4 +1,7 @@
import { RequestUtil, cookieToString, getBknFromCookie } from './request-util';
import { RequestUtil, cookieToString } from './request-util';
import https from 'node:https';
// import { readFileSync } from 'node:fs';
// import * as console from "node:console";
export interface SetNoticeRetSuccess {
ec?: number;
@@ -6,6 +9,12 @@ export interface SetNoticeRetSuccess {
[key: string]: any;
}
export interface UploadImageRetSuccess {
ec?: number;
id?: string;
[key: string]: any;
}
export interface WebApiGroupNoticeFeed {
fid: string;
u: number;
@@ -26,6 +35,14 @@ export interface WebApiGroupNoticeRet {
[key: string]: any;
}
export function calculateBkn(key: string): string {
let hash = 5381;
for (let i = 0; i < key.length; i++) {
const code = key.charCodeAt(i);
hash = hash + (hash << 5) + code;
}
return (hash & 0x7FFFFFFF).toString();
}
/**
* 发送群公告 Web API
@@ -44,33 +61,54 @@ export async function setGroupNoticeWebAPI(
imgHeight: number = 300
): Promise<SetNoticeRetSuccess | undefined> {
try {
// 分别获取 skey 和 p_skey
const skey = cookieObject['skey'] || '';
const pskey = cookieObject['p_skey'] || skey;
const bodyBkn = calculateBkn(skey);
const urlBkn = calculateBkn(pskey);
//
// console.log(skey)
// console.log(pskey)
//
// console.log(urlBkn);
// console.log(bodyBkn)
const settings = JSON.stringify({
is_show_edit_card: isShowEditCard,
tip_window_type: tipWindowType,
confirm_required: confirmRequired,
});
const externalParam = {
pic: picId,
imgWidth: imgWidth.toString(),
imgHeight: imgHeight.toString(),
};
const url = `https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
bkn: getBknFromCookie(cookieObject),
const bodyParams: Record<string, string> = {
qid: groupCode,
bkn: bodyBkn,
text: content,
pinned: pinned.toString(),
type: type.toString(),
settings,
...(picId === '' ? {} : externalParam),
}).toString()}`;
};
if (picId !== '') {
bodyParams.pic = picId;
bodyParams.imgWidth = imgWidth.toString();
bodyParams.imgHeight = imgHeight.toString();
}
// 注意这里:URL 必须使用 skey 算出来的 bkn
const url = `https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${urlBkn}`;
const body = new URLSearchParams(bodyParams).toString();
const ret = await RequestUtil.HttpGetJson<SetNoticeRetSuccess>(
url,
'POST', // 注意这里必须是 POST
'',
{ Cookie: cookieToString(cookieObject) }
'POST',
body,
{
Cookie: cookieToString(cookieObject),
'Content-Type': 'application/x-www-form-urlencoded'
},
true,
false
);
return ret;
} catch (e) {
@@ -78,35 +116,151 @@ export async function setGroupNoticeWebAPI(
}
}
/**
* 获取群公告列表 Web API
*/
export async function getGroupNoticeWebAPI(
cookieObject: Record<string, string>,
groupCode: string
groupCode: string,
start: number = -1, // -1 表示第一页
count: number = 20 // 抓包中是 10,你可以根据需要调整
): Promise<WebApiGroupNoticeRet | undefined> {
const bkn = getBknFromCookie(cookieObject);
const params = new URLSearchParams({
bkn: bkn,
qid: groupCode,
ft: '23',
ni: '1',
i: '1',
log_read: '1',
platform: '1',
s: '-1',
}).toString();
const url = `https://web.qun.qq.com/cgi-bin/announce/get_t_list?${params}&n=20`;
try {
const skey = cookieObject['skey'] || '';
const pskey = cookieObject['p_skey'] || skey;
const bodyBkn = calculateBkn(skey); // URL 使用 skey 算出的 bkn
const urlBkn = calculateBkn(pskey); // Body 使用 p_skey 算出的 bkn
const bodyParams = new URLSearchParams({
qid: groupCode,
bkn: bodyBkn, // Body 中传入 p_skey 计算结果
ft: '23',
s: start.toString(), // 分页游标
n: count.toString(), // 获取数量
i: '1',
ni: '1'
}).toString();
const url = `https://web.qun.qq.com/cgi-bin/announce/list_announce?bkn=${urlBkn}`;
const ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(
url,
'GET',
'',
{ Cookie: cookieToString(cookieObject) }
'POST',
bodyParams,
{
Cookie: cookieToString(cookieObject),
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': 'https://web.qun.qq.com/mannounce/index.html?_wv=1031&_bid=148'
},
true,
false
);
// console.log(JSON.stringify(ret, null, 2));
return ret?.ec === 0 ? ret : undefined;
} catch {
return undefined;
}
}
/**
* 上传群公告图片 Web API
*/
export async function uploadGroupNoticeImage(
cookieObject: Record<string, string>,
imageBuffer: Buffer
): Promise<{ id: string; width: number; height: number } | undefined> {
try {
const bkn = calculateBkn(cookieObject['skey']);
const boundary = `-----------------------------${Date.now()}`;
const parts: Buffer[] = [];
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="bkn"\r\n\r\n${bkn}\r\n`));
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="source"\r\n\r\ntroopNotice\r\n`));
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="m"\r\n\r\n0\r\n`));
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="pic_up"; filename="image.jpg"\r\nContent-Type: image/jpeg\r\n\r\n`));
parts.push(imageBuffer);
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
const body = Buffer.concat(parts);
const url = 'https://web.qun.qq.com/cgi-bin/announce/upload_img';
const options = {
hostname: 'web.qun.qq.com',
path: '/cgi-bin/announce/upload_img',
method: 'POST',
headers: {
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Content-Length': body.length,
'Cookie': cookieToString(cookieObject),
},
};
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
try {
const result = JSON.parse(data) as UploadImageRetSuccess;
if (result.ec === 0 && result.id) {
const unescapedIdStr = result.id.replace(/&quot;/g, '"');
const idObj = JSON.parse(unescapedIdStr);
resolve({ id: idObj.id, width: parseInt(idObj.w), height: parseInt(idObj.h) });
} else {
resolve(undefined);
}
} catch {
resolve(undefined);
}
});
});
req.on('error', () => resolve(undefined));
req.write(body);
req.end();
});
} catch {
return undefined;
}
}
/**
* 删除群公告 Web API
*/
export async function deleteGroupNotice(
cookieObject: Record<string, string>,
groupCode: string,
fid: string
): Promise<boolean> {
try {
const skey = cookieObject['skey'] || '';
const pskey = cookieObject['p_skey'] || skey;
const bodyBkn = calculateBkn(skey); // Body 使用 skey 算出的 bkn
const urlBkn = calculateBkn(pskey); // URL 使用 p_skey 算出的 bkn
const params = new URLSearchParams({
bkn: bodyBkn, // 注意这里:POST Body 放入 bodyBkn
fid: fid,
qid: groupCode,
}).toString();
// 注意这里:URL 拼接 urlBkn
const url = `https://web.qun.qq.com/cgi-bin/announce/del_feed?bkn=${urlBkn}`;
const ret = await RequestUtil.HttpGetJson<SetNoticeRetSuccess>(
url,
'POST',
params,
{
Cookie: cookieToString(cookieObject),
'Content-Type': 'application/x-www-form-urlencoded'
},
true,
false
);
return ret?.ec === 0;
} catch {
return false;
}
}
+210 -29
View File
@@ -239,16 +239,13 @@ export function register(h: ApiHandler, ctx: ApiActionContext): void {
return failedResponse(RETCODE.BAD_REQUEST, 'group_id and content are required');
}
if (image) {
return failedResponse(RETCODE.ACTION_FAILED, 'not implemented: image upload for group notice is not supported yet');
}
if (!ctx.sendGroupNotice) {
return failedResponse(RETCODE.ACTION_FAILED, 'not implemented');
}
try {
const options = {
image: image || undefined,
pinned: params.pinned !== undefined ? Number(params.pinned) : 0,
type: params.type !== undefined ? Number(params.type) : 1,
confirm_required: params.confirm_required !== undefined ? Number(params.confirm_required) : 1,
@@ -277,8 +274,28 @@ export function register(h: ApiHandler, ctx: ApiActionContext): void {
}
});
h.registerAction('_del_group_notice', async () => {
return failedResponse(RETCODE.ACTION_FAILED, 'not yet implemented');
h.registerAction('_del_group_notice', async (params) => {
const groupId = asNumber(params.group_id);
const fid = asString(params.fid) || asString(params.notice_id);
if (!groupId || !fid) {
return failedResponse(RETCODE.BAD_REQUEST, 'group_id and fid/notice_id are required');
}
if (!ctx.deleteGroupNotice) {
return failedResponse(RETCODE.ACTION_FAILED, 'not implemented');
}
try {
const success = await ctx.deleteGroupNotice(groupId, fid);
if (success) {
return okResponse();
} else {
return failedResponse(RETCODE.ACTION_FAILED, 'delete failed');
}
} catch (e) {
return failedResponse(RETCODE.ACTION_FAILED, String(e));
}
});
// --- Forward messages ---
@@ -605,12 +622,26 @@ export function register(h: ApiHandler, ctx: ApiActionContext): void {
return failedResponse(RETCODE.ACTION_FAILED, 'not yet implemented');
});
// todo 我的建议是引入数据库api 纯协议我不知道这种api怎么实现,ntQQ在实现这个方法的时候只进行了数据库查询,完全没碰网络
h.registerAction('get_recent_contact', async () => {
return okResponse([]);
});
h.registerAction('get_profile_like', async () => {
return okResponse({});
h.registerAction('get_profile_like', async (params) => {
const userId = asNumber(params.user_id);
const start = asNumber(params.start) || 0;
const count = asNumber(params.count) || 10;
if (!ctx.getProfileLike) {
return failedResponse(RETCODE.ACTION_FAILED, 'not implemented');
}
try {
const data = await ctx.getProfileLike(userId, start, count);
return okResponse(data);
} catch (e) {
return failedResponse(RETCODE.ACTION_FAILED, String(e));
}
});
h.registerAction('get_friends_with_category', async () => {
@@ -622,6 +653,7 @@ export function register(h: ApiHandler, ctx: ApiActionContext): void {
// --- Additional NapCat-compatible stubs ---
///napcat 似乎也用不了??,暂时不管了
h.registerAction('get_online_clients', async () => {
return okResponse({ clients: [] });
});
@@ -638,16 +670,55 @@ export function register(h: ApiHandler, ctx: ApiActionContext): void {
return failedResponse(RETCODE.ACTION_FAILED, 'not yet implemented');
});
h.registerAction('get_group_at_all_remain', async () => {
return okResponse({ can_at_all: false, remain_at_all_count_for_group: 0, remain_at_all_count_for_uin: 0 });
h.registerAction('get_group_at_all_remain', async (params) => {
const groupId = asNumber(params.group_id);
if (!groupId) {
return failedResponse(RETCODE.BAD_REQUEST, 'invalid group_id');
}
if (!ctx.getGroupAtAllRemain) {
return failedResponse(RETCODE.ACTION_FAILED, 'not implemented');
}
try {
const data = await ctx.getGroupAtAllRemain(groupId);
return okResponse(data);
} catch (e) {
return failedResponse(RETCODE.ACTION_FAILED, String(e));
}
});
h.registerAction('get_unidirectional_friend_list', async () => {
return okResponse([]);
if (!ctx.getUnidirectionalFriendList) {
return failedResponse(RETCODE.ACTION_FAILED, 'not implemented');
}
try {
const data = await ctx.getUnidirectionalFriendList();
return okResponse(data);
} catch (e) {
return failedResponse(RETCODE.ACTION_FAILED, String(e));
}
});
h.registerAction('set_self_longnick', async () => {
return failedResponse(RETCODE.ACTION_FAILED, 'not yet implemented');
h.registerAction('set_self_longnick', async (params) => {
const longNick = params.longNick || params.long_nick;
if (typeof longNick !== 'string') {
return failedResponse(RETCODE.BAD_REQUEST, 'invalid longNick');
}
if (!ctx.setSelfLongNick) {
return failedResponse(RETCODE.ACTION_FAILED, 'not implemented');
}
try {
await ctx.setSelfLongNick(longNick);
return okResponse({});
} catch (e) {
return failedResponse(RETCODE.ACTION_FAILED, String(e));
}
});
h.registerAction('get_collection_list', async () => {
@@ -658,16 +729,66 @@ export function register(h: ApiHandler, ctx: ApiActionContext): void {
return failedResponse(RETCODE.ACTION_FAILED, 'not yet implemented');
});
h.registerAction('set_qq_avatar', async () => {
return failedResponse(RETCODE.ACTION_FAILED, 'not yet implemented');
h.registerAction('set_qq_avatar', async (params) => {
const file = asString(params.file);
if (!file) return failedResponse(RETCODE.BAD_REQUEST, 'file is required');
if (!ctx.setAvatar) {
return failedResponse(RETCODE.ACTION_FAILED, 'not implemented');
}
try {
await ctx.setAvatar(file);
return okResponse();
} catch (e) {
return failedResponse(RETCODE.ACTION_FAILED, String(e));
}
});
h.registerAction('set_input_status', async () => {
return okResponse();
h.registerAction('set_input_status', async (params) => {
const userId = asNumber(params.user_id);
const eventType = asNumber(params.event_type);
if (!userId) {
return failedResponse(RETCODE.BAD_REQUEST, 'invalid user_id');
}
// event_type 有可能是 0 (取消输入状态),所以这里严格判断 undefined 或 isNaN
if (eventType === undefined || isNaN(eventType)) {
return failedResponse(RETCODE.BAD_REQUEST, 'invalid event_type');
}
if (!ctx.setInputStatus) {
return failedResponse(RETCODE.ACTION_FAILED, 'not implemented');
}
try {
await ctx.setInputStatus(userId, eventType);
return okResponse({});
} catch (e) {
return failedResponse(RETCODE.ACTION_FAILED, String(e));
}
});
h.registerAction('translate_en2zh', async () => {
return failedResponse(RETCODE.ACTION_FAILED, 'not yet implemented');
h.registerAction('translate_en2zh', async (params) => {
const rawWords = params.words;
if (!Array.isArray(rawWords)) {
return failedResponse(RETCODE.BAD_REQUEST, 'invalid words array');
}
const words = rawWords.map(w => String(w));
if (!ctx.translateEn2Zh) {
return failedResponse(RETCODE.ACTION_FAILED, 'not implemented');
}
try {
const translated = await ctx.translateEn2Zh(words);
return okResponse({ words: translated });
} catch (e) {
return failedResponse(RETCODE.ACTION_FAILED, String(e));
}
});
h.registerAction('get_clientkey', async () => {
@@ -681,21 +802,81 @@ export function register(h: ApiHandler, ctx: ApiActionContext): void {
return okResponse(clientKeyInfo);
});
h.registerAction('get_mini_app_ark', async () => {
return failedResponse(RETCODE.ACTION_FAILED, 'not yet implemented');
h.registerAction('get_mini_app_ark', async (params) => {
const type = params.type || 'bili';
const title = params.title || '';
const desc = params.desc || '';
const picUrl = params.picUrl || params.pic_url || '';
const jumpUrl = params.jumpUrl || params.jump_url || '';
if (!ctx.getMiniAppArk) {
return failedResponse(RETCODE.ACTION_FAILED, 'not implemented');
}
try {
const data = await ctx.getMiniAppArk(
String(type),
String(title),
String(desc),
String(picUrl),
String(jumpUrl)
);
return okResponse(data);
} catch (e) {
return failedResponse(RETCODE.ACTION_FAILED, String(e));
}
});
h.registerAction('click_inline_keyboard_button', async () => {
return failedResponse(RETCODE.ACTION_FAILED, 'not yet implemented');
h.registerAction('click_inline_keyboard_button', async (params) => {
const groupId = asNumber(params.group_id);
const botAppid = asNumber(params.bot_appid);
const buttonId = params.button_id;
const callbackData = params.callback_data || '';
const msgSeq = asNumber(params.msg_seq);
if (!groupId || !botAppid || !buttonId || !msgSeq) {
return failedResponse(RETCODE.BAD_REQUEST, 'missing required parameters');
}
if (!ctx.clickInlineKeyboardButton) {
return failedResponse(RETCODE.ACTION_FAILED, 'not implemented');
}
try {
const data = await ctx.clickInlineKeyboardButton(
groupId,
botAppid,
String(buttonId),
String(callbackData),
msgSeq
);
return okResponse(data);
} catch (e) {
return failedResponse(RETCODE.ACTION_FAILED, String(e));
}
});
h.registerAction('set_group_sign', async () => {
return failedResponse(RETCODE.ACTION_FAILED, 'not yet implemented');
});
const handleGroupSign = async (params: any) => {
const groupId = asNumber(params.group_id);
h.registerAction('send_group_sign', async () => {
return failedResponse(RETCODE.ACTION_FAILED, 'not yet implemented');
});
if (!groupId) {
return failedResponse(RETCODE.BAD_REQUEST, 'invalid group_id');
}
if (!ctx.sendGroupSign) {
return failedResponse(RETCODE.ACTION_FAILED, 'not implemented');
}
try {
await ctx.sendGroupSign(groupId);
return okResponse({});
} catch (e) {
return failedResponse(RETCODE.ACTION_FAILED, String(e));
}
};
h.registerAction('set_group_sign', handleGroupSign);
h.registerAction('send_group_sign', handleGroupSign);
h.registerAction('get_group_info_ex', async (params) => {
const groupId = asNumber(params.group_id);
+12
View File
@@ -55,6 +55,7 @@ export interface ApiActionContext {
setGroupName?: (groupId: number, name: string) => Promise<void>;
setGroupLeave?: (groupId: number) => Promise<void>;
setGroupSpecialTitle?: (groupId: number, userId: number, title: string) => Promise<void>;
getGroupAtAllRemain?: (groupId: number) => Promise<any>;
// Group file
uploadGroupFile?: (groupId: number, file: string, name?: string, folderId?: string, uploadFile?: boolean) => Promise<string | null>;
uploadPrivateFile?: (userId: number, file: string, name?: string, uploadFile?: boolean) => Promise<string | null>;
@@ -78,6 +79,13 @@ export interface ApiActionContext {
forceFetchClientKey? : () => Promise<{clientKey: string, keyIndex: string, expireTime: string}>;
setOnlineStatus?: (status: number, extStatus?: number, batteryStatus?: number) => Promise<void>;
setProfile?: (nickname?: string, personalNote?: string) => Promise<void>;
getUnidirectionalFriendList?: () => Promise<any>;
setSelfLongNick?: (longNick: string) => Promise<void>;
setInputStatus?: (userId: number, eventType: number) => Promise<void>;
translateEn2Zh?: (words: string[]) => Promise<string[]>;
getMiniAppArk?: (type: string, title: string, desc: string, picUrl: string, jumpUrl: string) => Promise<any>;
clickInlineKeyboardButton?: (groupId: number, botAppid: number, buttonId: string, callbackData: string, msgSeq: number) => Promise<any>;
sendGroupSign?: (groupId: number) => Promise<void>;
// New context methods
setGroupReaction?: (groupId: number, sequence: number, code: string, isSet: boolean) => Promise<void>;
handleDeleteFriend?: (userId: number, block?: boolean) => Promise<void>;
@@ -96,18 +104,22 @@ export interface ApiActionContext {
setMsgEmojiLike?: (messageId: number, emojiId: string, set: boolean) => Promise<void>;
markGroupMsgAsRead?: (groupId: number, sequence: number) => Promise<void>;
markPrivateMsgAsRead?: (userId: number, sequence: number) => Promise<void>;
getProfileLike?: (userId?: number, start?: number, limit?: number) => Promise<any>;
// Web
getGroupHonorInfo?: (groupId: number, type: WebHonorType | string) => Promise<any>;
getGroupEssence?: (groupId: number, pageStart?: number, pageLimit?: number) => Promise<GroupEssenceMsgRet>;
getGroupEssenceAll?: (groupId: number) => Promise<GroupEssenceMsgRet[]>;
sendGroupNotice?: (groupId: number, content: string, options?: any) => Promise<any>;
getGroupNotice?: (groupId: number) => Promise<any[]>;
deleteGroupNotice?: (groupId: number, fid: string) => Promise<boolean>;
getCookiesStr?: (domain: string) => Promise<string>;
getCsrfToken?: () => Promise<number>;
getCredentials?: (domain: string) => Promise<{ cookies: string; token: number; csrf_token: number }>;
// Media lookup (populated from previously dispatched message segments)
getImageInfo?: (file: string) => Promise<JsonObject | null>;
getRecordInfo?: (file: string) => Promise<JsonObject | null>;
// Avatar
setAvatar?: (source: string) => Promise<void>;
}
type ActionHandler = (params: JsonObject) => Promise<import('./types').ApiResponse>;
@@ -79,6 +79,7 @@ export function buildApiContext(ref: OneBotInstanceContext): ApiActionContext {
setGroupName: (groupId, name) => bridge.setGroupName(groupId, name),
setGroupLeave: (groupId) => bridge.leaveGroup(groupId),
setGroupSpecialTitle: (groupId, userId, title) => bridge.setGroupSpecialTitle(groupId, userId, title),
getGroupAtAllRemain: (groupId) => bridge.getGroupAtAllRemain(groupId),
// Group file
uploadGroupFile: async (groupId, file, name, folderId, uploadFile) => {
const result = await bridge.uploadGroupFile(groupId, file, name ?? '', folderId ?? '/', uploadFile ?? true);
@@ -105,6 +106,14 @@ export function buildApiContext(ref: OneBotInstanceContext): ApiActionContext {
sendGroupPoke: (groupId, userId) => bridge.sendPoke(true, groupId, userId),
setEssenceMsg: (messageId) => handleSetEssence(bridge, messageStore, messageId, true),
deleteEssenceMsg: (messageId) => handleSetEssence(bridge, messageStore, messageId, false),
getProfileLike: (userId?: number, start: number = 0, limit: number = 10) => bridge.getProfileLike(userId, start, limit),
getUnidirectionalFriendList: () => bridge.getUnidirectionalFriendList(),
setSelfLongNick: (longNick) => bridge.setSelfLongNick(longNick),
setInputStatus: (userId, eventType) => bridge.setInputStatus(userId, eventType),
translateEn2Zh: (words) => bridge.translateEn2Zh(words),
getMiniAppArk: (type, title, desc, picUrl, jumpUrl) => bridge.getMiniAppArk(type, title, desc, picUrl, jumpUrl),
clickInlineKeyboardButton: (groupId, botAppid, buttonId, callbackData, msgSeq) => bridge.clickInlineKeyboardButton(groupId, botAppid, buttonId, callbackData, msgSeq),
sendGroupSign: (groupId) => bridge.sendGroupSign(groupId),
// New extended
setGroupReaction: (groupId, sequence, code, isSet) => bridge.setGroupReaction(groupId, sequence, code, isSet),
handleDeleteFriend: (userId, block) => bridge.deleteFriend(userId, !!block),
@@ -161,12 +170,15 @@ export function buildApiContext(ref: OneBotInstanceContext): ApiActionContext {
getGroupEssenceAll: (groupId: number) => bridge.getGroupEssenceAll(groupId),
sendGroupNotice: (groupId: number, content: string, options?: any) => bridge.sendGroupNotice(groupId, content, options),
getGroupNotice: (groupId: number) => bridge.getGroupNotice(groupId),
deleteGroupNotice: (groupId: number, fid: string) => bridge.deleteGroupNotice(groupId, fid),
getCookiesStr: (domain: string) => bridge.getCookiesStr(domain),
getCsrfToken: () => bridge.getCsrfToken(),
getCredentials: (domain: string) => bridge.getCredentials(domain),
// Media lookup
getImageInfo: (file) => handleGetImageInfo(mediaStore, file),
getRecordInfo: (file) => handleGetRecordInfo(bridge, mediaStore, file),
// Avatar
setAvatar: (source: string) => bridge.setAvatar(source),
};
}