This commit is contained in:
Mirai Kumiko 2025-06-17 12:06:36 +02:00
parent 58049c93d4
commit 894866ca03
Signed by: miraikumiko
GPG key ID: 3F178B1B5E0CB278
22 changed files with 344 additions and 213 deletions

View file

@ -25,63 +25,35 @@ defmodule Nulla.ActivityPub do
]
end
@spec user(String.t(), Nulla.Models.User.t()) :: Jason.OrderedObject.t()
def user(domain, user) do
@spec actor(Nulla.Models.Actor.t()) :: Jason.OrderedObject.t()
def actor(actor) do
Jason.OrderedObject.new(
"@context": context(),
id: "https://#{domain}/@#{user.username}",
type: "Person",
following: "https://#{domain}/@#{user.username}/following",
followers: "https://#{domain}/@#{user.username}/followers",
inbox: "https://#{domain}/@#{user.username}/inbox",
outbox: "https://#{domain}/@#{user.username}/outbox",
featured: "https://#{domain}/@#{user.username}/collections/featured",
preferredUsername: user.username,
name: user.realname,
summary: user.bio,
url: "https://#{domain}/@#{user.username}",
manuallyApprovesFollowers: user.follow_approval,
discoverable: user.is_discoverable,
indexable: user.is_indexable,
published: DateTime.to_iso8601(user.inserted_at),
memorial: user.is_memorial,
publicKey:
Jason.OrderedObject.new(
id: "https://#{domain}/@#{user.username}#main-key",
owner: "https://#{domain}/@#{user.username}",
publicKeyPem: user.public_key
),
tag:
Enum.map(user.tags, fn tag ->
Jason.OrderedObject.new(
type: "Hashtag",
href: "https://#{domain}/tags/#{tag}",
name: "##{tag}"
)
end),
attachment:
Enum.map(user.fields, fn {name, value} ->
Jason.OrderedObject.new(
type: "PropertyValue",
name: name,
value: value
)
end),
endpoints: Jason.OrderedObject.new(sharedInbox: "https://#{domain}/inbox"),
icon:
Jason.OrderedObject.new(
type: "Image",
mediaType: MIME.from_path(user.avatar),
url: "https://#{domain}/files/#{user.avatar}"
),
image:
Jason.OrderedObject.new(
type: "Image",
mediaType: MIME.from_path(user.banner),
url: "https://#{domain}/files/#{user.banner}"
),
"vcard:bday": user.birthday,
"vcard:Address": user.location
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: DateTime.to_iso8601(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
@ -110,18 +82,18 @@ defmodule Nulla.ActivityPub do
"https://www.w3.org/ns/activitystreams",
Jason.OrderedObject.new(sensitive: "as:sensitive")
],
id: "https://#{domain}/@#{note.user.username}/#{note.id}",
id: "https://#{domain}/users/#{note.actor.preferredUsername}/statuses/#{note.id}",
type: "Note",
summary: nil,
inReplyTo: nil,
published: note.inserted_at,
url: "https://#{domain}/@#{note.user.username}/#{note.id}",
attributedTo: "https://#{domain}/@#{note.user.username}",
url: "https://#{domain}/@#{note.actor.preferredUsername}/#{note.id}",
attributedTo: "https://#{domain}/users/#{note.actor.preferredUsername}",
to: [
"https://www.w3.org/ns/activitystreams#Public"
],
cc: [
"https://#{domain}/@#{note.user.username}/followers"
"https://#{domain}/users/#{note.actor.preferredUsername}/followers"
],
sensetive: false,
content: note.content,
@ -141,35 +113,35 @@ defmodule Nulla.ActivityPub do
)
end
@spec following(String.t(), Nulla.Models.User.t(), Integer.t()) :: Jason.OrderedObject.t()
def following(domain, user, total) do
@spec following(String.t(), Nulla.Models.Actor.t(), Integer.t()) :: Jason.OrderedObject.t()
def following(domain, actor, total) do
Jason.OrderedObject.new(
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://#{domain}/@#{user.username}/following",
id: "https://#{domain}/users/#{actor.preferredUsername}/following",
type: "OrderedCollection",
totalItems: total,
first: "https://#{domain}/@#{user.username}/following?page=1"
first: "https://#{domain}/users/#{actor.preferredUsername}/following?page=1"
)
end
@spec following(
String.t(),
Nulla.Models.User.t(),
Nulla.Models.Actor.t(),
Integer.t(),
List.t(),
Integer.t(),
Integer.t()
) :: Jason.OrderedObject.t()
def following(domain, user, total, following_list, page, offset)
def following(domain, actor, total, following_list, page, offset)
when is_integer(page) and page > 0 do
data = [
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://#{domain}/@#{user.username}/following?page=#{page}",
id: "https://#{domain}/@#{actor.preferredUsername}/following?page=#{page}",
type: "OrderedCollectionPage",
totalItems: total,
next: "https://#{domain}/@#{user.username}/following?page=#{page + 1}",
prev: "https://#{domain}/@#{user.username}/following?page=#{page - 1}",
partOf: "https://#{domain}/@#{user.username}/following",
next: "https://#{domain}/users/#{actor.preferredUsername}/following?page=#{page + 1}",
prev: "https://#{domain}/users/#{actor.preferredUsername}/following?page=#{page - 1}",
partOf: "https://#{domain}/users/#{actor.preferredUsername}/following",
orderedItems: following_list
]
@ -192,35 +164,35 @@ defmodule Nulla.ActivityPub do
Jason.OrderedObject.new(data)
end
@spec followers(String.t(), Nulla.Models.User.t(), Integer.t()) :: Jason.OrderedObject.t()
def followers(domain, user, total) do
@spec followers(String.t(), Nulla.Models.Actor.t(), Integer.t()) :: Jason.OrderedObject.t()
def followers(domain, actor, total) do
Jason.OrderedObject.new(
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://#{domain}/@#{user.username}/followers",
id: "https://#{domain}/users/#{actor.preferredUsername}/followers",
type: "OrderedCollection",
totalItems: total,
first: "https://#{domain}/@#{user.username}/followers?page=1"
first: "https://#{domain}/users/#{actor.preferredUsername}/followers?page=1"
)
end
@spec followers(
String.t(),
Nulla.Models.User.t(),
Nulla.Models.Actor.t(),
Integer.t(),
List.t(),
Integer.t(),
Integer.t()
) :: Jason.OrderedObject.t()
def followers(domain, user, total, followers_list, page, offset)
def followers(domain, actor, total, followers_list, page, offset)
when is_integer(page) and page > 0 do
data = [
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://#{domain}/@#{user.username}/followers?page=#{page}",
id: "https://#{domain}/users#{actor.preferredUsername}/followers?page=#{page}",
type: "OrderedCollectionPage",
totalItems: total,
next: "https://#{domain}/@#{user.username}/followers?page=#{page + 1}",
prev: "https://#{domain}/@#{user.username}/followers?page=#{page - 1}",
partOf: "https://#{domain}/@#{user.username}/followers",
next: "https://#{domain}/users/#{actor.preferredUsername}/followers?page=#{page + 1}",
prev: "https://#{domain}/users/#{actor.preferredUsername}/followers?page=#{page - 1}",
partOf: "https://#{domain}/users/#{actor.preferredUsername}/followers",
orderedItems: followers_list
]
@ -243,15 +215,29 @@ defmodule Nulla.ActivityPub do
Jason.OrderedObject.new(data)
end
@spec webfinger(String.t(), String.t(), String.t()) :: Jason.OrderedObject.t()
def webfinger(domain, username, resource) do
@spec webfinger(Nulla.Models.Actor.t()) :: Jason.OrderedObject.t()
def webfinger(actor) do
Jason.OrderedObject.new(
subject: resource,
subject: "#{actor.preferredUsername}@#{actor.domain}",
aliases: [
"https://#{actor.domain}/@#{actor.preferredUsername}",
"https://#{actor.domain}/users/#{actor.preferredUsername}"
],
links: [
Jason.OrderedObject.new(
rel: "http://webfinger.net/rel/profile-page",
type: "text/html",
href: "https://#{actor.domain}/users/#{actor.preferredUsername}"
),
Jason.OrderedObject.new(
rel: "self",
type: "application/activity+json",
href: "https://#{domain}/@#{username}"
href: "https://#{actor.domain}/users/#{actor.preferredUsername}"
),
Jason.OrderedObject.new(
rel: "http://webfinger.net/rel/avatar",
type: actor.icon.mediaType,
href: actor.icon.url
)
]
)
@ -309,11 +295,11 @@ defmodule Nulla.ActivityPub do
def outbox(domain, username, total) do
Jason.OrderedObject.new(
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://#{domain}/@#{username}/outbox",
id: "https://#{domain}/users/#{username}/outbox",
type: "OrderedCollection",
totalItems: total,
first: "https://#{domain}/@#{username}/outbox?page=true",
last: "https://#{domain}/@#{username}/outbox?min_id=0&page=true"
first: "https://#{domain}/users/#{username}/outbox?page=true",
last: "https://#{domain}/users/#{username}/outbox?min_id=0&page=true"
)
end
@ -328,32 +314,49 @@ defmodule Nulla.ActivityPub do
Hashtag: "as:Hashtag"
)
],
id: "https://#{domain}/@#{username}/outbox?page=true",
id: "https://#{domain}/users/#{username}/outbox?page=true",
type: "OrderedCollectionPage",
next: "https://#{domain}/@#{username}/outbox?max_id=#{max_id}&page=true",
prev: "https://#{domain}/@#{username}/outbox?min_id=#{min_id}&page=true",
partOf: "https://#{domain}/@#{username}/outbox",
next: "https://#{domain}/users/#{username}/outbox?max_id=#{max_id}&page=true",
prev: "https://#{domain}/users/#{username}/outbox?min_id=#{min_id}&page=true",
partOf: "https://#{domain}/users/#{username}/outbox",
orderedItems: items
)
end
@spec render_activity(String.t(), Note.t()) :: Jason.OrderedObject.t()
def render_activity(domain, note) do
@spec activity_note(Nulla.Models.Note.t()) :: Jason.OrderedObject.t()
def activity_note(note) do
Jason.OrderedObject.new(
id: "https://#{domain}/@#{note.user.username}/#{note.id}/activity",
id:
"https://#{note.actor.domain}/users/#{note.actor.preferredUsername}/#{note.id}/activity",
type: "Create",
actor: "https://#{domain}/@#{note.user.username}",
actor: "https://#{note.actor.domain}/users/#{note.actor.preferredUsername}",
published: note.inserted_at |> DateTime.to_iso8601(),
to: ["https://www.w3.org/ns/activitystreams#Public"],
to: [
"https://www.w3.org/ns/activitystreams#Public"
],
object:
Jason.OrderedObject.new(
id: "https://#{domain}/@#{note.user.username}/#{note.id}",
id:
"https://#{note.actor.domain}/users/#{note.actor.preferredUsername}/statuses/#{note.id}",
type: "Note",
content: note.content,
published: note.inserted_at |> DateTime.to_iso8601(),
attributedTo: "https://#{domain}/@#{note.user.username}",
to: ["https://www.w3.org/ns/activitystreams#Public"]
attributedTo: "https://#{note.actor.domain}/users/#{note.actor.preferredUsername}",
to: [
"https://www.w3.org/ns/activitystreams#Public"
]
)
)
end
@spec follow_accept(Nulla.Models.Activity.t()) :: Jason.OrderedObject.t()
def follow_accept(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
)
end
end

