Compare commits

...

39 Commits

Author SHA1 Message Date
Kwoth
e49e3eec69 Version upped to 4.3.10, CHANGELOG.md updated 2022-11-10 22:42:34 +01:00
Kwoth
3992ae392b Fixed nullref in xploop 2022-11-10 22:32:27 +01:00
Kwoth
8f0c5fab47 Fixed nullref in cmdcd 2022-11-08 07:51:29 +01:00
Kwoth
780a260b88 If cleverbot is disabled through permissions, let other things execute 2022-11-08 07:51:20 +01:00
Kwoth
25692b9585 Fixed .feedadd for real 2022-11-04 17:21:05 +01:00
Kwoth
ed3ce52865 Fixed .feedadd, closes #388 2022-11-04 05:08:22 +01:00
Kwoth
f5f0f1e250 Fixed .prune @Target not working bug 2022-10-25 02:07:05 +02:00
Kwoth
9d9e61fdfb Fixed command cooldown calculation. Closes #387 2022-10-25 01:57:58 +02:00
Kwoth
e68e948a80 Aliases now support %target% placeholder. For example 'alias .bft .bf %target% t' 2022-10-22 23:33:42 +02:00
Kwoth
cb98f4aa15 Added .exprtoggleglobal / .extg which can be used to toggle usage of global expressions on the server 2022-10-21 21:31:59 +02:00
Kwoth
bfec0cbcbf OwnerId will be autofilled in creds.yml if it's missing at startup 2022-10-19 23:43:44 +02:00
Kwoth
3e1268f3bb You can now specify time+date (time is optional) in remind instead of relative time, in the format HH:mm dd.MM.YYYY 2022-10-18 23:31:34 +02:00
Kwoth
c28f458972 Updated help text for .antispam and .antiraid 2022-10-18 01:45:00 +02:00
Kwoth
27ac948463 Added .forwardtochannel which will forward messages to the current channel. It has lower priority than fwtoall 2022-10-17 22:20:48 +02:00
Kwoth
3f9a3c4c18 Merge branch 'hokutochen-v4-patch-12507' into 'v4'
upped medusa version and updated brew command

See merge request Kwoth/nadekobot!271
2022-10-17 11:11:37 +00:00
Hokuto Chen
9a5545a951 docs: upped medusa version and updated brew command 2022-10-17 11:11:37 +00:00
Kwoth
584193db18 Added .filterlist command 2022-10-16 22:57:36 +02:00
Kwoth
1a132fd234 Added a missing return 2022-10-15 13:17:30 +02:00
Kwoth
fd6a51ac82 Added appropriate error messages for every club related command. Removed obsolete error messages. 2022-10-15 13:13:31 +02:00
Kwoth
eb1fabb2b7 Prepared several more enums to clarify club related action results 2022-10-14 23:02:43 +02:00
Kwoth
d079e684bd Clubleave errors clarified. Moved enum results to their own files 2022-10-13 22:27:12 +02:00
Kwoth
bf817a1436 Medusa modules (sneks) should now inherit medusa description when listed in .mdls command 2022-10-12 23:29:48 +02:00
Kwoth
78f1624aaf .meload and .meunload are now case sensitive. Previously loaded medusae may need to be reloaded or data/medusae/medusa.yml may need to be edited manually 2022-10-12 20:59:04 +02:00
Kwoth
793a49fc64 Updated version to 4.3.9 2022-10-12 20:20:05 +02:00
Kwoth
8b6be656b3 Updated CHANGELOG.md 2022-10-12 20:19:33 +02:00
Kwoth
89a88304dc If version is not specified, Nadeko.Medusa will default to 5.0.0 to allow for full compatibility for from-source-selfhosters 2022-10-12 20:10:55 +02:00
Kwoth
a7fe9ae08f .betstats now looks uwu 2022-10-11 19:59:58 +02:00
Kwoth
0469705037 Clarified .feedadd errors 2022-10-11 19:14:16 +02:00
Kwoth
dc568fe0e2 Removed a duplicate string key 2022-10-11 18:29:53 +02:00
Kwoth
eb01bb6c08 Removed ambiguity in clubban and clubunban error messages 2022-10-11 18:21:45 +02:00
Kwoth
71a3539d0e Possible fix for Remind .timely button 2022-10-10 18:39:49 +02:00
Kwoth
c896a0cdb8 Added better error messages to .clubapply 2022-10-09 16:25:28 +02:00
Kwoth
8effe817ad Fixed a bug with postgresql and mysql migration folders as well as compilation warning related to it 2022-10-08 09:05:31 +02:00
Kwoth
eedf6998b6 Fixed a bug in cmdcd not modifying the database. Added total stats to .betstats 2022-10-07 11:01:26 +02:00
Kwoth
e6b7c31a72 Gambling Tracker will only track successful removes 2022-10-06 10:55:12 +02:00
Kwoth
2f77fd57b0 Improved .betstats formatting 2022-10-04 16:27:19 +02:00
Kwoth
fda3d92134 Added gambling stats migrations for postgres and mysql 2022-10-03 19:58:08 +02:00
Kwoth
fe6f28143b Updated changelog 2022-10-03 13:52:17 +02:00
Kwoth
df3909fc55 Added .betstats command, shows statistics for multiple gambling commands, .slotstats removed as it is obsolete 2022-10-03 13:49:52 +02:00
117 changed files with 14754 additions and 486 deletions

View File

@@ -104,7 +104,7 @@ publish-medusa-package:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
script:
- LAST_TAG=$(git describe --tags --abbrev=0)
- if [ $CI_COMMIT_TAG ];then MEDUSA_VERSION="$CI_COMMIT_TAG"; else MEDUSA_VERSION="$LAST_TAG-$CI_COMMIT_SHA"; fi
- if [ $CI_COMMIT_TAG ];then MEDUSA_VERSION="$CI_COMMIT_TAG"; else MEDUSA_VERSION="$LAST_TAG-$CI_COMMIT_SHORT_SHA"; fi
- cd src/Nadeko.Medusa/
- dotnet pack -c Release /p:Version=$MEDUSA_VERSION -o bin/Release/packed
- dotnet nuget push bin/Release/packed/ --source https://www.myget.org/F/nadeko/api/v2/package --api-key "$MYGET_API_KEY"

View File

@@ -2,7 +2,56 @@
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## Unreleased
## [4.3.10] - 10.11.2022
### Added
- `.filterlist` / `.fl` command which lists link and invite filtering channels and status
- Added support for `%target%` placeholder in `.alias` command
- Added .forwardtochannel which will forward messages to the current channel. It has lower priority than fwtoall
- Added .exprtoggleglobal / .extg which can be used to toggle usage of global expressions on the server
### Changed
- .meload and .meunload are now case sensitive. Previously loaded medusae may need to be reloaded or data/medusae/medusa.yml may need to be edited manually
- Several club related command have their error messages improved
- Updated help text for .antispam and .antiraid
- You can now specify time and date (time is optional) in `.remind` command instead of relative time, in the format `HH:mm dd.MM.YYYY`
- OwnerId will be automatically added to `creds.yml` at bot startup if it's missing
### Fixed
- Fixed `.cmdcd` console error
- Fixed an error when currency is add per xp
- Fixed an issue preventing execution of expressions starting with @Bot when cleverbot is enabled on the server
- Fixed `.feedadd`
- Fixed `.prune @target` not working
- Medusa modules (sneks) should now inherit medusa description when listed in .mdls command
- Fixed command cooldown calculation
## [4.3.9] - 12.10.2022
### Added
- `.betstats` shows sum of all bets, payouts and the payout rate in %. Updates once an hour
### Changed
- `.betstats` looks way better (except on Mac)
- `.feedadd` errors clarified and separated in individual error messages for each issue.
- `.clubban` and `.clubunban` errors clarified and separated in individual error messages for each issue.
- `.clubapply` better error messages
### Fixed
- `.timely` 'Remind' button fixed in DMs
- `.cmdcd` database bugs fixed
- Fixed bugged mysql and postgresql migrations
- Fixed issues with lodaing medusae due to strict versioning
### Removed
- `.slotstats` Superseded by `.betstats`
## [4.3.8] - 02.10.2022

View File

