244 lines
6.4 KiB
Go
244 lines
6.4 KiB
Go
package bkosmi
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/42wim/matterbridge/bridge"
|
|
"github.com/42wim/matterbridge/bridge/config"
|
|
"github.com/42wim/matterbridge/bridge/jackbox"
|
|
)
|
|
|
|
const (
|
|
defaultWebSocketURL = "wss://engine.kosmi.io/gql-ws"
|
|
)
|
|
|
|
// KosmiClient interface for different client implementations
|
|
type KosmiClient interface {
|
|
Connect() error
|
|
Disconnect() error
|
|
SendMessage(text string) error
|
|
OnMessage(callback func(*NewMessagePayload))
|
|
IsConnected() bool
|
|
}
|
|
|
|
// Bkosmi represents the Kosmi bridge
|
|
type Bkosmi struct {
|
|
*bridge.Config
|
|
client KosmiClient
|
|
roomID string
|
|
roomURL string
|
|
connected bool
|
|
msgChannel chan config.Message
|
|
jackboxClient *jackbox.Client
|
|
}
|
|
|
|
// New creates a new Kosmi bridge instance
|
|
func New(cfg *bridge.Config) bridge.Bridger {
|
|
b := &Bkosmi{
|
|
Config: cfg,
|
|
msgChannel: make(chan config.Message, 100),
|
|
}
|
|
|
|
return b
|
|
}
|
|
|
|
// Connect establishes connection to the Kosmi room
|
|
func (b *Bkosmi) Connect() error {
|
|
b.Log.Info("Connecting to Kosmi")
|
|
|
|
// Get room URL from config
|
|
b.roomURL = b.GetString("RoomURL")
|
|
if b.roomURL == "" {
|
|
return fmt.Errorf("RoomURL is required in configuration")
|
|
}
|
|
|
|
// Extract room ID from URL
|
|
roomID, err := extractRoomID(b.roomURL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to extract room ID from URL %s: %w", b.roomURL, err)
|
|
}
|
|
b.roomID = roomID
|
|
b.Log.Infof("Extracted room ID: %s", b.roomID)
|
|
|
|
// Create GraphQL WebSocket client (pure Go, no Playwright!)
|
|
b.client = NewGraphQLWSClient(b.roomURL, b.roomID, b.Log)
|
|
|
|
// Register message handler
|
|
b.client.OnMessage(b.handleIncomingMessage)
|
|
|
|
// Connect to Kosmi
|
|
if err := b.client.Connect(); err != nil {
|
|
return fmt.Errorf("failed to connect to Kosmi: %w", err)
|
|
}
|
|
|
|
b.connected = true
|
|
b.Log.Info("Successfully connected to Kosmi")
|
|
|
|
return nil
|
|
}
|
|
|
|
// Disconnect closes the connection to Kosmi
|
|
func (b *Bkosmi) Disconnect() error {
|
|
b.Log.Info("Disconnecting from Kosmi")
|
|
|
|
if b.client != nil {
|
|
if err := b.client.Disconnect(); err != nil {
|
|
b.Log.Errorf("Error closing Kosmi client: %v", err)
|
|
}
|
|
}
|
|
|
|
close(b.msgChannel)
|
|
b.connected = false
|
|
|
|
return nil
|
|
}
|
|
|
|
// JoinChannel joins a Kosmi room (no-op as we connect to a specific room)
|
|
func (b *Bkosmi) JoinChannel(channel config.ChannelInfo) error {
|
|
// Kosmi doesn't have a concept of joining channels after connection
|
|
// The room is specified in the configuration and joined on Connect()
|
|
b.Log.Infof("Channel %s is already connected via room URL", channel.Name)
|
|
return nil
|
|
}
|
|
|
|
// Send sends a message to Kosmi
|
|
func (b *Bkosmi) Send(msg config.Message) (string, error) {
|
|
b.Log.Debugf("=> Sending message to Kosmi: %#v", msg)
|
|
|
|
// Ignore delete messages
|
|
if msg.Event == config.EventMsgDelete {
|
|
return "", nil
|
|
}
|
|
|
|
// Check if we're connected
|
|
if !b.connected || b.client == nil {
|
|
b.Log.Error("Not connected to Kosmi, dropping message")
|
|
return "", nil
|
|
}
|
|
|
|
// The gateway already formatted the username with RemoteNickFormat
|
|
// So msg.Username contains the formatted string like "[irc] <cottongin>"
|
|
// Just send: username + text
|
|
formattedMsg := fmt.Sprintf("%s%s", msg.Username, msg.Text)
|
|
|
|
// Send message to Kosmi
|
|
if err := b.client.SendMessage(formattedMsg); err != nil {
|
|
b.Log.Errorf("Failed to send message to Kosmi: %v", err)
|
|
return "", err
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
// handleIncomingMessage processes messages received from Kosmi
|
|
func (b *Bkosmi) handleIncomingMessage(payload *NewMessagePayload) {
|
|
// Extract message details
|
|
body := payload.Data.NewMessage.Body
|
|
username := payload.Data.NewMessage.User.DisplayName
|
|
if username == "" {
|
|
username = payload.Data.NewMessage.User.Username
|
|
}
|
|
if username == "" {
|
|
username = "Unknown"
|
|
}
|
|
|
|
timestamp := time.Unix(payload.Data.NewMessage.Time, 0)
|
|
|
|
b.Log.Infof("Received message from Kosmi: [%s] %s: %s", timestamp.Format(time.RFC3339), username, body)
|
|
|
|
// Check if this is our own message (to avoid echo)
|
|
// Messages we send have [irc] prefix (from RemoteNickFormat)
|
|
if strings.HasPrefix(body, "[irc]") {
|
|
b.Log.Debug("Ignoring our own echoed message (has [irc] prefix)")
|
|
return
|
|
}
|
|
|
|
// Check for votes (thisgame++ or thisgame--)
|
|
// Only process votes from non-relayed messages
|
|
if !jackbox.IsRelayedMessage(body) {
|
|
if isVote, voteType := jackbox.DetectVote(body); isVote {
|
|
b.Log.Debugf("Detected vote from %s: %s", username, voteType)
|
|
if b.jackboxClient != nil {
|
|
go func() {
|
|
if err := b.jackboxClient.SendVote(username, voteType, timestamp); err != nil {
|
|
b.Log.Errorf("Failed to send vote to Jackbox API: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create Matterbridge message
|
|
// Use "main" as the channel name for gateway matching
|
|
// Don't add prefix here - let the gateway's RemoteNickFormat handle it
|
|
rmsg := config.Message{
|
|
Username: username,
|
|
Text: body,
|
|
Channel: "main",
|
|
Account: b.Account,
|
|
UserID: username,
|
|
Timestamp: timestamp,
|
|
Protocol: "kosmi",
|
|
}
|
|
|
|
// Send to Matterbridge
|
|
b.Log.Debugf("Forwarding to Matterbridge channel=%s account=%s: %s", rmsg.Channel, rmsg.Account, rmsg.Text)
|
|
|
|
if b.Remote == nil {
|
|
b.Log.Error("Remote channel is nil! Cannot forward message")
|
|
return
|
|
}
|
|
|
|
b.Remote <- rmsg
|
|
}
|
|
|
|
// extractRoomID extracts the room ID from a Kosmi room URL
|
|
// Supports formats:
|
|
// - https://app.kosmi.io/room/@roomname
|
|
// - https://app.kosmi.io/room/roomid
|
|
func extractRoomID(url string) (string, error) {
|
|
// Remove trailing slash if present
|
|
url = strings.TrimSuffix(url, "/")
|
|
|
|
// Pattern to match Kosmi room URLs
|
|
patterns := []string{
|
|
`https?://app\.kosmi\.io/room/(@?[a-zA-Z0-9_-]+)`,
|
|
`app\.kosmi\.io/room/(@?[a-zA-Z0-9_-]+)`,
|
|
`/room/(@?[a-zA-Z0-9_-]+)`,
|
|
}
|
|
|
|
for _, pattern := range patterns {
|
|
re := regexp.MustCompile(pattern)
|
|
matches := re.FindStringSubmatch(url)
|
|
if len(matches) >= 2 {
|
|
roomID := matches[1]
|
|
// Ensure @ prefix is present (required for WebSocket API)
|
|
if !strings.HasPrefix(roomID, "@") {
|
|
roomID = "@" + roomID
|
|
}
|
|
return roomID, nil
|
|
}
|
|
}
|
|
|
|
// If no pattern matches, assume the entire string is the room ID
|
|
// This allows for simple room ID configuration
|
|
parts := strings.Split(url, "/")
|
|
if len(parts) > 0 {
|
|
lastPart := parts[len(parts)-1]
|
|
if lastPart != "" {
|
|
return strings.TrimPrefix(lastPart, "@"), nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("could not extract room ID from URL: %s", url)
|
|
}
|
|
|
|
// SetJackboxClient sets the Jackbox API client for this bridge
|
|
func (b *Bkosmi) SetJackboxClient(client *jackbox.Client) {
|
|
b.jackboxClient = client
|
|
b.Log.Info("Jackbox client injected into Kosmi bridge")
|
|
}
|