],
finmoji_enabled: true,
mrf_transparency: true,
- autofollowed_nicknames: []
+ autofollowed_nicknames: [],
+ max_pinned_statuses: 1
config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because
config :pleroma, Pleroma.User,
restricted_nicknames: [
- "about",
+ ".well-known",
"~",
- "main",
- "users",
- "settings",
- "objects",
+ "about",
"activities",
- "web",
- "registration",
- "friend-requests",
- "pleroma",
"api",
- "tag",
+ "auth",
+ "dev",
+ "friend-requests",
+ "inbox",
+ "internal",
+ "main",
+ "media",
+ "nodeinfo",
"notice",
- "status",
- "user-search",
- "ostatus_subscribe",
"oauth",
+ "objects",
+ "ostatus_subscribe",
+ "pleroma",
+ "proxy",
"push",
+ "registration",
"relay",
- "inbox",
- ".well-known",
- "nodeinfo",
- "auth",
- "proxy",
- "dev",
- "internal",
- "media"
+ "settings",
+ "status",
+ "tag",
+ "user-search",
+ "users",
+ "web"
]
config :pleroma, Pleroma.Web.Federator, max_jobs: 50
## Pleroma.Mailer
* `adapter`: one of the mail adapters listed in [Swoosh readme](https://github.com/swoosh/swoosh#adapters), or `Swoosh.Adapters.Local` for in-memory mailbox.
-* `api_key` / `password` and / or other adapter-specific settings, per the above documentation.
+* `api_key` / `password` and / or other adapter-specific settings, per the above documentation.
An example for Sendgrid adapter:
* `always_show_subject_input`: When set to false, auto-hide the subject field when it's empty.
* `extended_nickname_format`: Set to `true` to use extended local nicknames format (allows underscores/dashes). This will break federation with
older software for theses nicknames.
+* `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature.
* `autofollowed_nicknames`: Set to nicknames of (local) users that every new user should automatically follow.
## :logger
)
|> Repo.all()
- autofollowed_users
- |> Enum.reduce({:ok, user}, fn other_user, {:ok, user} ->
- follow(user, other_user)
- end)
+ follow_all(user, autofollowed_users)
end
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
end
end
+ @doc "A mass follow for local users. Ignores blocks and has no side effects"
+ @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
+ def follow_all(follower, followeds) do
+ following =
+ (follower.following ++ Enum.map(followeds, fn %{follower_address: fa} -> fa end))
+ |> Enum.uniq()
+
+ {:ok, follower} =
+ follower
+ |> follow_changeset(%{following: following})
+ |> update_and_set_cache
+
+ Enum.each(followeds, fn followed ->
+ update_follower_count(followed)
+ end)
+
+ {:ok, follower}
+ end
+
def follow(%User{} = follower, %User{info: info} = followed) do
user_config = Application.get_env(:pleroma, :user)
deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
end
end
- def get_followers_query(%User{id: id, follower_address: follower_address}) do
+ def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
from(
u in User,
where: fragment("? <@ ?", ^[follower_address], u.following),
)
end
- def get_followers(user) do
- q = get_followers_query(user)
+ def get_followers_query(user, page) do
+ from(
+ u in get_followers_query(user, nil),
+ limit: 20,
+ offset: ^((page - 1) * 20)
+ )
+ end
+
+ def get_followers_query(user), do: get_followers_query(user, nil)
+
+ def get_followers(user, page \\ nil) do
+ q = get_followers_query(user, page)
{:ok, Repo.all(q)}
end
- def get_friends_query(%User{id: id, following: following}) do
+ def get_friends_query(%User{id: id, following: following}, nil) do
from(
u in User,
where: u.follower_address in ^following,
)
end
- def get_friends(user) do
- q = get_friends_query(user)
+ def get_friends_query(user, page) do
+ from(
+ u in get_friends_query(user, nil),
+ limit: 20,
+ offset: ^((page - 1) * 20)
+ )
+ end
+
+ def get_friends_query(user), do: get_friends_query(user, nil)
+
+ def get_friends(user, page \\ nil) do
+ q = get_friends_query(user, page)
{:ok, Repo.all(q)}
end
field(:hub, :string, default: nil)
field(:salmon, :string, default: nil)
field(:hide_network, :boolean, default: false)
+ field(:pinned_activities, {:array, :integer}, default: [])
# Found in the wild
# ap_id -> Where is this used?
:is_admin
])
end
+
+ def add_pinnned_activity(info, %Pleroma.Activity{id: id}) do
+ if id not in info.pinned_activities do
+ max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
+ params = %{pinned_activities: info.pinned_activities ++ [id]}
+
+ info
+ |> cast(params, [:pinned_activities])
+ |> validate_length(:pinned_activities,
+ max: max_pinned_statuses,
+ message: "You have already pinned the maximum number of statuses"
+ )
+ else
+ change(info)
+ end
+ end
+
+ def remove_pinnned_activity(info, %Pleroma.Activity{id: id}) do
+ params = %{pinned_activities: List.delete(info.pinned_activities, id)}
+
+ cast(info, params, [:pinned_activities])
+ end
end
@valid_visibilities ~w[direct unlisted public private]
- defp restrict_visibility(query, %{visibility: "direct"}) do
- public = "https://www.w3.org/ns/activitystreams#Public"
+ defp restrict_visibility(query, %{visibility: visibility})
+ when visibility in @valid_visibilities do
+ query =
+ from(
+ a in query,
+ where:
+ fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
+ )
- from(
- activity in query,
- join: sender in User,
- on: sender.ap_id == activity.actor,
- # Are non-direct statuses with no to/cc possible?
- where:
- fragment(
- "not (? && ?)",
- [^public, sender.follower_address],
- activity.recipients
- )
- )
+ Ecto.Adapters.SQL.to_sql(:all, Repo, query)
+
+ query
end
defp restrict_visibility(_query, %{visibility: visibility})
|> Map.put("type", ["Create", "Announce"])
|> Map.put("actor_id", user.ap_id)
|> Map.put("whole_db", true)
+ |> Map.put("pinned_activity_ids", user.info.pinned_activities)
recipients =
if reading_user do
)
end
+ defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) do
+ from(activity in query, where: activity.id in ^ids)
+ end
+
+ defp restrict_pinned(query, _), do: query
+
def fetch_activities_query(recipients, opts \\ %{}) do
base_query =
from(
|> restrict_visibility(opts)
|> restrict_replies(opts)
|> restrict_reblogs(opts)
+ |> restrict_pinned(opts)
end
def fetch_activities(recipients, opts \\ %{}) do
object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
})
end
+
+ def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
+ with %Activity{
+ actor: ^user_ap_id,
+ data: %{
+ "type" => "Create",
+ "object" => %{
+ "to" => object_to,
+ "type" => "Note"
+ }
+ }
+ } = activity <- get_by_id_or_ap_id(id_or_ap_id),
+ true <- Enum.member?(object_to, "https://www.w3.org/ns/activitystreams#Public"),
+ %{valid?: true} = info_changeset <-
+ Pleroma.User.Info.add_pinnned_activity(user.info, activity),
+ changeset <-
+ Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
+ {:ok, _user} <- User.update_and_set_cache(changeset) do
+ {:ok, activity}
+ else
+ %{errors: [pinned_activities: {err, _}]} ->
+ {:error, err}
+
+ _ ->
+ {:error, "Could not pin"}
+ end
+ end
+
+ def unpin(id_or_ap_id, user) do
+ with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
+ %{valid?: true} = info_changeset <-
+ Pleroma.User.Info.remove_pinnned_activity(user.info, activity),
+ changeset <-
+ Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
+ {:ok, _user} <- User.update_and_set_cache(changeset) do
+ {:ok, activity}
+ else
+ %{errors: [pinned_activities: {err, _}]} ->
+ {:error, err}
+
+ _ ->
+ {:error, "Could not unpin"}
+ end
+ end
end
def format_input(text, mentions, _tags, "text/html") do
text
|> Formatter.html_escape("text/html")
- |> String.replace(~r/\r?\n/, "<br>")
|> (&{[], &1}).()
|> Formatter.add_user_links(mentions)
|> Formatter.finalize()
|> Formatter.mentions_escape(mentions)
|> Earmark.as_html!()
|> Formatter.html_escape("text/html")
- |> String.replace(~r/\r?\n/, "")
|> (&{[], &1}).()
|> Formatter.add_user_links(mentions)
|> Formatter.add_hashtag_links(tags)
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- Repo.get(User, params["id"]) do
- # Since Pleroma has no "pinned" posts feature, we'll just set an empty list here
- activities =
- if params["pinned"] == "true" do
- []
- else
- ActivityPub.fetch_user_activities(user, reading_user, params)
- end
+ activities = ActivityPub.fetch_user_activities(user, reading_user, params)
conn
|> add_link_headers(:user_statuses, activities, params["id"])
end
end
+ def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
+ conn
+ |> put_view(StatusView)
+ |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ else
+ {:error, reason} ->
+ conn
+ |> put_resp_content_type("application/json")
+ |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
+ end
+ end
+
+ def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
+ conn
+ |> put_view(StatusView)
+ |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ end
+ end
+
def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params)
reblogged: false,
favourited: false,
muted: false,
+ pinned: pinned?(activity, user),
sensitive: false,
spoiler_text: "",
visibility: "public",
reblogged: present?(repeated),
favourited: present?(favorited),
muted: false,
+ pinned: pinned?(activity, user),
sensitive: sensitive,
spoiler_text: object["summary"] || "",
visibility: get_visibility(object),
defp present?(nil), do: false
defp present?(false), do: false
defp present?(_), do: true
+
+ defp pinned?(%Activity{id: id}, %User{info: %{pinned_activities: pinned_activities}}),
+ do: id in pinned_activities
end
defmodule Pleroma.Web.RichMedia.Parser do
- @parsers [Pleroma.Web.RichMedia.Parsers.OGP]
+ @parsers [Pleroma.Web.RichMedia.Parsers.OGP, Pleroma.Web.RichMedia.Parsers.TwitterCard]
if Mix.env() == :test do
def parse(url), do: parse_url(url)
else
def parse(url),
- do: {:commit, Cachex.fetch!(:rich_media_cache, url, fn _ -> parse_url(url) end)}
+ do: Cachex.fetch!(:rich_media_cache, url, fn _ -> parse_url(url) end)
end
defp parse_url(url) do
--- /dev/null
+defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do
+ def parse(html, data, prefix, error_message, key_name, value_name \\ "content") do
+ with elements = [_ | _] <- get_elements(html, key_name, prefix),
+ meta_data =
+ Enum.reduce(elements, data, fn el, acc ->
+ attributes = normalize_attributes(el, prefix, key_name, value_name)
+
+ Map.merge(acc, attributes)
+ end) do
+ {:ok, meta_data}
+ else
+ _e -> {:error, error_message}
+ end
+ end
+
+ defp get_elements(html, key_name, prefix) do
+ html |> Floki.find("meta[#{key_name}^='#{prefix}:']")
+ end
+
+ defp normalize_attributes(html_node, prefix, key_name, value_name) do
+ {_tag, attributes, _children} = html_node
+
+ data =
+ Enum.into(attributes, %{}, fn {name, value} ->
+ {name, String.trim_leading(value, "#{prefix}:")}
+ end)
+
+ %{String.to_atom(data[key_name]) => data[value_name]}
+ end
+end
defmodule Pleroma.Web.RichMedia.Parsers.OGP do
def parse(html, data) do
- with elements = [_ | _] <- get_elements(html),
- ogp_data =
- Enum.reduce(elements, data, fn el, acc ->
- attributes = normalize_attributes(el)
-
- Map.merge(acc, attributes)
- end) do
- {:ok, ogp_data}
- else
- _e -> {:error, "No OGP metadata found"}
- end
- end
-
- defp get_elements(html) do
- html |> Floki.find("meta[property^='og:']")
- end
-
- defp normalize_attributes(html_node) do
- {_tag, attributes, _children} = html_node
-
- data =
- Enum.into(attributes, %{}, fn {name, value} ->
- {name, String.trim_leading(value, "og:")}
- end)
-
- %{String.to_atom(data["property"]) => data["content"]}
+ Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse(
+ html,
+ data,
+ "og",
+ "No OGP metadata found",
+ "property"
+ )
end
end
--- /dev/null
+defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do
+ def parse(html, data) do
+ Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse(
+ html,
+ data,
+ "twitter",
+ "No twitter card metadata found",
+ "name"
+ )
+ end
+end
post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)
post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
+ post("/statuses/:id/pin", MastodonAPIController, :pin_status)
+ post("/statuses/:id/unpin", MastodonAPIController, :unpin_status)
post("/notifications/clear", MastodonAPIController, :clear_notifications)
post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
+ post("/statuses/pin/:id", TwitterAPI.Controller, :pin)
+ post("/statuses/unpin/:id", TwitterAPI.Controller, :unpin)
+
get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
announcement_count = object["announcement_count"] || 0
favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])
+ pinned = activity.id in user.info.pinned_activities
mentions = opts[:mentioned] || []
reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor)
+ summary = HTML.strip_tags(object["summary"])
+
%{
"id" => activity.id,
"uri" => activity.data["object"]["id"],
"repeat_num" => announcement_count,
"favorited" => to_boolean(favorited),
"repeated" => to_boolean(repeated),
+ "pinned" => pinned,
"external_url" => object["external_url"] || object["id"],
"tags" => tags,
"activity_type" => "post",
"possibly_sensitive" => possibly_sensitive,
"visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object),
- "summary" => HTML.strip_tags(object["summary"]) |> Formatter.emojify(object["emoji"])
+ "summary" => summary,
+ "summary_html" => summary |> Formatter.emojify(object["emoji"])
}
end
end
end
+ def pin(%User{} = user, ap_id_or_id) do
+ CommonAPI.pin(ap_id_or_id, user)
+ end
+
+ def unpin(%User{} = user, ap_id_or_id) do
+ CommonAPI.unpin(ap_id_or_id, user)
+ end
+
def fav(%User{} = user, ap_id_or_id) do
with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
end
end
+ def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
+ {:ok, activity} <- TwitterAPI.pin(user, id) do
+ conn
+ |> put_view(ActivityView)
+ |> render("activity.json", %{activity: activity, for: user})
+ else
+ {:error, message} -> bad_request_reply(conn, message)
+ err -> err
+ end
+ end
+
+ def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
+ {:ok, activity} <- TwitterAPI.unpin(user, id) do
+ conn
+ |> put_view(ActivityView)
+ |> render("activity.json", %{activity: activity, for: user})
+ else
+ {:error, message} -> bad_request_reply(conn, message)
+ err -> err
+ end
+ end
+
def register(conn, params) do
with {:ok, user} <- TwitterAPI.register_user(params) do
conn
end
def followers(%{assigns: %{user: for_user}} = conn, params) do
+ {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
+
with {:ok, user} <- TwitterAPI.get_user(for_user, params),
- {:ok, followers} <- User.get_followers(user) do
+ {:ok, followers} <- User.get_followers(user, page) do
followers =
cond do
for_user && user.id == for_user.id -> followers
end
def friends(%{assigns: %{user: for_user}} = conn, params) do
+ {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
+
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
- {:ok, friends} <- User.get_friends(user) do
+ {:ok, friends} <- User.get_friends(user, page) do
friends =
cond do
for_user && user.id == for_user.id -> friends
announcement_count = object["announcement_count"] || 0
favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])
+ pinned = activity.id in user.info.pinned_activities
attentions =
activity.recipients
reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor)
+ summary = HTML.strip_tags(summary)
+
%{
"id" => activity.id,
"uri" => activity.data["object"]["id"],
"repeat_num" => announcement_count,
"favorited" => !!favorited,
"repeated" => !!repeated,
+ "pinned" => pinned,
"external_url" => object["external_url"] || object["id"],
"tags" => tags,
"activity_type" => "post",
"possibly_sensitive" => possibly_sensitive,
"visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object),
- "summary" => HTML.strip_tags(summary) |> Formatter.emojify(object["emoji"])
+ "summary" => summary,
+ "summary_html" => summary |> Formatter.emojify(object["emoji"])
}
end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.AddVisibilityFunction do
+ use Ecto.Migration
+ @disable_ddl_transaction true
+
+ def up do
+ definition = """
+ create or replace function activity_visibility(actor varchar, recipients varchar[], data jsonb) returns varchar as $$
+ DECLARE
+ fa varchar;
+ public varchar := 'https://www.w3.org/ns/activitystreams#Public';
+ BEGIN
+ SELECT COALESCE(users.follower_address, '') into fa from users where users.ap_id = actor;
+
+ IF data->'to' ? public THEN
+ RETURN 'public';
+ ELSIF data->'cc' ? public THEN
+ RETURN 'unlisted';
+ ELSIF ARRAY[fa] && recipients THEN
+ RETURN 'private';
+ ELSIF not(ARRAY[fa, public] && recipients) THEN
+ RETURN 'direct';
+ ELSE
+ RETURN 'unknown';
+ END IF;
+ END;
+ $$ LANGUAGE plpgsql IMMUTABLE;
+ """
+
+ execute(definition)
+
+ create(
+ index(:activities, ["activity_visibility(actor, recipients, data)"],
+ name: :activities_visibility_index,
+ concurrently: true
+ )
+ )
+ end
+
+ def down do
+ drop(
+ index(:activities, ["activity_visibility(actor, recipients, data)"],
+ name: :activities_visibility_index
+ )
+ )
+
+ execute("drop function activity_visibility(actor varchar, recipients varchar[], data jsonb)")
+ end
+end
--- /dev/null
+<meta name="twitter:card" content="summary" />
+<meta name="twitter:site" content="@flickr" />
+<meta name="twitter:title" content="Small Island Developing States Photo Submission" />
+<meta name="twitter:description" content="View the album on Flickr." />
+<meta name="twitter:image" content="https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg" />
assert expected_followers_collection == User.ap_followers(user)
end
+ test "follow_all follows mutliple users" do
+ user = insert(:user)
+ followed_one = insert(:user)
+ followed_two = insert(:user)
+
+ {:ok, user} = User.follow_all(user, [followed_one, followed_two])
+
+ assert User.following?(user, followed_one)
+ assert User.following?(user, followed_two)
+ end
+
test "follow takes a user and another user" do
user = insert(:user)
followed = insert(:user)
:ok
end
+ describe "fetching restricted by visibility" do
+ test "it restricts by the appropriate visibility" do
+ user = insert(:user)
+
+ {:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
+
+ {:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
+
+ {:ok, unlisted_activity} =
+ CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
+
+ {:ok, private_activity} =
+ CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
+
+ activities =
+ ActivityPub.fetch_activities([], %{:visibility => "direct", "actor_id" => user.ap_id})
+
+ assert activities == [direct_activity]
+
+ activities =
+ ActivityPub.fetch_activities([], %{:visibility => "unlisted", "actor_id" => user.ap_id})
+
+ assert activities == [unlisted_activity]
+
+ activities =
+ ActivityPub.fetch_activities([], %{:visibility => "private", "actor_id" => user.ap_id})
+
+ assert activities == [private_activity]
+
+ activities =
+ ActivityPub.fetch_activities([], %{:visibility => "public", "actor_id" => user.ap_id})
+
+ assert activities == [public_activity]
+ end
+ end
+
describe "building a user from his ap id" do
test "it returns a user" do
user_id = "http://mastodon.example.org/users/admin"
assert object
end
+ test "returned pinned statuses" do
+ Pleroma.Config.put([:instance, :max_pinned_statuses], 3)
+ user = insert(:user)
+
+ {:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"})
+ {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
+ {:ok, activity_three} = CommonAPI.post(user, %{"status" => "HI!!!"})
+
+ CommonAPI.pin(activity_one.id, user)
+ user = refresh_record(user)
+
+ CommonAPI.pin(activity_two.id, user)
+ user = refresh_record(user)
+
+ CommonAPI.pin(activity_three.id, user)
+ user = refresh_record(user)
+
+ activities = ActivityPub.fetch_user_activities(user, nil, %{"pinned" => "true"})
+
+ assert 3 = length(activities)
+ end
+
def data_uri do
File.read!("test/fixtures/avatar_data_uri")
end
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.Test do
{:error, _} = CommonAPI.favorite(activity.id, user)
end
end
+
+ describe "pinned statuses" do
+ setup do
+ Pleroma.Config.put([:instance, :max_pinned_statuses], 1)
+
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
+
+ [user: user, activity: activity]
+ end
+
+ test "pin status", %{user: user, activity: activity} do
+ assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
+ end
+
+ test "only self-authored can be pinned", %{activity: activity} do
+ user = insert(:user)
+
+ assert {:error, "Could not pin"} = CommonAPI.pin(activity.id, user)
+ end
+
+ test "max pinned statuses", %{user: user, activity: activity_one} do
+ {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
+
+ assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user)
+
+ user = refresh_record(user)
+
+ assert {:error, "You have already pinned the maximum number of statuses"} =
+ CommonAPI.pin(activity_two.id, user)
+ end
+
+ test "unpin status", %{user: user, activity: activity} do
+ {:ok, activity} = CommonAPI.pin(activity.id, user)
+
+ assert {:ok, ^activity} = CommonAPI.unpin(activity.id, user)
+ end
+ end
end
assert expected == Utils.emoji_from_profile(user)
end
+
+ describe "format_input/4" do
+ test "works for bare text/plain" do
+ text = "hello world!"
+ expected = "hello world!"
+
+ output = Utils.format_input(text, [], [], "text/plain")
+
+ assert output == expected
+
+ text = "hello world!\n\nsecond paragraph!"
+ expected = "hello world!<br><br>second paragraph!"
+
+ output = Utils.format_input(text, [], [], "text/plain")
+
+ assert output == expected
+ end
+
+ test "works for bare text/html" do
+ text = "<p>hello world!</p>"
+ expected = "<p>hello world!</p>"
+
+ output = Utils.format_input(text, [], [], "text/html")
+
+ assert output == expected
+
+ text = "<p>hello world!</p>\n\n<p>second paragraph</p>"
+ expected = "<p>hello world!</p>\n\n<p>second paragraph</p>"
+
+ output = Utils.format_input(text, [], [], "text/html")
+
+ assert output == expected
+ end
+
+ test "works for bare text/markdown" do
+ text = "**hello world**"
+ expected = "<p><strong>hello world</strong></p>\n"
+
+ output = Utils.format_input(text, [], [], "text/markdown")
+
+ assert output == expected
+
+ text = "**hello world**\n\n*another paragraph*"
+ expected = "<p><strong>hello world</strong></p>\n<p><em>another paragraph</em></p>\n"
+
+ output = Utils.format_input(text, [], [], "text/markdown")
+
+ assert output == expected
+ end
+ end
end
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
user = User.get_cached_by_ap_id(user.ap_id)
assert user.info.settings == %{"programming" => "socks"}
end
+
+ describe "pinned statuses" do
+ setup do
+ Pleroma.Config.put([:instance, :max_pinned_statuses], 1)
+
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
+
+ [user: user, activity: activity]
+ end
+
+ test "returns pinned statuses", %{conn: conn, user: user, activity: activity} do
+ {:ok, _} = CommonAPI.pin(activity.id, user)
+
+ result =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
+ |> json_response(200)
+
+ id_str = to_string(activity.id)
+
+ assert [%{"id" => ^id_str, "pinned" => true}] = result
+ end
+
+ test "pin status", %{conn: conn, user: user, activity: activity} do
+ id_str = to_string(activity.id)
+
+ assert %{"id" => ^id_str, "pinned" => true} =
+ conn
+ |> assign(:user, user)
+ |> post("/api/v1/statuses/#{activity.id}/pin")
+ |> json_response(200)
+
+ assert [%{"id" => ^id_str, "pinned" => true}] =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
+ |> json_response(200)
+ end
+
+ test "unpin status", %{conn: conn, user: user, activity: activity} do
+ {:ok, _} = CommonAPI.pin(activity.id, user)
+
+ id_str = to_string(activity.id)
+ user = refresh_record(user)
+
+ assert %{"id" => ^id_str, "pinned" => false} =
+ conn
+ |> assign(:user, user)
+ |> post("/api/v1/statuses/#{activity.id}/unpin")
+ |> json_response(200)
+
+ assert [] =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
+ |> json_response(200)
+ end
+
+ test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
+ {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
+
+ id_str_one = to_string(activity_one.id)
+
+ assert %{"id" => ^id_str_one, "pinned" => true} =
+ conn
+ |> assign(:user, user)
+ |> post("/api/v1/statuses/#{id_str_one}/pin")
+ |> json_response(200)
+
+ user = refresh_record(user)
+
+ assert %{"error" => "You have already pinned the maximum number of statuses"} =
+ conn
+ |> assign(:user, user)
+ |> post("/api/v1/statuses/#{activity_two.id}/pin")
+ |> json_response(400)
+ end
+ end
end
reblogged: false,
favourited: false,
muted: false,
+ pinned: false,
sensitive: false,
spoiler_text: note.data["object"]["summary"],
visibility: "public",
} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}
+ %{
+ method: :get,
+ url: "http://example.com/twitter-card"
+ } ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}
+
%{method: :get, url: "http://example.com/empty"} ->
%Tesla.Env{status: 200, body: "hello"}
end)
url: "http://www.imdb.com/title/tt0117500/"
}}
end
+
+ test "parses twitter card" do
+ assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/twitter-card") ==
+ {:ok,
+ %{
+ card: "summary",
+ site: "@flickr",
+ image: "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg",
+ title: "Small Island Developing States Photo Submission",
+ description: "View the album on Flickr."
+ }}
+ end
end
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenterTest do
"published" => date,
"type" => "Note",
"content" => content_html,
- "summary" => "2hu",
+ "summary" => "2hu :2hu:",
"inReplyToStatusId" => 213_123,
"attachment" => [
object
}
expected_html =
- "<p>2hu</p>alert('YAY')Some <img height=\"32px\" width=\"32px\" alt=\"2hu\" title=\"2hu\" src=\"corndog.png\" /> content mentioning <a href=\"#{
+ "<p>2hu <img height=\"32px\" width=\"32px\" alt=\"2hu\" title=\"2hu\" src=\"corndog.png\" /></p>alert('YAY')Some <img height=\"32px\" width=\"32px\" alt=\"2hu\" title=\"2hu\" src=\"corndog.png\" /> content mentioning <a href=\"#{
mentioned_user.ap_id
}\">@shp</a>"
"user" => UserView.render("show.json", %{user: user, for: follower}),
"is_local" => false,
"statusnet_html" => expected_html,
- "text" => "2hu" <> content,
+ "text" => "2hu :2hu:" <> content,
"is_post_verb" => true,
"created_at" => "Tue May 24 13:26:08 +0000 2016",
"in_reply_to_status_id" => 213_123,
"repeat_num" => 3,
"favorited" => false,
"repeated" => false,
+ "pinned" => false,
"external_url" => "some url",
"tags" => ["nsfw", "content", "mentioning"],
"activity_type" => "post",
"possibly_sensitive" => true,
"uri" => activity.data["object"]["id"],
"visibility" => "direct",
- "summary" => "2hu"
+ "summary" => "2hu :2hu:",
+ "summary_html" =>
+ "2hu <img height=\"32px\" width=\"32px\" alt=\"2hu\" title=\"2hu\" src=\"corndog.png\" />"
}
assert ActivityRepresenter.to_map(activity, %{
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.ControllerTest do
assert Enum.sort(expected) == Enum.sort(result)
end
+ test "it returns 20 followers per page", %{conn: conn} do
+ user = insert(:user)
+ followers = insert_list(21, :user)
+
+ Enum.each(followers, fn follower ->
+ User.follow(follower, user)
+ end)
+
+ res_conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/statuses/followers")
+
+ result = json_response(res_conn, 200)
+ assert length(result) == 20
+
+ res_conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/statuses/followers?page=2")
+
+ result = json_response(res_conn, 200)
+ assert length(result) == 1
+ end
+
test "it returns a given user's followers with user_id", %{conn: conn} do
user = insert(:user)
follower_one = insert(:user)
assert Enum.sort(expected) == Enum.sort(result)
end
+ test "it returns 20 friends per page", %{conn: conn} do
+ user = insert(:user)
+ followeds = insert_list(21, :user)
+
+ {:ok, user} =
+ Enum.reduce(followeds, {:ok, user}, fn followed, {:ok, user} ->
+ User.follow(user, followed)
+ end)
+
+ res_conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/statuses/friends")
+
+ result = json_response(res_conn, 200)
+ assert length(result) == 20
+
+ res_conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/statuses/friends", %{page: 2})
+
+ result = json_response(res_conn, 200)
+ assert length(result) == 1
+ end
+
test "it returns a given user's friends with user_id", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
assert object.data["name"] == description
end
end
+
+ describe "POST /api/statuses/user_timeline.json?user_id=:user_id&pinned=true" do
+ test "it returns a list of pinned statuses", %{conn: conn} do
+ Pleroma.Config.put([:instance, :max_pinned_statuses], 1)
+
+ user = insert(:user, %{name: "egor"})
+ {:ok, %{id: activity_id}} = CommonAPI.post(user, %{"status" => "HI!!!"})
+ {:ok, _} = CommonAPI.pin(activity_id, user)
+
+ resp =
+ conn
+ |> get("/api/statuses/user_timeline.json", %{user_id: user.id, pinned: true})
+ |> json_response(200)
+
+ assert length(resp) == 1
+ assert [%{"id" => ^activity_id, "pinned" => true}] = resp
+ end
+ end
+
+ describe "POST /api/statuses/pin/:id" do
+ setup do
+ Pleroma.Config.put([:instance, :max_pinned_statuses], 1)
+ [user: insert(:user)]
+ end
+
+ test "without valid credentials", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ conn = post(conn, "/api/statuses/pin/#{note_activity.id}.json")
+ assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
+ end
+
+ test "with credentials", %{conn: conn, user: user} do
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "test!"})
+
+ request_path = "/api/statuses/pin/#{activity.id}.json"
+
+ response =
+ conn
+ |> with_credentials(user.nickname, "test")
+ |> post(request_path)
+
+ user = refresh_record(user)
+
+ assert json_response(response, 200) == ActivityRepresenter.to_map(activity, %{user: user})
+ end
+ end
+
+ describe "POST /api/statuses/unpin/:id" do
+ setup do
+ Pleroma.Config.put([:instance, :max_pinned_statuses], 1)
+ [user: insert(:user)]
+ end
+
+ test "without valid credentials", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ conn = post(conn, "/api/statuses/unpin/#{note_activity.id}.json")
+ assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
+ end
+
+ test "with credentials", %{conn: conn, user: user} do
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "test!"})
+ {:ok, activity} = CommonAPI.pin(activity.id, user)
+
+ request_path = "/api/statuses/unpin/#{activity.id}.json"
+
+ response =
+ conn
+ |> with_credentials(user.nickname, "test")
+ |> post(request_path)
+
+ user = refresh_record(user)
+
+ assert json_response(response, 200) == ActivityRepresenter.to_map(activity, %{user: user})
+ end
+ end
end
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do
result = ActivityView.render("activity.json", activity: activity)
- expected =
+ expected = ":woollysocks: meow"
+
+ expected_html =
"<img height=\"32px\" width=\"32px\" alt=\"woollysocks\" title=\"woollysocks\" src=\"http://localhost:4001/finmoji/128px/woollysocks-128.png\" /> meow"
assert result["summary"] == expected
+ assert result["summary_html"] == expected_html
end
test "a create activity with a summary containing invalid HTML" do
expected = "meow"
assert result["summary"] == expected
+ assert result["summary_html"] == expected
end
test "a create activity with a note" do
"possibly_sensitive" => false,
"repeat_num" => 0,
"repeated" => false,
+ "pinned" => false,
"statusnet_conversation_id" => convo_id,
"summary" => "",
+ "summary_html" => "",
"statusnet_html" =>
"Hey <span><a data-user=\"#{other_user.id}\" href=\"#{other_user.ap_id}\">@<span>shp</span></a></span>!",
"tags" => [],