196 lines
7.9 KiB
C#
196 lines
7.9 KiB
C#
// Project Makoto
|
|
// Copyright (C) 2024 Fortunevale
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY
|
|
|
|
namespace ProjectMakoto.Entities.Guilds;
|
|
|
|
public sealed class CrosspostSettings(Bot bot, Guild parent) : RequiresParent<Guild>(bot, parent)
|
|
{
|
|
[ColumnName("crosspostdelay"), ColumnType(ColumnTypes.Int), Default("5")]
|
|
public int DelayBeforePosting
|
|
{
|
|
get => this.Bot.DatabaseClient.GetValue<int>("guilds", "serverid", this.Parent.Id, "crosspostdelay", this.Bot.DatabaseClient.mainDatabaseConnection);
|
|
set => _ = this.Bot.DatabaseClient.SetValue("guilds", "serverid", this.Parent.Id, "crosspostdelay", value, this.Bot.DatabaseClient.mainDatabaseConnection);
|
|
}
|
|
|
|
[ColumnName("crosspostexcludebots"), ColumnType(ColumnTypes.TinyInt), Default("0")]
|
|
public bool ExcludeBots
|
|
{
|
|
get => this.Bot.DatabaseClient.GetValue<bool>("guilds", "serverid", this.Parent.Id, "crosspostexcludebots", this.Bot.DatabaseClient.mainDatabaseConnection);
|
|
set => _ = this.Bot.DatabaseClient.SetValue("guilds", "serverid", this.Parent.Id, "crosspostexcludebots", value, this.Bot.DatabaseClient.mainDatabaseConnection);
|
|
}
|
|
|
|
[ColumnName("crosspostchannels"), ColumnType(ColumnTypes.LongText), Default("[]")]
|
|
public ulong[] CrosspostChannels
|
|
{
|
|
get => JsonConvert.DeserializeObject<ulong[]>(this.Bot.DatabaseClient.GetValue<string>("guilds", "serverid", this.Parent.Id, "crosspostchannels", this.Bot.DatabaseClient.mainDatabaseConnection));
|
|
set => _ = this.Bot.DatabaseClient.SetValue("guilds", "serverid", this.Parent.Id, "crosspostchannels", JsonConvert.SerializeObject(value), this.Bot.DatabaseClient.mainDatabaseConnection);
|
|
}
|
|
|
|
[ColumnName("crosspost_ratelimits"), ColumnType(ColumnTypes.LongText), Default("[]")]
|
|
public CrosspostRatelimit[] CrosspostRatelimits
|
|
{
|
|
get => JsonConvert.DeserializeObject<CrosspostRatelimit[]>(this.Bot.DatabaseClient.GetValue<string>("guilds", "serverid", this.Parent.Id, "crosspost_ratelimits", this.Bot.DatabaseClient.mainDatabaseConnection))
|
|
.Select(x =>
|
|
{
|
|
x.Bot = this.Bot;
|
|
x.Parent = this.Parent;
|
|
|
|
return x;
|
|
}).ToArray();
|
|
set => _ = this.Bot.DatabaseClient.SetValue("guilds", "serverid", this.Parent.Id, "crosspost_ratelimits", JsonConvert.SerializeObject(value), this.Bot.DatabaseClient.mainDatabaseConnection);
|
|
}
|
|
|
|
private bool QueueInitialized = false;
|
|
private Dictionary<DiscordMessage, DiscordChannel> _queue = new();
|
|
|
|
public async Task CrosspostQueue()
|
|
{
|
|
this.QueueInitialized = true;
|
|
Log.Debug("Initializing crosspost queue for '{Guild}'", this.Parent.Id);
|
|
|
|
while (true)
|
|
{
|
|
DiscordChannel channel;
|
|
DiscordMessage message;
|
|
|
|
try
|
|
{
|
|
while (this._queue.Count == 0)
|
|
await Task.Delay(1000);
|
|
|
|
var keyValuePair = this._queue.First();
|
|
channel = keyValuePair.Value;
|
|
message = keyValuePair.Key;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
this._queue ??= new();
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!this.CrosspostRatelimits.Any(x => x.Id == channel.Id))
|
|
{
|
|
Log.Debug("Initialized new crosspost ratelimit for '{Channel}'", channel.Id);
|
|
this.CrosspostRatelimits = this.CrosspostRatelimits.Add(new()
|
|
{
|
|
Id = channel.Id,
|
|
});
|
|
}
|
|
|
|
var r = this.CrosspostRatelimits.First(x => x.Id == channel.Id);
|
|
|
|
Log.Debug("Crosspost Ratelimit '{Channel}': First: {First}; Remaining: {Remaining}", channel.Id, r.FirstPost, r.PostsRemaining);
|
|
|
|
async Task Crosspost()
|
|
{
|
|
if (message.Flags?.HasMessageFlag(MessageFlags.Crossposted) ?? false)
|
|
return;
|
|
|
|
r.PostsRemaining--;
|
|
var crossPostTask = channel.CrosspostMessageAsync(message);
|
|
|
|
Stopwatch sw = new();
|
|
sw.Start();
|
|
while (!crossPostTask.IsCompleted && sw.ElapsedMilliseconds < 3000)
|
|
await Task.Delay(50);
|
|
sw.Stop();
|
|
|
|
Log.Debug("It took {Milliseconds}ms to process a crosspost", sw.ElapsedMilliseconds);
|
|
|
|
if (!crossPostTask.IsCompleted)
|
|
{
|
|
Log.Warning("Crosspost Ratelimit tripped for '{Channel}': {Message}", channel.Id, message.Id);
|
|
|
|
r.FirstPost = DateTime.UtcNow;
|
|
r.PostsRemaining = 0;
|
|
}
|
|
|
|
_ = await crossPostTask;
|
|
|
|
_ = this._queue.Remove(message);
|
|
Log.Debug("Crossposted message in '{Channel}': {Message}", channel.Id, message.Id);
|
|
}
|
|
|
|
void ResetLimits()
|
|
{
|
|
r.PostsRemaining = 10;
|
|
r.FirstPost = DateTime.UtcNow;
|
|
}
|
|
|
|
if (r.FirstPost.AddHours(1).GetTotalSecondsUntil() <= 0)
|
|
{
|
|
Log.Debug("First crosspost for '{Channel}' was at {FirstPost}, resetting crosspost availability", channel.Id, r.FirstPost.AddHours(1));
|
|
ResetLimits();
|
|
}
|
|
|
|
if (r.PostsRemaining > 0)
|
|
{
|
|
Log.Debug("{Remaining} crossposts available for '{Channel}', allowing request", r.PostsRemaining, channel.Id);
|
|
await Crosspost();
|
|
continue;
|
|
}
|
|
|
|
if (r.FirstPost.AddHours(1).GetTotalSecondsUntil() > 0)
|
|
{
|
|
Log.Debug("No crossposts available for '{Channel}', waiting until {WaitUntil} ({WaitUntilSec} seconds)", channel.Id, r.FirstPost.AddHours(1), r.FirstPost.AddHours(1).GetTotalSecondsUntil());
|
|
await Task.Delay(r.FirstPost.AddHours(1).GetTimespanUntil());
|
|
}
|
|
|
|
ResetLimits();
|
|
|
|
Log.Debug("Crossposts for '{Channel}' available again, allowing request. {Remaining} requests remaining, first post at {First}.", channel.Id, r.PostsRemaining, r.FirstPost);
|
|
await Crosspost();
|
|
continue;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_ = this._queue.Remove(message);
|
|
Log.Error(ex, "Failed to process crosspost queue");
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task CrosspostWithRatelimit(DiscordClient client, DiscordMessage message)
|
|
{
|
|
if (message.Reference is not null || message.MessageType is not MessageType.Default)
|
|
return;
|
|
|
|
if (this.Parent.Crosspost.ExcludeBots)
|
|
if (message.WebhookMessage || message.Author.IsBot)
|
|
return;
|
|
|
|
var ReactionAdded = false;
|
|
|
|
if (!this.QueueInitialized)
|
|
_ = this.CrosspostQueue();
|
|
|
|
this._queue.Add(message, message.Channel);
|
|
|
|
await Task.Delay(5000);
|
|
|
|
if (this._queue.ContainsKey(message))
|
|
{
|
|
if (!ReactionAdded)
|
|
{
|
|
await message.CreateReactionAsync(DiscordEmoji.FromGuildEmote(client, 974029756355977216));
|
|
ReactionAdded = true;
|
|
}
|
|
}
|
|
|
|
while (this._queue.ContainsKey(message))
|
|
{
|
|
await Task.Delay(1000);
|
|
}
|
|
|
|
if (ReactionAdded)
|
|
_ = message.DeleteReactionsEmojiAsync(DiscordEmoji.FromGuildEmote(client, 974029756355977216));
|
|
}
|
|
}
|