mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 11:37:06 +08:00
122 lines
3.2 KiB
Go
122 lines
3.2 KiB
Go
package messaging
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Wxw-Gu/WechatExplorer/services/wechat-connector/ilink"
|
|
)
|
|
|
|
const (
|
|
webhookAttempts = 3
|
|
webhookTimeout = 5 * time.Second
|
|
)
|
|
|
|
type InboundWebhook struct {
|
|
url string
|
|
token string
|
|
client *http.Client
|
|
}
|
|
|
|
type inboundWebhookPayload struct {
|
|
AccountID string `json:"account_id"`
|
|
FromUserID string `json:"from_user_id"`
|
|
MessageID int64 `json:"message_id"`
|
|
MessageType int `json:"message_type"`
|
|
Items []inboundWebhookItem `json:"items"`
|
|
ReceivedAt time.Time `json:"received_at"`
|
|
}
|
|
|
|
type inboundWebhookItem struct {
|
|
Type int `json:"type"`
|
|
Text string `json:"text,omitempty"`
|
|
}
|
|
|
|
func NewInboundWebhook(url, token string) *InboundWebhook {
|
|
return &InboundWebhook{
|
|
url: strings.TrimSpace(url),
|
|
token: token,
|
|
client: &http.Client{Timeout: webhookTimeout},
|
|
}
|
|
}
|
|
|
|
// Dispatch is intentionally non-blocking so webhook failures never stall iLink polling.
|
|
func (w *InboundWebhook) Dispatch(ctx context.Context, client *ilink.Client, msg ilink.WeixinMessage) {
|
|
payload := normalizeInboundMessage(client.BotID(), msg)
|
|
go func() {
|
|
if err := w.deliver(ctx, payload); err != nil {
|
|
log.Printf("[webhook] inbound delivery failed for message %d: %v", msg.MessageID, err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (w *InboundWebhook) deliver(ctx context.Context, payload inboundWebhookPayload) error {
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("encode payload: %w", err)
|
|
}
|
|
var lastErr error
|
|
for attempt := 1; attempt <= webhookAttempts; attempt++ {
|
|
if attempt > 1 {
|
|
timer := time.NewTimer(time.Duration(attempt-1) * time.Second)
|
|
select {
|
|
case <-ctx.Done():
|
|
timer.Stop()
|
|
return ctx.Err()
|
|
case <-timer.C:
|
|
}
|
|
}
|
|
req, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, w.url, bytes.NewReader(body))
|
|
if reqErr != nil {
|
|
return fmt.Errorf("create request: %w", reqErr)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if w.token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+w.token)
|
|
}
|
|
resp, doErr := w.client.Do(req)
|
|
if doErr != nil {
|
|
lastErr = doErr
|
|
continue
|
|
}
|
|
responseBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
resp.Body.Close()
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
return nil
|
|
}
|
|
lastErr = fmt.Errorf("status %s: %s", resp.Status, strings.TrimSpace(string(responseBody)))
|
|
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
|
|
break
|
|
}
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
func normalizeInboundMessage(accountID string, msg ilink.WeixinMessage) inboundWebhookPayload {
|
|
items := make([]inboundWebhookItem, 0, len(msg.ItemList))
|
|
for _, item := range msg.ItemList {
|
|
normalized := inboundWebhookItem{Type: item.Type}
|
|
if item.TextItem != nil {
|
|
normalized.Text = item.TextItem.Text
|
|
} else if item.VoiceItem != nil {
|
|
normalized.Text = item.VoiceItem.Text
|
|
}
|
|
items = append(items, normalized)
|
|
}
|
|
return inboundWebhookPayload{
|
|
AccountID: accountID,
|
|
FromUserID: msg.FromUserID,
|
|
MessageID: msg.MessageID,
|
|
MessageType: msg.MessageType,
|
|
Items: items,
|
|
ReceivedAt: time.Now().UTC(),
|
|
}
|
|
}
|