using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using TwitchDownloaderCore.Interfaces;
using TwitchDownloaderCore.Tools;
using TwitchDownloaderCore.TwitchObjects;
namespace TwitchDownloaderCore.Chat
{
public static class ChatHtml
{
// TODO: Add support for embedding Twitch bits in HTML chats
///
/// Serializes a chat Html file.
///
public static async Task SerializeAsync(Stream outputStream, string filePath, ChatRoot chatRoot, ITaskLogger logger, bool embedData = true, CancellationToken cancellationToken = default)
{
Dictionary thirdEmoteData = new();
await BuildThirdPartyDictionary(chatRoot, embedData, thirdEmoteData, logger, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
Dictionary chatBadgeData = new();
await BuildChatBadgesDictionary(chatRoot, embedData, chatBadgeData, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
using var templateStream = new MemoryStream(Properties.Resources.chat_template);
using var templateReader = new StreamReader(templateStream);
await using var sw = new StreamWriter(outputStream, leaveOpen: true);
while (!templateReader.EndOfStream)
{
var line = await templateReader.ReadLineAsync();
switch (line)
{
case "":
await sw.WriteLineAsync(HttpUtility.HtmlEncode(Path.GetFileNameWithoutExtension(filePath)));
break;
case "/* [CUSTOM CSS] */":
if (embedData)
{
foreach (var emote in chatRoot.embeddedData.firstParty)
{
await sw.WriteLineAsync(".first-" + emote.id + " { content:url(\"data:image/png;base64, " + Convert.ToBase64String(emote.data) + "\"); }");
}
foreach (var emote in chatRoot.embeddedData.thirdParty)
{
await sw.WriteLineAsync(".third-" + emote.id + " { content:url(\"data:image/png;base64, " + Convert.ToBase64String(emote.data) + "\"); }");
}
foreach (var badge in chatRoot.embeddedData.twitchBadges)
{
foreach(var (version, badgeData) in badge.versions)
{
await sw.WriteLineAsync(".badge-" + badge.name + "-" + version + " { content:url(\"data:image/png;base64, " + Convert.ToBase64String(badgeData.bytes) + "\"); }");
}
}
}
break;
case "":
foreach (var comment in chatRoot.comments)
{
var relativeTime = TimeSpan.FromSeconds(comment.content_offset_seconds);
var timestamp = TimeSpanHFormat.ReusableInstance.Format(@"H\:mm\:ss", relativeTime);
await sw.WriteLineAsync($"");
}
break;
default:
await sw.WriteLineAsync(line);
break;
}
}
}
private static async Task BuildThirdPartyDictionary(ChatRoot chatRoot, bool embedData, Dictionary thirdEmoteData, ITaskLogger logger, CancellationToken cancellationToken)
{
EmoteResponse emotes = await TwitchHelper.GetThirdPartyEmotesMetadata(chatRoot.streamer.id, true, true, true, true, logger, cancellationToken);
List itemList = new();
itemList.AddRange(emotes.BTTV);
itemList.AddRange(emotes.FFZ);
itemList.AddRange(emotes.STV);
foreach (var item in itemList.Where(item => !thirdEmoteData.ContainsKey(item.Code)))
{
if (embedData)
{
EmbedEmoteData embedEmoteData = chatRoot.embeddedData.thirdParty.FirstOrDefault(x => x.id == item.Id);
if (embedEmoteData != null)
{
embedEmoteData.url = item.ImageUrl.Replace("[scale]", "1");
thirdEmoteData[item.Code] = embedEmoteData;
}
}
else
{
EmbedEmoteData embedEmoteData = new();
embedEmoteData.url = item.ImageUrl.Replace("[scale]", "1");
thirdEmoteData[item.Code] = embedEmoteData;
}
}
}
private static async Task BuildChatBadgesDictionary(ChatRoot chatRoot, bool embedData, Dictionary chatBadgeData, CancellationToken cancellationToken)
{
// No need to build the dictionary if badges are embedded
if (embedData)
return;
List badges = await TwitchHelper.GetChatBadgesData(chatRoot.comments, chatRoot.streamer.id, cancellationToken);
foreach (var badge in badges)
{
chatBadgeData[badge.name] = badge;
}
}
private static string GetChatBadgesHtml(bool embedData, IReadOnlyDictionary chatBadgeData, Comment comment)
{
if (comment.message.user_badges is null || comment.message.user_badges.Count == 0)
return "";
var badgesHtml = new List(comment.message.user_badges!.Count);
foreach (var messageBadge in comment.message.user_badges)
{
if (embedData)
{
badgesHtml.Add($"
{messageBadge._id}");
}
else
{
if (!chatBadgeData.TryGetValue(messageBadge._id, out var badgeId))
continue;
if (!badgeId.versions.TryGetValue(messageBadge.version, out var badge))
continue;
badgesHtml.Add($"
{messageBadge._id}");
}
}
badgesHtml.Add(""); // Ensure the html string ends with a space
return string.Join(' ', badgesHtml);
}
private static string GetMessageHtml(bool embedEmotes, IReadOnlyDictionary thirdEmoteData, ChatRoot chatRoot, Comment comment)
{
var message = new StringBuilder(comment.message.body.Length);
comment.message.fragments ??= new List { new() { text = comment.message.body } };
foreach (var fragment in comment.message.fragments)
{
if (fragment.emoticon == null)
{
foreach (var word in fragment.text.Split(' '))
{
if (thirdEmoteData.ContainsKey(word))
{
if (embedEmotes)
{
message.Append($"
{word} ");
}
else
{
message.Append($"
{word} ");
}
}
else if (word != "")
{
message.Append(HttpUtility.HtmlEncode(word));
message.Append(' ');
}
}
}
else
{
if (embedEmotes && chatRoot.embeddedData.firstParty.Any(x => x.id == fragment.emoticon.emoticon_id))
{
message.Append($"
{fragment.text} ");
}
else
{
message.Append($"
{fragment.text} ");
}
}
}
return message.ToString();
}
}
}