Author SHA1 Message Date
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
18 changed files with 1277 additions and 14 deletions
+16
View File
@@ -10,6 +10,8 @@ It currently includes:
- Scheduling (1:1 sessions with DM invites + reaction-based accept/decline/reschedule) - Scheduling (1:1 sessions with DM invites + reaction-based accept/decline/reschedule)
- Projects (list/create projects + helpers) - Projects (list/create projects + helpers)
- Music (Lavalink-powered `/play`, `/queue`, `/volume`) - 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 ## Requirements
@@ -34,6 +36,10 @@ Optional (music):
- `LAVALINK_HOST`: Lavalink WebSocket/HTTP host (depends on your Lavalink setup) - `LAVALINK_HOST`: Lavalink WebSocket/HTTP host (depends on your Lavalink setup)
- `LAVALINK_PASSWORD`: Lavalink password - `LAVALINK_PASSWORD`: Lavalink password
Optional (Twitch live notifications):
- `TWITCH_CLIENT_ID`: Twitch Client ID used to query live status
## Database setup ## Database setup
If you use the compose file: If you use the compose file:
@@ -90,6 +96,16 @@ just down # stop postgres
- `/projects list`, `/projects create`, `/projects add-helper` - `/projects list`, `/projects create`, `/projects add-helper`
- **Music** (requires **DJ** role) - **Music** (requires **DJ** role)
- `/play`, `/queue`, `/volume` - `/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 ## Notes
+7
View File
@@ -30,9 +30,11 @@ func NewBot(token, appID, guildID, lavalinkHost, lavalinkPass string, cmds []*di
// Intents needed for: // Intents needed for:
// - InteractionCreate (slash commands): Guilds // - InteractionCreate (slash commands): Guilds
// - MessageCreate (leveling): GuildMessages // - MessageCreate (leveling): GuildMessages
// - GuildMemberAdd (welcome messages): GuildMembers
// - VoiceStateUpdate (meeting lobby): GuildVoiceStates // - VoiceStateUpdate (meeting lobby): GuildVoiceStates
// - MessageReactionAdd (scheduling via reactions): GuildMessageReactions + DirectMessageReactions // - MessageReactionAdd (scheduling via reactions): GuildMessageReactions + DirectMessageReactions
session.Identify.Intents = discordgo.IntentsGuilds | session.Identify.Intents = discordgo.IntentsGuilds |
discordgo.IntentsGuildMembers |
discordgo.IntentsGuildMessages | discordgo.IntentsGuildMessages |
discordgo.IntentsGuildVoiceStates | discordgo.IntentsGuildVoiceStates |
discordgo.IntentsGuildMessageReactions | discordgo.IntentsGuildMessageReactions |
@@ -73,6 +75,10 @@ func (b *Bot) Start() error {
events.HandleMessageCreate(s, m, b.Services) 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) { b.Session.AddHandler(func(s *discordgo.Session, vs *discordgo.VoiceStateUpdate) {
events.HandleVoiceStateUpdate(s, vs, b.Services) events.HandleVoiceStateUpdate(s, vs, b.Services)
music.OnVoiceStateUpdate(vs) music.OnVoiceStateUpdate(vs)
@@ -87,6 +93,7 @@ func (b *Bot) Start() error {
}) })
events.StartScheduleReminderLoop(b.Session, b.Services) events.StartScheduleReminderLoop(b.Session, b.Services)
events.StartTwitchLiveLoop(b.Session, b.Services)
return nil return nil
} }
+3
View File
@@ -1,6 +1,7 @@
package commands package commands
import ( import (
"velox-bot/internal/commands/config"
"velox-bot/internal/commands/fun" "velox-bot/internal/commands/fun"
"velox-bot/internal/commands/help" "velox-bot/internal/commands/help"
cmdlevel "velox-bot/internal/commands/level" cmdlevel "velox-bot/internal/commands/level"
@@ -34,6 +35,7 @@ var AllCommands = []*discordgo.ApplicationCommand{
timezone.Timezone, timezone.Timezone,
schedule.Schedule, schedule.Schedule,
projects.Projects, projects.Projects,
config.Config,
} }
var handlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ 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, "play": cmdmusic.PlayHandler,
"queue": cmdmusic.QueueHandler, "queue": cmdmusic.QueueHandler,
"volume": cmdmusic.VolumeHandler, "volume": cmdmusic.VolumeHandler,
"config": config.ConfigHandler,
} }
func HandleInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { 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) }
+16
View File
@@ -106,6 +106,22 @@ func HelpHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
"`/leaderboard` — Top levels (server only)\n" + "`/leaderboard` — Top levels (server only)\n" +
"`/rewards` — List configured rewards (server only)", "`/rewards` — List configured rewards (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: "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: "Other", Name: "Other",
Value: "`/help` — Show this help in DMs", Value: "`/help` — Show this help in DMs",
+7
View File
@@ -14,6 +14,7 @@ type Config struct {
DBHost string DBHost string
LavalinkHost string LavalinkHost string
LavalinkPass string LavalinkPass string
TwitchClientID string
} }
func LoadConfig() (*Config, error) { func LoadConfig() (*Config, error) {
@@ -44,6 +45,11 @@ func LoadConfig() (*Config, error) {
lavalinkHost := os.Getenv("LAVALINK_HOST") lavalinkHost := os.Getenv("LAVALINK_HOST")
lavalinkPass := os.Getenv("LAVALINK_PASSWORD") 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{ return &Config{
BotToken: token, BotToken: token,
AppID: appID, AppID: appID,
@@ -51,5 +57,6 @@ func LoadConfig() (*Config, error) {
DBHost: dbHost, DBHost: dbHost,
LavalinkHost: lavalinkHost, LavalinkHost: lavalinkHost,
LavalinkPass: lavalinkPass, LavalinkPass: lavalinkPass,
TwitchClientID: twitchClientID,
}, nil }, 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/level"
"velox-bot/internal/db/services/levelsettings" "velox-bot/internal/db/services/levelsettings"
"velox-bot/internal/db/services/meeting" "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/projects"
"velox-bot/internal/db/services/rps" "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 { type Services struct {
@@ -18,11 +21,14 @@ type Services struct {
UserSettings *usersettings.Service UserSettings *usersettings.Service
Projects *projects.Service Projects *projects.Service
RPS *rps.Service RPS *rps.Service
Twitch *twitch.Service
Welcome *welcome.Service
DefaultRole *defaultrole.Service
} }
var Global *Services 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{ s := &Services{
Level: level, Level: level,
LevelSettings: levelSettings, LevelSettings: levelSettings,
@@ -31,6 +37,9 @@ func NewServices(level *level.Service, levelSettings *levelsettings.Service, mee
UserSettings: userSettings, UserSettings: userSettings,
Projects: projects, Projects: projects,
RPS: rps, RPS: rps,
Twitch: twitchSvc,
Welcome: welcomeSvc,
DefaultRole: defaultRoleSvc,
} }
Global = s Global = s
return 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/rpsrepo"
"velox-bot/internal/db/repos/schedulerepo" "velox-bot/internal/db/repos/schedulerepo"
"velox-bot/internal/db/repos/settingsrepo" "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/usersettingsrepo"
"velox-bot/internal/db/repos/defaultrolerepo"
"velox-bot/internal/db/services" "velox-bot/internal/db/services"
"velox-bot/internal/db/services/level" "velox-bot/internal/db/services/level"
"velox-bot/internal/db/services/levelsettings" "velox-bot/internal/db/services/levelsettings"
"velox-bot/internal/db/services/meeting" "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/projects"
"velox-bot/internal/db/services/rps" "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() { func main() {
@@ -46,6 +52,9 @@ func main() {
projectsRepo := projectsrepo.NewRepo(db) projectsRepo := projectsrepo.NewRepo(db)
scheduleRepo := schedulerepo.NewRepo(db) scheduleRepo := schedulerepo.NewRepo(db)
userSettingsRepo := usersettingsrepo.NewRepo(db) userSettingsRepo := usersettingsrepo.NewRepo(db)
welcomeRepo := welcomerepo.NewRepo(db)
twitchRepo := twitchrepo.NewRepo(db)
defaultRoleRepo := defaultrolerepo.NewRepo(db)
levelService := level.New(levelRepo, settingsRepo) levelService := level.New(levelRepo, settingsRepo)
levelSettingsService := levelsettings.New(settingsRepo) levelSettingsService := levelsettings.New(settingsRepo)
meetingService := meeting.New(settingsRepo) meetingService := meeting.New(settingsRepo)
@@ -53,7 +62,10 @@ func main() {
userSettingsService := usersettings.New(userSettingsRepo) userSettingsService := usersettings.New(userSettingsRepo)
projectsService := projects.New(projectsRepo) projectsService := projects.New(projectsRepo)
rpsService := rps.New(rpsRepo) 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) bot, err := bot.NewBot(config.BotToken, config.AppID, config.GuildID, config.LavalinkHost, config.LavalinkPass, commands.AllCommands, services)
if err != nil { if err != nil {
-2
View File
@@ -19,8 +19,6 @@ CREATE TABLE IF NOT EXISTS twitch (
CREATE TABLE IF NOT EXISTS levelsettings ( CREATE TABLE IF NOT EXISTS levelsettings (
guild_id BIGINT PRIMARY KEY, guild_id BIGINT PRIMARY KEY,
levelsys BOOLEAN NOT NULL DEFAULT FALSE, levelsys BOOLEAN NOT NULL DEFAULT FALSE,
role BIGINT,
levelreq INT,
message TEXT message TEXT
); );