added scheduled commands, .scha, .schd and .schl

This commit is contained in:
Toastie 2025-03-17 19:51:05 +13:00
parent 83701a9e0b
commit ca46786c5e
Signed by: toastie_t0ast
GPG key ID: 0861BE54AD481DC7
17 changed files with 14143 additions and 13540 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
START TRANSACTION;
CREATE TABLE scheduledcommand (
id integer GENERATED BY DEFAULT AS IDENTITY,
userid numeric(20,0) NOT NULL,
channelid numeric(20,0) NOT NULL,
guildid numeric(20,0) NOT NULL,
messageid numeric(20,0) NOT NULL,
text text NOT NULL,
"when" timestamp without time zone NOT NULL,
CONSTRAINT pk_scheduledcommand PRIMARY KEY (id)
);
CREATE INDEX ix_scheduledcommand_guildid ON scheduledcommand (guildid);
CREATE INDEX ix_scheduledcommand_userid ON scheduledcommand (userid);
CREATE INDEX ix_scheduledcommand_when ON scheduledcommand ("when");
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
VALUES ('20250317063129_scheduled-commands', '9.0.1');
COMMIT;

File diff suppressed because it is too large Load diff

View file

@ -855,6 +855,24 @@ namespace EllieBot.Migrations.PostgreSql
table.UniqueConstraint("ak_sargroup_guildid_groupnumber", x => new { x.guildid, x.groupnumber });
});
migrationBuilder.CreateTable(
name: "scheduledcommand",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
messageid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
text = table.Column<string>(type: "text", nullable: false),
when = table.Column<DateTime>(type: "timestamp without time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_scheduledcommand", x => x.id);
});
migrationBuilder.CreateTable(
name: "shopentry",
columns: table => new
@ -2048,6 +2066,21 @@ namespace EllieBot.Migrations.PostgreSql
column: "guildid",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_scheduledcommand_guildid",
table: "scheduledcommand",
column: "guildid");
migrationBuilder.CreateIndex(
name: "ix_scheduledcommand_userid",
table: "scheduledcommand",
column: "userid");
migrationBuilder.CreateIndex(
name: "ix_scheduledcommand_when",
table: "scheduledcommand",
column: "when");
migrationBuilder.CreateIndex(
name: "ix_shopentry_guildid_index",
table: "shopentry",
@ -2472,6 +2505,9 @@ namespace EllieBot.Migrations.PostgreSql
migrationBuilder.DropTable(
name: "sarautodelete");
migrationBuilder.DropTable(
name: "scheduledcommand");
migrationBuilder.DropTable(
name: "shopentryitem");
@ -2590,4 +2626,4 @@ namespace EllieBot.Migrations.PostgreSql
name: "discorduser");
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,22 @@
BEGIN TRANSACTION;
CREATE TABLE "ScheduledCommand" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ScheduledCommand" PRIMARY KEY AUTOINCREMENT,
"UserId" INTEGER NOT NULL,
"ChannelId" INTEGER NOT NULL,
"GuildId" INTEGER NOT NULL,
"MessageId" INTEGER NOT NULL,
"Text" TEXT NOT NULL,
"When" TEXT NOT NULL
);
CREATE INDEX "IX_ScheduledCommand_GuildId" ON "ScheduledCommand" ("GuildId");
CREATE INDEX "IX_ScheduledCommand_UserId" ON "ScheduledCommand" ("UserId");
CREATE INDEX "IX_ScheduledCommand_When" ON "ScheduledCommand" ("When");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20250317063119_scheduled-commands', '9.0.1');
COMMIT;

File diff suppressed because it is too large Load diff

View file

