diff --git a/lib/nulla/notes.ex b/lib/nulla/notes.ex index 99c4999..014804e 100644 --- a/lib/nulla/notes.ex +++ b/lib/nulla/notes.ex @@ -1,4 +1,6 @@ defmodule Nulla.Notes do + import Ecto.Query + @moduledoc """ The Notes context. """ @@ -21,6 +23,12 @@ defmodule Nulla.Notes do Repo.all(Note) end + def list_notes_by(by) when is_map(by) or is_list(by) do + Note + |> where(^by) + |> Repo.all() + end + @doc """ Gets a single note. diff --git a/lib/nulla/notes/note.ex b/lib/nulla/notes/note.ex index a1f607e..13c998d 100644 --- a/lib/nulla/notes/note.ex +++ b/lib/nulla/notes/note.ex @@ -2,11 +2,13 @@ defmodule Nulla.Notes.Note do use Ecto.Schema import Ecto.Changeset alias Nulla.Snowflake + alias Nulla.Actors alias Nulla.Actors.Actor alias Nulla.MediaAttachments.MediaAttachment @primary_key {:id, :integer, autogenerate: false} schema "notes" do + field :ap_id, :string field :inReplyTo, :string field :published, :utc_datetime field :url, :string @@ -16,6 +18,7 @@ defmodule Nulla.Notes.Note do field :sensitive, :boolean, default: false field :content, :string field :language, :string + field :featured, :boolean, default: false belongs_to :actor, Actor has_many :media_attachments, MediaAttachment @@ -27,6 +30,7 @@ defmodule Nulla.Notes.Note do def changeset(note, attrs) do note |> cast(attrs, [ + :ap_id, :inReplyTo, :published, :url, @@ -36,10 +40,13 @@ defmodule Nulla.Notes.Note do :sensitive, :content, :language, + :featured, :actor_id ]) |> maybe_put_id() + |> maybe_put_ap_id() |> validate_required([ + :ap_id, :published, :url, :visibility, @@ -47,17 +54,36 @@ defmodule Nulla.Notes.Note do :cc, :content, :language, + :featured, :actor_id ]) end defp maybe_put_id(changeset) do - id_in_attrs = get_field(changeset, :id) + id = get_field(changeset, :id) - if is_nil(id_in_attrs) do + if is_nil(id) do change(changeset, id: Snowflake.next_id()) else changeset end end + + defp maybe_put_ap_id(changeset) do + id = get_field(changeset, :id) + ap_id = get_field(changeset, :ap_id) + actor_id = get_field(changeset, :actor_id) + + cond do + not is_nil(ap_id) -> + changeset + + is_nil(actor_id) or is_nil(id) -> + changeset + + true -> + actor = Actors.get_actor!(actor_id) + change(changeset, ap_id: "#{actor.ap_id}/notes/#{id}") + end + end end diff --git a/lib/nulla_web/controllers/activitypub/featured_controller.ex b/lib/nulla_web/controllers/activitypub/featured_controller.ex new file mode 100644 index 0000000..51d36b1 --- /dev/null +++ b/lib/nulla_web/controllers/activitypub/featured_controller.ex @@ -0,0 +1,24 @@ +defmodule NullaWeb.ActivityPub.FeaturedController do + use NullaWeb, :controller + alias Nulla.Actors + alias Nulla.Notes + alias Nulla.Actors.Actor + alias NullaWeb.ActivityPub.FeaturedJSON + + def index(conn, %{"username" => username}) do + domain = NullaWeb.Endpoint.host() + actor = Actors.get_actor_by(acct: "#{username}@#{domain}") + + case actor do + %Actor{} = actor -> + notes = Notes.list_notes_by(actor_id: actor.id, featured: true) + + conn + |> put_resp_content_type("application/activity+json") + |> send_resp(200, Jason.encode!(FeaturedJSON.index(actor, notes))) + + _ -> + send_resp(conn, 404, "") + end + end +end diff --git a/lib/nulla_web/controllers/activitypub/featured_json.ex b/lib/nulla_web/controllers/activitypub/featured_json.ex new file mode 100644 index 0000000..86e4187 --- /dev/null +++ b/lib/nulla_web/controllers/activitypub/featured_json.ex @@ -0,0 +1,51 @@ +defmodule NullaWeb.ActivityPub.FeaturedJSON do + @doc """ + Renders a featured. + """ + def index(actor, notes) do + Jason.OrderedObject.new( + "@context": "https://www.w3.org/ns/activitystreams", + id: actor.featured, + type: "OrderedCollection", + totalItems: length(notes), + orderedItems: Enum.map(notes, &data/1) + ) + end + + defp data(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://#{note.actor.domain}/files/#{att.file}" + ) + end) + ] + end + + Jason.OrderedObject.new( + "@context": "https://www.w3.org/ns/activitystreams", + id: note.ap_id, + type: "Note", + summary: nil, + inReplyTo: note.inReplyTo, + published: note.published, + url: note.url, + attributedTo: note.actor.ap_id, + to: note.to, + cc: note.cc, + sensitive: note.sensitive, + content: note.content, + contentMap: Jason.OrderedObject.new("#{note.language}": note.content), + attachment: attachment + ) + end +end diff --git a/lib/nulla_web/controllers/activitypub/note_json.ex b/lib/nulla_web/controllers/activitypub/note_json.ex index c2c6436..a37c7b4 100644 --- a/lib/nulla_web/controllers/activitypub/note_json.ex +++ b/lib/nulla_web/controllers/activitypub/note_json.ex @@ -32,7 +32,7 @@ defmodule NullaWeb.ActivityPub.NoteJSON do "https://www.w3.org/ns/activitystreams", Jason.OrderedObject.new(sensitive: "as:sensitive") ], - id: "#{note.actor.ap_id}/notes/#{note.id}", + id: note.ap_id, type: "Note", summary: nil, inReplyTo: note.inReplyTo, diff --git a/lib/nulla_web/controllers/activitypub/outbox_json.ex b/lib/nulla_web/controllers/activitypub/outbox_json.ex index de78046..c68468a 100644 --- a/lib/nulla_web/controllers/activitypub/outbox_json.ex +++ b/lib/nulla_web/controllers/activitypub/outbox_json.ex @@ -13,6 +13,9 @@ defmodule NullaWeb.ActivityPub.OutboxJSON do ) end + @doc """ + Renders outbox items. + """ def show(actor, activities, max_id, min_id) do Jason.OrderedObject.new( "@context": [ diff --git a/lib/nulla_web/router.ex b/lib/nulla_web/router.ex index 74cd91a..7a6c409 100644 --- a/lib/nulla_web/router.ex +++ b/lib/nulla_web/router.ex @@ -85,6 +85,7 @@ defmodule NullaWeb.Router do get "/followers", FollowController, :followers post "/inbox", InboxController, :inbox get "/outbox", OutboxController, :index + get "/collections/featured", FeaturedController, :index get "/notes/:id", NoteController, :show end end diff --git a/priv/repo/migrations/20250702091405_create_notes.exs b/priv/repo/migrations/20250702091405_create_notes.exs index 6f65471..a605abb 100644 --- a/priv/repo/migrations/20250702091405_create_notes.exs +++ b/priv/repo/migrations/20250702091405_create_notes.exs @@ -4,6 +4,7 @@ defmodule Nulla.Repo.Migrations.CreateNotes do def change do create table(:notes, primary_key: false) do add :id, :bigint, primary_key: true + add :ap_id, :string add :inReplyTo, :string add :published, :utc_datetime add :url, :string @@ -13,6 +14,7 @@ defmodule Nulla.Repo.Migrations.CreateNotes do add :sensitive, :boolean, default: false, null: false add :content, :string add :language, :string + add :featured, :boolean, default: false, null: false add :actor_id, references(:actors, on_delete: :delete_all) timestamps(type: :utc_datetime)