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