Compare commits

...

26 Commits
4.1.0 ... 4.1.4

Author SHA1 Message Date
Kwoth
44478e0f47 Upped version to 4.1.4 2022-05-06 19:15:29 +02:00
Kwoth
c73c2da6a4 fixed .yun, closes #332 2022-05-06 04:31:44 +02:00
Kwoth
5ed005211e Added bank information to .economy 2022-05-05 23:14:58 +02:00
Kwoth
d80cbb4647 Refactors. Cleanup. Refactored responses source gen a little. Parametrized localized strings are now generic. Refactored .cash bank interaction. Updated changelog. Reverted clubapps/ban/unban to use .ToString method and configured it to not have right to left unicode. Added extension methods to SocketMessageComponent akin to ones on the IMessageChannel (RespondConfirm, etc...) 2022-05-05 22:59:07 +02:00
Kwoth
9a96ef76ba Reverted smarttext to newtonsoft to relax the allowed json rules. Upped version to 4.1.3 2022-05-05 05:00:47 +02:00
Kwoth
5b5bc278ff - Reaction roles rewritten completely. They now support multiple exclusivity groups per message and level requirements. However they can only be added one by one
- Bot now support much higher XP values for global and server levels
2022-05-05 04:47:31 +02:00
Kwoth
5cb95cf94d Fixed an issue with embed array not building if the color is invalid or unspecified 2022-05-01 19:20:48 +02:00
Kwoth
f132aa2624 - Added a simple bank system. Users can deposit, withdraw and check the balance of their currency in the bank.
- Users can't check other user's bank balances.
- Added a button on a .$ command which, when clicked, sends you a message with your bank balance that only you can see.
- Updated pagination, it now uses buttons instead of reactions
- using .h <command group> (atm only .bank is a proper group) will list commands with their descriptions in that group
2022-04-29 07:22:49 +02:00
Kwoth
3b6b3bcf07 Added missing postgresql and mysql stondel migrations 2022-04-28 06:46:35 +02:00
Kwoth
78d97db224 .modules commanad now has a medusa module description 2022-04-28 06:20:46 +02:00
Kwoth
35ddd150ba Pagination is now using buttons instead of reactions 2022-04-28 06:10:06 +02:00
Kwoth
39ae070c9d Added .stondel which makes the bot delete stream online messages after the stream goes offline 2022-04-28 01:23:08 +02:00
Kwoth
24a9a02cc3 .give will send dms again 2022-04-27 00:36:27 +02:00
Kwoth
0f68abcac9 Fixed .deletexp command 2022-04-26 14:15:58 +02:00
Kwoth
908c61633d Added bash script prereq installer reference 2022-04-26 02:47:37 +02:00
Kwoth
054fc30672 Added prerequisites to linux release installation guide 2022-04-26 02:45:54 +02:00
Kwoth
11ffdd84a3 Updated changelog 2022-04-26 02:34:52 +02:00
Kwoth
5d2d74b92a Full support for embed arrays in .greet/.bye, .say and other commands which use embeds
- Website to create them is live at eb2.nadeko.bot (it will soon be replacing eb.nadeko.bot)
- Embed arrays don't have a plainText property (it's renamed to 'content')
- Embed arrays use color hex values instead of an integer
- Old embed format will still work
- There shouldn't be any breaking changes
2022-04-26 02:33:19 +02:00
Kwoth
18400dc53a Fixed a nullref message when the bot is loading medusae. Added support for multiple embeds in features which support custom embeds in the form of
{plainText:text-here, embeds: [embedObject, embedObject, embedObject]}
2022-04-17 09:58:30 +02:00
Kwoth
29d94640af Fix some build warnings 2022-04-16 19:00:28 +02:00
Kwoth
f6a53b96c7 Upped version to 4.1.2 2022-04-16 17:27:17 +02:00
Kwoth
1aa95a5dd0 Don't load uwu 2022-04-16 17:25:52 +02:00
Kwoth
fcfeb152c9 Updated changelog 2022-04-16 17:07:23 +02:00
Kwoth
0b64df95ef Fixed an issue with publish not having required dlls 2022-04-16 17:04:10 +02:00
Kwoth
7512f4a1e0 docs: fixed a link to nadeko discord server 2022-04-16 16:35:20 +02:00
Kwoth
789c453863 Updated CHANGELOG.md 2022-04-16 16:25:24 +02:00
91 changed files with 32067 additions and 1146 deletions

View File

@@ -1,10 +1,67 @@
# Changelog
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
#todo .trans fix
## Unreleased
### [4.1.4] - 06-05-2022
### Fixed
- Fixed `.yun`
## [4.1.3] - 06.05.2022
### Added
- Added support for embed arrays in commands such as .say, .greet, .bye, etc...
- Website to create them is live at eb.nadeko.bot (old one is moved to oldeb.nadeko.bot)
- Embed arrays don't have a plainText property (it's renamed to 'content')
- Embed arrays use color hex values instead of an integer
- Old embed format will still work
- There shouldn't be any breaking changes
- Added `.stondel` command which, when toggled, will make the bot delete online stream messages on the server when the stream goes offline
- Added a simple bank system.
- Users can deposit, withdraw and check the balance of their currency in the bank.
- Users can't check other user's bank balances.
- Added a button on a .$ command which, when clicked, sends you a message with your bank balance that only you can see.
- Added `.h <command group>`
- Using this command will list all commands in the specified group
- Atm only .bank is a proper group (`.h bank`)
- Added "Bank Accounts" entry to `.economy`
### Changed
- Reaction roles rewritten completely
- Supports multiple exclusivity groups per message
- Supports level requirements
- However they can only be added one by one
- Use the following commands for more information
- `.h .reroa`
- `.h .reroli`
- `.h .rerot`
- `.h .rerorm`
- `.h .rerodela`
- Pagination is now using buttons instead of reactions
- Bot will now support much higher XP values for global and server levels
- [dev] Small change and generation perf improvement for the localized response strings
### Fixed
- Fixed `.deletexp` command
- `.give` command should send DMs again
- `.modules` commanad now has a medusa module description
## [4.1.2] - 16.04.2022
### Fixed
- Fixed an issue with missing `.dll` files in release versions
## [4.1.0] - 16.04.2022
### Added
- NadekoBot now supports mysql, postgresql and sqlite
@@ -29,14 +86,14 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.
## [4.0.6] - 21.03.2022
### Fixes
### Fixed
- Fixed voice presence logging
- Fixed .clubaccept, .clubban, .clubkick and .clubunban commands
## [4.0.5] - 21.03.2022
### Fixes
### Fixed
- Fixed several bugs in the currency code
- Fixed some potential memory leaks

View File

@@ -92,6 +92,19 @@ Open Terminal (if you're on an installation with a window manager) and navigate
##### Release Update Instructions
###### Prerequisites
1. Nadeko requires redis to function
- ubuntu installation command: `sudo apt-get install redis-server`
2. Playing music requires `ffmpeg`, `libopus`, `libsodium` and `youtube-dl` (which in turn requires python3)
- ubuntu installation command: `sudo apt-get install ffmpeg libopus0 opus-tools libopus-dev libsodium-dev -y`
3. Make sure your python is version 3+ with `python --version`
- if it's not, you can install python 3 and make it the default with: `sudo apt-get install python3.8 python-is-python3`
*You can use nadeko bash script [prerequisites installer](https://gitlab.com/Kwoth/nadeko-bash-installer/-/blob/v4/n-prereq.sh) as a reference*
###### Installation
1. Stop the bot
2. Download the latest release from <https://gitlab.com/Kwoth/nadekobot/-/releases>
- Look for the file called "x.x.x-linux-x64-build.tar" (where `X.X.X` is a version, for example 3.0.4) and download it

View File

@@ -27,5 +27,5 @@ Follow the [creating a medusa guide](creating-a-medusa.md)
**It is strongly recommended to run only the medusae you yourself wrote, and only on a hosted VPS or dedicated server which ONLY hosts your bot, to minimize the potential damage caused by bad actors.**
No easy way at the moment, except asking in the `#dev-and-modding` chat in [#NadekoLog server][https://discord.nadeko.bot]
No easy way at the moment, except asking in the `#dev-and-modding` chat in [#NadekoLog server](https://discord.nadeko.bot)

View File

@@ -97,13 +97,12 @@ public class CmdAttribute : System.Attribute
var name = $"{model.Namespace}.{string.Join(".", model.ClassHierarchy)}.g.cs";
try
{
Debug.WriteLine($"Writing {name}");
var source = GetSourceText(model);
ctx.AddSource(name, SourceText.From(source, Encoding.UTF8));
}
catch (Exception ex)
{
Debug.WriteLine($"Error writing source file {name}\n" + ex);
Console.WriteLine($"Error writing source file {name}\n" + ex);
}
}
}

View File

@@ -62,6 +62,7 @@ namespace NadekoBot.Generators
sw.WriteLine("{");
sw.Indent++;
var typedParamStrings = new List<string>(10);
foreach (var field in fields)
{
var matches = Regex.Matches(field.Value, @"{(?<num>\d)[}:]");
@@ -71,20 +72,30 @@ namespace NadekoBot.Generators
max = Math.Max(max, int.Parse(match.Groups["num"].Value) + 1);
}
List<string> typedParamStrings = new List<string>();
var paramStrings = string.Empty;
typedParamStrings.Clear();
var typeParams = new string[max];
var passedParamString = string.Empty;
for (var i = 0; i < max; i++)
{
typedParamStrings.Add($"object p{i}");
paramStrings += $", p{i}";
typedParamStrings.Add($"in T{i} p{i}");
passedParamString += $", p{i}";
typeParams[i] = $"T{i}";
}
var sig = string.Empty;
if(max > 0)
var typeParamStr = string.Empty;
if (max > 0)
{
sig = $"({string.Join(", ", typedParamStrings)})";
sw.WriteLine($"public static LocStr {field.Name}{sig} => new LocStr(\"{field.Name}\"{paramStrings});");
typeParamStr = $"<{string.Join(", ", typeParams)}>";
}
sw.WriteLine("public static LocStr {0}{1}{2} => new LocStr(\"{3}\"{4});",
field.Name,
typeParamStr,
sig,
field.Name,
passedParamString);
}
sw.Indent--;

View File

@@ -67,12 +67,13 @@ public sealed class Bot
? GatewayIntents.All
: GatewayIntents.AllUnprivileged,
LogGatewayIntentWarnings = false,
FormatUsersInBidirectionalUnicode = false,
});
_commandService = new(new()
{
CaseSensitiveCommands = false,
DefaultRunMode = RunMode.Sync
DefaultRunMode = RunMode.Sync,
});
// _interactionService = new(Client.Rest);

View File

@@ -0,0 +1,27 @@
namespace NadekoBot;
public sealed class NadekoActionInteraction : NadekoOwnInteraction
{
private readonly NadekoInteractionData _data;
private readonly Func<SocketMessageComponent, Task> _action;
public NadekoActionInteraction(
DiscordSocketClient client,
ulong authorId,
NadekoInteractionData data,
Func<SocketMessageComponent, Task> action
)
: base(client, authorId)
{
_data = data;
_action = action;
}
public override string Name
=> _data.CustomId;
public override IEmote Emote
=> _data.Emote;
public override Task ExecuteOnActionAsync(SocketMessageComponent smc)
=> _action(smc);
}

View File

@@ -0,0 +1,77 @@
namespace NadekoBot;
public abstract class NadekoInteraction
{
// improvements:
// - state in OnAction
// - configurable delay
// -
public abstract string Name { get; }
public abstract IEmote Emote { get; }
protected readonly DiscordSocketClient _client;
protected readonly TaskCompletionSource<bool> _interactionCompletedSource;
protected IUserMessage message = null!;
protected NadekoInteraction(DiscordSocketClient client)
{
_client = client;
_interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
}
public async Task RunAsync(IUserMessage msg)
{
message = msg;
_client.InteractionCreated += OnInteraction;
await Task.WhenAny(Task.Delay(10_000), _interactionCompletedSource.Task);
_client.InteractionCreated -= OnInteraction;
await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
}
protected abstract ValueTask<bool> Validate(SocketMessageComponent smc);
private async Task OnInteraction(SocketInteraction arg)
{
if (arg is not SocketMessageComponent smc)
return;
if (smc.Message.Id != message.Id)
return;
if (smc.Data.CustomId != Name)
return;
if (!await Validate(smc))
{
await smc.DeferAsync();
return;
}
_ = Task.Run(async () =>
{
await ExecuteOnActionAsync(smc);
// this should only be a thing on single-response buttons
_interactionCompletedSource.TrySetResult(true);
if (!smc.HasResponded)
{
await smc.DeferAsync();
}
});
}
public MessageComponent CreateComponent()
{
var comp = new ComponentBuilder()
.WithButton(new ButtonBuilder(style: ButtonStyle.Secondary, emote: Emote, customId: Name));
return comp.Build();
}
public abstract Task ExecuteOnActionAsync(SocketMessageComponent smc);
}

View File

@@ -0,0 +1,41 @@
namespace NadekoBot;
/// <summary>
/// Builder class for NadekoInteractions
/// </summary>
public class NadekoInteractionBuilder
{
private NadekoInteractionData? iData;
private Func<SocketMessageComponent, Task>? action;
// private bool isOwn;
public NadekoInteractionBuilder WithData<T>(in T data)
where T : NadekoInteractionData
{
iData = data;
return this;
}
// public NadekoOwnInteractionBuiler WithIsOwn(bool isOwn = true)
// {
// this.isOwn = isOwn;
// return this;
// }
public NadekoInteractionBuilder WithAction(in Func<SocketMessageComponent, Task> fn)
{
this.action = fn;
return this;
}
public NadekoActionInteraction Build(DiscordSocketClient client, ulong userId)
{
if (iData is null)
throw new InvalidOperationException("You have to specify the data before building the interaction");
if (action is null)
throw new InvalidOperationException("You have to specify the action before building the interaction");
return new(client, userId, iData, action);
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot;
/// <summary>
/// Represents essential interacation data
/// </summary>
/// <param name="Emote">Emote which will show on a button</param>
/// <param name="CustomId">Custom interaction id</param>
public record NadekoInteractionData(IEmote Emote, string CustomId);

View File

@@ -0,0 +1,15 @@
namespace NadekoBot;
/// <summary>
/// Interaction which only the author can use
/// </summary>
public abstract class NadekoOwnInteraction : NadekoInteraction
{
protected readonly ulong _authorId;
protected NadekoOwnInteraction(DiscordSocketClient client, ulong authorId) : base(client)
=> _authorId = authorId;
protected override ValueTask<bool> Validate(SocketMessageComponent smc)
=> new(smc.User.Id == _authorId);
}

View File

@@ -0,0 +1,16 @@
#nullable disable
using LinqToDB;
using System.Linq.Expressions;
namespace NadekoBot.Common;
public static class Linq2DbExpressions
{
[ExpressionMethod(nameof(GuildOnShardExpression))]
public static bool GuildOnShard(ulong guildId, int totalShards, int shardId)
=> throw new NotSupportedException();
private static Expression<Func<ulong, int, int, bool>> GuildOnShardExpression()
=> (guildId, totalShards, shardId)
=> guildId / 4194304 % (ulong)totalShards == (ulong)shardId;
}

View File

@@ -1,4 +1,5 @@
using Cloneable;
#nullable enable
using Cloneable;
using NadekoBot.Common.Yml;
namespace Nadeko.Medusa;
@@ -10,7 +11,7 @@ public sealed partial class MedusaConfig : ICloneable<MedusaConfig>
public int Version { get; set; } = 1;
[Comment("List of medusae automatically loaded at startup")]
public List<string> Loaded { get; set; }
public List<string>? Loaded { get; set; }
public MedusaConfig()
{

View File

@@ -18,7 +18,7 @@ public sealed class MedusaConfigService : ConfigServiceBase<MedusaConfig>, IMedu
}
public IReadOnlyCollection<string> GetLoadedMedusae()
=> Data.Loaded.ToList();
=> Data.Loaded?.ToList() ?? new List<string>();
public void AddLoadedMedusa(string name)
{
@@ -26,6 +26,9 @@ public sealed class MedusaConfigService : ConfigServiceBase<MedusaConfig>, IMedu
ModifyConfig(conf =>
{
if (conf.Loaded is null)
conf.Loaded = new();
if(!conf.Loaded.Contains(name))
conf.Loaded.Add(name);
});
@@ -37,6 +40,9 @@ public sealed class MedusaConfigService : ConfigServiceBase<MedusaConfig>, IMedu
ModifyConfig(conf =>
{
if (conf.Loaded is null)
conf.Loaded = new();
conf.Loaded.Remove(name);
});
}

View File

@@ -191,18 +191,15 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor,
await _lock.WaitAsync();
try
{
var success = LoadAssemblyInternal(safeName,
out var ctx,
out var snekData,
out var services,
out var strings,
out var typeReaders);
if (success)
if (LoadAssemblyInternal(safeName,
out var ctx,
out var snekData,
out var services,
out var strings,
out var typeReaders))
{
var moduleInfos = new List<ModuleInfo>();
// todo uncomment
LoadTypeReadersInternal(typeReaders);
foreach (var point in snekData)
@@ -770,7 +767,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor,
var paramName = pi.Name ?? "unnamed";
var isContext = paramCounter == 0 && pi.ParameterType.IsAssignableTo(typeof(AnyContext));
var leftoverAttribute = pi.GetCustomAttribute<Nadeko.Snake.leftoverAttribute>(true);
var leftoverAttribute = pi.GetCustomAttribute<leftoverAttribute>(true);
var hasDefaultValue = pi.HasDefaultValue;
var isLeftover = leftoverAttribute != null;
var isParams = pi.GetCustomAttribute<ParamArrayAttribute>() is not null;

View File

@@ -1,5 +1,6 @@
#nullable disable
using System.Globalization;
using MessageType = NadekoBot.Extensions.MessageType;
// ReSharper disable InconsistentNaming
@@ -29,20 +30,15 @@ public abstract class NadekoModule : ModuleBase
protected string GetText(in LocStr data)
=> Strings.GetText(data, Culture);
public Task<IUserMessage> SendErrorAsync(string error)
=> ctx.Channel.SendErrorAsync(_eb, error);
public Task<IUserMessage> SendErrorAsync(
string title,
string error,
string url = null,
string footer = null)
string footer = null,
NadekoInteraction inter = null)
=> ctx.Channel.SendErrorAsync(_eb, title, error, url, footer);
public Task<IUserMessage> SendConfirmAsync(string text)
=> ctx.Channel.SendConfirmAsync(_eb, text);
public Task<IUserMessage> SendConfirmAsync(
string title,
string text,
@@ -50,25 +46,33 @@ public abstract class NadekoModule : ModuleBase
string footer = null)
=> ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer);
public Task<IUserMessage> SendPendingAsync(string text)
=> ctx.Channel.SendPendingAsync(_eb, text);
//
public Task<IUserMessage> SendErrorAsync(string text, NadekoInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Error, inter);
public Task<IUserMessage> SendConfirmAsync(string text, NadekoInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Ok, inter);
public Task<IUserMessage> SendPendingAsync(string text, NadekoInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Pending, inter);
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str)
=> SendErrorAsync(GetText(str));
// localized normal
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendErrorAsync(GetText(str), inter);
public Task<IUserMessage> PendingLocalizedAsync(LocStr str)
=> SendPendingAsync(GetText(str));
public Task<IUserMessage> PendingLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendPendingAsync(GetText(str), inter);
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str)
=> SendConfirmAsync(GetText(str));
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendConfirmAsync(GetText(str), inter);
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str)
// localized replies
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str)
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str)
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public async Task<bool> PromptUserConfirmAsync(IEmbedBuilder embed)

