Added uncommited files. Fixed nullref in update loop when users gain voice xp

This commit is contained in:
Kwoth
2022-07-18 04:44:27 +02:00
parent 99c60459f8
commit b12e97a0a7
7 changed files with 358 additions and 350 deletions

View File

@@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="NonBlocking" Version="2.0.0" /> <PackageReference Include="NonBlocking" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
</ItemGroup> </ItemGroup>

View File

@@ -9,7 +9,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.45.0" /> <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
<PackageReference Include="Serilog" Version="2.11.0" /> <PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />

View File

@@ -1,336 +1,336 @@
#nullable enable // #nullable enable
using System; // using System;
using System.CodeDom.Compiler; // using System.CodeDom.Compiler;
using System.Collections.Generic; // using System.Collections.Generic;
using System.Collections.Immutable; // using System.Collections.Immutable;
using System.Collections.ObjectModel; // using System.Collections.ObjectModel;
using System.Diagnostics; // using System.Diagnostics;
using System.IO; // using System.IO;
using System.Linq; // using System.Linq;
using System.Text; // using System.Text;
using System.Threading; // using System.Threading;
using Microsoft.CodeAnalysis; // using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; // using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax; // using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text; // using Microsoft.CodeAnalysis.Text;
//
namespace NadekoBot.Generators.Command; // namespace NadekoBot.Generators.Command;
//
[Generator] // [Generator]
public class CommandAttributesGenerator : IIncrementalGenerator // public class CommandAttributesGenerator : IIncrementalGenerator
{ // {
public const string ATTRIBUTE = @"// <AutoGenerated /> // public const string ATTRIBUTE = @"// <AutoGenerated />
//
namespace NadekoBot.Common; // namespace NadekoBot.Common;
//
[System.AttributeUsage(System.AttributeTargets.Method)] // [System.AttributeUsage(System.AttributeTargets.Method)]
public class CmdAttribute : System.Attribute // public class CmdAttribute : System.Attribute
{ // {
//
}"; // }";
//
public class MethodModel // public class MethodModel
{ // {
public string? Namespace { get; } // public string? Namespace { get; }
public IReadOnlyCollection<string> Classes { get; } // public IReadOnlyCollection<string> Classes { get; }
public string ReturnType { get; } // public string ReturnType { get; }
public string MethodName { get; } // public string MethodName { get; }
public IEnumerable<string> Params { get; } // public IEnumerable<string> Params { get; }
//
public MethodModel(string? ns, IReadOnlyCollection<string> classes, string returnType, string methodName, IEnumerable<string> @params) // public MethodModel(string? ns, IReadOnlyCollection<string> classes, string returnType, string methodName, IEnumerable<string> @params)
{ // {
Namespace = ns; // Namespace = ns;
Classes = classes; // Classes = classes;
ReturnType = returnType; // ReturnType = returnType;
MethodName = methodName; // MethodName = methodName;
Params = @params; // Params = @params;
} // }
} // }
//
public class FileModel // public class FileModel
{ // {
public string? Namespace { get; } // public string? Namespace { get; }
public IReadOnlyCollection<string> ClassHierarchy { get; } // public IReadOnlyCollection<string> ClassHierarchy { get; }
public IReadOnlyCollection<MethodModel> Methods { get; } // public IReadOnlyCollection<MethodModel> Methods { get; }
//
public FileModel(string? ns, IReadOnlyCollection<string> classHierarchy, IReadOnlyCollection<MethodModel> methods) // public FileModel(string? ns, IReadOnlyCollection<string> classHierarchy, IReadOnlyCollection<MethodModel> methods)
{ // {
Namespace = ns; // Namespace = ns;
ClassHierarchy = classHierarchy; // ClassHierarchy = classHierarchy;
Methods = methods; // Methods = methods;
} // }
} // }
//
public void Initialize(IncrementalGeneratorInitializationContext context) // public void Initialize(IncrementalGeneratorInitializationContext context)
{ // {
// #if DEBUG // // #if DEBUG
// if (!Debugger.IsAttached) // // if (!Debugger.IsAttached)
// Debugger.Launch(); // // Debugger.Launch();
// // SpinWait.SpinUntil(() => Debugger.IsAttached); // // // SpinWait.SpinUntil(() => Debugger.IsAttached);
// #endif // // #endif
context.RegisterPostInitializationOutput(static ctx => ctx.AddSource( // context.RegisterPostInitializationOutput(static ctx => ctx.AddSource(
"CmdAttribute.g.cs", // "CmdAttribute.g.cs",
SourceText.From(ATTRIBUTE, Encoding.UTF8))); // SourceText.From(ATTRIBUTE, Encoding.UTF8)));
//
var methods = context.SyntaxProvider // var methods = context.SyntaxProvider
.CreateSyntaxProvider( // .CreateSyntaxProvider(
static (node, _) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 }, // static (node, _) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 },
static (ctx, cancel) => Transform(ctx, cancel)) // static (ctx, cancel) => Transform(ctx, cancel))
.Where(static m => m is not null) // .Where(static m => m is not null)
.Where(static m => m?.ChildTokens().Any(static x => x.IsKind(SyntaxKind.PublicKeyword)) ?? false); // .Where(static m => m?.ChildTokens().Any(static x => x.IsKind(SyntaxKind.PublicKeyword)) ?? false);
//
var compilationMethods = context.CompilationProvider.Combine(methods.Collect()); // var compilationMethods = context.CompilationProvider.Combine(methods.Collect());
//
context.RegisterSourceOutput(compilationMethods, // context.RegisterSourceOutput(compilationMethods,
static (ctx, tuple) => RegisterAction(in ctx, tuple.Left, in tuple.Right)); // static (ctx, tuple) => RegisterAction(in ctx, tuple.Left, in tuple.Right));
} // }
//
private static void RegisterAction(in SourceProductionContext ctx, // private static void RegisterAction(in SourceProductionContext ctx,
Compilation comp, // Compilation comp,
in ImmutableArray<MethodDeclarationSyntax?> methods) // in ImmutableArray<MethodDeclarationSyntax?> methods)
{ // {
if (methods is { IsDefaultOrEmpty: true }) // if (methods is { IsDefaultOrEmpty: true })
return; // return;
//
var models = GetModels(comp, methods, ctx.CancellationToken); // var models = GetModels(comp, methods, ctx.CancellationToken);
//
foreach (var model in models) // foreach (var model in models)
{ // {
var name = $"{model.Namespace}.{string.Join(".", model.ClassHierarchy)}.g.cs"; // var name = $"{model.Namespace}.{string.Join(".", model.ClassHierarchy)}.g.cs";
try // try
{ // {
var source = GetSourceText(model); // var source = GetSourceText(model);
ctx.AddSource(name, SourceText.From(source, Encoding.UTF8)); // ctx.AddSource(name, SourceText.From(source, Encoding.UTF8));
} // }
catch (Exception ex) // catch (Exception ex)
{ // {
Console.WriteLine($"Error writing source file {name}\n" + ex); // Console.WriteLine($"Error writing source file {name}\n" + ex);
} // }
} // }
} // }
//
private static string GetSourceText(FileModel model) // private static string GetSourceText(FileModel model)
{ // {
using var sw = new StringWriter(); // using var sw = new StringWriter();
using var tw = new IndentedTextWriter(sw); // using var tw = new IndentedTextWriter(sw);
//
tw.WriteLine("// <AutoGenerated />"); // tw.WriteLine("// <AutoGenerated />");
tw.WriteLine("#pragma warning disable CS1066"); // tw.WriteLine("#pragma warning disable CS1066");
//
if (model.Namespace is not null) // if (model.Namespace is not null)
{ // {
tw.WriteLine($"namespace {model.Namespace};"); // tw.WriteLine($"namespace {model.Namespace};");
tw.WriteLine(); // tw.WriteLine();
} // }
//
foreach (var className in model.ClassHierarchy) // foreach (var className in model.ClassHierarchy)
{ // {
tw.WriteLine($"public partial class {className}"); // tw.WriteLine($"public partial class {className}");
tw.WriteLine("{"); // tw.WriteLine("{");
tw.Indent ++; // tw.Indent ++;
} // }
//
foreach (var method in model.Methods) // foreach (var method in model.Methods)
{ // {
tw.WriteLine("[NadekoCommand]"); // tw.WriteLine("[NadekoCommand]");
tw.WriteLine("[NadekoDescription]"); // tw.WriteLine("[NadekoDescription]");
tw.WriteLine("[Aliases]"); // tw.WriteLine("[Aliases]");
tw.WriteLine($"public partial {method.ReturnType} {method.MethodName}({string.Join(", ", method.Params)});"); // tw.WriteLine($"public partial {method.ReturnType} {method.MethodName}({string.Join(", ", method.Params)});");
} // }
//
foreach (var _ in model.ClassHierarchy) // foreach (var _ in model.ClassHierarchy)
{ // {
tw.Indent --; // tw.Indent --;
tw.WriteLine("}"); // tw.WriteLine("}");
} // }
//
tw.Flush(); // tw.Flush();
return sw.ToString(); // return sw.ToString();
} // }
//
private static IReadOnlyCollection<FileModel> GetModels(Compilation compilation, // private static IReadOnlyCollection<FileModel> GetModels(Compilation compilation,
in ImmutableArray<MethodDeclarationSyntax?> inputMethods, // in ImmutableArray<MethodDeclarationSyntax?> inputMethods,
CancellationToken cancel) // CancellationToken cancel)
{ // {
var models = new List<FileModel>(); // var models = new List<FileModel>();
//
var methods = inputMethods // var methods = inputMethods
.Where(static x => x is not null) // .Where(static x => x is not null)
.Distinct(); // .Distinct();
//
var methodModels = methods // var methodModels = methods
.Select(x => MethodDeclarationToMethodModel(compilation, x!)) // .Select(x => MethodDeclarationToMethodModel(compilation, x!))
.Where(static x => x is not null) // .Where(static x => x is not null)
.Cast<MethodModel>(); // .Cast<MethodModel>();
//
var groups = methodModels // var groups = methodModels
.GroupBy(static x => $"{x.Namespace}.{string.Join(".", x.Classes)}"); // .GroupBy(static x => $"{x.Namespace}.{string.Join(".", x.Classes)}");
//
foreach (var group in groups) // foreach (var group in groups)
{ // {
if (cancel.IsCancellationRequested) // if (cancel.IsCancellationRequested)
return new Collection<FileModel>(); // return new Collection<FileModel>();
//
if (group is null) // if (group is null)
continue; // continue;
//
var elems = group.ToList(); // var elems = group.ToList();
if (elems.Count is 0) // if (elems.Count is 0)
continue; // continue;
//
var model = new FileModel( // var model = new FileModel(
methods: elems, // methods: elems,
ns: elems[0].Namespace, // ns: elems[0].Namespace,
classHierarchy: elems![0].Classes // classHierarchy: elems![0].Classes
); // );
//
models.Add(model); // models.Add(model);
} // }
//
//
return models; // return models;
} // }
//
private static MethodModel? MethodDeclarationToMethodModel(Compilation comp, MethodDeclarationSyntax decl) // private static MethodModel? MethodDeclarationToMethodModel(Compilation comp, MethodDeclarationSyntax decl)
{ // {
// SpinWait.SpinUntil(static () => Debugger.IsAttached); // // SpinWait.SpinUntil(static () => Debugger.IsAttached);
//
SemanticModel semanticModel; // SemanticModel semanticModel;
try // try
{ // {
semanticModel = comp.GetSemanticModel(decl.SyntaxTree); // semanticModel = comp.GetSemanticModel(decl.SyntaxTree);
} // }
catch // catch
{ // {
// for some reason this method can throw "Not part of this compilation" argument exception // // for some reason this method can throw "Not part of this compilation" argument exception
return null; // return null;
} // }
//
var methodModel = new MethodModel( // var methodModel = new MethodModel(
@params: decl.ParameterList.Parameters // @params: decl.ParameterList.Parameters
.Where(p => p.Type is not null) // .Where(p => p.Type is not null)
.Select(p => // .Select(p =>
{ // {
var prefix = p.Modifiers.Any(static x => x.IsKind(SyntaxKind.ParamsKeyword)) // var prefix = p.Modifiers.Any(static x => x.IsKind(SyntaxKind.ParamsKeyword))
? "params " // ? "params "
: string.Empty; // : string.Empty;
//
var type = semanticModel // var type = semanticModel
.GetTypeInfo(p.Type!) // .GetTypeInfo(p.Type!)
.Type // .Type
?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); // ?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
//
//
var name = p.Identifier.Text; // var name = p.Identifier.Text;
//
var suffix = string.Empty; // var suffix = string.Empty;
if (p.Default is not null) // if (p.Default is not null)
{ // {
if (p.Default.Value is LiteralExpressionSyntax) // if (p.Default.Value is LiteralExpressionSyntax)
{ // {
suffix = " = " + p.Default.Value; // suffix = " = " + p.Default.Value;
} // }
else if (p.Default.Value is MemberAccessExpressionSyntax maes) // else if (p.Default.Value is MemberAccessExpressionSyntax maes)
{ // {
var maesSemModel = comp.GetSemanticModel(maes.SyntaxTree); // var maesSemModel = comp.GetSemanticModel(maes.SyntaxTree);
var sym = maesSemModel.GetSymbolInfo(maes.Name); // var sym = maesSemModel.GetSymbolInfo(maes.Name);
if (sym.Symbol is null) // if (sym.Symbol is null)
{ // {
suffix = " = " + p.Default.Value; // suffix = " = " + p.Default.Value;
} // }
else // else
{ // {
suffix = " = " + sym.Symbol.ToDisplayString(); // suffix = " = " + sym.Symbol.ToDisplayString();
} // }
} // }
} // }
//
return $"{prefix}{type} {name}{suffix}"; // return $"{prefix}{type} {name}{suffix}";
}) // })
.ToList(), // .ToList(),
methodName: decl.Identifier.Text, // methodName: decl.Identifier.Text,
returnType: decl.ReturnType.ToString(), // returnType: decl.ReturnType.ToString(),
ns: GetNamespace(decl), // ns: GetNamespace(decl),
classes: GetClasses(decl) // classes: GetClasses(decl)
); // );
//
return methodModel; // return methodModel;
} // }
//
//https://github.com/andrewlock/NetEscapades.EnumGenerators/blob/main/src/NetEscapades.EnumGenerators/EnumGenerator.cs // //https://github.com/andrewlock/NetEscapades.EnumGenerators/blob/main/src/NetEscapades.EnumGenerators/EnumGenerator.cs
static string? GetNamespace(MethodDeclarationSyntax declarationSyntax) // static string? GetNamespace(MethodDeclarationSyntax declarationSyntax)
{ // {
// determine the namespace the class is declared in, if any // // determine the namespace the class is declared in, if any
string? nameSpace = null; // string? nameSpace = null;
var parentOfInterest = declarationSyntax.Parent; // var parentOfInterest = declarationSyntax.Parent;
while (parentOfInterest is not null) // while (parentOfInterest is not null)
{ // {
parentOfInterest = parentOfInterest.Parent; // parentOfInterest = parentOfInterest.Parent;
//
if (parentOfInterest is BaseNamespaceDeclarationSyntax ns) // if (parentOfInterest is BaseNamespaceDeclarationSyntax ns)
{ // {
nameSpace = ns.Name.ToString(); // nameSpace = ns.Name.ToString();
while (true) // while (true)
{ // {
if (ns.Parent is not NamespaceDeclarationSyntax parent) // if (ns.Parent is not NamespaceDeclarationSyntax parent)
{ // {
break; // break;
} // }
//
ns = parent; // ns = parent;
nameSpace = $"{ns.Name}.{nameSpace}"; // nameSpace = $"{ns.Name}.{nameSpace}";
} // }
//
return nameSpace; // return nameSpace;
} // }
//
} // }
//
return nameSpace; // return nameSpace;
} // }
//
static IReadOnlyCollection<string> GetClasses(MethodDeclarationSyntax declarationSyntax) // static IReadOnlyCollection<string> GetClasses(MethodDeclarationSyntax declarationSyntax)
{ // {
// determine the namespace the class is declared in, if any // // determine the namespace the class is declared in, if any
var classes = new LinkedList<string>(); // var classes = new LinkedList<string>();
var parentOfInterest = declarationSyntax.Parent; // var parentOfInterest = declarationSyntax.Parent;
while (parentOfInterest is not null) // while (parentOfInterest is not null)
{ // {
if (parentOfInterest is ClassDeclarationSyntax cds) // if (parentOfInterest is ClassDeclarationSyntax cds)
{ // {
classes.AddFirst(cds.Identifier.ToString()); // classes.AddFirst(cds.Identifier.ToString());
} // }
//
parentOfInterest = parentOfInterest.Parent; // parentOfInterest = parentOfInterest.Parent;
} // }
//
Debug.WriteLine($"Method {declarationSyntax.Identifier.Text} has {classes.Count} classes"); // Debug.WriteLine($"Method {declarationSyntax.Identifier.Text} has {classes.Count} classes");
//
return classes; // return classes;
} // }
//
private static MethodDeclarationSyntax? Transform(GeneratorSyntaxContext ctx, CancellationToken cancel) // private static MethodDeclarationSyntax? Transform(GeneratorSyntaxContext ctx, CancellationToken cancel)
{ // {
var methodDecl = ctx.Node as MethodDeclarationSyntax; // var methodDecl = ctx.Node as MethodDeclarationSyntax;
if (methodDecl is null) // if (methodDecl is null)
return default; // return default;
//
foreach (var attListSyntax in methodDecl.AttributeLists) // foreach (var attListSyntax in methodDecl.AttributeLists)
{ // {
foreach (var attSyntax in attListSyntax.Attributes) // foreach (var attSyntax in attListSyntax.Attributes)
{ // {
if (cancel.IsCancellationRequested) // if (cancel.IsCancellationRequested)
return default; // return default;
//
var symbol = ctx.SemanticModel.GetSymbolInfo(attSyntax).Symbol; // var symbol = ctx.SemanticModel.GetSymbolInfo(attSyntax).Symbol;
if (symbol is not IMethodSymbol attSymbol) // if (symbol is not IMethodSymbol attSymbol)
continue; // continue;
//
if (attSymbol.ContainingType.ToDisplayString() == "NadekoBot.Common.CmdAttribute") // if (attSymbol.ContainingType.ToDisplayString() == "NadekoBot.Common.CmdAttribute")
return methodDecl; // return methodDecl;
} // }
} // }
//
return default; // return default;
} // }
} // }

