diff --git a/bot/bot.go b/bot/bot.go index 58c80e5f..803ca471 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -34,7 +34,6 @@ import ( "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" "github.com/sabafly/gobot/database" - "github.com/sabafly/gobot/ent/migrate" "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" @@ -150,10 +149,7 @@ func run() error { // return fmt.Errorf("cacheを開けません: %w", err) // } - if err := db.Schema.Create(ctx, - migrate.WithForeignKeys(!config.DisableForeignKeys)); err != nil { - return fmt.Errorf("スキーマを定義できません: %w", err) - } + // ent schema migration is removed as we use GORM migration in database.NewDB if _, err := translate.LoadDir(config.TranslateDir); err != nil { return fmt.Errorf("翻訳ファイルが読み込めません path=%s: %w", config.TranslateDir, err) @@ -168,11 +164,11 @@ func run() error { } emoji.SetDefaultRegistry(reg) - component := components.New(ctx, db, *config, gormDB) + component := components.New(ctx, *config, gormDB) component.Version = version component.AddCommands( - debug.Command(component), + debug.Command(component, db), ping.Command(component), message.Command(component), role.Command(component), diff --git a/bot/commands/debug/debug.go b/bot/commands/debug/debug.go index 6de6be2a..6e36f0f8 100644 --- a/bot/commands/debug/debug.go +++ b/bot/commands/debug/debug.go @@ -21,33 +21,23 @@ package debug import ( - "fmt" "iter" "log/slog" "slices" - "strings" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" - "github.com/disgoorg/json/v2" "github.com/disgoorg/omit" "github.com/disgoorg/snowflake/v2" - "github.com/google/uuid" - "github.com/redis/go-redis/v9" - "github.com/sabafly/gobot/bot/commands/debug/db" - "github.com/sabafly/gobot/bot/commands/role" "github.com/sabafly/gobot/bot/components" "github.com/sabafly/gobot/bot/components/generic" "github.com/sabafly/gobot/ent" - "github.com/sabafly/gobot/ent/rolepanelplaced" - "github.com/sabafly/gobot/ent/schema" - "github.com/sabafly/gobot/internal/builtin" "github.com/sabafly/gobot/internal/errors" "github.com/sabafly/gobot/internal/i18n" "github.com/sabafly/gobot/internal/translate" ) -func Command(c *components.Components) *generic.Command { +func Command(c *components.Components, entClient *ent.Client) *generic.Command { return (&generic.Command{ Namespace: "debug", Private: true, @@ -86,28 +76,6 @@ func Command(c *components.Components) *generic.Command { }, }, }, - discord.ApplicationCommandOptionSubCommandGroup{ - Name: "redis", - Description: "redis", - Options: []discord.ApplicationCommandOptionSubCommand{ - { - Name: "import", - Description: "import", - Options: []discord.ApplicationCommandOption{ - discord.ApplicationCommandOptionString{ - Name: "addr", - Description: "address", - Required: true, - }, - discord.ApplicationCommandOptionInt{ - Name: "db", - Description: "db", - Required: true, - }, - }, - }, - }, - }, discord.ApplicationCommandOptionSubCommandGroup{ Name: "guild", Description: "guild", @@ -129,6 +97,16 @@ func Command(c *components.Components) *generic.Command { }, }, }, + discord.ApplicationCommandOptionSubCommandGroup{ + Name: "migration", + Description: "database migration", + Options: []discord.ApplicationCommandOptionSubCommand{ + { + Name: "ent_to_gorm", + Description: "migrate from ent to gorm", + }, + }, + }, }, }, }, @@ -222,122 +200,8 @@ func Command(c *components.Components) *generic.Command { } return nil }), - "/debug/redis/import": generic.CommandHandler(func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - client := redis.NewClient(&redis.Options{ - Addr: event.SlashCommandInteractionData().String("addr"), - DB: event.SlashCommandInteractionData().Int("db"), - }) - // GuildData - guildCmd := client.HGetAll(event, "guild-data") - if err := guildCmd.Err(); err != nil { - return errors.NewError(err) - } - - rpv2Cmd := client.HGetAll(event, "role-panel-v2") - if err := rpv2Cmd.Err(); err != nil { - return errors.NewError(err) - } - - rpv2List := map[uuid.UUID]db.RolePanelV2{} - - for _, v := range rpv2Cmd.Val() { - var rpv2 db.RolePanelV2 - if err := json.Unmarshal([]byte(v), &rpv2); err != nil { - slog.Error("unmarshalに失敗", "err", err) - continue - } - rpv2List[rpv2.ID] = rpv2 - } - - for _, v := range guildCmd.Val() { - var guildData db.GuildData - if err := json.Unmarshal([]byte(v), &guildData); err != nil { - slog.Error("unmarshalに失敗", "err", err) - continue - } - if guildData.DataVersion == nil || *guildData.DataVersion != 11 { - continue - } - - g, err := c.GuildCreateID(event, guildData.ID) - if err != nil { - slog.Error("guild取得に失敗", slog.Any("err", err)) - continue - } - - var createRolePanelBulk []*ent.RolePanelCreate - - for u := range guildData.RolePanelV2 { - rpv2, ok := rpv2List[u] - if !ok { - continue - } - - roles := make([]schema.Role, len(rpv2.Roles)) - - for i, r := range rpv2.Roles { - roles[i] = schema.Role{ - ID: r.RoleID, - Name: r.RoleName, - Emoji: r.Emoji, - } - } - - createRolePanelBulk = append(createRolePanelBulk, - c.DB().RolePanel.Create(). - SetID(rpv2.ID). - SetRoles(roles). - SetName(rpv2.Name). - SetGuild(g). - SetDescription(rpv2.Description), - ) - } - - rolePanels, err := c.DB().RolePanel.CreateBulk(createRolePanelBulk...).Save(event) - if err != nil { - return errors.NewError(err) - } - - placedIDMap := map[[2]snowflake.ID]uuid.UUID{} - for k, u := range guildData.RolePanelV2Placed { - ks := strings.Split(k, "/") - channelID, messageID := snowflake.MustParse(ks[0]), snowflake.MustParse(ks[1]) - placedIDMap[[2]snowflake.ID{channelID, messageID}] = u - } - - for k, u := range placedIDMap { - index := slices.IndexFunc(rolePanels, func(rp *ent.RolePanel) bool { return rp.ID == u }) - if index == -1 { - continue - } - - keyString := fmt.Sprintf("%d/%d", k[0], k[1]) - - placed := c.DB().RolePanelPlaced.Create(). - SetChannelID(k[0]). - SetMessageID(k[1]). - SetType(rolepanelplaced.Type(guildData.RolePanelV2PlacedConfig[keyString].PanelType)). - SetButtonType(builtin.Or(guildData.RolePanelV2PlacedConfig[keyString].ButtonStyle != 0, guildData.RolePanelV2PlacedConfig[keyString].ButtonStyle, 1)). - SetFoldingSelectMenu(guildData.RolePanelV2PlacedConfig[keyString].SimpleSelectMenu). - SetUseDisplayName(guildData.RolePanelV2PlacedConfig[keyString].UseDisplayName). - SetShowName(guildData.RolePanelV2PlacedConfig[keyString].ButtonShowName). - SetHideNotice(guildData.RolePanelV2PlacedConfig[keyString].HideNotice). - SetName(rolePanels[index].Name). - SetDescription(rolePanels[index].Description). - SetRoles(rolePanels[index].Roles). - SetRolePanel(rolePanels[index]). - SetGuild(g). - SaveX(event) - - role.UpdateRolePanel(event, placed, event.Locale(), event.Client()) - } - - } - - if err := event.RespondMessage(discord.NewMessageBuilder().SetContent("OK")); err != nil { - return errors.NewError(err) - } - return nil + "/debug/migration/ent_to_gorm": generic.CommandHandler(func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + return migrateEntToGormHandler(c, entClient, event) }), }, }).SetComponent(c) diff --git a/bot/commands/debug/migration.go b/bot/commands/debug/migration.go new file mode 100644 index 00000000..65fb2d3b --- /dev/null +++ b/bot/commands/debug/migration.go @@ -0,0 +1,273 @@ +package debug + +import ( + "encoding/json" + "log/slog" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" + "github.com/sabafly/gobot/bot/components" + gormModels "github.com/sabafly/gobot/database/models" + "github.com/sabafly/gobot/ent" + "github.com/sabafly/gobot/internal/errors" + "gorm.io/gorm" +) + +func migrateEntToGormHandler(c *components.Components, entClient *ent.Client, event *events.ApplicationCommandInteractionCreate) errors.Error { + if err := event.DeferCreateMessage(false); err != nil { + return errors.NewError(err) + } + + ctx := c.Ctx() + gormDB := c.GormDB() + + if err := gormDB.Transaction(func(tx *gorm.DB) error { + slog.Info("Starting migration...") + + // 1. Users + slog.Info("Migrating Users...") + users, err := entClient.User.Query().All(ctx) + if err != nil { + return err + } + for _, u := range users { + gu := gormModels.User{ + ID: u.ID, + Name: u.Name, + CreatedAt: u.CreatedAt, + Locale: u.Locale, + XP: u.Xp, + } + if err := tx.Save(&gu).Error; err != nil { + return err + } + } + + // 2. Guilds + slog.Info("Migrating Guilds...") + guilds, err := entClient.Guild.Query().WithOwner().All(ctx) + if err != nil { + return err + } + for _, g := range guilds { + gg := gormModels.Guild{ + ID: g.ID, + Name: g.Name, + Locale: g.Locale, + LevelUpMessage: g.LevelUpMessage, + LevelUpChannel: g.LevelUpChannel, + LevelUpExcludeChannel: g.LevelUpExcludeChannel, + LevelMee6Imported: g.LevelMee6Imported, + LevelRole: g.LevelRole, + Permissions: g.Permissions, + RemindCount: g.RemindCount, + RolePanelEditTimes: g.RolePanelEditTimes, + BumpEnabled: g.BumpEnabled, + BumpMessageTitle: g.BumpMessageTitle, + BumpMessage: g.BumpMessage, + BumpRemindMessageTitle: g.BumpRemindMessageTitle, + BumpRemindMessage: g.BumpRemindMessage, + UpEnabled: g.UpEnabled, + UpMessageTitle: g.UpMessageTitle, + UpMessage: g.UpMessage, + UpRemindMessageTitle: g.UpRemindMessageTitle, + UpRemindMessage: g.UpRemindMessage, + BumpMention: g.BumpMention, + UpMention: g.UpMention, + LevelingDisabled: g.LevelingDisabled, + OwnerID: &g.Edges.Owner.ID, + } + if err := tx.Save(&gg).Error; err != nil { + return err + } + } + + // 3. WordSuffix + slog.Info("Migrating WordSuffix...") + wordSuffixes, err := entClient.WordSuffix.Query().WithGuild().WithOwner().All(ctx) + if err != nil { + return err + } + for _, ws := range wordSuffixes { + gws := gormModels.WordSuffix{ + ID: ws.ID, + Suffix: ws.Suffix, + Expired: ws.Expired, + OwnerID: ws.Edges.Owner.ID, + Rule: string(ws.Rule), + } + if ws.Edges.Guild != nil { + gws.GuildID = &ws.Edges.Guild.ID + } + if err := tx.Save(&gws).Error; err != nil { + return err + } + } + + // 4. RolePanel Family + slog.Info("Migrating RolePanel...") + rolePanels, err := entClient.RolePanel.Query().WithGuild().All(ctx) + if err != nil { + return err + } + for _, rp := range rolePanels { + grp := gormModels.RolePanel{ + ID: rp.ID, + Name: rp.Name, + Description: rp.Description, + UpdatedAt: rp.UpdatedAt, + AppliedAt: rp.AppliedAt, + GuildID: rp.Edges.Guild.ID, + } + grp.Roles = make([]gormModels.Role, len(rp.Roles)) + for i, r := range rp.Roles { + grp.Roles[i] = gormModels.Role{ID: r.ID, Name: r.Name, Emoji: r.Emoji} + } + if err := tx.Save(&grp).Error; err != nil { + return err + } + } + + slog.Info("Migrating RolePanelEdit...") + rpEdits, err := entClient.RolePanelEdit.Query().WithGuild().WithParent().All(ctx) + if err != nil { + return err + } + for _, rpe := range rpEdits { + grpe := gormModels.RolePanelEdit{ + ID: rpe.ID, + ChannelID: rpe.ChannelID, + EmojiAuthor: rpe.EmojiAuthor, + Token: rpe.Token, + SelectedRole: rpe.SelectedRole, + Modified: rpe.Modified, + Name: rpe.Name, + Description: rpe.Description, + GuildID: rpe.Edges.Guild.ID, + ParentID: rpe.Edges.Parent.ID, + } + grpe.Roles = make([]gormModels.Role, len(rpe.Roles)) + for i, r := range rpe.Roles { + grpe.Roles[i] = gormModels.Role{ID: r.ID, Name: r.Name, Emoji: r.Emoji} + } + if err := tx.Save(&grpe).Error; err != nil { + return err + } + } + + slog.Info("Migrating RolePanelPlaced...") + rpPlaced, err := entClient.RolePanelPlaced.Query().WithGuild().WithRolePanel().All(ctx) + if err != nil { + return err + } + for _, rpp := range rpPlaced { + grpp := gormModels.RolePanelPlaced{ + ID: rpp.ID, + MessageID: rpp.MessageID, + ChannelID: rpp.ChannelID, + Type: string(rpp.Type), + ButtonType: rpp.ButtonType, + ShowName: rpp.ShowName, + FoldingSelectMenu: rpp.FoldingSelectMenu, + HideNotice: rpp.HideNotice, + UseDisplayName: rpp.UseDisplayName, + CreatedAt: rpp.CreatedAt, + Uses: rpp.Uses, + Name: rpp.Name, + Description: rpp.Description, + UpdatedAt: rpp.UpdatedAt, + GuildID: rpp.Edges.Guild.ID, + RolePanelID: rpp.Edges.RolePanel.ID, + } + grpp.Roles = make([]gormModels.Role, len(rpp.Roles)) + for i, r := range rpp.Roles { + grpp.Roles[i] = gormModels.Role{ID: r.ID, Name: r.Name, Emoji: r.Emoji} + } + if err := tx.Save(&grpp).Error; err != nil { + return err + } + } + + // 5. MessagePin + slog.Info("Migrating MessagePin...") + pins, err := entClient.MessagePin.Query().WithGuild().All(ctx) + if err != nil { + return err + } + for _, p := range pins { + gmp := gormModels.MessagePin{ + ID: p.ID, + GuildID: p.Edges.Guild.ID, + ChannelID: p.ChannelID, + Content: p.Content, + Embeds: p.Embeds, + BeforeID: p.BeforeID, + } + if rlData, err := json.Marshal(p.RateLimit); err == nil { + var gormRL gormModels.RateLimit + if err := json.Unmarshal(rlData, &gormRL); err == nil { + gmp.RateLimit = gormRL + } + } + if err := tx.Save(&gmp).Error; err != nil { + return err + } + } + + // 6. MessageRemind + slog.Info("Migrating MessageRemind...") + reminds, err := entClient.MessageRemind.Query().WithGuild().All(ctx) + if err != nil { + return err + } + for _, r := range reminds { + gmr := gormModels.MessageRemind{ + ID: r.ID, + GuildID: r.Edges.Guild.ID, + ChannelID: r.ChannelID, + AuthorID: r.AuthorID, + Time: r.Time, + Content: r.Content, + Name: r.Name, + } + if err := tx.Save(&gmr).Error; err != nil { + return err + } + } + + // 7. Members + slog.Info("Migrating Members...") + members, err := entClient.Member.Query().WithGuild().WithUser().All(ctx) + if err != nil { + return err + } + for _, m := range members { + gm := gormModels.Member{ + GuildID: m.Edges.Guild.ID, + UserID: m.Edges.User.ID, + Permission: m.Permission, + XP: m.Xp, + LastXP: m.LastXp, + MessageCount: m.MessageCount, + LastNotifiedLevel: m.LastNotifiedLevel, + LastMessageHashes: m.LastMessageHashes, + } + if err := tx.Save(&gm).Error; err != nil { + return err + } + } + + return nil + }); err != nil { + slog.Error("Migration failed", slog.Any("err", err)) + return errors.NewError(err) + } + + if err := event.RespondMessage( + discord.NewMessageBuilder(). + SetContent("Migration complete!"), + ); err != nil { + return errors.NewError(err) + } + return nil +} diff --git a/bot/commands/level/level.go b/bot/commands/level/level.go index 9c04cf7a..37211684 100644 --- a/bot/commands/level/level.go +++ b/bot/commands/level/level.go @@ -21,37 +21,20 @@ package level import ( - "cmp" "context" - "crypto/sha1" - "encoding/hex" - "encoding/json" - "fmt" "log/slog" - "math/rand/v2" - "net/http" - "slices" "strconv" "strings" "time" - "entgo.io/ent/dialect/sql" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" - "github.com/disgoorg/disgo/events" "github.com/disgoorg/snowflake/v2" - "github.com/sabafly/gobot/bot/commands/gopoint" "github.com/sabafly/gobot/bot/components" "github.com/sabafly/gobot/bot/components/generic" - "github.com/sabafly/gobot/ent" - "github.com/sabafly/gobot/ent/member" + "github.com/sabafly/gobot/database/models" "github.com/sabafly/gobot/internal/builtin" - "github.com/sabafly/gobot/internal/discordutil" - "github.com/sabafly/gobot/internal/embeds" - "github.com/sabafly/gobot/internal/errors" - "github.com/sabafly/gobot/internal/smap" - "github.com/sabafly/gobot/internal/translate" - "github.com/sabafly/gobot/internal/xppoint" + "gorm.io/gorm" ) func Command(c *components.Components) components.Command { @@ -252,809 +235,153 @@ func Command(c *components.Components) components.Command { Permission: []generic.Permission{ generic.PermissionDefaultString("level.required-point"), }, - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - mem, err := c.MemberCreate(event, event.User(), *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - level := uint64(0) - l, ok := event.SlashCommandInteractionData().OptInt("level") - level = uint64(l) - if !ok { - level = mem.Xp.Level() + 1 - } - builder := discord.NewMessageBuilder() - builder.SetEmbeds( - embeds.SetEmbedProperties(discord.NewEmbedBuilder(). - SetTitle(translate.Message(event.Locale(), "components.level.required-point.embed.title", translate.WithTemplate(map[string]any{"Level": level}))). - SetDescriptionf("# `%d`xp\n%s\n%s", - xppoint.TotalPoint(level), - translate.Message(event.Locale(), "components.level.required-point.embed.description", translate.WithTemplate(map[string]any{"User": event.Member().EffectiveName(), "Xp": mem.Xp})), - translate.Message(event.Locale(), "components.level.required-point.embed.description.diff", translate.WithTemplate(map[string]any{"Xp": builtin.Or(xppoint.TotalPoint(level) > uint64(mem.Xp), xppoint.TotalPoint(level)-uint64(mem.Xp), 0)})), - ). - Build()), - ) - if err := event.CreateMessage(builder.BuildCreate()); err != nil { - return errors.NewError(err) - } - return nil - }, + CommandHandler: requiredPointHandler, }, "/level/rank": generic.PCommandHandler{ Permission: []generic.Permission{ generic.PermissionDefaultString("level.rank"), }, - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - target, ok := event.SlashCommandInteractionData().OptMember("target") - if !ok { - target = *event.Member() - } - if target.User.Bot || target.User.System { - return errors.NewError(errors.ErrorMessage("errors.invalid.bot.target", event)) - } - m, err := c.MemberCreate(event, target.User, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - gl, err := c.GuildRequest(event.Client(), g.ID) - if err != nil { - return errors.NewError(err) - } - members, err := g.QueryMembers().Order( - member.ByXp( - sql.OrderDesc(), - ), - ).All(event) - if err != nil { - return errors.NewError(err) - } - ids := make([]int, len(members)) - for i, m := range slices.All(members) { - ids[i] = m.ID - } - index := slices.Index(ids, m.ID) - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetEmbeds( - embeds.SetEmbedProperties( - levelMessage(g, gl, m, index, target.Member, event), - ), - ). - BuildCreate(), - ); err != nil { - return errors.NewError(err) - } - return nil - }, + CommandHandler: rankHandler, }, "/level/leaderboard": generic.PCommandHandler{ Permission: []generic.Permission{ generic.PermissionDefaultString("level.leaderboard"), }, - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - const pageCount = 25 - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - gl, err := c.GuildRequest(event.Client(), g.ID) - if err != nil { - return errors.NewError(err) - } - page := event.SlashCommandInteractionData().Int("page") - page = builtin.Or(page > 0, page, 1) - count := g.QueryMembers().CountX(event) - if page > count/pageCount+1 { - return errors.NewError(errors.ErrorMessage("errors.invalid.page", event)) - } - members := g.QueryMembers(). - Order(member.ByXp(sql.OrderDesc())). - Offset((page - 1) * pageCount). - Limit(pageCount). - AllX(event) - var leaderboard string - for i, m := range members { - leaderboard += fmt.Sprintf("**#%d | %s XP: `%d` Level: `%d`**\n", - i+1+((page-1)*pageCount), - discord.UserMention(m.UserID), - m.Xp, m.Xp.Level(), - ) - } - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetEmbeds( - embeds.SetEmbedProperties( - discord.NewEmbedBuilder(). - SetEmbedAuthor( - &discord.EmbedAuthor{ - Name: g.Name, - IconURL: builtin.NonNil(gl.IconURL()), - }, - ). - SetTitlef("🏆%s(%d/%d)", - translate.Message(event.Locale(), "components.level.leaderboard.title"), - page, - count/pageCount+1, - ). - SetDescription(leaderboard). - Build(), - ), - ). - BuildCreate(), - ); err != nil { - return errors.NewError(err) - } - return nil - }, + CommandHandler: leaderboardHandler, }, "/level/transfer": generic.PCommandHandler{ Permission: []generic.Permission{ generic.PermissionString("level.transfer"), }, - DiscordPerm: discord.PermissionManageGuild.Add(discord.PermissionModerateMembers), - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - gl, err := c.GuildRequest(event.Client(), g.ID) - if err != nil { - return errors.NewError(err) - } - - to := event.SlashCommandInteractionData().Member("to") - from, ok := event.SlashCommandInteractionData().OptMember("from") - if !ok { - from = *event.Member() - } - if from.User.Bot || from.User.System || to.User.Bot || to.User.System { - return errors.NewError(errors.ErrorMessage("errors.invalid.bot.target", event)) - } - if to.User.ID == from.User.ID { - return errors.NewError(errors.ErrorMessage("errors.invalid.self.target", event)) - } - - fromUser, err := c.MemberCreate(event, from.User, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - toUser, err := c.MemberCreate(event, to.User, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - movedXp := uint64(fromUser.Xp) - fromUser.Xp = xppoint.XP(0) - fromUser = fromUser.Update().SetXp(fromUser.Xp).ClearLastNotifiedLevel().SaveX(event) - if toUser, err = addXp(event, toUser.Update(), movedXp, event.Client(), toUser, g, event.Channel().ID(), to.EffectiveName(), true); err != nil { - return errors.NewError(err) - } - ids := g.QueryMembers().Order( - member.ByXp( - sql.OrderDesc(), - ), - ).IDsX(event) - fromIndex := slices.Index(ids, fromUser.ID) - toIndex := slices.Index(ids, toUser.ID) - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetEmbeds( - embeds.SetEmbedsProperties( - []discord.Embed{ - levelMessage(g, gl, fromUser, fromIndex, from.Member, event), - levelMessage(g, gl, toUser, toIndex, to.Member, event), - discord.NewEmbedBuilder(). - SetTitlef("`%d`xp 移動しました", movedXp). - Build(), - }, - )..., - ). - BuildCreate(), - ); err != nil { - return errors.NewError(err) - } - return nil - }, + DiscordPerm: discord.PermissionManageGuild.Add(discord.PermissionModerateMembers), + CommandHandler: transferHandler, }, "/level/up/message": generic.PCommandHandler{ Permission: []generic.Permission{ generic.PermissionString("level.up.message"), }, - DiscordPerm: discord.PermissionManageGuild, - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - if err := event.Modal( - discord.NewModalCreateBuilder(). - SetTitle(translate.Message(event.Locale(), "components.level.up.message.modal.title")). - SetCustomID("level:up_message_modal"). - SetComponents( - discord.NewLabel(translate.Message(event.Locale(), "components.level.up.message.modal.input.message"), - discord.TextInputComponent{ - CustomID: "message", - Style: discord.TextInputStyleParagraph, - MinLength: builtin.Ptr(1), - MaxLength: 140, - Required: true, - Placeholder: translate.Message(event.Locale(), "components.level.up.message.modal.input.message.placeholder"), - Value: g.LevelUpMessage, - }, - ), - ). - Build(), - ); err != nil { - return errors.NewError(err) - } - return nil - }, + DiscordPerm: discord.PermissionManageGuild, + CommandHandler: upMessageHandler, }, "/level/up/message-channel": generic.PCommandHandler{ Permission: []generic.Permission{ generic.PermissionString("level.message-channel"), }, - DiscordPerm: discord.PermissionManageGuild, - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - if channel, ok := event.SlashCommandInteractionData().OptChannel("channel"); ok { - g = g.Update(). - SetLevelUpChannel(channel.ID). - SaveX(event) - } else { - g = g.Update(). - ClearLevelUpChannel(). - SaveX(event) - } - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetContent(translate.Message(event.Locale(), "components.level.up.message-channel.message", - translate.WithTemplate(map[string]any{ - "Channel": builtin.Or(g.LevelUpChannel != nil, - discord.ChannelMention(builtin.NonNil(g.LevelUpChannel)), - translate.Message(event.Locale(), "components.level.up.message-channel.default"), - ), - }), - )). - BuildCreate(), - ); err != nil { - return errors.NewError(err) - } - return nil - }, + DiscordPerm: discord.PermissionManageGuild, + CommandHandler: upMessageChannelHandler, }, "/level/exclude-channel/add": generic.PCommandHandler{ Permission: []generic.Permission{ generic.PermissionString("level.exclude-channel.add"), }, - DiscordPerm: discord.PermissionManageGuild, - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - channel := event.SlashCommandInteractionData().Channel("channel") - if slices.Contains(g.LevelUpExcludeChannel, channel.ID) { - return errors.NewError(errors.ErrorMessage("errors.already_exist", event)) - } - g.LevelUpExcludeChannel = append(g.LevelUpExcludeChannel, channel.ID) - g.Update(). - SetLevelUpExcludeChannel(g.LevelUpExcludeChannel). - ExecX(event) - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetContent(translate.Message(event.Locale(), "components.level.exclude-channel.add.message", - translate.WithTemplate(map[string]any{"Channel": discord.ChannelMention(channel.ID)}), - )). - BuildCreate(), - ); err != nil { - return errors.NewError(err) - } - return nil - }, + DiscordPerm: discord.PermissionManageGuild, + CommandHandler: excludeChannelAddHandler, }, "/level/exclude-channel/remove": generic.PCommandHandler{ Permission: []generic.Permission{ generic.PermissionString("level.exclude-channel.remove"), }, - DiscordPerm: discord.PermissionManageGuild, - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - channel := event.SlashCommandInteractionData().Channel("channel") - index := slices.Index(g.LevelUpExcludeChannel, channel.ID) - if index == -1 { - return errors.NewError(errors.ErrorMessage("errors.not_exist", event)) - } - g.LevelUpExcludeChannel = slices.Delete(g.LevelUpExcludeChannel, index, index+1) - g.Update(). - SetLevelUpExcludeChannel(g.LevelUpExcludeChannel). - ExecX(event) - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetContent(translate.Message(event.Locale(), "components.level.exclude-channel.remove.message", - translate.WithTemplate(map[string]any{"Channel": discord.ChannelMention(channel.ID)}), - )). - BuildCreate(), - ); err != nil { - return errors.NewError(err) - } - return nil - }, + DiscordPerm: discord.PermissionManageGuild, + CommandHandler: excludeChannelRemoveHandler, }, "/level/exclude-channel/clear": generic.PCommandHandler{ Permission: []generic.Permission{ generic.PermissionString("level.exclude-channel.clear"), }, - DiscordPerm: discord.PermissionManageGuild, - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - g.Update(). - ClearLevelUpExcludeChannel(). - ExecX(event) - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetContent(translate.Message(event.Locale(), "components.level.exclude-channel.clear.message")). - BuildCreate(), - ); err != nil { - return errors.NewError(err) - } - return nil - }, + DiscordPerm: discord.PermissionManageGuild, + CommandHandler: excludeChannelClearHandler, }, "/level/import-mee6": generic.PCommandHandler{ Permission: []generic.Permission{ generic.PermissionString("level.import-mee6"), }, - DiscordPerm: discord.PermissionManageGuild, - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - - if g.LevelMee6Imported { - return errors.NewError(errors.ErrorMessage("components.level.import-mee6.message.already", event)) - } - - var members []discord.Member - memberCount := 1000 - afterID := snowflake.ID(0) - for memberCount == 1000 { - m, err := event.Client().Rest.GetMembers(*event.GuildID(), memberCount, afterID) - if err != nil { - return errors.NewError(err) - } - memberCount = len(m) - members = append(members, m...) - afterID = m[len(m)-1].User.ID - } - - slog.Info("mee6インポート", slog.Any("gid", event.GuildID()), slog.Int("member_count", len(members))) - - memberCount = 0 - url := fmt.Sprintf("https://mee6.xyz/api/plugins/levels/leaderboard/%s", event.GuildID().String()) - for page := 0; true; page++ { - response, err := http.Get(fmt.Sprintf("%s?page=%d", url, page)) - if err != nil || response.StatusCode != http.StatusOK { - switch response.StatusCode { - case http.StatusUnauthorized: - if err := event.RespondMessage( - discord.NewMessageBuilder(). - SetContent( - fmt.Sprintf("# FAILED\n```| STATUS CODE | %d\n| RESPONSE | %v```%s", - response.StatusCode, - err, - translate.Message(event.Locale(), "components.level.import-mee6.message.unauthorized", - translate.WithTemplate(map[string]any{"GuildID": *event.GuildID()}), - ), - ), - ), - ); err != nil { - return errors.NewError(err) - } - return nil - default: - if err := event.RespondMessage( - discord.NewMessageBuilder(). - SetContent(fmt.Sprintf("# FAILED\n```| STATUS CODE | %d\n| RESPONSE | %v```", response.StatusCode, err)), - ); err != nil { - return errors.NewError(err) - } - return nil - } - } - var leaderboard mee6LeaderBoard - if err := json.NewDecoder(response.Body).Decode(&leaderboard); err != nil { - return errors.NewError(err) - } - _ = response.Body.Close() - if len(leaderboard.Players) < 1 { - break - } - for _, player := range leaderboard.Players { - index := slices.IndexFunc(members, func(m discord.Member) bool { return m.User.ID == player.ID }) - if index == -1 { - continue - } - slog.Info("mee6メンバーインポート", slog.Any("gid", event.GuildID()), slog.Any("member_id", player.ID)) - m, err := c.MemberCreate(event, members[index].User, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - m.Update().SetXp(xppoint.XP(player.Xp)).ExecX(event) - memberCount++ - } - } - - g.Update().SetLevelMee6Imported(true).ExecX(event) - - if err := event.RespondMessage( - discord.NewMessageBuilder(). - SetContent(fmt.Sprintf("# SUCCEED\n```| IMPORTED MEMBER COUNT | %d```", memberCount)), - ); err != nil { - return errors.NewError(err) - } - return nil - }, + DiscordPerm: discord.PermissionManageGuild, + CommandHandler: importMee6Handler, }, "/level/reset": generic.PCommandHandler{ Permission: []generic.Permission{ generic.PermissionString("level.reset"), }, - DiscordPerm: discord.PermissionManageGuild, - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - target := event.SlashCommandInteractionData().Member("target") - m, err := c.MemberCreate(event, target.User, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - m.Update().SetXp(xppoint.XP(0)).ExecX(event) - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetContent(translate.Message(event.Locale(), "components.level.reset.message", - translate.WithTemplate(map[string]any{"User": discord.UserMention(target.User.ID)}), - )). - BuildCreate(), - ); err != nil { - return errors.NewError(err) - } - return nil - }, + DiscordPerm: discord.PermissionManageGuild, + CommandHandler: resetHandler, }, "/level/exclude-channel/list": generic.PCommandHandler{ Permission: []generic.Permission{ generic.PermissionString("level.exclude-channel.list"), }, - DiscordPerm: discord.PermissionManageGuild, - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - var listStr string - for i, id := range g.LevelUpExcludeChannel { - listStr += fmt.Sprintf("%d. %s\n", i+1, discord.ChannelMention(id)) - } - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetEmbeds( - embeds.SetEmbedProperties( - discord.NewEmbedBuilder(). - SetTitle(translate.Message(event.Locale(), "components.level.exclude-channel.list.message")). - SetDescription( - builtin.Or(listStr != "", - listStr, - "- "+translate.Message(event.Locale(), "components.level.exclude-channel.list.message.none"), - ), - ). - Build(), - ), - ). - BuildCreate(), - ); err != nil { - return errors.NewError(err) - } - return nil - }, + DiscordPerm: discord.PermissionManageGuild, + CommandHandler: excludeChannelListHandler, }, "/level/role/set": generic.PCommandHandler{ Permission: []generic.Permission{ generic.PermissionString("level.role.set"), }, - DiscordPerm: discord.PermissionManageGuild, - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - if len(g.LevelRole) >= 20 { - return errors.NewError(errors.ErrorMessage("errors.create.reach_max", event)) - } - level := event.SlashCommandInteractionData().Int("level") - role := event.SlashCommandInteractionData().Role("role") - g.LevelRole = builtin.NonNilMap(g.LevelRole) - g.LevelRole[level] = role.ID - self, valid := event.Client().Caches.SelfMember(*event.GuildID()) - if !valid { - return errors.NewError(errors.ErrorMessage("errors.invalid.self", event)) - } - var roles []discord.Role - for _, id := range self.RoleIDs { - role, ok := event.Client().Caches.Role(*event.GuildID(), id) - if !ok { - continue - } - roles = append(roles, role) - } - highestRole := discordutil.GetHighestRole(roles) - if highestRole == nil { - return errors.NewError(errors.ErrorMessage("errors.invalid.self", event)) - } - - if role.Managed || role.Compare(*highestRole) != -1 || role.ID == *event.GuildID() { - return errors.NewError(errors.ErrorMessage("errors.invalid.role", event)) - } - - g.Update(). - SetLevelRole(g.LevelRole). - ExecX(event) - - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetEmbeds( - embeds.SetEmbedProperties( - discord.NewEmbedBuilder(). - SetTitle(translate.Message(event.Locale(), "components.level.role.set.message.embed.title")). - SetDescription( - translate.Message(event.Locale(), "components.level.role.set.message.embed.description", - translate.WithTemplate(map[string]any{ - "Level": strconv.Itoa(level), - "Role": discord.RoleMention(role.ID), - }), - ), - ). - Build(), - ), - ). - BuildCreate(), - ); err != nil { - return errors.NewError(err) - } - return nil - }, + DiscordPerm: discord.PermissionManageGuild, + CommandHandler: roleSetHandler, }, "/level/role/list": generic.PCommandHandler{ Permission: []generic.Permission{ generic.PermissionString("level.role.list"), }, - DiscordPerm: discord.PermissionManageGuild, - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - g.LevelRole = builtin.NonNilMap(g.LevelRole) - var listStr string - for k, v := range smap.MakeSortMap(g.LevelRole).Iter(cmp.Compare[int]) { - listStr += "- " + translate.Message(event.Locale(), "components.level.role.list.message", - translate.WithTemplate(map[string]any{ - "Level": strconv.Itoa(k), - "Role": discord.RoleMention(v), - }), - ) + "\n" - } - if err := event.RespondMessage( - discord.NewMessageBuilder(). - SetEmbeds( - embeds.SetEmbedProperties( - discord.NewEmbedBuilder(). - SetTitle(translate.Message(event.Locale(), "components.level.role.list.message.embed.title")). - SetDescription( - builtin.Or(listStr != "", - listStr, - translate.Message(event.Locale(), "components.level.role.list.message.none"), - ), - ). - Build(), - ), - ), - ); err != nil { - return errors.NewError(err) - } - return nil - }, + DiscordPerm: discord.PermissionManageGuild, + CommandHandler: roleListHandler, }, "/level/role/remove": generic.PCommandHandler{ Permission: []generic.Permission{ generic.PermissionString("level.role.remove"), }, - DiscordPerm: discord.PermissionManageGuild, - CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - g.LevelRole = builtin.NonNilMap(g.LevelRole) - level := event.SlashCommandInteractionData().Int("level") - r, ok := g.LevelRole[level] - if !ok { - return errors.NewError(errors.ErrorMessage("errors.not_exist", event)) - } - delete(g.LevelRole, level) - - g.Update(). - SetLevelRole(g.LevelRole). - ExecX(event) - - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetEmbeds( - embeds.SetEmbedProperties( - discord.NewEmbedBuilder(). - SetTitle(translate.Message(event.Locale(), "components.level.role.remove.message.embed.title")). - SetDescription( - translate.Message(event.Locale(), "components.level.role.remove.message.embed.description", - translate.WithTemplate(map[string]any{ - "Level": strconv.Itoa(level), - "Role": discord.RoleMention(r), - }), - ), - ). - Build(), - ), - ). - BuildCreate(), - ); err != nil { - return errors.NewError(err) - } - return nil - }, + DiscordPerm: discord.PermissionManageGuild, + CommandHandler: roleRemoveHandler, }, }, ModalHandlers: map[string]generic.ModalHandler{ - "level:up_message_modal": func(c *components.Components, event *events.ModalSubmitInteractionCreate) errors.Error { - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - g = g.Update(). - SetLevelUpMessage(event.ModalSubmitInteraction.Data.Text("message")). - SaveX(event) - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetEmbeds( - embeds.SetEmbedProperties( - discord.NewEmbedBuilder(). - SetTitle(translate.Message(event.Locale(), "components.level.up.message.message")). - SetDescription(g.LevelUpMessage). - Build(), - ), - ). - BuildCreate(), - ); err != nil { - return nil - } - return nil - }, - }, - EventHandler: func(c *components.Components, event bot.Event) errors.Error { - switch event := event.(type) { - case *events.GuildMessageCreate: - if event.Message.Author.Bot || event.Message.Author.System || event.Message.Type.System() { - return nil - } - if event.Message.Type != discord.MessageTypeDefault && event.Message.Type != discord.MessageTypeReply { - return nil - } - g, err := c.GuildCreateID(event, event.GuildID) - if err != nil { - return errors.NewError(err) - } - if g.LevelingDisabled { - return nil - } - if slices.Contains(g.LevelUpExcludeChannel, event.ChannelID) { - return nil - } - var channel discord.GuildChannel - channel, ok := event.Channel() - if !ok { - c, err := event.Client().Rest.GetChannel(event.ChannelID) - if err != nil { - return errors.NewError(err) - } - channel, _ = c.(discord.GuildChannel) - } - if channel.ParentID() != nil && slices.Contains(g.LevelUpExcludeChannel, *channel.ParentID()) { - return nil - } - m, err := c.MemberCreate(event, event.Message.Author, event.GuildID) - if err != nil { - return errors.NewError(err) - } - hash := sha1.Sum([]byte(event.Message.Content)) - hashStr := hex.EncodeToString(hash[:]) - if slices.Contains(m.LastMessageHashes, hashStr) { - return nil - } - if len(m.LastMessageHashes) >= 10 { - m.LastMessageHashes = slices.Delete(m.LastMessageHashes, 0, 1) - } - m.LastMessageHashes = append(m.LastMessageHashes, hashStr) - m.Update(). - SetLastMessageHashes(m.LastMessageHashes). - SaveX(event) - - if _, err = addXp(event, m.Update(), rand.N[uint64](16)+15, event.Client(), m, g, event.ChannelID, event.Message.Author.EffectiveName(), false); err != nil { - return errors.NewError(err) - } - - if err := gopoint.AddPoint(c, m.UserID, g.ID, rand.Int64N(2)*50); err != nil { - slog.Error("ポイント追加に失敗", slog.Any("err", err), slog.Any("user_id", m.UserID), slog.Any("guild_id", g.ID)) - } - - } - return nil + "level:up_message_modal": upMessageModalHandler, }, + EventHandler: eventHandler, }).SetComponent(c) } -func addXp(ctx context.Context, memberUpdate *ent.MemberUpdateOne, xp uint64, client *bot.Client, m *ent.Member, g *ent.Guild, channelID snowflake.ID, username string, ignoreCooldown bool) (*ent.Member, error) { - before := builtin.NonNilOrDefault(m.LastNotifiedLevel, m.Xp.Level()) - if ignoreCooldown || time.Now().After(m.LastXp.Add(time.Minute*3)) { - m.Xp.Add(xp) - memberUpdate. - SetXp(m.Xp). - SetLastXp(time.Now()) +func addXp(ctx context.Context, xp uint64, client *bot.Client, m *models.Member, g *models.Guild, channelID snowflake.ID, username string, ignoreCooldown bool, db *gorm.DB) (*models.Member, error) { + before := builtin.NonNilOrDefault(m.LastNotifiedLevel, m.XP.Level()) + if ignoreCooldown || time.Now().After(m.LastXP.Add(time.Minute*3)) { + m.XP.Add(xp) + m.LastXP = time.Now() + } + after := m.XP.Level() + m.LastNotifiedLevel = &after + m.MessageCount++ + + if err := db.Save(m).Error; err != nil { + return m, err } - after := m.Xp.Level() - m = memberUpdate. - SetLastNotifiedLevel(after). - SetMessageCount(m.MessageCount + 1). - SaveX(ctx) + if before < after { - for i := range after - before { - if err := levelUp(g, before+i+1, client, g.ID, m); err != nil { - return m, err - } + for i := uint64(0); i < after-before; i++ { + _ = levelUp(g, before+i+1, client, g.ID, m) } - // レベルアップ通知 content := g.LevelUpMessage content = strings.ReplaceAll(content, "{user}", discord.UserMention(m.UserID)) content = strings.ReplaceAll(content, "{username}", username) content = strings.ReplaceAll(content, "{before_level}", strconv.FormatUint(before, 10)) content = strings.ReplaceAll(content, "{after_level}", strconv.FormatUint(after, 10)) - content = strings.ReplaceAll(content, "{xp}", strconv.FormatUint(uint64(m.Xp), 10)) - if _, err := client.Rest. - CreateMessage( - builtin.Or(builtin.NonNil(g.LevelUpChannel) != 0, builtin.NonNil(g.LevelUpChannel), channelID), - discord.NewMessageBuilder(). - SetContent(content). - BuildCreate(), - ); err != nil { + content = strings.ReplaceAll(content, "{xp}", strconv.FormatUint(uint64(m.XP), 10)) + + targetChannel := channelID + if g.LevelUpChannel != nil && *g.LevelUpChannel != 0 { + targetChannel = *g.LevelUpChannel + } + + if _, err := client.Rest.CreateMessage(targetChannel, discord.NewMessageBuilder().SetContent(content).BuildCreate()); err != nil { return m, err } } return m, nil } -func levelUp(g *ent.Guild, after uint64, client *bot.Client, guildID snowflake.ID, m *ent.Member) error { - // レベルロール - r, ok := g.LevelRole[int(after)] +func levelUp(g *models.Guild, after uint64, client *bot.Client, guildID snowflake.ID, m *models.Member) error { + rID, ok := g.LevelRole[int(after)] if ok { - if err := client.Rest.AddMemberRole(guildID, m.UserID, r); err != nil { + if err := client.Rest.AddMemberRole(guildID, m.UserID, rID); err != nil { slog.Error("レベルロール付与に失敗", slog.Any("err", err)) } } diff --git a/bot/commands/level/level_components.go b/bot/commands/level/level_components.go new file mode 100644 index 00000000..375c8a78 --- /dev/null +++ b/bot/commands/level/level_components.go @@ -0,0 +1,116 @@ +/* + * gobot -- a useful discord bot + * + * Copyright (C) 2024 Sabafly Developers + * + * 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 . + * + */ + +package level + +import ( + "crypto/sha1" + "encoding/hex" + "log/slog" + "math/rand/v2" + "slices" + + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" + "github.com/sabafly/gobot/bot/commands/gopoint" + "github.com/sabafly/gobot/bot/components" + "github.com/sabafly/gobot/internal/embeds" + "github.com/sabafly/gobot/internal/errors" + "github.com/sabafly/gobot/internal/translate" +) + +func upMessageModalHandler(c *components.Components, event *events.ModalSubmitInteractionCreate) errors.Error { + g, err := c.GuildCreateID(event, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + g.LevelUpMessage = event.Data.Text("message") + if err := c.GormDB().Save(g).Error; err != nil { + return errors.NewError(err) + } + + embed := discord.NewEmbedBuilder(). + SetTitle(translate.Message(event.Locale(), "components.level.up.message.message")). + SetDescription(g.LevelUpMessage). + Build() + + if err := event.CreateMessage( + discord.NewMessageBuilder(). + SetEmbeds(embeds.SetEmbedProperties(embed)). + BuildCreate(), + ); err != nil { + return errors.NewError(err) + } + return nil +} + +func eventHandler(c *components.Components, event bot.Event) errors.Error { + switch event := event.(type) { + case *events.GuildMessageCreate: + if event.Message.Author.Bot || event.Message.Author.System || event.Message.Type.System() { + return nil + } + if event.Message.Type != discord.MessageTypeDefault && event.Message.Type != discord.MessageTypeReply { + return nil + } + g, err := c.GuildCreateID(event, event.GuildID) + if err != nil || g.LevelingDisabled || slices.Contains(g.LevelUpExcludeChannel, event.ChannelID) { + return nil + } + var channel discord.GuildChannel + ch, ok := event.Channel() + if !ok { + ch2, _ := event.Client().Rest.GetChannel(event.ChannelID) + channel, _ = ch2.(discord.GuildChannel) + } else { + channel = ch + } + if channel != nil && channel.ParentID() != nil && slices.Contains(g.LevelUpExcludeChannel, *channel.ParentID()) { + return nil + } + m, err := c.MemberCreate(event, event.Message.Author, event.GuildID) + if err != nil { + return errors.NewError(err) + } + hash := sha1.Sum([]byte(event.Message.Content)) + hashStr := hex.EncodeToString(hash[:]) + if slices.Contains(m.LastMessageHashes, hashStr) { + return nil + } + if len(m.LastMessageHashes) >= 10 { + m.LastMessageHashes = slices.Delete(m.LastMessageHashes, 0, 1) + } + m.LastMessageHashes = append(m.LastMessageHashes, hashStr) + if err := c.GormDB().Save(m).Error; err != nil { + return errors.NewError(err) + } + + if _, err = addXp(event, rand.N[uint64](16)+15, event.Client(), m, g, event.ChannelID, event.Message.Author.EffectiveName(), false, c.GormDB()); err != nil { + return errors.NewError(err) + } + + if err := gopoint.AddPoint(c, m.UserID, g.ID, rand.Int64N(2)*50); err != nil { + slog.Error("ポイント追加に失敗", slog.Any("err", err), slog.Any("user_id", m.UserID), slog.Any("guild_id", g.ID)) + } + + } + return nil +} diff --git a/bot/commands/level/level_handlers.go b/bot/commands/level/level_handlers.go new file mode 100644 index 00000000..bc92ba02 --- /dev/null +++ b/bot/commands/level/level_handlers.go @@ -0,0 +1,600 @@ +/* + * gobot -- a useful discord bot + * + * Copyright (C) 2024 Sabafly Developers + * + * 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 . + * + */ + +package level + +import ( + "cmp" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "slices" + "strconv" + "time" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/snowflake/v2" + "github.com/sabafly/gobot/bot/components" + "github.com/sabafly/gobot/database/models" + "github.com/sabafly/gobot/internal/builtin" + "github.com/sabafly/gobot/internal/discordutil" + "github.com/sabafly/gobot/internal/embeds" + "github.com/sabafly/gobot/internal/errors" + "github.com/sabafly/gobot/internal/smap" + "github.com/sabafly/gobot/internal/translate" + "github.com/sabafly/gobot/internal/xppoint" + "gorm.io/gorm" +) + +func requiredPointHandler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + mem, err := c.MemberCreate(event, event.User(), *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + levelNum := uint64(0) + if l, ok := event.SlashCommandInteractionData().OptInt("level"); ok { + levelNum = uint64(l) + } else { + levelNum = mem.XP.Level() + 1 + } + embed := discord.NewEmbedBuilder(). + SetTitle(translate.Message(event.Locale(), "components.level.required-point.embed.title", translate.WithTemplate(map[string]any{"Level": levelNum}))). + SetDescriptionf("# `%d`xp\n%s\n%s", + xppoint.TotalPoint(levelNum), + translate.Message(event.Locale(), "components.level.required-point.embed.description", translate.WithTemplate(map[string]any{"User": event.Member().EffectiveName(), "Xp": mem.XP})), + translate.Message(event.Locale(), "components.level.required-point.embed.description.diff", translate.WithTemplate(map[string]any{"Xp": builtin.Or(xppoint.TotalPoint(levelNum) > uint64(mem.XP), xppoint.TotalPoint(levelNum)-uint64(mem.XP), 0)})), + ). + Build() + + if err := event.CreateMessage(discord.NewMessageBuilder().SetEmbeds(embeds.SetEmbedProperties(embed)).BuildCreate()); err != nil { + return errors.NewError(err) + } + return nil +} + +func rankHandler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + g, err := c.GuildCreateID(event, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + target, ok := event.SlashCommandInteractionData().OptMember("target") + if !ok { + target = *event.Member() + } + if target.User.Bot || target.User.System { + return errors.NewError(errors.ErrorMessage("errors.invalid.bot.target", event)) + } + m, err := c.MemberCreate(event, target.User, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + gl, err := c.GuildRequest(event.Client(), g.ID) + if err != nil { + return errors.NewError(err) + } + + var members []models.Member + if err := c.GormDB().Where("guild_id = ?", g.ID).Order("xp desc").Find(&members).Error; err != nil { + return errors.NewError(err) + } + + ids := make([]int, len(members)) + for i, mem := range members { + ids[i] = mem.ID + } + index := slices.Index(ids, m.ID) + + embed := levelMessage(g, gl, m, index, target.Member, event) + + if err := event.CreateMessage(discord.NewMessageBuilder().SetEmbeds(embeds.SetEmbedProperties(embed)).BuildCreate()); err != nil { + return errors.NewError(err) + } + return nil +} + +func leaderboardHandler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + const pageCount = 25 + g, err := c.GuildCreateID(event, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + gl, err := c.GuildRequest(event.Client(), g.ID) + if err != nil { + return errors.NewError(err) + } + page := event.SlashCommandInteractionData().Int("page") + if page < 1 { + page = 1 + } + + var count int64 + c.GormDB().Model(&models.Member{}).Where("guild_id = ?", g.ID).Count(&count) + + if int64(page) > (count+pageCount-1)/pageCount { + return errors.NewError(errors.ErrorMessage("errors.invalid.page", event)) + } + + var members []models.Member + c.GormDB().Where("guild_id = ?", g.ID).Order("xp desc").Offset((page - 1) * pageCount).Limit(pageCount).Find(&members) + + var leaderboard string + for i, m := range members { + leaderboard += fmt.Sprintf("**#%d | %s XP: `%d` Level: `%d`**\n", + i+1+((page-1)*pageCount), + discord.UserMention(m.UserID), + m.XP, m.XP.Level(), + ) + } + + embed := discord.NewEmbedBuilder(). + SetEmbedAuthor( + &discord.EmbedAuthor{ + Name: g.Name, + IconURL: builtin.NonNil(gl.IconURL()), + }, + ). + SetTitlef("🏆%s(%d/%d)", + translate.Message(event.Locale(), "components.level.leaderboard.title"), + page, + (count+pageCount-1)/pageCount, + ). + SetDescription(leaderboard). + Build() + + if err := event.CreateMessage(discord.NewMessageBuilder().SetEmbeds(embeds.SetEmbedProperties(embed)).BuildCreate()); err != nil { + return errors.NewError(err) + } + return nil +} + +func transferHandler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + g, err := c.GuildCreateID(event, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + gl, err := c.GuildRequest(event.Client(), g.ID) + if err != nil { + return errors.NewError(err) + } + + to := event.SlashCommandInteractionData().Member("to") + from, ok := event.SlashCommandInteractionData().OptMember("from") + if !ok { + from = *event.Member() + } + if from.User.Bot || from.User.System || to.User.Bot || to.User.System { + return errors.NewError(errors.ErrorMessage("errors.invalid.bot.target", event)) + } + if to.User.ID == from.User.ID { + return errors.NewError(errors.ErrorMessage("errors.invalid.self.target", event)) + } + + fromUser, err := c.MemberCreate(event, from.User, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + toUser, err := c.MemberCreate(event, to.User, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + movedXp := uint64(fromUser.XP) + fromUser.XP = xppoint.XP(0) + fromUser.LastNotifiedLevel = nil + + if err := c.GormDB().Transaction(func(tx *gorm.DB) error { + if err := tx.Save(fromUser).Error; err != nil { + return err + } + + var err error + if toUser, err = addXp(event, movedXp, event.Client(), toUser, g, event.Channel().ID(), to.EffectiveName(), true, tx); err != nil { + return err + } + return nil + }); err != nil { + return errors.NewError(err) + } + + var ids []int + c.GormDB().Model(&models.Member{}).Where("guild_id = ?", g.ID).Order("xp desc").Pluck("id", &ids) + + fromIndex, toIndex := slices.Index(ids, fromUser.ID), slices.Index(ids, toUser.ID) + + embedsList := []discord.Embed{ + levelMessage(g, gl, fromUser, fromIndex, from.Member, event), + levelMessage(g, gl, toUser, toIndex, to.Member, event), + discord.NewEmbedBuilder().SetTitlef("`%d`xp 移動しました", movedXp).Build(), + } + + if err := event.CreateMessage(discord.NewMessageBuilder().SetEmbeds(embeds.SetEmbedsProperties(embedsList)...).BuildCreate()); err != nil { + return errors.NewError(err) + } + return nil +} + +func upMessageHandler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + g, err := c.GuildCreateID(event, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + + actionRow := discord.NewLabel( + translate.Message(event.Locale(), "components.level.up.message.modal.input.message"), + discord.TextInputComponent{ + CustomID: "message", + Style: discord.TextInputStyleParagraph, + MinLength: builtin.Ptr(1), + MaxLength: 140, + Required: true, + Placeholder: translate.Message(event.Locale(), "components.level.up.message.modal.input.message.placeholder"), + Value: g.LevelUpMessage, + }, + ) + + if err := event.Modal( + discord.NewModalCreateBuilder(). + SetTitle(translate.Message(event.Locale(), "components.level.up.message.modal.title")). + SetCustomID("level:up_message_modal"). + SetComponents(actionRow). + Build(), + ); err != nil { + return errors.NewError(err) + } + return nil +} + +func upMessageChannelHandler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + g, err := c.GuildCreateID(event, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + if channel, ok := event.SlashCommandInteractionData().OptChannel("channel"); ok { + g.LevelUpChannel = &channel.ID + } else { + g.LevelUpChannel = nil + } + c.GormDB().Save(g) + + var channelText string + if g.LevelUpChannel != nil { + channelText = discord.ChannelMention(builtin.NonNil(g.LevelUpChannel)) + } else { + channelText = translate.Message(event.Locale(), "components.level.up.message-channel.default") + } + + content := translate.Message(event.Locale(), "components.level.up.message-channel.message", + translate.WithTemplate(map[string]any{ + "Channel": channelText, + }), + ) + + if err := event.CreateMessage( + discord.NewMessageBuilder(). + SetContent(content). + BuildCreate(), + ); err != nil { + return errors.NewError(err) + } + return nil +} + +func excludeChannelAddHandler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + g, err := c.GuildCreateID(event, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + channel := event.SlashCommandInteractionData().Channel("channel") + if slices.Contains(g.LevelUpExcludeChannel, channel.ID) { + return errors.NewError(errors.ErrorMessage("errors.already_exist", event)) + } + g.LevelUpExcludeChannel = append(g.LevelUpExcludeChannel, channel.ID) + c.GormDB().Save(g) + + content := translate.Message(event.Locale(), "components.level.exclude-channel.add.message", + translate.WithTemplate(map[string]any{"Channel": discord.ChannelMention(channel.ID)}), + ) + + if err := event.CreateMessage( + discord.NewMessageBuilder(). + SetContent(content). + BuildCreate(), + ); err != nil { + return errors.NewError(err) + } + return nil +} + +func excludeChannelRemoveHandler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + g, err := c.GuildCreateID(event, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + channel := event.SlashCommandInteractionData().Channel("channel") + index := slices.Index(g.LevelUpExcludeChannel, channel.ID) + if index == -1 { + return errors.NewError(errors.ErrorMessage("errors.not_exist", event)) + } + g.LevelUpExcludeChannel = slices.Delete(g.LevelUpExcludeChannel, index, index+1) + c.GormDB().Save(g) + + content := translate.Message(event.Locale(), "components.level.exclude-channel.remove.message", + translate.WithTemplate(map[string]any{"Channel": discord.ChannelMention(channel.ID)}), + ) + + if err := event.CreateMessage( + discord.NewMessageBuilder(). + SetContent(content). + BuildCreate(), + ); err != nil { + return errors.NewError(err) + } + return nil +} + +func excludeChannelClearHandler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + g, err := c.GuildCreateID(event, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + g.LevelUpExcludeChannel = []snowflake.ID{} + c.GormDB().Save(g) + + content := translate.Message(event.Locale(), "components.level.exclude-channel.clear.message") + + if err := event.CreateMessage( + discord.NewMessageBuilder(). + SetContent(content). + BuildCreate(), + ); err != nil { + return errors.NewError(err) + } + return nil +} + +func excludeChannelListHandler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + g, err := c.GuildCreateID(event, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + var listStr string + for i, id := range g.LevelUpExcludeChannel { + listStr += fmt.Sprintf("%d. %s\n", i+1, discord.ChannelMention(id)) + } + + embed := discord.NewEmbedBuilder(). + SetTitle(translate.Message(event.Locale(), "components.level.exclude-channel.list.message")). + SetDescription(builtin.Or(listStr != "", listStr, "- "+translate.Message(event.Locale(), "components.level.exclude-channel.list.message.none"))). + Build() + + if err := event.CreateMessage(discord.NewMessageBuilder().SetEmbeds(embeds.SetEmbedProperties(embed)).BuildCreate()); err != nil { + return errors.NewError(err) + } + return nil +} + +func importMee6Handler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + g, err := c.GuildCreateID(event, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + + if g.LevelMee6Imported { + return errors.NewError(errors.ErrorMessage("components.level.import-mee6.message.already", event)) + } + + var discordMembers []discord.Member + afterID := snowflake.ID(0) + for { + m, err := event.Client().Rest.GetMembers(*event.GuildID(), 1000, afterID) + if err != nil { + return errors.NewError(err) + } + discordMembers = append(discordMembers, m...) + if len(m) < 1000 { + break + } + afterID = m[len(m)-1].User.ID + } + + slog.Info("mee6インポート", slog.Any("gid", event.GuildID()), slog.Int("member_count", len(discordMembers))) + + importedCount := 0 + url := fmt.Sprintf("https://mee6.xyz/api/plugins/levels/leaderboard/%s", event.GuildID().String()) + client := &http.Client{Timeout: 10 * time.Second} + for page := 0; true; page++ { + response, err := client.Get(fmt.Sprintf("%s?page=%d", url, page)) + if err != nil || response.StatusCode != http.StatusOK { + sc := 0 + if response != nil { + sc = response.StatusCode + _ = response.Body.Close() + } + if sc == http.StatusUnauthorized { + return errors.NewError(errors.ErrorMessage("components.level.import-mee6.message.unauthorized", event)) + } + if importedCount > 0 { + break + } + if err != nil { + return errors.NewError(err) + } + return errors.NewError(fmt.Errorf("mee6 API error: %d", sc)) + } + var leaderboard struct { + Players []struct { + ID string `json:"id"` + Xp int64 `json:"xp"` + } `json:"players"` + } + if err := json.NewDecoder(response.Body).Decode(&leaderboard); err != nil { + _ = response.Body.Close() + return errors.NewError(err) + } + _ = response.Body.Close() + if len(leaderboard.Players) < 1 { + break + } + for _, player := range leaderboard.Players { + pID, _ := snowflake.Parse(player.ID) + idx := slices.IndexFunc(discordMembers, func(m discord.Member) bool { return m.User.ID == pID }) + if idx != -1 { + m, _ := c.MemberCreate(event, discordMembers[idx].User, *event.GuildID()) + m.XP = xppoint.XP(player.Xp) + c.GormDB().Save(m) + importedCount++ + } + } + } + + g.LevelMee6Imported = true + c.GormDB().Save(g) + + if err := event.RespondMessage(discord.NewMessageBuilder().SetContent(fmt.Sprintf("# SUCCEED\n```| IMPORTED MEMBER COUNT | %d```", importedCount))); err != nil { + return errors.NewError(err) + } + return nil +} + +func resetHandler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + target := event.SlashCommandInteractionData().Member("target") + m, err := c.MemberCreate(event, target.User, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + m.XP = xppoint.XP(0) + c.GormDB().Save(m) + + content := translate.Message(event.Locale(), "components.level.reset.message", + translate.WithTemplate(map[string]any{"User": discord.UserMention(target.User.ID)}), + ) + + if err := event.CreateMessage( + discord.NewMessageBuilder(). + SetContent(content). + BuildCreate(), + ); err != nil { + return errors.NewError(err) + } + return nil +} + +func roleSetHandler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + g, err := c.GuildCreateID(event, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + if len(g.LevelRole) >= 20 { + return errors.NewError(errors.ErrorMessage("errors.create.reach_max", event)) + } + levelNum := event.SlashCommandInteractionData().Int("level") + role := event.SlashCommandInteractionData().Role("role") + g.LevelRole = builtin.NonNilMap(g.LevelRole) + g.LevelRole[levelNum] = role.ID + self, valid := event.Client().Caches.SelfMember(*event.GuildID()) + if !valid { + return errors.NewError(errors.ErrorMessage("errors.invalid.self", event)) + } + var roles []discord.Role + for _, id := range self.RoleIDs { + if r, ok := event.Client().Caches.Role(*event.GuildID(), id); ok { + roles = append(roles, r) + } + } + highestRole := discordutil.GetHighestRole(roles) + if highestRole == nil || role.Managed || role.Compare(*highestRole) != -1 || role.ID == *event.GuildID() { + return errors.NewError(errors.ErrorMessage("errors.invalid.role", event)) + } + + c.GormDB().Save(g) + + embed := discord.NewEmbedBuilder(). + SetTitle(translate.Message(event.Locale(), "components.level.role.set.message.embed.title")). + SetDescription( + translate.Message(event.Locale(), "components.level.role.set.message.embed.description", + translate.WithTemplate(map[string]any{ + "Level": strconv.Itoa(levelNum), + "Role": discord.RoleMention(role.ID), + }), + ), + ). + Build() + + if err := event.CreateMessage(discord.NewMessageBuilder().SetEmbeds(embeds.SetEmbedProperties(embed)).BuildCreate()); err != nil { + return errors.NewError(err) + } + return nil +} + +func roleRemoveHandler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + g, err := c.GuildCreateID(event, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + g.LevelRole = builtin.NonNilMap(g.LevelRole) + levelNum := event.SlashCommandInteractionData().Int("level") + rID, ok := g.LevelRole[levelNum] + if !ok { + return errors.NewError(errors.ErrorMessage("errors.not_exist", event)) + } + delete(g.LevelRole, levelNum) + + c.GormDB().Save(g) + + embed := discord.NewEmbedBuilder(). + SetTitle(translate.Message(event.Locale(), "components.level.role.remove.message.embed.title")). + SetDescription( + translate.Message(event.Locale(), "components.level.role.remove.message.embed.description", + translate.WithTemplate(map[string]any{ + "Level": strconv.Itoa(levelNum), + "Role": discord.RoleMention(rID), + }), + ), + ). + Build() + + if err := event.CreateMessage(discord.NewMessageBuilder().SetEmbeds(embeds.SetEmbedProperties(embed)).BuildCreate()); err != nil { + return errors.NewError(err) + } + return nil +} + +func roleListHandler(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { + g, err := c.GuildCreateID(event, *event.GuildID()) + if err != nil { + return errors.NewError(err) + } + g.LevelRole = builtin.NonNilMap(g.LevelRole) + var listStr string + for k, v := range smap.MakeSortMap(g.LevelRole).Iter(cmp.Compare[int]) { + listStr += "- " + translate.Message(event.Locale(), "components.level.role.list.message", translate.WithTemplate(map[string]any{"Level": strconv.Itoa(k), "Role": discord.RoleMention(v)})) + "\n" + } + + embed := discord.NewEmbedBuilder(). + SetTitle(translate.Message(event.Locale(), "components.level.role.list.message.embed.title")). + SetDescription(builtin.Or(listStr != "", listStr, translate.Message(event.Locale(), "components.level.role.list.message.none"))). + Build() + + if err := event.RespondMessage(discord.NewMessageBuilder().SetEmbeds(embeds.SetEmbedProperties(embed))); err != nil { + return errors.NewError(err) + } + return nil +} diff --git a/bot/commands/level/level_message.go b/bot/commands/level/level_message.go index c4614765..34345145 100644 --- a/bot/commands/level/level_message.go +++ b/bot/commands/level/level_message.go @@ -24,16 +24,16 @@ import ( "fmt" "github.com/disgoorg/disgo/discord" - "github.com/sabafly/gobot/ent" + "github.com/sabafly/gobot/database/models" "github.com/sabafly/gobot/internal/builtin" "github.com/sabafly/gobot/internal/translate" "github.com/sabafly/gobot/internal/xppoint" ) func levelMessage( - g *ent.Guild, + g *models.Guild, gl *discord.Guild, - m *ent.Member, + m *models.Member, index int, member discord.Member, event interface { @@ -60,8 +60,8 @@ func levelMessage( ). SetDescription("## "+translate.Message(event.Locale(), "components.level.rank.embed.description", translate.WithTemplate(map[string]any{ - "Level": m.Xp.Level(), - "Xp": m.Xp, + "Level": m.XP.Level(), + "Xp": m.XP, }), )). SetFields( @@ -72,11 +72,11 @@ func levelMessage( }, discord.EmbedField{ Name: translate.Message(event.Locale(), "components.level.rank.embed.fields.next_level", - translate.WithTemplate(map[string]any{"NextLevel": m.Xp.Level() + 1}), + translate.WithTemplate(map[string]any{"NextLevel": m.XP.Level() + 1}), ), Value: fmt.Sprintf("`%d`xp / `%d`xp", - xppoint.RequiredPoint(m.Xp.Level())-(xppoint.TotalPoint(m.Xp.Level()+1)-uint64(m.Xp)), - xppoint.RequiredPoint(m.Xp.Level()), + xppoint.RequiredPoint(m.XP.Level())-(xppoint.TotalPoint(m.XP.Level()+1)-uint64(m.XP)), + xppoint.RequiredPoint(m.XP.Level()), ), Inline: builtin.Ptr(true), }, diff --git a/bot/commands/message/message.go b/bot/commands/message/message.go index 804e728e..33cc0a4a 100644 --- a/bot/commands/message/message.go +++ b/bot/commands/message/message.go @@ -28,24 +28,23 @@ import ( "strconv" "strings" "time" + "unicode" "github.com/disgoorg/disgo/rest" "github.com/disgoorg/snowflake/v2" + "gorm.io/gorm" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" "github.com/sabafly/gobot/bot/components" "github.com/sabafly/gobot/bot/components/generic" - "github.com/sabafly/gobot/ent" - "github.com/sabafly/gobot/ent/guild" - "github.com/sabafly/gobot/ent/messagepin" - "github.com/sabafly/gobot/ent/messageremind" - "github.com/sabafly/gobot/ent/wordsuffix" + "github.com/sabafly/gobot/database/models" "github.com/sabafly/gobot/internal/builtin" "github.com/sabafly/gobot/internal/errors" "github.com/sabafly/gobot/internal/parse" "github.com/sabafly/gobot/internal/translate" + "github.com/sabafly/gobot/internal/uuidv7" ) const ( @@ -58,6 +57,12 @@ const ( PinArgumentTypeDuration1w ) +const ( + WordSuffixRuleWebhook = "webhook" + WordSuffixRuleWarn = "warn" + WordSuffixRuleDelete = "delete" +) + func Command(c *components.Components) *generic.Command { return (&generic.Command{ Namespace: "message", @@ -103,17 +108,17 @@ func Command(c *components.Components) *generic.Command { { Name: "webhook", NameLocalizations: translate.MessageMap("components.message.suffix.set.command.options.rule.webhook", false), - Value: wordsuffix.RuleWebhook.String(), + Value: WordSuffixRuleWebhook, }, { Name: "warn", NameLocalizations: translate.MessageMap("components.message.suffix.set.command.options.rule.warn", false), - Value: wordsuffix.RuleWarn.String(), + Value: WordSuffixRuleWarn, }, { Name: "delete", NameLocalizations: translate.MessageMap("components.message.suffix.set.command.options.rule.delete", false), - Value: wordsuffix.RuleDelete.String(), + Value: WordSuffixRuleDelete, }, }, }, @@ -281,25 +286,36 @@ func Command(c *components.Components) *generic.Command { } expired = builtin.Or(d != 0, builtin.Ptr(time.Now().Add(d)), nil) } - var w *ent.WordSuffix - if u.QueryWordSuffix().Where(wordsuffix.GuildID(g.ID)).ExistX(event) { - w = u.QueryWordSuffix().Where(wordsuffix.GuildID(g.ID)).OnlyX(event) - w = w.Update(). - SetSuffix(event.SlashCommandInteractionData().String("suffix")). - SetOwner(u). - SetRule(wordsuffix.Rule(event.SlashCommandInteractionData().String("rule"))). - SetNillableExpired(expired). - SaveX(event) + + var w models.WordSuffix + err = c.GormDB().Where("guild_id = ? AND owner_id = ?", g.ID, u.ID).First(&w).Error + if err == nil { + // Update + w.Suffix = event.SlashCommandInteractionData().String("suffix") + w.Rule = event.SlashCommandInteractionData().String("rule") + w.Expired = expired + if err := c.GormDB().Save(&w).Error; err != nil { + return errors.NewError(err) + } } else { - w = c.DB().WordSuffix. - Create(). - SetGuild(g). - SetSuffix(event.SlashCommandInteractionData().String("suffix")). - SetOwner(u). - SetRule(wordsuffix.Rule(event.SlashCommandInteractionData().String("rule"))). - SetNillableExpired(expired). - SaveX(event) + if !errors.Is(err, gorm.ErrRecordNotFound) { + return errors.NewError(err) + } + + // Create + w = models.WordSuffix{ + ID: uuidv7.New(), + GuildID: &g.ID, + Suffix: event.SlashCommandInteractionData().String("suffix"), + OwnerID: u.ID, + Rule: event.SlashCommandInteractionData().String("rule"), + Expired: expired, + } + if err := c.GormDB().Create(&w).Error; err != nil { + return errors.NewError(err) + } } + var durationString string if expired != nil { durationString = discord.FormattedTimestampMention(expired.Unix(), discord.TimestampStyleRelative) @@ -312,13 +328,11 @@ func Command(c *components.Components) *generic.Command { translate.Message( event.Locale(), "components.message.suffix.set.message", - translate.WithTemplate(map[string]any{"User": discord.UserMention(u.ID), "Suffix": w.Suffix}), - ), + translate.WithTemplate(map[string]any{"User": discord.UserMention(u.ID), "Suffix": w.Suffix})), translate.Message( event.Locale(), "components.message.suffix.duration.message", - translate.WithTemplate(map[string]any{"Duration": durationString}), - ), + translate.WithTemplate(map[string]any{"Duration": durationString})), ). BuildCreate(), ); err != nil { @@ -349,20 +363,28 @@ func Command(c *components.Components) *generic.Command { return errors.NewError(err) } - if !u.QueryWordSuffix().Where(wordsuffix.GuildID(g.ID)).ExistX(event) { - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetContent(translate.Message(event.Locale(), "components.message.suffix.remove.message.no_suffix", translate.WithTemplate(map[string]any{"User": discord.UserMention(u.ID)}))). - SetAllowedMentions(&discord.AllowedMentions{}). - SetFlags(discord.MessageFlagEphemeral). - BuildCreate(), - ); err != nil { - return errors.NewError(err) + var w models.WordSuffix + err = c.GormDB().Where("guild_id = ? AND owner_id = ?", g.ID, u.ID).First(&w).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + if err := event.CreateMessage( + discord.NewMessageBuilder(). + SetContent(translate.Message(event.Locale(), "components.message.suffix.remove.message.no_suffix", translate.WithTemplate(map[string]any{"User": discord.UserMention(u.ID)}))). + SetAllowedMentions(&discord.AllowedMentions{}). + SetFlags(discord.MessageFlagEphemeral). + BuildCreate(), + ); err != nil { + return errors.NewError(err) + } + return nil } - return nil + return errors.NewError(err) + } + + if err := c.GormDB().Delete(&w).Error; err != nil { + return errors.NewError(err) } - c.DB().WordSuffix.DeleteOneID(u.QueryWordSuffix().Where(wordsuffix.GuildID(g.ID)).FirstIDX(event)).ExecX(event) if err := event.CreateMessage( discord.NewMessageBuilder(). SetContent(translate.Message(event.Locale(), "components.message.suffix.remove.message", translate.WithTemplate(map[string]any{"User": discord.UserMention(u.ID)}))). @@ -402,12 +424,10 @@ func Command(c *components.Components) *generic.Command { messageStr := translate.Message(event.Locale(), "components.message.suffix.check.message.none", translate.WithTemplate(map[string]any{"User": discord.UserMention(u.ID)}), ) - if u.QueryWordSuffix().Where( - wordsuffix.GuildID(g.ID), - ).ExistX(event) { - w := u.QueryWordSuffix().Where( - wordsuffix.GuildID(g.ID), - ).FirstX(event) + + var w models.WordSuffix + err = c.GormDB().Where("guild_id = ? AND owner_id = ?", g.ID, u.ID).First(&w).Error + if err == nil { messageStr = translate.Message(event.Locale(), "components.message.suffix.check.message", translate.WithTemplate( map[string]any{ @@ -417,11 +437,12 @@ func Command(c *components.Components) *generic.Command { ), "User": discord.UserMention(u.ID), "Suffix": w.Suffix, - "Rule": translate.Message(event.Locale(), "components.message.suffix.set.command.options.rule."+w.Rule.String()), + "Rule": translate.Message(event.Locale(), "components.message.suffix.set.command.options.rule."+w.Rule), }, ), ) } + if err := event.CreateMessage( discord.NewMessageBuilder(). SetContent(messageStr). @@ -466,19 +487,27 @@ func Command(c *components.Components) *generic.Command { }, DiscordPerm: discord.PermissionManageMessages, CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - g, err := c.GuildCreateID(event, *event.GuildID()) + _, err := c.GuildCreateID(event, *event.GuildID()) if err != nil { return errors.NewError(err) } - if !g.QueryMessagePins().Where(messagepin.ChannelID(event.Channel().ID())).ExistX(event) { - return errors.NewError(errors.ErrorMessage("errors.unavailable.message.pin", event)) + var m models.MessagePin + err = c.GormDB().Where("channel_id = ?", event.Channel().ID()).First(&m).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return errors.NewError(errors.ErrorMessage("errors.unavailable.message.pin", event)) + } + return errors.NewError(err) } - if beforeID := g.QueryMessagePins().Where(messagepin.ChannelID(event.Channel().ID())).FirstX(event).BeforeID; beforeID != nil { - _ = event.Client().Rest.DeleteMessage(event.Channel().ID(), *beforeID) + + if m.BeforeID != nil { + _ = event.Client().Rest.DeleteMessage(event.Channel().ID(), *m.BeforeID) } - c.DB().MessagePin.Delete().Where(messagepin.ChannelID(event.Channel().ID())).ExecX(event) + if err := c.GormDB().Delete(&m).Error; err != nil { + return errors.NewError(err) + } if err := event.CreateMessage( discord.NewMessageBuilder(). @@ -555,15 +584,17 @@ func Command(c *components.Components) *generic.Command { }, DiscordPerm: discord.PermissionManageMessages, CommandHandler: func(c *components.Components, event *events.ApplicationCommandInteractionCreate) errors.Error { - count := c.DB().MessageRemind.Delete().Where( - messageremind.HasGuildWith(guild.ID(*event.GuildID())), - messageremind.NameContains(event.SlashCommandInteractionData().String("remind")), - ).ExecX(event) + res := c.GormDB().Where("guild_id = ? AND name LIKE ?", *event.GuildID(), "%"+event.SlashCommandInteractionData().String("remind")+"%").Delete(&models.MessageRemind{}) + if res.Error != nil { + return errors.NewError(res.Error) + } + count := res.RowsAffected + if err := event.CreateMessage( discord.NewMessageBuilder(). SetContent(translate.Message(event.Locale(), "components.message.remind.cancel.message", translate.WithTemplate(map[string]any{ - "Count": strconv.Itoa(count), + "Count": strconv.FormatInt(count, 10), }), )). BuildCreate(), @@ -582,12 +613,8 @@ func Command(c *components.Components) *generic.Command { }, DiscordPerm: discord.PermissionManageMessages, AutocompleteHandler: func(c *components.Components, event *events.AutocompleteInteractionCreate) errors.Error { - reminds := c.DB().MessageRemind.Query().Where( - messageremind.HasGuildWith(guild.ID(*event.GuildID())), - messageremind.NameContains(event.Data.String("remind")), - ). - Limit(25). - AllX(event) + var reminds []models.MessageRemind + c.GormDB().Where("guild_id = ? AND name LIKE ?", *event.GuildID(), "%"+event.Data.String("remind")+"%").Limit(25).Find(&reminds) choices := make([]discord.AutocompleteChoice, len(reminds)) for i, mr := range reminds { @@ -612,19 +639,23 @@ func Command(c *components.Components) *generic.Command { } // もし既にあったら抹消する - if g.QueryMessagePins().Where(messagepin.ChannelID(event.Channel().ID())).ExistX(event) { - if beforeID := g.QueryMessagePins().Where(messagepin.ChannelID(event.Channel().ID())).FirstX(event).BeforeID; beforeID != nil { - _ = event.Client().Rest.DeleteMessage(event.Channel().ID(), *beforeID) + var oldPin models.MessagePin + if err := component.GormDB().Where("channel_id = ?", event.Channel().ID()).First(&oldPin).Error; err == nil { + if oldPin.BeforeID != nil { + _ = event.Client().Rest.DeleteMessage(event.Channel().ID(), *oldPin.BeforeID) } + component.GormDB().Delete(&oldPin) + } - component.DB().MessagePin.Delete().Where(messagepin.ChannelID(event.Channel().ID())).ExecX(event) + m := models.MessagePin{ + ChannelID: event.Channel().ID(), + Content: event.Data.Text("content"), + GuildID: g.ID, + } + if err := component.GormDB().Create(&m).Error; err != nil { + return errors.NewError(err) } - m := component.DB().MessagePin.Create(). - SetChannelID(event.Channel().ID()). - SetContent(event.Data.Text("content")). - SetGuild(g). - SaveX(event) channel, err := event.Client().Rest.GetChannel(m.ChannelID) if err != nil { return errors.NewError(err) @@ -646,7 +677,10 @@ func Command(c *components.Components) *generic.Command { return errors.NewError(err) } - m.Update().SetBeforeID(message.ID).SaveX(event) + m.BeforeID = &message.ID + if err := component.GormDB().Save(&m).Error; err != nil { + return errors.NewError(err) + } if err := event.CreateMessage( discord.NewMessageBuilder(). @@ -668,15 +702,21 @@ func Command(c *components.Components) *generic.Command { if time.Now().After(tm) { return errors.NewError(errors.ErrorMessage("errors.invalid.time.before", event)) } - c.DB().MessageRemind.Create(). - SetGuild(g). - SetTime(tm). - SetContent(event.Data.Text("content")). - SetChannelID(event.Channel().ID()). - SetAuthorID(event.Member().User.ID). - SetName(event.Data.Text("name")). - ExecX(event) - g.Update().AddRemindCount(1).ExecX(event) + + remind := models.MessageRemind{ + GuildID: g.ID, + Time: tm, + Content: event.Data.Text("content"), + ChannelID: event.Channel().ID(), + AuthorID: event.Member().User.ID, + Name: event.Data.Text("name"), + } + if err := c.GormDB().Create(&remind).Error; err != nil { + return errors.NewError(err) + } + + g.RemindCount++ + c.GormDB().Save(g) if err := event.CreateMessage( discord.NewMessageBuilder(). @@ -697,11 +737,9 @@ func Command(c *components.Components) *generic.Command { { Duration: time.Minute, Worker: func(c *components.Components, client *bot.Client) error { - reminds := c.DB().MessageRemind.Query(). - Where( - messageremind.TimeLT(time.Now()), - ). - AllX(c.Ctx()) + var reminds []models.MessageRemind + c.GormDB().Where("time < ?", time.Now()).Find(&reminds) + for _, remind := range reminds { if _, err := client.Rest.CreateMessage(remind.ChannelID, discord.NewMessageBuilder(). @@ -712,11 +750,7 @@ func Command(c *components.Components) *generic.Command { } } - c.DB().MessageRemind.Delete(). - Where( - messageremind.TimeLT(time.Now()), - ). - ExecX(c.Ctx()) + c.GormDB().Where("time < ?", time.Now()).Delete(&models.MessageRemind{}) return nil }, }, @@ -738,8 +772,8 @@ func Command(c *components.Components) *generic.Command { // 変数が初期化されていないことが潜在的なバグの原因になりかねない // 語尾の処理 - var w *ent.WordSuffix - var u *ent.User + var w models.WordSuffix + var u *models.User if e.Message.Type.System() || e.Message.Author.System || e.Message.Author.Bot { goto messagePin @@ -753,26 +787,29 @@ func Command(c *components.Components) *generic.Command { return errors.NewError(err) } - if u.QueryWordSuffix().Where(wordsuffix.GuildID(e.GuildID)).ExistX(e) { - // Guild - w = u.QueryWordSuffix().Where(wordsuffix.GuildID(e.GuildID)).FirstX(e) + // Guild + if err := c.GormDB().Where("owner_id = ? AND guild_id = ?", u.ID, e.GuildID).First(&w).Error; err == nil { + // Found } else { // Global - if !u.QueryWordSuffix().Where(wordsuffix.GuildIDIsNil()).ExistX(e) { + if err := c.GormDB().Where("owner_id = ? AND guild_id IS NULL", u.ID).First(&w).Error; err != nil { + if err != gorm.ErrRecordNotFound { + slog.Error("語尾取得エラー", "err", err) + } + // Not found slog.Debug("語尾が存在しません") goto messagePin } - w = u.QueryWordSuffix().Where(wordsuffix.GuildIDIsNil()).FirstX(e) } { webhookFlag := false - if w.Rule == wordsuffix.RuleWebhook { + if w.Rule == WordSuffixRuleWebhook { c.GetLock("message_pin").Mutex(e.ChannelID).Lock() webhookFlag = true } - err = messageSuffixMessageCreateHandler(w, u, e, c) + err = messageSuffixMessageCreateHandler(&w, u, e, c) if webhookFlag { c.GetLock("message_pin").Mutex(e.ChannelID).Unlock() @@ -800,12 +837,20 @@ func Command(c *components.Components) *generic.Command { if err != nil { return errors.NewError(err) } - if !g.QueryMessagePins().Where(messagepin.ChannelID(event.ChannelID)).ExistX(event) { - return nil + + var m models.MessagePin + if err := c.GormDB().Where("guild_id = ? AND channel_id = ?", g.ID, event.ChannelID).First(&m).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil + } + return errors.NewError(err) } + c.GetLock("message_pin").Mutex(e.ChannelID).Lock() defer c.GetLock("message_pin").Mutex(e.ChannelID).Unlock() - m := g.QueryMessagePins().Where(messagepin.ChannelID(event.ChannelID)).FirstX(event) + + // Re-fetch to be safe under lock? Or is it overkill? + // The original code fetched it here. webhook, err := event.Client().WebhookManager.GetMessenger(channel) if err != nil { @@ -852,10 +897,11 @@ func Command(c *components.Components) *generic.Command { return errors.NewError(err) } - m.Update().SetBeforeID(message.ID).SetRateLimit(m.RateLimit).ExecX(event) + m.BeforeID = &message.ID + c.GormDB().Save(&m) slog.Info("ピン留め更新", "cid", event.ChannelID, "mid", event.MessageID) } else { - m.Update().SetRateLimit(m.RateLimit).ExecX(event) + c.GormDB().Save(&m) } return nil }(e, c); err != nil { @@ -874,14 +920,15 @@ func Command(c *components.Components) *generic.Command { if err != nil { return errors.NewError(err) } - if !g.QueryMessagePins().Where(messagepin.ChannelID(e.ChannelID)).ExistX(e) { + + var m models.MessagePin + if err := c.GormDB().Where("guild_id = ? AND channel_id = ?", g.ID, e.ChannelID).First(&m).Error; err != nil { return nil } - m := g.QueryMessagePins().Where(messagepin.ChannelID(e.ChannelID)).FirstX(e) if m.BeforeID != nil && *m.BeforeID == e.MessageID { slog.Info("ピン留め削除", "cid", e.ChannelID, "mid", e.MessageID) - c.DB().MessagePin.DeleteOneID(m.ID).ExecX(e) + c.GormDB().Delete(&m) } } return nil @@ -889,18 +936,18 @@ func Command(c *components.Components) *generic.Command { }).SetComponent(c) } -func messageSuffixMessageCreateHandler(w *ent.WordSuffix, u *ent.User, e *events.GuildMessageCreate, c *components.Components) errors.Error { +func messageSuffixMessageCreateHandler(w *models.WordSuffix, u *models.User, e *events.GuildMessageCreate, c *components.Components) errors.Error { slog.Debug("メッセージ作成") if e.Message.Content == "" { return nil } if w.Expired != nil && time.Now().Compare(*w.Expired) == 1 { - c.DB().WordSuffix.DeleteOneID(w.ID).ExecX(e) + c.GormDB().Delete(w) return nil } switch w.Rule { - case wordsuffix.RuleDelete: + case WordSuffixRuleDelete: if strings.HasSuffix(e.Message.Content, w.Suffix) { return nil } @@ -908,7 +955,7 @@ func messageSuffixMessageCreateHandler(w *ent.WordSuffix, u *ent.User, e *events slog.Error("メッセージを削除できません", "err", err) return errors.NewError(err) } - case wordsuffix.RuleWarn: + case WordSuffixRuleWarn: if strings.HasSuffix(e.Message.Content, w.Suffix) { return nil } @@ -924,11 +971,39 @@ func messageSuffixMessageCreateHandler(w *ent.WordSuffix, u *ent.User, e *events slog.Error("メッセージを作成できません", "err", err) return errors.NewError(err) } - case wordsuffix.RuleWebhook: - content := e.Message.Content - if !strings.HasSuffix(e.Message.Content, w.Suffix) { - content += w.Suffix + case WordSuffixRuleWebhook: + var content string + for s := range strings.SplitSeq(e.Message.Content, "\n") { + if s == "" { + content += "\n" + continue + } + // すでに語尾がある場合はそのまま通す + if strings.HasSuffix(s, w.Suffix) { + content += s + "\n" + continue + } + // 末尾に文字列以外の文字がある場合(アルファベット、かな漢字以外のすべての文字)それらの前に語尾をなければ追加する + // 例: "こんにちは→→"の場合、"こんにちは"の後ろに語尾を追加し、"→→"の後ろには追加しない + runes := []rune(s) + i := len(runes) - 1 + for i >= 0 { + if (runes[i] < 'A' || runes[i] > 'Z') && (runes[i] < 'a' || runes[i] > 'z') && (runes[i] < '0' || runes[i] > '9') && !unicode.In(runes[i], unicode.Hiragana, unicode.Katakana, unicode.Han) { + i-- + } else { + break + } + } + if i < 0 { + // すべて文字列以外の文字の場合、そのまま通す + content += s + "\n" + continue + } + content += string(runes[:i+1]) + w.Suffix + string(runes[i+1:]) + "\n" } + content = content[:len(content)-1] // 最後の改行を削除 + + // メッセージを削除 if err := e.Client().Rest.DeleteMessage(e.ChannelID, e.MessageID); err != nil { return errors.NewError(err) } diff --git a/bot/commands/permission/permission.go b/bot/commands/permission/permission.go index 5a73de31..6dc266ab 100644 --- a/bot/commands/permission/permission.go +++ b/bot/commands/permission/permission.go @@ -137,9 +137,9 @@ func Command(c *components.Components) components.Command { return errors.NewError(err) } t.Permission.Set(perm, value) - t = t.Update(). - SetPermission(t.Permission). - SaveX(event) + if err := c.GormDB().Save(t).Error; err != nil { + return errors.NewError(err) + } mention = discord.UserMention(t.UserID) } else { role := event.SlashCommandInteractionData().Role("target") @@ -150,7 +150,7 @@ func Command(c *components.Components) components.Command { p := g.Permissions[role.ID] p.Set(perm, value) g.Permissions[role.ID] = p - g.Update().SetPermissions(g.Permissions).ExecX(event) + c.GormDB().Save(g) mention = discord.RoleMention(role.ID) } if err := event.CreateMessage( @@ -189,9 +189,7 @@ func Command(c *components.Components) components.Command { return errors.NewError(err) } t.Permission.UnSet(perm) - t = t.Update(). - SetPermission(t.Permission). - SaveX(event) + c.GormDB().Save(t) mention = discord.UserMention(t.UserID) } else { role := event.SlashCommandInteractionData().Role("target") @@ -202,7 +200,7 @@ func Command(c *components.Components) components.Command { p := g.Permissions[role.ID] p.UnSet(perm) g.Permissions[role.ID] = p - g.Update().SetPermissions(g.Permissions).ExecX(event) + c.GormDB().Save(g) mention = discord.RoleMention(role.ID) } if err := event.CreateMessage( diff --git a/bot/commands/role/import.go b/bot/commands/role/import.go index 7889f04f..629971c6 100644 --- a/bot/commands/role/import.go +++ b/bot/commands/role/import.go @@ -30,7 +30,7 @@ import ( "github.com/disgoorg/snowflake/v2" "github.com/sabafly/gobot/bot/components" "github.com/sabafly/gobot/bot/components/generic" - "github.com/sabafly/gobot/ent/schema" + "github.com/sabafly/gobot/database/models" "github.com/sabafly/gobot/internal/builtin" "github.com/sabafly/gobot/internal/discordutil" "github.com/sabafly/gobot/internal/emoji" @@ -62,7 +62,7 @@ func ImportCommand(c *components.Components) components.Command { } message := event.MessageCommandInteractionData().TargetMessage() lines := strings.Split(message.Embeds[0].Description, "\n") - var roles []schema.Role + var roles []models.Role roleCount := 0 for _, v := range lines { if !roleRegexp.MatchString(v) { @@ -91,7 +91,7 @@ func ImportCommand(c *components.Components) components.Command { role = *rolePtr } roleCount++ - roles = append(roles, schema.Role{ + roles = append(roles, models.Role{ ID: role.ID, Name: role.Name, Emoji: &componentEmoji, @@ -106,25 +106,31 @@ func ImportCommand(c *components.Components) components.Command { return errors.NewError(err) } - panel := c.DB().RolePanel.Create(). - SetName(builtin.Or(message.Embeds[0].Title != "", message.Embeds[0].Title, translate.Message(event.Locale(), "components.role.panel.default_name"))). - SetDescription(""). - SetRoles(roles). - SetGuild(g). - SaveX(event) + panel := models.RolePanel{ + Name: builtin.Or(message.Embeds[0].Title != "", message.Embeds[0].Title, translate.Message(event.Locale(), "components.role.panel.default_name")), + Description: "", + Roles: roles, + GuildID: g.ID, + } + if err := c.GormDB().Create(&panel).Error; err != nil { + return errors.NewError(err) + } - place := c.DB().RolePanelPlaced.Create(). - SetGuild(g). - SetChannelID(event.Channel().ID()). - SetRolePanel(panel). - SetName(panel.Name). - SetDescription(panel.Description). - SetRoles(panel.Roles). - SetUpdatedAt(time.Now()). - SaveX(event) + place := models.RolePanelPlaced{ + GuildID: g.ID, + ChannelID: event.Channel().ID(), + RolePanelID: panel.ID, + Name: panel.Name, + Description: panel.Description, + Roles: panel.Roles, + UpdatedAt: time.Now(), + } + if err := c.GormDB().Create(&place).Error; err != nil { + return errors.NewError(err) + } if err := event.CreateMessage( - rpPlaceBaseMenu(place, event.Locale()). + rpPlaceBaseMenu(&place, event.Locale()). SetFlags(discord.MessageFlagEphemeral). BuildCreate(), ); err != nil { diff --git a/bot/commands/role/panel.go b/bot/commands/role/panel.go index 7d0e498d..eccc57e1 100644 --- a/bot/commands/role/panel.go +++ b/bot/commands/role/panel.go @@ -27,24 +27,27 @@ import ( "github.com/disgoorg/snowflake/v2" "github.com/google/uuid" "github.com/sabafly/gobot/bot/components" - "github.com/sabafly/gobot/ent/guild" - "github.com/sabafly/gobot/ent/rolepanel" + "github.com/sabafly/gobot/database/models" "github.com/sabafly/gobot/internal/errors" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" - "github.com/sabafly/gobot/ent" - "github.com/sabafly/gobot/ent/rolepanelplaced" "github.com/sabafly/gobot/internal/discordutil" ) -func rolePanelPlace(ctx context.Context, place *ent.RolePanelPlaced, locale discord.Locale, client *bot.Client, react bool) error { +const ( + RolePanelPlacedTypeReaction = "reaction" + RolePanelPlacedTypeButton = "button" + RolePanelPlacedTypeSelectMenu = "select_menu" +) + +func rolePanelPlace(ctx context.Context, place *models.RolePanelPlaced, locale discord.Locale, client *bot.Client, react bool, c *components.Components) error { builder := rpPlacedMessage(place, locale) if place.MessageID != nil { if _, err := client.Rest.UpdateMessage(place.ChannelID, *place.MessageID, builder.BuildUpdate()); err != nil { return err } - if place.Type == rolepanelplaced.TypeReaction && react { + if place.Type == RolePanelPlacedTypeReaction && react { if err := client.Rest.RemoveAllReactions(place.ChannelID, *place.MessageID); err != nil { return err } @@ -54,10 +57,13 @@ func rolePanelPlace(ctx context.Context, place *ent.RolePanelPlaced, locale disc if err != nil { return err } - *place = *place.Update().SetMessageID(m.ID).SaveX(ctx) + place.MessageID = &m.ID + if err := c.GormDB().Save(place).Error; err != nil { + return err + } } - if place.Type == rolepanelplaced.TypeReaction && react { + if place.Type == RolePanelPlacedTypeReaction && react { for i, r := range place.Roles { if r.Emoji == nil { r.Emoji = &discord.ComponentEmoji{ @@ -72,23 +78,29 @@ func rolePanelPlace(ctx context.Context, place *ent.RolePanelPlaced, locale disc return nil } -func createPanelPlace(ctx context.Context, c *components.Components, panelID uuid.UUID, channelID snowflake.ID, g *ent.Guild) (*ent.RolePanelPlaced, error) { +func createPanelPlace(ctx context.Context, c *components.Components, panelID uuid.UUID, channelID snowflake.ID, g *models.Guild) (*models.RolePanelPlaced, error) { - c.DB().RolePanelPlaced.Delete().Where(rolepanelplaced.And(rolepanelplaced.Or(rolepanelplaced.MessageIDIsNil(), rolepanelplaced.TypeIsNil()), rolepanelplaced.HasGuildWith(guild.ID(g.ID)))).ExecX(ctx) + // Clean up incomplete placements + c.GormDB().Where("(message_id IS NULL OR type = '') AND guild_id = ?", g.ID).Delete(&models.RolePanelPlaced{}) - if !g.QueryRolePanels().Where(rolepanel.ID(panelID)).ExistX(ctx) { + var panel models.RolePanel + if err := c.GormDB().Where("id = ? AND guild_id = ?", panelID, g.ID).First(&panel).Error; err != nil { return nil, errors.New("rolepanel not found") } - panel := g.QueryRolePanels().Where(rolepanel.ID(panelID)).FirstX(ctx) + placed := models.RolePanelPlaced{ + GuildID: g.ID, + ChannelID: channelID, + RolePanelID: panel.ID, + Name: panel.Name, + Description: panel.Description, + Roles: panel.Roles, + UpdatedAt: time.Now(), + } + + if err := c.GormDB().Create(&placed).Error; err != nil { + return nil, err + } - return c.DB().RolePanelPlaced.Create(). - SetGuild(g). - SetChannelID(channelID). - SetRolePanel(panel). - SetName(panel.Name). - SetDescription(panel.Description). - SetRoles(panel.Roles). - SetUpdatedAt(time.Now()). - SaveX(ctx), nil + return &placed, nil } diff --git a/bot/commands/role/panel_autocomplete.go b/bot/commands/role/panel_autocomplete.go index 170e7df5..360ca76c 100644 --- a/bot/commands/role/panel_autocomplete.go +++ b/bot/commands/role/panel_autocomplete.go @@ -27,8 +27,7 @@ import ( "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" "github.com/sabafly/gobot/bot/components" - "github.com/sabafly/gobot/ent" - "github.com/sabafly/gobot/ent/rolepanel" + "github.com/sabafly/gobot/database/models" "github.com/sabafly/gobot/internal/builtin" "github.com/sabafly/gobot/internal/errors" ) @@ -38,11 +37,16 @@ func panelAutocomplete(c *components.Components, event *events.AutocompleteInter if err != nil { return errors.NewError(err) } - panels := g.QueryRolePanels().Where(rolepanel.NameContains(event.Data.String("panel"))).AllX(event) + + var panels []models.RolePanel + if err := c.GormDB().Where("guild_id = ? AND name LIKE ?", g.ID, "%"+event.Data.String("panel")+"%").Find(&panels).Error; err != nil { + return errors.NewError(err) + } + choices := make([]discord.AutocompleteChoice, len(panels)) for i, p := range panels { choices[i] = discord.AutocompleteChoiceString{ - Name: builtin.Or(slices.ContainsFunc(panels, func(rp *ent.RolePanel) bool { return rp.ID != p.ID && rp.Name == p.Name }), fmt.Sprintf("%s (%s)", p.Name, p.ID), p.Name), + Name: builtin.Or(slices.ContainsFunc(panels, func(rp models.RolePanel) bool { return rp.ID != p.ID && rp.Name == p.Name }), fmt.Sprintf("%s (%s)", p.Name, p.ID), p.Name), Value: p.ID.String(), } } diff --git a/bot/commands/role/panel_message.go b/bot/commands/role/panel_message.go index 094ac34d..42dfeb1b 100644 --- a/bot/commands/role/panel_message.go +++ b/bot/commands/role/panel_message.go @@ -21,14 +21,12 @@ package role import ( - "context" "fmt" "slices" "github.com/disgoorg/disgo/discord" - "github.com/sabafly/gobot/ent" - "github.com/sabafly/gobot/ent/rolepanelplaced" - "github.com/sabafly/gobot/ent/schema" + "github.com/sabafly/gobot/bot/components" + "github.com/sabafly/gobot/database/models" "github.com/sabafly/gobot/internal/builtin" "github.com/sabafly/gobot/internal/discordutil" "github.com/sabafly/gobot/internal/embeds" @@ -36,7 +34,7 @@ import ( "github.com/sabafly/gobot/internal/translate" ) -func initialize(edit *ent.RolePanelEdit, panel *ent.RolePanel) { +func initialize(edit *models.RolePanelEdit, panel *models.RolePanel) { if edit.Roles == nil { edit.Roles = panel.Roles } @@ -48,48 +46,49 @@ func initialize(edit *ent.RolePanelEdit, panel *ent.RolePanel) { } } -func rpEditBaseMessage(ctx context.Context, panel *ent.RolePanel, edit *ent.RolePanelEdit, locale discord.Locale) discord.MessageBuilder { +func rpEditBaseMessage(c *components.Components, panel *models.RolePanel, edit *models.RolePanelEdit, locale discord.Locale) (discord.MessageBuilder, error) { initialize(edit, panel) builder := discord.NewMessageBuilder() var roleField string for i, r := range edit.Roles { - if r.Emoji == nil { - r.Emoji = &discord.ComponentEmoji{ + emojiVal := r.Emoji + if emojiVal == nil { + emojiVal = &discord.ComponentEmoji{ Name: discordutil.Index2Emoji(i), } } - roleField += fmt.Sprintf("%s: %s: %s\n", discordutil.FormatComponentEmoji(*r.Emoji), r.Name, discord.RoleMention(r.ID)) + roleField += fmt.Sprintf("%s: %s: %s\n", discordutil.FormatComponentEmoji(*emojiVal), r.Name, discord.RoleMention(r.ID)) } - builder.SetEmbeds( - embeds.SetEmbedsProperties( - []discord.Embed{ - discord.NewEmbedBuilder(). - SetTitle(translate.Message(locale, "components.role.panel.edit.menu.base.title")). - SetFields( - discord.EmbedField{ - Name: translate.Message(locale, "components.role.panel.edit.menu.base.field.name"), - Value: builtin.NonNil(edit.Name), - Inline: builtin.Ptr(true), - }, - discord.EmbedField{ - Name: translate.Message(locale, "components.role.panel.edit.menu.base.field.description"), - Value: builtin.Or(builtin.NonNil(edit.Description) != "", builtin.NonNil(edit.Description), fmt.Sprintf("`%s`", translate.Message(locale, "components.role.panel.edit.menu.base.field.value.empty"))), - Inline: builtin.Ptr(true), - }, - discord.EmbedField{ - Name: translate.Message(locale, "components.role.panel.edit.menu.base.field.roles"), - Value: builtin.Or(roleField != "", roleField, fmt.Sprintf("`%s`", translate.Message(locale, "components.role.panel.edit.menu.base.field.value.empty"))), - }, - ). - SetFooterTextf("id: %s", panel.ID). - Build(), - }, - )..., - ) - placeCount := panel.QueryPlacements().CountX(ctx) + embedList := []discord.Embed{ + discord.NewEmbedBuilder(). + SetTitle(translate.Message(locale, "components.role.panel.edit.menu.base.title")). + SetFields( + discord.EmbedField{ + Name: translate.Message(locale, "components.role.panel.edit.menu.base.field.name"), + Value: builtin.NonNil(edit.Name), + Inline: builtin.Ptr(true), + }, + discord.EmbedField{ + Name: translate.Message(locale, "components.role.panel.edit.menu.base.field.description"), + Value: builtin.Or(builtin.NonNil(edit.Description) != "", builtin.NonNil(edit.Description), fmt.Sprintf("`%s`", translate.Message(locale, "components.role.panel.edit.menu.base.field.value.empty"))), + Inline: builtin.Ptr(true), + }, + discord.EmbedField{ + Name: translate.Message(locale, "components.role.panel.edit.menu.base.field.roles"), + Value: builtin.Or(roleField != "", roleField, fmt.Sprintf("`%s`", translate.Message(locale, "components.role.panel.edit.menu.base.field.value.empty"))), + }). + SetFooterTextf("id: %s", panel.ID). + Build(), + } + builder.SetEmbeds(embeds.SetEmbedsProperties(embedList)...) + + var placeCount int64 + if err := c.GormDB().Model(&models.RolePanelPlaced{}).Where("role_panel_id = ?", panel.ID).Count(&placeCount).Error; err != nil { + return builder, err + } - disabled := len(edit.Roles) < 1 || edit.SelectedRole == nil || !slices.ContainsFunc(edit.Roles, func(r schema.Role) bool { return r.ID == *edit.SelectedRole }) + disabled := len(edit.Roles) < 1 || edit.SelectedRole == nil || !slices.ContainsFunc(edit.Roles, func(r models.Role) bool { return r.ID == *edit.SelectedRole }) builder.SetComponents( discord.NewActionRow( discord.ButtonComponent{ @@ -130,34 +129,32 @@ func rpEditBaseMessage(ctx context.Context, panel *ent.RolePanel, edit *ent.Role ), discord.NewActionRow( func() discord.StringSelectMenuComponent { - menu := discord.StringSelectMenuComponent{ + options := make([]discord.StringSelectMenuOption, len(edit.Roles)) + for i, r := range edit.Roles { + emojiVal := r.Emoji + if emojiVal == nil { + emojiVal = &discord.ComponentEmoji{ + Name: discordutil.Index2Emoji(i), + } + } + options[i] = discord.StringSelectMenuOption{ + Label: r.Name, + Value: r.ID.String(), + Emoji: emojiVal, + Default: edit.SelectedRole != nil && *edit.SelectedRole == r.ID, + } + } + if len(edit.Roles) < 1 { + options = append(options, discord.NewStringSelectMenuOption("nil", "nil")) + } + return discord.StringSelectMenuComponent{ CustomID: fmt.Sprintf("role:panel_edit_component:select_role:%s", edit.ID), Placeholder: translate.Message(locale, "components.role.panel.edit.menu.base.components.select_role"), MinValues: builtin.Ptr(0), MaxValues: 1, Disabled: len(edit.Roles) < 1, - Options: func() []discord.StringSelectMenuOption { - options := make([]discord.StringSelectMenuOption, len(edit.Roles)) - for i, r := range edit.Roles { - if r.Emoji == nil { - r.Emoji = &discord.ComponentEmoji{ - Name: discordutil.Index2Emoji(i), - } - } - options[i] = discord.StringSelectMenuOption{ - Label: r.Name, - Value: r.ID.String(), - Emoji: r.Emoji, - Default: edit.SelectedRole != nil && *edit.SelectedRole == r.ID, - } - } - if len(edit.Roles) < 1 { - options = append(options, discord.NewStringSelectMenuOption("nil", "nil")) - } - return options - }(), + Options: options, } - return menu }(), ), discord.NewActionRow( @@ -165,7 +162,7 @@ func rpEditBaseMessage(ctx context.Context, panel *ent.RolePanel, edit *ent.Role Style: discord.ButtonStylePrimary, Label: "↑", CustomID: fmt.Sprintf("role:panel_edit_component:move_up:%s", edit.ID), - Disabled: disabled || slices.IndexFunc(edit.Roles, func(r schema.Role) bool { return r.ID == *edit.SelectedRole }) == 0, + Disabled: disabled || slices.IndexFunc(edit.Roles, func(r models.Role) bool { return r.ID == *edit.SelectedRole }) == 0, }, discord.ButtonComponent{ Style: discord.ButtonStyleDanger, @@ -177,7 +174,7 @@ func rpEditBaseMessage(ctx context.Context, panel *ent.RolePanel, edit *ent.Role Style: discord.ButtonStylePrimary, Label: "↓", CustomID: fmt.Sprintf("role:panel_edit_component:move_down:%s", edit.ID), - Disabled: disabled || slices.IndexFunc(edit.Roles, func(r schema.Role) bool { return r.ID == *edit.SelectedRole }) == len(edit.Roles)-1, + Disabled: disabled || slices.IndexFunc(edit.Roles, func(r models.Role) bool { return r.ID == *edit.SelectedRole }) == len(edit.Roles)-1, }, ), discord.NewActionRow( @@ -196,35 +193,33 @@ func rpEditBaseMessage(ctx context.Context, panel *ent.RolePanel, edit *ent.Role ), ) - return builder + return builder, nil } -func rpEditModifyRolesMessage(edit *ent.RolePanelEdit, locale discord.Locale) discord.MessageBuilder { +func rpEditModifyRolesMessage(edit *models.RolePanelEdit, locale discord.Locale) discord.MessageBuilder { builder := discord.NewMessageBuilder() var roleField string for i, r := range edit.Roles { - if r.Emoji == nil { - r.Emoji = &discord.ComponentEmoji{ + emojiVal := r.Emoji + if emojiVal == nil { + emojiVal = &discord.ComponentEmoji{ Name: discordutil.Index2Emoji(i), } } - roleField += fmt.Sprintf("%s: %s: %s\n", discordutil.FormatComponentEmoji(*r.Emoji), r.Name, discord.RoleMention(r.ID)) + roleField += fmt.Sprintf("%s: %s: %s\n", discordutil.FormatComponentEmoji(*emojiVal), r.Name, discord.RoleMention(r.ID)) } - builder.SetEmbeds( - embeds.SetEmbedsProperties( - []discord.Embed{ - discord.NewEmbedBuilder(). - SetTitle(translate.Message(locale, "components.role.panel.edit.menu.modify_roles.title")). - SetFields( - discord.EmbedField{ - Name: translate.Message(locale, "components.role.panel.edit.menu.modify_roles.field.roles"), - Value: roleField, - }, - ). - Build(), - }, - )..., - ) + embedList := []discord.Embed{ + discord.NewEmbedBuilder(). + SetTitle(translate.Message(locale, "components.role.panel.edit.menu.modify_roles.title")). + SetFields( + discord.EmbedField{ + Name: translate.Message(locale, "components.role.panel.edit.menu.modify_roles.field.roles"), + Value: roleField, + }, + ). + Build(), + } + builder.SetEmbeds(embeds.SetEmbedsProperties(embedList)...) builder.SetComponents( discord.NewActionRow( @@ -252,16 +247,14 @@ func rpEditModifyRolesMessage(edit *ent.RolePanelEdit, locale discord.Locale) di return builder } -func rpEditSetEmojiMessage(edit *ent.RolePanelEdit, locale discord.Locale) discord.MessageBuilder { +func rpEditSetEmojiMessage(edit *models.RolePanelEdit, locale discord.Locale) discord.MessageBuilder { builder := discord.NewMessageBuilder() - builder.SetEmbeds( - embeds.SetEmbedProperties( - discord.NewEmbedBuilder(). - SetTitle(translate.Message(locale, "components.role.panel.edit.menu.set_emoji.title")). - SetDescription(translate.Message(locale, "components.role.panel.edit.menu.set_emoji.description")). - Build(), - ), - ) + embed := discord.NewEmbedBuilder(). + SetTitle(translate.Message(locale, "components.role.panel.edit.menu.set_emoji.title")). + SetDescription(translate.Message(locale, "components.role.panel.edit.menu.set_emoji.description")). + Build() + + builder.SetEmbeds(embeds.SetEmbedProperties(embed)) builder.SetComponents( discord.NewActionRow( @@ -280,36 +273,34 @@ func rpEditSetEmojiMessage(edit *ent.RolePanelEdit, locale discord.Locale) disco return builder } -func rpPlaceBaseMenu(place *ent.RolePanelPlaced, locale discord.Locale) discord.MessageBuilder { +func rpPlaceBaseMenu(place *models.RolePanelPlaced, locale discord.Locale) discord.MessageBuilder { builder := discord.NewMessageBuilder() var roleField string for i, r := range place.Roles { - if r.Emoji == nil { - r.Emoji = &discord.ComponentEmoji{ + emojiVal := r.Emoji + if emojiVal == nil { + emojiVal = &discord.ComponentEmoji{ Name: discordutil.Index2Emoji(i), } } - roleField += fmt.Sprintf("%s| %s\n", discordutil.FormatComponentEmoji(*r.Emoji), builtin.Or(place.UseDisplayName, r.Name, discord.RoleMention(r.ID))) + roleField += fmt.Sprintf("%s| %s\n", discordutil.FormatComponentEmoji(*emojiVal), builtin.Or(place.UseDisplayName, r.Name, discord.RoleMention(r.ID))) } - builder.SetEmbeds( - embeds.SetEmbedsProperties( - []discord.Embed{ - discord.NewEmbedBuilder(). - SetAuthorName(translate.Message(locale, "components.role.panel.place.menu.author.text")). - Build(), - discord.NewEmbedBuilder(). - SetTitle(place.Name). - SetDescription(place.Description). - SetFields( - discord.EmbedField{ - Name: translate.Message(locale, "components.role.panel.embed.field.role"), - Value: roleField, - }, - ). - Build(), - }, - )..., - ) + embedList := []discord.Embed{ + discord.NewEmbedBuilder(). + SetAuthorName(translate.Message(locale, "components.role.panel.place.menu.author.text")). + Build(), + discord.NewEmbedBuilder(). + SetTitle(place.Name). + SetDescription(place.Description). + SetFields( + discord.EmbedField{ + Name: translate.Message(locale, "components.role.panel.embed.field.role"), + Value: roleField, + }, + ). + Build(), + } + builder.SetEmbeds(embeds.SetEmbedsProperties(embedList)...) builder.AddComponents( discord.NewActionRow( @@ -321,24 +312,24 @@ func rpPlaceBaseMenu(place *ent.RolePanelPlaced, locale discord.Locale) discord. Options: []discord.StringSelectMenuOption{ { Label: translate.Message(locale, "components.role.panel.type.reaction"), - Value: rolepanelplaced.TypeReaction.String(), + Value: RolePanelPlacedTypeReaction, Description: translate.Message(locale, "components.role.panel.type.reaction.description"), Emoji: emoji.Reaction, - Default: place.Type == rolepanelplaced.TypeReaction, + Default: place.Type == RolePanelPlacedTypeReaction, }, { Label: translate.Message(locale, "components.role.panel.type.select_menu"), - Value: rolepanelplaced.TypeSelectMenu.String(), + Value: RolePanelPlacedTypeSelectMenu, Description: translate.Message(locale, "components.role.panel.type.select_menu.description"), Emoji: emoji.SelectMenu, - Default: place.Type == rolepanelplaced.TypeSelectMenu, + Default: place.Type == RolePanelPlacedTypeSelectMenu, }, { Label: translate.Message(locale, "components.role.panel.type.button"), - Value: rolepanelplaced.TypeButton.String(), + Value: RolePanelPlacedTypeButton, Description: translate.Message(locale, "components.role.panel.type.button.description"), Emoji: emoji.Button, - Default: place.Type == rolepanelplaced.TypeButton, + Default: place.Type == RolePanelPlacedTypeButton, }, }, }, @@ -346,7 +337,7 @@ func rpPlaceBaseMenu(place *ent.RolePanelPlaced, locale discord.Locale) discord. ) switch place.Type { - case rolepanelplaced.TypeButton: + case RolePanelPlacedTypeButton: builder.AddComponents( discord.NewActionRow( discord.StringSelectMenuComponent{ @@ -390,7 +381,7 @@ func rpPlaceBaseMenu(place *ent.RolePanelPlaced, locale discord.Locale) discord. }, ), ) - case rolepanelplaced.TypeSelectMenu: + case RolePanelPlacedTypeSelectMenu: builder.AddComponents( discord.NewActionRow( discord.ButtonComponent{ @@ -401,7 +392,7 @@ func rpPlaceBaseMenu(place *ent.RolePanelPlaced, locale discord.Locale) discord. }, ), ) - case rolepanelplaced.TypeReaction: + case RolePanelPlacedTypeReaction: builder.AddComponents( discord.NewActionRow( discord.ButtonComponent{ @@ -437,49 +428,49 @@ func rpPlaceBaseMenu(place *ent.RolePanelPlaced, locale discord.Locale) discord. return builder } -func rpPlacedMessage(place *ent.RolePanelPlaced, locale discord.Locale) discord.MessageBuilder { +func rpPlacedMessage(place *models.RolePanelPlaced, locale discord.Locale) discord.MessageBuilder { builder := discord.NewMessageBuilder() var roleField string for i, r := range place.Roles { - if r.Emoji == nil { - r.Emoji = &discord.ComponentEmoji{ + emojiVal := r.Emoji + if emojiVal == nil { + emojiVal = &discord.ComponentEmoji{ Name: discordutil.Index2Emoji(i), } } - roleField += fmt.Sprintf("%s| %s\n", discordutil.FormatComponentEmoji(*r.Emoji), builtin.Or(place.UseDisplayName, r.Name, discord.RoleMention(r.ID))) + roleField += fmt.Sprintf("%s| %s\n", discordutil.FormatComponentEmoji(*emojiVal), builtin.Or(place.UseDisplayName, r.Name, discord.RoleMention(r.ID))) } - builder.SetEmbeds( - embeds.SetEmbedsProperties( - []discord.Embed{ - discord.NewEmbedBuilder(). - SetTitle(place.Name). - SetDescription(place.Description). - SetFields( - discord.EmbedField{ - Name: translate.Message(locale, "components.role.panel.embed.field.role"), - Value: roleField, - }, - ). - Build(), - }, - )..., - ) + embedList := []discord.Embed{ + discord.NewEmbedBuilder(). + SetTitle(place.Name). + SetDescription(place.Description). + SetFields( + discord.EmbedField{ + Name: translate.Message(locale, "components.role.panel.embed.field.role"), + Value: roleField, + }, + ). + Build(), + } + builder.SetEmbeds(embeds.SetEmbedsProperties(embedList)...) + switch place.Type { - case rolepanelplaced.TypeButton: + case RolePanelPlacedTypeButton: buttons := make([]discord.InteractiveComponent, len(place.Roles)) for i, role := range place.Roles { var label string if place.ShowName { label = role.Name } - if role.Emoji == nil { - role.Emoji = &discord.ComponentEmoji{ + emojiVal := role.Emoji + if emojiVal == nil { + emojiVal = &discord.ComponentEmoji{ Name: discordutil.Index2Emoji(i), } } buttons[i] = discord.ButtonComponent{ Style: place.ButtonType, - Emoji: role.Emoji, + Emoji: emojiVal, Label: label, CustomID: fmt.Sprintf("role:panel_use:button:%s:%s", place.ID, role.ID), } @@ -496,7 +487,7 @@ func rpPlacedMessage(place *ent.RolePanelPlaced, locale discord.Locale) discord. builder.AddComponents( components..., ) - case rolepanelplaced.TypeSelectMenu: + case RolePanelPlacedTypeSelectMenu: if place.FoldingSelectMenu { builder.AddComponents( discord.NewActionRow( @@ -514,18 +505,19 @@ func rpPlacedMessage(place *ent.RolePanelPlaced, locale discord.Locale) discord. return builder } -func rpPlacedSelectMenu(place *ent.RolePanelPlaced, locale discord.Locale) discord.ActionRowComponent { +func rpPlacedSelectMenu(place *models.RolePanelPlaced, locale discord.Locale) discord.ActionRowComponent { options := make([]discord.StringSelectMenuOption, len(place.Roles)) for i, role := range place.Roles { - if role.Emoji == nil { - role.Emoji = &discord.ComponentEmoji{ + emojiVal := role.Emoji + if emojiVal == nil { + emojiVal = &discord.ComponentEmoji{ Name: discordutil.Index2Emoji(i), } } options[i] = discord.StringSelectMenuOption{ Label: role.Name, Value: role.ID.String(), - Emoji: role.Emoji, + Emoji: emojiVal, } } actionRow := discord.NewActionRow( diff --git a/bot/commands/role/role.go b/bot/commands/role/role.go index ec4ba05d..b4117387 100644 --- a/bot/commands/role/role.go +++ b/bot/commands/role/role.go @@ -34,14 +34,11 @@ import ( "github.com/disgoorg/disgo/rest" "github.com/disgoorg/snowflake/v2" "github.com/google/uuid" + "gorm.io/gorm" + "github.com/sabafly/gobot/bot/components" "github.com/sabafly/gobot/bot/components/generic" - "github.com/sabafly/gobot/ent" - "github.com/sabafly/gobot/ent/guild" - "github.com/sabafly/gobot/ent/rolepanel" - "github.com/sabafly/gobot/ent/rolepaneledit" - "github.com/sabafly/gobot/ent/rolepanelplaced" - "github.com/sabafly/gobot/ent/schema" + "github.com/sabafly/gobot/database/models" "github.com/sabafly/gobot/internal/builtin" "github.com/sabafly/gobot/internal/discordutil" "github.com/sabafly/gobot/internal/embeds" @@ -141,8 +138,7 @@ func Command(c *components.Components) components.Command { MaxLength: 32, Required: true, Value: translate.Message(event.Locale(), "components.role.panel.default_name"), - }, - ), + }), discord.NewLabel(translate.Message(event.Locale(), "components.role.panel.create.modal.input.2.label"), discord.TextInputComponent{ CustomID: "description", @@ -155,7 +151,6 @@ func Command(c *components.Components) components.Command { ); err != nil { return errors.NewError(err) } - return nil }, }, @@ -169,59 +164,49 @@ func Command(c *components.Components) components.Command { if err != nil { return errors.NewError(err) } - panelID, err := uuid.Parse(event.SlashCommandInteractionData().String("panel")) if err != nil { return errors.NewError(err) } - - if !g.QueryRolePanels().Where(rolepanel.ID(panelID)).ExistX(event) { + var rolePanel models.RolePanel + if err := c.GormDB().Where("id = ? AND guild_id = ?", panelID, g.ID).First(&rolePanel).Error; err != nil { return errors.NewError(errors.ErrorMessage("errors.not_exist", event)) } - - rolePanel := g.QueryRolePanels().WithEdit().Where(rolepanel.ID(panelID)).FirstX(event) - - if rolePanel.QueryEdit().ExistX(event) { - c.DB().RolePanelEdit.DeleteOneID(rolePanel.QueryEdit().FirstIDX(event)).ExecX(event) + var oldEdit models.RolePanelEdit + if err := c.GormDB().Where("parent_id = ?", rolePanel.ID).First(&oldEdit).Error; err == nil { + c.GormDB().Delete(&oldEdit) } - var removeRoles []snowflake.ID - var roles []discord.Role + var discordRoles []discord.Role for _, r := range rolePanel.Roles { - if roles == nil { - roles, err = event.Client().Rest.GetRoles(*event.GuildID()) + if discordRoles == nil { + discordRoles, err = event.Client().Rest.GetRoles(*event.GuildID()) if err != nil { return errors.NewError(err) } } - if slices.ContainsFunc(roles, func(role discord.Role) bool { return role.ID == r.ID }) { - continue + if !slices.ContainsFunc(discordRoles, func(role discord.Role) bool { return role.ID == r.ID }) { + removeRoles = append(removeRoles, r.ID) } - removeRoles = append(removeRoles, r.ID) } for _, id := range removeRoles { - rolePanel.Roles = slices.DeleteFunc(rolePanel.Roles, func(r schema.Role) bool { return r.ID == id }) + rolePanel.Roles = slices.DeleteFunc(rolePanel.Roles, func(r models.Role) bool { return r.ID == id }) } - rolePanel = - rolePanel.Update(). - SetUpdatedAt(time.Now()). - SetRoles(rolePanel.Roles). - SaveX(event) - - edit := c.DB().RolePanelEdit.Create(). - SetGuild(g). - SetParent(rolePanel). - SetChannelID(event.Channel().ID()). - SaveX(event) - - if err := event.CreateMessage( - rpEditBaseMessage(event, rolePanel, edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildCreate(), - ); err != nil { + rolePanel.UpdatedAt = time.Now() + c.GormDB().Save(&rolePanel) + edit := models.RolePanelEdit{ID: uuid.New(), GuildID: g.ID, ParentID: rolePanel.ID, ChannelID: event.Channel().ID()} + if err := c.GormDB().Create(&edit).Error; err != nil { + return errors.NewError(err) + } + edit.Parent = rolePanel + builder, err := rpEditBaseMessage(c, &rolePanel, &edit, event.Locale()) + if err != nil { + return errors.NewError(err) + } + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.CreateMessage(builder.BuildCreate()); err != nil { return errors.NewError(err) } - return nil }, }, @@ -235,22 +220,17 @@ func Command(c *components.Components) components.Command { if err != nil { return errors.NewError(err) } - panelID, err := uuid.Parse(event.SlashCommandInteractionData().String("panel")) if err != nil { return errors.NewError(err) } - place, err := createPanelPlace(event, c, panelID, event.Channel().ID(), g) if err != nil { return errors.NewError(errors.ErrorMessage("errors.not_exist", event)) } - - if err := event.CreateMessage( - rpPlaceBaseMenu(place, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildCreate(), - ); err != nil { + builder := rpPlaceBaseMenu(place, event.Locale()) + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.CreateMessage(builder.BuildCreate()); err != nil { return errors.NewError(err) } return nil @@ -270,42 +250,49 @@ func Command(c *components.Components) components.Command { if err != nil { return errors.NewError(err) } - - if !g.QueryRolePanels().Where(rolepanel.ID(panelID)).ExistX(event) { + var panel models.RolePanel + if err := c.GormDB().Where("id = ? AND guild_id = ?", panelID, g.ID).First(&panel).Error; err != nil { return errors.NewError(errors.ErrorMessage("errors.not_exist", event)) } - panel := g.QueryRolePanels().Where(rolepanel.ID(panelID)).FirstX(event) - - places := panel.QueryPlacements().AllX(event) - for _, place := range places { - if place.MessageID == nil { - continue + if err := c.GormDB().Transaction(func(tx *gorm.DB) error { + var places []models.RolePanelPlaced + if err := tx.Where("role_panel_id = ?", panel.ID).Find(&places).Error; err != nil { + return err } - _ = event.Client().Rest.DeleteMessage(place.ChannelID, *place.MessageID) - } - c.DB().RolePanelPlaced.Delete(). - Where(rolepanelplaced.HasRolePanelWith(rolepanel.ID(panel.ID))). - ExecX(event) - c.DB().RolePanelEdit.Delete(). - Where(rolepaneledit.HasParentWith(rolepanel.ID(panel.ID))). - ExecX(event) + for _, place := range places { + if place.MessageID != nil { + if err := event.Client().Rest.DeleteMessage(place.ChannelID, *place.MessageID); err != nil { + slog.Error("Failed to delete message", "error", err, "panel_id", panel.ID, "channel_id", place.ChannelID, "message_id", *place.MessageID) + return err + } + } + } - c.DB().RolePanel.DeleteOne(panel).ExecX(event) + if err := tx.Where("role_panel_id = ?", panel.ID).Delete(&models.RolePanelPlaced{}).Error; err != nil { + return err + } + if err := tx.Where("parent_id = ?", panel.ID).Delete(&models.RolePanelEdit{}).Error; err != nil { + return err + } + if err := tx.Delete(&panel).Error; err != nil { + return err + } + return nil + }); err != nil { + return errors.NewError(err) + } - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetEmbeds( - embeds.SetEmbedProperties( - discord.NewEmbedBuilder(). - SetTitle(translate.Message(event.Locale(), "components.role.panel.delete.message.embed.title")). - SetDescription(translate.Message(event.Locale(), "components.role.panel.delete.message.embed.description", translate.WithTemplate(map[string]any{"RolePanel": panel.Name}))). - Build(), - ), - ). - BuildCreate(), - ); err != nil { + builder := discord.NewMessageBuilder() + builder.SetEmbeds( + embeds.SetEmbedProperties( + discord.NewEmbedBuilder(). + SetTitle(translate.Message(event.Locale(), "components.role.panel.delete.message.embed.title")). + SetDescription(translate.Message(event.Locale(), "components.role.panel.delete.message.embed.description", translate.WithTemplate(map[string]any{"RolePanel": panel.Name}))). + Build(), + )) + if err := event.CreateMessage(builder.BuildCreate()); err != nil { return errors.NewError(err) } return nil @@ -313,27 +300,9 @@ func Command(c *components.Components) components.Command { }, }, AutocompleteHandlers: map[string]generic.PermissionAutocompleteHandler{ - "/role/panel/place:panel": generic.PAutocompleteHandler{ - Permission: []generic.Permission{ - generic.PermissionString("role.panel.place"), - }, - DiscordPerm: discord.PermissionManageRoles, - AutocompleteHandler: panelAutocomplete, - }, - "/role/panel/edit:panel": generic.PAutocompleteHandler{ - Permission: []generic.Permission{ - generic.PermissionString("role.panel.edit"), - }, - DiscordPerm: discord.PermissionManageRoles, - AutocompleteHandler: panelAutocomplete, - }, - "/role/panel/delete:panel": generic.PAutocompleteHandler{ - Permission: []generic.Permission{ - generic.PermissionString("role.panel.delete"), - }, - DiscordPerm: discord.PermissionManageRoles, - AutocompleteHandler: panelAutocomplete, - }, + "/role/panel/place:panel": generic.PAutocompleteHandler{Permission: []generic.Permission{generic.PermissionString("role.panel.place")}, DiscordPerm: discord.PermissionManageRoles, AutocompleteHandler: panelAutocomplete}, + "/role/panel/edit:panel": generic.PAutocompleteHandler{Permission: []generic.Permission{generic.PermissionString("role.panel.edit")}, DiscordPerm: discord.PermissionManageRoles, AutocompleteHandler: panelAutocomplete}, + "/role/panel/delete:panel": generic.PAutocompleteHandler{Permission: []generic.Permission{generic.PermissionString("role.panel.delete")}, DiscordPerm: discord.PermissionManageRoles, AutocompleteHandler: panelAutocomplete}, }, ModalHandlers: map[string]generic.ModalHandler{ "role:panel_create_modal": func(c *components.Components, event *events.ModalSubmitInteractionCreate) errors.Error { @@ -341,29 +310,24 @@ func Command(c *components.Components) components.Command { if err != nil { return errors.NewError(err) } - - rolePanel := c.DB().RolePanel.Create(). - SetName(event.Data.Text("name")). - SetDescription(event.Data.Text("description")). - SetGuild(g). - SaveX(event) - - edit := c.DB().RolePanelEdit.Create(). - SetGuild(g). - SetParent(rolePanel). - SetChannelID(event.Channel().ID()). - SaveX(event) - - initialize(edit, rolePanel) - - if err := event.CreateMessage( - rpEditBaseMessage(event, rolePanel, edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildCreate(), - ); err != nil { + rolePanel := models.RolePanel{ID: uuid.New(), Name: event.Data.Text("name"), Description: event.Data.Text("description"), GuildID: g.ID} + if err := c.GormDB().Create(&rolePanel).Error; err != nil { + return errors.NewError(err) + } + edit := models.RolePanelEdit{ID: uuid.New(), GuildID: g.ID, ParentID: rolePanel.ID, ChannelID: event.Channel().ID()} + if err := c.GormDB().Create(&edit).Error; err != nil { + return errors.NewError(err) + } + edit.Parent = rolePanel + initialize(&edit, &rolePanel) + builder, err := rpEditBaseMessage(c, &rolePanel, &edit, event.Locale()) + if err != nil { + return errors.NewError(err) + } + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.CreateMessage(builder.BuildCreate()); err != nil { return errors.NewError(err) } - return nil }, "role:panel_edit_modal": func(c *components.Components, event *events.ModalSubmitInteractionCreate) errors.Error { @@ -371,64 +335,43 @@ func Command(c *components.Components) components.Command { action := args[2] editID, err := uuid.Parse(args[3]) if err != nil { - return errors.NewError(err) + return errors.NewError(errors.ErrorMessage("errors.timeout", event)) } g, err := c.GuildCreateID(event, *event.GuildID()) if err != nil { return errors.NewError(err) } - if !g.QueryRolePanelEdits().Where(rolepaneledit.ID(editID)).ExistX(event) { + var edit models.RolePanelEdit + if err := c.GormDB().Where("id = ? AND guild_id = ?", editID, g.ID).First(&edit).Error; err != nil { return errors.NewError(errors.ErrorMessage("errors.timeout", event)) } - - edit := g.QueryRolePanelEdits().Where(rolepaneledit.ID(editID)).FirstX(event) - panel := edit.QueryParent().OnlyX(event) - - initialize(edit, panel) - + var panel models.RolePanel + c.GormDB().Where("id = ?", edit.ParentID).First(&panel) + initialize(&edit, &panel) switch action { case "change_name": - edit = edit.Update(). - SetModified(true). - SetName(event.Data.Text("name")). - SaveX(event) - if err := event.UpdateMessage( - rpEditBaseMessage(event, panel, edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { - return errors.NewError(err) - } + name := event.Data.Text("name") + edit.Modified, edit.Name = true, &name case "change_description": - edit = edit.Update(). - SetModified(true). - SetDescription(event.Data.Text("description")). - SaveX(event) - if err := event.UpdateMessage( - rpEditBaseMessage(event, panel, edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { - return errors.NewError(err) - } + desc := event.Data.Text("description") + edit.Modified, edit.Description = true, &desc case "set_display_name": if edit.SelectedRole != nil { - edit.Roles[slices.IndexFunc(edit.Roles, func(r schema.Role) bool { return r.ID == *edit.SelectedRole })].Name = event.Data.Text("display_name") - edit = edit.Update(). - SetModified(true). - SetRoles(edit.Roles). - SaveX(event) - } - - if err := event.UpdateMessage( - rpEditBaseMessage(event, panel, edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { - return errors.NewError(err) + idx := slices.IndexFunc(edit.Roles, func(r models.Role) bool { return r.ID == *edit.SelectedRole }) + if idx != -1 { + edit.Roles[idx].Name, edit.Modified = event.Data.Text("display_name"), true + } } } - + c.GormDB().Save(&edit) + builder, err := rpEditBaseMessage(c, &panel, &edit, event.Locale()) + if err != nil { + return errors.NewError(err) + } + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.UpdateMessage(builder.BuildUpdate()); err != nil { + return errors.NewError(err) + } return nil }, }, @@ -443,66 +386,42 @@ func Command(c *components.Components) components.Command { action := args[2] editID, err := uuid.Parse(args[3]) if err != nil { - return errors.NewError(err) + return errors.NewError(errors.ErrorMessage("errors.invalid_argument", event)) } - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - if !g.QueryRolePanelEdits().Where(rolepaneledit.ID(editID)).ExistX(event) { + g, _ := c.GuildCreateID(event, *event.GuildID()) + var edit models.RolePanelEdit + if err := c.GormDB().Where("id = ? AND guild_id = ?", editID, g.ID).First(&edit).Error; err != nil { return errors.NewError(errors.ErrorMessage("errors.timeout", event)) } - edit := g.QueryRolePanelEdits().Where(rolepaneledit.ID(editID)).FirstX(event) - panel := edit.QueryParent().OnlyX(event) - - initialize(edit, panel) - + var panel models.RolePanel + c.GormDB().Where("id = ?", edit.ParentID).First(&panel) + initialize(&edit, &panel) switch action { case "change_name", "change_description": - if err := event.Modal( - discord.NewModalCreateBuilder(). - SetTitle(translate.Message(event.Locale(), fmt.Sprintf("components.role.panel.edit.action.%s.title", action))). - SetCustomID(fmt.Sprintf("role:panel_edit_modal:%s:%s", action, edit.ID)). - SetComponents( - builtin.Or(action == "change_name", - discord.NewLabel(translate.Message(event.Locale(), "components.role.panel.edit.change_name.modal.input.name.label"), - discord.TextInputComponent{ - CustomID: "name", - Style: discord.TextInputStyleShort, - MinLength: builtin.Ptr(1), - MaxLength: 32, - Required: true, - Value: *edit.Name, - }, - ), - discord.NewLabel(translate.Message(event.Locale(), "components.role.panel.edit.change_description.modal.input.description.label"), - discord.TextInputComponent{ - CustomID: "description", - Style: discord.TextInputStyleParagraph, - MaxLength: 140, - Value: *edit.Description, - }, - ), - ), - ). - Build(), - ); err != nil { + nameVal, descVal := "", "" + if edit.Name != nil { + nameVal = *edit.Name + } + if edit.Description != nil { + descVal = *edit.Description + } + modal := discord.NewModalCreateBuilder().SetTitle(translate.Message(event.Locale(), fmt.Sprintf("components.role.panel.edit.action.%s.title", action))).SetCustomID(fmt.Sprintf("role:panel_edit_modal:%s:%s", action, edit.ID)).SetComponents(builtin.Or(action == "change_name", discord.NewLabel(translate.Message(event.Locale(), "components.role.panel.edit.change_name.modal.input.name.label"), discord.TextInputComponent{CustomID: "name", Style: discord.TextInputStyleShort, MinLength: builtin.Ptr(1), MaxLength: 32, Required: true, Value: nameVal}), discord.NewLabel(translate.Message(event.Locale(), "components.role.panel.edit.change_description.modal.input.description.label"), discord.TextInputComponent{CustomID: "description", Style: discord.TextInputStyleParagraph, MaxLength: 140, Value: descVal}))).Build() + if err := event.Modal(modal); err != nil { return errors.NewError(err) } case "modify_roles": - if err := event.UpdateMessage( - rpEditModifyRolesMessage(edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { + builder := rpEditModifyRolesMessage(&edit, event.Locale()) + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.UpdateMessage(builder.BuildUpdate()); err != nil { return errors.NewError(err) } case "base_menu": - if err := event.UpdateMessage( - rpEditBaseMessage(event, panel, edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { + builder, err := rpEditBaseMessage(c, &panel, &edit, event.Locale()) + if err != nil { + return errors.NewError(err) + } + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.UpdateMessage(builder.BuildUpdate()); err != nil { return errors.NewError(err) } case "add_role": @@ -512,24 +431,18 @@ func Command(c *components.Components) components.Command { return errors.NewError(errors.ErrorMessage("errors.invalid.self", event)) } var roles []discord.Role - roleMap := map[snowflake.ID]discord.Role{} for _, id := range self.RoleIDs { - role, err := event.Client().Rest.GetRole(*event.GuildID(), id) - if err != nil { - slog.Error("API ERROR GetRole", "error", err, "guild_id", *event.GuildID(), "id", id) - continue + if r, err := event.Client().Rest.GetRole(*event.GuildID(), id); err == nil { + roles = append(roles, *r) } - roleMap[id] = *role - roles = append(roles, *role) } highestRole := discordutil.GetHighestRole(roles) if highestRole == nil { return errors.NewError(errors.ErrorMessage("errors.invalid.self", event)) } var deletedRole []snowflake.ID - for i, r := range selectedRoles { - if slices.ContainsFunc(edit.Roles, func(r1 schema.Role) bool { return r1.ID == r.ID }) { + if slices.ContainsFunc(edit.Roles, func(r1 models.Role) bool { return r1.ID == r.ID }) { continue } if r.Managed || r.Compare(*highestRole) != -1 { @@ -537,46 +450,31 @@ func Command(c *components.Components) components.Command { deletedRole = append(deletedRole, i) continue } - edit.Roles = append(edit.Roles, schema.Role{ - ID: r.ID, - Name: r.Name, - }) + edit.Roles = append(edit.Roles, models.Role{ID: r.ID, Name: r.Name}) } - if len(deletedRole) > 0 { - var deletedRoleString string + var s string for _, id := range deletedRole { - deletedRoleString += fmt.Sprintf("- %s\r", discord.RoleMention(id)) + s += fmt.Sprintf("- %s\r", discord.RoleMention(id)) } - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetEmbeds( - embeds.SetEmbedProperties( - discord.NewEmbedBuilder(). - SetTitle(translate.Message(event.Locale(), "components.role.panel.edit.add_role.deleted_role.embed.title")). - SetDescriptionf("%s\n"+deletedRoleString, translate.Message(event.Locale(), "components.role.panel.edit.add_role.deleted_role.embed.description")). - Build(), - ), - ). - SetFlags(discord.MessageFlagEphemeral). - BuildCreate(), - ); err != nil { + embed := discord.NewEmbedBuilder().SetTitle(translate.Message(event.Locale(), "components.role.panel.edit.add_role.deleted_role.embed.title")).SetDescriptionf("%s\n"+s, translate.Message(event.Locale(), "components.role.panel.edit.add_role.deleted_role.embed.description")).Build() + if err := event.CreateMessage(discord.NewMessageBuilder(). + SetEmbeds(embeds.SetEmbedProperties(embed)). + SetFlags(discord.MessageFlagEphemeral). + BuildCreate()); err != nil { return errors.NewError(err) } return nil } - edit.Roles = slices.DeleteFunc(edit.Roles, func(r schema.Role) bool { _, ok := selectedRoles[r.ID]; return !ok }) - - edit = edit.Update(). - SetModified(true). - SetRoles(edit.Roles). - SaveX(event) - - if err := event.UpdateMessage( - rpEditBaseMessage(event, panel, edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { + edit.Roles = slices.DeleteFunc(edit.Roles, func(r models.Role) bool { _, ok := selectedRoles[r.ID]; return !ok }) + edit.Modified = true + c.GormDB().Save(&edit) + builder, err := rpEditBaseMessage(c, &panel, &edit, event.Locale()) + if err != nil { + return errors.NewError(err) + } + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.UpdateMessage(builder.BuildUpdate()); err != nil { return errors.NewError(err) } case "select_role": @@ -584,193 +482,144 @@ func Command(c *components.Components) components.Command { if values := event.StringSelectMenuInteractionData().Values; len(values) > 0 { id = builtin.Ptr(snowflake.MustParse(values[0])) } - if id == nil { - edit = edit.Update(). - ClearSelectedRole(). - SaveX(event) - } else { - edit = edit.Update(). - SetNillableSelectedRole(id). - SaveX(event) + edit.SelectedRole = id + c.GormDB().Save(&edit) + builder, err := rpEditBaseMessage(c, &panel, &edit, event.Locale()) + if err != nil { + return errors.NewError(err) } - - if err := event.UpdateMessage( - rpEditBaseMessage(event, panel, edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.UpdateMessage(builder.BuildUpdate()); err != nil { return errors.NewError(err) } case "delete": if edit.SelectedRole != nil { - edit.Roles = slices.DeleteFunc(edit.Roles, func(r schema.Role) bool { return r.ID == *edit.SelectedRole }) - edit = edit.Update(). - SetModified(true). - SetRoles(edit.Roles). - SaveX(event) + edit.Roles = slices.DeleteFunc(edit.Roles, func(r models.Role) bool { return r.ID == *edit.SelectedRole }) + edit.Modified = true + c.GormDB().Save(&edit) } - - if err := event.UpdateMessage( - rpEditBaseMessage(event, panel, edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { + builder, err := rpEditBaseMessage(c, &panel, &edit, event.Locale()) + if err != nil { + return errors.NewError(err) + } + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.UpdateMessage(builder.BuildUpdate()); err != nil { return errors.NewError(err) } case "move_up", "move_down": if edit.SelectedRole != nil { - index := slices.IndexFunc(edit.Roles, func(r schema.Role) bool { return r.ID == *edit.SelectedRole }) + idx := slices.IndexFunc(edit.Roles, func(r models.Role) bool { return r.ID == *edit.SelectedRole }) mv := builtin.Or(action == "move_up", -1, 1) - edit.Roles[index+mv], edit.Roles[index] = edit.Roles[index], edit.Roles[index+mv] - - edit = edit.Update(). - SetModified(true). - SetRoles(edit.Roles). - SaveX(event) + if idx+mv >= 0 && idx+mv < len(edit.Roles) { + edit.Roles[idx+mv], edit.Roles[idx], edit.Modified = edit.Roles[idx], edit.Roles[idx+mv], true + c.GormDB().Save(&edit) + } } - - if err := event.UpdateMessage( - rpEditBaseMessage(event, panel, edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { + builder, err := rpEditBaseMessage(c, &panel, &edit, event.Locale()) + if err != nil { + return errors.NewError(err) + } + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.UpdateMessage(builder.BuildUpdate()); err != nil { return errors.NewError(err) } case "set_display_name": if edit.SelectedRole != nil { - if err := event.Modal( - discord.NewModalCreateBuilder(). - SetTitle(translate.Message(event.Locale(), "components.role.panel.edit.set_display.name.modal.title")). - SetCustomID(fmt.Sprintf("role:panel_edit_modal:set_display_name:%s", edit.ID)). - SetComponents( - discord.NewLabel(translate.Message(event.Locale(), "components.role.panel.edit.set_display.name.modal.input.display_name.label"), - discord.TextInputComponent{ - CustomID: "display_name", - Style: discord.TextInputStyleShort, - MinLength: builtin.Ptr(1), - MaxLength: 100, - Required: true, - Value: edit.Roles[slices.IndexFunc(edit.Roles, func(r schema.Role) bool { return r.ID == *edit.SelectedRole })].Name, - }, - ), - ). - Build(), - ); err != nil { - return errors.NewError(err) + idx := slices.IndexFunc(edit.Roles, func(r models.Role) bool { return r.ID == *edit.SelectedRole }) + if idx != -1 { + modal := discord.NewModalCreateBuilder().SetTitle(translate.Message(event.Locale(), "components.role.panel.edit.set_display.name.modal.title")).SetCustomID(fmt.Sprintf("role:panel_edit_modal:set_display_name:%s", edit.ID)).SetComponents(discord.NewLabel(translate.Message(event.Locale(), "components.role.panel.edit.set_display.name.modal.input.display_name.label"), discord.TextInputComponent{CustomID: "display_name", Style: discord.TextInputStyleShort, MinLength: builtin.Ptr(1), MaxLength: 100, Required: true, Value: edit.Roles[idx].Name})).Build() + if err := event.Modal(modal); err != nil { + return errors.NewError(err) + } } } case "set_emoji": if edit.SelectedRole != nil { - edit = edit.Update(). - SetEmojiAuthor(event.User().ID). - SetToken(event.Token()). - SaveX(event) - } - - if err := event.UpdateMessage( - rpEditSetEmojiMessage(edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { + token := event.Token() + edit.EmojiAuthor = builtin.Ptr(event.User().ID) + edit.Token = &token + c.GormDB().Save(&edit) + } + builder := rpEditSetEmojiMessage(&edit, event.Locale()) + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.UpdateMessage(builder.BuildUpdate()); err != nil { return errors.NewError(err) } case "cancel_emoji", "reset_emoji": - edit = edit.Update(). - ClearEmojiAuthor(). - ClearToken(). - SaveX(event) - - if action == "reset_emoji" { - edit.Roles[slices.IndexFunc(edit.Roles, func(r schema.Role) bool { return r.ID == *edit.SelectedRole })].Emoji = nil - edit = edit.Update(). - SetModified(true). - SetRoles(edit.Roles). - SaveX(event) + edit.EmojiAuthor, edit.Token = nil, nil + if action == "reset_emoji" && edit.SelectedRole != nil { + idx := slices.IndexFunc(edit.Roles, func(r models.Role) bool { return r.ID == *edit.SelectedRole }) + if idx != -1 { + edit.Roles[idx].Emoji, edit.Modified = nil, true + } } - - if err := event.UpdateMessage( - rpEditBaseMessage(event, panel, edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { + if err := c.GormDB().Save(&edit).Error; err != nil { + return errors.NewError(err) + } + builder, err := rpEditBaseMessage(c, &panel, &edit, event.Locale()) + if err != nil { + return errors.NewError(err) + } + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.UpdateMessage(builder.BuildUpdate()); err != nil { return errors.NewError(err) } case "save_change": - - edit = edit.Update(). - SetModified(false). - SaveX(event) - - update := panel.Update(). - SetUpdatedAt(time.Now()). - SetNillableName(edit.Name). - SetNillableDescription(edit.Description) + edit.Modified = false + if err := c.GormDB().Save(&edit).Error; err != nil { + return errors.NewError(err) + } + if edit.Name != nil { + panel.Name = *edit.Name + } + if edit.Description != nil { + panel.Description = *edit.Description + } + panel.UpdatedAt = time.Now() if edit.Roles != nil { - update.SetRoles(edit.Roles) + panel.Roles = edit.Roles } - panel = update.SaveX(event) - - if err := event.UpdateMessage( - rpEditBaseMessage(event, panel, edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { + if err := c.GormDB().Save(&panel).Error; err != nil { + return errors.NewError(err) + } + builder, err := rpEditBaseMessage(c, &panel, &edit, event.Locale()) + if err != nil { + return errors.NewError(err) + } + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.UpdateMessage(builder.BuildUpdate()); err != nil { return errors.NewError(err) } case "apply_change": var ok bool - g.RolePanelEditTimes, ok = ratelimit.CheckLimit(g.RolePanelEditTimes, []ratelimit.Rule{ - { - Limit: 3, - Unit: time.Minute * 10, - }, - { - Limit: 5, - Unit: time.Minute * 30, - }, - }) - g.Update(). - SetRolePanelEditTimes(g.RolePanelEditTimes). - SaveX(event) + g.RolePanelEditTimes, ok = ratelimit.CheckLimit(g.RolePanelEditTimes, []ratelimit.Rule{{Limit: 3, Unit: time.Minute * 10}, {Limit: 5, Unit: time.Minute * 30}}) + c.GormDB().Save(g) if !ok || len(panel.Roles) < 1 { return errors.NewError(errors.ErrorMessage("errors.ratelimited", event)) } - - panel = panel.Update(). - SetAppliedAt(time.Now()). - SaveX(event) - - c.DB().RolePanelPlaced.Delete().Where(rolepanelplaced.And(rolepanelplaced.Or(rolepanelplaced.MessageIDIsNil(), rolepanelplaced.TypeIsNil()), rolepanelplaced.HasGuildWith(guild.ID(g.ID)))).ExecX(event) - go updateRolePanel(event, panel, event.Locale(), event.Client(), true) - if err := event.UpdateMessage( - rpEditBaseMessage(event, panel, edit, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { + panel.AppliedAt = time.Now() + c.GormDB().Save(&panel) + c.GormDB().Where("(message_id IS NULL OR type = '') AND guild_id = ?", g.ID).Delete(&models.RolePanelPlaced{}) + go updateRolePanel(context.Background(), &panel, event.Locale(), event.Client(), true, c) + builder, err := rpEditBaseMessage(c, &panel, &edit, event.Locale()) + if err != nil { return errors.NewError(err) } - case "place": - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.UpdateMessage(builder.BuildUpdate()); err != nil { return errors.NewError(err) } - + case "place": place, err := createPanelPlace(event, c, panel.ID, event.Channel().ID(), g) if err != nil { return errors.NewError(errors.ErrorMessage("errors.not_exist", event)) } - - if err := event.UpdateMessage( - rpPlaceBaseMenu(place, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { + builder := rpPlaceBaseMenu(place, event.Locale()) + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.UpdateMessage(builder.BuildUpdate()); err != nil { return errors.NewError(err) } - default: - slog.Warn("不明なcustom_id", "id", event.Data.CustomID()) } - return nil }, }, @@ -781,26 +630,17 @@ func Command(c *components.Components) components.Command { DiscordPerm: discord.PermissionManageRoles, ComponentHandler: func(c *components.Components, event *events.ComponentInteractionCreate) errors.Error { args := strings.Split(event.Data.CustomID(), ":") - action := args[2] - placeID, err := uuid.Parse(args[3]) - if err != nil { - return errors.NewError(err) - } - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - if !g.QueryRolePanelPlacements().Where(rolepanelplaced.ID(placeID)).ExistX(event) { + action, placeID := args[2], uuid.MustParse(args[3]) + g, _ := c.GuildCreateID(event, *event.GuildID()) + var place models.RolePanelPlaced + if err := c.GormDB().Where("id = ? AND guild_id = ?", placeID, g.ID).First(&place).Error; err != nil { return errors.NewError(errors.ErrorMessage("errors.timeout", event)) } - place := g.QueryRolePanelPlacements().Where(rolepanelplaced.ID(placeID)).FirstX(event) - panel := place.QueryRolePanel().OnlyX(event) - + var panel models.RolePanel + c.GormDB().Where("id = ?", place.RolePanelID).First(&panel) switch action { case "type": - place = place.Update(). - SetType(rolepanelplaced.Type(event.StringSelectMenuInteractionData().Values[0])). - SaveX(event) + place.Type = event.StringSelectMenuInteractionData().Values[0] case "button_type": var t = discord.ButtonStylePrimary switch event.StringSelectMenuInteractionData().Values[0] { @@ -813,56 +653,34 @@ func Command(c *components.Components) components.Command { case "gray": t = discord.ButtonStyleSecondary } - place = place.Update(). - SetButtonType(t). - SaveX(event) + place.ButtonType = t case "show_name": - place = place.Update(). - SetShowName(!place.ShowName). - SaveX(event) + place.ShowName = !place.ShowName case "folding_select_menu": - place = place.Update(). - SetFoldingSelectMenu(!place.FoldingSelectMenu). - SaveX(event) + place.FoldingSelectMenu = !place.FoldingSelectMenu case "hide_notice": - place = place.Update(). - SetHideNotice(!place.HideNotice). - SaveX(event) + place.HideNotice = !place.HideNotice case "use_display_name": - place = place.Update(). - SetUseDisplayName(!place.UseDisplayName). - SaveX(event) + place.UseDisplayName = !place.UseDisplayName case "create": if len(panel.Roles) < 1 { return errors.NewError(errors.ErrorMessage("errors.not_exist", event)) } - if err := rolePanelPlace(event, place, event.Locale(), event.Client(), true); err != nil { + if err := rolePanelPlace(event, &place, event.Locale(), event.Client(), true, c); err != nil { return errors.NewError(err) } - - updateMessage := discord.NewMessageBuilder(). - SetEmbeds( - embeds.SetEmbedProperties( - discord.NewEmbedBuilder(). - SetTitle(translate.Message(event.Locale(), "components.role.panel.create.message")). - SetDescription(translate.Message(event.Locale(), "components.role.panel.create.description")). - Build(), - ), - ). - BuildUpdate() - updateMessage.Components = &[]discord.LayoutComponent{} - if err := event.UpdateMessage( - updateMessage, - ); err != nil { + embed := discord.NewEmbedBuilder().SetTitle(translate.Message(event.Locale(), "components.role.panel.create.message")).SetDescription(translate.Message(event.Locale(), "components.role.panel.create.description")).Build() + if err := event.UpdateMessage(discord.NewMessageBuilder(). + SetEmbeds(embeds.SetEmbedProperties(embed)). + BuildUpdate()); err != nil { return errors.NewError(err) } return nil } - if err := event.UpdateMessage( - rpPlaceBaseMenu(place, event.Locale()). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { + c.GormDB().Save(&place) + builder := rpPlaceBaseMenu(&place, event.Locale()) + builder.SetFlags(discord.MessageFlagEphemeral) + if err := event.UpdateMessage(builder.BuildUpdate()); err != nil { return errors.NewError(err) } return nil @@ -870,99 +688,54 @@ func Command(c *components.Components) components.Command { }, "role:panel_use": generic.ComponentHandler(func(c *components.Components, event *events.ComponentInteractionCreate) errors.Error { args := strings.Split(event.Data.CustomID(), ":") - action := args[2] - placeID, err := uuid.Parse(args[3]) - if err != nil { - return errors.NewError(err) - } - g, err := c.GuildCreateID(event, *event.GuildID()) - if err != nil { - return errors.NewError(err) - } - if !g.QueryRolePanelPlacements().Where(rolepanelplaced.ID(placeID)).ExistX(event) { - if err := event.Client().Rest.DeleteMessage(event.Channel().ID(), event.Message.ID); err != nil { - return errors.NewError(err) - } + action, placeID := args[2], uuid.MustParse(args[3]) + g, _ := c.GuildCreateID(event, *event.GuildID()) + var place models.RolePanelPlaced + if err := c.GormDB().Where("id = ? AND guild_id = ?", placeID, g.ID).First(&place).Error; err != nil { + _ = event.Client().Rest.DeleteMessage(event.Channel().ID(), event.Message.ID) return errors.NewError(errors.ErrorMessage("errors.deleted", event)) } - place := g.QueryRolePanelPlacements().Where(rolepanelplaced.ID(placeID)).FirstX(event) - switch action { case "button": roleID := snowflake.MustParse(args[4]) - if !slices.ContainsFunc(place.Roles, func(r schema.Role) bool { return r.ID == roleID }) { - if err := event.UpdateMessage( - rpPlacedMessage(place, event.Locale()). - BuildUpdate(), - ); err != nil { + if !slices.ContainsFunc(place.Roles, func(r models.Role) bool { return r.ID == roleID }) { + if err := event.UpdateMessage(rpPlacedMessage(&place, event.Locale()).BuildUpdate()); err != nil { return errors.NewError(err) } return nil } - - _, ok := event.Client().Caches.Role(*event.GuildID(), roleID) - if !ok { - if err := event.DeferUpdateMessage(); err != nil { - return errors.NewError(err) - } + if _, ok := event.Client().Caches.Role(*event.GuildID(), roleID); !ok { + _ = event.DeferUpdateMessage() return nil } - contain := slices.Contains(event.Member().RoleIDs, roleID) + reason := rest.WithReason(fmt.Sprintf("Role Panel \"%s\" (%s)", place.Name, place.ID)) if contain { - if err := event.Client().Rest.RemoveMemberRole(g.ID, event.User().ID, roleID, rest.WithReason(fmt.Sprintf("Role Panel \"%s\" (%s)", place.Name, place.ID))); err != nil { + if err := event.Client().Rest.RemoveMemberRole(g.ID, event.User().ID, roleID, reason); err != nil { return errors.NewError(errors.ErrorMessage("errors.fail.role.panel", event)) } } else { - if err := event.Client().Rest.AddMemberRole(g.ID, event.User().ID, roleID, rest.WithReason(fmt.Sprintf("Role Panel \"%s\" (%s)", place.Name, place.ID))); err != nil { + if err := event.Client().Rest.AddMemberRole(g.ID, event.User().ID, roleID, reason); err != nil { return errors.NewError(errors.ErrorMessage("errors.fail.role.panel", event)) } } - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetEmbeds( - embeds.SetEmbedProperties( - discord.NewEmbedBuilder(). - SetTitle(translate.Message(event.Locale(), "components.role.panel.use."+builtin.Or(!contain, "added", "removed"))). - SetDescription(translate.Message(event.Locale(), "components.role.panel.use."+builtin.Or(!contain, "added", "removed")+".description", translate.WithTemplate(map[string]any{"Role": discord.RoleMention(roleID)}))). - Build(), - ), - ). - SetFlags(discord.MessageFlagEphemeral). - BuildCreate(), - ); err != nil { + embed := discord.NewEmbedBuilder().SetTitle(translate.Message(event.Locale(), "components.role.panel.use."+builtin.Or(!contain, "added", "removed"))).SetDescription(translate.Message(event.Locale(), "components.role.panel.use."+builtin.Or(!contain, "added", "removed")+`.description`, translate.WithTemplate(map[string]any{"Role": discord.RoleMention(roleID)}))).Build() + if err := event.CreateMessage(discord.NewMessageBuilder(). + SetEmbeds(embeds.SetEmbedProperties(embed)). + SetFlags(discord.MessageFlagEphemeral). + BuildCreate()); err != nil { return errors.NewError(err) } case "select_menu_fold": options := make([]discord.StringSelectMenuOption, len(place.Roles)) for i, role := range place.Roles { - if role.Emoji == nil { - role.Emoji = &discord.ComponentEmoji{ - Name: discordutil.Index2Emoji(i), - } - } - options[i] = discord.StringSelectMenuOption{ - Label: role.Name, - Value: role.ID.String(), - Emoji: role.Emoji, - Default: slices.Contains(event.Member().RoleIDs, role.ID), + emojiVal := role.Emoji + if emojiVal == nil { + emojiVal = &discord.ComponentEmoji{Name: discordutil.Index2Emoji(i)} } + options[i] = discord.StringSelectMenuOption{Label: role.Name, Value: role.ID.String(), Emoji: emojiVal, Default: slices.Contains(event.Member().RoleIDs, role.ID)} } - actionRow := discord.NewActionRow( - discord.StringSelectMenuComponent{ - CustomID: fmt.Sprintf("role:panel_use:select_menu:%s", place.ID.String()), - Placeholder: translate.Message(event.Locale(), "components.role.panel.components.select_menu.placeholder"), - MinValues: builtin.Ptr(0), - MaxValues: len(place.Roles), - Options: options, - }, - ) - if err := event.CreateMessage( - discord.NewMessageBuilder(). - SetComponents(actionRow). - SetFlags(discord.MessageFlagEphemeral). - BuildCreate(), - ); err != nil { + if err := event.CreateMessage(discord.NewMessageBuilder().SetComponents(discord.NewActionRow(discord.StringSelectMenuComponent{CustomID: fmt.Sprintf("role:panel_use:select_menu:%s", place.ID.String()), Placeholder: translate.Message(event.Locale(), "components.role.panel.components.select_menu.placeholder"), MinValues: builtin.Ptr(0), MaxValues: len(place.Roles), Options: options})).SetFlags(discord.MessageFlagEphemeral).BuildCreate()); err != nil { return errors.NewError(err) } case "select_menu": @@ -970,88 +743,47 @@ func Command(c *components.Components) components.Command { for _, v := range event.StringSelectMenuInteractionData().Values { selectedRoles = append(selectedRoles, snowflake.MustParse(v)) } - - var addRoles []snowflake.ID - var removedRoles []snowflake.ID - var unchangedRole []snowflake.ID + var addRoles, removedRoles, unchangedRole []snowflake.ID for _, role := range place.Roles { if slices.Contains(selectedRoles, role.ID) { - // 選ばれたとき if slices.Index(event.Member().RoleIDs, role.ID) != -1 { - // 持ってたなら unchangedRole = append(unchangedRole, role.ID) - continue } else { - // 持ってないなら - _, ok := event.Client().Caches.Role(*event.GuildID(), role.ID) - if !ok { - continue - } - addRoles = append(addRoles, role.ID) - - if err := event.Client().Rest.AddMemberRole(*event.GuildID(), event.User().ID, role.ID); err != nil { - return errors.NewError(errors.ErrorMessage("errors.fail.role.panel", event)) + if _, ok := event.Client().Caches.Role(*event.GuildID(), role.ID); ok { + addRoles = append(addRoles, role.ID) + _ = event.Client().Rest.AddMemberRole(*event.GuildID(), event.User().ID, role.ID) } } } else { - // 選ばれてないとき if slices.Index(event.Member().RoleIDs, role.ID) != -1 { - // 持ってたなら removedRoles = append(removedRoles, role.ID) - - if err := event.Client().Rest.RemoveMemberRole(*event.GuildID(), event.User().ID, role.ID); err != nil { - return errors.NewError(errors.ErrorMessage("errors.fail.role.panel", event)) - } - } else { - // 持ってないなら - continue + _ = event.Client().Rest.RemoveMemberRole(*event.GuildID(), event.User().ID, role.ID) } } } - - embed := discord.NewEmbedBuilder(). - SetTitle(translate.Message(event.Locale(), "components.role.panel.use.changed")) + embed := discord.NewEmbedBuilder().SetTitle(translate.Message(event.Locale(), "components.role.panel.use.changed")) if len(addRoles) > 0 { - var addRolesString string + var s string for _, id := range addRoles { - addRolesString += fmt.Sprintf("%s\n", discord.RoleMention(id)) + s += fmt.Sprintf("%s\n", discord.RoleMention(id)) } - embed.AddFields( - discord.EmbedField{ - Name: translate.Message(event.Locale(), "components.role.panel.use.changed.add"), - Value: addRolesString, - }, - ) + embed.AddFields(discord.EmbedField{Name: translate.Message(event.Locale(), "components.role.panel.use.changed.add"), Value: s}) } if len(unchangedRole) > 0 { - var unchangedRoleString string + var s string for _, id := range unchangedRole { - unchangedRoleString += fmt.Sprintf("%s\n", discord.RoleMention(id)) + s += fmt.Sprintf("%s\n", discord.RoleMention(id)) } - embed.AddFields( - discord.EmbedField{ - Name: translate.Message(event.Locale(), "components.role.panel.use.changed.unchanged"), - Value: unchangedRoleString, - }, - ) + embed.AddFields(discord.EmbedField{Name: translate.Message(event.Locale(), "components.role.panel.use.changed.unchanged"), Value: s}) } if len(removedRoles) > 0 { - var removedRolesString string + var s string for _, id := range removedRoles { - removedRolesString += fmt.Sprintf("%s\n", discord.RoleMention(id)) + s += fmt.Sprintf("%s\n", discord.RoleMention(id)) } - embed.AddFields( - discord.EmbedField{ - Name: translate.Message(event.Locale(), "components.role.panel.use.changed.remove"), - Value: removedRolesString, - }, - ) + embed.AddFields(discord.EmbedField{Name: translate.Message(event.Locale(), "components.role.panel.use.changed.remove"), Value: s}) } - if err := event.RespondMessage( - discord.NewMessageBuilder(). - SetEmbeds(embeds.SetEmbedProperties(embed.Build())). - SetFlags(discord.MessageFlagEphemeral), - ); err != nil { + if err := event.RespondMessage(discord.NewMessageBuilder().SetEmbeds(embeds.SetEmbedProperties(embed.Build())).SetFlags(discord.MessageFlagEphemeral)); err != nil { return errors.NewError(err) } } @@ -1064,177 +796,129 @@ func Command(c *components.Components) components.Command { if event.Message.Author.Bot || event.Message.Author.System { return nil } - g, err := c.GuildCreateID(event, event.GuildID) - if err != nil { - return errors.NewError(err) - } u, err := c.UserCreate(event, event.Message.Author) if err != nil { return errors.NewError(err) } - - edits := g.QueryRolePanelEdits().Where(rolepaneledit.ChannelID(event.ChannelID)).AllX(event) + var edits []models.RolePanelEdit + c.GormDB().Where("channel_id = ?", event.ChannelID).Find(&edits) for _, edit := range edits { if edit.EmojiAuthor == nil || *edit.EmojiAuthor != event.Message.Author.ID || edit.Token == nil { continue } - token := *edit.Token - emojis := emoji.FindAllString(event.Message.Content) + token, emojis := *edit.Token, emoji.FindAllString(event.Message.Content) if len(emojis) < 1 { continue } componentEmoji := discordutil.ParseComponentEmoji(emojis[0]) - panel := edit.QueryParent().OnlyX(event) - - initialize(edit, panel) - - edit.Roles[slices.IndexFunc(edit.Roles, func(r schema.Role) bool { return r.ID == *edit.SelectedRole })].Emoji = &componentEmoji - edit = edit.Update(). - ClearEmojiAuthor(). - ClearToken(). - SetRoles(edit.Roles). - SaveX(event) - - if err := event.Client().Rest.AddReaction(event.ChannelID, event.MessageID, "✅"); err != nil { - return errors.NewError(err) + var panel models.RolePanel + if err := c.GormDB().Where("id = ?", edit.ParentID).First(&panel).Error; err != nil { + continue } - - if _, err := event.Client().Rest.UpdateInteractionResponse(event.Client().ApplicationID, token, - rpEditBaseMessage(event, panel, edit, u.Locale). - SetFlags(discord.MessageFlagEphemeral). - BuildUpdate(), - ); err != nil { - return errors.NewError(err) + initialize(&edit, &panel) + if edit.SelectedRole != nil { + idx := slices.IndexFunc(edit.Roles, func(r models.Role) bool { return r.ID == *edit.SelectedRole }) + if idx != -1 { + edit.Roles[idx].Emoji, edit.EmojiAuthor, edit.Token = &componentEmoji, nil, nil + c.GormDB().Save(&edit) + } + } + _ = event.Client().Rest.AddReaction(event.ChannelID, event.MessageID, "✅") + builder, err := rpEditBaseMessage(c, &panel, &edit, u.Locale) + if err == nil { + _, _ = event.Client().Rest.UpdateInteractionResponse(event.Client().ApplicationID, token, builder.SetFlags(discord.MessageFlagEphemeral).BuildUpdate()) } } case *events.GuildMessageDelete: - g, err := c.GuildCreateID(event, event.GuildID) - if err != nil { - return errors.NewError(err) - } - - c.DB().RolePanelPlaced.Delete(). - Where( - rolepanelplaced.And( - rolepanelplaced.HasGuildWith(guild.ID(g.ID)), - rolepanelplaced.ChannelID(event.ChannelID), - rolepanelplaced.MessageID(event.MessageID), - ), - ). - ExecX(event) + g, _ := c.GuildCreateID(event, event.GuildID) + c.GormDB().Where("guild_id = ? AND channel_id = ? AND message_id = ?", g.ID, event.ChannelID, event.MessageID).Delete(&models.RolePanelPlaced{}) case *events.GuildMessageReactionAdd: if event.Member.User.Bot || event.Member.User.System { return nil } - g, err := c.GuildCreateID(event, event.GuildID) - if err != nil { - return errors.NewError(err) - } u, err := c.UserCreate(event, event.Member.User) if err != nil { return errors.NewError(err) } - - if !g.QueryRolePanelPlacements().Where(rolepanelplaced.ChannelID(event.ChannelID), rolepanelplaced.MessageID(event.MessageID)).ExistX(event) { + var place models.RolePanelPlaced + if err := c.GormDB().Where("channel_id = ? AND message_id = ?", event.ChannelID, event.MessageID).First(&place).Error; err != nil { return nil } - place := g.QueryRolePanelPlacements().Where(rolepanelplaced.ChannelID(event.ChannelID), rolepanelplaced.MessageID(event.MessageID)).FirstX(event) - panel := place.QueryRolePanel().OnlyX(event) - - if err := event.Client().Rest.RemoveUserReaction(event.ChannelID, event.MessageID, event.Emoji.Reaction(), event.UserID); err != nil { - return errors.NewError(err) + var panel models.RolePanel + if err := c.GormDB().Where("id = ?", place.RolePanelID).First(&panel).Error; err != nil { + return nil } - + _ = event.Client().Rest.RemoveUserReaction(event.ChannelID, event.MessageID, event.Emoji.Reaction(), event.UserID) for i, role := range panel.Roles { - if role.Emoji == nil { - role.Emoji = &discord.ComponentEmoji{ - Name: discordutil.Index2Emoji(i), - } + emojiVal := role.Emoji + if emojiVal == nil { + emojiVal = &discord.ComponentEmoji{Name: discordutil.Index2Emoji(i)} } - if event.Emoji.Reaction() != discordutil.ReactionComponentEmoji(*role.Emoji) { + if event.Emoji.Reaction() != discordutil.ReactionComponentEmoji(*emojiVal) { continue } - _, ok := event.Client().Caches.Role(event.GuildID, role.ID) - if !ok { + if _, ok := event.Client().Caches.Role(event.GuildID, role.ID); !ok { return nil } contains := slices.Contains(event.Member.RoleIDs, role.ID) + var err error if contains { err = event.Client().Rest.RemoveMemberRole(event.GuildID, event.UserID, role.ID) } else { err = event.Client().Rest.AddMemberRole(event.GuildID, event.UserID, role.ID) } if err != nil { - m, err := event.Client().Rest.CreateMessage(event.ChannelID, - discord.NewMessageBuilder(). - SetEmbeds( - embeds.SetEmbedProperties( - discord.NewEmbedBuilder(). - SetTitlef("❗ %s", translate.Message(u.Locale, "errors.fail.role.panel")). - SetDescription(translate.Message(u.Locale, "errors.fail.role.panel.description")). - SetColor(0xff2121). - Build(), - ), - ). - SetFlags(discord.MessageFlagEphemeral).BuildCreate(), - ) - if err != nil { - return errors.NewError(err) + embed := discord.NewEmbedBuilder().SetTitlef("❗ %s", translate.Message(u.Locale, "errors.fail.role.panel")).SetDescription(translate.Message(u.Locale, "errors.fail.role.panel.description")).SetColor(0xff2121).Build() + m, err := event.Client().Rest.CreateMessage(event.ChannelID, discord.NewMessageBuilder(). + SetEmbeds(embeds.SetEmbedProperties(embed)). + SetFlags(discord.MessageFlagEphemeral). + BuildCreate()) + if err == nil { + go func() { + if err := discordutil.DeleteMessageAfter(event.Client(), event.ChannelID, m.ID, time.Second*10); err != nil { + slog.Error("Failed to delete message", "err", err) + } + }() } - go func() { - if err := discordutil.DeleteMessageAfter(event.Client(), event.ChannelID, m.ID, time.Second*10); err != nil { - slog.Error("削除に失敗", "err", err, "channel_id", event.ChannelID, "message_id", m.ID) - } - }() - return nil - } - if place.HideNotice { return nil } - m, err := event.Client().Rest.CreateMessage(event.ChannelID, - discord.NewMessageBuilder(). + if !place.HideNotice { + embed := discord.NewEmbedBuilder(). + SetTitle(translate.Message(u.Locale, "components.role.panel.use."+builtin.Or(!contains, "added", "removed"))). + SetDescription(translate.Message(u.Locale, "components.role.panel.use."+builtin.Or(!contains, "added", "removed")+`.description`, translate.WithTemplate(map[string]any{"Role": discord.RoleMention(role.ID)}))). + Build() + m, err := event.Client().Rest.CreateMessage(event.ChannelID, discord.NewMessageBuilder(). SetContent(discord.UserMention(event.UserID)). - SetEmbeds( - embeds.SetEmbedProperties( - discord.NewEmbedBuilder(). - SetTitle(translate.Message(u.Locale, "components.role.panel.use."+builtin.Or(!contains, "added", "removed"))). - SetDescription(translate.Message(u.Locale, "components.role.panel.use."+builtin.Or(!contains, "added", "removed")+".description", translate.WithTemplate(map[string]any{"Role": discord.RoleMention(role.ID)}))). - Build(), - ), - ). - SetFlags(discord.MessageFlagEphemeral).BuildCreate(), - ) - if err != nil { - return errors.NewError(err) - } - go func() { - if err := discordutil.DeleteMessageAfter(event.Client(), event.ChannelID, m.ID, time.Second*10); err != nil { - slog.Error("削除に失敗", "err", err, "channel_id", event.ChannelID, "message_id", m.ID) + SetEmbeds(embeds.SetEmbedProperties(embed)). + SetFlags(discord.MessageFlagEphemeral). + BuildCreate()) + if err == nil { + go func() { + if err := discordutil.DeleteMessageAfter(event.Client(), event.ChannelID, m.ID, time.Second*10); err != nil { + slog.Error("Failed to delete message", "err", err) + } + }() } - }() + } } } return nil - }, - }).SetComponent(c) + }}).SetComponent(c) } -func UpdateRolePanel(ctx context.Context, place *ent.RolePanelPlaced, locale discord.Locale, client *bot.Client) { - if err := rolePanelPlace(ctx, place, locale, client, true); err != nil { +func UpdateRolePanel(ctx context.Context, place *models.RolePanelPlaced, locale discord.Locale, client *bot.Client, c *components.Components) { + if err := rolePanelPlace(ctx, place, locale, client, true, c); err != nil { slog.Error("アップデートに失敗", "err", err) } } -func updateRolePanel(ctx context.Context, panel *ent.RolePanel, locale discord.Locale, client *bot.Client, react bool) { - places := panel.QueryPlacements().AllX(ctx) +func updateRolePanel(ctx context.Context, panel *models.RolePanel, locale discord.Locale, client *bot.Client, react bool, c *components.Components) { + var places []models.RolePanelPlaced + c.GormDB().Where("role_panel_id = ?", panel.ID).Find(&places) for _, place := range places { - place = place.Update(). - SetName(panel.Name). - SetDescription(panel.Description). - SetRoles(panel.Roles). - SetUpdatedAt(time.Now()). - SaveX(ctx) - if err := rolePanelPlace(ctx, place, locale, client, react); err != nil { + place.Name, place.Description, place.Roles, place.UpdatedAt = panel.Name, panel.Description, panel.Roles, time.Now() + c.GormDB().Save(&place) + if err := rolePanelPlace(ctx, &place, locale, client, react, c); err != nil { slog.Error("アップデートに失敗", "err", err) } } diff --git a/bot/commands/setting/setting.go b/bot/commands/setting/setting.go index 9e05857c..a18d0409 100644 --- a/bot/commands/setting/setting.go +++ b/bot/commands/setting/setting.go @@ -31,7 +31,7 @@ import ( "github.com/disgoorg/snowflake/v2" "github.com/sabafly/gobot/bot/components" "github.com/sabafly/gobot/bot/components/generic" - "github.com/sabafly/gobot/ent" + "github.com/sabafly/gobot/database/models" "github.com/sabafly/gobot/internal/builtin" "github.com/sabafly/gobot/internal/embeds" "github.com/sabafly/gobot/internal/errors" @@ -125,29 +125,6 @@ func Command(c *components.Components) components.Command { }, }, }, - // discord.ApplicationCommandOptionSubCommandGroup{ - // Name: "welcome", - // Description: "welcome", - // Options: []discord.ApplicationCommandOptionSubCommand{ - // { - // Name: "set-message", - // Description: "set message", - // DescriptionLocalizations: translate.MessageMap("components.setting.welcome.set-message", false), - // }, - // { - // Name: "set-channel", - // Description: "set channel", - // DescriptionLocalizations: translate.MessageMap("components.setting.welcome.set-channel", false), - // Options: []discord.ApplicationCommandOption{ - // discord.ApplicationCommandOptionChannel{ - // Name: "channel", - // Description: "channel", - // DescriptionLocalizations: translate.MessageMap("components.setting.welcome.channel", false), - // }, - // }, - // }, - // }, - // }, }, }, }, @@ -200,9 +177,11 @@ func Command(c *components.Components) components.Command { if err != nil { return errors.NewError(err) } - g = g.Update(). - SetBumpEnabled(!g.BumpEnabled). - SaveX(event) + g.BumpEnabled = !g.BumpEnabled + if err := c.GormDB().Save(g).Error; err != nil { + return errors.NewError(err) + } + if err := event.CreateMessage( discord.NewMessageBuilder(). SetContent(translate.Message(event.Locale(), "components.setting.bump.toggle."+builtin.Or(g.BumpEnabled, "enabled", "disabled"))). @@ -223,9 +202,11 @@ func Command(c *components.Components) components.Command { if err != nil { return errors.NewError(err) } - g = g.Update(). - SetUpEnabled(!g.UpEnabled). - SaveX(event) + g.UpEnabled = !g.UpEnabled + if err := c.GormDB().Save(g).Error; err != nil { + return errors.NewError(err) + } + if err := event.CreateMessage( discord.NewMessageBuilder(). SetContent(translate.Message(event.Locale(), "components.setting.up.toggle."+builtin.Or(g.UpEnabled, "enabled", "disabled"))). @@ -246,13 +227,15 @@ func Command(c *components.Components) components.Command { if err != nil { return errors.NewError(err) } - update := g.Update() if r, ok := event.SlashCommandInteractionData().OptRole("target"); ok { - update.SetBumpMention(r.ID) + g.BumpMention = &r.ID } else { - update.ClearBumpMention() + g.BumpMention = nil } - g = update.SaveX(event) + if err := c.GormDB().Save(g).Error; err != nil { + return errors.NewError(err) + } + if err := event.CreateMessage( discord.NewMessageBuilder(). SetContent(translate.Message(event.Locale(), "components.setting.bump.mention.used", @@ -280,13 +263,15 @@ func Command(c *components.Components) components.Command { if err != nil { return errors.NewError(err) } - update := g.Update() if r, ok := event.SlashCommandInteractionData().OptRole("target"); ok { - update.SetUpMention(r.ID) + g.UpMention = &r.ID } else { - update.ClearUpMention() + g.UpMention = nil + } + if err := c.GormDB().Save(g).Error; err != nil { + return errors.NewError(err) } - g = update.SaveX(event) + if err := event.CreateMessage( discord.NewMessageBuilder(). SetContent(translate.Message(event.Locale(), "components.setting.up.mention.used", @@ -450,9 +435,11 @@ func Command(c *components.Components) components.Command { if err != nil { return errors.NewError(err) } - g = g.Update(). - SetLevelingDisabled(!g.LevelingDisabled). - SaveX(event) + g.LevelingDisabled = !g.LevelingDisabled + if err := c.GormDB().Save(g).Error; err != nil { + return errors.NewError(err) + } + if err := event.CreateMessage( discord.NewMessageBuilder(). SetContent(translate.Message(event.Locale(), "components.setting.leveling.enable."+builtin.Or(!g.LevelingDisabled, "enabled", "disabled"))). @@ -470,12 +457,14 @@ func Command(c *components.Components) components.Command { if err != nil { return errors.NewError(err) } - g.Update(). - SetBumpMessageTitle(event.ModalSubmitInteraction.Data.Text("message_title")). - SetBumpMessage(event.ModalSubmitInteraction.Data.Text("message")). - SetBumpRemindMessageTitle(event.ModalSubmitInteraction.Data.Text("remind.message_title")). - SetBumpRemindMessage(event.ModalSubmitInteraction.Data.Text("remind.message")). - ExecX(event) + g.BumpMessageTitle = event.ModalSubmitInteraction.Data.Text("message_title") + g.BumpMessage = event.ModalSubmitInteraction.Data.Text("message") + g.BumpRemindMessageTitle = event.ModalSubmitInteraction.Data.Text("remind.message_title") + g.BumpRemindMessage = event.ModalSubmitInteraction.Data.Text("remind.message") + if err := c.GormDB().Save(g).Error; err != nil { + return errors.NewError(err) + } + if err := event.DeferUpdateMessage(); err != nil { return errors.NewError(err) } @@ -486,12 +475,14 @@ func Command(c *components.Components) components.Command { if err != nil { return errors.NewError(err) } - g.Update(). - SetUpMessageTitle(event.ModalSubmitInteraction.Data.Text("message_title")). - SetUpMessage(event.ModalSubmitInteraction.Data.Text("message")). - SetUpRemindMessageTitle(event.ModalSubmitInteraction.Data.Text("remind.message_title")). - SetUpRemindMessage(event.ModalSubmitInteraction.Data.Text("remind.message")). - ExecX(event) + g.UpMessageTitle = event.ModalSubmitInteraction.Data.Text("message_title") + g.UpMessage = event.ModalSubmitInteraction.Data.Text("message") + g.UpRemindMessageTitle = event.ModalSubmitInteraction.Data.Text("remind.message_title") + g.UpRemindMessage = event.ModalSubmitInteraction.Data.Text("remind.message") + if err := c.GormDB().Save(g).Error; err != nil { + return errors.NewError(err) + } + if err := event.DeferUpdateMessage(); err != nil { return errors.NewError(err) } @@ -589,7 +580,7 @@ type notice struct { var bumpNotice = map[snowflake.ID]notice{} var bumpLock sync.Mutex -func bumpHandler(c *components.Components, g *ent.Guild, event *events.GuildMessageCreate) error { +func bumpHandler(c *components.Components, g *models.Guild, event *events.GuildMessageCreate) error { bumpLock.Lock() defer bumpLock.Unlock() if event.Message.Interaction == nil || event.Message.Interaction.Name != "bump" { @@ -615,7 +606,7 @@ func bumpHandler(c *components.Components, g *ent.Guild, event *events.GuildMess var upNotice = map[snowflake.ID]notice{} var upLock sync.Mutex -func upHandler(c *components.Components, g *ent.Guild, event *events.GuildMessageCreate) error { +func upHandler(c *components.Components, g *models.Guild, event *events.GuildMessageCreate) error { upLock.Lock() defer upLock.Unlock() if event.Message.Interaction == nil || event.Message.Interaction.Name != "dissoku up" { diff --git a/bot/components/components.go b/bot/components/components.go index 89a659a9..72c1999c 100644 --- a/bot/components/components.go +++ b/bot/components/components.go @@ -24,16 +24,14 @@ import ( "context" "github.com/sabafly/gobot/database" - "github.com/sabafly/gobot/ent" "github.com/sabafly/gobot/internal/smap" "gorm.io/gorm" "gorm.io/gorm/clause" ) -func New(ctx context.Context, db *ent.Client, conf Config, gormDb *database.DB) *Components { +func New(ctx context.Context, conf Config, gormDb *database.DB) *Components { return &Components{ ctx: ctx, - db: db, commandsRegistry: make(map[string]Command), config: conf, gormDb: gormDb, @@ -42,7 +40,6 @@ func New(ctx context.Context, db *ent.Client, conf Config, gormDb *database.DB) type Components struct { ctx context.Context - db *ent.Client gormDb *database.DB config Config @@ -56,8 +53,6 @@ type Components struct { func (c *Components) Ctx() context.Context { return c.ctx } -func (c *Components) DB() *ent.Client { return c.db } - // DO NOT REUSE RETURN VALUE, MUST CALL EACH TIME TO GET NEW SESSION func (c *Components) GormDB() *gorm.DB { return c.gormDb.DB.WithContext(c.ctx). diff --git a/bot/components/generic/permission.go b/bot/components/generic/permission.go index 79d631be..9b638cb5 100644 --- a/bot/components/generic/permission.go +++ b/bot/components/generic/permission.go @@ -31,10 +31,7 @@ import ( "github.com/disgoorg/disgo/rest" "github.com/disgoorg/snowflake/v2" "github.com/sabafly/gobot/bot/components" - "github.com/sabafly/gobot/ent" - "github.com/sabafly/gobot/ent/guild" - "github.com/sabafly/gobot/ent/member" - "github.com/sabafly/gobot/ent/user" + "github.com/sabafly/gobot/database/models" "github.com/sabafly/gobot/internal/translate" ) @@ -62,30 +59,26 @@ func noPermissionMessage(event interface { ) } -func PermissionCheck(ctx context.Context, c *components.Components, g *ent.Guild, client *bot.Client, m discord.ResolvedMember, guildID snowflake.ID, perms []Permission) bool { +func PermissionCheck(ctx context.Context, c *components.Components, g *models.Guild, client *bot.Client, m discord.ResolvedMember, guildID snowflake.ID, perms []Permission) bool { if len(perms) == 0 { return true } - if m := c.DB().Guild.Query(). - Where(guild.ID(guildID)). - FirstX(ctx). - QueryMembers(). - Where(member.HasUserWith(user.ID(m.User.ID))). - FirstX(ctx); m != nil { + var member models.Member + if err := c.GormDB().Where("guild_id = ? AND user_id = ?", guildID, m.User.ID).First(&member).Error; err == nil { for _, p := range perms { var r bool if p.Default() { - if m.Permission.Disabled(p.PermString()) { + if member.Permission.Disabled(p.PermString()) { return false } else { r = true } } else { - if m.Permission.Enabled(p.PermString()) { + if member.Permission.Enabled(p.PermString()) { r = true - } else if m.Permission.Disabled(p.PermString()) { + } else if member.Permission.Disabled(p.PermString()) { return false } } @@ -100,7 +93,7 @@ func PermissionCheck(ctx context.Context, c *components.Components, g *ent.Guild return RolePermissionCheck(g, guildID, client, m.RoleIDs, perms) } -func RolePermissionCheck(g *ent.Guild, guildID snowflake.ID, client *bot.Client, roleIds []snowflake.ID, perms []Permission) bool { +func RolePermissionCheck(g *models.Guild, guildID snowflake.ID, client *bot.Client, roleIds []snowflake.ID, perms []Permission) bool { if len(perms) == 0 { return true } @@ -152,14 +145,10 @@ func permissionCheck(event interface { } if dPerm != 0 && event.Member().Permissions.Has(dPerm) { - if m := c.DB().Guild.Query(). - Where(guild.ID(*event.GuildID())). - FirstX(event). - QueryMembers(). - Where(member.HasUserWith(user.ID(event.User().ID))). - FirstX(event); m != nil { + var member models.Member + if err := c.GormDB().Where("guild_id = ? AND user_id = ?", *event.GuildID(), event.User().ID).First(&member).Error; err == nil { for _, p := range perms { - if m.Permission.Disabled(p.PermString()) { + if member.Permission.Disabled(p.PermString()) { return false } } diff --git a/bot/components/guild.go b/bot/components/guild.go index b804d42a..5750da31 100644 --- a/bot/components/guild.go +++ b/bot/components/guild.go @@ -25,21 +25,12 @@ import ( "log/slog" "github.com/sabafly/gobot/database/models" - "github.com/sabafly/gobot/ent/member" - "github.com/sabafly/gobot/ent/messagepin" - "github.com/sabafly/gobot/ent/messageremind" - "github.com/sabafly/gobot/ent/rolepanel" - "github.com/sabafly/gobot/ent/rolepaneledit" - "github.com/sabafly/gobot/ent/rolepanelplaced" - "github.com/sabafly/gobot/ent/wordsuffix" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" "github.com/disgoorg/snowflake/v2" - "github.com/sabafly/gobot/ent" - "github.com/sabafly/gobot/ent/guild" - "github.com/sabafly/gobot/ent/user" + "gorm.io/gorm" ) func (c *Components) OnGuildReady() func(event *events.GuildReady) { @@ -66,8 +57,18 @@ func (c *Components) OnGuildReady() func(event *events.GuildReady) { return } - u = c.db.User.Query().Where(user.ID(u.ID)).OnlyX(event) - slog.Debug("ギルドオーナー情報", "id", u.ID, "name", u.Name, "own_guilds", u.QueryOwnGuilds().AllX(event), "guilds", u.QueryGuilds().AllX(event)) + var ownedGuilds []models.Guild + c.GormDB().Where("owner_id = ?", u.ID).Find(&ownedGuilds) + + // For joined guilds, need a join query via Members + var joinedMembers []models.Member + c.GormDB().Preload("Guild").Where("user_id = ?", u.ID).Find(&joinedMembers) + var joinedGuilds []models.Guild + for _, m := range joinedMembers { + joinedGuilds = append(joinedGuilds, m.Guild) + } + + slog.Debug("ギルドオーナー情報", "id", u.ID, "name", u.Name, "own_guilds", ownedGuilds, "guilds", joinedGuilds) } } @@ -95,48 +96,94 @@ func (c *Components) OnGuildJoin() func(event *events.GuildJoin) { return } - u = c.db.User.Query().Where(user.ID(u.ID)).OnlyX(event) - slog.Debug("ギルドオーナー情報", "id", u.ID, "name", u.Name, "own_guilds", u.QueryOwnGuilds().AllX(event), "guilds", u.QueryGuilds().AllX(event)) + var ownedGuilds []models.Guild + c.GormDB().Where("owner_id = ?", u.ID).Find(&ownedGuilds) + + var joinedMembers []models.Member + c.GormDB().Preload("Guild").Where("user_id = ?", u.ID).Find(&joinedMembers) + var joinedGuilds []models.Guild + for _, m := range joinedMembers { + joinedGuilds = append(joinedGuilds, m.Guild) + } + + slog.Debug("ギルドオーナー情報", "id", u.ID, "name", u.Name, "own_guilds", ownedGuilds, "guilds", joinedGuilds) } } func (c *Components) OnGuildLeave() func(event *events.GuildLeave) { return func(event *events.GuildLeave) { slog.Info("ギルド脱退", "id", event.Guild.ID, "name", event.Guild.Name) - c.db.Member.Delete().Where(member.HasGuildWith(guild.ID(event.Guild.ID))).ExecX(event) - c.db.MessagePin.Delete().Where(messagepin.HasGuildWith(guild.ID(event.Guild.ID))).ExecX(event) - c.db.MessageRemind.Delete().Where(messageremind.HasGuildWith(guild.ID(event.Guild.ID))).ExecX(event) - c.db.RolePanelPlaced.Delete().Where(rolepanelplaced.HasGuildWith(guild.ID(event.Guild.ID))).ExecX(event) - c.db.RolePanelEdit.Delete().Where(rolepaneledit.HasGuildWith(guild.ID(event.Guild.ID))).ExecX(event) - c.db.RolePanel.Delete().Where(rolepanel.HasGuildWith(guild.ID(event.Guild.ID))).ExecX(event) - c.db.WordSuffix.Delete().Where(wordsuffix.HasGuildWith(guild.ID(event.Guild.ID))).ExecX(event) - c.db.Guild.DeleteOneID(event.Guild.ID).ExecX(event) + + // Use transaction for deletion + if err := c.GormDB().Transaction(func(tx *gorm.DB) error { + if err := tx.Where("guild_id = ?", event.Guild.ID).Delete(&models.Member{}).Error; err != nil { + slog.Error("ギルド脱退 メンバー削除に失敗", "err", err) + return err + } + if err := tx.Where("guild_id = ?", event.Guild.ID).Delete(&models.MessagePin{}).Error; err != nil { + slog.Error("ギルド脱退 メッセージピン削除に失敗", "err", err) + return err + } + if err := tx.Where("guild_id = ?", event.Guild.ID).Delete(&models.MessageRemind{}).Error; err != nil { + slog.Error("ギルド脱退 メッセージリマインド削除に失敗", "err", err) + return err + } + if err := tx.Where("guild_id = ?", event.Guild.ID).Delete(&models.RolePanelPlaced{}).Error; err != nil { + slog.Error("ギルド脱退 ロールパネル配置削除に失敗", "err", err) + return err + } + if err := tx.Where("guild_id = ?", event.Guild.ID).Delete(&models.RolePanelEdit{}).Error; err != nil { + slog.Error("ギルド脱退 ロールパネル編集削除に失敗", "err", err) + return err + } + if err := tx.Where("guild_id = ?", event.Guild.ID).Delete(&models.RolePanel{}).Error; err != nil { + slog.Error("ギルド脱退 ロールパネル削除に失敗", "err", err) + return err + } + if err := tx.Where("guild_id = ?", event.Guild.ID).Delete(&models.WordSuffix{}).Error; err != nil { + slog.Error("ギルド脱退 ワードサフィックス削除に失敗", "err", err) + return err + } + if err := tx.Delete(&models.Guild{ID: event.Guild.ID}).Error; err != nil { + slog.Error("ギルド脱退 ギルド削除に失敗", "err", err) + return err + } + return nil + }); err != nil { + slog.Error("ギルド脱退 データベースからの削除に失敗", "err", err) + return + } } } -func (c *Components) GuildCreate(ctx context.Context, ownerID snowflake.ID, g *discord.Guild) (*ent.Guild, error) { - ok := c.db.Guild. - Query(). - Where(guild.ID(g.ID)).ExistX(ctx) - if ok { - return c.db.Guild. - Query(). - Where(guild.ID(g.ID)). - Only(ctx) +func (c *Components) GuildCreate(ctx context.Context, ownerID snowflake.ID, g *discord.Guild) (*models.Guild, error) { + var guild models.Guild + err := c.GormDB().Where("id = ?", g.ID).First(&guild).Error + if err == nil { + return &guild, nil } + if err != gorm.ErrRecordNotFound { + return nil, err + } + slog.Debug("新規ギルド作成", "gid", g.ID) - return c.db.Guild.Create(). - SetID(g.ID). - SetName(g.Name). - SetOwnerID(ownerID). - Save(ctx) + guild = models.Guild{ + ID: g.ID, + Name: g.Name, + OwnerID: &ownerID, + } + if err := c.GormDB().Create(&guild).Error; err != nil { + return nil, err + } + return &guild, nil } -func (c *Components) GuildCreateID(ctx context.Context, gid snowflake.ID) (*ent.Guild, error) { - return c.db.Guild. - Query(). - Where(guild.ID(gid)). - Only(ctx) +func (c *Components) GuildCreateID(ctx context.Context, gid snowflake.ID) (*models.Guild, error) { + var guild models.Guild + if err := c.GormDB().Where("id = ?", gid).First(&guild).Error; err != nil { + return nil, err + } + return &guild, nil } func (c *Components) GuildRequest(client *bot.Client, gid snowflake.ID) (*discord.Guild, error) { @@ -152,7 +199,8 @@ func (c *Components) GuildRequest(client *bot.Client, gid snowflake.ID) (*discor func (c *Components) InitializeGuild(ctx context.Context, guild discord.Guild) error { g := models.Guild{ - ID: guild.ID, + ID: guild.ID, + OwnerID: &guild.OwnerID, } if err := c.GormDB().Where(g).FirstOrCreate(&g).Error; err != nil { return err diff --git a/bot/components/member.go b/bot/components/member.go index 1a45d0ca..d61486c3 100644 --- a/bot/components/member.go +++ b/bot/components/member.go @@ -25,27 +25,32 @@ import ( "github.com/disgoorg/disgo/discord" "github.com/disgoorg/snowflake/v2" - "github.com/sabafly/gobot/ent" - "github.com/sabafly/gobot/ent/guild" - "github.com/sabafly/gobot/ent/member" - "github.com/sabafly/gobot/ent/user" + "github.com/sabafly/gobot/database/models" + "gorm.io/gorm" ) -func (c *Components) MemberCreate(ctx context.Context, u discord.User, gid snowflake.ID) (*ent.Member, error) { - eu, err := c.UserCreate(ctx, u) +func (c *Components) MemberCreate(ctx context.Context, u discord.User, gid snowflake.ID) (*models.Member, error) { + _, err := c.UserCreate(ctx, u) if err != nil { return nil, err } - ok := c.db.Member. - Query(). - Where(member.HasUserWith(user.ID(u.ID)), member.HasGuildWith(guild.ID(gid))).ExistX(ctx) - if ok { - return c.db.Member. - Query(). - Where(member.HasUserWith(user.ID(u.ID)), member.HasGuildWith(guild.ID(gid))).Only(ctx) + + var member models.Member + err = c.GormDB().Where("guild_id = ? AND user_id = ?", gid, u.ID).First(&member).Error + if err == nil { + return &member, nil + } + if err != gorm.ErrRecordNotFound { + return nil, err + } + + member = models.Member{ + GuildID: gid, + UserID: u.ID, + } + if err := c.GormDB().Create(&member).Error; err != nil { + return nil, err } - return c.db.Member.Create(). - SetUser(eu). - SetGuildID(gid). - Save(ctx) + + return &member, nil } diff --git a/bot/components/user.go b/bot/components/user.go index 67dbc980..9f6adfc5 100644 --- a/bot/components/user.go +++ b/bot/components/user.go @@ -25,25 +25,28 @@ import ( "log/slog" "github.com/disgoorg/disgo/discord" - "github.com/sabafly/gobot/ent" - "github.com/sabafly/gobot/ent/user" + "github.com/sabafly/gobot/database/models" "github.com/sabafly/gobot/internal/errors" ) -func (c *Components) UserCreate(ctx context.Context, u discord.User) (*ent.User, error) { +func (c *Components) UserCreate(ctx context.Context, u discord.User) (*models.User, error) { if u.Bot || u.System { return nil, errors.New("bot cannot use to create user") } - if ok := c.db.User. - Query(). - Where(user.ID(u.ID)).ExistX(ctx); ok { - return c.db.User. - Query(). - Where(user.ID(u.ID)).Only(ctx) + + user := models.User{ + ID: u.ID, + Name: u.EffectiveName(), + } + + result := c.GormDB().FirstOrCreate(&user, models.User{ID: u.ID}) + if result.Error != nil { + return nil, result.Error } - slog.Debug("新規ユーザー作成", "uid", u.ID, "uname", u.Username) - return c.db.User.Create(). - SetID(u.ID). - SetName(u.EffectiveName()). - Save(ctx) + + if result.RowsAffected > 0 { + slog.Debug("新規ユーザー作成", "uid", u.ID, "uname", u.Username) + } + + return &user, nil } diff --git a/database/database.go b/database/database.go index cf0d8281..e4011582 100644 --- a/database/database.go +++ b/database/database.go @@ -32,6 +32,13 @@ func NewDB(dsn string) (*DB, error) { &models.BetOption{}, &models.Bet{}, &models.BetEntrant{}, + &models.Member{}, + &models.RolePanel{}, + &models.RolePanelEdit{}, + &models.RolePanelPlaced{}, + &models.MessagePin{}, + &models.MessageRemind{}, + &models.WordSuffix{}, ); err != nil { return nil, err } diff --git a/database/models/bet.go b/database/models/bet.go index c126a5e8..d7aa8d26 100644 --- a/database/models/bet.go +++ b/database/models/bet.go @@ -6,6 +6,7 @@ import ( "github.com/disgoorg/snowflake/v2" "github.com/google/uuid" + "gorm.io/gorm" ) type BetHost struct { @@ -28,6 +29,13 @@ type BetHost struct { Locale string `gorm:"type:varchar(10);not null;default:'en';"` } +func (b *BetHost) BeforeCreate(tx *gorm.DB) error { + if b.ID == uuid.Nil { + b.ID = uuid.New() + } + return nil +} + type BetVoteType string const ( diff --git a/database/models/guild.go b/database/models/guild.go index 27b03e75..7df0f12f 100644 --- a/database/models/guild.go +++ b/database/models/guild.go @@ -1,7 +1,39 @@ package models -import "github.com/disgoorg/snowflake/v2" +import ( + "time" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/snowflake/v2" + "github.com/sabafly/gobot/internal/permissions" +) type Guild struct { - ID snowflake.ID `gorm:"primary_key;column:id;type:bigint(20) unsigned;not null"` + ID snowflake.ID `gorm:"primary_key;column:id;type:bigint(20) unsigned;not null"` + Name string `gorm:"not null"` + Locale discord.Locale `gorm:"default:'ja'"` + LevelUpMessage string `gorm:"default:'{user}がレベルアップしたよ!🥳\n**{before_level} レベル → {after_level} レベル**'"` + LevelUpChannel *snowflake.ID `gorm:"type:bigint(20)"` + LevelUpExcludeChannel []snowflake.ID `gorm:"serializer:json"` + LevelMee6Imported bool `gorm:"default:false"` + LevelRole map[int]snowflake.ID `gorm:"serializer:json"` + Permissions map[snowflake.ID]permissions.Permission `gorm:"serializer:json"` + RemindCount int `gorm:"default:0"` + RolePanelEditTimes []time.Time `gorm:"serializer:json"` + BumpEnabled bool `gorm:"default:true"` + BumpMessageTitle string `gorm:"default:'Bumpを検知しました'"` + BumpMessage string `gorm:"default:'2時間後に通知します'"` + BumpRemindMessageTitle string `gorm:"default:'Bumpの時間です'"` + BumpRemindMessage string `gorm:"default:'でBumpしましょう'"` + UpEnabled bool `gorm:"default:true"` + UpMessageTitle string `gorm:"default:'UPを検知しました'"` + UpMessage string `gorm:"default:'1時間後に通知します'"` + UpRemindMessageTitle string `gorm:"default:'UPの時間です'"` + UpRemindMessage string `gorm:"default:'でUPしましょう'"` + BumpMention *snowflake.ID `gorm:"type:bigint(20)"` + UpMention *snowflake.ID `gorm:"type:bigint(20)"` + LevelingDisabled bool `gorm:"default:false"` + + OwnerID *snowflake.ID `gorm:"type:bigint(20) unsigned;column:owner_id;index:idx_guild_owner"` + Owner *User `gorm:"foreignKey:OwnerID;constraint:OnDelete:SET NULL;"` } diff --git a/database/models/member.go b/database/models/member.go new file mode 100644 index 00000000..9b153c24 --- /dev/null +++ b/database/models/member.go @@ -0,0 +1,23 @@ +package models + +import ( + "time" + + "github.com/disgoorg/snowflake/v2" + "github.com/sabafly/gobot/internal/permissions" + "github.com/sabafly/gobot/internal/xppoint" +) + +type Member struct { + ID int `gorm:"primary_key;auto_increment"` + GuildID snowflake.ID `gorm:"type:bigint(20);not null;index:idx_member_guild_user,unique"` + Guild Guild `gorm:"foreignKey:GuildID;constraint:OnDelete:CASCADE;"` + UserID snowflake.ID `gorm:"type:bigint(20);not null;index:idx_member_guild_user,unique"` + User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE;"` + Permission permissions.Permission `gorm:"serializer:json"` + XP xppoint.XP `gorm:"default:0"` + LastXP time.Time + MessageCount uint64 `gorm:"default:0"` + LastNotifiedLevel *uint64 + LastMessageHashes []string `gorm:"serializer:json"` +} diff --git a/database/models/messagepin.go b/database/models/messagepin.go new file mode 100644 index 00000000..60c254a1 --- /dev/null +++ b/database/models/messagepin.go @@ -0,0 +1,50 @@ +package models + +import ( + "encoding/json" + "time" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/snowflake/v2" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type RateLimit struct { + Limit []time.Time +} + +func (r RateLimit) MarshalJSON() ([]byte, error) { + return json.Marshal(r.Limit) +} + +func (r *RateLimit) UnmarshalJSON(b []byte) error { + return json.Unmarshal(b, &r.Limit) +} + +func (r *RateLimit) CheckLimit() bool { + if (len(r.Limit) >= 3 && time.Since(r.Limit[2]) < time.Second*5) || (len(r.Limit) >= 10 && time.Since(r.Limit[9]) < time.Second*30) { + return false + } + r.Limit = append([]time.Time{time.Now()}, r.Limit[0:min(10, len(r.Limit))]...) + ok := (len(r.Limit) < 3 || time.Since(r.Limit[2]) >= time.Second*5) && (len(r.Limit) < 10 || time.Since(r.Limit[9]) >= time.Second*30) + return ok +} + +type MessagePin struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;"` + GuildID snowflake.ID `gorm:"type:bigint(20);not null;index"` + Guild Guild `gorm:"foreignKey:GuildID;constraint:OnDelete:CASCADE;"` + ChannelID snowflake.ID `gorm:"type:bigint(20);uniqueIndex"` + Content string `gorm:"type:text"` + Embeds []discord.Embed `gorm:"serializer:json"` + BeforeID *snowflake.ID `gorm:"type:bigint(20)"` + RateLimit RateLimit `gorm:"serializer:json"` +} + +func (m *MessagePin) BeforeCreate(tx *gorm.DB) error { + if m.ID == uuid.Nil { + m.ID = uuid.New() + } + return nil +} diff --git a/database/models/messageremind.go b/database/models/messageremind.go new file mode 100644 index 00000000..829ac755 --- /dev/null +++ b/database/models/messageremind.go @@ -0,0 +1,27 @@ +package models + +import ( + "time" + + "github.com/disgoorg/snowflake/v2" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type MessageRemind struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;"` + GuildID snowflake.ID `gorm:"type:bigint(20);not null;index"` + Guild Guild `gorm:"foreignKey:GuildID;constraint:OnDelete:CASCADE;"` + ChannelID snowflake.ID `gorm:"type:bigint(20)"` + AuthorID snowflake.ID `gorm:"type:bigint(20)"` + Time time.Time + Content string `gorm:"type:text;not null"` + Name string `gorm:"not null"` +} + +func (m *MessageRemind) BeforeCreate(tx *gorm.DB) error { + if m.ID == uuid.Nil { + m.ID = uuid.New() + } + return nil +} diff --git a/database/models/rolepanel.go b/database/models/rolepanel.go new file mode 100644 index 00000000..40eca399 --- /dev/null +++ b/database/models/rolepanel.go @@ -0,0 +1,94 @@ +package models + +import ( + "time" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/snowflake/v2" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Role struct { + ID snowflake.ID `json:"id"` + Name string `json:"name"` + Emoji *discord.ComponentEmoji `json:"emoji"` +} + +type RolePanel struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;"` + Name string `gorm:"not null"` + Description string `gorm:"type:text"` + Roles []Role `gorm:"serializer:json"` + UpdatedAt time.Time + AppliedAt time.Time + + GuildID snowflake.ID `gorm:"type:bigint(20);not null;index"` + Guild Guild `gorm:"foreignKey:GuildID;constraint:OnDelete:CASCADE;"` + + Placements []RolePanelPlaced `gorm:"foreignKey:RolePanelID"` + Edit *RolePanelEdit `gorm:"foreignKey:ParentID"` +} + +func (r *RolePanel) BeforeCreate(tx *gorm.DB) error { + if r.ID == uuid.Nil { + r.ID = uuid.New() + } + return nil +} + +type RolePanelEdit struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;"` + ChannelID snowflake.ID `gorm:"type:bigint(20)"` + EmojiAuthor *snowflake.ID `gorm:"type:bigint(20)"` + Token *string + SelectedRole *snowflake.ID `gorm:"type:bigint(20)"` + Modified bool `gorm:"default:false"` + Name *string + Description *string + Roles []Role `gorm:"serializer:json"` + + GuildID snowflake.ID `gorm:"type:bigint(20);not null;index"` + Guild Guild `gorm:"foreignKey:GuildID"` + + ParentID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex"` + Parent RolePanel `gorm:"foreignKey:ParentID"` +} + +func (r *RolePanelEdit) BeforeCreate(tx *gorm.DB) error { + if r.ID == uuid.Nil { + r.ID = uuid.New() + } + return nil +} + +type RolePanelPlaced struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;"` + MessageID *snowflake.ID `gorm:"type:bigint(20)"` + ChannelID snowflake.ID `gorm:"type:bigint(20)"` + Type string `gorm:"type:varchar(20)"` // button, reaction, select_menu + ButtonType discord.ButtonStyle `gorm:"default:1"` + ShowName bool `gorm:"default:false"` + FoldingSelectMenu bool `gorm:"default:true"` + HideNotice bool `gorm:"default:false"` + UseDisplayName bool `gorm:"default:false"` + CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP"` + Uses int `gorm:"default:0"` + Name string `gorm:"not null"` + Description string `gorm:"type:text"` + Roles []Role `gorm:"serializer:json"` + UpdatedAt time.Time + + GuildID snowflake.ID `gorm:"type:bigint(20);not null;index"` + Guild Guild `gorm:"foreignKey:GuildID;constraint:OnDelete:CASCADE;"` + + RolePanelID uuid.UUID `gorm:"type:uuid;not null;index"` + RolePanel RolePanel `gorm:"foreignKey:RolePanelID;constraint:OnDelete:CASCADE;"` +} + +func (r *RolePanelPlaced) BeforeCreate(tx *gorm.DB) error { + if r.ID == uuid.Nil { + r.ID = uuid.New() + } + return nil +} diff --git a/database/models/user.go b/database/models/user.go index b85e444e..f4baa07c 100644 --- a/database/models/user.go +++ b/database/models/user.go @@ -1,7 +1,17 @@ package models -import "github.com/disgoorg/snowflake/v2" +import ( + "time" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/snowflake/v2" + "github.com/sabafly/gobot/internal/xppoint" +) type User struct { - ID snowflake.ID `gorm:"primary_key;column:id;type:bigint(20) unsigned;not null"` + ID snowflake.ID `gorm:"primary_key;column:id;type:bigint(20) unsigned;not null"` + Name string `gorm:"not null"` + CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP"` + Locale discord.Locale `gorm:"default:'ja'"` + XP xppoint.XP `gorm:"default:0"` } diff --git a/database/models/wordsuffix.go b/database/models/wordsuffix.go new file mode 100644 index 00000000..ae3495d1 --- /dev/null +++ b/database/models/wordsuffix.go @@ -0,0 +1,31 @@ +package models + +import ( + "time" + + "github.com/disgoorg/snowflake/v2" + "github.com/google/uuid" + "github.com/sabafly/gobot/internal/uuidv7" + "gorm.io/gorm" +) + +type WordSuffix struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;"` + Suffix string `gorm:"not null"` + Expired *time.Time + + GuildID *snowflake.ID `gorm:"type:bigint(20);index"` + Guild *Guild `gorm:"foreignKey:GuildID"` + + OwnerID snowflake.ID `gorm:"type:bigint(20);not null;index"` + Owner User `gorm:"foreignKey:OwnerID"` + + Rule string `gorm:"default:'webhook'"` // webhook, warn, delete +} + +func (w *WordSuffix) BeforeCreate(tx *gorm.DB) error { + if w.ID == uuid.Nil { + w.ID = uuidv7.New() + } + return nil +} diff --git a/docker-compose.yml b/docker-compose.yml index d6106717..e6781e47 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,7 @@ services: build: context: . dockerfile: Dockerfile + # image: ghcr.io/sabafly/gobot:latest tty: true env_file: - ./mysql/.env