This commit is contained in:
+116
-19
@@ -13,9 +13,10 @@ import (
|
||||
)
|
||||
|
||||
type Bot struct {
|
||||
cfg Config
|
||||
store *Store
|
||||
dg *discordgo.Session
|
||||
cfg Config
|
||||
store *Store
|
||||
dg *discordgo.Session
|
||||
cleanupStop chan struct{}
|
||||
}
|
||||
|
||||
func NewBot(cfg Config, store *Store) (*Bot, error) {
|
||||
@@ -24,7 +25,7 @@ func NewBot(cfg Config, store *Store) (*Bot, error) {
|
||||
return nil, err
|
||||
}
|
||||
dg.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsDirectMessages
|
||||
b := &Bot{cfg: cfg, store: store, dg: dg}
|
||||
b := &Bot{cfg: cfg, store: store, dg: dg, cleanupStop: make(chan struct{})}
|
||||
dg.AddHandler(b.onReady)
|
||||
dg.AddHandler(b.onInteraction)
|
||||
return b, nil
|
||||
@@ -34,10 +35,17 @@ func (b *Bot) Start() error {
|
||||
if err := b.dg.Open(); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.registerCommands()
|
||||
if err := b.registerCommands(); err != nil {
|
||||
return err
|
||||
}
|
||||
b.startThreadCleanup()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bot) Stop() { _ = b.dg.Close() }
|
||||
func (b *Bot) Stop() {
|
||||
close(b.cleanupStop)
|
||||
_ = b.dg.Close()
|
||||
}
|
||||
|
||||
func (b *Bot) onReady(s *discordgo.Session, r *discordgo.Ready) {
|
||||
log.Println("Bot ist online als", r.User.String())
|
||||
@@ -51,7 +59,7 @@ func (b *Bot) registerCommands() error {
|
||||
{Name: "ware", Description: "Commodity oder Item", Type: discordgo.ApplicationCommandOptionString, Required: true},
|
||||
{Name: "qualitaet", Description: "Qualität 0-1000", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(0), MaxValue: float64(1000)},
|
||||
{Name: "menge", Description: "Menge als Zahl", Type: discordgo.ApplicationCommandOptionNumber, Required: true, MinValue: ptrFloat(0.0001)},
|
||||
{Name: "einheit", Description: "SCU für Commodities, Stück für Items", Type: discordgo.ApplicationCommandOptionString, Required: true, Choices: []*discordgo.ApplicationCommandOptionChoice{{Name: "SCU", Value: "SCU"}, {Name: "Stück", Value: "Stück"}}},
|
||||
{Name: "einheit", Description: "SCU/cSCU für Commodities, Stück für Items", Type: discordgo.ApplicationCommandOptionString, Required: true, Choices: unitChoices()},
|
||||
{Name: "frist", Description: "Datum im Format YYYY-MM-DD", Type: discordgo.ApplicationCommandOptionString, Required: true},
|
||||
{Name: "lieferort", Description: "Liefer-Ort als Freitext", Type: discordgo.ApplicationCommandOptionString, Required: true},
|
||||
{Name: "budget", Description: "Maximales Budget in aUEC", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(0)},
|
||||
@@ -64,10 +72,10 @@ func (b *Bot) registerCommands() error {
|
||||
{Name: "auftrag_status_setzen", Description: "Admin/Team: setzt den Status eines Auftrags", Options: []*discordgo.ApplicationCommandOption{{Name: "id", Description: "Auftrags-ID", Type: discordgo.ApplicationCommandOptionInteger, Required: true}, {Name: "status", Description: "Neuer Status", Type: discordgo.ApplicationCommandOptionString, Required: true, Choices: statusChoices()}, {Name: "notiz", Description: "Interne Notiz", Type: discordgo.ApplicationCommandOptionString, Required: false}}},
|
||||
{Name: "auftrag_abbrechen", Description: "Bricht einen Auftrag ab", Options: []*discordgo.ApplicationCommandOption{{Name: "id", Description: "Auftrags-ID", Type: discordgo.ApplicationCommandOptionInteger, Required: true}, {Name: "grund", Description: "Optionaler Grund", Type: discordgo.ApplicationCommandOptionString, Required: false}}},
|
||||
{Name: "auftrag_nachricht", Description: "Sendet eine Nachricht an Einreicher und Annehmer", Options: []*discordgo.ApplicationCommandOption{{Name: "id", Description: "Auftrags-ID", Type: discordgo.ApplicationCommandOptionInteger, Required: true}, {Name: "text", Description: "Nachricht", Type: discordgo.ApplicationCommandOptionString, Required: true}}},
|
||||
{Name: "lager_add", Description: "Admin: Lagerbestand hinzufügen oder Bestand setzen", Options: []*discordgo.ApplicationCommandOption{{Name: "ware", Description: "Commodity oder Item", Type: discordgo.ApplicationCommandOptionString, Required: true}, {Name: "qualitaet", Description: "Qualität 0-1000", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(0), MaxValue: float64(1000)}, {Name: "menge", Description: "Menge als Zahl", Type: discordgo.ApplicationCommandOptionNumber, Required: true}, {Name: "einheit", Description: "SCU oder Stück", Type: discordgo.ApplicationCommandOptionString, Required: true, Choices: []*discordgo.ApplicationCommandOptionChoice{{Name: "SCU", Value: "SCU"}, {Name: "Stück", Value: "Stück"}}}, {Name: "ort", Description: "Lagerort", Type: discordgo.ApplicationCommandOptionString, Required: false}, {Name: "notiz", Description: "Interne Notiz", Type: discordgo.ApplicationCommandOptionString, Required: false}, {Name: "modus", Description: "addiert oder setzt Bestand", Type: discordgo.ApplicationCommandOptionString, Required: false, Choices: []*discordgo.ApplicationCommandOptionChoice{{Name: "addieren", Value: "add"}, {Name: "setzen", Value: "set"}}}}},
|
||||
{Name: "lager_remove", Description: "Admin: Lagerbestand reduzieren", Options: []*discordgo.ApplicationCommandOption{{Name: "ware", Description: "Commodity oder Item", Type: discordgo.ApplicationCommandOptionString, Required: true}, {Name: "qualitaet", Description: "Qualität 0-1000", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(0), MaxValue: float64(1000)}, {Name: "menge", Description: "Menge als Zahl", Type: discordgo.ApplicationCommandOptionNumber, Required: true, MinValue: ptrFloat(0.0001)}, {Name: "einheit", Description: "SCU oder Stück", Type: discordgo.ApplicationCommandOptionString, Required: true, Choices: []*discordgo.ApplicationCommandOptionChoice{{Name: "SCU", Value: "SCU"}, {Name: "Stück", Value: "Stück"}}}, {Name: "ort", Description: "Lagerort", Type: discordgo.ApplicationCommandOptionString, Required: false}, {Name: "notiz", Description: "Interne Notiz", Type: discordgo.ApplicationCommandOptionString, Required: false}}},
|
||||
{Name: "lager_add", Description: "Admin: Lagerbestand hinzufügen oder Bestand setzen", Options: []*discordgo.ApplicationCommandOption{{Name: "ware", Description: "Commodity oder Item", Type: discordgo.ApplicationCommandOptionString, Required: true}, {Name: "qualitaet", Description: "Qualität 0-1000", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(0), MaxValue: float64(1000)}, {Name: "menge", Description: "Menge als Zahl", Type: discordgo.ApplicationCommandOptionNumber, Required: true}, {Name: "einheit", Description: "SCU, cSCU oder Stück", Type: discordgo.ApplicationCommandOptionString, Required: true, Choices: unitChoices()}, {Name: "ort", Description: "Lagerort", Type: discordgo.ApplicationCommandOptionString, Required: false}, {Name: "notiz", Description: "Interne Notiz", Type: discordgo.ApplicationCommandOptionString, Required: false}, {Name: "modus", Description: "addiert oder setzt Bestand", Type: discordgo.ApplicationCommandOptionString, Required: false, Choices: []*discordgo.ApplicationCommandOptionChoice{{Name: "addieren", Value: "add"}, {Name: "setzen", Value: "set"}}}}},
|
||||
{Name: "lager_remove", Description: "Admin: Lagerbestand reduzieren", Options: []*discordgo.ApplicationCommandOption{{Name: "ware", Description: "Commodity oder Item", Type: discordgo.ApplicationCommandOptionString, Required: true}, {Name: "qualitaet", Description: "Qualität 0-1000", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(0), MaxValue: float64(1000)}, {Name: "menge", Description: "Menge als Zahl", Type: discordgo.ApplicationCommandOptionNumber, Required: true, MinValue: ptrFloat(0.0001)}, {Name: "einheit", Description: "SCU, cSCU oder Stück", Type: discordgo.ApplicationCommandOptionString, Required: true, Choices: unitChoices()}, {Name: "ort", Description: "Lagerort", Type: discordgo.ApplicationCommandOptionString, Required: false}, {Name: "notiz", Description: "Interne Notiz", Type: discordgo.ApplicationCommandOptionString, Required: false}}},
|
||||
{Name: "lager_liste", Description: "Admin/Team: zeigt internes Lager", Options: []*discordgo.ApplicationCommandOption{{Name: "suche", Description: "Ware oder Ort", Type: discordgo.ApplicationCommandOptionString, Required: false}, {Name: "limit", Description: "Maximal 50", Type: discordgo.ApplicationCommandOptionInteger, Required: false, MinValue: ptrFloat(1), MaxValue: float64(50)}}},
|
||||
{Name: "lager_check", Description: "Admin/Team: prüft Lagerbestand für Ware", Options: []*discordgo.ApplicationCommandOption{{Name: "ware", Description: "Commodity oder Item", Type: discordgo.ApplicationCommandOptionString, Required: true}, {Name: "qualitaet", Description: "Mindestqualität 0-1000", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(0), MaxValue: float64(1000)}, {Name: "menge", Description: "Benötigte Menge", Type: discordgo.ApplicationCommandOptionNumber, Required: true, MinValue: ptrFloat(0.0001)}, {Name: "einheit", Description: "SCU oder Stück", Type: discordgo.ApplicationCommandOptionString, Required: true, Choices: []*discordgo.ApplicationCommandOptionChoice{{Name: "SCU", Value: "SCU"}, {Name: "Stück", Value: "Stück"}}}}},
|
||||
{Name: "lager_check", Description: "Admin/Team: prüft Lagerbestand für Ware", Options: []*discordgo.ApplicationCommandOption{{Name: "ware", Description: "Commodity oder Item", Type: discordgo.ApplicationCommandOptionString, Required: true}, {Name: "qualitaet", Description: "Mindestqualität 0-1000", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(0), MaxValue: float64(1000)}, {Name: "menge", Description: "Benötigte Menge", Type: discordgo.ApplicationCommandOptionNumber, Required: true, MinValue: ptrFloat(0.0001)}, {Name: "einheit", Description: "SCU, cSCU oder Stück", Type: discordgo.ApplicationCommandOptionString, Required: true, Choices: unitChoices()}}},
|
||||
}
|
||||
for _, cmd := range commands {
|
||||
if _, err := b.dg.ApplicationCommandCreate(b.cfg.AppID, b.cfg.GuildID, cmd); err != nil {
|
||||
@@ -78,16 +86,42 @@ func (b *Bot) registerCommands() error {
|
||||
}
|
||||
|
||||
func ptrFloat(v float64) *float64 { return &v }
|
||||
|
||||
func unitChoices() []*discordgo.ApplicationCommandOptionChoice {
|
||||
return []*discordgo.ApplicationCommandOptionChoice{
|
||||
{Name: "SCU", Value: "SCU"},
|
||||
{Name: "cSCU", Value: "cSCU"},
|
||||
{Name: "Stück", Value: "Stück"},
|
||||
}
|
||||
}
|
||||
|
||||
func statusChoices() []*discordgo.ApplicationCommandOptionChoice {
|
||||
return []*discordgo.ApplicationCommandOptionChoice{{Name: StatusOpen, Value: StatusOpen}, {Name: StatusAccepted, Value: StatusAccepted}, {Name: StatusInDelivery, Value: StatusInDelivery}, {Name: StatusDelivered, Value: StatusDelivered}, {Name: StatusCompleted, Value: StatusCompleted}, {Name: StatusDeclined, Value: StatusDeclined}, {Name: StatusCancelled, Value: StatusCancelled}}
|
||||
return []*discordgo.ApplicationCommandOptionChoice{
|
||||
{Name: statusLabel(StatusOpen), Value: StatusOpen},
|
||||
{Name: statusLabel(StatusAccepted), Value: StatusAccepted},
|
||||
{Name: statusLabel(StatusInDelivery), Value: StatusInDelivery},
|
||||
{Name: statusLabel(StatusDelivered), Value: StatusDelivered},
|
||||
{Name: statusLabel(StatusCompleted), Value: StatusCompleted},
|
||||
{Name: statusLabel(StatusDeclined), Value: StatusDeclined},
|
||||
{Name: statusLabel(StatusCancelled), Value: StatusCancelled},
|
||||
}
|
||||
}
|
||||
|
||||
func activeStatusChoices() []*discordgo.ApplicationCommandOptionChoice {
|
||||
return []*discordgo.ApplicationCommandOptionChoice{{Name: StatusOpen, Value: StatusOpen}, {Name: StatusAccepted, Value: StatusAccepted}, {Name: StatusInDelivery, Value: StatusInDelivery}, {Name: StatusDelivered, Value: StatusDelivered}}
|
||||
return []*discordgo.ApplicationCommandOptionChoice{
|
||||
{Name: statusLabel(StatusOpen), Value: StatusOpen},
|
||||
{Name: statusLabel(StatusAccepted), Value: StatusAccepted},
|
||||
{Name: statusLabel(StatusInDelivery), Value: StatusInDelivery},
|
||||
{Name: statusLabel(StatusDelivered), Value: StatusDelivered},
|
||||
}
|
||||
}
|
||||
|
||||
func archivedStatusChoices() []*discordgo.ApplicationCommandOptionChoice {
|
||||
return []*discordgo.ApplicationCommandOptionChoice{{Name: StatusCompleted, Value: StatusCompleted}, {Name: StatusDeclined, Value: StatusDeclined}, {Name: StatusCancelled, Value: StatusCancelled}}
|
||||
return []*discordgo.ApplicationCommandOptionChoice{
|
||||
{Name: statusLabel(StatusCompleted), Value: StatusCompleted},
|
||||
{Name: statusLabel(StatusDeclined), Value: StatusDeclined},
|
||||
{Name: statusLabel(StatusCancelled), Value: StatusCancelled},
|
||||
}
|
||||
}
|
||||
|
||||
func isArchivedStatus(status string) bool {
|
||||
@@ -214,7 +248,7 @@ func (b *Bot) cmdListOrders(s *discordgo.Session, i *discordgo.InteractionCreate
|
||||
|
||||
if status != "" && isArchivedStatus(status) != archived {
|
||||
if archived {
|
||||
b.replyEphemeral(s, i, "`/auftrag_archiv` zeigt nur completed, declined und cancelled. Aktive Aufträge findest du mit `/auftrag_liste`.")
|
||||
b.replyEphemeral(s, i, "`/auftrag_archiv` zeigt nur abgeschlossene, abgelehnte und abgebrochene Aufträge. Aktive Aufträge findest du mit `/auftrag_liste`.")
|
||||
} else {
|
||||
b.replyEphemeral(s, i, "`/auftrag_liste` zeigt nur aktive Aufträge. Abgeschlossene/abgelehnte/abgebrochene Aufträge findest du mit `/auftrag_archiv`.")
|
||||
}
|
||||
@@ -244,7 +278,7 @@ func (b *Bot) cmdListOrders(s *discordgo.Session, i *discordgo.InteractionCreate
|
||||
}
|
||||
var lines []string
|
||||
for _, o := range orders {
|
||||
line := fmt.Sprintf("#%d — %s, %s, Budget %d aUEC, Frist %s, Status %s", o.ID, o.Commodity, quantityString(o.QuantityAmount, o.QuantityUnit), o.MaxBudgetAUEC, o.Deadline, o.Status)
|
||||
line := fmt.Sprintf("#%d — %s, %s, Budget %d aUEC, Frist %s, Status %s", o.ID, o.Commodity, quantityString(o.QuantityAmount, o.QuantityUnit), o.MaxBudgetAUEC, o.Deadline, statusLabel(o.Status))
|
||||
if b.isTeamOrAdmin(i) {
|
||||
line += fmt.Sprintf(", Einreicher <@%s>", o.SubmitterID)
|
||||
}
|
||||
@@ -294,9 +328,9 @@ func (b *Bot) cmdSetStatus(s *discordgo.Session, i *discordgo.InteractionCreate,
|
||||
}
|
||||
active := status == StatusOpen
|
||||
b.updateOrderMessages(o, active)
|
||||
b.notifyParticipants(o, fmt.Sprintf("Status von Auftrag #%d wurde auf `%s` gesetzt. Hinweis: %s", id, status, note))
|
||||
b.audit(fmt.Sprintf("Auftrag #%d Status %s durch %s: %s", id, status, userString(i), note))
|
||||
b.replyEphemeral(s, i, fmt.Sprintf("Status von Auftrag #%d wurde auf %s gesetzt.", id, status))
|
||||
b.notifyParticipants(o, fmt.Sprintf("Status von Auftrag #%d wurde auf `%s` gesetzt. Hinweis: %s", id, statusLabel(status), note))
|
||||
b.audit(fmt.Sprintf("Auftrag #%d Status %s durch %s: %s", id, statusLabel(status), userString(i), note))
|
||||
b.replyEphemeral(s, i, fmt.Sprintf("Status von Auftrag #%d wurde auf %s gesetzt.", id, statusLabel(status)))
|
||||
}
|
||||
|
||||
func (b *Bot) cmdCancel(s *discordgo.Session, i *discordgo.InteractionCreate, id int64, reason string) {
|
||||
@@ -466,7 +500,7 @@ func orderButtons(id int64, active bool) []discordgo.MessageComponent {
|
||||
}
|
||||
|
||||
func publicOrderEmbed(o *Order) *discordgo.MessageEmbed {
|
||||
return &discordgo.MessageEmbed{Title: fmt.Sprintf("Auftrag #%d — %s", o.ID, o.Commodity), Description: "Ein neuer Lieferauftrag wurde erfasst.", Color: statusColor(o.Status), Fields: []*discordgo.MessageEmbedField{{Name: "Ware", Value: o.Commodity, Inline: true}, {Name: "Qualität", Value: fmt.Sprintf("%d/1000", o.Quality), Inline: true}, {Name: "Menge", Value: quantityString(o.QuantityAmount, o.QuantityUnit), Inline: true}, {Name: "Frist", Value: o.Deadline, Inline: true}, {Name: "Liefer-Ort", Value: o.DeliveryPlace, Inline: true}, {Name: "Budget", Value: fmt.Sprintf("%d aUEC", o.MaxBudgetAUEC), Inline: true}, {Name: "Status", Value: o.Status, Inline: true}}, Timestamp: o.UpdatedAt.Format(time.RFC3339)}
|
||||
return &discordgo.MessageEmbed{Title: fmt.Sprintf("Auftrag #%d — %s", o.ID, o.Commodity), Description: "Ein neuer Lieferauftrag wurde erfasst.", Color: statusColor(o.Status), Fields: []*discordgo.MessageEmbedField{{Name: "Ware", Value: o.Commodity, Inline: true}, {Name: "Qualität", Value: fmt.Sprintf("%d/1000", o.Quality), Inline: true}, {Name: "Menge", Value: quantityString(o.QuantityAmount, o.QuantityUnit), Inline: true}, {Name: "Frist", Value: o.Deadline, Inline: true}, {Name: "Liefer-Ort", Value: o.DeliveryPlace, Inline: true}, {Name: "Budget", Value: fmt.Sprintf("%d aUEC", o.MaxBudgetAUEC), Inline: true}, {Name: "Status", Value: statusLabel(o.Status), Inline: true}}, Timestamp: o.UpdatedAt.Format(time.RFC3339)}
|
||||
}
|
||||
|
||||
func (b *Bot) internalOrderEmbed(o *Order) *discordgo.MessageEmbed {
|
||||
@@ -507,6 +541,27 @@ func (b *Bot) inventoryText(o *Order) string {
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func statusLabel(status string) string {
|
||||
switch status {
|
||||
case StatusOpen:
|
||||
return "offen"
|
||||
case StatusAccepted:
|
||||
return "angenommen"
|
||||
case StatusInDelivery:
|
||||
return "in Lieferung"
|
||||
case StatusDelivered:
|
||||
return "geliefert"
|
||||
case StatusCompleted:
|
||||
return "abgeschlossen"
|
||||
case StatusDeclined:
|
||||
return "abgelehnt"
|
||||
case StatusCancelled:
|
||||
return "abgebrochen"
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
func statusColor(status string) int {
|
||||
switch status {
|
||||
case StatusOpen:
|
||||
@@ -531,6 +586,48 @@ func (b *Bot) tryCreateThread(channelID, messageID, name string) string {
|
||||
return thread.ID
|
||||
}
|
||||
|
||||
func (b *Bot) startThreadCleanup() {
|
||||
if b.cfg.ThreadDeleteAfter <= 0 {
|
||||
log.Println("Thread-Bereinigung ist deaktiviert.")
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
b.cleanupClosedThreads()
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
b.cleanupClosedThreads()
|
||||
case <-b.cleanupStop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (b *Bot) cleanupClosedThreads() {
|
||||
cutoff := time.Now().UTC().Add(-b.cfg.ThreadDeleteAfter)
|
||||
orders, err := b.store.ListOrdersWithDeletableThreads(cutoff, 50)
|
||||
if err != nil {
|
||||
log.Println("thread cleanup query failed:", err)
|
||||
return
|
||||
}
|
||||
for _, o := range orders {
|
||||
if o.ThreadID == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := b.dg.ChannelDelete(o.ThreadID); err != nil {
|
||||
log.Printf("thread cleanup failed for order #%d thread %s: %v", o.ID, o.ThreadID, err)
|
||||
continue
|
||||
}
|
||||
if err := b.store.ClearThreadID(o.ID); err != nil {
|
||||
log.Printf("thread cleanup db update failed for order #%d: %v", o.ID, err)
|
||||
}
|
||||
b.audit(fmt.Sprintf("Interner Thread zu Auftrag #%d wurde nach Archivierung gelöscht.", o.ID))
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) notifyParticipants(o *Order, body string) {
|
||||
b.dm(o.SubmitterID, body)
|
||||
if o.AcceptorID != "" && o.AcceptorID != o.SubmitterID {
|
||||
|
||||
Reference in New Issue
Block a user