fork
Configure Feed
Select the types of activity you want to include in your feed.
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.
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}