Mastodon List API See merge request pleroma/pleroma!138tags/v0.9.9
@@ -0,0 +1,87 @@ | |||
defmodule Pleroma.List do | |||
use Ecto.Schema | |||
import Ecto.{Changeset, Query} | |||
alias Pleroma.{User, Repo} | |||
schema "lists" do | |||
belongs_to(:user, Pleroma.User) | |||
field(:title, :string) | |||
field(:following, {:array, :string}, default: []) | |||
timestamps() | |||
end | |||
def title_changeset(list, attrs \\ %{}) do | |||
list | |||
|> cast(attrs, [:title]) | |||
|> validate_required([:title]) | |||
end | |||
def follow_changeset(list, attrs \\ %{}) do | |||
list | |||
|> cast(attrs, [:following]) | |||
|> validate_required([:following]) | |||
end | |||
def for_user(user, opts) do | |||
query = | |||
from( | |||
l in Pleroma.List, | |||
where: l.user_id == ^user.id, | |||
order_by: [desc: l.id], | |||
limit: 50 | |||
) | |||
Repo.all(query) | |||
end | |||
def get(id, %{id: user_id} = _user) do | |||
query = | |||
from( | |||
l in Pleroma.List, | |||
where: l.id == ^id, | |||
where: l.user_id == ^user_id | |||
) | |||
Repo.one(query) | |||
end | |||
def get_following(%Pleroma.List{following: following} = list) do | |||
q = | |||
from( | |||
u in User, | |||
where: u.follower_address in ^following | |||
) | |||
{:ok, Repo.all(q)} | |||
end | |||
def rename(%Pleroma.List{} = list, title) do | |||
list | |||
|> title_changeset(%{title: title}) | |||
|> Repo.update() | |||
end | |||
def create(title, %User{} = creator) do | |||
list = %Pleroma.List{user_id: creator.id, title: title} | |||
Repo.insert(list) | |||
end | |||
def follow(%Pleroma.List{following: following} = list, %User{} = followed) do | |||
update_follows(list, %{following: Enum.uniq([followed.follower_address | following])}) | |||
end | |||
def unfollow(%Pleroma.List{following: following} = list, %User{} = unfollowed) do | |||
update_follows(list, %{following: List.delete(following, unfollowed.follower_address)}) | |||
end | |||
def delete(%Pleroma.List{} = list) do | |||
Repo.delete(list) | |||
end | |||
def update_follows(%Pleroma.List{} = list, attrs) do | |||
list | |||
|> follow_changeset(attrs) | |||
|> Repo.update() | |||
end | |||
end |
@@ -2,7 +2,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
use Pleroma.Web, :controller | |||
alias Pleroma.{Repo, Activity, User, Notification, Stats} | |||
alias Pleroma.Web | |||
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView} | |||
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView} | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
alias Pleroma.Web.{CommonAPI, OStatus} | |||
alias Pleroma.Web.OAuth.{Authorization, Token, App} | |||
@@ -568,6 +568,102 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity}) | |||
end | |||
def get_lists(%{assigns: %{user: user}} = conn, opts) do | |||
lists = Pleroma.List.for_user(user, opts) | |||
res = ListView.render("lists.json", lists: lists) | |||
json(conn, res) | |||
end | |||
def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do | |||
with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do | |||
res = ListView.render("list.json", list: list) | |||
json(conn, res) | |||
else | |||
_e -> json(conn, "error") | |||
end | |||
end | |||
def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do | |||
with %Pleroma.List{} = list <- Pleroma.List.get(id, user), | |||
{:ok, _list} <- Pleroma.List.delete(list) do | |||
json(conn, %{}) | |||
else | |||
_e -> | |||
json(conn, "error") | |||
end | |||
end | |||
def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do | |||
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do | |||
res = ListView.render("list.json", list: list) | |||
json(conn, res) | |||
end | |||
end | |||
def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do | |||
accounts | |||
|> Enum.each(fn account_id -> | |||
with %Pleroma.List{} = list <- Pleroma.List.get(id, user), | |||
%User{} = followed <- Repo.get(User, account_id) do | |||
Pleroma.List.follow(list, followed) | |||
end | |||
end) | |||
json(conn, %{}) | |||
end | |||
def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do | |||
accounts | |||
|> Enum.each(fn account_id -> | |||
with %Pleroma.List{} = list <- Pleroma.List.get(id, user), | |||
%User{} = followed <- Repo.get(Pleroma.User, account_id) do | |||
Pleroma.List.unfollow(list, followed) | |||
end | |||
end) | |||
json(conn, %{}) | |||
end | |||
def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do | |||
with %Pleroma.List{} = list <- Pleroma.List.get(id, user), | |||
{:ok, users} = Pleroma.List.get_following(list) do | |||
render(conn, AccountView, "accounts.json", %{users: users, as: :user}) | |||
end | |||
end | |||
def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do | |||
with %Pleroma.List{} = list <- Pleroma.List.get(id, user), | |||
{:ok, list} <- Pleroma.List.rename(list, title) do | |||
res = ListView.render("list.json", list: list) | |||
json(conn, res) | |||
else | |||
_e -> | |||
json(conn, "error") | |||
end | |||
end | |||
def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do | |||
with %Pleroma.List{title: title, following: following} <- Pleroma.List.get(id, user) do | |||
params = | |||
params | |||
|> Map.put("type", "Create") | |||
|> Map.put("blocking_user", user) | |||
# adding title is a hack to not make empty lists function like a public timeline | |||
activities = | |||
ActivityPub.fetch_activities([title | following], params) | |||
|> Enum.reverse() | |||
conn | |||
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity}) | |||
else | |||
_e -> | |||
conn | |||
|> put_status(403) | |||
|> json(%{error: "Error."}) | |||
end | |||
end | |||
def index(%{assigns: %{user: user}} = conn, _params) do | |||
token = | |||
conn | |||
@@ -0,0 +1,15 @@ | |||
defmodule Pleroma.Web.MastodonAPI.ListView do | |||
use Pleroma.Web, :view | |||
alias Pleroma.Web.MastodonAPI.ListView | |||
def render("lists.json", %{lists: lists} = opts) do | |||
render_many(lists, ListView, "list.json", opts) | |||
end | |||
def render("list.json", %{list: list}) do | |||
%{ | |||
id: to_string(list.id), | |||
title: list.title | |||
} | |||
end | |||
end |
@@ -104,7 +104,6 @@ defmodule Pleroma.Web.Router do | |||
get("/domain_blocks", MastodonAPIController, :empty_array) | |||
get("/follow_requests", MastodonAPIController, :empty_array) | |||
get("/mutes", MastodonAPIController, :empty_array) | |||
get("/lists", MastodonAPIController, :empty_array) | |||
get("/timelines/home", MastodonAPIController, :home_timeline) | |||
@@ -124,6 +123,15 @@ defmodule Pleroma.Web.Router do | |||
get("/notifications/:id", MastodonAPIController, :get_notification) | |||
post("/media", MastodonAPIController, :upload) | |||
get("/lists", MastodonAPIController, :get_lists) | |||
get("/lists/:id", MastodonAPIController, :get_list) | |||
delete("/lists/:id", MastodonAPIController, :delete_list) | |||
post("/lists", MastodonAPIController, :create_list) | |||
put("/lists/:id", MastodonAPIController, :rename_list) | |||
get("/lists/:id/accounts", MastodonAPIController, :list_accounts) | |||
post("/lists/:id/accounts", MastodonAPIController, :add_to_list) | |||
delete("/lists/:id/accounts", MastodonAPIController, :remove_from_list) | |||
end | |||
scope "/api/web", Pleroma.Web.MastodonAPI do | |||
@@ -141,6 +149,7 @@ defmodule Pleroma.Web.Router do | |||
get("/timelines/public", MastodonAPIController, :public_timeline) | |||
get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline) | |||
get("/timelines/list/:list_id", MastodonAPIController, :list_timeline) | |||
get("/statuses/:id", MastodonAPIController, :get_status) | |||
get("/statuses/:id/context", MastodonAPIController, :get_context) | |||
@@ -0,0 +1,15 @@ | |||
defmodule Pleroma.Repo.Migrations.CreateLists do | |||
use Ecto.Migration | |||
def change do | |||
create table(:lists) do | |||
add :user_id, references(:users, on_delete: :delete_all) | |||
add :title, :string | |||
add :following, {:array, :string} | |||
timestamps() | |||
end | |||
create index(:lists, [:user_id]) | |||
end | |||
end |
@@ -0,0 +1,77 @@ | |||
defmodule Pleroma.ListTest do | |||
alias Pleroma.{User, Repo} | |||
use Pleroma.DataCase | |||
import Pleroma.Factory | |||
import Ecto.Query | |||
test "creating a list" do | |||
user = insert(:user) | |||
{:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) | |||
%Pleroma.List{title: title} = Pleroma.List.get(list.id, user) | |||
assert title == "title" | |||
end | |||
test "getting a list not belonging to the user" do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) | |||
ret = Pleroma.List.get(list.id, other_user) | |||
assert is_nil(ret) | |||
end | |||
test "adding an user to a list" do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, list} = Pleroma.List.create("title", user) | |||
{:ok, %{following: following}} = Pleroma.List.follow(list, other_user) | |||
assert [other_user.follower_address] == following | |||
end | |||
test "removing an user from a list" do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, list} = Pleroma.List.create("title", user) | |||
{:ok, %{following: following}} = Pleroma.List.follow(list, other_user) | |||
{:ok, %{following: following}} = Pleroma.List.unfollow(list, other_user) | |||
assert [] == following | |||
end | |||
test "renaming a list" do | |||
user = insert(:user) | |||
{:ok, list} = Pleroma.List.create("title", user) | |||
{:ok, %{title: title}} = Pleroma.List.rename(list, "new") | |||
assert "new" == title | |||
end | |||
test "deleting a list" do | |||
user = insert(:user) | |||
{:ok, list} = Pleroma.List.create("title", user) | |||
{:ok, list} = Pleroma.List.delete(list) | |||
assert is_nil(Repo.get(Pleroma.List, list.id)) | |||
end | |||
test "getting users in a list" do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
third_user = insert(:user) | |||
{:ok, list} = Pleroma.List.create("title", user) | |||
{:ok, list} = Pleroma.List.follow(list, other_user) | |||
{:ok, list} = Pleroma.List.follow(list, third_user) | |||
{:ok, following} = Pleroma.List.get_following(list) | |||
assert other_user in following | |||
assert third_user in following | |||
end | |||
test "getting all lists by an user" do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, list_one} = Pleroma.List.create("title", user) | |||
{:ok, list_two} = Pleroma.List.create("other title", user) | |||
{:ok, list_three} = Pleroma.List.create("third title", other_user) | |||
lists = Pleroma.List.for_user(user, %{}) | |||
assert list_one in lists | |||
assert list_two in lists | |||
refute list_three in lists | |||
end | |||
end |
@@ -0,0 +1,19 @@ | |||
defmodule Pleroma.Web.MastodonAPI.ListViewTest do | |||
use Pleroma.DataCase | |||
import Pleroma.Factory | |||
alias Pleroma.Web.MastodonAPI.ListView | |||
alias Pleroma.List | |||
test "Represent a list" do | |||
user = insert(:user) | |||
title = "mortal enemies" | |||
{:ok, list} = Pleroma.List.create(title, user) | |||
expected = %{ | |||
id: to_string(list.id), | |||
title: title | |||
} | |||
assert expected == ListView.render("list.json", %{list: list}) | |||
end | |||
end |
@@ -195,6 +195,125 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
end | |||
end | |||
describe "lists" do | |||
test "creating a list", %{conn: conn} do | |||
user = insert(:user) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> post("/api/v1/lists", %{"title" => "cuties"}) | |||
assert %{"title" => title} = json_response(conn, 200) | |||
assert title == "cuties" | |||
end | |||
test "adding users to a list", %{conn: conn} do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, list} = Pleroma.List.create("name", user) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) | |||
assert %{} == json_response(conn, 200) | |||
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user) | |||
assert following == [other_user.follower_address] | |||
end | |||
test "removing users from a list", %{conn: conn} do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
third_user = insert(:user) | |||
{:ok, list} = Pleroma.List.create("name", user) | |||
{:ok, list} = Pleroma.List.follow(list, other_user) | |||
{:ok, list} = Pleroma.List.follow(list, third_user) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) | |||
assert %{} == json_response(conn, 200) | |||
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user) | |||
assert following == [third_user.follower_address] | |||
end | |||
test "listing users in a list", %{conn: conn} do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, list} = Pleroma.List.create("name", user) | |||
{:ok, list} = Pleroma.List.follow(list, other_user) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) | |||
assert [%{"id" => id}] = json_response(conn, 200) | |||
assert id == to_string(other_user.id) | |||
end | |||
test "retrieving a list", %{conn: conn} do | |||
user = insert(:user) | |||
{:ok, list} = Pleroma.List.create("name", user) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> get("/api/v1/lists/#{list.id}") | |||
assert %{"id" => id} = json_response(conn, 200) | |||
assert id == to_string(list.id) | |||
end | |||
test "renaming a list", %{conn: conn} do | |||
user = insert(:user) | |||
{:ok, list} = Pleroma.List.create("name", user) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> put("/api/v1/lists/#{list.id}", %{"title" => "newname"}) | |||
assert %{"title" => name} = json_response(conn, 200) | |||
assert name == "newname" | |||
end | |||
test "deleting a list", %{conn: conn} do | |||
user = insert(:user) | |||
{:ok, list} = Pleroma.List.create("name", user) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> delete("/api/v1/lists/#{list.id}") | |||
assert %{} = json_response(conn, 200) | |||
assert is_nil(Repo.get(Pleroma.List, list.id)) | |||
end | |||
test "list timeline", %{conn: conn} do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, activity_one} = TwitterAPI.create_status(user, %{"status" => "Marisa is cute."}) | |||
{:ok, activity_two} = TwitterAPI.create_status(other_user, %{"status" => "Marisa is cute."}) | |||
{:ok, list} = Pleroma.List.create("name", user) | |||
{:ok, list} = Pleroma.List.follow(list, other_user) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> get("/api/v1/timelines/list/#{list.id}") | |||
assert [%{"id" => id}] = json_response(conn, 200) | |||
assert id == to_string(activity_two.id) | |||
end | |||
end | |||
describe "notifications" do | |||
test "list of notifications", %{conn: conn} do | |||
user = insert(:user) | |||