diff --git a/lib/nulla/keygen.ex b/lib/nulla/keygen.ex index 5cbaf06..c803920 100644 --- a/lib/nulla/keygen.ex +++ b/lib/nulla/keygen.ex @@ -1,5 +1,5 @@ defmodule Nulla.KeyGen do - def gen do + def generate_keys do rsa_key = :public_key.generate_key({:rsa, 2048, 65537}) {:RSAPrivateKey, :"two-prime", n, e, _d, _p, _q, _dp, _dq, _qi, _other} = rsa_key diff --git a/lib/nulla/models/actor.ex b/lib/nulla/models/actor.ex deleted file mode 100644 index 653c37f..0000000 --- a/lib/nulla/models/actor.ex +++ /dev/null @@ -1,109 +0,0 @@ -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 :type, :string - field :following, :string - field :followers, :string - field :inbox, :string - field :outbox, :string - field :featured, :string - field :featuredTags, :string - field :preferredUsername, :string - field :name, :string - field :summary, :string - field :url, :string - field :manuallyApprovesFollowers, :boolean - field :discoverable, :boolean, default: true - field :indexable, :boolean, default: true - field :published, :utc_datetime - field :memorial, :boolean, default: false - field :publicKey, {:array, :map} - field :tag, {:array, :map} - field :attachment, {:array, :map} - field :endpoints, :map - field :icon, :map - field :image, :map - 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 - - @doc false - def changeset(actor, attrs) do - actor - |> cast(attrs, [ - :id, - :type, - :following, - :followers, - :inbox, - :outbox, - :featured, - :featuredTags, - :preferredUsername, - :name, - :summary, - :url, - :manuallyApprovesFollowers, - :discoverable, - :indexable, - :published, - :memorial, - :publicKey, - :tag, - :attachment, - :endpoints, - :icon, - :image, - :vcard_bday, - :vcard_Address - ]) - |> validate_required([ - :id, - :type, - :following, - :followers, - :inbox, - :outbox, - :featured, - :featuredTags, - :preferredUsername, - :name, - :summary, - :url, - :manuallyApprovesFollowers, - :discoverable, - :indexable, - :published, - :memorial, - :publicKey, - :tag, - :attachment, - :endpoints, - :icon, - :image, - :vcard_bday, - :vcard_Address - ]) - end - - def create_user(attrs) when is_map(attrs) do - id = Snowflake.next_id() - - %__MODULE__{} - |> changeset(attrs) - |> Ecto.Changeset.put_change(:id, id) - |> Repo.insert() - end -end diff --git a/lib/nulla/models/follow.ex b/lib/nulla/models/follow.ex index bcd92f7..3646881 100644 --- a/lib/nulla/models/follow.ex +++ b/lib/nulla/models/follow.ex @@ -1,8 +1,6 @@ defmodule Nulla.Models.Follow do use Ecto.Schema import Ecto.Changeset - alias Nulla.Snowflake - alias Nulla.Models.Follow @primary_key {:id, :integer, autogenerate: false} schema "follows" do @@ -19,13 +17,4 @@ defmodule Nulla.Models.Follow do |> validate_required([:user_id, :target_id]) |> unique_constraint([:user_id, :target_id]) end - - def create_follow(attrs) do - id = Snowflake.next_id() - - %Follow{} - |> Follow.changeset(attrs) - |> Ecto.Changeset.put_change(:id, id) - |> Repo.insert() - end end diff --git a/lib/nulla/models/notification.ex b/lib/nulla/models/notification.ex index 70e1dcc..946f58b 100644 --- a/lib/nulla/models/notification.ex +++ b/lib/nulla/models/notification.ex @@ -1,8 +1,6 @@ defmodule Nulla.Models.Notification do use Ecto.Schema import Ecto.Changeset - alias Nulla.Models.User - alias Nulla.Models.Actor @primary_key {:id, :integer, autogenerate: false} schema "notifications" do @@ -10,8 +8,8 @@ defmodule Nulla.Models.Notification do field :data, :map field :read, :boolean, default: false - belongs_to :user, User - belongs_to :actor, Actor + belongs_to :user, Nulla.Models.User + belongs_to :actor, Nulla.Models.User timestamps() end diff --git a/lib/nulla/models/user.ex b/lib/nulla/models/user.ex index 4328761..ce1ce22 100644 --- a/lib/nulla/models/user.ex +++ b/lib/nulla/models/user.ex @@ -5,18 +5,34 @@ defmodule Nulla.Models.User do alias Nulla.Repo alias Nulla.Snowflake alias Nulla.Models.User - alias Nulla.Models.Actor - alias Nulla.Models.Session @primary_key {:id, :integer, autogenerate: false} schema "users" do + field :username, :string + field :domain, :string field :email, :string field :password, :string - field :privateKeyPem, :string + field :is_moderator, :boolean, default: false + field :realname, :string + field :bio, :string + field :location, :string + field :birthday, :date + field :fields, {:array, :map} + field :tags, {:array, :string} + field :follow_approval, :boolean, default: false + field :is_bot, :boolean, default: false + field :is_discoverable, :boolean, default: true + field :is_indexable, :boolean, default: true + field :is_memorial, :boolean, default: false + field :private_key, :string + field :public_key, :string + field :avatar, :string + field :banner, :string field :last_active_at, :utc_datetime - belongs_to :actor, Actor, define_field: false, foreign_key: :id, type: :integer - has_many :user_sessions, Session + has_many :user_sessions, Nulla.Models.Session + has_many :notes, Nulla.Models.Note + has_many :media_attachments, through: [:notes, :media_attachments] timestamps(type: :utc_datetime) end @@ -25,24 +41,57 @@ defmodule Nulla.Models.User do def changeset(user, attrs) do user |> cast(attrs, [ + :username, + :domain, :email, :password, - :privateKeyPem, - :last_active_at, - :actor_id + :is_moderator, + :realname, + :bio, + :location, + :birthday, + :fields, + :follow_approval, + :is_bot, + :is_discoverable, + :is_indexable, + :is_memorial, + :private_key, + :public_key, + :avatar, + :banner, + :last_active_at ]) |> validate_required([ + :username, + :domain, :email, :password, - :privateKeyPem, - :last_active_at, - :actor_id + :is_moderator, + :realname, + :bio, + :location, + :birthday, + :fields, + :follow_approval, + :is_bot, + :is_discoverable, + :is_indexable, + :is_memorial, + :private_key, + :public_key, + :avatar, + :banner, + :last_active_at ]) end - def create_user(attrs) when is_map(attrs) do - %__MODULE__{} - |> changeset(attrs) + def create_user(attrs) do + id = Snowflake.next_id() + + %User{} + |> User.changeset(attrs) + |> Ecto.Changeset.put_change(:id, id) |> Repo.insert() end @@ -50,13 +99,6 @@ defmodule Nulla.Models.User do 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 - ) - |> Repo.one() - end - def get_total_users_count(domain) do Repo.aggregate(from(u in User, where: u.domain == ^domain), :count, :id) end diff --git a/lib/nulla/utils.ex b/lib/nulla/utils.ex index fc788c0..80a66c3 100644 --- a/lib/nulla/utils.ex +++ b/lib/nulla/utils.ex @@ -87,38 +87,4 @@ defmodule Nulla.Utils do users end end - - def resolve_local_actor("https://" <> _ = uri) do - case URI.parse(uri).path do - "/@" <> username -> - instance_settings = InstanceSettings.get_instance_settings!() - domain = instance_settings.domain - - case User.get_user_by_username_and_domain(username, domain) do - nil -> {:error, :not_found} - user -> {:ok, user} - end - - _ -> - {:error, :invalid_actor} - end - end - - def fetch_remote_actor(uri) do - request = - Finch.build(:get, uri, [ - {"Accept", "application/activity+json"} - ]) - - case Finch.request(request, MyApp.Finch) do - {:ok, %Finch.Response{status: 200, body: body}} -> - case Jason.decode(body) do - {:ok, data} -> {:ok, data} - _ -> {:error, :invalid_json} - end - - _ -> - {:error, :actor_fetch_failed} - end - end end diff --git a/lib/nulla_web/controllers/inbox_controller.ex b/lib/nulla_web/controllers/inbox_controller.ex index d0ba0e3..e4ed4c4 100644 --- a/lib/nulla_web/controllers/inbox_controller.ex +++ b/lib/nulla_web/controllers/inbox_controller.ex @@ -1,23 +1,25 @@ defmodule NullaWeb.InboxController do use NullaWeb, :controller - alias Nulla.Models.Follow - alias Nulla.Utils - def inbox( - conn, - %{"type" => "Follow", "actor" => actor_uri, "object" => target_uri} = activity - ) 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"}) - else - error -> - IO.inspect(error, label: "Follow error") - json(conn, %{"error" => "Failed to process Follow"}) - end + def receive(conn, %{"type" => "Follow"} = activity) do + # Check signature + # Verify actor and object + # Save follow to db + # Send Accept or Reject + json(conn, %{"status" => "Follow received"}) + end + + def receive(conn, %{"type" => "Like"} = activity) do + # Process Like + json(conn, %{"status" => "Like received"}) + end + + def receive(conn, %{"type" => "Create"} = activity) do + # Create object and save + json(conn, %{"status" => "Object created"}) + end + + def receive(conn, _params) do + json(conn, %{"status" => "Unhandled type"}) end end diff --git a/lib/nulla_web/controllers/note_controller.ex b/lib/nulla_web/controllers/note_controller.ex index 2b271be..bff4723 100644 --- a/lib/nulla_web/controllers/note_controller.ex +++ b/lib/nulla_web/controllers/note_controller.ex @@ -5,11 +5,33 @@ defmodule NullaWeb.NoteController do alias Nulla.Models.Note alias Nulla.Models.InstanceSettings - def show(conn, %{"username" => username, "id" => id}) do + def index(conn, _params) do + notes = Notes.list_notes() + render(conn, :index, notes: notes) + end + + def new(conn, _params) do + changeset = Notes.change_note(%Note{}) + render(conn, :new, changeset: changeset) + end + + def create(conn, %{"note" => note_params}) do + case Notes.create_note(note_params) do + {:ok, note} -> + conn + |> put_flash(:info, "Note created successfully.") + |> redirect(to: ~p"/notes/#{note}") + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, :new, changeset: changeset) + end + end + + def show(conn, %{"username" => username, "note_id" => note_id}) do accept = List.first(get_req_header(conn, "accept")) instance_settings = InstanceSettings.get_instance_settings!() domain = instance_settings.domain - note = Note.get_note!(id) |> Repo.preload([:user, :media_attachments]) + note = Note.get_note!(note_id) |> Repo.preload([:user, :media_attachments]) if username != note.user.username do conn @@ -26,4 +48,38 @@ defmodule NullaWeb.NoteController do render(conn, :show, domain: domain, note: note, layout: false) end end + + # def show(conn, %{"id" => id}) do + # note = Notes.get_note!(id) + # render(conn, :show, note: note) + # end + + def edit(conn, %{"id" => id}) do + note = Notes.get_note!(id) + changeset = Notes.change_note(note) + render(conn, :edit, note: note, changeset: changeset) + end + + def update(conn, %{"id" => id, "note" => note_params}) do + note = Notes.get_note!(id) + + case Notes.update_note(note, note_params) do + {:ok, note} -> + conn + |> put_flash(:info, "Note updated successfully.") + |> redirect(to: ~p"/notes/#{note}") + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, :edit, note: note, changeset: changeset) + end + end + + def delete(conn, %{"id" => id}) do + note = Notes.get_note!(id) + {:ok, _note} = Notes.delete_note(note) + + conn + |> put_flash(:info, "Note deleted successfully.") + |> redirect(to: ~p"/notes") + end end diff --git a/lib/nulla_web/controllers/outbox_controller.ex b/lib/nulla_web/controllers/outbox_controller.ex index b62214b..fd9c8c4 100644 --- a/lib/nulla_web/controllers/outbox_controller.ex +++ b/lib/nulla_web/controllers/outbox_controller.ex @@ -5,7 +5,7 @@ defmodule NullaWeb.OutboxController do alias Nulla.Models.Note alias Nulla.Models.InstanceSettings - def outbox(conn, %{"username" => username} = params) do + def show(conn, %{"username" => username} = params) do case Map.get(params, "page") do "true" -> instance_settings = InstanceSettings.get_instance_settings!() diff --git a/lib/nulla_web/router.ex b/lib/nulla_web/router.ex index edaaf09..6e50c78 100644 --- a/lib/nulla_web/router.ex +++ b/lib/nulla_web/router.ex @@ -20,31 +20,12 @@ defmodule NullaWeb.Router do get "/.well-known/webfinger", WebfingerController, :index get "/.well-known/nodeinfo", NodeinfoController, :index get "/nodeinfo/2.0", NodeinfoController, :show - post "/inbox", InboxController, :inbox - scope "/auth" do - get "/sign_in", AuthController, :sign_in - post "/sign_out", AuthController, :sign_out - get "/sign_up", AuthController, :sign_up - end - - scope "/users/:username" do - get "/", UserController, :show - get "/following", FollowController, :following - get "/followers", FollowController, :followers - post "/inbox", InboxController, :inbox - get "/outbox", OutboxController, :outbox - get "/statuses/:id", NoteController, :show - end - - scope "/@:username" do - get "/", UserController, :show - get "/following", FollowController, :following - get "/followers", FollowController, :followers - post "/inbox", InboxController, :inbox - get "/outbox", OutboxController, :outbox - get "/:id", NoteController, :show - end + get "/@:username", UserController, :show + get "/@:username/outbox", OutboxController, :show + get "/@:username/following", FollowController, :following + get "/@:username/followers", FollowController, :followers + get "/@:username/:note_id", NoteController, :show end # Other scopes may use custom stacks. diff --git a/priv/repo/migrations/20250527054942_create_instance_settings.exs b/priv/repo/migrations/20250527054942_create_instance_settings.exs index 0f63ca5..64c45f0 100644 --- a/priv/repo/migrations/20250527054942_create_instance_settings.exs +++ b/priv/repo/migrations/20250527054942_create_instance_settings.exs @@ -22,8 +22,8 @@ defmodule Nulla.Repo.Migrations.CreateInstanceSettings do flush() execute(fn -> - {public_key, private_key} = Nulla.KeyGen.gen() - now = DateTime.utc_now() + {public_key, private_key} = Nulla.KeyGen.generate_keys() + now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) domain = Application.get_env(:nulla, NullaWeb.Endpoint, []) diff --git a/priv/repo/migrations/20250530110822_create_users.exs b/priv/repo/migrations/20250530110822_create_users.exs new file mode 100644 index 0000000..2b48cfa --- /dev/null +++ b/priv/repo/migrations/20250530110822_create_users.exs @@ -0,0 +1,32 @@ +defmodule Nulla.Repo.Migrations.CreateUsers do + use Ecto.Migration + + def change do + create table(:users, primary_key: false) do + add :id, :bigint, primary_key: true + add :username, :string, null: false, unique: true + add :domain, :string, null: false + add :email, :string + add :password, :string + add :is_moderator, :boolean, default: false, null: false + add :realname, :string + add :bio, :text + add :location, :string + add :birthday, :date + add :fields, :jsonb, default: "[]", null: false + add :tags, {:array, :string} + add :follow_approval, :boolean, default: false, null: false + add :is_bot, :boolean, default: false, null: false + add :is_discoverable, :boolean, default: true, null: false + add :is_indexable, :boolean, default: true, null: false + add :is_memorial, :boolean, default: false, null: false + add :private_key, :string, null: false + add :public_key, :string, null: false + add :avatar, :string + add :banner, :string + add :last_active_at, :utc_datetime + + timestamps(type: :utc_datetime) + end + end +end diff --git a/priv/repo/migrations/20250615131431_create_notes.exs b/priv/repo/migrations/20250604083506_create_notes.exs similarity index 100% rename from priv/repo/migrations/20250615131431_create_notes.exs rename to priv/repo/migrations/20250604083506_create_notes.exs diff --git a/priv/repo/migrations/20250615131800_create_bookmarks.exs b/priv/repo/migrations/20250606100445_create_bookmarks.exs similarity index 100% rename from priv/repo/migrations/20250615131800_create_bookmarks.exs rename to priv/repo/migrations/20250606100445_create_bookmarks.exs diff --git a/priv/repo/migrations/20250615132202_create_notifications.exs b/priv/repo/migrations/20250606103230_create_notifications.exs similarity index 88% rename from priv/repo/migrations/20250615132202_create_notifications.exs rename to priv/repo/migrations/20250606103230_create_notifications.exs index 34e3204..ec8cbc0 100644 --- a/priv/repo/migrations/20250615132202_create_notifications.exs +++ b/priv/repo/migrations/20250606103230_create_notifications.exs @@ -5,7 +5,7 @@ defmodule Nulla.Repo.Migrations.CreateNotifications do create table(:notifications, primary_key: false) do add :id, :bigint, primary_key: true add :user_id, references(:users, on_delete: :delete_all), null: false - add :actor_id, references(:actors, on_delete: :nilify_all) + add :actor_id, references(:users, on_delete: :nilify_all) add :type, :string, null: false add :data, :map add :read, :boolean, default: false, null: false diff --git a/priv/repo/migrations/20250615131534_create_moderations_logs.exs b/priv/repo/migrations/20250606103527_create_moderations_logs.exs similarity index 100% rename from priv/repo/migrations/20250615131534_create_moderations_logs.exs rename to priv/repo/migrations/20250606103527_create_moderations_logs.exs diff --git a/priv/repo/migrations/20250615131836_create_follows.exs b/priv/repo/migrations/20250606103707_create_follows.exs similarity index 100% rename from priv/repo/migrations/20250615131836_create_follows.exs rename to priv/repo/migrations/20250606103707_create_follows.exs diff --git a/priv/repo/migrations/20250615131610_create_sessions.exs b/priv/repo/migrations/20250606131715_create_sessions.exs similarity index 100% rename from priv/repo/migrations/20250615131610_create_sessions.exs rename to priv/repo/migrations/20250606131715_create_sessions.exs diff --git a/priv/repo/migrations/20250615131644_create_media_attachments.exs b/priv/repo/migrations/20250606132108_create_media_attachments.exs similarity index 100% rename from priv/repo/migrations/20250615131644_create_media_attachments.exs rename to priv/repo/migrations/20250606132108_create_media_attachments.exs diff --git a/priv/repo/migrations/20250615131856_create_activities.exs b/priv/repo/migrations/20250607124601_create_activities.exs similarity index 100% rename from priv/repo/migrations/20250615131856_create_activities.exs rename to priv/repo/migrations/20250607124601_create_activities.exs diff --git a/priv/repo/migrations/20250615130714_create_actors.exs b/priv/repo/migrations/20250615130714_create_actors.exs deleted file mode 100644 index 355f3e0..0000000 --- a/priv/repo/migrations/20250615130714_create_actors.exs +++ /dev/null @@ -1,33 +0,0 @@ -defmodule Nulla.Repo.Migrations.CreateActors do - use Ecto.Migration - - 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 :featured, :string - add :featuredTags, :string - add :preferredUsername, :string - add :name, :string - add :summary, :string - add :url, :string - add :manuallyApprovesFollowers, :boolean - add :discoverable, :boolean, default: true - add :indexable, :boolean, default: true - add :published, :utc_datetime - add :memorial, :boolean, default: false - add :publicKey, :map - add :tag, {:array, :map} - add :attachment, {:array, :map} - add :endpoints, :map - add :icon, :map - add :image, :map - add :vcard_bday, :date - add :vcard_Address, :string - end - end -end diff --git a/priv/repo/migrations/20250615131158_create_users.exs b/priv/repo/migrations/20250615131158_create_users.exs deleted file mode 100644 index e89889b..0000000 --- a/priv/repo/migrations/20250615131158_create_users.exs +++ /dev/null @@ -1,20 +0,0 @@ -defmodule Nulla.Repo.Migrations.CreateUsers do - use Ecto.Migration - - def change do - create table(:users, primary_key: false) do - add :id, :bigint, primary_key: true - add :email, :string - add :password, :string - add :privateKeyPem, :string - add :last_active_at, :utc_datetime - - add :actor_id, references(:actors, column: :id, type: :bigint, on_delete: :delete_all), - null: false - - timestamps(type: :utc_datetime) - end - - create unique_index(:users, [:actor_id]) - end -end