View file

@ -0,0 +1,5 @@
defmodule Nulla.HTTPSignature do
def verify(_conn, _actor) do
:ok
end
end

View file

@ -1,9 +1,11 @@
defmodule Nulla.Models.Activity do
use Ecto.Schema
import Ecto.Changeset
alias Nulla.SnowFlake
@primary_key {:id, :integer, autogenerate: false}
schema "activities" do
field :ap_id, :string
field :type, :string
field :actor, :string
field :object, :map
@ -15,8 +17,17 @@ defmodule Nulla.Models.Activity do
@doc false
def changeset(activity, attrs) do
activity
|> cast(attrs, [:type, :actor, :object, :to])
|> validate_required([:type, :actor, :object])
|> cast(attrs, [:ap_id, :type, :actor, :object, :to])
|> validate_required([:ap_id, :type, :actor, :object])
|> validate_inclusion(:type, ~w(Create Update Delete Undo Like Announce Follow Accept Reject))
end
def create_activity(attrs) do
id = Snowflake.next_id()
%__MODULE__{}
|> __MODULE__.changeset(attrs)
|> Ecto.Changeset.put_change(:id, id)
|> Repo.insert()
end
end

View file

@ -1,14 +1,14 @@
defmodule Nulla.Models.Actor do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Nulla.Repo
alias Nulla.Snowflake
alias Nulla.Models.User
alias Nulla.Models.Note
@primary_key {:id, :integer, autogenerate: false}
schema "actors" do
field :domain, :string
field :ap_id, :string
field :type, :string
field :following, :string
field :followers, :string
@ -34,7 +34,6 @@ defmodule Nulla.Models.Actor do
field :vcard_bday, :date
field :vcard_Address, :string
has_one :user, User
has_many :notes, Note
has_many :media_attachments, through: [:notes, :media_attachments]
end
@ -44,6 +43,8 @@ defmodule Nulla.Models.Actor do
actor
|> cast(attrs, [
:id,
:domain,
:ap_id,
:type,
:following,
:followers,
@ -71,6 +72,8 @@ defmodule Nulla.Models.Actor do
])
|> validate_required([
:id,
:domain,
:ap_id,
:type,
:following,
:followers,
@ -98,7 +101,7 @@ defmodule Nulla.Models.Actor do
])
end
def create_user(attrs) when is_map(attrs) do
def create_actor(attrs) when is_map(attrs) do
id = Snowflake.next_id()
%__MODULE__{}
@ -106,4 +109,8 @@ defmodule Nulla.Models.Actor do
|> Ecto.Changeset.put_change(:id, id)
|> Repo.insert()
end
def get_actor(username, domain) do
Repo.get_by(__MODULE__, preferredUsername: username, domain: domain)
end
end