@@ -7,7 +7,7 @@ Open Terminal (if you don't know how to, click on the magnifying glass on the to
###### Homebrew/wget
*Skip this step if you already have homebrew installed*
- Copy and paste this command, then press Enter:
- `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"`
- `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`
- Install wget
- `brew install wget`

View File

@@ -155,7 +155,7 @@ This section will guide you through how to create a simple custom medusa. You ca
<ItemGroup>
<!-- Base medusa package. You MUST reference this in order to have a working medusa -->
<!-- Also, this package comes from MyGet, which requires you to have a NuGet.Config file next to your .csproj -->
<PackageReference Include="Nadeko.Medusa" Version="1.0.1">
<PackageReference Include="Nadeko.Medusa" Version="4.3.9">
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View File

@@ -17,4 +17,7 @@
<PackageReference Include="YamlDotNet" Version="11.2.1" />
</ItemGroup>
<PropertyGroup Condition=" '$(Version)' == '' ">
<Version>5.0.0</Version>
</PropertyGroup>
</Project>

View File

@@ -313,10 +313,29 @@ public sealed class Bot
await _commandService.AddModulesAsync(typeof(Bot).Assembly, Services);
// await _interactionService.AddModulesAsync(typeof(Bot).Assembly, Services);
IsReady = true;
await EnsureBotOwnershipAsync();
_ = Task.Run(ExecuteReadySubscriptions);
Log.Information("Shard {ShardId} ready", Client.ShardId);
}
private async ValueTask EnsureBotOwnershipAsync()
{
try
{
if (_creds.OwnerIds.Count != 0)
return;
Log.Information("Initializing Owner Id...");
var info = await Client.GetApplicationInfoAsync();
_credsProvider.ModifyCredsFile(x => x.OwnerIds = new[] { info.Owner.Id });
}
catch (Exception ex)
{
Log.Warning("Getting application info failed: {ErrorMessage}", ex.Message);
}
}
private Task ExecuteReadySubscriptions()
{
var readyExecutors = Services.GetServices<IReadyExecutor>();

View File

@@ -12,7 +12,7 @@ namespace NadekoBot.Common.Configs;
public sealed partial class BotConfig : ICloneable<BotConfig>
{
[Comment(@"DO NOT CHANGE")]
public int Version { get; set; } = 4;
public int Version { get; set; } = 5;
[Comment(@"Most commands, when executed, have a small colored line
next to the response. The color depends whether the command
@@ -40,6 +40,10 @@ Allowed values: Simple, Normal, None")]
or all owners? (this might cause the bot to lag if there's a lot of owners specified)")]
public bool ForwardToAllOwners { get; set; }
[Comment(@"Any messages sent by users in Bot's DM to be forwarded to the specified channel.
This option will only work when ForwardToAllOwners is set to false")]
public ulong? ForwardToChannel { get; set; }
[Comment(@"When a user DMs the bot with a message which is not a command
they will receive this message. Leave empty for no response. The string which will be sent whenever someone DMs the bot.
Supports embeds. How it looks: https://puu.sh/B0BLV.png")]

View File

@@ -186,7 +186,6 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor,
return MedusaLoadResult.AlreadyLoaded;
var safeName = Uri.EscapeDataString(name);
name = name.ToLowerInvariant();
await _lock.WaitAsync();
try
@@ -525,7 +524,6 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor,
[MethodImpl(MethodImplOptions.NoInlining)]
private async Task<MedusaUnloadResult> InternalUnloadAsync(string name)
{
name = name.ToLowerInvariant();
if (!_resolved.Remove(name, out var lsi))
return MedusaUnloadResult.NotLoaded;

View File

@@ -1,15 +1,14 @@
#nullable disable
using CommandLine;
using CommandLine;
namespace NadekoBot.Common;
public static class OptionsParser
{
public static T ParseFrom<T>(string[] args)
public static T ParseFrom<T>(string[]? args)
where T : INadekoCommandOptions, new()
=> ParseFrom(new T(), args).Item1;
public static (T, bool) ParseFrom<T>(T options, string[] args)
public static (T, bool) ParseFrom<T>(T options, string[]? args)
where T : INadekoCommandOptions
{
using var p = new Parser(x =>

View File

@@ -0,0 +1,9 @@
#nullable disable
namespace NadekoBot.Services.Database.Models;
public class GamblingStats : DbEntity
{
public string Feature { get; set; }
public decimal Bet { get; set; }
public decimal PaidOut { get; set; }
}

View File

@@ -95,6 +95,8 @@ public class GuildConfig : DbEntity
public int WarnExpireHours { get; set; }
public WarnExpireAction WarnExpireAction { get; set; } = WarnExpireAction.Clear;
public bool DisableGlobalExpressions { get; set; } = false;
#region Boost Message
public bool SendBoostMessage { get; set; }

View File

@@ -472,6 +472,14 @@ public abstract class NadekoContext : DbContext
.IsUnique());
#endregion
#region GamblingStats
modelBuilder.Entity<GamblingStats>(gs => gs
.HasIndex(x => x.Feature)
.IsUnique());
#endregion
}
#if DEBUG

View File

@@ -2,6 +2,7 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NadekoBot.Services.Database;
@@ -10,15 +11,45 @@ using NadekoBot.Services.Database;
namespace NadekoBot.Migrations.Mysql
{
[DbContext(typeof(MysqlContext))]
partial class MysqlContextModelSnapshot : ModelSnapshot
[Migration("20221003175743_gambling-stats")]
partial class gamblingstats
{
protected override void BuildModel(ModelBuilder modelBuilder)
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("NadekoBot.Db.Models.AutoPublishChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<ulong>("ChannelId")
.HasColumnType("bigint unsigned")
.HasColumnName("channelid");
b.Property<DateTime?>("DateAdded")
.HasColumnType("datetime(6)")
.HasColumnName("dateadded");
b.Property<ulong>("GuildId")
.HasColumnType("bigint unsigned")
.HasColumnName("guildid");
b.HasKey("Id")
.HasName("pk_autopublishchannel");
b.HasIndex("GuildId")
.IsUnique()
.HasDatabaseName("ix_autopublishchannel_guildid");
b.ToTable("autopublishchannel", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
{
b.Property<int>("Id")
@@ -1072,6 +1103,39 @@ namespace NadekoBot.Migrations.Mysql
b.ToTable("filterwordschannelid", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.GamblingStats", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<decimal>("Bet")
.HasColumnType("decimal(65,30)")
.HasColumnName("bet");
b.Property<DateTime?>("DateAdded")
.HasColumnType("datetime(6)")
.HasColumnName("dateadded");
b.Property<string>("Feature")
.HasColumnType("varchar(255)")
.HasColumnName("feature");
b.Property<decimal>("PaidOut")
.HasColumnType("decimal(65,30)")
.HasColumnName("paidout");
b.HasKey("Id")
.HasName("pk_gamblingstats");
b.HasIndex("Feature")
.IsUnique()
.HasDatabaseName("ix_gamblingstats_feature");
b.ToTable("gamblingstats", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b =>
{
b.Property<int>("Id")
@@ -2236,6 +2300,10 @@ namespace NadekoBot.Migrations.Mysql
.HasColumnType("longtext")
.HasColumnName("rolename");
b.Property<ulong?>("RoleRequirement")
.HasColumnType("bigint unsigned")
.HasColumnName("rolerequirement");
b.Property<int>("Type")
.HasColumnType("int")
.HasColumnName("type");

View File

@@ -0,0 +1,44 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.Mysql
{
public partial class gamblingstats : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "gamblingstats",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
feature = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
bet = table.Column<decimal>(type: "decimal(65,30)", nullable: false),
paidout = table.Column<decimal>(type: "decimal(65,30)", nullable: false),
dateadded = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_gamblingstats", x => x.id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "ix_gamblingstats_feature",
table: "gamblingstats",
column: "feature",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "gamblingstats");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.Mysql
{
public partial class toggleglobalexpressions : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "disableglobalexpressions",
table: "guildconfigs",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "disableglobalexpressions",
table: "guildconfigs");
}
}
}

View File

@@ -1101,6 +1101,39 @@ namespace NadekoBot.Migrations.Mysql
b.ToTable("filterwordschannelid", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.GamblingStats", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<decimal>("Bet")
.HasColumnType("decimal(65,30)")
.HasColumnName("bet");
b.Property<DateTime?>("DateAdded")
.HasColumnType("datetime(6)")
.HasColumnName("dateadded");
b.Property<string>("Feature")
.HasColumnType("varchar(255)")
.HasColumnName("feature");
b.Property<decimal>("PaidOut")
.HasColumnType("decimal(65,30)")
.HasColumnName("paidout");
b.HasKey("Id")
.HasName("pk_gamblingstats");
b.HasIndex("Feature")
.IsUnique()
.HasDatabaseName("ix_gamblingstats_feature");
b.ToTable("gamblingstats", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b =>
{
b.Property<int>("Id")
@@ -1225,6 +1258,10 @@ namespace NadekoBot.Migrations.Mysql
.HasColumnType("tinyint(1)")
.HasColumnName("deletestreamonlinemessage");
b.Property<bool>("DisableGlobalExpressions")
.HasColumnType("tinyint(1)")
.HasColumnName("disableglobalexpressions");
b.Property<string>("DmGreetMessageText")
.HasColumnType("longtext")
.HasColumnName("dmgreetmessagetext");

View File

@@ -2,6 +2,7 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NadekoBot.Services.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
@@ -11,9 +12,10 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace NadekoBot.Migrations.PostgreSql
{
[DbContext(typeof(PostgreSqlContext))]
partial class PostgreSqlContextModelSnapshot : ModelSnapshot
[Migration("20221003175752_gambling-stats")]
partial class gamblingstats
{
protected override void BuildModel(ModelBuilder modelBuilder)
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
@@ -22,6 +24,37 @@ namespace NadekoBot.Migrations.PostgreSql
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("NadekoBot.Db.Models.AutoPublishChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("ChannelId")
.HasColumnType("numeric(20,0)")
.HasColumnName("channelid");
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.HasKey("Id")
.HasName("pk_autopublishchannel");
b.HasIndex("GuildId")
.IsUnique()
.HasDatabaseName("ix_autopublishchannel_guildid");
b.ToTable("autopublishchannel", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
{
b.Property<int>("Id")
@@ -1126,6 +1159,41 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("filterwordschannelid", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.GamblingStats", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("Bet")
.HasColumnType("numeric")
.HasColumnName("bet");
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<string>("Feature")
.HasColumnType("text")
.HasColumnName("feature");
b.Property<decimal>("PaidOut")
.HasColumnType("numeric")
.HasColumnName("paidout");
b.HasKey("Id")
.HasName("pk_gamblingstats");
b.HasIndex("Feature")
.IsUnique()
.HasDatabaseName("ix_gamblingstats_feature");
b.ToTable("gamblingstats", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b =>
{
b.Property<int>("Id")
@@ -2342,6 +2410,10 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("text")
.HasColumnName("rolename");
b.Property<decimal?>("RoleRequirement")
.HasColumnType("numeric(20,0)")
.HasColumnName("rolerequirement");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");

View File

@@ -0,0 +1,42 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
public partial class gamblingstats : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "gamblingstats",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
feature = table.Column<string>(type: "text", nullable: true),
bet = table.Column<decimal>(type: "numeric", nullable: false),
paidout = table.Column<decimal>(type: "numeric", nullable: false),
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_gamblingstats", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_gamblingstats_feature",
table: "gamblingstats",
column: "feature",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "gamblingstats");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
public partial class toggleglobalexpressions : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "disableglobalexpressions",
table: "guildconfigs",
type: "boolean",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "disableglobalexpressions",
table: "guildconfigs");
}
}
}

View File

@@ -1157,6 +1157,41 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("filterwordschannelid", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.GamblingStats", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("Bet")
.HasColumnType("numeric")
.HasColumnName("bet");
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<string>("Feature")
.HasColumnType("text")
.HasColumnName("feature");
b.Property<decimal>("PaidOut")
.HasColumnType("numeric")
.HasColumnName("paidout");
b.HasKey("Id")
.HasName("pk_gamblingstats");
b.HasIndex("Feature")
.IsUnique()
.HasDatabaseName("ix_gamblingstats_feature");
b.ToTable("gamblingstats", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b =>
{
b.Property<int>("Id")
@@ -1287,6 +1322,10 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("boolean")
.HasColumnName("deletestreamonlinemessage");
b.Property<bool>("DisableGlobalExpressions")
.HasColumnType("boolean")
.HasColumnName("disableglobalexpressions");
b.Property<string>("DmGreetMessageText")
.HasColumnType("text")
.HasColumnName("dmgreetmessagetext");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
public partial class gamblingstats : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "GamblingStats",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Feature = table.Column<string>(type: "TEXT", nullable: true),
Bet = table.Column<decimal>(type: "TEXT", nullable: false),
PaidOut = table.Column<decimal>(type: "TEXT", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_GamblingStats", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_GamblingStats_Feature",
table: "GamblingStats",
column: "Feature",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "GamblingStats");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
public partial class toggleglobalexpressions : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "DisableGlobalExpressions",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DisableGlobalExpressions",
table: "GuildConfigs");
}
}
}

View File

@@ -864,6 +864,32 @@ namespace NadekoBot.Migrations
b.ToTable("FilterWordsChannelId");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.GamblingStats", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal>("Bet")
.HasColumnType("TEXT");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<string>("Feature")
.HasColumnType("TEXT");
b.Property<decimal>("PaidOut")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Feature")
.IsUnique();
b.ToTable("GamblingStats");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b =>
{
b.Property<int>("Id")
@@ -960,6 +986,9 @@ namespace NadekoBot.Migrations
b.Property<bool>("DeleteStreamOnlineMessage")
.HasColumnType("INTEGER");
b.Property<bool>("DisableGlobalExpressions")
.HasColumnType("INTEGER");
b.Property<string>("DmGreetMessageText")
.HasColumnType("TEXT");

View File

@@ -72,7 +72,7 @@ public partial class Administration
[BotPerm(ChannelPerm.ManageMessages)]
[NadekoOptions(typeof(PruneOptions))]
[Priority(0)]
public Task Prune(IGuildUser user, int count = 100, string args = null)
public Task Prune(IGuildUser user, int count = 100, params string[] args)
=> Prune(user.Id, count, args);
//prune userid [x]

View File

@@ -230,6 +230,19 @@ public partial class Administration
await ReplyPendingLocalizedAsync(strs.fwall_stop);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task ForwardToChannel()
{
var enabled = _service.ForwardToChannel(ctx.Channel.Id);
if (enabled)
await ReplyConfirmLocalizedAsync(strs.fwch_start);
else
await ReplyPendingLocalizedAsync(strs.fwch_stop);
}
[Cmd]
public async Task ShardStats(int page = 1)
{

View File

@@ -85,12 +85,12 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
await using var uow = _db.GetDbContext();
autoCommands = uow.AutoCommands.AsNoTracking()
.Where(x => x.Interval >= 5)
.AsEnumerable()
.GroupBy(x => x.GuildId)
.ToDictionary(x => x.Key,
y => y.ToDictionary(x => x.Id, TimerFromAutoCommand).ToConcurrent())
.ToConcurrent();
.Where(x => x.Interval >= 5)
.AsEnumerable()
.GroupBy(x => x.GuildId)
.ToDictionary(x => x.Key,
y => y.ToDictionary(x => x.Id, TimerFromAutoCommand).ToConcurrent())
.ToConcurrent();
var startupCommands = uow.AutoCommands.AsNoTracking().Where(x => x.Interval == 0);
foreach (var cmd in startupCommands)
@@ -169,18 +169,18 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
private async Task LoadOwnerChannels()
{
var channels = await _creds.OwnerIds.Select(id =>
{
var user = _client.GetUser(id);
if (user is null)
return Task.FromResult<IDMChannel>(null);
{
var user = _client.GetUser(id);
if (user is null)
return Task.FromResult<IDMChannel>(null);
return user.CreateDMChannelAsync();
})
.WhenAll();
return user.CreateDMChannelAsync();
})
.WhenAll();
ownerChannels = channels.Where(x => x is not null)
.ToDictionary(x => x.Recipient.Id, x => x)
.ToImmutableDictionary();
.ToDictionary(x => x.Recipient.Id, x => x)
.ToImmutableDictionary();
if (!ownerChannels.Any())
{
@@ -202,7 +202,7 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
{
var bs = _bss.Data;
if (msg.Channel is IDMChannel && bs.ForwardMessages && ownerChannels.Any())
if (msg.Channel is IDMChannel && bs.ForwardMessages && (ownerChannels.Any() || bs.ForwardToChannel is not null))
{
var title = _strings.GetText(strs.dm_from) + $" [{msg.Author}]({msg.Author.Id})";
@@ -232,6 +232,18 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
}
}
}
else if (bs.ForwardToChannel is ulong cid)
{
try
{
if (_client.GetChannel(cid) is ITextChannel ch)
await ch.SendConfirmAsync(_eb, title, toSend);
}
catch
{
Log.Warning("Error forwarding message to the channel");
}
}
else
{
var firstOwnerChannel = ownerChannels.Values.First();
@@ -333,6 +345,20 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
return isToAll;
}
public bool ForwardToChannel(ulong? channelId)
{
using var uow = _db.GetDbContext();
_bss.ModifyConfig(config =>
{
config.ForwardToChannel = channelId == config.ForwardToChannel
? null
: channelId;
});
return channelId is not null;
}
private void HandleStatusChanges()
=> _pubSub.Sub(_activitySetKey,
async data =>

View File

@@ -41,6 +41,17 @@ public partial class NadekoExpressions : NadekoModule<NadekoExpressionsService>
message.Length > 1024 ? GetText(strs.redacted_too_long) : message));
}
[Cmd]
[UserPerm(GuildPerm.Administrator)]
public async Task ExprToggleGlobal()
{
var result = await _service.ToggleGlobalExpressionsAsync(ctx.Guild.Id);
if (result)
await ReplyConfirmLocalizedAsync(strs.expr_global_disabled);
else
await ReplyConfirmLocalizedAsync(strs.expr_global_enabled);
}
[Cmd]
[UserPerm(GuildPerm.Administrator)]
public async Task ExprAddServer(string key, [Leftover] string message)

View File

@@ -7,6 +7,7 @@ using NadekoBot.Modules.Permissions.Common;
using NadekoBot.Modules.Permissions.Services;
using NadekoBot.Services.Database.Models;
using System.Runtime.CompilerServices;
using LinqToDB.EntityFrameworkCore;
using Nadeko.Common;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
@@ -56,8 +57,8 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
// 1. expressions are almost never added (compared to how many times they are being looped through)
// 2. only need write locks for this as we'll rebuild+replace the array on every edit
// 3. there's never many of them (at most a thousand, usually < 100)
private NadekoExpression[] globalReactions;
private ConcurrentDictionary<ulong, NadekoExpression[]> newGuildReactions;
private NadekoExpression[] globalExpressions;
private ConcurrentDictionary<ulong, NadekoExpression[]> newguildExpressions;
private readonly DbService _db;
private readonly DiscordSocketClient _client;
@@ -72,6 +73,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
private readonly Random _rng;
private bool ready;
private ConcurrentHashSet<ulong> _disabledGlobalExpressionGuilds;
public NadekoExpressionsService(
PermissionService perms,
@@ -113,7 +115,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
.Where(x => allGuildIds.Contains(x.GuildId.Value))
.ToListAsync();
newGuildReactions = guildItems.GroupBy(k => k.GuildId!.Value)
newguildExpressions = guildItems.GroupBy(k => k.GuildId!.Value)
.ToDictionary(g => g.Key,
g => g.Select(x =>
{
@@ -123,6 +125,11 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
.ToArray())
.ToConcurrent();
_disabledGlobalExpressionGuilds = new (await uow.GuildConfigs
.Where(x => x.DisableGlobalExpressions)
.Select(x => x.GuildId)
.ToListAsyncLinqToDB());
lock (_gexprWriteLock)
{
var globalItems = uow.Expressions.AsNoTracking()
@@ -135,7 +142,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
})
.ToArray();
globalReactions = globalItems;
globalExpressions = globalItems;
}
ready = true;
@@ -151,14 +158,17 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
var content = umsg.Content.Trim().ToLowerInvariant();
if (newGuildReactions.TryGetValue(channel.Guild.Id, out var reactions) && reactions.Length > 0)
if (newguildExpressions.TryGetValue(channel.Guild.Id, out var expressions) && expressions.Length > 0)
{
var expr = MatchExpressions(content, reactions);
var expr = MatchExpressions(content, expressions);
if (expr is not null)
return expr;
}
var localGrs = globalReactions;
if (_disabledGlobalExpressionGuilds.Contains(channel.Guild.Id))
return null;
var localGrs = globalExpressions;
return MatchExpressions(content, localGrs);
}
@@ -345,7 +355,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
{
if (maybeGuildId is { } guildId)
{
newGuildReactions.AddOrUpdate(guildId,
newguildExpressions.AddOrUpdate(guildId,
new[] { expr },
(_, old) =>
{
@@ -363,7 +373,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
{
lock (_gexprWriteLock)
{
var exprs = globalReactions;
var exprs = globalExpressions;
for (var i = 0; i < exprs.Length; i++)
{
if (exprs[i].Id == expr.Id)
@@ -379,7 +389,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
expr.Trigger = expr.Trigger.Replace(MENTION_PH, _client.CurrentUser.Mention);
if (maybeGuildId is { } guildId)
newGuildReactions.AddOrUpdate(guildId, new[] { expr }, (_, old) => old.With(expr));
newguildExpressions.AddOrUpdate(guildId, new[] { expr }, (_, old) => old.With(expr));
else
return _pubSub.Pub(_gexprAddedKey, expr);
@@ -390,7 +400,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
{
if (maybeGuildId is { } guildId)
{
newGuildReactions.AddOrUpdate(guildId,
newguildExpressions.AddOrUpdate(guildId,
Array.Empty<NadekoExpression>(),
(key, old) => DeleteInternal(old, id, out _));
@@ -399,7 +409,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
lock (_gexprWriteLock)
{
var expr = Array.Find(globalReactions, item => item.Id == id);
var expr = Array.Find(globalExpressions, item => item.Id == id);
if (expr is not null)
return _pubSub.Pub(_gexprDeletedkey, expr.Id);
}
@@ -492,7 +502,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
var count = uow.Expressions.ClearFromGuild(guildId);
uow.SaveChanges();
newGuildReactions.TryRemove(guildId, out _);
newguildExpressions.TryRemove(guildId, out _);
return count;
}
@@ -562,10 +572,10 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
{
lock (_gexprWriteLock)
{
var newGlobalReactions = new NadekoExpression[globalReactions.Length + 1];
Array.Copy(globalReactions, newGlobalReactions, globalReactions.Length);
newGlobalReactions[globalReactions.Length] = c;
globalReactions = newGlobalReactions;
var newGlobalReactions = new NadekoExpression[globalExpressions.Length + 1];
Array.Copy(globalExpressions, newGlobalReactions, globalExpressions.Length);
newGlobalReactions[globalExpressions.Length] = c;
globalExpressions = newGlobalReactions;
}
return default;
@@ -575,11 +585,11 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
{
lock (_gexprWriteLock)
{
for (var i = 0; i < globalReactions.Length; i++)
for (var i = 0; i < globalExpressions.Length; i++)
{
if (globalReactions[i].Id == c.Id)
if (globalExpressions[i].Id == c.Id)
{
globalReactions[i] = c;
globalExpressions[i] = c;
return default;
}
}
@@ -596,8 +606,8 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
{
lock (_gexprWriteLock)
{
var newGlobalReactions = DeleteInternal(globalReactions, id, out _);
globalReactions = newGlobalReactions;
var newGlobalReactions = DeleteInternal(globalExpressions, id, out _);
globalExpressions = newGlobalReactions;
}
return default;
@@ -612,7 +622,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
private Task OnLeftGuild(SocketGuild arg)
{
newGuildReactions.TryRemove(arg.Id, out _);
newguildExpressions.TryRemove(arg.Id, out _);
return Task.CompletedTask;
}
@@ -622,7 +632,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
await using var uow = _db.GetDbContext();
var exprs = await uow.Expressions.AsNoTracking().Where(x => x.GuildId == gc.GuildId).ToArrayAsync();
newGuildReactions[gc.GuildId] = exprs;
newguildExpressions[gc.GuildId] = exprs;
}
#endregion
@@ -702,10 +712,25 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
public NadekoExpression[] GetExpressionsFor(ulong? maybeGuildId)
{
if (maybeGuildId is { } guildId)
return newGuildReactions.TryGetValue(guildId, out var exprs) ? exprs : Array.Empty<NadekoExpression>();
return newguildExpressions.TryGetValue(guildId, out var exprs) ? exprs : Array.Empty<NadekoExpression>();
return globalReactions;
return globalExpressions;
}
#endregion
public async Task<bool> ToggleGlobalExpressionsAsync(ulong guildId)
{
await using var ctx = _db.GetDbContext();
var gc = ctx.GuildConfigsForId(guildId, set => set);
var toReturn = gc.DisableGlobalExpressions = !gc.DisableGlobalExpressions;
await ctx.SaveChangesAsync();
if (toReturn)
_disabledGlobalExpressionGuilds.Add(guildId);
else
_disabledGlobalExpressionGuilds.TryRemove(guildId);
return toReturn;
}
}

View File

@@ -29,6 +29,7 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly IBankService _bank;
private readonly IPatronageService _ps;
private readonly RemindService _remind;
private readonly GamblingTxTracker _gamblingTxTracker;
private IUserMessage rdMsg;
@@ -41,7 +42,8 @@ public partial class Gambling : GamblingModule<GamblingService>
GamblingConfigService configService,
IBankService bank,
IPatronageService ps,
RemindService remind)
RemindService remind,
GamblingTxTracker gamblingTxTracker)
: base(configService)
{
_gs = gs;
@@ -51,6 +53,7 @@ public partial class Gambling : GamblingModule<GamblingService>
_bank = bank;
_ps = ps;
_remind = remind;
_gamblingTxTracker = gamblingTxTracker;
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
_enUsCulture.NumberDecimalDigits = 0;
@@ -65,6 +68,43 @@ public partial class Gambling : GamblingModule<GamblingService>
return N(bal);
}
[Cmd]
public async Task BetStats()
{
var stats = await _gamblingTxTracker.GetAllAsync();
var eb = _eb.Create(ctx)
.WithOkColor();
var str = "` Feature `` Bet ``Paid Out`` RoI `\n";
str += "――――――――――――――――――――\n";
foreach (var stat in stats)
{
var perc = (stat.PaidOut / stat.Bet).ToString("P2", Culture);
str += $"`{stat.Feature.PadBoth(9)}`" +
$"`{stat.Bet.ToString("N0").PadLeft(8, '')}`" +
$"`{stat.PaidOut.ToString("N0").PadLeft(8, '')}`" +
$"`{perc.PadLeft(6, '')}`\n";
}
var bet = stats.Sum(x => x.Bet);
var paidOut = stats.Sum(x => x.PaidOut);
if (bet == 0)
bet = 1;
var tPerc = (paidOut / bet).ToString("P2", Culture);
str += "――――――――――――――――――――\n";
str += $"` {("TOTAL").PadBoth(7)}` " +
$"**{N(bet).PadLeft(8, '')}**" +
$"**{N(paidOut).PadLeft(8, '')}**" +
$"`{tPerc.PadLeft(6, '')}`";
eb.WithDescription(str);
await ctx.Channel.EmbedAsync(eb);
}
[Cmd]
public async Task Economy()
{
@@ -79,15 +119,15 @@ public partial class Gambling : GamblingModule<GamblingService>
// [21:03] Bob Page: Kinda remids me of US economy
var embed = _eb.Create()
.WithTitle(GetText(strs.economy_state))
.AddField(GetText(strs.currency_owned), N(ec.Cash - ec.Bot))
.AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%")
.AddField(GetText(strs.currency_planted), N(ec.Planted))
.AddField(GetText(strs.owned_waifus_total), N(ec.Waifus))
.AddField(GetText(strs.bot_currency), N(ec.Bot))
.AddField(GetText(strs.bank_accounts), N(ec.Bank))
.AddField(GetText(strs.total), N(ec.Cash + ec.Planted + ec.Waifus + ec.Bank))
.WithOkColor();
.WithTitle(GetText(strs.economy_state))
.AddField(GetText(strs.currency_owned), N(ec.Cash - ec.Bot))
.AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%")
.AddField(GetText(strs.currency_planted), N(ec.Planted))
.AddField(GetText(strs.owned_waifus_total), N(ec.Waifus))
.AddField(GetText(strs.bot_currency), N(ec.Bot))
.AddField(GetText(strs.bank_accounts), N(ec.Bank))
.AddField(GetText(strs.total), N(ec.Cash + ec.Planted + ec.Waifus + ec.Bank))
.WithOkColor();
// ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table
await ctx.Channel.EmbedAsync(embed);
@@ -105,7 +145,7 @@ public partial class Gambling : GamblingModule<GamblingService>
await _remind.AddReminderAsync(ctx.User.Id,
ctx.User.Id,
ctx.Guild.Id,
ctx.Guild?.Id,
true,
when,
GetText(strs.timely_time));
@@ -253,9 +293,9 @@ public partial class Gambling : GamblingModule<GamblingService>
}
var embed = _eb.Create()
.WithTitle(GetText(strs.transactions(((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
?? $"{userId}")))
.WithOkColor();
.WithTitle(GetText(strs.transactions(((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
?? $"{userId}")))
.WithOkColor();
var sb = new StringBuilder();
foreach (var tr in trs)
@@ -292,8 +332,8 @@ public partial class Gambling : GamblingModule<GamblingService>
await using var uow = _db.GetDbContext();
var tr = await uow.CurrencyTransactions.ToLinqToDBTable()
.Where(x => x.Id == intId && x.UserId == ctx.User.Id)
.FirstOrDefaultAsync();
.Where(x => x.Id == intId && x.UserId == ctx.User.Id)
.FirstOrDefaultAsync();
if (tr is null)
{
@@ -354,9 +394,9 @@ public partial class Gambling : GamblingModule<GamblingService>
var balance = await _bank.GetBalanceAsync(ctx.User.Id);
await N(balance)
.Pipe(strs.bank_balance)
.Pipe(GetText)
.Pipe(text => smc.RespondConfirmAsync(_eb, text, ephemeral: true));
.Pipe(strs.bank_balance)
.Pipe(GetText)
.Pipe(text => smc.RespondConfirmAsync(_eb, text, ephemeral: true));
}
private NadekoInteraction CreateCashInteraction()
@@ -799,8 +839,8 @@ public partial class Gambling : GamblingModule<GamblingService>
}
else if (result.Result == RpsResultType.Win)
{
if((long)result.Won > 0)
embed.AddField(GetText(strs.won), N(amount.Value));
if ((long)result.Won > 0)
embed.AddField(GetText(strs.won), N(amount.Value));
msg = GetText(strs.rps_win(ctx.User.Mention,
GetRpsPick(pick),
@@ -863,99 +903,98 @@ public partial class Gambling : GamblingModule<GamblingService>
}
public enum GambleTestTarget
public enum GambleTestTarget
{
Slot,
Betroll,
Betflip,
BetflipT,
BetDraw,
BetDrawHL,
BetDrawRB,
Lula,
Rps,
}
[Cmd]
[OwnerOnly]
public async Task BetTest()
{
await SendConfirmAsync(GetText(strs.available_tests),
Enum.GetValues<GambleTestTarget>()
.Select(x => $"`{x}`")
.Join(", "));
}
[Cmd]
[OwnerOnly]
public async Task BetTest(GambleTestTarget target, int tests = 1000)
{
if (tests <= 0)
return;
await ctx.Channel.TriggerTypingAsync();
var streak = 0;
var maxW = 0;
var maxL = 0;
var dict = new Dictionary<decimal, int>();
for (var i = 0; i < tests; i++)
{
Slot,
Betroll,
Betflip,
BetflipT,
BetDraw,
BetDrawHL,
BetDrawRB,
Lula,
Rps,
}
[Cmd]
[OwnerOnly]
public async Task BetTest()
{
await SendConfirmAsync(GetText(strs.available_tests),
Enum.GetValues<GambleTestTarget>()
.Select(x => $"`{x}`")
.Join(", "));
}
[Cmd]
[OwnerOnly]
public async Task BetTest(GambleTestTarget target, int tests = 1000)
{
if (tests <= 0)
return;
await ctx.Channel.TriggerTypingAsync();
var streak = 0;
var maxW = 0;
var maxL = 0;
var dict = new Dictionary<decimal, int>();
for (var i = 0; i < tests; i++)
var multi = target switch
{
var multi = target switch
{
GambleTestTarget.BetDraw => (await _gs.BetDrawAsync(ctx.User.Id, 0, 1, 0)).AsT0.Multiplier,
GambleTestTarget.BetDrawRB => (await _gs.BetDrawAsync(ctx.User.Id, 0, null, 1)).AsT0.Multiplier,
GambleTestTarget.BetDrawHL => (await _gs.BetDrawAsync(ctx.User.Id, 0, 0, null)).AsT0.Multiplier,
GambleTestTarget.Slot => (await _gs.SlotAsync(ctx.User.Id, 0)).AsT0.Multiplier,
GambleTestTarget.Betflip => (await _gs.BetFlipAsync(ctx.User.Id, 0, 0)).AsT0.Multiplier,
GambleTestTarget.BetflipT => (await _gs.BetFlipAsync(ctx.User.Id, 0, 1)).AsT0.Multiplier,
GambleTestTarget.Lula => (await _gs.LulaAsync(ctx.User.Id, 0)).AsT0.Multiplier,
GambleTestTarget.Rps => (await _gs.RpsAsync(ctx.User.Id, 0, (byte)(i % 3))).AsT0.Multiplier,
GambleTestTarget.Betroll => (await _gs.BetRollAsync(ctx.User.Id, 0)).AsT0.Multiplier,
_ => throw new ArgumentOutOfRangeException(nameof(target))
};
GambleTestTarget.BetDraw => (await _gs.BetDrawAsync(ctx.User.Id, 0, 1, 0)).AsT0.Multiplier,
GambleTestTarget.BetDrawRB => (await _gs.BetDrawAsync(ctx.User.Id, 0, null, 1)).AsT0.Multiplier,
GambleTestTarget.BetDrawHL => (await _gs.BetDrawAsync(ctx.User.Id, 0, 0, null)).AsT0.Multiplier,
GambleTestTarget.Slot => (await _gs.SlotAsync(ctx.User.Id, 0)).AsT0.Multiplier,
GambleTestTarget.Betflip => (await _gs.BetFlipAsync(ctx.User.Id, 0, 0)).AsT0.Multiplier,
GambleTestTarget.BetflipT => (await _gs.BetFlipAsync(ctx.User.Id, 0, 1)).AsT0.Multiplier,
GambleTestTarget.Lula => (await _gs.LulaAsync(ctx.User.Id, 0)).AsT0.Multiplier,
GambleTestTarget.Rps => (await _gs.RpsAsync(ctx.User.Id, 0, (byte)(i % 3))).AsT0.Multiplier,
GambleTestTarget.Betroll => (await _gs.BetRollAsync(ctx.User.Id, 0)).AsT0.Multiplier,
_ => throw new ArgumentOutOfRangeException(nameof(target))
};
if (dict.ContainsKey(multi))
dict[multi] += 1;
if (dict.ContainsKey(multi))
dict[multi] += 1;
else
dict.Add(multi, 1);
if (multi < 1)
{
if (streak <= 0)
--streak;
else
dict.Add(multi, 1);
streak = -1;
if (multi < 1)
{
if (streak <= 0)
--streak;
else
streak = -1;
maxL = Math.Max(maxL, -streak);
}
else if (multi > 1)
{
if (streak >= 0)
++streak;
else
streak = 1;
maxW = Math.Max(maxW, streak);
}
maxL = Math.Max(maxL, -streak);
}
var sb = new StringBuilder();
decimal payout = 0;
foreach (var key in dict.Keys.OrderByDescending(x => x))
else if (multi > 1)
{
sb.AppendLine($"x**{key}** occured `{dict[key]}` times. {dict[key] * 1.0f / tests * 100}%");
payout += key * dict[key];
if (streak >= 0)
++streak;
else
streak = 1;
maxW = Math.Max(maxW, streak);
}
sb.AppendLine();
sb.AppendLine($"Longest win streak: `{maxW}`");
sb.AppendLine($"Longest lose streak: `{maxL}`");
await SendConfirmAsync(GetText(strs.test_results_for(target)),
sb.ToString(),
footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%");
}
var sb = new StringBuilder();
decimal payout = 0;
foreach (var key in dict.Keys.OrderByDescending(x => x))
{
sb.AppendLine($"x**{key}** occured `{dict[key]}` times. {dict[key] * 1.0f / tests * 100}%");
payout += key * dict[key];
}
sb.AppendLine();
sb.AppendLine($"Longest win streak: `{maxW}`");
sb.AppendLine($"Longest lose streak: `{maxL}`");
await SendConfirmAsync(GetText(strs.test_results_for(target)),
sb.ToString(),
footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%");
}
}

View File

@@ -47,27 +47,6 @@ public partial class Gambling
public Task Test()
=> Task.CompletedTask;
[Cmd]
[OwnerOnly]
public async Task SlotStats()
{
//i remembered to not be a moron
var paid = totalPaidOut;
var bet = totalBet;
if (bet <= 0)
bet = 1;
var embed = _eb.Create()
.WithOkColor()
.WithTitle("Slot Stats")
.AddField("Total Bet", N(bet), true)
.AddField("Paid Out", N(paid), true)
.WithFooter($"Payout Rate: {paid * 1.0M / bet * 100:f4}%");
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
public async Task Slot(ShmartNumber amount)
{

View File

@@ -126,7 +126,7 @@ public class ChatterBotService : IExecOnMessage
Log.Information("{PermissionMessage}", returnMsg);
}
return true;
return false;
}
if (await _ccs.TryBlock(sg, usrMsg.Author, CleverBotResponseStr.CLEVERBOT_RESPONSE))

View File

@@ -92,7 +92,7 @@ public partial class Help : NadekoModule<HelpService>
localModules.OrderBy(module => module.Name)
.ToList()
.ForEach(module => embed.AddField($"{GetModuleEmoji(module.Name)} {module.Name}",
GetText(GetModuleLocStr(module.Name))
GetModuleDescription(module.Name)
+ "\n"
+ Format.Code(GetText(strs.module_footer(prefix, module.Name.ToLowerInvariant()))),
true));
@@ -104,6 +104,25 @@ public partial class Help : NadekoModule<HelpService>
false);
}
private string GetModuleDescription(string moduleName)
{
var key = GetModuleLocStr(moduleName);
if (key.Key == strs.module_description_missing.Key)
{
var desc = _medusae
.GetLoadedMedusae(Culture)
.FirstOrDefault(m => m.Sneks
.Any(x => x.Name.Equals(moduleName, StringComparison.InvariantCultureIgnoreCase)))
?.Description;
if (desc is not null)
return desc;
}
return GetText(key);
}
private LocStr GetModuleLocStr(string moduleName)
{
switch (moduleName.ToLowerInvariant())

View File

@@ -1,10 +1,12 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
namespace NadekoBot.Modules.Permissions.Services;
public sealed class CmdCdService : IExecPreCommand, IReadyExecutor, INService
{
private readonly DbService _db;
private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<string, int>> _settings = new();
private readonly ConcurrentDictionary<(ulong, string), ConcurrentDictionary<ulong, DateTime>> _activeCooldowns =
@@ -12,11 +14,13 @@ public sealed class CmdCdService : IExecPreCommand, IReadyExecutor, INService
public int Priority => 0;
public CmdCdService(Bot bot)
public CmdCdService(Bot bot, DbService db)
{
_db = db;
_settings = bot
.AllGuildConfigs
.ToDictionary(x => x.GuildId, x => x.CommandCooldowns
.DistinctBy(x => x.CommandName.ToLowerInvariant())
.ToDictionary(c => c.CommandName, c => c.Seconds)
.ToConcurrent())
.ToConcurrent();
@@ -25,13 +29,16 @@ public sealed class CmdCdService : IExecPreCommand, IReadyExecutor, INService
public Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command)
=> TryBlock(context.Guild, context.User, command.Name.ToLowerInvariant());
public async Task<bool> TryBlock(IGuild guild, IUser user, string commandName)
public Task<bool> TryBlock(IGuild? guild, IUser user, string commandName)
{
if (guild is null)
return Task.FromResult(false);
if (!_settings.TryGetValue(guild.Id, out var cooldownSettings))
return false;
return Task.FromResult(false);
if (!cooldownSettings.TryGetValue(commandName, out var cdSeconds))
return false;
return Task.FromResult(false);
var cooldowns = _activeCooldowns.GetOrAdd(
(guild.Id, commandName),
@@ -40,7 +47,7 @@ public sealed class CmdCdService : IExecPreCommand, IReadyExecutor, INService
// if user is not already on cooldown, add
if (cooldowns.TryAdd(user.Id, DateTime.UtcNow))
{
return false;
return Task.FromResult(false);
}
// if there is an entry, maybe it expired. Try to check if it expired and don't fail if it did
@@ -48,14 +55,14 @@ public sealed class CmdCdService : IExecPreCommand, IReadyExecutor, INService
if (cooldowns.TryGetValue(user.Id, out var oldValue))
{
var diff = DateTime.UtcNow - oldValue;
if (diff.Seconds > cdSeconds)
if (diff.TotalSeconds > cdSeconds)
{
if (cooldowns.TryUpdate(user.Id, DateTime.UtcNow, oldValue))
return false;
return Task.FromResult(false);
}
}
return true;
return Task.FromResult(true);
}
public async Task OnReadyAsync()
@@ -64,7 +71,6 @@ public sealed class CmdCdService : IExecPreCommand, IReadyExecutor, INService
while (await timer.WaitForNextTickAsync())
{
var now = DateTime.UtcNow;
// once per hour delete expired entries
foreach (var ((guildId, commandName), dict) in _activeCooldowns)
{
@@ -85,7 +91,7 @@ public sealed class CmdCdService : IExecPreCommand, IReadyExecutor, INService
private void Cleanup(ConcurrentDictionary<ulong, DateTime> dict, int cdSeconds)
{
var now = DateTime.UtcNow;
foreach (var (key, _) in dict.Where(x => (now - x.Value).Seconds > cdSeconds).ToArray())
foreach (var (key, _) in dict.Where(x => (now - x.Value).TotalSeconds > cdSeconds).ToArray())
{
dict.TryRemove(key, out _);
}
@@ -97,16 +103,34 @@ public sealed class CmdCdService : IExecPreCommand, IReadyExecutor, INService
dict.TryRemove(cmdName, out _);
_activeCooldowns.TryRemove((guildId, cmdName), out _);
using var ctx = _db.GetDbContext();
var gc = ctx.GuildConfigsForId(guildId, x => x.Include(x => x.CommandCooldowns));
gc.CommandCooldowns.RemoveWhere(x => x.CommandName == cmdName);
ctx.SaveChanges();
}
public void AddCooldown(ulong guildId, string name, int secs)
{
if (secs <= 0)
throw new ArgumentOutOfRangeException(nameof(secs));
var sett = _settings.GetOrAdd(guildId, static _ => new());
sett[name] = secs;
// force cleanup
if (_activeCooldowns.TryGetValue((guildId, name), out var dict))
Cleanup(dict, secs);
using var ctx = _db.GetDbContext();
var gc = ctx.GuildConfigsForId(guildId, x => x.Include(x => x.CommandCooldowns));
gc.CommandCooldowns.RemoveWhere(x => x.CommandName == name);
gc.CommandCooldowns.Add(new()
{
Seconds = secs,
CommandName = name
});
ctx.SaveChanges();
}
public IReadOnlyCollection<(string CommandName, int Seconds)> GetCommandCooldowns(ulong guildId)

View File

@@ -25,6 +25,47 @@ public partial class Permissions
await ReplyConfirmLocalizedAsync(strs.fw_cleared);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task FilterList()
{
var embed = _eb.Create(ctx)
.WithOkColor()
.WithTitle("Server filter settings");
var config = await _service.GetFilterSettings(ctx.Guild.Id);
string GetEnabledEmoji(bool value)
=> value ? "\\🟢" : "\\🔴";
async Task<string> GetChannelListAsync(IReadOnlyCollection<ulong> channels)
{
var toReturn = (await channels
.Select(async cid =>
{
var ch = await ctx.Guild.GetChannelAsync(cid);
return ch is null
? $"{cid} *missing*"
: $"<#{cid}>";
})
.WhenAll())
.Join('\n');
if (string.IsNullOrWhiteSpace(toReturn))
return GetText(strs.no_channel_found);
return toReturn;
}
embed.AddField($"{GetEnabledEmoji(config.FilterLinksEnabled)} Filter Links",
await GetChannelListAsync(config.FilterLinksChannels));
embed.AddField($"{GetEnabledEmoji(config.FilterInvitesEnabled)} Filter Invites",
await GetChannelListAsync(config.FilterInvitesChannels));
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task SrvrFilterInv()

View File

@@ -1,4 +1,5 @@
#nullable disable
using AngleSharp.Dom;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
@@ -222,4 +223,20 @@ public sealed class FilterService : IExecOnMessage
return false;
}
public async Task<ServerFilterSettings> GetFilterSettings(ulong guildId)
{
await using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set
.Include(x => x.FilterInvitesChannelIds)
.Include(x => x.FilterLinksChannelIds));
return new()
{
FilterInvitesChannels = gc.FilterInvitesChannelIds.Map(x => x.ChannelId),
FilterLinksChannels = gc.FilterLinksChannelIds.Map(x => x.ChannelId),
FilterInvitesEnabled = gc.FilterInvites,
FilterLinksEnabled = gc.FilterLinks,
};
}
}

View File

@@ -0,0 +1,10 @@
#nullable disable
namespace NadekoBot.Modules.Permissions.Services;
public readonly struct ServerFilterSettings
{
public bool FilterInvitesEnabled { get; init; }
public bool FilterLinksEnabled { get; init; }
public IReadOnlyCollection<ulong> FilterInvitesChannels { get; init; }
public IReadOnlyCollection<ulong> FilterLinksChannels { get; init; }
}

View File

@@ -32,33 +32,43 @@ public partial class Searches
[UserPerm(GuildPerm.ManageMessages)]
public async Task Feed(string url, [Leftover] ITextChannel channel = null)
{
var success = Uri.TryCreate(url, UriKind.Absolute, out var uri)
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
if (success)
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)
|| (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
{
channel ??= (ITextChannel)ctx.Channel;
try
{
await FeedReader.ReadAsync(url);
}
catch (Exception ex)
{
Log.Information(ex, "Unable to get feeds from that url");
success = false;
}
await ReplyErrorLocalizedAsync(strs.feed_invalid_url);
return;
}
if (success)
channel ??= (ITextChannel)ctx.Channel;
try
{
success = _service.AddFeed(ctx.Guild.Id, channel.Id, url);
if (success)
{
await ReplyConfirmLocalizedAsync(strs.feed_added);
return;
}
await FeedReader.ReadAsync(url);
}
catch (Exception ex)
{
Log.Information(ex, "Unable to get feeds from that url");
await ReplyErrorLocalizedAsync(strs.feed_cant_parse);
return;
}
await ReplyConfirmLocalizedAsync(strs.feed_not_valid);
var result = _service.AddFeed(ctx.Guild.Id, channel.Id, url);
if (result == FeedAddResult.Success)
{
await ReplyConfirmLocalizedAsync(strs.feed_added);
return;
}
if (result == FeedAddResult.Duplicate)
{
await ReplyErrorLocalizedAsync(strs.feed_duplicate);
return;
}
if (result == FeedAddResult.LimitReached)
{
await ReplyErrorLocalizedAsync(strs.feed_limit_reached);
return;
}
}
[Cmd]

View File

@@ -207,7 +207,7 @@ public class FeedsService : INService
.ToList();
}
public bool AddFeed(ulong guildId, ulong channelId, string rssFeed)
public FeedAddResult AddFeed(ulong guildId, ulong channelId, string rssFeed)
{
ArgumentNullException.ThrowIfNull(rssFeed, nameof(rssFeed));
@@ -221,9 +221,9 @@ public class FeedsService : INService
var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.FeedSubs));
if (gc.FeedSubs.Any(x => x.Url.ToLower() == fs.Url.ToLower()))
return false;
return FeedAddResult.Duplicate;
if (gc.FeedSubs.Count >= 10)
return false;
return FeedAddResult.LimitReached;
gc.FeedSubs.Add(fs);
uow.SaveChanges();
@@ -242,7 +242,7 @@ public class FeedsService : INService
});
}
return true;
return FeedAddResult.Success;
}
public bool RemoveFeed(ulong guildId, int index)
@@ -271,3 +271,11 @@ public class FeedsService : INService
return true;
}
}
public enum FeedAddResult
{
Success,
LimitReached,
Invalid,
Duplicate,
}

View File

@@ -9,7 +9,7 @@ namespace NadekoBot.Modules.Utility;
public partial class Utility
{
[Group]
public partial class CommandMapCommands : NadekoModule<CommandMapService>
public partial class CommandMapCommands : NadekoModule<AliasService>
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;

View File

@@ -6,15 +6,14 @@ using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Utility.Services;
public class CommandMapService : IInputTransformer, INService
public class AliasService : IInputTransformer, INService
{
public ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>> AliasMaps { get; } = new();
private readonly IEmbedBuilderService _eb;
private readonly DbService _db;
//commandmap
public CommandMapService(DiscordSocketClient client, DbService db, IEmbedBuilderService eb)
public AliasService(DiscordSocketClient client, DbService db, IEmbedBuilderService eb)
{
_eb = eb;
@@ -66,7 +65,10 @@ public class CommandMapService : IInputTransformer, INService
}
else if (input.StartsWith(k + ' ', StringComparison.OrdinalIgnoreCase))
{
newInput = v + ' ' + input[k.Length..];
if (v.Contains("%target%"))
newInput = v.Replace("%target%", input[k.Length..]);
else
newInput = v + ' ' + input[k.Length..];
}
if (newInput is not null)
@@ -74,17 +76,11 @@ public class CommandMapService : IInputTransformer, INService
try
{
var toDelete = await channel.SendConfirmAsync(_eb, $"{input} => {newInput}");
_ = Task.Run(async () =>
{
await Task.Delay(1500);
await toDelete.DeleteAsync(new()
{
RetryMode = RetryMode.AlwaysRetry
});
});
toDelete.DeleteAfter(1.5f);
}
catch
{
// ignored
}
return newInput;
@@ -92,34 +88,6 @@ public class CommandMapService : IInputTransformer, INService
}
return null;
// var keys = maps.Keys.OrderByDescending(x => x.Length);
// foreach (var k in keys)
// {
// string newInput;
// if (input.StartsWith(k + " ", StringComparison.InvariantCultureIgnoreCase))
// newInput = maps[k] + input.Substring(k.Length, input.Length - k.Length);
// else if (input.Equals(k, StringComparison.InvariantCultureIgnoreCase))
// newInput = maps[k];
// else
// continue;
//
// try
// {
// var toDelete = await channel.SendConfirmAsync(_eb, $"{input} => {newInput}");
// _ = Task.Run(async () =>
// {
// await Task.Delay(1500);
// await toDelete.DeleteAsync(new()
// {
// RetryMode = RetryMode.AlwaysRetry
// });
// });
// }
// catch { }
//
// return newInput;
// }
}
return null;

View File

@@ -1,24 +1,24 @@
#nullable disable
using System.Globalization;
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Services.Database.Models;
using System.Text.RegularExpressions;
using Nadeko.Common;
namespace NadekoBot.Modules.Utility.Services;
public class RemindService : INService, IReadyExecutor
{
private readonly Regex _regex =
new(
@"^(?:in\s?)?\s*(?:(?<mo>\d+)(?:\s?(?:months?|mos?),?))?(?:(?:\sand\s|\s*)?(?<w>\d+)(?:\s?(?:weeks?|w),?))?(?:(?:\sand\s|\s*)?(?<d>\d+)(?:\s?(?:days?|d),?))?(?:(?:\sand\s|\s*)?(?<h>\d+)(?:\s?(?:hours?|h),?))?(?:(?:\sand\s|\s*)?(?<m>\d+)(?:\s?(?:minutes?|mins?|m),?))?\s+(?:to:?\s+)?(?<what>(?:\r\n|[\r\n]|.)+)",
new(@"^(?:(?:at|on(?:\sthe)?)?\s*(?<date>(?:\d{2}:\d{2}\s)?\d{1,2}\.\d{1,2}(?:\.\d{2,4})?)|(?:in\s?)?\s*(?:(?<mo>\d+)(?:\s?(?:months?|mos?),?))?(?:(?:\sand\s|\s*)?(?<w>\d+)(?:\s?(?:weeks?|w),?))?(?:(?:\sand\s|\s*)?(?<d>\d+)(?:\s?(?:days?|d),?))?(?:(?:\sand\s|\s*)?(?<h>\d+)(?:\s?(?:hours?|h),?))?(?:(?:\sand\s|\s*)?(?<m>\d+)(?:\s?(?:minutes?|mins?|m),?))?)\s+(?:to:?\s+)?(?<what>(?:\r\n|[\r\n]|.)+)",
RegexOptions.Compiled | RegexOptions.Multiline);
private readonly DiscordSocketClient _client;
private readonly DbService _db;
private readonly IBotCredentials _creds;
private readonly IEmbedBuilderService _eb;
private readonly CultureInfo _culture;
public RemindService(
DiscordSocketClient client,
@@ -30,6 +30,15 @@ public class RemindService : INService, IReadyExecutor
_db = db;
_creds = creds;
_eb = eb;
try
{
_culture = new CultureInfo("en-GB");
}
catch
{
_culture = CultureInfo.InvariantCulture;
}
}
public async Task OnReadyAsync()
@@ -105,32 +114,57 @@ public class RemindService : INService, IReadyExecutor
return false;
}
foreach (var groupName in _regex.GetGroupNames())
TimeSpan ts;
var dateString = m.Groups["date"].Value;
if (!string.IsNullOrWhiteSpace(dateString))
{
if (groupName is "0" or "what")
continue;
if (string.IsNullOrWhiteSpace(m.Groups[groupName].Value))
{
values[groupName] = 0;
continue;
}
var now = DateTime.UtcNow;
if (!int.TryParse(m.Groups[groupName].Value, out var value))
if (!DateTime.TryParse(dateString, _culture, DateTimeStyles.None, out var dt))
{
Log.Warning("Reminder regex group {GroupName} has invalid value", groupName);
Log.Warning("Invalid remind datetime format");
return false;
}
if (value < 1)
if (now >= dt)
{
Log.Warning("Reminder time value has to be an integer greater than 0");
Log.Warning("That remind time has already passed");
return false;
}
values[groupName] = value;
ts = dt - now;
}
else
{
foreach (var groupName in _regex.GetGroupNames())
{
if (groupName is "0" or "what")
continue;
if (string.IsNullOrWhiteSpace(m.Groups[groupName].Value))
{
values[groupName] = 0;
continue;
}
if (!int.TryParse(m.Groups[groupName].Value, out var value))
{
Log.Warning("Reminder regex group {GroupName} has invalid value", groupName);
return false;
}
if (value < 1)
{
Log.Warning("Reminder time value has to be an integer greater than 0");
return false;
}
values[groupName] = value;
}
ts = new TimeSpan((30 * values["mo"]) + (7 * values["w"]) + values["d"], values["h"], values["m"], 0);
}
var ts = new TimeSpan((30 * values["mo"]) + (7 * values["w"]) + values["d"], values["h"], values["m"], 0);
obj = new()
{

View File

@@ -17,11 +17,14 @@ public partial class Xp
[Cmd]
public async Task ClubTransfer([Leftover] IUser newOwner)
{
var club = _service.TransferClub(ctx.User, newOwner);
var result = _service.TransferClub(ctx.User, newOwner);
if (club is null)
if (!result.TryPickT0(out var club, out var error))
{
await ReplyErrorLocalizedAsync(strs.club_transfer_failed);
if(error == ClubTransferError.NotOwner)
await ReplyErrorLocalizedAsync(strs.club_owner_only);
else
await ReplyErrorLocalizedAsync(strs.club_target_not_member);
}
else
{
@@ -37,15 +40,20 @@ public partial class Xp
[Cmd]
public async Task ClubAdmin([Leftover] IUser toAdmin)
{
var admin = await _service.ToggleAdminAsync(ctx.User, toAdmin);
var result = await _service.ToggleAdminAsync(ctx.User, toAdmin);
if (admin is null)
await ReplyErrorLocalizedAsync(strs.club_admin_error);
else if (admin is true)
if (result == ToggleAdminResult.AddedAdmin)
await ReplyConfirmLocalizedAsync(strs.club_admin_add(Format.Bold(toAdmin.ToString())));
else
else if (result == ToggleAdminResult.RemovedAdmin)
await ReplyConfirmLocalizedAsync(strs.club_admin_remove(Format.Bold(toAdmin.ToString())));
else if (result == ToggleAdminResult.NotOwner)
await ReplyErrorLocalizedAsync(strs.club_owner_only);
else if (result == ToggleAdminResult.CantTargetThyself)
await ReplyErrorLocalizedAsync(strs.club_admin_invalid_target);
else if (result == ToggleAdminResult.TargetNotMember)
await ReplyErrorLocalizedAsync(strs.club_target_not_member);
}
[Cmd]
public async Task ClubCreate([Leftover] string clubName)
{
@@ -55,17 +63,23 @@ public partial class Xp
return;
}
var succ = await _service.CreateClubAsync(ctx.User, clubName);
var result = await _service.CreateClubAsync(ctx.User, clubName);
if (succ is null)
if (result == ClubCreateResult.NameTaken)
{
await ReplyErrorLocalizedAsync(strs.club_create_error_name);
return;
}
if (succ is false)
if (result == ClubCreateResult.InsufficientLevel)
{
await ReplyErrorLocalizedAsync(strs.club_create_error);
await ReplyErrorLocalizedAsync(strs.club_create_insuff_lvl);
return;
}
if (result == ClubCreateResult.AlreadyInAClub)
{
await ReplyErrorLocalizedAsync(strs.club_already_in);
return;
}
@@ -75,14 +89,21 @@ public partial class Xp
[Cmd]
public async Task ClubIcon([Leftover] string url = null)
{
if ((!Uri.IsWellFormedUriString(url, UriKind.Absolute) && url is not null)
|| !await _service.SetClubIconAsync(ctx.User.Id, url))
if ((!Uri.IsWellFormedUriString(url, UriKind.Absolute) && url is not null))
{
await ReplyErrorLocalizedAsync(strs.club_icon_error);
await ReplyErrorLocalizedAsync(strs.club_icon_url_format);
return;
}
await ReplyConfirmLocalizedAsync(strs.club_icon_set);
var result = await _service.SetClubIconAsync(ctx.User.Id, url);
if(result == SetClubIconResult.Success)
await ReplyConfirmLocalizedAsync(strs.club_icon_set);
else if (result == SetClubIconResult.NotOwner)
await ReplyErrorLocalizedAsync(strs.club_owner_only);
else if (result == SetClubIconResult.TooLarge)
await ReplyErrorLocalizedAsync(strs.club_icon_too_large);
else if (result == SetClubIconResult.InvalidFileType)
await ReplyErrorLocalizedAsync(strs.club_icon_invalid_filetype);
}
private async Task InternalClubInfoAsync(ClubInfo club)
@@ -102,27 +123,27 @@ public partial class Xp
page =>
{
var embed = _eb.Create()
.WithOkColor()
.WithTitle($"{club}")
.WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)")))
.AddField(GetText(strs.desc),
string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description)
.AddField(GetText(strs.owner), club.Owner.ToString(), true)
// .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true)
.AddField(GetText(strs.members),
string.Join("\n",
users.Skip(page * 10)
.Take(10)
.Select(x =>
{
var l = new LevelStats(x.TotalXp);
var lvlStr = Format.Bold($" ⟪{l.Level}⟫");
if (club.OwnerId == x.Id)
return x + "🌟" + lvlStr;
if (x.IsClubAdmin)
return x + "⭐" + lvlStr;
return x + lvlStr;
})));
.WithOkColor()
.WithTitle($"{club}")
.WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)")))
.AddField(GetText(strs.desc),
string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description)
.AddField(GetText(strs.owner), club.Owner.ToString(), true)
// .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true)
.AddField(GetText(strs.members),
string.Join("\n",
users.Skip(page * 10)
.Take(10)
.Select(x =>
{
var l = new LevelStats(x.TotalXp);
var lvlStr = Format.Bold($" ⟪{l.Level}⟫");
if (club.OwnerId == x.Id)
return x + "🌟" + lvlStr;
if (x.IsClubAdmin)
return x + "⭐" + lvlStr;
return x + lvlStr;
})));
if (Uri.IsWellFormedUriString(club.ImageUrl, UriKind.Absolute))
return embed.WithThumbnailUrl(club.ImageUrl);
@@ -175,19 +196,21 @@ public partial class Xp
var club = _service.GetClubWithBansAndApplications(ctx.User.Id);
if (club is null)
return ReplyErrorLocalizedAsync(strs.club_not_exists_owner);
return ReplyErrorLocalizedAsync(strs.club_admin_perms);
var bans = club.Bans.Select(x => x.User).ToArray();
return ctx.SendPaginatedConfirmAsync(page,
_ =>
{
var toShow = string.Join("\n", bans.Skip(page * 10).Take(10).Select(x => x.ToString()));
var toShow = string.Join("\n", bans
.Skip(page * 10).Take(10)
.Select(x => x.ToString()));
return _eb.Create()
.WithTitle(GetText(strs.club_bans_for(club.ToString())))
.WithDescription(toShow)
.WithOkColor();
.WithTitle(GetText(strs.club_bans_for(club.ToString())))
.WithDescription(toShow)
.WithOkColor();
},
bans.Length,
10);
@@ -201,7 +224,7 @@ public partial class Xp
var club = _service.GetClubWithBansAndApplications(ctx.User.Id);
if (club is null)
return ReplyErrorLocalizedAsync(strs.club_not_exists_owner);
return ReplyErrorLocalizedAsync(strs.club_admin_perms);
var apps = club.Applicants.Select(x => x.User).ToArray();
@@ -211,9 +234,9 @@ public partial class Xp
var toShow = string.Join("\n", apps.Skip(page * 10).Take(10).Select(x => x.ToString()));
return _eb.Create()
.WithTitle(GetText(strs.club_apps_for(club.ToString())))
.WithDescription(toShow)
.WithOkColor();
.WithTitle(GetText(strs.club_apps_for(club.ToString())))
.WithDescription(toShow)
.WithOkColor();
},
apps.Length,
10);
@@ -231,10 +254,15 @@ public partial class Xp
return;
}
if (_service.ApplyToClub(ctx.User, club))
var result = _service.ApplyToClub(ctx.User, club);
if (result == ClubApplyResult.Success)
await ReplyConfirmLocalizedAsync(strs.club_applied(Format.Bold(club.ToString())));
else
await ReplyErrorLocalizedAsync(strs.club_apply_error);
else if (result == ClubApplyResult.Banned)
await ReplyErrorLocalizedAsync(strs.club_join_banned);
else if (result == ClubApplyResult.InsufficientLevel)
await ReplyErrorLocalizedAsync(strs.club_insuff_lvl);
else if (result == ClubApplyResult.AlreadyInAClub)
await ReplyErrorLocalizedAsync(strs.club_already_in);
}
[Cmd]
@@ -246,19 +274,26 @@ public partial class Xp
[Priority(0)]
public async Task ClubAccept([Leftover] string userName)
{
if (_service.AcceptApplication(ctx.User.Id, userName, out var discordUser))
var result = _service.AcceptApplication(ctx.User.Id, userName, out var discordUser);
if (result == ClubAcceptResult.Accepted)
await ReplyConfirmLocalizedAsync(strs.club_accepted(Format.Bold(discordUser.ToString())));
else
await ReplyErrorLocalizedAsync(strs.club_accept_error);
else if(result == ClubAcceptResult.NoSuchApplicant)
await ReplyErrorLocalizedAsync(strs.club_accept_invalid_applicant);
else if(result == ClubAcceptResult.NotOwnerOrAdmin)
await ReplyErrorLocalizedAsync(strs.club_admin_perms);
}
[Cmd]
public async Task Clubleave()
public async Task ClubLeave()
{
if (_service.LeaveClub(ctx.User))
var res = _service.LeaveClub(ctx.User);
if (res == ClubLeaveResult.Success)
await ReplyConfirmLocalizedAsync(strs.club_left);
else if (res == ClubLeaveResult.NotInAClub)
await ReplyErrorLocalizedAsync(strs.club_not_in_a_club);
else
await ReplyErrorLocalizedAsync(strs.club_not_in_club);
await ReplyErrorLocalizedAsync(strs.club_owner_cant_leave);
}
[Cmd]
@@ -270,13 +305,20 @@ public partial class Xp
[Priority(0)]
public Task ClubKick([Leftover] string userName)
{
if (_service.Kick(ctx.User.Id, userName, out var club))
var result = _service.Kick(ctx.User.Id, userName, out var club);
if(result == ClubKickResult.Success)
{
return ReplyConfirmLocalizedAsync(strs.club_user_kick(Format.Bold(userName),
Format.Bold(club.ToString())));
}
return ReplyErrorLocalizedAsync(strs.club_user_kick_fail);
if (result == ClubKickResult.Hierarchy)
return ReplyErrorLocalizedAsync(strs.club_kick_hierarchy);
if (result == ClubKickResult.NotOwnerOrAdmin)
return ReplyErrorLocalizedAsync(strs.club_admin_perms);
return ReplyErrorLocalizedAsync(strs.club_target_not_member);
}
[Cmd]
@@ -288,13 +330,20 @@ public partial class Xp
[Priority(0)]
public Task ClubBan([Leftover] string userName)
{
if (_service.Ban(ctx.User.Id, userName, out var club))
var result = _service.Ban(ctx.User.Id, userName, out var club);
if (result == ClubBanResult.Success)
{
return ReplyConfirmLocalizedAsync(strs.club_user_banned(Format.Bold(userName),
Format.Bold(club.ToString())));
}
return ReplyErrorLocalizedAsync(strs.club_user_ban_fail);
if (result == ClubBanResult.Unbannable)
return ReplyErrorLocalizedAsync(strs.club_ban_fail_unbannable);
if (result == ClubBanResult.WrongUser)
return ReplyErrorLocalizedAsync(strs.club_ban_fail_user_not_found);
return ReplyErrorLocalizedAsync(strs.club_admin_perms);
}
[Cmd]
@@ -306,13 +355,20 @@ public partial class Xp
[Priority(0)]
public Task ClubUnBan([Leftover] string userName)
{
if (_service.UnBan(ctx.User.Id, userName, out var club))
var result = _service.UnBan(ctx.User.Id, userName, out var club);
if (result == ClubUnbanResult.Success)
{
return ReplyConfirmLocalizedAsync(strs.club_user_unbanned(Format.Bold(userName),
Format.Bold(club.ToString())));
}
return ReplyErrorLocalizedAsync(strs.club_user_unban_fail);
if (result == ClubUnbanResult.WrongUser)
{
return ReplyErrorLocalizedAsync(strs.club_unban_fail_user_not_found);
}
return ReplyErrorLocalizedAsync(strs.club_admin_perms);
}
[Cmd]
@@ -325,10 +381,10 @@ public partial class Xp
: desc;
var eb = _eb.Create(ctx)
.WithAuthor(ctx.User)
.WithTitle(GetText(strs.club_desc_update))
.WithOkColor()
.WithDescription(desc);
.WithAuthor(ctx.User)
.WithTitle(GetText(strs.club_desc_update))
.WithOkColor()
.WithDescription(desc);
await ctx.Channel.EmbedAsync(eb);
}

View File

@@ -2,9 +2,9 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Nadeko.Common;
using NadekoBot.Db;
using NadekoBot.Db.Models;
using OneOf;
namespace NadekoBot.Modules.Xp.Services;
@@ -19,7 +19,8 @@ public class ClubService : INService, IClubService
_httpFactory = httpFactory;
}
public async Task<bool?> CreateClubAsync(IUser user, string clubName)
public async Task<ClubCreateResult> CreateClubAsync(IUser user, string clubName)
{
//must be lvl 5 and must not be in a club already
@@ -27,11 +28,14 @@ public class ClubService : INService, IClubService
var du = uow.GetOrCreateUser(user);
var xp = new LevelStats(du.TotalXp);
if (xp.Level < 5 || du.ClubId is not null)
return false;
if (xp.Level < 5)
return ClubCreateResult.InsufficientLevel;
if (du.ClubId is not null)
return ClubCreateResult.AlreadyInAClub;
if (await uow.Clubs.AnyAsyncEF(x => x.Name == clubName))
return null;
return ClubCreateResult.NameTaken;
du.IsClubAdmin = true;
du.Club = new()
@@ -45,17 +49,20 @@ public class ClubService : INService, IClubService
await uow.GetTable<ClubApplicants>()
.DeleteAsync(x => x.UserId == du.Id);
return true;
return ClubCreateResult.Success;
}
public ClubInfo TransferClub(IUser from, IUser newOwner)
public OneOf<ClubInfo, ClubTransferError> TransferClub(IUser from, IUser newOwner)
{
using var uow = _db.GetDbContext();
var club = uow.Clubs.GetByOwner(@from.Id);
var newOwnerUser = uow.GetOrCreateUser(newOwner);
if (club is null || club.Owner.UserId != from.Id || !club.Members.Contains(newOwnerUser))
return null;
if (club is null || club.Owner.UserId != from.Id)
return ClubTransferError.NotOwner;
if (!club.Members.Contains(newOwnerUser))
return ClubTransferError.TargetNotMember;
club.Owner.IsClubAdmin = true; // old owner will stay as admin
newOwnerUser.IsClubAdmin = true;
@@ -64,21 +71,24 @@ public class ClubService : INService, IClubService
return club;
}
public async Task<bool?> ToggleAdminAsync(IUser owner, IUser toAdmin)
public async Task<ToggleAdminResult> ToggleAdminAsync(IUser owner, IUser toAdmin)
{
if (owner.Id == toAdmin.Id)
return ToggleAdminResult.CantTargetThyself;
await using var uow = _db.GetDbContext();
var club = uow.Clubs.GetByOwner(owner.Id);
var adminUser = uow.GetOrCreateUser(toAdmin);
if (club is null || club.Owner.UserId != owner.Id || !club.Members.Contains(adminUser))
return null;
if (club is null)
return ToggleAdminResult.NotOwner;
if (club.OwnerId == adminUser.Id)
return true;
if(!club.Members.Contains(adminUser))
return ToggleAdminResult.TargetNotMember;
var newState = adminUser.IsClubAdmin = !adminUser.IsClubAdmin;
await uow.SaveChangesAsync();
return newState;
return newState ? ToggleAdminResult.AddedAdmin : ToggleAdminResult.RemovedAdmin;
}
public ClubInfo GetClubByMember(IUser user)
@@ -88,26 +98,30 @@ public class ClubService : INService, IClubService
return member;
}
public async Task<bool> SetClubIconAsync(ulong ownerUserId, string url)
public async Task<SetClubIconResult> SetClubIconAsync(ulong ownerUserId, string url)
{
if (url is not null)
{
using var http = _httpFactory.CreateClient();
using var temp = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
if (!temp.IsImage() || temp.GetContentLength() > 5.Megabytes().Bytes)
return false;
if (!temp.IsImage())
return SetClubIconResult.InvalidFileType;
if (temp.GetContentLength() > 5.Megabytes().Bytes)
return SetClubIconResult.TooLarge;
}
await using var uow = _db.GetDbContext();
var club = uow.Clubs.GetByOwner(ownerUserId);
if (club is null)
return false;
return SetClubIconResult.NotOwner;
club.ImageUrl = url;
await uow.SaveChangesAsync();
return true;
return SetClubIconResult.Success;
}
public bool GetClubByName(string clubName, out ClubInfo club)
@@ -118,18 +132,22 @@ public class ClubService : INService, IClubService
return club is not null;
}
public bool ApplyToClub(IUser user, ClubInfo club)
public ClubApplyResult ApplyToClub(IUser user, ClubInfo club)
{
using var uow = _db.GetDbContext();
var du = uow.GetOrCreateUser(user);
uow.SaveChanges();
if (du.Club is not null
|| club.Bans.Any(x => x.UserId == du.Id)
|| club.Applicants.Any(x => x.UserId == du.Id))
//user banned or a member of a club, or already applied,
// or doesn't min minumum level requirement, can't apply
return false;
//user banned or a member of a club, or already applied,
// or doesn't min minumum level requirement, can't apply
if (du.Club is not null)
return ClubApplyResult.AlreadyInAClub;
if (club.Bans.Any(x => x.UserId == du.Id))
return ClubApplyResult.Banned;
if (club.Applicants.Any(x => x.UserId == du.Id))
return ClubApplyResult.InsufficientLevel;
var app = new ClubApplicants
{
@@ -138,23 +156,23 @@ public class ClubService : INService, IClubService
};
uow.Set<ClubApplicants>().Add(app);
uow.SaveChanges();
return true;
return ClubApplyResult.Success;
}
public bool AcceptApplication(ulong clubOwnerUserId, string userName, out DiscordUser discordUser)
public ClubAcceptResult AcceptApplication(ulong clubOwnerUserId, string userName, out DiscordUser discordUser)
{
discordUser = null;
using var uow = _db.GetDbContext();
var club = uow.Clubs.GetByOwnerOrAdmin(clubOwnerUserId);
if (club is null)
return false;
return ClubAcceptResult.NotOwnerOrAdmin;
var applicant =
club.Applicants.FirstOrDefault(x => x.User.ToString().ToUpperInvariant() == userName.ToUpperInvariant());
if (applicant is null)
return false;
return ClubAcceptResult.NoSuchApplicant;
applicant.User.Club = club;
applicant.User.IsClubAdmin = false;
@@ -166,7 +184,7 @@ public class ClubService : INService, IClubService
discordUser = applicant.User;
uow.SaveChanges();
return true;
return ClubAcceptResult.Accepted;
}
public ClubInfo GetClubWithBansAndApplications(ulong ownerUserId)
@@ -175,17 +193,19 @@ public class ClubService : INService, IClubService
return uow.Clubs.GetByOwnerOrAdmin(ownerUserId);
}
public bool LeaveClub(IUser user)
public ClubLeaveResult LeaveClub(IUser user)
{
using var uow = _db.GetDbContext();
var du = uow.GetOrCreateUser(user, x => x.Include(u => u.Club));
if (du.Club is null || du.Club.OwnerId == du.Id)
return false;
if (du.Club is null)
return ClubLeaveResult.NotInAClub;
if (du.Club.OwnerId == du.Id)
return ClubLeaveResult.OwnerCantLeave;
du.Club = null;
du.IsClubAdmin = false;
uow.SaveChanges();
return true;
return ClubLeaveResult.Success;
}
public bool SetDescription(ulong userId, string desc)
@@ -213,23 +233,23 @@ public class ClubService : INService, IClubService
return true;
}
public bool Ban(ulong bannerId, string userName, out ClubInfo club)
public ClubBanResult Ban(ulong bannerId, string userName, out ClubInfo club)
{
using var uow = _db.GetDbContext();
club = uow.Clubs.GetByOwnerOrAdmin(bannerId);
if (club is null)
return false;
return ClubBanResult.NotOwnerOrAdmin;
var usr = club.Members.FirstOrDefault(x => x.ToString().ToUpperInvariant() == userName.ToUpperInvariant())
?? club.Applicants
.FirstOrDefault(x => x.User.ToString().ToUpperInvariant() == userName.ToUpperInvariant())
?.User;
if (usr is null)
return false;
return ClubBanResult.WrongUser;
if (club.OwnerId == usr.Id
|| (usr.IsClubAdmin && club.Owner.UserId != bannerId)) // can't ban the owner kek, whew
return false;
return ClubBanResult.Unbannable;
club.Bans.Add(new()
{
@@ -244,39 +264,40 @@ public class ClubService : INService, IClubService
uow.SaveChanges();
return true;
return ClubBanResult.Success;
}
public bool UnBan(ulong ownerUserId, string userName, out ClubInfo club)
public ClubUnbanResult UnBan(ulong ownerUserId, string userName, out ClubInfo club)
{
using var uow = _db.GetDbContext();
club = uow.Clubs.GetByOwnerOrAdmin(ownerUserId);
if (club is null)
return false;
return ClubUnbanResult.NotOwnerOrAdmin;
var ban = club.Bans.FirstOrDefault(x => x.User.ToString().ToUpperInvariant() == userName.ToUpperInvariant());
if (ban is null)
return false;
return ClubUnbanResult.WrongUser;
club.Bans.Remove(ban);
uow.SaveChanges();
return true;
return ClubUnbanResult.Success;
}
public bool Kick(ulong kickerId, string userName, out ClubInfo club)
public ClubKickResult Kick(ulong kickerId, string userName, out ClubInfo club)
{
using var uow = _db.GetDbContext();
club = uow.Clubs.GetByOwnerOrAdmin(kickerId);
if (club is null)
return false;
return ClubKickResult.NotOwnerOrAdmin;
var usr = club.Members.FirstOrDefault(x => x.ToString().ToUpperInvariant() == userName.ToUpperInvariant());
if (usr is null)
return false;
return ClubKickResult.TargetNotAMember;
if (club.OwnerId == usr.Id || (usr.IsClubAdmin && club.Owner.UserId != kickerId))
return false;
return ClubKickResult.Hierarchy;
club.Members.Remove(usr);
var app = club.Applicants.FirstOrDefault(x => x.UserId == usr.Id);
@@ -284,7 +305,7 @@ public class ClubService : INService, IClubService
club.Applicants.Remove(app);
uow.SaveChanges();
return true;
return ClubKickResult.Success;
}
public List<ClubInfo> GetClubLeaderboardPage(int page)

View File

@@ -1,23 +1,33 @@
using NadekoBot.Db.Models;
using OneOf;
namespace NadekoBot.Modules.Xp.Services;
public interface IClubService
{
Task<bool?> CreateClubAsync(IUser user, string clubName);
ClubInfo? TransferClub(IUser from, IUser newOwner);
Task<bool?> ToggleAdminAsync(IUser owner, IUser toAdmin);
Task<ClubCreateResult> CreateClubAsync(IUser user, string clubName);
OneOf<ClubInfo,ClubTransferError> TransferClub(IUser from, IUser newOwner);
Task<ToggleAdminResult> ToggleAdminAsync(IUser owner, IUser toAdmin);
ClubInfo? GetClubByMember(IUser user);
Task<bool> SetClubIconAsync(ulong ownerUserId, string? url);
Task<SetClubIconResult> SetClubIconAsync(ulong ownerUserId, string? url);
bool GetClubByName(string clubName, out ClubInfo club);
bool ApplyToClub(IUser user, ClubInfo club);
bool AcceptApplication(ulong clubOwnerUserId, string userName, out DiscordUser discordUser);
ClubApplyResult ApplyToClub(IUser user, ClubInfo club);
ClubAcceptResult AcceptApplication(ulong clubOwnerUserId, string userName, out DiscordUser discordUser);
ClubInfo? GetClubWithBansAndApplications(ulong ownerUserId);
bool LeaveClub(IUser user);
ClubLeaveResult LeaveClub(IUser user);
bool SetDescription(ulong userId, string? desc);
bool Disband(ulong userId, out ClubInfo club);
bool Ban(ulong bannerId, string userName, out ClubInfo club);
bool UnBan(ulong ownerUserId, string userName, out ClubInfo club);
bool Kick(ulong kickerId, string userName, out ClubInfo club);
ClubBanResult Ban(ulong bannerId, string userName, out ClubInfo club);
ClubUnbanResult UnBan(ulong ownerUserId, string userName, out ClubInfo club);
ClubKickResult Kick(ulong kickerId, string userName, out ClubInfo club);
List<ClubInfo> GetClubLeaderboardPage(int page);
}
public enum ClubApplyResult
{
Success,
AlreadyInAClub,
Banned,
InsufficientLevel
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Xp.Services;
public enum ClubAcceptResult
{
Accepted,
NotOwnerOrAdmin,
NoSuchApplicant,
}

View File

@@ -0,0 +1,10 @@
namespace NadekoBot.Modules.Xp.Services;
public enum ClubBanResult
{
Success,
NotOwnerOrAdmin,
WrongUser,
Unbannable,
}

View File

@@ -0,0 +1,9 @@
namespace NadekoBot.Modules.Xp.Services;
public enum ClubCreateResult
{
Success,
AlreadyInAClub,
NameTaken,
InsufficientLevel,
}

View File

@@ -0,0 +1,9 @@
namespace NadekoBot.Modules.Xp.Services;
public enum ClubKickResult
{
Success,
NotOwnerOrAdmin,
TargetNotAMember,
Hierarchy
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Xp.Services;
public enum ClubLeaveResult
{
Success,
OwnerCantLeave,
NotInAClub
}

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