@evanp/activitypub-bot 0.24.2 → 0.25.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/README.md CHANGED
@@ -157,6 +157,19 @@ An *OKBot* instance will reply to any message that it's mentioned in with the co
157
157
 
158
158
  A *DoNothingBot* instance will only do default stuff, like accepting follows.
159
159
 
160
+ #### FollowBackBot
161
+
162
+ A *FollowBackBot* will follow back anyone who follows it. Useful for collecting
163
+ public information.
164
+
165
+ #### RelayClientBot
166
+
167
+ A *RelayClientBot* can be the client of a Mastodon or Pleroma relay.
168
+
169
+ #### RelayServerBot
170
+
171
+ A *RelayServerBot* will act as a relay server for remote servers.
172
+
160
173
  ## API
161
174
 
162
175
  Custom bots can implement the [Bot](#bot) interface, which is easiest if you inherit from the `Bot` class.
@@ -203,6 +216,10 @@ Called when the bot is mentioned in an incoming object. Can be used to implement
203
216
  Called when the bot is followed by another actor. The first argument is the actor, and the second is the `Follow` activity itself. This method is called after the acceptance has been sent and the new
204
217
  follower has been added to the `followers` collection.
205
218
 
219
+ #### async onUndoFollow (actor, undoActivity, followActivity)
220
+
221
+ Called when the bot is unfollowed by another actor. The first argument is the actor, the second is the `Undo` activity, and the third is the `Follow` activity. This method is called after the `followers` and/or `pendingFollowers` collections have been updated.
222
+
206
223
  #### async onLike (object, activity)
207
224
 
208
225
  Called when one of the bot's objects is liked by another actor. The first argument is the object, and the second is the `Like` activity itself. This method is called after the like has been added to the
@@ -804,13 +804,16 @@ export class ActivityHandler {
804
804
  this.#logger.warn({
805
805
  msg: 'Follow activity object is not the bot',
806
806
  activity: undoActivity.id,
807
- object: object.id
807
+ object: object.id,
808
+ botId: this.#botId(bot)
808
809
  })
809
810
  return
810
811
  }
811
- if (!(await this.#actorStorage.isInCollection(bot.username, 'followers', actor))) {
812
+ if (
813
+ !(await this.#actorStorage.isInCollection(bot.username, 'followers', actor)) &&
814
+ !(await this.#actorStorage.isInCollection(bot.username, 'pendingFollowers', actor))) {
812
815
  this.#logger.warn({
813
- msg: 'Undo follow activity from actor not in followers',
816
+ msg: 'Undo follow activity from actor not in followers or pendingFollowers',
814
817
  activity: undoActivity.id,
815
818
  followActivity: followActivity.id,
816
819
  actor: actor.id
@@ -819,6 +822,18 @@ export class ActivityHandler {
819
822
  }
820
823
  await this.#actorStorage.removeFromCollection(bot.username, 'followers', actor)
821
824
  await this.#actorStorage.removeFromCollection(bot.username, 'pendingFollowers', actor)
825
+ this.#logger.debug(
826
+ { bot: this.#botId(bot), activity: undoActivity.id },
827
+ 'Notifying bot of undo follow activity'
828
+ )
829
+ try {
830
+ await bot.onUndoFollow(actor, undoActivity, followActivity)
831
+ } catch (err) {
832
+ this.#logger.warn(
833
+ { err, bot: this.#botId(bot), activity: undoActivity.id },
834
+ 'Error notifying bot of undo follow activity'
835
+ )
836
+ }
822
837
  }
823
838
 
824
839
  async onIdle () {
package/lib/bot.js CHANGED
@@ -56,4 +56,8 @@ export default class Bot {
56
56
  async handleActivity (activity) {
57
57
  return false
58
58
  }
59
+
60
+ async onUndoFollow (actor, undoActivity, followActivity) {
61
+ ; // no-op
62
+ }
59
63
  }
package/lib/botcontext.js CHANGED
@@ -224,17 +224,32 @@ export class BotContext {
224
224
  async followActor (actor) {
225
225
  assert.ok(actor)
226
226
  assert.equal(typeof actor, 'object')
227
- await this.#actorStorage.addToCollection(
228
- this.#botId,
229
- 'pendingFollowing',
230
- actor
231
- )
232
- const activity = await this.#doActivity({
233
- type: 'Follow',
234
- object: actor.id,
235
- to: actor.id
236
- })
237
- await this.#actorStorage.setLastActivity(this.#botId, activity)
227
+ let activity
228
+ if (await this.#actorStorage.isInCollection(this.#botId, 'following', actor) ||
229
+ await this.#actorStorage.isInCollection(this.#botId, 'pendingFollowing', actor)) {
230
+ const originalId = await this.#actorStorage.getLastActivity(
231
+ this.#botId,
232
+ 'Follow',
233
+ actor
234
+ )
235
+ if (!originalId) {
236
+ throw new Error(`no Follow activity for ${actor.id}`)
237
+ }
238
+ activity = await this.#objectStorage.read(originalId)
239
+ } else {
240
+ await this.#actorStorage.addToCollection(
241
+ this.#botId,
242
+ 'pendingFollowing',
243
+ actor
244
+ )
245
+ activity = await this.#doActivity({
246
+ type: 'Follow',
247
+ object: actor.id,
248
+ to: actor.id
249
+ })
250
+ await this.#actorStorage.setLastActivity(this.#botId, activity)
251
+ }
252
+
238
253
  return activity
