Compare commits

..

52 Commits

Author SHA1 Message Date
13d332d306 [Activity] Fix command names 2023-04-13 21:43:31 +02:00
84cac63c9d [Activity] Fix embed field character limit 2023-04-13 21:24:16 +02:00
ad4ee02631 [Activity] Add database backup on migration 2023-04-10 12:38:11 +02:00
4b23dfd8a0 [Activity] Add has completed command 2023-04-10 12:26:17 +02:00
224fe4cb25 [Activity] Change event to minigame and add event 2023-04-10 11:56:49 +02:00
ca3e951d9a [Activity] Add stage modification 2023-04-10 11:55:52 +02:00
37a4da26d7 [Activity] Change permissions for activity delete 2023-03-30 17:48:55 +02:00
d000e6a299 [Activity] Fix embed symbols 2023-03-27 21:51:00 +02:00
08af0afbfb [Actitivy] Fix embed on mobile 2023-03-27 21:46:31 +02:00
b4e7e52827 [Activity] Add date for organized activities 2023-03-27 21:31:14 +02:00
7a76ca2147 [Activity] Update some messages 2023-03-27 01:34:57 +02:00
54b4e6fb3f [Activity] Add description change command in thread 2023-03-27 00:42:46 +02:00
78fe2d5913 [Activity] Add organized activities 2023-03-27 00:22:43 +02:00
a6e8ad0401 [CompositeRoles] Add fix command 2023-03-26 17:57:46 +02:00
a79ed18e26 [Activity] Add activity close and open commands 2023-03-26 15:34:53 +02:00
1083f604f7 [Activity] Add activity auto close 2023-03-26 14:56:54 +02:00
63e464e602 [Activity] Cleanup schema and DueDateTime 2023-03-26 10:33:03 +02:00
cd6a9e87ad [Activity] Add due time 2023-03-26 00:43:15 +01:00
6001a3dc24 [Database] Add migrations for production 2023-03-25 21:41:46 +01:00
cb0b993ceb [CompositeRoles] Directly deserialiaze to ulong 2023-03-25 10:57:57 +01:00
14f90a2d18 [CompositeRoles] Add listener and options 2023-03-25 09:25:20 +01:00
ced9d15083 [Activity] Add thread ping and refactor activity join/leave 2023-03-24 23:39:35 +01:00
a3051ed060 [Activity] Add thread add and remove player commands 2023-03-24 00:01:04 +01:00
bd52e37454 [Activity] Add thread creation and start message 2023-03-23 22:49:49 +01:00
79d9f16a8b [Activity] Add CDN refresh 2023-03-23 11:04:34 +01:00
b6897f7327 [Activity] Add interstellar exploration formatter 2023-03-23 10:16:25 +01:00
8db87b9098 [Activity] Add all activities 2023-03-22 22:59:37 +01:00
ef948dba27 [Activity] Update component style
- Add icons
- Disable button when activity is full
2023-03-22 12:26:21 +01:00
8dcefeeb39 [Activity] Cleanup embed and code 2023-03-22 11:33:34 +01:00
3f2dc16fad [Activit] Add banner and stage to embed 2023-03-22 10:12:04 +01:00
21996a2e03 [Activity] Add full check 2023-03-21 22:52:47 +01:00
bffa7d06bb [Activity] Add activity join/leave 2023-03-21 22:20:00 +01:00
094f07ea3c [Activity] Add persistence 2023-03-20 16:56:53 +01:00
d47442b984 [Activity] Add activity creation 2023-03-20 14:34:50 +01:00
e3df53738f [Groups] Add role setup 2023-03-20 10:23:06 +01:00
984f44585e [Raid] Fix dps substitute assignment 2023-01-10 16:39:54 +01:00
066ac52199 [Raid] Resolve conflicts based on role FC instead of total HP 2023-01-10 09:59:49 +01:00
b4d41a449f [Raid] Add max player count check 2023-01-10 09:47:39 +01:00
194feb94dd [Raid] Fix raid embed format 2022-12-07 23:49:58 +01:00
0535655f58 [Raid] Complete roster assigner 2022-12-07 23:26:19 +01:00
12507b0fed [Raid] Add preliminary roster assigner 2022-12-05 09:55:31 +01:00
e853583371 [Raid] Add test message commands 2022-11-28 23:32:18 +01:00
24fc0103ec [Raid] Reorder players in roster embed 2022-11-26 17:57:50 +01:00
044fae0f99 [Raid] Update role select with preferred role and emote 2022-11-26 16:43:49 +01:00
addaefdde7 [Raid] Refactor RosterManager into Raid 2022-11-26 13:02:44 +01:00
21803eb728 [Raid] Add FC and role prompt when joining a raid 2022-11-26 12:22:08 +01:00
e334e02768 [Raid] Add date parameter in start command 2022-11-24 10:28:42 +01:00
4f0ccb9464 [Raid] Move format code to another class 2022-11-23 22:36:56 +01:00
a6df178dff [Raid] Add Join and Leave buttons 2022-11-23 22:00:00 +01:00
ac9c3a405f [Raid] Add start command 2022-11-17 00:51:02 +01:00
5b4a15bc8f Add runtime, summon, modal and select to Ping module 2022-11-16 19:12:14 +01:00
978b73233b Migrate to .Net 7 2022-11-16 19:09:48 +01:00
77 changed files with 7779 additions and 204 deletions

2
.gitignore vendored
View File

@@ -684,5 +684,3 @@ fabric.properties
# Additional files built by Visual Studio
# End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,rider,csharp,dotnetcore
Cocotte/discord.json

View File

