Something went wrong. Try again.
Mirror of https://github.com/improsocial/improAn extensible Bluesky client for web
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127import { unique } from "/js/utils.js";
const MODERATION_ACTIONS = ["mute", "block", "feedback"];
export function getPermissionsFromManifest(manifest) { return parsePermissions(manifest.permissions ?? {});}
export function parsePermissions(permissions) { const parsed = {}; if (permissions.fetch) { const fetchArray = Array.isArray(permissions.fetch) ? permissions.fetch : [permissions.fetch]; const fetchPatterns = unique( fetchArray.filter((entry) => typeof entry === "string"), ); if (fetchPatterns.length > 0) parsed.fetch = fetchPatterns; } if (permissions.moderation) { const moderationArray = Array.isArray(permissions.moderation) ? permissions.moderation : [permissions.moderation]; const moderationScopes = unique( moderationArray.filter((entry) => MODERATION_ACTIONS.includes(entry)), ); if (moderationScopes.length > 0) parsed.moderation = moderationScopes; } return parsed;}
// action is one of "mute", "block", "feedback" (the "show fewer/more like// this" feed-interaction signal)export function isModerationActionAllowed(action, permissions) { return (permissions.moderation ?? []).includes(action);}
export function diffPermissions(current, next) { const diff = {}; let hasAny = false; for (const key of Object.keys(next)) { const have = new Set(current[key] ?? []); const added = (next[key] ?? []).filter((entry) => !have.has(entry)); if (added.length > 0) { diff[key] = added; hasAny = true; } } return hasAny ? diff : null;}
export function isEmptyPermissions(obj) { return Object.values(obj).every( (entries) => !Array.isArray(entries) || entries.length === 0, );}
export function isFetchAllowed(url, permissions) { let parsedUrl = null; try { parsedUrl = new URL(url); } catch { return false; } if (parsedUrl.protocol !== "https:") return false; return (permissions.fetch ?? []).some((pattern) => matchesPattern(parsedUrl, pattern), );}
// Permission pattern matching:// https://example.com/path — exact host, exact path// https://example.com/path/* — exact host, path prefix// https://*.example.com/* — example.com and any subdomain// https://example.com/* — exact host, any path
function matchesPattern(parsedUrl, pattern) { let parsedPattern = null; try { parsedPattern = parsePattern(pattern); } catch (e) { console.error(e); console.warn(`invalid permission: ${pattern}`); return false; } const { host, path } = parsedPattern; if (!hostMatches(parsedUrl.hostname, host)) return false; if (!pathMatches(parsedUrl.pathname, path)) return false; return true;}
function parsePattern(pattern) { if (typeof pattern !== "string") throw new Error("must be a string"); const schemeSep = pattern.indexOf("://"); if (schemeSep === -1) throw new Error("no protocol found"); const scheme = pattern.slice(0, schemeSep); if (scheme !== "https") throw new Error("https required"); const rest = pattern.slice(schemeSep + 3); const pathStart = rest.indexOf("/"); const host = ( pathStart === -1 ? rest : rest.slice(0, pathStart) ).toLowerCase(); const path = pathStart === -1 ? "/*" : rest.slice(pathStart); if (!host) throw new Error("no host found"); return { host, path };}
function hostMatches(actualHost, patternHost) { const actual = actualHost.toLowerCase(); if (patternHost.startsWith("*.")) { const suffix = patternHost.slice(2); if (!suffix || suffix.includes("*")) return false; if (actual === suffix) return true; return actual.endsWith("." + suffix); } if (patternHost.includes("*")) return false; return actual === patternHost;}
function pathMatches(actualPath, patternPath) { if (patternPath.endsWith("*")) { const prefix = patternPath.slice(0, -1); return actualPath.startsWith(prefix); } return actualPath === patternPath;}