initial version (alpha)
This commit is contained in:
commit
d16d7a128f
57 changed files with 11087 additions and 0 deletions
418
index.ts
Normal file
418
index.ts
Normal file
|
@ -0,0 +1,418 @@
|
|||
import { adventurerNeutral } from "@dicebear/collection";
|
||||
import { createAvatar } from "@dicebear/core";
|
||||
import { $ } from "bun";
|
||||
import { finalizeEvent, getPublicKey, type NostrEvent } from "nostr-tools";
|
||||
import type { SubCloser } from "nostr-tools/abstract-pool";
|
||||
import { fetchRemoteArxlets } from "./src/arxlets";
|
||||
import { CCN } from "./src/ccns";
|
||||
import arxletDocs from "./src/pages/docs/arxlets/arxlet-docs.html";
|
||||
import homePage from "./src/pages/home/home.html";
|
||||
import { getColorFromPubkey } from "./src/utils/color";
|
||||
import { decryptEvent } from "./src/utils/encryption";
|
||||
import { loadSeenEvents, saveSeenEvent } from "./src/utils/files";
|
||||
import {
|
||||
queryRemoteEvent,
|
||||
queryRemoteRelays,
|
||||
sendUnencryptedEventToLocalRelay,
|
||||
} from "./src/utils/general";
|
||||
import { DEFAULT_PERIOD_MINUTES, RollingIndex } from "./src/rollingIndex";
|
||||
|
||||
let currentActiveSub: SubCloser | undefined;
|
||||
let currentSubInterval: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
async function restartCCN() {
|
||||
currentActiveSub?.close();
|
||||
let ccn = await CCN.getActive();
|
||||
if (!ccn) {
|
||||
const allCCNs = await CCN.list();
|
||||
if (allCCNs.length > 0) {
|
||||
await allCCNs[0]!.setActive();
|
||||
ccn = allCCNs[0]!;
|
||||
} else return;
|
||||
}
|
||||
|
||||
async function handleNewEvent(original: NostrEvent) {
|
||||
if (!ccn) return process.exit(1);
|
||||
const seenEvents = await loadSeenEvents();
|
||||
if (seenEvents.includes(original.id)) return;
|
||||
await saveSeenEvent(original);
|
||||
const keyAtTime = ccn.getPrivateKeyAt(
|
||||
RollingIndex.at(original.created_at * 1000),
|
||||
);
|
||||
const decrypted = await decryptEvent(original, keyAtTime);
|
||||
if (seenEvents.includes(decrypted.id)) return;
|
||||
await saveSeenEvent(decrypted);
|
||||
await sendUnencryptedEventToLocalRelay(decrypted);
|
||||
}
|
||||
|
||||
await $`killall -9 strfry`.nothrow().quiet();
|
||||
await ccn.writeStrfryConfig();
|
||||
const strfry = Bun.spawn([
|
||||
"strfry",
|
||||
"--config",
|
||||
ccn.strfryConfigPath,
|
||||
"relay",
|
||||
]);
|
||||
process.on("exit", () => strfry.kill());
|
||||
const allKeysForCCN = ccn.allPubkeys;
|
||||
function resetActiveSub() {
|
||||
console.log(`Setting new subscription for ${allKeysForCCN.join(", ")}`);
|
||||
currentActiveSub?.close();
|
||||
currentActiveSub = queryRemoteRelays(
|
||||
{ kinds: [1060], "#p": allKeysForCCN },
|
||||
handleNewEvent,
|
||||
);
|
||||
}
|
||||
resetActiveSub();
|
||||
currentSubInterval = setInterval(
|
||||
() => {
|
||||
resetActiveSub();
|
||||
},
|
||||
DEFAULT_PERIOD_MINUTES * 60 * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
restartCCN();
|
||||
|
||||
class CorsResponse extends Response {
|
||||
constructor(body?: BodyInit, init?: ResponseInit) {
|
||||
super(body, init);
|
||||
this.headers.set("Access-Control-Allow-Origin", "*");
|
||||
this.headers.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT");
|
||||
this.headers.set("Access-Control-Allow-Headers", "Content-Type");
|
||||
}
|
||||
|
||||
static override json(json: unknown, init?: ResponseInit) {
|
||||
const res = Response.json(json, init);
|
||||
res.headers.set("Access-Control-Allow-Origin", "*");
|
||||
res.headers.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT");
|
||||
res.headers.set("Access-Control-Allow-Headers", "Content-Type");
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
const invalidRequest = CorsResponse.json(
|
||||
{ error: "Invalid Request" },
|
||||
{ status: 400 },
|
||||
);
|
||||
|
||||
const httpServer = Bun.serve({
|
||||
routes: {
|
||||
"/": homePage,
|
||||
"/docs/arxlets": arxletDocs,
|
||||
"/api/ccns": {
|
||||
GET: async () => {
|
||||
const ccns = await CCN.list();
|
||||
return CorsResponse.json(ccns.map((x) => x.toPublicJson()));
|
||||
},
|
||||
},
|
||||
"/api/ccns/active": {
|
||||
GET: async () => {
|
||||
const ccn = await CCN.getActive();
|
||||
if (!ccn)
|
||||
return CorsResponse.json({ error: "Not found" }, { status: 404 });
|
||||
return CorsResponse.json(ccn.toPublicJson());
|
||||
},
|
||||
POST: async (req) => {
|
||||
if (!req.body) return invalidRequest;
|
||||
const body = await req.body.json();
|
||||
if (!body.pubkey) return invalidRequest;
|
||||
const ccn = await CCN.fromPublicKey(body.pubkey);
|
||||
if (!ccn) return invalidRequest;
|
||||
await ccn.setActive();
|
||||
restartCCN();
|
||||
return CorsResponse.json(ccn.toPublicJson());
|
||||
},
|
||||
},
|
||||
"/api/ccns/active/invite": {
|
||||
GET: async () => {
|
||||
const ccn = await CCN.getActive();
|
||||
if (!ccn) return invalidRequest;
|
||||
const invite = await ccn.generateInvite();
|
||||
return CorsResponse.json({
|
||||
invite,
|
||||
});
|
||||
},
|
||||
},
|
||||
"/api/ccns/new": {
|
||||
POST: async (req) => {
|
||||
if (!req.body) return invalidRequest;
|
||||
const body = await req.body.json();
|
||||
if (!body.name || !body.description) return invalidRequest;
|
||||
const ccn = await CCN.create(body.name, body.description);
|
||||
const activeCCN = await CCN.getActive();
|
||||
if (!activeCCN) {
|
||||
await ccn.setActive();
|
||||
restartCCN();
|
||||
}
|
||||
return CorsResponse.json(ccn.toPublicJson());
|
||||
},
|
||||
},
|
||||
"/api/ccns/join": {
|
||||
POST: async (req) => {
|
||||
if (!req.body) return invalidRequest;
|
||||
const body = await req.body.json();
|
||||
if (!body.name || !body.description || !body.key) return invalidRequest;
|
||||
const version = body.version ? body.version : 1;
|
||||
const startIndex = body.startIndex
|
||||
? RollingIndex.fromHex(body.startIndex)
|
||||
: RollingIndex.get();
|
||||
const ccn = await CCN.join(
|
||||
version,
|
||||
startIndex,
|
||||
body.name,
|
||||
body.description,
|
||||
new Uint8Array(body.key),
|
||||
);
|
||||
return CorsResponse.json(ccn.toPublicJson());
|
||||
},
|
||||
},
|
||||
"/api/ccns/:pubkey": async (req) => {
|
||||
const ccns = await CCN.list();
|
||||
const ccnWithPubkey = ccns.find((x) => x.publicKey === req.params.pubkey);
|
||||
if (!ccnWithPubkey)
|
||||
return CorsResponse.json({ error: "Not Found" }, { status: 404 });
|
||||
return CorsResponse.json(ccnWithPubkey.toPublicJson());
|
||||
},
|
||||
"/api/ccns/icon/:pubkey": async (req) => {
|
||||
const pubkey = req.params.pubkey;
|
||||
if (!pubkey) return invalidRequest;
|
||||
const ccn = await CCN.fromPublicKey(pubkey);
|
||||
if (!ccn) return invalidRequest;
|
||||
const avatar = ccn.getCommunityIcon();
|
||||
return new CorsResponse(avatar, {
|
||||
headers: { "Content-Type": "image/svg+xml" },
|
||||
});
|
||||
},
|
||||
"/api/ccns/name/:pubkey": async (req) => {
|
||||
const pubkey = req.params.pubkey;
|
||||
if (!pubkey) return invalidRequest;
|
||||
const ccn = await CCN.fromPublicKey(pubkey);
|
||||
if (!ccn) return invalidRequest;
|
||||
const profile = await ccn.getProfile();
|
||||
return new CorsResponse(profile.name || ccn.name);
|
||||
},
|
||||
"/api/ccns/avatar/:pubkey": async (req) => {
|
||||
const pubkey = req.params.pubkey;
|
||||
if (!pubkey) return invalidRequest;
|
||||
const ccn = await CCN.fromPublicKey(pubkey);
|
||||
if (!ccn) return invalidRequest;
|
||||
const profile = await ccn.getProfile();
|
||||
if (profile.picture) return CorsResponse.redirect(profile.picture);
|
||||
const avatar = ccn.getCommunityIcon();
|
||||
return new CorsResponse(avatar, {
|
||||
headers: { "Content-Type": "image/svg+xml" },
|
||||
});
|
||||
},
|
||||
"/api/profile/:pubkey": async (req) => {
|
||||
const pubkey = req.params.pubkey;
|
||||
if (!pubkey) return invalidRequest;
|
||||
const ccn = await CCN.getActive();
|
||||
if (!ccn) return invalidRequest;
|
||||
|
||||
const profileEvent = await ccn.getFirstEvent({
|
||||
kinds: [0],
|
||||
authors: [pubkey],
|
||||
});
|
||||
try {
|
||||
if (!profileEvent) throw "No profile";
|
||||
return new CorsResponse(profileEvent.content, {
|
||||
headers: { "Content-Type": "text/json" },
|
||||
});
|
||||
} catch {
|
||||
return CorsResponse.json(
|
||||
{ error: "profile not found" },
|
||||
{ headers: { "Content-Type": "text/json" }, status: 404 },
|
||||
);
|
||||
}
|
||||
},
|
||||
"/api/avatars/:pubkey": async (req) => {
|
||||
const pubkey = req.params.pubkey;
|
||||
|
||||
if (!pubkey) return invalidRequest;
|
||||
const ccn = await CCN.getActive();
|
||||
if (!ccn) return invalidRequest;
|
||||
const profileEvent = await ccn.getFirstEvent({
|
||||
kinds: [0],
|
||||
authors: [pubkey],
|
||||
});
|
||||
try {
|
||||
if (!profileEvent) throw "No profile";
|
||||
const content = JSON.parse(profileEvent.content);
|
||||
if (!content.picture) throw "No picture";
|
||||
return CorsResponse.redirect(content.picture);
|
||||
} catch {
|
||||
const avatar = createAvatar(adventurerNeutral, {
|
||||
seed: pubkey,
|
||||
backgroundColor: [getColorFromPubkey(pubkey)],
|
||||
});
|
||||
return new CorsResponse(avatar.toString(), {
|
||||
headers: { "Content-Type": "image/svg+xml" },
|
||||
});
|
||||
}
|
||||
},
|
||||
"/api/events": {
|
||||
POST: async (req) => {
|
||||
const ccn = await CCN.getActive();
|
||||
if (!ccn) return invalidRequest;
|
||||
if (!req.body) return invalidRequest;
|
||||
const { search, ...query } = await req.body.json();
|
||||
let events = await ccn.getEvents(query);
|
||||
if (search)
|
||||
events = events.filter(
|
||||
(e) =>
|
||||
e.content.includes(search) ||
|
||||
e.tags.some((t) => t[1]?.includes(search)),
|
||||
);
|
||||
return CorsResponse.json(events);
|
||||
},
|
||||
PUT: async (req) => {
|
||||
const ccn = await CCN.getActive();
|
||||
if (!ccn) return invalidRequest;
|
||||
if (!req.body) return invalidRequest;
|
||||
const event = await req.body.json();
|
||||
try {
|
||||
await ccn.publish(event);
|
||||
return CorsResponse.json({
|
||||
success: true,
|
||||
message: "Event published successfully",
|
||||
});
|
||||
} catch {
|
||||
return CorsResponse.json({
|
||||
success: false,
|
||||
message: "Failed to publish event",
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
"/api/events/:id": async (req) => {
|
||||
const id = req.params.id;
|
||||
if (!id) return invalidRequest;
|
||||
const ccn = await CCN.getActive();
|
||||
if (!ccn) return invalidRequest;
|
||||
const event = await ccn.getFirstEvent({ ids: [id] });
|
||||
if (!event)
|
||||
return CorsResponse.json({ error: "Event Not Found" }, { status: 404 });
|
||||
return CorsResponse.json(event);
|
||||
},
|
||||
"/api/sign": {
|
||||
POST: async (req) => {
|
||||
const ccn = await CCN.getActive();
|
||||
if (!ccn) return invalidRequest;
|
||||
const userKey = await ccn.getUserKey();
|
||||
if (!req.body) return invalidRequest;
|
||||
const event = await req.body.json();
|
||||
const signedEvent = finalizeEvent(event, userKey);
|
||||
return CorsResponse.json(signedEvent);
|
||||
},
|
||||
},
|
||||
"/api/pubkey": async () => {
|
||||
const ccn = await CCN.getActive();
|
||||
if (!ccn) return invalidRequest;
|
||||
const userKey = await ccn.getUserKey();
|
||||
return CorsResponse.json({ pubkey: getPublicKey(userKey) });
|
||||
},
|
||||
"/api/arxlets": async () => {
|
||||
const ccn = await CCN.getActive();
|
||||
if (!ccn) return invalidRequest;
|
||||
const arxlets = await ccn.getArxlets();
|
||||
return CorsResponse.json(arxlets);
|
||||
},
|
||||
"/api/arxlets/:id": async (req) => {
|
||||
const ccn = await CCN.getActive();
|
||||
if (!ccn) return invalidRequest;
|
||||
const arxlet = await ccn.getArxletById(req.params.id);
|
||||
if (!arxlet)
|
||||
return CorsResponse.json(
|
||||
{ error: "Arxlet not found" },
|
||||
{ status: 404 },
|
||||
);
|
||||
return CorsResponse.json(arxlet);
|
||||
},
|
||||
"/api/arxlets-available": async () => {
|
||||
const remoteArxlets = await fetchRemoteArxlets();
|
||||
return CorsResponse.json(remoteArxlets);
|
||||
},
|
||||
"/api/clone-remote-event/:id": async (req) => {
|
||||
const ccn = await CCN.getActive();
|
||||
if (!ccn) return invalidRequest;
|
||||
const remoteEvent = await queryRemoteEvent(req.params.id);
|
||||
if (!remoteEvent)
|
||||
return CorsResponse.json({ error: "Event not found" }, { status: 404 });
|
||||
await ccn.publish(remoteEvent);
|
||||
return CorsResponse.json(remoteEvent);
|
||||
},
|
||||
"/api/reputation/:user": async (req) => {
|
||||
const ccn = await CCN.getActive();
|
||||
if (!ccn) return invalidRequest;
|
||||
const reputation = await ccn.getReputation(req.params.user);
|
||||
return CorsResponse.json({ reputation });
|
||||
},
|
||||
"/systemapi/timezone": {
|
||||
GET: async () => {
|
||||
const timezone = (
|
||||
await $`timedatectl show --property=Timezone --value`.text()
|
||||
).trim();
|
||||
return CorsResponse.json({ timezone });
|
||||
},
|
||||
POST: async (req) => {
|
||||
if (!req.body) return invalidRequest;
|
||||
const { timezone } = await req.body.json();
|
||||
await $`sudo timedatectl set-timezone ${timezone}`; // this is fine, bun escapes it
|
||||
return CorsResponse.json({ timezone });
|
||||
},
|
||||
},
|
||||
"/systemapi/wifi": {
|
||||
GET: async () => {
|
||||
const nmcliLines = (
|
||||
await $`nmcli -f ssid,bssid,mode,freq,chan,rate,signal,security,active -t dev wifi`.text()
|
||||
)
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((l) => l.split(/(?<!\\):/));
|
||||
|
||||
const wifi = nmcliLines
|
||||
.map(
|
||||
([
|
||||
ssid,
|
||||
bssid,
|
||||
mode,
|
||||
freq,
|
||||
chan,
|
||||
rate,
|
||||
signal,
|
||||
security,
|
||||
active,
|
||||
]) => ({
|
||||
ssid,
|
||||
bssid: bssid!.replace(/\\:/g, ":"),
|
||||
mode,
|
||||
freq: parseInt(freq!, 10),
|
||||
chan: parseInt(chan!, 10),
|
||||
rate,
|
||||
signal: parseInt(signal!, 10),
|
||||
security,
|
||||
active: active === "yes",
|
||||
}),
|
||||
)
|
||||
.filter((network) => network.ssid !== "") // filter out hidden networks
|
||||
.filter((network) => network.security !== "") // filter out open networks
|
||||
.sort((a, b) => (a.active ? -1 : b.active ? 1 : b.signal - a.signal));
|
||||
|
||||
return CorsResponse.json(wifi);
|
||||
},
|
||||
POST: async (req) => {
|
||||
if (!req.body) return invalidRequest;
|
||||
const { ssid, password } = await req.body.json();
|
||||
await $`nmcli device wifi connect ${ssid} password ${password}`; // this is fine, bun escapes it
|
||||
return CorsResponse.json({ ssid });
|
||||
},
|
||||
},
|
||||
},
|
||||
fetch() {
|
||||
return new CorsResponse("Eve Lite v0.0.1");
|
||||
},
|
||||
port: 4269,
|
||||
});
|
||||
|
||||
console.log(`Listening on ${httpServer.url.host}`);
|
Loading…
Add table
Add a link
Reference in a new issue