1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/**
 * Pi-hole — Glide config
 *
 * Temporarily disable Pi-hole ad blocking via the FTL REST API
 * (POST /api/dns/blocking with a `timer`, which auto-resumes).
 * The API key is read from 1Password on every trigger and never
 * stored anywhere.
 *
 * Excmds:
 *   :pihole-sleep — disable blocking for 30 seconds
 *
 * Keymaps:
 *   ,p (normal) — pihole-sleep
 */

const PIHOLE_URL = "https://pihole";
const OP = "/opt/homebrew/bin/op"; // glide doesn't inherit the shell PATH
const CURL = "/usr/bin/curl";
const SLEEP_SECONDS = 30;

// POST JSON to the Pi-hole API. Bodies go through stdin so the API key
// never appears in process argv. Transport failures throw via curl's
// exit code; API failures come back as JSON `error` objects.
async function piholeRequest(path: string, body: unknown, sid?: string): Promise<any> {
  const args = [
    "--silent",
    "--show-error",
    "--max-time", "10",
    "--header", "Content-Type: application/json",
    "--data", "@-",
  ];
  if (sid) args.push("--header", `X-FTL-SID: ${sid}`);
  args.push(`${PIHOLE_URL}${path}`);

  const proc = await glide.process.spawn(CURL, args);
  await proc.stdin.write(JSON.stringify(body));
  await proc.stdin.close();
  const text = (await (await proc.wait()).stdout.text()).trim();

  let json: any;
  try {
    json = JSON.parse(text);
  } catch {
    throw new Error(`unexpected response from ${path}: ${text.slice(0, 200) || "(empty)"}`);
  }
  if (json.error) throw new Error(`${json.error.key}: ${json.error.message}`);
  return json;
}

async function piholeSleep(seconds: number): Promise<void> {
  const key = (await (await glide.process.execute(OP, ["read", "op://Private/Pi-hole/api key"])).stdout.text()).trim();
  if (!key) throw new Error("op read returned an empty API key");

  const auth = await piholeRequest("/api/auth", { password: key });
  const sid = auth?.session?.sid;
  if (!sid) throw new Error("auth response contained no session id");

  const status = await piholeRequest("/api/dns/blocking", { blocking: false, timer: seconds }, sid);
  if (status.blocking !== "disabled") throw new Error(`blocking status is "${status.blocking}"`);
}

const piholeSleepCmd = glide.excmds.create(
  { name: "pihole-sleep", description: `Disable Pi-hole blocking for ${SLEEP_SECONDS} seconds` },
  async () => {
    try {
      await piholeSleep(SLEEP_SECONDS);
      glide.g["dev.kejadlen"]!.notify(`Pi-hole disabled for ${SLEEP_SECONDS}s`);
    } catch (err) {
      glide.g["dev.kejadlen"]!.notify(`Pi-hole: ${err instanceof Error ? err.message : String(err)}`);
    }
  },
);

declare global {
  interface ExcmdRegistry {
    "pihole-sleep": typeof piholeSleepCmd;
  }
}

glide.keymaps.set("normal", ",p", () => glide.excmds.execute("pihole-sleep"), {
  description: "Disable Pi-hole blocking for 30 seconds",
});