NapCatQQ source snapshot
This commit is contained in:
commit
f954252a34
1111 files changed
+139867
No files matched your search
@@ -0,0 +1,17 @@
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { OneBotAction } from '../OneBotAction';
|
||||
import { Type } from '@sinclair/typebox';
|
||||
|
||||
export class BotExit extends OneBotAction<void, void> {
|
||||
override actionName = ActionName.Exit;
|
||||
override payloadSchema = Type.Object({});
|
||||
override returnSchema = Type.Object({});
|
||||
override actionSummary = '退出登录';
|
||||
override actionTags = ['系统扩展'];
|
||||
override payloadExample = {};
|
||||
override returnExample = null;
|
||||
|
||||
async _handle () {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { OneBotAction } from '../OneBotAction';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
bot_appid: Type.String({ description: '机器人AppID' }),
|
||||
button_id: Type.String({ default: '', description: '按钮ID' }),
|
||||
callback_data: Type.String({ default: '', description: '回调数据' }),
|
||||
msg_seq: Type.String({ default: '10086', description: '消息序列号' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Any({ description: '点击结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class ClickInlineKeyboardButton extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.ClickInlineKeyboardButton;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '点击内联键盘按钮';
|
||||
override actionTags = ['消息扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
bot_appid: '1234567890',
|
||||
button_id: 'btn_1',
|
||||
callback_data: '',
|
||||
msg_seq: '10086',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
};
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return await this.core.apis.MsgApi.clickInlineKeyboardButton({
|
||||
buttonId: payload.button_id,
|
||||
peerId: payload.group_id.toString(),
|
||||
botAppid: payload.bot_appid,
|
||||
msgSeq: payload.msg_seq,
|
||||
callback_data: payload.callback_data,
|
||||
dmFlag: 0,
|
||||
chatType: 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
rawData: Type.String({ description: '原始数据' }),
|
||||
brief: Type.String({ description: '简要描述' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Any({ description: '创建结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class CreateCollection extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.CreateCollection;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '创建收藏';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = {
|
||||
rawData: '收藏内容',
|
||||
brief: '收藏标题',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
result: 0,
|
||||
errMsg: '',
|
||||
};
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return await this.core.apis.CollectionApi.createCollection(
|
||||
this.core.selfInfo.uin,
|
||||
this.core.selfInfo.uid,
|
||||
this.core.selfInfo.nick,
|
||||
payload.brief, payload.rawData
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { stat, readFile } from 'node:fs/promises';
|
||||
import { basename } from 'node:path';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
|
||||
const FetchCustomFaceDetailPayloadSchema = Type.Object({
|
||||
count: Type.Union([Type.Number(), Type.String()], { default: 48, description: '获取数量' }),
|
||||
});
|
||||
|
||||
type FetchCustomFaceDetailPayloadType = Static<typeof FetchCustomFaceDetailPayloadSchema>;
|
||||
|
||||
const FetchCustomFaceDetailReturnSchema = Type.Any({ description: '自定义表情详情列表' });
|
||||
|
||||
type FetchCustomFaceDetailReturnType = Static<typeof FetchCustomFaceDetailReturnSchema>;
|
||||
|
||||
export class FetchCustomFaceDetail extends OneBotAction<FetchCustomFaceDetailPayloadType, FetchCustomFaceDetailReturnType> {
|
||||
override actionName = ActionName.FetchCustomFaceDetail;
|
||||
override payloadSchema = FetchCustomFaceDetailPayloadSchema;
|
||||
override returnSchema = FetchCustomFaceDetailReturnSchema;
|
||||
override actionSummary = '获取自定义表情详情';
|
||||
override actionTags = ['系统扩展'];
|
||||
override payloadExample = {
|
||||
count: 10,
|
||||
};
|
||||
|
||||
override returnExample = null;
|
||||
|
||||
async _handle (payload: FetchCustomFaceDetailPayloadType) {
|
||||
const ret = await this.core.apis.MsgApi.fetchFavEmojiList(+payload.count);
|
||||
return ret.emojiInfoList;
|
||||
}
|
||||
}
|
||||
|
||||
const AddCustomFacePayloadSchema = Type.Object({
|
||||
file: Type.String({ description: '本地表情文件路径' }),
|
||||
emoji_id: Type.Optional(Type.Union([Type.String(), Type.Number()], { description: '表情ID,未提供时传空字符串' })),
|
||||
package_id: Type.Optional(Type.Union([Type.String(), Type.Number()], { description: '表情包ID,未提供时传0' })),
|
||||
file_name: Type.Optional(Type.String({ description: '文件名,未提供时从file路径取basename' })),
|
||||
file_size: Type.Optional(Type.Union([Type.String(), Type.Number()], { description: '文件大小,未提供时读取本地文件' })),
|
||||
md5: Type.Optional(Type.String({ description: '文件MD5,未提供时读取本地文件计算' })),
|
||||
is_mark_face: Type.Optional(Type.Boolean({ description: '是否商城表情' })),
|
||||
is_origin: Type.Optional(Type.Boolean({ description: '是否原图' })),
|
||||
});
|
||||
|
||||
type AddCustomFacePayloadType = Static<typeof AddCustomFacePayloadSchema>;
|
||||
|
||||
const AddCustomFaceReturnSchema = Type.Any({ description: '添加结果' });
|
||||
|
||||
type AddCustomFaceReturnType = Static<typeof AddCustomFaceReturnSchema>;
|
||||
|
||||
export class AddCustomFace extends OneBotAction<AddCustomFacePayloadType, AddCustomFaceReturnType> {
|
||||
override actionName = ActionName.AddCustomFace;
|
||||
override payloadSchema = AddCustomFacePayloadSchema;
|
||||
override returnSchema = AddCustomFaceReturnSchema;
|
||||
override actionSummary = '添加自定义表情';
|
||||
override actionTags = ['系统扩展'];
|
||||
override payloadExample = {
|
||||
file: 'C:\\Users\\Public\\face.png',
|
||||
is_origin: true,
|
||||
};
|
||||
|
||||
override returnExample = null;
|
||||
|
||||
async _handle (payload: AddCustomFacePayloadType) {
|
||||
const fileStat = await stat(payload.file);
|
||||
const md5 = payload.md5 ?? createHash('md5').update(await readFile(payload.file)).digest('hex');
|
||||
return await this.core.apis.MsgApi.addFavEmoji({
|
||||
emojiId: payload.emoji_id?.toString() ?? '',
|
||||
packageId: payload.package_id === undefined ? 0 : Number(payload.package_id),
|
||||
emojiPath: payload.file,
|
||||
fileSize: payload.file_size?.toString() ?? fileStat.size.toString(),
|
||||
fileName: payload.file_name ?? basename(payload.file),
|
||||
md5,
|
||||
isMarkFace: payload.is_mark_face ?? false,
|
||||
isOrigin: payload.is_origin ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const DeleteCustomFacePayloadSchema = Type.Object({
|
||||
res_id: Type.Optional(Type.Union([
|
||||
Type.String({ description: 'fetch_custom_face_detail返回的resId' }),
|
||||
Type.Array(Type.String({ description: 'fetch_custom_face_detail返回的resId' })),
|
||||
])),
|
||||
id: Type.Optional(Type.Union([
|
||||
Type.String({ description: 'native deleteFavEmoji字符串ID,通常为resId' }),
|
||||
Type.Array(Type.String({ description: 'native deleteFavEmoji字符串ID,通常为resId' })),
|
||||
])),
|
||||
ids: Type.Optional(Type.Array(Type.String({ description: 'native deleteFavEmoji字符串ID列表,通常为resId列表' }))),
|
||||
md5: Type.Optional(Type.Union([
|
||||
Type.String({ description: '表情MD5,不能直接删除,请先通过fetch_custom_face_detail获取resId' }),
|
||||
Type.Array(Type.String({ description: '表情MD5,不能直接删除,请先通过fetch_custom_face_detail获取resId' })),
|
||||
])),
|
||||
});
|
||||
|
||||
type DeleteCustomFacePayloadType = Static<typeof DeleteCustomFacePayloadSchema>;
|
||||
|
||||
const DeleteCustomFaceReturnSchema = Type.Any({ description: '删除结果' });
|
||||
|
||||
type DeleteCustomFaceReturnType = Static<typeof DeleteCustomFaceReturnSchema>;
|
||||
|
||||
export class DeleteCustomFace extends OneBotAction<DeleteCustomFacePayloadType, DeleteCustomFaceReturnType> {
|
||||
override actionName = ActionName.DeleteCustomFace;
|
||||
override payloadSchema = DeleteCustomFacePayloadSchema;
|
||||
override returnSchema = DeleteCustomFaceReturnSchema;
|
||||
override actionSummary = '删除自定义表情';
|
||||
override actionTags = ['系统扩展'];
|
||||
override payloadExample = {
|
||||
res_id: '2707600964_0_0_0_D8EAA70984B402EE10B0F33DB88F4173_0_0',
|
||||
};
|
||||
|
||||
override returnExample = null;
|
||||
|
||||
async _handle (payload: DeleteCustomFacePayloadType) {
|
||||
const ids = payload.ids ??
|
||||
(Array.isArray(payload.res_id) ? payload.res_id : payload.res_id ? [payload.res_id] : undefined) ??
|
||||
(Array.isArray(payload.id) ? payload.id : payload.id ? [payload.id] : undefined) ??
|
||||
[];
|
||||
if (ids.length === 0) throw new Error('res_id or ids is required');
|
||||
return await this.core.apis.MsgApi.deleteFavEmoji(ids);
|
||||
}
|
||||
}
|
||||
|
||||
const SetCustomFaceDescPayloadSchema = Type.Object({
|
||||
emoji_id: Type.Union([Type.Number(), Type.String()], { description: '表情ID' }),
|
||||
res_id: Type.String({ description: '资源ID' }),
|
||||
md5: Type.String({ description: '表情MD5' }),
|
||||
desc: Type.String({ description: '新的表情描述' }),
|
||||
});
|
||||
|
||||
type SetCustomFaceDescPayloadType = Static<typeof SetCustomFaceDescPayloadSchema>;
|
||||
|
||||
const SetCustomFaceDescReturnSchema = Type.Any({ description: '修改结果' });
|
||||
|
||||
type SetCustomFaceDescReturnType = Static<typeof SetCustomFaceDescReturnSchema>;
|
||||
|
||||
export class SetCustomFaceDesc extends OneBotAction<SetCustomFaceDescPayloadType, SetCustomFaceDescReturnType> {
|
||||
override actionName = ActionName.SetCustomFaceDesc;
|
||||
override payloadSchema = SetCustomFaceDescPayloadSchema;
|
||||
override returnSchema = SetCustomFaceDescReturnSchema;
|
||||
override actionSummary = '修改自定义表情描述';
|
||||
override actionTags = ['系统扩展'];
|
||||
override payloadExample = {
|
||||
emoji_id: 1,
|
||||
res_id: 'resource-id',
|
||||
md5: 'd41d8cd98f00b204e9800998ecf8427e',
|
||||
desc: '新的描述',
|
||||
};
|
||||
|
||||
override returnExample = null;
|
||||
|
||||
async _handle (payload: SetCustomFaceDescPayloadType) {
|
||||
return await this.core.apis.MsgApi.modifyFavEmojiDesc([{
|
||||
emojiId: Number(payload.emoji_id),
|
||||
resId: payload.res_id,
|
||||
md5: payload.md5,
|
||||
desc: payload.desc,
|
||||
}]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
album_id: Type.String({ description: '相册ID' }),
|
||||
lloc: Type.String({ description: '媒体ID (lloc)' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Any({ description: '删除结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class DelGroupAlbumMedia extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.DelGroupAlbumMedia;
|
||||
override actionSummary = '删除群相册媒体';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
album_id: 'album_id_1',
|
||||
lloc: 'media_id_1',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
result: {},
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return await this.core.apis.WebApi.deleteAlbumMediaByNTQQ(
|
||||
payload.group_id,
|
||||
payload.album_id,
|
||||
payload.lloc
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
|
||||
import { checkQzoneFields, qzoneIdentity, qzonePayload, SlQzoneIdentitySchema } from './qzone/common-snowluma';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
tid: SlQzoneIdentitySchema,
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
export class DeleteQzoneMsg extends OneBotAction<PayloadType, null> {
|
||||
override actionName = ActionName.DeleteQzoneMsg;
|
||||
override actionSummary = '删除QQ空间说说';
|
||||
override actionDescription = '删除QQ空间(Qzone)的一条说说, 按 tid 删除';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = ExtendsActionsExamples.DeleteQzoneMsg.payload;
|
||||
override returnExample = ExtendsActionsExamples.DeleteQzoneMsg.response;
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = Type.Null();
|
||||
|
||||
protected override async check (payload: PayloadType): Promise<BaseCheckResult> {
|
||||
try {
|
||||
checkQzoneFields(qzonePayload(payload), ['tid'], false);
|
||||
qzoneIdentity(payload.tid, 'tid');
|
||||
} catch (error) { return { valid: false, message: (error as Error).message }; }
|
||||
return await super.check(payload);
|
||||
}
|
||||
|
||||
async _handle (payload: PayloadType): Promise<null> {
|
||||
checkQzoneFields(qzonePayload(payload), ['tid'], false);
|
||||
await this.core.apis.WebApi.deleteQzoneMsg(qzoneIdentity(payload.tid, 'tid'));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
export const DoGroupAlbumCommentPayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
album_id: Type.String({ description: '相册 ID' }),
|
||||
lloc: Type.Optional(Type.String({ description: '图片 ID;不传或为空时评论整个上传批次,此时必须提供 batch_id' })),
|
||||
batch_id: Type.Optional(Type.String({ description: '上传批次 ID,可取自 get_group_album_media_list 返回的 batch.link_id 或 media[].batch_id' })),
|
||||
content: Type.String({ description: '评论内容' }),
|
||||
});
|
||||
|
||||
export type DoGroupAlbumCommentPayload = Static<typeof DoGroupAlbumCommentPayloadSchema>;
|
||||
|
||||
export class DoGroupAlbumComment extends OneBotAction<DoGroupAlbumCommentPayload, any> {
|
||||
override actionName = ActionName.DoGroupAlbumComment;
|
||||
override actionSummary = '发表群相册评论';
|
||||
override actionDescription = 'lloc 与 batch_id 至少提供一项。传入 lloc 时评论单张图片;仅传 batch_id 时评论整个上传批次。';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
album_id: 'album_id_1',
|
||||
batch_id: '2147483658',
|
||||
content: '很有意思',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
result: {},
|
||||
};
|
||||
|
||||
override payloadSchema = DoGroupAlbumCommentPayloadSchema;
|
||||
override returnSchema = Type.Any({ description: '评论结果' });
|
||||
|
||||
protected override async check (payload: DoGroupAlbumCommentPayload): Promise<BaseCheckResult> {
|
||||
const result = await super.check(payload);
|
||||
if (!result.valid) return result;
|
||||
if (!payload.lloc && !payload.batch_id) {
|
||||
return { valid: false, message: 'lloc 或 batch_id 至少需要提供一个' };
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
async _handle (payload: DoGroupAlbumCommentPayload) {
|
||||
return await this.core.apis.WebApi.doAlbumMediaPlainCommentByNTQQ(
|
||||
payload.group_id,
|
||||
payload.album_id,
|
||||
payload.lloc,
|
||||
payload.content,
|
||||
payload.batch_id
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
count: Type.Union([Type.Number(), Type.String()], { default: 48, description: '获取数量' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Array(Type.String(), { description: '表情URL列表' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class FetchCustomFace extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.FetchCustomFace;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '获取自定义表情';
|
||||
override actionTags = ['系统扩展'];
|
||||
override payloadExample = {
|
||||
count: 10,
|
||||
};
|
||||
|
||||
override returnExample = [
|
||||
'http://example.com/face1.png',
|
||||
];
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
const ret = await this.core.apis.MsgApi.fetchFavEmojiList(+payload.count);
|
||||
return ret.emojiInfoList.map(e => e.url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { MessageUnique } from 'napcat-common/src/message-unique';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
message_id: Type.Union([Type.Number(), Type.String()], { description: '消息ID' }),
|
||||
emojiId: Type.Union([Type.Number(), Type.String()], { description: '表情ID' }),
|
||||
emojiType: Type.Union([Type.Number(), Type.String()], { description: '表情类型' }),
|
||||
count: Type.Union([Type.Number(), Type.String()], { default: 20, description: '获取数量' }),
|
||||
cookie: Type.String({ default: '', description: '分页Cookie' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
emojiLikesList: Type.Array(Type.Object({
|
||||
tinyId: Type.String({ description: 'TinyID' }),
|
||||
nickName: Type.String({ description: '昵称' }),
|
||||
headUrl: Type.String({ description: '头像URL' }),
|
||||
}), { description: '表情回应列表' }),
|
||||
cookie: Type.String({ description: '分页Cookie' }),
|
||||
isLastPage: Type.Boolean({ description: '是否最后一页' }),
|
||||
isFirstPage: Type.Boolean({ description: '是否第一页' }),
|
||||
result: Type.Number({ description: '结果状态码' }),
|
||||
errMsg: Type.String({ description: '错 误信息' }),
|
||||
}, { description: '表情回应详情' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class FetchEmojiLike extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.FetchEmojiLike;
|
||||
override actionSummary = '获取表情点赞详情';
|
||||
override actionTags = ['消息扩展'];
|
||||
override payloadExample = {
|
||||
message_id: 12345,
|
||||
emojiId: '123',
|
||||
emojiType: 1,
|
||||
count: 10,
|
||||
cookie: '',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
emojiLikesList: [
|
||||
{
|
||||
tinyId: '123456',
|
||||
nickName: '测试用户',
|
||||
headUrl: 'http://example.com/avatar.png',
|
||||
},
|
||||
],
|
||||
cookie: '',
|
||||
isLastPage: true,
|
||||
isFirstPage: true,
|
||||
result: 0,
|
||||
errMsg: '',
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType): Promise<ReturnType> {
|
||||
const msgIdPeer = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id);
|
||||
if (!msgIdPeer) throw new Error('消息不存在');
|
||||
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(msgIdPeer.Peer, [msgIdPeer.MsgId])).msgList[0];
|
||||
if (!msg) throw new Error('消息不存在');
|
||||
const res = await this.core.apis.MsgApi.getMsgEmojiLikesList(
|
||||
msgIdPeer.Peer, msg.msgSeq, payload.emojiId.toString(), payload.emojiType.toString(), payload.cookie, +payload.count
|
||||
);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
|
||||
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
chat_type: Type.Union([Type.Number(), Type.String()], { default: 1, description: '聊天类型' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Array(
|
||||
Type.Object({
|
||||
type: Type.String({ description: '角色类型' }),
|
||||
characters: Type.Array(
|
||||
Type.Object({
|
||||
character_id: Type.String({ description: '角色ID' }),
|
||||
character_name: Type.String({ description: '角色名称' }),
|
||||
preview_url: Type.String({ description: '预览URL' }),
|
||||
}),
|
||||
{ description: '角色列表' }
|
||||
),
|
||||
}),
|
||||
{ description: 'AI角色列表' }
|
||||
);
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetAiCharacters extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.GetAiCharacters;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '获取AI角色列表';
|
||||
override actionDescription = '获取群聊中的AI角色列表';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = ExtendsActionsExamples.GetAiCharacters.payload;
|
||||
override returnExample = ExtendsActionsExamples.GetAiCharacters.response;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
const chatTypeNum = Number(payload.chat_type);
|
||||
const rawList = await this.core.apis.PacketApi.pkt.operation.FetchAiVoiceList(+payload.group_id, chatTypeNum);
|
||||
return rawList?.map((item) => ({
|
||||
type: item.category,
|
||||
characters: item.voices.map((voice) => ({
|
||||
character_id: voice.voiceId,
|
||||
character_name: voice.voiceDisplayName,
|
||||
preview_url: voice.voiceExampleUrl,
|
||||
})),
|
||||
})) ?? [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { OneBotAction } from '../OneBotAction';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
|
||||
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
clientkey: Type.Optional(Type.String({ description: '客户端Key' })),
|
||||
}, { description: '获取ClientKey结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetClientkey extends OneBotAction<void, ReturnType> {
|
||||
override actionName = ActionName.GetClientkey;
|
||||
override payloadSchema = Type.Object({});
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '获取ClientKey';
|
||||
override actionDescription = '获取当前登录帐号的ClientKey';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = ExtendsActionsExamples.GetClientkey.payload;
|
||||
override returnExample = ExtendsActionsExamples.GetClientkey.response;
|
||||
|
||||
async _handle () {
|
||||
return { clientkey: (await this.core.apis.UserApi.forceFetchClientKey()).clientKey };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
category: Type.String({ description: '分类ID' }),
|
||||
count: Type.String({ default: '50', description: '获取数量' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Any({ description: '收藏列表' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetCollectionList extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.GetCollectionList;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '获取收藏列表';
|
||||
override actionTags = ['系统扩展'];
|
||||
override payloadExample = {
|
||||
category: '0',
|
||||
count: '50',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
errCode: 0,
|
||||
errMsg: '',
|
||||
collectionSearchList: {
|
||||
collectionItemList: [
|
||||
{
|
||||
cid: '123456',
|
||||
type: 8,
|
||||
status: 1,
|
||||
author: {
|
||||
type: 2,
|
||||
numId: '123456',
|
||||
strId: '昵称',
|
||||
groupId: '123456',
|
||||
groupName: '群名',
|
||||
uid: '123456',
|
||||
},
|
||||
bid: 1,
|
||||
category: 1,
|
||||
createTime: '1769169157000',
|
||||
collectTime: '1769413477691',
|
||||
modifyTime: '1769413477691',
|
||||
sequence: '1769413476735',
|
||||
shareUrl: '',
|
||||
customGroupId: 0,
|
||||
securityBeat: false,
|
||||
summary: {
|
||||
textSummary: null,
|
||||
linkSummary: null,
|
||||
gallerySummary: null,
|
||||
audioSummary: null,
|
||||
videoSummary: null,
|
||||
fileSummary: null,
|
||||
locationSummary: null,
|
||||
richMediaSummary: {
|
||||
title: '',
|
||||
subTitle: '',
|
||||
brief: 'text',
|
||||
picList: [],
|
||||
contentType: 1,
|
||||
originalUri: '',
|
||||
publisher: '',
|
||||
richMediaVersion: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
hasMore: false,
|
||||
bottomTimeStamp: '1769413477691',
|
||||
},
|
||||
};
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return await this.core.apis.CollectionApi.getAllCollection(+payload.category, +payload.count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { MessageUnique } from 'napcat-common/src/message-unique';
|
||||
import { Peer, ChatType } from '@/napcat-core';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.Optional(Type.String({ description: '群号,短ID可不传' })),
|
||||
message_id: Type.String({ description: '消息ID,可以传递长ID或短ID' }),
|
||||
emoji_id: Type.String({ description: '表情ID' }),
|
||||
emoji_type: Type.Optional(Type.String({ description: '表情类型' })),
|
||||
count: Type.Number({ default: 0, description: '数量,0代表全部' }),
|
||||
});
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
emoji_like_list: Type.Array(
|
||||
Type.Object({
|
||||
user_id: Type.String({ description: '点击者QQ号' }),
|
||||
nick_name: Type.String({ description: '昵称?' }),
|
||||
}),
|
||||
{ description: '表情回应列表' }
|
||||
),
|
||||
});
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetEmojiLikes extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.GetEmojiLikes;
|
||||
override actionSummary = '获取消息表情点赞列表';
|
||||
override actionTags = ['消息扩展'];
|
||||
override payloadExample = {
|
||||
message_id: '12345',
|
||||
emoji_id: '123',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
emoji_like_list: [
|
||||
{
|
||||
user_id: '654321',
|
||||
nick_name: '测试用户',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
let peer: Peer;
|
||||
let msgId: string;
|
||||
|
||||
if (MessageUnique.isShortId(payload.message_id)) {
|
||||
const msgIdPeer = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id);
|
||||
if (!msgIdPeer) throw new Error('消息不存在');
|
||||
peer = msgIdPeer.Peer;
|
||||
msgId = msgIdPeer.MsgId;
|
||||
} else {
|
||||
if (!payload.group_id) throw new Error('长ID模式下必须提供群号');
|
||||
peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id };
|
||||
msgId = payload.message_id;
|
||||
}
|
||||
|
||||
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId])).msgList[0];
|
||||
if (!msg) throw new Error('消息不存在');
|
||||
|
||||
const emojiType = payload.emoji_type ?? (payload.emoji_id.length > 3 ? '2' : '1');
|
||||
const emojiLikeList: Array<{ user_id: string; nick_name: string; }> = [];
|
||||
let cookie = '';
|
||||
const needFetchCount = payload.count === 0 ? 200 : Math.ceil(payload.count / 15);
|
||||
for (let page = 0; page < needFetchCount; page++) {
|
||||
const res = await this.core.apis.MsgApi.getMsgEmojiLikesList(
|
||||
peer, msg.msgSeq, payload.emoji_id.toString(), emojiType, cookie, 15
|
||||
);
|
||||
|
||||
if (Array.isArray(res.emojiLikesList)) {
|
||||
for (const like of res.emojiLikesList) {
|
||||
emojiLikeList.push({ user_id: like.tinyId, nick_name: like.nickName });
|
||||
}
|
||||
}
|
||||
|
||||
if (res.isLastPage || !res.cookie) break;
|
||||
cookie = res.cookie;
|
||||
}
|
||||
// 切断多余部分
|
||||
if (payload.count > 0) {
|
||||
emojiLikeList.splice(payload.count);
|
||||
}
|
||||
return { emoji_like_list: emojiLikeList };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { OB11Construct } from '@/napcat-onebot/helper/data';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
import { OB11UserSchema } from '../schemas';
|
||||
|
||||
const ReturnSchema = Type.Array(
|
||||
Type.Object({
|
||||
categoryId: Type.Number({ description: '分组ID' }),
|
||||
categoryName: Type.String({ description: '分组名称' }),
|
||||
categoryMbCount: Type.Number({ description: '分组内好友数量' }),
|
||||
buddyList: Type.Array(OB11UserSchema, { description: '好友列表' }),
|
||||
}),
|
||||
{ description: '带分组的好友列表' }
|
||||
);
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetFriendWithCategory extends OneBotAction<void, ReturnType> {
|
||||
override actionName = ActionName.GetFriendsWithCategory;
|
||||
override payloadSchema = Type.Object({});
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '获取带分组的好友列表';
|
||||
override actionTags = ['用户扩展'];
|
||||
override payloadExample = {};
|
||||
override returnExample = [
|
||||
{
|
||||
categoryId: 1,
|
||||
categoryName: '我的好友',
|
||||
categoryMbCount: 1,
|
||||
buddyList: [],
|
||||
},
|
||||
];
|
||||
|
||||
async _handle () {
|
||||
const categories = await this.core.apis.FriendApi.getBuddyV2ExWithCate();
|
||||
return categories.map(category => ({
|
||||
...category,
|
||||
buddyList: OB11Construct.friends(category.buddyList),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { GroupNotifyMsgStatus } from 'napcat-core';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
|
||||
const ReturnSchema = Type.Array(
|
||||
Type.Object({
|
||||
request_id: Type.Number({ description: '请求ID' }),
|
||||
invitor_uin: Type.Number({ description: '邀请者QQ' }),
|
||||
invitor_nick: Type.Optional(Type.String({ description: '邀请者昵称' })),
|
||||
group_id: Type.Number({ description: '群号' }),
|
||||
message: Type.Optional(Type.String({ description: '验证信息' })),
|
||||
group_name: Type.Optional(Type.String({ description: '群名称' })),
|
||||
checked: Type.Boolean({ description: '是否已处理' }),
|
||||
actor: Type.Number({ description: '处理者QQ' }),
|
||||
requester_nick: Type.Optional(Type.String({ description: '请求者昵称' })),
|
||||
}),
|
||||
{ description: '群通知列表' }
|
||||
);
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export default class GetGroupAddRequest extends OneBotAction<void, ReturnType> {
|
||||
override actionName = ActionName.GetGroupIgnoreAddRequest;
|
||||
override payloadSchema = Type.Object({});
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '获取群被忽略的加群请求';
|
||||
override actionTags = ['群组接口'];
|
||||
override payloadExample = {};
|
||||
override returnExample = [
|
||||
{
|
||||
request_id: 12345,
|
||||
invitor_uin: 123456789,
|
||||
invitor_nick: '邀请者',
|
||||
group_id: 123456789,
|
||||
message: '加群请求',
|
||||
group_name: '群名称',
|
||||
checked: false,
|
||||
actor: 0,
|
||||
requester_nick: '请求者',
|
||||
},
|
||||
];
|
||||
|
||||
async _handle (): Promise<ReturnType> {
|
||||
const NTQQUserApi = this.core.apis.UserApi;
|
||||
const NTQQGroupApi = this.core.apis.GroupApi;
|
||||
const ignoredNotifies = await NTQQGroupApi.getSingleScreenNotifies(true, 10);
|
||||
const retData: ReturnType = [];
|
||||
|
||||
const notifyPromises = ignoredNotifies
|
||||
.filter(notify => notify.type === 7)
|
||||
.map(async SSNotify => {
|
||||
const invitorUin = SSNotify.user1?.uid ? +await NTQQUserApi.getUinByUidV2(SSNotify.user1.uid) : 0;
|
||||
const actorUin = SSNotify.user2?.uid ? +await NTQQUserApi.getUinByUidV2(SSNotify.user2.uid) : 0;
|
||||
retData.push({
|
||||
request_id: +SSNotify.seq,
|
||||
invitor_uin: invitorUin,
|
||||
invitor_nick: SSNotify.user1?.nickName,
|
||||
group_id: +SSNotify.group?.groupCode,
|
||||
message: SSNotify?.postscript,
|
||||
group_name: SSNotify.group?.groupName,
|
||||
checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE,
|
||||
actor: actorUin,
|
||||
requester_nick: SSNotify.user1?.nickName,
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(notifyPromises);
|
||||
|
||||
return retData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
album_id: Type.String({ description: '相册ID' }),
|
||||
attach_info: Type.Optional(Type.String({ default: '', description: '附加信息(用于分页)' })),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Any({ description: '相册媒体列表' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetGroupAlbumMediaList extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.GetGroupAlbumMediaList;
|
||||
override actionSummary = '获取群相册媒体列表';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
album_id: 'album_id_1',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
media_list: [
|
||||
{ media_id: 'media_id_1', url: 'http://example.com/1.jpg' },
|
||||
],
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return await this.core.apis.WebApi.getAlbumMediaListByNTQQ(
|
||||
payload.group_id,
|
||||
payload.album_id,
|
||||
payload.attach_info
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Any({ description: '群扩展信息' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetGroupInfoEx extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.GetGroupInfoEx;
|
||||
override actionSummary = '获取群详细信息 (扩展)';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return (await this.core.apis.GroupApi.getGroupExtFE0Info([payload.group_id.toString()])).result.groupExtInfos.get(payload.group_id.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.Union([Type.Number(), Type.String()], { description: '群号' }),
|
||||
need_short_url: Type.Optional(Type.Union([Type.Boolean(), Type.String()], { default: true, description: '是否返回短链 (qm.qq.com/q/xxx),false 返回完整 universal-share 链接' })),
|
||||
src_id: Type.Optional(Type.Union([Type.Number(), Type.String()], { default: 73, description: '分享来源 ID' })),
|
||||
additional_param: Type.Optional(Type.String({ default: '', description: '附加参数' })),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
url: Type.String({ description: '群分享链接' }),
|
||||
}, { description: '群分享链接' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetGroupShareLink extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.GetGroupShareLink;
|
||||
override actionSummary = '获取群分享链接';
|
||||
override actionDescription = '通过 NTQQ 原生 getJoinGroupLink 获取群的 H5 分享/加群链接 (qm.qq.com/q/xxx 或 qun.qq.com/universal-share/share?...)';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
need_short_url: true,
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
url: 'https://qm.qq.com/q/xxxxxxxxxx',
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
const needShortUrl = payload.need_short_url === undefined ? true : payload.need_short_url.toString() === 'true';
|
||||
const srcId = payload.src_id === undefined ? 73 : Number(payload.src_id);
|
||||
const ret = await this.core.apis.GroupApi.getJoinGroupLink(
|
||||
payload.group_id.toString(),
|
||||
needShortUrl,
|
||||
srcId,
|
||||
payload.additional_param ?? ''
|
||||
);
|
||||
if (!ret?.url) {
|
||||
throw new Error(`获取群分享链接失败: ${ret?.errMsg || JSON.stringify(ret)}`);
|
||||
}
|
||||
return { url: ret.url };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.Union([Type.String(), Type.Number()], { description: '群号' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Array(
|
||||
Type.Object({
|
||||
user_id: Type.Number({ description: '打卡者QQ' }),
|
||||
nick: Type.String({ description: '打卡者昵称' }),
|
||||
time: Type.Number({ description: '打卡时间' }),
|
||||
rank: Type.Number({ description: '打卡排名' }),
|
||||
}),
|
||||
{ description: '群组今日打卡列表' }
|
||||
);
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetGroupSignedList extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.GetGroupSignedList;
|
||||
override actionSummary = '获取群组今日打卡列表';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
const data = await this.core.apis.WebApi.getDaySignedList(payload.group_id.toString());
|
||||
if (!data.response.page) throw new Error('无法获取该群组打卡列表');
|
||||
return data.response.page[0]?.infos?.map(info => ({
|
||||
user_id: +info.uid,
|
||||
nick: info.uidGroupNick,
|
||||
time: +info.signedTimeStamp,
|
||||
rank: (info.signInRank - 1) / 2 + 1,
|
||||
})) ?? [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||
import { MiniAppInfo, MiniAppInfoHelper } from 'napcat-core/packet/utils/helper/miniAppHelper';
|
||||
import { MiniAppReqCustomParams, MiniAppReqParams } from 'napcat-core/packet/entities/miniApp';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Union([
|
||||
Type.Object({
|
||||
type: Type.Union([Type.Literal('bili'), Type.Literal('weibo')], { description: '模板类型' }),
|
||||
title: Type.String({ description: '标题' }),
|
||||
desc: Type.String({ description: '描述' }),
|
||||
picUrl: Type.String({ description: '图片URL' }),
|
||||
jumpUrl: Type.String({ description: '跳转URL' }),
|
||||
webUrl: Type.Optional(Type.String({ description: '网页URL' })),
|
||||
rawArkData: Type.Optional(Type.Union([Type.String()], { description: '是否返回原始Ark数据' })),
|
||||
}),
|
||||
Type.Object({
|
||||
title: Type.String({ description: '标题' }),
|
||||
desc: Type.String({ description: '描述' }),
|
||||
picUrl: Type.String({ description: '图片URL' }),
|
||||
jumpUrl: Type.String({ description: '跳转URL' }),
|
||||
iconUrl: Type.String({ description: '图标URL' }),
|
||||
webUrl: Type.Optional(Type.String({ description: '网页URL' })),
|
||||
appId: Type.String({ description: '小程序AppID' }),
|
||||
scene: Type.String({ description: '场景ID' }),
|
||||
templateType: Type.String({ description: '模板类型' }),
|
||||
businessType: Type.String({ description: '业务类型' }),
|
||||
verType: Type.String({ description: '版本类型' }),
|
||||
shareType: Type.String({ description: '分享类型' }),
|
||||
versionId: Type.String({ description: '版本ID' }),
|
||||
sdkId: Type.String({ description: 'SDK ID' }),
|
||||
withShareTicket: Type.String({ description: '是否携带分享票据' }),
|
||||
rawArkData: Type.Optional(Type.String({ description: '是否返回原始Ark数据' })),
|
||||
}),
|
||||
], { description: '小程序Ark参数' });
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
data: Type.Any({ description: 'Ark数据' }),
|
||||
}, { description: '获取小程序Ark结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetMiniAppArk extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.GetMiniAppArk;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema; override actionSummary = '获取小程序 Ark';
|
||||
override actionTags = ['系统扩展'];
|
||||
override payloadExample = {
|
||||
type: 'bili',
|
||||
title: '测试标题',
|
||||
desc: '测试描述',
|
||||
picUrl: 'http://example.com/pic.jpg',
|
||||
jumpUrl: 'http://example.com',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
data: {
|
||||
ark: 'ark_content',
|
||||
},
|
||||
};
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
let reqParam: MiniAppReqParams;
|
||||
const customParams: MiniAppReqCustomParams = {
|
||||
title: payload.title,
|
||||
desc: payload.desc,
|
||||
picUrl: payload.picUrl,
|
||||
jumpUrl: payload.jumpUrl,
|
||||
webUrl: payload.webUrl ?? '',
|
||||
};
|
||||
if ('type' in payload) {
|
||||
const template = MiniAppInfo.get(payload.type)?.template;
|
||||
if (!template) {
|
||||
throw new Error('未知的模板类型');
|
||||
}
|
||||
reqParam = MiniAppInfoHelper.generateReq(customParams, template);
|
||||
} else {
|
||||
const { appId, scene, iconUrl, templateType, businessType, verType, shareType, versionId, withShareTicket } = payload;
|
||||
reqParam = MiniAppInfoHelper.generateReq(
|
||||
customParams,
|
||||
{
|
||||
sdkId: payload.sdkId ?? MiniAppInfo.sdkId,
|
||||
appId,
|
||||
scene: +scene,
|
||||
iconUrl,
|
||||
templateType: +templateType,
|
||||
businessType: +businessType,
|
||||
verType: +verType,
|
||||
shareType: +shareType,
|
||||
versionId,
|
||||
withShareTicket: +withShareTicket,
|
||||
}
|
||||
);
|
||||
}
|
||||
const arkData = await this.core.apis.PacketApi.pkt.operation.GetMiniAppAdaptShareInfo(reqParam);
|
||||
return {
|
||||
data: payload.rawArkData === 'true' ? arkData : MiniAppInfoHelper.RawToSend(arkData),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
user_id: Type.Optional(Type.String({ description: 'QQ号' })),
|
||||
start: Type.Union([Type.Number(), Type.String()], { default: 0, description: '起始位置' }),
|
||||
count: Type.Union([Type.Number(), Type.String()], { default: 10, description: '获取数量' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
uid: Type.String({ description: '用户UID' }),
|
||||
time: Type.String({ description: '时间' }),
|
||||
favoriteInfo: Type.Object({
|
||||
userInfos: Type.Array(Type.Any(), { description: '点赞用户信息' }),
|
||||
total_count: Type.Number({ description: '总点赞数' }),
|
||||
last_time: Type.Number({ description: '最后点赞时间' }),
|
||||
today_count: Type.Number({ description: '今日点赞数' }),
|
||||
}),
|
||||
voteInfo: Type.Object({
|
||||
total_count: Type.Number({ description: '总点赞数' }),
|
||||
new_count: Type.Number({ description: '新增点赞数' }),
|
||||
new_nearby_count: Type.Number({ description: '新增附近点赞数' }),
|
||||
last_visit_time: Type.Number({ description: '最后访问时间' }),
|
||||
userInfos: Type.Array(Type.Any(), { description: '点赞用户信息' }),
|
||||
}),
|
||||
}, { description: '点赞详情' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetProfileLike extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.GetProfileLike;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '获取资料点赞';
|
||||
override actionTags = ['用户扩展'];
|
||||
override payloadExample = {
|
||||
user_id: '123456789',
|
||||
start: 0,
|
||||
count: 10,
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
uid: 'u_123',
|
||||
time: '1734567890',
|
||||
favoriteInfo: {
|
||||
userInfos: [],
|
||||
total_count: 10,
|
||||
last_time: 1734567890,
|
||||
today_count: 5,
|
||||
},
|
||||
voteInfo: {
|
||||
total_count: 100,
|
||||
new_count: 2,
|
||||
new_nearby_count: 0,
|
||||
last_visit_time: 1734567890,
|
||||
userInfos: [],
|
||||
},
|
||||
};
|
||||
|
||||
async _handle (payload: PayloadType): Promise<ReturnType> {
|
||||
const isSelf = this.core.selfInfo.uin === payload.user_id || !payload.user_id;
|
||||
const userUid = isSelf || !payload.user_id ? this.core.selfInfo.uid : await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
|
||||
const type = isSelf ? 2 : 1;
|
||||
const ret = await this.core.apis.UserApi.getProfileLike(userUid ?? this.core.selfInfo.uid, +payload.start, +payload.count, type);
|
||||
const data = ret.info.userLikeInfos[0];
|
||||
if (!data) {
|
||||
throw new Error('get info error');
|
||||
}
|
||||
for (const item of data.voteInfo.userInfos) {
|
||||
item.uin = +((await this.core.apis.UserApi.getUinByUidV2(item.uid)) ?? '');
|
||||
}
|
||||
for (const item of data.favoriteInfo.userInfos) {
|
||||
item.uin = +((await this.core.apis.UserApi.getUinByUidV2(item.uid)) ?? '');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
attach_info: Type.Optional(Type.String({ default: '', description: '附加信息(用于分页,从上一次返回结果中获取)' })),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
album_list: Type.Array(Type.Any(), { description: '群相册列表' }),
|
||||
attach_info: Type.String({ description: '分页附加信息,传入下一次请求以获取更多数据' }),
|
||||
has_more: Type.Boolean({ description: '是否有更多数据' }),
|
||||
}, { description: '群相册列表响应' });
|
||||
|
||||
type GetQunAlbumListReturn = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetQunAlbumList extends OneBotAction<PayloadType, GetQunAlbumListReturn> {
|
||||
override actionName = ActionName.GetQunAlbumList;
|
||||
override actionSummary = '获取群相册列表';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
attach_info: '',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
album_list: [
|
||||
{
|
||||
album_id: 'album_1',
|
||||
album_name: '测试相册',
|
||||
cover_url: 'http://example.com/cover.jpg',
|
||||
create_time: 1734567890,
|
||||
},
|
||||
],
|
||||
attach_info: '',
|
||||
has_more: false,
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType): Promise<GetQunAlbumListReturn> {
|
||||
const resp = (await this.core.apis.WebApi.getAlbumListByNTQQ(payload.group_id, payload.attach_info)).response;
|
||||
return {
|
||||
album_list: resp.album_list,
|
||||
attach_info: resp.attach_info,
|
||||
has_more: resp.has_more,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
|
||||
const ReturnSchema = Type.Array(Type.Any(), { description: 'Rkey列表' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetRkey extends GetPacketStatusDepends<void, ReturnType> {
|
||||
override actionName = ActionName.GetRkey;
|
||||
override payloadSchema = Type.Object({});
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '获取 RKey';
|
||||
override actionTags = ['系统扩展'];
|
||||
override payloadExample = {};
|
||||
override returnExample = [
|
||||
{
|
||||
key: 'rkey_value',
|
||||
expired: 1734567890,
|
||||
},
|
||||
];
|
||||
|
||||
async _handle () {
|
||||
return await this.core.apis.PacketApi.pkt.operation.FetchRkey();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
|
||||
const ReturnSchema = Type.Array(Type.Any(), { description: '机器人Uin范围列表' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetRobotUinRange extends OneBotAction<void, ReturnType> {
|
||||
override actionName = ActionName.GetRobotUinRange;
|
||||
override actionSummary = '获取机器人 UIN 范围';
|
||||
override actionTags = ['系统扩展'];
|
||||
override payloadExample = {};
|
||||
override returnExample = [
|
||||
{ minUin: '12345678', maxUin: '87654321' },
|
||||
];
|
||||
|
||||
override payloadSchema = Type.Object({});
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle () {
|
||||
return await this.core.apis.UserApi.getRobotUinRange();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { PacketBuf } from 'napcat-core/packet/transformer/base';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { ProtoBuf, ProtoBufBase, PBUint32, PBString } from 'napcat.protobuf';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
|
||||
const ReturnSchema = Type.Array(
|
||||
Type.Object({
|
||||
uin: Type.Number({ description: 'QQ号' }),
|
||||
uid: Type.String({ description: '用户UID' }),
|
||||
nick_name: Type.String({ description: '昵称' }),
|
||||
age: Type.Number({ description: '年龄' }),
|
||||
source: Type.String({ description: '来源' }),
|
||||
}),
|
||||
{ description: '单向好友列表' }
|
||||
);
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetUnidirectionalFriendList extends OneBotAction<void, ReturnType> {
|
||||
override actionName = ActionName.GetUnidirectionalFriendList;
|
||||
override payloadSchema = Type.Object({});
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '获取单向好友列表';
|
||||
override actionTags = ['用户扩展'];
|
||||
override payloadExample = {};
|
||||
override returnExample = [
|
||||
{
|
||||
uin: 123456789,
|
||||
uid: 'u_123',
|
||||
nick_name: '单向好友',
|
||||
age: 20,
|
||||
source: '来源',
|
||||
},
|
||||
];
|
||||
|
||||
async pack_data (data: string): Promise<Uint8Array> {
|
||||
return ProtoBuf(class extends ProtoBufBase {
|
||||
type = PBUint32(2, false, 0);
|
||||
data = PBString(3, false, data);
|
||||
}).encode();
|
||||
}
|
||||
|
||||
async _handle (): Promise<ReturnType> {
|
||||
const self_id = this.core.selfInfo.uin;
|
||||
const req_json = {
|
||||
uint64_uin: self_id,
|
||||
uint64_top: 0,
|
||||
uint32_req_num: 99,
|
||||
bytes_cookies: '',
|
||||
};
|
||||
const packed_data = await this.pack_data(JSON.stringify(req_json));
|
||||
const data = Buffer.from(packed_data);
|
||||
const rsq = { cmd: 'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0', data: data as unknown as PacketBuf };
|
||||
const rsp_data = await this.core.apis.PacketApi.pkt.operation.sendPacket(rsq, true);
|
||||
const block_json = ProtoBuf(class extends ProtoBufBase { data = PBString(4); }).decode(rsp_data);
|
||||
interface BlockItem {
|
||||
uint64_uin: number;
|
||||
str_uid: string;
|
||||
bytes_nick: string;
|
||||
uint32_age: number;
|
||||
bytes_source: string;
|
||||
}
|
||||
const block_data: { rpt_block_list: BlockItem[]; } = JSON.parse(block_json.data);
|
||||
const block_list = block_data.rpt_block_list;
|
||||
|
||||
return block_list.map((block) => ({
|
||||
uin: block.uint64_uin,
|
||||
uid: block.str_uid,
|
||||
nick_name: Buffer.from(block.bytes_nick, 'base64').toString(),
|
||||
age: block.uint32_age,
|
||||
source: Buffer.from(block.bytes_source, 'base64').toString(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
user_id: Type.String({ description: 'QQ号' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
status: Type.Number({ description: '在线状态' }),
|
||||
ext_status: Type.Number({ description: '扩展状态' }),
|
||||
}, { description: '用户状态' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class GetUserStatus extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.GetUserStatus;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '获取用户在线状态';
|
||||
override actionTags = ['系统扩展'];
|
||||
override payloadExample = {
|
||||
user_id: '123456789',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
status: 10,
|
||||
ext_status: 0,
|
||||
};
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
const res = await this.core.apis.PacketApi.pkt.operation.GetStrangerStatus(+payload.user_id);
|
||||
if (!res) {
|
||||
throw new Error('无法获取用户状态');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { FileNapCatOneBotUUID } from 'napcat-common/src/file-uuid';
|
||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
file_id: Type.String({ description: '文件ID' }),
|
||||
current_parent_directory: Type.String({ description: '当前父目录' }),
|
||||
target_parent_directory: Type.String({ description: '目标父目录' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
ok: Type.Boolean({ description: '是否成功' }),
|
||||
}, { description: '移动文件结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class MoveGroupFile extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.MoveGroupFile;
|
||||
override actionSummary = '移动群文件';
|
||||
override actionTags = ['文件扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
file_id: '/file_id',
|
||||
current_parent_directory: '/current_folder_id',
|
||||
target_parent_directory: '/target_folder_id',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
ok: true,
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
||||
if (contextMsgFile?.fileUUID) {
|
||||
await this.core.apis.PacketApi.pkt.operation.MoveGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.target_parent_directory);
|
||||
return {
|
||||
ok: true,
|
||||
};
|
||||
}
|
||||
throw new Error('real fileUUID not found!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { checkFileExist, uriToLocalFile } from 'napcat-common/src/file';
|
||||
import fs from 'fs';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
image: Type.String({ description: '图片路径、URL或Base64' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Any({ description: 'OCR结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
class OCRImageBase extends OneBotAction<PayloadType, ReturnType> {
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '图片 OCR 识别';
|
||||
override actionDescription = '识别图片中的文字内容(仅Windows端支持)';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = ExtendsActionsExamples.OCRImage.payload;
|
||||
override returnExample = ExtendsActionsExamples.OCRImage.response;
|
||||
|
||||
async _handle (payload: PayloadType): Promise<ReturnType> {
|
||||
const { path, success, isLocal } = await uriToLocalFile(this.core.NapCatTempPath, payload.image);
|
||||
if (!success) {
|
||||
throw new Error(`OCR ${payload.image}失败, image字段可能格式不正确`);
|
||||
}
|
||||
if (path) {
|
||||
try {
|
||||
await checkFileExist(path, 5000); // 避免崩溃
|
||||
const ret = await this.core.apis.SystemApi.ocrImage(path);
|
||||
if (!ret) {
|
||||
throw new Error(`OCR ${payload.image}失败`);
|
||||
}
|
||||
return ret.result;
|
||||
} finally {
|
||||
if (!isLocal) fs.unlink(path, () => { });
|
||||
}
|
||||
}
|
||||
throw new Error(`OCR ${payload.image}失败, 文件可能不存在`);
|
||||
}
|
||||
}
|
||||
|
||||
export class OCRImage extends OCRImageBase {
|
||||
override actionName = ActionName.OCRImage;
|
||||
override actionSummary = '图片 OCR 识别';
|
||||
}
|
||||
|
||||
export class IOCRImage extends OCRImageBase {
|
||||
override actionName = ActionName.IOCRImage;
|
||||
override actionSummary = '图片 OCR 识别 (内部)';
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { FileNapCatOneBotUUID } from 'napcat-common/src/file-uuid';
|
||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
file_id: Type.String({ description: '文件ID' }),
|
||||
current_parent_directory: Type.String({ description: '当前父目录' }),
|
||||
new_name: Type.String({ description: '新文件名' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
ok: Type.Boolean({ description: '是否成功' }),
|
||||
}, { description: '重命名文件结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class RenameGroupFile extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.RenameGroupFile;
|
||||
override actionSummary = '重命名群文件';
|
||||
override actionTags = ['文件扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
file_id: '/file_id',
|
||||
current_parent_directory: '/',
|
||||
new_name: 'new_name.jpg',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
ok: true,
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
||||
if (contextMsgFile?.fileUUID) {
|
||||
await this.core.apis.PacketApi.pkt.operation.RenameGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.new_name);
|
||||
return {
|
||||
ok: true,
|
||||
};
|
||||
}
|
||||
throw new Error('real fileUUID not found!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { PacketBuf } from 'napcat-core/packet/transformer/base';
|
||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
cmd: Type.String({ description: '命令字' }),
|
||||
data: Type.String({ description: '十六进制数据' }),
|
||||
rsp: Type.Union([Type.String(), Type.Boolean()], { default: true, description: '是否等待响应' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Union([Type.String({ description: '响应十六进制数据' }), Type.Undefined()], { description: '发包结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class SendPacket extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionName = ActionName.SendPacket;
|
||||
override actionSummary = '发送原始数据包';
|
||||
override actionTags = ['系统扩展'];
|
||||
override payloadExample = {
|
||||
cmd: 'Example.Cmd',
|
||||
data: '123456',
|
||||
rsp: true,
|
||||
};
|
||||
|
||||
override returnExample = '123456';
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
const rsp = typeof payload.rsp === 'boolean' ? payload.rsp : payload.rsp === 'true';
|
||||
const packetData = Buffer.from(payload.data, 'hex') as unknown as PacketBuf;
|
||||
const data = await this.core.apis.PacketApi.pkt.operation.sendPacket({ cmd: payload.cmd, data: packetData }, rsp);
|
||||
return typeof data === 'object' ? data.toString('hex') : undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { uriToLocalFile } from 'napcat-common/src/file';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { unlink } from 'node:fs/promises';
|
||||
import { parseQzonePublishTime } from '@/napcat-core/data/qzone';
|
||||
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
|
||||
import { checkQzoneFields, qzoneIdentity, qzoneImages, qzoneInteger, qzonePayload, qzoneText, qzoneUin, SlQzoneTextSchema } from './qzone/common-snowluma';
|
||||
|
||||
const ValidUgcRights = [1, 4, 16, 64, 128] as const;
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
content: SlQzoneTextSchema,
|
||||
images: Type.Optional(Type.Array(Type.String(), { description: '图片数组, 支持 file:// http(s):// base64:// , 自动上传' })),
|
||||
ugc_right: Type.Optional(Type.Union([Type.Number(), Type.String()], { default: 1, description: '查看权限: 1所有人可见 4好友可见 16部分好友可见 64仅自己可见 128部分好友不可见' })),
|
||||
target_uins: Type.Optional(Type.Array(Type.Union([Type.Number(), Type.String()]), { description: 'ugc_right为16/128时, 权限作用的QQ号数组' })),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
tid: Type.String({ description: '说说Tid' }),
|
||||
time: Type.Integer({ minimum: 0, description: '发表时间(Unix秒);上游未提供时为0,表示未知' }),
|
||||
}, { description: '发说说结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class SendQzoneMsg extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.SendQzoneMsg;
|
||||
override actionSummary = '发表QQ空间说说';
|
||||
override actionDescription = '在QQ空间(Qzone)发表说说, 支持纯文字或带(多)图, 可设置查看权限。纯文字使用QQ内置空间接口, 图片保留原有上传和发表方式';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = ExtendsActionsExamples.SendQzoneMsg.payload;
|
||||
override returnExample = { ...ExtendsActionsExamples.SendQzoneMsg.response, time: 0 };
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
private normalize (payload: PayloadType) {
|
||||
checkQzoneFields(qzonePayload(payload), ['content', 'images', 'ugc_right', 'target_uins'], false);
|
||||
const images = qzoneImages(payload.images);
|
||||
// 原有带图发表允许空配文,兼容既有调用。
|
||||
const content = payload.content === '' && images?.length ? '' : qzoneText(payload.content, 'content');
|
||||
const ugcRight = qzoneInteger(payload.ugc_right, 'ugc_right', 1, 1);
|
||||
if (!ValidUgcRights.includes(ugcRight as typeof ValidUgcRights[number])) {
|
||||
throw new Error(`ugc_right 参数不合法, 允许的值为: ${ValidUgcRights.join(', ')}`);
|
||||
}
|
||||
if (payload.target_uins !== undefined && !Array.isArray(payload.target_uins)) throw new Error('target_uins 必须为有效QQ号数组');
|
||||
const targetUins = payload.target_uins?.map((uin) => {
|
||||
const target = qzoneUin(uin, 'target_uins');
|
||||
if (target === undefined) throw new Error('target_uins 必须为有效QQ号数组');
|
||||
return target;
|
||||
});
|
||||
if ((ugcRight === 16 || ugcRight === 128) && (!targetUins || targetUins.length === 0)) {
|
||||
throw new Error('ugc_right 为 16 或 128 时, target_uins 不能为空');
|
||||
}
|
||||
|
||||
return { content, images, ugcRight, targetUins };
|
||||
}
|
||||
|
||||
protected override async check (payload: PayloadType): Promise<BaseCheckResult> {
|
||||
try { this.normalize(payload); } catch (error) { return { valid: false, message: (error as Error).message }; }
|
||||
return await super.check(payload);
|
||||
}
|
||||
|
||||
private published (result: { tid: string; time?: number; }): ReturnType {
|
||||
return { tid: qzoneIdentity(result.tid, '返回的tid'), time: parseQzonePublishTime(result.time) };
|
||||
}
|
||||
|
||||
async _handle (payload: PayloadType): Promise<ReturnType> {
|
||||
const { content, images, ugcRight, targetUins } = this.normalize(payload);
|
||||
if (!images?.length) return this.published(await this.core.apis.QzoneApi.publishMsg(content, ugcRight, targetUins));
|
||||
|
||||
const originalAccount = { uin: this.core.selfInfo?.uin, uid: this.core.selfInfo?.uid };
|
||||
if (!originalAccount.uin || !originalAccount.uid) throw new Error('QQ空间图片说说需要已登录的当前账号');
|
||||
const assertAccount = () => {
|
||||
if (this.core.selfInfo?.uin !== originalAccount.uin || this.core.selfInfo?.uid !== originalAccount.uid) {
|
||||
throw new Error('QQ账号已切换,图片说说发表已停止,请重新确认后操作');
|
||||
}
|
||||
};
|
||||
const richvals: string[] = [];
|
||||
const cleanupPaths: string[] = [];
|
||||
try {
|
||||
for (const image of images) {
|
||||
assertAccount();
|
||||
const downloadResult = await uriToLocalFile(this.core.NapCatTempPath, image);
|
||||
if (!downloadResult.success) {
|
||||
throw new Error(`图片${image}处理失败, ${downloadResult.errMsg}`);
|
||||
}
|
||||
if (!downloadResult.isLocal) {
|
||||
cleanupPaths.push(downloadResult.path);
|
||||
}
|
||||
assertAccount();
|
||||
const base64 = readFileSync(downloadResult.path).toString('base64');
|
||||
const richval = await this.core.apis.WebApi.uploadImageToQzone(base64);
|
||||
assertAccount();
|
||||
richvals.push(richval);
|
||||
}
|
||||
|
||||
assertAccount();
|
||||
const result = await this.core.apis.WebApi.publishQzoneMsg(content, richvals, ugcRight, targetUins);
|
||||
return this.published(result);
|
||||
} finally {
|
||||
for (const p of cleanupPaths) {
|
||||
if (existsSync(p)) {
|
||||
await unlink(p).catch(() => { });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
face_id: Type.Union([Type.Number(), Type.String()], { description: '图标ID' }), // 参考 face_config.json 的 QSid
|
||||
face_type: Type.Union([Type.Number(), Type.String()], { default: '1', description: '图标类型' }),
|
||||
wording: Type.String({ default: ' ', description: '状态文字内容' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.String({ description: '错误信息(如果有)' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class SetDiyOnlineStatus extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.SetDiyOnlineStatus;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema; override actionSummary = '设置自定义在线状态';
|
||||
override actionDescription = '设置自定义在线状态';
|
||||
override actionTags = ['用户扩展'];
|
||||
override payloadExample = {
|
||||
face_id: '123',
|
||||
face_type: '1',
|
||||
wording: '自定义状态',
|
||||
};
|
||||
|
||||
override returnExample = '';
|
||||
async _handle (payload: PayloadType) {
|
||||
const ret = await this.core.apis.UserApi.setDiySelfOnlineStatus(
|
||||
payload.face_id.toString(),
|
||||
payload.wording,
|
||||
payload.face_type.toString()
|
||||
);
|
||||
if (ret.result !== 0) {
|
||||
throw new Error('设置在线状态失败');
|
||||
}
|
||||
return ret.errMsg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
add_type: Type.Number({ description: '加群方式' }),
|
||||
group_question: Type.Optional(Type.String({ description: '加群问题' })),
|
||||
group_answer: Type.Optional(Type.String({ description: '加群答案' })),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Null({ description: '返回结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export default class SetGroupAddOption extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.SetGroupAddOption;
|
||||
override actionSummary = '设置群加群选项';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
add_type: 1,
|
||||
};
|
||||
|
||||
override returnExample = null;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
async _handle (payload: PayloadType): Promise<ReturnType> {
|
||||
const ret = await this.core.apis.GroupApi.setGroupAddOption(payload.group_id, {
|
||||
addOption: payload.add_type,
|
||||
groupQuestion: payload.group_question,
|
||||
groupAnswer: payload.group_answer,
|
||||
});
|
||||
if (ret.result !== 0) {
|
||||
throw new Error(`设置群添加选项失败, ${ret.result}:${ret.errMsg}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
album_id: Type.String({ description: '相册ID' }),
|
||||
batch_id: Type.String({ description: 'batch_id' }),
|
||||
lloc: Type.Optional(Type.String({ description: 'lloc,若对整个上传操作则不填' })),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Any({ description: '操作结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class SetGroupAlbumMediaLike extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.SetGroupAlbumMediaLike;
|
||||
override actionSummary = '点赞群相册媒体';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
album_id: 'album_id_123',
|
||||
batch_id: '112233',
|
||||
lloc: 'aabbcc12213123',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
result: {},
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return await this.core.apis.WebApi.doAlbumMediaLikeByNTQQ(
|
||||
payload.group_id,
|
||||
payload.album_id,
|
||||
payload.batch_id,
|
||||
payload.lloc,
|
||||
true // isLike = true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class CancelGroupAlbumMediaLike extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.CancelGroupAlbumMediaLike; // 注意:需要在 router/index.ts 的 ActionName 枚举中补充该定义
|
||||
override actionSummary = '取消点赞群相册媒体';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
album_id: 'album_id',
|
||||
batch_id: '112233',
|
||||
lloc: 'aabbcc',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
result: {},
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return await this.core.apis.WebApi.doAlbumMediaLikeByNTQQ(
|
||||
payload.group_id,
|
||||
payload.album_id,
|
||||
payload.batch_id,
|
||||
payload.lloc,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
user_id: Type.Array(Type.String(), { description: 'QQ号列表' }),
|
||||
reject_add_request: Type.Optional(Type.Union([Type.Boolean(), Type.String()], { description: '是否拒绝加群请求' })),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Null({ description: '返回结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export default class SetGroupKickMembers extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.SetGroupKickMembers;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '批量踢出群成员';
|
||||
override actionDescription = '从指定群聊中批量踢出多个成员';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = ExtendsActionsExamples.SetGroupKickMembers.payload;
|
||||
override returnExample = ExtendsActionsExamples.SetGroupKickMembers.response;
|
||||
|
||||
async _handle (payload: PayloadType): Promise<ReturnType> {
|
||||
const rejectReq = payload.reject_add_request?.toString() === 'true';
|
||||
const uids: string[] = await Promise.all(payload.user_id.map(async uin => await this.core.apis.UserApi.getUidByUinV2(uin)));
|
||||
await this.core.apis.GroupApi.kickMember(payload.group_id.toString(), uids.filter(uid => !!uid), rejectReq);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
remark: Type.String({ description: '备注' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Null({ description: '返回结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export default class SetGroupRemark extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.SetGroupRemark;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '设置群备注';
|
||||
override actionDescription = '设置群备注';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
remark: '测试群备注',
|
||||
};
|
||||
|
||||
override returnExample = null;
|
||||
|
||||
async _handle (payload: PayloadType): Promise<ReturnType> {
|
||||
const ret = await this.core.apis.GroupApi.setGroupRemark(payload.group_id, payload.remark);
|
||||
if (ret.result !== 0) {
|
||||
throw new Error(`设置群备注失败, ${ret.result}:${ret.errMsg}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
robot_member_switch: Type.Optional(Type.Number({ description: '机器人成员开关' })),
|
||||
robot_member_examine: Type.Optional(Type.Number({ description: '机器人成员审核' })),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Null({ description: '返回结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export default class SetGroupRobotAddOption extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.SetGroupRobotAddOption;
|
||||
override actionSummary = '设置群机器人加群选项';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
};
|
||||
|
||||
override returnExample = null;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
async _handle (payload: PayloadType): Promise<ReturnType> {
|
||||
const ret = await this.core.apis.GroupApi.setGroupRobotAddOption(
|
||||
payload.group_id,
|
||||
payload.robot_member_switch,
|
||||
payload.robot_member_examine
|
||||
);
|
||||
if (ret.result !== 0) {
|
||||
throw new Error(`设置群机器人添加选项失败, ${ret.result}:${ret.errMsg}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
no_code_finger_open: Type.Optional(Type.Number({ description: '未知' })),
|
||||
no_finger_open: Type.Optional(Type.Number({ description: '未知' })),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Null({ description: '返回结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export default class SetGroupSearch extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.SetGroupSearch;
|
||||
override actionSummary = '设置群搜索选项';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
};
|
||||
|
||||
override returnExample = null;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
async _handle (payload: PayloadType): Promise<ReturnType> {
|
||||
const ret = await this.core.apis.GroupApi.setGroupSearch(payload.group_id, {
|
||||
noCodeFingerOpenFlag: payload.no_code_finger_open,
|
||||
noFingerOpenFlag: payload.no_finger_open,
|
||||
});
|
||||
if (ret.result !== 0) {
|
||||
throw new Error(`设置群搜索失败, ${ret.result}:${ret.errMsg}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Void({ description: '打卡结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
class SetGroupSignBase extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '群打卡';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456789',
|
||||
};
|
||||
|
||||
override returnExample = null;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return await this.core.apis.PacketApi.pkt.operation.GroupSign(+payload.group_id);
|
||||
}
|
||||
}
|
||||
|
||||
export class SetGroupSign extends SetGroupSignBase {
|
||||
override actionName = ActionName.SetGroupSign;
|
||||
}
|
||||
|
||||
export class SendGroupSign extends SetGroupSignBase {
|
||||
override actionName = ActionName.SendGroupSign;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { ChatType } from 'napcat-core';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
user_id: Type.String({ description: 'QQ号' }),
|
||||
event_type: Type.Number({ description: '事件类型' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Any({ description: '设置结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class SetInputStatus extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.SetInputStatus;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '设置输入状态';
|
||||
override actionTags = ['系统扩展'];
|
||||
override payloadExample = {
|
||||
user_id: '123456789',
|
||||
event_type: 1,
|
||||
};
|
||||
|
||||
override returnExample = null;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
|
||||
if (!uid) throw new Error('uid is empty');
|
||||
const peer = {
|
||||
chatType: ChatType.KCHATTYPEC2C,
|
||||
peerUid: uid,
|
||||
};
|
||||
return await this.core.apis.MsgApi.sendShowInputStatusReq(peer, payload.event_type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
longNick: Type.String({ description: '签名内容' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Any({ description: '设置结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class SetLongNick extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.SetLongNick;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '设置个性签名';
|
||||
override actionDescription = '修改当前登录帐号的个性签名';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = ExtendsActionsExamples.SetLongNick.payload;
|
||||
override returnExample = ExtendsActionsExamples.SetLongNick.response;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return await this.core.apis.UserApi.setLongNick(payload.longNick);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
status: Type.Union([Type.Number(), Type.String()], { description: '在线状态' }),
|
||||
ext_status: Type.Union([Type.Number(), Type.String()], { description: '扩展状态' }),
|
||||
battery_status: Type.Union([Type.Number(), Type.String()], { description: '电量状态' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Null({ description: '设置结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class SetOnlineStatus extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.SetOnlineStatus;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '设置在线状态';
|
||||
override actionDescription = statusText;
|
||||
override actionTags = ['系统扩展'];
|
||||
override payloadExample = {
|
||||
status: 11,
|
||||
ext_status: 0,
|
||||
battery_status: 100,
|
||||
};
|
||||
|
||||
override returnExample = null;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
const ret = await this.core.apis.UserApi.setSelfOnlineStatus(
|
||||
+payload.status,
|
||||
+payload.ext_status,
|
||||
+payload.battery_status
|
||||
);
|
||||
if (ret.result !== 0) {
|
||||
throw new Error('设置在线状态失败');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const statusText = `
|
||||
## 状态列表
|
||||
|
||||
### 在线
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 0, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### Q我吧
|
||||
\`\`\`json5;
|
||||
{ "status": 60, "ext_status": 0, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 离开
|
||||
\`\`\`json5;
|
||||
{ "status": 30, "ext_status": 0, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 忙碌
|
||||
\`\`\`json5;
|
||||
{ "status": 50, "ext_status": 0, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 请勿打扰
|
||||
\`\`\`json5;
|
||||
{ "status": 70, "ext_status": 0, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 隐身
|
||||
\`\`\`json5;
|
||||
{ "status": 40, "ext_status": 0, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 听歌中
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1028, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 春日限定
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 2037, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 一起元梦
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 2025, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 求星搭子
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 2026, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 被掏空
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 2014, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 今日天气
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1030, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 我crash了
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 2019, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 爱你
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 2006, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 恋爱中
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1051, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 好运锦鲤
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1071, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 水逆退散
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1201, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 嗨到飞起
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1056, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 元气满满
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1058, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 宝宝认证
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1070, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 一言难尽
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1063, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 难得糊涂
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 2001, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### emo中
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1401, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 我太难了
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1062, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 我想开了
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 2013, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 我没事
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1052, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 想静静
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1061, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 悠哉哉
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1059, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 去旅行
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 2015, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 信号弱
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1011, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 出去浪
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 2003, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 肝作业
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 2012, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 学习中
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1018, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 搬砖中
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 2023, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 摸鱼中
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1300, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 无聊中
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1060, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### timi中
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1027, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 睡觉中
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1016, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 熬夜中
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1032, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 追剧中
|
||||
\`\`\`json5;
|
||||
{ "status": 10, "ext_status": 1021, "battery_status": 0; }
|
||||
\`\`\`
|
||||
|
||||
### 我的电量
|
||||
\`\`\`json5;
|
||||
{
|
||||
"status": 10,
|
||||
"ext_status": 1000,
|
||||
"battery_status": 0;
|
||||
}
|
||||
\`\`\`
|
||||
`;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import fs from 'node:fs/promises';
|
||||
import { checkFileExist, uriToLocalFile } from 'napcat-common/src/file';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
file: Type.String({ description: '图片路径、URL或Base64' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Null({ description: '设置结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export default class SetAvatar extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.SetQQAvatar;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '设置QQ头像';
|
||||
override actionDescription = '修改当前账号的QQ头像';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = ExtendsActionsExamples.SetQQAvatar.payload;
|
||||
override returnExample = ExtendsActionsExamples.SetQQAvatar.response;
|
||||
|
||||
async _handle (payload: PayloadType): Promise<ReturnType> {
|
||||
const { path, success, isLocal } = (await uriToLocalFile(this.core.NapCatTempPath, payload.file));
|
||||
if (!success) {
|
||||
throw new Error(`头像${payload.file}设置失败,file字段可能格式不正确`);
|
||||
}
|
||||
if (path) {
|
||||
await checkFileExist(path, 5000);// 避免崩溃
|
||||
const ret = await this.core.apis.UserApi.setQQAvatar(path);
|
||||
if (!isLocal) fs.unlink(path).catch(() => { });
|
||||
if (!ret) {
|
||||
throw new Error(`头像${payload.file}设置失败,api无返回`);
|
||||
}
|
||||
// log(`头像设置返回:${JSON.stringify(ret)}`)
|
||||
if (Number(ret.result) === 1004022) {
|
||||
throw new Error(`头像${payload.file}设置失败,文件可能不是图片格式`);
|
||||
} else if (ret.result !== 0) {
|
||||
throw new Error(`头像${payload.file}设置失败,未知的错误,${ret.result}:${ret.errMsg}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`头像${payload.file}设置失败,无法获取头像,文件可能不存在`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
user_id: Type.String({ description: 'QQ号' }),
|
||||
special_title: Type.String({ default: '', description: '专属头衔' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Void({ description: '设置结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class SetSpecialTitle extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.SetSpecialTitle;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '设置专属头衔';
|
||||
override actionDescription = '设置群聊中指定成员的专属头衔';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = ExtendsActionsExamples.SetSpecialTitle.payload;
|
||||
override returnExample = ExtendsActionsExamples.SetSpecialTitle.response;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
|
||||
if (!uid) throw new Error('User not found');
|
||||
await this.core.apis.PacketApi.pkt.operation.SetGroupSpecialTitle(+payload.group_id, uid, payload.special_title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
user_id: Type.Optional(Type.String({ description: 'QQ号' })),
|
||||
group_id: Type.Optional(Type.String({ description: '群号' })),
|
||||
phone_number: Type.String({ default: '', description: '手机号' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Any({ description: '分享结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class SharePeerBase extends OneBotAction<PayloadType, ReturnType> {
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '分享用户 (Ark)';
|
||||
override actionDescription = '获取用户推荐的 Ark 内容';
|
||||
override actionTags = ['消息扩展'];
|
||||
override payloadExample = {
|
||||
user_id: '123456',
|
||||
phone_number: '',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
ark: '...',
|
||||
};
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
if (payload.group_id) {
|
||||
return await this.core.apis.GroupApi.getGroupRecommendContactArkJson(payload.group_id.toString());
|
||||
} else if (payload.user_id) {
|
||||
return await this.core.apis.UserApi.getBuddyRecommendContactArkJson(payload.user_id.toString(), payload.phone_number);
|
||||
}
|
||||
throw new Error('group_id or user_id is required');
|
||||
}
|
||||
}
|
||||
|
||||
const PayloadSchemaGroupEx = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
});
|
||||
export class SharePeer extends SharePeerBase {
|
||||
override actionName = ActionName.SharePeer;
|
||||
}
|
||||
type PayloadTypeGroupEx = Static<typeof PayloadSchemaGroupEx>;
|
||||
|
||||
const ReturnSchemaGroupEx = Type.String({ description: 'Ark Json内容' });
|
||||
|
||||
type ReturnTypeGroupEx = Static<typeof ReturnSchemaGroupEx>;
|
||||
|
||||
export class ShareGroupExBase extends OneBotAction<PayloadTypeGroupEx, ReturnTypeGroupEx> {
|
||||
override payloadSchema = PayloadSchemaGroupEx;
|
||||
override returnSchema = ReturnSchemaGroupEx;
|
||||
override actionSummary = '分享群 (Ark)';
|
||||
override actionDescription = '获取群分享的 Ark 内容';
|
||||
override actionTags = ['消息扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
};
|
||||
|
||||
override returnExample = '{"app": "com.tencent.structmsg", ...}';
|
||||
|
||||
async _handle (payload: PayloadTypeGroupEx) {
|
||||
return await this.core.apis.GroupApi.getArkJsonGroupShare(payload.group_id.toString());
|
||||
}
|
||||
}
|
||||
export class ShareGroupEx extends ShareGroupExBase {
|
||||
override actionName = ActionName.ShareGroupEx;
|
||||
}
|
||||
export class SendGroupArkShare extends ShareGroupExBase {
|
||||
override actionName = ActionName.SendGroupArkShare;
|
||||
}
|
||||
|
||||
export class SendArkShare extends SharePeerBase {
|
||||
override actionName = ActionName.SendArkShare;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { FileNapCatOneBotUUID } from 'napcat-common/src/file-uuid';
|
||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
file_id: Type.String({ description: '文件ID' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
ok: Type.Boolean({ description: '是否成功' }),
|
||||
}, { description: '转发文件结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class TransGroupFile extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.TransGroupFile;
|
||||
override actionSummary = '传输群文件';
|
||||
override actionTags = ['文件扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
file_id: '/file_id',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
ok: true,
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
||||
if (contextMsgFile?.fileUUID) {
|
||||
const result = await this.core.apis.GroupApi.transGroupFile(payload.group_id.toString(), contextMsgFile.fileUUID);
|
||||
if (result.transGroupFileResult.result.retCode === 0) {
|
||||
return {
|
||||
ok: true,
|
||||
};
|
||||
}
|
||||
throw new Error(result.transGroupFileResult.result.retMsg);
|
||||
}
|
||||
throw new Error('real fileUUID not found!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
words: Type.Array(Type.String(), { description: '待翻译单词列表' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
words: Type.Array(Type.String(), { description: '翻译结果列表' }),
|
||||
}, { description: '翻译结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class TranslateEnWordToZn extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.TranslateEnWordToZn;
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override actionSummary = '英文单词翻译';
|
||||
override actionDescription = '将英文单词列表翻译为中文';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = ExtendsActionsExamples.TranslateEnWordToZn.payload;
|
||||
override returnExample = ExtendsActionsExamples.TranslateEnWordToZn.response;
|
||||
|
||||
async _handle (payload: PayloadType): Promise<ReturnType> {
|
||||
const ret = await this.core.apis.SystemApi.translateEnWordToZn(payload.words);
|
||||
if (ret.result !== 0) {
|
||||
throw new Error('翻译失败');
|
||||
}
|
||||
return {
|
||||
words: ret.words,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { uriToLocalFile } from 'napcat-common/src/file';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { unlink } from 'node:fs/promises';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
album_id: Type.String({ description: '相册ID' }),
|
||||
album_name: Type.String({ description: '相册名称' }),
|
||||
file: Type.String({ description: '图片路径、URL或Base64' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Any({ description: '上传结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class UploadImageToQunAlbum extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.UploadImageToQunAlbum;
|
||||
override actionSummary = '上传图片到群相册';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
album_id: 'album_id_1',
|
||||
album_name: '相册1',
|
||||
file: '/path/to/image.jpg',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
result: null,
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
const downloadResult = await uriToLocalFile(this.core.NapCatTempPath, payload.file);
|
||||
try {
|
||||
return await this.core.apis.WebApi.uploadImageToQunAlbum(payload.group_id, payload.album_id, payload.album_name, downloadResult.path);
|
||||
} finally {
|
||||
if (!downloadResult.isLocal && downloadResult.path && existsSync(downloadResult.path)) {
|
||||
await unlink(downloadResult.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { uriToLocalFile } from 'napcat-common/src/file';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { unlink } from 'node:fs/promises';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
album_id: Type.String({ description: '相册ID' }),
|
||||
album_name: Type.String({ description: '相册名称' }),
|
||||
files: Type.Array(Type.String(), { description: '图片路径、URL或Base64 数组,一次上传多张' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Any({ description: '上传结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class UploadImagesToQunAlbum extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.UploadImagesToQunAlbum;
|
||||
override actionSummary = '上传多张图片到群相册';
|
||||
override actionDescription = '一次上传多张图片,这些图片在群相册里显示为同一条上传';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
album_id: 'album_id_1',
|
||||
album_name: '相册1',
|
||||
files: ['/path/to/1.jpg', '/path/to/2.jpg'],
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
result: null,
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
if (!payload.files.length) throw new Error('files 不能为空');
|
||||
const localPaths: string[] = [];
|
||||
const tempPaths: string[] = [];
|
||||
try {
|
||||
for (const file of payload.files) {
|
||||
const downloadResult = await uriToLocalFile(this.core.NapCatTempPath, file);
|
||||
localPaths.push(downloadResult.path);
|
||||
if (!downloadResult.isLocal && downloadResult.path) {
|
||||
tempPaths.push(downloadResult.path);
|
||||
}
|
||||
}
|
||||
return await this.core.apis.WebApi.uploadImagesToQunAlbum(
|
||||
payload.group_id,
|
||||
payload.album_id,
|
||||
payload.album_name,
|
||||
localPaths
|
||||
);
|
||||
} finally {
|
||||
for (const path of tempPaths) {
|
||||
if (existsSync(path)) {
|
||||
await unlink(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { uriToLocalFile } from 'napcat-common/src/file';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { unlink } from 'node:fs/promises';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
group_id: Type.String({ description: '群号' }),
|
||||
album_id: Type.String({ description: '相册ID' }),
|
||||
album_name: Type.String({ description: '相册名称' }),
|
||||
file: Type.String({ description: '视频路径、URL或Base64' }),
|
||||
});
|
||||
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Any({ description: '上传结果' });
|
||||
|
||||
type ReturnType = Static<typeof ReturnSchema>;
|
||||
|
||||
export class UploadVideoToQunAlbum extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionName = ActionName.UploadVideoToQunAlbum;
|
||||
override actionSummary = '上传视频到群相册';
|
||||
override actionDescription = '上传视频到群相册,封面自动抽取首帧(失败时使用内置默认封面)';
|
||||
override actionTags = ['群组扩展'];
|
||||
override payloadExample = {
|
||||
group_id: '123456',
|
||||
album_id: 'album_id_1',
|
||||
album_name: '相册1',
|
||||
file: '/path/to/video.mp4',
|
||||
};
|
||||
|
||||
override returnExample = {
|
||||
result: null,
|
||||
};
|
||||
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
const downloadResult = await uriToLocalFile(this.core.NapCatTempPath, payload.file);
|
||||
try {
|
||||
return await this.core.apis.WebApi.uploadVideoToQunAlbum(
|
||||
payload.group_id,
|
||||
payload.album_id,
|
||||
payload.album_name,
|
||||
downloadResult.path
|
||||
);
|
||||
} finally {
|
||||
if (!downloadResult.isLocal && downloadResult.path && existsSync(downloadResult.path)) {
|
||||
await unlink(downloadResult.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { QzonePublicAction, QzonePublicContentSchema, QzonePublicFeedExample, QzonePublicFeedSchema } from './common';
|
||||
|
||||
const PayloadSchema = Type.Object({ feed: QzonePublicFeedSchema, content: QzonePublicContentSchema });
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
export class CommentQzonePublicFeed extends QzonePublicAction<PayloadType> {
|
||||
override actionName = ActionName.CommentQzonePublicFeed;
|
||||
override actionSummary = '评论空友爱看动态';
|
||||
override actionDescription = '对完整 feed 发表评论;content 接受非空文本或富文本对象数组,富文本字段原样保留';
|
||||
override payloadSchema = PayloadSchema;
|
||||
override payloadExample = { feed: QzonePublicFeedExample, content: '很有意思' };
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return this.core.apis.QzonePublicApi.addComment(payload.feed, payload.content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { QzonePublicAction, QzonePublicCommentExample, QzonePublicCommentSchema, QzonePublicFeedExample, QzonePublicFeedSchema } from './common';
|
||||
|
||||
const PayloadSchema = Type.Object({ feed: QzonePublicFeedSchema, comment: QzonePublicCommentSchema });
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
export class DeleteQzonePublicComment extends QzonePublicAction<PayloadType> {
|
||||
override actionName = ActionName.DeleteQzonePublicComment;
|
||||
override actionSummary = '删除空友爱看评论';
|
||||
override actionDescription = '传入完整 feed 及 comment 删除评论,操作权限由上游校验';
|
||||
override payloadSchema = PayloadSchema;
|
||||
override payloadExample = { feed: QzonePublicFeedExample, comment: QzonePublicCommentExample };
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return this.core.apis.QzonePublicApi.deleteComment(payload.feed, payload.comment);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import {
|
||||
QzonePublicAction,
|
||||
QzonePublicCommentExample,
|
||||
QzonePublicCommentSchema,
|
||||
QzonePublicFeedExample,
|
||||
QzonePublicFeedSchema,
|
||||
QzonePublicReplyExample,
|
||||
QzonePublicReplySchema,
|
||||
} from './common';
|
||||
|
||||
const PayloadSchema = Type.Object({ feed: QzonePublicFeedSchema, comment: QzonePublicCommentSchema, reply: QzonePublicReplySchema });
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
export class DeleteQzonePublicReply extends QzonePublicAction<PayloadType> {
|
||||
override actionName = ActionName.DeleteQzonePublicReply;
|
||||
override actionSummary = '删除空友爱看回复';
|
||||
override actionDescription = '传入完整 feed、父 comment 和目标 reply 删除回复,操作权限由上游校验';
|
||||
override payloadSchema = PayloadSchema;
|
||||
override payloadExample = { feed: QzonePublicFeedExample, comment: QzonePublicCommentExample, reply: QzonePublicReplyExample };
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return this.core.apis.QzonePublicApi.deleteReply(payload.feed, payload.comment, payload.reply);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { QzonePublicAction, QzonePublicFeedExample, QzonePublicFeedSchema } from './common';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
feed: QzonePublicFeedSchema,
|
||||
reason: Type.Optional(Type.String({ default: '', description: '转发附言,默认空字符串;数字或布尔值会转为文本' })),
|
||||
});
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
export class ForwardQzonePublicFeed extends QzonePublicAction<PayloadType> {
|
||||
override actionName = ActionName.ForwardQzonePublicFeed;
|
||||
override actionSummary = '转发空友爱看动态';
|
||||
override actionDescription = '将完整 feed 转发到自己的空间,可通过 reason 添加转发附言';
|
||||
override payloadSchema = PayloadSchema;
|
||||
override payloadExample = { feed: QzonePublicFeedExample, reason: '分享一条动态' };
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return this.core.apis.QzonePublicApi.forwardFeed(payload.feed, payload.reason ?? '');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { QzonePublicAction } from './common';
|
||||
|
||||
const PayloadSchema = Type.Object({});
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
export class GetQzonePublicCapabilities extends QzonePublicAction<PayloadType> {
|
||||
override actionName = ActionName.GetQzonePublicCapabilities;
|
||||
override actionSummary = '获取空友爱看接口能力';
|
||||
override actionDescription = '获取当前 QQ 版本和账号可用的空友爱看接口能力,不会执行写操作';
|
||||
override payloadSchema = PayloadSchema;
|
||||
override payloadExample = {};
|
||||
|
||||
async _handle () {
|
||||
return this.core.apis.QzonePublicApi.getCapabilities();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { QzonePublicAction, QzonePublicFeedExample, QzonePublicFeedSchema } from './common';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
feed: QzonePublicFeedSchema,
|
||||
cursor: Type.String({ minLength: 1, description: '动态详情或上一评论页返回的非空 attach_info;数字或布尔值会转为文本' }),
|
||||
});
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
export class GetQzonePublicComments extends QzonePublicAction<PayloadType> {
|
||||
override actionName = ActionName.GetQzonePublicComments;
|
||||
override actionSummary = '获取空友爱看评论';
|
||||
override actionDescription = '读取动态的后续评论;cursor 为必填非空字符串,使用动态详情或上一评论页的 attach_info,原始评论对象应完整保留';
|
||||
override payloadSchema = PayloadSchema;
|
||||
override payloadExample = { feed: QzonePublicFeedExample, cursor: 'detail-attach-info' };
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return this.core.apis.QzonePublicApi.getComments(payload.feed, payload.cursor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { QzonePublicAction, QzonePublicFeedExample, QzonePublicFeedSchema } from './common';
|
||||
|
||||
const PayloadSchema = Type.Object({ feed: QzonePublicFeedSchema });
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
export class GetQzonePublicFeedDetail extends QzonePublicAction<PayloadType> {
|
||||
override actionName = ActionName.GetQzonePublicFeedDetail;
|
||||
override actionSummary = '获取空友爱看动态详情';
|
||||
override actionDescription = '传入列表中的完整 feed 对象获取详情;继续读取评论时,将详情返回的 attach_info 作为 cursor';
|
||||
override payloadSchema = PayloadSchema;
|
||||
override payloadExample = { feed: QzonePublicFeedExample };
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return this.core.apis.QzonePublicApi.getFeedDetail(payload.feed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import type { QzonePublicFeedsPage } from '@/napcat-core/data/qzone-public';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { QzonePublicAction, QzonePublicFeedExample, QzonePublicObjectSchema } from './common';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
cursor: Type.Optional(Type.String({ minLength: 1, maxLength: 65536, description: '上一页返回的非空 next_cursor,首次请求可省略;数字或布尔值会转为文本' })),
|
||||
});
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
feeds: Type.Array(QzonePublicObjectSchema, { description: '完整动态对象,可直接传给详情和互动接口' }),
|
||||
has_more: Type.Boolean({ description: '是否还有下一页' }),
|
||||
next_cursor: Type.Union([Type.String(), Type.Null()], { description: '下一页游标,没有下一页时为 null' }),
|
||||
}, { additionalProperties: true, description: '动态列表分页结果,保留上游返回的额外字段' });
|
||||
|
||||
export class GetQzonePublicFeeds extends QzonePublicAction<PayloadType, QzonePublicFeedsPage> {
|
||||
override actionName = ActionName.GetQzonePublicFeeds;
|
||||
override actionSummary = '获取空友爱看动态';
|
||||
override actionDescription = '获取空友爱看动态列表;后续操作应直接传入 feeds 中的完整对象,翻页使用 next_cursor';
|
||||
override payloadSchema = PayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
override payloadExample = {};
|
||||
override returnExample = { feeds: [QzonePublicFeedExample], has_more: false, next_cursor: null };
|
||||
|
||||
async _handle (payload: PayloadType): Promise<QzonePublicFeedsPage> {
|
||||
return this.core.apis.QzonePublicApi.getFeeds(payload.cursor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import {
|
||||
parseQzonePublicPageSize,
|
||||
QzonePublicAction,
|
||||
QzonePublicCommentExample,
|
||||
QzonePublicCommentSchema,
|
||||
QzonePublicFeedExample,
|
||||
QzonePublicFeedSchema,
|
||||
} from './common';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
feed: QzonePublicFeedSchema,
|
||||
comment: QzonePublicCommentSchema,
|
||||
cursor: Type.Optional(Type.String({ default: '', description: '回复分页游标,默认为空字符串;空字符串时使用 comment.reply_attach_info;数字或布尔值会转为文本' })),
|
||||
page_size: Type.Optional(Type.Union([
|
||||
Type.Number({ minimum: 1 }), Type.String(),
|
||||
], { default: 20, description: '每页回复数量,默认 20;有限数字或非空数值字符串,向零截断后应大于等于 1' })),
|
||||
});
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
export class GetQzonePublicReplies extends QzonePublicAction<PayloadType> {
|
||||
override actionName = ActionName.GetQzonePublicReplies;
|
||||
override actionSummary = '获取空友爱看评论回复';
|
||||
override actionDescription = '传入完整动态及父评论读取回复分页;省略 cursor 时使用父评论中的 reply_attach_info';
|
||||
override payloadSchema = PayloadSchema;
|
||||
override payloadExample = { feed: QzonePublicFeedExample, comment: QzonePublicCommentExample, page_size: 20 };
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return this.core.apis.QzonePublicApi.getReplies(payload.feed, payload.comment, payload.cursor ?? '', parseQzonePublicPageSize(payload.page_size));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import {
|
||||
QzonePublicAction,
|
||||
QzonePublicCommentExample,
|
||||
QzonePublicCommentSchema,
|
||||
QzonePublicContentSchema,
|
||||
QzonePublicFeedExample,
|
||||
QzonePublicFeedSchema,
|
||||
QzonePublicReplySchema,
|
||||
} from './common';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
feed: QzonePublicFeedSchema,
|
||||
comment: QzonePublicCommentSchema,
|
||||
content: QzonePublicContentSchema,
|
||||
reply: Type.Optional(QzonePublicReplySchema),
|
||||
});
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
export class ReplyQzonePublicFeed extends QzonePublicAction<PayloadType> {
|
||||
override actionName = ActionName.ReplyQzonePublicFeed;
|
||||
override actionSummary = '回复空友爱看评论';
|
||||
override actionDescription = '传入完整 feed、父 comment 及非空 content 发表回复;需要回复已有回复时一并传入完整 reply';
|
||||
override payloadSchema = PayloadSchema;
|
||||
override payloadExample = { feed: QzonePublicFeedExample, comment: QzonePublicCommentExample, content: '谢谢分享' };
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return this.core.apis.QzonePublicApi.addReply(payload.feed, payload.comment, payload.content, payload.reply);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import {
|
||||
parseQzonePublicEnable,
|
||||
QzonePublicAction,
|
||||
QzonePublicCommentExample,
|
||||
QzonePublicCommentSchema,
|
||||
QzonePublicEnableSchema,
|
||||
QzonePublicFeedExample,
|
||||
QzonePublicFeedSchema,
|
||||
QzonePublicReplySchema,
|
||||
} from './common';
|
||||
|
||||
const PayloadSchema = Type.Object({
|
||||
feed: QzonePublicFeedSchema,
|
||||
comment: QzonePublicCommentSchema,
|
||||
reply: Type.Optional(QzonePublicReplySchema),
|
||||
enable: QzonePublicEnableSchema,
|
||||
});
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
export class SetQzonePublicCommentLike extends QzonePublicAction<PayloadType> {
|
||||
override actionName = ActionName.SetQzonePublicCommentLike;
|
||||
override actionSummary = '设置空友爱看评论或回复赞';
|
||||
override actionDescription = '对完整 comment 点赞或取消点赞;传入完整 reply 时操作该回复,保留 comment/reply 的 like.id 等字段';
|
||||
override payloadSchema = PayloadSchema;
|
||||
override payloadExample = { feed: QzonePublicFeedExample, comment: QzonePublicCommentExample, enable: true };
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return this.core.apis.QzonePublicApi.setCommentLike(payload.feed, payload.comment, parseQzonePublicEnable(payload.enable), payload.reply);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { parseQzonePublicEnable, QzonePublicAction, QzonePublicEnableSchema, QzonePublicFeedExample, QzonePublicFeedSchema } from './common';
|
||||
|
||||
const PayloadSchema = Type.Object({ feed: QzonePublicFeedSchema, enable: QzonePublicEnableSchema });
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
export class SetQzonePublicFollow extends QzonePublicAction<PayloadType> {
|
||||
override actionName = ActionName.SetQzonePublicFollow;
|
||||
override actionSummary = '蹲或取消蹲空友爱看作者';
|
||||
override actionDescription = '根据完整 feed 蹲或取消蹲动态作者;enable 默认 true,返回上游操作结果';
|
||||
override payloadSchema = PayloadSchema;
|
||||
override payloadExample = { feed: QzonePublicFeedExample, enable: true };
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return this.core.apis.QzonePublicApi.setFollow(payload.feed, parseQzonePublicEnable(payload.enable));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { parseQzonePublicEnable, QzonePublicAction, QzonePublicEnableSchema, QzonePublicFeedExample, QzonePublicFeedSchema } from './common';
|
||||
|
||||
const PayloadSchema = Type.Object({ feed: QzonePublicFeedSchema, enable: QzonePublicEnableSchema });
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
export class SetQzonePublicLike extends QzonePublicAction<PayloadType> {
|
||||
override actionName = ActionName.SetQzonePublicLike;
|
||||
override actionSummary = '设置空友爱看普通赞';
|
||||
override actionDescription = '对完整 feed 设置或取消普通点赞(like);enable 默认 true,此操作与推分别处理';
|
||||
override payloadSchema = PayloadSchema;
|
||||
override payloadExample = { feed: QzonePublicFeedExample, enable: true };
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return this.core.apis.QzonePublicApi.setLike(payload.feed, parseQzonePublicEnable(payload.enable));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { ActionName } from '@/napcat-onebot/action/router';
|
||||
import { parseQzonePublicEnable, QzonePublicAction, QzonePublicEnableSchema, QzonePublicFeedExample, QzonePublicFeedSchema } from './common';
|
||||
|
||||
const PayloadSchema = Type.Object({ feed: QzonePublicFeedSchema, enable: QzonePublicEnableSchema });
|
||||
type PayloadType = Static<typeof PayloadSchema>;
|
||||
|
||||
export class SetQzonePublicUpvote extends QzonePublicAction<PayloadType> {
|
||||
override actionName = ActionName.SetQzonePublicUpvote;
|
||||
override actionSummary = '推或取消推空友爱看动态';
|
||||
override actionDescription = '对完整 feed 设置推或取消推(upvote);enable 默认 true,此操作与普通点赞分别处理';
|
||||
override payloadSchema = PayloadSchema;
|
||||
override payloadExample = { feed: QzonePublicFeedExample, enable: true };
|
||||
|
||||
async _handle (payload: PayloadType) {
|
||||
return this.core.apis.QzonePublicApi.setUpvote(payload.feed, parseQzonePublicEnable(payload.enable));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import type { BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import type { QzonePublicObject } from '@/napcat-core/data/qzone-public';
|
||||
|
||||
export const QzonePublicObjectSchema = Type.Object({}, { additionalProperties: Type.Unknown() });
|
||||
export const QzonePublicFeedSchema = Type.Object({}, {
|
||||
additionalProperties: Type.Unknown(),
|
||||
description: 'get_qzone_public_feeds 返回的完整 feed 对象,请保留全部字段',
|
||||
});
|
||||
export const QzonePublicCommentSchema = Type.Object({}, {
|
||||
additionalProperties: Type.Unknown(),
|
||||
description: '详情或评论列表返回的完整 comment 对象,请保留 like.id 等全部字段',
|
||||
});
|
||||
export const QzonePublicReplySchema = Type.Object({}, {
|
||||
additionalProperties: Type.Unknown(),
|
||||
description: '回复列表返回的完整 reply 对象,请保留全部字段',
|
||||
});
|
||||
export const QzonePublicResultSchema = Type.Object({}, {
|
||||
additionalProperties: Type.Unknown(),
|
||||
description: '上游返回的原始 JSON 对象,字段随内容类型和服务端版本变化',
|
||||
});
|
||||
export const QzonePublicEnableSchema = Type.Optional(Type.Union([
|
||||
Type.Boolean(), Type.Number(), Type.String(),
|
||||
], { default: true, description: '是否启用,默认 true;数字非 0 为 true,文本支持 true/false、1/0、yes/no、on/off' }));
|
||||
export const QzonePublicContentSchema = Type.Union([
|
||||
Type.String({ minLength: 1 }),
|
||||
Type.Array(QzonePublicObjectSchema, { minItems: 1 }),
|
||||
], { description: '非空文本或非空富文本对象数组,保留每个对象的全部字段' });
|
||||
|
||||
// 示例仅说明对象传递方式;实际调用应直接使用上游返回的完整对象。
|
||||
export const QzonePublicFeedExample = {
|
||||
cell_common: {
|
||||
union_id: { uf_key: 'example-uf-key', ugc_id: 'example-ugc-id', feed_id: 'example-feed-id', ext_ids: [] },
|
||||
cur_like_key: 'example-like-key',
|
||||
domain_type: 3,
|
||||
},
|
||||
cell_user_info: { user: { uin: '10001' } },
|
||||
cell_feed_transfer: {},
|
||||
};
|
||||
export const QzonePublicCommentExample = { id: 'example-comment', like: { id: 'example-comment-like' }, reply_attach_info: '' };
|
||||
export const QzonePublicReplyExample = { id: 'example-reply', like: { id: 'example-reply-like' } };
|
||||
|
||||
export function parseQzonePublicEnable (value: boolean | number | string | undefined): boolean {
|
||||
if (value === undefined) return true;
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (typeof value === 'number') return value !== 0;
|
||||
if (typeof value === 'string') {
|
||||
const text = value.trim().toLowerCase();
|
||||
if (['true', '1', 'yes', 'on'].includes(text)) return true;
|
||||
if (['false', '0', 'no', 'off'].includes(text)) return false;
|
||||
}
|
||||
throw new Error('enable 必须为布尔值、数字或 true/false、1/0、yes/no、on/off 文本');
|
||||
}
|
||||
|
||||
export function parseQzonePublicPageSize (value: number | string | undefined): number {
|
||||
if (value === undefined) return 20;
|
||||
if (typeof value !== 'number' && typeof value !== 'string') {
|
||||
throw new Error('page_size 必须为数字或数值字符串,截断后应大于等于 1');
|
||||
}
|
||||
if (typeof value === 'string' && !value.trim()) {
|
||||
throw new Error('page_size 必须为数字或数值字符串,截断后应大于等于 1');
|
||||
}
|
||||
const numericValue = Number(value);
|
||||
const pageSize = Math.trunc(numericValue);
|
||||
if (!Number.isFinite(numericValue) || pageSize < 1) {
|
||||
throw new Error('page_size 必须为数字或数值字符串,截断后应大于等于 1');
|
||||
}
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
function isObject (value: unknown): value is QzonePublicObject {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export abstract class QzonePublicAction<PayloadType, ReturnType = QzonePublicObject> extends OneBotAction<PayloadType, ReturnType> {
|
||||
override actionTags = ['QQ空间扩展'];
|
||||
override returnSchema = QzonePublicResultSchema;
|
||||
override returnExample = {};
|
||||
|
||||
protected override async check (payload: PayloadType): Promise<BaseCheckResult> {
|
||||
// OneBot 的通用解析会转换原始类型。先校验这些透传参数,避免数字被当作文本或对象。
|
||||
if (!isObject(payload)) return { valid: false, message: '请求参数必须为 JSON 对象' };
|
||||
const parameters: QzonePublicObject = payload;
|
||||
for (const key of ['feed', 'comment', 'reply']) {
|
||||
if (parameters[key] !== undefined && !isObject(parameters[key])) {
|
||||
return { valid: false, message: `${key} 必须为上游返回的完整 JSON 对象` };
|
||||
}
|
||||
}
|
||||
for (const key of ['cursor', 'reason']) {
|
||||
const value = parameters[key];
|
||||
if (value !== undefined) {
|
||||
if (!['string', 'number', 'boolean'].includes(typeof value)) {
|
||||
return { valid: false, message: `${key} 必须为字符串、数字或布尔值` };
|
||||
}
|
||||
parameters[key] = String(value);
|
||||
}
|
||||
}
|
||||
if ('content' in parameters) {
|
||||
let content = parameters['content'];
|
||||
if (typeof content === 'string') {
|
||||
const text = content.trim();
|
||||
if (text.startsWith('[') && text.endsWith(']')) {
|
||||
try {
|
||||
const decoded: unknown = JSON.parse(text);
|
||||
if (Array.isArray(decoded)) content = decoded;
|
||||
} catch {
|
||||
// 无效 JSON 仍按原始文本处理。
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof content === 'string' ? content.length === 0 : !Array.isArray(content) || content.length === 0 || !content.every(isObject)) {
|
||||
return { valid: false, message: 'content 必须为非空文本或非空富文本对象数组' };
|
||||
}
|
||||
parameters['content'] = content;
|
||||
}
|
||||
try {
|
||||
if ('enable' in parameters) parameters['enable'] = parseQzonePublicEnable(parameters['enable'] as boolean | number | string | undefined);
|
||||
if ('page_size' in parameters) parameters['page_size'] = parseQzonePublicPageSize(parameters['page_size'] as number | string | undefined);
|
||||
} catch (error) {
|
||||
return { valid: false, message: (error as Error).message };
|
||||
}
|
||||
return super.check(payload as PayloadType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/** Decode individual GET / form values before applying the public Qzone action contract. */
|
||||
export function decodeQzonePublicHttpParameters (parameters: unknown): Record<string, unknown> {
|
||||
if (parameters === null || typeof parameters !== 'object' || Array.isArray(parameters)) return {};
|
||||
return Object.fromEntries(Object.entries(parameters).map(([key, value]) => {
|
||||
if (typeof value !== 'string') return [key, value];
|
||||
try {
|
||||
return [key, JSON.parse(value) as unknown];
|
||||
} catch {
|
||||
return [key, value];
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export { GetQzonePublicCapabilities } from './GetQzonePublicCapabilities';
|
||||
export { GetQzonePublicFeeds } from './GetQzonePublicFeeds';
|
||||
export { GetQzonePublicFeedDetail } from './GetQzonePublicFeedDetail';
|
||||
export { GetQzonePublicComments } from './GetQzonePublicComments';
|
||||
export { GetQzonePublicReplies } from './GetQzonePublicReplies';
|
||||
export { SetQzonePublicUpvote } from './SetQzonePublicUpvote';
|
||||
export { SetQzonePublicLike } from './SetQzonePublicLike';
|
||||
export { SetQzonePublicFollow } from './SetQzonePublicFollow';
|
||||
export { SetQzonePublicCommentLike } from './SetQzonePublicCommentLike';
|
||||
export { CommentQzonePublicFeed } from './CommentQzonePublicFeed';
|
||||
export { ReplyQzonePublicFeed } from './ReplyQzonePublicFeed';
|
||||
export { DeleteQzonePublicComment } from './DeleteQzonePublicComment';
|
||||
export { DeleteQzonePublicReply } from './DeleteQzonePublicReply';
|
||||
export { ForwardQzonePublicFeed } from './ForwardQzonePublicFeed';
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import type { QzoneComment } from '@/napcat-core/data/qzone-rpc';
|
||||
import type { SlQzoneCommentOptions, SlQzoneCommentResult } from '@/napcat-core/data/qzone-snowluma';
|
||||
import { uriToLocalFile } from 'napcat-common/src/file';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { unlink } from 'node:fs/promises';
|
||||
import { QzoneCommentSchema, QzoneFeedReferenceSchema, QzoneParentCommentSchema, QzoneUserSchema } from './common';
|
||||
import { normalizeQzoneCommentPayload, SlQzoneCommentReturnSchema, SlQzoneIdentitySchema, SlQzoneImagesSchema, SlQzoneTextSchema, SlQzoneUinSchema } from './common-snowluma';
|
||||
|
||||
export const CommentQzonePayloadSchema = Type.Object({
|
||||
feed_ref: Type.Optional(QzoneFeedReferenceSchema),
|
||||
tid: Type.Optional(SlQzoneIdentitySchema),
|
||||
target_uin: Type.Optional(SlQzoneUinSchema),
|
||||
content: SlQzoneTextSchema,
|
||||
comment: Type.Optional(QzoneParentCommentSchema),
|
||||
reply_user: Type.Optional(QzoneUserSchema),
|
||||
comment_id: Type.Optional(SlQzoneIdentitySchema),
|
||||
comment_uin: Type.Optional(SlQzoneUinSchema),
|
||||
reply_uin: Type.Optional(SlQzoneUinSchema),
|
||||
reply_name: Type.Optional(SlQzoneTextSchema),
|
||||
images: SlQzoneImagesSchema,
|
||||
}, { additionalProperties: Type.Unknown() });
|
||||
export type CommentQzonePayload = Static<typeof CommentQzonePayloadSchema>;
|
||||
|
||||
const NativeReturnSchema = Type.Object({
|
||||
comment_id: Type.String({ description: '新评论或回复 ID' }),
|
||||
comment: QzoneCommentSchema,
|
||||
});
|
||||
|
||||
type ReturnType = { comment_id: string; comment: QzoneComment; } | SlQzoneCommentResult;
|
||||
|
||||
export class CommentQzone extends OneBotAction<CommentQzonePayload, ReturnType> {
|
||||
override actionName = ActionName.CommentQzone;
|
||||
override actionSummary = '评论QQ空间说说';
|
||||
override actionDescription = '传 tid 使用 SnowLuma 参数,一级评论支持 images;回复须成对提供 comment_id/comment_uin,指定接收者时还需 reply_uin/reply_name,楼中楼仅文字。传 feed_ref 则保留原 comment/reply_user 方式及原始结果;两种参数不能混用。';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = { tid: 'sample-post', target_uin: '10002', content: '很有意思' };
|
||||
override returnExample = { comment_id: 'sample-comment' };
|
||||
|
||||
override payloadSchema = CommentQzonePayloadSchema;
|
||||
override returnSchema = Type.Union([SlQzoneCommentReturnSchema, NativeReturnSchema]);
|
||||
|
||||
protected override async check (payload: CommentQzonePayload): Promise<BaseCheckResult> {
|
||||
try { normalizeQzoneCommentPayload(payload); } catch (error) { return { valid: false, message: (error as Error).message }; }
|
||||
return await super.check(payload);
|
||||
}
|
||||
|
||||
async _handle (payload: CommentQzonePayload): Promise<ReturnType> {
|
||||
const normalized = normalizeQzoneCommentPayload(payload);
|
||||
if (normalized.format === 'native') {
|
||||
return await this.core.apis.QzoneApi.comment(normalized.reference, normalized.content, normalized.comment, normalized.replyUser);
|
||||
}
|
||||
const originalAccount = normalized.images?.length
|
||||
? { uin: this.core.selfInfo?.uin, uid: this.core.selfInfo?.uid }
|
||||
: undefined;
|
||||
if (originalAccount && (!originalAccount.uin || !originalAccount.uid)) throw new Error('QQ空间图片评论需要已登录的当前账号');
|
||||
const assertAccount = () => {
|
||||
if (originalAccount && (this.core.selfInfo?.uin !== originalAccount.uin || this.core.selfInfo?.uid !== originalAccount.uid)) {
|
||||
throw new Error('QQ账号已切换,图片评论已停止,请重新确认后操作');
|
||||
}
|
||||
};
|
||||
const cleanupPaths: string[] = [];
|
||||
const urls: string[] = [];
|
||||
try {
|
||||
for (const [index, source] of (normalized.images ?? []).entries()) {
|
||||
const image = await uriToLocalFile(this.core.NapCatTempPath, source);
|
||||
if (!image.success) throw new Error(`第${index + 1}张图片处理失败,${image.errMsg}`);
|
||||
if (!image.isLocal) cleanupPaths.push(image.path);
|
||||
assertAccount();
|
||||
const upload = await this.core.apis.QzoneApi.uploadCommentImage(readFileSync(image.path).toString('base64'));
|
||||
assertAccount();
|
||||
let url: URL;
|
||||
try { url = new URL(upload.url); } catch { throw new Error(`第${index + 1}张图片上传未返回有效地址`); }
|
||||
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || upload.url.length > 8192 || upload.url.trim() !== upload.url || /[\t\r\n]/.test(upload.url)) {
|
||||
throw new Error(`第${index + 1}张图片上传未返回有效地址`);
|
||||
}
|
||||
urls.push(upload.url);
|
||||
}
|
||||
const options: SlQzoneCommentOptions | undefined = urls.length
|
||||
? { ...normalized.options, imageUrls: urls }
|
||||
: normalized.options;
|
||||
assertAccount();
|
||||
return await this.core.apis.QzoneApi.commentSnowluma(normalized.tid, normalized.content, normalized.targetUin, options);
|
||||
} finally {
|
||||
for (const path of cleanupPaths) await unlink(path).catch(() => { });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import type { QzoneCommentPage } from '@/napcat-core/data/qzone-comments';
|
||||
import { QzoneCursorSchema, QzoneFeedReferenceExample, QzoneFeedReferenceSchema } from './common';
|
||||
import { QzoneCommentPageExample, QzoneCommentPageSchema, validateQzoneCommentReadPayload } from './common-comment';
|
||||
|
||||
export const GetQzoneCommentListPayloadSchema = Type.Object({
|
||||
feed_ref: QzoneFeedReferenceSchema,
|
||||
cursor: QzoneCursorSchema,
|
||||
}, { additionalProperties: false });
|
||||
export type GetQzoneCommentListPayload = Static<typeof GetQzoneCommentListPayloadSchema>;
|
||||
|
||||
export class GetQzoneCommentList extends OneBotAction<GetQzoneCommentListPayload, QzoneCommentPage> {
|
||||
override actionName = ActionName.GetQzoneCommentList;
|
||||
override actionSummary = '获取QQ空间说说评论';
|
||||
override actionDescription = '不传 cursor 读取详情中的首批评论,之后用返回的 feed_ref 和 next_cursor 续页。返回作者、正文、图片、赞数、内联回复及完整原始对象;未知值为 null,不自动拉取全部历史。';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = { feed_ref: QzoneFeedReferenceExample };
|
||||
override returnExample = QzoneCommentPageExample;
|
||||
override payloadSchema = GetQzoneCommentListPayloadSchema;
|
||||
override returnSchema = QzoneCommentPageSchema;
|
||||
|
||||
protected override async check (payload: GetQzoneCommentListPayload): Promise<BaseCheckResult> {
|
||||
try {
|
||||
validateQzoneCommentReadPayload(payload, false);
|
||||
} catch (error) {
|
||||
return { valid: false, message: (error as Error).message };
|
||||
}
|
||||
return await super.check(payload);
|
||||
}
|
||||
|
||||
async _handle (payload: GetQzoneCommentListPayload): Promise<QzoneCommentPage> {
|
||||
validateQzoneCommentReadPayload(payload, false);
|
||||
return await this.core.apis.QzoneApi.getComments(payload.feed_ref, payload.cursor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import type { QzoneComment } from '@/napcat-core/data/qzone-rpc';
|
||||
import type { QzoneReplyPage } from '@/napcat-core/data/qzone-comments';
|
||||
import { QzoneCursorSchema, QzoneFeedReferenceExample, QzoneFeedReferenceSchema } from './common';
|
||||
import { QzoneReadParentCommentExample, QzoneReadParentCommentSchema, QzoneReplyPageExample, QzoneReplyPageSchema, validateQzoneCommentReadPayload } from './common-comment';
|
||||
|
||||
export const GetQzoneCommentRepliesPayloadSchema = Type.Object({
|
||||
feed_ref: QzoneFeedReferenceSchema,
|
||||
comment: QzoneReadParentCommentSchema,
|
||||
cursor: QzoneCursorSchema,
|
||||
}, { additionalProperties: false });
|
||||
export type GetQzoneCommentRepliesPayload = Static<typeof GetQzoneCommentRepliesPayloadSchema>;
|
||||
|
||||
export class GetQzoneCommentReplies extends OneBotAction<GetQzoneCommentRepliesPayload, QzoneReplyPage> {
|
||||
override actionName = ActionName.GetQzoneCommentReplies;
|
||||
override actionSummary = '获取QQ空间评论楼中楼回复';
|
||||
override actionDescription = '传入读取结果的完整 comment,使用其回复游标读取追加回复;之后用返回的 feed_ref、同一父评论和 next_cursor 续页。与内联回复按 ID 合并,不自动拉取全部历史;每页使用空间固定页大小。';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = { feed_ref: QzoneFeedReferenceExample, comment: QzoneReadParentCommentExample };
|
||||
override returnExample = QzoneReplyPageExample;
|
||||
override payloadSchema = GetQzoneCommentRepliesPayloadSchema;
|
||||
override returnSchema = QzoneReplyPageSchema;
|
||||
|
||||
protected override async check (payload: GetQzoneCommentRepliesPayload): Promise<BaseCheckResult> {
|
||||
try {
|
||||
validateQzoneCommentReadPayload(payload, true);
|
||||
} catch (error) {
|
||||
return { valid: false, message: (error as Error).message };
|
||||
}
|
||||
return await super.check(payload);
|
||||
}
|
||||
|
||||
async _handle (payload: GetQzoneCommentRepliesPayload): Promise<QzoneReplyPage> {
|
||||
validateQzoneCommentReadPayload(payload, true);
|
||||
return await this.core.apis.QzoneApi.getCommentReplies(payload.feed_ref, payload.comment as QzoneComment, payload.cursor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import type { QzoneFeedPage } from '@/napcat-core/data/qzone-rpc';
|
||||
import type { SlQzoneFeedPage } from '@/napcat-core/data/qzone-snowluma-feeds';
|
||||
import { QzoneFeedsReturnSchema } from './common';
|
||||
import { normalizeQzoneFeedsPayload, QzoneFormatSchema, SlQzoneCursorSchema, SlQzoneFeedsExample, SlQzoneFeedsReturnSchema, SlQzoneIntegerSchema } from './common-snowluma';
|
||||
|
||||
export const GetQzoneFeedsPayloadSchema = Type.Object({
|
||||
format: QzoneFormatSchema,
|
||||
page_num: Type.Optional(SlQzoneIntegerSchema),
|
||||
count: Type.Optional(SlQzoneIntegerSchema),
|
||||
cursor: SlQzoneCursorSchema,
|
||||
}, { additionalProperties: Type.Unknown() });
|
||||
export type GetQzoneFeedsPayload = Static<typeof GetQzoneFeedsPayloadSchema>;
|
||||
|
||||
export class GetQzoneFeeds extends OneBotAction<GetQzoneFeedsPayload, QzoneFeedPage | SlQzoneFeedPage> {
|
||||
override actionName = ActionName.GetQzoneFeeds;
|
||||
override actionSummary = '获取QQ空间好友动态';
|
||||
override actionDescription = '默认返回 SnowLuma 格式,page_num 默认1、count 默认10,续页原样传 cursor。format=native 返回原 H5 Feed 和 feed_ref;旧 native 游标可自动识别,native 模式不接受 page_num/count。';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = { page_num: 1, count: 10 };
|
||||
override returnExample = SlQzoneFeedsExample;
|
||||
override payloadSchema = GetQzoneFeedsPayloadSchema;
|
||||
override returnSchema = Type.Union([SlQzoneFeedsReturnSchema, QzoneFeedsReturnSchema]);
|
||||
|
||||
protected override async check (payload: GetQzoneFeedsPayload): Promise<BaseCheckResult> {
|
||||
try { normalizeQzoneFeedsPayload(payload); } catch (error) { return { valid: false, message: (error as Error).message }; }
|
||||
return await super.check(payload);
|
||||
}
|
||||
|
||||
async _handle (payload: GetQzoneFeedsPayload): Promise<QzoneFeedPage | SlQzoneFeedPage> {
|
||||
const normalized = normalizeQzoneFeedsPayload(payload);
|
||||
if (normalized.format === 'native') return await this.core.apis.QzoneApi.getFriendFeeds(normalized.cursor);
|
||||
return await this.core.apis.QzoneApi.getSnowlumaFeeds(normalized.pageNum, normalized.count, normalized.cursor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import type { QzoneDetail } from '@/napcat-core/data/qzone-rpc';
|
||||
import type { SlQzoneMsgDetail } from '@/napcat-core/data/qzone-snowluma';
|
||||
import { QzoneDetailReturnSchema, QzoneFeedReferenceSchema } from './common';
|
||||
import { normalizeQzoneDetailPayload, SlQzoneIdentitySchema, SlQzoneIntegerSchema, SlQzoneMsgDetailExample, SlQzoneMsgDetailReturnSchema, SlQzoneUinSchema } from './common-snowluma';
|
||||
|
||||
export const GetQzoneMsgDetailPayloadSchema = Type.Object({
|
||||
feed_ref: Type.Optional(QzoneFeedReferenceSchema),
|
||||
tid: Type.Optional(SlQzoneIdentitySchema),
|
||||
target_uin: Type.Optional(SlQzoneUinSchema),
|
||||
pos: Type.Optional(SlQzoneIntegerSchema),
|
||||
num: Type.Optional(SlQzoneIntegerSchema),
|
||||
}, { additionalProperties: Type.Unknown() });
|
||||
export type GetQzoneMsgDetailPayload = Static<typeof GetQzoneMsgDetailPayloadSchema>;
|
||||
|
||||
export class GetQzoneMsgDetail extends OneBotAction<GetQzoneMsgDetailPayload, QzoneDetail | SlQzoneMsgDetail> {
|
||||
override actionName = ActionName.GetQzoneMsgDetail;
|
||||
override actionSummary = '获取QQ空间说说详情';
|
||||
override actionDescription = '传 tid 返回 SnowLuma 详情和评论,target_uin 默认当前账号、pos 默认0、num 默认20。传完整 feed_ref 则保留原 H5 详情、原始评论和续页信息;两种定位方式不能混用。';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = { tid: 'sample-post', pos: 0, num: 20 };
|
||||
override returnExample = SlQzoneMsgDetailExample;
|
||||
override payloadSchema = GetQzoneMsgDetailPayloadSchema;
|
||||
override returnSchema = Type.Union([SlQzoneMsgDetailReturnSchema, QzoneDetailReturnSchema]);
|
||||
|
||||
protected override async check (payload: GetQzoneMsgDetailPayload): Promise<BaseCheckResult> {
|
||||
try { normalizeQzoneDetailPayload(payload); } catch (error) { return { valid: false, message: (error as Error).message }; }
|
||||
return await super.check(payload);
|
||||
}
|
||||
|
||||
async _handle (payload: GetQzoneMsgDetailPayload): Promise<QzoneDetail | SlQzoneMsgDetail> {
|
||||
const normalized = normalizeQzoneDetailPayload(payload);
|
||||
if (normalized.format === 'native') return await this.core.apis.QzoneApi.getMsgDetail(normalized.reference);
|
||||
return await this.core.apis.QzoneApi.getSnowlumaMsgDetail(normalized.tid, normalized.targetUin, normalized.pos, normalized.num);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import type { QzoneMsgPage } from '@/napcat-core/data/qzone-rpc';
|
||||
import type { SlQzoneMsgList } from '@/napcat-core/data/qzone-snowluma';
|
||||
import { QzoneCursorSchema, QzoneMsgListReturnSchema } from './common';
|
||||
import { normalizeQzoneListPayload, QzoneFormatSchema, SlQzoneIntegerSchema, SlQzoneMsgListExample, SlQzoneMsgListReturnSchema, SlQzoneUinSchema } from './common-snowluma';
|
||||
|
||||
export const GetQzoneMsgListPayloadSchema = Type.Object({
|
||||
format: QzoneFormatSchema,
|
||||
target_uin: Type.Optional(SlQzoneUinSchema),
|
||||
pos: Type.Optional(SlQzoneIntegerSchema),
|
||||
num: Type.Optional(SlQzoneIntegerSchema),
|
||||
cursor: QzoneCursorSchema,
|
||||
}, { additionalProperties: Type.Unknown() });
|
||||
export type GetQzoneMsgListPayload = Static<typeof GetQzoneMsgListPayloadSchema>;
|
||||
|
||||
export class GetQzoneMsgList extends OneBotAction<GetQzoneMsgListPayload, QzoneMsgPage | SlQzoneMsgList> {
|
||||
override actionName = ActionName.GetQzoneMsgList;
|
||||
override actionSummary = '获取QQ空间说说列表';
|
||||
override actionDescription = '默认返回 SnowLuma 的 total/msglist,pos 默认0、num 默认20(1至100)。format=native 保留原 H5 Feed、feed_ref 和游标;仅传旧 cursor 也会选择 native。两种分页参数不能混用。';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = { target_uin: '10001', pos: 0, num: 20 };
|
||||
override returnExample = SlQzoneMsgListExample;
|
||||
override payloadSchema = GetQzoneMsgListPayloadSchema;
|
||||
override returnSchema = Type.Union([SlQzoneMsgListReturnSchema, QzoneMsgListReturnSchema]);
|
||||
|
||||
protected override async check (payload: GetQzoneMsgListPayload): Promise<BaseCheckResult> {
|
||||
try { normalizeQzoneListPayload(payload); } catch (error) { return { valid: false, message: (error as Error).message }; }
|
||||
return await super.check(payload);
|
||||
}
|
||||
|
||||
async _handle (payload: GetQzoneMsgListPayload): Promise<QzoneMsgPage | SlQzoneMsgList> {
|
||||
const normalized = normalizeQzoneListPayload(payload);
|
||||
if (normalized.format === 'native') return await this.core.apis.QzoneApi.getMsgList(normalized.targetUin, normalized.cursor);
|
||||
return await this.core.apis.QzoneApi.getSnowlumaMsgList(normalized.targetUin, normalized.pos, normalized.num);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import type { QzoneProfile } from '@/napcat-core/data/qzone-web';
|
||||
|
||||
export const GetQzoneProfilePayloadSchema = Type.Object({
|
||||
target_uin: Type.Optional(Type.Union([
|
||||
Type.String({ pattern: '^[1-9][0-9]*$', maxLength: 20 }),
|
||||
Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
|
||||
], { description: '空间所属 QQ 号;不传时为当前账号' })),
|
||||
}, { additionalProperties: false });
|
||||
export type GetQzoneProfilePayload = Static<typeof GetQzoneProfilePayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
name: Type.String({ description: '空间名称' }),
|
||||
description: Type.String({ description: '空间说明' }),
|
||||
});
|
||||
|
||||
function targetUin (payload: GetQzoneProfilePayload): string | undefined {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.keys(payload).some(key => key !== 'target_uin')) {
|
||||
throw new Error('获取空间资料只支持 target_uin 参数');
|
||||
}
|
||||
const value = payload.target_uin;
|
||||
if (value === undefined) return undefined;
|
||||
if (typeof value === 'number' && (!Number.isSafeInteger(value) || value <= 0)) throw new Error('target_uin 必须为有效QQ号');
|
||||
if ((typeof value !== 'string' && typeof value !== 'number') || !/^[1-9][0-9]{0,19}$/.test(String(value))) {
|
||||
throw new Error('target_uin 必须为有效QQ号');
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export class GetQzoneProfile extends OneBotAction<GetQzoneProfilePayload, QzoneProfile> {
|
||||
override actionName = ActionName.GetQzoneProfile;
|
||||
override actionSummary = '获取QQ空间资料';
|
||||
override actionDescription = '获取空间名称和空间说明;target_uin 不传时读取当前账号,空名称或说明按原值返回。';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = { target_uin: '10001' };
|
||||
override returnExample = { name: '我的空间', description: '记录生活' };
|
||||
override payloadSchema = GetQzoneProfilePayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
protected override async check (payload: GetQzoneProfilePayload): Promise<BaseCheckResult> {
|
||||
try {
|
||||
targetUin(payload);
|
||||
} catch (error) {
|
||||
return { valid: false, message: (error as Error).message };
|
||||
}
|
||||
return await super.check(payload);
|
||||
}
|
||||
|
||||
async _handle (payload: GetQzoneProfilePayload): Promise<QzoneProfile> {
|
||||
return await this.core.apis.QzoneApi.getProfile(targetUin(payload));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import { QzoneFeedReferenceSchema } from './common';
|
||||
import { normalizeQzoneLikePayload, SlQzoneIdentitySchema, SlQzoneIntegerSchema, SlQzoneUinSchema } from './common-snowluma';
|
||||
|
||||
export const LikeQzonePayloadSchema = Type.Object({
|
||||
feed_ref: Type.Optional(QzoneFeedReferenceSchema),
|
||||
tid: Type.Optional(SlQzoneIdentitySchema),
|
||||
target_uin: Type.Optional(SlQzoneUinSchema),
|
||||
abstime: Type.Optional(SlQzoneIntegerSchema),
|
||||
}, { additionalProperties: Type.Unknown() });
|
||||
export type LikeQzonePayload = Static<typeof LikeQzonePayloadSchema>;
|
||||
|
||||
export class LikeQzone extends OneBotAction<LikeQzonePayload, null> {
|
||||
override actionName = ActionName.LikeQzone;
|
||||
override actionSummary = '点赞QQ空间说说';
|
||||
override actionDescription = '支持 SnowLuma 的 tid、target_uin 和 abstime(默认0);传 feed_ref 则沿用原 H5 点赞方式。两种定位方式不能混用,写入失败不会切换接口重试。';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = { tid: 'sample-post', target_uin: '10002', abstime: 1700000000 };
|
||||
override returnExample = null;
|
||||
override payloadSchema = LikeQzonePayloadSchema;
|
||||
override returnSchema = Type.Null();
|
||||
|
||||
protected override async check (payload: LikeQzonePayload): Promise<BaseCheckResult> {
|
||||
try { normalizeQzoneLikePayload(payload); } catch (error) { return { valid: false, message: (error as Error).message }; }
|
||||
return await super.check(payload);
|
||||
}
|
||||
|
||||
async _handle (payload: LikeQzonePayload): Promise<null> {
|
||||
const normalized = normalizeQzoneLikePayload(payload);
|
||||
if (normalized.format === 'native') await this.core.apis.QzoneApi.setLike(normalized.reference, true);
|
||||
else await this.core.apis.QzoneApi.setSnowlumaLike(normalized.tid, normalized.targetUin, true, normalized.abstime);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import { qzoneBoolean } from './common-snowluma';
|
||||
|
||||
export const SetQzoneBanPayloadSchema = Type.Object({
|
||||
user_id: Type.Union([
|
||||
Type.String({ pattern: '^[1-9][0-9]*$', maxLength: 20 }),
|
||||
Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
|
||||
], { description: '需要设置空间访问限制的QQ号' }),
|
||||
enable: Type.Optional(Type.Union([
|
||||
Type.Boolean(), Type.Integer({ minimum: 0, maximum: 1 }), Type.String({ maxLength: 128 }),
|
||||
], { description: '默认true;支持布尔值及true/false、1/0、yes/no、on/off拼写' })),
|
||||
}, { additionalProperties: false });
|
||||
export type SetQzoneBanPayload = Static<typeof SetQzoneBanPayloadSchema>;
|
||||
|
||||
function banUpdate (payload: SetQzoneBanPayload): { uin: string; enable: boolean; } {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.keys(payload).some(key => key !== 'user_id' && key !== 'enable')) {
|
||||
throw new Error('设置空间访问限制只支持 user_id 和 enable 参数');
|
||||
}
|
||||
const value = payload.user_id;
|
||||
if (typeof value === 'number' && (!Number.isSafeInteger(value) || value <= 0)) throw new Error('user_id 必须为有效QQ号');
|
||||
if ((typeof value !== 'string' && typeof value !== 'number') || !/^[1-9][0-9]{0,19}$/.test(String(value))) {
|
||||
throw new Error('user_id 必须为有效QQ号');
|
||||
}
|
||||
if (typeof payload.enable === 'string' && payload.enable.length > 128) throw new Error('enable 超出允许长度');
|
||||
return { uin: String(value), enable: qzoneBoolean(payload.enable) };
|
||||
}
|
||||
|
||||
export class SetQzoneBan extends OneBotAction<SetQzoneBanPayload, null> {
|
||||
override actionName = ActionName.SetQzoneBan;
|
||||
override actionSummary = '设置QQ空间访问限制';
|
||||
override actionDescription = '在当前账号的空间设置指定用户的访问限制;enable 默认为 true,传 false 取消禁止。';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = { user_id: '10002', enable: true };
|
||||
override returnExample = null;
|
||||
override payloadSchema = SetQzoneBanPayloadSchema;
|
||||
override returnSchema = Type.Null();
|
||||
|
||||
protected override async check (payload: SetQzoneBanPayload): Promise<BaseCheckResult> {
|
||||
try {
|
||||
banUpdate(payload);
|
||||
} catch (error) {
|
||||
return { valid: false, message: (error as Error).message };
|
||||
}
|
||||
return await super.check(payload);
|
||||
}
|
||||
|
||||
async _handle (payload: SetQzoneBanPayload): Promise<null> {
|
||||
const normalized = banUpdate(payload);
|
||||
await this.core.apis.QzoneApi.setBan(normalized.uin, normalized.enable);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import { normalizeQzoneRight } from '@/napcat-core/data/qzone-web';
|
||||
import { qzoneIdentity, SlQzoneIdentitySchema } from './common-snowluma';
|
||||
|
||||
export const SetQzoneMsgRightPayloadSchema = Type.Object({
|
||||
tid: SlQzoneIdentitySchema,
|
||||
ugc_right: Type.Union([Type.Number(), Type.String({ maxLength: 32 })], { description: '查看权限:1所有人,4好友,16部分好友,64仅自己,128排除部分好友' }),
|
||||
target_uins: Type.Optional(Type.Array(Type.Union([
|
||||
Type.String({ pattern: '^[1-9][0-9]*$', maxLength: 20 }),
|
||||
Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
|
||||
]), { description: 'ugc_right 为16或128时的QQ号列表' })),
|
||||
}, { additionalProperties: false });
|
||||
export type SetQzoneMsgRightPayload = Static<typeof SetQzoneMsgRightPayloadSchema>;
|
||||
|
||||
const ReturnSchema = Type.Object({
|
||||
ugc_right: Type.Number({ description: '设置成功的查看权限' }),
|
||||
});
|
||||
|
||||
function rightUpdate (payload: SetQzoneMsgRightPayload) {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.keys(payload).some(key => !['tid', 'ugc_right', 'target_uins'].includes(key))) {
|
||||
throw new Error('修改说说权限只支持 tid、ugc_right 和 target_uins 参数');
|
||||
}
|
||||
const tid = qzoneIdentity(payload.tid, 'tid');
|
||||
if ((typeof payload.ugc_right !== 'number' && typeof payload.ugc_right !== 'string') ||
|
||||
(typeof payload.ugc_right === 'string' && payload.ugc_right.length > 32)) {
|
||||
throw new Error('ugc_right 必须为有效权限数字');
|
||||
}
|
||||
const right = Number(payload.ugc_right);
|
||||
if (!Number.isFinite(right)) throw new Error('ugc_right 必须为有效权限数字');
|
||||
if (payload.target_uins !== undefined && !Array.isArray(payload.target_uins)) throw new Error('target_uins 必须为QQ号数组');
|
||||
const targets = payload.target_uins?.map(value => {
|
||||
if (typeof value === 'number' && (!Number.isSafeInteger(value) || value <= 0)) throw new Error('target_uins 包含无效QQ号');
|
||||
if ((typeof value !== 'string' && typeof value !== 'number') || !/^[1-9][0-9]{0,19}$/.test(String(value))) {
|
||||
throw new Error('target_uins 包含无效QQ号');
|
||||
}
|
||||
return String(value);
|
||||
});
|
||||
return { ...normalizeQzoneRight(right, targets), tid };
|
||||
}
|
||||
|
||||
export class SetQzoneMsgRight extends OneBotAction<SetQzoneMsgRightPayload, { ugc_right: number; }> {
|
||||
override actionName = ActionName.SetQzoneMsgRight;
|
||||
override actionSummary = '修改QQ空间说说权限';
|
||||
override actionDescription = '仅修改当前账号发表的说说。ugc_right 为16或128时 target_uins 不能为空;其他权限不使用该列表。';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = { tid: 'shuoshuo-tid', ugc_right: 4 };
|
||||
override returnExample = { ugc_right: 4 };
|
||||
override payloadSchema = SetQzoneMsgRightPayloadSchema;
|
||||
override returnSchema = ReturnSchema;
|
||||
|
||||
protected override async check (payload: SetQzoneMsgRightPayload): Promise<BaseCheckResult> {
|
||||
try {
|
||||
rightUpdate(payload);
|
||||
} catch (error) {
|
||||
return { valid: false, message: (error as Error).message };
|
||||
}
|
||||
return await super.check(payload);
|
||||
}
|
||||
|
||||
async _handle (payload: SetQzoneMsgRightPayload): Promise<{ ugc_right: number; }> {
|
||||
const normalized = rightUpdate(payload);
|
||||
return await this.core.apis.QzoneApi.setMsgRight(normalized.tid, normalized.right, normalized.targetUins);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import type { QzoneProfileUpdate } from '@/napcat-core/data/qzone-web';
|
||||
|
||||
const profileValueSchema = (description: string) => Type.Union([
|
||||
Type.String({ maxLength: 8192 }), Type.Number(), Type.Boolean(),
|
||||
], { description });
|
||||
export const SetQzoneProfilePayloadSchema = Type.Object({
|
||||
name: Type.Optional(profileValueSchema('空间名称;空字符串表示清空,省略时不修改')),
|
||||
description: Type.Optional(profileValueSchema('空间说明;空字符串表示清空,省略时不修改')),
|
||||
}, { additionalProperties: false });
|
||||
export type SetQzoneProfilePayload = Static<typeof SetQzoneProfilePayloadSchema>;
|
||||
|
||||
function profileUpdate (payload: SetQzoneProfilePayload): QzoneProfileUpdate {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.keys(payload).some(key => key !== 'name' && key !== 'description')) {
|
||||
throw new Error('修改空间资料只支持 name 和 description 参数');
|
||||
}
|
||||
const profile: QzoneProfileUpdate = {};
|
||||
for (const key of ['name', 'description'] as const) {
|
||||
const value = payload[key];
|
||||
if (value === undefined) continue;
|
||||
if ((typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') || (typeof value === 'number' && !Number.isFinite(value))) {
|
||||
throw new Error(`${key} 必须为字符串、有限数字或布尔值`);
|
||||
}
|
||||
const normalized = String(value);
|
||||
if (normalized.length > 8192) throw new Error(`${key} 不能超过8192个字符`);
|
||||
profile[key] = normalized;
|
||||
}
|
||||
if (profile.name === undefined && profile.description === undefined) throw new Error('name 或 description 至少提供一项');
|
||||
return profile;
|
||||
}
|
||||
|
||||
export class SetQzoneProfile extends OneBotAction<SetQzoneProfilePayload, null> {
|
||||
override actionName = ActionName.SetQzoneProfile;
|
||||
override actionSummary = '修改QQ空间资料';
|
||||
override actionDescription = '仅修改当前账号的空间名称或说明,至少提供一项。省略字段保持不变;空字符串清空字段;数字和布尔值转换为字符串。';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = { name: '我的空间', description: '记录生活' };
|
||||
override returnExample = null;
|
||||
override payloadSchema = SetQzoneProfilePayloadSchema;
|
||||
override returnSchema = Type.Null();
|
||||
|
||||
protected override async check (payload: SetQzoneProfilePayload): Promise<BaseCheckResult> {
|
||||
try {
|
||||
profileUpdate(payload);
|
||||
} catch (error) {
|
||||
return { valid: false, message: (error as Error).message };
|
||||
}
|
||||
return await super.check(payload);
|
||||
}
|
||||
|
||||
async _handle (payload: SetQzoneProfilePayload): Promise<null> {
|
||||
await this.core.apis.QzoneApi.setProfile(profileUpdate(payload));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||
import { ActionName, BaseCheckResult } from '@/napcat-onebot/action/router';
|
||||
import { QzoneFeedReferenceSchema } from './common';
|
||||
import { normalizeQzoneLikePayload, SlQzoneIdentitySchema, SlQzoneIntegerSchema, SlQzoneUinSchema } from './common-snowluma';
|
||||
|
||||
export const UnlikeQzonePayloadSchema = Type.Object({
|
||||
feed_ref: Type.Optional(QzoneFeedReferenceSchema),
|
||||
tid: Type.Optional(SlQzoneIdentitySchema),
|
||||
target_uin: Type.Optional(SlQzoneUinSchema),
|
||||
abstime: Type.Optional(SlQzoneIntegerSchema),
|
||||
}, { additionalProperties: Type.Unknown() });
|
||||
export type UnlikeQzonePayload = Static<typeof UnlikeQzonePayloadSchema>;
|
||||
|
||||
export class UnlikeQzone extends OneBotAction<UnlikeQzonePayload, null> {
|
||||
override actionName = ActionName.UnlikeQzone;
|
||||
override actionSummary = '取消QQ空间说说点赞';
|
||||
override actionDescription = '支持 SnowLuma 的 tid、target_uin 和 abstime(默认0);传 feed_ref 则沿用原 H5 取消点赞方式。两种定位方式不能混用,写入失败不会切换接口重试。';
|
||||
override actionTags = ['扩展接口'];
|
||||
override payloadExample = { tid: 'sample-post', target_uin: '10002', abstime: 1700000000 };
|
||||
override returnExample = null;
|
||||
override payloadSchema = UnlikeQzonePayloadSchema;
|
||||
override returnSchema = Type.Null();
|
||||
|
||||
protected override async check (payload: UnlikeQzonePayload): Promise<BaseCheckResult> {
|
||||
try { normalizeQzoneLikePayload(payload); } catch (error) { return { valid: false, message: (error as Error).message }; }
|
||||
return await super.check(payload);
|
||||
}
|
||||
|
||||
async _handle (payload: UnlikeQzonePayload): Promise<null> {
|
||||
const normalized = normalizeQzoneLikePayload(payload);
|
||||
if (normalized.format === 'native') await this.core.apis.QzoneApi.setLike(normalized.reference, false);
|
||||
else await this.core.apis.QzoneApi.setSnowlumaLike(normalized.tid, normalized.targetUin, false, normalized.abstime);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { QZONE_REFERENCE_MAX_BYTES, validateQzoneFeedReference } from '@/napcat-core/data/qzone-rpc';
|
||||
import { QzoneFeedReferenceExample, QzoneFeedReferenceSchema } from './common';
|
||||
|
||||
const NullableString = Type.Union([Type.String(), Type.Null()]);
|
||||
const NullableNumber = Type.Union([Type.Number(), Type.Null()]);
|
||||
const NullableBoolean = Type.Union([Type.Boolean(), Type.Null()]);
|
||||
const RawObject = Type.Record(Type.String(), Type.Unknown());
|
||||
|
||||
export const QzoneReadParentCommentSchema = Type.Object({
|
||||
id: Type.Union([
|
||||
Type.String({ minLength: 1, maxLength: 8192 }),
|
||||
Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
|
||||
], { description: '原始父评论 ID,字符串或安全整数,0 是有效 ID' }),
|
||||
user: Type.Optional(RawObject),
|
||||
}, { additionalProperties: Type.Unknown(), description: '原样传回读取结果中的完整 comment 对象;整体不得超过64KiB' });
|
||||
|
||||
export const QzoneReplyItemSchema = Type.Object({
|
||||
reply_id: NullableString,
|
||||
user_id: NullableString,
|
||||
uid: NullableString,
|
||||
nickname: NullableString,
|
||||
time: NullableNumber,
|
||||
content: NullableString,
|
||||
images: Type.Union([Type.Array(Type.String()), Type.Null()]),
|
||||
like_count: NullableNumber,
|
||||
liked: NullableBoolean,
|
||||
target_user_id: NullableString,
|
||||
target_uid: NullableString,
|
||||
target_nickname: NullableString,
|
||||
reply: RawObject,
|
||||
}, { description: '楼中楼回复;未知字段为 null,完整原始对象保留在 reply' });
|
||||
|
||||
export const QzoneCommentItemSchema = Type.Object({
|
||||
comment_id: NullableString,
|
||||
user_id: NullableString,
|
||||
uid: NullableString,
|
||||
nickname: NullableString,
|
||||
time: NullableNumber,
|
||||
content: NullableString,
|
||||
images: Type.Union([Type.Array(Type.String()), Type.Null()]),
|
||||
like_count: NullableNumber,
|
||||
liked: NullableBoolean,
|
||||
reply_num: NullableNumber,
|
||||
has_more_replies: NullableBoolean,
|
||||
replies: Type.Array(QzoneReplyItemSchema),
|
||||
comment: RawObject,
|
||||
}, { description: '评论及其内联回复;replies 为空不代表没有尚未读取的历史回复' });
|
||||
|
||||
export const QzoneCommentPageSchema = Type.Object({
|
||||
feed_ref: QzoneFeedReferenceSchema,
|
||||
comment_num: NullableNumber,
|
||||
comments: Type.Array(QzoneCommentItemSchema),
|
||||
has_more: Type.Boolean(),
|
||||
next_cursor: NullableString,
|
||||
raw: RawObject,
|
||||
}, { description: '一页评论;续页时原样传回本页 feed_ref 和 next_cursor' });
|
||||
|
||||
export const QzoneReplyPageSchema = Type.Object({
|
||||
feed_ref: QzoneFeedReferenceSchema,
|
||||
comment_id: Type.String(),
|
||||
replies: Type.Array(QzoneReplyItemSchema),
|
||||
reply_num: NullableNumber,
|
||||
has_more: Type.Boolean(),
|
||||
next_cursor: NullableString,
|
||||
raw: RawObject,
|
||||
}, { description: '一页追加楼中楼回复;与父评论内联 replys 按 ID 合并,不代表完整历史' });
|
||||
|
||||
function record (value: unknown): value is Record<string, unknown> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
return prototype === Object.prototype || prototype === null;
|
||||
}
|
||||
|
||||
export function validateQzoneCommentReadPayload (payload: unknown, withParent: boolean): void {
|
||||
const allowed = withParent ? ['feed_ref', 'comment', 'cursor'] : ['feed_ref', 'cursor'];
|
||||
if (!record(payload) || Object.keys(payload).some(key => !allowed.includes(key))) throw new Error('QQ空间评论读取参数无效');
|
||||
validateQzoneFeedReference(payload['feed_ref']);
|
||||
const cursor = payload['cursor'];
|
||||
if (cursor !== undefined && (typeof cursor !== 'string' || !cursor || cursor.length > 16384)) throw new Error('QQ空间评论游标无效');
|
||||
if (!withParent) return;
|
||||
const comment = payload['comment'];
|
||||
if (!record(comment)) throw new Error('请提供读取结果中的完整父评论 comment');
|
||||
const id = comment['id'];
|
||||
if ((typeof id !== 'string' || !id.trim() || id.length > 8192) &&
|
||||
(typeof id !== 'number' || !Number.isSafeInteger(id) || id < 0)) {
|
||||
throw new Error('父评论 ID 必须为非空字符串或安全的非负整数');
|
||||
}
|
||||
if (comment['user'] !== undefined && !record(comment['user'])) throw new Error('父评论 user 必须为原始JSON对象');
|
||||
let serialized: string;
|
||||
try {
|
||||
serialized = JSON.stringify(comment);
|
||||
} catch {
|
||||
throw new Error('父评论必须为可序列化的JSON对象');
|
||||
}
|
||||
if (Buffer.byteLength(serialized) > QZONE_REFERENCE_MAX_BYTES) throw new Error('父评论不能超过64KiB');
|
||||
}
|
||||
|
||||
export const QzoneReadParentCommentExample = {
|
||||
id: 0,
|
||||
user: { uid: 'u_commenter', uin: '', nick: '评论作者', portrait: 'https://example.com/avatar.png' },
|
||||
time: 1792926000,
|
||||
content: [{ type: 0, content: '很有意思' }],
|
||||
media_items: [],
|
||||
like: { id: 'comment-like-key', count: 2, status: 1 },
|
||||
reply_num: 1,
|
||||
has_more_reply: true,
|
||||
replys: [],
|
||||
reply_attach_info: 'server-reply-pointer',
|
||||
};
|
||||
|
||||
export const QzoneCommentPageExample = {
|
||||
feed_ref: QzoneFeedReferenceExample,
|
||||
comment_num: 1,
|
||||
comments: [{
|
||||
comment_id: '0',
|
||||
user_id: null,
|
||||
uid: 'u_commenter',
|
||||
nickname: '评论作者',
|
||||
time: 1792926000,
|
||||
content: '很有意思',
|
||||
images: [],
|
||||
like_count: 2,
|
||||
liked: true,
|
||||
reply_num: 1,
|
||||
has_more_replies: true,
|
||||
replies: [],
|
||||
comment: QzoneReadParentCommentExample,
|
||||
}],
|
||||
has_more: false,
|
||||
next_cursor: null,
|
||||
raw: { cell_comment: { num: 1, comments: [QzoneReadParentCommentExample] }, has_more: false },
|
||||
};
|
||||
|
||||
const ReplyExample = {
|
||||
id: 'reply-1',
|
||||
user: { uid: 'u_commenter', nick: '回复作者' },
|
||||
target_user: { uin: '10001', uid: 'u_self', nick: 'Qiao' },
|
||||
time: 1792926001,
|
||||
content: [{ type: 0, content: '谢谢' }],
|
||||
};
|
||||
|
||||
export const QzoneReplyPageExample = {
|
||||
feed_ref: QzoneFeedReferenceExample,
|
||||
comment_id: '0',
|
||||
replies: [{
|
||||
reply_id: 'reply-1',
|
||||
user_id: null,
|
||||
uid: 'u_commenter',
|
||||
nickname: '回复作者',
|
||||
time: 1792926001,
|
||||
content: '谢谢',
|
||||
images: null,
|
||||
like_count: null,
|
||||
liked: null,
|
||||
target_user_id: '10001',
|
||||
target_uid: 'u_self',
|
||||
target_nickname: 'Qiao',
|
||||
reply: ReplyExample,
|
||||
}],
|
||||
reply_num: 1,
|
||||
has_more: false,
|
||||
next_cursor: null,
|
||||
raw: { replys: [ReplyExample], has_more: false, reply_attach_info: '' },
|
||||
};
|
||||
@@ -0,0 +1,280 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { Value } from '@sinclair/typebox/value';
|
||||
import {
|
||||
validateQzoneFeedReference,
|
||||
type QzoneComment,
|
||||
type QzoneFeedReference,
|
||||
type QzoneUser,
|
||||
} from '@/napcat-core/data/qzone-rpc';
|
||||
import type { SlQzoneCommentOptions } from '@/napcat-core/data/qzone-snowluma';
|
||||
import { QzoneParentCommentSchema, QzoneUserSchema } from './common';
|
||||
|
||||
export const QzoneFormatSchema = Type.Optional(Type.Union([Type.Literal('snowluma'), Type.Literal('native')], {
|
||||
description: '默认 snowluma;native 保留原来的 H5 列表格式',
|
||||
}));
|
||||
export const SlQzoneUinSchema = Type.Union([
|
||||
Type.String({ pattern: '^[1-9][0-9]*$', maxLength: 20 }),
|
||||
Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
|
||||
]);
|
||||
export const SlQzoneIdentitySchema = Type.Union([
|
||||
Type.String({ minLength: 1, maxLength: 8192 }),
|
||||
Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
|
||||
]);
|
||||
export const SlQzoneIntegerSchema = Type.Union([Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }), Type.String({ minLength: 1, maxLength: 128 })]);
|
||||
export const SlQzoneTextSchema = Type.Union([Type.String(), Type.Number(), Type.Boolean()]);
|
||||
export const SlQzoneCursorSchema = Type.Optional(Type.String({ minLength: 1, maxLength: 65536 }));
|
||||
export const SlQzoneImagesSchema = Type.Optional(Type.Array(Type.String({ minLength: 1 }), {
|
||||
description: '图片来源数组,支持 file://、HTTP(S)、base64://;仅一级评论支持图片',
|
||||
}));
|
||||
|
||||
export const SlQzoneEmotionSchema = Type.Object({
|
||||
tid: Type.String(), content: Type.String(), time: Type.Integer(), comment_num: Type.Integer(), is_private: Type.Boolean(), images: Type.Array(Type.String()),
|
||||
});
|
||||
export const SlQzoneMsgListReturnSchema = Type.Object({
|
||||
total: Type.Integer(), msglist: Type.Array(SlQzoneEmotionSchema), total_known: Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
const SlCommentFields = {
|
||||
uin: Type.Integer(), name: Type.String(), time: Type.Integer(), content: Type.String(), comment_id: Type.String(),
|
||||
};
|
||||
export const SlQzoneReplySchema = Type.Object({
|
||||
...SlCommentFields, parent_comment_id: Type.String(), parent_comment_uin: Type.Integer(),
|
||||
});
|
||||
export const SlQzoneCommentSchema = Type.Object({ ...SlCommentFields, replies: Type.Array(SlQzoneReplySchema) });
|
||||
export const SlQzoneMsgDetailReturnSchema = Type.Object({
|
||||
tid: Type.String(), content: Type.String(), comment_num: Type.Integer(), comments: Type.Array(SlQzoneCommentSchema),
|
||||
});
|
||||
const NullableString = Type.Union([Type.String(), Type.Null()]);
|
||||
const NullableNumber = Type.Union([Type.Integer(), Type.Null()]);
|
||||
export const SlQzoneFeedSchema = Type.Object({
|
||||
uin: Type.Integer(),
|
||||
nickname: Type.String(),
|
||||
time: Type.Integer(),
|
||||
appid: Type.Integer(),
|
||||
key: Type.String(),
|
||||
html: Type.String(),
|
||||
tid: NullableString,
|
||||
content: NullableString,
|
||||
content_complete: Type.Union([Type.Boolean(), Type.Null()]),
|
||||
images: Type.Union([Type.Array(Type.String()), Type.Null()]),
|
||||
like_count: NullableNumber,
|
||||
comment_count: NullableNumber,
|
||||
});
|
||||
export const SlQzoneFeedsReturnSchema = Type.Object({
|
||||
feeds: Type.Array(SlQzoneFeedSchema), has_more: Type.Boolean(), next_cursor: NullableString,
|
||||
});
|
||||
export const SlQzoneCommentReturnSchema = Type.Object({
|
||||
comment_id: Type.String(), parent_comment_id: Type.Optional(Type.String()), parent_comment_uin: Type.Optional(Type.Integer()),
|
||||
});
|
||||
|
||||
const KNOWN_FIELDS = new Set([
|
||||
'format', 'feed_ref', 'tid', 'target_uin', 'pos', 'num', 'page_num', 'count', 'cursor', 'abstime',
|
||||
'comment', 'reply_user', 'comment_id', 'comment_uin', 'reply_uin', 'reply_name', 'images', 'content',
|
||||
'ugc_right', 'target_uins', 'user_id', 'enable', 'name', 'description',
|
||||
]);
|
||||
|
||||
export function qzonePayload (value: unknown): Record<string, unknown> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('QQ空间参数必须为对象');
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function checkQzoneFields (payload: Record<string, unknown>, allowed: string[], native: boolean): void {
|
||||
for (const key of Object.keys(payload)) {
|
||||
if (allowed.includes(key) || payload[key] === undefined) continue;
|
||||
if (native || KNOWN_FIELDS.has(key)) throw new Error(`当前QQ空间调用不支持 ${key} 参数`);
|
||||
}
|
||||
}
|
||||
|
||||
export function qzoneInteger (value: unknown, name: string, fallback: number, minimum: number, maximum = Number.MAX_SAFE_INTEGER): number {
|
||||
if (value === undefined) return fallback;
|
||||
if ((typeof value !== 'number' && typeof value !== 'string') ||
|
||||
(typeof value === 'string' && (!value.trim() || value.length > 128))) throw new Error(`${name} 必须为整数`);
|
||||
const number = Number(value);
|
||||
if (!Number.isSafeInteger(number) || number < minimum || number > maximum) throw new Error(`${name} 超出允许范围`);
|
||||
return number;
|
||||
}
|
||||
|
||||
export function qzoneUin (value: unknown, name: string): string | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (typeof value === 'number' && (!Number.isSafeInteger(value) || value <= 0)) throw new Error(`${name} 必须为有效QQ号`);
|
||||
if ((typeof value !== 'string' && typeof value !== 'number') || !/^[1-9][0-9]{0,19}$/.test(String(value))) throw new Error(`${name} 必须为有效QQ号`);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function qzoneIdentity (value: unknown, name: string): string {
|
||||
if (typeof value === 'number' && (!Number.isSafeInteger(value) || value < 0)) throw new Error(`${name} 必须为有效标识`);
|
||||
if ((typeof value !== 'string' && typeof value !== 'number') || !String(value).trim() || String(value).length > 8192) throw new Error(`${name} 必须为有效标识`);
|
||||
for (const character of String(value)) {
|
||||
if (character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) throw new Error(`${name} 必须为有效标识`);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function qzoneText (value: unknown, name: string): string {
|
||||
if ((typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') || (typeof value === 'number' && !Number.isFinite(value))) {
|
||||
throw new Error(`${name} 必须为文本或有限标量`);
|
||||
}
|
||||
const text = String(value);
|
||||
if (!text.length) throw new Error(`${name} 不能为空`);
|
||||
return text;
|
||||
}
|
||||
|
||||
export function qzoneImages (value: unknown): string[] | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (!Array.isArray(value) || value.some(item => typeof item !== 'string' || !item.trim())) throw new Error('images 必须为非空图片来源字符串数组');
|
||||
return value as string[];
|
||||
}
|
||||
|
||||
export function qzoneBoolean (value: unknown, fallback = true): boolean {
|
||||
if (value === undefined) return fallback;
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (value === 1 || value === 0) return value === 1;
|
||||
if (typeof value === 'string') {
|
||||
const text = value.trim().toLowerCase();
|
||||
if (['true', '1', 'yes', 'on'].includes(text)) return true;
|
||||
if (['false', '0', 'no', 'off'].includes(text)) return false;
|
||||
}
|
||||
throw new Error('enable 必须为有效布尔值');
|
||||
}
|
||||
|
||||
function format (value: unknown): 'native' | 'snowluma' | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (value !== 'native' && value !== 'snowluma') throw new Error('format 仅支持 snowluma 或 native');
|
||||
return value;
|
||||
}
|
||||
|
||||
function cursor (value: unknown, maximum: number): string | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (typeof value !== 'string' || !value || value.length > maximum) throw new Error('QQ空间游标无效');
|
||||
return value;
|
||||
}
|
||||
|
||||
export function normalizeQzoneFeedsPayload (value: unknown) {
|
||||
const payload = qzonePayload(value);
|
||||
const pointer = cursor(payload['cursor'], 65536);
|
||||
const explicit = format(payload['format']);
|
||||
const selected = explicit ?? (pointer !== undefined && !pointer.startsWith('slqf:') ? 'native' : 'snowluma');
|
||||
if (selected === 'native') {
|
||||
checkQzoneFields(payload, ['format', 'cursor'], true);
|
||||
if (pointer?.startsWith('slqf:')) throw new Error('SnowLuma游标不能用于 native 格式');
|
||||
return { format: selected, cursor: cursor(pointer, 16384) } as const;
|
||||
}
|
||||
checkQzoneFields(payload, ['format', 'page_num', 'count', 'cursor'], false);
|
||||
if (pointer !== undefined && !pointer.startsWith('slqf:')) throw new Error('native 游标不能用于 SnowLuma 格式');
|
||||
return {
|
||||
format: selected,
|
||||
pageNum: qzoneInteger(payload['page_num'], 'page_num', 1, 1),
|
||||
count: qzoneInteger(payload['count'], 'count', 10, 1, 50),
|
||||
cursor: pointer,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function normalizeQzoneListPayload (value: unknown) {
|
||||
const payload = qzonePayload(value);
|
||||
const pointer = cursor(payload['cursor'], 16384);
|
||||
const selected = format(payload['format']) ?? (pointer !== undefined ? 'native' : 'snowluma');
|
||||
const targetUin = qzoneUin(payload['target_uin'], 'target_uin');
|
||||
if (selected === 'native') {
|
||||
checkQzoneFields(payload, ['format', 'target_uin', 'cursor'], true);
|
||||
if (pointer?.startsWith('slqf:')) throw new Error('好友动态游标不能用于说说列表');
|
||||
return { format: selected, targetUin, cursor: pointer } as const;
|
||||
}
|
||||
checkQzoneFields(payload, ['format', 'target_uin', 'pos', 'num'], false);
|
||||
return {
|
||||
format: selected,
|
||||
targetUin,
|
||||
pos: qzoneInteger(payload['pos'], 'pos', 0, 0),
|
||||
num: qzoneInteger(payload['num'], 'num', 20, 1, 100),
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function normalizeQzoneDetailPayload (value: unknown) {
|
||||
const payload = qzonePayload(value);
|
||||
if (payload['feed_ref'] !== undefined) {
|
||||
checkQzoneFields(payload, ['feed_ref'], true);
|
||||
return { format: 'native', reference: validateQzoneFeedReference(payload['feed_ref']) } as const;
|
||||
}
|
||||
checkQzoneFields(payload, ['tid', 'target_uin', 'pos', 'num'], false);
|
||||
return {
|
||||
format: 'snowluma',
|
||||
tid: qzoneIdentity(payload['tid'], 'tid'),
|
||||
targetUin: qzoneUin(payload['target_uin'], 'target_uin'),
|
||||
pos: qzoneInteger(payload['pos'], 'pos', 0, 0),
|
||||
num: qzoneInteger(payload['num'], 'num', 20, 1, 100),
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function normalizeQzoneLikePayload (value: unknown) {
|
||||
const payload = qzonePayload(value);
|
||||
if (payload['feed_ref'] !== undefined) {
|
||||
checkQzoneFields(payload, ['feed_ref'], true);
|
||||
return { format: 'native', reference: validateQzoneFeedReference(payload['feed_ref']) } as const;
|
||||
}
|
||||
checkQzoneFields(payload, ['tid', 'target_uin', 'abstime'], false);
|
||||
return {
|
||||
format: 'snowluma',
|
||||
tid: qzoneIdentity(payload['tid'], 'tid'),
|
||||
targetUin: qzoneUin(payload['target_uin'], 'target_uin'),
|
||||
abstime: qzoneInteger(payload['abstime'], 'abstime', 0, 0),
|
||||
} as const;
|
||||
}
|
||||
|
||||
function nativeComment (payload: Record<string, unknown>): { format: 'native'; reference: QzoneFeedReference; content: string; comment?: QzoneComment; replyUser?: QzoneUser; } {
|
||||
checkQzoneFields(payload, ['feed_ref', 'content', 'comment', 'reply_user'], true);
|
||||
const reference = validateQzoneFeedReference(payload['feed_ref']);
|
||||
if (typeof payload['content'] !== 'string' || !payload['content'].trim() || payload['content'].length > 2000) throw new Error('native 评论正文必须为2000字以内的非空文本');
|
||||
const parent = payload['comment'];
|
||||
const target = payload['reply_user'];
|
||||
if (parent !== undefined) {
|
||||
if (!Value.Check(QzoneParentCommentSchema, parent)) throw new Error('请提供符合写接口约束的完整父评论');
|
||||
let size: number;
|
||||
try { size = Buffer.byteLength(JSON.stringify(parent)); } catch { throw new Error('父评论必须为JSON对象'); }
|
||||
if (size > 64 * 1024) throw new Error('父评论不能超过64KiB');
|
||||
}
|
||||
if (target !== undefined && (!Value.Check(QzoneUserSchema, target) || parent === undefined)) throw new Error('reply_user 必须与有效父评论一起提供');
|
||||
return { format: 'native', reference, content: payload['content'], comment: parent as QzoneComment | undefined, replyUser: target as QzoneUser | undefined };
|
||||
}
|
||||
|
||||
export function normalizeQzoneCommentPayload (value: unknown) {
|
||||
const payload = qzonePayload(value);
|
||||
if (payload['feed_ref'] !== undefined) return nativeComment(payload);
|
||||
checkQzoneFields(payload, ['tid', 'target_uin', 'content', 'images', 'comment_id', 'comment_uin', 'reply_uin', 'reply_name'], false);
|
||||
const tid = qzoneIdentity(payload['tid'], 'tid');
|
||||
const content = qzoneText(payload['content'], 'content');
|
||||
const targetUin = qzoneUin(payload['target_uin'], 'target_uin');
|
||||
const images = qzoneImages(payload['images']);
|
||||
const hasParent = payload['comment_id'] !== undefined;
|
||||
const hasParentUin = payload['comment_uin'] !== undefined;
|
||||
const hasRecipient = payload['reply_uin'] !== undefined;
|
||||
const hasRecipientName = payload['reply_name'] !== undefined;
|
||||
if (hasParent !== hasParentUin) throw new Error('comment_id 和 comment_uin 必须同时提供');
|
||||
if (hasRecipient !== hasRecipientName) throw new Error('reply_uin 和 reply_name 必须同时提供');
|
||||
if (hasRecipient && !hasParent) throw new Error('指定回复目标时必须提供父评论');
|
||||
if (hasParent && images?.length) throw new Error('楼中楼回复不支持图片');
|
||||
const options: SlQzoneCommentOptions = {};
|
||||
if (hasParent) {
|
||||
options.comment_id = qzoneIdentity(payload['comment_id'], 'comment_id');
|
||||
options.comment_uin = qzoneUin(payload['comment_uin'], 'comment_uin');
|
||||
if (!Number.isSafeInteger(Number(options.comment_uin))) throw new Error('comment_uin 超出回复结果可安全表示的范围');
|
||||
if (!content.trim()) throw new Error('回复正文不能为空白');
|
||||
}
|
||||
if (hasRecipient) {
|
||||
options.reply_uin = qzoneUin(payload['reply_uin'], 'reply_uin');
|
||||
if (!Number.isSafeInteger(Number(options.reply_uin))) throw new Error('reply_uin 超出回复结果可安全表示的范围');
|
||||
options.reply_name = qzoneText(payload['reply_name'], 'reply_name');
|
||||
if (!options.reply_name.trim() || options.reply_name.length > 8192) throw new Error('reply_name 必须为8192字以内的非空昵称');
|
||||
}
|
||||
return { format: 'snowluma', tid, content, targetUin, images, options: Object.keys(options).length ? options : undefined } as const;
|
||||
}
|
||||
|
||||
export const SlQzoneMsgListExample = {
|
||||
total: 1,
|
||||
total_known: true,
|
||||
msglist: [{ tid: 'sample-post', content: '今天拍到了晚霞', time: 1700000000, comment_num: 0, is_private: false, images: [] }],
|
||||
};
|
||||
export const SlQzoneMsgDetailExample = { tid: 'sample-post', content: '今天拍到了晚霞', comment_num: 0, comments: [] };
|
||||
export const SlQzoneFeedsExample = {
|
||||
feeds: [{ uin: 10002, nickname: '好友', time: 1700000000, appid: 311, key: 'sample-feed', html: '<div>今天拍到了晚霞</div>', tid: 'sample-post', content: '今天拍到了晚霞', content_complete: true, images: [], like_count: 0, comment_count: 0 }],
|
||||
has_more: false,
|
||||
next_cursor: null,
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
|
||||
const NullableString = Type.Union([Type.String(), Type.Null()]);
|
||||
const NullableNumber = Type.Union([Type.Number(), Type.Null()]);
|
||||
const RawObject = Type.Record(Type.String(), Type.Unknown());
|
||||
|
||||
export const QzoneCursorSchema = Type.Optional(Type.String({
|
||||
minLength: 1,
|
||||
maxLength: 16384,
|
||||
description: '上一页返回的 next_cursor;不传时读取第一页,游标不能跨账号或查询使用',
|
||||
}));
|
||||
|
||||
export const QzoneUserSchema = Type.Object({
|
||||
uin: Type.String({ pattern: '^[1-9][0-9]*$', maxLength: 20, description: 'QQ 号' }),
|
||||
uid: Type.Optional(Type.String({ maxLength: 256, description: 'QQ UID;部分空间用户返回空字符串' })),
|
||||
nick: Type.Optional(Type.String({ maxLength: 256, description: '昵称' })),
|
||||
}, { additionalProperties: Type.Unknown() });
|
||||
|
||||
const QzoneParentUserSchema = Type.Object({
|
||||
...QzoneUserSchema.properties,
|
||||
uid: Type.Optional(Type.Union([Type.String({ maxLength: 256 }), Type.Null()], {
|
||||
description: '原始父评论作者的 UID;允许上游返回 null 并原样保留',
|
||||
})),
|
||||
}, { additionalProperties: Type.Unknown() });
|
||||
|
||||
export const QzoneUnionIdSchema = Type.Object({
|
||||
uf_key: Type.Optional(Type.String({ maxLength: 8192 })),
|
||||
ugc_id: Type.Optional(Type.String({ maxLength: 8192 })),
|
||||
feed_id: Type.Optional(Type.String({ maxLength: 8192 })),
|
||||
ext_ids: Type.Optional(Type.Array(RawObject)),
|
||||
serialized_data: Type.Optional(Type.String({ maxLength: 8192 })),
|
||||
}, { minProperties: 1, additionalProperties: Type.Unknown(), description: '读取结果中的完整动态身份,不要由 tid 拼造' });
|
||||
|
||||
export const QzoneFeedReferenceSchema = Type.Object({
|
||||
union_id: QzoneUnionIdSchema,
|
||||
cell_feed_transfer: Type.Optional(Type.Unknown()),
|
||||
feed_action_domain_type: Type.Optional(Type.Integer({ minimum: 1, maximum: 3 })),
|
||||
like_key: Type.Optional(Type.String({ minLength: 1, maxLength: 8192 })),
|
||||
}, {
|
||||
additionalProperties: false,
|
||||
description: '原样传回列表或详情返回的 feed_ref;整体不得超过 64 KiB',
|
||||
});
|
||||
|
||||
export const QzoneCommentSchema = Type.Object({
|
||||
id: Type.Optional(Type.Union([Type.String(), Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER })])),
|
||||
user: Type.Optional(RawObject),
|
||||
content: Type.Optional(Type.Array(RawObject)),
|
||||
}, { additionalProperties: Type.Unknown(), description: '空间返回的完整评论或回复对象,保留未知字段' });
|
||||
|
||||
export const QzoneParentCommentSchema = Type.Object({
|
||||
id: Type.String({ minLength: 1, maxLength: 8192, description: '父评论 ID' }),
|
||||
user: QzoneParentUserSchema,
|
||||
}, { additionalProperties: Type.Unknown(), description: '从详情 comments 中取得的完整父评论对象' });
|
||||
|
||||
export const QzoneFeedItemSchema = Type.Object({
|
||||
tid: NullableString,
|
||||
user_id: NullableString,
|
||||
content: NullableString,
|
||||
time: NullableNumber,
|
||||
comment_num: NullableNumber,
|
||||
like_num: NullableNumber,
|
||||
images: Type.Union([Type.Array(Type.String()), Type.Null()]),
|
||||
feed_ref: Type.Union([QzoneFeedReferenceSchema, Type.Null()]),
|
||||
feed: RawObject,
|
||||
}, { description: '空间动态;无法确定的字段为 null,完整原始数据保留在 feed' });
|
||||
|
||||
export const QzoneFeedsReturnSchema = Type.Object({
|
||||
feeds: Type.Array(QzoneFeedItemSchema),
|
||||
has_more: Type.Boolean(),
|
||||
next_cursor: NullableString,
|
||||
});
|
||||
|
||||
export const QzoneMsgListReturnSchema = Type.Object({
|
||||
msglist: Type.Array(QzoneFeedItemSchema),
|
||||
has_more: Type.Boolean(),
|
||||
next_cursor: NullableString,
|
||||
seal_off_memory: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: '空间返回的封存状态,按原始字段保留' })),
|
||||
});
|
||||
|
||||
export const QzoneDetailReturnSchema = Type.Composite([
|
||||
QzoneFeedItemSchema,
|
||||
Type.Object({
|
||||
comments: Type.Array(QzoneCommentSchema),
|
||||
comment_has_more: Type.Optional(Type.Boolean()),
|
||||
comment_next_cursor: Type.Optional(NullableString),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const QzoneFeedReferenceExample = {
|
||||
union_id: { uf_key: 'feed-key', ugc_id: 'shuoshuo-tid', feed_id: 'feed-id' },
|
||||
feed_action_domain_type: 1,
|
||||
like_key: 'like-key',
|
||||
};
|
||||
|
||||
export const QzoneFeedItemExample = {
|
||||
tid: 'shuoshuo-tid',
|
||||
user_id: '10001',
|
||||
content: '今天天气不错',
|
||||
time: 1792926000,
|
||||
comment_num: 0,
|
||||
like_num: 0,
|
||||
images: null,
|
||||
feed_ref: QzoneFeedReferenceExample,
|
||||
feed: {
|
||||
cell_common: { union_id: QzoneFeedReferenceExample.union_id, time: 1792926000, feed_type: 1, feed_sub_type: 1, cur_like_key: 'like-key' },
|
||||
cell_user_info: { user: { uin: '10001' } },
|
||||
cell_summary: { summary: [{ type: 0, content: '今天天气不错' }] },
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user