Skip to content

Instantly share code, notes, and snippets.

@StoneyEagle
Created July 20, 2025 20:30
Show Gist options
  • Select an option

  • Save StoneyEagle/a823ca929d5b7e347ffdb0004393c5cd to your computer and use it in GitHub Desktop.

Select an option

Save StoneyEagle/a823ca929d5b7e347ffdb0004393c5cd to your computer and use it in GitHub Desktop.
using System.Text;
using NoMercyBot.Database.Models.ChatMessage;
using NoMercyBot.Services.Emotes;
using NoMercyBot.Services.Emotes.Dto;
using NoMercyBot.Services.Other;
namespace NoMercyBot.Services.Twitch;
public class TwitchMessageDecorator
{
private readonly FrankerFacezService _frankerFacezService;
private readonly BttvService _bttvService;
private readonly SevenTvService _sevenTvService;
private readonly HtmlMetadataService _htmlMetadataService;
private ChatMessage ChatMessage { get; set; }
private List<ChatMessageFragment> _fragments = [];
public TwitchMessageDecorator(
FrankerFacezService frankerFacezService,
BttvService bttvService,
SevenTvService sevenTvService,
HtmlMetadataService htmlMetadataService)
{
_frankerFacezService = frankerFacezService;
_bttvService = bttvService;
_sevenTvService = sevenTvService;
_htmlMetadataService = htmlMetadataService;
ChatMessage = new();
}
public void DecorateMessage(ChatMessage chatMessage)
{
ChatMessage = chatMessage;
// Copy the fragments to a new list to avoid modifying the original message
_fragments = chatMessage.Fragments.ToList();
// Split up all text fragments into individual word fragments so that we can decorate them with emotes
ExplodeTextFragments();
DecorateTwitchEmotes();
DecorateFrankerFaceEmotes();
DecorateBttvEmotes();
DecorateSevenTvEmotes();
DecorateUrlFragments();
// Merge all consecutive text fragments back together to avoid having too many text fragments
ImplodeTextFragments();
DecorateCodeSnippet();
// Overwrite the original fragments with the modified ones
ChatMessage.Fragments = _fragments;
}
private void ExplodeTextFragments()
{
List<ChatMessageFragment> newFragments = [];
foreach (ChatMessageFragment fragment in _fragments.ToList())
{
int index = _fragments.IndexOf(fragment);
if (fragment.Type != "text")
{
newFragments.Add(fragment);
if (_fragments.ElementAtOrDefault(index - 0)?.Text != " ")
{
newFragments.Add(new()
{
Type = "text",
Text = " ",
});
}
continue;
}
string[] words = fragment.Text.Split(' ', StringSplitOptions.RemoveEmptyEntries);
foreach (string word in words)
{
if (newFragments.Count > 0 || _fragments.ElementAtOrDefault(index - 0)?.Text != " ")
{
newFragments.Add(new()
{
Type = "text",
Text = " ",
});
}
newFragments.Add(new()
{
Type = "text",
Text = string.IsNullOrEmpty(word)
? $"{word} "
: word,
});
if(newFragments.LastOrDefault()?.Text != " ")
{
newFragments.Add(new()
{
Type = "text",
Text = " ",
});
}
}
}
if(newFragments.FirstOrDefault()?.Text == " ")
{
newFragments.RemoveAt(0);
}
if(newFragments.LastOrDefault()?.Text == " ")
{
newFragments.RemoveAt(newFragments.Count - 1);
}
_fragments = newFragments;
}
private void ImplodeTextFragments()
{
List<ChatMessageFragment> newFragments = [];
StringBuilder currentText = new();
foreach (ChatMessageFragment fragment in _fragments.ToList())
{
if (fragment.Type == "text")
{
currentText.Append(fragment.Text);
}
else
{
if (currentText.Length > 0)
{
newFragments.Add(new()
{
Type = "text",
Text = currentText.ToString()
});
currentText.Clear();
}
newFragments.Add(fragment);
}
}
if (currentText.Length > 0)
{
newFragments.Add(new()
{
Type = "text",
Text = currentText.ToString()
});
}
if(newFragments.LastOrDefault()?.Text == " ")
{
newFragments.RemoveAt(newFragments.Count - 1);
}
_fragments = newFragments;
}
private void DecorateTwitchEmotes()
{
foreach (ChatMessageFragment fragment in _fragments.ToList())
{
if (fragment.Type != "emote" || fragment.Emote is null) continue;
int index = _fragments.IndexOf(fragment);
_fragments[index] = new()
{
Type = fragment.Type,
Text = fragment.Text,
Cheermote = fragment.Cheermote,
Mention = fragment.Mention,
Emote = new()
{
Id = fragment.Emote.Id,
Format = fragment.Emote.Format,
OwnerId = fragment.Emote.OwnerId,
EmoteSetId = fragment.Emote.EmoteSetId,
Urls = new()
{
{ "1", new($"https://static-cdn.jtvnw.net/emoticons/v2/{fragment.Emote.Id}/default/dark/1.0") },
{ "2", new($"https://static-cdn.jtvnw.net/emoticons/v2/{fragment.Emote.Id}/default/dark/2.0") },
{ "3", new($"https://static-cdn.jtvnw.net/emoticons/v2/{fragment.Emote.Id}/default/dark/3.0") }
}
}
};
}
}
private void DecorateFrankerFaceEmotes()
{
foreach (ChatMessageFragment fragment in _fragments.ToList())
{
if (fragment.Type != "text" || string.IsNullOrWhiteSpace(fragment.Text)) continue;
Emoticon? emote = _frankerFacezService.FrankerFacezEmotes
.FirstOrDefault(e => e.Name.Equals(fragment.Text, StringComparison.OrdinalIgnoreCase));
if (emote == null) continue;
int index = _fragments.IndexOf(fragment);
_fragments[index] = new()
{
Type = "emote",
Text = fragment.Text,
Emote = new()
{
Id = emote.Id.ToString(),
Urls = emote.Urls,
}
};
}
}
private void DecorateBttvEmotes()
{
foreach (ChatMessageFragment fragment in _fragments.ToList())
{
if (fragment.Type != "text" || string.IsNullOrWhiteSpace(fragment.Text)) continue;
BttvEmote? emote = _bttvService.BttvEmotes
.FirstOrDefault(e => e.Code.Equals(fragment.Text, StringComparison.OrdinalIgnoreCase));
if (emote == null) continue;
int index = _fragments.IndexOf(fragment);
_fragments[index] = new()
{
Type = "emote",
Text = fragment.Text,
Emote = new()
{
Id = emote.Id,
Urls = new()
{
{ "1", new($"https://cdn.betterttv.net/emote/{emote.Id}/1x") },
{ "2", new($"https://cdn.betterttv.net/emote/{emote.Id}/2x") },
{ "3", new($"https://cdn.betterttv.net/emote/{emote.Id}/3x") }
}
}
};
}
}
private void DecorateSevenTvEmotes()
{
foreach (ChatMessageFragment fragment in _fragments.ToList())
{
if (fragment.Type != "text" || string.IsNullOrWhiteSpace(fragment.Text)) continue;
SevenTvEmote? emote = _sevenTvService.SevenTvEmotes
.FirstOrDefault(e => e.Name.Equals(fragment.Text, StringComparison.OrdinalIgnoreCase));
if (emote == null) continue;
int index = _fragments.IndexOf(fragment);
_fragments[index] = new()
{
Type = "emote",
Text = fragment.Text,
Emote = new()
{
Id = emote.Id,
Urls = new()
{
{ "1", new($"https://7tv.app/emotes/{emote.Id}/1x") },
{ "2", new($"https://7tv.app/emotes/{emote.Id}/2x") },
{ "3", new($"https://7tv.app/emotes/{emote.Id}/3x") },
{ "4", new($"https://7tv.app/emotes/{emote.Id}/4x") },
}
}
};
}
}
private void DecorateCodeSnippet()
{
throw new NotImplementedException();
}
private void DecorateUrlFragments()
{
foreach (ChatMessageFragment fragment in _fragments.ToList())
{
if (fragment.Type != "text" || string.IsNullOrWhiteSpace(fragment.Text)) continue;
// Check if the text is a URL
if (Uri.TryCreate(fragment.Text, UriKind.Absolute, out Uri? uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
{
_htmlMetadataService.Initialize(uri);
_htmlMetadataService.DecorateOgData();
_htmlMetadataService.DecorateYoutube();
_htmlMetadataService.DecorateTwitch();
int index = _fragments.IndexOf(fragment);
_fragments[index] = new()
{
Type = "url",
Text = fragment.Text,
HtmlPreviewCustomContent = _htmlMetadataService.MakeComponent(),
};
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment