UNPKG

coldsky

Version:

Library and the app for BlueSky

265 lines (223 loc) 8.88 kB
// @ts-check /** @param {string | null | undefined} text */ export function likelyDID(text) { return !!text && ( !text.trim().indexOf('did:') || text.trim().length === 24 && !/[^\sa-z0-9]/i.test(text) ); } /** * @param {T} did * @returns {T} * @template {string | undefined | null} T */ export function shortenDID(did) { return did && /** @type {T} */(did.replace(_shortenDID_Regex, '').toLowerCase() || undefined); } const _shortenDID_Regex = /^did\:plc\:/; /** * @param {T} shortDID * @returns {T} * @template {string | undefined | null} T */ export function unwrapShortDID(shortDID) { return /** @type {T} */( !shortDID ? undefined : shortDID.indexOf(':') < 0 ? 'did:plc:' + shortDID.toLowerCase() : shortDID.toLowerCase() ); } /** * @param {T} handle * @returns {T} * @template {string | undefined | null} T */ export function shortenHandle(handle) { handle = cheapNormalizeHandle(handle); return handle && /** @type {T} */(handle.replace(_shortenHandle_Regex, '').toLowerCase() || undefined); } const _shortenHandle_Regex = /\.bsky\.social$/; /** * @param {T} shortHandle * @returns {T} * @template {string | undefined | null} T */ export function unwrapShortHandle(shortHandle) { if (likelyDID(shortHandle)) return unwrapShortDID(shortHandle); shortHandle = cheapNormalizeHandle(shortHandle); return /** @type {T} */( !shortHandle ? undefined : shortHandle.indexOf('.') < 0 ? shortHandle.toLowerCase() + '.bsky.social' : shortHandle.toLowerCase() ); } function cheapNormalizeHandle(handle) { handle = handle && handle.trim().toLowerCase(); if (handle && handle.charCodeAt(0) === 64) handle = handle.slice(1); const urlprefix = 'https://bsky.app/'; if (handle && handle.lastIndexOf(urlprefix, 0) === 0) { const postURL = breakPostURL(handle); if (postURL && postURL.shortDID) return postURL.shortDID; } if (handle && handle.lastIndexOf('at:', 0) === 0) { const feedUri = breakFeedURI(handle); if (feedUri && feedUri.shortDID) return feedUri.shortDID; if (handle && handle.lastIndexOf('at://', 0) === 0) handle = handle.slice(5); else handle = handle.slice(3); } return handle || undefined; } /** @param {string | undefined | null} pdc */ export function shortenPDS(pdc) { if (!pdc) return undefined; pdc = pdc.trim().toLowerCase(); if (pdc === 'https://bsky.social') return '.s'; else if (pdc === 'https://bsky.network') return '.n'; else if (pdc === 'https://bsky.app') return '.a'; // https://morel.us-east.host.bsky.network return pdc.replace(/^https:\/\//, '').replace(/host\.bsky\.network$/, ''); } export function unwrapShortPDS(shortPDC) { if (!shortPDC) return undefined; if (shortPDC === '.s') return 'https://bsky.social'; else if (shortPDC === '.n') return 'https://bsky.network'; else if (shortPDC === '.a') return 'https://bsky.app'; if (/^http:/i.test(shortPDC) || /^https:/i.test(shortPDC)) return shortPDC; return 'https://' + shortPDC + 'host.bsky.network'; } /** * dd+hh:mm:ss - like 30+23:59:59 * @param {string | null | undefined} dtOffsetStr */ export function parseTimestampOffset(dtOffsetStr) { if (!dtOffsetStr) return undefined; let offset = 0; let lead = 0; const plusPos = dtOffsetStr.indexOf('+'); if (plusPos >= 0) { offset = Number(dtOffsetStr.substring(0, plusPos)) * 24 * 60 * 60 * 1000; lead = plusPos + 1; } const secondsColonPos = dtOffsetStr.lastIndexOf(':'); if (secondsColonPos < 0) { offset += Number(dtOffsetStr.substring(lead)) * 1000; } else { offset += Number(dtOffsetStr.substring(secondsColonPos + 1)) * 1000; const minutesColonPos = dtOffsetStr.lastIndexOf(':', secondsColonPos - 1); if (minutesColonPos < 0) { offset += Number(dtOffsetStr.substring(lead, secondsColonPos)) * 60 * 1000; } else { offset += Number(dtOffsetStr.substring(minutesColonPos + 1, secondsColonPos)) * 60 * 1000; offset += Number(dtOffsetStr.substring(lead, minutesColonPos)) * 60 * 60 * 1000; } } return offset; } const offsetTooLarge = Date.UTC(2022, 1, 1); /** * @param {number} offset * @returns dd+hh:mm:ss like 30+23:59:59 or 59:59.999 */ export function timestampOffsetToString(offset) { if (offset > offsetTooLarge) { console.error('timestampOffsetToString: offset too large', offset, new Date(offset)); } const milliseconds = offset % 1000; offset = (offset - milliseconds) / 1000; const seconds = offset % 60; offset = (offset - seconds) / 60; const minutes = offset % 60; offset = (offset - minutes) / 60; const hours = offset % 24; const days = (offset - hours) / 24; let str = (100 + seconds).toString().slice(1); if (milliseconds) { str = str + '.' + (1000 + milliseconds).toString().slice(1).replace(/0+$/, ''); } if (days + hours + minutes) { str = (100 + minutes).toString().slice(1) + ':' + str; if (days + hours) { str = hours.toString() + ':' + str; if (days) { str = days + '+' + str; } } } // no need for leading zero if (str.lastIndexOf('0', 0) === 0) str = str.slice(1); return str; } /** * @param {string | null | undefined} url */ export function breakPostURL(url) { if (!url) return; const matchBsky = _breakBskyPostURL_Regex.exec(url); if (matchBsky) return { shortDID: shortenDID(matchBsky[1]), postID: matchBsky[2]?.toString().toLowerCase() }; const matchGisting = _breakGistingPostURL_Regex.exec(url); if (matchGisting) return { shortDID: shortenDID(matchGisting[2]), postID: matchGisting[3]?.toString().toLowerCase() }; const matchBskyStyle = _breakBskyStylePostURL_Regex.exec(url); if (matchBskyStyle) return { shortDID: shortenDID(matchBskyStyle[2]), postID: matchBskyStyle[3]?.toString().toLowerCase() }; } const _breakBskyPostURL_Regex = /^http[s]?\:\/\/bsky\.app\/profile\/([a-z0-9\.\:\-]+)\/post\/([a-z0-9]+)(\/|$)/i; const _breakBskyStylePostURL_Regex = /^http[s]?\:\/\/(bsky\.app|6sky\.app|gist\.ing|gisti\.ng|gist\.ink)\/profile\/([a-z0-9\.\:\-]+)\/post\/([a-z0-9]+)(\/|$)/i; const _breakGistingPostURL_Regex = /^http[s]?\:\/\/(6sky\.app|gist\.ing|gisti\.ng|gist\.ink)\/([a-z0-9\.\:\-]+)\/([a-z0-9]+)(\/|$)/i; /** * @param {string} shortDID * @param {string} postID */ export function makeBskyPostURL(shortDID, postID) { return 'https://bsky.app/profile/' + unwrapShortDID(shortDID) + '/post/' + postID; } /** * @param {string | null | undefined} url */ export function detectProfileURL(url) { if (!url) return; const matchBsky = _detectBskyProfileURL_Regex.exec(url); if (matchBsky) return shortenDID(matchBsky[1]); const matchGisting = _detectGistingProfileURL_Regex.exec(url); if (matchGisting) return shortenDID(matchGisting[2]); const matchOyinboReceipts = _detectOyinboReceiptsURL_Regex.exec(url); if (matchOyinboReceipts) return shortenDID(matchOyinboReceipts[1]); const matchClearSky = _detectClearSkyProfileURL_Regex.exec(url); if (matchClearSky) return shortenDID(matchClearSky[2]); } const _detectBskyProfileURL_Regex = /^http[s]?\:\/\/bsky\.app\/profile\/([a-z0-9\.\:\-]+)(\/|$)/i; const _detectGistingProfileURL_Regex = /^http[s]?\:\/\/(gist\.ing|gisti\.ng|gist\.ink)\/([a-z0-9\.\:\-]+)(\/|$)/i; const _detectOyinboReceiptsURL_Regex = /^http[s]?\:\/\/oyin\.bo\/receipts\/?\?handle\=([a-z0-9\.\:\-]+)(\/|$)/i; const _detectClearSkyProfileURL_Regex = /^http[s]?\:\/\/(clearsky\.app|bsky\.thieflord\.dev)\/([a-z0-9\.\:\-]+)(\/|$)/i; export function makeFeedUri(shortDID, postID) { return 'at://' + unwrapShortDID(shortDID) + '/app.bsky.feed.post/' + postID; } /** * @param {string | null | undefined} uri */ export function breakFeedURI(uri) { if (!uri) return; const match = _breakFeedUri_Regex.exec(uri); if (!match || !match[4]) return; if (match[3] === 'app.bsky.feed.post') return { shortDID: shortenDID(match[2]), postID: match[4] }; return { shortDID: match[2], postID: match[4], feedType: match[3] }; } const _breakFeedUri_Regex = /^at\:\/\/(did:plc:)?([a-z0-9]+)\/([a-z\.]+)\/?(.*)?$/; /** * @param {string | null | undefined} uri */ export function breakFeedURIPostOnly(uri) { if (!uri) return; const match = _breakFeedUri_Regex.exec(uri); if (!match || !match[4]) return; if (match[3] === 'app.bsky.feed.post') return { shortDID: shortenDID(match[2]), postID: match[4] }; } export function getProfileBlobUrl(did, cid) { if (!did || !cid) return undefined; return `https://cdn.bsky.app/img/avatar/plain/${unwrapShortDID(did)}/${cid}@jpeg`; } export function getFeedBlobUrl(did, cid) { if (!did || !cid) return undefined; return `https://cdn.bsky.app/img/feed_thumbnail/plain/${unwrapShortDID(did)}/${cid}@jpeg`; } export function getFeedVideoBlobUrl(did, cid) { if (!did || !cid) return undefined; return `https://video.bsky.app/watch/${unwrapShortDID(did)}/${cid}/thumbnail.jpg`; }