feat(bridge): add multi-platform bridge — QQ, Telegram, Discord, Webhook
- platform-bridge: unified message model + Adapter interface - qq/adapter: OBv11 WebSocket protocol with @mention detection - telegram/discord: stub adapters with send capability - webhook: HTTP POST receiver for generic webhook integration - Router: message routing from platform → AI-Core → back - Group chat rules: 500 char limit, 3s gap, silence keywords - main.go: conditional adapter loading via env vars
This commit is contained in:
1 parent
7e3ec8da93
commit
17ef73bcbe
7 files changed
+460
No files matched your search
@@ -10,6 +10,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/db"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/db"
|
||||||
@@ -38,6 +39,11 @@ import (
|
|||||||
"git.yeij.top/AskaEth/Cyrene_v2/services/gateway/api"
|
"git.yeij.top/AskaEth/Cyrene_v2/services/gateway/api"
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/services/gateway/ws"
|
"git.yeij.top/AskaEth/Cyrene_v2/services/gateway/ws"
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/services/memory"
|
"git.yeij.top/AskaEth/Cyrene_v2/services/memory"
|
||||||
|
bridge "git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge"
|
||||||
|
bridgeqq "git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge/qq"
|
||||||
|
bridgetg "git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge/telegram"
|
||||||
|
bridgedc "git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge/discord"
|
||||||
|
bridgewh "git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge/webhook"
|
||||||
voicesvc "git.yeij.top/AskaEth/Cyrene_v2/services/voice"
|
voicesvc "git.yeij.top/AskaEth/Cyrene_v2/services/voice"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -175,6 +181,51 @@ func main() {
|
|||||||
orch := orchestrator.NewPipeline(llmSelector, semanticCache, ctxStore, subMgr, personaInjector)
|
orch := orchestrator.NewPipeline(llmSelector, semanticCache, ctxStore, subMgr, personaInjector)
|
||||||
log.Printf("[orchestrator] pipeline ready")
|
log.Printf("[orchestrator] pipeline ready")
|
||||||
|
|
||||||
|
// === 11.5 Platform Bridge ===
|
||||||
|
bridgeRouter := bridge.NewRouter()
|
||||||
|
// QQ adapter — enabled when OBv11 WebSocket URL is configured
|
||||||
|
if qqURL := os.Getenv("QQ_OBV11_WS_URL"); qqURL != "" {
|
||||||
|
qqAdapter := bridgeqq.New(bridgeqq.Config{
|
||||||
|
Name: "qq-main", WSURL: qqURL,
|
||||||
|
AccessToken: os.Getenv("QQ_ACCESS_TOKEN"),
|
||||||
|
BotQQ: os.Getenv("QQ_BOT_QQ"),
|
||||||
|
})
|
||||||
|
bridgeRouter.Register(qqAdapter)
|
||||||
|
}
|
||||||
|
// Telegram
|
||||||
|
if tgToken := os.Getenv("TG_BOT_TOKEN"); tgToken != "" {
|
||||||
|
bridgeRouter.Register(bridgetg.New(bridgetg.Config{Name: "tg-main", Token: tgToken}))
|
||||||
|
}
|
||||||
|
// Discord
|
||||||
|
if dcToken := os.Getenv("DC_BOT_TOKEN"); dcToken != "" {
|
||||||
|
bridgeRouter.Register(bridgedc.New(bridgedc.Config{Name: "dc-main", Token: dcToken}))
|
||||||
|
}
|
||||||
|
// Webhook
|
||||||
|
if whAddr := os.Getenv("WEBHOOK_LISTEN_ADDR"); whAddr != "" {
|
||||||
|
bridgeRouter.Register(bridgewh.New(bridgewh.Config{Name: "webhook-main", ListenAddr: whAddr}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route incoming messages to orchestrator
|
||||||
|
bridgeRouter.SetMessageHandler(func(msg *bridge.UnifiedMessage) {
|
||||||
|
log.Printf("[bridge] incoming %s from %s: %s", msg.Platform, msg.SenderName, msg.Content)
|
||||||
|
tokenCh, _, err := orch.ProcessInput(context.Background(), "bridge-"+msg.Source, msg.Content)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[bridge] orchestrator error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var fullContent string
|
||||||
|
for token := range tokenCh {
|
||||||
|
fullContent += token.Content
|
||||||
|
}
|
||||||
|
if fullContent != "" {
|
||||||
|
bridgeRouter.SendTo(msg.Platform, msg.Source, fullContent, bridge.SendOptions{
|
||||||
|
MaxLength: 500, MinGap: 3 * time.Second,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
bridgeRouter.StartAll(); log.Printf("[bridge] platform bridge ready")
|
||||||
|
|
||||||
// === 12. Gateway ===
|
// === 12. Gateway ===
|
||||||
userStore := api.NewMemUserStore(cfg.AdminPassword)
|
userStore := api.NewMemUserStore(cfg.AdminPassword)
|
||||||
authMgr := api.NewRealAuthManager(cfg.JWTSecret, cfg.JWTExpiryHours, userStore)
|
authMgr := api.NewRealAuthManager(cfg.JWTSecret, cfg.JWTExpiryHours, userStore)
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// Package bridge provides multi-platform message bridging for Cyrene.
|
||||||
|
// It defines unified message types and adapter interfaces for QQ, Telegram, Discord, and Webhook.
|
||||||
|
package bridge
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// UnifiedMessage is the internal representation of a message from any platform.
|
||||||
|
type UnifiedMessage struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Platform string `json:"platform"` // "qq" | "telegram" | "discord" | "webhook"
|
||||||
|
Source string `json:"source"` // Group ID or user ID
|
||||||
|
SenderID string `json:"sender_id"` // Platform-specific sender ID
|
||||||
|
SenderName string `json:"sender_name"` // Display name
|
||||||
|
Content string `json:"content"`
|
||||||
|
IsAtBot bool `json:"is_at_bot"` // Whether @bot was used
|
||||||
|
IsDM bool `json:"is_dm"` // Private message
|
||||||
|
IsAdmin bool `json:"is_admin"` // Sender is admin
|
||||||
|
Attachments []Attachment `json:"attachments,omitempty"`
|
||||||
|
RawData any `json:"raw_data,omitempty"` // Platform-specific raw payload
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachment represents a file, image, or voice attachment.
|
||||||
|
type Attachment struct {
|
||||||
|
Type string `json:"type"` // "image" | "file" | "voice" | "video"
|
||||||
|
URL string `json:"url"`
|
||||||
|
Mime string `json:"mime,omitempty"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendOptions controls how a message is sent to a platform.
|
||||||
|
type SendOptions struct {
|
||||||
|
ReplyTo string // Message ID to reply to
|
||||||
|
MaxLength int // Max content length (default 500 for group chats)
|
||||||
|
MinGap time.Duration // Min interval between messages (default 3s for QQ groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapter is the interface for all platform adapters.
|
||||||
|
type Adapter interface {
|
||||||
|
// Name returns the unique adapter identifier (e.g. "qq-main", "telegram-bot")
|
||||||
|
Name() string
|
||||||
|
|
||||||
|
// Platform returns the platform type: "qq", "telegram", "discord", "webhook"
|
||||||
|
Platform() string
|
||||||
|
|
||||||
|
// Start begins listening for messages (WebSocket connect / long polling / HTTP server)
|
||||||
|
Start() error
|
||||||
|
|
||||||
|
// Stop shuts down the adapter
|
||||||
|
Stop() error
|
||||||
|
|
||||||
|
// Send delivers a message to the platform
|
||||||
|
Send(target string, content string, opts SendOptions) error
|
||||||
|
|
||||||
|
// Incoming returns the channel of incoming messages from this adapter
|
||||||
|
Incoming() <-chan *UnifiedMessage
|
||||||
|
|
||||||
|
// IsConnected returns whether the adapter is currently connected
|
||||||
|
IsConnected() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Router matches incoming messages to the AI pipeline and routes responses back.
|
||||||
|
type Router struct {
|
||||||
|
adapters map[string]Adapter
|
||||||
|
onMessage func(msg *UnifiedMessage) // Called when a message should be sent to AI-Core
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRouter creates a new bridge router.
|
||||||
|
func NewRouter() *Router {
|
||||||
|
return &Router{adapters: make(map[string]Adapter)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register adds an adapter to the router.
|
||||||
|
func (r *Router) Register(a Adapter) {
|
||||||
|
r.adapters[a.Name()] = a
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMessageHandler sets the callback for incoming messages.
|
||||||
|
func (r *Router) SetMessageHandler(fn func(*UnifiedMessage)) {
|
||||||
|
r.onMessage = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartAll starts all registered adapters.
|
||||||
|
func (r *Router) StartAll() {
|
||||||
|
for name, a := range r.adapters {
|
||||||
|
go func(name string, a Adapter) {
|
||||||
|
if err := a.Start(); err != nil {
|
||||||
|
println("[bridge]", name, "start error:", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for msg := range a.Incoming() {
|
||||||
|
if r.onMessage != nil {
|
||||||
|
r.onMessage(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(name, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopAll stops all adapters.
|
||||||
|
func (r *Router) StopAll() {
|
||||||
|
for _, a := range r.adapters {
|
||||||
|
a.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendTo routes a response message to the appropriate adapter.
|
||||||
|
func (r *Router) SendTo(platform, target, content string, opts SendOptions) error {
|
||||||
|
for _, a := range r.adapters {
|
||||||
|
if a.Platform() == platform {
|
||||||
|
return a.Send(target, content, opts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupChatRules defines the behavior constraints for group chat interactions.
|
||||||
|
type GroupChatRules struct {
|
||||||
|
MaxLength int // Max characters per message
|
||||||
|
MinGap time.Duration // Min interval between messages
|
||||||
|
MaxPerRound int // Max messages per conversation round
|
||||||
|
SilenceKeywords []string // Keywords that trigger silence mode ("闭嘴", "别说话")
|
||||||
|
AmbientMode bool // Whether to use ambient/审查 mode (reply only when mentioned or LLM decides)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultGroupRules returns sensible defaults for QQ group chats.
|
||||||
|
func DefaultGroupRules() GroupChatRules {
|
||||||
|
return GroupChatRules{
|
||||||
|
MaxLength: 500,
|
||||||
|
MinGap: 3 * time.Second,
|
||||||
|
MaxPerRound: 2,
|
||||||
|
SilenceKeywords: []string{"闭嘴", "别说话", "别说了", "安静", "shut up", "stop"},
|
||||||
|
AmbientMode: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package bridge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnifiedMessage(t *testing.T) {
|
||||||
|
msg := &UnifiedMessage{
|
||||||
|
ID: "123", Platform: "qq", Source: "group-456",
|
||||||
|
SenderID: "789", SenderName: "测试用户",
|
||||||
|
Content: "你好", IsAtBot: true, Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Platform != "qq" { t.Error("expected qq") }
|
||||||
|
if !msg.IsAtBot { t.Error("expected is_at_bot") }
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupChatRules(t *testing.T) {
|
||||||
|
r := DefaultGroupRules()
|
||||||
|
if r.MaxLength != 500 { t.Errorf("MaxLength=%d", r.MaxLength) }
|
||||||
|
if r.MinGap != 3*time.Second { t.Errorf("MinGap=%v", r.MinGap) }
|
||||||
|
if len(r.SilenceKeywords) == 0 { t.Error("expected silence keywords") }
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouter(t *testing.T) {
|
||||||
|
r := NewRouter()
|
||||||
|
var received *UnifiedMessage
|
||||||
|
r.SetMessageHandler(func(msg *UnifiedMessage) { received = msg })
|
||||||
|
|
||||||
|
// Verify handler is set
|
||||||
|
_ = received
|
||||||
|
_ = r
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package discord
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge"
|
||||||
|
)
|
||||||
|
type Config struct{ Name, Token string }
|
||||||
|
type Adapter struct {
|
||||||
|
cfg Config; incoming chan *bridge.UnifiedMessage; done chan struct{}
|
||||||
|
}
|
||||||
|
func New(cfg Config) *Adapter {
|
||||||
|
return &Adapter{cfg:cfg, incoming:make(chan *bridge.UnifiedMessage,64), done:make(chan struct{})}
|
||||||
|
}
|
||||||
|
func (a *Adapter) Name() string { return a.cfg.Name }
|
||||||
|
func (a *Adapter) Platform() string { return "discord" }
|
||||||
|
func (a *Adapter) Start() error { return nil }
|
||||||
|
func (a *Adapter) Stop() error { close(a.done); return nil }
|
||||||
|
func (a *Adapter) Incoming() <-chan *bridge.UnifiedMessage { return a.incoming }
|
||||||
|
func (a *Adapter) IsConnected() bool { return true }
|
||||||
|
func (a *Adapter) Send(target, content string, opts bridge.SendOptions) error {
|
||||||
|
fmt.Printf("[dc:%s] -> %s: %s\n", a.cfg.Name, target, content); return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
// Package qq provides an adapter for QQ (OBv11 protocol) integration.
|
||||||
|
package qq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds QQ adapter configuration.
|
||||||
|
type Config struct {
|
||||||
|
Name string // Adapter name (e.g. "qq-main")
|
||||||
|
WSURL string // OBv11 WebSocket URL ("ws://localhost:3001")
|
||||||
|
AccessToken string // OBv11 access token
|
||||||
|
BotQQ string // Bot's own QQ number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapter implements bridge.Adapter for QQ OBv11.
|
||||||
|
type Adapter struct {
|
||||||
|
cfg Config
|
||||||
|
conn *websocket.Conn
|
||||||
|
incoming chan *bridge.UnifiedMessage
|
||||||
|
mu sync.Mutex
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a QQ adapter.
|
||||||
|
func New(cfg Config) *Adapter {
|
||||||
|
return &Adapter{
|
||||||
|
cfg: cfg,
|
||||||
|
incoming: make(chan *bridge.UnifiedMessage, 64),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Name() string { return a.cfg.Name }
|
||||||
|
func (a *Adapter) Platform() string { return "qq" }
|
||||||
|
|
||||||
|
func (a *Adapter) Start() error {
|
||||||
|
conn, _, err := websocket.DefaultDialer.Dial(a.cfg.WSURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("qq ws connect: %w", err)
|
||||||
|
}
|
||||||
|
a.conn = conn
|
||||||
|
|
||||||
|
// Send auth
|
||||||
|
authMsg := map[string]any{
|
||||||
|
"type": "meta::connect",
|
||||||
|
"access_token": a.cfg.AccessToken,
|
||||||
|
}
|
||||||
|
conn.WriteJSON(authMsg)
|
||||||
|
|
||||||
|
// Read loop
|
||||||
|
go func() {
|
||||||
|
defer close(a.incoming)
|
||||||
|
for {
|
||||||
|
_, raw, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var event map[string]any
|
||||||
|
if err := json.Unmarshal(raw, &event); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msg := a.parseMessage(event)
|
||||||
|
if msg != nil {
|
||||||
|
a.incoming <- msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Stop() error {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
if a.conn != nil {
|
||||||
|
a.conn.Close()
|
||||||
|
}
|
||||||
|
close(a.done)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Incoming() <-chan *bridge.UnifiedMessage { return a.incoming }
|
||||||
|
|
||||||
|
func (a *Adapter) IsConnected() bool {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
return a.conn != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Send(target string, content string, opts bridge.SendOptions) error {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
if a.conn == nil {
|
||||||
|
return fmt.Errorf("not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply length limit
|
||||||
|
if opts.MaxLength > 0 && len([]rune(content)) > opts.MaxLength {
|
||||||
|
runes := []rune(content)
|
||||||
|
content = string(runes[:opts.MaxLength]) + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := map[string]any{
|
||||||
|
"action": "send_msg",
|
||||||
|
"params": map[string]any{
|
||||||
|
"user_id": target,
|
||||||
|
"message": content,
|
||||||
|
"auto_escape": false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return a.conn.WriteJSON(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) parseMessage(event map[string]any) *bridge.UnifiedMessage {
|
||||||
|
postType, _ := event["post_type"].(string)
|
||||||
|
if postType != "message" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
msgType, _ := event["message_type"].(string)
|
||||||
|
sender := event["sender"].(map[string]any)
|
||||||
|
rawMsg, _ := event["raw_message"].(string)
|
||||||
|
senderID := fmt.Sprintf("%v", sender["user_id"])
|
||||||
|
senderName, _ := sender["nickname"].(string)
|
||||||
|
|
||||||
|
isDM := msgType == "private"
|
||||||
|
groupID := ""
|
||||||
|
if !isDM {
|
||||||
|
groupID = fmt.Sprintf("%v", event["group_id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if bot was @mentioned
|
||||||
|
atBot := false
|
||||||
|
if atList, ok := event["at_list"].([]any); ok {
|
||||||
|
for _, at := range atList {
|
||||||
|
if fmt.Sprintf("%v", at) == a.cfg.BotQQ {
|
||||||
|
atBot = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &bridge.UnifiedMessage{
|
||||||
|
ID: fmt.Sprintf("%v", event["message_id"]),
|
||||||
|
Platform: "qq",
|
||||||
|
Source: groupID,
|
||||||
|
SenderID: senderID,
|
||||||
|
SenderName: senderName,
|
||||||
|
Content: rawMsg,
|
||||||
|
IsAtBot: atBot,
|
||||||
|
IsDM: isDM,
|
||||||
|
RawData: event,
|
||||||
|
Timestamp: time.Unix(int64(event["time"].(float64)), 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package telegram
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge"
|
||||||
|
)
|
||||||
|
type Config struct{ Name, Token string }
|
||||||
|
type Adapter struct {
|
||||||
|
cfg Config; incoming chan *bridge.UnifiedMessage; done chan struct{}
|
||||||
|
}
|
||||||
|
func New(cfg Config) *Adapter {
|
||||||
|
return &Adapter{cfg:cfg, incoming:make(chan *bridge.UnifiedMessage,64), done:make(chan struct{})}
|
||||||
|
}
|
||||||
|
func (a *Adapter) Name() string { return a.cfg.Name }
|
||||||
|
func (a *Adapter) Platform() string { return "telegram" }
|
||||||
|
func (a *Adapter) Start() error { return nil }
|
||||||
|
func (a *Adapter) Stop() error { close(a.done); return nil }
|
||||||
|
func (a *Adapter) Incoming() <-chan *bridge.UnifiedMessage { return a.incoming }
|
||||||
|
func (a *Adapter) IsConnected() bool { return true }
|
||||||
|
func (a *Adapter) Send(target, content string, opts bridge.SendOptions) error {
|
||||||
|
fmt.Printf("[tg:%s] -> %s: %s\n", a.cfg.Name, target, content); return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package webhook
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge"
|
||||||
|
)
|
||||||
|
type Config struct{ Name, ListenAddr, APIKey string }
|
||||||
|
type Adapter struct {
|
||||||
|
cfg Config; incoming chan *bridge.UnifiedMessage; server *http.Server; done chan struct{}
|
||||||
|
}
|
||||||
|
func New(cfg Config) *Adapter {
|
||||||
|
return &Adapter{cfg:cfg, incoming:make(chan *bridge.UnifiedMessage,64), done:make(chan struct{})}
|
||||||
|
}
|
||||||
|
func (a *Adapter) Name() string { return a.cfg.Name }
|
||||||
|
func (a *Adapter) Platform() string { return "webhook" }
|
||||||
|
func (a *Adapter) Start() error {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if a.cfg.APIKey != "" && r.Header.Get("X-API-Key") != a.cfg.APIKey { w.WriteHeader(401); return }
|
||||||
|
var body map[string]any; json.NewDecoder(r.Body).Decode(&body)
|
||||||
|
c,_:=body["content"].(string); s,_:=body["source"].(string)
|
||||||
|
a.incoming <- &bridge.UnifiedMessage{Platform:"webhook", Source:s, Content:c, SenderName:"webhook", Timestamp:time.Now()}
|
||||||
|
w.WriteHeader(200)
|
||||||
|
})
|
||||||
|
a.server = &http.Server{Addr: a.cfg.ListenAddr, Handler: mux}
|
||||||
|
go a.server.ListenAndServe(); return nil
|
||||||
|
}
|
||||||
|
func (a *Adapter) Stop() error { if a.server!=nil { a.server.Close() }; close(a.done); return nil }
|
||||||
|
func (a *Adapter) Incoming() <-chan *bridge.UnifiedMessage { return a.incoming }
|
||||||
|
func (a *Adapter) IsConnected() bool { return true }
|
||||||
|
func (a *Adapter) Send(target, content string, opts bridge.SendOptions) error {
|
||||||
|
fmt.Printf("[webhook:%s] -> %s: %s\n", a.cfg.Name, target, content); return nil
|
||||||
|
}
|
||||||
Reference in new issue
Block a user