Compare commits

...

26 commits

Author SHA1 Message Date
fb17ad7ad5
5.1.0 2024-06-28 17:05:04 +12:00
657805d51c
Updated changelog, version upped to 5.1.0 2024-06-28 15:19:37 +12:00
eab5344c61
Updated Services\Impl files 2024-06-28 15:16:12 +12:00
df56c76a89
Updated Help module 2024-06-27 21:08:15 +12:00
b7b8366fe2
Updated Xp module 2024-06-27 21:07:29 +12:00
a9576e5aa1
Updated CheckForUpdatesService.cs 2024-06-27 21:06:48 +12:00
fbff028271
Updated Patronage module 2024-06-27 20:22:54 +12:00
a976df5549
Updated MuteService.cs 2024-06-27 18:25:07 +12:00
b8fa164f8d
Updated Administration module 2024-06-27 18:23:14 +12:00
eb1ebc72db
Updated Expressions module 2024-06-27 17:14:55 +12:00
00f8c70de0
Updated Marmalades module 2024-06-27 16:49:50 +12:00
3a70370287
Updated Games module 2024-06-27 16:41:10 +12:00
3df4f710da
Updated the Discord server name 2024-06-27 16:40:00 +12:00
496a0cc68c
Updated things to be right 2024-06-27 16:38:10 +12:00
3181f4dcaa
Updated Gambling module 2024-06-27 16:16:52 +12:00
f9b21520fb
Updated Music module 2024-06-26 23:58:21 +12:00
4732254805
Updated Utility module 2024-06-26 23:52:29 +12:00
0466701e28
Updated Searches module 2024-06-26 23:51:35 +12:00
888994dd67
Updated Permissions module 2024-06-26 22:20:28 +12:00
a4adabb3ea
Updated common files again 2024-06-26 21:23:06 +12:00
ed7deeb99d
Updated Migrations files 2024-06-26 21:03:14 +12:00
0f927a3dd1
Updated Db files 2024-06-26 21:01:23 +12:00
a63889b2f3
Updated common files 2024-06-26 19:44:07 +12:00
b045015efb
Updated common abstractions 2024-06-26 17:59:28 +12:00
4fe4c4eff2
Added cleanup code for invalid database data in sqlite 2024-06-26 01:00:44 +12:00
404f51347d
Updated data files
The first of many commits
2024-06-26 00:56:13 +12:00
238 changed files with 12627 additions and 2130 deletions

View file

@ -2,6 +2,40 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [5.1.0] - 28.06.2024
### Added
- Added `'prompt` command, Ellie Ai Assistant
- You can send natural language questions, queries or execute commands. For example "@Ellie how's the weather in paris" and it will return `'we Paris` and run it for you.
- In case the bot can't execute a command using your query, It will fall back to your chatter bot, in case you have it enabled in data/games.yml. (Cleverbot or chatgpt)
- (It's far from perfect so please don't ask the bot to do dangerous things like banning or pruning)
- Requires Patreon subscription, after which you'll be able to run it on global @Ellie bot. If you're selfhosting, you also will need to acquire the api key from <https://dashy.elliebot.net/api> (coming soon(ish)...)
- Added support for `gpt-4o` in `data/games.yml`
- Added EllieAiToken to `creds.yml`
### Changed
- Remind will now show a timestamp tag for durations
- Only `Gpt35Turbo` and `Gpt4o` are valid inputs in games.yml now
- `data/patron.yml` changed. It now has limits. The entire feature limit system has been reworked. Your previous settings will be reset
- A lot of updates to bot strings (thanks Ene)
- Improved cleanup command to delete a lot more data once cleanup is ran, not only guild configs (please don't use this command unless you have your database bakced up and you know 100% what you're doing)
### Fixed
- Fixed xp bg buy button not working, and possibly some other buttons too
- Fixed shopbuy %user% placeholders and updated help text
- All 'feed overloads should now work"
- `'xpexclude` should will now work with forums too. If you exclude a forum you won't be able to gain xp in any of the threads.
- Fixed remind not showing correct time (thx cata)
### Removed
- Removed PoE related commands
- dev: Removed patron quota data from the database, it will now be stored in redis
## [5.0.8] - 19.06.2024 ## [5.0.8] - 19.06.2024
### Added ### Added

View file

@ -53,8 +53,6 @@ public abstract class EllieContext : DbContext
public DbSet<PatronUser> Patrons { get; set; } public DbSet<PatronUser> Patrons { get; set; }
public DbSet<PatronQuota> PatronQuotas { get; set; }
public DbSet<StreamOnlineMessage> StreamOnlineMessages { get; set; } public DbSet<StreamOnlineMessage> StreamOnlineMessages { get; set; }
public DbSet<StickyRole> StickyRoles { get; set; } public DbSet<StickyRole> StickyRoles { get; set; }
@ -597,16 +595,6 @@ public abstract class EllieContext : DbContext
}); });
// quotes are per user id // quotes are per user id
modelBuilder.Entity<PatronQuota>(pq =>
{
pq.HasIndex(x => x.UserId).IsUnique(false);
pq.HasKey(x => new
{
x.UserId,
x.FeatureType,
x.Feature
});
});
#endregion #endregion

View file

@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace EllieBot.Db.Models; namespace EllieBot.Db.Models;
public class AntiRaidSetting : DbEntity public class AntiRaidSetting : DbEntity
{ {
public int GuildConfigId { get; set; } public int GuildConfigId { get; set; }

View file

@ -1,41 +1,17 @@
#nullable disable #nullable disable
namespace EllieBot.Db.Models; namespace EllieBot.Db.Models;
/// <summary>
/// Contains data about usage of Patron-Only commands per user
/// in order to provide support for quota limitations
/// (allow user x who is pledging amount y to use the specified command only
/// x amount of times in the specified time period)
/// </summary>
public class PatronQuota
{
public ulong UserId { get; set; }
public FeatureType FeatureType { get; set; }
public string Feature { get; set; }
public uint HourlyCount { get; set; }
public uint DailyCount { get; set; }
public uint MonthlyCount { get; set; }
}
public enum FeatureType
{
Command,
Group,
Module,
Limit
}
public class PatronUser public class PatronUser
{ {
public string UniquePlatformUserId { get; set; } public string UniquePlatformUserId { get; set; }
public ulong UserId { get; set; } public ulong UserId { get; set; }
public int AmountCents { get; set; } public int AmountCents { get; set; }
public DateTime LastCharge { get; set; } public DateTime LastCharge { get; set; }
// Date Only component // Date Only component
public DateTime ValidThru { get; set; } public DateTime ValidThru { get; set; }
public PatronUser Clone() public PatronUser Clone()
=> new PatronUser() => new PatronUser()
{ {

View file

@ -4,7 +4,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.0.8</Version> <Version>5.1.0</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
@ -22,88 +22,88 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AngleSharp" Version="1.1.2"> <PackageReference Include="AngleSharp" Version="1.1.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<Publish>True</Publish> <Publish>True</Publish>
</PackageReference> </PackageReference>
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.6" /> <PackageReference Include="CodeHollow.FeedReader" Version="1.2.6"/>
<PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="CommandLineParser" Version="2.9.1"/>
<PackageReference Include="Discord.Net" Version="3.204.0" /> <PackageReference Include="Discord.Net" Version="3.204.0"/>
<PackageReference Include="CoreCLR-NCalc" Version="3.1.246" /> <PackageReference Include="CoreCLR-NCalc" Version="3.1.246"/>
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138" /> <PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138"/>
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414" /> <PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414"/>
<PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084" /> <PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084"/>
<!-- <PackageReference Include="Grpc.AspNetCore" Version="2.62.0" />--> <!-- <PackageReference Include="Grpc.AspNetCore" Version="2.62.0" />-->
<PackageReference Include="Google.Protobuf" Version="3.26.1" /> <PackageReference Include="Google.Protobuf" Version="3.26.1"/>
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.62.0" /> <PackageReference Include="Grpc.Net.ClientFactory" Version="2.62.0"/>
<PackageReference Include="Grpc.Tools" Version="2.63.0"> <PackageReference Include="Grpc.Tools" Version="2.63.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.5.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.5.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0"/>
<PackageReference Include="MorseCode.ITask" Version="2.0.3" /> <PackageReference Include="MorseCode.ITask" Version="2.0.3"/>
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0" /> <PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0"/>
<!-- DI --> <!-- DI -->
<!-- <PackageReference Include="Ninject" Version="3.3.6"/>--> <!-- <PackageReference Include="Ninject" Version="3.3.6"/>-->
<!-- <PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0"/>--> <!-- <PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0"/>-->
<!-- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />--> <!-- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />-->
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1"/>
<PackageReference Include="DryIoc.dll" Version="5.4.3" /> <PackageReference Include="DryIoc.dll" Version="5.4.3"/>
<!-- <PackageReference Include="Scrutor" Version="4.2.0" />--> <!-- <PackageReference Include="Scrutor" Version="4.2.0" />-->
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0"/>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" /> <PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="NonBlocking" Version="2.1.2" /> <PackageReference Include="NonBlocking" Version="2.1.2"/>
<PackageReference Include="OneOf" Version="3.0.263" /> <PackageReference Include="OneOf" Version="3.0.263"/>
<PackageReference Include="OneOf.SourceGenerator" Version="3.0.263" /> <PackageReference Include="OneOf.SourceGenerator" Version="3.0.263"/>
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/>
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1" /> <PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1"/>
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta17" /> <PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta17"/>
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.8" /> <PackageReference Include="SixLabors.ImageSharp" Version="2.1.8"/>
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14" /> <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14"/>
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009" /> <PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009"/>
<PackageReference Include="StackExchange.Redis" Version="2.7.33" /> <PackageReference Include="StackExchange.Redis" Version="2.7.33"/>
<PackageReference Include="YamlDotNet" Version="15.1.4" /> <PackageReference Include="YamlDotNet" Version="15.1.4"/>
<PackageReference Include="SharpToken" Version="2.0.2" /> <PackageReference Include="SharpToken" Version="2.0.3" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" /> <PackageReference Include="JetBrains.Annotations" Version="2023.3.0"/>
<!-- Db-related packages --> <!-- Db-related packages -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.4"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0" /> <PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2"/>
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" /> <PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/>
<!-- Used by stream notifications --> <!-- Used by stream notifications -->
<PackageReference Include="TwitchLib.Api" Version="3.4.1" /> <PackageReference Include="TwitchLib.Api" Version="3.4.1"/>
<!-- sqlselectcsv and stock --> <!-- sqlselectcsv and stock -->
<PackageReference Include="CsvHelper" Version="32.0.3" /> <PackageReference Include="CsvHelper" Version="32.0.3"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ellie.Marmalade\Ellie.Marmalade.csproj" /> <ProjectReference Include="..\Ellie.Marmalade\Ellie.Marmalade.csproj" />
<ProjectReference Include="..\EllieBot.Voice\EllieBot.Voice.csproj" /> <ProjectReference Include="..\EllieBot.Voice\EllieBot.Voice.csproj" />
<ProjectReference Include="..\EllieBot.Generators\EllieBot.Generators.csproj" OutputItemType="Analyzer" /> <ProjectReference Include="..\EllieBot.Generators\EllieBot.Generators.csproj" OutputItemType="Analyzer" />

View file

@ -1418,7 +1418,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1422,7 +1422,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1451,7 +1451,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1451,7 +1451,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1521,7 +1521,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1554,7 +1554,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1558,7 +1558,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1596,7 +1596,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1600,7 +1600,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1588,7 +1588,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1592,7 +1592,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1592,7 +1592,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1621,7 +1621,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1654,7 +1654,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1658,7 +1658,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1666,7 +1666,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1670,7 +1670,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1700,7 +1700,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Db.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1670,7 +1670,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Db.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations.Mysql
{
/// <inheritdoc />
public partial class removepatronlimits : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "patronquotas");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "patronquotas",
columns: table => new
{
userid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
featuretype = table.Column<int>(type: "int", nullable: false),
feature = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
dailycount = table.Column<uint>(type: "int unsigned", nullable: false),
hourlycount = table.Column<uint>(type: "int unsigned", nullable: false),
monthlycount = table.Column<uint>(type: "int unsigned", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_patronquotas", x => new { x.userid, x.featuretype, x.feature });
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "ix_patronquotas_userid",
table: "patronquotas",
column: "userid");
}
}
}

View file

@ -1718,41 +1718,6 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("expressions", (string)null); b.ToTable("expressions", (string)null);
}); });
modelBuilder.Entity("EllieBot.Db.Models.PatronQuota", b =>
{
b.Property<ulong>("UserId")
.HasColumnType("bigint unsigned")
.HasColumnName("userid");
b.Property<int>("FeatureType")
.HasColumnType("int")
.HasColumnName("featuretype");
b.Property<string>("Feature")
.HasColumnType("varchar(255)")
.HasColumnName("feature");
b.Property<uint>("DailyCount")
.HasColumnType("int unsigned")
.HasColumnName("dailycount");
b.Property<uint>("HourlyCount")
.HasColumnType("int unsigned")
.HasColumnName("hourlycount");
b.Property<uint>("MonthlyCount")
.HasColumnType("int unsigned")
.HasColumnName("monthlycount");
b.HasKey("UserId", "FeatureType", "Feature")
.HasName("pk_patronquotas");
b.HasIndex("UserId")
.HasDatabaseName("ix_patronquotas_userid");
b.ToTable("patronquotas", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.PatronUser", b => modelBuilder.Entity("EllieBot.Db.Models.PatronUser", b =>
{ {
b.Property<ulong>("UserId") b.Property<ulong>("UserId")

View file

@ -1490,7 +1490,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1521,7 +1521,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1521,7 +1521,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1591,7 +1591,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1626,7 +1626,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1630,7 +1630,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1670,7 +1670,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1674,7 +1674,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1662,7 +1662,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1666,7 +1666,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1666,7 +1666,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1697,7 +1697,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1732,7 +1732,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1736,7 +1736,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1744,7 +1744,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1748,7 +1748,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1699,7 +1699,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Db.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

View file

@ -1669,7 +1669,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("EllieBot.Db.Models.NadekoExpression", b => modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class removepatronlimits : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "patronquotas");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "patronquotas",
columns: table => new
{
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
featuretype = table.Column<int>(type: "integer", nullable: false),
feature = table.Column<string>(type: "text", nullable: false),
dailycount = table.Column<long>(type: "bigint", nullable: false),
hourlycount = table.Column<long>(type: "bigint", nullable: false),
monthlycount = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_patronquotas", x => new { x.userid, x.featuretype, x.feature });
});
migrationBuilder.CreateIndex(
name: "ix_patronquotas_userid",
table: "patronquotas",
column: "userid");
}
}
}

View file

@ -1717,41 +1717,6 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("expressions", (string)null); b.ToTable("expressions", (string)null);
}); });
modelBuilder.Entity("EllieBot.Db.Models.PatronQuota", b =>
{
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.Property<int>("FeatureType")
.HasColumnType("integer")
.HasColumnName("featuretype");
b.Property<string>("Feature")
.HasColumnType("text")
.HasColumnName("feature");
b.Property<long>("DailyCount")
.HasColumnType("bigint")
.HasColumnName("dailycount");
b.Property<long>("HourlyCount")
.HasColumnType("bigint")
.HasColumnName("hourlycount");
b.Property<long>("MonthlyCount")
.HasColumnType("bigint")
.HasColumnName("monthlycount");
b.HasKey("UserId", "FeatureType", "Feature")
.HasName("pk_patronquotas");
b.HasIndex("UserId")
.HasDatabaseName("ix_patronquotas_userid");
b.ToTable("patronquotas", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.PatronUser", b => modelBuilder.Entity("EllieBot.Db.Models.PatronUser", b =>
{ {
b.Property<decimal>("UserId") b.Property<decimal>("UserId")

View file

@ -11,6 +11,8 @@ namespace EllieBot.Migrations
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
MigrationQueries.GuildConfigCleanup(migrationBuilder);
migrationBuilder.DropForeignKey( migrationBuilder.DropForeignKey(
name: "FK_AntiRaidSetting_GuildConfigs_GuildConfigId", name: "FK_AntiRaidSetting_GuildConfigs_GuildConfigId",
table: "AntiRaidSetting"); table: "AntiRaidSetting");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations
{
/// <inheritdoc />
public partial class removepatronlimits : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PatronQuotas");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PatronQuotas",
columns: table => new
{
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
FeatureType = table.Column<int>(type: "INTEGER", nullable: false),
Feature = table.Column<string>(type: "TEXT", nullable: false),
DailyCount = table.Column<uint>(type: "INTEGER", nullable: false),
HourlyCount = table.Column<uint>(type: "INTEGER", nullable: false),
MonthlyCount = table.Column<uint>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PatronQuotas", x => new { x.UserId, x.FeatureType, x.Feature });
});
migrationBuilder.CreateIndex(
name: "IX_PatronQuotas_UserId",
table: "PatronQuotas",
column: "UserId");
}
}
}

View file

@ -1279,33 +1279,6 @@ namespace EllieBot.Migrations
b.ToTable("Expressions"); b.ToTable("Expressions");
}); });
modelBuilder.Entity("EllieBot.Db.Models.PatronQuota", b =>
{
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("FeatureType")
.HasColumnType("INTEGER");
b.Property<string>("Feature")
.HasColumnType("TEXT");
b.Property<uint>("DailyCount")
.HasColumnType("INTEGER");
b.Property<uint>("HourlyCount")
.HasColumnType("INTEGER");
b.Property<uint>("MonthlyCount")
.HasColumnType("INTEGER");
b.HasKey("UserId", "FeatureType", "Feature");
b.HasIndex("UserId");
b.ToTable("PatronQuotas");
});
modelBuilder.Entity("EllieBot.Db.Models.PatronUser", b => modelBuilder.Entity("EllieBot.Db.Models.PatronUser", b =>
{ {
b.Property<ulong>("UserId") b.Property<ulong>("UserId")

View file

@ -61,17 +61,66 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
})); }));
} }
// delete guild configs
await ctx.GetTable<GuildConfig>() await ctx.GetTable<GuildConfig>()
.Where(x => !tempTable.Select(x => x.GuildId) .Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId)) .Contains(x.GuildId))
.DeleteAsync(); .DeleteAsync();
// delete guild xp
await ctx.GetTable<UserXpStats>() await ctx.GetTable<UserXpStats>()
.Where(x => !tempTable.Select(x => x.GuildId) .Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId)) .Contains(x.GuildId))
.DeleteAsync(); .DeleteAsync();
// delete expressions
await ctx.GetTable<EllieExpression>()
.Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId.Value))
.DeleteAsync();
// delete quotes
await ctx.GetTable<Quote>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete planted currencies
await ctx.GetTable<PlantedCurrency>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete image only channels
await ctx.GetTable<ImageOnlyChannel>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete reaction roles
await ctx.GetTable<ReactionRoleV2>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete ignored users
await ctx.GetTable<DiscordPermOverride>()
.Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId.Value))
.DeleteAsync();
// delete perm overrides
await ctx.GetTable<DiscordPermOverride>()
.Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId.Value))
.DeleteAsync();
// delete repeaters
await ctx.GetTable<Repeater>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
return new() return new()
{ {
GuildCount = guildIds.Keys.Count, GuildCount = guildIds.Keys.Count,

View file

@ -45,23 +45,43 @@ public partial class Administration
var progressMsg = await Response().Pending(strs.prune_progress(0, 100)).SendAsync(); var progressMsg = await Response().Pending(strs.prune_progress(0, 100)).SendAsync();
var progress = GetProgressTracker(progressMsg); var progress = GetProgressTracker(progressMsg);
PruneResult result;
if (opts.Safe) if (opts.Safe)
await _service.PruneWhere((ITextChannel)ctx.Channel, result = await _service.PruneWhere((ITextChannel)ctx.Channel,
100, 100,
x => x.Author.Id == user.Id && !x.IsPinned, x => x.Author.Id == user.Id && !x.IsPinned,
progress, progress,
opts.After); opts.After);
else else
await _service.PruneWhere((ITextChannel)ctx.Channel, result = await _service.PruneWhere((ITextChannel)ctx.Channel,
100, 100,
x => x.Author.Id == user.Id, x => x.Author.Id == user.Id,
progress, progress,
opts.After); opts.After);
ctx.Message.DeleteAfter(3); ctx.Message.DeleteAfter(3);
await SendResult(result);
await progressMsg.DeleteAsync(); await progressMsg.DeleteAsync();
} }
private async Task SendResult(PruneResult result)
{
switch (result)
{
case PruneResult.Success:
break;
case PruneResult.AlreadyRunning:
break;
case PruneResult.FeatureLimit:
await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
break;
default:
throw new ArgumentOutOfRangeException(nameof(result), result, null);
}
}
// prune x // prune x
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
@ -83,19 +103,21 @@ public partial class Administration
var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync(); var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync();
var progress = GetProgressTracker(progressMsg); var progress = GetProgressTracker(progressMsg);
PruneResult result;
if (opts.Safe) if (opts.Safe)
await _service.PruneWhere((ITextChannel)ctx.Channel, result = await _service.PruneWhere((ITextChannel)ctx.Channel,
count, count,
x => !x.IsPinned && x.Id != progressMsg.Id, x => !x.IsPinned && x.Id != progressMsg.Id,
progress, progress,
opts.After); opts.After);
else else
await _service.PruneWhere((ITextChannel)ctx.Channel, result = await _service.PruneWhere((ITextChannel)ctx.Channel,
count, count,
x => x.Id != progressMsg.Id, x => x.Id != progressMsg.Id,
progress, progress,
opts.After); opts.After);
await SendResult(result);
await progressMsg.DeleteAsync(); await progressMsg.DeleteAsync();
} }
@ -155,9 +177,10 @@ public partial class Administration
var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync(); var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync();
var progress = GetProgressTracker(progressMsg); var progress = GetProgressTracker(progressMsg);
PruneResult result;
if (opts.Safe) if (opts.Safe)
{ {
await _service.PruneWhere((ITextChannel)ctx.Channel, result = await _service.PruneWhere((ITextChannel)ctx.Channel,
count, count,
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned, m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned,
progress, progress,
@ -166,7 +189,7 @@ public partial class Administration
} }
else else
{ {
await _service.PruneWhere((ITextChannel)ctx.Channel, result = await _service.PruneWhere((ITextChannel)ctx.Channel,
count, count,
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks, m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks,
progress, progress,
@ -174,6 +197,7 @@ public partial class Administration
); );
} }
await SendResult(result);
await progressMsg.DeleteAsync(); await progressMsg.DeleteAsync();
} }

View file

@ -0,0 +1,9 @@
#nullable disable
namespace EllieBot.Modules.Administration.Services;
public enum PruneResult
{
Success,
AlreadyRunning,
FeatureLimit,
}

View file

@ -1,4 +1,6 @@
#nullable disable #nullable disable
using EllieBot.Modules.Patronage;
namespace EllieBot.Modules.Administration.Services; namespace EllieBot.Modules.Administration.Services;
public class PruneService : IEService public class PruneService : IEService
@ -7,11 +9,15 @@ public class PruneService : IEService
private readonly ConcurrentDictionary<ulong, CancellationTokenSource> _pruningGuilds = new(); private readonly ConcurrentDictionary<ulong, CancellationTokenSource> _pruningGuilds = new();
private readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14); private readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14);
private readonly ILogCommandService _logService; private readonly ILogCommandService _logService;
private readonly IPatronageService _ps;
public PruneService(ILogCommandService logService) public PruneService(ILogCommandService logService, IPatronageService ps)
=> _logService = logService; {
_logService = logService;
_ps = ps;
}
public async Task PruneWhere( public async Task<PruneResult> PruneWhere(
ITextChannel channel, ITextChannel channel,
int amount, int amount,
Func<IMessage, bool> predicate, Func<IMessage, bool> predicate,
@ -26,8 +32,13 @@ public class PruneService : IEService
using var cancelSource = new CancellationTokenSource(); using var cancelSource = new CancellationTokenSource();
if (!_pruningGuilds.TryAdd(channel.GuildId, cancelSource)) if (!_pruningGuilds.TryAdd(channel.GuildId, cancelSource))
return; return PruneResult.AlreadyRunning;
if (!await _ps.LimitHitAsync(LimitedFeatureName.Prune, channel.Guild.OwnerId))
{
return PruneResult.FeatureLimit;
}
try try
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
@ -47,7 +58,7 @@ public class PruneService : IEService
.ToArray(); .ToArray();
if (!msgs.Any()) if (!msgs.Any())
return; return PruneResult.Success;
lastMessage = msgs[^1]; lastMessage = msgs[^1];
@ -88,6 +99,8 @@ public class PruneService : IEService
{ {
_pruningGuilds.TryRemove(channel.GuildId, out _); _pruningGuilds.TryRemove(channel.GuildId, out _);
} }
return PruneResult.Success;
} }
public async Task<bool> CancelAsync(ulong guildId) public async Task<bool> CancelAsync(ulong guildId)

View file

@ -18,7 +18,7 @@ public interface IReactionRoleService
/// <param name="group"></param> /// <param name="group"></param>
/// <param name="levelReq"></param> /// <param name="levelReq"></param>
/// <returns>The result of the operation</returns> /// <returns>The result of the operation</returns>
Task<OneOf<Success, FeatureLimit>> AddReactionRole( Task<OneOf<Success, Error>> AddReactionRole(
IGuild guild, IGuild guild,
IMessage msg, IMessage msg,
string emote, string emote,

View file

@ -55,12 +55,10 @@ public partial class Administration
await res.Match( await res.Match(
_ => ctx.OkAsync(), _ => ctx.OkAsync(),
fl => async fl =>
{ {
_ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser); _ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
return !fl.IsPatronLimit await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
? Response().Error(strs.limit_reached(fl.Quota)).SendAsync()
: Response().Pending(strs.feature_limit_reached_owner(fl.Quota, fl.Name)).SendAsync();
}); });
} }

View file

@ -21,22 +21,16 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
private readonly SemaphoreSlim _assignementLock = new(1, 1); private readonly SemaphoreSlim _assignementLock = new(1, 1);
private readonly IPatronageService _ps; private readonly IPatronageService _ps;
private static readonly FeatureLimitKey _reroFLKey = new()
{
Key = "rero:max_count",
PrettyName = "Reaction Role"
};
public ReactionRolesService( public ReactionRolesService(
DiscordSocketClient client, DiscordSocketClient client,
IPatronageService ps,
DbService db, DbService db,
IBotCredentials creds, IBotCredentials creds)
IPatronageService ps)
{ {
_db = db; _db = db;
_ps = ps;
_client = client; _client = client;
_creds = creds; _creds = creds;
_ps = ps;
_cache = new(); _cache = new();
} }
@ -242,7 +236,7 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
/// <param name="group"></param> /// <param name="group"></param>
/// <param name="levelReq"></param> /// <param name="levelReq"></param>
/// <returns>The result of the operation</returns> /// <returns>The result of the operation</returns>
public async Task<OneOf<Success, FeatureLimit>> AddReactionRole( public async Task<OneOf<Success, Error>> AddReactionRole(
IGuild guild, IGuild guild,
IMessage msg, IMessage msg,
string emote, string emote,
@ -261,9 +255,12 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
.Where(x => x.GuildId == guild.Id) .Where(x => x.GuildId == guild.Id)
.CountAsync(); .CountAsync();
var result = await _ps.TryGetFeatureLimitAsync(_reroFLKey, guild.OwnerId, 50); var limit = await _ps.GetUserLimit(LimitedFeatureName.ReactionRole, guild.OwnerId);
if (result.Quota != -1 && activeReactionRoles >= result.Quota)
return result; if (!_creds.IsOwner(guild.OwnerId) && (activeReactionRoles >= limit.Quota && limit.Quota >= 0))
{
return new Error();
}
await ctx.GetTable<ReactionRoleV2>() await ctx.GetTable<ReactionRoleV2>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()

View file

@ -19,7 +19,7 @@ public sealed class CheckForUpdatesService : IEService, IReadyExecutor
private readonly IMessageSenderService _sender; private readonly IMessageSenderService _sender;
private const string RELEASES_URL = "https://toastielab.dev/api/v1/repos/Emotions-stuff/EllieBot/releases"; private const string RELEASES_URL = "https://toastielab.dev/api/v1/repos/Emotions-stuff/elliebot/releases";
public CheckForUpdatesService( public CheckForUpdatesService(
BotConfigService bcs, BotConfigService bcs,
@ -72,7 +72,7 @@ public sealed class CheckForUpdatesService : IEService, IReadyExecutor
UpdateLastKnownVersion(latestVersion); UpdateLastKnownVersion(latestVersion);
// pull changelog // pull changelog
var changelog = await http.GetStringAsync("https://toastielab.dev/Emotions-stuff/Ellie/raw/branch/v5/CHANGELOG.md"); var changelog = await http.GetStringAsync("https://toastielab.dev/Emotions-stuff/elliebot/raw/branch/v5/CHANGELOG.md");
var thisVersionChangelog = GetVersionChangelog(latestVersion, changelog); var thisVersionChangelog = GetVersionChangelog(latestVersion, changelog);
@ -95,7 +95,7 @@ public sealed class CheckForUpdatesService : IEService, IReadyExecutor
.WithOkColor() .WithOkColor()
.WithAuthor($"EllieBot v{latest} Released!") .WithAuthor($"EllieBot v{latest} Released!")
.WithTitle("Changelog") .WithTitle("Changelog")
.WithUrl("https://toastielab.dev/Emotions-stuff/Ellie/src/branch/v5/CHANGELOG.md") .WithUrl("https://toastielab.dev/Emotions-stuff/elliebot/src/branch/v5/CHANGELOG.md")
.WithDescription(thisVersionChangelog.TrimTo(4096)) .WithDescription(thisVersionChangelog.TrimTo(4096))
.WithFooter( .WithFooter(
"You may disable these messages by typing '.conf bot checkforupdates false'"); "You may disable these messages by typing '.conf bot checkforupdates false'");

View file

@ -42,7 +42,7 @@ public static class EllieExpressionExtensions
() => canMentionEveryone () => canMentionEveryone
? ctx.Content[substringIndex..].Trim() ? ctx.Content[substringIndex..].Trim()
: ctx.Content[substringIndex..].Trim().SanitizeMentions(true)); : ctx.Content[substringIndex..].Trim().SanitizeMentions(true));
var text = SmartText.CreateFrom(cr.Response); var text = SmartText.CreateFrom(cr.Response);
text = await repSvc.ReplaceAsync(text, repCtx); text = await repSvc.ReplaceAsync(text, repCtx);

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
using EllieBot.Db.Models; using EllieBot.Db.Models;
namespace EllieBot.Modules.EllieExpressions; namespace EllieBot.Modules.EllieExpressions;

View file

@ -124,11 +124,11 @@ public sealed class EllieExpressionsService : IExecOnMessage, IReadyExecutor
newguildExpressions = guildItems.GroupBy(k => k.GuildId!.Value) newguildExpressions = guildItems.GroupBy(k => k.GuildId!.Value)
.ToDictionary(g => g.Key, .ToDictionary(g => g.Key,
g => g.Select(x => g => g.Select(x =>
{ {
x.Trigger = x.Trigger.Replace(MENTION_PH, x.Trigger = x.Trigger.Replace(MENTION_PH,
_client.CurrentUser.Mention); _client.CurrentUser.Mention);
return x; return x;
}) })
.ToArray()) .ToArray())
.ToConcurrent(); .ToConcurrent();
@ -259,7 +259,7 @@ public sealed class EllieExpressionsService : IExecOnMessage, IReadyExecutor
"ACTUALEXPRESSIONS", "ACTUALEXPRESSIONS",
expr.Trigger expr.Trigger
); );
if (!result.IsAllowed) if (!result.IsAllowed)
{ {
var cache = _pc.GetCacheFor(guild.Id); var cache = _pc.GetCacheFor(guild.Id);

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
using EllieBot.Db.Models; using EllieBot.Db.Models;
namespace EllieBot.Modules.EllieExpressions; namespace EllieBot.Modules.EllieExpressions;

View file

@ -3,6 +3,7 @@ using EllieBot.Common.TypeReaders;
using EllieBot.Modules.Gambling.Common; using EllieBot.Modules.Gambling.Common;
using EllieBot.Modules.Gambling.Common.Blackjack; using EllieBot.Modules.Gambling.Common.Blackjack;
using EllieBot.Modules.Gambling.Services; using EllieBot.Modules.Gambling.Services;
using EllieBot.Modules.Utility;
namespace EllieBot.Modules.Gambling; namespace EllieBot.Modules.Gambling;

View file

@ -1,7 +1,6 @@
#nullable disable #nullable disable
using LinqToDB; using LinqToDB;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using EllieBot.Db;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using EllieBot.Modules.Gambling.Bank; using EllieBot.Modules.Gambling.Bank;
using EllieBot.Modules.Gambling.Common; using EllieBot.Modules.Gambling.Common;
@ -14,6 +13,7 @@ using System.Text;
using EllieBot.Modules.Gambling.Rps; using EllieBot.Modules.Gambling.Rps;
using EllieBot.Common.TypeReaders; using EllieBot.Common.TypeReaders;
using EllieBot.Modules.Patronage; using EllieBot.Modules.Patronage;
using EllieBot.Modules.Utility;
namespace EllieBot.Modules.Gambling; namespace EllieBot.Modules.Gambling;
@ -27,9 +27,9 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly DownloadTracker _tracker; private readonly DownloadTracker _tracker;
private readonly GamblingConfigService _configService; private readonly GamblingConfigService _configService;
private readonly IBankService _bank; private readonly IBankService _bank;
private readonly IPatronageService _ps;
private readonly IRemindService _remind; private readonly IRemindService _remind;
private readonly GamblingTxTracker _gamblingTxTracker; private readonly GamblingTxTracker _gamblingTxTracker;
private readonly IPatronageService _ps;
private IUserMessage rdMsg; private IUserMessage rdMsg;
@ -41,8 +41,8 @@ public partial class Gambling : GamblingModule<GamblingService>
DownloadTracker tracker, DownloadTracker tracker,
GamblingConfigService configService, GamblingConfigService configService,
IBankService bank, IBankService bank,
IPatronageService ps,
IRemindService remind, IRemindService remind,
IPatronageService patronage,
GamblingTxTracker gamblingTxTracker) GamblingTxTracker gamblingTxTracker)
: base(configService) : base(configService)
{ {
@ -51,9 +51,9 @@ public partial class Gambling : GamblingModule<GamblingService>
_cs = currency; _cs = currency;
_client = client; _client = client;
_bank = bank; _bank = bank;
_ps = ps;
_remind = remind; _remind = remind;
_gamblingTxTracker = gamblingTxTracker; _gamblingTxTracker = gamblingTxTracker;
_ps = patronage;
_enUsCulture = new CultureInfo("en-US", false).NumberFormat; _enUsCulture = new CultureInfo("en-US", false).NumberFormat;
_enUsCulture.NumberDecimalDigits = 0; _enUsCulture.NumberDecimalDigits = 0;
@ -133,12 +133,6 @@ public partial class Gambling : GamblingModule<GamblingService>
await Response().Embed(embed).SendAsync(); await Response().Embed(embed).SendAsync();
} }
private static readonly FeatureLimitKey _timelyKey = new FeatureLimitKey()
{
Key = "timely:extra_percent",
PrettyName = "Timely"
};
private async Task RemindTimelyAction(SocketMessageComponent smc, DateTime when) private async Task RemindTimelyAction(SocketMessageComponent smc, DateTime when)
{ {
var tt = TimestampTag.FromDateTime(when, TimestampTagStyles.Relative); var tt = TimestampTag.FromDateTime(when, TimestampTagStyles.Relative);
@ -154,6 +148,7 @@ public partial class Gambling : GamblingModule<GamblingService>
await smc.RespondConfirmAsync(_sender, GetText(strs.remind_timely(tt)), ephemeral: true); await smc.RespondConfirmAsync(_sender, GetText(strs.remind_timely(tt)), ephemeral: true);
} }
// Creates timely reminder button, parameter in hours.
private EllieInteractionBase CreateRemindMeInteraction(int period) private EllieInteractionBase CreateRemindMeInteraction(int period)
=> _inter => _inter
.Create(ctx.User.Id, .Create(ctx.User.Id,
@ -163,6 +158,17 @@ public partial class Gambling : GamblingModule<GamblingService>
customId: "timely:remind_me"), customId: "timely:remind_me"),
(smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromHours(period))) (smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromHours(period)))
); );
// Creates timely reminder button, parameter in milliseconds.
private EllieInteractionBase CreateRemindMeInteraction(double ms)
=> _inter
.Create(ctx.User.Id,
new ButtonBuilder(
label: "Remind me",
emote: Emoji.Parse("⏰"),
customId: "timely:remind_me"),
(smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromMilliseconds(ms)))
);
[Cmd] [Cmd]
public async Task Timely() public async Task Timely()
@ -175,25 +181,31 @@ public partial class Gambling : GamblingModule<GamblingService>
return; return;
} }
var inter = CreateRemindMeInteraction(period); if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } remainder)
if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } rem)
{ {
// Get correct time form remainder
var interaction = CreateRemindMeInteraction(remainder.TotalMilliseconds);
// Removes timely button if there is a timely reminder in DB // Removes timely button if there is a timely reminder in DB
if (_service.UserHasTimelyReminder(ctx.User.Id)) if (_service.UserHasTimelyReminder(ctx.User.Id))
{ {
inter = null; interaction = null;
} }
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var relativeTag = TimestampTag.FromDateTime(now.Add(rem), TimestampTagStyles.Relative); var relativeTag = TimestampTag.FromDateTime(now.Add(remainder), TimestampTagStyles.Relative);
await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(inter).SendAsync(); await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(interaction).SendAsync();
return; return;
} }
var result = await _ps.TryGetFeatureLimitAsync(_timelyKey, ctx.User.Id, 0); var patron = await _ps.GetPatronAsync(ctx.User.Id);
val = (int)(val * (1 + (result.Quota! * 0.01f))); var percentBonus = (_ps.PercentBonus(patron) / 100f);
val += (int)(val * percentBonus);
var inter = CreateRemindMeInteraction(period);
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim")); await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
@ -892,6 +904,7 @@ public partial class Gambling : GamblingModule<GamblingService>
private static readonly ImmutableArray<string> _emojis = private static readonly ImmutableArray<string> _emojis =
new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray(); new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray();
[Cmd] [Cmd]
public async Task LuckyLadder([OverrideTypeReader(typeof(BalanceTypeReader))] long amount) public async Task LuckyLadder([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
{ {

View file

@ -247,7 +247,14 @@ public partial class Gambling
} }
else else
{ {
var cmd = entry.Command.Replace("%you%", ctx.User.Id.ToString()); var buyer = (IGuildUser)ctx.User;
var cmd = entry.Command
.Replace("%you%", buyer.Mention)
.Replace("%you.mention%", buyer.Mention)
.Replace("%you.username%", buyer.Username)
.Replace("%you.name%", buyer.GlobalName ?? buyer.Username)
.Replace("%you.nick%", buyer.DisplayName);
var eb = _sender.CreateEmbed() var eb = _sender.CreateEmbed()
.WithPendingColor() .WithPendingColor()
.WithTitle("Executing shop command") .WithTitle("Executing shop command")
@ -259,6 +266,7 @@ public partial class Gambling
GetProfitAmount(entry.Price), GetProfitAmount(entry.Price),
new("shop", "sell", entry.Name)); new("shop", "sell", entry.Name));
await Task.Delay(250);
await _cmdHandler.TryRunCommand(guild, await _cmdHandler.TryRunCommand(guild,
channel, channel,
new DoAsUserMessage( new DoAsUserMessage(

View file

@ -9,6 +9,7 @@ using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using EllieBot.Modules.Gambling; using EllieBot.Modules.Gambling;
using EllieBot.Common.TypeReaders; using EllieBot.Common.TypeReaders;
using EllieBot.Modules.Utility;
using Color = SixLabors.ImageSharp.Color; using Color = SixLabors.ImageSharp.Color;
using Image = SixLabors.ImageSharp.Image; using Image = SixLabors.ImageSharp.Image;

View file

@ -5,7 +5,7 @@ public class WaifuInfo : DbEntity
{ {
public int WaifuId { get; set; } public int WaifuId { get; set; }
public DiscordUser Waifu { get; set; } public DiscordUser Waifu { get; set; }
public int? ClaimerId { get; set; } public int? ClaimerId { get; set; }
public DiscordUser Claimer { get; set; } public DiscordUser Claimer { get; set; }

View file

@ -66,12 +66,12 @@ public static class WaifuExtensions
await ctx.Set<WaifuInfo>() await ctx.Set<WaifuInfo>()
.ToLinqToDBTable() .ToLinqToDBTable()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
AffinityId = null, AffinityId = null,
ClaimerId = null, ClaimerId = null,
Price = 1, Price = 1,
WaifuId = ctx.Set<DiscordUser>().Where(x => x.UserId == userId).Select(x => x.Id).First() WaifuId = ctx.Set<DiscordUser>().Where(x => x.UserId == userId).Select(x => x.Id).First()
}, },
_ => new(), _ => new(),
() => new() () => new()
{ {

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
namespace EllieBot.Db.Models; namespace EllieBot.Db.Models;
public class WaifuItem : DbEntity public class WaifuItem : DbEntity

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
namespace EllieBot.Db.Models; namespace EllieBot.Db.Models;
public class WaifuUpdate : DbEntity public class WaifuUpdate : DbEntity

View file

@ -18,7 +18,8 @@ public partial class Games
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)] [UserPerm(GuildPerm.ManageMessages)]
public async Task Cleverbot() [NoPublicBot]
public async Task CleverBot()
{ {
var channel = (ITextChannel)ctx.Channel; var channel = (ITextChannel)ctx.Channel;
@ -30,7 +31,7 @@ public partial class Games
await uow.SaveChangesAsync(); await uow.SaveChangesAsync();
} }
await Response().Confirm(strs.cleverbot_disabled).SendAsync(); await Response().Confirm(strs.chatbot_disabled).SendAsync();
return; return;
} }
@ -42,7 +43,7 @@ public partial class Games
await uow.SaveChangesAsync(); await uow.SaveChangesAsync();
} }
await Response().Confirm(strs.cleverbot_enabled).SendAsync(); await Response().Confirm(strs.chatbot_enabled).SendAsync();
} }
} }
} }

View file

@ -15,43 +15,32 @@ public class ChatterBotService : IExecOnMessage
public int Priority public int Priority
=> 1; => 1;
private readonly FeatureLimitKey _flKey;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly IPermissionChecker _perms; private readonly IPermissionChecker _perms;
private readonly CommandHandler _cmd;
private readonly IBotCredentials _creds; private readonly IBotCredentials _creds;
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly IPatronageService _ps;
private readonly GamesConfigService _gcs; private readonly GamesConfigService _gcs;
private readonly IMessageSenderService _sender; private readonly IMessageSenderService _sender;
public readonly IPatronageService _ps;
public ChatterBotService( public ChatterBotService(
DiscordSocketClient client, DiscordSocketClient client,
IPermissionChecker perms, IPermissionChecker perms,
IBot bot, IBot bot,
CommandHandler cmd, IPatronageService ps,
IHttpClientFactory factory, IHttpClientFactory factory,
IBotCredentials creds, IBotCredentials creds,
IPatronageService ps,
GamesConfigService gcs, GamesConfigService gcs,
IMessageSenderService sender) IMessageSenderService sender)
{ {
_client = client; _client = client;
_perms = perms; _perms = perms;
_cmd = cmd;
_creds = creds; _creds = creds;
_sender = sender; _sender = sender;
_httpFactory = factory; _httpFactory = factory;
_ps = ps;
_perms = perms; _perms = perms;
_gcs = gcs; _gcs = gcs;
_ps = ps;
_flKey = new FeatureLimitKey()
{
Key = CleverBotResponseStr.CLEVERBOT_RESPONSE,
PrettyName = "Cleverbot Replies"
};
ChatterBotGuilds = new(bot.AllGuildConfigs ChatterBotGuilds = new(bot.AllGuildConfigs
.Where(gc => gc.CleverbotEnabled) .Where(gc => gc.CleverbotEnabled)
@ -69,9 +58,9 @@ public class ChatterBotService : IExecOnMessage
Log.Information("Cleverbot will not work as the api key is missing"); Log.Information("Cleverbot will not work as the api key is missing");
return null; return null;
case ChatBotImplementation.Gpt3: case ChatBotImplementation.Gpt:
if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey)) if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey))
return new OfficialGpt3Session(_creds.Gpt3ApiKey, return new OfficialGptSession(_creds.Gpt3ApiKey,
_gcs.Data.ChatGpt.ModelName, _gcs.Data.ChatGpt.ModelName,
_gcs.Data.ChatGpt.ChatHistory, _gcs.Data.ChatGpt.ChatHistory,
_gcs.Data.ChatGpt.MaxTokens, _gcs.Data.ChatGpt.MaxTokens,
@ -87,22 +76,21 @@ public class ChatterBotService : IExecOnMessage
} }
} }
public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot) public IChatterBotSession GetOrCreateSession(ulong guildId)
{ {
var channel = msg.Channel as ITextChannel; if (ChatterBotGuilds.TryGetValue(guildId, out var lazyChatBot))
cleverbot = null; return lazyChatBot.Value;
if (channel is null) lazyChatBot = new(() => CreateSession(), true);
return null; ChatterBotGuilds.TryAdd(guildId, lazyChatBot);
return lazyChatBot.Value;
}
if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out var lazyCleverbot)) public string PrepareMessage(IUserMessage msg)
return null; {
var nadekoId = _client.CurrentUser.Id;
cleverbot = lazyCleverbot.Value; var normalMention = $"<@{nadekoId}> ";
var nickMention = $"<@!{nadekoId}> ";
var ellieId = _client.CurrentUser.Id;
var normalMention = $"<@{ellieId}> ";
var nickMention = $"<@!{ellieId}> ";
string message; string message;
if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture)) if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture))
message = msg.Content[normalMention.Length..].Trim(); message = msg.Content[normalMention.Length..].Trim();
@ -119,13 +107,31 @@ public class ChatterBotService : IExecOnMessage
if (guild is not SocketGuild sg) if (guild is not SocketGuild sg)
return false; return false;
var channel = usrMsg.Channel as ITextChannel;
if (channel is null)
return false;
if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out var lazyChatBot))
return false;
var chatBot = lazyChatBot.Value;
var message = PrepareMessage(usrMsg);
if (message is null)
return false;
return await RunChatterBot(sg, usrMsg, channel, chatBot, message);
}
public async Task<bool> RunChatterBot(
SocketGuild guild,
IUserMessage usrMsg,
ITextChannel channel,
IChatterBotSession chatBot,
string message)
{
try try
{ {
var message = PrepareMessage(usrMsg, out var cbs); var res = await _perms.CheckPermsAsync(guild,
if (message is null || cbs is null)
return false;
var res = await _perms.CheckPermsAsync(sg,
usrMsg.Channel, usrMsg.Channel,
usrMsg.Author, usrMsg.Author,
CleverBotResponseStr.CLEVERBOT_RESPONSE, CleverBotResponseStr.CLEVERBOT_RESPONSE,
@ -134,59 +140,33 @@ public class ChatterBotService : IExecOnMessage
if (!res.IsAllowed) if (!res.IsAllowed)
return false; return false;
var channel = (ITextChannel)usrMsg.Channel; if (!await _ps.LimitHitAsync(LimitedFeatureName.ChatBot, usrMsg.Author.Id, 2048 / 2))
var conf = _ps.GetConfig();
if (!_creds.IsOwner(sg.OwnerId) && conf.IsEnabled)
{ {
var quota = await _ps.TryGetFeatureLimitAsync(_flKey, sg.OwnerId, 0); // limit exceeded
return false;
uint? daily = quota.Quota is int dVal and < 0
? (uint)-dVal
: null;
uint? monthly = quota.Quota is int mVal and >= 0
? (uint)mVal
: null;
var maybeLimit = await _ps.TryIncrementQuotaCounterAsync(sg.OwnerId,
sg.OwnerId == usrMsg.Author.Id,
FeatureType.Limit,
_flKey.Key,
null,
daily,
monthly);
if (maybeLimit.TryPickT1(out var ql, out var counters))
{
if (ql.Quota == 0)
{
await _sender.Response(channel)
.Error(null,
text:
"In order to use the cleverbot feature, the owner of this server should be [Patron Tier X](https://patreon.com/join/elliebot) on patreon.",
footer:
"You may disable the cleverbot feature, and this message via '.cleverbot' command")
.SendAsync();
return true;
}
await _sender.Response(channel)
.Error(
null!,
$"You've reached your quota limit of **{ql.Quota}** responses {ql.QuotaPeriod.ToFullName()} for the cleverbot feature.",
footer: "You may wait for the quota reset or .")
.SendAsync();
return true;
}
} }
_ = channel.TriggerTypingAsync(); _ = channel.TriggerTypingAsync();
var response = await cbs.Think(message, usrMsg.Author.ToString()); var response = await chatBot.Think(message, usrMsg.Author.ToString());
await _sender.Response(channel)
.Confirm(response) if (response.TryPickT0(out var result, out var error))
.SendAsync(); {
// calculate the diff in case we overestimated user's usage
var inTokens = (result.TokensIn - 2048) / 2;
// add the output tokens to the limit
await _ps.LimitForceHit(LimitedFeatureName.ChatBot,
usrMsg.Author.Id,
(inTokens) + (result.TokensOut / 2 * 3));
await _sender.Response(channel)
.Confirm(result.Text)
.SendAsync();
}
else
{
Log.Warning("Error in chatterbot: {Error}", error);
}
Log.Information(""" Log.Information("""
CleverBot Executed CleverBot Executed

View file

@ -3,10 +3,25 @@ using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot; namespace EllieBot.Modules.Games.Common.ChatterBot;
public class Gpt3Response public class OpenAiCompletionResponse
{ {
[JsonPropertyName("choices")] [JsonPropertyName("choices")]
public Choice[] Choices { get; set; } public Choice[] Choices { get; set; }
[JsonPropertyName("usage")]
public OpenAiUsageData Usage { get; set; }
}
public class OpenAiUsageData
{
[JsonPropertyName("prompt_tokens")]
public int PromptTokens { get; set; }
[JsonPropertyName("completion_tokens")]
public int CompletionTokens { get; set; }
[JsonPropertyName("total_tokens")]
public int TotalTokens { get; set; }
} }
public class Choice public class Choice

View file

@ -1,7 +1,10 @@
#nullable disable #nullable disable
using OneOf;
using OneOf.Types;
namespace EllieBot.Modules.Games.Common.ChatterBot; namespace EllieBot.Modules.Games.Common.ChatterBot;
public interface IChatterBotSession public interface IChatterBotSession
{ {
Task<string> Think(string input, string username); Task<OneOf<ThinkResult, Error<string>>> Think(string input, string username);
} }

View file

@ -1,5 +1,7 @@
#nullable disable #nullable disable
using Newtonsoft.Json; using Newtonsoft.Json;
using OneOf;
using OneOf.Types;
namespace EllieBot.Modules.Games.Common.ChatterBot; namespace EllieBot.Modules.Games.Common.ChatterBot;
@ -18,7 +20,7 @@ public class OfficialCleverbotSession : IChatterBotSession
_httpFactory = factory; _httpFactory = factory;
} }
public async Task<string> Think(string input, string username) public async Task<OneOf<ThinkResult, Error<string>>> Think(string input, string username)
{ {
using var http = _httpFactory.CreateClient(); using var http = _httpFactory.CreateClient();
var dataString = await http.GetStringAsync(string.Format(QueryString, input, cs ?? "")); var dataString = await http.GetStringAsync(string.Format(QueryString, input, cs ?? ""));
@ -27,12 +29,17 @@ public class OfficialCleverbotSession : IChatterBotSession
var data = JsonConvert.DeserializeObject<CleverbotResponse>(dataString); var data = JsonConvert.DeserializeObject<CleverbotResponse>(dataString);
cs = data?.Cs; cs = data?.Cs;
return data?.Output; return new ThinkResult
{
Text = data?.Output,
TokensIn = 2,
TokensOut = 1
};
} }
catch catch
{ {
Log.Warning("Unexpected cleverbot response received: {ResponseString}", dataString); Log.Warning("Unexpected response from CleverBot: {ResponseString}", dataString);
return null; return new Error<string>("Unexpected CleverBot response received");
} }
} }
} }

View file

@ -1,105 +0,0 @@
#nullable disable
using Newtonsoft.Json;
using System.Net.Http.Json;
using SharpToken;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OfficialGpt3Session : IChatterBotSession
{
private string Uri
=> $"https://api.openai.com/v1/chat/completions";
private readonly string _apiKey;
private readonly string _model;
private readonly int _maxHistory;
private readonly int _maxTokens;
private readonly int _minTokens;
private readonly string _ellieUsername;
private readonly GptEncoding _encoding;
private List<GPTMessage> messages = new();
private readonly IHttpClientFactory _httpFactory;
public OfficialGpt3Session(
string apiKey,
ChatGptModel model,
int chatHistory,
int maxTokens,
int minTokens,
string personality,
string ellieUsername,
IHttpClientFactory factory)
{
_apiKey = apiKey;
_httpFactory = factory;
switch (model)
{
case ChatGptModel.Gpt35Turbo:
_model = "gpt-3.5-turbo";
break;
case ChatGptModel.Gpt4:
_model = "gpt-4";
break;
case ChatGptModel.Gpt432k:
_model = "gpt-4-32k";
break;
}
_maxHistory = chatHistory;
_maxTokens = maxTokens;
_minTokens = minTokens;
_ellieUsername = ellieUsername;
_encoding = GptEncoding.GetEncodingForModel(_model);
messages.Add(new GPTMessage(){Role = "user", Content = personality, Name = _ellieUsername});
}
public async Task<string> Think(string input, string username)
{
messages.Add(new GPTMessage(){Role = "user", Content = input, Name = username});
while(messages.Count > _maxHistory + 2){
messages.RemoveAt(1);
}
int tokensUsed = 0;
foreach(GPTMessage message in messages){
tokensUsed += _encoding.Encode(message.Content).Count;
}
tokensUsed *= 2; //Unsure why this is the case, but the token count chatgpt reports back is double what I calculate.
//check if we have the minimum number of tokens available to use. Remove messages until we have enough, otherwise exit out and inform the user why.
while(_maxTokens - tokensUsed <= _minTokens){
if(messages.Count > 2){
int tokens = _encoding.Encode(messages[1].Content).Count * 2;
tokensUsed -= tokens;
messages.RemoveAt(1);
}
else{
return "Token count exceeded, please increase the number of tokens in the bot config and restart.";
}
}
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Authorization = new("Bearer", _apiKey);
var data = await http.PostAsJsonAsync(Uri, new Gpt3ApiRequest()
{
Model = _model,
Messages = messages,
MaxTokens = _maxTokens - tokensUsed,
Temperature = 1,
});
var dataString = await data.Content.ReadAsStringAsync();
try
{
var response = JsonConvert.DeserializeObject<Gpt3Response>(dataString);
string message = response?.Choices[0]?.Message?.Content;
//Can't rely on the return to except, now that we need to add it to the messages list.
_ = message ?? throw new ArgumentNullException(nameof(message));
messages.Add(new GPTMessage(){Role = "assistant", Content = message, Name = _ellieUsername});
return message;
}
catch
{
Log.Warning("Unexpected GPT-3 response received: {ResponseString}", dataString);
return null;
}
}
}

View file

@ -0,0 +1,141 @@
#nullable disable
using Newtonsoft.Json;
using OneOf.Types;
using System.Net.Http.Json;
using SharpToken;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OfficialGptSession : IChatterBotSession
{
private string Uri
=> $"https://api.openai.com/v1/chat/completions";
private readonly string _apiKey;
private readonly string _model;
private readonly int _maxHistory;
private readonly int _maxTokens;
private readonly int _minTokens;
private readonly string _nadekoUsername;
private readonly GptEncoding _encoding;
private List<GPTMessage> messages = new();
private readonly IHttpClientFactory _httpFactory;
public OfficialGptSession(
string apiKey,
ChatGptModel model,
int chatHistory,
int maxTokens,
int minTokens,
string personality,
string nadekoUsername,
IHttpClientFactory factory)
{
_apiKey = apiKey;
_httpFactory = factory;
_model = model switch
{
ChatGptModel.Gpt35Turbo => "gpt-3.5-turbo",
ChatGptModel.Gpt4o => "gpt-4o",
_ => throw new ArgumentException("Unknown, unsupported or obsolete model", nameof(model))
};
_maxHistory = chatHistory;
_maxTokens = maxTokens;
_minTokens = minTokens;
_nadekoUsername = nadekoUsername;
_encoding = GptEncoding.GetEncodingForModel(_model);
messages.Add(new()
{
Role = "system",
Content = personality,
Name = _nadekoUsername
});
}
public async Task<OneOf.OneOf<ThinkResult, Error<string>>> Think(string input, string username)
{
messages.Add(new()
{
Role = "user",
Content = input,
Name = username
});
while (messages.Count > _maxHistory + 2)
{
messages.RemoveAt(1);
}
var tokensUsed = messages.Sum(message => _encoding.Encode(message.Content).Count);
tokensUsed *= 2;
//check if we have the minimum number of tokens available to use. Remove messages until we have enough, otherwise exit out and inform the user why.
while (_maxTokens - tokensUsed <= _minTokens)
{
if (messages.Count > 2)
{
var tokens = _encoding.Encode(messages[1].Content).Count * 2;
tokensUsed -= tokens;
messages.RemoveAt(1);
}
else
{
return new Error<string>("Token count exceeded, please increase the number of tokens in the bot config and restart.");
}
}
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Authorization = new("Bearer", _apiKey);
var data = await http.PostAsJsonAsync(Uri,
new Gpt3ApiRequest()
{
Model = _model,
Messages = messages,
MaxTokens = _maxTokens - tokensUsed,
Temperature = 1,
});
var dataString = await data.Content.ReadAsStringAsync();
try
{
var response = JsonConvert.DeserializeObject<OpenAiCompletionResponse>(dataString);
var res = response?.Choices?[0];
var message = res?.Message?.Content;
if (message is null)
{
return new Error<string>("ChatGpt: Received no response.");
}
messages.Add(new()
{
Role = "assistant",
Content = message,
Name = _nadekoUsername
});
return new ThinkResult()
{
Text = message,
TokensIn = response.Usage.PromptTokens,
TokensOut = response.Usage.CompletionTokens
};
}
catch
{
Log.Warning("Unexpected response received from OpenAI: {ResponseString}", dataString);
return new Error<string>("Unexpected response received");
}
}
}
public sealed class ThinkResult
{
public string Text { get; set; }
public int TokensIn { get; set; }
public int TokensOut { get; set; }
}

View file

@ -8,7 +8,7 @@ namespace EllieBot.Modules.Games.Common;
public sealed partial class GamesConfig : ICloneable<GamesConfig> public sealed partial class GamesConfig : ICloneable<GamesConfig>
{ {
[Comment("DO NOT CHANGE")] [Comment("DO NOT CHANGE")]
public int Version { get; set; } = 3; public int Version { get; set; } = 4;
[Comment("Hangman related settings (.hangman command)")] [Comment("Hangman related settings (.hangman command)")]
public HangmanConfig Hangman { get; set; } = new() public HangmanConfig Hangman { get; set; } = new()
@ -105,8 +105,8 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
[Comment(@"Which chatbot API should bot use. [Comment(@"Which chatbot API should bot use.
'cleverbot' - bot will use Cleverbot API. 'cleverbot' - bot will use Cleverbot API.
'gpt3' - bot will use GPT-3 API")] 'gpt' - bot will use GPT API")]
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt3; public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt;
public ChatGptConfig ChatGpt { get; set; } = new(); public ChatGptConfig ChatGpt { get; set; } = new();
} }
@ -114,10 +114,10 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
[Cloneable] [Cloneable]
public sealed partial class ChatGptConfig public sealed partial class ChatGptConfig
{ {
[Comment(@"Which GPT-3 Model should bot use. [Comment(@"Which GPT Model should bot use.
gpt35turbo - cheapest gpt35turbo - cheapest
gpt4 - 30x more expensive, higher quality gpt4o - more expensive, higher quality
gp432k - same model as above, but with a 32k token limit")] ")]
public ChatGptModel ModelName { get; set; } = ChatGptModel.Gpt35Turbo; public ChatGptModel ModelName { get; set; } = ChatGptModel.Gpt35Turbo;
[Comment(@"How should the chat bot behave, what's its personality? (Usage of this counts towards the max tokens)")] [Comment(@"How should the chat bot behave, what's its personality? (Usage of this counts towards the max tokens)")]
@ -126,10 +126,10 @@ public sealed partial class ChatGptConfig
[Comment(@"The maximum number of messages in a conversation that can be remembered. (This will increase the number of tokens used)")] [Comment(@"The maximum number of messages in a conversation that can be remembered. (This will increase the number of tokens used)")]
public int ChatHistory { get; set; } = 5; public int ChatHistory { get; set; } = 5;
[Comment(@"The maximum number of tokens to use per GPT-3 API call")] [Comment(@"The maximum number of tokens to use per GPT API call")]
public int MaxTokens { get; set; } = 100; public int MaxTokens { get; set; } = 100;
[Comment(@"The minimum number of tokens to use per GPT-3 API call, such that chat history is removed to make room.")] [Comment(@"The minimum number of tokens to use per GPT API call, such that chat history is removed to make room.")]
public int MinTokens { get; set; } = 30; public int MinTokens { get; set; } = 30;
} }
@ -163,12 +163,18 @@ public sealed partial class RaceAnimal
public enum ChatBotImplementation public enum ChatBotImplementation
{ {
Cleverbot, Cleverbot,
Gpt3 Gpt = 1,
[Obsolete]
Gpt3 = 1,
} }
public enum ChatGptModel public enum ChatGptModel
{ {
Gpt35Turbo, [Obsolete]
Gpt4, Gpt4,
Gpt432k [Obsolete]
Gpt432k,
Gpt35Turbo,
Gpt4o,
} }

View file

@ -73,15 +73,6 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
}); });
} }
if (data.Version < 2)
{
ModifyConfig(c =>
{
c.Version = 2;
c.ChatBot = ChatBotImplementation.Cleverbot;
});
}
if (data.Version < 3) if (data.Version < 3)
{ {
ModifyConfig(c => ModifyConfig(c =>
@ -90,5 +81,19 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
c.ChatGpt.ModelName = ChatGptModel.Gpt35Turbo; c.ChatGpt.ModelName = ChatGptModel.Gpt35Turbo;
}); });
} }
if (data.Version < 4)
{
ModifyConfig(c =>
{
c.Version = 4;
#pragma warning disable CS0612 // Type or member is obsolete
c.ChatGpt.ModelName =
c.ChatGpt.ModelName == ChatGptModel.Gpt4 || c.ChatGpt.ModelName == ChatGptModel.Gpt432k
? ChatGptModel.Gpt4o
: c.ChatGpt.ModelName;
#pragma warning restore CS0612 // Type or member is obsolete
});
}
} }
} }

View file

@ -524,7 +524,7 @@ public sealed partial class Help : EllieModule<HelpService>
=> smc.RespondConfirmAsync(_sender, => smc.RespondConfirmAsync(_sender,
""" """
- In case you don't want or cannot Donate to EllieBot project, but you - In case you don't want or cannot Donate to EllieBot project, but you
- EllieBot is a free and [open source](https://toastielab.dev/Emotions-stuff/Ellie) project which means you can run your own "selfhosted" instance on your computer. - EllieBot is a free and [open source](https://toastielab.dev/Emotions-stuff/elliebot) project which means you can run your own "selfhosted" instance on your computer.
*Keep in mind that running the bot on your computer means that the bot will be offline when you turn off your computer* *Keep in mind that running the bot on your computer means that the bot will be offline when you turn off your computer*

View file

@ -1,6 +1,7 @@
#nullable disable #nullable disable
using EllieBot.Modules.Music.Services; using EllieBot.Modules.Music.Services;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using EllieBot.Modules.Utility;
namespace EllieBot.Modules.Music; namespace EllieBot.Modules.Music;

View file

@ -212,7 +212,7 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider
if (settings.AutoDisconnect) if (settings.AutoDisconnect)
return LeaveVoiceChannelAsync(guildId); return LeaveVoiceChannelAsync(guildId);
} }
return Task.CompletedTask; return Task.CompletedTask;
}; };

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
namespace EllieBot.Db.Models; namespace EllieBot.Db.Models;
public class MusicPlaylist : DbEntity public class MusicPlaylist : DbEntity

View file

@ -4,10 +4,11 @@ namespace EllieBot.Modules.Patronage;
public class PatronageConfig : ConfigServiceBase<PatronConfigData> public class PatronageConfig : ConfigServiceBase<PatronConfigData>
{ {
public override string Name public override string Name
=> "patron"; => "patron";
private static readonly TypedKey<PatronConfigData> _changeKey; private static readonly TypedKey<PatronConfigData> _changeKey
= new("config.patron.updated");
private const string FILE_PATH = "data/patron.yml"; private const string FILE_PATH = "data/patron.yml";
@ -31,5 +32,14 @@ public class PatronageConfig : ConfigServiceBase<PatronConfigData>
c.IsEnabled = false; c.IsEnabled = false;
} }
}); });
ModifyConfig(c =>
{
if (c.Version == 2)
{
c.Version = 3;
}
});
} }
} }

View file

@ -1,6 +1,7 @@
#nullable disable #nullable disable
using LinqToDB; using LinqToDB;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Modules.Gambling.Services; using EllieBot.Modules.Gambling.Services;
using EllieBot.Modules.Patronage; using EllieBot.Modules.Patronage;
using EllieBot.Services.Currency; using EllieBot.Services.Currency;
@ -8,7 +9,7 @@ using EllieBot.Db.Models;
namespace EllieBot.Modules.Utility; namespace EllieBot.Modules.Utility;
public sealed class CurrencyRewardService : IEService, IDisposable public sealed class CurrencyRewardService : IEService, IReadyExecutor
{ {
private readonly ICurrencyService _cs; private readonly ICurrencyService _cs;
private readonly IPatronageService _ps; private readonly IPatronageService _ps;
@ -32,16 +33,14 @@ public sealed class CurrencyRewardService : IEService, IDisposable
_config = config; _config = config;
_client = client; _client = client;
}
public Task OnReadyAsync()
{
_ps.OnNewPatronPayment += OnNewPayment; _ps.OnNewPatronPayment += OnNewPayment;
_ps.OnPatronRefunded += OnPatronRefund; _ps.OnPatronRefunded += OnPatronRefund;
_ps.OnPatronUpdated += OnPatronUpdate; _ps.OnPatronUpdated += OnPatronUpdate;
} return Task.CompletedTask;
public void Dispose()
{
_ps.OnNewPatronPayment -= OnNewPayment;
_ps.OnPatronRefunded -= OnPatronRefund;
_ps.OnPatronUpdated -= OnPatronUpdate;
} }
private async Task OnPatronUpdate(Patron oldPatron, Patron newPatron) private async Task OnPatronUpdate(Patron oldPatron, Patron newPatron)
@ -58,17 +57,17 @@ public sealed class CurrencyRewardService : IEService, IDisposable
old = await ctx.GetTable<RewardedUser>() old = await ctx.GetTable<RewardedUser>()
.Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId) .Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (old is null) if (old is null)
{ {
await OnNewPayment(newPatron); await OnNewPayment(newPatron);
return; return;
} }
// no action as the amount is the same or lower // no action as the amount is the same or lower
if (old.AmountRewardedThisMonth >= newAmount) if (old.AmountRewardedThisMonth >= newAmount)
return; return;
var count = await ctx.GetTable<RewardedUser>() var count = await ctx.GetTable<RewardedUser>()
.Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId) .Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId)
.UpdateAsync(_ => new() .UpdateAsync(_ => new()
@ -91,21 +90,21 @@ public sealed class CurrencyRewardService : IEService, IDisposable
(int)(newAmount / conf.PatreonCurrencyPerCent), (int)(newAmount / conf.PatreonCurrencyPerCent),
newAmount, newAmount,
out var percentBonus); out var percentBonus);
var realOldAmount = GetRealCurrencyReward( var realOldAmount = GetRealCurrencyReward(
(int)(oldAmount / conf.PatreonCurrencyPerCent), (int)(oldAmount / conf.PatreonCurrencyPerCent),
oldAmount, oldAmount,
out _); out _);
var diff = realNewAmount - realOldAmount; var diff = realNewAmount - realOldAmount;
if (diff <= 0) if (diff <= 0)
return; // no action if new is lower return; // no action if new is lower
// if the user pledges 5$ or more, they will get X % more flowers where X is amount in dollars, // if the user pledges 5$ or more, they will get X % more flowers where X is amount in dollars,
// up to 100% // up to 100%
await _cs.AddAsync(newPatron.UserId, diff, new TxData("patron", "update")); await _cs.AddAsync(newPatron.UserId, diff, new TxData("patron","update"));
_ = SendMessageToUser(newPatron.UserId, _ = SendMessageToUser(newPatron.UserId,
$"You've received an additional **{diff}**{_config.Data.Currency.Sign} as a currency reward (+{percentBonus}%)!"); $"You've received an additional **{diff}**{_config.Data.Currency.Sign} as a currency reward (+{percentBonus}%)!");
} }
@ -140,12 +139,12 @@ public sealed class CurrencyRewardService : IEService, IDisposable
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
await ctx.GetTable<RewardedUser>() await ctx.GetTable<RewardedUser>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
PlatformUserId = patron.UniquePlatformUserId, PlatformUserId = patron.UniquePlatformUserId,
UserId = patron.UserId, UserId = patron.UserId,
AmountRewardedThisMonth = amount, AmountRewardedThisMonth = amount,
LastReward = patron.PaidAt, LastReward = patron.PaidAt,
}, },
old => new() old => new()
{ {
AmountRewardedThisMonth = amount, AmountRewardedThisMonth = amount,
@ -156,7 +155,7 @@ public sealed class CurrencyRewardService : IEService, IDisposable
{ {
PlatformUserId = patron.UniquePlatformUserId PlatformUserId = patron.UniquePlatformUserId
}); });
var realAmount = GetRealCurrencyReward(patron.Amount, amount, out var percentBonus); var realAmount = GetRealCurrencyReward(patron.Amount, amount, out var percentBonus);
await _cs.AddAsync(patron.UserId, realAmount, new("patron", "new")); await _cs.AddAsync(patron.UserId, realAmount, new("patron", "new"));
_ = SendMessageToUser(patron.UserId, _ = SendMessageToUser(patron.UserId,
@ -174,7 +173,7 @@ public sealed class CurrencyRewardService : IEService, IDisposable
var eb = _sender.CreateEmbed() var eb = _sender.CreateEmbed()
.WithOkColor() .WithOkColor()
.WithDescription(message); .WithDescription(message);
await _sender.Response(user).Embed(eb).SendAsync(); await _sender.Response(user).Embed(eb).SendAsync();
} }
catch catch

View file

@ -1,11 +0,0 @@
using EllieBot.Db.Models;
namespace EllieBot.Modules.Patronage;
public readonly struct InsufficientTier
{
public FeatureType FeatureType { get; init; }
public string Feature { get; init; }
public PatronTier RequiredTier { get; init; }
public PatronTier UserTier { get; init; }
}

View file

@ -11,11 +11,11 @@ public class PatreonClient : IDisposable
private readonly string _clientId; private readonly string _clientId;
private readonly string _clientSecret; private readonly string _clientSecret;
private string refreshToken; private string refreshToken;
private string accessToken = string.Empty; private string accessToken = string.Empty;
private readonly HttpClient _http; private readonly HttpClient _http;
private DateTime refreshAt = DateTime.UtcNow; private DateTime refreshAt = DateTime.UtcNow;
public PatreonClient(string clientId, string clientSecret, string refreshToken) public PatreonClient(string clientId, string clientSecret, string refreshToken)
@ -101,7 +101,7 @@ public class PatreonClient : IDisposable
return OneOf<IAsyncEnumerable<IReadOnlyCollection<PatreonMemberData>>, Error<string>>.FromT0( return OneOf<IAsyncEnumerable<IReadOnlyCollection<PatreonMemberData>>, Error<string>>.FromT0(
GetMembersInternalAsync(campaignId)); GetMembersInternalAsync(campaignId));
} }
private async IAsyncEnumerable<IReadOnlyCollection<PatreonMemberData>> GetMembersInternalAsync(string campaignId) private async IAsyncEnumerable<IReadOnlyCollection<PatreonMemberData>> GetMembersInternalAsync(string campaignId)
{ {
_http.DefaultRequestHeaders.Clear(); _http.DefaultRequestHeaders.Clear();
@ -140,9 +140,8 @@ public class PatreonClient : IDisposable
LastChargeDate = m.Attributes.LastChargeDate, LastChargeDate = m.Attributes.LastChargeDate,
LastChargeStatus = m.Attributes.LastChargeStatus LastChargeStatus = m.Attributes.LastChargeStatus
}) })
.Where(x => x.UserId == 140788173885276160)
.ToArray(); .ToArray();
yield return userData; yield return userData;
} while (!string.IsNullOrWhiteSpace(page = data.Links?.Next)); } while (!string.IsNullOrWhiteSpace(page = data.Links?.Next));

View file

@ -7,4 +7,4 @@ public readonly struct PatreonCredentials
public string ClientSecret { get; init; } public string ClientSecret { get; init; }
public string AccessToken { get; init; } public string AccessToken { get; init; }
public string RefreshToken { get; init; } public string RefreshToken { get; init; }
} }

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace EllieBot.Modules.Patronage; namespace EllieBot.Modules.Patronage;

View file

@ -25,9 +25,4 @@ public sealed class PatreonMemberData : ISubscriberData
"Declined" or "Pending" => SubscriptionChargeStatus.Unpaid, "Declined" or "Pending" => SubscriptionChargeStatus.Unpaid,
_ => SubscriptionChargeStatus.Other, _ => SubscriptionChargeStatus.Other,
}; };
}
public sealed class PatreonPledgeData
{
} }

View file

@ -6,8 +6,8 @@ namespace EllieBot.Modules.Patronage;
/// </summary> /// </summary>
public sealed class PatreonSubscriptionHandler : ISubscriptionHandler, IEService public sealed class PatreonSubscriptionHandler : ISubscriptionHandler, IEService
{ {
private readonly IBotCredsProvider _credsProvider; private readonly IBotCredsProvider _credsProvider;
private readonly PatreonClient _patreonClient; private readonly PatreonClient _patreonClient;
public PatreonSubscriptionHandler(IBotCredsProvider credsProvider) public PatreonSubscriptionHandler(IBotCredsProvider credsProvider)
{ {
@ -15,26 +15,26 @@ public sealed class PatreonSubscriptionHandler : ISubscriptionHandler, IEService
var botCreds = credsProvider.GetCreds(); var botCreds = credsProvider.GetCreds();
_patreonClient = new PatreonClient(botCreds.Patreon.ClientId, botCreds.Patreon.ClientSecret, botCreds.Patreon.RefreshToken); _patreonClient = new PatreonClient(botCreds.Patreon.ClientId, botCreds.Patreon.ClientSecret, botCreds.Patreon.RefreshToken);
} }
public async IAsyncEnumerable<IReadOnlyCollection<ISubscriberData>> GetPatronsAsync() public async IAsyncEnumerable<IReadOnlyCollection<ISubscriberData>> GetPatronsAsync()
{ {
var botCreds = _credsProvider.GetCreds(); var botCreds = _credsProvider.GetCreds();
if (string.IsNullOrWhiteSpace(botCreds.Patreon.CampaignId) if (string.IsNullOrWhiteSpace(botCreds.Patreon.CampaignId)
|| string.IsNullOrWhiteSpace(botCreds.Patreon.ClientId) || string.IsNullOrWhiteSpace(botCreds.Patreon.ClientId)
|| string.IsNullOrWhiteSpace(botCreds.Patreon.ClientSecret) || string.IsNullOrWhiteSpace(botCreds.Patreon.ClientSecret)
|| string.IsNullOrWhiteSpace(botCreds.Patreon.RefreshToken)) || string.IsNullOrWhiteSpace(botCreds.Patreon.RefreshToken))
yield break; yield break;
var result = await _patreonClient.RefreshTokenAsync(false); var result = await _patreonClient.RefreshTokenAsync(false);
if (!result.TryPickT0(out _, out var error)) if (!result.TryPickT0(out _, out var error))
{ {
Log.Warning("Unable to refresh patreon token: {ErrorMessage}", error.Value); Log.Warning("Unable to refresh patreon token: {ErrorMessage}", error.Value);
yield break; yield break;
} }
var patreonCreds = _patreonClient.GetCredentials(); var patreonCreds = _patreonClient.GetCredentials();
_credsProvider.ModifyCredsFile(c => _credsProvider.ModifyCredsFile(c =>
{ {
c.Patreon.AccessToken = patreonCreds.AccessToken; c.Patreon.AccessToken = patreonCreds.AccessToken;
@ -58,7 +58,7 @@ public sealed class PatreonSubscriptionHandler : ISubscriptionHandler, IEService
Log.Warning(ex, Log.Warning(ex,
"Unexpected error while refreshing patreon members: {ErroMessage}", "Unexpected error while refreshing patreon members: {ErroMessage}",
ex.Message); ex.Message);
yield break; yield break;
} }
@ -71,7 +71,7 @@ public sealed class PatreonSubscriptionHandler : ISubscriptionHandler, IEService
&& x.LastCharge is { } lc && x.LastCharge is { } lc
&& lc.ToUniversalTime().ToDateOnly() >= firstOfThisMonth) && lc.ToUniversalTime().ToDateOnly() >= firstOfThisMonth)
.ToArray(); .ToArray();
if (toReturn.Length > 0) if (toReturn.Length > 0)
yield return toReturn; yield return toReturn;
} }

View file

@ -71,17 +71,16 @@ public partial class Help
return; return;
} }
var patron = await _service.GetPatronAsync(user.Id); var maybePatron = await _service.GetPatronAsync(user.Id);
var quotaStats = await _service.GetUserQuotaStatistic(user.Id);
var quotaStats = await _service.LimitStats(user.Id);
var eb = _sender.CreateEmbed() var eb = _sender.CreateEmbed()
.WithAuthor(user) .WithAuthor(user)
.WithTitle(GetText(strs.patron_info)) .WithTitle(GetText(strs.patron_info))
.WithOkColor(); .WithOkColor();
if (quotaStats.Commands.Count == 0 if (quotaStats.Count == 0 || maybePatron is not { } patron)
&& quotaStats.Groups.Count == 0
&& quotaStats.Modules.Count == 0)
{ {
eb.WithDescription(GetText(strs.no_quota_found)); eb.WithDescription(GetText(strs.no_quota_found));
} }
@ -97,26 +96,9 @@ public partial class Help
eb.AddField(GetText(strs.quotas), "", false); eb.AddField(GetText(strs.quotas), "", false);
if (quotaStats.Commands.Count > 0) var text = GetQuotaList(quotaStats);
{ if (!string.IsNullOrWhiteSpace(text))
var text = GetQuotaList(quotaStats.Commands); eb.AddField(GetText(strs.modules), text, true);
if (!string.IsNullOrWhiteSpace(text))
eb.AddField(GetText(strs.commands), text, true);
}
if (quotaStats.Groups.Count > 0)
{
var text = GetQuotaList(quotaStats.Groups);
if (!string.IsNullOrWhiteSpace(text))
eb.AddField(GetText(strs.groups), text, true);
}
if (quotaStats.Modules.Count > 0)
{
var text = GetQuotaList(quotaStats.Modules);
if (!string.IsNullOrWhiteSpace(text))
eb.AddField(GetText(strs.modules), text, true);
}
} }
@ -131,26 +113,28 @@ public partial class Help
} }
} }
private string GetQuotaList(IReadOnlyDictionary<string, FeatureQuotaStats> featureQuotaStats) private string GetQuotaList(
IReadOnlyDictionary<LimitedFeatureName, (int Cur, QuotaLimit Quota)> featureQuotaStats)
{ {
var text = string.Empty; var text = string.Empty;
foreach (var (key, q) in featureQuotaStats) foreach (var (key, (cur, quota)) in featureQuotaStats)
{ {
text += $"\n\t`{key}`\n"; text += $"\n\t`{key}`\n";
if (q.Hourly != default) if (quota.QuotaPeriod == QuotaPer.PerHour)
text += $" {GetEmoji(q.Hourly)} {q.Hourly.Cur}/{q.Hourly.Max} per hour\n"; text += $" {cur}/{(quota.Quota == -1 ? "" : quota.Quota)} {QuotaPeriodToString(quota.QuotaPeriod)}\n";
if (q.Daily != default)
text += $" {GetEmoji(q.Daily)} {q.Daily.Cur}/{q.Daily.Max} per day\n";
if (q.Monthly != default)
text += $" {GetEmoji(q.Monthly)} {q.Monthly.Cur}/{q.Monthly.Max} per month\n";
} }
return text; return text;
} }
private string GetEmoji((uint Cur, uint Max) limit) public string QuotaPeriodToString(QuotaPer per)
=> limit.Cur < limit.Max => per switch
? "✅" {
: "⚠️"; QuotaPer.PerHour => "per hour",
QuotaPer.PerDay => "per day",
QuotaPer.PerMonth => "per month",
QuotaPer.Total => "total",
_ => throw new ArgumentOutOfRangeException(nameof(per), per, null)
};
} }
} }

View file

@ -2,9 +2,8 @@
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using OneOf; using StackExchange.Redis;
using OneOf.Types; using System.Diagnostics;
using CommandInfo = Discord.Commands.CommandInfo;
namespace EllieBot.Modules.Patronage; namespace EllieBot.Modules.Patronage;
@ -12,7 +11,6 @@ namespace EllieBot.Modules.Patronage;
public sealed class PatronageService public sealed class PatronageService
: IPatronageService, : IPatronageService,
IReadyExecutor, IReadyExecutor,
IExecPreCommand,
IEService IEService
{ {
public event Func<Patron, Task> OnNewPatronPayment = static delegate { return Task.CompletedTask; }; public event Func<Patron, Task> OnNewPatronPayment = static delegate { return Task.CompletedTask; };
@ -60,7 +58,7 @@ public sealed class PatronageService
if (_client.ShardId != 0) if (_client.ShardId != 0)
return Task.CompletedTask; return Task.CompletedTask;
return Task.WhenAll(ResetLoopAsync(), LoadSubscribersLoopAsync()); return Task.WhenAll(LoadSubscribersLoopAsync());
} }
private async Task LoadSubscribersLoopAsync() private async Task LoadSubscribersLoopAsync()
@ -85,71 +83,6 @@ public sealed class PatronageService
} }
} }
public async Task ResetLoopAsync()
{
await Task.Delay(1.Minutes());
while (true)
{
try
{
if (!_pConf.Data.IsEnabled)
{
await Task.Delay(1.Minutes());
continue;
}
var now = DateTime.UtcNow;
var lastRun = DateTime.MinValue;
var result = await _cache.GetAsync(_quotaKey);
if (result.TryGetValue(out var lastVal) && lastVal != default)
{
lastRun = DateTime.FromBinary(lastVal);
}
var nowDate = now.ToDateOnly();
var lastDate = lastRun.ToDateOnly();
await using var ctx = _db.GetDbContext();
if ((lastDate.Day == 1 || (lastDate.Month != nowDate.Month)) && nowDate.Day > 1)
{
// assumes bot won't be offline for a year
await ctx.GetTable<PatronQuota>()
.TruncateAsync();
}
else if (nowDate.DayNumber != lastDate.DayNumber)
{
// day is different, means hour is different.
// reset both hourly and daily quota counts.
await ctx.GetTable<PatronQuota>()
.UpdateAsync((old) => new()
{
HourlyCount = 0,
DailyCount = 0,
});
}
else if (now.Hour != lastRun.Hour) // if it's not, just reset hourly quotas
{
await ctx.GetTable<PatronQuota>()
.UpdateAsync((old) => new()
{
HourlyCount = 0
});
}
// assumes that the code above runs in less than an hour
await _cache.AddAsync(_quotaKey, now.ToBinary());
}
catch (Exception ex)
{
Log.Error(ex, "Error in quota reset loop. Message: {ErrorMessage}", ex.Message);
}
await Task.Delay(TimeSpan.FromHours(1).Add(TimeSpan.FromMinutes(1)));
}
}
private async Task ProcesssPatronsAsync(IReadOnlyCollection<ISubscriberData> subscribersEnum) private async Task ProcesssPatronsAsync(IReadOnlyCollection<ISubscriberData> subscribersEnum)
{ {
// process only users who have discord accounts connected // process only users who have discord accounts connected
@ -203,7 +136,8 @@ public sealed class PatronageService
// if his sub would end in teh future, extend it by one month. // if his sub would end in teh future, extend it by one month.
// if it's not, just add 1 month to the last charge date // if it's not, just add 1 month to the last charge date
var count = await ctx.GetTable<PatronUser>() var count = await ctx.GetTable<PatronUser>()
.Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId) .Where(x => x.UniquePlatformUserId
== subscriber.UniquePlatformUserId)
.UpdateAsync(old => new() .UpdateAsync(old => new()
{ {
UserId = subscriber.UserId, UserId = subscriber.UserId,
@ -215,14 +149,13 @@ public sealed class PatronageService
: dateInOneMonth, : dateInOneMonth,
}); });
// this should never happen
if (count == 0)
{
// await tran.RollbackAsync();
continue;
}
// await tran.CommitAsync(); dbPatron.UserId = subscriber.UserId;
dbPatron.AmountCents = subscriber.Cents;
dbPatron.LastCharge = lastChargeUtc;
dbPatron.ValidThru = dbPatron.ValidThru >= todayDate
? dbPatron.ValidThru.AddMonths(1)
: dateInOneMonth;
await OnNewPatronPayment(PatronUserToPatron(dbPatron)); await OnNewPatronPayment(PatronUserToPatron(dbPatron));
} }
@ -284,313 +217,7 @@ public sealed class PatronageService
} }
} }
public async Task<bool> ExecPreCommandAsync( public async Task<Patron?> GetPatronAsync(ulong userId)
ICommandContext ctx,
string moduleName,
CommandInfo command)
{
var ownerId = ctx.Guild?.OwnerId ?? 0;
var result = await AttemptRunCommand(
ctx.User.Id,
ownerId: ownerId,
command.Aliases.First().ToLowerInvariant(),
command.Module.Parent == null ? string.Empty : command.Module.GetGroupName().ToLowerInvariant(),
moduleName.ToLowerInvariant()
);
return result.Match(
_ => false,
ins =>
{
var eb = _sender.CreateEmbed()
.WithPendingColor()
.WithTitle("Insufficient Patron Tier")
.AddField("For", $"{ins.FeatureType}: `{ins.Feature}`", true)
.AddField("Required Tier",
$"[{ins.RequiredTier.ToFullName()}](https://patreon.com/join/elliebot)",
true);
if (ctx.Guild is null || ctx.Guild?.OwnerId == ctx.User.Id)
eb.WithDescription("You don't have the sufficent Patron Tier to run this command.")
.WithFooter("You can use '.patron' and '.donate' commands for more info");
else
eb.WithDescription(
"Neither you nor the server owner have the sufficent Patron Tier to run this command.")
.WithFooter("You can use '.patron' and '.donate' commands for more info");
_ = ctx.WarningAsync();
if (ctx.Guild?.OwnerId == ctx.User.Id)
_ = _sender.Response(ctx)
.Context(ctx)
.Embed(eb)
.SendAsync();
else
_ = _sender.Response(ctx).User(ctx.User).Embed(eb).SendAsync();
return true;
},
quota =>
{
var eb = _sender.CreateEmbed()
.WithPendingColor()
.WithTitle("Quota Limit Reached");
if (quota.IsOwnQuota || ctx.User.Id == ownerId)
{
eb.WithDescription($"You've reached your quota of `{quota.Quota} {quota.QuotaPeriod.ToFullName()}`")
.WithFooter("You may want to check your quota by using the '.patron' command.");
}
else
{
eb.WithDescription(
$"This server reached the quota of {quota.Quota} `{quota.QuotaPeriod.ToFullName()}`")
.WithFooter("You may contact the server owner about this issue.\n"
+ "Alternatively, you can become patron yourself by using the '.donate' command.\n"
+ "If you're already a patron, it means you've reached your quota.\n"
+ "You can use '.patron' command to check your quota status.");
}
eb.AddField("For", $"{quota.FeatureType}: `{quota.Feature}`", true)
.AddField("Resets At", quota.ResetsAt.ToShortAndRelativeTimestampTag(), true);
_ = ctx.WarningAsync();
// send the message in the server in case it's the owner
if (ctx.Guild?.OwnerId == ctx.User.Id)
_ = _sender.Response(ctx)
.Embed(eb)
.SendAsync();
else
_ = _sender.Response(ctx).User(ctx.User).Embed(eb).SendAsync();
return true;
});
}
private async ValueTask<OneOf<OneOf.Types.Success, InsufficientTier, QuotaLimit>> AttemptRunCommand(
ulong userId,
ulong ownerId,
string commandName,
string groupName,
string moduleName)
{
// try to run as a user
var res = await AttemptRunCommand(userId, commandName, groupName, moduleName, true);
// if it fails, try to run as an owner
// but only if the command is ran in a server
// and if the owner is not the user
if (!res.IsT0 && ownerId != 0 && ownerId != userId)
res = await AttemptRunCommand(ownerId, commandName, groupName, moduleName, false);
return res;
}
/// <summary>
/// Returns either the current usage counter if limit wasn't reached, or QuotaLimit if it is.
/// </summary>
public async ValueTask<OneOf<(uint Hourly, uint Daily, uint Monthly), QuotaLimit>> TryIncrementQuotaCounterAsync(
ulong userId,
bool isSelf,
FeatureType featureType,
string featureName,
uint? maybeHourly,
uint? maybeDaily,
uint? maybeMonthly)
{
await using var ctx = _db.GetDbContext();
var now = DateTime.UtcNow;
await using var tran = await ctx.Database.BeginTransactionAsync();
var userQuotaData = await ctx.GetTable<PatronQuota>()
.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId
&& x.Feature == featureName)
?? new PatronQuota();
// if hourly exists, if daily exists, etc...
if (maybeHourly is uint hourly && userQuotaData.HourlyCount >= hourly)
{
return new QuotaLimit()
{
QuotaPeriod = QuotaPer.PerHour,
Quota = hourly,
// quite a neat trick. https://stackoverflow.com/a/5733560
ResetsAt = now.Date.AddHours(now.Hour + 1),
Feature = featureName,
FeatureType = featureType,
IsOwnQuota = isSelf
};
}
if (maybeDaily is uint daily
&& userQuotaData.DailyCount >= daily)
{
return new QuotaLimit()
{
QuotaPeriod = QuotaPer.PerDay,
Quota = daily,
ResetsAt = now.Date.AddDays(1),
Feature = featureName,
FeatureType = featureType,
IsOwnQuota = isSelf
};
}
if (maybeMonthly is uint monthly && userQuotaData.MonthlyCount >= monthly)
{
return new QuotaLimit()
{
QuotaPeriod = QuotaPer.PerMonth,
Quota = monthly,
ResetsAt = now.Date.SecondOfNextMonth(),
Feature = featureName,
FeatureType = featureType,
IsOwnQuota = isSelf
};
}
await ctx.GetTable<PatronQuota>()
.InsertOrUpdateAsync(() => new()
{
UserId = userId,
FeatureType = featureType,
Feature = featureName,
DailyCount = 1,
MonthlyCount = 1,
HourlyCount = 1,
},
(old) => new()
{
HourlyCount = old.HourlyCount + 1,
DailyCount = old.DailyCount + 1,
MonthlyCount = old.MonthlyCount + 1,
},
() => new()
{
UserId = userId,
FeatureType = featureType,
Feature = featureName,
});
await tran.CommitAsync();
return (userQuotaData.HourlyCount + 1, userQuotaData.DailyCount + 1, userQuotaData.MonthlyCount + 1);
}
/// <summary>
/// Attempts to add 1 to user's quota for the command, group and module.
/// Input MUST BE lowercase
/// </summary>
/// <param name="userId">Id of the user who is attempting to run the command</param>
/// <param name="commandName">Name of the command the user is trying to run</param>
/// <param name="groupName">Name of the command's group</param>
/// <param name="moduleName">Name of the command's top level module</param>
/// <param name="isSelf">Whether this is check is for the user himself. False if it's someone else's id (owner)</param>
/// <returns>Either a succcess (user can run the command) or one of the error values.</returns>
private async ValueTask<OneOf<OneOf.Types.Success, InsufficientTier, QuotaLimit>> AttemptRunCommand(
ulong userId,
string commandName,
string groupName,
string moduleName,
bool isSelf)
{
var confData = _pConf.Data;
if (!confData.IsEnabled)
return default;
if (_creds.GetCreds().IsOwner(userId))
return default;
// get user tier
var patron = await GetPatronAsync(userId);
FeatureType quotaForFeatureType;
if (confData.Quotas.Commands.TryGetValue(commandName, out var quotaData))
{
quotaForFeatureType = FeatureType.Command;
}
else if (confData.Quotas.Groups.TryGetValue(groupName, out quotaData))
{
quotaForFeatureType = FeatureType.Group;
}
else if (confData.Quotas.Modules.TryGetValue(moduleName, out quotaData))
{
quotaForFeatureType = FeatureType.Module;
}
else
{
return default;
}
var featureName = quotaForFeatureType switch
{
FeatureType.Command => commandName,
FeatureType.Group => groupName,
FeatureType.Module => moduleName,
_ => throw new ArgumentOutOfRangeException(nameof(quotaForFeatureType))
};
if (!TryGetTierDataOrLower(quotaData, patron.Tier, out var data))
{
return new InsufficientTier()
{
Feature = featureName,
FeatureType = quotaForFeatureType,
RequiredTier = quotaData.Count == 0
? PatronTier.ComingSoon
: quotaData.Keys.First(),
UserTier = patron.Tier,
};
}
// no quota limits for this tier
if (data is null)
return default;
var quotaCheckResult = await TryIncrementQuotaCounterAsync(userId,
isSelf,
quotaForFeatureType,
featureName,
data.TryGetValue(QuotaPer.PerHour, out var hourly) ? hourly : null,
data.TryGetValue(QuotaPer.PerDay, out var daily) ? daily : null,
data.TryGetValue(QuotaPer.PerMonth, out var monthly) ? monthly : null
);
return quotaCheckResult.Match<OneOf<Success, InsufficientTier, QuotaLimit>>(
_ => new Success(),
x => x);
}
private bool TryGetTierDataOrLower<T>(
IReadOnlyDictionary<PatronTier, T?> data,
PatronTier tier,
out T? o)
{
// check for quotas on this tier
if (data.TryGetValue(tier, out o))
return true;
// if there are none, get the quota first tier below this one
// which has quotas specified
for (var i = _tiers.Length - 1; i >= 0; i--)
{
var lowerTier = _tiers[i];
if (lowerTier < tier && data.TryGetValue(lowerTier, out o))
return true;
}
// if there are none, that means the feature is intended
// to be patron-only but the quotas haven't been specified yet
// so it will be marked as "Coming Soon"
o = default;
return false;
}
public async Task<Patron> GetPatronAsync(ulong userId)
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
@ -616,128 +243,135 @@ public sealed class PatronageService
return PatronUserToPatron(max); return PatronUserToPatron(max);
} }
public async Task<UserQuotaStats> GetUserQuotaStatistic(ulong userId) public async Task<bool> LimitHitAsync(LimitedFeatureName key, ulong userId, int amount = 1)
{ {
var pConfData = _pConf.Data; if (_creds.GetCreds().IsOwner(userId))
return true;
if (!pConfData.IsEnabled) if (!_pConf.Data.IsEnabled)
return new(); return true;
var patron = await GetPatronAsync(userId); var userLimit = await GetUserLimit(key, userId);
await using var ctx = _db.GetDbContext(); if (userLimit.Quota == 0)
var allPatronQuotas = await ctx.GetTable<PatronQuota>() return false;
.Where(x => x.UserId == userId)
.ToListAsync();
var allQuotasDict = allPatronQuotas if (userLimit.Quota == -1)
.GroupBy(static x => x.FeatureType) return true;
.ToDictionary(static x => x.Key, static x => x.ToDictionary(static y => y.Feature));
allQuotasDict.TryGetValue(FeatureType.Command, out var data); return await TryAddLimit(key, userLimit, userId, amount);
var userCommandQuotaStats = GetFeatureQuotaStats(patron.Tier, data, pConfData.Quotas.Commands);
allQuotasDict.TryGetValue(FeatureType.Group, out data);
var userGroupQuotaStats = GetFeatureQuotaStats(patron.Tier, data, pConfData.Quotas.Groups);
allQuotasDict.TryGetValue(FeatureType.Module, out data);
var userModuleQuotaStats = GetFeatureQuotaStats(patron.Tier, data, pConfData.Quotas.Modules);
return new UserQuotaStats()
{
Tier = patron.Tier,
Commands = userCommandQuotaStats,
Groups = userGroupQuotaStats,
Modules = userModuleQuotaStats,
};
} }
private IReadOnlyDictionary<string, FeatureQuotaStats> GetFeatureQuotaStats( public async Task<bool> LimitForceHit(LimitedFeatureName key, ulong userId, int amount)
PatronTier patronTier,
IReadOnlyDictionary<string, PatronQuota>? allQuotasDict,
Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> commands)
{ {
var userCommandQuotaStats = new Dictionary<string, FeatureQuotaStats>(); if (_creds.GetCreds().IsOwner(userId))
foreach (var (key, quotaData) in commands) return true;
if (!_pConf.Data.IsEnabled)
return true;
var userLimit = await GetUserLimit(key, userId);
var cacheKey = CreateKey(key, userId);
await _cache.GetOrAddAsync(cacheKey, () => Task.FromResult(0), GetExpiry(userLimit));
return await TryAddLimit(key, userLimit, userId, amount);
}
private async Task<bool> TryAddLimit(
LimitedFeatureName key,
QuotaLimit userLimit,
ulong userId,
int amount)
{
var cacheKey = CreateKey(key, userId);
var cur = await _cache.GetOrAddAsync(cacheKey, () => Task.FromResult(0), GetExpiry(userLimit));
if (cur + amount < userLimit.Quota)
{ {
if (TryGetTierDataOrLower(quotaData, patronTier, out var data)) await _cache.AddAsync(cacheKey, cur + amount);
return true;
}
return false;
}
private TimeSpan? GetExpiry(QuotaLimit userLimit)
{
var now = DateTime.UtcNow;
switch (userLimit.QuotaPeriod)
{
case QuotaPer.PerHour:
return TimeSpan.FromMinutes(60 - now.Minute);
case QuotaPer.PerDay:
return TimeSpan.FromMinutes((24 * 60) - ((now.Hour * 60) + now.Minute));
case QuotaPer.PerMonth:
var firstOfNextMonth = now.FirstOfNextMonth();
return firstOfNextMonth - now;
default:
return null;
}
}
private TypedKey<int> CreateKey(LimitedFeatureName key, ulong userId)
=> new($"limited_feature:{key}:{userId}");
private readonly QuotaLimit _emptyQuota = new QuotaLimit()
{
Quota = 0,
QuotaPeriod = QuotaPer.PerDay,
};
private readonly QuotaLimit _infiniteQuota = new QuotaLimit()
{
Quota = -1,
QuotaPeriod = QuotaPer.PerDay,
};
public async Task<QuotaLimit> GetUserLimit(LimitedFeatureName name, ulong userId)
{
if (!_pConf.Data.IsEnabled)
return _infiniteQuota;
var maybePatron = await GetPatronAsync(userId);
if (maybePatron is not { } patron)
return _emptyQuota;
if (patron.ValidThru < DateTime.UtcNow)
return _emptyQuota;
foreach (var (key, value) in _pConf.Data.Limits)
{
if (patron.Amount >= key)
{ {
// if data is null that means the quota for the user's tier is unlimited if (value.TryGetValue(name, out var quotaLimit))
// no point in returning it?
if (data is null)
continue;
var (daily, hourly, monthly) = default((uint, uint, uint));
// try to get users stats for this feature
// if it fails just leave them at 0
if (allQuotasDict?.TryGetValue(key, out var quota) ?? false)
(daily, hourly, monthly) = (quota.DailyCount, quota.HourlyCount, quota.MonthlyCount);
userCommandQuotaStats[key] = new FeatureQuotaStats()
{ {
Hourly = data.TryGetValue(QuotaPer.PerHour, out var hourD) return quotaLimit;
? (hourly, hourD) }
: default,
Daily = data.TryGetValue(QuotaPer.PerDay, out var maxD) break;
? (daily, maxD)
: default,
Monthly = data.TryGetValue(QuotaPer.PerMonth, out var maxM)
? (monthly, maxM)
: default,
};
} }
} }
return userCommandQuotaStats; return _emptyQuota;
} }
public async Task<FeatureLimit> TryGetFeatureLimitAsync(FeatureLimitKey key, ulong userId, int? defaultValue) public async Task<Dictionary<LimitedFeatureName, (int, QuotaLimit)>> LimitStats(ulong userId)
{ {
var conf = _pConf.Data; var dict = new Dictionary<LimitedFeatureName, (int, QuotaLimit)>();
foreach (var featureName in Enum.GetValues<LimitedFeatureName>())
// if patron system is disabled, the quota is just default
if (!conf.IsEnabled)
return new()
{
Name = key.PrettyName,
Quota = defaultValue,
IsPatronLimit = false
};
if (!conf.Quotas.Features.TryGetValue(key.Key, out var data))
return new()
{
Name = key.PrettyName,
Quota = defaultValue,
IsPatronLimit = false,
};
var patron = await GetPatronAsync(userId);
if (!TryGetTierDataOrLower(data, patron.Tier, out var limit))
return new()
{
Name = key.PrettyName,
Quota = 0,
IsPatronLimit = true,
};
return new()
{ {
Name = key.PrettyName, var cacheKey = CreateKey(featureName, userId);
Quota = limit, var userLimit = await GetUserLimit(featureName, userId);
IsPatronLimit = true var cur = await _cache.GetOrAddAsync(cacheKey, () => Task.FromResult(0), GetExpiry(userLimit));
};
dict[featureName] = (cur, userLimit);
}
return dict;
} }
// public async Task<Patron> GiftPatronAsync(IUser user, int amount)
// {
// if (amount < 1)
// throw new ArgumentOutOfRangeException(nameof(amount));
//
//
// }
private Patron PatronUserToPatron(PatronUser user) private Patron PatronUserToPatron(PatronUser user)
=> new Patron() => new Patron()
@ -767,6 +401,22 @@ public sealed class PatronageService
}; };
} }
public int PercentBonus(Patron? maybePatron)
=> maybePatron is { } user && user.ValidThru > DateTime.UtcNow
? PercentBonus(user.Amount)
: 0;
public int PercentBonus(long amount)
=> amount switch
{
>= 10_000 => 100,
>= 5000 => 50,
>= 2000 => 20,
>= 1000 => 10,
>= 500 => 5,
_ => 0
};
private async Task SendWelcomeMessage(Patron patron) private async Task SendWelcomeMessage(Patron patron)
{ {
try try
@ -776,28 +426,28 @@ public sealed class PatronageService
return; return;
var eb = _sender.CreateEmbed() var eb = _sender.CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle("❤️ Thank you for supporting EllieBot! ❤️") .WithTitle("❤️ Thank you for supporting EllieBot! ❤️")
.WithDescription( .WithDescription(
"Your donation has been processed and you will receive the rewards shortly.\n" "Your donation has been processed and you will receive the rewards shortly.\n"
+ "You can visit <https://www.patreon.com/join/elliebot> to see rewards for your tier. 🎉") + "You can visit <https://www.patreon.com/join/elliebot> to see rewards for your tier. 🎉")
.AddField("Tier", Format.Bold(patron.Tier.ToString()), true) .AddField("Tier", Format.Bold(patron.Tier.ToString()), true)
.AddField("Pledge", $"**{patron.Amount / 100.0f:N1}$**", true) .AddField("Pledge", $"**{patron.Amount / 100.0f:N1}$**", true)
.AddField("Expires", .AddField("Expires",
patron.ValidThru.AddDays(1).ToShortAndRelativeTimestampTag(), patron.ValidThru.AddDays(1).ToShortAndRelativeTimestampTag(),
true) true)
.AddField("Instructions", .AddField("Instructions",
""" """
*- Within the next **1-2 minutes** you will have all of the benefits of the Tier you've subscribed to.* *- Within the next **1-2 minutes** you will have all of the benefits of the Tier you've subscribed to.*
*- You can check your benefits on <https://www.patreon.com/join/elliebot>* *- You can check your benefits on <https://www.patreon.com/join/elliebot>*
*- You can use the `.patron` command in this chat to check your current quota usage for the Patron-only commands* *- You can use the `.patron` command in this chat to check your current quota usage for the Patron-only commands*
*- **ALL** of the servers that you **own** will enjoy your Patron benefits.* *- **ALL** of the servers that you **own** will enjoy your Patron benefits.*
*- You can use any of the commands available in your tier on any server (assuming you have sufficient permissions to run those commands)* *- You can use any of the commands available in your tier on any server (assuming you have sufficient permissions to run those commands)*
*- Any user in any of your servers can use Patron-only commands, but they will spend **your quota**, which is why it's recommended to use Ellie's command cooldown system (.h .cmdcd) or permission system to limit the command usage for your server members.* *- Any user in any of your servers can use Patron-only commands, but they will spend **your quota**, which is why it's recommended to use Ellie's command cooldown system (.h .cmdcd) or permission system to limit the command usage for your server members.*
*- Permission guide can be found here if you're not familiar with it: <https://docs.elliebot.net/ellie/placeholders/>* *- Permission guide can be found here if you're not familiar with it: <https://docs.elliebot.net/ellie/features/permissions-system/>*
""", """,
inline: false) inline: false)
.WithFooter($"platform id: {patron.UniquePlatformUserId}"); .WithFooter($"platform id: {patron.UniquePlatformUserId}");
await _sender.Response(user).Embed(eb).SendAsync(); await _sender.Response(user).Embed(eb).SendAsync();
} }

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
using EllieBot.Modules.Permissions.Services; using EllieBot.Modules.Permissions.Services;
using EllieBot.Db.Models; using EllieBot.Db.Models;
@ -46,7 +46,7 @@ public partial class Permissions
Log.Warning("Can't get {BlacklistType} [{BlacklistItemId}]", Log.Warning("Can't get {BlacklistType} [{BlacklistItemId}]",
i.Type, i.Type,
i.ItemId); i.ItemId);
return Task.FromResult(Format.Code(i.ItemId.ToString())); return Task.FromResult(Format.Code(i.ItemId.ToString()));
} }
}) })

Some files were not shown because too many files have changed in this diff Show more