@@ -1,14 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-Cocotter-70E782F3-B3C1-4BA0-965D-D21E31F2F052</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.8.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
<PackageReference Include="AppAny.Quartz.EntityFrameworkCore.Migrations.SQLite" Version="0.4.0" />
<PackageReference Include="Discord.Net" Version="3.10.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Quartz" Version="3.6.2" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.6.2" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.6.2" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.6.2" />
</ItemGroup>
<ItemGroup>
<None Remove="cocotte.db" />
<Content Include="cocotte.db">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,159 @@
// <auto-generated />
using System;
using Cocotte.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Cocotte.Migrations
{
[DbContext(typeof(CocotteDbContext))]
[Migration("20230326083141_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.4");
modelBuilder.Entity("Cocotte.Modules.Activities.Models.Activity", b =>
{
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<bool>("AreRolesEnabled")
.HasColumnType("INTEGER");
b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");
b.Property<string>("CreatorDisplayName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<ulong>("CreatorUserId")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("DueDateTime")
.HasColumnType("TEXT");
b.Property<uint>("MaxPlayers")
.HasColumnType("INTEGER");
b.Property<int>("Name")
.HasColumnType("INTEGER");
b.Property<ulong>("ThreadId")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("GuildId", "ChannelId", "MessageId");
b.HasIndex("ThreadId");
b.ToTable("Activities");
b.HasDiscriminator<string>("Discriminator").HasValue("Activity");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityPlayer", b =>
{
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("GuildId", "ChannelId", "MessageId", "UserId");
b.ToTable("ActivityPlayers");
b.HasDiscriminator<string>("Discriminator").HasValue("ActivityPlayer");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.InterstellarActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.Property<int>("Color")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("InterstellarActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.StagedActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.Property<uint>("Stage")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("StagedActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityRolePlayer", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.ActivityPlayer");
b.Property<byte>("Roles")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("ActivityRolePlayer");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityPlayer", b =>
{
b.HasOne("Cocotte.Modules.Activities.Models.Activity", "Activity")
.WithMany("ActivityPlayers")
.HasForeignKey("GuildId", "ChannelId", "MessageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Activity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.Activity", b =>
{
b.Navigation("ActivityPlayers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Cocotte.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Activities",
columns: table => new
{
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
MessageId = table.Column<ulong>(type: "INTEGER", nullable: false),
ThreadId = table.Column<ulong>(type: "INTEGER", nullable: false),
CreatorUserId = table.Column<ulong>(type: "INTEGER", nullable: false),
CreatorDisplayName = table.Column<string>(type: "TEXT", nullable: false),
DueDateTime = table.Column<DateTime>(type: "TEXT", nullable: true),
Description = table.Column<string>(type: "TEXT", nullable: true),
Type = table.Column<int>(type: "INTEGER", nullable: false),
Name = table.Column<int>(type: "INTEGER", nullable: false),
AreRolesEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
MaxPlayers = table.Column<uint>(type: "INTEGER", nullable: false),
CreationDate = table.Column<DateTime>(type: "TEXT", nullable: false),
Discriminator = table.Column<string>(type: "TEXT", nullable: false),
Color = table.Column<int>(type: "INTEGER", nullable: true),
Stage = table.Column<uint>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Activities", x => new { x.GuildId, x.ChannelId, x.MessageId });
});
migrationBuilder.CreateTable(
name: "ActivityPlayers",
columns: table => new
{
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
MessageId = table.Column<ulong>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Discriminator = table.Column<string>(type: "TEXT", nullable: false),
Roles = table.Column<byte>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ActivityPlayers", x => new { x.GuildId, x.ChannelId, x.MessageId, x.UserId });
table.ForeignKey(
name: "FK_ActivityPlayers_Activities_GuildId_ChannelId_MessageId",
columns: x => new { x.GuildId, x.ChannelId, x.MessageId },
principalTable: "Activities",
principalColumns: new[] { "GuildId", "ChannelId", "MessageId" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Activities_ThreadId",
table: "Activities",
column: "ThreadId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ActivityPlayers");
migrationBuilder.DropTable(
name: "Activities");
}
}
}

View File

@@ -0,0 +1,673 @@
// <auto-generated />
using System;
using Cocotte.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Cocotte.Migrations
{
[DbContext(typeof(CocotteDbContext))]
[Migration("20230326095220_AddQuartz")]
partial class AddQuartz
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.4");
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<byte[]>("BlobData")
.HasColumnType("bytea")
.HasColumnName("BLOB_DATA");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_BLOB_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("CalendarName")
.HasColumnType("text")
.HasColumnName("CALENDAR_NAME");
b.Property<byte[]>("Calendar")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("CALENDAR");
b.HasKey("SchedulerName", "CalendarName");
b.ToTable("QRTZ_CALENDARS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("CronExpression")
.IsRequired()
.HasColumnType("text")
.HasColumnName("CRON_EXPRESSION");
b.Property<string>("TimeZoneId")
.HasColumnType("text")
.HasColumnName("TIME_ZONE_ID");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_CRON_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("EntryId")
.HasColumnType("text")
.HasColumnName("ENTRY_ID");
b.Property<long>("FiredTime")
.HasColumnType("bigint")
.HasColumnName("FIRED_TIME");
b.Property<string>("InstanceName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("INSTANCE_NAME");
b.Property<bool>("IsNonConcurrent")
.HasColumnType("bool")
.HasColumnName("IS_NONCONCURRENT");
b.Property<string>("JobGroup")
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("JobName")
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<int>("Priority")
.HasColumnType("integer")
.HasColumnName("PRIORITY");
b.Property<bool?>("RequestsRecovery")
.HasColumnType("bool")
.HasColumnName("REQUESTS_RECOVERY");
b.Property<long>("ScheduledTime")
.HasColumnType("bigint")
.HasColumnName("SCHED_TIME");
b.Property<string>("State")
.IsRequired()
.HasColumnType("text")
.HasColumnName("STATE");
b.Property<string>("TriggerGroup")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("TriggerName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.HasKey("SchedulerName", "EntryId");
b.HasIndex("InstanceName")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_INST_NAME");
b.HasIndex("JobGroup")
.HasDatabaseName("IDX_QRTZ_FT_JOB_GROUP");
b.HasIndex("JobName")
.HasDatabaseName("IDX_QRTZ_FT_JOB_NAME");
b.HasIndex("RequestsRecovery")
.HasDatabaseName("IDX_QRTZ_FT_JOB_REQ_RECOVERY");
b.HasIndex("TriggerGroup")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_GROUP");
b.HasIndex("TriggerName")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_NAME");
b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_NM_GP");
b.ToTable("QRTZ_FIRED_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("JobName")
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<string>("JobGroup")
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("DESCRIPTION");
b.Property<bool>("IsDurable")
.HasColumnType("bool")
.HasColumnName("IS_DURABLE");
b.Property<bool>("IsNonConcurrent")
.HasColumnType("bool")
.HasColumnName("IS_NONCONCURRENT");
b.Property<bool>("IsUpdateData")
.HasColumnType("bool")
.HasColumnName("IS_UPDATE_DATA");
b.Property<string>("JobClassName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_CLASS_NAME");
b.Property<byte[]>("JobData")
.HasColumnType("bytea")
.HasColumnName("JOB_DATA");
b.Property<bool>("RequestsRecovery")
.HasColumnType("bool")
.HasColumnName("REQUESTS_RECOVERY");
b.HasKey("SchedulerName", "JobName", "JobGroup");
b.HasIndex("RequestsRecovery")
.HasDatabaseName("IDX_QRTZ_J_REQ_RECOVERY");
b.ToTable("QRTZ_JOB_DETAILS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("LockName")
.HasColumnType("text")
.HasColumnName("LOCK_NAME");
b.HasKey("SchedulerName", "LockName");
b.ToTable("QRTZ_LOCKS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.HasKey("SchedulerName", "TriggerGroup");
b.ToTable("QRTZ_PAUSED_TRIGGER_GRPS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("InstanceName")
.HasColumnType("text")
.HasColumnName("INSTANCE_NAME");
b.Property<long>("CheckInInterval")
.HasColumnType("bigint")
.HasColumnName("CHECKIN_INTERVAL");
b.Property<long>("LastCheckInTime")
.HasColumnType("bigint")
.HasColumnName("LAST_CHECKIN_TIME");
b.HasKey("SchedulerName", "InstanceName");
b.ToTable("QRTZ_SCHEDULER_STATE", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<bool?>("BooleanProperty1")
.HasColumnType("bool")
.HasColumnName("BOOL_PROP_1");
b.Property<bool?>("BooleanProperty2")
.HasColumnType("bool")
.HasColumnName("BOOL_PROP_2");
b.Property<decimal?>("DecimalProperty1")
.HasColumnType("numeric")
.HasColumnName("DEC_PROP_1");
b.Property<decimal?>("DecimalProperty2")
.HasColumnType("numeric")
.HasColumnName("DEC_PROP_2");
b.Property<int?>("IntegerProperty1")
.HasColumnType("integer")
.HasColumnName("INT_PROP_1");
b.Property<int?>("IntegerProperty2")
.HasColumnType("integer")
.HasColumnName("INT_PROP_2");
b.Property<long?>("LongProperty1")
.HasColumnType("bigint")
.HasColumnName("LONG_PROP_1");
b.Property<long?>("LongProperty2")
.HasColumnType("bigint")
.HasColumnName("LONG_PROP_2");
b.Property<string>("StringProperty1")
.HasColumnType("text")
.HasColumnName("STR_PROP_1");
b.Property<string>("StringProperty2")
.HasColumnType("text")
.HasColumnName("STR_PROP_2");
b.Property<string>("StringProperty3")
.HasColumnType("text")
.HasColumnName("STR_PROP_3");
b.Property<string>("TimeZoneId")
.HasColumnType("text")
.HasColumnName("TIME_ZONE_ID");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_SIMPROP_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<long>("RepeatCount")
.HasColumnType("bigint")
.HasColumnName("REPEAT_COUNT");
b.Property<long>("RepeatInterval")
.HasColumnType("bigint")
.HasColumnName("REPEAT_INTERVAL");
b.Property<long>("TimesTriggered")
.HasColumnType("bigint")
.HasColumnName("TIMES_TRIGGERED");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_SIMPLE_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("CalendarName")
.HasColumnType("text")
.HasColumnName("CALENDAR_NAME");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("DESCRIPTION");
b.Property<long?>("EndTime")
.HasColumnType("bigint")
.HasColumnName("END_TIME");
b.Property<byte[]>("JobData")
.HasColumnType("bytea")
.HasColumnName("JOB_DATA");
b.Property<string>("JobGroup")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("JobName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<short?>("MisfireInstruction")
.HasColumnType("smallint")
.HasColumnName("MISFIRE_INSTR");
b.Property<long?>("NextFireTime")
.HasColumnType("bigint")
.HasColumnName("NEXT_FIRE_TIME");
b.Property<long?>("PreviousFireTime")
.HasColumnType("bigint")
.HasColumnName("PREV_FIRE_TIME");
b.Property<int?>("Priority")
.HasColumnType("integer")
.HasColumnName("PRIORITY");
b.Property<long>("StartTime")
.HasColumnType("bigint")
.HasColumnName("START_TIME");
b.Property<string>("TriggerState")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_STATE");
b.Property<string>("TriggerType")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_TYPE");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.HasIndex("NextFireTime")
.HasDatabaseName("IDX_QRTZ_T_NEXT_FIRE_TIME");
b.HasIndex("TriggerState")
.HasDatabaseName("IDX_QRTZ_T_STATE");
b.HasIndex("NextFireTime", "TriggerState")
.HasDatabaseName("IDX_QRTZ_T_NFT_ST");
b.HasIndex("SchedulerName", "JobName", "JobGroup");
b.ToTable("QRTZ_TRIGGERS", (string)null);
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.Activity", b =>
{
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<bool>("AreRolesEnabled")
.HasColumnType("INTEGER");
b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");
b.Property<string>("CreatorDisplayName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<ulong>("CreatorUserId")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("DueDateTime")
.HasColumnType("TEXT");
b.Property<uint>("MaxPlayers")
.HasColumnType("INTEGER");
b.Property<int>("Name")
.HasColumnType("INTEGER");
b.Property<ulong>("ThreadId")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("GuildId", "ChannelId", "MessageId");
b.HasIndex("ThreadId");
b.ToTable("Activities");
b.HasDiscriminator<string>("Discriminator").HasValue("Activity");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityPlayer", b =>
{
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("GuildId", "ChannelId", "MessageId", "UserId");
b.ToTable("ActivityPlayers");
b.HasDiscriminator<string>("Discriminator").HasValue("ActivityPlayer");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.InterstellarActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.Property<int>("Color")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("InterstellarActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.StagedActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.Property<uint>("Stage")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("StagedActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityRolePlayer", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.ActivityPlayer");
b.Property<byte>("Roles")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("ActivityRolePlayer");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("BlobTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("CronTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("SimplePropertyTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("SimpleTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail")
.WithMany("Triggers")
.HasForeignKey("SchedulerName", "JobName", "JobGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("JobDetail");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityPlayer", b =>
{
b.HasOne("Cocotte.Modules.Activities.Models.Activity", "Activity")
.WithMany("ActivityPlayers")
.HasForeignKey("GuildId", "ChannelId", "MessageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Activity");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b =>
{
b.Navigation("Triggers");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.Navigation("BlobTriggers");
b.Navigation("CronTriggers");
b.Navigation("SimplePropertyTriggers");
b.Navigation("SimpleTriggers");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.Activity", b =>
{
b.Navigation("ActivityPlayers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,331 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Cocotte.Migrations
{
/// <inheritdoc />
public partial class AddQuartz : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "QRTZ_CALENDARS",
columns: table => new
{
SCHED_NAME = table.Column<string>(type: "text", nullable: false),
CALENDAR_NAME = table.Column<string>(type: "text", nullable: false),
CALENDAR = table.Column<byte[]>(type: "bytea", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_QRTZ_CALENDARS", x => new { x.SCHED_NAME, x.CALENDAR_NAME });
});
migrationBuilder.CreateTable(
name: "QRTZ_FIRED_TRIGGERS",
columns: table => new
{
SCHED_NAME = table.Column<string>(type: "text", nullable: false),
ENTRY_ID = table.Column<string>(type: "text", nullable: false),
TRIGGER_NAME = table.Column<string>(type: "text", nullable: false),
TRIGGER_GROUP = table.Column<string>(type: "text", nullable: false),
INSTANCE_NAME = table.Column<string>(type: "text", nullable: false),
FIRED_TIME = table.Column<long>(type: "bigint", nullable: false),
SCHED_TIME = table.Column<long>(type: "bigint", nullable: false),
PRIORITY = table.Column<int>(type: "integer", nullable: false),
STATE = table.Column<string>(type: "text", nullable: false),
JOB_NAME = table.Column<string>(type: "text", nullable: true),
JOB_GROUP = table.Column<string>(type: "text", nullable: true),
IS_NONCONCURRENT = table.Column<bool>(type: "bool", nullable: false),
REQUESTS_RECOVERY = table.Column<bool>(type: "bool", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_QRTZ_FIRED_TRIGGERS", x => new { x.SCHED_NAME, x.ENTRY_ID });
});
migrationBuilder.CreateTable(
name: "QRTZ_JOB_DETAILS",
columns: table => new
{
SCHED_NAME = table.Column<string>(type: "text", nullable: false),
JOB_NAME = table.Column<string>(type: "text", nullable: false),
JOB_GROUP = table.Column<string>(type: "text", nullable: false),
DESCRIPTION = table.Column<string>(type: "text", nullable: true),
JOB_CLASS_NAME = table.Column<string>(type: "text", nullable: false),
IS_DURABLE = table.Column<bool>(type: "bool", nullable: false),
IS_NONCONCURRENT = table.Column<bool>(type: "bool", nullable: false),
IS_UPDATE_DATA = table.Column<bool>(type: "bool", nullable: false),
REQUESTS_RECOVERY = table.Column<bool>(type: "bool", nullable: false),
JOB_DATA = table.Column<byte[]>(type: "bytea", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_QRTZ_JOB_DETAILS", x => new { x.SCHED_NAME, x.JOB_NAME, x.JOB_GROUP });
});
migrationBuilder.CreateTable(
name: "QRTZ_LOCKS",
columns: table => new
{
SCHED_NAME = table.Column<string>(type: "text", nullable: false),
LOCK_NAME = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_QRTZ_LOCKS", x => new { x.SCHED_NAME, x.LOCK_NAME });
});
migrationBuilder.CreateTable(
name: "QRTZ_PAUSED_TRIGGER_GRPS",
columns: table => new
{
SCHED_NAME = table.Column<string>(type: "text", nullable: false),
TRIGGER_GROUP = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_QRTZ_PAUSED_TRIGGER_GRPS", x => new { x.SCHED_NAME, x.TRIGGER_GROUP });
});
migrationBuilder.CreateTable(
name: "QRTZ_SCHEDULER_STATE",
columns: table => new
{
SCHED_NAME = table.Column<string>(type: "text", nullable: false),
INSTANCE_NAME = table.Column<string>(type: "text", nullable: false),
LAST_CHECKIN_TIME = table.Column<long>(type: "bigint", nullable: false),
CHECKIN_INTERVAL = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_QRTZ_SCHEDULER_STATE", x => new { x.SCHED_NAME, x.INSTANCE_NAME });
});
migrationBuilder.CreateTable(
name: "QRTZ_TRIGGERS",
columns: table => new
{
SCHED_NAME = table.Column<string>(type: "text", nullable: false),
TRIGGER_NAME = table.Column<string>(type: "text", nullable: false),
TRIGGER_GROUP = table.Column<string>(type: "text", nullable: false),
JOB_NAME = table.Column<string>(type: "text", nullable: false),
JOB_GROUP = table.Column<string>(type: "text", nullable: false),
DESCRIPTION = table.Column<string>(type: "text", nullable: true),
NEXT_FIRE_TIME = table.Column<long>(type: "bigint", nullable: true),
PREV_FIRE_TIME = table.Column<long>(type: "bigint", nullable: true),
PRIORITY = table.Column<int>(type: "integer", nullable: true),
TRIGGER_STATE = table.Column<string>(type: "text", nullable: false),
TRIGGER_TYPE = table.Column<string>(type: "text", nullable: false),
START_TIME = table.Column<long>(type: "bigint", nullable: false),
END_TIME = table.Column<long>(type: "bigint", nullable: true),
CALENDAR_NAME = table.Column<string>(type: "text", nullable: true),
MISFIRE_INSTR = table.Column<short>(type: "smallint", nullable: true),
JOB_DATA = table.Column<byte[]>(type: "bytea", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_QRTZ_TRIGGERS", x => new { x.SCHED_NAME, x.TRIGGER_NAME, x.TRIGGER_GROUP });
table.ForeignKey(
name: "FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS_SCHED_NAME_JOB_NAME_JOB_GROUP",
columns: x => new { x.SCHED_NAME, x.JOB_NAME, x.JOB_GROUP },
principalTable: "QRTZ_JOB_DETAILS",
principalColumns: new[] { "SCHED_NAME", "JOB_NAME", "JOB_GROUP" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "QRTZ_BLOB_TRIGGERS",
columns: table => new
{
SCHED_NAME = table.Column<string>(type: "text", nullable: false),
TRIGGER_NAME = table.Column<string>(type: "text", nullable: false),
TRIGGER_GROUP = table.Column<string>(type: "text", nullable: false),
BLOB_DATA = table.Column<byte[]>(type: "bytea", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_QRTZ_BLOB_TRIGGERS", x => new { x.SCHED_NAME, x.TRIGGER_NAME, x.TRIGGER_GROUP });
table.ForeignKey(
name: "FK_QRTZ_BLOB_TRIGGERS_QRTZ_TRIGGERS_SCHED_NAME_TRIGGER_NAME_TRIGGER_GROUP",
columns: x => new { x.SCHED_NAME, x.TRIGGER_NAME, x.TRIGGER_GROUP },
principalTable: "QRTZ_TRIGGERS",
principalColumns: new[] { "SCHED_NAME", "TRIGGER_NAME", "TRIGGER_GROUP" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "QRTZ_CRON_TRIGGERS",
columns: table => new
{
SCHED_NAME = table.Column<string>(type: "text", nullable: false),
TRIGGER_NAME = table.Column<string>(type: "text", nullable: false),
TRIGGER_GROUP = table.Column<string>(type: "text", nullable: false),
CRON_EXPRESSION = table.Column<string>(type: "text", nullable: false),
TIME_ZONE_ID = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_QRTZ_CRON_TRIGGERS", x => new { x.SCHED_NAME, x.TRIGGER_NAME, x.TRIGGER_GROUP });
table.ForeignKey(
name: "FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS_SCHED_NAME_TRIGGER_NAME_TRIGGER_GROUP",
columns: x => new { x.SCHED_NAME, x.TRIGGER_NAME, x.TRIGGER_GROUP },
principalTable: "QRTZ_TRIGGERS",
principalColumns: new[] { "SCHED_NAME", "TRIGGER_NAME", "TRIGGER_GROUP" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "QRTZ_SIMPLE_TRIGGERS",
columns: table => new
{
SCHED_NAME = table.Column<string>(type: "text", nullable: false),
TRIGGER_NAME = table.Column<string>(type: "text", nullable: false),
TRIGGER_GROUP = table.Column<string>(type: "text", nullable: false),
REPEAT_COUNT = table.Column<long>(type: "bigint", nullable: false),
REPEAT_INTERVAL = table.Column<long>(type: "bigint", nullable: false),
TIMES_TRIGGERED = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_QRTZ_SIMPLE_TRIGGERS", x => new { x.SCHED_NAME, x.TRIGGER_NAME, x.TRIGGER_GROUP });
table.ForeignKey(
name: "FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS_SCHED_NAME_TRIGGER_NAME_TRIGGER_GROUP",
columns: x => new { x.SCHED_NAME, x.TRIGGER_NAME, x.TRIGGER_GROUP },
principalTable: "QRTZ_TRIGGERS",
principalColumns: new[] { "SCHED_NAME", "TRIGGER_NAME", "TRIGGER_GROUP" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "QRTZ_SIMPROP_TRIGGERS",
columns: table => new
{
SCHED_NAME = table.Column<string>(type: "text", nullable: false),
TRIGGER_NAME = table.Column<string>(type: "text", nullable: false),
TRIGGER_GROUP = table.Column<string>(type: "text", nullable: false),
STR_PROP_1 = table.Column<string>(type: "text", nullable: true),
STR_PROP_2 = table.Column<string>(type: "text", nullable: true),
STR_PROP_3 = table.Column<string>(type: "text", nullable: true),
INT_PROP_1 = table.Column<int>(type: "integer", nullable: true),
INT_PROP_2 = table.Column<int>(type: "integer", nullable: true),
LONG_PROP_1 = table.Column<long>(type: "bigint", nullable: true),
LONG_PROP_2 = table.Column<long>(type: "bigint", nullable: true),
DEC_PROP_1 = table.Column<decimal>(type: "numeric", nullable: true),
DEC_PROP_2 = table.Column<decimal>(type: "numeric", nullable: true),
BOOL_PROP_1 = table.Column<bool>(type: "bool", nullable: true),
BOOL_PROP_2 = table.Column<bool>(type: "bool", nullable: true),
TIME_ZONE_ID = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_QRTZ_SIMPROP_TRIGGERS", x => new { x.SCHED_NAME, x.TRIGGER_NAME, x.TRIGGER_GROUP });
table.ForeignKey(
name: "FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS_SCHED_NAME_TRIGGER_NAME_TRIGGER_GROUP",
columns: x => new { x.SCHED_NAME, x.TRIGGER_NAME, x.TRIGGER_GROUP },
principalTable: "QRTZ_TRIGGERS",
principalColumns: new[] { "SCHED_NAME", "TRIGGER_NAME", "TRIGGER_GROUP" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IDX_QRTZ_FT_JOB_GROUP",
table: "QRTZ_FIRED_TRIGGERS",
column: "JOB_GROUP");
migrationBuilder.CreateIndex(
name: "IDX_QRTZ_FT_JOB_NAME",
table: "QRTZ_FIRED_TRIGGERS",
column: "JOB_NAME");
migrationBuilder.CreateIndex(
name: "IDX_QRTZ_FT_JOB_REQ_RECOVERY",
table: "QRTZ_FIRED_TRIGGERS",
column: "REQUESTS_RECOVERY");
migrationBuilder.CreateIndex(
name: "IDX_QRTZ_FT_TRIG_GROUP",
table: "QRTZ_FIRED_TRIGGERS",
column: "TRIGGER_GROUP");
migrationBuilder.CreateIndex(
name: "IDX_QRTZ_FT_TRIG_INST_NAME",
table: "QRTZ_FIRED_TRIGGERS",
column: "INSTANCE_NAME");
migrationBuilder.CreateIndex(
name: "IDX_QRTZ_FT_TRIG_NAME",
table: "QRTZ_FIRED_TRIGGERS",
column: "TRIGGER_NAME");
migrationBuilder.CreateIndex(
name: "IDX_QRTZ_FT_TRIG_NM_GP",
table: "QRTZ_FIRED_TRIGGERS",
columns: new[] { "SCHED_NAME", "TRIGGER_NAME", "TRIGGER_GROUP" });
migrationBuilder.CreateIndex(
name: "IDX_QRTZ_J_REQ_RECOVERY",
table: "QRTZ_JOB_DETAILS",
column: "REQUESTS_RECOVERY");
migrationBuilder.CreateIndex(
name: "IDX_QRTZ_T_NEXT_FIRE_TIME",
table: "QRTZ_TRIGGERS",
column: "NEXT_FIRE_TIME");
migrationBuilder.CreateIndex(
name: "IDX_QRTZ_T_NFT_ST",
table: "QRTZ_TRIGGERS",
columns: new[] { "NEXT_FIRE_TIME", "TRIGGER_STATE" });
migrationBuilder.CreateIndex(
name: "IDX_QRTZ_T_STATE",
table: "QRTZ_TRIGGERS",
column: "TRIGGER_STATE");
migrationBuilder.CreateIndex(
name: "IX_QRTZ_TRIGGERS_SCHED_NAME_JOB_NAME_JOB_GROUP",
table: "QRTZ_TRIGGERS",
columns: new[] { "SCHED_NAME", "JOB_NAME", "JOB_GROUP" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "QRTZ_BLOB_TRIGGERS");
migrationBuilder.DropTable(
name: "QRTZ_CALENDARS");
migrationBuilder.DropTable(
name: "QRTZ_CRON_TRIGGERS");
migrationBuilder.DropTable(
name: "QRTZ_FIRED_TRIGGERS");
migrationBuilder.DropTable(
name: "QRTZ_LOCKS");
migrationBuilder.DropTable(
name: "QRTZ_PAUSED_TRIGGER_GRPS");
migrationBuilder.DropTable(
name: "QRTZ_SCHEDULER_STATE");
migrationBuilder.DropTable(
name: "QRTZ_SIMPLE_TRIGGERS");
migrationBuilder.DropTable(
name: "QRTZ_SIMPROP_TRIGGERS");
migrationBuilder.DropTable(
name: "QRTZ_TRIGGERS");
migrationBuilder.DropTable(
name: "QRTZ_JOB_DETAILS");
}
}
}

View File

@@ -0,0 +1,676 @@
// <auto-generated />
using System;
using Cocotte.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Cocotte.Migrations
{
[DbContext(typeof(CocotteDbContext))]
[Migration("20230326133102_AddActivityIsClosed")]
partial class AddActivityIsClosed
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.4");
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<byte[]>("BlobData")
.HasColumnType("bytea")
.HasColumnName("BLOB_DATA");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_BLOB_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("CalendarName")
.HasColumnType("text")
.HasColumnName("CALENDAR_NAME");
b.Property<byte[]>("Calendar")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("CALENDAR");
b.HasKey("SchedulerName", "CalendarName");
b.ToTable("QRTZ_CALENDARS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("CronExpression")
.IsRequired()
.HasColumnType("text")
.HasColumnName("CRON_EXPRESSION");
b.Property<string>("TimeZoneId")
.HasColumnType("text")
.HasColumnName("TIME_ZONE_ID");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_CRON_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("EntryId")
.HasColumnType("text")
.HasColumnName("ENTRY_ID");
b.Property<long>("FiredTime")
.HasColumnType("bigint")
.HasColumnName("FIRED_TIME");
b.Property<string>("InstanceName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("INSTANCE_NAME");
b.Property<bool>("IsNonConcurrent")
.HasColumnType("bool")
.HasColumnName("IS_NONCONCURRENT");
b.Property<string>("JobGroup")
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("JobName")
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<int>("Priority")
.HasColumnType("integer")
.HasColumnName("PRIORITY");
b.Property<bool?>("RequestsRecovery")
.HasColumnType("bool")
.HasColumnName("REQUESTS_RECOVERY");
b.Property<long>("ScheduledTime")
.HasColumnType("bigint")
.HasColumnName("SCHED_TIME");
b.Property<string>("State")
.IsRequired()
.HasColumnType("text")
.HasColumnName("STATE");
b.Property<string>("TriggerGroup")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("TriggerName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.HasKey("SchedulerName", "EntryId");
b.HasIndex("InstanceName")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_INST_NAME");
b.HasIndex("JobGroup")
.HasDatabaseName("IDX_QRTZ_FT_JOB_GROUP");
b.HasIndex("JobName")
.HasDatabaseName("IDX_QRTZ_FT_JOB_NAME");
b.HasIndex("RequestsRecovery")
.HasDatabaseName("IDX_QRTZ_FT_JOB_REQ_RECOVERY");
b.HasIndex("TriggerGroup")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_GROUP");
b.HasIndex("TriggerName")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_NAME");
b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_NM_GP");
b.ToTable("QRTZ_FIRED_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("JobName")
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<string>("JobGroup")
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("DESCRIPTION");
b.Property<bool>("IsDurable")
.HasColumnType("bool")
.HasColumnName("IS_DURABLE");
b.Property<bool>("IsNonConcurrent")
.HasColumnType("bool")
.HasColumnName("IS_NONCONCURRENT");
b.Property<bool>("IsUpdateData")
.HasColumnType("bool")
.HasColumnName("IS_UPDATE_DATA");
b.Property<string>("JobClassName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_CLASS_NAME");
b.Property<byte[]>("JobData")
.HasColumnType("bytea")
.HasColumnName("JOB_DATA");
b.Property<bool>("RequestsRecovery")
.HasColumnType("bool")
.HasColumnName("REQUESTS_RECOVERY");
b.HasKey("SchedulerName", "JobName", "JobGroup");
b.HasIndex("RequestsRecovery")
.HasDatabaseName("IDX_QRTZ_J_REQ_RECOVERY");
b.ToTable("QRTZ_JOB_DETAILS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("LockName")
.HasColumnType("text")
.HasColumnName("LOCK_NAME");
b.HasKey("SchedulerName", "LockName");
b.ToTable("QRTZ_LOCKS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.HasKey("SchedulerName", "TriggerGroup");
b.ToTable("QRTZ_PAUSED_TRIGGER_GRPS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("InstanceName")
.HasColumnType("text")
.HasColumnName("INSTANCE_NAME");
b.Property<long>("CheckInInterval")
.HasColumnType("bigint")
.HasColumnName("CHECKIN_INTERVAL");
b.Property<long>("LastCheckInTime")
.HasColumnType("bigint")
.HasColumnName("LAST_CHECKIN_TIME");
b.HasKey("SchedulerName", "InstanceName");
b.ToTable("QRTZ_SCHEDULER_STATE", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<bool?>("BooleanProperty1")
.HasColumnType("bool")
.HasColumnName("BOOL_PROP_1");
b.Property<bool?>("BooleanProperty2")
.HasColumnType("bool")
.HasColumnName("BOOL_PROP_2");
b.Property<decimal?>("DecimalProperty1")
.HasColumnType("numeric")
.HasColumnName("DEC_PROP_1");
b.Property<decimal?>("DecimalProperty2")
.HasColumnType("numeric")
.HasColumnName("DEC_PROP_2");
b.Property<int?>("IntegerProperty1")
.HasColumnType("integer")
.HasColumnName("INT_PROP_1");
b.Property<int?>("IntegerProperty2")
.HasColumnType("integer")
.HasColumnName("INT_PROP_2");
b.Property<long?>("LongProperty1")
.HasColumnType("bigint")
.HasColumnName("LONG_PROP_1");
b.Property<long?>("LongProperty2")
.HasColumnType("bigint")
.HasColumnName("LONG_PROP_2");
b.Property<string>("StringProperty1")
.HasColumnType("text")
.HasColumnName("STR_PROP_1");
b.Property<string>("StringProperty2")
.HasColumnType("text")
.HasColumnName("STR_PROP_2");
b.Property<string>("StringProperty3")
.HasColumnType("text")
.HasColumnName("STR_PROP_3");
b.Property<string>("TimeZoneId")
.HasColumnType("text")
.HasColumnName("TIME_ZONE_ID");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_SIMPROP_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<long>("RepeatCount")
.HasColumnType("bigint")
.HasColumnName("REPEAT_COUNT");
b.Property<long>("RepeatInterval")
.HasColumnType("bigint")
.HasColumnName("REPEAT_INTERVAL");
b.Property<long>("TimesTriggered")
.HasColumnType("bigint")
.HasColumnName("TIMES_TRIGGERED");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_SIMPLE_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("CalendarName")
.HasColumnType("text")
.HasColumnName("CALENDAR_NAME");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("DESCRIPTION");
b.Property<long?>("EndTime")
.HasColumnType("bigint")
.HasColumnName("END_TIME");
b.Property<byte[]>("JobData")
.HasColumnType("bytea")
.HasColumnName("JOB_DATA");
b.Property<string>("JobGroup")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("JobName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<short?>("MisfireInstruction")
.HasColumnType("smallint")
.HasColumnName("MISFIRE_INSTR");
b.Property<long?>("NextFireTime")
.HasColumnType("bigint")
.HasColumnName("NEXT_FIRE_TIME");
b.Property<long?>("PreviousFireTime")
.HasColumnType("bigint")
.HasColumnName("PREV_FIRE_TIME");
b.Property<int?>("Priority")
.HasColumnType("integer")
.HasColumnName("PRIORITY");
b.Property<long>("StartTime")
.HasColumnType("bigint")
.HasColumnName("START_TIME");
b.Property<string>("TriggerState")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_STATE");
b.Property<string>("TriggerType")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_TYPE");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.HasIndex("NextFireTime")
.HasDatabaseName("IDX_QRTZ_T_NEXT_FIRE_TIME");
b.HasIndex("TriggerState")
.HasDatabaseName("IDX_QRTZ_T_STATE");
b.HasIndex("NextFireTime", "TriggerState")
.HasDatabaseName("IDX_QRTZ_T_NFT_ST");
b.HasIndex("SchedulerName", "JobName", "JobGroup");
b.ToTable("QRTZ_TRIGGERS", (string)null);
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.Activity", b =>
{
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<bool>("AreRolesEnabled")
.HasColumnType("INTEGER");
b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");
b.Property<string>("CreatorDisplayName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<ulong>("CreatorUserId")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("DueDateTime")
.HasColumnType("TEXT");
b.Property<bool>("IsClosed")
.HasColumnType("INTEGER");
b.Property<uint>("MaxPlayers")
.HasColumnType("INTEGER");
b.Property<int>("Name")
.HasColumnType("INTEGER");
b.Property<ulong>("ThreadId")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("GuildId", "ChannelId", "MessageId");
b.HasIndex("ThreadId");
b.ToTable("Activities");
b.HasDiscriminator<string>("Discriminator").HasValue("Activity");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityPlayer", b =>
{
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("GuildId", "ChannelId", "MessageId", "UserId");
b.ToTable("ActivityPlayers");
b.HasDiscriminator<string>("Discriminator").HasValue("ActivityPlayer");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.InterstellarActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.Property<int>("Color")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("InterstellarActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.StagedActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.Property<uint>("Stage")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("StagedActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityRolePlayer", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.ActivityPlayer");
b.Property<byte>("Roles")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("ActivityRolePlayer");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("BlobTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("CronTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("SimplePropertyTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("SimpleTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail")
.WithMany("Triggers")
.HasForeignKey("SchedulerName", "JobName", "JobGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("JobDetail");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityPlayer", b =>
{
b.HasOne("Cocotte.Modules.Activities.Models.Activity", "Activity")
.WithMany("ActivityPlayers")
.HasForeignKey("GuildId", "ChannelId", "MessageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Activity");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b =>
{
b.Navigation("Triggers");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.Navigation("BlobTriggers");
b.Navigation("CronTriggers");
b.Navigation("SimplePropertyTriggers");
b.Navigation("SimpleTriggers");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.Activity", b =>
{
b.Navigation("ActivityPlayers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Cocotte.Migrations
{
/// <inheritdoc />
public partial class AddActivityIsClosed : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsClosed",
table: "Activities",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsClosed",
table: "Activities");
}
}
}

View File

@@ -0,0 +1,686 @@
// <auto-generated />
using System;
using Cocotte.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Cocotte.Migrations
{
[DbContext(typeof(CocotteDbContext))]
[Migration("20230326160040_AddOrganizedActivities")]
partial class AddOrganizedActivities
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.4");
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<byte[]>("BlobData")
.HasColumnType("bytea")
.HasColumnName("BLOB_DATA");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_BLOB_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("CalendarName")
.HasColumnType("text")
.HasColumnName("CALENDAR_NAME");
b.Property<byte[]>("Calendar")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("CALENDAR");
b.HasKey("SchedulerName", "CalendarName");
b.ToTable("QRTZ_CALENDARS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("CronExpression")
.IsRequired()
.HasColumnType("text")
.HasColumnName("CRON_EXPRESSION");
b.Property<string>("TimeZoneId")
.HasColumnType("text")
.HasColumnName("TIME_ZONE_ID");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_CRON_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("EntryId")
.HasColumnType("text")
.HasColumnName("ENTRY_ID");
b.Property<long>("FiredTime")
.HasColumnType("bigint")
.HasColumnName("FIRED_TIME");
b.Property<string>("InstanceName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("INSTANCE_NAME");
b.Property<bool>("IsNonConcurrent")
.HasColumnType("bool")
.HasColumnName("IS_NONCONCURRENT");
b.Property<string>("JobGroup")
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("JobName")
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<int>("Priority")
.HasColumnType("integer")
.HasColumnName("PRIORITY");
b.Property<bool?>("RequestsRecovery")
.HasColumnType("bool")
.HasColumnName("REQUESTS_RECOVERY");
b.Property<long>("ScheduledTime")
.HasColumnType("bigint")
.HasColumnName("SCHED_TIME");
b.Property<string>("State")
.IsRequired()
.HasColumnType("text")
.HasColumnName("STATE");
b.Property<string>("TriggerGroup")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("TriggerName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.HasKey("SchedulerName", "EntryId");
b.HasIndex("InstanceName")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_INST_NAME");
b.HasIndex("JobGroup")
.HasDatabaseName("IDX_QRTZ_FT_JOB_GROUP");
b.HasIndex("JobName")
.HasDatabaseName("IDX_QRTZ_FT_JOB_NAME");
b.HasIndex("RequestsRecovery")
.HasDatabaseName("IDX_QRTZ_FT_JOB_REQ_RECOVERY");
b.HasIndex("TriggerGroup")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_GROUP");
b.HasIndex("TriggerName")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_NAME");
b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_NM_GP");
b.ToTable("QRTZ_FIRED_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("JobName")
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<string>("JobGroup")
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("DESCRIPTION");
b.Property<bool>("IsDurable")
.HasColumnType("bool")
.HasColumnName("IS_DURABLE");
b.Property<bool>("IsNonConcurrent")
.HasColumnType("bool")
.HasColumnName("IS_NONCONCURRENT");
b.Property<bool>("IsUpdateData")
.HasColumnType("bool")
.HasColumnName("IS_UPDATE_DATA");
b.Property<string>("JobClassName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_CLASS_NAME");
b.Property<byte[]>("JobData")
.HasColumnType("bytea")
.HasColumnName("JOB_DATA");
b.Property<bool>("RequestsRecovery")
.HasColumnType("bool")
.HasColumnName("REQUESTS_RECOVERY");
b.HasKey("SchedulerName", "JobName", "JobGroup");
b.HasIndex("RequestsRecovery")
.HasDatabaseName("IDX_QRTZ_J_REQ_RECOVERY");
b.ToTable("QRTZ_JOB_DETAILS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("LockName")
.HasColumnType("text")
.HasColumnName("LOCK_NAME");
b.HasKey("SchedulerName", "LockName");
b.ToTable("QRTZ_LOCKS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.HasKey("SchedulerName", "TriggerGroup");
b.ToTable("QRTZ_PAUSED_TRIGGER_GRPS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("InstanceName")
.HasColumnType("text")
.HasColumnName("INSTANCE_NAME");
b.Property<long>("CheckInInterval")
.HasColumnType("bigint")
.HasColumnName("CHECKIN_INTERVAL");
b.Property<long>("LastCheckInTime")
.HasColumnType("bigint")
.HasColumnName("LAST_CHECKIN_TIME");
b.HasKey("SchedulerName", "InstanceName");
b.ToTable("QRTZ_SCHEDULER_STATE", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<bool?>("BooleanProperty1")
.HasColumnType("bool")
.HasColumnName("BOOL_PROP_1");
b.Property<bool?>("BooleanProperty2")
.HasColumnType("bool")
.HasColumnName("BOOL_PROP_2");
b.Property<decimal?>("DecimalProperty1")
.HasColumnType("numeric")
.HasColumnName("DEC_PROP_1");
b.Property<decimal?>("DecimalProperty2")
.HasColumnType("numeric")
.HasColumnName("DEC_PROP_2");
b.Property<int?>("IntegerProperty1")
.HasColumnType("integer")
.HasColumnName("INT_PROP_1");
b.Property<int?>("IntegerProperty2")
.HasColumnType("integer")
.HasColumnName("INT_PROP_2");
b.Property<long?>("LongProperty1")
.HasColumnType("bigint")
.HasColumnName("LONG_PROP_1");
b.Property<long?>("LongProperty2")
.HasColumnType("bigint")
.HasColumnName("LONG_PROP_2");
b.Property<string>("StringProperty1")
.HasColumnType("text")
.HasColumnName("STR_PROP_1");
b.Property<string>("StringProperty2")
.HasColumnType("text")
.HasColumnName("STR_PROP_2");
b.Property<string>("StringProperty3")
.HasColumnType("text")
.HasColumnName("STR_PROP_3");
b.Property<string>("TimeZoneId")
.HasColumnType("text")
.HasColumnName("TIME_ZONE_ID");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_SIMPROP_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<long>("RepeatCount")
.HasColumnType("bigint")
.HasColumnName("REPEAT_COUNT");
b.Property<long>("RepeatInterval")
.HasColumnType("bigint")
.HasColumnName("REPEAT_INTERVAL");
b.Property<long>("TimesTriggered")
.HasColumnType("bigint")
.HasColumnName("TIMES_TRIGGERED");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_SIMPLE_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("CalendarName")
.HasColumnType("text")
.HasColumnName("CALENDAR_NAME");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("DESCRIPTION");
b.Property<long?>("EndTime")
.HasColumnType("bigint")
.HasColumnName("END_TIME");
b.Property<byte[]>("JobData")
.HasColumnType("bytea")
.HasColumnName("JOB_DATA");
b.Property<string>("JobGroup")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("JobName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<short?>("MisfireInstruction")
.HasColumnType("smallint")
.HasColumnName("MISFIRE_INSTR");
b.Property<long?>("NextFireTime")
.HasColumnType("bigint")
.HasColumnName("NEXT_FIRE_TIME");
b.Property<long?>("PreviousFireTime")
.HasColumnType("bigint")
.HasColumnName("PREV_FIRE_TIME");
b.Property<int?>("Priority")
.HasColumnType("integer")
.HasColumnName("PRIORITY");
b.Property<long>("StartTime")
.HasColumnType("bigint")
.HasColumnName("START_TIME");
b.Property<string>("TriggerState")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_STATE");
b.Property<string>("TriggerType")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_TYPE");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.HasIndex("NextFireTime")
.HasDatabaseName("IDX_QRTZ_T_NEXT_FIRE_TIME");
b.HasIndex("TriggerState")
.HasDatabaseName("IDX_QRTZ_T_STATE");
b.HasIndex("NextFireTime", "TriggerState")
.HasDatabaseName("IDX_QRTZ_T_NFT_ST");
b.HasIndex("SchedulerName", "JobName", "JobGroup");
b.ToTable("QRTZ_TRIGGERS", (string)null);
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.Activity", b =>
{
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<bool>("AreRolesEnabled")
.HasColumnType("INTEGER");
b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");
b.Property<string>("CreatorDisplayName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<ulong>("CreatorUserId")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("DueDateTime")
.HasColumnType("TEXT");
b.Property<bool>("IsClosed")
.HasColumnType("INTEGER");
b.Property<uint>("MaxPlayers")
.HasColumnType("INTEGER");
b.Property<int>("Name")
.HasColumnType("INTEGER");
b.Property<ulong>("ThreadId")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("GuildId", "ChannelId", "MessageId");
b.HasIndex("ThreadId");
b.ToTable("Activities");
b.HasDiscriminator<string>("Discriminator").HasValue("Activity");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityPlayer", b =>
{
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("IsOrganizer")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("GuildId", "ChannelId", "MessageId", "UserId");
b.ToTable("ActivityPlayers");
b.HasDiscriminator<string>("Discriminator").HasValue("ActivityPlayer");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.InterstellarActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.Property<int>("Color")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("InterstellarActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.OrganizedActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.HasDiscriminator().HasValue("OrganizedActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.StagedActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.Property<uint>("Stage")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("StagedActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityRolePlayer", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.ActivityPlayer");
b.Property<byte>("Roles")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("ActivityRolePlayer");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("BlobTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("CronTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("SimplePropertyTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("SimpleTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail")
.WithMany("Triggers")
.HasForeignKey("SchedulerName", "JobName", "JobGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("JobDetail");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityPlayer", b =>
{
b.HasOne("Cocotte.Modules.Activities.Models.Activity", "Activity")
.WithMany("ActivityPlayers")
.HasForeignKey("GuildId", "ChannelId", "MessageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Activity");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b =>
{
b.Navigation("Triggers");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.Navigation("BlobTriggers");
b.Navigation("CronTriggers");
b.Navigation("SimplePropertyTriggers");
b.Navigation("SimpleTriggers");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.Activity", b =>
{
b.Navigation("ActivityPlayers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Cocotte.Migrations
{
/// <inheritdoc />
public partial class AddOrganizedActivities : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsOrganizer",
table: "ActivityPlayers",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsOrganizer",
table: "ActivityPlayers");
}
}
}

View File

@@ -0,0 +1,689 @@
// <auto-generated />
using System;
using Cocotte.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Cocotte.Migrations
{
[DbContext(typeof(CocotteDbContext))]
[Migration("20230410102447_AddActivityPlayerHasCompleted")]
partial class AddActivityPlayerHasCompleted
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.4");
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<byte[]>("BlobData")
.HasColumnType("bytea")
.HasColumnName("BLOB_DATA");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_BLOB_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("CalendarName")
.HasColumnType("text")
.HasColumnName("CALENDAR_NAME");
b.Property<byte[]>("Calendar")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("CALENDAR");
b.HasKey("SchedulerName", "CalendarName");
b.ToTable("QRTZ_CALENDARS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("CronExpression")
.IsRequired()
.HasColumnType("text")
.HasColumnName("CRON_EXPRESSION");
b.Property<string>("TimeZoneId")
.HasColumnType("text")
.HasColumnName("TIME_ZONE_ID");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_CRON_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("EntryId")
.HasColumnType("text")
.HasColumnName("ENTRY_ID");
b.Property<long>("FiredTime")
.HasColumnType("bigint")
.HasColumnName("FIRED_TIME");
b.Property<string>("InstanceName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("INSTANCE_NAME");
b.Property<bool>("IsNonConcurrent")
.HasColumnType("bool")
.HasColumnName("IS_NONCONCURRENT");
b.Property<string>("JobGroup")
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("JobName")
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<int>("Priority")
.HasColumnType("integer")
.HasColumnName("PRIORITY");
b.Property<bool?>("RequestsRecovery")
.HasColumnType("bool")
.HasColumnName("REQUESTS_RECOVERY");
b.Property<long>("ScheduledTime")
.HasColumnType("bigint")
.HasColumnName("SCHED_TIME");
b.Property<string>("State")
.IsRequired()
.HasColumnType("text")
.HasColumnName("STATE");
b.Property<string>("TriggerGroup")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("TriggerName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.HasKey("SchedulerName", "EntryId");
b.HasIndex("InstanceName")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_INST_NAME");
b.HasIndex("JobGroup")
.HasDatabaseName("IDX_QRTZ_FT_JOB_GROUP");
b.HasIndex("JobName")
.HasDatabaseName("IDX_QRTZ_FT_JOB_NAME");
b.HasIndex("RequestsRecovery")
.HasDatabaseName("IDX_QRTZ_FT_JOB_REQ_RECOVERY");
b.HasIndex("TriggerGroup")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_GROUP");
b.HasIndex("TriggerName")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_NAME");
b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_NM_GP");
b.ToTable("QRTZ_FIRED_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("JobName")
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<string>("JobGroup")
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("DESCRIPTION");
b.Property<bool>("IsDurable")
.HasColumnType("bool")
.HasColumnName("IS_DURABLE");
b.Property<bool>("IsNonConcurrent")
.HasColumnType("bool")
.HasColumnName("IS_NONCONCURRENT");
b.Property<bool>("IsUpdateData")
.HasColumnType("bool")
.HasColumnName("IS_UPDATE_DATA");
b.Property<string>("JobClassName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_CLASS_NAME");
b.Property<byte[]>("JobData")
.HasColumnType("bytea")
.HasColumnName("JOB_DATA");
b.Property<bool>("RequestsRecovery")
.HasColumnType("bool")
.HasColumnName("REQUESTS_RECOVERY");
b.HasKey("SchedulerName", "JobName", "JobGroup");
b.HasIndex("RequestsRecovery")
.HasDatabaseName("IDX_QRTZ_J_REQ_RECOVERY");
b.ToTable("QRTZ_JOB_DETAILS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("LockName")
.HasColumnType("text")
.HasColumnName("LOCK_NAME");
b.HasKey("SchedulerName", "LockName");
b.ToTable("QRTZ_LOCKS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.HasKey("SchedulerName", "TriggerGroup");
b.ToTable("QRTZ_PAUSED_TRIGGER_GRPS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("InstanceName")
.HasColumnType("text")
.HasColumnName("INSTANCE_NAME");
b.Property<long>("CheckInInterval")
.HasColumnType("bigint")
.HasColumnName("CHECKIN_INTERVAL");
b.Property<long>("LastCheckInTime")
.HasColumnType("bigint")
.HasColumnName("LAST_CHECKIN_TIME");
b.HasKey("SchedulerName", "InstanceName");
b.ToTable("QRTZ_SCHEDULER_STATE", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<bool?>("BooleanProperty1")
.HasColumnType("bool")
.HasColumnName("BOOL_PROP_1");
b.Property<bool?>("BooleanProperty2")
.HasColumnType("bool")
.HasColumnName("BOOL_PROP_2");
b.Property<decimal?>("DecimalProperty1")
.HasColumnType("numeric")
.HasColumnName("DEC_PROP_1");
b.Property<decimal?>("DecimalProperty2")
.HasColumnType("numeric")
.HasColumnName("DEC_PROP_2");
b.Property<int?>("IntegerProperty1")
.HasColumnType("integer")
.HasColumnName("INT_PROP_1");
b.Property<int?>("IntegerProperty2")
.HasColumnType("integer")
.HasColumnName("INT_PROP_2");
b.Property<long?>("LongProperty1")
.HasColumnType("bigint")
.HasColumnName("LONG_PROP_1");
b.Property<long?>("LongProperty2")
.HasColumnType("bigint")
.HasColumnName("LONG_PROP_2");
b.Property<string>("StringProperty1")
.HasColumnType("text")
.HasColumnName("STR_PROP_1");
b.Property<string>("StringProperty2")
.HasColumnType("text")
.HasColumnName("STR_PROP_2");
b.Property<string>("StringProperty3")
.HasColumnType("text")
.HasColumnName("STR_PROP_3");
b.Property<string>("TimeZoneId")
.HasColumnType("text")
.HasColumnName("TIME_ZONE_ID");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_SIMPROP_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<long>("RepeatCount")
.HasColumnType("bigint")
.HasColumnName("REPEAT_COUNT");
b.Property<long>("RepeatInterval")
.HasColumnType("bigint")
.HasColumnName("REPEAT_INTERVAL");
b.Property<long>("TimesTriggered")
.HasColumnType("bigint")
.HasColumnName("TIMES_TRIGGERED");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_SIMPLE_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("CalendarName")
.HasColumnType("text")
.HasColumnName("CALENDAR_NAME");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("DESCRIPTION");
b.Property<long?>("EndTime")
.HasColumnType("bigint")
.HasColumnName("END_TIME");
b.Property<byte[]>("JobData")
.HasColumnType("bytea")
.HasColumnName("JOB_DATA");
b.Property<string>("JobGroup")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("JobName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<short?>("MisfireInstruction")
.HasColumnType("smallint")
.HasColumnName("MISFIRE_INSTR");
b.Property<long?>("NextFireTime")
.HasColumnType("bigint")
.HasColumnName("NEXT_FIRE_TIME");
b.Property<long?>("PreviousFireTime")
.HasColumnType("bigint")
.HasColumnName("PREV_FIRE_TIME");
b.Property<int?>("Priority")
.HasColumnType("integer")
.HasColumnName("PRIORITY");
b.Property<long>("StartTime")
.HasColumnType("bigint")
.HasColumnName("START_TIME");
b.Property<string>("TriggerState")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_STATE");
b.Property<string>("TriggerType")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_TYPE");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.HasIndex("NextFireTime")
.HasDatabaseName("IDX_QRTZ_T_NEXT_FIRE_TIME");
b.HasIndex("TriggerState")
.HasDatabaseName("IDX_QRTZ_T_STATE");
b.HasIndex("NextFireTime", "TriggerState")
.HasDatabaseName("IDX_QRTZ_T_NFT_ST");
b.HasIndex("SchedulerName", "JobName", "JobGroup");
b.ToTable("QRTZ_TRIGGERS", (string)null);
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.Activity", b =>
{
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<bool>("AreRolesEnabled")
.HasColumnType("INTEGER");
b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");
b.Property<string>("CreatorDisplayName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<ulong>("CreatorUserId")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("DueDateTime")
.HasColumnType("TEXT");
b.Property<bool>("IsClosed")
.HasColumnType("INTEGER");
b.Property<uint>("MaxPlayers")
.HasColumnType("INTEGER");
b.Property<int>("Name")
.HasColumnType("INTEGER");
b.Property<ulong>("ThreadId")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("GuildId", "ChannelId", "MessageId");
b.HasIndex("ThreadId");
b.ToTable("Activities");
b.HasDiscriminator<string>("Discriminator").HasValue("Activity");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityPlayer", b =>
{
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("HasCompleted")
.HasColumnType("INTEGER");
b.Property<bool>("IsOrganizer")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("GuildId", "ChannelId", "MessageId", "UserId");
b.ToTable("ActivityPlayers");
b.HasDiscriminator<string>("Discriminator").HasValue("ActivityPlayer");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.InterstellarActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.Property<int>("Color")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("InterstellarActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.OrganizedActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.HasDiscriminator().HasValue("OrganizedActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.StagedActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.Property<uint>("Stage")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("StagedActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityRolePlayer", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.ActivityPlayer");
b.Property<byte>("Roles")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("ActivityRolePlayer");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("BlobTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("CronTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("SimplePropertyTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("SimpleTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail")
.WithMany("Triggers")
.HasForeignKey("SchedulerName", "JobName", "JobGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("JobDetail");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityPlayer", b =>
{
b.HasOne("Cocotte.Modules.Activities.Models.Activity", "Activity")
.WithMany("ActivityPlayers")
.HasForeignKey("GuildId", "ChannelId", "MessageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Activity");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b =>
{
b.Navigation("Triggers");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.Navigation("BlobTriggers");
b.Navigation("CronTriggers");
b.Navigation("SimplePropertyTriggers");
b.Navigation("SimpleTriggers");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.Activity", b =>
{
b.Navigation("ActivityPlayers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Cocotte.Migrations
{
/// <inheritdoc />
public partial class AddActivityPlayerHasCompleted : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "HasCompleted",
table: "ActivityPlayers",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "HasCompleted",
table: "ActivityPlayers");
}
}
}

View File

@@ -0,0 +1,686 @@
// <auto-generated />
using System;
using Cocotte.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Cocotte.Migrations
{
[DbContext(typeof(CocotteDbContext))]
partial class CocotteDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.4");
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<byte[]>("BlobData")
.HasColumnType("bytea")
.HasColumnName("BLOB_DATA");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_BLOB_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("CalendarName")
.HasColumnType("text")
.HasColumnName("CALENDAR_NAME");
b.Property<byte[]>("Calendar")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("CALENDAR");
b.HasKey("SchedulerName", "CalendarName");
b.ToTable("QRTZ_CALENDARS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("CronExpression")
.IsRequired()
.HasColumnType("text")
.HasColumnName("CRON_EXPRESSION");
b.Property<string>("TimeZoneId")
.HasColumnType("text")
.HasColumnName("TIME_ZONE_ID");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_CRON_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("EntryId")
.HasColumnType("text")
.HasColumnName("ENTRY_ID");
b.Property<long>("FiredTime")
.HasColumnType("bigint")
.HasColumnName("FIRED_TIME");
b.Property<string>("InstanceName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("INSTANCE_NAME");
b.Property<bool>("IsNonConcurrent")
.HasColumnType("bool")
.HasColumnName("IS_NONCONCURRENT");
b.Property<string>("JobGroup")
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("JobName")
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<int>("Priority")
.HasColumnType("integer")
.HasColumnName("PRIORITY");
b.Property<bool?>("RequestsRecovery")
.HasColumnType("bool")
.HasColumnName("REQUESTS_RECOVERY");
b.Property<long>("ScheduledTime")
.HasColumnType("bigint")
.HasColumnName("SCHED_TIME");
b.Property<string>("State")
.IsRequired()
.HasColumnType("text")
.HasColumnName("STATE");
b.Property<string>("TriggerGroup")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("TriggerName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.HasKey("SchedulerName", "EntryId");
b.HasIndex("InstanceName")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_INST_NAME");
b.HasIndex("JobGroup")
.HasDatabaseName("IDX_QRTZ_FT_JOB_GROUP");
b.HasIndex("JobName")
.HasDatabaseName("IDX_QRTZ_FT_JOB_NAME");
b.HasIndex("RequestsRecovery")
.HasDatabaseName("IDX_QRTZ_FT_JOB_REQ_RECOVERY");
b.HasIndex("TriggerGroup")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_GROUP");
b.HasIndex("TriggerName")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_NAME");
b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup")
.HasDatabaseName("IDX_QRTZ_FT_TRIG_NM_GP");
b.ToTable("QRTZ_FIRED_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("JobName")
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<string>("JobGroup")
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("DESCRIPTION");
b.Property<bool>("IsDurable")
.HasColumnType("bool")
.HasColumnName("IS_DURABLE");
b.Property<bool>("IsNonConcurrent")
.HasColumnType("bool")
.HasColumnName("IS_NONCONCURRENT");
b.Property<bool>("IsUpdateData")
.HasColumnType("bool")
.HasColumnName("IS_UPDATE_DATA");
b.Property<string>("JobClassName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_CLASS_NAME");
b.Property<byte[]>("JobData")
.HasColumnType("bytea")
.HasColumnName("JOB_DATA");
b.Property<bool>("RequestsRecovery")
.HasColumnType("bool")
.HasColumnName("REQUESTS_RECOVERY");
b.HasKey("SchedulerName", "JobName", "JobGroup");
b.HasIndex("RequestsRecovery")
.HasDatabaseName("IDX_QRTZ_J_REQ_RECOVERY");
b.ToTable("QRTZ_JOB_DETAILS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("LockName")
.HasColumnType("text")
.HasColumnName("LOCK_NAME");
b.HasKey("SchedulerName", "LockName");
b.ToTable("QRTZ_LOCKS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.HasKey("SchedulerName", "TriggerGroup");
b.ToTable("QRTZ_PAUSED_TRIGGER_GRPS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("InstanceName")
.HasColumnType("text")
.HasColumnName("INSTANCE_NAME");
b.Property<long>("CheckInInterval")
.HasColumnType("bigint")
.HasColumnName("CHECKIN_INTERVAL");
b.Property<long>("LastCheckInTime")
.HasColumnType("bigint")
.HasColumnName("LAST_CHECKIN_TIME");
b.HasKey("SchedulerName", "InstanceName");
b.ToTable("QRTZ_SCHEDULER_STATE", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<bool?>("BooleanProperty1")
.HasColumnType("bool")
.HasColumnName("BOOL_PROP_1");
b.Property<bool?>("BooleanProperty2")
.HasColumnType("bool")
.HasColumnName("BOOL_PROP_2");
b.Property<decimal?>("DecimalProperty1")
.HasColumnType("numeric")
.HasColumnName("DEC_PROP_1");
b.Property<decimal?>("DecimalProperty2")
.HasColumnType("numeric")
.HasColumnName("DEC_PROP_2");
b.Property<int?>("IntegerProperty1")
.HasColumnType("integer")
.HasColumnName("INT_PROP_1");
b.Property<int?>("IntegerProperty2")
.HasColumnType("integer")
.HasColumnName("INT_PROP_2");
b.Property<long?>("LongProperty1")
.HasColumnType("bigint")
.HasColumnName("LONG_PROP_1");
b.Property<long?>("LongProperty2")
.HasColumnType("bigint")
.HasColumnName("LONG_PROP_2");
b.Property<string>("StringProperty1")
.HasColumnType("text")
.HasColumnName("STR_PROP_1");
b.Property<string>("StringProperty2")
.HasColumnType("text")
.HasColumnName("STR_PROP_2");
b.Property<string>("StringProperty3")
.HasColumnType("text")
.HasColumnName("STR_PROP_3");
b.Property<string>("TimeZoneId")
.HasColumnType("text")
.HasColumnName("TIME_ZONE_ID");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_SIMPROP_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<long>("RepeatCount")
.HasColumnType("bigint")
.HasColumnName("REPEAT_COUNT");
b.Property<long>("RepeatInterval")
.HasColumnType("bigint")
.HasColumnName("REPEAT_INTERVAL");
b.Property<long>("TimesTriggered")
.HasColumnType("bigint")
.HasColumnName("TIMES_TRIGGERED");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.ToTable("QRTZ_SIMPLE_TRIGGERS", (string)null);
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.Property<string>("SchedulerName")
.HasColumnType("text")
.HasColumnName("SCHED_NAME");
b.Property<string>("TriggerName")
.HasColumnType("text")
.HasColumnName("TRIGGER_NAME");
b.Property<string>("TriggerGroup")
.HasColumnType("text")
.HasColumnName("TRIGGER_GROUP");
b.Property<string>("CalendarName")
.HasColumnType("text")
.HasColumnName("CALENDAR_NAME");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("DESCRIPTION");
b.Property<long?>("EndTime")
.HasColumnType("bigint")
.HasColumnName("END_TIME");
b.Property<byte[]>("JobData")
.HasColumnType("bytea")
.HasColumnName("JOB_DATA");
b.Property<string>("JobGroup")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_GROUP");
b.Property<string>("JobName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("JOB_NAME");
b.Property<short?>("MisfireInstruction")
.HasColumnType("smallint")
.HasColumnName("MISFIRE_INSTR");
b.Property<long?>("NextFireTime")
.HasColumnType("bigint")
.HasColumnName("NEXT_FIRE_TIME");
b.Property<long?>("PreviousFireTime")
.HasColumnType("bigint")
.HasColumnName("PREV_FIRE_TIME");
b.Property<int?>("Priority")
.HasColumnType("integer")
.HasColumnName("PRIORITY");
b.Property<long>("StartTime")
.HasColumnType("bigint")
.HasColumnName("START_TIME");
b.Property<string>("TriggerState")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_STATE");
b.Property<string>("TriggerType")
.IsRequired()
.HasColumnType("text")
.HasColumnName("TRIGGER_TYPE");
b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
b.HasIndex("NextFireTime")
.HasDatabaseName("IDX_QRTZ_T_NEXT_FIRE_TIME");
b.HasIndex("TriggerState")
.HasDatabaseName("IDX_QRTZ_T_STATE");
b.HasIndex("NextFireTime", "TriggerState")
.HasDatabaseName("IDX_QRTZ_T_NFT_ST");
b.HasIndex("SchedulerName", "JobName", "JobGroup");
b.ToTable("QRTZ_TRIGGERS", (string)null);
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.Activity", b =>
{
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<bool>("AreRolesEnabled")
.HasColumnType("INTEGER");
b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");
b.Property<string>("CreatorDisplayName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<ulong>("CreatorUserId")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("DueDateTime")
.HasColumnType("TEXT");
b.Property<bool>("IsClosed")
.HasColumnType("INTEGER");
b.Property<uint>("MaxPlayers")
.HasColumnType("INTEGER");
b.Property<int>("Name")
.HasColumnType("INTEGER");
b.Property<ulong>("ThreadId")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("GuildId", "ChannelId", "MessageId");
b.HasIndex("ThreadId");
b.ToTable("Activities");
b.HasDiscriminator<string>("Discriminator").HasValue("Activity");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityPlayer", b =>
{
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("HasCompleted")
.HasColumnType("INTEGER");
b.Property<bool>("IsOrganizer")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("GuildId", "ChannelId", "MessageId", "UserId");
b.ToTable("ActivityPlayers");
b.HasDiscriminator<string>("Discriminator").HasValue("ActivityPlayer");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.InterstellarActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.Property<int>("Color")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("InterstellarActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.OrganizedActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.HasDiscriminator().HasValue("OrganizedActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.StagedActivity", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.Activity");
b.Property<uint>("Stage")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("StagedActivity");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityRolePlayer", b =>
{
b.HasBaseType("Cocotte.Modules.Activities.Models.ActivityPlayer");
b.Property<byte>("Roles")
.HasColumnType("INTEGER");
b.HasDiscriminator().HasValue("ActivityRolePlayer");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("BlobTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("CronTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("SimplePropertyTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger")
.WithMany("SimpleTriggers")
.HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trigger");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail")
.WithMany("Triggers")
.HasForeignKey("SchedulerName", "JobName", "JobGroup")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("JobDetail");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.ActivityPlayer", b =>
{
b.HasOne("Cocotte.Modules.Activities.Models.Activity", "Activity")
.WithMany("ActivityPlayers")
.HasForeignKey("GuildId", "ChannelId", "MessageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Activity");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b =>
{
b.Navigation("Triggers");
});
modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
{
b.Navigation("BlobTriggers");
b.Navigation("CronTriggers");
b.Navigation("SimplePropertyTriggers");
b.Navigation("SimpleTriggers");
});
modelBuilder.Entity("Cocotte.Modules.Activities.Models.Activity", b =>
{
b.Navigation("ActivityPlayers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,51 @@
using Cocotte.Modules.Activities.Models;
using Discord.WebSocket;
using Quartz;
namespace Cocotte.Modules.Activities;
public class ActivityCloseJob : IJob
{
public long GuildId { private get; set; }
public long ChannelId { private get; set; }
public long MessageId { private get; set; }
private readonly ILogger<ActivityCloseJob> _logger;
private readonly ActivitiesRepository _activitiesRepository;
private readonly ActivityHelper _activityHelper;
private readonly DiscordSocketClient _discordClient;
public ActivityCloseJob(ILogger<ActivityCloseJob> logger, ActivitiesRepository activitiesRepository, ActivityHelper activityHelper, DiscordSocketClient discordClient)
{
_logger = logger;
_activitiesRepository = activitiesRepository;
_activityHelper = activityHelper;
_discordClient = discordClient;
}
public async Task Execute(IJobExecutionContext context)
{
// Get associated activity
if (await _activitiesRepository.FindActivity((ulong)GuildId, (ulong)ChannelId, (ulong)MessageId) is not { } activity)
{
_logger.LogTrace("Activity {MessageId} does not exist anymore", MessageId);
return;
}
// Close activity
activity.IsClosed = true;
await _activitiesRepository.SaveChanges();
_logger.LogInformation("Closed activity {Activity}", activity);
// Get channel
if (_discordClient.GetChannel(activity.ChannelId) is not SocketTextChannel channel)
{
return;
}
// Update embed
await _activityHelper.UpdateActivityEmbed(channel, activity, ActivityUpdateReason.Update);
}
}

View File

@@ -0,0 +1,240 @@
using System.Text;
using Cocotte.Modules.Activities.Models;
using Cocotte.Options;
using Cocotte.Utils;
using Discord;
using Microsoft.Extensions.Options;
namespace Cocotte.Modules.Activities;
public class ActivityFormatter
{
private const int FieldsChunkSize = 20;
private const string EmptyField = "\u200B";
private readonly ActivityOptions _options;
private readonly InterstellarFormatter _interstellarFormatter;
public ActivityFormatter(IOptions<ActivityOptions> options, InterstellarFormatter interstellarFormatter)
{
_interstellarFormatter = interstellarFormatter;
_options = options.Value;
}
public string FormatActivityName(ActivityName activityName)
{
return activityName switch
{
ActivityName.Abyss => "Abîme du Néant",
ActivityName.Raids => "Raids",
ActivityName.FrontierClash => "Conflit frontalier",
ActivityName.VoidRift => "Failles du néant",
ActivityName.OriginsOfWar => "Origine de la guerre",
ActivityName.JointOperation => "Opération conjointe",
ActivityName.InterstellarExploration => "Porte interstellaire",
ActivityName.BreakFromDestiny => "Échapper au destin (3v3)",
ActivityName.CriticalAbyss => "Abîme critique (8v8)",
ActivityName.Minigame => "Mini-Jeux",
ActivityName.Fishing => "Pêche",
ActivityName.MirroriaRace => "Course Mirroria",
ActivityName.Event => "Évènement",
_ => throw new ArgumentOutOfRangeException(nameof(activityName), activityName, null)
};
}
public string GetActivityBanner(ActivityName activityName)
{
return CdnUtils.GetAsset($"banner/{GetActivityCode(activityName)}.webp");
}
public EmbedBuilder ActivityEmbed(Activity activity, IReadOnlyCollection<ActivityPlayer> players)
{
int GetNamesPadding(IReadOnlyCollection<ActivityPlayer> activityPlayers) => activityPlayers.Count > 0 ? activityPlayers.Max(p => p.Name.Length) : 0;
// Load activity players and organizers
var participants = activity.Participants.OrderBy(p => p.HasCompleted).ToArray();
var organizers = activity.Organizers.ToArray();
// Activity full
bool activityFull = participants.Length >= activity.MaxPlayers;
// Players and organizers fields
var fields = new List<EmbedFieldBuilder>();
// Add organizers if it's an organized activity
if (activity is OrganizedActivity)
{
var organizersFields = organizers.Chunk(FieldsChunkSize).Select(organizersChunk =>
new EmbedFieldBuilder()
.WithName(EmptyField)
.WithIsInline(true)
.WithValue($"{(!organizersChunk.Any() ? "*Aucun organisateur inscrit*" : string.Join("\n", organizersChunk.Select(p => FormatActivityPlayer(p, GetNamesPadding(organizersChunk), showOrganizerRole: ActivityHelper.IsEventActivity(activity.Type)))))}")
).ToArray();
if (organizersFields.Length > 0)
{
organizersFields[0].Name = "Organisateurs";
}
// Complete with empty fields to go to next line
var emptyFields = Enumerable.Repeat(0, (3 - organizersFields.Length) % 3).Select(_ =>
new EmbedFieldBuilder()
.WithName(EmptyField)
.WithValue(EmptyField)
.WithIsInline(true)
);
fields.AddRange(organizersFields);
fields.AddRange(emptyFields);
}
// Players field
var playersFields = participants.Chunk(FieldsChunkSize).Select(participantsChunk =>
new EmbedFieldBuilder()
.WithName(EmptyField)
.WithIsInline(true)
.WithValue($"{(!participantsChunk.Any() ? "*Aucun joueur inscrit*" : string.Join("\n", participantsChunk.Select(p => FormatActivityPlayer(p, GetNamesPadding(participantsChunk), hideRoles: activity is OrganizedActivity))))}")
).ToList();
// Insert empty fields in third column
for (int i = 2; i <= playersFields.Count; i += 3)
{
playersFields.Insert(i, new EmbedFieldBuilder().WithName(EmptyField).WithValue(EmptyField).WithIsInline(true));
}
if (playersFields.Count > 0)
{
playersFields[0].Name = "Joueurs inscrits";
}
fields.AddRange(playersFields);
string countTitlePart = activity.MaxPlayers == ActivityHelper.UnlimitedPlayers
? ""
: $"({participants.Length}/{activity.MaxPlayers})";
var title = activity switch
{
StagedActivity stagedActivity =>
$"{FormatActivityName(activity.Name)} {countTitlePart} - Étage {stagedActivity.Stage}",
InterstellarActivity interstellar =>
$"{FormatActivityName(activity.Name)} {_interstellarFormatter.FormatInterstellarColor(interstellar.Color)} {countTitlePart}",
OrganizedActivity =>
$"{(ActivityHelper.IsEventActivity(activity.Type) ? "Organisation d'évènement" : $"Proposition d'aide - {FormatActivityName(activity.Name)}")} {countTitlePart}",
_ =>
$"{FormatActivityName(activity.Name)} {countTitlePart}"
};
// Build description
var descriptionBuilder = new StringBuilder();
// Add time if specified
if (activity.DueDateTime is { } dueDateTime)
{
// Also Add date of organized
if (activity is OrganizedActivity)
{
descriptionBuilder.AppendLine($"**:date: {TimestampTag.FormatFromDateTime(dueDateTime, TimestampTagStyles.LongDate)}**");
}
descriptionBuilder.AppendLine($"**:clock2: {TimestampTag.FormatFromDateTime(dueDateTime, TimestampTagStyles.ShortTime)} | {(activity.IsClosed ? "Fermée" : TimestampTag.FormatFromDateTime(dueDateTime, TimestampTagStyles.Relative))}**");
descriptionBuilder.AppendLine();
}
else
{
descriptionBuilder.AppendLine("**:clock2: Maintenant**");
}
// Add generic message or specified activity description
descriptionBuilder.AppendLine(
string.IsNullOrWhiteSpace(activity.Description)
? $"Rejoignez l'activité de {MentionUtils.MentionUser(activity.CreatorUserId)}"
: activity.Description
);
// Add thread link
descriptionBuilder.AppendLine();
descriptionBuilder.Append(
$"""
**⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯**
**[Fil associé]({ChannelUtils.GetChannelLink(activity.GuildId, activity.ThreadId)})**
**⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯**
""");
string bannerUrl = GetActivityBanner(activity.Name);
var color = activity.IsClosed ?
Colors.CocotteRed : activityFull ?
Colors.CocotteOrange : Colors.CocotteBlue;
var builder = new EmbedBuilder()
.WithColor(color)
.WithTitle(title)
.WithDescription(descriptionBuilder.ToString())
.WithImageUrl(bannerUrl)
.WithFields(fields);
// Add material for interstellar exploration
if (activity is InterstellarActivity interstellarActivity)
{
builder.WithThumbnailUrl(_interstellarFormatter.GetColorIcon(interstellarActivity.Color));
}
return builder;
}
private string GetActivityCode(ActivityName activityName) => activityName switch
{
ActivityName.Abyss => "VA",
ActivityName.OriginsOfWar => "OOW",
ActivityName.Raids => "RD",
ActivityName.FrontierClash => "FCH",
ActivityName.VoidRift => "VR",
ActivityName.JointOperation => "JO",
ActivityName.InterstellarExploration => "IE",
ActivityName.BreakFromDestiny => "BR",
ActivityName.CriticalAbyss => "CA",
ActivityName.Fishing => "FI",
ActivityName.Minigame => "MG",
ActivityName.MirroriaRace => "MR",
ActivityName.Event => "EV",
_ => "NA"
};
public string FormatActivityPlayer(ActivityPlayer player, int namePadding, bool showOrganizerRole = false, bool hideRoles = false) =>
player.HasCompleted
? $"~~{FormatActivityPlayerSub(player, namePadding, showOrganizerRole, hideRoles)}~~"
: FormatActivityPlayerSub(player, namePadding, showOrganizerRole, hideRoles);
private string FormatActivityPlayerSub(ActivityPlayer player, int namePadding, bool showOrganizerRole = false, bool hideRoles = false) => player switch
{
ActivityRolePlayer rolePlayer when !hideRoles => $"` {player.Name.PadRight(namePadding)} ` **|** {RolesToEmotes(rolePlayer.Roles)}",
_ when showOrganizerRole && player.IsOrganizer => $"` {player.Name.PadRight(namePadding)} ` **|** {_options.OrganizerEmote} ",
_ => $"` {player.Name.PadRight(namePadding)} `"
};
private string RolesToEmotes(PlayerRoles rolePlayerRoles)
{
var emotesBuilder = new StringBuilder();
if (rolePlayerRoles.HasFlag(PlayerRoles.Helper))
{
emotesBuilder.Append($" {_options.HelperEmote} **|**");
}
if (rolePlayerRoles.HasFlag(PlayerRoles.Support))
{
emotesBuilder.Append($" {_options.SupportEmote} ");
}
if (rolePlayerRoles.HasFlag(PlayerRoles.Tank))
{
emotesBuilder.Append($" {_options.TankEmote} ");
}
if (rolePlayerRoles.HasFlag(PlayerRoles.Dps))
{
emotesBuilder.Append($" {_options.DpsEmote} ");
}
return emotesBuilder.ToString();
}
}

View File

@@ -0,0 +1,142 @@
using Cocotte.Modules.Activities.Models;
using Cocotte.Options;
using Cocotte.Utils;
using Discord;
using Discord.WebSocket;
using Microsoft.Extensions.Options;
namespace Cocotte.Modules.Activities;
public class ActivityHelper
{
public const uint UnlimitedPlayers = uint.MaxValue;
private readonly ActivityOptions _options;
private readonly ActivitiesRepository _activitiesRepository;
private readonly ActivityFormatter _activityFormatter;
public ActivityHelper(IOptions<ActivityOptions> options, ActivitiesRepository activitiesRepository, ActivityFormatter activityFormatter)
{
_activitiesRepository = activitiesRepository;
_activityFormatter = activityFormatter;
_options = options.Value;
}
public PlayerRoles GetPlayerRoles(IEnumerable<SocketRole> userRoles)
{
var roles = PlayerRoles.None;
foreach (var socketRole in userRoles)
{
roles |= socketRole.Id switch
{
var role when role == _options.HelperRoleId => PlayerRoles.Helper,
var role when role == _options.DpsRoleId => PlayerRoles.Dps,
var role when role == _options.TankRoleId => PlayerRoles.Tank,
var role when role == _options.SupportRoleId => PlayerRoles.Support,
var role when role == _options.OrganizerRoleId => PlayerRoles.Organizer,
_ => PlayerRoles.None
};
}
return roles;
}
public static ActivityType ActivityNameToType(ActivityName activityName) => activityName switch
{
ActivityName.Abyss or
ActivityName.FrontierClash or
ActivityName.InterstellarExploration or
ActivityName.JointOperation or
ActivityName.VoidRift or
ActivityName.OriginsOfWar => ActivityType.Pve4Players,
ActivityName.Raids => ActivityType.Pve8Players,
ActivityName.CriticalAbyss => ActivityType.Pvp8Players,
ActivityName.BreakFromDestiny => ActivityType.Pvp3Players,
ActivityName.Minigame => ActivityType.Other8Players,
ActivityName.MirroriaRace => ActivityType.Other4Players,
_ => ActivityType.OtherUnlimitedPlayers
};
public static uint ActivityTypeToMaxPlayers(ActivityType activityType) => activityType switch
{
ActivityType.Pve4Players or
ActivityType.Other4Players => 4,
ActivityType.Pve8Players or
ActivityType.Pvp8Players or
ActivityType.Other8Players => 8,
ActivityType.Pvp3Players => 3,
ActivityType.OtherUnlimitedPlayers => UnlimitedPlayers,
_ => 0
};
public static bool IsEventActivity(ActivityType activityType) =>
activityType is ActivityType.Other8Players or ActivityType.Other4Players or ActivityType.OtherUnlimitedPlayers;
public bool IsOrganizer(OrganizedActivity organizedActivity, SocketGuildUser user)
{
// If it is an event, check if the user has organizer role, otherwise helper
if (IsEventActivity(organizedActivity.Type))
{
return user.Roles.Any(r => r.Id == _options.OrganizerRoleId);
}
return user.Roles.Any(r => r.Id == _options.HelperRoleId);
}
public async Task UpdateActivityEmbed(IMessageChannel channel, Activity activity, ActivityUpdateReason updateReason)
{
// Fetch players
var players = await _activitiesRepository.LoadActivityPlayers(activity);
await channel.ModifyMessageAsync(activity.MessageId, properties =>
{
properties.Embed = _activityFormatter.ActivityEmbed(activity, players).Build();
// Disable join button if the activity is full on join, enable it on leave if activity is not full anymore
var isActivityFull = players.Count >= activity.MaxPlayers;
properties.Components = updateReason switch
{
ActivityUpdateReason.PlayerJoin when isActivityFull && activity is not OrganizedActivity => ActivityComponents(activity.MessageId, disabled: true).Build(),
ActivityUpdateReason.PlayerLeave when !isActivityFull => ActivityComponents(activity.MessageId, disabled: false).Build(),
_ => Optional<MessageComponent>.Unspecified
};
});
}
public ComponentBuilder ActivityComponents(ulong activityId, bool disabled = false)
{
return new ComponentBuilder()
.AddRow(new ActionRowBuilder()
.WithButton(new ButtonBuilder()
.WithLabel("Rejoindre")
.WithCustomId($"activity join:{activityId}")
.WithEmote(":white_check_mark:".ToEmote())
.WithStyle(ButtonStyle.Primary)
.WithDisabled(disabled)
)
.WithButton(new ButtonBuilder()
.WithLabel("Se désinscrire")
.WithCustomId($"activity leave:{activityId}")
.WithEmote(":x:".ToEmote())
.WithStyle(ButtonStyle.Secondary)
)
.WithButton(new ButtonBuilder()
.WithLabel("Supprimer")
.WithCustomId($"activity delete:{activityId}")
.WithEmote(":wastebasket:".ToEmote())
.WithStyle(ButtonStyle.Danger)
)
);
}
}

View File

@@ -0,0 +1,659 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Cocotte.Modules.Activities.Models;
using Cocotte.Options;
using Cocotte.Utils;
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.Extensions.Options;
using Quartz;
using Alias = Discord.Commands.AliasAttribute;
namespace Cocotte.Modules.Activities;
/// <summary>
/// Module to ask and propose groups for different activities: Abyss, OOW, FC, ...
/// </summary>
[Group("activite", "Organise des activités")]
[SuppressMessage("ReSharper", "UnusedMember.Local")]
public partial class ActivityModule : InteractionModuleBase<SocketInteractionContext>
{
private readonly ILogger<ActivityModule> _logger;
private readonly ActivityOptions _options;
private readonly ActivityHelper _activityHelper;
private readonly ActivitiesRepository _activitiesRepository;
private readonly ActivityFormatter _activityFormatter;
private readonly ISchedulerFactory _schedulerFactory;
private SocketGuildUser User => (SocketGuildUser) Context.User;
public ActivityModule(ILogger<ActivityModule> logger, IOptions<ActivityOptions> options,
ActivityHelper activityHelper, ActivitiesRepository activitiesRepository,
ActivityFormatter activityFormatter, ISchedulerFactory schedulerFactory)
{
_logger = logger;
_activityHelper = activityHelper;
_activitiesRepository = activitiesRepository;
_activityFormatter = activityFormatter;
_schedulerFactory = schedulerFactory;
_options = options.Value;
}
#if DEBUG
[RequireOwner]
[SlashCommand("setup-info", "Display activity setup info")]
public async Task SetupInfo()
{
await RespondAsync($"""
- Helper: {MentionUtils.MentionRole(_options.HelperRoleId)} {_options.HelperEmote.ToEmote()}
- Dps: {MentionUtils.MentionRole(_options.DpsRoleId)} {_options.DpsEmote.ToEmote()}
- Tank: {MentionUtils.MentionRole(_options.TankRoleId)} {_options.TankEmote.ToEmote()}
- Healer: {MentionUtils.MentionRole(_options.SupportRoleId)} {_options.SupportEmote.ToEmote()}
""");
}
#endif
#region Activities
[SlashCommand("abime-neant", "Créer un groupe pour l'Abîme du Néant")]
[Alias("abime", "abyss")]
public async Task ActivityVoidAbyss(
[Summary("étage", "A quel étage vous êtes")] [MinValue(1), MaxValue(6)] uint stage,
[Summary("heure", "Heure à laquelle l'activité est prévue")] string? timeInput = null,
[Summary("description", "Message accompagnant la demande de groupe")] string description = "")
{
await CreateActivity(ActivityName.Abyss, timeInput, description, stage: stage);
}
[SlashCommand("origine-guerre", "Créer un groupe pour l'Origine de la guerre")]
[Alias("origine", "OOW")]
public async Task ActivityOrigins(
[Summary("étage", "A quel étage vous êtes")] [MinValue(1), MaxValue(25)] uint stage,
[Summary("heure", "Heure à laquelle l'activité est prévue")] string? timeInput = null,
[Summary("description", "Message accompagnant la demande de groupe")] string description = "")
{
await CreateActivity(ActivityName.OriginsOfWar, timeInput, description, stage: stage);
}
[SlashCommand("raids", "Créer un groupe pour les Raids")]
public async Task ActivityRaids(
[Summary("heure", "Heure à laquelle l'activité est prévue")] string? timeInput = null,
[Summary("description", "Message accompagnant la demande de groupe")] string description = "")
{
await CreateActivity(ActivityName.Raids, timeInput, description);
}
[SlashCommand("conflit-frontalier", "Créer un groupe pour Conflit frontalier")]
[Alias("conflit", "FC")]
public async Task ActivityFrontierClash(
[Summary("heure", "Heure à laquelle l'activité est prévue")] string? timeInput = null,
[Summary("description", "Message accompagnant la demande de groupe")] string description = "")
{
await CreateActivity(ActivityName.FrontierClash, timeInput, description);
}
[SlashCommand("failles-neant", "Créer un groupe pour les Failles du néant")]
[Alias("failles", "rift")]
public async Task ActivityVoidRift(
[Summary("heure", "Heure à laquelle l'activité est prévue")] string? timeInput = null,
[Summary("description", "Message accompagnant la demande de groupe")] string description = "")
{
await CreateActivity(ActivityName.VoidRift, timeInput, description);
}
[SlashCommand("operations-conjointes", "Créer un groupe pour les Opérations conjointes")]
[Alias("operations", "JO")]
public async Task ActivityJointOperation(
[Summary("heure", "Heure à laquelle l'activité est prévue")] string? timeInput = null,
[Summary("description", "Message accompagnant la demande de groupe")] string description = "")
{
await CreateActivity(ActivityName.JointOperation, timeInput, description);
}
[SlashCommand("portes-interstellaires", "Créer un groupe pour les Portes interstellaires")]
[Alias("portes")]
public async Task ActivityInterstellarExploration(
[Summary("couleur", "De quel couleur de matériaux s'agît-il")] InterstellarColor color,
[Summary("heure", "Heure à laquelle l'activité est prévue")] string? timeInput = null,
[Summary("description", "Message accompagnant la demande de groupe")] string description = "")
{
await CreateActivity(ActivityName.InterstellarExploration, timeInput, description, areRolesEnabled: false,
interstellarColor: color);
}
[SlashCommand("3v3", "Créer un groupe pour le 3v3 (Échapper au destin)")]
[Alias("BR")]
public async Task ActivityBreakFromDestiny(
[Summary("heure", "Heure à laquelle l'activité est prévue")] string? timeInput = null,
[Summary("description", "Message accompagnant la demande de groupe")] string description = "")
{
await CreateActivity(ActivityName.BreakFromDestiny, timeInput, description, areRolesEnabled: false);
}
[SlashCommand("8v8", "Créer un groupe pour le 8v8 (Abîme critique)")]
[Alias("critical")]
public async Task ActivityCriticalAbyss(
[Summary("heure", "Heure à laquelle l'activité est prévue")] string? timeInput = null,
[Summary("description", "Message accompagnant la demande de groupe")] string description = "")
{
await CreateActivity(ActivityName.CriticalAbyss, timeInput, description);
}
[SlashCommand("mini-jeux", "Créer un groupe pour les mini-jeux")]
[Alias("jeux")]
public async Task ActivityMinigame(
[Summary("joueurs", "Nombre de joueurs maximum pour cette activité")] [MinValue(2), MaxValue(16)]
uint maxPlayers = 8, [Summary("heure", "Heure à laquelle l'activité est prévue")] string? timeInput = null,
[Summary("description", "Message accompagnant la demande de groupe")] string description = "")
{
await CreateActivity(ActivityName.Minigame, timeInput, description, areRolesEnabled: false,
maxPlayers: maxPlayers);
}
[SlashCommand("peche", "Créer un groupe pour de la pêche")]
[Alias("fishing")]
public async Task ActivityFishing(
[Summary("heure", "Heure à laquelle l'activité est prévue")] string? timeInput = null,
[Summary("description", "Message accompagnant la demande de groupe")] string description = "")
{
await CreateActivity(ActivityName.Fishing, timeInput, description, areRolesEnabled: false);
}
[SlashCommand("course", "Créer un groupe pour les courses de Mirroria")]
[Alias("BR")]
public async Task ActivityMirroriaRace(
[Summary("heure", "Heure à laquelle l'activité est prévue")] string? timeInput = null,
[Summary("description", "Message accompagnant la demande de groupe")] string description = "")
{
await CreateActivity(ActivityName.MirroriaRace, timeInput, description, areRolesEnabled: false);
}
#endregion
#region Organization
[SlashCommand("organiser-abime", "Organiser un groupe d'aide pour l'abîme du néant")]
public async Task OrganizeVoidAbyss(
[Summary("jour", "Jour auquel l'activité est prévu")] CocotteDayOfWeek day,
[Summary("heure", "Heure à laquelle l'activité est prévue")] string timeInput,
[Summary("joueurs", "Nombre de joueurs maximum qui peuvent s'inscrire")] uint maxPlayers,
string description = "")
{
if (!TimeOnly.TryParse(timeInput, out var time))
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils
.ErrorEmbed(
"**Heure invalide**, essayez avec le **format** `heure:minutes`\nPar exemple: `15:30`")
.Build()
);
return;
}
// Calculate date
var date = DateTimeUtils.NextDateWithDayAndTime((DayOfWeek) day, time);
await CreateActivity(ActivityName.Abyss, null, description, true, maxPlayers, date: date);
}
[SlashCommand("organiser-origine", "Organiser un groupe d'aide pour l'origine de la guerre")]
public async Task OrganizeOrigins(
[Summary("jour", "Jour auquel l'activité est prévu")] CocotteDayOfWeek day,
[Summary("heure", "Heure à laquelle l'activité est prévue")] string timeInput,
[Summary("joueurs", "Nombre de joueurs maximum qui peuvent s'inscrire")] uint maxPlayers,
string description = "")
{
if (!TimeOnly.TryParse(timeInput, out var time))
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils
.ErrorEmbed(
"**Heure invalide**, essayez avec le **format** `heure:minutes`\nPar exemple: `15:30`")
.Build()
);
return;
}
// Calculate date
var date = DateTimeUtils.NextDateWithDayAndTime((DayOfWeek) day, time);
await CreateActivity(ActivityName.OriginsOfWar, null, description, true, maxPlayers, date: date);
}
[SlashCommand("organiser-evenement", "Organiser un événement")]
public async Task OrganizeEvent(
[Summary("jour", "Jour auquel l'activité est prévu")] CocotteDayOfWeek day,
[Summary("heure", "Heure à laquelle l'activité est prévue")] string timeInput,
[Summary("joueurs", "Nombre de joueurs maximum qui peuvent s'inscrire")] uint maxPlayers,
string description = "")
{
if (!TimeOnly.TryParse(timeInput, out var time))
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils
.ErrorEmbed(
"**Heure invalide**, essayez avec le **format** `heure:minutes`\nPar exemple: `15:30`")
.Build()
);
return;
}
// Calculate date
var date = DateTimeUtils.NextDateWithDayAndTime((DayOfWeek) day, time);
await CreateActivity(ActivityName.Event, null, description, false, maxPlayers, date: date);
}
#endregion
private async Task CreateActivity(ActivityName activityName, string? timeInput, string description,
bool areRolesEnabled = true, uint? maxPlayers = null, uint? stage = null,
InterstellarColor? interstellarColor = null, DateTime? date = null)
{
// Check time if it's specified
var dueDate = date;
if (timeInput is not null)
{
if (!TimeOnly.TryParse(timeInput, out var parsedTime))
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils
.ErrorEmbed(
"**Heure invalide**, essayez avec le **format** `heure:minutes`\nPar exemple: `15:30`")
.Build()
);
return;
}
dueDate = DateTimeUtils.NextDateWithTimeOfDay(parsedTime);
}
_logger.LogTrace("{User} is creating activity {Activity}", User.DisplayName, activityName);
// Activities are identified using their original message id
await RespondAsync("> ***Création de l'activité en cours...***");
var response = await GetOriginalResponseAsync();
// Create associated thread
var threadId = await CreateThread(activityName, User.DisplayName);
var activityType = ActivityHelper.ActivityNameToType(activityName);
maxPlayers ??= ActivityHelper.ActivityTypeToMaxPlayers(activityType);
Activity activity;
// Create organized activity if date is not null
if (date is not null)
{
activity = new OrganizedActivity
{
MessageId = response.Id,
ChannelId = Context.Channel.Id,
GuildId = Context.Guild.Id,
ThreadId = threadId,
CreatorUserId = Context.User.Id,
CreatorDisplayName = ((SocketGuildUser) Context.User).DisplayName,
Description = description,
DueDateTime = dueDate,
Type = activityType,
Name = activityName,
AreRolesEnabled = areRolesEnabled,
MaxPlayers = (uint) maxPlayers
};
}
else if (stage is not null)
{
activity = new StagedActivity
{
MessageId = response.Id,
ChannelId = Context.Channel.Id,
GuildId = Context.Guild.Id,
ThreadId = threadId,
CreatorUserId = Context.User.Id,
CreatorDisplayName = ((SocketGuildUser) Context.User).DisplayName,
Description = description,
DueDateTime = dueDate,
Type = activityType,
Name = activityName,
AreRolesEnabled = areRolesEnabled,
MaxPlayers = (uint) maxPlayers,
Stage = (uint) stage
};
}
else if (interstellarColor is not null)
{
activity = new InterstellarActivity
{
MessageId = response.Id,
ChannelId = Context.Channel.Id,
GuildId = Context.Guild.Id,
ThreadId = threadId,
CreatorUserId = Context.User.Id,
CreatorDisplayName = ((SocketGuildUser) Context.User).DisplayName,
Description = description,
DueDateTime = dueDate,
Type = activityType,
Name = activityName,
AreRolesEnabled = false,
MaxPlayers = (uint) maxPlayers,
Color = (InterstellarColor) interstellarColor
};
}
else
{
activity = new Activity
{
MessageId = response.Id,
ChannelId = Context.Channel.Id,
GuildId = Context.Guild.Id,
ThreadId = threadId,
CreatorUserId = Context.User.Id,
CreatorDisplayName = ((SocketGuildUser) Context.User).DisplayName,
Description = description,
DueDateTime = dueDate,
Type = activityType,
Name = activityName,
AreRolesEnabled = areRolesEnabled,
MaxPlayers = (uint) maxPlayers
};
}
// Add activity to db
await _activitiesRepository.AddActivity(activity);
// Add creator to activity, make it an helper
var activityPlayer = CreateActivityPlayer(activity, User, areRolesEnabled, activity is OrganizedActivity);
activity.ActivityPlayers.Add(activityPlayer);
await _activitiesRepository.SaveChanges();
// Add components
var components = _activityHelper.ActivityComponents(activity.MessageId);
await ModifyOriginalResponseAsync(m =>
{
m.Content = "";
m.Components = components.Build();
m.Embed = _activityFormatter.ActivityEmbed(activity, Enumerable.Repeat(activityPlayer, 1).ToImmutableList())
.Build();
});
// Add job to close this activity in scheduler if due date is specified
if (dueDate is { } dueDateTime)
{
var scheduler = await _schedulerFactory.GetScheduler();
var job = JobBuilder.Create<ActivityCloseJob>()
.WithIdentity(activity.JobKey, "activity-close")
.WithDescription("Automatically close an activity after due date has passed")
.UsingJobData(nameof(ActivityCloseJob.GuildId), (long) activity.GuildId)
.UsingJobData(nameof(ActivityCloseJob.ChannelId), (long) activity.ChannelId)
.UsingJobData(nameof(ActivityCloseJob.MessageId), (long) activity.MessageId)
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity($"{activity.JobKey}-trigger", "activity-close")
.StartAt(dueDateTime)
.ForJob(job)
.Build();
await scheduler.ScheduleJob(job, trigger);
}
}
private ActivityPlayer CreateActivityPlayer(Activity activity, SocketGuildUser user, bool areRolesEnabled, bool isOrganizer = false)
{
return areRolesEnabled
? new ActivityRolePlayer
{
Activity = activity,
UserId = user.Id,
Name = user.DisplayName,
Roles = _activityHelper.GetPlayerRoles(user.Roles),
IsOrganizer = isOrganizer
}
: new ActivityPlayer
{
Activity = activity,
UserId = user.Id,
Name = user.DisplayName,
IsOrganizer = isOrganizer
};
}
[ComponentInteraction("activity join:*", ignoreGroupNames: true)]
public async Task JoinActivity(ulong messageId)
{
// Check if activity exists
if (await _activitiesRepository.FindActivity(Context.Guild.Id, Context.Channel.Id, messageId) is not
{ } activity)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Cette activité **n'existe plus**").Build()
);
return;
}
if (!await AddUserToActivity(activity, User, self: true))
{
return;
}
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.SuccessEmbed("Vous avez bien été **inscrit** pour cette activité").Build()
);
}
[ComponentInteraction("activity leave:*", ignoreGroupNames: true)]
public async Task LeaveActivity(ulong messageId)
{
// Check if activity exists
if (await _activitiesRepository.FindActivity(Context.Guild.Id, Context.Channel.Id, messageId) is not
{ } activity)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Cette activité **n'existe plus**").Build()
);
return;
}
if (!await RemovePlayerFromActivity(activity, User, self: true))
{
return;
}
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.SuccessEmbed("Vous avez bien été **désinscrit** pour cette activité").Build()
);
}
[ComponentInteraction("activity delete:*", ignoreGroupNames: true)]
public async Task DeleteActivity(ulong messageId)
{
// Check if activity exists
if (await _activitiesRepository.FindActivity(Context.Guild.Id, Context.Channel.Id, messageId) is not
{ } activity)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Cette activité **n'existe plus**").Build()
);
return;
}
// Check if user has permission to delete this activity
if (User.Id != activity.CreatorUserId && Context.User.Id != (await Context.Client.GetApplicationInfoAsync()).Owner.Id)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Vous n'avez **pas la permission** d'exécuter cette action").Build()
);
return;
}
// Remove from database
_activitiesRepository.DeleteActivity(activity);
await _activitiesRepository.SaveChanges();
// Remove thread
await Context.Guild.GetThreadChannel(activity.ThreadId).DeleteAsync();
// Delete response
await Context.Channel.DeleteMessageAsync(messageId);
}
private async Task<bool> AddUserToActivity(Activity activity, SocketGuildUser user, bool self)
{
// If player is already registered
if (await _activitiesRepository.FindActivityPlayer(activity.GuildId, activity.ChannelId, activity.MessageId,
user.Id) is not
null)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils
.ErrorEmbed(self
? "Vous êtes **déjà inscrit** à cette activité"
: $"{MentionUtils.MentionUser(user.Id)} est **déjà inscrit** à cette activité").Build()
);
return false;
}
// If activity is an organized activity and user is an organizer, bypass full and closed checks
var isOrganizer = activity is OrganizedActivity organizedActivity &&
_activityHelper.IsOrganizer(organizedActivity, user);
if (!isOrganizer)
{
// Check if activity is closed
if (activity.IsClosed)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Cette activité est fermée").Build()
);
return false;
}
// Check if activity is full
if (await _activitiesRepository.ActivityPlayerCount(activity) >= activity.MaxPlayers)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("L'activité est **complète**").Build()
);
return false;
}
}
_logger.LogTrace("Player {Player} joined activity {Id}", user.DisplayName, activity.MessageId);
var activityPlayer = CreateActivityPlayer(activity, user, activity.AreRolesEnabled, isOrganizer: isOrganizer);
// Add player to activity
activity.ActivityPlayers.Add(activityPlayer);
await _activitiesRepository.SaveChanges();
// Update activity embed
await UpdateActivityEmbed(activity, ActivityUpdateReason.PlayerJoin);
// Send join message to thread and add user to thread
var thread = Context.Guild.GetThreadChannel(activity.ThreadId);
await thread.AddUserAsync(user);
string embedContent = $"{MentionUtils.MentionUser(user.Id)} a été **ajouté** à l'activité";
if (self)
{
await thread.SendMessageAsync(
embed: EmbedUtils.InfoEmbed(embedContent).Build()
);
}
else
{
await RespondAsync(
embed: EmbedUtils.InfoEmbed(embedContent).Build()
);
}
return true;
}
private async Task<bool> RemovePlayerFromActivity(Activity activity, SocketGuildUser user, bool self)
{
// Check if player is in activity
if (await _activitiesRepository.FindActivityPlayer(activity.GuildId, activity.ChannelId, activity.MessageId,
user.Id) is not { } activityPlayer)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils
.ErrorEmbed(self
? "Vous n'êtes **pas inscrit** à cette activité"
: $"{MentionUtils.MentionUser(user.Id)} n'est **pas inscrit** à cette activité").Build()
);
return false;
}
_logger.LogTrace("Player {Player} left activity {Id}", user.DisplayName, activity.MessageId);
activity.ActivityPlayers.Remove(activityPlayer);
await _activitiesRepository.SaveChanges();
// Update activity embed
await UpdateActivityEmbed(activity, ActivityUpdateReason.PlayerLeave);
// Send leave message to thread and remove user from thread
var thread = Context.Guild.GetThreadChannel(activity.ThreadId);
await thread.RemoveUserAsync(user);
string embedContent = $"{MentionUtils.MentionUser(user.Id)} a été **enlevé** de l'activité";
if (self)
{
await thread.SendMessageAsync(
embed: EmbedUtils.InfoEmbed(embedContent).Build()
);
}
else
{
await RespondAsync(
embed: EmbedUtils.InfoEmbed(embedContent).Build()
);
}
return true;
}
private async Task UpdateActivityEmbed(Activity activity, ActivityUpdateReason updateReason)
{
// Get channel
if (Context.Guild.GetChannel(activity.ChannelId) is not SocketTextChannel channel)
{
return;
}
await _activityHelper.UpdateActivityEmbed(channel, activity, updateReason);
}
}

View File

@@ -0,0 +1,42 @@
using Cocotte.Modules.Activities.Models;
using Cocotte.Utils;
using Discord;
using Discord.Interactions;
namespace Cocotte.Modules.Activities;
#if DEBUG
public partial class ActivityModule
{
[MessageCommand("Add role player")]
public async Task AddRolePlayer(IMessage message)
{
if (message is IUserMessage userMessage && userMessage.Author.IsBot)
{
if (await _activitiesRepository.FindActivity(Context.Guild.Id, Context.Channel.Id, message.Id) is { } activity)
{
// Generate random player
var player = new ActivityRolePlayer
{
Activity = activity,
Name = $"Player{Random.Shared.Next(1, 100)}",
UserId = (ulong) Random.Shared.NextInt64(),
Roles = (PlayerRoles) Random.Shared.Next((int)PlayerRoles.Helper, (int) (PlayerRoles.Dps | PlayerRoles.Helper |
PlayerRoles.Support | PlayerRoles.Tank) + 1 - (int)PlayerRoles.Helper)
};
// Add the player to the activity
activity.ActivityPlayers.Add(player);
await _activitiesRepository.SaveChanges();
await UpdateActivityEmbed(activity, ActivityUpdateReason.PlayerJoin);
}
}
await RespondAsync(
embed: EmbedUtils.SuccessEmbed($"Successfully added a player").Build(),
ephemeral: true
);
}
}
#endif

View File

@@ -0,0 +1,361 @@
using System.Text;
using Cocotte.Modules.Activities.Models;
using Cocotte.Utils;
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
namespace Cocotte.Modules.Activities;
public partial class ActivityModule
{
private string GetStartMessage(ActivityName activityName, string creatorName) =>
$"""
**― Bienvenue ―**
Bienvenue sur le thread lié à l'activité **{_activityFormatter.FormatActivityName(activityName)}** de **{creatorName}**
Ici, vous pouvez **discuter** de l'activité, mais aussi **gérer** cette activité à l'aide de diverses **commandes**.
**― Commandes ―**
- `/activite ajouter <joueur>` - **Ajoute un joueur** à cette activité
- `/activite enlever <joueur>` - **Enlève un joueur** de cette activité
- `/activite fermer` - **Ferme l'activité**, désactive les inscriptions
- `/activite ouvrir` - **Ouvre l'activité**, réactive les inscriptions après une fermeture
- `/activite ping` - **Ping les joueurs** inscrits à cette activité
- `/activite description` - **Modifie la description** de l'activité
- `/activite etage` - Pour l'abîme du néant et l'origine de la guerre, **modifie l'étage** de l'activité
- `/activite completer` - Marquer un joueur comme ayant complété l'activité, le barrant dans la liste des inscrits
""";
private async Task<ulong> CreateThread(ActivityName activityName, string creatorName)
{
var channel = (SocketTextChannel) Context.Channel;
var message = await GetOriginalResponseAsync();
// Create thread
var thread = await channel.CreateThreadAsync(
$"{_activityFormatter.FormatActivityName(activityName)} - {creatorName}", ThreadType.PublicThread,
ThreadArchiveDuration.OneHour,
message, true
);
// Send management message
await thread.SendMessageAsync(GetStartMessage(activityName, creatorName));
// Add activity creator
await thread.AddUserAsync((IGuildUser) Context.User);
return thread.Id;
}
[SlashCommand("ajouter", "Ajouter un joueur à cette activité")]
public async Task ThreadAddPlayer(IUser user)
{
// Get activity linked to this thread
var activity = _activitiesRepository.FindActivityByThreadId(Context.Channel.Id);
if (!await CheckCommandInThread(activity) || activity is null)
{
return;
}
await AddUserToActivity(activity, (SocketGuildUser) user, self: false);
}
[SlashCommand("enlever", "Enlever un joueur de cette activité")]
public async Task ThreadRemovePlayer(IUser user)
{
// Get activity linked to this thread
var activity = _activitiesRepository.FindActivityByThreadId(Context.Channel.Id);
if (!await CheckCommandInThread(activity, checkCreator: true) || activity is null)
{
return;
}
await RemovePlayerFromActivity(activity, (SocketGuildUser) user, self: false);
}
[SlashCommand("fermer", "Fermer l'activité, désactivant les inscriptions")]
public async Task ThreadCloseActivity()
{
// Get activity linked to this thread
var activity = _activitiesRepository.FindActivityByThreadId(Context.Channel.Id);
if (!await CheckCommandInThread(activity, checkCreator: true) || activity is null)
{
return;
}
// Do nothing if already closed
if (activity.IsClosed)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.InfoEmbed("Cette activité est **déjà fermée**").Build()
);
return;
}
activity.IsClosed = true;
await _activitiesRepository.SaveChanges();
// Get activity channel to update
if (Context.Guild.GetChannel(activity.ChannelId) is ITextChannel channel)
{
await _activityHelper.UpdateActivityEmbed(channel, activity, ActivityUpdateReason.Update);
}
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.InfoEmbed("L'activité a bien été **fermée**").Build()
);
}
[SlashCommand("ouvrir", "Ouvrir l'activité, réactivant les inscriptions")]
public async Task ThreadOpenActivity()
{
// Get activity linked to this thread
var activity = _activitiesRepository.FindActivityByThreadId(Context.Channel.Id);
if (!await CheckCommandInThread(activity, checkCreator: true) || activity is null)
{
return;
}
// Do nothing if already opened
if (!activity.IsClosed)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.InfoEmbed("Cette activité est **déjà ouverte**").Build()
);
return;
}
activity.IsClosed = false;
await _activitiesRepository.SaveChanges();
// Get activity channel to update
if (Context.Guild.GetChannel(activity.ChannelId) is ITextChannel channel)
{
await _activityHelper.UpdateActivityEmbed(channel, activity, ActivityUpdateReason.Update);
}
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.InfoEmbed("L'activité a bien été **ouverte**").Build()
);
}
[SlashCommand("ping", "Ping les joueurs inscrits à cette activité")]
public async Task ThreadPingPlayers(string message = "**Appel de groupe**")
{
// Get activity linked to this thread
var activity = _activitiesRepository.FindActivityByThreadId(Context.Channel.Id);
if (!await CheckCommandInThread(activity) || activity is null)
{
return;
}
// Get user ids
var userIds = await _activitiesRepository.GetActivityPlayerIds(activity);
// Generate message
var pingMessageBuilder = new StringBuilder(message);
pingMessageBuilder.AppendLine("\n");
pingMessageBuilder.Append(string.Join(", ", userIds.Select(id => MentionUtils.MentionUser(id))));
await RespondAsync(pingMessageBuilder.ToString());
}
[SlashCommand("description", "Changer la description de l'activité")]
public async Task ThreadChangeDescription()
{
// Get activity linked to this thread
var activity = _activitiesRepository.FindActivityByThreadId(Context.Channel.Id);
if (!await CheckCommandInThread(activity, checkCreator: true) || activity is null)
{
return;
}
// Open modal
await RespondWithModalAsync<ActivityDescriptionModal>("activity description_modal");
}
[SlashCommand("etage", "Changer l'étage de l'activité pour l'abîme du néant et l'origine de la guerre")]
public async Task ThreadChangeStage([Summary("étage", "Nouvel étage, de 1 à 6 en abîme du néant, et de 1 à 25 en origine de la guerre")] [MinValue(1), MaxValue(25)] uint stage)
{
// Get activity linked to this thread
var activity = _activitiesRepository.FindActivityByThreadId(Context.Channel.Id);
if (!await CheckCommandInThread(activity, checkCreator: true) || activity is null)
{
return;
}
if (activity is not StagedActivity stagedActivity)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Cette commande n'est utilisable que pour l'abîme du néant et l'origine de la guerre").Build()
);
return;
}
// Check if stage is valid
var stageValid = (activity.Name, stage) switch
{
(ActivityName.Abyss, >= 1 and <= 6) => true,
(ActivityName.OriginsOfWar, >= 1 and <= 25) => true,
_ => false
};
if (!stageValid)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Etage invalide").Build()
);
return;
}
stagedActivity.Stage = stage;
await _activitiesRepository.SaveChanges();
await UpdateActivityEmbed(activity, ActivityUpdateReason.Update);
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.InfoEmbed($"L'activité est maintenant bien définie à **l'étage {stage}**").Build()
);
}
[SlashCommand("completer", "Marquer un jour comme ayant complété une activité, le barrant dans la liste des inscrits")]
public async Task ThreadPlayerComplete([Summary("joueur", "Le joueur qui a complété l'activité")] IUser user)
{
// Get activity linked to this thread
var activity = _activitiesRepository.FindActivityByThreadId(Context.Channel.Id);
if (!await CheckCommandInThread(activity, checkCreator: false) || activity is null)
{
return;
}
// Check if activity is organized activity
if (activity is not OrganizedActivity)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Cette commande n'est pas supporté dans ce type d'activité").Build()
);
return;
}
// Check if player is in activity
var players = await _activitiesRepository.LoadActivityPlayers(activity);
var player = players.FirstOrDefault(p => p.UserId == user.Id);
if (player is null)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Ce joueur n'est pas dans cette activité").Build()
);
return;
}
// Check if user who used the command is an organizer
var organizer = players.FirstOrDefault(p => p.UserId == User.Id);
if (organizer is not { IsOrganizer: true })
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Seul un organisateur de l'activité peut effectuer cette action").Build()
);
return;
}
player.HasCompleted = !player.HasCompleted;
await _activitiesRepository.SaveChanges();
await UpdateActivityEmbed(activity, ActivityUpdateReason.Update);
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.InfoEmbed($"L'inscription du joueur {((IGuildUser)user).DisplayName} a bien été mis à jour").Build()
);
}
[ModalInteraction("activity description_modal", ignoreGroupNames: true)]
public async Task ActivityDescriptionSubmit(ActivityDescriptionModal descriptionModal)
{
// Get activity linked to this thread
var activity = _activitiesRepository.FindActivityByThreadId(Context.Channel.Id);
if (!await CheckCommandInThread(activity, checkCreator: true) || activity is null)
{
return;
}
// Update description
activity.Description = descriptionModal.Description;
await _activitiesRepository.SaveChanges();
await UpdateActivityEmbed(activity, ActivityUpdateReason.Update);
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.InfoEmbed("**La description** a bien été changée").Build()
);
}
private async Task<bool> CheckCommandInThread(Activity? activity, bool checkCreator = false)
{
// Check if activity is not null (means we are in a valid thread)
if (activity is null)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Vous devez être dans un **thread lié à une activité** pour utiliser cette commande").Build()
);
return false;
}
if (checkCreator && User.Id != activity.CreatorUserId)
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Seul le **créateur de l'activité** a le droit dexécuter cette action").Build()
);
return false;
}
return true;
}
}
public class ActivityDescriptionModal : IModal
{
public string Title => "Nouvelle description";
[InputLabel("Description")]
[ModalTextInput("activity_description", TextInputStyle.Paragraph)]
public required string Description { get; set; }
}

View File

@@ -0,0 +1,18 @@
namespace Cocotte.Modules.Activities;
public enum ActivityName
{
Abyss,
OriginsOfWar,
Raids,
FrontierClash,
VoidRift,
JointOperation,
InterstellarExploration,
BreakFromDestiny,
CriticalAbyss,
Fishing,
Minigame,
MirroriaRace,
Event
}

View File

@@ -0,0 +1,12 @@
namespace Cocotte.Modules.Activities;
public enum ActivityType
{
Pve4Players,
Pve8Players,
Pvp8Players,
Pvp3Players,
Other8Players,
Other4Players,
OtherUnlimitedPlayers
}

View File

@@ -0,0 +1,8 @@
namespace Cocotte.Modules.Activities;
public enum ActivityUpdateReason
{
PlayerJoin,
PlayerLeave,
Update
}

View File

@@ -0,0 +1,30 @@
using Cocotte.Modules.Activities.Models;
using Cocotte.Utils;
namespace Cocotte.Modules.Activities;
public class InterstellarFormatter
{
public string FormatInterstellarColor(InterstellarColor color) => color switch
{
InterstellarColor.Black => "noire",
InterstellarColor.Blue => "bleue",
InterstellarColor.Green => "verte",
InterstellarColor.Red => "rouge",
_ => "N/A"
};
public string InterstellarColorEmote(InterstellarColor color) => color switch
{
InterstellarColor.Black => "<:gate_black:1088385899022258248>",
InterstellarColor.Blue => "<:gate_blue:1088385901324935200>",
InterstellarColor.Green => "<:gate_green:1088385902792945664>",
InterstellarColor.Red => "<:gate_red:1088385905405984859>",
_ => ""
};
public string GetColorIcon(InterstellarColor color)
{
return CdnUtils.GetAsset($"icons/tof/gate/gate_{color.ToString().ToLowerInvariant()}.webp");
}
}

View File

@@ -0,0 +1,73 @@
using Cocotte.Services;
using Microsoft.EntityFrameworkCore;
namespace Cocotte.Modules.Activities.Models;
public class ActivitiesRepository
{
private readonly CocotteDbContext _cocotteDbContext;
public ActivitiesRepository(CocotteDbContext cocotteDbContext)
{
_cocotteDbContext = cocotteDbContext;
}
public async Task<Activity?> FindActivity(ulong guildId, ulong channelId, ulong messageId)
{
return await _cocotteDbContext.Activities.FindAsync(guildId, channelId, messageId);
}
public async Task<ActivityPlayer?> FindActivityPlayer(ulong guildId, ulong channelId, ulong messageId, ulong userId)
{
return await _cocotteDbContext.ActivityPlayers.FindAsync(guildId, channelId, messageId, userId);
}
public async Task<int> ActivityPlayerCount(Activity activity) => await _cocotteDbContext.Entry(activity).Collection(a => a.ActivityPlayers).Query().CountAsync();
public async Task<IReadOnlyCollection<ActivityPlayer>> LoadActivityPlayers(Activity activity)
{
await _cocotteDbContext
.Entry(activity)
.Collection(a => a.ActivityPlayers)
.LoadAsync();
return activity.ActivityPlayers;
}
public async Task AddActivity(Activity activity)
{
await _cocotteDbContext.AddAsync(activity);
}
public async Task SaveChanges()
{
await _cocotteDbContext.SaveChangesAsync();
}
public void DeleteActivity(Activity activity)
{
_cocotteDbContext.Activities.Remove(activity);
}
public Activity? FindActivityByThreadId(ulong threadId)
{
return _cocotteDbContext.Activities.FirstOrDefault(activity => activity.ThreadId == threadId);
}
public async Task<bool> ActivityContainsUser(Activity activity, ulong userId)
{
return await _cocotteDbContext.Entry(activity)
.Collection(a => a.ActivityPlayers)
.Query()
.AnyAsync(p => p.UserId == userId);
}
public async Task<IList<ulong>> GetActivityPlayerIds(Activity activity)
{
return await _cocotteDbContext.Entry(activity)
.Collection(a => a.ActivityPlayers)
.Query()
.Select(p => p.UserId)
.ToListAsync();
}
}

View File

@@ -0,0 +1,44 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace Cocotte.Modules.Activities.Models;
[PrimaryKey(nameof(GuildId), nameof(ChannelId), nameof(MessageId))]
[Index(nameof(ThreadId))]
public class Activity
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public required ulong GuildId { get; init; }
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public required ulong ChannelId { get; init; }
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public required ulong MessageId { get; init; }
public required ulong ThreadId { get; init; }
public required ulong CreatorUserId { get; init; }
public required string CreatorDisplayName { get; init; }
public DateTime? DueDateTime { get; init; }
public string? Description { get; set; }
public required ActivityType Type { get; init; }
public required ActivityName Name { get; init; }
public required bool AreRolesEnabled { get; init; }
public required uint MaxPlayers { get; set; }
public DateTime CreationDate { get; init; } = DateTime.Now;
public bool IsClosed { get; set; }
public List<ActivityPlayer> ActivityPlayers { get; init; } = new();
[NotMapped]
public IEnumerable<ActivityPlayer> Participants => ActivityPlayers.Where(p => !p.IsOrganizer);
[NotMapped]
public IEnumerable<ActivityPlayer> Organizers => ActivityPlayers.Where(p => p.IsOrganizer);
[NotMapped]
public string JobKey => $"{GuildId}/{ChannelId}/{MessageId}";
public override string ToString()
{
return $"{nameof(MessageId)}: {MessageId}, {nameof(CreatorUserId)}: {CreatorUserId}, {nameof(Description)}: {Description}, {nameof(Type)}: {Type}, {nameof(Name)}: {Name}, {nameof(MaxPlayers)}: {MaxPlayers}";
}
}

View File

@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace Cocotte.Modules.Activities.Models;
[PrimaryKey(nameof(GuildId), nameof(ChannelId), nameof(MessageId), nameof(UserId))]
public class ActivityPlayer
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public required ulong UserId { get; init; }
public required string Name { get; init; }
public bool IsOrganizer { get; init; }
public bool HasCompleted { get; set; }
public ulong GuildId { get; set; }
public ulong ChannelId { get; set; }
public ulong MessageId { get; init; }
public required Activity Activity { get; init; }
public override string ToString()
{
return $"{nameof(UserId)}: {UserId}, {nameof(Name)}: {Name}";
}
}

View File

@@ -0,0 +1,6 @@
namespace Cocotte.Modules.Activities.Models;
public class ActivityRolePlayer : ActivityPlayer
{
public required PlayerRoles Roles { get; init; }
}

View File

@@ -0,0 +1,11 @@
namespace Cocotte.Modules.Activities.Models;
public class InterstellarActivity : Activity
{
public required InterstellarColor Color { get; init; }
public override string ToString()
{
return $"{base.ToString()}, {nameof(InterstellarColor)}: {Color}";
}
}

View File

@@ -0,0 +1,15 @@
using Discord.Interactions;
namespace Cocotte.Modules.Activities.Models;
public enum InterstellarColor
{
[ChoiceDisplay("Verte")]
Green,
[ChoiceDisplay("Rouge")]
Red,
[ChoiceDisplay("Bleue")]
Blue,
[ChoiceDisplay("Noire")]
Black
}

View File

@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Cocotte.Modules.Activities.Models;
public class OrganizedActivity : Activity
{
}

View File

@@ -0,0 +1,11 @@
namespace Cocotte.Modules.Activities.Models;
public class StagedActivity : Activity
{
public uint Stage { get; set; }
public override string ToString()
{
return $"{base.ToString()}, {nameof(Stage)}: {Stage}";
}
}

View File

@@ -0,0 +1,12 @@
namespace Cocotte.Modules.Activities;
[Flags]
public enum PlayerRoles : byte
{
None = 0b0000_0000,
Helper = 0b0000_0001,
Dps = 0b0000_0010,
Tank = 0b0000_0100,
Support = 0b0000_1000,
Organizer = 0b0001_0000
}

View File

@@ -0,0 +1,59 @@
using Cocotte.Options;
using Discord;
using Discord.WebSocket;
using Microsoft.Extensions.Options;
namespace Cocotte.Modules.CompositeRoles;
public class CompositeRolesListener
{
private readonly ILogger<CompositeRolesListener> _logger;
private readonly IReadOnlyDictionary<ulong, GuildCompositeRoles[]> _compositeRoles;
public CompositeRolesListener(ILogger<CompositeRolesListener> logger,
IOptions<CompositeRolesOptions> compositeRolesOptions)
{
_logger = logger;
_compositeRoles = compositeRolesOptions.Value.CompositeRoles;
}
public async Task UserUpdated(Cacheable<SocketGuildUser, ulong> cacheable, SocketGuildUser guildUser)
{
// Fetch composite roles for this guild
if (!_compositeRoles.TryGetValue(guildUser.Guild.Id, out var guildCompositeRoles))
{
return;
}
_logger.LogTrace("Guild {Guild} has at least one composite role, checking for user {User}", guildUser.Guild.Name, guildUser.DisplayName);
// Check roles for each composite roles
var roles = guildUser.Roles;
foreach (var compositeRole in guildCompositeRoles)
{
// If the user has the target role, check if we need to remove it
if (roles.FirstOrDefault(r => r.Id == compositeRole.TargetRoleId) is { } presentTargetRole)
{
// Check that the user no associated role
if (!roles.Any(r => compositeRole.CompositeRolesIds.Contains(r.Id)))
{
await guildUser.RemoveRoleAsync(presentTargetRole);
_logger.LogInformation("CompositeRoles removed role {Role} from {User}", presentTargetRole.Name, guildUser.DisplayName);
}
}
// It the user doesn't have the target role, check if we need to add it
else
{
// Check that the user has at least one of the desired roles
if (roles.Any(r => compositeRole.CompositeRolesIds.Contains(r.Id)))
{
var missingTargetRole = guildUser.Guild.GetRole(compositeRole.TargetRoleId);
await guildUser.AddRoleAsync(missingTargetRole);
_logger.LogInformation("CompositeRoles added role {Role} from {User}", missingTargetRole.Name, guildUser.DisplayName);
}
}
}
}
}

View File

@@ -0,0 +1,88 @@
using Cocotte.Options;
using Cocotte.Utils;
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.Extensions.Options;
namespace Cocotte.Modules.CompositeRoles;
[Group("autoroles", "Commandes liées aux rôles composés")]
public class CompositeRolesModule : InteractionModuleBase<SocketInteractionContext>
{
private readonly ILogger<CompositeRolesModule> _logger;
private readonly CompositeRolesOptions _compositeRolesOptions;
public CompositeRolesModule(ILogger<CompositeRolesModule> logger, IOptions<CompositeRolesOptions> options)
{
_logger = logger;
_compositeRolesOptions = options.Value;
}
[RequireOwner]
[SlashCommand("fix", "Réattribuer les roles composés")]
public async Task FixRoles()
{
var count = 0;
await RespondAsync("***`Checking autoroles for guild users...`***", ephemeral: true);
// Check if there's composite roles for this guild
if (!_compositeRolesOptions.CompositeRoles.TryGetValue(Context.Guild.Id, out var compositeRoles))
{
await ModifyOriginalResponseAsync(properties =>
{
properties.Content = "";
properties.Embed = EmbedUtils.InfoEmbed("Il n'y a pas de rôle composé pour ce serveur").Build();
});
return;
}
// Check for all guild members
await Context.Guild.DownloadUsersAsync();
foreach (var guildUser in Context.Guild.Users)
{
var roles = ((SocketGuildUser)guildUser).Roles;
// Check for each target role if they have at least one of the composite roles
foreach (var compositeRole in compositeRoles)
{
// If the user has the target role, check if we need to remove it
if (roles.FirstOrDefault(r => r.Id == compositeRole.TargetRoleId) is { } presentTargetRole)
{
// Check that the user no associated role
if (!roles.Any(r => compositeRole.CompositeRolesIds.Contains(r.Id)))
{
await guildUser.RemoveRoleAsync(presentTargetRole);
_logger.LogInformation("CompositeRoles removed role {Role} from {User}", presentTargetRole.Name, guildUser.DisplayName);
count++;
}
}
// It the user doesn't have the target role, check if we need to add it
else
{
// Check that the user has at least one of the desired roles
if (roles.Any(r => compositeRole.CompositeRolesIds.Contains(r.Id)))
{
var missingTargetRole = guildUser.Guild.GetRole(compositeRole.TargetRoleId);
await guildUser.AddRoleAsync(missingTargetRole);
_logger.LogInformation("CompositeRoles added role {Role} from {User}", missingTargetRole.Name, guildUser.DisplayName);
count++;
}
}
}
}
await ModifyOriginalResponseAsync(properties =>
{
properties.Content = "";
properties.Embed = EmbedUtils.InfoEmbed($"Les rôles de **{count}** utilisateurs ont été mis à jour").Build();
});
}
}

View File

@@ -0,0 +1,239 @@
#if DEBUG
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.InteropServices;
using Cocotte.Services;
using Cocotte.Utils;
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
namespace Cocotte.Modules.Ping;
/// <summary>
/// Module containing different test and debug commands
/// </summary>
[RequireOwner]
[Group("ping", "Debug related commands")]
public class PingModule : InteractionModuleBase<SocketInteractionContext>
{
private readonly ILogger<PingModule> _logger;
private static readonly SemaphoreSlim CounterWait = new(0);
public PingModule(ILogger<PingModule> logger)
{
_logger = logger;
}
[SlashCommand("runtime-info", "Get runtime info")]
public async Task RuntimeInfo()
{
var embed = new EmbedBuilder()
.WithTitle("Cocotte runtime info")
.WithColor(0x3196c8)
.WithFields(
new EmbedFieldBuilder()
.WithName(":computer: OS")
.WithValue(RuntimeInformation.OSDescription)
.WithIsInline(true),
new EmbedFieldBuilder()
.WithName(":pencil: .Net info")
.WithValue(RuntimeInformation.FrameworkDescription)
.WithIsInline(true),
new EmbedFieldBuilder()
.WithName(":mirror_ball: Discord.Net info")
.WithValue(Assembly.GetAssembly(typeof(Discord.Net.BucketId))!.ToString())
.WithIsInline(false)
);
await RespondAsync(embed: embed.Build());
}
[SlashCommand("ping", "Check if Coco is alive and get latency")]
public async Task Ping()
{
_logger.LogTrace("[Ping/ping] Received ping command");
await RespondAsync($":ping_pong: It took me {Context.Client.Latency}ms to respond to you!");
}
[SlashCommand("echo", "Repeat the input")]
public async Task Echo(string echo, [Summary(description: "mention the user")] bool mention = false)
{
_logger.LogTrace("[Ping/echo] Received ping command with arg: {{ echo:'{Echo}', mention:{Mention} }}", echo, mention);
await RespondAsync($"{echo} {(mention ? Context.User.Mention : string.Empty)}");
}
// This command will greet target user in the channel this was executed in.
[UserCommand("Greet")]
public async Task GreetUserAsync(IUser user)
{
await RespondAsync($":wave: {Context.User.Username} said hi to you, <@{user.Id}>!");
}
[SlashCommand("summon", "Summon Coco to test if she can see and write to this channel")]
public async Task Summon()
{
_logger.LogTrace("{User} summoned Coco in {Channel}", Context.User.Username, Context.Channel.Name);
await RespondAsync(":white_check_mark: Coco at your service!");
}
[SlashCommand("reply", "Reply to a message")]
public async Task Reply(string channelId, string messageId)
{
var channel = Context.Client.GetChannel(ulong.Parse(channelId)) as ISocketMessageChannel;
var message = await channel?.GetMessageAsync(ulong.Parse(messageId))!;
if (message is IUserMessage)
{
await message.Channel.SendMessageAsync("What can I do for you?",
messageReference: new MessageReference(message.Id));
await RespondAsync("Reply successfully sent", ephemeral: true);
}
else
{
await RespondAsync("An error occured while sending message", ephemeral: true);
}
}
// Pins a message in the channel it is in.
[MessageCommand("Pin")]
public async Task PinMessageAsync(IMessage message)
{
// make a safety cast to check if the message is ISystem- or IUserMessage
if (message is not IUserMessage userMessage)
await RespondAsync(":x: You cant pin system messages!");
// if the pins in this channel are equal to or above 50, no more messages can be pinned.
else if ((await Context.Channel.GetPinnedMessagesAsync()).Count >= 50)
await RespondAsync(":x: You cant pin any more messages, the max has already been reached in this channel!");
else
{
await userMessage.PinAsync();
await RespondAsync(":white_check_mark: Successfully pinned message!");
}
}
[SlashCommand("emote", "Test sending an emote")]
public async Task SendEmote(string emoteText)
{
if (Emote.TryParse(emoteText, out var emote))
{
await RespondAsync($"{emote}/{emoteText}: `{emoteText}/{emote}`");
}
else
{
await RespondAsync(embed: EmbedUtils.ErrorEmbed("Couldn't parse the emote").Build());
}
}
[SlashCommand("emoji", "Test sending an emoji")]
public async Task SendEmoji(string emojiText)
{
if (Emoji.TryParse(emojiText, out var emoji))
{
await RespondAsync($"{emoji}/{emojiText}: `{emojiText}/{emoji}`");
}
else
{
await RespondAsync(embed: EmbedUtils.ErrorEmbed("Couldn't parse the emoji").Build());
}
}
[SlashCommand("test-button", "Test buttons components")]
public async Task TestButton()
{
var component = new ComponentBuilder()
.WithButton("Button1", $"button:1:{Context.User.Username}")
.WithButton("Button2", $"button:2:{Context.User.Username}")
.AddRow(new ActionRowBuilder()
.WithButton("Button3", $"button:3:{Context.User.Username}"));
await RespondAsync(components: component.Build());
}
[ComponentInteraction("button:*:*")]
public async Task TestButtonClick(int buttonId, string userName)
{
await RespondAsync($"{userName} clicked on button: {buttonId}");
}
[SlashCommand("select-test", "Test menu select")]
public async Task SelectTest()
{
var menuBuilder = new SelectMenuBuilder()
.WithPlaceholder("Select an option")
.WithCustomId("menu-1")
.WithMinValues(1)
.WithMaxValues(1)
.AddOption("Option A", "opt-a", "Option B is lying!")
.AddOption("Option B", "opt-b", "Option A is telling the truth!");
var builder = new ComponentBuilder()
.WithSelectMenu(menuBuilder);
await RespondAsync("Whos really lying?", components: builder.Build());
}
[ComponentInteraction("menu-1")]
public async Task TestMenuSelected(string[] selectedChoice)
{
await RespondAsync($"You have selected: {string.Join(", ", selectedChoice)}");
}
[SlashCommand("modal-test", "Test a modal")]
public async Task ModalTest()
{
await RespondWithModalAsync<FoodModal>("food_menu");
}
[ModalInteraction("food_menu")]
public async Task FoodMenuSubmit(FoodModal modal)
{
// Check if "Why??" field is populated
string reason = string.IsNullOrWhiteSpace(modal.Reason)
? "."
: $" because {modal.Reason}";
// Build the message to send.
string message = "hey @everyone, I just learned " +
$"{Context.User.Mention}'s favorite food is " +
$"{modal.Food}{reason}";
// Specify the AllowedMentions so we don't actually ping everyone.
AllowedMentions mentions = new()
{
AllowedTypes = AllowedMentionTypes.Users
};
// Respond to the modal.
await RespondAsync(message, allowedMentions: mentions, ephemeral: true);
}
}
// Defines the modal that will be sent.
public class FoodModal : IModal
{
public string Title => "Fav Food";
// Strings with the ModalTextInput attribute will automatically become components.
[NotNull]
[InputLabel("What??")]
[ModalTextInput("food_name", placeholder: "Pizza", maxLength: 20)]
public string? Food { get; set; }
// Additional paremeters can be specified to further customize the input.
// Parameters can be optional
[RequiredInput(false)]
[InputLabel("Why??")]
[ModalTextInput("food_reason", TextInputStyle.Paragraph, "Kuz it's tasty", maxLength: 500)]
public required string Reason { get; set; }
}
#endif

View File

@@ -1,176 +0,0 @@
#if DEBUG
using Cocotte.Services;
using Discord;
using Discord.Interactions;
namespace Cocotte.Modules;
/// <summary>
/// Module containing different test and debug commands
/// </summary>
[RequireOwner]
public class PingModule : InteractionModuleBase<SocketInteractionContext>
{
private readonly ILogger<PingModule> _logger;
private readonly SharedCounter _sharedCounter;
private readonly TransientCounter _transientCounter;
private static readonly SemaphoreSlim CounterWait = new(0);
public PingModule(ILogger<PingModule> logger, SharedCounter sharedCounter, TransientCounter transientCounter)
{
_logger = logger;
_sharedCounter = sharedCounter;
_transientCounter = transientCounter;
}
[SlashCommand("ping", "Check if Coco is alive and get latency")]
public async Task Ping()
{
_logger.LogTrace("[Ping/ping] Received ping command");
await RespondAsync($":ping_pong: It took me {Context.Client.Latency}ms to respond to you!");
}
[SlashCommand("echo", "Repeat the input")]
public async Task Echo(string echo, [Summary(description: "mention the user")] bool mention = false)
{
_logger.LogTrace("[Ping/echo] Received ping command with arg: {{ echo:'{Echo}', mention:{Mention} }}", echo, mention);
await RespondAsync($"{echo} {(mention ? Context.User.Mention : string.Empty)}");
}
// This command will greet target user in the channel this was executed in.
[UserCommand("Greet")]
public async Task GreetUserAsync(IUser user)
{
await RespondAsync($":wave: {Context.User.Username} said hi to you, <@{user.Id}>!");
}
// Pins a message in the channel it is in.
[MessageCommand("Pin")]
public async Task PinMessageAsync(IMessage message)
{
// make a safety cast to check if the message is ISystem- or IUserMessage
if (message is not IUserMessage userMessage)
await RespondAsync(":x: You cant pin system messages!");
// if the pins in this channel are equal to or above 50, no more messages can be pinned.
else if ((await Context.Channel.GetPinnedMessagesAsync()).Count >= 50)
await RespondAsync(":x: You cant pin any more messages, the max has already been reached in this channel!");
else
{
await userMessage.PinAsync();
await RespondAsync(":white_check_mark: Successfully pinned message!");
}
}
[SlashCommand("test-button", "Test buttons components")]
public async Task TestButton()
{
var component = new ComponentBuilder()
.WithButton("Button1", $"button:1:{Context.User.Username}")
.WithButton("Button2", $"button:2:{Context.User.Username}")
.AddRow(new ActionRowBuilder()
.WithButton("Button3", $"button:3:{Context.User.Username}"));
await RespondAsync(components: component.Build());
}
[ComponentInteraction("button:*:*")]
public async Task TestButtonClick(int buttonId, string userName)
{
await RespondAsync($"{userName} clicked on button: {buttonId}");
}
[SlashCommand("counter-shared", "Spawn a shared counter")]
public async Task CounterShared()
{
var component = new ComponentBuilder()
.WithButton("Increment", "increment_shared");
await RespondAsync($"Counter: {_sharedCounter.Count}", components: component.Build());
}
[ComponentInteraction("increment_shared")]
public async Task SharedCounterIncrement()
{
_logger.LogTrace("Received increment on shared counter");
_sharedCounter.Count++;
try
{
await (Context.Interaction as IComponentInteraction)!.UpdateAsync(msg =>
{
msg.Content = $"Counter: {_sharedCounter.Count}";
});
}
catch (Exception e)
{
_logger.LogError(e, "An error occured while updating original message:");
}
await RespondAsync();
}
[SlashCommand("counter-transient", "Spawn a transient counter")]
public async Task CounterTransient()
{
var component = new ComponentBuilder()
.WithButton("Increment", "increment_transient");
await RespondAsync($"Counter: {_transientCounter.Count}", components: component.Build());
}
[ComponentInteraction("increment_transient")]
public async Task TransientCounterIncrement()
{
_logger.LogTrace("Received increment on transient counter");
_transientCounter.Count++;
try
{
await (Context.Interaction as IComponentInteraction)!.UpdateAsync(msg =>
{
msg.Content = $"Counter: {_transientCounter.Count}";
});
}
catch (Exception e)
{
_logger.LogError(e, "An error occured while updating original message:");
}
await RespondAsync();
}
[SlashCommand("counter-transient-wait", "Spawn a transient counter using wait")]
public async Task CounterTransientWait()
{
var component = new ComponentBuilder()
.WithButton("Increment", "increment_transient_wait");
await RespondAsync($"Counter: {_transientCounter.Count}", components: component.Build());
var response = await GetOriginalResponseAsync();
while (true)
{
// Wait for the button to be clicked
_logger.LogTrace("Waiting for semaphore release");
await CounterWait.WaitAsync();
_logger.LogTrace("Received increment on transient wait counter");
_transientCounter.Count++;
await ModifyOriginalResponseAsync(m => m.Content = $"Counter: {_transientCounter.Count}");
}
}
[ComponentInteraction("increment_transient_wait")]
public async Task WaitCounterIncrement()
{
CounterWait.Release();
await RespondAsync();
}
}
#endif

View File

@@ -0,0 +1,9 @@
using System.Diagnostics.CodeAnalysis;
namespace Cocotte.Modules.Raids;
public interface IPlayerInfosRepository
{
bool TryGetPlayerInfo(ulong playerId, [MaybeNullWhen(false)] out PlayerInfo playerInfo);
void UpdatePlayerInfo(ulong id, Action<PlayerInfo> updater);
}

View File

@@ -0,0 +1,12 @@
using System.Diagnostics.CodeAnalysis;
namespace Cocotte.Modules.Raids;
public interface IRaidsRepository
{
Raid this[ulong raidId] { get; }
bool AddNewRaid(ulong raidId, DateTime dateTime);
bool TryGetRaid(ulong raidId, [MaybeNullWhen(false)] out Raid raid);
}

View File

@@ -0,0 +1,33 @@
using System.Diagnostics.CodeAnalysis;
namespace Cocotte.Modules.Raids;
public class MemoryPlayerInfosRepository : IPlayerInfosRepository
{
private readonly IDictionary<ulong, PlayerInfo> _playerInfos;
public MemoryPlayerInfosRepository()
{
_playerInfos = new Dictionary<ulong, PlayerInfo>();
}
public bool TryGetPlayerInfo(ulong playerId, [MaybeNullWhen(false)] out PlayerInfo playerInfo)
{
return _playerInfos.TryGetValue(playerId, out playerInfo);
}
public void UpdatePlayerInfo(ulong id, Action<PlayerInfo> updater)
{
if (_playerInfos.TryGetValue(id, out var playerInfo))
{
updater(playerInfo);
}
else
{
playerInfo = new PlayerInfo(id, 0);
updater(playerInfo);
_playerInfos[playerInfo.Id] = playerInfo;
}
}
}

View File

@@ -0,0 +1,25 @@
using System.Diagnostics.CodeAnalysis;
namespace Cocotte.Modules.Raids;
public class MemoryRaidRepository : IRaidsRepository
{
private readonly Dictionary<ulong, Raid> _raids;
public Raid this[ulong raidId] => _raids[raidId];
public MemoryRaidRepository()
{
_raids = new Dictionary<ulong, Raid>();
}
public bool AddNewRaid(ulong raidId, DateTime dateTime)
{
return _raids.TryAdd(raidId, new Raid(raidId, dateTime));
}
public bool TryGetRaid(ulong raidId, [MaybeNullWhen(false)] out Raid raid)
{
return _raids.TryGetValue(raidId, out raid);
}
}

View File

@@ -0,0 +1,32 @@
namespace Cocotte.Modules.Raids;
public class PlayerInfo
{
public static TimeSpan FcUpdateInterval { get; } = TimeSpan.FromDays(3);
public ulong Id { get; }
public PlayerRole PreferredRole { get; set; } = PlayerRole.Dps;
private uint _fc;
public uint Fc
{
get => _fc;
set
{
_fc = value;
_lastFcUpdate = DateTime.Today;
}
}
public bool IsFcUpdateRequired => DateTime.Today - _lastFcUpdate > FcUpdateInterval;
private DateTime _lastFcUpdate;
public PlayerInfo(ulong id, uint fc)
{
Id = id;
Fc = fc;
_lastFcUpdate = DateTime.Today;
}
}

View File

@@ -0,0 +1,8 @@
namespace Cocotte.Modules.Raids;
public enum PlayerRole
{
Dps,
Tank,
Healer
}

View File

@@ -0,0 +1,73 @@
using Cocotte.Utils;
namespace Cocotte.Modules.Raids;
public class Raid
{
public ulong Id { get; }
public DateTime DateTime { get; }
public IEnumerable<IGrouping<int, RosterPlayer>> Rosters => _players.Select(p => p.Value).GroupBy(p => p.RosterNumber);
private readonly IDictionary<ulong, RosterPlayer> _players = new Dictionary<ulong, RosterPlayer>();
public Raid(ulong id, DateTime dateTime)
{
Id = id;
DateTime = dateTime;
}
public bool AddPlayer(RosterPlayer rosterPlayer)
{
// TODO add logic to split player in multiple rosters
rosterPlayer.RosterNumber = 1;
return _players.TryAdd(rosterPlayer.Id, rosterPlayer);
}
public bool UpdatePlayer(RosterPlayer rosterPlayer)
{
if (!_players.ContainsKey(rosterPlayer.Id))
{
return false;
}
_players[rosterPlayer.Id] = rosterPlayer;
return true;
}
public RosterPlayer GetPlayer(ulong id)
{
return _players[id];
}
public bool ContainsPlayer(ulong userId)
{
return _players.ContainsKey(userId);
}
public bool RemovePlayer(ulong id)
{
return _players.Remove(id);
}
public void AssignRosters(RosterAssigner assigner)
{
assigner.AssignRosters(_players.Values, 8);
}
public override bool Equals(object? other)
{
return other is Raid roster && roster.Id == Id;
}
public override int GetHashCode()
{
return (int) (Id % int.MaxValue);
}
public override string ToString()
{
return $"Raid({DateTime})";
}
}

View File

@@ -0,0 +1,60 @@
using System.Numerics;
using Cocotte.Options;
using Cocotte.Utils;
using Discord;
namespace Cocotte.Modules.Raids;
public class RaidFormatter
{
private readonly RolesOptions _rolesOptions;
public RaidFormatter(RolesOptions rolesOptions)
{
_rolesOptions = rolesOptions;
}
public IEmote RoleToEmote(PlayerRole role) => role switch
{
PlayerRole.Dps => _rolesOptions.DpsEmote.ToEmote(),
PlayerRole.Tank => _rolesOptions.TankEmote.ToEmote(),
PlayerRole.Healer => _rolesOptions.HealerEmote.ToEmote(),
_ => ":question:".ToEmote()
};
public static string FcFormat<T>(T fc) where T : IBinaryInteger<T> => fc switch
{
< 1_000 => $"{fc}",
_ => $"{fc/T.CreateChecked(1000)}k"
};
public string FormatRosterPlayer(RosterPlayer player) => player.Substitute switch
{
false => $"{RoleToEmote(player.Role)} {player.Name} ({FcFormat(player.Fc)} FC)",
true => $"*{RoleToEmote(player.Role)} {player.Name} ({FcFormat(player.Fc)} FC)*"
};
public EmbedBuilder RaidEmbed(Raid raid)
{
EmbedFieldBuilder RosterEmbed(int rosterNumber, IEnumerable<RosterPlayer> players)
{
var rosterPlayers = players.OrderByDescending(p => p.Role).ThenByDescending(p => p.Fc).ToList();
var nonSubstitute = rosterPlayers.Where(p => !p.Substitute);
var substitute = rosterPlayers.Where(p => p.Substitute);
var separatorLength = players.Select(p => p.Name.Length).Max();
separatorLength = (int) ((separatorLength + 5) * 1.31); // Don't ask why, it just works
return new EmbedFieldBuilder()
.WithName($"Roster {rosterNumber} ({FcFormat(nonSubstitute.Sum(p => p.Fc))} FC)")
.WithValue($"{string.Join("\n", nonSubstitute.Select(FormatRosterPlayer))}\n{new string('━', separatorLength)}\n{string.Join("\n", substitute.Select(FormatRosterPlayer))}")
.WithIsInline(true);
}
return new EmbedBuilder()
.WithColor(Colors.CocotteBlue)
.WithTitle(":crossed_swords: Raid")
.WithDescription($"**Date:** {TimestampTag.FromDateTime(raid.DateTime, TimestampTagStyles.LongDateTime)}")
.WithFields(raid.Rosters.OrderBy(r => r.Key).Select(r => RosterEmbed(r.Key, r)));
}
}

View File

@@ -0,0 +1,358 @@
using System.Diagnostics.CodeAnalysis;
using Cocotte.Utils;
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
// ReSharper disable UnusedMember.Global
namespace Cocotte.Modules.Raids;
[Group("raid", "Raid related commands")]
[SuppressMessage("Performance", "CA1822:Mark members as static")]
public partial class RaidModule : InteractionModuleBase<SocketInteractionContext>
{
private readonly ILogger<RaidModule> _logger;
private readonly IRaidsRepository _raids;
private readonly IPlayerInfosRepository _playerInfos;
private readonly RaidFormatter _raidFormatter;
private readonly RaidRegisterManager _registerManager;
private readonly RosterAssigner _rosterAssigner;
public RaidModule(ILogger<RaidModule> logger, IRaidsRepository raids, IPlayerInfosRepository playerInfos,
RaidFormatter raidFormatter, RaidRegisterManager registerManager, RosterAssigner rosterAssigner)
{
_logger = logger;
_raids = raids;
_playerInfos = playerInfos;
_raidFormatter = raidFormatter;
_registerManager = registerManager;
_rosterAssigner = rosterAssigner;
}
[EnabledInDm(false)]
[SlashCommand("start", "Start a raid formation")]
public async Task Start(DayOfWeek day, string time)
{
// Check if time is valid
if (!TimeOnly.TryParse(time, out var timeOnly))
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Invalid time, try using format 'hh:mm'").Build()
);
return;
}
// Raids are identified using their original message id
await RespondAsync("`Creating a new raid...`");
var response = await GetOriginalResponseAsync();
var raidId = response.Id;
_logger.LogInformation("Created new raid with id {RaidId}", raidId);
// Calculate date
var date = DateTime.Today;
date = date.AddDays(DateTimeUtils.CalculateDayOfWeekOffset(date.DayOfWeek, day))
.Add(timeOnly.ToTimeSpan());
// New raid instance
if (!_raids.AddNewRaid(raidId, date))
{
// A raid with this message id already exists, how??
_logger.LogWarning("Tried to create a new raid with already existing id: {RaidId}", raidId);
await FollowupAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Can't create a new raid with same raid id").Build()
);
await DeleteOriginalResponseAsync();
return;
}
// Build the raid message
var raid = _raids[raidId];
var components = RaidComponents(raidId);
var embed = _raidFormatter.RaidEmbed(raid);
await ModifyOriginalResponseAsync(m =>
{
m.Content = "";
m.Components = components.Build();
m.Embed = embed.Build();
});
}
[ComponentInteraction("raid raid_join:*", true)]
public async Task RaidJoin(ulong raidId)
{
if (!_raids.TryGetRaid(raidId, out var raid))
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("This raid does not exist").Build()
);
return;
}
var user = (IGuildUser) Context.User;
// Check if player is already registered early
if (raid.ContainsPlayer(user.Id))
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.InfoEmbed("You're already registered for this raid").Build());
return;
}
// Ask player info
_registerManager.RegisteringPlayers[(raidId, user.Id)] = new RosterPlayer(user.Id, user.DisplayName);
_logger.LogDebug("User {User} is registering for raid {Raid}", user.Username, raid);
// Add name
await RespondAsync($"Please select a role for raid", components: PlayerRoleComponent(raid, user).Build(), ephemeral: true);
}
[ComponentInteraction("raid player_select_role:*:*", ignoreGroupNames: true)]
public async Task PlayerSelectRole(ulong raidId, ulong playerId, string selectedRoleInput)
{
var selectedRole = Enum.Parse<PlayerRole>(selectedRoleInput);
if (_registerManager.RegisteringPlayers.TryGetValue((raidId, playerId), out var rosterPlayer))
{
_registerManager.RegisteringPlayers[(raidId, playerId)] = rosterPlayer with { Role = selectedRole };
// Also update preferred role
_playerInfos.UpdatePlayerInfo(playerId, p => p.PreferredRole = selectedRole);
await RespondAsync();
}
// The user is not currently registering, wonder how he got here then
else
{
await RespondAsync(ephemeral: true,
embed: EmbedUtils.ErrorEmbed("You are not registering for this raid :thinking:").Build());
}
}
[ComponentInteraction("raid player_join:*:*", ignoreGroupNames: true)]
public async Task PlayerJoinNonSubstitute(ulong raidId, ulong playerId)
{
await PlayerJoin(raidId, playerId, false);
}
[ComponentInteraction("raid player_join_substitute:*:*", ignoreGroupNames: true)]
public async Task PlayerJoinSubstitute(ulong raidId, ulong playerId)
{
await PlayerJoin(raidId, playerId, true);
}
private async Task PlayerJoin(ulong raidId, ulong playerId, bool substitute)
{
// Check if player is registering
if (!_registerManager.RegisteringPlayers.TryGetValue((raidId, playerId), out var rosterPlayer))
{
await RespondAsync(ephemeral: true,
embed: EmbedUtils.ErrorEmbed("You are not registering for this raid :thinking:").Build());
return;
}
// Check if we need to ask FC
if (!_playerInfos.TryGetPlayerInfo(playerId, out var playerInfo))
{
await Context.Interaction.RespondWithModalAsync<FcModal>($"raid modal_fc:{raidId}");
return;
}
// Player already has FC registered but it's outdated
if (playerInfo.IsFcUpdateRequired)
{
await Context.Interaction.RespondWithModalAsync<FcModal>($"raid modal_fc:{raidId}",
modifyModal: m =>
m.UpdateTextInput("fc", component => component.Value = playerInfo.Fc.FormatSpaced())
);
return;
}
// Register user for raid
await RegisterPlayer(raidId, rosterPlayer with { Fc = rosterPlayer.Fc, Substitute = substitute});
}
[ModalInteraction("raid modal_fc:*", ignoreGroupNames: true)]
public async Task ModalFcSubmit(ulong raidId, FcModal modal)
{
var playerId = Context.User.Id;
_logger.LogTrace("Received modal FC modal from {User} with value: {Fc}", Context.User.Username, modal.Fc);
// Check if player is registering
if (!_registerManager.RegisteringPlayers.TryGetValue((raidId, playerId), out var rosterPlayer))
{
await RespondAsync(ephemeral: true,
embed: EmbedUtils.ErrorEmbed("You are not registering for this raid :thinking:").Build());
return;
}
var fcInput = modal.Fc.Replace(" ", "");
if (!uint.TryParse(fcInput, out var fc))
{
await RespondAsync(ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Invalid fc, try registering again").Build());
_registerManager.RegisteringPlayers.Remove((raidId, playerId));
return;
}
_playerInfos.UpdatePlayerInfo(playerId, p => p.Fc = fc);
await RegisterPlayer(raidId, rosterPlayer with { Fc = fc });
}
private async Task RegisterPlayer(ulong raidId, RosterPlayer rosterPlayer)
{
// Check if raid exists
if (!_raids.TryGetRaid(raidId, out var raid))
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.InfoEmbed("This raid does not exist").Build());
return;
}
_registerManager.RegisteringPlayers[(raidId, Context.User.Id)] = rosterPlayer;
// Player is already registered, update info
if (!raid.AddPlayer(rosterPlayer))
{
raid.UpdatePlayer(rosterPlayer);
await UpdateRaidRosterEmbed(raid);
// await RespondAsync(
// ephemeral: true,
// embed: EmbedUtils.SuccessEmbed($"Successfully update you're role to: {rosterPlayer.Role}").Build());
await RespondAsync();
}
// It's a new player
else
{
_logger.LogInformation("User {User} joined raid {Raid}", Context.User.Username, raid);
await UpdateRaidRosterEmbed(raid);
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.SuccessEmbed($"Successfully joined the raid as {raid.GetPlayer(rosterPlayer.Id).Role}").Build()
);
}
}
[ComponentInteraction("raid raid_leave:*", ignoreGroupNames: true)]
public async Task Leave(ulong raidId)
{
if (!_raids.TryGetRaid(raidId, out var raid))
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.ErrorEmbed("This raid does not exist").Build()
);
return;
}
var user = (IGuildUser) Context.User;
if (!raid.RemovePlayer(user.Id))
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.WarningEmbed("You're not registered for this raid").Build()
);
return;
}
await UpdateRaidRosterEmbed(raid);
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.SuccessEmbed("Successfully left the raid").Build()
);
}
public async Task UpdateRaidRosterEmbed(Raid raid)
{
var message = await Context.Channel.GetMessageAsync(raid.Id);
if (message is SocketUserMessage userMessage)
{
raid.AssignRosters(_rosterAssigner);
await userMessage.ModifyAsync(
m => m.Embed = _raidFormatter.RaidEmbed(raid).Build()
);
}
}
private static ComponentBuilder RaidComponents(ulong raidId)
{
return new ComponentBuilder()
.AddRow(new ActionRowBuilder()
.WithButton(new ButtonBuilder()
.WithLabel("Join")
.WithCustomId($"raid raid_join:{raidId}")
.WithStyle(ButtonStyle.Primary)
)
.WithButton(new ButtonBuilder()
.WithLabel("Leave")
.WithCustomId($"raid raid_leave:{raidId}")
.WithStyle(ButtonStyle.Danger)
)
);
}
private ComponentBuilder PlayerRoleComponent(Raid raid, IGuildUser user)
{
var select = new SelectMenuBuilder()
.WithCustomId($"raid player_select_role:{raid.Id}:{user.Id}")
.WithMinValues(1)
.WithMaxValues(1);
// Preselect preferred role
var preferredRole = _playerInfos.TryGetPlayerInfo(user.Id, out var playerInfo) ? playerInfo.PreferredRole : PlayerRole.Dps;
foreach (var role in Enum.GetValues<PlayerRole>())
{
var roleText = role.ToString();
select.AddOption(roleText, roleText, emote: _raidFormatter.RoleToEmote(role), isDefault: role == preferredRole);
}
return new ComponentBuilder()
.AddRow(new ActionRowBuilder()
.WithSelectMenu(select)
)
.AddRow(new ActionRowBuilder()
.WithButton("Join", $"raid player_join:{raid.Id}:{user.Id}")
.WithButton("Join substitute", $"raid player_join_substitute:{raid.Id}:{user.Id}")
);
}
}
public class FcModal : IModal
{
public string Title => "Please enter your FC";
[NotNull]
[InputLabel("FC")]
[ModalTextInput("fc", placeholder: "30 000", minLength: 1, maxLength: 7)]
public string? Fc { get; set; }
}

View File

@@ -0,0 +1,115 @@
using Cocotte.Utils;
using Discord;
using Discord.Interactions;
namespace Cocotte.Modules.Raids;
#if DEBUG
public partial class RaidModule
{
[MessageCommand("Add dps")]
public async Task AddDps(IMessage message)
{
await AddTestPlayer(message, PlayerRole.Dps);
}
[MessageCommand("Add tank")]
public async Task AddTank(IMessage message)
{
await AddTestPlayer(message, PlayerRole.Tank);
}
[MessageCommand("Add healer")]
public async Task AddHealer(IMessage message)
{
await AddTestPlayer(message, PlayerRole.Healer);
}
[MessageCommand("Fill roster")]
public async Task FillRoster(IMessage message)
{
if (message is IUserMessage userMessage && userMessage.Author.IsBot)
{
if (_raids.TryGetRaid(userMessage.Id, out var raid))
{
// Add 3 healers
for (int i = 0; i < 3; i++)
{
raid.AddPlayer(new RosterPlayer(
(ulong) Random.Shared.NextInt64(),
$"Healer{Random.Shared.Next(1, 100)}",
PlayerRole.Healer,
(uint) (1000 * Random.Shared.Next(30, 60)))
);
}
// Add 3 tanks
for (int i = 0; i < 3; i++)
{
raid.AddPlayer(new RosterPlayer(
(ulong) Random.Shared.NextInt64(),
$"Tank{Random.Shared.Next(1, 100)}",
PlayerRole.Tank,
(uint) (1000 * Random.Shared.Next(30, 60)))
);
}
// Add 8 dps
for (int i = 0; i < 8; i++)
{
raid.AddPlayer(new RosterPlayer(
(ulong) Random.Shared.NextInt64(),
$"Dps{Random.Shared.Next(1, 100)}",
PlayerRole.Dps,
(uint) (1000 * Random.Shared.Next(30, 60)))
);
}
// Fill rest with substitutes
for (int i = 0; i < 6; i++)
{
raid.AddPlayer(new RosterPlayer(
(ulong) Random.Shared.NextInt64(),
$"Dps{Random.Shared.Next(1, 100)}",
PlayerRole.Dps,
(uint) (1000 * Random.Shared.Next(30, 60)),
true)
);
}
await UpdateRaidRosterEmbed(raid);
}
}
await RespondAsync(
embed: EmbedUtils.SuccessEmbed("Successfully filled the roster").Build(),
ephemeral: true
);
}
private async Task AddTestPlayer(IMessage message, PlayerRole playerRole)
{
if (message is IUserMessage userMessage && userMessage.Author.IsBot)
{
if (_raids.TryGetRaid(userMessage.Id, out var raid))
{
raid.AddPlayer(new RosterPlayer(
(ulong) Random.Shared.NextInt64(),
$"Player{Random.Shared.Next(1, 100)}",
playerRole,
(uint) (1000 * Random.Shared.Next(30, 60)),
Random.Shared.Next(0, 2) == 0)
);
await UpdateRaidRosterEmbed(raid);
}
}
await RespondAsync(
embed: EmbedUtils.SuccessEmbed($"Successfully added a {playerRole} player").Build(),
ephemeral: true
);
}
}
#endif

View File

@@ -0,0 +1,7 @@
namespace Cocotte.Modules.Raids;
public class RaidRegisterManager
{
public readonly IDictionary<(ulong raidId, ulong playerId), RosterPlayer> RegisteringPlayers =
new Dictionary<(ulong raidId, ulong playerId), RosterPlayer>();
}

View File

@@ -0,0 +1,170 @@
using Cocotte.Utils;
namespace Cocotte.Modules.Raids;
public class RosterAssigner
{
public void AssignRosters(IEnumerable<RosterPlayer> players, uint playersPerRoster)
{
// Start by grouping players
var groups = GroupPlayers(players.OrderByDescending(p => p.Fc));
// Create rosters
var neededRosters = (int)Math.Ceiling(players.Count(p => !p.Substitute) / (double)playersPerRoster);
var rosters = Enumerable.Range(0, neededRosters).Select(_ => new RosterInfo(playersPerRoster)).ToList();
// Todo Check when there's more than max players per roster
// First pass: assign healers and tanks
// Always assign to the group which have the least amount of healer/tank, biased towards healers
// Skip groups without players
var dpsGroup = new List<PlayerGroup>();
foreach (var group in groups.Where(g => !g.AllSubstitutes))
{
if (group.Players.AnyHealer())
{
var nextHealerRoster = rosters.NonFull(group.Players.Count()).MinBy(r => r.RealHealerCount(), (x, y) => x.RealHealerFc() > y.RealHealerFc() ? y : x);
nextHealerRoster.AddGroup(group);
}
else if (group.Players.AnyTank())
{
var nextTankRoster = rosters.NonFull(group.Players.Count()).MinBy(r => r.RealTankCount(), (x, y) => x.RealTankFc() < y.RealTankFc() ? x : y);
nextTankRoster.AddGroup(group);
}
// Those groups will be used to assign dps, they should still be in descending order of FC
else
{
dpsGroup.Add(group);
}
}
// Third pass: assign dps
foreach (var group in dpsGroup)
{
var nextDpsRoster = rosters.NonFull(group.Players.Count()).MinBy(r => r.TotalRealFc);
nextDpsRoster!.AddGroup(group);
}
// Last pass: do the same but with substitutes
dpsGroup = new List<PlayerGroup>();
foreach (var group in groups.Where(g => g.AllSubstitutes))
{
if (group.Substitutes.AnyHealer())
{
var nextHealerRoster = rosters.MinBy(r => r.TotalHealerCount(), (x, y) => x.TotalFc > y.TotalFc ? y : x);
nextHealerRoster.AddGroup(group);
}
else if (group.Substitutes.AnyTank())
{
var nextTankRoster = rosters.MinBy(r => r.TotalTankCount(), (x, y) => x.TotalFc < y.TotalFc ? x : y);
nextTankRoster.AddGroup(group);
}
// Those groups will be used to assign dps, they should still be in descending order of FC
else
{
dpsGroup.Add(group);
}
}
// Third pass: assign dps
foreach (var group in dpsGroup)
{
var nextDpsRoster = rosters.MinBy(r => r.TotalFc);
nextDpsRoster!.AddGroup(group);
}
// Assign rosters
for (int i = 0; i < rosters.Count; i++)
{
var roster = rosters[i];
roster.AssignRosterNumer(i + 1);
}
}
private IList<PlayerGroup> GroupPlayers(IEnumerable<RosterPlayer> players)
{
var groups = new List<PlayerGroup>();
// Todo create groups from player preferences
foreach (var rosterPlayer in players)
{
groups.Add(new PlayerGroup(rosterPlayer));
}
return groups;
}
}
public class RosterInfo
{
public long TotalRealFc => _groups.Sum(g => g.RealFc);
public long TotalFc => _groups.Sum(g => g.TotalFc);
public IEnumerable<PlayerGroup> PlayerGroups => _groups.Where(g => !g.AllSubstitutes);
public IEnumerable<PlayerGroup> SubstituteGroups => _groups.Where(g => g.AllSubstitutes);
public uint MaxPlayerCount { get; }
private readonly IList<PlayerGroup> _groups;
public RosterInfo(uint maxPlayerCount)
{
MaxPlayerCount = maxPlayerCount;
_groups = new List<PlayerGroup>();
}
public void AddGroup(PlayerGroup group)
{
_groups.Add(group);
}
public void AssignRosterNumer(int rosterNumber)
{
foreach (var group in _groups)
{
group.AssignRosterNumber(rosterNumber);
}
}
}
public class PlayerGroup
{
public long RealFc => _players.Where(p => !p.Substitute).TotalFc();
public long TotalFc => _players.TotalFc();
public bool AllSubstitutes => _players.All(p => p.Substitute);
public IEnumerable<RosterPlayer> Players => _players.Where(p => !p.Substitute);
public IEnumerable<RosterPlayer> Substitutes => _players.Where(p => p.Substitute);
private readonly IList<RosterPlayer> _players;
public PlayerGroup()
{
_players = new List<RosterPlayer>();
}
public PlayerGroup(params RosterPlayer[] players)
{
_players = players;
}
public void AddPlayer(RosterPlayer player)
{
_players.Add(player);
}
public void AssignRosterNumber(int rosterNumber)
{
foreach (var rosterPlayer in _players)
{
rosterPlayer.RosterNumber = rosterNumber;
}
}
}

View File

@@ -0,0 +1,75 @@
namespace Cocotte.Modules.Raids;
public static class RosterExtensions
{
public static bool AnyHealer(this IEnumerable<RosterPlayer> players)
{
return players.Any(p => p.Role == PlayerRole.Healer);
}
public static bool AnyTank(this IEnumerable<RosterPlayer> players)
{
return players.Any(p => p.Role == PlayerRole.Tank);
}
public static int HealerCount(this IEnumerable<RosterPlayer> players)
{
return players.Count(p => p.Role == PlayerRole.Healer);
}
public static int TankCount(this IEnumerable<RosterPlayer> players)
{
return players.Count(p => p.Role == PlayerRole.Tank);
}
public static long TotalFc(this IEnumerable<RosterPlayer> players)
{
return players.Sum(p => p.Fc);
}
public static long HealerFc(this IEnumerable<RosterPlayer> players)
{
return players.Where(p => p.Role == PlayerRole.Healer).Sum(p => p.Fc);
}
public static long TankFc(this IEnumerable<RosterPlayer> players)
{
return players.Where(p => p.Role == PlayerRole.Tank).Sum(p => p.Fc);
}
public static long RealHealerFc(this RosterInfo roster)
{
return roster.PlayerGroups.Sum(group => group.Players.HealerFc());
}
public static long RealTankFc(this RosterInfo roster)
{
return roster.PlayerGroups.Sum(group => group.Players.TankFc());
}
public static int RealHealerCount(this RosterInfo rosterInfo)
{
return rosterInfo.PlayerGroups.Sum(g => g.Players.HealerCount());
}
public static int TotalHealerCount(this RosterInfo rosterInfo)
{
return rosterInfo.PlayerGroups.Concat(rosterInfo.SubstituteGroups).Sum(g => g.Players.Concat(g.Substitutes).HealerCount());
}
public static int RealTankCount(this RosterInfo rosterInfo)
{
return rosterInfo.PlayerGroups.Sum(g => g.Players.TankCount());
}
public static int TotalTankCount(this RosterInfo rosterInfo)
{
return rosterInfo.PlayerGroups.Concat(rosterInfo.SubstituteGroups).Sum(g => g.Players.Concat(g.Substitutes).TankCount());
}
public static int TotalPlayerCount(this RosterInfo rosterInfo) =>
rosterInfo.PlayerGroups.Sum(group => group.Players.Count());
public static IEnumerable<RosterInfo> NonFull(this IEnumerable<RosterInfo> rosters, int addedPlayersCount) =>
rosters.Where(roster => roster.TotalPlayerCount() + addedPlayersCount <= roster.MaxPlayerCount);
}

View File

@@ -0,0 +1,16 @@
namespace Cocotte.Modules.Raids;
public record RosterPlayer(ulong Id, string Name, PlayerRole Role = PlayerRole.Dps, uint Fc = 0, bool Substitute = false)
{
public int RosterNumber { get; set; }
public override int GetHashCode()
{
return (int) (Id % int.MaxValue);
}
public virtual bool Equals(RosterPlayer? other)
{
return other is not null && other.Id == Id;
}
}

View File

@@ -0,0 +1,21 @@
namespace Cocotte.Options;
public class ActivityOptions
{
public const string SectionName = "ActivityOptions";
public ulong OrganizerRoleId { get; init; }
public required string OrganizerEmote { get; init; }
public ulong HelperRoleId { get; init; }
public required string HelperEmote { get; init; }
public ulong DpsRoleId { get; init; }
public required string DpsEmote { get; init; }
public ulong TankRoleId { get; init; }
public required string TankEmote { get; init; }
public ulong SupportRoleId { get; init; }
public required string SupportEmote { get; init; }
}

View File

@@ -0,0 +1,14 @@
namespace Cocotte.Options;
public class CompositeRolesOptions
{
public const string SectionName = "CompositeRolesOptions";
public required IReadOnlyDictionary<ulong, GuildCompositeRoles[]> CompositeRoles { get; init; }
}
public class GuildCompositeRoles
{
public required ulong TargetRoleId { get; init; }
public required ulong[] CompositeRolesIds { get; init; }
}

View File

@@ -0,0 +1,8 @@
namespace Cocotte.Options;
public class RolesOptions
{
public string DpsEmote { get; set; } = ":red_circle:";
public string TankEmote { get; set; } = ":yellow_circle:";
public string HealerEmote { get; set; } = ":green_circle:";
}

View File

@@ -1,26 +1,53 @@
using Cocotte;
using Cocotte.Modules.Activities;
using Cocotte.Modules.Activities.Models;
using Cocotte.Modules.CompositeRoles;
using Cocotte.Modules.Raids;
using Cocotte.Options;
using Cocotte.Services;
using Discord;
using Discord.Commands;
using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using Quartz;
DiscordSocketConfig discordSocketConfig = new()
{
LogLevel = LogSeverity.Debug,
MessageCacheSize = 200
MessageCacheSize = 200,
GatewayIntents = GatewayIntents.GuildMembers | GatewayIntents.Guilds
};
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((_, configuration) =>
{
configuration.AddJsonFile("discord.json", false, false);
configuration.AddJsonFile("activity.json", false, false);
configuration.AddJsonFile("compositeRoles.json", false, false);
})
.ConfigureServices((context, services) =>
{
// Quartz service
services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionJobFactory();
q.UsePersistentStore(options =>
{
options.UseJsonSerializer();
options.UseMicrosoftSQLite("Data Source=cocotte.db");
});
});
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
// Options
services.Configure<DiscordOptions>(context.Configuration.GetSection(DiscordOptions.SectionName));
services.Configure<ActivityOptions>(context.Configuration.GetSection(ActivityOptions.SectionName));
services.Configure<CompositeRolesOptions>(context.Configuration.GetSection(CompositeRolesOptions.SectionName));
// Database
services.AddDbContext<CocotteDbContext>(options =>
options.UseSqlite(context.Configuration.GetConnectionString("CocotteContext")));
services.AddScoped<ActivitiesRepository>();
// Discord.Net
services.AddHostedService<DiscordLoggingService>();
@@ -32,10 +59,49 @@ IHost host = Host.CreateDefaultBuilder(args)
services.AddHostedService<CocotteService>();
// Custom
services.AddSingleton<SharedCounter>();
services.AddTransient<TransientCounter>();
// Data
services.AddSingleton<IRaidsRepository, MemoryRaidRepository>();
services.AddSingleton<IPlayerInfosRepository, MemoryPlayerInfosRepository>();
services.AddSingleton<RolesOptions>();
// Activities
services.AddTransient<ActivityFormatter>();
services.AddTransient<InterstellarFormatter>();
services.AddScoped<ActivityHelper>();
services.AddScoped<ActivityCloseJob>();
// Composite roles
services.AddSingleton<CompositeRolesListener>();
// Raids
// services.AddTransient<RaidFormatter>();
// services.AddSingleton<RaidRegisterManager>();
// services.AddTransient<RosterAssigner>();
})
.Build();
// Recreate database if in development environment
await using(var scope = host.Services.CreateAsyncScope())
{
var hostEnvironment = scope.ServiceProvider.GetRequiredService<IHostEnvironment>();
var dbContext = scope.ServiceProvider.GetRequiredService<CocotteDbContext>();
if (hostEnvironment.IsDevelopment())
{
// await dbContext.Database.EnsureDeletedAsync();
await dbContext.Database.EnsureCreatedAsync();
}
else
{
// Backup database before migrations
if (File.Exists("cocotte.db"))
{
File.Copy("cocotte.db", $"cocotte.{DateTime.UtcNow:yy-MM-dd hh-mm-ss}.db");
}
// Apply migrations
await dbContext.Database.MigrateAsync();
}
}
await host.RunAsync();

View File

@@ -5,7 +5,16 @@
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
},
"workingDirectory": "bin/Debug/net7.0"
},
"Cocotte-Prod": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Production"
},
"workingDirectory": "bin/Debug/net7.0"
}
}
}

View File

@@ -0,0 +1,30 @@
using AppAny.Quartz.EntityFrameworkCore.Migrations;
using AppAny.Quartz.EntityFrameworkCore.Migrations.SQLite;
using Cocotte.Modules.Activities.Models;
using Microsoft.EntityFrameworkCore;
namespace Cocotte.Services;
public class CocotteDbContext : DbContext
{
public DbSet<Activity> Activities => Set<Activity>();
public DbSet<StagedActivity> StagedActivities => Set<StagedActivity>();
public DbSet<InterstellarActivity> InterstellarActivities => Set<InterstellarActivity>();
public DbSet<OrganizedActivity> OrganizedActivities => Set<OrganizedActivity>();
public DbSet<ActivityPlayer> ActivityPlayers => Set<ActivityPlayer>();
public DbSet<ActivityRolePlayer> ActivityRolePlayers => Set<ActivityRolePlayer>();
public CocotteDbContext(DbContextOptions<CocotteDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Adds Quartz.NET SQLite schema to EntityFrameworkCore
modelBuilder.AddQuartz(builder => builder.UseSqlite());
}
}

View File

@@ -1,4 +1,6 @@
using System.Reflection;
using Cocotte.Modules.Activities;
using Cocotte.Modules.CompositeRoles;
using Cocotte.Options;
using Discord;
using Discord.Interactions;
@@ -15,18 +17,20 @@ public class CocotteService : BackgroundService
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly DiscordSocketClient _client;
private readonly DiscordOptions _options;
private readonly ActivityOptions _activityOptions;
private readonly InteractionService _interactionService;
public CocotteService(ILogger<CocotteService> logger, IServiceProvider serviceProvider,
IHostEnvironment hostEnvironment,
IHostApplicationLifetime hostApplicationLifetime, DiscordSocketClient client,
IOptions<DiscordOptions> options, InteractionService interactionService)
IOptions<DiscordOptions> options, IOptions<ActivityOptions> groupOptions, InteractionService interactionService)
{
_logger = logger;
_serviceProvider = serviceProvider;
_hostApplicationLifetime = hostApplicationLifetime;
_client = client;
_options = options.Value;
_activityOptions = groupOptions.Value;
_interactionService = interactionService;
_hostEnvironment = hostEnvironment;
}
@@ -34,7 +38,7 @@ public class CocotteService : BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Check token first
if (_options.Token is null)
if (string.IsNullOrWhiteSpace(_options.Token))
{
_logger.LogError("Couldn't find any discord bot token, exiting...");
@@ -43,8 +47,20 @@ public class CocotteService : BackgroundService
return;
}
if (!ValidateOptions())
{
return;
}
// Initialize modules and commands
await _interactionService.AddModulesAsync(Assembly.GetEntryAssembly(), _serviceProvider);
using var scope = _serviceProvider.CreateScope();
#if DEBUG
await _interactionService.AddModuleAsync(typeof(Modules.Ping.PingModule), scope.ServiceProvider);
#endif
await _interactionService.AddModuleAsync(typeof(ActivityModule), scope.ServiceProvider);
await _interactionService.AddModuleAsync(typeof(CompositeRolesModule), scope.ServiceProvider);
_client.Ready += ClientOnReady;
_client.InteractionCreated += HandleInteraction;
@@ -53,9 +69,35 @@ public class CocotteService : BackgroundService
await _client.LoginAsync(TokenType.Bot, _options.Token);
await _client.StartAsync();
// Register events
RegisterEvents();
await Task.Delay(Timeout.Infinite, stoppingToken);
}
private void RegisterEvents()
{
var composteRolesListener = _serviceProvider.GetRequiredService<CompositeRolesListener>();
_client.GuildMemberUpdated += composteRolesListener.UserUpdated;
}
private bool ValidateOptions()
{
// Validate group options
if ((_activityOptions.HelperRoleId
| _activityOptions.DpsRoleId
| _activityOptions.TankRoleId
| _activityOptions.SupportRoleId) == 0)
{
_logger.LogError("One of the group options id is invalid, it cannot be 0");
return false;
}
return true;
}
private async Task ClientOnReady()
{
// Context & Slash commands can be automatically registered, but this process needs to happen after the client enters the READY state.
@@ -63,7 +105,7 @@ public class CocotteService : BackgroundService
if (_hostEnvironment.IsDevelopment())
{
// Check that a dev guild is set
if (!_options.DevGuildId.HasValue)
if (!_options.DevGuildId.HasValue && _options.DevGuildId!.Value != 0)
{
_logger.LogError("Couldn't find any dev guild while application is run in dev mode, exiting...");

View File

@@ -1,6 +0,0 @@
namespace Cocotte.Services;
public class SharedCounter
{
public int Count { get; set; } = 0;
}

View File

@@ -1,6 +0,0 @@
namespace Cocotte.Services;
public class TransientCounter
{
public int Count { get; set; } = 0;
}

19
Cocotte/Utils/CdnUtils.cs Normal file
View File

@@ -0,0 +1,19 @@
namespace Cocotte.Utils;
public class CdnUtils
{
/// <summary>
/// Use this to force discord media cache to fetch new content from CDN
/// </summary>
private const string QuerySuffix = $"?q={RandomSuffix}";
/// <summary>
/// Needs to be updated each time a media is updated on the CDN
/// </summary>
private const string RandomSuffix = "assets2";
public static string GetAsset(string assetName)
{
return $"https://sage.cdn.ilysix.fr/assets/Cocotte/{assetName}{QuerySuffix}";
}
}

View File

@@ -0,0 +1,16 @@
using Discord;
namespace Cocotte.Utils;
public class ChannelUtils
{
public static string GetChannelLink(IGuildChannel guildChannel)
{
return GetChannelLink(guildChannel.GuildId, guildChannel.Id);
}
public static string GetChannelLink(ulong guildId, ulong channelId)
{
return $"https://discord.com/channels/{guildId}/{channelId}";
}
}

View File

@@ -0,0 +1,21 @@
using Discord.Interactions;
namespace Cocotte.Utils;
public enum CocotteDayOfWeek
{
[ChoiceDisplay("Lundi")]
Monday = DayOfWeek.Monday,
[ChoiceDisplay("Mardi")]
Tuesday = DayOfWeek.Tuesday,
[ChoiceDisplay("Mercredi")]
Wednesday = DayOfWeek.Wednesday,
[ChoiceDisplay("Jeudi")]
Thursday = DayOfWeek.Thursday,
[ChoiceDisplay("Vendredi")]
Friday = DayOfWeek.Friday,
[ChoiceDisplay("Samedi")]
Saturday = DayOfWeek.Saturday,
[ChoiceDisplay("Dimanche")]
Sunday = DayOfWeek.Sunday
}

17
Cocotte/Utils/Colors.cs Normal file
View File

@@ -0,0 +1,17 @@
using Discord;
namespace Cocotte.Utils;
public static class Colors
{
// Main Cocotte colors
public static Color CocotteBlue => new(0x3196c8);
public static Color CocotteRed => new(0xe40808);
public static Color CocotteOrange => new(0xff6d01);
// Colors used in embeds
public static Color ErrorColor => new(0xFB6060);
public static Color InfoColor => new(0x66D9EF);
public static Color SuccessColor => new(0x2Ecc71);
public static Color WarningColor => new(0xf1c40F);
}

View File

@@ -0,0 +1,35 @@
namespace Cocotte.Utils;
public static class DateTimeUtils
{
// Source: https://stackoverflow.com/a/50033489
public static int CalculateDayOfWeekOffset(DayOfWeek current, DayOfWeek desired) {
// f( c, d ) = [7 - (c - d)] mod 7
// f( c, d ) = [7 - c + d] mod 7
// c is current day of week and 0 <= c < 7l
// d is desired day of the week and 0 <= d < 7
int c = (int)current;
int d = (int)desired;
int offset = (7 - c + d) % 7;
return offset;
}
public static DateTime NextDateWithDayAndTime(DayOfWeek desired, TimeOnly time)
{
var date = DateTime.Today;
date = date.AddDays(CalculateDayOfWeekOffset(date.DayOfWeek, desired)).WithTimeOnly(time);
// If it's previous than current date, add 7 days
return date < DateTime.Now ? date.AddDays(7) : date;
}
public static DateTime WithTimeOnly(this DateTime dateTime, TimeOnly timeOnly)
{
return new DateTime(
dateTime.Year, dateTime.Month, dateTime.Day, timeOnly.Hour, timeOnly.Minute, timeOnly.Second
);
}
public static DateTime NextDateWithTimeOfDay(TimeOnly timeOfDay) =>
DateTime.Now.TimeOfDay.Ticks > timeOfDay.Ticks ? DateTime.Now.AddDays(1).WithTimeOnly(timeOfDay) : DateTime.Now.WithTimeOnly(timeOfDay);
}

View File

@@ -0,0 +1,50 @@
using Discord;
namespace Cocotte.Utils;
public static class EmbedUtils
{
public static EmbedBuilder ErrorEmbed(string message, string title = "Erreur")
{
return new EmbedBuilder()
.WithColor(Colors.ErrorColor)
.WithAuthor(a => a
.WithName(title)
.WithIconUrl(CdnUtils.GetAsset("icons/error.webp"))
)
.WithDescription(message);
}
public static EmbedBuilder InfoEmbed(string message, string title = "Info")
{
return new EmbedBuilder()
.WithColor(Colors.InfoColor)
.WithAuthor(a => a
.WithName(title)
.WithIconUrl(CdnUtils.GetAsset("icons/info.webp"))
)
.WithDescription(message);
}
public static EmbedBuilder SuccessEmbed(string message, string title = "Succès")
{
return new EmbedBuilder()
.WithColor(Colors.SuccessColor)
.WithAuthor(a => a
.WithName(title)
.WithIconUrl(CdnUtils.GetAsset("icons/success.webp"))
)
.WithDescription(message);
}
public static EmbedBuilder WarningEmbed(string message, string title = "Attention")
{
return new EmbedBuilder()
.WithColor(Colors.WarningColor)
.WithAuthor(a => a
.WithName(title)
.WithIconUrl(CdnUtils.GetAsset("icons/warning.webp"))
)
.WithDescription(message);
}
}

View File

@@ -0,0 +1,16 @@
using Discord;
namespace Cocotte.Utils;
public static class EmoteUtils
{
public static IEmote ToEmote(this string emoteText)
{
if (Emote.TryParse(emoteText, out var emote))
{
return emote;
}
return Emoji.Parse(emoteText);
}
}

View File

@@ -0,0 +1,31 @@
namespace Cocotte.Utils;
public static class EnumerableExtensions
{
public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
Func<TSource, TSource, TSource> conflictResolver)
{
var comparer = Comparer<TKey>.Default;
var min = source.First();
var minKey = keySelector(min);
foreach (var element in source.Skip(1))
{
var key = keySelector(element);
if (comparer.Compare(key, minKey) < 0)
{
min = element;
minKey = key;
}
else if (comparer.Compare(key, minKey) == 0)
{
min = conflictResolver(min, element);
minKey = keySelector(min);
}
}
return min;
}
}

View File

@@ -0,0 +1,34 @@
using Discord;
namespace Cocotte.Utils;
public static class ModalExtensions
{
public static ModalBuilder UpdateTextInput(this ModalBuilder modal, string customId, Action<TextInputBuilder> inputUpdater)
{
var components = modal.Components.ActionRows.SelectMany(r => r.Components).OfType<TextInputComponent>();
var component = components.First(c => c.CustomId == customId);
var builder = new TextInputBuilder
{
CustomId = customId,
Label = component.Label,
MaxLength = component.MaxLength,
MinLength = component.MinLength,
Placeholder = component.Placeholder,
Required = component.Required,
Style = component.Style,
Value = component.Value
};
inputUpdater(builder);
foreach (var row in modal.Components.ActionRows.Where(row => row.Components.Any(c => c.CustomId == customId)))
{
row.Components.RemoveAll(c => c.CustomId == customId);
row.AddComponent(builder.Build());
}
return modal;
}
}

View File

@@ -0,0 +1,43 @@
using System.Numerics;
namespace Cocotte.Utils;
public static class NumberUtils
{
/// <summary>
/// Return a string representation of an <see cref="IBinaryInteger{TSelf}" /> with digits separated by spaces
/// </summary>
/// <param name="number"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static string FormatSpaced<T>(this IBinaryInteger<T> number) where T : IBinaryInteger<T>?
{
var stringNumber = number.ToString(null, null);
Span<char> result = stackalloc char[stringNumber.Length + stringNumber.Length / 3];
int resultOffset = 0;
for (int i = 0; i < stringNumber.Length; i++)
{
// Add a space
if (i > 2 && (i + 1) % 3 == 1)
{
result[resultOffset] = ' ';
resultOffset++;
}
result[resultOffset] = stringNumber[^(i + 1)];
resultOffset++;
}
var realResult = result[..resultOffset];
Span<char> reversed = stackalloc char[resultOffset];
for (int i = 0; i < reversed.Length; i++)
{
reversed[i] = realResult[^(i + 1)];
}
return reversed.ToString();
}
}

18
Cocotte/activity.json Normal file
View File

@@ -0,0 +1,18 @@
{
"ActivityOptions": {
"OrganizerRoleId": 0,
"OrganizerEmote": "",
"HelperRoleId": 0,
"HelperEmote": "",
"DpsRoleId": 0,
"DpsEmote": "",
"TankRoleId": 0,
"TankEmote": "",
"SupportRoleId": 0,
"SupportEmote": ""
}
}

View File

@@ -3,7 +3,10 @@
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information",
"Cocotte": "Trace"
"Cocotte": "Trace",
"Quartz": "Information",
"Quartz.Core.QuartzSchedulerThread": "Information",
"Quartz.Core.JobRunShell": "Information"
}
}
}

View File

@@ -4,5 +4,8 @@
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"CocotteContext": "Data Source=cocotte.db"
}
}

0
Cocotte/cocotte.db Normal file
View File

View File

@@ -0,0 +1,26 @@
{
"CompositeRolesOptions": {
"CompositeRoles": {
"someGuildId": [
{
"TargetRoleId": 1,
"CompositeRolesIds": [
0,
1,
2
]
}
],
"anotherGuildId": [
{
"TargetRoleId": 45,
"CompositeRolesIds": [
98,
1,
2
]
}
]
}
}
}

6
Cocotte/discord.json Normal file
View File

@@ -0,0 +1,6 @@
{
"DiscordOptions": {
"Token": "",
"DevGuildId": 0
}
}

7
global.json Normal file
View File

@@ -0,0 +1,7 @@
{
"sdk": {
"version": "7.0.0",
"rollForward": "latestMajor",
"allowPrerelease": true
}
}

0
identifier.sqlite Normal file
View File