diff options
Diffstat (limited to 'lib')
56 files changed, 1489 insertions, 522 deletions
| diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 1ba452275..8f8d86a11 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -81,6 +81,14 @@ defmodule Mix.Tasks.Pleroma.Instance do        email = Common.get_option(options, :admin_email, "What is your admin email address?") +      indexable = +        Common.get_option( +          options, +          :indexable, +          "Do you want search engines to index your site? (y/n)", +          "y" +        ) === "y" +        dbhost =          Common.get_option(options, :dbhost, "What is the hostname of your database?", "localhost") @@ -142,6 +150,8 @@ defmodule Mix.Tasks.Pleroma.Instance do        Mix.shell().info("Writing #{psql_path}.")        File.write(psql_path, result_psql) +      write_robots_txt(indexable) +        Mix.shell().info(          "\n" <>            """ @@ -163,4 +173,28 @@ defmodule Mix.Tasks.Pleroma.Instance do        )      end    end + +  defp write_robots_txt(indexable) do +    robots_txt = +      EEx.eval_file( +        Path.expand("robots_txt.eex", __DIR__), +        indexable: indexable +      ) + +    static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/") + +    unless File.exists?(static_dir) do +      File.mkdir_p!(static_dir) +    end + +    robots_txt_path = Path.join(static_dir, "robots.txt") + +    if File.exists?(robots_txt_path) do +      File.cp!(robots_txt_path, "#{robots_txt_path}.bak") +      Mix.shell().info("Backing up existing robots.txt to #{robots_txt_path}.bak") +    end + +    File.write(robots_txt_path, robots_txt) +    Mix.shell().info("Writing #{robots_txt_path}.") +  end  end diff --git a/lib/mix/tasks/pleroma/robots_txt.eex b/lib/mix/tasks/pleroma/robots_txt.eex new file mode 100644 index 000000000..1af3c47ee --- /dev/null +++ b/lib/mix/tasks/pleroma/robots_txt.eex @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: <%= if indexable, do: "", else: "/" %> diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index f6cca0d06..0d0bea8c0 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -6,7 +6,6 @@ defmodule Mix.Tasks.Pleroma.User do    use Mix.Task    import Ecto.Changeset    alias Mix.Tasks.Pleroma.Common -  alias Pleroma.Repo    alias Pleroma.User    @shortdoc "Manages Pleroma users" @@ -23,7 +22,7 @@ defmodule Mix.Tasks.Pleroma.User do    - `--password PASSWORD` - the user's password    - `--moderator`/`--no-moderator` - whether the user is a moderator    - `--admin`/`--no-admin` - whether the user is an admin -  - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions  +  - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions    ## Generate an invite link. @@ -33,6 +32,10 @@ defmodule Mix.Tasks.Pleroma.User do        mix pleroma.user rm NICKNAME +  ## Delete the user's activities. + +      mix pleroma.user delete_activities NICKNAME +    ## Deactivate or activate the user's account.        mix pleroma.user toggle_activated NICKNAME @@ -202,7 +205,7 @@ defmodule Mix.Tasks.Pleroma.User do        {:ok, friends} = User.get_friends(user)        Enum.each(friends, fn friend -> -        user = Repo.get(User, user.id) +        user = User.get_by_id(user.id)          Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}")          User.unfollow(user, friend) @@ -210,7 +213,7 @@ defmodule Mix.Tasks.Pleroma.User do        :timer.sleep(500) -      user = Repo.get(User, user.id) +      user = User.get_by_id(user.id)        if Enum.empty?(user.following) do          Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}") @@ -304,6 +307,18 @@ defmodule Mix.Tasks.Pleroma.User do      end    end +  def run(["delete_activities", nickname]) do +    Common.start_pleroma() + +    with %User{local: true} = user <- User.get_by_nickname(nickname) do +      User.delete_user_activities(user) +      Mix.shell().info("User #{nickname} statuses deleted.") +    else +      _ -> +        Mix.shell().error("No local user #{nickname}") +    end +  end +    defp set_moderator(user, value) do      info_cng = User.Info.admin_api_update(user.info, %{is_moderator: value}) diff --git a/lib/pleroma/PasswordResetToken.ex b/lib/pleroma/PasswordResetToken.ex index 772c239a1..7afbc8751 100644 --- a/lib/pleroma/PasswordResetToken.ex +++ b/lib/pleroma/PasswordResetToken.ex @@ -39,7 +39,7 @@ defmodule Pleroma.PasswordResetToken do    def reset_password(token, data) do      with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), -         %User{} = user <- Repo.get(User, token.user_id), +         %User{} = user <- User.get_by_id(token.user_id),           {:ok, _user} <- User.reset_password(user, data),           {:ok, token} <- Repo.update(used_changeset(token)) do        {:ok, token} diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index bc3f8caba..ab8861b27 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -31,7 +31,7 @@ defmodule Pleroma.Activity do      field(:data, :map)      field(:local, :boolean, default: true)      field(:actor, :string) -    field(:recipients, {:array, :string}) +    field(:recipients, {:array, :string}, default: [])      has_many(:notifications, Notification, on_delete: :delete_all)      # Attention: this is a fake relation, don't try to preload it blindly and expect it to work! diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index cc81e1805..eeb415084 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -25,6 +25,7 @@ defmodule Pleroma.Application do      import Cachex.Spec      Pleroma.Config.DeprecationWarnings.warn() +    setup_instrumenters()      # Define workers and child supervisors to be supervised      children = @@ -103,15 +104,15 @@ defmodule Pleroma.Application do            ],            id: :cachex_idem          ), -        worker(Pleroma.FlakeId, []) +        worker(Pleroma.FlakeId, []), +        worker(Pleroma.ScheduledActivityWorker, [])        ] ++          hackney_pool_children() ++          [            worker(Pleroma.Web.Federator.RetryQueue, []),            worker(Pleroma.Stats, []), -          worker(Pleroma.Web.Push, []), -          worker(Pleroma.Jobs, []), -          worker(Task, [&Pleroma.Web.Federator.init/0], restart: :temporary) +          worker(Task, [&Pleroma.Web.Push.init/0], restart: :temporary, id: :web_push_init), +          worker(Task, [&Pleroma.Web.Federator.init/0], restart: :temporary, id: :federator_init)          ] ++          streamer_child() ++          chat_child() ++ @@ -127,6 +128,24 @@ defmodule Pleroma.Application do      Supervisor.start_link(children, opts)    end +  defp setup_instrumenters do +    require Prometheus.Registry + +    :ok = +      :telemetry.attach( +        "prometheus-ecto", +        [:pleroma, :repo, :query], +        &Pleroma.Repo.Instrumenter.handle_event/4, +        %{} +      ) + +    Prometheus.Registry.register_collector(:prometheus_process_collector) +    Pleroma.Web.Endpoint.MetricsExporter.setup() +    Pleroma.Web.Endpoint.PipelineInstrumenter.setup() +    Pleroma.Web.Endpoint.Instrumenter.setup() +    Pleroma.Repo.Instrumenter.setup() +  end +    def enabled_hackney_pools do      [:media] ++        if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex index 21507cd38..189faa15f 100644 --- a/lib/pleroma/config.ex +++ b/lib/pleroma/config.ex @@ -57,4 +57,8 @@ defmodule Pleroma.Config do    def delete(key) do      Application.delete_env(:pleroma, key)    end + +  def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], []) + +  def oauth_consumer_enabled?, do: oauth_consumer_strategies() != []  end diff --git a/lib/pleroma/emails/mailer.ex b/lib/pleroma/emails/mailer.ex index f7e3aa78b..b384e6fec 100644 --- a/lib/pleroma/emails/mailer.ex +++ b/lib/pleroma/emails/mailer.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Mailer do    use Swoosh.Mailer, otp_app: :pleroma    def deliver_async(email, config \\ []) do -    Pleroma.Jobs.enqueue(:mailer, __MODULE__, [:deliver_async, email, config]) +    PleromaJobQueue.enqueue(:mailer, __MODULE__, [:deliver_async, email, config])    end    def perform(:deliver_async, email, config), do: deliver(email, config) diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index f3f08cd9d..87c7f2cec 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -8,13 +8,19 @@ defmodule Pleroma.Emoji do      * the built-in Finmojis (if enabled in configuration),      * the files: `config/emoji.txt` and `config/custom_emoji.txt` -    * glob paths +    * glob paths, nested folder is used as tag name for grouping e.g. priv/static/emoji/custom/nested_folder    This GenServer stores in an ETS table the list of the loaded emojis, and also allows to reload the list at runtime.    """    use GenServer + +  @type pattern :: Regex.t() | module() | String.t() +  @type patterns :: pattern() | [pattern()] +  @type group_patterns :: keyword(patterns()) +    @ets __MODULE__.Ets    @ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}] +  @groups Application.get_env(:pleroma, :emoji)[:groups]    @doc false    def start_link do @@ -73,13 +79,14 @@ defmodule Pleroma.Emoji do    end    defp load do +    finmoji_enabled = Keyword.get(Application.get_env(:pleroma, :instance), :finmoji_enabled) +    shortcode_globs = Application.get_env(:pleroma, :emoji)[:shortcode_globs] || [] +      emojis = -      (load_finmoji(Keyword.get(Application.get_env(:pleroma, :instance), :finmoji_enabled)) ++ +      (load_finmoji(finmoji_enabled) ++           load_from_file("config/emoji.txt") ++           load_from_file("config/custom_emoji.txt") ++ -         load_from_globs( -           Keyword.get(Application.get_env(:pleroma, :emoji, []), :shortcode_globs, []) -         )) +         load_from_globs(shortcode_globs))        |> Enum.reject(fn value -> value == nil end)      true = :ets.insert(@ets, emojis) @@ -151,9 +158,12 @@ defmodule Pleroma.Emoji do      "white_nights",      "woollysocks"    ] +    defp load_finmoji(true) do      Enum.map(@finmoji, fn finmoji -> -      {finmoji, "/finmoji/128px/#{finmoji}-128.png"} +      file_name = "/finmoji/128px/#{finmoji}-128.png" +      group = match_extra(@groups, file_name) +      {finmoji, file_name, to_string(group)}      end)    end @@ -172,8 +182,14 @@ defmodule Pleroma.Emoji do      |> Stream.map(&String.trim/1)      |> Stream.map(fn line ->        case String.split(line, ~r/,\s*/) do -        [name, file] -> {name, file} -        _ -> nil +        [name, file, tags] -> +          {name, file, tags} + +        [name, file] -> +          {name, file, to_string(match_extra(@groups, file))} + +        _ -> +          nil        end      end)      |> Enum.to_list() @@ -190,9 +206,40 @@ defmodule Pleroma.Emoji do        |> Enum.concat()      Enum.map(paths, fn path -> +      tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path)))        shortcode = Path.basename(path, Path.extname(path))        external_path = Path.join("/", Path.relative_to(path, static_path)) -      {shortcode, external_path} +      {shortcode, external_path, to_string(tag)} +    end) +  end + +  @doc """ +  Finds a matching group for the given emoji filename +  """ +  @spec match_extra(group_patterns(), String.t()) :: atom() | nil +  def match_extra(group_patterns, filename) do +    match_group_patterns(group_patterns, fn pattern -> +      case pattern do +        %Regex{} = regex -> Regex.match?(regex, filename) +        string when is_binary(string) -> filename == string +      end +    end) +  end + +  defp match_group_patterns(group_patterns, matcher) do +    Enum.find_value(group_patterns, fn {group, patterns} -> +      patterns = +        patterns +        |> List.wrap() +        |> Enum.map(fn pattern -> +          if String.contains?(pattern, "*") do +            ~r(#{String.replace(pattern, "*", ".*")}) +          else +            pattern +          end +        end) + +      Enum.any?(patterns, matcher) && group      end)    end  end diff --git a/lib/pleroma/flake_id.ex b/lib/pleroma/flake_id.ex index 4259d5718..58ab3650d 100644 --- a/lib/pleroma/flake_id.ex +++ b/lib/pleroma/flake_id.ex @@ -46,7 +46,7 @@ defmodule Pleroma.FlakeId do    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)>> +      {id, ""} -> <<0::integer-size(64), id::integer-size(64)>>        _ -> nil      end    end diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index e3625383b..8ea9dbd38 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -77,9 +77,9 @@ defmodule Pleroma.Formatter do    def emojify(text, nil), do: text    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) +    Enum.reduce(emoji, text, fn emoji_data, text -> +      emoji = HTML.strip_tags(elem(emoji_data, 0)) +      file = HTML.strip_tags(elem(emoji_data, 1))        html =          if not strip do @@ -101,7 +101,7 @@ defmodule Pleroma.Formatter do    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) +    Enum.filter(Emoji.get_all(), fn {emoji, _, _} -> String.contains?(text, ":#{emoji}:") end)    end    def get_emoji(_), do: [] diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex index 3b9629d77..6a56a6f67 100644 --- a/lib/pleroma/gopher/server.ex +++ b/lib/pleroma/gopher/server.ex @@ -38,7 +38,6 @@ end  defmodule Pleroma.Gopher.Server.ProtocolHandler do    alias Pleroma.Activity    alias Pleroma.HTML -  alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Visibility @@ -111,7 +110,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do    end    def response("/notices/" <> id) do -    with %Activity{} = activity <- Repo.get(Activity, id), +    with %Activity{} = activity <- Activity.get_by_id(id),           true <- Visibility.is_public?(activity) do        activities =          ActivityPub.fetch_activities_for_context(activity.data["context"]) diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 5b152d926..7f1dbe28c 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -28,27 +28,39 @@ defmodule Pleroma.HTML do    def filter_tags(html), do: filter_tags(html, nil)    def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags) -  def get_cached_scrubbed_html_for_object(content, scrubbers, object, module) do -    key = "#{module}#{generate_scrubber_signature(scrubbers)}|#{object.id}" -    Cachex.fetch!(:scrubber_cache, key, fn _key -> ensure_scrubbed_html(content, scrubbers) end) +  def get_cached_scrubbed_html_for_activity(content, scrubbers, activity, key \\ "") do +    key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}" + +    Cachex.fetch!(:scrubber_cache, key, fn _key -> +      ensure_scrubbed_html(content, scrubbers, activity.data["object"]["fake"] || false) +    end)    end -  def get_cached_stripped_html_for_object(content, object, module) do -    get_cached_scrubbed_html_for_object( +  def get_cached_stripped_html_for_activity(content, activity, key) do +    get_cached_scrubbed_html_for_activity(        content,        HtmlSanitizeEx.Scrubber.StripTags, -      object, -      module +      activity, +      key      )    end    def ensure_scrubbed_html(          content, -        scrubbers +        scrubbers, +        false = _fake        ) do      {:commit, filter_tags(content, scrubbers)}    end +  def ensure_scrubbed_html( +        content, +        scrubbers, +        true = _fake +      ) do +    {:ignore, filter_tags(content, scrubbers)} +  end +    defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do      generate_scrubber_signature([scrubber])    end diff --git a/lib/pleroma/jobs.ex b/lib/pleroma/jobs.ex deleted file mode 100644 index 24b7e5e46..000000000 --- a/lib/pleroma/jobs.ex +++ /dev/null @@ -1,152 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Jobs do -  @moduledoc """ -  A basic job queue -  """ -  use GenServer - -  require Logger - -  def init(args) do -    {:ok, args} -  end - -  def start_link do -    queues = -      Pleroma.Config.get(Pleroma.Jobs) -      |> Enum.map(fn {name, _} -> create_queue(name) end) -      |> Enum.into(%{}) - -    state = %{ -      queues: queues, -      refs: %{} -    } - -    GenServer.start_link(__MODULE__, state, name: __MODULE__) -  end - -  def create_queue(name) do -    {name, {:sets.new(), []}} -  end - -  @doc """ -  Enqueues a job. - -  Returns `:ok`. - -  ## Arguments - -  - `queue_name` - a queue name(must be specified in the config). -  - `mod` - a worker module (must have `perform` function). -  - `args` - a list of arguments for the `perform` function of the worker module. -  - `priority` - a job priority (`0` by default). - -  ## Examples - -  Enqueue `Module.perform/0` with `priority=1`: - -      iex> Pleroma.Jobs.enqueue(:example_queue, Module, []) -      :ok - -  Enqueue `Module.perform(:job_name)` with `priority=5`: - -      iex> Pleroma.Jobs.enqueue(:example_queue, Module, [:job_name], 5) -      :ok - -  Enqueue `Module.perform(:another_job, data)` with `priority=1`: - -      iex> data = "foobar" -      iex> Pleroma.Jobs.enqueue(:example_queue, Module, [:another_job, data]) -      :ok - -  Enqueue `Module.perform(:foobar_job, :foo, :bar, 42)` with `priority=1`: - -      iex> Pleroma.Jobs.enqueue(:example_queue, Module, [:foobar_job, :foo, :bar, 42]) -      :ok - -  """ - -  def enqueue(queue_name, mod, args, priority \\ 1) - -  if Mix.env() == :test do -    def enqueue(_queue_name, mod, args, _priority) do -      apply(mod, :perform, args) -    end -  else -    @spec enqueue(atom(), atom(), [any()], integer()) :: :ok -    def enqueue(queue_name, mod, args, priority) do -      GenServer.cast(__MODULE__, {:enqueue, queue_name, mod, args, priority}) -    end -  end - -  def handle_cast({:enqueue, queue_name, mod, args, priority}, state) do -    {running_jobs, queue} = state[:queues][queue_name] - -    queue = enqueue_sorted(queue, {mod, args}, priority) - -    state = -      state -      |> update_queue(queue_name, {running_jobs, queue}) -      |> maybe_start_job(queue_name, running_jobs, queue) - -    {:noreply, state} -  end - -  def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do -    queue_name = state.refs[ref] - -    {running_jobs, queue} = state[:queues][queue_name] - -    running_jobs = :sets.del_element(ref, running_jobs) - -    state = -      state -      |> remove_ref(ref) -      |> update_queue(queue_name, {running_jobs, queue}) -      |> maybe_start_job(queue_name, running_jobs, queue) - -    {:noreply, state} -  end - -  def maybe_start_job(state, queue_name, running_jobs, queue) do -    if :sets.size(running_jobs) < Pleroma.Config.get([__MODULE__, queue_name, :max_jobs]) && -         queue != [] do -      {{mod, args}, queue} = queue_pop(queue) -      {:ok, pid} = Task.start(fn -> apply(mod, :perform, args) end) -      mref = Process.monitor(pid) - -      state -      |> add_ref(queue_name, mref) -      |> update_queue(queue_name, {:sets.add_element(mref, running_jobs), queue}) -    else -      state -    end -  end - -  def enqueue_sorted(queue, element, priority) do -    [%{item: element, priority: priority} | queue] -    |> Enum.sort_by(fn %{priority: priority} -> priority end) -  end - -  def queue_pop([%{item: element} | queue]) do -    {element, queue} -  end - -  defp add_ref(state, queue_name, ref) do -    refs = Map.put(state[:refs], ref, queue_name) -    Map.put(state, :refs, refs) -  end - -  defp remove_ref(state, ref) do -    refs = Map.delete(state[:refs], ref) -    Map.put(state, :refs, refs) -  end - -  defp update_queue(state, queue_name, data) do -    queues = Map.put(state[:queues], queue_name, data) -    Map.put(state, :queues, queues) -  end -end diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index 55c4cf6df..110be8355 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -80,7 +80,7 @@ defmodule Pleroma.List do    # Get lists to which the account belongs.    def get_lists_account_belongs(%User{} = owner, account_id) do -    user = Repo.get(User, account_id) +    user = User.get_by_id(account_id)      query =        from( diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index 8a670645d..013d62157 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -44,6 +44,11 @@ defmodule Pleroma.Object do    # Use this whenever possible, especially when walking graphs in an O(N) loop!    def normalize(%Activity{object: %Object{} = object}), do: object +  # A hack for fake activities +  def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}) do +    %Object{id: "pleroma:fake_object_id", data: data} +  end +    # Catch and log Object.normalize() calls where the Activity's child object is not    # preloaded.    def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}) do diff --git a/lib/pleroma/plugs/user_fetcher_plug.ex b/lib/pleroma/plugs/user_fetcher_plug.ex index 5a77f6833..4089aa958 100644 --- a/lib/pleroma/plugs/user_fetcher_plug.ex +++ b/lib/pleroma/plugs/user_fetcher_plug.ex @@ -3,9 +3,7 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Plugs.UserFetcherPlug do -  alias Pleroma.Repo    alias Pleroma.User -    import Plug.Conn    def init(options) do @@ -14,26 +12,10 @@ defmodule Pleroma.Plugs.UserFetcherPlug do    def call(conn, _options) do      with %{auth_credentials: %{username: username}} <- conn.assigns, -         {:ok, %User{} = user} <- user_fetcher(username) do -      conn -      |> assign(:auth_user, user) +         %User{} = user <- User.get_by_nickname_or_email(username) do +      assign(conn, :auth_user, user)      else        _ -> conn      end    end - -  defp user_fetcher(username_or_email) do -    { -      :ok, -      cond do -        # First, try logging in as if it was a name -        user = Repo.get_by(User, %{nickname: username_or_email}) -> -          user - -        # If we get nil, we try using it as an email -        user = Repo.get_by(User, %{email: username_or_email}) -> -          user -      end -    } -  end  end diff --git a/lib/pleroma/registration.ex b/lib/pleroma/registration.ex new file mode 100644 index 000000000..21fd1fc3f --- /dev/null +++ b/lib/pleroma/registration.ex @@ -0,0 +1,57 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Registration do +  use Ecto.Schema + +  import Ecto.Changeset + +  alias Pleroma.Registration +  alias Pleroma.Repo +  alias Pleroma.User + +  @primary_key {:id, Pleroma.FlakeId, autogenerate: true} + +  schema "registrations" do +    belongs_to(:user, User, type: Pleroma.FlakeId) +    field(:provider, :string) +    field(:uid, :string) +    field(:info, :map, default: %{}) + +    timestamps() +  end + +  def nickname(registration, default \\ nil), +    do: Map.get(registration.info, "nickname", default) + +  def email(registration, default \\ nil), +    do: Map.get(registration.info, "email", default) + +  def name(registration, default \\ nil), +    do: Map.get(registration.info, "name", default) + +  def description(registration, default \\ nil), +    do: Map.get(registration.info, "description", default) + +  def changeset(registration, params \\ %{}) do +    registration +    |> cast(params, [:user_id, :provider, :uid, :info]) +    |> validate_required([:provider, :uid]) +    |> foreign_key_constraint(:user_id) +    |> unique_constraint(:uid, name: :registrations_provider_uid_index) +  end + +  def bind_to_user(registration, user) do +    registration +    |> changeset(%{user_id: (user && user.id) || nil}) +    |> Repo.update() +  end + +  def get_by_provider_uid(provider, uid) do +    Repo.get_by(Registration, +      provider: to_string(provider), +      uid: to_string(uid) +    ) +  end +end diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex index 4af1bde56..aa5d427ae 100644 --- a/lib/pleroma/repo.ex +++ b/lib/pleroma/repo.ex @@ -8,6 +8,10 @@ defmodule Pleroma.Repo do      adapter: Ecto.Adapters.Postgres,      migration_timestamps: [type: :naive_datetime_usec] +  defmodule Instrumenter do +    use Prometheus.EctoInstrumenter +  end +    @doc """    Dynamically loads the repository url from the    DATABASE_URL environment variable. diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex new file mode 100644 index 000000000..de0e54699 --- /dev/null +++ b/lib/pleroma/scheduled_activity.ex @@ -0,0 +1,161 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ScheduledActivity do +  use Ecto.Schema + +  alias Pleroma.Config +  alias Pleroma.Repo +  alias Pleroma.ScheduledActivity +  alias Pleroma.User +  alias Pleroma.Web.CommonAPI.Utils + +  import Ecto.Query +  import Ecto.Changeset + +  @min_offset :timer.minutes(5) + +  schema "scheduled_activities" do +    belongs_to(:user, User, type: Pleroma.FlakeId) +    field(:scheduled_at, :naive_datetime) +    field(:params, :map) + +    timestamps() +  end + +  def changeset(%ScheduledActivity{} = scheduled_activity, attrs) do +    scheduled_activity +    |> cast(attrs, [:scheduled_at, :params]) +    |> validate_required([:scheduled_at, :params]) +    |> validate_scheduled_at() +    |> with_media_attachments() +  end + +  defp with_media_attachments( +         %{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset +       ) +       when is_list(media_ids) do +    media_attachments = Utils.attachments_from_ids(%{"media_ids" => media_ids}) + +    params = +      params +      |> Map.put("media_attachments", media_attachments) +      |> Map.put("media_ids", media_ids) + +    put_change(changeset, :params, params) +  end + +  defp with_media_attachments(changeset), do: changeset + +  def update_changeset(%ScheduledActivity{} = scheduled_activity, attrs) do +    scheduled_activity +    |> cast(attrs, [:scheduled_at]) +    |> validate_required([:scheduled_at]) +    |> validate_scheduled_at() +  end + +  def validate_scheduled_at(changeset) do +    validate_change(changeset, :scheduled_at, fn _, scheduled_at -> +      cond do +        not far_enough?(scheduled_at) -> +          [scheduled_at: "must be at least 5 minutes from now"] + +        exceeds_daily_user_limit?(changeset.data.user_id, scheduled_at) -> +          [scheduled_at: "daily limit exceeded"] + +        exceeds_total_user_limit?(changeset.data.user_id) -> +          [scheduled_at: "total limit exceeded"] + +        true -> +          [] +      end +    end) +  end + +  def exceeds_daily_user_limit?(user_id, scheduled_at) do +    ScheduledActivity +    |> where(user_id: ^user_id) +    |> where([sa], type(sa.scheduled_at, :date) == type(^scheduled_at, :date)) +    |> select([sa], count(sa.id)) +    |> Repo.one() +    |> Kernel.>=(Config.get([ScheduledActivity, :daily_user_limit])) +  end + +  def exceeds_total_user_limit?(user_id) do +    ScheduledActivity +    |> where(user_id: ^user_id) +    |> select([sa], count(sa.id)) +    |> Repo.one() +    |> Kernel.>=(Config.get([ScheduledActivity, :total_user_limit])) +  end + +  def far_enough?(scheduled_at) when is_binary(scheduled_at) do +    with {:ok, scheduled_at} <- Ecto.Type.cast(:naive_datetime, scheduled_at) do +      far_enough?(scheduled_at) +    else +      _ -> false +    end +  end + +  def far_enough?(scheduled_at) do +    now = NaiveDateTime.utc_now() +    diff = NaiveDateTime.diff(scheduled_at, now, :millisecond) +    diff > @min_offset +  end + +  def new(%User{} = user, attrs) do +    %ScheduledActivity{user_id: user.id} +    |> changeset(attrs) +  end + +  def create(%User{} = user, attrs) do +    user +    |> new(attrs) +    |> Repo.insert() +  end + +  def get(%User{} = user, scheduled_activity_id) do +    ScheduledActivity +    |> where(user_id: ^user.id) +    |> where(id: ^scheduled_activity_id) +    |> Repo.one() +  end + +  def update(%ScheduledActivity{} = scheduled_activity, attrs) do +    scheduled_activity +    |> update_changeset(attrs) +    |> Repo.update() +  end + +  def delete(%ScheduledActivity{} = scheduled_activity) do +    scheduled_activity +    |> Repo.delete() +  end + +  def delete(id) when is_binary(id) or is_integer(id) do +    ScheduledActivity +    |> where(id: ^id) +    |> select([sa], sa) +    |> Repo.delete_all() +    |> case do +      {1, [scheduled_activity]} -> {:ok, scheduled_activity} +      _ -> :error +    end +  end + +  def for_user_query(%User{} = user) do +    ScheduledActivity +    |> where(user_id: ^user.id) +  end + +  def due_activities(offset \\ 0) do +    naive_datetime = +      NaiveDateTime.utc_now() +      |> NaiveDateTime.add(offset, :millisecond) + +    ScheduledActivity +    |> where([sa], sa.scheduled_at < ^naive_datetime) +    |> Repo.all() +  end +end diff --git a/lib/pleroma/scheduled_activity_worker.ex b/lib/pleroma/scheduled_activity_worker.ex new file mode 100644 index 000000000..65b38622f --- /dev/null +++ b/lib/pleroma/scheduled_activity_worker.ex @@ -0,0 +1,58 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ScheduledActivityWorker do +  @moduledoc """ +  Sends scheduled activities to the job queue. +  """ + +  alias Pleroma.Config +  alias Pleroma.ScheduledActivity +  alias Pleroma.User +  alias Pleroma.Web.CommonAPI +  use GenServer +  require Logger + +  @schedule_interval :timer.minutes(1) + +  def start_link do +    GenServer.start_link(__MODULE__, nil) +  end + +  def init(_) do +    if Config.get([ScheduledActivity, :enabled]) do +      schedule_next() +      {:ok, nil} +    else +      :ignore +    end +  end + +  def perform(:execute, scheduled_activity_id) do +    try do +      {:ok, scheduled_activity} = ScheduledActivity.delete(scheduled_activity_id) +      %User{} = user = User.get_cached_by_id(scheduled_activity.user_id) +      {:ok, _result} = CommonAPI.post(user, scheduled_activity.params) +    rescue +      error -> +        Logger.error( +          "#{__MODULE__} Couldn't create a status from the scheduled activity: #{inspect(error)}" +        ) +    end +  end + +  def handle_info(:perform, state) do +    ScheduledActivity.due_activities(@schedule_interval) +    |> Enum.each(fn scheduled_activity -> +      PleromaJobQueue.enqueue(:scheduled_activities, __MODULE__, [:execute, scheduled_activity.id]) +    end) + +    schedule_next() +    {:noreply, state} +  end + +  defp schedule_next do +    Process.send_after(self(), :perform, @schedule_interval) +  end +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 73c2a82a7..ed23b8ef0 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -13,6 +13,7 @@ defmodule Pleroma.User do    alias Pleroma.Formatter    alias Pleroma.Notification    alias Pleroma.Object +  alias Pleroma.Registration    alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web @@ -55,6 +56,7 @@ defmodule Pleroma.User do      field(:bookmarks, {:array, :string}, default: [])      field(:last_refreshed_at, :naive_datetime_usec)      has_many(:notifications, Notification) +    has_many(:registrations, Registration)      embeds_one(:info, Pleroma.User.Info)      timestamps() @@ -216,7 +218,7 @@ defmodule Pleroma.User do      changeset =        struct        |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation]) -      |> validate_required([:email, :name, :nickname, :password, :password_confirmation]) +      |> validate_required([:name, :nickname, :password, :password_confirmation])        |> validate_confirmation(:password)        |> unique_constraint(:email)        |> unique_constraint(:nickname) @@ -227,6 +229,13 @@ defmodule Pleroma.User do        |> validate_length(:name, min: 1, max: 100)        |> put_change(:info, info_change) +    changeset = +      if opts[:external] do +        changeset +      else +        validate_required(changeset, [:email]) +      end +      if changeset.valid? do        hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])        ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]}) @@ -505,11 +514,10 @@ defmodule Pleroma.User do        end    end +  def get_by_email(email), do: Repo.get_by(User, email: email) +    def get_by_nickname_or_email(nickname_or_email) do -    case user = Repo.get_by(User, nickname: nickname_or_email) do -      %User{} -> user -      nil -> Repo.get_by(User, email: nickname_or_email) -    end +    get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)    end    def get_cached_user_info(user) do @@ -937,6 +945,8 @@ defmodule Pleroma.User do        unfollow(blocked, blocker)      end +    {:ok, blocker} = update_follower_count(blocker) +      info_cng =        blocker.info        |> User.Info.add_to_block(ap_id) @@ -1096,28 +1106,27 @@ defmodule Pleroma.User do      # Remove all relationships      {:ok, followers} = User.get_followers(user) -    followers -    |> Enum.each(fn follower -> User.unfollow(follower, user) end) +    Enum.each(followers, fn follower -> User.unfollow(follower, user) end)      {:ok, friends} = User.get_friends(user) -    friends -    |> Enum.each(fn followed -> User.unfollow(user, followed) end) +    Enum.each(friends, fn followed -> User.unfollow(user, followed) end) -    query = -      from(a in Activity, where: a.actor == ^user.ap_id) -      |> Activity.with_preloaded_object() +    delete_user_activities(user) +  end -    Repo.all(query) -    |> Enum.each(fn activity -> -      case activity.data["type"] do -        "Create" -> -          ActivityPub.delete(Object.normalize(activity)) +  def delete_user_activities(%User{ap_id: ap_id} = user) do +    Activity +    |> where(actor: ^ap_id) +    |> Activity.with_preloaded_object() +    |> Repo.all() +    |> Enum.each(fn +      %{data: %{"type" => "Create"}} = activity -> +        activity |> Object.normalize() |> ActivityPub.delete() -        # TODO: Do something with likes, follows, repeats. -        _ -> -          "Doing nothing" -      end +      # TODO: Do something with likes, follows, repeats. +      _ -> +        "Doing nothing"      end)      {:ok, user} @@ -1239,8 +1248,8 @@ defmodule Pleroma.User do    # this is because we have synchronous follow APIs and need to simulate them    # with an async handshake    def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do -    with %User{} = a <- Repo.get(User, a.id), -         %User{} = b <- Repo.get(User, b.id) do +    with %User{} = a <- User.get_by_id(a.id), +         %User{} = b <- User.get_by_id(b.id) do        {:ok, a, b}      else        _e -> @@ -1250,8 +1259,8 @@ defmodule Pleroma.User do    def wait_and_refresh(timeout, %User{} = a, %User{} = b) do      with :ok <- :timer.sleep(timeout), -         %User{} = a <- Repo.get(User, a.id), -         %User{} = b <- Repo.get(User, b.id) do +         %User{} = a <- User.get_by_id(a.id), +         %User{} = b <- User.get_by_id(b.id) do        {:ok, a, b}      else        _e -> diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 6e1ed7ec9..f217e7bac 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -113,15 +113,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    def decrease_replies_count_if_reply(_object), do: :noop -  def insert(map, local \\ true) when is_map(map) do +  def insert(map, local \\ true, fake \\ false) when is_map(map) do      with nil <- Activity.normalize(map), -         map <- lazy_put_activity_defaults(map), +         map <- lazy_put_activity_defaults(map, fake),           :ok <- check_actor_is_active(map["actor"]),           {_, true} <- {:remote_limit_error, check_remote_limit(map)},           {:ok, map} <- MRF.filter(map), +         {recipients, _, _} = get_recipients(map), +         {:fake, false, map, recipients} <- {:fake, fake, map, recipients},           {:ok, object} <- insert_full_object(map) do -      {recipients, _, _} = get_recipients(map) -        {:ok, activity} =          Repo.insert(%Activity{            data: map, @@ -146,8 +146,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        stream_out(activity)        {:ok, activity}      else -      %Activity{} = activity -> {:ok, activity} -      error -> {:error, error} +      %Activity{} = activity -> +        {:ok, activity} + +      {:fake, true, map, recipients} -> +        activity = %Activity{ +          data: map, +          local: local, +          actor: map["actor"], +          recipients: recipients, +          id: "pleroma:fakeid" +        } + +        Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) +        {:ok, activity} + +      error -> +        {:error, error}      end    end @@ -190,7 +205,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  def create(%{to: to, actor: actor, context: context, object: object} = params) do +  def create(%{to: to, actor: actor, context: context, object: object} = params, fake \\ false) do      additional = params[:additional] || %{}      # only accept false as false value      local = !(params[:local] == false) @@ -201,13 +216,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do               %{to: to, actor: actor, published: published, context: context, object: object},               additional             ), -         {:ok, activity} <- insert(create_data, local), +         {:ok, activity} <- insert(create_data, local, fake), +         {:fake, false, activity} <- {:fake, fake, activity},           _ <- increase_replies_count_if_reply(create_data),           # Changing note count prior to enqueuing federation task in order to avoid           # race conditions on updating user.info           {:ok, _actor} <- increase_note_count_if_public(actor, activity),           :ok <- maybe_federate(activity) do        {:ok, activity} +    else +      {:fake, true, activity} -> +        {:ok, activity}      end    end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index f733ae7e1..593ae3188 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -954,7 +954,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    defp strip_internal_tags(object), do: object -  defp user_upgrade_task(user) do +  def perform(:user_upgrade, user) do      # we pass a fake user so that the followers collection is stripped away      old_follower_address = User.ap_followers(%User{nickname: user.nickname}) @@ -999,28 +999,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      Repo.update_all(q, [])    end -  def upgrade_user_from_ap_id(ap_id, async \\ true) do +  def upgrade_user_from_ap_id(ap_id) do      with %User{local: false} = user <- User.get_by_ap_id(ap_id), -         {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do -      already_ap = User.ap_enabled?(user) - -      {:ok, user} = -        User.upgrade_changeset(user, data) -        |> Repo.update() - -      if !already_ap do -        # This could potentially take a long time, do it in the background -        if async do -          Task.start(fn -> -            user_upgrade_task(user) -          end) -        else -          user_upgrade_task(user) -        end +         {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id), +         already_ap <- User.ap_enabled?(user), +         {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do +      unless already_ap do +        PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])        end        {:ok, user}      else +      %User{} = user -> {:ok, user}        e -> e      end    end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 2e9ffe41c..0b53f71c3 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -99,7 +99,10 @@ defmodule Pleroma.Web.ActivityPub.Utils do      %{        "@context" => [          "https://www.w3.org/ns/activitystreams", -        "#{Web.base_url()}/schemas/litepub-0.1.jsonld" +        "#{Web.base_url()}/schemas/litepub-0.1.jsonld", +        %{ +          "@language" => "und" +        }        ]      }    end @@ -175,18 +178,26 @@ defmodule Pleroma.Web.ActivityPub.Utils do    Adds an id and a published data if they aren't there,    also adds it to an included object    """ -  def lazy_put_activity_defaults(map) do -    %{data: %{"id" => context}, id: context_id} = create_context(map["context"]) - +  def lazy_put_activity_defaults(map, fake \\ false) do      map = -      map -      |> Map.put_new_lazy("id", &generate_activity_id/0) -      |> Map.put_new_lazy("published", &make_date/0) -      |> Map.put_new("context", context) -      |> Map.put_new("context_id", context_id) +      unless fake do +        %{data: %{"id" => context}, id: context_id} = create_context(map["context"]) + +        map +        |> Map.put_new_lazy("id", &generate_activity_id/0) +        |> Map.put_new_lazy("published", &make_date/0) +        |> Map.put_new("context", context) +        |> Map.put_new("context_id", context_id) +      else +        map +        |> Map.put_new("id", "pleroma:fakeid") +        |> Map.put_new_lazy("published", &make_date/0) +        |> Map.put_new("context", "pleroma:fakecontext") +        |> Map.put_new("context_id", -1) +      end      if is_map(map["object"]) do -      object = lazy_put_object_defaults(map["object"], map) +      object = lazy_put_object_defaults(map["object"], map, fake)        %{map | "object" => object}      else        map @@ -196,7 +207,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do    @doc """    Adds an id and published date if they aren't there.    """ -  def lazy_put_object_defaults(map, activity \\ %{}) do +  def lazy_put_object_defaults(map, activity \\ %{}, fake) + +  def lazy_put_object_defaults(map, activity, true = _fake) do +    map +    |> Map.put_new_lazy("published", &make_date/0) +    |> Map.put_new("id", "pleroma:fake_object_id") +    |> Map.put_new("context", activity["context"]) +    |> Map.put_new("fake", true) +    |> Map.put_new("context_id", activity["context_id"]) +  end + +  def lazy_put_object_defaults(map, activity, _fake) do      map      |> Map.put_new_lazy("id", &generate_object_id/0)      |> Map.put_new_lazy("published", &make_date/0) @@ -354,7 +376,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do          [state, actor, object]        ) -      activity = Repo.get(Activity, activity.id) +      activity = Activity.get_by_id(activity.id)        {:ok, activity}      rescue        e -> @@ -404,13 +426,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do              activity.data            ),          where: activity.actor == ^follower_id, +        # this is to use the index          where:            fragment( -            "? @> ?", +            "coalesce((?)->'object'->>'id', (?)->>'object') = ?",              activity.data, -            ^%{object: followed_id} +            activity.data, +            ^followed_id            ), -        order_by: [desc: :id], +        order_by: [fragment("? desc nulls last", activity.id)],          limit: 1        ) @@ -567,13 +591,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do              activity.data            ),          where: activity.actor == ^blocker_id, +        # this is to use the index          where:            fragment( -            "? @> ?", +            "coalesce((?)->'object'->>'id', (?)->>'object') = ?", +            activity.data,              activity.data, -            ^%{object: blocked_id} +            ^blocked_id            ), -        order_by: [desc: :id], +        order_by: [fragment("? desc nulls last", activity.id)],          limit: 1        ) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 3fa9c6909..78bf31893 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -25,6 +25,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      |> json(nickname)    end +  def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do +    with %User{} = follower <- User.get_by_nickname(follower_nick), +         %User{} = followed <- User.get_by_nickname(followed_nick) do +      User.follow(follower, followed) +    end + +    conn +    |> json("ok") +  end + +  def user_unfollow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do +    with %User{} = follower <- User.get_by_nickname(follower_nick), +         %User{} = followed <- User.get_by_nickname(followed_nick) do +      User.unfollow(follower, followed) +    end + +    conn +    |> json("ok") +  end +    def user_create(          conn,          %{"nickname" => nickname, "email" => email, "password" => password} @@ -45,6 +65,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      |> json(user.nickname)    end +  def user_show(conn, %{"nickname" => nickname}) do +    with %User{} = user <- User.get_by_nickname(nickname) do +      conn +      |> json(AccountView.render("show.json", %{user: user})) +    else +      _ -> {:error, :not_found} +    end +  end +    def user_toggle_activation(conn, %{"nickname" => nickname}) do      user = User.get_by_nickname(nickname) @@ -231,6 +260,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      |> json(token.token)    end +  def errors(conn, {:error, :not_found}) do +    conn +    |> put_status(404) +    |> json("Not found") +  end +    def errors(conn, {:param_cast, _}) do      conn      |> put_status(400) diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex index 82267c595..89d88af32 100644 --- a/lib/pleroma/web/auth/authenticator.ex +++ b/lib/pleroma/web/auth/authenticator.ex @@ -3,6 +3,7 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.Auth.Authenticator do +  alias Pleroma.Registration    alias Pleroma.User    def implementation do @@ -12,14 +13,33 @@ defmodule Pleroma.Web.Auth.Authenticator do      )    end -  @callback get_user(Plug.Conn.t()) :: {:ok, User.t()} | {:error, any()} -  def get_user(plug), do: implementation().get_user(plug) +  @callback get_user(Plug.Conn.t(), Map.t()) :: {:ok, User.t()} | {:error, any()} +  def get_user(plug, params), do: implementation().get_user(plug, params) + +  @callback create_from_registration(Plug.Conn.t(), Map.t(), Registration.t()) :: +              {:ok, User.t()} | {:error, any()} +  def create_from_registration(plug, params, registration), +    do: implementation().create_from_registration(plug, params, registration) + +  @callback get_registration(Plug.Conn.t(), Map.t()) :: +              {:ok, Registration.t()} | {:error, any()} +  def get_registration(plug, params), +    do: implementation().get_registration(plug, params)    @callback handle_error(Plug.Conn.t(), any()) :: any()    def handle_error(plug, error), do: implementation().handle_error(plug, error)    @callback auth_template() :: String.t() | nil    def auth_template do -    implementation().auth_template() || Pleroma.Config.get(:auth_template, "show.html") +    # Note: `config :pleroma, :auth_template, "..."` support is deprecated +    implementation().auth_template() || +      Pleroma.Config.get([:auth, :auth_template], Pleroma.Config.get(:auth_template)) || +      "show.html" +  end + +  @callback oauth_consumer_template() :: String.t() | nil +  def oauth_consumer_template do +    implementation().oauth_consumer_template() || +      Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html")    end  end diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex index 88217aab8..8b6d5a77f 100644 --- a/lib/pleroma/web/auth/ldap_authenticator.ex +++ b/lib/pleroma/web/auth/ldap_authenticator.ex @@ -8,14 +8,19 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do    require Logger    @behaviour Pleroma.Web.Auth.Authenticator +  @base Pleroma.Web.Auth.PleromaAuthenticator    @connection_timeout 10_000    @search_timeout 10_000 -  def get_user(%Plug.Conn{} = conn) do +  defdelegate get_registration(conn, params), to: @base + +  defdelegate create_from_registration(conn, params, registration), to: @base + +  def get_user(%Plug.Conn{} = conn, params) do      if Pleroma.Config.get([:ldap, :enabled]) do        {name, password} = -        case conn.params do +        case params do            %{"authorization" => %{"name" => name, "password" => password}} ->              {name, password} @@ -29,14 +34,14 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do          {:error, {:ldap_connection_error, _}} ->            # When LDAP is unavailable, try default authenticator -          Pleroma.Web.Auth.PleromaAuthenticator.get_user(conn) +          @base.get_user(conn, params)          error ->            error        end      else        # Fall back to default authenticator -      Pleroma.Web.Auth.PleromaAuthenticator.get_user(conn) +      @base.get_user(conn, params)      end    end @@ -46,6 +51,8 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do    def auth_template, do: nil +  def oauth_consumer_template, do: nil +    defp ldap_user(name, password) do      ldap = Pleroma.Config.get(:ldap, [])      host = Keyword.get(ldap, :host, "localhost") diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index 94a19ad49..c826adb4c 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -4,13 +4,15 @@  defmodule Pleroma.Web.Auth.PleromaAuthenticator do    alias Comeonin.Pbkdf2 +  alias Pleroma.Registration +  alias Pleroma.Repo    alias Pleroma.User    @behaviour Pleroma.Web.Auth.Authenticator -  def get_user(%Plug.Conn{} = conn) do +  def get_user(%Plug.Conn{} = _conn, params) do      {name, password} = -      case conn.params do +      case params do          %{"authorization" => %{"name" => name, "password" => password}} ->            {name, password} @@ -27,9 +29,69 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do      end    end +  def get_registration( +        %Plug.Conn{assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}}, +        _params +      ) do +    registration = Registration.get_by_provider_uid(provider, uid) + +    if registration do +      {:ok, registration} +    else +      info = auth.info + +      Registration.changeset(%Registration{}, %{ +        provider: to_string(provider), +        uid: to_string(uid), +        info: %{ +          "nickname" => info.nickname, +          "email" => info.email, +          "name" => info.name, +          "description" => info.description +        } +      }) +      |> Repo.insert() +    end +  end + +  def get_registration(%Plug.Conn{} = _conn, _params), do: {:error, :missing_credentials} + +  def create_from_registration(_conn, params, registration) do +    nickname = value([params["nickname"], Registration.nickname(registration)]) +    email = value([params["email"], Registration.email(registration)]) +    name = value([params["name"], Registration.name(registration)]) || nickname +    bio = value([params["bio"], Registration.description(registration)]) + +    random_password = :crypto.strong_rand_bytes(64) |> Base.encode64() + +    with {:ok, new_user} <- +           User.register_changeset( +             %User{}, +             %{ +               email: email, +               nickname: nickname, +               name: name, +               bio: bio, +               password: random_password, +               password_confirmation: random_password +             }, +             external: true, +             confirmed: true +           ) +           |> Repo.insert(), +         {:ok, _} <- +           Registration.changeset(registration, %{user_id: new_user.id}) |> Repo.update() do +      {:ok, new_user} +    end +  end + +  defp value(list), do: Enum.find(list, &(to_string(&1) != "")) +    def handle_error(%Plug.Conn{} = _conn, error) do      error    end    def auth_template, do: nil + +  def oauth_consumer_template, do: nil  end diff --git a/lib/pleroma/web/channels/user_socket.ex b/lib/pleroma/web/channels/user_socket.ex index 3a700fa3b..6503979a1 100644 --- a/lib/pleroma/web/channels/user_socket.ex +++ b/lib/pleroma/web/channels/user_socket.ex @@ -24,7 +24,7 @@ defmodule Pleroma.Web.UserSocket do    def connect(%{"token" => token}, socket) do      with true <- Pleroma.Config.get([:chat, :enabled]),           {:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84_600), -         %User{} = user <- Pleroma.Repo.get(User, user_id) do +         %User{} = user <- Pleroma.User.get_by_id(user_id) do        {:ok, assign(socket, :user_name, user.nickname)}      else        _e -> :error diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 25b990677..74babdf14 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -167,18 +167,21 @@ defmodule Pleroma.Web.CommonAPI do               object,               "emoji",               (Formatter.get_emoji(status) ++ Formatter.get_emoji(data["spoiler_text"])) -             |> Enum.reduce(%{}, fn {name, file}, acc -> +             |> Enum.reduce(%{}, fn {name, file, _}, acc ->                 Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")               end)             ) do        res = -        ActivityPub.create(%{ -          to: to, -          actor: user, -          context: context, -          object: object, -          additional: %{"cc" => cc, "directMessage" => visibility == "direct"} -        }) +        ActivityPub.create( +          %{ +            to: to, +            actor: user, +            context: context, +            object: object, +            additional: %{"cc" => cc, "directMessage" => visibility == "direct"} +          }, +          Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false +        )        res      end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index f596f703b..051db6c79 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -15,6 +15,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do    alias Pleroma.Web.Endpoint    alias Pleroma.Web.MediaProxy +  require Logger +    # This is a hack for twidere.    def get_by_id_or_ap_id(id) do      activity = @@ -31,7 +33,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    def get_replied_to_activity(""), do: nil    def get_replied_to_activity(id) when not is_nil(id) do -    Repo.get(Activity, id) +    Activity.get_by_id(id)    end    def get_replied_to_activity(_), do: nil @@ -240,15 +242,21 @@ defmodule Pleroma.Web.CommonAPI.Utils do      Strftime.strftime!(date, "%a %b %d %H:%M:%S %z %Y")    end -  def date_to_asctime(date) do -    with {:ok, date, _offset} <- date |> DateTime.from_iso8601() do +  def date_to_asctime(date) when is_binary(date) do +    with {:ok, date, _offset} <- DateTime.from_iso8601(date) do        format_asctime(date)      else        _e -> +        Logger.warn("Date #{date} in wrong format, must be ISO 8601")          ""      end    end +  def date_to_asctime(date) do +    Logger.warn("Date #{date} in wrong format, must be ISO 8601") +    "" +  end +    def to_masto_date(%NaiveDateTime{} = date) do      date      |> NaiveDateTime.to_iso8601() @@ -275,7 +283,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    end    def confirm_current_password(user, password) do -    with %User{local: true} = db_user <- Repo.get(User, user.id), +    with %User{local: true} = db_user <- User.get_by_id(user.id),           true <- Pbkdf2.checkpw(password, db_user.password_hash) do        {:ok, db_user}      else @@ -285,7 +293,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    def emoji_from_profile(%{info: _info} = user) do      (Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name)) -    |> Enum.map(fn {shortcode, url} -> +    |> Enum.map(fn {shortcode, url, _} ->        %{          "type" => "Emoji",          "icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"}, diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 4d6192db0..181483664 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -5,6 +5,11 @@  defmodule Pleroma.Web.ControllerHelper do    use Pleroma.Web, :controller +  # As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html +  @falsy_param_values [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"] +  def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil +  def truthy_param?(value), do: value not in @falsy_param_values +    def oauth_scopes(params, default) do      # Note: `scopes` is used by Mastodon — supporting it but sticking to      # OAuth's standard `scope` wherever we control it diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index fa2d1cbe7..1633477c3 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -51,11 +51,22 @@ defmodule Pleroma.Web.Endpoint do    plug(Plug.MethodOverride)    plug(Plug.Head) +  secure_cookies = Pleroma.Config.get([__MODULE__, :secure_cookie_flag]) +    cookie_name = -    if Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag), +    if secure_cookies,        do: "__Host-pleroma_key",        else: "pleroma_key" +  same_site = +    if Pleroma.Config.oauth_consumer_enabled?() do +      # Note: "SameSite=Strict" prevents sign in with external OAuth provider +      #   (there would be no cookies during callback request from OAuth provider) +      "SameSite=Lax" +    else +      "SameSite=Strict" +    end +    # The session will be stored in the cookie and signed,    # this means its contents can be read but not tampered with.    # Set :encryption_salt if you would also like to encrypt it. @@ -65,11 +76,30 @@ defmodule Pleroma.Web.Endpoint do      key: cookie_name,      signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]},      http_only: true, -    secure: -      Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag), -    extra: "SameSite=Strict" +    secure: secure_cookies, +    extra: same_site    ) +  # Note: the plug and its configuration is compile-time this can't be upstreamed yet +  if proxies = Pleroma.Config.get([__MODULE__, :reverse_proxies]) do +    plug(RemoteIp, proxies: proxies) +  end + +  defmodule Instrumenter do +    use Prometheus.PhoenixInstrumenter +  end + +  defmodule PipelineInstrumenter do +    use Prometheus.PlugPipelineInstrumenter +  end + +  defmodule MetricsExporter do +    use Prometheus.PlugExporter +  end + +  plug(PipelineInstrumenter) +  plug(MetricsExporter) +    plug(Pleroma.Web.Router)    @doc """ diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index 5e690ddb8..c47328e13 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -4,7 +4,6 @@  defmodule Pleroma.Web.Federator do    alias Pleroma.Activity -  alias Pleroma.Jobs    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Relay @@ -31,39 +30,39 @@ defmodule Pleroma.Web.Federator do    # Client API    def incoming_doc(doc) do -    Jobs.enqueue(:federator_incoming, __MODULE__, [:incoming_doc, doc]) +    PleromaJobQueue.enqueue(:federator_incoming, __MODULE__, [:incoming_doc, doc])    end    def incoming_ap_doc(params) do -    Jobs.enqueue(:federator_incoming, __MODULE__, [:incoming_ap_doc, params]) +    PleromaJobQueue.enqueue(:federator_incoming, __MODULE__, [:incoming_ap_doc, params])    end    def publish(activity, priority \\ 1) do -    Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority) +    PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority)    end    def publish_single_ap(params) do -    Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish_single_ap, params]) +    PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_ap, params])    end    def publish_single_websub(websub) do -    Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish_single_websub, websub]) +    PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_websub, websub])    end    def verify_websub(websub) do -    Jobs.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub]) +    PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub])    end    def request_subscription(sub) do -    Jobs.enqueue(:federator_outgoing, __MODULE__, [:request_subscription, sub]) +    PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:request_subscription, sub])    end    def refresh_subscriptions do -    Jobs.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions]) +    PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions])    end    def publish_single_salmon(params) do -    Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params]) +    PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params])    end    # Job Worker Callbacks diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index 08ea5f967..382f07e6b 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do    alias Pleroma.Activity    alias Pleroma.Notification    alias Pleroma.Pagination +  alias Pleroma.ScheduledActivity    alias Pleroma.User    def get_followers(user, params \\ %{}) do @@ -28,6 +29,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do      |> Pagination.fetch_paginated(params)    end +  def get_scheduled_activities(user, params \\ %{}) do +    user +    |> ScheduledActivity.for_user_query() +    |> Pagination.fetch_paginated(params) +  end +    defp cast_params(params) do      param_types = %{        exclude_types: {:array, :string} diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index eee4e7678..5462ce8be 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -5,12 +5,14 @@  defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    use Pleroma.Web, :controller +  alias Ecto.Changeset    alias Pleroma.Activity    alias Pleroma.Config    alias Pleroma.Filter    alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.Repo +  alias Pleroma.ScheduledActivity    alias Pleroma.Stats    alias Pleroma.User    alias Pleroma.Web @@ -25,6 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    alias Pleroma.Web.MastodonAPI.MastodonView    alias Pleroma.Web.MastodonAPI.NotificationView    alias Pleroma.Web.MastodonAPI.ReportView +  alias Pleroma.Web.MastodonAPI.ScheduledActivityView    alias Pleroma.Web.MastodonAPI.StatusView    alias Pleroma.Web.MediaProxy    alias Pleroma.Web.OAuth.App @@ -178,14 +181,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    defp mastodonized_emoji do      Pleroma.Emoji.get_all() -    |> Enum.map(fn {shortcode, relative_url} -> +    |> Enum.map(fn {shortcode, relative_url, tags} ->        url = to_string(URI.merge(Web.base_url(), relative_url))        %{          "shortcode" => shortcode,          "static_url" => url,          "visible_in_picker" => true, -        "url" => url +        "url" => url, +        "tags" => String.split(tags, ",")        }      end)    end @@ -285,7 +289,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do -    with %User{} = user <- Repo.get(User, params["id"]) do +    with %User{} = user <- User.get_by_id(params["id"]) do        activities = ActivityPub.fetch_user_activities(user, reading_user, params)        conn @@ -319,7 +323,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %Activity{} = activity <- Repo.get(Activity, id), +    with %Activity{} = activity <- Activity.get_by_id(id),           true <- Visibility.visible_for_user?(activity, user) do        conn        |> put_view(StatusView) @@ -328,7 +332,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %Activity{} = activity <- Repo.get(Activity, id), +    with %Activity{} = activity <- Activity.get_by_id(id),           activities <-             ActivityPub.fetch_activities_for_context(activity.data["context"], %{               "blocking_user" => user, @@ -364,6 +368,55 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end +  def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do +    with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do +      conn +      |> add_link_headers(:scheduled_statuses, scheduled_activities) +      |> put_view(ScheduledActivityView) +      |> render("index.json", %{scheduled_activities: scheduled_activities}) +    end +  end + +  def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do +    with %ScheduledActivity{} = scheduled_activity <- +           ScheduledActivity.get(user, scheduled_activity_id) do +      conn +      |> put_view(ScheduledActivityView) +      |> render("show.json", %{scheduled_activity: scheduled_activity}) +    else +      _ -> {:error, :not_found} +    end +  end + +  def update_scheduled_status( +        %{assigns: %{user: user}} = conn, +        %{"id" => scheduled_activity_id} = params +      ) do +    with %ScheduledActivity{} = scheduled_activity <- +           ScheduledActivity.get(user, scheduled_activity_id), +         {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do +      conn +      |> put_view(ScheduledActivityView) +      |> render("show.json", %{scheduled_activity: scheduled_activity}) +    else +      nil -> {:error, :not_found} +      error -> error +    end +  end + +  def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do +    with %ScheduledActivity{} = scheduled_activity <- +           ScheduledActivity.get(user, scheduled_activity_id), +         {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do +      conn +      |> put_view(ScheduledActivityView) +      |> render("show.json", %{scheduled_activity: scheduled_activity}) +    else +      nil -> {:error, :not_found} +      error -> error +    end +  end +    def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)        when length(media_ids) > 0 do      params = @@ -384,12 +437,27 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          _ -> Ecto.UUID.generate()        end -    {:ok, activity} = -      Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end) +    scheduled_at = params["scheduled_at"] -    conn -    |> put_view(StatusView) -    |> try_render("status.json", %{activity: activity, for: user, as: :activity}) +    if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do +      with {:ok, scheduled_activity} <- +             ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do +        conn +        |> put_view(ScheduledActivityView) +        |> render("show.json", %{scheduled_activity: scheduled_activity}) +      end +    else +      params = Map.drop(params, ["scheduled_at"]) + +      {:ok, activity} = +        Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> +          CommonAPI.post(user, params) +        end) + +      conn +      |> put_view(StatusView) +      |> try_render("status.json", %{activity: activity, for: user, as: :activity}) +    end    end    def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do @@ -460,7 +528,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %Activity{} = activity <- Repo.get(Activity, id), +    with %Activity{} = activity <- Activity.get_by_id(id),           %User{} = user <- User.get_by_nickname(user.nickname),           true <- Visibility.visible_for_user?(activity, user),           {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do @@ -471,7 +539,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %Activity{} = activity <- Repo.get(Activity, id), +    with %Activity{} = activity <- Activity.get_by_id(id),           %User{} = user <- User.get_by_nickname(user.nickname),           true <- Visibility.visible_for_user?(activity, user),           {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do @@ -593,7 +661,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def favourited_by(conn, %{"id" => id}) do -    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do +    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do        q = from(u in User, where: u.ap_id in ^likes)        users = Repo.all(q) @@ -606,7 +674,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def reblogged_by(conn, %{"id" => id}) do -    with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do +    with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do        q = from(u in User, where: u.ap_id in ^announces)        users = Repo.all(q) @@ -657,7 +725,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do -    with %User{} = user <- Repo.get(User, id), +    with %User{} = user <- User.get_by_id(id),           followers <- MastodonAPI.get_followers(user, params) do        followers =          cond do @@ -674,7 +742,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do -    with %User{} = user <- Repo.get(User, id), +    with %User{} = user <- User.get_by_id(id),           followers <- MastodonAPI.get_friends(user, params) do        followers =          cond do @@ -699,7 +767,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do -    with %User{} = follower <- Repo.get(User, id), +    with %User{} = follower <- User.get_by_id(id),           {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do        conn        |> put_view(AccountView) @@ -713,7 +781,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do -    with %User{} = follower <- Repo.get(User, id), +    with %User{} = follower <- User.get_by_id(id),           {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do        conn        |> put_view(AccountView) @@ -727,7 +795,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do -    with %User{} = followed <- Repo.get(User, id), +    with %User{} = followed <- User.get_by_id(id),           false <- User.following?(follower, followed),           {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do        conn @@ -755,7 +823,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do -    with %User{} = followed <- Repo.get_by(User, nickname: uri), +    with %User{} = followed <- User.get_by_nickname(uri),           {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do        conn        |> put_view(AccountView) @@ -769,7 +837,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do -    with %User{} = followed <- Repo.get(User, id), +    with %User{} = followed <- User.get_by_id(id),           {:ok, follower} <- CommonAPI.unfollow(follower, followed) do        conn        |> put_view(AccountView) @@ -778,7 +846,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do -    with %User{} = muted <- Repo.get(User, id), +    with %User{} = muted <- User.get_by_id(id),           {:ok, muter} <- User.mute(muter, muted) do        conn        |> put_view(AccountView) @@ -792,7 +860,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do -    with %User{} = muted <- Repo.get(User, id), +    with %User{} = muted <- User.get_by_id(id),           {:ok, muter} <- User.unmute(muter, muted) do        conn        |> put_view(AccountView) @@ -813,7 +881,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do -    with %User{} = blocked <- Repo.get(User, id), +    with %User{} = blocked <- User.get_by_id(id),           {:ok, blocker} <- User.block(blocker, blocked),           {:ok, _activity} <- ActivityPub.block(blocker, blocked) do        conn @@ -828,7 +896,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do -    with %User{} = blocked <- Repo.get(User, id), +    with %User{} = blocked <- User.get_by_id(id),           {:ok, blocker} <- User.unblock(blocker, blocked),           {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do        conn @@ -966,7 +1034,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def bookmarks(%{assigns: %{user: user}} = conn, _) do -    user = Repo.get(User, user.id) +    user = User.get_by_id(user.id)      activities =        user.bookmarks @@ -1023,7 +1091,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      accounts      |> Enum.each(fn account_id ->        with %Pleroma.List{} = list <- Pleroma.List.get(id, user), -           %User{} = followed <- Repo.get(User, account_id) do +           %User{} = followed <- User.get_by_id(account_id) do          Pleroma.List.follow(list, followed)        end      end) @@ -1035,7 +1103,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      accounts      |> Enum.each(fn account_id ->        with %Pleroma.List{} = list <- Pleroma.List.get(id, user), -           %User{} = followed <- Repo.get(Pleroma.User, account_id) do +           %User{} = followed <- Pleroma.User.get_by_id(account_id) do          Pleroma.List.unfollow(list, followed)        end      end) @@ -1091,9 +1159,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def index(%{assigns: %{user: user}} = conn, _params) do -    token = -      conn -      |> get_session(:oauth_token) +    token = get_session(conn, :oauth_token)      if user && token do        mastodon_emoji = mastodonized_emoji() @@ -1121,7 +1187,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do              auto_play_gif: false,              display_sensitive_media: false,              reduce_motion: false, -            max_toot_chars: limit +            max_toot_chars: limit, +            mascot: "/images/pleroma-fox-tan-smol.png"            },            rights: %{              delete_others_notice: present?(user.info.is_moderator), @@ -1193,6 +1260,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> render("index.html", %{initial_state: initial_state, flavour: flavour})      else        conn +      |> put_session(:return_to, conn.request_path)        |> redirect(to: "/web/login")      end    end @@ -1249,16 +1317,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      "glitch"    end -  def login(conn, %{"code" => code}) do +  def login(%{assigns: %{user: %User{}}} = conn, _params) do +    redirect(conn, to: local_mastodon_root_path(conn)) +  end + +  @doc "Local Mastodon FE login init action" +  def login(conn, %{"code" => auth_token}) do      with {:ok, app} <- get_or_make_app(), -         %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id), +         %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),           {:ok, token} <- Token.exchange_token(app, auth) do        conn        |> put_session(:oauth_token, token.token) -      |> redirect(to: "/web/getting-started") +      |> redirect(to: local_mastodon_root_path(conn))      end    end +  @doc "Local Mastodon FE callback action"    def login(conn, _) do      with {:ok, app} <- get_or_make_app() do        path = @@ -1271,8 +1345,18 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do            scope: Enum.join(app.scopes, " ")          ) -      conn -      |> redirect(to: path) +      redirect(conn, to: path) +    end +  end + +  defp local_mastodon_root_path(conn) do +    case get_session(conn, :return_to) do +      nil -> +        mastodon_api_path(conn, :index, ["getting-started"]) + +      return_to -> +        delete_session(conn, :return_to) +        return_to      end    end @@ -1312,7 +1396,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do      Logger.debug("Unimplemented, returning unmodified relationship") -    with %User{} = target <- Repo.get(User, id) do +    with %User{} = target <- User.get_by_id(id) do        conn        |> put_view(AccountView)        |> render("relationship.json", %{user: user, target: target}) @@ -1390,6 +1474,23 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    # fallback action    # +  def errors(conn, {:error, %Changeset{} = changeset}) do +    error_message = +      changeset +      |> Changeset.traverse_errors(fn {message, _opt} -> message end) +      |> Enum.map_join(", ", fn {_k, v} -> v end) + +    conn +    |> put_status(422) +    |> json(%{error: error_message}) +  end + +  def errors(conn, {:error, :not_found}) do +    conn +    |> put_status(404) +    |> json(%{error: "Record not found"}) +  end +    def errors(conn, _) do      conn      |> put_status(500) @@ -1454,7 +1555,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do -    with %Activity{} = activity <- Repo.get(Activity, status_id), +    with %Activity{} = activity <- Activity.get_by_id(status_id),           true <- Visibility.visible_for_user?(activity, user) do        data =          StatusView.render( diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex new file mode 100644 index 000000000..0aae15ab9 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex @@ -0,0 +1,57 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do +  use Pleroma.Web, :view + +  alias Pleroma.ScheduledActivity +  alias Pleroma.Web.CommonAPI +  alias Pleroma.Web.MastodonAPI.ScheduledActivityView +  alias Pleroma.Web.MastodonAPI.StatusView + +  def render("index.json", %{scheduled_activities: scheduled_activities}) do +    render_many(scheduled_activities, ScheduledActivityView, "show.json") +  end + +  def render("show.json", %{scheduled_activity: %ScheduledActivity{} = scheduled_activity}) do +    %{ +      id: to_string(scheduled_activity.id), +      scheduled_at: CommonAPI.Utils.to_masto_date(scheduled_activity.scheduled_at), +      params: status_params(scheduled_activity.params) +    } +    |> with_media_attachments(scheduled_activity) +  end + +  defp with_media_attachments(data, %{params: %{"media_attachments" => media_attachments}}) do +    try do +      attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment) +      Map.put(data, :media_attachments, attachments) +    rescue +      _ -> data +    end +  end + +  defp with_media_attachments(data, _), do: data + +  defp status_params(params) do +    data = %{ +      text: params["status"], +      sensitive: params["sensitive"], +      spoiler_text: params["spoiler_text"], +      visibility: params["visibility"], +      scheduled_at: params["scheduled_at"], +      poll: params["poll"], +      in_reply_to_id: params["in_reply_to_id"] +    } + +    data = +      if media_ids = params["media_ids"] do +        Map.put(data, :media_ids, media_ids) +      else +        data +      end + +    data +  end +end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 200bb453d..d4a8e4fff 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -147,10 +147,37 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      content =        object        |> render_content() -      |> HTML.get_cached_scrubbed_html_for_object( + +    content_html = +      content +      |> HTML.get_cached_scrubbed_html_for_activity( +        User.html_filter_policy(opts[:for]), +        activity, +        "mastoapi:content" +      ) + +    content_plaintext = +      content +      |> HTML.get_cached_stripped_html_for_activity( +        activity, +        "mastoapi:content" +      ) + +    summary = object["summary"] || "" + +    summary_html = +      summary +      |> HTML.get_cached_scrubbed_html_for_activity(          User.html_filter_policy(opts[:for]),          activity, -        __MODULE__ +        "mastoapi:summary" +      ) + +    summary_plaintext = +      summary +      |> HTML.get_cached_stripped_html_for_activity( +        activity, +        "mastoapi:summary"        )      card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)) @@ -171,7 +198,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),        reblog: nil,        card: card, -      content: content, +      content: content_html,        created_at: created_at,        reblogs_count: announcement_count,        replies_count: object["repliesCount"] || 0, @@ -182,7 +209,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        muted: CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user),        pinned: pinned?(activity, user),        sensitive: sensitive, -      spoiler_text: object["summary"] || "", +      spoiler_text: summary_html,        visibility: get_visibility(object),        media_attachments: attachments,        mentions: mentions, @@ -195,7 +222,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        emojis: build_emojis(activity.data["object"]["emoji"]),        pleroma: %{          local: activity.local, -        conversation_id: get_context_id(activity) +        conversation_id: get_context_id(activity), +        content: %{"text/plain" => content_plaintext}, +        spoiler_text: %{"text/plain" => summary_plaintext}        }      }    end diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 9b262f461..1b3721e2b 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -90,7 +90,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do    # Authenticated streams.    defp allow_request(stream, {"access_token", access_token}) when stream in @streams do      with %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token), -         user = %User{} <- Repo.get(User, user_id) do +         user = %User{} <- User.get_by_id(user_id) do        {:ok, user}      else        _ -> {:error, 403} diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex index 23bbde1a6..58385a3d1 100644 --- a/lib/pleroma/web/metadata/utils.ex +++ b/lib/pleroma/web/metadata/utils.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.Metadata.Utils do      # 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__) +    |> HTML.get_cached_stripped_html_for_activity(object, "metadata")      |> Formatter.demojify()      |> Formatter.truncate()    end diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex index f0fe3b578..afaa00242 100644 --- a/lib/pleroma/web/oauth/fallback_controller.ex +++ b/lib/pleroma/web/oauth/fallback_controller.ex @@ -6,8 +6,21 @@ defmodule Pleroma.Web.OAuth.FallbackController do    use Pleroma.Web, :controller    alias Pleroma.Web.OAuth.OAuthController -  # No user/password -  def call(conn, _) do +  def call(conn, {:register, :generic_error}) do +    conn +    |> put_status(:internal_server_error) +    |> put_flash(:error, "Unknown error, please check the details and try again.") +    |> OAuthController.registration_details(conn.params) +  end + +  def call(conn, {:register, _error}) do +    conn +    |> put_status(:unauthorized) +    |> put_flash(:error, "Invalid Username/Password") +    |> OAuthController.registration_details(conn.params) +  end + +  def call(conn, _error) do      conn      |> put_status(:unauthorized)      |> put_flash(:error, "Invalid Username/Password") diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index ebb3dd253..bee7084ad 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -5,21 +5,46 @@  defmodule Pleroma.Web.OAuth.OAuthController do    use Pleroma.Web, :controller +  alias Pleroma.Registration    alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web.Auth.Authenticator +  alias Pleroma.Web.ControllerHelper    alias Pleroma.Web.OAuth.App    alias Pleroma.Web.OAuth.Authorization    alias Pleroma.Web.OAuth.Token    import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2] +  if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) +    plug(:fetch_session)    plug(:fetch_flash)    action_fallback(Pleroma.Web.OAuth.FallbackController) -  def authorize(conn, params) do +  def authorize(%{assigns: %{token: %Token{} = token}} = conn, params) do +    if ControllerHelper.truthy_param?(params["force_login"]) do +      do_authorize(conn, params) +    else +      redirect_uri = +        if is_binary(params["redirect_uri"]) do +          params["redirect_uri"] +        else +          app = Repo.preload(token, :app).app + +          app.redirect_uris +          |> String.split() +          |> Enum.at(0) +        end + +      redirect(conn, external: redirect_uri(conn, redirect_uri)) +    end +  end + +  def authorize(conn, params), do: do_authorize(conn, params) + +  defp do_authorize(conn, params) do      app = Repo.get_by(App, client_id: params["client_id"])      available_scopes = (app && app.scopes) || []      scopes = oauth_scopes(params, nil) || available_scopes @@ -35,72 +60,65 @@ defmodule Pleroma.Web.OAuth.OAuthController do      })    end -  def create_authorization(conn, %{ -        "authorization" => -          %{ -            "client_id" => client_id, -            "redirect_uri" => redirect_uri -          } = auth_params -      }) do -    with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)}, -         %App{} = app <- Repo.get_by(App, client_id: client_id), -         true <- redirect_uri in String.split(app.redirect_uris), -         scopes <- oauth_scopes(auth_params, []), -         {:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes}, -         # Note: `scope` param is intentionally not optional in this context -         {:missing_scopes, false} <- {:missing_scopes, scopes == []}, -         {:auth_active, true} <- {:auth_active, User.auth_active?(user)}, -         {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do -      redirect_uri = -        if redirect_uri == "." do -          # Special case: Local MastodonFE -          mastodon_api_url(conn, :login) -        else -          redirect_uri -        end +  def create_authorization( +        conn, +        %{"authorization" => auth_params} = params, +        opts \\ [] +      ) do +    with {:ok, auth} <- do_create_authorization(conn, params, opts[:user]) do +      after_create_authorization(conn, auth, auth_params) +    else +      error -> +        handle_create_authorization_error(conn, error, auth_params) +    end +  end -      cond do -        redirect_uri == "urn:ietf:wg:oauth:2.0:oob" -> -          render(conn, "results.html", %{ -            auth: auth -          }) +  def after_create_authorization(conn, auth, %{"redirect_uri" => redirect_uri} = auth_params) do +    redirect_uri = redirect_uri(conn, redirect_uri) + +    if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do +      render(conn, "results.html", %{ +        auth: auth +      }) +    else +      connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" +      url = "#{redirect_uri}#{connector}" +      url_params = %{:code => auth.token} -        true -> -          connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" -          url = "#{redirect_uri}#{connector}" -          url_params = %{:code => auth.token} +      url_params = +        if auth_params["state"] do +          Map.put(url_params, :state, auth_params["state"]) +        else +          url_params +        end -          url_params = -            if auth_params["state"] do -              Map.put(url_params, :state, auth_params["state"]) -            else -              url_params -            end +      url = "#{url}#{Plug.Conn.Query.encode(url_params)}" -          url = "#{url}#{Plug.Conn.Query.encode(url_params)}" +      redirect(conn, external: url) +    end +  end -          redirect(conn, external: url) -      end -    else -      {scopes_issue, _} when scopes_issue in [:unsupported_scopes, :missing_scopes] -> -        # Per https://github.com/tootsuite/mastodon/blob/ -        #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 -        conn -        |> put_flash(:error, "This action is outside the authorized scopes") -        |> put_status(:unauthorized) -        |> authorize(auth_params) +  defp handle_create_authorization_error(conn, {scopes_issue, _}, auth_params) +       when scopes_issue in [:unsupported_scopes, :missing_scopes] do +    # Per https://github.com/tootsuite/mastodon/blob/ +    #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 +    conn +    |> put_flash(:error, "This action is outside the authorized scopes") +    |> put_status(:unauthorized) +    |> authorize(auth_params) +  end -      {:auth_active, false} -> -        # Per https://github.com/tootsuite/mastodon/blob/ -        #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76 -        conn -        |> put_flash(:error, "Your login is missing a confirmed e-mail address") -        |> put_status(:forbidden) -        |> authorize(auth_params) +  defp handle_create_authorization_error(conn, {:auth_active, false}, auth_params) do +    # Per https://github.com/tootsuite/mastodon/blob/ +    #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76 +    conn +    |> put_flash(:error, "Your login is missing a confirmed e-mail address") +    |> put_status(:forbidden) +    |> authorize(auth_params) +  end -      error -> -        Authenticator.handle_error(conn, error) -    end +  defp handle_create_authorization_error(conn, error, _auth_params) do +    Authenticator.handle_error(conn, error)    end    def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do @@ -108,7 +126,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do           fixed_token = fix_padding(params["code"]),           %Authorization{} = auth <-             Repo.get_by(Authorization, token: fixed_token, app_id: app.id), -         %User{} = user <- Repo.get(User, auth.user_id), +         %User{} = user <- User.get_by_id(auth.user_id),           {:ok, token} <- Token.exchange_token(app, auth),           {:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do        response = %{ @@ -133,9 +151,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do          conn,          %{"grant_type" => "password"} = params        ) do -    with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)}, +    with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn, params)},           %App{} = app <- get_app_from_request(conn, params),           {:auth_active, true} <- {:auth_active, User.auth_active?(user)}, +         {:user_active, true} <- {:user_active, !user.info.deactivated},           scopes <- oauth_scopes(params, app.scopes),           [] <- scopes -- app.scopes,           true <- Enum.any?(scopes), @@ -159,6 +178,11 @@ defmodule Pleroma.Web.OAuth.OAuthController do          |> put_status(:forbidden)          |> json(%{error: "Your login is missing a confirmed e-mail address"}) +      {:user_active, false} -> +        conn +        |> put_status(:forbidden) +        |> json(%{error: "Your account is currently disabled"}) +        _error ->          put_status(conn, 400)          |> json(%{error: "Invalid credentials"}) @@ -189,6 +213,184 @@ defmodule Pleroma.Web.OAuth.OAuthController do      end    end +  @doc "Prepares OAuth request to provider for Ueberauth" +  def prepare_request(conn, %{"provider" => provider} = params) do +    scope = +      oauth_scopes(params, []) +      |> Enum.join(" ") + +    state = +      params +      |> Map.delete("scopes") +      |> Map.put("scope", scope) +      |> Poison.encode!() + +    params = +      params +      |> Map.drop(~w(scope scopes client_id redirect_uri)) +      |> Map.put("state", state) + +    # Handing the request to Ueberauth +    redirect(conn, to: o_auth_path(conn, :request, provider, params)) +  end + +  def request(conn, params) do +    message = +      if params["provider"] do +        "Unsupported OAuth provider: #{params["provider"]}." +      else +        "Bad OAuth request." +      end + +    conn +    |> put_flash(:error, message) +    |> redirect(to: "/") +  end + +  def callback(%{assigns: %{ueberauth_failure: failure}} = conn, params) do +    params = callback_params(params) +    messages = for e <- Map.get(failure, :errors, []), do: e.message +    message = Enum.join(messages, "; ") + +    conn +    |> put_flash(:error, "Failed to authenticate: #{message}.") +    |> redirect(external: redirect_uri(conn, params["redirect_uri"])) +  end + +  def callback(conn, params) do +    params = callback_params(params) + +    with {:ok, registration} <- Authenticator.get_registration(conn, params) do +      user = Repo.preload(registration, :user).user +      auth_params = Map.take(params, ~w(client_id redirect_uri scope scopes state)) + +      if user do +        create_authorization( +          conn, +          %{"authorization" => auth_params}, +          user: user +        ) +      else +        registration_params = +          Map.merge(auth_params, %{ +            "nickname" => Registration.nickname(registration), +            "email" => Registration.email(registration) +          }) + +        conn +        |> put_session(:registration_id, registration.id) +        |> registration_details(registration_params) +      end +    else +      _ -> +        conn +        |> put_flash(:error, "Failed to set up user account.") +        |> redirect(external: redirect_uri(conn, params["redirect_uri"])) +    end +  end + +  defp callback_params(%{"state" => state} = params) do +    Map.merge(params, Poison.decode!(state)) +  end + +  def registration_details(conn, params) do +    render(conn, "register.html", %{ +      client_id: params["client_id"], +      redirect_uri: params["redirect_uri"], +      state: params["state"], +      scopes: oauth_scopes(params, []), +      nickname: params["nickname"], +      email: params["email"] +    }) +  end + +  def register(conn, %{"op" => "connect"} = params) do +    authorization_params = Map.put(params, "name", params["auth_name"]) +    create_authorization_params = %{"authorization" => authorization_params} + +    with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), +         %Registration{} = registration <- Repo.get(Registration, registration_id), +         {_, {:ok, auth}} <- +           {:create_authorization, do_create_authorization(conn, create_authorization_params)}, +         %User{} = user <- Repo.preload(auth, :user).user, +         {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do +      conn +      |> put_session_registration_id(nil) +      |> after_create_authorization(auth, authorization_params) +    else +      {:create_authorization, error} -> +        {:register, handle_create_authorization_error(conn, error, create_authorization_params)} + +      _ -> +        {:register, :generic_error} +    end +  end + +  def register(conn, %{"op" => "register"} = params) do +    with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), +         %Registration{} = registration <- Repo.get(Registration, registration_id), +         {:ok, user} <- Authenticator.create_from_registration(conn, params, registration) do +      conn +      |> put_session_registration_id(nil) +      |> create_authorization( +        %{ +          "authorization" => %{ +            "client_id" => params["client_id"], +            "redirect_uri" => params["redirect_uri"], +            "scopes" => oauth_scopes(params, nil) +          } +        }, +        user: user +      ) +    else +      {:error, changeset} -> +        message = +          Enum.map(changeset.errors, fn {field, {error, _}} -> +            "#{field} #{error}" +          end) +          |> Enum.join("; ") + +        message = +          String.replace( +            message, +            "ap_id has already been taken", +            "nickname has already been taken" +          ) + +        conn +        |> put_status(:forbidden) +        |> put_flash(:error, "Error: #{message}.") +        |> registration_details(params) + +      _ -> +        {:register, :generic_error} +    end +  end + +  defp do_create_authorization( +         conn, +         %{ +           "authorization" => +             %{ +               "client_id" => client_id, +               "redirect_uri" => redirect_uri +             } = auth_params +         } = params, +         user \\ nil +       ) do +    with {_, {:ok, %User{} = user}} <- +           {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn, params)}, +         %App{} = app <- Repo.get_by(App, client_id: client_id), +         true <- redirect_uri in String.split(app.redirect_uris), +         scopes <- oauth_scopes(auth_params, []), +         {:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes}, +         # Note: `scope` param is intentionally not optional in this context +         {:missing_scopes, false} <- {:missing_scopes, scopes == []}, +         {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do +      Authorization.create_authorization(app, user, scopes) +    end +  end +    # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be    # decoding it.  Investigate sometime.    defp fix_padding(token) do @@ -221,4 +423,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do        nil      end    end + +  # Special case: Local MastodonFE +  defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login) + +  defp redirect_uri(_conn, redirect_uri), do: redirect_uri + +  defp get_session_registration_id(conn), do: get_session(conn, :registration_id) + +  defp put_session_registration_id(conn, registration_id), +    do: put_session(conn, :registration_id, registration_id)  end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index a8b06db36..2b5ad9b94 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -27,7 +27,7 @@ defmodule Pleroma.Web.OAuth.Token do    def exchange_token(app, auth) do      with {:ok, auth} <- Authorization.use_token(auth),           true <- auth.app_id == app.id do -      create_token(app, Repo.get(User, auth.user_id), auth.scopes) +      create_token(app, User.get_by_id(auth.user_id), auth.scopes)      end    end diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 863573185..2233480c5 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -19,8 +19,8 @@ defmodule Pleroma.Web.Push.Impl do    @types ["Create", "Follow", "Announce", "Like"]    @doc "Performs sending notifications for user subscriptions" -  @spec perform_send(Notification.t()) :: list(any) -  def perform_send( +  @spec perform(Notification.t()) :: list(any) | :error +  def perform(          %{activity: %{data: %{"type" => activity_type}, id: activity_id}, user_id: user_id} =            notif        ) @@ -50,7 +50,7 @@ defmodule Pleroma.Web.Push.Impl do      end    end -  def perform_send(_) do +  def perform(_) do      Logger.warn("Unknown notification type")      :error    end diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push/push.ex index 5259e8e33..729dad02a 100644 --- a/lib/pleroma/web/push/push.ex +++ b/lib/pleroma/web/push/push.ex @@ -3,18 +3,20 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.Push do -  use GenServer -    alias Pleroma.Web.Push.Impl    require Logger -  ############## -  # Client API # -  ############## +  def init do +    unless enabled() do +      Logger.warn(""" +      VAPID key pair is not found. If you wish to enabled web push, please run + +          mix web_push.gen.keypair -  def start_link do -    GenServer.start_link(__MODULE__, :ok, name: __MODULE__) +      and add the resulting output to your configuration file. +      """) +    end    end    def vapid_config do @@ -30,35 +32,5 @@ defmodule Pleroma.Web.Push do    end    def send(notification), -    do: GenServer.cast(__MODULE__, {:send, notification}) - -  #################### -  # Server Callbacks # -  #################### - -  @impl true -  def init(:ok) do -    if enabled() do -      {:ok, nil} -    else -      Logger.warn(""" -      VAPID key pair is not found. If you wish to enabled web push, please run - -          mix web_push.gen.keypair - -      and add the resulting output to your configuration file. -      """) - -      :ignore -    end -  end - -  @impl true -  def handle_cast({:send, notification}, state) do -    if enabled() do -      Impl.perform_send(notification) -    end - -    {:noreply, state} -  end +    do: PleromaJobQueue.enqueue(:web_push, Impl, [notification])  end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 36cbf0f57..66db5e628 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -5,6 +5,16 @@  defmodule Pleroma.Web.Router do    use Pleroma.Web, :router +  pipeline :browser do +    plug(:accepts, ["html"]) +    plug(:fetch_session) +  end + +  pipeline :oauth do +    plug(:fetch_session) +    plug(Pleroma.Plugs.OAuthPlug) +  end +    pipeline :api do      plug(:accepts, ["json"])      plug(:fetch_session) @@ -105,10 +115,6 @@ defmodule Pleroma.Web.Router do      plug(:accepts, ["json", "xml"])    end -  pipeline :oauth do -    plug(:accepts, ["html", "json"]) -  end -    pipeline :pleroma_api do      plug(:accepts, ["html", "json"])    end @@ -139,7 +145,12 @@ defmodule Pleroma.Web.Router do    scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do      pipe_through([:admin_api, :oauth_write]) +    post("/user/follow", AdminAPIController, :user_follow) +    post("/user/unfollow", AdminAPIController, :user_unfollow) +      get("/users", AdminAPIController, :list_users) +    get("/users/:nickname", AdminAPIController, :user_show) +      delete("/user", AdminAPIController, :user_delete)      patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)      post("/user", AdminAPIController, :user_create) @@ -200,10 +211,24 @@ defmodule Pleroma.Web.Router do    end    scope "/oauth", Pleroma.Web.OAuth do -    get("/authorize", OAuthController, :authorize) +    scope [] do +      pipe_through(:oauth) +      get("/authorize", OAuthController, :authorize) +    end +      post("/authorize", OAuthController, :create_authorization)      post("/token", OAuthController, :token_exchange)      post("/revoke", OAuthController, :token_revoke) +    get("/registration_details", OAuthController, :registration_details) + +    scope [] do +      pipe_through(:browser) + +      get("/prepare_request", OAuthController, :prepare_request) +      get("/:provider", OAuthController, :request) +      get("/:provider/callback", OAuthController, :callback) +      post("/register", OAuthController, :register) +    end    end    scope "/api/v1", Pleroma.Web.MastodonAPI do @@ -218,6 +243,7 @@ defmodule Pleroma.Web.Router do        get("/accounts/search", MastodonAPIController, :account_search)        get("/accounts/:id/lists", MastodonAPIController, :account_lists) +      get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)        get("/follow_requests", MastodonAPIController, :follow_requests)        get("/blocks", MastodonAPIController, :blocks) @@ -234,6 +260,9 @@ defmodule Pleroma.Web.Router do        get("/notifications", MastodonAPIController, :notifications)        get("/notifications/:id", MastodonAPIController, :get_notification) +      get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses) +      get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status) +        get("/lists", MastodonAPIController, :get_lists)        get("/lists/:id", MastodonAPIController, :get_list)        get("/lists/:id/accounts", MastodonAPIController, :list_accounts) @@ -268,6 +297,9 @@ defmodule Pleroma.Web.Router do        post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)        post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation) +      put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status) +      delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status) +        post("/media", MastodonAPIController, :upload)        put("/media/:id", MastodonAPIController, :update_media) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 592749b42..a82109f92 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.Streamer do    alias Pleroma.Activity    alias Pleroma.Notification    alias Pleroma.Object -  alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Visibility @@ -82,7 +81,7 @@ defmodule Pleroma.Web.Streamer do          _ ->            Pleroma.List.get_lists_from_activity(item)            |> Enum.filter(fn list -> -            owner = Repo.get(User, list.user_id) +            owner = User.get_by_id(list.user_id)              Visibility.visible_for_user?(item, owner)            end) diff --git a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex new file mode 100644 index 000000000..4b8fb5dae --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex @@ -0,0 +1,13 @@ +<div class="scopes-input"> +  <%= label @form, :scope, "Permissions" %> + +  <div class="scopes"> +    <%= for scope <- @available_scopes do %> +      <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> +      <div class="scope"> +        <%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: assigns[:scope_param] || "scope[]" %> +        <%= label @form, :"scope_#{scope}", String.capitalize(scope) %> +      </div> +    <% end %> +  </div> +</div> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex new file mode 100644 index 000000000..85f62ca64 --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex @@ -0,0 +1,13 @@ +<h2>Sign in with external provider</h2> + +<%= form_for @conn, o_auth_path(@conn, :prepare_request), [method: "get"], fn f -> %> +  <%= render @view_module, "_scopes.html", Map.put(assigns, :form, f) %> + +  <%= hidden_input f, :client_id, value: @client_id %> +  <%= hidden_input f, :redirect_uri, value: @redirect_uri %> +  <%= hidden_input f, :state, value: @state %> + +    <%= for strategy <- Pleroma.Config.oauth_consumer_strategies() do %> +      <%= submit "Sign in with #{String.capitalize(strategy)}", name: "provider", value: strategy %> +    <% end %> +<% end %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex new file mode 100644 index 000000000..126390391 --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex @@ -0,0 +1,43 @@ +<%= if get_flash(@conn, :info) do %> +  <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p> +<% end %> +<%= if get_flash(@conn, :error) do %> +  <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p> +<% end %> + +<h2>Registration Details</h2> + +<p>If you'd like to register a new account, please provide the details below.</p> + +<%= form_for @conn, o_auth_path(@conn, :register), [], fn f -> %> + +<div class="input"> +  <%= label f, :nickname, "Nickname" %> +  <%= text_input f, :nickname, value: @nickname %> +</div> +<div class="input"> +  <%= label f, :email, "Email" %> +  <%= text_input f, :email, value: @email %> +</div> + +<%= submit "Proceed as new user", name: "op", value: "register" %> + +<p>Alternatively, sign in to connect to existing account.</p> + +<div class="input"> +  <%= label f, :auth_name, "Name or email" %> +  <%= text_input f, :auth_name %> +</div> +<div class="input"> +  <%= label f, :password, "Password" %> +  <%= password_input f, :password %> +</div> + +<%= submit "Proceed as existing user", name: "op", value: "connect" %> + +<%= hidden_input f, :client_id, value: @client_id %> +<%= hidden_input f, :redirect_uri, value: @redirect_uri %> +<%= hidden_input f, :scope, value: Enum.join(@scopes, " ") %> +<%= hidden_input f, :state, value: @state %> + +<% end %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index 161333847..87278e636 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -4,7 +4,9 @@  <%= if get_flash(@conn, :error) do %>  <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>  <% end %> +  <h2>OAuth Authorization</h2> +  <%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>  <div class="input">    <%= label f, :name, "Name or email" %> @@ -14,22 +16,16 @@    <%= label f, :password, "Password" %>    <%= password_input f, :password %>  </div> -<div class="scopes-input"> -<%= label f, :scope, "Permissions" %> -  <div class="scopes"> -    <%= for scope <- @available_scopes do %> -      <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> -      <div class="scope"> -        <%= checkbox f, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %> -        <%= label f, :"scope_#{scope}", String.capitalize(scope) %> -      </div> -    <% end %> -  </div> -</div> + +<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f, scope_param: "authorization[scope][]"}) %>  <%= hidden_input f, :client_id, value: @client_id %>  <%= hidden_input f, :response_type, value: @response_type %>  <%= hidden_input f, :redirect_uri, value: @redirect_uri %> -<%= hidden_input f, :state, value: @state%> +<%= hidden_input f, :state, value: @state %>  <%= submit "Authorize" %>  <% end %> + +<%= if Pleroma.Config.oauth_consumer_enabled?() do %> +  <%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %> +<% end %> diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 2708299cb..bb71742ec 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    require Logger    alias Comeonin.Pbkdf2 +  alias Pleroma.Activity    alias Pleroma.Emoji    alias Pleroma.Notification    alias Pleroma.PasswordResetToken @@ -21,7 +22,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    def show_password_reset(conn, %{"token" => token}) do      with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), -         %User{} = user <- Repo.get(User, token.user_id) do +         %User{} = user <- User.get_by_id(token.user_id) do        render(conn, "password_reset.html", %{          token: token,          user: user @@ -73,36 +74,52 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    end    def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do -    {err, followee} = OStatus.find_or_make_user(acct) -    avatar = User.avatar_url(followee) -    name = followee.nickname -    id = followee.id - -    if !!user do -      conn -      |> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id}) +    if is_status?(acct) do +      {:ok, object} = ActivityPub.fetch_object_from_id(acct) +      %Activity{id: activity_id} = Activity.get_create_by_object_ap_id(object.data["id"]) +      redirect(conn, to: "/notice/#{activity_id}")      else -      conn -      |> render("follow_login.html", %{ -        error: false, -        acct: acct, -        avatar: avatar, -        name: name, -        id: id -      }) +      {err, followee} = OStatus.find_or_make_user(acct) +      avatar = User.avatar_url(followee) +      name = followee.nickname +      id = followee.id + +      if !!user do +        conn +        |> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id}) +      else +        conn +        |> render("follow_login.html", %{ +          error: false, +          acct: acct, +          avatar: avatar, +          name: name, +          id: id +        }) +      end +    end +  end + +  defp is_status?(acct) do +    case ActivityPub.fetch_and_contain_remote_object_from_id(acct) do +      {:ok, %{"type" => type}} when type in ["Article", "Note", "Video", "Page", "Question"] -> +        true + +      _ -> +        false      end    end    def do_remote_follow(conn, %{          "authorization" => %{"name" => username, "password" => password, "id" => id}        }) do -    followee = Repo.get(User, id) +    followee = User.get_by_id(id)      avatar = User.avatar_url(followee)      name = followee.nickname      with %User{} = user <- User.get_cached_by_nickname(username),           true <- Pbkdf2.checkpw(password, user.password_hash), -         %User{} = _followed <- Repo.get(User, id), +         %User{} = _followed <- User.get_by_id(id),           {:ok, follower} <- User.follow(user, followee),           {:ok, _activity} <- ActivityPub.follow(follower, followee) do        conn @@ -124,7 +141,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    end    def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do -    with %User{} = followee <- Repo.get(User, id), +    with %User{} = followee <- User.get_by_id(id),           {:ok, follower} <- User.follow(user, followee),           {:ok, _activity} <- ActivityPub.follow(follower, followee) do        conn @@ -266,7 +283,13 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    end    def emoji(conn, _params) do -    json(conn, Enum.into(Emoji.get_all(), %{})) +    emoji = +      Emoji.get_all() +      |> Enum.map(fn {short_code, path, tags} -> +        %{short_code => %{image_url: path, tags: String.split(tags, ",")}} +      end) + +    json(conn, emoji)    end    def update_notificaton_settings(%{assigns: %{user: user}} = conn, params) do diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 9978c7f64..9b081a316 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -20,7 +20,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do    end    def delete(%User{} = user, id) do -    with %Activity{data: %{"type" => _type}} <- Repo.get(Activity, id), +    with %Activity{data: %{"type" => _type}} <- Activity.get_by_id(id),           {:ok, activity} <- CommonAPI.delete(id, user) do        {:ok, activity}      end @@ -227,12 +227,9 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do          end        %{"screen_name" => nickname} -> -        case target = Repo.get_by(User, nickname: nickname) do -          nil -> -            {:error, "No user with such screen_name"} - -          _ -> -            {:ok, target} +        case User.get_by_nickname(nickname) do +          nil -> {:error, "No user with such screen_name"} +          target -> {:ok, target}          end        _ -> diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 62cce18dc..a7ec9949c 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -270,7 +270,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    end    def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %Activity{} = activity <- Repo.get(Activity, id), +    with %Activity{} = activity <- Activity.get_by_id(id),           true <- Visibility.visible_for_user?(activity, user) do        conn        |> put_view(ActivityView) @@ -342,7 +342,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    end    def get_by_id_or_ap_id(id) do -    activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id) +    activity = Activity.get_by_id(id) || Activity.get_create_by_object_ap_id(id)      if activity.data["type"] == "Create" do        activity @@ -434,7 +434,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    end    def confirm_email(conn, %{"user_id" => uid, "token" => token}) do -    with %User{} = user <- Repo.get(User, uid), +    with %User{} = user <- User.get_by_id(uid),           true <- user.local,           true <- user.info.confirmation_pending,           true <- user.info.confirmation_token == token, @@ -587,7 +587,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    def approve_friend_request(conn, %{"user_id" => uid} = _params) do      with followed <- conn.assigns[:user], -         %User{} = follower <- Repo.get(User, uid), +         %User{} = follower <- User.get_by_id(uid),           {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do        conn        |> put_view(UserView) @@ -599,7 +599,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    def deny_friend_request(conn, %{"user_id" => uid} = _params) do      with followed <- conn.assigns[:user], -         %User{} = follower <- Repo.get(User, uid), +         %User{} = follower <- User.get_by_id(uid),           {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do        conn        |> put_view(UserView) diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex index aa1d41fa2..433322eb8 100644 --- a/lib/pleroma/web/twitter_api/views/activity_view.ex +++ b/lib/pleroma/web/twitter_api/views/activity_view.ex @@ -254,10 +254,10 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do      html =        content -      |> HTML.get_cached_scrubbed_html_for_object( +      |> HTML.get_cached_scrubbed_html_for_activity(          User.html_filter_policy(opts[:for]),          activity, -        __MODULE__ +        "twitterapi:content"        )        |> Formatter.emojify(object["emoji"]) @@ -265,7 +265,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do        if content do          content          |> String.replace(~r/<br\s?\/?>/, "\n") -        |> HTML.get_cached_stripped_html_for_object(activity, __MODULE__) +        |> HTML.get_cached_stripped_html_for_activity(activity, "twitterapi:content")        else          ""        end | 
