6 Commits

Author SHA1 Message Date
e6d17e796f Fix ticket log format and limit to 100 messages
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-12-12 12:37:00 -08:00
2c84e6b79c Make sure open vetting requests get removed if a user leaves
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-12-08 08:11:17 -08:00
52038b4765 Switch delimeter to \x1F 2023-12-08 07:59:12 -08:00
c68fcfe439 Add toml config file
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-12-06 13:28:07 -08:00
96a6360629 Fix starboard GuildID handling
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-12-06 19:53:55 +00:00
9c2bba193a Manually inherit permissions in ticket category
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-12-06 07:16:12 -08:00
15 changed files with 156 additions and 45 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/dist/
/owobot
/owobot.db
/owobot.db

View File

@@ -17,6 +17,7 @@ builds:
archives:
- files:
- owobot.service
- owobot.toml
nfpms:
- id: owobot
description: "Your server's guardian and entertainer"
@@ -34,6 +35,9 @@ nfpms:
contents:
- src: owobot.service
dst: /etc/systemd/system/owobot.service
- src: owobot.toml
dst: /etc/owobot/config.toml
type: "config|noreplace"
aurs:
- name: owobot-bin
homepage: 'https://gitea.elara.ws/owobot/owobot'
@@ -47,10 +51,15 @@ aurs:
- owobot
conflicts:
- owobot
backup:
- etc/owobot/config.toml
package: |-
# binaries
install -Dm755 ./owobot "${pkgdir}/usr/bin/owobot"
# configs
install -Dm644 ./owobot.toml "${pkgdir}/etc/owobot/config.toml"
# services
install -Dm644 ./owobot.service "${pkgdir}/etc/systemd/system/owobot.service"
release:

View File

@@ -59,7 +59,7 @@ If a moderator accepts the request, a new ticket will be created in which mods c
### Tickets
owobot can create tickets, which are private channels that allow users to talk directly to your server's moderators. When a ticket is closed, owobot compiles a log containing all the messages in the ticket and sends it to the event log ticket channel.
owobot can create tickets, which are private channels that allow users to talk directly to your server's moderators. When a ticket is closed, owobot compiles a log containing up to 100 messages from the ticket and sends it to the event log ticket channel.
A user can only have one open ticket at a time.

View File

@@ -27,20 +27,33 @@ import (
)
type Config struct {
Token string `env:"TOKEN,notEmpty" toml:"token"`
DBPath string `env:"DB_PATH" envDefault:"owobot.db" toml:"db_path"`
Token string `env:"TOKEN" toml:"token"`
DBPath string `env:"DB_PATH" toml:"db_path"`
Activity Activity `envPrefix:"ACTIVITY_" toml:"activity"`
}
type Activity struct {
Type discordgo.ActivityType `env:"TYPE" envDefault:"-1" toml:"type"`
Name string `env:"NAME" envDefault:"" toml:"name"`
Type discordgo.ActivityType `env:"TYPE" toml:"type"`
Name string `env:"NAME" toml:"name"`
}
func loadConfig() (*Config, error) {
cfg := &Config{}
// Create a new config struct with default values
cfg := &Config{
Token: "",
DBPath: "owobot.db",
Activity: Activity{
Type: -1,
Name: "",
},
}
fl, err := os.Open("/etc/owobot.toml")
configPath := os.Getenv("OWOBOT_CONFIG_PATH")
if configPath == "" {
configPath = "/etc/owobot/config.toml"
}
fl, err := os.Open(configPath)
if err == nil {
err = toml.NewDecoder(fl).Decode(cfg)
if err != nil {
@@ -49,5 +62,7 @@ func loadConfig() (*Config, error) {
fl.Close()
}
println(cfg.Activity.Type, cfg.Activity.Name)
return cfg, env.ParseWithOptions(cfg, env.Options{Prefix: "OWOBOT_"})
}

View File

@@ -66,6 +66,30 @@ func Role(s *discordgo.Session, guildID, roleID string) (*discordgo.Role, error)
return role, nil
}
// Channel gets a discord channel from the cache. If it doesn't exist in the cache, it
// gets it from discord and adds it to the cache.
func Channel(s *discordgo.Session, guildID, channelID string) (*discordgo.Channel, error) {
role, err := s.State.Channel(channelID)
if errors.Is(err, discordgo.ErrStateNotFound) {
// If the role wasn't found in the state struct,
// get the guild roles from discord and add them.
channels, err := s.GuildChannels(guildID)
if err != nil {
return nil, err
}
for _, channel := range channels {
err = s.State.ChannelAdd(channel)
if err != nil {
return nil, err
}
}
return s.State.Channel(channelID)
} else if err != nil {
return nil, err
}
return role, nil
}
// Roles gets a list of roles in a discord guild from the cache. If it doesn't
// exist in the cache, it gets it from discord and adds it to the cache.
func Roles(s *discordgo.Session, guildID string) ([]*discordgo.Role, error) {

View File

@@ -0,0 +1,7 @@
/* change the string delimeter from comma to Unit Separator, to allow commas */
/* to be used in polls and the like */
UPDATE reactions SET reaction = REPLACE(reaction, ',', X'1F');
UPDATE polls SET opt_emojis = REPLACE(opt_emojis, ',', X'1F');
UPDATE polls SET opt_text = REPLACE(opt_text, ',', X'1F');
UPDATE reaction_role_categories SET emoji = REPLACE(emoji, ',', X'1F');
UPDATE reaction_role_categories SET roles = REPLACE(roles, ',', X'1F');

View File

@@ -56,6 +56,10 @@ func GetPoll(msgID string) (*Poll, error) {
}
func AddPollOptionText(msgID string, text string) error {
if strings.Contains(text, "\x1F") {
return errors.New("option string cannot contain unit separator")
}
var optText string
err := db.QueryRow("SELECT opt_text FROM polls WHERE msg_id = ?", msgID).Scan(&optText)
if err != nil {
@@ -65,11 +69,15 @@ func AddPollOptionText(msgID string, text string) error {
splitText := splitOptions(optText)
splitText = append(splitText, text)
_, err = db.Exec("UPDATE polls SET opt_text = ? WHERE msg_id = ?", strings.Join(splitText, ","), msgID)
_, err = db.Exec("UPDATE polls SET opt_text = ? WHERE msg_id = ?", strings.Join(splitText, "\x1F"), msgID)
return err
}
func AddPollOptionEmoji(msgID string, emoji string) error {
if strings.Contains(emoji, "\x1F") {
return errors.New("emoji string cannot contain unit separator")
}
var optEmojis string
err := db.QueryRow("SELECT opt_emojis FROM polls WHERE msg_id = ?", msgID).Scan(&optEmojis)
if err != nil {
@@ -82,7 +90,7 @@ func AddPollOptionEmoji(msgID string, emoji string) error {
}
splitEmojis = append(splitEmojis, emoji)
_, err = db.Exec("UPDATE polls SET opt_emojis = ? WHERE msg_id = ?", strings.Join(splitEmojis, ","), msgID)
_, err = db.Exec("UPDATE polls SET opt_emojis = ? WHERE msg_id = ?", strings.Join(splitEmojis, "\x1F"), msgID)
return err
}
@@ -119,5 +127,5 @@ func splitOptions(s string) []string {
if s == "" {
return nil
}
return strings.Split(s, ",")
return strings.Split(s, "\x1F")
}

View File

@@ -19,6 +19,7 @@
package db
import (
"errors"
"slices"
"strings"
@@ -41,8 +42,8 @@ func AddReactionRoleCategory(channelID string, rrc ReactionRoleCategory) error {
channelID,
rrc.Name,
rrc.Description,
strings.Join(rrc.Emoji, ","),
strings.Join(rrc.Roles, ","),
strings.Join(rrc.Emoji, "\x1F"),
strings.Join(rrc.Roles, "\x1F"),
)
return err
}
@@ -74,6 +75,10 @@ func DeleteReactionRoleCategory(channelID, name string) error {
}
func AddReactionRole(channelID, category, emoji string, role *discordgo.Role) error {
if strings.Contains(category, "\x1F") || strings.Contains(emoji, "\x1F") {
return errors.New("reaction roles cannot contain unit separator")
}
var oldEmoji, oldRoles string
err := db.QueryRow("SELECT emoji, roles FROM reaction_role_categories WHERE name = ? AND channel_id = ?", category, channelID).Scan(&oldEmoji, &oldRoles)
if err != nil {
@@ -86,8 +91,8 @@ func AddReactionRole(channelID, category, emoji string, role *discordgo.Role) er
_, err = db.Exec(
"UPDATE reaction_role_categories SET emoji = ?, roles = ? WHERE name = ? AND channel_id = ?",
strings.Join(splitEmoji, ","),
strings.Join(splitRoles, ","),
strings.Join(splitEmoji, "\x1F"),
strings.Join(splitRoles, "\x1F"),
category,
channelID,
)
@@ -111,8 +116,8 @@ func DeleteReactionRole(channelID, category string, role *discordgo.Role) error
_, err = db.Exec(
"UPDATE reaction_role_categories SET emoji = ?, roles = ? WHERE name = ? AND channel_id = ?",
strings.Join(splitEmoji, ","),
strings.Join(splitRoles, ","),
strings.Join(splitEmoji, "\x1F"),
strings.Join(splitRoles, "\x1F"),
category,
channelID,
)

View File

@@ -77,6 +77,8 @@ func reactionsAddCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error
if err := validateEmoji(reaction.Reaction); err != nil {
return err
}
// Use the correct delimeter for the DB
reaction.Reaction = strings.ReplaceAll(reaction.Reaction, ",", "\x1F")
}
err := db.AddReaction(i.GuildID, reaction)
@@ -140,6 +142,8 @@ func validateEmoji(s string) error {
return fmt.Errorf("invalid reaction emoji: %s", emojiStr)
}
}
} else if strings.Contains(s, "\x1F") {
return fmt.Errorf("emoji string cannot contain unit separator")
} else {
if _, ok := emoji.Parse(s); !ok {
return fmt.Errorf("invalid reaction emoji: %s", s)

View File

@@ -195,7 +195,7 @@ func onReaction(s *discordgo.Session, mra *discordgo.MessageReactionAdd) {
},
Description: fmt.Sprintf(
"[**Jump to Message**](https://discord.com/channels/%s/%s/%s)",
msg.GuildID,
mra.GuildID,
msg.ChannelID,
msg.ID,
),

View File

@@ -9,6 +9,7 @@ import (
"github.com/bwmarrin/discordgo"
"go.elara.ws/logger/log"
"go.elara.ws/owobot/internal/cache"
"go.elara.ws/owobot/internal/db"
"go.elara.ws/owobot/internal/systems/commands"
"go.elara.ws/owobot/internal/systems/eventlog"
@@ -139,15 +140,29 @@ func Open(s *discordgo.Session, guildID string, user, executor *discordgo.User)
return "", err
}
overwrites := []*discordgo.PermissionOverwrite{{
ID: user.ID,
Type: discordgo.PermissionOverwriteTypeMember,
Allow: ticketPermissions,
}}
if guild.TicketCategoryID != "" {
category, err := cache.Channel(s, guildID, guild.TicketCategoryID)
if err != nil {
log.Error("Error getting ticket category").Err(err).Send()
// If we can't get the ticket category, set it to empty string
// so that ChannelCreate doesn't try to use it.
guild.TicketCategoryID = ""
} else {
overwrites = append(overwrites, category.PermissionOverwrites...)
}
}
c, err := s.GuildChannelCreateComplex(guildID, discordgo.GuildChannelCreateData{
Name: "ticket-" + user.Username,
Type: discordgo.ChannelTypeGuildText,
ParentID: guild.TicketCategoryID,
PermissionOverwrites: []*discordgo.PermissionOverwrite{{
ID: user.ID,
Type: discordgo.PermissionOverwriteTypeMember,
Allow: ticketPermissions,
}},
Name: "ticket-" + user.Username,
Type: discordgo.ChannelTypeGuildText,
ParentID: guild.TicketCategoryID,
PermissionOverwrites: overwrites,
})
if err != nil {
return "", err
@@ -228,26 +243,13 @@ func getChannelMessageLog(s *discordgo.Session, channelID string) (*bytes.Buffer
return nil, err
}
msgAmt := len(msgs)
for msgAmt == 100 {
innerMsgs, err := s.ChannelMessages(channelID, 100, "", msgs[99].ID, "")
if err != nil {
return nil, err
}
err = writeMsgs(innerMsgs, out)
if err != nil {
return nil, err
}
msgAmt = len(innerMsgs)
}
return out, nil
}
// writeMsgs writes a slice of messages to w.
func writeMsgs(msgs []*discordgo.Message, w io.Writer) error {
for _, msg := range msgs {
_, err := io.WriteString(w, fmt.Sprintf("%s - %s\n", msg.Author.Username, msg.Content))
for i := len(msgs); i >= 0; i-- {
_, err := io.WriteString(w, fmt.Sprintf("%s - %s\n", msgs[i].Author.Username, msgs[i].Content))
if err != nil {
return err
}

View File

@@ -232,10 +232,10 @@ func onVettingRequest(s *discordgo.Session, i *discordgo.InteractionCreate) erro
return util.RespondEphemeral(s, i.Interaction, "Successfully sent your vetting request!")
}
// onApprove approves a user in vetting. It removes their vetting role, assigns a
// 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 onApprove(s *discordgo.Session, i *discordgo.InteractionCreate) error {
func approveCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
guild, err := db.GuildByID(i.GuildID)
if err != nil {
return err
@@ -287,7 +287,7 @@ func onApprove(s *discordgo.Session, i *discordgo.InteractionCreate) error {
err = eventlog.Log(s, i.GuildID, eventlog.Entry{
Title: "New Member Approved!",
Description: fmt.Sprintf("User: %s\nRole: %s\nApproved By: %s", user.Mention(), role.Mention(), i.Member.User.Mention()),
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 {
@@ -397,3 +397,31 @@ func onVettingResponse(s *discordgo.Session, i *discordgo.InteractionCreate) err
return db.RemoveVettingReq(i.GuildID, i.Message.ID)
}
func onMemberLeave(s *discordgo.Session, gmr *discordgo.GuildMemberRemove) {
msgID, err := db.VettingReqMsgID(gmr.GuildID, gmr.Member.User.ID)
if errors.Is(err, sql.ErrNoRows) {
return
} else if err != nil {
log.Error("Error getting vetting request ID after member leave").Str("user-id", gmr.Member.User.ID).Err(err).Send()
return
}
guild, err := db.GuildByID(gmr.GuildID)
if err != nil {
log.Error("Error getting guild").Str("guild-id", gmr.GuildID).Err(err).Send()
return
}
if guild.VettingReqChanID != "" {
err = s.ChannelMessageDelete(guild.VettingReqChanID, msgID)
if err != nil {
log.Error("Error deleting vetting request message after member leave").Str("msg-id", msgID).Err(err).Send()
}
}
err = db.RemoveVettingReq(gmr.GuildID, msgID)
if err != nil {
log.Error("Error removing vetting request after member leave").Str("user-id", gmr.Member.User.ID).Err(err).Send()
}
}

View File

@@ -34,6 +34,7 @@ func Init(s *discordgo.Session) error {
s.AddHandler(onMemberJoin)
s.AddHandler(util.InteractionErrorHandler("on-vetting-req", onVettingRequest))
s.AddHandler(util.InteractionErrorHandler("on-vetting-resp", onVettingResponse))
s.AddHandler(onMemberLeave)
commands.Register(s, onMakeVettingMsg, &discordgo.ApplicationCommand{
Name: "Make Vetting Message",
@@ -103,7 +104,7 @@ func Init(s *discordgo.Session) error {
},
})
commands.Register(s, onApprove, &discordgo.ApplicationCommand{
commands.Register(s, approveCmd, &discordgo.ApplicationCommand{
Name: "approve",
Description: "Approve a member in vetting",
DefaultMemberPermissions: util.Pointer[int64](discordgo.PermissionKickMembers),

View File

@@ -68,6 +68,7 @@ func main() {
s.StateEnabled = true
s.State.TrackMembers = true
s.State.TrackRoles = true
s.State.TrackChannels = true
s.Identify.Intents |= discordgo.IntentMessageContent | discordgo.IntentGuildMembers
err = s.Open()

6
owobot.toml Normal file
View File

@@ -0,0 +1,6 @@
token = "CHANGE ME"
db_path = "/etc/owobot/owobot.db"
[activity]
type = -1
name = ""