@satorijs/adapter-discord 3.5.7 → 3.6.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/bot.d.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  import { Bot, Context, Fragment, Quester, Schema, SendOptions } from '@satorijs/satori';
2
2
  import { DiscordMessenger } from './message';
3
- import { Internal } from './types';
3
+ import { Internal, Webhook } from './types';
4
4
  import { WsClient } from './ws';
5
5
  export declare class DiscordBot extends Bot<DiscordBot.Config> {
6
6
  http: Quester;
7
7
  internal: Internal;
8
+ webhooks: Record<string, Webhook>;
9
+ webhookLock: Record<string, Promise<Webhook>>;
8
10
  constructor(ctx: Context, config: DiscordBot.Config);
11
+ private _ensureWebhook;
12
+ ensureWebhook(channelId: string): Promise<Webhook>;
9
13
  getSelf(): Promise<import("@satorijs/core").Universal.User>;
10
14
  sendMessage(channelId: string, content: Fragment, guildId?: string, options?: SendOptions): Promise<string[]>;
11
15
  sendPrivateMessage(channelId: string, content: Fragment, options?: SendOptions): Promise<string[]>;
package/lib/index.js CHANGED
@@ -41,7 +41,8 @@ __export(src_exports, {
41
41
  adaptSession: () => adaptSession,
42
42
  adaptUser: () => adaptUser,
43
43
  default: () => src_default,
44
- prepareMessage: () => prepareMessage
44
+ prepareMessage: () => prepareMessage,
45
+ sanitize: () => sanitize
45
46
  });
46
47
  module.exports = __toCommonJS(src_exports);
47
48
 
@@ -50,6 +51,7 @@ var import_satori5 = require("@satorijs/satori");
50
51
 
51
52
  // satori/adapters/discord/src/utils.ts
52
53
  var import_satori = require("@satorijs/satori");
54
+ var sanitize = /* @__PURE__ */ __name((val) => val.replace(/[\\*_`~|()\[\]]/g, "\\$&").replace(/@everyone/g, () => "\\@everyone").replace(/@here/g, () => "\\@here"), "sanitize");
53
55
  var adaptUser = /* @__PURE__ */ __name((user) => ({
54
56
  userId: user.id,
55
57
  avatar: `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`,
@@ -87,20 +89,20 @@ async function adaptMessage(bot, meta, session = {}) {
87
89
  session.content = meta.content.replace(/<@[!&]?(.+?)>/g, (_, id) => {
88
90
  var _a2;
89
91
  if (meta.mention_roles.includes(id)) {
90
- return (0, import_satori.segment)("at", { role: id }).toString();
92
+ return (0, import_satori.h)("at", { role: id }).toString();
91
93
  } else {
92
94
  const user = (_a2 = meta.mentions) == null ? void 0 : _a2.find((u) => u.id === id || `${u.username}#${u.discriminator}` === id);
93
- return import_satori.segment.at(id, { name: user == null ? void 0 : user.username }).toString();
95
+ return import_satori.h.at(id, { name: user == null ? void 0 : user.username }).toString();
94
96
  }
95
97
  }).replace(/<a?:(.*):(.+?)>/g, (_, name, id) => {
96
98
  const animated = _[1] === "a";
97
- return (0, import_satori.segment)("face", { id, name, animated, platform }, [
98
- import_satori.segment.image(`https://cdn.discordapp.com/emojis/${id}.gif?quality=lossless`)
99
+ return (0, import_satori.h)("face", { id, name, animated, platform }, [
100
+ import_satori.h.image(`https://cdn.discordapp.com/emojis/${id}.gif?quality=lossless`)
99
101
  ]).toString();
100
- }).replace(/@everyone/g, () => (0, import_satori.segment)("at", { type: "all" }).toString()).replace(/@here/g, () => (0, import_satori.segment)("at", { type: "here" }).toString()).replace(/<#(.+?)>/g, (_, id) => {
102
+ }).replace(/@everyone/g, () => (0, import_satori.h)("at", { type: "all" }).toString()).replace(/@here/g, () => (0, import_satori.h)("at", { type: "here" }).toString()).replace(/<#(.+?)>/g, (_, id) => {
101
103
  var _a2;
102
104
  const channel = (_a2 = meta.mention_channels) == null ? void 0 : _a2.find((c) => c.id === id);
103
- return import_satori.segment.sharp(id, { name: channel == null ? void 0 : channel.name }).toString();
105
+ return import_satori.h.sharp(id, { name: channel == null ? void 0 : channel.name }).toString();
104
106
  });
105
107
  }
