public async Task Add( [Help("channel", "The guild channel of this server.")] ITextChannel textChannel, [Help("linkChannelId", "The ID of the channel from the other server that you wish to link to the selected 'channel'.", "example: 334120131626621412")] ulong linkChannelId, [Help("date", "The optional date-time for the first post to synchronise.", "example: \"2020-01-01 10:00 AM\"")] DateTime?fromDate = null) { if (!Permissions.IsBotOwner(Context)) { await Context.ApplyResultReaction(CommandResult.FailedUserPermission).ConfigureAwait(false); return; } var linkChannel = _discordClient.GetChannel(linkChannelId) as ITextChannel; if (textChannel == null || linkChannel == null) { await Context.ApplyResultReaction(CommandResult.FailedBotPermission).ConfigureAwait(false); return; } var link = await LinkUtility.TryAddLinkAsync(LinkType.Discord, textChannel, linkChannel.Id.ToString(), fromDate).ConfigureAwait(false); Log.Debug($"Added link {link.Id}: {linkChannelId} -> {textChannel.Id}"); await Context.ApplyResultReaction(link == null?CommandResult.Failed : CommandResult.Success).ConfigureAwait(false); }
static DiscordLinkUtility() { LastUpdate = new ConcurrentDictionary <int, DateTime>(); _reconnecting = false; _initialLogin = true; _discordClient = new DiscordClientEx(new DiscordSocketConfig() { MessageCacheSize = Ditto.Settings.Cache.AmountOfCachedMessages, LogLevel = LogSeverity.Warning, ConnectionTimeout = (int)(Ditto.Settings.Timeout * 60), HandlerTimeout = (int)(Ditto.Settings.Timeout * 60), DefaultRetryMode = RetryMode.AlwaysRetry, }); var loginAction = new Func <Task>(async() => { // Cancel out when already reconnecting if (_reconnecting) { return; } if (!_initialLogin) { Log.Info("Reconnecting discord slave user..."); } // Attempt to continuously reconnect. _reconnecting = true; _connected = false; int retryAttempt = 0; while (true) { try { await _discordClient.LoginAsync(0, Ditto.Settings.Credentials.UserSlaveToken, true); await _discordClient.StartAsync().ConfigureAwait(false); _reconnecting = false; break; } catch { if (_initialLogin) { _initialLogin = false; Log.Error("Slave user could not connect at first login, will not retry further."); break; } else { _initialLogin = false; Log.Warn($"Slave user could not connect ({++retryAttempt})"); await Task.Delay(500).ConfigureAwait(false); } } } }); _discordClient.Connected += () => { _connected = true; Log.Info("Slave user connected!"); return(Task.CompletedTask); }; _discordClient.Disconnected += (ex) => { if (!_initialLogin) { Log.Warn($"Slave user has been disconnected."); Task.Run(() => loginAction()); } else { _initialLogin = false; _reconnecting = false; Log.Error("Slave user could not connect at first login, will not retry further."); return(_discordClient.StopAsync()); } return(Task.CompletedTask); }; _discordClient.LoggedOut += () => { if (!_initialLogin) { Log.Warn($"Slave user got logged out."); Task.Run(() => loginAction()); } return(Task.CompletedTask); }; Task.Run(async() => { await loginAction().ConfigureAwait(false); }); LinkUtility.TryAddHandler(LinkType.Discord, async(link, channel, cancellationToken) => { var messageIds = new List <string>(); // Only pull discord channel feeds every 2 minutes for each individual channel. var lastUpdateTime = LastUpdate.GetOrAdd(link.Id, DateTime.MinValue); if (!_connected || (DateTime.UtcNow - lastUpdateTime).TotalSeconds < 120) { return(messageIds); } if (link != null) { if (ulong.TryParse(link.Value, out ulong linkChannelId)) { if (_discordClient.GetChannel(linkChannelId) is ITextChannel linkChannel) { // Retrieve the latest messages in bulk from the targeted channel. var messages = new List <IMessage>(); ulong lastMessageId = ulong.MaxValue; while (true) { if (cancellationToken.IsCancellationRequested) { return(messageIds); } var messagesChunk = Enumerable.Empty <IMessage>(); if (lastMessageId != ulong.MaxValue) { messagesChunk = (await linkChannel.GetMessagesAsync(lastMessageId, Direction.Before, 100, CacheMode.AllowDownload).ToListAsync().ConfigureAwait(false)) .SelectMany(m => m) .Where(m => m.CreatedAt.UtcDateTime > link.Date) .Where(m => null == link.Links.FirstOrDefault(l => l.Identity == m.Id.ToString())); } else { messagesChunk = (await linkChannel.GetMessagesAsync(100, CacheMode.AllowDownload).ToListAsync().ConfigureAwait(false)) .SelectMany(m => m) .Where(m => m.CreatedAt.UtcDateTime > link.Date) .Where(m => null == link.Links.FirstOrDefault(l => l.Identity == m.Id.ToString())); } messages.AddRange(messagesChunk); lastMessageId = messagesChunk.LastOrDefault()?.Id ?? ulong.MaxValue; // Cancel when message count is less than the maximum. if (messagesChunk.Count() < 100) { break; } } // Update link date-time. var lastMessageDate = DateTime.MinValue; var funcUpdateLinkDate = new Func <Task>(async() => { if (lastMessageDate > link.Date) { link.Date = lastMessageDate; await Ditto.Database.WriteAsync(uow => { uow.Links.Update(link); }).ConfigureAwait(false); await Task.Delay(10).ConfigureAwait(false); } }); // Attempt to post the messages in sync with the created date. try { var guildUsers = new List <IGuildUser>(); foreach (var message in messages.OrderBy(m => m.CreatedAt)) { int retryCount = 0; while (retryCount < 10) { // Cancel out where needed if (cancellationToken.IsCancellationRequested) { await funcUpdateLinkDate().ConfigureAwait(false); return(messageIds); } // Attempt to send the message. try { var authorGuildUser = guildUsers.FirstOrDefault(x => x.Id == message.Author.Id); if (authorGuildUser == null) { try { authorGuildUser = await linkChannel.Guild.GetUserAsync(message.Author.Id).ConfigureAwait(false); if (authorGuildUser != null) { guildUsers.Add(authorGuildUser); } } catch { } } var dateUtc = message.CreatedAt.UtcDateTime; var embedBuilder = new EmbedBuilder().WithAuthor(new EmbedAuthorBuilder() .WithIconUrl(message.Author.GetAvatarUrl()) .WithName(authorGuildUser?.Nickname ?? message.Author?.Username) ) //.WithTitle(message.Channel.Name) .WithDescription(message.Content) .WithFooter($"{dateUtc:dddd, MMMM} {dateUtc.Day.Ordinal()} {dateUtc:yyyy} at {dateUtc:HH:mm} UTC") .WithDiscordLinkColour(channel.Guild) ; if (message.Attachments.Count > 0) { embedBuilder.WithImageUrl(message.Attachments.FirstOrDefault().Url); } var postedMessage = await channel.SendMessageAsync(embed: embedBuilder.Build(), options: new RequestOptions() { RetryMode = RetryMode.AlwaysFail }).ConfigureAwait(false); if (postedMessage != null) { // Do not add the message to the messageIds, we do not use the link_items database for this. //messageIds.Add(message.Id.ToString()); lastMessageDate = message.CreatedAt.UtcDateTime; } // OK, cancel out. break; } catch (Exception ex) { // Update the link date just in case. await funcUpdateLinkDate().ConfigureAwait(false); // Cancel out where needed if (cancellationToken.IsCancellationRequested) { return(messageIds); } // Attempt to retry sending the message if (!await LinkUtility.SendRetryLinkMessageAsync(link.Type, retryCount++, ex is Discord.Net.RateLimitedException ? null : ex)) { return(messageIds); } } } } } finally { // Update the link date time. await funcUpdateLinkDate().ConfigureAwait(false); } } } } LastUpdate.TryUpdate(link.Id, DateTime.UtcNow, lastUpdateTime); return(messageIds); }); }