mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
fork

Configure Feed

Select the types of activity you want to include in your feed.

at samuel/patch-onpaste 409 lines 10 kB view raw
1import {AtUri} from '@atproto/api' 2import psl from 'psl' 3import TLDs from 'tlds' 4 5import {BSKY_SERVICE} from '#/lib/constants' 6import {isInvalidHandle} from '#/lib/strings/handles' 7import {startUriToStarterPackUri} from '#/lib/strings/starter-pack' 8import {logger} from '#/logger' 9 10export const BSKY_APP_HOST = 'https://bsky.app' 11const BSKY_TRUSTED_HOSTS = [ 12 'bsky\\.app', 13 'bsky\\.social', 14 'blueskyweb\\.xyz', 15 'blueskyweb\\.zendesk\\.com', 16 ...(__DEV__ ? ['localhost:19006', 'localhost:8100'] : []), 17] 18 19/* 20 * This will allow any BSKY_TRUSTED_HOSTS value by itself or with a subdomain. 21 * It will also allow relative paths like /profile as well as #. 22 */ 23const TRUSTED_REGEX = new RegExp( 24 `^(http(s)?://(([\\w-]+\\.)?${BSKY_TRUSTED_HOSTS.join( 25 '|([\\w-]+\\.)?', 26 )})|/|#)`, 27) 28 29export function isValidDomain(str: string): boolean { 30 return !!TLDs.find(tld => { 31 let i = str.lastIndexOf(tld) 32 if (i === -1) { 33 return false 34 } 35 return str.charAt(i - 1) === '.' && i === str.length - tld.length 36 }) 37} 38 39export function makeRecordUri( 40 didOrName: string, 41 collection: string, 42 rkey: string, 43) { 44 const urip = new AtUri('at://host/') 45 urip.host = didOrName 46 urip.collection = collection 47 urip.rkey = rkey 48 return urip.toString() 49} 50 51export function toNiceDomain(url: string): string { 52 try { 53 const urlp = new URL(url) 54 if (`https://${urlp.host}` === BSKY_SERVICE) { 55 return 'Bluesky Social' 56 } 57 return urlp.host ? urlp.host : url 58 } catch (e) { 59 return url 60 } 61} 62 63export function toShortUrl(url: string): string { 64 try { 65 const urlp = new URL(url) 66 if (urlp.protocol !== 'http:' && urlp.protocol !== 'https:') { 67 return url 68 } 69 const path = 70 (urlp.pathname === '/' ? '' : urlp.pathname) + urlp.search + urlp.hash 71 if (path.length > 15) { 72 return urlp.host + path.slice(0, 13) + '...' 73 } 74 return urlp.host + path 75 } catch (e) { 76 return url 77 } 78} 79 80export function toShareUrl(url: string): string { 81 if (!url.startsWith('https')) { 82 const urlp = new URL('https://bsky.app') 83 urlp.pathname = url 84 url = urlp.toString() 85 } 86 return url 87} 88 89export function toBskyAppUrl(url: string): string { 90 return new URL(url, BSKY_APP_HOST).toString() 91} 92 93export function isBskyAppUrl(url: string): boolean { 94 return url.startsWith('https://bsky.app/') 95} 96 97export function isRelativeUrl(url: string): boolean { 98 return /^\/[^/]/.test(url) 99} 100 101export function isBskyRSSUrl(url: string): boolean { 102 return ( 103 (url.startsWith('https://bsky.app/') || isRelativeUrl(url)) && 104 /\/rss\/?$/.test(url) 105 ) 106} 107 108export function isExternalUrl(url: string): boolean { 109 const external = !isBskyAppUrl(url) && url.startsWith('http') 110 const rss = isBskyRSSUrl(url) 111 return external || rss 112} 113 114export function isTrustedUrl(url: string): boolean { 115 return TRUSTED_REGEX.test(url) 116} 117 118export function isBskyPostUrl(url: string): boolean { 119 if (isBskyAppUrl(url)) { 120 try { 121 const urlp = new URL(url) 122 return /profile\/(?<name>[^/]+)\/post\/(?<rkey>[^/]+)/i.test( 123 urlp.pathname, 124 ) 125 } catch {} 126 } 127 return false 128} 129 130export function isBskyCustomFeedUrl(url: string): boolean { 131 if (isBskyAppUrl(url)) { 132 try { 133 const urlp = new URL(url) 134 return /profile\/(?<name>[^/]+)\/feed\/(?<rkey>[^/]+)/i.test( 135 urlp.pathname, 136 ) 137 } catch {} 138 } 139 return false 140} 141 142export function isBskyListUrl(url: string): boolean { 143 if (isBskyAppUrl(url)) { 144 try { 145 const urlp = new URL(url) 146 return /profile\/(?<name>[^/]+)\/lists\/(?<rkey>[^/]+)/i.test( 147 urlp.pathname, 148 ) 149 } catch { 150 console.error('Unexpected error in isBskyListUrl()', url) 151 } 152 } 153 return false 154} 155 156export function isBskyStartUrl(url: string): boolean { 157 if (isBskyAppUrl(url)) { 158 try { 159 const urlp = new URL(url) 160 return /start\/(?<name>[^/]+)\/(?<rkey>[^/]+)/i.test(urlp.pathname) 161 } catch { 162 console.error('Unexpected error in isBskyStartUrl()', url) 163 } 164 } 165 return false 166} 167 168export function isBskyStarterPackUrl(url: string): boolean { 169 if (isBskyAppUrl(url)) { 170 try { 171 const urlp = new URL(url) 172 return /starter-pack\/(?<name>[^/]+)\/(?<rkey>[^/]+)/i.test(urlp.pathname) 173 } catch { 174 console.error('Unexpected error in isBskyStartUrl()', url) 175 } 176 } 177 return false 178} 179 180export function isBskyDownloadUrl(url: string): boolean { 181 if (isExternalUrl(url)) { 182 return false 183 } 184 return url === '/download' || url.startsWith('/download?') 185} 186 187export function convertBskyAppUrlIfNeeded(url: string): string { 188 if (isBskyAppUrl(url)) { 189 try { 190 const urlp = new URL(url) 191 192 if (isBskyStartUrl(url)) { 193 return startUriToStarterPackUri(urlp.pathname) 194 } 195 196 // special-case search links 197 if (urlp.pathname === '/search') { 198 return `/search?q=${urlp.searchParams.get('q')}` 199 } 200 201 return urlp.pathname 202 } catch (e) { 203 console.error('Unexpected error in convertBskyAppUrlIfNeeded()', e) 204 } 205 } else if (isShortLink(url)) { 206 // We only want to do this on native, web handles the 301 for us 207 return shortLinkToHref(url) 208 } 209 return url 210} 211 212export function listUriToHref(url: string): string { 213 try { 214 const {hostname, rkey} = new AtUri(url) 215 return `/profile/${hostname}/lists/${rkey}` 216 } catch { 217 return '' 218 } 219} 220 221export function feedUriToHref(url: string): string { 222 try { 223 const {hostname, rkey} = new AtUri(url) 224 return `/profile/${hostname}/feed/${rkey}` 225 } catch { 226 return '' 227 } 228} 229 230export function postUriToRelativePath( 231 uri: string, 232 options?: {handle?: string}, 233): string | undefined { 234 try { 235 const {hostname, rkey} = new AtUri(uri) 236 const handleOrDid = 237 options?.handle && !isInvalidHandle(options.handle) 238 ? options.handle 239 : hostname 240 return `/profile/${handleOrDid}/post/${rkey}` 241 } catch { 242 return undefined 243 } 244} 245 246/** 247 * Checks if the label in the post text matches the host of the link facet. 248 * 249 * Hosts are case-insensitive, so should be lowercase for comparison. 250 * @see https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 251 */ 252export function linkRequiresWarning(uri: string, label: string) { 253 const labelDomain = labelToDomain(label) 254 255 // We should trust any relative URL or a # since we know it links to internal content 256 if (isRelativeUrl(uri) || uri === '#') { 257 return false 258 } 259 260 let urip 261 try { 262 urip = new URL(uri) 263 } catch { 264 return true 265 } 266 267 const host = urip.hostname.toLowerCase() 268 if (isTrustedUrl(uri)) { 269 // if this is a link to internal content, warn if it represents itself as a URL to another app 270 return !!labelDomain && labelDomain !== host && isPossiblyAUrl(labelDomain) 271 } else { 272 // if this is a link to external content, warn if the label doesnt match the target 273 if (!labelDomain) { 274 return true 275 } 276 return labelDomain !== host 277 } 278} 279 280/** 281 * Returns a lowercase domain hostname if the label is a valid URL. 282 * 283 * Hosts are case-insensitive, so should be lowercase for comparison. 284 * @see https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 285 */ 286export function labelToDomain(label: string): string | undefined { 287 // any spaces just immediately consider the label a non-url 288 if (/\s/.test(label)) { 289 return undefined 290 } 291 try { 292 return new URL(label).hostname.toLowerCase() 293 } catch {} 294 try { 295 return new URL('https://' + label).hostname.toLowerCase() 296 } catch {} 297 return undefined 298} 299 300export function isPossiblyAUrl(str: string): boolean { 301 str = str.trim() 302 if (str.startsWith('http://')) { 303 return true 304 } 305 if (str.startsWith('https://')) { 306 return true 307 } 308 const [firstWord] = str.split(/[\s\/]/) 309 return isValidDomain(firstWord) 310} 311 312export function splitApexDomain(hostname: string): [string, string] { 313 const hostnamep = psl.parse(hostname) 314 if (hostnamep.error || !hostnamep.listed || !hostnamep.domain) { 315 return ['', hostname] 316 } 317 return [ 318 hostnamep.subdomain ? `${hostnamep.subdomain}.` : '', 319 hostnamep.domain, 320 ] 321} 322 323export function createBskyAppAbsoluteUrl(path: string): string { 324 const sanitizedPath = path.replace(BSKY_APP_HOST, '').replace(/^\/+/, '') 325 return `${BSKY_APP_HOST.replace(/\/$/, '')}/${sanitizedPath}` 326} 327 328export function createProxiedUrl(url: string): string { 329 let u 330 try { 331 u = new URL(url) 332 } catch { 333 return url 334 } 335 336 if (u?.protocol !== 'http:' && u?.protocol !== 'https:') { 337 return url 338 } 339 340 return `https://go.bsky.app/redirect?u=${encodeURIComponent(url)}` 341} 342 343export function isShortLink(url: string): boolean { 344 return url.startsWith('https://go.bsky.app/') 345} 346 347export function shortLinkToHref(url: string): string { 348 try { 349 const urlp = new URL(url) 350 351 // For now we only support starter packs, but in the future we should add additional paths to this check 352 const parts = urlp.pathname.split('/').filter(Boolean) 353 if (parts.length === 1) { 354 return `/starter-pack-short/${parts[0]}` 355 } 356 return url 357 } catch (e) { 358 logger.error('Failed to parse possible short link', {safeMessage: e}) 359 return url 360 } 361} 362 363export function getHostnameFromUrl(url: string | URL): string | null { 364 let urlp 365 try { 366 urlp = new URL(url) 367 } catch (e) { 368 return null 369 } 370 return urlp.hostname 371} 372 373export function getServiceAuthAudFromUrl(url: string | URL): string | null { 374 const hostname = getHostnameFromUrl(url) 375 if (!hostname) { 376 return null 377 } 378 return `did:web:${hostname}` 379} 380 381// passes URL.parse, and has a TLD etc 382export function definitelyUrl(maybeUrl: string) { 383 try { 384 if (maybeUrl.endsWith('.')) return null 385 386 // Prepend 'https://' if the input doesn't start with a protocol 387 if (!maybeUrl.startsWith('https://') && !maybeUrl.startsWith('http://')) { 388 maybeUrl = 'https://' + maybeUrl 389 } 390 391 const url = new URL(maybeUrl) 392 393 // Extract the hostname and split it into labels 394 const hostname = url.hostname 395 const labels = hostname.split('.') 396 397 // Ensure there are at least two labels (e.g., 'example' and 'com') 398 if (labels.length < 2) return null 399 400 const tld = labels[labels.length - 1] 401 402 // Check that the TLD is at least two characters long and contains only letters 403 if (!/^[a-z]{2,}$/i.test(tld)) return null 404 405 return url.toString() 406 } catch { 407 return null 408 } 409}