26 包 monorepo(TS 全源码): - snowcat-core: NT 内核 API 封装 + listener(含 9.9.37 六回调适配)+ Packet 发包 - snowcat-onebot: OneBot v11 全动作 + MCP Streamable HTTP 适配器 + 稳定线 - snowcat-webui-frontend/backend: 雪猫控制台(React+HeroUI) - snowcat-shell: 单文件 bundle 出口(vite inlineDynamicImports) - snowcat-native: napi2native / MoeHoo / ffmpeg / pty 运行时 - 56 项 SnowLuma 独门动作移植(qzone 全家桶/群相册/装扮/闪照/收藏) - 工具件: 解包器/启动器/便携制作器/doctor GitHub 关联已全量指向 DandanLLab/SnowCatQQ
1001 lines
38 KiB
TypeScript
1001 lines
38 KiB
TypeScript
import { RequestUtil } from 'snowcat-common/src/request';
|
||
import {
|
||
GroupEssenceMsgRet,
|
||
InstanceContext,
|
||
WebApiGroupMember,
|
||
WebApiGroupMemberRet,
|
||
WebApiGroupNoticeRet,
|
||
WebHonorType, SnowCatCore,
|
||
} from '@/snowcat-core/index';
|
||
|
||
import { createReadStream, readFileSync, statSync } from 'node:fs';
|
||
import { createHash } from 'node:crypto';
|
||
import { basename } from 'node:path';
|
||
import { qunAlbumControl } from '../data/webapi';
|
||
import { createAlbumCommentRequest, createAlbumFeedPublish, createAlbumMediaFeed } from '../data/album';
|
||
import {
|
||
buildQzoneDeleteBody,
|
||
buildQzonePublishBody,
|
||
buildQzoneUploadImageBody,
|
||
parseQzoneDeleteResponse,
|
||
parseQzonePublishResponse,
|
||
parseQzoneUploadImageResponseText,
|
||
toQzoneRichval,
|
||
QzoneDeleteResponse,
|
||
QzonePublishResponse,
|
||
} from '../data/qzone';
|
||
// SnowCat · qzone Web 线扩展(SnowLuma 移植):读列表/详情/点赞/评论/回复/资料/拉黑/权限
|
||
import {
|
||
decodeQzoneFeedCursor,
|
||
encodeQzoneFeedCursor,
|
||
initialQzoneFeedState,
|
||
mapQzoneFeeds,
|
||
nextQzoneFeedState,
|
||
qzoneFeedPosition,
|
||
type QzoneFeedState,
|
||
type RawFeedsResponse,
|
||
} from '../data/qzone-feeds';
|
||
import { parseQzoneCallback } from '../data/qzone-web';
|
||
import {
|
||
buildQzoneBlackBody,
|
||
buildQzoneCommentBody,
|
||
buildQzoneLikeBody,
|
||
buildQzoneMsgDetailQuery,
|
||
buildQzoneProfileQuery,
|
||
buildQzoneProfileUpdateBody,
|
||
buildQzoneReplyBody,
|
||
legacyMsgListRoute,
|
||
mapQzoneMsgDetail,
|
||
mapQzoneMsgList,
|
||
parseQzoneBlackResponse,
|
||
parseQzoneCommentResponse,
|
||
parseQzoneDetailError,
|
||
parseQzoneJson,
|
||
parseQzoneLikeResponse,
|
||
parseQzoneProfileResponse,
|
||
parseQzoneProfileUpdateResponse,
|
||
parseQzonePublishFull,
|
||
parseQzoneUpdateRightResponse,
|
||
userMsgListRoute,
|
||
buildQzoneUpdatePayload,
|
||
type QzoneProfile,
|
||
type QzoneProfileUpdate,
|
||
type QzoneReplyTarget,
|
||
type QzoneUpdateRightResult,
|
||
type RawMsgListResponse,
|
||
type RawMsgDetailResponse,
|
||
type QzoneRoute,
|
||
} from '../data/qzone-web';
|
||
export interface SetNoticeRetSuccess {
|
||
ec: number;
|
||
em: string;
|
||
id: number;
|
||
ltsm: number;
|
||
new_fid: string;
|
||
read_only: number;
|
||
role: number;
|
||
srv_code: number;
|
||
}
|
||
|
||
export class NTQQWebApi {
|
||
context: InstanceContext;
|
||
core: SnowCatCore;
|
||
|
||
constructor (context: InstanceContext, core: SnowCatCore) {
|
||
this.context = context;
|
||
this.core = core;
|
||
}
|
||
|
||
async shareDigest (groupCode: string, msgSeq: string, msgRandom: string, targetGroupCode: string) {
|
||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||
const url = `https://qun.qq.com/cgi-bin/group_digest/share_digest?${new URLSearchParams({
|
||
bkn: this.getBknFromCookie(cookieObject),
|
||
group_code: groupCode,
|
||
msg_seq: msgSeq,
|
||
msg_random: msgRandom,
|
||
target_group_code: targetGroupCode,
|
||
}).toString()}`;
|
||
try {
|
||
return RequestUtil.HttpGetText(url, 'GET', '', { Cookie: this.cookieToString(cookieObject) });
|
||
} catch {
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
async getGroupEssenceMsgAll (GroupCode: string) {
|
||
const ret: GroupEssenceMsgRet[] = [];
|
||
for (let i = 0; i < 20; i++) {
|
||
const data = await this.getGroupEssenceMsg(GroupCode, i, 50);
|
||
if (!data) break;
|
||
ret.push(data);
|
||
if (data.data.is_end) break;
|
||
}
|
||
return ret;
|
||
}
|
||
|
||
async getGroupEssenceMsg (GroupCode: string, page_start: number = 0, page_limit: number = 50) {
|
||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||
const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?${new URLSearchParams({
|
||
bkn: this.getBknFromCookie(cookieObject),
|
||
page_start: page_start.toString(),
|
||
page_limit: page_limit.toString(),
|
||
group_code: GroupCode,
|
||
}).toString()}`;
|
||
try {
|
||
const ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(
|
||
url,
|
||
'GET',
|
||
'',
|
||
{ Cookie: this.cookieToString(cookieObject) }
|
||
);
|
||
return ret.retcode === 0 ? ret : undefined;
|
||
} catch {
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
async getGroupMembers (GroupCode: string): Promise<WebApiGroupMember[]> {
|
||
// logDebug('webapi 获取群成员', GroupCode);
|
||
const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>();
|
||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||
const retList: Promise<WebApiGroupMemberRet>[] = [];
|
||
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
|
||
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
|
||
st: '0',
|
||
end: '40',
|
||
sort: '1',
|
||
gc: GroupCode,
|
||
bkn: this.getBknFromCookie(cookieObject),
|
||
}).toString()}`,
|
||
'POST',
|
||
'',
|
||
{ Cookie: this.cookieToString(cookieObject) }
|
||
);
|
||
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
|
||
return [];
|
||
} else {
|
||
for (const key in fastRet.mems) {
|
||
if (fastRet.mems[key]) {
|
||
memberData.push(fastRet.mems[key]);
|
||
}
|
||
}
|
||
}
|
||
// 初始化获取PageNum
|
||
const PageNum = Math.ceil(fastRet.count / 40);
|
||
// 遍历批量请求
|
||
for (let i = 2; i <= PageNum; i++) {
|
||
const ret = RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
|
||
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
|
||
st: ((i - 1) * 40).toString(),
|
||
end: (i * 40).toString(),
|
||
sort: '1',
|
||
gc: GroupCode,
|
||
bkn: this.getBknFromCookie(cookieObject),
|
||
}).toString()}`,
|
||
'POST',
|
||
'',
|
||
{ Cookie: this.cookieToString(cookieObject) }
|
||
);
|
||
retList.push(ret);
|
||
}
|
||
// 批量等待
|
||
for (let i = 1; i <= PageNum; i++) {
|
||
const ret = await (retList[i]);
|
||
if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) {
|
||
continue;
|
||
}
|
||
for (const key in ret.mems) {
|
||
if (ret.mems[key]) {
|
||
memberData.push(ret.mems[key]);
|
||
}
|
||
}
|
||
}
|
||
return memberData;
|
||
}
|
||
|
||
// public async addGroupDigest(groupCode: string, msgSeq: string) {
|
||
// const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`;
|
||
// const res = await this.request(url);
|
||
// return await res.json();
|
||
// }
|
||
|
||
// public async getGroupDigest(groupCode: string) {
|
||
// const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20`;
|
||
// const res = await this.request(url);
|
||
// return await res.json();
|
||
// }
|
||
|
||
async setGroupNotice (
|
||
GroupCode: string,
|
||
Content: string,
|
||
pinned: number = 0,
|
||
type: number = 1,
|
||
is_show_edit_card: number = 1,
|
||
tip_window_type: number = 1,
|
||
confirm_required: number = 1,
|
||
picId: string = '',
|
||
imgWidth: number = 540,
|
||
imgHeight: number = 300
|
||
) {
|
||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||
|
||
try {
|
||
const settings = JSON.stringify({
|
||
is_show_edit_card,
|
||
tip_window_type,
|
||
confirm_required,
|
||
});
|
||
const externalParam = {
|
||
pic: picId,
|
||
imgWidth: imgWidth.toString(),
|
||
imgHeight: imgHeight.toString(),
|
||
};
|
||
const ret: SetNoticeRetSuccess = await RequestUtil.HttpGetJson<SetNoticeRetSuccess>(
|
||
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
|
||
bkn: this.getBknFromCookie(cookieObject),
|
||
qid: GroupCode,
|
||
text: Content,
|
||
pinned: pinned.toString(),
|
||
type: type.toString(),
|
||
settings,
|
||
...(picId === '' ? {} : externalParam),
|
||
}).toString()}`,
|
||
'POST',
|
||
'',
|
||
{ Cookie: this.cookieToString(cookieObject) }
|
||
);
|
||
return ret;
|
||
} catch {
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
async getGroupNotice (GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> {
|
||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||
try {
|
||
const ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(
|
||
`https://web.qun.qq.com/cgi-bin/announce/get_t_list?${new URLSearchParams({
|
||
bkn: this.getBknFromCookie(cookieObject),
|
||
qid: GroupCode,
|
||
ft: '23',
|
||
ni: '1',
|
||
n: '1',
|
||
i: '1',
|
||
log_read: '1',
|
||
platform: '1',
|
||
s: '-1',
|
||
}).toString()}&n=20`,
|
||
'GET',
|
||
'',
|
||
{ Cookie: this.cookieToString(cookieObject) }
|
||
);
|
||
return ret?.ec === 0 ? ret : undefined;
|
||
} catch {
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
private async getDataInternal (cookieObject: { [key: string]: string; }, groupCode: string, type: number) {
|
||
let resJson;
|
||
try {
|
||
const res = await RequestUtil.HttpGetText(
|
||
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
|
||
gc: groupCode,
|
||
type: type.toString(),
|
||
}).toString()}`,
|
||
'GET',
|
||
'',
|
||
{ Cookie: this.cookieToString(cookieObject) }
|
||
);
|
||
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
|
||
if (match?.[1]) {
|
||
resJson = JSON.parse(match[1].trim());
|
||
}
|
||
return type === 1 ? resJson?.talkativeList : resJson?.actorList;
|
||
} catch (e) {
|
||
this.context.logger.logDebug('获取当前群荣耀失败', e);
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
private async getHonorList (cookieObject: { [key: string]: string; }, groupCode: string, type: number) {
|
||
const data = await this.getDataInternal(cookieObject, groupCode, type);
|
||
if (!data) {
|
||
this.context.logger.logError(`获取类型 ${type} 的荣誉信息失败`);
|
||
return [];
|
||
}
|
||
return data.map((item: {
|
||
uin: string,
|
||
name: string,
|
||
avatar: string,
|
||
desc: string,
|
||
}) => ({
|
||
user_id: item?.uin,
|
||
nickname: item?.name,
|
||
avatar: item?.avatar,
|
||
description: item?.desc,
|
||
}));
|
||
}
|
||
|
||
async getGroupHonorInfo (groupCode: string, getType: WebHonorType) {
|
||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||
const HonorInfo = {
|
||
group_id: Number(groupCode),
|
||
current_talkative: {},
|
||
talkative_list: [],
|
||
performer_list: [],
|
||
legend_list: [],
|
||
emotion_list: [],
|
||
strong_newbie_list: [],
|
||
};
|
||
|
||
if (getType === WebHonorType.TALKATIVE || getType === WebHonorType.ALL) {
|
||
const talkativeList = await this.getHonorList(cookieObject, groupCode, 1);
|
||
if (talkativeList.length > 0) {
|
||
HonorInfo.current_talkative = talkativeList[0];
|
||
HonorInfo.talkative_list = talkativeList;
|
||
}
|
||
}
|
||
|
||
if (getType === WebHonorType.PERFORMER || getType === WebHonorType.ALL) {
|
||
HonorInfo.performer_list = await this.getHonorList(cookieObject, groupCode, 2);
|
||
}
|
||
|
||
if (getType === WebHonorType.LEGEND || getType === WebHonorType.ALL) {
|
||
HonorInfo.legend_list = await this.getHonorList(cookieObject, groupCode, 3);
|
||
}
|
||
|
||
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
|
||
HonorInfo.emotion_list = await this.getHonorList(cookieObject, groupCode, 6);
|
||
}
|
||
|
||
// 冒尖小春笋好像已经被tx扬了 R.I.P.
|
||
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
|
||
HonorInfo.strong_newbie_list = [];
|
||
}
|
||
|
||
return HonorInfo;
|
||
}
|
||
|
||
private cookieToString (cookieObject: { [key: string]: string; }) {
|
||
return Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ');
|
||
}
|
||
|
||
public getBknFromCookie (cookieObject: { [key: string]: string; }) {
|
||
const sKey = cookieObject['skey'] as string;
|
||
|
||
let hash = 5381;
|
||
for (let i = 0; i < sKey.length; i++) {
|
||
const code = sKey.charCodeAt(i);
|
||
hash = hash + (hash << 5) + code;
|
||
}
|
||
return (hash & 0x7FFFFFFF).toString();
|
||
}
|
||
|
||
public getBknFromSKey (sKey: string) {
|
||
let hash = 5381;
|
||
for (let i = 0; i < sKey.length; i++) {
|
||
const code = sKey.charCodeAt(i);
|
||
hash = hash + (hash << 5) + code;
|
||
}
|
||
return (hash & 0x7FFFFFFF).toString();
|
||
}
|
||
|
||
public getBknFromPSKey (psKey: string) {
|
||
return this.getBknFromSKey(psKey);
|
||
}
|
||
|
||
async getAlbumListByNTQQ (gc: string, attach_info: string = '') {
|
||
return await this.context.session.getAlbumService().getAlbumList({
|
||
qun_id: gc,
|
||
attach_info,
|
||
seq: 3331,
|
||
request_time_line: {
|
||
request_invoke_time: '0',
|
||
},
|
||
});
|
||
}
|
||
|
||
async getAlbumList (gc: string) {
|
||
const skey = await this.core.apis.UserApi.getSKey() || '';
|
||
const pskey = (await this.core.apis.UserApi.getPSkey(['qzone.qq.com'])).domainPskeyMap.get('qzone.qq.com') || '';
|
||
const bkn = this.getBknFromSKey(skey);
|
||
const uin = this.core.selfInfo.uin || '10001';
|
||
const cookies = `p_uin=o${this.core.selfInfo.uin}; p_skey=${pskey}; skey=${skey}; uin=o${uin} `;
|
||
const api = 'https://h5.qzone.qq.com/proxy/domain/u.photo.qzone.qq.com/cgi-bin/upp/qun_list_album_v2?';
|
||
const params = new URLSearchParams({
|
||
random: '7570',
|
||
g_tk: bkn,
|
||
format: 'json',
|
||
inCharset: 'utf-8',
|
||
outCharset: 'utf-8',
|
||
qua: 'V1_IPH_SQ_6.2.0_0_HDBM_T',
|
||
cmd: 'qunGetAlbumList',
|
||
qunId: gc,
|
||
qunid: gc,
|
||
start: '0',
|
||
num: '1000',
|
||
uin,
|
||
getMemberRole: '0',
|
||
});
|
||
const response = await RequestUtil.HttpGetJson<{ data: { album: Array<{ id: string, title: string; }>; }; }>(api + params.toString(), 'GET', '', {
|
||
Cookie: cookies,
|
||
});
|
||
return response.data.album;
|
||
}
|
||
|
||
async createQunAlbumSession (gc: string, sAlbumID: string, sAlbumName: string, path: string, skey: string, pskey: string, img_md5: string, uin: string) {
|
||
const img = readFileSync(path);
|
||
const img_size = img.length;
|
||
const img_name = basename(path);
|
||
const GTK = this.getBknFromSKey(skey);
|
||
const cookie = `p_uin=o${uin}; p_skey=${pskey}; skey=${skey}; uin=o${uin}`;
|
||
const body = qunAlbumControl({
|
||
uin,
|
||
group_id: gc,
|
||
pskey,
|
||
pic_md5: img_md5,
|
||
img_size,
|
||
img_name,
|
||
sAlbumName,
|
||
sAlbumID,
|
||
});
|
||
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileBatchControl/${img_md5}?g_tk=${GTK}`;
|
||
const post = await RequestUtil.HttpGetJson<{ data: { session: string; }, ret: number, msg: string; }>(api, 'POST', body, {
|
||
Cookie: cookie,
|
||
'Content-Type': 'application/json',
|
||
});
|
||
return post;
|
||
}
|
||
|
||
async uploadQunAlbumSlice (path: string, session: string, skey: string, pskey: string, uin: string, slice_size: number) {
|
||
const img_size = statSync(path).size;
|
||
let seq = 0;
|
||
let offset = 0;
|
||
const GTK = this.getBknFromSKey(skey);
|
||
const cookie = `p_uin=o${uin}; p_skey=${pskey}; skey=${skey}; uin=o${uin}`;
|
||
|
||
const stream = createReadStream(path, { highWaterMark: slice_size });
|
||
|
||
for await (const chunk of stream) {
|
||
const end = Math.min(offset + chunk.length, img_size);
|
||
const form = new FormData();
|
||
form.append('uin', uin);
|
||
form.append('appid', 'qun');
|
||
form.append('session', session);
|
||
form.append('offset', offset.toString());
|
||
form.append('data', new Blob([chunk], { type: 'application/octet-stream' }), 'blob');
|
||
form.append('checksum', '');
|
||
form.append('check_type', '0');
|
||
form.append('retry', '0');
|
||
form.append('seq', seq.toString());
|
||
form.append('end', end.toString());
|
||
form.append('cmd', 'FileUpload');
|
||
form.append('slice_size', slice_size.toString());
|
||
form.append('biz_req.iUploadType', '0');
|
||
|
||
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileUpload?seq=${seq}&retry=0&offset=${offset}&end=${end}&total=${img_size}&type=form&g_tk=${GTK}`;
|
||
const response = await fetch(api, {
|
||
method: 'POST',
|
||
headers: {
|
||
Cookie: cookie,
|
||
},
|
||
body: form,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const post = await response.json() as { ret: number, msg: string; }; if (post.ret !== 0) {
|
||
throw new Error(`分片 ${seq} 上传失败: ${post.msg}`);
|
||
}
|
||
offset += chunk.length;
|
||
seq++;
|
||
}
|
||
|
||
return { success: true, message: '上传完成' };
|
||
}
|
||
|
||
async uploadImageToQunAlbum (gc: string, sAlbumID: string, sAlbumName: string, path: string) {
|
||
const skey = await this.core.apis.UserApi.getSKey() || '';
|
||
const pskey = (await this.core.apis.UserApi.getPSkey(['qzone.qq.com'])).domainPskeyMap.get('qzone.qq.com') || '';
|
||
const img_md5 = createHash('md5').update(readFileSync(path)).digest('hex');
|
||
const uin = this.core.selfInfo.uin || '10001';
|
||
const session = (await this.createQunAlbumSession(gc, sAlbumID, sAlbumName, path, skey, pskey, img_md5, uin)).data.session;
|
||
if (!session) throw new Error('创建群相册会话失败');
|
||
await this.uploadQunAlbumSlice(path, session, skey, pskey, uin, 16384);
|
||
}
|
||
|
||
async getAlbumMediaListByNTQQ (gc: string, albumId: string, attach_info: string = '') {
|
||
return (await this.context.session.getAlbumService().getMediaList({
|
||
qun_id: gc,
|
||
attach_info,
|
||
seq: 0,
|
||
request_time_line: {
|
||
request_invoke_time: '0',
|
||
},
|
||
album_id: albumId,
|
||
lloc: '',
|
||
batch_id: '',
|
||
})).response;
|
||
}
|
||
|
||
async doAlbumMediaPlainCommentByNTQQ (
|
||
qunId: string,
|
||
albumId: string,
|
||
lloc: string,
|
||
content: string) {
|
||
const random_seq = Math.floor(Math.random() * 9000) + 1000;
|
||
const uin = this.core.selfInfo.uin || '10001';
|
||
// 16位number数字
|
||
const client_key = Date.now() * 1000;
|
||
return await this.context.session.getAlbumService().doQunComment(
|
||
random_seq, {
|
||
map_info: [],
|
||
map_bytes_info: [],
|
||
map_user_account: [],
|
||
},
|
||
qunId,
|
||
2,
|
||
createAlbumMediaFeed(uin, albumId, lloc),
|
||
createAlbumCommentRequest(uin, content, client_key)
|
||
);
|
||
}
|
||
|
||
async deleteAlbumMediaByNTQQ (
|
||
qunId: string,
|
||
albumId: string,
|
||
lloc: string) {
|
||
const random_seq = Math.floor(Math.random() * 9000) + 1000;
|
||
return await this.context.session.getAlbumService().deleteMedias(
|
||
random_seq,
|
||
qunId,
|
||
albumId,
|
||
[lloc],
|
||
[]
|
||
);
|
||
}
|
||
|
||
async doAlbumMediaLikeByNTQQ (
|
||
qunId: string,
|
||
albumId: string,
|
||
batchId: string,
|
||
lloc: string | undefined,
|
||
isLike: boolean
|
||
) {
|
||
const random_seq = Math.floor(Math.random() * 9000) + 1000;
|
||
const uin = this.core.selfInfo.uin || '10001';
|
||
|
||
const type = isLike ? 2 : 1;
|
||
const status = isLike ? 0 : 1;
|
||
|
||
let id = '';
|
||
if (lloc) {
|
||
id = `421_1_0_${qunId}|${albumId}|${batchId}^||^421_1_0_${qunId}|${albumId}|${lloc}^||^0`;
|
||
} else {
|
||
id = `421_1_0_${qunId}|${albumId}|${batchId}`;
|
||
}
|
||
|
||
return await this.context.session.getAlbumService().doQunLike(
|
||
random_seq,
|
||
{
|
||
map_info: [],
|
||
map_bytes_info: [],
|
||
map_user_account: [],
|
||
},
|
||
type,
|
||
{
|
||
id,
|
||
status,
|
||
},
|
||
createAlbumFeedPublish(qunId, uin, albumId, batchId)
|
||
);
|
||
}
|
||
|
||
private async getQzoneAuth () {
|
||
const skey = await this.core.apis.UserApi.getSKey() || '';
|
||
const pskey = (await this.core.apis.UserApi.getPSkey(['qzone.qq.com'])).domainPskeyMap.get('qzone.qq.com') || '';
|
||
const uin = this.core.selfInfo.uin || '10001';
|
||
const g_tk = this.getBknFromSKey(skey);
|
||
const cookie = `p_uin=o${uin}; p_skey=${pskey}; skey=${skey}; uin=o${uin}`;
|
||
return { skey, pskey, uin, g_tk, cookie };
|
||
}
|
||
|
||
/**
|
||
* 上传图片到 QQ 空间相册, 返回可用于发说说的 richval 字符串
|
||
* @param base64 图片Base64编码内容(不带data:前缀)
|
||
*/
|
||
async uploadImageToQzone (base64: string): Promise<string> {
|
||
const { skey, pskey, uin, g_tk, cookie } = await this.getQzoneAuth();
|
||
const body = buildQzoneUploadImageBody({ uin, skey, pskey, g_tk, base64 });
|
||
const api = `https://up.qzone.qq.com/cgi-bin/upload/cgi_upload_image?g_tk=${g_tk}`;
|
||
const resultText = await RequestUtil.HttpGetText(api, 'POST', body, {
|
||
Cookie: cookie,
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
});
|
||
const parsed = parseQzoneUploadImageResponseText(resultText);
|
||
if (parsed.code !== undefined && parsed.code !== 0) {
|
||
throw new Error(parsed.msg || `QQ空间图片上传失败, code=${parsed.code}`);
|
||
}
|
||
if (!parsed.data) {
|
||
throw new Error('QQ空间图片上传失败, 响应缺少data字段');
|
||
}
|
||
return toQzoneRichval(parsed.data);
|
||
}
|
||
|
||
/**
|
||
* 发表QQ空间说说
|
||
* @param content 说说正文
|
||
* @param richvals 每张图片对应的richval数组, 为空表示纯文字说说
|
||
* @param ugcRight 查看权限 1所有人可见 4好友可见 16部分好友可见 64仅自己可见 128部分好友不可见
|
||
* @param targetUins 权限作用QQ号数组, ugcRight为16/128时使用
|
||
*/
|
||
async publishQzoneMsg (content: string, richvals: string[], ugcRight: number, targetUins?: number[]) {
|
||
const { uin, g_tk, cookie } = await this.getQzoneAuth();
|
||
const richval = richvals.length > 0 ? richvals.join('\t') : undefined;
|
||
const allowUins = targetUins && targetUins.length > 0 ? targetUins.join('|') : undefined;
|
||
const body = buildQzonePublishBody({
|
||
hostuin: uin,
|
||
content,
|
||
richval,
|
||
ugcRight,
|
||
allowUins,
|
||
});
|
||
const api = `https://user.qzone.qq.com/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_publish_v6?g_tk=${g_tk}`;
|
||
const result = await RequestUtil.HttpGetJson<QzonePublishResponse>(api, 'POST', body, {
|
||
Cookie: cookie,
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
}, true, false);
|
||
return parseQzonePublishResponse(result);
|
||
}
|
||
|
||
/**
|
||
* 删除QQ空间说说
|
||
* @param tid 说说Tid, 来自 publishQzoneMsg 的返回值
|
||
*/
|
||
async deleteQzoneMsg (tid: string) {
|
||
const { uin, g_tk, cookie } = await this.getQzoneAuth();
|
||
const body = buildQzoneDeleteBody({ hostuin: uin, tid });
|
||
const api = `https://user.qzone.qq.com/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_delete_v6?g_tk=${g_tk}`;
|
||
const result = await RequestUtil.HttpGetJson<QzoneDeleteResponse>(api, 'POST', body, {
|
||
Cookie: cookie,
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
}, true, false);
|
||
return parseQzoneDeleteResponse(result);
|
||
}
|
||
|
||
async getDaySignedList (groupCode: string) {
|
||
const pSkey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
|
||
const selfUin = this.core.selfInfo.uin;
|
||
const cookie = `p_uin=o${selfUin}; p_skey=${pSkey}; uin=o${selfUin}`;
|
||
const post = await RequestUtil.HttpGetJson<{
|
||
retCode: number,
|
||
costTime: number,
|
||
response: {
|
||
ret?: {
|
||
code: string,
|
||
msg: string,
|
||
},
|
||
page?: {
|
||
infos?: {
|
||
uid: string,
|
||
uidGroupNick: string,
|
||
signedTimeStamp: string,
|
||
signInRank: number,
|
||
}[],
|
||
offset: number,
|
||
total: number,
|
||
}[],
|
||
},
|
||
funcCode: number,
|
||
}>(`https://qun.qq.com/v2/signin/trpc/GetDaySignedList?g_tk=${this.getBknFromPSKey(pSkey)}`, 'POST', {
|
||
dayYmd: new Date().toISOString().slice(0, 10).replace(/-/g, ''),
|
||
offset: 0,
|
||
limit: 100,
|
||
uid: selfUin,
|
||
groupId: groupCode,
|
||
}, {
|
||
Cookie: cookie,
|
||
'Content-Type': 'application/json',
|
||
});
|
||
return post;
|
||
}
|
||
|
||
// ───────────── SnowCat · qzone Web 线扩展(SnowLuma 移植) ─────────────
|
||
|
||
/** 获取说说列表(默认自己空间;-10000 限频自动切 user.qzone 备路) */
|
||
async getQzoneMsgList (targetUin?: string, pos = 0, num = 20) {
|
||
const { uin, g_tk, cookie } = await this.getQzoneAuth();
|
||
const host = targetUin ?? uin;
|
||
let route: QzoneRoute = legacyMsgListRoute(cookie, host, pos, num, g_tk);
|
||
let text = await RequestUtil.HttpGetText(route.url, 'GET', '', route.headers);
|
||
let data = parseQzoneJson<RawMsgListResponse>(text);
|
||
// -10000「使用人数过多」是 legacy 网关限频,切备路重试一次
|
||
if (data.code === -10000) {
|
||
route = userMsgListRoute(cookie, host, pos, num, g_tk);
|
||
text = await RequestUtil.HttpGetText(route.url, 'GET', '', route.headers);
|
||
data = parseQzoneJson<RawMsgListResponse>(text);
|
||
}
|
||
if (typeof data.code === 'number' && data.code !== 0) {
|
||
throw new Error(`qzone msglist failed: code=${data.code} ${data.message ?? ''}`.trim());
|
||
}
|
||
return mapQzoneMsgList(data);
|
||
}
|
||
|
||
/** 获取单条说说详情 + 一页评论(含内联回复) */
|
||
async getQzoneMsgDetail (targetUin: string, tid: string, pos = 0, num = 20) {
|
||
const { g_tk, cookie } = await this.getQzoneAuth();
|
||
const query = buildQzoneMsgDetailQuery(targetUin, tid, g_tk, { pos, num });
|
||
const url = `https://h5.qzone.qq.com/proxy/domain/taotao.qq.com/cgi-bin/emotion_cgi_msgdetail_v6?${query}`;
|
||
const text = await RequestUtil.HttpGetText(url, 'GET', '', { Cookie: cookie });
|
||
const data = parseQzoneJson<RawMsgDetailResponse>(text);
|
||
parseQzoneDetailError(data, tid);
|
||
return mapQzoneMsgDetail(data, tid);
|
||
}
|
||
|
||
/** 点赞/取消赞一条说说(abstime 传目标帖的发帖时间更稳,0 可容忍) */
|
||
async setQzoneLike (targetUin: string, tid: string, like: boolean, abstime = 0) {
|
||
const { uin, g_tk, cookie } = await this.getQzoneAuth();
|
||
const verb = like ? 'like' : 'unlike';
|
||
const cgi = like ? 'internal_dolike_app' : 'internal_unlike_app';
|
||
const url = `https://h5.qzone.qq.com/proxy/domain/w.qzone.qq.com/cgi-bin/likes/${cgi}?g_tk=${g_tk}`;
|
||
const body = buildQzoneLikeBody(uin, targetUin, tid, abstime);
|
||
const text = await RequestUtil.HttpGetText(url, 'POST', body, {
|
||
Cookie: cookie,
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
});
|
||
parseQzoneLikeResponse(text, verb);
|
||
}
|
||
|
||
/** 评论一条说说(richval 传图片直链,与发说说的 richval 串不同) */
|
||
async commentQzoneMsg (hostUin: string, tid: string, content: string, richval?: string) {
|
||
const { uin, g_tk, cookie } = await this.getQzoneAuth();
|
||
const url = `https://h5.qzone.qq.com/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_re_feeds?g_tk=${g_tk}`;
|
||
const body = buildQzoneCommentBody(uin, hostUin, tid, content, richval ? 1 : undefined, richval);
|
||
const text = await RequestUtil.HttpGetText(url, 'POST', body, {
|
||
Cookie: cookie,
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
});
|
||
return parseQzoneCommentResponse(text, hostUin, tid);
|
||
}
|
||
|
||
/** 楼中楼回复(独立端点 emotion_cgi_addreply_ugc;replyTo 自动转 @ 提及) */
|
||
async replyQzoneComment (hostUin: string, tid: string, content: string, target: QzoneReplyTarget) {
|
||
const { uin, g_tk, cookie } = await this.getQzoneAuth();
|
||
const url = `https://h5.qzone.qq.com/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_addreply_ugc?g_tk=${g_tk}`;
|
||
const body = buildQzoneReplyBody(uin, hostUin, tid, content, target);
|
||
const text = await RequestUtil.HttpGetText(url, 'POST', body, {
|
||
Cookie: cookie,
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
});
|
||
// 复用评论解析:回复的 id 在 data.id(与评论的 commentid 不同名)
|
||
const response = parseQzoneJson<{ code?: number; subcode?: number; message?: string; data?: { id?: string | number } }>(text);
|
||
if (typeof response.code === 'number' && response.code !== 0) {
|
||
throw new Error(`qzone reply failed: code=${response.code} ${response.message ?? ''}`.trim());
|
||
}
|
||
const id = response.data?.id;
|
||
return { comment_id: id !== undefined ? String(id) : '' };
|
||
}
|
||
|
||
/** 发说说(富返回:tid + time;SnowCat 原版 publishQzoneMsg 只返回 tid,保留不动) */
|
||
async publishQzoneMsgFull (content: string, richvals: string[], ugcRight: number, targetUins?: string[]) {
|
||
const { uin, g_tk, cookie } = await this.getQzoneAuth();
|
||
const richval = richvals.length > 0 ? richvals.join('\t') : undefined;
|
||
const allowUins = targetUins && targetUins.length > 0 ? targetUins.join('|') : undefined;
|
||
const body = buildQzonePublishBody({ hostuin: uin, content, richval, ugcRight, allowUins });
|
||
const api = `https://user.qzone.qq.com/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_publish_v6?g_tk=${g_tk}`;
|
||
const resultText = await RequestUtil.HttpGetText(api, 'POST', body, {
|
||
Cookie: cookie,
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
});
|
||
return parseQzonePublishFull(resultText);
|
||
}
|
||
|
||
/** 获取空间名称与说明(默认自己,可查别人——需有访问权) */
|
||
async getQzoneProfile (targetUin?: string): Promise<QzoneProfile> {
|
||
const { uin, g_tk, cookie } = await this.getQzoneAuth();
|
||
const query = buildQzoneProfileQuery(uin, targetUin ?? uin, g_tk);
|
||
const url = `https://h5.qzone.qq.com/proxy/domain/base.qzone.qq.com/cgi-bin/user/cgi_userinfo_get_all?${query}`;
|
||
const text = await RequestUtil.HttpGetText(url, 'GET', '', { Cookie: cookie });
|
||
return parseQzoneProfileResponse(text);
|
||
}
|
||
|
||
/** 修改空间名称/说明(只动传了的字段;空串=清空) */
|
||
async setQzoneProfile (profile: QzoneProfileUpdate): Promise<void> {
|
||
const { uin, g_tk, cookie } = await this.getQzoneAuth();
|
||
const body = buildQzoneProfileUpdateBody(uin, profile);
|
||
const url = `https://h5.qzone.qq.com/proxy/domain/w.qzone.qq.com/cgi-bin/user/cgi_apply_updateuserinfo_new?g_tk=${g_tk}`;
|
||
const text = await RequestUtil.HttpGetText(url, 'POST', body, {
|
||
Cookie: cookie,
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
});
|
||
parseQzoneProfileUpdateResponse(text);
|
||
}
|
||
|
||
/** 空间拉黑/解拉黑(action=1 拉黑 / 2 解拉黑) */
|
||
async setQzoneBlack (targetUin: string, ban: boolean): Promise<void> {
|
||
const { uin, g_tk, cookie } = await this.getQzoneAuth();
|
||
const url = `https://h5.qzone.qq.com/proxy/domain/w.qzone.qq.com/cgi-bin/right/cgi_black_action_new?g_tk=${g_tk}`;
|
||
const body = buildQzoneBlackBody(uin, targetUin, ban);
|
||
const text = await RequestUtil.HttpGetText(url, 'POST', body, {
|
||
Cookie: cookie,
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
});
|
||
parseQzoneBlackResponse(text, ban);
|
||
}
|
||
|
||
/** 改已有说说的查看权限(msgdetail_v6 → emotion_cgi_update 两步全量重提) */
|
||
async setQzoneMsgRight (tid: string, ugcRight: number, targetUins?: string[]): Promise<QzoneUpdateRightResult> {
|
||
const { uin, g_tk, cookie } = await this.getQzoneAuth();
|
||
// 第一步:拉详情
|
||
const detailQuery = buildQzoneMsgDetailQuery(uin, tid, g_tk);
|
||
const detailUrl = `https://h5.qzone.qq.com/proxy/domain/taotao.qq.com/cgi-bin/emotion_cgi_msgdetail_v6?${detailQuery}`;
|
||
const detailText = await RequestUtil.HttpGetText(detailUrl, 'GET', '', { Cookie: cookie });
|
||
const detail = parseQzoneJson<RawMsgDetailResponse>(detailText);
|
||
parseQzoneDetailError(detail, tid);
|
||
// 第二步:全量重提 + 覆写权限
|
||
const bodyParams = buildQzoneUpdatePayload(detail, uin);
|
||
bodyParams.set('ugc_right', String(ugcRight));
|
||
if (targetUins && targetUins.length > 0) {
|
||
bodyParams.set('allow_uins', targetUins.join('|'));
|
||
}
|
||
const url = `https://h5.qzone.qq.com/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_update?g_tk=${g_tk}`;
|
||
const text = await RequestUtil.HttpGetText(url, 'POST', bodyParams.toString(), {
|
||
Cookie: cookie,
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
});
|
||
return parseQzoneUpdateRightResponse(text, tid, ugcRight);
|
||
}
|
||
|
||
/**
|
||
* 获取空间好友动态流(feeds3_html_more,HTML 载荷数据级解析)。
|
||
* cursor 传上次返回的 next_cursor 续翻;不传从第 page_num 页走。
|
||
*/
|
||
async getQzoneFeeds (pageNum = 1, count = 10, cursor?: string) {
|
||
const { uin, g_tk, cookie } = await this.getQzoneAuth();
|
||
if (!Number.isSafeInteger(pageNum) || pageNum < 1 || !Number.isSafeInteger(count) || count < 1 || count > 50) {
|
||
throw new Error('空间好友动态页码或数量无效');
|
||
}
|
||
const start = cursor === undefined
|
||
? { page: 1, state: initialQzoneFeedState() }
|
||
: decodeQzoneFeedCursor(cursor, uin);
|
||
if (cursor !== undefined && pageNum !== 1 && pageNum !== start.page) {
|
||
throw new Error('page_num 与空间好友动态 cursor 不匹配');
|
||
}
|
||
const target = cursor === undefined ? pageNum : start.page;
|
||
let state: QzoneFeedState = start.state;
|
||
const seen = new Set([qzoneFeedPosition(state)]);
|
||
|
||
for (let page = start.page; page <= target; page++) {
|
||
const query = new URLSearchParams({
|
||
uin,
|
||
scope: '0',
|
||
view: '1',
|
||
gid: '',
|
||
flag: '1',
|
||
filter: 'all',
|
||
applist: 'all',
|
||
refresh: '0',
|
||
firstGetGroup: '0',
|
||
mixnocache: '0',
|
||
scene: '0',
|
||
sidomain: 'qzonestyle.gtimg.cn',
|
||
count: String(count),
|
||
...state,
|
||
getAisort: state.aisortNextTime,
|
||
g_tk: g_tk,
|
||
callback: '_preloadCallback',
|
||
format: 'jsonp',
|
||
useutf8: '1',
|
||
outputhtmlfeed: '1',
|
||
usertime: String(Date.now()),
|
||
rd: String(Math.random()),
|
||
windowId: String(Math.random()),
|
||
});
|
||
query.delete('aisortNextTime');
|
||
const url = `https://h5.qzone.qq.com/proxy/domain/ic2.qzone.qq.com/cgi-bin/feeds/feeds3_html_more?${query}`;
|
||
const body = await RequestUtil.HttpGetText(url, 'GET', '', { Cookie: cookie });
|
||
const data = parseQzoneCallback<RawFeedsResponse>(body);
|
||
if (data.code !== 0 || (data.subcode !== undefined && data.subcode !== 0)) {
|
||
throw new Error(
|
||
`qzone feeds failed: code=${data.code} subcode=${data.subcode ?? 0} ${typeof data.message === 'string' ? data.message : ''}`.trim(),
|
||
);
|
||
}
|
||
const main = data.data?.main;
|
||
if (main !== undefined && (main === null || typeof main !== 'object' || Array.isArray(main))) {
|
||
throw new Error('空间好友动态分页信息结构异常');
|
||
}
|
||
if (main?.['error'] !== undefined && main['error'] !== '') {
|
||
throw new Error('空间好友动态服务端报告拉取失败');
|
||
}
|
||
const result = mapQzoneFeeds(data);
|
||
if (result.has_more) {
|
||
if (!main) throw new Error('空间好友动态缺少下一页游标');
|
||
state = nextQzoneFeedState(main, state);
|
||
const position = qzoneFeedPosition(state);
|
||
if (seen.has(position)) throw new Error('空间好友动态分页游标未推进');
|
||
seen.add(position);
|
||
result.next_cursor = encodeQzoneFeedCursor(uin, page + 1, state);
|
||
}
|
||
if (page === target) return result;
|
||
if (!result.has_more) {
|
||
return { feeds: [], has_more: false, next_cursor: null };
|
||
}
|
||
}
|
||
throw new Error('空间好友动态分页未完成');
|
||
}
|
||
|
||
// ───────────── SnowCat · qzone 公共空间(空友)代理(SnowLuma 移植) ─────────────
|
||
|
||
async getQzonePublicCapabilities (cookie: string, bkn: string, uin: string) {
|
||
const impl = await import('./qzone-public-web');
|
||
return impl.getQzonePublicCapabilities(cookie, bkn, uin);
|
||
}
|
||
|
||
async getQzonePublicFeeds (cookie: string, bkn: string, uin: string, cursor?: string) {
|
||
const impl = await import('./qzone-public-web');
|
||
return impl.getQzonePublicFeeds(cookie, bkn, uin, cursor);
|
||
}
|
||
|
||
async getQzonePublicFeedDetail (cookie: string, bkn: string, feed: Record<string, unknown>) {
|
||
const impl = await import('./qzone-public-web');
|
||
return impl.getQzonePublicFeedDetail(cookie, bkn, feed);
|
||
}
|
||
|
||
async getQzonePublicComments (cookie: string, bkn: string, feed: Record<string, unknown>, cursor: string) {
|
||
const impl = await import('./qzone-public-web');
|
||
return impl.getQzonePublicComments(cookie, bkn, feed, cursor);
|
||
}
|
||
|
||
async getQzonePublicReplies (cookie: string, bkn: string, feed: Record<string, unknown>, comment: Record<string, unknown>, cursor: string, pageSize: number) {
|
||
const impl = await import('./qzone-public-web');
|
||
return impl.getQzonePublicReplies(cookie, bkn, feed, comment, cursor, pageSize);
|
||
}
|
||
|
||
async setQzonePublicUpvote (cookie: string, bkn: string, feed: Record<string, unknown>, enable: boolean) {
|
||
const impl = await import('./qzone-public-web');
|
||
return impl.setQzonePublicUpvote(cookie, bkn, feed, enable);
|
||
}
|
||
|
||
async setQzonePublicLike (cookie: string, bkn: string, feed: Record<string, unknown>, enable: boolean) {
|
||
const impl = await import('./qzone-public-web');
|
||
return impl.setQzonePublicLike(cookie, bkn, feed, enable);
|
||
}
|
||
|
||
async setQzonePublicCommentLike (cookie: string, bkn: string, feed: Record<string, unknown>, comment: Record<string, unknown>, enable: boolean, reply?: Record<string, unknown>) {
|
||
const impl = await import('./qzone-public-web');
|
||
return impl.setQzonePublicCommentLike(cookie, bkn, feed, comment, enable, reply);
|
||
}
|
||
|
||
async commentQzonePublicFeed (cookie: string, bkn: string, feed: Record<string, unknown>, user: Record<string, unknown>, content: string | Record<string, unknown>[]) {
|
||
const impl = await import('./qzone-public-web');
|
||
return impl.commentQzonePublicFeed(cookie, bkn, feed, user, content);
|
||
}
|
||
|
||
async replyQzonePublicFeed (cookie: string, bkn: string, feed: Record<string, unknown>, user: Record<string, unknown>, comment: Record<string, unknown>, content: string | Record<string, unknown>[], reply?: Record<string, unknown>) {
|
||
const impl = await import('./qzone-public-web');
|
||
return impl.replyQzonePublicFeed(cookie, bkn, feed, user, comment, content, reply);
|
||
}
|
||
|
||
async deleteQzonePublicComment (cookie: string, bkn: string, feed: Record<string, unknown>, comment: Record<string, unknown>) {
|
||
const impl = await import('./qzone-public-web');
|
||
return impl.deleteQzonePublicComment(cookie, bkn, feed, comment);
|
||
}
|
||
|
||
async deleteQzonePublicReply (cookie: string, bkn: string, feed: Record<string, unknown>, comment: Record<string, unknown>, reply: Record<string, unknown>) {
|
||
const impl = await import('./qzone-public-web');
|
||
return impl.deleteQzonePublicReply(cookie, bkn, feed, comment, reply);
|
||
}
|
||
|
||
async setQzonePublicFollow (cookie: string, bkn: string, feed: Record<string, unknown>, enable: boolean) {
|
||
const impl = await import('./qzone-public-web');
|
||
return impl.setQzonePublicFollow(cookie, bkn, feed, enable);
|
||
}
|
||
|
||
async forwardQzonePublicFeed (cookie: string, bkn: string, feed: Record<string, unknown>, reason: string) {
|
||
const impl = await import('./qzone-public-web');
|
||
return impl.forwardQzonePublicFeed(cookie, bkn, feed, reason);
|
||
}
|
||
}
|