this repo has no description
0
fork

Configure Feed

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

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