diff --git a/lib/nulla/models/follow.ex b/lib/nulla/models/follow.ex index 3646881..bcd92f7 100644 --- a/lib/nulla/models/follow.ex +++ b/lib/nulla/models/follow.ex @@ -1,6 +1,8 @@ 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 @@ -17,4 +19,13 @@ 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 946f58b..70e1dcc 100644 --- a/lib/nulla/models/notification.ex +++ b/lib/nulla/models/notification.ex @@ -1,6 +1,8 @@ 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 @@ -8,8 +10,8 @@ defmodule Nulla.Models.Notification do field :data, :map field :read, :boolean, default: false - belongs_to :user, Nulla.Models.User - belongs_to :actor, Nulla.Models.User + belongs_to :user, User + belongs_to :actor, Actor timestamps() end diff --git a/lib/nulla/models/user.ex b/lib/nulla/models/user.ex index ce1ce22..4328761 100644 --- a/lib/nulla/models/user.ex +++ b/lib/nulla/models/user.ex @@ -5,34 +5,18 @@ 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 :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 :privateKeyPem, :string field :last_active_at, :utc_datetime - has_many :user_sessions, Nulla.Models.Session - has_many :notes, Nulla.Models.Note - has_many :media_attachments, through: [:notes, :media_attachments] + belongs_to :actor, Actor, define_field: false, foreign_key: :id, type: :integer + has_many :user_sessions, Session timestamps(type: :utc_datetime) end @@ -41,57 +25,24 @@ defmodule Nulla.Models.User do def changeset(user, attrs) do user |> cast(attrs, [ - :username, - :domain, :email, :password, - :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 + :privateKeyPem, + :last_active_at, + :actor_id ]) |> validate_required([ - :username, - :domain, :email, :password, - :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 + :privateKeyPem, + :last_active_at, + :actor_id ]) end - def create_user(attrs) do - id = Snowflake.next_id() - - %User{} - |> User.changeset(attrs) - |> Ecto.Changeset.put_change(:id, id) + def create_user(attrs) when is_map(attrs) do + %__MODULE__{} + |> changeset(attrs) |> Repo.insert() end @@ -99,6 +50,13 @@ 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 80a66c3..fc788c0 100644 --- a/lib/nulla/utils.ex +++ b/lib/nulla/utils.ex @@ -87,4 +87,38 @@ 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 e4ed4c4..d0ba0e3 100644 --- a/lib/nulla_web/controllers/inbox_controller.ex +++ b/lib/nulla_web/controllers/inbox_controller.ex @@ -1,25 +1,23 @@ defmodule NullaWeb.InboxController do use NullaWeb, :controller + alias Nulla.Models.Follow + alias Nulla.Utils - 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"}) + 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 end end diff --git a/lib/nulla_web/controllers/note_controller.ex b/lib/nulla_web/controllers/note_controller.ex index bff4723..2b271be 100644 --- a/lib/nulla_web/controllers/note_controller.ex +++ b/lib/nulla_web/controllers/note_controller.ex @@ -5,33 +5,11 @@ defmodule NullaWeb.NoteController do alias Nulla.Models.Note alias Nulla.Models.InstanceSettings - 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 + def show(conn, %{"username" => username, "id" => id}) do accept = List.first(get_req_header(conn, "accept")) instance_settings = InstanceSettings.get_instance_settings!() domain = instance_settings.domain - note = Note.get_note!(note_id) |> Repo.preload([:user, :media_attachments]) + note = Note.get_note!(id) |> Repo.preload([:user, :media_attachments]) if username != note.user.username do conn @@ -48,38 +26,4 @@ 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 fd9c8c4..b62214b 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 show(conn, %{"username" => username} = params) do + def outbox(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 6e50c78..edaaf09 100644 --- a/lib/nulla_web/router.ex +++ b/lib/nulla_web/router.ex @@ -20,12 +20,31 @@ 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 - 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 + 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 end # Other scopes may use custom stacks.