diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1b7c03ebb..aad28a2d8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -48,6 +48,7 @@ benchmark:
unit-testing:
stage: test
+ retry: 2
cache: &testing_cache_policy
<<: *global_cache_policy
policy: pull
@@ -80,6 +81,7 @@ unit-testing:
unit-testing-rum:
stage: test
+ retry: 2
cache: *testing_cache_policy
services:
- name: minibikini/postgres-with-rum:12
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f2c9e106e..b385264f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -53,6 +53,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed
- Healthcheck reporting the number of memory currently used, rather than allocated in total
+- `InsertSkeletonsForDeletedUsers` failing on some instances
## [2.0.3] - 2020-05-02
diff --git a/benchmarks/load_testing/users.ex b/benchmarks/load_testing/users.ex
index 1a8c6e22f..e4d0b22ff 100644
--- a/benchmarks/load_testing/users.ex
+++ b/benchmarks/load_testing/users.ex
@@ -55,7 +55,7 @@ defp generate_user(i) do
name: "Test テスト User #{i}",
email: "user#{i}@example.com",
nickname: "nick#{i}",
- password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
+ password_hash: Pbkdf2.hash_pwd_salt("test"),
bio: "Tester Number #{i}",
local: !remote
}
diff --git a/config/description.exs b/config/description.exs
index 504161a9f..36ec3d40a 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -28,7 +28,8 @@
%{
key: :filters,
type: {:list, :module},
- description: "List of filter modules for uploads",
+ description:
+ "List of filter modules for uploads. Module names are shortened (removed leading `Pleroma.Upload.Filter.` part), but on adding custom module you need to use full name.",
suggestions:
Generator.list_modules_in_dir(
"lib/pleroma/upload/filter",
@@ -681,7 +682,8 @@
%{
key: :federation_publisher_modules,
type: {:list, :module},
- description: "List of modules for federation publishing",
+ description:
+ "List of modules for federation publishing. Module names are shortened (removed leading `Pleroma.Web.` part), but on adding custom module you need to use full name.",
suggestions: [
Pleroma.Web.ActivityPub.Publisher
]
@@ -694,7 +696,8 @@
%{
key: :rewrite_policy,
type: [:module, {:list, :module}],
- description: "A list of MRF policies enabled",
+ description:
+ "A list of enabled MRF policies. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.",
suggestions:
Generator.list_modules_in_dir(
"lib/pleroma/web/activity_pub/mrf",
@@ -2031,7 +2034,8 @@
%{
key: :parsers,
type: {:list, :module},
- description: "List of Rich Media parsers.",
+ description:
+ "List of Rich Media parsers. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parsers.` part), but on adding custom module you need to use full name.",
suggestions: [
Pleroma.Web.RichMedia.Parsers.MetaTagsParser,
Pleroma.Web.RichMedia.Parsers.OEmbed,
@@ -2043,7 +2047,8 @@
key: :ttl_setters,
label: "TTL setters",
type: {:list, :module},
- description: "List of rich media TTL setters.",
+ description:
+ "List of rich media TTL setters. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parser.` part), but on adding custom module you need to use full name.",
suggestions: [
Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl
]
@@ -2717,6 +2722,8 @@
%{
key: :scrub_policy,
type: {:list, :module},
+ description:
+ "Module names are shortened (removed leading `Pleroma.HTML.` part), but on adding custom module you need to use full name.",
suggestions: [Pleroma.HTML.Transform.MediaProxy, Pleroma.HTML.Scrubber.Default]
}
]
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index a00bc0624..9d3d92b38 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -56,7 +56,7 @@ def start(_type, _args) do
if (major == 22 and minor < 2) or major < 22 do
raise "
!!!OTP VERSION WARNING!!!
- You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains.
+ You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains. Please update your Erlang/OTP to at least 22.2.
"
end
else
diff --git a/lib/pleroma/bbs/authenticator.ex b/lib/pleroma/bbs/authenticator.ex
index e5b37f33e..d4494b003 100644
--- a/lib/pleroma/bbs/authenticator.ex
+++ b/lib/pleroma/bbs/authenticator.ex
@@ -4,7 +4,6 @@
defmodule Pleroma.BBS.Authenticator do
use Sshd.PasswordAuthenticator
- alias Comeonin.Pbkdf2
alias Pleroma.User
def authenticate(username, password) do
@@ -12,7 +11,7 @@ def authenticate(username, password) do
password = to_string(password)
with %User{} = user <- User.get_by_nickname(username) do
- Pbkdf2.checkpw(password, user.password_hash)
+ Pbkdf2.verify_pass(password, user.password_hash)
else
_e -> false
end
diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex
index c7bc8ef6c..12d64c2fe 100644
--- a/lib/pleroma/bbs/handler.ex
+++ b/lib/pleroma/bbs/handler.ex
@@ -66,7 +66,7 @@ def handle_command(%{user: user} = state, "r " <> text) do
with %Activity{} <- Activity.get_by_id(activity_id),
{:ok, _activity} <-
- CommonAPI.post(user, %{"status" => rest, "in_reply_to_status_id" => activity_id}) do
+ CommonAPI.post(user, %{status: rest, in_reply_to_status_id: activity_id}) do
IO.puts("Replied!")
else
_e -> IO.puts("Could not reply...")
@@ -78,7 +78,7 @@ def handle_command(%{user: user} = state, "r " <> text) do
def handle_command(%{user: user} = state, "p " <> text) do
text = String.trim(text)
- with {:ok, _activity} <- CommonAPI.post(user, %{"status" => text}) do
+ with {:ok, _activity} <- CommonAPI.post(user, %{status: text}) do
IO.puts("Posted!")
else
_e -> IO.puts("Could not post...")
diff --git a/lib/pleroma/mfa.ex b/lib/pleroma/mfa.ex
index d353a4dad..2b77f5426 100644
--- a/lib/pleroma/mfa.ex
+++ b/lib/pleroma/mfa.ex
@@ -7,7 +7,6 @@ defmodule Pleroma.MFA do
The MFA context.
"""
- alias Comeonin.Pbkdf2
alias Pleroma.User
alias Pleroma.MFA.BackupCodes
@@ -72,7 +71,7 @@ def invalidate_backup_code(%User{} = user, hash_code) do
@spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
def generate_backup_codes(%User{} = user) do
with codes <- BackupCodes.generate(),
- hashed_codes <- Enum.map(codes, &Pbkdf2.hashpwsalt/1),
+ hashed_codes <- Enum.map(codes, &Pbkdf2.hash_pwd_salt/1),
changeset <- Changeset.cast_backup_codes(user, hashed_codes),
{:ok, _} <- User.update_and_set_cache(changeset) do
{:ok, codes}
diff --git a/lib/pleroma/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex
index 0061c69dc..ae4a235bd 100644
--- a/lib/pleroma/plugs/authentication_plug.ex
+++ b/lib/pleroma/plugs/authentication_plug.ex
@@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.AuthenticationPlug do
- alias Comeonin.Pbkdf2
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
@@ -18,7 +17,7 @@ def checkpw(password, "$6" <> _ = password_hash) do
end
def checkpw(password, "$pbkdf2" <> _ = password_hash) do
- Pbkdf2.checkpw(password, password_hash)
+ Pbkdf2.verify_pass(password, password_hash)
end
def checkpw(_password, _password_hash) do
@@ -37,7 +36,7 @@ def call(
} = conn,
_
) do
- if Pbkdf2.checkpw(password, password_hash) do
+ if Pbkdf2.verify_pass(password, password_hash) do
conn
|> assign(:user, auth_user)
|> OAuthScopesPlug.skip_plug()
@@ -47,7 +46,7 @@ def call(
end
def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do
- Pbkdf2.dummy_checkpw()
+ Pbkdf2.no_user_verify()
conn
end
diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex
index 8ff06a462..0937cb7db 100644
--- a/lib/pleroma/scheduled_activity.ex
+++ b/lib/pleroma/scheduled_activity.ex
@@ -40,7 +40,7 @@ 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})
+ media_attachments = Utils.attachments_from_ids(%{media_ids: media_ids})
params =
params
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 2a6a23fec..cba391072 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -9,7 +9,6 @@ defmodule Pleroma.User do
import Ecto.Query
import Ecto, only: [assoc: 2]
- alias Comeonin.Pbkdf2
alias Ecto.Multi
alias Pleroma.Activity
alias Pleroma.Config
@@ -1554,10 +1553,23 @@ def delete_user_activities(%User{ap_id: ap_id} = user) do
|> Stream.run()
end
- defp delete_activity(%{data: %{"type" => "Create", "object" => object}}, user) do
- {:ok, delete_data, _} = Builder.delete(user, object)
+ defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
+ with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
+ {:ok, delete_data, _} <- Builder.delete(user, object) do
+ Pipeline.common_pipeline(delete_data, local: user.local)
+ else
+ {:find_object, nil} ->
+ # We have the create activity, but not the object, it was probably pruned.
+ # Insert a tombstone and try again
+ with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
+ {:ok, _tombstone} <- Object.create(tombstone_data) do
+ delete_activity(activity, user)
+ end
- Pipeline.common_pipeline(delete_data, local: user.local)
+ e ->
+ Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
+ Logger.error("Error: #{inspect(e)}")
+ end
end
defp delete_activity(%{data: %{"type" => type}} = activity, user)
@@ -1913,7 +1925,7 @@ def get_ap_ids_by_nicknames(nicknames) do
defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do
- change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
+ change(changeset, password_hash: Pbkdf2.hash_pwd_salt(password))
end
defp put_password_hash(changeset), do: changeset
diff --git a/lib/pleroma/user/welcome_message.ex b/lib/pleroma/user/welcome_message.ex
index f0ac8ebae..f8f520285 100644
--- a/lib/pleroma/user/welcome_message.ex
+++ b/lib/pleroma/user/welcome_message.ex
@@ -10,8 +10,8 @@ def post_welcome_message_to_user(user) do
with %User{} = sender_user <- welcome_user(),
message when is_binary(message) <- welcome_message() do
CommonAPI.post(sender_user, %{
- "visibility" => "direct",
- "status" => "@#{user.nickname}\n#{message}"
+ visibility: "direct",
+ status: "@#{user.nickname}\n#{message}"
})
else
_ -> {:ok, nil}
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 4955243ab..d752f4f04 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -439,7 +439,6 @@ def block(blocker, blocked, activity_id \\ nil, local \\ true) do
end
defp do_block(blocker, blocked, activity_id, local) do
- outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
if unfollow_blocked do
@@ -447,8 +446,7 @@ defp do_block(blocker, blocked, activity_id, local) do
if follow_activity, do: unfollow(blocker, blocked, nil, local)
end
- with true <- outgoing_blocks,
- block_data <- make_block_data(blocker, blocked, activity_id),
+ with block_data <- make_block_data(blocker, blocked, activity_id),
{:ok, activity} <- insert(block_data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
index 922a444a9..4a247ad0c 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -62,6 +62,16 @@ def delete(actor, object_id) do
}, []}
end
+ @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
+ def tombstone(actor, id) do
+ {:ok,
+ %{
+ "id" => id,
+ "actor" => actor,
+ "type" => "Tombstone"
+ }, []}
+ end
+
@spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
def like(actor, object) do
with {:ok, data, meta} <- object_action(actor, object) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
index e06de3dff..f42c03510 100644
--- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
@@ -51,6 +51,7 @@ def add_deleted_activity_id(cng) do
Page
Question
Video
+ Tombstone
}
def validate_data(cng) do
cng
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index be7b57f13..80701bb63 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -14,7 +14,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@@ -590,6 +592,9 @@ def handle_incoming(
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
+ User.update_follower_count(followed)
+ User.update_following_count(follower)
+
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
@@ -599,7 +604,8 @@ def handle_incoming(
activity_id: id
})
else
- _e -> :error
+ _e ->
+ :error
end
end
@@ -720,6 +726,19 @@ def handle_incoming(
) do
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
+ else
+ {:error, {:validate_object, _}} = e ->
+ # Check if we have a create activity for this
+ with {:ok, object_id} <- Types.ObjectID.cast(data["object"]),
+ %Activity{data: %{"actor" => actor}} <-
+ Activity.create_by_object_ap_id(object_id) |> Repo.one(),
+ # We have one, insert a tombstone and retry
+ {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
+ {:ok, _tombstone} <- Object.create(tombstone_data) do
+ handle_incoming(data)
+ else
+ _ -> e
+ end
end
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 09b80fa57..f2375bcc4 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Ecto.Changeset
alias Ecto.UUID
alias Pleroma.Activity
+ alias Pleroma.Config
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
@@ -169,8 +170,11 @@ def create_context(context) do
Enqueues an activity for federation if it's local
"""
@spec maybe_federate(any()) :: :ok
- def maybe_federate(%Activity{local: true} = activity) do
- if Pleroma.Config.get!([:instance, :federating]) do
+ def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) do
+ outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
+
+ with true <- Config.get!([:instance, :federating]),
+ true <- type != "Block" || outgoing_blocks do
Pleroma.Web.Federator.publish(activity)
end
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 4db9f4cac..9175b1904 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -844,15 +844,20 @@ def status_show(conn, %{"id" => id}) do
end
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
+ params =
+ params
+ |> Map.take(["sensitive", "visibility"])
+ |> Map.new(fn {key, value} -> {String.to_existing_atom(key), value} end)
+
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
- {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
+ {:ok, sensitive} = Ecto.Type.cast(:boolean, params[:sensitive])
ModerationLog.insert_log(%{
action: "status_update",
actor: admin,
subject: activity,
sensitive: sensitive,
- visibility: params["visibility"]
+ visibility: params[:visibility]
})
conn
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
new file mode 100644
index 000000000..a6bb87560
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -0,0 +1,499 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
), "\n") |> Pleroma.HTML.strip_tags()
+ bio
+ |> String.replace(~r(
), "\n")
+ |> Pleroma.HTML.strip_tags()
+ |> HtmlEntities.decode()
end
defp prepare_user_bio(_), do: ""
@@ -334,7 +337,11 @@ defp maybe_put_role(data, %User{id: user_id} = user, %User{id: user_id}) do
defp maybe_put_role(data, _, _), do: data
defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do
- Kernel.put_in(data, [:pleroma, :notification_settings], user.notification_settings)
+ Kernel.put_in(
+ data,
+ [:pleroma, :notification_settings],
+ Map.from_struct(user.notification_settings)
+ )
end
defp maybe_put_notification_settings(data, _, _), do: data
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index e2ffd02d0..94e4595d8 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -12,31 +12,19 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
@behaviour :cowboy_websocket
+ # Client ping period.
+ @tick :timer.seconds(30)
# Cowboy timeout period.
- @timeout :timer.seconds(30)
+ @timeout :timer.seconds(60)
# Hibernate every X messages
@hibernate_every 100
- @streams [
- "public",
- "public:local",
- "public:media",
- "public:local:media",
- "user",
- "user:notification",
- "direct",
- "list",
- "hashtag"
- ]
- @anonymous_streams ["public", "public:local", "hashtag"]
-
def init(%{qs: qs} = req, state) do
- with params <- :cow_qs.parse_qs(qs),
+ with params <- Enum.into(:cow_qs.parse_qs(qs), %{}),
sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil),
- access_token <- List.keyfind(params, "access_token", 0),
- {_, stream} <- List.keyfind(params, "stream", 0),
- {:ok, user} <- allow_request(stream, [access_token, sec_websocket]),
- topic when is_binary(topic) <- expand_topic(stream, params) do
+ access_token <- Map.get(params, "access_token"),
+ {:ok, user} <- authenticate_request(access_token, sec_websocket),
+ {:ok, topic} <- Streamer.get_topic(Map.get(params, "stream"), user, params) do
req =
if sec_websocket do
:cowboy_req.set_resp_header("sec-websocket-protocol", sec_websocket, req)
@@ -44,16 +32,17 @@ def init(%{qs: qs} = req, state) do
req
end
- {:cowboy_websocket, req, %{user: user, topic: topic, count: 0}, %{idle_timeout: @timeout}}
+ {:cowboy_websocket, req, %{user: user, topic: topic, count: 0, timer: nil},
+ %{idle_timeout: @timeout}}
else
- {:error, code} ->
- Logger.debug("#{__MODULE__} denied connection: #{inspect(code)} - #{inspect(req)}")
- {:ok, req} = :cowboy_req.reply(code, req)
+ {:error, :bad_topic} ->
+ Logger.debug("#{__MODULE__} bad topic #{inspect(req)}")
+ {:ok, req} = :cowboy_req.reply(404, req)
{:ok, req, state}
- error ->
- Logger.debug("#{__MODULE__} denied connection: #{inspect(error)} - #{inspect(req)}")
- {:ok, req} = :cowboy_req.reply(400, req)
+ {:error, :unauthorized} ->
+ Logger.debug("#{__MODULE__} authentication error: #{inspect(req)}")
+ {:ok, req} = :cowboy_req.reply(401, req)
{:ok, req, state}
end
end
@@ -66,11 +55,18 @@ def websocket_init(state) do
)
Streamer.add_socket(state.topic, state.user)
- {:ok, state}
+ {:ok, %{state | timer: timer()}}
+ end
+
+ # Client's Pong frame.
+ def websocket_handle(:pong, state) do
+ if state.timer, do: Process.cancel_timer(state.timer)
+ {:ok, %{state | timer: timer()}}
end
# We never receive messages.
- def websocket_handle(_frame, state) do
+ def websocket_handle(frame, state) do
+ Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
{:ok, state}
end
@@ -94,6 +90,14 @@ def websocket_info({:text, message}, state) do
end
end
+ # Ping tick. We don't re-queue a timer there, it is instead queued when :pong is received.
+ # As we hibernate there, reset the count to 0.
+ # If the client misses :pong, Cowboy will automatically timeout the connection after
+ # `@idle_timeout`.
+ def websocket_info(:tick, state) do
+ {:reply, :ping, %{state | timer: nil, count: 0}, :hibernate}
+ end
+
def terminate(reason, _req, state) do
Logger.debug(
"#{__MODULE__} terminating websocket connection for user #{
@@ -106,47 +110,24 @@ def terminate(reason, _req, state) do
end
# Public streams without authentication.
- defp allow_request(stream, [nil, nil]) when stream in @anonymous_streams do
+ defp authenticate_request(nil, nil) do
{:ok, nil}
end
# Authenticated streams.
- defp allow_request(stream, [access_token, sec_websocket]) when stream in @streams do
- token =
- with {"access_token", token} <- access_token do
- token
- else
- _ -> sec_websocket
- end
+ defp authenticate_request(access_token, sec_websocket) do
+ token = access_token || sec_websocket
with true <- is_bitstring(token),
%Token{user_id: user_id} <- Repo.get_by(Token, token: token),
user = %User{} <- User.get_cached_by_id(user_id) do
{:ok, user}
else
- _ -> {:error, 403}
+ _ -> {:error, :unauthorized}
end
end
- # Not authenticated.
- defp allow_request(stream, _) when stream in @streams, do: {:error, 403}
-
- # No matching stream.
- defp allow_request(_, _), do: {:error, 404}
-
- defp expand_topic("hashtag", params) do
- case List.keyfind(params, "tag", 0) do
- {_, tag} -> "hashtag:#{tag}"
- _ -> nil
- end
+ defp timer do
+ Process.send_after(self(), :tick, @tick)
end
-
- defp expand_topic("list", params) do
- case List.keyfind(params, "list", 0) do
- {_, list} -> "list:#{list}"
- _ -> nil
- end
- end
-
- defp expand_topic(topic, _), do: topic
end
diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
index 1ed6ee521..0814b3bc3 100644
--- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
+++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
@@ -5,7 +5,6 @@
defmodule Pleroma.Web.MongooseIM.MongooseIMController do
use Pleroma.Web, :controller
- alias Comeonin.Pbkdf2
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.User
@@ -28,7 +27,7 @@ def user_exists(conn, %{"user" => username}) do
def check_password(conn, %{"user" => username, "pass" => password}) do
with %User{password_hash: password_hash, deactivated: false} <-
Repo.get_by(User, nickname: username, local: true),
- true <- Pbkdf2.checkpw(password, password_hash) do
+ true <- Pbkdf2.verify_pass(password, password_hash) do
conn
|> json(true)
else
diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex
index 5ad4aa936..49a400df7 100644
--- a/lib/pleroma/web/streamer/streamer.ex
+++ b/lib/pleroma/web/streamer/streamer.ex
@@ -21,12 +21,68 @@ defmodule Pleroma.Web.Streamer do
def registry, do: @registry
- def add_socket(topic, %User{} = user) do
- if should_env_send?(), do: Registry.register(@registry, user_topic(topic, user), true)
+ @public_streams ["public", "public:local", "public:media", "public:local:media"]
+ @user_streams ["user", "user:notification", "direct"]
+
+ @doc "Expands and authorizes a stream, and registers the process for streaming."
+ @spec get_topic_and_add_socket(stream :: String.t(), User.t() | nil, Map.t() | nil) ::
+ {:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized}
+ def get_topic_and_add_socket(stream, user, params \\ %{}) do
+ case get_topic(stream, user, params) do
+ {:ok, topic} -> add_socket(topic, user)
+ error -> error
+ end
end
- def add_socket(topic, _) do
- if should_env_send?(), do: Registry.register(@registry, topic, false)
+ @doc "Expand and authorizes a stream"
+ @spec get_topic(stream :: String.t(), User.t() | nil, Map.t()) ::
+ {:ok, topic :: String.t()} | {:error, :bad_topic}
+ def get_topic(stream, user, params \\ %{})
+
+ # Allow all public steams.
+ def get_topic(stream, _, _) when stream in @public_streams do
+ {:ok, stream}
+ end
+
+ # Allow all hashtags streams.
+ def get_topic("hashtag", _, %{"tag" => tag}) do
+ {:ok, "hashtag:" <> tag}
+ end
+
+ # Expand user streams.
+ def get_topic(stream, %User{} = user, _) when stream in @user_streams do
+ {:ok, stream <> ":" <> to_string(user.id)}
+ end
+
+ def get_topic(stream, _, _) when stream in @user_streams do
+ {:error, :unauthorized}
+ end
+
+ # List streams.
+ def get_topic("list", %User{} = user, %{"list" => id}) do
+ if Pleroma.List.get(id, user) do
+ {:ok, "list:" <> to_string(id)}
+ else
+ {:error, :bad_topic}
+ end
+ end
+
+ def get_topic("list", _, _) do
+ {:error, :unauthorized}
+ end
+
+ def get_topic(_, _, _) do
+ {:error, :bad_topic}
+ end
+
+ @doc "Registers the process for streaming. Use `get_topic/3` to get the full authorized topic."
+ def add_socket(topic, user) do
+ if should_env_send?() do
+ auth? = if user, do: true
+ Registry.register(@registry, topic, auth?)
+ end
+
+ {:ok, topic}
end
def remove_socket(topic) do
@@ -231,13 +287,4 @@ def should_env_send?, do: false
true ->
def should_env_send?, do: true
end
-
- defp user_topic(topic, user)
- when topic in ~w[user user:notification direct] do
- "#{topic}:#{user.id}"
- end
-
- defp user_topic(topic, _) do
- topic
- end
end
diff --git a/lib/pleroma/workers/scheduled_activity_worker.ex b/lib/pleroma/workers/scheduled_activity_worker.ex
index 8905f4ad0..97d1efbfb 100644
--- a/lib/pleroma/workers/scheduled_activity_worker.ex
+++ b/lib/pleroma/workers/scheduled_activity_worker.ex
@@ -30,6 +30,8 @@ def perform(%{"activity_id" => activity_id}, _job) do
end
defp post_activity(%ScheduledActivity{user_id: user_id, params: params} = scheduled_activity) do
+ params = Map.new(params, fn {key, value} -> {String.to_existing_atom(key), value} end)
+
with {:delete, {:ok, _}} <- {:delete, ScheduledActivity.delete(scheduled_activity)},
{:user, %User{} = user} <- {:user, User.get_cached_by_id(user_id)},
{:post, {:ok, _}} <- {:post, CommonAPI.post(user, params)} do
diff --git a/mix.exs b/mix.exs
index 6d65e18d4..0186d291f 100644
--- a/mix.exs
+++ b/mix.exs
@@ -36,7 +36,7 @@ def project do
releases: [
pleroma: [
include_executables_for: [:unix],
- applications: [ex_syslogger: :load, syslog: :load],
+ applications: [ex_syslogger: :load, syslog: :load, eldap: :transient],
steps: [:assemble, &put_otp_version/1, ©_files/1, ©_nginx_config/1]
]
]
@@ -72,7 +72,14 @@ def copy_nginx_config(%{path: target_path} = release) do
def application do
[
mod: {Pleroma.Application, []},
- extra_applications: [:logger, :runtime_tools, :comeonin, :quack, :fast_sanitize, :ssl],
+ extra_applications: [
+ :logger,
+ :runtime_tools,
+ :comeonin,
+ :quack,
+ :fast_sanitize,
+ :ssl
+ ],
included_applications: [:ex_syslogger]
]
end
@@ -119,8 +126,7 @@ defp deps do
{:postgrex, ">= 0.13.5"},
{:oban, "~> 1.2"},
{:gettext, "~> 0.15"},
- {:comeonin, "~> 4.1.1"},
- {:pbkdf2_elixir, "~> 0.12.3"},
+ {:pbkdf2_elixir, "~> 1.0"},
{:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.1"},
{:html_entities, "~> 0.5", override: true},
@@ -192,7 +198,7 @@ defp deps do
{:restarter, path: "./restarter"},
{:open_api_spex,
git: "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git",
- ref: "b862ebd78de0df95875cf46feb6e9607130dc2a8"}
+ ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"}
] ++ oauth_deps()
end
diff --git a/mix.lock b/mix.lock
index c400202b7..62fe35146 100644
--- a/mix.lock
+++ b/mix.lock
@@ -13,7 +13,7 @@
"castore": {:hex, :castore, "0.1.5", "591c763a637af2cc468a72f006878584bc6c306f8d111ef8ba1d4c10e0684010", [:mix], [], "hexpm", "6db356b2bc6cc22561e051ff545c20ad064af57647e436650aa24d7d06cd941a"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
- "comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm", "d8700a0ca4dbb616c22c9b3f6dd539d88deaafec3efe66869d6370c9a559b3e9"},
+ "comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9af027d20dc12dd0c4345a6b87247e0c62965871feea0bfecf9764648b02cc69"},
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
@@ -74,9 +74,9 @@
"nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
"oban": {:hex, :oban, "1.2.0", "7cca94d341be43d220571e28f69131c4afc21095b25257397f50973d3fc59b07", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba5f8b3f7d76967b3e23cf8014f6a13e4ccb33431e4808f036709a7f822362ee"},
- "open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "b862ebd78de0df95875cf46feb6e9607130dc2a8", [ref: "b862ebd78de0df95875cf46feb6e9607130dc2a8"]},
+ "open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "f296ac0924ba3cf79c7a588c4c252889df4c2edd", [ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"]},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
- "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm", "595d09db74cb093b1903381c9de423276a931a2480a46a1a5dc7f932a2a6375b"},
+ "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"},
"phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ab765a0feddb81fc62e2116c827b5f068df85159c162bee760745276ad7ddc1b"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
"phoenix_html": {:hex, :phoenix_html, "2.14.0", "d8c6bc28acc8e65f8ea0080ee05aa13d912c8758699283b8d3427b655aabe284", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "b0bb30eda478a06dbfbe96728061a93833db3861a49ccb516f839ecb08493fbb"},
diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po
deleted file mode 100644
index 25a2f73e4..000000000
--- a/priv/gettext/en/LC_MESSAGES/errors.po
+++ /dev/null
@@ -1,465 +0,0 @@
-## `msgid`s in this file come from POT (.pot) files.
-##
-## Do not add, change, or remove `msgid`s manually here as
-## they're tied to the ones in the corresponding POT file
-## (with the same domain).
-##
-## Use `mix gettext.extract --merge` or `mix gettext.merge`
-## to merge POT files into PO files.
-msgid ""
-msgstr ""
-"Language: en\n"
-
-## From Ecto.Changeset.cast/4
-msgid "can't be blank"
-msgstr ""
-
-## From Ecto.Changeset.unique_constraint/3
-msgid "has already been taken"
-msgstr ""
-
-## From Ecto.Changeset.put_change/3
-msgid "is invalid"
-msgstr ""
-
-## From Ecto.Changeset.validate_format/3
-msgid "has invalid format"
-msgstr ""
-
-## From Ecto.Changeset.validate_subset/3
-msgid "has an invalid entry"
-msgstr ""
-
-## From Ecto.Changeset.validate_exclusion/3
-msgid "is reserved"
-msgstr ""
-
-## From Ecto.Changeset.validate_confirmation/3
-msgid "does not match confirmation"
-msgstr ""
-
-## From Ecto.Changeset.no_assoc_constraint/3
-msgid "is still associated with this entry"
-msgstr ""
-
-msgid "are still associated with this entry"
-msgstr ""
-
-## From Ecto.Changeset.validate_length/3
-msgid "should be %{count} character(s)"
-msgid_plural "should be %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should have %{count} item(s)"
-msgid_plural "should have %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be at least %{count} character(s)"
-msgid_plural "should be at least %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should have at least %{count} item(s)"
-msgid_plural "should have at least %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be at most %{count} character(s)"
-msgid_plural "should be at most %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should have at most %{count} item(s)"
-msgid_plural "should have at most %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-## From Ecto.Changeset.validate_number/3
-msgid "must be less than %{number}"
-msgstr ""
-
-msgid "must be greater than %{number}"
-msgstr ""
-
-msgid "must be less than or equal to %{number}"
-msgstr ""
-
-msgid "must be greater than or equal to %{number}"
-msgstr ""
-
-msgid "must be equal to %{number}"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:381
-msgid "Account not found"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:153
-msgid "Already voted"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:263
-msgid "Bad request"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:254
-msgid "Can't delete object"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:569
-msgid "Can't delete this post"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1731
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1737
-msgid "Can't display this activity"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:195
-msgid "Can't find user"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1148
-msgid "Can't get favorites"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:263
-msgid "Can't like object"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:518
-msgid "Cannot post an empty status without attachments"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:461
-msgid "Comment must be up to %{max_size} characters"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/admin_api/config.ex:63
-msgid "Config with params %{params} not found"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:78
-msgid "Could not delete"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:110
-msgid "Could not favorite"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:310
-msgid "Could not pin"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:89
-msgid "Could not repeat"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:120
-msgid "Could not unfavorite"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:327
-msgid "Could not unpin"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:99
-msgid "Could not unrepeat"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:392
-msgid "Could not update state"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1271
-msgid "Error."
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/captcha/kocaptcha.ex:36
-msgid "Invalid CAPTCHA"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1700
-#: lib/pleroma/web/oauth/oauth_controller.ex:465
-msgid "Invalid credentials"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/plugs/ensure_authenticated_plug.ex:20
-msgid "Invalid credentials."
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:154
-msgid "Invalid indices"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/admin_api/admin_api_controller.ex:411
-msgid "Invalid parameters"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:377
-msgid "Invalid password."
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:163
-msgid "Invalid request"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/captcha/kocaptcha.ex:16
-msgid "Kocaptcha service unavailable"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1696
-msgid "Missing parameters"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:496
-msgid "No such conversation"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/admin_api/admin_api_controller.ex:163
-#: lib/pleroma/web/admin_api/admin_api_controller.ex:206
-msgid "No such permission_group"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/plugs/uploaded_media.ex:69
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:311
-#: lib/pleroma/web/admin_api/admin_api_controller.ex:399
-#: lib/pleroma/web/mastodon_api/subscription_controller.ex:63
-#: lib/pleroma/web/ostatus/ostatus_controller.ex:248
-msgid "Not found"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:152
-msgid "Poll's author can't vote"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:443
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:444
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:473
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:476
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1180
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1564
-msgid "Record not found"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/admin_api/admin_api_controller.ex:417
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1570
-#: lib/pleroma/web/mastodon_api/subscription_controller.ex:69
-#: lib/pleroma/web/ostatus/ostatus_controller.ex:252
-msgid "Something went wrong"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:253
-msgid "The message visibility must be direct"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:521
-msgid "The status is over the character limit"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:27
-msgid "This resource requires authentication."
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/plugs/rate_limiter.ex:89
-msgid "Throttled"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:155
-msgid "Too many choices"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:268
-msgid "Unhandled activity type"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/plugs/user_is_admin_plug.ex:20
-msgid "User is not admin."
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:380
-msgid "Valid `account_id` required"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/admin_api/admin_api_controller.ex:185
-msgid "You can't revoke your own admin status."
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:216
-msgid "Your account is currently disabled"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:158
-#: lib/pleroma/web/oauth/oauth_controller.ex:213
-msgid "Your login is missing a confirmed e-mail address"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:221
-msgid "can't read inbox of %{nickname} as %{as_nickname}"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:297
-msgid "can't update outbox of %{nickname} as %{as_nickname}"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:335
-msgid "conversation is already muted"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:192
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:317
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1196
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1247
-msgid "error"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:789
-msgid "mascots can only be images"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:34
-msgid "not found"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:298
-msgid "Bad OAuth request."
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/captcha/captcha.ex:92
-msgid "CAPTCHA already used"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/captcha/captcha.ex:89
-msgid "CAPTCHA expired"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/plugs/uploaded_media.ex:50
-msgid "Failed"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:314
-msgid "Failed to authenticate: %{message}."
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:345
-msgid "Failed to set up user account."
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/plugs/oauth_scopes_plug.ex:37
-msgid "Insufficient permissions: %{permissions}."
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/plugs/uploaded_media.ex:89
-msgid "Internal Error"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/oauth/fallback_controller.ex:22
-#: lib/pleroma/web/oauth/fallback_controller.ex:29
-msgid "Invalid Username/Password"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/captcha/captcha.ex:107
-msgid "Invalid answer data"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:204
-msgid "Nodeinfo schema version not handled"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:145
-msgid "This action is outside the authorized scopes"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/oauth/fallback_controller.ex:14
-msgid "Unknown error, please check the details and try again."
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:93
-#: lib/pleroma/web/oauth/oauth_controller.ex:131
-msgid "Unlisted redirect_uri."
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:294
-msgid "Unsupported OAuth provider: %{provider}."
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/uploaders/uploader.ex:71
-msgid "Uploader callback timeout"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/uploader_controller.ex:11
-#: lib/pleroma/web/uploader_controller.ex:23
-msgid "bad request"
-msgstr ""
diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot
index 2fd9c42e3..0e1cf37eb 100644
--- a/priv/gettext/errors.pot
+++ b/priv/gettext/errors.pot
@@ -90,326 +90,312 @@ msgid "must be equal to %{number}"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:381
+#: lib/pleroma/web/common_api/common_api.ex:421
msgid "Account not found"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:153
+#: lib/pleroma/web/common_api/common_api.ex:249
msgid "Already voted"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:263
+#: lib/pleroma/web/oauth/oauth_controller.ex:360
msgid "Bad request"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:254
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:425
msgid "Can't delete object"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:569
+#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:196
msgid "Can't delete this post"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1731
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1737
+#: lib/pleroma/web/controller_helper.ex:95
+#: lib/pleroma/web/controller_helper.ex:101
msgid "Can't display this activity"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:195
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:227
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254
msgid "Can't find user"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1148
+#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:114
msgid "Can't get favorites"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:263
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:437
msgid "Can't like object"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:518
+#: lib/pleroma/web/common_api/utils.ex:556
msgid "Cannot post an empty status without attachments"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:461
+#: lib/pleroma/web/common_api/utils.ex:504
msgid "Comment must be up to %{max_size} characters"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/admin_api/config.ex:63
+#: lib/pleroma/config/config_db.ex:222
msgid "Config with params %{params} not found"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:78
+#: lib/pleroma/web/common_api/common_api.ex:95
msgid "Could not delete"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:110
+#: lib/pleroma/web/common_api/common_api.ex:141
msgid "Could not favorite"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:310
+#: lib/pleroma/web/common_api/common_api.ex:370
msgid "Could not pin"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:89
+#: lib/pleroma/web/common_api/common_api.ex:112
msgid "Could not repeat"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:120
+#: lib/pleroma/web/common_api/common_api.ex:188
msgid "Could not unfavorite"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:327
+#: lib/pleroma/web/common_api/common_api.ex:380
msgid "Could not unpin"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:99
+#: lib/pleroma/web/common_api/common_api.ex:126
msgid "Could not unrepeat"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:392
+#: lib/pleroma/web/common_api/common_api.ex:428
+#: lib/pleroma/web/common_api/common_api.ex:437
msgid "Could not update state"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1271
+#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:202
msgid "Error."
msgstr ""
#, elixir-format
-#: lib/pleroma/captcha/kocaptcha.ex:36
+#: lib/pleroma/web/twitter_api/twitter_api.ex:106
msgid "Invalid CAPTCHA"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1700
-#: lib/pleroma/web/oauth/oauth_controller.ex:465
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:117
+#: lib/pleroma/web/oauth/oauth_controller.ex:569
msgid "Invalid credentials"
msgstr ""
#, elixir-format
-#: lib/pleroma/plugs/ensure_authenticated_plug.ex:20
+#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38
msgid "Invalid credentials."
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:154
+#: lib/pleroma/web/common_api/common_api.ex:265
msgid "Invalid indices"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/admin_api/admin_api_controller.ex:411
+#: lib/pleroma/web/admin_api/admin_api_controller.ex:1147
msgid "Invalid parameters"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:377
+#: lib/pleroma/web/common_api/utils.ex:411
msgid "Invalid password."
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:163
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:187
msgid "Invalid request"
msgstr ""
#, elixir-format
-#: lib/pleroma/captcha/kocaptcha.ex:16
+#: lib/pleroma/web/twitter_api/twitter_api.ex:109
msgid "Kocaptcha service unavailable"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1696
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:113
msgid "Missing parameters"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:496
+#: lib/pleroma/web/common_api/utils.ex:540
msgid "No such conversation"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/admin_api/admin_api_controller.ex:163
-#: lib/pleroma/web/admin_api/admin_api_controller.ex:206
+#: lib/pleroma/web/admin_api/admin_api_controller.ex:439
+#: lib/pleroma/web/admin_api/admin_api_controller.ex:465 lib/pleroma/web/admin_api/admin_api_controller.ex:507
msgid "No such permission_group"
msgstr ""
#, elixir-format
-#: lib/pleroma/plugs/uploaded_media.ex:69
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:311
-#: lib/pleroma/web/admin_api/admin_api_controller.ex:399
-#: lib/pleroma/web/mastodon_api/subscription_controller.ex:63
-#: lib/pleroma/web/ostatus/ostatus_controller.ex:248
+#: lib/pleroma/plugs/uploaded_media.ex:74
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:485 lib/pleroma/web/admin_api/admin_api_controller.ex:1135
+#: lib/pleroma/web/feed/user_controller.ex:73 lib/pleroma/web/ostatus/ostatus_controller.ex:143
msgid "Not found"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:152
+#: lib/pleroma/web/common_api/common_api.ex:241
msgid "Poll's author can't vote"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:443
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:444
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:473
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:476
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1180
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1564
+#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
+#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49
+#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:50 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:290
+#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
msgid "Record not found"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/admin_api/admin_api_controller.ex:417
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1570
-#: lib/pleroma/web/mastodon_api/subscription_controller.ex:69
-#: lib/pleroma/web/ostatus/ostatus_controller.ex:252
+#: lib/pleroma/web/admin_api/admin_api_controller.ex:1153
+#: lib/pleroma/web/feed/user_controller.ex:79 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:32
+#: lib/pleroma/web/ostatus/ostatus_controller.ex:149
msgid "Something went wrong"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:253
+#: lib/pleroma/web/common_api/activity_draft.ex:107
msgid "The message visibility must be direct"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:521
+#: lib/pleroma/web/common_api/utils.ex:566
msgid "The status is over the character limit"
msgstr ""
#, elixir-format
-#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:27
+#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
msgid "This resource requires authentication."
msgstr ""
#, elixir-format
-#: lib/pleroma/plugs/rate_limiter.ex:89
+#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
msgid "Throttled"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:155
+#: lib/pleroma/web/common_api/common_api.ex:266
msgid "Too many choices"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:268
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:442
msgid "Unhandled activity type"
msgstr ""
#, elixir-format
-#: lib/pleroma/plugs/user_is_admin_plug.ex:20
-msgid "User is not admin."
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:380
-msgid "Valid `account_id` required"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/admin_api/admin_api_controller.ex:185
+#: lib/pleroma/web/admin_api/admin_api_controller.ex:536
msgid "You can't revoke your own admin status."
msgstr ""
#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:216
+#: lib/pleroma/web/oauth/oauth_controller.ex:218
+#: lib/pleroma/web/oauth/oauth_controller.ex:309
msgid "Your account is currently disabled"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:158
-#: lib/pleroma/web/oauth/oauth_controller.ex:213
+#: lib/pleroma/web/oauth/oauth_controller.ex:180
+#: lib/pleroma/web/oauth/oauth_controller.ex:332
msgid "Your login is missing a confirmed e-mail address"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:221
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:389
msgid "can't read inbox of %{nickname} as %{as_nickname}"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:297
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:472
msgid "can't update outbox of %{nickname} as %{as_nickname}"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:335
+#: lib/pleroma/web/common_api/common_api.ex:388
msgid "conversation is already muted"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:192
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:317
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1196
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1247
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:316
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491
msgid "error"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:789
+#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:29
msgid "mascots can only be images"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:34
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:60
msgid "not found"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:298
+#: lib/pleroma/web/oauth/oauth_controller.ex:395
msgid "Bad OAuth request."
msgstr ""
#, elixir-format
-#: lib/pleroma/captcha/captcha.ex:92
+#: lib/pleroma/web/twitter_api/twitter_api.ex:115
msgid "CAPTCHA already used"
msgstr ""
#, elixir-format
-#: lib/pleroma/captcha/captcha.ex:89
+#: lib/pleroma/web/twitter_api/twitter_api.ex:112
msgid "CAPTCHA expired"
msgstr ""
#, elixir-format
-#: lib/pleroma/plugs/uploaded_media.ex:50
+#: lib/pleroma/plugs/uploaded_media.ex:55
msgid "Failed"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:314
+#: lib/pleroma/web/oauth/oauth_controller.ex:411
msgid "Failed to authenticate: %{message}."
msgstr ""
#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:345
+#: lib/pleroma/web/oauth/oauth_controller.ex:442
msgid "Failed to set up user account."
msgstr ""
#, elixir-format
-#: lib/pleroma/plugs/oauth_scopes_plug.ex:37
+#: lib/pleroma/plugs/oauth_scopes_plug.ex:38
msgid "Insufficient permissions: %{permissions}."
msgstr ""
#, elixir-format
-#: lib/pleroma/plugs/uploaded_media.ex:89
+#: lib/pleroma/plugs/uploaded_media.ex:94
msgid "Internal Error"
msgstr ""
@@ -420,17 +406,17 @@ msgid "Invalid Username/Password"
msgstr ""
#, elixir-format
-#: lib/pleroma/captcha/captcha.ex:107
+#: lib/pleroma/web/twitter_api/twitter_api.ex:118
msgid "Invalid answer data"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:204
+#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:128
msgid "Nodeinfo schema version not handled"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:145
+#: lib/pleroma/web/oauth/oauth_controller.ex:169
msgid "This action is outside the authorized scopes"
msgstr ""
@@ -440,23 +426,139 @@ msgid "Unknown error, please check the details and try again."
msgstr ""
#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:93
-#: lib/pleroma/web/oauth/oauth_controller.ex:131
+#: lib/pleroma/web/oauth/oauth_controller.ex:116
+#: lib/pleroma/web/oauth/oauth_controller.ex:155
msgid "Unlisted redirect_uri."
msgstr ""
#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:294
+#: lib/pleroma/web/oauth/oauth_controller.ex:391
msgid "Unsupported OAuth provider: %{provider}."
msgstr ""
#, elixir-format
-#: lib/pleroma/uploaders/uploader.ex:71
+#: lib/pleroma/uploaders/uploader.ex:72
msgid "Uploader callback timeout"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/uploader_controller.ex:11
#: lib/pleroma/web/uploader_controller.ex:23
msgid "bad request"
msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/twitter_api/twitter_api.ex:103
+msgid "CAPTCHA Error"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api/common_api.ex:200
+msgid "Could not add reaction emoji"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api/common_api.ex:211
+msgid "Could not remove reaction emoji"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/twitter_api/twitter_api.ex:129
+msgid "Invalid CAPTCHA (Missing parameter: %{name})"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
+msgid "List not found"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:124
+msgid "Missing parameter: %{name}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/oauth/oauth_controller.ex:207
+#: lib/pleroma/web/oauth/oauth_controller.ex:322
+msgid "Password reset is required"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/tests/auth_test_controller.ex:9
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/admin_api_controller.ex:6
+#: lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/fallback_redirect_controller.ex:6
+#: lib/pleroma/web/feed/tag_controller.ex:6 lib/pleroma/web/feed/user_controller.ex:6
+#: lib/pleroma/web/mailer/subscription_controller.ex:2 lib/pleroma/web/masto_fe_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/app_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14 lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/report_controller.ex:8 lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7 lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6 lib/pleroma/web/media_proxy/media_proxy_controller.ex:6
+#: lib/pleroma/web/mongooseim/mongoose_im_controller.ex:6 lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6
+#: lib/pleroma/web/oauth/fallback_controller.ex:6 lib/pleroma/web/oauth/mfa_controller.ex:10
+#: lib/pleroma/web/oauth/oauth_controller.ex:6 lib/pleroma/web/ostatus/ostatus_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:2
+#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/static_fe/static_fe_controller.ex:6
+#: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6
+#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/twitter_api/twitter_api_controller.ex:6
+#: lib/pleroma/web/uploader_controller.ex:6 lib/pleroma/web/web_finger/web_finger_controller.ex:6
+msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
+msgid "Two-factor authentication enabled, you must use a access token."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:210
+msgid "Unexpected error occurred while adding file to pack."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:138
+msgid "Unexpected error occurred while creating pack."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:278
+msgid "Unexpected error occurred while removing file from pack."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:250
+msgid "Unexpected error occurred while updating file in pack."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:179
+msgid "Unexpected error occurred while updating pack metadata."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/plugs/user_is_admin_plug.ex:40
+msgid "User is not an admin or OAuth admin scope is not granted."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
+msgid "Web push subscription is disabled on this Pleroma instance"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/admin_api/admin_api_controller.ex:502
+msgid "You can't revoke your own admin/moderator status."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:105
+msgid "authorization required for timeline view"
+msgstr ""
diff --git a/priv/repo/migrations/20200415181818_update_markers.exs b/priv/repo/migrations/20200415181818_update_markers.exs
index 976363565..bb9d8e860 100644
--- a/priv/repo/migrations/20200415181818_update_markers.exs
+++ b/priv/repo/migrations/20200415181818_update_markers.exs
@@ -32,9 +32,13 @@ defp update_markers do
|> Map.put_new(:updated_at, now)
end)
- Repo.insert_all("markers", markers_attrs,
- on_conflict: {:replace, [:last_read_id]},
- conflict_target: [:user_id, :timeline]
- )
+ markers_attrs
+ |> Enum.chunk_every(1000)
+ |> Enum.each(fn markers_attrs_chunked ->
+ Repo.insert_all("markers", markers_attrs_chunked,
+ on_conflict: {:replace, [:last_read_id]},
+ conflict_target: [:user_id, :timeline]
+ )
+ end)
end
end
diff --git a/priv/repo/migrations/20200428221338_insert_skeletons_for_deleted_users.exs b/priv/repo/migrations/20200428221338_insert_skeletons_for_deleted_users.exs
index 11d9a70ba..2adc38186 100644
--- a/priv/repo/migrations/20200428221338_insert_skeletons_for_deleted_users.exs
+++ b/priv/repo/migrations/20200428221338_insert_skeletons_for_deleted_users.exs
@@ -30,7 +30,7 @@ def change do
Repo,
"select distinct unnest(nonexistent_locals.recipients) from activities, lateral (select array_agg(recipient) as recipients from unnest(activities.recipients) as recipient where recipient similar to '#{
instance_uri
- }/users/[A-Za-z0-9]*' and not(recipient in (select ap_id from users where local = true))) nonexistent_locals;",
+ }/users/[A-Za-z0-9]*' and not(recipient in (select ap_id from users))) nonexistent_locals;",
[],
timeout: :infinity
)
diff --git a/priv/static/index.html b/priv/static/index.html
index 4fac5c100..b37cbaa67 100644
--- a/priv/static/index.html
+++ b/priv/static/index.html
@@ -1 +1 @@
-
for paragraphs, GS uses
between them)\n // as well as approximate line count by counting characters and approximating ~80\n // per line.\n //\n // Using max-height + overflow: auto for status components resulted in false positives\n // very often with japanese characters, and it was very annoying.\n tallStatus () {\n const lengthScore = this.status.statusnet_html.split(/
20\n },\n longSubject () {\n return this.status.summary.length > 900\n },\n // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.\n mightHideBecauseSubject () {\n return this.status.summary && (!this.tallStatus || this.localCollapseSubjectDefault)\n },\n mightHideBecauseTall () {\n return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)\n },\n hideSubjectStatus () {\n return this.mightHideBecauseSubject && !this.expandingSubject\n },\n hideTallStatus () {\n return this.mightHideBecauseTall && !this.showingTall\n },\n showingMore () {\n return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)\n },\n nsfwClickthrough () {\n if (!this.status.nsfw) {\n return false\n }\n if (this.status.summary && this.localCollapseSubjectDefault) {\n return false\n }\n return true\n },\n attachmentSize () {\n if ((this.mergedConfig.hideAttachments && !this.inConversation) ||\n (this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||\n (this.status.attachments.length > this.maxThumbnails)) {\n return 'hide'\n } else if (this.compact) {\n return 'small'\n }\n return 'normal'\n },\n galleryTypes () {\n if (this.attachmentSize === 'hide') {\n return []\n }\n return this.mergedConfig.playVideosInModal\n ? ['image', 'video']\n : ['image']\n },\n galleryAttachments () {\n return this.status.attachments.filter(\n file => fileType.fileMatchesSomeType(this.galleryTypes, file)\n )\n },\n nonGalleryAttachments () {\n return this.status.attachments.filter(\n file => !fileType.fileMatchesSomeType(this.galleryTypes, file)\n )\n },\n hasImageAttachments () {\n return this.status.attachments.some(\n file => fileType.fileType(file.mimetype) === 'image'\n )\n },\n hasVideoAttachments () {\n return this.status.attachments.some(\n file => fileType.fileType(file.mimetype) === 'video'\n )\n },\n maxThumbnails () {\n return this.mergedConfig.maxThumbnails\n },\n postBodyHtml () {\n const html = this.status.statusnet_html\n\n if (this.mergedConfig.greentext) {\n try {\n if (html.includes('>')) {\n // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works\n return processHtml(html, (string) => {\n if (string.includes('>') &&\n string\n .replace(/<[^>]+?>/gi, '') // remove all tags\n .replace(/@\\w+/gi, '') // remove mentions (even failed ones)\n .trim()\n .startsWith('>')) {\n return `${string}`\n } else {\n return string\n }\n })\n } else {\n return html\n }\n } catch (e) {\n console.err('Failed to process status html', e)\n return html\n }\n } else {\n return html\n }\n },\n contentHtml () {\n if (!this.status.summary_html) {\n return this.postBodyHtml\n }\n return this.status.summary_html + '
' + this.postBodyHtml\n },\n ...mapGetters(['mergedConfig']),\n ...mapState({\n betterShadow: state => state.interface.browserSupport.cssFilter,\n currentUser: state => state.users.currentUser\n })\n },\n components: {\n Attachment,\n Poll,\n Gallery,\n LinkPreview\n },\n methods: {\n linkClicked (event) {\n const target = event.target.closest('.status-content a')\n if (target) {\n if (target.className.match(/mention/)) {\n const href = target.href\n const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))\n if (attn) {\n event.stopPropagation()\n event.preventDefault()\n const link = this.generateUserProfileLink(attn.id, attn.screen_name)\n this.$router.push(link)\n return\n }\n }\n if (target.rel.match(/(?:^|\\s)tag(?:$|\\s)/) || target.className.match(/hashtag/)) {\n // Extract tag name from link url\n const tag = extractTagFromUrl(target.href)\n if (tag) {\n const link = this.generateTagLink(tag)\n this.$router.push(link)\n return\n }\n }\n window.open(target.href, '_blank')\n }\n },\n toggleShowMore () {\n if (this.mightHideBecauseTall) {\n this.showingTall = !this.showingTall\n } else if (this.mightHideBecauseSubject) {\n this.expandingSubject = !this.expandingSubject\n }\n },\n generateUserProfileLink (id, name) {\n return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)\n },\n generateTagLink (tag) {\n return `/tag/${tag}`\n },\n setMedia () {\n const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments\n return () => this.$store.dispatch('setMedia', attachments)\n }\n }\n}\n\nexport default StatusContent\n","/**\n * This is a tiny purpose-built HTML parser/processor. This basically detects any type of visual newline and\n * allows it to be processed, useful for greentexting, mostly\n *\n * known issue: doesn't handle CDATA so nested CDATA might not work well\n *\n * @param {Object} input - input data\n * @param {(string) => string} processor - function that will be called on every line\n * @return {string} processed html\n */\nexport const processHtml = (html, processor) => {\n const handledTags = new Set(['p', 'br', 'div'])\n const openCloseTags = new Set(['p', 'div'])\n\n let buffer = '' // Current output buffer\n const level = [] // How deep we are in tags and which tags were there\n let textBuffer = '' // Current line content\n let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag\n\n // Extracts tag name from tag, i.e. => span\n const getTagName = (tag) => {\n const result = /(?:<\\/(\\w+)>|<(\\w+)\\s?[^/]*?\\/?>)/gi.exec(tag)\n return result && (result[1] || result[2])\n }\n\n const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer\n if (textBuffer.trim().length > 0) {\n buffer += processor(textBuffer)\n } else {\n buffer += textBuffer\n }\n textBuffer = ''\n }\n\n const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing\n flush()\n buffer += tag\n }\n\n const handleOpen = (tag) => { // handles opening tags\n flush()\n buffer += tag\n level.push(tag)\n }\n\n const handleClose = (tag) => { // handles closing tags\n flush()\n buffer += tag\n if (level[level.length - 1] === tag) {\n level.pop()\n }\n }\n\n for (let i = 0; i < html.length; i++) {\n const char = html[i]\n if (char === '<' && tagBuffer === null) {\n tagBuffer = char\n } else if (char !== '>' && tagBuffer !== null) {\n tagBuffer += char\n } else if (char === '>' && tagBuffer !== null) {\n tagBuffer += char\n const tagFull = tagBuffer\n tagBuffer = null\n const tagName = getTagName(tagFull)\n if (handledTags.has(tagName)) {\n if (tagName === 'br') {\n handleBr(tagFull)\n } else if (openCloseTags.has(tagName)) {\n if (tagFull[1] === '/') {\n handleClose(tagFull)\n } else if (tagFull[tagFull.length - 2] === '/') {\n // self-closing\n handleBr(tagFull)\n } else {\n handleOpen(tagFull)\n }\n }\n } else {\n textBuffer += tagFull\n }\n } else if (char === '\\n') {\n handleBr(char)\n } else {\n textBuffer += char\n }\n }\n if (tagBuffer) {\n textBuffer += tagBuffer\n }\n\n flush()\n\n return buffer\n}\n","export const mentionMatchesUrl = (attention, url) => {\n if (url === attention.statusnet_profile_url) {\n return true\n }\n const [namepart, instancepart] = attention.screen_name.split('@')\n const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g')\n\n return !!url.match(matchstring)\n}\n\n/**\n * Extract tag name from pleroma or mastodon url.\n * i.e https://bikeshed.party/tag/photo or https://quey.org/tags/sky\n * @param {string} url\n */\nexport const extractTagFromUrl = (url) => {\n const regex = /tag[s]*\\/(\\w+)$/g\n const result = regex.exec(url)\n if (!result) {\n return false\n }\n return result[1]\n}\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./status_content.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./status_content.js\"\nimport __vue_script__ from \"!!babel-loader!./status_content.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-43c5cfd4\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./status_content.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"status-body\"},[_vm._t(\"header\"),_vm._v(\" \"),(_vm.longSubject)?_c('div',{staticClass:\"status-content-wrapper\",class:{ 'tall-status': !_vm.showingLongSubject }},[(!_vm.showingLongSubject)?_c('a',{staticClass:\"tall-status-hider\",class:{ 'tall-status-hider_focused': _vm.focused },attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();_vm.showingLongSubject=true}}},[_vm._v(\"\\n \"+_vm._s(_vm.$t(\"general.show_more\"))+\"\\n \"),(_vm.hasImageAttachments)?_c('span',{staticClass:\"icon-picture\"}):_vm._e(),_vm._v(\" \"),(_vm.hasVideoAttachments)?_c('span',{staticClass:\"icon-video\"}):_vm._e(),_vm._v(\" \"),(_vm.status.card)?_c('span',{staticClass:\"icon-link\"}):_vm._e()]):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"status-content media-body\",domProps:{\"innerHTML\":_vm._s(_vm.contentHtml)},on:{\"click\":function($event){$event.preventDefault();return _vm.linkClicked($event)}}}),_vm._v(\" \"),(_vm.showingLongSubject)?_c('a',{staticClass:\"status-unhider\",attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();_vm.showingLongSubject=false}}},[_vm._v(_vm._s(_vm.$t(\"general.show_less\")))]):_vm._e()]):_c('div',{staticClass:\"status-content-wrapper\",class:{'tall-status': _vm.hideTallStatus}},[(_vm.hideTallStatus)?_c('a',{staticClass:\"tall-status-hider\",class:{ 'tall-status-hider_focused': _vm.focused },attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleShowMore($event)}}},[_vm._v(_vm._s(_vm.$t(\"general.show_more\")))]):_vm._e(),_vm._v(\" \"),(!_vm.hideSubjectStatus)?_c('div',{staticClass:\"status-content media-body\",domProps:{\"innerHTML\":_vm._s(_vm.contentHtml)},on:{\"click\":function($event){$event.preventDefault();return _vm.linkClicked($event)}}}):_c('div',{staticClass:\"status-content media-body\",domProps:{\"innerHTML\":_vm._s(_vm.status.summary_html)},on:{\"click\":function($event){$event.preventDefault();return _vm.linkClicked($event)}}}),_vm._v(\" \"),(_vm.hideSubjectStatus)?_c('a',{staticClass:\"cw-status-hider\",attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleShowMore($event)}}},[_vm._v(_vm._s(_vm.$t(\"general.show_more\")))]):_vm._e(),_vm._v(\" \"),(_vm.showingMore)?_c('a',{staticClass:\"status-unhider\",attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleShowMore($event)}}},[_vm._v(_vm._s(_vm.$t(\"general.show_less\")))]):_vm._e()]),_vm._v(\" \"),(_vm.status.poll && _vm.status.poll.options)?_c('div',[_c('poll',{attrs:{\"base-poll\":_vm.status.poll}})],1):_vm._e(),_vm._v(\" \"),(_vm.status.attachments.length !== 0 && (!_vm.hideSubjectStatus || _vm.showingLongSubject))?_c('div',{staticClass:\"attachments media-body\"},[_vm._l((_vm.nonGalleryAttachments),function(attachment){return _c('attachment',{key:attachment.id,staticClass:\"non-gallery\",attrs:{\"size\":_vm.attachmentSize,\"nsfw\":_vm.nsfwClickthrough,\"attachment\":attachment,\"allow-play\":true,\"set-media\":_vm.setMedia()}})}),_vm._v(\" \"),(_vm.galleryAttachments.length > 0)?_c('gallery',{attrs:{\"nsfw\":_vm.nsfwClickthrough,\"attachments\":_vm.galleryAttachments,\"set-media\":_vm.setMedia()}}):_vm._e()],2):_vm._e(),_vm._v(\" \"),(_vm.status.card && !_vm.hideSubjectStatus && !_vm.noHeading)?_c('div',{staticClass:\"link-preview media-body\"},[_c('link-preview',{attrs:{\"card\":_vm.status.card,\"size\":_vm.attachmentSize,\"nsfw\":_vm.nsfwClickthrough}})],1):_vm._e(),_vm._v(\" \"),_vm._t(\"footer\")],2)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import { find } from 'lodash'\n\nconst StatusPopover = {\n name: 'StatusPopover',\n props: [\n 'statusId'\n ],\n data () {\n return {\n error: false\n }\n },\n computed: {\n status () {\n return find(this.$store.state.statuses.allStatuses, { id: this.statusId })\n }\n },\n components: {\n Status: () => import('../status/status.vue'),\n Popover: () => import('../popover/popover.vue')\n },\n methods: {\n enter () {\n if (!this.status) {\n this.$store.dispatch('fetchStatus', this.statusId)\n .then(data => (this.error = false))\n .catch(e => (this.error = true))\n }\n }\n }\n}\n\nexport default StatusPopover\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./status_popover.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./status_popover.js\"\nimport __vue_script__ from \"!!babel-loader!./status_popover.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-3b873076\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./status_popover.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('Popover',{attrs:{\"trigger\":\"hover\",\"popover-class\":\"status-popover\",\"bound-to\":{ x: 'container' }},on:{\"show\":_vm.enter}},[_c('template',{slot:\"trigger\"},[_vm._t(\"default\")],2),_vm._v(\" \"),_c('div',{attrs:{\"slot\":\"content\"},slot:\"content\"},[(_vm.status)?_c('Status',{attrs:{\"is-preview\":true,\"statusoid\":_vm.status,\"compact\":true}}):(_vm.error)?_c('div',{staticClass:\"status-preview-no-content faint\"},[_vm._v(\"\\n \"+_vm._s(_vm.$t('status.status_unavailable'))+\"\\n \")]):_c('div',{staticClass:\"status-preview-no-content\"},[_c('i',{staticClass:\"icon-spin4 animate-spin\"})])],1)],2)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import UserAvatar from '../user_avatar/user_avatar.vue'\nimport Popover from '../popover/popover.vue'\n\nconst EMOJI_REACTION_COUNT_CUTOFF = 12\n\nconst EmojiReactions = {\n name: 'EmojiReactions',\n components: {\n UserAvatar,\n Popover\n },\n props: ['status'],\n data: () => ({\n showAll: false\n }),\n computed: {\n tooManyReactions () {\n return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF\n },\n emojiReactions () {\n return this.showAll\n ? this.status.emoji_reactions\n : this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)\n },\n showMoreString () {\n return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`\n },\n accountsForEmoji () {\n return this.status.emoji_reactions.reduce((acc, reaction) => {\n acc[reaction.name] = reaction.accounts || []\n return acc\n }, {})\n },\n loggedIn () {\n return !!this.$store.state.users.currentUser\n }\n },\n methods: {\n toggleShowAll () {\n this.showAll = !this.showAll\n },\n reactedWith (emoji) {\n return this.status.emoji_reactions.find(r => r.name === emoji).me\n },\n fetchEmojiReactionsByIfMissing () {\n const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)\n if (hasNoAccounts) {\n this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)\n }\n },\n reactWith (emoji) {\n this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })\n },\n unreact (emoji) {\n this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })\n },\n emojiOnClick (emoji, event) {\n if (!this.loggedIn) return\n\n if (this.reactedWith(emoji)) {\n this.unreact(emoji)\n } else {\n this.reactWith(emoji)\n }\n }\n }\n}\n\nexport default EmojiReactions\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./emoji_reactions.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./emoji_reactions.js\"\nimport __vue_script__ from \"!!babel-loader!./emoji_reactions.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-09ec7fb6\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./emoji_reactions.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"emoji-reactions\"},[_vm._l((_vm.emojiReactions),function(reaction){return _c('Popover',{key:reaction.name,attrs:{\"trigger\":\"hover\",\"placement\":\"top\",\"offset\":{ y: 5 }}},[_c('div',{staticClass:\"reacted-users\",attrs:{\"slot\":\"content\"},slot:\"content\"},[(_vm.accountsForEmoji[reaction.name].length)?_c('div',_vm._l((_vm.accountsForEmoji[reaction.name]),function(account){return _c('div',{key:account.id,staticClass:\"reacted-user\"},[_c('UserAvatar',{staticClass:\"avatar-small\",attrs:{\"user\":account,\"compact\":true}}),_vm._v(\" \"),_c('div',{staticClass:\"reacted-user-names\"},[_c('span',{staticClass:\"reacted-user-name\",domProps:{\"innerHTML\":_vm._s(account.name_html)}}),_vm._v(\" \"),_c('span',{staticClass:\"reacted-user-screen-name\"},[_vm._v(_vm._s(account.screen_name))])])],1)}),0):_c('div',[_c('i',{staticClass:\"icon-spin4 animate-spin\"})])]),_vm._v(\" \"),_c('button',{staticClass:\"emoji-reaction btn btn-default\",class:{ 'picked-reaction': _vm.reactedWith(reaction.name), 'not-clickable': !_vm.loggedIn },attrs:{\"slot\":\"trigger\"},on:{\"click\":function($event){return _vm.emojiOnClick(reaction.name, $event)},\"mouseenter\":function($event){return _vm.fetchEmojiReactionsByIfMissing()}},slot:\"trigger\"},[_c('span',{staticClass:\"reaction-emoji\"},[_vm._v(_vm._s(reaction.name))]),_vm._v(\" \"),_c('span',[_vm._v(_vm._s(reaction.count))])])])}),_vm._v(\" \"),(_vm.tooManyReactions)?_c('a',{staticClass:\"emoji-reaction-expand faint\",attrs:{\"href\":\"javascript:void(0)\"},on:{\"click\":_vm.toggleShowAll}},[_vm._v(\"\\n \"+_vm._s(_vm.showAll ? _vm.$t('general.show_less') : _vm.showMoreString)+\"\\n \")]):_vm._e()],2)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import FavoriteButton from '../favorite_button/favorite_button.vue'\nimport ReactButton from '../react_button/react_button.vue'\nimport RetweetButton from '../retweet_button/retweet_button.vue'\nimport ExtraButtons from '../extra_buttons/extra_buttons.vue'\nimport PostStatusForm from '../post_status_form/post_status_form.vue'\nimport UserCard from '../user_card/user_card.vue'\nimport UserAvatar from '../user_avatar/user_avatar.vue'\nimport AvatarList from '../avatar_list/avatar_list.vue'\nimport Timeago from '../timeago/timeago.vue'\nimport StatusContent from '../status_content/status_content.vue'\nimport StatusPopover from '../status_popover/status_popover.vue'\nimport EmojiReactions from '../emoji_reactions/emoji_reactions.vue'\nimport generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'\nimport { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'\nimport { filter, unescape, uniqBy } from 'lodash'\nimport { mapGetters, mapState } from 'vuex'\n\nconst Status = {\n name: 'Status',\n props: [\n 'statusoid',\n 'expandable',\n 'inConversation',\n 'focused',\n 'highlight',\n 'compact',\n 'replies',\n 'isPreview',\n 'noHeading',\n 'inlineExpanded',\n 'showPinned',\n 'inProfile',\n 'profileUserId'\n ],\n data () {\n return {\n replying: false,\n unmuted: false,\n userExpanded: false,\n error: null\n }\n },\n computed: {\n muteWords () {\n return this.mergedConfig.muteWords\n },\n repeaterClass () {\n const user = this.statusoid.user\n return highlightClass(user)\n },\n userClass () {\n const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user\n return highlightClass(user)\n },\n deleted () {\n return this.statusoid.deleted\n },\n repeaterStyle () {\n const user = this.statusoid.user\n const highlight = this.mergedConfig.highlight\n return highlightStyle(highlight[user.screen_name])\n },\n userStyle () {\n if (this.noHeading) return\n const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user\n const highlight = this.mergedConfig.highlight\n return highlightStyle(highlight[user.screen_name])\n },\n userProfileLink () {\n return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)\n },\n replyProfileLink () {\n if (this.isReply) {\n return this.generateUserProfileLink(this.status.in_reply_to_user_id, this.replyToName)\n }\n },\n retweet () { return !!this.statusoid.retweeted_status },\n retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name },\n retweeterHtml () { return this.statusoid.user.name_html },\n retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },\n status () {\n if (this.retweet) {\n return this.statusoid.retweeted_status\n } else {\n return this.statusoid\n }\n },\n statusFromGlobalRepository () {\n // NOTE: Consider to replace status with statusFromGlobalRepository\n return this.$store.state.statuses.allStatusesObject[this.status.id]\n },\n loggedIn () {\n return !!this.currentUser\n },\n muteWordHits () {\n const statusText = this.status.text.toLowerCase()\n const statusSummary = this.status.summary.toLowerCase()\n const hits = filter(this.muteWords, (muteWord) => {\n return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())\n })\n\n return hits\n },\n muted () {\n const relationship = this.$store.getters.relationship(this.status.user.id)\n return !this.unmuted && (\n (!(this.inProfile && this.status.user.id === this.profileUserId) && relationship.muting) ||\n (!this.inConversation && this.status.thread_muted) ||\n this.muteWordHits.length > 0)\n },\n hideFilteredStatuses () {\n return this.mergedConfig.hideFilteredStatuses\n },\n hideStatus () {\n return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses)\n },\n isFocused () {\n // retweet or root of an expanded conversation\n if (this.focused) {\n return true\n } else if (!this.inConversation) {\n return false\n }\n // use conversation highlight only when in conversation\n return this.status.id === this.highlight\n },\n isReply () {\n return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)\n },\n replyToName () {\n if (this.status.in_reply_to_screen_name) {\n return this.status.in_reply_to_screen_name\n } else {\n const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)\n return user && user.screen_name\n }\n },\n hideReply () {\n if (this.mergedConfig.replyVisibility === 'all') {\n return false\n }\n if (this.inConversation || !this.isReply) {\n return false\n }\n if (this.status.user.id === this.currentUser.id) {\n return false\n }\n if (this.status.type === 'retweet') {\n return false\n }\n const checkFollowing = this.mergedConfig.replyVisibility === 'following'\n for (var i = 0; i < this.status.attentions.length; ++i) {\n if (this.status.user.id === this.status.attentions[i].id) {\n continue\n }\n // There's zero guarantee of this working. If we happen to have that user and their\n // relationship in store then it will work, but there's kinda little chance of having\n // them for people you're not following.\n const relationship = this.$store.state.users.relationships[this.status.attentions[i].id]\n if (checkFollowing && relationship && relationship.following) {\n return false\n }\n if (this.status.attentions[i].id === this.currentUser.id) {\n return false\n }\n }\n return this.status.attentions.length > 0\n },\n replySubject () {\n if (!this.status.summary) return ''\n const decodedSummary = unescape(this.status.summary)\n const behavior = this.mergedConfig.subjectLineBehavior\n const startsWithRe = decodedSummary.match(/^re[: ]/i)\n if ((behavior !== 'noop' && startsWithRe) || behavior === 'masto') {\n return decodedSummary\n } else if (behavior === 'email') {\n return 're: '.concat(decodedSummary)\n } else if (behavior === 'noop') {\n return ''\n }\n },\n combinedFavsAndRepeatsUsers () {\n // Use the status from the global status repository since favs and repeats are saved in it\n const combinedUsers = [].concat(\n this.statusFromGlobalRepository.favoritedBy,\n this.statusFromGlobalRepository.rebloggedBy\n )\n return uniqBy(combinedUsers, 'id')\n },\n tags () {\n return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')\n },\n hidePostStats () {\n return this.mergedConfig.hidePostStats\n },\n ...mapGetters(['mergedConfig']),\n ...mapState({\n betterShadow: state => state.interface.browserSupport.cssFilter,\n currentUser: state => state.users.currentUser\n })\n },\n components: {\n FavoriteButton,\n ReactButton,\n RetweetButton,\n ExtraButtons,\n PostStatusForm,\n UserCard,\n UserAvatar,\n AvatarList,\n Timeago,\n StatusPopover,\n EmojiReactions,\n StatusContent\n },\n methods: {\n visibilityIcon (visibility) {\n switch (visibility) {\n case 'private':\n return 'icon-lock'\n case 'unlisted':\n return 'icon-lock-open-alt'\n case 'direct':\n return 'icon-mail-alt'\n default:\n return 'icon-globe'\n }\n },\n showError (error) {\n this.error = error\n },\n clearError () {\n this.error = undefined\n },\n toggleReplying () {\n this.replying = !this.replying\n },\n gotoOriginal (id) {\n if (this.inConversation) {\n this.$emit('goto', id)\n }\n },\n toggleExpanded () {\n this.$emit('toggleExpanded')\n },\n toggleMute () {\n this.unmuted = !this.unmuted\n },\n toggleUserExpanded () {\n this.userExpanded = !this.userExpanded\n },\n generateUserProfileLink (id, name) {\n return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)\n }\n },\n watch: {\n 'highlight': function (id) {\n if (this.status.id === id) {\n let rect = this.$el.getBoundingClientRect()\n if (rect.top < 100) {\n // Post is above screen, match its top to screen top\n window.scrollBy(0, rect.top - 100)\n } else if (rect.height >= (window.innerHeight - 50)) {\n // Post we want to see is taller than screen so match its top to screen top\n window.scrollBy(0, rect.top - 100)\n } else if (rect.bottom > window.innerHeight - 50) {\n // Post is below screen, match its bottom to screen bottom\n window.scrollBy(0, rect.bottom - window.innerHeight + 50)\n }\n }\n },\n 'status.repeat_num': function (num) {\n // refetch repeats when repeat_num is changed in any way\n if (this.isFocused && this.statusFromGlobalRepository.rebloggedBy && this.statusFromGlobalRepository.rebloggedBy.length !== num) {\n this.$store.dispatch('fetchRepeats', this.status.id)\n }\n },\n 'status.fave_num': function (num) {\n // refetch favs when fave_num is changed in any way\n if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) {\n this.$store.dispatch('fetchFavs', this.status.id)\n }\n }\n },\n filters: {\n capitalize: function (str) {\n return str.charAt(0).toUpperCase() + str.slice(1)\n }\n }\n}\n\nexport default Status\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./status.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./status.js\"\nimport __vue_script__ from \"!!babel-loader!./status.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-2d68efa0\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./status.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (!_vm.hideStatus)?_c('div',{staticClass:\"status-el\",class:[{ 'status-el_focused': _vm.isFocused }, { 'status-conversation': _vm.inlineExpanded }]},[(_vm.error)?_c('div',{staticClass:\"alert error\"},[_vm._v(\"\\n \"+_vm._s(_vm.error)+\"\\n \"),_c('i',{staticClass:\"button-icon icon-cancel\",on:{\"click\":_vm.clearError}})]):_vm._e(),_vm._v(\" \"),(_vm.muted && !_vm.isPreview)?[_c('div',{staticClass:\"media status container muted\"},[_c('small',[_c('router-link',{attrs:{\"to\":_vm.userProfileLink}},[_vm._v(\"\\n \"+_vm._s(_vm.status.user.screen_name)+\"\\n \")])],1),_vm._v(\" \"),_c('small',{staticClass:\"muteWords\"},[_vm._v(_vm._s(_vm.muteWordHits.join(', ')))]),_vm._v(\" \"),_c('a',{staticClass:\"unmute\",attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleMute($event)}}},[_c('i',{staticClass:\"button-icon icon-eye-off\"})])])]:[(_vm.showPinned)?_c('div',{staticClass:\"status-pin\"},[_c('i',{staticClass:\"fa icon-pin faint\"}),_vm._v(\" \"),_c('span',{staticClass:\"faint\"},[_vm._v(_vm._s(_vm.$t('status.pinned')))])]):_vm._e(),_vm._v(\" \"),(_vm.retweet && !_vm.noHeading && !_vm.inConversation)?_c('div',{staticClass:\"media container retweet-info\",class:[_vm.repeaterClass, { highlighted: _vm.repeaterStyle }],style:([_vm.repeaterStyle])},[(_vm.retweet)?_c('UserAvatar',{staticClass:\"media-left\",attrs:{\"better-shadow\":_vm.betterShadow,\"user\":_vm.statusoid.user}}):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"media-body faint\"},[_c('span',{staticClass:\"user-name\"},[(_vm.retweeterHtml)?_c('router-link',{attrs:{\"to\":_vm.retweeterProfileLink},domProps:{\"innerHTML\":_vm._s(_vm.retweeterHtml)}}):_c('router-link',{attrs:{\"to\":_vm.retweeterProfileLink}},[_vm._v(_vm._s(_vm.retweeter))])],1),_vm._v(\" \"),_c('i',{staticClass:\"fa icon-retweet retweeted\",attrs:{\"title\":_vm.$t('tool_tip.repeat')}}),_vm._v(\"\\n \"+_vm._s(_vm.$t('timeline.repeated'))+\"\\n \")])],1):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"media status\",class:[_vm.userClass, { highlighted: _vm.userStyle, 'is-retweet': _vm.retweet && !_vm.inConversation }],style:([ _vm.userStyle ]),attrs:{\"data-tags\":_vm.tags}},[(!_vm.noHeading)?_c('div',{staticClass:\"media-left\"},[_c('router-link',{attrs:{\"to\":_vm.userProfileLink},nativeOn:{\"!click\":function($event){$event.stopPropagation();$event.preventDefault();return _vm.toggleUserExpanded($event)}}},[_c('UserAvatar',{attrs:{\"compact\":_vm.compact,\"better-shadow\":_vm.betterShadow,\"user\":_vm.status.user}})],1)],1):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"status-body\"},[(_vm.userExpanded)?_c('UserCard',{staticClass:\"status-usercard\",attrs:{\"user-id\":_vm.status.user.id,\"rounded\":true,\"bordered\":true}}):_vm._e(),_vm._v(\" \"),(!_vm.noHeading)?_c('div',{staticClass:\"media-heading\"},[_c('div',{staticClass:\"heading-name-row\"},[_c('div',{staticClass:\"name-and-account-name\"},[(_vm.status.user.name_html)?_c('h4',{staticClass:\"user-name\",domProps:{\"innerHTML\":_vm._s(_vm.status.user.name_html)}}):_c('h4',{staticClass:\"user-name\"},[_vm._v(\"\\n \"+_vm._s(_vm.status.user.name)+\"\\n \")]),_vm._v(\" \"),_c('router-link',{staticClass:\"account-name\",attrs:{\"to\":_vm.userProfileLink}},[_vm._v(\"\\n \"+_vm._s(_vm.status.user.screen_name)+\"\\n \")])],1),_vm._v(\" \"),_c('span',{staticClass:\"heading-right\"},[_c('router-link',{staticClass:\"timeago faint-link\",attrs:{\"to\":{ name: 'conversation', params: { id: _vm.status.id } }}},[_c('Timeago',{attrs:{\"time\":_vm.status.created_at,\"auto-update\":60}})],1),_vm._v(\" \"),(_vm.status.visibility)?_c('div',{staticClass:\"button-icon visibility-icon\"},[_c('i',{class:_vm.visibilityIcon(_vm.status.visibility),attrs:{\"title\":_vm._f(\"capitalize\")(_vm.status.visibility)}})]):_vm._e(),_vm._v(\" \"),(!_vm.status.is_local && !_vm.isPreview)?_c('a',{staticClass:\"source_url\",attrs:{\"href\":_vm.status.external_url,\"target\":\"_blank\",\"title\":\"Source\"}},[_c('i',{staticClass:\"button-icon icon-link-ext-alt\"})]):_vm._e(),_vm._v(\" \"),(_vm.expandable && !_vm.isPreview)?[_c('a',{attrs:{\"href\":\"#\",\"title\":\"Expand\"},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleExpanded($event)}}},[_c('i',{staticClass:\"button-icon icon-plus-squared\"})])]:_vm._e(),_vm._v(\" \"),(_vm.unmuted)?_c('a',{attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleMute($event)}}},[_c('i',{staticClass:\"button-icon icon-eye-off\"})]):_vm._e()],2)]),_vm._v(\" \"),_c('div',{staticClass:\"heading-reply-row\"},[(_vm.isReply)?_c('div',{staticClass:\"reply-to-and-accountname\"},[(!_vm.isPreview)?_c('StatusPopover',{staticClass:\"reply-to-popover\",staticStyle:{\"min-width\":\"0\"},attrs:{\"status-id\":_vm.status.in_reply_to_status_id}},[_c('a',{staticClass:\"reply-to\",attrs:{\"href\":\"#\",\"aria-label\":_vm.$t('tool_tip.reply')},on:{\"click\":function($event){$event.preventDefault();return _vm.gotoOriginal(_vm.status.in_reply_to_status_id)}}},[_c('i',{staticClass:\"button-icon icon-reply\"}),_vm._v(\" \"),_c('span',{staticClass:\"faint-link reply-to-text\"},[_vm._v(_vm._s(_vm.$t('status.reply_to')))])])]):_c('span',{staticClass:\"reply-to\"},[_c('span',{staticClass:\"reply-to-text\"},[_vm._v(_vm._s(_vm.$t('status.reply_to')))])]),_vm._v(\" \"),_c('router-link',{attrs:{\"to\":_vm.replyProfileLink}},[_vm._v(\"\\n \"+_vm._s(_vm.replyToName)+\"\\n \")]),_vm._v(\" \"),(_vm.replies && _vm.replies.length)?_c('span',{staticClass:\"faint replies-separator\"},[_vm._v(\"\\n -\\n \")]):_vm._e()],1):_vm._e(),_vm._v(\" \"),(_vm.inConversation && !_vm.isPreview && _vm.replies && _vm.replies.length)?_c('div',{staticClass:\"replies\"},[_c('span',{staticClass:\"faint\"},[_vm._v(_vm._s(_vm.$t('status.replies_list')))]),_vm._v(\" \"),_vm._l((_vm.replies),function(reply){return _c('StatusPopover',{key:reply.id,attrs:{\"status-id\":reply.id}},[_c('a',{staticClass:\"reply-link\",attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.gotoOriginal(reply.id)}}},[_vm._v(_vm._s(reply.name))])])})],2):_vm._e()])]):_vm._e(),_vm._v(\" \"),_c('StatusContent',{attrs:{\"status\":_vm.status,\"no-heading\":_vm.noHeading,\"highlight\":_vm.highlight,\"focused\":_vm.isFocused}}),_vm._v(\" \"),_c('transition',{attrs:{\"name\":\"fade\"}},[(!_vm.hidePostStats && _vm.isFocused && _vm.combinedFavsAndRepeatsUsers.length > 0)?_c('div',{staticClass:\"favs-repeated-users\"},[_c('div',{staticClass:\"stats\"},[(_vm.statusFromGlobalRepository.rebloggedBy && _vm.statusFromGlobalRepository.rebloggedBy.length > 0)?_c('div',{staticClass:\"stat-count\"},[_c('a',{staticClass:\"stat-title\"},[_vm._v(_vm._s(_vm.$t('status.repeats')))]),_vm._v(\" \"),_c('div',{staticClass:\"stat-number\"},[_vm._v(\"\\n \"+_vm._s(_vm.statusFromGlobalRepository.rebloggedBy.length)+\"\\n \")])]):_vm._e(),_vm._v(\" \"),(_vm.statusFromGlobalRepository.favoritedBy && _vm.statusFromGlobalRepository.favoritedBy.length > 0)?_c('div',{staticClass:\"stat-count\"},[_c('a',{staticClass:\"stat-title\"},[_vm._v(_vm._s(_vm.$t('status.favorites')))]),_vm._v(\" \"),_c('div',{staticClass:\"stat-number\"},[_vm._v(\"\\n \"+_vm._s(_vm.statusFromGlobalRepository.favoritedBy.length)+\"\\n \")])]):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"avatar-row\"},[_c('AvatarList',{attrs:{\"users\":_vm.combinedFavsAndRepeatsUsers}})],1)])]):_vm._e()]),_vm._v(\" \"),((_vm.mergedConfig.emojiReactionsOnTimeline || _vm.isFocused) && (!_vm.noHeading && !_vm.isPreview))?_c('EmojiReactions',{attrs:{\"status\":_vm.status}}):_vm._e(),_vm._v(\" \"),(!_vm.noHeading && !_vm.isPreview)?_c('div',{staticClass:\"status-actions media-body\"},[_c('div',[(_vm.loggedIn)?_c('i',{staticClass:\"button-icon icon-reply\",class:{'button-icon-active': _vm.replying},attrs:{\"title\":_vm.$t('tool_tip.reply')},on:{\"click\":function($event){$event.preventDefault();return _vm.toggleReplying($event)}}}):_c('i',{staticClass:\"button-icon button-icon-disabled icon-reply\",attrs:{\"title\":_vm.$t('tool_tip.reply')}}),_vm._v(\" \"),(_vm.status.replies_count > 0)?_c('span',[_vm._v(_vm._s(_vm.status.replies_count))]):_vm._e()]),_vm._v(\" \"),_c('retweet-button',{attrs:{\"visibility\":_vm.status.visibility,\"logged-in\":_vm.loggedIn,\"status\":_vm.status}}),_vm._v(\" \"),_c('favorite-button',{attrs:{\"logged-in\":_vm.loggedIn,\"status\":_vm.status}}),_vm._v(\" \"),(_vm.loggedIn)?_c('ReactButton',{attrs:{\"status\":_vm.status}}):_vm._e(),_vm._v(\" \"),_c('extra-buttons',{attrs:{\"status\":_vm.status},on:{\"onError\":_vm.showError,\"onSuccess\":_vm.clearError}})],1):_vm._e()],1)]),_vm._v(\" \"),(_vm.replying)?_c('div',{staticClass:\"container\"},[_c('PostStatusForm',{staticClass:\"reply-body\",attrs:{\"reply-to\":_vm.status.id,\"attentions\":_vm.status.attentions,\"replied-user\":_vm.status.user,\"copy-message-scope\":_vm.status.visibility,\"subject\":_vm.replySubject},on:{\"posted\":_vm.toggleReplying}})],1):_vm._e()]],2):_vm._e()}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","\nconst Popover = {\n name: 'Popover',\n props: {\n // Action to trigger popover: either 'hover' or 'click'\n trigger: String,\n // Either 'top' or 'bottom'\n placement: String,\n // Takes object with properties 'x' and 'y', values of these can be\n // 'container' for using offsetParent as boundaries for either axis\n // or 'viewport'\n boundTo: Object,\n // Takes a top/bottom/left/right object, how much space to leave\n // between boundary and popover element\n margin: Object,\n // Takes a x/y object and tells how many pixels to offset from\n // anchor point on either axis\n offset: Object,\n // Additional styles you may want for the popover container\n popoverClass: String\n },\n data () {\n return {\n hidden: true,\n styles: { opacity: 0 },\n oldSize: { width: 0, height: 0 }\n }\n },\n methods: {\n updateStyles () {\n if (this.hidden) {\n this.styles = {\n opacity: 0\n }\n return\n }\n\n // Popover will be anchored around this element, trigger ref is the container, so\n // its children are what are inside the slot. Expect only one slot=\"trigger\".\n const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el\n const screenBox = anchorEl.getBoundingClientRect()\n // Screen position of the origin point for popover\n const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }\n const content = this.$refs.content\n // Minor optimization, don't call a slow reflow call if we don't have to\n const parentBounds = this.boundTo &&\n (this.boundTo.x === 'container' || this.boundTo.y === 'container') &&\n this.$el.offsetParent.getBoundingClientRect()\n const margin = this.margin || {}\n\n // What are the screen bounds for the popover? Viewport vs container\n // when using viewport, using default margin values to dodge the navbar\n const xBounds = this.boundTo && this.boundTo.x === 'container' ? {\n min: parentBounds.left + (margin.left || 0),\n max: parentBounds.right - (margin.right || 0)\n } : {\n min: 0 + (margin.left || 10),\n max: window.innerWidth - (margin.right || 10)\n }\n\n const yBounds = this.boundTo && this.boundTo.y === 'container' ? {\n min: parentBounds.top + (margin.top || 0),\n max: parentBounds.bottom - (margin.bottom || 0)\n } : {\n min: 0 + (margin.top || 50),\n max: window.innerHeight - (margin.bottom || 5)\n }\n\n let horizOffset = 0\n\n // If overflowing from left, move it so that it doesn't\n if ((origin.x - content.offsetWidth * 0.5) < xBounds.min) {\n horizOffset += -(origin.x - content.offsetWidth * 0.5) + xBounds.min\n }\n\n // If overflowing from right, move it so that it doesn't\n if ((origin.x + horizOffset + content.offsetWidth * 0.5) > xBounds.max) {\n horizOffset -= (origin.x + horizOffset + content.offsetWidth * 0.5) - xBounds.max\n }\n\n // Default to whatever user wished with placement prop\n let usingTop = this.placement !== 'bottom'\n\n // Handle special cases, first force to displaying on top if there's not space on bottom,\n // regardless of what placement value was. Then check if there's not space on top, and\n // force to bottom, again regardless of what placement value was.\n if (origin.y + content.offsetHeight > yBounds.max) usingTop = true\n if (origin.y - content.offsetHeight < yBounds.min) usingTop = false\n\n const yOffset = (this.offset && this.offset.y) || 0\n const translateY = usingTop\n ? -anchorEl.offsetHeight - yOffset - content.offsetHeight\n : yOffset\n\n const xOffset = (this.offset && this.offset.x) || 0\n const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset\n\n // Note, separate translateX and translateY avoids blurry text on chromium,\n // single translate or translate3d resulted in blurry text.\n this.styles = {\n opacity: 1,\n transform: `translateX(${Math.floor(translateX)}px) translateY(${Math.floor(translateY)}px)`\n }\n },\n showPopover () {\n if (this.hidden) this.$emit('show')\n this.hidden = false\n this.$nextTick(this.updateStyles)\n },\n hidePopover () {\n if (!this.hidden) this.$emit('close')\n this.hidden = true\n this.styles = { opacity: 0 }\n },\n onMouseenter (e) {\n if (this.trigger === 'hover') this.showPopover()\n },\n onMouseleave (e) {\n if (this.trigger === 'hover') this.hidePopover()\n },\n onClick (e) {\n if (this.trigger === 'click') {\n if (this.hidden) {\n this.showPopover()\n } else {\n this.hidePopover()\n }\n }\n },\n onClickOutside (e) {\n if (this.hidden) return\n if (this.$el.contains(e.target)) return\n this.hidePopover()\n }\n },\n updated () {\n // Monitor changes to content size, update styles only when content sizes have changed,\n // that should be the only time we need to move the popover box if we don't care about scroll\n // or resize\n const content = this.$refs.content\n if (!content) return\n if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {\n this.updateStyles()\n this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }\n }\n },\n created () {\n document.addEventListener('click', this.onClickOutside)\n },\n destroyed () {\n document.removeEventListener('click', this.onClickOutside)\n this.hidePopover()\n }\n}\n\nexport default Popover\n","function injectStyle (context) {\n require(\"!!vue-style-loader!css-loader?minimize!../../../node_modules/vue-loader/lib/style-compiler/index?{\\\"optionsId\\\":\\\"0\\\",\\\"vue\\\":true,\\\"scoped\\\":false,\\\"sourceMap\\\":false}!sass-loader!../../../node_modules/vue-loader/lib/selector?type=styles&index=0!./popover.vue\")\n}\n/* script */\nexport * from \"!!babel-loader!./popover.js\"\nimport __vue_script__ from \"!!babel-loader!./popover.js\"/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-10f1984d\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./popover.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = injectStyle\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{on:{\"mouseenter\":_vm.onMouseenter,\"mouseleave\":_vm.onMouseleave}},[_c('div',{ref:\"trigger\",on:{\"click\":_vm.onClick}},[_vm._t(\"trigger\")],2),_vm._v(\" \"),(!_vm.hidden)?_c('div',{ref:\"content\",staticClass:\"popover\",class:_vm.popoverClass,style:(_vm.styles)},[_vm._t(\"content\",null,{\"close\":_vm.hidePopover})],2):_vm._e()])}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","export const SECOND = 1000\nexport const MINUTE = 60 * SECOND\nexport const HOUR = 60 * MINUTE\nexport const DAY = 24 * HOUR\nexport const WEEK = 7 * DAY\nexport const MONTH = 30 * DAY\nexport const YEAR = 365.25 * DAY\n\nexport const relativeTime = (date, nowThreshold = 1) => {\n if (typeof date === 'string') date = Date.parse(date)\n const round = Date.now() > date ? Math.floor : Math.ceil\n const d = Math.abs(Date.now() - date)\n let r = { num: round(d / YEAR), key: 'time.years' }\n if (d < nowThreshold * SECOND) {\n r.num = 0\n r.key = 'time.now'\n } else if (d < MINUTE) {\n r.num = round(d / SECOND)\n r.key = 'time.seconds'\n } else if (d < HOUR) {\n r.num = round(d / MINUTE)\n r.key = 'time.minutes'\n } else if (d < DAY) {\n r.num = round(d / HOUR)\n r.key = 'time.hours'\n } else if (d < WEEK) {\n r.num = round(d / DAY)\n r.key = 'time.days'\n } else if (d < MONTH) {\n r.num = round(d / WEEK)\n r.key = 'time.weeks'\n } else if (d < YEAR) {\n r.num = round(d / MONTH)\n r.key = 'time.months'\n }\n // Remove plural form when singular\n if (r.num === 1) r.key = r.key.slice(0, -1)\n return r\n}\n\nexport const relativeTimeShort = (date, nowThreshold = 1) => {\n const r = relativeTime(date, nowThreshold)\n r.key += '_short'\n return r\n}\n","\n \n\n\n\n","/* script */\nexport * from \"!!babel-loader!../../../node_modules/vue-loader/lib/selector?type=script&index=0!./progress_button.vue\"\nimport __vue_script__ from \"!!babel-loader!../../../node_modules/vue-loader/lib/selector?type=script&index=0!./progress_button.vue\"\n/* template */\nimport {render as __vue_render__, staticRenderFns as __vue_static_render_fns__} from \"!!../../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-9f751ae6\\\",\\\"hasScoped\\\":false,\\\"optionsId\\\":\\\"0\\\",\\\"buble\\\":{\\\"transforms\\\":{}}}!../../../node_modules/vue-loader/lib/selector?type=template&index=0!./progress_button.vue\"\n/* template functional */\nvar __vue_template_functional__ = false\n/* styles */\nvar __vue_styles__ = null\n/* scopeId */\nvar __vue_scopeId__ = null\n/* moduleIdentifier (server only) */\nvar __vue_module_identifier__ = null\nimport normalizeComponent from \"!../../../node_modules/vue-loader/lib/runtime/component-normalizer\"\nvar Component = normalizeComponent(\n __vue_script__,\n __vue_render__,\n __vue_static_render_fns__,\n __vue_template_functional__,\n __vue_styles__,\n __vue_scopeId__,\n __vue_module_identifier__\n)\n\nexport default Component.exports\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('button',{attrs:{\"disabled\":_vm.progress || _vm.disabled},on:{\"click\":_vm.onClick}},[(_vm.progress && _vm.$slots.progress)?[_vm._t(\"progress\")]:[_vm._t(\"default\")]],2)}\nvar staticRenderFns = []\nexport { render, staticRenderFns }","import { hex2rgb } from '../color_convert/color_convert.js'\nconst highlightStyle = (prefs) => {\n if (prefs === undefined) return\n const { color, type } = prefs\n if (typeof color !== 'string') return\n const rgb = hex2rgb(color)\n if (rgb == null) return\n const solidColor = `rgb(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)})`\n const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .1)`\n const tintColor2 = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .2)`\n if (type === 'striped') {\n return {\n backgroundImage: [\n 'repeating-linear-gradient(135deg,',\n `${tintColor} ,`,\n `${tintColor} 20px,`,\n `${tintColor2} 20px,`,\n `${tintColor2} 40px`\n ].join(' '),\n backgroundPosition: '0 0'\n }\n } else if (type === 'solid') {\n return {\n backgroundColor: tintColor2\n }\n } else if (type === 'side') {\n return {\n backgroundImage: [\n 'linear-gradient(to right,',\n `${solidColor} ,`,\n `${solidColor} 2px,`,\n `transparent 6px`\n ].join(' '),\n backgroundPosition: '0 0'\n }\n }\n}\n\nconst highlightClass = (user) => {\n return 'USER____' + user.screen_name\n .replace(/\\./g, '_')\n .replace(/@/g, '_AT_')\n}\n\nexport {\n highlightClass,\n highlightStyle\n}\n","import Vue from 'vue'\n\nimport './tab_switcher.scss'\n\nexport default Vue.component('tab-switcher', {\n name: 'TabSwitcher',\n props: {\n renderOnlyFocused: {\n required: false,\n type: Boolean,\n default: false\n },\n onSwitch: {\n required: false,\n type: Function,\n default: undefined\n },\n activeTab: {\n required: false,\n type: String,\n default: undefined\n },\n scrollableTabs: {\n required: false,\n type: Boolean,\n default: false\n }\n },\n data () {\n return {\n active: this.$slots.default.findIndex(_ => _.tag)\n }\n },\n computed: {\n activeIndex () {\n // In case of controlled component\n if (this.activeTab) {\n return this.$slots.default.findIndex(slot => this.activeTab === slot.key)\n } else {\n return this.active\n }\n }\n },\n beforeUpdate () {\n const currentSlot = this.$slots.default[this.active]\n if (!currentSlot.tag) {\n this.active = this.$slots.default.findIndex(_ => _.tag)\n }\n },\n methods: {\n activateTab (index) {\n return (e) => {\n e.preventDefault()\n if (typeof this.onSwitch === 'function') {\n this.onSwitch.call(null, this.$slots.default[index].key)\n }\n this.active = index\n }\n }\n },\n render (h) {\n const tabs = this.$slots.default\n .map((slot, index) => {\n if (!slot.tag) return\n const classesTab = ['tab']\n const classesWrapper = ['tab-wrapper']\n\n if (this.activeIndex === index) {\n classesTab.push('active')\n classesWrapper.push('active')\n }\n if (slot.data.attrs.image) {\n return (\n