View File

@@ -47,7 +47,7 @@ namespace NadekoBot.Tests
|| !(type.GetCustomAttribute<GroupAttribute>(true) is null)) // or a submodule || !(type.GetCustomAttribute<GroupAttribute>(true) is null)) // or a submodule
.SelectMany(x => x.GetMethods() .SelectMany(x => x.GetMethods()
.Where(mi => mi.CustomAttributes .Where(mi => mi.CustomAttributes
.Any(ca => ca.AttributeType == typeof(NadekoCommandAttribute)))) .Any(ca => ca.AttributeType == typeof(CmdAttribute))))
.Select(x => x.Name.ToLowerInvariant()) .Select(x => x.Name.ToLowerInvariant())
.ToArray(); .ToArray();

View File

@@ -7,7 +7,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="MorseCode.ITask" Version="2.0.3" /> <PackageReference Include="MorseCode.ITask" Version="2.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,4 +1,3 @@
#nullable disable warnings
using LinqToDB; using LinqToDB;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Common.ModuleBehaviors;
@@ -342,7 +341,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
} }
else // channel else // channel
{ {
await ch.SendConfirmAsync(_eb, await ch?.SendConfirmAsync(_eb,
_strings.GetText(strs.level_up_channel(user.Mention, _strings.GetText(strs.level_up_channel(user.Mention,
Format.Bold(newLevel.ToString())), Format.Bold(newLevel.ToString())),
guild.Id)); guild.Id));
@@ -494,7 +493,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
{ {
Level = level, Level = level,
RoleId = roleId, RoleId = roleId,
Remove = remove Remove = remove,
}); });
} }
@@ -654,6 +653,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
Guild = channel.Guild, Guild = channel.Guild,
User = user, User = user,
XpAmount = actualXp, XpAmount = actualXp,
Channel = channel
}); });
} }
} }
@@ -862,7 +862,15 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
public Task<(Stream Image, IImageFormat Format)> GenerateXpImageAsync(FullUserStats stats) public Task<(Stream Image, IImageFormat Format)> GenerateXpImageAsync(FullUserStats stats)
=> Task.Run(async () => => Task.Run(async () =>
{ {
using var img = Image.Load<Rgba32>(await GetXpBackgroundAsync(stats.User.UserId), out var imageFormat); var bgBytes = await GetXpBackgroundAsync(stats.User.UserId);
if (bgBytes is null)
{
Log.Warning("Xp background image could not be loaded");
throw new ArgumentNullException(nameof(bgBytes));
}
using var img = Image.Load<Rgba32>(bgBytes, out var imageFormat);
if (template.User.Name.Show) if (template.User.Name.Show)
{ {
var fontSize = (int)(template.User.Name.FontSize * 0.9); var fontSize = (int)(template.User.Name.FontSize * 0.9);
@@ -1113,7 +1121,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
return output; return output;
}); });
private async Task<byte[]> GetXpBackgroundAsync(ulong userId) private async Task<byte[]?> GetXpBackgroundAsync(ulong _)
{ {
var img = await _images.GetXpBackgroundImageAsync(); var img = await _images.GetXpBackgroundImageAsync();
return img; return img;
@@ -1123,7 +1131,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
private async Task DrawFrame(Image<Rgba32> img, ulong userId) private async Task DrawFrame(Image<Rgba32> img, ulong userId)
{ {
var patron = await _ps.GetPatronAsync(userId); var patron = await _ps.GetPatronAsync(userId);
Image frame = null; Image? frame = null;
if (patron.Tier == PatronTier.V) if (patron.Tier == PatronTier.V)
frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_silver.png")); frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_silver.png"));
else if (patron.Tier >= PatronTier.X || _creds.IsOwner(userId)) else if (patron.Tier >= PatronTier.X || _creds.IsOwner(userId))

View File

@@ -1,4 +1,4 @@
#nullable disable #nullable disable warnings
using Cloneable; using Cloneable;
namespace NadekoBot.Modules.Xp.Services; namespace NadekoBot.Modules.Xp.Services;
@@ -6,8 +6,8 @@ namespace NadekoBot.Modules.Xp.Services;
[Cloneable] [Cloneable]
public sealed partial class UserXpGainData : ICloneable<UserXpGainData> public sealed partial class UserXpGainData : ICloneable<UserXpGainData>
{ {
public IGuildUser User { get; set; } public IGuildUser User { get; init; }
public IGuild Guild { get; set; } public IGuild Guild { get; init; }
public IMessageChannel Channel { get; set; } public IMessageChannel Channel { get; init; }
public int XpAmount { get; set; } public int XpAmount { get; set; }
} }