package gzev import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" "time" ) type ServerChan struct { SendKey, SessionPath, UploadURL string HTTP *http.Client } func ServerChanFromEnv() ServerChan { path := os.Getenv("GZEV_SESSION_FILE") if path == "" { path = "/var/lib/lingniu-gzev/session.json" } return ServerChan{SendKey: strings.TrimSpace(os.Getenv("SERVERCHAN_SENDKEY")), SessionPath: path, UploadURL: os.Getenv("GZEV_SESSION_UPLOAD_URL"), HTTP: &http.Client{Timeout: 20 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}} } func (s ServerChan) Send(ctx context.Context) error { return s.send(ctx, false) } func (s ServerChan) SendTest(ctx context.Context) error { return s.send(ctx, true) } func (s ServerChan) send(ctx context.Context, test bool) error { if !regexp.MustCompile(`^SCT[A-Za-z0-9]{8,253}$`).MatchString(s.SendKey) { return errors.New("ServerChan SendKey missing or invalid") } if s.UploadURL != "https://vehicle.d.lnoneos.com/gzev/session/" { return errors.New("unsupported recovery URL") } code, err := EnsureRecoveryCode(s.SessionPath + ".recovery.json") if err != nil { return errors.New("cannot create recovery code") } title := "广州里程采集登录失效" intro := "定时采集前验证未通过,当前采集已暂停。请登录广州平台并提交新的登录状态。" if test { title = "广州里程采集推送测试" intro = "这是 Server酱推送渠道连通性测试,不表示登录状态已失效。以后采集前发现登录失效,将通过此渠道通知。" } body := fmt.Sprintf("%s\n\n[打开登录状态上传页面](%s)\n\n通知恢复码:\n\n`%s`\n\n恢复码有效期 48 小时,成功上传后失效。上传页面支持粘贴 token 或上传会话 JSON;验证通过后,下次定时采集自动继续。请勿转发恢复码。", intro, s.UploadURL, code) return s.SendMessage(ctx, title, body) } // SendMessage uses the existing configured channel without creating a recovery ticket. func (s ServerChan) SendMessage(ctx context.Context, title, body string) error { if !regexp.MustCompile(`^SCT[A-Za-z0-9]{8,253}$`).MatchString(s.SendKey) { return errors.New("ServerChan SendKey missing or invalid") } form := url.Values{"title": {title}, "desp": {body}} req, err := http.NewRequestWithContext(ctx, "POST", "https://sctapi.ftqq.com/"+s.SendKey+".send", strings.NewReader(form.Encode())) if err != nil { return errors.New("cannot create push request") } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := s.HTTP.Do(req) if err != nil { return errors.New("ServerChan transport failure; delivery status may be unknown") } defer resp.Body.Close() var result struct { Code *int `json:"code"` Data struct { Errno *int `json:"errno"` } `json:"data"` } if json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&result) != nil || resp.StatusCode != 200 || result.Code == nil { return errors.New("ServerChan did not confirm acceptance") } if *result.Code != 0 { return fmt.Errorf("ServerChan rejected push (code=%d)", *result.Code) } if result.Data.Errno != nil && *result.Data.Errno != 0 { return fmt.Errorf("ServerChan channel error (errno=%d)", *result.Data.Errno) } return nil }