216 lines
7.0 KiB
C#
Executable File
216 lines
7.0 KiB
C#
Executable File
using System.Text;
|
|
using ChatTwo.Code;
|
|
using ChatTwo.Util;
|
|
using Dalamud.Game.Text.SeStringHandling;
|
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
|
using System.Text.RegularExpressions;
|
|
using LiteDB;
|
|
|
|
namespace ChatTwo;
|
|
|
|
internal class SortCode {
|
|
internal ChatType Type { get; }
|
|
internal ChatSource Source { get; }
|
|
|
|
[BsonCtor] // Used by LegacyMessageImporter
|
|
public SortCode(ChatType type, ChatSource source) {
|
|
Type = type;
|
|
Source = source;
|
|
}
|
|
|
|
internal SortCode(uint raw) {
|
|
Type = (ChatType)(raw >> 16);
|
|
Source = (ChatSource)(raw & 0xFFFF);
|
|
}
|
|
|
|
internal uint Encode() {
|
|
return ((uint) Type << 16) | (uint) Source;
|
|
}
|
|
|
|
private bool Equals(SortCode other) {
|
|
return Type == other.Type && Source == other.Source;
|
|
}
|
|
|
|
public override bool Equals(object? obj) {
|
|
if (ReferenceEquals(null, obj)) {
|
|
return false;
|
|
}
|
|
|
|
if (ReferenceEquals(this, obj)) {
|
|
return true;
|
|
}
|
|
|
|
return obj.GetType() == GetType() && Equals((SortCode) obj);
|
|
}
|
|
|
|
public override int GetHashCode() {
|
|
unchecked {
|
|
return ((int) Type * 397) ^ (int) Source;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal class Message
|
|
{
|
|
internal Guid Id { get; } = Guid.NewGuid();
|
|
internal ulong Receiver { get; }
|
|
internal ulong ContentId { get; set; }
|
|
|
|
internal DateTimeOffset Date { get; }
|
|
internal ChatCode Code { get; }
|
|
internal List<Chunk> Sender { get; }
|
|
internal List<Chunk> Content { get; }
|
|
|
|
internal SeString SenderSource { get; }
|
|
internal SeString ContentSource { get; }
|
|
|
|
internal SortCode SortCode { get; }
|
|
internal Guid ExtraChatChannel { get; }
|
|
|
|
// Not stored in the database:
|
|
internal int Hash { get; }
|
|
internal Dictionary<Guid, float?> Height { get; } = new();
|
|
internal Dictionary<Guid, bool> IsVisible { get; } = new();
|
|
|
|
internal Message(ulong receiver, ulong contentId, ChatCode code, List<Chunk> sender, List<Chunk> content, SeString senderSource, SeString contentSource) {
|
|
Receiver = receiver;
|
|
ContentId = contentId;
|
|
Date = DateTimeOffset.UtcNow;
|
|
Code = code;
|
|
Sender = sender;
|
|
Content = CheckMessageContent(content);
|
|
SenderSource = senderSource;
|
|
ContentSource = contentSource;
|
|
SortCode = new SortCode(Code.Type, Code.Source);
|
|
ExtraChatChannel = ExtractExtraChatChannel();
|
|
Hash = GenerateHash();
|
|
|
|
foreach (var chunk in sender.Concat(content))
|
|
chunk.Message = this;
|
|
}
|
|
|
|
internal Message(Guid id, ulong receiver, ulong contentId, DateTimeOffset date, ChatCode code, List<Chunk> sender, List<Chunk> content, SeString senderSource, SeString contentSource, SortCode sortCode, Guid extraChatChannel)
|
|
{
|
|
Id = id;
|
|
Receiver = receiver;
|
|
ContentId = contentId;
|
|
Date = date;
|
|
Code = code;
|
|
Sender = sender;
|
|
// Don't call ReplaceContentURLs here since we're loading the message
|
|
// from the database, and it should already have parsed URL data.
|
|
Content = content;
|
|
SenderSource = senderSource;
|
|
ContentSource = contentSource;
|
|
SortCode = sortCode;
|
|
ExtraChatChannel = extraChatChannel;
|
|
Hash = GenerateHash();
|
|
|
|
foreach (var chunk in sender.Concat(content))
|
|
chunk.Message = this;
|
|
}
|
|
|
|
private int GenerateHash()
|
|
{
|
|
return SortCode.GetHashCode()
|
|
^ ExtraChatChannel.GetHashCode()
|
|
^ string.Join("", Sender.Select(c => c.StringValue())).GetHashCode()
|
|
^ string.Join("", Content.Select(c => c.StringValue())).GetHashCode();
|
|
}
|
|
|
|
private Guid ExtractExtraChatChannel()
|
|
{
|
|
if (ContentSource.Payloads.Count > 0 && ContentSource.Payloads[0] is RawPayload raw)
|
|
{
|
|
// this does an encode and clone every time it's accessed, so cache
|
|
var data = raw.Data;
|
|
if (data[1] == 0x27 && data[2] == 18 && data[3] == 0x20)
|
|
return new Guid(data[4..^1]);
|
|
}
|
|
|
|
return Guid.Empty;
|
|
}
|
|
|
|
private List<Chunk> CheckMessageContent(List<Chunk> content)
|
|
{
|
|
var newChunks = new List<Chunk>();
|
|
void AddChunkWithMessage(Chunk chunk)
|
|
{
|
|
chunk.Message = this;
|
|
newChunks.Add(chunk);
|
|
}
|
|
|
|
foreach (var chunk in content)
|
|
{
|
|
// Use as is if it's not a text chunk or it already has a payload.
|
|
if (chunk is not TextChunk text || chunk.Link != null)
|
|
{
|
|
// No need to call AddChunkWithMessage here since the chunk
|
|
// already has the Message field set.
|
|
newChunks.Add(chunk);
|
|
continue;
|
|
}
|
|
|
|
// We replace every emote before checking for URLs
|
|
var builder = new StringBuilder();
|
|
foreach (var word in text.Content.Split(" "))
|
|
{
|
|
if (EmoteCache.Exists(word))
|
|
{
|
|
// We add all the previous collected text parts
|
|
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, builder.ToString()));
|
|
builder.Clear();
|
|
|
|
newChunks.Add(new TextChunk(chunk.Source, EmotePayload.ResolveEmote(word), "Cool BetterTTV"));
|
|
builder.Append(' ');
|
|
continue;
|
|
}
|
|
|
|
if (URLRegex.IsMatch(word))
|
|
{
|
|
// We add all the previous collected text parts
|
|
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, builder.ToString()));
|
|
builder.Clear();
|
|
|
|
// Create a new TextChunk with a URIPayload for the URL text.
|
|
try
|
|
{
|
|
var link = UriPayload.ResolveURI(word);
|
|
AddChunkWithMessage(text.NewWithStyle(chunk.Source, link, word));
|
|
builder.Append(' ');
|
|
|
|
continue;
|
|
}
|
|
catch (UriFormatException)
|
|
{
|
|
Plugin.Log.Debug($"Invalid URL accepted by Regex but failed URI parsing: '{word}'");
|
|
}
|
|
}
|
|
|
|
builder.Append($"{word} ");
|
|
}
|
|
// We add the leftovers
|
|
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, builder.ToString()[..^1]));
|
|
}
|
|
|
|
return newChunks;
|
|
}
|
|
|
|
/// <summary>
|
|
/// URLRegex returns a regex object that matches URLs like:
|
|
/// - https://example.com
|
|
/// - http://example.com
|
|
/// - www.example.com
|
|
/// - https://sub.example.com
|
|
/// - example.com
|
|
/// - sub.example.com
|
|
///
|
|
/// It matches URLs with www. or https:// prefix, and also matches URLs
|
|
/// without a prefix on specific TLDs.
|
|
/// </summary>
|
|
private static Regex URLRegex = new(
|
|
@"((https?:\/\/|www\.)[a-z0-9-]+(\.[a-z0-9-]+)*|([a-z0-9-]+(\.[a-z0-9-]+)*\.(com|net|org|co|io|app)))(:[\d]{1,5})?(\/[^\s]+)?",
|
|
RegexOptions.Compiled | RegexOptions.IgnoreCase
|
|
);
|
|
}
|