summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/pleroma/constants.ex7
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/content_language_map.ex49
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/language_code.ex27
-rw-r--r--lib/pleroma/hashtag.ex27
-rw-r--r--lib/pleroma/pagination.ex6
-rw-r--r--lib/pleroma/user.ex58
-rw-r--r--lib/pleroma/user/hashtag_follow.ex55
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex27
-rw-r--r--lib/pleroma/web/activity_pub/object_validator.ex12
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex4
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_fields.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_fixes.ex61
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/event_validator.ex5
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex23
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex11
-rw-r--r--lib/pleroma/web/activity_pub/views/object_view.ex6
-rw-r--r--lib/pleroma/web/api_spec.ex3
-rw-r--r--lib/pleroma/web/api_spec/operations/tag_operation.ex103
-rw-r--r--lib/pleroma/web/api_spec/schemas/tag.ex13
-rw-r--r--lib/pleroma/web/common_api/activity_draft.ex16
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/tag_controller.ex77
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/views/tag_view.ex25
-rw-r--r--lib/pleroma/web/router.ex5
-rw-r--r--lib/pleroma/web/streamer.ex13
26 files changed, 618 insertions, 31 deletions
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index c11c66f4d..3762c0035 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -20,7 +20,8 @@ defmodule Pleroma.Constants do
"deleted_activity_id",
"pleroma_internal",
"generator",
- "rules"
+ "rules",
+ "language"
]
)
@@ -36,10 +37,12 @@ defmodule Pleroma.Constants do
"updated",
"emoji",
"content",
+ "contentMap",
"summary",
"sensitive",
"attachment",
- "generator"
+ "generator",
+ "language"
]
)
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/content_language_map.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/content_language_map.ex
new file mode 100644
index 000000000..dcdab19f8
--- /dev/null
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/content_language_map.ex
@@ -0,0 +1,49 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMap do
+ use Ecto.Type
+
+ import Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode,
+ only: [good_locale_code?: 1]
+
+ def type, do: :map
+
+ def cast(%{} = object) do
+ with {status, %{} = data} when status in [:modified, :ok] <- validate_map(object) do
+ {:ok, data}
+ else
+ {_, nil} -> {:ok, nil}
+ {:error, _} -> :error
+ end
+ end
+
+ def cast(_), do: :error
+
+ def dump(data), do: {:ok, data}
+
+ def load(data), do: {:ok, data}
+
+ defp validate_map(%{} = object) do
+ {status, data} =
+ object
+ |> Enum.reduce({:ok, %{}}, fn
+ {lang, value}, {status, acc} when is_binary(lang) and is_binary(value) ->
+ if good_locale_code?(lang) do
+ {status, Map.put(acc, lang, value)}
+ else
+ {:modified, acc}
+ end
+
+ _, {_status, acc} ->
+ {:modified, acc}
+ end)
+
+ if data == %{} do
+ {status, nil}
+ else
+ {status, data}
+ end
+ end
+end
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/language_code.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/language_code.ex
new file mode 100644
index 000000000..4779deeb0
--- /dev/null
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/language_code.ex
@@ -0,0 +1,27 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode do
+ use Ecto.Type
+
+ def type, do: :string
+
+ def cast(language) when is_binary(language) do
+ if good_locale_code?(language) do
+ {:ok, language}
+ else
+ {:error, :invalid_language}
+ end
+ end
+
+ def cast(_), do: :error
+
+ def dump(data), do: {:ok, data}
+
+ def load(data), do: {:ok, data}
+
+ def good_locale_code?(code) when is_binary(code), do: code =~ ~r<^[a-zA-Z0-9\-]+\z$>
+
+ def good_locale_code?(_code), do: false
+end
diff --git a/lib/pleroma/hashtag.ex b/lib/pleroma/hashtag.ex
index a43d88220..3682f0c14 100644
--- a/lib/pleroma/hashtag.ex
+++ b/lib/pleroma/hashtag.ex
@@ -12,6 +12,7 @@ defmodule Pleroma.Hashtag do
alias Pleroma.Hashtag
alias Pleroma.Object
alias Pleroma.Repo
+ alias Pleroma.User.HashtagFollow
schema "hashtags" do
field(:name, :string)
@@ -27,6 +28,14 @@ defmodule Pleroma.Hashtag do
|> String.trim()
end
+ def get_by_id(id) do
+ Repo.get(Hashtag, id)
+ end
+
+ def get_by_name(name) do
+ Repo.get_by(Hashtag, name: normalize_name(name))
+ end
+
def get_or_create_by_name(name) do
changeset = changeset(%Hashtag{}, %{name: name})
@@ -103,4 +112,22 @@ defmodule Pleroma.Hashtag do
{:ok, deleted_count}
end
end
+
+ def get_followers(%Hashtag{id: hashtag_id}) do
+ from(hf in HashtagFollow)
+ |> where([hf], hf.hashtag_id == ^hashtag_id)
+ |> join(:inner, [hf], u in assoc(hf, :user))
+ |> select([hf, u], u.id)
+ |> Repo.all()
+ end
+
+ def get_recipients_for_activity(%Pleroma.Activity{object: %{hashtags: tags}})
+ when is_list(tags) do
+ tags
+ |> Enum.map(&get_followers/1)
+ |> List.flatten()
+ |> Enum.uniq()
+ end
+
+ def get_recipients_for_activity(_activity), do: []
end
diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex
index 8db732cc9..66812b17b 100644
--- a/lib/pleroma/pagination.ex
+++ b/lib/pleroma/pagination.ex
@@ -89,9 +89,9 @@ defmodule Pleroma.Pagination do
defp cast_params(params) do
param_types = %{
- min_id: :string,
- since_id: :string,
- max_id: :string,
+ min_id: params[:id_type] || :string,
+ since_id: params[:id_type] || :string,
+ max_id: params[:id_type] || :string,
offset: :integer,
limit: :integer,
skip_extra_order: :boolean,
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 7a36ece77..d9da9ede1 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -19,6 +19,7 @@ defmodule Pleroma.User do
alias Pleroma.Emoji
alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
+ alias Pleroma.Hashtag
alias Pleroma.HTML
alias Pleroma.Keys
alias Pleroma.MFA
@@ -27,6 +28,7 @@ defmodule Pleroma.User do
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.User.HashtagFollow
alias Pleroma.UserRelationship
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
@@ -174,6 +176,12 @@ defmodule Pleroma.User do
has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
+ many_to_many(:followed_hashtags, Hashtag,
+ on_replace: :delete,
+ on_delete: :delete_all,
+ join_through: HashtagFollow
+ )
+
for {relationship_type,
[
{outgoing_relation, outgoing_relation_target},
@@ -2861,4 +2869,54 @@ defmodule Pleroma.User do
birthday_month: month
})
end
+
+ defp maybe_load_followed_hashtags(%User{followed_hashtags: follows} = user)
+ when is_list(follows),
+ do: user
+
+ defp maybe_load_followed_hashtags(%User{} = user) do
+ followed_hashtags = HashtagFollow.get_by_user(user)
+ %{user | followed_hashtags: followed_hashtags}
+ end
+
+ def followed_hashtags(%User{followed_hashtags: follows})
+ when is_list(follows),
+ do: follows
+
+ def followed_hashtags(%User{} = user) do
+ {:ok, user} =
+ user
+ |> maybe_load_followed_hashtags()
+ |> set_cache()
+
+ user.followed_hashtags
+ end
+
+ def follow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
+ Logger.debug("Follow hashtag #{hashtag.name} for user #{user.nickname}")
+ user = maybe_load_followed_hashtags(user)
+
+ with {:ok, _} <- HashtagFollow.new(user, hashtag),
+ follows <- HashtagFollow.get_by_user(user),
+ %User{} = user <- user |> Map.put(:followed_hashtags, follows) do
+ user
+ |> set_cache()
+ end
+ end
+
+ def unfollow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
+ Logger.debug("Unfollow hashtag #{hashtag.name} for user #{user.nickname}")
+ user = maybe_load_followed_hashtags(user)
+
+ with {:ok, _} <- HashtagFollow.delete(user, hashtag),
+ follows <- HashtagFollow.get_by_user(user),
+ %User{} = user <- user |> Map.put(:followed_hashtags, follows) do
+ user
+ |> set_cache()
+ end
+ end
+
+ def following_hashtag?(%User{} = user, %Hashtag{} = hashtag) do
+ not is_nil(HashtagFollow.get(user, hashtag))
+ end
end
diff --git a/lib/pleroma/user/hashtag_follow.ex b/lib/pleroma/user/hashtag_follow.ex
new file mode 100644
index 000000000..3e28b130b
--- /dev/null
+++ b/lib/pleroma/user/hashtag_follow.ex
@@ -0,0 +1,55 @@
+defmodule Pleroma.User.HashtagFollow do
+ use Ecto.Schema
+ import Ecto.Query
+ import Ecto.Changeset
+
+ alias Pleroma.Hashtag
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ schema "user_follows_hashtag" do
+ belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+ belongs_to(:hashtag, Hashtag)
+ end
+
+ def changeset(%__MODULE__{} = user_hashtag_follow, attrs) do
+ user_hashtag_follow
+ |> cast(attrs, [:user_id, :hashtag_id])
+ |> unique_constraint(:hashtag_id,
+ name: :user_hashtag_follows_user_id_hashtag_id_index,
+ message: "already following"
+ )
+ |> validate_required([:user_id, :hashtag_id])
+ end
+
+ def new(%User{} = user, %Hashtag{} = hashtag) do
+ %__MODULE__{}
+ |> changeset(%{user_id: user.id, hashtag_id: hashtag.id})
+ |> Repo.insert(on_conflict: :nothing)
+ end
+
+ def delete(%User{} = user, %Hashtag{} = hashtag) do
+ with %__MODULE__{} = user_hashtag_follow <- get(user, hashtag) do
+ Repo.delete(user_hashtag_follow)
+ else
+ _ -> {:ok, nil}
+ end
+ end
+
+ def get(%User{} = user, %Hashtag{} = hashtag) do
+ from(hf in __MODULE__)
+ |> where([hf], hf.user_id == ^user.id and hf.hashtag_id == ^hashtag.id)
+ |> Repo.one()
+ end
+
+ def get_by_user(%User{} = user) do
+ user
+ |> followed_hashtags_query()
+ |> Repo.all()
+ end
+
+ def followed_hashtags_query(%User{} = user) do
+ Ecto.assoc(user, :followed_hashtags)
+ |> Ecto.Query.order_by([h], desc: h.id)
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index df8795fe4..62c7a7b31 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -924,6 +924,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
)
end
+ # Essentially, either look for activities addressed to `recipients`, _OR_ ones
+ # that reference a hashtag that the user follows
+ # Firstly, two fallbacks in case there's no hashtag constraint, or the user doesn't
+ # follow any
+ defp restrict_recipients_or_hashtags(query, recipients, user, nil) do
+ restrict_recipients(query, recipients, user)
+ end
+
+ defp restrict_recipients_or_hashtags(query, recipients, user, []) do
+ restrict_recipients(query, recipients, user)
+ end
+
+ defp restrict_recipients_or_hashtags(query, recipients, _user, hashtag_ids) do
+ from([activity, object] in query)
+ |> join(:left, [activity, object], hto in "hashtags_objects",
+ on: hto.object_id == object.id,
+ as: :hto
+ )
+ |> where(
+ [activity, object, hto: hto],
+ (hto.hashtag_id in ^hashtag_ids and ^Constants.as_public() in activity.recipients) or
+ fragment("? && ?", ^recipients, activity.recipients)
+ )
+ end
+
defp restrict_local(query, %{local_only: true}) do
from(activity in query, where: activity.local == true)
end
@@ -1414,7 +1439,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> maybe_preload_report_notes(opts)
|> maybe_set_thread_muted_field(opts)
|> maybe_order(opts)
- |> restrict_recipients(recipients, opts[:user])
+ |> restrict_recipients_or_hashtags(recipients, opts[:user], opts[:followed_hashtags])
|> restrict_replies(opts)
|> restrict_since(opts)
|> restrict_local(opts)
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index c509890f6..ee12f3ebf 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -26,6 +26,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
@@ -115,7 +116,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
meta
)
when objtype in ~w[Question Answer Audio Video Image Event Article Note Page] do
- with {:ok, object_data} <- cast_and_apply_and_stringify_with_history(object),
+ with {:ok, object_data} <-
+ object
+ |> CommonFixes.maybe_add_language_from_activity(create_activity)
+ |> cast_and_apply_and_stringify_with_history(),
meta = Keyword.put(meta, :object_data, object_data),
{:ok, create_activity} <-
create_activity
@@ -165,7 +169,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
)
when objtype in ~w[Question Answer Audio Video Event Article Note Page] do
with {_, false} <- {:local, Access.get(meta, :local, false)},
- {_, {:ok, object_data, _}} <- {:object_validation, validate(object, meta)},
+ {_, {:ok, object_data, _}} <-
+ {:object_validation,
+ object
+ |> CommonFixes.maybe_add_language_from_activity(update_activity)
+ |> validate(meta)},
meta = Keyword.put(meta, :object_data, object_data),
{:ok, update_activity} <-
update_activity
diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
index ada1a4ea9..81ab354fe 100644
--- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
def cast_and_apply(data) do
data
- |> cast_data
+ |> cast_data()
|> apply_action(:insert)
end
@@ -88,6 +88,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|> CommonFixes.fix_likes()
|> Transmogrifier.fix_emoji()
|> Transmogrifier.fix_content_map()
+ |> CommonFixes.maybe_add_language()
+ |> CommonFixes.maybe_add_content_map()
end
def changeset(struct, data) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
index 1a5d02601..22cf0cc05 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
@@ -31,6 +31,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
defmacro object_fields do
quote bind_quoted: binding() do
field(:content, :string)
+ field(:contentMap, ObjectValidators.ContentLanguageMap)
field(:published, ObjectValidators.DateTime)
field(:updated, ObjectValidators.DateTime)
@@ -58,6 +59,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0)
field(:quotes_count, :integer, default: 0)
+ field(:language, ObjectValidators.LanguageCode)
field(:inReplyTo, ObjectValidators.ObjectID)
field(:quoteUrl, ObjectValidators.ObjectID)
field(:url, ObjectValidators.BareUri)
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
index a39110e10..87d3e0c8f 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
@@ -11,6 +11,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
+ import Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode,
+ only: [good_locale_code?: 1]
+
+ import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
+
require Pleroma.Constants
def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do
@@ -132,4 +137,60 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
end
def object_link_tag?(_), do: false
+
+ def maybe_add_language_from_activity(object, activity) do
+ language = get_language_from_context(activity)
+
+ if language do
+ Map.put(object, "language", language)
+ else
+ object
+ end
+ end
+
+ def maybe_add_language(object) do
+ language =
+ [
+ get_language_from_context(object),
+ get_language_from_content_map(object)
+ ]
+ |> Enum.find(&good_locale_code?(&1))
+
+ if language do
+ Map.put(object, "language", language)
+ else
+ object
+ end
+ end
+
+ defp get_language_from_context(%{"@context" => context}) when is_list(context) do
+ case context
+ |> Enum.find(fn
+ %{"@language" => language} -> language != "und"
+ _ -> nil
+ end) do
+ %{"@language" => language} -> language
+ _ -> nil
+ end
+ end
+
+ defp get_language_from_context(_), do: nil
+
+ defp get_language_from_content_map(%{"contentMap" => content_map, "content" => source_content}) do
+ content_groups = Map.to_list(content_map)
+
+ case Enum.find(content_groups, fn {_, content} -> content == source_content end) do
+ {language, _} -> language
+ _ -> nil
+ end
+ end
+
+ defp get_language_from_content_map(_), do: nil
+
+ def maybe_add_content_map(%{"language" => language, "content" => content} = object)
+ when not_empty_string(language) do
+ Map.put(object, "contentMap", Map.put(%{}, language, content))
+ end
+
+ def maybe_add_content_map(object), do: object
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex
index c87515e80..ea14d6aca 100644
--- a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
def cast_and_apply(data) do
data
- |> cast_data
+ |> cast_data()
|> apply_action(:insert)
end
@@ -38,6 +38,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
|> validate_data()
end
+ @spec cast_data(map()) :: map()
def cast_data(data) do
%__MODULE__{}
|> changeset(data)
@@ -49,6 +50,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
|> CommonFixes.fix_object_defaults()
|> CommonFixes.fix_likes()
|> Transmogrifier.fix_emoji()
+ |> CommonFixes.maybe_add_language()
+ |> CommonFixes.maybe_add_content_map()
end
def changeset(struct, data) do
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 2f8a7f8f2..4c9956c7a 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -16,12 +16,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator
import Ecto.Query
+ import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
require Pleroma.Constants
@@ -166,7 +168,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_quote_url_and_maybe_fetch(object, options \\ []) do
quote_url =
- case Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes.fix_quote_url(object) do
+ case CommonFixes.fix_quote_url(object) do
%{"quoteUrl" => quote_url} -> quote_url
_ -> nil
end
@@ -336,6 +338,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_tag(object), do: object
+ # prefer content over contentMap
+ def fix_content_map(%{"content" => content} = object) when not_empty_string(content), do: object
+
# content map usually only has one language so this will do for now.
def fix_content_map(%{"contentMap" => content_map} = object) do
content_groups = Map.to_list(content_map)
@@ -716,6 +721,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> set_reply_to_uri
|> set_quote_url
|> set_replies
+ |> CommonFixes.maybe_add_content_map()
|> strip_internal_fields
|> strip_internal_tags
|> set_type
@@ -750,12 +756,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object_id
|> Object.normalize(fetch: false)
|> Map.get(:data)
- |> prepare_object
data =
data
- |> Map.put("object", object)
- |> Map.merge(Utils.make_json_ld_header())
+ |> Map.put("object", prepare_object(object))
+ |> Map.merge(Utils.make_json_ld_header(object))
|> Map.delete("bcc")
{:ok, data}
@@ -763,14 +768,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
when objtype in Pleroma.Constants.updatable_object_types() do
- object =
- object
- |> prepare_object
-
data =
data
- |> Map.put("object", object)
- |> Map.merge(Utils.make_json_ld_header())
+ |> Map.put("object", prepare_object(object))
+ |> Map.merge(Utils.make_json_ld_header(object))
|> Map.delete("bcc")
{:ok, data}
@@ -840,7 +841,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data
|> strip_internal_fields
|> maybe_fix_object_url
- |> Map.merge(Utils.make_json_ld_header())
+ |> Map.merge(Utils.make_json_ld_header(data))
{:ok, data}
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 6c792804d..f30c92abf 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -20,6 +20,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Pleroma.Web.Router.Helpers
import Ecto.Query
+ import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
require Logger
require Pleroma.Constants
@@ -109,18 +110,24 @@ defmodule Pleroma.Web.ActivityPub.Utils do
end
end
- def make_json_ld_header do
+ def make_json_ld_header(data \\ %{}) do
%{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
%{
- "@language" => "und"
+ "@language" => get_language(data)
}
]
}
end
+ defp get_language(%{"language" => language}) when not_empty_string(language) do
+ language
+ end
+
+ defp get_language(_), do: "und"
+
def make_date do
DateTime.utc_now() |> DateTime.to_iso8601()
end
diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex
index 63caa915c..13b5b2542 100644
--- a/lib/pleroma/web/activity_pub/views/object_view.ex
+++ b/lib/pleroma/web/activity_pub/views/object_view.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
alias Pleroma.Web.ActivityPub.Transmogrifier
def render("object.json", %{object: %Object{} = object}) do
- base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
+ base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(object.data)
additional = Transmogrifier.prepare_object(object.data)
Map.merge(base, additional)
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
def render("object.json", %{object: %Activity{data: %{"type" => activity_type}} = activity})
when activity_type in ["Create", "Listen"] do
- base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
+ base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(activity.data)
object = Object.normalize(activity, fetch: false)
additional =
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
end
def render("object.json", %{object: %Activity{} = activity}) do
- base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
+ base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(activity.data)
object_id = Object.normalize(activity, id_only: true)
additional =
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
index 314782818..63409870e 100644
--- a/lib/pleroma/web/api_spec.ex
+++ b/lib/pleroma/web/api_spec.ex
@@ -139,7 +139,8 @@ defmodule Pleroma.Web.ApiSpec do
"Search",
"Status actions",
"Media attachments",
- "Bookmark folders"
+ "Bookmark folders",
+ "Tags"
]
},
%{
diff --git a/lib/pleroma/web/api_spec/operations/tag_operation.ex b/lib/pleroma/web/api_spec/operations/tag_operation.ex
new file mode 100644
index 000000000..ce4f4ad5b
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/tag_operation.ex
@@ -0,0 +1,103 @@
+defmodule Pleroma.Web.ApiSpec.TagOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.Tag
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Tags"],
+ summary: "Hashtag",
+ description: "View a hashtag",
+ security: [%{"oAuth" => ["read"]}],
+ parameters: [id_param()],
+ operationId: "TagController.show",
+ responses: %{
+ 200 => Operation.response("Hashtag", "application/json", Tag),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def follow_operation do
+ %Operation{
+ tags: ["Tags"],
+ summary: "Follow a hashtag",
+ description: "Follow a hashtag",
+ security: [%{"oAuth" => ["write:follows"]}],
+ parameters: [id_param()],
+ operationId: "TagController.follow",
+ responses: %{
+ 200 => Operation.response("Hashtag", "application/json", Tag),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def unfollow_operation do
+ %Operation{
+ tags: ["Tags"],
+ summary: "Unfollow a hashtag",
+ description: "Unfollow a hashtag",
+ security: [%{"oAuth" => ["write:follows"]}],
+ parameters: [id_param()],
+ operationId: "TagController.unfollow",
+ responses: %{
+ 200 => Operation.response("Hashtag", "application/json", Tag),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def show_followed_operation do
+ %Operation{
+ tags: ["Tags"],
+ summary: "Followed hashtags",
+ description: "View a list of hashtags the currently authenticated user is following",
+ parameters: pagination_params(),
+ security: [%{"oAuth" => ["read:follows"]}],
+ operationId: "TagController.show_followed",
+ responses: %{
+ 200 =>
+ Operation.response("Hashtags", "application/json", %Schema{
+ type: :array,
+ items: Tag
+ }),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(
+ :id,
+ :path,
+ %Schema{type: :string},
+ "Name of the hashtag"
+ )
+ end
+
+ def pagination_params do
+ [
+ Operation.parameter(:max_id, :query, :integer, "Return items older than this ID"),
+ Operation.parameter(
+ :min_id,
+ :query,
+ :integer,
+ "Return the oldest items newer than this ID"
+ ),
+ Operation.parameter(
+ :limit,
+ :query,
+ %Schema{type: :integer, default: 20},
+ "Maximum number of items to return. Will be ignored if it's more than 40"
+ )
+ ]
+ end
+end
diff --git a/lib/pleroma/web/api_spec/schemas/tag.ex b/lib/pleroma/web/api_spec/schemas/tag.ex
index 66bf0ca71..05ff10cd3 100644
--- a/lib/pleroma/web/api_spec/schemas/tag.ex
+++ b/lib/pleroma/web/api_spec/schemas/tag.ex
@@ -17,11 +17,22 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Tag do
type: :string,
format: :uri,
description: "A link to the hashtag on the instance"
+ },
+ following: %Schema{
+ type: :boolean,
+ description: "Whether the authenticated user is following the hashtag"
+ },
+ history: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description:
+ "A list of historical uses of the hashtag (not implemented, for compatibility only)"
}
},
example: %{
name: "cofe",
- url: "https://lain.com/tag/cofe"
+ url: "https://lain.com/tag/cofe",
+ following: false
}
})
end
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 8aa1e258d..4220757df 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -11,6 +11,9 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
+ import Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode,
+ only: [good_locale_code?: 1]
+
import Pleroma.Web.Gettext
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
@@ -38,6 +41,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
cc: [],
context: nil,
sensitive: false,
+ language: nil,
object: nil,
preview?: false,
changes: %{}
@@ -64,6 +68,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|> content()
|> with_valid(&to_and_cc/1)
|> with_valid(&context/1)
+ |> with_valid(&language/1)
|> sensitive()
|> with_valid(&object/1)
|> preview?()
@@ -249,6 +254,16 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
%__MODULE__{draft | sensitive: sensitive}
end
+ defp language(draft) do
+ language = draft.params[:language]
+
+ if good_locale_code?(language) do
+ %__MODULE__{draft | language: language}
+ else
+ draft
+ end
+ end
+
defp object(draft) do
emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
@@ -288,6 +303,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
"mediaType" => Utils.get_content_type(draft.params[:content_type])
})
|> Map.put("generator", draft.params[:generator])
+ |> Map.put("language", draft.language)
%__MODULE__{draft | object: object}
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/tag_controller.ex b/lib/pleroma/web/mastodon_api/controllers/tag_controller.ex
new file mode 100644
index 000000000..21c21e984
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/tag_controller.ex
@@ -0,0 +1,77 @@
+defmodule Pleroma.Web.MastodonAPI.TagController do
+ @moduledoc "Hashtag routes for mastodon API"
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Hashtag
+ alias Pleroma.Pagination
+ alias Pleroma.User
+
+ import Pleroma.Web.ControllerHelper,
+ only: [
+ add_link_headers: 2
+ ]
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ Pleroma.Web.Plugs.OAuthScopesPlug,
+ %{scopes: ["read"]} when action in [:show]
+ )
+
+ plug(
+ Pleroma.Web.Plugs.OAuthScopesPlug,
+ %{scopes: ["read:follows"]} when action in [:show_followed]
+ )
+
+ plug(
+ Pleroma.Web.Plugs.OAuthScopesPlug,
+ %{scopes: ["write:follows"]} when action in [:follow, :unfollow]
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TagOperation
+
+ def show(conn, %{id: id}) do
+ with %Hashtag{} = hashtag <- Hashtag.get_by_name(id) do
+ render(conn, "show.json", tag: hashtag, for_user: conn.assigns.user)
+ else
+ _ -> conn |> render_error(:not_found, "Hashtag not found")
+ end
+ end
+
+ def follow(conn, %{id: id}) do
+ with %Hashtag{} = hashtag <- Hashtag.get_by_name(id),
+ %User{} = user <- conn.assigns.user,
+ {:ok, _} <-
+ User.follow_hashtag(user, hashtag) do
+ render(conn, "show.json", tag: hashtag, for_user: user)
+ else
+ _ -> render_error(conn, :not_found, "Hashtag not found")
+ end
+ end
+
+ def unfollow(conn, %{id: id}) do
+ with %Hashtag{} = hashtag <- Hashtag.get_by_name(id),
+ %User{} = user <- conn.assigns.user,
+ {:ok, _} <-
+ User.unfollow_hashtag(user, hashtag) do
+ render(conn, "show.json", tag: hashtag, for_user: user)
+ else
+ _ -> render_error(conn, :not_found, "Hashtag not found")
+ end
+ end
+
+ def show_followed(conn, params) do
+ with %{assigns: %{user: %User{} = user}} <- conn do
+ params = Map.put(params, :id_type, :integer)
+
+ hashtags =
+ user
+ |> User.HashtagFollow.followed_hashtags_query()
+ |> Pagination.fetch_paginated(params)
+
+ conn
+ |> add_link_headers(hashtags)
+ |> render("index.json", tags: hashtags, for_user: user)
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index 293c61b41..5ee74a80e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -40,6 +40,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
# GET /api/v1/timelines/home
def home(%{assigns: %{user: user}} = conn, params) do
+ followed_hashtags =
+ user
+ |> User.followed_hashtags()
+ |> Enum.map(& &1.id)
+
params =
params
|> Map.put(:type, ["Create", "Announce"])
@@ -49,6 +54,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put(:announce_filtering_user, user)
|> Map.put(:user, user)
|> Map.put(:local_only, params[:local])
+ |> Map.put(:followed_hashtags, followed_hashtags)
|> Map.delete(:local)
activities =
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 3bf870c24..10966edd6 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -227,7 +227,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
mentions: mentions,
tags: reblogged[:tags] || [],
application: build_application(object.data["generator"]),
- language: nil,
+ language: get_language(object),
emojis: [],
pleroma: %{
local: activity.local,
@@ -445,7 +445,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
mentions: mentions,
tags: build_tags(tags),
application: build_application(object.data["generator"]),
- language: nil,
+ language: get_language(object),
emojis: build_emojis(object.data["emoji"]),
pleroma: %{
local: activity.local,
@@ -829,6 +829,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
Utils.get_content_type(nil)
end
+ defp get_language(%{data: %{"language" => "und"}}), do: nil
+
+ defp get_language(object), do: object.data["language"]
+
defp proxied_url(url, page_url_data) do
if is_binary(url) do
build_image_url(URI.parse(url), page_url_data) |> MediaProxy.url()
diff --git a/lib/pleroma/web/mastodon_api/views/tag_view.ex b/lib/pleroma/web/mastodon_api/views/tag_view.ex
new file mode 100644
index 000000000..e24d423c2
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/tag_view.ex
@@ -0,0 +1,25 @@
+defmodule Pleroma.Web.MastodonAPI.TagView do
+ use Pleroma.Web, :view
+ alias Pleroma.User
+ alias Pleroma.Web.Router.Helpers
+
+ def render("index.json", %{tags: tags, for_user: user}) do
+ safe_render_many(tags, __MODULE__, "show.json", %{for_user: user})
+ end
+
+ def render("show.json", %{tag: tag, for_user: user}) do
+ following =
+ with %User{} <- user do
+ User.following_hashtag?(user, tag)
+ else
+ _ -> false
+ end
+
+ %{
+ name: tag.name,
+ url: Helpers.tag_feed_url(Pleroma.Web.Endpoint, :feed, tag.name),
+ history: [],
+ following: following
+ }
+ end
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 0423ca9e2..ca76427ac 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -755,6 +755,11 @@ defmodule Pleroma.Web.Router do
get("/announcements", AnnouncementController, :index)
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
+
+ get("/tags/:id", TagController, :show)
+ post("/tags/:id/follow", TagController, :follow)
+ post("/tags/:id/unfollow", TagController, :unfollow)
+ get("/followed_tags", TagController, :show_followed)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index 76dc0f42d..cc149e04c 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -19,6 +19,7 @@ defmodule Pleroma.Web.Streamer do
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.StreamerView
+ require Pleroma.Constants
@registry Pleroma.Web.StreamerRegistry
@@ -305,7 +306,17 @@ defmodule Pleroma.Web.Streamer do
User.get_recipients_from_activity(item)
|> Enum.map(fn %{id: id} -> "user:#{id}" end)
- Enum.each(recipient_topics, fn topic ->
+ hashtag_recipients =
+ if Pleroma.Constants.as_public() in item.recipients do
+ Pleroma.Hashtag.get_recipients_for_activity(item)
+ |> Enum.map(fn id -> "user:#{id}" end)
+ else
+ []
+ end
+
+ all_recipients = Enum.uniq(recipient_topics ++ hashtag_recipients)
+
+ Enum.each(all_recipients, fn topic ->
push_to_socket(topic, item)
end)
end