feat: 增加 SafeW 已监控群列表接口
通过 getUpdates 将群写入 Redis,供创建/编辑渠道时选择 chat_id,避免前端重复传递 token。
This commit is contained in:
+181
-1
@@ -90,11 +90,73 @@ func (s *SafeWSender) Send(title, content string, config json.RawMessage) error
|
||||
}
|
||||
|
||||
func (s *SafeWSender) endpoint(token string) string {
|
||||
return s.methodURL(token, "sendMessage")
|
||||
}
|
||||
|
||||
func (s *SafeWSender) methodURL(token, method string) string {
|
||||
base := s.apiBase
|
||||
if base == "" {
|
||||
base = safewAPIBase
|
||||
}
|
||||
return strings.TrimRight(base, "/") + "/bot" + token + "/sendMessage"
|
||||
return strings.TrimRight(base, "/") + "/bot" + token + "/" + method
|
||||
}
|
||||
|
||||
type SafewAuthError struct {
|
||||
Description string
|
||||
}
|
||||
|
||||
func (e *SafewAuthError) Error() string {
|
||||
if e.Description == "" {
|
||||
return "safew unauthorized"
|
||||
}
|
||||
return e.Description
|
||||
}
|
||||
|
||||
func (s *SafeWSender) PollGroupChats(token string, offset int64, timeout int) ([]SafewChat, int64, error) {
|
||||
token = strings.TrimSpace(token)
|
||||
if token == "" {
|
||||
return nil, offset, fmt.Errorf("safew: token is required")
|
||||
}
|
||||
reqBody, _ := json.Marshal(map[string]any{
|
||||
"timeout": timeout,
|
||||
"offset": offset,
|
||||
"limit": 100,
|
||||
})
|
||||
resp, err := http.Post(s.methodURL(token, "getUpdates"), "application/json", bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, offset, fmt.Errorf("safew getUpdates: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, offset, fmt.Errorf("safew getUpdates read: %w", err)
|
||||
}
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
return nil, offset, &SafewAuthError{Description: safewErrorDescription(respBody)}
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
desc := safewErrorDescription(respBody)
|
||||
if desc != "" {
|
||||
return nil, offset, fmt.Errorf("safew getUpdates status %d: %s", resp.StatusCode, desc)
|
||||
}
|
||||
return nil, offset, fmt.Errorf("safew getUpdates status %d", resp.StatusCode)
|
||||
}
|
||||
var api safewAPIResponse
|
||||
if err := json.Unmarshal(respBody, &api); err == nil && !api.OK {
|
||||
if resp.StatusCode == 401 || strings.Contains(strings.ToLower(api.Description), "token") {
|
||||
return nil, offset, &SafewAuthError{Description: api.Description}
|
||||
}
|
||||
return nil, offset, fmt.Errorf("safew: %s", api.Description)
|
||||
}
|
||||
chats, maxID, err := GroupsFromUpdates(respBody)
|
||||
if err != nil {
|
||||
return nil, offset, err
|
||||
}
|
||||
next := offset
|
||||
if maxID > 0 {
|
||||
next = maxID + 1
|
||||
}
|
||||
return chats, next, nil
|
||||
}
|
||||
|
||||
func parseSafewChatID(raw json.RawMessage) (string, error) {
|
||||
@@ -136,3 +198,121 @@ func escapeMarkdownV2(s string) string {
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type SafewChat struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Username *string `json:"username"`
|
||||
}
|
||||
|
||||
func GroupsFromUpdates(body []byte) ([]SafewChat, int64, error) {
|
||||
dec := json.NewDecoder(bytes.NewReader(body))
|
||||
dec.UseNumber()
|
||||
var wrap struct {
|
||||
OK bool `json:"ok"`
|
||||
Result []json.RawMessage `json:"result"`
|
||||
}
|
||||
if err := dec.Decode(&wrap); err != nil {
|
||||
return nil, 0, fmt.Errorf("safew updates decode: %w", err)
|
||||
}
|
||||
seen := map[string]SafewChat{}
|
||||
var maxID int64
|
||||
for _, item := range wrap.Result {
|
||||
id, chat, err := parseUpdateItem(item)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if id > maxID {
|
||||
maxID = id
|
||||
}
|
||||
if chat == nil {
|
||||
continue
|
||||
}
|
||||
if chat.Type != "group" && chat.Type != "supergroup" {
|
||||
continue
|
||||
}
|
||||
seen[chat.ID] = *chat
|
||||
}
|
||||
out := make([]SafewChat, 0, len(seen))
|
||||
for _, c := range seen {
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, maxID, nil
|
||||
}
|
||||
|
||||
func parseUpdateItem(item json.RawMessage) (int64, *SafewChat, error) {
|
||||
dec := json.NewDecoder(bytes.NewReader(item))
|
||||
dec.UseNumber()
|
||||
var u map[string]json.RawMessage
|
||||
if err := dec.Decode(&u); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
var updateID int64
|
||||
if raw, ok := u["update_id"]; ok {
|
||||
var n json.Number
|
||||
if err := json.Unmarshal(raw, &n); err == nil {
|
||||
updateID, _ = n.Int64()
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"message", "edited_message", "channel_post", "edited_channel_post", "my_chat_member", "chat_member"} {
|
||||
raw, ok := u[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
chat := chatFromNested(raw)
|
||||
if chat != nil {
|
||||
return updateID, chat, nil
|
||||
}
|
||||
}
|
||||
return updateID, nil, nil
|
||||
}
|
||||
|
||||
func chatFromNested(raw json.RawMessage) *SafewChat {
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
var obj map[string]json.RawMessage
|
||||
if err := dec.Decode(&obj); err != nil {
|
||||
return nil
|
||||
}
|
||||
chatRaw, ok := obj["chat"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
dec = json.NewDecoder(bytes.NewReader(chatRaw))
|
||||
dec.UseNumber()
|
||||
var c struct {
|
||||
ID json.Number `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Username *string `json:"username"`
|
||||
}
|
||||
if err := dec.Decode(&c); err != nil {
|
||||
return nil
|
||||
}
|
||||
id := strings.TrimSpace(c.ID.String())
|
||||
if id == "" {
|
||||
return nil
|
||||
}
|
||||
return &SafewChat{ID: id, Type: c.Type, Title: c.Title, Username: c.Username}
|
||||
}
|
||||
|
||||
func FilterSafewChats(chats []SafewChat, q string) []SafewChat {
|
||||
q = strings.TrimSpace(strings.ToLower(q))
|
||||
if q == "" {
|
||||
return chats
|
||||
}
|
||||
var out []SafewChat
|
||||
for _, c := range chats {
|
||||
uname := ""
|
||||
if c.Username != nil {
|
||||
uname = *c.Username
|
||||
}
|
||||
if strings.Contains(strings.ToLower(c.Title), q) ||
|
||||
strings.Contains(strings.ToLower(uname), q) ||
|
||||
strings.Contains(strings.ToLower(c.ID), q) {
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user