Update
This commit is contained in:
parent
8f63a831c4
commit
f963620cf0
11 changed files with 135 additions and 20 deletions
|
@ -87,12 +87,12 @@ defmodule Nulla.ActivityPub do
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
Jason.OrderedObject.new(sensitive: "as:sensitive")
|
Jason.OrderedObject.new(sensitive: "as:sensitive")
|
||||||
],
|
],
|
||||||
id: "#{note.actor.ap_id}/statuses/#{note.id}",
|
id: "#{note.actor.ap_id}/notes/#{note.id}",
|
||||||
type: "Note",
|
type: "Note",
|
||||||
summary: nil,
|
summary: nil,
|
||||||
inReplyTo: nil,
|
inReplyTo: note.inReplyTo,
|
||||||
published: note.inserted_at,
|
published: note.inserted_at,
|
||||||
url: "#{note.actor.ap_id}/#{note.id}",
|
url: note.url,
|
||||||
attributedTo: note.actor.ap_id,
|
attributedTo: note.actor.ap_id,
|
||||||
to: [
|
to: [
|
||||||
"https://www.w3.org/ns/activitystreams#Public"
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
@ -100,7 +100,7 @@ defmodule Nulla.ActivityPub do
|
||||||
cc: [
|
cc: [
|
||||||
"#{note.actor.ap_id}/followers"
|
"#{note.actor.ap_id}/followers"
|
||||||
],
|
],
|
||||||
sensetive: false,
|
sensitive: note.sensitive,
|
||||||
content: note.content,
|
content: note.content,
|
||||||
contentMap: Jason.OrderedObject.new("#{note.language}": note.content),
|
contentMap: Jason.OrderedObject.new("#{note.language}": note.content),
|
||||||
attachment: attachment
|
attachment: attachment
|
||||||
|
@ -329,7 +329,7 @@ defmodule Nulla.ActivityPub do
|
||||||
@spec activity_note(Note.t()) :: Jason.OrderedObject.t()
|
@spec activity_note(Note.t()) :: Jason.OrderedObject.t()
|
||||||
def activity_note(note) do
|
def activity_note(note) do
|
||||||
Jason.OrderedObject.new(
|
Jason.OrderedObject.new(
|
||||||
id: "#{note.actor.ap_id}/statuses/#{note.id}/activity",
|
id: "#{note.actor.ap_id}/notes/#{note.id}/activity",
|
||||||
type: "Create",
|
type: "Create",
|
||||||
actor: note.actor.ap_id,
|
actor: note.actor.ap_id,
|
||||||
published: note.inserted_at |> DateTime.to_iso8601(),
|
published: note.inserted_at |> DateTime.to_iso8601(),
|
||||||
|
@ -338,7 +338,7 @@ defmodule Nulla.ActivityPub do
|
||||||
],
|
],
|
||||||
object:
|
object:
|
||||||
Jason.OrderedObject.new(
|
Jason.OrderedObject.new(
|
||||||
id: "#{note.actor.ap_id}/statuses/#{note.id}",
|
id: "#{note.actor.ap_id}/notes/#{note.id}",
|
||||||
type: "Note",
|
type: "Note",
|
||||||
content: note.content,
|
content: note.content,
|
||||||
published: note.inserted_at |> DateTime.to_iso8601(),
|
published: note.inserted_at |> DateTime.to_iso8601(),
|
||||||
|
|
|
@ -24,7 +24,7 @@ defmodule Nulla.Models.Activity do
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_activity(attrs) do
|
def create_activity(attrs) do
|
||||||
id = Snowflake.next_id()
|
id = Map.get(attrs, :id, Snowflake.next_id())
|
||||||
|
|
||||||
%__MODULE__{}
|
%__MODULE__{}
|
||||||
|> __MODULE__.changeset(attrs)
|
|> __MODULE__.changeset(attrs)
|
||||||
|
|
|
@ -93,7 +93,7 @@ defmodule Nulla.Models.Actor do
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_actor(attrs) when is_map(attrs) do
|
def create_actor(attrs) when is_map(attrs) do
|
||||||
id = Snowflake.next_id()
|
id = Map.get(attrs, :id, Snowflake.next_id())
|
||||||
|
|
||||||
%__MODULE__{}
|
%__MODULE__{}
|
||||||
|> changeset(attrs)
|
|> changeset(attrs)
|
||||||
|
|
|
@ -3,20 +3,22 @@ defmodule Nulla.Models.Note do
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
alias Nulla.Repo
|
alias Nulla.Repo
|
||||||
|
alias Nulla.Snowflake
|
||||||
alias Nulla.Models.Actor
|
alias Nulla.Models.Actor
|
||||||
alias Nulla.Models.MediaAttachment
|
alias Nulla.Models.MediaAttachment
|
||||||
|
|
||||||
@primary_key {:id, :integer, autogenerate: false}
|
@primary_key {:id, :integer, autogenerate: false}
|
||||||
schema "notes" do
|
schema "notes" do
|
||||||
field :content, :string
|
field :inReplyTo, :string
|
||||||
|
field :url, :string
|
||||||
|
|
||||||
field :visibility, Ecto.Enum,
|
field :visibility, Ecto.Enum,
|
||||||
values: [:public, :unlisted, :followers, :private],
|
values: [:public, :unlisted, :followers, :private],
|
||||||
default: :public
|
default: :public
|
||||||
|
|
||||||
field :sensitive, :boolean, default: false
|
field :sensitive, :boolean, default: false
|
||||||
|
field :content, :string
|
||||||
field :language, :string
|
field :language, :string
|
||||||
field :in_reply_to, :string
|
|
||||||
|
|
||||||
belongs_to :actor, Actor
|
belongs_to :actor, Actor
|
||||||
has_many :media_attachments, MediaAttachment
|
has_many :media_attachments, MediaAttachment
|
||||||
|
@ -27,11 +29,20 @@ defmodule Nulla.Models.Note do
|
||||||
@doc false
|
@doc false
|
||||||
def changeset(note, attrs) do
|
def changeset(note, attrs) do
|
||||||
note
|
note
|
||||||
|> cast(attrs, [:content, :visibility, :sensitive, :language, :in_reply_to, :actor_id])
|
|> cast(attrs, [:content, :visibility, :sensitive, :language, :inReplyTo, :actor_id])
|
||||||
|> validate_required([:content, :visibility, :sensitive, :language, :in_reply_to, :actor_id])
|
|> validate_required([:content, :visibility, :sensitive, :language, :actor_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_note!(id), do: Repo.get!(__MODULE__, id)
|
def create_note(attrs) when is_map(attrs) do
|
||||||
|
id = Map.get(attrs, :id, Snowflake.next_id())
|
||||||
|
|
||||||
|
%__MODULE__{}
|
||||||
|
|> changeset(attrs)
|
||||||
|
|> put_change(:id, id)
|
||||||
|
|> Repo.insert()
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_note(id), do: Repo.get(__MODULE__, id)
|
||||||
|
|
||||||
def get_latest_notes(actor_id, limit \\ 20) do
|
def get_latest_notes(actor_id, limit \\ 20) do
|
||||||
from(n in __MODULE__,
|
from(n in __MODULE__,
|
||||||
|
|
|
@ -53,7 +53,7 @@ defmodule Nulla.Models.Relation do
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_relation(attrs) do
|
def create_relation(attrs) do
|
||||||
id = Snowflake.next_id()
|
id = Map.get(attrs, :id, Snowflake.next_id())
|
||||||
|
|
||||||
%__MODULE__{}
|
%__MODULE__{}
|
||||||
|> __MODULE__.changeset(attrs)
|
|> __MODULE__.changeset(attrs)
|
||||||
|
|
|
@ -95,3 +95,9 @@ defmodule NullaWeb.ActorHTML do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defmodule NullaWeb.NoteHTML do
|
||||||
|
use NullaWeb, :html
|
||||||
|
|
||||||
|
embed_templates "templates/note/*"
|
||||||
|
end
|
||||||
|
|
22
lib/nulla_web/components/templates/note/show.html.heex
Normal file
22
lib/nulla_web/components/templates/note/show.html.heex
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<main class="grid grid-cols-[25%_50%_25%]">
|
||||||
|
<div class="flex flex-col items-center mt-5 gap-5">
|
||||||
|
<input
|
||||||
|
placeholder="Search"
|
||||||
|
class="border border-gray-300 px-4 py-3 rounded-xl outline-none w-[90%]"
|
||||||
|
/>
|
||||||
|
<div class="text-sm rounded-xl border border-gray-300 p-2 w-[90%]">
|
||||||
|
<textarea
|
||||||
|
placeholder="What's on your mind?"
|
||||||
|
class="h-[150px] w-full resize-none border-none focus:ring-0"
|
||||||
|
></textarea>
|
||||||
|
<div>
|
||||||
|
<button class="text-white bg-black px-3 py-1 rounded-xl">Post</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative border border-gray-300 shadow-md mt-5 rounded-t-xl overflow-hidden">
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center mt-5 gap-5">
|
||||||
|
<div class="text-sm rounded-xl border border-gray-300 p-4 w-[90%] h-[300px]"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
|
@ -6,12 +6,12 @@ defmodule NullaWeb.NoteController do
|
||||||
|
|
||||||
def show(conn, %{"username" => username, "id" => id}) do
|
def show(conn, %{"username" => username, "id" => id}) do
|
||||||
accept = List.first(get_req_header(conn, "accept"))
|
accept = List.first(get_req_header(conn, "accept"))
|
||||||
note = Note.get_note!(id) |> Repo.preload([:user, :media_attachments])
|
note = Note.get_note(id) |> Repo.preload([:actor, :media_attachments])
|
||||||
|
|
||||||
if username != note.user.username do
|
if username != note.actor.preferredUsername do
|
||||||
conn
|
conn
|
||||||
|> put_status(:not_found)
|
|> put_status(:not_found)
|
||||||
|> json(%{error: "Note not found"})
|
|> json(%{error: "Not Found"})
|
||||||
|> halt()
|
|> halt()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ defmodule NullaWeb.Router do
|
||||||
get "/followers", FollowController, :followers
|
get "/followers", FollowController, :followers
|
||||||
post "/inbox", InboxController, :inbox
|
post "/inbox", InboxController, :inbox
|
||||||
get "/outbox", OutboxController, :outbox
|
get "/outbox", OutboxController, :outbox
|
||||||
get "/statuses/:id", NoteController, :show
|
get "/notes/:id", NoteController, :show
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/@:username" do
|
scope "/@:username" do
|
||||||
|
|
|
@ -4,11 +4,12 @@ defmodule Nulla.Repo.Migrations.CreateNotes do
|
||||||
def change do
|
def change do
|
||||||
create table(:notes, primary_key: false) do
|
create table(:notes, primary_key: false) do
|
||||||
add :id, :bigint, primary_key: true
|
add :id, :bigint, primary_key: true
|
||||||
add :content, :text
|
add :inReplyTo, :string
|
||||||
|
add :url, :string
|
||||||
add :visibility, :string, default: "public"
|
add :visibility, :string, default: "public"
|
||||||
add :sensitive, :boolean, default: false
|
add :sensitive, :boolean, default: false
|
||||||
|
add :content, :text
|
||||||
add :language, :string
|
add :language, :string
|
||||||
add :in_reply_to, :string
|
|
||||||
add :actor_id, references(:actors, on_delete: :delete_all)
|
add :actor_id, references(:actors, on_delete: :delete_all)
|
||||||
|
|
||||||
timestamps(type: :utc_datetime)
|
timestamps(type: :utc_datetime)
|
||||||
|
|
75
test/nulla_web/controllers/note_controller_test.exs
Normal file
75
test/nulla_web/controllers/note_controller_test.exs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
defmodule NullaWeb.NoteControllerTest do
|
||||||
|
use NullaWeb.ConnCase
|
||||||
|
alias Nulla.KeyGen
|
||||||
|
alias Nulla.Snowflake
|
||||||
|
alias Nulla.Models.Actor
|
||||||
|
alias Nulla.Models.Note
|
||||||
|
|
||||||
|
describe "GET /notes/id" do
|
||||||
|
test "returns ActivityPub JSON with note", %{conn: conn} do
|
||||||
|
{publicKeyPem, _privateKeyPem} = KeyGen.gen()
|
||||||
|
|
||||||
|
{:ok, actor} =
|
||||||
|
Actor.create_actor(%{
|
||||||
|
domain: "localhost",
|
||||||
|
ap_id: "http://localhost/users/test",
|
||||||
|
type: "Person",
|
||||||
|
following: "http://localhost/users/test/following",
|
||||||
|
followers: "http://localhost/users/test/followers",
|
||||||
|
inbox: "http://localhost/users/test/inbox",
|
||||||
|
outbox: "http://localhost/users/test/outbox",
|
||||||
|
featured: "http://localhost/users/test/collections/featured",
|
||||||
|
featuredTags: "http://localhost/users/test/collections/tags",
|
||||||
|
preferredUsername: "test",
|
||||||
|
name: "Test",
|
||||||
|
summary: "Test User",
|
||||||
|
url: "http://localhost/@test",
|
||||||
|
manuallyApprovesFollowers: false,
|
||||||
|
discoverable: true,
|
||||||
|
indexable: true,
|
||||||
|
published: DateTime.utc_now(),
|
||||||
|
memorial: false,
|
||||||
|
publicKey:
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
id: "http://localhost/users/test#main-key",
|
||||||
|
owner: "http://localhost/users/test",
|
||||||
|
publicKeyPem: publicKeyPem
|
||||||
|
),
|
||||||
|
endpoints: Jason.OrderedObject.new(sharedInbox: "http://localhost/inbox")
|
||||||
|
})
|
||||||
|
|
||||||
|
note_id = Snowflake.next_id()
|
||||||
|
|
||||||
|
{:ok, note} =
|
||||||
|
Note.create_note(%{
|
||||||
|
id: note_id,
|
||||||
|
url: "#{actor.url}/#{note_id}",
|
||||||
|
content: "Hello World from Nulla!",
|
||||||
|
language: "en",
|
||||||
|
actor_id: actor.id
|
||||||
|
})
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get(~p"/users/#{actor.preferredUsername}/notes/#{note.id}")
|
||||||
|
|
||||||
|
assert response = json_response(conn, 200)
|
||||||
|
|
||||||
|
assert is_list(response["@context"])
|
||||||
|
assert response["id"] == "http://localhost/users/test/notes/#{note.id}"
|
||||||
|
assert response["type"] == "Note"
|
||||||
|
assert response["summary"] == nil
|
||||||
|
assert response["inReplyTo"] == nil
|
||||||
|
assert {:ok, _dt, _offset} = DateTime.from_iso8601(response["published"])
|
||||||
|
assert response["url"] == note.url
|
||||||
|
assert response["attributedTo"] == "http://localhost/users/test"
|
||||||
|
assert is_list(response["to"])
|
||||||
|
assert is_list(response["cc"])
|
||||||
|
assert response["sensitive"] == false
|
||||||
|
assert is_binary(response["content"])
|
||||||
|
assert is_map(response["contentMap"])
|
||||||
|
assert is_list(response["attachment"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Add table
Add a link
Reference in a new issue