View File

@@ -34,61 +34,59 @@ public class Replacer
public SmartText Replace(SmartText data)
=> data switch
{
SmartEmbedText embedData => Replace(embedData),
SmartEmbedText embedData => Replace(embedData) with
{
PlainText = Replace(embedData.PlainText),
Color = embedData.Color
},
SmartPlainText plain => Replace(plain),
SmartEmbedTextArray arr => Replace(arr),
_ => throw new ArgumentOutOfRangeException(nameof(data), "Unsupported argument type")
};
public SmartPlainText Replace(SmartPlainText plainText)
=> Replace(plainText.Text);
public SmartEmbedText Replace(SmartEmbedText embedData)
{
var newEmbedData = new SmartEmbedText
private SmartEmbedTextArray Replace(SmartEmbedTextArray embedArr)
=> new()
{
Embeds = embedArr.Embeds.Map(e => Replace(e) with
{
Color = e.Color
}),
Content = Replace(embedArr.Content)
};
private SmartPlainText Replace(SmartPlainText plain)
=> Replace(plain.Text);
private T Replace<T>(T embedData) where T: SmartEmbedTextBase, new()
{
var newEmbedData = new T
{
PlainText = Replace(embedData.PlainText),
Description = Replace(embedData.Description),
Title = Replace(embedData.Title),
Thumbnail = Replace(embedData.Thumbnail),
Image = Replace(embedData.Image),
Url = Replace(embedData.Url)
};
if (embedData.Author is not null)
{
newEmbedData.Author = new()
{
Name = Replace(embedData.Author.Name),
IconUrl = Replace(embedData.Author.IconUrl)
};
}
if (embedData.Fields is not null)
{
var fields = new List<SmartTextEmbedField>();
foreach (var f in embedData.Fields)
{
var newF = new SmartTextEmbedField
Url = Replace(embedData.Url),
Author = embedData.Author is null
? null
: new()
{
Name = Replace(f.Name),
Value = Replace(f.Value),
Inline = f.Inline
};
fields.Add(newF);
}
newEmbedData.Fields = fields.ToArray();
}
if (embedData.Footer is not null)
{
newEmbedData.Footer = new()
Name = Replace(embedData.Author.Name),
IconUrl = Replace(embedData.Author.IconUrl)
},
Fields = embedData.Fields?.Map(f => new SmartTextEmbedField
{
Text = Replace(embedData.Footer.Text),
IconUrl = Replace(embedData.Footer.IconUrl)
};
}
newEmbedData.Color = embedData.Color;
Name = Replace(f.Name),
Value = Replace(f.Value),
Inline = f.Inline
}),
Footer = embedData.Footer is null
? null
: new()
{
Text = Replace(embedData.Footer.Text),
IconUrl = Replace(embedData.Footer.IconUrl)
}
};
return newEmbedData;
}

View File

@@ -1,20 +1,67 @@
#nullable disable
using SixLabors.ImageSharp.PixelFormats;
#nullable disable
namespace NadekoBot;
public sealed record SmartEmbedText : SmartText
public sealed record SmartEmbedArrayElementText : SmartEmbedTextBase
{
public string PlainText { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Url { get; set; }
public string Thumbnail { get; set; }
public string Image { get; set; }
public string Color { get; init; } = string.Empty;
public SmartTextEmbedAuthor Author { get; set; }
public SmartTextEmbedFooter Footer { get; set; }
public SmartTextEmbedField[] Fields { get; set; }
public SmartEmbedArrayElementText() : base()
{
}
public SmartEmbedArrayElementText(IEmbed eb) : base(eb)
{
}
public uint Color { get; set; } = 7458112;
protected override EmbedBuilder GetEmbedInternal()
{
var embed = base.GetEmbedInternal();
if (Rgba32.TryParseHex(Color, out var color))
return embed.WithColor(color.ToDiscordColor());
return embed;
}
}
public sealed record SmartEmbedText : SmartEmbedTextBase
{
public string PlainText { get; init; }
public uint Color { get; init; } = 7458112;
public SmartEmbedText()
{
}
private SmartEmbedText(IEmbed eb, string plainText = null)
: base(eb)
=> (PlainText, Color) = (plainText, eb.Color?.RawValue ?? 0);
public static SmartEmbedText FromEmbed(IEmbed eb, string plainText = null)
=> new(eb, plainText);
protected override EmbedBuilder GetEmbedInternal()
{
var embed = base.GetEmbedInternal();
return embed.WithColor(Color);
}
}
public abstract record SmartEmbedTextBase : SmartText
{
public string Title { get; init; }
public string Description { get; init; }
public string Url { get; init; }
public string Thumbnail { get; init; }
public string Image { get; init; }
public SmartTextEmbedAuthor Author { get; init; }
public SmartTextEmbedFooter Footer { get; init; }
public SmartTextEmbedField[] Fields { get; init; }
public bool IsValid
=> !string.IsNullOrWhiteSpace(Title)
@@ -26,36 +73,37 @@ public sealed record SmartEmbedText : SmartText
&& (!string.IsNullOrWhiteSpace(Footer.Text) || !string.IsNullOrWhiteSpace(Footer.IconUrl)))
|| Fields is { Length: > 0 };
public static SmartEmbedText FromEmbed(IEmbed eb, string plainText = null)
protected SmartEmbedTextBase()
{
var set = new SmartEmbedText
{
PlainText = plainText,
Title = eb.Title,
Description = eb.Description,
Url = eb.Url,
Thumbnail = eb.Thumbnail?.Url,
Image = eb.Image?.Url,
Author = eb.Author is { } ea
? new()
{
Name = ea.Name,
Url = ea.Url,
IconUrl = ea.IconUrl
}
: null,
Footer = eb.Footer is { } ef
? new()
{
Text = ef.Text,
IconUrl = ef.IconUrl
}
: null
};
}
protected SmartEmbedTextBase(IEmbed eb)
{
Title = eb.Title;
Description = eb.Description;
Url = eb.Url;
Thumbnail = eb.Thumbnail?.Url;
Image = eb.Image?.Url;
Author = eb.Author is { } ea
? new()
{
Name = ea.Name,
Url = ea.Url,
IconUrl = ea.IconUrl
}
: null;
Footer = eb.Footer is { } ef
? new()
{
Text = ef.Text,
IconUrl = ef.IconUrl
}
: null;
if (eb.Fields.Length > 0)
{
set.Fields = eb.Fields.Select(field
Fields = eb.Fields.Select(field
=> new SmartTextEmbedField
{
Inline = field.Inline,
@@ -64,14 +112,14 @@ public sealed record SmartEmbedText : SmartText
})
.ToArray();
}
set.Color = eb.Color?.RawValue ?? 0;
return set;
}
public EmbedBuilder GetEmbed()
=> GetEmbedInternal();
protected virtual EmbedBuilder GetEmbedInternal()
{
var embed = new EmbedBuilder().WithColor(Color);
var embed = new EmbedBuilder();
if (!string.IsNullOrWhiteSpace(Title))
embed.WithTitle(Title);

View File

@@ -0,0 +1,28 @@
#nullable disable
namespace NadekoBot;
public sealed record SmartEmbedTextArray : SmartText
{
public string Content { get; set; }
public SmartEmbedArrayElementText[] Embeds { get; set; }
public bool IsValid
=> Embeds?.All(x => x.IsValid) ?? false;
public EmbedBuilder[] GetEmbedBuilders()
{
if (Embeds is null)
return Array.Empty<EmbedBuilder>();
return Embeds.Map(em => em.GetEmbed());
}
public void NormalizeFields()
{
if (Embeds is null)
return;
foreach(var eb in Embeds)
eb.NormalizeFields();
}
}

View File

@@ -1,5 +1,6 @@
#nullable disable
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace NadekoBot;
@@ -11,6 +12,9 @@ public abstract record SmartText
public bool IsPlainText
=> this is SmartPlainText;
public bool IsEmbedArray
=> this is SmartEmbedTextArray;
public static SmartText operator +(SmartText text, string input)
=> text switch
{
@@ -19,6 +23,10 @@ public abstract record SmartText
PlainText = set.PlainText + input
},
SmartPlainText spt => new SmartPlainText(spt.Text + input),
SmartEmbedTextArray arr => arr with
{
Content = arr.Content + input
},
_ => throw new ArgumentOutOfRangeException(nameof(text))
};
@@ -30,27 +38,46 @@ public abstract record SmartText
PlainText = input + set.PlainText
},
SmartPlainText spt => new SmartPlainText(input + spt.Text),
SmartEmbedTextArray arr => arr with
{
Content = input + arr.Content
},
_ => throw new ArgumentOutOfRangeException(nameof(text))
};
[CanBeNull]
public static SmartText CreateFrom(string input)
{
if (string.IsNullOrWhiteSpace(input) || !input.TrimStart().StartsWith("{"))
if (string.IsNullOrWhiteSpace(input))
return new SmartPlainText(input);
try
{
var smartEmbedText = JsonConvert.DeserializeObject<SmartEmbedText>(input);
var doc = JObject.Parse(input);
var root = doc.Root;
if (root.Type == JTokenType.Object)
{
if (((JObject)root).TryGetValue("embeds", out _))
{
var arr = root.ToObject<SmartEmbedTextArray>();
if (smartEmbedText is null)
throw new FormatException();
if (arr is null)
return new SmartPlainText(input);
smartEmbedText.NormalizeFields();
arr!.NormalizeFields();
return arr;
}
if (!smartEmbedText.IsValid)
return new SmartPlainText(input);
var obj = root.ToObject<SmartEmbedText>();
return smartEmbedText;
if (obj is null)
return new SmartPlainText(input);
obj.NormalizeFields();
return obj;
}
return new SmartPlainText(input);
}
catch
{

View File

@@ -1,95 +0,0 @@
#nullable disable
namespace NadekoBot.Common;
public sealed class ReactionEventWrapper : IDisposable
{
public event Action<SocketReaction> OnReactionAdded = delegate { };
public event Action<SocketReaction> OnReactionRemoved = delegate { };
public event Action OnReactionsCleared = delegate { };
public IUserMessage Message { get; }
private readonly DiscordSocketClient _client;
private bool disposing;
public ReactionEventWrapper(DiscordSocketClient client, IUserMessage msg)
{
Message = msg ?? throw new ArgumentNullException(nameof(msg));
_client = client;
_client.ReactionAdded += Discord_ReactionAdded;
_client.ReactionRemoved += Discord_ReactionRemoved;
_client.ReactionsCleared += Discord_ReactionsCleared;
}
public void Dispose()
{
if (disposing)
return;
disposing = true;
UnsubAll();
}
private Task Discord_ReactionsCleared(Cacheable<IUserMessage, ulong> msg, Cacheable<IMessageChannel, ulong> channel)
{
Task.Run(() =>
{
try
{
if (msg.Id == Message.Id)
OnReactionsCleared?.Invoke();
}
catch { }
});
return Task.CompletedTask;
}
private Task Discord_ReactionRemoved(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> cacheable,
SocketReaction reaction)
{
Task.Run(() =>
{
try
{
if (msg.Id == Message.Id)
OnReactionRemoved?.Invoke(reaction);
}
catch { }
});
return Task.CompletedTask;
}
private Task Discord_ReactionAdded(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> cacheable,
SocketReaction reaction)
{
Task.Run(() =>
{
try
{
if (msg.Id == Message.Id)
OnReactionAdded?.Invoke(reaction);
}
catch
{
}
});
return Task.CompletedTask;
}
public void UnsubAll()
{
_client.ReactionAdded -= Discord_ReactionAdded;
_client.ReactionRemoved -= Discord_ReactionRemoved;
_client.ReactionsCleared -= Discord_ReactionsCleared;
OnReactionAdded = null;
OnReactionRemoved = null;
OnReactionsCleared = null;
}
}

View File

@@ -50,9 +50,7 @@ public static class GuildConfigExtensions
.Include(gc => gc.StreamRole)
.Include(gc => gc.XpSettings)
.ThenInclude(x => x.ExclusionList)
.Include(gc => gc.DelMsgOnCmdChannels)
.Include(gc => gc.ReactionRoleMessages)
.ThenInclude(x => x.ReactionRoles);
.Include(gc => gc.DelMsgOnCmdChannels);
public static IEnumerable<GuildConfig> GetAllGuildConfigs(
this DbSet<GuildConfig> configs,

View File

@@ -0,0 +1,9 @@
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Db.Models;
public class BankUser : DbEntity
{
public ulong UserId { get; set; }
public long Balance { get; set; }
}

View File

@@ -14,7 +14,7 @@ public class DiscordUser : DbEntity
public ClubInfo Club { get; set; }
public bool IsClubAdmin { get; set; }
public int TotalXp { get; set; }
public long TotalXp { get; set; }
public DateTime LastLevelUp { get; set; } = DateTime.UtcNow;
public DateTime LastXpGain { get; set; } = DateTime.MinValue;
public XpNotificationLocation NotifyOnLevelUp { get; set; }

View File

@@ -90,8 +90,8 @@ public class GuildConfig : DbEntity
public XpSettings XpSettings { get; set; }
public List<FeedSub> FeedSubs { get; set; } = new();
public IndexedCollection<ReactionRoleMessage> ReactionRoleMessages { get; set; } = new();
public bool NotifyStreamOffline { get; set; }
public bool DeleteStreamOnlineMessage { get; set; }
public List<GroupName> SelfAssignableRoleGroupNames { get; set; }
public int WarnExpireHours { get; set; }
public WarnExpireAction WarnExpireAction { get; set; } = WarnExpireAction.Clear;

View File

@@ -1,22 +1,18 @@
#nullable disable
using System.ComponentModel.DataAnnotations;
namespace NadekoBot.Services.Database.Models;
public class ReactionRoleMessage : DbEntity, IIndexed
public class ReactionRoleV2 : DbEntity
{
public int Index { get; set; }
public int GuildConfigId { get; set; }
public GuildConfig GuildConfig { get; set; }
public ulong GuildId { get; set; }
public ulong ChannelId { get; set; }
public ulong MessageId { get; set; }
public List<ReactionRole> ReactionRoles { get; set; }
public bool Exclusive { get; set; }
}
public class ReactionRole : DbEntity
{
public string EmoteName { get; set; }
[MaxLength(100)]
public string Emote { get; set; }
public ulong RoleId { get; set; }
public int Group { get; set; }
public int LevelReq { get; set; }
}

View File

@@ -5,8 +5,8 @@ public class UserXpStats : DbEntity
{
public ulong UserId { get; set; }
public ulong GuildId { get; set; }
public int Xp { get; set; }
public int AwardedXp { get; set; }
public long Xp { get; set; }
public long AwardedXp { get; set; }
public XpNotificationLocation NotifyOnLevelUp { get; set; }
public DateTime LastLevelUp { get; set; } = DateTime.UtcNow;
}

View File

@@ -51,6 +51,10 @@ public abstract class NadekoContext : DbContext
public DbSet<AutoTranslateUser> AutoTranslateUsers { get; set; }
public DbSet<Permissionv2> Permissions { get; set; }
public DbSet<BankUser> BankUsers { get; set; }
public DbSet<ReactionRoleV2> ReactionRoles { get; set; }
#region Mandatory Provider-Specific Values
@@ -359,11 +363,18 @@ public abstract class NadekoContext : DbContext
#region Reaction roles
modelBuilder.Entity<ReactionRoleMessage>(rrm => rrm
.HasMany(x => x.ReactionRoles)
.WithOne()
.OnDelete(DeleteBehavior.Cascade));
modelBuilder.Entity<ReactionRoleV2>(rr2 =>
{
rr2.HasIndex(x => x.GuildId)
.IsUnique(false);
rr2.HasIndex(x => new
{
x.MessageId,
x.Emote
}).IsUnique();
});
#endregion
#region LogSettings
@@ -402,6 +413,13 @@ public abstract class NadekoContext : DbContext
x.ChannelId,
x.UserId
}));
#region BANK
modelBuilder.Entity<BankUser>(bu => bu.HasIndex(x => x.UserId).IsUnique());
#endregion
}
#if DEBUG

