Add following and followers

This commit is contained in:
Mirai Kumiko 2025-06-11 15:03:21 +02:00
parent f8bedff913
commit 0b88881934
Signed by: miraikumiko
GPG key ID: 3F178B1B5E0CB278
8 changed files with 261 additions and 4 deletions

View file

@ -139,4 +139,90 @@ defmodule Nulla.ActivityPub do
to: activity.to
)
end
@spec following(String.t(), Nulla.Models.User.t(), Integer.t()) :: Jason.OrderedObject.t()
def following(domain, user, total) do
Jason.OrderedObject.new(
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://#{domain}/@#{user.username}/following",
type: "OrderedCollection",
totalItems: total,
first: "https://#{domain}/@#{user.username}/following?page=1"
)
end
@spec following(String.t(), Nulla.Models.User.t(), Integer.t(), List.t(), Integer.t(), Integer.t()) :: Jason.OrderedObject.t()
def following(domain, user, total, following_list, page, offset) when is_integer(page) and page > 0 do
data = [
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://#{domain}/@#{user.username}/following?page=#{page}",
type: "OrderedCollectionPage",
totalItems: total,
next: "https://#{domain}/@#{user.username}/following?page=#{page + 1}",
prev: "https://#{domain}/@#{user.username}/following?page=#{page - 1}",
partOf: "https://#{domain}/@#{user.username}/following",
orderedItems: following_list
]
data =
if page <= 1 do
Keyword.delete(data, :prev)
else
data
end
data =
if page * offset > total do
data
|> Keyword.delete(:next)
|> Keyword.delete(:prev)
else
data
end
Jason.OrderedObject.new(data)
end
@spec followers(String.t(), Nulla.Models.User.t(), Integer.t()) :: Jason.OrderedObject.t()
def followers(domain, user, total) do
Jason.OrderedObject.new(
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://#{domain}/@#{user.username}/followers",
type: "OrderedCollection",
totalItems: total,
first: "https://#{domain}/@#{user.username}/followers?page=1"
)
end
@spec followers(String.t(), Nulla.Models.User.t(), Integer.t(), List.t(), Integer.t(), Integer.t()) :: Jason.OrderedObject.t()
def followers(domain, user, total, followers_list, page, offset) when is_integer(page) and page > 0 do
data = [
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://#{domain}/@#{user.username}/followers?page=#{page}",
type: "OrderedCollectionPage",
totalItems: total,
next: "https://#{domain}/@#{user.username}/followers?page=#{page + 1}",
prev: "https://#{domain}/@#{user.username}/followers?page=#{page - 1}",
partOf: "https://#{domain}/@#{user.username}/followers",
orderedItems: followers_list
]
data =
if page <= 1 do
Keyword.delete(data, :prev)
else
data
end
data =
if page * offset > total do
data
|> Keyword.delete(:next)
|> Keyword.delete(:prev)
else
data
end
Jason.OrderedObject.new(data)
end
end

View file

@ -11,6 +11,7 @@ defmodule Nulla.Models.InstanceSettings do
field :registration, :boolean, default: false
field :max_characters, :integer, default: 5000
field :max_upload_size, :integer, default: 50
field :offset, :integer, default: 100
field :public_key, :string
field :private_key, :string
end
@ -25,6 +26,7 @@ defmodule Nulla.Models.InstanceSettings do
:registration,
:max_characters,
:max_upload_size,
:offset,
:public_key,
:private_key
])
@ -35,6 +37,7 @@ defmodule Nulla.Models.InstanceSettings do
:registration,
:max_characters,
:max_upload_size,
:offset,
:public_key,
:private_key
])

87
lib/nulla/utils.ex Normal file
View file

@ -0,0 +1,87 @@
defmodule Nulla.Utils do
import Ecto.Query
alias Nulla.Repo
alias Nulla.Models.User
alias Nulla.Models.Follow
alias Nulla.Models.InstanceSettings
def count_following_by_username!(username) do
case Repo.get_by(User, username: username) do
nil ->
{:error, :user_not_found}
%User{id: user_id} ->
count =
Follow
|> where([f], f.user_id == ^user_id)
|> select([f], count(f.id))
|> Repo.one()
count
end
end
def get_following_users_by_username!(username, page) when is_integer(page) and page > 0 do
case Repo.get_by(User, username: username) do
nil ->
{:error, :user_not_found}
%User{id: user_id} ->
instance_settings = InstanceSettings.get_instance_settings!()
per_page = instance_settings.offset
offset = (page - 1) * per_page
query =
from [f, u] in
from(f in Follow,
join: u in User, on: u.id == f.target_id,
where: f.user_id == ^user_id,
order_by: [asc: u.inserted_at],
offset: ^offset,
limit: ^per_page,
select: u
)
users = Repo.all(query)
users
end
end
def count_followers_by_username!(username) do
case Repo.get_by(User, username: username) do
nil ->
0
%User{id: user_id} ->
from(f in Follow, where: f.target_id == ^user_id)
|> select([f], count(f.id))
|> Repo.one()
end
end
def get_followers_by_username!(username, page) when is_integer(page) and page > 0 do
case Repo.get_by(User, username: username) do
nil ->
{:error, :user_not_found}
%User{id: user_id} ->
instance_settings = InstanceSettings.get_instance_settings!()
per_page = instance_settings.offset
offset = (page - 1) * per_page
query =
from f in Follow,
where: f.target_id == ^user_id,
join: u in User, on: u.id == f.user_id,
order_by: [asc: u.inserted_at],
offset: ^offset,
limit: ^per_page,
select: u
users = Repo.all(query)
users
end
end
end

