mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at verify-intent 567 lines 16 kB view raw
1import {Dimensions} from 'react-native' 2 3import {isSafari} from '#/lib/browser' 4import {isWeb} from '#/platform/detection' 5 6const {height: SCREEN_HEIGHT} = Dimensions.get('window') 7 8const IFRAME_HOST = isWeb 9 ? // @ts-ignore only for web 10 window.location.host === 'localhost:8100' 11 ? 'http://localhost:8100' 12 : 'https://bsky.app' 13 : __DEV__ && !process.env.JEST_WORKER_ID 14 ? 'http://localhost:8100' 15 : 'https://bsky.app' 16 17export const embedPlayerSources = [ 18 'youtube', 19 'youtubeShorts', 20 'twitch', 21 'spotify', 22 'soundcloud', 23 'appleMusic', 24 'vimeo', 25 'giphy', 26 'tenor', 27 'flickr', 28] as const 29 30export type EmbedPlayerSource = (typeof embedPlayerSources)[number] 31 32export type EmbedPlayerType = 33 | 'youtube_video' 34 | 'youtube_short' 35 | 'twitch_video' 36 | 'spotify_album' 37 | 'spotify_playlist' 38 | 'spotify_song' 39 | 'soundcloud_track' 40 | 'soundcloud_set' 41 | 'apple_music_playlist' 42 | 'apple_music_album' 43 | 'apple_music_song' 44 | 'vimeo_video' 45 | 'giphy_gif' 46 | 'tenor_gif' 47 | 'flickr_album' 48 49export const externalEmbedLabels: Record<EmbedPlayerSource, string> = { 50 youtube: 'YouTube', 51 youtubeShorts: 'YouTube Shorts', 52 vimeo: 'Vimeo', 53 twitch: 'Twitch', 54 giphy: 'GIPHY', 55 tenor: 'Tenor', 56 spotify: 'Spotify', 57 appleMusic: 'Apple Music', 58 soundcloud: 'SoundCloud', 59 flickr: 'Flickr', 60} 61 62export interface EmbedPlayerParams { 63 type: EmbedPlayerType 64 playerUri: string 65 isGif?: boolean 66 source: EmbedPlayerSource 67 metaUri?: string 68 hideDetails?: boolean 69 dimensions?: { 70 height: number 71 width: number 72 } 73} 74 75const giphyRegex = /media(?:[0-4]\.giphy\.com|\.giphy\.com)/i 76const gifFilenameRegex = /^(\S+)\.(webp|gif|mp4)$/i 77 78export function parseEmbedPlayerFromUrl( 79 url: string, 80): EmbedPlayerParams | undefined { 81 let urlp 82 try { 83 urlp = new URL(url) 84 } catch (e) { 85 return undefined 86 } 87 88 // youtube 89 if (urlp.hostname === 'youtu.be') { 90 const videoId = urlp.pathname.split('/')[1] 91 const seek = encodeURIComponent(urlp.searchParams.get('t') ?? 0) 92 if (videoId) { 93 return { 94 type: 'youtube_video', 95 source: 'youtube', 96 playerUri: `${IFRAME_HOST}/iframe/youtube.html?videoId=${videoId}&start=${seek}`, 97 } 98 } 99 } 100 if ( 101 urlp.hostname === 'www.youtube.com' || 102 urlp.hostname === 'youtube.com' || 103 urlp.hostname === 'm.youtube.com' || 104 urlp.hostname === 'music.youtube.com' 105 ) { 106 const [_, page, shortOrLiveVideoId] = urlp.pathname.split('/') 107 108 const isShorts = page === 'shorts' 109 const isLive = page === 'live' 110 const videoId = 111 isShorts || isLive 112 ? shortOrLiveVideoId 113 : (urlp.searchParams.get('v') as string) 114 const seek = encodeURIComponent(urlp.searchParams.get('t') ?? 0) 115 116 if (videoId) { 117 return { 118 type: isShorts ? 'youtube_short' : 'youtube_video', 119 source: isShorts ? 'youtubeShorts' : 'youtube', 120 hideDetails: isShorts ? true : undefined, 121 playerUri: `${IFRAME_HOST}/iframe/youtube.html?videoId=${videoId}&start=${seek}`, 122 } 123 } 124 } 125 126 // twitch 127 if ( 128 urlp.hostname === 'twitch.tv' || 129 urlp.hostname === 'www.twitch.tv' || 130 urlp.hostname === 'm.twitch.tv' 131 ) { 132 const parent = isWeb 133 ? // @ts-ignore only for web 134 window.location.hostname 135 : 'localhost' 136 137 const [_, channelOrVideo, clipOrId, id] = urlp.pathname.split('/') 138 139 if (channelOrVideo === 'videos') { 140 return { 141 type: 'twitch_video', 142 source: 'twitch', 143 playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&video=${clipOrId}&parent=${parent}`, 144 } 145 } else if (clipOrId === 'clip') { 146 return { 147 type: 'twitch_video', 148 source: 'twitch', 149 playerUri: `https://clips.twitch.tv/embed?volume=0.5&autoplay=true&clip=${id}&parent=${parent}`, 150 } 151 } else if (channelOrVideo) { 152 return { 153 type: 'twitch_video', 154 source: 'twitch', 155 playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=${channelOrVideo}&parent=${parent}`, 156 } 157 } 158 } 159 160 // spotify 161 if (urlp.hostname === 'open.spotify.com') { 162 const [_, typeOrLocale, idOrType, id] = urlp.pathname.split('/') 163 164 if (idOrType) { 165 if (typeOrLocale === 'playlist' || idOrType === 'playlist') { 166 return { 167 type: 'spotify_playlist', 168 source: 'spotify', 169 playerUri: `https://open.spotify.com/embed/playlist/${ 170 id ?? idOrType 171 }`, 172 } 173 } 174 if (typeOrLocale === 'album' || idOrType === 'album') { 175 return { 176 type: 'spotify_album', 177 source: 'spotify', 178 playerUri: `https://open.spotify.com/embed/album/${id ?? idOrType}`, 179 } 180 } 181 if (typeOrLocale === 'track' || idOrType === 'track') { 182 return { 183 type: 'spotify_song', 184 source: 'spotify', 185 playerUri: `https://open.spotify.com/embed/track/${id ?? idOrType}`, 186 } 187 } 188 if (typeOrLocale === 'episode' || idOrType === 'episode') { 189 return { 190 type: 'spotify_song', 191 source: 'spotify', 192 playerUri: `https://open.spotify.com/embed/episode/${id ?? idOrType}`, 193 } 194 } 195 if (typeOrLocale === 'show' || idOrType === 'show') { 196 return { 197 type: 'spotify_song', 198 source: 'spotify', 199 playerUri: `https://open.spotify.com/embed/show/${id ?? idOrType}`, 200 } 201 } 202 } 203 } 204 205 // soundcloud 206 if ( 207 urlp.hostname === 'soundcloud.com' || 208 urlp.hostname === 'www.soundcloud.com' 209 ) { 210 const [_, user, trackOrSets, set] = urlp.pathname.split('/') 211 212 if (user && trackOrSets) { 213 if (trackOrSets === 'sets' && set) { 214 return { 215 type: 'soundcloud_set', 216 source: 'soundcloud', 217 playerUri: `https://w.soundcloud.com/player/?url=${url}&auto_play=true&visual=false&hide_related=true`, 218 } 219 } 220 221 return { 222 type: 'soundcloud_track', 223 source: 'soundcloud', 224 playerUri: `https://w.soundcloud.com/player/?url=${url}&auto_play=true&visual=false&hide_related=true`, 225 } 226 } 227 } 228 229 if ( 230 urlp.hostname === 'music.apple.com' || 231 urlp.hostname === 'music.apple.com' 232 ) { 233 // This should always have: locale, type (playlist or album), name, and id. We won't use spread since we want 234 // to check if the length is correct 235 const pathParams = urlp.pathname.split('/') 236 const type = pathParams[2] 237 const songId = urlp.searchParams.get('i') 238 239 if (pathParams.length === 5 && (type === 'playlist' || type === 'album')) { 240 // We want to append the songId to the end of the url if it exists 241 const embedUri = `https://embed.music.apple.com${urlp.pathname}${ 242 urlp.search ? '?i=' + songId : '' 243 }` 244 245 if (type === 'playlist') { 246 return { 247 type: 'apple_music_playlist', 248 source: 'appleMusic', 249 playerUri: embedUri, 250 } 251 } else if (type === 'album') { 252 if (songId) { 253 return { 254 type: 'apple_music_song', 255 source: 'appleMusic', 256 playerUri: embedUri, 257 } 258 } else { 259 return { 260 type: 'apple_music_album', 261 source: 'appleMusic', 262 playerUri: embedUri, 263 } 264 } 265 } 266 } 267 } 268 269 if (urlp.hostname === 'vimeo.com' || urlp.hostname === 'www.vimeo.com') { 270 const [_, videoId] = urlp.pathname.split('/') 271 if (videoId) { 272 return { 273 type: 'vimeo_video', 274 source: 'vimeo', 275 playerUri: `https://player.vimeo.com/video/${videoId}?autoplay=1`, 276 } 277 } 278 } 279 280 if (urlp.hostname === 'giphy.com' || urlp.hostname === 'www.giphy.com') { 281 const [_, gifs, nameAndId] = urlp.pathname.split('/') 282 283 /* 284 * nameAndId is a string that consists of the name (dash separated) and the id of the gif (the last part of the name) 285 * We want to get the id of the gif, then direct to media.giphy.com/media/{id}/giphy.webp so we can 286 * use it in an <Image> component 287 */ 288 289 if (gifs === 'gifs' && nameAndId) { 290 const gifId = nameAndId.split('-').pop() 291 292 if (gifId) { 293 return { 294 type: 'giphy_gif', 295 source: 'giphy', 296 isGif: true, 297 hideDetails: true, 298 metaUri: `https://giphy.com/gifs/${gifId}`, 299 playerUri: `https://i.giphy.com/media/${gifId}/200.webp`, 300 } 301 } 302 } 303 } 304 305 // There are five possible hostnames that also can be giphy urls: media.giphy.com and media0-4.giphy.com 306 // These can include (presumably) a tracking id in the path name, so we have to check for that as well 307 if (giphyRegex.test(urlp.hostname)) { 308 // We can link directly to the gif, if its a proper link 309 const [_, media, trackingOrId, idOrFilename, filename] = 310 urlp.pathname.split('/') 311 312 if (media === 'media') { 313 if (idOrFilename && gifFilenameRegex.test(idOrFilename)) { 314 return { 315 type: 'giphy_gif', 316 source: 'giphy', 317 isGif: true, 318 hideDetails: true, 319 metaUri: `https://giphy.com/gifs/${trackingOrId}`, 320 playerUri: `https://i.giphy.com/media/${trackingOrId}/200.webp`, 321 } 322 } else if (filename && gifFilenameRegex.test(filename)) { 323 return { 324 type: 'giphy_gif', 325 source: 'giphy', 326 isGif: true, 327 hideDetails: true, 328 metaUri: `https://giphy.com/gifs/${idOrFilename}`, 329 playerUri: `https://i.giphy.com/media/${idOrFilename}/200.webp`, 330 } 331 } 332 } 333 } 334 335 // Finally, we should see if it is a link to i.giphy.com. These links don't necessarily end in .gif but can also 336 // be .webp 337 if (urlp.hostname === 'i.giphy.com' || urlp.hostname === 'www.i.giphy.com') { 338 const [_, mediaOrFilename, filename] = urlp.pathname.split('/') 339 340 if (mediaOrFilename === 'media' && filename) { 341 const gifId = filename.split('.')[0] 342 return { 343 type: 'giphy_gif', 344 source: 'giphy', 345 isGif: true, 346 hideDetails: true, 347 metaUri: `https://giphy.com/gifs/${gifId}`, 348 playerUri: `https://i.giphy.com/media/${gifId}/200.webp`, 349 } 350 } else if (mediaOrFilename) { 351 const gifId = mediaOrFilename.split('.')[0] 352 return { 353 type: 'giphy_gif', 354 source: 'giphy', 355 isGif: true, 356 hideDetails: true, 357 metaUri: `https://giphy.com/gifs/${gifId}`, 358 playerUri: `https://i.giphy.com/media/${ 359 mediaOrFilename.split('.')[0] 360 }/200.webp`, 361 } 362 } 363 } 364 365 const tenorGif = parseTenorGif(urlp) 366 if (tenorGif.success) { 367 const {playerUri, dimensions} = tenorGif 368 369 return { 370 type: 'tenor_gif', 371 source: 'tenor', 372 isGif: true, 373 hideDetails: true, 374 playerUri, 375 dimensions, 376 } 377 } 378 379 // this is a standard flickr path! we can use the embedder for albums and groups, so validate the path 380 if (urlp.hostname === 'www.flickr.com' || urlp.hostname === 'flickr.com') { 381 let i = urlp.pathname.length - 1 382 while (i > 0 && urlp.pathname.charAt(i) === '/') { 383 --i 384 } 385 386 const path_components = urlp.pathname.slice(1, i + 1).split('/') 387 if (path_components.length === 4) { 388 // discard username - it's not relevant 389 const [photos, _, albums, id] = path_components 390 if (photos === 'photos' && albums === 'albums') { 391 // this at least has the shape of a valid photo-album URL! 392 return { 393 type: 'flickr_album', 394 source: 'flickr', 395 playerUri: `https://embedr.flickr.com/photosets/${id}`, 396 } 397 } 398 } 399 400 if (path_components.length === 3) { 401 const [groups, id, pool] = path_components 402 if (groups === 'groups' && pool === 'pool') { 403 return { 404 type: 'flickr_album', 405 source: 'flickr', 406 playerUri: `https://embedr.flickr.com/groups/${id}`, 407 } 408 } 409 } 410 // not an album or a group pool, don't know what to do with this! 411 return undefined 412 } 413 414 // link shortened flickr path 415 if (urlp.hostname === 'flic.kr') { 416 const b58alph = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ' 417 let [_, type, idBase58Enc] = urlp.pathname.split('/') 418 let id = 0n 419 for (const char of idBase58Enc) { 420 const nextIdx = b58alph.indexOf(char) 421 if (nextIdx >= 0) { 422 id = id * 58n + BigInt(nextIdx) 423 } else { 424 // not b58 encoded, ergo not a valid link to embed 425 return undefined 426 } 427 } 428 429 switch (type) { 430 case 'go': 431 const formattedGroupId = `${id}` 432 return { 433 type: 'flickr_album', 434 source: 'flickr', 435 playerUri: `https://embedr.flickr.com/groups/${formattedGroupId.slice( 436 0, 437 -2, 438 )}@N${formattedGroupId.slice(-2)}`, 439 } 440 case 's': 441 return { 442 type: 'flickr_album', 443 source: 'flickr', 444 playerUri: `https://embedr.flickr.com/photosets/${id}`, 445 } 446 default: 447 // we don't know what this is so we can't embed it 448 return undefined 449 } 450 } 451} 452 453export function getPlayerAspect({ 454 type, 455 hasThumb, 456 width, 457}: { 458 type: EmbedPlayerParams['type'] 459 hasThumb: boolean 460 width: number 461}): {aspectRatio?: number; height?: number} { 462 if (!hasThumb) return {aspectRatio: 16 / 9} 463 464 switch (type) { 465 case 'youtube_video': 466 case 'twitch_video': 467 case 'vimeo_video': 468 return {aspectRatio: 16 / 9} 469 case 'youtube_short': 470 if (SCREEN_HEIGHT < 600) { 471 return {aspectRatio: (9 / 16) * 1.75} 472 } else { 473 return {aspectRatio: (9 / 16) * 1.5} 474 } 475 case 'spotify_album': 476 case 'apple_music_album': 477 case 'apple_music_playlist': 478 case 'spotify_playlist': 479 case 'soundcloud_set': 480 return {height: 380} 481 case 'spotify_song': 482 if (width <= 300) { 483 return {height: 155} 484 } 485 return {height: 232} 486 case 'soundcloud_track': 487 return {height: 165} 488 case 'apple_music_song': 489 return {height: 150} 490 default: 491 return {aspectRatio: 16 / 9} 492 } 493} 494 495export function getGifDims( 496 originalHeight: number, 497 originalWidth: number, 498 viewWidth: number, 499) { 500 const scaledHeight = (originalHeight / originalWidth) * viewWidth 501 502 return { 503 height: scaledHeight > 250 ? 250 : scaledHeight, 504 width: (250 / scaledHeight) * viewWidth, 505 } 506} 507 508export function getGiphyMetaUri(url: URL) { 509 if (giphyRegex.test(url.hostname) || url.hostname === 'i.giphy.com') { 510 const params = parseEmbedPlayerFromUrl(url.toString()) 511 if (params && params.type === 'giphy_gif') { 512 return params.metaUri 513 } 514 } 515} 516 517export function parseTenorGif(urlp: URL): 518 | {success: false} 519 | { 520 success: true 521 playerUri: string 522 dimensions: {height: number; width: number} 523 } { 524 if (urlp.hostname !== 'media.tenor.com') { 525 return {success: false} 526 } 527 528 let [_, id, filename] = urlp.pathname.split('/') 529 530 if (!id || !filename) { 531 return {success: false} 532 } 533 534 if (!id.includes('AAAAC')) { 535 return {success: false} 536 } 537 538 const h = urlp.searchParams.get('hh') 539 const w = urlp.searchParams.get('ww') 540 541 if (!h || !w) { 542 return {success: false} 543 } 544 545 const dimensions = { 546 height: Number(h), 547 width: Number(w), 548 } 549 550 if (isWeb) { 551 if (isSafari) { 552 id = id.replace('AAAAC', 'AAAP1') 553 filename = filename.replace('.gif', '.mp4') 554 } else { 555 id = id.replace('AAAAC', 'AAAP3') 556 filename = filename.replace('.gif', '.webm') 557 } 558 } else { 559 id = id.replace('AAAAC', 'AAAAM') 560 } 561 562 return { 563 success: true, 564 playerUri: `https://t.gifs.bsky.app/${id}/${filename}`, 565 dimensions, 566 } 567}