More restructuring

This commit is contained in:
Kwoth
2023-03-26 14:44:25 +02:00
parent 01f70f0a24
commit 308ba36b2e
319 changed files with 681 additions and 218 deletions

View File

@@ -1,153 +0,0 @@
using System.Net.Http.Json;
using System.Text;
using NadekoBot.Common.ModuleBehaviors;
namespace NadekoBot.Modules.Administration.Self;
public sealed class CheckForUpdatesService : INService, IReadyExecutor
{
private readonly BotConfigService _bcs;
private readonly IBotCredsProvider _bcp;
private readonly IHttpClientFactory _httpFactory;
private readonly DiscordSocketClient _client;
private readonly IEmbedBuilderService _ebs;
public CheckForUpdatesService(BotConfigService bcs, IBotCredsProvider bcp, IHttpClientFactory httpFactory,
DiscordSocketClient client, IEmbedBuilderService ebs)
{
_bcs = bcs;
_bcp = bcp;
_httpFactory = httpFactory;
_client = client;
_ebs = ebs;
}
public async Task OnReadyAsync()
{
if (_client.ShardId != 0)
return;
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
while (await timer.WaitForNextTickAsync())
{
var conf = _bcs.Data;
if (!conf.CheckForUpdates)
continue;
try
{
const string URL = "https://cdn.nadeko.bot/cmds/versions.json";
using var http = _httpFactory.CreateClient();
var versions = await http.GetFromJsonAsync<string[]>(URL);
if (versions is null)
continue;
var latest = versions[0];
var latestVersion = Version.Parse(latest);
var lastKnownVersion = GetLastKnownVersion();
if (lastKnownVersion is null)
{
UpdateLastKnownVersion(latestVersion);
continue;
}
if (latestVersion > lastKnownVersion)
{
UpdateLastKnownVersion(latestVersion);
// pull changelog
var changelog = await http.GetStringAsync("https://gitlab.com/Kwoth/nadekobot/-/raw/v4/CHANGELOG.md");
var thisVersionChangelog = GetVersionChangelog(latestVersion, changelog);
if (string.IsNullOrWhiteSpace(thisVersionChangelog))
{
Log.Warning("New version {BotVersion} was found but changelog is unavailable",
thisVersionChangelog);
continue;
}
var creds = _bcp.GetCreds();
await creds.OwnerIds
.Select(async x =>
{
var user = await _client.GetUserAsync(x);
if (user is null)
return;
var eb = _ebs.Create()
.WithOkColor()
.WithAuthor($"NadekoBot v{latestVersion} Released!")
.WithTitle("Changelog")
.WithUrl("https://gitlab.com/Kwoth/nadekobot/-/blob/v4/CHANGELOG.md")
.WithDescription(thisVersionChangelog.TrimTo(4096))
.WithFooter("You may disable these messages by typing '.conf bot checkforupdates false'");
await user.EmbedAsync(eb);
}).WhenAll();
}
}
catch (Exception ex)
{
Log.Error(ex, "Error while checking for new bot release: {ErrorMessage}", ex.Message);
}
}
}
private string? GetVersionChangelog(Version latestVersion, string changelog)
{
var clSpan = changelog.AsSpan();
var sb = new StringBuilder();
var started = false;
foreach (var line in clSpan.EnumerateLines())
{
// if we're at the current version, keep reading lines and adding to the output
if (started)
{
// if we got to previous version, end
if (line.StartsWith("## ["))
break;
// if we're reading a new segment, reformat it to print it better to discord
if (line.StartsWith("### "))
{
sb.AppendLine(Format.Bold(line.ToString()));
}
else
{
sb.AppendLine(line.ToString());
}
continue;
}
if (line.StartsWith($"## [{latestVersion.ToString()}]"))
{
started = true;
continue;
}
}
return sb.ToString();
}
private const string LAST_KNOWN_VERSION_PATH = "data/last_known_version.txt";
private Version? GetLastKnownVersion()
{
if (!File.Exists(LAST_KNOWN_VERSION_PATH))
return null;
return Version.TryParse(File.ReadAllText(LAST_KNOWN_VERSION_PATH), out var ver)
? ver
: null;
}
private void UpdateLastKnownVersion(Version version)
{
File.WriteAllText("data/last_known_version.txt", version.ToString());
}
}

View File

