[Misc] External link favicons (now without a spritesheet) (#610)

* [Misc] Add favicons to some external links

* RuboCop changes from master

* Use packs for favicon images

This works better with caching.
If the file changes, the url changes

* Add favicons to an artists domain list

* Fix aliases

The lookup is done by strings, the keys were symbols

* Add old deviantart cdn alias

* Guard against links with that parse down to no host

http:twitter.com for example

* Tweak flow

* Add basic tests, fix some of what I probably broke

---------

Co-authored-by: Earlopain <14981592+Earlopain@users.noreply.github.com>
This commit is contained in:
Cinder 2024-02-25 11:27:54 -08:00 committed by GitHub
parent fc7d84affd
commit e1e4b94a50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 232 additions and 3 deletions

173
app/helpers/link_helper.rb Normal file
View File

@ -0,0 +1,173 @@
# frozen_string_literal: true
module LinkHelper
NONE = "empty"
DECORATABLE_DOMAINS = [
"e621.net",
#
# Aggregators
"linktr.ee",
"carrd.co",
#
# Art sites
"artfight.net",
"artstation.com",
"archiveofourown.com",
"aryion.com",
"derpibooru.org",
"deviantart.com",
"furaffinity.net",
"furrynetwork.com",
"furrystation.com",
"hentai-foundry.com",
"hiccears.com",
"imgur.com",
"inkbunny.net",
"itaku.ee",
"pillowfort.social",
"pixiv.net",
"skeb.jp",
"sofurry.com",
"toyhou.se",
"tumblr.com",
"newgrounds.com",
"weasyl.com",
"webtoons.com",
#
# Social media
"aethy.com",
"baraag.net",
"bsky.app",
"cohost.org",
"facebook.com",
"instagram.com",
"pawoo.net",
"plurk.com",
"privatter.net",
"reddit.com",
"tiktok.com",
"twitter.com",
"vk.com",
"weibo.com",
"youtube.com",
#
# Livestreams
"picarto.tv",
"piczel.tv",
"twitch.tv",
#
# Paysites
"artconomy.com",
"boosty.to",
"buymeacoffee.com",
"commishes.com",
"gumroad.com",
"etsy.com",
"fanbox.cc",
"itch.io",
"ko-fi.com",
"patreon.com",
"redbubble.com",
"subscribestar.adult",
#
# Bulk storage
"amazonaws.com",
"catbox.moe",
"drive.google.com",
"dropbox.com",
"mega.nz",
"onedrive.live.com",
#
# Imageboards
"4chan.org",
"danbooru.donmai.us",
"desuarchive.org",
"e-hentai.org",
"gelbooru.com",
"rule34.paheal.net",
"rule34.xxx",
"u18chan.com",
#
# Other
"curiouscat.me",
"discord.com",
"steamcommunity.com",
"t.me",
"trello.com",
"web.archive.org",
].freeze
DECORATABLE_ALIASES = {
# alt names
"e926.net" => "e621.net",
"discord.gg" => "discord.com",
"pixiv.me" => "pixiv.net",
"x.com" => "twitter.com",
# same icon
"cloudfront.net" => "amazonaws.com",
"mastodon.art" => "baraag.net",
"meow.social" => "baraag.net",
"sta.sh" => "deviantart.com",
# image servers
"4cdn.org" => "4chan.org",
"discordapp.com" => "discord.com",
"derpicdn.net" => "derpibooru.org",
"deviantart.net" => "deviantart.com",
"dropboxusercontent.com" => "dropbox.com",
"facdn.net" => "furaffinity.net",
"fbcdn.net" => "facebook.com",
"ib.metapix.net" => "inkbunny.net",
"ngfiles.com" => "newgrounds.com",
"pximg.net" => "pixiv.net",
"redd.it" => "reddit.com",
"twimg.com" => "twitter.com",
"ungrounded.net" => "newgrounds.com",
"wixmp.com" => "deviantart.com",
}.freeze
def decorated_link_to(text, path, **)
link_to(path, class: "decorated", **) do
favicon_for_link(path) + text
end
end
def favicon_for_link(path)
hostname = hostname_for_link(path)
image_url = asset_pack_path("static/#{hostname}.png")
tag.span(
class: "link-decoration",
style: "background-image: url(#{image_url})",
data: {
hostname: hostname,
},
)
end
def hostname_for_link(path)
begin
uri = URI.parse(path)
rescue URI::InvalidURIError
return NONE
end
return NONE unless uri.host
hostname = uri.host.delete_prefix("www.")
# 1: direct match
return hostname if DECORATABLE_DOMAINS.include?(hostname)
# 2: aliases
return DECORATABLE_ALIASES[hostname] if DECORATABLE_ALIASES[hostname]
# 3: Try the same, this time with the leftmost subdomain removed
if hostname.count(".") > 1
_removed, remaining_hostname = hostname.split(".", 2)
return remaining_hostname if DECORATABLE_DOMAINS.include?(remaining_hostname)
return DECORATABLE_ALIASES[remaining_hostname] if DECORATABLE_ALIASES[remaining_hostname]
end
NONE
end
end

View File

@ -22,7 +22,7 @@ module PostsHelper
def post_source_tag(source)
# Only allow http:// and https:// links. Disallow javascript: links.
if source =~ %r{\Ahttps?://}i
source_link = link_to(source.sub(%r{\Ahttps?://(?:www\.)?}i, ""), source, target: "_blank", rel: "nofollow noreferrer noopener")
source_link = decorated_link_to(source.sub(%r{\Ahttps?://(?:www\.)?}i, ""), source, target: "_blank", rel: "nofollow noreferrer noopener")
if CurrentUser.is_janitor?
source_link += " ".html_safe + link_to("»", posts_path(tags: "source:#{source.sub(%r{[^/]*$}, '')}"), rel: "nofollow")

View File

@ -32,6 +32,8 @@ require('../src/styles/base.scss');
importAll(require.context('../src/javascripts', true, /\.js(\.erb)?$/));
require.context("../../../public/images/favicons", true)
export { default as Artist } from '../src/javascripts/artist.js';
export { default as Autocomplete } from '../src/javascripts/autocomplete.js.erb';
export { default as Blacklist } from '../src/javascripts/blacklists.js';

View File

@ -19,6 +19,7 @@
@import "common/forms.scss";
@import "common/inline.scss";
@import "common/jquery_ui_custom.scss";
@import "common/link_decorator.scss";
@import "common/main_layout.scss";
@import "common/news.scss";
@import "common/notices.scss";

View File

@ -0,0 +1,9 @@
span.link-decoration {
vertical-align: sub;
display: inline-block;
width: 16px;
height: 16px;
margin-right: 4px;
line-height: 0;
background-size: 16px 16px;
}

View File

@ -19,7 +19,7 @@
<li><strong>Domains</strong></li>
<ul>
<% artist.domains.each do |url, count| %>
<li><%= url %>: <%= count %></li>
<li><%= favicon_for_link("https://#{url}") %> <%= url %>: <%= count %></li>
<% end %>
</ul>
<% end %>
@ -29,7 +29,7 @@
<% artist.sorted_urls.each do |url| %>
<li>
<% if url.is_active? %>
<%= link_to h(url.to_s), h(url) %>
<%= decorated_link_to(h(url.to_s), h(url)) %>
<% else %>
<del><%= h(url.url) %></del>
<% end %>

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
require "test_helper"
class LinkHelperTest < ActionView::TestCase
test "for a non-handled url" do
assert_equal(LinkHelper::NONE, hostname_for_link("https://example.com"))
end
test "for a invalid url" do
assert_equal(LinkHelper::NONE, hostname_for_link("https:example.com"))
end
test "for a non-url" do
assert_equal(LinkHelper::NONE, hostname_for_link("text"))
end
test "for a normal domain" do
assert_equal("e621.net", hostname_for_link("https://e621.net"))
assert_equal("e621.net", hostname_for_link("https://www.e621.net"))
assert_equal("imgur.com", hostname_for_link("https://imgur.com"))
assert_equal("imgur.com", hostname_for_link("https://www.imgur.com"))
end
test "for a domain with aliases" do
assert_equal("discord.com", hostname_for_link("https://discordapp.com"))
assert_equal("furaffinity.net", hostname_for_link("https://d.facdn.net"))
assert_equal("inkbunny.net", hostname_for_link("https://ib.metapix.net"))
assert_equal("inkbunny.net", hostname_for_link("https://qb.ib.metapix.net"))
end
test "all listed images exist" do
favicon_folder = Rails.public_path.join("images/favicons")
all_domains = LinkHelper::DECORATABLE_DOMAINS + LinkHelper::DECORATABLE_ALIASES.values + [LinkHelper::NONE]
all_domains.each do |domain|
assert(favicon_folder.join("#{domain}.png").exist?, "missing #{domain}")
end
# No extraneous files
all_files = favicon_folder.children.map { |file| file.basename.to_s.delete_suffix(".png") }
assert_empty(all_files - all_domains, "unused files in favicon folder")
end
end