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