diff --git a/lib/discordgo/discord.go b/lib/discordgo/discord.go index 7352512464..78b3b910e6 100644 --- a/lib/discordgo/discord.go +++ b/lib/discordgo/discord.go @@ -34,15 +34,16 @@ var ErrMFA = errors.New("account has 2FA enabled") // tasks if given enough information to do so. Currently you can pass zero // arguments and it will return an empty Discord session. // There are 3 ways to call New: -// With a single auth token - All requests will use the token blindly, -// no verification of the token will be done and requests may fail. -// IF THE TOKEN IS FOR A BOT, IT MUST BE PREFIXED WITH `BOT ` -// eg: `"Bot "` -// With an email and password - Discord will sign in with the provided -// credentials. -// With an email, password and auth token - Discord will verify the auth -// token, if it is invalid it will sign in with the provided -// credentials. This is the Discord recommended way to sign in. +// +// With a single auth token - All requests will use the token blindly, +// no verification of the token will be done and requests may fail. +// IF THE TOKEN IS FOR A BOT, IT MUST BE PREFIXED WITH `BOT ` +// eg: `"Bot "` +// With an email and password - Discord will sign in with the provided +// credentials. +// With an email, password and auth token - Discord will verify the auth +// token, if it is invalid it will sign in with the provided +// credentials. This is the Discord recommended way to sign in. // // NOTE: While email/pass authentication is supported by DiscordGo it is // HIGHLY DISCOURAGED by Discord. Please only use email/pass to obtain a token @@ -153,3 +154,7 @@ func CheckRetry(_ context.Context, resp *http.Response, err error) (bool, error) func StrID(id int64) string { return strconv.FormatInt(id, 10) } + +func ParseID(s string) (int64, error) { + return strconv.ParseInt(s, 10, 64) +} diff --git a/reminders/models/boil_queries.go b/reminders/models/boil_queries.go new file mode 100644 index 0000000000..20c2563fdb --- /dev/null +++ b/reminders/models/boil_queries.go @@ -0,0 +1,38 @@ +// Code generated by SQLBoiler 4.16.2 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package models + +import ( + "regexp" + + "github.com/volatiletech/sqlboiler/v4/drivers" + "github.com/volatiletech/sqlboiler/v4/queries" + "github.com/volatiletech/sqlboiler/v4/queries/qm" +) + +var dialect = drivers.Dialect{ + LQ: 0x22, + RQ: 0x22, + + UseIndexPlaceholders: true, + UseLastInsertID: false, + UseSchema: false, + UseDefaultKeyword: true, + UseAutoColumns: false, + UseTopClause: false, + UseOutputClause: false, + UseCaseWhenExistsClause: false, +} + +// This is a dummy variable to prevent unused regexp import error +var _ = ®exp.Regexp{} + +// NewQuery initializes a new Query using the passed in QueryMods +func NewQuery(mods ...qm.QueryMod) *queries.Query { + q := &queries.Query{} + queries.SetDialect(q, &dialect) + qm.Apply(q, mods...) + + return q +} diff --git a/reminders/models/boil_table_names.go b/reminders/models/boil_table_names.go new file mode 100644 index 0000000000..879f7cf0bd --- /dev/null +++ b/reminders/models/boil_table_names.go @@ -0,0 +1,10 @@ +// Code generated by SQLBoiler 4.16.2 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package models + +var TableNames = struct { + Reminders string +}{ + Reminders: "reminders", +} diff --git a/reminders/models/boil_types.go b/reminders/models/boil_types.go new file mode 100644 index 0000000000..02a6fdfdc5 --- /dev/null +++ b/reminders/models/boil_types.go @@ -0,0 +1,52 @@ +// Code generated by SQLBoiler 4.16.2 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package models + +import ( + "strconv" + + "github.com/friendsofgo/errors" + "github.com/volatiletech/sqlboiler/v4/boil" + "github.com/volatiletech/strmangle" +) + +// M type is for providing columns and column values to UpdateAll. +type M map[string]interface{} + +// ErrSyncFail occurs during insert when the record could not be retrieved in +// order to populate default value information. This usually happens when LastInsertId +// fails or there was a primary key configuration that was not resolvable. +var ErrSyncFail = errors.New("models: failed to synchronize data after insert") + +type insertCache struct { + query string + retQuery string + valueMapping []uint64 + retMapping []uint64 +} + +type updateCache struct { + query string + valueMapping []uint64 +} + +func makeCacheKey(cols boil.Columns, nzDefaults []string) string { + buf := strmangle.GetBuffer() + + buf.WriteString(strconv.Itoa(cols.Kind)) + for _, w := range cols.Cols { + buf.WriteString(w) + } + + if len(nzDefaults) != 0 { + buf.WriteByte('.') + } + for _, nz := range nzDefaults { + buf.WriteString(nz) + } + + str := buf.String() + strmangle.PutBuffer(buf) + return str +} diff --git a/reminders/models/boil_view_names.go b/reminders/models/boil_view_names.go new file mode 100644 index 0000000000..01504d82bf --- /dev/null +++ b/reminders/models/boil_view_names.go @@ -0,0 +1,7 @@ +// Code generated by SQLBoiler 4.16.2 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package models + +var ViewNames = struct { +}{} diff --git a/reminders/models/psql_upsert.go b/reminders/models/psql_upsert.go new file mode 100644 index 0000000000..07602da9c5 --- /dev/null +++ b/reminders/models/psql_upsert.go @@ -0,0 +1,99 @@ +// Code generated by SQLBoiler 4.16.2 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package models + +import ( + "fmt" + "strings" + + "github.com/volatiletech/sqlboiler/v4/drivers" + "github.com/volatiletech/strmangle" +) + +type UpsertOptions struct { + conflictTarget string + updateSet string +} + +type UpsertOptionFunc func(o *UpsertOptions) + +func UpsertConflictTarget(conflictTarget string) UpsertOptionFunc { + return func(o *UpsertOptions) { + o.conflictTarget = conflictTarget + } +} + +func UpsertUpdateSet(updateSet string) UpsertOptionFunc { + return func(o *UpsertOptions) { + o.updateSet = updateSet + } +} + +// buildUpsertQueryPostgres builds a SQL statement string using the upsertData provided. +func buildUpsertQueryPostgres(dia drivers.Dialect, tableName string, updateOnConflict bool, ret, update, conflict, whitelist []string, opts ...UpsertOptionFunc) string { + conflict = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, conflict) + whitelist = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, whitelist) + ret = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, ret) + + upsertOpts := &UpsertOptions{} + for _, o := range opts { + o(upsertOpts) + } + + buf := strmangle.GetBuffer() + defer strmangle.PutBuffer(buf) + + columns := "DEFAULT VALUES" + if len(whitelist) != 0 { + columns = fmt.Sprintf("(%s) VALUES (%s)", + strings.Join(whitelist, ", "), + strmangle.Placeholders(dia.UseIndexPlaceholders, len(whitelist), 1, 1)) + } + + fmt.Fprintf( + buf, + "INSERT INTO %s %s ON CONFLICT ", + tableName, + columns, + ) + + if upsertOpts.conflictTarget != "" { + buf.WriteString(upsertOpts.conflictTarget) + } else if len(conflict) != 0 { + buf.WriteByte('(') + buf.WriteString(strings.Join(conflict, ", ")) + buf.WriteByte(')') + } + buf.WriteByte(' ') + + if !updateOnConflict || len(update) == 0 { + buf.WriteString("DO NOTHING") + } else { + buf.WriteString("DO UPDATE SET ") + + if upsertOpts.updateSet != "" { + buf.WriteString(upsertOpts.updateSet) + } else { + for i, v := range update { + if len(v) == 0 { + continue + } + if i != 0 { + buf.WriteByte(',') + } + quoted := strmangle.IdentQuote(dia.LQ, dia.RQ, v) + buf.WriteString(quoted) + buf.WriteString(" = EXCLUDED.") + buf.WriteString(quoted) + } + } + } + + if len(ret) != 0 { + buf.WriteString(" RETURNING ") + buf.WriteString(strings.Join(ret, ", ")) + } + + return buf.String() +} diff --git a/reminders/models/reminders.go b/reminders/models/reminders.go new file mode 100644 index 0000000000..dd68b9f533 --- /dev/null +++ b/reminders/models/reminders.go @@ -0,0 +1,998 @@ +// Code generated by SQLBoiler 4.16.2 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package models + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "strconv" + "strings" + "sync" + "time" + + "github.com/friendsofgo/errors" + "github.com/volatiletech/null/v8" + "github.com/volatiletech/sqlboiler/v4/boil" + "github.com/volatiletech/sqlboiler/v4/queries" + "github.com/volatiletech/sqlboiler/v4/queries/qm" + "github.com/volatiletech/sqlboiler/v4/queries/qmhelper" + "github.com/volatiletech/strmangle" +) + +// Reminder is an object representing the database table. +type Reminder struct { + ID int `boil:"id" json:"id" toml:"id" yaml:"id"` + CreatedAt time.Time `boil:"created_at" json:"created_at" toml:"created_at" yaml:"created_at"` + UpdatedAt time.Time `boil:"updated_at" json:"updated_at" toml:"updated_at" yaml:"updated_at"` + DeletedAt null.Time `boil:"deleted_at" json:"deleted_at,omitempty" toml:"deleted_at" yaml:"deleted_at,omitempty"` + UserID string `boil:"user_id" json:"user_id" toml:"user_id" yaml:"user_id"` + ChannelID string `boil:"channel_id" json:"channel_id" toml:"channel_id" yaml:"channel_id"` + GuildID int64 `boil:"guild_id" json:"guild_id" toml:"guild_id" yaml:"guild_id"` + Message string `boil:"message" json:"message" toml:"message" yaml:"message"` + When int64 `boil:"when" json:"when" toml:"when" yaml:"when"` + + R *reminderR `boil:"-" json:"-" toml:"-" yaml:"-"` + L reminderL `boil:"-" json:"-" toml:"-" yaml:"-"` +} + +var ReminderColumns = struct { + ID string + CreatedAt string + UpdatedAt string + DeletedAt string + UserID string + ChannelID string + GuildID string + Message string + When string +}{ + ID: "id", + CreatedAt: "created_at", + UpdatedAt: "updated_at", + DeletedAt: "deleted_at", + UserID: "user_id", + ChannelID: "channel_id", + GuildID: "guild_id", + Message: "message", + When: "when", +} + +var ReminderTableColumns = struct { + ID string + CreatedAt string + UpdatedAt string + DeletedAt string + UserID string + ChannelID string + GuildID string + Message string + When string +}{ + ID: "reminders.id", + CreatedAt: "reminders.created_at", + UpdatedAt: "reminders.updated_at", + DeletedAt: "reminders.deleted_at", + UserID: "reminders.user_id", + ChannelID: "reminders.channel_id", + GuildID: "reminders.guild_id", + Message: "reminders.message", + When: "reminders.when", +} + +// Generated where + +type whereHelperint struct{ field string } + +func (w whereHelperint) EQ(x int) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) } +func (w whereHelperint) NEQ(x int) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) } +func (w whereHelperint) LT(x int) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) } +func (w whereHelperint) LTE(x int) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) } +func (w whereHelperint) GT(x int) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) } +func (w whereHelperint) GTE(x int) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) } +func (w whereHelperint) IN(slice []int) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...) +} +func (w whereHelperint) NIN(slice []int) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereNotIn(fmt.Sprintf("%s NOT IN ?", w.field), values...) +} + +type whereHelpertime_Time struct{ field string } + +func (w whereHelpertime_Time) EQ(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.EQ, x) +} +func (w whereHelpertime_Time) NEQ(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.NEQ, x) +} +func (w whereHelpertime_Time) LT(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LT, x) +} +func (w whereHelpertime_Time) LTE(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LTE, x) +} +func (w whereHelpertime_Time) GT(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GT, x) +} +func (w whereHelpertime_Time) GTE(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GTE, x) +} + +type whereHelpernull_Time struct{ field string } + +func (w whereHelpernull_Time) EQ(x null.Time) qm.QueryMod { + return qmhelper.WhereNullEQ(w.field, false, x) +} +func (w whereHelpernull_Time) NEQ(x null.Time) qm.QueryMod { + return qmhelper.WhereNullEQ(w.field, true, x) +} +func (w whereHelpernull_Time) LT(x null.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LT, x) +} +func (w whereHelpernull_Time) LTE(x null.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LTE, x) +} +func (w whereHelpernull_Time) GT(x null.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GT, x) +} +func (w whereHelpernull_Time) GTE(x null.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GTE, x) +} + +func (w whereHelpernull_Time) IsNull() qm.QueryMod { return qmhelper.WhereIsNull(w.field) } +func (w whereHelpernull_Time) IsNotNull() qm.QueryMod { return qmhelper.WhereIsNotNull(w.field) } + +type whereHelperstring struct{ field string } + +func (w whereHelperstring) EQ(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) } +func (w whereHelperstring) NEQ(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) } +func (w whereHelperstring) LT(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) } +func (w whereHelperstring) LTE(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) } +func (w whereHelperstring) GT(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) } +func (w whereHelperstring) GTE(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) } +func (w whereHelperstring) LIKE(x string) qm.QueryMod { return qm.Where(w.field+" LIKE ?", x) } +func (w whereHelperstring) NLIKE(x string) qm.QueryMod { return qm.Where(w.field+" NOT LIKE ?", x) } +func (w whereHelperstring) ILIKE(x string) qm.QueryMod { return qm.Where(w.field+" ILIKE ?", x) } +func (w whereHelperstring) NILIKE(x string) qm.QueryMod { return qm.Where(w.field+" NOT ILIKE ?", x) } +func (w whereHelperstring) IN(slice []string) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...) +} +func (w whereHelperstring) NIN(slice []string) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereNotIn(fmt.Sprintf("%s NOT IN ?", w.field), values...) +} + +type whereHelperint64 struct{ field string } + +func (w whereHelperint64) EQ(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) } +func (w whereHelperint64) NEQ(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) } +func (w whereHelperint64) LT(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) } +func (w whereHelperint64) LTE(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) } +func (w whereHelperint64) GT(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) } +func (w whereHelperint64) GTE(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) } +func (w whereHelperint64) IN(slice []int64) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...) +} +func (w whereHelperint64) NIN(slice []int64) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereNotIn(fmt.Sprintf("%s NOT IN ?", w.field), values...) +} + +var ReminderWhere = struct { + ID whereHelperint + CreatedAt whereHelpertime_Time + UpdatedAt whereHelpertime_Time + DeletedAt whereHelpernull_Time + UserID whereHelperstring + ChannelID whereHelperstring + GuildID whereHelperint64 + Message whereHelperstring + When whereHelperint64 +}{ + ID: whereHelperint{field: "\"reminders\".\"id\""}, + CreatedAt: whereHelpertime_Time{field: "\"reminders\".\"created_at\""}, + UpdatedAt: whereHelpertime_Time{field: "\"reminders\".\"updated_at\""}, + DeletedAt: whereHelpernull_Time{field: "\"reminders\".\"deleted_at\""}, + UserID: whereHelperstring{field: "\"reminders\".\"user_id\""}, + ChannelID: whereHelperstring{field: "\"reminders\".\"channel_id\""}, + GuildID: whereHelperint64{field: "\"reminders\".\"guild_id\""}, + Message: whereHelperstring{field: "\"reminders\".\"message\""}, + When: whereHelperint64{field: "\"reminders\".\"when\""}, +} + +// ReminderRels is where relationship names are stored. +var ReminderRels = struct { +}{} + +// reminderR is where relationships are stored. +type reminderR struct { +} + +// NewStruct creates a new relationship struct +func (*reminderR) NewStruct() *reminderR { + return &reminderR{} +} + +// reminderL is where Load methods for each relationship are stored. +type reminderL struct{} + +var ( + reminderAllColumns = []string{"id", "created_at", "updated_at", "deleted_at", "user_id", "channel_id", "guild_id", "message", "when"} + reminderColumnsWithoutDefault = []string{"created_at", "updated_at", "user_id", "channel_id", "guild_id", "message", "when"} + reminderColumnsWithDefault = []string{"id", "deleted_at"} + reminderPrimaryKeyColumns = []string{"id"} + reminderGeneratedColumns = []string{} +) + +type ( + // ReminderSlice is an alias for a slice of pointers to Reminder. + // This should almost always be used instead of []Reminder. + ReminderSlice []*Reminder + + reminderQuery struct { + *queries.Query + } +) + +// Cache for insert, update and upsert +var ( + reminderType = reflect.TypeOf(&Reminder{}) + reminderMapping = queries.MakeStructMapping(reminderType) + reminderPrimaryKeyMapping, _ = queries.BindMapping(reminderType, reminderMapping, reminderPrimaryKeyColumns) + reminderInsertCacheMut sync.RWMutex + reminderInsertCache = make(map[string]insertCache) + reminderUpdateCacheMut sync.RWMutex + reminderUpdateCache = make(map[string]updateCache) + reminderUpsertCacheMut sync.RWMutex + reminderUpsertCache = make(map[string]insertCache) +) + +var ( + // Force time package dependency for automated UpdatedAt/CreatedAt. + _ = time.Second + // Force qmhelper dependency for where clause generation (which doesn't + // always happen) + _ = qmhelper.Where +) + +// OneG returns a single reminder record from the query using the global executor. +func (q reminderQuery) OneG(ctx context.Context) (*Reminder, error) { + return q.One(ctx, boil.GetContextDB()) +} + +// One returns a single reminder record from the query. +func (q reminderQuery) One(ctx context.Context, exec boil.ContextExecutor) (*Reminder, error) { + o := &Reminder{} + + queries.SetLimit(q.Query, 1) + + err := q.Bind(ctx, exec, o) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "models: failed to execute a one query for reminders") + } + + return o, nil +} + +// AllG returns all Reminder records from the query using the global executor. +func (q reminderQuery) AllG(ctx context.Context) (ReminderSlice, error) { + return q.All(ctx, boil.GetContextDB()) +} + +// All returns all Reminder records from the query. +func (q reminderQuery) All(ctx context.Context, exec boil.ContextExecutor) (ReminderSlice, error) { + var o []*Reminder + + err := q.Bind(ctx, exec, &o) + if err != nil { + return nil, errors.Wrap(err, "models: failed to assign all query results to Reminder slice") + } + + return o, nil +} + +// CountG returns the count of all Reminder records in the query using the global executor +func (q reminderQuery) CountG(ctx context.Context) (int64, error) { + return q.Count(ctx, boil.GetContextDB()) +} + +// Count returns the count of all Reminder records in the query. +func (q reminderQuery) Count(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + if err != nil { + return 0, errors.Wrap(err, "models: failed to count reminders rows") + } + + return count, nil +} + +// ExistsG checks if the row exists in the table using the global executor. +func (q reminderQuery) ExistsG(ctx context.Context) (bool, error) { + return q.Exists(ctx, boil.GetContextDB()) +} + +// Exists checks if the row exists in the table. +func (q reminderQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (bool, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + queries.SetLimit(q.Query, 1) + + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + if err != nil { + return false, errors.Wrap(err, "models: failed to check if reminders exists") + } + + return count > 0, nil +} + +// Reminders retrieves all the records using an executor. +func Reminders(mods ...qm.QueryMod) reminderQuery { + mods = append(mods, qm.From("\"reminders\""), qmhelper.WhereIsNull("\"reminders\".\"deleted_at\"")) + q := NewQuery(mods...) + if len(queries.GetSelect(q)) == 0 { + queries.SetSelect(q, []string{"\"reminders\".*"}) + } + + return reminderQuery{q} +} + +// FindReminderG retrieves a single record by ID. +func FindReminderG(ctx context.Context, iD int, selectCols ...string) (*Reminder, error) { + return FindReminder(ctx, boil.GetContextDB(), iD, selectCols...) +} + +// FindReminder retrieves a single record by ID with an executor. +// If selectCols is empty Find will return all columns. +func FindReminder(ctx context.Context, exec boil.ContextExecutor, iD int, selectCols ...string) (*Reminder, error) { + reminderObj := &Reminder{} + + sel := "*" + if len(selectCols) > 0 { + sel = strings.Join(strmangle.IdentQuoteSlice(dialect.LQ, dialect.RQ, selectCols), ",") + } + query := fmt.Sprintf( + "select %s from \"reminders\" where \"id\"=$1 and \"deleted_at\" is null", sel, + ) + + q := queries.Raw(query, iD) + + err := q.Bind(ctx, exec, reminderObj) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "models: unable to select from reminders") + } + + return reminderObj, nil +} + +// InsertG a single record. See Insert for whitelist behavior description. +func (o *Reminder) InsertG(ctx context.Context, columns boil.Columns) error { + return o.Insert(ctx, boil.GetContextDB(), columns) +} + +// Insert a single record using an executor. +// See boil.Columns.InsertColumnSet documentation to understand column list inference for inserts. +func (o *Reminder) Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error { + if o == nil { + return errors.New("models: no reminders provided for insertion") + } + + var err error + if !boil.TimestampsAreSkipped(ctx) { + currTime := time.Now().In(boil.GetLocation()) + + if o.CreatedAt.IsZero() { + o.CreatedAt = currTime + } + if o.UpdatedAt.IsZero() { + o.UpdatedAt = currTime + } + } + + nzDefaults := queries.NonZeroDefaultSet(reminderColumnsWithDefault, o) + + key := makeCacheKey(columns, nzDefaults) + reminderInsertCacheMut.RLock() + cache, cached := reminderInsertCache[key] + reminderInsertCacheMut.RUnlock() + + if !cached { + wl, returnColumns := columns.InsertColumnSet( + reminderAllColumns, + reminderColumnsWithDefault, + reminderColumnsWithoutDefault, + nzDefaults, + ) + + cache.valueMapping, err = queries.BindMapping(reminderType, reminderMapping, wl) + if err != nil { + return err + } + cache.retMapping, err = queries.BindMapping(reminderType, reminderMapping, returnColumns) + if err != nil { + return err + } + if len(wl) != 0 { + cache.query = fmt.Sprintf("INSERT INTO \"reminders\" (\"%s\") %%sVALUES (%s)%%s", strings.Join(wl, "\",\""), strmangle.Placeholders(dialect.UseIndexPlaceholders, len(wl), 1, 1)) + } else { + cache.query = "INSERT INTO \"reminders\" %sDEFAULT VALUES%s" + } + + var queryOutput, queryReturning string + + if len(cache.retMapping) != 0 { + queryReturning = fmt.Sprintf(" RETURNING \"%s\"", strings.Join(returnColumns, "\",\"")) + } + + cache.query = fmt.Sprintf(cache.query, queryOutput, queryReturning) + } + + value := reflect.Indirect(reflect.ValueOf(o)) + vals := queries.ValuesFromMapping(value, cache.valueMapping) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.query) + fmt.Fprintln(writer, vals) + } + + if len(cache.retMapping) != 0 { + err = exec.QueryRowContext(ctx, cache.query, vals...).Scan(queries.PtrsFromMapping(value, cache.retMapping)...) + } else { + _, err = exec.ExecContext(ctx, cache.query, vals...) + } + + if err != nil { + return errors.Wrap(err, "models: unable to insert into reminders") + } + + if !cached { + reminderInsertCacheMut.Lock() + reminderInsertCache[key] = cache + reminderInsertCacheMut.Unlock() + } + + return nil +} + +// UpdateG a single Reminder record using the global executor. +// See Update for more documentation. +func (o *Reminder) UpdateG(ctx context.Context, columns boil.Columns) (int64, error) { + return o.Update(ctx, boil.GetContextDB(), columns) +} + +// Update uses an executor to update the Reminder. +// See boil.Columns.UpdateColumnSet documentation to understand column list inference for updates. +// Update does not automatically update the record in case of default values. Use .Reload() to refresh the records. +func (o *Reminder) Update(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) (int64, error) { + if !boil.TimestampsAreSkipped(ctx) { + currTime := time.Now().In(boil.GetLocation()) + + o.UpdatedAt = currTime + } + + var err error + key := makeCacheKey(columns, nil) + reminderUpdateCacheMut.RLock() + cache, cached := reminderUpdateCache[key] + reminderUpdateCacheMut.RUnlock() + + if !cached { + wl := columns.UpdateColumnSet( + reminderAllColumns, + reminderPrimaryKeyColumns, + ) + + if !columns.IsWhitelist() { + wl = strmangle.SetComplement(wl, []string{"created_at"}) + } + if len(wl) == 0 { + return 0, errors.New("models: unable to update reminders, could not build whitelist") + } + + cache.query = fmt.Sprintf("UPDATE \"reminders\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, wl), + strmangle.WhereClause("\"", "\"", len(wl)+1, reminderPrimaryKeyColumns), + ) + cache.valueMapping, err = queries.BindMapping(reminderType, reminderMapping, append(wl, reminderPrimaryKeyColumns...)) + if err != nil { + return 0, err + } + } + + values := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), cache.valueMapping) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.query) + fmt.Fprintln(writer, values) + } + var result sql.Result + result, err = exec.ExecContext(ctx, cache.query, values...) + if err != nil { + return 0, errors.Wrap(err, "models: unable to update reminders row") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by update for reminders") + } + + if !cached { + reminderUpdateCacheMut.Lock() + reminderUpdateCache[key] = cache + reminderUpdateCacheMut.Unlock() + } + + return rowsAff, nil +} + +// UpdateAllG updates all rows with the specified column values. +func (q reminderQuery) UpdateAllG(ctx context.Context, cols M) (int64, error) { + return q.UpdateAll(ctx, boil.GetContextDB(), cols) +} + +// UpdateAll updates all rows with the specified column values. +func (q reminderQuery) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) { + queries.SetUpdate(q.Query, cols) + + result, err := q.Query.ExecContext(ctx, exec) + if err != nil { + return 0, errors.Wrap(err, "models: unable to update all for reminders") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: unable to retrieve rows affected for reminders") + } + + return rowsAff, nil +} + +// UpdateAllG updates all rows with the specified column values. +func (o ReminderSlice) UpdateAllG(ctx context.Context, cols M) (int64, error) { + return o.UpdateAll(ctx, boil.GetContextDB(), cols) +} + +// UpdateAll updates all rows with the specified column values, using an executor. +func (o ReminderSlice) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) { + ln := int64(len(o)) + if ln == 0 { + return 0, nil + } + + if len(cols) == 0 { + return 0, errors.New("models: update all requires at least one column argument") + } + + colNames := make([]string, len(cols)) + args := make([]interface{}, len(cols)) + + i := 0 + for name, value := range cols { + colNames[i] = name + args[i] = value + i++ + } + + // Append all of the primary key values for each column + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), reminderPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := fmt.Sprintf("UPDATE \"reminders\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, colNames), + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), len(colNames)+1, reminderPrimaryKeyColumns, len(o))) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args...) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "models: unable to update all in reminder slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: unable to retrieve rows affected all in update all reminder") + } + return rowsAff, nil +} + +// UpsertG attempts an insert, and does an update or ignore on conflict. +func (o *Reminder) UpsertG(ctx context.Context, updateOnConflict bool, conflictColumns []string, updateColumns, insertColumns boil.Columns, opts ...UpsertOptionFunc) error { + return o.Upsert(ctx, boil.GetContextDB(), updateOnConflict, conflictColumns, updateColumns, insertColumns, opts...) +} + +// Upsert attempts an insert using an executor, and does an update or ignore on conflict. +// See boil.Columns documentation for how to properly use updateColumns and insertColumns. +func (o *Reminder) Upsert(ctx context.Context, exec boil.ContextExecutor, updateOnConflict bool, conflictColumns []string, updateColumns, insertColumns boil.Columns, opts ...UpsertOptionFunc) error { + if o == nil { + return errors.New("models: no reminders provided for upsert") + } + if !boil.TimestampsAreSkipped(ctx) { + currTime := time.Now().In(boil.GetLocation()) + + if o.CreatedAt.IsZero() { + o.CreatedAt = currTime + } + o.UpdatedAt = currTime + } + + nzDefaults := queries.NonZeroDefaultSet(reminderColumnsWithDefault, o) + + // Build cache key in-line uglily - mysql vs psql problems + buf := strmangle.GetBuffer() + if updateOnConflict { + buf.WriteByte('t') + } else { + buf.WriteByte('f') + } + buf.WriteByte('.') + for _, c := range conflictColumns { + buf.WriteString(c) + } + buf.WriteByte('.') + buf.WriteString(strconv.Itoa(updateColumns.Kind)) + for _, c := range updateColumns.Cols { + buf.WriteString(c) + } + buf.WriteByte('.') + buf.WriteString(strconv.Itoa(insertColumns.Kind)) + for _, c := range insertColumns.Cols { + buf.WriteString(c) + } + buf.WriteByte('.') + for _, c := range nzDefaults { + buf.WriteString(c) + } + key := buf.String() + strmangle.PutBuffer(buf) + + reminderUpsertCacheMut.RLock() + cache, cached := reminderUpsertCache[key] + reminderUpsertCacheMut.RUnlock() + + var err error + + if !cached { + insert, _ := insertColumns.InsertColumnSet( + reminderAllColumns, + reminderColumnsWithDefault, + reminderColumnsWithoutDefault, + nzDefaults, + ) + + update := updateColumns.UpdateColumnSet( + reminderAllColumns, + reminderPrimaryKeyColumns, + ) + + if updateOnConflict && len(update) == 0 { + return errors.New("models: unable to upsert reminders, could not build update column list") + } + + ret := strmangle.SetComplement(reminderAllColumns, strmangle.SetIntersect(insert, update)) + + conflict := conflictColumns + if len(conflict) == 0 && updateOnConflict && len(update) != 0 { + if len(reminderPrimaryKeyColumns) == 0 { + return errors.New("models: unable to upsert reminders, could not build conflict column list") + } + + conflict = make([]string, len(reminderPrimaryKeyColumns)) + copy(conflict, reminderPrimaryKeyColumns) + } + cache.query = buildUpsertQueryPostgres(dialect, "\"reminders\"", updateOnConflict, ret, update, conflict, insert, opts...) + + cache.valueMapping, err = queries.BindMapping(reminderType, reminderMapping, insert) + if err != nil { + return err + } + if len(ret) != 0 { + cache.retMapping, err = queries.BindMapping(reminderType, reminderMapping, ret) + if err != nil { + return err + } + } + } + + value := reflect.Indirect(reflect.ValueOf(o)) + vals := queries.ValuesFromMapping(value, cache.valueMapping) + var returns []interface{} + if len(cache.retMapping) != 0 { + returns = queries.PtrsFromMapping(value, cache.retMapping) + } + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.query) + fmt.Fprintln(writer, vals) + } + if len(cache.retMapping) != 0 { + err = exec.QueryRowContext(ctx, cache.query, vals...).Scan(returns...) + if errors.Is(err, sql.ErrNoRows) { + err = nil // Postgres doesn't return anything when there's no update + } + } else { + _, err = exec.ExecContext(ctx, cache.query, vals...) + } + if err != nil { + return errors.Wrap(err, "models: unable to upsert reminders") + } + + if !cached { + reminderUpsertCacheMut.Lock() + reminderUpsertCache[key] = cache + reminderUpsertCacheMut.Unlock() + } + + return nil +} + +// DeleteG deletes a single Reminder record. +// DeleteG will match against the primary key column to find the record to delete. +func (o *Reminder) DeleteG(ctx context.Context, hardDelete bool) (int64, error) { + return o.Delete(ctx, boil.GetContextDB(), hardDelete) +} + +// Delete deletes a single Reminder record with an executor. +// Delete will match against the primary key column to find the record to delete. +func (o *Reminder) Delete(ctx context.Context, exec boil.ContextExecutor, hardDelete bool) (int64, error) { + if o == nil { + return 0, errors.New("models: no Reminder provided for delete") + } + + var ( + sql string + args []interface{} + ) + if hardDelete { + args = queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), reminderPrimaryKeyMapping) + sql = "DELETE FROM \"reminders\" WHERE \"id\"=$1" + } else { + currTime := time.Now().In(boil.GetLocation()) + o.DeletedAt = null.TimeFrom(currTime) + wl := []string{"deleted_at"} + sql = fmt.Sprintf("UPDATE \"reminders\" SET %s WHERE \"id\"=$2", + strmangle.SetParamNames("\"", "\"", 1, wl), + ) + valueMapping, err := queries.BindMapping(reminderType, reminderMapping, append(wl, reminderPrimaryKeyColumns...)) + if err != nil { + return 0, err + } + args = queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), valueMapping) + } + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args...) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "models: unable to delete from reminders") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by delete for reminders") + } + + return rowsAff, nil +} + +func (q reminderQuery) DeleteAllG(ctx context.Context, hardDelete bool) (int64, error) { + return q.DeleteAll(ctx, boil.GetContextDB(), hardDelete) +} + +// DeleteAll deletes all matching rows. +func (q reminderQuery) DeleteAll(ctx context.Context, exec boil.ContextExecutor, hardDelete bool) (int64, error) { + if q.Query == nil { + return 0, errors.New("models: no reminderQuery provided for delete all") + } + + if hardDelete { + queries.SetDelete(q.Query) + } else { + currTime := time.Now().In(boil.GetLocation()) + queries.SetUpdate(q.Query, M{"deleted_at": currTime}) + } + + result, err := q.Query.ExecContext(ctx, exec) + if err != nil { + return 0, errors.Wrap(err, "models: unable to delete all from reminders") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for reminders") + } + + return rowsAff, nil +} + +// DeleteAllG deletes all rows in the slice. +func (o ReminderSlice) DeleteAllG(ctx context.Context, hardDelete bool) (int64, error) { + return o.DeleteAll(ctx, boil.GetContextDB(), hardDelete) +} + +// DeleteAll deletes all rows in the slice, using an executor. +func (o ReminderSlice) DeleteAll(ctx context.Context, exec boil.ContextExecutor, hardDelete bool) (int64, error) { + if len(o) == 0 { + return 0, nil + } + + var ( + sql string + args []interface{} + ) + if hardDelete { + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), reminderPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + sql = "DELETE FROM \"reminders\" WHERE " + + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 1, reminderPrimaryKeyColumns, len(o)) + } else { + currTime := time.Now().In(boil.GetLocation()) + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), reminderPrimaryKeyMapping) + args = append(args, pkeyArgs...) + obj.DeletedAt = null.TimeFrom(currTime) + } + wl := []string{"deleted_at"} + sql = fmt.Sprintf("UPDATE \"reminders\" SET %s WHERE "+ + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 2, reminderPrimaryKeyColumns, len(o)), + strmangle.SetParamNames("\"", "\"", 1, wl), + ) + args = append([]interface{}{currTime}, args...) + } + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "models: unable to delete all from reminder slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for reminders") + } + + return rowsAff, nil +} + +// ReloadG refetches the object from the database using the primary keys. +func (o *Reminder) ReloadG(ctx context.Context) error { + if o == nil { + return errors.New("models: no Reminder provided for reload") + } + + return o.Reload(ctx, boil.GetContextDB()) +} + +// Reload refetches the object from the database +// using the primary keys with an executor. +func (o *Reminder) Reload(ctx context.Context, exec boil.ContextExecutor) error { + ret, err := FindReminder(ctx, exec, o.ID) + if err != nil { + return err + } + + *o = *ret + return nil +} + +// ReloadAllG refetches every row with matching primary key column values +// and overwrites the original object slice with the newly updated slice. +func (o *ReminderSlice) ReloadAllG(ctx context.Context) error { + if o == nil { + return errors.New("models: empty ReminderSlice provided for reload all") + } + + return o.ReloadAll(ctx, boil.GetContextDB()) +} + +// ReloadAll refetches every row with matching primary key column values +// and overwrites the original object slice with the newly updated slice. +func (o *ReminderSlice) ReloadAll(ctx context.Context, exec boil.ContextExecutor) error { + if o == nil || len(*o) == 0 { + return nil + } + + slice := ReminderSlice{} + var args []interface{} + for _, obj := range *o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), reminderPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "SELECT \"reminders\".* FROM \"reminders\" WHERE " + + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 1, reminderPrimaryKeyColumns, len(*o)) + + "and \"deleted_at\" is null" + + q := queries.Raw(sql, args...) + + err := q.Bind(ctx, exec, &slice) + if err != nil { + return errors.Wrap(err, "models: unable to reload all in ReminderSlice") + } + + *o = slice + + return nil +} + +// ReminderExistsG checks if the Reminder row exists. +func ReminderExistsG(ctx context.Context, iD int) (bool, error) { + return ReminderExists(ctx, boil.GetContextDB(), iD) +} + +// ReminderExists checks if the Reminder row exists. +func ReminderExists(ctx context.Context, exec boil.ContextExecutor, iD int) (bool, error) { + var exists bool + sql := "select exists(select 1 from \"reminders\" where \"id\"=$1 and \"deleted_at\" is null limit 1)" + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, iD) + } + row := exec.QueryRowContext(ctx, sql, iD) + + err := row.Scan(&exists) + if err != nil { + return false, errors.Wrap(err, "models: unable to check if reminders exists") + } + + return exists, nil +} + +// Exists checks if the Reminder row exists. +func (o *Reminder) Exists(ctx context.Context, exec boil.ContextExecutor) (bool, error) { + return ReminderExists(ctx, exec, o.ID) +} diff --git a/reminders/plugin_bot.go b/reminders/plugin_bot.go index 83af5579b0..354237b70a 100644 --- a/reminders/plugin_bot.go +++ b/reminders/plugin_bot.go @@ -1,12 +1,12 @@ package reminders import ( + "context" + "database/sql" "errors" "fmt" - "strconv" "strings" "time" - "unicode/utf8" "github.com/botlabs-gg/yagpdb/v2/bot" "github.com/botlabs-gg/yagpdb/v2/commands" @@ -16,7 +16,8 @@ import ( "github.com/botlabs-gg/yagpdb/v2/lib/dcmd" "github.com/botlabs-gg/yagpdb/v2/lib/discordgo" "github.com/botlabs-gg/yagpdb/v2/lib/dstate" - "github.com/jinzhu/gorm" + "github.com/botlabs-gg/yagpdb/v2/reminders/models" + "github.com/volatiletech/sqlboiler/v4/queries/qm" ) var logger = common.GetPluginLogger(&Plugin{}) @@ -29,11 +30,17 @@ func (p *Plugin) AddCommands() { } func (p *Plugin) BotInit() { - // scheduledevents.RegisterEventHandler("reminders_check_user", checkUserEvtHandlerLegacy) scheduledevents2.RegisterHandler("reminders_check_user", int64(0), checkUserScheduledEvent) scheduledevents2.RegisterLegacyMigrater("reminders_check_user", migrateLegacyScheduledEvents) } +const ( + MaxReminders = 25 + + MaxReminderOffset = time.Hour * 24 * 366 + MaxReminderOffsetExceededMsg = "Can be max 1 year from now..." +) + // Reminder management commands var cmds = []*commands.YAGCommand{ { @@ -52,23 +59,19 @@ var cmds = []*commands.YAGCommand{ SlashCommandEnabled: true, DefaultEnabled: true, RunFunc: func(parsed *dcmd.Data) (interface{}, error) { - currentReminders, _ := GetUserReminders(parsed.Author.ID) - if len(currentReminders) >= 25 { - return "You can have a maximum of 25 active reminders, list all your reminders with the `reminders` command in DM, doing it in a server will only show reminders set in the server", nil + uid := discordgo.StrID(parsed.Author.ID) + count, _ := models.Reminders(models.ReminderWhere.UserID.EQ(uid)).CountG(parsed.Context()) + if count >= MaxReminders { + return fmt.Sprintf("You can have a maximum of %d active reminders; list all your reminders with the `reminders` command in DM, doing it in a server will only show reminders set in the server", MaxReminders), nil } if parsed.Author.Bot { - return nil, errors.New("cannot create reminder for Bots, you're most likely trying to use `execAdmin` to create a reminder, use `exec` instead") + return nil, errors.New("cannot create reminder for bots; you're likely trying to use `execAdmin` to create a reminder (use `exec` instead)") } - fromNow := parsed.Args[0].Value.(time.Duration) - - durString := common.HumanizeDuration(common.DurationPrecisionSeconds, fromNow) - when := time.Now().Add(fromNow) - tUnix := fmt.Sprint(when.Unix()) - - if when.After(time.Now().Add(time.Hour * 24 * 366)) { - return "Can be max 365 days from now...", nil + offsetFromNow := parsed.Args[0].Value.(time.Duration) + if offsetFromNow > MaxReminderOffset { + return MaxReminderOffsetExceededMsg, nil } id := parsed.ChannelID @@ -77,7 +80,7 @@ var cmds = []*commands.YAGCommand{ hasPerms, err := bot.AdminOrPermMS(parsed.GuildData.GS.ID, id, parsed.GuildData.MS, discordgo.PermissionSendMessages|discordgo.PermissionViewChannel) if err != nil { - return "Failed checking permissions, please try again or join the support server.", err + return "Failed checking permissions; please try again or join the support server.", err } if !hasPerms { @@ -85,12 +88,14 @@ var cmds = []*commands.YAGCommand{ } } + when := time.Now().Add(offsetFromNow) _, err := NewReminder(parsed.Author.ID, parsed.GuildData.GS.ID, id, parsed.Args[1].Str(), when) if err != nil { return nil, err } - return "Set a reminder in " + durString + " from now ()\nView reminders with the `reminders` command", nil + durString := common.HumanizeDuration(common.DurationPrecisionSeconds, offsetFromNow) + return fmt.Sprintf("Set a reminder in %s from now ()\nView reminders with the `reminders` command", durString, when.Unix()), nil }, }, { @@ -102,28 +107,29 @@ var cmds = []*commands.YAGCommand{ IsResponseEphemeral: true, RunInDM: true, RunFunc: func(parsed *dcmd.Data) (interface{}, error) { + uid := discordgo.StrID(parsed.Author.ID) + qms := []qm.QueryMod{models.ReminderWhere.UserID.EQ(uid)} + + // if command used in server, only show reminders in that server + var inServerSuffix string + if inServer := parsed.GuildData != nil; inServer { + inServerSuffix = " in this server" - var currentReminders []*Reminder - var err error - //command was used in DM - inServerString := "" - if parsed.GuildData == nil { - currentReminders, err = GetUserReminders(parsed.Author.ID) - } else { - inServerString = " in this server" - currentReminders, err = GetGuildUserReminder(parsed.Author.ID, parsed.GuildData.GS.ID) + guildID := parsed.GuildData.GS.ID + qms = append(qms, models.ReminderWhere.GuildID.EQ(guildID)) } + currentReminders, err := models.Reminders(qms...).AllG(parsed.Context()) if err != nil { return nil, err } if len(currentReminders) == 0 { - return fmt.Sprintf("You have no reminders%s. Create reminders with the `remindme` command.", inServerString), nil + return fmt.Sprintf("You have no reminders%s. Create reminders with the `remindme` command", inServerSuffix), nil } - out := fmt.Sprintf("Your reminders%s:\n", inServerString) - out += stringReminders(currentReminders, false) + out := fmt.Sprintf("Your reminders%s:\n", inServerSuffix) + out += DisplayReminders(currentReminders, ModeDisplayUserReminders) out += "\nRemove a reminder with `delreminder/rmreminder (id)` where id is the first number for each reminder above.\nTo clear all reminders, use `delreminder` with the `-a` switch." return out, nil }, @@ -132,20 +138,14 @@ var cmds = []*commands.YAGCommand{ CmdCategory: commands.CategoryTool, Name: "CReminders", Aliases: []string{"channelreminders"}, - Description: "Lists reminders in channel, only users with 'manage channel' permissions can use this.", + Description: "Lists reminders in channel", + RequireDiscordPerms: []int64{discordgo.PermissionManageChannels}, SlashCommandEnabled: true, DefaultEnabled: true, IsResponseEphemeral: true, RunFunc: func(parsed *dcmd.Data) (interface{}, error) { - ok, err := bot.AdminOrPermMS(parsed.GuildData.GS.ID, parsed.ChannelID, parsed.GuildData.MS, discordgo.PermissionManageChannels) - if err != nil { - return nil, err - } - if !ok { - return "You do not have access to this command (requires manage channel permission)", nil - } - - currentReminders, err := GetChannelReminders(parsed.ChannelID) + cid := discordgo.StrID(parsed.ChannelID) + currentReminders, err := models.Reminders(models.ReminderWhere.ChannelID.EQ(cid)).AllG(parsed.Context()) if err != nil { return nil, err } @@ -155,7 +155,7 @@ var cmds = []*commands.YAGCommand{ } out := "Reminders in this channel:\n" - out += stringReminders(currentReminders, true) + out += DisplayReminders(currentReminders, ModeDisplayChannelReminders) out += "\nRemove a reminder with `delreminder/rmreminder (id)` where id is the first number for each reminder above" return out, nil }, @@ -177,17 +177,13 @@ var cmds = []*commands.YAGCommand{ DefaultEnabled: true, IsResponseEphemeral: true, RunFunc: func(parsed *dcmd.Data) (interface{}, error) { - var reminder Reminder - - clearAll := parsed.Switch("a").Value != nil && parsed.Switch("a").Value.(bool) - if clearAll { - db := common.GORM.Where("user_id = ?", parsed.Author.ID).Delete(&reminder) - err := db.Error + if clearAll := parsed.Switch("a").Bool(); clearAll { + uid := discordgo.StrID(parsed.Author.ID) + count, err := models.Reminders(models.ReminderWhere.UserID.EQ(uid)).DeleteAllG(parsed.Context(), false /* hardDelete */) if err != nil { return "Error clearing reminders", err } - count := db.RowsAffected if count == 0 { return "No reminders to clear", nil } @@ -198,20 +194,22 @@ var cmds = []*commands.YAGCommand{ return "No reminder ID provided", nil } - err := common.GORM.Where(parsed.Args[0].Int()).First(&reminder).Error + reminder, err := models.FindReminderG(parsed.Context(), parsed.Args[0].Int()) if err != nil { - if err == gorm.ErrRecordNotFound { - return "No reminder by that id found", nil + if err == sql.ErrNoRows { + return "No reminder by that ID found", nil } return "Error retrieving reminder", err } - // Check perms + // check perms if reminder.UserID != discordgo.StrID(parsed.Author.ID) { if reminder.GuildID != parsed.GuildData.GS.ID { return "You can only delete reminders that are not your own in the guild the reminder was originally created", nil } - ok, err := bot.AdminOrPermMS(reminder.GuildID, reminder.ChannelIDInt(), parsed.GuildData.MS, discordgo.PermissionManageChannels) + + cid, _ := discordgo.ParseID(reminder.ChannelID) + ok, err := bot.AdminOrPermMS(reminder.GuildID, cid, parsed.GuildData.MS, discordgo.PermissionManageChannels) if err != nil { return nil, err } @@ -220,70 +218,33 @@ var cmds = []*commands.YAGCommand{ } } - // Do the actual deletion - err = common.GORM.Delete(reminder).Error - if err != nil { - return nil, err - } - - // Check if we should remove the scheduled event - currentReminders, err := GetUserReminders(reminder.UserIDInt()) + // just deleting from database is enough; we need not delete the + // scheduled event since the handler will check database + _, err = reminder.DeleteG(parsed.Context(), false /* hardDelete */) if err != nil { return nil, err } - delMsg := fmt.Sprintf("Deleted reminder **#%d**: '%s'", reminder.ID, limitString(reminder.Message)) - - // If there is another reminder with the same timestamp, do not remove the scheduled event - for _, v := range currentReminders { - if v.When == reminder.When { - return delMsg, nil - } - } - - return delMsg, nil + return fmt.Sprintf("Deleted reminder **#%d**: '%s'", reminder.ID, CutReminderShort(reminder.Message)), nil }, }, } -func stringReminders(reminders []*Reminder, displayUsernames bool) string { - out := "" - for _, v := range reminders { - parsedCID, _ := strconv.ParseInt(v.ChannelID, 10, 64) - - t := time.Unix(v.When, 0) - tUnix := t.Unix() - timeFromNow := common.HumanizeTime(common.DurationPrecisionMinutes, t) - if !displayUsernames { - channel := "<#" + discordgo.StrID(parsedCID) + ">" - out += fmt.Sprintf("**%d**: %s: '%s' - %s from now ()\n", v.ID, channel, limitString(v.Message), timeFromNow, tUnix) - } else { - member, _ := bot.GetMember(v.GuildID, v.UserIDInt()) - username := "Unknown user" - if member != nil { - username = member.User.Username - } - out += fmt.Sprintf("**%d**: %s: '%s' - %s from now ()\n", v.ID, username, limitString(v.Message), timeFromNow, tUnix) - } - } - return out -} - func checkUserScheduledEvent(evt *seventsmodels.ScheduledEvent, data interface{}) (retry bool, err error) { - // !important! the evt.GuildID can be 1 in cases where it was migrated from the legacy scheduled event system + // IMPORTANT: evt.GuildID can be 1 in cases where it was migrated from the + // legacy scheduled event system. - userID := *data.(*int64) - - reminders, err := GetUserReminders(userID) + userID := discordgo.StrID(*data.(*int64)) + reminders, err := models.Reminders(models.ReminderWhere.UserID.EQ(userID)).AllG(context.Background()) if err != nil { return true, err } - now := time.Now() - nowUnix := now.Unix() - for _, v := range reminders { - if v.When <= nowUnix { - err := v.Trigger() + // TODO: can we move this filtering step into the database query? + nowUnix := time.Now().Unix() + for _, r := range reminders { + if r.When <= nowUnix { + err := TriggerReminder(r) if err != nil { // possibly try again return scheduledevents2.CheckDiscordErrRetry(err), err @@ -295,22 +256,12 @@ func checkUserScheduledEvent(evt *seventsmodels.ScheduledEvent, data interface{} } func migrateLegacyScheduledEvents(t time.Time, data string) error { - split := strings.Split(data, ":") - if len(split) < 2 { + _, userID, ok := strings.Cut(data, ":") + if !ok { logger.Error("invalid check user scheduled event: ", data) return nil } - parsed, _ := strconv.ParseInt(split[1], 10, 64) - + parsed, _ := discordgo.ParseID(userID) return scheduledevents2.ScheduleEvent("reminders_check_user", 1, t, parsed) } - -func limitString(s string) string { - if utf8.RuneCountInString(s) < 50 { - return s - } - - runes := []rune(s) - return string(runes[:47]) + "..." -} diff --git a/reminders/reminders.go b/reminders/reminders.go index 1b8bcbcd2b..425554ba84 100644 --- a/reminders/reminders.go +++ b/reminders/reminders.go @@ -1,27 +1,30 @@ package reminders import ( - "strconv" + "context" + "fmt" + "strings" "time" + "github.com/botlabs-gg/yagpdb/v2/bot" "github.com/botlabs-gg/yagpdb/v2/common" "github.com/botlabs-gg/yagpdb/v2/common/mqueue" "github.com/botlabs-gg/yagpdb/v2/common/scheduledevents2" "github.com/botlabs-gg/yagpdb/v2/lib/discordgo" - "github.com/jinzhu/gorm" + "github.com/botlabs-gg/yagpdb/v2/reminders/models" "github.com/sirupsen/logrus" + "github.com/volatiletech/sqlboiler/v4/boil" ) +//go:generate sqlboiler --no-hooks --add-soft-deletes psql + type Plugin struct{} func RegisterPlugin() { - err := common.GORM.AutoMigrate(&Reminder{}).Error - if err != nil { - panic(err) - } - p := &Plugin{} common.RegisterPlugin(p) + + common.InitSchemas("reminders", DBSchemas...) } func (p *Plugin) PluginInfo() *common.PluginInfo { @@ -32,31 +35,8 @@ func (p *Plugin) PluginInfo() *common.PluginInfo { } } -type Reminder struct { - gorm.Model - UserID string - ChannelID string - GuildID int64 - Message string - When int64 -} - -func (r *Reminder) UserIDInt() (i int64) { - i, _ = strconv.ParseInt(r.UserID, 10, 64) - return -} - -func (r *Reminder) ChannelIDInt() (i int64) { - i, _ = strconv.ParseInt(r.ChannelID, 10, 64) - return -} - -func (r *Reminder) Trigger() error { - // remove the actual reminder - rows := common.GORM.Delete(r).RowsAffected - if rows < 1 { - logger.Info("Tried to execute multiple reminders at once") - } +func TriggerReminder(r *models.Reminder) error { + r.DeleteG(context.Background(), false /* hardDelete */) logger.WithFields(logrus.Fields{"channel": r.ChannelID, "user": r.UserID, "message": r.Message, "id": r.ID}).Info("Triggered reminder") embed := &discordgo.MessageEmbed{ @@ -64,62 +44,76 @@ func (r *Reminder) Trigger() error { Description: common.ReplaceServerInvites(r.Message, r.GuildID, "(removed-invite)"), } - mqueue.QueueMessage(&mqueue.QueuedElement{ + channelID, _ := discordgo.ParseID(r.ChannelID) + userID, _ := discordgo.ParseID(r.UserID) + return mqueue.QueueMessage(&mqueue.QueuedElement{ Source: "reminder", SourceItemID: "", GuildID: r.GuildID, - ChannelID: r.ChannelIDInt(), + ChannelID: channelID, MessageEmbed: embed, MessageStr: "**Reminder** for <@" + r.UserID + ">", AllowedMentions: discordgo.AllowedMentions{ - Users: []int64{r.UserIDInt()}, + Users: []int64{userID}, }, Priority: 10, // above all feeds }) - return nil -} - -func GetGuildUserReminder(userID, guildID int64) (results []*Reminder, err error) { - err = common.GORM.Where(&Reminder{UserID: discordgo.StrID(userID), GuildID: guildID}).Find(&results).Error - if err == gorm.ErrRecordNotFound { - err = nil - } - return } -func GetUserReminders(userID int64) (results []*Reminder, err error) { - err = common.GORM.Where(&Reminder{UserID: discordgo.StrID(userID)}).Find(&results).Error - if err == gorm.ErrRecordNotFound { - err = nil - } - return -} - -func GetChannelReminders(channel int64) (results []*Reminder, err error) { - err = common.GORM.Where(&Reminder{ChannelID: discordgo.StrID(channel)}).Find(&results).Error - if err == gorm.ErrRecordNotFound { - err = nil - } - return -} - -func NewReminder(userID int64, guildID int64, channelID int64, message string, when time.Time) (*Reminder, error) { - whenUnix := when.Unix() - reminder := &Reminder{ +func NewReminder(userID int64, guildID int64, channelID int64, message string, when time.Time) (*models.Reminder, error) { + reminder := &models.Reminder{ UserID: discordgo.StrID(userID), ChannelID: discordgo.StrID(channelID), Message: message, - When: whenUnix, + When: when.Unix(), GuildID: guildID, } - err := common.GORM.Create(reminder).Error + err := reminder.InsertG(context.Background(), boil.Infer()) if err != nil { return nil, err } err = scheduledevents2.ScheduleEvent("reminders_check_user", guildID, when, userID) - // err = scheduledevents.ScheduleEvent("reminders_check_user:"+strconv.FormatInt(whenUnix, 10), discordgo.StrID(userID), when) return reminder, err } + +type DisplayRemindersMode int + +const ( + ModeDisplayChannelReminders DisplayRemindersMode = iota + ModeDisplayUserReminders +) + +func DisplayReminders(reminders models.ReminderSlice, mode DisplayRemindersMode) string { + var out strings.Builder + for _, r := range reminders { + t := time.Unix(r.When, 0) + timeFromNow := common.HumanizeTime(common.DurationPrecisionMinutes, t) + + switch mode { + case ModeDisplayChannelReminders: + // don't show the channel; do show the user + uid, _ := discordgo.ParseID(r.UserID) + member, _ := bot.GetMember(r.GuildID, uid) + username := "Unknown user" + if member != nil { + username = member.User.Username + } + + fmt.Fprintf(&out, "**%d**: %s: '%s' - %s from now ()\n", r.ID, username, CutReminderShort(r.Message), timeFromNow, t.Unix()) + + case ModeDisplayUserReminders: + // do show the channel; don't show the user + channel := "<#" + r.ChannelID + ">" + fmt.Fprintf(&out, "**%d**: %s: '%s' - %s from now ()\n", r.ID, channel, CutReminderShort(r.Message), timeFromNow, t.Unix()) + } + } + + return out.String() +} + +func CutReminderShort(msg string) string { + return common.CutStringShort(msg, 50) +} diff --git a/reminders/schema.go b/reminders/schema.go new file mode 100644 index 0000000000..a6990b2bde --- /dev/null +++ b/reminders/schema.go @@ -0,0 +1,57 @@ +package reminders + +var DBSchemas = []string{` +CREATE TABLE IF NOT EXISTS reminders ( + id SERIAL PRIMARY KEY, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL, + deleted_at TIMESTAMP WITH TIME ZONE, + + -- text instead of bigint for legacy compatibility + user_id TEXT NOT NULL, + channel_id TEXT NOT NULL, + guild_id BIGINT NOT NULL, + message TEXT NOT NULL, + "when" BIGINT NOT NULL +); +`, ` +CREATE INDEX IF NOT EXISTS idx_reminders_deleted_at ON reminders(deleted_at); +`, ` +-- Previous versions of the reputation module used gorm instead of sqlboiler, +-- which does not add NOT NULL constraints by default. Therefore, ensure the +-- NOT NULL constraints are present in existing tables as well. + +-- The first few columns below have always been set since the reminders plugin was +-- added, so barring the presence of invalid entries, we can safely add NOT NULL +-- constraints without error. + +ALTER TABLE reminders ALTER COLUMN created_at SET NOT NULL; +`, ` +ALTER TABLE reminders ALTER COLUMN updated_at SET NOT NULL; +`, ` +ALTER TABLE reminders ALTER COLUMN user_id SET NOT NULL; +`, ` +ALTER TABLE reminders ALTER COLUMN channel_id SET NOT NULL; +`, ` +ALTER TABLE reminders ALTER COLUMN message SET NOT NULL; +`, ` +ALTER TABLE reminders ALTER COLUMN "when" SET NOT NULL; +`, ` +DO $$ +BEGIN + +-- The guild_id column is more annoying to deal with. When the reminders plugin +-- was first created, the reminders table did not have a guild_id column -- it +-- was added later, in October 2018 (9f5ef28). So reminders before then could +-- plausibly have guild_id = NULL, meaning directly adding the NOT NULL +-- constraint would fail. But since the maximum offset of a reminder is 1 year, +-- all such reminders have now expired and so we can just delete them before +-- adding the constraint. + +-- Only run if we haven't added the NOT NULL constraint yet. +IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name='reminders' AND column_name='guild_id' AND is_nullable='YES') THEN + DELETE FROM reminders WHERE guild_id IS NULL; + ALTER TABLE reminders ALTER COLUMN guild_id SET NOT NULL; +END IF; +END $$; +`} diff --git a/reminders/sqlboiler.toml b/reminders/sqlboiler.toml new file mode 100644 index 0000000000..cc4cee83ad --- /dev/null +++ b/reminders/sqlboiler.toml @@ -0,0 +1,15 @@ +add-global-variants = true +no-hooks = true +no-tests = true + +[psql] +dbname = "yagpdb" +host = "localhost" +user = "postgres" +pass = "pass" +sslmode = "disable" +whitelist = ["reminders"] + +[auto-columns] +created = "created_at" +updated = "updated_at"