diff --git a/lib/nulla/activitypub.ex b/lib/nulla/activitypub.ex index 081678f..2e58798 100644 --- a/lib/nulla/activitypub.ex +++ b/lib/nulla/activitypub.ex @@ -88,20 +88,22 @@ defmodule Nulla.ActivityPub do @spec note(String.t(), Nulla.Models.Note.t()) :: Jason.OrderedObject.t() def note(domain, note) do attachment = - case note.media_attachments do - [] -> [] - attachments -> - [ - attachment: - Enum.map(attachments, fn att -> - Jason.OrderedObject.new( - type: "Document", - mediaType: att.mime_type, - url: "https://#{domain}/files/#{att.file}" - ) - end) - ] - end + case note.media_attachments do + [] -> + [] + + attachments -> + [ + attachment: + Enum.map(attachments, fn att -> + Jason.OrderedObject.new( + type: "Document", + mediaType: att.mime_type, + url: "https://#{domain}/files/#{att.file}" + ) + end) + ] + end Jason.OrderedObject.new( "@context": [ @@ -151,8 +153,16 @@ defmodule Nulla.ActivityPub do ) 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 + @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}", @@ -194,8 +204,16 @@ defmodule Nulla.ActivityPub do ) 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 + @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}", @@ -225,4 +243,17 @@ defmodule Nulla.ActivityPub do Jason.OrderedObject.new(data) end + + def webfinger(domain, username, resource) do + Jason.OrderedObject.new( + subject: resource, + links: [ + Jason.OrderedObject.new( + rel: "self", + type: "application/activity+json", + href: "https://#{domain}/@#{username}" + ) + ] + ) + end end diff --git a/lib/nulla/key_gen.ex b/lib/nulla/key_gen.ex index 48740c8..c803920 100644 --- a/lib/nulla/key_gen.ex +++ b/lib/nulla/key_gen.ex @@ -5,11 +5,14 @@ defmodule Nulla.KeyGen do {:RSAPrivateKey, :"two-prime", n, e, _d, _p, _q, _dp, _dq, _qi, _other} = rsa_key public_key = {:RSAPublicKey, n, e} - private_entry = {:PrivateKeyInfo, :public_key.der_encode(:RSAPrivateKey, rsa_key), :not_encrypted} - public_entry = {:SubjectPublicKeyInfo, :public_key.der_encode(:RSAPublicKey, public_key), :not_encrypted} + private_entry = + {:PrivateKeyInfo, :public_key.der_encode(:RSAPrivateKey, rsa_key), :not_encrypted} + + public_entry = + {:SubjectPublicKeyInfo, :public_key.der_encode(:RSAPublicKey, public_key), :not_encrypted} private_pem = :public_key.pem_encode([private_entry]) - public_pem = :public_key.pem_encode([public_entry]) + public_pem = :public_key.pem_encode([public_entry]) {public_pem, private_pem} end diff --git a/lib/nulla/models/user.ex b/lib/nulla/models/user.ex index cd20f48..caf90c5 100644 --- a/lib/nulla/models/user.ex +++ b/lib/nulla/models/user.ex @@ -77,5 +77,7 @@ defmodule Nulla.Models.User do ]) end + 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) end diff --git a/lib/nulla/utils.ex b/lib/nulla/utils.ex index 9335dd3..80a66c3 100644 --- a/lib/nulla/utils.ex +++ b/lib/nulla/utils.ex @@ -32,15 +32,17 @@ defmodule Nulla.Utils do offset = (page - 1) * per_page query = - from [f, u] in - from(f in Follow, - join: u in User, on: u.id == f.target_id, + 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) @@ -73,7 +75,8 @@ defmodule Nulla.Utils do query = from f in Follow, where: f.target_id == ^user_id, - join: u in User, on: u.id == f.user_id, + join: u in User, + on: u.id == f.user_id, order_by: [asc: u.inserted_at], offset: ^offset, limit: ^per_page, diff --git a/lib/nulla_web/controllers/follow_controller.ex b/lib/nulla_web/controllers/follow_controller.ex index ff82b6a..6f8a76a 100644 --- a/lib/nulla_web/controllers/follow_controller.ex +++ b/lib/nulla_web/controllers/follow_controller.ex @@ -1,9 +1,9 @@ defmodule NullaWeb.FollowController do use NullaWeb, :controller - alias Nulla.Models.User - alias Nulla.Models.InstanceSettings alias Nulla.ActivityPub alias Nulla.Utils + alias Nulla.Models.User + alias Nulla.Models.InstanceSettings def following(conn, %{"username" => username, "page" => page_param}) do instance_settings = InstanceSettings.get_instance_settings!() @@ -11,16 +11,18 @@ defmodule NullaWeb.FollowController do 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 + 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)) + |> put_resp_content_type("application/activity+json") + |> json(ActivityPub.following(domain, user, total, following_list, page, offset)) end def following(conn, %{"username" => username}) do @@ -30,8 +32,8 @@ defmodule NullaWeb.FollowController do total = Utils.count_following_by_username!(user.username) conn - |> put_resp_content_type("application/activity+json") - |> json(ActivityPub.following(domain, user, total)) + |> put_resp_content_type("application/activity+json") + |> json(ActivityPub.following(domain, user, total)) end def followers(conn, %{"username" => username, "page" => page_param}) do @@ -40,16 +42,18 @@ defmodule NullaWeb.FollowController do 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 + 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)) + |> put_resp_content_type("application/activity+json") + |> json(ActivityPub.followers(domain, user, total, followers_list, page, offset)) end def followers(conn, %{"username" => username}) do @@ -59,7 +63,7 @@ defmodule NullaWeb.FollowController do total = Utils.count_followers_by_username!(user.username) conn - |> put_resp_content_type("application/activity+json") - |> json(ActivityPub.followers(domain, user, total)) + |> put_resp_content_type("application/activity+json") + |> json(ActivityPub.followers(domain, user, total)) end end diff --git a/lib/nulla_web/controllers/note_controller.ex b/lib/nulla_web/controllers/note_controller.ex index ae4516a..bff4723 100644 --- a/lib/nulla_web/controllers/note_controller.ex +++ b/lib/nulla_web/controllers/note_controller.ex @@ -1,9 +1,9 @@ defmodule NullaWeb.NoteController do use NullaWeb, :controller alias Nulla.Repo + alias Nulla.ActivityPub alias Nulla.Models.Note alias Nulla.Models.InstanceSettings - alias Nulla.ActivityPub def index(conn, _params) do notes = Notes.list_notes() @@ -49,10 +49,10 @@ defmodule NullaWeb.NoteController do end end - #def show(conn, %{"id" => id}) do + # def show(conn, %{"id" => id}) do # note = Notes.get_note!(id) # render(conn, :show, note: note) - #end + # end def edit(conn, %{"id" => id}) do note = Notes.get_note!(id) diff --git a/lib/nulla_web/controllers/user_controller.ex b/lib/nulla_web/controllers/user_controller.ex index 45ebd86..a608f48 100644 --- a/lib/nulla_web/controllers/user_controller.ex +++ b/lib/nulla_web/controllers/user_controller.ex @@ -1,10 +1,10 @@ defmodule NullaWeb.UserController do use NullaWeb, :controller + alias Nulla.ActivityPub + alias Nulla.Utils alias Nulla.Models.User 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")) diff --git a/lib/nulla_web/controllers/webfinger_controller.ex b/lib/nulla_web/controllers/webfinger_controller.ex new file mode 100644 index 0000000..ab6e2bf --- /dev/null +++ b/lib/nulla_web/controllers/webfinger_controller.ex @@ -0,0 +1,34 @@ +defmodule NullaWeb.WebfingerController do + use NullaWeb, :controller + alias Nulla.Repo + alias Nulla.ActivityPub + alias Nulla.Models.User + alias Nulla.Models.InstanceSettings + + def show(conn, %{"resource" => resource}) do + case Regex.run(~r/^acct:([^@]+)@(.+)$/, resource) do + [_, username, domain] -> + case User.get_user_by_username(username) do + nil -> + conn + |> put_status(:not_found) + |> json(%{error: "Not Found"}) + + user -> + instance_settings = InstanceSettings.get_instance_settings!() + + if domain == instance_settings.domain do + json(conn, ActivityPub.webfinger(domain, username, resource)) + else + conn + |> put_status(:not_found) + |> json(%{error: "Not Found"}) + end + end + _ -> + conn + |> put_status(:bad_request) + |> json(%{error: "Bad Request"}) + end + end +end diff --git a/lib/nulla_web/router.ex b/lib/nulla_web/router.ex index 04268ed..0ad10df 100644 --- a/lib/nulla_web/router.ex +++ b/lib/nulla_web/router.ex @@ -21,6 +21,8 @@ defmodule NullaWeb.Router do scope "/", NullaWeb do pipe_through :browser + get "/.well-known/webfinger", WebfingerController, :show + get "/@:username", UserController, :show get "/@:username/following", FollowController, :following get "/@:username/followers", FollowController, :followers diff --git a/priv/repo/migrations/20250527054942_create_instance_settings.exs b/priv/repo/migrations/20250527054942_create_instance_settings.exs index 45fd5f9..3f8be91 100644 --- a/priv/repo/migrations/20250527054942_create_instance_settings.exs +++ b/priv/repo/migrations/20250527054942_create_instance_settings.exs @@ -21,11 +21,14 @@ defmodule Nulla.Repo.Migrations.CreateInstanceSettings do execute(fn -> {public_key, private_key} = Nulla.KeyGen.generate_keys() now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) + domain = - Application.get_env(:nulla, NullaWeb.Endpoint, []) - |> Keyword.get(:url, []) - |> Keyword.get(:host, "localhost") + Application.get_env(:nulla, NullaWeb.Endpoint, []) + |> Keyword.get(:url, []) + |> Keyword.get(:host, "localhost") + esc = fn str -> "'#{String.replace(str, "'", "''")}'" end + sql = """ INSERT INTO instance_settings ( name, description, domain, registration, diff --git a/priv/repo/migrations/20250530110822_create_users.exs b/priv/repo/migrations/20250530110822_create_users.exs index 54ab8cb..cc7ab33 100644 --- a/priv/repo/migrations/20250530110822_create_users.exs +++ b/priv/repo/migrations/20250530110822_create_users.exs @@ -8,7 +8,7 @@ defmodule Nulla.Repo.Migrations.CreateUsers do add :password, :string add :is_moderator, :boolean, default: false, null: false add :realname, :string - add :bio, :string + add :bio, :text add :location, :string add :birthday, :date add :fields, :map diff --git a/priv/static/favicon.ico b/priv/static/favicon.ico index 7f372bf..145cc29 100644 Binary files a/priv/static/favicon.ico and b/priv/static/favicon.ico differ