View File

@@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace NadekoBot.Migrations;
public static class MigrationQueries
{
public static void MigrateRero(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"insert or ignore into reactionroles(guildid, channelid, messageid, emote, roleid, 'group', levelreq, dateadded)
select guildid, channelid, messageid, emotename, roleid, exclusive, 0, reactionrolemessage.dateadded
from reactionrole
left join reactionrolemessage on reactionrolemessage.id = reactionrole.reactionrolemessageid
left join guildconfigs on reactionrolemessage.guildconfigid = guildconfigs.id;");
}
}

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 stondel : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "deletestreamonlinemessage",
table: "guildconfigs",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "deletestreamonlinemessage",
table: "guildconfigs");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.Mysql
{
public partial class bank : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "bankusers",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
userid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
balance = table.Column<long>(type: "bigint", nullable: false),
dateadded = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_bankusers", x => x.id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "ix_bankusers_userid",
table: "bankusers",
column: "userid",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "bankusers");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,121 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.Mysql
{
public partial class newrero : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "reactionroles",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
guildid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
channelid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
messageid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
emote = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
roleid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
group = table.Column<int>(type: "int", nullable: false),
levelreq = table.Column<int>(type: "int", nullable: false),
dateadded = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_reactionroles", x => x.id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "ix_reactionroles_guildid",
table: "reactionroles",
column: "guildid");
migrationBuilder.CreateIndex(
name: "ix_reactionroles_messageid_emote",
table: "reactionroles",
columns: new[] { "messageid", "emote" },
unique: true);
MigrationQueries.MigrateRero(migrationBuilder);
migrationBuilder.DropTable(
name: "reactionrole");
migrationBuilder.DropTable(
name: "reactionrolemessage");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "reactionroles");
migrationBuilder.CreateTable(
name: "reactionrolemessage",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
guildconfigid = table.Column<int>(type: "int", nullable: false),
channelid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
dateadded = table.Column<DateTime>(type: "datetime(6)", nullable: true),
exclusive = table.Column<bool>(type: "tinyint(1)", nullable: false),
index = table.Column<int>(type: "int", nullable: false),
messageid = table.Column<ulong>(type: "bigint unsigned", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_reactionrolemessage", x => x.id);
table.ForeignKey(
name: "fk_reactionrolemessage_guildconfigs_guildconfigid",
column: x => x.guildconfigid,
principalTable: "guildconfigs",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "reactionrole",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
dateadded = table.Column<DateTime>(type: "datetime(6)", nullable: true),
emotename = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
reactionrolemessageid = table.Column<int>(type: "int", nullable: true),
roleid = table.Column<ulong>(type: "bigint unsigned", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_reactionrole", x => x.id);
table.ForeignKey(
name: "fk_reactionrole_reactionrolemessage_reactionrolemessageid",
column: x => x.reactionrolemessageid,
principalTable: "reactionrolemessage",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "ix_reactionrole_reactionrolemessageid",
table: "reactionrole",
column: "reactionrolemessageid");
migrationBuilder.CreateIndex(
name: "ix_reactionrolemessage_guildconfigid",
table: "reactionrolemessage",
column: "guildconfigid");
}
}
}

View File

@@ -16,9 +16,38 @@ namespace NadekoBot.Migrations.Mysql
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.3")
.HasAnnotation("ProductVersion", "6.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<long>("Balance")
.HasColumnType("bigint")
.HasColumnName("balance");
b.Property<DateTime?>("DateAdded")
.HasColumnType("datetime(6)")
.HasColumnName("dateadded");
b.Property<ulong>("UserId")
.HasColumnType("bigint unsigned")
.HasColumnName("userid");
b.HasKey("Id")
.HasName("pk_bankusers");
b.HasIndex("UserId")
.IsUnique()
.HasDatabaseName("ix_bankusers_userid");
b.ToTable("bankusers", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b =>
{
b.Property<int>("ClubId")
@@ -1032,6 +1061,10 @@ namespace NadekoBot.Migrations.Mysql
.HasColumnType("tinyint(1)")
.HasColumnName("deletemessageoncommand");
b.Property<bool>("DeleteStreamOnlineMessage")
.HasColumnType("tinyint(1)")
.HasColumnName("deletestreamonlinemessage");
b.Property<string>("DmGreetMessageText")
.HasColumnType("longtext")
.HasColumnName("dmgreetmessagetext");
@@ -1780,39 +1813,7 @@ namespace NadekoBot.Migrations.Mysql
b.ToTable("quotes", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<DateTime?>("DateAdded")
.HasColumnType("datetime(6)")
.HasColumnName("dateadded");
b.Property<string>("EmoteName")
.HasColumnType("longtext")
.HasColumnName("emotename");
b.Property<int?>("ReactionRoleMessageId")
.HasColumnType("int")
.HasColumnName("reactionrolemessageid");
b.Property<ulong>("RoleId")
.HasColumnType("bigint unsigned")
.HasColumnName("roleid");
b.HasKey("Id")
.HasName("pk_reactionrole");
b.HasIndex("ReactionRoleMessageId")
.HasDatabaseName("ix_reactionrole_reactionrolemessageid");
b.ToTable("reactionrole", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleV2", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1827,29 +1828,42 @@ namespace NadekoBot.Migrations.Mysql
.HasColumnType("datetime(6)")
.HasColumnName("dateadded");
b.Property<bool>("Exclusive")
.HasColumnType("tinyint(1)")
.HasColumnName("exclusive");
b.Property<string>("Emote")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("emote");
b.Property<int>("GuildConfigId")
b.Property<int>("Group")
.HasColumnType("int")
.HasColumnName("guildconfigid");
.HasColumnName("group");
b.Property<int>("Index")
b.Property<ulong>("GuildId")
.HasColumnType("bigint unsigned")
.HasColumnName("guildid");
b.Property<int>("LevelReq")
.HasColumnType("int")
.HasColumnName("index");
.HasColumnName("levelreq");
b.Property<ulong>("MessageId")
.HasColumnType("bigint unsigned")
.HasColumnName("messageid");
b.Property<ulong>("RoleId")
.HasColumnType("bigint unsigned")
.HasColumnName("roleid");
b.HasKey("Id")
.HasName("pk_reactionrolemessage");
.HasName("pk_reactionroles");
b.HasIndex("GuildConfigId")
.HasDatabaseName("ix_reactionrolemessage_guildconfigid");
b.HasIndex("GuildId")
.HasDatabaseName("ix_reactionroles_guildid");
b.ToTable("reactionrolemessage", (string)null);
b.HasIndex("MessageId", "Emote")
.IsUnique()
.HasDatabaseName("ix_reactionroles_messageid_emote");
b.ToTable("reactionroles", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b =>
@@ -3075,27 +3089,6 @@ namespace NadekoBot.Migrations.Mysql
.HasConstraintName("fk_pollvote_poll_pollid");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.ReactionRoleMessage", null)
.WithMany("ReactionRoles")
.HasForeignKey("ReactionRoleMessageId")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_reactionrole_reactionrolemessage_reactionrolemessageid");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig")
.WithMany("ReactionRoleMessages")
.HasForeignKey("GuildConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_reactionrolemessage_guildconfigs_guildconfigid");
b.Navigation("GuildConfig");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
@@ -3345,8 +3338,6 @@ namespace NadekoBot.Migrations.Mysql
b.Navigation("Permissions");
b.Navigation("ReactionRoleMessages");
b.Navigation("SelfAssignableRoleGroupNames");
b.Navigation("ShopEntries");
@@ -3387,11 +3378,6 @@ namespace NadekoBot.Migrations.Mysql
b.Navigation("Votes");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
{
b.Navigation("ReactionRoles");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
{
b.Navigation("Items");

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 stondel : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "deletestreamonlinemessage",
table: "guildconfigs",
type: "boolean",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "deletestreamonlinemessage",
table: "guildconfigs");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
public partial class bank : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "bankusers",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
balance = table.Column<long>(type: "bigint", nullable: false),
dateadded = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_bankusers", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_bankusers_userid",
table: "bankusers",
column: "userid",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "bankusers");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,115 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
public partial class newrero : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "reactionroles",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
messageid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
emote = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
group = table.Column<int>(type: "integer", nullable: false),
levelreq = table.Column<int>(type: "integer", nullable: false),
dateadded = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_reactionroles", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_reactionroles_guildid",
table: "reactionroles",
column: "guildid");
migrationBuilder.CreateIndex(
name: "ix_reactionroles_messageid_emote",
table: "reactionroles",
columns: new[] { "messageid", "emote" },
unique: true);
MigrationQueries.MigrateRero(migrationBuilder);
migrationBuilder.DropTable(
name: "reactionrole");
migrationBuilder.DropTable(
name: "reactionrolemessage");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "reactionroles");
migrationBuilder.CreateTable(
name: "reactionrolemessage",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildconfigid = table.Column<int>(type: "integer", nullable: false),
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
dateadded = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
exclusive = table.Column<bool>(type: "boolean", nullable: false),
index = table.Column<int>(type: "integer", nullable: false),
messageid = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_reactionrolemessage", x => x.id);
table.ForeignKey(
name: "fk_reactionrolemessage_guildconfigs_guildconfigid",
column: x => x.guildconfigid,
principalTable: "guildconfigs",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "reactionrole",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
dateadded = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
emotename = table.Column<string>(type: "text", nullable: true),
reactionrolemessageid = table.Column<int>(type: "integer", nullable: true),
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_reactionrole", x => x.id);
table.ForeignKey(
name: "fk_reactionrole_reactionrolemessage_reactionrolemessageid",
column: x => x.reactionrolemessageid,
principalTable: "reactionrolemessage",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_reactionrole_reactionrolemessageid",
table: "reactionrole",
column: "reactionrolemessageid");
migrationBuilder.CreateIndex(
name: "ix_reactionrolemessage_guildconfigid",
table: "reactionrolemessage",
column: "guildconfigid");
}
}
}

View File

@@ -17,11 +17,42 @@ namespace NadekoBot.Migrations.PostgreSql
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.3")
.HasAnnotation("ProductVersion", "6.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<long>("Balance")
.HasColumnType("bigint")
.HasColumnName("balance");
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp with time zone")
.HasColumnName("dateadded");
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.HasKey("Id")
.HasName("pk_bankusers");
b.HasIndex("UserId")
.IsUnique()
.HasDatabaseName("ix_bankusers_userid");
b.ToTable("bankusers", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b =>
{
b.Property<int>("ClubId")
@@ -1086,6 +1117,10 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("boolean")
.HasColumnName("deletemessageoncommand");
b.Property<bool>("DeleteStreamOnlineMessage")
.HasColumnType("boolean")
.HasColumnName("deletestreamonlinemessage");
b.Property<string>("DmGreetMessageText")
.HasColumnType("text")
.HasColumnName("dmgreetmessagetext");
@@ -1866,41 +1901,7 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("quotes", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp with time zone")
.HasColumnName("dateadded");
b.Property<string>("EmoteName")
.HasColumnType("text")
.HasColumnName("emotename");
b.Property<int?>("ReactionRoleMessageId")
.HasColumnType("integer")
.HasColumnName("reactionrolemessageid");
b.Property<decimal>("RoleId")
.HasColumnType("numeric(20,0)")
.HasColumnName("roleid");
b.HasKey("Id")
.HasName("pk_reactionrole");
b.HasIndex("ReactionRoleMessageId")
.HasDatabaseName("ix_reactionrole_reactionrolemessageid");
b.ToTable("reactionrole", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleV2", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1917,29 +1918,42 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("timestamp with time zone")
.HasColumnName("dateadded");
b.Property<bool>("Exclusive")
.HasColumnType("boolean")
.HasColumnName("exclusive");
b.Property<string>("Emote")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("emote");
b.Property<int>("GuildConfigId")
b.Property<int>("Group")
.HasColumnType("integer")
.HasColumnName("guildconfigid");
.HasColumnName("group");
b.Property<int>("Index")
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<int>("LevelReq")
.HasColumnType("integer")
.HasColumnName("index");
.HasColumnName("levelreq");
b.Property<decimal>("MessageId")
.HasColumnType("numeric(20,0)")
.HasColumnName("messageid");
b.Property<decimal>("RoleId")
.HasColumnType("numeric(20,0)")
.HasColumnName("roleid");
b.HasKey("Id")
.HasName("pk_reactionrolemessage");
.HasName("pk_reactionroles");
b.HasIndex("GuildConfigId")
.HasDatabaseName("ix_reactionrolemessage_guildconfigid");
b.HasIndex("GuildId")
.HasDatabaseName("ix_reactionroles_guildid");
b.ToTable("reactionrolemessage", (string)null);
b.HasIndex("MessageId", "Emote")
.IsUnique()
.HasDatabaseName("ix_reactionroles_messageid_emote");
b.ToTable("reactionroles", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b =>
@@ -3215,27 +3229,6 @@ namespace NadekoBot.Migrations.PostgreSql
.HasConstraintName("fk_pollvote_poll_pollid");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.ReactionRoleMessage", null)
.WithMany("ReactionRoles")
.HasForeignKey("ReactionRoleMessageId")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_reactionrole_reactionrolemessage_reactionrolemessageid");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig")
.WithMany("ReactionRoleMessages")
.HasForeignKey("GuildConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_reactionrolemessage_guildconfigs_guildconfigid");
b.Navigation("GuildConfig");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
@@ -3485,8 +3478,6 @@ namespace NadekoBot.Migrations.PostgreSql
b.Navigation("Permissions");
b.Navigation("ReactionRoleMessages");
b.Navigation("SelfAssignableRoleGroupNames");
b.Navigation("ShopEntries");
@@ -3527,11 +3518,6 @@ namespace NadekoBot.Migrations.PostgreSql
b.Navigation("Votes");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
{
b.Navigation("ReactionRoles");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
{
b.Navigation("Items");

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 stondel : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "DeleteStreamOnlineMessage",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DeleteStreamOnlineMessage",
table: "GuildConfigs");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
public partial class bank : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "BankUsers",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
Balance = table.Column<long>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_BankUsers", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_BankUsers_UserId",
table: "BankUsers",
column: "UserId",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BankUsers");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,114 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
public partial class newrero : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ReactionRoles",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
MessageId = table.Column<ulong>(type: "INTEGER", nullable: false),
Emote = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false),
Group = table.Column<int>(type: "INTEGER", nullable: false),
LevelReq = table.Column<int>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ReactionRoles", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ReactionRoles_GuildId",
table: "ReactionRoles",
column: "GuildId");
migrationBuilder.CreateIndex(
name: "IX_ReactionRoles_MessageId_Emote",
table: "ReactionRoles",
columns: new[] { "MessageId", "Emote" },
unique: true);
MigrationQueries.MigrateRero(migrationBuilder);
migrationBuilder.DropTable(
name: "ReactionRole");
migrationBuilder.DropTable(
name: "ReactionRoleMessage");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ReactionRoles");
migrationBuilder.CreateTable(
name: "ReactionRoleMessage",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildConfigId = table.Column<int>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
Exclusive = table.Column<bool>(type: "INTEGER", nullable: false),
Index = table.Column<int>(type: "INTEGER", nullable: false),
MessageId = table.Column<ulong>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ReactionRoleMessage", x => x.Id);
table.ForeignKey(
name: "FK_ReactionRoleMessage_GuildConfigs_GuildConfigId",
column: x => x.GuildConfigId,
principalTable: "GuildConfigs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ReactionRole",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
EmoteName = table.Column<string>(type: "TEXT", nullable: true),
ReactionRoleMessageId = table.Column<int>(type: "INTEGER", nullable: true),
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ReactionRole", x => x.Id);
table.ForeignKey(
name: "FK_ReactionRole_ReactionRoleMessage_ReactionRoleMessageId",
column: x => x.ReactionRoleMessageId,
principalTable: "ReactionRoleMessage",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ReactionRole_ReactionRoleMessageId",
table: "ReactionRole",
column: "ReactionRoleMessageId");
migrationBuilder.CreateIndex(
name: "IX_ReactionRoleMessage_GuildConfigId",
table: "ReactionRoleMessage",
column: "GuildConfigId");
}
}
}

View File

@@ -17,6 +17,29 @@ namespace NadekoBot.Migrations
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.3");
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<long>("Balance")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("BankUsers");
});
modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b =>
{
b.Property<int>("ClubId")
@@ -809,6 +832,9 @@ namespace NadekoBot.Migrations
b.Property<bool>("DeleteMessageOnCommand")
.HasColumnType("INTEGER");
b.Property<bool>("DeleteStreamOnlineMessage")
.HasColumnType("INTEGER");
b.Property<string>("DmGreetMessageText")
.HasColumnType("TEXT");
@@ -1389,32 +1415,7 @@ namespace NadekoBot.Migrations
b.ToTable("Quotes");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<string>("EmoteName")
.HasColumnType("TEXT");
b.Property<int?>("ReactionRoleMessageId")
.HasColumnType("INTEGER");
b.Property<ulong>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ReactionRoleMessageId");
b.ToTable("ReactionRole");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleV2", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1426,23 +1427,33 @@ namespace NadekoBot.Migrations
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<bool>("Exclusive")
b.Property<string>("Emote")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int>("Group")
.HasColumnType("INTEGER");
b.Property<int>("GuildConfigId")
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<int>("Index")
b.Property<int>("LevelReq")
.HasColumnType("INTEGER");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<ulong>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GuildConfigId");
b.HasIndex("GuildId");
b.ToTable("ReactionRoleMessage");
b.HasIndex("MessageId", "Emote")
.IsUnique();
b.ToTable("ReactionRoles");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b =>
@@ -2430,25 +2441,6 @@ namespace NadekoBot.Migrations
.HasForeignKey("PollId");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.ReactionRoleMessage", null)
.WithMany("ReactionRoles")
.HasForeignKey("ReactionRoleMessageId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig")
.WithMany("ReactionRoleMessages")
.HasForeignKey("GuildConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("GuildConfig");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
@@ -2676,8 +2668,6 @@ namespace NadekoBot.Migrations
b.Navigation("Permissions");
b.Navigation("ReactionRoleMessages");
b.Navigation("SelfAssignableRoleGroupNames");
b.Navigation("ShopEntries");
@@ -2718,11 +2708,6 @@ namespace NadekoBot.Migrations
b.Navigation("Votes");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
{
b.Navigation("ReactionRoles");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
{
b.Navigation("Items");

View File

@@ -19,7 +19,7 @@ public class DangerousCommandsService : INService
await using var ctx = _db.GetDbContext();
await ctx.DiscordUser.UpdateAsync(_ => new DiscordUser()
{
Club = null,
ClubId = null,
// IsClubAdmin = false,
TotalXp = 0
});

View File

@@ -256,15 +256,54 @@ public class GreetService : INService, IReadyExecutor
{
text = new SmartEmbedText()
{
PlainText = pt.Text
Description = pt.Text
};
}
((SmartEmbedText)text).Footer = new()
else if (text is SmartEmbedText set)
{
Text = $"This message was sent from {user.Guild} server.",
IconUrl = user.Guild.IconUrl
};
text = set with
{
Footer = CreateFooterSource(user)
};
}
else if (text is SmartEmbedTextArray seta)
{
// if the greet dm message is a text array
var ebElem = seta.Embeds.LastOrDefault();
if (ebElem is null)
{
// if there are no embeds, add an embed with the footer
text = seta with
{
Embeds = new[]
{
new SmartEmbedArrayElementText()
{
Footer = CreateFooterSource(user)
}
}
};
}
else
{
// if the maximum amount of embeds is reached, edit the last embed
if (seta.Embeds.Length >= 10)
{
seta.Embeds[^1] = seta.Embeds[^1] with
{
Footer = CreateFooterSource(user)
};
}
else
{
// if there is less than 10 embeds, add an embed with footer only
seta.Embeds = seta.Embeds.Append(new SmartEmbedArrayElementText()
{
Footer = CreateFooterSource(user)
}).ToArray();
}
}
}
await user.SendAsync(text);
}
@@ -276,6 +315,13 @@ public class GreetService : INService, IReadyExecutor
return true;
}
private static SmartTextEmbedFooter CreateFooterSource(IGuildUser user)
=> new()
{
Text = $"This message was sent from {user.Guild} server.",
IconUrl = user.Guild.IconUrl
};
private Task OnUserJoined(IGuildUser user)
{
_ = Task.Run(async () =>

View File

@@ -0,0 +1,52 @@
#nullable disable
using NadekoBot.Services.Database.Models;
using System.Collections;
namespace NadekoBot.Modules.Administration.Services;
public interface IReactionRoleService
{
/// <summary>
/// Adds a single reaction role
/// </summary>
/// <param name="guildId"></param>
/// <param name="msg"></param>
/// <param name="channel"></param>
/// <param name="emote"></param>
/// <param name="role"></param>
/// <param name="group"></param>
/// <param name="levelReq"></param>
/// <returns></returns>
Task<bool> AddReactionRole(
ulong guildId,
IMessage msg,
ITextChannel channel,
string emote,
IRole role,
int group = 0,
int levelReq = 0);
/// <summary>
/// Get all reaction roles on the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
Task<IReadOnlyCollection<ReactionRoleV2>> GetReactionRolesAsync(ulong guildId);
/// <summary>
/// Remove reaction roles on the specified message
/// </summary>
/// <param name="guildId"></param>
/// <param name="messageId"></param>
/// <returns></returns>
Task<bool> RemoveReactionRoles(ulong guildId, ulong messageId);
/// <summary>
/// Remove all reaction roles in the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
Task<int> RemoveAllReactionRoles(ulong guildId);
Task<IReadOnlyCollection<IEmote>> TransferReactionRolesAsync(ulong guildId, ulong fromMessageId, ulong toMessageId);
}

View File

@@ -0,0 +1,166 @@
using NadekoBot.Modules.Administration.Services;
namespace NadekoBot.Modules.Administration;
public partial class Administration
{
public partial class ReactionRoleCommands : NadekoModule
{
private readonly IReactionRoleService _rero;
public ReactionRoleCommands(IReactionRoleService rero)
{
_rero = rero;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRoleAdd(
ulong messageId,
string emoteStr,
IRole role,
int group = 0,
int levelReq = 0)
{
if (group < 0)
return;
if (levelReq < 0)
return;
var msg = await ctx.Channel.GetMessageAsync(messageId);
if (msg is null)
{
await ReplyErrorLocalizedAsync(strs.not_found);
return;
}
if (ctx.User.Id != ctx.Guild.OwnerId && ((IGuildUser)ctx.User).GetRoles().Max(x => x.Position) <= role.Position)
{
await ReplyErrorLocalizedAsync(strs.hierarchy);
return;
}
var emote = emoteStr.ToIEmote();
await msg.AddReactionAsync(emote);
var succ = await _rero.AddReactionRole(ctx.Guild.Id,
msg,
(ITextChannel)ctx.Channel,
emoteStr,
role,
group,
levelReq);
if (succ)
{
await ctx.OkAsync();
}
else
{
await ctx.ErrorAsync();
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesList()
{
var reros = await _rero.GetReactionRolesAsync(ctx.Guild.Id);
var embed = _eb.Create(ctx)
.WithOkColor();
var content = string.Empty;
foreach (var g in reros.GroupBy(x => x.MessageId).OrderBy(x => x.Key))
{
var messageId = g.Key;
content +=
$"[{messageId}](https://discord.com/channels/{ctx.Guild.Id}/{g.First().ChannelId}/{g.Key})\n";
var groupGroups = g.GroupBy(x => x.Group);
foreach (var ggs in groupGroups)
{
content += $"`< {(g.Key == 0 ? ("Not Exclusive (Group 0)") : ($"Group {ggs.Key}"))} >`\n";
foreach (var rero in ggs)
{
content += $"\t{rero.Emote} -> {(ctx.Guild.GetRole(rero.RoleId)?.Mention ?? "<missing role>")}";
if (rero.LevelReq > 0)
content += $" (lvl {rero.LevelReq}+)";
content += '\n';
}
}
}
embed.WithDescription(string.IsNullOrWhiteSpace(content)
? "There are no reaction roles on this server"
: content);
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesRemove(ulong messageId)
{
var succ = await _rero.RemoveReactionRoles(ctx.Guild.Id, messageId);
if (succ)
await ctx.OkAsync();
else
await ctx.ErrorAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesDeleteAll()
{
await _rero.RemoveAllReactionRoles(ctx.Guild.Id);
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Ratelimit(60)]
public async partial Task ReactionRolesTransfer(ulong fromMessageId, ulong toMessageId)
{
var msg = await ctx.Channel.GetMessageAsync(toMessageId);
if (msg is null)
{
await ctx.ErrorAsync();
return;
}
var reactions = await _rero.TransferReactionRolesAsync(ctx.Guild.Id, fromMessageId, toMessageId);
if (reactions.Count == 0)
{
await ctx.ErrorAsync();
}
else
{
foreach (var r in reactions)
{
await msg.AddReactionAsync(r);
}
}
}
}
}

View File

@@ -0,0 +1,356 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Modules.Xp.Extensions;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Administration.Services;
public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionRoleService
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
private readonly object _cacheLock = new();
private readonly SemaphoreSlim _assignementLock = new(1, 1);
public ReactionRolesService(DiscordSocketClient client, DbService db, IBotCredentials creds)
{
_db = db;
_client = client;
_creds = creds;
_cache = new();
}
public async Task OnReadyAsync()
{
await using var uow = _db.GetDbContext();
var reros = await uow.GetTable<ReactionRoleV2>()
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId, _creds.TotalShards, _client.ShardId))
.ToListAsyncLinqToDB();
foreach (var group in reros.GroupBy(x => x.MessageId))
{
_cache[group.Key] = group.ToList();
}
_client.ReactionAdded += ClientOnReactionAdded;
_client.ReactionRemoved += ClientOnReactionRemoved;
}
private async Task<(IGuildUser, IRole)> GetUserAndRoleAsync(
SocketReaction r,
ReactionRoleV2 rero)
{
var guild = _client.GetGuild(rero.GuildId);
var role = guild?.GetRole(rero.RoleId);
if (role is null)
return default;
var user = guild.GetUser(r.UserId) as IGuildUser
?? await _client.Rest.GetGuildUserAsync(guild.Id, r.UserId);
if (user is null)
return default;
return (user, role);
}
private Task ClientOnReactionRemoved(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> ch,
SocketReaction r)
{
if (!_cache.TryGetValue(msg.Id, out var reros))
return Task.CompletedTask;
_ = Task.Run(async () =>
{
var rero = reros.FirstOrDefault(x => x.Emote == r.Emote.Name || x.Emote == r.Emote.ToString());
if (rero is null)
return;
var (user, role) = await GetUserAndRoleAsync(r, rero);
if (user.IsBot)
return;
await _assignementLock.WaitAsync();
try
{
if (user.RoleIds.Contains(role.Id))
{
await user.RemoveRoleAsync(role.Id);
}
}
finally
{
_assignementLock.Release();
}
});
return Task.CompletedTask;
}
private Task ClientOnReactionAdded(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> ch,
SocketReaction r)
{
if (!_cache.TryGetValue(msg.Id, out var reros))
return Task.CompletedTask;
_ = Task.Run(async () =>
{
var rero = reros.FirstOrDefault(x => x.Emote == r.Emote.Name || x.Emote == r.Emote.ToString());
if (rero is null)
return;
var (user, role) = await GetUserAndRoleAsync(r, rero);
if (user.IsBot)
return;
await _assignementLock.WaitAsync();
try
{
if (!user.RoleIds.Contains(role.Id))
{
// first check if there is a level requirement
// and if there is, make sure user satisfies it
if (rero.LevelReq > 0)
{
await using var ctx = _db.GetDbContext();
var levelData = await ctx.GetTable<UserXpStats>()
.GetLevelDataFor(user.GuildId, user.Id);
if (levelData.Level < rero.LevelReq)
return;
}
// remove all other roles from the same group from the user
// execept in group 0, which is a special, non-exclusive group
if (rero.Group != 0)
{
var exclusive = reros
.Where(x => x.Group == rero.Group && x.RoleId != role.Id)
.Select(x => x.RoleId)
.Distinct();
try { await user.RemoveRolesAsync(exclusive); }
catch { }
// remove user's previous reaction
try
{
var m = await msg.GetOrDownloadAsync();
if (m is not null)
{
var reactToRemove = m.Reactions
.FirstOrDefault(x => x.Key.ToString() != r.Emote.ToString())
.Key;
if (reactToRemove is not null)
{
await m.RemoveReactionAsync(reactToRemove, user);
}
}
}
catch
{
}
}
await user.AddRoleAsync(role.Id);
}
}
finally
{
_assignementLock.Release();
}
});
return Task.CompletedTask;
}
/// <summary>
/// Adds a single reaction role
/// </summary>
/// <param name="guildId"></param>
/// <param name="msg"></param>
/// <param name="channel"></param>
/// <param name="emote"></param>
/// <param name="role"></param>
/// <param name="group"></param>
/// <param name="levelReq"></param>
/// <returns></returns>
public async Task<bool> AddReactionRole(
ulong guildId,
IMessage msg,
ITextChannel channel,
string emote,
IRole role,
int group = 0,
int levelReq = 0)
{
if (group < 0)
throw new ArgumentOutOfRangeException(nameof(group));
if (levelReq < 0)
throw new ArgumentOutOfRangeException(nameof(group));
await using var ctx = _db.GetDbContext();
var activeReactionRoles = await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId)
.CountAsync();
if (activeReactionRoles >= 50)
return false;
var changed = await ctx.GetTable<ReactionRoleV2>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
ChannelId = channel.Id,
MessageId = msg.Id,
Emote = emote,
RoleId = role.Id,
Group = group,
LevelReq = levelReq
},
(old) => new()
{
RoleId = role.Id,
Group = group,
LevelReq = levelReq
},
() => new()
{
MessageId = msg.Id,
Emote = emote,
});
if (changed == 0)
return false;
var obj = new ReactionRoleV2()
{
GuildId = guildId,
MessageId = msg.Id,
Emote = emote,
RoleId = role.Id,
Group = group,
LevelReq = levelReq
};
lock (_cacheLock)
{
_cache.AddOrUpdate(msg.Id,
_ => new()
{
obj
},
(_, list) =>
{
list.RemoveAll(x => x.Emote == emote);
list.Add(obj);
return list;
});
}
return true;
}
/// <summary>
/// Get all reaction roles on the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
public async Task<IReadOnlyCollection<ReactionRoleV2>> GetReactionRolesAsync(ulong guildId)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId)
.ToListAsync();
}
/// <summary>
/// Remove reaction roles on the specified message
/// </summary>
/// <param name="guildId"></param>
/// <param name="messageId"></param>
/// <returns></returns>
public async Task<bool> RemoveReactionRoles(ulong guildId, ulong messageId)
{
// guildid is used for quick index lookup
await using var ctx = _db.GetDbContext();
var changed = await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId && x.MessageId == messageId)
.DeleteAsync();
_cache.TryRemove(messageId, out _);
if (changed == 0)
return false;
return true;
}
/// <summary>
/// Remove all reaction roles in the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
public async Task<int> RemoveAllReactionRoles(ulong guildId)
{
await using var ctx = _db.GetDbContext();
var output = await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId)
.DeleteWithOutputAsync(x => x.MessageId);
lock (_cacheLock)
{
foreach (var o in output)
{
_cache.TryRemove(o, out _);
}
}
return output.Length;
}
public async Task<IReadOnlyCollection<IEmote>> TransferReactionRolesAsync(ulong guildId, ulong fromMessageId, ulong toMessageId)
{
await using var ctx = _db.GetDbContext();
var updated = ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId && x.MessageId == fromMessageId)
.UpdateWithOutput(old => new()
{
MessageId = toMessageId
},
(old, neu) => neu);
lock (_cacheLock)
{
if (_cache.TryRemove(fromMessageId, out var data))
{
if (_cache.TryGetValue(toMessageId, out var newData))
{
newData.AddRange(data);
}
else
{
_cache[toMessageId] = data;
}
}
}
return updated.Select(x => x.Emote.ToIEmote()).ToList();
}
}

View File

@@ -7,172 +7,18 @@ using Color = SixLabors.ImageSharp.Color;
namespace NadekoBot.Modules.Administration;
public partial class Administration
{
public partial class RoleCommands : NadekoModule<RoleCommandsService>
public partial class RoleCommands : NadekoModule
{
public enum Exclude { Excl }
private readonly IServiceProvider _services;
public RoleCommands(IServiceProvider services)
=> _services = services;
public async Task InternalReactionRoles(bool exclusive, ulong? messageId, params string[] input)
{
var target = messageId is { } msgId
? await ctx.Channel.GetMessageAsync(msgId)
: (await ctx.Channel.GetMessagesAsync(2).FlattenAsync()).Skip(1).FirstOrDefault();
if (input.Length % 2 != 0 || target is null)
return;
var all = await input.Chunk(2)
.Select(async x =>
{
var inputRoleStr = x.First();
var roleReader = new RoleTypeReader<SocketRole>();
var roleResult = await roleReader.ReadAsync(ctx, inputRoleStr, _services);
if (!roleResult.IsSuccess)
{
Log.Warning("Role {Role} not found", inputRoleStr);
return null;
}
var role = (IRole)roleResult.BestMatch;
if (role.Position
> ((IGuildUser)ctx.User).GetRoles()
.Select(r => r.Position)
.Max()
&& ctx.User.Id != ctx.Guild.OwnerId)
return null;
var emote = x.Last().ToIEmote();
return new
{
role,
emote
};
})
.Where(x => x is not null)
.WhenAll();
if (!all.Any())
return;
foreach (var x in all)
{
try
{
await target.AddReactionAsync(x.emote,
new()
{
RetryMode = RetryMode.Retry502 | RetryMode.RetryRatelimit
});
}
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.BadRequest)
{
await ReplyErrorLocalizedAsync(strs.reaction_cant_access(Format.Code(x.emote.ToString())));
return;
}
await Task.Delay(500);
}
if (_service.Add(ctx.Guild.Id,
new()
{
Exclusive = exclusive,
MessageId = target.Id,
ChannelId = target.Channel.Id,
ReactionRoles = all.Select(x =>
{
return new ReactionRole
{
EmoteName = x.emote.ToString(),
RoleId = x.role.Id
};
})
.ToList()
}))
await ctx.OkAsync();
else
await ReplyErrorLocalizedAsync(strs.reaction_roles_full);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public partial Task ReactionRoles(ulong messageId, params string[] input)
=> InternalReactionRoles(false, messageId, input);
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(1)]
public partial Task ReactionRoles(ulong messageId, Exclude _, params string[] input)
=> InternalReactionRoles(true, messageId, input);
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public partial Task ReactionRoles(params string[] input)
=> InternalReactionRoles(false, null, input);
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(1)]
public partial Task ReactionRoles(Exclude _, params string[] input)
=> InternalReactionRoles(true, null, input);
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesList()
{
var embed = _eb.Create().WithOkColor();
if (!_service.Get(ctx.Guild.Id, out var rrs) || !rrs.Any())
embed.WithDescription(GetText(strs.no_reaction_roles));
else
{
var g = (SocketGuild)ctx.Guild;
foreach (var rr in rrs)
{
var ch = g.GetTextChannel(rr.ChannelId);
IUserMessage msg = null;
if (ch is not null)
msg = await ch.GetMessageAsync(rr.MessageId) as IUserMessage;
var content = msg?.Content.TrimTo(30) ?? "DELETED!";
embed.AddField($"**{rr.Index + 1}.** {ch?.Name ?? "DELETED!"}",
GetText(strs.reaction_roles_message(rr.ReactionRoles?.Count ?? 0, content)));
}
}
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesRemove(int index)
{
if (index < 1 || !_service.Get(ctx.Guild.Id, out var rrs) || !rrs.Any() || rrs.Count < index)
return;
index--;
_service.Remove(ctx.Guild.Id, index);
await ReplyConfirmLocalizedAsync(strs.reaction_role_removed(index + 1));
_services = services;
}
[Cmd]

View File

@@ -1,253 +0,0 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.Collections;
using NadekoBot.Db;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Administration.Services;
public class RoleCommandsService : INService
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly ConcurrentDictionary<ulong, IndexedCollection<ReactionRoleMessage>> _models;
/// <summary>
/// Contains the (Message ID, User ID) of reaction roles that are currently being processed.
/// </summary>
private readonly ConcurrentHashSet<(ulong, ulong)> _reacting = new();
public RoleCommandsService(DiscordSocketClient client, DbService db, Bot bot)
{
_db = db;
_client = client;
#if !GLOBAL_NADEKO
_models = bot.AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.ReactionRoleMessages).ToConcurrent();
_client.ReactionAdded += _client_ReactionAdded;
_client.ReactionRemoved += _client_ReactionRemoved;
#endif
}
private Task _client_ReactionAdded(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> chan,
SocketReaction reaction)
{
_ = Task.Run(async () =>
{
if (!reaction.User.IsSpecified
|| reaction.User.Value.IsBot
|| reaction.User.Value is not SocketGuildUser gusr
|| chan.Value is not SocketGuildChannel gch
|| !_models.TryGetValue(gch.Guild.Id, out var confs))
return;
var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
if (conf is null)
return;
// compare emote names for backwards compatibility :facepalm:
var reactionRole = conf.ReactionRoles.FirstOrDefault(x
=> x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
if (reactionRole is not null)
{
if (!conf.Exclusive)
{
await AddReactionRoleAsync(gusr, reactionRole);
return;
}
// If same (message, user) are being processed in an exclusive rero, quit
if (!_reacting.Add((msg.Id, reaction.UserId)))
return;
try
{
var removeExclusiveTask = RemoveExclusiveReactionRoleAsync(msg,
gusr,
reaction,
conf,
reactionRole,
CancellationToken.None);
var addRoleTask = AddReactionRoleAsync(gusr, reactionRole);
await Task.WhenAll(removeExclusiveTask, addRoleTask);
}
finally
{
// Free (message/user) for another exclusive rero
_reacting.TryRemove((msg.Id, reaction.UserId));
}
}
else
{
var dl = await msg.GetOrDownloadAsync();
await dl.RemoveReactionAsync(reaction.Emote,
dl.Author,
new()
{
RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502
});
Log.Warning("User {Author} is adding unrelated reactions to the reaction roles message", dl.Author);
}
});
return Task.CompletedTask;
}
private Task _client_ReactionRemoved(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> chan,
SocketReaction reaction)
{
_ = Task.Run(async () =>
{
try
{
if (!reaction.User.IsSpecified
|| reaction.User.Value.IsBot
|| reaction.User.Value is not SocketGuildUser gusr)
return;
if (chan.Value is not SocketGuildChannel gch)
return;
if (!_models.TryGetValue(gch.Guild.Id, out var confs))
return;
var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
if (conf is null)
return;
var reactionRole = conf.ReactionRoles.FirstOrDefault(x
=> x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
if (reactionRole is not null)
{
var role = gusr.Guild.GetRole(reactionRole.RoleId);
if (role is null)
return;
await gusr.RemoveRoleAsync(role);
}
}
catch { }
});
return Task.CompletedTask;
}
public bool Get(ulong id, out IndexedCollection<ReactionRoleMessage> rrs)
=> _models.TryGetValue(id, out rrs);
public bool Add(ulong id, ReactionRoleMessage rrm)
{
using var uow = _db.GetDbContext();
var table = uow.GetTable<ReactionRoleMessage>();
table.Delete(x => x.MessageId == rrm.MessageId);
var gc = uow.GuildConfigsForId(id,
set => set.Include(x => x.ReactionRoleMessages).ThenInclude(x => x.ReactionRoles));
if (gc.ReactionRoleMessages.Count >= 10)
return false;
gc.ReactionRoleMessages.Add(rrm);
uow.SaveChanges();
_models.AddOrUpdate(id, gc.ReactionRoleMessages, delegate { return gc.ReactionRoleMessages; });
return true;
}
public void Remove(ulong id, int index)
{
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(id,
set => set.Include(x => x.ReactionRoleMessages).ThenInclude(x => x.ReactionRoles));
uow.Set<ReactionRole>().RemoveRange(gc.ReactionRoleMessages[index].ReactionRoles);
gc.ReactionRoleMessages.RemoveAt(index);
_models.AddOrUpdate(id, gc.ReactionRoleMessages, delegate { return gc.ReactionRoleMessages; });
uow.SaveChanges();
}
/// <summary>
/// Adds a reaction role to the specified user.
/// </summary>
/// <param name="user">A Discord guild user.</param>
/// <param name="dbRero">The database settings of this reaction role.</param>
private Task AddReactionRoleAsync(SocketGuildUser user, ReactionRole dbRero)
{
var toAdd = user.Guild.GetRole(dbRero.RoleId);
return toAdd is not null && !user.Roles.Contains(toAdd) ? user.AddRoleAsync(toAdd) : Task.CompletedTask;
}
/// <summary>
/// Removes the exclusive reaction roles and reactions from the specified user.
/// </summary>
/// <param name="reactionMessage">The Discord message that contains the reaction roles.</param>
/// <param name="user">A Discord guild user.</param>
/// <param name="reaction">The Discord reaction of the user.</param>
/// <param name="dbReroMsg">The database entry of the reaction role message.</param>
/// <param name="dbRero">The database settings of this reaction role.</param>
/// <param name="cToken">A cancellation token to cancel the operation.</param>
/// <exception cref="OperationCanceledException">Occurs when the operation is cancelled before it began.</exception>
/// <exception cref="TaskCanceledException">Occurs when the operation is cancelled while it's still executing.</exception>
private Task RemoveExclusiveReactionRoleAsync(
Cacheable<IUserMessage, ulong> reactionMessage,
SocketGuildUser user,
SocketReaction reaction,
ReactionRoleMessage dbReroMsg,
ReactionRole dbRero,
CancellationToken cToken = default)
{
cToken.ThrowIfCancellationRequested();
var roleIds = dbReroMsg.ReactionRoles.Select(x => x.RoleId)
.Where(x => x != dbRero.RoleId)
.Select(x => user.Guild.GetRole(x))
.Where(x => x is not null);
var removeReactionsTask = RemoveOldReactionsAsync(reactionMessage, user, reaction, cToken);
var removeRolesTask = user.RemoveRolesAsync(roleIds);
return Task.WhenAll(removeReactionsTask, removeRolesTask);
}
/// <summary>
/// Removes old reactions from an exclusive reaction role.
/// </summary>
/// <param name="reactionMessage">The Discord message that contains the reaction roles.</param>
/// <param name="user">A Discord guild user.</param>
/// <param name="reaction">The Discord reaction of the user.</param>
/// <param name="cToken">A cancellation token to cancel the operation.</param>
/// <exception cref="OperationCanceledException">Occurs when the operation is cancelled before it began.</exception>
/// <exception cref="TaskCanceledException">Occurs when the operation is cancelled while it's still executing.</exception>
private async Task RemoveOldReactionsAsync(
Cacheable<IUserMessage, ulong> reactionMessage,
SocketGuildUser user,
SocketReaction reaction,
CancellationToken cToken = default)
{
cToken.ThrowIfCancellationRequested();
//if the role is exclusive,
// remove all other reactions user added to the message
var dl = await reactionMessage.GetOrDownloadAsync();
foreach (var r in dl.Reactions)
{
if (r.Key.Name == reaction.Emote.Name)
continue;
try { await dl.RemoveReactionAsync(r.Key, user); }
catch { }
await Task.Delay(100, cToken);
}
}
}

View File

@@ -530,7 +530,7 @@ public class UserPunishService : INService, IReadyExecutor
return default;
// if template is an embed, send that embed with replacements
// otherwise, treat template as a regular string with replacements
else if (!SmartText.CreateFrom(template).IsEmbed)
else if (SmartText.CreateFrom(template) is not { IsEmbed: true } or { IsEmbedArray: true })
{
template = JsonConvert.SerializeObject(new
{

View File

@@ -0,0 +1,72 @@
using NadekoBot.Modules.Gambling.Bank;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling;
public partial class Gambling
{
[Name("Bank")]
[Group("bank")]
public partial class BankCommands : GamblingModule<IBankService>
{
private readonly IBankService _bank;
public BankCommands(GamblingConfigService gcs, IBankService bank) : base(gcs)
{
_bank = bank;
}
[Cmd]
public async partial Task BankDeposit(ShmartNumber amount)
{
if (amount <= 0)
return;
if (await _bank.DepositAsync(ctx.User.Id, amount))
{
await ReplyConfirmLocalizedAsync(strs.bank_deposited(N(amount)));
}
else
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
}
}
[Cmd]
public async partial Task BankWithdraw(ShmartNumber amount)
{
if (amount <= 0)
return;
if (await _bank.WithdrawAsync(ctx.User.Id, amount))
{
await ReplyConfirmLocalizedAsync(strs.bank_withdrew(N(amount)));
}
else
{
await ReplyErrorLocalizedAsync(strs.bank_withdraw_insuff(CurrencySign));
}
}
[Cmd]
public async partial Task BankBalance()
{
var bal = await _bank.GetBalanceAsync(ctx.User.Id);
var eb = _eb.Create(ctx)
.WithOkColor()
.WithDescription(GetText(strs.bank_balance(N(bal))));
try
{
await ctx.User.EmbedAsync(eb);
await ctx.OkAsync();
}
catch
{
await ReplyErrorLocalizedAsync(strs.unable_to_dm_user);
}
}
}
}

View File

@@ -0,0 +1,77 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
namespace NadekoBot.Modules.Gambling.Bank;
public sealed class BankService : IBankService, INService
{
private readonly ICurrencyService _cur;
private readonly DbService _db;
public BankService(ICurrencyService cur, DbService db)
{
_cur = cur;
_db = db;
}
public async Task<bool> DepositAsync(ulong userId, long amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
if (!await _cur.RemoveAsync(userId, amount, new("bank", "deposit")))
return false;
await using var ctx = _db.GetDbContext();
await ctx.BankUsers
.ToLinqToDBTable()
.InsertOrUpdateAsync(() => new()
{
UserId = userId,
Balance = amount
},
(old) => new()
{
Balance = old.Balance + amount
},
() => new()
{
UserId = userId
});
return true;
}
public async Task<bool> WithdrawAsync(ulong userId, long amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
await using var ctx = _db.GetDbContext();
var rows = await ctx.BankUsers
.ToLinqToDBTable()
.Where(x => x.UserId == userId && x.Balance >= amount)
.UpdateAsync((old) => new()
{
Balance = old.Balance - amount
});
if (rows > 0)
{
await _cur.AddAsync(userId, amount, new("bank", "withdraw"));
return true;
}
return false;
}
public async Task<long> GetBalanceAsync(ulong userId)
{
await using var ctx = _db.GetDbContext();
return (await ctx.BankUsers
.ToLinqToDBTable()
.FirstOrDefaultAsync(x => x.UserId == userId))
?.Balance
?? 0;
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Gambling.Bank;
public interface IBankService
{
Task<bool> DepositAsync(ulong userId, long amount);
Task<bool> WithdrawAsync(ulong userId, long amount);
Task<long> GetBalanceAsync(ulong userId);
}

View File

@@ -0,0 +1,17 @@
#nullable disable
namespace NadekoBot.Modules.Gambling;
public class CashInteraction
{
public static NadekoInteractionData Data =
new NadekoInteractionData(new Emoji("🏦"), "cash:bank_show_balance");
public static NadekoInteraction CreateInstance(
DiscordSocketClient client,
ulong userId,
Func<SocketMessageComponent, Task> action)
=> new NadekoInteractionBuilder()
.WithData(Data)
.WithAction(action)
.Build(client, userId);
}

View File

@@ -3,6 +3,7 @@ using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Db;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Gambling.Bank;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Services.Currency;
@@ -40,6 +41,7 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly NumberFormatInfo _enUsCulture;
private readonly DownloadTracker _tracker;
private readonly GamblingConfigService _configService;
private readonly IBankService _bank;
private IUserMessage rdMsg;
@@ -49,13 +51,16 @@ public partial class Gambling : GamblingModule<GamblingService>
IDataCache cache,
DiscordSocketClient client,
DownloadTracker tracker,
GamblingConfigService configService)
GamblingConfigService configService,
IBankService bank)
: base(configService)
{
_db = db;
_cs = currency;
_cache = cache;
_client = client;
_bank = bank;
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
_enUsCulture.NumberDecimalDigits = 0;
_enUsCulture.NumberGroupSeparator = "";
@@ -72,7 +77,7 @@ public partial class Gambling : GamblingModule<GamblingService>
[Cmd]
public async partial Task Economy()
{
var ec = _service.GetEconomy();
var ec = await _service.GetEconomyAsync();
decimal onePercent = 0;
// This stops the top 1% from owning more than 100% of the money
@@ -84,13 +89,13 @@ 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_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.total), N(ec.Cash + ec.Planted + ec.Waifus))
.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
@@ -232,7 +237,6 @@ public partial class Gambling : GamblingModule<GamblingService>
var kwumId = new kwum(tr.Id).ToString();
var date = $"#{Format.Code(kwumId)} `〖{GetFormattedCurtrDate(tr)}〗`";
sb.AppendLine($"\\{change} {date} {Format.Bold(N(tr.Amount))}");
var transactionString = GetHumanReadableTransaction(tr.Type, tr.Extra, tr.OtherId);
if (transactionString is not null)
@@ -260,8 +264,7 @@ public partial class Gambling : GamblingModule<GamblingService>
int intId = id;
await using var uow = _db.GetDbContext();
var tr = await uow.CurrencyTransactions
.ToLinqToDBTable()
var tr = await uow.CurrencyTransactions.ToLinqToDBTable()
.Where(x => x.Id == intId && x.UserId == ctx.User.Id)
.FirstOrDefaultAsync();
@@ -271,8 +274,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
var eb = _eb.Create(ctx)
.WithOkColor();
var eb = _eb.Create(ctx).WithOkColor();
eb.WithAuthor(ctx.User);
eb.WithTitle(GetText(strs.transaction));
@@ -291,7 +293,6 @@ public partial class Gambling : GamblingModule<GamblingService>
eb.AddField("Note", tr.Note);
}
eb.WithFooter(GetFormattedCurtrDate(tr));
await ctx.Channel.EmbedAsync(eb);
@@ -311,7 +312,7 @@ public partial class Gambling : GamblingModule<GamblingService>
(_, _, ulong userId) => $"{type.Titleize()} - {subType.Titleize()} | [{userId}]",
_ => $"{type.Titleize()} - {subType.Titleize()}"
};
[Cmd]
[Priority(0)]
public async partial Task Cash(ulong userId)
@@ -320,13 +321,36 @@ public partial class Gambling : GamblingModule<GamblingService>
await ReplyConfirmLocalizedAsync(strs.has(Format.Code(userId.ToString()), cur));
}
private async Task BankAction(SocketMessageComponent smc)
{
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));
}
private NadekoInteraction CreateCashInteraction()
=> CashInteraction.CreateInstance(_client, ctx.User.Id, BankAction);
[Cmd]
[Priority(1)]
public async partial Task Cash([Leftover] IUser user = null)
{
user ??= ctx.User;
var cur = await GetBalanceStringAsync(user.Id);
await ConfirmLocalizedAsync(strs.has(Format.Bold(user.ToString()), cur));
var inter = user == ctx.User
? CreateCashInteraction()
: null;
await ConfirmLocalizedAsync(
user.ToString()
.Pipe(Format.Bold)
.With(cur)
.Pipe(strs.has),
inter);
}
[Cmd]
@@ -339,7 +363,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
if (!await _cs.TransferAsync(ctx.User.Id, receiver.Id, amount, ctx.User.ToString(), msg))
if (!await _cs.TransferAsync(_eb, ctx.User, receiver, amount, msg))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
@@ -386,10 +410,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
await _cs.AddAsync(usr.Id,
amount,
new("award", ctx.User.ToString()!, msg, ctx.User.Id)
);
await _cs.AddAsync(usr.Id, amount, new("award", ctx.User.ToString()!, msg, ctx.User.Id));
await ReplyConfirmLocalizedAsync(strs.awarded(N(amount), $"<@{usrId}>"));
}
@@ -403,10 +424,7 @@ public partial class Gambling : GamblingModule<GamblingService>
await _cs.AddBulkAsync(users.Select(x => x.Id).ToList(),
amount,
new("award",
ctx.User.ToString()!,
role.Name,
ctx.User.Id));
new("award", ctx.User.ToString()!, role.Name, ctx.User.Id));
await ReplyConfirmLocalizedAsync(strs.mass_award(N(amount),
Format.Bold(users.Count.ToString()),
@@ -423,10 +441,7 @@ public partial class Gambling : GamblingModule<GamblingService>
await _cs.RemoveBulkAsync(users.Select(x => x.Id).ToList(),
amount,
new("take",
ctx.User.ToString()!,
null,
ctx.User.Id));
new("take", ctx.User.ToString()!, null, ctx.User.Id));
await ReplyConfirmLocalizedAsync(strs.mass_take(N(amount),
Format.Bold(users.Count.ToString()),
@@ -444,10 +459,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
var extra = new TxData("take",
ctx.User.ToString()!,
null,
ctx.User.Id);
var extra = new TxData("take", ctx.User.ToString()!, null, ctx.User.Id);
if (await _cs.RemoveAsync(user.Id, amount, extra))
{
@@ -459,7 +471,6 @@ public partial class Gambling : GamblingModule<GamblingService>
}
}
[Cmd]
[OwnerOnly]
public async partial Task Take(long amount, [Leftover] ulong usrId)
@@ -469,10 +480,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
var extra = new TxData("take",
ctx.User.ToString()!,
null,
ctx.User.Id);
var extra = new TxData("take", ctx.User.ToString()!, null, ctx.User.Id);
if (await _cs.RemoveAsync(usrId, amount, extra))
{
@@ -560,10 +568,7 @@ public partial class Gambling : GamblingModule<GamblingService>
}
else
{
await rdMsg.ModifyAsync(x =>
{
x.Embed = embed.Build();
});
await rdMsg.ModifyAsync(x => { x.Embed = embed.Build(); });
}
}
@@ -613,7 +618,6 @@ public partial class Gambling : GamblingModule<GamblingService>
var result = br.Roll();
var str = Format.Bold(ctx.User.ToString()) + Format.Code(GetText(strs.roll(result.Roll)));
if (result.Multiplier > 0)
{
@@ -742,9 +746,7 @@ public partial class Gambling : GamblingModule<GamblingService>
if (amount > 0)
{
if (!await _cs.RemoveAsync(ctx.User.Id,
amount,
new("rps", "bet", "")))
if (!await _cs.RemoveAsync(ctx.User.Id, amount, new("rps", "bet", "")))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;

View File

@@ -1,8 +1,11 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using NadekoBot.Db.Models;
using NadekoBot.Migrations;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Common.Connect4;
using NadekoBot.Modules.Gambling.Common.Slot;
@@ -158,7 +161,7 @@ public class GamblingService : INService, IReadyExecutor
return toReturn;
}
public EconomyResult GetEconomy()
public async Task<EconomyResult> GetEconomyAsync()
{
if (_cache.TryGetEconomy(out var data))
{
@@ -173,6 +176,7 @@ public class GamblingService : INService, IReadyExecutor
decimal onePercent;
decimal planted;
decimal waifus;
decimal bank;
long bot;
using (var uow = _db.GetDbContext())
@@ -182,6 +186,8 @@ public class GamblingService : INService, IReadyExecutor
planted = uow.PlantedCurrency.AsQueryable().Sum(x => x.Amount);
waifus = uow.WaifuInfo.GetTotalValue();
bot = uow.DiscordUser.GetUserCurrency(_client.CurrentUser.Id);
bank = await uow.GetTable<BankUser>()
.SumAsyncLinqToDB(x => x.Balance);
}
var result = new EconomyResult
@@ -190,7 +196,8 @@ public class GamblingService : INService, IReadyExecutor
Planted = planted,
Bot = bot,
Waifus = waifus,
OnePercent = onePercent
OnePercent = onePercent,
Bank = bank
};
_cache.SetEconomy(JsonConvert.SerializeObject(result));
@@ -207,6 +214,7 @@ public class GamblingService : INService, IReadyExecutor
public decimal Planted { get; set; }
public decimal Waifus { get; set; }
public decimal OnePercent { get; set; }
public decimal Bank { get; set; }
public long Bot { get; set; }
}
}

