mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
feat(groups): optional forge source label in message footers
- Group.forgeLabel (default off, editable per group in the console) shows the source forge on every message the group's routes dispatch - Discord embed footer: forge name next to the repo + the site's favicon as icon_url (Gitea instance's own favicon from its origin) - Telegram footer: hyperlinked site name (GitHub or Gitea hostname); custom webhooks render a plain 'Custom' label - forgeInfo() derives branding from event.provider + repository.html_url; dispatch attaches it to the NeutralMessage like mentionRoleIds - validateGroups accepts forgeLabel booleans; GroupEditor gains the toggle - tests: forgeInfo unit, discord/telegram footer render, dispatch on/off - docs: groups.md + message-format.md (en/zh), config.example.yaml, AGENTS.md - fix: wrangler.jsonc compatibility_date was an incomplete '2026-'
This commit is contained in:
parent
a5324fb0ea
commit
3dce114cec
19 changed files with 273 additions and 16 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import type { Config, WebhookEvent, Env, Route, Group, NeutralMessage } from "../types";
|
||||
import { formatEvent } from "../formatters";
|
||||
import { emojiPrefix } from "../formatters/helpers";
|
||||
import { emojiPrefix, forgeInfo } from "../formatters/helpers";
|
||||
import { matchRoute, eventOwners } from "../events/match";
|
||||
import { log } from "../lib/log";
|
||||
import { loadTranslations, t as translate, type Translations } from "../lib/i18n";
|
||||
|
|
@ -156,6 +156,9 @@ export async function dispatchEvent(
|
|||
const tr = trMap.get(group?.lang ?? "en")!;
|
||||
const showEmoji = group?.emoji !== false;
|
||||
const message = formatEvent(route, event, tr, showEmoji);
|
||||
if (group?.forgeLabel) {
|
||||
message.forge = forgeInfo(event);
|
||||
}
|
||||
if (route.discordRoleIds?.length) {
|
||||
message.mentionRoleIds = route.discordRoleIds;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ export function renderNeutralMessage(message: NeutralMessage): FormattedMessage
|
|||
|
||||
// Safety net: clamp every embed part to the Discord API limits so a single
|
||||
// oversized formatter or custom payload can never hard-fail the request.
|
||||
const footerText = cap(
|
||||
[message.forge?.name, message.footer].filter(Boolean).join(" · "),
|
||||
MAX_FOOTER,
|
||||
);
|
||||
return {
|
||||
content,
|
||||
embeds: [
|
||||
|
|
@ -61,7 +65,9 @@ export function renderNeutralMessage(message: NeutralMessage): FormattedMessage
|
|||
value: cap(f.value, MAX_FIELD_VALUE),
|
||||
inline: f.inline,
|
||||
})),
|
||||
footer: message.footer ? { text: cap(message.footer, MAX_FOOTER) } : undefined,
|
||||
footer: footerText
|
||||
? { text: footerText, icon_url: message.forge?.iconUrl }
|
||||
: undefined,
|
||||
timestamp: message.timestamp,
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -89,6 +89,10 @@ export function renderNeutralMessage(message: NeutralMessage): string {
|
|||
}
|
||||
|
||||
const meta: string[] = [];
|
||||
if (message.forge) {
|
||||
const name = mdToHtml(message.forge.name);
|
||||
meta.push(message.forge.url ? `<a href="${esc(message.forge.url)}">${name}</a>` : name);
|
||||
}
|
||||
if (message.footer) meta.push(esc(message.footer));
|
||||
const ts = formatTimestamp(message.timestamp);
|
||||
if (ts) meta.push(ts);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { NeutralMessage } from "../types";
|
||||
import type { NeutralForge, NeutralMessage, WebhookEvent } from "../types";
|
||||
import { t as translate } from "../lib/i18n";
|
||||
import type { Translations } from "../lib/i18n";
|
||||
|
||||
|
|
@ -160,3 +160,29 @@ export function senderProfileUrl(repoUrl: string | undefined, login: string): st
|
|||
}
|
||||
return `https://github.com/${login}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forge branding for an event (used when the group enables `forgeLabel`):
|
||||
* GitHub gets its brand + favicon, Gitea the instance hostname + favicon
|
||||
* derived from the repository URL, custom webhooks a plain "Custom" label.
|
||||
*/
|
||||
export function forgeInfo(event: WebhookEvent): NeutralForge | undefined {
|
||||
const repoUrl = (event.payload.repository as { html_url?: string } | undefined)?.html_url;
|
||||
switch (event.provider) {
|
||||
case "github":
|
||||
return { name: "GitHub", url: "https://github.com", iconUrl: "https://github.com/favicon.ico" };
|
||||
case "gitea": {
|
||||
if (!repoUrl) return undefined;
|
||||
try {
|
||||
const origin = new URL(repoUrl).origin;
|
||||
return { name: new URL(origin).hostname, url: origin, iconUrl: `${origin}/favicon.ico` };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
case "custom":
|
||||
return { name: "Custom" };
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,11 @@ export interface Group {
|
|||
* Defaults to true when omitted.
|
||||
*/
|
||||
emoji?: boolean;
|
||||
/**
|
||||
* Whether to show the forge source (GitHub / Gitea instance / custom) in the
|
||||
* footer of this group's messages. Defaults to false when omitted.
|
||||
*/
|
||||
forgeLabel?: boolean;
|
||||
/**
|
||||
* Message language for every route in this group (e.g. "en", "zh"; custom
|
||||
* via KV i18n:<lang>). Defaults to "en" when omitted.
|
||||
|
|
@ -165,6 +170,20 @@ export interface NeutralAuthor {
|
|||
url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The forge (source platform) that produced an event: shown in the message
|
||||
* footer when the group enables `Group.forgeLabel` so recipients can tell
|
||||
* GitHub, a Gitea instance or a custom webhook apart at a glance.
|
||||
*/
|
||||
export interface NeutralForge {
|
||||
/** Display name, e.g. "GitHub" or the Gitea instance hostname. */
|
||||
name: string;
|
||||
/** Site URL for the hyperlink (Telegram) / brand context (Discord). */
|
||||
url?: string;
|
||||
/** Site favicon used as the Discord embed footer icon. */
|
||||
iconUrl?: string;
|
||||
}
|
||||
|
||||
export interface NeutralField {
|
||||
name: string;
|
||||
value: string;
|
||||
|
|
@ -189,6 +208,11 @@ export interface NeutralMessage {
|
|||
footer?: string;
|
||||
timestamp?: string;
|
||||
actions?: NeutralAction[];
|
||||
/**
|
||||
* Forge branding set by dispatch when `Group.forgeLabel` is enabled; drivers
|
||||
* render it in the footer (Discord icon + name, Telegram linked name).
|
||||
*/
|
||||
forge?: NeutralForge;
|
||||
/**
|
||||
* Stable key identifying a message chain that should be updated in place
|
||||
* (e.g. workflow run progress). When set, subsequent events edit the
|
||||
|
|
@ -215,7 +239,7 @@ export interface FormattedMessage {
|
|||
url?: string;
|
||||
};
|
||||
fields?: Array<{ name: string; value: string; inline?: boolean }>;
|
||||
footer?: { text: string };
|
||||
footer?: { text: string; icon_url?: string };
|
||||
timestamp?: string;
|
||||
}>;
|
||||
components?: Array<{
|
||||
|
|
|
|||
|
|
@ -301,6 +301,9 @@ export function validateGroups(
|
|||
if (g.emoji !== undefined && typeof g.emoji !== "boolean") {
|
||||
return { ok: false, error: `group "${g.id}".emoji must be a boolean` };
|
||||
}
|
||||
if (g.forgeLabel !== undefined && typeof g.forgeLabel !== "boolean") {
|
||||
return { ok: false, error: `group "${g.id}".forgeLabel must be a boolean` };
|
||||
}
|
||||
if (g.lang !== undefined && typeof g.lang !== "string") {
|
||||
return { ok: false, error: `group "${g.id}".lang must be a string` };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue