@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/lib/client.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { MetaGraphTransport, MetaWebhookClient } from "@onebots/meta";
|
|
3
|
+
import { emitAllAwaited } from "onebots";
|
|
4
|
+
import { parseAttachmentId, parseBusinessProfile, parseConversation, parseConversationList, parseSendResponse, parseUserProfile, } from "./entities.js";
|
|
5
|
+
import { InstagramError } from "./errors.js";
|
|
6
|
+
import { assertHttpsUrl, assertInstagramConfig, assertMetaId, assertNumericMetaId, } from "./validation.js";
|
|
7
|
+
import { InstagramWebhookCodec } from "./webhook-codec.js";
|
|
8
|
+
/** Graph API 与 Webhook/manual ingress 共用的可嵌入 Instagram Client。 */
|
|
9
|
+
export class InstagramClient extends EventEmitter {
|
|
10
|
+
config;
|
|
11
|
+
dependencies;
|
|
12
|
+
transport;
|
|
13
|
+
webhook;
|
|
14
|
+
profile;
|
|
15
|
+
startTask;
|
|
16
|
+
startAbort;
|
|
17
|
+
generation = 0;
|
|
18
|
+
started = false;
|
|
19
|
+
constructor(config, dependencies = {}) {
|
|
20
|
+
super();
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.dependencies = dependencies;
|
|
23
|
+
assertInstagramConfig(config);
|
|
24
|
+
this.transport = new MetaGraphTransport({
|
|
25
|
+
accessToken: config.access_token,
|
|
26
|
+
appSecret: config.app_secret,
|
|
27
|
+
apiOrigin: config.api_origin || "https://graph.instagram.com",
|
|
28
|
+
apiVersion: config.api_version,
|
|
29
|
+
}, dependencies.fetcher);
|
|
30
|
+
this.webhook = new MetaWebhookClient({
|
|
31
|
+
receiveMode: config.receive_mode || "webhook",
|
|
32
|
+
verifyToken: config.verify_token,
|
|
33
|
+
appSecret: config.app_secret,
|
|
34
|
+
httpPath: config.http_path,
|
|
35
|
+
maxBodyBytes: config.max_body_bytes,
|
|
36
|
+
}, new InstagramWebhookCodec(config.instagram_user_id, config.event_types), { reportError: error => this.reportError(error) });
|
|
37
|
+
this.webhook.on("event", delivery => this.forward(delivery));
|
|
38
|
+
this.webhook.on("ready", () => emitAllAwaited(this, "ready"));
|
|
39
|
+
this.webhook.on("stop", () => emitAllAwaited(this, "stop"));
|
|
40
|
+
}
|
|
41
|
+
get receiveMode() {
|
|
42
|
+
return this.config.receive_mode || "webhook";
|
|
43
|
+
}
|
|
44
|
+
get isStarted() {
|
|
45
|
+
return this.started;
|
|
46
|
+
}
|
|
47
|
+
get businessProfile() {
|
|
48
|
+
return this.profile ? structuredClone(this.profile) : undefined;
|
|
49
|
+
}
|
|
50
|
+
async start() {
|
|
51
|
+
if (this.started)
|
|
52
|
+
return;
|
|
53
|
+
if (this.startTask)
|
|
54
|
+
return this.startTask;
|
|
55
|
+
const generation = ++this.generation;
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
this.startAbort = controller;
|
|
58
|
+
const task = this.startInternal(generation, controller.signal);
|
|
59
|
+
this.startTask = task;
|
|
60
|
+
try {
|
|
61
|
+
await task;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
if (this.startTask === task)
|
|
65
|
+
this.startTask = undefined;
|
|
66
|
+
if (this.startAbort === controller)
|
|
67
|
+
this.startAbort = undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async stop() {
|
|
71
|
+
++this.generation;
|
|
72
|
+
this.startAbort?.abort();
|
|
73
|
+
await this.startTask?.catch(() => undefined);
|
|
74
|
+
this.started = false;
|
|
75
|
+
await this.webhook.stop();
|
|
76
|
+
}
|
|
77
|
+
call(method, path, options) {
|
|
78
|
+
return this.transport.call(method, path, options);
|
|
79
|
+
}
|
|
80
|
+
ingest(rawEvent) {
|
|
81
|
+
return this.webhook.ingest(rawEvent);
|
|
82
|
+
}
|
|
83
|
+
ingestHttp(request) {
|
|
84
|
+
return this.webhook.ingestHttp(request);
|
|
85
|
+
}
|
|
86
|
+
acceptHttp(request) {
|
|
87
|
+
return this.webhook.acceptHttp(request);
|
|
88
|
+
}
|
|
89
|
+
async send(recipientId, message, options = {}) {
|
|
90
|
+
assertNumericMetaId(recipientId, "recipient_id");
|
|
91
|
+
return parseSendResponse(await this.call("POST", `/${this.config.instagram_user_id}/messages`, {
|
|
92
|
+
body: {
|
|
93
|
+
recipient: { id: recipientId },
|
|
94
|
+
message,
|
|
95
|
+
...(options.humanAgent ? { tag: "HUMAN_AGENT" } : {}),
|
|
96
|
+
},
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
async sendPrivateReply(commentId, text) {
|
|
100
|
+
assertNumericMetaId(commentId, "comment_id");
|
|
101
|
+
if (!text)
|
|
102
|
+
throw InstagramError.invalid("private reply text 不能为空");
|
|
103
|
+
return parseSendResponse(await this.call("POST", `/${this.config.instagram_user_id}/messages`, {
|
|
104
|
+
body: { recipient: { comment_id: commentId }, message: { text } },
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
react(recipientId, messageId, action) {
|
|
108
|
+
assertNumericMetaId(recipientId, "recipient_id");
|
|
109
|
+
assertMetaId(messageId, "message_id");
|
|
110
|
+
return this.call("POST", `/${this.config.instagram_user_id}/messages`, {
|
|
111
|
+
body: {
|
|
112
|
+
recipient: { id: recipientId },
|
|
113
|
+
sender_action: action,
|
|
114
|
+
payload: { message_id: messageId, reaction: "love" },
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async uploadAttachment(type, source, reusable = true) {
|
|
119
|
+
let response;
|
|
120
|
+
if ("url" in source) {
|
|
121
|
+
response = await this.call("POST", `/${this.config.instagram_user_id}/message_attachments`, {
|
|
122
|
+
body: {
|
|
123
|
+
message: {
|
|
124
|
+
attachment: {
|
|
125
|
+
type,
|
|
126
|
+
payload: {
|
|
127
|
+
url: assertHttpsUrl(source.url, "attachment.url"),
|
|
128
|
+
is_reusable: reusable,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const form = new FormData();
|
|
137
|
+
form.set("message", JSON.stringify({ attachment: { type, payload: { is_reusable: reusable } } }));
|
|
138
|
+
form.set("filedata", source.blob, source.filename);
|
|
139
|
+
response = await this.call("POST", `/${this.config.instagram_user_id}/message_attachments`, {
|
|
140
|
+
form,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return parseAttachmentId(response);
|
|
144
|
+
}
|
|
145
|
+
async getUserProfile(userId) {
|
|
146
|
+
assertNumericMetaId(userId, "user_id");
|
|
147
|
+
return parseUserProfile(await this.call("GET", `/${userId}`, {
|
|
148
|
+
query: {
|
|
149
|
+
fields: "id,name,username,profile_pic,follower_count,is_user_follow_business,is_business_follow_user,is_verified_user",
|
|
150
|
+
},
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
async listConversations(after, limit = 25) {
|
|
154
|
+
return parseConversationList(await this.call("GET", `/${this.config.instagram_user_id}/conversations`, {
|
|
155
|
+
query: {
|
|
156
|
+
platform: "instagram",
|
|
157
|
+
fields: "id,updated_time,participants",
|
|
158
|
+
limit: boundedLimit(limit),
|
|
159
|
+
after,
|
|
160
|
+
},
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
async findConversation(userId) {
|
|
164
|
+
assertNumericMetaId(userId, "user_id");
|
|
165
|
+
const result = parseConversationList(await this.call("GET", `/${this.config.instagram_user_id}/conversations`, {
|
|
166
|
+
query: {
|
|
167
|
+
platform: "instagram",
|
|
168
|
+
user_id: userId,
|
|
169
|
+
fields: "id,updated_time,participants",
|
|
170
|
+
},
|
|
171
|
+
}));
|
|
172
|
+
if (result.data.length > 1) {
|
|
173
|
+
throw new InstagramError("Conversations API 对单个 IGSID 返回了多个会话", {
|
|
174
|
+
code: "INSTAGRAM_AMBIGUOUS_CONVERSATION",
|
|
175
|
+
details: { user_id: userId, count: result.data.length },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return result.data[0];
|
|
179
|
+
}
|
|
180
|
+
async getConversation(conversationId, limit = 20) {
|
|
181
|
+
assertMetaId(conversationId, "conversation_id");
|
|
182
|
+
return parseConversation(await this.call("GET", `/${conversationId}`, {
|
|
183
|
+
query: {
|
|
184
|
+
fields: `id,updated_time,participants,messages.limit(${boundedMessageLimit(limit)}){id,created_time,from,to,message}`,
|
|
185
|
+
},
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
async startInternal(generation, signal) {
|
|
189
|
+
const profile = parseBusinessProfile(await this.call("GET", `/${this.config.instagram_user_id}`, {
|
|
190
|
+
query: { fields: "id,username" },
|
|
191
|
+
signal,
|
|
192
|
+
}));
|
|
193
|
+
if (profile.id !== this.config.instagram_user_id) {
|
|
194
|
+
throw new InstagramError("access token 返回了不同的 Instagram User ID", {
|
|
195
|
+
code: "INSTAGRAM_USER_ID_MISMATCH",
|
|
196
|
+
details: { expected: this.config.instagram_user_id, actual: profile.id },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (this.config.auto_subscribe)
|
|
200
|
+
await this.subscribe(signal);
|
|
201
|
+
if (generation !== this.generation)
|
|
202
|
+
return;
|
|
203
|
+
this.profile = profile;
|
|
204
|
+
await this.webhook.start();
|
|
205
|
+
if (generation !== this.generation) {
|
|
206
|
+
await this.webhook.stop();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.started = true;
|
|
210
|
+
}
|
|
211
|
+
async subscribe(signal) {
|
|
212
|
+
const fields = this.config.subscribed_fields?.length
|
|
213
|
+
? this.config.subscribed_fields
|
|
214
|
+
: ["messages", "messaging_postbacks", "messaging_seen", "message_reactions"];
|
|
215
|
+
await this.call("POST", `/${this.config.instagram_user_id}/subscribed_apps`, {
|
|
216
|
+
query: { subscribed_fields: fields.join(",") },
|
|
217
|
+
signal,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
forward(delivery) {
|
|
221
|
+
return emitAllAwaited(this, "event", delivery);
|
|
222
|
+
}
|
|
223
|
+
reportError(error) {
|
|
224
|
+
this.dependencies.reportError?.(error);
|
|
225
|
+
if (this.listenerCount("error"))
|
|
226
|
+
this.emit("error", error);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function boundedLimit(value) {
|
|
230
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
231
|
+
throw InstagramError.invalid("limit 必须是正安全整数");
|
|
232
|
+
}
|
|
233
|
+
return Math.min(value, 100);
|
|
234
|
+
}
|
|
235
|
+
function boundedMessageLimit(value) {
|
|
236
|
+
return Math.min(boundedLimit(value), 20);
|
|
237
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { InstagramApiMessage, InstagramBusinessProfile, InstagramConversation, InstagramList, InstagramSendResponse, InstagramUserProfile } from "./types.js";
|
|
2
|
+
export declare function parseBusinessProfile(value: unknown): InstagramBusinessProfile;
|
|
3
|
+
export declare function parseUserProfile(value: unknown): InstagramUserProfile;
|
|
4
|
+
export declare function parseSendResponse(value: unknown): InstagramSendResponse;
|
|
5
|
+
export declare function parseConversationList(value: unknown): InstagramList<InstagramConversation>;
|
|
6
|
+
export declare function parseConversation(value: unknown, field?: string): InstagramConversation;
|
|
7
|
+
export declare function parseApiMessage(value: unknown, field?: string): InstagramApiMessage;
|
|
8
|
+
export declare function parseAttachmentId(value: unknown): string;
|
|
9
|
+
export declare function parseSuccess(value: unknown, field: string): true;
|
package/lib/entities.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { InstagramError } from "./errors.js";
|
|
2
|
+
import { assertNumericMetaId, optionalString, requireArray, requireNumber, requireRecord, requireString, } from "./validation.js";
|
|
3
|
+
export function parseBusinessProfile(value) {
|
|
4
|
+
const root = requireRecord(value, "Instagram business profile");
|
|
5
|
+
return {
|
|
6
|
+
id: assertNumericMetaId(root.id, "profile.id"),
|
|
7
|
+
user_id: optionalNumericId(root.user_id, "profile.user_id"),
|
|
8
|
+
username: optionalString(root.username, "profile.username"),
|
|
9
|
+
name: optionalString(root.name, "profile.name"),
|
|
10
|
+
profile_picture_url: optionalString(root.profile_picture_url, "profile.profile_picture_url"),
|
|
11
|
+
account_type: optionalString(root.account_type, "profile.account_type"),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function parseUserProfile(value) {
|
|
15
|
+
const root = requireRecord(value, "Instagram user profile");
|
|
16
|
+
return {
|
|
17
|
+
id: assertNumericMetaId(root.id, "user.id"),
|
|
18
|
+
name: optionalString(root.name, "user.name"),
|
|
19
|
+
username: optionalString(root.username, "user.username"),
|
|
20
|
+
profile_pic: optionalString(root.profile_pic, "user.profile_pic"),
|
|
21
|
+
follower_count: optionalNonNegativeInteger(root.follower_count, "user.follower_count"),
|
|
22
|
+
is_user_follow_business: optionalBoolean(root.is_user_follow_business, "user.is_user_follow_business"),
|
|
23
|
+
is_business_follow_user: optionalBoolean(root.is_business_follow_user, "user.is_business_follow_user"),
|
|
24
|
+
is_verified_user: optionalBoolean(root.is_verified_user, "user.is_verified_user"),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function parseSendResponse(value) {
|
|
28
|
+
const root = requireRecord(value, "Instagram send response");
|
|
29
|
+
return {
|
|
30
|
+
recipient_id: assertNumericMetaId(root.recipient_id, "send.recipient_id"),
|
|
31
|
+
message_id: requireString(root.message_id, "send.message_id"),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function parseConversationList(value) {
|
|
35
|
+
const root = requireRecord(value, "Instagram conversations response");
|
|
36
|
+
return {
|
|
37
|
+
data: requireArray(root.data, "conversations.data").map((item, index) => parseConversation(item, `conversations.data[${index}]`)),
|
|
38
|
+
paging: parsePaging(root.paging, "conversations.paging"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function parseConversation(value, field = "conversation") {
|
|
42
|
+
const root = requireRecord(value, field);
|
|
43
|
+
const participants = root.participants;
|
|
44
|
+
const messages = root.messages;
|
|
45
|
+
return {
|
|
46
|
+
id: requireString(root.id, `${field}.id`),
|
|
47
|
+
updated_time: optionalString(root.updated_time, `${field}.updated_time`),
|
|
48
|
+
participants: participants === undefined
|
|
49
|
+
? undefined
|
|
50
|
+
: {
|
|
51
|
+
data: requireArray(requireRecord(participants, `${field}.participants`).data, `${field}.participants.data`).map((item, index) => parsePerson(item, `${field}.participants.data[${index}]`)),
|
|
52
|
+
},
|
|
53
|
+
messages: messages === undefined
|
|
54
|
+
? undefined
|
|
55
|
+
: parseMessageConnection(messages, `${field}.messages`),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function parseApiMessage(value, field = "message") {
|
|
59
|
+
const root = requireRecord(value, field);
|
|
60
|
+
const from = root.from;
|
|
61
|
+
const to = root.to;
|
|
62
|
+
return {
|
|
63
|
+
id: requireString(root.id, `${field}.id`),
|
|
64
|
+
created_time: requireString(root.created_time, `${field}.created_time`),
|
|
65
|
+
from: from === undefined ? undefined : parsePerson(from, `${field}.from`),
|
|
66
|
+
to: to === undefined
|
|
67
|
+
? undefined
|
|
68
|
+
: {
|
|
69
|
+
data: requireArray(requireRecord(to, `${field}.to`).data, `${field}.to.data`).map((item, index) => parsePerson(item, `${field}.to.data[${index}]`)),
|
|
70
|
+
},
|
|
71
|
+
message: optionalString(root.message, `${field}.message`),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function parseAttachmentId(value) {
|
|
75
|
+
return requireString(requireRecord(value, "Instagram attachment response").attachment_id, "attachment_id");
|
|
76
|
+
}
|
|
77
|
+
export function parseSuccess(value, field) {
|
|
78
|
+
const root = requireRecord(value, field);
|
|
79
|
+
if (root.success !== true) {
|
|
80
|
+
throw new InstagramError(`${field} 未返回 success`, {
|
|
81
|
+
code: "INSTAGRAM_INVALID_RESPONSE",
|
|
82
|
+
details: { response: root },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
function parseMessageConnection(value, field) {
|
|
88
|
+
const root = requireRecord(value, field);
|
|
89
|
+
return {
|
|
90
|
+
data: requireArray(root.data, `${field}.data`).map((item, index) => parseApiMessage(item, `${field}.data[${index}]`)),
|
|
91
|
+
paging: parsePaging(root.paging, `${field}.paging`),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function parsePaging(value, field) {
|
|
95
|
+
if (value === undefined)
|
|
96
|
+
return undefined;
|
|
97
|
+
const root = requireRecord(value, field);
|
|
98
|
+
if (root.cursors === undefined)
|
|
99
|
+
return {};
|
|
100
|
+
const cursors = requireRecord(root.cursors, `${field}.cursors`);
|
|
101
|
+
return {
|
|
102
|
+
cursors: {
|
|
103
|
+
before: optionalString(cursors.before, `${field}.cursors.before`),
|
|
104
|
+
after: optionalString(cursors.after, `${field}.cursors.after`),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function parsePerson(value, field) {
|
|
109
|
+
const root = requireRecord(value, field);
|
|
110
|
+
return {
|
|
111
|
+
id: assertNumericMetaId(root.id, `${field}.id`),
|
|
112
|
+
username: optionalString(root.username ?? root.name, `${field}.username`),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function optionalNumericId(value, field) {
|
|
116
|
+
return value === undefined ? undefined : assertNumericMetaId(value, field);
|
|
117
|
+
}
|
|
118
|
+
function optionalBoolean(value, field) {
|
|
119
|
+
if (value === undefined)
|
|
120
|
+
return undefined;
|
|
121
|
+
if (typeof value !== "boolean")
|
|
122
|
+
throw InstagramError.invalid(`${field} 必须是 boolean`);
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
function optionalNonNegativeInteger(value, field) {
|
|
126
|
+
if (value === undefined)
|
|
127
|
+
return undefined;
|
|
128
|
+
const number = requireNumber(value, field);
|
|
129
|
+
if (!Number.isSafeInteger(number) || number < 0) {
|
|
130
|
+
throw InstagramError.invalid(`${field} 必须是非负安全整数`);
|
|
131
|
+
}
|
|
132
|
+
return number;
|
|
133
|
+
}
|
package/lib/errors.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { MetaError, type MetaErrorOptions } from "@onebots/meta";
|
|
2
|
+
import { ValidationError } from "onebots";
|
|
3
|
+
export declare class InstagramError extends MetaError {
|
|
4
|
+
constructor(message: string, options: MetaErrorOptions);
|
|
5
|
+
static invalid(message: string, details?: Record<string, unknown>): ValidationError;
|
|
6
|
+
static wrap(error: unknown, code?: string): InstagramError;
|
|
7
|
+
}
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MetaError } from "@onebots/meta";
|
|
2
|
+
import { ValidationError } from "onebots";
|
|
3
|
+
export class InstagramError extends MetaError {
|
|
4
|
+
constructor(message, options) {
|
|
5
|
+
super(message, options);
|
|
6
|
+
this.name = "InstagramError";
|
|
7
|
+
}
|
|
8
|
+
static invalid(message, details) {
|
|
9
|
+
return new ValidationError(message, { context: { platform: "instagram", ...details } });
|
|
10
|
+
}
|
|
11
|
+
static wrap(error, code = "INSTAGRAM_ERROR") {
|
|
12
|
+
if (error instanceof InstagramError)
|
|
13
|
+
return error;
|
|
14
|
+
if (error instanceof MetaError) {
|
|
15
|
+
return new InstagramError(error.message, {
|
|
16
|
+
code: error.code,
|
|
17
|
+
status: error.status,
|
|
18
|
+
details: error.details,
|
|
19
|
+
cause: error,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return new InstagramError(error instanceof Error ? error.message : String(error), {
|
|
23
|
+
code,
|
|
24
|
+
cause: error,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
package/lib/events.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type CommonEvent, type CommonTypes } from "onebots";
|
|
2
|
+
import type { InstagramDelivery } from "./types.js";
|
|
3
|
+
export interface InstagramProjectionContext {
|
|
4
|
+
botId: CommonTypes.Id;
|
|
5
|
+
createId(value: string | number): CommonTypes.Id;
|
|
6
|
+
}
|
|
7
|
+
export declare function projectInstagramEvent(delivery: InstagramDelivery, context: InstagramProjectionContext): CommonEvent.Event<unknown>[];
|
package/lib/events.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { projectWebhookMessage } from "./messages.js";
|
|
2
|
+
export function projectInstagramEvent(delivery, context) {
|
|
3
|
+
const event = delivery.event;
|
|
4
|
+
const item = event.messaging;
|
|
5
|
+
if (event.event_type === "message" && item?.message) {
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
...base(delivery, context),
|
|
9
|
+
type: "message",
|
|
10
|
+
message_type: "direct",
|
|
11
|
+
message_id: context.createId(item.message.mid),
|
|
12
|
+
sender: { id: context.createId(item.sender.id) },
|
|
13
|
+
message: projectWebhookMessage(item.message),
|
|
14
|
+
raw_message: item.message.text,
|
|
15
|
+
extensions: messagingExtensions(event.source, item),
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
if (event.event_type === "message_echo" && item?.message) {
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
...base(delivery, context),
|
|
23
|
+
type: "notice",
|
|
24
|
+
notice_type: "message_status",
|
|
25
|
+
sub_type: "echo",
|
|
26
|
+
message_id: context.createId(item.message.mid),
|
|
27
|
+
user: { id: context.createId(item.recipient.id) },
|
|
28
|
+
message: projectWebhookMessage(item.message),
|
|
29
|
+
extensions: messagingExtensions(event.source, item),
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
if (event.event_type === "message_deleted" && item?.message) {
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
...base(delivery, context),
|
|
37
|
+
type: "notice",
|
|
38
|
+
notice_type: "message_deleted",
|
|
39
|
+
message_id: context.createId(item.message.mid),
|
|
40
|
+
user: { id: context.createId(item.sender.id) },
|
|
41
|
+
extensions: messagingExtensions(event.source, item),
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
if (event.event_type === "message_edit" && item?.message_edit) {
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
...base(delivery, context),
|
|
49
|
+
type: "notice",
|
|
50
|
+
notice_type: "message_updated",
|
|
51
|
+
message_id: context.createId(String(item.message_edit.mid)),
|
|
52
|
+
user: { id: context.createId(item.sender.id) },
|
|
53
|
+
message: [{ type: "text", data: { text: String(item.message_edit.text) } }],
|
|
54
|
+
extensions: messagingExtensions(event.source, item),
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
if (event.event_type === "read" && item?.read) {
|
|
59
|
+
return [
|
|
60
|
+
{
|
|
61
|
+
...base(delivery, context),
|
|
62
|
+
type: "notice",
|
|
63
|
+
notice_type: "message_status",
|
|
64
|
+
sub_type: "read",
|
|
65
|
+
message_id: context.createId(String(item.read.mid)),
|
|
66
|
+
user: { id: context.createId(item.sender.id) },
|
|
67
|
+
extensions: messagingExtensions(event.source, item),
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
if (event.event_type === "reaction" && item?.reaction) {
|
|
72
|
+
const removed = item.reaction.action === "unreact";
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
...base(delivery, context),
|
|
76
|
+
type: "notice",
|
|
77
|
+
notice_type: removed ? "reaction_removed" : "reaction_added",
|
|
78
|
+
message_id: context.createId(String(item.reaction.mid)),
|
|
79
|
+
user: { id: context.createId(item.sender.id) },
|
|
80
|
+
extensions: messagingExtensions(event.source, item),
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
if (event.event_type === "postback" && item?.postback) {
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
...base(delivery, context),
|
|
88
|
+
type: "notice",
|
|
89
|
+
notice_type: "interaction",
|
|
90
|
+
sub_type: "postback",
|
|
91
|
+
message_id: context.createId(String(item.postback.mid)),
|
|
92
|
+
user: { id: context.createId(item.sender.id) },
|
|
93
|
+
extensions: messagingExtensions(event.source, item),
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
...base(delivery, context),
|
|
100
|
+
type: "notice",
|
|
101
|
+
notice_type: "custom",
|
|
102
|
+
sub_type: event.source === "standby" ? `standby_${event.event_type}` : event.event_type,
|
|
103
|
+
user: item ? { id: context.createId(item.sender.id) } : undefined,
|
|
104
|
+
extensions: item
|
|
105
|
+
? messagingExtensions(event.source, item)
|
|
106
|
+
: { instagram: { change: event.change } },
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
function base(delivery, context) {
|
|
111
|
+
return {
|
|
112
|
+
id: context.createId(`event:${delivery.id}`),
|
|
113
|
+
timestamp: delivery.event.messaging?.timestamp || delivery.event.entry_time,
|
|
114
|
+
type: "custom",
|
|
115
|
+
platform: "instagram",
|
|
116
|
+
bot_id: context.botId,
|
|
117
|
+
raw_event: delivery.rawEnvelope.raw,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function messagingExtensions(source, item) {
|
|
121
|
+
return { instagram: { source, messaging: item.raw } };
|
|
122
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BaseApp } from "onebots";
|
|
2
|
+
import type { InstagramClient } from "./client.js";
|
|
3
|
+
/** 路径只注册一次,热重载时动态解析当前 Client,并传递签名覆盖的精确原始体。 */
|
|
4
|
+
export declare class InstagramHttpHost {
|
|
5
|
+
private readonly app;
|
|
6
|
+
private readonly resolveClient;
|
|
7
|
+
private readonly owners;
|
|
8
|
+
private readonly accountPaths;
|
|
9
|
+
private readonly mounted;
|
|
10
|
+
constructor(app: BaseApp, resolveClient: (accountId: string) => InstagramClient | undefined);
|
|
11
|
+
mount(accountId: string, client: InstagramClient): void;
|
|
12
|
+
private accept;
|
|
13
|
+
private isActive;
|
|
14
|
+
}
|
package/lib/http-host.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { InstagramError } from "./errors.js";
|
|
2
|
+
/** 路径只注册一次,热重载时动态解析当前 Client,并传递签名覆盖的精确原始体。 */
|
|
3
|
+
export class InstagramHttpHost {
|
|
4
|
+
app;
|
|
5
|
+
resolveClient;
|
|
6
|
+
owners = new Map();
|
|
7
|
+
accountPaths = new Map();
|
|
8
|
+
mounted = new Set();
|
|
9
|
+
constructor(app, resolveClient) {
|
|
10
|
+
this.app = app;
|
|
11
|
+
this.resolveClient = resolveClient;
|
|
12
|
+
}
|
|
13
|
+
mount(accountId, client) {
|
|
14
|
+
if (client.receiveMode === "manual")
|
|
15
|
+
return;
|
|
16
|
+
const path = normalizePath(client.config.http_path || "");
|
|
17
|
+
if (!path)
|
|
18
|
+
throw InstagramError.invalid("webhook 模式必须配置 http_path");
|
|
19
|
+
const current = this.owners.get(path);
|
|
20
|
+
if (current && current !== accountId && this.isActive(path, current)) {
|
|
21
|
+
throw new InstagramError(`Instagram HTTP 路径 ${path} 已由账号 ${current} 使用`, {
|
|
22
|
+
code: "INSTAGRAM_HTTP_PATH_CONFLICT",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
this.owners.set(path, accountId);
|
|
26
|
+
this.accountPaths.set(accountId, path);
|
|
27
|
+
if (this.mounted.has(path))
|
|
28
|
+
return;
|
|
29
|
+
this.mounted.add(path);
|
|
30
|
+
this.app.router.get(path, ctx => this.accept(path, ctx));
|
|
31
|
+
this.app.router.post(path, ctx => this.accept(path, ctx));
|
|
32
|
+
}
|
|
33
|
+
async accept(path, ctx) {
|
|
34
|
+
const owner = this.owners.get(path);
|
|
35
|
+
const client = owner ? this.resolveClient(owner) : undefined;
|
|
36
|
+
if (!owner || !client || !this.isActive(path, owner)) {
|
|
37
|
+
ctx.status = 404;
|
|
38
|
+
ctx.body = { error: { code: "NOT_FOUND", message: "Instagram 路由未激活" } };
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const raw = ctx.request.rawBody;
|
|
42
|
+
const response = await client.ingestHttp({
|
|
43
|
+
method: ctx.method,
|
|
44
|
+
url: ctx.url,
|
|
45
|
+
headers: { "x-hub-signature-256": ctx.get("x-hub-signature-256") || undefined },
|
|
46
|
+
rawBody: typeof raw === "string"
|
|
47
|
+
? new TextEncoder().encode(raw)
|
|
48
|
+
: raw instanceof Uint8Array
|
|
49
|
+
? new Uint8Array(raw)
|
|
50
|
+
: undefined,
|
|
51
|
+
});
|
|
52
|
+
ctx.status = response.status;
|
|
53
|
+
ctx.body = response.body;
|
|
54
|
+
for (const [name, value] of Object.entries(response.headers))
|
|
55
|
+
ctx.set(name, value);
|
|
56
|
+
}
|
|
57
|
+
isActive(path, accountId) {
|
|
58
|
+
const client = this.resolveClient(accountId);
|
|
59
|
+
return Boolean(client && client.receiveMode !== "manual" && this.accountPaths.get(accountId) === path);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function normalizePath(value) {
|
|
63
|
+
return value === "/" ? "/" : value.replace(/\/+$/u, "");
|
|
64
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Schema } from "onebots";
|
|
2
|
+
import { InstagramAdapter } from "./adapter.js";
|
|
3
|
+
import type { InstagramConfig } from "./types.js";
|
|
4
|
+
export { InstagramAdapter };
|
|
5
|
+
export { InstagramClient, type InstagramClientDependencies } 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, type InstagramAttachmentUploader, } from "./messages.js";
|
|
10
|
+
export { executeInstagramPlatformAction, INSTAGRAM_PLATFORM_ACTIONS, type InstagramPlatformAction, } from "./platform-actions.js";
|
|
11
|
+
export { InstagramWebhookCodec } from "./webhook-codec.js";
|
|
12
|
+
export type { InstagramApiMessage, InstagramAttachment, InstagramBusinessProfile, InstagramCallOptions, InstagramClientEvents, InstagramConfig, InstagramConversation, InstagramDelivery, InstagramEvent, InstagramEventType, InstagramGraphMethod, InstagramHttpRequest, InstagramHttpResponse, InstagramIngestResult, InstagramMessage, InstagramMessagingItem, InstagramOutgoingMessage, InstagramReceiveMode, InstagramSendResponse, InstagramUserProfile, InstagramWebhookEnvelope, } from "./types.js";
|
|
13
|
+
export declare const instagramSchema: Schema;
|
|
14
|
+
declare module "onebots" {
|
|
15
|
+
namespace Adapter {
|
|
16
|
+
interface Configs {
|
|
17
|
+
instagram: InstagramConfig;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|