diff --git a/config/config.exs b/config/config.exs index fc9357b..c3983d9 100644 --- a/config/config.exs +++ b/config/config.exs @@ -23,8 +23,7 @@ config :nulla, NullaWeb.Endpoint, live_view: [signing_salt: "jcAt5/U+"] # Snowflake configuration -config :nulla, :snowflake, - worker_id: 1 +config :nulla, :snowflake, worker_id: 1 # Configures the mailer # diff --git a/lib/nulla/activitypub.ex b/lib/nulla/activitypub.ex index 019aef8..ec6332e 100644 --- a/lib/nulla/activitypub.ex +++ b/lib/nulla/activitypub.ex @@ -304,4 +304,16 @@ defmodule Nulla.ActivityPub do ) ) end + + @spec outbox(String.t(), String.t(), Integer.t()) :: Jason.OrderedObject.t() + def outbox(domain, username, total) do + Jason.OrderedObject.new( + "@context": "https://www.w3.org/ns/activitystreams", + id: "https://#{domain}/@#{username}/outbox", + type: "OrderedCollection", + totalItems: total, + first: "https://#{domain}/@#{username}/outbox?page=true", + last: "https://#{domain}/@#{username}/outbox?min_id=0&page=true" + ) + end end diff --git a/lib/nulla/key_gen.ex b/lib/nulla/keygen.ex similarity index 100% rename from lib/nulla/key_gen.ex rename to lib/nulla/keygen.ex diff --git a/lib/nulla/models/note.ex b/lib/nulla/models/note.ex index efaf328..83e1eb5 100644 --- a/lib/nulla/models/note.ex +++ b/lib/nulla/models/note.ex @@ -33,4 +33,9 @@ defmodule Nulla.Models.Note do def get_note!(id), do: Repo.get!(Note, id) def get_all_notes!(user_id), do: Repo.all(from n in Note, where: n.user_id == ^user_id) + + def get_total_notes_count(user_id) do + from(n in Note, where: n.user_id == ^user_id) + |> Repo.aggregate(:count, :id) + end end diff --git a/lib/nulla/models/user.ex b/lib/nulla/models/user.ex index b3ef7ce..ce1ce22 100644 --- a/lib/nulla/models/user.ex +++ b/lib/nulla/models/user.ex @@ -1,6 +1,7 @@ defmodule Nulla.Models.User do use Ecto.Schema import Ecto.Changeset + import Ecto.Query alias Nulla.Repo alias Nulla.Snowflake alias Nulla.Models.User @@ -8,6 +9,7 @@ defmodule Nulla.Models.User do @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 @@ -15,7 +17,7 @@ defmodule Nulla.Models.User do field :bio, :string field :location, :string field :birthday, :date - field :fields, :map + field :fields, {:array, :map} field :tags, {:array, :string} field :follow_approval, :boolean, default: false field :is_bot, :boolean, default: false @@ -26,6 +28,7 @@ defmodule Nulla.Models.User do field :public_key, :string field :avatar, :string field :banner, :string + field :last_active_at, :utc_datetime has_many :user_sessions, Nulla.Models.Session has_many :notes, Nulla.Models.Note @@ -39,6 +42,7 @@ defmodule Nulla.Models.User do user |> cast(attrs, [ :username, + :domain, :email, :password, :is_moderator, @@ -55,10 +59,12 @@ defmodule Nulla.Models.User do :private_key, :public_key, :avatar, - :banner + :banner, + :last_active_at ]) |> validate_required([ :username, + :domain, :email, :password, :is_moderator, @@ -75,7 +81,8 @@ defmodule Nulla.Models.User do :private_key, :public_key, :avatar, - :banner + :banner, + :last_active_at ]) end @@ -91,4 +98,23 @@ 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_total_users_count(domain) do + Repo.aggregate(from(u in User, where: u.domain == ^domain), :count, :id) + end + + def get_active_users_count(domain, days) do + cutoff = DateTime.add(DateTime.utc_now(), -days * 86400, :second) + + from(u in User, + where: u.domain == ^domain and u.last_active_at > ^cutoff + ) + |> Repo.aggregate(:count, :id) + end + + def update_last_active(user) do + user + |> Ecto.Changeset.change(last_active_at: DateTime.utc_now()) + |> Repo.update() + end end diff --git a/lib/nulla_web/components/templates.ex b/lib/nulla_web/components/templates.ex index 87fc303..acec111 100644 --- a/lib/nulla_web/components/templates.ex +++ b/lib/nulla_web/components/templates.ex @@ -3,14 +3,6 @@ defmodule NullaWeb.UserHTML do embed_templates "templates/user/*" - @doc """ - Renders a user form. - """ - attr :changeset, Ecto.Changeset, required: true - attr :action, :string, required: true - - def user_form(assigns) - def format_birthdate(date) do formatted = Date.to_string(date) |> String.replace("-", "/") age = Timex.diff(Timex.today(), date, :years) @@ -84,17 +76,3 @@ defmodule NullaWeb.UserHTML do end end end - -defmodule NullaWeb.NoteHTML do - use NullaWeb, :html - - embed_templates "templates/note/*" - - @doc """ - Renders a note form. - """ - attr :changeset, Ecto.Changeset, required: true - attr :action, :string, required: true - - def note_form(assigns) -end diff --git a/lib/nulla_web/components/templates/note/edit.html.heex b/lib/nulla_web/components/templates/note/edit.html.heex deleted file mode 100644 index 3bef388..0000000 --- a/lib/nulla_web/components/templates/note/edit.html.heex +++ /dev/null @@ -1,8 +0,0 @@ -<.header> - Edit Note {@note.id} - <:subtitle>Use this form to manage note records in your database. - - -<.note_form changeset={@changeset} action={~p"/notes/#{@note}"} /> - -<.back navigate={~p"/notes"}>Back to notes diff --git a/lib/nulla_web/components/templates/note/index.html.heex b/lib/nulla_web/components/templates/note/index.html.heex deleted file mode 100644 index ffeedbc..0000000 --- a/lib/nulla_web/components/templates/note/index.html.heex +++ /dev/null @@ -1,23 +0,0 @@ -<.header> - Listing Notes - <:actions> - <.link href={~p"/notes/new"}> - <.button>New Note - - - - -<.table id="notes" rows={@notes} row_click={&JS.navigate(~p"/notes/#{&1}")}> - <:col :let={note} label="Content">{note.content} - <:action :let={note}> -
- <.link navigate={~p"/notes/#{note}"}>Show -
- <.link navigate={~p"/notes/#{note}/edit"}>Edit - - <:action :let={note}> - <.link href={~p"/notes/#{note}"} method="delete" data-confirm="Are you sure?"> - Delete - - - diff --git a/lib/nulla_web/components/templates/note/new.html.heex b/lib/nulla_web/components/templates/note/new.html.heex deleted file mode 100644 index 4cf47a4..0000000 --- a/lib/nulla_web/components/templates/note/new.html.heex +++ /dev/null @@ -1,8 +0,0 @@ -<.header> - New Note - <:subtitle>Use this form to manage note records in your database. - - -<.note_form changeset={@changeset} action={~p"/notes"} /> - -<.back navigate={~p"/notes"}>Back to notes diff --git a/lib/nulla_web/components/templates/note/note_form.html.heex b/lib/nulla_web/components/templates/note/note_form.html.heex deleted file mode 100644 index da6ac0f..0000000 --- a/lib/nulla_web/components/templates/note/note_form.html.heex +++ /dev/null @@ -1,9 +0,0 @@ -<.simple_form :let={f} for={@changeset} action={@action}> - <.error :if={@changeset.action}> - Oops, something went wrong! Please check the errors below. - - <.input field={f[:content]} type="text" label="Content" /> - <:actions> - <.button>Save Note - - diff --git a/lib/nulla_web/components/templates/note/show.html.heex b/lib/nulla_web/components/templates/note/show.html.heex deleted file mode 100644 index d7f2f70..0000000 --- a/lib/nulla_web/components/templates/note/show.html.heex +++ /dev/null @@ -1,15 +0,0 @@ -<.header> - Note {@note.id} - <:subtitle>This is a note record from your database. - <:actions> - <.link href={~p"/notes/#{@note}/edit"}> - <.button>Edit note - - - - -<.list> - <:item title="Content">{@note.content} - - -<.back navigate={~p"/notes"}>Back to notes diff --git a/lib/nulla_web/components/templates/user/edit.html.heex b/lib/nulla_web/components/templates/user/edit.html.heex deleted file mode 100644 index 2f8aa66..0000000 --- a/lib/nulla_web/components/templates/user/edit.html.heex +++ /dev/null @@ -1,8 +0,0 @@ -<.header> - Edit User {@user.id} - <:subtitle>Use this form to manage user records in your database. - - -<.user_form changeset={@changeset} action={~p"/users/#{@user}"} /> - -<.back navigate={~p"/users"}>Back to users diff --git a/lib/nulla_web/components/templates/user/index.html.heex b/lib/nulla_web/components/templates/user/index.html.heex deleted file mode 100644 index 9eca5b7..0000000 --- a/lib/nulla_web/components/templates/user/index.html.heex +++ /dev/null @@ -1,23 +0,0 @@ -<.header> - Listing Users - <:actions> - <.link href={~p"/users/new"}> - <.button>New User - - - - -<.table id="users" rows={@users} row_click={&JS.navigate(~p"/users/#{&1}")}> - <:col :let={user} label="Username">{user.username} - <:action :let={user}> -
- <.link navigate={~p"/users/#{user}"}>Show -
- <.link navigate={~p"/users/#{user}/edit"}>Edit - - <:action :let={user}> - <.link href={~p"/users/#{user}"} method="delete" data-confirm="Are you sure?"> - Delete - - - diff --git a/lib/nulla_web/components/templates/user/new.html.heex b/lib/nulla_web/components/templates/user/new.html.heex deleted file mode 100644 index 9248fb0..0000000 --- a/lib/nulla_web/components/templates/user/new.html.heex +++ /dev/null @@ -1,8 +0,0 @@ -<.header> - New User - <:subtitle>Use this form to manage user records in your database. - - -<.user_form changeset={@changeset} action={~p"/users"} /> - -<.back navigate={~p"/users"}>Back to users diff --git a/lib/nulla_web/components/templates/user/user_form.html.heex b/lib/nulla_web/components/templates/user/user_form.html.heex deleted file mode 100644 index 6871618..0000000 --- a/lib/nulla_web/components/templates/user/user_form.html.heex +++ /dev/null @@ -1,9 +0,0 @@ -<.simple_form :let={f} for={@changeset} action={@action}> - <.error :if={@changeset.action}> - Oops, something went wrong! Please check the errors below. - - <.input field={f[:username]} type="text" label="Username" /> - <:actions> - <.button>Save User - - diff --git a/lib/nulla_web/controllers/nodeinfo_controller.ex b/lib/nulla_web/controllers/nodeinfo_controller.ex index 92efc88..46a3bed 100644 --- a/lib/nulla_web/controllers/nodeinfo_controller.ex +++ b/lib/nulla_web/controllers/nodeinfo_controller.ex @@ -14,11 +14,16 @@ defmodule NullaWeb.NodeinfoController do def show(conn, _params) do version = Application.spec(:nulla, :vsn) |> to_string() + instance_settings = InstanceSettings.get_instance_settings!() + domain = instance_settings.domain + total = User.get_total_users_count(domain) + month = User.get_active_users_count(domain, 30) + halfyear = User.get_active_users_count(domain, 180) users = %{ - total: 0, - month: 0, - halfyear: 0 + total: total, + month: month, + halfyear: halfyear } instance_settings = InstanceSettings.get_instance_settings!() diff --git a/lib/nulla_web/controllers/outbox_controller.ex b/lib/nulla_web/controllers/outbox_controller.ex new file mode 100644 index 0000000..55d13da --- /dev/null +++ b/lib/nulla_web/controllers/outbox_controller.ex @@ -0,0 +1,18 @@ +defmodule NullaWeb.OutboxController do + use NullaWeb, :controller + alias Nulla.ActivityPub + alias Nulla.Models.User + alias Nulla.Models.Note + alias Nulla.Models.InstanceSettings + + def show(conn, %{"username" => username}) 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) + + conn + |> put_resp_content_type("application/activity+json") + |> send_resp(200, Jason.encode!(ActivityPub.outbox(domain, username, total))) + end +end diff --git a/lib/nulla_web/controllers/user_controller.ex b/lib/nulla_web/controllers/user_controller.ex index a5467e7..d85d580 100644 --- a/lib/nulla_web/controllers/user_controller.ex +++ b/lib/nulla_web/controllers/user_controller.ex @@ -16,7 +16,7 @@ defmodule NullaWeb.UserController do if accept in ["application/activity+json", "application/ld+json"] do conn |> put_resp_content_type("application/activity+json") - |> send_resp(200, ActivityPub.user(domain, user)) + |> 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) diff --git a/lib/nulla_web/router.ex b/lib/nulla_web/router.ex index ac27911..6e50c78 100644 --- a/lib/nulla_web/router.ex +++ b/lib/nulla_web/router.ex @@ -22,6 +22,7 @@ defmodule NullaWeb.Router do get "/nodeinfo/2.0", NodeinfoController, :show 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 diff --git a/priv/repo/migrations/20250530110822_create_users.exs b/priv/repo/migrations/20250530110822_create_users.exs index b597c2f..2b48cfa 100644 --- a/priv/repo/migrations/20250530110822_create_users.exs +++ b/priv/repo/migrations/20250530110822_create_users.exs @@ -5,6 +5,7 @@ defmodule Nulla.Repo.Migrations.CreateUsers 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 @@ -12,7 +13,7 @@ defmodule Nulla.Repo.Migrations.CreateUsers do add :bio, :text add :location, :string add :birthday, :date - add :fields, :map + 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 @@ -23,6 +24,7 @@ defmodule Nulla.Repo.Migrations.CreateUsers do add :public_key, :string, null: false add :avatar, :string add :banner, :string + add :last_active_at, :utc_datetime timestamps(type: :utc_datetime) end