A work-in-progress chat bot for Streamplace with chat overlay functionality
2
fork

Configure Feed

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

R.I.P. @pronouns.diy

+39 -154
+1 -1
islands/ChatMessage.tsx
··· 112 112 {data.author.pronouns && data.author.pronouns.length > 0 && 113 113 ( 114 114 <span class="chat-message-pronouns"> 115 - {data.author.pronouns.join(" ")} 115 + {data.author.pronouns} 116 116 </span> 117 117 )} 118 118 </div>
+5 -21
utils/didResolver.ts
··· 1 - import { 2 - getDidDocument, 3 - getHandle, 4 - getPDS, 5 - getRecord, 6 - } from "./atcuteUtils.ts"; 7 - import { fetchPronouns } from "./labelUtils.ts"; 1 + import { getDidDocument, getHandle, getPDS, getRecord } from "./atcuteUtils.ts"; 8 2 declare type ActorProfile = import("@atcute/bluesky").AppBskyActorProfile.Main; 9 3 10 4 class DidResolver { ··· 47 41 throw new Error(`Error fetching PDS host or handle for ${did}`); 48 42 } 49 43 50 - const [actorProfileResult, chatProfileResult, pronounLabelsResult] = 51 - await Promise.allSettled([ 44 + const [actorProfileResult, chatProfileResult] = await Promise 45 + .allSettled([ 52 46 getRecord(pdsHostUrl, { 53 47 repo: did, 54 48 collection: "app.bsky.actor.profile", ··· 59 53 collection: "place.stream.chat.profile", 60 54 rkey: "self", 61 55 }), 62 - fetchPronouns([did]), 63 56 ]); 64 57 65 58 // Extract values from results, handling failures gracefully ··· 69 62 const chatProfile = chatProfileResult.status === "fulfilled" 70 63 ? chatProfileResult.value 71 64 : null; 72 - const pronounLabels = pronounLabelsResult.status === "fulfilled" 73 - ? pronounLabelsResult.value.get(did) 74 - : []; 75 65 76 66 const userProfile: UserProfile = { 77 67 handle: handle as Handle, 78 68 pdsEndpoint: pdsHostUrl, 79 69 website: undefined, 80 - pronouns: [], 70 + pronouns: undefined, 81 71 description: undefined, 82 72 displayName: undefined, 83 73 color: undefined, ··· 113 103 ? `${pdsHostUrl}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${banner.ref.$link}` 114 104 : undefined; 115 105 if (actor.pronouns) { 116 - userProfile.pronouns = [ 117 - actor.pronouns, 118 - ]; 119 - } else if (pronounLabels && pronounLabels.length > 0) { 120 - userProfile.pronouns = pronounLabels; 121 - } else { 122 - userProfile.pronouns = []; 106 + userProfile.pronouns = actor.pronouns; 123 107 } 124 108 } 125 109 if (chatProfile?.value?.color) {
+33 -33
utils/globals.d.ts
··· 41 41 42 42 // Raw chat message as received from Jetstream WebSocket 43 43 interface JetstreamMessage { 44 - did: Did; 45 - time_us: number; 46 - kind: "commit"; 47 - commit: { 48 - rev: string; 49 - operation: "create" | "delete" | "update"; 50 - collection: string; 51 - rkey: string; 52 - record?: { 53 - $type: "place.stream.chat.message"; 54 - createdAt: string; 55 - streamer: Did; 56 - text: string; 57 - facets?: Array<{ 58 - features: unknown[]; 59 - index: { 60 - byteStart: number; 61 - byteEnd: number; 62 - }; 63 - }>; 64 - reply?: { 65 - root: unknown; 66 - parent: unknown; 67 - }; 68 - }; 69 - cid: string; 70 - }; 44 + did: Did; 45 + time_us: number; 46 + kind: "commit"; 47 + commit: { 48 + rev: string; 49 + operation: "create" | "delete" | "update"; 50 + collection: string; 51 + rkey: string; 52 + record?: { 53 + $type: "place.stream.chat.message"; 54 + createdAt: string; 55 + streamer: Did; 56 + text: string; 57 + facets?: Array<{ 58 + features: unknown[]; 59 + index: { 60 + byteStart: number; 61 + byteEnd: number; 62 + }; 63 + }>; 64 + reply?: { 65 + root: unknown; 66 + parent: unknown; 67 + }; 68 + }; 69 + cid: string; 70 + }; 71 71 } 72 72 73 73 // Handle + app.bsky.actor.profile + place.stream.chat.profile ··· 80 80 green: number; 81 81 blue: number; 82 82 }; 83 - avatarUrl?: string, 84 - bannerUrl?: string, 85 - website?: string, 86 - pronouns?: string[], 87 - description?: string, 88 - displayName?: string, 83 + avatarUrl?: string; 84 + bannerUrl?: string; 85 + website?: string; 86 + pronouns?: string; 87 + description?: string; 88 + displayName?: string; 89 89 } 90 90 91 91 // User chat role
-99
utils/labelUtils.ts
··· 83 83 84 84 return results; 85 85 } 86 - 87 - // Pronoun-specific fetching 88 - const PRONOUN_API_BASE = "https://api.pronouns.diy/xrpc"; 89 - const PRONOUN_DEFINITIONS_ENDPOINT = 90 - "https://pds.juli.ee/xrpc/com.atproto.repo.getRecord?repo=did:plc:wkoofae5uytcm7bjncmev6n6&collection=app.bsky.labeler.service&rkey=self"; 91 - 92 - async function loadPronounDefinitions(): Promise< 93 - Map<string, LabelDefinition> 94 - > { 95 - const definitions = new Map<string, LabelDefinition>(); 96 - 97 - try { 98 - const response = await fetch(PRONOUN_DEFINITIONS_ENDPOINT); 99 - if (!response.ok) { 100 - console.error("Failed to load pronoun definitions"); 101 - return definitions; 102 - } 103 - 104 - const data = await response.json() as { 105 - value: LabelServiceDefinition; 106 - }; 107 - 108 - for (const def of data.value.policies.labelValueDefinitions) { 109 - definitions.set(def.identifier, def); 110 - } 111 - } catch (error) { 112 - console.error("Error loading pronoun definitions:", error); 113 - } 114 - 115 - return definitions; 116 - } 117 - 118 - export async function fetchPronouns( 119 - dids: Did[], 120 - ): Promise<Map<Did, string[]>> { 121 - const results = new Map<Did, string[]>(); 122 - 123 - if (dids.length === 0) { 124 - return results; 125 - } 126 - 127 - try { 128 - // Load definitions and labels in parallel 129 - const [definitions, labelsResponse] = await Promise.all([ 130 - loadPronounDefinitions(), 131 - fetch( 132 - `${PRONOUN_API_BASE}/com.atproto.label.queryLabels?uriPatterns=${ 133 - encodeURIComponent(dids.join(",")) 134 - }`, 135 - ), 136 - ]); 137 - 138 - if (!labelsResponse.ok) { 139 - console.warn("Failed to fetch pronouns"); 140 - return results; 141 - } 142 - 143 - const data = await labelsResponse.json() as LabelApiResponse; 144 - 145 - // Group labels by DID 146 - const labelsByDid = new Map<string, Label[]>(); 147 - for (const label of data.labels) { 148 - if (!labelsByDid.has(label.uri)) { 149 - labelsByDid.set(label.uri, []); 150 - } 151 - labelsByDid.get(label.uri)!.push(label); 152 - } 153 - 154 - // Convert labels to pronoun strings 155 - for (const did of dids) { 156 - const labels = labelsByDid.get(did) || []; 157 - const pronouns: string[] = []; 158 - 159 - for (const label of labels) { 160 - if (label.neg) continue; // Skip negative labels 161 - 162 - const definition = definitions.get(label.val); 163 - if (!definition) { 164 - // Fallback for unknown labels 165 - pronouns.push(label.val); 166 - continue; 167 - } 168 - 169 - const englishLocale = definition.locales.find((l) => 170 - l.lang === "en" 171 - ); 172 - if (englishLocale) { 173 - pronouns.push(englishLocale.name); 174 - } 175 - } 176 - 177 - results.set(did, pronouns); 178 - } 179 - } catch (error) { 180 - console.error("Error fetching pronouns:", error); 181 - } 182 - 183 - return results; 184 - }