@evanp/activitypub-bot 0.12.1 → 0.13.1
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/app.js +19 -1
- package/lib/index.js +2 -0
- package/lib/routes/collection.js +83 -67
- package/lib/routes/inbox.js +8 -0
- package/lib/routes/object.js +25 -5
- package/package.json +3 -3
- package/.github/dependabot.yml +0 -11
- package/.github/workflows/main.yml +0 -34
- package/.github/workflows/tag.yml +0 -106
- package/.nvmrc +0 -1
- package/Dockerfile +0 -17
- package/docs/activitypub.bot.drawio +0 -110
- package/tests/activitydistributor.test.js +0 -606
- package/tests/activityhandler.test.js +0 -2276
- package/tests/activitypubclient.test.js +0 -210
- package/tests/actorstorage.test.js +0 -283
- package/tests/app.test.js +0 -17
- package/tests/authorizer.test.js +0 -301
- package/tests/bot.donothing.test.js +0 -30
- package/tests/bot.ok.test.js +0 -101
- package/tests/botcontext.test.js +0 -720
- package/tests/botdatastorage.test.js +0 -88
- package/tests/botfactory.provincebotfactory.test.js +0 -430
- package/tests/digester.test.js +0 -56
- package/tests/fixtures/bots.js +0 -27
- package/tests/fixtures/eventloggingbot.js +0 -57
- package/tests/fixtures/provincebotfactory.js +0 -53
- package/tests/httpsignature.test.js +0 -199
- package/tests/httpsignatureauthenticator.test.js +0 -463
- package/tests/index.test.js +0 -10
- package/tests/keystorage.test.js +0 -124
- package/tests/microsyntax.test.js +0 -123
- package/tests/objectcache.test.js +0 -133
- package/tests/objectstorage.test.js +0 -149
- package/tests/remotekeystorage.test.js +0 -78
- package/tests/routes.actor.test.js +0 -214
- package/tests/routes.collection.test.js +0 -433
- package/tests/routes.health.test.js +0 -41
- package/tests/routes.inbox.test.js +0 -136
- package/tests/routes.object.test.js +0 -525
- package/tests/routes.server.test.js +0 -69
- package/tests/routes.sharedinbox.test.js +0 -473
- package/tests/routes.webfinger.test.js +0 -68
- package/tests/urlformatter.test.js +0 -164
- package/tests/utils/digest.js +0 -7
- package/tests/utils/nock.js +0 -499
package/tests/authorizer.test.js
DELETED
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, after } from 'node:test'
|
|
2
|
-
import { Authorizer } from '../lib/authorizer.js'
|
|
3
|
-
import { ActorStorage } from '../lib/actorstorage.js'
|
|
4
|
-
import { Sequelize } from 'sequelize'
|
|
5
|
-
import { UrlFormatter } from '../lib/urlformatter.js'
|
|
6
|
-
import { KeyStorage } from '../lib/keystorage.js'
|
|
7
|
-
import { ActivityPubClient } from '../lib/activitypubclient.js'
|
|
8
|
-
import as2 from '../lib/activitystreams.js'
|
|
9
|
-
import assert from 'node:assert/strict'
|
|
10
|
-
import { nanoid } from 'nanoid'
|
|
11
|
-
import { HTTPSignature } from '../lib/httpsignature.js'
|
|
12
|
-
import Logger from 'pino'
|
|
13
|
-
import { Digester } from '../lib/digester.js'
|
|
14
|
-
import { runMigrations } from '../lib/migrations/index.js'
|
|
15
|
-
|
|
16
|
-
describe('Authorizer', () => {
|
|
17
|
-
let authorizer = null
|
|
18
|
-
let actorStorage = null
|
|
19
|
-
let formatter = null
|
|
20
|
-
let connection = null
|
|
21
|
-
let keyStorage = null
|
|
22
|
-
let client = null
|
|
23
|
-
|
|
24
|
-
let actor1 = null
|
|
25
|
-
let actor2 = null
|
|
26
|
-
let actor3 = null
|
|
27
|
-
let publicObject = null
|
|
28
|
-
let followersOnlyObject = null
|
|
29
|
-
let privateObject = null
|
|
30
|
-
let remoteUnconnected = null
|
|
31
|
-
let remoteFollower = null
|
|
32
|
-
let remoteAddressee = null
|
|
33
|
-
let remotePublicObject = null
|
|
34
|
-
let remotePrivateObject = null
|
|
35
|
-
|
|
36
|
-
before(async () => {
|
|
37
|
-
const logger = Logger({
|
|
38
|
-
level: 'silent'
|
|
39
|
-
})
|
|
40
|
-
formatter = new UrlFormatter('https://activitypubbot.example')
|
|
41
|
-
connection = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
|
42
|
-
await connection.authenticate()
|
|
43
|
-
await runMigrations(connection)
|
|
44
|
-
actorStorage = new ActorStorage(connection, formatter)
|
|
45
|
-
keyStorage = new KeyStorage(connection, logger)
|
|
46
|
-
const signer = new HTTPSignature(logger)
|
|
47
|
-
const digester = new Digester(logger)
|
|
48
|
-
client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger)
|
|
49
|
-
actor1 = await actorStorage.getActor('test1')
|
|
50
|
-
actor2 = await actorStorage.getActor('test2')
|
|
51
|
-
await actorStorage.addToCollection(
|
|
52
|
-
'test1',
|
|
53
|
-
'followers',
|
|
54
|
-
actor2
|
|
55
|
-
)
|
|
56
|
-
actor3 = await actorStorage.getActor('test3')
|
|
57
|
-
remoteUnconnected = await as2.import({
|
|
58
|
-
id: 'https://remote.example/user/remote1',
|
|
59
|
-
type: 'Person',
|
|
60
|
-
preferredUsername: 'remote1',
|
|
61
|
-
to: 'as:Public'
|
|
62
|
-
})
|
|
63
|
-
remoteFollower = await as2.import({
|
|
64
|
-
id: 'https://remote.example/user/remote2',
|
|
65
|
-
type: 'Person',
|
|
66
|
-
preferredUsername: 'remote2',
|
|
67
|
-
to: 'as:Public'
|
|
68
|
-
})
|
|
69
|
-
await actorStorage.addToCollection(
|
|
70
|
-
'test1',
|
|
71
|
-
'followers',
|
|
72
|
-
remoteFollower
|
|
73
|
-
)
|
|
74
|
-
remoteAddressee = await as2.import({
|
|
75
|
-
id: 'https://remote.example/user/remote3',
|
|
76
|
-
type: 'Person',
|
|
77
|
-
preferredUsername: 'remote3',
|
|
78
|
-
to: 'as:Public'
|
|
79
|
-
})
|
|
80
|
-
publicObject = await as2.import({
|
|
81
|
-
id: formatter.format({
|
|
82
|
-
username: 'test1',
|
|
83
|
-
type: 'object',
|
|
84
|
-
nanoid: nanoid()
|
|
85
|
-
}),
|
|
86
|
-
type: 'Object',
|
|
87
|
-
attributedTo: actor1.id,
|
|
88
|
-
to: 'as:Public'
|
|
89
|
-
})
|
|
90
|
-
followersOnlyObject = await as2.import({
|
|
91
|
-
id: formatter.format({
|
|
92
|
-
username: 'test1',
|
|
93
|
-
type: 'object',
|
|
94
|
-
nanoid: nanoid()
|
|
95
|
-
}),
|
|
96
|
-
type: 'Object',
|
|
97
|
-
attributedTo: actor1.id,
|
|
98
|
-
to: formatter.format({
|
|
99
|
-
username: 'test1',
|
|
100
|
-
collection: 'followers'
|
|
101
|
-
})
|
|
102
|
-
})
|
|
103
|
-
privateObject = await as2.import({
|
|
104
|
-
id: formatter.format({
|
|
105
|
-
username: 'test1',
|
|
106
|
-
type: 'object',
|
|
107
|
-
nanoid: nanoid()
|
|
108
|
-
}),
|
|
109
|
-
type: 'Object',
|
|
110
|
-
attributedTo: actor1.id,
|
|
111
|
-
to: [actor2.id, remoteAddressee.id]
|
|
112
|
-
})
|
|
113
|
-
remotePublicObject = await as2.import({
|
|
114
|
-
id: 'https://remote.example/user/remote1/object/1',
|
|
115
|
-
type: 'Object',
|
|
116
|
-
attributedTo: remoteUnconnected.id,
|
|
117
|
-
to: 'as:Public'
|
|
118
|
-
})
|
|
119
|
-
remotePrivateObject = await as2.import({
|
|
120
|
-
id: 'https://remote.example/user/remote1/object/2',
|
|
121
|
-
type: 'Object',
|
|
122
|
-
attributedTo: remoteUnconnected.id,
|
|
123
|
-
to: actor2.id
|
|
124
|
-
})
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
after(async () => {
|
|
128
|
-
await connection.close()
|
|
129
|
-
formatter = null
|
|
130
|
-
actorStorage = null
|
|
131
|
-
connection = null
|
|
132
|
-
authorizer = null
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('should be a class', async () => {
|
|
136
|
-
assert.strictEqual(typeof Authorizer, 'function')
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
it('can be instantiated', async () => {
|
|
140
|
-
try {
|
|
141
|
-
authorizer = new Authorizer(actorStorage, formatter, client)
|
|
142
|
-
assert.strictEqual(typeof authorizer, 'object')
|
|
143
|
-
} catch (error) {
|
|
144
|
-
assert.fail(error)
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('can check the creator can read a public local object', async () => {
|
|
149
|
-
assert.strictEqual(true, await authorizer.canRead(actor1, publicObject))
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('can check the creator can read a followers-only local object', async () => {
|
|
153
|
-
assert.strictEqual(
|
|
154
|
-
true,
|
|
155
|
-
await authorizer.canRead(actor1, followersOnlyObject)
|
|
156
|
-
)
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('can check the creator can read a private local object', async () => {
|
|
160
|
-
assert.strictEqual(
|
|
161
|
-
true,
|
|
162
|
-
await authorizer.canRead(actor1, privateObject)
|
|
163
|
-
)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('can check if a local follower can read a public local object', async () => {
|
|
167
|
-
assert.strictEqual(true, await authorizer.canRead(actor2, publicObject))
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('can check if a local follower can read a followers-only local object', async () => {
|
|
171
|
-
assert.strictEqual(true, await authorizer.canRead(actor2, followersOnlyObject))
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('can check if a local addressee can read a private local object', async () => {
|
|
175
|
-
assert.strictEqual(true, await authorizer.canRead(actor2, privateObject))
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
it('can check if a local non-follower can read a public local object', async () => {
|
|
179
|
-
assert.strictEqual(true, await authorizer.canRead(actor3, publicObject))
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('can check if a local non-follower can read a followers-only local object', async () => {
|
|
183
|
-
assert.strictEqual(false, await authorizer.canRead(actor3, followersOnlyObject))
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
it('can check if a local non-addressee can read a private local object', async () => {
|
|
187
|
-
assert.strictEqual(false, await authorizer.canRead(actor3, privateObject))
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('can check if the null actor can read a public local object', async () => {
|
|
191
|
-
assert.strictEqual(true, await authorizer.canRead(null, publicObject))
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it('can check if the null actor can read a followers-only local object', async () => {
|
|
195
|
-
assert.strictEqual(false, await authorizer.canRead(null, followersOnlyObject))
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it('can check if the null actor can read a private local object', async () => {
|
|
199
|
-
assert.strictEqual(false, await authorizer.canRead(null, privateObject))
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('can check that an unconnected remote actor can read a public local object', async () => {
|
|
203
|
-
assert.strictEqual(true, await authorizer.canRead(remoteUnconnected, publicObject))
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('can check that an unconnected remote actor cannot read a followers-only local object', async () => {
|
|
207
|
-
assert.strictEqual(
|
|
208
|
-
false,
|
|
209
|
-
await authorizer.canRead(remoteUnconnected, followersOnlyObject)
|
|
210
|
-
)
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
it('can check that an unconnected remote actor cannot read a private local object', async () => {
|
|
214
|
-
assert.strictEqual(
|
|
215
|
-
false,
|
|
216
|
-
await authorizer.canRead(remoteUnconnected, privateObject)
|
|
217
|
-
)
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('can check that a remote follower can read a public local object', async () => {
|
|
221
|
-
assert.strictEqual(true, await authorizer.canRead(remoteFollower, publicObject))
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it('can check that a remote follower can read a followers-only local object', async () => {
|
|
225
|
-
assert.strictEqual(
|
|
226
|
-
true,
|
|
227
|
-
await authorizer.canRead(remoteFollower, followersOnlyObject)
|
|
228
|
-
)
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
it('can check that a remote follower cannot read a private local object', async () => {
|
|
232
|
-
assert.strictEqual(
|
|
233
|
-
false,
|
|
234
|
-
await authorizer.canRead(remoteFollower, privateObject)
|
|
235
|
-
)
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
it('can check that a remote addressee can read a private local object', async () => {
|
|
239
|
-
assert.strictEqual(true, await authorizer.canRead(remoteAddressee, privateObject))
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it('can check that a local actor can read a public remote object', async () => {
|
|
243
|
-
assert.strictEqual(true, await authorizer.canRead(actor1, remotePublicObject))
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
it('can check that a local non-addressee cannot read a private remote object', async () => {
|
|
247
|
-
assert.strictEqual(null, await authorizer.canRead(actor1, remotePrivateObject))
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
it('can check that a local addressee can read a private remote object', async () => {
|
|
251
|
-
assert.strictEqual(true, await authorizer.canRead(actor2, remotePrivateObject))
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
it('can check that two objects have the same origin', async () => {
|
|
255
|
-
const object1 = await as2.import({
|
|
256
|
-
id: 'https://example.com/object/1',
|
|
257
|
-
type: 'Object'
|
|
258
|
-
})
|
|
259
|
-
const object2 = await as2.import({
|
|
260
|
-
id: 'https://example.com/object/2',
|
|
261
|
-
type: 'Object'
|
|
262
|
-
})
|
|
263
|
-
assert.strictEqual(true, await authorizer.sameOrigin(object1, object2))
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
it('can check that two objects have different origins', async () => {
|
|
267
|
-
const object1 = await as2.import({
|
|
268
|
-
id: 'https://example.com/object/1',
|
|
269
|
-
type: 'Object'
|
|
270
|
-
})
|
|
271
|
-
const object2 = await as2.import({
|
|
272
|
-
id: 'https://other.example/object/2',
|
|
273
|
-
type: 'Object'
|
|
274
|
-
})
|
|
275
|
-
assert.strictEqual(false, await authorizer.sameOrigin(object1, object2))
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
it('can check that two objects have different origins by port', async () => {
|
|
279
|
-
const object1 = await as2.import({
|
|
280
|
-
id: 'https://example.com/object/1',
|
|
281
|
-
type: 'Object'
|
|
282
|
-
})
|
|
283
|
-
const object2 = await as2.import({
|
|
284
|
-
id: 'https://example.com:8000/object/2',
|
|
285
|
-
type: 'Object'
|
|
286
|
-
})
|
|
287
|
-
assert.strictEqual(false, await authorizer.sameOrigin(object1, object2))
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
it('can check that two objects have different origins by protocol', async () => {
|
|
291
|
-
const object1 = await as2.import({
|
|
292
|
-
id: 'https://example.com/object/1',
|
|
293
|
-
type: 'Object'
|
|
294
|
-
})
|
|
295
|
-
const object2 = await as2.import({
|
|
296
|
-
id: 'http://example.com/object/2',
|
|
297
|
-
type: 'Object'
|
|
298
|
-
})
|
|
299
|
-
assert.strictEqual(false, await authorizer.sameOrigin(object1, object2))
|
|
300
|
-
})
|
|
301
|
-
})
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, it, before } from 'node:test'
|
|
2
|
-
import assert from 'node:assert'
|
|
3
|
-
import request from 'supertest'
|
|
4
|
-
|
|
5
|
-
import { makeApp } from '../lib/app.js'
|
|
6
|
-
|
|
7
|
-
import { nockSetup } from './utils/nock.js'
|
|
8
|
-
import bots from './fixtures/bots.js'
|
|
9
|
-
|
|
10
|
-
describe('DoNothing bot', async () => {
|
|
11
|
-
const host = 'activitypubbot.example'
|
|
12
|
-
const origin = `https://${host}`
|
|
13
|
-
const databaseUrl = 'sqlite::memory:'
|
|
14
|
-
let app = null
|
|
15
|
-
|
|
16
|
-
before(async () => {
|
|
17
|
-
nockSetup('social.example')
|
|
18
|
-
app = await makeApp(databaseUrl, origin, bots, 'silent')
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
describe('Bot exists', async () => {
|
|
22
|
-
let response = null
|
|
23
|
-
it('should work without an error', async () => {
|
|
24
|
-
response = await request(app).get('/user/null')
|
|
25
|
-
})
|
|
26
|
-
it('should return 200 OK', async () => {
|
|
27
|
-
assert.strictEqual(response.status, 200)
|
|
28
|
-
})
|
|
29
|
-
})
|
|
30
|
-
})
|
package/tests/bot.ok.test.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { describe, it, before } from 'node:test'
|
|
2
|
-
import assert from 'node:assert'
|
|
3
|
-
import as2 from '../lib/activitystreams.js'
|
|
4
|
-
import request from 'supertest'
|
|
5
|
-
|
|
6
|
-
import { makeApp } from '../lib/app.js'
|
|
7
|
-
|
|
8
|
-
import { nockSetup, nockSignature, nockFormat, postInbox } from './utils/nock.js'
|
|
9
|
-
import { makeDigest } from './utils/digest.js'
|
|
10
|
-
import bots from './fixtures/bots.js'
|
|
11
|
-
|
|
12
|
-
describe('OK bot', async () => {
|
|
13
|
-
const host = 'activitypubbot.example'
|
|
14
|
-
const origin = `https://${host}`
|
|
15
|
-
const databaseUrl = 'sqlite::memory:'
|
|
16
|
-
let app = null
|
|
17
|
-
|
|
18
|
-
before(async () => {
|
|
19
|
-
nockSetup('social.example')
|
|
20
|
-
app = await makeApp(databaseUrl, origin, bots, 'silent')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
describe('responds to a mention', async () => {
|
|
24
|
-
const username = 'actor2'
|
|
25
|
-
const path = '/user/ok/inbox'
|
|
26
|
-
const url = `${origin}${path}`
|
|
27
|
-
const date = new Date().toUTCString()
|
|
28
|
-
const activity = await as2.import({
|
|
29
|
-
type: 'Create',
|
|
30
|
-
actor: nockFormat({ username }),
|
|
31
|
-
id: nockFormat({ username, type: 'create', num: 1 }),
|
|
32
|
-
object: {
|
|
33
|
-
id: nockFormat({ username, type: 'note', num: 1 }),
|
|
34
|
-
type: 'Note',
|
|
35
|
-
source: 'Hello, @ok!',
|
|
36
|
-
content: `Hello, @<a href="${origin}/user/ok">ok</a>!`,
|
|
37
|
-
to: `${origin}/user/ok`,
|
|
38
|
-
cc: 'as:Public',
|
|
39
|
-
attributedTo: nockFormat({ username }),
|
|
40
|
-
tag: [
|
|
41
|
-
{
|
|
42
|
-
type: 'Mention',
|
|
43
|
-
href: `${origin}/user/ok`,
|
|
44
|
-
name: `@ok@${host}`
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
},
|
|
48
|
-
to: `${origin}/user/ok`,
|
|
49
|
-
cc: 'as:Public'
|
|
50
|
-
})
|
|
51
|
-
const body = await activity.write()
|
|
52
|
-
const digest = makeDigest(body)
|
|
53
|
-
const signature = await nockSignature({
|
|
54
|
-
method: 'POST',
|
|
55
|
-
username,
|
|
56
|
-
url,
|
|
57
|
-
digest,
|
|
58
|
-
date
|
|
59
|
-
})
|
|
60
|
-
let response = null
|
|
61
|
-
it('should work without an error', async () => {
|
|
62
|
-
response = await request(app)
|
|
63
|
-
.post(path)
|
|
64
|
-
.send(body)
|
|
65
|
-
.set('Signature', signature)
|
|
66
|
-
.set('Date', date)
|
|
67
|
-
.set('Host', host)
|
|
68
|
-
.set('Digest', digest)
|
|
69
|
-
.set('Content-Type', 'application/activity+json')
|
|
70
|
-
assert.ok(response)
|
|
71
|
-
await app.onIdle()
|
|
72
|
-
})
|
|
73
|
-
it('should return a 200 status', async () => {
|
|
74
|
-
assert.strictEqual(response.status, 200, JSON.stringify(response.body))
|
|
75
|
-
})
|
|
76
|
-
it('should deliver the reply to the mentioned actor', async () => {
|
|
77
|
-
assert.strictEqual(postInbox.actor2, 1)
|
|
78
|
-
})
|
|
79
|
-
let reply = null
|
|
80
|
-
let note = null
|
|
81
|
-
it('should have the reply in its outbox', async () => {
|
|
82
|
-
const { actorStorage, objectStorage } = app.locals
|
|
83
|
-
const outbox = await actorStorage.getCollection('ok', 'outbox')
|
|
84
|
-
assert.strictEqual(outbox.totalItems, 1)
|
|
85
|
-
const outboxPage = await actorStorage.getCollectionPage('ok', 'outbox', 1)
|
|
86
|
-
assert.strictEqual(outboxPage.items.length, 1)
|
|
87
|
-
const arry = Array.from(outboxPage.items)
|
|
88
|
-
reply = await objectStorage.read(arry[0].id)
|
|
89
|
-
assert.ok(reply)
|
|
90
|
-
const objects = Array.from(reply.object)
|
|
91
|
-
note = await objectStorage.read(objects[0].id)
|
|
92
|
-
assert.ok(note)
|
|
93
|
-
})
|
|
94
|
-
it('should have the inReplyTo property', async () => {
|
|
95
|
-
assert.strictEqual(
|
|
96
|
-
Array.from(note.inReplyTo)[0].id,
|
|
97
|
-
Array.from(activity.object)[0].id
|
|
98
|
-
)
|
|
99
|
-
})
|
|
100
|
-
})
|
|
101
|
-
})
|