Files
KfChatDotNet/KfChatDotNetWsClient/ChatClient.cs

284 lines
9.6 KiB
C#

using System.Net;
using System.Net.WebSockets;
using System.Text.Json;
using System.Xml;
using KfChatDotNetWsClient.Models;
using KfChatDotNetWsClient.Models.Events;
using KfChatDotNetWsClient.Models.Json;
using NLog;
using Websocket.Client;
// It's a fucking lie. You must use conditional access or you WILL get NullReferenceErrors if an event is not in use
// ReSharper disable ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
namespace KfChatDotNetWsClient;
public class ChatClient
{
public event EventHandlers.OnMessagesEventHandler OnMessages;
public event EventHandlers.OnUsersPartedEventHandler OnUsersParted;
public event EventHandlers.OnUsersJoinedEventHandler OnUsersJoined;
public event EventHandlers.OnWsReconnectEventHandler OnWsReconnect;
public event EventHandlers.OnDeleteMessagesEventHandler OnDeleteMessages;
public event EventHandlers.OnWsDisconnectionEventHandler OnWsDisconnection;
public event EventHandlers.OnFailedToJoinRoom OnFailedToJoinRoom;
public event EventHandlers.OnUnknownCommand OnUnknownCommand;
private WebsocketClient _wsClient;
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
private ChatClientConfigModel _config;
public ChatClient(ChatClientConfigModel config)
{
_config = config;
}
public void UpdateConfig(ChatClientConfigModel config)
{
_config = config;
}
public void UpdateToken(string newToken)
{
_config.XfSessionToken = newToken;
}
public async Task StartWsClient()
{
_wsClient = await CreateWsClient();
}
public void Disconnect()
{
_wsClient.Stop(WebSocketCloseStatus.NormalClosure, "Closing websocket").Wait();
}
private async Task<WebsocketClient> CreateWsClient()
{
var factory = new Func<ClientWebSocket>(() =>
{
var clientWs = new ClientWebSocket();
// Guest mode
if (_config.XfSessionToken == null)
{
return clientWs;
}
var cookieContainer = new CookieContainer();
cookieContainer.Add(new Cookie("xf_session", _config.XfSessionToken, "/", _config.CookieDomain));
clientWs.Options.Cookies = cookieContainer;
if (_config.Proxy != null)
{
clientWs.Options.Proxy = new WebProxy(_config.Proxy);
}
return clientWs;
});
var client = new WebsocketClient(_config.WsUri, factory)
{
ReconnectTimeout = TimeSpan.FromSeconds(_config.ReconnectTimeout)
};
client.ReconnectionHappened.Subscribe(WsReconnection);
client.MessageReceived.Subscribe(WsMessageReceived);
client.DisconnectionHappened.Subscribe(WsDisconnection);
_logger.Debug("Websocket client has been built, about to start");
await client.Start();
_logger.Debug("Websocket client started!");
return client;
}
public bool IsConnected()
{
return _wsClient is { IsRunning: true };
}
private void WsDisconnection(DisconnectionInfo disconnectionInfo)
{
_logger.Error($"Client disconnected from the chat (or never successfully connected). Type is {disconnectionInfo.Type}");
_logger.Error(disconnectionInfo.Exception);
OnWsDisconnection?.Invoke(this, disconnectionInfo);
}
private void WsReconnection(ReconnectionInfo reconnectionInfo)
{
_logger.Error($"Websocket connection dropped and reconnected. Reconnection type is {reconnectionInfo.Type}");
if (reconnectionInfo.Type == ReconnectionType.Initial)
{
_logger.Error("Not firing the reconnection event as this is the initial event");
return;
}
OnWsReconnect?.Invoke(this, reconnectionInfo);
}
private void WsMessageReceived(ResponseMessage message)
{
if (message.Text == null)
{
_logger.Info("Websocket message was null, ignoring packet");
return;
}
if (message.Text.StartsWith("You cannot join this room"))
{
_logger.Debug("Got a message saying we failed to join the room");
OnFailedToJoinRoom?.Invoke(this, message.Text);
return;
}
if (message.Text.StartsWith("Unknown command"))
{
_logger.Debug("Unknown command received");
OnUnknownCommand?.Invoke(this, message.Text);
return;
}
Dictionary<string, object> packetType = new Dictionary<string, object>();
try
{
packetType = JsonSerializer.Deserialize<Dictionary<string, object>>(message.Text)!;
}
catch (Exception e)
{
_logger.Error("Failed to parse packet");
_logger.Error(e);
_logger.Error($"Packet contents: {message.Text}");
return;
}
_logger.Debug($"Received packet from KF: {string.Join(',', packetType.Keys)}");
// Message(s) received
if (packetType.ContainsKey("messages"))
{
_logger.Debug("Looks like it's a chat message");
WsChatMessagesReceived(message);
return;
}
// User(s) joined
if (packetType.ContainsKey("users"))
{
_logger.Debug("Looks like this is a user(s) joined packet");
WsChatUsersJoined(message);
return;
}
// User(s) parted
if (packetType.ContainsKey("user"))
{
_logger.Debug("Looks like this is a user(s) parted packet");
WsChatUsersParted(message);
return;
}
if (packetType.ContainsKey("delete"))
{
_logger.Debug($"Looks like this is a message deletion packet");
WsDeleteMessagesReceived(message);
return;
}
_logger.Info($"Received packet this was not handled: {message.Text}");
}
public void JoinRoom(int roomId)
{
_logger.Debug($"Joining {roomId}");
_wsClient.Send($"/join {roomId}");
}
public void SendMessage(string message)
{
_logger.Debug($"Sending '{message}'");
_wsClient.Send(message);
}
public void DeleteMessage(int messageId)
{
_logger.Debug($"Deleting {messageId}");
_wsClient.Send($"/delete {messageId}");
}
public void EditMessage(int messageId, string newMessage)
{
var payload = JsonSerializer.Serialize(new EditMessageJsonModel {Id = messageId, Message = newMessage});
_logger.Debug($"Editing {messageId} with '{newMessage}'");
_wsClient.Send($"/edit {payload}");
}
private void WsDeleteMessagesReceived(ResponseMessage message)
{
var data = JsonSerializer.Deserialize<DeleteMessagesJsonModel>(message.Text);
_logger.Debug($"Received delete packet for messages: {string.Join(',', data.MessageIdsToDelete)}");
OnDeleteMessages?.Invoke(this, data.MessageIdsToDelete);
}
private void WsChatMessagesReceived(ResponseMessage message)
{
var data = JsonSerializer.Deserialize<MessagesJsonModel>(message.Text);
var messages = new List<MessageModel>();
foreach (var chatMessage in data.Messages)
{
var model = new MessageModel
{
Author = new UserModel
{
Id = chatMessage.Author.Id,
Username = chatMessage.Author.Username,
AvatarUrl = chatMessage.Author.AvatarUrl,
// It isn't sent on chat messages
LastActivity = null
},
Message = chatMessage.Message,
MessageId = chatMessage.MessageId,
MessageRaw = chatMessage.MessageRaw,
RoomId = chatMessage.RoomId
};
if(chatMessage.MessageEditDate == 0)
{
model.MessageEditDate = null;
}
else
{
model.MessageEditDate = DateTimeOffset.FromUnixTimeSeconds(chatMessage.MessageEditDate);
}
model.MessageDate = DateTimeOffset.FromUnixTimeSeconds(chatMessage.MessageDate);
messages.Add(model);
}
_logger.Debug($"Received {messages.Count} chat messages");
if (messages.Count == 1)
{
_logger.Debug($"{JsonSerializer.Serialize(messages[0])}");
}
OnMessages?.Invoke(this, messages, data);
}
private void WsChatUsersJoined(ResponseMessage message)
{
var data = JsonSerializer.Deserialize<UsersJsonModel>(message.Text);
var users = new List<UserModel>();
foreach (var user in data.Users.Keys)
{
users.Add(new UserModel
{
Id = int.Parse(user),
Username = data.Users[user].Username,
AvatarUrl = data.Users[user].AvatarUrl,
LastActivity = DateTimeOffset.FromUnixTimeSeconds(data.Users[user].LastActivity)
});
}
var usersJoined= data.Users.Select(user => int.Parse(user.Key)).ToList();
_logger.Debug($"Following users have joined: {string.Join(',', usersJoined)}");
OnUsersJoined?.Invoke(this, users, data);
}
private void WsChatUsersParted(ResponseMessage message)
{
// {"user":{"1337":false}}
var data = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, bool>>>(message.Text);
var usersParted = data!["user"].Select(user => int.Parse(user.Key)).ToList();
_logger.Debug($"Following users have parted: {string.Join(',', usersParted)}");
OnUsersParted?.Invoke(this, usersParted);
}
}