You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

221 lines
5.9 KiB

export function isWebsocketUrl(url: string): boolean {
return /^wss?:\/\/.+$/.test(url)
}
// copy from nostr-tools/utils
export function normalizeUrl(url: string): string {
try {
if (url.indexOf('://') === -1) {
if (url.startsWith('localhost:') || url.startsWith('localhost/')) {
url = 'ws://' + url
} else {
url = 'wss://' + url
}
}
const p = new URL(url)
p.pathname = p.pathname.replace(/\/+/g, '/')
if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1)
if (p.protocol === 'https:') {
p.protocol = 'wss:'
} else if (p.protocol === 'http:') {
p.protocol = 'ws:'
}
// Normalize localhost and local network addresses to always use ws:// instead of wss://
// This fixes the common typo where people use wss:// for local relays
if (isLocalNetworkUrl(p.toString())) {
p.protocol = 'ws:'
}
if ((p.port === '80' && p.protocol === 'ws:') || (p.port === '443' && p.protocol === 'wss:')) {
p.port = ''
}
p.searchParams.sort()
p.hash = ''
return p.toString()
} catch {
console.error('Invalid URL:', url)
return ''
}
}
export function normalizeHttpUrl(url: string): string {
try {
if (url.indexOf('://') === -1) url = 'https://' + url
const p = new URL(url)
p.pathname = p.pathname.replace(/\/+/g, '/')
if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1)
if (p.protocol === 'wss:') {
p.protocol = 'https:'
} else if (p.protocol === 'ws:') {
p.protocol = 'http:'
}
if (
(p.port === '80' && p.protocol === 'http:') ||
(p.port === '443' && p.protocol === 'https:')
) {
p.port = ''
}
p.searchParams.sort()
p.hash = ''
return p.toString()
} catch {
console.error('Invalid URL:', url)
return ''
}
}
export function simplifyUrl(url: string): string {
return url
.replace('wss://', '')
.replace('ws://', '')
.replace('https://', '')
.replace('http://', '')
.replace(/\/$/, '')
}
export function isLocalNetworkUrl(urlString: string): boolean {
try {
const url = new URL(urlString)
const hostname = url.hostname
// Check if it's localhost
if (hostname === 'localhost' || hostname === '::1') {
return true
}
// Check if it's an IPv4 local network address
const ipv4Match = hostname.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)
if (ipv4Match) {
const [, a, b, c, d] = ipv4Match.map(Number)
return (
a === 10 ||
(a === 172 && b >= 16 && b <= 31) ||
(a === 192 && b === 168) ||
(a === 127 && b === 0 && c === 0 && d === 1)
)
}
// Check if it's an IPv6 address
if (hostname.includes(':')) {
if (hostname === '::1') {
return true // IPv6 loopback address
}
if (hostname.startsWith('fe80:')) {
return true // Link-local address
}
if (hostname.startsWith('fc') || hostname.startsWith('fd')) {
return true // Unique local address (ULA)
}
}
return false
} catch {
return false // Return false for invalid URLs
}
}
export function isImage(url: string) {
try {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.heic', '.svg']
return imageExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext))
} catch {
return false
}
}
export function isMedia(url: string) {
try {
const mediaExtensions = [
'.mp4',
'.webm',
'.ogg',
'.mov',
'.mp3',
'.wav',
'.flac',
'.aac',
'.m4a',
'.opus',
'.wma'
]
return mediaExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext))
} catch {
return false
}
}
/**
* Remove tracking parameters from URLs
* Removes common tracking parameters like utm_*, fbclid, gclid, etc.
*/
export function cleanUrl(url: string): string {
try {
const parsedUrl = new URL(url)
// List of tracking parameter prefixes and exact names to remove
const trackingParams = [
// Google Analytics & Ads
'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
'utm_id', 'utm_source_platform', 'utm_creative_format', 'utm_marketing_tactic',
'gclid', 'gclsrc', 'dclid', 'gbraid', 'wbraid',
// Facebook
'fbclid', 'fb_action_ids', 'fb_action_types', 'fb_source', 'fb_ref',
// Twitter/X
'twclid', 'twsrc',
// Microsoft/Bing
'msclkid', 'mc_cid', 'mc_eid',
// Adobe
'adobe_mc', 'adobe_mc_ref', 'adobe_mc_sdid',
// Mailchimp
'mc_cid', 'mc_eid',
// HubSpot
'hsCtaTracking', 'hsa_acc', 'hsa_cam', 'hsa_grp', 'hsa_ad', 'hsa_src', 'hsa_tgt', 'hsa_kw', 'hsa_mt', 'hsa_net', 'hsa_ver',
// Marketo
'mkt_tok',
// YouTube
'si', 'feature', 'kw', 'pp',
// Other common tracking
'ref', 'referrer', 'source', 'campaign', 'medium', 'content',
'yclid', 'srsltid', '_ga', '_gl', 'igshid', 'epik', 'pk_campaign', 'pk_kwd',
// Mobile app tracking
'adjust_tracker', 'adjust_campaign', 'adjust_adgroup', 'adjust_creative',
// Amazon
'tag', 'linkCode', 'creative', 'creativeASIN', 'linkId', 'ascsubtag',
// Affiliate tracking
'aff_id', 'affiliate_id', 'aff', 'ref_', 'refer',
// Social media share tracking
'share', 'shared', 'sharesource'
]
// Remove all tracking parameters
trackingParams.forEach(param => {
parsedUrl.searchParams.delete(param)
})
// Remove any parameter that starts with utm_
Array.from(parsedUrl.searchParams.keys()).forEach(key => {
if (key.startsWith('utm_') || key.startsWith('_')) {
parsedUrl.searchParams.delete(key)
}
})
return parsedUrl.toString()
} catch {
// If URL parsing fails, return original URL
return url
}
}