diff options
author | Ivan Tashkinov <ivantashkinov@gmail.com> | 2019-01-28 15:39:14 +0300 |
---|---|---|
committer | Ivan Tashkinov <ivantashkinov@gmail.com> | 2019-01-28 15:39:14 +0300 |
commit | d3f9e6f6fed382ede8e314c370c21e84a119f65a (patch) | |
tree | c8cd63007993d25bb8c17b5b83e8761db9de3295 /lib | |
parent | 1d2f41642cfec5710055bcf8409778bb362beecb (diff) | |
parent | de0fb88a9cf587b9613e267d5dddfe55b0f414b7 (diff) | |
download | pleroma-d3f9e6f6fed382ede8e314c370c21e84a119f65a.tar.gz |
[#534] Merged `upstream/develop`.
Diffstat (limited to 'lib')
38 files changed, 996 insertions, 117 deletions
diff --git a/lib/pleroma/PasswordResetToken.ex b/lib/pleroma/PasswordResetToken.ex index 1dccdadae..c3c0384d2 100644 --- a/lib/pleroma/PasswordResetToken.ex +++ b/lib/pleroma/PasswordResetToken.ex @@ -10,7 +10,7 @@ defmodule Pleroma.PasswordResetToken do alias Pleroma.{User, PasswordResetToken, Repo} schema "password_reset_tokens" do - belongs_to(:user, User) + belongs_to(:user, User, type: Pleroma.FlakeId) field(:token, :string) field(:used, :boolean, default: false) diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index cd61f6ac8..f0aa3ce97 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Activity do import Ecto.Query @type t :: %__MODULE__{} + @primary_key {:id, Pleroma.FlakeId, autogenerate: true} # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19 @mastodon_notification_types %{ diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index ad2797209..47c0e5b68 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -99,6 +99,7 @@ defmodule Pleroma.Application do ], id: :cachex_idem ), + worker(Pleroma.FlakeId, []), worker(Pleroma.Web.Federator.RetryQueue, []), worker(Pleroma.Web.Federator, []), worker(Pleroma.Stats, []), diff --git a/lib/pleroma/clippy.ex b/lib/pleroma/clippy.ex new file mode 100644 index 000000000..4e9bdbe19 --- /dev/null +++ b/lib/pleroma/clippy.ex @@ -0,0 +1,155 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Clippy do + @moduledoc false + # No software is complete until they have a Clippy implementation. + # A ballmer peak _may_ be required to change this module. + + def tip() do + tips() + |> Enum.random() + |> puts() + end + + def tips() do + host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) + + [ + "“πλήρωμα” is “pleroma” in greek", + "For an extended Pleroma Clippy Experience, use the “Redmond” themes in Pleroma FE settings", + "Staff accounts and MRF policies of Pleroma instances are disclosed on the NodeInfo endpoints for easy transparency!\n +- https://catgirl.science/misc/nodeinfo.lua?#{host} +- https://fediverse.network/#{host}/federation", + "Pleroma can federate to the Dark Web!\n +- Tor: https://git.pleroma.social/pleroma/pleroma/wikis/Easy%20Onion%20Federation%20(Tor) +- i2p: https://git.pleroma.social/pleroma/pleroma/wikis/I2p%20federation", + "Lists of Pleroma instances:\n\n- http://distsn.org/pleroma-instances.html\n- https://fediverse.network/pleroma\n- https://the-federation.info/pleroma", + "Pleroma uses the LitePub protocol - https://litepub.social", + "To receive more federated posts, subscribe to relays!\n +- How-to: https://git.pleroma.social/pleroma/pleroma/wikis/Admin%20tasks#relay-managment +- Relays: https://fediverse.network/activityrelay" + ] + end + + @spec puts(String.t() | [[IO.ANSI.ansicode() | String.t(), ...], ...]) :: nil + def puts(text_or_lines) do + import IO.ANSI + + lines = + if is_binary(text_or_lines) do + String.split(text_or_lines, ~r/\n/) + else + text_or_lines + end + + longest_line_size = + lines + |> Enum.map(&charlist_count_text/1) + |> Enum.sort(&>=/2) + |> List.first() + + pad_text = longest_line_size + + pad = + for(_ <- 1..pad_text, do: "_") + |> Enum.join("") + + pad_spaces = + for(_ <- 1..pad_text, do: " ") + |> Enum.join("") + + spaces = " " + + pre_lines = [ + " / \\#{spaces} _#{pad}___", + " | |#{spaces} / #{pad_spaces} \\" + ] + + for l <- pre_lines do + IO.puts(l) + end + + clippy_lines = [ + " #{bright()}@ @#{reset()}#{spaces} ", + " || ||#{spaces}", + " || || <--", + " |\\_/| ", + " \\___/ " + ] + + noclippy_line = " " + + env = %{ + max_size: pad_text, + pad: pad, + pad_spaces: pad_spaces, + spaces: spaces, + pre_lines: pre_lines, + noclippy_line: noclippy_line + } + + # surrond one/five line clippy with blank lines around to not fuck up the layout + # + # yes this fix sucks but it's good enough, have you ever seen a release of windows wihtout some butched + # features anyway? + lines = + if length(lines) == 1 or length(lines) == 5 do + [""] ++ lines ++ [""] + else + lines + end + + clippy_line(lines, clippy_lines, env) + rescue + e -> + IO.puts("(Clippy crashed, sorry: #{inspect(e)})") + IO.puts(text_or_lines) + end + + defp clippy_line([line | lines], [prefix | clippy_lines], env) do + IO.puts([prefix <> "| ", rpad_line(line, env.max_size)]) + clippy_line(lines, clippy_lines, env) + end + + # more text lines but clippy's complete + defp clippy_line([line | lines], [], env) do + IO.puts([env.noclippy_line, "| ", rpad_line(line, env.max_size)]) + + if lines == [] do + IO.puts(env.noclippy_line <> "\\_#{env.pad}___/") + end + + clippy_line(lines, [], env) + end + + # no more text lines but clippy's not complete + defp clippy_line([], [clippy | clippy_lines], env) do + if env.pad do + IO.puts(clippy <> "\\_#{env.pad}___/") + clippy_line([], clippy_lines, %{env | pad: nil}) + else + IO.puts(clippy) + clippy_line([], clippy_lines, env) + end + end + + defp clippy_line(_, _, _) do + end + + defp rpad_line(line, max) do + pad = max - (charlist_count_text(line) - 2) + pads = Enum.join(for(_ <- 1..pad, do: " ")) + [IO.ANSI.format(line), pads <> " |"] + end + + defp charlist_count_text(line) do + if is_list(line) do + text = Enum.join(Enum.filter(line, &is_binary/1)) + String.length(text) + else + String.length(line) + end + end +end diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex index df5374a5c..308bd70e1 100644 --- a/lib/pleroma/filter.ex +++ b/lib/pleroma/filter.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Filter do alias Pleroma.{User, Repo} schema "filters" do - belongs_to(:user, User) + belongs_to(:user, User, type: Pleroma.FlakeId) field(:filter_id, :integer) field(:hide, :boolean, default: false) field(:whole_word, :boolean, default: true) diff --git a/lib/pleroma/flake_id.ex b/lib/pleroma/flake_id.ex new file mode 100644 index 000000000..69ab8ccf9 --- /dev/null +++ b/lib/pleroma/flake_id.ex @@ -0,0 +1,172 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.FlakeId do + @moduledoc """ + Flake is a decentralized, k-ordered id generation service. + + Adapted from: + + * [flaky](https://github.com/nirvana/flaky), released under the terms of the Truly Free License, + * [Flake](https://github.com/boundary/flake), Copyright 2012, Boundary, Apache License, Version 2.0 + """ + + @type t :: binary + + @behaviour Ecto.Type + use GenServer + require Logger + alias __MODULE__ + import Kernel, except: [to_string: 1] + + defstruct node: nil, time: 0, sq: 0 + + @doc "Converts a binary Flake to a String" + def to_string(<<0::integer-size(64), id::integer-size(64)>>) do + Kernel.to_string(id) + end + + def to_string(flake = <<_::integer-size(64), _::integer-size(48), _::integer-size(16)>>) do + encode_base62(flake) + end + + def to_string(s), do: s + + def from_string(int) when is_integer(int) do + from_string(Kernel.to_string(int)) + end + + for i <- [-1, 0] do + def from_string(unquote(i)), do: <<0::integer-size(128)>> + def from_string(unquote(Kernel.to_string(i))), do: <<0::integer-size(128)>> + end + + def from_string(flake = <<_::integer-size(128)>>), do: flake + + def from_string(string) when is_binary(string) and byte_size(string) < 18 do + case Integer.parse(string) do + {id, _} -> <<0::integer-size(64), id::integer-size(64)>> + _ -> nil + end + end + + def from_string(string) do + string |> decode_base62 |> from_integer + end + + def to_integer(<<integer::integer-size(128)>>), do: integer + + def from_integer(integer) do + <<_time::integer-size(64), _node::integer-size(48), _seq::integer-size(16)>> = + <<integer::integer-size(128)>> + end + + @doc "Generates a Flake" + @spec get :: binary + def get, do: to_string(:gen_server.call(:flake, :get)) + + # -- Ecto.Type API + @impl Ecto.Type + def type, do: :uuid + + @impl Ecto.Type + def cast(value) do + {:ok, FlakeId.to_string(value)} + end + + @impl Ecto.Type + def load(value) do + {:ok, FlakeId.to_string(value)} + end + + @impl Ecto.Type + def dump(value) do + {:ok, FlakeId.from_string(value)} + end + + def autogenerate(), do: get() + + # -- GenServer API + def start_link do + :gen_server.start_link({:local, :flake}, __MODULE__, [], []) + end + + @impl GenServer + def init([]) do + {:ok, %FlakeId{node: worker_id(), time: time()}} + end + + @impl GenServer + def handle_call(:get, _from, state) do + {flake, new_state} = get(time(), state) + {:reply, flake, new_state} + end + + # Matches when the calling time is the same as the state time. Incr. sq + defp get(time, %FlakeId{time: time, node: node, sq: seq}) do + new_state = %FlakeId{time: time, node: node, sq: seq + 1} + {gen_flake(new_state), new_state} + end + + # Matches when the times are different, reset sq + defp get(newtime, %FlakeId{time: time, node: node}) when newtime > time do + new_state = %FlakeId{time: newtime, node: node, sq: 0} + {gen_flake(new_state), new_state} + end + + # Error when clock is running backwards + defp get(newtime, %FlakeId{time: time}) when newtime < time do + {:error, :clock_running_backwards} + end + + defp gen_flake(%FlakeId{time: time, node: node, sq: seq}) do + <<time::integer-size(64), node::integer-size(48), seq::integer-size(16)>> + end + + defp nthchar_base62(n) when n <= 9, do: ?0 + n + defp nthchar_base62(n) when n <= 35, do: ?A + n - 10 + defp nthchar_base62(n), do: ?a + n - 36 + + defp encode_base62(<<integer::integer-size(128)>>) do + integer + |> encode_base62([]) + |> List.to_string() + end + + defp encode_base62(int, acc) when int < 0, do: encode_base62(-int, acc) + defp encode_base62(int, []) when int == 0, do: '0' + defp encode_base62(int, acc) when int == 0, do: acc + + defp encode_base62(int, acc) do + r = rem(int, 62) + id = div(int, 62) + acc = [nthchar_base62(r) | acc] + encode_base62(id, acc) + end + + defp decode_base62(s) do + decode_base62(String.to_charlist(s), 0) + end + + defp decode_base62([c | cs], acc) when c >= ?0 and c <= ?9, + do: decode_base62(cs, 62 * acc + (c - ?0)) + + defp decode_base62([c | cs], acc) when c >= ?A and c <= ?Z, + do: decode_base62(cs, 62 * acc + (c - ?A + 10)) + + defp decode_base62([c | cs], acc) when c >= ?a and c <= ?z, + do: decode_base62(cs, 62 * acc + (c - ?a + 36)) + + defp decode_base62([], acc), do: acc + + defp time do + {mega_seconds, seconds, micro_seconds} = :erlang.timestamp() + 1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000) + end + + defp worker_id() do + <<worker::integer-size(48)>> = :crypto.strong_rand_bytes(6) + worker + end +end diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index 37737853a..386096a52 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -43,7 +43,7 @@ defmodule Pleroma.Formatter do def emojify(text, nil), do: text - def emojify(text, emoji) do + def emojify(text, emoji, strip \\ false) do Enum.reduce(emoji, text, fn {emoji, file}, text -> emoji = HTML.strip_tags(emoji) file = HTML.strip_tags(file) @@ -51,14 +51,24 @@ defmodule Pleroma.Formatter do String.replace( text, ":#{emoji}:", - "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{ - MediaProxy.url(file) - }' />" + if not strip do + "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{ + MediaProxy.url(file) + }' />" + else + "" + end ) |> HTML.filter_tags() end) end + def demojify(text) do + emojify(text, Emoji.get_all(), true) + end + + def demojify(text, nil), do: text + def get_emoji(text) when is_binary(text) do Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end) end @@ -189,4 +199,16 @@ defmodule Pleroma.Formatter do String.replace(result_text, uuid, replacement) end) end + + def truncate(text, max_length \\ 200, omission \\ "...") do + # Remove trailing whitespace + text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}") + + if String.length(text) < max_length do + text + else + length_with_omission = max_length - String.length(omission) + String.slice(text, 0, length_with_omission) <> omission + end + end end diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index f5c6e5033..fb602d6b6 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -58,6 +58,20 @@ defmodule Pleroma.HTML do "#{signature}#{to_string(scrubber)}" end) end + + def extract_first_external_url(object, content) do + key = "URL|#{object.id}" + + Cachex.fetch!(:scrubber_cache, key, fn _key -> + result = + content + |> Floki.filter_out("a.mention") + |> Floki.attribute("a", "href") + |> Enum.at(0) + + {:commit, result} + end) + end end defmodule Pleroma.HTML.Scrubber.TwitterText do diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index a75dc006e..ca66c6916 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -8,7 +8,7 @@ defmodule Pleroma.List do alias Pleroma.{User, Repo, Activity} schema "lists" do - belongs_to(:user, Pleroma.User) + belongs_to(:user, User, type: Pleroma.FlakeId) field(:title, :string) field(:following, {:array, :string}, default: []) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index c7d01f63b..2364d36da 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -4,13 +4,14 @@ defmodule Pleroma.Notification do use Ecto.Schema - alias Pleroma.{User, Activity, Notification, Repo, Object} + alias Pleroma.{User, Activity, Notification, Repo} + alias Pleroma.Web.CommonAPI.Utils import Ecto.Query schema "notifications" do field(:seen, :boolean, default: false) - belongs_to(:user, Pleroma.User) - belongs_to(:activity, Pleroma.Activity) + belongs_to(:user, User, type: Pleroma.FlakeId) + belongs_to(:activity, Activity, type: Pleroma.FlakeId) timestamps() end @@ -34,7 +35,8 @@ defmodule Pleroma.Notification do n in Notification, where: n.user_id == ^user.id, order_by: [desc: n.id], - preload: [:activity], + join: activity in assoc(n, :activity), + preload: [activity: activity], limit: 20 ) @@ -65,7 +67,8 @@ defmodule Pleroma.Notification do from( n in Notification, where: n.id == ^id, - preload: [:activity] + join: activity in assoc(n, :activity), + preload: [activity: activity] ) notification = Repo.one(query) @@ -96,7 +99,7 @@ defmodule Pleroma.Notification do end end - def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity) + def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity) when type in ["Create", "Like", "Announce", "Follow"] do users = get_notified_from_activity(activity) @@ -132,54 +135,12 @@ defmodule Pleroma.Notification do when type in ["Create", "Like", "Announce", "Follow"] do recipients = [] - |> maybe_notify_to_recipients(activity) - |> maybe_notify_mentioned_recipients(activity) + |> Utils.maybe_notify_to_recipients(activity) + |> Utils.maybe_notify_mentioned_recipients(activity) |> Enum.uniq() User.get_users_from_set(recipients, local_only) end def get_notified_from_activity(_, _local_only), do: [] - - defp maybe_notify_to_recipients( - recipients, - %Activity{data: %{"to" => to, "type" => _type}} = _activity - ) do - recipients ++ to - end - - defp maybe_notify_mentioned_recipients( - recipients, - %Activity{data: %{"to" => _to, "type" => type} = data} = _activity - ) - when type == "Create" do - object = Object.normalize(data["object"]) - - object_data = - cond do - !is_nil(object) -> - object.data - - is_map(data["object"]) -> - data["object"] - - true -> - %{} - end - - tagged_mentions = maybe_extract_mentions(object_data) - - recipients ++ tagged_mentions - end - - defp maybe_notify_mentioned_recipients(recipients, _), do: recipients - - defp maybe_extract_mentions(%{"tag" => tag}) do - tag - |> Enum.filter(fn x -> is_map(x) end) - |> Enum.filter(fn x -> x["type"] == "Mention" end) - |> Enum.map(fn x -> x["href"] end) - end - - defp maybe_extract_mentions(_), do: [] end diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex index 437aa95b3..945a1d49f 100644 --- a/lib/pleroma/plugs/oauth_plug.ex +++ b/lib/pleroma/plugs/oauth_plug.ex @@ -33,7 +33,12 @@ defmodule Pleroma.Plugs.OAuthPlug do # @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil defp fetch_user_and_token(token) do - query = from(q in Token, where: q.token == ^token, preload: [:user]) + query = + from(t in Token, + where: t.token == ^token, + join: user in assoc(t, :user), + preload: [user: user] + ) with %Token{user: %{info: %{deactivated: false} = _} = user} = token_record <- Repo.one(query) do {:ok, user, token_record} diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 18137106e..1468cc133 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -17,6 +17,8 @@ defmodule Pleroma.User do @type t :: %__MODULE__{} + @primary_key {:id, Pleroma.FlakeId, autogenerate: true} + @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/ @@ -404,6 +406,10 @@ defmodule Pleroma.User do user.info.locked || false end + def get_by_id(id) do + Repo.get_by(User, id: id) + end + def get_by_ap_id(ap_id) do Repo.get_by(User, ap_id: ap_id) end @@ -439,11 +445,33 @@ defmodule Pleroma.User do Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end) end + def get_cached_by_id(id) do + key = "id:#{id}" + + ap_id = + Cachex.fetch!(:user_cache, key, fn _ -> + user = get_by_id(id) + + if user do + Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user) + {:commit, user.ap_id} + else + {:ignore, ""} + end + end) + + get_cached_by_ap_id(ap_id) + end + def get_cached_by_nickname(nickname) do key = "nickname:#{nickname}" Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end) end + def get_cached_by_nickname_or_id(nickname_or_id) do + get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id) + end + def get_by_nickname(nickname) do Repo.get_by(User, nickname: nickname) || if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index fb1791c20..c6c923aac 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -31,7 +31,7 @@ defmodule Pleroma.User.Info do field(:hub, :string, default: nil) field(:salmon, :string, default: nil) field(:hide_network, :boolean, default: false) - field(:pinned_activities, {:array, :integer}, default: []) + field(:pinned_activities, {:array, :string}, default: []) # Found in the wild # ap_id -> Where is this used? diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 6cad02da6..22c7824fa 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -36,6 +36,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do {recipients, to, cc} end + defp get_recipients(%{"type" => "Create"} = data) do + to = data["to"] || [] + cc = data["cc"] || [] + actor = data["actor"] || [] + recipients = (to ++ cc ++ [actor]) |> Enum.uniq() + {recipients, to, cc} + end + defp get_recipients(data) do to = data["to"] || [] cc = data["cc"] || [] @@ -56,7 +64,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end - defp check_remote_limit(%{"object" => %{"content" => content}}) do + defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do limit = Pleroma.Config.get([:instance, :remote_limit]) String.length(content) <= limit end @@ -410,13 +418,42 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> Enum.reverse() end + defp restrict_since(query, %{"since_id" => ""}), do: query + defp restrict_since(query, %{"since_id" => since_id}) do from(activity in query, where: activity.id > ^since_id) end defp restrict_since(query, _), do: query - defp restrict_tag(query, %{"tag" => tag}) do + defp restrict_tag_reject(query, %{"tag_reject" => tag_reject}) + when is_list(tag_reject) and tag_reject != [] do + from( + activity in query, + where: fragment("(not (? #> '{\"object\",\"tag\"}') \\?| ?)", activity.data, ^tag_reject) + ) + end + + defp restrict_tag_reject(query, _), do: query + + defp restrict_tag_all(query, %{"tag_all" => tag_all}) + when is_list(tag_all) and tag_all != [] do + from( + activity in query, + where: fragment("(? #> '{\"object\",\"tag\"}') \\?& ?", activity.data, ^tag_all) + ) + end + + defp restrict_tag_all(query, _), do: query + + defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do + from( + activity in query, + where: fragment("(? #> '{\"object\",\"tag\"}') \\?| ?", activity.data, ^tag) + ) + end + + defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do from( activity in query, where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data) @@ -465,6 +502,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_local(query, _), do: query + defp restrict_max(query, %{"max_id" => ""}), do: query + defp restrict_max(query, %{"max_id" => max_id}) do from(activity in query, where: activity.id < ^max_id) end @@ -563,6 +602,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do base_query |> restrict_recipients(recipients, opts["user"]) |> restrict_tag(opts) + |> restrict_tag_reject(opts) + |> restrict_tag_all(opts) |> restrict_since(opts) |> restrict_local(opts) |> restrict_limit(opts) diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex new file mode 100644 index 000000000..7c6ad582a --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex @@ -0,0 +1,57 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do + alias Pleroma.User + + @behaviour Pleroma.Web.ActivityPub.MRF + + # XXX: this should become User.normalize_by_ap_id() or similar, really. + defp normalize_by_ap_id(%{"id" => id}), do: User.get_cached_by_ap_id(id) + defp normalize_by_ap_id(uri) when is_binary(uri), do: User.get_cached_by_ap_id(uri) + defp normalize_by_ap_id(_), do: nil + + defp score_nickname("followbot@" <> _), do: 1.0 + defp score_nickname("federationbot@" <> _), do: 1.0 + defp score_nickname("federation_bot@" <> _), do: 1.0 + defp score_nickname(_), do: 0.0 + + defp score_displayname("federation bot"), do: 1.0 + defp score_displayname("federationbot"), do: 1.0 + defp score_displayname("fedibot"), do: 1.0 + defp score_displayname(_), do: 0.0 + + defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do + nick_score = + nickname + |> String.downcase() + |> score_nickname() + + name_score = + displayname + |> String.downcase() + |> score_displayname() + + nick_score + name_score + end + + defp determine_if_followbot(_), do: 0.0 + + @impl true + def filter(%{"type" => "Follow", "actor" => actor_id} = message) do + %User{} = actor = normalize_by_ap_id(actor_id) + + score = determine_if_followbot(actor) + + # TODO: scan biography data for keywords and score it somehow. + if score < 0.8 do + {:ok, message} + else + {:reject, nil} + end + end + + @impl true + def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 46b1646f7..c2ced51d8 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -141,11 +141,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> Map.put("actor", get_actor(%{"actor" => actor})) end - def fix_likes(%{"likes" => likes} = object) - when is_bitstring(likes) do - # Check for standardisation - # This is what Peertube does - # curl -H 'Accept: application/activity+json' $likes | jq .totalItems + # Check for standardisation + # This is what Peertube does + # curl -H 'Accept: application/activity+json' $likes | jq .totalItems + # Prismo returns only an integer (count) as "likes" + def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do object |> Map.put("likes", []) |> Map.put("like_count", 0) @@ -900,15 +900,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do maybe_retire_websub(user.ap_id) - # Only do this for recent activties, don't go through the whole db. - # Only look at the last 1000 activities. - since = (Repo.aggregate(Activity, :max, :id) || 0) - 1_000 - q = from( a in Activity, where: ^old_follower_address in a.recipients, - where: a.id > ^since, update: [ set: [ recipients: diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index fe8248107..dcf681b6d 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -160,7 +160,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do "partOf" => iri, "totalItems" => info.note_count, "orderedItems" => collection, - "next" => "#{iri}?max_id=#{min_id - 1}" + "next" => "#{iri}?max_id=#{min_id}" } if max_qid == nil do @@ -207,7 +207,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do "partOf" => iri, "totalItems" => -1, "orderedItems" => collection, - "next" => "#{iri}?max_id=#{min_id - 1}" + "next" => "#{iri}?max_id=#{min_id}" } if max_qid == nil do diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index a0f59d900..208677bd7 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -261,4 +261,46 @@ defmodule Pleroma.Web.CommonAPI.Utils do } end) end + + def maybe_notify_to_recipients( + recipients, + %Activity{data: %{"to" => to, "type" => _type}} = _activity + ) do + recipients ++ to + end + + def maybe_notify_mentioned_recipients( + recipients, + %Activity{data: %{"to" => _to, "type" => type} = data} = _activity + ) + when type == "Create" do + object = Object.normalize(data["object"]) + + object_data = + cond do + !is_nil(object) -> + object.data + + is_map(data["object"]) -> + data["object"] + + true -> + %{} + end + + tagged_mentions = maybe_extract_mentions(object_data) + + recipients ++ tagged_mentions + end + + def maybe_notify_mentioned_recipients(recipients, _), do: recipients + + def maybe_extract_mentions(%{"tag" => tag}) do + tag + |> Enum.filter(fn x -> is_map(x) end) + |> Enum.filter(fn x -> x["type"] == "Mention" end) + |> Enum.map(fn x -> x["href"] end) + end + + def maybe_extract_mentions(_), do: [] end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index f4736fcb5..a366a149f 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do use Pleroma.Web, :controller alias Pleroma.{Repo, Object, Activity, User, Notification, Stats} alias Pleroma.Web + alias Pleroma.HTML alias Pleroma.Web.MastodonAPI.{ StatusView, @@ -540,15 +541,34 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do local_only = params["local"] in [true, "True", "true", "1"] - params = + tags = + [params["tag"], params["any"]] + |> List.flatten() + |> Enum.uniq() + |> Enum.filter(& &1) + |> Enum.map(&String.downcase(&1)) + + tag_all = + params["all"] || + [] + |> Enum.map(&String.downcase(&1)) + + tag_reject = + params["none"] || + [] + |> Enum.map(&String.downcase(&1)) + + query_params = params |> Map.put("type", "Create") |> Map.put("local_only", local_only) |> Map.put("blocking_user", user) - |> Map.put("tag", String.downcase(params["tag"])) + |> Map.put("tag", tags) + |> Map.put("tag_all", tag_all) + |> Map.put("tag_reject", tag_reject) activities = - ActivityPub.fetch_public_activities(params) + ActivityPub.fetch_public_activities(query_params) |> Enum.reverse() conn @@ -1322,6 +1342,29 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end + def get_status_card(status_id) do + with %Activity{} = activity <- Repo.get(Activity, status_id), + true <- ActivityPub.is_public?(activity), + %Object{} = object <- Object.normalize(activity.data["object"]), + page_url <- HTML.extract_first_external_url(object, object.data["content"]), + {:ok, rich_media} <- Pleroma.Web.RichMedia.Parser.parse(page_url) do + page_url = rich_media[:url] || page_url + site_name = rich_media[:site_name] || URI.parse(page_url).host + + rich_media + |> Map.take([:image, :title, :description]) + |> Map.put(:type, "link") + |> Map.put(:provider_name, site_name) + |> Map.put(:url, page_url) + else + _ -> %{} + end + end + + def status_card(conn, %{"id" => status_id}) do + json(conn, get_status_card(status_id)) + end + def try_render(conn, target, params) when is_binary(target) do res = render(conn, target, params) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index bfd6b8b22..0ba4289da 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -112,7 +112,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do # Pleroma extension pleroma: %{ confirmation_pending: user_info.confirmation_pending, - tags: user.tags + tags: user.tags, + is_moderator: user.info.is_moderator, + is_admin: user.info.is_admin } } end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 7a384e941..ddfe6788c 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -49,12 +49,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do replied_to_activities = get_replied_to_activities(opts.activities) opts.activities - |> render_many( + |> safe_render_many( StatusView, "status.json", Map.put(opts, :replied_to_activities, replied_to_activities) ) - |> Enum.filter(fn x -> not is_nil(x) end) end def render( diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex new file mode 100644 index 000000000..8761260f2 --- /dev/null +++ b/lib/pleroma/web/metadata.ex @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata do + alias Phoenix.HTML + + def build_tags(params) do + Enum.reduce(Pleroma.Config.get([__MODULE__, :providers], []), "", fn parser, acc -> + rendered_html = + params + |> parser.build_tags() + |> Enum.map(&to_tag/1) + |> Enum.map(&HTML.safe_to_string/1) + |> Enum.join() + + acc <> rendered_html + end) + end + + def to_tag(data) do + with {name, attrs, _content = []} <- data do + HTML.Tag.tag(name, attrs) + else + {name, attrs, content} -> + HTML.Tag.content_tag(name, content, attrs) + + _ -> + raise ArgumentError, message: "make_tag invalid args" + end + end + + def activity_nsfw?(%{data: %{"sensitive" => sensitive}}) do + Pleroma.Config.get([__MODULE__, :unfurl_nsfw], false) == false and sensitive + end + + def activity_nsfw?(_) do + false + end +end diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex new file mode 100644 index 000000000..30333785e --- /dev/null +++ b/lib/pleroma/web/metadata/opengraph.ex @@ -0,0 +1,154 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.OpenGraph do + alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.Web.Metadata + alias Pleroma.{HTML, Formatter, User} + alias Pleroma.Web.MediaProxy + + @behaviour Provider + + @impl Provider + def build_tags(%{ + object: object, + url: url, + user: user + }) do + attachments = build_attachments(object) + scrubbed_content = scrub_html_and_truncate(object) + # Zero width space + content = + if scrubbed_content != "" and scrubbed_content != "\u200B" do + ": “" <> scrubbed_content <> "”" + else + "" + end + + # Most previews only show og:title which is inconvenient. Instagram + # hacks this by putting the description in the title and making the + # description longer prefixed by how many likes and shares the post + # has. Here we use the descriptive nickname in the title, and expand + # the full account & nickname in the description. We also use the cute^Wevil + # smart quotes around the status text like Instagram, too. + [ + {:meta, + [ + property: "og:title", + content: "#{user.name}" <> content + ], []}, + {:meta, [property: "og:url", content: url], []}, + {:meta, + [ + property: "og:description", + content: "#{user_name_string(user)}" <> content + ], []}, + {:meta, [property: "og:type", content: "website"], []} + ] ++ + if attachments == [] or Metadata.activity_nsfw?(object) do + [ + {:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + ] + else + attachments + end + end + + @impl Provider + def build_tags(%{user: user}) do + with truncated_bio = scrub_html_and_truncate(user.bio || "") do + [ + {:meta, + [ + property: "og:title", + content: user_name_string(user) + ], []}, + {:meta, [property: "og:url", content: User.profile_url(user)], []}, + {:meta, [property: "og:description", content: truncated_bio], []}, + {:meta, [property: "og:type", content: "website"], []}, + {:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + ] + end + end + + defp build_attachments(%{data: %{"attachment" => attachments}}) do + Enum.reduce(attachments, [], fn attachment, acc -> + rendered_tags = + Enum.reduce(attachment["url"], [], fn url, acc -> + media_type = + Enum.find(["image", "audio", "video"], fn media_type -> + String.starts_with?(url["mediaType"], media_type) + end) + + # TODO: Add additional properties to objects when we have the data available. + # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image + # object when a Video or GIF is attached it will display that in the Whatsapp Rich Preview. + case media_type do + "audio" -> + [ + {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []} + | acc + ] + + "image" -> + [ + {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], + []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + | acc + ] + + "video" -> + [ + {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []} + | acc + ] + + _ -> + acc + end + end) + + acc ++ rendered_tags + end) + end + + defp scrub_html_and_truncate(%{data: %{"content" => content}} = object) do + content + # html content comes from DB already encoded, decode first and scrub after + |> HtmlEntities.decode() + |> String.replace(~r/<br\s?\/?>/, " ") + |> HTML.get_cached_stripped_html_for_object(object, __MODULE__) + |> Formatter.demojify() + |> Formatter.truncate() + end + + defp scrub_html_and_truncate(content) when is_binary(content) do + content + # html content comes from DB already encoded, decode first and scrub after + |> HtmlEntities.decode() + |> String.replace(~r/<br\s?\/?>/, " ") + |> HTML.strip_tags() + |> Formatter.demojify() + |> Formatter.truncate() + end + + defp attachment_url(url) do + MediaProxy.url(url) + end + + defp user_name_string(user) do + "#{user.name} " <> + if user.local do + "(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})" + else + "(@#{user.nickname})" + end + end +end diff --git a/lib/pleroma/web/metadata/provider.ex b/lib/pleroma/web/metadata/provider.ex new file mode 100644 index 000000000..197fb2a77 --- /dev/null +++ b/lib/pleroma/web/metadata/provider.ex @@ -0,0 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.Provider do + @callback build_tags(map()) :: list() +end diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex new file mode 100644 index 000000000..32b979357 --- /dev/null +++ b/lib/pleroma/web/metadata/twitter_card.ex @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.TwitterCard do + alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.Web.Metadata + + @behaviour Provider + + @impl Provider + def build_tags(%{object: object}) do + if Metadata.activity_nsfw?(object) or object.data["attachment"] == [] do + build_tags(nil) + else + case find_first_acceptable_media_type(object) do + "image" -> + [{:meta, [property: "twitter:card", content: "summary_large_image"], []}] + + "audio" -> + [{:meta, [property: "twitter:card", content: "player"], []}] + + "video" -> + [{:meta, [property: "twitter:card", content: "player"], []}] + + _ -> + build_tags(nil) + end + end + end + + @impl Provider + def build_tags(_) do + [{:meta, [property: "twitter:card", content: "summary"], []}] + end + + def find_first_acceptable_media_type(%{data: %{"attachment" => attachment}}) do + Enum.find_value(attachment, fn attachment -> + Enum.find_value(attachment["url"], fn url -> + Enum.find(["image", "audio", "video"], fn media_type -> + String.starts_with?(url["mediaType"], media_type) + end) + end) + end) + end +end diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex index cc4b74bc5..f8c65602d 100644 --- a/lib/pleroma/web/oauth/authorization.ex +++ b/lib/pleroma/web/oauth/authorization.ex @@ -14,7 +14,7 @@ defmodule Pleroma.Web.OAuth.Authorization do field(:token, :string) field(:valid_until, :naive_datetime) field(:used, :boolean, default: false) - belongs_to(:user, Pleroma.User) + belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId) belongs_to(:app, App) timestamps() diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex index 1eeda3d24..f0fe3b578 100644 --- a/lib/pleroma/web/oauth/fallback_controller.ex +++ b/lib/pleroma/web/oauth/fallback_controller.ex @@ -9,7 +9,8 @@ defmodule Pleroma.Web.OAuth.FallbackController do # No user/password def call(conn, _) do conn + |> put_status(:unauthorized) |> put_flash(:error, "Invalid Username/Password") - |> OAuthController.authorize(conn.params) + |> OAuthController.authorize(conn.params["authorization"]) end end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index f0ebc63f6..4e01b123b 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -14,7 +14,7 @@ defmodule Pleroma.Web.OAuth.Token do field(:token, :string) field(:refresh_token, :string) field(:valid_until, :naive_datetime) - belongs_to(:user, Pleroma.User) + belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId) belongs_to(:app, App) timestamps() diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index e483447ed..9392a97f0 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -7,7 +7,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.{User, Activity, Object} alias Pleroma.Web.OStatus.{FeedRepresenter, ActivityRepresenter} - alias Pleroma.Repo alias Pleroma.Web.{OStatus, Federator} alias Pleroma.Web.XML alias Pleroma.Web.ActivityPub.ObjectView @@ -22,7 +21,11 @@ defmodule Pleroma.Web.OStatus.OStatusController do def feed_redirect(conn, %{"nickname" => nickname}) do case get_format(conn) do "html" -> - Fallback.RedirectController.redirector(conn, nil) + with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do + Fallback.RedirectController.redirector_with_meta(conn, %{user: user}) + else + nil -> {:error, :not_found} + end "activity+json" -> ActivityPubController.call(conn, :user) @@ -138,24 +141,40 @@ defmodule Pleroma.Web.OStatus.OStatusController do end def notice(conn, %{"id" => id}) do - with {_, %Activity{} = activity} <- {:activity, Repo.get(Activity, id)}, + with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id(id)}, {_, true} <- {:public?, ActivityPub.is_public?(activity)}, %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do case format = get_format(conn) do "html" -> - conn - |> put_resp_content_type("text/html") - |> send_file(200, Pleroma.Plugs.InstanceStatic.file_path("index.html")) + if activity.data["type"] == "Create" do + %Object{} = object = Object.normalize(activity.data["object"]) + + Fallback.RedirectController.redirector_with_meta(conn, %{ + object: object, + url: + Pleroma.Web.Router.Helpers.o_status_url( + Pleroma.Web.Endpoint, + :notice, + activity.id + ), + user: user + }) + else + Fallback.RedirectController.redirector(conn, nil) + end _ -> represent_activity(conn, format, activity, user) end else {:public?, false} -> - {:error, :not_found} + conn + |> put_status(404) + |> Fallback.RedirectController.redirector(nil, 404) {:activity, nil} -> - {:error, :not_found} + conn + |> Fallback.RedirectController.redirector(nil, 404) e -> e diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex index 82b30950c..bd9d9f3a7 100644 --- a/lib/pleroma/web/push/subscription.ex +++ b/lib/pleroma/web/push/subscription.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.Push.Subscription do alias Pleroma.Web.Push.Subscription schema "push_subscriptions" do - belongs_to(:user, User) + belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:token, Token) field(:endpoint, :string) field(:key_p256dh, :string) diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index 6da83c6e4..947dc0c3c 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -5,11 +5,19 @@ defmodule Pleroma.Web.RichMedia.Parser do Pleroma.Web.RichMedia.Parsers.OEmbed ] + def parse(nil), do: {:error, "No URL provided"} + if Mix.env() == :test do def parse(url), do: parse_url(url) else - def parse(url), - do: Cachex.fetch!(:rich_media_cache, url, fn _ -> parse_url(url) end) + def parse(url) do + with {:ok, data} <- Cachex.fetch(:rich_media_cache, url, fn _ -> parse_url(url) end) do + data + else + _e -> + {:error, "Parsing error"} + end + end end defp parse_url(url) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 69ab58c6a..31f739738 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -258,7 +258,7 @@ defmodule Pleroma.Web.Router do get("/statuses/:id", MastodonAPIController, :get_status) get("/statuses/:id/context", MastodonAPIController, :get_context) - get("/statuses/:id/card", MastodonAPIController, :empty_object) + get("/statuses/:id/card", MastodonAPIController, :status_card) get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by) get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by) @@ -396,7 +396,11 @@ defmodule Pleroma.Web.Router do end pipeline :ostatus do - plug(:accepts, ["xml", "atom", "html", "activity+json"]) + plug(:accepts, ["html", "xml", "atom", "activity+json"]) + end + + pipeline :oembed do + plug(:accepts, ["json", "xml"]) end scope "/", Pleroma.Web do @@ -414,6 +418,12 @@ defmodule Pleroma.Web.Router do post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming) end + scope "/", Pleroma.Web do + pipe_through(:oembed) + + get("/oembed", OEmbed.OEmbedController, :url) + end + pipeline :activitypub do plug(:accepts, ["activity+json"]) plug(Pleroma.Web.Plugs.HTTPSignaturePlug) @@ -501,6 +511,7 @@ defmodule Pleroma.Web.Router do scope "/", Fallback do get("/registration/:token", RedirectController, :registration_page) + get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta) get("/*path", RedirectController, :redirector) options("/*path", RedirectController, :empty) @@ -509,11 +520,36 @@ end defmodule Fallback.RedirectController do use Pleroma.Web, :controller + alias Pleroma.Web.Metadata + alias Pleroma.User - def redirector(conn, _params) do + def redirector(conn, _params, code \\ 200) do conn |> put_resp_content_type("text/html") - |> send_file(200, Pleroma.Plugs.InstanceStatic.file_path("index.html")) + |> send_file(code, index_file_path()) + end + + def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do + with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do + redirector_with_meta(conn, %{user: user}) + else + nil -> + redirector(conn, params) + end + end + + def redirector_with_meta(conn, params) do + {:ok, index_content} = File.read(index_file_path()) + tags = Metadata.build_tags(params) + response = String.replace(index_content, "<!--server-generated-meta-->", tags) + + conn + |> put_resp_content_type("text/html") + |> send_resp(200, response) + end + + def index_file_path do + Pleroma.Plugs.InstanceStatic.file_path("index.html") end def registration_page(conn, params) do diff --git a/lib/pleroma/web/twitter_api/representers/activity_representer.ex b/lib/pleroma/web/twitter_api/representers/activity_representer.ex index 4f8f228ab..19b723586 100644 --- a/lib/pleroma/web/twitter_api/representers/activity_representer.ex +++ b/lib/pleroma/web/twitter_api/representers/activity_representer.ex @@ -158,7 +158,9 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do mentions = opts[:mentioned] || [] attentions = - activity.recipients + [] + |> Utils.maybe_notify_to_recipients(activity) + |> Utils.maybe_notify_mentioned_recipients(activity) |> Enum.map(fn ap_id -> Enum.find(mentions, fn user -> ap_id == user.ap_id end) end) |> Enum.filter(& &1) |> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end) diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 8c9060cf2..3064d61ea 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -265,8 +265,6 @@ defmodule Pleroma.Web.TwitterAPI.Controller do end def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do - id = String.to_integer(id) - with context when is_binary(context) <- TwitterAPI.conversation_id_to_context(id), activities <- ActivityPub.fetch_activities_for_context(context, %{ @@ -340,44 +338,47 @@ defmodule Pleroma.Web.TwitterAPI.Controller do end def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)}, - {:ok, activity} <- TwitterAPI.fav(user, id) do + with {:ok, activity} <- TwitterAPI.fav(user, id) do conn |> put_view(ActivityView) |> render("activity.json", %{activity: activity, for: user}) + else + _ -> json_reply(conn, 400, Jason.encode!(%{})) end end def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)}, - {:ok, activity} <- TwitterAPI.unfav(user, id) do + with {:ok, activity} <- TwitterAPI.unfav(user, id) do conn |> put_view(ActivityView) |> render("activity.json", %{activity: activity, for: user}) + else + _ -> json_reply(conn, 400, Jason.encode!(%{})) end end def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)}, - {:ok, activity} <- TwitterAPI.repeat(user, id) do + with {:ok, activity} <- TwitterAPI.repeat(user, id) do conn |> put_view(ActivityView) |> render("activity.json", %{activity: activity, for: user}) + else + _ -> json_reply(conn, 400, Jason.encode!(%{})) end end def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)}, - {:ok, activity} <- TwitterAPI.unrepeat(user, id) do + with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do conn |> put_view(ActivityView) |> render("activity.json", %{activity: activity, for: user}) + else + _ -> json_reply(conn, 400, Jason.encode!(%{})) end end def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)}, - {:ok, activity} <- TwitterAPI.pin(user, id) do + with {:ok, activity} <- TwitterAPI.pin(user, id) do conn |> put_view(ActivityView) |> render("activity.json", %{activity: activity, for: user}) @@ -388,8 +389,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do end def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)}, - {:ok, activity} <- TwitterAPI.unpin(user, id) do + with {:ok, activity} <- TwitterAPI.unpin(user, id) do conn |> put_view(ActivityView) |> render("activity.json", %{activity: activity, for: user}) @@ -556,7 +556,6 @@ defmodule Pleroma.Web.TwitterAPI.Controller do def approve_friend_request(conn, %{"user_id" => uid} = _params) do with followed <- conn.assigns[:user], - uid when is_number(uid) <- String.to_integer(uid), %User{} = follower <- Repo.get(User, uid), {:ok, follower} <- User.maybe_follow(follower, followed), %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), @@ -578,7 +577,6 @@ defmodule Pleroma.Web.TwitterAPI.Controller do def deny_friend_request(conn, %{"user_id" => uid} = _params) do with followed <- conn.assigns[:user], - uid when is_number(uid) <- String.to_integer(uid), %User{} = follower <- Repo.get(User, uid), %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"), diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex index 5eb06a26e..a01ee0010 100644 --- a/lib/pleroma/web/twitter_api/views/activity_view.ex +++ b/lib/pleroma/web/twitter_api/views/activity_view.ex @@ -114,7 +114,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do |> Map.put(:context_ids, context_ids) |> Map.put(:users, users) - render_many( + safe_render_many( opts.activities, ActivityView, "activity.json", @@ -236,7 +236,9 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do pinned = activity.id in user.info.pinned_activities attentions = - activity.recipients + [] + |> Utils.maybe_notify_to_recipients(activity) + |> Utils.maybe_notify_mentioned_recipients(activity) |> Enum.map(fn ap_id -> get_user(ap_id, opts) end) |> Enum.filter(& &1) |> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end) diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex index a8cf83613..15682db8f 100644 --- a/lib/pleroma/web/twitter_api/views/user_view.ex +++ b/lib/pleroma/web/twitter_api/views/user_view.ex @@ -108,6 +108,7 @@ defmodule Pleroma.Web.TwitterAPI.UserView do "locked" => user.info.locked, "default_scope" => user.info.default_scope, "no_rich_text" => user.info.no_rich_text, + "hide_network" => user.info.hide_network, "fields" => fields, # Pleroma extension diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex index 74b13f929..30558e692 100644 --- a/lib/pleroma/web/web.ex +++ b/lib/pleroma/web/web.ex @@ -38,6 +38,33 @@ defmodule Pleroma.Web do import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] import Pleroma.Web.{ErrorHelpers, Gettext, Router.Helpers} + + require Logger + + @doc "Same as `render/3` but wrapped in a rescue block" + def safe_render(view, template, assigns \\ %{}) do + Phoenix.View.render(view, template, assigns) + rescue + error -> + Logger.error( + "#{__MODULE__} failed to render #{inspect({view, template})}: #{inspect(error)}" + ) + + Logger.error(inspect(__STACKTRACE__)) + nil + end + + @doc """ + Same as `render_many/4` but wrapped in rescue block. + """ + def safe_render_many(collection, view, template, assigns \\ %{}) do + Enum.map(collection, fn resource -> + as = Map.get(assigns, :as) || view.__resource__ + assigns = Map.put(assigns, as, resource) + safe_render(view, template, assigns) + end) + |> Enum.filter(& &1) + end end end diff --git a/lib/pleroma/web/websub/websub_client_subscription.ex b/lib/pleroma/web/websub/websub_client_subscription.ex index 105b0069f..969ee0684 100644 --- a/lib/pleroma/web/websub/websub_client_subscription.ex +++ b/lib/pleroma/web/websub/websub_client_subscription.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.Websub.WebsubClientSubscription do field(:state, :string) field(:subscribers, {:array, :string}, default: []) field(:hub, :string) - belongs_to(:user, User) + belongs_to(:user, User, type: Pleroma.FlakeId) timestamps() end |