mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at rm-opacity 290 lines 7.5 kB view raw
1import { 2 AppBskyEmbedImages, 3 AppBskyEmbedExternal, 4 AppBskyEmbedRecord, 5 AppBskyEmbedRecordWithMedia, 6 AppBskyRichtextFacet, 7 ComAtprotoLabelDefs, 8 ComAtprotoRepoUploadBlob, 9 RichText, 10} from '@atproto/api' 11import {AtUri} from '@atproto/api' 12import {RootStoreModel} from 'state/models/root-store' 13import {isNetworkError} from 'lib/strings/errors' 14import {LinkMeta} from '../link-meta/link-meta' 15import {isWeb} from 'platform/detection' 16import {ImageModel} from 'state/models/media/image' 17import {shortenLinks} from 'lib/strings/rich-text-manip' 18 19export interface ExternalEmbedDraft { 20 uri: string 21 isLoading: boolean 22 meta?: LinkMeta 23 embed?: AppBskyEmbedRecord.Main 24 localThumb?: ImageModel 25} 26 27export async function resolveName(store: RootStoreModel, didOrHandle: string) { 28 if (!didOrHandle) { 29 throw new Error('Invalid handle: ""') 30 } 31 if (didOrHandle.startsWith('did:')) { 32 return didOrHandle 33 } 34 35 // we run the resolution always to ensure freshness 36 const promise = store.agent 37 .resolveHandle({ 38 handle: didOrHandle, 39 }) 40 .then(res => { 41 store.handleResolutions.cache.set(didOrHandle, res.data.did) 42 return res.data.did 43 }) 44 45 // but we can return immediately if it's cached 46 const cached = store.handleResolutions.cache.get(didOrHandle) 47 if (cached) { 48 return cached 49 } 50 51 return promise 52} 53 54export async function uploadBlob( 55 store: RootStoreModel, 56 blob: string, 57 encoding: string, 58): Promise<ComAtprotoRepoUploadBlob.Response> { 59 if (isWeb) { 60 // `blob` should be a data uri 61 return store.agent.uploadBlob(convertDataURIToUint8Array(blob), { 62 encoding, 63 }) 64 } else { 65 // `blob` should be a path to a file in the local FS 66 return store.agent.uploadBlob( 67 blob, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts 68 {encoding}, 69 ) 70 } 71} 72 73interface PostOpts { 74 rawText: string 75 replyTo?: string 76 quote?: { 77 uri: string 78 cid: string 79 } 80 extLink?: ExternalEmbedDraft 81 images?: ImageModel[] 82 labels?: string[] 83 knownHandles?: Set<string> 84 onStateChange?: (state: string) => void 85 langs?: string[] 86} 87 88export async function post(store: RootStoreModel, opts: PostOpts) { 89 let embed: 90 | AppBskyEmbedImages.Main 91 | AppBskyEmbedExternal.Main 92 | AppBskyEmbedRecord.Main 93 | AppBskyEmbedRecordWithMedia.Main 94 | undefined 95 let reply 96 let rt = new RichText( 97 {text: opts.rawText.trimEnd()}, 98 { 99 cleanNewlines: true, 100 }, 101 ) 102 103 opts.onStateChange?.('Processing...') 104 await rt.detectFacets(store.agent) 105 rt = shortenLinks(rt) 106 107 // filter out any mention facets that didn't map to a user 108 rt.facets = rt.facets?.filter(facet => { 109 const mention = facet.features.find(feature => 110 AppBskyRichtextFacet.isMention(feature), 111 ) 112 if (mention && !mention.did) { 113 return false 114 } 115 return true 116 }) 117 118 // add quote embed if present 119 if (opts.quote) { 120 embed = { 121 $type: 'app.bsky.embed.record', 122 record: { 123 uri: opts.quote.uri, 124 cid: opts.quote.cid, 125 }, 126 } as AppBskyEmbedRecord.Main 127 } 128 129 // add image embed if present 130 if (opts.images?.length) { 131 const images: AppBskyEmbedImages.Image[] = [] 132 for (const image of opts.images) { 133 opts.onStateChange?.(`Uploading image #${images.length + 1}...`) 134 await image.compress() 135 const path = image.compressed?.path ?? image.path 136 const {width, height} = image.compressed || image 137 const res = await uploadBlob(store, path, 'image/jpeg') 138 images.push({ 139 image: res.data.blob, 140 alt: image.altText ?? '', 141 aspectRatio: {width, height}, 142 }) 143 } 144 145 if (opts.quote) { 146 embed = { 147 $type: 'app.bsky.embed.recordWithMedia', 148 record: embed, 149 media: { 150 $type: 'app.bsky.embed.images', 151 images, 152 }, 153 } as AppBskyEmbedRecordWithMedia.Main 154 } else { 155 embed = { 156 $type: 'app.bsky.embed.images', 157 images, 158 } as AppBskyEmbedImages.Main 159 } 160 } 161 162 // add external embed if present 163 if (opts.extLink && !opts.images?.length) { 164 if (opts.extLink.embed) { 165 embed = opts.extLink.embed 166 } else { 167 let thumb 168 if (opts.extLink.localThumb) { 169 opts.onStateChange?.('Uploading link thumbnail...') 170 let encoding 171 if (opts.extLink.localThumb.mime) { 172 encoding = opts.extLink.localThumb.mime 173 } else if (opts.extLink.localThumb.path.endsWith('.png')) { 174 encoding = 'image/png' 175 } else if ( 176 opts.extLink.localThumb.path.endsWith('.jpeg') || 177 opts.extLink.localThumb.path.endsWith('.jpg') 178 ) { 179 encoding = 'image/jpeg' 180 } else { 181 store.log.warn( 182 'Unexpected image format for thumbnail, skipping', 183 opts.extLink.localThumb.path, 184 ) 185 } 186 if (encoding) { 187 const thumbUploadRes = await uploadBlob( 188 store, 189 opts.extLink.localThumb.path, 190 encoding, 191 ) 192 thumb = thumbUploadRes.data.blob 193 } 194 } 195 196 if (opts.quote) { 197 embed = { 198 $type: 'app.bsky.embed.recordWithMedia', 199 record: embed, 200 media: { 201 $type: 'app.bsky.embed.external', 202 external: { 203 uri: opts.extLink.uri, 204 title: opts.extLink.meta?.title || '', 205 description: opts.extLink.meta?.description || '', 206 thumb, 207 }, 208 } as AppBskyEmbedExternal.Main, 209 } as AppBskyEmbedRecordWithMedia.Main 210 } else { 211 embed = { 212 $type: 'app.bsky.embed.external', 213 external: { 214 uri: opts.extLink.uri, 215 title: opts.extLink.meta?.title || '', 216 description: opts.extLink.meta?.description || '', 217 thumb, 218 }, 219 } as AppBskyEmbedExternal.Main 220 } 221 } 222 } 223 224 // add replyTo if post is a reply to another post 225 if (opts.replyTo) { 226 const replyToUrip = new AtUri(opts.replyTo) 227 const parentPost = await store.agent.getPost({ 228 repo: replyToUrip.host, 229 rkey: replyToUrip.rkey, 230 }) 231 if (parentPost) { 232 const parentRef = { 233 uri: parentPost.uri, 234 cid: parentPost.cid, 235 } 236 reply = { 237 root: parentPost.value.reply?.root || parentRef, 238 parent: parentRef, 239 } 240 } 241 } 242 243 // set labels 244 let labels: ComAtprotoLabelDefs.SelfLabels | undefined 245 if (opts.labels?.length) { 246 labels = { 247 $type: 'com.atproto.label.defs#selfLabels', 248 values: opts.labels.map(val => ({val})), 249 } 250 } 251 252 // add top 3 languages from user preferences if langs is provided 253 let langs = opts.langs 254 if (opts.langs) { 255 langs = opts.langs.slice(0, 3) 256 } 257 258 try { 259 opts.onStateChange?.('Posting...') 260 return await store.agent.post({ 261 text: rt.text, 262 facets: rt.facets, 263 reply, 264 embed, 265 langs, 266 labels, 267 }) 268 } catch (e: any) { 269 console.error(`Failed to create post: ${e.toString()}`) 270 if (isNetworkError(e)) { 271 throw new Error( 272 'Post failed to upload. Please check your Internet connection and try again.', 273 ) 274 } else { 275 throw e 276 } 277 } 278} 279 280// helpers 281// = 282 283function convertDataURIToUint8Array(uri: string): Uint8Array { 284 var raw = window.atob(uri.substring(uri.indexOf(';base64,') + 8)) 285 var binary = new Uint8Array(new ArrayBuffer(raw.length)) 286 for (let i = 0; i < raw.length; i++) { 287 binary[i] = raw.charCodeAt(i) 288 } 289 return binary 290}