Update
This commit is contained in:
parent
188bc08494
commit
4af88f3e1d
44 changed files with 1041 additions and 34 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -28,6 +28,9 @@ nulla-*.tar
|
||||||
# Ignore assets that are produced by build tools.
|
# Ignore assets that are produced by build tools.
|
||||||
/priv/static/assets/
|
/priv/static/assets/
|
||||||
|
|
||||||
|
# The directory of uploaded files
|
||||||
|
/priv/static/system/
|
||||||
|
|
||||||
# Ignore digested assets cache.
|
# Ignore digested assets cache.
|
||||||
/priv/static/cache_manifest.json
|
/priv/static/cache_manifest.json
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,11 @@ config :logger, :console,
|
||||||
# Use Jason for JSON parsing in Phoenix
|
# Use Jason for JSON parsing in Phoenix
|
||||||
config :phoenix, :json_library, Jason
|
config :phoenix, :json_library, Jason
|
||||||
|
|
||||||
|
# Custom mime types
|
||||||
|
config :mime, :types, %{
|
||||||
|
"application/activity+json" => ["activity+json"]
|
||||||
|
}
|
||||||
|
|
||||||
# Import environment specific config. This must remain at the bottom
|
# Import environment specific config. This must remain at the bottom
|
||||||
# of this file so it overrides the configuration defined above.
|
# of this file so it overrides the configuration defined above.
|
||||||
import_config "#{config_env()}.exs"
|
import_config "#{config_env()}.exs"
|
||||||
|
|
|
@ -3,6 +3,7 @@ defmodule Nulla.Accounts do
|
||||||
The Accounts context.
|
The Accounts context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
alias Nulla.Repo
|
alias Nulla.Repo
|
||||||
|
|
||||||
|
@ -374,4 +375,21 @@ defmodule Nulla.Accounts do
|
||||||
_ -> :error
|
_ -> :error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_total_users_count() do
|
||||||
|
Repo.aggregate(from(u in User), :count, :id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_active_users_count(days) do
|
||||||
|
cutoff = DateTime.add(DateTime.utc_now(), -days * 86400, :second)
|
||||||
|
|
||||||
|
from(u in User, where: u.last_active_at > ^cutoff)
|
||||||
|
|> Repo.aggregate(:count, :id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_last_active(user) do
|
||||||
|
user
|
||||||
|
|> change(last_active_at: DateTime.utc_now())
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,6 +11,7 @@ defmodule Nulla.Accounts.User do
|
||||||
field :hashed_password, :string, redact: true
|
field :hashed_password, :string, redact: true
|
||||||
field :current_password, :string, virtual: true, redact: true
|
field :current_password, :string, virtual: true, redact: true
|
||||||
field :confirmed_at, :utc_datetime
|
field :confirmed_at, :utc_datetime
|
||||||
|
field :last_active_at, :utc_datetime
|
||||||
|
|
||||||
timestamps(type: :utc_datetime)
|
timestamps(type: :utc_datetime)
|
||||||
end
|
end
|
||||||
|
|
|
@ -101,4 +101,29 @@ defmodule Nulla.Activities do
|
||||||
def change_activity(%Activity{} = activity, attrs \\ %{}) do
|
def change_activity(%Activity{} = activity, attrs \\ %{}) do
|
||||||
Activity.changeset(activity, attrs)
|
Activity.changeset(activity, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_latest_activities(actor_ap_id, limit \\ 20) do
|
||||||
|
from(a in Activity,
|
||||||
|
where: a.actor == ^actor_ap_id,
|
||||||
|
order_by: [desc: a.inserted_at],
|
||||||
|
limit: ^limit,
|
||||||
|
preload: [:actor, :media_attachments]
|
||||||
|
)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_before_activities(actor_ap_id, max_id, limit \\ 20) do
|
||||||
|
from(a in Activity,
|
||||||
|
where: a.actor == ^actor_ap_id and a.id < ^max_id,
|
||||||
|
order_by: [desc: a.inserted_at],
|
||||||
|
limit: ^limit,
|
||||||
|
preload: [:actor, :media_attachments]
|
||||||
|
)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_total_activities_count(actor_ap_id) do
|
||||||
|
from(a in Activity, where: a.actor == ^actor_ap_id)
|
||||||
|
|> Repo.aggregate(:count, :id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,6 +37,8 @@ defmodule Nulla.Actors do
|
||||||
"""
|
"""
|
||||||
def get_actor!(id), do: Repo.get!(Actor, id)
|
def get_actor!(id), do: Repo.get!(Actor, id)
|
||||||
|
|
||||||
|
def get_actor_by(by) when is_map(by) or is_list(by), do: Repo.get_by(Actor, by)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a actor.
|
Creates a actor.
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
defmodule Nulla.Relations do
|
defmodule Nulla.Relations do
|
||||||
|
alias Nulla.Actors.Actor
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
The Relations context.
|
The Relations context.
|
||||||
"""
|
"""
|
||||||
|
@ -37,6 +39,8 @@ defmodule Nulla.Relations do
|
||||||
"""
|
"""
|
||||||
def get_relation!(id), do: Repo.get!(Relation, id)
|
def get_relation!(id), do: Repo.get!(Relation, id)
|
||||||
|
|
||||||
|
def get_relation_by(by) when is_map(by) or is_list(by), do: Repo.get_by(Relation, by)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a relation.
|
Creates a relation.
|
||||||
|
|
||||||
|
@ -101,4 +105,50 @@ defmodule Nulla.Relations do
|
||||||
def change_relation(%Relation{} = relation, attrs \\ %{}) do
|
def change_relation(%Relation{} = relation, attrs \\ %{}) do
|
||||||
Relation.changeset(relation, attrs)
|
Relation.changeset(relation, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def count_following(local_actor_id) do
|
||||||
|
Relation
|
||||||
|
|> where([r], r.local_actor_id == ^local_actor_id and r.following == true)
|
||||||
|
|> select([r], count(r.id))
|
||||||
|
|> Repo.one()
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_following(local_actor_id, page, limit) when is_integer(page) and page > 0 do
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
|
query =
|
||||||
|
from r in Relation,
|
||||||
|
join: a in Actor,
|
||||||
|
on: a.id == r.remote_actor_id,
|
||||||
|
where: r.local_actor_id == ^local_actor_id and r.following == true,
|
||||||
|
order_by: [asc: a.published],
|
||||||
|
offset: ^offset,
|
||||||
|
limit: ^limit,
|
||||||
|
select: a
|
||||||
|
|
||||||
|
Repo.all(query)
|
||||||
|
end
|
||||||
|
|
||||||
|
def count_followers(local_actor_id) do
|
||||||
|
Relation
|
||||||
|
|> where([r], r.local_actor_id == ^local_actor_id and r.followed_by == true)
|
||||||
|
|> select([r], count(r.id))
|
||||||
|
|> Repo.one()
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_followers(local_actor_id, page, limit) when is_integer(page) and page > 0 do
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
|
query =
|
||||||
|
from r in Relation,
|
||||||
|
join: a in Actor,
|
||||||
|
on: a.id == r.remote_actor_id,
|
||||||
|
where: r.local_actor_id == ^local_actor_id and r.followed_by == true,
|
||||||
|
order_by: [asc: a.published],
|
||||||
|
offset: ^offset,
|
||||||
|
limit: ^limit,
|
||||||
|
select: a
|
||||||
|
|
||||||
|
Repo.all(query)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,7 @@ defmodule Nulla.Relations.Relation do
|
||||||
alias Nulla.Snowflake
|
alias Nulla.Snowflake
|
||||||
alias Nulla.Actors.Actor
|
alias Nulla.Actors.Actor
|
||||||
|
|
||||||
|
@primary_key {:id, :integer, autogenerate: false}
|
||||||
schema "relations" do
|
schema "relations" do
|
||||||
field :following, :boolean, default: false
|
field :following, :boolean, default: false
|
||||||
field :followed_by, :boolean, default: false
|
field :followed_by, :boolean, default: false
|
||||||
|
@ -61,6 +62,7 @@ defmodule Nulla.Relations.Relation do
|
||||||
:local_actor_id,
|
:local_actor_id,
|
||||||
:remote_actor_id
|
:remote_actor_id
|
||||||
])
|
])
|
||||||
|
|> unique_constraint([:local_actor_id, :remote_actor_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_put_id(%Changeset{data: %{id: nil}} = changeset) do
|
defp maybe_put_id(%Changeset{data: %{id: nil}} = changeset) do
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
defmodule Nulla.Sender do
|
defmodule Nulla.Sender do
|
||||||
alias Nulla.HTTPSignature
|
alias Nulla.HTTPSignature
|
||||||
alias NullaWeb.ActivityJSON
|
alias NullaWeb.ActivityPub.ActivityJSON
|
||||||
|
|
||||||
def send_activity(method, inbox, activity, publicKeyId, privateKeyPem) do
|
def send_activity(method, inbox, activity, publicKeyId, privateKeyPem) do
|
||||||
body = Jason.encode!(ActivityJSON.activitypub(activity))
|
body = Jason.encode!(ActivityJSON.show(activity))
|
||||||
headers = HTTPSignature.make_headers(body, inbox, publicKeyId, privateKeyPem)
|
headers = HTTPSignature.make_headers(body, inbox, publicKeyId, privateKeyPem)
|
||||||
request = Finch.build(method, inbox, headers, body)
|
request = Finch.build(method, inbox, headers, body)
|
||||||
|
|
||||||
|
|
25
lib/nulla/utils.ex
Normal file
25
lib/nulla/utils.ex
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
defmodule Nulla.Utils do
|
||||||
|
def fetch_remote_actor(uri) do
|
||||||
|
headers = [
|
||||||
|
{"Accept", "application/activity+json"},
|
||||||
|
{"User-Agent", "Nulla/1.0"},
|
||||||
|
{"Host", URI.parse(uri).host}
|
||||||
|
]
|
||||||
|
|
||||||
|
request = Finch.build(:get, uri, headers)
|
||||||
|
|
||||||
|
case Finch.request(request, Nulla.Finch) do
|
||||||
|
{:ok, %Finch.Response{status: 200, body: body}} ->
|
||||||
|
case Jason.decode(body) do
|
||||||
|
{:ok, data} -> {:ok, data}
|
||||||
|
_ -> {:error, :invalid_json}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, %Finch.Response{status: code}} when code in 300..399 ->
|
||||||
|
{:error, :redirect_not_followed}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :actor_fetch_failed}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -17,7 +17,7 @@ defmodule NullaWeb do
|
||||||
those modules here.
|
those modules here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
|
def static_paths, do: ~w(assets system fonts images favicon.ico robots.txt)
|
||||||
|
|
||||||
def router do
|
def router do
|
||||||
quote do
|
quote do
|
||||||
|
|
21
lib/nulla_web/controllers/activitypub/activity_json.ex
Normal file
21
lib/nulla_web/controllers/activitypub/activity_json.ex
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
defmodule NullaWeb.ActivityPub.ActivityJSON do
|
||||||
|
alias Nulla.Activities.Activity
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a single activity.
|
||||||
|
"""
|
||||||
|
def show(activity) do
|
||||||
|
data(activity)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp data(%Activity{} = activity) do
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
id: activity.ap_id,
|
||||||
|
type: activity.type,
|
||||||
|
actor: activity.actor,
|
||||||
|
object: activity.object,
|
||||||
|
to: activity.to,
|
||||||
|
cc: activity.cc
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
22
lib/nulla_web/controllers/activitypub/actor_controller.ex
Normal file
22
lib/nulla_web/controllers/activitypub/actor_controller.ex
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
defmodule NullaWeb.ActivityPub.ActorController do
|
||||||
|
use NullaWeb, :controller
|
||||||
|
alias NullaWeb.ActivityPub.ActorJSON
|
||||||
|
alias Nulla.Actors
|
||||||
|
alias Nulla.Actors.Actor
|
||||||
|
|
||||||
|
def show(conn, %{"username" => username}) do
|
||||||
|
domain = NullaWeb.Endpoint.host()
|
||||||
|
|
||||||
|
case Actors.get_actor_by(acct: "#{username}@#{domain}") do
|
||||||
|
nil ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{error: "Not Found"})
|
||||||
|
|
||||||
|
%Actor{} = actor ->
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/activity+json")
|
||||||
|
|> send_resp(200, Jason.encode!(ActorJSON.show(actor)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
54
lib/nulla_web/controllers/activitypub/actor_json.ex
Normal file
54
lib/nulla_web/controllers/activitypub/actor_json.ex
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
defmodule NullaWeb.ActivityPub.ActorJSON do
|
||||||
|
alias Nulla.Actors.Actor
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a single actor.
|
||||||
|
"""
|
||||||
|
def show(actor) do
|
||||||
|
data(actor)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp data(%Actor{} = actor) do
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
manuallyApprovesFollowers: "as:manuallyApprovesFollowers",
|
||||||
|
alsoKnownAs: %{"@id" => "as:alsoKnownAs", "@type" => "@id"},
|
||||||
|
movedTo: %{"@id" => "as:movedTo", "@type" => "@id"},
|
||||||
|
schema: "http://schema.org#",
|
||||||
|
PropertyValue: "schema:PropertyValue",
|
||||||
|
value: "schema:value",
|
||||||
|
Hashtag: "as:Hashtag",
|
||||||
|
vcard: "http://www.w3.org/2006/vcard/ns#"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
ap_id: actor.ap_id,
|
||||||
|
type: actor.type,
|
||||||
|
following: actor.following,
|
||||||
|
followers: actor.followers,
|
||||||
|
inbox: actor.inbox,
|
||||||
|
outbox: actor.outbox,
|
||||||
|
featured: actor.featured,
|
||||||
|
featuredTags: actor.featuredTags,
|
||||||
|
preferredUsername: actor.preferredUsername,
|
||||||
|
name: actor.name,
|
||||||
|
summary: actor.summary,
|
||||||
|
url: actor.url,
|
||||||
|
manuallyApprovesFollowers: actor.manuallyApprovesFollowers,
|
||||||
|
discoverable: actor.discoverable,
|
||||||
|
indexable: actor.indexable,
|
||||||
|
published: actor.published,
|
||||||
|
memorial: actor.memorial,
|
||||||
|
publicKey: actor.publicKey,
|
||||||
|
tag: actor.tag,
|
||||||
|
attachment: actor.attachment,
|
||||||
|
endpoints: actor.endpoints,
|
||||||
|
icon: actor.icon,
|
||||||
|
image: actor.image,
|
||||||
|
"vcard:bday": actor.vcard_bday,
|
||||||
|
"vcard:Address": actor.vcard_Address
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
64
lib/nulla_web/controllers/activitypub/follow_controller.ex
Normal file
64
lib/nulla_web/controllers/activitypub/follow_controller.ex
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
defmodule NullaWeb.ActivityPub.FollowController do
|
||||||
|
use NullaWeb, :controller
|
||||||
|
alias NullaWeb.ActivityPub.FollowJSON
|
||||||
|
alias Nulla.Actors
|
||||||
|
alias Nulla.Relations
|
||||||
|
|
||||||
|
def following(conn, %{"username" => username, "page" => page_param}) do
|
||||||
|
domain = NullaWeb.Endpoint.host()
|
||||||
|
limit = Application.get_env(:nulla, :instance)[:api_limit]
|
||||||
|
actor = Actors.get_actor_by(acct: "#{username}@#{domain}")
|
||||||
|
total = Relations.count_following(actor.id)
|
||||||
|
|
||||||
|
page =
|
||||||
|
case Integer.parse(page_param) do
|
||||||
|
{int, _} when int > 0 -> int
|
||||||
|
_ -> 1
|
||||||
|
end
|
||||||
|
|
||||||
|
following_list = Enum.map(Relations.get_following(actor.id, page, limit), & &1.ap_id)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/activity+json")
|
||||||
|
|> json(FollowJSON.following(actor, total, following_list, page, limit))
|
||||||
|
end
|
||||||
|
|
||||||
|
def following(conn, %{"username" => username}) do
|
||||||
|
domain = NullaWeb.Endpoint.host()
|
||||||
|
actor = Actors.get_actor_by(acct: "#{username}@#{domain}")
|
||||||
|
total = Relations.count_following(actor.id)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/activity+json")
|
||||||
|
|> json(FollowJSON.following(actor, total))
|
||||||
|
end
|
||||||
|
|
||||||
|
def followers(conn, %{"username" => username, "page" => page_param}) do
|
||||||
|
domain = NullaWeb.Endpoint.host()
|
||||||
|
limit = Application.get_env(:nulla, :instance)[:api_limit]
|
||||||
|
actor = Actors.get_actor_by(acct: "#{username}@#{domain}")
|
||||||
|
total = Relations.count_followers(actor.id)
|
||||||
|
|
||||||
|
page =
|
||||||
|
case Integer.parse(page_param) do
|
||||||
|
{int, _} when int > 0 -> int
|
||||||
|
_ -> 1
|
||||||
|
end
|
||||||
|
|
||||||
|
followers_list = Enum.map(Relations.get_followers(actor.id, page, limit), & &1.ap_id)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/activity+json")
|
||||||
|
|> json(FollowJSON.followers(actor, total, followers_list, page, limit))
|
||||||
|
end
|
||||||
|
|
||||||
|
def followers(conn, %{"username" => username}) do
|
||||||
|
domain = NullaWeb.Endpoint.host()
|
||||||
|
actor = Actors.get_actor_by(acct: "#{username}@#{domain}")
|
||||||
|
total = Relations.count_followers(actor.id)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/activity+json")
|
||||||
|
|> json(FollowJSON.followers(actor, total))
|
||||||
|
end
|
||||||
|
end
|
84
lib/nulla_web/controllers/activitypub/follow_json.ex
Normal file
84
lib/nulla_web/controllers/activitypub/follow_json.ex
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
defmodule NullaWeb.ActivityPub.FollowJSON do
|
||||||
|
def following(actor, total) do
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
id: "#{actor.ap_id}/following",
|
||||||
|
type: "OrderedCollection",
|
||||||
|
totalItems: total,
|
||||||
|
first: "#{actor.ap_id}/following?page=1"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def following(actor, total, following_list, page, limit) when is_integer(page) and page > 0 do
|
||||||
|
data = [
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
id: "#{actor.ap_id}/following?page=#{page}",
|
||||||
|
type: "OrderedCollectionPage",
|
||||||
|
totalItems: total,
|
||||||
|
next: "#{actor.ap_id}/following?page=#{page + 1}",
|
||||||
|
prev: "#{actor.ap_id}/following?page=#{page - 1}",
|
||||||
|
partOf: "#{actor.ap_id}/following",
|
||||||
|
orderedItems: following_list
|
||||||
|
]
|
||||||
|
|
||||||
|
data =
|
||||||
|
if page <= 1 do
|
||||||
|
Keyword.delete(data, :prev)
|
||||||
|
else
|
||||||
|
data
|
||||||
|
end
|
||||||
|
|
||||||
|
data =
|
||||||
|
if page * limit > total do
|
||||||
|
data
|
||||||
|
|> Keyword.delete(:next)
|
||||||
|
|> Keyword.delete(:prev)
|
||||||
|
else
|
||||||
|
data
|
||||||
|
end
|
||||||
|
|
||||||
|
Jason.OrderedObject.new(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def followers(actor, total) do
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
id: "#{actor.ap_id}/followers",
|
||||||
|
type: "OrderedCollection",
|
||||||
|
totalItems: total,
|
||||||
|
first: "#{actor.ap_id}/followers?page=1"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def followers(actor, total, followers_list, page, limit)
|
||||||
|
when is_integer(page) and page > 0 do
|
||||||
|
data = [
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
id: "#{actor.ap_id}/followers?page=#{page}",
|
||||||
|
type: "OrderedCollectionPage",
|
||||||
|
totalItems: total,
|
||||||
|
next: "#{actor.ap_id}/followers?page=#{page + 1}",
|
||||||
|
prev: "#{actor.ap_id}/followers?page=#{page - 1}",
|
||||||
|
partOf: "#{actor.ap_id}/followers",
|
||||||
|
orderedItems: followers_list
|
||||||
|
]
|
||||||
|
|
||||||
|
data =
|
||||||
|
if page <= 1 do
|
||||||
|
Keyword.delete(data, :prev)
|
||||||
|
else
|
||||||
|
data
|
||||||
|
end
|
||||||
|
|
||||||
|
data =
|
||||||
|
if page * limit > total do
|
||||||
|
data
|
||||||
|
|> Keyword.delete(:next)
|
||||||
|
|> Keyword.delete(:prev)
|
||||||
|
else
|
||||||
|
data
|
||||||
|
end
|
||||||
|
|
||||||
|
Jason.OrderedObject.new(data)
|
||||||
|
end
|
||||||
|
end
|
243
lib/nulla_web/controllers/activitypub/inbox_controller.ex
Normal file
243
lib/nulla_web/controllers/activitypub/inbox_controller.ex
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
defmodule NullaWeb.ActivityPub.InboxController do
|
||||||
|
use NullaWeb, :controller
|
||||||
|
alias Nulla.Snowflake
|
||||||
|
alias Nulla.HTTPSignature
|
||||||
|
alias Nulla.Sender
|
||||||
|
alias Nulla.Utils
|
||||||
|
alias Nulla.Actors
|
||||||
|
alias Nulla.Relations
|
||||||
|
alias Nulla.Activities
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _create_id,
|
||||||
|
"type" => "Create",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _read_id,
|
||||||
|
"type" => "Read",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _update_id,
|
||||||
|
"type" => "Update",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _delete_id,
|
||||||
|
"type" => "Delete",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _add_id,
|
||||||
|
"type" => "Add",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _view_id,
|
||||||
|
"type" => "View",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _move_id,
|
||||||
|
"type" => "Move",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _undo_id,
|
||||||
|
"type" => "Undo",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => follow_id,
|
||||||
|
"type" => "Follow",
|
||||||
|
"actor" => actor_uri,
|
||||||
|
"object" => target_uri
|
||||||
|
}) do
|
||||||
|
accept_id = Snowflake.next_id()
|
||||||
|
|
||||||
|
with local_actor <- Actors.get_actor_by(ap_id: target_uri),
|
||||||
|
{:ok, remote_actor_json} <- Utils.fetch_remote_actor(actor_uri),
|
||||||
|
:ok <- HTTPSignature.verify(conn, remote_actor_json["publicKey"]["publicKeyPem"]) do
|
||||||
|
remote_actor =
|
||||||
|
case Actors.get_actor_by(ap_id: remote_actor_json["id"]) do
|
||||||
|
nil ->
|
||||||
|
case Actors.create_actor(remote_actor_json) do
|
||||||
|
{:ok, actor} -> actor
|
||||||
|
{:error, error} -> {:error, error}
|
||||||
|
end
|
||||||
|
|
||||||
|
actor ->
|
||||||
|
actor
|
||||||
|
end
|
||||||
|
|
||||||
|
with {:ok, follow_activity} <-
|
||||||
|
Activities.create_activity(%{
|
||||||
|
ap_id: follow_id,
|
||||||
|
type: "Follow",
|
||||||
|
actor: remote_actor.ap_id,
|
||||||
|
object: target_uri
|
||||||
|
}),
|
||||||
|
{:ok, accept_activity} <-
|
||||||
|
Activities.create_activity(%{
|
||||||
|
id: accept_id,
|
||||||
|
ap_id: "https://#{local_actor.domain}/activities/accept/#{accept_id}",
|
||||||
|
type: "Accept",
|
||||||
|
actor: local_actor.ap_id,
|
||||||
|
object: Jason.encode!(follow_activity)
|
||||||
|
}) do
|
||||||
|
{:ok, _relation} =
|
||||||
|
case Relations.get_relation_by(%{
|
||||||
|
local_actor_id: local_actor.id,
|
||||||
|
remote_actor_id: remote_actor.id
|
||||||
|
}) do
|
||||||
|
nil ->
|
||||||
|
case Relations.create_relation(%{
|
||||||
|
local_actor_id: local_actor.id,
|
||||||
|
remote_actor_id: remote_actor.id,
|
||||||
|
followed_by: true
|
||||||
|
}) do
|
||||||
|
{:ok, relation} -> relation
|
||||||
|
{:error, changeset} -> {:error, {:relation_creation_failed, changeset}}
|
||||||
|
end
|
||||||
|
|
||||||
|
relation ->
|
||||||
|
Relations.update_relation(relation, %{followed_by: true})
|
||||||
|
end
|
||||||
|
|
||||||
|
Sender.send_activity(
|
||||||
|
:post,
|
||||||
|
remote_actor.inbox,
|
||||||
|
accept_activity,
|
||||||
|
local_actor.publicKey["id"],
|
||||||
|
local_actor.privateKeyPem
|
||||||
|
)
|
||||||
|
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
else
|
||||||
|
error ->
|
||||||
|
json(conn, %{"error" => "Failed to process Follow: #{error}"})
|
||||||
|
end
|
||||||
|
else
|
||||||
|
error ->
|
||||||
|
json(conn, %{"error" => "Failed to process Follow: #{error}"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _accept_id,
|
||||||
|
"type" => "Accept",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _reject_id,
|
||||||
|
"type" => "Reject",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _block_id,
|
||||||
|
"type" => "Block",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _join_id,
|
||||||
|
"type" => "Join",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _leave_id,
|
||||||
|
"type" => "Leave",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _like_id,
|
||||||
|
"type" => "Like",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _dislike_id,
|
||||||
|
"type" => "Dislike",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _announce_id,
|
||||||
|
"type" => "Announce",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, %{
|
||||||
|
"id" => _question_id,
|
||||||
|
"type" => "Question",
|
||||||
|
"actor" => _actor_uri,
|
||||||
|
"object" => _target_uri
|
||||||
|
}) do
|
||||||
|
send_resp(conn, 200, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox(conn, _params) do
|
||||||
|
send_resp(conn, 400, "")
|
||||||
|
end
|
||||||
|
end
|
38
lib/nulla_web/controllers/activitypub/note_controller.ex
Normal file
38
lib/nulla_web/controllers/activitypub/note_controller.ex
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
defmodule NullaWeb.ActivityPub.NoteController do
|
||||||
|
use NullaWeb, :controller
|
||||||
|
alias Nulla.Repo
|
||||||
|
alias NullaWeb.ActivityPub.NoteJSON
|
||||||
|
alias Nulla.Notes
|
||||||
|
|
||||||
|
def show(conn, %{"username" => username, "id" => id}) do
|
||||||
|
case Integer.parse(id) do
|
||||||
|
{int_id, ""} ->
|
||||||
|
note = Notes.get_note!(int_id) |> Repo.preload([:actor, :media_attachments])
|
||||||
|
|
||||||
|
cond do
|
||||||
|
is_nil(note) ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{error: "Not Found"})
|
||||||
|
|> halt()
|
||||||
|
|
||||||
|
username != note.actor.preferredUsername ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{error: "Not Found"})
|
||||||
|
|> halt()
|
||||||
|
|
||||||
|
true ->
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/activity+json")
|
||||||
|
|> json(NoteJSON.show(note))
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{error: "Not Found"})
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
50
lib/nulla_web/controllers/activitypub/note_json.ex
Normal file
50
lib/nulla_web/controllers/activitypub/note_json.ex
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
defmodule NullaWeb.ActivityPub.NoteJSON do
|
||||||
|
alias Nulla.Notes.Note
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a single note.
|
||||||
|
"""
|
||||||
|
def show(note) do
|
||||||
|
data(note)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp data(%Note{} = note) do
|
||||||
|
attachment =
|
||||||
|
case note.media_attachments do
|
||||||
|
[] ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
attachments ->
|
||||||
|
[
|
||||||
|
attachment:
|
||||||
|
Enum.map(attachments, fn att ->
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
type: "Document",
|
||||||
|
mediaType: att.mime_type,
|
||||||
|
url: "https://#{note.actor.domain}/files/#{att.file}"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
Jason.OrderedObject.new(sensitive: "as:sensitive")
|
||||||
|
],
|
||||||
|
id: "#{note.actor.ap_id}/notes/#{note.id}",
|
||||||
|
type: "Note",
|
||||||
|
summary: nil,
|
||||||
|
inReplyTo: note.inReplyTo,
|
||||||
|
published: note.published,
|
||||||
|
url: note.url,
|
||||||
|
attributedTo: note.actor.ap_id,
|
||||||
|
to: note.to,
|
||||||
|
cc: note.cc,
|
||||||
|
sensitive: note.sensitive,
|
||||||
|
content: note.content,
|
||||||
|
contentMap: Jason.OrderedObject.new("#{note.language}": note.content),
|
||||||
|
attachment: attachment
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
57
lib/nulla_web/controllers/activitypub/outbox_controller.ex
Normal file
57
lib/nulla_web/controllers/activitypub/outbox_controller.ex
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
defmodule NullaWeb.ActivityPub.OutboxController do
|
||||||
|
use NullaWeb, :controller
|
||||||
|
alias NullaWeb.ActivityPub.OutboxJSON
|
||||||
|
alias Nulla.Actors
|
||||||
|
alias Nulla.Activities
|
||||||
|
alias Nulla.Actors.Actor
|
||||||
|
|
||||||
|
def index(conn, %{"username" => username, "page" => "true"} = params) do
|
||||||
|
domain = NullaWeb.Endpoint.host()
|
||||||
|
actor = Actors.get_actor_by(acct: "#{username}@#{domain}")
|
||||||
|
|
||||||
|
max_id = params["max_id"] && String.to_integer(params["max_id"])
|
||||||
|
|
||||||
|
activities =
|
||||||
|
if max_id do
|
||||||
|
Activities.get_before_activities(actor.ap_id, max_id)
|
||||||
|
else
|
||||||
|
Activities.get_latest_activities(actor.ap_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
next_max_id =
|
||||||
|
case List.last(activities) do
|
||||||
|
nil -> 0
|
||||||
|
last -> last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
min_id =
|
||||||
|
case List.first(activities) do
|
||||||
|
nil -> 0
|
||||||
|
first -> first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/activity+json")
|
||||||
|
|> send_resp(
|
||||||
|
200,
|
||||||
|
Jason.encode!(OutboxJSON.show(actor, activities, next_max_id, min_id))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def index(conn, %{"username" => username}) do
|
||||||
|
domain = NullaWeb.Endpoint.host()
|
||||||
|
actor = Actors.get_actor_by(acct: "#{username}@#{domain}")
|
||||||
|
|
||||||
|
case actor do
|
||||||
|
%Actor{} = actor ->
|
||||||
|
total = Activities.get_total_activities_count(actor.ap_id)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/activity+json")
|
||||||
|
|> send_resp(200, Jason.encode!(OutboxJSON.index(actor, total)))
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
send_resp(conn, 404, "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
44
lib/nulla_web/controllers/activitypub/outbox_json.ex
Normal file
44
lib/nulla_web/controllers/activitypub/outbox_json.ex
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
defmodule NullaWeb.ActivityPub.OutboxJSON do
|
||||||
|
@doc """
|
||||||
|
Renders an outbox.
|
||||||
|
"""
|
||||||
|
def index(actor, total) do
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
id: actor.outbox,
|
||||||
|
type: "OrderedCollection",
|
||||||
|
totalItems: total,
|
||||||
|
first: "#{actor.outbox}?page=true",
|
||||||
|
last: "#{actor.outbox}?min_id=0&page=true"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show(actor, activities, max_id, min_id) do
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
sensitive: "as:sensitive",
|
||||||
|
Hashtag: "as:Hashtag"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
id: "#{actor.outbox}?page=true",
|
||||||
|
type: "OrderedCollectionPage",
|
||||||
|
next: "#{actor.outbox}?max_id=#{max_id}&page=true",
|
||||||
|
prev: "#{actor.outbox}?min_id=#{min_id}&page=true",
|
||||||
|
partOf: actor.outbox,
|
||||||
|
orderedItems: Enum.map(activities, &data/1)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp data(activity) do
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
id: activity.ap_id,
|
||||||
|
type: activity.type,
|
||||||
|
actor: activity.actor,
|
||||||
|
object: activity.object,
|
||||||
|
to: activity.to,
|
||||||
|
cc: activity.cc
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.ActivityController do
|
defmodule NullaWeb.Api.ActivityController do
|
||||||
use NullaWeb, :controller
|
use NullaWeb, :controller
|
||||||
|
|
||||||
alias Nulla.Activities
|
alias Nulla.Activities
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.ActivityJSON do
|
defmodule NullaWeb.Api.ActivityJSON do
|
||||||
alias Nulla.Activities.Activity
|
alias Nulla.Activities.Activity
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -26,16 +26,4 @@ defmodule NullaWeb.ActivityJSON do
|
||||||
cc: activity.cc
|
cc: activity.cc
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def activitypub(%Activity{} = activity) do
|
|
||||||
Jason.OrderedObject.new(
|
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
|
||||||
id: activity.ap_id,
|
|
||||||
type: activity.type,
|
|
||||||
actor: activity.actor,
|
|
||||||
object: activity.object,
|
|
||||||
to: activity.to,
|
|
||||||
cc: activity.cc
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.ActorController do
|
defmodule NullaWeb.Api.ActorController do
|
||||||
use NullaWeb, :controller
|
use NullaWeb, :controller
|
||||||
|
|
||||||
alias Nulla.Actors
|
alias Nulla.Actors
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.ActorJSON do
|
defmodule NullaWeb.Api.ActorJSON do
|
||||||
alias Nulla.Actors.Actor
|
alias Nulla.Actors.Actor
|
||||||
|
|
||||||
@doc """
|
@doc """
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.MediaAttachmentController do
|
defmodule NullaWeb.Api.MediaAttachmentController do
|
||||||
use NullaWeb, :controller
|
use NullaWeb, :controller
|
||||||
|
|
||||||
alias Nulla.MediaAttachments
|
alias Nulla.MediaAttachments
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.MediaAttachmentJSON do
|
defmodule NullaWeb.Api.MediaAttachmentJSON do
|
||||||
alias Nulla.MediaAttachments.MediaAttachment
|
alias Nulla.MediaAttachments.MediaAttachment
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -23,7 +23,8 @@ defmodule NullaWeb.MediaAttachmentJSON do
|
||||||
url: media_attachment.url,
|
url: media_attachment.url,
|
||||||
name: media_attachment.name,
|
name: media_attachment.name,
|
||||||
width: media_attachment.width,
|
width: media_attachment.width,
|
||||||
height: media_attachment.height
|
height: media_attachment.height,
|
||||||
|
note_id: media_attachment.note_id
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.NoteController do
|
defmodule NullaWeb.Api.NoteController do
|
||||||
use NullaWeb, :controller
|
use NullaWeb, :controller
|
||||||
|
|
||||||
alias Nulla.Notes
|
alias Nulla.Notes
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.NoteJSON do
|
defmodule NullaWeb.Api.NoteJSON do
|
||||||
alias Nulla.Notes.Note
|
alias Nulla.Notes.Note
|
||||||
|
|
||||||
@doc """
|
@doc """
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.RelationController do
|
defmodule NullaWeb.Api.RelationController do
|
||||||
use NullaWeb, :controller
|
use NullaWeb, :controller
|
||||||
|
|
||||||
alias Nulla.Relations
|
alias Nulla.Relations
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.RelationJSON do
|
defmodule NullaWeb.Api.RelationJSON do
|
||||||
alias Nulla.Relations.Relation
|
alias Nulla.Relations.Relation
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -29,7 +29,9 @@ defmodule NullaWeb.RelationJSON do
|
||||||
blocked_by: relation.blocked_by,
|
blocked_by: relation.blocked_by,
|
||||||
domain_blocking: relation.domain_blocking,
|
domain_blocking: relation.domain_blocking,
|
||||||
requested: relation.requested,
|
requested: relation.requested,
|
||||||
note: relation.note
|
note: relation.note,
|
||||||
|
local_actor_id: relation.local_actor_id,
|
||||||
|
remote_actor_id: relation.remote_actor_id
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
18
lib/nulla_web/controllers/generic/hostmeta_controller.ex
Normal file
18
lib/nulla_web/controllers/generic/hostmeta_controller.ex
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
defmodule NullaWeb.Generic.HostmetaController do
|
||||||
|
use NullaWeb, :controller
|
||||||
|
|
||||||
|
def index(conn, _params) do
|
||||||
|
url = NullaWeb.Endpoint.url()
|
||||||
|
|
||||||
|
xml = """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||||
|
<Link rel="lrdd" type="application/xrd+xml" template="#{url}/.well-known/webfinger?resource={uri}"/>
|
||||||
|
</XRD>
|
||||||
|
"""
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/xrd+xml")
|
||||||
|
|> send_resp(200, xml)
|
||||||
|
end
|
||||||
|
end
|
28
lib/nulla_web/controllers/generic/nodeinfo_controller.ex
Normal file
28
lib/nulla_web/controllers/generic/nodeinfo_controller.ex
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
defmodule NullaWeb.Generic.NodeinfoController do
|
||||||
|
use NullaWeb, :controller
|
||||||
|
alias Nulla.Accounts
|
||||||
|
alias NullaWeb.Generic.NodeinfoJSON
|
||||||
|
|
||||||
|
def index(conn, _params) do
|
||||||
|
url = NullaWeb.Endpoint.url()
|
||||||
|
|
||||||
|
json(conn, NodeinfoJSON.index(url))
|
||||||
|
end
|
||||||
|
|
||||||
|
def show(conn, _params) do
|
||||||
|
version = Application.spec(:nulla, :vsn) |> to_string()
|
||||||
|
total = Accounts.get_total_users_count()
|
||||||
|
month = Accounts.get_active_users_count(30)
|
||||||
|
halfyear = Accounts.get_active_users_count(180)
|
||||||
|
|
||||||
|
users = %{
|
||||||
|
total: total,
|
||||||
|
month: month,
|
||||||
|
halfyear: halfyear
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = Application.get_env(:nulla, :instance) |> Map.new()
|
||||||
|
|
||||||
|
json(conn, NodeinfoJSON.show(version, users, instance))
|
||||||
|
end
|
||||||
|
end
|
52
lib/nulla_web/controllers/generic/nodeinfo_json.ex
Normal file
52
lib/nulla_web/controllers/generic/nodeinfo_json.ex
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
defmodule NullaWeb.Generic.NodeinfoJSON do
|
||||||
|
@doc """
|
||||||
|
Renders a nodeinfo.
|
||||||
|
"""
|
||||||
|
def index(url) do
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
links: [
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||||
|
href: "#{url}/nodeinfo/2.0"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a nodeinfo.
|
||||||
|
"""
|
||||||
|
def show(version, users, instance) do
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
version: "2.0",
|
||||||
|
software:
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
name: "nulla",
|
||||||
|
version: version
|
||||||
|
),
|
||||||
|
protocols: [
|
||||||
|
"activitypub"
|
||||||
|
],
|
||||||
|
services:
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
outbound: [],
|
||||||
|
inbound: []
|
||||||
|
),
|
||||||
|
usage:
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
users:
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
total: users.total,
|
||||||
|
activeMonth: users.month,
|
||||||
|
activeHalfyear: users.halfyear
|
||||||
|
)
|
||||||
|
),
|
||||||
|
openRegistrations: instance.registration,
|
||||||
|
metadata:
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
nodeName: instance.name,
|
||||||
|
nodeDescription: instance.description
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
32
lib/nulla_web/controllers/generic/webfinger_controller.ex
Normal file
32
lib/nulla_web/controllers/generic/webfinger_controller.ex
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
defmodule NullaWeb.Generic.WebfingerController do
|
||||||
|
use NullaWeb, :controller
|
||||||
|
alias Nulla.Actors
|
||||||
|
alias Nulla.Actors.Actor
|
||||||
|
alias NullaWeb.Generic.WebfingerJSON
|
||||||
|
|
||||||
|
def index(conn, %{"resource" => resource}) do
|
||||||
|
case Regex.run(~r/^acct:(.+)$/, resource) do
|
||||||
|
[_, acct] ->
|
||||||
|
case Actors.get_actor_by(acct: acct) do
|
||||||
|
nil ->
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("text/plain")
|
||||||
|
|> send_resp(404, "")
|
||||||
|
|
||||||
|
%Actor{} = actor ->
|
||||||
|
json(conn, WebfingerJSON.show(actor))
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("text/plain")
|
||||||
|
|> send_resp(400, "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def index(conn, _params) do
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("text/plain")
|
||||||
|
|> send_resp(400, "")
|
||||||
|
end
|
||||||
|
end
|
50
lib/nulla_web/controllers/generic/webfinger_json.ex
Normal file
50
lib/nulla_web/controllers/generic/webfinger_json.ex
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
defmodule NullaWeb.Generic.WebfingerJSON do
|
||||||
|
alias Nulla.Actors.Actor
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a webfinger.
|
||||||
|
"""
|
||||||
|
def show(actor) do
|
||||||
|
data(actor)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp data(%Actor{} = actor) do
|
||||||
|
data = [
|
||||||
|
subject: actor.acct,
|
||||||
|
aliases: [
|
||||||
|
actor.url,
|
||||||
|
actor.ap_id
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
rel: "http://webfinger.net/rel/profile-page",
|
||||||
|
type: "text/html",
|
||||||
|
href: actor.url
|
||||||
|
),
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
rel: "self",
|
||||||
|
type: "application/activity+json",
|
||||||
|
href: actor.ap_id
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
data =
|
||||||
|
if actor.icon do
|
||||||
|
Keyword.update!(data, :links, fn links ->
|
||||||
|
links ++
|
||||||
|
[
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
rel: "http://webfinger.net/rel/avatar",
|
||||||
|
type: Map.get(actor.icon, :mediaType),
|
||||||
|
href: Map.get(actor.icon, :url)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
data
|
||||||
|
end
|
||||||
|
|
||||||
|
Jason.OrderedObject.new(data)
|
||||||
|
end
|
||||||
|
end
|
|
@ -18,13 +18,17 @@ defmodule NullaWeb.Router do
|
||||||
plug :fetch_api_user
|
plug :fetch_api_user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
pipeline :activitypub do
|
||||||
|
plug :accepts, ["activity+json"]
|
||||||
|
end
|
||||||
|
|
||||||
scope "/", NullaWeb do
|
scope "/", NullaWeb do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
get "/", PageController, :home
|
get "/", PageController, :home
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/api", NullaWeb do
|
scope "/api", NullaWeb.Api, as: :api do
|
||||||
pipe_through :api
|
pipe_through :api
|
||||||
|
|
||||||
resources "/actors", ActorController, except: [:new, :edit]
|
resources "/actors", ActorController, except: [:new, :edit]
|
||||||
|
@ -34,6 +38,28 @@ defmodule NullaWeb.Router do
|
||||||
resources "/activities", ActivityController, except: [:new, :edit]
|
resources "/activities", ActivityController, except: [:new, :edit]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scope "/", NullaWeb.ActivityPub, as: :activitypub do
|
||||||
|
pipe_through :activitypub
|
||||||
|
|
||||||
|
post "/inbox", InboxController, :inbox
|
||||||
|
|
||||||
|
scope "/users/:username" do
|
||||||
|
get "/", ActorController, :show
|
||||||
|
get "/following", FollowController, :following
|
||||||
|
get "/followers", FollowController, :followers
|
||||||
|
post "/inbox", InboxController, :inbox
|
||||||
|
get "/outbox", OutboxController, :index
|
||||||
|
get "/notes/:id", NoteController, :show
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scope "/", NullaWeb.Generic, as: :generic do
|
||||||
|
get "/.well-known/host-meta", HostmetaController, :index
|
||||||
|
get "/.well-known/webfinger", WebfingerController, :index
|
||||||
|
get "/.well-known/nodeinfo", NodeinfoController, :index
|
||||||
|
get "/nodeinfo/2.0", NodeinfoController, :show
|
||||||
|
end
|
||||||
|
|
||||||
# Enable LiveDashboard and Swoosh mailbox preview in development
|
# Enable LiveDashboard and Swoosh mailbox preview in development
|
||||||
if Application.compile_env(:nulla, :dev_routes) do
|
if Application.compile_env(:nulla, :dev_routes) do
|
||||||
# If you want to use the LiveDashboard in production, you should put
|
# If you want to use the LiveDashboard in production, you should put
|
||||||
|
|
|
@ -9,6 +9,7 @@ defmodule Nulla.Repo.Migrations.CreateUsersAuthTables do
|
||||||
add :email, :citext, null: false
|
add :email, :citext, null: false
|
||||||
add :hashed_password, :string, null: false
|
add :hashed_password, :string, null: false
|
||||||
add :confirmed_at, :utc_datetime
|
add :confirmed_at, :utc_datetime
|
||||||
|
add :last_active_at, :utc_datetime
|
||||||
|
|
||||||
timestamps(type: :utc_datetime)
|
timestamps(type: :utc_datetime)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateRelations do
|
||||||
use Ecto.Migration
|
use Ecto.Migration
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
create table(:relations) do
|
create table(:relations, primary_key: false) do
|
||||||
|
add :id, :bigint, primary_key: true
|
||||||
add :following, :boolean, default: false, null: false
|
add :following, :boolean, default: false, null: false
|
||||||
add :followed_by, :boolean, default: false, null: false
|
add :followed_by, :boolean, default: false, null: false
|
||||||
add :showing_replies, :boolean, default: false, null: false
|
add :showing_replies, :boolean, default: false, null: false
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.ActivityControllerTest do
|
defmodule NullaWeb.Api.ActivityControllerTest do
|
||||||
use NullaWeb.ConnCase
|
use NullaWeb.ConnCase
|
||||||
|
|
||||||
import Nulla.ActivitiesFixtures
|
import Nulla.ActivitiesFixtures
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.ActorControllerTest do
|
defmodule NullaWeb.Api.ActorControllerTest do
|
||||||
use NullaWeb.ConnCase
|
use NullaWeb.ConnCase
|
||||||
|
|
||||||
import Nulla.ActorsFixtures
|
import Nulla.ActorsFixtures
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.MediaAttachmentControllerTest do
|
defmodule NullaWeb.Api.MediaAttachmentControllerTest do
|
||||||
use NullaWeb.ConnCase
|
use NullaWeb.ConnCase
|
||||||
|
|
||||||
import Nulla.NotesFixtures
|
import Nulla.NotesFixtures
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.NoteControllerTest do
|
defmodule NullaWeb.Api.NoteControllerTest do
|
||||||
use NullaWeb.ConnCase
|
use NullaWeb.ConnCase
|
||||||
|
|
||||||
import Nulla.ActorsFixtures
|
import Nulla.ActorsFixtures
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule NullaWeb.RelationControllerTest do
|
defmodule NullaWeb.Api.RelationControllerTest do
|
||||||
use NullaWeb.ConnCase
|
use NullaWeb.ConnCase
|
||||||
|
|
||||||
import Nulla.ActorsFixtures
|
import Nulla.ActorsFixtures
|
Loading…
Add table
Add a link
Reference in a new issue