View file

@ -1,13 +1,14 @@
defmodule Nulla.Models.Follow do
use Ecto.Schema
import Ecto.Changeset
alias Nulla.Repo
alias Nulla.Snowflake
alias Nulla.Models.Follow
alias Nulla.Models.Actor
@primary_key {:id, :integer, autogenerate: false}
schema "follows" do
belongs_to :user, Nulla.Models.User
belongs_to :target, Nulla.Models.User
belongs_to :follower, Actor
belongs_to :followed, Actor
timestamps()
end
@ -23,8 +24,8 @@ defmodule Nulla.Models.Follow do
def create_follow(attrs) do
id = Snowflake.next_id()
%Follow{}
|> Follow.changeset(attrs)
%__MODULE__{}
|> __MODULE__.changeset(attrs)
|> Ecto.Changeset.put_change(:id, id)
|> Repo.insert()
end

View file

@ -3,7 +3,8 @@ defmodule Nulla.Models.Note do
import Ecto.Changeset
import Ecto.Query
alias Nulla.Repo
alias Nulla.Models.Note
alias Nulla.Models.Actor
alias Nulla.Models.MediaAttachment
@primary_key {:id, :integer, autogenerate: false}
schema "notes" do
@ -17,8 +18,8 @@ defmodule Nulla.Models.Note do
field :language, :string
field :in_reply_to, :string
belongs_to :user, Nulla.Models.User
has_many :media_attachments, Nulla.Models.MediaAttachment
belongs_to :actor, Actor
has_many :media_attachments, MediaAttachment
timestamps(type: :utc_datetime)
end
@ -26,32 +27,32 @@ defmodule Nulla.Models.Note do
@doc false
def changeset(note, attrs) do
note
|> cast(attrs, [:content, :visibility, :sensitive, :language, :in_reply_to, :user_id])
|> validate_required([:content, :visibility, :sensitive, :language, :in_reply_to, :user_id])
|> cast(attrs, [:content, :visibility, :sensitive, :language, :in_reply_to, :actor_id])
|> validate_required([:content, :visibility, :sensitive, :language, :in_reply_to, :actor_id])
end
def get_note!(id), do: Repo.get!(Note, id)
def get_note!(id), do: Repo.get!(__MODULE__, id)
def get_latest_notes(user_id, limit \\ 20) do
from(n in Note,
where: n.user_id == ^user_id,
def get_latest_notes(actor_id, limit \\ 20) do
from(n in __MODULE__,
where: n.actor_id == ^actor_id,
order_by: [desc: n.inserted_at],
limit: ^limit
)
|> Repo.all()
end
def get_before_notes(user_id, max_id, limit \\ 20) do
from(n in Note,
where: n.user_id == ^user_id and n.id < ^max_id,
def get_before_notes(actor_id, max_id, limit \\ 20) do
from(n in __MODULE__,
where: n.actor_id == ^actor_id and n.id < ^max_id,
order_by: [desc: n.inserted_at],
limit: ^limit
)
|> Nulla.Repo.all()
|> Repo.all()
end
def get_total_notes_count(user_id) do
from(n in Note, where: n.user_id == ^user_id)
def get_total_notes_count(actor_id) do
from(n in __MODULE__, where: n.actor_id == ^actor_id)
|> Repo.aggregate(:count, :id)
end
end

View file

@ -0,0 +1,30 @@
defmodule Nulla.Models.Relation do
use Ecto.Schema
import Ecto.Changeset
alias Nulla.Models.Actor
alias Nulla.Models.Activity
@primary_key {:id, :integer, autogenerate: false}
schema "relations" do
field :type, :string
field :status, :string
belongs_to :source, Actor, foreign_key: :source_id, type: :integer
belongs_to :target, Actor, foreign_key: :target_id, type: :integer
belongs_to :activity, Activity, foreign_key: :activity_id, type: :integer
timestamps()
end
def changeset(relation, attrs) do
relation
|> cast(attrs, [:id, :source_id, :target_id, :type, :status, :activity_id])
|> validate_required([:id, :source_id, :target_id, :type])
|> validate_inclusion(:type, ~w(follow block mute friend_request))
|> validate_inclusion(:status, ~w(pending accepted rejected active))
|> foreign_key_constraint(:source_id)
|> foreign_key_constraint(:target_id)
|> foreign_key_constraint(:activity_id)
|> unique_constraint([:source_id, :target_id, :type])
end
end

View file

@ -3,7 +3,6 @@ defmodule Nulla.Models.User do
import Ecto.Changeset
import Ecto.Query
alias Nulla.Repo
alias Nulla.Snowflake
alias Nulla.Models.User
alias Nulla.Models.Actor
alias Nulla.Models.Session
@ -48,8 +47,6 @@ defmodule Nulla.Models.User do
def get_user_by_username(username), do: Repo.get_by(User, username: username)
def get_user_by_username!(username), do: Repo.get_by!(User, username: username)
def get_user_by_username_and_domain(username, domain) do
from(u in User,
where: u.username == ^username and u.domain == ^domain

View file

@ -0,0 +1,43 @@
defmodule NullaWeb.ActorController do
use NullaWeb, :controller
alias Nulla.ActivityPub
alias Nulla.Utils
alias Nulla.Models.Actor
alias Nulla.Models.Note
alias Nulla.Models.InstanceSettings
def show(conn, %{"username" => username}) do
accept = List.first(get_req_header(conn, "accept"))
instance_settings = InstanceSettings.get_instance_settings!()
domain = instance_settings.domain
case Actor.get_actor(username, domain) do
nil ->
conn
|> put_status(:not_found)
|> json(%{error: "Not Found"})
%Actor{} = actor ->
if accept in ["application/activity+json", "application/ld+json"] do
conn
|> put_resp_content_type("application/activity+json")
|> send_resp(200, Jason.encode!(ActivityPub.actor(actor)))
else
notes = Note.get_latest_notes(actor.id)
following = Utils.count_following_by_username!(actor.preferredUsername)
followers = Utils.count_followers_by_username!(actor.preferredUsername)
render(
conn,
:show,
domain: domain,
actor: actor,
notes: notes,
following: following,
followers: followers,
layout: false
)
end
end
end
end

View file

@ -0,0 +1,12 @@
defmodule NullaWeb.AuthController do
use NullaWeb, :controller
def sign_in do
end
def sign_out do
end
def sign_up do
end
end

View file

@ -2,15 +2,15 @@ defmodule NullaWeb.FollowController do
use NullaWeb, :controller
alias Nulla.ActivityPub
alias Nulla.Utils
alias Nulla.Models.User
alias Nulla.Models.Actor
alias Nulla.Models.InstanceSettings
def following(conn, %{"username" => username, "page" => page_param}) do
instance_settings = InstanceSettings.get_instance_settings!()
domain = instance_settings.domain
offset = instance_settings.offset
user = User.get_user_by_username!(username)
total = Utils.count_following_by_username!(user.username)
actor = Actor.get_actor(username, domain)
total = Utils.count_following_by_username!(actor.preferredUsername)
page =
case Integer.parse(page_param) do
@ -18,30 +18,30 @@ defmodule NullaWeb.FollowController do
_ -> 1
end
following_list = Utils.get_following_users_by_username!(user.username, page)
following_list = Utils.get_following_users_by_username!(actor.preferredUsername, page)
conn
|> put_resp_content_type("application/activity+json")
|> json(ActivityPub.following(domain, user, total, following_list, page, offset))
|> json(ActivityPub.following(domain, actor, total, following_list, page, offset))
end
def following(conn, %{"username" => username}) do
instance_settings = InstanceSettings.get_instance_settings!()
domain = instance_settings.domain
user = User.get_user_by_username!(username)
total = Utils.count_following_by_username!(user.username)
actor = Actor.get_actor(username, domain)
total = Utils.count_following_by_username!(actor.preferredUsername)
conn
|> put_resp_content_type("application/activity+json")
|> json(ActivityPub.following(domain, user, total))
|> json(ActivityPub.following(domain, actor, total))
end
def followers(conn, %{"username" => username, "page" => page_param}) do
instance_settings = InstanceSettings.get_instance_settings!()
domain = instance_settings.domain
offset = instance_settings.offset
user = User.get_user_by_username!(username)
total = Utils.count_followers_by_username!(user.username)
actor = Actor.get_actor(username, domain)
total = Utils.count_followers_by_username!(actor.preferredUsername)
page =
case Integer.parse(page_param) do
@ -49,21 +49,21 @@ defmodule NullaWeb.FollowController do
_ -> 1
end
followers_list = Utils.get_followers_by_username!(user.username, page)
followers_list = Utils.get_followers_by_username!(actor.preferredUsername, page)
conn
|> put_resp_content_type("application/activity+json")
|> json(ActivityPub.followers(domain, user, total, followers_list, page, offset))
|> json(ActivityPub.followers(domain, actor, total, followers_list, page, offset))
end
def followers(conn, %{"username" => username}) do
instance_settings = InstanceSettings.get_instance_settings!()
domain = instance_settings.domain
user = User.get_user_by_username!(username)
total = Utils.count_followers_by_username!(user.username)
actor = Actor.get_actor(username, domain)
total = Utils.count_followers_by_username!(actor.preferredUsername)
conn
|> put_resp_content_type("application/activity+json")
|> json(ActivityPub.followers(domain, user, total))
|> json(ActivityPub.followers(domain, actor, total))
end
end

View file

@ -1,19 +1,49 @@
defmodule NullaWeb.InboxController do
use NullaWeb, :controller
alias Nulla.Models.Follow
alias Nulla.HTTPSignature
alias Nulla.ActivityPub
alias Nulla.Utils
alias Nulla.Models.Actor
alias Nulla.Models.Relation
def inbox(
conn,
%{"type" => "Follow", "actor" => actor_uri, "object" => target_uri} = activity
%{"id" => follow_id, "type" => "Follow", "actor" => actor_uri, "object" => target_uri}
) do
with {:ok, target_user} <- Utils.resolve_local_actor(target_uri),
{:ok, remote_actor} <- Utils.fetch_remote_actor(actor_uri),
:ok <- HTTPSignature.verify(conn, remote_actor),
remote_user <- Follow.create_remote_user(remote_actor),
follow <- Follow.create_follow(%{user: remote_user, target: target_user}),
:ok <- Utils.send_accept_activity(remote_actor, target_user, follow, activity) do
json(conn, %{"status" => "Follow accepted"})
with {:ok, target_actor} <- Utils.resolve_local_actor(target_uri),
{:ok, remote_actor_json} <- Utils.fetch_remote_actor(actor_uri),
:ok <- HTTPSignature.verify(conn, remote_actor_json),
remote_actor <-
Actor.create_actor(
remote_actor_json
|> Map.put("ap_id", remote_actor_json["id"])
|> Map.delete("id")
|> Map.put("domain", URI.parse(remote_actor_json["id"]).host)
),
follow_activity <-
Activity.create_activity(%{
ap_id: follow_id,
type: "Follow",
actor: actor_uri,
object: target_uri
}),
accept_activity <-
Activity.create_activity(%{
type: "Accept",
actor: target_uri,
object: follow_activity
}),
relation <- Relation.create_relation(%{
id: 1,
follower: remote_actor.id,
followed: target_actor.id
}) do
conn
|> put_resp_content_type("application/activity+json")
|> send_resp(
200,
Jason.encode!(ActivityPub.follow_accept(accept_activity))
)
else
error ->
IO.inspect(error, label: "Follow error")

View file

@ -1,6 +1,5 @@
defmodule NullaWeb.NodeinfoController do
use NullaWeb, :controller
alias Nulla.Repo
alias Nulla.ActivityPub
alias Nulla.Models.User
alias Nulla.Models.InstanceSettings

View file

@ -1,7 +1,7 @@
defmodule NullaWeb.OutboxController do
use NullaWeb, :controller
alias Nulla.ActivityPub
alias Nulla.Models.User
alias Nulla.Models.Actor
alias Nulla.Models.Note
alias Nulla.Models.InstanceSettings
@ -10,17 +10,17 @@ defmodule NullaWeb.OutboxController do
"true" ->
instance_settings = InstanceSettings.get_instance_settings!()
domain = instance_settings.domain
user = User.get_user_by_username!(username)
actor = Actor.get_actor(username, domain)
max_id = params["max_id"] && String.to_integer(params["max_id"])
notes =
if max_id do
Note.get_before_notes(user.id, max_id)
Note.get_before_notes(actor.id, max_id)
else
Note.get_latest_notes(user.id)
Note.get_latest_notes(actor.id)
end
items = Enum.map(notes, &ActivityPub.render_activity(&1, domain))
items = Enum.map(notes, &ActivityPub.activity_note(&1))
next_max_id =
case List.last(notes) do
@ -44,8 +44,8 @@ defmodule NullaWeb.OutboxController do
_ ->
instance_settings = InstanceSettings.get_instance_settings!()
domain = instance_settings.domain
user = User.get_user_by_username!(username)
total = Note.get_total_notes_count(user.id)
actor = Actor.get_actor(username, domain)
total = Note.get_total_notes_count(actor.id)
conn
|> put_resp_content_type("application/activity+json")

View file

@ -1,36 +1,3 @@
defmodule NullaWeb.UserController do
use NullaWeb, :controller
alias Nulla.ActivityPub
alias Nulla.Utils
alias Nulla.Models.User
alias Nulla.Models.Note
alias Nulla.Models.InstanceSettings
def show(conn, %{"username" => username}) do
accept = List.first(get_req_header(conn, "accept"))
instance_settings = InstanceSettings.get_instance_settings!()
domain = instance_settings.domain
user = User.get_user_by_username!(username)
notes = Note.get_notes(user.id)
if accept in ["application/activity+json", "application/ld+json"] do
conn
|> put_resp_content_type("application/activity+json")
|> send_resp(200, Jason.encode!(ActivityPub.user(domain, user)))
else
following = Utils.count_following_by_username!(user.username)
followers = Utils.count_followers_by_username!(user.username)
render(
conn,
:show,
domain: domain,
user: user,
notes: notes,
following: following,
followers: followers,
layout: false
)
end
end
end

View file

@ -1,24 +1,23 @@
defmodule NullaWeb.WebfingerController do
use NullaWeb, :controller
alias Nulla.Repo
alias Nulla.ActivityPub
alias Nulla.Models.User
alias Nulla.Models.Actor
alias Nulla.Models.InstanceSettings
def index(conn, %{"resource" => resource}) do
case Regex.run(~r/^acct:([^@]+)@(.+)$/, resource) do
[_, username, domain] ->
case User.get_user_by_username(username) do
case Actor.get_actor(username, domain) do
nil ->
conn
|> put_status(:not_found)
|> json(%{error: "Not Found"})
user ->
%Actor{} = actor ->
instance_settings = InstanceSettings.get_instance_settings!()
if domain == instance_settings.domain do
json(conn, ActivityPub.webfinger(domain, username, resource))
json(conn, ActivityPub.webfinger(actor))
else
conn
|> put_status(:not_found)

View file

@ -29,7 +29,7 @@ defmodule NullaWeb.Router do
end
scope "/users/:username" do
get "/", UserController, :show
get "/", ActorController, :show
get "/following", FollowController, :following
get "/followers", FollowController, :followers
post "/inbox", InboxController, :inbox
@ -38,7 +38,7 @@ defmodule NullaWeb.Router do
end
scope "/@:username" do
get "/", UserController, :show
get "/", ActorController, :show
get "/following", FollowController, :following
get "/followers", FollowController, :followers
post "/inbox", InboxController, :inbox

View file

@ -4,14 +4,16 @@ defmodule Nulla.Repo.Migrations.CreateActors do
def change do
create table(:actors, primary_key: false) do
add :id, :bigint, primary_key: true
add :type, :string
add :following, :string
add :followers, :string
add :inbox, :string
add :outbox, :string
add :domain, :string
add :ap_id, :string, null: false
add :type, :string, null: false
add :following, :string, null: false
add :followers, :string, null: false
add :inbox, :string, null: false
add :outbox, :string, null: false
add :featured, :string
add :featuredTags, :string
add :preferredUsername, :string
add :preferredUsername, :string, null: false
add :name, :string
add :summary, :string
add :url, :string

View file

@ -9,11 +9,11 @@ defmodule Nulla.Repo.Migrations.CreateNotes do
add :sensitive, :boolean, default: false
add :language, :string
add :in_reply_to, :string
add :user_id, references(:users, on_delete: :delete_all)
add :actor_id, references(:actors, on_delete: :delete_all)
timestamps(type: :utc_datetime)
end
create index(:notes, [:user_id])
create index(:notes, [:actor_id])
end
end

View file

@ -4,13 +4,13 @@ defmodule Nulla.Repo.Migrations.CreateFollows do
def change do
create table(:follows, primary_key: false) do
add :id, :bigint, primary_key: true
add :user_id, references(:users, on_delete: :delete_all), null: false
add :target_id, references(:users, on_delete: :delete_all), null: false
add :follower_id, references(:actors, on_delete: :delete_all), null: false
add :following_id, references(:actors, on_delete: :delete_all), null: false
timestamps()
end
create unique_index(:follows, [:user_id, :target_id])
create index(:follows, [:target_id])
create unique_index(:follows, [:follower_id, :following_id])
create index(:follows, [:following_id])
end
end

View file

@ -4,15 +4,16 @@ defmodule Nulla.Repo.Migrations.CreateActivities do
def change do
create table(:activities, primary_key: false) do
add :id, :bigint, primary_key: true
add :ap_id, :string, null: false
add :type, :string, null: false
add :actor, :string, null: false
add :actor_id, references(:actors, type: :bigint, on_delete: :nothing), null: false
add :object, :map, null: false
add :to, {:array, :string}, default: []
timestamps()
end
create index(:activities, [:actor])
create index(:activities, [:type])
create index(:activities, [:actor_id])
end
end

View file

@ -0,0 +1,23 @@
defmodule Nulla.Repo.Migrations.CreateActorRelations do
use Ecto.Migration
def change do
create table(:relations, primary_key: false) do
add :id, :bigint, primary_key: true
add :source_id, :bigint, null: false
add :target_id, :bigint, null: false
add :type, :string, null: false
add :status, :string, null: false
add :activity_id, :bigint
timestamps()
end
create index(:relations, [:source_id])
create index(:relations, [:target_id])
create index(:relations, [:type])
create index(:relations, [:activity_id])
create unique_index(:relations, [:source_id, :target_id, :type])
end
end