From f6d09fafee83514889bbcf6531e0bc01e33b0b16 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Sun, 1 Mar 2020 09:48:32 +0100
Subject: [PATCH 1/6] Add support for remote favicons

---
 lib/pleroma/user.ex                           | 30 +++++++++++++++++++
 .../web/mastodon_api/views/account_view.ex    |  3 +-
 test/support/http_request_mock.ex             |  4 +++
 .../mastodon_api/views/account_view_test.exs  |  2 ++
 4 files changed, 38 insertions(+), 1 deletion(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index e98332744..25ea112a2 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -2253,4 +2253,34 @@ defmodule Pleroma.User do
     |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
     |> Map.put(:fields, fields)
   end
+
+  def get_cached_favicon(%User{} = user) do
+    key = "favicon:#{user.ap_id}"
+    Cachex.fetch!(:user_cache, key, fn _ -> get_favicon(user) end)
+  end
+
+  def get_cached_favicon(_user) do
+    nil
+  end
+
+  def get_favicon(user) do
+    try do
+      with url <- user.ap_id,
+           true <- is_binary(url),
+           {:ok, %Tesla.Env{body: html}} <- Pleroma.HTTP.get(url),
+           favicon_rel <-
+             html
+             |> Floki.parse_document!()
+             |> Floki.attribute("link[rel=icon]", "href")
+             |> List.first(),
+           favicon_url <- URI.merge(URI.parse(url), favicon_rel) |> to_string(),
+           true <- is_binary(favicon_url) do
+        favicon_url
+      else
+        _ -> nil
+      end
+    rescue
+      _ -> nil
+    end
+  end
 end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index a6e64b4ab..efe835e3c 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -245,7 +245,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
         hide_favorites: user.hide_favorites,
         relationship: relationship,
         skip_thread_containment: user.skip_thread_containment,
-        background_image: image_url(user.background) |> MediaProxy.url()
+        background_image: image_url(user.background) |> MediaProxy.url(),
+        favicon: User.get_cached_favicon(user) |> MediaProxy.url()
       }
     }
     |> maybe_put_role(user, opts[:for])
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index da04ac6f1..4d33c6250 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -1342,6 +1342,10 @@ defmodule HttpRequestMock do
     {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/relay/relay.json")}}
   end
 
+  def get("http://localhost:4001/users/" <> _, _, _, _) do
+    {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/7369654.html")}}
+  end
+
   def get(url, query, body, headers) do
     {:error,
      "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{
diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs
index 80b1f734c..e01a7c1ee 100644
--- a/test/web/mastodon_api/views/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -75,6 +75,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
       pleroma: %{
         ap_id: user.ap_id,
         background_image: "https://example.com/images/asuka_hospital.png",
+        favicon: nil,
         confirmation_pending: false,
         tags: [],
         is_admin: false,
@@ -152,6 +153,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
       pleroma: %{
         ap_id: user.ap_id,
         background_image: nil,
+        favicon: nil,
         confirmation_pending: false,
         tags: [],
         is_admin: false,

From 6a679d80c9030afa8327377928f8ac2fcf1a4a0e Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Mon, 2 Mar 2020 05:38:25 +0100
Subject: [PATCH 2/6] Move get_favicon to Pleroma.Instances, use /

---
 lib/pleroma/application.ex                    |   1 +
 lib/pleroma/instances.ex                      |  28 ++
 lib/pleroma/user.ex                           |  30 --
 .../web/mastodon_api/views/account_view.ex    |  11 +-
 .../https___osada.macgirvin.com.html          | 301 ++++++++++++++++++
 test/support/http_request_mock.ex             |  10 +-
 .../mastodon_api/views/account_view_test.exs  |   6 +-
 7 files changed, 353 insertions(+), 34 deletions(-)
 create mode 100644 test/fixtures/tesla_mock/https___osada.macgirvin.com.html

diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 9615af122..c7fc95f75 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -150,6 +150,7 @@ defmodule Pleroma.Application do
       build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
       build_cachex("failed_proxy_url", limit: 2500),
       build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000)
+      build_cachex("instances", default_ttl: 25_000, ttl_interval: 1000, limit: 2500)
     ]
   end
 
diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex
index 557e8decf..c9b1ed4ce 100644
--- a/lib/pleroma/instances.ex
+++ b/lib/pleroma/instances.ex
@@ -37,4 +37,32 @@ defmodule Pleroma.Instances do
       url_or_host
     end
   end
+
+  def get_cached_favicon(instance_url) when is_binary(instance_url) do
+    Cachex.fetch!(:instances_cache, instance_url, fn _ -> get_favicon(instance_url) end)
+  end
+
+  def get_cached_favicon(_instance_url) do
+    nil
+  end
+
+  def get_favicon(instance_url) when is_binary(instance_url) do
+    try do
+      with {:ok, %Tesla.Env{body: html}} <-
+             Pleroma.HTTP.get(instance_url, [{:Accept, "text/html"}]),
+           favicon_rel <-
+             html
+             |> Floki.parse_document!()
+             |> Floki.attribute("link[rel=icon]", "href")
+             |> List.first(),
+           favicon_url <- URI.merge(URI.parse(instance_url), favicon_rel) |> to_string(),
+           true <- is_binary(favicon_url) do
+        favicon_url
+      else
+        _ -> nil
+      end
+    rescue
+      _ -> nil
+    end
+  end
 end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 25ea112a2..e98332744 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -2253,34 +2253,4 @@ defmodule Pleroma.User do
     |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
     |> Map.put(:fields, fields)
   end
-
-  def get_cached_favicon(%User{} = user) do
-    key = "favicon:#{user.ap_id}"
-    Cachex.fetch!(:user_cache, key, fn _ -> get_favicon(user) end)
-  end
-
-  def get_cached_favicon(_user) do
-    nil
-  end
-
-  def get_favicon(user) do
-    try do
-      with url <- user.ap_id,
-           true <- is_binary(url),
-           {:ok, %Tesla.Env{body: html}} <- Pleroma.HTTP.get(url),
-           favicon_rel <-
-             html
-             |> Floki.parse_document!()
-             |> Floki.attribute("link[rel=icon]", "href")
-             |> List.first(),
-           favicon_url <- URI.merge(URI.parse(url), favicon_rel) |> to_string(),
-           true <- is_binary(favicon_url) do
-        favicon_url
-      else
-        _ -> nil
-      end
-    rescue
-      _ -> nil
-    end
-  end
 end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index efe835e3c..3ee50dfd0 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -204,6 +204,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
         %{}
       end
 
+    favicon =
+      user
+      |> Map.get(:ap_id, "")
+      |> URI.parse()
+      |> URI.merge("/")
+      |> to_string()
+      |> Pleroma.Instances.get_cached_favicon()
+      |> MediaProxy.url()
+
     %{
       id: to_string(user.id),
       username: username_from_nickname(user.nickname),
@@ -246,7 +255,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
         relationship: relationship,
         skip_thread_containment: user.skip_thread_containment,
         background_image: image_url(user.background) |> MediaProxy.url(),
-        favicon: User.get_cached_favicon(user) |> MediaProxy.url()
+        favicon: favicon
       }
     }
     |> maybe_put_role(user, opts[:for])
diff --git a/test/fixtures/tesla_mock/https___osada.macgirvin.com.html b/test/fixtures/tesla_mock/https___osada.macgirvin.com.html
new file mode 100644
index 000000000..880273d74
--- /dev/null
+++ b/test/fixtures/tesla_mock/https___osada.macgirvin.com.html
@@ -0,0 +1,301 @@
+<!DOCTYPE html >
+<html prefix="og: http://ogp.me/ns#">
+<head>
+  <title>Osada</title>
+  <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
+<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, user-scalable=0"/>
+<meta property="generator" content="osada"/>
+<link rel="stylesheet" href="http://osada.macgirvin.com/library/fork-awesome/css/fork-awesome.min.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/vendor/twbs/bootstrap/dist/css/bootstrap.min.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/library/bootstrap-tagsinput/bootstrap-tagsinput.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/view/css/bootstrap-red.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/library/datetimepicker/jquery.datetimepicker.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/library/bootstrap-colorpicker/dist/css/bootstrap-colorpicker.min.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/library/tiptip/tipTip.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/library/jgrowl/jquery.jgrowl.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/library/jRange/jquery.range.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/view/css/conversation.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/view/css/widgets.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/view/css/colorbox.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/library/justifiedGallery/justifiedGallery.min.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/view/css/default.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/view/css/mod_home.css?v=2.3" type="text/css" media="screen">
+<link rel="stylesheet" href="http://osada.macgirvin.com/view/theme/redbasic/php/style.pcss?v=2.3" type="text/css" media="screen">
+
+<script>
+
+	var aStr = {
+
+		'delitem'     : "Delete this item?",
+		'comment'     : "Comment",
+		'showmore'    : "<i class='fa fa-chevron-down'></i> show all",
+		'showfewer'   : "<i class='fa fa-chevron-up'></i> show less",
+		'divgrowmore' : "<i class='fa fa-chevron-down'></i> expand",
+		'divgrowless' : "<i class='fa fa-chevron-up'></i> collapse",
+		'pwshort'     : "Password too short",
+		'pwnomatch'   : "Passwords do not match",
+		'everybody'   : "everybody",
+		'passphrase'  : "Secret Passphrase",
+		'passhint'    : "Passphrase hint",
+		'permschange' : "Notice: Permissions have changed but have not yet been submitted.",
+		'closeAll'    : "close all",
+		'nothingnew'  : "Nothing new here",
+		'rating_desc' : "Rate This Channel (this is public)",
+		'rating_val'  : "Rating",
+		'rating_text' : "Describe (optional)",
+		'submit'      : "Submit",
+		'linkurl'     : "Please enter a link URL",
+		'leavethispage' : "Unsaved changes. Are you sure you wish to leave this page?",
+		'location'    : "Location",
+		'lovely'      : "lovely",
+		'wonderful'   : "wonderful",
+		'fantastic'   : "fantastic",
+		'great'       : "great",
+		'nick_invld1' : "Your chosen nickname was either already taken or not valid. Please use our suggestion (",
+		'nick_invld2' : ") or enter a new one.",
+		'nick_valid'  : "Thank you, this nickname is valid.",
+		'name_empty'  : "A channel name is required.",
+		'name_ok1'    : "This is a ",
+		'name_ok2'    : " channel name",
+
+
+
+		't01' : "",
+		't02' : "",
+		't03' : "ago",
+		't04' : "from now",
+		't05' : "less than a minute",
+		't06' : "about a minute",
+		't07' : "%d minutes",
+		't08' : "about an hour",
+		't09' : "about %d hours",
+		't10' : "a day",
+		't11' : "%d days",
+		't12' : "about a month",
+		't13' : "%d months",
+		't14' : "about a year",
+		't15' : "%d years",
+		't16' : " ",
+		't17' : "[]",
+
+		'monthNames' : [ "January","February","March","April","May","June","July","August","September","October","November","December" ],
+		'monthNamesShort' : [ "Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec" ],
+		'dayNames' : ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],
+		'dayNamesShort' : ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],
+		'today' : "today",
+		'month' : "month",
+		'week' : "week",
+		'day' : "day",
+		'allday' : "All day"
+	};
+
+</script>
+		
+
+<script src="http://osada.macgirvin.com/view/js/jquery.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/justifiedGallery/jquery.justifiedGallery.min.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/sprintf.js/dist/sprintf.min.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/textcomplete/textcomplete.min.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/view/js/autocomplete.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/jquery.timeago.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/readmore.js/readmore.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/sticky-kit/sticky-kit.min.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/jgrowl/jquery.jgrowl_minimized.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/view/js/acl.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/view/js/webtoolkit.base64.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/jRange/jquery.range.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/colorbox/jquery.colorbox-min.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/jquery.AreYouSure/jquery.are-you-sure.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/tableofcontents/jquery.toc.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/imagesloaded/imagesloaded.pkgd.min.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/vendor/twbs/bootstrap/dist/js/bootstrap.bundle.min.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/bootbox/bootbox.min.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/bootstrap-tagsinput/bootstrap-tagsinput.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/datetimepicker/jquery.datetimepicker.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/library/bootstrap-colorpicker/dist/js/bootstrap-colorpicker.js?v=2.3"></script>
+<script src="http://osada.macgirvin.com/view/theme/redbasic/js/redbasic.js?v=2.3"></script>
+
+
+
+<script>
+	var updateInterval = 80000;
+	var localUser = false;
+	var zid = null;
+	var justifiedGalleryActive = false;
+			var preloadImages = 0;
+</script>
+
+
+
+<script>$(document).ready(function() { $("#nav-search-text").search_autocomplete('https://osada.macgirvin.com/acl');});</script><script src="http://osada.macgirvin.com/view/js/main.js?v=2.3"></script>
+</head>
+<body>
+		<header></header>
+	<nav class="navbar fixed-top navbar-expand-lg navbar-dark bg-dark"><div class="project-banner" title="Powered by Osada"><a href="https://osada.macgirvin.com/">&#x2638;</a></div>
+<div class="d-lg-none pt-1 pb-1">
+		<a class="btn btn-primary btn-sm text-white" href="#" title="Sign in" id="login_nav_btn_collapse" data-toggle="modal" data-target="#nav-login">
+		Login
+	</a>
+		</div>
+<div class="navbar-toggler-right">
+		<button id="expand-aside" type="button" class="d-lg-none navbar-toggler border-0" data-toggle="offcanvas" data-target="#region_1">
+		<i class="fa fa-arrow-circle-right" id="expand-aside-icon"></i>
+	</button>
+		<button id="menu-btn" class="navbar-toggler border-0" type="button" data-toggle="collapse" data-target="#navbar-collapse-2">
+		<i class="fa fa-bars"></i>
+	</button>
+</div>
+<div class="collapse navbar-collapse" id="navbar-collapse-1">
+	<ul class="navbar-nav mr-auto">
+				<li class="nav-item d-lg-flex">
+						<a class="nav-link" href="#" title="Sign in" id="login_nav_btn" data-toggle="modal" data-target="#nav-login">
+			Login
+			</a>
+					</li>
+							</ul>
+
+	<div id="banner" class="navbar-text"></div>
+
+	<ul id="nav-right" class="navbar-nav ml-auto">
+		<li class="nav-item collapse clearfix" id="nav-search">
+			<form class="form-inline" method="get" action="search" role="search">
+				<input class="form-control form-control-sm mt-1 mr-2" id="nav-search-text" type="text" value="" placeholder="@name, !forum, #tag, content" name="search" title="Search site @name, !forum, #tag, ?docs, content" onclick="this.submit();" onblur="closeMenu('nav-search'); openMenu('nav-search-btn');"/>
+			</form>
+			<div id="nav-search-spinner" class="spinner-wrapper">
+				<div class="spinner s"></div>
+			</div>
+		</li>
+		<li class="nav-item" id="nav-search-btn">
+			<a class="nav-link" href="#nav-search" title="Search site @name, !forum, #tag, ?docs, content" onclick="openMenu('nav-search'); closeMenu('nav-search-btn'); $('#nav-search-text').focus(); return false;"><i class="fa fa-fw fa-search"></i></a>
+		</li>
+										<li class="nav-item dropdown" id="app-menu">
+			<a class="nav-link" href="#" data-toggle="dropdown"><i class="fa fa-fw fa-bars"></i></a>
+			<div id="dropdown-menu" class="dropdown-menu dropdown-menu-right">
+												<a class="dropdown-item" href="https://osada.macgirvin.com/directory"><i class="generic-icons-nav fa fa-fw fa-sitemap"></i>Directory</a>
+
+
+								<a class="dropdown-item" href="https://osada.macgirvin.com/lang"><i class="generic-icons-nav fa fa-fw fa-language"></i>Language</a>
+
+
+								<a class="dropdown-item" href="https://osada.macgirvin.com/search"><i class="generic-icons-nav fa fa-fw fa-search"></i>Search</a>
+
+
+															</div>
+		</li>
+	</ul>
+</div>
+<div class="collapse d-lg-none" id="navbar-collapse-2">
+	<div class="navbar-nav mr-auto">
+								<a class="nav-link" href="https://osada.macgirvin.com/directory"><i class="generic-icons-nav fa fa-fw fa-sitemap"></i>Directory</a>
+
+
+				<a class="nav-link" href="https://osada.macgirvin.com/lang"><i class="generic-icons-nav fa fa-fw fa-language"></i>Language</a>
+
+
+				<a class="nav-link" href="https://osada.macgirvin.com/search"><i class="generic-icons-nav fa fa-fw fa-search"></i>Search</a>
+
+
+							</div>
+</div>
+</nav>
+	<main>
+		<aside id="region_1"><div class="aside_spacer"><div id="left_aside_wrapper"></div></div></aside>
+		<section id="region_2"><h1 class="home-welcome">Welcome to Osada</h1><form action="https://osada.macgirvin.com/" id="main-login" method="post">
+	<input type="hidden" name="auth-params" value="login"/>
+	<div id="login-main">
+		<div id="login-input" class="form-group">
+				<div id="id_username_wrapper" class="form-group">
+		<label for="id_username" id="label_username">Login/Email</label>
+		<input class="form-control" name="username" id="id_username" type="text" value="">
+		<small id="help_username" class="form-text text-muted"></small>
+	</div>
+				<div class="form-group">
+		<label for="id_password">Password</label>
+		<input class="form-control" type="password" name="password" id="id_password" value="">		<small id="help_password" class="form-text text-muted"></small>
+	</div>
+				<div id="remember_container" class="clearfix form-group checkbox">
+		<label for="id_remember">Remember me</label>
+		<div class="float-right"><input type="checkbox" name="remember" id="id_remember" value="1"/><label class="switchlabel" for="id_remember"> <span class="onoffswitch-inner" data-on="Yes" data-off="No"></span><span class="onoffswitch-switch"></span></label></div>
+		<small class="form-text text-muted"></small>
+	</div>
+			<button type="submit" name="submit" class="btn btn-block btn-primary">Login</button>
+		</div>
+		<div id="login-extra-links">
+			<a href="https://osada.macgirvin.com/pubsites" title="Create an account to access services and applications" id="register-link" class="pull-right">Register</a>			<a href="lostpass" title="Forgot your password?" id="lost-password-link">Password Reset</a>
+		</div>
+		<hr>
+		<a href="rmagic" class="btn btn-block btn-outline-success rmagic-button">Remote Authentication</a>
+	</div>
+			<input type="hidden" name="0" value=""/>
+	</form>
+<script type="text/javascript"> $(document).ready(function() { $("#id_username").focus();} );</script>
+<div id="nav-login" class="modal" tabindex="-1" role="dialog">
+	<div class="modal-dialog" role="document">
+		<div class="modal-content">
+			<div class="modal-header">
+				<h4 class="modal-title">Login</h4>
+				<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+			</div>
+			<div class="modal-body">
+				<div class="form-group">
+					<form action="https://osada.macgirvin.com/" id="main-login" method="post">
+	<input type="hidden" name="auth-params" value="login"/>
+	<div id="login-main">
+		<div id="login-input" class="form-group">
+				<div id="id_username_wrapper" class="form-group">
+		<label for="id_username" id="label_username">Login/Email</label>
+		<input class="form-control" name="username" id="id_username" type="text" value="">
+		<small id="help_username" class="form-text text-muted"></small>
+	</div>
+				<div class="form-group">
+		<label for="id_password">Password</label>
+		<input class="form-control" type="password" name="password" id="id_password" value="">		<small id="help_password" class="form-text text-muted"></small>
+	</div>
+				<div id="remember_me_container" class="clearfix form-group checkbox">
+		<label for="id_remember_me">Remember me</label>
+		<div class="float-right"><input type="checkbox" name="remember_me" id="id_remember_me" value="1"/><label class="switchlabel" for="id_remember_me"> <span class="onoffswitch-inner" data-on="Yes" data-off="No"></span><span class="onoffswitch-switch"></span></label></div>
+		<small class="form-text text-muted"></small>
+	</div>
+			<button type="submit" name="submit" class="btn btn-block btn-primary">Login</button>
+		</div>
+		<div id="login-extra-links">
+			<a href="https://osada.macgirvin.com/pubsites" title="Create an account to access services and applications" id="register-link" class="pull-right">Register</a>			<a href="lostpass" title="Forgot your password?" id="lost-password-link">Password Reset</a>
+		</div>
+		<hr>
+		<a href="rmagic" class="btn btn-block btn-outline-success rmagic-button">Remote Authentication</a>
+	</div>
+			<input type="hidden" name="0" value=""/>
+	</form>
+
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+					<div id="page-footer"></div>
+			<div id="pause"></div>
+		</section>
+		<aside id="region_3" class="d-none d-xl-table-cell"><div class="aside_spacer"><div id="right_aside_wrapper"></div></div></aside>
+	</main>
+	<footer></footer>
+</body>
+</html>
+<!--
+     FILE ARCHIVED ON 11:49:38 Jan 26, 2019 AND RETRIEVED FROM THE
+     INTERNET ARCHIVE ON 04:27:56 Mar 02, 2020.
+
+     CONTENT MAY BE PROTECTED BY COPYRIGHT (17 U.S.C. SECTION 108(a)(3)).
+-->
+<!--
+playback timings (ms):
+  exclusion.robots: 0.217
+  CDXLines.iter: 14.7 (3)
+  LoadShardBlock: 165.298 (3)
+  esindex: 0.01
+  PetaboxLoader3.datanode: 72.599 (4)
+  exclusion.robots.policy: 0.208
+  RedisCDXSource: 16.804
+  PetaboxLoader3.resolve: 146.316 (4)
+  captures_list: 199.59
+  load_resource: 56.473
+-->
\ No newline at end of file
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index 4d33c6250..19a202654 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -1342,10 +1342,18 @@ defmodule HttpRequestMock do
     {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/relay/relay.json")}}
   end
 
-  def get("http://localhost:4001/users/" <> _, _, _, _) do
+  def get("http://localhost:4001/", _, "", Accept: "text/html") do
     {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/7369654.html")}}
   end
 
+  def get("https://osada.macgirvin.com/", _, "", Accept: "text/html") do
+    {:ok,
+     %Tesla.Env{
+       status: 200,
+       body: File.read!("test/fixtures/tesla_mock/https___osada.macgirvin.com.html")
+     }}
+  end
+
   def get(url, query, body, headers) do
     {:error,
      "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{
diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs
index e01a7c1ee..c4341cb28 100644
--- a/test/web/mastodon_api/views/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -75,7 +75,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
       pleroma: %{
         ap_id: user.ap_id,
         background_image: "https://example.com/images/asuka_hospital.png",
-        favicon: nil,
+        favicon:
+          "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png",
         confirmation_pending: false,
         tags: [],
         is_admin: false,
@@ -153,7 +154,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
       pleroma: %{
         ap_id: user.ap_id,
         background_image: nil,
-        favicon: nil,
+        favicon:
+          "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png",
         confirmation_pending: false,
         tags: [],
         is_admin: false,

From 013e2c505786dff311bcc8bf23631d6a1a1636ef Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Tue, 7 Jul 2020 11:13:38 +0200
Subject: [PATCH 3/6] Use instances table instead of Cachex

---
 lib/pleroma/application.ex                    |  1 -
 lib/pleroma/instances.ex                      | 28 ----------
 lib/pleroma/instances/instance.ex             | 55 ++++++++++++++++++-
 .../web/mastodon_api/views/account_view.ex    |  3 +-
 .../20200707112859_instances_add_favicon.exs  | 10 ++++
 5 files changed, 65 insertions(+), 32 deletions(-)
 create mode 100644 priv/repo/migrations/20200707112859_instances_add_favicon.exs

diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index c7fc95f75..9615af122 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -150,7 +150,6 @@ defmodule Pleroma.Application do
       build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
       build_cachex("failed_proxy_url", limit: 2500),
       build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000)
-      build_cachex("instances", default_ttl: 25_000, ttl_interval: 1000, limit: 2500)
     ]
   end
 
diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex
index c9b1ed4ce..557e8decf 100644
--- a/lib/pleroma/instances.ex
+++ b/lib/pleroma/instances.ex
@@ -37,32 +37,4 @@ defmodule Pleroma.Instances do
       url_or_host
     end
   end
-
-  def get_cached_favicon(instance_url) when is_binary(instance_url) do
-    Cachex.fetch!(:instances_cache, instance_url, fn _ -> get_favicon(instance_url) end)
-  end
-
-  def get_cached_favicon(_instance_url) do
-    nil
-  end
-
-  def get_favicon(instance_url) when is_binary(instance_url) do
-    try do
-      with {:ok, %Tesla.Env{body: html}} <-
-             Pleroma.HTTP.get(instance_url, [{:Accept, "text/html"}]),
-           favicon_rel <-
-             html
-             |> Floki.parse_document!()
-             |> Floki.attribute("link[rel=icon]", "href")
-             |> List.first(),
-           favicon_url <- URI.merge(URI.parse(instance_url), favicon_rel) |> to_string(),
-           true <- is_binary(favicon_url) do
-        favicon_url
-      else
-        _ -> nil
-      end
-    rescue
-      _ -> nil
-    end
-  end
 end
diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex
index 74458c09a..b97e229e5 100644
--- a/lib/pleroma/instances/instance.ex
+++ b/lib/pleroma/instances/instance.ex
@@ -17,6 +17,8 @@ defmodule Pleroma.Instances.Instance do
   schema "instances" do
     field(:host, :string)
     field(:unreachable_since, :naive_datetime_usec)
+    field(:favicon, :string)
+    field(:favicon_updated_at, :naive_datetime)
 
     timestamps()
   end
@@ -25,7 +27,7 @@ defmodule Pleroma.Instances.Instance do
 
   def changeset(struct, params \\ %{}) do
     struct
-    |> cast(params, [:host, :unreachable_since])
+    |> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at])
     |> validate_required([:host])
     |> unique_constraint(:host)
   end
@@ -120,4 +122,55 @@ defmodule Pleroma.Instances.Instance do
   end
 
   defp parse_datetime(datetime), do: datetime
+
+  def get_or_update_favicon(%URI{host: host} = instance_uri) do
+    existing_record = Repo.get_by(Instance, %{host: host})
+    now = NaiveDateTime.utc_now()
+
+    if existing_record && existing_record.favicon &&
+         NaiveDateTime.diff(now, existing_record.favicon_updated_at) < 86_400 do
+      existing_record.favicon
+    else
+      favicon = scrape_favicon(instance_uri)
+
+      cond do
+        is_binary(favicon) && existing_record ->
+          existing_record
+          |> changeset(%{favicon: favicon, favicon_updated_at: now})
+          |> Repo.update()
+
+          favicon
+
+        is_binary(favicon) ->
+          %Instance{}
+          |> changeset(%{host: host, favicon: favicon, favicon_updated_at: now})
+          |> Repo.insert()
+
+          favicon
+
+        true ->
+          nil
+      end
+    end
+  end
+
+  defp scrape_favicon(%URI{} = instance_uri) do
+    try do
+      with {:ok, %Tesla.Env{body: html}} <-
+             Pleroma.HTTP.get(to_string(instance_uri), [{:Accept, "text/html"}]),
+           favicon_rel <-
+             html
+             |> Floki.parse_document!()
+             |> Floki.attribute("link[rel=icon]", "href")
+             |> List.first(),
+           favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(),
+           true <- is_binary(favicon) do
+        favicon
+      else
+        _ -> nil
+      end
+    rescue
+      _ -> nil
+    end
+  end
 end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 3ee50dfd0..db5739254 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -209,8 +209,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
       |> Map.get(:ap_id, "")
       |> URI.parse()
       |> URI.merge("/")
-      |> to_string()
-      |> Pleroma.Instances.get_cached_favicon()
+      |> Pleroma.Instances.Instance.get_or_update_favicon()
       |> MediaProxy.url()
 
     %{
diff --git a/priv/repo/migrations/20200707112859_instances_add_favicon.exs b/priv/repo/migrations/20200707112859_instances_add_favicon.exs
new file mode 100644
index 000000000..5538749dc
--- /dev/null
+++ b/priv/repo/migrations/20200707112859_instances_add_favicon.exs
@@ -0,0 +1,10 @@
+defmodule Pleroma.Repo.Migrations.InstancesAddFavicon do
+  use Ecto.Migration
+
+  def change do
+    alter table(:instances) do
+      add(:favicon, :string)
+      add(:favicon_updated_at, :naive_datetime)
+    end
+  end
+end

From 8c9df2d2e6a5a3639f6b411cd3e9b57248a0650c Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Tue, 7 Jul 2020 12:07:30 +0200
Subject: [PATCH 4/6] instance: Prevent loop of updates

---
 lib/pleroma/instances/instance.ex | 29 +++++++++++------------------
 1 file changed, 11 insertions(+), 18 deletions(-)

diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex
index b97e229e5..a1f935232 100644
--- a/lib/pleroma/instances/instance.ex
+++ b/lib/pleroma/instances/instance.ex
@@ -127,30 +127,23 @@ defmodule Pleroma.Instances.Instance do
     existing_record = Repo.get_by(Instance, %{host: host})
     now = NaiveDateTime.utc_now()
 
-    if existing_record && existing_record.favicon &&
+    if existing_record && existing_record.favicon_updated_at &&
          NaiveDateTime.diff(now, existing_record.favicon_updated_at) < 86_400 do
       existing_record.favicon
     else
       favicon = scrape_favicon(instance_uri)
 
-      cond do
-        is_binary(favicon) && existing_record ->
-          existing_record
-          |> changeset(%{favicon: favicon, favicon_updated_at: now})
-          |> Repo.update()
-
-          favicon
-
-        is_binary(favicon) ->
-          %Instance{}
-          |> changeset(%{host: host, favicon: favicon, favicon_updated_at: now})
-          |> Repo.insert()
-
-          favicon
-
-        true ->
-          nil
+      if existing_record do
+        existing_record
+        |> changeset(%{favicon: favicon, favicon_updated_at: now})
+        |> Repo.update()
+      else
+        %Instance{}
+        |> changeset(%{host: host, favicon: favicon, favicon_updated_at: now})
+        |> Repo.insert()
       end
+
+      favicon
     end
   end
 

From 312fc55f14e1b7f88ec43b72c577bf5df595beac Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Wed, 8 Jul 2020 05:56:24 +0200
Subject: [PATCH 5/6] Add [:instances_favicons, :enabled] setting, defaults to
 false

---
 config/config.exs                             |  2 ++
 config/description.exs                        | 13 ++++++++++++
 config/test.exs                               |  2 ++
 docs/configuration/cheatsheet.md              |  6 ++++++
 .../web/mastodon_api/views/account_view.ex    | 16 +++++++++------
 .../mastodon_api/views/account_view_test.exs  | 20 +++++++++++++++++++
 6 files changed, 53 insertions(+), 6 deletions(-)

diff --git a/config/config.exs b/config/config.exs
index 458d3a99a..3577cd101 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -706,6 +706,8 @@ config :tzdata, :http_client, Pleroma.HTTP.Tzdata
 
 config :ex_aws, http_client: Pleroma.HTTP.ExAws
 
+config :pleroma, :instances_favicons, enabled: false
+
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
 import_config "#{Mix.env()}.exs"
diff --git a/config/description.exs b/config/description.exs
index 650610fbe..7c432d67d 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -3447,5 +3447,18 @@ config :pleroma, :config_description, [
         suggestions: [false]
       }
     ]
+  },
+  %{
+    group: :pleroma,
+    key: :instances_favicons,
+    type: :group,
+    description: "Control favicons for instances",
+    children: [
+      %{
+        key: :enabled,
+        type: :boolean,
+        description: "Allow/disallow displaying and getting instances favicons"
+      }
+    ]
   }
 ]
