Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DiscordBot/Database/MongoOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ public class MongoOptions

public string SubWordsCollectionName { get; set; } = "SubWords";
public string VotesCollectionName { get; set; } = "Votes";
public string UserSettingsCollectionName { get; set; } = "UserSettings";
}
}
3 changes: 2 additions & 1 deletion DiscordBot/DiscordBot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<RepositoryUrl>https://github.com/DevSubmarine/DiscordBot</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>bot; discord; devsub; devsubmarine</PackageTags>
<Version>1.0.1</Version>
<Version>1.1.0</Version>
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -75,6 +75,7 @@
<PackageReference Include="Serilog.Sinks.Datadog.Logs" Version="0.3.6" />
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="TehGM.Utilities.Randomization" Version="0.1.0" />
</ItemGroup>

</Project>
7 changes: 5 additions & 2 deletions DiscordBot/Features/BlogsManagement/BlogManagementCommands.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Discord;
using Discord.Interactions;
using Discord.Net;
using TehGM.Utilities.Randomization;

namespace DevSubmarine.DiscordBot.BlogsManagement.Services
{
Expand All @@ -10,14 +11,16 @@ public class BlogManagementCommands : DevSubInteractionModule
{
private readonly IBlogChannelManager _manager;
private readonly IBlogChannelNameConverter _nameConverter;
private readonly IRandomizer _randomizer;
private readonly BlogsManagementOptions _options;
private readonly ILogger _log;

public BlogManagementCommands(IBlogChannelManager manager, IBlogChannelNameConverter nameConverter,
public BlogManagementCommands(IBlogChannelManager manager, IBlogChannelNameConverter nameConverter, IRandomizer randomizer,
IOptionsMonitor<BlogsManagementOptions> options, ILogger<BlogManagementCommands> log)
{
this._manager = manager;
this._nameConverter = nameConverter;
this._randomizer = randomizer;
this._options = options.CurrentValue;
this._log = log;
}
Expand Down Expand Up @@ -71,7 +74,7 @@ public async Task CmdClearAsync(
if (CreatingForSelf() && memberAge < this._options.MinMemberAge)
{
string[] emojis = new string[] { ResponseEmoji.FeelsBeanMan, ResponseEmoji.FeelsDumbMan, ResponseEmoji.EyesBlurry, ResponseEmoji.BlobSweatAnimated };
await RespondFailureAsync($"{ResponseEmoji.Failure} You need to be here for at least {this._options.MinMemberAge.ToDisplayString()} to create a blog channel.\nYou've been here for {memberAge.ToDisplayString()} so far. {emojis[new Random().Next(emojis.Length)]}");
await RespondFailureAsync($"{ResponseEmoji.Failure} You need to be here for at least {this._options.MinMemberAge.ToDisplayString()} to create a blog channel.\nYou've been here for {memberAge.ToDisplayString()} so far. {this._randomizer.GetRandomValue(emojis)}");
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ private async Task OnClientMessageReceived(SocketMessage message)

this._log.LogInformation("Message received from inactive blog channel {ChannelName} ({ChannelID})", channel.Name, channel.Id);
CancellationToken cancellationToken = this._cts.Token;
SocketCategoryChannel category = channel.Guild.GetCategoryChannel(channel.CategoryId.Value);
SocketCategoryChannel categoryToSort = channel.Guild.GetCategoryChannel(this.Options.ActiveBlogsCategoryID);
try
{
await this._activator.ActivateBlogChannel(channel, cancellationToken).ConfigureAwait(false);
await this._sorter.SortChannelsAsync(category, cancellationToken).ConfigureAwait(false);
await this._sorter.SortChannelsAsync(categoryToSort, cancellationToken).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.IsMissingPermissions() &&
ex.LogAsError(this._log, "Failed moving {ChannelName} ({ChannelID}) due to missing permissions", channel.Name, channel.Id)) { }
Expand Down
82 changes: 58 additions & 24 deletions DiscordBot/Features/ColourRoles/ColourRolesCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,33 @@
using Discord.Interactions;
using Discord.Net;
using Discord.WebSocket;
using TehGM.Utilities.Randomization;

namespace DevSubmarine.DiscordBot.ColourRoles
{
[Group("colour", "Commands for changing your nickname colour")]
[EnabledInDm(false)]
public class ColourRolesCommands : DevSubInteractionModule
{
private readonly IRandomizer _randomizer;
private readonly IOptionsMonitor<ColourRolesOptions> _options;
private readonly ILogger _log;

public ColourRolesCommands(IOptionsMonitor<ColourRolesOptions> options, ILogger<ColourRolesCommands> log)
public ColourRolesCommands(IRandomizer randomizer, IOptionsMonitor<ColourRolesOptions> options, ILogger<ColourRolesCommands> log)
{
this._randomizer = randomizer;
this._options = options;
this._log = log;
}

[SlashCommand("set", "Sets your colour role to the one you selected")]
[EnabledInDm(false)]
public async Task CmdSetAsync(
[Summary("Role", "Role to apply")] IRole role,
[Summary("User", "Which user to apply the role to; can only be used by administrators")] IGuildUser user = null)
{
ColourRolesOptions options = this._options.CurrentValue;

await base.DeferAsync(options: base.GetRequestOptions()).ConfigureAwait(false);

if (!this.GetAvailableRoles().Any(r => r.Id == role.Id))
{
await base.RespondAsync(
Expand All @@ -36,6 +38,8 @@ await base.RespondAsync(
return;
}

await base.DeferAsync(options: base.GetRequestOptions()).ConfigureAwait(false);

// if changing role of the other user, it should be only possible for user with specific permissions (admins basically)
IGuildUser callerUser = await base.Context.Guild.GetGuildUserAsync(base.Context.User.Id, base.Context.CancellationToken).ConfigureAwait(false);
if (user != null)
Expand All @@ -57,30 +61,12 @@ await base.RespondAsync(
else
user = callerUser;


try
{
if (this._options.CurrentValue.RemoveOldRoles)
await this.RemoveColourRolesAsync(user, oldRole => oldRole.Id != role.Id).ConfigureAwait(false);

// special case is when user already has requested role. Just skip doing any changes then to prevent exceptions, Discord vomiting or whatever else
if (!user.RoleIds.Contains(role.Id))
{
this._log.LogDebug("Adding role {RoleName} ({RoleID}) to user {UserID}", role.Name, role.Id, user.Id);
await user.AddRoleAsync(role, base.GetRequestOptions()).ConfigureAwait(false);
}
}
catch (HttpException ex) when (ex.IsMissingPermissions())
{
await base.ModifyOriginalResponseAsync(msg => msg.Content = $"Oops! {ResponseEmoji.Failure}\nI lack permissions to change your role! {ResponseEmoji.FeelsBeanMan}",
base.GetRequestOptions()).ConfigureAwait(false);
return;
}

await this.SetUserRoleAsync(user, role).ConfigureAwait(false);
await this.ConfirmRoleChangeAsync(role.Color, role.Mention).ConfigureAwait(false);
}

[SlashCommand("list", "Lists all colour roles you can pick")]
[EnabledInDm(false)]
public Task CmdListAsync()
{
IEnumerable<SocketRole> availableRoles = this.GetAvailableRoles();
Expand All @@ -102,7 +88,8 @@ public Task CmdListAsync()
options: base.GetRequestOptions());
}

[SlashCommand("clear", "Clears your role colour")]
[SlashCommand("clear", "Clears your colour role")]
[EnabledInDm(false)]
public async Task CmdClearAsync(
[Summary("User", "Which user to apply the role to; can only be used by administrators")] IGuildUser user = null)
{
Expand Down Expand Up @@ -145,6 +132,53 @@ await base.ModifyOriginalResponseAsync(msg => msg.Content = $"Oops! {ResponseEmo
await this.ConfirmRoleChangeAsync(Color.Default, "colour-naked").ConfigureAwait(false);
}

[SlashCommand("random", "Changes your colour role to a random one")]
[EnabledInDm(false)]
public async Task CmdRandomAsync()
{
ColourRolesOptions options = this._options.CurrentValue;
await base.DeferAsync(options: base.GetRequestOptions()).ConfigureAwait(false);

IGuildUser user = await base.Context.Guild.GetGuildUserAsync(base.Context.User.Id).ConfigureAwait(false);

// to ensure role does change, exclude user's current role
// they might have multiple for some damn reason (wtf Nerdu?), but it's okay - we only care about the highest one as that influences colour
// note they might have none
ulong? excludedRoleID = user.GetHighestRole(r => r.Color != Color.Default && options.AllowedRoleIDs.Contains(r.Id))?.Id;
IEnumerable<ulong> availableIDs = excludedRoleID == null ? options.AllowedRoleIDs : options.AllowedRoleIDs.Except(new[] { excludedRoleID.Value });

// keep in mind that available roles might contain roles from other guilds, so we have to verify them
availableIDs = availableIDs.Intersect(base.Context.Guild.Roles.Select(r => r.Id));

ulong selectedRoleID = this._randomizer.GetRandomValue(availableIDs);
IRole selectedRole = base.Context.Guild.GetRole(selectedRoleID);

await this.SetUserRoleAsync(user, selectedRole).ConfigureAwait(false);
await this.ConfirmRoleChangeAsync(selectedRole.Color, selectedRole.Mention).ConfigureAwait(false);
}

private async Task SetUserRoleAsync(IGuildUser user, IRole role)
{
try
{
if (this._options.CurrentValue.RemoveOldRoles)
await this.RemoveColourRolesAsync(user, oldRole => oldRole.Id != role.Id).ConfigureAwait(false);

// special case is when user already has requested role. Just skip doing any changes then to prevent exceptions, Discord vomiting or whatever else
if (!user.RoleIds.Contains(role.Id))
{
this._log.LogDebug("Adding role {RoleName} ({RoleID}) to user {UserID}", role.Name, role.Id, user.Id);
await user.AddRoleAsync(role, base.GetRequestOptions()).ConfigureAwait(false);
}
}
catch (HttpException ex) when (ex.IsMissingPermissions())
{
await base.ModifyOriginalResponseAsync(msg => msg.Content = $"Oops! {ResponseEmoji.Failure}\nI lack permissions to change your role! {ResponseEmoji.FeelsBeanMan}",
base.GetRequestOptions()).ConfigureAwait(false);
return;
}
}

private IEnumerable<SocketRole> GetAvailableRoles()
{
return base.Context.Guild.Roles.Where(role =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using DevSubmarine.DiscordBot.RandomStatus;
using DevSubmarine.DiscordBot.RandomStatus.Services;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
{
public static class RandomStatusDependencyInjectionExtensions
{
public static IServiceCollection AddRandomStatus(this IServiceCollection services, Action<RandomStatusOptions> configureOptions = null)
{
if (services == null)
throw new ArgumentNullException(nameof(services));

if (configureOptions != null)
services.Configure(configureOptions);

services.AddHostedService<RandomStatusService>();

return services;
}
}
}
11 changes: 11 additions & 0 deletions DiscordBot/Features/RandomStatus/RandomStatusOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace DevSubmarine.DiscordBot.RandomStatus
{
public class RandomStatusOptions
{
public Status[] Statuses { get; set; } = new Status[0];
public TimeSpan ChangeRate { get; set; } = TimeSpan.FromMinutes(10);
public bool Enable { get; set; } = true;

public bool IsEnabled => this.Enable && this.Statuses?.Any() == true;
}
}
103 changes: 103 additions & 0 deletions DiscordBot/Features/RandomStatus/RandomStatusService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using Discord;
using Discord.WebSocket;
using Microsoft.Extensions.Hosting;
using TehGM.Utilities.Randomization;

namespace DevSubmarine.DiscordBot.RandomStatus.Services
{
/// <summary>Background service that periodically scans blog channels for last activity and activates or deactivates them.</summary>
internal class RandomStatusService : IHostedService, IDisposable
{
private readonly DiscordSocketClient _client;
private readonly IRandomizer _randomizer;
private readonly ILogger _log;
private readonly IOptionsMonitor<RandomStatusOptions> _options;
private CancellationTokenSource _cts;

private DateTime _lastChangeUtc;

public RandomStatusService(DiscordSocketClient client, IRandomizer randomizer,
ILogger<RandomStatusService> log, IOptionsMonitor<RandomStatusOptions> options)
{
this._client = client;
this._randomizer = randomizer;
this._log = log;
this._options = options;
}

private async Task AutoChangeLoopAsync(CancellationToken cancellationToken)
{
this._log.LogDebug("Starting status randomization loop. Change rate is {ChangeRate}", this._options.CurrentValue.ChangeRate);
if (this._options.CurrentValue.ChangeRate <= TimeSpan.FromSeconds(10))
this._log.LogWarning("Change rate is less than 10 seconds!");

while (!cancellationToken.IsCancellationRequested && this._options.CurrentValue.IsEnabled)
{
RandomStatusOptions options = this._options.CurrentValue;

while (this._client.ConnectionState != ConnectionState.Connected)
{
this._log.LogTrace("Client not connected, waiting");
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
}

DateTime nextChangeUtc = this._lastChangeUtc + this._options.CurrentValue.ChangeRate;
TimeSpan remainingWait = nextChangeUtc - DateTime.UtcNow;
if (remainingWait > TimeSpan.Zero)
await Task.Delay(remainingWait, cancellationToken).ConfigureAwait(false);
await this.RandomizeStatusAsync(cancellationToken).ConfigureAwait(false);
this._log.LogTrace("Next status change: {ChangeTime}", this._lastChangeUtc + options.ChangeRate);
await Task.Delay(options.ChangeRate, cancellationToken).ConfigureAwait(false);
}
}

private async Task<Status> RandomizeStatusAsync(CancellationToken cancellationToken)
{
RandomStatusOptions options = this._options.CurrentValue;

if (!options.IsEnabled)
return null;

Status status = this._randomizer.GetRandomValue(options.Statuses);
try
{
if (this._client.CurrentUser == null || this._client.ConnectionState != ConnectionState.Connected)
return null;
if (status == null)
return null;
if (!string.IsNullOrWhiteSpace(status.Text))
this._log.LogDebug("Changing status to `{NewStatus}`", status);
else
this._log.LogDebug("Clearing status");
await this._client.SetGameAsync(status.Text, status.Link, status.ActivityType).ConfigureAwait(false);
return status;
}
catch (Exception ex) when (options.IsEnabled && ex.LogAsError(this._log, "Failed changing status to {Status}", status))
{
return null;
}
finally
{
this._lastChangeUtc = DateTime.UtcNow;
}
}

Task IHostedService.StartAsync(CancellationToken cancellationToken)
{
this._cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_ = this.AutoChangeLoopAsync(this._cts.Token);
return Task.CompletedTask;
}

Task IHostedService.StopAsync(CancellationToken cancellationToken)
{
try { this._cts?.Cancel(); } catch { }
return Task.CompletedTask;
}

public void Dispose()
{
try { this._cts?.Dispose(); } catch { }
}
}
}
28 changes: 28 additions & 0 deletions DiscordBot/Features/RandomStatus/Status.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Discord;

namespace DevSubmarine.DiscordBot.RandomStatus
{
public class Status
{
public string Text { get; set; } = null;
public string Link { get; set; } = null;
public ActivityType ActivityType { get; set; } = ActivityType.Playing;

public override string ToString()
{
switch (this.ActivityType)
{
case ActivityType.Playing:
return $"Playing {this.Text}";
case ActivityType.Streaming:
return $"Streaming {this.Text}";
case ActivityType.Watching:
return $"Watching {this.Text}";
case ActivityType.Listening:
return $"Listening to {this.Text}";
default:
throw new NotSupportedException($"Activity of type {this.ActivityType} is not supported");
}
}
}
}
Loading