@evanp/activitypub-bot 0.45.3 → 0.45.5
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/CHANGELOG.md +13 -0
- package/lib/botcontext.js +4 -0
- package/lib/bots/followback.js +68 -0
- package/lib/distributionworker.js +7 -0
- package/lib/requestthrottler.js +12 -1
- package/lib/worker.js +6 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,19 @@ and this project adheres to
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.45.5] - 2026-04-27
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Clear stale follow requests in followback bot
|
|
17
|
+
- Synchronize followers collection in followback bot
|
|
18
|
+
|
|
19
|
+
## [0.45.4] - 2026-04-26
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Retry throttled deliveries
|
|
24
|
+
|
|
12
25
|
## [0.45.3] - 2026-04-24
|
|
13
26
|
|
|
14
27
|
### Fixed
|
package/lib/botcontext.js
CHANGED
|
@@ -487,6 +487,10 @@ export class BotContext {
|
|
|
487
487
|
yield * this.#actorStorage.items(this.#botId, 'following')
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
+
async * pendingFollowing () {
|
|
491
|
+
yield * this.#actorStorage.items(this.#botId, 'pendingFollowing')
|
|
492
|
+
}
|
|
493
|
+
|
|
490
494
|
async addFollowingUnsafe (actor) {
|
|
491
495
|
assert.ok(actor)
|
|
492
496
|
assert.equal(typeof actor, 'object')
|
package/lib/bots/followback.js
CHANGED
|
@@ -3,14 +3,30 @@ import Bot from '../bot.js'
|
|
|
3
3
|
const DEFAULT_NAME = 'FollowBackBot'
|
|
4
4
|
const DEFAULT_DESCRIPTION = 'A bot that follows you back'
|
|
5
5
|
|
|
6
|
+
// 7-day default timeout
|
|
7
|
+
|
|
8
|
+
const DEFAULT_STALE_FOLLOW_TIMEOUT = 7 * 24 * 60 * 60 * 1000
|
|
9
|
+
|
|
6
10
|
export default class FollowBackBot extends Bot {
|
|
7
11
|
#fullname
|
|
8
12
|
#description
|
|
13
|
+
#staleFollowTimeout
|
|
9
14
|
|
|
10
15
|
constructor (username, options = {}) {
|
|
11
16
|
super(username, options)
|
|
12
17
|
this.#fullname = options.fullname || DEFAULT_NAME
|
|
13
18
|
this.#description = options.description || DEFAULT_DESCRIPTION
|
|
19
|
+
this.#staleFollowTimeout = ('staleFollowTimeout' in options)
|
|
20
|
+
? options.staleFollowTimeout
|
|
21
|
+
: DEFAULT_STALE_FOLLOW_TIMEOUT
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async initialize (context) {
|
|
25
|
+
await super.initialize(context)
|
|
26
|
+
await this.#undoStalePendingFollowing()
|
|
27
|
+
// Drain the queue so undos arrive before re-follows
|
|
28
|
+
await this._context.onIdle()
|
|
29
|
+
await this.#synchronizeFollowers()
|
|
14
30
|
}
|
|
15
31
|
|
|
16
32
|
get fullname () {
|
|
@@ -30,4 +46,56 @@ export default class FollowBackBot extends Bot {
|
|
|
30
46
|
this._context.logger.info({ actorId: actor.id }, 'Unfollowing user back')
|
|
31
47
|
await this._context.unfollowActor(actor)
|
|
32
48
|
}
|
|
49
|
+
|
|
50
|
+
async #synchronizeFollowers () {
|
|
51
|
+
for await (const follower of this._context.followers()) {
|
|
52
|
+
try {
|
|
53
|
+
if (!await this._context.isFollowing(follower) &&
|
|
54
|
+
!await this._context.isPendingFollowing(follower)) {
|
|
55
|
+
await this._context.followActor(follower)
|
|
56
|
+
this._context.logger.info(
|
|
57
|
+
{
|
|
58
|
+
actorId: follower.id
|
|
59
|
+
},
|
|
60
|
+
'Synchronized a follower not yet followed'
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
this._context.logger.error(
|
|
65
|
+
{
|
|
66
|
+
err,
|
|
67
|
+
follower: follower.id
|
|
68
|
+
},
|
|
69
|
+
'Error checking for followback; skipping'
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async #undoStalePendingFollowing () {
|
|
76
|
+
const now = new Date()
|
|
77
|
+
for await (const follow of this._context.pendingFollowing()) {
|
|
78
|
+
try {
|
|
79
|
+
const activity = await this._context.getObject(follow.id)
|
|
80
|
+
if (activity.published && (now - activity.published > this.#staleFollowTimeout)) {
|
|
81
|
+
await this._context.unfollowActor(activity.object.first)
|
|
82
|
+
this._context.logger.info(
|
|
83
|
+
{
|
|
84
|
+
actorId: activity.object.first?.id,
|
|
85
|
+
published: activity.published
|
|
86
|
+
},
|
|
87
|
+
'Unfollowed stale actor'
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
this._context.logger.error(
|
|
92
|
+
{
|
|
93
|
+
err,
|
|
94
|
+
activity: follow.id
|
|
95
|
+
},
|
|
96
|
+
'Error checking stale follow; skipping'
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
33
101
|
}
|
|
@@ -2,6 +2,7 @@ import assert from 'node:assert'
|
|
|
2
2
|
|
|
3
3
|
import as2 from './activitystreams.js'
|
|
4
4
|
import { Worker, RecoverableError } from './worker.js'
|
|
5
|
+
import { ThrottleError } from './requestthrottler.js'
|
|
5
6
|
|
|
6
7
|
export class DistributionWorker extends Worker {
|
|
7
8
|
static #MAX_ATTEMPTS = 21 // ~24 days
|
|
@@ -21,6 +22,12 @@ export class DistributionWorker extends Worker {
|
|
|
21
22
|
this._logger.info({ activity: activity.id, inbox }, 'Delivered activity')
|
|
22
23
|
} catch (err) {
|
|
23
24
|
if (!err.status) {
|
|
25
|
+
if (err instanceof ThrottleError) {
|
|
26
|
+
this._logger.warn(
|
|
27
|
+
{ err, activity: activity.id, inbox },
|
|
28
|
+
'Throttled delivery, waiting to retry')
|
|
29
|
+
throw new RecoverableError(err.message, err.waitTime)
|
|
30
|
+
}
|
|
24
31
|
this._logger.warn(
|
|
25
32
|
{ err, activity: activity.id, inbox },
|
|
26
33
|
'Could not deliver activity and no HTTP status available')
|
package/lib/requestthrottler.js
CHANGED
|
@@ -3,6 +3,14 @@ import assert from 'node:assert'
|
|
|
3
3
|
|
|
4
4
|
const BETA = 0.75
|
|
5
5
|
|
|
6
|
+
export class ThrottleError extends Error {
|
|
7
|
+
constructor (message, waitTime) {
|
|
8
|
+
super(message)
|
|
9
|
+
this.name = this.constructor.name
|
|
10
|
+
this.waitTime = waitTime
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
export class RequestThrottler {
|
|
7
15
|
#connection
|
|
8
16
|
#logger
|
|
@@ -19,7 +27,10 @@ export class RequestThrottler {
|
|
|
19
27
|
assert.strictEqual(typeof maxWaitTime, 'number')
|
|
20
28
|
const waitTime = await this.#getWaitTime(host, maxWaitTime)
|
|
21
29
|
if (waitTime > maxWaitTime) {
|
|
22
|
-
throw new
|
|
30
|
+
throw new ThrottleError(
|
|
31
|
+
`Wait time is too long; ${waitTime} > ${maxWaitTime}`,
|
|
32
|
+
waitTime
|
|
33
|
+
)
|
|
23
34
|
}
|
|
24
35
|
if (waitTime > 0) {
|
|
25
36
|
await this.#decrement(host)
|
package/lib/worker.js
CHANGED
|
@@ -2,7 +2,12 @@ import assert from 'node:assert'
|
|
|
2
2
|
import { nanoid } from 'nanoid'
|
|
3
3
|
|
|
4
4
|
export class RecoverableError extends Error {
|
|
5
|
-
delay
|
|
5
|
+
delay
|
|
6
|
+
constructor (message, delay = 1000) {
|
|
7
|
+
super(message)
|
|
8
|
+
this.name = this.constructor.name
|
|
9
|
+
this.delay = delay
|
|
10
|
+
}
|
|
6
11
|
}
|
|
7
12
|
|
|
8
13
|
export class Worker {
|