@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/LICENSE +21 -0
- package/README.md +36 -0
- package/lib/adapter-support.d.ts +10 -0
- package/lib/adapter-support.js +78 -0
- package/lib/adapter.d.ts +24 -0
- package/lib/adapter.js +191 -0
- package/lib/capabilities.d.ts +6 -0
- package/lib/capabilities.js +167 -0
- package/lib/client.d.ts +49 -0
- package/lib/client.js +237 -0
- package/lib/entities.d.ts +9 -0
- package/lib/entities.js +133 -0
- package/lib/errors.d.ts +7 -0
- package/lib/errors.js +27 -0
- package/lib/events.d.ts +7 -0
- package/lib/events.js +122 -0
- package/lib/http-host.d.ts +14 -0
- package/lib/http-host.js +64 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +166 -0
- package/lib/messages.d.ts +12 -0
- package/lib/messages.js +177 -0
- package/lib/platform-actions.d.ts +4 -0
- package/lib/platform-actions.js +259 -0
- package/lib/types.d.ts +171 -0
- package/lib/types.js +29 -0
- package/lib/validation.d.ts +12 -0
- package/lib/validation.js +96 -0
- package/lib/webhook-codec.d.ts +10 -0
- package/lib/webhook-codec.js +263 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 凉菜
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @onebots/adapter-instagram
|
|
2
|
+
|
|
3
|
+
Instagram Messaging adapter for OneBots, based on the current Instagram API with Instagram Login and Graph v25.0. It uses `graph.instagram.com`, does not require a Facebook Page, and never opens its own listener port.
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 可嵌入 `InstagramClient`,支持已有 Fetch/Koa Host 的 `acceptHttp()` / `ingestHttp()` 与最底层 `ingest(rawEvent)`;
|
|
8
|
+
- 精确 raw-body `X-Hub-Signature-256` 校验、batch 展开、可靠去重与严格外部 JSON 校验;
|
|
9
|
+
- Send API、附件上传、Conversations、消息详情与 IGSID User Profile;
|
|
10
|
+
- quick replies、generic/button 原生消息、like-heart、published post media share 与 reaction;
|
|
11
|
+
- Messenger Profile、Professional Account webhook subscription 与 Welcome Message Flows;
|
|
12
|
+
- comment private reply 与显式 Human Agent 动作,并保留官方时间窗和用途限制;
|
|
13
|
+
- 配置的 webhook fields、事件和已声明权限会动态收敛账号能力。
|
|
14
|
+
|
|
15
|
+
## Standalone client
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { InstagramClient } from "@onebots/adapter-instagram";
|
|
19
|
+
|
|
20
|
+
const client = new InstagramClient({
|
|
21
|
+
account_id: "support",
|
|
22
|
+
instagram_user_id: "1234567890",
|
|
23
|
+
access_token: process.env.INSTAGRAM_ACCESS_TOKEN!,
|
|
24
|
+
receive_mode: "manual",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
client.on("event", delivery => dispatch(delivery));
|
|
28
|
+
await client.start();
|
|
29
|
+
await client.ingest(rawInstagramWebhookEnvelope);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Webhook 模式必须把宿主收到的精确原始字节交给 Client,不能先解析再序列化。Human Agent 只允许 7 天内由真实人工客服发送;适配器不会把它设为普通消息默认行为。
|
|
33
|
+
|
|
34
|
+
## English
|
|
35
|
+
|
|
36
|
+
The typed client shares one Graph transport and reliable webhook pipeline with the OneBots adapter. Existing hosts can pass a Fetch `Request` to `acceptHttp()`, use structured `ingestHttp()` with exact raw bytes, or pass a decoded envelope to `ingest()`. See the [Chinese platform guide](../../docs/src/platform/instagram.md) or [English platform guide](../../docs/src/en/platform/instagram.md).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Account, Adapter } from "onebots";
|
|
2
|
+
import type { InstagramConfig } from "./types.js";
|
|
3
|
+
export declare function normalizeInstagramConfig(config: Account.Config<"instagram">): InstagramConfig;
|
|
4
|
+
export declare function instagramUploadSource(params: Adapter.UploadFileParams): {
|
|
5
|
+
url: string;
|
|
6
|
+
} | {
|
|
7
|
+
blob: Blob;
|
|
8
|
+
filename: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function instagramAttachmentType(filename: string): "image" | "video" | "audio";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { InstagramError } from "./errors.js";
|
|
2
|
+
const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
3
|
+
export function normalizeInstagramConfig(config) {
|
|
4
|
+
const receiveMode = config.receive_mode || "webhook";
|
|
5
|
+
return {
|
|
6
|
+
...config,
|
|
7
|
+
receive_mode: receiveMode,
|
|
8
|
+
http_path: receiveMode === "manual"
|
|
9
|
+
? config.http_path
|
|
10
|
+
: config.http_path || `/instagram/${config.account_id}/events`,
|
|
11
|
+
api_version: config.api_version || "v25.0",
|
|
12
|
+
api_origin: config.api_origin || "https://graph.instagram.com",
|
|
13
|
+
subscribed_fields: config.subscribed_fields?.length
|
|
14
|
+
? [...config.subscribed_fields]
|
|
15
|
+
: ["messages", "messaging_postbacks", "messaging_seen", "message_reactions"],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function instagramUploadSource(params) {
|
|
19
|
+
const sources = [params.url, params.path, params.data].filter(value => value !== undefined);
|
|
20
|
+
if (sources.length !== 1) {
|
|
21
|
+
throw InstagramError.invalid("upload_file 必须且只能提供 url、path、data 之一");
|
|
22
|
+
}
|
|
23
|
+
if (params.path) {
|
|
24
|
+
throw new InstagramError("Instagram upload_file 不读取宿主本地路径", {
|
|
25
|
+
code: "INSTAGRAM_LOCAL_PATH_REJECTED",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (params.url) {
|
|
29
|
+
if (!URL.canParse(params.url)) {
|
|
30
|
+
throw InstagramError.invalid("upload_file.url 不是有效 URL");
|
|
31
|
+
}
|
|
32
|
+
const url = new URL(params.url);
|
|
33
|
+
if (url.protocol !== "https:" || url.username || url.password) {
|
|
34
|
+
throw InstagramError.invalid("upload_file.url 必须是无凭据 HTTPS URL");
|
|
35
|
+
}
|
|
36
|
+
return { url: url.toString() };
|
|
37
|
+
}
|
|
38
|
+
const raw = params.data || "";
|
|
39
|
+
const match = raw.match(/^data:([^;,]+);base64,(.*)$/su);
|
|
40
|
+
const encoded = match ? match[2] : raw;
|
|
41
|
+
if (!encoded || encoded.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(encoded)) {
|
|
42
|
+
throw InstagramError.invalid("upload_file.data 不是有效 base64");
|
|
43
|
+
}
|
|
44
|
+
const bytes = new Uint8Array(Buffer.from(encoded, "base64"));
|
|
45
|
+
if (!bytes.byteLength || bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
46
|
+
throw InstagramError.invalid("upload_file.data 必须介于 1 byte 与 25 MiB");
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
blob: new Blob([bytes], { type: match?.[1] || contentType(params.name) }),
|
|
50
|
+
filename: params.name,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function instagramAttachmentType(filename) {
|
|
54
|
+
const mime = contentType(filename);
|
|
55
|
+
if (mime.startsWith("image/"))
|
|
56
|
+
return "image";
|
|
57
|
+
if (mime.startsWith("video/"))
|
|
58
|
+
return "video";
|
|
59
|
+
if (mime.startsWith("audio/"))
|
|
60
|
+
return "audio";
|
|
61
|
+
throw InstagramError.invalid("Instagram Messaging 仅支持 image、video 与 audio 附件");
|
|
62
|
+
}
|
|
63
|
+
function contentType(filename) {
|
|
64
|
+
const extension = filename.toLowerCase().split(".").at(-1);
|
|
65
|
+
return ({
|
|
66
|
+
png: "image/png",
|
|
67
|
+
jpg: "image/jpeg",
|
|
68
|
+
jpeg: "image/jpeg",
|
|
69
|
+
gif: "image/gif",
|
|
70
|
+
webp: "image/webp",
|
|
71
|
+
mp4: "video/mp4",
|
|
72
|
+
mov: "video/quicktime",
|
|
73
|
+
webm: "video/webm",
|
|
74
|
+
mp3: "audio/mpeg",
|
|
75
|
+
ogg: "audio/ogg",
|
|
76
|
+
wav: "audio/wav",
|
|
77
|
+
}[extension || ""] || "application/octet-stream");
|
|
78
|
+
}
|
package/lib/adapter.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Account, Adapter, BaseApp, type AdapterCapabilityManifest } from "onebots";
|
|
2
|
+
import { InstagramClient } from "./client.js";
|
|
3
|
+
export declare class InstagramAdapter extends Adapter<InstagramClient, "instagram"> {
|
|
4
|
+
private readonly httpHost;
|
|
5
|
+
constructor(app: BaseApp);
|
|
6
|
+
describeCapabilities(uin?: string): AdapterCapabilityManifest;
|
|
7
|
+
sendMessage(uin: string, params: Adapter.SendMessageParams): Promise<Adapter.SendMessageResult>;
|
|
8
|
+
getMessage(uin: string, params: Adapter.GetMessageParams): Promise<Adapter.MessageInfo>;
|
|
9
|
+
getMessageHistory(uin: string, params: Adapter.GetMessageHistoryParams): Promise<Adapter.MessageInfo[]>;
|
|
10
|
+
getLoginInfo(uin: string): Promise<Adapter.UserInfo>;
|
|
11
|
+
getUserInfo(uin: string, params: Adapter.GetUserInfoParams): Promise<Adapter.UserInfo>;
|
|
12
|
+
uploadFile(uin: string, params: Adapter.UploadFileParams): Promise<Adapter.FileInfo>;
|
|
13
|
+
executePlatformAction(uin: string, action: string, params: Readonly<Record<string, unknown>>): Promise<unknown>;
|
|
14
|
+
isPlatformActionImplemented(action: string): boolean;
|
|
15
|
+
getVersion(): Promise<Adapter.VersionInfo>;
|
|
16
|
+
getStatus(uin: string): Promise<Adapter.StatusInfo>;
|
|
17
|
+
canSendImage(): Promise<boolean>;
|
|
18
|
+
canSendRecord(): Promise<boolean>;
|
|
19
|
+
createAccount(config: Account.Config<"instagram">): Account<"instagram", InstagramClient>;
|
|
20
|
+
private requireClient;
|
|
21
|
+
private assertDirect;
|
|
22
|
+
private userInfo;
|
|
23
|
+
private messageInfo;
|
|
24
|
+
}
|
package/lib/adapter.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { Account, AccountStatus, Adapter, readPackageVersion, } from "onebots";
|
|
2
|
+
import { instagramAttachmentType, instagramUploadSource, normalizeInstagramConfig, } from "./adapter-support.js";
|
|
3
|
+
import { describeInstagramCapabilities, instagramCapabilities } from "./capabilities.js";
|
|
4
|
+
import { InstagramClient } from "./client.js";
|
|
5
|
+
import { parseApiMessage, parseBusinessProfile } from "./entities.js";
|
|
6
|
+
import { InstagramError } from "./errors.js";
|
|
7
|
+
import { projectInstagramEvent } from "./events.js";
|
|
8
|
+
import { InstagramHttpHost } from "./http-host.js";
|
|
9
|
+
import { compileInstagramMessage, projectApiMessage } from "./messages.js";
|
|
10
|
+
import { executeInstagramPlatformAction, INSTAGRAM_PLATFORM_ACTIONS } from "./platform-actions.js";
|
|
11
|
+
import { assertMetaId } from "./validation.js";
|
|
12
|
+
export class InstagramAdapter extends Adapter {
|
|
13
|
+
httpHost;
|
|
14
|
+
constructor(app) {
|
|
15
|
+
super(app, "instagram", instagramCapabilities);
|
|
16
|
+
this.icon = "https://static.cdninstagram.com/rsrc.php/v4/yR/r/lam-fZmwmvn.png";
|
|
17
|
+
this.httpHost = new InstagramHttpHost(app, accountId => this.getAccount(accountId)?.client);
|
|
18
|
+
}
|
|
19
|
+
describeCapabilities(uin) {
|
|
20
|
+
const config = uin ? this.getAccount(uin)?.client.config : undefined;
|
|
21
|
+
return config ? describeInstagramCapabilities(config) : instagramCapabilities;
|
|
22
|
+
}
|
|
23
|
+
async sendMessage(uin, params) {
|
|
24
|
+
this.assertDirect(params.scene_type);
|
|
25
|
+
const client = this.requireClient(uin);
|
|
26
|
+
const message = await compileInstagramMessage(params.message, {
|
|
27
|
+
upload: (type, source, reusable) => client.uploadAttachment(type, source, reusable),
|
|
28
|
+
});
|
|
29
|
+
const result = await client.send(params.scene_id.string, message);
|
|
30
|
+
return { message_id: this.createId(result.message_id) };
|
|
31
|
+
}
|
|
32
|
+
async getMessage(uin, params) {
|
|
33
|
+
const message = parseApiMessage(await this.requireClient(uin).call("GET", `/${assertMetaId(params.message_id.string, "message_id")}`, { query: { fields: "id,created_time,from,to,message" } }));
|
|
34
|
+
return this.messageInfo(this.requireClient(uin), message);
|
|
35
|
+
}
|
|
36
|
+
async getMessageHistory(uin, params) {
|
|
37
|
+
this.assertDirect(params.scene_type);
|
|
38
|
+
if (params.offset !== undefined || params.start_message_id) {
|
|
39
|
+
throw new InstagramError("Instagram 使用不透明 cursor;canonical offset/start_message_id 无等价语义", { code: "INSTAGRAM_UNSUPPORTED_PAGINATION" });
|
|
40
|
+
}
|
|
41
|
+
const client = this.requireClient(uin);
|
|
42
|
+
const conversation = await client.findConversation(params.scene_id.string);
|
|
43
|
+
if (!conversation)
|
|
44
|
+
return [];
|
|
45
|
+
const full = await client.getConversation(conversation.id, params.limit || 20);
|
|
46
|
+
return (full.messages?.data || []).map(message => this.messageInfo(client, message));
|
|
47
|
+
}
|
|
48
|
+
async getLoginInfo(uin) {
|
|
49
|
+
const client = this.requireClient(uin);
|
|
50
|
+
const profile = client.businessProfile ||
|
|
51
|
+
parseBusinessProfile(await client.call("GET", `/${client.config.instagram_user_id}`, {
|
|
52
|
+
query: { fields: "id,username" },
|
|
53
|
+
}));
|
|
54
|
+
return {
|
|
55
|
+
user_id: this.createId(profile.id),
|
|
56
|
+
user_name: profile.username || profile.name || profile.id,
|
|
57
|
+
user_displayname: profile.name,
|
|
58
|
+
avatar: profile.profile_picture_url,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async getUserInfo(uin, params) {
|
|
62
|
+
return this.userInfo(await this.requireClient(uin).getUserProfile(params.user_id.string));
|
|
63
|
+
}
|
|
64
|
+
async uploadFile(uin, params) {
|
|
65
|
+
this.assertDirect(params.scene_type);
|
|
66
|
+
const attachmentId = await this.requireClient(uin).uploadAttachment(instagramAttachmentType(params.name), instagramUploadSource(params), true);
|
|
67
|
+
return {
|
|
68
|
+
file_id: this.createId(attachmentId),
|
|
69
|
+
file_name: params.name,
|
|
70
|
+
url: `instagram://attachment/${attachmentId}`,
|
|
71
|
+
expire_time: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
executePlatformAction(uin, action, params) {
|
|
75
|
+
return INSTAGRAM_PLATFORM_ACTIONS.has(action)
|
|
76
|
+
? executeInstagramPlatformAction(this.requireClient(uin), action, params)
|
|
77
|
+
: super.executePlatformAction(uin, action, params);
|
|
78
|
+
}
|
|
79
|
+
isPlatformActionImplemented(action) {
|
|
80
|
+
return INSTAGRAM_PLATFORM_ACTIONS.has(action);
|
|
81
|
+
}
|
|
82
|
+
async getVersion() {
|
|
83
|
+
return {
|
|
84
|
+
app_name: "onebots Instagram Adapter",
|
|
85
|
+
app_version: await readPackageVersion(import.meta.url),
|
|
86
|
+
impl: "Instagram API with Instagram Login",
|
|
87
|
+
version: "Graph v25.0",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async getStatus(uin) {
|
|
91
|
+
const account = this.getAccount(uin);
|
|
92
|
+
const online = account?.status === AccountStatus.Online;
|
|
93
|
+
return {
|
|
94
|
+
online,
|
|
95
|
+
good: online,
|
|
96
|
+
bots: account
|
|
97
|
+
? [{ self: this.createId(account.client.config.instagram_user_id), online }]
|
|
98
|
+
: [],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async canSendImage() {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
async canSendRecord() {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
createAccount(config) {
|
|
108
|
+
const client = new InstagramClient(normalizeInstagramConfig(config), {
|
|
109
|
+
reportError: error => this.logger.error("Instagram 接收管线异常", error),
|
|
110
|
+
});
|
|
111
|
+
const account = new Account(this, client, config);
|
|
112
|
+
client.on("event", (delivery) => account.dispatchManyAwaited(projectInstagramEvent(delivery, {
|
|
113
|
+
botId: this.createId(client.config.instagram_user_id),
|
|
114
|
+
createId: value => this.createId(value),
|
|
115
|
+
})));
|
|
116
|
+
this.httpHost.mount(account.account_id, client);
|
|
117
|
+
account.on("start", async () => {
|
|
118
|
+
try {
|
|
119
|
+
await client.start();
|
|
120
|
+
account.status = AccountStatus.Online;
|
|
121
|
+
account.nickname =
|
|
122
|
+
client.businessProfile?.username ||
|
|
123
|
+
client.businessProfile?.name ||
|
|
124
|
+
client.config.instagram_user_id;
|
|
125
|
+
this.logger.info(`Instagram ${account.account_id} 已就绪(${client.receiveMode})`);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
account.status = AccountStatus.OffLine;
|
|
129
|
+
this.logger.error(`启动 Instagram ${account.account_id} 失败`, error);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
account.on("stop", async () => {
|
|
134
|
+
try {
|
|
135
|
+
await client.stop();
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
account.status = AccountStatus.OffLine;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return account;
|
|
142
|
+
}
|
|
143
|
+
requireClient(uin) {
|
|
144
|
+
const client = this.getAccount(uin)?.client;
|
|
145
|
+
if (!client) {
|
|
146
|
+
throw new InstagramError(`Instagram 账号 ${uin} 不存在`, {
|
|
147
|
+
code: "ACCOUNT_NOT_FOUND",
|
|
148
|
+
status: 404,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return client;
|
|
152
|
+
}
|
|
153
|
+
assertDirect(scene) {
|
|
154
|
+
if (scene !== "direct") {
|
|
155
|
+
throw InstagramError.invalid("Instagram Messaging 只支持 Professional Account 与 IGSID 的一对一 direct 会话");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
userInfo(profile) {
|
|
159
|
+
return {
|
|
160
|
+
user_id: this.createId(profile.id),
|
|
161
|
+
user_name: profile.username || profile.name || profile.id,
|
|
162
|
+
user_displayname: profile.name,
|
|
163
|
+
avatar: profile.profile_pic,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
messageInfo(client, message) {
|
|
167
|
+
const from = message.from;
|
|
168
|
+
if (!from)
|
|
169
|
+
throw InstagramError.invalid("Instagram message response 缺少 from");
|
|
170
|
+
const selfId = client.config.instagram_user_id;
|
|
171
|
+
const peer = from.id === selfId ? message.to?.data.find(person => person.id !== selfId) : from;
|
|
172
|
+
if (!peer)
|
|
173
|
+
throw InstagramError.invalid("Instagram message response 缺少对端 IGSID");
|
|
174
|
+
const timestamp = Date.parse(message.created_time);
|
|
175
|
+
if (!Number.isFinite(timestamp)) {
|
|
176
|
+
throw InstagramError.invalid("Instagram message.created_time 无效");
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
message_id: this.createId(message.id),
|
|
180
|
+
time: Math.floor(timestamp / 1000),
|
|
181
|
+
sender: {
|
|
182
|
+
scene_type: "direct",
|
|
183
|
+
sender_id: this.createId(from.id),
|
|
184
|
+
scene_id: this.createId(peer.id),
|
|
185
|
+
sender_name: from.username || from.id,
|
|
186
|
+
scene_name: peer.username || peer.id,
|
|
187
|
+
},
|
|
188
|
+
message: projectApiMessage(message),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type AdapterCapabilityManifest } from "onebots";
|
|
2
|
+
import { INSTAGRAM_EVENT_TYPES, INSTAGRAM_WEBHOOK_FIELDS, type InstagramConfig } from "./types.js";
|
|
3
|
+
export { INSTAGRAM_EVENT_TYPES, INSTAGRAM_WEBHOOK_FIELDS };
|
|
4
|
+
/** Instagram Login 当前稳定接口的真实边界;Instagram Messaging 不支持群聊。 */
|
|
5
|
+
export declare const instagramCapabilities: AdapterCapabilityManifest;
|
|
6
|
+
export declare function describeInstagramCapabilities(config: Pick<InstagramConfig, "declared_permissions" | "event_types" | "receive_mode" | "subscribed_fields">): AdapterCapabilityManifest;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { defineAdapterCapabilities, definePlatformActionCapabilities, restrictAdapterEventCapabilities, } from "onebots";
|
|
2
|
+
import { INSTAGRAM_PLATFORM_ACTIONS } from "./platform-actions.js";
|
|
3
|
+
import { INSTAGRAM_EVENT_TYPES, INSTAGRAM_WEBHOOK_FIELDS, } from "./types.js";
|
|
4
|
+
export { INSTAGRAM_EVENT_TYPES, INSTAGRAM_WEBHOOK_FIELDS };
|
|
5
|
+
const permission = (permissions, note) => ({
|
|
6
|
+
support: "native",
|
|
7
|
+
availability: "permission",
|
|
8
|
+
permissions,
|
|
9
|
+
note,
|
|
10
|
+
});
|
|
11
|
+
const basic = permission(["instagram_business_basic"]);
|
|
12
|
+
const messaging = permission(["instagram_business_manage_messages"], "仅可联系已主动开启会话的用户,并受标准消息窗口约束");
|
|
13
|
+
const conversations = permission(["instagram_business_manage_messages"], "Requests 文件夹中 30 天未活跃会话不会返回;单次最多读取最近 20 条消息详情");
|
|
14
|
+
const platformActions = definePlatformActionCapabilities(INSTAGRAM_PLATFORM_ACTIONS, action => {
|
|
15
|
+
if (action === "call_instagram_api")
|
|
16
|
+
return permission(["目标 Graph edge 要求的 permission"]);
|
|
17
|
+
if (action === "send_instagram_human_agent") {
|
|
18
|
+
return permission(["instagram_business_manage_messages", "Human Agent"], "仅限 7 天内由真实人工客服发送,不得用于自动化或无关内容");
|
|
19
|
+
}
|
|
20
|
+
if (action === "send_instagram_private_reply") {
|
|
21
|
+
return permission(["instagram_business_manage_messages", "instagram_business_manage_comments"], "每条评论只能私信回复一次,须在评论后 7 天内发送");
|
|
22
|
+
}
|
|
23
|
+
if (action.includes("welcome_message_flow"))
|
|
24
|
+
return messaging;
|
|
25
|
+
if (action.includes("profile"))
|
|
26
|
+
return messaging;
|
|
27
|
+
if (action.includes("conversation"))
|
|
28
|
+
return conversations;
|
|
29
|
+
if (action.includes("subscribed") || action.startsWith("subscribe_"))
|
|
30
|
+
return messaging;
|
|
31
|
+
return messaging;
|
|
32
|
+
});
|
|
33
|
+
/** Instagram Login 当前稳定接口的真实边界;Instagram Messaging 不支持群聊。 */
|
|
34
|
+
export const instagramCapabilities = defineAdapterCapabilities({
|
|
35
|
+
actions: {
|
|
36
|
+
send_message: { ...messaging, scenes: ["direct"] },
|
|
37
|
+
get_message: { ...conversations, scenes: ["direct"] },
|
|
38
|
+
get_message_history: { ...conversations, scenes: ["direct"] },
|
|
39
|
+
get_login_info: basic,
|
|
40
|
+
get_user_info: permission([
|
|
41
|
+
"instagram_business_basic",
|
|
42
|
+
"instagram_business_manage_messages",
|
|
43
|
+
]),
|
|
44
|
+
upload_file: { ...messaging, scenes: ["direct"] },
|
|
45
|
+
can_send_image: { support: "native" },
|
|
46
|
+
can_send_record: { support: "native" },
|
|
47
|
+
get_version: { support: "native" },
|
|
48
|
+
get_status: { support: "native" },
|
|
49
|
+
get_supported_actions: { support: "native" },
|
|
50
|
+
...platformActions,
|
|
51
|
+
},
|
|
52
|
+
events: {
|
|
53
|
+
message: { support: "native", scenes: ["direct"] },
|
|
54
|
+
message_deleted: { support: "native" },
|
|
55
|
+
message_updated: { support: "native" },
|
|
56
|
+
message_status: { support: "native" },
|
|
57
|
+
reaction_added: { support: "native" },
|
|
58
|
+
reaction_removed: { support: "native" },
|
|
59
|
+
interaction: { support: "native" },
|
|
60
|
+
custom: {
|
|
61
|
+
support: "native",
|
|
62
|
+
note: "unsupported message、referral、opt-in、handover、standby、comment 与其他 field/value 原样保留",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
segments: {
|
|
66
|
+
text: { support: "native", direction: "both" },
|
|
67
|
+
reply: { support: "native", direction: "both" },
|
|
68
|
+
image: { support: "native", direction: "both" },
|
|
69
|
+
video: { support: "native", direction: "both" },
|
|
70
|
+
audio: { support: "native", direction: "both" },
|
|
71
|
+
instagram_quick_replies: { support: "native", direction: "send" },
|
|
72
|
+
instagram_quick_reply: { support: "native", direction: "receive" },
|
|
73
|
+
instagram_referral: { support: "native", direction: "receive" },
|
|
74
|
+
instagram_reply_context: { support: "native", direction: "receive" },
|
|
75
|
+
instagram: { support: "native", direction: "send" },
|
|
76
|
+
instagram_attachment: { support: "native", direction: "receive" },
|
|
77
|
+
},
|
|
78
|
+
transports: {
|
|
79
|
+
webhook: {
|
|
80
|
+
support: "native",
|
|
81
|
+
mode: "webhook",
|
|
82
|
+
note: "GET challenge、精确 raw-body SHA256、batch 展开与可靠去重",
|
|
83
|
+
},
|
|
84
|
+
manual: {
|
|
85
|
+
support: "native",
|
|
86
|
+
mode: "native",
|
|
87
|
+
note: "ingest(rawEvent) 与已有 Host 共用 Client 和事件投影",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const eventProjection = {
|
|
92
|
+
message: ["message"],
|
|
93
|
+
message_deleted: ["message_deleted"],
|
|
94
|
+
message_updated: ["message_edit"],
|
|
95
|
+
message_status: ["message_echo", "read"],
|
|
96
|
+
reaction_added: ["reaction"],
|
|
97
|
+
reaction_removed: ["reaction"],
|
|
98
|
+
interaction: ["postback"],
|
|
99
|
+
custom: ["message_unsupported", "referral", "optin", "handover", "change", "unknown"],
|
|
100
|
+
};
|
|
101
|
+
const fieldEvents = {
|
|
102
|
+
messages: ["message", "message_echo", "message_deleted", "message_unsupported", "message_edit"],
|
|
103
|
+
messaging_postbacks: ["postback"],
|
|
104
|
+
messaging_seen: ["read"],
|
|
105
|
+
messaging_handover: ["handover"],
|
|
106
|
+
messaging_referral: ["referral"],
|
|
107
|
+
messaging_optins: ["optin"],
|
|
108
|
+
message_reactions: ["reaction"],
|
|
109
|
+
standby: ["unknown"],
|
|
110
|
+
comments: ["change"],
|
|
111
|
+
live_comments: ["change"],
|
|
112
|
+
mentions: ["change"],
|
|
113
|
+
story_insights: ["change"],
|
|
114
|
+
};
|
|
115
|
+
export function describeInstagramCapabilities(config) {
|
|
116
|
+
const configuredEvents = new Set(config.event_types?.length ? config.event_types : INSTAGRAM_EVENT_TYPES);
|
|
117
|
+
if (config.subscribed_fields?.length) {
|
|
118
|
+
const fromFields = new Set(config.subscribed_fields.flatMap(field => fieldEvents[field] || ["unknown"]));
|
|
119
|
+
for (const event of configuredEvents) {
|
|
120
|
+
if (!fromFields.has(event))
|
|
121
|
+
configuredEvents.delete(event);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const enabledCanonical = new Set();
|
|
125
|
+
for (const [canonical, sources] of Object.entries(eventProjection)) {
|
|
126
|
+
if (sources.some(source => configuredEvents.has(source)))
|
|
127
|
+
enabledCanonical.add(canonical);
|
|
128
|
+
}
|
|
129
|
+
let manifest = restrictAdapterEventCapabilities(instagramCapabilities, enabledCanonical, event => `当前 event_types/subscribed_fields 不会生成 ${event}`);
|
|
130
|
+
if (config.receive_mode === "manual") {
|
|
131
|
+
manifest = {
|
|
132
|
+
...manifest,
|
|
133
|
+
transports: {
|
|
134
|
+
...manifest.transports,
|
|
135
|
+
webhook: {
|
|
136
|
+
support: "unsupported",
|
|
137
|
+
availability: "context",
|
|
138
|
+
mode: "webhook",
|
|
139
|
+
note: "当前 receive_mode 为 manual",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return restrictDeclaredPermissions(manifest, config.declared_permissions);
|
|
145
|
+
}
|
|
146
|
+
function restrictDeclaredPermissions(manifest, permissions) {
|
|
147
|
+
if (!permissions?.length)
|
|
148
|
+
return manifest;
|
|
149
|
+
const declared = new Set(permissions);
|
|
150
|
+
const actions = { ...manifest.actions };
|
|
151
|
+
for (const [action, descriptor] of Object.entries(actions)) {
|
|
152
|
+
if (action === "call_instagram_api")
|
|
153
|
+
continue;
|
|
154
|
+
const required = descriptor.permissions || [];
|
|
155
|
+
if (!required.length)
|
|
156
|
+
continue;
|
|
157
|
+
if (required.every(permissionName => declared.has(permissionName)))
|
|
158
|
+
continue;
|
|
159
|
+
actions[action] = {
|
|
160
|
+
support: "unsupported",
|
|
161
|
+
availability: "permission",
|
|
162
|
+
permissions: required,
|
|
163
|
+
note: "declared_permissions 未包含该动作要求的全部权限",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return { ...manifest, actions };
|
|
167
|
+
}
|
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { InstagramBusinessProfile, InstagramCallOptions, InstagramClientEvents, InstagramConfig, InstagramConversation, InstagramGraphMethod, InstagramHttpRequest, InstagramHttpResponse, InstagramIngestResult, InstagramList, InstagramOutgoingMessage, InstagramSendResponse, InstagramUserProfile } from "./types.js";
|
|
3
|
+
export interface InstagramClientDependencies {
|
|
4
|
+
fetcher?: typeof fetch;
|
|
5
|
+
reportError?(error: Error): void;
|
|
6
|
+
}
|
|
7
|
+
/** Graph API 与 Webhook/manual ingress 共用的可嵌入 Instagram Client。 */
|
|
8
|
+
export declare class InstagramClient extends EventEmitter<InstagramClientEvents> {
|
|
9
|
+
readonly config: InstagramConfig;
|
|
10
|
+
private readonly dependencies;
|
|
11
|
+
private readonly transport;
|
|
12
|
+
private readonly webhook;
|
|
13
|
+
private profile?;
|
|
14
|
+
private startTask?;
|
|
15
|
+
private startAbort?;
|
|
16
|
+
private generation;
|
|
17
|
+
private started;
|
|
18
|
+
constructor(config: InstagramConfig, dependencies?: InstagramClientDependencies);
|
|
19
|
+
get receiveMode(): InstagramReceiveMode;
|
|
20
|
+
get isStarted(): boolean;
|
|
21
|
+
get businessProfile(): InstagramBusinessProfile | undefined;
|
|
22
|
+
start(): Promise<void>;
|
|
23
|
+
stop(): Promise<void>;
|
|
24
|
+
call<T = unknown>(method: InstagramGraphMethod, path: string, options?: InstagramCallOptions): Promise<T>;
|
|
25
|
+
ingest(rawEvent: unknown): Promise<InstagramIngestResult[]>;
|
|
26
|
+
ingestHttp(request: InstagramHttpRequest): Promise<InstagramHttpResponse>;
|
|
27
|
+
acceptHttp(request: Request): Promise<Response>;
|
|
28
|
+
send(recipientId: string, message: InstagramOutgoingMessage, options?: {
|
|
29
|
+
humanAgent?: boolean;
|
|
30
|
+
}): Promise<InstagramSendResponse>;
|
|
31
|
+
sendPrivateReply(commentId: string, text: string): Promise<InstagramSendResponse>;
|
|
32
|
+
react(recipientId: string, messageId: string, action: "react" | "unreact"): Promise<unknown>;
|
|
33
|
+
uploadAttachment(type: "image" | "video" | "audio" | "file", source: {
|
|
34
|
+
url: string;
|
|
35
|
+
} | {
|
|
36
|
+
blob: Blob;
|
|
37
|
+
filename: string;
|
|
38
|
+
}, reusable?: boolean): Promise<string>;
|
|
39
|
+
getUserProfile(userId: string): Promise<InstagramUserProfile>;
|
|
40
|
+
listConversations(after?: string, limit?: number): Promise<InstagramList<InstagramConversation>>;
|
|
41
|
+
findConversation(userId: string): Promise<InstagramConversation | undefined>;
|
|
42
|
+
getConversation(conversationId: string, limit?: number): Promise<InstagramConversation>;
|
|
43
|
+
private startInternal;
|
|
44
|
+
private subscribe;
|
|
45
|
+
private forward;
|
|
46
|
+
private reportError;
|
|
47
|
+
}
|
|
48
|
+
type InstagramReceiveMode = "webhook" | "manual";
|
|
49
|
+
export {};
|