Thread viewer for Bluesky
at svelte 135 lines 3.7 kB view raw
1import { BlueskyAPI, type TimelineFetchOptions } from "./bluesky_api"; 2import { AuthError } from './minisky.js'; 3import { Post } from '../models/posts.js'; 4import { atURI, feedPostTime } from '../utils.js'; 5 6/** 7 * Stores user's access tokens and data in local storage after they log in. 8 */ 9 10class LocalStorageConfig { 11 user: json; 12 13 constructor() { 14 let data = localStorage.getItem('userData'); 15 this.user = data ? JSON.parse(data) : {}; 16 } 17 18 save() { 19 if (this.user) { 20 localStorage.setItem('userData', JSON.stringify(this.user)); 21 } else { 22 localStorage.removeItem('userData'); 23 } 24 } 25} 26 27export class AuthenticatedAPI extends BlueskyAPI { 28 override user: json; 29 30 constructor() { 31 let config = new LocalStorageConfig(); 32 let pds: string | null = config.user.pdsEndpoint || null; 33 super(pds, config); 34 this.user = config.user; 35 } 36 37 async getCurrentUserAvatar(): Promise<json | undefined> { 38 let json = await this.getRequest('com.atproto.repo.getRecord', { 39 repo: this.user.did, 40 collection: 'app.bsky.actor.profile', 41 rkey: 'self' 42 }); 43 44 return json.value.avatar; 45 } 46 47 async loadCurrentUserAvatar(): Promise<string | null> { 48 if (!this.config || !this.config.user) { 49 throw new AuthError("User isn't logged in"); 50 } 51 52 let avatar = await this.getCurrentUserAvatar(); 53 54 if (avatar) { 55 let url = `https://cdn.bsky.app/img/avatar/plain/${this.user.did}/${avatar.ref.$link}@jpeg`; 56 this.config.user.avatar = url; 57 this.config.save(); 58 return url; 59 } else { 60 return null; 61 } 62 } 63 64 async loadNotifications(params?: json): Promise<json> { 65 return await this.getRequest('app.bsky.notification.listNotifications', params || {}); 66 } 67 68 async loadMentions(cursor?: string): Promise<{ cursor: string | undefined, posts: json[] }> { 69 let response = await this.loadNotifications({ cursor: cursor ?? '', limit: 100, reasons: ['reply', 'mention'] }); 70 let uris = response.notifications.map((x: any) => x.uri); 71 let batches: Promise<json[]>[] = []; 72 73 for (let i = 0; i < uris.length; i += 25) { 74 let batch = this.loadPosts(uris.slice(i, i + 25)); 75 batches.push(batch); 76 } 77 78 let postGroups = await Promise.all(batches); 79 80 return { cursor: response.cursor, posts: postGroups.flat() }; 81 } 82 83 async loadHomeTimeline(days: number, options: TimelineFetchOptions = {}): Promise<json[]> { 84 let now = new Date(); 85 let timeLimit = now.getTime() - days * 86400 * 1000; 86 87 return await this.fetchAll('app.bsky.feed.getTimeline', { 88 params: { limit: 100 }, 89 field: 'feed', 90 breakWhen: (x: json) => feedPostTime(x) < timeLimit, 91 ...options 92 }); 93 } 94 95 async loadUserLists(): Promise<json[]> { 96 let lists = await this.fetchAll('app.bsky.graph.getLists', { 97 params: { 98 actor: this.user.did, 99 limit: 100 100 }, 101 field: 'lists' 102 }); 103 104 return lists.filter((x: json) => x.purpose == 'app.bsky.graph.defs#curatelist'); 105 } 106 107 async likePost(post: Post): Promise<json> { 108 return await this.postRequest('com.atproto.repo.createRecord', { 109 repo: this.user.did, 110 collection: 'app.bsky.feed.like', 111 record: { 112 subject: { 113 uri: post.uri, 114 cid: post.cid 115 }, 116 createdAt: new Date().toISOString() 117 } 118 }); 119 } 120 121 async removeLike(uri: string) { 122 let { rkey } = atURI(uri); 123 124 await this.postRequest('com.atproto.repo.deleteRecord', { 125 repo: this.user.did, 126 collection: 'app.bsky.feed.like', 127 rkey: rkey 128 }); 129 } 130 131 override resetTokens() { 132 delete this.user.avatar; 133 super.resetTokens(); 134 } 135}