@@ -1,572 +0,0 @@
#nullable disable
using Nadeko.Medusa;
using NadekoBot.Modules.Administration.Services;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Administration;
public partial class Administration
{
[Group]
public partial class SelfCommands : NadekoModule<SelfService>
{
public enum SettableUserStatus
{
Online,
Invisible,
Idle,
Dnd
}
private readonly DiscordSocketClient _client;
private readonly IBotStrings _strings;
private readonly IMedusaLoaderService _medusaLoader;
private readonly ICoordinator _coord;
public SelfCommands(
DiscordSocketClient client,
IBotStrings strings,
ICoordinator coord,
IMedusaLoaderService medusaLoader)
{
_client = client;
_strings = strings;
_coord = coord;
_medusaLoader = medusaLoader;
}
[Cmd]
[OwnerOnly]
public async Task DoAs(IUser user, [Leftover] string message)
{
if (ctx.User is not IGuildUser { GuildPermissions.Administrator: true })
return;
if (ctx.Guild is SocketGuild sg && ctx.Channel is ISocketMessageChannel ch
&& ctx.Message is SocketUserMessage msg)
{
var fakeMessage = new DoAsUserMessage(msg, user, message);
await _cmdHandler.TryRunCommand(sg, ch, fakeMessage);
}
else
{
await ReplyErrorLocalizedAsync(strs.error_occured);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task StartupCommandAdd([Leftover] string cmdText)
{
if (cmdText.StartsWith(prefix + "die", StringComparison.InvariantCulture))
return;
var guser = (IGuildUser)ctx.User;
var cmd = new AutoCommand
{
CommandText = cmdText,
ChannelId = ctx.Channel.Id,
ChannelName = ctx.Channel.Name,
GuildId = ctx.Guild?.Id,
GuildName = ctx.Guild?.Name,
VoiceChannelId = guser.VoiceChannel?.Id,
VoiceChannelName = guser.VoiceChannel?.Name,
Interval = 0
};
_service.AddNewAutoCommand(cmd);
await ctx.Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.scadd))
.AddField(GetText(strs.server),
cmd.GuildId is null ? "-" : $"{cmd.GuildName}/{cmd.GuildId}",
true)
.AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true)
.AddField(GetText(strs.command_text), cmdText));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task AutoCommandAdd(int interval, [Leftover] string cmdText)
{
if (cmdText.StartsWith(prefix + "die", StringComparison.InvariantCulture))
return;
if (interval < 5)
return;
var guser = (IGuildUser)ctx.User;
var cmd = new AutoCommand
{
CommandText = cmdText,
ChannelId = ctx.Channel.Id,
ChannelName = ctx.Channel.Name,
GuildId = ctx.Guild?.Id,
GuildName = ctx.Guild?.Name,
VoiceChannelId = guser.VoiceChannel?.Id,
VoiceChannelName = guser.VoiceChannel?.Name,
Interval = interval
};
_service.AddNewAutoCommand(cmd);
await ReplyConfirmLocalizedAsync(strs.autocmd_add(Format.Code(Format.Sanitize(cmdText)), cmd.Interval));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task StartupCommandsList(int page = 1)
{
if (page-- < 1)
return;
var scmds = _service.GetStartupCommands().Skip(page * 5).Take(5).ToList();
if (scmds.Count == 0)
await ReplyErrorLocalizedAsync(strs.startcmdlist_none);
else
{
var i = 0;
await SendConfirmAsync(text: string.Join("\n",
scmds.Select(x => $@"```css
#{++i + (page * 5)}
[{GetText(strs.server)}]: {(x.GuildId.HasValue ? $"{x.GuildName} #{x.GuildId}" : "-")}
[{GetText(strs.channel)}]: {x.ChannelName} #{x.ChannelId}
[{GetText(strs.command_text)}]: {x.CommandText}```")),
title: string.Empty,
footer: GetText(strs.page(page + 1)));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task AutoCommandsList(int page = 1)
{
if (page-- < 1)
return;
var scmds = _service.GetAutoCommands().Skip(page * 5).Take(5).ToList();
if (!scmds.Any())
await ReplyErrorLocalizedAsync(strs.autocmdlist_none);
else
{
var i = 0;
await SendConfirmAsync(text: string.Join("\n",
scmds.Select(x => $@"```css
#{++i + (page * 5)}
[{GetText(strs.server)}]: {(x.GuildId.HasValue ? $"{x.GuildName} #{x.GuildId}" : "-")}
[{GetText(strs.channel)}]: {x.ChannelName} #{x.ChannelId}
{GetIntervalText(x.Interval)}
[{GetText(strs.command_text)}]: {x.CommandText}```")),
title: string.Empty,
footer: GetText(strs.page(page + 1)));
}
}
private string GetIntervalText(int interval)
=> $"[{GetText(strs.interval)}]: {interval}";
[Cmd]
[OwnerOnly]
public async Task Wait(int miliseconds)
{
if (miliseconds <= 0)
return;
ctx.Message.DeleteAfter(0);
try
{
var msg = await SendConfirmAsync($"⏲ {miliseconds}ms");
msg.DeleteAfter(miliseconds / 1000);
}
catch { }
await Task.Delay(miliseconds);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task AutoCommandRemove([Leftover] int index)
{
if (!_service.RemoveAutoCommand(--index, out _))
{
await ReplyErrorLocalizedAsync(strs.acrm_fail);
return;
}
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task StartupCommandRemove([Leftover] int index)
{
if (!_service.RemoveStartupCommand(--index, out _))
await ReplyErrorLocalizedAsync(strs.scrm_fail);
else
await ReplyConfirmLocalizedAsync(strs.scrm);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task StartupCommandsClear()
{
_service.ClearStartupCommands();
await ReplyConfirmLocalizedAsync(strs.startcmds_cleared);
}
[Cmd]
[OwnerOnly]
public async Task ForwardMessages()
{
var enabled = _service.ForwardMessages();
if (enabled)
await ReplyConfirmLocalizedAsync(strs.fwdm_start);
else
await ReplyPendingLocalizedAsync(strs.fwdm_stop);
}
[Cmd]
[OwnerOnly]
public async Task ForwardToAll()
{
var enabled = _service.ForwardToAll();
if (enabled)
await ReplyConfirmLocalizedAsync(strs.fwall_start);
else
await ReplyPendingLocalizedAsync(strs.fwall_stop);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task ForwardToChannel()
{
var enabled = _service.ForwardToChannel(ctx.Channel.Id);
if (enabled)
await ReplyConfirmLocalizedAsync(strs.fwch_start);
else
await ReplyPendingLocalizedAsync(strs.fwch_stop);
}
[Cmd]
public async Task ShardStats(int page = 1)
{
if (--page < 0)
return;
var statuses = _coord.GetAllShardStatuses();
var status = string.Join(" : ",
statuses.Select(x => (ConnectionStateToEmoji(x), x))
.GroupBy(x => x.Item1)
.Select(x => $"`{x.Count()} {x.Key}`")
.ToArray());
var allShardStrings = statuses.Select(st =>
{
var timeDiff = DateTime.UtcNow - st.LastUpdate;
var stateStr = ConnectionStateToEmoji(st);
var maxGuildCountLength =
statuses.Max(x => x.GuildCount).ToString().Length;
return $"`{stateStr} "
+ $"| #{st.ShardId.ToString().PadBoth(3)} "
+ $"| {timeDiff:mm\\:ss} "
+ $"| {st.GuildCount.ToString().PadBoth(maxGuildCountLength)} `";
})
.ToArray();
await ctx.SendPaginatedConfirmAsync(page,
curPage =>
{
var str = string.Join("\n", allShardStrings.Skip(25 * curPage).Take(25));
if (string.IsNullOrWhiteSpace(str))
str = GetText(strs.no_shards_on_page);
return _eb.Create().WithOkColor().WithDescription($"{status}\n\n{str}");
},
allShardStrings.Length,
25);
}
private static string ConnectionStateToEmoji(ShardStatus status)
{
var timeDiff = DateTime.UtcNow - status.LastUpdate;
return status.ConnectionState switch
{
ConnectionState.Disconnected => "🔻",
_ when timeDiff > TimeSpan.FromSeconds(30) => " ❗ ",
ConnectionState.Connected => "✅",
_ => " ⏳"
};
}
[Cmd]
[OwnerOnly]
public async Task RestartShard(int shardId)
{
var success = _coord.RestartShard(shardId);
if (success)
await ReplyConfirmLocalizedAsync(strs.shard_reconnecting(Format.Bold("#" + shardId)));
else
await ReplyErrorLocalizedAsync(strs.no_shard_id);
}
[Cmd]
[OwnerOnly]
public Task Leave([Leftover] string guildStr)
=> _service.LeaveGuild(guildStr);
[Cmd]
[OwnerOnly]
public async Task DeleteEmptyServers()
{
await ctx.Channel.TriggerTypingAsync();
var toLeave = _client.Guilds
.Where(s => s.MemberCount == 1 && s.Users.Count == 1)
.ToList();
foreach (var server in toLeave)
{
try
{
await server.DeleteAsync();
Log.Information("Deleted server {ServerName} [{ServerId}]",
server.Name,
server.Id);
}
catch (Exception ex)
{
Log.Warning(ex,
"Error leaving server {ServerName} [{ServerId}]",
server.Name,
server.Id);
}
}
await ReplyConfirmLocalizedAsync(strs.deleted_x_servers(toLeave.Count));
}
[Cmd]
[OwnerOnly]
public async Task Die(bool graceful = false)
{
try
{
await _client.SetStatusAsync(UserStatus.Invisible);
_ = _client.StopAsync();
await ReplyConfirmLocalizedAsync(strs.shutting_down);
}
catch
{
// ignored
}
await Task.Delay(2000);
_coord.Die(graceful);
}
[Cmd]
[OwnerOnly]
public async Task Restart()
{
var success = _coord.RestartBot();
if (!success)
{
await ReplyErrorLocalizedAsync(strs.restart_fail);
return;
}
try { await ReplyConfirmLocalizedAsync(strs.restarting); }
catch { }
}
[Cmd]
[OwnerOnly]
public async Task SetName([Leftover] string newName)
{
if (string.IsNullOrWhiteSpace(newName))
return;
try
{
await _client.CurrentUser.ModifyAsync(u => u.Username = newName);
}
catch (RateLimitedException)
{
Log.Warning("You've been ratelimited. Wait 2 hours to change your name");
}
await ReplyConfirmLocalizedAsync(strs.bot_name(Format.Bold(newName)));
}
[Cmd]
[UserPerm(GuildPerm.ManageNicknames)]
[BotPerm(GuildPerm.ChangeNickname)]
[Priority(0)]
public async Task SetNick([Leftover] string newNick = null)
{
if (string.IsNullOrWhiteSpace(newNick))
return;
var curUser = await ctx.Guild.GetCurrentUserAsync();
await curUser.ModifyAsync(u => u.Nickname = newNick);
await ReplyConfirmLocalizedAsync(strs.bot_nick(Format.Bold(newNick) ?? "-"));
}
[Cmd]
[BotPerm(GuildPerm.ManageNicknames)]
[UserPerm(GuildPerm.ManageNicknames)]
[Priority(1)]
public async Task SetNick(IGuildUser gu, [Leftover] string newNick = null)
{
var sg = (SocketGuild)ctx.Guild;
if (sg.OwnerId == gu.Id
|| gu.GetRoles().Max(r => r.Position) >= sg.CurrentUser.GetRoles().Max(r => r.Position))
{
await ReplyErrorLocalizedAsync(strs.insuf_perms_i);
return;
}
await gu.ModifyAsync(u => u.Nickname = newNick);
await ReplyConfirmLocalizedAsync(strs.user_nick(Format.Bold(gu.ToString()), Format.Bold(newNick) ?? "-"));
}
[Cmd]
[OwnerOnly]
public async Task SetStatus([Leftover] SettableUserStatus status)
{
await _client.SetStatusAsync(SettableUserStatusToUserStatus(status));
await ReplyConfirmLocalizedAsync(strs.bot_status(Format.Bold(status.ToString())));
}
[Cmd]
[OwnerOnly]
public async Task SetAvatar([Leftover] string img = null)
{
var success = await _service.SetAvatar(img);
if (success)
await ReplyConfirmLocalizedAsync(strs.set_avatar);
}
[Cmd]
[OwnerOnly]
public async Task SetGame(ActivityType type, [Leftover] string game = null)
{
var rep = new ReplacementBuilder().WithDefault(Context).Build();
await _service.SetGameAsync(game is null ? game : rep.Replace(game), type);
await ReplyConfirmLocalizedAsync(strs.set_game);
}
[Cmd]
[OwnerOnly]
public async Task SetStream(string url, [Leftover] string name = null)
{
name ??= "";
await _service.SetStreamAsync(name, url);
await ReplyConfirmLocalizedAsync(strs.set_stream);
}
[Cmd]
[OwnerOnly]
public async Task Send(string where, [Leftover] SmartText text = null)
{
var ids = where.Split('|');
if (ids.Length != 2)
return;
var sid = ulong.Parse(ids[0]);
var server = _client.Guilds.FirstOrDefault(s => s.Id == sid);
if (server is null)
return;
var rep = new ReplacementBuilder().WithDefault(Context).Build();
if (ids[1].ToUpperInvariant().StartsWith("C:", StringComparison.InvariantCulture))
{
var cid = ulong.Parse(ids[1][2..]);
var ch = server.TextChannels.FirstOrDefault(c => c.Id == cid);
if (ch is null)
return;
text = rep.Replace(text);
await ch.SendAsync(text);
}
else if (ids[1].ToUpperInvariant().StartsWith("U:", StringComparison.InvariantCulture))
{
var uid = ulong.Parse(ids[1][2..]);
var user = server.Users.FirstOrDefault(u => u.Id == uid);
if (user is null)
return;
var ch = await user.CreateDMChannelAsync();
text = rep.Replace(text);
await ch.SendAsync(text);
}
else
{
await ReplyErrorLocalizedAsync(strs.invalid_format);
return;
}
await ReplyConfirmLocalizedAsync(strs.message_sent);
}
[Cmd]
[OwnerOnly]
public async Task StringsReload()
{
_strings.Reload();
await _medusaLoader.ReloadStrings();
await ReplyConfirmLocalizedAsync(strs.bot_strings_reloaded);
}
[Cmd]
[OwnerOnly]
public async Task CoordReload()
{
await _coord.Reload();
await ctx.OkAsync();
}
private static UserStatus SettableUserStatusToUserStatus(SettableUserStatus sus)
{
switch (sus)
{
case SettableUserStatus.Online:
return UserStatus.Online;
case SettableUserStatus.Invisible:
return UserStatus.Invisible;
case SettableUserStatus.Idle:
return UserStatus.AFK;
case SettableUserStatus.Dnd:
return UserStatus.DoNotDisturb;
}
return UserStatus.Online;
}
}
}

View File

@@ -1,400 +0,0 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Services.Database.Models;
using System.Collections.Immutable;
using Nadeko.Common;
namespace NadekoBot.Modules.Administration.Services;
public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
{
private readonly CommandHandler _cmdHandler;
private readonly DbService _db;
private readonly IBotStrings _strings;
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private ImmutableDictionary<ulong, IDMChannel> ownerChannels =
new Dictionary<ulong, IDMChannel>().ToImmutableDictionary();
private ConcurrentDictionary<ulong?, ConcurrentDictionary<int, Timer>> autoCommands = new();
private readonly IHttpClientFactory _httpFactory;
private readonly BotConfigService _bss;
private readonly IPubSub _pubSub;
private readonly IEmbedBuilderService _eb;
//keys
private readonly TypedKey<ActivityPubData> _activitySetKey;
private readonly TypedKey<string> _guildLeaveKey;
public SelfService(
DiscordSocketClient client,
CommandHandler cmdHandler,
DbService db,
IBotStrings strings,
IBotCredentials creds,
IHttpClientFactory factory,
BotConfigService bss,
IPubSub pubSub,
IEmbedBuilderService eb)
{
_cmdHandler = cmdHandler;
_db = db;
_strings = strings;
_client = client;
_creds = creds;
_httpFactory = factory;
_bss = bss;
_pubSub = pubSub;
_eb = eb;
_activitySetKey = new("activity.set");
_guildLeaveKey = new("guild.leave");
HandleStatusChanges();
_pubSub.Sub(_guildLeaveKey,
async input =>
{
var guildStr = input.ToString().Trim().ToUpperInvariant();
if (string.IsNullOrWhiteSpace(guildStr))
return;
var server = _client.Guilds.FirstOrDefault(g => g.Id.ToString() == guildStr
|| g.Name.Trim().ToUpperInvariant() == guildStr);
if (server is null)
return;
if (server.OwnerId != _client.CurrentUser.Id)
{
await server.LeaveAsync();
Log.Information("Left server {Name} [{Id}]", server.Name, server.Id);
}
else
{
await server.DeleteAsync();
Log.Information("Deleted server {Name} [{Id}]", server.Name, server.Id);
}
});
}
public async Task OnReadyAsync()
{
await using var uow = _db.GetDbContext();
autoCommands = uow.AutoCommands.AsNoTracking()
.Where(x => x.Interval >= 5)
.AsEnumerable()
.GroupBy(x => x.GuildId)
.ToDictionary(x => x.Key,
y => y.ToDictionary(x => x.Id, TimerFromAutoCommand).ToConcurrent())
.ToConcurrent();
var startupCommands = uow.AutoCommands.AsNoTracking().Where(x => x.Interval == 0);
foreach (var cmd in startupCommands)
{
try
{
await ExecuteCommand(cmd);
}
catch
{
}
}
if (_client.ShardId == 0)
await LoadOwnerChannels();
}
private Timer TimerFromAutoCommand(AutoCommand x)
=> new(async obj => await ExecuteCommand((AutoCommand)obj), x, x.Interval * 1000, x.Interval * 1000);
private async Task ExecuteCommand(AutoCommand cmd)
{
try
{
if (cmd.GuildId is null)
return;
var guildShard = (int)((cmd.GuildId.Value >> 22) % (ulong)_creds.TotalShards);
if (guildShard != _client.ShardId)
return;
var prefix = _cmdHandler.GetPrefix(cmd.GuildId);
//if someone already has .die as their startup command, ignore it
if (cmd.CommandText.StartsWith(prefix + "die", StringComparison.InvariantCulture))
return;
await _cmdHandler.ExecuteExternal(cmd.GuildId, cmd.ChannelId, cmd.CommandText);
}
catch (Exception ex)
{
Log.Warning(ex, "Error in SelfService ExecuteCommand");
}
}
public void AddNewAutoCommand(AutoCommand cmd)
{
using (var uow = _db.GetDbContext())
{
uow.AutoCommands.Add(cmd);
uow.SaveChanges();
}
if (cmd.Interval >= 5)
{
var autos = autoCommands.GetOrAdd(cmd.GuildId, new ConcurrentDictionary<int, Timer>());
autos.AddOrUpdate(cmd.Id,
_ => TimerFromAutoCommand(cmd),
(_, old) =>
{
old.Change(Timeout.Infinite, Timeout.Infinite);
return TimerFromAutoCommand(cmd);
});
}
}
public IEnumerable<AutoCommand> GetStartupCommands()
{
using var uow = _db.GetDbContext();
return uow.AutoCommands.AsNoTracking().Where(x => x.Interval == 0).OrderBy(x => x.Id).ToList();
}
public IEnumerable<AutoCommand> GetAutoCommands()
{
using var uow = _db.GetDbContext();
return uow.AutoCommands.AsNoTracking().Where(x => x.Interval >= 5).OrderBy(x => x.Id).ToList();
}
private async Task LoadOwnerChannels()
{
var channels = await _creds.OwnerIds.Select(id =>
{
var user = _client.GetUser(id);
if (user is null)
return Task.FromResult<IDMChannel>(null);
return user.CreateDMChannelAsync();
})
.WhenAll();
ownerChannels = channels.Where(x => x is not null)
.ToDictionary(x => x.Recipient.Id, x => x)
.ToImmutableDictionary();
if (!ownerChannels.Any())
{
Log.Warning(
"No owner channels created! Make sure you've specified the correct OwnerId in the creds.yml file and invited the bot to a Discord server");
}
else
{
Log.Information("Created {OwnerChannelCount} out of {TotalOwnerChannelCount} owner message channels",
ownerChannels.Count,
_creds.OwnerIds.Count);
}
}
public Task LeaveGuild(string guildStr)
=> _pubSub.Pub(_guildLeaveKey, guildStr);
// forwards dms
public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
{
var bs = _bss.Data;
if (msg.Channel is IDMChannel && bs.ForwardMessages && (ownerChannels.Any() || bs.ForwardToChannel is not null))
{
var title = _strings.GetText(strs.dm_from) + $" [{msg.Author}]({msg.Author.Id})";
var attachamentsTxt = _strings.GetText(strs.attachments);
var toSend = msg.Content;
if (msg.Attachments.Count > 0)
{
toSend += $"\n\n{Format.Code(attachamentsTxt)}:\n"
+ string.Join("\n", msg.Attachments.Select(a => a.ProxyUrl));
}
if (bs.ForwardToAllOwners)
{
var allOwnerChannels = ownerChannels.Values;
foreach (var ownerCh in allOwnerChannels.Where(ch => ch.Recipient.Id != msg.Author.Id))
{
try
{
await ownerCh.SendConfirmAsync(_eb, title, toSend);
}
catch
{
Log.Warning("Can't contact owner with id {OwnerId}", ownerCh.Recipient.Id);
}
}
}
else if (bs.ForwardToChannel is ulong cid)
{
try
{
if (_client.GetChannel(cid) is ITextChannel ch)
await ch.SendConfirmAsync(_eb, title, toSend);
}
catch
{
Log.Warning("Error forwarding message to the channel");
}
}
else
{
var firstOwnerChannel = ownerChannels.Values.First();
if (firstOwnerChannel.Recipient.Id != msg.Author.Id)
{
try
{
await firstOwnerChannel.SendConfirmAsync(_eb, title, toSend);
}
catch
{
// ignored
}
}
}
}
}
public bool RemoveStartupCommand(int index, out AutoCommand cmd)
{
using var uow = _db.GetDbContext();
cmd = uow.AutoCommands.AsNoTracking().Where(x => x.Interval == 0).Skip(index).FirstOrDefault();
if (cmd is not null)
{
uow.Remove(cmd);
uow.SaveChanges();
return true;
}
return false;
}
public bool RemoveAutoCommand(int index, out AutoCommand cmd)
{
using var uow = _db.GetDbContext();
cmd = uow.AutoCommands.AsNoTracking().Where(x => x.Interval >= 5).Skip(index).FirstOrDefault();
if (cmd is not null)
{
uow.Remove(cmd);
if (autoCommands.TryGetValue(cmd.GuildId, out var autos))
{
if (autos.TryRemove(cmd.Id, out var timer))
timer.Change(Timeout.Infinite, Timeout.Infinite);
}
uow.SaveChanges();
return true;
}
return false;
}
public async Task<bool> SetAvatar(string img)
{
if (string.IsNullOrWhiteSpace(img))
return false;
if (!Uri.IsWellFormedUriString(img, UriKind.Absolute))
return false;
var uri = new Uri(img);
using var http = _httpFactory.CreateClient();
using var sr = await http.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
if (!sr.IsImage())
return false;
// i can't just do ReadAsStreamAsync because dicord.net's image poops itself
var imgData = await sr.Content.ReadAsByteArrayAsync();
await using var imgStream = imgData.ToStream();
await _client.CurrentUser.ModifyAsync(u => u.Avatar = new Image(imgStream));
return true;
}
public void ClearStartupCommands()
{
using var uow = _db.GetDbContext();
var toRemove = uow.AutoCommands.AsNoTracking().Where(x => x.Interval == 0);
uow.AutoCommands.RemoveRange(toRemove);
uow.SaveChanges();
}
public bool ForwardMessages()
{
var isForwarding = false;
_bss.ModifyConfig(config => { isForwarding = config.ForwardMessages = !config.ForwardMessages; });
return isForwarding;
}
public bool ForwardToAll()
{
var isToAll = false;
_bss.ModifyConfig(config => { isToAll = config.ForwardToAllOwners = !config.ForwardToAllOwners; });
return isToAll;
}
public bool ForwardToChannel(ulong? channelId)
{
using var uow = _db.GetDbContext();
_bss.ModifyConfig(config =>
{
config.ForwardToChannel = channelId == config.ForwardToChannel
? null
: channelId;
});
return channelId is not null;
}
private void HandleStatusChanges()
=> _pubSub.Sub(_activitySetKey,
async data =>
{
try
{
await _client.SetGameAsync(data.Name, data.Link, data.Type);
}
catch (Exception ex)
{
Log.Warning(ex, "Error setting activity");
}
});
public Task SetGameAsync(string game, ActivityType type)
=> _pubSub.Pub(_activitySetKey,
new()
{
Name = game,
Link = null,
Type = type
});
public Task SetStreamAsync(string name, string link)
=> _pubSub.Pub(_activitySetKey,
new()
{
Name = name,
Link = link,
Type = ActivityType.Streaming
});
private sealed class ActivityPubData
{
public string Name { get; init; }
public string Link { get; init; }
public ActivityType Type { get; init; }
}
}