Skip to content

Commit

Permalink
Merge pull request #35 from EpicOfficer/FEATURE/WebDashboard
Browse files Browse the repository at this point in the history
Add guild configuration to web dashboard
  • Loading branch information
EpicOfficer authored Apr 20, 2024
2 parents 299439d + 51f704c commit eb14293
Show file tree
Hide file tree
Showing 59 changed files with 1,154 additions and 342 deletions.
4 changes: 4 additions & 0 deletions Blink3.API/Blink3.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@

<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.Discord" Version="8.0.0"/>
<PackageReference Include="Discord.Addons.Hosting" Version="6.0.0" />
<PackageReference Include="Discord.Net" Version="3.14.1" />
<PackageReference Include="Discord.Net.Rest" Version="3.14.1"/>
<PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="8.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.4" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.4" />
<PackageReference Include="Serilog" Version="3.1.1"/>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1"/>
Expand Down
71 changes: 70 additions & 1 deletion Blink3.API/Controllers/ApiControllerBase.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
using System.Net.Mime;
using Blink3.Core.Caching;
using Blink3.Core.DiscordAuth.Extensions;
using Blink3.Core.Models;
using Discord;
using Discord.Addons.Hosting.Util;
using Discord.Rest;
using Discord.WebSocket;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
Expand All @@ -18,8 +24,12 @@ namespace Blink3.API.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public abstract class ApiControllerBase : ControllerBase
public abstract class ApiControllerBase(DiscordSocketClient discordSocketClient, ICachingService cachingService) : ControllerBase
{
protected readonly ICachingService CachingService = cachingService;
protected DiscordRestClient? Client;
protected readonly DiscordSocketClient DiscordBotClient = discordSocketClient;

/// <summary>
/// Represents an Unauthorized Access message.
/// </summary>
Expand Down Expand Up @@ -79,4 +89,63 @@ private ObjectResult ProblemForUnauthorizedAccess()
if (userId is null) return ProblemForMissingItem();
return userId != UserId ? ProblemForUnauthorizedAccess() : null;
}

protected async Task InitDiscordClientAsync()
{
await DiscordBotClient.WaitForReadyAsync(CancellationToken.None);
if (Client is not null) return;
string? accessToken = await CachingService.GetAsync<string>($"token:{UserId}");
if (accessToken is null) return;

Client = new DiscordRestClient();
await Client.LoginAsync(TokenType.Bearer, accessToken);
}

protected async Task<List<DiscordPartialGuild>> GetUserGuilds()
{
await InitDiscordClientAsync();

List<DiscordPartialGuild> managedGuilds = await CachingService.GetOrAddAsync($"discord:guilds:{UserId}",
async () =>
{
List<DiscordPartialGuild> manageable = [];
if (Client is null) return manageable;
IAsyncEnumerable<IReadOnlyCollection<RestUserGuild>> guilds = Client.GetGuildSummariesAsync();
await foreach (IReadOnlyCollection<RestUserGuild> guildCollection in guilds)
{
manageable.AddRange(guildCollection.Where(g => g.Permissions.ManageGuild).Select(g =>
new DiscordPartialGuild
{
Id = g.Id,
Name = g.Name,
IconUrl = g.IconUrl
}));
}
return manageable;
}, TimeSpan.FromMinutes(5));

List<ulong> discordGuildIds = DiscordBotClient.Guilds.Select(b => b.Id).ToList();
return managedGuilds.Where(g => discordGuildIds.Contains(g.Id)).ToList();
}

/// <summary>
/// Checks if the user has access to the specified guild.
/// </summary>
/// <param name="guildId">The ID of the guild to check access for.</param>
/// <returns>
/// Returns an <see cref="ObjectResult"/> representing a problem response if the user doesn't have access, or null if the user has access.
/// </returns>
protected async Task<ObjectResult?> CheckGuildAccessAsync(ulong guildId)
{
List<DiscordPartialGuild> guilds = await GetUserGuilds();
return guilds.Any(g => g.Id == guildId) ? null : ProblemForUnauthorizedAccess();
}

~ApiControllerBase()
{
Client?.Dispose();
Client = null;
}
}
4 changes: 3 additions & 1 deletion Blink3.API/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using AspNet.Security.OAuth.Discord;
using Blink3.API.Interfaces;
using Blink3.API.Models;
using Blink3.Core.Caching;
using Blink3.Core.DiscordAuth;
using Blink3.Core.DiscordAuth.Extensions;
using Microsoft.AspNetCore.Authentication;
Expand All @@ -18,7 +19,8 @@ namespace Blink3.API.Controllers;
[Consumes(MediaTypeNames.Application.Json)]
public class AuthController(
IAuthenticationService authenticationService,
IDiscordTokenService discordTokenService) : ControllerBase
IDiscordTokenService discordTokenService,
ICachingService cachingService) : ControllerBase

