url.ts raw
1 export function isWebsocketUrl(url: string): boolean {
2 try {
3 const protocol = new URL(url).protocol
4 return protocol === 'ws:' || protocol === 'wss:'
5 } catch {
6 return false
7 }
8 }
9
10 export function isOnionUrl(url: string): boolean {
11 try {
12 const hostname = new URL(url).hostname
13 return hostname.endsWith('.onion')
14 } catch {
15 return false
16 }
17 }
18
19 // copy from nostr-tools/utils
20 export function normalizeUrl(url: string): string {
21 try {
22 if (url.indexOf('://') === -1) {
23 if (
24 url.startsWith('localhost:') ||
25 url.startsWith('localhost/') ||
26 url.startsWith('127.') ||
27 url.startsWith('192.168.')
28 ) {
29 url = 'ws://' + url
30 } else {
31 url = 'wss://' + url
32 }
33 }
34 const p = new URL(url)
35 p.pathname = p.pathname.replace(/\/+/g, '/')
36 if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1)
37 if (p.protocol === 'https:') {
38 p.protocol = 'wss:'
39 } else if (p.protocol === 'http:') {
40 p.protocol = 'ws:'
41 }
42 if ((p.port === '80' && p.protocol === 'ws:') || (p.port === '443' && p.protocol === 'wss:')) {
43 p.port = ''
44 }
45 p.searchParams.sort()
46 p.hash = ''
47 return p.toString()
48 } catch {
49 console.error('Invalid URL:', url)
50 return ''
51 }
52 }
53
54 export function normalizeHttpUrl(url: string): string {
55 try {
56 if (url.indexOf('://') === -1) url = 'https://' + url
57 const p = new URL(url)
58 p.pathname = p.pathname.replace(/\/+/g, '/')
59 if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1)
60 if (p.protocol === 'wss:') {
61 p.protocol = 'https:'
62 } else if (p.protocol === 'ws:') {
63 p.protocol = 'http:'
64 }
65 if (
66 (p.port === '80' && p.protocol === 'http:') ||
67 (p.port === '443' && p.protocol === 'https:')
68 ) {
69 p.port = ''
70 }
71 p.searchParams.sort()
72 p.hash = ''
73 return p.toString()
74 } catch {
75 console.error('Invalid URL:', url)
76 return ''
77 }
78 }
79
80 export function simplifyUrl(url: string): string {
81 return url
82 .replace('wss://', '')
83 .replace('ws://', '')
84 .replace('https://', '')
85 .replace('http://', '')
86 .replace(/\/$/, '')
87 }
88
89 export function isLocalNetworkUrl(urlString: string): boolean {
90 try {
91 const url = new URL(urlString)
92 const hostname = url.hostname
93
94 // Check if it's localhost
95 if (hostname === 'localhost' || hostname === '::1') {
96 return true
97 }
98
99 // Check if it's an IPv4 local network address
100 const ipv4Match = hostname.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)
101 if (ipv4Match) {
102 const [, a, b, c, d] = ipv4Match.map(Number)
103 return (
104 a === 10 ||
105 (a === 172 && b >= 16 && b <= 31) ||
106 (a === 192 && b === 168) ||
107 (a === 127 && b === 0 && c === 0 && d === 1)
108 )
109 }
110
111 // Check if it's an IPv6 address
112 if (hostname.includes(':')) {
113 if (hostname === '::1') {
114 return true // IPv6 loopback address
115 }
116 if (hostname.startsWith('fe80:')) {
117 return true // Link-local address
118 }
119 if (hostname.startsWith('fc') || hostname.startsWith('fd')) {
120 return true // Unique local address (ULA)
121 }
122 }
123
124 return false
125 } catch {
126 return false // Return false for invalid URLs
127 }
128 }
129
130 export function isImage(url: string) {
131 try {
132 const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.heic', '.svg']
133 return imageExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext))
134 } catch {
135 return false
136 }
137 }
138
139 export function isMedia(url: string) {
140 try {
141 const mediaExtensions = [
142 '.mp4',
143 '.webm',
144 '.ogg',
145 '.mov',
146 '.mp3',
147 '.wav',
148 '.flac',
149 '.aac',
150 '.m4a',
151 '.opus',
152 '.wma',
153 '.3gp'
154 ]
155 return mediaExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext))
156 } catch {
157 return false
158 }
159 }
160
161 export const truncateUrl = (url: string, maxLength: number = 40) => {
162 try {
163 const urlObj = new URL(url)
164 let domain = urlObj.hostname
165 let path = urlObj.pathname
166
167 if (domain.startsWith('www.')) {
168 domain = domain.slice(4)
169 }
170
171 if (!path || path === '/') {
172 return domain
173 }
174
175 if (path.endsWith('/')) {
176 path = path.slice(0, -1)
177 }
178
179 const u = domain + path
180
181 if (u.length > maxLength) {
182 return domain + path.slice(0, maxLength - domain.length - 3) + '...'
183 }
184
185 return u
186 } catch {
187 // invalid URL
188 let truncated = url
189 if (truncated.startsWith('https://')) {
190 truncated = truncated.slice(8)
191 } else if (truncated.startsWith('http://')) {
192 truncated = truncated.slice(7)
193 }
194 if (truncated.startsWith('www.')) {
195 truncated = truncated.slice(4)
196 }
197 if (truncated.length > maxLength) {
198 return truncated.slice(0, maxLength - 3) + '...'
199 }
200 return truncated
201 }
202 }
203