fix(formatters): use raster PNG forge icons Discord renders

- GitHub footer icon now uses fluidicon.png instead of favicon.ico —
  Discord silently drops .ico embed footer icons (png/jpg/webp/gif only)
- Gitea instances use {origin}/assets/img/favicon.png (verified on
  codeberg.org and gitea.com); links are still derived from the repo URL
- tests + docs updated to the new icon URLs
This commit is contained in:
RhenCloud 2026-08-14 08:59:51 +08:00
parent c58b800987
commit c982356e0c
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
6 changed files with 25 additions and 22 deletions

View file

@ -23,7 +23,7 @@ Core pipeline: GitHub Webhook → Worker (verify + filter + format) → Discord
- Invites: single-use 7-day links (`invite:{token}`) for joining a group as admin/viewer; `ALLOW_SELF_SIGNUP=1` gives access-less users a personal group on first login (self-service SaaS entry)
- Audit log: every admin operation (logins, group/route/member/invite changes) recorded in D1 `audit_logs`; pruned by the scheduled trigger after `AUDIT_RETENTION_DAYS` (default 90)
- Group webhook log channel: optional `Group.logTarget` (Discord channel/thread or Telegram chat/topic) receives one summary message per webhook the group's routes dispatched (event, repo, delivery id, per route×target ✅/❌ outcome, green/red color); best-effort, not recorded in `send_logs`
- Per-group forge branding: optional `Group.forgeSources` (a list of `{ host, type: "github" | "gitea", name? }` the group defines itself) labels each message's footer with the first entry whose type matches the event's provider and whose host matches the repository URL's hostname (GitHub matches `github.com`, so two Gitea instances can be `git1.example.com`/`git2.example.com`); the label is the entry's optional `name` (fallback: host); links/favicons are derived from the repo URL; Discord shows the footer icon + name, Telegram a linked name
- Per-group forge branding: optional `Group.forgeSources` (a list of `{ host, type: "github" | "gitea", name? }` the group defines itself) labels each message's footer with the first entry whose type matches the event's provider and whose host matches the repository URL's hostname (GitHub matches `github.com`, so two Gitea instances can be `git1.example.com`/`git2.example.com`); the label is the entry's optional `name` (fallback: host); links are derived from the repo URL and footer icons use raster PNGs Discord renders (GitHub's `fluidicon.png`, Gitea's `/assets/img/favicon.png``.ico` favicons are silently ignored); Discord shows the footer icon + name, Telegram a linked name
- Local dev: wrangler + Miniflare
## Architecture

View file

