Resolve merge conflict
authorrinpatch <rinpatch@sdf.org>
Sun, 13 Jan 2019 10:38:28 +0000 (13:38 +0300)
committerrinpatch <rinpatch@sdf.org>
Sun, 13 Jan 2019 10:38:28 +0000 (13:38 +0300)
1  2 
config/config.exs
lib/pleroma/formatter.ex
lib/pleroma/user.ex
lib/pleroma/web/ostatus/ostatus_controller.ex
lib/pleroma/web/router.ex
mix.exs
test/web/ostatus/ostatus_controller_test.exs

diff --combined config/config.exs
index 90237d136f2ca0c4216055ff259a5f7aad8787c2,1c55807b7cfdbf629262c4dae2bdfb7b33329ec4..895dbb3ab2b8da5db104401df3b26d27a30dd5c5
@@@ -12,7 -12,7 +12,7 @@@ config :pleroma, Pleroma.Repo, types: P
  
  config :pleroma, Pleroma.Captcha,
    enabled: false,
-   seconds_retained: 180,
+   seconds_valid: 60,
    method: Pleroma.Captcha.Kocaptcha
  
  config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
@@@ -54,6 -54,17 +54,17 @@@ config :pleroma, :uri_schemes
      "xmpp"
    ]
  
+ websocket_config = [
+   path: "/websocket",
+   serializer: [
+     {Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"},
+     {Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"}
+   ],
+   timeout: 60_000,
+   transport_log: false,
+   compress: false
+ ]
  # Configures the endpoint
  config :pleroma, Pleroma.Web.Endpoint,
    url: [host: "localhost"],
@@@ -62,6 -73,8 +73,8 @@@
        {:_,
         [
           {"/api/v1/streaming", Elixir.Pleroma.Web.MastodonAPI.WebsocketHandler, []},
+          {"/socket/websocket", Phoenix.Endpoint.CowboyWebSocket,
+           {nil, {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
           {:_, Plug.Adapters.Cowboy.Handler, {Pleroma.Web.Endpoint, []}}
         ]}
      ]
@@@ -78,6 -91,12 +91,12 @@@ config :logger, :console
    format: "$time $metadata[$level] $message\n",
    metadata: [:request_id]
  
+ config :logger, :ex_syslogger,
+   level: :debug,
+   ident: "Pleroma",
+   format: "$date $time $metadata[$level] $message",
+   metadata: [:request_id]
  config :mime, :types, %{
    "application/xml" => ["xml"],
    "application/xrd+xml" => ["xrd+xml"],
@@@ -98,7 -117,8 +117,8 @@@ config :pleroma, :instance
    name: "Pleroma",
    email: "example@example.com",
    description: "A Pleroma instance, an alternative fediverse server",
-   limit: 5000,
+   limit: 5_000,
+   remote_limit: 100_000,
    upload_limit: 16_000_000,
    avatar_upload_limit: 2_000_000,
    background_upload_limit: 4_000_000,
      "text/markdown"
    ],
    finmoji_enabled: true,
-   mrf_transparency: true
+   mrf_transparency: true,
+   autofollowed_nicknames: [],
+   max_pinned_statuses: 1
  
  config :pleroma, :markup,
    # XXX - unfortunately, inline images must be enabled by default right now, because
@@@ -137,8 -159,8 +159,8 @@@ config :pleroma, :fe
    logo_mask: true,
    logo_margin: "0.1em",
    background: "/static/aurora_borealis.jpg",
-   redirect_root_no_login: "/~/main/all",
-   redirect_root_login: "/~/main/friends",
+   redirect_root_no_login: "/main/all",
+   redirect_root_login: "/main/friends",
    show_instance_panel: true,
    scope_options_enabled: false,
    formatting_options_enabled: false,
@@@ -163,6 -185,8 +185,8 @@@ config :pleroma, :mrf_rejectnonpublic
    allow_followersonly: false,
    allow_direct: false
  
+ config :pleroma, :mrf_hellthread, threshold: 10
  config :pleroma, :mrf_simple,
    media_removal: [],
    media_nsfw: [],
    reject: [],
    accept: []
  
- config :pleroma, :media_proxy,
-   enabled: false,
-   # base_url: "https://cache.pleroma.social",
-   proxy_opts: [
-     # inline_content_types: [] | false | true,
-     # http: [:insecure]
-   ]
+ config :pleroma, :media_proxy, enabled: false
  
  config :pleroma, :chat, enabled: true
  
@@@ -189,8 -207,6 +207,8 @@@ config :pleroma, :gopher
    ip: {0, 0, 0, 0},
    port: 9999
  
 +config :pleroma, :metadata, opengraph: true
 +
  config :pleroma, :suggestions,
    enabled: false,
    third_party_engine:
@@@ -220,6 -236,46 +238,46 @@@ config :cors_plug
    credentials: true,
    headers: ["Authorization", "Content-Type", "Idempotency-Key"]
  
+ config :pleroma, Pleroma.User,
+   restricted_nicknames: [
+     ".well-known",
+     "~",
+     "about",
+     "activities",
+     "api",
+     "auth",
+     "dev",
+     "friend-requests",
+     "inbox",
+     "internal",
+     "main",
+     "media",
+     "nodeinfo",
+     "notice",
+     "oauth",
+     "objects",
+     "ostatus_subscribe",
+     "pleroma",
+     "proxy",
+     "push",
+     "registration",
+     "relay",
+     "settings",
+     "status",
+     "tag",
+     "user-search",
+     "users",
+     "web"
+   ]
+ config :pleroma, Pleroma.Web.Federator, max_jobs: 50
+ config :pleroma, Pleroma.Web.Federator.RetryQueue,
+   enabled: false,
+   max_jobs: 20,
+   initial_timeout: 30,
+   max_retries: 5
  # Import environment specific config. This must remain at the bottom
  # of this file so it overrides the configuration defined above.
  import_config "#{Mix.env()}.exs"
diff --combined lib/pleroma/formatter.ex
index 74626bbc13861ab4f2114eccd6bc584e73e98b5f,d80ae6576cd071b3e9b49a35a0bdb43c25d2699c..49f7075e6f1640667bae117e9feb7325ec0d44f1
@@@ -1,3 -1,7 +1,7 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Formatter do
    alias Pleroma.User
    alias Pleroma.Web.MediaProxy
@@@ -7,6 -11,9 +11,9 @@@
    @tag_regex ~r/((?<=[^&])|\A)(\#)(\w+)/u
    @markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
  
+   # Modified from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address
+   @mentions_regex ~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]*@?[a-zA-Z0-9_-](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/u
    def parse_tags(text, data \\ %{}) do
      Regex.scan(@tag_regex, text)
      |> Enum.map(fn ["#" <> tag = full_tag | _] -> {full_tag, String.downcase(tag)} end)
          end).()
    end
  
+   @doc "Parses mentions text and returns list {nickname, user}."
+   @spec parse_mentions(binary()) :: list({binary(), User.t()})
    def parse_mentions(text) do
-     # Modified from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address
-     regex =
-       ~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]*@?[a-zA-Z0-9_-](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/u
-     Regex.scan(regex, text)
+     Regex.scan(@mentions_regex, text)
      |> List.flatten()
      |> Enum.uniq()
-     |> Enum.map(fn "@" <> match = full_match ->
-       {full_match, User.get_cached_by_nickname(match)}
+     |> Enum.map(fn nickname ->
+       with nickname <- String.trim_leading(nickname, "@"),
+            do: {"@" <> nickname, User.get_cached_by_nickname(nickname)}
      end)
      |> Enum.filter(fn {_match, user} -> user end)
    end
        String.replace(result_text, uuid, replacement)
      end)
    end
 +
 +  def truncate(text, opts \\ []) do
 +    max_length = opts[:max_length] || 200
 +    omission = opts[:omission] || "..."
 +
 +    cond do
 +      not String.valid?(text) ->
 +        text
 +
 +      String.length(text) < max_length ->
 +        text
 +
 +      true ->
 +        length_with_omission = max_length - String.length(omission)
 +
 +        "#{String.slice(text, 0, length_with_omission)}#{omission}"
 +    end
 +  end
  end
diff --combined lib/pleroma/user.ex
index c86ad4afee38238172ef9bd32935eaa051d8cfe1,68128053903e5f070b3a88413f3010e8a2f9ab7f..3120b13b63bd5018523f42c4fbe0712cdb345cbb
@@@ -1,3 -1,7 +1,7 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.User do
    use Ecto.Schema
  
@@@ -9,6 -13,8 +13,8 @@@
    alias Pleroma.Web.{OStatus, Websub, OAuth}
    alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
  
+   require Logger
    @type t :: %__MODULE__{}
  
    @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
      timestamps()
    end
  
+   def auth_active?(%User{local: false}), do: true
+   def auth_active?(%User{info: %User.Info{confirmation_pending: false}}), do: true
+   def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
+     do: !Pleroma.Config.get([:instance, :account_activation_required])
+   def auth_active?(_), do: false
+   def visible_for?(user, for_user \\ nil)
+   def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
+   def visible_for?(%User{} = user, for_user) do
+     auth_active?(user) || superuser?(for_user)
+   end
+   def visible_for?(_, _), do: false
+   def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
+   def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
+   def superuser?(_), do: false
    def avatar_url(user) do
      case user.avatar do
        %{"url" => [%{"href" => href} | _]} -> href
        note_count: user.info.note_count,
        follower_count: user.info.follower_count,
        locked: user.info.locked,
+       confirmation_pending: user.info.confirmation_pending,
        default_scope: user.info.default_scope
      }
    end
      update_and_set_cache(password_update_changeset(user, data))
    end
  
-   def register_changeset(struct, params \\ %{}) do
+   def register_changeset(struct, params \\ %{}, opts \\ []) do
+     confirmation_status =
+       if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
+         :confirmed
+       else
+         :unconfirmed
+       end
+     info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)
      changeset =
        struct
        |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
        |> validate_confirmation(:password)
        |> unique_constraint(:email)
        |> unique_constraint(:nickname)
+       |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
        |> validate_format(:nickname, local_nickname_regex())
        |> validate_format(:email, @email_regex)
        |> validate_length(:bio, max: 1000)
        |> validate_length(:name, min: 1, max: 100)
-       |> put_change(:info, %Pleroma.User.Info{})
+       |> put_change(:info, info_change)
  
      if changeset.valid? do
        hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
      end
    end
  
+   defp autofollow_users(user) do
+     candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
+     autofollowed_users =
+       from(u in User,
+         where: u.local == true,
+         where: u.nickname in ^candidates
+       )
+       |> Repo.all()
+     follow_all(user, autofollowed_users)
+   end
+   @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
+   def register(%Ecto.Changeset{} = changeset) do
+     with {:ok, user} <- Repo.insert(changeset),
+          {:ok, _} <- try_send_confirmation_email(user),
+          {:ok, user} <- autofollow_users(user) do
+       {:ok, user}
+     end
+   end
+   def try_send_confirmation_email(%User{} = user) do
+     if user.info.confirmation_pending &&
+          Pleroma.Config.get([:instance, :account_activation_required]) do
+       user
+       |> Pleroma.UserEmail.account_confirmation_email()
+       |> Pleroma.Mailer.deliver()
+     else
+       {:ok, :noop}
+     end
+   end
    def needs_update?(%User{local: true}), do: false
  
    def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
      end
    end
  
+   @doc "A mass follow for local users. Ignores blocks and has no side effects"
+   @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
+   def follow_all(follower, followeds) do
+     following =
+       (follower.following ++ Enum.map(followeds, fn %{follower_address: fa} -> fa end))
+       |> Enum.uniq()
+     {:ok, follower} =
+       follower
+       |> follow_changeset(%{following: following})
+       |> update_and_set_cache
+     Enum.each(followeds, fn followed ->
+       update_follower_count(followed)
+     end)
+     {:ok, follower}
+   end
    def follow(%User{} = follower, %User{info: info} = followed) do
      user_config = Application.get_env(:pleroma, :user)
      deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
      Enum.member?(follower.following, followed.follower_address)
    end
  
+   def follow_import(%User{} = follower, followed_identifiers)
+       when is_list(followed_identifiers) do
+     Enum.map(
+       followed_identifiers,
+       fn followed_identifier ->
+         with %User{} = followed <- get_or_fetch(followed_identifier),
+              {:ok, follower} <- maybe_direct_follow(follower, followed),
+              {:ok, _} <- ActivityPub.follow(follower, followed) do
+           followed
+         else
+           err ->
+             Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
+             err
+         end
+       end
+     )
+   end
    def locked?(%User{} = user) do
      user.info.locked || false
    end
  
 +  def get_by_id(id) do
 +    Repo.get_by(User, id: id)
 +  end
 +
    def get_by_ap_id(ap_id) do
      Repo.get_by(User, ap_id: ap_id)
    end
  
+   # This is mostly an SPC migration fix. This guesses the user nickname (by taking the last part of the ap_id and the domain) and tries to get that user
+   def get_by_guessed_nickname(ap_id) do
+     domain = URI.parse(ap_id).host
+     name = List.last(String.split(ap_id, "/"))
+     nickname = "#{name}@#{domain}"
+     get_by_nickname(nickname)
+   end
    def update_and_set_cache(changeset) do
      with {:ok, user} <- Repo.update(changeset) do
        Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
      Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
    end
  
 +  def get_cached_by_id(id) do
 +    key = "id:#{id}"
 +    Cachex.fetch!(:user_cache, key, fn _ -> get_by_id(id) end)
 +  end
 +
    def get_cached_by_nickname(nickname) do
      key = "nickname:#{nickname}"
      Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
    end
  
 +  def get_cached_by_nickname_or_id(nickname_or_id) do
 +    get_cached_by_nickname(nickname_or_id) || get_cached_by_id(nickname_or_id)
 +  end
 +
    def get_by_nickname(nickname) do
-     Repo.get_by(User, nickname: nickname)
+     Repo.get_by(User, nickname: nickname) ||
+       if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
+         [local_nickname, _] = String.split(nickname, "@")
+         Repo.get_by(User, nickname: local_nickname)
+       end
    end
  
    def get_by_nickname_or_email(nickname_or_email) do
      end
    end
  
-   def get_followers_query(%User{id: id, follower_address: follower_address}) do
+   def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
      from(
        u in User,
        where: fragment("? <@ ?", ^[follower_address], u.following),
      )
    end
  
-   def get_followers(user) do
-     q = get_followers_query(user)
+   def get_followers_query(user, page) do
+     from(
+       u in get_followers_query(user, nil),
+       limit: 20,
+       offset: ^((page - 1) * 20)
+     )
+   end
+   def get_followers_query(user), do: get_followers_query(user, nil)
+   def get_followers(user, page \\ nil) do
+     q = get_followers_query(user, page)
  
      {:ok, Repo.all(q)}
    end
  
-   def get_friends_query(%User{id: id, following: following}) do
+   def get_friends_query(%User{id: id, following: following}, nil) do
      from(
        u in User,
        where: u.follower_address in ^following,
      )
    end
  
-   def get_friends(user) do
-     q = get_friends_query(user)
+   def get_friends_query(user, page) do
+     from(
+       u in get_friends_query(user, nil),
+       limit: 20,
+       offset: ^((page - 1) * 20)
+     )
+   end
+   def get_friends_query(user), do: get_friends_query(user, nil)
+   def get_friends(user, page \\ nil) do
+     q = get_friends_query(user, page)
  
      {:ok, Repo.all(q)}
    end
        Enum.map(reqs, fn req -> req.actor end)
        |> Enum.uniq()
        |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
+       |> Enum.filter(fn u -> !is_nil(u) end)
        |> Enum.filter(fn u -> !following?(u, user) end)
  
      {:ok, users}
          select_merge: %{
            search_distance:
              fragment(
-               "? <-> (? || ?)",
+               "? <-> (? || coalesce(?, ''))",
                ^query,
                u.nickname,
                u.name
      Repo.all(q)
    end
  
+   def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
+     Enum.map(
+       blocked_identifiers,
+       fn blocked_identifier ->
+         with %User{} = blocked <- get_or_fetch(blocked_identifier),
+              {:ok, blocker} <- block(blocker, blocked),
+              {:ok, _} <- ActivityPub.block(blocker, blocked) do
+           blocked
+         else
+           err ->
+             Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
+             err
+         end
+       end
+     )
+   end
    def block(blocker, %User{ap_id: ap_id} = blocked) do
      # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
      blocker =
        end)
    end
  
+   def blocked_users(user),
+     do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
    def block_domain(user, domain) do
      info_cng =
        user.info
      Pleroma.HTML.Scrubber.TwitterText
    end
  
-   def html_filter_policy(_), do: nil
+   @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
+   def html_filter_policy(_), do: @default_scrubbers
  
    def get_or_fetch_by_ap_id(ap_id) do
      user = get_by_ap_id(ap_id)
index 5dbee20e1c9429f112db4d8b0362ca9e2f31f070,332cbef0e1dfa366d841ed8f30a72d9bc8638586..be648a6ee8bb80ba6fa3fd55eaa5f490982fb4bc
@@@ -1,3 -1,7 +1,7 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.OStatus.OStatusController do
    use Pleroma.Web, :controller
  
    def feed_redirect(conn, %{"nickname" => nickname}) do
      case get_format(conn) do
        "html" ->
 -        Fallback.RedirectController.redirector(conn, nil)
 +        with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
 +          Fallback.RedirectController.redirector_with_meta(conn, %{user: user})
 +        else
 +          nil -> {:error, :not_found}
 +        end
  
        "activity+json" ->
          ActivityPubController.call(conn, :user)
    end
  
    def activity(conn, %{"uuid" => uuid}) do
-     with id <- o_status_url(conn, :activity, uuid),
-          {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
-          {_, true} <- {:public?, ActivityPub.is_public?(activity)},
-          %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
-       case format = get_format(conn) do
-         "html" -> redirect(conn, to: "/notice/#{activity.id}")
-         _ -> represent_activity(conn, format, activity, user)
-       end
+     if get_format(conn) == "activity+json" do
+       ActivityPubController.call(conn, :activity)
      else
-       {:public?, false} ->
-         {:error, :not_found}
+       with id <- o_status_url(conn, :activity, uuid),
+            {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
+            {_, true} <- {:public?, ActivityPub.is_public?(activity)},
+            %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
+         case format = get_format(conn) do
+           "html" -> redirect(conn, to: "/notice/#{activity.id}")
+           _ -> represent_activity(conn, format, activity, user)
+         end
+       else
+         {:public?, false} ->
+           {:error, :not_found}
  
-       {:activity, nil} ->
-         {:error, :not_found}
+         {:activity, nil} ->
+           {:error, :not_found}
  
-       e ->
-         e
+         e ->
+           e
+       end
      end
    end
  
           %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
        case format = get_format(conn) do
          "html" ->
 -          conn
 -          |> put_resp_content_type("text/html")
 -          |> send_file(200, Pleroma.Plugs.InstanceStatic.file_path("index.html"))
 +          Fallback.RedirectController.redirector_with_meta(conn, %{activity: activity, user: user})
  
          _ ->
            represent_activity(conn, format, activity, user)
index 59784549d18d8701c2cbd2e42fe34e1e3ae26178,a5f4d812681ce08dba458301b0b6dbb1e8669222..5ef99bec5c5c47cbd49e1db62c6c96ec27d5a42e
@@@ -1,3 -1,7 +1,7 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.Router do
    use Pleroma.Web, :router
  
  
    scope "/api/pleroma", Pleroma.Web.TwitterAPI do
      pipe_through(:authenticated_api)
+     post("/blocks_import", UtilController, :blocks_import)
      post("/follow_import", UtilController, :follow_import)
      post("/change_password", UtilController, :change_password)
      post("/delete_account", UtilController, :delete_account)
      post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)
      post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
      post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
+     post("/statuses/:id/pin", MastodonAPIController, :pin_status)
+     post("/statuses/:id/unpin", MastodonAPIController, :unpin_status)
  
      post("/notifications/clear", MastodonAPIController, :clear_notifications)
      post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
      put("/settings", MastodonAPIController, :put_settings)
    end
  
+   scope "/api", Pleroma.Web.RichMedia do
+     pipe_through(:authenticated_api)
+     get("/rich_media/parse", RichMediaController, :parse)
+   end
    scope "/api/v1", Pleroma.Web.MastodonAPI do
      pipe_through(:api)
      get("/instance", MastodonAPIController, :masto_instance)
  
      get("/statuses/followers", TwitterAPI.Controller, :followers)
      get("/statuses/friends", TwitterAPI.Controller, :friends)
+     get("/statuses/blocks", TwitterAPI.Controller, :blocks)
      get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)
      get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
  
      post("/account/register", TwitterAPI.Controller, :register)
      post("/account/password_reset", TwitterAPI.Controller, :password_reset)
  
+     get(
+       "/account/confirm_email/:user_id/:token",
+       TwitterAPI.Controller,
+       :confirm_email,
+       as: :confirm_email
+     )
+     post("/account/resend_confirmation_email", TwitterAPI.Controller, :resend_confirmation_email)
      get("/search", TwitterAPI.Controller, :search)
      get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
    end
      post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
      post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
  
+     post("/statuses/pin/:id", TwitterAPI.Controller, :pin)
+     post("/statuses/unpin/:id", TwitterAPI.Controller, :unpin)
      get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
      post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
      post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
    end
  
    pipeline :ostatus do
 -    plug(:accepts, ["xml", "atom", "html", "activity+json"])
 +    plug(:accepts, ["html", "xml", "atom", "activity+json"])
 +  end
 +
 +  pipeline :oembed do
 +    plug(:accepts, ["json", "xml"])
    end
  
    scope "/", Pleroma.Web do
      post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
    end
  
 +  scope "/", Pleroma.Web do
 +    pipe_through(:oembed)
 +
 +    get("/oembed", OEmbed.OEmbedController, :url)
 +  end
 +
    pipeline :activitypub do
      plug(:accepts, ["activity+json"])
      plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
      get("/users/:nickname/outbox", ActivityPubController, :outbox)
    end
  
+   pipeline :activitypub_client do
+     plug(:accepts, ["activity+json"])
+     plug(:fetch_session)
+     plug(Pleroma.Plugs.OAuthPlug)
+     plug(Pleroma.Plugs.BasicAuthDecoderPlug)
+     plug(Pleroma.Plugs.UserFetcherPlug)
+     plug(Pleroma.Plugs.SessionAuthenticationPlug)
+     plug(Pleroma.Plugs.LegacyAuthenticationPlug)
+     plug(Pleroma.Plugs.AuthenticationPlug)
+     plug(Pleroma.Plugs.UserEnabledPlug)
+     plug(Pleroma.Plugs.SetUserSessionIdPlug)
+     plug(Pleroma.Plugs.EnsureUserKeyPlug)
+   end
+   scope "/", Pleroma.Web.ActivityPub do
+     pipe_through([:activitypub_client])
+     get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
+     post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
+   end
    scope "/relay", Pleroma.Web.ActivityPub do
      pipe_through(:ap_relay)
      get("/", ActivityPubController, :relay)
@@@ -466,26 -503,11 +513,26 @@@ en
  
  defmodule Fallback.RedirectController do
    use Pleroma.Web, :controller
 +  alias Pleroma.Web.Metadata
  
    def redirector(conn, _params) do
      conn
      |> put_resp_content_type("text/html")
 -    |> send_file(200, Pleroma.Plugs.InstanceStatic.file_path("index.html"))
 +    |> send_file(200, index_file_path())
 +  end
 +
 +  def redirector_with_meta(conn, params) do
 +    {:ok, index_content} = File.read(index_file_path())
 +    tags = Metadata.build_tags(params)
 +    response = String.replace(index_content, "<!--server-generated-meta-->", tags)
 +
 +    conn
 +    |> put_resp_content_type("text/html")
 +    |> send_resp(200, response)
 +  end
 +
 +  def index_file_path do
 +    Pleroma.Plugs.InstanceStatic.file_path("index.html")
    end
  
    def registration_page(conn, params) do
diff --combined mix.exs
index 31ebcc5f3cfb7b0f1ecfea0c2edcab2b37f60311,ccf7790b088af1266bc2ae991f6d6e44b471575d..d46998891d15f5688ea178993cc7ce13abdeb325
+++ b/mix.exs
@@@ -5,7 -5,7 +5,7 @@@ defmodule Pleroma.Mixfile d
      [
        app: :pleroma,
        version: version("0.9.0"),
-       elixir: "~> 1.4",
+       elixir: "~> 1.7",
        elixirc_paths: elixirc_paths(Mix.env()),
        compilers: [:phoenix, :gettext] ++ Mix.compilers(),
        elixirc_options: [warnings_as_errors: true],
@@@ -21,8 -21,9 +21,9 @@@
        homepage_url: "https://pleroma.social/",
        docs: [
          logo: "priv/static/static/logo.png",
-         extras: ["README.md", "config/config.md"],
-         main: "readme"
+         extras: ["README.md", "docs/config.md", "docs/Pleroma-API.md", "docs/Admin-API.md"],
+         main: "readme",
+         output: "priv/static/doc"
        ]
      ]
    end
    #
    # Type `mix help compile.app` for more information.
    def application do
-     [mod: {Pleroma.Application, []}, extra_applications: [:logger, :runtime_tools, :comeonin]]
+     [
+       mod: {Pleroma.Application, []},
+       extra_applications: [:logger, :runtime_tools, :comeonin],
+       included_applications: [:ex_syslogger]
+     ]
    end
  
    # Specifies which paths to compile per environment.
@@@ -54,7 -59,6 +59,7 @@@
        {:pbkdf2_elixir, "~> 0.12.3"},
        {:trailing_format_plug, "~> 0.0.7"},
        {:html_sanitize_ex, "~> 1.3.0"},
 +      {:html_entities, "~> 0.4"},
        {:phoenix_html, "~> 2.10"},
        {:calendar, "~> 0.17.4"},
        {:cachex, "~> 3.0.2"},
        {:crypt,
         git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"},
        {:cors_plug, "~> 1.5"},
-       {:ex_doc, "> 0.18.3 and < 0.20.0", only: :dev, runtime: false},
+       {:ex_doc, "~> 0.19", only: :dev, runtime: false},
        {:web_push_encryption, "~> 0.2.1"},
        {:swoosh, "~> 0.20"},
        {:gen_smtp, "~> 0.13"},
-       {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test}
+       {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test},
+       {:floki, "~> 0.20.0"},
+       {:ex_syslogger, github: "slashmili/ex_syslogger", tag: "1.4.0"}
      ]
    end
  
index e9e9bdb169b12064fe4727be67775ba9271d3305,995cc00d6359973393a0c1f6f5dd9a684d6d0498..8e9d2b69ae7055f6a650efdd6d4a8d51f64b21f3
@@@ -1,7 -1,11 +1,11 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.OStatus.OStatusControllerTest do
    use Pleroma.Web.ConnCase
    import Pleroma.Factory
-   alias Pleroma.{User, Repo}
+   alias Pleroma.{User, Repo, Object}
    alias Pleroma.Web.CommonAPI
    alias Pleroma.Web.OStatus.ActivityRepresenter
  
@@@ -84,7 -88,6 +88,7 @@@
  
      conn =
        conn
 +      |> put_req_header("accept", "application/xml")
        |> get(url)
  
      expected =
      |> response(404)
    end
  
 -  test "gets an activity", %{conn: conn} do
 -    note_activity = insert(:note_activity)
 -    [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
 -
 -    conn
 -    |> get("/activities/#{uuid}")
 -    |> response(200)
 -  end
 -
 +  test "gets an activity in xml format", %{conn: conn} do
 +    note_activity = insert(:note_activity)
 +    [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
 +
 +    conn
 +    |> put_req_header("accept", "application/xml")
 +    |> get("/activities/#{uuid}")
 +    |> response(200)
 +  end
 +
+   test "404s on deleted objects", %{conn: conn} do
+     note_activity = insert(:note_activity)
+     [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["object"]["id"]))
+     object = Object.get_by_ap_id(note_activity.data["object"]["id"])
+     conn
+     |> get("/objects/#{uuid}")
+     |> response(200)
+     Object.delete(object)
+     conn
+     |> get("/objects/#{uuid}")
+     |> response(404)
+   end
    test "404s on private activities", %{conn: conn} do
      note_activity = insert(:direct_note_activity)
      [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
      |> response(404)
    end
  
 -  test "gets a notice", %{conn: conn} do
 +  test "renders notice metatags in html format", %{conn: conn} do
 +    note_activity = insert(:note_activity)
 +    conn = get(conn, "/notice/#{note_activity.id}")
 +    body = html_response(conn, 200)
 +    twitter_card_summary = "<meta content=\"summary\" property=\"twitter:card\">"
 +
 +    description_content =
 +      "<meta content=\"#{note_activity.data["object"]["content"]}\" property=\"og:description\">"
 +
 +    assert body =~ twitter_card_summary
 +    assert body =~ description_content
 +  end
 +
 +  test "gets a notice in xml format", %{conn: conn} do
      note_activity = insert(:note_activity)
  
      conn