@onebots/adapter-instagram 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,166 @@
1
+ import { AdapterRegistry } from "onebots";
2
+ import { InstagramAdapter } from "./adapter.js";
3
+ import { INSTAGRAM_EVENT_TYPES, INSTAGRAM_WEBHOOK_FIELDS, instagramCapabilities, } from "./capabilities.js";
4
+ export { InstagramAdapter };
5
+ export { InstagramClient } from "./client.js";
6
+ export { describeInstagramCapabilities, INSTAGRAM_EVENT_TYPES, INSTAGRAM_WEBHOOK_FIELDS, instagramCapabilities, } from "./capabilities.js";
7
+ export { InstagramError } from "./errors.js";
8
+ export { projectInstagramEvent } from "./events.js";
9
+ export { compileInstagramMessage, projectApiMessage, projectWebhookMessage, } from "./messages.js";
10
+ export { executeInstagramPlatformAction, INSTAGRAM_PLATFORM_ACTIONS, } from "./platform-actions.js";
11
+ export { InstagramWebhookCodec } from "./webhook-codec.js";
12
+ const INSTAGRAM_PERMISSIONS = [
13
+ "instagram_business_basic",
14
+ "instagram_business_manage_messages",
15
+ "instagram_business_manage_comments",
16
+ "Human Agent",
17
+ ];
18
+ export const instagramSchema = {
19
+ account_id: {
20
+ type: "string",
21
+ required: true,
22
+ label: "账号标识",
23
+ description: "OneBots 内区分 Instagram Professional Account 的稳定标识",
24
+ ui: { section: "credentials" },
25
+ },
26
+ instagram_user_id: {
27
+ type: "string",
28
+ required: true,
29
+ label: "Instagram User ID",
30
+ pattern: /^\d+$/,
31
+ description: "Instagram Professional Account 的 Meta ID,不是 username",
32
+ ui: { section: "credentials" },
33
+ },
34
+ access_token: {
35
+ type: "string",
36
+ required: true,
37
+ label: "Instagram User Access Token",
38
+ sensitive: true,
39
+ description: "使用 Business Login for Instagram 签发;无需关联 Facebook Page",
40
+ ui: { section: "credentials" },
41
+ },
42
+ app_secret: {
43
+ type: "string",
44
+ label: "Meta App Secret",
45
+ sensitive: true,
46
+ description: "Webhook 签名校验必需,也用于 Graph appsecret_proof",
47
+ ui: { section: "credentials" },
48
+ },
49
+ verify_token: {
50
+ type: "string",
51
+ label: "Webhook Verify Token",
52
+ sensitive: true,
53
+ description: "与 Meta App Dashboard 中填写的自定义验证令牌完全一致",
54
+ ui: {
55
+ section: "credentials",
56
+ visibleWhen: { path: "receive_mode", oneOf: ["webhook"] },
57
+ },
58
+ },
59
+ declared_permissions: {
60
+ type: "array",
61
+ label: "已授予权限与功能",
62
+ choices: INSTAGRAM_PERMISSIONS.map(value => ({ value, label: value })),
63
+ allowCustomValues: true,
64
+ description: "用于动态收敛 Web 能力展示;Human Agent 是需单独审核的平台功能",
65
+ ui: {
66
+ widget: "choice-list",
67
+ section: "credentials",
68
+ itemLabel: "Permission / feature",
69
+ addLabel: "添加权限",
70
+ },
71
+ },
72
+ receive_mode: {
73
+ type: "string",
74
+ default: "webhook",
75
+ label: "事件接收方式",
76
+ choices: [
77
+ { value: "webhook", label: "Meta Webhook(挂载已有 Host)" },
78
+ { value: "manual", label: "手动 ingest(rawEvent)" },
79
+ ],
80
+ description: "两种方式共用同一个 Client、严格解析、去重与 canonical 投影",
81
+ ui: { section: "transport" },
82
+ },
83
+ http_path: {
84
+ type: "string",
85
+ label: "Webhook 挂载路径",
86
+ placeholder: "/instagram/{account_id}/events",
87
+ pattern: /^\/(?!\/)[^?#\u0000-\u001f\u007f]*$/,
88
+ description: "留空使用账号隔离路径;适配器不会另开监听端口",
89
+ ui: {
90
+ section: "transport",
91
+ visibleWhen: { path: "receive_mode", oneOf: ["webhook"] },
92
+ },
93
+ },
94
+ auto_subscribe: {
95
+ type: "boolean",
96
+ default: false,
97
+ label: "启动时订阅 Professional Account",
98
+ description: "调用 /{ig-user-id}/subscribed_apps",
99
+ ui: {
100
+ section: "transport",
101
+ visibleWhen: { path: "receive_mode", oneOf: ["webhook"] },
102
+ },
103
+ },
104
+ subscribed_fields: {
105
+ type: "array",
106
+ default: ["messages", "messaging_postbacks", "messaging_seen", "message_reactions"],
107
+ label: "Webhook Fields",
108
+ choices: INSTAGRAM_WEBHOOK_FIELDS.map(value => ({ value, label: value })),
109
+ description: "可逐项增减,并同步收敛账号实际能力;无需手写 JSON",
110
+ ui: {
111
+ widget: "choice-list",
112
+ section: "transport",
113
+ itemLabel: "Webhook field",
114
+ addLabel: "添加 field",
115
+ visibleWhen: { path: "receive_mode", oneOf: ["webhook"] },
116
+ },
117
+ },
118
+ event_types: {
119
+ type: "array",
120
+ default: [...INSTAGRAM_EVENT_TYPES],
121
+ label: "接收事件",
122
+ choices: INSTAGRAM_EVENT_TYPES.map(value => ({ value, label: value })),
123
+ description: "在 batch 展开后过滤;已接收事件仍完整保留原始 envelope",
124
+ ui: {
125
+ widget: "choice-list",
126
+ section: "filter",
127
+ itemLabel: "Instagram 事件",
128
+ addLabel: "添加事件",
129
+ },
130
+ },
131
+ api_version: {
132
+ type: "string",
133
+ default: "v25.0",
134
+ label: "Graph API Version",
135
+ pattern: /^v\d+\.\d+$/,
136
+ ui: { section: "advanced" },
137
+ },
138
+ api_origin: {
139
+ type: "string",
140
+ default: "https://graph.instagram.com",
141
+ label: "Graph API Origin",
142
+ description: "仅代理、测试或私有网关需要修改;生产应保持官方 HTTPS host",
143
+ ui: { section: "advanced" },
144
+ },
145
+ max_body_bytes: {
146
+ type: "number",
147
+ default: 10485760,
148
+ min: 1,
149
+ max: 52428800,
150
+ label: "Webhook Body 上限(bytes)",
151
+ ui: {
152
+ section: "advanced",
153
+ visibleWhen: { path: "receive_mode", oneOf: ["webhook"] },
154
+ },
155
+ },
156
+ };
157
+ AdapterRegistry.registerSchema("instagram", instagramSchema);
158
+ AdapterRegistry.register("instagram", InstagramAdapter, {
159
+ name: "instagram",
160
+ displayName: "Instagram Messaging",
161
+ description: "Instagram Login、Send、Conversations、Profile、Webhook 与平台扩展适配器",
162
+ icon: "https://static.cdninstagram.com/rsrc.php/v4/yR/r/lam-fZmwmvn.png",
163
+ homepage: "https://www.postman.com/meta/instagram/overview",
164
+ author: "凉菜",
165
+ capabilities: instagramCapabilities,
166
+ });
@@ -0,0 +1,12 @@
1
+ import type { CommonTypes } from "onebots";
2
+ import type { InstagramApiMessage, InstagramMessage, InstagramOutgoingMessage } from "./types.js";
3
+ export interface InstagramAttachmentUploader {
4
+ upload(type: "image" | "video" | "audio", source: {
5
+ blob: Blob;
6
+ filename: string;
7
+ }, reusable: boolean): Promise<string>;
8
+ }
9
+ /** 编译为一个 Instagram Send API message;不隐式拆分成多个 message_id。 */
10
+ export declare function compileInstagramMessage(segments: readonly CommonTypes.Segment[], uploader: InstagramAttachmentUploader): Promise<InstagramOutgoingMessage>;
11
+ export declare function projectWebhookMessage(message: InstagramMessage): CommonTypes.Segment[];
12
+ export declare function projectApiMessage(message: InstagramApiMessage): CommonTypes.Segment[];
@@ -0,0 +1,177 @@
1
+ import { InstagramError } from "./errors.js";
2
+ import { assertHttpsUrl, requireArray, requireRecord, requireString } from "./validation.js";
3
+ /** 编译为一个 Instagram Send API message;不隐式拆分成多个 message_id。 */
4
+ export async function compileInstagramMessage(segments, uploader) {
5
+ const native = segments.filter(segment => segment.type === "instagram");
6
+ if (native.length) {
7
+ if (native.length !== 1 || segments.length !== 1) {
8
+ return invalid("instagram 原生段必须独占一条消息");
9
+ }
10
+ return structuredClone(requireRecord(native[0].data.message ?? native[0].data, "instagram.message"));
11
+ }
12
+ const replies = segments.filter(segment => segment.type === "reply");
13
+ const quickReplies = segments.filter(segment => segment.type === "instagram_quick_replies");
14
+ const media = segments.filter(segment => ["image", "video", "audio", "record"].includes(segment.type));
15
+ const unsupported = segments.filter(segment => ![
16
+ "text",
17
+ "reply",
18
+ "image",
19
+ "video",
20
+ "audio",
21
+ "record",
22
+ "instagram_quick_replies",
23
+ ].includes(segment.type));
24
+ if (unsupported.length)
25
+ return invalid(`不支持消息段 ${unsupported[0].type}`);
26
+ if (replies.length > 1)
27
+ return invalid("一条消息只能包含一个 reply 段");
28
+ if (quickReplies.length > 1)
29
+ return invalid("一条消息只能包含一组 quick replies");
30
+ if (media.length > 1)
31
+ return invalid("Instagram 单次 Send API 只支持一个媒体附件");
32
+ const text = segments
33
+ .filter(segment => segment.type === "text")
34
+ .map(segment => String(segment.data.text ?? ""))
35
+ .join("");
36
+ if (text && media.length) {
37
+ return invalid("Instagram 单次 Send API 不能同时发送文本和媒体,请拆成两条消息");
38
+ }
39
+ if (quickReplies.length && !text)
40
+ return invalid("quick replies 必须附着在文本消息上");
41
+ const message = {};
42
+ if (text)
43
+ message.text = text;
44
+ if (media.length)
45
+ message.attachment = await compileAttachment(media[0], uploader);
46
+ if (replies.length) {
47
+ message.reply_to = {
48
+ mid: firstString(replies[0].data, ["message_id", "id"], "reply"),
49
+ };
50
+ }
51
+ if (quickReplies.length) {
52
+ const items = requireArray(quickReplies[0].data.items ?? quickReplies[0].data.quick_replies, "instagram_quick_replies.items");
53
+ if (!items.length || items.length > 13) {
54
+ return invalid("Instagram quick replies 必须包含 1 到 13 项");
55
+ }
56
+ message.quick_replies = items.map((item, index) => validateQuickReply(item, `instagram_quick_replies.items[${index}]`));
57
+ }
58
+ if (!message.text && !message.attachment)
59
+ return invalid("消息没有可发送内容");
60
+ return message;
61
+ }
62
+ export function projectWebhookMessage(message) {
63
+ const segments = [];
64
+ const replyMid = message.reply_to?.mid;
65
+ if (typeof replyMid === "string" && replyMid) {
66
+ segments.push({ type: "reply", data: { id: replyMid } });
67
+ }
68
+ if (message.text)
69
+ segments.push({ type: "text", data: { text: message.text } });
70
+ for (const attachment of message.attachments || []) {
71
+ segments.push(projectWebhookAttachment(attachment));
72
+ }
73
+ if (message.quick_reply) {
74
+ segments.push({
75
+ type: "instagram_quick_reply",
76
+ data: { payload: message.quick_reply.payload },
77
+ });
78
+ }
79
+ if (message.referral) {
80
+ segments.push({ type: "instagram_referral", data: structuredClone(message.referral) });
81
+ }
82
+ if (message.reply_to && typeof replyMid !== "string") {
83
+ segments.push({ type: "instagram_reply_context", data: structuredClone(message.reply_to) });
84
+ }
85
+ return segments;
86
+ }
87
+ export function projectApiMessage(message) {
88
+ return message.message ? [{ type: "text", data: { text: message.message } }] : [];
89
+ }
90
+ async function compileAttachment(segment, uploader) {
91
+ const type = normalizeMediaType(segment.type);
92
+ const data = requireRecord(segment.data, `${segment.type}.data`);
93
+ const attachmentId = optionalString(data.attachment_id ?? data.id);
94
+ if (attachmentId)
95
+ return { type, payload: { attachment_id: attachmentId } };
96
+ if (data.path !== undefined) {
97
+ return invalid("消息媒体不读取宿主本地路径;请传 base64 data、HTTPS URL 或 attachment_id");
98
+ }
99
+ const remote = optionalString(data.url ?? data.file);
100
+ if (remote) {
101
+ return { type, payload: { url: assertHttpsUrl(remote, `${segment.type}.url`) } };
102
+ }
103
+ const encoded = optionalString(data.data);
104
+ if (!encoded)
105
+ return invalid(`${segment.type} 缺少 url/file、data 或 attachment_id`);
106
+ const materialized = decodeMediaData(encoded, optionalString(data.name ?? data.filename) || "attachment.bin", optionalString(data.mime_type ?? data.content_type));
107
+ const attachmentIdFromUpload = await uploader.upload(type, {
108
+ blob: new Blob([new Uint8Array(materialized.data)], {
109
+ type: materialized.contentType,
110
+ }),
111
+ filename: materialized.filename,
112
+ }, true);
113
+ return { type, payload: { attachment_id: attachmentIdFromUpload } };
114
+ }
115
+ function validateQuickReply(value, field) {
116
+ const item = structuredClone(requireRecord(value, field));
117
+ const contentType = requireString(item.content_type, `${field}.content_type`);
118
+ if (!["text", "user_phone_number", "user_email"].includes(contentType)) {
119
+ return invalid(`${field}.content_type 无效`);
120
+ }
121
+ if (contentType === "text") {
122
+ const title = requireString(item.title, `${field}.title`);
123
+ if ([...title].length > 20)
124
+ return invalid(`${field}.title 不能超过 20 个字符`);
125
+ }
126
+ requireString(item.payload, `${field}.payload`);
127
+ return item;
128
+ }
129
+ function projectWebhookAttachment(attachment) {
130
+ const type = ["image", "video", "audio"].includes(attachment.type)
131
+ ? attachment.type
132
+ : "instagram_attachment";
133
+ return {
134
+ type,
135
+ data: {
136
+ ...(typeof attachment.payload.url === "string" ? { url: attachment.payload.url } : {}),
137
+ instagram_attachment: structuredClone(attachment),
138
+ },
139
+ };
140
+ }
141
+ function decodeMediaData(source, filename, declaredType) {
142
+ const match = source.match(/^data:([^;,]+);base64,(.*)$/su);
143
+ const encoded = match ? match[2] : source;
144
+ if (!encoded || encoded.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(encoded)) {
145
+ return invalid("媒体 data 不是有效 base64");
146
+ }
147
+ const data = new Uint8Array(Buffer.from(encoded, "base64"));
148
+ if (!data.byteLength || data.byteLength > 25 * 1024 * 1024) {
149
+ return invalid("媒体 data 必须介于 1 byte 与 25 MiB");
150
+ }
151
+ const contentType = match?.[1] || declaredType || "application/octet-stream";
152
+ if (!/^[\w!#$&^.+-]+\/[\w!#$&^.+-]+$/u.test(contentType)) {
153
+ return invalid("媒体 content type 无效");
154
+ }
155
+ return { data, filename: safeFilename(filename), contentType };
156
+ }
157
+ function safeFilename(value) {
158
+ const filename = value.replace(/\\/gu, "/").split("/").at(-1) || "attachment.bin";
159
+ return filename.replace(/[\u0000-\u001f\u007f"\\]/gu, "_").slice(0, 255);
160
+ }
161
+ function normalizeMediaType(type) {
162
+ return type === "record" ? "audio" : type;
163
+ }
164
+ function firstString(data, fields, context) {
165
+ for (const field of fields) {
166
+ const value = optionalString(data[field]);
167
+ if (value)
168
+ return value;
169
+ }
170
+ return invalid(`${context} 缺少 ${fields.join("/")}`);
171
+ }
172
+ function optionalString(value) {
173
+ return typeof value === "string" && value ? value : undefined;
174
+ }
175
+ function invalid(message) {
176
+ throw InstagramError.invalid(`Instagram ${message}`);
177
+ }
@@ -0,0 +1,4 @@
1
+ import type { InstagramClient } from "./client.js";
2
+ export declare const INSTAGRAM_PLATFORM_ACTIONS: import("@onebots/core").PlatformActionSet<"call_instagram_api" | "send_instagram_native" | "send_instagram_human_agent" | "send_instagram_private_reply" | "react_instagram_message" | "send_instagram_media_share" | "send_instagram_like_heart" | "list_instagram_conversations" | "find_instagram_conversation" | "get_instagram_conversation" | "get_instagram_messenger_profile" | "set_instagram_messenger_profile" | "delete_instagram_messenger_profile" | "upload_instagram_attachment" | "subscribe_instagram_webhooks" | "get_instagram_subscribed_apps" | "delete_instagram_webhook_subscription" | "list_instagram_welcome_message_flows" | "create_instagram_welcome_message_flow" | "update_instagram_welcome_message_flow" | "delete_instagram_welcome_message_flow">;
3
+ export type InstagramPlatformAction = typeof INSTAGRAM_PLATFORM_ACTIONS extends ReadonlySet<infer T> ? T : never;
4
+ export declare function executeInstagramPlatformAction(client: InstagramClient, action: string, params: Readonly<Record<string, unknown>>): Promise<unknown>;
@@ -0,0 +1,259 @@
1
+ import { definePlatformActionContract } from "onebots";
2
+ import { parseAttachmentId, parseSuccess } from "./entities.js";
3
+ import { InstagramError } from "./errors.js";
4
+ import { INSTAGRAM_WEBHOOK_FIELDS, } from "./types.js";
5
+ import { assertHttpsUrl, assertMetaId, assertNumericMetaId, requireArray, requireRecord, requireString, } from "./validation.js";
6
+ const handlers = {
7
+ call_instagram_api: (client, params) => client.call(graphMethod(params.method), requireString(params.path, "path"), {
8
+ query: queryRecord(params.query),
9
+ body: params.body === undefined ? undefined : cloneValue(params.body, "body"),
10
+ }),
11
+ send_instagram_native: (client, params) => client.send(assertNumericMetaId(params.recipient_id, "recipient_id"), cloneMessage(params.message)),
12
+ send_instagram_human_agent: (client, params) => client.send(assertNumericMetaId(params.recipient_id, "recipient_id"), cloneMessage(params.message), { humanAgent: true }),
13
+ send_instagram_private_reply: (client, params) => client.sendPrivateReply(assertNumericMetaId(params.comment_id, "comment_id"), requireString(params.text, "text")),
14
+ react_instagram_message: (client, params) => client.react(assertNumericMetaId(params.recipient_id, "recipient_id"), assertMetaId(params.message_id, "message_id"), reactionAction(params.action)),
15
+ send_instagram_media_share: (client, params) => client.send(assertNumericMetaId(params.recipient_id, "recipient_id"), {
16
+ attachment: {
17
+ type: "MEDIA_SHARE",
18
+ payload: { id: assertNumericMetaId(params.media_id, "media_id") },
19
+ },
20
+ }),
21
+ send_instagram_like_heart: (client, params) => client.send(assertNumericMetaId(params.recipient_id, "recipient_id"), {
22
+ attachment: { type: "like_heart" },
23
+ }),
24
+ list_instagram_conversations: (client, params) => client.listConversations(optionalString(params.after, "after"), optionalLimit(params.limit)),
25
+ find_instagram_conversation: (client, params) => client.findConversation(assertNumericMetaId(params.user_id, "user_id")),
26
+ get_instagram_conversation: (client, params) => client.getConversation(assertMetaId(params.conversation_id, "conversation_id"), optionalMessageLimit(params.limit)),
27
+ get_instagram_messenger_profile: (client, params) => client.call("GET", `/${client.config.instagram_user_id}/messenger_profile`, {
28
+ query: { fields: profileFields(params.fields).join(",") },
29
+ }),
30
+ set_instagram_messenger_profile: (client, params) => client.call("POST", `/${client.config.instagram_user_id}/messenger_profile`, {
31
+ body: profileBody(params.profile),
32
+ }),
33
+ delete_instagram_messenger_profile: (client, params) => client.call("DELETE", `/${client.config.instagram_user_id}/messenger_profile`, {
34
+ body: { fields: profileFields(params.fields) },
35
+ }),
36
+ upload_instagram_attachment: async (client, params) => {
37
+ const attachmentId = await client.uploadAttachment(attachmentType(params.type), { url: assertHttpsUrl(params.url, "url") }, params.is_reusable !== false);
38
+ return { attachment_id: parseAttachmentId({ attachment_id: attachmentId }) };
39
+ },
40
+ subscribe_instagram_webhooks: async (client, params) => {
41
+ const fields = webhookFields(params.subscribed_fields);
42
+ return parseSuccess(await client.call("POST", `/${client.config.instagram_user_id}/subscribed_apps`, {
43
+ query: { subscribed_fields: fields.join(",") },
44
+ }), "subscribe Instagram webhook response");
45
+ },
46
+ get_instagram_subscribed_apps: client => client.call("GET", `/${client.config.instagram_user_id}/subscribed_apps`),
47
+ delete_instagram_webhook_subscription: async (client) => parseSuccess(await client.call("DELETE", `/${client.config.instagram_user_id}/subscribed_apps`), "delete Instagram webhook subscription response"),
48
+ list_instagram_welcome_message_flows: (client, params) => client.call("GET", `/${client.config.instagram_user_id}/welcome_message_flows`, {
49
+ query: { flow_id: optionalMetaId(params.flow_id, "flow_id") },
50
+ }),
51
+ create_instagram_welcome_message_flow: (client, params) => client.call("POST", `/${client.config.instagram_user_id}/welcome_message_flows`, {
52
+ body: welcomeFlow(params.flow, false),
53
+ }),
54
+ update_instagram_welcome_message_flow: (client, params) => client.call("POST", `/${client.config.instagram_user_id}/welcome_message_flows`, {
55
+ query: { flow_id: assertMetaId(params.flow_id, "flow_id") },
56
+ body: welcomeFlow(params.flow, true),
57
+ }),
58
+ delete_instagram_welcome_message_flow: async (client, params) => parseSuccess(await client.call("DELETE", `/${client.config.instagram_user_id}/welcome_message_flows`, {
59
+ query: { flow_id: assertMetaId(params.flow_id, "flow_id") },
60
+ }), "delete Instagram welcome message flow response"),
61
+ };
62
+ const parameters = {
63
+ call_instagram_api: ["method", "path", "query", "body"],
64
+ send_instagram_native: ["recipient_id", "message"],
65
+ send_instagram_human_agent: ["recipient_id", "message"],
66
+ send_instagram_private_reply: ["comment_id", "text"],
67
+ react_instagram_message: ["recipient_id", "message_id", "action"],
68
+ send_instagram_media_share: ["recipient_id", "media_id"],
69
+ send_instagram_like_heart: ["recipient_id"],
70
+ list_instagram_conversations: ["after", "limit"],
71
+ find_instagram_conversation: ["user_id"],
72
+ get_instagram_conversation: ["conversation_id", "limit"],
73
+ get_instagram_messenger_profile: ["fields"],
74
+ set_instagram_messenger_profile: ["profile"],
75
+ delete_instagram_messenger_profile: ["fields"],
76
+ upload_instagram_attachment: ["type", "url", "is_reusable"],
77
+ subscribe_instagram_webhooks: ["subscribed_fields"],
78
+ get_instagram_subscribed_apps: [],
79
+ delete_instagram_webhook_subscription: [],
80
+ list_instagram_welcome_message_flows: ["flow_id"],
81
+ create_instagram_welcome_message_flow: ["flow"],
82
+ update_instagram_welcome_message_flow: ["flow_id", "flow"],
83
+ delete_instagram_welcome_message_flow: ["flow_id"],
84
+ };
85
+ const actions = definePlatformActionContract(handlers, parameters, {
86
+ unsupported: action => new InstagramError(`未知 Instagram 平台动作: ${action}`, {
87
+ code: "INSTAGRAM_ACTION_NOT_FOUND",
88
+ status: 404,
89
+ }),
90
+ unexpectedParameter: (action, parameter) => InstagramError.invalid(`Instagram 动作 ${action} 不接受参数 ${parameter}`),
91
+ });
92
+ export const INSTAGRAM_PLATFORM_ACTIONS = actions.actions;
93
+ export async function executeInstagramPlatformAction(client, action, params) {
94
+ return actions.execute(client, action, params);
95
+ }
96
+ function graphMethod(value) {
97
+ if (value === "GET" || value === "POST" || value === "DELETE")
98
+ return value;
99
+ throw InstagramError.invalid("method 必须是 GET、POST 或 DELETE");
100
+ }
101
+ function queryRecord(value) {
102
+ if (value === undefined)
103
+ return undefined;
104
+ const query = requireRecord(value, "query");
105
+ const result = {};
106
+ for (const [key, item] of Object.entries(query)) {
107
+ if (typeof item === "string" ||
108
+ typeof item === "number" ||
109
+ typeof item === "boolean" ||
110
+ item === undefined) {
111
+ result[key] = item;
112
+ }
113
+ else if (Array.isArray(item) && item.every(entry => typeof entry === "string")) {
114
+ result[key] = item;
115
+ }
116
+ else {
117
+ throw InstagramError.invalid(`query.${key} 类型无效`);
118
+ }
119
+ }
120
+ return result;
121
+ }
122
+ function cloneValue(value, field) {
123
+ try {
124
+ return structuredClone(value);
125
+ }
126
+ catch (error) {
127
+ throw InstagramError.invalid(`${field} 必须是可结构化克隆的数据`, {
128
+ cause: String(error),
129
+ });
130
+ }
131
+ }
132
+ function cloneMessage(value) {
133
+ return structuredClone(requireRecord(value, "message"));
134
+ }
135
+ function stringList(value, field) {
136
+ const values = requireArray(value, field).map((item, index) => requireString(item, `${field}[${index}]`));
137
+ if (!values.length)
138
+ throw InstagramError.invalid(`${field} 不能为空`);
139
+ if (new Set(values).size !== values.length) {
140
+ throw InstagramError.invalid(`${field} 不能包含重复项`);
141
+ }
142
+ return values;
143
+ }
144
+ function webhookFields(value) {
145
+ const fields = stringList(value, "subscribed_fields");
146
+ const supported = new Set(INSTAGRAM_WEBHOOK_FIELDS);
147
+ if (fields.some(field => !supported.has(field))) {
148
+ throw InstagramError.invalid("subscribed_fields 包含当前 Instagram API 未定义字段");
149
+ }
150
+ return fields;
151
+ }
152
+ function profileFields(value) {
153
+ const fields = stringList(value, "fields");
154
+ if (fields.some(field => field !== "persistent_menu" && field !== "ice_breakers")) {
155
+ throw InstagramError.invalid("Messenger Profile fields 仅支持 persistent_menu 与 ice_breakers");
156
+ }
157
+ return fields;
158
+ }
159
+ function profileBody(value) {
160
+ const profile = structuredClone(requireRecord(value, "profile"));
161
+ const allowed = new Set(["platform", "persistent_menu", "ice_breakers"]);
162
+ const unexpected = Object.keys(profile).find(field => !allowed.has(field));
163
+ if (unexpected)
164
+ throw InstagramError.invalid(`profile 不接受字段 ${unexpected}`);
165
+ if (profile.platform !== undefined && profile.platform !== "instagram") {
166
+ throw InstagramError.invalid("profile.platform 必须是 instagram");
167
+ }
168
+ if (profile.persistent_menu === undefined && profile.ice_breakers === undefined) {
169
+ throw InstagramError.invalid("profile 必须包含 persistent_menu 或 ice_breakers");
170
+ }
171
+ if (profile.persistent_menu !== undefined) {
172
+ profile.persistent_menu = requireArray(profile.persistent_menu, "profile.persistent_menu").map((item, index) => profileLocale(item, `profile.persistent_menu[${index}]`));
173
+ }
174
+ if (profile.ice_breakers !== undefined) {
175
+ const iceBreakers = requireArray(profile.ice_breakers, "profile.ice_breakers");
176
+ if (!iceBreakers.length || iceBreakers.length > 4) {
177
+ throw InstagramError.invalid("profile.ice_breakers 必须包含 1 到 4 项");
178
+ }
179
+ profile.ice_breakers = iceBreakers.map((item, index) => profileIceBreaker(item, `profile.ice_breakers[${index}]`));
180
+ }
181
+ return { ...profile, platform: "instagram" };
182
+ }
183
+ function profileLocale(value, field) {
184
+ const locale = structuredClone(requireRecord(value, field));
185
+ requireString(locale.locale, `${field}.locale`);
186
+ const actions = requireArray(locale.call_to_actions, `${field}.call_to_actions`);
187
+ if (!actions.length)
188
+ throw InstagramError.invalid(`${field}.call_to_actions 不能为空`);
189
+ locale.call_to_actions = actions.map((item, index) => profileMenuAction(item, `${field}.call_to_actions[${index}]`));
190
+ return locale;
191
+ }
192
+ function profileMenuAction(value, field) {
193
+ const action = structuredClone(requireRecord(value, field));
194
+ const type = requireString(action.type, `${field}.type`);
195
+ requireString(action.title, `${field}.title`);
196
+ if (type === "postback")
197
+ requireString(action.payload, `${field}.payload`);
198
+ else if (type === "web_url")
199
+ action.url = assertHttpsUrl(action.url, `${field}.url`);
200
+ else
201
+ throw InstagramError.invalid(`${field}.type 必须是 postback 或 web_url`);
202
+ return action;
203
+ }
204
+ function profileIceBreaker(value, field) {
205
+ const action = structuredClone(requireRecord(value, field));
206
+ requireString(action.question, `${field}.question`);
207
+ requireString(action.payload, `${field}.payload`);
208
+ return action;
209
+ }
210
+ function welcomeFlow(value, updating) {
211
+ const flow = structuredClone(requireRecord(value, "flow"));
212
+ const name = requireString(flow.name, "flow.name");
213
+ const messages = requireArray(flow.welcome_message_flow, "flow.welcome_message_flow").map((item, index) => structuredClone(requireRecord(item, `flow.welcome_message_flow[${index}]`)));
214
+ if (!messages.length)
215
+ throw InstagramError.invalid("flow.welcome_message_flow 不能为空");
216
+ if (updating && flow.eligible_platforms !== undefined) {
217
+ throw InstagramError.invalid("更新 Welcome Message Flow 时不接受 eligible_platforms");
218
+ }
219
+ return updating
220
+ ? { ...flow, name, welcome_message_flow: messages }
221
+ : {
222
+ ...flow,
223
+ name,
224
+ welcome_message_flow: messages,
225
+ eligible_platforms: ["instagram"],
226
+ };
227
+ }
228
+ function optionalString(value, field) {
229
+ return value === undefined ? undefined : requireString(value, field);
230
+ }
231
+ function optionalMetaId(value, field) {
232
+ return value === undefined ? undefined : assertMetaId(value, field);
233
+ }
234
+ function optionalLimit(value) {
235
+ if (value === undefined)
236
+ return 25;
237
+ if (!Number.isSafeInteger(value) || Number(value) < 1 || Number(value) > 100) {
238
+ throw InstagramError.invalid("limit 必须是 1 到 100 的安全整数");
239
+ }
240
+ return Number(value);
241
+ }
242
+ function optionalMessageLimit(value) {
243
+ if (value === undefined)
244
+ return 20;
245
+ if (!Number.isSafeInteger(value) || Number(value) < 1 || Number(value) > 20) {
246
+ throw InstagramError.invalid("message limit 必须是 1 到 20 的安全整数");
247
+ }
248
+ return Number(value);
249
+ }
250
+ function attachmentType(value) {
251
+ if (value === "image" || value === "video" || value === "audio")
252
+ return value;
253
+ throw InstagramError.invalid("attachment type 必须是 image、video 或 audio");
254
+ }
255
+ function reactionAction(value) {
256
+ if (value === "react" || value === "unreact")
257
+ return value;
258
+ throw InstagramError.invalid("reaction action 必须是 react 或 unreact");
259
+ }