239
254
  }
240
255
 
@@ -420,13 +435,13 @@ export class BotContext {
420
435
  #getRecipients (obj) {
421
436
  assert.ok(obj)
422
437
  assert.strictEqual(typeof obj, 'object', 'obj must be an object')
423
- const to = obj.to ? Array.from(obj.to).map((to) => to.id) : null
424
- const cc = obj.cc ? Array.from(obj.cc).map((cc) => cc.id) : null
425
- const bto = obj.bto ? Array.from(obj.bto).map((bto) => bto.id) : null
426
- const bcc = obj.bcc ? Array.from(obj.bcc).map((bcc) => bcc.id) : null
438
+ const to = obj.to ? Array.from(obj.to).map((to) => to.id) : undefined
439
+ const cc = obj.cc ? Array.from(obj.cc).map((cc) => cc.id) : undefined
440
+ const bto = obj.bto ? Array.from(obj.bto).map((bto) => bto.id) : undefined
441
+ const bcc = obj.bcc ? Array.from(obj.bcc).map((bcc) => bcc.id) : undefined
427
442
  const audience = obj.audience
428
443
  ? Array.from(obj.audience).map((audience) => audience.id)
429
- : null
444
+ : undefined
430
445
  return { to, cc, bto, bcc, audience }
431
446
  }
432
447
 
@@ -445,7 +460,7 @@ export class BotContext {
445
460
  async #doActivity (activityData, distribute = true) {
446
461
  const now = new Date().toISOString()
447
462
  const type = activityData.type || 'Activity'
448
- const activity = await as2.import({
463
+ const json = {
449
464
  ...activityData,
450
465
  type,
451
466
  id: this.#formatter.format({
@@ -459,7 +474,14 @@ export class BotContext {
459
474
  },
460
475
  published: now,
461
476
  updated: now
462
- })
477
+ }
478
+ let activity
479
+ try {
480
+ activity = await as2.import(json)
481
+ } catch (err) {
482
+ this.#logger.warn({ err, json, method: '#doActivity' }, 'Import error')
483
+ throw err
484
+ }
463
485
  await this.#objectStorage.create(activity)
464
486
  await this.#actorStorage.addToCollection(this.#botId, 'outbox', activity)
465
487
  await this.#actorStorage.addToCollection(this.#botId, 'inbox', activity)
@@ -488,7 +510,7 @@ export class BotContext {
488
510
  type,
489
511
  object: {
490
512
  id: obj.id,
491
- type: obj.type
513
+ ...(obj.type && { type: obj.type })
492
514
  }
493
515
  },
494
516
  ...recipients
@@ -0,0 +1,33 @@
1
+ import Bot from '../bot.js'
2
+
3
+ const DEFAULT_NAME = 'FollowBackBot'
4
+ const DEFAULT_DESCRIPTION = 'A bot that follows you back'
5
+
6
+ export default class FollowBackBot extends Bot {
7
+ #fullname
8
+ #description
9
+
10
+ constructor (username, { fullname = DEFAULT_NAME, description = DEFAULT_DESCRIPTION } = {}) {
11
+ super(username)
12
+ this.#fullname = fullname
13
+ this.#description = description
14
+ }
15
+
16
+ get fullname () {
17
+ return this.#fullname
18
+ }
19
+
20
+ get description () {
21
+ return this.#description
22
+ }
23
+
24
+ async onFollow (actor, activity) {
25
+ this._context.logger.info({ actorId: actor.id }, 'Following user back')
26
+ await this._context.followActor(actor)
27
+ }
28
+
29
+ async onUndoFollow (actor, undoActivity, followActivity) {
30
+ this._context.logger.info({ actorId: actor.id }, 'Unfollowing user back')
31
+ await this._context.unfollowActor(actor)
32
+ }
33
+ }
package/lib/index.js CHANGED
@@ -5,3 +5,4 @@ export { default as OKBot } from './bots/ok.js'
5
5
  export { default as DoNothingBot } from './bots/donothing.js'
6
6
  export { default as RelayClientBot } from './bots/relayclient.js'
7
7
  export { default as RelayServerBot } from './bots/relayserver.js'
8
+ export { default as FollowBackBot } from './bots/followback.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evanp/activitypub-bot",
3
- "version": "0.24.2",
3
+ "version": "0.25.0",
4
4
  "description": "server-side ActivityPub bot framework",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -43,7 +43,7 @@
43
43
  "sequelize": "^6.37.7"
44
44
  },
45
45
  "devDependencies": {
46
- "@evanp/activitypub-nock": "^0.5.0",
46
+ "@evanp/activitypub-nock": "^0.6.0",
47
47
  "eslint": "^8.57.1",
48
48
  "eslint-config-standard": "^17.1.0",
49
49
  "eslint-plugin-import": "^2.29.1",