View file

@ -66,9 +66,9 @@
</dl>
<% end %>
<div class="flex mt-5 gap-3">
<a href={~p"/@#{@user.username}"}>1.7K Posts</a>
<a href={~p"/@#{@user.username}/following"}>33 Following</a>
<a href={~p"/@#{@user.username}/followers"}>31 Followers</a>
<a href={~p"/@#{@user.username}"}>{length(@notes)} Posts</a>
<a href={~p"/@#{@user.username}/following"}>{@following} Following</a>
<a href={~p"/@#{@user.username}/followers"}>{@followers} Followers</a>
</div>
</div>
<div class="flex justify-between px-20 py-2 mt-5 border-y border-gray-300">

View file

@ -0,0 +1,65 @@
defmodule NullaWeb.FollowController do
use NullaWeb, :controller
alias Nulla.Models.User
alias Nulla.Models.InstanceSettings
alias Nulla.ActivityPub
alias Nulla.Utils
def following(conn, %{"username" => username, "page" => page_param}) do
instance_settings = InstanceSettings.get_instance_settings!()
domain = instance_settings.domain
offset = instance_settings.offset
user = User.get_user_by_username!(username)
total = Utils.count_following_by_username!(user.username)
page =
case Integer.parse(page_param) do
{int, _} when int > 0 -> int
_ -> 1
end
following_list = Utils.get_following_users_by_username!(user.username, page)
conn
|> put_resp_content_type("application/activity+json")
|> json(ActivityPub.following(domain, user, total, following_list, page, offset))
end
def following(conn, %{"username" => username}) do
instance_settings = InstanceSettings.get_instance_settings!()
domain = instance_settings.domain
user = User.get_user_by_username!(username)
total = Utils.count_following_by_username!(user.username)
conn
|> put_resp_content_type("application/activity+json")
|> json(ActivityPub.following(domain, user, total))
end
def followers(conn, %{"username" => username, "page" => page_param}) do
instance_settings = InstanceSettings.get_instance_settings!()
domain = instance_settings.domain
offset = instance_settings.offset
user = User.get_user_by_username!(username)
total = Utils.count_followers_by_username!(user.username)
page =
case Integer.parse(page_param) do
{int, _} when int > 0 -> int
_ -> 1
end
followers_list = Utils.get_followers_by_username!(user.username, page)
conn
|> put_resp_content_type("application/activity+json")
|> json(ActivityPub.followers(domain, user, total, followers_list, page, offset))
end
def followers(conn, %{"username" => username}) do
instance_settings = InstanceSettings.get_instance_settings!()
domain = instance_settings.domain
user = User.get_user_by_username!(username)
total = Utils.count_followers_by_username!(user.username)
conn
|> put_resp_content_type("application/activity+json")
|> json(ActivityPub.followers(domain, user, total))
end
end

View file

@ -4,6 +4,7 @@ defmodule NullaWeb.UserController do
alias Nulla.Models.Note
alias Nulla.Models.InstanceSettings
alias Nulla.ActivityPub
alias Nulla.Utils
def show(conn, %{"username" => username}) do
accept = List.first(get_req_header(conn, "accept"))
@ -17,7 +18,19 @@ defmodule NullaWeb.UserController do
|> put_resp_content_type("application/activity+json")
|> json(ActivityPub.user(domain, user))
else
render(conn, :show, domain: domain, user: user, notes: notes, layout: false)
following = Utils.count_following_by_username!(user.username)
followers = Utils.count_followers_by_username!(user.username)
render(
conn,
:show,
domain: domain,
user: user,
notes: notes,
following: following,
followers: followers,
layout: false
)
end
end
end

View file

@ -22,6 +22,8 @@ defmodule NullaWeb.Router do
pipe_through :browser
get "/@:username", UserController, :show
get "/@:username/following", FollowController, :following
get "/@:username/followers", FollowController, :followers
get "/@:username/:note_id", NoteController, :show
end

View file

@ -9,6 +9,7 @@ defmodule Nulla.Repo.Migrations.CreateInstanceSettings do
add :registration, :boolean, default: false, null: false
add :max_characters, :integer, default: 5000, null: false
add :max_upload_size, :integer, default: 50, null: false
add :offset, :integer, default: 100, null: false
add :public_key, :string
add :private_key, :string