nulla/lib/nulla/activitypub.ex
2025-06-15 08:59:03 +02:00

359 lines
11 KiB
Elixir

defmodule Nulla.ActivityPub do
@spec context() :: list()
defp context do
[
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
Jason.OrderedObject.new(
manuallyApprovesFollowers: "as:manuallyApprovesFollowers",
toot: "http://joinmastodon.org/ns#",
featured: %{"@id" => "toot:featured", "@type" => "@id"},
featuredTags: %{"@id" => "toot:featuredTags", "@type" => "@id"},
alsoKnownAs: %{"@id" => "as:alsoKnownAs", "@type" => "@id"},
movedTo: %{"@id" => "as:movedTo", "@type" => "@id"},
schema: "http://schema.org#",
PropertyValue: "schema:PropertyValue",
value: "schema:value",
discoverable: "toot:discoverable",
suspended: "toot:suspended",
memorial: "toot:memorial",
indexable: "toot:indexable",
attributionDomains: %{"@id" => "toot:attributionDomains", "@type" => "@id"},
Hashtag: "as:Hashtag",
vcard: "http://www.w3.org/2006/vcard/ns#"
)
]
end
@spec user(String.t(), Nulla.Models.User.t()) :: Jason.OrderedObject.t()
def user(domain, user) 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
)
end
@spec note(String.t(), Nulla.Models.Note.t()) :: Jason.OrderedObject.t()
def note(domain, 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://#{domain}/files/#{att.file}"
)
end)
]
end
Jason.OrderedObject.new(
"@context": [
"https://www.w3.org/ns/activitystreams",
Jason.OrderedObject.new(sensitive: "as:sensitive")
],
id: "https://#{domain}/@#{note.user.username}/#{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}",
to: [
"https://www.w3.org/ns/activitystreams#Public"
],
cc: [
"https://#{domain}/@#{note.user.username}/followers"
],
sensetive: false,
content: note.content,
contentMap: Jason.OrderedObject.new("#{note.language}": note.content),
attachment: attachment
)
end
@spec activity(String.t(), Nulla.Models.Activity.t()) :: Jason.OrderedObject.t()
def activity(domain, activity) do
Jason.OrderedObject.new(
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://#{domain}/activities/#{activity.id}",
type: activity.type,
actor: activity.actor,
object: activity.object
)
end
@spec following(String.t(), Nulla.Models.User.t(), Integer.t()) :: Jason.OrderedObject.t()
def following(domain, user, total) do
Jason.OrderedObject.new(
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://#{domain}/@#{user.username}/following",
type: "OrderedCollection",
totalItems: total,
first: "https://#{domain}/@#{user.username}/following?page=1"
)
end
@spec following(
String.t(),
Nulla.Models.User.t(),
Integer.t(),
List.t(),
Integer.t(),
Integer.t()
) :: Jason.OrderedObject.t()
def following(domain, user, 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}",
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",
orderedItems: following_list
]
data =
if page <= 1 do
Keyword.delete(data, :prev)
else
data
end
data =
if page * offset > total do
data
|> Keyword.delete(:next)
|> Keyword.delete(:prev)
else
data
end
Jason.OrderedObject.new(data)
end
@spec followers(String.t(), Nulla.Models.User.t(), Integer.t()) :: Jason.OrderedObject.t()
def followers(domain, user, total) do
Jason.OrderedObject.new(
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://#{domain}/@#{user.username}/followers",
type: "OrderedCollection",
totalItems: total,
first: "https://#{domain}/@#{user.username}/followers?page=1"
)
end
@spec followers(
String.t(),
Nulla.Models.User.t(),
Integer.t(),
List.t(),
Integer.t(),
Integer.t()
) :: Jason.OrderedObject.t()
def followers(domain, user, 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}",
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",
orderedItems: followers_list
]
data =
if page <= 1 do
Keyword.delete(data, :prev)
else
data
end
data =
if page * offset > total do
data
|> Keyword.delete(:next)
|> Keyword.delete(:prev)
else
data
end
Jason.OrderedObject.new(data)
end
@spec webfinger(String.t(), String.t(), String.t()) :: Jason.OrderedObject.t()
def webfinger(domain, username, resource) do
Jason.OrderedObject.new(
subject: resource,
links: [
Jason.OrderedObject.new(
rel: "self",
type: "application/activity+json",
href: "https://#{domain}/@#{username}"
)
]
)
end
@spec nodeinfo(String.t()) :: Jason.OrderedObject.t()
def nodeinfo(domain) do
Jason.OrderedObject.new(
links: [
Jason.OrderedObject.new(
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
href: "https://#{domain}/nodeinfo/2.0"
)
]
)
end
@spec nodeinfo(String.t(), Map.t(), Nulla.Models.InstanceSettings.t()) ::
Jason.OrderedObject.t()
def nodeinfo(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
@spec outbox(String.t(), String.t(), Integer.t()) :: Jason.OrderedObject.t()
def outbox(domain, username, total) do
Jason.OrderedObject.new(
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://#{domain}/@#{username}/outbox",
type: "OrderedCollection",
totalItems: total,
first: "https://#{domain}/@#{username}/outbox?page=true",
last: "https://#{domain}/@#{username}/outbox?min_id=0&page=true"
)
end
@spec outbox(String.t(), Integer.t(), Integer.t(), String.t(), List.t()) ::
Jason.OrderedObject.t()
def outbox(domain, username, max_id, min_id, items) do
Jason.OrderedObject.new(
"@context": [
"https://www.w3.org/ns/activitystreams",
Jason.OrderedObject.new(
sensitive: "as:sensitive",
Hashtag: "as:Hashtag"
)
],
id: "https://#{domain}/@#{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",
orderedItems: items
)
end
@spec render_activity(String.t(), Note.t()) :: Jason.OrderedObject.t()
def render_activity(domain, note) do
Jason.OrderedObject.new(
id: "https://#{domain}/@#{note.user.username}/#{note.id}/activity",
type: "Create",
actor: "https://#{domain}/@#{note.user.username}",
published: note.inserted_at |> DateTime.to_iso8601(),
to: ["https://www.w3.org/ns/activitystreams#Public"],
object:
Jason.OrderedObject.new(
id: "https://#{domain}/@#{note.user.username}/#{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"]
)
)
end
end