diff --git a/README.md b/README.md index e9cc216..9ba9a81 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,79 @@ -# Tarscord +# Tarscord 🤖 -## Summary -Tarscord is a sarcastic discord bot that can help you manage your discord server. Some of the features that Tarscord supports are: +Named after TARS from Interstellar, Tarscord is a sarcastic Discord bot designed to help you manage your server with a touch of humor. -* Mute/Unmute members -* Add event and save them to a local database -* Generate random numbers -* Set reminders -* Others will be added soon... +✨ Features + • Moderation Tools: Mute and unmute members seamlessly. + • Event Management: Add events and save them to a local database for easy tracking. + • Random Number Generation: Generate random numbers for games or decision-making. + • Reminders: Set reminders to keep your community engaged and informed. + • More to Come: Continuous updates with new and exciting features. +📦 Installation -## Getting started -Create a new application inside the developer portal in Discord, then paste the Token Id inside the ```config.yml``` file and run the Tarscord. +1️⃣ Prerequisites + • .NET SDK: Ensure you have the latest version installed. You can download it from the .NET official website. + +2️⃣ Clone the Repository + +git clone https://github.com/armendu/Tarscord.git +cd Tarscord + +3️⃣ Configure the Bot + 1. Create a Discord Application: + • Navigate to the Discord Developer Portal. + • Click on “New Application” and provide a name. + • In the “Bot” section, add a new bot to your application. + • Copy the Token provided for the bot. + 2. Set Up Configuration: + • In the src directory, locate the config.yml file. + • Replace the placeholder TokenId with the token you copied: + +TokenId: "YOUR_DISCORD_BOT_TOKEN" + + + +4️⃣ Build and Run + +Navigate to the src directory and execute: + +dotnet build +dotnet run + +🔧 Configuration + • config.yml: Located in the src directory, this file contains essential configurations: + • TokenId: Your Discord bot token. + • Other settings can be adjusted as needed to customize Tarscord’s behavior. + +💡 Usage + +Once Tarscord is up and running: + 1. Invite the Bot to Your Server: + • Generate an OAuth2 URL in the Discord Developer Portal with the necessary permissions. + • Use the URL to invite Tarscord to your server. + 2. Interact with Tarscord: + • Use commands such as /mute, /unmute, /addevent, /random, and /remind to utilize its features. + • Tarscord responds with a unique, sarcastic flair to keep interactions entertaining. + +🤝 Contributing + +We welcome contributions! To get involved: + 1. Fork the repository. + 2. Create a new branch (feature/your-feature). + 3. Commit your changes. + 4. Push the branch and create a Pull Request. + +Please ensure your code adheres to the project’s coding standards and includes appropriate tests. + +📜 License + +Tarscord is licensed under the MIT License. For more details, refer to the LICENSE file. + +⭐ Support & Community + +If you find Tarscord useful or entertaining: + • Star the Repository: Show your support by starring the project on GitHub! + • Report Issues: Encountered a bug or have a feature request? Open an issue. + • Stay Updated: Watch the repository for updates and new features. + +Elevate your Discord server management with Tarscord’s unique blend of functionality and humor. \ No newline at end of file diff --git a/src/Tarscord.Core/Extensions/UserExtensions.cs b/src/Tarscord.Core/Extensions/UserExtensions.cs index 535f7f5..69f974c 100644 --- a/src/Tarscord.Core/Extensions/UserExtensions.cs +++ b/src/Tarscord.Core/Extensions/UserExtensions.cs @@ -1,6 +1,4 @@ using Discord; -using System.Collections.Generic; -using System.Linq; using Tarscord.Core.Persistence.Entities; namespace Tarscord.Core.Extensions; diff --git a/src/Tarscord.Core/Features/Loans/Create.cs b/src/Tarscord.Core/Features/Loans/Create.cs index 1ac311e..845452f 100644 --- a/src/Tarscord.Core/Features/Loans/Create.cs +++ b/src/Tarscord.Core/Features/Loans/Create.cs @@ -23,35 +23,27 @@ public class Command : IRequest>, IPerforme public required string PerformedByUser { get; set; } } - public class CreateLoanCommandValidator : AbstractValidator + public class CommandValidator : AbstractValidator { - public CreateLoanCommandValidator() + public CommandValidator() { - // TODO: Add proper validation - // RuleFor(x => x.Loan).NotNull(); + RuleFor(x => x.Amount).GreaterThan(0); } } - public class CommandHandler : IRequestHandler> + public class CommandHandler( + ILogger logger, + TarscordContext context, + TimeProvider timeProvider) + : IRequestHandler> { - private readonly ILogger _logger; - private readonly TarscordContext _context; - private readonly TimeProvider _timeProvider; - - public CommandHandler( - ILogger logger, - TarscordContext context, - TimeProvider timeProvider) - { - _logger = logger; - _context = context; - _timeProvider = timeProvider; - } - public async Task> Handle(Command command, CancellationToken cancellationToken) { - var createdLoan = await _context.AddAsync(new Loan + logger.LogInformation("Command {Command} executed by {PerformedByUser}", + nameof(Command), command.PerformedByUser); + + var createdLoan = await context.AddAsync(new Loan { LoanedFrom = command.LoanedFrom, LoanedFromId = command.LoanedFromId, @@ -61,10 +53,10 @@ public async Task> Handle(Command command, AmountLoaned = command.Amount, AmountPayed = 0, Confirmed = false, - Created = _timeProvider.GetUtcNow().UtcDateTime + Created = timeProvider.GetUtcNow().UtcDateTime }, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); + await context.SaveChangesAsync(cancellationToken); return LoanEnvelope.FromEntity(createdLoan.Entity); } diff --git a/src/Tarscord.Core/Features/Loans/List.cs b/src/Tarscord.Core/Features/Loans/List.cs index ebb9c41..99d72da 100644 --- a/src/Tarscord.Core/Features/Loans/List.cs +++ b/src/Tarscord.Core/Features/Loans/List.cs @@ -22,7 +22,9 @@ public async Task Handle(Query request, CancellationToken cancella logger.LogInformation("Query {Query} executed by {PerformedByUser}", nameof(List), request.PerformedByUser); - var eventInfos = await context.Loans.ToListAsync(cancellationToken: cancellationToken); + var eventInfos = await context.Loans + .Where(x => x.LoanedFrom == request.PerformedByUser || x.LoanedTo == request.PerformedByUser) + .ToListAsync(cancellationToken: cancellationToken); return new ListResponse(eventInfos.ConvertAll(LoanEnvelope.FromEntity)); } diff --git a/src/Tarscord.Core/Features/Loans/LoanEnvelope.cs b/src/Tarscord.Core/Features/Loans/LoanEnvelope.cs index 1f3a975..9651e1b 100644 --- a/src/Tarscord.Core/Features/Loans/LoanEnvelope.cs +++ b/src/Tarscord.Core/Features/Loans/LoanEnvelope.cs @@ -13,6 +13,8 @@ public class LoanEnvelope public required string LoanedTo { get; init; } public string? Description { get; init; } + public decimal AmountPaid { get; init; } + public static LoanEnvelope FromEntity(Loan loan) { return new LoanEnvelope @@ -22,7 +24,8 @@ public static LoanEnvelope FromEntity(Loan loan) LoanedFromId = loan.LoanedFromId, LoanedTo = loan.LoanedTo, LoanedToId = loan.LoanedToId, - Description = loan.Description + Description = loan.Description, + AmountPaid = loan.AmountPayed }; } diff --git a/src/Tarscord.Core/Features/Loans/Update.cs b/src/Tarscord.Core/Features/Loans/Update.cs new file mode 100644 index 0000000..61bd7b9 --- /dev/null +++ b/src/Tarscord.Core/Features/Loans/Update.cs @@ -0,0 +1,73 @@ +using FluentValidation; +using MediatR; +using OneOf; +using Microsoft.Extensions.Logging; +using Microsoft.EntityFrameworkCore; +using Tarscord.Core.Features.Common; +using Tarscord.Core.Features.Events; +using Tarscord.Core.Persistence; + +namespace Tarscord.Core.Features.Loans; + +internal static class Update +{ + public class Command : IRequest>, IPerformedByUser + { + public decimal Amount { get; set; } + public ulong LoanedFrom { get; set; } + public required string LoanedFromUsername { get; set; } + public ulong LoanedTo { get; set; } + public required string LoanedToUsername { get; set; } + + public required string PerformedByUser { get; set; } + } + + public class CommandValidator : AbstractValidator + { + public CommandValidator() + { + RuleFor(x => x.Amount).GreaterThan(0); + } + } + + public class UpdateLoanCommandHandler( + ILogger logger, + TarscordContext context) + : IRequestHandler> + { + public async Task> Handle(Command request, CancellationToken cancellationToken) + { + logger.LogInformation("Command {Command} executed by {PerformedByUser}", + nameof(Command), request.PerformedByUser); + + // Find the loan between the two users that isn't fully paid yet + var loan = await context.Loans + .LastOrDefaultAsync(x => + x.LoanedFromId == request.LoanedTo && + x.LoanedToId == request.LoanedFrom && + x.AmountPayed < x.AmountLoaned, + cancellationToken); + + if (loan is null) + { + return new FailureResponse("No active loan found between these users."); + } + + // Calculate remaining balance before adding payment + var remainingBalance = loan.AmountLoaned - loan.AmountPayed; + + // Ensure we don't overpay + if (request.Amount > remainingBalance) + { + return new FailureResponse($"Payment of {request.Amount:C} would exceed the remaining balance of {remainingBalance:C}"); + } + + // Add the payment amount to existing amount paid (supports partial payments) + loan.AmountPayed += request.Amount; + + await context.SaveChangesAsync(cancellationToken); + + return LoanEnvelope.FromEntity(loan); + } + } +} \ No newline at end of file diff --git a/src/Tarscord.Core/Features/Loans/UpdateLoanCommand.cs b/src/Tarscord.Core/Features/Loans/UpdateLoanCommand.cs deleted file mode 100644 index 1de14f9..0000000 --- a/src/Tarscord.Core/Features/Loans/UpdateLoanCommand.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FluentValidation; -using MediatR; -using Microsoft.Extensions.Logging; -using Tarscord.Core.Persistence; - -namespace Tarscord.Core.Features.Loans; - -public class UpdateLoanCommand : IRequest -{ - public decimal Amount { get; set; } - public ulong LoanedFrom { get; set; } - public required string LoanedFromUsername { get; set; } - public ulong LoanedTo { get; set; } - public required string LoanedToUsername { get; set; } -} - -public class CommandValidator : AbstractValidator -{ - public CommandValidator() - { - // RuleFor(x => x.Loan).NotNull(); - } -} - -public class UpdateLoanCommandHandler : IRequestHandler -{ - private readonly ILogger _logger; - - public UpdateLoanCommandHandler(ILogger logger) - { - _logger = logger; - } - - // public async Task Handle(UpdateLoanCommandCommand request, CancellationToken cancellationToken) - // { - // var loans = await _loanRepository - // .FindBy(x => x.LoanedFrom == request.Loan.LoanedFrom - // && x.LoanedTo == request.Loan.LoanedTo); - // - // var loanToUpdate = loans?.FirstOrDefault(); - // if (loanToUpdate == null) - // { - // return new LoanEnvelope(null); - // } - // - // loanToUpdate.AmountPayed += request.Loan.Amount; - // loanToUpdate.AmountLoaned -= request.Loan.Amount; - // var updatedLoan = await _loanRepository.UpdateItem(_mapper.Map(loanToUpdate)); - // - // return new LoanDto(); - // } - - public Task Handle(UpdateLoanCommand request, CancellationToken cancellationToken) - { - throw new System.NotImplementedException(); - } -} \ No newline at end of file diff --git a/src/Tarscord.Core/Features/Logging/ProcessLog.cs b/src/Tarscord.Core/Features/Logging/ProcessLog.cs index db6d462..c85c805 100644 --- a/src/Tarscord.Core/Features/Logging/ProcessLog.cs +++ b/src/Tarscord.Core/Features/Logging/ProcessLog.cs @@ -1,7 +1,3 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; using Discord; using MediatR; diff --git a/src/Tarscord.Core/Features/Mute/Mute.cs b/src/Tarscord.Core/Features/Mute/Mute.cs new file mode 100644 index 0000000..c00aa56 --- /dev/null +++ b/src/Tarscord.Core/Features/Mute/Mute.cs @@ -0,0 +1,67 @@ +using Discord; +using MediatR; +using Microsoft.Extensions.Logging; +using Tarscord.Core.Features.Common; + +namespace Tarscord.Core.Features.Mute; + +internal static class Mute +{ + public record Command( + IMessageChannel ContextChannel, + IUser User, + CommandType Action, + string PerformedByUser, + int Minutes = 0) + : IRequest, IPerformedByUser; + + internal sealed class CommandHandler(ILogger logger) : IRequestHandler + { + public async Task Handle(Command request, CancellationToken cancellationToken) + { + logger.LogInformation("Query {Query} executed by {PerformedByUser}", + nameof(Mute.Command), request.PerformedByUser); + + // TODO: After the specified minutes, the user should be un muted. + if (request.ContextChannel is not IGuildChannel channel) return ""; + + OverwritePermissions? possiblePermissions = channel.GetPermissionOverwrite(request.User); + OverwritePermissions overwritePermissions = new OverwritePermissions(); + + string messageToBeShownByBot = "No action taken."; + + switch (request.Action) + { + case CommandType.Mute: + overwritePermissions = possiblePermissions?.Modify(sendMessages: PermValue.Deny) ?? + new OverwritePermissions(sendMessages: PermValue.Deny); + messageToBeShownByBot = $"The user '{request.User.Username}' was muted."; + break; + + case CommandType.Unmute: + if (possiblePermissions is OverwritePermissions permissions) + overwritePermissions = permissions.Modify(sendMessages: PermValue.Allow); + + messageToBeShownByBot = $"The user '{request.User.Username}' was unmuted."; + break; + + case CommandType.DenyReacting: + overwritePermissions = possiblePermissions?.Modify(addReactions: PermValue.Deny) + ?? new OverwritePermissions(addReactions: PermValue.Deny); + + messageToBeShownByBot = $"The user '{request.User.Username}' has been stopped from reacting."; + break; + } + + await channel.AddPermissionOverwriteAsync(request.User, overwritePermissions); + return messageToBeShownByBot; + } + } + + internal enum CommandType + { + Mute, + Unmute, + DenyReacting + } +} \ No newline at end of file diff --git a/src/Tarscord.Core/Modules/AdminModule.cs b/src/Tarscord.Core/Modules/AdminModule.cs index 0a7b03d..ef7a57c 100644 --- a/src/Tarscord.Core/Modules/AdminModule.cs +++ b/src/Tarscord.Core/Modules/AdminModule.cs @@ -1,26 +1,32 @@ using Discord; using Discord.Commands; using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; +using MediatR; using Tarscord.Core.Extensions; +using Tarscord.Core.Features.Mute; namespace Tarscord.Core.Modules; [RequireOwner] [Name("Admin commands")] -public class AdminModule : ModuleBase +public class AdminModule(IMediator mediator) : ModuleBase { /// /// Usage: mute {user} {minutes}? /// [Command("mute"), Summary("Mutes a user for a specified time")] - public async Task MuteUserAsync( + public async Task MuteUser( [Summary("The user to be muted"), Required(ErrorMessage = "Please provide member of the channel.")] - IUser user, + IUser? user = null, [Summary("Minutes for which the user is muted")] int minutes = 1) { - await ExecuteCommandAsync(user, CommandType.Mute, minutes).ConfigureAwait(false); + using var typingState = Context.Channel.EnterTypingState(); + + var message = await mediator.Send( + new Mute.Command(Context.Channel, user, Mute.CommandType.Mute, Context.User.Username, minutes)); + + await ReplyAsync(embed: message.EmbedMessage()); } /// @@ -31,7 +37,12 @@ public async Task UnmuteUser( [Summary("The user to be unmuted"), Required(ErrorMessage = "Please provide member of the channel.")] IUser? user = null) { - await ExecuteCommandAsync(user!, CommandType.Unmute).ConfigureAwait(false); + using var typingState = Context.Channel.EnterTypingState(); + + var message = await mediator.Send( + new Mute.Command(Context.Channel, user, Mute.CommandType.Unmute, Context.User.Username)); + + await ReplyAsync(embed: message.EmbedMessage()); } /// @@ -44,55 +55,14 @@ public async Task DenyReactingAsync( IUser? user = null, [Summary("Minutes for which the user cannot react")] int minutes = 1) - { - // TODO: Add small validation here - await ExecuteCommandAsync(user!, CommandType.DenyReacting, minutes).ConfigureAwait(false); - } - - private async Task ExecuteCommandAsync(IUser user, CommandType action, int minutes = 0) { using var typingState = Context.Channel.EnterTypingState(); - // TODO: After the specified minutes, the user should be un muted. - if (Context.Channel is IGuildChannel channel) - { - OverwritePermissions? possiblePermissions = channel.GetPermissionOverwrite(user); - OverwritePermissions overwritePermissions = new OverwritePermissions(); - - string? messageToBeShownByBot = null; - - switch (action) - { - case CommandType.Mute: - overwritePermissions = possiblePermissions?.Modify(sendMessages: PermValue.Deny) ?? - new OverwritePermissions(sendMessages: PermValue.Deny); - messageToBeShownByBot = $"The user '{user.Username}' was muted."; - break; - - case CommandType.Unmute: - if (possiblePermissions is OverwritePermissions permissions) - overwritePermissions = permissions.Modify(sendMessages: PermValue.Allow); - - messageToBeShownByBot = $"The user '{user.Username}' was unmuted."; - break; - - case CommandType.DenyReacting: - overwritePermissions = possiblePermissions?.Modify(addReactions: PermValue.Deny) - ?? new OverwritePermissions(addReactions: PermValue.Deny); - - messageToBeShownByBot = $"The user '{user.Username}' has been stopped from reacting."; - break; - } - - await channel.AddPermissionOverwriteAsync(user, overwritePermissions).ConfigureAwait(false); - await ReplyAsync(embed: messageToBeShownByBot!.EmbedMessage()).ConfigureAwait(false); - } - } + // TODO: Add small validation here + var message = await mediator.Send( + new Mute.Command(Context.Channel, user, Mute.CommandType.DenyReacting, Context.User.Username, + minutes)); - private enum CommandType - { - Mute, - Unmute, - DenyReacting + await ReplyAsync(embed: message.EmbedMessage()); } } \ No newline at end of file diff --git a/src/Tarscord.Core/Modules/InteractionModule.cs b/src/Tarscord.Core/Modules/InteractionModule.cs index 11b91ea..785b219 100644 --- a/src/Tarscord.Core/Modules/InteractionModule.cs +++ b/src/Tarscord.Core/Modules/InteractionModule.cs @@ -1,7 +1,5 @@ using Discord; using Discord.Commands; -using System; -using System.Threading.Tasks; using Tarscord.Core.Extensions; namespace Tarscord.Core.Modules; diff --git a/src/Tarscord.Core/Modules/LoanGroupModule.cs b/src/Tarscord.Core/Modules/LoanGroupModule.cs index f74df1a..cd8b333 100644 --- a/src/Tarscord.Core/Modules/LoanGroupModule.cs +++ b/src/Tarscord.Core/Modules/LoanGroupModule.cs @@ -28,29 +28,32 @@ public async Task ShowLoans() { var loanList = await _mediator.Send(new List.Query(Context.User.Username)); - var messageToReplyWith = "No active loans were found"; - - if (loanList.Loans.Any()) + if (!loanList.Loans.Any()) { - string formattedEventInformation = - FormatEventInformation(loanList.Loans); - - messageToReplyWith = $"Here are all the loans:\n{formattedEventInformation}"; + const string messageToReplyWith = "No active loans were found"; + await ReplyAsync(embed: messageToReplyWith.EmbedMessage()); } - await ReplyAsync(embed: messageToReplyWith.EmbedMessage()).ConfigureAwait(false); + await ReplyAsync(embed: "Here are all the loans:\n".EmbedMessage(), + message: FormatEventInformation(loanList.Loans)); } private static string FormatEventInformation(IReadOnlyList loans) { var messageToReply = new StringBuilder(); - for (int i = 0; i < loans.Count; i++) + for (var i = 0; i < loans.Count; i++) { + if (loans[i].Amount == loans[i].AmountPaid) + { + continue; + } messageToReply.Append(i + 1).Append(". '") .Append(loans[i].LoanedTo).Append("' owns '") .Append(loans[i].LoanedFrom).Append("' ") - .Append(loans[i].Amount).Append('€').Append(".\n"); + .Append(loans[i].Amount).Append('€') + .Append(" (from them paid: ").Append(loans[i].AmountPaid) + .Append(".\n"); } return messageToReply.ToString(); @@ -62,17 +65,25 @@ private static string FormatEventInformation(IReadOnlyList loans) /// The generated random number [Command("to"), Summary("Loans a user some money")] public async Task LoanToUser( - [Summary("The user to loan money to")] IUser user, + [Summary("The user to loan money to")] string user, [Summary("The amount of the money being lent")] decimal amount, [Summary("The reason you're loaning the money")] params string[] description) { + var guildUser = await GetMentionedUser(user); + + if (guildUser is null) + { + await ReplyAsync(embed: "Invalid user mention. Please mention a user like @username".EmbedMessage()); + return; + } + var response = await _mediator.Send(new Create.Command { Amount = amount, - LoanedToId = user.Id, - LoanedTo = user.Username, + LoanedToId = guildUser.Id, + LoanedTo = guildUser.Username, LoanedFromId = Context.User.Id, LoanedFrom = Context.User.Username, Description = string.Join(" ", description), @@ -93,31 +104,46 @@ public async Task LoanToUser( [Command("payback"), Summary("Pays back the amount to the loaner")] [Alias("return", "removeloan", "deleteloan", "payloan")] public async Task PaybackToUser( - [Summary("The user to loan money to")] IUser user, + [Summary("The user to loan money to")] string user, [Summary("The value of the money being lent")] decimal amountBeingPayedBack) { - await Task.CompletedTask; - // var loanEnvelope = await _mediator.Send(new UpdateLoanCommand - // { - // Loan = new UpdateLoanCommand.Loan - // { - // Amount = amountBeingPayedBack, - // LoanedTo = user.Id, - // LoanedToUsername = user.Username, - // LoanedFrom = Context.User.Id, - // LoanedFromUsername = Context.User.Username - // } - // }); - // - // var messageToReplyWith = ""; - // if (loanEnvelope.Loan != null) - // { - // var formattedEventInformation = - // FormatEventInformation(_mapper.Map>(loanEnvelope.Loan)); - // messageToReplyWith = $"Here are all the loans:\n{formattedEventInformation}"; - // } - // - // await ReplyAsync(embed: messageToReplyWith.EmbedMessage()).ConfigureAwait(false); + var guildUser = await GetMentionedUser(user); + + if (guildUser is null) + { + await ReplyAsync(embed: "Invalid user mention. Please mention a user like @username".EmbedMessage()); + return; + } + + var response = await _mediator.Send(new Update.Command + { + Amount = amountBeingPayedBack, + LoanedTo = guildUser.Id, + LoanedToUsername = guildUser.Username, + LoanedFrom = Context.User.Id, + LoanedFromUsername = Context.User.Username, + PerformedByUser = Context.User.Username + }); + + var embeddedMessage = response.Match( + eventInfoEnvelope => eventInfoEnvelope.ToEmbeddedMessage(), + failureResponse => failureResponse.ErrorMessage.EmbedMessage()); + + await ReplyAsync(embed: embeddedMessage); + } + + private async Task GetMentionedUser(string userMention) + { + // Parse the user mention to get the user ID + if (!MentionUtils.TryParseUser(userMention, out var userId)) + { + return null; + } + + // Get the user from the guild + var guildUser = await Context.Guild.GetUserAsync(userId); + + return guildUser ?? null; } } \ No newline at end of file diff --git a/src/Tarscord.Core/Services/AdminService.cs b/src/Tarscord.Core/Services/AdminService.cs deleted file mode 100644 index 5d654ce..0000000 --- a/src/Tarscord.Core/Services/AdminService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Tarscord.Core.Persistence; - -namespace Tarscord.Core.Services; - -public class AdminService -{ - // public async Task MuteUser(string id, int minutesToMute) - // { - // var userToMute = - // (await _userRepository.FindBy(u => u.Id == id).ConfigureAwait(false)).FirstOrDefault(); - // } -} \ No newline at end of file diff --git a/src/Tarscord.Core/Tarscord.Core.csproj b/src/Tarscord.Core/Tarscord.Core.csproj index 70f7608..fe076c6 100644 --- a/src/Tarscord.Core/Tarscord.Core.csproj +++ b/src/Tarscord.Core/Tarscord.Core.csproj @@ -19,15 +19,15 @@ - - - - - + + + + + - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tarscord.DbMigrator/Tarscord.DbMigrator.csproj b/src/Tarscord.DbMigrator/Tarscord.DbMigrator.csproj index e5674d0..effd285 100644 --- a/src/Tarscord.DbMigrator/Tarscord.DbMigrator.csproj +++ b/src/Tarscord.DbMigrator/Tarscord.DbMigrator.csproj @@ -12,7 +12,7 @@ - + diff --git a/tests/Tarscord.Core.Tests.csproj b/tests/Tarscord.Core.Tests.csproj index 546f2b6..b70e5e5 100644 --- a/tests/Tarscord.Core.Tests.csproj +++ b/tests/Tarscord.Core.Tests.csproj @@ -9,21 +9,21 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - +