Change structure

This commit is contained in:
Mirai Kumiko 2025-07-01 10:15:06 +02:00
parent 01c2c57933
commit ddfb58c041
Signed by: miraikumiko
GPG key ID: 3F178B1B5E0CB278
16 changed files with 153 additions and 66 deletions

View file

@ -0,0 +1,21 @@
defmodule NullaWeb.ActivityPub.ActorController do
use NullaWeb, :controller
alias Nulla.ActivityPub
alias Nulla.Models.Actor
def show(conn, %{"username" => username}) do
domain = NullaWeb.Endpoint.host()
case Actor.get_actor(preferredUsername: username, domain: 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!(ActivityPub.actor(actor)))
end
end
end

View file

@ -0,0 +1,67 @@
defmodule NullaWeb.ActivityPub.FollowController do
use NullaWeb, :controller
alias Nulla.ActivityPub
alias Nulla.Models.Actor
alias Nulla.Models.Relation
alias Nulla.Models.InstanceSettings
def following(conn, %{"username" => username, "page" => page_param}) do
instance_settings = InstanceSettings.get_instance_settings!()
domain = NullaWeb.Endpoint.host()
limit = instance_settings.api_limit
actor = Actor.get_actor(preferredUsername: username, domain: domain)
total = Relation.count_following(actor.id)
page =
case Integer.parse(page_param) do
{int, _} when int > 0 -> int
_ -> 1
end
following_list = Enum.map(Relation.get_following(actor.id, page, limit), & &1.ap_id)
conn
|> put_resp_content_type("application/activity+json")
|> json(ActivityPub.following(actor, total, following_list, page, limit))
end
def following(conn, %{"username" => username}) do
domain = NullaWeb.Endpoint.host()
actor = Actor.get_actor(preferredUsername: username, domain: domain)
total = Relation.count_following(actor.id)
conn
|> put_resp_content_type("application/activity+json")
|> json(ActivityPub.following(actor, total))
end
def followers(conn, %{"username" => username, "page" => page_param}) do
instance_settings = InstanceSettings.get_instance_settings!()
domain = NullaWeb.Endpoint.host()
limit = instance_settings.api_limit
actor = Actor.get_actor(preferredUsername: username, domain: domain)
total = Relation.count_followers(actor.id)
page =
case Integer.parse(page_param) do
{int, _} when int > 0 -> int
_ -> 1
end
followers_list = Enum.map(Relation.get_followers(actor.id, page, limit), & &1.ap_id)
conn
|> put_resp_content_type("application/activity+json")
|> json(ActivityPub.followers(actor, total, followers_list, page, limit))
end
def followers(conn, %{"username" => username}) do
domain = NullaWeb.Endpoint.host()
actor = Actor.get_actor(preferredUsername: username, domain: domain)
total = Relation.count_followers(actor.id)
conn
|> put_resp_content_type("application/activity+json")
|> json(ActivityPub.followers(actor, total))
end
end

View file

@ -0,0 +1,18 @@
defmodule NullaWeb.ActivityPub.HostmetaController do
use NullaWeb, :controller
def index(conn, _params) do
domain = NullaWeb.Endpoint.host()
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="https://#{domain}/.well-known/webfinger?resource={uri}"/>
</XRD>
"""
conn
|> put_resp_content_type("application/xrd+xml")
|> send_resp(200, xml)
end
end

View file

@ -0,0 +1,215 @@
defmodule NullaWeb.ActivityPub.InboxController do
use NullaWeb, :controller
alias Nulla.Snowflake
alias Nulla.HTTPSignature
alias Nulla.Sender
alias Nulla.Utils
alias Nulla.Models.User
alias Nulla.Models.Actor
alias Nulla.Models.Relation
alias Nulla.Models.Activity
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 <- Actor.get_actor(ap_id: target_uri),
{:ok, remote_actor_json} <- Utils.fetch_remote_actor(actor_uri),
:ok <- HTTPSignature.verify(conn, remote_actor_json["publicKey"]["publicKeyPem"]),
{:ok, remote_actor} <- Actor.get_or_create_actor(remote_actor_json),
{:ok, follow_activity} <-
Activity.create_activity(%{
ap_id: follow_id,
type: "Follow",
actor: remote_actor.ap_id,
object: target_uri
}),
{:ok, accept_activity} <-
Activity.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)
}),
{:ok, _relation} <-
Relation.get_or_create_relation(local_actor.id, remote_actor.id, followed_by: true) do
user = User.get_user(id: local_actor.id)
Sender.send_activity(
:post,
remote_actor.inbox,
accept_activity,
local_actor.publicKey["id"],
user.privateKeyPem
)
send_resp(conn, 200, "")
else
error ->
IO.inspect(error, label: "Follow error")
json(conn, %{"error" => "Failed to process Follow"})
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

View file

@ -0,0 +1,29 @@
defmodule NullaWeb.ActivityPub.NodeinfoController do
use NullaWeb, :controller
alias Nulla.ActivityPub
alias Nulla.Models.User
alias Nulla.Models.InstanceSettings
def index(conn, _params) do
domain = NullaWeb.Endpoint.host()
json(conn, ActivityPub.nodeinfo(domain))
end
def show(conn, _params) do
version = Application.spec(:nulla, :vsn) |> to_string()
total = User.get_total_users_count()
month = User.get_active_users_count(30)
halfyear = User.get_active_users_count(180)
users = %{
total: total,
month: month,
halfyear: halfyear
}
instance_settings = InstanceSettings.get_instance_settings!()
json(conn, ActivityPub.nodeinfo(version, users, instance_settings))
end
end

View file

@ -0,0 +1,44 @@
defmodule NullaWeb.ActivityPub.NoteController do
use NullaWeb, :controller
alias Nulla.Repo
alias Nulla.ActivityPub
alias Nulla.Models.Note
def show(conn, %{"username" => username, "id" => id}) do
case Integer.parse(id) do
{int_id, ""} ->
note = Note.get_note(id: 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 ->
format = Phoenix.Controller.get_format(conn)
if format == "activity+json" do
conn
|> put_resp_content_type("application/activity+json")
|> json(ActivityPub.note(note))
else
render(conn, :show, note: note, layout: false)
end
end
_ ->
conn
|> put_status(:not_found)
|> json(%{error: "Not Found"})
|> halt()
end
end
end

View file

@ -0,0 +1,51 @@
defmodule NullaWeb.ActivityPub.OutboxController do
use NullaWeb, :controller
alias Nulla.ActivityPub
alias Nulla.Models.Actor
alias Nulla.Models.Note
def outbox(conn, %{"username" => username} = params) do
domain = NullaWeb.Endpoint.host()
actor = Actor.get_actor(preferredUsername: username, domain: domain)
case Map.get(params, "page") do
"true" ->
max_id = params["max_id"] && String.to_integer(params["max_id"])
notes =
if max_id do
Note.get_before_notes(actor.id, max_id)
else
Note.get_latest_notes(actor.id)
end
items = Enum.map(notes, &ActivityPub.activity_note(&1))
next_max_id =
case List.last(notes) do
nil -> 0
last -> last.id
end
min_id =
case List.first(notes) do
nil -> 0
first -> first.id
end
conn
|> put_resp_content_type("application/activity+json")
|> send_resp(
200,
Jason.encode!(ActivityPub.outbox(actor, next_max_id, min_id || 0, items))
)
_ ->
total = Note.get_total_notes_count(actor.id)
conn
|> put_resp_content_type("application/activity+json")
|> send_resp(200, Jason.encode!(ActivityPub.outbox(actor, total)))
end
end
end

View file

@ -0,0 +1,39 @@
defmodule NullaWeb.ActivityPub.WebfingerController do
use NullaWeb, :controller
alias Nulla.ActivityPub
alias Nulla.Models.Actor
def index(conn, %{"resource" => resource}) do
case Regex.run(~r/^acct:(.+)@(.+)$/, resource) do
[_, preferredUsername, actor_domain] ->
case Actor.get_actor(preferredUsername: preferredUsername, domain: actor_domain) do
nil ->
conn
|> put_resp_content_type("text/plain")
|> send_resp(404, "")
%Actor{} = actor ->
domain = NullaWeb.Endpoint.host()
if actor_domain == domain do
json(conn, ActivityPub.webfinger(actor))
else
conn
|> put_resp_content_type("text/plain")
|> send_resp(404, "")
end
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