mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
feat: add Telegram push support with multi-target routes
Add a platform-aware route target system so a single route can forward to several destinations at once (e.g. a Discord channel and a Telegram group). Route.target becomes Route.targets[] with per-entry platform, channelId/threadId for Discord and chatId/topicId for Telegram; the legacy single-target format is normalized on load and accepted by the admin API. Implement the Telegram driver with HTML rendering and Bot API sendMessage (chat_id + message_thread_id for topics, retry on 429/5xx), plus /gh commands served over POST /telegram/webhook: login, logout, comment, merge and close. The comment/merge/close commands resolve the issue or PR from the replied-to notification message. OAuth binding now stores a D1 telegram_links mapping and replies with a confirmation. Sync the Telegram webhook from the scheduled trigger via setWebhook.
This commit is contained in:
parent
dcbe93be91
commit
bd7a8f2632
39 changed files with 1165 additions and 162 deletions
|
|
@ -74,27 +74,48 @@
|
|||
</button>
|
||||
<div class="err">{{ filterError }}</div>
|
||||
</div>
|
||||
<div class="row2">
|
||||
<div class="field">
|
||||
<label>{{ t("routeEditor.channel") }}</label>
|
||||
<input
|
||||
v-model="form.channelId"
|
||||
type="text"
|
||||
:placeholder="t('routeEditor.channelPlaceholder')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label
|
||||
>{{ t("routeEditor.thread") }}
|
||||
<span class="lbl-note">{{ t("routeEditor.threadNote") }}</span></label
|
||||
>
|
||||
<input
|
||||
v-model="form.threadId"
|
||||
type="text"
|
||||
:placeholder="t('routeEditor.threadPlaceholder')"
|
||||
/>
|
||||
<div class="field">
|
||||
<label
|
||||
>{{ t("routeEditor.targets") }}
|
||||
<span class="lbl-note">{{ t("routeEditor.targetsNote") }}</span></label
|
||||
>
|
||||
<div v-for="(tg, i) in form.targets" :key="i" class="filter-row">
|
||||
<select v-model="tg.platform">
|
||||
<option value="discord">Discord</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
</select>
|
||||
<template v-if="tg.platform === 'discord'">
|
||||
<input
|
||||
v-model="tg.channelId"
|
||||
type="text"
|
||||
:placeholder="t('routeEditor.channelPlaceholder')"
|
||||
/>
|
||||
<input
|
||||
v-model="tg.threadId"
|
||||
type="text"
|
||||
:placeholder="t('routeEditor.threadPlaceholder')"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<input
|
||||
v-model="tg.chatId"
|
||||
type="text"
|
||||
:placeholder="t('routeEditor.chatPlaceholder')"
|
||||
/>
|
||||
<input
|
||||
v-model="tg.topicId"
|
||||
type="text"
|
||||
:placeholder="t('routeEditor.topicPlaceholder')"
|
||||
/>
|
||||
</template>
|
||||
<button type="button" class="icon-btn danger" @click="form.targets.splice(i, 1)">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-ghost add-filter" @click="addTarget">
|
||||
{{ t("routeEditor.addTarget") }}
|
||||
</button>
|
||||
<div class="err">{{ targetError }}</div>
|
||||
</div>
|
||||
<div class="err">{{ formError }}</div>
|
||||
</form>
|
||||
|
|
@ -113,7 +134,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from "vue";
|
||||
import type { Filter, Route } from "~/types";
|
||||
import type { Filter, Route, RouteTarget } from "~/types";
|
||||
import { FILTER_TYPES, fmtMatch } from "~/types";
|
||||
|
||||
interface FilterForm extends Filter {
|
||||
|
|
@ -129,16 +150,30 @@ const emit = defineEmits<{
|
|||
const { t } = useI18n();
|
||||
const isEdit = computed(() => props.route != null);
|
||||
const filterError = ref("");
|
||||
const targetError = ref("");
|
||||
const formError = ref("");
|
||||
|
||||
interface TargetForm extends RouteTarget {
|
||||
platform: "discord" | "telegram";
|
||||
}
|
||||
|
||||
function blankTarget(): TargetForm {
|
||||
return {
|
||||
platform: "discord",
|
||||
channelId: "",
|
||||
threadId: "",
|
||||
chatId: "",
|
||||
topicId: "",
|
||||
};
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
id: "",
|
||||
name: "",
|
||||
lang: "",
|
||||
enabled: true,
|
||||
fallback: false,
|
||||
channelId: "",
|
||||
threadId: "",
|
||||
targets: [] as TargetForm[],
|
||||
filters: [] as FilterForm[],
|
||||
});
|
||||
|
||||
|
|
@ -160,6 +195,11 @@ function addFilter(): void {
|
|||
filterError.value = "";
|
||||
}
|
||||
|
||||
function addTarget(): void {
|
||||
form.targets.push(blankTarget());
|
||||
targetError.value = "";
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
|
|
@ -170,8 +210,10 @@ watch(
|
|||
form.lang = r?.lang ?? "";
|
||||
form.enabled = r?.enabled ?? true;
|
||||
form.fallback = r?.fallback ?? false;
|
||||
form.channelId = r?.target.channelId ?? "";
|
||||
form.threadId = r?.target.threadId ?? "";
|
||||
form.targets =
|
||||
r && r.targets.length
|
||||
? r.targets.map((tg) => ({ ...blankTarget(), ...tg }))
|
||||
: [blankTarget()];
|
||||
form.filters = (
|
||||
r && r.filters.length
|
||||
? r.filters
|
||||
|
|
@ -180,6 +222,7 @@ watch(
|
|||
: [{ type: "event", match: "", exclude: false }]
|
||||
).map((f) => ({ ...f, matchText: fmtMatch(f.match) })) as FilterForm[];
|
||||
filterError.value = "";
|
||||
targetError.value = "";
|
||||
formError.value = "";
|
||||
},
|
||||
);
|
||||
|
|
@ -214,6 +257,20 @@ function collect(): Route | null {
|
|||
filterError.value = t("routeEditor.errAddFilter");
|
||||
return null;
|
||||
}
|
||||
|
||||
const targets: RouteTarget[] = [];
|
||||
for (let i = 0; i < form.targets.length; i++) {
|
||||
const tg = form.targets[i]!;
|
||||
targets.push({
|
||||
platform: tg.platform,
|
||||
channelId: tg.channelId.trim() || undefined,
|
||||
threadId: tg.threadId.trim() || undefined,
|
||||
chatId: tg.chatId.trim() || undefined,
|
||||
topicId: tg.topicId.trim() || undefined,
|
||||
});
|
||||
}
|
||||
targetError.value = "";
|
||||
|
||||
return {
|
||||
id: form.id.trim(),
|
||||
name: form.name.trim(),
|
||||
|
|
@ -221,10 +278,7 @@ function collect(): Route | null {
|
|||
fallback: form.fallback || undefined,
|
||||
lang: form.lang.trim() || undefined,
|
||||
filters,
|
||||
target: {
|
||||
channelId: form.channelId.trim(),
|
||||
threadId: form.threadId.trim() || undefined,
|
||||
},
|
||||
targets,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -240,10 +294,23 @@ function save(): void {
|
|||
formError.value = t("routeEditor.errName");
|
||||
return;
|
||||
}
|
||||
if (!route.target.channelId) {
|
||||
formError.value = t("routeEditor.errChannel");
|
||||
if (!route.targets.length) {
|
||||
targetError.value = t("routeEditor.errTargets");
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < route.targets.length; i++) {
|
||||
const tg = route.targets[i]!;
|
||||
if (tg.platform === "telegram") {
|
||||
if (!tg.chatId) {
|
||||
targetError.value = t("routeEditor.errChat", { n: i + 1 });
|
||||
return;
|
||||
}
|
||||
} else if (!tg.channelId) {
|
||||
targetError.value = t("routeEditor.errChannel", { n: i + 1 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
emit("save", route);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue