Implement Unified Embeds for StackExchange and StackOverflow URLs (#16415)

* complete implementation and add specs

* improvements

* more robust condition

* update API version; re-record VCR cassettes

* PR review comments
This commit is contained in:
Arit Amana 2022-02-07 11:21:11 -05:00 committed by GitHub
parent 0559a808c4
commit 63c957b12d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 108 additions and 78 deletions

View file

@ -1,7 +1,11 @@
class StackexchangeTag < LiquidTagBase
PARTIAL = "liquids/stackexchange".freeze
# update API version here and in stackexchange_tag_spec when a new version is out
API_URL = "https://api.stackexchange.com/2.2/".freeze
REGISTRY_REGEXP = %r{https://(?:(?<subdomain>\w+)\.)?(?:stackexchange\.com|stackoverflow\.com)/(?<post_type>q|a|questions)/(?<id>\d{1,20})}
ID_REGEXP = /\A(?<id>\d{1,20})\Z/
SITE_REGEXP = /(?<subdomain>\b[a-zA-Z]+\b)/
REGEXP_OPTIONS = [REGISTRY_REGEXP, ID_REGEXP, SITE_REGEXP].freeze
STACKOVERFLOW_REGEXP = %r{https://stackoverflow\.com/(q|a|questions)/\d{1,20}(?:/[\w\-]+)?}
API_URL = "https://api.stackexchange.com/2.3/".freeze
# Filter codes come from the example tools in the docs. For example: https://api.stackexchange.com/docs/posts-by-ids
FILTERS = {
"post" => "!3tz1WbZW5JxrG-f99",
@ -9,16 +13,16 @@ class StackexchangeTag < LiquidTagBase
"question" => "!*1SgQGDOL9bPBHULz9sKS.y6qv7V9fYNszvdhDuv5",
"site" => "!mWxO_PNa4i"
}.freeze
ID_REGEXP = /\A\d{1,20}\z/
attr_reader :site, :post_type
def initialize(_tag_name, input, _parse_context)
super
@site = parse_site(input.strip)
@post_type = "question"
@json_content = get_data(input.strip)
stripped_input = strip_tags(input)
unescaped_input = CGI.unescape_html(stripped_input)
@site = parse_site(unescaped_input)
@json_content, @post_type = get_data(unescaped_input)
end
def render(_context)
@ -42,43 +46,29 @@ class StackexchangeTag < LiquidTagBase
private
def valid_site?(site)
(site =~ /[a-z.]+/i)&.zero?
end
def parse_site(input)
return "stackoverflow" if tag_name == "stackoverflow"
return "stackoverflow" if tag_name == "stackoverflow" || input.match?(STACKOVERFLOW_REGEXP)
site = input.match(/[a-z.]+/i)[0]
unless valid_site?(site)
raise StandardError, I18n.t("liquid_tags.stackexchange_tag.invalid_site", tag: tag_name, input: input)
end
match = pattern_match_for(input, REGEXP_OPTIONS)
# rubocop:disable Layout/LineLength
raise StandardError, I18n.t("liquid_tags.stackexchange_tag.invalid_site", tag: tag_name, input: input) unless match && match_group_present?(match, "subdomain")
# rubocop:enable Layout/LineLength
site
match[:subdomain].downcase
end
def valid_input?(input)
return false if input.nil?
ID_REGEXP.match?(input.split.first)
end
def handle_response_error(response, input)
raise StandardError, "Calling StackExchange API failed: #{response&.error_message}" if response.code != 200
return unless response["items"].length.zero?
raise StandardError, I18n.t("liquid_tags.stackexchange_tag.post_not_found", tag: tag_name, input: input)
def match_group_present?(match, group_name)
match.names.include?(group_name)
end
def get_data(input)
unless valid_input?(input)
id = input.split.first
match = pattern_match_for(id, REGEXP_OPTIONS)
unless match && match_group_present?(match, "id")
raise StandardError, I18n.t("liquid_tags.stackexchange_tag.invalid_id", tag: tag_name, input: input)
end
id = input.split.first
url = "#{API_URL}posts/#{id}?site=#{@site}&filter=#{FILTERS['post']}" \
url = "#{API_URL}posts/#{match[:id]}?site=#{@site}&filter=#{FILTERS['post']}" \
"&key=#{ApplicationConfig['STACK_EXCHANGE_APP_KEY']}"
post_response = HTTParty.get(url)
@ -86,15 +76,24 @@ class StackexchangeTag < LiquidTagBase
@post_type = post_response["items"][0]["post_type"]
url = "#{API_URL}#{@post_type.pluralize}/#{id}?site=#{@site}" \
url = "#{API_URL}#{@post_type.pluralize}/#{match[:id]}?site=#{@site}" \
"&filter=#{FILTERS[@post_type]}&key=#{ApplicationConfig['STACK_EXCHANGE_APP_KEY']}"
final_response = HTTParty.get(url)
handle_response_error(final_response, input)
final_response["items"][0]
[final_response["items"][0], @post_type]
end
def handle_response_error(response, input)
raise StandardError, "Calling StackExchange API failed: #{response&.error_message}" unless response.ok?
return unless response["items"].length.zero?
raise StandardError, I18n.t("liquid_tags.stackexchange_tag.post_not_found", tag: tag_name, input: input)
end
end
Liquid::Template.register_tag("stackoverflow", StackexchangeTag)
Liquid::Template.register_tag("stackexchange", StackexchangeTag)
UnifiedEmbed.register(StackexchangeTag, regexp: StackexchangeTag::REGISTRY_REGEXP)

View file

@ -68,6 +68,15 @@ RSpec.describe UnifiedEmbed::Registry do
"https://stackblitz.com/edit/web-platform-3tqbd4?embed=1&file=index.html&theme=light",
]
valid_stackexchange_stackoverflow_url_formats = [
"https://travel.stackexchange.com/questions/172014/is-it-okay-to-mix-in-local-language-when-i-know-it-poorly",
"https://diy.stackexchange.com/q/244088",
"https://academia.stackexchange.com/a/181893",
"https://stackoverflow.com/q/70974409/9091371",
"https://stackoverflow.com/questions/70976451/changing-h1-element-based-on-input-element-content-vanilla-js",
"https://stackoverflow.com/a/70976251/9091371",
]
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",
@ -226,6 +235,13 @@ RSpec.describe UnifiedEmbed::Registry do
.to eq(StackeryTag)
end
it "returns StackexchangeTag for a valid stackexchange or stackoverflow url", :aggregate_failures do
valid_stackexchange_stackoverflow_url_formats.each do |url|
expect(described_class.find_liquid_tag_for(link: url))
.to eq(StackexchangeTag)
end
end
it "returns TweetTag for a tweet url" do
expect(described_class.find_liquid_tag_for(link: "https://twitter.com/aritdeveloper/status/1483614684884484099"))
.to eq(TweetTag)

View file

@ -2,7 +2,7 @@
http_interactions:
- request:
method: get
uri: https://api.stackexchange.com/2.2/posts/59679915?filter=!3tz1WbZW5JxrG-f99&key=&site=askubuntu
uri: https://api.stackexchange.com/2.3/posts/59679915?filter=!3tz1WbZW5JxrG-f99&key=&site=askubuntu
body:
encoding: US-ASCII
string: ''
@ -18,12 +18,14 @@ http_interactions:
code: 200
message: OK
headers:
Cache-Control:
- private
Content-Length:
- '87'
Content-Type:
- application/json; charset=utf-8
Strict-Transport-Security:
- max-age=2592000
- max-age=15552000
Access-Control-Allow-Origin:
- "*"
Access-Control-Allow-Methods:
@ -33,12 +35,13 @@ http_interactions:
X-Content-Type-Options:
- nosniff
X-Request-Guid:
- 356f2fd4-3560-40c4-a494-6a4f0f361e62
- 39492868-d51e-49b4-9c87-d5d963306c83
Content-Security-Policy:
- upgrade-insecure-requests; frame-ancestors 'self' https://stackexchange.com
Date:
- Mon, 13 Jan 2020 12:23:24 GMT
- Fri, 04 Feb 2022 14:00:22 GMT
body:
encoding: ASCII-8BIT
string: '{"items":[],"has_more":false,"quota_max":300,"quota_remaining":287}'
http_version:
recorded_at: Mon, 13 Jan 2020 12:23:25 GMT
recorded_with: VCR 5.0.0
string: '{"items":[],"has_more":false,"quota_max":300,"quota_remaining":192}'
recorded_at: Fri, 04 Feb 2022 14:00:22 GMT
recorded_with: VCR 6.0.0

View file

@ -2,7 +2,7 @@
http_interactions:
- request:
method: get
uri: https://api.stackexchange.com/2.2/posts/1163633?filter=!3tz1WbZW5JxrG-f99&key=&site=askubuntu
uri: https://api.stackexchange.com/2.3/posts/1163633?filter=!3tz1WbZW5JxrG-f99&key=&site=askubuntu
body:
encoding: US-ASCII
string: ''
@ -18,12 +18,14 @@ http_interactions:
code: 200
message: OK
headers:
Cache-Control:
- private
Content-Length:
- '138'
- '139'
Content-Type:
- application/json; charset=utf-8
Strict-Transport-Security:
- max-age=2592000
- max-age=15552000
Access-Control-Allow-Origin:
- "*"
Access-Control-Allow-Methods:
@ -33,17 +35,18 @@ http_interactions:
X-Content-Type-Options:
- nosniff
X-Request-Guid:
- a8df412f-cc88-496c-ad89-9e40ad387650
- 1f73ab59-8040-47d7-9562-c3ffbff9be16
Content-Security-Policy:
- upgrade-insecure-requests; frame-ancestors 'self' https://stackexchange.com
Date:
- Mon, 13 Jan 2020 12:22:38 GMT
- Fri, 04 Feb 2022 14:00:21 GMT
body:
encoding: ASCII-8BIT
string: '{"items":[{"last_activity_date":1565109531,"post_type":"answer","post_id":1163633}],"has_more":false,"quota_max":300,"quota_remaining":289}'
http_version:
recorded_at: Mon, 13 Jan 2020 12:22:39 GMT
string: '{"items":[{"last_activity_date":1565109531,"post_type":"answer","post_id":1163633}],"has_more":false,"quota_max":300,"quota_remaining":192}'
recorded_at: Fri, 04 Feb 2022 14:00:21 GMT
- request:
method: get
uri: https://api.stackexchange.com/2.2/answers/1163633?filter=!.Fjr38AQkcvWfJTF-2exSL50At_pT&key=&site=askubuntu
uri: https://api.stackexchange.com/2.3/answers/1163633?filter=!.Fjr38AQkcvWfJTF-2exSL50At_pT&key=&site=askubuntu
body:
encoding: US-ASCII
string: ''
@ -59,12 +62,14 @@ http_interactions:
code: 200
message: OK
headers:
Cache-Control:
- private
Content-Length:
- '608'
- '607'
Content-Type:
- application/json; charset=utf-8
Strict-Transport-Security:
- max-age=2592000
- max-age=15552000
Access-Control-Allow-Origin:
- "*"
Access-Control-Allow-Methods:
@ -74,12 +79,14 @@ http_interactions:
X-Content-Type-Options:
- nosniff
X-Request-Guid:
- 9e23222f-b1d1-4841-9349-623e806a0321
- 5b3286b4-7d9b-44d9-9ed1-a2b0abda5ae4
Content-Security-Policy:
- upgrade-insecure-requests; frame-ancestors 'self' https://stackexchange.com
Date:
- Mon, 13 Jan 2020 12:22:39 GMT
- Fri, 04 Feb 2022 14:00:21 GMT
body:
encoding: ASCII-8BIT
string: '{"items":[{"tags":[],"comment_count":3,"is_accepted":false,"score":1,"last_activity_date":1565109531,"last_edit_date":1565109531,"creation_date":1565033637,"answer_id":1163633,"question_id":885432,"link":"https://askubuntu.com/questions/885432/help-setting-up-sshfs/1163633#1163633","title":"Help
string: '{"items":[{"tags":[],"comment_count":3,"is_accepted":false,"score":3,"last_activity_date":1565109531,"last_edit_date":1565109531,"creation_date":1565033637,"answer_id":1163633,"question_id":885432,"link":"https://askubuntu.com/questions/885432/help-setting-up-sshfs/1163633#1163633","title":"Help
setting up sshfs","body":"<p>As you are getting the following error message:</p>\n\n<blockquote>\n <p>gpasswd:
group''fuse'' does not exist in /etc/group</p>\n</blockquote>\n\n<p>This indicates
that there is no such group, yet!<br>\nPlease test if there is and subsequently
@ -88,7 +95,6 @@ http_interactions:
there is it would look like: </p>\n\n<pre><code>fuse:x:1001: \n</code></pre></li>\n<li><p>If
there isn''t, add one like this: </p>\n\n<pre><code>sudo groupadd fuse \n</code></pre></li>\n<li><p>At
this point you can add yourself to the group with: </p>\n\n<pre><code>sudo
usermod -a -G fuse your_username \n</code></pre></li>\n</ol>\n"}],"has_more":false,"quota_max":300,"quota_remaining":289}'
http_version:
recorded_at: Mon, 13 Jan 2020 12:22:39 GMT
recorded_with: VCR 5.0.0
usermod -a -G fuse your_username \n</code></pre></li>\n</ol>\n"}],"has_more":false,"quota_max":300,"quota_remaining":192}'
recorded_at: Fri, 04 Feb 2022 14:00:21 GMT
recorded_with: VCR 6.0.0

View file

@ -2,7 +2,7 @@
http_interactions:
- request:
method: get
uri: https://api.stackexchange.com/2.2/posts/57496168?filter=!3tz1WbZW5JxrG-f99&key=&site=stackoverflow
uri: https://api.stackexchange.com/2.3/posts/57496168?filter=!3tz1WbZW5JxrG-f99&key=&site=stackoverflow
body:
encoding: US-ASCII
string: ''
@ -18,12 +18,14 @@ http_interactions:
code: 200
message: OK
headers:
Cache-Control:
- private
Content-Length:
- '141'
- '143'
Content-Type:
- application/json; charset=utf-8
Strict-Transport-Security:
- max-age=2592000
- max-age=15552000
Access-Control-Allow-Origin:
- "*"
Access-Control-Allow-Methods:
@ -33,17 +35,18 @@ http_interactions:
X-Content-Type-Options:
- nosniff
X-Request-Guid:
- d25e24d9-a659-49ff-ab38-aaaef36d5bbf
- 11201cf1-0f43-4e4c-b839-6f9b4e8aa3c3
Content-Security-Policy:
- upgrade-insecure-requests; frame-ancestors 'self' https://stackexchange.com
Date:
- Mon, 13 Jan 2020 12:20:49 GMT
- Fri, 04 Feb 2022 14:00:21 GMT
body:
encoding: ASCII-8BIT
string: '{"items":[{"last_activity_date":1565806685,"post_type":"question","post_id":57496168}],"has_more":false,"quota_max":300,"quota_remaining":291}'
http_version:
recorded_at: Mon, 13 Jan 2020 12:20:50 GMT
string: '{"items":[{"last_activity_date":1565806685,"post_type":"question","post_id":57496168}],"has_more":false,"quota_max":300,"quota_remaining":194}'
recorded_at: Fri, 04 Feb 2022 14:00:21 GMT
- request:
method: get
uri: https://api.stackexchange.com/2.2/questions/57496168?filter=!*1SgQGDOL9bPBHULz9sKS.y6qv7V9fYNszvdhDuv5&key=&site=stackoverflow
uri: https://api.stackexchange.com/2.3/questions/57496168?filter=!*1SgQGDOL9bPBHULz9sKS.y6qv7V9fYNszvdhDuv5&key=&site=stackoverflow
body:
encoding: US-ASCII
string: ''
@ -59,12 +62,14 @@ http_interactions:
code: 200
message: OK
headers:
Cache-Control:
- private
Content-Length:
- '532'
- '530'
Content-Type:
- application/json; charset=utf-8
Strict-Transport-Security:
- max-age=2592000
- max-age=15552000
Access-Control-Allow-Origin:
- "*"
Access-Control-Allow-Methods:
@ -74,18 +79,19 @@ http_interactions:
X-Content-Type-Options:
- nosniff
X-Request-Guid:
- 644cb806-8678-4b08-bfbf-6f0cdb72a10d
- a9c1799b-3e84-4869-8316-7013814a36b7
Content-Security-Policy:
- upgrade-insecure-requests; frame-ancestors 'self' https://stackexchange.com
Date:
- Mon, 13 Jan 2020 12:20:50 GMT
- Fri, 04 Feb 2022 14:00:21 GMT
body:
encoding: ASCII-8BIT
string: '{"items":[{"tags":["laravel","foreach","view","relationship"],"comment_count":1,"view_count":61,"answer_count":4,"score":0,"last_activity_date":1565806685,"creation_date":1565790943,"question_id":57496168,"link":"https://stackoverflow.com/questions/57496168/use-where-and-limit-to-child-in-foreach","title":"Use
string: '{"items":[{"tags":["laravel","foreach","view","relationship"],"comment_count":1,"view_count":79,"answer_count":4,"score":0,"last_activity_date":1565806685,"creation_date":1565790943,"question_id":57496168,"link":"https://stackoverflow.com/questions/57496168/use-where-and-limit-to-child-in-foreach","title":"Use
&quot;where&quot; and &quot;limit&quot; to child in @foreach","body":"<p>I
want to display all user''s profile into views and they posts. It''s pretty
simple:</p>\n\n<pre><code>@foreach($profiles as $profile)\n{{ $profile-&gt;name
}}\n @foreach($profile-&gt;posts as $post)\n {{$post-&gt;title}}\n @endforeach\n@endforeach\n</code></pre>\n\n<p>But
I want to display only the latests posts (->orderBy(''created_at'', ''desc'')->limit(4)
) and only accepted posts (->where(''accept'', 1)). How can I do that?</p>\n"}],"has_more":false,"quota_max":300,"quota_remaining":290}'
http_version:
recorded_at: Mon, 13 Jan 2020 12:20:51 GMT
recorded_with: VCR 5.0.0
) and only accepted posts (->where(''accept'', 1)). How can I do that?</p>\n"}],"has_more":false,"quota_max":300,"quota_remaining":193}'
recorded_at: Fri, 04 Feb 2022 14:00:21 GMT
recorded_with: VCR 6.0.0