nulla/lib/nulla/activitypub.ex
2025-06-12 13:31:59 +02:00

246 lines
7.7 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,
to: activity.to
)
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
end