Restructure the code to standardize file structure
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
This commit is contained in:
parent
dec4702e8a
commit
a1257cd9e7
|
@ -8,10 +8,16 @@ Thanks for your interest in contributing to owobot! This page contains informati
|
|||
|
||||
owobot consists of several independent systems, such as the `starboard` system, `members` system, `commands` system, etc. These systems are what actually interact with users and they're all in the `internal/systems` directory.
|
||||
|
||||
All the systems that require initialization have an `Init(*discordgo.Session) error` function, which does things like registers all the commands and handlers, and performs any other initialization steps that need to be done for that system. These `Init` functions are called by `main.go` when the bot starts up.
|
||||
All the systems that require initialization have an `init.go` file with an `Init(*discordgo.Session) error` function, which does things like registers all the commands and handlers, and performs any other initialization steps that need to be done for that system. These `Init` functions are called by `main.go` when the bot starts up.
|
||||
|
||||
The `commands` system always starts last because the other systems register commands that it needs to know about before it does its initialization.
|
||||
|
||||
System file structure:
|
||||
|
||||
- `init.go`: This file contains the Init function that does all the required initialization, as well as any functions meant to be imported by other systems, such as the `commands.Register()` and `eventlog.Log()` functions.
|
||||
- `handlers.go`: This file contains all the event handler functions.
|
||||
- `commands.go`: This file contains all the command handler functions.
|
||||
|
||||
### Database
|
||||
|
||||
All the database code is in `internal/db`. owobot doesn't use any ORM or framework for the database, it directly executes SQL queries. Database migrations are stored in `internal/db/migrations`. They are sql files whose names contain the date when they were made and an extra number to avoid collisions in case multiple migrations are ever made in the same day.
|
||||
|
|
3
go.mod
3
go.mod
|
@ -13,6 +13,7 @@ require (
|
|||
go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae
|
||||
modernc.org/sqlite v1.27.0
|
||||
mvdan.cc/xurls v1.1.0
|
||||
mvdan.cc/xurls/v2 v2.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -28,7 +29,7 @@ require (
|
|||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
golang.org/x/crypto v0.5.0 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
|
|
6
go.sum
6
go.sum
|
@ -66,8 +66,8 @@ go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae/go.mod h1:qng49owViqsW5Aey
|
|||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -113,3 +113,5 @@ modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
|||
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
|
||||
mvdan.cc/xurls v1.1.0 h1:kj0j2lonKseISJCiq1Tfk+iTv65dDGCl0rTbanXJGGc=
|
||||
mvdan.cc/xurls v1.1.0/go.mod h1:TNWuhvo+IqbUCmtUIb/3LJSQdrzel8loVpgFm0HikbI=
|
||||
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
||||
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/logger/log"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
// onCmd handles any command interaction and routes it to the correct command
|
||||
// if it was registered using the [Register] function.
|
||||
func onCmd(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if i.Type != discordgo.InteractionApplicationCommand {
|
||||
return
|
||||
}
|
||||
|
||||
data := i.ApplicationCommandData()
|
||||
|
||||
mu.Lock()
|
||||
cmdFn, ok := cmds[data.Name]
|
||||
if !ok {
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
err := cmdFn(s, i)
|
||||
if err != nil {
|
||||
log.Warn("Error in command function").Str("cmd", data.Name).Err(err).Send()
|
||||
sendError(s, i.Interaction, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// sendError responds to an interaction with an ephemeral message containing an error
|
||||
func sendError(s *discordgo.Session, i *discordgo.Interaction, serr error) {
|
||||
err := util.RespondEphemeral(s, i, "ERROR: "+serr.Error())
|
||||
if err != nil {
|
||||
log.Warn("Error while trying to send error").Err(err).Send()
|
||||
return
|
||||
}
|
||||
}
|
|
@ -41,28 +41,6 @@ func Init(s *discordgo.Session) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func onCmd(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if i.Type != discordgo.InteractionApplicationCommand {
|
||||
return
|
||||
}
|
||||
|
||||
data := i.ApplicationCommandData()
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
cmdFn, ok := cmds[data.Name]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
err := cmdFn(s, i)
|
||||
if err != nil {
|
||||
log.Warn("Error in command function").Str("cmd", data.Name).Err(err).Send()
|
||||
sendError(s, i.Interaction, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func Register(s *discordgo.Session, fn CmdFunc, ac *discordgo.ApplicationCommand) {
|
||||
// If the DM permission hasn't been explicitly set, assume false
|
||||
if ac.DMPermission == nil {
|
||||
|
@ -81,14 +59,6 @@ func Register(s *discordgo.Session, fn CmdFunc, ac *discordgo.ApplicationCommand
|
|||
acs = append(acs, ac)
|
||||
}
|
||||
|
||||
func sendError(s *discordgo.Session, i *discordgo.Interaction, serr error) {
|
||||
err := util.RespondEphemeral(s, i, "ERROR: "+serr.Error())
|
||||
if err != nil {
|
||||
log.Warn("Error while trying to send error").Err(err).Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// commandSync checks if any registered commands have been removed and, if so,
|
||||
// deletes them.
|
||||
func commandSync(s *discordgo.Session) error {
|
|
@ -0,0 +1,66 @@
|
|||
package eventlog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
// eventlogCmd handles the `/eventlog` command and routes it to the correct subcommand.
|
||||
func eventlogCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
switch name := data.Options[0].Name; name {
|
||||
case "channel":
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// channelCmd handles the `/eventlog channel` command.
|
||||
func channelCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
// Get the subcommand options
|
||||
args := i.ApplicationCommandData().Options[0].Options
|
||||
|
||||
c := args[0].ChannelValue(s)
|
||||
err := db.SetLogChannel(i.GuildID, c.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set event log channel to <#%s>!", c.ID))
|
||||
}
|
||||
|
||||
// ticketChannelCmd handles the `/eventlog ticket_channel` command.
|
||||
func ticketChannelCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
// Get the subcommand options
|
||||
args := i.ApplicationCommandData().Options[0].Options
|
||||
|
||||
c := args[0].ChannelValue(s)
|
||||
err := db.SetTicketLogChannel(i.GuildID, c.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set ticket log channel to <#%s>!", c.ID))
|
||||
}
|
||||
|
||||
// timeFormatCmd handles the `/eventlog time_format` command
|
||||
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!")
|
||||
}
|
|
@ -19,7 +19,6 @@
|
|||
package eventlog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
|
@ -81,59 +80,7 @@ func Init(s *discordgo.Session) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func eventlogCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
switch name := data.Options[0].Name; name {
|
||||
case "channel":
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func channelCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
// Get the subcommand options
|
||||
args := i.ApplicationCommandData().Options[0].Options
|
||||
|
||||
c := args[0].ChannelValue(s)
|
||||
err := db.SetLogChannel(i.GuildID, c.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set event log channel to <#%s>!", c.ID))
|
||||
}
|
||||
|
||||
func ticketChannelCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
// Get the subcommand options
|
||||
args := i.ApplicationCommandData().Options[0].Options
|
||||
|
||||
c := args[0].ChannelValue(s)
|
||||
err := db.SetTicketLogChannel(i.GuildID, c.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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!")
|
||||
}
|
||||
|
||||
// Entry represents an entry in the event log
|
||||
type Entry struct {
|
||||
Title string
|
||||
Description string
|
||||
|
@ -141,6 +88,7 @@ type Entry struct {
|
|||
Author *discordgo.User
|
||||
}
|
||||
|
||||
// Log writes an entry to the event log channel if it exists
|
||||
func Log(s *discordgo.Session, guildID string, e Entry) error {
|
||||
guild, err := db.GuildByID(guildID)
|
||||
if err != nil {
|
||||
|
@ -173,6 +121,7 @@ func Log(s *discordgo.Session, guildID string, e Entry) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// TicketMsgLog writes a message log to the ticket log channel if it exists
|
||||
func TicketMsgLog(s *discordgo.Session, guildID string, msgLog io.Reader) error {
|
||||
guild, err := db.GuildByID(guildID)
|
||||
if err != nil {
|
|
@ -0,0 +1,17 @@
|
|||
package guilds
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/logger/log"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
)
|
||||
|
||||
// onGuildCreate listens for when the bot joins a new guild and adds it
|
||||
// to the database if it doesn't already exist.
|
||||
func onGuildCreate(s *discordgo.Session, gc *discordgo.GuildCreate) {
|
||||
err := db.CreateGuild(gc.ID)
|
||||
if err != nil {
|
||||
log.Warn("Error creating guild").Err(err).Send()
|
||||
return
|
||||
}
|
||||
}
|
|
@ -29,17 +29,8 @@ func Init(s *discordgo.Session) error {
|
|||
return guildSync(s)
|
||||
}
|
||||
|
||||
// onGuildCreate adds a guild to the database if it doesn't already exist
|
||||
func onGuildCreate(s *discordgo.Session, gc *discordgo.GuildCreate) {
|
||||
err := db.CreateGuild(gc.ID)
|
||||
if err != nil {
|
||||
log.Warn("Error creating guild").Err(err).Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// guildSync makes sure all the guilds the bot is in
|
||||
// exist in the database. If not, it adds them.
|
||||
// guildSync looks through all the guilds that the bot is in,
|
||||
// and if any of them don't exist in the database, it adds them.
|
||||
func guildSync(s *discordgo.Session) error {
|
||||
for _, guild := range s.State.Guilds {
|
||||
err := db.CreateGuild(guild.ID)
|
|
@ -0,0 +1,191 @@
|
|||
package members
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/logger/log"
|
||||
"go.elara.ws/owobot/internal/cache"
|
||||
"go.elara.ws/owobot/internal/systems/eventlog"
|
||||
)
|
||||
|
||||
// onMemberAdd attempts to detect which invite(s) were used to invite the user
|
||||
// and logs the member join.
|
||||
func onMemberAdd(s *discordgo.Session, gma *discordgo.GuildMemberAdd) {
|
||||
invites, err := findLastUsedInvites(s, gma.GuildID)
|
||||
if err != nil {
|
||||
log.Warn("Error finding last used invite").Err(err).Send()
|
||||
}
|
||||
|
||||
code := "Unknown"
|
||||
if len(invites) > 0 {
|
||||
code = strings.Join(invites, " or ")
|
||||
}
|
||||
|
||||
err = eventlog.Log(s, gma.GuildID, eventlog.Entry{
|
||||
Title: "New Member Joined!",
|
||||
Description: fmt.Sprintf("**User:**\n%s\n**ID:**\n%s\n**Invite Code:**\n%s", gma.Member.User.Mention(), gma.Member.User.ID, code),
|
||||
Author: gma.Member.User,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("Error sending member joined log").Str("member", gma.Member.User.Username).Err(err).Send()
|
||||
}
|
||||
}
|
||||
|
||||
// onMemberUpdate logs member updates, such as roles being assigned or removed
|
||||
func onMemberUpdate(s *discordgo.Session, gmu *discordgo.GuildMemberUpdate) {
|
||||
if gmu.BeforeUpdate == nil || gmu.Member == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !slices.Equal(gmu.BeforeUpdate.Roles, gmu.Member.Roles) {
|
||||
var added, removed []string
|
||||
for _, newRole := range gmu.Member.Roles {
|
||||
if !slices.Contains(gmu.BeforeUpdate.Roles, newRole) {
|
||||
added = append(added, fmt.Sprintf("<@&%s>", newRole))
|
||||
}
|
||||
}
|
||||
for _, oldRole := range gmu.BeforeUpdate.Roles {
|
||||
if !slices.Contains(gmu.Member.Roles, oldRole) {
|
||||
removed = append(removed, fmt.Sprintf("<@&%s>", oldRole))
|
||||
}
|
||||
}
|
||||
|
||||
err := eventlog.Log(s, gmu.GuildID, eventlog.Entry{
|
||||
Title: "Roles Updated",
|
||||
Description: fmt.Sprintf(
|
||||
"**User:** %s\n**Added:** %s\n**Removed:** %s",
|
||||
gmu.Member.User.Mention(),
|
||||
strings.Join(added, " "),
|
||||
strings.Join(removed, " "),
|
||||
),
|
||||
Author: gmu.Member.User,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("Error roles updated log").Str("member", gmu.Member.User.Username).Err(err).Send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// onMemberLeave logs member leave events and handles bans and kicks
|
||||
func onMemberLeave(s *discordgo.Session, gmr *discordgo.GuildMemberRemove) {
|
||||
err := handleBanOrKick(s, gmr)
|
||||
if err != nil {
|
||||
log.Warn("Error logging ban or kick").Str("member", gmr.Member.User.Username).Err(err).Send()
|
||||
}
|
||||
|
||||
err = eventlog.Log(s, gmr.GuildID, eventlog.Entry{
|
||||
Title: "Member Left",
|
||||
Description: fmt.Sprintf("**User:**\n%s\n**ID:**\n%s", gmr.Member.User.Mention(), gmr.Member.User.ID),
|
||||
Author: gmr.Member.User,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("Error sending member left log").Str("member", gmr.Member.User.Username).Err(err).Send()
|
||||
}
|
||||
}
|
||||
|
||||
// onChannelDelete attempts to detect the user responsible for a channel deletion
|
||||
// and logs it. It also handles rate limiting for channel delete events.
|
||||
func onChannelDelete(s *discordgo.Session, cd *discordgo.ChannelDelete) {
|
||||
if cd.Type == discordgo.ChannelTypeDM || cd.Type == discordgo.ChannelTypeGroupDM {
|
||||
return
|
||||
}
|
||||
|
||||
auditLog, err := s.GuildAuditLog(cd.GuildID, "", "", int(discordgo.AuditLogActionChannelDelete), 5)
|
||||
if err != nil {
|
||||
log.Error("Error getting audit log").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range auditLog.AuditLogEntries {
|
||||
// If the deleted channel isn't the one this event is for,
|
||||
// skip it.
|
||||
if entry.TargetID != cd.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the bot deleted the channel, we don't care about this event
|
||||
if entry.UserID == s.State.User.ID {
|
||||
return
|
||||
}
|
||||
|
||||
err = handleRatelimit(s, "channel_delete", cd.GuildID, entry.UserID)
|
||||
if err != nil {
|
||||
log.Error("Error handling rate limit").Err(err).Send()
|
||||
}
|
||||
|
||||
member, err := cache.Member(s, cd.GuildID, entry.UserID)
|
||||
if err != nil {
|
||||
log.Error("Error getting member").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
err = eventlog.Log(s, cd.GuildID, eventlog.Entry{
|
||||
Title: "Channel Deleted",
|
||||
Description: fmt.Sprintf("**Name:** `%s`\n**Deleted By:** %s", cd.Name, member.User.Mention()),
|
||||
Author: member.User,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("Error sending channel deleted log").Str("channel", cd.Name).Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleBanOrKick attempts to detect the user responsible for a ban or kick, and
|
||||
// logs it. It also handles rate limiting for bans and kicks.
|
||||
func handleBanOrKick(s *discordgo.Session, gmr *discordgo.GuildMemberRemove) error {
|
||||
auditLog, err := s.GuildAuditLog(gmr.GuildID, "", "", 0, 5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range auditLog.AuditLogEntries {
|
||||
// If there's no action type or the user isn't the one this
|
||||
// event is for, skip it.
|
||||
if entry.ActionType == nil || entry.TargetID != gmr.User.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
switch *entry.ActionType {
|
||||
case discordgo.AuditLogActionMemberBanAdd:
|
||||
executor, err := cache.Member(s, gmr.GuildID, entry.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = eventlog.Log(s, gmr.GuildID, eventlog.Entry{
|
||||
Title: "User banned",
|
||||
Description: fmt.Sprintf("**Target:** %s\n**Banned by:** %s\n**Reason:** %s", gmr.User.Mention(), executor.User.Mention(), entry.Reason),
|
||||
Author: gmr.User,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handleRatelimit(s, "ban", gmr.GuildID, executor.User.ID)
|
||||
case discordgo.AuditLogActionMemberKick:
|
||||
executor, err := cache.Member(s, gmr.GuildID, entry.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = eventlog.Log(s, gmr.GuildID, eventlog.Entry{
|
||||
Title: "User kicked",
|
||||
Description: fmt.Sprintf("**Target:** %s\n**Kicked by:** %s\n**Reason:** %s", gmr.User.Mention(), executor.User.Mention(), entry.Reason),
|
||||
Author: gmr.User,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handleRatelimit(s, "kick", gmr.GuildID, executor.User.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -42,7 +42,7 @@ func addOneToMap(invite *discordgo.Invite) {
|
|||
inviteMap[invite.Code] = invite
|
||||
}
|
||||
|
||||
// findLastUsedInvites attempts to detect the invites that potentially might've been used last
|
||||
// findLastUsedInvites tries to detect the invites that potentially might've been used last
|
||||
// in order to figure out what invite a user used to join.
|
||||
func findLastUsedInvites(s *discordgo.Session, guildID string) ([]string, error) {
|
||||
invites, err := s.GuildInvites(guildID)
|
||||
|
|
|
@ -1,15 +1,6 @@
|
|||
package members
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/logger/log"
|
||||
"go.elara.ws/owobot/internal/cache"
|
||||
"go.elara.ws/owobot/internal/systems/eventlog"
|
||||
)
|
||||
import "github.com/bwmarrin/discordgo"
|
||||
|
||||
func Init(s *discordgo.Session) error {
|
||||
go populateInviteMap(s)
|
||||
|
@ -19,182 +10,3 @@ func Init(s *discordgo.Session) error {
|
|||
s.AddHandler(onChannelDelete)
|
||||
return nil
|
||||
}
|
||||
|
||||
// onMemberAdd attempts to detect which invite(s) were used to invite the user
|
||||
// and logs the member join.
|
||||
func onMemberAdd(s *discordgo.Session, gma *discordgo.GuildMemberAdd) {
|
||||
invites, err := findLastUsedInvites(s, gma.GuildID)
|
||||
if err != nil {
|
||||
log.Warn("Error finding last used invite").Err(err).Send()
|
||||
}
|
||||
|
||||
code := "Unknown"
|
||||
if len(invites) > 0 {
|
||||
code = strings.Join(invites, " or ")
|
||||
}
|
||||
|
||||
err = eventlog.Log(s, gma.GuildID, eventlog.Entry{
|
||||
Title: "New Member Joined!",
|
||||
Description: fmt.Sprintf("**User:**\n%s\n**ID:**\n%s\n**Invite Code:**\n%s", gma.Member.User.Mention(), gma.Member.User.ID, code),
|
||||
Author: gma.Member.User,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("Error sending member joined log").Str("member", gma.Member.User.Username).Err(err).Send()
|
||||
}
|
||||
}
|
||||
|
||||
// onMemberUpdate logs member updates, such as roles being assigned or removed
|
||||
func onMemberUpdate(s *discordgo.Session, gmu *discordgo.GuildMemberUpdate) {
|
||||
if gmu.BeforeUpdate == nil || gmu.Member == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !slices.Equal(gmu.BeforeUpdate.Roles, gmu.Member.Roles) {
|
||||
var added, removed []string
|
||||
for _, newRole := range gmu.Member.Roles {
|
||||
if !slices.Contains(gmu.BeforeUpdate.Roles, newRole) {
|
||||
added = append(added, fmt.Sprintf("<@&%s>", newRole))
|
||||
}
|
||||
}
|
||||
for _, oldRole := range gmu.BeforeUpdate.Roles {
|
||||
if !slices.Contains(gmu.Member.Roles, oldRole) {
|
||||
removed = append(removed, fmt.Sprintf("<@&%s>", oldRole))
|
||||
}
|
||||
}
|
||||
|
||||
err := eventlog.Log(s, gmu.GuildID, eventlog.Entry{
|
||||
Title: "Roles Updated",
|
||||
Description: fmt.Sprintf(
|
||||
"**User:** %s\n**Added:** %s\n**Removed:** %s",
|
||||
gmu.Member.User.Mention(),
|
||||
strings.Join(added, " "),
|
||||
strings.Join(removed, " "),
|
||||
),
|
||||
Author: gmu.Member.User,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("Error roles updated log").Str("member", gmu.Member.User.Username).Err(err).Send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// onMemberLeave logs member leave events and handles bans and kicks
|
||||
func onMemberLeave(s *discordgo.Session, gmr *discordgo.GuildMemberRemove) {
|
||||
err := handleBanOrKick(s, gmr)
|
||||
if err != nil {
|
||||
log.Warn("Error logging ban or kick").Str("member", gmr.Member.User.Username).Err(err).Send()
|
||||
}
|
||||
|
||||
err = eventlog.Log(s, gmr.GuildID, eventlog.Entry{
|
||||
Title: "Member Left",
|
||||
Description: fmt.Sprintf("**User:**\n%s\n**ID:**\n%s", gmr.Member.User.Mention(), gmr.Member.User.ID),
|
||||
Author: gmr.Member.User,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("Error sending member left log").Str("member", gmr.Member.User.Username).Err(err).Send()
|
||||
}
|
||||
}
|
||||
|
||||
// onChannelDelete attempts to detect the user responsible for a channel deletion
|
||||
// and logs it. It also handles rate limiting for channel delete events.
|
||||
func onChannelDelete(s *discordgo.Session, cd *discordgo.ChannelDelete) {
|
||||
if cd.Type == discordgo.ChannelTypeDM || cd.Type == discordgo.ChannelTypeGroupDM {
|
||||
return
|
||||
}
|
||||
|
||||
auditLog, err := s.GuildAuditLog(cd.GuildID, "", "", int(discordgo.AuditLogActionChannelDelete), 5)
|
||||
if err != nil {
|
||||
log.Error("Error getting audit log").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range auditLog.AuditLogEntries {
|
||||
// If the deleted channel isn't the one this event is for,
|
||||
// skip it.
|
||||
if entry.TargetID != cd.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the bot deleted the channel, we don't care about this event
|
||||
if entry.UserID == s.State.User.ID {
|
||||
return
|
||||
}
|
||||
|
||||
err = handleRatelimit(s, "channel_delete", cd.GuildID, entry.UserID)
|
||||
if err != nil {
|
||||
log.Error("Error handling rate limit").Err(err).Send()
|
||||
}
|
||||
|
||||
member, err := cache.Member(s, cd.GuildID, entry.UserID)
|
||||
if err != nil {
|
||||
log.Error("Error getting member").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
err = eventlog.Log(s, cd.GuildID, eventlog.Entry{
|
||||
Title: "Channel Deleted",
|
||||
Description: fmt.Sprintf("**Name:** `%s`\n**Deleted By:** %s", cd.Name, member.User.Mention()),
|
||||
Author: member.User,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("Error sending channel deleted log").Str("channel", cd.Name).Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleBanOrKick attempts to detect the user responsible for a ban or kick, and
|
||||
// logs it. It also handles rate limiting for bans and kicks.
|
||||
func handleBanOrKick(s *discordgo.Session, gmr *discordgo.GuildMemberRemove) error {
|
||||
auditLog, err := s.GuildAuditLog(gmr.GuildID, "", "", 0, 5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range auditLog.AuditLogEntries {
|
||||
// If there's no action type or the user isn't the one this
|
||||
// event is for, skip it.
|
||||
if entry.ActionType == nil || entry.TargetID != gmr.User.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
switch *entry.ActionType {
|
||||
case discordgo.AuditLogActionMemberBanAdd:
|
||||
executor, err := cache.Member(s, gmr.GuildID, entry.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = eventlog.Log(s, gmr.GuildID, eventlog.Entry{
|
||||
Title: "User banned",
|
||||
Description: fmt.Sprintf("**Target:** %s\n**Banned by:** %s\n**Reason:** %s", gmr.User.Mention(), executor.User.Mention(), entry.Reason),
|
||||
Author: gmr.User,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handleRatelimit(s, "ban", gmr.GuildID, executor.User.ID)
|
||||
case discordgo.AuditLogActionMemberKick:
|
||||
executor, err := cache.Member(s, gmr.GuildID, entry.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = eventlog.Log(s, gmr.GuildID, eventlog.Entry{
|
||||
Title: "User kicked",
|
||||
Description: fmt.Sprintf("**Target:** %s\n**Kicked by:** %s\n**Reason:** %s", gmr.User.Mention(), executor.User.Mention(), entry.Reason),
|
||||
Author: gmr.User,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handleRatelimit(s, "kick", gmr.GuildID, executor.User.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package polls
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
)
|
||||
|
||||
func pollCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
title := data.Options[0].StringValue()
|
||||
|
||||
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: "**" + title + "**",
|
||||
Components: []discordgo.MessageComponent{
|
||||
discordgo.ActionsRow{Components: []discordgo.MessageComponent{
|
||||
discordgo.Button{
|
||||
Label: "Add Option",
|
||||
Style: discordgo.PrimaryButton,
|
||||
CustomID: "poll-add-opt",
|
||||
},
|
||||
discordgo.Button{
|
||||
Label: "Finish",
|
||||
Style: discordgo.SuccessButton,
|
||||
CustomID: "poll-finish",
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg, err := s.InteractionResponse(i.Interaction)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.CreatePoll(msg.ID, i.Member.User.ID, title)
|
||||
}
|
|
@ -15,69 +15,10 @@ import (
|
|||
"go.elara.ws/logger/log"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
"go.elara.ws/owobot/internal/emoji"
|
||||
"go.elara.ws/owobot/internal/systems/commands"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
"go.elara.ws/owobot/internal/xsync"
|
||||
)
|
||||
|
||||
func Init(s *discordgo.Session) error {
|
||||
s.AddHandler(util.InteractionErrorHandler("poll-add-opt", onPollAddOpt))
|
||||
s.AddHandler(util.InteractionErrorHandler("poll-opt-submit", onAddOptModalSubmit))
|
||||
s.AddHandler(util.InteractionErrorHandler("poll-finish", onPollFinish))
|
||||
s.AddHandler(onPollReaction)
|
||||
s.AddHandler(onVote)
|
||||
|
||||
commands.Register(s, pollCmd, &discordgo.ApplicationCommand{
|
||||
Name: "poll",
|
||||
Description: "Create a new poll",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "title",
|
||||
Description: "The title of the poll",
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pollCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
title := data.Options[0].StringValue()
|
||||
|
||||
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: "**" + title + "**",
|
||||
Components: []discordgo.MessageComponent{
|
||||
discordgo.ActionsRow{Components: []discordgo.MessageComponent{
|
||||
discordgo.Button{
|
||||
Label: "Add Option",
|
||||
Style: discordgo.PrimaryButton,
|
||||
CustomID: "poll-add-opt",
|
||||
},
|
||||
discordgo.Button{
|
||||
Label: "Finish",
|
||||
Style: discordgo.SuccessButton,
|
||||
CustomID: "poll-finish",
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg, err := s.InteractionResponse(i.Interaction)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.CreatePoll(msg.ID, i.Member.User.ID, title)
|
||||
}
|
||||
|
||||
// onPollAddOpt handles the Add Option button on unfinished polls.
|
||||
func onPollAddOpt(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
if i.Type != discordgo.InteractionMessageComponent {
|
|
@ -0,0 +1,30 @@
|
|||
package polls
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/owobot/internal/systems/commands"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
func Init(s *discordgo.Session) error {
|
||||
s.AddHandler(util.InteractionErrorHandler("poll-add-opt", onPollAddOpt))
|
||||
s.AddHandler(util.InteractionErrorHandler("poll-opt-submit", onAddOptModalSubmit))
|
||||
s.AddHandler(util.InteractionErrorHandler("poll-finish", onPollFinish))
|
||||
s.AddHandler(onPollReaction)
|
||||
s.AddHandler(onVote)
|
||||
|
||||
commands.Register(s, pollCmd, &discordgo.ApplicationCommand{
|
||||
Name: "poll",
|
||||
Description: "Create a new poll",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "title",
|
||||
Description: "The title of the poll",
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -31,6 +31,7 @@ import (
|
|||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
// reactionsCmd handles the `/reactions` command and routes it to the correct subcommand.
|
||||
func reactionsCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
|
||||
|
@ -50,6 +51,7 @@ func reactionsCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
|||
}
|
||||
}
|
||||
|
||||
// reactionsAddCmd handles the `/reactions add` command.
|
||||
func reactionsAddCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
|
@ -96,6 +98,7 @@ func reactionsAddCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error
|
|||
return util.RespondEphemeral(s, i.Interaction, "Successfully added reaction!")
|
||||
}
|
||||
|
||||
// reactionsListCmd handles the `/reactions list` command.
|
||||
func reactionsListCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
reactions, err := db.Reactions(i.GuildID)
|
||||
if err != nil {
|
||||
|
@ -125,6 +128,7 @@ func reactionsListCmd(s *discordgo.Session, i *discordgo.InteractionCreate) erro
|
|||
return util.RespondEphemeral(s, i.Interaction, sb.String())
|
||||
}
|
||||
|
||||
// reactionsDeleteCmd handles the `/reactions delete` command.
|
||||
func reactionsDeleteCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
// Make sure the user has the manage expressions permission
|
||||
// in case a role/member override allows someone else to use it
|
||||
|
@ -143,6 +147,7 @@ func reactionsDeleteCmd(s *discordgo.Session, i *discordgo.InteractionCreate) er
|
|||
return util.RespondEphemeral(s, i.Interaction, "Successfully removed reaction")
|
||||
}
|
||||
|
||||
// reactionsExcludeCmd handles the `/reactions exclude` command.
|
||||
func reactionsExcludeCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
// Make sure the user has the manage expressions permission
|
||||
// in case a role/member override allows someone else to use it
|
||||
|
@ -168,6 +173,7 @@ func reactionsExcludeCmd(s *discordgo.Session, i *discordgo.InteractionCreate) e
|
|||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully excluded %s from receiving reactions", channel.Mention()))
|
||||
}
|
||||
|
||||
// reactionsUnexcludeCmd handles the `/reactions unexclude` command.
|
||||
func reactionsUnexcludeCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
// Make sure the user has the manage expressions permission
|
||||
// in case a role/member override allows someone else to use it
|
||||
|
@ -193,6 +199,8 @@ func reactionsUnexcludeCmd(s *discordgo.Session, i *discordgo.InteractionCreate)
|
|||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully unexcluded %s from receiving reactions", channel.Mention()))
|
||||
}
|
||||
|
||||
// validateEmoji checks if the given slice of emoji is valid.
|
||||
// If an invalid emoji is found, it returns an error.
|
||||
func validateEmoji(s db.StringSlice) error {
|
||||
for i := range s {
|
||||
s[i] = strings.TrimSpace(s[i])
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
package reactions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/valyala/fasttemplate"
|
||||
"go.elara.ws/logger/log"
|
||||
"go.elara.ws/owobot/internal/cache"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
"go.elara.ws/owobot/internal/emoji"
|
||||
)
|
||||
|
||||
// onMessage handles all new messages. It checks if the message matches any reaction
|
||||
// registered for that guild, and if it does, it performs all the matching reactions.
|
||||
func onMessage(s *discordgo.Session, mc *discordgo.MessageCreate) {
|
||||
if mc.Author.ID == s.State.User.ID {
|
||||
return
|
||||
}
|
||||
|
||||
reactions, err := db.Reactions(mc.GuildID)
|
||||
if err != nil {
|
||||
log.Error("Error getting reactions from database").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
for _, reaction := range reactions {
|
||||
if slices.Contains(reaction.ExcludedChannels, mc.ChannelID) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch reaction.MatchType {
|
||||
case db.MatchTypeContains:
|
||||
if strings.Contains(strings.ToLower(mc.Content), reaction.Match) {
|
||||
err = performReaction(s, reaction, reaction.Reaction, mc)
|
||||
if err != nil {
|
||||
log.Error("Error performing reaction").Err(err).Send()
|
||||
continue
|
||||
}
|
||||
}
|
||||
case db.MatchTypeRegex:
|
||||
re, err := cache.Regex(reaction.Match)
|
||||
if err != nil {
|
||||
log.Error("Error compiling regex").Err(err).Send()
|
||||
continue
|
||||
}
|
||||
|
||||
content := reaction.Reaction
|
||||
switch reaction.ReactionType {
|
||||
case db.ReactionTypeText:
|
||||
submatch := re.FindSubmatch([]byte(mc.Content))
|
||||
if len(submatch) > 1 {
|
||||
replacements := map[string]any{}
|
||||
for i, match := range submatch {
|
||||
replacements[strconv.Itoa(i)] = match
|
||||
}
|
||||
content = db.StringSlice{
|
||||
fasttemplate.ExecuteStringStd(reaction.Reaction[0], "{", "}", replacements),
|
||||
}
|
||||
} else if len(submatch) == 1 {
|
||||
content = reaction.Reaction
|
||||
}
|
||||
case db.ReactionTypeEmoji:
|
||||
if re.MatchString(mc.Content) {
|
||||
content = reaction.Reaction
|
||||
}
|
||||
}
|
||||
|
||||
if content[0] != "" {
|
||||
err = performReaction(s, reaction, content, mc)
|
||||
if err != nil {
|
||||
log.Error("Error performing reaction").Err(err).Send()
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
rngMtx = sync.Mutex{}
|
||||
rng = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
)
|
||||
|
||||
func performReaction(s *discordgo.Session, reaction db.Reaction, content db.StringSlice, mc *discordgo.MessageCreate) error {
|
||||
if reaction.Chance < 100 {
|
||||
rngMtx.Lock()
|
||||
randNum := rng.Intn(100) + 1
|
||||
rngMtx.Unlock()
|
||||
if randNum > reaction.Chance {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch reaction.ReactionType {
|
||||
case db.ReactionTypeText:
|
||||
_, err := s.ChannelMessageSendReply(mc.ChannelID, content[0], mc.Reference())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case db.ReactionTypeEmoji:
|
||||
for _, emojiStr := range content {
|
||||
e, ok := emoji.Parse(emojiStr)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid emoji: %s", emojiStr)
|
||||
}
|
||||
|
||||
err := s.MessageReactionAdd(mc.ChannelID, mc.ID, e.APIFormat())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -19,20 +19,7 @@
|
|||
package reactions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/valyala/fasttemplate"
|
||||
"go.elara.ws/logger/log"
|
||||
"go.elara.ws/owobot/internal/cache"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
"go.elara.ws/owobot/internal/emoji"
|
||||
"go.elara.ws/owobot/internal/systems/commands"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
@ -170,104 +157,3 @@ func Init(s *discordgo.Session) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func onMessage(s *discordgo.Session, mc *discordgo.MessageCreate) {
|
||||
if mc.Author.ID == s.State.User.ID {
|
||||
return
|
||||
}
|
||||
|
||||
reactions, err := db.Reactions(mc.GuildID)
|
||||
if err != nil {
|
||||
log.Error("Error getting reactions from database").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
for _, reaction := range reactions {
|
||||
if slices.Contains(reaction.ExcludedChannels, mc.ChannelID) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch reaction.MatchType {
|
||||
case db.MatchTypeContains:
|
||||
if strings.Contains(strings.ToLower(mc.Content), reaction.Match) {
|
||||
err = performReaction(s, reaction, reaction.Reaction, mc)
|
||||
if err != nil {
|
||||
log.Error("Error performing reaction").Err(err).Send()
|
||||
continue
|
||||
}
|
||||
}
|
||||
case db.MatchTypeRegex:
|
||||
re, err := cache.Regex(reaction.Match)
|
||||
if err != nil {
|
||||
log.Error("Error compiling regex").Err(err).Send()
|
||||
continue
|
||||
}
|
||||
|
||||
content := reaction.Reaction
|
||||
switch reaction.ReactionType {
|
||||
case db.ReactionTypeText:
|
||||
submatch := re.FindSubmatch([]byte(mc.Content))
|
||||
if len(submatch) > 1 {
|
||||
replacements := map[string]any{}
|
||||
for i, match := range submatch {
|
||||
replacements[strconv.Itoa(i)] = match
|
||||
}
|
||||
content = db.StringSlice{
|
||||
fasttemplate.ExecuteStringStd(reaction.Reaction[0], "{", "}", replacements),
|
||||
}
|
||||
} else if len(submatch) == 1 {
|
||||
content = reaction.Reaction
|
||||
}
|
||||
case db.ReactionTypeEmoji:
|
||||
if re.MatchString(mc.Content) {
|
||||
content = reaction.Reaction
|
||||
}
|
||||
}
|
||||
|
||||
if content[0] != "" {
|
||||
err = performReaction(s, reaction, content, mc)
|
||||
if err != nil {
|
||||
log.Error("Error performing reaction").Err(err).Send()
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
rngMtx = sync.Mutex{}
|
||||
rng = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
)
|
||||
|
||||
func performReaction(s *discordgo.Session, reaction db.Reaction, content db.StringSlice, mc *discordgo.MessageCreate) error {
|
||||
if reaction.Chance < 100 {
|
||||
rngMtx.Lock()
|
||||
randNum := rng.Intn(100) + 1
|
||||
rngMtx.Unlock()
|
||||
if randNum > reaction.Chance {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch reaction.ReactionType {
|
||||
case db.ReactionTypeText:
|
||||
_, err := s.ChannelMessageSendReply(mc.ChannelID, content[0], mc.Reference())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case db.ReactionTypeEmoji:
|
||||
for _, emojiStr := range content {
|
||||
e, ok := emoji.Parse(emojiStr)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid emoji: %s", emojiStr)
|
||||
}
|
||||
|
||||
err := s.MessageReactionAdd(mc.ChannelID, mc.ID, e.APIFormat())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -20,15 +20,18 @@ package roles
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/owobot/internal/cache"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
"go.elara.ws/owobot/internal/emoji"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
// reactionRolesCmd calls the correct subcommand handler for the reaction_roles command
|
||||
// reactionRolesCmd handles the `/reaction_roles` command and routes it to the correct subcommand.
|
||||
func reactionRolesCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
|
||||
|
@ -46,7 +49,7 @@ func reactionRolesCmd(s *discordgo.Session, i *discordgo.InteractionCreate) erro
|
|||
}
|
||||
}
|
||||
|
||||
// reactionRolesNewCategoryCmd creates a new reaction role category.
|
||||
// reactionRolesNewCategoryCmd handles the `/reaction_roles new_category` command.
|
||||
func reactionRolesNewCategoryCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
|
@ -76,7 +79,7 @@ func reactionRolesNewCategoryCmd(s *discordgo.Session, i *discordgo.InteractionC
|
|||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully added a new reaction role category called `%s`!", rrc.Name))
|
||||
}
|
||||
|
||||
// reactionRolesRemoveCategoryCmd removes an existing reaction role category.
|
||||
// reactionRolesRemoveCategoryCmd handles the `/reaction_roles remove_category` command.
|
||||
func reactionRolesRemoveCategoryCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
|
@ -101,7 +104,7 @@ func reactionRolesRemoveCategoryCmd(s *discordgo.Session, i *discordgo.Interacti
|
|||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Removed reaction role category `%s`", args[0].StringValue()))
|
||||
}
|
||||
|
||||
// reactionRolesAddCmd adds a reaction role to a category.
|
||||
// reactionRolesAddCmd handles the `/reaction_roles add` command.
|
||||
func reactionRolesAddCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
|
@ -128,7 +131,7 @@ func reactionRolesAddCmd(s *discordgo.Session, i *discordgo.InteractionCreate) e
|
|||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Added reaction role %s to `%s`", role.Mention(), category))
|
||||
}
|
||||
|
||||
// reactionRolesRemoveCmd removes a reaction role from a category.
|
||||
// reactionRolesRemoveCmd handles the `/reaction_roles remove` command.
|
||||
func reactionRolesRemoveCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
|
@ -149,6 +152,64 @@ func reactionRolesRemoveCmd(s *discordgo.Session, i *discordgo.InteractionCreate
|
|||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Removed reaction role %s from `%s`", role.Mention(), category))
|
||||
}
|
||||
|
||||
var neopronounValidationRegex = regexp.MustCompile(`^[a-z]+(/[a-z]+)+$`)
|
||||
|
||||
// neopronounCmd handles the `/neopronoun` command.
|
||||
func neopronounCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
name := data.Options[0].StringValue()
|
||||
name = strings.ToLower(name)
|
||||
|
||||
if !neopronounValidationRegex.MatchString(name) {
|
||||
return fmt.Errorf("invalid neopronoun: `%s`", name)
|
||||
}
|
||||
|
||||
roles, err := cache.Roles(s, i.GuildID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var roleID string
|
||||
for _, role := range roles {
|
||||
// Skip this role if it provides any permissions, so that
|
||||
// we don't accidentally grant the member any extra permissions
|
||||
if role.Permissions != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if role.Name == name {
|
||||
roleID = role.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if roleID == "" {
|
||||
role, err := s.GuildRoleCreate(i.GuildID, &discordgo.RoleParams{
|
||||
Name: name,
|
||||
Mentionable: util.Pointer(false),
|
||||
Permissions: util.Pointer[int64](0),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
roleID = role.ID
|
||||
}
|
||||
|
||||
if slices.Contains(i.Member.Roles, roleID) {
|
||||
err = s.GuildMemberRoleRemove(i.GuildID, i.Member.User.ID, roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Unassigned the `%s` role", name))
|
||||
} else {
|
||||
err = s.GuildMemberRoleAdd(i.GuildID, i.Member.User.ID, roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully assigned the `%s` role to you!", name))
|
||||
}
|
||||
}
|
||||
|
||||
// updateReactionRoleCategoryMsg updates a reaction role category message
|
||||
func updateReactionRoleCategoryMsg(s *discordgo.Session, channelID, category string) error {
|
||||
rrc, err := db.GetReactionRoleCategory(channelID, category)
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package roles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
// onRoleButton handles users clicking a role reaction button. It checks if they have
|
||||
// the role the button is codes for, and if they do, it removes it. Otherwise, it
|
||||
// assigns it to them.
|
||||
func onRoleButton(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
if i.Type != discordgo.InteractionMessageComponent {
|
||||
return nil
|
||||
}
|
||||
|
||||
data := i.MessageComponentData()
|
||||
|
||||
buttonID, roleID, ok := strings.Cut(data.CustomID, ":")
|
||||
if !ok || buttonID != "role" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if slices.Contains(i.Member.Roles, roleID) {
|
||||
err := s.GuildMemberRoleRemove(i.GuildID, i.Member.User.ID, roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Unassigned role <@&%s>", roleID))
|
||||
} else {
|
||||
err := s.GuildMemberRoleAdd(i.GuildID, i.Member.User.ID, roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully assigned role <@&%s> to you", roleID))
|
||||
}
|
||||
}
|
|
@ -19,10 +19,6 @@
|
|||
package roles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/owobot/internal/systems/commands"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
|
@ -129,33 +125,3 @@ func Init(s *discordgo.Session) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// onRoleButton handles users clicking a role reaction button. It checks if they have
|
||||
// the role the button is codes for, and if they do, it removes it. Otherwise, it
|
||||
// assigns it to them.
|
||||
func onRoleButton(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
if i.Type != discordgo.InteractionMessageComponent {
|
||||
return nil
|
||||
}
|
||||
|
||||
data := i.MessageComponentData()
|
||||
|
||||
buttonID, roleID, ok := strings.Cut(data.CustomID, ":")
|
||||
if !ok || buttonID != "role" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if slices.Contains(i.Member.Roles, roleID) {
|
||||
err := s.GuildMemberRoleRemove(i.GuildID, i.Member.User.ID, roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Unassigned role <@&%s>", roleID))
|
||||
} else {
|
||||
err := s.GuildMemberRoleAdd(i.GuildID, i.Member.User.ID, roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully assigned role <@&%s> to you", roleID))
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
/*
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package roles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/owobot/internal/cache"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
var neopronounValidationRegex = regexp.MustCompile(`^[a-z]+(/[a-z]+)+$`)
|
||||
|
||||
// neopronounCmd assigns a neopronoun role to the user that ran it.
|
||||
func neopronounCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
name := data.Options[0].StringValue()
|
||||
name = strings.ToLower(name)
|
||||
|
||||
if !neopronounValidationRegex.MatchString(name) {
|
||||
return fmt.Errorf("invalid neopronoun: `%s`", name)
|
||||
}
|
||||
|
||||
roles, err := cache.Roles(s, i.GuildID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var roleID string
|
||||
for _, role := range roles {
|
||||
// Skip this role if it provides any permissions, so that
|
||||
// we don't accidentally grant the member any extra permissions
|
||||
if role.Permissions != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if role.Name == name {
|
||||
roleID = role.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if roleID == "" {
|
||||
role, err := s.GuildRoleCreate(i.GuildID, &discordgo.RoleParams{
|
||||
Name: name,
|
||||
Mentionable: util.Pointer(false),
|
||||
Permissions: util.Pointer[int64](0),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
roleID = role.ID
|
||||
}
|
||||
|
||||
if slices.Contains(i.Member.Roles, roleID) {
|
||||
err = s.GuildMemberRoleRemove(i.GuildID, i.Member.User.ID, roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Unassigned the `%s` role", name))
|
||||
} else {
|
||||
err = s.GuildMemberRoleAdd(i.GuildID, i.Member.User.ID, roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully assigned the `%s` role to you!", name))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package starboard
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
// starboardCmd handles the `/starboard` command and routes it to the correct subcommand.
|
||||
func starboardCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
switch name := data.Options[0].Name; name {
|
||||
case "channel":
|
||||
return channelCmd(s, i)
|
||||
case "stars":
|
||||
return starsCmd(s, i)
|
||||
default:
|
||||
return fmt.Errorf("unknown subcommand: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
// channelCmd handles the `/starboard channel` command.
|
||||
func channelCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
// Get the subcommand options
|
||||
args := i.ApplicationCommandData().Options[0].Options
|
||||
|
||||
c := args[0].ChannelValue(s)
|
||||
err := db.SetStarboardChannel(i.GuildID, c.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set starboard channel to <#%s>!", c.ID))
|
||||
}
|
||||
|
||||
// starsCmd handles the `/starboard stars` command.
|
||||
func starsCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
// Get the subcommand options
|
||||
args := i.ApplicationCommandData().Options[0].Options
|
||||
|
||||
stars := args[0].IntValue()
|
||||
if stars <= 0 {
|
||||
return errors.New("star amount must be greater than 0")
|
||||
}
|
||||
|
||||
err := db.SetStarboardStars(i.GuildID, stars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set the amount of stars required to get on the starboard to %d!", stars))
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
package starboard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/logger/log"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
"go.elara.ws/owobot/internal/systems/eventlog"
|
||||
"mvdan.cc/xurls/v2"
|
||||
)
|
||||
|
||||
// onReaction detects star reactions, and if the message qualifies for starboard
|
||||
// based on the guild's settings, it replies to it and adds it to the starboard.
|
||||
func onReaction(s *discordgo.Session, mra *discordgo.MessageReactionAdd) {
|
||||
if mra.Emoji.Name != starEmoji {
|
||||
return
|
||||
}
|
||||
|
||||
msgExists, err := db.ExistsInStarboard(mra.MessageID)
|
||||
if err != nil {
|
||||
log.Warn("Error checking if the message exists in the starboard").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
// If the message has already been added to the starboard,
|
||||
// we can skip it.
|
||||
if msgExists {
|
||||
return
|
||||
}
|
||||
|
||||
guild, err := db.GuildByID(mra.GuildID)
|
||||
if err != nil {
|
||||
log.Warn("Error getting guild from the database").Str("id", mra.GuildID).Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
// If the guild has no starboard channel ID set, we can
|
||||
// skip this message.
|
||||
if guild.StarboardChanID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
reactions, err := s.MessageReactions(mra.ChannelID, mra.MessageID, starEmoji, guild.StarboardStars, "", "")
|
||||
if err != nil {
|
||||
log.Warn("Error getting message reactions").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
if len(reactions) >= guild.StarboardStars {
|
||||
msg, err := s.ChannelMessage(mra.ChannelID, mra.MessageID)
|
||||
if err != nil {
|
||||
log.Warn("Error getting channel message").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
ch, err := s.Channel(mra.ChannelID)
|
||||
if err != nil {
|
||||
log.Warn("Error getting channel").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.ChannelMessageSendReply(
|
||||
msg.ChannelID,
|
||||
fmt.Sprintf("Congrats %s! You've made it to <#%s>!!", msg.Author.Mention(), guild.StarboardChanID),
|
||||
msg.Reference(),
|
||||
)
|
||||
if err != nil {
|
||||
log.Warn("Error sending message reply").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: fmt.Sprintf("%s - #%s - %s has made it!", starEmoji, ch.Name, msg.Author.Username),
|
||||
Author: &discordgo.MessageEmbedAuthor{
|
||||
Name: msg.Author.Username,
|
||||
IconURL: msg.Author.AvatarURL(""),
|
||||
},
|
||||
Description: fmt.Sprintf(
|
||||
"[**Jump to Message**](https://discord.com/channels/%s/%s/%s)",
|
||||
mra.GuildID,
|
||||
msg.ChannelID,
|
||||
msg.ID,
|
||||
),
|
||||
Color: embedColor,
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
if msg.Content != "" {
|
||||
// If the message has content, we add it above the
|
||||
// jump to message link currently in the embed description.
|
||||
embed.Description = fmt.Sprintf(
|
||||
"**Message Content**\n%s\n\n%s",
|
||||
msg.Content,
|
||||
embed.Description,
|
||||
)
|
||||
}
|
||||
|
||||
_, err = s.ChannelMessageSendEmbed(guild.StarboardChanID, embed)
|
||||
if err != nil {
|
||||
log.Warn("Error sending starboard message").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
err = db.AddToStarboard(mra.MessageID)
|
||||
if err != nil {
|
||||
log.Warn("Error adding message to starboard").Err(err).Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getImageURL looks through the message content and attachments
|
||||
// to try to find images. If it finds one, it returns the URL.
|
||||
// Otherwise, it returns an empty string.
|
||||
func getImageURL(msg *discordgo.Message) string {
|
||||
if xurl := xurls.Strict().FindString(msg.Content); xurl != "" {
|
||||
u, err := url.Parse(xurl)
|
||||
if err == nil {
|
||||
mt := mime.TypeByExtension(path.Ext(u.Path))
|
||||
if strings.HasPrefix(mt, "image/") {
|
||||
return xurl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, attachment := range msg.Attachments {
|
||||
if strings.HasPrefix(attachment.ContentType, "image/") {
|
||||
return attachment.URL
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package starboard
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/owobot/internal/systems/commands"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
const (
|
||||
starEmoji = "\u2b50"
|
||||
embedColor = 0xFF5833
|
||||
)
|
||||
|
||||
func Init(s *discordgo.Session) error {
|
||||
s.AddHandler(onReaction)
|
||||
|
||||
commands.Register(s, starboardCmd, &discordgo.ApplicationCommand{
|
||||
Name: "starboard",
|
||||
Description: "Modify starboard settings",
|
||||
DefaultMemberPermissions: util.Pointer[int64](discordgo.PermissionManageServer),
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "channel",
|
||||
Description: "Set the starboard channel",
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "channel",
|
||||
Description: "The channel to use for the starboard",
|
||||
Type: discordgo.ApplicationCommandOptionChannel,
|
||||
ChannelTypes: []discordgo.ChannelType{discordgo.ChannelTypeGuildText},
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "stars",
|
||||
Description: "Set the amount of stars for the starboard",
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "stars",
|
||||
Description: "The amount of stars to require",
|
||||
Type: discordgo.ApplicationCommandOptionInteger,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,257 +0,0 @@
|
|||
/*
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package starboard
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"mvdan.cc/xurls"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
starEmoji = "\u2b50"
|
||||
embedColor = 0xFF5833
|
||||
)
|
||||
|
||||
func Init(s *discordgo.Session) error {
|
||||
s.AddHandler(onReaction)
|
||||
|
||||
commands.Register(s, starboardCmd, &discordgo.ApplicationCommand{
|
||||
Name: "starboard",
|
||||
Description: "Modify starboard settings",
|
||||
DefaultMemberPermissions: util.Pointer[int64](discordgo.PermissionManageServer),
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "channel",
|
||||
Description: "Set the starboard channel",
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "channel",
|
||||
Description: "The channel to use for the starboard",
|
||||
Type: discordgo.ApplicationCommandOptionChannel,
|
||||
ChannelTypes: []discordgo.ChannelType{discordgo.ChannelTypeGuildText},
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "stars",
|
||||
Description: "Set the amount of stars for the starboard",
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "stars",
|
||||
Description: "The amount of stars to require",
|
||||
Type: discordgo.ApplicationCommandOptionInteger,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// starboardCmd calls the correct subcommand handler for the starboard command
|
||||
func starboardCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
switch name := data.Options[0].Name; name {
|
||||
case "channel":
|
||||
return channelCmd(s, i)
|
||||
case "stars":
|
||||
return starsCmd(s, i)
|
||||
default:
|
||||
return fmt.Errorf("unknown subcommand: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
// channelCmd sets the starboard channel for the guild
|
||||
func channelCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
// Get the subcommand options
|
||||
args := i.ApplicationCommandData().Options[0].Options
|
||||
|
||||
c := args[0].ChannelValue(s)
|
||||
err := db.SetStarboardChannel(i.GuildID, c.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set starboard channel to <#%s>!", c.ID))
|
||||
}
|
||||
|
||||
// starsCmd sets the amount of stars that trigger the starboard for the guild
|
||||
func starsCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
// Get the subcommand options
|
||||
args := i.ApplicationCommandData().Options[0].Options
|
||||
|
||||
stars := args[0].IntValue()
|
||||
if stars <= 0 {
|
||||
return errors.New("star amount must be greater than 0")
|
||||
}
|
||||
|
||||
err := db.SetStarboardStars(i.GuildID, stars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set the amount of stars required to get on the starboard to %d!", stars))
|
||||
}
|
||||
|
||||
// onReaction detects star reactions, and if the message qualifies for starboard
|
||||
// based on the guild's settings, it replies to it and adds it to the starboard.
|
||||
func onReaction(s *discordgo.Session, mra *discordgo.MessageReactionAdd) {
|
||||
if mra.Emoji.Name != starEmoji {
|
||||
return
|
||||
}
|
||||
|
||||
msgExists, err := db.ExistsInStarboard(mra.MessageID)
|
||||
if err != nil {
|
||||
log.Warn("Error checking if the message exists in the starboard").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
// If the message has already been added to the starboard,
|
||||
// we can skip it.
|
||||
if msgExists {
|
||||
return
|
||||
}
|
||||
|
||||
guild, err := db.GuildByID(mra.GuildID)
|
||||
if err != nil {
|
||||
log.Warn("Error getting guild from the database").Str("id", mra.GuildID).Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
// If the guild has no starboard channel ID set, we can
|
||||
// skip this message.
|
||||
if guild.StarboardChanID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
reactions, err := s.MessageReactions(mra.ChannelID, mra.MessageID, starEmoji, guild.StarboardStars, "", "")
|
||||
if err != nil {
|
||||
log.Warn("Error getting message reactions").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
if len(reactions) >= guild.StarboardStars {
|
||||
msg, err := s.ChannelMessage(mra.ChannelID, mra.MessageID)
|
||||
if err != nil {
|
||||
log.Warn("Error getting channel message").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
ch, err := s.Channel(mra.ChannelID)
|
||||
if err != nil {
|
||||
log.Warn("Error getting channel").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.ChannelMessageSendReply(
|
||||
msg.ChannelID,
|
||||
fmt.Sprintf("Congrats %s! You've made it to <#%s>!!", msg.Author.Mention(), guild.StarboardChanID),
|
||||
msg.Reference(),
|
||||
)
|
||||
if err != nil {
|
||||
log.Warn("Error sending message reply").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: fmt.Sprintf("%s - #%s - %s has made it!", starEmoji, ch.Name, msg.Author.Username),
|
||||
Author: &discordgo.MessageEmbedAuthor{
|
||||
Name: msg.Author.Username,
|
||||
IconURL: msg.Author.AvatarURL(""),
|
||||
},
|
||||
Description: fmt.Sprintf(
|
||||
"[**Jump to Message**](https://discord.com/channels/%s/%s/%s)",
|
||||
mra.GuildID,
|
||||
msg.ChannelID,
|
||||
msg.ID,
|
||||
),
|
||||
Color: embedColor,
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
if msg.Content != "" {
|
||||
// If the message has content, we add it above the
|
||||
// jump to message link currently in the embed description.
|
||||
embed.Description = fmt.Sprintf(
|
||||
"**Message Content**\n%s\n\n%s",
|
||||
msg.Content,
|
||||
embed.Description,
|
||||
)
|
||||
}
|
||||
|
||||
_, err = s.ChannelMessageSendEmbed(guild.StarboardChanID, embed)
|
||||
if err != nil {
|
||||
log.Warn("Error sending starboard message").Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
err = db.AddToStarboard(mra.MessageID)
|
||||
if err != nil {
|
||||
log.Warn("Error adding message to starboard").Err(err).Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getImageURL looks through the message content and attachments
|
||||
// to try to find images. If it finds one, it returns the URL.
|
||||
// Otherwise, it returns an empty string.
|
||||
func getImageURL(msg *discordgo.Message) string {
|
||||
if xurl := xurls.Strict.FindString(msg.Content); xurl != "" {
|
||||
u, err := url.Parse(xurl)
|
||||
if err == nil {
|
||||
mt := mime.TypeByExtension(path.Ext(u.Path))
|
||||
if strings.HasPrefix(mt, "image/") {
|
||||
return xurl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, attachment := range msg.Attachments {
|
||||
if strings.HasPrefix(attachment.ContentType, "image/") {
|
||||
return attachment.URL
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package tickets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
// ticketCmd handles the `/ticket` command.
|
||||
func ticketCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
chID, err := Open(s, i.GuildID, i.Member.User, i.Member.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully opened a ticket at <#%s>!", chID))
|
||||
}
|
||||
|
||||
// modTicketCmd handles the `/mod_ticket` command.
|
||||
func modTicketCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
chID, err := Open(s, i.GuildID, data.Options[0].UserValue(s), i.Member.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully opened a ticket at <#%s>!", chID))
|
||||
}
|
||||
|
||||
// closeTicketCmd handles the `/close_ticket` command.
|
||||
func closeTicketCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
user := data.Options[0].UserValue(s)
|
||||
err := Close(s, i.GuildID, user, i.Member.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully closed ticket for <@%s>", user.ID))
|
||||
}
|
||||
|
||||
// ticketCategoryCmd handles the `/ticket_category` command.
|
||||
func ticketCategoryCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
category := data.Options[0].ChannelValue(s)
|
||||
err := db.SetTicketCategory(i.GuildID, category.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set the ticket category to `%s`!", category.Name))
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package tickets
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"go.elara.ws/logger/log"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
// onMemberLeave closes any tickets a user had open when they leave
|
||||
func onMemberLeave(s *discordgo.Session, gmr *discordgo.GuildMemberRemove) {
|
||||
// If the user had a ticket open when they left, make sure to close it.
|
||||
err := Close(s, gmr.GuildID, gmr.User, s.State.User)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
// If the error is ErrNoRows, the user didn't have a ticket, so just return
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Warn("Error removing ticket after user left").Err(err).Send()
|
||||
return
|
||||
}
|
||||
}
|
|
@ -2,8 +2,6 @@ package tickets
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
|
@ -72,61 +70,6 @@ func Init(s *discordgo.Session) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ticketCategoryCmd sets the category in which future ticket channels will be created
|
||||
func ticketCategoryCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
category := data.Options[0].ChannelValue(s)
|
||||
err := db.SetTicketCategory(i.GuildID, category.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set the ticket category to `%s`!", category.Name))
|
||||
}
|
||||
|
||||
// modTicketCmd handles the mod_ticket command. It opens a new ticket for the given user.
|
||||
func modTicketCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
chID, err := Open(s, i.GuildID, data.Options[0].UserValue(s), i.Member.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully opened a ticket at <#%s>!", chID))
|
||||
}
|
||||
|
||||
// ticketCmd handles the ticket command. It opens a new ticket for the user that ran it.
|
||||
func ticketCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
chID, err := Open(s, i.GuildID, i.Member.User, i.Member.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully opened a ticket at <#%s>!", chID))
|
||||
}
|
||||
|
||||
// closeTicketCmd handles the close_ticket command. It closes the ticket that the given user
|
||||
// has open if it exists.
|
||||
func closeTicketCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
user := data.Options[0].UserValue(s)
|
||||
err := Close(s, i.GuildID, user, i.Member.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully closed ticket for <@%s>", user.ID))
|
||||
}
|
||||
|
||||
// onMemberLeave closes any tickets a user had open when they leave
|
||||
func onMemberLeave(s *discordgo.Session, gmr *discordgo.GuildMemberRemove) {
|
||||
// If the user had a ticket open when they left, make sure to close it.
|
||||
err := Close(s, gmr.GuildID, gmr.User, s.State.User)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
// If the error is ErrNoRows, the user didn't have a ticket, so just return
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Warn("Error removing ticket after user left").Err(err).Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Open opens a new ticket. It checks if a ticket already exists, and if not, creates a new channel for it,
|
||||
// allows the user it's for to see and send messages in it, adds it to the database, and logs the ticket open.
|
||||
func Open(s *discordgo.Session, guildID string, user, executor *discordgo.User) (string, error) {
|
|
@ -0,0 +1,169 @@
|
|||
package vetting
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/owobot/internal/cache"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
"go.elara.ws/owobot/internal/systems/eventlog"
|
||||
"go.elara.ws/owobot/internal/systems/tickets"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
// vettingCmd handles the `/vetting` command and routes it to the correct subcommand.
|
||||
func vettingCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
switch name := data.Options[0].Name; name {
|
||||
case "role":
|
||||
return vettingRoleCmd(s, i)
|
||||
case "req_channel":
|
||||
return vettingReqChannelCmd(s, i)
|
||||
case "welcome_channel":
|
||||
return welcomeChannelCmd(s, i)
|
||||
case "welcome_msg":
|
||||
return welcomeMsgCmd(s, i)
|
||||
default:
|
||||
return fmt.Errorf("unknown vetting subcommand: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
// vettingRoleCmd handles the `/vetting role` command.
|
||||
func vettingRoleCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
role := args[0].RoleValue(s, i.GuildID)
|
||||
|
||||
err := db.SetVettingRoleID(i.GuildID, role.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set %s as the vetting role!", role.Mention()))
|
||||
}
|
||||
|
||||
// vettingReqChannelCmd handles the `/vetting req_channel` command.
|
||||
func vettingReqChannelCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
channel := args[0].ChannelValue(s)
|
||||
|
||||
err := db.SetVettingReqChannel(i.GuildID, channel.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set %s as the vetting request channel!", channel.Mention()))
|
||||
}
|
||||
|
||||
// welcomeChannelCmd handles the `/vetting welcome_channel` command.
|
||||
func welcomeChannelCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
channel := args[0].ChannelValue(s)
|
||||
|
||||
err := db.SetWelcomeChannel(i.GuildID, channel.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set %s as the welcome channel!", channel.Mention()))
|
||||
}
|
||||
|
||||
// welcomeMsgCmd handles the `/vetting welcome_msg` command.
|
||||
func welcomeMsgCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
|
||||
err := db.SetWelcomeMsg(i.GuildID, args[0].StringValue())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, "Successfully set the welcome message!")
|
||||
}
|
||||
|
||||
// approveCmd handles the `/approve` command.
|
||||
func approveCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
guild, err := db.GuildByID(i.GuildID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if guild.VettingRoleID == "" {
|
||||
return errors.New("vetting role id is not set for this guild")
|
||||
}
|
||||
|
||||
data := i.ApplicationCommandData()
|
||||
user := data.Options[0].UserValue(s)
|
||||
role := data.Options[1].RoleValue(s, i.GuildID)
|
||||
|
||||
_, err = db.TicketChannelID(i.GuildID, user.ID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("%s has no open ticket", user.Mention())
|
||||
}
|
||||
|
||||
roleSetAllowed := false
|
||||
for _, roleID := range i.Member.Roles {
|
||||
executorRole, err := cache.Role(s, i.GuildID, roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if executorRole.Position >= role.Position {
|
||||
roleSetAllowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !roleSetAllowed {
|
||||
return errors.New("you don't have permission to approve a user as a role higher than your own")
|
||||
}
|
||||
|
||||
err = s.GuildMemberRoleAdd(i.GuildID, user.ID, role.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.GuildMemberRoleRemove(i.GuildID, user.ID, guild.VettingRoleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tickets.Close(s, i.GuildID, user, i.Member.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.RemoveVettingReq(i.GuildID, i.Message.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = eventlog.Log(s, i.GuildID, eventlog.Entry{
|
||||
Title: "New Member Approved!",
|
||||
Description: fmt.Sprintf("**User:** %s\n**Role:** %s\n**Approved By:** %s", user.Mention(), role.Mention(), i.Member.User.Mention()),
|
||||
Author: user,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = welcomeUser(s, guild, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, "Successfully approved "+user.Mention()+" as "+role.Mention()+"!")
|
||||
}
|
||||
|
||||
func welcomeUser(s *discordgo.Session, guild db.Guild, user *discordgo.User) error {
|
||||
if guild.WelcomeChanID != "" && guild.WelcomeMsg != "" {
|
||||
msg := strings.Replace(guild.WelcomeMsg, "$user", user.Mention(), 1)
|
||||
_, err := s.ChannelMessageSend(guild.WelcomeChanID, msg)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -34,78 +34,6 @@ import (
|
|||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
// vettingCmd runs the correct subcommand handler for the vetting command
|
||||
func vettingCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
switch name := data.Options[0].Name; name {
|
||||
case "role":
|
||||
return vettingRoleCmd(s, i)
|
||||
case "req_channel":
|
||||
return vettingReqChannelCmd(s, i)
|
||||
case "welcome_channel":
|
||||
return welcomeChannelCmd(s, i)
|
||||
case "welcome_msg":
|
||||
return welcomeMsgCmd(s, i)
|
||||
default:
|
||||
return fmt.Errorf("unknown vetting subcommand: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
// vettingRoleCmd sets the vetting role for a guild
|
||||
func vettingRoleCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
role := args[0].RoleValue(s, i.GuildID)
|
||||
|
||||
err := db.SetVettingRoleID(i.GuildID, role.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set %s as the vetting role!", role.Mention()))
|
||||
}
|
||||
|
||||
// vettingReqChannelCmd sets the vetting request channel for a guild
|
||||
func vettingReqChannelCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
channel := args[0].ChannelValue(s)
|
||||
|
||||
err := db.SetVettingReqChannel(i.GuildID, channel.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set %s as the vetting request channel!", channel.Mention()))
|
||||
}
|
||||
|
||||
// welcomeChannelCmd sets the welcome channel command for a guild
|
||||
func welcomeChannelCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
channel := args[0].ChannelValue(s)
|
||||
|
||||
err := db.SetWelcomeChannel(i.GuildID, channel.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully set %s as the welcome channel!", channel.Mention()))
|
||||
}
|
||||
|
||||
// welcomeChannelCmd sets the welcome message for a guild
|
||||
func welcomeMsgCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
|
||||
err := db.SetWelcomeMsg(i.GuildID, args[0].StringValue())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, "Successfully set the welcome message!")
|
||||
}
|
||||
|
||||
// onMemberJoin adds the vetting role to a user when they join in order to allow them
|
||||
// to access the vetting questions
|
||||
func onMemberJoin(s *discordgo.Session, gma *discordgo.GuildMemberAdd) {
|
||||
|
@ -232,90 +160,6 @@ func onVettingRequest(s *discordgo.Session, i *discordgo.InteractionCreate) erro
|
|||
return util.RespondEphemeral(s, i.Interaction, "Successfully sent your vetting request!")
|
||||
}
|
||||
|
||||
// approveCmd approves a user in vetting. It removes their vetting role, assigns a
|
||||
// role of the approver's choosing, closes the user's vetting ticket, and logs
|
||||
// the approval.
|
||||
func approveCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
guild, err := db.GuildByID(i.GuildID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if guild.VettingRoleID == "" {
|
||||
return errors.New("vetting role id is not set for this guild")
|
||||
}
|
||||
|
||||
data := i.ApplicationCommandData()
|
||||
user := data.Options[0].UserValue(s)
|
||||
role := data.Options[1].RoleValue(s, i.GuildID)
|
||||
|
||||
_, err = db.TicketChannelID(i.GuildID, user.ID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("%s has no open ticket", user.Mention())
|
||||
}
|
||||
|
||||
roleSetAllowed := false
|
||||
for _, roleID := range i.Member.Roles {
|
||||
executorRole, err := cache.Role(s, i.GuildID, roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if executorRole.Position >= role.Position {
|
||||
roleSetAllowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !roleSetAllowed {
|
||||
return errors.New("you don't have permission to approve a user as a role higher than your own")
|
||||
}
|
||||
|
||||
err = s.GuildMemberRoleAdd(i.GuildID, user.ID, role.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.GuildMemberRoleRemove(i.GuildID, user.ID, guild.VettingRoleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tickets.Close(s, i.GuildID, user, i.Member.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.RemoveVettingReq(i.GuildID, i.Message.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = eventlog.Log(s, i.GuildID, eventlog.Entry{
|
||||
Title: "New Member Approved!",
|
||||
Description: fmt.Sprintf("**User:** %s\n**Role:** %s\n**Approved By:** %s", user.Mention(), role.Mention(), i.Member.User.Mention()),
|
||||
Author: user,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = welcomeUser(s, guild, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, "Successfully approved "+user.Mention()+" as "+role.Mention()+"!")
|
||||
}
|
||||
|
||||
func welcomeUser(s *discordgo.Session, guild db.Guild, user *discordgo.User) error {
|
||||
if guild.WelcomeChanID != "" && guild.WelcomeMsg != "" {
|
||||
msg := strings.Replace(guild.WelcomeMsg, "$user", user.Mention(), 1)
|
||||
_, err := s.ChannelMessageSend(guild.WelcomeChanID, msg)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// onVettingResponse handles responses to vetting requests. If the user was accepted,
|
||||
// it creates a vetting ticket for them. If they were rejected, it kicks them from the server.
|
||||
func onVettingResponse(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
|
@ -403,6 +247,7 @@ func onVettingResponse(s *discordgo.Session, i *discordgo.InteractionCreate) err
|
|||
return nil
|
||||
}
|
||||
|
||||
// onMemberLeave handles users leaving the server. It closes any tickets they might've had open.
|
||||
func onMemberLeave(s *discordgo.Session, gmr *discordgo.GuildMemberRemove) {
|
||||
msgID, err := db.VettingReqMsgID(gmr.GuildID, gmr.Member.User.ID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
|
|
Loading…
Reference in New Issue