diff --git a/spec/invidious/comments/content_spec.cr b/spec/invidious/comments/content_spec.cr
new file mode 100644
index 000000000..ea7b8102d
--- /dev/null
+++ b/spec/invidious/comments/content_spec.cr
@@ -0,0 +1,55 @@
+require "../../parsers_helper.cr"
+
+Spectator.describe "YouTube comment descriptions" do
+ it "renders inline custom emoji attachments" do
+ content = JSON.parse(%({
+ "content":"Before:star:after",
+ "attachmentRuns":[{
+ "startIndex":6,
+ "length":6,
+ "element":{"type":{"imageType":{"image":{"sources":[{
+ "url":"https://lh3.googleusercontent.com/custom=s16-w24-h24-c-k-nd",
+ "width":16,
+ "height":16
+ }]}}}},
+ "properties":{"accessibilityProperties":{"label":"star"}}
+ }]
+ }))
+
+ expect(parse_description(content, "video-id")).to eq(
+ %(Before
after)
+ )
+ end
+
+ it "proxies ggpht attachments and preserves unsupported attachments" do
+ content = JSON.parse(%({
+ "content":"one:emoji:two:unknown:three",
+ "attachmentRuns":[
+ {
+ "startIndex":3,
+ "length":7,
+ "element":{"type":{"imageType":{"image":{"sources":[{
+ "url":"https://yt3.ggpht.com/emoji/test?s=16",
+ "width":16,
+ "height":16
+ }]}}}},
+ "properties":{"accessibilityProperties":{"label":"emoji"}}
+ },
+ {
+ "startIndex":13,
+ "length":9,
+ "element":{"type":{"imageType":{"image":{"sources":[{
+ "url":"https://example.com/unknown.png",
+ "width":16,
+ "height":16
+ }]}}}},
+ "properties":{"accessibilityProperties":{"label":"unknown"}}
+ }
+ ]
+ }))
+
+ expect(parse_description(content, "video-id")).to eq(
+ %(one
two:unknown:three)
+ )
+ end
+end
diff --git a/src/invidious/videos/description.cr b/src/invidious/videos/description.cr
index 2c9832690..551d3c6c4 100644
--- a/src/invidious/videos/description.cr
+++ b/src/invidious/videos/description.cr
@@ -40,6 +40,46 @@ private def copy_string(str : String::Builder, iter : Iterator, count : Int) : I
return copied
end
+private def image_source(url : String) : String?
+ uri = URI.parse(url)
+
+ case uri.host
+ when "yt3.ggpht.com"
+ "/ggpht#{uri.request_target}"
+ when "lh3.googleusercontent.com"
+ # Google serves these image paths from the ggpht host as well, keeping
+ # inline emoji behind Invidious' existing image proxy and CSP.
+ "/ggpht#{uri.request_target}"
+ end
+rescue
+ nil
+end
+
+private def render_attachment(attachment : JSON::Any) : String?
+ source = attachment.dig?("element", "type", "imageType", "image", "sources", 0)
+ return if source.nil?
+
+ source_url = source["url"]?.try &.as_s
+ return if source_url.nil?
+
+ src = image_source(source_url)
+ return if src.nil?
+
+ alt = attachment.dig?("properties", "accessibilityProperties", "label").try &.as_s || ""
+ width = source["width"]?.try &.as_i || 16_i64
+ height = source["height"]?.try &.as_i || 16_i64
+
+ String.build do |str|
+ str << %(![)
+ str << HTML.escape(alt)
+ str << %( )
+ str << HTML.escape(alt)
+ str << %(]()
+ str << HTML.escape(src)
+ str << %()