mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at new-image-api 460 lines 11 kB view raw
1import {AtUri, BskyAgent} from '@atproto/api' 2import {TestBsky, TestNetwork} from '@atproto/dev-env' 3import fs from 'fs' 4import net from 'net' 5import path from 'path' 6 7export interface TestUser { 8 email: string 9 did: string 10 handle: string 11 password: string 12 agent: BskyAgent 13} 14 15export interface TestPDS { 16 pdsUrl: string 17 mocker: Mocker 18 close: () => Promise<void> 19} 20 21class StringIdGenerator { 22 _nextId = [0] 23 constructor( 24 public _chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 25 ) {} 26 27 next() { 28 const r = [] 29 for (const char of this._nextId) { 30 r.unshift(this._chars[char]) 31 } 32 this._increment() 33 return r.join('') 34 } 35 36 _increment() { 37 for (let i = 0; i < this._nextId.length; i++) { 38 const val = ++this._nextId[i] 39 if (val >= this._chars.length) { 40 this._nextId[i] = 0 41 } else { 42 return 43 } 44 } 45 this._nextId.push(0) 46 } 47 48 *[Symbol.iterator]() { 49 while (true) { 50 yield this.next() 51 } 52 } 53} 54 55const ids = new StringIdGenerator() 56 57export async function createServer( 58 {inviteRequired}: {inviteRequired: boolean} = { 59 inviteRequired: false, 60 }, 61): Promise<TestPDS> { 62 const port = 3000 63 const port2 = await getPort(port + 1) 64 const port3 = await getPort(port2 + 1) 65 const pdsUrl = `http://localhost:${port}` 66 const id = ids.next() 67 68 const testNet = await TestNetwork.create({ 69 pds: { 70 port, 71 hostname: 'localhost', 72 inviteRequired, 73 }, 74 bsky: { 75 dbPostgresSchema: `bsky_${id}`, 76 port: port3, 77 publicUrl: 'http://localhost:2584', 78 }, 79 plc: {port: port2}, 80 }) 81 82 // DISABLED - looks like dev-env added this and now it conflicts 83 // add the test mod authority 84 // const agent = new BskyAgent({service: pdsUrl}) 85 // const res = await agent.api.com.atproto.server.createAccount({ 86 // email: 'mod-authority@test.com', 87 // handle: 'mod-authority.test', 88 // password: 'hunter2', 89 // }) 90 // agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`) 91 // await agent.api.app.bsky.actor.profile.create( 92 // {repo: res.data.did}, 93 // { 94 // displayName: 'Dev-env Moderation', 95 // description: `The pretend version of mod.bsky.app`, 96 // }, 97 // ) 98 99 // await agent.api.app.bsky.labeler.service.create( 100 // {repo: res.data.did, rkey: 'self'}, 101 // { 102 // policies: { 103 // labelValues: ['!hide', '!warn'], 104 // labelValueDefinitions: [], 105 // }, 106 // createdAt: new Date().toISOString(), 107 // }, 108 // ) 109 110 const pic = fs.readFileSync( 111 path.join(__dirname, '..', 'assets', 'default-avatar.png'), 112 ) 113 114 return { 115 pdsUrl, 116 mocker: new Mocker(testNet, pdsUrl, pic), 117 async close() { 118 await testNet.close() 119 }, 120 } 121} 122 123class Mocker { 124 agent: BskyAgent 125 users: Record<string, TestUser> = {} 126 127 constructor( 128 public testNet: TestNetwork, 129 public service: string, 130 public pic: Uint8Array, 131 ) { 132 this.agent = new BskyAgent({service}) 133 } 134 135 get pds() { 136 return this.testNet.pds 137 } 138 139 get bsky() { 140 return this.testNet.bsky 141 } 142 143 get plc() { 144 return this.testNet.plc 145 } 146 147 // NOTE 148 // deterministic date generator 149 // we use this to ensure the mock dataset is always the same 150 // which is very useful when testing 151 *dateGen() { 152 let start = 1657846031914 153 while (true) { 154 yield new Date(start).toISOString() 155 start += 1e3 156 } 157 } 158 159 async createUser(name: string) { 160 const agent = new BskyAgent({service: this.service}) 161 162 const inviteRes = await agent.api.com.atproto.server.createInviteCode( 163 {useCount: 1}, 164 { 165 headers: this.pds.adminAuthHeaders(), 166 encoding: 'application/json', 167 }, 168 ) 169 170 const email = `fake${Object.keys(this.users).length + 1}@fake.com` 171 const res = await agent.createAccount({ 172 inviteCode: inviteRes.data.code, 173 email, 174 handle: name + '.test', 175 password: 'hunter2', 176 }) 177 await agent.upsertProfile(async () => { 178 const blob = await agent.uploadBlob(this.pic, { 179 encoding: 'image/jpeg', 180 }) 181 return { 182 displayName: name, 183 avatar: blob.data.blob, 184 } 185 }) 186 this.users[name] = { 187 did: res.data.did, 188 email, 189 handle: name + '.test', 190 password: 'hunter2', 191 agent: agent, 192 } 193 } 194 195 async follow(a: string, b: string) { 196 await this.users[a].agent.follow(this.users[b].did) 197 } 198 199 async generateStandardGraph() { 200 await this.createUser('alice') 201 await this.createUser('bob') 202 await this.createUser('carla') 203 204 await this.users.alice.agent.upsertProfile(() => ({ 205 displayName: 'Alice', 206 description: 'Test user 1', 207 })) 208 209 await this.users.bob.agent.upsertProfile(() => ({ 210 displayName: 'Bob', 211 description: 'Test user 2', 212 })) 213 214 await this.users.carla.agent.upsertProfile(() => ({ 215 displayName: 'Carla', 216 description: 'Test user 3', 217 })) 218 219 await this.follow('alice', 'bob') 220 await this.follow('alice', 'carla') 221 await this.follow('bob', 'alice') 222 await this.follow('bob', 'carla') 223 await this.follow('carla', 'alice') 224 await this.follow('carla', 'bob') 225 } 226 227 async createPost(user: string, text: string) { 228 const agent = this.users[user]?.agent 229 if (!agent) { 230 throw new Error(`Not a user: ${user}`) 231 } 232 return await agent.post({ 233 text, 234 langs: ['en'], 235 createdAt: new Date().toISOString(), 236 }) 237 } 238 239 async createImagePost(user: string, text: string) { 240 const agent = this.users[user]?.agent 241 if (!agent) { 242 throw new Error(`Not a user: ${user}`) 243 } 244 const blob = await agent.uploadBlob(this.pic, { 245 encoding: 'image/jpeg', 246 }) 247 return await agent.post({ 248 text, 249 langs: ['en'], 250 embed: { 251 $type: 'app.bsky.embed.images', 252 images: [{image: blob.data.blob, alt: ''}], 253 }, 254 createdAt: new Date().toISOString(), 255 }) 256 } 257 258 async createQuotePost( 259 user: string, 260 text: string, 261 {uri, cid}: {uri: string; cid: string}, 262 ) { 263 const agent = this.users[user]?.agent 264 if (!agent) { 265 throw new Error(`Not a user: ${user}`) 266 } 267 return await agent.post({ 268 text, 269 embed: {$type: 'app.bsky.embed.record', record: {uri, cid}}, 270 langs: ['en'], 271 createdAt: new Date().toISOString(), 272 }) 273 } 274 275 async createReply( 276 user: string, 277 text: string, 278 {uri, cid}: {uri: string; cid: string}, 279 ) { 280 const agent = this.users[user]?.agent 281 if (!agent) { 282 throw new Error(`Not a user: ${user}`) 283 } 284 return await agent.post({ 285 text, 286 reply: {root: {uri, cid}, parent: {uri, cid}}, 287 langs: ['en'], 288 createdAt: new Date().toISOString(), 289 }) 290 } 291 292 async like(user: string, {uri, cid}: {uri: string; cid: string}) { 293 const agent = this.users[user]?.agent 294 if (!agent) { 295 throw new Error(`Not a user: ${user}`) 296 } 297 return await agent.like(uri, cid) 298 } 299 300 async createFeed(user: string, rkey: string, posts: string[]) { 301 const agent = this.users[user]?.agent 302 if (!agent) { 303 throw new Error(`Not a user: ${user}`) 304 } 305 const fgUri = AtUri.make( 306 this.users[user].did, 307 'app.bsky.feed.generator', 308 rkey, 309 ) 310 const fg1 = await this.testNet.createFeedGen({ 311 [fgUri.toString()]: async () => { 312 return { 313 encoding: 'application/json', 314 body: { 315 feed: posts.slice(0, 30).map(uri => ({post: uri})), 316 }, 317 } 318 }, 319 }) 320 const avatarRes = await agent.api.com.atproto.repo.uploadBlob(this.pic, { 321 encoding: 'image/png', 322 }) 323 return await agent.api.app.bsky.feed.generator.create( 324 {repo: this.users[user].did, rkey}, 325 { 326 did: fg1.did, 327 displayName: rkey, 328 description: 'all my fav stuff', 329 avatar: avatarRes.data.blob, 330 createdAt: new Date().toISOString(), 331 }, 332 ) 333 } 334 335 async createInvite(forAccount: string) { 336 const agent = new BskyAgent({service: this.service}) 337 await agent.api.com.atproto.server.createInviteCode( 338 {useCount: 1, forAccount}, 339 { 340 headers: this.pds.adminAuthHeaders(), 341 encoding: 'application/json', 342 }, 343 ) 344 } 345 346 async labelAccount(label: string, user: string) { 347 const did = this.users[user]?.did 348 if (!did) { 349 throw new Error(`Invalid user: ${user}`) 350 } 351 const ctx = this.bsky.ctx 352 if (!ctx) { 353 throw new Error('Invalid appview') 354 } 355 await createLabel(this.bsky, { 356 uri: did, 357 cid: '', 358 val: label, 359 }) 360 } 361 362 async labelProfile(label: string, user: string) { 363 const agent = this.users[user]?.agent 364 const did = this.users[user]?.did 365 if (!did) { 366 throw new Error(`Invalid user: ${user}`) 367 } 368 369 const profile = await agent.app.bsky.actor.profile.get({ 370 repo: user + '.test', 371 rkey: 'self', 372 }) 373 374 const ctx = this.bsky.ctx 375 if (!ctx) { 376 throw new Error('Invalid appview') 377 } 378 await createLabel(this.bsky, { 379 uri: profile.uri, 380 cid: profile.cid, 381 val: label, 382 }) 383 } 384 385 async labelPost(label: string, {uri, cid}: {uri: string; cid: string}) { 386 const ctx = this.bsky.ctx 387 if (!ctx) { 388 throw new Error('Invalid appview') 389 } 390 await createLabel(this.bsky, { 391 uri, 392 cid, 393 val: label, 394 }) 395 } 396 397 async createMuteList(user: string, name: string): Promise<string> { 398 const res = await this.users[user]?.agent.app.bsky.graph.list.create( 399 {repo: this.users[user]?.did}, 400 { 401 purpose: 'app.bsky.graph.defs#modlist', 402 name, 403 createdAt: new Date().toISOString(), 404 }, 405 ) 406 await this.users[user]?.agent.app.bsky.graph.muteActorList({ 407 list: res.uri, 408 }) 409 return res.uri 410 } 411 412 async addToMuteList(owner: string, list: string, subject: string) { 413 await this.users[owner]?.agent.app.bsky.graph.listitem.create( 414 {repo: this.users[owner]?.did}, 415 { 416 list, 417 subject, 418 createdAt: new Date().toISOString(), 419 }, 420 ) 421 } 422} 423 424const checkAvailablePort = (port: number) => 425 new Promise(resolve => { 426 const server = net.createServer() 427 server.unref() 428 server.on('error', () => resolve(false)) 429 server.listen({port}, () => { 430 server.close(() => { 431 resolve(true) 432 }) 433 }) 434 }) 435 436async function getPort(start = 3000) { 437 for (let i = start; i < 65000; i++) { 438 if (await checkAvailablePort(i)) { 439 return i 440 } 441 } 442 throw new Error('Unable to find an available port') 443} 444 445const createLabel = async ( 446 bsky: TestBsky, 447 opts: {uri: string; cid: string; val: string}, 448) => { 449 await bsky.db.db 450 .insertInto('label') 451 .values({ 452 uri: opts.uri, 453 cid: opts.cid, 454 val: opts.val, 455 cts: new Date().toISOString(), 456 neg: false, 457 src: 'did:example:labeler', 458 }) 459 .execute() 460}