From a31af93e1d10d9db8796d86ccda35873697b5a4c Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Tue, 10 Sep 2019 16:43:10 +0300
Subject: [PATCH 01/15] added tests /activity_pub/transmogrifier.ex
---
.../web/activity_pub/transmogrifier.ex | 264 +++++++-----------
test/web/activity_pub/transmogrifier_test.exs | 162 +++++++++++
2 files changed, 270 insertions(+), 156 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 468961bd0..93b3a1f97 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -41,8 +41,7 @@ def fix_object(object, options \\ []) do
end
def fix_summary(%{"summary" => nil} = object) do
- object
- |> Map.put("summary", "")
+ Map.put(object, "summary", "")
end
def fix_summary(%{"summary" => _} = object) do
@@ -50,10 +49,7 @@ def fix_summary(%{"summary" => _} = object) do
object
end
- def fix_summary(object) do
- object
- |> Map.put("summary", "")
- end
+ def fix_summary(object), do: Map.put(object, "summary", "")
def fix_addressing_list(map, field) do
cond do
@@ -73,13 +69,9 @@ def fix_explicit_addressing(
explicit_mentions,
follower_collection
) do
- explicit_to =
- to
- |> Enum.filter(fn x -> x in explicit_mentions end)
+ explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
- explicit_cc =
- to
- |> Enum.filter(fn x -> x not in explicit_mentions end)
+ explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
final_cc =
(cc ++ explicit_cc)
@@ -97,13 +89,19 @@ def fix_explicit_addressing(object, _explicit_mentions, _followers_collection),
def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
def fix_explicit_addressing(object) do
- explicit_mentions =
+ explicit_mentions = Utils.determine_explicit_mentions(object)
+
+ %User{follower_address: follower_collection} =
object
- |> Utils.determine_explicit_mentions()
+ |> Containment.get_actor()
+ |> User.get_cached_by_ap_id()
- follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
-
- explicit_mentions = explicit_mentions ++ [Pleroma.Constants.as_public(), follower_collection]
+ explicit_mentions =
+ explicit_mentions ++
+ [
+ Pleroma.Constants.as_public(),
+ follower_collection
+ ]
fix_explicit_addressing(object, explicit_mentions, follower_collection)
end
@@ -147,48 +145,25 @@ def fix_addressing(object) do
end
def fix_actor(%{"attributedTo" => actor} = object) do
- object
- |> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
+ Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
end
def fix_in_reply_to(object, options \\ [])
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
when not is_nil(in_reply_to) do
- in_reply_to_id =
- cond do
- is_bitstring(in_reply_to) ->
- in_reply_to
-
- is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
- in_reply_to["id"]
-
- is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
- Enum.at(in_reply_to, 0)
-
- # Maybe I should output an error too?
- true ->
- ""
- end
-
+ in_reply_to_id = prepare_in_reply_to(in_reply_to)
object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
if Federator.allowed_incoming_reply_depth?(options[:depth]) do
- case get_obj_helper(in_reply_to_id, options) do
- {:ok, replied_object} ->
- with %Activity{} = _activity <-
- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
- object
- |> Map.put("inReplyTo", replied_object.data["id"])
- |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
- |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
- |> Map.put("context", replied_object.data["context"] || object["conversation"])
- else
- e ->
- Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
- object
- end
-
+ with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
+ %Activity{} = _ <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
+ object
+ |> Map.put("inReplyTo", replied_object.data["id"])
+ |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
+ |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
+ |> Map.put("context", replied_object.data["context"] || object["conversation"])
+ else
e ->
Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
object
@@ -200,6 +175,22 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
def fix_in_reply_to(object, _options), do: object
+ defp prepare_in_reply_to(in_reply_to) do
+ cond do
+ is_bitstring(in_reply_to) ->
+ in_reply_to
+
+ is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
+ in_reply_to["id"]
+
+ is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
+ Enum.at(in_reply_to, 0)
+
+ true ->
+ ""
+ end
+ end
+
def fix_context(object) do
context = object["context"] || object["conversation"] || Utils.generate_context_id()
@@ -210,8 +201,7 @@ def fix_context(object) do
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments =
- attachment
- |> Enum.map(fn data ->
+ Enum.map(attachment, fn data ->
media_type = data["mediaType"] || data["mimeType"]
href = data["url"] || data["href"]
@@ -222,30 +212,23 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
|> Map.put("url", url)
end)
- object
- |> Map.put("attachment", attachments)
+ Map.put(object, "attachment", attachments)
end
def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
- Map.put(object, "attachment", [attachment])
- |> fix_attachments()
+ fix_attachments(Map.put(object, "attachment", [attachment]))
end
def fix_attachments(object), do: object
def fix_url(%{"url" => url} = object) when is_map(url) do
- object
- |> Map.put("url", url["href"])
+ Map.put(object, "url", url["href"])
end
def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
first_element = Enum.at(url, 0)
- link_element =
- url
- |> Enum.filter(fn x -> is_map(x) end)
- |> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
- |> Enum.at(0)
+ link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
object
|> Map.put("attachment", [first_element])
@@ -263,36 +246,32 @@ def fix_url(%{"type" => object_type, "url" => url} = object)
true -> ""
end
- object
- |> Map.put("url", url_string)
+ Map.put(object, "url", url_string)
end
def fix_url(object), do: object
def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
- emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
-
emoji =
- emoji
+ tags
+ |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
|> Enum.reduce(%{}, fn data, mapping ->
name = String.trim(data["name"], ":")
- mapping |> Map.put(name, data["icon"]["url"])
+ Map.put(mapping, name, data["icon"]["url"])
end)
# we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
emoji = Map.merge(object["emoji"] || %{}, emoji)
- object
- |> Map.put("emoji", emoji)
+ Map.put(object, "emoji", emoji)
end
def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
name = String.trim(tag["name"], ":")
emoji = %{name => tag["icon"]["url"]}
- object
- |> Map.put("emoji", emoji)
+ Map.put(object, "emoji", emoji)
end
def fix_emoji(object), do: object
@@ -303,17 +282,13 @@ def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
- combined = tag ++ tags
-
- object
- |> Map.put("tag", combined)
+ Map.put(object, "tag", tag ++ tags)
end
def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
combined = [tag, String.slice(hashtag, 1..-1)]
- object
- |> Map.put("tag", combined)
+ Map.put(object, "tag", combined)
end
def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
@@ -325,8 +300,7 @@ def fix_content_map(%{"contentMap" => content_map} = object) do
content_groups = Map.to_list(content_map)
{_, content} = Enum.at(content_groups, 0)
- object
- |> Map.put("content", content)
+ Map.put(object, "content", content)
end
def fix_content_map(object), do: object
@@ -335,16 +309,11 @@ def fix_type(object, options \\ [])
def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
when is_binary(reply_id) do
- reply =
- with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
- {:ok, object} <- get_obj_helper(reply_id, options) do
- object
- end
-
- if reply && reply.data["type"] == "Question" do
+ with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
+ {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
Map.put(object, "type", "Answer")
else
- object
+ _ -> object
end
end
@@ -376,6 +345,17 @@ defp get_follow_activity(follow_object, followed) do
end
end
+ # Reduce the object list to find the reported user.
+ defp get_reported(objects) do
+ Enum.reduce_while(objects, nil, fn ap_id, _ ->
+ with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
+ {:halt, user}
+ else
+ _ -> {:cont, nil}
+ end
+ end)
+ end
+
def handle_incoming(data, options \\ [])
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
@@ -384,31 +364,19 @@ def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} =
with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "",
%User{} = actor <- User.get_cached_by_ap_id(actor),
-
# Reduce the object list to find the reported user.
- %User{} = account <-
- Enum.reduce_while(objects, nil, fn ap_id, _ ->
- with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
- {:halt, user}
- else
- _ -> {:cont, nil}
- end
- end),
-
+ %User{} = account <- get_reported(objects),
# Remove the reported user from the object list.
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
- params = %{
+ %{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content,
- additional: %{
- "cc" => [account.ap_id]
- }
+ additional: %{"cc" => [account.ap_id]}
}
-
- ActivityPub.flag(params)
+ |> ActivityPub.flag()
end
end
@@ -755,8 +723,13 @@ def handle_incoming(
def handle_incoming(_, _), do: :error
+ @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do
- if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
+ if object = Object.normalize(id, true, options) do
+ {:ok, object}
+ else
+ nil
+ end
end
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
@@ -855,27 +828,24 @@ def prepare_outgoing(%{"type" => _type} = data) do
{:ok, data}
end
- def maybe_fix_object_url(data) do
- if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
- case get_obj_helper(data["object"]) do
- {:ok, relative_object} ->
- if relative_object.data["external_url"] do
- _data =
- data
- |> Map.put("object", relative_object.data["external_url"])
- else
- data
- end
-
- e ->
- Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
- data
- end
+ def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
+ with false <- String.starts_with?(object, "http"),
+ {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
+ %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
+ relative_object do
+ Map.put(data, "object", external_url)
else
- data
+ {:fetch, e} ->
+ Logger.error("Couldn't fetch #{object} #{inspect(e)}")
+ data
+
+ _ ->
+ data
end
end
+ def maybe_fix_object_url(data), do: data
+
def add_hashtags(object) do
tags =
(object["tag"] || [])
@@ -893,8 +863,7 @@ def add_hashtags(object) do
tag
end)
- object
- |> Map.put("tag", tags)
+ Map.put(object, "tag", tags)
end
def add_mention_tags(object) do
@@ -907,15 +876,13 @@ def add_mention_tags(object) do
tags = object["tag"] || []
- object
- |> Map.put("tag", tags ++ mentions)
+ Map.put(object, "tag", tags ++ mentions)
end
def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
user_info = add_emoji_tags(user_info)
- object
- |> Map.put(:info, user_info)
+ Map.put(object, :info, user_info)
end
# TODO: we should probably send mtime instead of unix epoch time for updated
@@ -923,8 +890,7 @@ def add_emoji_tags(%{"emoji" => emoji} = object) do
tags = object["tag"] || []
out =
- emoji
- |> Enum.map(fn {name, url} ->
+ Enum.map(emoji, fn {name, url} ->
%{
"icon" => %{"url" => url, "type" => "Image"},
"name" => ":" <> name <> ":",
@@ -934,13 +900,10 @@ def add_emoji_tags(%{"emoji" => emoji} = object) do
}
end)
- object
- |> Map.put("tag", tags ++ out)
+ Map.put(object, "tag", tags ++ out)
end
- def add_emoji_tags(object) do
- object
- end
+ def add_emoji_tags(object), do: object
def set_conversation(object) do
Map.put(object, "conversation", object["context"])
@@ -959,9 +922,7 @@ def set_type(object), do: object
def add_attributed_to(object) do
attributed_to = object["attributedTo"] || object["actor"]
-
- object
- |> Map.put("attributedTo", attributed_to)
+ Map.put(object, "attributedTo", attributed_to)
end
def prepare_attachments(object) do
@@ -972,8 +933,7 @@ def prepare_attachments(object) do
%{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
end)
- object
- |> Map.put("attachment", attachments)
+ Map.put(object, "attachment", attachments)
end
defp strip_internal_fields(object) do
@@ -990,12 +950,9 @@ defp strip_internal_fields(object) do
end
defp strip_internal_tags(%{"tag" => tags} = object) do
- tags =
- tags
- |> Enum.filter(fn x -> is_map(x) end)
+ tags = Enum.filter(tags, fn x -> is_map(x) end)
- object
- |> Map.put("tag", tags)
+ Map.put(object, "tag", tags)
end
defp strip_internal_tags(object), do: object
@@ -1074,16 +1031,11 @@ def maybe_retire_websub(ap_id) do
end
end
- def maybe_fix_user_url(data) do
- if is_map(data["url"]) do
- Map.put(data, "url", data["url"]["href"])
- else
- data
- end
+ def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
+ Map.put(data, "url", url["href"])
end
- def maybe_fix_user_object(data) do
- data
- |> maybe_fix_user_url
- end
+ def maybe_fix_user_url(data), do: data
+
+ def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
end
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 0661d5d7c..63c869d35 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -1451,4 +1451,166 @@ test "removes recipient's follower collection from cc", %{user: user} do
refute recipient.follower_address in fixed_object["to"]
end
end
+
+ describe "fix_summary/1" do
+ test "returns fixed object" do
+ assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""}
+ assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"}
+ assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""}
+ end
+ end
+
+ describe "fix_in_reply_to/2" do
+ clear_config([:instance, :federation_incoming_replies_max_depth])
+
+ setup do
+ data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
+ [data: data]
+ end
+
+ test "returns not modified object when hasn't containts inReplyTo field", %{data: data} do
+ assert Transmogrifier.fix_in_reply_to(data) == data
+ end
+
+ test "returns object with inReplyToAtomUri when denied incoming reply", %{data: data} do
+ Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
+
+ object_with_reply =
+ Map.put(data["object"], "inReplyTo", "https://shitposter.club/notice/2827873")
+
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+ assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873"
+ assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
+
+ object_with_reply =
+ Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"})
+
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+ assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"}
+ assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
+
+ object_with_reply =
+ Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"])
+
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+ assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"]
+ assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
+
+ object_with_reply = Map.put(data["object"], "inReplyTo", [])
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+ assert modified_object["inReplyTo"] == []
+ assert modified_object["inReplyToAtomUri"] == ""
+ end
+
+ test "returns modified object when allowed incoming reply", %{data: data} do
+ object_with_reply =
+ Map.put(
+ data["object"],
+ "inReplyTo",
+ "https://shitposter.club/notice/2827873"
+ )
+
+ Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 5)
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+
+ assert modified_object["inReplyTo"] ==
+ "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
+
+ assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
+
+ assert modified_object["conversation"] ==
+ "tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26"
+
+ assert modified_object["context"] ==
+ "tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26"
+ end
+ end
+
+ describe "fix_url/1" do
+ test "fixes data for object when url is map" do
+ object = %{
+ "url" => %{
+ "type" => "Link",
+ "mimeType" => "video/mp4",
+ "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
+ }
+ }
+
+ assert Transmogrifier.fix_url(object) == %{
+ "url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
+ }
+ end
+
+ test "fixes data for video object" do
+ object = %{
+ "type" => "Video",
+ "url" => [
+ %{
+ "type" => "Link",
+ "mimeType" => "video/mp4",
+ "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
+ },
+ %{
+ "type" => "Link",
+ "mimeType" => "video/mp4",
+ "href" => "https://peertube46fb-ad81-2d4c2d1630e3-240.mp4"
+ },
+ %{
+ "type" => "Link",
+ "mimeType" => "text/html",
+ "href" => "https://peertube.-2d4c2d1630e3"
+ },
+ %{
+ "type" => "Link",
+ "mimeType" => "text/html",
+ "href" => "https://peertube.-2d4c2d16377-42"
+ }
+ ]
+ }
+
+ assert Transmogrifier.fix_url(object) == %{
+ "attachment" => [
+ %{
+ "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4",
+ "mimeType" => "video/mp4",
+ "type" => "Link"
+ }
+ ],
+ "type" => "Video",
+ "url" => "https://peertube.-2d4c2d1630e3"
+ }
+ end
+
+ test "fixes url for not Video object" do
+ object = %{
+ "type" => "Text",
+ "url" => [
+ %{
+ "type" => "Link",
+ "mimeType" => "text/html",
+ "href" => "https://peertube.-2d4c2d1630e3"
+ },
+ %{
+ "type" => "Link",
+ "mimeType" => "text/html",
+ "href" => "https://peertube.-2d4c2d16377-42"
+ }
+ ]
+ }
+
+ assert Transmogrifier.fix_url(object) == %{
+ "type" => "Text",
+ "url" => "https://peertube.-2d4c2d1630e3"
+ }
+
+ assert Transmogrifier.fix_url(%{"type" => "Text", "url" => []}) == %{
+ "type" => "Text",
+ "url" => ""
+ }
+ end
+
+ test "retunrs not modified object" do
+ assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
+ end
+ end
end
From fcf604fa43031be747b33c05866a192d9651322c Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Wed, 11 Sep 2019 07:23:33 +0300
Subject: [PATCH 02/15] added tests
---
lib/pleroma/object/fetcher.ex | 77 ++++++++++---------
.../web/activity_pub/transmogrifier.ex | 12 +--
test/web/activity_pub/transmogrifier_test.exs | 74 ++++++++++++++++++
3 files changed, 121 insertions(+), 42 deletions(-)
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index c1795ae0f..2217d1eb3 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -13,6 +13,7 @@ defmodule Pleroma.Object.Fetcher do
require Logger
+ @spec reinject_object(map()) :: {:ok, Object.t()} | {:error, any()}
defp reinject_object(data) do
Logger.debug("Reinjecting object #{data["id"]}")
@@ -29,50 +30,54 @@ defp reinject_object(data) do
# TODO:
# This will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id, options \\ []) do
- if object = Object.get_cached_by_ap_id(id) do
+ with {:fetch_object, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
+ {:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
+ {:normalize, nil} <- {:normalize, Object.normalize(data, false)},
+ params <- prepare_activity_params(data),
+ {:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
+ {:ok, activity} <- Transmogrifier.handle_incoming(params, options),
+ {:object, _data, %Object{} = object} <-
+ {:object, data, Object.normalize(activity, false)} do
{:ok, object}
else
- Logger.info("Fetching #{id} via AP")
+ {:containment, _} ->
+ {:error, "Object containment failed."}
- with {:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
- {:normalize, nil} <- {:normalize, Object.normalize(data, false)},
- params <- %{
- "type" => "Create",
- "to" => data["to"],
- "cc" => data["cc"],
- # Should we seriously keep this attributedTo thing?
- "actor" => data["actor"] || data["attributedTo"],
- "object" => data
- },
- {:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
- {:ok, activity} <- Transmogrifier.handle_incoming(params, options),
- {:object, _data, %Object{} = object} <-
- {:object, data, Object.normalize(activity, false)} do
+ {:error, {:reject, nil}} ->
+ {:reject, nil}
+
+ {:object, data, nil} ->
+ reinject_object(data)
+
+ {:normalize, object = %Object{}} ->
{:ok, object}
- else
- {:containment, _} ->
- {:error, "Object containment failed."}
- {:error, {:reject, nil}} ->
- {:reject, nil}
+ {:fetch_object, %Object{} = object} ->
+ {:ok, object}
- {:object, data, nil} ->
- reinject_object(data)
+ _e ->
+ # Only fallback when receiving a fetch/normalization error with ActivityPub
+ Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
- {:normalize, object = %Object{}} ->
- {:ok, object}
-
- _e ->
- # Only fallback when receiving a fetch/normalization error with ActivityPub
- Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
-
- # FIXME: OStatus Object Containment?
- case OStatus.fetch_activity_from_url(id) do
- {:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
- e -> e
- end
- end
+ # FIXME: OStatus Object Containment?
+ case OStatus.fetch_activity_from_url(id) do
+ {:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
+ e -> e
+ end
end
+
+ # end
+ end
+
+ defp prepare_activity_params(data) do
+ %{
+ "type" => "Create",
+ "to" => data["to"],
+ "cc" => data["cc"],
+ # Should we seriously keep this attributedTo thing?
+ "actor" => data["actor"] || data["attributedTo"],
+ "object" => data
+ }
end
def fetch_object_from_id!(id, options \\ []) do
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 93b3a1f97..18a3c3f39 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -204,7 +204,6 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
Enum.map(attachment, fn data ->
media_type = data["mediaType"] || data["mimeType"]
href = data["url"] || data["href"]
-
url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
data
@@ -216,7 +215,9 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
end
def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
- fix_attachments(Map.put(object, "attachment", [attachment]))
+ object
+ |> Map.put("attachment", [attachment])
+ |> fix_attachments()
end
def fix_attachments(object), do: object
@@ -725,10 +726,9 @@ def handle_incoming(_, _), do: :error
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do
- if object = Object.normalize(id, true, options) do
- {:ok, object}
- else
- nil
+ case Object.normalize(id, true, options) do
+ %Object{} = object -> {:ok, object}
+ _ -> nil
end
end
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 63c869d35..ab6e76056 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -1613,4 +1613,78 @@ test "retunrs not modified object" do
assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
end
end
+
+ describe "get_obj_helper/2" do
+ test "returns nil when cannot normalize object" do
+ refute Transmogrifier.get_obj_helper("test-obj-id")
+ end
+
+ test "returns {:ok, %Object{}} for success case" do
+ assert {:ok, %Object{}} =
+ Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873")
+ end
+ end
+
+ describe "fix_attachments/1" do
+ test "returns not modified object" do
+ data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
+ assert Transmogrifier.fix_attachments(data) == data
+ end
+
+ test "returns modified object when attachment is map" do
+ assert Transmogrifier.fix_attachments(%{
+ "attachment" => %{
+ "mediaType" => "video/mp4",
+ "url" => "https://peertube.moe/stat-480.mp4"
+ }
+ }) == %{
+ "attachment" => [
+ %{
+ "mediaType" => "video/mp4",
+ "url" => [
+ %{
+ "href" => "https://peertube.moe/stat-480.mp4",
+ "mediaType" => "video/mp4",
+ "type" => "Link"
+ }
+ ]
+ }
+ ]
+ }
+ end
+
+ test "returns modified object when attachment is list" do
+ assert Transmogrifier.fix_attachments(%{
+ "attachment" => [
+ %{"mediaType" => "video/mp4", "url" => "https://pe.er/stat-480.mp4"},
+ %{"mimeType" => "video/mp4", "href" => "https://pe.er/stat-480.mp4"}
+ ]
+ }) == %{
+ "attachment" => [
+ %{
+ "mediaType" => "video/mp4",
+ "url" => [
+ %{
+ "href" => "https://pe.er/stat-480.mp4",
+ "mediaType" => "video/mp4",
+ "type" => "Link"
+ }
+ ]
+ },
+ %{
+ "href" => "https://pe.er/stat-480.mp4",
+ "mediaType" => "video/mp4",
+ "mimeType" => "video/mp4",
+ "url" => [
+ %{
+ "href" => "https://pe.er/stat-480.mp4",
+ "mediaType" => "video/mp4",
+ "type" => "Link"
+ }
+ ]
+ }
+ ]
+ }
+ end
+ end
end
From 007e0c1ce158bdfc11738a194944534837ae0258 Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Wed, 11 Sep 2019 23:19:06 +0300
Subject: [PATCH 03/15] added tests
---
.../web/activity_pub/transmogrifier.ex | 35 ++++++++++---------
.../web/activity_pub/views/user_view.ex | 7 ++--
test/web/activity_pub/transmogrifier_test.exs | 31 ++++++++++++++++
.../web/activity_pub/views/user_view_test.exs | 16 +++++++++
4 files changed, 68 insertions(+), 21 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 18a3c3f39..9f699de9e 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -870,41 +870,44 @@ def add_mention_tags(object) do
mentions =
object
|> Utils.get_notified_from_object()
- |> Enum.map(fn user ->
- %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
- end)
+ |> Enum.map(&build_mention_tag/1)
tags = object["tag"] || []
Map.put(object, "tag", tags ++ mentions)
end
- def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
- user_info = add_emoji_tags(user_info)
+ defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
+ %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
+ end
- Map.put(object, :info, user_info)
+ def take_emoji_tags(%User{info: %{emoji: emoji} = _user_info} = _user) do
+ emoji
+ |> Enum.flat_map(&Map.to_list/1)
+ |> Enum.map(&build_emoji_tag/1)
end
# TODO: we should probably send mtime instead of unix epoch time for updated
def add_emoji_tags(%{"emoji" => emoji} = object) do
tags = object["tag"] || []
- out =
- Enum.map(emoji, fn {name, url} ->
- %{
- "icon" => %{"url" => url, "type" => "Image"},
- "name" => ":" <> name <> ":",
- "type" => "Emoji",
- "updated" => "1970-01-01T00:00:00Z",
- "id" => url
- }
- end)
+ out = Enum.map(emoji, &build_emoji_tag/1)
Map.put(object, "tag", tags ++ out)
end
def add_emoji_tags(object), do: object
+ defp build_emoji_tag({name, url}) do
+ %{
+ "icon" => %{"url" => url, "type" => "Image"},
+ "name" => ":" <> name <> ":",
+ "type" => "Emoji",
+ "updated" => "1970-01-01T00:00:00Z",
+ "id" => url
+ }
+ end
+
def set_conversation(object) do
Map.put(object, "conversation", object["context"])
end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 7be734b26..8abfa1fcd 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -75,10 +75,7 @@ def render("user.json", %{user: user}) do
endpoints = render("endpoints.json", %{user: user})
- user_tags =
- user
- |> Transmogrifier.add_emoji_tags()
- |> Map.get("tag", [])
+ emoji_tags = Transmogrifier.take_emoji_tags(user)
fields =
user.info
@@ -110,7 +107,7 @@ def render("user.json", %{user: user}) do
},
"endpoints" => endpoints,
"attachment" => fields,
- "tag" => (user.info.source_data["tag"] || []) ++ user_tags
+ "tag" => (user.info.source_data["tag"] || []) ++ emoji_tags
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index ab6e76056..87ef843c6 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -1687,4 +1687,35 @@ test "returns modified object when attachment is list" do
}
end
end
+
+ describe "fix_emoji/1" do
+ test "returns not modified object when object not contains tags" do
+ data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
+ assert Transmogrifier.fix_emoji(data) == data
+ end
+
+ test "returns object with emoji when object contains list tags" do
+ assert Transmogrifier.fix_emoji(%{
+ "tag" => [
+ %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}},
+ %{"type" => "Hashtag"}
+ ]
+ }) == %{
+ "emoji" => %{"bib" => "/test"},
+ "tag" => [
+ %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"},
+ %{"type" => "Hashtag"}
+ ]
+ }
+ end
+
+ test "returns object with emoji when object contains map tag" do
+ assert Transmogrifier.fix_emoji(%{
+ "tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}
+ }) == %{
+ "emoji" => %{"bib" => "/test"},
+ "tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}
+ }
+ end
+ end
end
diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs
index fb7fd9e79..4390f9272 100644
--- a/test/web/activity_pub/views/user_view_test.exs
+++ b/test/web/activity_pub/views/user_view_test.exs
@@ -37,6 +37,22 @@ test "Renders profile fields" do
} = UserView.render("user.json", %{user: user})
end
+ test "Renders with emoji tags" do
+ user = insert(:user, %{info: %{emoji: [%{"bib" => "/test"}]}})
+
+ assert %{
+ "tag" => [
+ %{
+ "icon" => %{"type" => "Image", "url" => "/test"},
+ "id" => "/test",
+ "name" => ":bib:",
+ "type" => "Emoji",
+ "updated" => "1970-01-01T00:00:00Z"
+ }
+ ]
+ } = UserView.render("user.json", %{user: user})
+ end
+
test "Does not add an avatar image if the user hasn't set one" do
user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
From cf3041220a7a14dc3fac24177fac1f4aecc77f5f Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn
Date: Tue, 17 Sep 2019 15:22:46 +0700
Subject: [PATCH 04/15] Add support for `rel="ugc"`
---
config/config.exs | 2 +-
config/description.exs | 2 +-
docs/config.md | 2 +-
lib/pleroma/html.ex | 6 +++--
test/formatter_test.exs | 24 ++++++++++---------
test/web/common_api/common_api_utils_test.exs | 6 ++---
.../update_credentials_test.exs | 2 +-
7 files changed, 24 insertions(+), 20 deletions(-)
diff --git a/config/config.exs b/config/config.exs
index c7e0cf09f..26dc4d16d 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -507,7 +507,7 @@
class: false,
strip_prefix: false,
new_window: false,
- rel: false
+ rel: "ugc"
]
config :pleroma, :ldap,
diff --git a/config/description.exs b/config/description.exs
index 65ea6bf01..abfb6370f 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -1900,7 +1900,7 @@
key: :rel,
type: [:string, false],
description: "override the rel attribute. false to clear",
- suggestions: ["noopener noreferrer", false]
+ suggestions: ["ugc", false]
},
%{
key: :new_window,
diff --git a/docs/config.md b/docs/config.md
index 3f37fa561..def462900 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -521,7 +521,7 @@ config :auto_linker,
class: false,
strip_prefix: false,
new_window: false,
- rel: false
+ rel: "ugc"
]
```
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index 3951f0f51..937bafed5 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -184,7 +184,8 @@ defmodule Pleroma.HTML.Scrubber.Default do
"tag",
"nofollow",
"noopener",
- "noreferrer"
+ "noreferrer",
+ "ugc"
])
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
@@ -304,7 +305,8 @@ defmodule Pleroma.HTML.Scrubber.LinksOnly do
"nofollow",
"noopener",
"noreferrer",
- "me"
+ "me",
+ "ugc"
])
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
diff --git a/test/formatter_test.exs b/test/formatter_test.exs
index c443dfe7c..3674577d6 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -39,21 +39,21 @@ test "turning urls into links" do
text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ."
expected =
- "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ."
+ ~S(Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla .)
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://mastodon.social/@lambadalambda"
expected =
- "https://mastodon.social/@lambadalambda"
+ ~S(https://mastodon.social/@lambadalambda)
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://mastodon.social:4000/@lambadalambda"
expected =
- "https://mastodon.social:4000/@lambadalambda"
+ ~S(https://mastodon.social:4000/@lambadalambda)
assert {^expected, [], []} = Formatter.linkify(text)
@@ -63,55 +63,57 @@ test "turning urls into links" do
assert {^expected, [], []} = Formatter.linkify(text)
text = "http://www.cs.vu.nl/~ast/intel/"
- expected = "http://www.cs.vu.nl/~ast/intel/"
+
+ expected =
+ ~S(http://www.cs.vu.nl/~ast/intel/)
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087"
expected =
- "https://forum.zdoom.org/viewtopic.php?f=44&t=57087"
+ "https://forum.zdoom.org/viewtopic.php?f=44&t=57087"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul"
expected =
- "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul"
+ "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://www.google.co.jp/search?q=Nasim+Aghdam"
expected =
- "https://www.google.co.jp/search?q=Nasim+Aghdam"
+ "https://www.google.co.jp/search?q=Nasim+Aghdam"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://en.wikipedia.org/wiki/Duff's_device"
expected =
- "https://en.wikipedia.org/wiki/Duff's_device"
+ "https://en.wikipedia.org/wiki/Duff's_device"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://pleroma.com https://pleroma.com/sucks"
expected =
- "https://pleroma.com https://pleroma.com/sucks"
+ "https://pleroma.com https://pleroma.com/sucks"
assert {^expected, [], []} = Formatter.linkify(text)
text = "xmpp:contact@hacktivis.me"
- expected = "xmpp:contact@hacktivis.me"
+ expected = "xmpp:contact@hacktivis.me"
assert {^expected, [], []} = Formatter.linkify(text)
text =
"magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com"
- expected = "#{text}"
+ expected = "#{text}"
assert {^expected, [], []} = Formatter.linkify(text)
end
diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs
index 230146451..78cfe3c5f 100644
--- a/test/web/common_api/common_api_utils_test.exs
+++ b/test/web/common_api/common_api_utils_test.exs
@@ -157,11 +157,11 @@ test "works for text/markdown with mentions" do
text = "**hello world**\n\n*another @user__test and @user__test google.com paragraph*"
expected =
- "hello world
\nanother hello world
\nanother @user__test and @user__test and @user__test google.com paragraph
\n"
+ }" class="u-url mention" href="http://foo.com/user__test">@user__test google.com paragraph
\n)
{output, _, _} = Utils.format_input(text, "text/markdown")
diff --git a/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
index 89d4ca37e..1e8d0d03b 100644
--- a/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
+++ b/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
@@ -334,7 +334,7 @@ test "update fields", %{conn: conn} do
assert account["fields"] == [
%{"name" => "foo", "value" => "bar"},
- %{"name" => "link", "value" => "cofe.io"}
+ %{"name" => "link", "value" => ~S(cofe.io)}
]
assert account["source"]["fields"] == [
From d639cdcecb1b9cd2326b98c926dff8b0f4c27e3c Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn
Date: Thu, 19 Sep 2019 14:04:13 +0700
Subject: [PATCH 05/15] Update "config/description.exs"
---
config/description.exs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/config/description.exs b/config/description.exs
index abfb6370f..510e285df 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -1900,7 +1900,7 @@
key: :rel,
type: [:string, false],
description: "override the rel attribute. false to clear",
- suggestions: ["ugc", false]
+ suggestions: ["ugc", "noopener noreferrer", false]
},
%{
key: :new_window,
From 95c948110ca130559fd6a5302011aa58900274ac Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn
Date: Thu, 19 Sep 2019 14:39:52 +0700
Subject: [PATCH 06/15] Add `rel="ugc"` to hashtags and mentions
---
lib/pleroma/formatter.ex | 6 ++--
test/formatter_test.exs | 30 +++++++++++--------
test/user_test.exs | 4 +--
test/web/common_api/common_api_utils_test.exs | 4 +--
.../update_credentials_test.exs | 7 ++---
.../mastodon_api_controller_test.exs | 8 ++---
test/web/twitter_api/twitter_api_test.exs | 4 ++-
7 files changed, 35 insertions(+), 28 deletions(-)
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 607843a5b..23a5ac8fe 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -36,9 +36,9 @@ def mention_handler("@" <> nickname, buffer, opts, acc) do
nickname_text = get_nickname_text(nickname, opts)
link =
- "@#{
+ ~s(@#{
nickname_text
- }"
+ })
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
@@ -50,7 +50,7 @@ def mention_handler("@" <> nickname, buffer, opts, acc) do
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
tag = String.downcase(tag)
url = "#{Pleroma.Web.base_url()}/tag/#{tag}"
- link = "#{tag_text}"
+ link = ~s(#{tag_text})
{link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
end
diff --git a/test/formatter_test.exs b/test/formatter_test.exs
index 3674577d6..2e4280fc2 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -19,7 +19,7 @@ test "turns hashtags into links" do
text = "I love #cofe and #2hu"
expected_text =
- "I love #cofe and #2hu"
+ ~s(I love #cofe and #2hu)
assert {^expected_text, [], _tags} = Formatter.linkify(text)
end
@@ -28,7 +28,7 @@ test "does not turn html characters to tags" do
text = "#fact_3: pleroma does what mastodon't"
expected_text =
- "#fact_3: pleroma does what mastodon't"
+ ~s(#fact_3: pleroma does what mastodon't)
assert {^expected_text, [], _tags} = Formatter.linkify(text)
end
@@ -137,13 +137,13 @@ test "gives a replacement for user links, using local nicknames in user links te
assert length(mentions) == 3
expected_text =
- "@gsimg According to @archa_eme_, that is @daggsy. Also hello @archaeme"
+ }" class="u-url mention" href="#{archaeme_remote.ap_id}" rel="ugc">@archaeme)
assert expected_text == text
end
@@ -158,7 +158,9 @@ test "gives a replacement for user links when the user is using Osada" do
assert length(mentions) == 1
expected_text =
- "@mike test"
+ ~s(@mike test)
assert expected_text == text
end
@@ -172,7 +174,7 @@ test "gives a replacement for single-character local nicknames" do
assert length(mentions) == 1
expected_text =
- "@o hi"
+ ~s(@o hi)
assert expected_text == text
end
@@ -194,13 +196,17 @@ test "given the 'safe_mention' option, it will only mention people in the beginn
assert mentions == [{"@#{user.nickname}", user}, {"@#{other_user.nickname}", other_user}]
assert expected_text ==
- "@#{user.nickname} @#{other_user.nickname} hey dudes i hate @#{third_user.nickname}"
+ }" class="u-url mention" href="#{third_user.ap_id}" rel="ugc">@#{
+ third_user.nickname
+ })
end
test "given the 'safe_mention' option, it will still work without any mention" do
diff --git a/test/user_test.exs b/test/user_test.exs
index 39ba69668..6852fcd40 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1294,9 +1294,9 @@ test "preserves hosts in user links text" do
bio = "A.k.a. @nick@domain.com"
expected_text =
- "A.k.a. @nick@domain.com"
+ }" rel="ugc">@nick@domain.com)
assert expected_text == User.parse_bio(bio, user)
end
diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs
index 78cfe3c5f..2588898d0 100644
--- a/test/web/common_api/common_api_utils_test.exs
+++ b/test/web/common_api/common_api_utils_test.exs
@@ -159,9 +159,9 @@ test "works for text/markdown with mentions" do
expected =
~s(hello world
\nanother @user__test and @user__test and @user__test google.com paragraph
\n)
+ }" class="u-url mention" href="http://foo.com/user__test" rel="ugc">@user__test google.com paragraph\n)
{output, _, _} = Utils.format_input(text, "text/markdown")
diff --git a/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
index 1e8d0d03b..560f55137 100644
--- a/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
+++ b/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
@@ -86,10 +86,9 @@ test "updates the user's bio", %{conn: conn} do
assert user = json_response(conn, 200)
assert user["note"] ==
- ~s(I drink #cofe with @) <> user2.nickname <> ~s()
+ ~s(I drink #cofe with @#{user2.nickname})
end
test "updates the user's locking status", %{conn: conn} do
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index fb04748bb..b85f3e758 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -996,9 +996,9 @@ test "list of notifications", %{conn: conn} do
|> get("/api/v1/notifications")
expected_response =
- "hi @#{user.nickname}"
+ }" rel="ugc">@#{user.nickname})
assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
assert response == expected_response
@@ -1018,9 +1018,9 @@ test "getting a single notification", %{conn: conn} do
|> get("/api/v1/notifications/#{notification.id}")
expected_response =
- "hi @#{user.nickname}"
+ }" rel="ugc">@#{user.nickname})
assert %{"status" => %{"content" => response}} = json_response(conn, 200)
assert response == expected_response
diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs
index 08f264431..bf1e233f5 100644
--- a/test/web/twitter_api/twitter_api_test.exs
+++ b/test/web/twitter_api/twitter_api_test.exs
@@ -109,7 +109,9 @@ test "it registers a new user and parses mentions in the bio" do
{:ok, user2} = TwitterAPI.register_user(data2)
expected_text =
- "@john test"
+ ~s(@john test)
assert user2.bio == expected_text
end
From ae1d371428e16b738b8ec638e411e5e8c1ac4937 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn
Date: Thu, 19 Sep 2019 14:53:34 +0700
Subject: [PATCH 07/15] Update CHANGELOG
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 906aa985e..f84b0ac68 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
- Improve digest email template
– Pagination: (optional) return `total` alongside with `items` when paginating
+- Add `rel="ugc"` to all links in statuses, to prevent SEO spam
### Fixed
- Following from Osada
From 7cf125245512eb49a118535eda52ddbdd0c4c6bf Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Fri, 20 Sep 2019 17:54:38 +0300
Subject: [PATCH 08/15] Mastodon API: Fix private and direct statuses not being
filtered out from the public timeline for an authenticated user (`GET
/api/v1/timelines/public`)
---
CHANGELOG.md | 2 ++
lib/pleroma/web/activity_pub/activity_pub.ex | 5 +++--
.../controllers/mastodon_api_controller.ex | 1 -
.../mastodon_api_controller_test.exs | 16 ++++++++++++++++
4 files changed, 21 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 84b64e2b9..93b7e2a10 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
- Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
+### Fixed
+- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
## [1.1.0] - 2019-??-??
### Security
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index e1e90d667..1cf8b6151 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -520,9 +520,10 @@ def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
end
def fetch_public_activities(opts \\ %{}) do
- q = fetch_activities_query([Pleroma.Constants.as_public()], opts)
+ opts = Map.drop(opts, ["user"])
- q
+ [Pleroma.Constants.as_public()]
+ |> fetch_activities_query(opts)
|> restrict_unlisted()
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
index 6704ee7e8..6421c2c53 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -381,7 +381,6 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
- |> Map.put("user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 35a0d3fe1..51f5215c2 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -97,6 +97,22 @@ test "the public timeline when public is set to false", %{conn: conn} do
|> json_response(403) == %{"error" => "This resource requires authentication."}
end
+ test "the public timeline includes only public statuses for an authenticated user" do
+ user = insert(:user)
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+
+ {:ok, _activity} = CommonAPI.post(user, %{"status" => "test"})
+ {:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "private"})
+ {:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "unlisted"})
+ {:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
+
+ res_conn = get(conn, "/api/v1/timelines/public")
+ assert length(json_response(res_conn, 200)) == 1
+ end
+
describe "posting statuses" do
setup do
user = insert(:user)
From 6f25668215f7f9fe20bfaf3dd72e2262a6d8915e Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Sun, 22 Sep 2019 16:08:07 +0300
Subject: [PATCH 09/15] Admin API: Add ability to force user's password reset
---
CHANGELOG.md | 2 ++
docs/api/admin_api.md | 8 ++++++
lib/pleroma/user.ex | 17 ++++++++++++
lib/pleroma/user/info.ex | 13 ++++++---
.../web/admin_api/admin_api_controller.ex | 9 +++++++
lib/pleroma/web/oauth/oauth_controller.ex | 5 ++++
lib/pleroma/web/router.ex | 1 +
lib/pleroma/workers/background_worker.ex | 5 ++++
test/user_test.exs | 17 ++++++++++++
.../admin_api/admin_api_controller_test.exs | 26 ++++++++++++++++++
test/web/oauth/oauth_controller_test.exs | 27 +++++++++++++++++++
.../twitter_api/password_controller_test.exs | 21 +++++++++++++++
12 files changed, 148 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 84b64e2b9..e5a84f5ae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Refreshing poll results for remote polls
+- Admin API: Add ability to force user's password reset
+
### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md
index 7637fa0d4..c6b9dd2b6 100644
--- a/docs/api/admin_api.md
+++ b/docs/api/admin_api.md
@@ -310,6 +310,14 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Params: none
- Response: password reset token (base64 string)
+## `/api/pleroma/admin/users/:nickname/force_password_reset`
+
+### Force passord reset for a user with a given nickname
+
+- Methods: `PATCH`
+- Params: none
+- Response: none (code `204`)
+
## `/api/pleroma/admin/reports`
### Get a list of reports
- Method `GET`
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index fb1f24254..ab253a274 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -269,6 +269,7 @@ def password_update_changeset(struct, params) do
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
|> put_password_hash
+ |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
end
@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
@@ -285,6 +286,20 @@ def reset_password(%User{id: user_id} = user, data) do
end
end
+ def force_password_reset_async(user) do
+ BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
+ end
+
+ @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
+ def force_password_reset(user) do
+ info_cng = User.Info.set_password_reset_pending(user.info, true)
+
+ user
+ |> change()
+ |> put_embed(:info, info_cng)
+ |> update_and_set_cache()
+ end
+
def register_changeset(struct, params \\ %{}, opts \\ []) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
@@ -1115,6 +1130,8 @@ def delete(%User{} = user) do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end
+ def perform(:force_password_reset, user), do: force_password_reset(user)
+
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
{:ok, _user} = ActivityPub.delete(user)
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index b150a57cd..67abc3ecd 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -20,6 +20,7 @@ defmodule Pleroma.User.Info do
field(:following_count, :integer, default: nil)
field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false)
+ field(:password_reset_pending, :boolean, default: false)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public")
field(:blocks, {:array, :string}, default: [])
@@ -82,6 +83,14 @@ def set_activation_status(info, deactivated) do
|> validate_required([:deactivated])
end
+ def set_password_reset_pending(info, pending) do
+ params = %{password_reset_pending: pending}
+
+ info
+ |> cast(params, [:password_reset_pending])
+ |> validate_required([:password_reset_pending])
+ end
+
def update_notification_settings(info, settings) do
settings =
settings
@@ -333,9 +342,7 @@ defp valid_field?(%{"name" => name, "value" => value}) do
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
- is_binary(name) &&
- is_binary(value) &&
- String.length(name) <= name_limit &&
+ is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
String.length(value) <= value_limit
end
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 8a8091daa..711e4dfc2 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -447,6 +447,15 @@ def get_password_reset(conn, %{"nickname" => nickname}) do
|> json(token.token)
end
+ @doc "Force password reset for a given user"
+ def force_password_reset(conn, %{"nickname" => nickname}) do
+ (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
+
+ User.force_password_reset_async(user)
+
+ json_response(conn, :no_content, "")
+ end
+
def list_reports(conn, params) do
params =
params
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 81eae2c8b..a57670e02 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -202,6 +202,8 @@ def token_exchange(
{:ok, app} <- Token.Utils.fetch_app(conn),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:user_active, true} <- {:user_active, !user.info.deactivated},
+ {:password_reset_pending, false} <-
+ {:password_reset_pending, user.info.password_reset_pending},
{:ok, scopes} <- validate_scopes(app, params),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:ok, token} <- Token.exchange_token(app, auth) do
@@ -215,6 +217,9 @@ def token_exchange(
{:user_active, false} ->
render_error(conn, :forbidden, "Your account is currently disabled")
+ {:password_reset_pending, true} ->
+ render_error(conn, :forbidden, "Password reset is required")
+
_error ->
render_invalid_credentials_error(conn)
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index b9b85fd67..a306c1b80 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -186,6 +186,7 @@ defmodule Pleroma.Web.Router do
post("/users/email_invite", AdminAPIController, :email_invite)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
+ patch("/users/:nickname/force_password_reset", AdminAPIController, :force_password_reset)
get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show)
diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex
index 082f20ab7..7ffc8eabe 100644
--- a/lib/pleroma/workers/background_worker.ex
+++ b/lib/pleroma/workers/background_worker.ex
@@ -26,6 +26,11 @@ def perform(%{"op" => "delete_user", "user_id" => user_id}, _job) do
User.perform(:delete, user)
end
+ def perform(%{"op" => "force_password_reset", "user_id" => user_id}, _job) do
+ user = User.get_cached_by_id(user_id)
+ User.perform(:force_password_reset, user)
+ end
+
def perform(
%{
"op" => "blocks_import",
diff --git a/test/user_test.exs b/test/user_test.exs
index 39ba69668..164172405 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1690,4 +1690,21 @@ test "changes email", %{user: user} do
assert {:ok, %User{email: "cofe@cofe.party"}} = User.change_email(user, "cofe@cofe.party")
end
end
+
+ describe "set_password_reset_pending/2" do
+ setup do
+ [user: insert(:user)]
+ end
+
+ test "sets password_reset_pending to true", %{user: user} do
+ %{password_reset_pending: password_reset_pending} = user.info
+
+ refute password_reset_pending
+
+ {:ok, %{info: %{password_reset_pending: password_reset_pending}}} =
+ User.force_password_reset(user)
+
+ assert password_reset_pending
+ end
+ end
end
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index 108143f6a..f00e02a7a 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -4,11 +4,13 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
use Pleroma.Web.ConnCase
+ use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.ModerationLog
alias Pleroma.Repo
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.CommonAPI
@@ -2351,6 +2353,30 @@ test "returns the log with pagination", %{conn: conn, admin: admin} do
"@#{admin.nickname} followed relay: https://example.org/relay"
end
end
+
+ describe "PATCH /users/:nickname/force_password_reset" do
+ setup %{conn: conn} do
+ admin = insert(:user, info: %{is_admin: true})
+ user = insert(:user)
+
+ %{conn: assign(conn, :user, admin), admin: admin, user: user}
+ end
+
+ test "sets password_reset_pending to true", %{admin: admin, user: user} do
+ assert user.info.password_reset_pending == false
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> patch("/api/pleroma/admin/users/#{user.nickname}/force_password_reset")
+
+ assert json_response(conn, 204) == ""
+
+ ObanHelpers.perform_all()
+
+ assert User.get_by_id(user.id).info.password_reset_pending == true
+ end
+ end
end
# Needed for testing
diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs
index 2780e1746..8b88fd784 100644
--- a/test/web/oauth/oauth_controller_test.exs
+++ b/test/web/oauth/oauth_controller_test.exs
@@ -831,6 +831,33 @@ test "rejects token exchange for valid credentials belonging to deactivated user
refute Map.has_key?(resp, "access_token")
end
+ test "rejects token exchange for user with password_reset_pending set to true" do
+ password = "testpassword"
+
+ user =
+ insert(:user,
+ password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
+ info: %{password_reset_pending: true}
+ )
+
+ app = insert(:oauth_app, scopes: ["read", "write"])
+
+ conn =
+ build_conn()
+ |> post("/oauth/token", %{
+ "grant_type" => "password",
+ "username" => user.nickname,
+ "password" => password,
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret
+ })
+
+ assert resp = json_response(conn, 403)
+
+ assert resp["error"] == "Password reset is required"
+ refute Map.has_key?(resp, "access_token")
+ end
+
test "rejects an invalid authorization code" do
app = insert(:oauth_app)
diff --git a/test/web/twitter_api/password_controller_test.exs b/test/web/twitter_api/password_controller_test.exs
index 3a7246ea8..dc6d4e3e3 100644
--- a/test/web/twitter_api/password_controller_test.exs
+++ b/test/web/twitter_api/password_controller_test.exs
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.PasswordResetToken
+ alias Pleroma.User
alias Pleroma.Web.OAuth.Token
import Pleroma.Factory
@@ -56,5 +57,25 @@ test "it returns HTTP 200", %{conn: conn} do
assert Comeonin.Pbkdf2.checkpw("test", user.password_hash)
assert length(Token.get_user_tokens(user)) == 0
end
+
+ test "it sets password_reset_pending to false", %{conn: conn} do
+ user = insert(:user, info: %{password_reset_pending: true})
+
+ {:ok, token} = PasswordResetToken.create_token(user)
+ {:ok, _access_token} = Token.create_token(insert(:oauth_app), user, %{})
+
+ params = %{
+ "password" => "test",
+ password_confirmation: "test",
+ token: token.token
+ }
+
+ conn
+ |> assign(:user, user)
+ |> post("/api/pleroma/password_reset", %{data: params})
+ |> html_response(:ok)
+
+ assert User.get_by_id(user.id).info.password_reset_pending == false
+ end
end
end
From d72d4757a8e66c29d58e0a3b7fb36356ae419a54 Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Sun, 22 Sep 2019 23:13:48 +0300
Subject: [PATCH 10/15] Format
---
lib/pleroma/user/info.ex | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index 67abc3ecd..99745f496 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -342,7 +342,9 @@ defp valid_field?(%{"name" => name, "value" => value}) do
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
- is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
+ is_binary(name) &&
+ is_binary(value) &&
+ String.length(name) <= name_limit &&
String.length(value) <= value_limit
end
From cf1960d5961a3a01a6d92c44ab4a6d0ce9570a09 Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Sun, 22 Sep 2019 23:14:18 +0300
Subject: [PATCH 11/15] Better changelog wording
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5a84f5ae..f28299666 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Refreshing poll results for remote polls
-- Admin API: Add ability to force user's password reset
+- Admin API: Add ability to require password reset
### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
From 646bf0160893f01fe14d1d38f24420ac6c962804 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Mon, 23 Sep 2019 21:13:39 +0200
Subject: [PATCH 12/15] Update AdminFE bundle
---
.../{app.34fc670f.css => app.40438ff5.css} | Bin 12809 -> 12809 bytes
priv/static/adminfe/chunk-06db.75709645.css | Bin 0 -> 2044 bytes
priv/static/adminfe/chunk-15fa.bcc01554.css | Bin 0 -> 4748 bytes
priv/static/adminfe/chunk-1a7d.38eb00cf.css | Bin 0 -> 480 bytes
...1.6aaab273.css => chunk-1f27.c0efd1fc.css} | Bin
priv/static/adminfe/chunk-2325.0d22684d.css | Bin 4748 -> 0 bytes
...8.e12401fb.css => chunk-3d1c.2880a519.css} | Bin
priv/static/adminfe/chunk-5913.33f0e7ff.css | Bin 0 -> 3252 bytes
...f.d7a1893c.css => chunk-598f.dc5869e7.css} | Bin
...7.ac97b15a.css => chunk-6292.d1c82a11.css} | Bin
priv/static/adminfe/chunk-7c6b.4a8663a9.css | Bin 0 -> 1737 bytes
priv/static/adminfe/chunk-8b70.9ba0945c.css | Bin 1865 -> 0 bytes
priv/static/adminfe/chunk-e547.e4b6230b.css | Bin 3304 -> 0 bytes
...d8da6.css => chunk-elementUI.f35d8ab1.css} | Bin
...s.4e8c4664.css => chunk-libs.00388c73.css} | Bin
priv/static/adminfe/index.html | 2 +-
.../static/adminfe/static/js/7zzA.e1ae1c94.js | Bin 374 -> 416 bytes
.../adminfe/static/js/7zzA.e1ae1c94.js.map | Bin 0 -> 1913 bytes
.../static/adminfe/static/js/JEtC.f9ba4594.js | Bin 388 -> 430 bytes
.../adminfe/static/js/JEtC.f9ba4594.js.map | Bin 0 -> 1903 bytes
priv/static/adminfe/static/js/app.8e186193.js | Bin 137815 -> 0 bytes
priv/static/adminfe/static/js/app.90c455c5.js | Bin 0 -> 161629 bytes
.../adminfe/static/js/app.90c455c5.js.map | Bin 0 -> 354948 bytes
.../adminfe/static/js/chunk-02a0.db6ec114.js | Bin 266229 -> 0 bytes
.../adminfe/static/js/chunk-0620.c765c190.js | Bin 12982 -> 13030 bytes
.../static/js/chunk-0620.c765c190.js.map | Bin 0 -> 63567 bytes
.../adminfe/static/js/chunk-06db.12facc20.js | Bin 0 -> 5112 bytes
.../static/js/chunk-06db.12facc20.js.map | Bin 0 -> 19586 bytes
.../adminfe/static/js/chunk-15fa.b0633695.js | Bin 0 -> 7919 bytes
.../static/js/chunk-15fa.b0633695.js.map | Bin 0 -> 17438 bytes
.../adminfe/static/js/chunk-16d0.6ce78978.js | Bin 0 -> 1576 bytes
.../static/js/chunk-16d0.6ce78978.js.map | Bin 0 -> 4426 bytes
.../adminfe/static/js/chunk-1a7d.8173d81f.js | Bin 0 -> 16157 bytes
.../static/js/chunk-1a7d.8173d81f.js.map | Bin 0 -> 57112 bytes
...8e1.7f9c377c.js => chunk-1f27.d3c35fbc.js} | Bin 2032 -> 2080 bytes
.../static/js/chunk-1f27.d3c35fbc.js.map | Bin 0 -> 9090 bytes
.../adminfe/static/js/chunk-2325.154a537b.js | Bin 8220 -> 0 bytes
...e18.208cd826.js => chunk-3d1c.20303ef7.js} | Bin 4774 -> 4822 bytes
.../static/js/chunk-3d1c.20303ef7.js.map | Bin 0 -> 18519 bytes
.../adminfe/static/js/chunk-5913.1d21a547.js | Bin 0 -> 27091 bytes
.../static/js/chunk-5913.1d21a547.js.map | Bin 0 -> 88770 bytes
...fbf.616fb309.js => chunk-598f.dd8089ce.js} | Bin 17717 -> 17765 bytes
.../static/js/chunk-598f.dd8089ce.js.map | Bin 0 -> 66937 bytes
.../adminfe/static/js/chunk-5e57.7313703a.js | Bin 217441 -> 0 bytes
.../adminfe/static/js/chunk-6292.0e668979.js | Bin 0 -> 231394 bytes
.../static/js/chunk-6292.0e668979.js.map | Bin 0 -> 689117 bytes
.../adminfe/static/js/chunk-7c6b.c306c730.js | Bin 0 -> 7947 bytes
.../static/js/chunk-7c6b.c306c730.js.map | Bin 0 -> 26432 bytes
.../adminfe/static/js/chunk-7fe2.458f9da5.js | Bin 408401 -> 408449 bytes
.../static/js/chunk-7fe2.458f9da5.js.map | Bin 0 -> 1242154 bytes
.../adminfe/static/js/chunk-8b70.46525646.js | Bin 3190 -> 0 bytes
.../adminfe/static/js/chunk-df62.6c5105a6.js | Bin 0 -> 265970 bytes
.../static/js/chunk-df62.6c5105a6.js.map | Bin 0 -> 796489 bytes
.../adminfe/static/js/chunk-e547.d57d1b91.js | Bin 23125 -> 0 bytes
...911151b.js => chunk-elementUI.708d6b68.js} | Bin 638883 -> 638936 bytes
.../static/js/chunk-elementUI.708d6b68.js.map | Bin 0 -> 2312798 bytes
.../adminfe/static/js/chunk-libs.14514767.js | Bin 0 -> 275816 bytes
.../static/js/chunk-libs.14514767.js.map | Bin 0 -> 1641569 bytes
.../adminfe/static/js/chunk-libs.fb0b7f4a.js | Bin 204635 -> 0 bytes
.../static/adminfe/static/js/oAJy.840fb1c2.js | Bin 0 -> 28900 bytes
.../adminfe/static/js/oAJy.840fb1c2.js.map | Bin 0 -> 135594 bytes
.../adminfe/static/js/runtime.e85850af.js | Bin 0 -> 3859 bytes
.../adminfe/static/js/runtime.e85850af.js.map | Bin 0 -> 16537 bytes
.../adminfe/static/js/runtime.f40c8ec4.js | Bin 3608 -> 0 bytes
64 files changed, 1 insertion(+), 1 deletion(-)
rename priv/static/adminfe/{app.34fc670f.css => app.40438ff5.css} (92%)
create mode 100644 priv/static/adminfe/chunk-06db.75709645.css
create mode 100644 priv/static/adminfe/chunk-15fa.bcc01554.css
create mode 100644 priv/static/adminfe/chunk-1a7d.38eb00cf.css
rename priv/static/adminfe/{chunk-18e1.6aaab273.css => chunk-1f27.c0efd1fc.css} (100%)
delete mode 100644 priv/static/adminfe/chunk-2325.0d22684d.css
rename priv/static/adminfe/{chunk-0e18.e12401fb.css => chunk-3d1c.2880a519.css} (100%)
create mode 100644 priv/static/adminfe/chunk-5913.33f0e7ff.css
rename priv/static/adminfe/{chunk-1fbf.d7a1893c.css => chunk-598f.dc5869e7.css} (100%)
rename priv/static/adminfe/{chunk-5e57.ac97b15a.css => chunk-6292.d1c82a11.css} (100%)
create mode 100644 priv/static/adminfe/chunk-7c6b.4a8663a9.css
delete mode 100644 priv/static/adminfe/chunk-8b70.9ba0945c.css
delete mode 100644 priv/static/adminfe/chunk-e547.e4b6230b.css
rename priv/static/adminfe/{chunk-elementUI.e5cd8da6.css => chunk-elementUI.f35d8ab1.css} (100%)
rename priv/static/adminfe/{chunk-libs.4e8c4664.css => chunk-libs.00388c73.css} (100%)
create mode 100644 priv/static/adminfe/static/js/7zzA.e1ae1c94.js.map
create mode 100644 priv/static/adminfe/static/js/JEtC.f9ba4594.js.map
delete mode 100644 priv/static/adminfe/static/js/app.8e186193.js
create mode 100644 priv/static/adminfe/static/js/app.90c455c5.js
create mode 100644 priv/static/adminfe/static/js/app.90c455c5.js.map
delete mode 100644 priv/static/adminfe/static/js/chunk-02a0.db6ec114.js
create mode 100644 priv/static/adminfe/static/js/chunk-0620.c765c190.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-06db.12facc20.js
create mode 100644 priv/static/adminfe/static/js/chunk-06db.12facc20.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-15fa.b0633695.js
create mode 100644 priv/static/adminfe/static/js/chunk-15fa.b0633695.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-16d0.6ce78978.js
create mode 100644 priv/static/adminfe/static/js/chunk-16d0.6ce78978.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-1a7d.8173d81f.js
create mode 100644 priv/static/adminfe/static/js/chunk-1a7d.8173d81f.js.map
rename priv/static/adminfe/static/js/{chunk-18e1.7f9c377c.js => chunk-1f27.d3c35fbc.js} (83%)
create mode 100644 priv/static/adminfe/static/js/chunk-1f27.d3c35fbc.js.map
delete mode 100644 priv/static/adminfe/static/js/chunk-2325.154a537b.js
rename priv/static/adminfe/static/js/{chunk-0e18.208cd826.js => chunk-3d1c.20303ef7.js} (96%)
create mode 100644 priv/static/adminfe/static/js/chunk-3d1c.20303ef7.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-5913.1d21a547.js
create mode 100644 priv/static/adminfe/static/js/chunk-5913.1d21a547.js.map
rename priv/static/adminfe/static/js/{chunk-1fbf.616fb309.js => chunk-598f.dd8089ce.js} (99%)
create mode 100644 priv/static/adminfe/static/js/chunk-598f.dd8089ce.js.map
delete mode 100644 priv/static/adminfe/static/js/chunk-5e57.7313703a.js
create mode 100644 priv/static/adminfe/static/js/chunk-6292.0e668979.js
create mode 100644 priv/static/adminfe/static/js/chunk-6292.0e668979.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-7c6b.c306c730.js
create mode 100644 priv/static/adminfe/static/js/chunk-7c6b.c306c730.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-7fe2.458f9da5.js.map
delete mode 100644 priv/static/adminfe/static/js/chunk-8b70.46525646.js
create mode 100644 priv/static/adminfe/static/js/chunk-df62.6c5105a6.js
create mode 100644 priv/static/adminfe/static/js/chunk-df62.6c5105a6.js.map
delete mode 100644 priv/static/adminfe/static/js/chunk-e547.d57d1b91.js
rename priv/static/adminfe/static/js/{chunk-elementUI.1911151b.js => chunk-elementUI.708d6b68.js} (99%)
create mode 100644 priv/static/adminfe/static/js/chunk-elementUI.708d6b68.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-libs.14514767.js
create mode 100644 priv/static/adminfe/static/js/chunk-libs.14514767.js.map
delete mode 100644 priv/static/adminfe/static/js/chunk-libs.fb0b7f4a.js
create mode 100644 priv/static/adminfe/static/js/oAJy.840fb1c2.js
create mode 100644 priv/static/adminfe/static/js/oAJy.840fb1c2.js.map
create mode 100644 priv/static/adminfe/static/js/runtime.e85850af.js
create mode 100644 priv/static/adminfe/static/js/runtime.e85850af.js.map
delete mode 100644 priv/static/adminfe/static/js/runtime.f40c8ec4.js
diff --git a/priv/static/adminfe/app.34fc670f.css b/priv/static/adminfe/app.40438ff5.css
similarity index 92%
rename from priv/static/adminfe/app.34fc670f.css
rename to priv/static/adminfe/app.40438ff5.css
index 136aa8bb169c25e0b8d589fba77f046107febb84..b82fcc39e855a2a8b6907b9c8ffd69ea0651eab3 100644
GIT binary patch
delta 199
zcmeB7=}g(MMv)`Q)YvrH)I4$W7DXon=bz$C1ZTdICxY`qNe{tM&=j96pvXVjM_C>r
zSgh=Z;2czTL~s-|B_=DWbaBD#jIExmp%gNiOVt3OX1r
zSgh=Z;2czTL~s-|B_=DWbaBD#jIExmp%gNiOVt3OX1Z*pI{cOhF@89jEnY0dw!Z^b*A;kL^=Nc|urPz&1?N5fn+4LGkn8+C=c~CgTR^cXm
z++hC4%{E%c6M1c(v`;r-_oP*L5OAW?;pkrr$ve{}HGt7gS7M(hE}2C)w#*}By>QZ$
zXzs$G&Ct9XNM#{@j#1_4F!ltFlRiF4
z4A!-z|EZu48entkS>%3xn2f-!5>00YV%uR5JuTY7o%3;a%>p)q3V68fWdA*Fh8U`OJi!GdLTiGFY$aX;rBs;Z5wsYZ7jnMX-!Ady^DOA
zgM%l~j)#t2ibGM@t~oBRRQ>9L-5r!m9%}pO*yP33$X__Jjo5U($WK7d6%Dc}t!2t|OI&D(h
zn~Iv_^YeY@k9gZM&cQ~?V(L#p%yWrZPQ_1xB?iv`lKCni0sgfITe^B;5=epbGT|aa
z<|)OJ-h!EbbEp4&jxM||c*!K=Il`-wrD&
zj+xvcKWg~LIm;C8JO??>D2h{FkuOR$=w(cFwVCQ}oH!`g3so)A?O!g+Z{Fd~Rypxj
zRhpI?HqgZFT4nc3XY&g?o2JgziGOL+Yz!`!oEN3Lhb3zB1||DVQKQSJ!
zV|5Volne9;P8UJqM;R7tmV*_Sl4r=2K$A(QwQFEiIrp+Z^G+UbC??t=$LqThRL=&v
zt)sg46=s(PVwo~RzZaLL;h30x?@73mo<*_6!$o_JZk&y&wpB)IrVd
zfSYC$@dh?XedfylGpbphaEInrc+d>SR&oY<4%W%J7IfDO&vbIv-Fg`wTE6s|EB}AP
z8_!}vWhK0?QfM5m&Gx<=YXV{F8gC{@oZI5Rihv^b_w
z*s0{Ya@3hC|JpPf*eypojULU7y@6jqe%$IIJow{C^U7w^K`oGHRrw=&)On)>b_Vpj
G@%axmH?Iu<
literal 0
HcmV?d00001
diff --git a/priv/static/adminfe/chunk-1a7d.38eb00cf.css b/priv/static/adminfe/chunk-1a7d.38eb00cf.css
new file mode 100644
index 0000000000000000000000000000000000000000..cbf59cfb567a8c2a9d315533d7146d905da471a6
GIT binary patch
literal 480
zcmZvZOKyWO5QeYPO?MqD5JjrUX&mE%TiY|Thd@e=Af>wh^!hs}tYtxQw%bX9(NXX(5S-bcLHg!$Y%SoQuAc$T6y2UT6!j{1zHL{uV4P*FVD@jCjbBd
literal 0
HcmV?d00001
diff --git a/priv/static/adminfe/chunk-18e1.6aaab273.css b/priv/static/adminfe/chunk-1f27.c0efd1fc.css
similarity index 100%
rename from priv/static/adminfe/chunk-18e1.6aaab273.css
rename to priv/static/adminfe/chunk-1f27.c0efd1fc.css
diff --git a/priv/static/adminfe/chunk-2325.0d22684d.css b/priv/static/adminfe/chunk-2325.0d22684d.css
deleted file mode 100644
index bdb7387006581053fe797fbda2db974e5eb7f4c0..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 4748
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
diff --git a/priv/static/adminfe/chunk-e547.e4b6230b.css b/priv/static/adminfe/chunk-e547.e4b6230b.css
deleted file mode 100644
index f740543a0ae4961dea98604533843ffc08f9f480..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 3304
zcmds3+lt#T5d9THS?EG#Y-cyg$}jzovMjPJJ8EpnNOCR>`S*@wJC37dd)qCg1Om}W
zlewI8WMhz-BtMV^7F$8VUBF^YMn=PLYTb`R0o;^m*?)bT8IVZu#nhTV(4W|rs
z=EZ#3?Ct|1axjwJ)EGy}16V&vQSAdw6=Pz&IlKq=_lNLjLW!g3F0F|wA&5*GBWwH!
zu^mG~4VOZrtc61{Wb*WoB#SI&&j-VH29b)F0#~ds=~-rJL-L%962~^p3r~njVF(vY
z`95>5n~Ll)+OllT4Ft4HPiM78Ma6dKXZCk`b{REF)W4XX_Er}L69#K|
z!%)V|7AkoRS%q>Xn_OH-yT(!|
z5L06zC#UWQ;)c~4cNdQ?M+*$U9z7&bSCGU3LrAoen=3%T?)X4uB^CZKkLJmIo@UV7
zQpr~2P)27ciXx@0(r}5iM!r9xfNjJu)M};>P`8q0Pv~WIcMN+gp%j7y1(2$Cvw8EjO#E{+
z_Ns=XAO=MXMlISf1Y8^-I_BS_>LGe!0IULJ3Z%tlw}_%n#=570RSCy^x?*(wLr
z3^6Srpa?!TvWe!PiVNT2z=sa&jpUA&lXyoxYN}TIA^Cb_QLINr3Rdy0S2#%~@m{_R
z8v^5l*m-f=h3!F6{M
zoRVsyRXJC%{{a=SfB|5Q!x}PLpwewEQoFvu6d3OJryWzRY}qU!-DBt_IUO~AD?6
z-R{ccLA;1ka*NJ4oly&&`Z)l^!2K&uUMJ~|e|}$HE+NuMEB;=at`rEWe09q7Dh9nh
zt+rf%>cb9NNxHt+#bMR=Cy@t-?!W9*vP&k)i73ad>eSRvK8`LP>XuTbuS}0&d$XH9
Iwu*544!JC~8UO$Q
diff --git a/priv/static/adminfe/chunk-elementUI.e5cd8da6.css b/priv/static/adminfe/chunk-elementUI.f35d8ab1.css
similarity index 100%
rename from priv/static/adminfe/chunk-elementUI.e5cd8da6.css
rename to priv/static/adminfe/chunk-elementUI.f35d8ab1.css
diff --git a/priv/static/adminfe/chunk-libs.4e8c4664.css b/priv/static/adminfe/chunk-libs.00388c73.css
similarity index 100%
rename from priv/static/adminfe/chunk-libs.4e8c4664.css
rename to priv/static/adminfe/chunk-libs.00388c73.css
diff --git a/priv/static/adminfe/index.html b/priv/static/adminfe/index.html
index c31247c03..ce53d8318 100644
--- a/priv/static/adminfe/index.html
+++ b/priv/static/adminfe/index.html
@@ -1 +1 @@
-Admin FE
\ No newline at end of file
+Admin FE
\ No newline at end of file
diff --git a/priv/static/adminfe/static/js/7zzA.e1ae1c94.js b/priv/static/adminfe/static/js/7zzA.e1ae1c94.js
index 4387b832165fe39a626396494ee4962069f51ebf..526e228f59b19d4984b7336dd89c10f5b5caa51c 100644
GIT binary patch
delta 50
zcmeyyw19a-8KV}LzP_?Taeir0a;k4)K|y9-dT5Z3t$9_Iqh6|EVya=XrHNivv0iRs
F0RX8a5R(7^
delta 7
OcmZ3${EcZt86yA;Oaj9I
diff --git a/priv/static/adminfe/static/js/7zzA.e1ae1c94.js.map b/priv/static/adminfe/static/js/7zzA.e1ae1c94.js.map
new file mode 100644
index 0000000000000000000000000000000000000000..840e8a26be5fde3f8c660ba8cc8b85a38d9275c9
GIT binary patch
literal 1913
zcmdT_ZBN@U5dJGBLcGnyW1BS5!H`J{E2y9`O=w!C$W7fgU212JQz)qaeRp<9tL{bY
zbC<_pCQX@X6|`Q{pwyK~Wf`=-26wVwi1d4lF;<$C%`$mcG9w3>k*Q^w
z8p!)_R>^>_dWL`XJ$ZE~`~MOUe4~L9QxC`?NS+Wt>BP3K>EHM
zY9k{fg++LY9cSg{rqPvq#W&D4?}ad8ikcslG;{bbgdH~y1|%bkT%?%FNH{jC&>_{+
zLMy2($k+V|bY)d}rOj02*@JU0t*}{o0e
zr=ln_HG(?{=I1=(v>UZw_BcIncMf_H6}-*qtE@4d)$*jrfJja7jP944<48bJ8w6xN~en)ei5vloUN8>fm7sPQRaa+71mL
zokmUBCKNCZ`k=l!`|OOD5POSx;e1%%KYG0DkaH;GkzjKAg+~$(3LK#A*?%Bi5WB+LHmBjU`h(
zAVZ_4Bo6jB!jSo&(gT@*smf)^`l2uM{ag!}1(s+1dKpa0lT@T**>)R3oB1EYVRC;P
ze6lp`*W
z+wgR{=XI)KdefEX#>FaDY>%j?wFZMwuRpTK8cdDZJmGpC2~4GIHkGVNx?G_}sUXK}P##tc-^f*IMUvLy?!#Nvg{8Uj@t-D;T#}HTA-pkD*`IP(BWtFCd~@
Wlmc!5
diff --git a/priv/static/adminfe/static/js/JEtC.f9ba4594.js.map b/priv/static/adminfe/static/js/JEtC.f9ba4594.js.map
new file mode 100644
index 0000000000000000000000000000000000000000..633bbc5d634ce6a65e3922e8b7e6a15740db351a
GIT binary patch
literal 1903
zcmdT_TTk0C6#gs5lW1Wk(KPlz2SXt(r7$k*H1V)Zk(;<}y3~&Bq`(mWedpL|S}xPZ
z}I0D|iTjV8|$j8cV=LW2M&9^<>t00Iz_Tp62I
z%?l-kEJ1fUngJweTq-Y=p7T8WWm9M&Q!AJf>Lo+_dMpj5EwBNU;&`0pm^hB>D~QEy
zp>%2D7%`ahqR8X~?ub_XAPm?b>}D4M>vwyzi;$fKLBIs)L-r*&4%l_j2NzI=tY@E>
z_W5;9J_ng$suPCBfqkAZbQ#!upRFdenlmf2Z(zKFj93SQE}1#%;R_U*966nz++MBPzR+yGyZ9>
z488H*i0^*b+x2FMWliT$$r3LJSiSG)@hOrHRFaA@ukw=IN0Ja^N{6%p;PF!4wA{?SX@QL8
z9woCJY}*14k%ZsA-<%JQp8+#SqB|R&=ewU&sffW;<-+(QJ`(v(t~l`1
z&$E#aqakhq#KVLqQ_;1fuJqj>!eRJc3_e^Lc4~%ic+b`cdTrwD9=PT=adr=8W;3E6
z$&+h>h8x}X|9UF8ng!Rytpi4$Y=cPF_vX-89`aSVHS}&UdS!SdBk6Awxm;gB_yCMn6bBA_=d
z_5oTHC<+{)r$BGoi(VAy)j`qE)4hRyi~jy&&gYG!Y-O#)8>eSwS$sI3`8vj!<9$wU
zT#Y7!=~ekEJU;LB-+VitPR_U1l>PeatJj6{`Ne#=@#)?_2!+N^$?FFcr(CmX#&xpz~{-|szb
ze4_QFJlj|ll1}Phhd<>D#o3nYrZ`g1&w5W66i%&hR4C53CgD|XR_<+Fj|ScRs6U;Q
z&Ic#?BJ)bVmM-SK(=cDW9EDe3Pv3R(m0TqkRH}9RlP}V05l$A}fB0(t{_J=<&iRU4
zOBHkP&c>7ZR(`lxoOd@judV=QtvsEbZdNOm%I5s?G@ly{wvuW5>8tbJVwj_7uifmF
zE3I0t+G>?+mDWzP(JBW)aM)~=YxVlL8U*D|vzl|CwGU^dYOP(a)+lL~tF5S7sorXq
z>z#I~TxrxV>p`>JX|#r|pk8j(E=xhHU2f8Ktx|5+1Nx}d8s&DanX5G`%3%R&G{!TW*%BbQ$PrSgN%~;)6(Q>xel-E
zrB=C4pERge+NFRul~zvg8_c>{spjg;i?^Lx%O0!MawQ0Im68C>XpI_U0xcS=roC4G
zEv;JX0s%mv102*%G)VAaPf%k*^`P9YG#r#G^opaXnM)mFJxZ{=vp3hS(bVeQMX)NGb3wdN&MU9LCyS_Sb?SEW^M1g&9E1!>Jz
zpDAe6O0FhAFa}e#eyp3S!%kqM@v_QjhOtTu8q##yZdMIdZ#Fgi8q*e;u$Q1$y3{HI
zEwUO^K^LG9rPb)OUZEU7vr_k^RtIBkQ4J7m31{@rpp7aRs)J!>*$z4%_L}v2xsz*G
zz-C}%EU1clfKN>@%hgzK&@NT$Hmog){(w6)0|?uoTkqN+gKr?CUIA$}t*Qz#pp&59
zWL0!r0r6j+CTTC9&=Dy>VSreP3t
z*twUs#XG!nUjtEVqwQ%d1_dmz+__|rw5!x*5Mw|hid+>+4T3%!9i%dWYLksy2Oyvb
z7*y&dYOwke1HgemTZ4^Rx7q->(J-z=)0*CkVX*ySM08PYTn25{)ou?Ptf1QIYxj$!
zMCtTcuES3ZfDP+0FrsY$kq$h{@UE>0L`4{ygJ#t6JgW&sLO!AcFxUi$w9q$tY=Sz$
z(y>w+=vhS8YWOb(aB(F16dPhP+QP7HrczDT!@vNSVOx&pm8x(ru?5JVX6&VQ(EMG>GxoD$hqvR-PlqhMxhWcD3ob5^YTo>CIqO(Z3>g9_rT-N{u2=W)t37Xq&u~vu`w#6P6!-Cld=nzfm
zHTKw76VwZOgQS=T!lBbBA+V}db`jmf+^7!ic92hWPhE($3M=*BG#LEC)}(1LwOUT|
zwB9-L2Y{fFsSwrIDSK3f1@jI4lf7vp
zi57GSwq2_x34@YkB2wF}-Vn%WE!_z20;94oV&k=XRcuuZ3+&PhvQf+vVdCoeFHtwH
zR7-AF8vcu~0#?+GVU%vfV(2{@*S?5Dm)opJJhWbya?z>6-#{GNqxd@Nkb7=`2oOaL
zi2*>{mZH;PDSGc8)m#*6O(gzBKI#h5MAbR11B9gYW&OHG>5}kaD6UzqwW@%WjUn|0
zveOQzioR+vRQCniX|=`pTq$jN7k#lRypxV8G_nZMAXw2Rkfs5e8k$?xl!sQwks1n?
z>6LDOv=NX-mT0XtQyu6+6#~D3P{xpL9m!OPhZU!@5iA8qyKcJvF+6m5mei6F~OP$XZV2>H54jnq*+_QT4htA
z@EZ&1pi!~~TI@S4ixw&@yt^%))Raz#4&Yy*bE1yXs$FgEnmNfpZ=swTI=cE2kF5nT
zX6x|)K>)YNH8944)uuCvpT0qxd-7TNAJ->SZ?67@l=)V9+qVd%EZl1;c-a?32SmRhx0a?32W5dj^j
zTg%j|dZ$^kWvUhYb+^LMwQ{S{dTSG5udz(z2g@{7ybU;6B{OAOe2Ck!O7+;TOLb+D
zC6^qn+Osvi(tv5@+S~Xc&9f2S%7yc>5xXtYOfK*JK1%xcHiU5a0blzIe
zqMfEw&HbB3b&)ixL0w6QCc^w+2#k8;poS(3vuq;Xs*7Zb^>*9g5ut;!xo~Td4Jg`h
z^&H)FBp$^)S+|J5twmT0&G6-Ly)qrP&A$;BoLEkr1?MJ&QtEtY2k`RRR|1
z(?H;wdFZ$vvA=CGel!nNKNAmdKTBH;1{U+j*kI$a3q2C_MXQA7k+7BKK@+4)!xkYo
zAvz?J`;aL`k$+tSD1a}jQbU460IITZ9kD3u*T(j*V@j#Nm3dc_4D@7(V69ZD2X$%m
z45@xxk#=0IX1z2zK(ba}Sc!zTTKGb1;mcf?*2MSs;qLss+73@Y8-IT{T9i&M#^ZT^
z7A~>*?5p+M-s{e*p0mGS3lFp7!{cK;~d3$a(Q
zti(CxC#oRd+20jhB9L{XZXC1{R~U!Y0^xT8?M0o?UH@rHx{J(po--w&sK~$#gYUn+3cc6$Ew#vy@hW
zBBV)QA~BlPKC436Ew|)QfyJ8kZ12roRvWYUN>$cHrA1vfc%?!~TU^B)d-@#9Aw)@|
z-hn$O*R?eUByvAZ-B7^1Yt5kTZ<51
z(gWxiYG-^nVTP{2nQLW;3GgDiu76yXGr5no7DQ&O?_Ox)E
zU1)}ePARgL`dHEclc0)Mfu%`1V;)WZR0B;!##c2MYqp?|0kwAIkN!B+u_l>stXy5R
z5hLxU>@i%Rz`7!zAW|CU$$H1w^c2>c09gQLoB%g&1yjl%f_ebeU37a3!Z@tZXC*K^#bdA^p4L-7Ln+6pvM-akCJvKO+1&ALI07C}I
z{-&)|bU9Q3!pFp)nCv5mCu7!D%ltMnyFI8nY1(sRts_PNQjiMTFw^
zilF5;3uNUt6chuNvGk#=Dr5|XP#(4UU~tf#_owZdr%UP!G-SJ(2>=3zW>GXYHTD_d
z1n7$y;$aiAB|FV~E9Zff~5SC}&ax
zDyWF>T~*A|WD^^V#-i6IoLD>Fa76>-fepyLss5WV2YUo#EgW_@OF&vsGo)eDgEYBQ
z`OjD^WW5X3R;99IX*(W`zKM4|FeWK>B^E2Jxe3YHhf~kZNW8!9dB3<1+BORnr)x^t
z73s2cf(<59AV%hXSM^j=NMgH)!K;!Z&Jem)%_jsJ$3I
z`CKZ%B%Ck8LE6A`C7)g3`-!CFJOoi?x5~``a-_imSPB#~LU~09yJv8QIv|@H(*&GW
z*#7R>y~l+^a~%3Khmc?$Sl+5?&-*rv1U$^k4M^w*Hv!6C1xFoDzfL#ZZ;NT3(I_*0s5Pk^2uE`Deos;WL|qp(I#8*`Oke7O1MSu_dwAVo%Gt5Cgy_%t|EI7nosqYvuwGJ_}vC4mgQP4Q}^PfK#M_3HQ
zvy30!3O+kOaMMV!12mK}UT0$6?fNc&tf8b3GO2Cj!e}W7hDd-Vu#4n3#|ctKACz{$
z-hwXS67UVH#2!POSz~k+e3a655V-is6{tkpAjVX-xG3HNRhA||Aaw^h&}bn2(w%BU
zGP3KGH|^Rk4vtohfSqQC9ZeM5&yG+~$8=BPYb~^irTO`4)|G3%i!kwP-X`{`W>Q`(
zS}5;)<++KV9~Nv5v|Q0BagBVQ1a7b!sUk{!UydQ+Nc4Lci|h?ia5O~rEj
z6GcG015ul^P7_-5U@f
zmzC9Gn>UG`656P-je%dIqhU>d!&xLL4j==3$LJQUiX{`7)w&ca$LixPQ{8nWfxX{E
zDF=7-)K-L4_88+;P5;)jAOQ^sM(8~NNo-0+MZ;cIf5TR6zPtSXb8W?w>Fg{mn)Us&
zZ$z6!Y9F?xgIml)@hcI(*glXaJR%DZph_?9G<)D0Macb?8b?f*eIbhuU%i6cFxBWI
zdS6ix0nJe67bJ)lk^jjaHvRMbaT>BX^vCR>B`Vx6&I;Ri$=AjDansb`9HT6)Z@&
z3xX(>&+d9j1PMR&|r7^77$WY0uZh0OxKoWF&n0<=ovdwC}SId13Gaghx`i|
zyEI{L1miG;fvb!UY%#%0NN52mbbNL-pe0$>R4kVf_OaN_2wk3hS
zdeHOEZU3RVLYu|^9q=_VJlXEU-txX>rY8VnhX-<(bg-gjoG9F*3=oVlxt%phO%=Tj
z|DtMWEd<+Oz?8L-?TWf%!=q|oc}PJQQah-NA}FySvWnuris(T;7K5t8+3;Sr+aLCX
z15%Sfa-|A8tuPrfJ}g@W+eO%yZ6&bdp1@A&hza3VsO`vtsFAnAl5lrQ>uU;{b(t!b
z_5xq+VU;%Ov~V8|o@ysy;x1-@1Mr&w;KLJ~#fOp$s_qL=Au}
z)mWIAlVOvlz5`=$L~Q^T-ciX^xT8G}@zn(cz%?Iv7CH?#Ezv1!Md(MfvIe)#O2)Sz
zIGUAYuPl?>XJw4@_F1vZ3_N18aHbZ(V3rEr+bRTRZGSc#YZ3GTkCXt92z;HA{D9~0
z#FIHR*`rdWNKYKQEhXTJ2SE=E9Ka6iY${}p|8TSmUA9FcRkWoBDN-$kUi@dYgDUJ>
zR3`HCpkJ1!T`9E+*+;@3bZG%ZmZ~k&9K%}`>Tp&QbQjjRT?7^EcKlR68!JHcAm8I7lRWL#ICwhm$E3H7R
z4td}^?GOPY*y6}fjM%?5DbbeS5UxyR>}Q)J?1*Kk1&cB-uRO%CA}ZnV6}2*FV5}nj
zMG>|n1vZW^Lx=)ksVfzD7cx)yui@xsED<42;=b65Ni6VBH?RWeSJc`G+C5N**FhN6
zha&?wMG52JHz=i+YQ2({P5xJ>{_UsowSTt^u0d};Jf8Mu8E;Ztc)Q+Osr+7fT=bOW
zam6F2V@IX|C~Anqgp?p77X>_U0J2*|lTEzAA|=rTmZAPatL#WIT-@V&sgRI8L&B)A
zVv#gs^GL!fZOgq?2ht~6#;ltr=Ey#=LBzut#5gxuVw!>Wt2L#fducPU4y47PU|PR=
z8I2(}XT4|!S_nHvul)qoHd=!kvo`uM$r7*g!x{Tgk_x>G5BD8_BKrN!K=G4zP)z|v
zZvO}<(jspKO7v1o@=#p{N(7)ZP+&buocgUm*=sQlRswl>o%UEMw-AT2dWimz#m6$(
z{p8ny%$fk-P!JM9iC*0_{m-)N(U-Jf^Y0tS^ARN`MNSz_`3t*|eo$AYm_-o?C_$al
zA25K$GgK3$5(Kst^U`EkshbS1P(Gw(p>#;wWnxHYg7&5v%ij-6K}n@Im^ql{S>8?s
zMCBq-ehN%j`YoAdQb&~vrkq7MwRk(>Z&QIfrb^>I6rIY;fI^2c4(C$dha3rrE{If#
z>C9FtiW<`gD<`cWy*nr_I+1>0=zq@}|LY6+sR=P7G
zyWQ<4sI~@D^`PBteq+D8{W_K224LCVeAH_mDB2XOEU&SRu$HPxCI;aba?4>Y_-BcW
zKmqn3*H;3RgylJ)n1OGWv)F_28?DT>lIKeD7lpH)X#=j`)x3fZ2G2+!)0;Lao=7hG{=xoC}$Ys!$!i~_7dl?N5
zg9*ylCt2UN5}BmpmQ0DIiUHY_%<*pw60bB?ug1q5qlyh8Vq*{{uNQDOxrQ)33ARdO
zmrgGB%L?GO>^mDj&UH+=H;$M05(Gbj0eH_dQ9oSvVL
zCZ}m%W>j{&Uib&)_eTS#)k5t5j_8ckh?;qlExTLh9HIwafkcvQ!?=^dVT8nAr?Jar
z2+_C5hsGdrjwP%-RD5(X2#zyk{6**?bqQ4{{Tt$;pNoL@)#@KY7Dc$DmX1K}0MJbTNbmyLt<_z
z#J${7_>q`GL&5Ww`2cyZzU_FI`-uC;yg>s&urMbUufHaeNWa(Aum){{bY&C7glB`Z
zLG|8Vc!`h?LBXYylmsFkD|a;4;JlBWSdq8%Vst*vZu!1Dq72A1aWkW)pv
zw{~|^DJ$|UyT5J>=+5@Hr6pQiO*;c>6;a4Xi|jILMEyk)&=piE@!4eiv0{-#wb^SB
z$gCo3qE81rsMlMO?Wk{!9cuqC_Iw
zxiBw;y`pO@U2B0%F}_20kI2dO8z5yP1Zl}8F+QanIqW(HmZgrjL5ou*O_IDK
zGPd}_STJeqD3I0!NBE)$$cW+8s8liZR8X%0Z@?L9F)N*Ea*j_nA8G~11r+Zxlb)=P
z9f-@~4pqgD8NX(sI&4#U0ui?*us3`
znW%CQrqE2*neA(WMG^xTgmeJuv7lyW+d$5?jzVW@QAa*ihA;^%V3=^rr~)CZt*c`L
zq9AP(ZAelL8;~yG()1*gAq<`IlnYLgM~Kb+wb`nQ&V`{hDnN#4p(~d;)519@&Psco
zY*XlpI9bhRivWS?NYbJ(>K*++guK-zhP&|A%xMI>jw#VStF;E=V&VWYb!$WXs_1-o
z*V&%d#?L;Ja&~-?(aZSy*;O)4Ar)a(FcTtDmW(V`z^YTh(g*;0Q|I{&>}VnuWzlhe
zQG`?tovfE2fZU{V>5-$B>P{&Oa8fu7SRxz1aIH*KB|6Ymy)j0~)|p@4kZq>;kMCF#
zfaCKFUPaq?&$N&Nvvz9E7s4Lz7;U$OM3{C2Ui=(=dCKk+b0BMe*Xk-)3(04Sf;u5CKRGNooKh<~CN9PjMkw_|M
zK?}B_q%ft6DySenXYx%vlRn{gXd*6Rp>;QPFo+pQi-7y}@ItiM=l&y_Hv*A)+BFM+
z3VV>*AVwKZec-B<`^g-wja_MOa4y}!07%*E!m2o;u|y}?kp!&Lgfs~Pv~F9jke&f0
z@<2GMi+>BmbZ&sI8S}1KOj;%~NYfkwfT)F71V^!S@oZ2H+b|j>)msQMhheKjVl&Y!
zImuvG1l@p~Ihu%Bi&L^55vMuJAaKpHx`SeXMM|H@1e+STim68tn?_@wJ~@<$RKo9L
zkHD)YrV{IleQmux+7E2@XS6xXQBZu@jbX(~V_g<&g*#)L>ejQA&
znE?#;GHW=U_BJ7I|oY!l$ap$XX(5Y`Yg
zt=1tprb0G@71~z**s@qRi4E)u^{?rs2XH{qU!w^zd|MYKF4nA>J5GQC7cnWfY$<@2
z2Vv83b8K9~ZaS?9mQ2pDXk)J%+A}vB&ha4adGR9y8mY*38i9OIG)2320(xpSq!88X
zvZ>@9ruVvZI9LMai~KE2kP1j*Sc1=ap&CItqTdw+vH3mPy3z#{vpmR0xQ3pXkaD5b
z4VOnqLt}&Bnt&FqV^g)|{$LQxA!v5)F8K0KJNFMhSANxq3x8&lo~|t1tTHSDrQGefl0tBpaSh2l1N~Z&PP`l(Zg*ql64Fk+ez$SJCS@j{_4;He8@O3
zr%})w(V#D7TK1tGX=yIzfWnH@fjLl_XW?7c4LgHTm1jv73_c3YKw2Tj2^2uW(-_$*
zmf~&lrm}}%Gkl13o012S(UwbS;7710CxbPP22VV&At0E+I@30j}QE&5Oe`jfdRQ6!AWBGaOsJqE)8
zLl)?%n^&M=YQEj2fEbj>Y2q-EvZCY?8v#Lvxx#cnlmBq5!)8MRCACz>8#9+|!F#Lp
zE=9VLk2?Hn;>-{QA2G)bd{ZFNTO$`qN%Dl?%*>O^4@{lq4bzuMXtjIr23>@~A2+fr
zF|p37EM#SOB2nB&^uA+>)!<9z(O*kuDm9Wj>v$p3o3{qM*sK4s-){Knvf??yvA6!
z7QU1s_y28QOQXb(&HCNh>C?xL$DhfLc>6ZZLEpap>}-<>q>Ts@e>%vG2Bh;k3T|?n
zk~}a{&1ItNCG>0OkwGz2tvq-;_ai`0s*mLreNZCf4hJChL
zIvpjex`lUMeo%;&jpAx3J}N53TLpPBBDQB0X$(7s>Xe6qDXZYE+!Zz(btfwxWeKHQ
z(~?4|Y4E^BE9^e;bm9`IqUxFLBqsv=1PzVkZk6#8$C#BdZ*w8&Jhj;!9Be{Yc4hQv
zFP9gaqqNv1#JbCg=bF5BT8)ZoTF1JYi2(UQ@m^={~aU
zRBBd
zE+n4dG?HP~5KbzUh-%RI
z5|;R@Lr(%BGa(&;bcmd7mQ
z>!k_WjSUTyIIK^Rv~|70ohUdl4GB#aU2k53l(r;1U-9{0mhB6jDP;#J&`Knv-EM*u
zb*WLk1fuNEfbpxO0%D{&8aGqfu#JC>mUb$FVKGS*h5@R;kB-$TQ#bM7l&iyVBOfT!
zHy;OQ6JZGkHX`z}GBkV0%RtHcc%lg0utX6R=Ac21G7!U-(A2c2%>1|a6mg58#^pdX
zoyq^!Fad_bX$
zub#A1_yqd)fLk3j)H%%Bs7t#-P%iQt$*KWZ`iok}=Y
zpgN6)aw*N%NH3*JyqwA6?Om}Sj9ZrW^@U>cyK9Jj*x2WQ6VUF3g*uI
ziF@P7*Nkl2YY%Y9)Ht{t(d^$AJ3(P$scJ@*HjefL_aX_Z1a
z=21S5_6h=r1Mg_oAPu^N=50T+B-Shz#a58m;8Q!rz#w~~#@^t5Qh>s1)M~IId4DxMa7P+KpNLeDVRX{(1Pd+MfWrD<U))RF!Pe5h}v
z!<~o_xTF?GxQz&C&vG=pdeTG$MzK-G1v)s-$x+~e)b469ME`5lxHE#yDYT2~VAtR+
z01s>siC}BU2_c4*`f11e-Dzy$OcSCt0V6qr^(YK7w5p%b=^cF>?Nj~gXo#hf)|IM8B#VQpgMkn1rm+h*}%GYG7@+U2OdcBy~f+ylEbX2obqFlD|AnxzN(s3MPSXb^4hWC0n06rTz9KB3rKmt`O39h%-28z9NKZuIu~>lvrYlJphwkrN(zDPsh?-^$Pn
zaMIHNxL6pHwX;1TUeToOaJg8d(WuZgUL8AA3sclAc0)NpQs2rhb6cHhnYh%|s3J#2
z`hY*`l(w<7U~>fMMr)dBg-%T7
zmTeMgds*^c7=z1jpT~|^#vv{%^t>Tkh7^fQ`&QrKgRs?Q2fr~J&V3wmombFkauc$s
z$N4AX3!6xqhLWT&5yl^jUgTZjuWq>0L237aGe>3ot^2d_Ne`wblbv8H4bsx)<)^np
zaFw_e=Rr}C(R%Lg9eK7Lo_{9q$mzvs5az0BwELp82m}q)chV({4@n8;B&kk
zs6tKH1Ztt;v($5*R6_l@3vn=trtwgcWJmB9T^zcDFLJ>Lcer8SYBcorG57S)1MG%%
zaGZ;{k^8P?RhpMoR9kNBzB^%k`_tnOpNX)>y~*i?{jNcBb8=L+p0s3W{z18w(ZFd(
z9DXoEKRyn!t7`Zc(3L<;LyivMDxWj{YMJ$Tr4gq1Sye{|6L^KJz+sMH;r&o|6gJxA
zv?F8`d9GJs!6_I
zW`Gim4_q{cy2+E>1w-Tl5{9(cpY}rWwdgTua=4a|8|`sr%Njyc@YT4xdyj+>ge5Ct
z7g_wqye-PZA>4~JQ?3#auqUQqxjIx)E|=oL^y>Z(6m;X91gliy_FP(sgs;h3j1TLd
zx+>uVj_vEB9H}pP&f7ce-WIoc;$Ld!ShrPC$kLPq#--d(jWyDb&CB61JJ)DF@iesz
z%_W+Tp48AHQT|D8Z(AfUZTbxCrtD_SXdKZk5g-%)d`YGYhFgPHu4j^TsL6YK*gtFB`W7LhJkC#Ul0o~iCStPn?Ozz
zpgXB5LRmC3gRNf}*<=L6bTT>f)(_$!Z_8E(!lWC${$WpoRTmnNdMxIM9z_qLH)R^C
zp=}PhwCrBIOu{Ey4i_oh5QH$Q+y{24n{cDM&DD#h@2OU#;p*9D+?G>;{iQqobYhdA
z&S3hwlN4=`i%NzJ%f^+ySsmJD6;%oM3gF1|t~ogBXmdZAMg^-5I2u-LTY)YmzKcUZ
zrb86<4i}I3zLix%vz*&u>`*Lw%}q%uRb5M>W?~_f=@M}5WrKqXbUB>}Aom+$DBtzZ
zYAekAruf}2)%r^DAEPjyb93_5G<&czD*Nm_J{ma9<4Fww%F1x5R(CLZnwQ`ik!`=i
zBFxJphIr-%V%?DnJ@{u)5V2t^w5%MgN@GV>p?axaf*o0EuE%XX8p?@N%Oxr9r~q4~
zkBW%Jpv`K?hbpLI&)689X5jkLbyw&jCc09<`X4vREJ_u^{(|ilZcTD}tbE
zl1GwP_STpKmu#>h<*9`u;X9SCq(gC7lb0ek@==vj(n8SC5KlS*74#9R`6t4tIPxqk
z4pkGP(mS>XwGOfE@r4nccFsy=&q$;wlT;$YJ=<6?0%UrVUjhO+I>E^luy
zYTVHPXU_)DZQgQ|o?0Tw*s3*R(>#&rK_7<-W6}ZlPSP3OD3yk8bh$cUu;-x=-ACTa
zYe`EDAU8U^E*b0YI@9jEZ$6g+c5XlMFi$JvzUcGsCO?&wBCe3TRBx%uXwP>0oh6fT
zc@e}1uEbp(l!|oeDlf$8wOX@*3&
z(Rc!ubv_(By1Pq|#KAG78~yqb+(>tK&>iP}Bo458-7`5P1d-VBNFirqD4M9$5*%p^
z$gdFDJ?PAnGE5hCsvwGllRI1aSkV_vPoZJ8wX0Tuy5PxzP*P=M6E0w}Qd->?I>F}g
z=Wmcv+UW8hSs1Q^vx7~Cdrzr9;z1;B`e&9!ki)>Kz3;(EJ%uP(o}B48N|c%5!AZv$
zWFi?IBi4~Q>M7`kqNN-g#Bt>}SUY=9e%k#^lJIOg3E!vptrdLsUF)Wi+OrCna7@-R
zm!0C;N)ZH6CypfTYV(o*j;+}oD^>&gW9G4>YRMA`_|C<;QdMlgPh%YcwB`-I(WseEmyHP1g8lt3$oqh;$O&`jFur}R|$5p
z@YN7gy@DHuIn=nA2Qk&uZz>^%%->_v)+4%94R@=1;Z|pBuHBu%@9ph<`kp9CZA-OaG<$`ruQgfus!(;UIkfz(dXT=BQi?hEr7%wxE`AGM!yANj~6tPX_
z+-$l@1%22HetX_PRuG4dFEXYg{Bj`Tp0(sev1WD+F@v}v-B_rdHso(0=jofT
zKNC5RNApFR>FN7tCuEyMii!~u;ybVpJ6oYJ2?#N{ByM4bmz;q1jG#2#95aDPi?n*(
z2Fye=9-CJwpYgahO&h4W00srOe>bl%5W~Ce_Trt!totZ|)U+yUnQxvNbtmL&@Gz
zUlz_lf3RWPknZ`z9uwp)OJ2y!q$S(c5At;a1{k*1WIBYt_(gj_eH>``cLc~4ph=x^
zohCqY1KqK$s=h#|^&kqQOlkVIRQ^U_lPwfgt12$=X|Pof
z$*toD0R%UpN5=|p1)DMwIHm0Yw@Pb%S${a#V15Wt2+$z@5xAwvt(GM>3)p&c`&;0K
z>L4_@OZ?o0jUIA31r<}}-MQKG@Tbwoxmo->f$tv={*w8fz!&Y&pFiIA_q$0TTr9Xq
zAY;1Cec4)$SQj1U?C;-p%ar|;N%x(?W$S+Omve!gO?$iaqgLsGlekMX*ZiOUI{WM7
zt5f^<>mq*0Jn|)ufN@fz929jrkt`O4MA@jaUF;v$6~=;2fpX_ROi;2cz?Y|BTCJ#0
zEVWJ%R)}HF1wwEN`y;QR)v@>XMKy{QP42G?_n+Nj85FHc8x8uBbK9O7Q6nF!|Lq!P
zUZ+S=Q7LMTVx`D&yCNQUl1Vz-O9=iZkxAT
zTTu(N^mp~4xoSZB#8z$2SwIm(tX|X`l^D=$h3NsMpaeD>&CSDDJ?+?PHATaR)}%Zu
zeRj~HbwOO8X9T1z)LEsdVj~2Xz6bK9`y!e(E!L0YEuNF
z;k878LC;zd!x%y`6S#dbZn3shunjg1bM~M=_<=>LbY5~0^uvf|5}=Dx-3)1xnreCu
zdDsgv>t=gsgqj|8n_X(pix2-U5@*|jsc3HtJ_X6V#
zHnDh~He*@Px^q9?h3RM0fRfP@flA(gku=LPN1xHh5mTshI9iHCwxE7{w7>HPFipO?
z=ed>ebB6A#tG_R)$EH!I#kl(q$>4uolE2!FbqxO$m3W=w|Hof9@A3=TFE`tt`i1P@O?y2H=kujh
z4PUfYcU5eLe}9|a`iWn;j;4|_=m}N9#hR^4QtcZ0RU3(QnWPQEVD<_Nix7cMwy0ur
zC_f;Xq;~p}nAa|01ZJ$H?kXNdwrrJqJYYNc?&-~%!kO)Kg!|(9A+)Cvae-NujyVlr
z0Yp1!MPyOG2UZ!DaE7-V+nSEL(`^pwDVp_%GdVP=i5;s;Oyv|;m<{J9W9qmcLlS0i
z&-hgdC8!Tw;BKC9_o&y3$XRpL6rB^bG8(F?dscJ#>Rp#=?jP+n&Np0Ay11@#>qHb}
zWKwhMA(?;z^Y)_{w@kplXLXvGn}FR-<0jyGt-A@3qH?FCk7JU5Nhwo`_z+zI4P5(sLWn#HL?
zyqx4T>V1z
zhad0ESBkn*a7sxlYr1oBG!ilgPGxc~ECepxBbWTM2r
zn;*|=i;VB);NBsMNr=8gP`R)rR4!x}E<}Hw6xeULP&}-IHH-c*dsg0-o
z#_1qSHB<%`!a+yWbTD07wysFFM}(T*5KcAkuFipzH77qQ9!qp*LJXXHj1VGZ%<#F2coO(K2!Y`E|ZE-srLmEq6mtPcjPrHUT&kQ6S=ztk>
zuY=AKnku2o>!e+9^1*A?}2Y>Xvyt}ERYcpLD5x1Il#4;|Fw90%}1ltTXwEDm<|ydG%~
zK4idRAp_dzS#byGiYE~QxwX&$K^bsJauHe=(YlDR+MH!#espEZFB>lWRzvMTu^V1(@@A5^8xG&5X>7Fzz>JNLfg)e-+{pQjYjz{PAiv<3~
z<>SL|+>7biIhh2LsKw#pr~CFI{?P#6!hSzNLk_0>i!&P7WikHcn->=(^y}rU7ftA_
zb>HLJGtT!4*lWc{5c7-lh?TZy8_r?7%s{8ZLXKq1Dzl!bK*iVLA
z|FG2Qc{uUq!TqZ%*Vp^d7m3|lk$%<7m)S3xsd@5C;Qr0o{?5MD_QwT$v5rk7#lG<8
z>GQw}NBwCufyvK16IYhLXva6Oi+(h0lr-+kl{;^Z
z_UaLVQu_4fjc;x?o)s&7#1|@S#p=3W9`&X6+pDS?r8)X?WqVrf^TCNPy5D=+a7C8G
zldz?^5>Vy2aU*
zk3K|59=>_$ATwX|X1-qk*||eJ`e!s$D|8q$mDCrSMy~bS?=Sj7`}s*LM9=-kfasKAP$;G6v%a?Mqjj(X7Ex*_r
z@%UgCE-q%1++<@>xQQ$2&GqPH
zgDuD2_$3x&F`r4(>PPG?t#8GJw`yH~l&>KkIjw!6>J3Q};jWBJ_pwtstjJ<;*FO4p
z9Pw)N!j*kEbDuiIQEl5RR}jBPqqCb@(1qH~Z3T?c?_Brtq(~{yFNWz+RS|eRAw~7q
zRhGvAj|J)uS+wNrk%|*d$yfAqu%XOLqQtDI7dHXx+>a5RM7fK8f4CTYA_$vN#;Y`nNps9{Pw4Ve4$Y6TZMU{cwrSDH=Dk~{Pg}as$NfeXJI%0==?m-
zO2#(K&fq?823u~6Z;Z>m!p21!mYPwxwHS`(|W0~
zf_U7Ub+4r(goExE({itT8Zrm}W}kW&^L*jgU%&X`{%tRdSEB{!TRfVKAR^||{UW$2
z4#LY(A8-!B`J2TQ^&pb!-MF8qME&POgybBNmAuwBdv%8Dba9ryDV|+8bqkmsJpbk!
znDzd%$4|a~^fJXp3)cuFw=Te=Wo>=2Tmn~)sw~fa3pgilT?Y>}W5n+=SU`=kR!rdi?n4$;+3|Umoo~|7NM*
zu&fs4^NaaV`ceuBy}`irqTOlMZL|!@Oz+WzPQ^p^4u77&LoSV%o)(H%D*WY1=VuC!
zw>%5o>75;4%udDl&u7zfvAFpo2;mZ*vv_|VcE6rZ$6;>*lQ`*Jj2Ff*9A>u1qy8IT
z>#5fU<7#4nH>~HqG4T6e&yN{3&6P|#KdKw7LCu2`rBiUYjyL(1S)+ixz3N~C2f$)FyFO)A~1n6CmOPjFvS>1
zu)ywcNl5wOVsYNx+`PKFDqq#g)7j}}m9yQOHu%+Ouo!mpIJ}C(aCACc*t4IWoUDle
zLjLUO2nZMH4@qm_YJRyZ6ny7=bg4}^MwyJ1s-#3osl7veJ7j|MO6L?Q&b2Qs4N$GE
zWsV{u4?1n9I}kJFfA2W_J_mP()Va$(sM^hzioSeoB18Q+^&J)_jk8&+{_frG>3T3-
z??@DEx604RkY*Y1OPBaal+P)E+kfUP+jW&|x}rxp406HX1r!Fje@vfvzxc^#vhQnj
zp_rV`YUw|F$(08;H-*>ym(d;awJ>Zq2X#n7Td7cZ@P0*wz)%X`B}&A*t?O7@XCKEq
zyZb)UEnv`C7pN?Q?&U^dD^9z3`I^$E5+>elWd4mCH`kp*)#mE@@mEEyA
zZpwlrwOj{J72IPj^ab#1D-If?W_w(ucm=tDI+`bq$RAih$n&@muh-g-9y#&8rKx#vdd+)pX
zW46zmjC?d@ge@uT+3Z&*%bvBpSTK$yizoe$c8T?qZ-iXO+8Oj?4tBJ{!M?Yhld!OZ
zt)qxn75AmC)N1cfAnPZl!|WFluhSbjl)O{N@tZ~H5mg4tg@?W{UDge6WCL!itM_9t7=uDz489nvtI|l4u2i}I{&p-xPP;Gns8~ugitow
zn!Q@Q_U-dM0dw*2RsLW9!~ceC`hWhf|4M)V@Bh}G|2=>ITr?%kVw74UasvX!so?@xfwufJjjrW}@!92tZH-#^|D9j?5BE^y%KdOO-so*ME9~`u`RD)U|NS5S
zmw);9|DCFEBhFGCHBznr^S}Ss|L~uDE#gaUs^x=!`CtEcD_T5s1nV&o;q89@U;pQS
zW@SRJ!yP3+%awTispvoXqP+-*Hm!W(x!r|e{6^dScN@}T=MTHTDi{I-{B;qIyM>rs
zZF}uSro|re=9%%rNM`rO-7hM!odJz_>%q}&Y&*ceii^=g>b#jCtFLj~hA2O2<5Zj0
zk;kCDMO2jQ-Mcq(h8K(jHN2#{iK3m?R|GQl0O&!=7K*b>
z3(s3kfLQMmW;1C>BN*tjj2~*RcsU&na+T;~jfd#j3Ud_f64v-)>sibicebv*u?7w@
zFq(yLFGe%@hbl}WR@EfAUYL0op(4R*P8fcO*@tHf`(qZKblG<>z7WZHbc|nt3(wH7
zy7|e}oGeIfS&7}OSY#$4+Sg6->&(h`lHKg^v(^LW_sxbe?L_YiaWcdzEw-&(cSpz4
zTUUzX6}mna(*ka|0`{G?LE3uzRc^X!R@IJub%h6CuOMMpfvnh1_cEewIY-d>9C^o6
zL@8ED_eBx!o?3Rlj&KGtM(;)woCBt$n+|^M9d9vCqK%ew!pmvU<^U|CE_v^e@H)L9
zC^uM9oli#ZzP>npHW@wNhutRVPnDzEA)V2*~JdjMKXXt+3T9s{5ZoUUasd7cel)d@<{A50=56z
z7j6N>Lo~iMhMO#+Qpyw&u2%4l-e6rveW8u(JC5)$gU)hP;od#^NuvrCVF@v6Yn}GS
zSH1Uhz10-4BG`LorxlU8C*8c?im{z#CO<|{3Z2pXLb1mUm>w;^A_
zP;93>biH&bh^pVampsR6oEG4N17@1C5W>D)$Pm(*G|cpAp8Pg^2`!i-B2IPrjTmqfO_1mXI&h8l2eWVZx)xpHoAwPLz9_ruW>*j6}uvx)Y?3xRu;5G-D$#GT3zHk^;BK58I&f|$C2})tBq?cKK7%o
zC-KaFibq_U$wz>+rlvtG-ie>4_skS-rF#cZLDNw&8#1wpQ>w<}6;&gpQzRP4^R35`
z3Q_z)hU95Ym78Q^w$*!8dClA2tEjvbz!_%pI2Fi+-ex2+P@;x%uO7#a9
ze~M;g?G_KyU$-twi|+WVbdf3V2^P1;r~+)Rw-##9{RBA6idWq*gRvIIAV6j{MWa4G`9M_8xc|!gvkmEoJFCml1
zF6N6LPl~b3zkQp5Gvdri{87s~&xO>-jX{WCLDYrg@$IK}{?AL|%*NC9?!JvU0tqYY
z4Z6$Ge1x!KndA0y(;gIcftu);gFPwYLYX|GJg~!J>5EC
zw=mLWU;m$id-t5%4i`rJ@>THqVfu@LKycT@uZS;TI(yP1St?db$&J^9ECA;BhmXRU7GYa0ijwEhL%JG)2<2JLch4)Cr>t^_yZO_Bnk_vytO%VA*&@J<8qL=Nq4
zVRucGFxA^~P=2tOy}t%_8|rd~%n4uZzdt*kj%ASIJ($j3ZxkMUv9Z`fERD(&G^C9}
zp*$cjocv(Ab8DY&&B~X(@kRJxY9PaOG|9IdkA~z9lMSCQPFW>O2XAZ^CRUMO=LXr-
z8;{>#Yi`7O#KyK5+>za{zeZ1Nx~L|Yo)lcVGHzqojoYqin0ca2)2fjB(V3EA==Qtg
z3UY5odPK7K(}N_4iFD^!=#x#YBuGK}*s138BpfHWsyWdtq(k3u4*=?I=gxU_;L?q5!8OdBMIT5BD8c!ADoE=J!&RHl9Q
zP6xUQj1U(VKY>5bn6geW8#zYW3Wr?8YiC{*T=72lF
zp)lM}$)YrWNH~#4&hpezK(}EkPCPN~)pQ7Tp}d6%0yt>bqFCCWNhlWJ!f@V$xJlrE
z-~nO%`x9yLnzqa6`#UBr`M74f6Gst>(WHeZbB@?N+fZJg#dNnfU#J%)>_;wP>zM|lZRT3GvJ5nD2`
zO7Cn6e%y||ikHwD{8*aY4;l`-(gL?;f9gH#b!U!+q8-LHe{w}$W73W{=9oe_48U!J
z?8JS#Qrs{Qv;WQvg3X<34}$=^^{4k|zbu&sKi}CjKs;uwg2>@PziXV?3pRsgqNgkX
z=MDCT0Rc-ngj1G;Gn+F;MAA#*7iSXk-=_CzvWA4Xn3ki0tZ!6=M)(0|Hhn9Sc=Bw*
zaRa$sZyDLSMxF0a?m?&aVRSt74iU~1#>R~zpc*1_`@X#hXYv8tC)0Eif`aG~mk4%1
z-g0AZ(5)hK2vv0+UZj3gh>hk1b^iEbu^=`);#AYUdjZmBQg%fTB?@r#Eptg3NaWfP
zqASsg-
zY&Y6v6)JG?dsmz{Igt>QWDS2ixu7e7)@HNa42N9Atm9=)kdAe&S
zeK1cICN-@$KPNN-(e$pqM?u1~RdNNUMF4oe`-+_0Lb}S6NgZbf=7}+82rPvUSZN_EiV~V&f5$TLY7X_T}+kmhWtt6cuKW}{(IWCIdMqW$O
z1NO)O)kO(DANRT+D|Z={CSxi%olry#iAE!(8r~EY|BAs9@h|UoFy@@FK@jnyNvogl
zpZ>HYqrOe}yVKLwW^F`vMdG+hpPQN^v)PaAoXjp%e2MI#iLzz10cO1k+PJIeeamj=
zpY#S{$zEd<5~OslZ4}p%8oSnS!tlJ*8!Ib6_WF+|{qY4}a1-Nc;J7AUL|vL%>f(h`
zI{=)5U`ak>7WF4wI^3_HB&h#!1@#`MN$4gFw|#6#fZwff5rri(n@Y|8lkvFnHZ6KBud6`icaPL*1dZ#
z7pa_W+Y_1s2Z4a_@*y2L6yX!a;}8@nHDr=W=O%T64C6J9CRnu&j5$rqec3Vh;ta8Q
ztIKG5edezlh1f1-&!-EE*htM`jV(m4*+awI*lxkR87jhb$aT18JEsBOm@r
zPx)3x#D*^W`tec1+gMPVfvKINM5))L+2KE59q=0H*oiVCd)+VAP^_60(Joo{euZc=
zm6qQ^e#%8)aFT_~Nw1lUAN+xW-VD^hvic70)H(1M7@dDwvqCibX?KEO6`0fL|u&
z;shB2#5a2687v+2b?iM63$t*GTK=j5T(CaaT*ms?fLXfa>`}P&YQ-yK8`LC3cNP{M
z+mIO*`E^m6%@WGTj-1V|3jQ%AThb)zxAde;0&MHUhevtuYQVaIb8l!#MgIa0@%kpp
zG~479K%~65>8$Q|>$E?-;MZ~6$F0iRt|yD1#%NnN$x@ObdKd7~*dLWz`7J|z`(&}b
zb`bYs{UBUVa|h74RaCj3aP0t_;g9v(R#C5TOGlg@dK>KB07088J=S61haaAEvVH9e
z(w##R5xj^==={ul6EudLfqY$@hL*byeQjJjcfi--uy;Aa4qpK%#{v{BH$`dQ9a-=-
zMUK|<(!eVe=m!4i(y{NP1gO$MCM1lgvoOx_mZ8p<-~7VK
zdtl>q#vin5R7C|zuFWLw>Mk{vS#DcC{`IiQ)V!(6kW5oH9#P!@@<%SO+FM#)aB|EU
zm$l35KY7l%`gnOSquFH^7|+=)G14o{4u>(VUnmI#{xbfAJfWc96OpZxu$o0=BW&D;
z$e;SpzDuoiKb~G@smZL}T9fa$EYod+?r88Si~=ZgFc<|XA2Z!xWsBre)L99G_S$`0
zKYq%^7vh|27#ut~wOfr5#SH!C^#l3x`&bBO>vgZ4(V~F0!bva9dvD?E^gRnYmHzVi
z4+l?P?kAhta+TR+aVrl0hC_UBEAU`1~86uWKWv?9o)_vs&sNfY>R?`69oi
z6VJ2v8u!dIOMX^3f6ya5=S{RZ*@lBNBuU|6Dt{~&n)A!DL(*|oJb24-RqTENiYS#k
z&AKQ&NR2JWvJ-J%I8PW?V|bUCO+~4HSw}h3jnrJ{R**rob`vXq
z|Kkf?4s!0w2b2%3{POWz<)NIp@*yFjUfErAvH>$Z3qg9f5!T2!Kf|r6YrXp6Ibqc~
z-lsWts=z{e@mAcK)eo
z$9mL+1i9kig5a(^cQ&_TBo}6(u4*C*}_v`Ci(vJx5U6`DBNWc<}pg6K8CkTxb
zHH-OQxPAq9BIlOJL#Er+ohjLOcClrd-)7|94Lv-JE)K{?J2fnDKY@;6ahpJ@Fm)pr
zDcEe(B1aSB2^_!z&da~9dqao1K!
z!Ga9ouva%8JSfbuuM|-?UpLVVMTQIbx=0kOpv2luVXkzmT099iu9boP!^_?7yHNQD
z7c-6>ZT8MboBSdi`a*Bh7zX;SiRonh9N}|zSvSx5c3q=@_lOjnJvJlTM)^XXmvXwb
z@W3WeO(xJgo?V>djdC+mP!O8jVMb_jcy)^CRkw
zM^p^Q?N1_UJB6g}Pa-Mq_z&AR?1jGxK?J@-9T`dO9Yx$fKMoG#%@PCa!zZ!uA%z95
zIQq|me?1(J(4e+rE>6bxy|doCBi%h%i1>Yqwc&Nlpa*y9zX5t4#pp@#vfWQ2Xg7tR
z-8&%Y->?p7t^D3r~-$55p&NHW8@@6q=y4^Si?Y0n=$e)j#ko3(75sG93`)ila3
zXQ}ql(`UOFR4X$vs&Gr91^!x#brHDP#w&apIza3zP?t1~Jp3PJO|BC9RgpBi;=I@|
zUKGaW8TH#0iQx5<-r&n=avY&{#G6-j;Dsuxi(
zHMdz=UOMUBJT9b;du~+k_R;mELRpf&)GyNTN98Q2(JZq%W2_It|F*W~4=*@Tj`g&W
zH#^CwVjuGHgS0=bmPK%jHF9NDBs%$ejrfM#1&2j};9|btCn@9iz+!PwJdbAdc`zxQ
zUd+Lq&^Vg!M~ar+uW4x#rfPwtby!Acbrkp!JdYzh54OhKWCNbhKaFS0g*F^tIC9sK
z8#mW*{t2p>J{*WDA~<@(KXgH6>7HFKAC2w;UPnIXpM!ijAU}JWWs@-tVik%fYt7z+
zg5i;&@Kc6{QW2f)*O6`
zB!>_lNe*RP3h1Gd9Yw^uP8jEP>p-yrpt)aM@13ml=M8k#sfm)dPr8nAW2KEE7Ql>L
z@JNTS>$uY@1}?Q&^z<^Kr-7GgQHi*cA0`OgIL?%5Csw=-7gUOZA!7*T`=(bh`9M$
zW8#;PvnKx*HsIrBH_7u)Li~9O;?F+;@s#KC4}X@}H9D
z`AkKAeq(g<{wS(HA*qj;!R__AFup$CG;iD=|vXZhmNTl!AFtp$x*Q*eEq>0}n3a{AYGF=sLgsC|*`X*9XyJjVPe
zoaiDD;^%)|^!69$9!q0gim!
z@@T*{N%>+cGjyuW`Ni>gH0Mm%k^AKsK|0zD<-df-WjjMchc7OU%lvbdlMQ2asK-S$
zONINr@flgnS-6q2GQDD*T%@|Pl392)17b&@ebndmt<%_h`Lz7~^!fPL97O_Z>9sWNQU8cz6bu_}!cAAYncZu)0(uhXJR(@f&p{Z>bYg{InCqYH6aizO_L#Xd*k3}Lh7U#m!AL?
zv$UmMa6V$24UV)3nfGk*44@{{BeKRJ7>?O2*kzM9R9VxFvpE6WnN@W8$K2SN>1^IR
z36BP6N2&rLul7V}WV{&S{nR=6rBOVaGJqoPTo;~hJJ~%oe=|Bi;)pOoN}*DhL2Jkr
zKp8_kT1<~DVr*5dTE2TRk^Q4FII_&H%q(N@9-u-8j!JxV-F2>dUbdXsZ_Vhy0C&`I
z2Q900Qsr8AKta2zX~9{#bem5_c5~D!mNJ
zX<90?H%nzNC8Q_DN!6Y!6LrJcGx&4<*?`;MkeHMH`_gye`;X3u^boElT#S0-82w-1
zmXBy;Wd%Mqm@FX^L16H09eSHAN(WHX$LE4j8Kc5E4D|-RbJT>W-~54ImLIv&@(!0O
z@#AkC8Hpygq!@`WUGT#-D{7Hr)T0TEAw04R!?mfiyV2Fr$^hr;o_DY9ch+uudBUPs
z-=P~_9LHiH5l1@*d%NWWoqG}AB~J!bmZX<=X<1=)_SFGD&Q$jIu!zs{U-3(`hX4F*
z_Y7t>-3#UYjx(wT=H1o4<)Lj~{SBOUGaXeQ)>L!ywZ&@VwX>&8QyD+%>yMv;Gq=eWRd1x6C*&j;JQPPr9L82Gk^qbekw}i^#jROPA
z>@k#Z#{=^5R-$9ip7PF~nZxf$o*E8l9IIXYuz)V$(a_P)8y=-(fXzusF?v56U78Z`
zTEDTNYx!TrtV2b}rLD_p6A5Er{_h%e4;yXZ0TSsNrG^)8bJ)L3ptn|c6%ezX|TKi`2@iYB;
zX*6-yrJC&5(#6<0sjsGqBDl+9n_hYw=Av#?;mKrhJ|#VGsohhjJWprkmm#;2{P`l}
z*Ta$tsO27c&+jntG#_!Fsz9M$u@Qig4nWvfw9?s;2?>k}L2fFrR(ZhG
ziOtshYy|W0ZVRq;Wy{UIj+tk%M{_8h>_}Rvzhs9U{W3j1LbzXOXWFN>-`fkoN5PnD
zxL*`4#*Ki_WLTYi>%^ES4N26%-)-Kf%#sN1voew
zuuvF>bef;jt`<3yoh10e{TPDho9?<9?lE5ATpwI;Z$(xfhRM$gS5B;z$f68Frlb)?c>6QL~rd2Ofe4mtPxq~z~r
zBIGPlQVFrVKtkyr424$WtWwWHlv0{~5o4D;Yf?_Tae4=xL>K&^3Wbcx)f*{t_o_fF`OpQ%KnnYthG@H*3|@Ni{-
zd{?|-i5X~>50mAjSL0#DxE$MxU3Keg6uN&=!nU1jo|DkJI{GbL{1`HQEsZQ4+Rx|~qy-u8XnqTE&W>ReQBS)Yr_mlV3F*lBd}H?Pu(?qVa4mu{piQ&$_O
z;^}Fk3oGQRa2lKFYi@6Dd=&Y9(b}dqZgj1@Q#4=FqC)w5GRZ+i6Xe
z#C*EtrCCt7yu*mfzL8R#DUrP+)pW^}TB#FTQ{8JLd2TgSs2v1xUnLS>pETO>sRt?Ojia9%SEQ%Rt{!rh&eiG2TT~a-k*PZ;G)t
zr?+>|+;6LvQQmK>w5-41Rxhij-3Cr8`Qzkm8{Ie*xx+9>Y5V^g&yFCHkV)t#F~jBarw
zYAWR^$;9(#E`6SJlu=C81Jab26Of~EmPLE&s3j^pisV980oKGrd(e;c;LzIOcdhL^
zbIe*cuHURGyRaJ-qr{?n_mZLNaCmaztPFm0%2BodeiH*QIsxi+pjFe2O8_GcDQ#pk!O27Pu-NcQfCRE)
z6RevBO7a5eL=d4XIY*Os0u4Xxa$8gPI#b<^_b{!wNAKzD)_HzMc1b){kbBE=vbB;H
zA6bHyXNHC@vP)XrI+LV|n@{-{yi#UlaXZRdTPF+s49ss^M>0r}Up<#)%w={)7vx=e
zV=gD9H9F&~XFDs4+7|D|LTYDjO<&nfHhNGrgSEb?o2War}?0a`-yne=#1OF3G`pbxZmpA48e!bVXn(T!*ll(
z^4k)D-U~vq*0U=mLg`suEkRq}ImQ`_-6Sy|BK8*12@DmtHS~8NEQ}_uk@y=OF?v1L
ze*Cf7$Fcf_A4{+tb{9?3Nb&NmFqGIq7crQb#|53Pf
zj>FlSg15(vWzV@PJ04URqnF1H9Xd08WC|%&zlXmbBa9a^
zlXdOr%_5GDU=G%wMA6mj&eu7p@GOwyA=b0zn%IKQiB2+rn8dx-h@GgJ$2@g$d~AGT
zryeFo7wa%P=9=GB)zSnIAL26pvmPq2i25JM-jG
zX7M}udAgssncEpgd!D!iXg=d?CH?=~v|Dvu4$tnt4+~&yvX^`$XVRgd?&N(2=wV~tTTWYkK~F9206JZPN+30%SE60Z9q9d@
zft?)1Jy@!}$hq%w+eh$AdtTAz{PpuBV2M4Hg>9*y?_-2+V(?4Zz`Hpb+y~&nmNOCQ
z119WJ$0JwEcE3n2wWi~cAbmCW%J%*III1A7bJy|-jBv$qydfXpeFgi61Y2V#rMq;Y
zyeGpc?=7i^z%WXya?99Bjr-~DTMYF^y|TDJJ!=4t7EM=s)O}B+Xom}}>TPd_k@$_o
zAmiv~`M%TJbTjNmJivR~i$17ve7AKj!agM8hwRXa5KjjMYFKjwT6WA1uYrL37*Bxv
zhEt^^HSi!zChZx-TU*~@c%Ko{+i~07A--kUpG&6_@Sp1GQ*tqc2r%Y@x9rf9&kB93
zc3aT%<#U94U%QD>#eCC)Tp>&LM5Hj+2w7y3;Z3n?a!ac>Fp#OzL}q$Ndat|E1Le3U
zTVT8;kW`{0vPJ6U8eTb&9H&r-o2~H;R_s^U(j^oVt%OeM>
zmElInKJHq5=(qPC=ziRJL$RBGJa?xf0>xKHQG3m2@P7jn|a-l(5^X+LmS)^52p^;DeZ{tl4T?jdTVom-O>r>IoZb9=ho|+#Qie8C3-J>G^4OX-Q0h3u8HYD;?GaCAW#D}AI`HJgJF0SETUNxZ&XkpbS
zMAExC0F{o$pZj4kq0q`p^%G0Aj?AK^u<6m*NM$*G{LtY(f`GJ7r^2(qQ^&u0_}G>l
z*4;46kz=P$9zS$?T(?TL=FhOi5hi_+g6-0Ie)B@_#dfvG|@+z^Q{DUKC3*S
z16}A$)A{ni`F99_{+aaW9$X}MGr98ZZ`1K<(HehaEtuD(g1O-UYbyB}<$8n8I~TCn
z#y(V@J)D&l2MpmOww`CqME=3h6^}4iS4b^2h5aOrK(uVMfnl(_nKDxkY5~^bBf3Hp
z)MPOF1sd_yXc<96BGigL966-q#$1%(){_$55{f<~$pGdLCLPJ;!?pl413X{@{D??#
z2e3TgJLnNm)%J}X7esQ9J1h^lPYz=NvwNNXxPJT9LA1OcoL7@k0}dy9#GzF$4#g+^
z!P2e*(NaPnTG~}0N_a24KcbLFJnzF+^d{*k6@euV27Sh^5E3#m6ml@#>=h*Z^`X&8
zNDlbmPEzitOWU~!LWzkyw=0Zrvm1&ML+ADYqrb5mNP|)=xH#c8G4~KuC(4F*nMd)j
zuFFk0vkm#i-^i8iVg#UY!h_{q!LytI&+;C?6IJ~WwS?G5_PDzJn=PVw52I)r7j}iv
zLIOeyyFw_c_;Z5~lquc-Y(wH63|B_NiE2#Dwt>Xo#_8}{KP1lY3W@UxNSyx%fW)?L
zxqG%^tb@PJ?9cSi{>5Eq|6*eHFYaOXrB6ruaWHazAByvLy)qPy-Wyfs|J#3y?nXBr
z%Rm00{)}day{aVN@8f>g#UnR#@xocb)37^HY$rTKvjq=jgPoz7-=hO0e7D}UxBClQ
zInSA0;dv$j&ojHiGrDw!SG{`=Q3bL05G^Di+Ip<;IPUlgcYnIao^vJyNz#MDbu@xWjX0QB+*n|C@tdwmeyC@V(C=&jS7t5usa2eYb
zE@KI}jQw4}Wj7(BcfmL{dAB+st9GEg?`!-7O5D2{rY`Y(D2nBJh
zx^oKEJKhc0#uohk1P#FOcg`XPcuWdi_4~<@qosuDx3^Sr@gCW2HKgqHdVC)3@MGUF
zOS0#K%!b`3t|nR7rs8NjJer$5Xx5kQVeBqf56)tDNtl6I%r3*82#%@YRZ|t4t+0R_
z0)yRN6Xx;FOpdPx@w&~zV
zpv|?*gejDcWW4x@^yi%orfpv8@+C*-zK_`!@froxW)58d^2>s>TETZ$UFleuSO
z4)+o2y^c?MyS`;8y|?)x@G>^)5NGTjjL;4!PaoTQs@7@sU{(m;6Jw8TJq%3XTr%I8
zsUweW&8+>+6s~r{oKuI#PL5BU`P%8jC%+MsI&ab5j?q2LZ>Z>9g7?FPi8K@2=739XQ|e?AUd#ekn3kp;
zCAE#{vdQ3dva$1O3Gtd^JHPiS4zp)#4?os=d^_43l2?wp;#Hu#S3O>6;`JEE-mv{n
zFUsQZo=p&5C|ZweU4`}trO%I^4v$;Z4TlUdR_y_hE{jD>xO))Z=}w&)M#%_ue3BJl
z^l&OfYql+#ctd_8XPm&AssMNYi6vo?l}3pY@1V7Z*o5JbV*ZpMigyTsgON<9eyo0e
z%o%hHG^5*yBw;u1$>QiaxQ~}d7>lB%TQ{EE+eIZik+G{v;h9XhBnrcvW9=H|2FRFf
z76L{DO$ItK;|&3_2mj&WOvC_5Sbg0=VDNEY)Q7(-U~EgNLQuqtPo3^ZK-ot03lX{%
zN_Pr^VTMlAdHt2$nHcfqDIPPINWTRw&V+@-oG{#X+_UKoS8sxpMf(F?m5($;efxvYQyg`mIoViPOqd$(VY{_ub8z=S
z*-B7D$tK)0+m@M&0gIjCFEr9-!Ktk&md_S&C?uSbBF6`!!-lF6ORabCVW7BkR;Icw
z1r0wAW7oL13ah9&LthBPmz^DY`0n~2xkPC73<<8XHI5gmM!|r9qM)&@T`Z=u^r~l
zx@zC4&-q}wQ-v-Ep8V=7%MCnYC@@iLReHGXOU3zEotBT8o0~hi$1-tNbi*qvtrhI8
zcW5d6j3RrX?L6)6kkgi3kB*!^`6HL@N8#Y&K;{rU4hPr-W<+RuX%yrxJ4vJ75J?#*KNvl1zb>YfYNJ*?~)kj%u|yE%?De
zHEa#5@VIksJ5ViYxx=NLbA~%$QCCQ%SK23tr8AC$LJpY}DQ0`riC`Z+%*I9HdJbFs
zi@`eMVrIm3On5i8+mer{`RKu$Ysck})-H_gMBbxP(Sr}qktpus1MPJh+>tsfRmv=d
zR2XdVVJYXPSl(aTgH;fFH>L{tHXg9Jicr!y;~cQ8E2Ppcro}+T<5eIptdS#EmO}kP
z6t`rc8q>*$C%A0(K@JA0BM~}b$8ZUB(M~jvxWE`ExT-cF(5!fGU=8XtU($!0E)2^+
zy-xSVaG^5qg?hkhw)b5v2h|D&F;HdO&SZ~YTISI?z8
zp*?6F=6Bn<+8(eduDD&fDIW`E@bZx>@7R&|V3maKzn>gF8%FRMsDgO;dJotMXV(DM
ze25QJAtt$mJE0^_#|JD(3CdZZqldh|_`zxjz06}KVKIvaDjm;6uU~Iw!8uMJs5Z`H
z743|&QRJ`z%g27%RY|8dXmPw(S~;<%fy%}cA&P#Krd5Taxe%|=MT!})zQ`SROpE)S
zR$J}M^;6uOACL;@vORnoXSWkkXrY1nm+tjxZ&iKO1tl819u%s&^KX?Ll_=I4P@Yz2
zyw`lI8K&p;63X1xo@?etUrOo5k*iMyw*OC{Z{Fe1z;MyF6ZXoRpEX7e}+nY
zNvr0x^t|#Jp=eKo*QE_s94#u0BrsS#=3XKf#~v7Cvm$)pPIqf%u&To2@ZL~zRPF9a
zfA*StSGwxybJWkO^o>ZQ9jMoF-4+WWu1T=@j5VpK=#U)u4^%~3B`AlZ|3i(zT2F_8
z`oh=%gEh>75J5j(Gyw|{if&@C8sazv)Di`i8>o&L7CS~tU{)3U%pBehRChX#nTEAOqDG&){xBN7Q9&@UH6Dl6KX&(<2}2@viFZk9`s~!TPg!k#|))
zmH4QWQN0S3a@TcQw3VpJsr+46NQf3yx6;sBz*opX72>@)ig`#SgVJp~Q1H=24k1Mn
z5^%8Qd*YBpQAkd&EDcsoB6