@ -857,6 +857,24 @@ namespace EllieBot.Migrations.Sqlite
table.UniqueConstraint("AK_SarGroup_GuildId_GroupNumber", x => new { x.GuildId, x.GroupNumber });
});
migrationBuilder.CreateTable(
name: "ScheduledCommand",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
MessageId = table.Column<ulong>(type: "INTEGER", nullable: false),
Text = table.Column<string>(type: "TEXT", nullable: false),
When = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ScheduledCommand", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ShopEntry",
columns: table => new
@ -2050,6 +2068,21 @@ namespace EllieBot.Migrations.Sqlite
column: "GuildId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ScheduledCommand_GuildId",
table: "ScheduledCommand",
column: "GuildId");
migrationBuilder.CreateIndex(
name: "IX_ScheduledCommand_UserId",
table: "ScheduledCommand",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_ScheduledCommand_When",
table: "ScheduledCommand",
column: "When");
migrationBuilder.CreateIndex(
name: "IX_ShopEntry_GuildId_Index",
table: "ShopEntry",
@ -2474,6 +2507,9 @@ namespace EllieBot.Migrations.Sqlite
migrationBuilder.DropTable(
name: "SarAutoDelete");
migrationBuilder.DropTable(
name: "ScheduledCommand");
migrationBuilder.DropTable(
name: "ShopEntryItem");
@ -2592,4 +2628,4 @@ namespace EllieBot.Migrations.Sqlite
name: "DiscordUser");
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,150 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Modules.Administration;
namespace EllieBot.Modules.Utility.Scheduled;
public sealed class ScheduleCommandService(
DbService db,
ICommandHandler cmdHandler,
DiscordSocketClient client,
ShardData shardData) : IEService, IReadyExecutor
{
private TaskCompletionSource _tcs = new();
public async Task OnReadyAsync()
{
while (true)
{
_tcs = new();
// get the next scheduled command
var scheduledCommand = await db.GetDbContext()
.GetTable<ScheduledCommand>()
.Where(x => Queries.GuildOnShard(x.GuildId, shardData.TotalShards, shardData.ShardId))
.OrderBy(x => x.When)
.FirstOrDefaultAsyncLinqToDB();
if (scheduledCommand is null)
{
await _tcs.Task;
continue;
}
var now = DateTime.UtcNow;
if (scheduledCommand.When > now)
{
var diff = scheduledCommand.When - now;
await Task.WhenAny(Task.Delay(diff), _tcs.Task);
continue;
}
await db.GetDbContext()
.GetTable<ScheduledCommand>()
.Where(x => x.Id == scheduledCommand.Id)
.DeleteAsync();
var guild = client.GetGuild(scheduledCommand.GuildId);
var channel = guild?.GetChannel(scheduledCommand.ChannelId) as ISocketMessageChannel;
if (guild is null || channel is null)
continue;
var message = await channel.GetMessageAsync(scheduledCommand.MessageId) as IUserMessage;
var user = await (guild as IGuild).GetUserAsync(scheduledCommand.UserId);
if (message is null || user is null)
continue;
_ = Task.Run(async ()
=> await cmdHandler.TryRunCommand(guild,
channel,
new DoAsUserMessage(message, user, scheduledCommand.Text)));
}
}
/// <summary>
/// Adds a scheduled command to be executed after the specified time
/// </summary>
/// <param name="guildId">ID of the guild</param>
/// <param name="channelId">ID of the channel where the command was issued</param>
/// <param name="messageId">ID of the message that triggered this command</param>
/// <param name="userId">ID of the user who scheduled the command</param>
/// <param name="commandText">The command text to execute</param>
/// <param name="when">Time span after which the command will be executed</param>
/// <returns>True if command was added, false if user reached the limit</returns>
public async Task<bool> AddScheduledCommandAsync(
ulong guildId,
ulong channelId,
ulong messageId,
ulong userId,
string commandText,
TimeSpan when)
{
ArgumentException.ThrowIfNullOrWhiteSpace(commandText, nameof(commandText));
await using var uow = db.GetDbContext();
var count = await uow.GetTable<ScheduledCommand>()
.Where(x => x.GuildId == guildId && x.UserId == userId)
.CountAsyncLinqToDB();
if (count >= 5)
return false;
await uow.GetTable<ScheduledCommand>()
.InsertAsync(() => new()
{
GuildId = guildId,
UserId = userId,
Text = commandText,
When = DateTime.UtcNow + when,
ChannelId = channelId,
MessageId = messageId
});
_tcs.TrySetResult();
return true;
}
/// <summary>
/// Gets all scheduled commands for a specific user in a guild
/// </summary>
/// <param name="guildId">Guild ID</param>
/// <param name="userId">User ID</param>
/// <returns>List of scheduled commands</returns>
public async Task<List<ScheduledCommand>> GetUserScheduledCommandsAsync(ulong guildId, ulong userId)
{
await using var uow = db.GetDbContext();
return await uow.GetTable<ScheduledCommand>()
.Where(x => x.GuildId == guildId && x.UserId == userId)
.OrderBy(x => x.When)
.AsNoTracking()
.ToListAsyncLinqToDB();
}
/// <summary>
/// Deletes a scheduled command by its ID
/// </summary>
/// <param name="id">ID of the scheduled command</param>
/// <param name="guildId">Guild ID</param>
/// <param name="userId">User ID</param>
/// <returns>True if command was deleted, false otherwise</returns>
public async Task<bool> DeleteScheduledCommandAsync(int id, ulong guildId, ulong userId)
{
await using var uow = db.GetDbContext();
var result = await uow.GetTable<ScheduledCommand>()
.Where(x => x.Id == id && x.GuildId == guildId && x.UserId == userId)
.DeleteAsync();
if (result > 0)
_tcs.TrySetResult();
return result > 0;
}
}

View file

@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace EllieBot.Modules.Utility.Scheduled;
public sealed class ScheduledCommand
{
[Key]
public int Id { get; set; }
public ulong UserId { get; set; }
public ulong ChannelId { get; set; }
public ulong GuildId { get; set; }
public ulong MessageId { get; set; }
public string Text { get; set; } = string.Empty;
public DateTime When { get; set; }
}
public sealed class ScheduledCommandEntityConfiguration : IEntityTypeConfiguration<ScheduledCommand>
{
public void Configure(EntityTypeBuilder<ScheduledCommand> builder)
{
builder.HasIndex(x => x.UserId);
builder.HasIndex(x => x.GuildId);
builder.HasIndex(x => x.When);
}
}

