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