diff --git a/config/test.exs b/config/test.exs
index e38b9967d..e6596e0bc 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -111,6 +111,8 @@ config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
 
 config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
 
+config :pleroma, :instances_favicons, enabled: true
+
 if File.exists?("./config/test.secret.exs") do
   import_config "test.secret.exs"
 else
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 6b640cebc..7a3200e01 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -987,3 +987,9 @@ Restrict access for unauthenticated users to timelines (public and federate), us
 ## Pleroma.Web.ApiSpec.CastAndValidate
 
 * `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`.
+
+## :instances_favicons
+
+Control favicons for instances.
+
+* `enabled`: Allow/disallow displaying and getting instances favicons
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index db5739254..2feba4778 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -205,12 +205,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
       end
 
     favicon =
-      user
-      |> Map.get(:ap_id, "")
-      |> URI.parse()
-      |> URI.merge("/")
-      |> Pleroma.Instances.Instance.get_or_update_favicon()
-      |> MediaProxy.url()
+      if Pleroma.Config.get([:instances_favicons, :enabled]) do
+        user
+        |> Map.get(:ap_id, "")
+        |> URI.parse()
+        |> URI.merge("/")
+        |> Pleroma.Instances.Instance.get_or_update_favicon()
+        |> MediaProxy.url()
+      else
+        nil
+      end
 
     %{
       id: to_string(user.id),
diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs
index c4341cb28..ac6d50e3a 100644
--- a/test/web/mastodon_api/views/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -5,6 +5,7 @@
 defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
   use Pleroma.DataCase
 
+  alias Pleroma.Config
   alias Pleroma.User
   alias Pleroma.UserRelationship
   alias Pleroma.Web.CommonAPI
@@ -18,6 +19,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
     :ok
   end
 
+  setup do: clear_config([:instances_favicons, :enabled])
+
   test "Represent a user account" do
     background_image = %{
       "url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}]
@@ -94,6 +97,23 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
     assert expected == AccountView.render("show.json", %{user: user})
   end
 
+  test "Favicon is nil when :instances_favicons is disabled" do
+    user = insert(:user)
+
+    Config.put([:instances_favicons, :enabled], true)
+
+    assert %{
+             pleroma: %{
+               favicon:
+                 "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png"
+             }
+           } = AccountView.render("show.json", %{user: user})
+
+    Config.put([:instances_favicons, :enabled], false)
+
+    assert %{pleroma: %{favicon: nil}} = AccountView.render("show.json", %{user: user})
+  end
+
   test "Represent the user account for the account owner" do
     user = insert(:user)
 

From 31fef95e35d3cbc2d65c76a57ba8f4047809ce1b Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Wed, 8 Jul 2020 06:09:39 +0200
Subject: [PATCH 6/6] Add changelog and documentation

---
 CHANGELOG.md                                  | 2 ++
 docs/API/differences_in_mastoapi_responses.md | 1 +
 lib/pleroma/web/api_spec/schemas/account.ex   | 6 ++++++
 3 files changed, 9 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92d8c3d8e..0f3447069 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -55,6 +55,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Added `:reject_deletes` group to SimplePolicy
 - MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances
 - Support pagination in emoji packs API (for packs and for files in pack)
+- Support for viewing instances favicons next to posts and accounts
 
 <details>
   <summary>API Changes</summary>
@@ -65,6 +66,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Mastodon API: Add support for filtering replies in public and home timelines.
 - Mastodon API: Support for `bot` field in `/api/v1/accounts/update_credentials`.
 - Mastodon API: Support irreversible property for filters.
+- Mastodon API: Add pleroma.favicon field to accounts.
 - Admin API: endpoints for create/update/delete OAuth Apps.
 - Admin API: endpoint for status view.
 - OTP: Add command to reload emoji packs
diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md
index 29141ed0c..03c7f4608 100644
--- a/docs/API/differences_in_mastoapi_responses.md
+++ b/docs/API/differences_in_mastoapi_responses.md
@@ -71,6 +71,7 @@ Has these additional fields under the `pleroma` object:
 - `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
 - `unread_notifications_count`: The count of unread notifications. Only returned to the account owner.
 - `notification_settings`: object, can be absent. See `/api/pleroma/notification_settings` for the parameters/keys returned.
+- `favicon`: nullable URL string, Favicon image of the user's instance
 
 ### Source
 
diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex
index 84f18f1b6..e6f163cb7 100644
--- a/lib/pleroma/web/api_spec/schemas/account.ex
+++ b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -102,6 +102,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
             type: :object,
             description:
               "A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`"
+          },
+          favicon: %Schema{
+            type: :string,
+            format: :uri,
+            nullable: true,
+            description: "Favicon image of the user's instance"
           }
         }
       },