first commit
This commit is contained in:
671
packages/plugins/atproto/src/sandbox-entry.ts
Normal file
671
packages/plugins/atproto/src/sandbox-entry.ts
Normal file
@@ -0,0 +1,671 @@
|
||||
/**
|
||||
* Sandbox Entry Point -- AT Protocol
|
||||
*
|
||||
* Canonical plugin implementation using the standard format.
|
||||
* The bundler (tsdown) inlines all local imports from atproto.ts,
|
||||
* bluesky.ts, and standard-site.ts into a single self-contained file.
|
||||
*/
|
||||
|
||||
import { definePlugin } from "emdash";
|
||||
import type { PluginContext } from "emdash";
|
||||
|
||||
import {
|
||||
ensureSession,
|
||||
createRecord,
|
||||
putRecord,
|
||||
deleteRecord,
|
||||
rkeyFromUri,
|
||||
uploadBlob,
|
||||
requireHttp,
|
||||
} from "./atproto.js";
|
||||
import { buildBskyPost } from "./bluesky.js";
|
||||
import { buildPublication, buildDocument } from "./standard-site.js";
|
||||
|
||||
// ── Types ───────────────────────────────────────────────────────
|
||||
|
||||
interface SyndicationRecord {
|
||||
collection: string;
|
||||
contentId: string;
|
||||
atUri: string;
|
||||
atCid: string;
|
||||
bskyPostUri?: string;
|
||||
bskyPostCid?: string;
|
||||
publishedAt: string;
|
||||
lastSyncedAt: string;
|
||||
status: "synced" | "error" | "pending";
|
||||
errorMessage?: string;
|
||||
retryCount?: number;
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────
|
||||
|
||||
async function isCollectionAllowed(ctx: PluginContext, collection: string): Promise<boolean> {
|
||||
const setting = await ctx.kv.get<string>("settings:collections");
|
||||
if (!setting || setting.trim() === "") return true;
|
||||
const allowed = setting.split(",").map((s) => s.trim().toLowerCase());
|
||||
return allowed.includes(collection.toLowerCase());
|
||||
}
|
||||
|
||||
async function syndicateContent(
|
||||
ctx: PluginContext,
|
||||
collection: string,
|
||||
contentId: string,
|
||||
content: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const storageKey = `${collection}:${contentId}`;
|
||||
const existing = (await ctx.storage.records!.get(storageKey)) as SyndicationRecord | null;
|
||||
|
||||
if (existing && existing.status === "synced") {
|
||||
const syncOnUpdate = (await ctx.kv.get<boolean>("settings:syncOnUpdate")) ?? true;
|
||||
if (!syncOnUpdate) return;
|
||||
}
|
||||
|
||||
const siteUrl = await ctx.kv.get<string>("settings:siteUrl");
|
||||
if (!siteUrl) throw new Error("Site URL not configured");
|
||||
|
||||
const publicationUri = await ctx.kv.get<string>("state:publicationUri");
|
||||
if (!publicationUri)
|
||||
throw new Error("Publication record not created yet. Use Sync Publication first.");
|
||||
|
||||
const { accessJwt, did, pdsHost } = await ensureSession(ctx);
|
||||
|
||||
// Upload cover image if present
|
||||
let coverImageBlob;
|
||||
const rawCoverImage = content.cover_image as string | undefined;
|
||||
if (rawCoverImage) {
|
||||
let imageUrl = rawCoverImage;
|
||||
if (imageUrl.startsWith("/")) imageUrl = `${siteUrl}${imageUrl}`;
|
||||
|
||||
if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) {
|
||||
try {
|
||||
const http = requireHttp(ctx);
|
||||
const imageRes = await http.fetch(imageUrl);
|
||||
if (imageRes.ok) {
|
||||
const bytes = await imageRes.arrayBuffer();
|
||||
if (bytes.byteLength <= 1_000_000) {
|
||||
const mimeType = imageRes.headers.get("content-type") || "image/jpeg";
|
||||
coverImageBlob = await uploadBlob(ctx, pdsHost, accessJwt, bytes, mimeType);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.log.warn("Failed to upload cover image, skipping", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let bskyPostRef: { uri: string; cid: string } | undefined;
|
||||
|
||||
if (existing && existing.atUri) {
|
||||
const rkey = rkeyFromUri(existing.atUri);
|
||||
const doc = buildDocument({
|
||||
publicationUri,
|
||||
content,
|
||||
coverImageBlob,
|
||||
bskyPostRef:
|
||||
existing.bskyPostUri && existing.bskyPostCid
|
||||
? { uri: existing.bskyPostUri, cid: existing.bskyPostCid }
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const result = await putRecord(
|
||||
ctx,
|
||||
pdsHost,
|
||||
accessJwt,
|
||||
did,
|
||||
"site.standard.document",
|
||||
rkey,
|
||||
doc,
|
||||
);
|
||||
|
||||
await ctx.storage.records!.put(storageKey, {
|
||||
collection: existing.collection,
|
||||
contentId: existing.contentId,
|
||||
atUri: result.uri,
|
||||
atCid: result.cid,
|
||||
bskyPostUri: existing.bskyPostUri,
|
||||
bskyPostCid: existing.bskyPostCid,
|
||||
publishedAt: existing.publishedAt,
|
||||
lastSyncedAt: new Date().toISOString(),
|
||||
status: "synced",
|
||||
retryCount: 0,
|
||||
} satisfies SyndicationRecord);
|
||||
|
||||
ctx.log.info(`Updated AT Protocol document for ${collection}/${contentId}`);
|
||||
} else {
|
||||
const doc = buildDocument({ publicationUri, content, coverImageBlob });
|
||||
const result = await createRecord(ctx, pdsHost, accessJwt, did, "site.standard.document", doc);
|
||||
|
||||
const enableCrosspost = (await ctx.kv.get<boolean>("settings:enableBskyCrosspost")) ?? true;
|
||||
if (enableCrosspost) {
|
||||
try {
|
||||
const template =
|
||||
(await ctx.kv.get<string>("settings:crosspostTemplate")) || "{title}\n\n{url}";
|
||||
const langsStr = (await ctx.kv.get<string>("settings:langs")) || "en";
|
||||
const langs = langsStr
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 3);
|
||||
const post = buildBskyPost({
|
||||
template,
|
||||
content,
|
||||
siteUrl,
|
||||
thumbBlob: coverImageBlob,
|
||||
langs,
|
||||
});
|
||||
|
||||
const postResult = await createRecord(
|
||||
ctx,
|
||||
pdsHost,
|
||||
accessJwt,
|
||||
did,
|
||||
"app.bsky.feed.post",
|
||||
post,
|
||||
);
|
||||
bskyPostRef = { uri: postResult.uri, cid: postResult.cid };
|
||||
|
||||
const rkey = rkeyFromUri(result.uri);
|
||||
const updatedDoc = buildDocument({ publicationUri, content, coverImageBlob, bskyPostRef });
|
||||
await putRecord(ctx, pdsHost, accessJwt, did, "site.standard.document", rkey, updatedDoc);
|
||||
|
||||
ctx.log.info(`Cross-posted ${collection}/${contentId} to Bluesky`);
|
||||
} catch (error) {
|
||||
ctx.log.warn("Failed to cross-post to Bluesky, document still synced", error);
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.storage.records!.put(storageKey, {
|
||||
collection,
|
||||
contentId,
|
||||
atUri: result.uri,
|
||||
atCid: result.cid,
|
||||
bskyPostUri: bskyPostRef?.uri,
|
||||
bskyPostCid: bskyPostRef?.cid,
|
||||
publishedAt: (content.published_at as string) || new Date().toISOString(),
|
||||
lastSyncedAt: new Date().toISOString(),
|
||||
status: "synced",
|
||||
} satisfies SyndicationRecord);
|
||||
|
||||
ctx.log.info(`Created AT Protocol document for ${collection}/${contentId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Plugin definition ───────────────────────────────────────────
|
||||
|
||||
export default definePlugin({
|
||||
hooks: {
|
||||
"plugin:install": async (_event: unknown, ctx: PluginContext) => {
|
||||
ctx.log.info("AT Protocol plugin installed");
|
||||
},
|
||||
|
||||
"content:afterSave": {
|
||||
handler: async (
|
||||
event: { content: Record<string, unknown>; collection: string; isNew: boolean },
|
||||
ctx: PluginContext,
|
||||
) => {
|
||||
const { content, collection } = event;
|
||||
const contentId = typeof content.id === "string" ? content.id : String(content.id);
|
||||
const status = content.status as string | undefined;
|
||||
|
||||
if (status !== "published") return;
|
||||
if (!(await isCollectionAllowed(ctx, collection))) return;
|
||||
|
||||
try {
|
||||
await syndicateContent(ctx, collection, contentId, content);
|
||||
} catch (error) {
|
||||
ctx.log.error(`Failed to syndicate ${collection}/${contentId}`, error);
|
||||
|
||||
const storageKey = `${collection}:${contentId}`;
|
||||
const existing = await ctx.storage.records!.get(storageKey);
|
||||
const record = (existing as SyndicationRecord | null) || {
|
||||
collection,
|
||||
contentId,
|
||||
atUri: "",
|
||||
atCid: "",
|
||||
publishedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await ctx.storage.records!.put(storageKey, {
|
||||
...record,
|
||||
status: "error",
|
||||
lastSyncedAt: new Date().toISOString(),
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
retryCount: ((record as SyndicationRecord).retryCount || 0) + 1,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
"content:afterDelete": {
|
||||
handler: async (event: { id: string; collection: string }, ctx: PluginContext) => {
|
||||
const { id, collection } = event;
|
||||
const deleteOnUnpublish = (await ctx.kv.get<boolean>("settings:deleteOnUnpublish")) ?? true;
|
||||
if (!deleteOnUnpublish) return;
|
||||
|
||||
const storageKey = `${collection}:${id}`;
|
||||
const existing = (await ctx.storage.records!.get(storageKey)) as SyndicationRecord | null;
|
||||
if (!existing || !existing.atUri) return;
|
||||
|
||||
try {
|
||||
const { accessJwt, did, pdsHost } = await ensureSession(ctx);
|
||||
const rkey = rkeyFromUri(existing.atUri);
|
||||
await deleteRecord(ctx, pdsHost, accessJwt, did, "site.standard.document", rkey);
|
||||
|
||||
if (existing.bskyPostUri) {
|
||||
const postRkey = rkeyFromUri(existing.bskyPostUri);
|
||||
await deleteRecord(ctx, pdsHost, accessJwt, did, "app.bsky.feed.post", postRkey);
|
||||
}
|
||||
|
||||
await ctx.storage.records!.delete(storageKey);
|
||||
ctx.log.info(`Deleted AT Protocol records for ${collection}/${id}`);
|
||||
} catch (error) {
|
||||
ctx.log.error(`Failed to delete AT Protocol records for ${collection}/${id}`, error);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
"page:metadata": async (
|
||||
event: { page: { content?: { collection: string; id: string } } },
|
||||
ctx: PluginContext,
|
||||
) => {
|
||||
const pageContent = event.page.content;
|
||||
if (!pageContent) return null;
|
||||
|
||||
const storageKey = `${pageContent.collection}:${pageContent.id}`;
|
||||
const record = (await ctx.storage.records!.get(storageKey)) as SyndicationRecord | null;
|
||||
|
||||
if (!record || !record.atUri || record.status !== "synced") return null;
|
||||
|
||||
return {
|
||||
kind: "link" as const,
|
||||
rel: "site.standard.document",
|
||||
href: record.atUri,
|
||||
key: "atproto-document",
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
routes: {
|
||||
status: {
|
||||
handler: async (_routeCtx: unknown, ctx: PluginContext) => {
|
||||
try {
|
||||
const handle = await ctx.kv.get<string>("settings:handle");
|
||||
const did = await ctx.kv.get<string>("state:did");
|
||||
const pubUri = await ctx.kv.get<string>("state:publicationUri");
|
||||
const synced = await ctx.storage.records!.count({
|
||||
status: "synced",
|
||||
});
|
||||
const errors = await ctx.storage.records!.count({
|
||||
status: "error",
|
||||
});
|
||||
const pending = await ctx.storage.records!.count({
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
return {
|
||||
configured: !!handle,
|
||||
connected: !!did,
|
||||
handle: handle || null,
|
||||
did: did || null,
|
||||
publicationUri: pubUri || null,
|
||||
stats: { synced, errors, pending },
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.log.error("Failed to get status", error);
|
||||
return {
|
||||
configured: false,
|
||||
connected: false,
|
||||
handle: null,
|
||||
did: null,
|
||||
publicationUri: null,
|
||||
stats: { synced: 0, errors: 0, pending: 0 },
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
"test-connection": {
|
||||
handler: async (_routeCtx: unknown, ctx: PluginContext) => {
|
||||
try {
|
||||
const session = await ensureSession(ctx);
|
||||
return {
|
||||
success: true,
|
||||
did: session.did,
|
||||
pdsHost: session.pdsHost,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
"sync-publication": {
|
||||
handler: async (_routeCtx: unknown, ctx: PluginContext) => {
|
||||
try {
|
||||
const siteUrl = await ctx.kv.get<string>("settings:siteUrl");
|
||||
const siteName = await ctx.kv.get<string>("settings:siteName");
|
||||
if (!siteUrl || !siteName)
|
||||
return {
|
||||
success: false,
|
||||
error: "Site URL and name are required",
|
||||
};
|
||||
|
||||
const { accessJwt, did, pdsHost } = await ensureSession(ctx);
|
||||
const publication = buildPublication(siteUrl, siteName);
|
||||
const existingUri = await ctx.kv.get<string>("state:publicationUri");
|
||||
|
||||
let result;
|
||||
if (existingUri) {
|
||||
const rkey = rkeyFromUri(existingUri);
|
||||
result = await putRecord(
|
||||
ctx,
|
||||
pdsHost,
|
||||
accessJwt,
|
||||
did,
|
||||
"site.standard.publication",
|
||||
rkey,
|
||||
publication,
|
||||
);
|
||||
} else {
|
||||
result = await createRecord(
|
||||
ctx,
|
||||
pdsHost,
|
||||
accessJwt,
|
||||
did,
|
||||
"site.standard.publication",
|
||||
publication,
|
||||
);
|
||||
}
|
||||
|
||||
await ctx.kv.set("state:publicationUri", result.uri);
|
||||
await ctx.kv.set("state:publicationCid", result.cid);
|
||||
return {
|
||||
success: true,
|
||||
uri: result.uri,
|
||||
cid: result.cid,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
"recent-syncs": {
|
||||
handler: async (_routeCtx: unknown, ctx: PluginContext) => {
|
||||
try {
|
||||
const result = await ctx.storage.records!.query({
|
||||
orderBy: { lastSyncedAt: "desc" },
|
||||
limit: 20,
|
||||
});
|
||||
return {
|
||||
items: result.items.map((item: { id: string; data: unknown }) => ({
|
||||
id: item.id,
|
||||
...(item.data as SyndicationRecord),
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.log.error("Failed to get recent syncs", error);
|
||||
return { items: [] };
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
verification: {
|
||||
handler: async (_routeCtx: unknown, ctx: PluginContext) => {
|
||||
const pubUri = await ctx.kv.get<string>("state:publicationUri");
|
||||
const siteUrl = await ctx.kv.get<string>("settings:siteUrl");
|
||||
return {
|
||||
publicationUri: pubUri || null,
|
||||
siteUrl: siteUrl || null,
|
||||
wellKnownPath: "/.well-known/site.standard.publication",
|
||||
wellKnownContent: pubUri || "(not configured yet)",
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
admin: {
|
||||
handler: async (routeCtx: any, ctx: PluginContext) => {
|
||||
const interaction = routeCtx.input as {
|
||||
type: string;
|
||||
page?: string;
|
||||
action_id?: string;
|
||||
values?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
if (interaction.type === "page_load" && interaction.page === "widget:sync-status") {
|
||||
return buildSyncWidget(ctx);
|
||||
}
|
||||
if (interaction.type === "page_load" && interaction.page === "/status") {
|
||||
return buildStatusPage(ctx);
|
||||
}
|
||||
if (interaction.type === "form_submit" && interaction.action_id === "save_settings") {
|
||||
return saveSettings(ctx, interaction.values ?? {});
|
||||
}
|
||||
if (interaction.type === "block_action" && interaction.action_id === "test_connection") {
|
||||
return testConnection(ctx);
|
||||
}
|
||||
return { blocks: [] };
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ── Block Kit admin helpers ─────────────────────────────────────
|
||||
|
||||
async function buildSyncWidget(ctx: PluginContext) {
|
||||
try {
|
||||
const handle = await ctx.kv.get<string>("settings:handle");
|
||||
const did = await ctx.kv.get<string>("state:did");
|
||||
const synced = await ctx.storage.records!.count({ status: "synced" });
|
||||
const errors = await ctx.storage.records!.count({ status: "error" });
|
||||
|
||||
if (!handle) {
|
||||
return {
|
||||
blocks: [
|
||||
{ type: "context", text: "Not configured -- set your handle in AT Protocol settings." },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
blocks: [
|
||||
{
|
||||
type: "fields",
|
||||
fields: [
|
||||
{ label: "Handle", value: `@${handle}` },
|
||||
{ label: "Status", value: did ? "Connected" : "Not connected" },
|
||||
{ label: "Synced", value: String(synced) },
|
||||
{ label: "Errors", value: String(errors) },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.log.error("Failed to build sync widget", error);
|
||||
return { blocks: [{ type: "context", text: "Failed to load status" }] };
|
||||
}
|
||||
}
|
||||
|
||||
async function buildStatusPage(ctx: PluginContext) {
|
||||
try {
|
||||
const handle = await ctx.kv.get<string>("settings:handle");
|
||||
const appPassword = await ctx.kv.get<string>("settings:appPassword");
|
||||
const pdsHost = await ctx.kv.get<string>("settings:pdsHost");
|
||||
const siteUrl = await ctx.kv.get<string>("settings:siteUrl");
|
||||
const enableCrosspost = await ctx.kv.get<boolean>("settings:enableCrosspost");
|
||||
const did = await ctx.kv.get<string>("state:did");
|
||||
const pubUri = await ctx.kv.get<string>("state:publicationUri");
|
||||
|
||||
const blocks: unknown[] = [
|
||||
{ type: "header", text: "AT Protocol" },
|
||||
{
|
||||
type: "section",
|
||||
text: "Syndicate content to the AT Protocol network (Bluesky, standard.site).",
|
||||
},
|
||||
{ type: "divider" },
|
||||
];
|
||||
|
||||
if (did) {
|
||||
blocks.push({
|
||||
type: "banner",
|
||||
style: "success",
|
||||
text: `Connected as ${handle} (${did})`,
|
||||
});
|
||||
} else if (handle) {
|
||||
blocks.push({
|
||||
type: "banner",
|
||||
style: "warning",
|
||||
text: "Handle configured but not yet connected. Save settings and test the connection.",
|
||||
});
|
||||
}
|
||||
|
||||
blocks.push({
|
||||
type: "form",
|
||||
block_id: "atproto-settings",
|
||||
fields: [
|
||||
{
|
||||
type: "text_input",
|
||||
action_id: "handle",
|
||||
label: "AT Protocol Handle",
|
||||
initial_value: handle ?? "",
|
||||
},
|
||||
{ type: "secret_input", action_id: "appPassword", label: "App Password" },
|
||||
{
|
||||
type: "text_input",
|
||||
action_id: "pdsHost",
|
||||
label: "PDS Host",
|
||||
initial_value: pdsHost ?? "https://bsky.social",
|
||||
},
|
||||
{
|
||||
type: "text_input",
|
||||
action_id: "siteUrl",
|
||||
label: "Site URL",
|
||||
initial_value: siteUrl ?? "",
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
action_id: "enableCrosspost",
|
||||
label: "Cross-post to Bluesky",
|
||||
initial_value: enableCrosspost ?? false,
|
||||
},
|
||||
],
|
||||
submit: { label: "Save Settings", action_id: "save_settings" },
|
||||
});
|
||||
|
||||
blocks.push({
|
||||
type: "actions",
|
||||
elements: [
|
||||
{
|
||||
type: "button",
|
||||
text: "Test Connection",
|
||||
action_id: "test_connection",
|
||||
style: handle && appPassword ? "primary" : undefined,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (did) {
|
||||
const result = await ctx.storage.records!.query({
|
||||
orderBy: { lastSyncedAt: "desc" },
|
||||
limit: 10,
|
||||
});
|
||||
const items = result.items.map((item: { id: string; data: unknown }) => ({
|
||||
id: item.id,
|
||||
...(item.data as SyndicationRecord),
|
||||
}));
|
||||
|
||||
if (items.length > 0) {
|
||||
blocks.push(
|
||||
{ type: "divider" },
|
||||
{ type: "header", text: "Recent Syncs" },
|
||||
{
|
||||
type: "table",
|
||||
columns: [
|
||||
{ key: "collection", label: "Collection", format: "text" },
|
||||
{ key: "contentId", label: "Content", format: "code" },
|
||||
{ key: "status", label: "Status", format: "badge" },
|
||||
{ key: "lastSyncedAt", label: "Synced", format: "relative_time" },
|
||||
],
|
||||
rows: items.map((r) => ({
|
||||
collection: r.collection,
|
||||
contentId: r.contentId,
|
||||
status: r.status,
|
||||
lastSyncedAt: r.lastSyncedAt,
|
||||
})),
|
||||
emptyText: "No syncs yet",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (pubUri) {
|
||||
blocks.push(
|
||||
{ type: "divider" },
|
||||
{ type: "header", text: "Verification" },
|
||||
{
|
||||
type: "fields",
|
||||
fields: [
|
||||
{ label: "Publication URI", value: pubUri },
|
||||
{ label: "Well-known path", value: "/.well-known/site.standard.publication" },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "context",
|
||||
text: "Add this path to your site to verify ownership on the AT Protocol network.",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { blocks };
|
||||
} catch (error) {
|
||||
ctx.log.error("Failed to build status page", error);
|
||||
return { blocks: [{ type: "banner", style: "error", text: "Failed to load settings" }] };
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(ctx: PluginContext, values: Record<string, unknown>) {
|
||||
try {
|
||||
if (typeof values.handle === "string") await ctx.kv.set("settings:handle", values.handle);
|
||||
if (typeof values.appPassword === "string" && values.appPassword)
|
||||
await ctx.kv.set("settings:appPassword", values.appPassword);
|
||||
if (typeof values.pdsHost === "string") await ctx.kv.set("settings:pdsHost", values.pdsHost);
|
||||
if (typeof values.siteUrl === "string") await ctx.kv.set("settings:siteUrl", values.siteUrl);
|
||||
if (typeof values.enableCrosspost === "boolean")
|
||||
await ctx.kv.set("settings:enableCrosspost", values.enableCrosspost);
|
||||
|
||||
const page = await buildStatusPage(ctx);
|
||||
return { ...page, toast: { message: "Settings saved", type: "success" } };
|
||||
} catch (error) {
|
||||
ctx.log.error("Failed to save settings", error);
|
||||
return {
|
||||
blocks: [{ type: "banner", style: "error", text: "Failed to save settings" }],
|
||||
toast: { message: "Failed to save settings", type: "error" },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function testConnection(ctx: PluginContext) {
|
||||
try {
|
||||
const session = await ensureSession(ctx);
|
||||
const page = await buildStatusPage(ctx);
|
||||
return {
|
||||
...page,
|
||||
toast: { message: `Connected to ${session.pdsHost} as ${session.did}`, type: "success" },
|
||||
};
|
||||
} catch (error) {
|
||||
const page = await buildStatusPage(ctx);
|
||||
return {
|
||||
...page,
|
||||
toast: {
|
||||
message: `Connection failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
type: "error",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user