From 46e6e4407625428556f856c2475b2819c46b0aca Mon Sep 17 00:00:00 2001 From: Arit Amana <32520970+msarit@users.noreply.github.com> Date: Wed, 5 Jan 2022 18:27:46 -0500 Subject: [PATCH] Implement TwitchTV Unified Embed (#15968) * still building * complete twitch unifiedembed implementation * quick youtube regexp fix * add some documentation --- app/liquid_tags/twitch_tag.rb | 57 +++++++++++++++---- app/liquid_tags/youtube_tag.rb | 4 +- app/views/liquids/_twitch.html.erb | 6 +- spec/liquid_tags/twitch_tag_spec.rb | 37 ++++++++---- .../unified_embed/registry_spec.rb | 15 +++++ 5 files changed, 94 insertions(+), 25 deletions(-) diff --git a/app/liquid_tags/twitch_tag.rb b/app/liquid_tags/twitch_tag.rb index 91e01e41d..393db6f23 100644 --- a/app/liquid_tags/twitch_tag.rb +++ b/app/liquid_tags/twitch_tag.rb @@ -1,10 +1,13 @@ class TwitchTag < LiquidTagBase PARTIAL = "liquids/twitch".freeze + REGISTRY_REGEXP = %r{https://(?:clips|player|www).twitch.tv/(?:(?:embed\?clip=|\w+/clip/)|(?:\?video=|videos/))(?[a-zA-Z0-9-]{,100})(?:&[^$]+)?} + VALID_VIDEO_REGEXP = /\A(?\d+)\Z/ + VALID_CLIP_REGEXP = /\A(?[a-zA-Z0-9-]{,100})\Z/ + REGEXP_OPTIONS = [VALID_VIDEO_REGEXP, VALID_CLIP_REGEXP, REGISTRY_REGEXP].freeze - def initialize(_tag_name, slug, _parse_context) + def initialize(_tag_name, input, _parse_context) super - @url = parsed_url(Settings::General.app_domain) - @slug = parsed_slug(slug) + @url = parsed_input(strip_tags(input)) @width = 710 @height = 399 end @@ -14,7 +17,6 @@ class TwitchTag < LiquidTagBase partial: PARTIAL, locals: { url: @url, - slug: @slug, width: @width, height: @height }, @@ -23,15 +25,50 @@ class TwitchTag < LiquidTagBase private - # Strip out port number because it confuses Twitch - def parsed_url(url) - url.split(":")[0] + # The `parsed_input` method handles two Twitch Liquid Tag use-cases: + # `{% twitch %}` + # `{% embed %}` + # + # The iframe src for a video is different from that for a clip. + # + # In the case of a video_id or clip_slug, this method validates the input + # and then returns the appropriate src. + # + # In the case of a url, this method validates the input, then determines + # whether the id contained in the url is a video_id or clip_slug. + # + # Seeing as the clip_slug regexp would match a video_id, the check + # against the video_id regexp occurs first 😅 + + def parsed_input(input) + input = input.split("&")[0] # prevent param injection + + match = pattern_match_for(input, REGEXP_OPTIONS) + raise StandardError, "Invalid Twitch ID, Slug or URL" unless match + + return player_url(match[:video_id]) if match.names.include?("video_id") + return clip_url(match[:clip_slug]) if match.names.include?("clip_slug") + return player_or_clip_url(match) if match.names.include?("id") end - # prevent param injection - def parsed_slug(slug) - slug.strip.split("&")[0] + def clip_url(id) + "https://clips.twitch.tv/embed?clip=#{id}&parent=#{parent_url}&autoplay=false" + end + + def player_url(id) + "https://player.twitch.tv/?video=#{id}&parent=#{parent_url}&autoplay=false" + end + + def player_or_clip_url(match) + return player_url(match[:id]) if match[:id].match?(VALID_VIDEO_REGEXP) + return clip_url(match[:id]) if match[:id].match?(VALID_CLIP_REGEXP) + end + + def parent_url + Settings::General.app_domain.split(":")[0] end end Liquid::Template.register_tag("twitch", TwitchTag) + +UnifiedEmbed.register(TwitchTag, regexp: TwitchTag::REGISTRY_REGEXP) diff --git a/app/liquid_tags/youtube_tag.rb b/app/liquid_tags/youtube_tag.rb index ed56731cd..61c748389 100644 --- a/app/liquid_tags/youtube_tag.rb +++ b/app/liquid_tags/youtube_tag.rb @@ -1,8 +1,8 @@ class YoutubeTag < LiquidTagBase PARTIAL = "liquids/youtube".freeze # rubocop:disable Layout/LineLength - REGISTRY_REGEXP = %r{https?://(?:www\.)?(?:youtube\.com|youtu\.be)/(?:embed/|watch\?v=)?(?[a-zA-Z0-9_-]{11})(?:\?|&)?(?:t=|start=)?(?(?:\d{1,}h?)?(?:\d{1,2}m)?(?:\d{1,2}s)?{5,11})?} - VALID_ID_REGEXP = /\A(?[a-zA-Z0-9_-]{11})(?:(?:&|\?)(?:t=|start=)(?(?:\d{1,}h?)?(?:\d{1,2}m)?(?:\d{1,2}s)?))?\Z/ + REGISTRY_REGEXP = %r{https?://(?:www\.)?(?:youtube\.com|youtu\.be)/(?:embed/|watch\?v=)?(?[a-zA-Z0-9_-]{11})(?:(?:&|\?)(?:t=|start=)(?(?:\d{1,}h)?(?:\d{1,2}m)?(?:\d{1,2}s)?))?} + VALID_ID_REGEXP = /\A(?[a-zA-Z0-9_-]{11})(?:(?:&|\?)(?:t=|start=)(?(?:\d{1,}h)?(?:\d{1,2}m)?(?:\d{1,2}s)?))?\Z/ # rubocop:enable Layout/LineLength REGEXP_OPTIONS = [REGISTRY_REGEXP, VALID_ID_REGEXP].freeze diff --git a/app/views/liquids/_twitch.html.erb b/app/views/liquids/_twitch.html.erb index c490d1156..9c2ae4380 100644 --- a/app/views/liquids/_twitch.html.erb +++ b/app/views/liquids/_twitch.html.erb @@ -1,6 +1,8 @@ diff --git a/spec/liquid_tags/twitch_tag_spec.rb b/spec/liquid_tags/twitch_tag_spec.rb index 23abff33d..38091672d 100644 --- a/spec/liquid_tags/twitch_tag_spec.rb +++ b/spec/liquid_tags/twitch_tag_spec.rb @@ -2,26 +2,41 @@ require "rails_helper" require "nokogiri" RSpec.describe TwitchTag, type: :liquid_tag do - let(:slug) { "CuteSpicyNostrilDoritosChip" } + let(:clip_slug) { "CuteSpicyNostrilDoritosChip" } + let(:video_id) { "1196406756" } - def assert_parses(slug, token) + def assert_parses_clip(slug, token) liquid = Liquid::Template.parse("{% twitch #{token} %}").render - expect(liquid).to include "https://clips.twitch.tv/embed?autoplay=false&clip=#{slug}&parent=localhost" + expect(liquid).to include "https://clips.twitch.tv/embed?clip=#{slug}&parent=localhost&autoplay=false" end - it "accepts twitch clip slug" do - assert_parses slug, slug + def assert_parses_video(id, token) + liquid = Liquid::Template.parse("{% twitch #{token} %}").render + expect(liquid).to include "https://player.twitch.tv/?video=#{id}&parent=localhost&autoplay=false" end - it "accepts twitch clip slug with wonky whitespace" do - assert_parses slug, " #{slug} \t" + context "when twitch clip slug passed in" do + it "accepts slug" do + assert_parses_clip clip_slug, clip_slug + end + + it "accepts slug with wonky whitespace" do + assert_parses_clip clip_slug, " #{clip_slug} \t" + end end - it "forbids inserting autoplay option" do - assert_parses slug, "#{slug}&autoplay=true" + context "when twitch video id passed in" do + it "accepts id" do + assert_parses_video video_id, video_id + end + + it "accepts id with wonky whitespace" do + assert_parses_video video_id, " #{video_id} \t" + end end - it "forbids inserting mute option" do - assert_parses slug, "#{slug}&muted=true" + it "prevents param injection" do + assert_parses_clip clip_slug, "#{clip_slug}&autoplay=true" + assert_parses_video video_id, "#{video_id}&muted=true" end end diff --git a/spec/liquid_tags/unified_embed/registry_spec.rb b/spec/liquid_tags/unified_embed/registry_spec.rb index e43ef7c50..ab46d0755 100644 --- a/spec/liquid_tags/unified_embed/registry_spec.rb +++ b/spec/liquid_tags/unified_embed/registry_spec.rb @@ -25,6 +25,14 @@ RSpec.describe UnifiedEmbed::Registry do "instagram.com/p/CXgzXWXroHK/", ] + valid_twitch_url_formats = [ + "https://clips.twitch.tv/embed?clip=SpeedyVivaciousDolphinKappaRoss-IQl5YslMAGKbMOGM&parent=www.example.com", + "https://player.twitch.tv/?video=1222841752&parent=www.example.com", + "https://player.twitch.tv/?video=1222841752", + "https://www.twitch.tv/videos/1250164963", + "https://www.twitch.tv/monchi_tv/clip/CrepuscularSparklingGalagoBudBlast-ij3jvc4r437D4L4L", + ] + valid_vimeo_url_formats = [ "https://player.vimeo.com/video/652446985?h=a68f6ed1f5", "https://vimeo.com/ondemand/withchude/647355334", @@ -98,6 +106,13 @@ RSpec.describe UnifiedEmbed::Registry do .to eq(SoundcloudTag) end + it "returns Twitch for a valid twitch url" do + valid_twitch_url_formats.each do |url| + expect(described_class.find_liquid_tag_for(link: url)) + .to eq(TwitchTag) + end + end + it "returns TwitterTimelineTag for a twitter timeline url" do expect(described_class.find_liquid_tag_for(link: "https://twitter.com/FreyaHolmer/timelines/1215413954505297922")) .to eq(TwitterTimelineTag)