Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6d17e796f | |||
| 2c84e6b79c | |||
| 52038b4765 | |||
| c68fcfe439 | |||
| 96a6360629 | |||
| 9c2bba193a |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/dist/
|
||||
/owobot
|
||||
/owobot.db
|
||||
/owobot.db
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
27
config.go
27
config.go
@@ -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_"})
|
||||
}
|
||||
|
||||
24
internal/cache/cache.go
vendored
24
internal/cache/cache.go
vendored
@@ -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) {
|
||||
|
||||
7
internal/db/migrations/2023120800.sql
Normal file
7
internal/db/migrations/2023120800.sql
Normal 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');
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
1
main.go
1
main.go
@@ -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
6
owobot.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
token = "CHANGE ME"
|
||||
db_path = "/etc/owobot/owobot.db"
|
||||
|
||||
[activity]
|
||||
type = -1
|
||||
name = ""
|
||||
Reference in New Issue
Block a user