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