This commit is contained in:
51
.gitea/workflows/registry.yml
Normal file
51
.gitea/workflows/registry.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: release-tag
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
jobs:
|
||||||
|
release-image:
|
||||||
|
runs-on: ubuntu-fast
|
||||||
|
env:
|
||||||
|
DOCKER_ORG: ${{ vars.DOCKER_ORG }}
|
||||||
|
DOCKER_LATEST: latest
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
with: # replace it with your local IP
|
||||||
|
config-inline: |
|
||||||
|
[registry."${{ vars.DOCKER_REGISTRY }}"]
|
||||||
|
http = true
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.DOCKER_REGISTRY }} # replace it with your local IP
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Get Meta
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||||
|
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: | # replace it with your local IP and tags
|
||||||
|
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
|
||||||
|
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
35
Dockerfile
Normal file
35
Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# -------- Dockerfile (Multi-Stage Build) --------
|
||||||
|
# 1. Builder-Stage
|
||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY go.* ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /bin/autovoice
|
||||||
|
|
||||||
|
# 2. Runtime-Stage
|
||||||
|
FROM alpine:3.22
|
||||||
|
|
||||||
|
# HTTPS-Callouts in Alpine brauchen ca-certificates
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
#RUN mkdir /data
|
||||||
|
#RUN mkdir /dynamicsrc
|
||||||
|
#RUN mkdir /tempsrc
|
||||||
|
COPY --from=builder /bin/autovoice /bin/autovoice
|
||||||
|
#COPY ./static /data/static
|
||||||
|
# COPY ./static /tempsrc/static
|
||||||
|
#COPY ./dynamicsrc /dynamicsrc
|
||||||
|
|
||||||
|
# Default listens on :8080 – siehe main.go
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Environment defaults; können per compose überschrieben werden
|
||||||
|
ENV LOBBY_CHANNEL_ID=0 \
|
||||||
|
DISCORD_TOKEN=0 \
|
||||||
|
GUILD_ID=0 \
|
||||||
|
TIMEOUT_MIN=1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/autovoice"]
|
11
go.mod
Normal file
11
go.mod
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module discord-auto-voice
|
||||||
|
|
||||||
|
go 1.24.4
|
||||||
|
|
||||||
|
require github.com/bwmarrin/discordgo v0.29.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
|
||||||
|
)
|
12
go.sum
Normal file
12
go.sum
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
||||||
|
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||||
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
324
main.go
Normal file
324
main.go
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bwmarrin/discordgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// optional: Voice-Channels unter dieser Kategorie anlegen (leer lassen = keine Kategorie)
|
||||||
|
envCategoryID = "CATEGORY_ID"
|
||||||
|
// wie oft wir prüfen, ob der Channel leer ist
|
||||||
|
pollInterval = 15 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var lobbyID = os.Getenv("LOBBY_CHANNEL_ID")
|
||||||
|
var discordToken = os.Getenv("DISCORD_TOKEN")
|
||||||
|
|
||||||
|
// /makevc Optionen:
|
||||||
|
// - name (string, optional) → Name des Voice-Channels
|
||||||
|
// - user_limit (int, optional) → Max. User (0 = unbegrenzt)
|
||||||
|
// - timeout_min (int, optional) → Auto-Delete wenn so lange leer (Default 60)
|
||||||
|
var slashCommands = []*discordgo.ApplicationCommand{
|
||||||
|
{
|
||||||
|
Name: "makevc",
|
||||||
|
Description: "Erstellt einen privaten Voice-Channel nur für dich (Auto-Cleanup)",
|
||||||
|
Options: []*discordgo.ApplicationCommandOption{
|
||||||
|
{
|
||||||
|
Type: discordgo.ApplicationCommandOptionString,
|
||||||
|
Name: "name",
|
||||||
|
Description: "Name des Channels (z.B. 'Mein Raum')",
|
||||||
|
Required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: discordgo.ApplicationCommandOptionInteger,
|
||||||
|
Name: "user_limit",
|
||||||
|
Description: "Max. Nutzer (0=unbegrenzt)",
|
||||||
|
Required: false,
|
||||||
|
MaxValue: 99, // statt ptrF(99)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: discordgo.ApplicationCommandOptionInteger,
|
||||||
|
Name: "timeout_min",
|
||||||
|
Description: "Löschen nach X Minuten Inaktivität",
|
||||||
|
Required: false,
|
||||||
|
MaxValue: 480,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func onVoiceStateUpdate(s *discordgo.Session, e *discordgo.VoiceStateUpdate) {
|
||||||
|
// nur Join-Events in die Lobby interessieren
|
||||||
|
if e.UserID == "" || e.ChannelID != lobbyID || e.BeforeUpdate != nil && e.BeforeUpdate.ChannelID == lobbyID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Bots ignorieren
|
||||||
|
m, err := s.GuildMember(e.GuildID, e.UserID)
|
||||||
|
if err == nil && m.User.Bot {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel anlegen wie im /makevc-Handler:
|
||||||
|
categoryID := os.Getenv(envCategoryID)
|
||||||
|
name := "🔒 " + safeDisplayName(m)
|
||||||
|
userLimit := 0
|
||||||
|
timeoutMin := 60
|
||||||
|
if v := os.Getenv("TIMEOUT_MIN"); v != "" {
|
||||||
|
fmt.Sscanf(v, "%d", &timeoutMin)
|
||||||
|
}
|
||||||
|
|
||||||
|
everyoneID := e.GuildID
|
||||||
|
allowOwner := int64(discordgo.PermissionViewChannel |
|
||||||
|
discordgo.PermissionVoiceConnect |
|
||||||
|
discordgo.PermissionVoiceSpeak |
|
||||||
|
discordgo.PermissionVoiceStreamVideo |
|
||||||
|
discordgo.PermissionVoiceUseVAD)
|
||||||
|
denyEveryone := int64(discordgo.PermissionViewChannel |
|
||||||
|
discordgo.PermissionVoiceConnect)
|
||||||
|
|
||||||
|
newChan, err := s.GuildChannelCreateComplex(e.GuildID, discordgo.GuildChannelCreateData{
|
||||||
|
Name: name,
|
||||||
|
Type: discordgo.ChannelTypeGuildVoice,
|
||||||
|
ParentID: categoryID,
|
||||||
|
UserLimit: userLimit,
|
||||||
|
Bitrate: 64000,
|
||||||
|
PermissionOverwrites: []*discordgo.PermissionOverwrite{
|
||||||
|
{ID: everyoneID, Type: discordgo.PermissionOverwriteTypeRole, Deny: denyEveryone},
|
||||||
|
{ID: e.UserID, Type: discordgo.PermissionOverwriteTypeMember, Allow: allowOwner},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("VC-Anlage fehlgeschlagen: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// User rüber bewegen
|
||||||
|
if moveErr := s.GuildMemberMove(e.GuildID, e.UserID, &newChan.ID); moveErr != nil {
|
||||||
|
log.Printf("Move fehlgeschlagen: %v", moveErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-Cleanup starten
|
||||||
|
go watchAndCleanup(s, e.GuildID, newChan.ID, time.Duration(timeoutMin)*time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeDisplayName(m *discordgo.Member) string {
|
||||||
|
if m == nil {
|
||||||
|
return "privat"
|
||||||
|
}
|
||||||
|
if m.Nick != "" {
|
||||||
|
return m.Nick
|
||||||
|
}
|
||||||
|
if m.User.GlobalName != "" {
|
||||||
|
return m.User.GlobalName
|
||||||
|
}
|
||||||
|
return m.User.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
token := discordToken
|
||||||
|
if token == "" {
|
||||||
|
log.Fatal("Bitte setze DISCORD_TOKEN")
|
||||||
|
}
|
||||||
|
guildID := os.Getenv("GUILD_ID") // optional: für schnelle Command-Registrierung
|
||||||
|
categoryID := os.Getenv(envCategoryID)
|
||||||
|
|
||||||
|
s, err := discordgo.New("Bot " + token)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Session fehlgeschlagen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intents für Slash + Voice-State-Events
|
||||||
|
s.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildVoiceStates
|
||||||
|
s.AddHandler(onVoiceStateUpdate) // ← neu
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
s.AddHandler(func(_ *discordgo.Session, r *discordgo.Ready) {
|
||||||
|
log.Printf("Eingeloggt als %s", r.User.Username)
|
||||||
|
})
|
||||||
|
s.AddHandler(onInteractionCreate(categoryID))
|
||||||
|
|
||||||
|
if err := s.Open(); err != nil {
|
||||||
|
log.Fatalf("Gateway-Start fehlgeschlagen: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("Bot online. Ctrl+C zum Beenden.")
|
||||||
|
|
||||||
|
// Slash-Commands registrieren (Guild-spezifisch = sofort sichtbar)
|
||||||
|
appID := s.State.User.ID
|
||||||
|
created := make([]*discordgo.ApplicationCommand, 0, len(slashCommands))
|
||||||
|
for _, cmd := range slashCommands {
|
||||||
|
c, err := s.ApplicationCommandCreate(appID, guildID, cmd)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Command-Registrierung fehlgeschlagen: %v", err)
|
||||||
|
}
|
||||||
|
created = append(created, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown
|
||||||
|
stop := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||||
|
<-stop
|
||||||
|
log.Println("Fahre herunter…")
|
||||||
|
for _, c := range created {
|
||||||
|
_ = s.ApplicationCommandDelete(appID, guildID, c.ID)
|
||||||
|
}
|
||||||
|
_ = s.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func onInteractionCreate(categoryID string) func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
|
return func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
|
if i.Type != discordgo.InteractionApplicationCommand {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if i.ApplicationCommandData().Name != "makevc" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options lesen
|
||||||
|
var name string
|
||||||
|
userLimit := 0
|
||||||
|
timeoutMin := 60
|
||||||
|
for _, o := range i.ApplicationCommandData().Options {
|
||||||
|
switch o.Name {
|
||||||
|
case "name":
|
||||||
|
name = o.StringValue()
|
||||||
|
case "user_limit":
|
||||||
|
userLimit = int(o.IntValue())
|
||||||
|
case "timeout_min":
|
||||||
|
timeoutMin = int(o.IntValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user := i.User
|
||||||
|
if user == nil && i.Member != nil {
|
||||||
|
user = i.Member.User
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
name = fmt.Sprintf("🔒 %s", displayName(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission Overwrites: @everyone deny, Owner allow
|
||||||
|
guildID := i.GuildID
|
||||||
|
everyoneID := guildID // @everyone Rolle hat die ID der Guild
|
||||||
|
allowOwner := int64(discordgo.PermissionViewChannel |
|
||||||
|
discordgo.PermissionVoiceConnect |
|
||||||
|
discordgo.PermissionVoiceSpeak |
|
||||||
|
discordgo.PermissionVoiceStreamVideo |
|
||||||
|
discordgo.PermissionVoiceUseVAD)
|
||||||
|
denyEveryone := int64(discordgo.PermissionViewChannel |
|
||||||
|
discordgo.PermissionVoiceConnect)
|
||||||
|
|
||||||
|
newChan, err := s.GuildChannelCreateComplex(guildID, discordgo.GuildChannelCreateData{
|
||||||
|
Name: name,
|
||||||
|
Type: discordgo.ChannelTypeGuildVoice,
|
||||||
|
// optional unter Kategorie
|
||||||
|
ParentID: categoryID,
|
||||||
|
// Userlimit (0 = unlimited)
|
||||||
|
UserLimit: userLimit,
|
||||||
|
// Bitrate (optional): 64kbps ist sicher für die meisten Server
|
||||||
|
Bitrate: 64000,
|
||||||
|
PermissionOverwrites: []*discordgo.PermissionOverwrite{
|
||||||
|
{
|
||||||
|
ID: everyoneID,
|
||||||
|
Type: discordgo.PermissionOverwriteTypeRole,
|
||||||
|
Deny: denyEveryone,
|
||||||
|
Allow: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: user.ID,
|
||||||
|
Type: discordgo.PermissionOverwriteTypeMember,
|
||||||
|
Allow: allowOwner,
|
||||||
|
Deny: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
errMsg := "Konnte Voice-Channel nicht erstellen. Fehlen dem Bot die Rechte **Manage Channels**?"
|
||||||
|
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: errMsg,
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sofort antworten (ephemeral)
|
||||||
|
msg := fmt.Sprintf("✅ Voice-Channel **%s** erstellt. Ich lösche ihn, wenn er leer ist (Timeout: %d min).", newChan.Name, timeoutMin)
|
||||||
|
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: msg,
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Auto-Cleanup starten
|
||||||
|
go watchAndCleanup(s, i.GuildID, newChan.ID, time.Duration(timeoutMin)*time.Minute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayName(i *discordgo.InteractionCreate) string {
|
||||||
|
if i.Member != nil && i.Member.Nick != "" {
|
||||||
|
return i.Member.Nick
|
||||||
|
}
|
||||||
|
if i.Member != nil && i.Member.User.GlobalName != "" {
|
||||||
|
return i.Member.User.GlobalName
|
||||||
|
}
|
||||||
|
if i.Member != nil {
|
||||||
|
return i.Member.User.Username
|
||||||
|
}
|
||||||
|
if i.User != nil && i.User.GlobalName != "" {
|
||||||
|
return i.User.GlobalName
|
||||||
|
}
|
||||||
|
if i.User != nil {
|
||||||
|
return i.User.Username
|
||||||
|
}
|
||||||
|
return "privat"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüft regelmäßig, ob der Channel leer ist. Wenn er für `timeout` leer bleibt, wird er gelöscht.
|
||||||
|
// Sobald wieder jemand drin ist, wird der Timer zurückgesetzt.
|
||||||
|
func watchAndCleanup(s *discordgo.Session, guildID, channelID string, timeout time.Duration) {
|
||||||
|
lastActive := time.Now()
|
||||||
|
ticker := time.NewTicker(pollInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
// Guild-State holen (State Cache kann fehlen → REST-Fallback)
|
||||||
|
g, err := s.State.Guild(guildID)
|
||||||
|
if err != nil {
|
||||||
|
// als Fallback Guild live ziehen
|
||||||
|
g, err = s.Guild(guildID)
|
||||||
|
if err != nil {
|
||||||
|
// Wenn wir die Guild nicht bekommen, versuche später erneut
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
occupied := false
|
||||||
|
for _, vs := range g.VoiceStates {
|
||||||
|
if vs.ChannelID == channelID {
|
||||||
|
occupied = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if occupied {
|
||||||
|
lastActive = time.Now()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// keiner im Channel
|
||||||
|
if time.Since(lastActive) >= timeout {
|
||||||
|
_, _ = s.ChannelDelete(channelID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user