links-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev 9e2af813edac
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 3151231032497ed786b88a0a
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/lib/mine.ts
// The links this browser owns (code -> owner key), opening one as its owner, and moving or exporting the list.
import { me, sdk } from "./bounded";
import { SITE_URL } from "../../functions/lib/config";
import { shortUrl, editUrl } from "../../functions/lib/links";

export interface Link { url: string; createdAt: number; clicks: number; disabled: boolean }
export interface Saved { code: string; key: string }

const STORE = "links:mine";
const CODE_RE = /^[A-Za-z0-9_-]{3,32}$/;
const KEY_RE = /^[A-Za-z0-9]{20,64}$/;

export function savedLinks(): Saved[] {
  try {
    const list = JSON.parse(localStorage.getItem(STORE) || "[]");
    return Array.isArray(list) ? list.filter((s) => s && typeof s.code === "string" && typeof s.key === "string") : [];
  } catch {
    return [];
  }
}

function store(list: Saved[]) {
  localStorage.setItem(STORE, JSON.stringify(list));
  window.dispatchEvent(new Event("links:saved"));
}

export const savedKey = (code: string) => savedLinks().find((s) => s.code === code)?.key ?? null;

/** Remembers a link newest first, keeping a key already saved for it. */
export function saveLink(code: string, key: string) {
  const list = savedLinks();
  if (list.some((s) => s.code === code)) return;
  store([{ code, key }, ...list]);
}

/** Forgets a link in this browser only (the link keeps working); returns a function that puts it back. */
export function forgetLink(code: string): () => void {
  const list = savedLinks();
  const at = list.findIndex((s) => s.code === code);
  if (at < 0) return () => {};
  const [gone] = list.splice(at, 1);
  store(list);
  return () => {
    const now = savedLinks().filter((s) => s.code !== code);
    now.splice(Math.min(at, now.length), 0, gone);
    store(now);
  };
}

/** One link that adds every saved link to another browser. The keys ride in the #fragment, which never reaches a server. */
export const moveUrl = (list: Saved[]) => `${SITE_URL}/#links=${list.map((s) => `${s.code}.${s.key}`).join(",")}`;

/** Adds the links carried by a #links= address to this browser: how many it carried, and how many were new here. */
export function importFromHash(hash: string): { found: number; added: number } {
  const m = hash.match(/^#links=([A-Za-z0-9_.,-]+)$/);
  if (!m) return { found: 0, added: 0 };
  const incoming = m[1].split(",").map((pair) => {
    const [code, key] = pair.split(".");
    return { code, key };
  }).filter((s) => CODE_RE.test(s.code) && KEY_RE.test(s.key));
  const list = savedLinks();
  const fresh = incoming.filter((s) => !list.some((x) => x.code === s.code));
  if (fresh.length) store([...fresh, ...list]);
  return { found: incoming.length, added: fresh.length };
}

async function read(code: string): Promise<Link | null> {
  try {
    const { get } = await sdk();
    return ((await get(`links/${code}`)) as Link | null) ?? null;
  } catch {
    return null;
  }
}

/** Reads a link as its owner, first becoming one with the key if this browser is not yet. Null when the key does not work. */
export async function openLink(code: string, key: string): Promise<Link | null> {
  const uid = await me();
  const link = await read(code);
  if (link) return link;
  try {
    const { setMany, serverTimestamp } = await sdk();
    await setMany([
      { path: `linkclaims/${code}/c/${uid}`, document: { key } },
      { path: `links/${code}/owners/${uid}`, document: { at: serverTimestamp() } },
    ]);
  } catch {
    return null;
  }
  return read(code);
}

/** Opens many links, a few at a time, calling back as each arrives. */
export async function openAll(list: Saved[], each: (code: string, link: Link | null) => void) {
  const queue = [...list];
  const worker = async () => {
    for (let s = queue.shift(); s; s = queue.shift()) {
      const link = await openLink(s.code, s.key).catch(() => null);
      each(s.code, link);
    }
  };
  await Promise.all(Array.from({ length: Math.min(6, queue.length) }, worker));
}

const cell = (v: string) => (/[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v);

/** A spreadsheet of the saved links, for Excel, Google Sheets or Numbers. */
export function toCsv(list: Saved[], data: Record<string, Link | null | undefined>): string {
  const rows = [["Short link", "Goes to", "Clicks", "Status", "Created", "Edit link (private)"]];
  for (const s of list) {
    const l = data[s.code];
    rows.push([
      shortUrl(s.code),
      l?.url ?? "",
      l ? l.clicks : "",
      l ? (l.disabled ? "Off" : "On") : "Could not open",
      l ? new Date(l.createdAt * 1000).toISOString().slice(0, 10) : "",
      editUrl(s.code, s.key),
    ].map(String));
  }
  return rows.map((r) => r.map(cell).join(",")).join("\r\n") + "\r\n";
}

Change history

Quality pass: clicks per day, CSV export, links on other devices
9e2af813ed · 6 hours ago
Bounded source sync
29eee7dafa · 1 day ago
Bounded source sync
c5e634046a · 1 day ago
Bounded source sync
12abf93e7e · 1 day ago
Bounded source sync
d0d6a654b1 · 1 day ago
Bounded source sync
e03049f905 · 1 day ago
Bounded source sync
eb5c793bfa · 1 day ago
Bounded source sync
5dd8ec26e1 · 1 day ago