mirror of
https://git.pleroma.social/pleroma/pleroma.git
synced 2025-03-12 22:52:41 +00:00
Merge branch and resolve conflict in database_test.exs
This commit is contained in:
commit
5851d787b6
42 changed files with 1181 additions and 41 deletions
1
changelog.d/follow-hashtags.add
Normal file
1
changelog.d/follow-hashtags.add
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Hashtag following
|
1
changelog.d/post-languages.add
Normal file
1
changelog.d/post-languages.add
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Allow to specify post language
|
|
@ -20,7 +20,8 @@ defmodule Pleroma.Constants do
|
||||||
"deleted_activity_id",
|
"deleted_activity_id",
|
||||||
"pleroma_internal",
|
"pleroma_internal",
|
||||||
"generator",
|
"generator",
|
||||||
"rules"
|
"rules",
|
||||||
|
"language"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,10 +37,12 @@ defmodule Pleroma.Constants do
|
||||||
"updated",
|
"updated",
|
||||||
"emoji",
|
"emoji",
|
||||||
"content",
|
"content",
|
||||||
|
"contentMap",
|
||||||
"summary",
|
"summary",
|
||||||
"sensitive",
|
"sensitive",
|
||||||
"attachment",
|
"attachment",
|
||||||
"generator"
|
"generator",
|
||||||
|
"language"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -12,6 +12,7 @@ defmodule Pleroma.Hashtag do
|
||||||
alias Pleroma.Hashtag
|
alias Pleroma.Hashtag
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
|
alias Pleroma.User.HashtagFollow
|
||||||
|
|
||||||
schema "hashtags" do
|
schema "hashtags" do
|
||||||
field(:name, :string)
|
field(:name, :string)
|
||||||
|
@ -27,6 +28,14 @@ defmodule Pleroma.Hashtag do
|
||||||
|> String.trim()
|
|> String.trim()
|
||||||
end
|
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
|
def get_or_create_by_name(name) do
|
||||||
changeset = changeset(%Hashtag{}, %{name: name})
|
changeset = changeset(%Hashtag{}, %{name: name})
|
||||||
|
|
||||||
|
@ -103,4 +112,22 @@ defmodule Pleroma.Hashtag do
|
||||||
{:ok, deleted_count}
|
{:ok, deleted_count}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -89,9 +89,9 @@ defmodule Pleroma.Pagination do
|
||||||
|
|
||||||
defp cast_params(params) do
|
defp cast_params(params) do
|
||||||
param_types = %{
|
param_types = %{
|
||||||
min_id: :string,
|
min_id: params[:id_type] || :string,
|
||||||
since_id: :string,
|
since_id: params[:id_type] || :string,
|
||||||
max_id: :string,
|
max_id: params[:id_type] || :string,
|
||||||
offset: :integer,
|
offset: :integer,
|
||||||
limit: :integer,
|
limit: :integer,
|
||||||
skip_extra_order: :boolean,
|
skip_extra_order: :boolean,
|
||||||
|
|
|
@ -19,6 +19,7 @@ defmodule Pleroma.User do
|
||||||
alias Pleroma.Emoji
|
alias Pleroma.Emoji
|
||||||
alias Pleroma.FollowingRelationship
|
alias Pleroma.FollowingRelationship
|
||||||
alias Pleroma.Formatter
|
alias Pleroma.Formatter
|
||||||
|
alias Pleroma.Hashtag
|
||||||
alias Pleroma.HTML
|
alias Pleroma.HTML
|
||||||
alias Pleroma.Keys
|
alias Pleroma.Keys
|
||||||
alias Pleroma.MFA
|
alias Pleroma.MFA
|
||||||
|
@ -27,6 +28,7 @@ defmodule Pleroma.User do
|
||||||
alias Pleroma.Registration
|
alias Pleroma.Registration
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
alias Pleroma.User.HashtagFollow
|
||||||
alias Pleroma.UserRelationship
|
alias Pleroma.UserRelationship
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
alias Pleroma.Web.ActivityPub.Builder
|
alias Pleroma.Web.ActivityPub.Builder
|
||||||
|
@ -174,6 +176,12 @@ defmodule Pleroma.User do
|
||||||
has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
|
has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
|
||||||
has_many(:incoming_relationships, UserRelationship, foreign_key: :target_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,
|
for {relationship_type,
|
||||||
[
|
[
|
||||||
{outgoing_relation, outgoing_relation_target},
|
{outgoing_relation, outgoing_relation_target},
|
||||||
|
@ -2861,4 +2869,54 @@ defmodule Pleroma.User do
|
||||||
birthday_month: month
|
birthday_month: month
|
||||||
})
|
})
|
||||||
end
|
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
|
end
|
||||||
|
|
55
lib/pleroma/user/hashtag_follow.ex
Normal file
55
lib/pleroma/user/hashtag_follow.ex
Normal file
|
@ -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
|
|
@ -924,6 +924,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
)
|
)
|
||||||
end
|
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
|
defp restrict_local(query, %{local_only: true}) do
|
||||||
from(activity in query, where: activity.local == true)
|
from(activity in query, where: activity.local == true)
|
||||||
end
|
end
|
||||||
|
@ -1414,7 +1439,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
|> maybe_preload_report_notes(opts)
|
|> maybe_preload_report_notes(opts)
|
||||||
|> maybe_set_thread_muted_field(opts)
|
|> maybe_set_thread_muted_field(opts)
|
||||||
|> maybe_order(opts)
|
|> maybe_order(opts)
|
||||||
|> restrict_recipients(recipients, opts[:user])
|
|> restrict_recipients_or_hashtags(recipients, opts[:user], opts[:followed_hashtags])
|
||||||
|> restrict_replies(opts)
|
|> restrict_replies(opts)
|
||||||
|> restrict_since(opts)
|
|> restrict_since(opts)
|
||||||
|> restrict_local(opts)
|
|> restrict_local(opts)
|
||||||
|
|
|
@ -26,6 +26,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
|
||||||
|
@ -115,7 +116,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
||||||
meta
|
meta
|
||||||
)
|
)
|
||||||
when objtype in ~w[Question Answer Audio Video Image Event Article Note Page] do
|
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),
|
meta = Keyword.put(meta, :object_data, object_data),
|
||||||
{:ok, create_activity} <-
|
{:ok, create_activity} <-
|
||||||
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
|
when objtype in ~w[Question Answer Audio Video Event Article Note Page] do
|
||||||
with {_, false} <- {:local, Access.get(meta, :local, false)},
|
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),
|
meta = Keyword.put(meta, :object_data, object_data),
|
||||||
{:ok, update_activity} <-
|
{:ok, update_activity} <-
|
||||||
update_activity
|
update_activity
|
||||||
|
|
|
@ -30,7 +30,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
||||||
|
|
||||||
def cast_and_apply(data) do
|
def cast_and_apply(data) do
|
||||||
data
|
data
|
||||||
|> cast_data
|
|> cast_data()
|
||||||
|> apply_action(:insert)
|
|> apply_action(:insert)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -88,6 +88,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
||||||
|> CommonFixes.fix_likes()
|
|> CommonFixes.fix_likes()
|
||||||
|> Transmogrifier.fix_emoji()
|
|> Transmogrifier.fix_emoji()
|
||||||
|> Transmogrifier.fix_content_map()
|
|> Transmogrifier.fix_content_map()
|
||||||
|
|> CommonFixes.maybe_add_language()
|
||||||
|
|> CommonFixes.maybe_add_content_map()
|
||||||
end
|
end
|
||||||
|
|
||||||
def changeset(struct, data) do
|
def changeset(struct, data) do
|
||||||
|
|
|
@ -31,6 +31,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
|
||||||
defmacro object_fields do
|
defmacro object_fields do
|
||||||
quote bind_quoted: binding() do
|
quote bind_quoted: binding() do
|
||||||
field(:content, :string)
|
field(:content, :string)
|
||||||
|
field(:contentMap, ObjectValidators.ContentLanguageMap)
|
||||||
|
|
||||||
field(:published, ObjectValidators.DateTime)
|
field(:published, ObjectValidators.DateTime)
|
||||||
field(:updated, ObjectValidators.DateTime)
|
field(:updated, ObjectValidators.DateTime)
|
||||||
|
@ -58,6 +59,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
|
||||||
field(:like_count, :integer, default: 0)
|
field(:like_count, :integer, default: 0)
|
||||||
field(:announcement_count, :integer, default: 0)
|
field(:announcement_count, :integer, default: 0)
|
||||||
field(:quotes_count, :integer, default: 0)
|
field(:quotes_count, :integer, default: 0)
|
||||||
|
field(:language, ObjectValidators.LanguageCode)
|
||||||
field(:inReplyTo, ObjectValidators.ObjectID)
|
field(:inReplyTo, ObjectValidators.ObjectID)
|
||||||
field(:quoteUrl, ObjectValidators.ObjectID)
|
field(:quoteUrl, ObjectValidators.ObjectID)
|
||||||
field(:url, ObjectValidators.BareUri)
|
field(:url, ObjectValidators.BareUri)
|
||||||
|
|
|
@ -11,6 +11,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
||||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
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
|
require Pleroma.Constants
|
||||||
|
|
||||||
def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do
|
def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do
|
||||||
|
@ -132,4 +137,60 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
||||||
end
|
end
|
||||||
|
|
||||||
def object_link_tag?(_), do: false
|
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
|
end
|
||||||
|
|
|
@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
|
||||||
|
|
||||||
def cast_and_apply(data) do
|
def cast_and_apply(data) do
|
||||||
data
|
data
|
||||||
|> cast_data
|
|> cast_data()
|
||||||
|> apply_action(:insert)
|
|> apply_action(:insert)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
|
||||||
|> validate_data()
|
|> validate_data()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec cast_data(map()) :: map()
|
||||||
def cast_data(data) do
|
def cast_data(data) do
|
||||||
%__MODULE__{}
|
%__MODULE__{}
|
||||||
|> changeset(data)
|
|> changeset(data)
|
||||||
|
@ -49,6 +50,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
|
||||||
|> CommonFixes.fix_object_defaults()
|
|> CommonFixes.fix_object_defaults()
|
||||||
|> CommonFixes.fix_likes()
|
|> CommonFixes.fix_likes()
|
||||||
|> Transmogrifier.fix_emoji()
|
|> Transmogrifier.fix_emoji()
|
||||||
|
|> CommonFixes.maybe_add_language()
|
||||||
|
|> CommonFixes.maybe_add_content_map()
|
||||||
end
|
end
|
||||||
|
|
||||||
def changeset(struct, data) do
|
def changeset(struct, data) do
|
||||||
|
|
|
@ -16,12 +16,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
alias Pleroma.Web.ActivityPub.Builder
|
alias Pleroma.Web.ActivityPub.Builder
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidator
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||||
alias Pleroma.Web.ActivityPub.Pipeline
|
alias Pleroma.Web.ActivityPub.Pipeline
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
alias Pleroma.Web.ActivityPub.Visibility
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
alias Pleroma.Web.Federator
|
alias Pleroma.Web.Federator
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||||
|
|
||||||
require Pleroma.Constants
|
require Pleroma.Constants
|
||||||
|
|
||||||
|
@ -166,7 +168,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
|
|
||||||
def fix_quote_url_and_maybe_fetch(object, options \\ []) do
|
def fix_quote_url_and_maybe_fetch(object, options \\ []) do
|
||||||
quote_url =
|
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
|
%{"quoteUrl" => quote_url} -> quote_url
|
||||||
_ -> nil
|
_ -> nil
|
||||||
end
|
end
|
||||||
|
@ -336,6 +338,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
|
|
||||||
def fix_tag(object), do: object
|
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.
|
# content map usually only has one language so this will do for now.
|
||||||
def fix_content_map(%{"contentMap" => content_map} = object) do
|
def fix_content_map(%{"contentMap" => content_map} = object) do
|
||||||
content_groups = Map.to_list(content_map)
|
content_groups = Map.to_list(content_map)
|
||||||
|
@ -716,6 +721,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
|> set_reply_to_uri
|
|> set_reply_to_uri
|
||||||
|> set_quote_url
|
|> set_quote_url
|
||||||
|> set_replies
|
|> set_replies
|
||||||
|
|> CommonFixes.maybe_add_content_map()
|
||||||
|> strip_internal_fields
|
|> strip_internal_fields
|
||||||
|> strip_internal_tags
|
|> strip_internal_tags
|
||||||
|> set_type
|
|> set_type
|
||||||
|
@ -750,12 +756,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
object_id
|
object_id
|
||||||
|> Object.normalize(fetch: false)
|
|> Object.normalize(fetch: false)
|
||||||
|> Map.get(:data)
|
|> Map.get(:data)
|
||||||
|> prepare_object
|
|
||||||
|
|
||||||
data =
|
data =
|
||||||
data
|
data
|
||||||
|> Map.put("object", object)
|
|> Map.put("object", prepare_object(object))
|
||||||
|> Map.merge(Utils.make_json_ld_header())
|
|> Map.merge(Utils.make_json_ld_header(object))
|
||||||
|> Map.delete("bcc")
|
|> Map.delete("bcc")
|
||||||
|
|
||||||
{:ok, data}
|
{:ok, data}
|
||||||
|
@ -763,14 +768,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
|
|
||||||
def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
|
def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
|
||||||
when objtype in Pleroma.Constants.updatable_object_types() do
|
when objtype in Pleroma.Constants.updatable_object_types() do
|
||||||
object =
|
|
||||||
object
|
|
||||||
|> prepare_object
|
|
||||||
|
|
||||||
data =
|
data =
|
||||||
data
|
data
|
||||||
|> Map.put("object", object)
|
|> Map.put("object", prepare_object(object))
|
||||||
|> Map.merge(Utils.make_json_ld_header())
|
|> Map.merge(Utils.make_json_ld_header(object))
|
||||||
|> Map.delete("bcc")
|
|> Map.delete("bcc")
|
||||||
|
|
||||||
{:ok, data}
|
{:ok, data}
|
||||||
|
@ -840,7 +841,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
data
|
data
|
||||||
|> strip_internal_fields
|
|> strip_internal_fields
|
||||||
|> maybe_fix_object_url
|
|> maybe_fix_object_url
|
||||||
|> Map.merge(Utils.make_json_ld_header())
|
|> Map.merge(Utils.make_json_ld_header(data))
|
||||||
|
|
||||||
{:ok, data}
|
{:ok, data}
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,6 +20,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
||||||
alias Pleroma.Web.Router.Helpers
|
alias Pleroma.Web.Router.Helpers
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
require Pleroma.Constants
|
require Pleroma.Constants
|
||||||
|
@ -109,18 +110,24 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_json_ld_header do
|
def make_json_ld_header(data \\ %{}) do
|
||||||
%{
|
%{
|
||||||
"@context" => [
|
"@context" => [
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
"#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
|
"#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
|
||||||
%{
|
%{
|
||||||
"@language" => "und"
|
"@language" => get_language(data)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp get_language(%{"language" => language}) when not_empty_string(language) do
|
||||||
|
language
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_language(_), do: "und"
|
||||||
|
|
||||||
def make_date do
|
def make_date do
|
||||||
DateTime.utc_now() |> DateTime.to_iso8601()
|
DateTime.utc_now() |> DateTime.to_iso8601()
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
|
||||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
|
|
||||||
def render("object.json", %{object: %Object{} = object}) do
|
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)
|
additional = Transmogrifier.prepare_object(object.data)
|
||||||
Map.merge(base, additional)
|
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})
|
def render("object.json", %{object: %Activity{data: %{"type" => activity_type}} = activity})
|
||||||
when activity_type in ["Create", "Listen"] do
|
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)
|
object = Object.normalize(activity, fetch: false)
|
||||||
|
|
||||||
additional =
|
additional =
|
||||||
|
@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("object.json", %{object: %Activity{} = activity}) do
|
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)
|
object_id = Object.normalize(activity, id_only: true)
|
||||||
|
|
||||||
additional =
|
additional =
|
||||||
|
|
|
@ -139,7 +139,8 @@ defmodule Pleroma.Web.ApiSpec do
|
||||||
"Search",
|
"Search",
|
||||||
"Status actions",
|
"Status actions",
|
||||||
"Media attachments",
|
"Media attachments",
|
||||||
"Bookmark folders"
|
"Bookmark folders",
|
||||||
|
"Tags"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
|
|
103
lib/pleroma/web/api_spec/operations/tag_operation.ex
Normal file
103
lib/pleroma/web/api_spec/operations/tag_operation.ex
Normal file
|
@ -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
|
|
@ -17,11 +17,22 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Tag do
|
||||||
type: :string,
|
type: :string,
|
||||||
format: :uri,
|
format: :uri,
|
||||||
description: "A link to the hashtag on the instance"
|
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: %{
|
example: %{
|
||||||
name: "cofe",
|
name: "cofe",
|
||||||
url: "https://lain.com/tag/cofe"
|
url: "https://lain.com/tag/cofe",
|
||||||
|
following: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,6 +11,9 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.CommonAPI.Utils
|
alias Pleroma.Web.CommonAPI.Utils
|
||||||
|
|
||||||
|
import Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode,
|
||||||
|
only: [good_locale_code?: 1]
|
||||||
|
|
||||||
import Pleroma.Web.Gettext
|
import Pleroma.Web.Gettext
|
||||||
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||||
|
|
||||||
|
@ -38,6 +41,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
||||||
cc: [],
|
cc: [],
|
||||||
context: nil,
|
context: nil,
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
|
language: nil,
|
||||||
object: nil,
|
object: nil,
|
||||||
preview?: false,
|
preview?: false,
|
||||||
changes: %{}
|
changes: %{}
|
||||||
|
@ -64,6 +68,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
||||||
|> content()
|
|> content()
|
||||||
|> with_valid(&to_and_cc/1)
|
|> with_valid(&to_and_cc/1)
|
||||||
|> with_valid(&context/1)
|
|> with_valid(&context/1)
|
||||||
|
|> with_valid(&language/1)
|
||||||
|> sensitive()
|
|> sensitive()
|
||||||
|> with_valid(&object/1)
|
|> with_valid(&object/1)
|
||||||
|> preview?()
|
|> preview?()
|
||||||
|
@ -249,6 +254,16 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
||||||
%__MODULE__{draft | sensitive: sensitive}
|
%__MODULE__{draft | sensitive: sensitive}
|
||||||
end
|
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
|
defp object(draft) do
|
||||||
emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
|
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])
|
"mediaType" => Utils.get_content_type(draft.params[:content_type])
|
||||||
})
|
})
|
||||||
|> Map.put("generator", draft.params[:generator])
|
|> Map.put("generator", draft.params[:generator])
|
||||||
|
|> Map.put("language", draft.language)
|
||||||
|
|
||||||
%__MODULE__{draft | object: object}
|
%__MODULE__{draft | object: object}
|
||||||
end
|
end
|
||||||
|
|
77
lib/pleroma/web/mastodon_api/controllers/tag_controller.ex
Normal file
77
lib/pleroma/web/mastodon_api/controllers/tag_controller.ex
Normal file
|
@ -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
|
|
@ -40,6 +40,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
||||||
|
|
||||||
# GET /api/v1/timelines/home
|
# GET /api/v1/timelines/home
|
||||||
def home(%{assigns: %{user: user}} = conn, params) do
|
def home(%{assigns: %{user: user}} = conn, params) do
|
||||||
|
followed_hashtags =
|
||||||
|
user
|
||||||
|
|> User.followed_hashtags()
|
||||||
|
|> Enum.map(& &1.id)
|
||||||
|
|
||||||
params =
|
params =
|
||||||
params
|
params
|
||||||
|> Map.put(:type, ["Create", "Announce"])
|
|> Map.put(:type, ["Create", "Announce"])
|
||||||
|
@ -49,6 +54,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
||||||
|> Map.put(:announce_filtering_user, user)
|
|> Map.put(:announce_filtering_user, user)
|
||||||
|> Map.put(:user, user)
|
|> Map.put(:user, user)
|
||||||
|> Map.put(:local_only, params[:local])
|
|> Map.put(:local_only, params[:local])
|
||||||
|
|> Map.put(:followed_hashtags, followed_hashtags)
|
||||||
|> Map.delete(:local)
|
|> Map.delete(:local)
|
||||||
|
|
||||||
activities =
|
activities =
|
||||||
|
|
|
@ -227,7 +227,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
||||||
mentions: mentions,
|
mentions: mentions,
|
||||||
tags: reblogged[:tags] || [],
|
tags: reblogged[:tags] || [],
|
||||||
application: build_application(object.data["generator"]),
|
application: build_application(object.data["generator"]),
|
||||||
language: nil,
|
language: get_language(object),
|
||||||
emojis: [],
|
emojis: [],
|
||||||
pleroma: %{
|
pleroma: %{
|
||||||
local: activity.local,
|
local: activity.local,
|
||||||
|
@ -445,7 +445,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
||||||
mentions: mentions,
|
mentions: mentions,
|
||||||
tags: build_tags(tags),
|
tags: build_tags(tags),
|
||||||
application: build_application(object.data["generator"]),
|
application: build_application(object.data["generator"]),
|
||||||
language: nil,
|
language: get_language(object),
|
||||||
emojis: build_emojis(object.data["emoji"]),
|
emojis: build_emojis(object.data["emoji"]),
|
||||||
pleroma: %{
|
pleroma: %{
|
||||||
local: activity.local,
|
local: activity.local,
|
||||||
|
@ -829,6 +829,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
||||||
Utils.get_content_type(nil)
|
Utils.get_content_type(nil)
|
||||||
end
|
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
|
defp proxied_url(url, page_url_data) do
|
||||||
if is_binary(url) do
|
if is_binary(url) do
|
||||||
build_image_url(URI.parse(url), page_url_data) |> MediaProxy.url()
|
build_image_url(URI.parse(url), page_url_data) |> MediaProxy.url()
|
||||||
|
|
25
lib/pleroma/web/mastodon_api/views/tag_view.ex
Normal file
25
lib/pleroma/web/mastodon_api/views/tag_view.ex
Normal file
|
@ -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
|
|
@ -755,6 +755,11 @@ defmodule Pleroma.Web.Router do
|
||||||
|
|
||||||
get("/announcements", AnnouncementController, :index)
|
get("/announcements", AnnouncementController, :index)
|
||||||
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
|
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
|
end
|
||||||
|
|
||||||
scope "/api/v1", Pleroma.Web.MastodonAPI do
|
scope "/api/v1", Pleroma.Web.MastodonAPI do
|
||||||
|
|
|
@ -19,6 +19,7 @@ defmodule Pleroma.Web.Streamer do
|
||||||
alias Pleroma.Web.OAuth.Token
|
alias Pleroma.Web.OAuth.Token
|
||||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||||
alias Pleroma.Web.StreamerView
|
alias Pleroma.Web.StreamerView
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
@registry Pleroma.Web.StreamerRegistry
|
@registry Pleroma.Web.StreamerRegistry
|
||||||
|
|
||||||
|
@ -305,7 +306,17 @@ defmodule Pleroma.Web.Streamer do
|
||||||
User.get_recipients_from_activity(item)
|
User.get_recipients_from_activity(item)
|
||||||
|> Enum.map(fn %{id: id} -> "user:#{id}" end)
|
|> 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)
|
push_to_socket(topic, item)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.AddUserFollowsHashtag do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:user_follows_hashtag) do
|
||||||
|
add(:hashtag_id, references(:hashtags))
|
||||||
|
add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
|
||||||
|
end
|
||||||
|
|
||||||
|
create(unique_index(:user_follows_hashtag, [:user_id, :hashtag_id]))
|
||||||
|
|
||||||
|
create_if_not_exists(index(:user_follows_hashtag, [:hashtag_id]))
|
||||||
|
end
|
||||||
|
end
|
|
@ -412,6 +412,7 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
||||||
["schema_migrations"],
|
["schema_migrations"],
|
||||||
["thread_mutes"],
|
["thread_mutes"],
|
||||||
["user_follows_hashtag"],
|
["user_follows_hashtag"],
|
||||||
|
# ["user_frontend_setting_profiles"], # not in pleroma
|
||||||
["user_invite_tokens"],
|
["user_invite_tokens"],
|
||||||
["user_notes"],
|
["user_notes"],
|
||||||
["user_relationships"],
|
["user_relationships"],
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
# 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.ContentLanguageMapTest do
|
||||||
|
use Pleroma.DataCase, async: true
|
||||||
|
|
||||||
|
alias Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMap
|
||||||
|
|
||||||
|
test "it validates" do
|
||||||
|
data = %{
|
||||||
|
"en-US" => "mew mew",
|
||||||
|
"en-GB" => "meow meow"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:ok, ^data} = ContentLanguageMap.cast(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it validates empty strings" do
|
||||||
|
data = %{
|
||||||
|
"en-US" => "mew mew",
|
||||||
|
"en-GB" => ""
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:ok, ^data} = ContentLanguageMap.cast(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it ignores non-strings within the map" do
|
||||||
|
data = %{
|
||||||
|
"en-US" => "mew mew",
|
||||||
|
"en-GB" => 123
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:ok, validated_data} = ContentLanguageMap.cast(data)
|
||||||
|
|
||||||
|
assert validated_data == %{"en-US" => "mew mew"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it ignores bad locale codes" do
|
||||||
|
data = %{
|
||||||
|
"en-US" => "mew mew",
|
||||||
|
"en_GB" => "meow meow",
|
||||||
|
"en<<#@!$#!@%!GB" => "meow meow"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:ok, validated_data} = ContentLanguageMap.cast(data)
|
||||||
|
|
||||||
|
assert validated_data == %{"en-US" => "mew mew"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it complains with non-map data" do
|
||||||
|
assert :error = ContentLanguageMap.cast("mew")
|
||||||
|
assert :error = ContentLanguageMap.cast(["mew"])
|
||||||
|
assert :error = ContentLanguageMap.cast([%{"en-US" => "mew"}])
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
# 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.LanguageCodeTest do
|
||||||
|
use Pleroma.DataCase, async: true
|
||||||
|
|
||||||
|
alias Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode
|
||||||
|
|
||||||
|
test "it accepts language code" do
|
||||||
|
text = "pl"
|
||||||
|
assert {:ok, ^text} = LanguageCode.cast(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it accepts language code with region" do
|
||||||
|
text = "pl-PL"
|
||||||
|
assert {:ok, ^text} = LanguageCode.cast(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "errors for invalid language code" do
|
||||||
|
assert {:error, :invalid_language} = LanguageCode.cast("ru_RU")
|
||||||
|
assert {:error, :invalid_language} = LanguageCode.cast(" ")
|
||||||
|
assert {:error, :invalid_language} = LanguageCode.cast("en-US\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "errors for non-text" do
|
||||||
|
assert :error == LanguageCode.cast(42)
|
||||||
|
end
|
||||||
|
end
|
|
@ -2919,4 +2919,74 @@ defmodule Pleroma.UserTest do
|
||||||
|
|
||||||
assert [%{"verified_at" => ^verified_at}] = user.fields
|
assert [%{"verified_at" => ^verified_at}] = user.fields
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "follow_hashtag/2" do
|
||||||
|
test "should follow a hashtag" do
|
||||||
|
user = insert(:user)
|
||||||
|
hashtag = insert(:hashtag)
|
||||||
|
|
||||||
|
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||||
|
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
|
||||||
|
assert user.followed_hashtags |> Enum.count() == 1
|
||||||
|
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not follow a hashtag twice" do
|
||||||
|
user = insert(:user)
|
||||||
|
hashtag = insert(:hashtag)
|
||||||
|
|
||||||
|
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||||
|
|
||||||
|
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||||
|
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
|
||||||
|
assert user.followed_hashtags |> Enum.count() == 1
|
||||||
|
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can follow multiple hashtags" do
|
||||||
|
user = insert(:user)
|
||||||
|
hashtag = insert(:hashtag)
|
||||||
|
other_hashtag = insert(:hashtag)
|
||||||
|
|
||||||
|
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||||
|
assert {:ok, _} = user |> User.follow_hashtag(other_hashtag)
|
||||||
|
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
|
||||||
|
assert user.followed_hashtags |> Enum.count() == 2
|
||||||
|
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||||
|
assert other_hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "unfollow_hashtag/2" do
|
||||||
|
test "should unfollow a hashtag" do
|
||||||
|
user = insert(:user)
|
||||||
|
hashtag = insert(:hashtag)
|
||||||
|
|
||||||
|
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||||
|
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
|
||||||
|
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
|
||||||
|
assert user.followed_hashtags |> Enum.count() == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not error when trying to unfollow a hashtag twice" do
|
||||||
|
user = insert(:user)
|
||||||
|
hashtag = insert(:hashtag)
|
||||||
|
|
||||||
|
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||||
|
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
|
||||||
|
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
|
||||||
|
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
|
||||||
|
assert user.followed_hashtags |> Enum.count() == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -867,6 +867,33 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "fetch activities for followed hashtags" do
|
||||||
|
test "it should return public activities that reference a given hashtag" do
|
||||||
|
hashtag = insert(:hashtag, name: "tenshi")
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, normally_visible} =
|
||||||
|
CommonAPI.post(other_user, %{status: "hello :)", visibility: "public"})
|
||||||
|
|
||||||
|
{:ok, public} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "public"})
|
||||||
|
{:ok, _unrelated} = CommonAPI.post(user, %{status: "dai #tensh", visibility: "public"})
|
||||||
|
{:ok, unlisted} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "unlisted"})
|
||||||
|
{:ok, _private} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "private"})
|
||||||
|
|
||||||
|
activities =
|
||||||
|
ActivityPub.fetch_activities([other_user.follower_address], %{
|
||||||
|
followed_hashtags: [hashtag.id]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert length(activities) == 3
|
||||||
|
normal_id = normally_visible.id
|
||||||
|
public_id = public.id
|
||||||
|
unlisted_id = unlisted.id
|
||||||
|
assert [%{id: ^normal_id}, %{id: ^public_id}, %{id: ^unlisted_id}] = activities
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "fetch activities in context" do
|
describe "fetch activities in context" do
|
||||||
test "retrieves activities that have a given context" do
|
test "retrieves activities that have a given context" do
|
||||||
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
|
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
|
||||||
|
|
|
@ -187,4 +187,71 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
|
||||||
name: "RE: https://server.example/objects/123"
|
name: "RE: https://server.example/objects/123"
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "Note language" do
|
||||||
|
test "it detects language from JSON-LD context" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
note_activity = %{
|
||||||
|
"@context" => ["https://www.w3.org/ns/activitystreams", %{"@language" => "pl"}],
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => [],
|
||||||
|
"type" => "Create",
|
||||||
|
"object" => %{
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => [],
|
||||||
|
"id" => Utils.generate_object_id(),
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => "Szczęść Boże",
|
||||||
|
"attributedTo" => user.ap_id
|
||||||
|
},
|
||||||
|
"actor" => user.ap_id
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, _create_activity, meta} = ObjectValidator.validate(note_activity, [])
|
||||||
|
|
||||||
|
assert meta[:object_data]["language"] == "pl"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it detects language from contentMap" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
note = %{
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => [],
|
||||||
|
"id" => Utils.generate_object_id(),
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => "Szczęść Boże",
|
||||||
|
"contentMap" => %{
|
||||||
|
"de" => "Gott segne",
|
||||||
|
"pl" => "Szczęść Boże"
|
||||||
|
},
|
||||||
|
"attributedTo" => user.ap_id
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, object} = ArticleNotePageValidator.cast_and_apply(note)
|
||||||
|
|
||||||
|
assert object.language == "pl"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it adds contentMap if language is specified" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
note = %{
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => [],
|
||||||
|
"id" => Utils.generate_object_id(),
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => "тест",
|
||||||
|
"language" => "uk",
|
||||||
|
"attributedTo" => user.ap_id
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, object} = ArticleNotePageValidator.cast_and_apply(note)
|
||||||
|
|
||||||
|
assert object.contentMap == %{
|
||||||
|
"uk" => "тест"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -219,6 +219,36 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
|
||||||
"<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
|
"<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it only uses contentMap if content is not present" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
message = %{
|
||||||
|
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => [],
|
||||||
|
"type" => "Create",
|
||||||
|
"object" => %{
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => [],
|
||||||
|
"id" => Utils.generate_object_id(),
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => "Hi",
|
||||||
|
"contentMap" => %{
|
||||||
|
"de" => "Hallo",
|
||||||
|
"uk" => "Привіт"
|
||||||
|
},
|
||||||
|
"inReplyTo" => nil,
|
||||||
|
"attributedTo" => user.ap_id
|
||||||
|
},
|
||||||
|
"actor" => user.ap_id
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(message)
|
||||||
|
object = Object.normalize(data["object"], fetch: false)
|
||||||
|
|
||||||
|
assert object.data["content"] == "Hi"
|
||||||
|
end
|
||||||
|
|
||||||
test "it works for incoming notices with a nil contentMap (firefish)" do
|
test "it works for incoming notices with a nil contentMap (firefish)" do
|
||||||
data =
|
data =
|
||||||
File.read!("test/fixtures/mastodon-post-activity-contentmap.json")
|
File.read!("test/fixtures/mastodon-post-activity-contentmap.json")
|
||||||
|
|
|
@ -384,6 +384,24 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
|
||||||
assert modified["object"]["quoteUrl"] == quote_id
|
assert modified["object"]["quoteUrl"] == quote_id
|
||||||
assert modified["object"]["quoteUri"] == quote_id
|
assert modified["object"]["quoteUri"] == quote_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it adds language of the object to its json-ld context" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
|
||||||
|
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.object.data)
|
||||||
|
|
||||||
|
assert [_, _, %{"@language" => "pl"}] = modified["@context"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it adds language of the object to Create activity json-ld context" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
|
||||||
|
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
|
||||||
|
|
||||||
|
assert [_, _, %{"@language" => "pl"}] = modified["@context"]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "actor rewriting" do
|
describe "actor rewriting" do
|
||||||
|
@ -621,5 +639,14 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
|
||||||
processed = Transmogrifier.prepare_object(original)
|
processed = Transmogrifier.prepare_object(original)
|
||||||
assert processed["formerRepresentations"] == original["formerRepresentations"]
|
assert processed["formerRepresentations"] == original["formerRepresentations"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it uses contentMap to specify post language" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
|
||||||
|
object = Transmogrifier.prepare_object(activity.object.data)
|
||||||
|
|
||||||
|
assert %{"contentMap" => %{"pl" => "Cześć"}} = object
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -173,16 +173,30 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "make_json_ld_header/0" do
|
describe "make_json_ld_header/1" do
|
||||||
assert Utils.make_json_ld_header() == %{
|
test "makes jsonld header" do
|
||||||
"@context" => [
|
assert Utils.make_json_ld_header() == %{
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"@context" => [
|
||||||
"http://localhost:4001/schemas/litepub-0.1.jsonld",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
%{
|
"http://localhost:4001/schemas/litepub-0.1.jsonld",
|
||||||
"@language" => "und"
|
%{
|
||||||
}
|
"@language" => "und"
|
||||||
]
|
}
|
||||||
}
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes language if specified" do
|
||||||
|
assert Utils.make_json_ld_header(%{"language" => "pl"}) == %{
|
||||||
|
"@context" => [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"http://localhost:4001/schemas/litepub-0.1.jsonld",
|
||||||
|
%{
|
||||||
|
"@language" => "pl"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "get_existing_votes" do
|
describe "get_existing_votes" do
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.TagControllerTest do
|
||||||
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
import Tesla.Mock
|
||||||
|
|
||||||
|
alias Pleroma.User
|
||||||
|
|
||||||
|
setup do
|
||||||
|
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /api/v1/tags/:id" do
|
||||||
|
test "returns 200 with tag" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read"])
|
||||||
|
|
||||||
|
tag = insert(:hashtag, name: "jubjub")
|
||||||
|
{:ok, _user} = User.follow_hashtag(user, tag)
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/tags/jubjub")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
"name" => "jubjub",
|
||||||
|
"url" => "http://localhost:4001/tags/jubjub",
|
||||||
|
"history" => [],
|
||||||
|
"following" => true
|
||||||
|
} = response
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 404 with unknown tag" do
|
||||||
|
%{conn: conn} = oauth_access(["read"])
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/tags/jubjub")
|
||||||
|
|> json_response_and_validate_schema(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "POST /api/v1/tags/:id/follow" do
|
||||||
|
test "should follow a hashtag" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["write:follows"])
|
||||||
|
hashtag = insert(:hashtag, name: "jubjub")
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/tags/jubjub/follow")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert response["following"] == true
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
assert User.following_hashtag?(user, hashtag)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should 404 if hashtag doesn't exist" do
|
||||||
|
%{conn: conn} = oauth_access(["write:follows"])
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/tags/rubrub/follow")
|
||||||
|
|> json_response_and_validate_schema(404)
|
||||||
|
|
||||||
|
assert response["error"] == "Hashtag not found"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "POST /api/v1/tags/:id/unfollow" do
|
||||||
|
test "should unfollow a hashtag" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["write:follows"])
|
||||||
|
hashtag = insert(:hashtag, name: "jubjub")
|
||||||
|
{:ok, user} = User.follow_hashtag(user, hashtag)
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/tags/jubjub/unfollow")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert response["following"] == false
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
refute User.following_hashtag?(user, hashtag)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should 404 if hashtag doesn't exist" do
|
||||||
|
%{conn: conn} = oauth_access(["write:follows"])
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/tags/rubrub/unfollow")
|
||||||
|
|> json_response_and_validate_schema(404)
|
||||||
|
|
||||||
|
assert response["error"] == "Hashtag not found"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /api/v1/followed_tags" do
|
||||||
|
test "should list followed tags" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:follows"])
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/followed_tags")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert Enum.empty?(response)
|
||||||
|
|
||||||
|
hashtag = insert(:hashtag, name: "jubjub")
|
||||||
|
{:ok, _user} = User.follow_hashtag(user, hashtag)
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/followed_tags")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert [%{"name" => "jubjub"}] = response
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should include a link header to paginate" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:follows"])
|
||||||
|
|
||||||
|
for i <- 1..21 do
|
||||||
|
hashtag = insert(:hashtag, name: "jubjub#{i}}")
|
||||||
|
{:ok, _user} = User.follow_hashtag(user, hashtag)
|
||||||
|
end
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/followed_tags")
|
||||||
|
|
||||||
|
json = json_response_and_validate_schema(response, 200)
|
||||||
|
assert Enum.count(json) == 20
|
||||||
|
assert [link_header] = get_resp_header(response, "link")
|
||||||
|
assert link_header =~ "rel=\"next\""
|
||||||
|
next_link = extract_next_link_header(link_header)
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> get(next_link)
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert Enum.count(response) == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should refuse access without read:follows scope" do
|
||||||
|
%{conn: conn} = oauth_access(["write"])
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/followed_tags")
|
||||||
|
|> json_response_and_validate_schema(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_next_link_header(header) do
|
||||||
|
[_, next_link] = Regex.run(~r{<(?<next_link>.*)>; rel="next"}, header)
|
||||||
|
next_link
|
||||||
|
end
|
||||||
|
end
|
|
@ -951,6 +951,26 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
||||||
assert status.edited_at
|
assert status.edited_at
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it shows post language" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, post} = CommonAPI.post(user, %{status: "Szczęść Boże", language: "pl"})
|
||||||
|
|
||||||
|
status = StatusView.render("show.json", activity: post)
|
||||||
|
|
||||||
|
assert status.language == "pl"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't show post language if it's 'und'" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, post} = CommonAPI.post(user, %{status: "sdifjogijodfg", language: "und"})
|
||||||
|
|
||||||
|
status = StatusView.render("show.json", activity: post)
|
||||||
|
|
||||||
|
assert status.language == nil
|
||||||
|
end
|
||||||
|
|
||||||
test "with a source object" do
|
test "with a source object" do
|
||||||
note =
|
note =
|
||||||
insert(:note,
|
insert(:note,
|
||||||
|
|
|
@ -558,6 +558,36 @@ defmodule Pleroma.Web.StreamerTest do
|
||||||
assert_receive {:render_with_user, _, "status_update.json", ^create, _}
|
assert_receive {:render_with_user, _, "status_update.json", ^create, _}
|
||||||
refute Streamer.filtered_by_user?(user, edited)
|
refute Streamer.filtered_by_user?(user, edited)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it streams posts containing followed hashtags on the 'user' stream", %{
|
||||||
|
user: user,
|
||||||
|
token: oauth_token
|
||||||
|
} do
|
||||||
|
hashtag = insert(:hashtag, %{name: "tenshi"})
|
||||||
|
other_user = insert(:user)
|
||||||
|
{:ok, user} = User.follow_hashtag(user, hashtag)
|
||||||
|
|
||||||
|
Streamer.get_topic_and_add_socket("user", user, oauth_token)
|
||||||
|
{:ok, activity} = CommonAPI.post(other_user, %{status: "hey #tenshi"})
|
||||||
|
|
||||||
|
assert_receive {:render_with_user, _, "update.json", ^activity, _}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not stream private posts containing followed hashtags on the 'user' stream", %{
|
||||||
|
user: user,
|
||||||
|
token: oauth_token
|
||||||
|
} do
|
||||||
|
hashtag = insert(:hashtag, %{name: "tenshi"})
|
||||||
|
other_user = insert(:user)
|
||||||
|
{:ok, user} = User.follow_hashtag(user, hashtag)
|
||||||
|
|
||||||
|
Streamer.get_topic_and_add_socket("user", user, oauth_token)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(other_user, %{status: "hey #tenshi", visibility: "private"})
|
||||||
|
|
||||||
|
refute_receive {:render_with_user, _, "update.json", ^activity, _}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "public streams" do
|
describe "public streams" do
|
||||||
|
|
|
@ -668,4 +668,11 @@ defmodule Pleroma.Factory do
|
||||||
|> Map.merge(params)
|
|> Map.merge(params)
|
||||||
|> Pleroma.Announcement.add_rendered_properties()
|
|> Pleroma.Announcement.add_rendered_properties()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hashtag_factory(params \\ %{}) do
|
||||||
|
%Pleroma.Hashtag{
|
||||||
|
name: "test #{sequence(:hashtag_name, & &1)}"
|
||||||
|
}
|
||||||
|
|> Map.merge(params)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue