Update
This commit is contained in:
parent
58049c93d4
commit
894866ca03
22 changed files with 344 additions and 213 deletions
|
@ -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
|
||||
|
|
5
lib/nulla/httpsignature.ex
Normal file
5
lib/nulla/httpsignature.ex
Normal file
|
@ -0,0 +1,5 @@
|
|||
defmodule Nulla.HTTPSignature do
|
||||
def verify(_conn, _actor) do
|
||||
:ok
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
30
lib/nulla/models/relation.ex
Normal file
30
lib/nulla/models/relation.ex
Normal 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
|
|
@ -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
|
||||
|
|
43
lib/nulla_web/controllers/actor_controller.ex
Normal file
43
lib/nulla_web/controllers/actor_controller.ex
Normal 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
|
12
lib/nulla_web/controllers/auth_controller.ex
Normal file
12
lib/nulla_web/controllers/auth_controller.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
23
priv/repo/migrations/20250617091354_create_relations.exs
Normal file
23
priv/repo/migrations/20250617091354_create_relations.exs
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue