Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0c8366b7b | |||
| ddff13518f | |||
| 310f417521 | |||
| d2f901e1b1 | |||
| 5662e3dbb8 | |||
| b059438179 | |||
| 4f533eac6b | |||
| 5d327f3fd2 | |||
| c28bc88939 | |||
| 4f57cbf780 | |||
| 48668d3af7 | |||
| 3cc0003d65 | |||
| 53ecef743c | |||
| 29b787d31e | |||
| a1257cd9e7 | |||
| dec4702e8a | |||
| 2aa95df91b | |||
| 0a98a24833 | |||
| b563fae9b0 | |||
| ee61581b29 | |||
| 8452a2e8e9 | |||
| 4f7b22505b | |||
| b469248538 | |||
| 4b1ef6872a | |||
| 8de82dd138 | |||
| 082348465e | |||
| fc1cf3a2a3 | |||
| e6d17e796f | |||
| 2c84e6b79c | |||
| 52038b4765 | |||
| c68fcfe439 | |||
| 96a6360629 | |||
| 9c2bba193a | |||
| c0a27c8a52 | |||
| 759dd9a390 | |||
| e90b6809fe | |||
| a5933b2a97 | |||
| dd550b10cf | |||
| 20c6771e7b | |||
| ddfd220eea | |||
| 76c2ee59d2 | |||
| 74b554b381 | |||
| 91d0bbf45d | |||
| 36ab48a81b | |||
| b58753eb61 | |||
| 530b5a03d3 | |||
| dfe53d2f6a | |||
| 4a3bf65100 | |||
| 7bc24e986a | |||
| 9790206f16 | |||
| b16ef37e72 | |||
| f856e1619f | |||
| 070e352912 | |||
| 72b80da644 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
/dist/
|
||||
/plugins/
|
||||
/owobot
|
||||
/owobot.db
|
||||
/owobot.db
|
||||
|
||||
@@ -17,9 +17,10 @@ builds:
|
||||
archives:
|
||||
- files:
|
||||
- owobot.service
|
||||
- owobot.toml
|
||||
nfpms:
|
||||
- id: owobot
|
||||
description: "The coolest bot ever written"
|
||||
description: "Your server's guardian and entertainer"
|
||||
homepage: 'https://gitea.elara.ws/owobot/owobot'
|
||||
maintainer: 'Elara Musayelyan <elara@elara.ws>'
|
||||
license: AGPLv3
|
||||
@@ -34,23 +35,31 @@ 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'
|
||||
description: "The coolest bot ever written"
|
||||
description: "Your server's guardian and entertainer"
|
||||
maintainers:
|
||||
- 'Elara Musayelyan <elara@elara.ws>'
|
||||
license: GPLv3
|
||||
license: AGPLv3
|
||||
private_key: '{{ .Env.AUR_KEY }}'
|
||||
git_url: 'ssh://aur@aur.archlinux.org/owobot-bin.git'
|
||||
provides:
|
||||
- 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:
|
||||
|
||||
@@ -10,7 +10,7 @@ steps:
|
||||
secrets: [ registry_password ]
|
||||
commands:
|
||||
- registry-login
|
||||
- ko build -B --platform=linux/amd64,linux/arm64,linux/riscv64 --sbom=none
|
||||
- ko build -B --platform=linux/amd64,linux/arm64,linux/riscv64 -t latest,${CI_COMMIT_TAG} --sbom=none
|
||||
when:
|
||||
event: tag
|
||||
|
||||
|
||||
29
CONTRIBUTING.md
Normal file
29
CONTRIBUTING.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Contributing to owobot
|
||||
|
||||
Thanks for your interest in contributing to owobot! This page contains information that you should know before contributing.
|
||||
|
||||
## Code structure
|
||||
|
||||
### Systems
|
||||
|
||||
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.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.
|
||||
|
||||
If you change anything in the database, always make a new migration file rather than editing existing ones. This way, owobot will automatically apply the the changes whenever it's run next. Changing migrations requires a full recompile because they're embedded into the binary.
|
||||
|
||||
## Testing
|
||||
|
||||
If you want to test out your changes, you'll need to make a test server and bot account. To do that, go to https://discord.com/developers/applications and create a new application. Then, go to `Bot` in the sidebar, and enable the privileged gateway intents for `Message Content` and `Server Members`. Now, go to `OAuth2 > URL Generator`, select `bot` in Scopes, and then `Administrator` in Bot Permissions. That will give you a URL. Next, go to Discord and make a new server that you'll use for testing. Then, paste the URL you generated into your browser and invite your test bot into your new server.
|
||||
29
INSTALL.md
Normal file
29
INSTALL.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Installing owobot
|
||||
|
||||
## Discord setup
|
||||
|
||||
Before installing the bot, you need to do some set up in your Discord account. Start by opening the [Developer Portal](https://discord.com/developers/applications) and creating a new application. Once you've created it, click on your new application. Then, go to `Bot` in the sidebar and enable the Presence Intent, Server Members Intent, and Message Content intent.
|
||||
|
||||
To get the token that you have to pass to `owobot`, click on the `Reset Token` button to generate a new token, and then copy it and store it somewhere safe (you won't be able to see it later).
|
||||
|
||||
## Linux packages
|
||||
|
||||
The [latest release](https://gitea.elara.ws/owobot/owobot/releases/latest) contains RPM, Deb, and Arch packages, and owobot is available [on the AUR](https://aur.archlinux.org/packages/owobot-bin/) as well. Choose whichever one of those you need and install it with your package manager.
|
||||
|
||||
Once it's installed, there should be a default config file at `/etc/owobot.toml`. You can edit that to add your token, change the activity text, etc. and then run the bot by running `sudo systemctl enable --now owobot`. Systemd will now start running the bot and monitoring it to make sure it doesn't go down.
|
||||
|
||||
That's it! Your bot should be up and running!
|
||||
|
||||
## Docker
|
||||
|
||||
This guide will use Docker, but `owobot` should work with any other OCI-compatible container engine, such as Podman. The container image is hosted on [Gitea](https://gitea.elara.ws/owobot/-/packages/container/owobot/latest).
|
||||
|
||||
There's a [`docker-compose.yml`](docker-compose.yml) file provided in this repo as a starting point. Here's how you can use it:
|
||||
|
||||
1. First, make sure `docker` and `docker-compose` are installed and working
|
||||
2. Create a new folder for owobot to use to store its data
|
||||
3. Put the example `docker-compose.yml` file into the new folder
|
||||
4. Edit the `docker-compose.yml` file to set the token and anything else you may want to change
|
||||
5. Make sure the directory can be accessed by the container's user (`sudo chown -R 65532:65532 folder`)
|
||||
6. Run `docker-compose up -d`
|
||||
7. That's it! Your bot should now be running.
|
||||
98
README.md
98
README.md
@@ -1,32 +1,71 @@
|
||||
# owobot - The coolest bot ever written
|
||||
<p align="center">
|
||||
<img src="assets/images/banner.png">
|
||||
</p>
|
||||
|
||||
## Introduction
|
||||
<p align = "center">
|
||||
<a href="https://aur.archlinux.org/packages/owobot-bin/"><img alt="owobot-bin AUR package" src="https://img.shields.io/aur/version/owobot-bin?label=owobot-bin&logo=archlinux&style=for-the-badge"></a>
|
||||
<a href="https://gitea.elara.ws/owobot/owobot/releases/latest"><img alt="Gitea Releases Badge" src="https://img.shields.io/badge/gitea-release-609926?logo=gitea&style=for-the-badge"></a>
|
||||
<a href="https://gitea.elara.ws/owobot/-/packages/container/owobot/latest"><img alt="OCI Image Badge" src="https://img.shields.io/badge/oci-images-24184C?logo=opencontainersinitiative&style=for-the-badge"></a>
|
||||
<a href="https://discord.gg/5B2wVS7gnY"><img alt="Discord badge" src="https://img.shields.io/discord/1180630319402057758?style=for-the-badge&logo=discord&color=5865F2"></a>
|
||||
</p>
|
||||
|
||||
owobot is a powerful Discord bot designed to handle a wide range of tasks in your server, from moderation to entertainment. It takes advantage of several cutting-edge discord features, such as message buttons and modals.
|
||||
owobot is a versatile Discord bot with a wide range of capabilities, from moderation to utilities to entertainment. Using state-of-the-art Discord features, owobot takes your server experience to the next level. It's the ultimate companion for your Discord server!
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Installation Options](#installation-options)
|
||||
- [Linux packages](#linux-packages)
|
||||
- [OCI/Docker images](#ocidocker-images)
|
||||
- [Features](#features)
|
||||
- [Vetting](#vetting)
|
||||
- [Tickets](#tickets)
|
||||
- [Eventlog](#eventlog)
|
||||
- [Reactions](#reactions)
|
||||
- [Reaction Roles](#reaction-roles)
|
||||
- [Polls](#polls)
|
||||
- [Starboard](#starboard)
|
||||
- [Rate Limiting](#rate-limiting)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
## Installation Options
|
||||
|
||||
See the [installation guide](INSTALL.md) for in-depth instructions on installing and running owobot.
|
||||
|
||||
### Linux packages
|
||||
|
||||
The [latest release](https://gitea.elara.ws/owobot/owobot/releases/latest) of owobot has packages for many distros and processors. They include a systemd service to make the bot easier to use. Just install the package, edit the bot's configuration to suit your needs, and run it using `sudo systemctl enable --now owobot`. Systemd will monitor it and make sure it's always running.
|
||||
|
||||
### OCI/Docker images
|
||||
|
||||
For more advanced users, owobot provides OCI/Docker images, which can be used with Docker, Podman, LXC, and many other programs. Make sure to set `OWOBOT_DB_PATH` to wherever you've mounted the database. The image is rootless by default, so make sure that uid `65532` can access the database, or manually set the image to run as root using `-u root`.
|
||||
|
||||
## Features
|
||||
|
||||
### Vetting
|
||||
|
||||
In order to catch trolls and other troublemakers before they get access to your server, owobot can be configured to require new users to go through a vetting process before gaining access to the server.
|
||||
To catch trolls and other troublemakers before they get access to your server, owobot can be configured to require new users to go through a vetting process before they can get access to the server.
|
||||
|
||||
To create your vetting message, just choose any message, click `More > Apps > Make Vetting Message`, and that's it! owobot will delete the message and post a new one with a message button which can be used by users to request vetting.
|
||||
To create a vetting message, just choose any message, click `More > Apps > Make Vetting Message`, and that's it! owobot will delete the message and post a new one with a message button which can be used by users to request vetting.
|
||||
|
||||
When users click the request vetting button, owobot will send a vetting request in the vetting request channel.
|
||||
|
||||
If a moderator accepts the request, a new ticket will be created in which mods can talk to the user. When they're finished, they can either kick the user which will automatically close the ticket, or they can approve the user using the `/approve` command.
|
||||
|
||||
Commands:
|
||||
**Commands:**
|
||||
|
||||
- `/vetting role` can be used by anyone with the `Manage Server` permission to set the server's vetting role. owobot will assign this role to all new users.
|
||||
- `/vetting req_channel` can be used by anyone with the `Manage Server` permission to set the server's vetting request channel. This is where owobot will post vetting requests.
|
||||
- `/vetting welcome_channel` can be used by anyone with the `Manage Server` permission to set the server's welcome channel. This is where owobot will welcome new users.
|
||||
- `/vetting welcome_msg` can be used by anyone with the `Manage Server` permission to set the server's welcome message. This is the message owobot will use to welcome new users. You can use `$user` in the message, which will be replaced with a user mention.
|
||||
- `/approve` can be used by anyone with the `Kick Members` permission to approve users that are in vetting.
|
||||
|
||||
### Tickets
|
||||
|
||||
owobot can create tickets to allow users to privately talk to your server's moderators. Only one ticket per user can exist at any time. When a ticket is closed, a log containing all the messages in the ticket is sent 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.
|
||||
|
||||
Commands:
|
||||
A user can only have one open ticket at a time.
|
||||
|
||||
**Commands:**
|
||||
|
||||
- `/ticket` can be used by any user to create a ticket for themselves
|
||||
- `/mod_ticket` can be used by anyone with the `Manage Channels` permission to create a ticket for another user
|
||||
@@ -35,24 +74,25 @@ Commands:
|
||||
|
||||
### Eventlog
|
||||
|
||||
The eventlog sends important events such as kicks/bans, role changes, etc. to a configurable discord channel.
|
||||
The eventlog listens for important events such as kicks, bans, role changes, etc. and sends them to a configurable discord channel so you can always access them.
|
||||
|
||||
Commands:
|
||||
**Commands:**
|
||||
|
||||
- `/eventlog channel` can be used by anyone with the `Manage Server` permission to set the channel for the event log
|
||||
- `/eventlog ticket_channel` can be used by anyone with the `Manage Server` permission to set the channel in which ticket conversations logs will be sent
|
||||
- `/eventlog time_format` can be used by anyone with the `Manage Server` permission to set the time format for embeds. You can either use `discord` for a timezone-agnostic timestamp or a [strftime string](https://github.com/lestrrat-go/strftime#supported-conversion-specifications), which will be in UTC.
|
||||
|
||||
### Reactions
|
||||
|
||||
owobot has a very powerful reaction system which can find content inside of messages and then react with an emoji or reply with text.
|
||||
owobot has a very powerful reactions system which can find content inside of messages and then react with an emoji or reply with text.
|
||||
|
||||
A single reaction consists of a match type, match, reaction type, reaction, and an optional random chance.
|
||||
A reaction consists of a match type, match, reaction type, reaction, and an optional random chance.
|
||||
|
||||
The match type can either be `contains` or `regex`. The `contains` matcher checks if a message contains the match. The `regex` matcher checks if a message matches a regular expression and extracts any submatches. If you're using the `regex` matcher with the `text` reaction type, you can include submatches in your reply by putting the submatch index in curly braces (for example: `{1}` or `{5}`).
|
||||
The match type can either be `contains` or `regex`. The `contains` matcher just checks if a message contains some text, while the `regex` matcher checks if a message matches a specific pattern. If you're using the `regex` matcher with the `text` reaction type, you can include submatches in your reply by putting the submatch index in curly braces (for example: `{1}` or `{5}`), which lets you put parts of the original message in your reply.
|
||||
|
||||
The optional random chance allows you to add reactions that only occur a certain percentage of the time. Setting it to `10`, for example, means the reaction will only happen in 10% of detected messages.
|
||||
|
||||
Commands:
|
||||
**Commands:**
|
||||
|
||||
- `/reactions add` can be used by anyone with the `Manage Expressions` permission to add new reactions
|
||||
- `/reactions list` can be used by anyone with the `Manage Expressions` permission to get a list of all existing reactions
|
||||
@@ -63,11 +103,9 @@ Commands:
|
||||
|
||||
### Reaction Roles
|
||||
|
||||
Reaction roles allow users to easily assign roles to themselves using message buttons.
|
||||
Reaction roles allow users to easily assign roles to themselves using message buttons. Reaction roles are organized in categories, which can have a name and a description. Having multiple categories with the same name is allowed, as long as they're in different channels.
|
||||
|
||||
Reaction roles are organized in categories, which can have a name and a description. You can't have more than one category with the same name in a given channel.
|
||||
|
||||
Commands:
|
||||
**Commands:**
|
||||
|
||||
- `/reaction_roles new_category` can be used by anyone with the `Manage Server` permission to create a new reaction role category in the current channel
|
||||
- `/reaction_roles remove_category` can be used by anyone with the `Manage Server` permission to remove an existing reaction role category from the current channel
|
||||
@@ -79,29 +117,39 @@ Commands:
|
||||
|
||||
### Polls
|
||||
|
||||
owobot can easily create polls for your members to vote in. Polls use message components and privacy tokens to ensure that votes are always private and even the person running the bot can't find out who voted for what.
|
||||
owobot can create polls for your members to vote in. Polls use message components and privacy tokens to ensure that votes are always private and even the person running the bot can't find out who voted for what.
|
||||
|
||||
A poll can be created using the `/poll` command. owobot will create a message with just the title and two buttons: `Add Options` and `Finish`. Clicking the `Add Options` button opens a modal (pop up) where you can type the text for a new option. Once that's done, owobot edits the message and asks the poll owner to react with the emoji they'd like to use for that poll. Once they react, that option is added. Options can keep being added until the Finish button is clicked, which finalizes the poll, creates a thread, and opens it up to votes.
|
||||
You can create a poll with the `/poll` command. owobot will create a message with the title and two buttons: `Add Option` and `Finish`.
|
||||
|
||||
Commands:
|
||||
Clicking the `Add Option` button opens a pop up where you can type the text for your new option. Once you've submitted that, owobot edits the message and asks you to react with the emoji you'd like to use for that option.
|
||||
|
||||
Once you react, the option is added. You can keep adding options until you click the Finish button, which finalizes the poll, creates a thread, and opens it up to votes.
|
||||
|
||||
If a user votes multiple times, only the latest vote will be counted.
|
||||
|
||||
**Commands:**
|
||||
|
||||
- `/poll` can be used by any user to create a poll
|
||||
|
||||
### Starboard
|
||||
|
||||
The starboard is a way for your users to feature the messages they like. Users can react to messages with stars, and once a configurable threshold of stars is reached, the message will be posted to the starboard channel.
|
||||
The starboard is a way for your users to feature messages that they like. Users can react to messages with stars, and once a configurable threshold of stars is reached, the message will be posted to the starboard channel.
|
||||
|
||||
Commands:
|
||||
**Commands:**
|
||||
|
||||
- `/starboard stars` can be used by anyone with the `Manage Server` permission to set the star reaction threshold for the starboard (The default is 3)
|
||||
- `/starboard channel` can be used by anyone with the `Manage Server` permission to set the starboard channel for the server.
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
owobot will rate limit events such as channel deletions, kicks, and bans, ensuring that compromised mod accounts can't destroy the server. If a user gets near the rate limit, they'll receive two warnings and then they'll be kicked from the server.
|
||||
owobot limits the speed at which events such as channel deletions, kicks, and bans can happen, ensuring that compromised mod accounts can't destroy the server. If a user gets near the rate limit, they'll receive two warnings and then they'll be kicked from the server if they continue.
|
||||
|
||||
Here are the current rate limits:
|
||||
|
||||
- `channel_delete`: 10 / minute
|
||||
- `kick`: 10 / minute
|
||||
- `ban`: 7 / 5 minutes
|
||||
- `ban`: 7 / 5 minutes
|
||||
|
||||
## Contributing
|
||||
|
||||
See the [CONTRIBUTING.md](CONTRIBUTING.md) file for more information about contributing to owobot.
|
||||
BIN
assets/images/banner.png
Normal file
BIN
assets/images/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
44
config.go
44
config.go
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,22 +19,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/caarlos0/env/v10"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Token string `env:"TOKEN,notEmpty"`
|
||||
DBPath string `env:"DB_PATH" envDefault:"owobot.db"`
|
||||
Activity Activity `envPrefix:"ACTIVITY_"`
|
||||
Token string `env:"TOKEN" toml:"token"`
|
||||
DBPath string `env:"DB_PATH" toml:"db_path"`
|
||||
PluginDir string `env:"PLUGIN_DIR" toml:"plugin_dir"`
|
||||
Activity Activity `envPrefix:"ACTIVITY_" toml:"activity"`
|
||||
}
|
||||
|
||||
type Activity struct {
|
||||
Type discordgo.ActivityType `env:"TYPE" envDefault:"-1"`
|
||||
Name string `env:"NAME" envDefault:""`
|
||||
Type discordgo.ActivityType `env:"TYPE" toml:"type"`
|
||||
Name string `env:"NAME" toml:"name"`
|
||||
}
|
||||
|
||||
func loadEnv() (*Config, error) {
|
||||
cfg := &Config{}
|
||||
func loadConfig() (*Config, error) {
|
||||
// Create a new config struct with default values
|
||||
cfg := &Config{
|
||||
Token: "",
|
||||
DBPath: "owobot.db",
|
||||
PluginDir: "plugins",
|
||||
Activity: Activity{
|
||||
Type: -1,
|
||||
Name: "",
|
||||
},
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
fl.Close()
|
||||
}
|
||||
|
||||
return cfg, env.ParseWithOptions(cfg, env.Options{Prefix: "OWOBOT_"})
|
||||
}
|
||||
|
||||
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
version: '3'
|
||||
services:
|
||||
owobot:
|
||||
image: gitea.elara.ws/owobot/owobot:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./:/data
|
||||
environment:
|
||||
OWOBOT_TOKEN: 'Change Me'
|
||||
OWOBOT_DB_PATH: /data/owobot.db
|
||||
OWOBOT_ACTIVITY_TYPE: '-1'
|
||||
OWOBOT_ACTIVITY_NAME: ''
|
||||
31
go.mod
31
go.mod
@@ -3,32 +3,43 @@ module go.elara.ws/owobot
|
||||
go 1.21.0
|
||||
|
||||
require (
|
||||
github.com/bwmarrin/discordgo v0.27.1
|
||||
github.com/bwmarrin/discordgo v0.28.1
|
||||
github.com/caarlos0/env/v10 v10.0.0
|
||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
|
||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d
|
||||
github.com/jmoiron/sqlx v1.3.5
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/lestrrat-go/strftime v1.0.6
|
||||
github.com/pelletier/go-toml/v2 v2.1.0
|
||||
github.com/rivo/uniseg v0.4.4
|
||||
github.com/rqlite/sql v0.0.0-20241029220113-152a320b02f7
|
||||
github.com/valyala/fasttemplate v1.2.2
|
||||
go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae
|
||||
go.elara.ws/vercmp v0.0.0-20231003203944-671892886053
|
||||
golang.org/x/net v0.24.0
|
||||
modernc.org/sqlite v1.27.0
|
||||
mvdan.cc/xurls v1.1.0
|
||||
mvdan.cc/xurls/v2 v2.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gookit/color v1.5.1 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mvdan/xurls v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/stretchr/testify v1.8.1 // indirect
|
||||
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/sys v0.9.0 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
golang.org/x/crypto v0.22.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
|
||||
105
go.sum
105
go.sum
@@ -1,28 +1,57 @@
|
||||
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
|
||||
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
|
||||
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA=
|
||||
github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18=
|
||||
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
|
||||
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
||||
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
|
||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw=
|
||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d h1:W1n4DvpzZGOISgp7wWNtraLcHtnmnTwBlJidqtMIuwQ=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gookit/color v1.5.1 h1:Vjg2VEcdHpwq+oY63s/ksHrgJYCTo0bwWvmmYWdE9fQ=
|
||||
github.com/gookit/color v1.5.1/go.mod h1:wZFzea4X8qN6vHOSP2apMb4/+w/orMznEzYsIHPaqKM=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
|
||||
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
|
||||
github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ=
|
||||
github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
@@ -30,47 +59,89 @@ github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mvdan/xurls v1.1.0 h1:OpuDelGQ1R1ueQ6sSryzi6P+1RtBpfQHM8fJwlE45ww=
|
||||
github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rqlite/sql v0.0.0-20241029220113-152a320b02f7 h1:Mnz6yd4FWtiD6bbH9WHFFHfrOM2OYTUTmwrsckRc4W8=
|
||||
github.com/rqlite/sql v0.0.0-20241029220113-152a320b02f7/go.mod h1:ib9zVtNgRKiGuoMyUqqL5aNpk+r+++YlyiVIkclVqPg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae h1:d+gJUhEWSrOjrrfgeydYWEr8TTnx0DLvcVhghaOsFeE=
|
||||
go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae/go.mod h1:qng49owViqsW5Aey93lwBXONw20oGbJIoLVscB16mPM=
|
||||
go.elara.ws/vercmp v0.0.0-20231003203944-671892886053 h1:tQ6Kyq9I0Sw9bmXQ1MZdH5EVpEc5brXe8utBCTI5pr0=
|
||||
go.elara.ws/vercmp v0.0.0-20231003203944-671892886053/go.mod h1:/7PNW7nFnDR5W7UXZVc04gdVLR/wBNgkm33KgIz0OBk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -102,5 +173,5 @@ modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
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=
|
||||
|
||||
26
internal/cache/cache.go
vendored
26
internal/cache/cache.go
vendored
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -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) {
|
||||
|
||||
2
internal/cache/regex.go
vendored
2
internal/cache/regex.go
vendored
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -34,6 +34,11 @@ var migrations embed.FS
|
||||
|
||||
var db *sqlx.DB
|
||||
|
||||
// DB returns the global database instance
|
||||
func DB() *sqlx.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
// Init opens the database and applies migrations
|
||||
func Init(ctx context.Context, dsn string) error {
|
||||
g, err := sqlx.Open("sqlite", dsn)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -21,17 +21,23 @@ package db
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type Guild struct {
|
||||
ID string `db:"id"`
|
||||
StarboardChanID string `db:"starboard_chan_id"`
|
||||
StarboardStars int `db:"starboard_stars"`
|
||||
LogChanID string `db:"log_chan_id"`
|
||||
TicketLogChanID string `db:"ticket_log_chan_id"`
|
||||
TicketCategoryID string `db:"ticket_category_id"`
|
||||
VettingReqChanID string `db:"vetting_req_chan_id"`
|
||||
VettingRoleID string `db:"vetting_role_id"`
|
||||
ID string `db:"id"`
|
||||
StarboardChanID string `db:"starboard_chan_id"`
|
||||
StarboardStars int `db:"starboard_stars"`
|
||||
LogChanID string `db:"log_chan_id"`
|
||||
TicketLogChanID string `db:"ticket_log_chan_id"`
|
||||
TicketCategoryID string `db:"ticket_category_id"`
|
||||
VettingReqChanID string `db:"vetting_req_chan_id"`
|
||||
VettingRoleID string `db:"vetting_role_id"`
|
||||
TimeFormat string `db:"time_format"`
|
||||
WelcomeChanID string `db:"welcome_chan_id"`
|
||||
WelcomeMsg string `db:"welcome_msg"`
|
||||
EnabledPlugins StringSlice `db:"enabled_plugins"`
|
||||
}
|
||||
|
||||
func AllGuilds() ([]Guild, error) {
|
||||
@@ -86,6 +92,54 @@ func SetVettingRoleID(guildID, roleID string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func SetTimeFormat(guildID, timeFmt string) error {
|
||||
_, err := db.Exec("UPDATE guilds SET time_format = ? WHERE id = ?", timeFmt, guildID)
|
||||
return err
|
||||
}
|
||||
|
||||
func SetWelcomeChannel(guildID, channelID string) error {
|
||||
_, err := db.Exec("UPDATE guilds SET welcome_chan_id = ? WHERE id = ?", channelID, guildID)
|
||||
return err
|
||||
}
|
||||
|
||||
func SetWelcomeMsg(guildID, msg string) error {
|
||||
_, err := db.Exec("UPDATE guilds SET welcome_msg = ? WHERE id = ?", msg, guildID)
|
||||
return err
|
||||
}
|
||||
|
||||
func EnablePlugin(guildID, pluginName string) error {
|
||||
var enabledPlugins StringSlice
|
||||
err := db.QueryRow("SELECT enabled_plugins FROM guilds WHERE id = ?", guildID).Scan(&enabledPlugins)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if slices.Contains(enabledPlugins, pluginName) {
|
||||
return fmt.Errorf("y: ploogin %q is already enabled", pluginName)
|
||||
}
|
||||
enabledPlugins = append(enabledPlugins, pluginName)
|
||||
|
||||
_, err = db.Exec("UPDATE guilds SET enabled_plugins = ? WHERE id = ?", enabledPlugins, guildID)
|
||||
return err
|
||||
}
|
||||
|
||||
func DisablePlugin(guildID, pluginName string) error {
|
||||
var enabledPlugins StringSlice
|
||||
err := db.QueryRow("SELECT enabled_plugins FROM guilds WHERE id = ?", guildID).Scan(&enabledPlugins)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if i := slices.Index(enabledPlugins, pluginName); i == -1 {
|
||||
return fmt.Errorf("ploogin %q is already disabled", pluginName)
|
||||
} else {
|
||||
enabledPlugins = append(enabledPlugins[:i], enabledPlugins[i+1:]...)
|
||||
}
|
||||
|
||||
_, err = db.Exec("UPDATE guilds SET enabled_plugins = ? WHERE id = ?", enabledPlugins, guildID)
|
||||
return err
|
||||
}
|
||||
|
||||
func IsVettingMsg(msgID string) (bool, error) {
|
||||
var out bool
|
||||
err := db.QueryRow("SELECT 1 FROM guild WHERE vetting_msg_id = ?", msgID).Scan(&out)
|
||||
|
||||
6
internal/db/migrations/2023120500.sql
Normal file
6
internal/db/migrations/2023120500.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
/* add time format column */
|
||||
ALTER TABLE guilds ADD COLUMN time_format TEXT NOT NULL DEFAULT 'discord';
|
||||
|
||||
/* add welcome message columns */
|
||||
ALTER TABLE guilds ADD COLUMN welcome_chan_id TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE guilds ADD COLUMN welcome_msg TEXT NOT NULL DEFAULT '';
|
||||
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');
|
||||
1
internal/db/migrations/2023122700.sql
Normal file
1
internal/db/migrations/2023122700.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE reactions ADD COLUMN excluded_channels TEXT NOT NULL DEFAULT '';
|
||||
11
internal/db/migrations/2024020300.sql
Normal file
11
internal/db/migrations/2024020300.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
/* plugins stores information about all the plugins defined for this bot. */
|
||||
/* This will be used to let plugins perform actions when they're updated */
|
||||
CREATE TABLE plugins (
|
||||
name TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
UNIQUE(name) ON CONFLICT REPLACE
|
||||
);
|
||||
|
||||
/* Add a column to allow guilds to enable whichever plugins they want */
|
||||
ALTER TABLE guilds ADD COLUMN enabled_plugins TEXT NOT NULL DEFAULT '';
|
||||
42
internal/db/plugins.go
Normal file
42
internal/db/plugins.go
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 db
|
||||
|
||||
type PluginInfo struct {
|
||||
Name string `db:"name"`
|
||||
Version string `db:"version"`
|
||||
Desc string `db:"description"`
|
||||
}
|
||||
|
||||
func (pi PluginInfo) IsValid() bool {
|
||||
if pi.Name == "" || pi.Version == "" || pi.Desc == "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func AddPlugin(pi PluginInfo) error {
|
||||
_, err := db.NamedExec(`INSERT OR REPLACE INTO plugins VALUES (:name, :version, :description)`, pi)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetPlugin(name string) (out PluginInfo, err error) {
|
||||
err = db.QueryRowx("SELECT * FROM plugins WHERE name = ? LIMIT 1", name).StructScan(&out)
|
||||
return
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -25,12 +25,12 @@ import (
|
||||
)
|
||||
|
||||
type Poll struct {
|
||||
MsgID string
|
||||
OwnerID string
|
||||
Title string
|
||||
Finished bool
|
||||
OptionEmojis []string
|
||||
OptionText []string
|
||||
MsgID string `db:"msg_id"`
|
||||
OwnerID string `db:"owner_id"`
|
||||
Title string `db:"title"`
|
||||
Finished bool `db:"finished"`
|
||||
OptionEmojis StringSlice `db:"opt_emojis"`
|
||||
OptionText StringSlice `db:"opt_text"`
|
||||
}
|
||||
|
||||
func CreatePoll(msgID, ownerID, title string) error {
|
||||
@@ -39,50 +39,47 @@ func CreatePoll(msgID, ownerID, title string) error {
|
||||
}
|
||||
|
||||
func GetPoll(msgID string) (*Poll, error) {
|
||||
var title, ownerID, emojis, text string
|
||||
var finished bool
|
||||
err := db.QueryRow("SELECT title, owner_id, finished, opt_emojis, opt_text FROM polls WHERE msg_id = ?", msgID).Scan(&title, &ownerID, &finished, &emojis, &text)
|
||||
out := &Poll{}
|
||||
err := db.QueryRowx("SELECT * FROM polls WHERE msg_id = ?", msgID).StructScan(out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Poll{
|
||||
MsgID: msgID,
|
||||
OwnerID: ownerID,
|
||||
Title: title,
|
||||
Finished: finished,
|
||||
OptionEmojis: splitOptions(emojis),
|
||||
OptionText: splitOptions(text),
|
||||
}, nil
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func AddPollOptionText(msgID string, text string) error {
|
||||
var optText string
|
||||
if strings.Contains(text, "\x1F") {
|
||||
return errors.New("option string cannot contain unit separator")
|
||||
}
|
||||
|
||||
var optText StringSlice
|
||||
err := db.QueryRow("SELECT opt_text FROM polls WHERE msg_id = ?", msgID).Scan(&optText)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
optText = append(optText, text)
|
||||
|
||||
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 = ?", optText, msgID)
|
||||
return err
|
||||
}
|
||||
|
||||
func AddPollOptionEmoji(msgID string, emoji string) error {
|
||||
var optEmojis string
|
||||
if strings.Contains(emoji, "\x1F") {
|
||||
return errors.New("emoji string cannot contain unit separator")
|
||||
}
|
||||
|
||||
var optEmojis StringSlice
|
||||
err := db.QueryRow("SELECT opt_emojis FROM polls WHERE msg_id = ?", msgID).Scan(&optEmojis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
splitEmojis := splitOptions(optEmojis)
|
||||
if slices.Contains(splitEmojis, emoji) {
|
||||
if slices.Contains(optEmojis, emoji) {
|
||||
return errors.New("emojis can only be used once")
|
||||
}
|
||||
splitEmojis = append(splitEmojis, emoji)
|
||||
optEmojis = append(optEmojis, 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 = ?", optEmojis, msgID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -114,10 +111,3 @@ func VoteAmount(msgID string, option int) (int64, error) {
|
||||
err := db.QueryRow("SELECT COUNT(1) FROM votes WHERE poll_msg_id = ? AND option = ?", msgID, option).Scan(&out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
func splitOptions(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(s, ",")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -33,22 +33,23 @@ const (
|
||||
)
|
||||
|
||||
type Reaction struct {
|
||||
GuildID string `db:"guild_id"`
|
||||
MatchType MatchType `db:"match_type"`
|
||||
Match string `db:"match"`
|
||||
ReactionType ReactionType `db:"reaction_type"`
|
||||
Reaction string `db:"reaction"`
|
||||
Chance int `db:"chance"`
|
||||
GuildID string `db:"guild_id"`
|
||||
MatchType MatchType `db:"match_type"`
|
||||
Match string `db:"match"`
|
||||
ReactionType ReactionType `db:"reaction_type"`
|
||||
Reaction StringSlice `db:"reaction"`
|
||||
Chance int `db:"chance"`
|
||||
ExcludedChannels StringSlice `db:"excluded_channels"`
|
||||
}
|
||||
|
||||
func AddReaction(guildID string, r Reaction) error {
|
||||
r.GuildID = guildID
|
||||
_, err := db.NamedExec("INSERT INTO reactions VALUES (:guild_id, :match_type, :match, :reaction_type, :reaction, :chance)", r)
|
||||
_, err := db.NamedExec("INSERT INTO reactions VALUES (:guild_id, :match_type, :match, :reaction_type, :reaction, :chance, :excluded_channels)", r)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteReaction(guildID string, match string) error {
|
||||
_, err := db.Exec("DELETE FROM reactions WHERE guild_id = ? AND match = ? LIMIT 1", guildID, match)
|
||||
_, err := db.Exec("DELETE FROM reactions WHERE guild_id = ? AND match = ?", guildID, match)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -56,3 +57,21 @@ func Reactions(guildID string) (rs []Reaction, err error) {
|
||||
err = db.Select(&rs, "SELECT * FROM reactions WHERE guild_id = ?", guildID)
|
||||
return rs, err
|
||||
}
|
||||
|
||||
func ReactionsExclude(guildID, match, channelID string) (err error) {
|
||||
if match == "" {
|
||||
_, err = db.Exec("UPDATE reactions SET excluded_channels = trim(excluded_channels || X'1F' || ?, X'1F') WHERE guild_id = ?", channelID, guildID)
|
||||
} else {
|
||||
_, err = db.Exec("UPDATE reactions SET excluded_channels = trim(excluded_channels || X'1F' || ?, X'1F') WHERE guild_id = ? AND match = ?", channelID, guildID, match)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ReactionsUnexclude(guildID, match, channelID string) (err error) {
|
||||
if match == "" {
|
||||
_, err = db.Exec("UPDATE reactions SET excluded_channels = trim(replace(replace(excluded_channels, ?, ''), X'1F1F', X'1F'), X'1F') WHERE guild_id = ?", channelID, guildID)
|
||||
} else {
|
||||
_, err = db.Exec("UPDATE reactions SET excluded_channels = trim(replace(replace(excluded_channels, ?, ''), X'1F1F', X'1F'), X'1F') WHERE guild_id = ? AND match = ?", channelID, guildID, match)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,6 +19,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@@ -26,12 +27,12 @@ import (
|
||||
)
|
||||
|
||||
type ReactionRoleCategory struct {
|
||||
MsgID string `db:"msg_id"`
|
||||
ChannelID string `db:"channel_id"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
Emoji []string `db:"emoji"`
|
||||
Roles []string `db:"roles"`
|
||||
MsgID string `db:"msg_id"`
|
||||
ChannelID string `db:"channel_id"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
Emoji StringSlice `db:"emoji"`
|
||||
Roles StringSlice `db:"roles"`
|
||||
}
|
||||
|
||||
func AddReactionRoleCategory(channelID string, rrc ReactionRoleCategory) error {
|
||||
@@ -41,31 +42,16 @@ func AddReactionRoleCategory(channelID string, rrc ReactionRoleCategory) error {
|
||||
channelID,
|
||||
rrc.Name,
|
||||
rrc.Description,
|
||||
strings.Join(rrc.Emoji, ","),
|
||||
strings.Join(rrc.Roles, ","),
|
||||
rrc.Emoji,
|
||||
rrc.Roles,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetReactionRoleCategory(channelID, name string) (*ReactionRoleCategory, error) {
|
||||
var msgID, description, emoji, roles string
|
||||
err := db.QueryRow(
|
||||
"SELECT msg_id, description, emoji, roles FROM reaction_role_categories WHERE channel_id = ? AND name = ?",
|
||||
channelID,
|
||||
name,
|
||||
).Scan(&msgID, &description, &emoji, &roles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ReactionRoleCategory{
|
||||
MsgID: msgID,
|
||||
ChannelID: channelID,
|
||||
Name: name,
|
||||
Description: description,
|
||||
Emoji: splitOptions(emoji),
|
||||
Roles: splitOptions(roles),
|
||||
}, nil
|
||||
out := &ReactionRoleCategory{}
|
||||
err := db.QueryRowx("SELECT * FROM reaction_role_categories WHERE channel_id = ? AND name = ?", channelID, name).StructScan(out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
func DeleteReactionRoleCategory(channelID, name string) error {
|
||||
@@ -73,21 +59,24 @@ func DeleteReactionRoleCategory(channelID, name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func AddReactionRole(channelID, category, emoji string, role *discordgo.Role) error {
|
||||
var oldEmoji, oldRoles string
|
||||
err := db.QueryRow("SELECT emoji, roles FROM reaction_role_categories WHERE name = ? AND channel_id = ?", category, channelID).Scan(&oldEmoji, &oldRoles)
|
||||
func AddReactionRole(channelID, category, emojiStr string, role *discordgo.Role) error {
|
||||
if strings.Contains(category, "\x1F") || strings.Contains(emojiStr, "\x1F") {
|
||||
return errors.New("reaction roles cannot contain unit separator")
|
||||
}
|
||||
|
||||
var emoji, roles StringSlice
|
||||
err := db.QueryRow("SELECT emoji, roles FROM reaction_role_categories WHERE name = ? AND channel_id = ?", category, channelID).Scan(&emoji, &roles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
splitEmoji, splitRoles := splitOptions(oldEmoji), splitOptions(oldRoles)
|
||||
splitEmoji = append(splitEmoji, strings.TrimSpace(emoji))
|
||||
splitRoles = append(splitRoles, role.ID)
|
||||
emoji = append(emoji, strings.TrimSpace(emojiStr))
|
||||
roles = append(roles, role.ID)
|
||||
|
||||
_, err = db.Exec(
|
||||
"UPDATE reaction_role_categories SET emoji = ?, roles = ? WHERE name = ? AND channel_id = ?",
|
||||
strings.Join(splitEmoji, ","),
|
||||
strings.Join(splitRoles, ","),
|
||||
emoji,
|
||||
roles,
|
||||
category,
|
||||
channelID,
|
||||
)
|
||||
@@ -95,24 +84,23 @@ func AddReactionRole(channelID, category, emoji string, role *discordgo.Role) er
|
||||
}
|
||||
|
||||
func DeleteReactionRole(channelID, category string, role *discordgo.Role) error {
|
||||
var oldEmoji, oldRoles string
|
||||
err := db.QueryRow("SELECT emoji, roles FROM reaction_role_categories WHERE name = ? AND channel_id = ?", category, channelID).Scan(&oldEmoji, &oldRoles)
|
||||
var emoji, roles StringSlice
|
||||
err := db.QueryRow("SELECT emoji, roles FROM reaction_role_categories WHERE name = ? AND channel_id = ?", category, channelID).Scan(&emoji, &roles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
splitEmoji, splitRoles := splitOptions(oldEmoji), splitOptions(oldRoles)
|
||||
if i := slices.Index(splitRoles, role.ID); i == -1 {
|
||||
if i := slices.Index(roles, role.ID); i == -1 {
|
||||
return nil
|
||||
} else {
|
||||
splitEmoji = append(splitEmoji[:i], splitEmoji[i+1:]...)
|
||||
splitRoles = append(splitRoles[:i], splitRoles[i+1:]...)
|
||||
emoji = append(emoji[:i], emoji[i+1:]...)
|
||||
roles = append(roles[:i], roles[i+1:]...)
|
||||
}
|
||||
|
||||
_, err = db.Exec(
|
||||
"UPDATE reaction_role_categories SET emoji = ?, roles = ? WHERE name = ? AND channel_id = ?",
|
||||
strings.Join(splitEmoji, ","),
|
||||
strings.Join(splitRoles, ","),
|
||||
emoji,
|
||||
roles,
|
||||
category,
|
||||
channelID,
|
||||
)
|
||||
|
||||
33
internal/db/slice.go
Normal file
33
internal/db/slice.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type StringSlice []string
|
||||
|
||||
func (s StringSlice) String() string {
|
||||
return strings.Join(s, ", ")
|
||||
}
|
||||
|
||||
func (s StringSlice) Value() (driver.Value, error) {
|
||||
return strings.Join(s, "\x1F"), nil
|
||||
}
|
||||
|
||||
func (s *StringSlice) Scan(value any) error {
|
||||
str, ok := value.(string)
|
||||
if !ok {
|
||||
return errors.New("incompatible type for StringSlice")
|
||||
}
|
||||
*s = splitOptions(str)
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitOptions(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(s, "\x1F")
|
||||
}
|
||||
156
internal/db/sqltabler/sqltabler.go
Normal file
156
internal/db/sqltabler/sqltabler.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package sqltabler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
sqlparser "github.com/rqlite/sql"
|
||||
)
|
||||
|
||||
// Modify adds a prefix and suffix to every table name found in stmt.
|
||||
func Modify(stmt, prefix, suffix string) (string, error) {
|
||||
parser := sqlparser.NewParser(strings.NewReader(stmt))
|
||||
sb := strings.Builder{}
|
||||
for {
|
||||
s, err := parser.ParseStatement()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
modify(s, prefix, suffix)
|
||||
sb.WriteString(s.String())
|
||||
sb.WriteByte(';')
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// modify changes all the table, view, trigger, and index names in a single statement
|
||||
func modify(stmt any, prefix, suffix string) {
|
||||
switch stmt := stmt.(type) {
|
||||
case *sqlparser.SelectStatement:
|
||||
modifySource(stmt.Source, prefix, suffix)
|
||||
modify(stmt.WhereExpr, prefix, suffix)
|
||||
case *sqlparser.InsertStatement:
|
||||
stmt.Table.Name = prefix + stmt.Table.Name + suffix
|
||||
if stmt.Select != nil {
|
||||
modify(stmt.Select, prefix, suffix)
|
||||
}
|
||||
case *sqlparser.UpdateStatement:
|
||||
stmt.Table.Name.Name = prefix + stmt.Table.Name.Name + suffix
|
||||
for _, assignment := range stmt.Assignments {
|
||||
modify(assignment, prefix, suffix)
|
||||
}
|
||||
case *sqlparser.CreateTableStatement:
|
||||
stmt.Name.Name = prefix + stmt.Name.Name + suffix
|
||||
if stmt.Select != nil {
|
||||
modify(stmt.Select, prefix, suffix)
|
||||
}
|
||||
for _, col := range stmt.Columns {
|
||||
modify(col, prefix, suffix)
|
||||
}
|
||||
for _, constraint := range stmt.Constraints {
|
||||
modify(constraint, prefix, suffix)
|
||||
}
|
||||
case *sqlparser.CreateViewStatement:
|
||||
stmt.Name.Name = prefix + stmt.Name.Name + suffix
|
||||
if stmt.Select != nil {
|
||||
modify(stmt.Select, prefix, suffix)
|
||||
}
|
||||
case *sqlparser.AlterTableStatement:
|
||||
stmt.Name.Name = prefix + stmt.Name.Name + suffix
|
||||
if stmt.NewName != nil {
|
||||
stmt.NewName.Name = prefix + stmt.NewName.Name + suffix
|
||||
}
|
||||
if stmt.ColumnDef != nil {
|
||||
modify(stmt.ColumnDef, prefix, suffix)
|
||||
}
|
||||
case *sqlparser.Call:
|
||||
for _, arg := range stmt.Args {
|
||||
modify(arg, prefix, suffix)
|
||||
}
|
||||
case *sqlparser.FilterClause:
|
||||
modify(stmt.X, prefix, suffix)
|
||||
case *sqlparser.DeleteStatement:
|
||||
stmt.Table.Name.Name = prefix + stmt.Table.Name.Name + suffix
|
||||
if stmt.WhereExpr != nil {
|
||||
modify(stmt.WhereExpr, prefix, suffix)
|
||||
}
|
||||
case *sqlparser.AnalyzeStatement:
|
||||
stmt.Name.Name = prefix + stmt.Name.Name + suffix
|
||||
case *sqlparser.ExplainStatement:
|
||||
modify(stmt.Stmt, prefix, suffix)
|
||||
case *sqlparser.CreateIndexStatement:
|
||||
stmt.Name.Name = prefix + stmt.Name.Name + suffix
|
||||
stmt.Table.Name = prefix + stmt.Table.Name + suffix
|
||||
case *sqlparser.CreateTriggerStatement:
|
||||
stmt.Name.Name = prefix + stmt.Name.Name + suffix
|
||||
stmt.Table.Name = prefix + stmt.Table.Name + suffix
|
||||
if stmt.WhenExpr != nil {
|
||||
modify(stmt.WhenExpr, prefix, suffix)
|
||||
}
|
||||
for _, istmt := range stmt.Body {
|
||||
modify(istmt, prefix, suffix)
|
||||
}
|
||||
case *sqlparser.CTE:
|
||||
stmt.TableName.Name = prefix + stmt.TableName.Name + suffix
|
||||
if stmt.Select != nil {
|
||||
modify(stmt.Select, prefix, suffix)
|
||||
}
|
||||
case *sqlparser.DropTableStatement:
|
||||
stmt.Name.Name = prefix + stmt.Name.Name + suffix
|
||||
case *sqlparser.DropViewStatement:
|
||||
stmt.Name.Name = prefix + stmt.Name.Name + suffix
|
||||
case *sqlparser.DropIndexStatement:
|
||||
stmt.Name.Name = prefix + stmt.Name.Name + suffix
|
||||
case *sqlparser.DropTriggerStatement:
|
||||
stmt.Name.Name = prefix + stmt.Name.Name + suffix
|
||||
case *sqlparser.ForeignKeyConstraint:
|
||||
stmt.ForeignTable.Name = prefix + stmt.ForeignTable.Name + suffix
|
||||
case *sqlparser.OnConstraint:
|
||||
modify(stmt.X, prefix, suffix)
|
||||
case *sqlparser.ExprList:
|
||||
for _, expr := range stmt.Exprs {
|
||||
modify(expr, prefix, suffix)
|
||||
}
|
||||
case *sqlparser.UnaryExpr:
|
||||
modify(stmt.X, prefix, suffix)
|
||||
case *sqlparser.BinaryExpr:
|
||||
modify(stmt.X, prefix, suffix)
|
||||
modify(stmt.Y, prefix, suffix)
|
||||
case *sqlparser.ParenExpr:
|
||||
modify(stmt.X, prefix, suffix)
|
||||
case *sqlparser.CastExpr:
|
||||
modify(stmt.X, prefix, suffix)
|
||||
case *sqlparser.OrderingTerm:
|
||||
modify(stmt.X, prefix, suffix)
|
||||
case *sqlparser.Assignment:
|
||||
modify(stmt.Expr, prefix, suffix)
|
||||
case *sqlparser.ColumnDefinition:
|
||||
for _, constraint := range stmt.Constraints {
|
||||
modify(constraint, prefix, suffix)
|
||||
}
|
||||
case *sqlparser.QualifiedRef:
|
||||
if stmt.Table != nil {
|
||||
stmt.Table.Name = prefix + stmt.Table.Name + suffix
|
||||
}
|
||||
case *sqlparser.CaseExpr:
|
||||
modify(stmt.ElseExpr, prefix, suffix)
|
||||
for _, block := range stmt.Blocks {
|
||||
modify(block.Condition, prefix, suffix)
|
||||
modify(block.Body, prefix, suffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func modifySource(source sqlparser.Source, prefix, suffix string) {
|
||||
switch source := source.(type) {
|
||||
case *sqlparser.QualifiedTableName:
|
||||
source.Name.Name = prefix + source.Name.Name + suffix
|
||||
case *sqlparser.JoinClause:
|
||||
modifySource(source.X, prefix, suffix)
|
||||
modifySource(source.Y, prefix, suffix)
|
||||
modify(source.Constraint, prefix, suffix)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,11 +19,11 @@
|
||||
package db
|
||||
|
||||
func AddVettingReq(guildID, userID, msgID string) error {
|
||||
_, err := db.Exec("INSERT OR ABORT INTO vetting_requests (guild_id, user_id, msg_id) VALUES (?, ?, ?)", guildID, userID, msgID)
|
||||
_, err := db.Exec("INSERT INTO vetting_requests (guild_id, user_id, msg_id) VALUES (?, ?, ?)", guildID, userID, msgID)
|
||||
return err
|
||||
}
|
||||
|
||||
func VettingReqID(guildID, userID string) (string, error) {
|
||||
func VettingReqMsgID(guildID, userID string) (string, error) {
|
||||
var out string
|
||||
row := db.QueryRowx("SELECT msg_id FROM vetting_requests WHERE user_id = ? AND guild_id = ?", userID, guildID)
|
||||
err := row.Scan(&out)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -60,6 +60,11 @@ var (
|
||||
Hi: 0x1F9FF,
|
||||
Stride: 1,
|
||||
},
|
||||
{ // Symbols and Pictographs Extended-A
|
||||
Lo: 0x1FA70,
|
||||
Hi: 0x1FAFF,
|
||||
Stride: 1,
|
||||
},
|
||||
},
|
||||
R16: []unicode.Range16{
|
||||
{ // Zero-width characters
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
||||
68
internal/systems/about/about.go
Normal file
68
internal/systems/about/about.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package about
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/owobot/internal/systems/commands"
|
||||
)
|
||||
|
||||
const aboutTmpl = `**Copyright © %d owobot contributors**
|
||||
|
||||
This program comes with **ABSOLUTELY NO WARRANTY**. This is free software, and you are welcome to redistribute it under certain conditions. See [here](https://www.gnu.org/licenses/agpl-3.0.html) for details.
|
||||
|
||||
**Running Commit:**
|
||||
%s
|
||||
|
||||
**Source Code:**
|
||||
https://gitea.elara.ws/owobot/owobot
|
||||
**GitHub Mirror:**
|
||||
https://github.com/owobot-org/owobot`
|
||||
|
||||
func Init(s *discordgo.Session) error {
|
||||
commands.Register(s, aboutCmd, &discordgo.ApplicationCommand{
|
||||
Name: "about",
|
||||
Description: "Information about owobot",
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func aboutCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
return s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
Embeds: []*discordgo.MessageEmbed{{
|
||||
Title: "About owobot",
|
||||
Description: fmt.Sprintf(
|
||||
aboutTmpl,
|
||||
time.Now().Year(),
|
||||
getCommit(),
|
||||
),
|
||||
}},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func getCommit() string {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return "`<unknown>`"
|
||||
}
|
||||
|
||||
commit := "`<unknown>`"
|
||||
for _, setting := range info.Settings {
|
||||
switch setting.Key {
|
||||
case "vcs.revision":
|
||||
commit = "`" + setting.Value + "`"
|
||||
case "vcs.modified":
|
||||
if setting.Value == "true" {
|
||||
commit += " (modified)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commit
|
||||
}
|
||||
41
internal/systems/commands/handlers.go
Normal file
41
internal/systems/commands/handlers.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -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 {
|
||||
66
internal/systems/eventlog/commands.go
Normal file
66
internal/systems/eventlog/commands.go
Normal file
@@ -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!")
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,9 +19,7 @@
|
||||
package eventlog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
@@ -63,50 +61,26 @@ func Init(s *discordgo.Session) error {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "time_format",
|
||||
Description: "Set the time format for embeds",
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "format",
|
||||
Description: "The time format to use",
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
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)
|
||||
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))
|
||||
}
|
||||
|
||||
// Entry represents an entry in the event log
|
||||
type Entry struct {
|
||||
Title string
|
||||
Description string
|
||||
@@ -114,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 {
|
||||
@@ -127,9 +102,6 @@ func Log(s *discordgo.Session, guildID string, e Entry) error {
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: e.Title,
|
||||
Description: e.Description,
|
||||
Footer: &discordgo.MessageEmbedFooter{
|
||||
Text: util.FormatJucheTime(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
if e.Author != nil {
|
||||
@@ -143,10 +115,13 @@ func Log(s *discordgo.Session, guildID string, e Entry) error {
|
||||
embed.Image = &discordgo.MessageEmbedImage{URL: e.ImageURL}
|
||||
}
|
||||
|
||||
AddTimeToEmbed(guild.TimeFormat, embed)
|
||||
|
||||
_, err = s.ChannelMessageSendEmbed(guild.LogChanID, embed)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
38
internal/systems/eventlog/time.go
Normal file
38
internal/systems/eventlog/time.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package eventlog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/lestrrat-go/strftime"
|
||||
)
|
||||
|
||||
// AddTimeToEmbed formats the current time using timeFmt and adds it to e.
|
||||
// The timeFmt can either be "discord", "juche", or a strftime string.
|
||||
func AddTimeToEmbed(timeFmt string, e *discordgo.MessageEmbed) *discordgo.MessageEmbed {
|
||||
t := time.Now().In(time.UTC)
|
||||
switch timeFmt {
|
||||
case "discord":
|
||||
e.Timestamp = t.Format(time.RFC3339)
|
||||
case "juche":
|
||||
e.Footer = &discordgo.MessageEmbedFooter{Text: formatJuche(t)}
|
||||
default:
|
||||
e.Footer = &discordgo.MessageEmbedFooter{Text: format(timeFmt, t)}
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// formatJuche formats the given time in Juche calendar format
|
||||
func formatJuche(t time.Time) string {
|
||||
return fmt.Sprintf("%02d:%02d %02d-%02d Juche %d", t.Hour(), t.Minute(), t.Day(), t.Month(), t.Year()-1911)
|
||||
}
|
||||
|
||||
// format formats t using timeFmt
|
||||
func format(timeFmt string, t time.Time) string {
|
||||
timeStr, err := strftime.Format(timeFmt, t)
|
||||
if err != nil {
|
||||
return "ERROR: " + err.Error()
|
||||
}
|
||||
return timeStr
|
||||
}
|
||||
17
internal/systems/guilds/handlers.go
Normal file
17
internal/systems/guilds/handlers.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -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)
|
||||
191
internal/systems/members/handlers.go
Normal file
191
internal/systems/members/handlers.go
Normal file
@@ -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
|
||||
}
|
||||
|
||||
100
internal/systems/plugins/api.go
Normal file
100
internal/systems/plugins/api.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/dop251/goja_nodejs/eventloop"
|
||||
"go.elara.ws/logger/log"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
// Plugins is a list of plugins
|
||||
var Plugins []Plugin
|
||||
|
||||
// Plugin represents an owobot plugin
|
||||
type Plugin struct {
|
||||
Info db.PluginInfo
|
||||
Commands []Command
|
||||
Loop *eventloop.EventLoop
|
||||
api *owobotAPI
|
||||
}
|
||||
|
||||
// Command represents a plugin command
|
||||
type Command struct {
|
||||
Name string
|
||||
Desc string
|
||||
Usage goja.Value
|
||||
OnExec goja.Value
|
||||
Permissions []int64
|
||||
Subcommands []Command
|
||||
}
|
||||
|
||||
func (c Command) usage() string {
|
||||
if c.Usage == nil {
|
||||
return ""
|
||||
} else {
|
||||
return c.Usage.String()
|
||||
}
|
||||
}
|
||||
|
||||
type owobotAPI struct {
|
||||
PluginInfo db.PluginInfo
|
||||
Init goja.Value
|
||||
OnEnable goja.Value
|
||||
OnDisable goja.Value
|
||||
Commands []Command
|
||||
|
||||
path string
|
||||
loop *eventloop.EventLoop
|
||||
}
|
||||
|
||||
func (oa *owobotAPI) Enabled(guildID string) bool {
|
||||
return pluginEnabled(guildID, oa.PluginInfo.Name)
|
||||
}
|
||||
|
||||
func (oa *owobotAPI) Respond(s *discordgo.Session, i *discordgo.Interaction, content string) error {
|
||||
return util.Respond(s, i, content)
|
||||
}
|
||||
|
||||
func (oa *owobotAPI) RespondEphemeral(s *discordgo.Session, i *discordgo.Interaction, content string) error {
|
||||
return util.RespondEphemeral(s, i, content)
|
||||
}
|
||||
|
||||
// On adds an event handler function for the given event type
|
||||
func (oa *owobotAPI) On(eventType string, fn goja.Value) {
|
||||
if !oa.PluginInfo.IsValid() {
|
||||
log.Warn("No plugin information provided, ignoring handler registration.").Str("path", oa.path).Send()
|
||||
return
|
||||
}
|
||||
|
||||
callable, ok := goja.AssertFunction(fn)
|
||||
if !ok {
|
||||
log.Warn("Value passed to handler registrar is not a function, ignoring.").
|
||||
Str("plugin", oa.PluginInfo.Name).
|
||||
Str("event-type", eventType).
|
||||
Send()
|
||||
return
|
||||
}
|
||||
|
||||
handlersMtx.Lock()
|
||||
defer handlersMtx.Unlock()
|
||||
|
||||
oa.loop.RunOnLoop(func(vm *goja.Runtime) {
|
||||
this := vm.ToValue(oa)
|
||||
|
||||
handlerMap[eventType] = append(handlerMap[eventType], Handler{
|
||||
PluginName: oa.PluginInfo.Name,
|
||||
Func: func(s *discordgo.Session, data any) {
|
||||
_, err := callable(this, vm.ToValue(s), vm.ToValue(data))
|
||||
if err != nil {
|
||||
log.Error("Exception thrown in plugin function").
|
||||
Str("plugin", oa.PluginInfo.Name).
|
||||
Str("event-type", eventType).
|
||||
Err(err).
|
||||
Send()
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
346
internal/systems/plugins/builtins/discord.go
Normal file
346
internal/systems/plugins/builtins/discord.go
Normal file
@@ -0,0 +1,346 @@
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
var Constants = map[string]any{
|
||||
"Permissions": map[string]int64{
|
||||
"ReadMessages": discordgo.PermissionViewChannel,
|
||||
"SendMessages": discordgo.PermissionSendMessages,
|
||||
"SendTTSMessages": discordgo.PermissionSendTTSMessages,
|
||||
"ManageMessages": discordgo.PermissionManageMessages,
|
||||
"EmbedLinks": discordgo.PermissionEmbedLinks,
|
||||
"AttachFiles": discordgo.PermissionAttachFiles,
|
||||
"ReadMessageHistory": discordgo.PermissionReadMessageHistory,
|
||||
"MentionEveryone": discordgo.PermissionMentionEveryone,
|
||||
"UseExternalEmojis": discordgo.PermissionUseExternalEmojis,
|
||||
"UseSlashCommands": discordgo.PermissionUseSlashCommands,
|
||||
"ManageThreads": discordgo.PermissionManageThreads,
|
||||
"CreatePublicThreads": discordgo.PermissionCreatePublicThreads,
|
||||
"CreatePrivateThreads": discordgo.PermissionCreatePrivateThreads,
|
||||
"UseExternalStickers": discordgo.PermissionUseExternalStickers,
|
||||
"SendMessagesInThreads": discordgo.PermissionSendMessagesInThreads,
|
||||
"VoicePrioritySpeaker": discordgo.PermissionVoicePrioritySpeaker,
|
||||
"VoiceStreamVideo": discordgo.PermissionVoiceStreamVideo,
|
||||
"VoiceConnect": discordgo.PermissionVoiceConnect,
|
||||
"VoiceSpeak": discordgo.PermissionVoiceSpeak,
|
||||
"VoiceMuteMembers": discordgo.PermissionVoiceMuteMembers,
|
||||
"VoiceDeafenMembers": discordgo.PermissionVoiceDeafenMembers,
|
||||
"VoiceMoveMembers": discordgo.PermissionVoiceMoveMembers,
|
||||
"VoiceUseVAD": discordgo.PermissionVoiceUseVAD,
|
||||
"VoiceRequestToSpeak": discordgo.PermissionVoiceRequestToSpeak,
|
||||
"UseActivities": discordgo.PermissionUseActivities,
|
||||
"ChangeNickname": discordgo.PermissionChangeNickname,
|
||||
"ManageNicknames": discordgo.PermissionManageNicknames,
|
||||
"ManageRoles": discordgo.PermissionManageRoles,
|
||||
"ManageWebhooks": discordgo.PermissionManageWebhooks,
|
||||
"ManageEmojis": discordgo.PermissionManageEmojis,
|
||||
"ManageEvents": discordgo.PermissionManageEvents,
|
||||
"CreateInstantInvite": discordgo.PermissionCreateInstantInvite,
|
||||
"KickMembers": discordgo.PermissionKickMembers,
|
||||
"BanMembers": discordgo.PermissionBanMembers,
|
||||
"Administrator": discordgo.PermissionAdministrator,
|
||||
"ManageChannels": discordgo.PermissionManageChannels,
|
||||
"ManageServer": discordgo.PermissionManageServer,
|
||||
"AddReactions": discordgo.PermissionAddReactions,
|
||||
"ViewAuditLogs": discordgo.PermissionViewAuditLogs,
|
||||
"ViewChannel": discordgo.PermissionViewChannel,
|
||||
"ViewGuildInsights": discordgo.PermissionViewGuildInsights,
|
||||
"ModerateMembers": discordgo.PermissionModerateMembers,
|
||||
"AllText": discordgo.PermissionAllText,
|
||||
"AllVoice": discordgo.PermissionAllVoice,
|
||||
"AllChannel": discordgo.PermissionAllChannel,
|
||||
"All": discordgo.PermissionAll,
|
||||
},
|
||||
"MessageFlag": map[string]discordgo.MessageFlags{
|
||||
"CrossPosted": discordgo.MessageFlagsCrossPosted,
|
||||
"IsCrossPosted": discordgo.MessageFlagsIsCrossPosted,
|
||||
"SuppressEmbeds": discordgo.MessageFlagsSuppressEmbeds,
|
||||
"SupressEmbeds": discordgo.MessageFlagsSupressEmbeds,
|
||||
"SourceMessageDeleted": discordgo.MessageFlagsSourceMessageDeleted,
|
||||
"Urgent": discordgo.MessageFlagsUrgent,
|
||||
"HasThread": discordgo.MessageFlagsHasThread,
|
||||
"Ephemeral": discordgo.MessageFlagsEphemeral,
|
||||
"Loading": discordgo.MessageFlagsLoading,
|
||||
"FailedToMentionSomeRolesInThread": discordgo.MessageFlagsFailedToMentionSomeRolesInThread,
|
||||
"SuppressNotifications": discordgo.MessageFlagsSuppressNotifications,
|
||||
"IsVoiceMessage": discordgo.MessageFlagsIsVoiceMessage,
|
||||
},
|
||||
"MessageType": map[string]discordgo.MessageType{
|
||||
"Default": discordgo.MessageTypeDefault,
|
||||
"RecipientAdd": discordgo.MessageTypeRecipientAdd,
|
||||
"RecipientRemove": discordgo.MessageTypeRecipientRemove,
|
||||
"Call": discordgo.MessageTypeCall,
|
||||
"ChannelNameChange": discordgo.MessageTypeChannelNameChange,
|
||||
"ChannelIconChange": discordgo.MessageTypeChannelIconChange,
|
||||
"ChannelPinnedMessage": discordgo.MessageTypeChannelPinnedMessage,
|
||||
"GuildMemberJoin": discordgo.MessageTypeGuildMemberJoin,
|
||||
"UserPremiumGuildSubscription": discordgo.MessageTypeUserPremiumGuildSubscription,
|
||||
"UserPremiumGuildSubscriptionTierOne": discordgo.MessageTypeUserPremiumGuildSubscriptionTierOne,
|
||||
"UserPremiumGuildSubscriptionTierTwo": discordgo.MessageTypeUserPremiumGuildSubscriptionTierTwo,
|
||||
"UserPremiumGuildSubscriptionTierThree": discordgo.MessageTypeUserPremiumGuildSubscriptionTierThree,
|
||||
"ChannelFollowAdd": discordgo.MessageTypeChannelFollowAdd,
|
||||
"GuildDiscoveryDisqualified": discordgo.MessageTypeGuildDiscoveryDisqualified,
|
||||
"GuildDiscoveryRequalified": discordgo.MessageTypeGuildDiscoveryRequalified,
|
||||
"ThreadCreated": discordgo.MessageTypeThreadCreated,
|
||||
"Reply": discordgo.MessageTypeReply,
|
||||
"ChatInputCommand": discordgo.MessageTypeChatInputCommand,
|
||||
"ThreadStarterMessage": discordgo.MessageTypeThreadStarterMessage,
|
||||
"ContextMenuCommand": discordgo.MessageTypeContextMenuCommand,
|
||||
},
|
||||
"Status": map[string]discordgo.Status{
|
||||
"Online": discordgo.StatusOnline,
|
||||
"Idle": discordgo.StatusIdle,
|
||||
"DoNotDisturb": discordgo.StatusDoNotDisturb,
|
||||
"Invisible": discordgo.StatusInvisible,
|
||||
"Offline": discordgo.StatusOffline,
|
||||
},
|
||||
"UserFlags": map[string]discordgo.UserFlags{
|
||||
"DiscordEmployee": discordgo.UserFlagDiscordEmployee,
|
||||
"DiscordPartner": discordgo.UserFlagDiscordPartner,
|
||||
"HypeSquadEvents": discordgo.UserFlagHypeSquadEvents,
|
||||
"BugHunterLevel1": discordgo.UserFlagBugHunterLevel1,
|
||||
"HouseBravery": discordgo.UserFlagHouseBravery,
|
||||
"HouseBrilliance": discordgo.UserFlagHouseBrilliance,
|
||||
"HouseBalance": discordgo.UserFlagHouseBalance,
|
||||
"EarlySupporter": discordgo.UserFlagEarlySupporter,
|
||||
"TeamUser": discordgo.UserFlagTeamUser,
|
||||
"System": discordgo.UserFlagSystem,
|
||||
"BugHunterLevel2": discordgo.UserFlagBugHunterLevel2,
|
||||
"VerifiedBot": discordgo.UserFlagVerifiedBot,
|
||||
"VerifiedBotDeveloper": discordgo.UserFlagVerifiedBotDeveloper,
|
||||
"DiscordCertifiedModerator": discordgo.UserFlagDiscordCertifiedModerator,
|
||||
"BotHTTPInteractions": discordgo.UserFlagBotHTTPInteractions,
|
||||
"ActiveBotDeveloper": discordgo.UserFlagActiveBotDeveloper,
|
||||
},
|
||||
"RoleFlags": map[string]discordgo.RoleFlags{
|
||||
"InPrompt": discordgo.RoleFlagInPrompt,
|
||||
},
|
||||
"SelectMenuType": map[string]discordgo.SelectMenuType{
|
||||
"String": discordgo.StringSelectMenu,
|
||||
"User": discordgo.UserSelectMenu,
|
||||
"Role": discordgo.RoleSelectMenu,
|
||||
"Mentionable": discordgo.MentionableSelectMenu,
|
||||
"Channel": discordgo.ChannelSelectMenu,
|
||||
},
|
||||
"ComponentType": map[string]discordgo.ComponentType{
|
||||
"ActionsRow": discordgo.ActionsRowComponent,
|
||||
"Button": discordgo.ButtonComponent,
|
||||
"SelectMenu": discordgo.SelectMenuComponent,
|
||||
"TextInput": discordgo.TextInputComponent,
|
||||
"UserSelectMenu": discordgo.UserSelectMenuComponent,
|
||||
"RoleSelectMenu": discordgo.RoleSelectMenuComponent,
|
||||
"MentionableSelectMenu": discordgo.MentionableSelectMenuComponent,
|
||||
"ChannelSelectMenu": discordgo.ChannelSelectMenuComponent,
|
||||
},
|
||||
"EmbedType": map[string]discordgo.EmbedType{
|
||||
"Rich": discordgo.EmbedTypeRich,
|
||||
"Image": discordgo.EmbedTypeImage,
|
||||
"Video": discordgo.EmbedTypeVideo,
|
||||
"Gifv": discordgo.EmbedTypeGifv,
|
||||
"Article": discordgo.EmbedTypeArticle,
|
||||
"Link": discordgo.EmbedTypeLink,
|
||||
},
|
||||
"MfaLevel": map[string]discordgo.MfaLevel{
|
||||
"None": discordgo.MfaLevelNone,
|
||||
"Elevated": discordgo.MfaLevelElevated,
|
||||
},
|
||||
"PermissionOverwriteType": map[string]discordgo.PermissionOverwriteType{
|
||||
"Role": discordgo.PermissionOverwriteTypeRole,
|
||||
"Member": discordgo.PermissionOverwriteTypeMember,
|
||||
},
|
||||
"PremiumTier": map[string]discordgo.PremiumTier{
|
||||
"None": discordgo.PremiumTierNone,
|
||||
"Tier1": discordgo.PremiumTier1,
|
||||
"Tier2": discordgo.PremiumTier2,
|
||||
"Tier3": discordgo.PremiumTier3,
|
||||
},
|
||||
"SelectMenuDefaultValueType": map[string]discordgo.SelectMenuDefaultValueType{
|
||||
"User": discordgo.SelectMenuDefaultValueUser,
|
||||
"Role": discordgo.SelectMenuDefaultValueRole,
|
||||
"Channel": discordgo.SelectMenuDefaultValueChannel,
|
||||
},
|
||||
"StageInstancePrivacyLevel": map[string]discordgo.StageInstancePrivacyLevel{
|
||||
"Public": discordgo.StageInstancePrivacyLevelPublic,
|
||||
"GuildOnly": discordgo.StageInstancePrivacyLevelGuildOnly,
|
||||
},
|
||||
"StickerFormat": map[string]discordgo.StickerFormat{
|
||||
"PNG": discordgo.StickerFormatTypePNG,
|
||||
"APNG": discordgo.StickerFormatTypeAPNG,
|
||||
"Lottie": discordgo.StickerFormatTypeLottie,
|
||||
"GIF": discordgo.StickerFormatTypeGIF,
|
||||
},
|
||||
"StickerType": map[string]discordgo.StickerType{
|
||||
"Standard": discordgo.StickerTypeStandard,
|
||||
"Guild": discordgo.StickerTypeGuild,
|
||||
},
|
||||
"ExpireBehavior": map[string]discordgo.ExpireBehavior{
|
||||
"RemoveRole": discordgo.ExpireBehaviorRemoveRole,
|
||||
"Kick": discordgo.ExpireBehaviorKick,
|
||||
},
|
||||
"ExplicitContentFilterLevel": map[string]discordgo.ExplicitContentFilterLevel{
|
||||
"Disabled": discordgo.ExplicitContentFilterDisabled,
|
||||
"MembersWithoutRoles": discordgo.ExplicitContentFilterMembersWithoutRoles,
|
||||
"AllMembers": discordgo.ExplicitContentFilterAllMembers,
|
||||
},
|
||||
"ForumLayout": map[string]discordgo.ForumLayout{
|
||||
"NotSet": discordgo.ForumLayoutNotSet,
|
||||
"ListView": discordgo.ForumLayoutListView,
|
||||
"GalleryView": discordgo.ForumLayoutGalleryView,
|
||||
},
|
||||
"ForumSortOrderType": map[string]discordgo.ForumSortOrderType{
|
||||
"LatestActivity": discordgo.ForumSortOrderLatestActivity,
|
||||
"CreationDate": discordgo.ForumSortOrderCreationDate,
|
||||
},
|
||||
"GuildFeature": map[string]discordgo.GuildFeature{
|
||||
"AnimatedBanner": discordgo.GuildFeatureAnimatedBanner,
|
||||
"AnimatedIcon": discordgo.GuildFeatureAnimatedIcon,
|
||||
"AutoModeration": discordgo.GuildFeatureAutoModeration,
|
||||
"Banner": discordgo.GuildFeatureBanner,
|
||||
"Community": discordgo.GuildFeatureCommunity,
|
||||
"Discoverable": discordgo.GuildFeatureDiscoverable,
|
||||
"Featurable": discordgo.GuildFeatureFeaturable,
|
||||
"InviteSplash": discordgo.GuildFeatureInviteSplash,
|
||||
"MemberVerificationGateEnabled": discordgo.GuildFeatureMemberVerificationGateEnabled,
|
||||
"MonetizationEnabled": discordgo.GuildFeatureMonetizationEnabled,
|
||||
"MoreStickers": discordgo.GuildFeatureMoreStickers,
|
||||
"News": discordgo.GuildFeatureNews,
|
||||
"Partnered": discordgo.GuildFeaturePartnered,
|
||||
"PreviewEnabled": discordgo.GuildFeaturePreviewEnabled,
|
||||
"PrivateThreads": discordgo.GuildFeaturePrivateThreads,
|
||||
"RoleIcons": discordgo.GuildFeatureRoleIcons,
|
||||
"TicketedEventsEnabled": discordgo.GuildFeatureTicketedEventsEnabled,
|
||||
"VanityURL": discordgo.GuildFeatureVanityURL,
|
||||
"Verified": discordgo.GuildFeatureVerified,
|
||||
"VipRegions": discordgo.GuildFeatureVipRegions,
|
||||
"WelcomeScreenEnabled": discordgo.GuildFeatureWelcomeScreenEnabled,
|
||||
},
|
||||
"GuildNSFWLevel": map[string]discordgo.GuildNSFWLevel{
|
||||
"Default": discordgo.GuildNSFWLevelDefault,
|
||||
"Explicit": discordgo.GuildNSFWLevelExplicit,
|
||||
"Safe": discordgo.GuildNSFWLevelSafe,
|
||||
"AgeRestricted": discordgo.GuildNSFWLevelAgeRestricted,
|
||||
},
|
||||
"GuildOnboardingMode": map[string]discordgo.GuildOnboardingMode{
|
||||
"Default": discordgo.GuildOnboardingModeDefault,
|
||||
"Advanced": discordgo.GuildOnboardingModeAdvanced,
|
||||
},
|
||||
"GuildOnboardingPromptType": map[string]discordgo.GuildOnboardingPromptType{
|
||||
"MultipleChoice": discordgo.GuildOnboardingPromptTypeMultipleChoice,
|
||||
"Dropdown": discordgo.GuildOnboardingPromptTypeDropdown,
|
||||
},
|
||||
"GuildScheduledEventEntityType": map[string]discordgo.GuildScheduledEventEntityType{
|
||||
"StageInstance": discordgo.GuildScheduledEventEntityTypeStageInstance,
|
||||
"Voice": discordgo.GuildScheduledEventEntityTypeVoice,
|
||||
"External": discordgo.GuildScheduledEventEntityTypeExternal,
|
||||
},
|
||||
"GuildScheduledEventPrivacyLevel": map[string]discordgo.GuildScheduledEventPrivacyLevel{
|
||||
"GuildOnly": discordgo.GuildScheduledEventPrivacyLevelGuildOnly,
|
||||
},
|
||||
"GuildScheduledEventStatus": map[string]discordgo.GuildScheduledEventStatus{
|
||||
"Scheduled": discordgo.GuildScheduledEventStatusScheduled,
|
||||
"Active": discordgo.GuildScheduledEventStatusActive,
|
||||
"Completed": discordgo.GuildScheduledEventStatusCompleted,
|
||||
"Canceled": discordgo.GuildScheduledEventStatusCanceled,
|
||||
},
|
||||
"Intent": map[string]discordgo.Intent{
|
||||
"Guilds": discordgo.IntentGuilds,
|
||||
"GuildMembers": discordgo.IntentGuildMembers,
|
||||
"GuildModeration": discordgo.IntentGuildModeration,
|
||||
"GuildEmojis": discordgo.IntentGuildEmojis,
|
||||
"GuildIntegrations": discordgo.IntentGuildIntegrations,
|
||||
"GuildWebhooks": discordgo.IntentGuildWebhooks,
|
||||
"GuildInvites": discordgo.IntentGuildInvites,
|
||||
"GuildVoiceStates": discordgo.IntentGuildVoiceStates,
|
||||
"GuildPresences": discordgo.IntentGuildPresences,
|
||||
"GuildMessages": discordgo.IntentGuildMessages,
|
||||
"GuildMessageReactions": discordgo.IntentGuildMessageReactions,
|
||||
"GuildMessageTyping": discordgo.IntentGuildMessageTyping,
|
||||
"GuildBans": discordgo.IntentGuildBans,
|
||||
"DirectMessages": discordgo.IntentDirectMessages,
|
||||
"DirectMessageReactions": discordgo.IntentDirectMessageReactions,
|
||||
"DirectMessageTyping": discordgo.IntentDirectMessageTyping,
|
||||
"MessageContent": discordgo.IntentMessageContent,
|
||||
"GuildScheduledEvents": discordgo.IntentGuildScheduledEvents,
|
||||
"AutoModerationConfiguration": discordgo.IntentAutoModerationConfiguration,
|
||||
"AutoModerationExecution": discordgo.IntentAutoModerationExecution,
|
||||
"AllWithoutPrivileged": discordgo.IntentsAllWithoutPrivileged,
|
||||
"IntentsAll": discordgo.IntentsAll,
|
||||
"IntentsNone": discordgo.IntentsNone,
|
||||
},
|
||||
"InteractionResponseType": map[string]discordgo.InteractionResponseType{
|
||||
"Pong": discordgo.InteractionResponsePong,
|
||||
"ChannelMessageWithSource": discordgo.InteractionResponseChannelMessageWithSource,
|
||||
"DeferredChannelMessageWithSource": discordgo.InteractionResponseDeferredChannelMessageWithSource,
|
||||
"DeferredMessageUpdate": discordgo.InteractionResponseDeferredMessageUpdate,
|
||||
"UpdateMessage": discordgo.InteractionResponseUpdateMessage,
|
||||
"ApplicationCommandAutocompleteResult": discordgo.InteractionApplicationCommandAutocompleteResult,
|
||||
"Modal": discordgo.InteractionResponseModal,
|
||||
},
|
||||
"InteractionType": map[string]discordgo.InteractionType{
|
||||
"Ping": discordgo.InteractionPing,
|
||||
"ApplicationCommand": discordgo.InteractionApplicationCommand,
|
||||
"MessageComponent": discordgo.InteractionMessageComponent,
|
||||
"ApplicationCommandAutocomplete": discordgo.InteractionApplicationCommandAutocomplete,
|
||||
"ModalSubmit": discordgo.InteractionModalSubmit,
|
||||
},
|
||||
"InviteTargetType": map[string]discordgo.InviteTargetType{
|
||||
"Stream": discordgo.InviteTargetStream,
|
||||
"EmbeddedApplication": discordgo.InviteTargetEmbeddedApplication,
|
||||
},
|
||||
"Locale": map[string]discordgo.Locale{
|
||||
"EnglishUS": discordgo.EnglishUS,
|
||||
"EnglishGB": discordgo.EnglishGB,
|
||||
"Bulgarian": discordgo.Bulgarian,
|
||||
"ChineseCN": discordgo.ChineseCN,
|
||||
"ChineseTW": discordgo.ChineseTW,
|
||||
"Croatian": discordgo.Croatian,
|
||||
"Czech": discordgo.Czech,
|
||||
"Danish": discordgo.Danish,
|
||||
"Dutch": discordgo.Dutch,
|
||||
"Finnish": discordgo.Finnish,
|
||||
"French": discordgo.French,
|
||||
"German": discordgo.German,
|
||||
"Greek": discordgo.Greek,
|
||||
"Hindi": discordgo.Hindi,
|
||||
"Hungarian": discordgo.Hungarian,
|
||||
"Italian": discordgo.Italian,
|
||||
"Japanese": discordgo.Japanese,
|
||||
"Korean": discordgo.Korean,
|
||||
"Lithuanian": discordgo.Lithuanian,
|
||||
"Norwegian": discordgo.Norwegian,
|
||||
"Polish": discordgo.Polish,
|
||||
"PortugueseBR": discordgo.PortugueseBR,
|
||||
"Romanian": discordgo.Romanian,
|
||||
"Russian": discordgo.Russian,
|
||||
"SpanishES": discordgo.SpanishES,
|
||||
"SpanishLATAM": discordgo.SpanishLATAM,
|
||||
"Swedish": discordgo.Swedish,
|
||||
"Thai": discordgo.Thai,
|
||||
"Turkish": discordgo.Turkish,
|
||||
"Ukrainian": discordgo.Ukrainian,
|
||||
"Vietnamese": discordgo.Vietnamese,
|
||||
"Unknown": discordgo.Unknown,
|
||||
},
|
||||
"MemberFlags": map[string]discordgo.MemberFlags{
|
||||
"DidRejoin": discordgo.MemberFlagDidRejoin,
|
||||
"CompletedOnboarding": discordgo.MemberFlagCompletedOnboarding,
|
||||
"BypassesVerification": discordgo.MemberFlagBypassesVerification,
|
||||
"StartedOnboarding": discordgo.MemberFlagStartedOnboarding,
|
||||
},
|
||||
"MembershipState": map[string]discordgo.MembershipState{
|
||||
"Invited": discordgo.MembershipStateInvited,
|
||||
"Accepted": discordgo.MembershipStateAccepted,
|
||||
},
|
||||
"MessageActivityType": map[string]discordgo.MessageActivityType{
|
||||
"Join": discordgo.MessageActivityTypeJoin,
|
||||
"Spectate": discordgo.MessageActivityTypeSpectate,
|
||||
"Listen": discordgo.MessageActivityTypeListen,
|
||||
"JoinRequest": discordgo.MessageActivityTypeJoinRequest,
|
||||
},
|
||||
"MessageNotifications": map[string]discordgo.MessageNotifications{
|
||||
"AllMessages": discordgo.MessageNotificationsAllMessages,
|
||||
"OnlyMentions": discordgo.MessageNotificationsOnlyMentions,
|
||||
},
|
||||
}
|
||||
118
internal/systems/plugins/builtins/fetch.go
Normal file
118
internal/systems/plugins/builtins/fetch.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
// Options contains options for the JavaScript fetch function
|
||||
type Options struct {
|
||||
Method string
|
||||
Body string
|
||||
Headers map[string]any
|
||||
HandleCookies *bool
|
||||
}
|
||||
|
||||
// Response contains the response object for the JavaScript fetch function
|
||||
type Response struct {
|
||||
Status string
|
||||
StatusCode int
|
||||
Headers http.Header
|
||||
body []byte
|
||||
}
|
||||
|
||||
func (r Response) JSON() (v any, err error) {
|
||||
err = json.Unmarshal(r.body, &v)
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (r Response) String() string {
|
||||
return string(r.body)
|
||||
}
|
||||
|
||||
// FetchFunc is the fetch function signature
|
||||
type FetchFunc = func(string, *Options) (*Response, error)
|
||||
|
||||
func fetch(pluginName, pluginVersion string) FetchFunc {
|
||||
// cookiejar.New always returns a nil error
|
||||
jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
|
||||
|
||||
return func(url string, opts *Options) (*Response, error) {
|
||||
if opts == nil {
|
||||
t := true
|
||||
opts = &Options{HandleCookies: &t}
|
||||
}
|
||||
|
||||
if opts.HandleCookies == nil {
|
||||
t := true
|
||||
opts.HandleCookies = &t
|
||||
}
|
||||
|
||||
if opts.Method == "" {
|
||||
opts.Method = http.MethodGet
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(opts.Method, url, strings.NewReader(opts.Body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range opts.Headers {
|
||||
req.Header.Add(key, value.(string))
|
||||
}
|
||||
|
||||
if req.Header.Get("User-Agent") == "" {
|
||||
req.Header.Set("User-Agent", getUserAgent(pluginName, pluginVersion))
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
if *opts.HandleCookies {
|
||||
client.Jar = jar
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Response{
|
||||
Status: resp.Status,
|
||||
StatusCode: resp.StatusCode,
|
||||
Headers: resp.Header,
|
||||
body: responseBody,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// getUserAgent uses the built in vcs information to generate a user agent string
|
||||
func getUserAgent(pluginName, pluginVersion string) string {
|
||||
commit := "unknown"
|
||||
modified := "unmodified"
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
for _, setting := range info.Settings {
|
||||
switch setting.Key {
|
||||
case "vcs.revision":
|
||||
commit = setting.Value[:8]
|
||||
case "vcs.modified":
|
||||
if setting.Value == "true" {
|
||||
modified = "modified"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("owobot/%s (%s; %s/%s)", commit, modified, pluginName, pluginVersion)
|
||||
}
|
||||
42
internal/systems/plugins/builtins/internal.go
Normal file
42
internal/systems/plugins/builtins/internal.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/owobot/internal/cache"
|
||||
"go.elara.ws/owobot/internal/systems/eventlog"
|
||||
"go.elara.ws/owobot/internal/systems/tickets"
|
||||
)
|
||||
|
||||
type eventLogAPI struct{}
|
||||
|
||||
func (eventLogAPI) Log(s *discordgo.Session, guildID string, e eventlog.Entry) error {
|
||||
return eventlog.Log(s, guildID, e)
|
||||
}
|
||||
|
||||
type ticketsAPI struct{}
|
||||
|
||||
func (ticketsAPI) Open(s *discordgo.Session, guildID string, user, executor *discordgo.User) (string, error) {
|
||||
return tickets.Open(s, guildID, user, executor)
|
||||
}
|
||||
|
||||
func (ticketsAPI) Close(s *discordgo.Session, guildID string, user, executor *discordgo.User) {
|
||||
tickets.Close(s, guildID, user, executor)
|
||||
}
|
||||
|
||||
type cacheAPI struct{}
|
||||
|
||||
func (cacheAPI) Channel(s *discordgo.Session, guildID, channelID string) (*discordgo.Channel, error) {
|
||||
return cache.Channel(s, guildID, channelID)
|
||||
}
|
||||
|
||||
func (cacheAPI) Member(s *discordgo.Session, guildID, userID string) (*discordgo.Member, error) {
|
||||
return cache.Member(s, guildID, userID)
|
||||
}
|
||||
|
||||
func (cacheAPI) Role(s *discordgo.Session, guildID, roleID string) (*discordgo.Role, error) {
|
||||
return cache.Role(s, guildID, roleID)
|
||||
}
|
||||
|
||||
func (cacheAPI) Roles(s *discordgo.Session, guildID string) ([]*discordgo.Role, error) {
|
||||
return cache.Roles(s, guildID)
|
||||
}
|
||||
19
internal/systems/plugins/builtins/register.go
Normal file
19
internal/systems/plugins/builtins/register.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
// Register registers all the owobot APIs in JavaScript.
|
||||
func Register(vm *goja.Runtime, pluginName, pluginVersion string) error {
|
||||
return errors.Join(
|
||||
vm.GlobalObject().Set("sql", sqlAPI{pluginName: pluginName}),
|
||||
vm.GlobalObject().Set("vercmp", vercmpAPI{}),
|
||||
vm.GlobalObject().Set("cache", cacheAPI{}),
|
||||
vm.GlobalObject().Set("tickets", ticketsAPI{}),
|
||||
vm.GlobalObject().Set("eventlog", eventLogAPI{}),
|
||||
vm.GlobalObject().Set("fetch", fetch(pluginName, pluginVersion)),
|
||||
)
|
||||
}
|
||||
59
internal/systems/plugins/builtins/sql.go
Normal file
59
internal/systems/plugins/builtins/sql.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
"go.elara.ws/owobot/internal/db/sqltabler"
|
||||
)
|
||||
|
||||
type sqlAPI struct {
|
||||
pluginName string
|
||||
}
|
||||
|
||||
func (s sqlAPI) Exec(query string, args ...any) error {
|
||||
newQuery, err := sqltabler.Modify(query, "_owobot_plugin_", "_"+s.pluginName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.DB().Exec(newQuery, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s sqlAPI) Query(query string, args ...any) ([]map[string]any, error) {
|
||||
newQuery, err := sqltabler.Modify(query, "_owobot_plugin_", "_"+s.pluginName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := db.DB().Queryx(newQuery, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rowsToMap(rows)
|
||||
}
|
||||
|
||||
func (s sqlAPI) QueryOne(query string, args ...any) (map[string]any, error) {
|
||||
newQuery, err := sqltabler.Modify(query, "_owobot_plugin_", "_"+s.pluginName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row := db.DB().QueryRowx(newQuery, args...)
|
||||
if err := row.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]any{}
|
||||
return out, row.MapScan(out)
|
||||
}
|
||||
|
||||
func rowsToMap(rows *sqlx.Rows) ([]map[string]any, error) {
|
||||
var out []map[string]any
|
||||
for rows.Next() {
|
||||
resultMap := map[string]any{}
|
||||
err := rows.MapScan(resultMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, resultMap)
|
||||
}
|
||||
|
||||
return out, rows.Err()
|
||||
}
|
||||
21
internal/systems/plugins/builtins/vercmp.go
Normal file
21
internal/systems/plugins/builtins/vercmp.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package builtins
|
||||
|
||||
import "go.elara.ws/vercmp"
|
||||
|
||||
type vercmpAPI struct{}
|
||||
|
||||
func (vercmpAPI) Newer(v1, v2 string) bool {
|
||||
return vercmp.Compare(v1, v2) == 1
|
||||
}
|
||||
|
||||
func (vercmpAPI) Older(v1, v2 string) bool {
|
||||
return vercmp.Compare(v1, v2) == -1
|
||||
}
|
||||
|
||||
func (vercmpAPI) Equal(v1, v2 string) bool {
|
||||
return vercmp.Compare(v1, v2) == 0
|
||||
}
|
||||
|
||||
func (vercmpAPI) Compare(v1, v2 string) int {
|
||||
return vercmp.Compare(v1, v2)
|
||||
}
|
||||
272
internal/systems/plugins/commands.go
Normal file
272
internal/systems/plugins/commands.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/kballard/go-shellquote"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
// pluginadmCmd handles the `/plugin` command and routes it to the correct subcommand.
|
||||
func pluginadmCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
switch name := data.Options[0].Name; name {
|
||||
case "list":
|
||||
return listCmd(s, i)
|
||||
case "enable":
|
||||
return enableCmd(s, i)
|
||||
case "disable":
|
||||
return disableCmd(s, i)
|
||||
default:
|
||||
return fmt.Errorf("unknown pluginadm subcommand: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
// listCmd handles the `/plugin list` command.
|
||||
func listCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
sb := strings.Builder{}
|
||||
for _, plugin := range Plugins {
|
||||
sb.WriteString(plugin.Info.Name)
|
||||
sb.WriteString(" (")
|
||||
sb.WriteString(plugin.Info.Version)
|
||||
sb.WriteString(`): "`)
|
||||
sb.WriteString(plugin.Info.Desc)
|
||||
sb.WriteByte('"')
|
||||
if pluginEnabled(i.GuildID, plugin.Info.Name) {
|
||||
sb.WriteString(" *")
|
||||
}
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
return util.RespondEphemeral(s, i.Interaction, sb.String())
|
||||
}
|
||||
|
||||
// enableCmd handles the `/plugin enable` command.
|
||||
func enableCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
pluginName := data.Options[0].Options[0].StringValue()
|
||||
|
||||
plugin, ok := findPlugin(pluginName)
|
||||
if !ok {
|
||||
return fmt.Errorf("no such plugin: %q", pluginName)
|
||||
}
|
||||
|
||||
err := enablePlugin(i.GuildID, pluginName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if plugin.api.OnEnable != nil {
|
||||
callable, ok := goja.AssertFunction(plugin.api.OnEnable)
|
||||
if !ok {
|
||||
return fmt.Errorf("onEnable value is not callable")
|
||||
}
|
||||
|
||||
errCh := make(chan error)
|
||||
plugin.Loop.RunOnLoop(func(vm *goja.Runtime) {
|
||||
_, err := callable(vm.ToValue(plugin.api), vm.ToValue(i.GuildID))
|
||||
errCh <- err
|
||||
})
|
||||
if err := <-errCh; err != nil {
|
||||
return fmt.Errorf("%s onEnable: %w", plugin.Info.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully enabled the %q plugin!", pluginName))
|
||||
}
|
||||
|
||||
// disableCmd handles the `/plugin disable` command.
|
||||
func disableCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
pluginName := data.Options[0].Options[0].StringValue()
|
||||
|
||||
plugin, ok := findPlugin(pluginName)
|
||||
if !ok {
|
||||
return fmt.Errorf("no such plugin: %q", pluginName)
|
||||
}
|
||||
|
||||
err := disablePlugin(i.GuildID, pluginName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if plugin.api.OnDisable != nil {
|
||||
callable, ok := goja.AssertFunction(plugin.api.OnDisable)
|
||||
if !ok {
|
||||
return fmt.Errorf("onDisable value is not callable")
|
||||
}
|
||||
|
||||
errCh := make(chan error)
|
||||
plugin.Loop.RunOnLoop(func(vm *goja.Runtime) {
|
||||
_, err := callable(vm.ToValue(plugin.api), vm.ToValue(i.GuildID))
|
||||
errCh <- err
|
||||
})
|
||||
if err := <-errCh; err != nil {
|
||||
return fmt.Errorf("%s onDisable: %w", plugin.Info.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, fmt.Sprintf("Successfully disabled the %q plugin", pluginName))
|
||||
}
|
||||
|
||||
func pluginCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
switch name := data.Options[0].Name; name {
|
||||
case "run":
|
||||
return pluginRunCmd(s, i)
|
||||
case "help":
|
||||
return pluginHelpCmd(s, i)
|
||||
default:
|
||||
return fmt.Errorf("unknown plugin subcommand: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
// pluginHelpCmd handles the `/phelp` command.
|
||||
func pluginHelpCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
cmdStr := data.Options[0].Options[0].StringValue()
|
||||
|
||||
args, err := shellquote.Split(cmdStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, plugin := range Plugins {
|
||||
if !pluginEnabled(i.GuildID, plugin.Info.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
cmd, _, ok := findCmd(plugin.Commands, args)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, perm := range cmd.Permissions {
|
||||
if i.Member.Permissions&perm == 0 {
|
||||
return errors.New("you don't have permission to execute this command")
|
||||
}
|
||||
}
|
||||
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString("Usage: `")
|
||||
sb.WriteString(cmdStr)
|
||||
if usage := cmd.usage(); usage != "" {
|
||||
sb.WriteString(" " + usage)
|
||||
}
|
||||
sb.WriteByte('`')
|
||||
|
||||
sb.WriteString("\n\n")
|
||||
sb.WriteString("Description:\n```text\n")
|
||||
sb.WriteString(cmd.Desc)
|
||||
sb.WriteString("\n```\n")
|
||||
|
||||
if len(cmd.Subcommands) > 0 {
|
||||
sb.WriteString("Subcommands:\n")
|
||||
for _, subcmd := range cmd.Subcommands {
|
||||
sb.WriteString("- `")
|
||||
sb.WriteString(subcmd.Name)
|
||||
if usage := subcmd.usage(); usage != "" {
|
||||
sb.WriteString(" " + usage)
|
||||
}
|
||||
sb.WriteString("`: `")
|
||||
sb.WriteString(subcmd.Desc)
|
||||
sb.WriteString("`\n")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
Embeds: []*discordgo.MessageEmbed{{
|
||||
Title: "Command `" + cmd.Name + "`",
|
||||
Description: sb.String(),
|
||||
}},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return fmt.Errorf("command not found: %q", args[0])
|
||||
}
|
||||
|
||||
// pluginRunCmd handles the `/pluginRunCmd` command.
|
||||
func pluginRunCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
cmdStr := data.Options[0].Options[0].StringValue()
|
||||
|
||||
args, err := shellquote.Split(cmdStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, plugin := range Plugins {
|
||||
if !pluginEnabled(i.GuildID, plugin.Info.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
cmd, newArgs, ok := findCmd(plugin.Commands, args)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, perm := range cmd.Permissions {
|
||||
if i.Member.Permissions&perm == 0 {
|
||||
return errors.New("you don't have permission to execute this command")
|
||||
}
|
||||
}
|
||||
|
||||
callable, ok := goja.AssertFunction(cmd.OnExec)
|
||||
if !ok {
|
||||
return fmt.Errorf("value in onExec is not callable")
|
||||
}
|
||||
|
||||
errCh := make(chan error)
|
||||
plugin.Loop.RunOnLoop(func(vm *goja.Runtime) {
|
||||
_, err = callable(
|
||||
vm.ToValue(cmd),
|
||||
vm.ToValue(s),
|
||||
vm.ToValue(i),
|
||||
vm.ToValue(newArgs),
|
||||
)
|
||||
errCh <- err
|
||||
})
|
||||
return <-errCh
|
||||
}
|
||||
|
||||
return fmt.Errorf("command not found: %q", args[0])
|
||||
}
|
||||
|
||||
func findPlugin(name string) (Plugin, bool) {
|
||||
for _, plugin := range Plugins {
|
||||
if plugin.Info.Name == name {
|
||||
return plugin, true
|
||||
}
|
||||
}
|
||||
return Plugin{}, false
|
||||
}
|
||||
|
||||
func findCmd(cmds []Command, args []string) (Command, []string, bool) {
|
||||
if len(args) == 0 {
|
||||
return Command{}, nil, false
|
||||
}
|
||||
|
||||
for _, cmd := range cmds {
|
||||
if args[0] != cmd.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(cmd.Subcommands) != 0 && len(args) > 1 {
|
||||
subcmd, newArgs, ok := findCmd(cmd.Subcommands, args[1:])
|
||||
if ok {
|
||||
return subcmd, newArgs, true
|
||||
}
|
||||
}
|
||||
|
||||
return cmd, args[1:], true
|
||||
}
|
||||
return Command{}, nil, false
|
||||
}
|
||||
45
internal/systems/plugins/enable.go
Normal file
45
internal/systems/plugins/enable.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
)
|
||||
|
||||
var enabled = map[string][]string{}
|
||||
|
||||
func loadEnabled() error {
|
||||
guilds, err := db.AllGuilds()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, guild := range guilds {
|
||||
enabled[guild.ID] = []string(guild.EnabledPlugins)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func enablePlugin(guildID, pluginName string) error {
|
||||
if slices.Contains(enabled[guildID], pluginName) {
|
||||
return fmt.Errorf("plugin %q is already enabled", pluginName)
|
||||
}
|
||||
enabled[guildID] = append(enabled[guildID], pluginName)
|
||||
return db.EnablePlugin(guildID, pluginName)
|
||||
}
|
||||
|
||||
func disablePlugin(guildID, pluginName string) error {
|
||||
if i := slices.Index(enabled[guildID], pluginName); i > -1 {
|
||||
enabled[guildID] = append(enabled[guildID][:i], enabled[guildID][i+1:]...)
|
||||
} else {
|
||||
return fmt.Errorf("plugin %q is already disabled", pluginName)
|
||||
}
|
||||
return db.DisablePlugin(guildID, pluginName)
|
||||
}
|
||||
|
||||
func pluginEnabled(guildID, pluginName string) bool {
|
||||
if guildID == "" {
|
||||
return false
|
||||
}
|
||||
return slices.Contains(enabled[guildID], pluginName)
|
||||
}
|
||||
34
internal/systems/plugins/field_mapper.go
Normal file
34
internal/systems/plugins/field_mapper.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type lowerCamelNameMapper struct{}
|
||||
|
||||
func (lowerCamelNameMapper) FieldName(_ reflect.Type, f reflect.StructField) string {
|
||||
return toLowerCamel(f.Name)
|
||||
}
|
||||
|
||||
func (lowerCamelNameMapper) MethodName(_ reflect.Type, m reflect.Method) string {
|
||||
return toLowerCamel(m.Name)
|
||||
}
|
||||
|
||||
func toLowerCamel(name string) string {
|
||||
if isUpper(name) {
|
||||
return strings.ToLower(name)
|
||||
} else {
|
||||
return strings.ToLower(name[:1]) + name[1:]
|
||||
}
|
||||
}
|
||||
|
||||
func isUpper(s string) bool {
|
||||
for _, char := range s {
|
||||
if unicode.IsLower(char) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
134
internal/systems/plugins/handlers.go
Normal file
134
internal/systems/plugins/handlers.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
// HandlerFunc is an event handler function.
|
||||
type HandlerFunc func(session *discordgo.Session, data any)
|
||||
|
||||
// Handler represents a plugin event handler.
|
||||
type Handler struct {
|
||||
PluginName string
|
||||
Func HandlerFunc
|
||||
}
|
||||
|
||||
var (
|
||||
handlersMtx = sync.Mutex{}
|
||||
handlerMap = map[string][]Handler{}
|
||||
)
|
||||
|
||||
// handlePluginEvent handles any discord event we receive and
|
||||
// routes it to the appropriate plugin handler(s).
|
||||
func handlePluginEvent(s *discordgo.Session, data any) {
|
||||
name := reflect.TypeOf(data).Elem().Name()
|
||||
handlers, ok := handlerMap[name]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, h := range handlers {
|
||||
if !pluginEnabled(eventGuildID(data), h.PluginName) {
|
||||
continue
|
||||
}
|
||||
|
||||
h.Func(s, data)
|
||||
}
|
||||
}
|
||||
|
||||
// eventGuildID uses reflection to get the guild ID from an event
|
||||
func eventGuildID(event any) string {
|
||||
evt := reflect.ValueOf(event)
|
||||
|
||||
for evt.Kind() == reflect.Pointer {
|
||||
evt = evt.Elem()
|
||||
}
|
||||
|
||||
if evt.Kind() != reflect.Struct {
|
||||
return ""
|
||||
}
|
||||
|
||||
if id := evt.FieldByName("GuildID"); id.IsValid() {
|
||||
return id.String()
|
||||
} else if guild := evt.FieldByName("Guild"); guild.IsValid() {
|
||||
if id := guild.FieldByName("ID"); id.IsValid() {
|
||||
return id.String()
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// handleAutocomplete handles autocomplete events for the /plugin run command.
|
||||
func handleAutocomplete(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if i.Type != discordgo.InteractionApplicationCommandAutocomplete {
|
||||
return
|
||||
}
|
||||
|
||||
data := i.ApplicationCommandData()
|
||||
if data.Name != "plugin" {
|
||||
return
|
||||
}
|
||||
|
||||
cmdStr := data.Options[0].Options[0].StringValue()
|
||||
|
||||
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionApplicationCommandAutocompleteResult,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Choices: getAllChoices(i.GuildID, cmdStr, i.Member),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// getAllChoices gets possible command strings for each plugin and converts them
|
||||
// to Discord command options.
|
||||
func getAllChoices(guildID, partial string, member *discordgo.Member) (out []*discordgo.ApplicationCommandOptionChoice) {
|
||||
for _, plugin := range Plugins {
|
||||
if !pluginEnabled(guildID, plugin.Info.Name) {
|
||||
continue
|
||||
}
|
||||
out = append(out, getChoiceStrs(partial, "", plugin.Commands, member)...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// getChoiceStrs recursively looks through every command in cmds,
|
||||
// and generates a list of strings to use as autocomplete options.
|
||||
func getChoiceStrs(partial, prefix string, cmds []Command, member *discordgo.Member) []*discordgo.ApplicationCommandOptionChoice {
|
||||
if len(cmds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
partial = strings.TrimSpace(partial)
|
||||
var out []*discordgo.ApplicationCommandOptionChoice
|
||||
|
||||
for _, cmd := range cmds {
|
||||
for _, perm := range cmd.Permissions {
|
||||
if member.Permissions&perm == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sub := getChoiceStrs(strings.TrimPrefix(partial, cmd.Name), cmd.Name+" ", cmd.Subcommands, member)
|
||||
out = append(out, sub...)
|
||||
|
||||
if cmd.OnExec == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
qualifiedCmd := prefix + cmd.Name
|
||||
|
||||
if strings.Contains(qualifiedCmd, partial) {
|
||||
out = append(out, &discordgo.ApplicationCommandOptionChoice{
|
||||
Name: qualifiedCmd,
|
||||
Value: qualifiedCmd,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
195
internal/systems/plugins/init.go
Normal file
195
internal/systems/plugins/init.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/dop251/goja_nodejs/eventloop"
|
||||
"go.elara.ws/logger/log"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
"go.elara.ws/owobot/internal/systems/commands"
|
||||
"go.elara.ws/owobot/internal/systems/plugins/builtins"
|
||||
"go.elara.ws/owobot/internal/util"
|
||||
)
|
||||
|
||||
func Init(s *discordgo.Session) error {
|
||||
if err := loadEnabled(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commands.Register(s, pluginCmd, &discordgo.ApplicationCommand{
|
||||
Name: "plugin",
|
||||
Description: "Interact with the plugins on this server",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "run",
|
||||
Description: "Run a plugin command",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "cmd",
|
||||
Description: "The plugin command to run",
|
||||
Required: true,
|
||||
Autocomplete: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "help",
|
||||
Description: "See how to use a plugin command",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "cmd",
|
||||
Description: "The plugin command to help with",
|
||||
Required: true,
|
||||
Autocomplete: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
commands.Register(s, pluginadmCmd, &discordgo.ApplicationCommand{
|
||||
Name: "pluginadm",
|
||||
Description: "Manage dynamic plugins for your server",
|
||||
DefaultMemberPermissions: util.Pointer[int64](discordgo.PermissionManageServer),
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "list",
|
||||
Description: "List all available plugins",
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "enable",
|
||||
Description: "Enable a plugin in this guild",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "plugin",
|
||||
Description: "The name of the plugin to enable",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "disable",
|
||||
Description: "Disable a plugin in this guild",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "plugin",
|
||||
Description: "The name of the plugin to disable",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
s.AddHandler(handleAutocomplete)
|
||||
s.AddHandler(handlePluginEvent)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load recursively loads plugins from the given directory.
|
||||
func Load(dir string, sess *discordgo.Session) error {
|
||||
return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() || filepath.Ext(path) != ".js" {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loop := eventloop.NewEventLoop()
|
||||
|
||||
loop.Run(func(vm *goja.Runtime) {
|
||||
vm.SetFieldNameMapper(lowerCamelNameMapper{})
|
||||
})
|
||||
|
||||
api := &owobotAPI{loop: loop, path: path}
|
||||
|
||||
loop.Run(func(vm *goja.Runtime) {
|
||||
err = errors.Join(
|
||||
vm.GlobalObject().Set("owobot", api),
|
||||
vm.GlobalObject().Set("discord", builtins.Constants),
|
||||
vm.GlobalObject().Set("print", fmt.Println),
|
||||
)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loop.Start()
|
||||
errCh := make(chan error)
|
||||
|
||||
loop.RunOnLoop(func(vm *goja.Runtime) {
|
||||
_, err := vm.RunScript(path, string(data))
|
||||
errCh <- err
|
||||
})
|
||||
if err := <-errCh; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !api.PluginInfo.IsValid() {
|
||||
log.Warn("Plugin info not provided, skipping.").Str("path", path).Send()
|
||||
return nil
|
||||
}
|
||||
|
||||
prev, _ := db.GetPlugin(api.PluginInfo.Name)
|
||||
|
||||
err = db.AddPlugin(api.PluginInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loop.RunOnLoop(func(vm *goja.Runtime) {
|
||||
err := builtins.Register(vm, api.PluginInfo.Name, api.PluginInfo.Version)
|
||||
errCh <- err
|
||||
})
|
||||
if err := <-errCh; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Plugins = append(Plugins, Plugin{
|
||||
Info: api.PluginInfo,
|
||||
Commands: api.Commands,
|
||||
Loop: loop,
|
||||
api: api,
|
||||
})
|
||||
|
||||
if api.Init != nil {
|
||||
callableInit, ok := goja.AssertFunction(api.Init)
|
||||
if !ok {
|
||||
log.Warn("Init value is not callable, ignoring.").Str("plugin", api.PluginInfo.Name).Send()
|
||||
return nil
|
||||
}
|
||||
|
||||
loop.RunOnLoop(func(vm *goja.Runtime) {
|
||||
_, err := callableInit(vm.ToValue(api), vm.ToValue(prev), vm.ToValue(sess))
|
||||
errCh <- err
|
||||
})
|
||||
if err := <-errCh; err != nil {
|
||||
return fmt.Errorf("%s init: %w", api.PluginInfo.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
41
internal/systems/polls/commands.go
Normal file
41
internal/systems/polls/commands.go
Normal file
@@ -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 {
|
||||
@@ -217,7 +158,7 @@ func onPollFinish(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
currentRow.Components = append(currentRow.Components, discordgo.Button{
|
||||
CustomID: "vote:" + strconv.Itoa(i) + ":" + privacyToken,
|
||||
Style: discordgo.SecondaryButton,
|
||||
Emoji: discordgo.ComponentEmoji{
|
||||
Emoji: &discordgo.ComponentEmoji{
|
||||
Name: e.Name,
|
||||
ID: e.ID,
|
||||
},
|
||||
@@ -364,7 +305,7 @@ func updatePollUnfinished(s *discordgo.Session, msgID, channelID string) error {
|
||||
ID: msgID,
|
||||
Channel: channelID,
|
||||
Content: &content,
|
||||
Components: []discordgo.MessageComponent{
|
||||
Components: &[]discordgo.MessageComponent{
|
||||
discordgo.ActionsRow{Components: []discordgo.MessageComponent{
|
||||
discordgo.Button{
|
||||
Label: "Add Option",
|
||||
30
internal/systems/polls/init.go
Normal file
30
internal/systems/polls/init.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -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()
|
||||
|
||||
@@ -41,11 +42,16 @@ func reactionsCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
return reactionsListCmd(s, i)
|
||||
case "delete":
|
||||
return reactionsDeleteCmd(s, i)
|
||||
case "exclude":
|
||||
return reactionsExcludeCmd(s, i)
|
||||
case "unexclude":
|
||||
return reactionsUnexcludeCmd(s, i)
|
||||
default:
|
||||
return fmt.Errorf("unknown reactions subcommand: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
// reactionsAddCmd handles the `/reactions add` command.
|
||||
func reactionsAddCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
@@ -54,7 +60,6 @@ func reactionsAddCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error
|
||||
MatchType: db.MatchType(args[0].StringValue()),
|
||||
Match: strings.TrimSpace(args[1].StringValue()),
|
||||
ReactionType: db.ReactionType(args[2].StringValue()),
|
||||
Reaction: strings.TrimSpace(args[3].StringValue()),
|
||||
Chance: 100,
|
||||
}
|
||||
|
||||
@@ -73,10 +78,16 @@ func reactionsAddCmd(s *discordgo.Session, i *discordgo.InteractionCreate) error
|
||||
reaction.Match = strings.ToLower(reaction.Match)
|
||||
}
|
||||
|
||||
if reaction.ReactionType == db.ReactionTypeEmoji {
|
||||
switch reaction.ReactionType {
|
||||
case db.ReactionTypeEmoji:
|
||||
// Convert comma-separated emoji into a StringSlice value
|
||||
reaction.Reaction = db.StringSlice(strings.Split(strings.TrimSpace(args[3].StringValue()), ","))
|
||||
if err := validateEmoji(reaction.Reaction); err != nil {
|
||||
return err
|
||||
}
|
||||
case db.ReactionTypeText:
|
||||
// Create a StringSlice with the desired text inside
|
||||
reaction.Reaction = db.StringSlice{args[3].StringValue()}
|
||||
}
|
||||
|
||||
err := db.AddReaction(i.GuildID, reaction)
|
||||
@@ -87,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 {
|
||||
@@ -105,7 +117,7 @@ func reactionsListCmd(s *discordgo.Session, i *discordgo.InteractionCreate) erro
|
||||
sb.WriteString("]_ `")
|
||||
sb.WriteString(reaction.Match)
|
||||
sb.WriteString("`: \"")
|
||||
sb.WriteString(reaction.Reaction)
|
||||
sb.WriteString(reaction.Reaction.String())
|
||||
sb.WriteString("\" _(")
|
||||
sb.WriteString(string(reaction.ReactionType))
|
||||
sb.WriteString(")_\n")
|
||||
@@ -114,6 +126,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
|
||||
@@ -132,17 +145,65 @@ func reactionsDeleteCmd(s *discordgo.Session, i *discordgo.InteractionCreate) er
|
||||
return util.RespondEphemeral(s, i.Interaction, "Successfully removed reaction")
|
||||
}
|
||||
|
||||
func validateEmoji(s string) error {
|
||||
if strings.Contains(s, ",") {
|
||||
split := strings.Split(s, ",")
|
||||
for _, emojiStr := range split {
|
||||
if _, ok := emoji.Parse(emojiStr); !ok {
|
||||
return fmt.Errorf("invalid reaction emoji: %s", emojiStr)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if _, ok := emoji.Parse(s); !ok {
|
||||
return fmt.Errorf("invalid reaction emoji: %s", s)
|
||||
// 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
|
||||
if i.Member.Permissions&discordgo.PermissionManageEmojis == 0 {
|
||||
return errors.New("you do not have permission to exclude channels")
|
||||
}
|
||||
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
|
||||
channel := args[0].ChannelValue(s)
|
||||
|
||||
var match string
|
||||
if len(args) == 2 {
|
||||
match = args[1].StringValue()
|
||||
}
|
||||
|
||||
err := db.ReactionsExclude(i.GuildID, match, channel.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
if i.Member.Permissions&discordgo.PermissionManageEmojis == 0 {
|
||||
return errors.New("you do not have permission to unexclude channels")
|
||||
}
|
||||
|
||||
data := i.ApplicationCommandData()
|
||||
args := data.Options[0].Options
|
||||
|
||||
channel := args[0].ChannelValue(s)
|
||||
|
||||
var match string
|
||||
if len(args) == 2 {
|
||||
match = args[1].StringValue()
|
||||
}
|
||||
|
||||
err := db.ReactionsUnexclude(i.GuildID, match, channel.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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])
|
||||
if _, ok := emoji.Parse(s[i]); !ok {
|
||||
return fmt.Errorf("invalid reaction emoji: %s", s[i])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
121
internal/systems/reactions/handlers.go
Normal file
121
internal/systems/reactions/handlers.go
Normal file
@@ -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
|
||||
}
|
||||
|
||||
var content db.StringSlice
|
||||
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 != nil {
|
||||
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
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,19 +19,7 @@
|
||||
package reactions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"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"
|
||||
)
|
||||
@@ -120,110 +108,52 @@ func Init(s *discordgo.Session) error {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "exclude",
|
||||
Description: "Exclude a channel from having reactions",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "channel",
|
||||
Description: "The channel which shouldn't receive reactions",
|
||||
Type: discordgo.ApplicationCommandOptionChannel,
|
||||
ChannelTypes: []discordgo.ChannelType{
|
||||
discordgo.ChannelTypeGuildText,
|
||||
discordgo.ChannelTypeGuildForum,
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "match",
|
||||
Description: "The match value to exclude",
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "unexclude",
|
||||
Description: "Unexclude a channel from having reactions",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "channel",
|
||||
Description: "The channel which should receive reactions",
|
||||
Type: discordgo.ApplicationCommandOptionChannel,
|
||||
ChannelTypes: []discordgo.ChannelType{
|
||||
discordgo.ChannelTypeGuildText,
|
||||
discordgo.ChannelTypeGuildForum,
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "match",
|
||||
Description: "The match value to unexclude",
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
var content string
|
||||
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 = fasttemplate.ExecuteStringStd(reaction.Reaction, "{", "}", replacements)
|
||||
} else if len(submatch) == 1 {
|
||||
content = reaction.Reaction
|
||||
}
|
||||
case db.ReactionTypeEmoji:
|
||||
if re.MatchString(mc.Content) {
|
||||
content = reaction.Reaction
|
||||
}
|
||||
}
|
||||
|
||||
if content != "" {
|
||||
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 string, 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, mc.Reference())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case db.ReactionTypeEmoji:
|
||||
var emojis []string
|
||||
if strings.Contains(content, ",") {
|
||||
emojis = strings.Split(content, ",")
|
||||
} else {
|
||||
emojis = []string{content}
|
||||
}
|
||||
|
||||
for _, emojiStr := range emojis {
|
||||
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
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -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)
|
||||
@@ -189,7 +250,7 @@ func updateReactionRoleCategoryMsg(s *discordgo.Session, channelID, category str
|
||||
currentRow.Components = append(currentRow.Components, discordgo.Button{
|
||||
CustomID: "role:" + roleID,
|
||||
Style: discordgo.SecondaryButton,
|
||||
Emoji: discordgo.ComponentEmoji{
|
||||
Emoji: &discordgo.ComponentEmoji{
|
||||
Name: e.Name,
|
||||
ID: e.ID,
|
||||
},
|
||||
@@ -203,13 +264,13 @@ func updateReactionRoleCategoryMsg(s *discordgo.Session, channelID, category str
|
||||
_, err = s.ChannelMessageEditComplex(&discordgo.MessageEdit{
|
||||
Channel: channelID,
|
||||
ID: rrc.MsgID,
|
||||
Embeds: []*discordgo.MessageEmbed{
|
||||
Embeds: &[]*discordgo.MessageEmbed{
|
||||
{
|
||||
Title: rrc.Name,
|
||||
Description: sb.String(),
|
||||
},
|
||||
},
|
||||
Components: components,
|
||||
Components: &components,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
40
internal/systems/roles/handlers.go
Normal file
40
internal/systems/roles/handlers.go
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -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 - The coolest Discord bot ever written
|
||||
* 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))
|
||||
}
|
||||
}
|
||||
55
internal/systems/starboard/commands.go
Normal file
55
internal/systems/starboard/commands.go
Normal file
@@ -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))
|
||||
}
|
||||
144
internal/systems/starboard/handlers.go
Normal file
144
internal/systems/starboard/handlers.go
Normal file
@@ -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 ""
|
||||
}
|
||||
71
internal/systems/starboard/init.go
Normal file
71
internal/systems/starboard/init.go
Normal file
@@ -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,258 +0,0 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* 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"
|
||||
"time"
|
||||
|
||||
"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/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)",
|
||||
msg.GuildID,
|
||||
msg.ChannelID,
|
||||
msg.ID,
|
||||
),
|
||||
Color: embedColor,
|
||||
Footer: &discordgo.MessageEmbedFooter{
|
||||
Text: util.FormatJucheTime(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
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 ""
|
||||
}
|
||||
50
internal/systems/tickets/commands.go
Normal file
50
internal/systems/tickets/commands.go
Normal file
@@ -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))
|
||||
}
|
||||
22
internal/systems/tickets/handlers.go
Normal file
22
internal/systems/tickets/handlers.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package tickets
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/logger/log"
|
||||
)
|
||||
|
||||
// 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,13 +2,12 @@ package tickets
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"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"
|
||||
@@ -71,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) {
|
||||
@@ -134,20 +78,38 @@ func Open(s *discordgo.Session, guildID string, user, executor *discordgo.User)
|
||||
return "", fmt.Errorf("ticket already exists for %s at <#%s>", user.Mention(), channelID)
|
||||
}
|
||||
|
||||
if executor == nil {
|
||||
executor = s.State.User
|
||||
}
|
||||
|
||||
guild, err := db.GuildByID(guildID)
|
||||
if err != nil {
|
||||
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
|
||||
@@ -173,6 +135,10 @@ func Close(s *discordgo.Session, guildID string, user, executor *discordgo.User)
|
||||
return err
|
||||
}
|
||||
|
||||
if executor == nil {
|
||||
executor = s.State.User
|
||||
}
|
||||
|
||||
guild, err := db.GuildByID(guildID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -228,26 +194,17 @@ 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))
|
||||
if len(msgs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := len(msgs) - 1; i >= 0; i-- {
|
||||
_, err := io.WriteString(w, fmt.Sprintf("%s - %s\n", msgs[i].Author.Username, msgs[i].Content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
169
internal/systems/vetting/commands.go
Normal file
169
internal/systems/vetting/commands.go
Normal file
@@ -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, user.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
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/logger/log"
|
||||
@@ -35,47 +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)
|
||||
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 vettign 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()))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -85,7 +43,11 @@ func onMemberJoin(s *discordgo.Session, gma *discordgo.GuildMemberAdd) {
|
||||
return
|
||||
}
|
||||
|
||||
if guild.VettingRoleID == "" {
|
||||
if guild.VettingRoleID == "" || guild.VettingReqChanID == "" {
|
||||
err = welcomeUser(s, guild, gma.Member.User)
|
||||
if err != nil {
|
||||
log.Warn("Error welcoming user").Err(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -112,7 +74,7 @@ func onMakeVettingMsg(s *discordgo.Session, i *discordgo.InteractionCreate) erro
|
||||
Label: "Request Vetting",
|
||||
Style: discordgo.SuccessButton,
|
||||
Disabled: false,
|
||||
Emoji: discordgo.ComponentEmoji{Name: clipboardEmoji},
|
||||
Emoji: &discordgo.ComponentEmoji{Name: clipboardEmoji},
|
||||
CustomID: "vetting-req",
|
||||
},
|
||||
}},
|
||||
@@ -138,6 +100,11 @@ func onVettingRequest(s *discordgo.Session, i *discordgo.InteractionCreate) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := db.VettingReqMsgID(i.GuildID, i.Member.User.ID)
|
||||
if err == nil {
|
||||
return errors.New("you've already sent a vetting request")
|
||||
}
|
||||
|
||||
guild, err := db.GuildByID(i.GuildID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -151,29 +118,30 @@ func onVettingRequest(s *discordgo.Session, i *discordgo.InteractionCreate) erro
|
||||
return errors.New("you do not have the vetting role")
|
||||
}
|
||||
|
||||
_, err = s.ChannelMessageSendComplex(guild.VettingReqChanID, &discordgo.MessageSend{
|
||||
Embed: &discordgo.MessageEmbed{
|
||||
Title: "Vetting Request",
|
||||
Author: &discordgo.MessageEmbedAuthor{
|
||||
Name: i.Member.User.Username,
|
||||
IconURL: i.Member.User.AvatarURL(""),
|
||||
},
|
||||
Description: "Accept the vetting request to create a ticket, or reject it to kick the user.",
|
||||
Footer: &discordgo.MessageEmbedFooter{
|
||||
Text: util.FormatJucheTime(time.Now()),
|
||||
},
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: "Vetting Request",
|
||||
Author: &discordgo.MessageEmbedAuthor{
|
||||
Name: i.Member.User.Username,
|
||||
IconURL: i.Member.User.AvatarURL(""),
|
||||
},
|
||||
Description: "Accept the vetting request to create a ticket, or reject it to kick the user.",
|
||||
}
|
||||
|
||||
eventlog.AddTimeToEmbed(guild.TimeFormat, embed)
|
||||
|
||||
msg, err := s.ChannelMessageSendComplex(guild.VettingReqChanID, &discordgo.MessageSend{
|
||||
Embeds: []*discordgo.MessageEmbed{embed},
|
||||
Components: []discordgo.MessageComponent{
|
||||
discordgo.ActionsRow{Components: []discordgo.MessageComponent{
|
||||
discordgo.Button{
|
||||
Label: "Accept",
|
||||
Emoji: discordgo.ComponentEmoji{Name: checkEmoji},
|
||||
Emoji: &discordgo.ComponentEmoji{Name: checkEmoji},
|
||||
Style: discordgo.SuccessButton,
|
||||
CustomID: "vetting-accept:" + i.Member.User.ID,
|
||||
},
|
||||
discordgo.Button{
|
||||
Label: "Reject",
|
||||
Emoji: discordgo.ComponentEmoji{Name: crossEmoji},
|
||||
Emoji: &discordgo.ComponentEmoji{Name: crossEmoji},
|
||||
Style: discordgo.DangerButton,
|
||||
CustomID: "vetting-reject:" + i.Member.User.ID,
|
||||
},
|
||||
@@ -184,74 +152,14 @@ func onVettingRequest(s *discordgo.Session, i *discordgo.InteractionCreate) erro
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.AddVettingReq(i.GuildID, i.Member.User.ID, msg.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, "Successfully sent your vetting request!")
|
||||
}
|
||||
|
||||
// onApprove 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 {
|
||||
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 = 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()),
|
||||
Author: user,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.RespondEphemeral(s, i.Interaction, "Successfully approved "+user.Mention()+" as "+role.Mention()+"!")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -270,6 +178,11 @@ func onVettingResponse(s *discordgo.Session, i *discordgo.InteractionCreate) err
|
||||
return nil
|
||||
}
|
||||
|
||||
guild, err := db.GuildByID(i.GuildID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
executor := i.Member
|
||||
member, err := cache.Member(s, i.GuildID, userID)
|
||||
if err != nil {
|
||||
@@ -283,22 +196,21 @@ func onVettingResponse(s *discordgo.Session, i *discordgo.InteractionCreate) err
|
||||
return err
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: "Vetting Request Accepted!",
|
||||
Description: fmt.Sprintf("This vetting request has been accepted and a vetting ticket has been created at <#%s>.\n\n**Accepted By:** <@%s>", channelID, executor.User.ID),
|
||||
Author: &discordgo.MessageEmbedAuthor{
|
||||
Name: member.User.Username,
|
||||
IconURL: member.User.AvatarURL(""),
|
||||
},
|
||||
}
|
||||
|
||||
eventlog.AddTimeToEmbed(guild.TimeFormat, embed)
|
||||
|
||||
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseUpdateMessage,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Embeds: []*discordgo.MessageEmbed{
|
||||
{
|
||||
Title: "Vetting Request Accepted!",
|
||||
Description: fmt.Sprintf("This vetting request has been accepted and a vetting ticket has been created at <#%s>.\n\n**Accepted By:** <@%s>", channelID, executor.User.ID),
|
||||
Author: &discordgo.MessageEmbedAuthor{
|
||||
Name: member.User.Username,
|
||||
IconURL: member.User.AvatarURL(""),
|
||||
},
|
||||
Footer: &discordgo.MessageEmbedFooter{
|
||||
Text: util.FormatJucheTime(time.Now()),
|
||||
},
|
||||
},
|
||||
},
|
||||
Embeds: []*discordgo.MessageEmbed{embed},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -310,27 +222,56 @@ func onVettingResponse(s *discordgo.Session, i *discordgo.InteractionCreate) err
|
||||
return err
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: "Vetting Request Rejected",
|
||||
Description: fmt.Sprintf("This vetting request has been rejected and <@%s> has been kicked from the server.\n\n**Rejected By:** <@%s>", member.User.ID, executor.User.ID),
|
||||
Author: &discordgo.MessageEmbedAuthor{
|
||||
Name: member.User.Username,
|
||||
IconURL: member.User.AvatarURL(""),
|
||||
},
|
||||
}
|
||||
|
||||
eventlog.AddTimeToEmbed(guild.TimeFormat, embed)
|
||||
|
||||
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseUpdateMessage,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Embeds: []*discordgo.MessageEmbed{
|
||||
{
|
||||
Title: "Vetting Request Rejected",
|
||||
Description: fmt.Sprintf("This vetting request has been rejected and <@%s> has been kicked from the server.\n\n**Rejected By:** <@%s>", member.User.ID, executor.User.ID),
|
||||
Author: &discordgo.MessageEmbedAuthor{
|
||||
Name: member.User.Username,
|
||||
IconURL: member.User.AvatarURL(""),
|
||||
},
|
||||
Footer: &discordgo.MessageEmbedFooter{
|
||||
Text: util.FormatJucheTime(time.Now()),
|
||||
},
|
||||
},
|
||||
},
|
||||
Embeds: []*discordgo.MessageEmbed{embed},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return 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) {
|
||||
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, gmr.Member.User.ID)
|
||||
if err != nil {
|
||||
log.Error("Error removing vetting request after member leave").Str("user-id", gmr.Member.User.ID).Err(err).Send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -31,6 +31,11 @@ const (
|
||||
)
|
||||
|
||||
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",
|
||||
Type: discordgo.MessageApplicationCommand,
|
||||
@@ -61,7 +66,7 @@ func Init(s *discordgo.Session) error {
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "role",
|
||||
Name: "channel",
|
||||
Description: "The channel to use for vetting requests",
|
||||
Type: discordgo.ApplicationCommandOptionChannel,
|
||||
ChannelTypes: []discordgo.ChannelType{discordgo.ChannelTypeGuildText},
|
||||
@@ -69,10 +74,37 @@ func Init(s *discordgo.Session) error {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "welcome_channel",
|
||||
Description: "Set the welcome channel",
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "channel",
|
||||
Description: "The channel to use for welcoming new users",
|
||||
Type: discordgo.ApplicationCommandOptionChannel,
|
||||
ChannelTypes: []discordgo.ChannelType{discordgo.ChannelTypeGuildText},
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "welcome_msg",
|
||||
Description: "Set the welcome message",
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "msg",
|
||||
Description: "The message to welcome new users with",
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
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),
|
||||
@@ -92,9 +124,5 @@ 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))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,9 +19,6 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"go.elara.ws/logger/log"
|
||||
)
|
||||
@@ -33,17 +30,6 @@ func Pointer[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
// FormatJucheTime formats the given time in Juche calendar format,
|
||||
// using pyongyang time if it's available, and otherwise UTC.
|
||||
func FormatJucheTime(t time.Time) string {
|
||||
tz, err := time.LoadLocation("Asia/Pyongyang")
|
||||
if err != nil {
|
||||
tz = time.UTC
|
||||
}
|
||||
t = t.In(tz)
|
||||
return fmt.Sprintf("%02d:%02d %02d-%02d Juche %d", t.Hour(), t.Minute(), t.Day(), t.Month(), t.Year()-1911)
|
||||
}
|
||||
|
||||
// RespondEphemeral responds to an interaction with an ephemeral message.
|
||||
func RespondEphemeral(s *discordgo.Session, i *discordgo.Interaction, content string) error {
|
||||
return s.InteractionRespond(i, &discordgo.InteractionResponse{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
||||
16
main.go
16
main.go
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* owobot - The coolest Discord bot ever written
|
||||
* owobot - Your server's guardian and entertainer
|
||||
* Copyright (C) 2023 owobot Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -28,10 +28,12 @@ import (
|
||||
"go.elara.ws/logger"
|
||||
"go.elara.ws/logger/log"
|
||||
"go.elara.ws/owobot/internal/db"
|
||||
"go.elara.ws/owobot/internal/systems/about"
|
||||
"go.elara.ws/owobot/internal/systems/commands"
|
||||
"go.elara.ws/owobot/internal/systems/eventlog"
|
||||
"go.elara.ws/owobot/internal/systems/guilds"
|
||||
"go.elara.ws/owobot/internal/systems/members"
|
||||
"go.elara.ws/owobot/internal/systems/plugins"
|
||||
"go.elara.ws/owobot/internal/systems/polls"
|
||||
"go.elara.ws/owobot/internal/systems/reactions"
|
||||
"go.elara.ws/owobot/internal/systems/roles"
|
||||
@@ -49,9 +51,9 @@ func main() {
|
||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
cfg, err := loadEnv()
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
log.Fatal("Error loading environment variables").Err(err).Send()
|
||||
log.Fatal("Error loading configuration").Err(err).Send()
|
||||
}
|
||||
|
||||
err = db.Init(ctx, cfg.DBPath+"?_pragma=busy_timeout(30000)")
|
||||
@@ -67,6 +69,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()
|
||||
@@ -83,6 +86,11 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
err = plugins.Load(cfg.PluginDir, s)
|
||||
if err != nil {
|
||||
log.Error("Error running plugin file").Err(err).Send()
|
||||
}
|
||||
|
||||
initSystems(
|
||||
s,
|
||||
starboard.Init,
|
||||
@@ -94,6 +102,8 @@ func main() {
|
||||
vetting.Init,
|
||||
reactions.Init,
|
||||
roles.Init,
|
||||
about.Init,
|
||||
plugins.Init,
|
||||
commands.Init, // The commands system should always go last
|
||||
)
|
||||
|
||||
|
||||
7
owobot.toml
Normal file
7
owobot.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
token = "CHANGE ME"
|
||||
db_path = "/etc/owobot/owobot.db"
|
||||
plugin_dir = "/etc/owobot/plugins"
|
||||
|
||||
[activity]
|
||||
type = -1
|
||||
name = ""
|
||||
Reference in New Issue
Block a user