8 Commits
Author SHA1 Message Date
FernandoJVideira a92c4131db Merge pull request 'ft/welcome_message' (#7) from ft/welcome_message into dev
Reviewed-on: https://gitea.fernandovideira.com/FernandoJVideira/velox-bot/pulls/7
2026-03-18 05:16:08 +00:00
FernandoJVideira 397a8a459a chore: readme & help update 2026-03-18 05:15:12 +00:00
FernandoJVideira 4080ce8aec feat: add default role management functionality
- Introduced a new default role service and repository for managing default roles assigned to new members.
- Added command to set the default role through the bot's configuration.
- Implemented event handling to assign the default role to new guild members upon joining.
- Updated main application to include the new default role service in the bot's services.
2026-03-18 05:12:40 +00:00
FernandoJVideira e6aa5def96 feat: add welcome message functionality
- Introduced a new welcome service and repository for managing welcome messages, channels, and GIFs.
- Added commands to set welcome channel, message, DM, and GIF through the bot's configuration.
- Implemented event handling for new guild members to send welcome messages and DMs based on configuration.
- Updated main application to include the new welcome service in the bot's services.
2026-03-18 05:07:11 +00:00
FernandoJVideira 6410619662 Merge pull request 'feat: twitch notifications' (#6) from ft/twitch_notifications into dev
Reviewed-on: https://gitea.fernandovideira.com/FernandoJVideira/velox-bot/pulls/6
2026-03-18 04:42:54 +00:00
FernandoJVideira df656ca12d feat: twitch notifications 2026-03-18 04:42:02 +00:00
FernandoJVideira 076e616b9c feat: readme 2026-03-18 04:02:11 +00:00
FernandoJVideira 891f3f6ad2 Merge pull request 'feat: integrate music commands with Lavalink support' (#5) from ft/music into dev
Reviewed-on: https://gitea.fernandovideira.com/FernandoJVideira/velox-bot/pulls/5
2026-03-18 03:55:06 +00:00
18 changed files with 1420 additions and 67 deletions
+113
View File
@@ -0,0 +1,113 @@
# Velox Bot
A Discord bot written in Go with a PostgreSQL backing store.
It currently includes:
- Fun commands (`/ping`, `/joke`, `/dice`, `/rps`, …)
- Leveling system (opt-in per server via `/slvl toggle`)
- Meeting rooms (voice lobby → auto-created temporary voice channels)
- Scheduling (1:1 sessions with DM invites + reaction-based accept/decline/reschedule)
- Projects (list/create projects + helpers)
- Music (Lavalink-powered `/play`, `/queue`, `/volume`)
- Twitch live notifications (per-guild `/config` setup)
- Welcome messages, DMs, and default role assignment (per-guild `/config` setup)
## Requirements
- Go (see `go.mod`)
- PostgreSQL 16+ (or use the provided `docker-compose.yml`)
- (Optional) [Air](https://github.com/air-verse/air) for live reload during development
- (Optional) Lavalink if you want music commands
## Configuration
The app loads environment variables from `.env` (see `internal/config/config.go`).
Required:
- `BOT_TOKEN`: Discord bot token
- `APP_ID`: Discord application ID
- `GUILD_ID`: Target guild/server ID (commands are registered per-guild)
- `DB_HOST`: Postgres connection string (pgx), e.g. `postgres://velox:velox_pwd@localhost:5432/velox?sslmode=disable`
Optional (music):
- `LAVALINK_HOST`: Lavalink WebSocket/HTTP host (depends on your Lavalink setup)
- `LAVALINK_PASSWORD`: Lavalink password
Optional (Twitch live notifications):
- `TWITCH_CLIENT_ID`: Twitch Client ID used to query live status
## Database setup
If you use the compose file:
```bash
docker compose up -d
```
Then apply the schema:
```bash
psql "postgres://velox:velox_pwd@localhost:5432/velox?sslmode=disable" -f schema.sql
```
## Run
Plain Go:
```bash
go run .
```
With live reload (Air):
```bash
air
```
If you use `just`, a small workflow is included:
```bash
just up # start postgres
just start # start postgres + run air
just down # stop postgres
```
## Commands (high-level)
- **Fun**
- `/ping`, `/joke [type]`, `/coinflip`, `/dice <expr>`
- `/rps <hand>`, `/rpsstats`, `/rpsleaderboard` (server only)
- **Help**
- `/help` (sends a DM with command reference)
- **Leveling**
- `/slvl ...` admin/config
- `/rank`, `/leaderboard`, `/rewards`
- **Meetings** (**Manage Server**)
- `/meeting set-lobby`, `/meeting lock`, `/meeting lock-private`, `/meeting unlock`, `/meeting invite`, `/meeting uninvite`
- **Timezone**
- `/timezone set`, `/timezone show`
- **Scheduling**
- `/schedule create`, `/schedule list`, `/schedule reschedule`
- **Projects**
- `/projects list`, `/projects create`, `/projects add-helper`
- **Music** (requires **DJ** role)
- `/play`, `/queue`, `/volume`
- **Twitch live notifications** (**Manage Server**)
- `/config addstreamer username:<twitch-name>` — Add a streamer for notifications
- `/config removestreamer username:<twitch-name>` — Remove a streamer
- `/config settwitchnotificationchannel channel:#channel` — Channel for live pings
- **Welcome / onboarding** (**Manage Server**)
- `/config setwelcomechannel channel:#channel` — Channel for welcome messages
- `/config setwelcomemessage message:"..."` — Server welcome template (supports `{user}`, `{server}`)
- `/config setwelcomedm message:"..."` — DM welcome template (supports `{user}`, `{server}`)
- `/config setwelcomegif url:<image-url>` — GIF/image used in welcome embed
- `/config setdefaultrole role:@role` — Role automatically granted on join
## Notes
- Slash commands are registered to the configured `GUILD_ID` on startup.
- Some features require server permissions (e.g. **Manage Server**) or roles (e.g. **DJ** for music).
+7
View File
@@ -30,9 +30,11 @@ func NewBot(token, appID, guildID, lavalinkHost, lavalinkPass string, cmds []*di
// Intents needed for:
// - InteractionCreate (slash commands): Guilds
// - MessageCreate (leveling): GuildMessages
// - GuildMemberAdd (welcome messages): GuildMembers
// - VoiceStateUpdate (meeting lobby): GuildVoiceStates
// - MessageReactionAdd (scheduling via reactions): GuildMessageReactions + DirectMessageReactions
session.Identify.Intents = discordgo.IntentsGuilds |
discordgo.IntentsGuildMembers |
discordgo.IntentsGuildMessages |
discordgo.IntentsGuildVoiceStates |
discordgo.IntentsGuildMessageReactions |
@@ -73,6 +75,10 @@ func (b *Bot) Start() error {
events.HandleMessageCreate(s, m, b.Services)
})
b.Session.AddHandler(func(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
events.HandleGuildMemberAdd(s, m, b.Services)
})
b.Session.AddHandler(func(s *discordgo.Session, vs *discordgo.VoiceStateUpdate) {
events.HandleVoiceStateUpdate(s, vs, b.Services)
music.OnVoiceStateUpdate(vs)
@@ -87,6 +93,7 @@ func (b *Bot) Start() error {
})
events.StartScheduleReminderLoop(b.Session, b.Services)
events.StartTwitchLiveLoop(b.Session, b.Services)
return nil
}
+3
View File
@@ -1,6 +1,7 @@
package commands
import (
"velox-bot/internal/commands/config"
"velox-bot/internal/commands/fun"
"velox-bot/internal/commands/help"
cmdlevel "velox-bot/internal/commands/level"
@@ -34,6 +35,7 @@ var AllCommands = []*discordgo.ApplicationCommand{
timezone.Timezone,
schedule.Schedule,
projects.Projects,
config.Config,
}
var handlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
@@ -56,6 +58,7 @@ var handlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCre
"play": cmdmusic.PlayHandler,
"queue": cmdmusic.QueueHandler,
"volume": cmdmusic.VolumeHandler,
"config": config.ConfigHandler,
}
func HandleInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) {
+472
View File
@@ -0,0 +1,472 @@
package public
import (
"context"
"log"
"strconv"
"strings"
"velox-bot/internal/db/services"
"github.com/bwmarrin/discordgo"
)
var Config = &discordgo.ApplicationCommand{
Name: "config",
Description: "Server configuration",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "addstreamer",
Description: "Add a Twitch streamer for live notifications",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "username",
Description: "Twitch username (without https://)",
Required: true,
},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "removestreamer",
Description: "Remove a Twitch streamer from live notifications",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "username",
Description: "Twitch username to remove",
Required: true,
},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "settwitchnotificationchannel",
Description: "Set the channel for Twitch live notifications",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionChannel,
Name: "channel",
Description: "Text channel for notifications",
Required: true,
ChannelTypes: []discordgo.ChannelType{discordgo.ChannelTypeGuildText, discordgo.ChannelTypeGuildNews},
},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "setwelcomechannel",
Description: "Set the channel for welcome messages",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionChannel,
Name: "channel",
Description: "Text channel for welcomes",
Required: true,
ChannelTypes: []discordgo.ChannelType{discordgo.ChannelTypeGuildText, discordgo.ChannelTypeGuildNews},
},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "setwelcomemessage",
Description: "Set the welcome message template shown in the server",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "message",
Description: "Use {user} where the new member mention should appear",
Required: true,
},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "setwelcomedm",
Description: "Set the welcome DM template sent to new members",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "message",
Description: "Use {user} where the new member mention should appear",
Required: true,
},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "setwelcomegif",
Description: "Set the GIF/image URL used in welcome messages",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "url",
Description: "Direct image or GIF URL",
Required: true,
},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "setdefaultrole",
Description: "Set the default role given to new members",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionRole,
Name: "role",
Description: "Role to assign on join",
Required: true,
},
},
},
},
}
func ConfigHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
if i.GuildID == "" {
respondEphemeral(s, i, "This command can only be used in a server.")
return
}
if i.Member == nil || (i.Member.Permissions&discordgo.PermissionManageGuild) == 0 {
respondEphemeral(s, i, "You need the **Manage Server** permission to use this.")
return
}
if services.Global == nil {
respondEphemeral(s, i, "Configuration services are not available.")
return
}
data := i.ApplicationCommandData()
if len(data.Options) == 0 {
respondEphemeral(s, i, "Missing subcommand.")
return
}
switch data.Options[0].Name {
case "addstreamer":
log.Printf("twitch: /config addstreamer invoked by %s in guild %s", i.Member.User.ID, i.GuildID)
handleAddStreamer(s, i)
case "removestreamer":
log.Printf("twitch: /config removestreamer invoked by %s in guild %s", i.Member.User.ID, i.GuildID)
handleRemoveStreamer(s, i)
case "settwitchnotificationchannel":
log.Printf("twitch: /config settwitchnotificationchannel invoked by %s in guild %s", i.Member.User.ID, i.GuildID)
handleSetChannel(s, i)
case "setwelcomechannel":
log.Printf("welcome: /config setwelcomechannel invoked by %s in guild %s", i.Member.User.ID, i.GuildID)
handleSetWelcomeChannel(s, i)
case "setwelcomemessage":
log.Printf("welcome: /config setwelcomemessage invoked by %s in guild %s", i.Member.User.ID, i.GuildID)
handleSetWelcomeMessage(s, i)
case "setwelcomedm":
log.Printf("welcome: /config setwelcomedm invoked by %s in guild %s", i.Member.User.ID, i.GuildID)
handleSetWelcomeDM(s, i)
case "setwelcomegif":
log.Printf("welcome: /config setwelcomegif invoked by %s in guild %s", i.Member.User.ID, i.GuildID)
handleSetWelcomeGIF(s, i)
case "setdefaultrole":
log.Printf("defaultrole: /config setdefaultrole invoked by %s in guild %s", i.Member.User.ID, i.GuildID)
handleSetDefaultRole(s, i)
default:
respondEphemeral(s, i, "Unknown subcommand.")
}
}
func handleAddStreamer(s *discordgo.Session, i *discordgo.InteractionCreate) {
guildID, err := strconv.ParseInt(i.GuildID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid guild ID.")
return
}
opt := i.ApplicationCommandData().Options[0]
var username string
for _, o := range opt.Options {
if o.Name == "username" {
username = strings.TrimSpace(o.StringValue())
}
}
username = strings.TrimPrefix(username, "https://www.twitch.tv/")
username = strings.TrimPrefix(username, "http://www.twitch.tv/")
username = strings.TrimPrefix(username, "twitch.tv/")
username = strings.TrimSpace(username)
if username == "" {
respondEphemeral(s, i, "Username cannot be empty.")
return
}
if err := services.Global.Twitch.UpsertStreamer(context.Background(), guildID, username); err != nil {
respondEphemeral(s, i, "Failed to add streamer.")
return
}
respondEphemeral(s, i, "Added `"+username+"` for Twitch live notifications.")
}
func handleRemoveStreamer(s *discordgo.Session, i *discordgo.InteractionCreate) {
guildID, err := strconv.ParseInt(i.GuildID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid guild ID.")
return
}
opt := i.ApplicationCommandData().Options[0]
var username string
for _, o := range opt.Options {
if o.Name == "username" {
username = strings.TrimSpace(o.StringValue())
}
}
username = strings.TrimSpace(username)
if username == "" {
respondEphemeral(s, i, "Username cannot be empty.")
return
}
if err := services.Global.Twitch.RemoveStreamer(context.Background(), guildID, username); err != nil {
respondEphemeral(s, i, "Failed to remove streamer.")
return
}
respondEphemeral(s, i, "Removed `"+username+"` from Twitch live notifications.")
}
func handleSetChannel(s *discordgo.Session, i *discordgo.InteractionCreate) {
guildID, err := strconv.ParseInt(i.GuildID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid guild ID.")
return
}
opt := i.ApplicationCommandData().Options[0]
var chOpt *discordgo.ApplicationCommandInteractionDataOption
for _, o := range opt.Options {
if o.Name == "channel" {
chOpt = o
break
}
}
if chOpt == nil {
respondEphemeral(s, i, "Missing channel option.")
return
}
ch := chOpt.ChannelValue(s)
if ch == nil {
respondEphemeral(s, i, "Invalid channel.")
return
}
chID, err := strconv.ParseInt(ch.ID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid channel ID.")
return
}
if err := services.Global.Twitch.SetNotificationChannel(context.Background(), guildID, chID); err != nil {
respondEphemeral(s, i, "Failed to set notification channel.")
return
}
respondEphemeral(s, i, "Twitch notifications will be sent to "+ch.Mention()+".")
}
func handleSetWelcomeChannel(s *discordgo.Session, i *discordgo.InteractionCreate) {
if services.Global.Welcome == nil {
respondEphemeral(s, i, "Welcome configuration is not available.")
return
}
guildID, err := strconv.ParseInt(i.GuildID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid guild ID.")
return
}
opt := i.ApplicationCommandData().Options[0]
var chOpt *discordgo.ApplicationCommandInteractionDataOption
for _, o := range opt.Options {
if o.Name == "channel" {
chOpt = o
break
}
}
if chOpt == nil {
respondEphemeral(s, i, "Missing channel option.")
return
}
ch := chOpt.ChannelValue(s)
if ch == nil {
respondEphemeral(s, i, "Invalid channel.")
return
}
chID, err := strconv.ParseInt(ch.ID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid channel ID.")
return
}
if err := services.Global.Welcome.SetChannel(context.Background(), guildID, chID); err != nil {
respondEphemeral(s, i, "Failed to set welcome channel.")
return
}
respondEphemeral(s, i, "Welcome messages will be sent to "+ch.Mention()+".")
}
func handleSetWelcomeMessage(s *discordgo.Session, i *discordgo.InteractionCreate) {
if services.Global.Welcome == nil {
respondEphemeral(s, i, "Welcome configuration is not available.")
return
}
guildID, err := strconv.ParseInt(i.GuildID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid guild ID.")
return
}
opt := i.ApplicationCommandData().Options[0]
var msg string
for _, o := range opt.Options {
if o.Name == "message" {
msg = strings.TrimSpace(o.StringValue())
}
}
if msg == "" {
respondEphemeral(s, i, "Message cannot be empty.")
return
}
if err := services.Global.Welcome.SetMessage(context.Background(), guildID, msg); err != nil {
respondEphemeral(s, i, "Failed to set welcome message.")
return
}
respondEphemeral(s, i, "Welcome message updated.")
}
func handleSetWelcomeDM(s *discordgo.Session, i *discordgo.InteractionCreate) {
if services.Global.Welcome == nil {
respondEphemeral(s, i, "Welcome configuration is not available.")
return
}
guildID, err := strconv.ParseInt(i.GuildID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid guild ID.")
return
}
opt := i.ApplicationCommandData().Options[0]
var msg string
for _, o := range opt.Options {
if o.Name == "message" {
msg = strings.TrimSpace(o.StringValue())
}
}
if msg == "" {
respondEphemeral(s, i, "DM message cannot be empty.")
return
}
if err := services.Global.Welcome.SetDM(context.Background(), guildID, msg); err != nil {
respondEphemeral(s, i, "Failed to set welcome DM.")
return
}
respondEphemeral(s, i, "Welcome DM updated.")
}
func handleSetWelcomeGIF(s *discordgo.Session, i *discordgo.InteractionCreate) {
if services.Global.Welcome == nil {
respondEphemeral(s, i, "Welcome configuration is not available.")
return
}
guildID, err := strconv.ParseInt(i.GuildID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid guild ID.")
return
}
opt := i.ApplicationCommandData().Options[0]
var url string
for _, o := range opt.Options {
if o.Name == "url" {
url = strings.TrimSpace(o.StringValue())
}
}
if url == "" {
respondEphemeral(s, i, "URL cannot be empty.")
return
}
if err := services.Global.Welcome.SetGIF(context.Background(), guildID, url); err != nil {
respondEphemeral(s, i, "Failed to set welcome GIF.")
return
}
respondEphemeral(s, i, "Welcome GIF updated.")
}
func handleSetDefaultRole(s *discordgo.Session, i *discordgo.InteractionCreate) {
if services.Global.DefaultRole == nil {
respondEphemeral(s, i, "Default role configuration is not available.")
return
}
guildID, err := strconv.ParseInt(i.GuildID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid guild ID.")
return
}
opt := i.ApplicationCommandData().Options[0]
var roleOpt *discordgo.ApplicationCommandInteractionDataOption
for _, o := range opt.Options {
if o.Name == "role" {
roleOpt = o
break
}
}
if roleOpt == nil {
respondEphemeral(s, i, "Missing role option.")
return
}
role := roleOpt.RoleValue(s, i.GuildID)
if role == nil {
respondEphemeral(s, i, "Invalid role.")
return
}
roleID, err := strconv.ParseInt(role.ID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid role ID.")
return
}
if err := services.Global.DefaultRole.SetRole(context.Background(), guildID, roleID); err != nil {
respondEphemeral(s, i, "Failed to set default role.")
return
}
respondEphemeral(s, i, "Default role set to "+role.Mention()+".")
}
func respondEphemeral(s *discordgo.Session, i *discordgo.InteractionCreate, msg string) {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: msg,
Flags: discordgo.MessageFlagsEphemeral,
},
})
}
+14
View File
@@ -0,0 +1,14 @@
package config
import (
"velox-bot/internal/commands/config/public"
"github.com/bwmarrin/discordgo"
)
var (
Config *discordgo.ApplicationCommand = public.Config
)
func ConfigHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.ConfigHandler(s, i) }
+62 -53
View File
@@ -42,80 +42,89 @@ func HelpHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
embed := &discordgo.MessageEmbed{
Title: "Velox Bot Help",
Description: "Here is the help for the bot.",
Description: "Command reference. Some commands require server permissions or roles.",
Color: 0xFFA500,
Fields: []*discordgo.MessageEmbedField{
{
Name: "Fun Commands",
Value: "Small utility + games.",
Name: "Fun",
Value: "" +
"`/ping` — Pong!\n" +
"`/joke [type]` — Random joke (optional category)\n" +
"`/coinflip` — Flip a coin\n" +
"`/dice <expr>` — Roll dice (e.g. `d20`, `2d20+1`, `2#d20+1`)\n" +
"`/rps <hand>` — Rock Paper Scissors (server only)\n" +
"`/rpsstats` — Your RPS score (server only)\n" +
"`/rpsleaderboard` — Top RPS scores (server only)",
},
{
Name: "/ping",
Value: "Pong!",
Name: "Music (requires **DJ** role)",
Value: "" +
"`/play <query>` — Play a song (search or URL)\n" +
"`/queue` — Show the current queue\n" +
"`/volume <0-150>` — Set playback volume",
},
{
Name: "/joke [type]",
Value: "Get a random joke (optional category).",
Name: "Scheduling (1:1 sessions)",
Value: "" +
"`/schedule create user:@someone datetime:\"YYYY-MM-DD HH:MM\" [description]` — Create a session\n" +
"`/schedule list` — List your upcoming sessions\n" +
"`/schedule reschedule id:<id> datetime:\"YYYY-MM-DD HH:MM\"` — Request a new time\n\n" +
"Invites are sent via DM; react with ✅ / ❌ / 🔁 to respond.",
},
{
Name: "/coinflip",
Value: "Flip a coin.",
Name: "Timezone",
Value: "" +
"`/timezone set <IANA>` — Set your timezone (e.g. `Europe/Lisbon`)\n" +
"`/timezone show` — Show your timezone",
},
{
Name: "/dice <expr>",
Value: "Roll dice expressions (e.g. `d20`, `2d20+1`, `2#d20+1`).",
Name: "Projects",
Value: "" +
"`/projects list` — List active projects\n" +
"`/projects create name:\"...\" [description]` — Create a project (**Manage Server**)\n" +
"`/projects add-helper project-id:<id> user:@someone` — Add helper (**Manage Server**)",
},
{
Name: "/rps <hand>",
Value: "Play Rock Paper Scissors (server only).",
Name: "Meetings (**Manage Server**)",
Value: "" +
"`/meeting set-lobby channel:#voice` — Set voice lobby used to create meetings\n" +
"`/meeting lock` — Lock your meeting room\n" +
"`/meeting lock-private` — Lock + hide your meeting room\n" +
"`/meeting unlock` — Unlock your meeting room\n" +
"`/meeting invite user:@someone` — Allow a user to join\n" +
"`/meeting uninvite user:@someone` — Remove a user's permission",
},
{
Name: "/rpsstats",
Value: "Show your RPS score (server only).",
Name: "Leveling",
Value: "" +
"`/slvl toggle` — Enable/disable leveling (**Manage Server**)\n" +
"`/slvl set user:@someone level:<n>` — Set user level (**Manage Server**)\n" +
"`/slvl set-channel channel:#channel` — Level-up message channel (**Manage Server**)\n" +
"`/slvl set-message message:\"...\"` — Custom level-up message (**Manage Server**)\n" +
"`/slvl set-reward level:<n> role:@role` — Role rewards (**Manage Server**)\n" +
"`/rank [user]` — Your rank card (server only)\n" +
"`/leaderboard` — Top levels (server only)\n" +
"`/rewards` — List configured rewards (server only)",
},
{
Name: "/rpsleaderboard",
Value: "Show top RPS scores (server only).",
Name: "Twitch live notifications (**Manage Server**)",
Value: "" +
"`/config addstreamer username:<twitch-name>` — Add a streamer\n" +
"`/config removestreamer username:<twitch-name>` — Remove a streamer\n" +
"`/config settwitchnotificationchannel channel:#channel` — Set notification channel",
},
{
Name: "/help",
Value: "Get help with the bot.",
Name: "Welcome / onboarding (**Manage Server**)",
Value: "" +
"`/config setwelcomechannel channel:#channel` — Channel for welcome messages\n" +
"`/config setwelcomemessage message:\"...\"` — Welcome text (supports `{user}`, `{server}`)\n" +
"`/config setwelcomedm message:\"...\"` — DM template (supports `{user}`, `{server}`)\n" +
"`/config setwelcomegif url:<image-url>` — GIF/image used in welcome embed\n" +
"`/config setdefaultrole role:@role` — Role granted to new members",
},
{
Name: "Leveling System",
Value: `The leveling system is a system that allows users to gain XP and level up. The system is based on the amount of messages sent in a server. The more messages you send, the more XP you gain. The more XP you gain, the higher your level will be. The leveling system is disabled by default, however you can enable/disable it by using the /slvl toggle command. You can also configure the leveling system by using the /slvl command.`,
},
{
Name: "/slvl toggle",
Value: "Toggle the leveling system.",
},
{
Name: "/slvl set",
Value: "Set the level of a user.",
},
{
Name: "/slvl set-channel",
Value: "Set the channel for level-up messages.",
},
{
Name: "/slvl set-message",
Value: "Set the custom level-up message.",
},
{
Name: "/slvl set-reward",
Value: "Set a role reward for a level.",
},
{
Name: "/rank [user]",
Value: "Show a rank card (server only).",
},
{
Name: "/leaderboard",
Value: "Show top levels (server only).",
},
{
Name: "/rewards",
Value: "List configured level rewards (server only).",
Name: "Other",
Value: "`/help` — Show this help in DMs",
},
},
Author: &author,
+13 -6
View File
@@ -14,6 +14,7 @@ type Config struct {
DBHost string
LavalinkHost string
LavalinkPass string
TwitchClientID string
}
func LoadConfig() (*Config, error) {
@@ -44,12 +45,18 @@ func LoadConfig() (*Config, error) {
lavalinkHost := os.Getenv("LAVALINK_HOST")
lavalinkPass := os.Getenv("LAVALINK_PASSWORD")
twitchClientID := os.Getenv("TWITCH_CLIENT_ID")
if twitchClientID == "" {
return nil, fmt.Errorf("TWITCH_CLIENT_ID is not set")
}
return &Config{
BotToken: token,
AppID: appID,
GuildID: guildID,
DBHost: dbHost,
LavalinkHost: lavalinkHost,
LavalinkPass: lavalinkPass,
BotToken: token,
AppID: appID,
GuildID: guildID,
DBHost: dbHost,
LavalinkHost: lavalinkHost,
LavalinkPass: lavalinkPass,
TwitchClientID: twitchClientID,
}, nil
}
+47
View File
@@ -0,0 +1,47 @@
package defaultrolerepo
import (
"context"
"database/sql"
)
type Repo struct {
db *sql.DB
}
func NewRepo(db *sql.DB) *Repo {
return &Repo{db: db}
}
// GetRole returns the configured default role ID for a guild, or 0 if none.
func (r *Repo) GetRole(ctx context.Context, guildID int64) (int64, error) {
const q = `
SELECT role_id
FROM defaultrole
WHERE guild_id = $1
`
var roleID sql.NullInt64
if err := r.db.QueryRowContext(ctx, q, guildID).Scan(&roleID); err != nil {
if err == sql.ErrNoRows {
return 0, nil
}
return 0, err
}
if !roleID.Valid {
return 0, nil
}
return roleID.Int64, nil
}
// SetRole upserts the default role for a guild.
func (r *Repo) SetRole(ctx context.Context, guildID, roleID int64) error {
const q = `
INSERT INTO defaultrole (guild_id, role_id)
VALUES ($1, $2)
ON CONFLICT (guild_id)
DO UPDATE SET role_id = EXCLUDED.role_id
`
_, err := r.db.ExecContext(ctx, q, guildID, roleID)
return err
}
+115
View File
@@ -0,0 +1,115 @@
package twitchrepo
import (
"context"
"database/sql"
)
type Repo struct {
db *sql.DB
}
func NewRepo(db *sql.DB) *Repo {
return &Repo{db: db}
}
func (r *Repo) ListGuilds(ctx context.Context) ([]int64, error) {
const q = `SELECT DISTINCT guild_id FROM twitch`
rows, err := r.db.QueryContext(ctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
var ids []int64
for rows.Next() {
var gid int64
if err := rows.Scan(&gid); err != nil {
return nil, err
}
ids = append(ids, gid)
}
return ids, rows.Err()
}
func (r *Repo) ListUsersForGuild(ctx context.Context, guildID int64) ([]string, error) {
const q = `SELECT twitch_user FROM twitch WHERE guild_id = $1`
rows, err := r.db.QueryContext(ctx, q, guildID)
if err != nil {
return nil, err
}
defer rows.Close()
var users []string
for rows.Next() {
var u string
if err := rows.Scan(&u); err != nil {
return nil, err
}
users = append(users, u)
}
return users, rows.Err()
}
func (r *Repo) GetStatus(ctx context.Context, guildID int64, username string) (string, bool, error) {
const q = `SELECT status FROM twitch WHERE guild_id = $1 AND twitch_user = $2`
var status string
err := r.db.QueryRowContext(ctx, q, guildID, username).Scan(&status)
if err == sql.ErrNoRows {
return "", false, nil
}
if err != nil {
return "", false, err
}
return status, true, nil
}
func (r *Repo) UpsertStreamer(ctx context.Context, guildID int64, username string) error {
const q = `
INSERT INTO twitch (twitch_user, guild_id, status)
VALUES ($1, $2, 'not live')
ON CONFLICT (twitch_user, guild_id)
DO NOTHING
`
_, err := r.db.ExecContext(ctx, q, username, guildID)
return err
}
func (r *Repo) RemoveStreamer(ctx context.Context, guildID int64, username string) error {
const q = `DELETE FROM twitch WHERE guild_id = $1 AND twitch_user = $2`
_, err := r.db.ExecContext(ctx, q, guildID, username)
return err
}
func (r *Repo) UpdateStatus(ctx context.Context, guildID int64, username, status string) error {
const q = `UPDATE twitch SET status = $1 WHERE guild_id = $2 AND twitch_user = $3`
_, err := r.db.ExecContext(ctx, q, status, guildID, username)
return err
}
func (r *Repo) GetNotificationChannel(ctx context.Context, guildID int64) (int64, bool, error) {
const q = `SELECT twitch_channel_id FROM twitch_config WHERE guild_id = $1`
var chID sql.NullInt64
if err := r.db.QueryRowContext(ctx, q, guildID).Scan(&chID); err != nil {
if err == sql.ErrNoRows {
return 0, false, nil
}
return 0, false, err
}
if !chID.Valid {
return 0, false, nil
}
return chID.Int64, true, nil
}
func (r *Repo) SetNotificationChannel(ctx context.Context, guildID, channelID int64) error {
const q = `
INSERT INTO twitch_config (guild_id, twitch_channel_id)
VALUES ($1, $2)
ON CONFLICT (guild_id)
DO UPDATE SET twitch_channel_id = EXCLUDED.twitch_channel_id
`
_, err := r.db.ExecContext(ctx, q, guildID, channelID)
return err
}
+111
View File
@@ -0,0 +1,111 @@
package welcomerepo
import (
"context"
"database/sql"
)
type Repo struct {
db *sql.DB
}
type Settings struct {
GuildID int64
ChannelID int64
WelcomeMessage string
WelcomeDM string
WelcomeGIFURL string
HasConfiguration bool
}
func NewRepo(db *sql.DB) *Repo {
return &Repo{db: db}
}
func (r *Repo) GetSettings(ctx context.Context, guildID int64) (*Settings, error) {
const q = `
SELECT welcome_channel_id, welcome_message, welcome_dm, welcome_gif_url
FROM welcome
WHERE guild_id = $1
`
row := r.db.QueryRowContext(ctx, q, guildID)
var ch sql.NullInt64
var msg, dm, gif sql.NullString
switch err := row.Scan(&ch, &msg, &dm, &gif); err {
case nil:
s := &Settings{
GuildID: guildID,
ChannelID: 0,
WelcomeMessage: "",
WelcomeDM: "",
WelcomeGIFURL: "",
HasConfiguration: true,
}
if ch.Valid {
s.ChannelID = ch.Int64
}
if msg.Valid {
s.WelcomeMessage = msg.String
}
if dm.Valid {
s.WelcomeDM = dm.String
}
if gif.Valid {
s.WelcomeGIFURL = gif.String
}
return s, nil
case sql.ErrNoRows:
return &Settings{
GuildID: guildID,
HasConfiguration: false,
}, nil
default:
return nil, err
}
}
func (r *Repo) UpsertChannel(ctx context.Context, guildID, channelID int64) error {
const q = `
INSERT INTO welcome (guild_id, welcome_channel_id)
VALUES ($1, $2)
ON CONFLICT (guild_id)
DO UPDATE SET welcome_channel_id = EXCLUDED.welcome_channel_id
`
_, err := r.db.ExecContext(ctx, q, guildID, channelID)
return err
}
func (r *Repo) UpsertMessage(ctx context.Context, guildID int64, message string) error {
const q = `
INSERT INTO welcome (guild_id, welcome_message)
VALUES ($1, $2)
ON CONFLICT (guild_id)
DO UPDATE SET welcome_message = EXCLUDED.welcome_message
`
_, err := r.db.ExecContext(ctx, q, guildID, message)
return err
}
func (r *Repo) UpsertDM(ctx context.Context, guildID int64, dm string) error {
const q = `
INSERT INTO welcome (guild_id, welcome_dm)
VALUES ($1, $2)
ON CONFLICT (guild_id)
DO UPDATE SET welcome_dm = EXCLUDED.welcome_dm
`
_, err := r.db.ExecContext(ctx, q, guildID, dm)
return err
}
func (r *Repo) UpsertGIF(ctx context.Context, guildID int64, gifURL string) error {
const q = `
INSERT INTO welcome (guild_id, welcome_gif_url)
VALUES ($1, $2)
ON CONFLICT (guild_id)
DO UPDATE SET welcome_gif_url = EXCLUDED.welcome_gif_url
`
_, err := r.db.ExecContext(ctx, q, guildID, gifURL)
return err
}
@@ -0,0 +1,24 @@
package defaultrole
import (
"context"
"velox-bot/internal/db/repos/defaultrolerepo"
)
type Service struct {
repo *defaultrolerepo.Repo
}
func New(repo *defaultrolerepo.Repo) *Service {
return &Service{repo: repo}
}
func (s *Service) GetRole(ctx context.Context, guildID int64) (int64, error) {
return s.repo.GetRole(ctx, guildID)
}
func (s *Service) SetRole(ctx context.Context, guildID, roleID int64) error {
return s.repo.SetRole(ctx, guildID, roleID)
}
+12 -3
View File
@@ -4,10 +4,13 @@ import (
"velox-bot/internal/db/services/level"
"velox-bot/internal/db/services/levelsettings"
"velox-bot/internal/db/services/meeting"
"velox-bot/internal/db/services/schedule"
"velox-bot/internal/db/services/usersettings"
"velox-bot/internal/db/services/projects"
"velox-bot/internal/db/services/rps"
"velox-bot/internal/db/services/schedule"
"velox-bot/internal/db/services/twitch"
"velox-bot/internal/db/services/usersettings"
"velox-bot/internal/db/services/welcome"
"velox-bot/internal/db/services/defaultrole"
)
type Services struct {
@@ -18,11 +21,14 @@ type Services struct {
UserSettings *usersettings.Service
Projects *projects.Service
RPS *rps.Service
Twitch *twitch.Service
Welcome *welcome.Service
DefaultRole *defaultrole.Service
}
var Global *Services
func NewServices(level *level.Service, levelSettings *levelsettings.Service, meeting *meeting.Service, schedule *schedule.Service, userSettings *usersettings.Service, projects *projects.Service, rps *rps.Service) *Services {
func NewServices(level *level.Service, levelSettings *levelsettings.Service, meeting *meeting.Service, schedule *schedule.Service, userSettings *usersettings.Service, projects *projects.Service, rps *rps.Service, twitchSvc *twitch.Service, welcomeSvc *welcome.Service, defaultRoleSvc *defaultrole.Service) *Services {
s := &Services{
Level: level,
LevelSettings: levelSettings,
@@ -31,6 +37,9 @@ func NewServices(level *level.Service, levelSettings *levelsettings.Service, mee
UserSettings: userSettings,
Projects: projects,
RPS: rps,
Twitch: twitchSvc,
Welcome: welcomeSvc,
DefaultRole: defaultRoleSvc,
}
Global = s
return s
+109
View File
@@ -0,0 +1,109 @@
package twitch
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"velox-bot/internal/db/repos/twitchrepo"
)
type Service struct {
repo *twitchrepo.Repo
client *http.Client
twitchClient string
}
func New(repo *twitchrepo.Repo, twitchClientID string) *Service {
return &Service{
repo: repo,
client: &http.Client{
Timeout: 5 * time.Second,
},
twitchClient: twitchClientID,
}
}
func (s *Service) ListGuilds(ctx context.Context) ([]int64, error) {
return s.repo.ListGuilds(ctx)
}
func (s *Service) ListUsersForGuild(ctx context.Context, guildID int64) ([]string, error) {
return s.repo.ListUsersForGuild(ctx, guildID)
}
func (s *Service) GetStatus(ctx context.Context, guildID int64, username string) (string, bool, error) {
return s.repo.GetStatus(ctx, guildID, username)
}
func (s *Service) UpdateStatus(ctx context.Context, guildID int64, username, status string) error {
return s.repo.UpdateStatus(ctx, guildID, username, status)
}
func (s *Service) UpsertStreamer(ctx context.Context, guildID int64, username string) error {
return s.repo.UpsertStreamer(ctx, guildID, username)
}
func (s *Service) RemoveStreamer(ctx context.Context, guildID int64, username string) error {
return s.repo.RemoveStreamer(ctx, guildID, username)
}
func (s *Service) GetNotificationChannel(ctx context.Context, guildID int64) (int64, bool, error) {
return s.repo.GetNotificationChannel(ctx, guildID)
}
func (s *Service) SetNotificationChannel(ctx context.Context, guildID, channelID int64) error {
return s.repo.SetNotificationChannel(ctx, guildID, channelID)
}
func (s *Service) IsUserStreaming(ctx context.Context, username string) (bool, error) {
type gqlRequest struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
type gqlResponse struct {
Data struct {
User *struct {
Stream *struct {
ID string `json:"id"`
} `json:"stream"`
} `json:"user"`
} `json:"data"`
}
q := fmt.Sprintf(`query { user(login: "%s") { stream { id } } }`, username)
body, err := json.Marshal(gqlRequest{
Query: q,
Variables: map[string]any{},
})
if err != nil {
return false, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://gql.twitch.tv/gql", bytes.NewReader(body))
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Client-Id", s.twitchClient)
resp, err := s.client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
var respBody gqlResponse
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
return false, err
}
if respBody.Data.User == nil {
return false, nil
}
return respBody.Data.User.Stream != nil, nil
}
+36
View File
@@ -0,0 +1,36 @@
package welcome
import (
"context"
"velox-bot/internal/db/repos/welcomerepo"
)
type Service struct {
repo *welcomerepo.Repo
}
func New(repo *welcomerepo.Repo) *Service {
return &Service{repo: repo}
}
func (s *Service) GetSettings(ctx context.Context, guildID int64) (*welcomerepo.Settings, error) {
return s.repo.GetSettings(ctx, guildID)
}
func (s *Service) SetChannel(ctx context.Context, guildID, channelID int64) error {
return s.repo.UpsertChannel(ctx, guildID, channelID)
}
func (s *Service) SetMessage(ctx context.Context, guildID int64, message string) error {
return s.repo.UpsertMessage(ctx, guildID, message)
}
func (s *Service) SetDM(ctx context.Context, guildID int64, dm string) error {
return s.repo.UpsertDM(ctx, guildID, dm)
}
func (s *Service) SetGIF(ctx context.Context, guildID int64, gifURL string) error {
return s.repo.UpsertGIF(ctx, guildID, gifURL)
}
+134
View File
@@ -0,0 +1,134 @@
package events
import (
"context"
"log"
"strconv"
"time"
"velox-bot/internal/db/services"
"github.com/bwmarrin/discordgo"
)
// StartTwitchLiveLoop periodically checks configured Twitch streamers and
// sends live notifications to the configured channel.
func StartTwitchLiveLoop(s *discordgo.Session, svc *services.Services) {
if svc == nil || svc.Twitch == nil || s == nil {
return
}
const (
interval = 30 * time.Second
initialDelay = 10 * time.Second
)
go func() {
time.Sleep(initialDelay)
log.Printf("twitch: starting live notification loop (interval=%s)", interval)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
ctx := context.Background()
guildIDs, err := svc.Twitch.ListGuilds(ctx)
if err != nil {
log.Printf("twitch: failed to list guilds: %v", err)
continue
}
if len(guildIDs) == 0 {
log.Printf("twitch: no guilds with configured streamers")
}
for _, guildID := range guildIDs {
log.Printf("twitch: processing guild %d", guildID)
channelID, ok, err := svc.Twitch.GetNotificationChannel(ctx, guildID)
if err != nil {
log.Printf("twitch: failed to get channel for guild %d: %v", guildID, err)
continue
}
if !ok || channelID == 0 {
log.Printf("twitch: no notification channel configured for guild %d", guildID)
continue
}
users, err := svc.Twitch.ListUsersForGuild(ctx, guildID)
if err != nil {
log.Printf("twitch: failed to list users for guild %d: %v", guildID, err)
continue
}
if len(users) == 0 {
log.Printf("twitch: no streamers configured for guild %d", guildID)
continue
}
chIDStr := strconv.FormatInt(channelID, 10)
for _, username := range users {
log.Printf("twitch: checking live status for guild %d, user %s", guildID, username)
isLive, err := svc.Twitch.IsUserStreaming(ctx, username)
if err != nil {
log.Printf("twitch: failed to check stream for %s: %v", username, err)
continue
}
status, ok, err := svc.Twitch.GetStatus(ctx, guildID, username)
if err != nil {
log.Printf("twitch: failed to get status for %s: %v", username, err)
continue
}
if !ok {
status = "not live"
}
if isLive {
if status == "not live" {
log.Printf("twitch: %s went live in guild %d, sending notification to channel %s", username, guildID, chIDStr)
liveURL := "https://www.twitch.tv/" + username
content := "@everyone"
previewURL := "https://static-cdn.jtvnw.net/previews-ttv/live_user_" + username + "-640x360.jpg"
embed := &discordgo.MessageEmbed{
Title: ":red_circle: " + username + " is now LIVE on Twitch!",
Description: "Click the link below to watch the stream.",
URL: liveURL,
Color: 0x9146FF, // Twitch purple
Image: &discordgo.MessageEmbedImage{
URL: previewURL,
},
}
if _, err := s.ChannelMessageSendComplex(chIDStr, &discordgo.MessageSend{
Content: content,
Embed: embed,
}); err != nil {
log.Printf("twitch: failed to send notification for %s: %v", username, err)
continue
}
if err := svc.Twitch.UpdateStatus(ctx, guildID, username, "live"); err != nil {
log.Printf("twitch: failed to update status to live for %s in guild %d: %v", username, guildID, err)
}
}
} else {
if status != "not live" {
log.Printf("twitch: %s is no longer live in guild %d, updating status", username, guildID)
if err := svc.Twitch.UpdateStatus(ctx, guildID, username, "not live"); err != nil {
log.Printf("twitch: failed to update status to not live for %s in guild %d: %v", username, guildID, err)
}
}
}
}
}
}
}()
}
+133
View File
@@ -0,0 +1,133 @@
package events
import (
"context"
"log"
"strconv"
"strings"
"velox-bot/internal/db/services"
"github.com/bwmarrin/discordgo"
)
// HandleGuildMemberAdd sends a configurable welcome message in a guild channel
// and an optional configurable DM to the joining user.
func HandleGuildMemberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd, svc *services.Services) {
if s == nil || m == nil || svc == nil || svc.Welcome == nil {
return
}
if m.User == nil {
return
}
ctx := context.Background()
guildID := mustParseInt64(m.GuildID)
settings, err := svc.Welcome.GetSettings(ctx, guildID)
if err != nil {
log.Printf("welcome: failed to load settings for guild %s: %v", m.GuildID, err)
return
}
hasWelcomeChannel := settings != nil && settings.ChannelID != 0
memberMention := m.User.Mention()
// Resolve guild/server name and user avatar for placeholders / visuals.
guildName := ""
userAvatarURL := ""
if g, err := s.State.Guild(m.GuildID); err == nil && g != nil {
guildName = g.Name
} else if g, err := s.Guild(m.GuildID); err == nil && g != nil {
guildName = g.Name
}
if m.User.Avatar != "" {
userAvatarURL = discordgo.EndpointUserAvatar(m.User.ID, m.User.Avatar)
}
if hasWelcomeChannel {
// Build guild welcome message.
channelID := strconv.FormatInt(settings.ChannelID, 10)
welcomeMsg := strings.TrimSpace(settings.WelcomeMessage)
if welcomeMsg == "" {
// Default message when none configured.
// "<user>! Welcome to <server>! Have fun!"
welcomeMsg = "<user>! Welcome to <server>! Have fun!"
}
welcomeMsg = strings.ReplaceAll(welcomeMsg, "{user}", memberMention)
if guildName != "" {
welcomeMsg = strings.ReplaceAll(welcomeMsg, "{server}", guildName)
welcomeMsg = strings.ReplaceAll(welcomeMsg, "<server>", guildName)
}
welcomeMsg = strings.ReplaceAll(welcomeMsg, "<user>", memberMention)
embed := &discordgo.MessageEmbed{
Title: "👋 Welcome!",
Description: welcomeMsg,
Color: 0xFFA500,
Footer: &discordgo.MessageEmbedFooter{
Text: "ID: " + m.User.ID,
},
}
if userAvatarURL != "" {
embed.Author = &discordgo.MessageEmbedAuthor{
Name: m.User.Username,
IconURL: userAvatarURL,
}
embed.Thumbnail = &discordgo.MessageEmbedThumbnail{
URL: userAvatarURL,
}
}
gif := strings.TrimSpace(settings.WelcomeGIFURL)
if gif == "" {
// Default GIF when none configured.
gif = "https://images-ext-1.discordapp.net/external/uJ6XfdK2WwDnei3RmNWUqSiOVboC4mK9r78TtgVE_9g/https/media.giphy.com/media/XD9o33QG9BoMis7iM4/giphy.gif"
}
embed.Image = &discordgo.MessageEmbedImage{URL: gif}
if _, err := s.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{
Content: memberMention,
Embed: embed,
}); err != nil {
log.Printf("welcome: failed to send channel welcome in guild %s: %v", m.GuildID, err)
}
}
// Optional DM.
dmText := ""
if settings != nil {
dmText = strings.TrimSpace(settings.WelcomeDM)
}
if dmText == "" {
// Default DM when none configured.
// "Welcome to <server>! Have fun!"
dmText = "Welcome to <server>! Have fun!"
}
dmText = strings.ReplaceAll(dmText, "{user}", memberMention)
if guildName != "" {
dmText = strings.ReplaceAll(dmText, "{server}", guildName)
dmText = strings.ReplaceAll(dmText, "<server>", guildName)
}
dmCh, err := s.UserChannelCreate(m.User.ID)
if err != nil {
log.Printf("welcome: failed to create DM channel for user %s: %v", m.User.ID, err)
return
}
if _, err := s.ChannelMessageSend(dmCh.ID, dmText); err != nil {
log.Printf("welcome: failed to send DM welcome to user %s: %v", m.User.ID, err)
}
// Default role assignment.
if svc.DefaultRole != nil {
roleID, err := svc.DefaultRole.GetRole(ctx, guildID)
if err != nil {
log.Printf("defaultrole: failed to get default role for guild %s: %v", m.GuildID, err)
} else if roleID != 0 {
if err := s.GuildMemberRoleAdd(m.GuildID, m.User.ID, strconv.FormatInt(roleID, 10)); err != nil {
log.Printf("defaultrole: failed to assign role %d to user %s in guild %s: %v", roleID, m.User.ID, m.GuildID, err)
}
}
}
}
+15 -3
View File
@@ -15,15 +15,21 @@ import (
"velox-bot/internal/db/repos/rpsrepo"
"velox-bot/internal/db/repos/schedulerepo"
"velox-bot/internal/db/repos/settingsrepo"
"velox-bot/internal/db/repos/welcomerepo"
"velox-bot/internal/db/repos/twitchrepo"
"velox-bot/internal/db/repos/usersettingsrepo"
"velox-bot/internal/db/repos/defaultrolerepo"
"velox-bot/internal/db/services"
"velox-bot/internal/db/services/level"
"velox-bot/internal/db/services/levelsettings"
"velox-bot/internal/db/services/meeting"
"velox-bot/internal/db/services/schedule"
"velox-bot/internal/db/services/usersettings"
"velox-bot/internal/db/services/projects"
"velox-bot/internal/db/services/rps"
"velox-bot/internal/db/services/schedule"
"velox-bot/internal/db/services/twitch"
"velox-bot/internal/db/services/usersettings"
"velox-bot/internal/db/services/welcome"
"velox-bot/internal/db/services/defaultrole"
)
func main() {
@@ -46,6 +52,9 @@ func main() {
projectsRepo := projectsrepo.NewRepo(db)
scheduleRepo := schedulerepo.NewRepo(db)
userSettingsRepo := usersettingsrepo.NewRepo(db)
welcomeRepo := welcomerepo.NewRepo(db)
twitchRepo := twitchrepo.NewRepo(db)
defaultRoleRepo := defaultrolerepo.NewRepo(db)
levelService := level.New(levelRepo, settingsRepo)
levelSettingsService := levelsettings.New(settingsRepo)
meetingService := meeting.New(settingsRepo)
@@ -53,7 +62,10 @@ func main() {
userSettingsService := usersettings.New(userSettingsRepo)
projectsService := projects.New(projectsRepo)
rpsService := rps.New(rpsRepo)
services := services.NewServices(levelService, levelSettingsService, meetingService, scheduleService, userSettingsService, projectsService, rpsService)
twitchService := twitch.New(twitchRepo, config.TwitchClientID)
welcomeService := welcome.New(welcomeRepo)
defaultRoleService := defaultrole.New(defaultRoleRepo)
services := services.NewServices(levelService, levelSettingsService, meetingService, scheduleService, userSettingsService, projectsService, rpsService, twitchService, welcomeService, defaultRoleService)
bot, err := bot.NewBot(config.BotToken, config.AppID, config.GuildID, config.LavalinkHost, config.LavalinkPass, commands.AllCommands, services)
if err != nil {
-2
View File
@@ -19,8 +19,6 @@ CREATE TABLE IF NOT EXISTS twitch (
CREATE TABLE IF NOT EXISTS levelsettings (
guild_id BIGINT PRIMARY KEY,
levelsys BOOLEAN NOT NULL DEFAULT FALSE,
role BIGINT,
levelreq INT,
message TEXT
);