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( + %(Beforestarafter) + ) + 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( + %(oneemojitwo: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 << %(