mirror of
https://git.pleroma.social/pleroma/pleroma.git
synced 2025-04-08 12:04:10 +00:00
Merge remote-tracking branch 'origin/develop' into translate-posts
This commit is contained in:
commit
013c60e13a
52 changed files with 955 additions and 31 deletions
1
changelog.d/301-small-image-redirect.change
Normal file
1
changelog.d/301-small-image-redirect.change
Normal file
|
@ -0,0 +1 @@
|
|||
Performance: Use 301 (permanent) redirect instead of 302 (temporary) when redirecting small images in media proxy. This allows browsers to cache the redirect response.
|
1
changelog.d/actor-published-date.add
Normal file
1
changelog.d/actor-published-date.add
Normal file
|
@ -0,0 +1 @@
|
|||
Include "published" in actor view
|
1
changelog.d/backup-links.add
Normal file
1
changelog.d/backup-links.add
Normal file
|
@ -0,0 +1 @@
|
|||
Link to exported outbox/followers/following collections in backup actor.json
|
0
changelog.d/description-update-suggestions.skip
Normal file
0
changelog.d/description-update-suggestions.skip
Normal file
1
changelog.d/fix-mastodon-edits.fix
Normal file
1
changelog.d/fix-mastodon-edits.fix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix Mastodon incoming edits with inlined "likes"
|
0
changelog.d/fix-wrong-config-section.skip
Normal file
0
changelog.d/fix-wrong-config-section.skip
Normal file
1
changelog.d/follow-hashtags.add
Normal file
1
changelog.d/follow-hashtags.add
Normal file
|
@ -0,0 +1 @@
|
|||
Hashtag following
|
1
changelog.d/incoming-scrobbles.fix
Normal file
1
changelog.d/incoming-scrobbles.fix
Normal file
|
@ -0,0 +1 @@
|
|||
Allow incoming "Listen" activities
|
1
changelog.d/rich-media-ignore-host.fix
Normal file
1
changelog.d/rich-media-ignore-host.fix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix missing check for domain presence in rich media ignore_host configuration
|
1
changelog.d/vips-blurhash.fix
Normal file
1
changelog.d/vips-blurhash.fix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix blurhash generation crashes
|
|
@ -3302,8 +3302,7 @@ config :pleroma, :config_description, [
|
|||
suggestions: [
|
||||
Pleroma.Web.Preload.Providers.Instance,
|
||||
Pleroma.Web.Preload.Providers.User,
|
||||
Pleroma.Web.Preload.Providers.Timelines,
|
||||
Pleroma.Web.Preload.Providers.StatusNet
|
||||
Pleroma.Web.Preload.Providers.Timelines
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -98,7 +98,7 @@ To add configuration to your config file, you can copy it from the base config.
|
|||
* `moderator_privileges`: A list of privileges a moderator has (e.g. delete messages, manage reports...)
|
||||
* Possible values are the same as for `admin_privileges`
|
||||
|
||||
## :database
|
||||
## :features
|
||||
* `improved_hashtag_timeline`: Setting to force toggle / force disable improved hashtags timeline. `:enabled` forces hashtags to be fetched from `hashtags` table for hashtags timeline. `:disabled` forces object-embedded hashtags to be used (slower). Keep it `:auto` for automatic behaviour (it is auto-set to `:enabled` [unless overridden] when HashtagsTableMigrator completes).
|
||||
|
||||
## Background migrations
|
||||
|
|
|
@ -37,6 +37,7 @@ defmodule Pleroma.Constants do
|
|||
"updated",
|
||||
"emoji",
|
||||
"content",
|
||||
"contentMap",
|
||||
"summary",
|
||||
"sensitive",
|
||||
"attachment",
|
||||
|
@ -102,7 +103,8 @@ defmodule Pleroma.Constants do
|
|||
"Announce",
|
||||
"Undo",
|
||||
"Flag",
|
||||
"EmojiReact"
|
||||
"EmojiReact",
|
||||
"Listen"
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -90,9 +90,13 @@ defmodule Pleroma.Upload.Filter.AnalyzeMetadata do
|
|||
{:ok, rgb} =
|
||||
if Image.has_alpha?(resized_image) do
|
||||
# remove alpha channel
|
||||
resized_image
|
||||
|> Operation.extract_band!(0, n: 3)
|
||||
|> Image.write_to_binary()
|
||||
case Operation.extract_band(resized_image, 0, n: 3) do
|
||||
{:ok, data} ->
|
||||
Image.write_to_binary(data)
|
||||
|
||||
_ ->
|
||||
Image.write_to_binary(resized_image)
|
||||
end
|
||||
else
|
||||
Image.write_to_binary(resized_image)
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -246,7 +246,13 @@ defmodule Pleroma.User.Backup do
|
|||
defp actor(dir, user) do
|
||||
with {:ok, json} <-
|
||||
UserView.render("user.json", %{user: user})
|
||||
|> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"})
|
||||
|> Map.merge(%{
|
||||
"bookmarks" => "bookmarks.json",
|
||||
"likes" => "likes.json",
|
||||
"outbox" => "outbox.json",
|
||||
"followers" => "followers.json",
|
||||
"following" => "following.json"
|
||||
})
|
||||
|> Jason.encode() do
|
||||
File.write(Path.join(dir, "actor.json"), json)
|
||||
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
|
||||
|
||||
# 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)
|
||||
|
|
|
@ -85,6 +85,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
|||
|> fix_replies()
|
||||
|> fix_attachments()
|
||||
|> CommonFixes.fix_quote_url()
|
||||
|> CommonFixes.fix_likes()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> Transmogrifier.fix_content_map()
|
||||
|> CommonFixes.maybe_add_language()
|
||||
|
|
|
@ -100,6 +100,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator do
|
|||
|> CommonFixes.fix_actor()
|
||||
|> CommonFixes.fix_object_defaults()
|
||||
|> CommonFixes.fix_quote_url()
|
||||
|> CommonFixes.fix_likes()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> fix_url()
|
||||
|> fix_content()
|
||||
|
|
|
@ -119,6 +119,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
|||
|
||||
def fix_quote_url(data), do: data
|
||||
|
||||
# On Mastodon, `"likes"` attribute includes an inlined `Collection` with `totalItems`,
|
||||
# not a list of users.
|
||||
# https://github.com/mastodon/mastodon/pull/32007
|
||||
def fix_likes(%{"likes" => %{}} = data), do: Map.drop(data, ["likes"])
|
||||
|
||||
def fix_likes(data), do: data
|
||||
|
||||
# https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
|
||||
def object_link_tag?(%{
|
||||
"type" => "Link",
|
||||
|
|
|
@ -48,6 +48,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
|
|||
data
|
||||
|> CommonFixes.fix_actor()
|
||||
|> CommonFixes.fix_object_defaults()
|
||||
|> CommonFixes.fix_likes()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> CommonFixes.maybe_add_language()
|
||||
|> CommonFixes.maybe_add_content_map()
|
||||
|
|
|
@ -64,6 +64,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
|
|||
|> CommonFixes.fix_actor()
|
||||
|> CommonFixes.fix_object_defaults()
|
||||
|> CommonFixes.fix_quote_url()
|
||||
|> CommonFixes.fix_likes()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> fix_closed()
|
||||
end
|
||||
|
|
|
@ -16,6 +16,7 @@ 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
|
||||
|
@ -167,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
|
||||
|
@ -720,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
|
||||
|
|
|
@ -127,7 +127,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"capabilities" => capabilities,
|
||||
"alsoKnownAs" => user.also_known_as,
|
||||
"vcard:bday" => birthday,
|
||||
"webfinger" => "acct:#{User.full_nickname(user)}"
|
||||
"webfinger" => "acct:#{User.full_nickname(user)}",
|
||||
"published" => Pleroma.Web.CommonAPI.Utils.to_masto_date(user.inserted_at)
|
||||
}
|
||||
|> Map.merge(
|
||||
maybe_make_image(
|
||||
|
|
|
@ -139,7 +139,8 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
"Search",
|
||||
"Status actions",
|
||||
"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,
|
||||
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
|
||||
|
|
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
|
||||
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 =
|
||||
|
|
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
|
|
@ -71,11 +71,15 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
|
|||
drop_static_param_and_redirect(conn)
|
||||
|
||||
content_type == "image/gif" ->
|
||||
redirect(conn, external: media_proxy_url)
|
||||
conn
|
||||
|> put_status(301)
|
||||
|> redirect(external: media_proxy_url)
|
||||
|
||||
min_content_length_for_preview() > 0 and content_length > 0 and
|
||||
content_length < min_content_length_for_preview() ->
|
||||
redirect(conn, external: media_proxy_url)
|
||||
conn
|
||||
|> put_status(301)
|
||||
|> redirect(external: media_proxy_url)
|
||||
|
||||
true ->
|
||||
handle_preview(content_type, conn, media_proxy_url)
|
||||
|
|
|
@ -54,7 +54,10 @@ defmodule Pleroma.Web.RichMedia.Card do
|
|||
|
||||
@spec get_by_url(String.t() | nil) :: t() | nil | :error
|
||||
def get_by_url(url) when is_binary(url) do
|
||||
if @config_impl.get([:rich_media, :enabled]) do
|
||||
host = URI.parse(url).host
|
||||
|
||||
with true <- @config_impl.get([:rich_media, :enabled]),
|
||||
true <- host not in @config_impl.get([:rich_media, :ignore_hosts], []) do
|
||||
url_hash = url_to_hash(url)
|
||||
|
||||
@cachex.fetch!(:rich_media_cache, url_hash, fn _ ->
|
||||
|
@ -69,7 +72,7 @@ defmodule Pleroma.Web.RichMedia.Card do
|
|||
end
|
||||
end)
|
||||
else
|
||||
:error
|
||||
false -> :error
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -77,7 +80,10 @@ defmodule Pleroma.Web.RichMedia.Card do
|
|||
|
||||
@spec get_or_backfill_by_url(String.t(), keyword()) :: t() | nil
|
||||
def get_or_backfill_by_url(url, opts \\ []) do
|
||||
if @config_impl.get([:rich_media, :enabled]) do
|
||||
host = URI.parse(url).host
|
||||
|
||||
with true <- @config_impl.get([:rich_media, :enabled]),
|
||||
true <- host not in @config_impl.get([:rich_media, :ignore_hosts], []) do
|
||||
case get_by_url(url) do
|
||||
%__MODULE__{} = card ->
|
||||
card
|
||||
|
@ -94,7 +100,7 @@ defmodule Pleroma.Web.RichMedia.Card do
|
|||
nil
|
||||
end
|
||||
else
|
||||
nil
|
||||
false -> nil
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -756,6 +756,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
BIN
test/fixtures/break_analyze.png
vendored
Normal file
BIN
test/fixtures/break_analyze.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 360 KiB |
90
test/fixtures/mastodon-update-with-likes.json
vendored
Normal file
90
test/fixtures/mastodon-update-with-likes.json
vendored
Normal file
|
@ -0,0 +1,90 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
{
|
||||
"atomUri": "ostatus:atomUri",
|
||||
"conversation": "ostatus:conversation",
|
||||
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
|
||||
"ostatus": "http://ostatus.org#",
|
||||
"sensitive": "as:sensitive",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"votersCount": "toot:votersCount"
|
||||
},
|
||||
"https://w3id.org/security/v1"
|
||||
],
|
||||
"actor": "https://pol.social/users/mkljczk",
|
||||
"cc": ["https://www.w3.org/ns/activitystreams#Public",
|
||||
"https://pol.social/users/aemstuz", "https://gts.mkljczk.pl/users/mkljczk",
|
||||
"https://pl.fediverse.pl/users/mkljczk",
|
||||
"https://fedi.kutno.pl/users/mkljczk"],
|
||||
"id": "https://pol.social/users/mkljczk/statuses/113907871635572263#updates/1738096776",
|
||||
"object": {
|
||||
"atomUri": "https://pol.social/users/mkljczk/statuses/113907871635572263",
|
||||
"attachment": [],
|
||||
"attributedTo": "https://pol.social/users/mkljczk",
|
||||
"cc": ["https://www.w3.org/ns/activitystreams#Public",
|
||||
"https://pol.social/users/aemstuz", "https://gts.mkljczk.pl/users/mkljczk",
|
||||
"https://pl.fediverse.pl/users/mkljczk",
|
||||
"https://fedi.kutno.pl/users/mkljczk"],
|
||||
"content": "<p>test</p>",
|
||||
"contentMap": {
|
||||
"pl": "<p>test</p>"
|
||||
},
|
||||
"conversation": "https://fedi.kutno.pl/contexts/43c14c70-d3fb-42b4-a36d-4eacfab9695a",
|
||||
"id": "https://pol.social/users/mkljczk/statuses/113907871635572263",
|
||||
"inReplyTo": "https://pol.social/users/aemstuz/statuses/113907854282654767",
|
||||
"inReplyToAtomUri": "https://pol.social/users/aemstuz/statuses/113907854282654767",
|
||||
"likes": {
|
||||
"id": "https://pol.social/users/mkljczk/statuses/113907871635572263/likes",
|
||||
"totalItems": 1,
|
||||
"type": "Collection"
|
||||
},
|
||||
"published": "2025-01-28T20:29:45Z",
|
||||
"replies": {
|
||||
"first": {
|
||||
"items": [],
|
||||
"next": "https://pol.social/users/mkljczk/statuses/113907871635572263/replies?only_other_accounts=true&page=true",
|
||||
"partOf": "https://pol.social/users/mkljczk/statuses/113907871635572263/replies",
|
||||
"type": "CollectionPage"
|
||||
},
|
||||
"id": "https://pol.social/users/mkljczk/statuses/113907871635572263/replies",
|
||||
"type": "Collection"
|
||||
},
|
||||
"sensitive": false,
|
||||
"shares": {
|
||||
"id": "https://pol.social/users/mkljczk/statuses/113907871635572263/shares",
|
||||
"totalItems": 0,
|
||||
"type": "Collection"
|
||||
},
|
||||
"summary": null,
|
||||
"tag": [
|
||||
{
|
||||
"href": "https://pol.social/users/aemstuz",
|
||||
"name": "@aemstuz",
|
||||
"type": "Mention"
|
||||
},
|
||||
{
|
||||
"href": "https://gts.mkljczk.pl/users/mkljczk",
|
||||
"name": "@mkljczk@gts.mkljczk.pl",
|
||||
"type": "Mention"
|
||||
},
|
||||
{
|
||||
"href": "https://pl.fediverse.pl/users/mkljczk",
|
||||
"name": "@mkljczk@fediverse.pl",
|
||||
"type": "Mention"
|
||||
},
|
||||
{
|
||||
"href": "https://fedi.kutno.pl/users/mkljczk",
|
||||
"name": "@mkljczk@fedi.kutno.pl",
|
||||
"type": "Mention"
|
||||
}
|
||||
],
|
||||
"to": ["https://pol.social/users/mkljczk/followers"],
|
||||
"type": "Note",
|
||||
"updated": "2025-01-28T20:39:36Z",
|
||||
"url": "https://pol.social/@mkljczk/113907871635572263"
|
||||
},
|
||||
"published": "2025-01-28T20:39:36Z",
|
||||
"to": ["https://pol.social/users/mkljczk/followers"],
|
||||
"type": "Update"
|
||||
}
|
|
@ -411,7 +411,7 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
|||
["scheduled_activities"],
|
||||
["schema_migrations"],
|
||||
["thread_mutes"],
|
||||
# ["user_follows_hashtag"], # not in pleroma
|
||||
["user_follows_hashtag"],
|
||||
# ["user_frontend_setting_profiles"], # not in pleroma
|
||||
["user_invite_tokens"],
|
||||
["user_notes"],
|
||||
|
|
|
@ -34,6 +34,20 @@ defmodule Pleroma.Upload.Filter.AnalyzeMetadataTest do
|
|||
assert meta.blurhash == "eXJi-E:SwCEm5rCmn$+YWYn+15K#5A$xxCi{SiV]s*W:Efa#s.jE-T"
|
||||
end
|
||||
|
||||
test "it gets dimensions for grayscale images" do
|
||||
upload = %Pleroma.Upload{
|
||||
name: "break_analyze.png",
|
||||
content_type: "image/png",
|
||||
path: Path.absname("test/fixtures/break_analyze.png"),
|
||||
tempfile: Path.absname("test/fixtures/break_analyze.png")
|
||||
}
|
||||
|
||||
{:ok, :filtered, meta} = AnalyzeMetadata.filter(upload)
|
||||
|
||||
assert %{width: 1410, height: 2048} = meta
|
||||
assert is_nil(meta.blurhash)
|
||||
end
|
||||
|
||||
test "adds the dimensions for videos" do
|
||||
upload = %Pleroma.Upload{
|
||||
name: "coolvideo.mp4",
|
||||
|
|
|
@ -185,13 +185,13 @@ defmodule Pleroma.User.BackupTest do
|
|||
%{"@language" => "und"}
|
||||
],
|
||||
"bookmarks" => "bookmarks.json",
|
||||
"followers" => "http://cofe.io/users/cofe/followers",
|
||||
"following" => "http://cofe.io/users/cofe/following",
|
||||
"followers" => "followers.json",
|
||||
"following" => "following.json",
|
||||
"id" => "http://cofe.io/users/cofe",
|
||||
"inbox" => "http://cofe.io/users/cofe/inbox",
|
||||
"likes" => "likes.json",
|
||||
"name" => "Cofe",
|
||||
"outbox" => "http://cofe.io/users/cofe/outbox",
|
||||
"outbox" => "outbox.json",
|
||||
"preferredUsername" => "cofe",
|
||||
"publicKey" => %{
|
||||
"id" => "http://cofe.io/users/cofe#main-key",
|
||||
|
|
|
@ -2919,4 +2919,74 @@ defmodule Pleroma.UserTest do
|
|||
|
||||
assert [%{"verified_at" => ^verified_at}] = user.fields
|
||||
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
|
||||
|
|
|
@ -867,6 +867,33 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
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
|
||||
test "retrieves activities that have a given context" do
|
||||
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
|
||||
|
|
|
@ -128,6 +128,17 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
|
|||
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
|
||||
end
|
||||
|
||||
test "a Note with validated likes collection validates" do
|
||||
insert(:user, ap_id: "https://pol.social/users/mkljczk")
|
||||
|
||||
%{"object" => note} =
|
||||
"test/fixtures/mastodon-update-with-likes.json"
|
||||
|> File.read!()
|
||||
|> Jason.decode!()
|
||||
|
||||
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
|
||||
end
|
||||
|
||||
test "Fedibird quote post" do
|
||||
insert(:user, ap_id: "https://fedibird.com/users/noellabo")
|
||||
|
||||
|
|
|
@ -639,5 +639,14 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
|
|||
processed = Transmogrifier.prepare_object(original)
|
||||
assert processed["formerRepresentations"] == original["formerRepresentations"]
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -248,8 +248,8 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
|
|||
|
||||
response = get(conn, url)
|
||||
|
||||
assert response.status == 302
|
||||
assert redirected_to(response) == media_proxy_url
|
||||
assert response.status == 301
|
||||
assert redirected_to(response, 301) == media_proxy_url
|
||||
end
|
||||
|
||||
test "with `static` param and non-GIF image preview requested, " <>
|
||||
|
@ -290,8 +290,8 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
|
|||
|
||||
response = get(conn, url)
|
||||
|
||||
assert response.status == 302
|
||||
assert redirected_to(response) == media_proxy_url
|
||||
assert response.status == 301
|
||||
assert redirected_to(response, 301) == media_proxy_url
|
||||
end
|
||||
|
||||
test "thumbnails PNG images into PNG", %{
|
||||
|
@ -356,5 +356,32 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
|
|||
assert response.status == 302
|
||||
assert redirected_to(response) == media_proxy_url
|
||||
end
|
||||
|
||||
test "redirects to media proxy URI with 301 when image is too small for preview", %{
|
||||
conn: conn,
|
||||
url: url,
|
||||
media_proxy_url: media_proxy_url
|
||||
} do
|
||||
clear_config([:media_preview_proxy],
|
||||
enabled: true,
|
||||
min_content_length: 1000,
|
||||
image_quality: 85,
|
||||
thumbnail_max_width: 100,
|
||||
thumbnail_max_height: 100
|
||||
)
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{method: :head, url: ^media_proxy_url} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: "",
|
||||
headers: [{"content-type", "image/png"}, {"content-length", "500"}]
|
||||
}
|
||||
end)
|
||||
|
||||
response = get(conn, url)
|
||||
assert response.status == 301
|
||||
assert redirected_to(response, 301) == media_proxy_url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -83,4 +83,23 @@ defmodule Pleroma.Web.RichMedia.CardTest do
|
|||
Card.get_by_activity(activity)
|
||||
)
|
||||
end
|
||||
|
||||
test "refuses to crawl URL in activity from ignored host/domain" do
|
||||
clear_config([:rich_media, :ignore_hosts], ["example.com"])
|
||||
|
||||
user = insert(:user)
|
||||
|
||||
url = "https://example.com/ogp"
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "[test](#{url})",
|
||||
content_type: "text/markdown"
|
||||
})
|
||||
|
||||
refute_enqueued(
|
||||
worker: RichMediaWorker,
|
||||
args: %{"url" => url, "activity_id" => activity.id}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -558,6 +558,36 @@ defmodule Pleroma.Web.StreamerTest do
|
|||
assert_receive {:render_with_user, _, "status_update.json", ^create, _}
|
||||
refute Streamer.filtered_by_user?(user, edited)
|
||||
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
|
||||
|
||||
describe "public streams" do
|
||||
|
|
|
@ -668,4 +668,11 @@ defmodule Pleroma.Factory do
|
|||
|> Map.merge(params)
|
||||
|> Pleroma.Announcement.add_rendered_properties()
|
||||
end
|
||||
|
||||
def hashtag_factory(params \\ %{}) do
|
||||
%Pleroma.Hashtag{
|
||||
name: "test #{sequence(:hashtag_name, & &1)}"
|
||||
}
|
||||
|> Map.merge(params)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue