mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-23 00:21:28 +00:00
chore: auto-fix lint & formatting [skip ci]
This commit is contained in:
parent
4edd468ace
commit
2e4cb24c1d
17 changed files with 386 additions and 120 deletions
|
|
@ -167,7 +167,11 @@ header {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: border-color 0.15s, background 0.15s, color 0.15s, transform 0.1s;
|
transition:
|
||||||
|
border-color 0.15s,
|
||||||
|
background 0.15s,
|
||||||
|
color 0.15s,
|
||||||
|
transform 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover {
|
.btn:hover {
|
||||||
|
|
@ -279,7 +283,9 @@ main {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 20px 22px;
|
padding: 20px 22px;
|
||||||
transition: border-color 0.15s, transform 0.15s;
|
transition:
|
||||||
|
border-color 0.15s,
|
||||||
|
transform 0.15s;
|
||||||
animation: rise 0.35s cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
animation: rise 0.35s cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -407,7 +413,9 @@ main {
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border-strong);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.18s, border-color 0.18s;
|
transition:
|
||||||
|
background 0.18s,
|
||||||
|
border-color 0.18s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch .track::after {
|
.switch .track::after {
|
||||||
|
|
@ -620,7 +628,9 @@ main {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-family: var(--ui);
|
font-family: var(--ui);
|
||||||
font-size: 13.5px;
|
font-size: 13.5px;
|
||||||
transition: border-color 0.15s, box-shadow 0.15s;
|
transition:
|
||||||
|
border-color 0.15s,
|
||||||
|
box-shadow 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field input[type="text"]:focus {
|
.field input[type="text"]:focus {
|
||||||
|
|
@ -733,7 +743,9 @@ main {
|
||||||
|
|
||||||
.toast-enter-active,
|
.toast-enter-active,
|
||||||
.toast-leave-active {
|
.toast-leave-active {
|
||||||
transition: opacity 0.22s, transform 0.22s;
|
transition:
|
||||||
|
opacity 0.22s,
|
||||||
|
transform 0.22s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-enter-from,
|
.toast-enter-from,
|
||||||
|
|
@ -782,7 +794,9 @@ main {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s, color 0.15s;
|
transition:
|
||||||
|
background 0.15s,
|
||||||
|
color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab:hover {
|
.tab:hover {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,12 @@
|
||||||
<div class="row2">
|
<div class="row2">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ t("groupEditor.name") }}</label>
|
<label>{{ t("groupEditor.name") }}</label>
|
||||||
<input v-model="form.name" type="text" :placeholder="t('groupEditor.namePlaceholder')" required />
|
<input
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('groupEditor.namePlaceholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ t("groupEditor.id") }}</label>
|
<label>{{ t("groupEditor.id") }}</label>
|
||||||
|
|
@ -28,19 +33,35 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ t("groupEditor.admins") }} <span class="lbl-note">{{ t("groupEditor.adminsNote") }}</span></label>
|
<label
|
||||||
<input v-model="form.adminIds" type="text" :placeholder="t('groupEditor.adminsPlaceholder')" />
|
>{{ t("groupEditor.admins") }}
|
||||||
|
<span class="lbl-note">{{ t("groupEditor.adminsNote") }}</span></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="form.adminIds"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('groupEditor.adminsPlaceholder')"
|
||||||
|
/>
|
||||||
<div class="hint">{{ t("groupEditor.adminsHint") }}</div>
|
<div class="hint">{{ t("groupEditor.adminsHint") }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ t("groupEditor.owners") }} <span class="lbl-note">{{ t("groupEditor.ownersNote") }}</span></label>
|
<label
|
||||||
<input v-model="form.owners" type="text" :placeholder="t('groupEditor.ownersPlaceholder')" />
|
>{{ t("groupEditor.owners") }}
|
||||||
|
<span class="lbl-note">{{ t("groupEditor.ownersNote") }}</span></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="form.owners"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('groupEditor.ownersPlaceholder')"
|
||||||
|
/>
|
||||||
<div class="hint">{{ t("groupEditor.ownersHint") }}</div>
|
<div class="hint">{{ t("groupEditor.ownersHint") }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="err">{{ formError }}</div>
|
<div class="err">{{ formError }}</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="editor-foot">
|
<div class="editor-foot">
|
||||||
<button class="btn btn-ghost" type="button" @click="close">{{ t("groupEditor.cancel") }}</button>
|
<button class="btn btn-ghost" type="button" @click="close">
|
||||||
|
{{ t("groupEditor.cancel") }}
|
||||||
|
</button>
|
||||||
<button class="btn btn-accent" type="button" :disabled="saving" @click="save">
|
<button class="btn btn-accent" type="button" :disabled="saving" @click="save">
|
||||||
{{ t("groupEditor.save") }}
|
{{ t("groupEditor.save") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -12,20 +12,38 @@
|
||||||
<span v-if="route.fallback" class="badge fallback">{{ t("route.fallback") }}</span>
|
<span v-if="route.fallback" class="badge fallback">{{ t("route.fallback") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="icon-btn" :title="t('routeEditor.editTitle')" @click="$emit('edit', route)">✎</button>
|
<button class="icon-btn" :title="t('routeEditor.editTitle')" @click="$emit('edit', route)">
|
||||||
<button class="icon-btn danger" :title="t('routeEditor.close')" @click="$emit('delete', route)">✕</button>
|
✎
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="icon-btn danger"
|
||||||
|
:title="t('routeEditor.close')"
|
||||||
|
@click="$emit('delete', route)"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chips">
|
<div class="chips">
|
||||||
<span v-for="(f, i) in route.filters" :key="i" class="chip" :class="{ exclude: f.exclude }">
|
<span v-for="(f, i) in route.filters" :key="i" class="chip" :class="{ exclude: f.exclude }">
|
||||||
<span class="f-type">{{ f.exclude ? t("routeEditor.not") + " " : "" }}{{ t("filter." + f.type) }}</span>
|
<span class="f-type"
|
||||||
|
>{{ f.exclude ? t("routeEditor.not") + " " : "" }}{{ t("filter." + f.type) }}</span
|
||||||
|
>
|
||||||
<span class="f-val">{{ fmtMatch(f.match) }}</span>
|
<span class="f-val">{{ fmtMatch(f.match) }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!route.filters.length" class="chip"><span class="f-type">{{ t("route.noFilters") }}</span></span>
|
<span v-if="!route.filters.length" class="chip"
|
||||||
|
><span class="f-type">{{ t("route.noFilters") }}</span></span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="target">
|
<div class="target">
|
||||||
<span><b>{{ t("route.channel") }}</b><code>{{ route.target.channelId }}</code></span>
|
<span
|
||||||
<span v-if="route.target.threadId"><b>{{ t("route.thread") }}</b><code>{{ route.target.threadId }}</code></span>
|
><b>{{ t("route.channel") }}</b
|
||||||
|
><code>{{ route.target.channelId }}</code></span
|
||||||
|
>
|
||||||
|
<span v-if="route.target.threadId"
|
||||||
|
><b>{{ t("route.thread") }}</b
|
||||||
|
><code>{{ route.target.threadId }}</code></span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,12 @@
|
||||||
<form class="editor-body" @submit.prevent="save">
|
<form class="editor-body" @submit.prevent="save">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ t("routeEditor.name") }}</label>
|
<label>{{ t("routeEditor.name") }}</label>
|
||||||
<input v-model="form.name" type="text" :placeholder="t('routeEditor.namePlaceholder')" required />
|
<input
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('routeEditor.namePlaceholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row2">
|
<div class="row2">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
@ -22,7 +27,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ t("routeEditor.language") }}</label>
|
<label>{{ t("routeEditor.language") }}</label>
|
||||||
<input v-model="form.lang" type="text" :placeholder="t('routeEditor.langPlaceholder')" />
|
<input
|
||||||
|
v-model="form.lang"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('routeEditor.langPlaceholder')"
|
||||||
|
/>
|
||||||
<div class="hint">{{ t("routeEditor.langHint") }}</div>
|
<div class="hint">{{ t("routeEditor.langHint") }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -32,38 +41,70 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="field inline">
|
<div class="field inline">
|
||||||
<input v-model="form.fallback" type="checkbox" />
|
<input v-model="form.fallback" type="checkbox" />
|
||||||
<span>{{ t("routeEditor.fallback") }} <span class="lbl-note">{{ t("routeEditor.fallbackHint") }}</span></span>
|
<span
|
||||||
|
>{{ t("routeEditor.fallback") }}
|
||||||
|
<span class="lbl-note">{{ t("routeEditor.fallbackHint") }}</span></span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ t("routeEditor.filters") }} <span class="lbl-note">{{ t("routeEditor.filtersNote") }}</span></label>
|
<label
|
||||||
|
>{{ t("routeEditor.filters") }}
|
||||||
|
<span class="lbl-note">{{ t("routeEditor.filtersNote") }}</span></label
|
||||||
|
>
|
||||||
<div v-for="(f, i) in form.filters" :key="i" class="filter-row">
|
<div v-for="(f, i) in form.filters" :key="i" class="filter-row">
|
||||||
<select v-model="f.type">
|
<select v-model="f.type">
|
||||||
<option v-for="ft in FILTER_TYPES" :key="ft" :value="ft">{{ t("filter." + ft) }}</option>
|
<option v-for="ft in FILTER_TYPES" :key="ft" :value="ft">
|
||||||
|
{{ t("filter." + ft) }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<input v-model="f.matchText" type="text" :placeholder="t('routeEditor.matchPlaceholder')" />
|
<input
|
||||||
|
v-model="f.matchText"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('routeEditor.matchPlaceholder')"
|
||||||
|
/>
|
||||||
<label class="inline">
|
<label class="inline">
|
||||||
<input v-model="f.exclude" type="checkbox" /><span>{{ t("routeEditor.not") }}</span>
|
<input v-model="f.exclude" type="checkbox" /><span>{{ t("routeEditor.not") }}</span>
|
||||||
</label>
|
</label>
|
||||||
<button type="button" class="icon-btn danger" @click="form.filters.splice(i, 1)">✕</button>
|
<button type="button" class="icon-btn danger" @click="form.filters.splice(i, 1)">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-ghost add-filter" @click="addFilter">{{ t("routeEditor.addFilter") }}</button>
|
<button type="button" class="btn btn-ghost add-filter" @click="addFilter">
|
||||||
|
{{ t("routeEditor.addFilter") }}
|
||||||
|
</button>
|
||||||
<div class="err">{{ filterError }}</div>
|
<div class="err">{{ filterError }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row2">
|
<div class="row2">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ t("routeEditor.channel") }}</label>
|
<label>{{ t("routeEditor.channel") }}</label>
|
||||||
<input v-model="form.channelId" type="text" :placeholder="t('routeEditor.channelPlaceholder')" required />
|
<input
|
||||||
|
v-model="form.channelId"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('routeEditor.channelPlaceholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ t("routeEditor.thread") }} <span class="lbl-note">{{ t("routeEditor.threadNote") }}</span></label>
|
<label
|
||||||
<input v-model="form.threadId" type="text" :placeholder="t('routeEditor.threadPlaceholder')" />
|
>{{ t("routeEditor.thread") }}
|
||||||
|
<span class="lbl-note">{{ t("routeEditor.threadNote") }}</span></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="form.threadId"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('routeEditor.threadPlaceholder')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="err">{{ formError }}</div>
|
<div class="err">{{ formError }}</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="editor-foot">
|
<div class="editor-foot">
|
||||||
<button class="btn btn-ghost" type="button" @click="close">{{ t("routeEditor.cancel") }}</button>
|
<button class="btn btn-ghost" type="button" @click="close">
|
||||||
<button class="btn btn-accent" type="button" :disabled="saving" @click="save">{{ t("routeEditor.save") }}</button>
|
{{ t("routeEditor.cancel") }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-accent" type="button" :disabled="saving" @click="save">
|
||||||
|
{{ t("routeEditor.save") }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
@ -131,7 +172,8 @@ watch(
|
||||||
form.fallback = r?.fallback ?? false;
|
form.fallback = r?.fallback ?? false;
|
||||||
form.channelId = r?.target.channelId ?? "";
|
form.channelId = r?.target.channelId ?? "";
|
||||||
form.threadId = r?.target.threadId ?? "";
|
form.threadId = r?.target.threadId ?? "";
|
||||||
form.filters = (r && r.filters.length
|
form.filters = (
|
||||||
|
r && r.filters.length
|
||||||
? r.filters
|
? r.filters
|
||||||
: form.fallback
|
: form.fallback
|
||||||
? []
|
? []
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="log-toolbar">
|
<div class="log-toolbar">
|
||||||
<span class="kpi-label">{{ t("logs.lastSends", { n: logs.length }) }}</span>
|
<span class="kpi-label">{{ t("logs.lastSends", { n: logs.length }) }}</span>
|
||||||
<button class="btn btn-ghost btn-sm" :disabled="loading" @click="refresh">{{ t("logs.refresh") }}</button>
|
<button class="btn btn-ghost btn-sm" :disabled="loading" @click="refresh">
|
||||||
|
{{ t("logs.refresh") }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="error" class="err">{{ error }}</p>
|
<p v-if="error" class="err">{{ error }}</p>
|
||||||
|
|
@ -17,8 +19,14 @@
|
||||||
<span class="log-time">{{ fmtTime(l.ts) }}</span>
|
<span class="log-time">{{ fmtTime(l.ts) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="log-meta">
|
<div class="log-meta">
|
||||||
<span v-if="l.repo"><b>{{ t("logs.repo") }}</b><code>{{ l.repo }}</code></span>
|
<span v-if="l.repo"
|
||||||
<span><b>{{ t("logs.target") }}</b><code>{{ l.target }}</code></span>
|
><b>{{ t("logs.repo") }}</b
|
||||||
|
><code>{{ l.repo }}</code></span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
><b>{{ t("logs.target") }}</b
|
||||||
|
><code>{{ l.target }}</code></span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!l.ok && l.error" class="log-error">{{ l.error }}</div>
|
<div v-if="!l.ok && l.error" class="log-error">{{ l.error }}</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,8 @@ const zh: Dict = {
|
||||||
"groupEditor.owners": "来源限定",
|
"groupEditor.owners": "来源限定",
|
||||||
"groupEditor.ownersNote": "(GitHub 组织 / 用户登录名,逗号分隔)",
|
"groupEditor.ownersNote": "(GitHub 组织 / 用户登录名,逗号分隔)",
|
||||||
"groupEditor.ownersPlaceholder": "my-org, some-user",
|
"groupEditor.ownersPlaceholder": "my-org, some-user",
|
||||||
"groupEditor.ownersHint": "只有来自这些组织/用户的 webhook 事件才会进入本分组的路由。留空表示不限制。",
|
"groupEditor.ownersHint":
|
||||||
|
"只有来自这些组织/用户的 webhook 事件才会进入本分组的路由。留空表示不限制。",
|
||||||
"groupEditor.cancel": "取消",
|
"groupEditor.cancel": "取消",
|
||||||
"groupEditor.save": "保存分组",
|
"groupEditor.save": "保存分组",
|
||||||
"groupEditor.errIdFormat": "ID 只能是 a-z / 0-9 / 短横线",
|
"groupEditor.errIdFormat": "ID 只能是 a-z / 0-9 / 短横线",
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,11 @@
|
||||||
<span class="route-id">{{ g.id }}</span>
|
<span class="route-id">{{ g.id }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isSuper" class="card-actions" @click.stop>
|
<div v-if="isSuper" class="card-actions" @click.stop>
|
||||||
<button class="icon-btn" :title="t('groupEditor.editTitle')" @click="openEditGroup(g)">
|
<button
|
||||||
|
class="icon-btn"
|
||||||
|
:title="t('groupEditor.editTitle')"
|
||||||
|
@click="openEditGroup(g)"
|
||||||
|
>
|
||||||
✎
|
✎
|
||||||
</button>
|
</button>
|
||||||
<button class="icon-btn danger" @click="onDeleteGroup(g)">✕</button>
|
<button class="icon-btn danger" @click="onDeleteGroup(g)">✕</button>
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ import { sendMessage } from "../discord-rest";
|
||||||
import { dispatchEvent, isGatewayEnabled } from "../discord";
|
import { dispatchEvent, isGatewayEnabled } from "../discord";
|
||||||
import type { Env, Route } from "../types";
|
import type { Env, Route } from "../types";
|
||||||
|
|
||||||
function mockFetch(
|
function mockFetch(handler: (url: string, init?: RequestInit) => Response): void {
|
||||||
handler: (url: string, init?: RequestInit) => Response,
|
|
||||||
): void {
|
|
||||||
globalThis.fetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> =>
|
globalThis.fetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> =>
|
||||||
Promise.resolve(handler(String(input), init));
|
Promise.resolve(handler(String(input), init));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,11 @@ function createMockKV(): KVNamespace {
|
||||||
delete: async (key: string) => {
|
delete: async (key: string) => {
|
||||||
store.delete(key);
|
store.delete(key);
|
||||||
},
|
},
|
||||||
list: async () => ({ keys: [...store.keys()].map((k) => ({ name: k })), list_complete: true, cacheStatus: null }),
|
list: async () => ({
|
||||||
|
keys: [...store.keys()].map((k) => ({ name: k })),
|
||||||
|
list_complete: true,
|
||||||
|
cacheStatus: null,
|
||||||
|
}),
|
||||||
} as unknown as KVNamespace;
|
} as unknown as KVNamespace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,11 @@ function createMockKV(): KVNamespace {
|
||||||
delete: async (key: string) => {
|
delete: async (key: string) => {
|
||||||
store.delete(key);
|
store.delete(key);
|
||||||
},
|
},
|
||||||
list: async () => ({ keys: [...store.keys()].map((k) => ({ name: k })), list_complete: true, cacheStatus: null }),
|
list: async () => ({
|
||||||
|
keys: [...store.keys()].map((k) => ({ name: k })),
|
||||||
|
list_complete: true,
|
||||||
|
cacheStatus: null,
|
||||||
|
}),
|
||||||
} as unknown as KVNamespace;
|
} as unknown as KVNamespace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,16 @@ export function createActionRoutes(): Hono<{ Bindings: Env }> {
|
||||||
if (!userId) return c.json({ error: "Invalid or expired token" }, 401);
|
if (!userId) return c.json({ error: "Invalid or expired token" }, 401);
|
||||||
|
|
||||||
const body = await readJson(c);
|
const body = await readJson(c);
|
||||||
const reactions = ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"] as const;
|
const reactions = [
|
||||||
|
"+1",
|
||||||
|
"-1",
|
||||||
|
"laugh",
|
||||||
|
"confused",
|
||||||
|
"heart",
|
||||||
|
"hooray",
|
||||||
|
"rocket",
|
||||||
|
"eyes",
|
||||||
|
] as const;
|
||||||
if (
|
if (
|
||||||
!body ||
|
!body ||
|
||||||
!isNonEmptyString(body.owner) ||
|
!isNonEmptyString(body.owner) ||
|
||||||
|
|
@ -164,14 +173,7 @@ export function createActionRoutes(): Hono<{ Bindings: Env }> {
|
||||||
repo: body.repo,
|
repo: body.repo,
|
||||||
issue_number: body.issueNumber,
|
issue_number: body.issueNumber,
|
||||||
content: body.reaction as
|
content: body.reaction as
|
||||||
| "+1"
|
"+1" | "-1" | "laugh" | "confused" | "heart" | "hooray" | "rocket" | "eyes",
|
||||||
| "-1"
|
|
||||||
| "laugh"
|
|
||||||
| "confused"
|
|
||||||
| "heart"
|
|
||||||
| "hooray"
|
|
||||||
| "rocket"
|
|
||||||
| "eyes",
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error({ err }, "Failed to create reaction");
|
log.error({ err }, "Failed to create reaction");
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,7 @@ import {
|
||||||
clearAdminCookie,
|
clearAdminCookie,
|
||||||
type AdminSession,
|
type AdminSession,
|
||||||
} from "./admin-session";
|
} from "./admin-session";
|
||||||
import {
|
import { loadGroups, saveGroups, resolveScope, hasAnyAccess, type AccessScope } from "./groups";
|
||||||
loadGroups,
|
|
||||||
saveGroups,
|
|
||||||
resolveScope,
|
|
||||||
hasAnyAccess,
|
|
||||||
type AccessScope,
|
|
||||||
} from "./groups";
|
|
||||||
import { getSendLog } from "./send-log";
|
import { getSendLog } from "./send-log";
|
||||||
import { log } from "./log";
|
import { log } from "./log";
|
||||||
|
|
||||||
|
|
@ -40,9 +34,7 @@ function deepEqual(a: unknown, b: unknown): boolean {
|
||||||
const ak = Object.keys(ao);
|
const ak = Object.keys(ao);
|
||||||
const bk = Object.keys(bo);
|
const bk = Object.keys(bo);
|
||||||
if (ak.length !== bk.length) return false;
|
if (ak.length !== bk.length) return false;
|
||||||
return ak.every(
|
return ak.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k]));
|
||||||
(k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -314,7 +306,10 @@ export function createAdminRoutes(): Hono<{ Bindings: Env }> {
|
||||||
const groupId = c.req.param("groupId");
|
const groupId = c.req.param("groupId");
|
||||||
const access = groupAccess(s, groupId);
|
const access = groupAccess(s, groupId);
|
||||||
if (!access.ok) {
|
if (!access.ok) {
|
||||||
return c.json({ error: access.status === 404 ? "Group not found" : "Forbidden" }, access.status);
|
return c.json(
|
||||||
|
{ error: access.status === 404 ? "Group not found" : "Forbidden" },
|
||||||
|
access.status,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const all = await loadRoutes(c.env.KV);
|
const all = await loadRoutes(c.env.KV);
|
||||||
return c.json({ group: access.group, routes: all.filter((r) => r.groupId === groupId) });
|
return c.json({ group: access.group, routes: all.filter((r) => r.groupId === groupId) });
|
||||||
|
|
@ -326,7 +321,10 @@ export function createAdminRoutes(): Hono<{ Bindings: Env }> {
|
||||||
const groupId = c.req.param("groupId");
|
const groupId = c.req.param("groupId");
|
||||||
const access = groupAccess(s, groupId);
|
const access = groupAccess(s, groupId);
|
||||||
if (!access.ok) {
|
if (!access.ok) {
|
||||||
return c.json({ error: access.status === 404 ? "Group not found" : "Forbidden" }, access.status);
|
return c.json(
|
||||||
|
{ error: access.status === 404 ? "Group not found" : "Forbidden" },
|
||||||
|
access.status,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let body: unknown;
|
let body: unknown;
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,11 @@ const APP_COMMANDS = [
|
||||||
type: COMMAND_TYPE.CHAT_INPUT,
|
type: COMMAND_TYPE.CHAT_INPUT,
|
||||||
description: "GitHub 集成",
|
description: "GitHub 集成",
|
||||||
options: [
|
options: [
|
||||||
{ type: OPTION_TYPE.SUB_COMMAND, name: "login", description: "绑定你的 GitHub 账号以用本人身份评论" },
|
{
|
||||||
|
type: OPTION_TYPE.SUB_COMMAND,
|
||||||
|
name: "login",
|
||||||
|
description: "绑定你的 GitHub 账号以用本人身份评论",
|
||||||
|
},
|
||||||
{ type: OPTION_TYPE.SUB_COMMAND, name: "logout", description: "解绑你的 GitHub 账号" },
|
{ type: OPTION_TYPE.SUB_COMMAND, name: "logout", description: "解绑你的 GitHub 账号" },
|
||||||
{
|
{
|
||||||
type: OPTION_TYPE.SUB_COMMAND_GROUP,
|
type: OPTION_TYPE.SUB_COMMAND_GROUP,
|
||||||
|
|
@ -62,7 +66,12 @@ const APP_COMMANDS = [
|
||||||
name: "add",
|
name: "add",
|
||||||
description: "在 issue/PR 下新增评论",
|
description: "在 issue/PR 下新增评论",
|
||||||
options: [
|
options: [
|
||||||
{ type: OPTION_TYPE.STRING, name: "link", description: "issue/PR 链接", required: true },
|
{
|
||||||
|
type: OPTION_TYPE.STRING,
|
||||||
|
name: "link",
|
||||||
|
description: "issue/PR 链接",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -70,7 +79,12 @@ const APP_COMMANDS = [
|
||||||
name: "edit",
|
name: "edit",
|
||||||
description: "编辑一条评论",
|
description: "编辑一条评论",
|
||||||
options: [
|
options: [
|
||||||
{ type: OPTION_TYPE.STRING, name: "link", description: "评论链接(含 #issuecomment-)", required: true },
|
{
|
||||||
|
type: OPTION_TYPE.STRING,
|
||||||
|
name: "link",
|
||||||
|
description: "评论链接(含 #issuecomment-)",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -78,7 +92,12 @@ const APP_COMMANDS = [
|
||||||
name: "del",
|
name: "del",
|
||||||
description: "删除一条评论",
|
description: "删除一条评论",
|
||||||
options: [
|
options: [
|
||||||
{ type: OPTION_TYPE.STRING, name: "link", description: "评论链接(含 #issuecomment-)", required: true },
|
{
|
||||||
|
type: OPTION_TYPE.STRING,
|
||||||
|
name: "link",
|
||||||
|
description: "评论链接(含 #issuecomment-)",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -186,7 +205,10 @@ export class DiscordGateway {
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener("error", (event) => {
|
ws.addEventListener("error", (event) => {
|
||||||
log.error({ err: String((event as ErrorEvent).message ?? event) }, "Gateway WebSocket error");
|
log.error(
|
||||||
|
{ err: String((event as ErrorEvent).message ?? event) },
|
||||||
|
"Gateway WebSocket error",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.connecting = false;
|
this.connecting = false;
|
||||||
|
|
@ -196,10 +218,7 @@ export class DiscordGateway {
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleReconnect(): void {
|
private scheduleReconnect(): void {
|
||||||
const delay = Math.min(
|
const delay = Math.min(BASE_RECONNECT_DELAY * 2 ** this.reconnectAttempt, MAX_RECONNECT_DELAY);
|
||||||
BASE_RECONNECT_DELAY * 2 ** this.reconnectAttempt,
|
|
||||||
MAX_RECONNECT_DELAY,
|
|
||||||
);
|
|
||||||
this.reconnectAttempt++;
|
this.reconnectAttempt++;
|
||||||
this.state.storage.setAlarm(Date.now() + delay);
|
this.state.storage.setAlarm(Date.now() + delay);
|
||||||
}
|
}
|
||||||
|
|
@ -277,7 +296,10 @@ export class DiscordGateway {
|
||||||
|
|
||||||
private identify(): void {
|
private identify(): void {
|
||||||
if (!this.socket || this.socket.readyState !== WebSocket.OPEN || !this.token) {
|
if (!this.socket || this.socket.readyState !== WebSocket.OPEN || !this.token) {
|
||||||
log.warn({ hasSocket: !!this.socket, readyState: this.socket?.readyState ?? null }, "Cannot identify");
|
log.warn(
|
||||||
|
{ hasSocket: !!this.socket, readyState: this.socket?.readyState ?? null },
|
||||||
|
"Cannot identify",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.info("Sending IDENTIFY");
|
log.info("Sending IDENTIFY");
|
||||||
|
|
@ -374,7 +396,14 @@ export class DiscordGateway {
|
||||||
custom_id?: string;
|
custom_id?: string;
|
||||||
message?: { id?: string };
|
message?: { id?: string };
|
||||||
};
|
};
|
||||||
return this.handleButton(id, token, userId, interaction.channel_id, data.message?.id, data.custom_id);
|
return this.handleButton(
|
||||||
|
id,
|
||||||
|
token,
|
||||||
|
userId,
|
||||||
|
interaction.channel_id,
|
||||||
|
data.message?.id,
|
||||||
|
data.custom_id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interaction.type === INTERACTION_TYPE.COMMAND) {
|
if (interaction.type === INTERACTION_TYPE.COMMAND) {
|
||||||
|
|
@ -382,14 +411,29 @@ export class DiscordGateway {
|
||||||
name?: string;
|
name?: string;
|
||||||
type?: number;
|
type?: number;
|
||||||
target_id?: string;
|
target_id?: string;
|
||||||
options?: Array<{ name: string; options?: Array<{ name: string; value?: string; options?: Array<{ name: string; value?: string }> }> }>;
|
options?: Array<{
|
||||||
resolved?: { messages?: Record<string, { embeds?: Array<{ url?: string }>; content?: string }> };
|
name: string;
|
||||||
|
options?: Array<{
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
options?: Array<{ name: string; value?: string }>;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
resolved?: {
|
||||||
|
messages?: Record<string, { embeds?: Array<{ url?: string }>; content?: string }>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Right-click (message context-menu) commands.
|
// Right-click (message context-menu) commands.
|
||||||
if (data.type === COMMAND_TYPE.MESSAGE) {
|
if (data.type === COMMAND_TYPE.MESSAGE) {
|
||||||
const op =
|
const op =
|
||||||
data.name === MSG_CMD_ADD ? "add" : data.name === MSG_CMD_EDIT ? "edit" : data.name === MSG_CMD_DEL ? "del" : null;
|
data.name === MSG_CMD_ADD
|
||||||
|
? "add"
|
||||||
|
: data.name === MSG_CMD_EDIT
|
||||||
|
? "edit"
|
||||||
|
: data.name === MSG_CMD_DEL
|
||||||
|
? "del"
|
||||||
|
: null;
|
||||||
if (!op) return;
|
if (!op) return;
|
||||||
const target = data.target_id ? data.resolved?.messages?.[data.target_id] : undefined;
|
const target = data.target_id ? data.resolved?.messages?.[data.target_id] : undefined;
|
||||||
const source = target?.embeds?.[0]?.url ?? target?.content ?? "";
|
const source = target?.embeds?.[0]?.url ?? target?.content ?? "";
|
||||||
|
|
@ -403,7 +447,14 @@ export class DiscordGateway {
|
||||||
if (top?.name === "logout") return this.cmdLogout(id, token, userId);
|
if (top?.name === "logout") return this.cmdLogout(id, token, userId);
|
||||||
if (top?.name === "comment") {
|
if (top?.name === "comment") {
|
||||||
const sub = top.options?.[0];
|
const sub = top.options?.[0];
|
||||||
const op = sub?.name === "add" ? "add" : sub?.name === "edit" ? "edit" : sub?.name === "del" ? "del" : null;
|
const op =
|
||||||
|
sub?.name === "add"
|
||||||
|
? "add"
|
||||||
|
: sub?.name === "edit"
|
||||||
|
? "edit"
|
||||||
|
: sub?.name === "del"
|
||||||
|
? "del"
|
||||||
|
: null;
|
||||||
if (!op) return;
|
if (!op) return;
|
||||||
const link = sub?.options?.find((o) => o.name === "link")?.value ?? "";
|
const link = sub?.options?.find((o) => o.name === "link")?.value ?? "";
|
||||||
return this.commentOp(id, token, userId, op, link);
|
return this.commentOp(id, token, userId, op, link);
|
||||||
|
|
@ -518,7 +569,8 @@ export class DiscordGateway {
|
||||||
private async cmdLogin(id: string, token: string, userId: string | null): Promise<void> {
|
private async cmdLogin(id: string, token: string, userId: string | null): Promise<void> {
|
||||||
if (!userId) return this.respond(id, token, "无法识别你的 Discord 账号。");
|
if (!userId) return this.respond(id, token, "无法识别你的 Discord 账号。");
|
||||||
const clientId = this.env.GITHUB_CLIENT_ID;
|
const clientId = this.env.GITHUB_CLIENT_ID;
|
||||||
if (!clientId) return this.respond(id, token, "服务器未配置 GitHub OAuth(GITHUB_CLIENT_ID)。");
|
if (!clientId)
|
||||||
|
return this.respond(id, token, "服务器未配置 GitHub OAuth(GITHUB_CLIENT_ID)。");
|
||||||
|
|
||||||
const state = crypto.randomUUID().replace(/-/g, "");
|
const state = crypto.randomUUID().replace(/-/g, "");
|
||||||
await this.env.KV.put(
|
await this.env.KV.put(
|
||||||
|
|
@ -543,7 +595,8 @@ export class DiscordGateway {
|
||||||
/** Map a GitHub op error code to a user-facing (Chinese) message. */
|
/** Map a GitHub op error code to a user-facing (Chinese) message. */
|
||||||
private errText(err: unknown): string {
|
private errText(err: unknown): string {
|
||||||
const t = err instanceof Error ? err.message : String(err);
|
const t = err instanceof Error ? err.message : String(err);
|
||||||
if (t === "GITHUB_TOKEN_EXPIRED") return "GitHub 授权已过期或无效,请重新使用 `/gh login` 绑定。";
|
if (t === "GITHUB_TOKEN_EXPIRED")
|
||||||
|
return "GitHub 授权已过期或无效,请重新使用 `/gh login` 绑定。";
|
||||||
if (t === "GITHUB_FORBIDDEN") return "GitHub 拒绝了此操作:你的账号没有权限修改/删除这条评论。";
|
if (t === "GITHUB_FORBIDDEN") return "GitHub 拒绝了此操作:你的账号没有权限修改/删除这条评论。";
|
||||||
if (t === "GITHUB_NOT_FOUND") return "找不到目标(可能评论已被删除或仓库不可访问)。";
|
if (t === "GITHUB_NOT_FOUND") return "找不到目标(可能评论已被删除或仓库不可访问)。";
|
||||||
return `操作失败:${t}`;
|
return `操作失败:${t}`;
|
||||||
|
|
@ -568,14 +621,28 @@ export class DiscordGateway {
|
||||||
|
|
||||||
if (op === "add") {
|
if (op === "add") {
|
||||||
const m = source.match(GITHUB_ISSUE_RE);
|
const m = source.match(GITHUB_ISSUE_RE);
|
||||||
if (!m) return this.respond(id, token, "找不到 issue / PR 链接(右键 issue/PR 通知,或用 link 传入链接)。");
|
if (!m)
|
||||||
return this.openCommentModal(id, token, `${MODAL_ADD}${m[1]}|${m[2]}|${m[3]}`, `评论 ${m[1]}/${m[2]}#${m[3]}`);
|
return this.respond(
|
||||||
|
id,
|
||||||
|
token,
|
||||||
|
"找不到 issue / PR 链接(右键 issue/PR 通知,或用 link 传入链接)。",
|
||||||
|
);
|
||||||
|
return this.openCommentModal(
|
||||||
|
id,
|
||||||
|
token,
|
||||||
|
`${MODAL_ADD}${m[1]}|${m[2]}|${m[3]}`,
|
||||||
|
`评论 ${m[1]}/${m[2]}#${m[3]}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// edit / del both need a specific comment id.
|
// edit / del both need a specific comment id.
|
||||||
const m = source.match(GITHUB_COMMENT_RE);
|
const m = source.match(GITHUB_COMMENT_RE);
|
||||||
if (!m) {
|
if (!m) {
|
||||||
return this.respond(id, token, "找不到评论链接(需含 `#issuecomment-...`,请右键某条评论通知,或粘贴评论链接)。");
|
return this.respond(
|
||||||
|
id,
|
||||||
|
token,
|
||||||
|
"找不到评论链接(需含 `#issuecomment-...`,请右键某条评论通知,或粘贴评论链接)。",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const [, owner, repo, commentId] = m;
|
const [, owner, repo, commentId] = m;
|
||||||
|
|
||||||
|
|
@ -591,7 +658,13 @@ export class DiscordGateway {
|
||||||
// edit: fetch current body to prefill the modal.
|
// edit: fetch current body to prefill the modal.
|
||||||
let prefill = "";
|
let prefill = "";
|
||||||
try {
|
try {
|
||||||
const { body } = await getCommentAsUser(this.env.KV, githubUserId, owner!, repo!, Number(commentId));
|
const { body } = await getCommentAsUser(
|
||||||
|
this.env.KV,
|
||||||
|
githubUserId,
|
||||||
|
owner!,
|
||||||
|
repo!,
|
||||||
|
Number(commentId),
|
||||||
|
);
|
||||||
prefill = body;
|
prefill = body;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return this.respond(id, token, this.errText(err));
|
return this.respond(id, token, this.errText(err));
|
||||||
|
|
@ -662,7 +735,8 @@ export class DiscordGateway {
|
||||||
// ghc|add|owner|repo|issueNumber
|
// ghc|add|owner|repo|issueNumber
|
||||||
if (customId.startsWith(MODAL_ADD)) {
|
if (customId.startsWith(MODAL_ADD)) {
|
||||||
const [owner, repo, number] = customId.slice(MODAL_ADD.length).split("|");
|
const [owner, repo, number] = customId.slice(MODAL_ADD.length).split("|");
|
||||||
if (!owner || !repo || !number) return this.respond(id, token, "内部错误:无法解析目标 issue。");
|
if (!owner || !repo || !number)
|
||||||
|
return this.respond(id, token, "内部错误:无法解析目标 issue。");
|
||||||
try {
|
try {
|
||||||
const { htmlUrl, login } = await commentAsUser(
|
const { htmlUrl, login } = await commentAsUser(
|
||||||
this.env.KV,
|
this.env.KV,
|
||||||
|
|
@ -681,7 +755,8 @@ export class DiscordGateway {
|
||||||
// ghc|edit|owner|repo|commentId
|
// ghc|edit|owner|repo|commentId
|
||||||
if (customId.startsWith(MODAL_EDIT)) {
|
if (customId.startsWith(MODAL_EDIT)) {
|
||||||
const [owner, repo, commentId] = customId.slice(MODAL_EDIT.length).split("|");
|
const [owner, repo, commentId] = customId.slice(MODAL_EDIT.length).split("|");
|
||||||
if (!owner || !repo || !commentId) return this.respond(id, token, "内部错误:无法解析目标评论。");
|
if (!owner || !repo || !commentId)
|
||||||
|
return this.respond(id, token, "内部错误:无法解析目标评论。");
|
||||||
try {
|
try {
|
||||||
const { htmlUrl } = await editCommentAsUser(
|
const { htmlUrl } = await editCommentAsUser(
|
||||||
this.env.KV,
|
this.env.KV,
|
||||||
|
|
|
||||||
123
src/formatter.ts
123
src/formatter.ts
|
|
@ -67,7 +67,11 @@ const WORKFLOW_CONCLUSION_EMOJI: Record<string, string> = {
|
||||||
|
|
||||||
type T = (key: string, params?: Record<string, string | number>) => string;
|
type T = (key: string, params?: Record<string, string | number>) => string;
|
||||||
|
|
||||||
export function formatEvent(route: Route, event: WebhookEvent, tr?: Translations): FormattedMessage {
|
export function formatEvent(
|
||||||
|
route: Route,
|
||||||
|
event: WebhookEvent,
|
||||||
|
tr?: Translations,
|
||||||
|
): FormattedMessage {
|
||||||
const { event: eventType, payload } = event;
|
const { event: eventType, payload } = event;
|
||||||
const repo = (payload.repository as { full_name?: string })?.full_name;
|
const repo = (payload.repository as { full_name?: string })?.full_name;
|
||||||
const sender = (payload.sender as { login?: string })?.login;
|
const sender = (payload.sender as { login?: string })?.login;
|
||||||
|
|
@ -163,7 +167,9 @@ function formatPush(
|
||||||
descriptionParts.push(t("events.push.branch_created"));
|
descriptionParts.push(t("events.push.branch_created"));
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptionParts.push(t("events.push.commits_pushed", { count, s: count !== 1 ? "s" : "", ref: ref ?? "" }));
|
descriptionParts.push(
|
||||||
|
t("events.push.commits_pushed", { count, s: count !== 1 ? "s" : "", ref: ref ?? "" }),
|
||||||
|
);
|
||||||
|
|
||||||
if (compareUrl) {
|
if (compareUrl) {
|
||||||
descriptionParts.push(t("events.push.view_comparison", { url: compareUrl }));
|
descriptionParts.push(t("events.push.view_comparison", { url: compareUrl }));
|
||||||
|
|
@ -219,7 +225,11 @@ function formatPush(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.push.title", { count, s: count !== 1 ? "s" : "", repo: repo ?? t("common.repository") }),
|
title: t("events.push.title", {
|
||||||
|
count,
|
||||||
|
s: count !== 1 ? "s" : "",
|
||||||
|
repo: repo ?? t("common.repository"),
|
||||||
|
}),
|
||||||
url: compareUrl,
|
url: compareUrl,
|
||||||
color: GITHUB_COLORS.push,
|
color: GITHUB_COLORS.push,
|
||||||
description: descriptionParts.join("\n"),
|
description: descriptionParts.join("\n"),
|
||||||
|
|
@ -331,7 +341,11 @@ function formatPullRequest(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.pr.title", { repo: repo ?? t("common.repository"), number: pr.number ?? "?", title: pr.title ?? t("common.untitled") }),
|
title: t("events.pr.title", {
|
||||||
|
repo: repo ?? t("common.repository"),
|
||||||
|
number: pr.number ?? "?",
|
||||||
|
title: pr.title ?? t("common.untitled"),
|
||||||
|
}),
|
||||||
url: pr.html_url,
|
url: pr.html_url,
|
||||||
color: GITHUB_COLORS[colorKey],
|
color: GITHUB_COLORS[colorKey],
|
||||||
description: descriptionParts.join("\n"),
|
description: descriptionParts.join("\n"),
|
||||||
|
|
@ -412,7 +426,11 @@ function formatIssues(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.issues.title", { repo: repo ?? t("common.repository"), number: issue.number ?? "?", title: issue.title ?? t("common.untitled") }),
|
title: t("events.issues.title", {
|
||||||
|
repo: repo ?? t("common.repository"),
|
||||||
|
number: issue.number ?? "?",
|
||||||
|
title: issue.title ?? t("common.untitled"),
|
||||||
|
}),
|
||||||
url: issue.html_url,
|
url: issue.html_url,
|
||||||
color: GITHUB_COLORS[colorKey],
|
color: GITHUB_COLORS[colorKey],
|
||||||
description: descriptionParts.join("\n"),
|
description: descriptionParts.join("\n"),
|
||||||
|
|
@ -449,7 +467,11 @@ function formatIssueComment(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.issue_comment.comment_on", { repo: repo ?? t("common.repository"), number: issue.number ?? "?", title: issue.title ?? t("common.untitled") }),
|
title: t("events.issue_comment.comment_on", {
|
||||||
|
repo: repo ?? t("common.repository"),
|
||||||
|
number: issue.number ?? "?",
|
||||||
|
title: issue.title ?? t("common.untitled"),
|
||||||
|
}),
|
||||||
url: comment.html_url ?? issue.html_url,
|
url: comment.html_url ?? issue.html_url,
|
||||||
color: GITHUB_COLORS.issue_comment,
|
color: GITHUB_COLORS.issue_comment,
|
||||||
description: `${t("events.issue_comment.action_comment", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
|
description: `${t("events.issue_comment.action_comment", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
|
||||||
|
|
@ -563,7 +585,13 @@ function formatRelease(
|
||||||
const emoji = action === "deleted" ? "🗑️" : isPrerelease ? "⚠️" : "🚀";
|
const emoji = action === "deleted" ? "🗑️" : isPrerelease ? "⚠️" : "🚀";
|
||||||
|
|
||||||
const descriptionParts: string[] = [];
|
const descriptionParts: string[] = [];
|
||||||
descriptionParts.push(t("events.release.action_release", { emoji, action: al, tag: release.tag_name ?? t("common.unknown") }));
|
descriptionParts.push(
|
||||||
|
t("events.release.action_release", {
|
||||||
|
emoji,
|
||||||
|
action: al,
|
||||||
|
tag: release.tag_name ?? t("common.unknown"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if (release.body) {
|
if (release.body) {
|
||||||
const truncated = release.body.slice(0, 300);
|
const truncated = release.body.slice(0, 300);
|
||||||
|
|
@ -574,7 +602,10 @@ function formatRelease(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.release.title", { name: release.name ?? release.tag_name ?? "Release", repo: repo ?? t("common.repository") }),
|
title: t("events.release.title", {
|
||||||
|
name: release.name ?? release.tag_name ?? "Release",
|
||||||
|
repo: repo ?? t("common.repository"),
|
||||||
|
}),
|
||||||
url: release.html_url,
|
url: release.html_url,
|
||||||
color: GITHUB_COLORS[colorKey],
|
color: GITHUB_COLORS[colorKey],
|
||||||
description: descriptionParts.join("\n"),
|
description: descriptionParts.join("\n"),
|
||||||
|
|
@ -622,7 +653,12 @@ function formatCreate(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.create.title", { emoji, type: refType, ref, repo: repo ?? t("common.repository") }),
|
title: t("events.create.title", {
|
||||||
|
emoji,
|
||||||
|
type: refType,
|
||||||
|
ref,
|
||||||
|
repo: repo ?? t("common.repository"),
|
||||||
|
}),
|
||||||
color: GITHUB_COLORS.create,
|
color: GITHUB_COLORS.create,
|
||||||
fields,
|
fields,
|
||||||
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
|
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
|
||||||
|
|
@ -647,7 +683,12 @@ function formatDelete(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.delete.title", { emoji, type: refType, ref, repo: repo ?? t("common.repository") }),
|
title: t("events.delete.title", {
|
||||||
|
emoji,
|
||||||
|
type: refType,
|
||||||
|
ref,
|
||||||
|
repo: repo ?? t("common.repository"),
|
||||||
|
}),
|
||||||
color: GITHUB_COLORS.delete,
|
color: GITHUB_COLORS.delete,
|
||||||
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
|
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|
@ -693,7 +734,10 @@ function formatFork(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.fork.title", { repo: repo ?? t("common.repository"), forkee: forkee?.full_name ?? t("common.unknown") }),
|
title: t("events.fork.title", {
|
||||||
|
repo: repo ?? t("common.repository"),
|
||||||
|
forkee: forkee?.full_name ?? t("common.unknown"),
|
||||||
|
}),
|
||||||
url: forkee?.html_url ?? repoUrl,
|
url: forkee?.html_url ?? repoUrl,
|
||||||
color: GITHUB_COLORS.fork,
|
color: GITHUB_COLORS.fork,
|
||||||
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
|
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
|
||||||
|
|
@ -797,7 +841,11 @@ function formatPullRequestReview(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.pr_review.title", { repo: repo ?? t("common.repository"), number: pr.number ?? "?", title: pr.title ?? t("common.untitled") }),
|
title: t("events.pr_review.title", {
|
||||||
|
repo: repo ?? t("common.repository"),
|
||||||
|
number: pr.number ?? "?",
|
||||||
|
title: pr.title ?? t("common.untitled"),
|
||||||
|
}),
|
||||||
url: review.html_url,
|
url: review.html_url,
|
||||||
color: GITHUB_COLORS[colorKey],
|
color: GITHUB_COLORS[colorKey],
|
||||||
description: descriptionParts.join("\n"),
|
description: descriptionParts.join("\n"),
|
||||||
|
|
@ -833,7 +881,10 @@ function formatPullRequestReviewComment(
|
||||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||||
|
|
||||||
if (comment.path) {
|
if (comment.path) {
|
||||||
const loc = comment.position != null ? t("events.pr_review_comment.line", { position: comment.position }) : "";
|
const loc =
|
||||||
|
comment.position != null
|
||||||
|
? t("events.pr_review_comment.line", { position: comment.position })
|
||||||
|
: "";
|
||||||
fields.push({
|
fields.push({
|
||||||
name: t("fields.file"),
|
name: t("fields.file"),
|
||||||
value: `\`${comment.path}\`${loc}`,
|
value: `\`${comment.path}\`${loc}`,
|
||||||
|
|
@ -845,7 +896,10 @@ function formatPullRequestReviewComment(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.pr_review_comment.title", { repo: repo ?? t("common.repository"), number: pr.number ?? "?" }),
|
title: t("events.pr_review_comment.title", {
|
||||||
|
repo: repo ?? t("common.repository"),
|
||||||
|
number: pr.number ?? "?",
|
||||||
|
}),
|
||||||
url: comment.html_url,
|
url: comment.html_url,
|
||||||
color: GITHUB_COLORS.pull_request_review_commented,
|
color: GITHUB_COLORS.pull_request_review_commented,
|
||||||
description: `${t("events.pr_review_comment.action_inline", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
|
description: `${t("events.pr_review_comment.action_inline", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
|
||||||
|
|
@ -889,7 +943,10 @@ function formatCommitComment(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.commit_comment.title", { sha: shortSha, repo: repo ?? t("common.repository") }),
|
title: t("events.commit_comment.title", {
|
||||||
|
sha: shortSha,
|
||||||
|
repo: repo ?? t("common.repository"),
|
||||||
|
}),
|
||||||
url: comment.html_url,
|
url: comment.html_url,
|
||||||
color: GITHUB_COLORS.commit_comment,
|
color: GITHUB_COLORS.commit_comment,
|
||||||
description: `${t("events.commit_comment.action_comment", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
|
description: `${t("events.commit_comment.action_comment", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
|
||||||
|
|
@ -1062,7 +1119,11 @@ function formatLabel(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.label.title", { emoji, action: al, name: label.name ?? t("common.unknown") }),
|
title: t("events.label.title", {
|
||||||
|
emoji,
|
||||||
|
action: al,
|
||||||
|
name: label.name ?? t("common.unknown"),
|
||||||
|
}),
|
||||||
color: GITHUB_COLORS.label,
|
color: GITHUB_COLORS.label,
|
||||||
fields: fields.length > 0 ? fields : undefined,
|
fields: fields.length > 0 ? fields : undefined,
|
||||||
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
|
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
|
||||||
|
|
@ -1133,7 +1194,11 @@ function formatMilestone(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.milestone.title", { emoji: stateEmoji, action: al, title: milestone.title ?? t("common.unknown") }),
|
title: t("events.milestone.title", {
|
||||||
|
emoji: stateEmoji,
|
||||||
|
action: al,
|
||||||
|
title: milestone.title ?? t("common.unknown"),
|
||||||
|
}),
|
||||||
url: milestone.html_url,
|
url: milestone.html_url,
|
||||||
color:
|
color:
|
||||||
milestone.state === "closed"
|
milestone.state === "closed"
|
||||||
|
|
@ -1179,13 +1244,20 @@ function formatDiscussion(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.discussion.title", { emoji: stateEmoji, number: discussion.number ?? "?", title: discussion.title ?? t("common.untitled") }),
|
title: t("events.discussion.title", {
|
||||||
|
emoji: stateEmoji,
|
||||||
|
number: discussion.number ?? "?",
|
||||||
|
title: discussion.title ?? t("common.untitled"),
|
||||||
|
}),
|
||||||
url: discussion.html_url,
|
url: discussion.html_url,
|
||||||
color:
|
color:
|
||||||
action === "answered"
|
action === "answered"
|
||||||
? GITHUB_COLORS.discussion_answered
|
? GITHUB_COLORS.discussion_answered
|
||||||
: GITHUB_COLORS.discussion_created,
|
: GITHUB_COLORS.discussion_created,
|
||||||
description: t("events.discussion.action_discussion", { action: al, category: category ? ` in **${category}**` : "" }),
|
description: t("events.discussion.action_discussion", {
|
||||||
|
action: al,
|
||||||
|
category: category ? ` in **${category}**` : "",
|
||||||
|
}),
|
||||||
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
|
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
|
|
@ -1217,7 +1289,10 @@ function formatDiscussionComment(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.discussion_comment.comment_on", { number: discussion.number ?? "?", title: discussion.title ?? t("common.untitled") }),
|
title: t("events.discussion_comment.comment_on", {
|
||||||
|
number: discussion.number ?? "?",
|
||||||
|
title: discussion.title ?? t("common.untitled"),
|
||||||
|
}),
|
||||||
url: comment.html_url,
|
url: comment.html_url,
|
||||||
color: GITHUB_COLORS.discussion_comment,
|
color: GITHUB_COLORS.discussion_comment,
|
||||||
description: `${t("events.discussion_comment.action_comment", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
|
description: `${t("events.discussion_comment.action_comment", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
|
||||||
|
|
@ -1243,7 +1318,8 @@ function formatRepository(
|
||||||
|
|
||||||
if (action === "renamed") {
|
if (action === "renamed") {
|
||||||
const changes = payload.changes as { name?: { from?: string } } | undefined;
|
const changes = payload.changes as { name?: { from?: string } } | undefined;
|
||||||
const newName = (payload.repository as { full_name?: string })?.full_name ?? t("common.unknown");
|
const newName =
|
||||||
|
(payload.repository as { full_name?: string })?.full_name ?? t("common.unknown");
|
||||||
fields.push({
|
fields.push({
|
||||||
name: t("fields.renamed"),
|
name: t("fields.renamed"),
|
||||||
value: changes?.name?.from ? `${changes.name.from} → ${newName}` : newName,
|
value: changes?.name?.from ? `${changes.name.from} → ${newName}` : newName,
|
||||||
|
|
@ -1492,7 +1568,10 @@ function formatGeneric(
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author,
|
author,
|
||||||
title: t("events.generic.title", { event: eventType, action: payload.action ? `: ${payload.action}` : "" }),
|
title: t("events.generic.title", {
|
||||||
|
event: eventType,
|
||||||
|
action: payload.action ? `: ${payload.action}` : "",
|
||||||
|
}),
|
||||||
color: GITHUB_COLORS.default,
|
color: GITHUB_COLORS.default,
|
||||||
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
|
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@ export function t(
|
||||||
translations?: Translations,
|
translations?: Translations,
|
||||||
): string {
|
): string {
|
||||||
const dict = translations ?? en;
|
const dict = translations ?? en;
|
||||||
const raw = getByPath(dict as Record<string, unknown>, key) ?? getByPath(en as Record<string, unknown>, key) ?? key;
|
const raw =
|
||||||
|
getByPath(dict as Record<string, unknown>, key) ??
|
||||||
|
getByPath(en as Record<string, unknown>, key) ??
|
||||||
|
key;
|
||||||
return params ? interpolate(raw, params) : raw;
|
return params ? interpolate(raw, params) : raw;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,7 @@ export async function recordSend(kv: KVNamespace, record: SendRecord): Promise<v
|
||||||
export async function getSendLog(kv: KVNamespace, limit = 50): Promise<SendRecord[]> {
|
export async function getSendLog(kv: KVNamespace, limit = 50): Promise<SendRecord[]> {
|
||||||
try {
|
try {
|
||||||
const list = await kv.list({ prefix: KEY_PREFIX, limit: MAX_READ });
|
const list = await kv.list({ prefix: KEY_PREFIX, limit: MAX_READ });
|
||||||
const records = await Promise.all(
|
const records = await Promise.all(list.keys.map((k) => kv.get<SendRecord>(k.name, "json")));
|
||||||
list.keys.map((k) => kv.get<SendRecord>(k.name, "json")),
|
|
||||||
);
|
|
||||||
return records
|
return records
|
||||||
.filter((r): r is SendRecord => r != null)
|
.filter((r): r is SendRecord => r != null)
|
||||||
.sort((a, b) => b.ts - a.ts)
|
.sort((a, b) => b.ts - a.ts)
|
||||||
|
|
|
||||||
|
|
@ -87,9 +87,6 @@ export async function getDiscordLink(
|
||||||
return await kv.get(`discord-link:${discordUserId}`, "text");
|
return await kv.get(`discord-link:${discordUserId}`, "text");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeDiscordLink(
|
export async function removeDiscordLink(kv: KVNamespace, discordUserId: string): Promise<void> {
|
||||||
kv: KVNamespace,
|
|
||||||
discordUserId: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await kv.delete(`discord-link:${discordUserId}`);
|
await kv.delete(`discord-link:${discordUserId}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue