Compare commits
No commits in common. "fb17ad7ad57a07e0a9cbcd82da2f7cdda664612e" and "ae42d6ce37ff1a5110e5af040245e146d5ea11c6" have entirely different histories.
fb17ad7ad5
...
ae42d6ce37
238 changed files with 2126 additions and 12623 deletions
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -2,40 +2,6 @@
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -53,6 +53,8 @@ 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; }
|
||||||
|
@ -595,6 +597,16 @@ 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
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ 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; }
|
||||||
|
|
|
@ -1,17 +1,41 @@
|
||||||
#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()
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>true</ImplicitUsings>
|
<ImplicitUsings>true</ImplicitUsings>
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
<Version>5.1.0</Version>
|
<Version>5.0.8</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.3" />
|
<PackageReference Include="SharpToken" Version="2.0.2" />
|
||||||
|
|
||||||
<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" />
|
||||||
|
|
|
@ -1418,7 +1418,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1422,7 +1422,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1451,7 +1451,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1451,7 +1451,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1521,7 +1521,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1554,7 +1554,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1558,7 +1558,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1596,7 +1596,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1600,7 +1600,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1588,7 +1588,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1592,7 +1592,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1592,7 +1592,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1621,7 +1621,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1654,7 +1654,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1658,7 +1658,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1666,7 +1666,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1670,7 +1670,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1700,7 +1700,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Db.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1670,7 +1670,7 @@ namespace EllieBot.Migrations.Mysql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Db.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,44 +0,0 @@
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1718,6 +1718,41 @@ 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")
|
||||||
|
|
|
@ -1490,7 +1490,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1521,7 +1521,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1521,7 +1521,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1591,7 +1591,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1626,7 +1626,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1630,7 +1630,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1670,7 +1670,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1674,7 +1674,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1662,7 +1662,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1666,7 +1666,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1666,7 +1666,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1697,7 +1697,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1732,7 +1732,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1736,7 +1736,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1744,7 +1744,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1748,7 +1748,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1699,7 +1699,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Db.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -1669,7 +1669,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
modelBuilder.Entity("EllieBot.Db.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,42 +0,0 @@
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1717,6 +1717,41 @@ 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")
|
||||||
|
|
|
@ -11,8 +11,6 @@ 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
|
@ -1,42 +0,0 @@
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1279,6 +1279,33 @@ 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")
|
||||||
|
|
|
@ -61,66 +61,17 @@ 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,
|
||||||
|
|
|
@ -45,43 +45,23 @@ 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)
|
||||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
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
|
||||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
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)]
|
||||||
|
@ -103,21 +83,19 @@ 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)
|
||||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
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
|
||||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,10 +155,9 @@ 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)
|
||||||
{
|
{
|
||||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
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,
|
||||||
|
@ -189,7 +166,7 @@ public partial class Administration
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
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,
|
||||||
|
@ -197,7 +174,6 @@ public partial class Administration
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await SendResult(result);
|
|
||||||
await progressMsg.DeleteAsync();
|
await progressMsg.DeleteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace EllieBot.Modules.Administration.Services;
|
|
||||||
|
|
||||||
public enum PruneResult
|
|
||||||
{
|
|
||||||
Success,
|
|
||||||
AlreadyRunning,
|
|
||||||
FeatureLimit,
|
|
||||||
}
|
|
|
@ -1,6 +1,4 @@
|
||||||
#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
|
||||||
|
@ -9,15 +7,11 @@ 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, IPatronageService ps)
|
public PruneService(ILogCommandService logService)
|
||||||
{
|
=> _logService = logService;
|
||||||
_logService = logService;
|
|
||||||
_ps = ps;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PruneResult> PruneWhere(
|
public async Task PruneWhere(
|
||||||
ITextChannel channel,
|
ITextChannel channel,
|
||||||
int amount,
|
int amount,
|
||||||
Func<IMessage, bool> predicate,
|
Func<IMessage, bool> predicate,
|
||||||
|
@ -32,13 +26,8 @@ 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 PruneResult.AlreadyRunning;
|
return;
|
||||||
|
|
||||||
if (!await _ps.LimitHitAsync(LimitedFeatureName.Prune, channel.Guild.OwnerId))
|
|
||||||
{
|
|
||||||
return PruneResult.FeatureLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
@ -58,7 +47,7 @@ public class PruneService : IEService
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
if (!msgs.Any())
|
if (!msgs.Any())
|
||||||
return PruneResult.Success;
|
return;
|
||||||
|
|
||||||
lastMessage = msgs[^1];
|
lastMessage = msgs[^1];
|
||||||
|
|
||||||
|
@ -99,8 +88,6 @@ 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)
|
||||||
|
|
|
@ -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, Error>> AddReactionRole(
|
Task<OneOf<Success, FeatureLimit>> AddReactionRole(
|
||||||
IGuild guild,
|
IGuild guild,
|
||||||
IMessage msg,
|
IMessage msg,
|
||||||
string emote,
|
string emote,
|
||||||
|
|
|
@ -55,10 +55,12 @@ public partial class Administration
|
||||||
|
|
||||||
await res.Match(
|
await res.Match(
|
||||||
_ => ctx.OkAsync(),
|
_ => ctx.OkAsync(),
|
||||||
async fl =>
|
fl =>
|
||||||
{
|
{
|
||||||
_ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
|
_ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
|
||||||
await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
|
return !fl.IsPatronLimit
|
||||||
|
? Response().Error(strs.limit_reached(fl.Quota)).SendAsync()
|
||||||
|
: Response().Pending(strs.feature_limit_reached_owner(fl.Quota, fl.Name)).SendAsync();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,16 +21,22 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +242,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, Error>> AddReactionRole(
|
public async Task<OneOf<Success, FeatureLimit>> AddReactionRole(
|
||||||
IGuild guild,
|
IGuild guild,
|
||||||
IMessage msg,
|
IMessage msg,
|
||||||
string emote,
|
string emote,
|
||||||
|
@ -255,12 +261,9 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
|
||||||
.Where(x => x.GuildId == guild.Id)
|
.Where(x => x.GuildId == guild.Id)
|
||||||
.CountAsync();
|
.CountAsync();
|
||||||
|
|
||||||
var limit = await _ps.GetUserLimit(LimitedFeatureName.ReactionRole, guild.OwnerId);
|
var result = await _ps.TryGetFeatureLimitAsync(_reroFLKey, guild.OwnerId, 50);
|
||||||
|
if (result.Quota != -1 && activeReactionRoles >= result.Quota)
|
||||||
if (!_creds.IsOwner(guild.OwnerId) && (activeReactionRoles >= limit.Quota && limit.Quota >= 0))
|
return result;
|
||||||
{
|
|
||||||
return new Error();
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.GetTable<ReactionRoleV2>()
|
await ctx.GetTable<ReactionRoleV2>()
|
||||||
.InsertOrUpdateAsync(() => new()
|
.InsertOrUpdateAsync(() => new()
|
||||||
|
|
|
@ -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/elliebot/raw/branch/v5/CHANGELOG.md");
|
var changelog = await http.GetStringAsync("https://toastielab.dev/Emotions-stuff/Ellie/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/elliebot/src/branch/v5/CHANGELOG.md")
|
.WithUrl("https://toastielab.dev/Emotions-stuff/Ellie/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'");
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using EllieBot.Db.Models;
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
namespace EllieBot.Modules.EllieExpressions;
|
namespace EllieBot.Modules.EllieExpressions;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using EllieBot.Db.Models;
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
namespace EllieBot.Modules.EllieExpressions;
|
namespace EllieBot.Modules.EllieExpressions;
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#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;
|
||||||
|
@ -13,7 +14,6 @@ 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,6 +133,12 @@ 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);
|
||||||
|
@ -148,7 +154,6 @@ 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,
|
||||||
|
@ -158,17 +163,6 @@ 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()
|
||||||
|
@ -181,31 +175,25 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } remainder)
|
var inter = CreateRemindMeInteraction(period);
|
||||||
{
|
|
||||||
// Get correct time form remainder
|
|
||||||
var interaction = CreateRemindMeInteraction(remainder.TotalMilliseconds);
|
|
||||||
|
|
||||||
|
if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } rem)
|
||||||
|
{
|
||||||
// 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))
|
||||||
{
|
{
|
||||||
interaction = null;
|
inter = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var relativeTag = TimestampTag.FromDateTime(now.Add(remainder), TimestampTagStyles.Relative);
|
var relativeTag = TimestampTag.FromDateTime(now.Add(rem), TimestampTagStyles.Relative);
|
||||||
await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(interaction).SendAsync();
|
await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(inter).SendAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var patron = await _ps.GetPatronAsync(ctx.User.Id);
|
var result = await _ps.TryGetFeatureLimitAsync(_timelyKey, ctx.User.Id, 0);
|
||||||
|
|
||||||
var percentBonus = (_ps.PercentBonus(patron) / 100f);
|
val = (int)(val * (1 + (result.Quota! * 0.01f)));
|
||||||
|
|
||||||
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"));
|
||||||
|
|
||||||
|
@ -904,7 +892,6 @@ 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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -247,14 +247,7 @@ public partial class Gambling
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var buyer = (IGuildUser)ctx.User;
|
var cmd = entry.Command.Replace("%you%", ctx.User.Id.ToString());
|
||||||
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")
|
||||||
|
@ -266,7 +259,6 @@ 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(
|
||||||
|
|
|
@ -9,7 +9,6 @@ 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;
|
||||||
|
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#nullable disable
|
#nullable disable
|
||||||
namespace EllieBot.Db.Models;
|
namespace EllieBot.Db.Models;
|
||||||
|
|
||||||
public class WaifuItem : DbEntity
|
public class WaifuItem : DbEntity
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#nullable disable
|
#nullable disable
|
||||||
namespace EllieBot.Db.Models;
|
namespace EllieBot.Db.Models;
|
||||||
|
|
||||||
public class WaifuUpdate : DbEntity
|
public class WaifuUpdate : DbEntity
|
||||||
|
|
|
@ -15,32 +15,43 @@ 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,
|
||||||
IPatronageService ps,
|
CommandHandler cmd,
|
||||||
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)
|
||||||
|
@ -58,9 +69,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.Gpt:
|
case ChatBotImplementation.Gpt3:
|
||||||
if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey))
|
if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey))
|
||||||
return new OfficialGptSession(_creds.Gpt3ApiKey,
|
return new OfficialGpt3Session(_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,
|
||||||
|
@ -76,21 +87,22 @@ public class ChatterBotService : IExecOnMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IChatterBotSession GetOrCreateSession(ulong guildId)
|
public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot)
|
||||||
{
|
{
|
||||||
if (ChatterBotGuilds.TryGetValue(guildId, out var lazyChatBot))
|
var channel = msg.Channel as ITextChannel;
|
||||||
return lazyChatBot.Value;
|
cleverbot = null;
|
||||||
|
|
||||||
lazyChatBot = new(() => CreateSession(), true);
|
if (channel is null)
|
||||||
ChatterBotGuilds.TryAdd(guildId, lazyChatBot);
|
return null;
|
||||||
return lazyChatBot.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PrepareMessage(IUserMessage msg)
|
if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out var lazyCleverbot))
|
||||||
{
|
return null;
|
||||||
var nadekoId = _client.CurrentUser.Id;
|
|
||||||
var normalMention = $"<@{nadekoId}> ";
|
cleverbot = lazyCleverbot.Value;
|
||||||
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();
|
||||||
|
@ -107,31 +119,13 @@ 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 res = await _perms.CheckPermsAsync(guild,
|
var message = PrepareMessage(usrMsg, out var cbs);
|
||||||
|
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,
|
||||||
|
@ -140,33 +134,59 @@ public class ChatterBotService : IExecOnMessage
|
||||||
if (!res.IsAllowed)
|
if (!res.IsAllowed)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!await _ps.LimitHitAsync(LimitedFeatureName.ChatBot, usrMsg.Author.Id, 2048 / 2))
|
var channel = (ITextChannel)usrMsg.Channel;
|
||||||
|
var conf = _ps.GetConfig();
|
||||||
|
if (!_creds.IsOwner(sg.OwnerId) && conf.IsEnabled)
|
||||||
{
|
{
|
||||||
// limit exceeded
|
var quota = await _ps.TryGetFeatureLimitAsync(_flKey, sg.OwnerId, 0);
|
||||||
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 chatBot.Think(message, usrMsg.Author.ToString());
|
var response = await cbs.Think(message, usrMsg.Author.ToString());
|
||||||
|
await _sender.Response(channel)
|
||||||
if (response.TryPickT0(out var result, out var error))
|
.Confirm(response)
|
||||||
{
|
.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
|
||||||
|
|
|
@ -18,8 +18,7 @@ public partial class Games
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[UserPerm(GuildPerm.ManageMessages)]
|
[UserPerm(GuildPerm.ManageMessages)]
|
||||||
[NoPublicBot]
|
public async Task Cleverbot()
|
||||||
public async Task CleverBot()
|
|
||||||
{
|
{
|
||||||
var channel = (ITextChannel)ctx.Channel;
|
var channel = (ITextChannel)ctx.Channel;
|
||||||
|
|
||||||
|
@ -31,7 +30,7 @@ public partial class Games
|
||||||
await uow.SaveChangesAsync();
|
await uow.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
await Response().Confirm(strs.chatbot_disabled).SendAsync();
|
await Response().Confirm(strs.cleverbot_disabled).SendAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +42,7 @@ public partial class Games
|
||||||
await uow.SaveChangesAsync();
|
await uow.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
await Response().Confirm(strs.chatbot_enabled).SendAsync();
|
await Response().Confirm(strs.cleverbot_enabled).SendAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,25 +3,10 @@ using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EllieBot.Modules.Games.Common.ChatterBot;
|
namespace EllieBot.Modules.Games.Common.ChatterBot;
|
||||||
|
|
||||||
public class OpenAiCompletionResponse
|
public class Gpt3Response
|
||||||
{
|
{
|
||||||
[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
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
#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<OneOf<ThinkResult, Error<string>>> Think(string input, string username);
|
Task<string> Think(string input, string username);
|
||||||
}
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
#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;
|
||||||
|
|
||||||
|
@ -20,7 +18,7 @@ public class OfficialCleverbotSession : IChatterBotSession
|
||||||
_httpFactory = factory;
|
_httpFactory = factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OneOf<ThinkResult, Error<string>>> Think(string input, string username)
|
public async Task<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 ?? ""));
|
||||||
|
@ -29,17 +27,12 @@ public class OfficialCleverbotSession : IChatterBotSession
|
||||||
var data = JsonConvert.DeserializeObject<CleverbotResponse>(dataString);
|
var data = JsonConvert.DeserializeObject<CleverbotResponse>(dataString);
|
||||||
|
|
||||||
cs = data?.Cs;
|
cs = data?.Cs;
|
||||||
return new ThinkResult
|
return data?.Output;
|
||||||
{
|
|
||||||
Text = data?.Output,
|
|
||||||
TokensIn = 2,
|
|
||||||
TokensOut = 1
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
Log.Warning("Unexpected response from CleverBot: {ResponseString}", dataString);
|
Log.Warning("Unexpected cleverbot response received: {ResponseString}", dataString);
|
||||||
return new Error<string>("Unexpected CleverBot response received");
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,141 +0,0 @@
|
||||||
#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; }
|
|
||||||
}
|
|
|
@ -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; } = 4;
|
public int Version { get; set; } = 3;
|
||||||
|
|
||||||
[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.
|
||||||
'gpt' - bot will use GPT API")]
|
'gpt3' - bot will use GPT-3 API")]
|
||||||
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt;
|
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt3;
|
||||||
|
|
||||||
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 Model should bot use.
|
[Comment(@"Which GPT-3 Model should bot use.
|
||||||
gpt35turbo - cheapest
|
gpt35turbo - cheapest
|
||||||
gpt4o - more expensive, higher quality
|
gpt4 - 30x 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 API call")]
|
[Comment(@"The maximum number of tokens to use per GPT-3 API call")]
|
||||||
public int MaxTokens { get; set; } = 100;
|
public int MaxTokens { get; set; } = 100;
|
||||||
|
|
||||||
[Comment(@"The minimum number of tokens to use per GPT API call, such that chat history is removed to make room.")]
|
[Comment(@"The minimum number of tokens to use per GPT-3 API call, such that chat history is removed to make room.")]
|
||||||
public int MinTokens { get; set; } = 30;
|
public int MinTokens { get; set; } = 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,18 +163,12 @@ public sealed partial class RaceAnimal
|
||||||
public enum ChatBotImplementation
|
public enum ChatBotImplementation
|
||||||
{
|
{
|
||||||
Cleverbot,
|
Cleverbot,
|
||||||
Gpt = 1,
|
Gpt3
|
||||||
[Obsolete]
|
|
||||||
Gpt3 = 1,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ChatGptModel
|
public enum ChatGptModel
|
||||||
{
|
{
|
||||||
[Obsolete]
|
|
||||||
Gpt4,
|
|
||||||
[Obsolete]
|
|
||||||
Gpt432k,
|
|
||||||
|
|
||||||
Gpt35Turbo,
|
Gpt35Turbo,
|
||||||
Gpt4o,
|
Gpt4,
|
||||||
|
Gpt432k
|
||||||
}
|
}
|
|
@ -73,6 +73,15 @@ 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 =>
|
||||||
|
@ -81,19 +90,5 @@ 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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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/elliebot) 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/Ellie) 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*
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#nullable disable
|
#nullable disable
|
||||||
namespace EllieBot.Db.Models;
|
namespace EllieBot.Db.Models;
|
||||||
|
|
||||||
public class MusicPlaylist : DbEntity
|
public class MusicPlaylist : DbEntity
|
||||||
|
|
|
@ -4,11 +4,10 @@ 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";
|
||||||
|
|
||||||
|
@ -32,14 +31,5 @@ public class PatronageConfig : ConfigServiceBase<PatronConfigData>
|
||||||
c.IsEnabled = false;
|
c.IsEnabled = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
ModifyConfig(c =>
|
|
||||||
{
|
|
||||||
if (c.Version == 2)
|
|
||||||
{
|
|
||||||
c.Version = 3;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
#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;
|
||||||
|
@ -9,7 +8,7 @@ using EllieBot.Db.Models;
|
||||||
|
|
||||||
namespace EllieBot.Modules.Utility;
|
namespace EllieBot.Modules.Utility;
|
||||||
|
|
||||||
public sealed class CurrencyRewardService : IEService, IReadyExecutor
|
public sealed class CurrencyRewardService : IEService, IDisposable
|
||||||
{
|
{
|
||||||
private readonly ICurrencyService _cs;
|
private readonly ICurrencyService _cs;
|
||||||
private readonly IPatronageService _ps;
|
private readonly IPatronageService _ps;
|
||||||
|
@ -33,14 +32,16 @@ public sealed class CurrencyRewardService : IEService, IReadyExecutor
|
||||||
_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)
|
||||||
|
@ -57,17 +58,17 @@ public sealed class CurrencyRewardService : IEService, IReadyExecutor
|
||||||
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()
|
||||||
|
@ -90,21 +91,21 @@ public sealed class CurrencyRewardService : IEService, IReadyExecutor
|
||||||
(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}%)!");
|
||||||
}
|
}
|
||||||
|
@ -139,12 +140,12 @@ public sealed class CurrencyRewardService : IEService, IReadyExecutor
|
||||||
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,
|
||||||
|
@ -155,7 +156,7 @@ public sealed class CurrencyRewardService : IEService, IReadyExecutor
|
||||||
{
|
{
|
||||||
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,
|
||||||
|
@ -173,7 +174,7 @@ public sealed class CurrencyRewardService : IEService, IReadyExecutor
|
||||||
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
|
||||||
|
|
11
src/EllieBot/Modules/Patronage/InsufficientTier.cs
Normal file
11
src/EllieBot/Modules/Patronage/InsufficientTier.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
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; }
|
||||||
|
}
|
|
@ -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,8 +140,9 @@ 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));
|
||||||
|
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -25,4 +25,9 @@ public sealed class PatreonMemberData : ISubscriberData
|
||||||
"Declined" or "Pending" => SubscriptionChargeStatus.Unpaid,
|
"Declined" or "Pending" => SubscriptionChargeStatus.Unpaid,
|
||||||
_ => SubscriptionChargeStatus.Other,
|
_ => SubscriptionChargeStatus.Other,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PatreonPledgeData
|
||||||
|
{
|
||||||
|
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,16 +71,17 @@ public partial class Help
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var maybePatron = await _service.GetPatronAsync(user.Id);
|
var patron = 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.Count == 0 || maybePatron is not { } patron)
|
if (quotaStats.Commands.Count == 0
|
||||||
|
&& quotaStats.Groups.Count == 0
|
||||||
|
&& quotaStats.Modules.Count == 0)
|
||||||
{
|
{
|
||||||
eb.WithDescription(GetText(strs.no_quota_found));
|
eb.WithDescription(GetText(strs.no_quota_found));
|
||||||
}
|
}
|
||||||
|
@ -96,9 +97,26 @@ public partial class Help
|
||||||
|
|
||||||
eb.AddField(GetText(strs.quotas), "", false);
|
eb.AddField(GetText(strs.quotas), "", false);
|
||||||
|
|
||||||
var text = GetQuotaList(quotaStats);
|
if (quotaStats.Commands.Count > 0)
|
||||||
if (!string.IsNullOrWhiteSpace(text))
|
{
|
||||||
eb.AddField(GetText(strs.modules), text, true);
|
var text = GetQuotaList(quotaStats.Commands);
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,28 +131,26 @@ public partial class Help
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetQuotaList(
|
private string GetQuotaList(IReadOnlyDictionary<string, FeatureQuotaStats> featureQuotaStats)
|
||||||
IReadOnlyDictionary<LimitedFeatureName, (int Cur, QuotaLimit Quota)> featureQuotaStats)
|
|
||||||
{
|
{
|
||||||
var text = string.Empty;
|
var text = string.Empty;
|
||||||
foreach (var (key, (cur, quota)) in featureQuotaStats)
|
foreach (var (key, q) in featureQuotaStats)
|
||||||
{
|
{
|
||||||
text += $"\n\t`{key}`\n";
|
text += $"\n\t`{key}`\n";
|
||||||
if (quota.QuotaPeriod == QuotaPer.PerHour)
|
if (q.Hourly != default)
|
||||||
text += $" {cur}/{(quota.Quota == -1 ? "∞" : quota.Quota)} {QuotaPeriodToString(quota.QuotaPeriod)}\n";
|
text += $" {GetEmoji(q.Hourly)} {q.Hourly.Cur}/{q.Hourly.Max} per hour\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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string QuotaPeriodToString(QuotaPer per)
|
private string GetEmoji((uint Cur, uint Max) limit)
|
||||||
=> per switch
|
=> limit.Cur < limit.Max
|
||||||
{
|
? "✅"
|
||||||
QuotaPer.PerHour => "per hour",
|
: "⚠️";
|
||||||
QuotaPer.PerDay => "per day",
|
|
||||||
QuotaPer.PerMonth => "per month",
|
|
||||||
QuotaPer.Total => "total",
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(per), per, null)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,8 +2,9 @@
|
||||||
using LinqToDB.EntityFrameworkCore;
|
using LinqToDB.EntityFrameworkCore;
|
||||||
using EllieBot.Common.ModuleBehaviors;
|
using EllieBot.Common.ModuleBehaviors;
|
||||||
using EllieBot.Db.Models;
|
using EllieBot.Db.Models;
|
||||||
using StackExchange.Redis;
|
using OneOf;
|
||||||
using System.Diagnostics;
|
using OneOf.Types;
|
||||||
|
using CommandInfo = Discord.Commands.CommandInfo;
|
||||||
|
|
||||||
namespace EllieBot.Modules.Patronage;
|
namespace EllieBot.Modules.Patronage;
|
||||||
|
|
||||||
|
@ -11,6 +12,7 @@ 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; };
|
||||||
|
@ -58,7 +60,7 @@ public sealed class PatronageService
|
||||||
if (_client.ShardId != 0)
|
if (_client.ShardId != 0)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
return Task.WhenAll(LoadSubscribersLoopAsync());
|
return Task.WhenAll(ResetLoopAsync(), LoadSubscribersLoopAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadSubscribersLoopAsync()
|
private async Task LoadSubscribersLoopAsync()
|
||||||
|
@ -83,6 +85,71 @@ 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
|
||||||
|
@ -136,8 +203,7 @@ 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
|
.Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId)
|
||||||
== subscriber.UniquePlatformUserId)
|
|
||||||
.UpdateAsync(old => new()
|
.UpdateAsync(old => new()
|
||||||
{
|
{
|
||||||
UserId = subscriber.UserId,
|
UserId = subscriber.UserId,
|
||||||
|
@ -149,13 +215,14 @@ public sealed class PatronageService
|
||||||
: dateInOneMonth,
|
: dateInOneMonth,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// this should never happen
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
// await tran.RollbackAsync();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
dbPatron.UserId = subscriber.UserId;
|
// await tran.CommitAsync();
|
||||||
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));
|
||||||
}
|
}
|
||||||
|
@ -217,7 +284,313 @@ public sealed class PatronageService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Patron?> GetPatronAsync(ulong userId)
|
public async Task<bool> ExecPreCommandAsync(
|
||||||
|
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();
|
||||||
|
|
||||||
|
@ -243,135 +616,128 @@ public sealed class PatronageService
|
||||||
return PatronUserToPatron(max);
|
return PatronUserToPatron(max);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> LimitHitAsync(LimitedFeatureName key, ulong userId, int amount = 1)
|
public async Task<UserQuotaStats> GetUserQuotaStatistic(ulong userId)
|
||||||
{
|
{
|
||||||
if (_creds.GetCreds().IsOwner(userId))
|
var pConfData = _pConf.Data;
|
||||||
return true;
|
|
||||||
|
|
||||||
if (!_pConf.Data.IsEnabled)
|
if (!pConfData.IsEnabled)
|
||||||
return true;
|
return new();
|
||||||
|
|
||||||
var userLimit = await GetUserLimit(key, userId);
|
var patron = await GetPatronAsync(userId);
|
||||||
|
|
||||||
if (userLimit.Quota == 0)
|
await using var ctx = _db.GetDbContext();
|
||||||
return false;
|
var allPatronQuotas = await ctx.GetTable<PatronQuota>()
|
||||||
|
.Where(x => x.UserId == userId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
if (userLimit.Quota == -1)
|
var allQuotasDict = allPatronQuotas
|
||||||
return true;
|
.GroupBy(static x => x.FeatureType)
|
||||||
|
.ToDictionary(static x => x.Key, static x => x.ToDictionary(static y => y.Feature));
|
||||||
|
|
||||||
return await TryAddLimit(key, userLimit, userId, amount);
|
allQuotasDict.TryGetValue(FeatureType.Command, out var data);
|
||||||
}
|
var userCommandQuotaStats = GetFeatureQuotaStats(patron.Tier, data, pConfData.Quotas.Commands);
|
||||||
|
|
||||||
public async Task<bool> LimitForceHit(LimitedFeatureName key, ulong userId, int amount)
|
allQuotasDict.TryGetValue(FeatureType.Group, out data);
|
||||||
{
|
var userGroupQuotaStats = GetFeatureQuotaStats(patron.Tier, data, pConfData.Quotas.Groups);
|
||||||
if (_creds.GetCreds().IsOwner(userId))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (!_pConf.Data.IsEnabled)
|
allQuotasDict.TryGetValue(FeatureType.Module, out data);
|
||||||
return true;
|
var userModuleQuotaStats = GetFeatureQuotaStats(patron.Tier, data, pConfData.Quotas.Modules);
|
||||||
|
|
||||||
var userLimit = await GetUserLimit(key, userId);
|
return new UserQuotaStats()
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
await _cache.AddAsync(cacheKey, cur + amount);
|
Tier = patron.Tier,
|
||||||
return true;
|
Commands = userCommandQuotaStats,
|
||||||
}
|
Groups = userGroupQuotaStats,
|
||||||
|
Modules = userModuleQuotaStats,
|
||||||
return false;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private TimeSpan? GetExpiry(QuotaLimit userLimit)
|
private IReadOnlyDictionary<string, FeatureQuotaStats> GetFeatureQuotaStats(
|
||||||
|
PatronTier patronTier,
|
||||||
|
IReadOnlyDictionary<string, PatronQuota>? allQuotasDict,
|
||||||
|
Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> commands)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var userCommandQuotaStats = new Dictionary<string, FeatureQuotaStats>();
|
||||||
switch (userLimit.QuotaPeriod)
|
foreach (var (key, quotaData) in commands)
|
||||||
{
|
{
|
||||||
case QuotaPer.PerHour:
|
if (TryGetTierDataOrLower(quotaData, patronTier, out var data))
|
||||||
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 (value.TryGetValue(name, out var quotaLimit))
|
// if data is null that means the quota for the user's tier is unlimited
|
||||||
{
|
// no point in returning it?
|
||||||
return quotaLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
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)
|
||||||
|
? (hourly, hourD)
|
||||||
|
: default,
|
||||||
|
Daily = data.TryGetValue(QuotaPer.PerDay, out var maxD)
|
||||||
|
? (daily, maxD)
|
||||||
|
: default,
|
||||||
|
Monthly = data.TryGetValue(QuotaPer.PerMonth, out var maxM)
|
||||||
|
? (monthly, maxM)
|
||||||
|
: default,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return _emptyQuota;
|
return userCommandQuotaStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Dictionary<LimitedFeatureName, (int, QuotaLimit)>> LimitStats(ulong userId)
|
public async Task<FeatureLimit> TryGetFeatureLimitAsync(FeatureLimitKey key, ulong userId, int? defaultValue)
|
||||||
{
|
{
|
||||||
var dict = new Dictionary<LimitedFeatureName, (int, QuotaLimit)>();
|
var conf = _pConf.Data;
|
||||||
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()
|
||||||
{
|
{
|
||||||
var cacheKey = CreateKey(featureName, userId);
|
Name = key.PrettyName,
|
||||||
var userLimit = await GetUserLimit(featureName, userId);
|
Quota = limit,
|
||||||
var cur = await _cache.GetOrAddAsync(cacheKey, () => Task.FromResult(0), GetExpiry(userLimit));
|
IsPatronLimit = true
|
||||||
|
};
|
||||||
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()
|
||||||
|
@ -401,22 +767,6 @@ 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
|
||||||
|
@ -426,28 +776,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/features/permissions-system/>*
|
*- Permission guide can be found here if you're not familiar with it: <https://docs.elliebot.net/ellie/placeholders/>*
|
||||||
""",
|
""",
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue