feat(notify): 新增站级 Webhook 与用户级 Telegram 通知通道
在现有邮件通知之外补两条广播/点对点通道: - 站级 Webhook([notify] webhook_url):新提问 / 新回答 / 新追问 / 新私聊 / 新投票回复统一 POST JSON 到通用 Webhook(Discord / Slack / 企业微信机器人等)。 消息体同时带 content(Discord 直接可用)与结构化 event/title/body/link 字段。 - 用户级 Telegram([notify] telegram_bot_token + users.telegram_chat_id):箱主 自填 Chat ID,新提问 / 新投票回复点对点推送。 设计约束:通知是「尽力而为」旁路 —— 任一通道发送失败只记日志、不向上抛错, 绝不影响提问/回答的主流程;通道未配置时静默跳过。发送放在 shared.SafeGo 异步 执行,与现有邮件通知同一条路径。 6 处业务触发点接入 notify;私聊消息保留仅邮件(已有 5 分钟节流,避免广播刷屏)。 profile 页新增 Telegram Chat ID 输入框 + 四语言字典。app.sample.ini 加 [notify] 段。 测试覆盖 webhook 的 JSON 结构与未配置时的静默跳过。
This commit is contained in:
+57
-54
@@ -200,34 +200,37 @@ var enDict = map[string]string{
|
||||
"admin.export": "Export data",
|
||||
|
||||
// Admin settings (additional)
|
||||
"admin.oldPasswordHint": "Only needed when changing password; leave empty to keep it unchanged",
|
||||
"admin.newPasswordHint": "6-64 characters; leave empty to keep it unchanged",
|
||||
"admin.notifyEmail": "Email",
|
||||
"admin.useCustomFg": "Use custom foreground color (otherwise auto-matched)",
|
||||
"admin.customFgHint": "Only takes effect when enabled; otherwise the color is auto-generated",
|
||||
"admin.avatarPlaceholder": "Upload avatar (non-1:1 images will be squished)",
|
||||
"admin.backgroundPlaceholder": "Upload box background",
|
||||
"admin.qrBackgroundPlaceholder": "Upload QR background (a background that is too dark may make the QR code unreadable)",
|
||||
"admin.qrPreviewHint": "The preview updates instantly after choosing an image",
|
||||
"admin.preview": "Live preview",
|
||||
"admin.themePreset": "Preset themes",
|
||||
"admin.themeDefault": "Default",
|
||||
"admin.themeDark": "Dark",
|
||||
"admin.themeOcean": "Ocean",
|
||||
"admin.themeSunset": "Sunset",
|
||||
"admin.resetDefaults": "🔄 Reset to defaults",
|
||||
"admin.previewHint": "Adjustments are for preview only; click \"Save changes\" to apply",
|
||||
"admin.socialHint": "Check the box and fill in the profile URL (no https:// needed). Unchecked items won't be shown on your page.",
|
||||
"admin.enableAcfun": "Enable AcFun",
|
||||
"admin.enableBili": "Enable Bilibili",
|
||||
"admin.enableWeibo": "Enable Weibo",
|
||||
"admin.enableXiaohongshu": "Enable Xiaohongshu",
|
||||
"admin.enableBlog": "Enable Personal Blog",
|
||||
"admin.registerOnly": "Only allow registered users to ask me questions",
|
||||
"admin.updateHarassment": "Update anti-harassment settings",
|
||||
"admin.globalBlockWords": "Global blocked words",
|
||||
"admin.globalBlockWordsHint": "Blocked words apply to questions, chat messages sent to you, and vote submissions. This single setting is shared across the question / chat / vote panels and takes effect immediately from any of them.",
|
||||
"admin.updateBlockWords": "Save blocked words",
|
||||
"admin.oldPasswordHint": "Only needed when changing password; leave empty to keep it unchanged",
|
||||
"admin.newPasswordHint": "6-64 characters; leave empty to keep it unchanged",
|
||||
"admin.notifyEmail": "Email",
|
||||
"admin.notifyTelegram": "Telegram",
|
||||
"admin.notifyTelegramPlaceholder": "Telegram Chat ID (empty = disabled)",
|
||||
"admin.notifyTelegramHint": "Send any message to @userinfobot to get your Chat ID. Requires a site-configured Telegram Bot.",
|
||||
"admin.useCustomFg": "Use custom foreground color (otherwise auto-matched)",
|
||||
"admin.customFgHint": "Only takes effect when enabled; otherwise the color is auto-generated",
|
||||
"admin.avatarPlaceholder": "Upload avatar (non-1:1 images will be squished)",
|
||||
"admin.backgroundPlaceholder": "Upload box background",
|
||||
"admin.qrBackgroundPlaceholder": "Upload QR background (a background that is too dark may make the QR code unreadable)",
|
||||
"admin.qrPreviewHint": "The preview updates instantly after choosing an image",
|
||||
"admin.preview": "Live preview",
|
||||
"admin.themePreset": "Preset themes",
|
||||
"admin.themeDefault": "Default",
|
||||
"admin.themeDark": "Dark",
|
||||
"admin.themeOcean": "Ocean",
|
||||
"admin.themeSunset": "Sunset",
|
||||
"admin.resetDefaults": "🔄 Reset to defaults",
|
||||
"admin.previewHint": "Adjustments are for preview only; click \"Save changes\" to apply",
|
||||
"admin.socialHint": "Check the box and fill in the profile URL (no https:// needed). Unchecked items won't be shown on your page.",
|
||||
"admin.enableAcfun": "Enable AcFun",
|
||||
"admin.enableBili": "Enable Bilibili",
|
||||
"admin.enableWeibo": "Enable Weibo",
|
||||
"admin.enableXiaohongshu": "Enable Xiaohongshu",
|
||||
"admin.enableBlog": "Enable Personal Blog",
|
||||
"admin.registerOnly": "Only allow registered users to ask me questions",
|
||||
"admin.updateHarassment": "Update anti-harassment settings",
|
||||
"admin.globalBlockWords": "Global blocked words",
|
||||
"admin.globalBlockWordsHint": "Blocked words apply to questions, chat messages sent to you, and vote submissions. This single setting is shared across the question / chat / vote panels and takes effect immediately from any of them.",
|
||||
"admin.updateBlockWords": "Save blocked words",
|
||||
|
||||
// Moderation strictness: shared by questions / polls / chat. Decision rules live in internal/security/censor/policy.go.
|
||||
"admin.moderationStrictness": "Moderation strictness",
|
||||
@@ -1055,36 +1058,36 @@ var enDict = map[string]string{
|
||||
"stats.weekday.7": "Sunday",
|
||||
|
||||
// Owner insights panel (/user/insights)
|
||||
"insights.title": "Insights",
|
||||
"insights.heading": "Your insights",
|
||||
"insights.lead": "These numbers are yours alone: how many people opened your box, how many questions arrived, and how many are still waiting. Everything is aggregated — no visitor or asker is ever identified.",
|
||||
"insights.back": "Back to settings",
|
||||
"insights.entryTitle": "Insights",
|
||||
"insights.entryDesc": "See how many people visit your box and how your questions are doing",
|
||||
"insights.summary": "At a glance",
|
||||
"insights.noViews": "No visits recorded yet. Numbers will appear once someone opens your box.",
|
||||
"insights.today": "Today",
|
||||
"insights.week": "Last 7 days",
|
||||
"insights.month": "Last 30 days",
|
||||
"insights.newQuestions": "new questions",
|
||||
"insights.pv": "views",
|
||||
"insights.uv": "visitors",
|
||||
"insights.trendTitle": "Views over the last 30 days",
|
||||
"insights.trendNote": "One bar per day: the light part is page views, the dark part is unique visitors that day. Hover a bar for its views / visitors / questions.",
|
||||
"insights.legendPV": "Page views",
|
||||
"insights.legendUV": "Unique visitors (per day)",
|
||||
"insights.legendHover": "Hover for daily details",
|
||||
"insights.hourTitle": "When people show up",
|
||||
"insights.hourNote": "When questions arrived during the last 30 days. The brightest bar is your busiest hour.",
|
||||
"insights.funnelTitle": "Question handling",
|
||||
"insights.funnelNote": "All questions split into answered / read but unanswered / untouched.",
|
||||
"insights.title": "Insights",
|
||||
"insights.heading": "Your insights",
|
||||
"insights.lead": "These numbers are yours alone: how many people opened your box, how many questions arrived, and how many are still waiting. Everything is aggregated — no visitor or asker is ever identified.",
|
||||
"insights.back": "Back to settings",
|
||||
"insights.entryTitle": "Insights",
|
||||
"insights.entryDesc": "See how many people visit your box and how your questions are doing",
|
||||
"insights.summary": "At a glance",
|
||||
"insights.noViews": "No visits recorded yet. Numbers will appear once someone opens your box.",
|
||||
"insights.today": "Today",
|
||||
"insights.week": "Last 7 days",
|
||||
"insights.month": "Last 30 days",
|
||||
"insights.newQuestions": "new questions",
|
||||
"insights.pv": "views",
|
||||
"insights.uv": "visitors",
|
||||
"insights.trendTitle": "Views over the last 30 days",
|
||||
"insights.trendNote": "One bar per day: the light part is page views, the dark part is unique visitors that day. Hover a bar for its views / visitors / questions.",
|
||||
"insights.legendPV": "Page views",
|
||||
"insights.legendUV": "Unique visitors (per day)",
|
||||
"insights.legendHover": "Hover for daily details",
|
||||
"insights.hourTitle": "When people show up",
|
||||
"insights.hourNote": "When questions arrived during the last 30 days. The brightest bar is your busiest hour.",
|
||||
"insights.funnelTitle": "Question handling",
|
||||
"insights.funnelNote": "All questions split into answered / read but unanswered / untouched.",
|
||||
"insights.funnelTotal": "All questions",
|
||||
"insights.funnelAnswered": "Answered",
|
||||
"insights.funnelReadOnly": "Read, not answered",
|
||||
"insights.funnelUnread": "Not read",
|
||||
"insights.answerRate": "Answer rate: answered ÷ all questions",
|
||||
"insights.privacyTitle": "About these numbers",
|
||||
"insights.privacyNote": "Visit stats only record \"someone visited N times on a given day\". A visitor is an IP hash with a per-day salt — no IP, no user agent, no page path is stored, and the same person cannot be linked across days. The 7-day / 30-day visitor counts are therefore the sum of daily uniques: someone visiting on several days counts several times. Your own visits to your own box are excluded.",
|
||||
"insights.answerRate": "Answer rate: answered ÷ all questions",
|
||||
"insights.privacyTitle": "About these numbers",
|
||||
"insights.privacyNote": "Visit stats only record \"someone visited N times on a given day\". A visitor is an IP hash with a per-day salt — no IP, no user agent, no page path is stored, and the same person cannot be linked across days. The 7-day / 30-day visitor counts are therefore the sum of daily uniques: someone visiting on several days counts several times. Your own visits to your own box are excluded.",
|
||||
|
||||
"insights.insight.empty": "No data yet. This page comes alive once you get your first question or someone opens your box.",
|
||||
"insights.insight.weekViews": "%s views from %s visitors in the last 7 days.",
|
||||
|
||||
Reference in New Issue
Block a user