Compare commits
2 Commits
85de6d175f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adba77b3e5 | ||
|
|
a749e33737 |
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
FROM golang:1.25.6-alpine AS builder
|
||||||
|
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN git clone https://github.com/Salastil/Sneedchat-Discord-Bridge-Go.git .
|
||||||
|
|
||||||
|
RUN go mod tidy && go build -o Sneedchat-Discord-Bridge .
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /build/Sneedchat-Discord-Bridge .
|
||||||
|
|
||||||
|
ENTRYPOINT ["./Sneedchat-Discord-Bridge"]
|
||||||
@@ -22,6 +22,9 @@ import (
|
|||||||
const (
|
const (
|
||||||
MaxAttachments = 4
|
MaxAttachments = 4
|
||||||
ProcessedCacheSize = 250
|
ProcessedCacheSize = 250
|
||||||
|
OverflowThreshold = 10
|
||||||
|
OverflowRecoveryThreshold = 5
|
||||||
|
OverflowBatchSize = 15
|
||||||
MappingCacheSize = 1000
|
MappingCacheSize = 1000
|
||||||
MappingMaxAge = 1 * time.Hour
|
MappingMaxAge = 1 * time.Hour
|
||||||
MappingCleanupInterval = 5 * time.Minute
|
MappingCleanupInterval = 5 * time.Minute
|
||||||
@@ -73,8 +76,10 @@ type Bridge struct {
|
|||||||
outageMessagesMu sync.Mutex
|
outageMessagesMu sync.Mutex
|
||||||
outageStart time.Time
|
outageStart time.Time
|
||||||
|
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
msgQueue chan map[string]interface{}
|
||||||
|
overflow bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBridge(cfg *config.Config, sneedClient *sneed.Client) (*Bridge, error) {
|
func NewBridge(cfg *config.Config, sneedClient *sneed.Client) (*Bridge, error) {
|
||||||
@@ -98,10 +103,11 @@ func NewBridge(cfg *config.Config, sneedClient *sneed.Client) (*Bridge, error) {
|
|||||||
queuedOutbound: make([]QueuedMessage, 0),
|
queuedOutbound: make([]QueuedMessage, 0),
|
||||||
outageNotices: make([]*discordgo.Message, 0),
|
outageNotices: make([]*discordgo.Message, 0),
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
|
msgQueue: make(chan map[string]interface{}, 500),
|
||||||
}
|
}
|
||||||
|
|
||||||
// hook Sneed client callbacks
|
// hook Sneed client callbacks
|
||||||
sneedClient.OnMessage = b.onSneedMessage
|
sneedClient.OnMessage = b.enqueueSneedMessage
|
||||||
sneedClient.OnEdit = b.handleSneedEdit
|
sneedClient.OnEdit = b.handleSneedEdit
|
||||||
sneedClient.OnDelete = b.handleSneedDelete
|
sneedClient.OnDelete = b.handleSneedDelete
|
||||||
sneedClient.OnConnect = b.onSneedConnect
|
sneedClient.OnConnect = b.onSneedConnect
|
||||||
@@ -124,8 +130,10 @@ func (b *Bridge) Start() error {
|
|||||||
if err := b.session.Open(); err != nil {
|
if err := b.session.Open(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
b.wg.Add(1)
|
b.wg.Add(3)
|
||||||
go b.cleanupLoop()
|
go b.cleanupLoop()
|
||||||
|
go b.messageWorker()
|
||||||
|
go b.messageWorker()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,6 +323,91 @@ func (b *Bridge) onDiscordMessageDelete(s *discordgo.Session, m *discordgo.Messa
|
|||||||
b.sneed.Send(fmt.Sprintf("/delete %s", sneedUUID))
|
b.sneed.Send(fmt.Sprintf("/delete %s", sneedUUID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) messageWorker() {
|
||||||
|
defer b.wg.Done()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-b.stopCh:
|
||||||
|
return
|
||||||
|
case msg, ok := <-b.msgQueue:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
depth := len(b.msgQueue)
|
||||||
|
if !b.overflow && depth >= OverflowThreshold {
|
||||||
|
b.overflow = true
|
||||||
|
log.Printf("⚠️ Message queue overflow (%d pending), switching to batch mode", depth)
|
||||||
|
} else if b.overflow && depth <= OverflowRecoveryThreshold {
|
||||||
|
b.overflow = false
|
||||||
|
log.Printf("✅ Message queue recovered (%d pending), switching to normal mode", depth)
|
||||||
|
}
|
||||||
|
if b.overflow {
|
||||||
|
batch := []map[string]interface{}{msg}
|
||||||
|
for len(batch) < OverflowBatchSize {
|
||||||
|
select {
|
||||||
|
case next, ok := <-b.msgQueue:
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
batch = append(batch, next)
|
||||||
|
default:
|
||||||
|
goto flushBatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushBatch:
|
||||||
|
b.sendOverflowBatch(batch)
|
||||||
|
} else {
|
||||||
|
b.onSneedMessage(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) sendOverflowBatch(batch []map[string]interface{}) {
|
||||||
|
if len(batch) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var description strings.Builder
|
||||||
|
for _, msg := range batch {
|
||||||
|
username, _ := msg["username"].(string)
|
||||||
|
rawContent, _ := msg["content"].(string)
|
||||||
|
content := utils.BBCodeToMarkdown(rawContent)
|
||||||
|
content = sneed.ReplaceBridgeMention(content, b.cfg.BridgeUsername, b.cfg.DiscordPingUserID)
|
||||||
|
|
||||||
|
var ts string
|
||||||
|
if raw, ok := msg["raw"].(sneed.SneedMessage); ok && raw.MessageDate > 0 {
|
||||||
|
ts = time.Unix(int64(raw.MessageDate), 0).Format("3:04:05 PM")
|
||||||
|
}
|
||||||
|
|
||||||
|
description.WriteString(fmt.Sprintf("**%s** %s\n%s\n\n", username, ts, content))
|
||||||
|
}
|
||||||
|
|
||||||
|
webhookID, webhookToken := parseWebhookURL(b.cfg.DiscordWebhookURL)
|
||||||
|
embed := &discordgo.MessageEmbed{
|
||||||
|
Title: "overflow mode",
|
||||||
|
Description: strings.TrimSpace(description.String()),
|
||||||
|
Color: OutageEmbedColorActive,
|
||||||
|
}
|
||||||
|
params := &discordgo.WebhookParams{
|
||||||
|
Embeds: []*discordgo.MessageEmbed{embed},
|
||||||
|
}
|
||||||
|
_, err := b.session.WebhookExecute(webhookID, webhookToken, false, params)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ Failed to send overflow batch: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("📦 Sent overflow batch of %d messages", len(batch))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) enqueueSneedMessage(msg map[string]interface{}) {
|
||||||
|
select {
|
||||||
|
case b.msgQueue <- msg:
|
||||||
|
default:
|
||||||
|
log.Printf("⚠️ Message queue full, dropping message from %s", msg["username"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Bridge) onSneedMessage(msg map[string]interface{}) {
|
func (b *Bridge) onSneedMessage(msg map[string]interface{}) {
|
||||||
username, _ := msg["username"].(string)
|
username, _ := msg["username"].(string)
|
||||||
rawContent, _ := msg["content"].(string)
|
rawContent, _ := msg["content"].(string)
|
||||||
|
|||||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
sneedchat-bridge:
|
||||||
|
build: .
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
|
||||||
|
- DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_ID}
|
||||||
|
- DISCORD_GUILD_ID=${DISCORD_GUILD_ID}
|
||||||
|
- DISCORD_PING_USER_ID=${DISCORD_PING_USER_ID}
|
||||||
|
- DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL}
|
||||||
|
- RECONNECT_INTERVAL=${RECONNECT_INTERVAL:-5}
|
||||||
|
- MEDIA_UPLOAD_SERVICE=${MEDIA_UPLOAD_SERVICE:-litterbox}
|
||||||
|
- SNEEDCHAT_ROOM_ID=${SNEEDCHAT_ROOM_ID:-1}
|
||||||
|
- ENABLE_FILE_LOGGING=${ENABLE_FILE_LOGGING:-false}
|
||||||
|
- BRIDGE_USER_ID=${BRIDGE_USER_ID}
|
||||||
|
- BRIDGE_USERNAME=${BRIDGE_USERNAME}
|
||||||
|
- BRIDGE_PASSWORD=${BRIDGE_PASSWORD}
|
||||||
@@ -5,6 +5,7 @@ type SneedMessage struct {
|
|||||||
MessageUUID string `json:"message_uuid"`
|
MessageUUID string `json:"message_uuid"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
MessageRaw string `json:"message_raw"`
|
MessageRaw string `json:"message_raw"`
|
||||||
|
MessageDate int `json:"message_date"`
|
||||||
MessageEditDate int `json:"message_edit_date"`
|
MessageEditDate int `json:"message_edit_date"`
|
||||||
Author map[string]interface{} `json:"author"`
|
Author map[string]interface{} `json:"author"`
|
||||||
Deleted bool `json:"deleted"`
|
Deleted bool `json:"deleted"`
|
||||||
|
|||||||
Reference in New Issue
Block a user