Files
echoboard/plugins/gitea-sync/index.js

170 lines
4.6 KiB
JavaScript

// gitea-sync plugin
// config: { profileUrl: "https://git.example.com/username", token: "optional" }
const ICONS = [
"IconCode", "IconBrandGit", "IconTerminal2", "IconServer", "IconDatabase",
"IconCloud", "IconRocket", "IconPuzzle", "IconCpu", "IconBug",
"IconFileCode", "IconGitBranch", "IconPackage", "IconApi", "IconBolt",
"IconBrowser", "IconDevices", "IconLock", "IconWorld", "IconSettings",
];
const COLORS = [
"#F59E0B", "#EF4444", "#3B82F6", "#10B981", "#8B5CF6",
"#EC4899", "#06B6D4", "#F97316", "#14B8A6", "#6366F1",
];
function titleCase(str) {
return str
.replace(/[-_]+/g, " ")
.replace(/([a-z])([A-Z])/g, "$1 $2")
.split(" ")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join(" ");
}
function pickRandom(arr, seed) {
let hash = 0;
for (let i = 0; i < seed.length; i++) hash = ((hash << 5) - hash + seed.charCodeAt(i)) | 0;
return arr[Math.abs(hash) % arr.length];
}
async function fetchPublicRepos(url, user, token) {
const repos = [];
let page = 1;
const headers = {};
if (token) headers["Authorization"] = `token ${token}`;
while (true) {
const res = await fetch(
`${url}/api/v1/repos/search?owner=${encodeURIComponent(user)}&limit=50&page=${page}&sort=updated`,
{ headers }
);
if (!res.ok) break;
const body = await res.json();
const data = body.data || [];
if (!data.length) break;
for (const repo of data) {
if (!repo.private && !repo.fork) {
repos.push({
name: repo.name,
description: repo.description || null,
htmlUrl: repo.html_url,
updatedAt: repo.updated_at,
});
}
}
if (data.length < 50) break;
page++;
}
return repos;
}
function parseProfileUrl(raw) {
const clean = String(raw).replace(/\/+$/, "");
const parts = clean.split("/");
const user = parts.pop();
const url = parts.join("/");
return { url, user };
}
async function syncRepos(ctx) {
const { profileUrl, token } = ctx.config;
if (!profileUrl) {
ctx.logger.warn("gitea-sync: missing profileUrl in config");
return { synced: 0, error: "missing profile URL in config" };
}
const { url, user } = parseProfileUrl(profileUrl);
if (!url || !user) {
ctx.logger.warn("gitea-sync: could not parse profile URL");
return { synced: 0, error: "invalid profile URL - expected https://instance/username" };
}
const repos = await fetchPublicRepos(url, user, token ? String(token) : null);
let created = 0;
let updated = 0;
for (const repo of repos) {
const slug = repo.name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-");
const existing = await ctx.prisma.board.findUnique({ where: { slug } });
if (existing) {
await ctx.prisma.board.update({
where: { slug },
data: { externalUrl: repo.htmlUrl, description: repo.description || existing.description },
});
updated++;
} else {
const name = titleCase(repo.name);
const iconName = pickRandom(ICONS, repo.name);
const iconColor = pickRandom(COLORS, repo.name + "color");
await ctx.prisma.board.create({
data: {
slug,
name,
description: repo.description || `Feedback for ${name}`,
externalUrl: repo.htmlUrl,
iconName,
iconColor,
},
});
created++;
}
}
const lastSync = new Date().toISOString();
await ctx.store.set("lastSync", lastSync);
await ctx.store.set("repoCount", repos.length);
ctx.logger.info({ created, updated, total: repos.length }, "gitea-sync completed");
return { synced: repos.length, created, updated };
}
export default {
routes: [
{
method: "POST",
path: "/sync",
async handler(req, reply, ctx) {
const result = await syncRepos(ctx);
reply.send(result);
},
},
{
method: "GET",
path: "/status",
async handler(_req, reply, ctx) {
const lastSync = await ctx.store.get("lastSync");
const repoCount = await ctx.store.get("repoCount");
reply.send({ lastSync, repoCount, profileUrl: ctx.config.profileUrl });
},
},
],
events: {
async post_created(data, ctx) {
ctx.logger.info({ postId: data.postId, board: data.boardSlug }, "gitea-sync: new post created");
},
},
async onEnable(ctx) {
ctx.logger.info("gitea-sync plugin enabled");
if (ctx.config.profileUrl) {
try {
await syncRepos(ctx);
} catch (err) {
ctx.logger.warn({ error: err.message }, "gitea-sync: initial sync failed");
}
}
},
async onDisable(ctx) {
ctx.logger.info("gitea-sync plugin disabled");
},
}