package reward import ( "encoding/json" "fmt" "net/http" "strconv" "strings" "github.com/wangjia/pangolin/server/internal/apierr" "github.com/wangjia/pangolin/server/internal/auth" ) // tgMsgs 是 TG bot 回执文案表(中/英双语)。key 与 ClaimResult / 失效态对应。 type tgMsgSet struct { expired, granted, already, notMember, errored string } var tgMsgsZH = tgMsgSet{ expired: "链接已失效,请回 App 重新点「验证领取」。", granted: "已到账 +3 天 Pro,感谢加入!", already: "你已领取过本奖励(+3 天),无需重复领取。", notMember: "未检测到你已加入频道 %s,请先加入频道,再回 App 点「验证领取」。", errored: "验证出错,请稍后重试。", } var tgMsgsEN = tgMsgSet{ expired: "This link has expired. Please tap \u201cVerify & claim\u201d in the app again.", granted: "+3 days of Pro credited — thanks for joining!", already: "You've already claimed this reward (+3 days) — no need to claim again.", notMember: "You don't seem to be a member of %s yet. Please join the channel first, then tap \u201cVerify & claim\u201d in the app.", errored: "Verification failed, please try again later.", } // tgMsgSetFor 按 Telegram language_code 选中/英文案表:zh* → 中文,其余(含空)→ 英文。 func tgMsgSetFor(languageCode string) tgMsgSet { if strings.HasPrefix(languageCode, "zh") { return tgMsgsZH } return tgMsgsEN } const inviteLinkBase = "https://pangolin.yanmeiai.com/i/" type Handler struct { svc *Service st *Store tgEnabled bool channel string } func NewHandler(s *Service, st *Store, tgEnabled bool, channel string) *Handler { return &Handler{svc: s, st: st, tgEnabled: tgEnabled, channel: channel} } func (h *Handler) GetInvite(w http.ResponseWriter, r *http.Request) { ctx := r.Context() uid, ok := auth.UserIDFromContext(ctx) if !ok { apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized) return } code, err := h.svc.EnsureCode(ctx, uid) if err != nil { apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal) return } invited, converted, earned, err := h.st.Summary(ctx, uid) if err != nil { apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal) return } joined, _ := h.st.TelegramClaimed(ctx, uid) w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(map[string]any{ "invite_code": code, "invite_link": inviteLinkBase + code, "invited": invited, "converted": converted, "earned_days": earned, "telegram": map[string]any{ "enabled": h.tgEnabled, "joined": joined, "channel": h.channel, }, }) } func (h *Handler) TelegramStart(w http.ResponseWriter, r *http.Request) { ctx := r.Context() uid, ok := auth.UserIDFromContext(ctx) if !ok { apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized) return } if !h.tgEnabled { apierr.WriteJSON(w, http.StatusNotFound, apierr.ErrNotFound) return } tok, err := h.svc.IssueTelegramToken(ctx, uid) if err != nil { apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(map[string]any{"deep_link": h.svc.TelegramDeepLink(tok)}) } // TelegramWebhook 处理 Telegram Bot API 回调:校验 secret 头 → 解析 `/start ` // → ConsumeTelegramToken → ClaimTelegram → sendMessage 回执。Telegram 只要求 200 即不重投, // 业务结果通过 sendMessage 异步告知用户。 func (h *Handler) TelegramWebhook(w http.ResponseWriter, r *http.Request) { if !h.tgEnabled || r.Header.Get("X-Telegram-Bot-Api-Secret-Token") != h.svc.WebhookSecret() { w.WriteHeader(http.StatusNotFound) return } var upd struct { Message struct { Text string `json:"text"` From struct { ID int64 `json:"id"` LanguageCode string `json:"language_code"` } `json:"from"` } `json:"message"` } _ = json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)).Decode(&upd) w.WriteHeader(http.StatusOK) // Telegram 只要 200;业务异步在下方 text, fromID := upd.Message.Text, upd.Message.From.ID if !strings.HasPrefix(text, "/start ") || fromID == 0 { return } msgs := tgMsgSetFor(upd.Message.From.LanguageCode) token := strings.TrimSpace(strings.TrimPrefix(text, "/start ")) ctx := r.Context() uid, ok, _ := h.svc.ConsumeTelegramToken(ctx, token) if !ok { h.svc.SendTelegram(ctx, fromID, msgs.expired) return } result, err := h.svc.ClaimTelegram(ctx, uid, strconv.FormatInt(fromID, 10)) switch { case err != nil: h.svc.SendTelegram(ctx, fromID, msgs.errored) case result == ClaimGranted: h.svc.SendTelegram(ctx, fromID, msgs.granted) case result == ClaimAlready: h.svc.SendTelegram(ctx, fromID, msgs.already) default: // ClaimNotMember h.svc.SendTelegram(ctx, fromID, fmt.Sprintf(msgs.notMember, h.svc.Channel())) } }