View File

@@ -39,13 +39,19 @@ public abstract class GamblingModule<TService> : NadekoModule<TService>
return true;
}
public static string N(long cur, IFormatProvider format)
=> cur.ToString("C0", format);
public static string N(decimal cur, IFormatProvider format)
=> cur.ToString("C0", format);
protected string N(long cur)
=> cur.ToString("C0", GetFlowersCiInternal());
=> N(cur, GetFlowersCiInternal());
protected string N(decimal cur)
=> cur.ToString("C0", GetFlowersCiInternal());
=> N(cur, GetFlowersCiInternal());
private IFormatProvider GetFlowersCiInternal()
protected IFormatProvider GetFlowersCiInternal()
{
var flowersCi = (CultureInfo)Culture.Clone();
flowersCi.NumberFormat.CurrencySymbol = CurrencySign;

View File

@@ -129,6 +129,8 @@ public partial class Help : NadekoModule<HelpService>
return strs.module_description_permissions;
case "xp":
return strs.module_description_xp;
case "medusa":
return strs.module_description_medusa;
default:
return strs.module_description_missing;
}
@@ -264,6 +266,20 @@ public partial class Help : NadekoModule<HelpService>
await ctx.Channel.EmbedAsync(embed);
}
private async Task Group(ModuleInfo group)
{
var eb = _eb.Create(ctx)
.WithTitle(GetText(strs.cmd_group_commands(group.Name)))
.WithOkColor();
foreach (var cmd in group.Commands)
{
eb.AddField(prefix + cmd.Aliases.First(), cmd.RealSummary(_strings, _medusae, Culture, prefix));
}
await ctx.Channel.EmbedAsync(eb);
}
[Cmd]
[Priority(0)]
public async partial Task H([Leftover] string fail)
@@ -276,6 +292,20 @@ public partial class Help : NadekoModule<HelpService>
return;
}
if (fail.StartsWith(prefix))
fail = fail.Substring(prefix.Length);
var group = _cmds.Modules
.SelectMany(x => x.Submodules)
.Where(x => !string.IsNullOrWhiteSpace(x.Group))
.FirstOrDefault(x => x.Group.Equals(fail, StringComparison.InvariantCultureIgnoreCase));
if (group is not null)
{
await Group(group);
return;
}
await ReplyErrorLocalizedAsync(strs.command_not_found);
}

View File

@@ -81,8 +81,7 @@ public class HelpService : IExecNoCommand, INService
em.AddField(GetText(strs.requires, guild), string.Join("\n", reqs));
em.AddField(_strings.GetText(strs.usage),
string.Join("\n",
Array.ConvertAll(com.RealRemarksArr(_strings,_medusae, culture, prefix), arg => Format.Code(arg))))
string.Join("\n", com.RealRemarksArr(_strings,_medusae, culture, prefix).Map(arg => Format.Code(arg))))
.WithFooter(GetText(strs.module(com.Module.GetTopLevelModule().Name), guild))
.WithOkColor();

View File

@@ -53,7 +53,7 @@ public class FeedsService : INService
if (kvp.Value.Count == 0)
continue;
var rssUrl = kvp.Key;
var rssUrl = kvp.Value.First().Url;
try
{
var feed = await FeedReader.ReadAsync(rssUrl);
@@ -143,8 +143,9 @@ public class FeedsService : INService
allSendTasks.Add(feedSendTasks.WhenAll());
}
}
catch
catch (Exception ex)
{
Log.Warning("An error occured while getting rss stream: {Message}", ex.Message);
}
}

View File

@@ -121,6 +121,18 @@ public partial class Searches
else
await ReplyConfirmLocalizedAsync(strs.stream_off_disabled);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async partial Task StreamOnlineDelete()
{
var newValue = _service.ToggleStreamOnlineDelete(ctx.Guild.Id);
if (newValue)
await ReplyConfirmLocalizedAsync(strs.stream_online_delete_enabled);
else
await ReplyConfirmLocalizedAsync(strs.stream_online_delete_disabled);
}
[Cmd]
[RequireContext(ContextType.Guild)]

View File

@@ -1,4 +1,6 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
@@ -24,6 +26,7 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
private readonly Dictionary<StreamDataKey, Dictionary<ulong, HashSet<FollowedStream>>> _shardTrackedStreams;
private readonly ConcurrentHashSet<ulong> _offlineNotificationServers;
private readonly ConcurrentHashSet<ulong> _deleteOnOfflineServers;
private readonly IPubSub _pubSub;
private readonly IEmbedBuilderService _eb;
@@ -33,6 +36,7 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
private readonly TypedKey<FollowStreamPubData> _streamFollowKey;
private readonly TypedKey<FollowStreamPubData> _streamUnfollowKey;
private readonly ConnectionMultiplexer _redis;
public StreamNotificationService(
DbService db,
@@ -50,6 +54,7 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
_strings = strings;
_pubSub = pubSub;
_eb = eb;
_redis = redis;
_streamTracker = new(httpFactory, creds, redis, creds.GetCreds().RedisKey(), client.ShardId == 0);
_streamsOnlineKey = new("streams.online");
@@ -71,6 +76,11 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
.Where(gc => gc.NotifyStreamOffline)
.Select(x => x.GuildId)
.ToList());
_deleteOnOfflineServers = new(guildConfigs
.Where(gc => gc.DeleteStreamOnlineMessage)
.Select(x => x.GuildId)
.ToList());
var followedStreams = guildConfigs.SelectMany(x => x.FollowedStreams).ToList();
@@ -243,6 +253,44 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
.WhenAll();
}
}
if (_client.ShardId == 0)
{
foreach (var stream in offlineStreams)
{
await DeleteOnlineMessages(stream);
}
}
}
private async Task DeleteOnlineMessages(StreamData stream)
{
var db = _redis.GetDatabase();
var data = await db.ListRangeAsync($"streams_online_del:{stream.CreateKey()}");
await db.KeyDeleteAsync($"streams_online_del:{stream.CreateKey()}");
foreach (string pair in data)
{
var pairArr = pair.Split(',');
if (pairArr.Length != 2)
continue;
if (!ulong.TryParse(pairArr[0], out var chId) || !ulong.TryParse(pairArr[1], out var msgId))
continue;
try
{
var textChannel = await _client.GetChannelAsync(chId) as ITextChannel;
if (textChannel is null)
continue;
await textChannel.DeleteMessageAsync(msgId);
}
catch
{
continue;
}
}
}
private async ValueTask HandleStreamsOnline(List<StreamData> onlineStreams)
@@ -252,13 +300,13 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
var key = stream.CreateKey();
if (_shardTrackedStreams.TryGetValue(key, out var fss))
{
await fss.SelectMany(x => x.Value)
.Select(fs =>
var messages = await fss.SelectMany(x => x.Value)
.Select(async fs =>
{
var textChannel = _client.GetGuild(fs.GuildId)?.GetTextChannel(fs.ChannelId);
if (textChannel is null)
return Task.CompletedTask;
return default;
var rep = new ReplacementBuilder().WithOverride("%user%", () => fs.Username)
.WithOverride("%platform%", () => fs.Type.ToString())
@@ -266,9 +314,38 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
var message = string.IsNullOrWhiteSpace(fs.Message) ? "" : rep.Replace(fs.Message);
return textChannel.EmbedAsync(GetEmbed(fs.GuildId, stream), message);
var msg = await textChannel.EmbedAsync(GetEmbed(fs.GuildId, stream), message);
// only cache the ids of channel/message pairs
if(_deleteOnOfflineServers.Contains(fs.GuildId))
return (textChannel.Id, msg.Id);
else
return default;
})
.WhenAll();
// push online stream messages to redis
// when streams go offline, any server which
// has the online stream message deletion feature
// enabled will have the online messages deleted
try
{
var pairs = messages
.Where(x => x != default)
.Select(x => (RedisValue)$"{x.Item1},{x.Item2}")
.ToArray();
if (pairs.Length > 0)
{
var db = _redis.GetDatabase();
await db.ListRightPushAsync($"streams_online_del:{key}", pairs);
}
}
catch
{
}
}
}
}
@@ -484,6 +561,21 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
return newValue;
}
public bool ToggleStreamOnlineDelete(ulong guildId)
{
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set);
var newValue = gc.DeleteStreamOnlineMessage = !gc.DeleteStreamOnlineMessage;
uow.SaveChanges();
if (newValue)
_deleteOnOfflineServers.Add(guildId);
else
_deleteOnOfflineServers.TryRemove(guildId);
return newValue;
}
public Task<StreamData> GetStreamDataAsync(string url)
=> _streamTracker.GetStreamDataByUrlAsync(url);

View File

@@ -84,13 +84,12 @@ public class PatreonRewardsService : INService, IReadyExecutor
try
{
using var http = _httpFactory.CreateClient();
using var content = new StringContent(string.Empty);
using var res = await http.PostAsync("https://www.patreon.com/api/oauth2/token"
+ "?grant_type=refresh_token"
+ $"&refresh_token={creds.Patreon.RefreshToken}"
+ $"&client_id={creds.Patreon.ClientId}"
+ $"&client_secret={creds.Patreon.ClientSecret}",
content);
null);
res.EnsureSuccessStatusCode();
@@ -149,7 +148,7 @@ public class PatreonRewardsService : INService, IReadyExecutor
if (!success)
return;
}
LastUpdate = DateTime.UtcNow;
try
{

View File

@@ -76,6 +76,7 @@ public class RemindService : INService, IReadyExecutor
await uow.SaveChangesAsync();
}
// todo move isonshard to a method
private async Task<List<Reminder>> GetRemindersBeforeAsync(DateTime now)
{
await using var uow = _db.GetDbContext();

View File

@@ -48,7 +48,7 @@ public partial class Utility : NadekoModule
_tracker = tracker;
_httpFactory = httpFactory;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]

