initial commit

This commit is contained in:
laura 2025-11-06 21:45:58 -03:00
commit e2466b202a
Signed by: w
GPG key ID: BCD2117C99E69817
50 changed files with 4356 additions and 0 deletions

88
utils/atproto.ts Normal file
View file

@ -0,0 +1,88 @@
/**
* Copyright (c) 2025 favewa
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { withInterval } from "./temp.ts";
export interface Report {
rkey: string;
cid: string;
title: string;
excerpt: string;
content: string;
createdAt: string;
tags?: string[];
visibility: string;
date: Date;
}
async function xrpc(
method: string,
params: Record<string, string>,
): Promise<any> {
const url = new URL(`https://blacksky.app/xrpc/${method}`);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
const response = await fetch(url.toString());
if (!response.ok) {
throw new Error(`XRPC call failed: ${response.statusText}`);
}
return await response.json();
}
const rkey = (input: string) => input.slice(input.lastIndexOf("/") + 1 || 0);
export const getCachedReports: () => Report[] = await withInterval(
getPosts,
300,
);
export async function getPosts(cursor?: string): Promise<Report[]> {
const params: Record<string, string> = {
collection: "com.whtwnd.blog.entry",
repo: "acpi.at",
};
if (cursor) params.cursor = cursor;
const response = await xrpc("com.atproto.repo.listRecords", params);
if (!response) return [];
const reports = (response.records as Record<string, any>[]).map((
{ cid, uri, value },
) => ({
...value,
rkey: rkey(uri),
date: new Date(value.createdAt),
createdAt: new Date(value.createdAt).toISOString().slice(0, 10).replace(
/-/g,
".",
),
cid,
} as Report)).filter((r) => r.visibility === "author").sort((a, b) =>
b.date as any - (a.date as any)
);
return reports;
}
export async function retrieveReport(rkey: string): Promise<Report | null> {
const response = await xrpc("com.atproto.repo.getRecord", {
collection: "com.whtwnd.blog.entry",
repo: "acpi.at",
rkey,
});
if (!response) return null;
return {
...response.value,
cid: response.cid,
rkey,
} as Report;
}

35
utils/fm.ts Normal file
View file

@ -0,0 +1,35 @@
/**
* Copyright (c) 2025 favewa
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { LastClient } from "@musicorum/lastfm";
import { withInterval } from "./temp.ts";
interface Track {
name: string;
artist: string;
loved: boolean;
cover?: string;
}
const secret = Deno.env.get("FM_SECRET");
export let tracks: (() => Track[]) | undefined;
if (secret) {
const client = new LastClient(secret);
tracks = await withInterval(async () => {
const { tracks: raw } = await client.user.getRecentTracks("favewa", {
limit: 5,
extended: true,
});
return raw?.filter((x) => !x.nowPlaying).map((track) => ({
artist: track.artist.name,
name: track.name,
loved: "loved" in track ? track.loved as boolean : false,
cover: track.images.pop()?.url,
}));
}, 60);
}

25
utils/temp.ts Normal file
View file

@ -0,0 +1,25 @@
/**
* Copyright (c) 2025 favewa
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
export async function withInterval<T>(
callback: () => Promise<T>,
seconds: number,
): Promise<() => T> {
let value: T = await callback();
let timer: number | undefined;
async function tick() {
clearInterval(timer);
try {
value = await callback();
} catch (_) {
// no-op
}
timer = setInterval(tick, seconds * 1000);
}
timer = setInterval(tick, seconds * 1000);
return () => value;
}