Compare commits
2 Commits
RC-1.0.Ori
...
3c2ae9a10e
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c2ae9a10e | |||
| b66b17d095 |
19
Dockerfile
19
Dockerfile
@@ -1,12 +1,12 @@
|
|||||||
# -------- Dockerfile (Multi-Stage Build) --------
|
# -------- Dockerfile (Multi-Stage Build) --------
|
||||||
# 1. Builder-Stage
|
# 1. Builder-Stage
|
||||||
FROM golang:1.24-alpine AS builder
|
FROM golang:1.25-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY go.* ./
|
COPY go.* ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /bin/autovoice
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /bin/vocalforge
|
||||||
|
|
||||||
# 2. Runtime-Stage
|
# 2. Runtime-Stage
|
||||||
FROM alpine:3.22
|
FROM alpine:3.22
|
||||||
@@ -16,7 +16,7 @@ RUN apk add --no-cache ca-certificates
|
|||||||
#RUN mkdir /data
|
#RUN mkdir /data
|
||||||
#RUN mkdir /dynamicsrc
|
#RUN mkdir /dynamicsrc
|
||||||
RUN mkdir /tempsrc
|
RUN mkdir /tempsrc
|
||||||
COPY --from=builder /bin/autovoice /bin/autovoice
|
COPY --from=builder /bin/vocalforge /bin/vocalforge
|
||||||
#COPY ./static /data/static
|
#COPY ./static /data/static
|
||||||
COPY ./language.json /tempsrc/language.json
|
COPY ./language.json /tempsrc/language.json
|
||||||
#COPY ./dynamicsrc /dynamicsrc
|
#COPY ./dynamicsrc /dynamicsrc
|
||||||
@@ -25,13 +25,12 @@ COPY ./language.json /tempsrc/language.json
|
|||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# Environment defaults; können per compose überschrieben werden
|
# Environment defaults; können per compose überschrieben werden
|
||||||
ENV LOBBY_CHANNEL_ID=0 \
|
ENV PRESENCE_GUILD_ID=0 \
|
||||||
DISCORD_TOKEN=0 \
|
DB_PATH=/data/guild_config.db \
|
||||||
GUILD_ID=0 \
|
|
||||||
CATEGORY_ID=0 \
|
|
||||||
TIMEOUT_MIN=1 \
|
TIMEOUT_MIN=1 \
|
||||||
CONFIG_PATH=/data/guild_config.json
|
DISCORD_TOKEN=0 \
|
||||||
|
TRANSLATIONS_FILE=/tempsrc/language.json
|
||||||
|
|
||||||
|
VOLUME /data
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/vocalforge"]
|
||||||
ENTRYPOINT ["/bin/autovoice"]
|
|
||||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module discord-auto-voice
|
module discord-auto-voice
|
||||||
|
|
||||||
go 1.24.4
|
go 1.25.3
|
||||||
|
|
||||||
require github.com/bwmarrin/discordgo v0.29.0
|
require github.com/bwmarrin/discordgo v0.29.0
|
||||||
|
|
||||||
|
|||||||
227
main.go
227
main.go
@@ -29,6 +29,9 @@ var dbPath = func() string {
|
|||||||
return GetENV("DB_PATH", "guild_config.db")
|
return GetENV("DB_PATH", "guild_config.db")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// irgendwo global
|
||||||
|
var presenceGuildID = GetENV("PRESENCE_GUILD_ID", "")
|
||||||
|
|
||||||
// ===== Per-Guild Config (in-memory) =====
|
// ===== Per-Guild Config (in-memory) =====
|
||||||
type GuildConfig struct {
|
type GuildConfig struct {
|
||||||
LobbyName string `json:"lobby_name"`
|
LobbyName string `json:"lobby_name"`
|
||||||
@@ -171,6 +174,19 @@ func initDB() {
|
|||||||
addLanguageColumnIfNotExists()
|
addLanguageColumnIfNotExists()
|
||||||
ensureLobbyRuleTable()
|
ensureLobbyRuleTable()
|
||||||
|
|
||||||
|
_, _ = db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS eventlog(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
guild_id TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL, -- z.B. 'vc_created' | 'vc_deleted' | 'member_moved'
|
||||||
|
channel_id TEXT,
|
||||||
|
user_id TEXT,
|
||||||
|
ts INTEGER NOT NULL, -- unix seconds
|
||||||
|
extra TEXT
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_eventlog_guild_ts ON eventlog(guild_id, ts DESC);
|
||||||
|
`)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close DB connection
|
// Close DB connection
|
||||||
@@ -180,6 +196,21 @@ func closeDB() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func logEvent(gid, typ, chID, userID string, extra any) {
|
||||||
|
if db == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var extraJSON string
|
||||||
|
if extra != nil {
|
||||||
|
if b, err := json.Marshal(extra); err == nil {
|
||||||
|
extraJSON = string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _ = db.Exec(`INSERT INTO eventlog(guild_id, type, channel_id, user_id, ts, extra)
|
||||||
|
VALUES(?,?,?,?,?,?)`,
|
||||||
|
gid, typ, chID, userID, time.Now().Unix(), extraJSON)
|
||||||
|
}
|
||||||
|
|
||||||
// Laden der Guild-Konfiguration mit Sprache
|
// Laden der Guild-Konfiguration mit Sprache
|
||||||
func loadGuildCfgs() error {
|
func loadGuildCfgs() error {
|
||||||
rows, err := db.Query("SELECT guild_id, lobby_name, category_name, timeout_min, language FROM guild_config")
|
rows, err := db.Query("SELECT guild_id, lobby_name, category_name, timeout_min, language FROM guild_config")
|
||||||
@@ -497,6 +528,10 @@ func createPrivateVCAndMove(
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("VC-Anlage fehlgeschlagen: %w", err)
|
return nil, fmt.Errorf("VC-Anlage fehlgeschlagen: %w", err)
|
||||||
|
} else {
|
||||||
|
logEvent(guildID, "vc_created", newChan.ID, requesterID, map[string]any{
|
||||||
|
"name": newChan.Name, "timeout_min": timeoutMin,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// optionaler Move
|
// optionaler Move
|
||||||
@@ -529,6 +564,9 @@ func createPrivateVCAndMove(
|
|||||||
if moveErr != nil {
|
if moveErr != nil {
|
||||||
log.Printf("Move endgültig fehlgeschlagen: %v", moveErr)
|
log.Printf("Move endgültig fehlgeschlagen: %v", moveErr)
|
||||||
}
|
}
|
||||||
|
if moveErr == nil {
|
||||||
|
logEvent(guildID, "member_moved", newChan.ID, requesterID, nil)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,6 +602,7 @@ func watchAndCleanup(s *discordgo.Session, guildID, channelID string, timeout ti
|
|||||||
if time.Since(lastActive) >= timeout {
|
if time.Since(lastActive) >= timeout {
|
||||||
_, _ = s.ChannelDelete(channelID)
|
_, _ = s.ChannelDelete(channelID)
|
||||||
log.Println("➖ Deleted channel for guildID: " + guildID + " with ID: " + channelID)
|
log.Println("➖ Deleted channel for guildID: " + guildID + " with ID: " + channelID)
|
||||||
|
logEvent(guildID, "vc_deleted", channelID, "", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1131,29 +1170,181 @@ func onInteractionCreate(_ string) func(s *discordgo.Session, i *discordgo.Inter
|
|||||||
Data: &discordgo.InteractionResponseData{Content: b.String(), Flags: discordgo.MessageFlagsEphemeral},
|
Data: &discordgo.InteractionResponseData{Content: b.String(), Flags: discordgo.MessageFlagsEphemeral},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
case "setpresence":
|
||||||
|
// nur erlaubte Guild
|
||||||
|
if presenceGuildID == "" || i.GuildID != presenceGuildID {
|
||||||
|
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: "❌ This command is only available on the owner guild.",
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// zusätzlich Admin-Check
|
||||||
|
if !isGuildAdmin(s, i.GuildID, i.User, i.Member) {
|
||||||
|
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: "❌ Admins only.",
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options lesen
|
||||||
|
var mode, text, status, url string
|
||||||
|
for _, o := range i.ApplicationCommandData().Options {
|
||||||
|
switch o.Name {
|
||||||
|
case "mode":
|
||||||
|
mode = strings.ToLower(o.StringValue())
|
||||||
|
case "text":
|
||||||
|
text = o.StringValue()
|
||||||
|
case "status":
|
||||||
|
status = strings.ToLower(o.StringValue())
|
||||||
|
case "url":
|
||||||
|
url = o.StringValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if text == "" {
|
||||||
|
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: "Please provide activity text.",
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping Mode → ActivityType
|
||||||
|
var at discordgo.ActivityType
|
||||||
|
switch mode {
|
||||||
|
case "playing":
|
||||||
|
at = discordgo.ActivityTypeGame
|
||||||
|
case "listening":
|
||||||
|
at = discordgo.ActivityTypeListening
|
||||||
|
case "watching":
|
||||||
|
at = discordgo.ActivityTypeWatching
|
||||||
|
case "streaming":
|
||||||
|
at = discordgo.ActivityTypeStreaming
|
||||||
|
case "competing":
|
||||||
|
at = discordgo.ActivityTypeCompeting
|
||||||
|
default:
|
||||||
|
at = discordgo.ActivityTypeGame
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status normalisieren
|
||||||
|
switch status {
|
||||||
|
case "online", "idle", "dnd", "invisible":
|
||||||
|
// ok
|
||||||
|
default:
|
||||||
|
status = "online"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presence setzen
|
||||||
|
err := s.UpdateStatusComplex(discordgo.UpdateStatusData{
|
||||||
|
Status: status, // "online" | "idle" | "dnd" | "invisible"
|
||||||
|
Activities: []*discordgo.Activity{
|
||||||
|
{
|
||||||
|
Name: text,
|
||||||
|
Type: at,
|
||||||
|
URL: func() string {
|
||||||
|
if at == discordgo.ActivityTypeStreaming {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AFK: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: "⚠️ Failed to update presence: " + err.Error(),
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: "✅ Presence updated.",
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// zusätzliches privates Kommando
|
||||||
|
var privateCommands = []*discordgo.ApplicationCommand{
|
||||||
|
{
|
||||||
|
Name: "setpresence",
|
||||||
|
Description: "Set bot presence (owner guild only)",
|
||||||
|
DefaultMemberPermissions: &adminPerm, // nur Admins deiner Guild
|
||||||
|
Options: []*discordgo.ApplicationCommandOption{
|
||||||
|
{
|
||||||
|
Type: discordgo.ApplicationCommandOptionString,
|
||||||
|
Name: "mode",
|
||||||
|
Description: "Activity type",
|
||||||
|
Required: true,
|
||||||
|
Choices: []*discordgo.ApplicationCommandOptionChoice{
|
||||||
|
{Name: "playing", Value: "playing"},
|
||||||
|
{Name: "listening", Value: "listening"},
|
||||||
|
{Name: "watching", Value: "watching"},
|
||||||
|
{Name: "streaming", Value: "streaming"},
|
||||||
|
{Name: "competing", Value: "competing"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{Type: discordgo.ApplicationCommandOptionString, Name: "text", Description: "Activity text", Required: true},
|
||||||
|
{
|
||||||
|
Type: discordgo.ApplicationCommandOptionString, Name: "status", Description: "online/idle/dnd/invisible", Required: false,
|
||||||
|
Choices: []*discordgo.ApplicationCommandOptionChoice{
|
||||||
|
{Name: "online", Value: "online"},
|
||||||
|
{Name: "idle", Value: "idle"},
|
||||||
|
{Name: "dnd", Value: "dnd"},
|
||||||
|
{Name: "invisible", Value: "invisible"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{Type: discordgo.ApplicationCommandOptionString, Name: "url", Description: "Streaming URL (only for streaming)", Required: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerCommandsForGuild(s *discordgo.Session, guildID string) {
|
||||||
|
appID := s.State.User.ID
|
||||||
|
|
||||||
|
// Basis-Commands für alle
|
||||||
|
cmds := make([]*discordgo.ApplicationCommand, 0, len(slashCommands)+len(privateCommands))
|
||||||
|
cmds = append(cmds, slashCommands...)
|
||||||
|
|
||||||
|
// Private Commands nur für Owner-Guild
|
||||||
|
if presenceGuildID != "" && guildID == presenceGuildID {
|
||||||
|
cmds = append(cmds, privateCommands...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomar überschreiben → keine Duplikate / keine alten Schemas
|
||||||
|
created, err := s.ApplicationCommandBulkOverwrite(appID, guildID, cmds)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("BulkOverwrite %s failed: %v", guildID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
createdCmds.Store(guildID, created)
|
||||||
|
log.Printf("Registered %d commands in guild %s", len(created), guildID)
|
||||||
|
}
|
||||||
|
|
||||||
// Handler: Commands registrieren, wenn Bot auf neuen Server kommt
|
// Handler: Commands registrieren, wenn Bot auf neuen Server kommt
|
||||||
func onGuildCreate(s *discordgo.Session, g *discordgo.GuildCreate) {
|
func onGuildCreate(s *discordgo.Session, g *discordgo.GuildCreate) {
|
||||||
appID := s.State.User.ID
|
// Hinweis: GuildCreate feuert auch beim (Re)Connect. Genau das wollen wir hier.
|
||||||
log.Printf("Registriere Commands in neuer Guild: %s (%s)", g.Name, g.ID)
|
log.Printf("GuildCreate: %s (%s) unavailable=%v", g.Name, g.ID, g.Unavailable)
|
||||||
for _, cmd := range slashCommands {
|
registerCommandsForGuild(s, g.ID)
|
||||||
c, err := s.ApplicationCommandCreate(appID, g.ID, cmd)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Command-Registrierung in %s fehlgeschlagen: %v", g.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commands für die Guild speichern
|
|
||||||
// Wir verwenden sync.Map, also müssen wir die 'Load' und 'Store' Methoden verwenden.
|
|
||||||
commands, _ := createdCmds.LoadOrStore(g.ID, []*discordgo.ApplicationCommand{})
|
|
||||||
commands = append(commands.([]*discordgo.ApplicationCommand), c)
|
|
||||||
createdCmds.Store(g.ID, commands)
|
|
||||||
|
|
||||||
log.Printf("Registriere Command %s in Guild %s", cmd.Name, g.Name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Commands Definition =====
|
// ===== Commands Definition =====
|
||||||
@@ -1298,6 +1489,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("Bot online. Ctrl+C zum Beenden.")
|
log.Println("Bot online. Ctrl+C zum Beenden.")
|
||||||
|
|
||||||
|
_ = s.UpdateGameStatus(0, "creating private VCs")
|
||||||
|
|
||||||
stop := make(chan os.Signal, 1)
|
stop := make(chan os.Signal, 1)
|
||||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||||
<-stop
|
<-stop
|
||||||
|
|||||||
Reference in New Issue
Block a user