View File

@@ -240,7 +240,7 @@ public partial class Xp
[Cmd]
[Priority(1)]
public partial Task ClubAccept(IUser user)
=> ClubAccept($"{user.Username}#{user.Discriminator}");
=> ClubAccept(user.ToString());
[Cmd]
[Priority(0)]
@@ -282,7 +282,7 @@ public partial class Xp
[Cmd]
[Priority(1)]
public partial Task ClubBan([Leftover] IUser user)
=> ClubBan($"{user.Username}#{user.Discriminator}");
=> ClubBan(user.ToString());
[Cmd]
[Priority(0)]
@@ -300,7 +300,7 @@ public partial class Xp
[Cmd]
[Priority(1)]
public partial Task ClubUnBan([Leftover] IUser user)
=> ClubUnBan($"{user.Username}#{user.Discriminator}");
=> ClubUnBan(user.ToString());
[Cmd]
[Priority(0)]

View File

@@ -17,7 +17,6 @@ using Image = SixLabors.ImageSharp.Image;
namespace NadekoBot.Modules.Xp.Services;
// todo improve xp with linqtodb
public class XpService : INService, IReadyExecutor, IExecNoCommand
{
public const int XP_REQUIRED_LVL_1 = 36;
@@ -133,7 +132,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
try
{
var toNotify =
new List<(IGuild Guild, IMessageChannel MessageChannel, IUser User, int Level,
new List<(IGuild Guild, IMessageChannel MessageChannel, IUser User, long Level,
XpNotificationLocation NotifyType, NotifOf NotifOf)>();
var roleRewards = new Dictionary<ulong, List<XpRoleReward>>();
var curRewards = new Dictionary<ulong, List<XpCurrencyReward>>();
@@ -640,7 +639,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
{
DiscordUser du;
UserXpStats stats;
int totalXp;
long totalXp;
int globalRank;
int guildRank;
await using (var uow = _db.GetDbContext())

View File

@@ -1,29 +1,15 @@
#nullable disable
using NadekoBot.Modules.Xp.Services;
using LinqToDB;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Xp.Extensions;
public static class Extensions
{
public static (int Level, int LevelXp, int LevelRequiredXp) GetLevelData(this UserXpStats stats)
{
var baseXp = XpService.XP_REQUIRED_LVL_1;
var required = baseXp;
var totalXp = 0;
var lvl = 1;
while (true)
{
required = (int)(baseXp + (baseXp / 4.0 * (lvl - 1)));
if (required + totalXp > stats.Xp)
break;
totalXp += required;
lvl++;
}
return (lvl - 1, stats.Xp - totalXp, required);
}
public static async Task<LevelStats> GetLevelDataFor(this ITable<UserXpStats> userXp, ulong guildId, ulong userId)
=> await userXp
.Where(x => x.GuildId == guildId && x.UserId == userId)
.FirstOrDefaultAsync() is UserXpStats uxs
? new(uxs.Xp + uxs.AwardedXp)
: new(0);
}

View File

@@ -5,12 +5,12 @@ namespace NadekoBot.Modules.Xp;
public class LevelStats
{
public int Level { get; }
public int LevelXp { get; }
public int RequiredXp { get; }
public int TotalXp { get; }
public long Level { get; }
public long LevelXp { get; }
public long RequiredXp { get; }
public long TotalXp { get; }
public LevelStats(int xp)
public LevelStats(long xp)
{
if (xp < 0)
xp = 0;

View File

@@ -21,6 +21,7 @@
<ItemGroup>
<PackageReference Include="AngleSharp" Version="0.16.1">
<PrivateAssets>all</PrivateAssets>
<Publish>True</Publish>
</PackageReference>
<PackageReference Include="AWSSDK.S3" Version="3.7.8.4" />
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.4" />
@@ -59,25 +60,25 @@
<PackageReference Include="Humanizer" Version="2.14.1">
<PrivateAssets>all</PrivateAssets>
<Publish>True</Publish>
</PackageReference>
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" />
<!-- Db-related packages -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.3">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="linq2db.EntityFrameworkCore" Version="6.6.1" />
<PackageReference Include="linq2db.EntityFrameworkCore" Version="6.7.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1" />
<!-- Remove this when static abstract interface members support is released -->
<PackageReference Include="System.Runtime.Experimental" Version="6.0.0" />
<!-- <PackageReference Include="System.Runtime.Experimental" Version="6.0.2" />-->
<!-- Used by .crypto command -->
<PackageReference Include="YahooFinanceApi" Version="2.1.2" />

View File

@@ -13,17 +13,26 @@ public static class CurrencyServiceExtensions
// todo transfer should be a transaction
public static async Task<bool> TransferAsync(
this ICurrencyService cs,
ulong fromId,
ulong toId,
IEmbedBuilderService ebs,
IUser from,
IUser to,
long amount,
string fromName,
string note)
string? note)
{
var fromWallet = await cs.GetWalletAsync(fromId);
var toWallet = await cs.GetWalletAsync(toId);
var fromWallet = await cs.GetWalletAsync(from.Id);
var toWallet = await cs.GetWalletAsync(to.Id);
var extra = new TxData("gift", fromName, note, fromId);
var extra = new TxData("gift", from.ToString()!, note, from.Id);
return await fromWallet.Transfer(amount, toWallet, extra);
if (await fromWallet.Transfer(amount, toWallet, extra))
{
await to.SendConfirmAsync(ebs,
string.IsNullOrWhiteSpace(note)
? $"Gift from {from}"
: $"Gift from {from}: {note}");
return true;
}
return false;
}
}

View File

@@ -53,9 +53,8 @@ public sealed class BotCredsProvider : IBotCredsProvider
_config = new ConfigurationBuilder().AddYamlFile(CredsPath, false, true)
.AddEnvironmentVariables("NadekoBot_")
.Build();
#if !GLOBAL_NADEKO
_changeToken = ChangeToken.OnChange(() => _config.GetReloadToken(), Reload);
#endif
Reload();
}

View File

@@ -7,7 +7,7 @@ namespace NadekoBot.Services;
public sealed class StatsService : IStatsService, IReadyExecutor, INService
{
public const string BOT_VERSION = "4.1.0";
public const string BOT_VERSION = "4.1.4";
public string Author
=> "Kwoth#2452";

View File

@@ -0,0 +1,17 @@
#nullable disable
using System.Globalization;
namespace NadekoBot.Services;
public static class BotStringsExtensions
{
// this one is for pipe fun, see PipeExtensions.cs
public static string GetText(this IBotStrings strings, in LocStr str, in ulong guildId)
=> strings.GetText(str.Key, guildId, str.Params);
public static string GetText(this IBotStrings strings, in LocStr str, ulong? guildId = null)
=> strings.GetText(str.Key, guildId, str.Params);
public static string GetText(this IBotStrings strings, in LocStr str, CultureInfo culture)
=> strings.GetText(str.Key, culture, str.Params);
}

View File

@@ -46,7 +46,7 @@ public class RedisBotStringsProvider : IBotStringsProvider
if (descStr == default)
return null;
var args = Array.ConvertAll(argsStr.Split('&'), HttpUtility.UrlDecode);
var args = argsStr.Split('&').Map(HttpUtility.UrlDecode);
return new()
{
Args = args,
@@ -68,7 +68,7 @@ public class RedisBotStringsProvider : IBotStringsProvider
{
var hashFields = localeStrings
.Select(x => new HashEntry($"{x.Key}::args",
string.Join('&', Array.ConvertAll(x.Value.Args, HttpUtility.UrlEncode))))
string.Join('&', x.Value.Args.Map(HttpUtility.UrlEncode))))
.Concat(localeStrings.Select(x => new HashEntry($"{x.Key}::desc", x.Value.Desc)))
.ToArray();

View File

@@ -23,6 +23,11 @@ public static class Extensions
x.Embed = set.GetEmbed().Build();
x.Content = set.PlainText?.SanitizeMentions() ?? "";
}),
SmartEmbedTextArray set => msg.ModifyAsync(x =>
{
x.Embeds = set.GetEmbedBuilders().Map(eb => eb.Build());
x.Content = set.Content?.SanitizeMentions() ?? "";
}),
SmartPlainText spt => msg.ModifyAsync(x =>
{
x.Content = spt.Text.SanitizeMentions();
@@ -116,8 +121,7 @@ public static class Extensions
args = strings.GetCommandStrings(cmd.Summary, culture).Args;
}
return Array.ConvertAll(args,
arg => GetFullUsage(cmd.Name, arg, prefix));
return args.Map(arg => GetFullUsage(cmd.Aliases.First(), arg, prefix));
}
private static string GetFullUsage(string commandName, string args, string prefix)
@@ -139,27 +143,6 @@ public static class Extensions
public static IEmbedBuilder WithErrorColor(this IEmbedBuilder eb)
=> eb.WithColor(EmbedColor.Error);
public static ReactionEventWrapper OnReaction(
this IUserMessage msg,
DiscordSocketClient client,
Func<SocketReaction, Task> reactionAdded,
Func<SocketReaction, Task>? reactionRemoved = null)
{
if (reactionRemoved is null)
reactionRemoved = _ => Task.CompletedTask;
var wrap = new ReactionEventWrapper(client, msg);
wrap.OnReactionAdded += r =>
{
_ = Task.Run(() => reactionAdded(r));
};
wrap.OnReactionRemoved += r =>
{
_ = Task.Run(() => reactionRemoved(r));
};
return wrap;
}
public static HttpClient AddFakeHeaders(this HttpClient http)
{
AddFakeHeaders(http.DefaultRequestHeaders);
@@ -231,10 +214,4 @@ public static class Extensions
=> msg.Content.Headers.ContentLength is long length
? length
: long.MaxValue;
public static string GetText(this IBotStrings strings, in LocStr str, ulong? guildId = null)
=> strings.GetText(str.Key, guildId, str.Params);
public static string GetText(this IBotStrings strings, in LocStr str, CultureInfo culture)
=> strings.GetText(str.Key, culture, str.Params);
}

View File

@@ -2,48 +2,116 @@ namespace NadekoBot.Extensions;
public static class MessageChannelExtensions
{
private static readonly IEmote _arrowLeft = new Emoji("⬅");
private static readonly IEmote _arrowRight = new Emoji("➡");
// main overload that all other send methods reduce to
public static Task<IUserMessage> SendAsync(
this IMessageChannel channel,
string? plainText,
Embed? embed = null,
IReadOnlyCollection<Embed>? embeds = null,
bool sanitizeAll = false,
MessageComponent? components = null)
{
plainText = sanitizeAll
? plainText?.SanitizeAllMentions() ?? ""
: plainText?.SanitizeMentions() ?? "";
public static Task<IUserMessage> EmbedAsync(this IMessageChannel ch, IEmbedBuilder embed, string msg = "")
=> ch.SendMessageAsync(msg,
embed: embed.Build(),
return channel.SendMessageAsync(plainText,
embed: embed,
embeds: embeds is null
? null
: embeds as Embed[] ?? embeds.ToArray(),
components: components,
options: new()
{
RetryMode = RetryMode.AlwaysRetry
});
}
public static async Task<IUserMessage> SendAsync(
this IMessageChannel channel,
string? plainText,
NadekoInteraction? inter,
Embed? embed = null,
IReadOnlyCollection<Embed>? embeds = null,
bool sanitizeAll = false)
{
var msg = await channel.SendAsync(plainText,
embed,
embeds,
sanitizeAll,
inter?.CreateComponent());
if (inter is not null)
await inter.RunAsync(msg);
return msg;
}
public static Task<IUserMessage> SendAsync(
this IMessageChannel channel,
string? plainText,
Embed? embed,
SmartText text,
bool sanitizeAll = false)
{
plainText = sanitizeAll ? plainText?.SanitizeAllMentions() ?? "" : plainText?.SanitizeMentions() ?? "";
return channel.SendMessageAsync(plainText, embed: embed);
}
public static Task<IUserMessage> SendAsync(this IMessageChannel channel, SmartText text, bool sanitizeAll = false)
=> text switch
{
SmartEmbedText set => channel.SendAsync(set.PlainText, set.GetEmbed().Build(), sanitizeAll),
SmartPlainText st => channel.SendAsync(st.Text, null, sanitizeAll),
SmartEmbedText set => channel.SendAsync(set.PlainText,
set.GetEmbed().Build(),
sanitizeAll: sanitizeAll),
SmartPlainText st => channel.SendAsync(st.Text,
default(Embed),
sanitizeAll: sanitizeAll),
SmartEmbedTextArray arr => channel.SendAsync(arr.Content,
embeds: arr.GetEmbedBuilders().Map(e => e.Build())),
_ => throw new ArgumentOutOfRangeException(nameof(text))
};
// this is a huge problem, because now i don't have
// access to embed builder service
// as this is an extension of the message channel
public static Task<IUserMessage> SendErrorAsync(
public static Task<IUserMessage> EmbedAsync(
this IMessageChannel ch,
IEmbedBuilder? embed,
string plainText = "",
IReadOnlyCollection<IEmbedBuilder>? embeds = null,
NadekoInteraction? inter = null)
=> ch.SendAsync(plainText,
inter,
embed: embed?.Build(),
embeds: embeds?.Map(x => x.Build()));
public static Task<IUserMessage> SendAsync(
this IMessageChannel ch,
IEmbedBuilderService eb,
string text,
MessageType type,
NadekoInteraction? inter = null)
{
var builder = eb.Create().WithDescription(text);
builder = (type switch
{
MessageType.Error => builder.WithErrorColor(),
MessageType.Ok => builder.WithOkColor(),
MessageType.Pending => builder.WithPendingColor(),
_ => throw new ArgumentOutOfRangeException(nameof(type))
});
return ch.EmbedAsync(builder, inter: inter);
}
// regular send overloads
public static Task<IUserMessage> SendErrorAsync(this IMessageChannel ch, IEmbedBuilderService eb, string text)
=> ch.SendAsync(eb, text, MessageType.Error);
public static Task<IUserMessage> SendConfirmAsync(this IMessageChannel ch, IEmbedBuilderService eb, string text)
=> ch.SendAsync(eb, text, MessageType.Ok);
public static Task<IUserMessage> SendAsync(
this IMessageChannel ch,
IEmbedBuilderService eb,
MessageType type,
string title,
string error,
string text,
string? url = null,
string? footer = null)
{
var embed = eb.Create().WithErrorColor().WithDescription(error).WithTitle(title);
var embed = eb.Create().WithDescription(text).WithTitle(title);
if (url is not null && Uri.IsWellFormedUriString(url, UriKind.Absolute))
embed.WithUrl(url);
@@ -51,15 +119,19 @@ public static class MessageChannelExtensions
if (!string.IsNullOrWhiteSpace(footer))
embed.WithFooter(footer);
return ch.SendMessageAsync("", embed: embed.Build());
embed = type switch
{
MessageType.Error => embed.WithErrorColor(),
MessageType.Ok => embed.WithOkColor(),
MessageType.Pending => embed.WithPendingColor(),
_ => throw new ArgumentOutOfRangeException(nameof(type))
};
return ch.EmbedAsync(embed);
}
public static Task<IUserMessage> SendErrorAsync(this IMessageChannel ch, IEmbedBuilderService eb, string error)
=> ch.SendMessageAsync("", embed: eb.Create().WithErrorColor().WithDescription(error).Build());
public static Task<IUserMessage> SendPendingAsync(this IMessageChannel ch, IEmbedBuilderService eb, string message)
=> ch.SendMessageAsync("", embed: eb.Create().WithPendingColor().WithDescription(message).Build());
// embed title and optional footer overloads
public static Task<IUserMessage> SendConfirmAsync(
this IMessageChannel ch,
IEmbedBuilderService eb,
@@ -67,21 +139,19 @@ public static class MessageChannelExtensions
string text,
string? url = null,
string? footer = null)
{
var embed = eb.Create().WithOkColor().WithDescription(text).WithTitle(title);
if (url is not null && Uri.IsWellFormedUriString(url, UriKind.Absolute))
embed.WithUrl(url);
if (!string.IsNullOrWhiteSpace(footer))
embed.WithFooter(footer);
return ch.SendMessageAsync("", embed: embed.Build());
}
public static Task<IUserMessage> SendConfirmAsync(this IMessageChannel ch, IEmbedBuilderService eb, string text)
=> ch.SendMessageAsync("", embed: eb.Create().WithOkColor().WithDescription(text).Build());
=> ch.SendAsync(eb, MessageType.Ok, title, text, url, footer);
public static Task<IUserMessage> SendErrorAsync(
this IMessageChannel ch,
IEmbedBuilderService eb,
string title,
string text,
string? url = null,
string? footer = null)
=> ch.SendAsync(eb, MessageType.Error, title, text, url, footer);
// weird stuff
public static Task<IUserMessage> SendTableAsync<T>(
this IMessageChannel ch,
string seed,
@@ -114,9 +184,12 @@ public static class MessageChannelExtensions
itemsPerPage,
addPaginatedFooter);
/// <summary>
/// danny kamisama
/// </summary>
private const string BUTTON_LEFT = "BUTTON_LEFT";
private const string BUTTON_RIGHT = "BUTTON_RIGHT";
private static readonly IEmote _arrowLeft = Emote.Parse("<:x:969658061805465651>");
private static readonly IEmote _arrowRight = Emote.Parse("<:x:969658062220701746>");
public static async Task SendPaginatedConfirmAsync(
this ICommandContext ctx,
int currentPage,
@@ -125,95 +198,116 @@ public static class MessageChannelExtensions
int itemsPerPage,
bool addPaginatedFooter = true)
{
var lastPage = (totalElements - 1) / itemsPerPage;
var embed = await pageFunc(currentPage);
var lastPage = (totalElements - 1) / itemsPerPage;
var canPaginate = true;
if (ctx.Guild is SocketGuild sg && !sg.CurrentUser.GetPermissions((IGuildChannel)ctx.Channel).AddReactions)
canPaginate = false;
if (!canPaginate)
embed.WithFooter("⚠️ AddReaction permission required for pagination.");
else if (addPaginatedFooter)
if (addPaginatedFooter)
embed.AddPaginatedFooter(currentPage, lastPage);
var msg = await ctx.Channel.EmbedAsync(embed);
if (lastPage == 0 || !canPaginate)
return;
await msg.AddReactionAsync(_arrowLeft);
await msg.AddReactionAsync(_arrowRight);
await Task.Delay(2000);
var lastPageChange = DateTime.MinValue;
async Task ChangePage(SocketReaction r)
var component = new ComponentBuilder()
.WithButton(new ButtonBuilder()
.WithStyle(ButtonStyle.Primary)
.WithCustomId(BUTTON_LEFT)
.WithDisabled(lastPage == 0)
.WithEmote(_arrowLeft))
.WithButton(new ButtonBuilder()
.WithStyle(ButtonStyle.Primary)
.WithCustomId(BUTTON_RIGHT)
.WithDisabled(lastPage == 0)
.WithEmote(_arrowRight))
.Build();
var msg = await ctx.Channel.SendAsync(null, embed: embed.Build(), components: component);
Task OnInteractionAsync(SocketInteraction si)
{
try
_ = Task.Run(async () =>
{
if (r.UserId != ctx.User.Id)
if (si is not SocketMessageComponent smc)
return;
if (DateTime.UtcNow - lastPageChange < TimeSpan.FromSeconds(1))
if (smc.Message.Id != msg.Id)
return;
if (r.Emote.Name == _arrowLeft.Name)
if (smc.Data.CustomId != BUTTON_LEFT && smc.Data.CustomId != BUTTON_RIGHT)
return;
await si.DeferAsync();
if (smc.User.Id != ctx.User.Id)
return;
if (smc.Data.CustomId == BUTTON_LEFT)
{
if (currentPage == 0)
return;
lastPageChange = DateTime.UtcNow;
var toSend = await pageFunc(--currentPage);
if (addPaginatedFooter)
toSend.AddPaginatedFooter(currentPage, lastPage);
await msg.ModifyAsync(x => x.Embed = toSend.Build());
await smc.ModifyOriginalResponseAsync(x => x.Embed = toSend.Build());
}
else if (r.Emote.Name == _arrowRight.Name)
else if (smc.Data.CustomId == BUTTON_RIGHT)
{
if (lastPage > currentPage)
{
lastPageChange = DateTime.UtcNow;
var toSend = await pageFunc(++currentPage);
if (addPaginatedFooter)
toSend.AddPaginatedFooter(currentPage, lastPage);
await msg.ModifyAsync(x => x.Embed = toSend.Build());
await smc.ModifyOriginalResponseAsync(x => x.Embed = toSend.Build());
}
}
}
catch (Exception)
{
//ignored
}
});
return Task.CompletedTask;
}
using (msg.OnReaction((DiscordSocketClient)ctx.Client, ChangePage, ChangePage))
{
await Task.Delay(30000);
}
if (lastPage == 0)
return;
try
var client = (DiscordSocketClient)ctx.Client;
client.InteractionCreated += OnInteractionAsync;
await Task.Delay(30_000);
client.InteractionCreated -= OnInteractionAsync;
await msg.ModifyAsync(mp => mp.Components = new ComponentBuilder().Build());
}
private static readonly Emoji _okEmoji = new Emoji("✅");
private static readonly Emoji _warnEmoji = new Emoji("⚠️");
private static readonly Emoji _errorEmoji = new Emoji("❌");
public static Task ReactAsync(this ICommandContext ctx, MessageType type)
{
var emoji = type switch
{
if (msg.Channel is ITextChannel && ((SocketGuild)ctx.Guild).CurrentUser.GuildPermissions.ManageMessages)
await msg.RemoveAllReactionsAsync();
else
{
await msg.Reactions.Where(x => x.Value.IsMe)
.Select(x => msg.RemoveReactionAsync(x.Key, ctx.Client.CurrentUser))
.WhenAll();
}
}
catch
{
// ignored
}
MessageType.Error => _errorEmoji,
MessageType.Pending => _warnEmoji,
MessageType.Ok => _okEmoji,
_ => throw new ArgumentOutOfRangeException(nameof(type)),
};
return ctx.Message.AddReactionAsync(emoji);
}
public static Task OkAsync(this ICommandContext ctx)
=> ctx.Message.AddReactionAsync(new Emoji("✅"));
=> ctx.ReactAsync(MessageType.Ok);
public static Task ErrorAsync(this ICommandContext ctx)
=> ctx.Message.AddReactionAsync(new Emoji("❌"));
=> ctx.ReactAsync(MessageType.Error);
public static Task WarningAsync(this ICommandContext ctx)
=> ctx.Message.AddReactionAsync(new Emoji("⚠️"));
=> ctx.ReactAsync(MessageType.Pending);
}
public enum MessageType
{
Ok,
Pending,
Error
}

View File

@@ -0,0 +1,23 @@
namespace NadekoBot.Extensions;
public delegate TOut PipeFunc<TIn, out TOut>(in TIn a);
public delegate TOut PipeFunc<TIn1, TIn2, out TOut>(in TIn1 a, in TIn2 b);
public static class PipeExtensions
{
public static TOut Pipe<TIn, TOut>(this TIn a, Func<TIn, TOut> fn)
=> fn(a);
public static TOut Pipe<TIn, TOut>(this TIn a, PipeFunc<TIn, TOut> fn)
=> fn(a);
public static TOut Pipe<TIn1, TIn2, TOut>(this (TIn1, TIn2) a, PipeFunc<TIn1, TIn2, TOut> fn)
=> fn(a.Item1, a.Item2);
public static (TIn, TExtra) With<TIn, TExtra>(this TIn a, TExtra b)
=> (a, b);
public static async Task<TOut> Pipe<TIn, TOut>(this Task<TIn> a, Func<TIn, TOut> fn)
=> fn(await a);
}

View File

@@ -0,0 +1,99 @@
namespace NadekoBot.Extensions;
public static class SocketMessageComponentExtensions
{
public static Task RespondAsync(
this SocketMessageComponent smc,
string? plainText,
Embed? embed = null,
IReadOnlyCollection<Embed>? embeds = null,
bool sanitizeAll = false,
MessageComponent? components = null,
bool ephemeral = true)
{
plainText = sanitizeAll
? plainText?.SanitizeAllMentions() ?? ""
: plainText?.SanitizeMentions() ?? "";
return smc.RespondAsync(plainText,
embed: embed,
embeds: embeds is null
? null
: embeds as Embed[] ?? embeds.ToArray(),
components: components,
ephemeral: ephemeral,
options: new()
{
RetryMode = RetryMode.AlwaysRetry
});
}
public static Task RespondAsync(
this SocketMessageComponent smc,
SmartText text,
bool sanitizeAll = false,
bool ephemeral = true)
=> text switch
{
SmartEmbedText set => smc.RespondAsync(set.PlainText,
set.GetEmbed().Build(),
sanitizeAll: sanitizeAll,
ephemeral: ephemeral),
SmartPlainText st => smc.RespondAsync(st.Text,
default(Embed),
sanitizeAll: sanitizeAll,
ephemeral: ephemeral),
SmartEmbedTextArray arr => smc.RespondAsync(arr.Content,
embeds: arr.GetEmbedBuilders().Map(e => e.Build()),
ephemeral: ephemeral),
_ => throw new ArgumentOutOfRangeException(nameof(text))
};
public static Task EmbedAsync(
this SocketMessageComponent smc,
IEmbedBuilder? embed,
string plainText = "",
IReadOnlyCollection<IEmbedBuilder>? embeds = null,
NadekoInteraction? inter = null,
bool ephemeral = false)
=> smc.RespondAsync(plainText,
embed: embed?.Build(),
embeds: embeds?.Map(x => x.Build()));
public static Task RespondAsync(
this SocketMessageComponent ch,
IEmbedBuilderService eb,
string text,
MessageType type,
bool ephemeral = false,
NadekoInteraction? inter = null)
{
var builder = eb.Create().WithDescription(text);
builder = (type switch
{
MessageType.Error => builder.WithErrorColor(),
MessageType.Ok => builder.WithOkColor(),
MessageType.Pending => builder.WithPendingColor(),
_ => throw new ArgumentOutOfRangeException(nameof(type))
});
return ch.EmbedAsync(builder, inter: inter, ephemeral: ephemeral);
}
// embed title and optional footer overloads
public static Task RespondErrorAsync(
this SocketMessageComponent smc,
IEmbedBuilderService eb,
string text,
bool ephemeral = false)
=> smc.RespondAsync(eb, text, MessageType.Error, ephemeral);
public static Task RespondConfirmAsync(
this SocketMessageComponent smc,
IEmbedBuilderService eb,
string text,
bool ephemeral = false)
=> smc.RespondAsync(eb, text, MessageType.Ok, ephemeral);
}

View File

@@ -518,6 +518,9 @@ streamoffline:
- streamoffline
- sto
- stoff
streamonlinedelete:
- streamonlinedelete
- stondel
streammessage:
- streammsg
- stm
@@ -1161,15 +1164,21 @@ pathofexilecurrency:
- poec
rollduel:
- rollduel
reactionroles:
- reactionroles
- rero
reactionroleadd:
- reactionroleadd
- reroa
reactionroleslist:
- reactionroleslist
- reroli
reactionrolesremove:
- reactionrolesremove
- rerorm
reactionrolesdeleteall:
- rerodeleteall
- rerodela
reactionrolestransfer:
- rerotransfer
- rerot
blackjack:
- blackjack
- bj
@@ -1282,4 +1291,16 @@ medusalist:
- melist
medusainfo:
- medusainfo
- meinfo
- meinfo
bankdeposit:
- deposit
- d
- dep
bankwithdraw:
- withdraw
- w
- with
bankbalance:
- balance
- b
- bal

View File

@@ -2,4 +2,3 @@
version: 1
# List of medusae automatically loaded at startup
loaded:
- uwu

View File

@@ -912,6 +912,10 @@ streamoffline:
desc: "Toggles whether the bot will also notify when added streams go offline."
args:
- ""
streamonlinedelete:
desc: "Toggles whether the bot will delete stream online message when the stream goes offline."
args:
- ""
streammessage:
desc: "Sets the message which will show when the stream on the specified index comes online. You can use %user% and %platform% placeholders."
args:
@@ -2064,21 +2068,33 @@ rollduel:
args:
- "50 @Someone"
- "@Challenger"
reactionroles:
desc: "Specify role names and server emojis with which they're represented, the bot will then add those emojis to the previous message in the channel, and users will be able to get the roles by clicking on the emoji. You can set 'excl' as the parameter before the reactions and roles to make them exclusive. You can have up to 10 of these enabled on one server at a time. Optionally you can specify target message if you don't want it to be the previous one."
reactionroleadd:
desc: |-
Specify a message id, emote and a role name to have the bot assign the specified role to the user who reacts to the specified message (in this channel) with the specified emoji.
You can optionally specify an exclusivity group. Default is group 0 which is non-exclusive. Other groups are exclusive. Exclusive groups will let the user only have one of the roles specified in that group.
You can optionally specify a level requirement after a group. Users who don't meet the level requirement will not receive the role.
You can have up to 50 reaction roles per server in total.
args:
- "Gamer :SomeServerEmoji: Streamer :Other: Watcher :Other2:"
- "excl Horde :Horde: Alliance :Alliance:"
- "886382471732662332 excl Horde :Horde: Alliance :Alliance:"
- "886382471732662332 Gamer :SomeServerEmoji: Streamer :Other: Watcher :Other2:"
- 971276352684691466 😊 gamer
- 971276352684691466 😢 emo 1
- 971276352684691466 🤔 philosopher 5 20
- 971276352684691466 👨 normie 5 20
reactionroleslist:
desc: "Lists all ReactionRole messages on this channel and their indexes."
desc: "Lists all ReactionRole messages on this server with their message ids. Clicking/Tapping message ids will send you to that message."
args:
- ""
reactionrolesremove:
desc: "Removed a ReactionRole message on the specified index."
desc: "Remove all reaction roles from message specified by the id"
args:
- "1"
- "971276352684691466"
reactionrolesdeleteall:
desc: "Deletes all reaction roles on the server. This action is irreversible."
args:
- ""
reactionrolestransfer:
desc: "Transfers reaction roles from one message to another by specifying their ids. If the target message has reaction roles specified already, the reaction roles will be MERGED, not overwritten."
args:
- "971276352684691466 971427748448964628"
blackjack:
desc: "Start or join a blackjack game. You must specify the amount you're betting. Use `{0}hit`, `{0}stand` and `{0}double` commands to play. Game is played with 4 decks. Dealer hits on soft 17 and wins draws."
args:
@@ -2181,4 +2197,15 @@ medusalist:
Read about the medusa system [here](https://nadekobot.readthedocs.io/en/latest/medusa/creating-a-medusa/)
args:
- ""
bankdeposit:
desc: "Deposits the specified amount of currency into the bank for later use."
args:
- "50"
bankwithdraw:
desc: "Withdraws the specified amount of currency from the bank if available."
args:
- "49"
bankbalance:
desc: "Shows your current bank balance available for withdrawal."
args:
- ""

View File

@@ -519,6 +519,8 @@
"stream_no": "No such stream.",
"stream_off_enabled": "Stream notifications will now show when a stream goes offline.",
"stream_off_disabled": "Stream notifications will no longer show when a stream goes offline.",
"stream_online_delete_enabled": "Online stream notifications will now be deleted when the stream goes offline.",
"stream_online_delete_disabled": "Online stream notifications will no longer be deleted when the stream goes offline.",
"stream_not_added": "Stream was not added. Either stream doesn't exist, that platform is unsupported, or you've reached the maximum number of streams allowed.",
"stream_message_reset": "Announcement message for {0} stream has been reset.",
"stream_message_set": "Announcement message when {0} stream goes online has been set.",
@@ -937,6 +939,7 @@
"owned_waifus_total": "Total value of owned waifus",
"bot_currency": "Currency owned by the bot",
"total": "Total",
"bank_accounts": "Bank Accounts",
"no_invites": "No invites on this page.",
"invite_deleted": "Invite {0} has been deleted.",
"group_name_added": "Group #{0} now has a name: {1}",
@@ -958,6 +961,7 @@
"module_description_permissions": "Setup perms for commands, filter words and set up command cooldowns",
"module_description_searches": "Search for jokes, images of animals, anime and manga",
"module_description_xp": "Gain xp based on chat activity, check users' xp cards",
"module_description_medusa": "**Bot Owner only.** Load, unload and handle dynamic modules. Read more [here](https://nadekobot.readthedocs.io/en/latest/medusa/creating-a-medusa/)",
"module_description_missing": "Description is missing for this module.",
"obsolete_use": "⚠ Obsolete, use {0} instead.",
"purge_user_confirm": "Are you sure that you want to purge {0} from the database?",
@@ -990,5 +994,10 @@
"medusa_unloaded": "Medusa {0} has been unloaded.",
"medusa_empty": "Medusa wasn't loaded as it didn't contain any Sneks.",
"medusa_already_loaded": "Medusa {0} is already loaded",
"medusa_invalid_not_found": "Medusa with that name wasn't found or the file was invalid"
"medusa_invalid_not_found": "Medusa with that name wasn't found or the file was invalid",
"bank_balance": "You have {0} in your bank account.",
"bank_deposited": "You deposited {0} to your bank account.",
"bank_withdrew": "You withdrew {0} from your bank account.",
"bank_withdraw_insuff": "You don't have sufficient {0} in your bank account.",
"cmd_group_commands": "'{0}' command group"
}