From 379f55b9fe3340d699fd45b883e5551687129fdf Mon Sep 17 00:00:00 2001 From: LittleC <26459759+XxLittleCxX@users.noreply.github.com> Date: Sat, 30 Sep 2023 18:10:58 +0800 Subject: [PATCH] feat(qq): add internal api --- adapters/qq/src/bot.ts | 2 +- adapters/qq/src/internal.ts | 228 +++++++++++++++-- adapters/qq/src/types.ts | 474 +++++++++++++++++++++++++++++++++++- 3 files changed, 666 insertions(+), 38 deletions(-) diff --git a/adapters/qq/src/bot.ts b/adapters/qq/src/bot.ts index c6681d4b..776f6feb 100644 --- a/adapters/qq/src/bot.ts +++ b/adapters/qq/src/bot.ts @@ -118,7 +118,7 @@ export class QQBot extends Bot { } async kickGuildMember(guildId: string, userId: string) { - await this.internal.deleteGuildMember(guildId, userId) + await this.internal.removeGuildMember(guildId, userId) } async muteGuildMember(guildId: string, userId: string, duration: number) { diff --git a/adapters/qq/src/internal.ts b/adapters/qq/src/internal.ts index 7d67d2b9..d5ea8542 100644 --- a/adapters/qq/src/internal.ts +++ b/adapters/qq/src/internal.ts @@ -8,20 +8,6 @@ export class Internal { return this.http().get('/users/@me') } - /** https://bot.q.qq.com/wiki/develop/api/openapi/dms/post_dms.html */ - async createDMS(recipient_id: string, source_guild_id: string) { - return this.http().post('/users/@me/dms', { - recipient_id, source_guild_id, - }) - } - - async getMessage(channelId: string, messageId: string) { - const { message } = await this.http().get<{ - message: QQ.Message - }>(`/channels/${channelId}/messages/${messageId}`) - return message - } - async getGuilds(params?: Partial<{ before: string after: string @@ -44,6 +30,24 @@ export class Internal { return this.http().get(`/channels/${channel_id}`) } + async createGuildChannel(guild_id: string, params: QQ.CreateGuildParams) { + return this.http().post(`/guilds/${guild_id}/channels`, params) + } + + async modifyChannel(channel_id: string, params: QQ.ModifyGuildParams) { + return this.http().patch(`/channels/${channel_id}/channels`, params) + } + + async deleteChannel(channel_id: string) { + return this.http().delete(`/channels/${channel_id}`) + } + + async getChannelOnlineNums(channel_id: string) { + return this.http().get<{ + online_nums: number + }>(`/channels/${channel_id}/online_nums`) + } + async getGuildMembers(guild_id: string, params?: Partial<{ after: string limit: number @@ -51,11 +55,23 @@ export class Internal { return this.http().get(`/guilds/${guild_id}/members`, { params }) } + async getGuildRoleMembers(guild_id: string, role_id: string, nextInput = '0') { + return await this.http().get<{ + data: QQ.Member[] + next: string + }>(`/guilds/${guild_id}/roles/${role_id}/members`, { + params: { + start_index: nextInput, + limit: 0, + }, + }) + } + async getGuildMember(guild_id: string, user_id: string) { return this.http().get(`/guilds/${guild_id}/members/${user_id}`) } - async deleteGuildMember(guild_id: string, user_id: string) { + async removeGuildMember(guild_id: string, user_id: string) { return this.http().delete(`/guilds/${guild_id}/members/${user_id}`) } @@ -63,12 +79,159 @@ export class Internal { return this.http().get(`/guilds/${guild_id}/roles`) } + async createGuildRole(guild_id: string, params: QQ.CreateGuildRoleParams) { + return this.http().post<{ + role_id: string + role: QQ.Role + }>(`/guilds/${guild_id}/roles`, params) + } + + async modifyGuildRole(guild_id: string, role_id: string, params: QQ.CreateGuildRoleParams) { + return this.http().patch<{ + guild_id: string + role_id: string + role: QQ.Role + }>(`/guilds/${guild_id}/roles/${role_id}`, params) + } + + async removeGuildRole(guild_id: string, role_id: string) { + return this.http().delete(`/guilds/${guild_id}/roles/${role_id}`) + } + + async addGuildMemberRole(guild_id: string, user_id: string, role_id: string, channel_id?: string) { + return this.http().put(`/guilds/${guild_id}/members/${user_id}/roles/${role_id}`, channel_id ? { + channel: { id: channel_id }, + } : {}) + } + + async removeGuildMemberRole(guild_id: string, user_id: string, role_id: string, channel_id?: string) { + return this.http().axios({ + url: `/guilds/${guild_id}/members/${user_id}/roles/${role_id}`, + method: 'delete', + data: channel_id ? { + channel: { id: channel_id }, + } : {}, + }) + } + + async getChannelMemberPermissions(channel_id: string, user_id: string) { + return this.http().get(`/channels/${channel_id}/members/${user_id}/permissions`) + } + + async modifyChannelMemberPermissions(channel_id: string, user_id: string, params: { + add: string + remove: string + }) { + return this.http().put(`/channels/${channel_id}/members/${user_id}/permissions`, params) + } + + async getChannelRole(channel_id: string, role_id: string) { + return this.http().get(`/channels/${channel_id}/roles/${role_id}/permissions`) + } + + async modifyChannelRole(channel_id: string, role_id: string, params: { + add: string + remove: string + }) { + return this.http().put(`/channels/${channel_id}/roles/${role_id}/permissions`, params) + } + + async getMessage(channelId: string, messageId: string) { + const { message } = await this.http().get<{ + message: QQ.Message + }>(`/channels/${channelId}/messages/${messageId}`) + return message + } + + async deleteMessage(channel_id: string, message_id: string, hidetip = false) { + return this.http().delete(`/channels/${channel_id}/messages/${message_id}?hidetip=${hidetip.toString()}`) + } + + async getMessageSetting(guild_id: string) { + return this.http().get(`/guilds/${guild_id}/messages/setting`) + } + + async createDMS(recipient_id: string, source_guild_id: string) { + return this.http().post('/users/@me/dms', { + recipient_id, source_guild_id, + }) + } + + async deleteDM(guild_id: string, message_id: string, hidetip = false) { + // guild_id 是 createDMS 之后的 id + return this.http().delete(`/dms/${guild_id}/messages/${message_id}?hidetip=${hidetip.toString()}`) + } + + async muteGuild(guild_id: string, duration: number) { + return this.http().patch(`/guilds/${guild_id}/mute`, { + mute_seconds: duration / 1000, + }) + } + async muteGuildMember(guild_id: string, user_id: string, duration: number) { return this.http().patch(`/guilds/${guild_id}/members/${user_id}/mute`, { mute_seconds: duration / 1000, }) } + async muteGuildMembers(guild_id: string, user_ids: string[], duration: number) { + return this.http().patch<{ + user_ids: string[] + }>(`/guilds/${guild_id}/mute`, { + mute_seconds: duration / 1000, + user_ids, + }) + } + + async createGuildAnnounce(guild_id: string, params: Partial) { + return this.http().post(`/guilds/${guild_id}/announces`, params) + } + + async removeGuildAnnounce(guild_id: string, message_id: string) { + return this.http().delete(`/guilds/${guild_id}/announces/${message_id}`) + } + + async createPinsMessage(channel_id: string, message_id: string) { + return this.http().put(`/channels/${channel_id}/pins/${message_id}`) + } + + async removePinsMessage(channel_id: string, message_id: string) { + return this.http().delete(`/channels/${channel_id}/pins/${message_id}`) + } + + async getPinsMessage(channel_id: string) { + return this.http().get(`/channels/${channel_id}/pins`) + } + + // @TODO test: since + async getSchedules(channel_id: string, since?: number) { + return this.http().get(`/channels/${channel_id}/schedules?since=${since ?? ''}`) + } + + async getSchedule(channel_id: string, schedule_id: string) { + return this.http().get(`/channels/${channel_id}/schedules/${schedule_id}`) + } + + async createSchedule(channel_id: string, schedule: QQ.Schedule) { + return this.http().post(`/channels/${channel_id}/schedules`, schedule) + } + + async modifySchedule(channel_id: string, schedule_id: string, schedule: QQ.Schedule) { + return this.http().patch(`/channels/${channel_id}/schedules/${schedule_id}`, schedule) + } + + async removeSchedule(channel_id: string, schedule_id: string) { + return this.http().delete(`/channels/${channel_id}/schedules/${schedule_id}`) + } + + async createReaction(channel_id: string, message_id: string, type: string, id: string) { + return this.http().put(`/channels/${channel_id}/messages/${message_id}/reactions/${type}/${id}`) + } + + async deleteReaction(channel_id: string, message_id: string, type: string, id: string) { + return this.http().delete(`/channels/${channel_id}/messages/${message_id}/reactions/${type}/${id}`) + } + async getReactions(channel_id: string, message_id: string, type: string, id: string, params?: Partial<{ cookie: string limit: number @@ -82,20 +245,37 @@ export class Internal { }) } - async createReaction(channel_id: string, message_id: string, type: string, id: string) { - return this.http().put(`/channels/${channel_id}/messages/${message_id}/reactions/${type}/${id}`) + async listThreads(channel_id: string) { + return this.http().get<{ + threads: QQ.Forum.Thread[] + is_finish: number + }>(`/channels/${channel_id}/threads`) } - async deleteReaction(channel_id: string, message_id: string, type: string, id: string) { - return this.http().delete(`/channels/${channel_id}/messages/${message_id}/reactions/${type}/${id}`) + async getThread(channel_id: string, thread_id: string) { + return this.http().get<{ + thread: QQ.Forum.Thread + }>(`/channels/${channel_id}/threads/${thread_id}`) } - async deleteMessage(channel_id: string, message_id: string) { - return this.http().delete(`/channels/${channel_id}/messages/${message_id}`) + async createPost(channel_id: string, data: QQ.CreatePostRequest) { + return this.http().put<{ + task_id: string + create_time: string + }>(`/channels/${channel_id}/threads`, data) } - async deleteDM(guild_id: string, message_id: string) { - // guild_id 是 createDMS 之后的 id - return this.http().delete(`/dms/${guild_id}/messages/${message_id}`) + async removePost(channel_id: string, thread_id: string) { + return this.http().delete(`/channels/${channel_id}/threads/${thread_id}`) + } + + async getGuildApiPermissions(guild_id: string) { + return this.http().get<{ + apis: QQ.APIPermission[] + }>(`/guilds/${guild_id}/api_permissions`) + } + + async createGuildApiPermissionDemand(guild_id: string, data: QQ.APIPermissionDemand) { + return this.http().post(`/guilds/${guild_id}/api_permissions/demand`, data) } } diff --git a/adapters/qq/src/types.ts b/adapters/qq/src/types.ts index a3f6ad72..efcc6304 100644 --- a/adapters/qq/src/types.ts +++ b/adapters/qq/src/types.ts @@ -34,6 +34,23 @@ export enum Intents { * - DIRECT_MESSAGE_DELETE 删除(撤回)消息事件 */ DIRECT_MESSAGES = 1 << 12, + /** + * 论坛事件, 此为公域的论坛事件 + * - OPEN_FORUM_THREAD_CREATE 当用户创建主题时 + * - OPEN_FORUM_THREAD_UPDATE 当用户更新主题时 + * - OPEN_FORUM_THREAD_DELETE 当用户删除主题时 + * - OPEN_FORUM_POST_CREATE 当用户创建帖子时 + * - OPEN_FORUM_POST_DELETE 当用户删除帖子时 + * - OPEN_FORUM_REPLY_CREATE 当用户回复评论时 + * - OPEN_FORUM_REPLY_DELETE 当用户删除评论时 + */ + OPEN_FORUMS_EVENT = 1 << 18, + /** + * 音视频/直播子频道成员进出事件 + * - AUDIO_OR_LIVE_CHANNEL_MEMBER_ENTER 当用户进入音视频/直播子频道 + * - AUDIO_OR_LIVE_CHANNEL_MEMBER_EXIT 当用户离开音视频/直播子频道 + */ + AUDIO_OR_LIVE_CHANNEL_MEMBER = 1 << 19, /** * - INTERACTION_CREATE 互动事件创建时 */ @@ -87,9 +104,13 @@ export enum Opcode { /** 当客户端与网关建立ws连接之后,网关下发的第一条消息 */ HELLO = 10, /** 当发送心跳成功之后,就会收到该消息 */ - HEARTBEAT_ACK = 11 + HEARTBEAT_ACK = 11, + /** 仅用于 http 回调模式的回包,代表机器人收到了平台推送的数据 */ + HTTP_CAKKBACK_ACK = 12 } +export type WithOpUser = T & { op_user_id: string } + export interface GatewayEvents { READY: { version: number @@ -99,22 +120,45 @@ export interface GatewayEvents { } RESUMED: string MESSAGE_CREATE: Message + MESSAGE_AUDIT_PASS: MessageAudited + MESSAGE_AUDIT_REJECT: MessageAudited AT_MESSAGE_CREATE: Message DIRECT_MESSAGE_CREATE: Message MESSAGE_REACTION_ADD: MessageReaction MESSAGE_REACTION_REMOVE: MessageReaction - GUILD_CREATE: Guild - GUILD_UPDATE: Guild - GUILD_DELETE: Guild - CHANNEL_CREATE: Channel - CHANNEL_UPDATE: Channel - CHANNEL_DELETE: Channel - GUILD_MEMBER_ADD: MemberWithGuild - GUILD_MEMBER_UPDATE: MemberWithGuild - GUILD_MEMBER_DELETE: MemberWithGuild + GUILD_CREATE: WithOpUser + GUILD_UPDATE: WithOpUser + GUILD_DELETE: WithOpUser + CHANNEL_CREATE: WithOpUser + CHANNEL_UPDATE: WithOpUser + CHANNEL_DELETE: WithOpUser + GUILD_MEMBER_ADD: WithOpUser + GUILD_MEMBER_UPDATE: WithOpUser + GUILD_MEMBER_DELETE: WithOpUser MESSAGE_DELETE: Message.DeletionPayload PUBLIC_MESSAGE_DELETE: Message.DeletionPayload DIRECT_MESSAGE_DELETE: Message.DeletionPayload + AUDIO_START: Partial + AUDIO_FINISH: Partial + AUDIO_ON_MIC: Partial + AUDIO_OFF_MIC: Partial + FORUM_THREAD_CREATE: Forum.Thread + FORUM_THREAD_UPDATE: Forum.Thread + FORUM_THREAD_DELETE: Forum.Thread + FORUM_POST_CREATE: Forum.Post + FORUM_POST_DELETE: Forum.Post + FORUM_REPLY_CREATE: Forum.Reply + FORUM_REPLY_DELETE: Forum.Reply + FORUM_PUBLISH_AUDIT_RESULT: Forum.AuditResult + OPEN_FORUM_THREAD_CREATE: Partial + OPEN_FORUM_THREAD_UPDATE: Partial + OPEN_FORUM_THREAD_DELETE: Partial + OPEN_FORUM_POST_CREATE: Partial + OPEN_FORUM_POST_DELETE: Partial + OPEN_FORUM_REPLY_CREATE: Partial + OPEN_FORUM_REPLY_DELETE: Partial + AUDIO_OR_LIVE_CHANNEL_MEMBER_ENTER: Partial + AUDIO_OR_LIVE_CHANNEL_MEMBER_EXIT: Partial } export interface PayloadStructure { @@ -183,6 +227,34 @@ export interface Attachment { width: number } +export interface AudioAction { + /** 频道id */ + guild_id: string + /** 子频道id */ + channel_id: string + /** 音频数据的url status为0时传 */ + audio_url: string + /** 状态文本(比如:简单爱-周杰伦),可选,status为0时传,其他操作不传 */ + text: string +} + +export interface MessageAudited { + /** 消息审核 id */ + audit_id: string + /** 消息 id,只有审核通过事件才会有值 */ + message_id: string + /** 频道 id */ + guild_id: string + /* 子频道 id */ + channel_id: string + /* 消息审核时间 */ + audit_time: string + /* 消息创建时间 */ + create_time: string + /** 子频道消息 seq,用于消息间的排序,seq 在同一子频道中按从先到后的顺序递增,不同的子频道之间消息无法排序 */ + seq_in_channel: string +} + export interface Message { /** 消息 id */ id: string @@ -327,8 +399,15 @@ export interface User { username: string avatar: string bot: boolean + /** 特殊关联应用的 openid,需要特殊申请并配置后才会返回。如需申请,请联系平台运营人员。 */ + union_openid: string + /** 机器人关联的互联应用的用户信息,与union_openid关联的应用是同一个。如需申请,请联系平台运营人员。 */ + union_user_account: string } +export type CreateGuildRoleParams = + Partial> + export interface Role { /** 身份组 ID , 默认值可参考 DefaultRoles */ id: string @@ -378,6 +457,16 @@ export interface Guild { joined_at?: string } +export type CreateGuildParams = + Pick & { + private_user_ids: string[] + } + +export type ModifyGuildParams = + Pick + + export enum ChannelType { /** 文字子频道 */ TEXT = 0, @@ -471,10 +560,30 @@ export interface MemberWithGuild { joined_at: string } +export interface CreateGuildAnnounceParams { + message_id: string + channel_id: string + announces_type: AnnounceType + recommend_channels: RecommendChannel[] +} + +export enum AnnounceType { + MEMBER = 0, + WELCOME = 1 +} + +/** 推荐子频道对象 */ +export interface RecommendChannel { + /** 子频道 id */ + channel_id: string + /** 推荐语 */ + introduce: string +} + /** * 公告对象 */ -export interface Announce { +export interface Announces { /** 频道 id */ guild_id: string /** 子频道 id */ @@ -564,9 +673,9 @@ export interface Schedule { /** 创建者 */ creator: Member /** 日程开始时跳转到的子频道 id */ - jumpchannel_id: string + jump_channel_id: string /** 日程提醒类型,取值参考 RemindType */ - remindType: RemindType + remind_type: RemindType } /** @@ -692,3 +801,342 @@ export interface Options { /** 重连时间间隔,单位 ms */ retryInterval?: number } + +export namespace Forum { + /** 话题频道内发表的主帖称为主题 */ + export interface Thread { + /** 频道ID */ + guild_id: string + /** 子频道ID */ + channel_id: string + /** 作者ID */ + author_id: string + /** 主帖内容 */ + thread_info: ThreadInfo + } + + /** 帖子事件包含的主帖内容相关信息 */ + export interface ThreadInfo { + /** 主帖ID */ + thread_id: string + /** 帖子标题 */ + title: string + /** 帖子内容 */ + content: string + /** 发表时间 */ + date_time: string + } + + /** 话题频道内对主题的评论称为帖子 */ + export interface Post { + /** 频道ID */ + guild_id: string + /** 子频道ID */ + channel_id: string + /** 作者ID */ + author_id: string + /** 帖子内容 */ + post_info: PostInfo + } + + /** 帖子事件包含的帖子内容信息 */ + export interface PostInfo { + /** 主题ID */ + thread_id: string + /** 帖子ID */ + post_id: string + /** 帖子内容 */ + content: string + /** 评论时间 */ + date_time: string + } + + /** 话题频道内对帖子的评论称为回复 */ + export interface Reply { + /** 频道ID */ + guild_id: string + /** 子频道ID */ + channel_id: string + /** 作者ID */ + author_id: string + /** 回复内容 */ + reply_info: ReplyInfo + } + + /** 回复事件包含的回复内容信息 */ + interface ReplyInfo { + /** 主题ID */ + thread_id: string + /** 帖子ID */ + post_id: string + /** 回复ID */ + reply_id: string + /** 回复内容 */ + content: string + /** 回复时间 */ + date_time: string + } + + /** 论坛帖子审核结果事件 */ + export interface AuditResult { + /** 频道ID */ + guild_id: string + /** 子频道ID */ + channel_id: string + /** 作者ID */ + author_id: string + /** 主题ID */ + thread_id: string + /** 帖子ID */ + post_id: string + /** 回复ID */ + reply_id: string + /** 审核的类型 */ + type: AuditType + /** 审核结果. 0:成功 1:失败 */ + result: number + /** result不为0时错误信息 */ + err_msg: string + } + + export enum AuditType { + /** 帖子 */ + PUBLISH_THREAD = 1, + /** 评论 */ + PUBLISH_POST = 2, + /** 回复 */ + PUBLISH_REPLY = 3 + } + /** 富文本内容 */ + export interface RichObject { + /** 富文本类型 */ + type: RichType + /** 文本 */ + text_info: TextInfo + /** @ 内容 */ + at_info: AtInfo + /** 链接 */ + url_info: URLInfo + /** 表情 */ + emoji_info: EmojiInfo + /** 提到的子频道 */ + channel_info: ChannelInfo + } + + export enum RichType { + /** 普通文本 */ + TEXT = 1, + /** at信息 */ + AT = 2, + /** url信息 */ + URL = 3, + /** 表情 */ + EMOJI = 4, + /** #子频道 */ + CHANNEL = 5, + /** 视频 */ + VIDEO = 10, + /** 图片 */ + IMAGE = 11 + } + + export interface TextInfo { + /** 普通文本 */ + text: string + } + + export interface AtInfo { + /** at类型 */ + type: AtType + /** 用户 */ + user_info: AtUserInfo + /** 角色组信息 */ + role_info: AtRoleInfo + /** 频道信息 */ + guild_info: AtGuildInfo + } + + export enum AtType { + /** at特定人 */ + AT_EXPLICIT_USER = 1, + /** at角色组所有人 */ + AT_ROLE_GROUP = 2, + /** at频道所有人 */ + AT_GUILD = 3 + } + + export interface AtUserInfo { + /** 身份组ID */ + id: string + /** 用户昵称 */ + nick: string + } + + export interface AtRoleInfo { + /** 身份组ID */ + role_id: number + /** 身份组名称 */ + name: string + /** 颜色值 */ + color: number + } + + export interface AtGuildInfo { + /** 频道ID */ + guild_id: string + /** 频道名称 */ + guild_name: string + } + + export interface URLInfo { + /** 链接地址 */ + url: string + /** 链接显示文本 */ + display_text: string + } + + export interface EmojiInfo { + /** 表情id */ + id: string + /** 表情类型 */ + type: string + /** 名称 */ + name: string + /** 链接 */ + url: string + } + + export interface ChannelInfo { + /** 子频道id */ + channel_id: number + /** 子频道名称 */ + channel_name: string + } + + /** 富文本内容 */ + export interface RichText { + /** 段落,一段落一行,段落内无元素的为空行 */ + paragraphs: Paragraph[] + } + + export interface Paragraph { + /** 元素列表 */ + elems: Elem[] + /** 段落属性 */ + props: ParagraphProps + } + + export interface Elem { + /** 文本元素 */ + text: TextElem + /** 图片元素 */ + image: ImageElem + /** 视频元素 */ + video: VideoElem + /** URL元素 */ + url: URLElem + /** 元素类型 */ + type: ElemType + } + + export enum ElemType { + /** 文本 */ + ELEM_TYPE_TEXT = 1, + /** 图片 */ + ELEM_TYPE_IMAGE = 2, + /** 视频 */ + ELEM_TYPE_VIDEO = 3, + /** URL */ + ELEM_TYPE_URL = 4 + } + + export interface TextElem { + /** 正文 */ + text: string + /** 文本属性 */ + props: TextProps + } + + export interface TextProps { + /** 加粗 */ + font_bold: boolean + /** 斜体 */ + italic: boolean + /** 下划线 */ + underline: boolean + } + + export interface ImageElem { + /** 第三方图片链接 */ + third_url: string + /** 宽度比例(缩放比,在屏幕里显示的比例) */ + width_percent: number + } + + export interface PlatImage { + /** 架平图片链接 */ + url: string + /** 图片宽度 */ + width: number + /** 图片高度 */ + height: number + /** 图片ID */ + image_id: string + } + + export interface VideoElem { + /** 第三方视频文件链接 */ + third_url: string + } + + export interface PlatVideo { + /** 架平图片链接 */ + url: string + /** 图片宽度 */ + width: number + /** 图片高度 */ + height: number + /** 视频ID */ + video_id: string + /** 视频时长 */ + duration: number + /** 视频封面图属性 */ + cover: PlatImage + } + + export interface URLElem { + /** URL链接 */ + url: string + /** URL描述 */ + desc: string + } + + export interface ParagraphProps { + /** 段落对齐方向属性,数值可以参考Alignment */ + alignment: Alignment + } + + enum Alignment { + /** 左对齐 */ + ALIGNMENT_LEFT = 0, + /** 居中 */ + ALIGNMENT_MIDDLE = 1, + /** 右对齐 */ + ALIGNMENT_RIGHT = 2 + } +} + +export interface CreatePostRequest { + /** 帖子标题 */ + title: string + /** 帖子内容 */ + content: string + /** 帖子文本格式 */ + format: PostFormat +} +export enum PostFormat { + FORMAT_TEXT = 1, + FORMAT_HTML = 2, + FORMAT_MARKDOWN = 3, + FORMAT_JSON = 4 +}