Skip to content
Snippets Groups Projects
Program.cs 18.4 KiB
Newer Older
  • Learn to ignore specific revisions
  • Emilia's avatar
    Emilia committed
    using System.Web;
    using Discord;
    using Discord.WebSocket;
    using Lightquark.Types;
    
    Emilia's avatar
    Emilia committed
    using Lightquark.Types.EventBus;
    
    Emilia's avatar
    Emilia committed
    using Lightquark.Types.EventBus.Events;
    using Lightquark.Types.EventBus.Messages;
    using MongoDB.Bson;
    using MongoDB.Driver;
    using Newtonsoft.Json.Linq;
    using Quarkcord.Objects;
    
    Emilia's avatar
    Emilia committed
    
    namespace Quarkcord;
    
    public class QuarkcordPlugin : IPlugin
    {
    
    Emilia's avatar
    Emilia committed
        private static Task Log(LogMessage msg)
        {
            Console.WriteLine($"[Quarkcord] {msg.ToString()}");
            return Task.CompletedTask;
        }
    
    Emilia's avatar
    Emilia committed
        private string? _token;
        private string? _botUserId;
        private Lightquark.Types.Mongo.IUser? _user;
        private DiscordSocketClient? _client;
        private readonly ManualResetEvent _eventBusGetEvent = new(false);
        private IEventBus _eventBus = null!;
        private NetworkInformation? _networkInformation;
        private MongoClient _mongoClient = null!;
        private IMongoDatabase _database = null!;
        private IMongoCollection<MessagePair> MessagePairs => _database.GetCollection<MessagePair>("messages");
        private IMongoCollection<ChannelPair> ChannelPairs => _database.GetCollection<ChannelPair>("channels");
        private List<ChannelPair> _bridgeChannels = null!;
    
    Emilia's avatar
    Emilia committed
        public void Initialize(IEventBus eventBus)
        {
    
    Emilia's avatar
    Emilia committed
            Task.Run(async () =>
            {
                eventBus.Subscribe<BusReadyEvent>(_ => _eventBusGetEvent.Set());
                _eventBusGetEvent.WaitOne();
                _eventBus = eventBus;
    
                var filePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "lightquark",
                    "quarkcord");
    
    Emilia's avatar
    Emilia committed
                string mongoConnectionString;
                string mongoDb;
                if (File.Exists(filePath))
                {
                    var text = (await File.ReadAllTextAsync(filePath)).Trim().Split(";");
                    _token = text[0];
                    _botUserId = text[1];
                    mongoConnectionString = text[2];
                    mongoDb = text[3];
                }
                else
                {
    
                    await File.WriteAllTextAsync(filePath,
                        "INSERT_DISCORD_TOKEN_HERE;INSERT_LQ_BOT_USER_ID_HERE;INSERT_MONGO_CONNECTION_STRING_HERE;INSERT_MONGO_DB_HERE");
    
    Emilia's avatar
    Emilia committed
                    _token = "INSERT_DISCORD_TOKEN_HERE";
                    _botUserId = "INSERT_LQ_BOT_USER_ID_HERE";
                    mongoConnectionString = "INSERT_MONGO_CONNECTION_STRING_HERE";
                    mongoDb = "INSERT_MONGO_DB_HERE";
                }
    
    Emilia's avatar
    Emilia committed
                if (_token == "INSERT_DISCORD_TOKEN_HERE") throw new Exception($"Please add discord token to {filePath}");
    
                if (_botUserId == "INSERT_LQ_BOT_USER_ID_HERE")
                    throw new Exception($"Please add bot user id to {filePath}");
                if (mongoConnectionString == "INSERT_MONGO_CONNECTION_STRING_HERE")
                    throw new Exception($"Please add mongo connection string to {filePath}");
    
    Emilia's avatar
    Emilia committed
                if (mongoDb == "INSERT_MONGO_DB_HERE") throw new Exception($"Please add mongo db to {filePath}");
    
                _mongoClient = new MongoClient(mongoConnectionString);
                _database = _mongoClient.GetDatabase(mongoDb);
    
                var channelPairCursor = await ChannelPairs.FindAsync(new BsonDocument());
                var channelPairs = await channelPairCursor.ToListAsync();
                _bridgeChannels = channelPairs;
                if (_bridgeChannels.Count == 0)
                {
                    await ChannelPairs.InsertOneAsync(new ChannelPair
                    {
                        DiscordId = 0,
                        Id = ObjectId.GenerateNewId(),
                        LqId = ObjectId.Empty
                    });
                    await MessagePairs.InsertOneAsync(new MessagePair
                    {
                        DiscordId = 0,
                        Id = ObjectId.GenerateNewId(),
                        LqId = ObjectId.Empty
                    });
                }
    
    Emilia's avatar
    Emilia committed
                eventBus.Publish(new GetUserMessage
                {
                    UserId = new ObjectId(_botUserId),
                    Callback = user =>
                    {
                        _user = user;
                        _eventBusGetEvent.Set();
                    }
                });
                _eventBusGetEvent.WaitOne();
                _eventBusGetEvent.Reset();
                eventBus.Publish(new GetNetworkMessage
                {
                    Callback = network =>
                    {
                        _networkInformation = network;
                        _eventBusGetEvent.Set();
                    }
                });
                _eventBusGetEvent.WaitOne();
                _client = new DiscordSocketClient(new DiscordSocketConfig
                {
                    GatewayIntents = (GatewayIntents)130813
                });
                _client.Log += Log;
                await Log(new LogMessage(LogSeverity.Info, "Quarkcord", "Logging in"));
                await _client.LoginAsync(TokenType.Bot, _token);
                _client.Ready += () =>
                {
                    Log(new LogMessage(LogSeverity.Info, "Quarkcord", "Connected!"));
                    return Task.CompletedTask;
                };
    
                _client.MessageReceived += DiscordMessageReceived;
                _client.MessageUpdated += DiscordMessageUpdated;
                _client.MessageDeleted += DiscordMessageDeleted;
    
                _client.ReactionAdded += DiscordReactionAdded;
    
    Emilia's avatar
    Emilia committed
                eventBus.Subscribe<MessageCreateEvent>(LqMessageReceived);
                eventBus.Subscribe<MessageDeleteEvent>(LqMessageDeleted);
    
                await _client.StartAsync();
            });
        }
    
    Emilia's avatar
    Emilia committed
        private async void LqMessageDeleted(MessageDeleteEvent deleteEvent)
        {
    
            try
            {
                if (_bridgeChannels.All(bc => bc.LqId != deleteEvent.Message.ChannelId)) return;
                var bridgeChannel = _bridgeChannels.Find(bc => bc.LqId == deleteEvent.Message.ChannelId);
                if (_client!.GetChannel(bridgeChannel!.DiscordId) is not ITextChannel discordChannel) return;
                var messagePairCursor = await MessagePairs.FindAsync(mp => mp.LqId == deleteEvent.Message.Id);
                var messagePair = await messagePairCursor.FirstOrDefaultAsync();
                if (messagePair == null) return;
                await discordChannel.DeleteMessageAsync(messagePair.DiscordId);
                await MessagePairs.DeleteOneAsync(mp => mp.Id == messagePair.Id);
            }
            catch (Exception _)
            {
                // Plugin error ignored
            }
    
    Emilia's avatar
    Emilia committed
        private async void LqMessageReceived(MessageCreateEvent createEvent)
        {
    
                if (createEvent.Message.Author!.Id == _user!.Id) return;
                if (_bridgeChannels.All(bc => bc.LqId != createEvent.Message.ChannelId)) return;
                var bridgeChannel = _bridgeChannels.Find(bc => bc.LqId == createEvent.Message.ChannelId);
                if (_client!.GetChannel(bridgeChannel!.DiscordId) is not ITextChannel discordChannel) return;
                var webhooks = await discordChannel.GetWebhooksAsync();
                var webhook = webhooks.FirstOrDefault(w => w.Name == $"Quarkcord {_networkInformation?.Name}")
                              ?? await discordChannel.CreateWebhookAsync($"Quarkcord {_networkInformation?.Name}");
                var webhookClient = new Discord.Webhook.DiscordWebhookClient(webhook.Id, webhook.Token);
                var username =
                    $"{createEvent.Message.Author.Username} via {createEvent.Message.UserAgent} ({_networkInformation!.Name})";
                if (username.Length > 80)
                {
                    username = $"{createEvent.Message.Author.Username} ({_networkInformation!.Name})";
                }
    
                if (username.Length > 80)
                {
                    var toRemove = $" ({_networkInformation!.Name})".Length;
                    username = $"{createEvent.Message.Author.Username[..(80 - toRemove)]} ({_networkInformation!.Name})";
                }
    
                var message = await webhookClient.SendMessageAsync(
                    createEvent.Message.Content?.Length <= 2000
                        ? createEvent.Message.Content
                        : "A message was sent but it is too long. Please view it on Lightquark",
                    false,
                    createEvent.Message.Attachments.Select(a => a.MimeType.StartsWith("image")
                        ? new EmbedBuilder().WithImageUrl(a.Url.ToString()).Build()
                        : new EmbedBuilder().WithTitle($"{a.Filename} ({HumanReadable.BytesToString(a.Size)})")
                            .WithUrl(a.Url.ToString()).Build()),
                    username,
                    createEvent.Message.Author.AvatarUriGetter.ToString(),
                    null,
                    AllowedMentions.None);
    
                var messagePair = new MessagePair
                {
                    Id = ObjectId.GenerateNewId(),
                    LqId = createEvent.Message.Id,
                    DiscordId = message
                };
                await MessagePairs.InsertOneAsync(messagePair);
    
            catch (Exception _)
    
                // Ignore plugin error
    
    Emilia's avatar
    Emilia committed
            }
        }
    
        private async Task DiscordMessageUpdated(Cacheable<IMessage, ulong> oldMessageParam, SocketMessage messageParam,
            ISocketMessageChannel channelParam)
        {
    
                if (messageParam is not SocketUserMessage message) return;
                if (message.Author.Id == _client!.CurrentUser.Id) return;
                if (_user == null)
                {
                    await Log(new LogMessage(LogSeverity.Warning, "Quarkcord", "Message received but bot user is null"));
                    return;
                }
    
                if (_bridgeChannels.All(bc => bc.DiscordId != message.Channel.Id)) return;
                var bridgeChannel = _bridgeChannels.Find(bc => bc.DiscordId == message.Channel.Id);
                var messagePairCursor = await MessagePairs.FindAsync(mp => mp.DiscordId == message.Id);
                var messagePair = await messagePairCursor.FirstOrDefaultAsync();
                if (messagePair == null)
                {
                    // Somehow we didn't have this one, lets bridge it now!
                    await SendOrUpdateLqMessage(message, bridgeChannel!);
                }
                else
                {
                    await SendOrUpdateLqMessage(message, bridgeChannel!, true, messagePair);
                }
    
            catch (Exception _)
    
        private async Task DiscordReactionAdded(Cacheable<IUserMessage, ulong> messageParam,
            Cacheable<IMessageChannel, ulong> channelParam, SocketReaction reactionParam)
        {
    
                if (_bridgeChannels.All(bc => bc.DiscordId != channelParam.Id)) return;
                var bridgeChannel = _bridgeChannels.Find(bc => bc.DiscordId == channelParam.Id);
                var discordMessageId = messageParam.Id;
                var messagePairCursor = await MessagePairs.FindAsync(m => m.DiscordId == discordMessageId);
                var messagePair = await messagePairCursor.FirstOrDefaultAsync();
                if (messagePair == null) return;
                var message = await messageParam.DownloadAsync();
    
                var specialAttributes = new JArray
    
                    new JObject
                    {
                        ["type"] = "reply",
                        ["replyTo"] = messagePair.LqId.ToString()
                    }
                };
    
                _eventBus.Publish(new CreateMessageMessage
    
                    Message = new LqMessage
                    {
                        VirtualAuthors = [_user!],
                        ChannelId = bridgeChannel!.LqId,
                        Id = ObjectId.GenerateNewId(),
                        AuthorId = _user!.Id,
                        Content = $"{(reactionParam.User.GetValueOrDefault() as SocketGuildUser)?.Nickname
                                     ?? reactionParam.User.GetValueOrDefault().GlobalName
                                     ?? reactionParam.User.GetValueOrDefault().Username
                                     ?? "Someone"} reacted with {reactionParam.Emote}",
                        UserAgent = "Quarkcord",
                        Timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(),
                        Edited = false,
                        Attachments = [],
                        SpecialAttributes = specialAttributes
                    }
                });
            }
            catch (Exception _)
            {
                // shut up
            }
    
    
        private async Task DiscordMessageDeleted(Cacheable<IMessage, ulong> messageParam,
            Cacheable<IMessageChannel, ulong> channelParam)
    
                if (_bridgeChannels.All(bc => bc.DiscordId != channelParam.Id)) return;
                var discordMessageId = messageParam.Id;
                var messagePairCursor = await MessagePairs.FindAsync(m => m.DiscordId == discordMessageId);
                var messagePair = await messagePairCursor.FirstOrDefaultAsync();
                if (messagePair == null) return;
    
                _eventBus.Publish(new DeleteMessageMessage
                {
                    MessageId = messagePair.LqId
                });
    
                await MessagePairs.DeleteOneAsync(mp => mp.Id == messagePair.Id);
            }
            catch (Exception _)
            {
                // Ignore
            }
    
        private async Task SendOrUpdateLqMessage(SocketUserMessage message, ChannelPair bridgeChannel, bool update = false,
            MessagePair? existingMessagePair = null)
    
    Emilia's avatar
    Emilia committed
        {
            if (message.Author.Username.EndsWith($"({_networkInformation!.Name})")) return;
            var specialAttributes = new JArray
            {
                new JObject
                {
                    ["type"] = "botMessage",
    
                    ["username"] = (message.Author as SocketGuildUser)?.Nickname ??
                                   message.Author.GlobalName ?? message.Author.Username,
                    ["avatarUri"] =
                        $"{_networkInformation!.CdnBaseUrl}/external/{HttpUtility.UrlEncode(message.Author.GetDisplayAvatarUrl())}"
    
    Emilia's avatar
    Emilia committed
                }
            };
            if (message.ReferencedMessage != null)
            {
                var replyCursor = await MessagePairs.FindAsync(mp => mp.DiscordId == message.ReferencedMessage.Id);
                var reply = await replyCursor.FirstOrDefaultAsync();
                if (reply != null)
                {
                    specialAttributes.Add(new JObject
                    {
                        ["type"] = "reply",
                        ["replyTo"] = reply.LqId.ToString()
                    });
                }
            }
    
    Emilia's avatar
    Emilia committed
            var lqMessageId = existingMessagePair?.LqId ?? ObjectId.GenerateNewId();
            var messagePair = existingMessagePair ?? new MessagePair
            {
                DiscordId = message.Id,
                Id = ObjectId.GenerateNewId(),
                LqId = lqMessageId
            };
            if (!update)
            {
                await MessagePairs.InsertOneAsync(messagePair);
            }
    
    Emilia's avatar
    Emilia committed
            var lqAttachments = message.Attachments.Select(a => new LqAttachment
            {
                FileId = ObjectId.Empty,
                Filename = a.Filename,
                MimeType = a.ContentType,
                Size = a.Size,
    
                Width = a.Width,
                Height = a.Height,
    
    Emilia's avatar
    Emilia committed
                Url = new Uri($"{_networkInformation!.CdnBaseUrl}/external/{HttpUtility.UrlEncode(a.Url)}")
            }).ToArray();
            LqMessage lqMessage;
    
            if (message.Attachments.Count == 0 && message.Content.Length == 0)
    
    Emilia's avatar
    Emilia committed
            {
                specialAttributes.Add(new JObject
                {
                    ["type"] = "clientAttributes",
                    ["discordMessageId"] = message.Id,
                    ["quarkcord"] = true,
                    ["quarkcordUnsupported"] = true
                });
                lqMessage = new LqMessage
                {
                    Id = lqMessageId,
                    AuthorId = _user!.Id,
                    Content = "Discord message with unsupported content",
                    ChannelId = bridgeChannel!.LqId,
                    UserAgent = "Quarkcord",
                    Timestamp = message.Timestamp.ToUnixTimeMilliseconds(),
                    VirtualAuthors = [_user],
                    Edited = update,
                    Attachments = [],
                    SpecialAttributes = specialAttributes
                };
            }
            else
            {
                specialAttributes.Add(new JObject
                {
                    ["type"] = "clientAttributes",
                    ["discordMessageId"] = message.Id,
                    ["quarkcord"] = true
                });
                lqMessage = new LqMessage
                {
                    Id = lqMessageId,
                    AuthorId = _user!.Id,
    
    Emilia's avatar
    Emilia committed
                    ChannelId = bridgeChannel!.LqId,
                    UserAgent = "Quarkcord",
                    Timestamp = message.Timestamp.ToUnixTimeMilliseconds(),
                    VirtualAuthors = [_user],
                    Edited = update,
                    Attachments = lqAttachments,
                    SpecialAttributes = specialAttributes
                };
            }
    
    Emilia's avatar
    Emilia committed
            if (update)
            {
                _eventBus.Publish(new EditMessageMessage
                {
                    Message = lqMessage
                });
            }
            else
            {
                _eventBus.Publish(new CreateMessageMessage
                {
                    Message = lqMessage
                });
            }
    
    Emilia's avatar
    Emilia committed
        }
    
    Emilia's avatar
    Emilia committed
        private async Task DiscordMessageReceived(SocketMessage messageParam)
        {
    
                if (messageParam is not SocketUserMessage message) return;
                if (message.Author.Id == _client!.CurrentUser.Id) return;
                if (_user == null)
                {
                    await Log(new LogMessage(LogSeverity.Warning, "Quarkcord", "Message received but bot user is null"));
                    return;
                }
    
                if (_bridgeChannels.All(bc => bc.DiscordId != message.Channel.Id)) return;
                var bridgeChannel = _bridgeChannels.Find(bc => bc.DiscordId == message.Channel.Id);
    
                await SendOrUpdateLqMessage(message, bridgeChannel!);
            }
            catch (Exception _)
            {
                // Plugin error ignor
            }
    
    Emilia's avatar
    Emilia committed
        }
    }
    
    public class LqMessage : Lightquark.Types.Mongo.IMessage
    {
        public ObjectId Id { get; set; }
        public ObjectId AuthorId { get; set; }
        public string? Content { get; set; }
        public ObjectId ChannelId { get; set; }
        public string UserAgent { get; set; }
        public long Timestamp { get; set; }
        public bool Edited { get; set; }
        public Lightquark.Types.Mongo.IAttachment[] Attachments { get; set; }
        public JArray SpecialAttributes { get; set; }
        public Lightquark.Types.Mongo.IUser? Author => VirtualAuthors?.FirstOrDefault();
        public Lightquark.Types.Mongo.IUser[]? VirtualAuthors { get; set; }
    }
    
    public class LqAttachment : Lightquark.Types.Mongo.IAttachment
    {
        public Uri Url { get; set; }
        public long Size { get; set; }
        public string MimeType { get; set; }
        public string Filename { get; set; }
        public ObjectId FileId { get; set; }
    
        public int? Height { get; set; }
        public int? Width { get; set; }
    
    Emilia's avatar
    Emilia committed
    }