diff --git a/README.md b/README.md index 29b9ce1..b4c8c0e 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ The eventlog listens for important events such as kicks, bans, role changes, etc - `/eventlog channel` can be used by anyone with the `Manage Server` permission to set the channel for the event log - `/eventlog ticket_channel` can be used by anyone with the `Manage Server` permission to set the channel in which ticket conversations logs will be sent +- `/eventlog time_format` can be used by anyone with the `Manage Server` permission to set the time format for embeds. You can either use `discord` for a timezone-agnostic timestamp or a [strftime string](https://github.com/lestrrat-go/strftime#supported-conversion-specifications), which will be in UTC. ### Reactions diff --git a/config.go b/config.go index 416c6c7..130bf3b 100644 --- a/config.go +++ b/config.go @@ -48,7 +48,6 @@ func loadConfig() (*Config, error) { } fl.Close() } - return cfg, env.ParseWithOptions(cfg, env.Options{Prefix: "OWOBOT_"}) } diff --git a/go.mod b/go.mod index 9bae0bd..de60aa0 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/bwmarrin/discordgo v0.27.1 github.com/caarlos0/env/v10 v10.0.0 github.com/jmoiron/sqlx v1.3.5 + github.com/lestrrat-go/strftime v1.0.6 github.com/pelletier/go-toml/v2 v2.1.0 github.com/rivo/uniseg v0.4.4 github.com/valyala/fasttemplate v1.2.2 @@ -22,6 +23,7 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mvdan/xurls v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect diff --git a/go.sum b/go.sum index a8cae5d..3716348 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,10 @@ github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= +github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ= +github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= @@ -34,6 +38,8 @@ github.com/mvdan/xurls v1.1.0 h1:OpuDelGQ1R1ueQ6sSryzi6P+1RtBpfQHM8fJwlE45ww= github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -43,6 +49,7 @@ github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= diff --git a/internal/db/guilds.go b/internal/db/guilds.go index ca836eb..28d6f7a 100644 --- a/internal/db/guilds.go +++ b/internal/db/guilds.go @@ -32,6 +32,7 @@ type Guild struct { TicketCategoryID string `db:"ticket_category_id"` VettingReqChanID string `db:"vetting_req_chan_id"` VettingRoleID string `db:"vetting_role_id"` + TimeFormat string `db:"time_format"` } func AllGuilds() ([]Guild, error) { @@ -86,6 +87,11 @@ func SetVettingRoleID(guildID, roleID string) error { return err } +func SetTimeFormat(guildID, timeFmt string) error { + _, err := db.Exec("UPDATE guilds SET time_format = ? WHERE id = ?", timeFmt, guildID) + return err +} + func IsVettingMsg(msgID string) (bool, error) { var out bool err := db.QueryRow("SELECT 1 FROM guild WHERE vetting_msg_id = ?", msgID).Scan(&out) diff --git a/internal/db/migrations/2023120400.sql b/internal/db/migrations/2023120400.sql index 7a56366..02c20a7 100644 --- a/internal/db/migrations/2023120400.sql +++ b/internal/db/migrations/2023120400.sql @@ -12,7 +12,8 @@ CREATE TABLE guilds ( ticket_log_chan_id TEXT NOT NULL DEFAULT '', ticket_category_id TEXT NOT NULL DEFAULT '', vetting_req_chan_id TEXT NOT NULL DEFAULT '', - vetting_role_id TEXT NOT NULL DEFAULT '' + vetting_role_id TEXT NOT NULL DEFAULT '', + time_format TEXT NOT NULL DEFAULT 'discord' ); /* tickets holds all open tickets */ diff --git a/internal/systems/eventlog/eventlog.go b/internal/systems/eventlog/eventlog.go index 9be32e1..ea33be8 100644 --- a/internal/systems/eventlog/eventlog.go +++ b/internal/systems/eventlog/eventlog.go @@ -21,7 +21,6 @@ package eventlog import ( "fmt" "io" - "time" "github.com/bwmarrin/discordgo" "go.elara.ws/owobot/internal/db" @@ -63,6 +62,19 @@ func Init(s *discordgo.Session) error { }, }, }, + { + Name: "time_format", + Description: "Set the time format for embeds", + Type: discordgo.ApplicationCommandOptionSubCommand, + Options: []*discordgo.ApplicationCommandOption{ + { + Name: "format", + Description: "The time format to use", + Type: discordgo.ApplicationCommandOptionString, + Required: true, + }, + }, + }, }, }) @@ -76,6 +88,8 @@ func eventlogCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error { return channelCmd(s, i) case "ticket_channel": return ticketChannelCmd(s, i) + case "time_format": + return timeFormatCmd(s, i) default: return fmt.Errorf("unknown eventlog subcommand: %s", name) } @@ -107,6 +121,19 @@ func ticketChannelCmd(s *discordgo.Session, i *discordgo.InteractionCreate) erro return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set ticket log channel to <#%s>!", c.ID)) } +func timeFormatCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error { + // Get the subcommand options + args := i.ApplicationCommandData().Options[0].Options + timeFmt := args[0].StringValue() + + err := db.SetTimeFormat(i.GuildID, timeFmt) + if err != nil { + return err + } + + return util.RespondEphemeral(s, i.Interaction, "Successfully set the time format!") +} + type Entry struct { Title string Description string @@ -127,9 +154,6 @@ func Log(s *discordgo.Session, guildID string, e Entry) error { embed := &discordgo.MessageEmbed{ Title: e.Title, Description: e.Description, - Footer: &discordgo.MessageEmbedFooter{ - Text: util.FormatJucheTime(time.Now()), - }, } if e.Author != nil { @@ -143,6 +167,8 @@ func Log(s *discordgo.Session, guildID string, e Entry) error { embed.Image = &discordgo.MessageEmbedImage{URL: e.ImageURL} } + AddTimeToEmbed(guild.TimeFormat, embed) + _, err = s.ChannelMessageSendEmbed(guild.LogChanID, embed) return err } diff --git a/internal/systems/eventlog/time.go b/internal/systems/eventlog/time.go new file mode 100644 index 0000000..7635be5 --- /dev/null +++ b/internal/systems/eventlog/time.go @@ -0,0 +1,38 @@ +package eventlog + +import ( + "fmt" + "time" + + "github.com/bwmarrin/discordgo" + "github.com/lestrrat-go/strftime" +) + +// AddTimeToEmbed formats the current time using timeFmt and adds it to e. +// The timeFmt can either be "discord", "juche", or a strftime string. +func AddTimeToEmbed(timeFmt string, e *discordgo.MessageEmbed) *discordgo.MessageEmbed { + t := time.Now().In(time.UTC) + switch timeFmt { + case "discord": + e.Timestamp = t.Format(time.RFC3339) + case "juche": + e.Footer = &discordgo.MessageEmbedFooter{Text: formatJuche(t)} + default: + e.Footer = &discordgo.MessageEmbedFooter{Text: format(timeFmt, t)} + } + return e +} + +// formatJuche formats the given time in Juche calendar format +func formatJuche(t time.Time) string { + return fmt.Sprintf("%02d:%02d %02d-%02d Juche %d", t.Hour(), t.Minute(), t.Day(), t.Month(), t.Year()-1911) +} + +// format formats t using timeFmt +func format(timeFmt string, t time.Time) string { + timeStr, err := strftime.Format(timeFmt, t) + if err != nil { + return "ERROR: " + err.Error() + } + return timeStr +} diff --git a/internal/systems/starboard/starboard.go b/internal/systems/starboard/starboard.go index 6c6ba42..c893284 100644 --- a/internal/systems/starboard/starboard.go +++ b/internal/systems/starboard/starboard.go @@ -25,7 +25,6 @@ import ( "net/url" "path" "strings" - "time" "mvdan.cc/xurls" @@ -33,6 +32,7 @@ import ( "go.elara.ws/logger/log" "go.elara.ws/owobot/internal/db" "go.elara.ws/owobot/internal/systems/commands" + "go.elara.ws/owobot/internal/systems/eventlog" "go.elara.ws/owobot/internal/util" ) @@ -200,11 +200,10 @@ func onReaction(s *discordgo.Session, mra *discordgo.MessageReactionAdd) { msg.ID, ), Color: embedColor, - Footer: &discordgo.MessageEmbedFooter{ - Text: util.FormatJucheTime(time.Now()), - }, } + eventlog.AddTimeToEmbed(guild.TimeFormat, embed) + if imageURL := getImageURL(msg); imageURL != "" { // If the message has an image, add it to the embed embed.Image = &discordgo.MessageEmbedImage{URL: imageURL} diff --git a/internal/systems/vetting/handlers.go b/internal/systems/vetting/handlers.go index 0847ba6..b04a62a 100644 --- a/internal/systems/vetting/handlers.go +++ b/internal/systems/vetting/handlers.go @@ -24,7 +24,6 @@ import ( "fmt" "slices" "strings" - "time" "github.com/bwmarrin/discordgo" "go.elara.ws/logger/log" @@ -151,18 +150,19 @@ func onVettingRequest(s *discordgo.Session, i *discordgo.InteractionCreate) erro return errors.New("you do not have the vetting role") } - _, err = s.ChannelMessageSendComplex(guild.VettingReqChanID, &discordgo.MessageSend{ - Embed: &discordgo.MessageEmbed{ - Title: "Vetting Request", - Author: &discordgo.MessageEmbedAuthor{ - Name: i.Member.User.Username, - IconURL: i.Member.User.AvatarURL(""), - }, - Description: "Accept the vetting request to create a ticket, or reject it to kick the user.", - Footer: &discordgo.MessageEmbedFooter{ - Text: util.FormatJucheTime(time.Now()), - }, + embed := &discordgo.MessageEmbed{ + Title: "Vetting Request", + Author: &discordgo.MessageEmbedAuthor{ + Name: i.Member.User.Username, + IconURL: i.Member.User.AvatarURL(""), }, + Description: "Accept the vetting request to create a ticket, or reject it to kick the user.", + } + + eventlog.AddTimeToEmbed(guild.TimeFormat, embed) + + _, err = s.ChannelMessageSendComplex(guild.VettingReqChanID, &discordgo.MessageSend{ + Embeds: []*discordgo.MessageEmbed{embed}, Components: []discordgo.MessageComponent{ discordgo.ActionsRow{Components: []discordgo.MessageComponent{ discordgo.Button{ @@ -270,6 +270,11 @@ func onVettingResponse(s *discordgo.Session, i *discordgo.InteractionCreate) err return nil } + guild, err := db.GuildByID(i.GuildID) + if err != nil { + return err + } + executor := i.Member member, err := cache.Member(s, i.GuildID, userID) if err != nil { @@ -283,22 +288,21 @@ func onVettingResponse(s *discordgo.Session, i *discordgo.InteractionCreate) err return err } + embed := &discordgo.MessageEmbed{ + Title: "Vetting Request Accepted!", + Description: fmt.Sprintf("This vetting request has been accepted and a vetting ticket has been created at <#%s>.\n\n**Accepted By:** <@%s>", channelID, executor.User.ID), + Author: &discordgo.MessageEmbedAuthor{ + Name: member.User.Username, + IconURL: member.User.AvatarURL(""), + }, + } + + eventlog.AddTimeToEmbed(guild.TimeFormat, embed) + err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseUpdateMessage, Data: &discordgo.InteractionResponseData{ - Embeds: []*discordgo.MessageEmbed{ - { - Title: "Vetting Request Accepted!", - Description: fmt.Sprintf("This vetting request has been accepted and a vetting ticket has been created at <#%s>.\n\n**Accepted By:** <@%s>", channelID, executor.User.ID), - Author: &discordgo.MessageEmbedAuthor{ - Name: member.User.Username, - IconURL: member.User.AvatarURL(""), - }, - Footer: &discordgo.MessageEmbedFooter{ - Text: util.FormatJucheTime(time.Now()), - }, - }, - }, + Embeds: []*discordgo.MessageEmbed{embed}, }, }) if err != nil { @@ -310,22 +314,21 @@ func onVettingResponse(s *discordgo.Session, i *discordgo.InteractionCreate) err return err } + embed := &discordgo.MessageEmbed{ + Title: "Vetting Request Rejected", + Description: fmt.Sprintf("This vetting request has been rejected and <@%s> has been kicked from the server.\n\n**Rejected By:** <@%s>", member.User.ID, executor.User.ID), + Author: &discordgo.MessageEmbedAuthor{ + Name: member.User.Username, + IconURL: member.User.AvatarURL(""), + }, + } + + eventlog.AddTimeToEmbed(guild.TimeFormat, embed) + err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseUpdateMessage, Data: &discordgo.InteractionResponseData{ - Embeds: []*discordgo.MessageEmbed{ - { - Title: "Vetting Request Rejected", - Description: fmt.Sprintf("This vetting request has been rejected and <@%s> has been kicked from the server.\n\n**Rejected By:** <@%s>", member.User.ID, executor.User.ID), - Author: &discordgo.MessageEmbedAuthor{ - Name: member.User.Username, - IconURL: member.User.AvatarURL(""), - }, - Footer: &discordgo.MessageEmbedFooter{ - Text: util.FormatJucheTime(time.Now()), - }, - }, - }, + Embeds: []*discordgo.MessageEmbed{embed}, }, }) if err != nil { diff --git a/internal/util/util.go b/internal/util/util.go index 2d5ee36..85d9312 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -19,9 +19,6 @@ package util import ( - "fmt" - "time" - "github.com/bwmarrin/discordgo" "go.elara.ws/logger/log" ) @@ -33,17 +30,6 @@ func Pointer[T any](v T) *T { return &v } -// FormatJucheTime formats the given time in Juche calendar format, -// using pyongyang time if it's available, and otherwise UTC. -func FormatJucheTime(t time.Time) string { - tz, err := time.LoadLocation("Asia/Pyongyang") - if err != nil { - tz = time.UTC - } - t = t.In(tz) - return fmt.Sprintf("%02d:%02d %02d-%02d Juche %d", t.Hour(), t.Minute(), t.Day(), t.Month(), t.Year()-1911) -} - // RespondEphemeral responds to an interaction with an ephemeral message. func RespondEphemeral(s *discordgo.Session, i *discordgo.Interaction, content string) error { return s.InteractionRespond(i, &discordgo.InteractionResponse{