Check warning on line 23 in Blink3.API/Controllers/AuthController.cs

View workflow job for this annotation

GitHub Actions / build

Parameter 'cachingService' is unread.

Check warning on line 23 in Blink3.API/Controllers/AuthController.cs

View workflow job for this annotation

GitHub Actions / build

Parameter 'cachingService' is unread.
{
[HttpGet("login")]
[SwaggerOperation(
Expand Down
125 changes: 125 additions & 0 deletions Blink3.API/Controllers/BlinkGuildsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using Blink3.Core.Caching;
using Blink3.Core.DTOs;
using Blink3.Core.Entities;
using Blink3.Core.Models;
using Blink3.Core.Repositories.Interfaces;
using Discord.WebSocket;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

namespace Blink3.API.Controllers;

/// <summary>
/// Controller for performing CRUD operations on BlinkGuild items.
/// </summary>
[SwaggerTag("All CRUD operations for BlinkGuild items")]
public class BlinkGuildsController(DiscordSocketClient discordSocketClient, ICachingService cachingService, IBlinkGuildRepository blinkGuildRepository) : ApiControllerBase(discordSocketClient, cachingService)
{
/// <summary>
/// Retrieves all BlinkGuild items that are manageable by the logged in user.
/// </summary>
/// <returns>A list of BlinkGuild objects representing the guild configurations.</returns>
[HttpGet]
[SwaggerOperation(
Summary = "Returns all BlinkGuild items",
Description = "Returns a list of all of the BlinkGuilds that are manageable by the logged in user",
OperationId = "BlinkGuilds.GetAll",
Tags = ["BlinkGuilds"]
)]
[SwaggerResponse(StatusCodes.Status200OK, "Success", typeof(IEnumerable<BlinkGuild>))]
public async Task<ActionResult<IEnumerable<BlinkGuild>>> GetAllBlinkGuilds(CancellationToken cancellationToken)
{
List<DiscordPartialGuild> guilds = await GetUserGuilds();
IReadOnlyCollection<BlinkGuild> blinkGuilds = await blinkGuildRepository.FindByIdsAsync(guilds.Select(g => g.Id).ToHashSet());
return Ok(blinkGuilds);
}

/// <summary>
/// Retrieves a specific BlinkGuild item by its Id.
/// </summary>
/// <param name="id">The Id of the BlinkGuild item.</param>
/// <returns>The BlinkGuild item with the specified Id.</returns>
[HttpGet("{id}")]
[SwaggerOperation(
Summary = "Returns a specific BlinkGuild item",
Description = "Returns a BlinkGuild item by Id",
OperationId = "BlinkGuilds.GetBlinkGuild",
Tags = ["BlinkGuilds"]
)]
[SwaggerResponse(StatusCodes.Status200OK, "Success", typeof(BlinkGuild))]
public async Task<ActionResult<UserTodo>> GetBlinkGuild(ulong id)
{
ObjectResult? accessCheckResult = await CheckGuildAccessAsync(id);
if (accessCheckResult is not null) return accessCheckResult;

BlinkGuild blinkGuild = await blinkGuildRepository.GetOrCreateByIdAsync(id);
return Ok(blinkGuild);
}

/// <summary>
/// Updates the content of a specific BlinkGuild item.
/// </summary>
/// <param name="id">The ID of the BlinkGuild item to update.</param>
/// <param name="blinkGuild">The updated BlinkGuild item data.</param>
/// <returns>
/// No content.
/// </returns>
[HttpPut("{id}")]
[SwaggerOperation(
Summary = "Updates a specific BlinkGuild item",
Description = "Updates the content of a specific BlinkGuild item",
OperationId = "BlinkGuilds.Update",
Tags = ["BlinkGuilds"]
)]
[SwaggerResponse(StatusCodes.Status204NoContent, "No content")]
public async Task<ActionResult> UpdateBlinkGuild(ulong id, [FromBody] BlinkGuild blinkGuild,
CancellationToken cancellationToken)
{
ObjectResult? accessCheckResult = await CheckGuildAccessAsync(id);
if (accessCheckResult is not null) return accessCheckResult;

blinkGuild.Id = id;
await blinkGuildRepository.UpdateAsync(blinkGuild, cancellationToken);

return NoContent();
}

/// <summary>
/// Patches a specific BlinkGuild item.
/// Updates the content of a specific BlinkGuild item partially.
/// </summary>
/// <param name="id">The ID of the BlinkGuild item to patch.</param>
/// <param name="patchDoc">The <see cref="JsonPatchDocument{T}"/> containing the partial update.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Returns 204 (No content) if the patch is successful.</returns>
[HttpPatch("{id}")]
[SwaggerOperation(
Summary = "Patches a specific BlinkGuild item",
Description = "Updates the content of a specific BlinkGuild item partially",
OperationId = "BlinkGuilds.Patch",
Tags = ["BlinkGuilds"]
)]
[SwaggerResponse(StatusCodes.Status204NoContent, "No content")]
public async Task<IActionResult> PatchBlinkGuild(ulong id, [FromBody] JsonPatchDocument<BlinkGuild> patchDoc,
CancellationToken cancellationToken)
{
ObjectResult? accessCheckResult = await CheckGuildAccessAsync(id);
if (accessCheckResult is not null) return accessCheckResult;

BlinkGuild? blinkGuild = await blinkGuildRepository.GetByIdAsync(id);
if (blinkGuild is null)
{
return NotFound();
}
patchDoc.ApplyTo(blinkGuild);

if (!TryValidateModel(blinkGuild))
{
return BadRequest(ModelState);
}

await blinkGuildRepository.UpdateAsync(blinkGuild, cancellationToken);
return NoContent();
}
}
75 changes: 75 additions & 0 deletions Blink3.API/Controllers/GuildsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using Blink3.Core.Caching;
using Blink3.Core.Models;
using Discord.WebSocket;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

namespace Blink3.API.Controllers;

[SwaggerTag("Endpoints for getting information on discord guilds")]
public class GuildsController(DiscordSocketClient discordSocketClient, ICachingService cachingService)
: ApiControllerBase(discordSocketClient, cachingService)
{
[HttpGet]
[SwaggerOperation(
Summary = "Returns all Discord guilds",
Description = "Returns a list of Discord guilds the currently logged in user has access to manage.",
OperationId = "Guilds.GetAll",
Tags = ["Guilds"]
)]
[SwaggerResponse(StatusCodes.Status200OK, "Success", typeof(DiscordPartialGuild[]))]
public async Task<ActionResult<DiscordPartialGuild[]>> GetAllGuilds()
{
List<DiscordPartialGuild> guilds = await GetUserGuilds();

return Ok(guilds);
}

[HttpGet("{id}/categories")]
[SwaggerOperation(
Summary = "Returns all categories for a guild",
Description = "Returns a list of all Discord category channels for a given guild ID",
OperationId = "Guilds.GetCategories",
Tags = ["Guilds"]
)]
[SwaggerResponse(StatusCodes.Status200OK, "Success", typeof(DiscordPartialChannel[]))]
public async Task<ActionResult<IReadOnlyCollection<DiscordPartialChannel>>> GetCategories(ulong id)
{
ObjectResult? accessCheckResult = await CheckGuildAccessAsync(id);
if (accessCheckResult is not null) return accessCheckResult;

return DiscordBotClient.GetGuild(id).CategoryChannels
.OrderBy(c => c.Position)
.Select(c =>
new DiscordPartialChannel
{
Id = c.Id,
Name = c.Name
})
.ToList();
}

[HttpGet("{id}/channels")]
[SwaggerOperation(
Summary = "Returns all chanels for a guild",
Description = "Returns a list of all Discord channels for a given guild ID",
OperationId = "Guilds.GetChannels",
Tags = ["Guilds"]
)]
[SwaggerResponse(StatusCodes.Status200OK, "Success", typeof(DiscordPartialChannel[]))]
public async Task<ActionResult<IReadOnlyCollection<DiscordPartialChannel>>> GetChannels(ulong id)
{
ObjectResult? accessCheckResult = await CheckGuildAccessAsync(id);
if (accessCheckResult is not null) return accessCheckResult;

return DiscordBotClient.GetGuild(id).TextChannels
.OrderBy(c => c.Position)
.Select(c =>
new DiscordPartialChannel
{
Id = c.Id,
Name = c.Name
})
.ToList();
}
}
4 changes: 3 additions & 1 deletion Blink3.API/Controllers/TodoController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Blink3.Core.Caching;
using Blink3.Core.DTOs;
using Blink3.Core.Entities;
using Blink3.Core.Repositories.Interfaces;
using Discord.WebSocket;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

Expand All @@ -10,7 +12,7 @@ namespace Blink3.API.Controllers;
/// Controller for performing CRUD operations on userTodo items.
/// </summary>
[SwaggerTag("All CRUD operations for todo items")]
public class TodoController(IUserTodoRepository todoRepository) : ApiControllerBase
public class TodoController(DiscordSocketClient discordSocketClient, ICachingService cachingService, IUserTodoRepository todoRepository) : ApiControllerBase(discordSocketClient, cachingService)
{
/// <summary>
/// Retrieves all userTodo items for the current user.
Expand Down
Loading

0 comments on commit eb14293

Please sign in to comment.