@ -71,7 +71,7 @@ A group defines the forges it receives events from via `forgeSources`, a list of
}
```
- **Discord** — the embed footer shows the label next to the repo (`内网 Gitea · acme/widget`) with the site's favicon as the footer icon (derived from the repository URL).
- **Discord** — the embed footer shows the label next to the repo (`内网 Gitea · acme/widget`) with a site icon as the footer icon: GitHub's `fluidicon.png` or the Gitea instance's `/assets/img/favicon.png` (both raster PNGs — Discord silently ignores `.ico` favicons).
- **Telegram** — the footer line starts with the hyperlinked label (`[内网 Gitea](https://git1.example.com)`).
- Events whose repository host has no matching entry (e.g. custom webhooks, or a repo hosted elsewhere) get no label.

View file

@ -71,7 +71,7 @@
}
```
- **Discord** — embed 底部在仓库名旁显示标识(`内网 Gitea · acme/widget`),并以按仓库 URL 推导的站点 favicon 作为底部图标
- **Discord** — embed 底部在仓库名旁显示标识(`内网 Gitea · acme/widget`),并以站点图标作为底部图标GitHub 使用 `fluidicon.png`Gitea 实例使用 `/assets/img/favicon.png`(均为 Discord 可渲染的栅格 PNG——`.ico` favicon 会被 Discord 静默忽略)
- **Telegram** — 底部行以带超链接的标识开头(`[内网 Gitea](https://git1.example.com)`)。
- 仓库主机没有匹配条目的事件(如自定义 webhook或仓库托管在别处不显示标识。

View file

@ -166,8 +166,9 @@ export function senderProfileUrl(repoUrl: string | undefined, login: string): st
* The event's repository host (github.com for GitHub, the instance hostname
* for Gitea) is matched case-insensitively against the configured hosts; the
* first entry whose type and host both match wins. The footer label is the
* entry's display `name` (or its host when no name is set). Link/favicon are
* derived from the repository URL.
* entry's display `name` (or its host when no name is set). Link is derived
* from the repository URL; the footer icon uses a raster PNG Discord can
* render (GitHub's fluidicon, Gitea's /assets/img/favicon.png).
*/
export function forgeInfo(event: WebhookEvent, sources?: ForgeSource[]): NeutralForge | undefined {
const repoUrl = (event.payload.repository as { html_url?: string } | undefined)?.html_url;
@ -187,20 +188,22 @@ export function forgeInfo(event: WebhookEvent, sources?: ForgeSource[]): Neutral
if (!source) return undefined;
const label = source.name?.trim() || source.host;
const githubIcon = "https://github.com/fluidicon.png";
if (repoUrl) {
try {
const origin = new URL(repoUrl).origin;
return { name: label, url: origin, iconUrl: `${origin}/favicon.ico` };
return {
name: label,
url: origin,
iconUrl:
event.provider === "github" ? githubIcon : `${origin}/assets/img/favicon.png`,
};
} catch {
// unparseable repo URL — name only
}
}
if (event.provider === "github") {
return {
name: label,
url: "https://github.com",
iconUrl: "https://github.com/favicon.ico",
};
return { name: label, url: "https://github.com", iconUrl: githubIcon };
}
return { name: label };
}

View file

@ -91,12 +91,12 @@ describe("discord role mentions", () => {
forge: {
name: "GitHub",
url: "https://github.com",
iconUrl: "https://github.com/favicon.ico",
iconUrl: "https://github.com/fluidicon.png",
},
});
expect(out.embeds?.[0]?.footer).toEqual({
text: "GitHub · acme/widget",
icon_url: "https://github.com/favicon.ico",
icon_url: "https://github.com/fluidicon.png",
});
});
});
@ -451,7 +451,7 @@ describe("dispatchEvent fallback routing", () => {
});
expect(footers).toContainEqual({
text: "内网 Gitea · owner/repo",
icon_url: "https://git.example.com/favicon.ico",
icon_url: "https://git.example.com/assets/img/favicon.png",
});
expect(footers).toContainEqual({ text: "owner/repo" });
});

View file

@ -535,7 +535,7 @@ describe("forge source branding", () => {
).toEqual({
name: "github.com",
url: "https://github.com",
iconUrl: "https://github.com/favicon.ico",
iconUrl: "https://github.com/fluidicon.png",
});
});
@ -543,7 +543,7 @@ describe("forge source branding", () => {
expect(forgeInfo({ event: "ping", provider: "github", payload: {} }, [ghHost])).toEqual({
name: "github.com",
url: "https://github.com",
iconUrl: "https://github.com/favicon.ico",
iconUrl: "https://github.com/fluidicon.png",
});
});
@ -564,7 +564,7 @@ describe("forge source branding", () => {
).toEqual({
name: "git1.example.com",
url: "https://git1.example.com",
iconUrl: "https://git1.example.com/favicon.ico",
iconUrl: "https://git1.example.com/assets/img/favicon.png",
});
expect(
forgeInfo(
@ -578,7 +578,7 @@ describe("forge source branding", () => {
).toEqual({
name: "git2.example.com",
url: "https://git2.example.com",
iconUrl: "https://git2.example.com/favicon.ico",
iconUrl: "https://git2.example.com/assets/img/favicon.png",
});
});
@ -599,7 +599,7 @@ describe("forge source branding", () => {
).toEqual({
name: "内网 Gitea",
url: "https://git1.example.com",
iconUrl: "https://git1.example.com/favicon.ico",
iconUrl: "https://git1.example.com/assets/img/favicon.png",
});
expect(
forgeInfo(
@ -613,7 +613,7 @@ describe("forge source branding", () => {
).toEqual({
name: "git2.example.com",
url: "https://git2.example.com",
iconUrl: "https://git2.example.com/favicon.ico",
iconUrl: "https://git2.example.com/assets/img/favicon.png",
});
expect(
forgeInfo(
@ -627,7 +627,7 @@ describe("forge source branding", () => {
).toEqual({
name: "git1.example.com",
url: "https://git1.example.com",
iconUrl: "https://git1.example.com/favicon.ico",
iconUrl: "https://git1.example.com/assets/img/favicon.png",
});
});
@ -644,7 +644,7 @@ describe("forge source branding", () => {
).toEqual({
name: "Git1.Example.com",
url: "https://git1.example.com",
iconUrl: "https://git1.example.com/favicon.ico",
iconUrl: "https://git1.example.com/assets/img/favicon.png",
});
});