Initial commit
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
name: Build presences
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- "presences/**"
|
||||||
|
- "scripts/**"
|
||||||
|
- "package.json"
|
||||||
|
- "package-lock.json"
|
||||||
|
- ".gitea/workflows/build.yml"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Build all presences
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Generate index.json
|
||||||
|
run: npm run index
|
||||||
|
|
||||||
|
- name: Commit and push artifacts
|
||||||
|
env:
|
||||||
|
BUILD_USER: gitea-actions
|
||||||
|
BUILD_EMAIL: actions@gits.hibna.com.tr
|
||||||
|
run: |
|
||||||
|
git config user.name "$BUILD_USER"
|
||||||
|
git config user.email "$BUILD_EMAIL"
|
||||||
|
git add presences/*/dist/presence.js index.json
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No build artifacts changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -m "ci: rebuild presence artifacts"
|
||||||
|
git push origin HEAD:main
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# Source Presences
|
||||||
|
|
||||||
|
Public registry of Source Presence extensions, served from
|
||||||
|
[`gits.hibna.com.tr/hibna/source-presences`](https://gits.hibna.com.tr/hibna/source-presences).
|
||||||
|
|
||||||
|
The Source Presence browser extension reads:
|
||||||
|
|
||||||
|
- `index.json` — list of presences and their current versions.
|
||||||
|
- `presences/<id>/metadata.json` — presence manifest.
|
||||||
|
- `presences/<id>/dist/presence.js` — built CJS module (produced by `npm run build`).
|
||||||
|
- `presences/<id>/<icon>` — optional 128×128 PNG.
|
||||||
|
|
||||||
|
Both `index.json` and each presence's `dist/presence.js` are committed by the
|
||||||
|
Gitea Actions workflow at [`.gitea/workflows/build.yml`](.gitea/workflows/build.yml)
|
||||||
|
on every push to `main`.
|
||||||
|
|
||||||
|
## Add a new presence
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p presences/<id>/src
|
||||||
|
cat > presences/<id>/metadata.json <<'JSON'
|
||||||
|
{
|
||||||
|
"id": "<id>",
|
||||||
|
"name": "<Display Name>",
|
||||||
|
"description": "<short description>",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "<your name>",
|
||||||
|
"match": ["https://example.com/*"],
|
||||||
|
"tickInterval": 1000,
|
||||||
|
"sdkVersion": "^0.1.0"
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
```
|
||||||
|
|
||||||
|
Write your TypeScript entry at `presences/<id>/src/presence.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { ActivityType, definePresence } from "@source/presence-sdk";
|
||||||
|
|
||||||
|
export default definePresence({
|
||||||
|
match: ["https://example.com/*"],
|
||||||
|
tick(ctx) {
|
||||||
|
ctx.setActivity({
|
||||||
|
type: ActivityType.WATCHING,
|
||||||
|
name: "Example",
|
||||||
|
details: document.title,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Push to `main`. Gitea Actions builds the presence, regenerates `index.json`,
|
||||||
|
and commits the artifacts back. The extension picks up the new version on its
|
||||||
|
next 6-hour refresh, or immediately when users hit "Refresh" / "Update all".
|
||||||
|
|
||||||
|
## Local build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build # builds every presence under presences/*
|
||||||
|
npm run index # regenerates index.json
|
||||||
|
npm run release # both
|
||||||
|
```
|
||||||
|
|
||||||
|
## Presence API
|
||||||
|
|
||||||
|
`PresenceDefinition` shape (also defined in [`types/presence-sdk.d.ts`](types/presence-sdk.d.ts)):
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|----------------|----------|--------------------------------------------------------------------|
|
||||||
|
| `match` | yes | Array of [match patterns](https://developer.chrome.com/docs/extensions/reference/match_patterns). |
|
||||||
|
| `tickInterval` | no | Override default 1000ms tick. |
|
||||||
|
| `onLoad(ctx)` | no | Runs once when a matching tab loads the presence. |
|
||||||
|
| `tick(ctx)` | no | Runs every `tickInterval`; call `ctx.setActivity(...)` to publish. |
|
||||||
|
| `onUnload(ctx)`| no | Runs when the presence is torn down (tab nav / disable). |
|
||||||
|
|
||||||
|
`PresenceContext`:
|
||||||
|
|
||||||
|
- `ctx.url` / `ctx.tabActive` — read-only state.
|
||||||
|
- `ctx.setActivity(activity)` / `ctx.clear()` — publish or clear.
|
||||||
|
- `ctx.storage` — namespaced async storage (`get` / `set` / `delete` / `clear`).
|
||||||
|
- `ctx.log` / `warn` / `error` — surfaced in the extension's background console.
|
||||||
|
|
||||||
|
## Activity payload
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
type: ActivityType.PLAYING | LISTENING | WATCHING,
|
||||||
|
name: string,
|
||||||
|
details?: string,
|
||||||
|
state?: string,
|
||||||
|
url?: string,
|
||||||
|
largeImage?: string,
|
||||||
|
largeText?: string,
|
||||||
|
smallImage?: string,
|
||||||
|
smallText?: string,
|
||||||
|
startedAt?: number, // unix ms — Source renders an elapsed timer
|
||||||
|
endsAt?: number, // unix ms — Source renders a remaining timer
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Presences run in the **content script's isolated world** with full DOM read
|
||||||
|
access. They cannot access page JS globals.
|
||||||
|
- The extension debounces identical payloads; calling `setActivity` with the
|
||||||
|
same data on every tick is fine.
|
||||||
|
- Keep presence builds small (< 100 KB). They're shipped with every install.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "source-presences",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "node scripts/build.mjs",
|
||||||
|
"index": "node scripts/generate-index.mjs",
|
||||||
|
"release": "npm run build && npm run index"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.24.2",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@source/presence-sdk": "0.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"id": "spotify",
|
||||||
|
"name": "Spotify Web",
|
||||||
|
"description": "Reflects what you're listening to on open.spotify.com.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "hibna",
|
||||||
|
"match": ["https://open.spotify.com/*"],
|
||||||
|
"tickInterval": 1500,
|
||||||
|
"sdkVersion": "^0.1.0"
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { ActivityType, definePresence } from "@source/presence-sdk";
|
||||||
|
|
||||||
|
function readNowPlayingTitle(): string | null {
|
||||||
|
return (
|
||||||
|
document
|
||||||
|
.querySelector<HTMLElement>('[data-testid="now-playing-widget"] [data-testid="context-item-info-title"]')
|
||||||
|
?.textContent?.trim() ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readArtist(): string | null {
|
||||||
|
const artistNodes = document.querySelectorAll<HTMLElement>(
|
||||||
|
'[data-testid="now-playing-widget"] [data-testid="context-item-info-artist"]',
|
||||||
|
);
|
||||||
|
if (artistNodes.length === 0) return null;
|
||||||
|
return Array.from(artistNodes).map((n) => n.textContent?.trim()).filter(Boolean).join(", ") || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPlayState(): "playing" | "paused" {
|
||||||
|
const button = document.querySelector<HTMLButtonElement>(
|
||||||
|
'[data-testid="control-button-playpause"]',
|
||||||
|
);
|
||||||
|
const label = button?.getAttribute("aria-label")?.toLowerCase() ?? "";
|
||||||
|
if (label.includes("pause")) return "playing";
|
||||||
|
return "paused";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePresence({
|
||||||
|
match: ["https://open.spotify.com/*"],
|
||||||
|
tickInterval: 1500,
|
||||||
|
tick(ctx) {
|
||||||
|
const title = readNowPlayingTitle();
|
||||||
|
if (!title) {
|
||||||
|
ctx.setActivity({
|
||||||
|
type: ActivityType.LISTENING,
|
||||||
|
name: "Spotify",
|
||||||
|
details: "Browsing",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const artist = readArtist();
|
||||||
|
const state = readPlayState();
|
||||||
|
ctx.setActivity({
|
||||||
|
type: ActivityType.LISTENING,
|
||||||
|
name: "Spotify",
|
||||||
|
details: title,
|
||||||
|
state: artist ? `${state === "playing" ? "Playing" : "Paused"} · ${artist}` : state === "playing" ? "Playing" : "Paused",
|
||||||
|
url: location.href,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"id": "youtube",
|
||||||
|
"name": "YouTube",
|
||||||
|
"description": "Shows the video you're watching on YouTube as a Source rich presence.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "hibna",
|
||||||
|
"match": [
|
||||||
|
"https://www.youtube.com/*",
|
||||||
|
"https://music.youtube.com/*"
|
||||||
|
],
|
||||||
|
"tickInterval": 1000,
|
||||||
|
"sdkVersion": "^0.1.0"
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { ActivityType, definePresence } from "@source/presence-sdk";
|
||||||
|
|
||||||
|
function findVideo(): HTMLVideoElement | null {
|
||||||
|
return document.querySelector<HTMLVideoElement>("video.html5-main-video, video");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTitle(): string | null {
|
||||||
|
const titleNode =
|
||||||
|
document.querySelector<HTMLElement>("h1.ytd-watch-metadata yt-formatted-string") ??
|
||||||
|
document.querySelector<HTMLElement>("h1.title yt-formatted-string");
|
||||||
|
const text = titleNode?.textContent?.trim();
|
||||||
|
if (text) return text;
|
||||||
|
const fallback = document.title.replace(/\s*-\s*YouTube\s*$/i, "").trim();
|
||||||
|
return fallback.length > 0 ? fallback : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readChannel(): string | null {
|
||||||
|
const node =
|
||||||
|
document.querySelector<HTMLElement>("ytd-channel-name #text a") ??
|
||||||
|
document.querySelector<HTMLElement>("#owner #channel-name a");
|
||||||
|
return node?.textContent?.trim() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePresence({
|
||||||
|
match: ["https://www.youtube.com/*", "https://music.youtube.com/*"],
|
||||||
|
tickInterval: 1000,
|
||||||
|
tick(ctx) {
|
||||||
|
const video = findVideo();
|
||||||
|
const isMusic = location.hostname === "music.youtube.com";
|
||||||
|
|
||||||
|
if (location.pathname === "/" || location.pathname === "/feed/subscriptions") {
|
||||||
|
ctx.setActivity({
|
||||||
|
type: ActivityType.WATCHING,
|
||||||
|
name: isMusic ? "YouTube Music" : "YouTube",
|
||||||
|
details: "Browsing",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!video) {
|
||||||
|
ctx.setActivity({
|
||||||
|
type: ActivityType.WATCHING,
|
||||||
|
name: isMusic ? "YouTube Music" : "YouTube",
|
||||||
|
details: document.title.slice(0, 128),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = readTitle();
|
||||||
|
if (!title) {
|
||||||
|
ctx.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = readChannel();
|
||||||
|
const paused = video.paused;
|
||||||
|
const liveBadge = document.querySelector(".ytp-live") !== null;
|
||||||
|
|
||||||
|
ctx.setActivity({
|
||||||
|
type: isMusic ? ActivityType.LISTENING : ActivityType.WATCHING,
|
||||||
|
name: isMusic ? "YouTube Music" : "YouTube",
|
||||||
|
details: title,
|
||||||
|
state: channel ? `${paused ? "Paused" : liveBadge ? "Live" : "Playing"} · ${channel}` : paused ? "Paused" : liveBadge ? "Live" : "Playing",
|
||||||
|
url: location.href,
|
||||||
|
startedAt: paused || liveBadge ? null : Date.now() - Math.floor(video.currentTime * 1000),
|
||||||
|
endsAt: paused || liveBadge || !Number.isFinite(video.duration) ? null : Date.now() + Math.floor((video.duration - video.currentTime) * 1000),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { readdir, readFile, mkdir, writeFile, stat } from "node:fs/promises";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import path from "node:path";
|
||||||
|
import * as esbuild from "esbuild";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const ROOT = path.resolve(__dirname, "..");
|
||||||
|
const PRESENCES_DIR = path.join(ROOT, "presences");
|
||||||
|
const SDK_RUNTIME = path.join(ROOT, "scripts", "sdk-runtime.mjs");
|
||||||
|
|
||||||
|
async function listPresences() {
|
||||||
|
const entries = await readdir(PRESENCES_DIR, { withFileTypes: true });
|
||||||
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readMetadata(presenceDir) {
|
||||||
|
const metaPath = path.join(presenceDir, "metadata.json");
|
||||||
|
const json = JSON.parse(await readFile(metaPath, "utf8"));
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findEntry(presenceDir) {
|
||||||
|
const candidates = ["src/presence.ts", "src/index.ts", "presence.ts", "index.ts"];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const full = path.join(presenceDir, candidate);
|
||||||
|
try {
|
||||||
|
await stat(full);
|
||||||
|
return full;
|
||||||
|
} catch {
|
||||||
|
// try next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`No entry script found in ${presenceDir} (tried ${candidates.join(", ")})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildPresence(slug) {
|
||||||
|
const presenceDir = path.join(PRESENCES_DIR, slug);
|
||||||
|
const meta = await readMetadata(presenceDir);
|
||||||
|
if (meta.id !== slug) {
|
||||||
|
throw new Error(`metadata.id "${meta.id}" does not match folder name "${slug}"`);
|
||||||
|
}
|
||||||
|
const entry = await findEntry(presenceDir);
|
||||||
|
const distDir = path.join(presenceDir, "dist");
|
||||||
|
await mkdir(distDir, { recursive: true });
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: [entry],
|
||||||
|
outfile: path.join(distDir, "presence.js"),
|
||||||
|
bundle: true,
|
||||||
|
platform: "browser",
|
||||||
|
format: "cjs",
|
||||||
|
target: ["chrome120", "firefox115"],
|
||||||
|
minify: true,
|
||||||
|
legalComments: "none",
|
||||||
|
alias: {
|
||||||
|
"@source/presence-sdk": SDK_RUNTIME,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✓ ${slug} v${meta.version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const list = await listPresences();
|
||||||
|
if (list.length === 0) {
|
||||||
|
console.log("No presences found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const slug of list) {
|
||||||
|
await buildPresence(slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { readdir, readFile, writeFile, stat } from "node:fs/promises";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const ROOT = path.resolve(__dirname, "..");
|
||||||
|
const PRESENCES_DIR = path.join(ROOT, "presences");
|
||||||
|
|
||||||
|
async function listPresences() {
|
||||||
|
const entries = await readdir(PRESENCES_DIR, { withFileTypes: true });
|
||||||
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readMetadata(presenceDir) {
|
||||||
|
return JSON.parse(await readFile(path.join(presenceDir, "metadata.json"), "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureDistExists(presenceDir, slug) {
|
||||||
|
const distFile = path.join(presenceDir, "dist", "presence.js");
|
||||||
|
try {
|
||||||
|
await stat(distFile);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`dist/presence.js missing for ${slug} — run "npm run build" first`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const slugs = await listPresences();
|
||||||
|
const entries = [];
|
||||||
|
for (const slug of slugs) {
|
||||||
|
const dir = path.join(PRESENCES_DIR, slug);
|
||||||
|
const meta = await readMetadata(dir);
|
||||||
|
await ensureDistExists(dir, slug);
|
||||||
|
entries.push({
|
||||||
|
id: meta.id,
|
||||||
|
name: meta.name,
|
||||||
|
description: meta.description,
|
||||||
|
version: meta.version,
|
||||||
|
author: meta.author,
|
||||||
|
match: meta.match,
|
||||||
|
...(meta.icon ? { icon: meta.icon } : {}),
|
||||||
|
...(typeof meta.tickInterval === "number" ? { tickInterval: meta.tickInterval } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
entries.sort((a, b) => a.id.localeCompare(b.id));
|
||||||
|
const index = {
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
presences: entries,
|
||||||
|
};
|
||||||
|
await writeFile(path.join(ROOT, "index.json"), JSON.stringify(index, null, 2) + "\n");
|
||||||
|
console.log(`Wrote index.json (${entries.length} presence${entries.length === 1 ? "" : "s"})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Runtime stand-in for `@source/presence-sdk` when bundling presences.
|
||||||
|
// definePresence is just identity; the extension's content-script runtime
|
||||||
|
// passes a PresenceContext to lifecycle methods, so the package itself only
|
||||||
|
// needs to export constants and a pass-through helper.
|
||||||
|
|
||||||
|
export const ActivityType = Object.freeze({
|
||||||
|
PLAYING: 0,
|
||||||
|
LISTENING: 2,
|
||||||
|
WATCHING: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function definePresence(def) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||||
|
"types": [],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"paths": {
|
||||||
|
"@source/presence-sdk": ["./types/presence-sdk.d.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["presences/**/*.ts", "types/**/*.d.ts"]
|
||||||
|
}
|
||||||
Vendored
+52
@@ -0,0 +1,52 @@
|
|||||||
|
// Local copy of the @source/presence-sdk type contract used by presences in this repo.
|
||||||
|
// Keep in sync with src/presence-sdk/index.ts in the extension.
|
||||||
|
|
||||||
|
export declare const ActivityType: {
|
||||||
|
readonly PLAYING: 0;
|
||||||
|
readonly LISTENING: 2;
|
||||||
|
readonly WATCHING: 3;
|
||||||
|
};
|
||||||
|
export type ActivityTypeValue = (typeof ActivityType)[keyof typeof ActivityType];
|
||||||
|
|
||||||
|
export interface PresenceActivityInput {
|
||||||
|
type: ActivityTypeValue;
|
||||||
|
name: string;
|
||||||
|
details?: string | null;
|
||||||
|
state?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
largeImage?: string | null;
|
||||||
|
largeText?: string | null;
|
||||||
|
smallImage?: string | null;
|
||||||
|
smallText?: string | null;
|
||||||
|
startedAt?: number | null;
|
||||||
|
endsAt?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresenceStorage {
|
||||||
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
||||||
|
set(key: string, value: unknown): Promise<void>;
|
||||||
|
delete(key: string): Promise<void>;
|
||||||
|
clear(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresenceContext {
|
||||||
|
readonly url: string;
|
||||||
|
readonly tabActive: boolean;
|
||||||
|
readonly storage: PresenceStorage;
|
||||||
|
setActivity(activity: PresenceActivityInput | null): void;
|
||||||
|
clear(): void;
|
||||||
|
log(...args: unknown[]): void;
|
||||||
|
warn(...args: unknown[]): void;
|
||||||
|
error(...args: unknown[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresenceDefinition {
|
||||||
|
match: string[];
|
||||||
|
tickInterval?: number;
|
||||||
|
onLoad?: (ctx: PresenceContext) => void | Promise<void>;
|
||||||
|
tick?: (ctx: PresenceContext) => void | Promise<void>;
|
||||||
|
onUnload?: (ctx: PresenceContext) => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function definePresence(def: PresenceDefinition): PresenceDefinition;
|
||||||
|
export type { PresenceDefinition as Presence };
|
||||||
Reference in New Issue
Block a user