View file

@ -0,0 +1,89 @@
using EllieBot.Common.TypeReaders.Models;
namespace EllieBot.Modules.Utility.Scheduled;
public partial class Utility
{
[Group]
public class ScheduledCommands(ScheduleCommandService scs) : EllieModule
{
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task ScheduleList()
{
var scheduledCommands = await scs.GetUserScheduledCommandsAsync(ctx.Guild.Id, ctx.User.Id);
if (scheduledCommands.Count == 0)
{
await Response().Error(strs.schedule_list_none).SendAsync();
return;
}
await Response()
.Paginated()
.Items(scheduledCommands)
.PageSize(5)
.Page((pageCommands, _) =>
{
var eb = CreateEmbed()
.WithTitle(GetText(strs.schedule_list_title))
.WithAuthor(ctx.User)
.WithOkColor();
foreach (var cmd in pageCommands)
{
eb.AddField(
$"`{GetText(strs.schedule_id)}:` {cmd.Id}",
$"""
`{GetText(strs.schedule_command)}:` {cmd.Text}
`{GetText(strs.schedule_when)}:` {TimestampTag.FromDateTime(cmd.When, TimestampTagStyles.Relative)}
""");
}
return eb;
})
.SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task ScheduleDelete([Leftover] int id)
{
var success = await scs.DeleteScheduledCommandAsync(id, ctx.Guild.Id, ctx.User.Id);
if (success)
{
await Response().Confirm(strs.schedule_deleted(id)).SendAsync();
}
else
{
await Response().Error(strs.schedule_delete_error).SendAsync();
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task ScheduleAdd(ParsedTimespan timeString, [Leftover] string commandText)
{
if (timeString.Time < TimeSpan.FromMinutes(1))
return;
var success = await scs.AddScheduledCommandAsync(
ctx.Guild.Id,
ctx.Channel.Id,
ctx.Message.Id,
ctx.User.Id,
commandText,
timeString.Time);
if (success)
{
await Response().Confirm(strs.schedule_add_success).SendAsync();
}
else
{
await Response().Error(strs.schedule_add_error).SendAsync();
}
}
}
}

View file

@ -8,7 +8,7 @@ public sealed class DoAsUserMessage : IUserMessage
private IUserMessage _msg;
private readonly IUser _user;
public DoAsUserMessage(SocketUserMessage msg, IUser user, string message)
public DoAsUserMessage(IUserMessage msg, IUser user, string message)
{
_msg = msg;
_user = user;

View file

@ -1614,4 +1614,16 @@ userrolename:
- urn
userroleicon:
- userroleicon
- uri
- uri
schedulelist:
- schedulelist
- schl
- schli
scheduledelete:
- scheduledelete
- schd
- schdel
scheduleadd:
- scheduleadd
- scha
- schadd

View file

@ -5073,4 +5073,31 @@ userrolename:
- role:
desc: 'The assigned role to rename.'
name:
desc: 'The new name for the role.'
desc: 'The new name for the role.'
schedulelist:
desc: |-
Lists your scheduled commands in the current server.
ex:
- ''
params:
- { }
scheduledelete:
desc: |-
Deletes one of your scheduled commands by its ID.
ex:
- '5'
params:
- id:
desc: "The ID of the scheduled command to delete."
scheduleadd:
desc: |-
Schedules a command to be executed after the specified amount of time.
You can schedule up to 5 commands at a time.
ex:
- '1h5m .say Hello after 1 hour and 5 minutes'
- '3h .br all'
params:
- time:
desc: "How long it takes for the command to execute. Example: 1h30m = 1 hour and 30 minutes"
- command:
desc: "Command that will be executed after the specified time has elapsed"

View file

@ -1207,5 +1207,15 @@
"userrole_icon_invalid": "The role icon cannot be empty.",
"userrole_hierarchy_error": "You can't assign or modify roles that are higher than or equal to your, or bots highest role.",
"userrole_role_not_exists": "That role doesn't exist.",
"whos_playing_game": "{0} users are playing {1}"
}
"whos_playing_game": "{0} users are playing {1}",
"schedule_list_title": "Scheduled Commands",
"schedule_list_none": "You don't have any scheduled commands.",
"schedule_list_for_user": "Scheduled Commands",
"schedule_deleted": "Scheduled command #{0} successfully deleted.",
"schedule_delete_error": "Error deleting scheduled command.",
"schedule_id": "ID",
"schedule_command": "Command",
"schedule_when": "Executes At",
"schedule_add_success": "Scheduled command successfully added.",
"schedule_add_error": "You already have 5 scheduled commands. Please delete some before adding more."
}