106
108
  if ((_c = meta.attachments) == null ? void 0 : _c.length) {
@@ -109,25 +111,25 @@ async function adaptMessage(bot, meta, session = {}) {
109
111
  session.content += meta.attachments.map((v) => {
110
112
  var _a2, _b2, _c2;
111
113
  if (v.height && v.width && ((_a2 = v.content_type) == null ? void 0 : _a2.startsWith("image/"))) {
112
- return (0, import_satori.segment)("image", {
114
+ return (0, import_satori.h)("image", {
113
115
  url: v.url,
114
116
  proxy_url: v.proxy_url,
115
117
  file: v.filename
116
118
  });
117
119
  } else if (v.height && v.width && ((_b2 = v.content_type) == null ? void 0 : _b2.startsWith("video/"))) {
118
- return (0, import_satori.segment)("video", {
120
+ return (0, import_satori.h)("video", {
119
121
  url: v.url,
120
122
  proxy_url: v.proxy_url,
121
123
  file: v.filename
122
124
  });
123
125
  } else if ((_c2 = v.content_type) == null ? void 0 : _c2.startsWith("audio/")) {
124
- return (0, import_satori.segment)("record", {
126
+ return (0, import_satori.h)("record", {
125
127
  url: v.url,
126
128
  proxy_url: v.proxy_url,
127
129
  file: v.filename
128
130
  });
129
131
  } else {
130
- return (0, import_satori.segment)("file", {
132
+ return (0, import_satori.h)("file", {
131
133
  url: v.url,
132
134
  proxy_url: v.proxy_url,
133
135
  file: v.filename
@@ -137,16 +139,16 @@ async function adaptMessage(bot, meta, session = {}) {
137
139
  }
138
140
  for (const embed of meta.embeds) {
139
141
  if (embed.image) {
140
- session.content += (0, import_satori.segment)("image", { url: embed.image.url, proxy_url: embed.image.proxy_url });
142
+ session.content += (0, import_satori.h)("image", { url: embed.image.url, proxy_url: embed.image.proxy_url });
141
143
  }
142
144
  if (embed.thumbnail) {
143
- session.content += (0, import_satori.segment)("image", { url: embed.thumbnail.url, proxy_url: embed.thumbnail.proxy_url });
145
+ session.content += (0, import_satori.h)("image", { url: embed.thumbnail.url, proxy_url: embed.thumbnail.proxy_url });
144
146
  }
145
147
  if (embed.video) {
146
- session.content += (0, import_satori.segment)("video", { url: embed.video.url, proxy_url: embed.video.proxy_url });
148
+ session.content += (0, import_satori.h)("video", { url: embed.video.url, proxy_url: embed.video.proxy_url });
147
149
  }
148
150
  }
149
- session.elements = import_satori.segment.parse(session.content);
151
+ session.elements = import_satori.h.parse(session.content);
150
152
  if (meta.message_reference) {
151
153
  const { message_id, channel_id } = meta.message_reference;
152
154
  session.quote = await bot.getMessage(channel_id, message_id);
@@ -175,6 +177,12 @@ __name(prepareReactionSession, "prepareReactionSession");
175
177
  async function adaptSession(bot, input) {
176
178
  const session = bot.session();
177
179
  if (input.t === "MESSAGE_CREATE") {
180
+ if (input.d.webhook_id) {
181
+ const webhook = await bot.ensureWebhook(input.d.channel_id);
182
+ if (webhook.id === input.d.webhook_id) {
183
+ return;
184
+ }
185
+ }
178
186
  session.type = "message";
179
187
  await adaptMessage(bot, input.d, session);
180
188
  } else if (input.t === "MESSAGE_UPDATE") {
@@ -215,22 +223,67 @@ __name(adaptSession, "adaptSession");
215
223
  // satori/adapters/discord/src/message.ts
216
224
  var import_satori2 = require("@satorijs/satori");
217
225
  var import_form_data = __toESM(require("form-data"));
226
+ var logger = new import_satori2.Logger("discord");
227
+ var State = class {
228
+ // forward: send the first message and create a thread
229
+ constructor(type) {
230
+ this.type = type;
231
+ this.author = {};
232
+ this.quote = {};
233
+ this.channel = {};
234
+ this.fakeMessageMap = {};
235
+ // [userInput] = discord messages
236
+ this.threadCreated = false;
237
+ }
238
+ };
239
+ __name(State, "State");
218
240
  var DiscordMessenger = class extends import_satori2.Messenger {
219
241
  constructor() {
220
242
  super(...arguments);
243
+ this.stack = [new State("message")];
221
244
  this.buffer = "";
222
245
  this.addition = {};
223
246
  this.figure = null;
224
247
  this.mode = "default";
225
248
  }
226
249
  async post(data, headers) {
250
+ var _a, _b, _c;
227
251
  try {
228
- const result = await this.bot.http.post(`/channels/${this.channelId}/messages`, data, { headers });
252
+ let url = `/channels/${this.channelId}/messages`;
253
+ if (this.stack[0].author.nickname || this.stack[0].author.avatar || this.stack[0].type === "forward" && !this.stack[0].threadCreated) {
254
+ const webhook = await this.ensureWebhook();
255
+ url = `/webhooks/${webhook.id}/${webhook.token}?wait=true`;
256
+ }
257
+ if (this.stack[0].type === "forward" && ((_a = this.stack[0].channel) == null ? void 0 : _a.id)) {
258
+ if (this.stack[1].author.nickname || this.stack[1].author.avatar) {
259
+ const webhook = await this.ensureWebhook();
260
+ url = `/webhooks/${webhook.id}/${webhook.token}?wait=true&thread_id=${(_b = this.stack[0].channel) == null ? void 0 : _b.id}`;
261
+ } else {
262
+ url = `/channels/${this.stack[0].channel.id}/messages`;
263
+ }
264
+ }
265
+ const result = await this.bot.http.post(url, data, { headers });
229
266
  const session = this.bot.session();
230
- await adaptMessage(this.bot, result, session);
267
+ const message = await adaptMessage(this.bot, result, session);
231
268
  session.app.emit(session, "send", session);
232
269
  this.results.push(session);
270
+ if (this.stack[0].type === "forward" && !this.stack[0].threadCreated) {
271
+ this.stack[0].threadCreated = true;
272
+ const thread = await this.bot.internal.startThreadFromMessage(this.channelId, result.id, {
273
+ name: "Forward",
274
+ auto_archive_duration: 60
275
+ });
276
+ this.stack[0].channel = thread;
277
+ }
278
+ return message;
233
279
  } catch (e) {
280
+ if (import_satori2.Quester.isAxiosError(e) && ((_c = e.response) == null ? void 0 : _c.data.code) === 10015) {
281
+ logger.debug("webhook has been deleted, recreating..., %o", e.response.data);
282
+ if (!this.bot.webhookLock[this.channelId])
283
+ this.bot.webhooks[this.channelId] = null;
284
+ await this.ensureWebhook();
285
+ return this.post(data, headers);
286
+ }
234
287
  this.errors.push(e);
235
288
  }
236
289
  }
@@ -242,9 +295,6 @@ var DiscordMessenger = class extends import_satori2.Messenger {
242
295
  form.append("payload_json", JSON.stringify(payload));
243
296
  return this.post(form, form.getHeaders());
244
297
  }
245
- async sendContent(content, addition) {
246
- return this.post({ ...addition, content });
247
- }
248
298
  async sendAsset(type, attrs, addition) {
249
299
  const { handleMixedContent, handleExternalAsset } = this.bot.config;
250
300
  if (handleMixedContent === "separate" && addition.content) {
@@ -277,6 +327,9 @@ var DiscordMessenger = class extends import_satori2.Messenger {
277
327
  }
278
328
  }, sendDownload);
279
329
  }
330
+ async ensureWebhook() {
331
+ return this.bot.ensureWebhook(this.channelId);
332
+ }
280
333
  async flush() {
281
334
  const content = this.buffer.trim();
282
335
  if (!content)
@@ -286,9 +339,10 @@ var DiscordMessenger = class extends import_satori2.Messenger {
286
339
  this.addition = {};
287
340
  }
288
341
  async visit(element) {
342
+ var _a;
289
343
  const { type, attrs, children } = element;
290
344
  if (type === "text") {
291
- this.buffer += attrs.content.replace(/[\\*_`~|()]/g, "\\$&");
345
+ this.buffer += sanitize(attrs.content);
292
346
  } else if (type === "b" || type === "strong") {
293
347
  this.buffer += "**";
294
348
  await this.render(children);
@@ -355,7 +409,7 @@ var DiscordMessenger = class extends import_satori2.Messenger {
355
409
  ...this.addition,
356
410
  embeds: [{ ...attrs }]
357
411
  });
358
- } else if (type === "record") {
412
+ } else if (type === "audio") {
359
413
  await this.sendAsset("file", attrs, {
360
414
  ...this.addition,
361
415
  content: this.buffer.trim()
@@ -371,20 +425,77 @@ var DiscordMessenger = class extends import_satori2.Messenger {
371
425
  });
372
426
  this.buffer = "";
373
427
  this.mode = "default";
374
- } else if (type === "quote") {
375
- await this.flush();
376
- this.addition.message_reference = {
377
- message_id: attrs.id
378
- };
379
- } else if (type === "message") {
428
+ } else if (type === "message" && !attrs.forward) {
380
429
  if (this.mode === "figure") {
381
430
  await this.render(children);
382
431
  this.buffer += "\n";
383
432
  } else {
433
+ const resultLength = +this.results.length;
384
434
  await this.flush();
435
+ const [author] = import_satori2.segment.select(children, "author");
436
+ if (author) {
437
+ const { avatar, nickname } = author.attrs;
438
+ if (avatar)
439
+ this.addition.avatar_url = avatar;
440
+ if (nickname)
441
+ this.addition.username = nickname;
442
+ if (this.stack[0].type === "message") {
443
+ this.stack[0].author = author.attrs;
444
+ }
445
+ if (this.stack[0].type === "forward") {
446
+ this.stack[1].author = author.attrs;
447
+ }
448
+ }
449
+ const [quote] = import_satori2.segment.select(children, "quote");
450
+ if (quote) {
451
+ const parse = /* @__PURE__ */ __name((val) => val.replace(/\\([\\*_`~|()\[\]])/g, "$1"), "parse");
452
+ const message = this.stack[this.stack[0].type === "forward" ? 1 : 0];
453
+ if (!message.author.avatar && !message.author.nickname && this.stack[0].type !== "forward") {
454
+ await this.flush();
455
+ this.addition.message_reference = {
456
+ message_id: quote.attrs.id
457
+ };
458
+ } else {
459
+ let replyId = quote.attrs.id, channelId = this.channelId;
460
+ if (this.stack[0].type === "forward" && ((_a = this.stack[0].fakeMessageMap[quote.attrs.id]) == null ? void 0 : _a.length) >= 1) {
461
+ replyId = this.stack[0].fakeMessageMap[quote.attrs.id][0].messageId;
462
+ channelId = this.stack[0].fakeMessageMap[quote.attrs.id][0].channelId;
463
+ }
464
+ const quoted = await this.bot.getMessage(channelId, replyId);
465
+ this.addition.embeds = [{
466
+ description: `${sanitize(parse(quoted.elements.filter((v) => v.type === "text").join("")).slice(0, 30))}
467
+
468
+ <t:${Math.ceil(quoted.timestamp / 1e3)}:R> [[ ↑ ]](https://discord.com/channels/${this.guildId}/${channelId}/${replyId})`,
469
+ author: {
470
+ name: quoted.author.nickname || quoted.author.username,
471
+ icon_url: quoted.author.avatar
472
+ }
473
+ }];
474
+ }
475
+ }
385
476
  await this.render(children);
386
477
  await this.flush();
478
+ const newLength = +this.results.length;
479
+ const sentMessages = this.results.slice(resultLength, newLength);
480
+ if (this.stack[0].type === "forward" && attrs.id) {
481
+ this.stack[0].fakeMessageMap[attrs.id] = sentMessages;
482
+ }
483
+ if (this.stack[0].type === "message") {
484
+ this.stack[0].author = {};
485
+ }
486
+ if (this.stack[0].type === "forward") {
487
+ this.stack[1].author = {};
488
+ }
387
489
  }
490
+ } else if (type === "message" && attrs.forward) {
491
+ this.stack.unshift(new State("forward"));
492
+ await this.render(children);
493
+ await this.flush();
494
+ await this.bot.internal.modifyChannel(this.stack[0].channel.id, {
495
+ archived: true,
496
+ locked: true
497
+ });
498
+ this.stack.shift();
388
499
  } else {
389
500
  await this.render(children);
390
501
  }
@@ -1445,13 +1556,13 @@ Internal.define({
1445
1556
 
1446
1557
  // satori/adapters/discord/src/types/webhook.ts
1447
1558
  var Webhook2;
1448
- ((Webhook3) => {
1559
+ ((Webhook4) => {
1449
1560
  let Type;
1450
1561
  ((Type2) => {
1451
1562
  Type2[Type2["INCOMING"] = 1] = "INCOMING";
1452
1563
  Type2[Type2["CHANNEL_FOLLOWER"] = 2] = "CHANNEL_FOLLOWER";
1453
1564
  Type2[Type2["APPLICATION"] = 3] = "APPLICATION";
1454
- })(Type = Webhook3.Type || (Webhook3.Type = {}));
1565
+ })(Type = Webhook4.Type || (Webhook4.Type = {}));
1455
1566
  })(Webhook2 || (Webhook2 = {}));
1456
1567
  Internal.define({
1457
1568
  "/channels/{channel.id}/webhooks": {
@@ -1487,7 +1598,7 @@ Internal.define({
1487
1598
 
1488
1599
  // satori/adapters/discord/src/ws.ts
1489
1600
  var import_satori4 = require("@satorijs/satori");
1490
- var logger = new import_satori4.Logger("discord");
1601
+ var logger2 = new import_satori4.Logger("discord");
1491
1602
  var WsClient = class extends import_satori4.Adapter.WsClient {
1492
1603
  constructor() {
1493
1604
  super(...arguments);
@@ -1502,7 +1613,7 @@ var WsClient = class extends import_satori4.Adapter.WsClient {
1502
1613
  return this.bot.http.ws(url);
1503
1614
  }
1504
1615
  heartbeat() {
1505
- logger.debug(`heartbeat d ${this._d}`);
1616
+ logger2.debug(`heartbeat d ${this._d}`);
1506
1617
  this.bot.socket.send(JSON.stringify({
1507
1618
  op: 1 /* HEARTBEAT */,
1508
1619
  d: this._d
@@ -1515,16 +1626,16 @@ var WsClient = class extends import_satori4.Adapter.WsClient {
1515
1626
  try {
1516
1627
  parsed = JSON.parse(data.toString());
1517
1628
  } catch (error) {
1518
- return logger.warn("cannot parse message", data);
1629
+ return logger2.warn("cannot parse message", data);
1519
1630
  }
1520
- logger.debug(require("util").inspect(parsed, false, null, true));
1631
+ logger2.debug(require("util").inspect(parsed, false, null, true));
1521
1632
  if (parsed.s) {
1522
1633
  this._d = parsed.s;
1523
1634
  }
1524
1635
  if (parsed.op === 10 /* HELLO */) {
1525
1636
  this._ping = setInterval(() => this.heartbeat(), parsed.d.heartbeat_interval);
1526
1637
  if (this._sessionId) {
1527
- logger.debug("resuming");
1638
+ logger2.debug("resuming");
1528
1639
  this.bot.socket.send(JSON.stringify({
1529
1640
  op: 6 /* RESUME */,
1530
1641
  d: {
@@ -1549,7 +1660,7 @@ var WsClient = class extends import_satori4.Adapter.WsClient {
1549
1660
  if (parsed.d)
1550
1661
  return;
1551
1662
  this._sessionId = "";
1552
- logger.warn("offline: invalid session");
1663
+ logger2.warn("offline: invalid session");
1553
1664
  this.bot.offline();
1554
1665
  (_a = this.bot.socket) == null ? void 0 : _a.close();
1555
1666
  }
@@ -1561,7 +1672,7 @@ var WsClient = class extends import_satori4.Adapter.WsClient {
1561
1672
  self.selfId = self.userId;
1562
1673
  delete self.userId;
1563
1674
  Object.assign(this.bot, self);
1564
- logger.debug("session_id " + this._sessionId);
1675
+ logger2.debug("session_id " + this._sessionId);
1565
1676
  return this.bot.online();
1566
1677
  }
1567
1678
  if (parsed.t === "RESUMED") {
@@ -1573,7 +1684,7 @@ var WsClient = class extends import_satori4.Adapter.WsClient {
1573
1684
  }
1574
1685
  if (parsed.op === 7 /* RECONNECT */) {
1575
1686
  this.bot.offline();
1576
- logger.warn("offline: discord request reconnect");
1687
+ logger2.warn("offline: discord request reconnect");
1577
1688
  (_b = this.bot.socket) == null ? void 0 : _b.close();
1578
1689
  }
1579
1690
  });
@@ -1597,6 +1708,8 @@ var import_package = require("../package.json");
1597
1708
  var DiscordBot = class extends import_satori5.Bot {
1598
1709
  constructor(ctx, config) {
1599
1710
  super(ctx, config);
1711
+ this.webhooks = {};
1712
+ this.webhookLock = {};
1600
1713
  this.http = ctx.http.extend({
1601
1714
  ...config,
1602
1715
  headers: {
@@ -1608,6 +1721,31 @@ var DiscordBot = class extends import_satori5.Bot {
1608
1721
  this.internal = new Internal(this.http);
1609
1722
  ctx.plugin(WsClient, this);
1610
1723
  }
1724
+ async _ensureWebhook(channelId) {
1725
+ let webhook;
1726
+ const webhooks = await this.internal.getChannelWebhooks(channelId);
1727
+ const selfId = this.selfId;
1728
+ if (!webhooks.find((v) => v.name === "Koishi" && v.user.id === selfId)) {
1729
+ webhook = await this.internal.createWebhook(channelId, {
1730
+ name: "Koishi"
1731
+ });
1732
+ } else {
1733
+ webhook = webhooks.find((v) => v.name === "Koishi" && v.user.id === this.selfId);
1734
+ }
1735
+ return this.webhooks[channelId] = webhook;
1736
+ }
1737
+ async ensureWebhook(channelId) {
1738
+ var _a;
1739
+ if (this.webhooks[channelId] === null) {
1740
+ delete this.webhooks[channelId];
1741
+ delete this.webhookLock[channelId];
1742
+ }
1743
+ if (this.webhooks[channelId]) {
1744
+ delete this.webhookLock[channelId];
1745
+ return this.webhooks[channelId];
1746
+ }
1747
+ return (_a = this.webhookLock)[channelId] || (_a[channelId] = this._ensureWebhook(channelId));
1748
+ }
1611
1749
  async getSelf() {
1612
1750
  const data = await this.internal.getCurrentUser();
1613
1751
  return adaptUser(data);
@@ -1622,7 +1760,7 @@ var DiscordBot = class extends import_satori5.Bot {
1622
1760
  await this.internal.deleteMessage(channelId, messageId);
1623
1761
  }
1624
1762
  async editMessage(channelId, messageId, content) {
1625
- const elements = import_satori5.segment.normalize(content);
1763
+ const elements = import_satori5.h.normalize(content);
1626
1764
  content = elements.toString();
1627
1765
  const image = elements.find((v) => v.type === "image");
1628
1766
  if (image) {
@@ -1702,6 +1840,7 @@ var src_default = DiscordBot;
1702
1840
  adaptMessage,
1703
1841
  adaptSession,
1704
1842
  adaptUser,
1705
- prepareMessage
1843
+ prepareMessage,
1844
+ sanitize
1706
1845
  });
1707
1846
  //# sourceMappingURL=index.js.map