From 4fb1e200f1fb792718c8f9613bbef6c33c5c6583 Mon Sep 17 00:00:00 2001 From: miraikumiko Date: Sun, 8 Jun 2025 10:58:48 +0200 Subject: [PATCH] mix format --- lib/nulla/activitypub.ex | 78 +++++++++++-------- lib/nulla/models/activity.ex | 21 +++++ lib/nulla/models/instance_settings.ex | 22 +++++- lib/nulla/models/note.ex | 6 +- lib/nulla/models/user.ex | 42 +++++++++- lib/nulla/uploader.ex | 2 +- lib/nulla_web/controllers/inbox_controller.ex | 25 ++++++ lib/nulla_web/controllers/user_controller.ex | 2 +- lib/nulla_web/controllers/user_html.ex | 23 ++++-- .../controllers/user_html/show.html.heex | 45 ++++++----- lib/nulla_web/router.ex | 7 +- .../20250607124601_create_activities.exs | 17 ++++ 12 files changed, 221 insertions(+), 69 deletions(-) create mode 100644 lib/nulla/models/activity.ex create mode 100644 lib/nulla_web/controllers/inbox_controller.ex create mode 100644 priv/repo/migrations/20250607124601_create_activities.exs diff --git a/lib/nulla/activitypub.ex b/lib/nulla/activitypub.ex index 2bea870..66a439a 100644 --- a/lib/nulla/activitypub.ex +++ b/lib/nulla/activitypub.ex @@ -25,9 +25,9 @@ defmodule Nulla.ActivityPub do ] end - @spec ap_user(String.t(), Nulla.Models.User.t()) :: Jason.OrderedObject.t() - def ap_user(domain, user) do - Jason.OrderedObject.new([ + @spec user(String.t(), Nulla.Models.User.t()) :: Jason.OrderedObject.t() + def user(domain, user) do + Jason.OrderedObject.new( "@context": context(), id: "https://#{domain}/@#{user.username}", type: "Person", @@ -45,51 +45,52 @@ defmodule Nulla.ActivityPub do indexable: user.is_indexable, published: DateTime.to_iso8601(user.inserted_at), memorial: user.is_memorial, - publicKey: Jason.OrderedObject.new( - id: "https://#{domain}/@#{user.username}#main-key", - owner: "https://#{domain}/@#{user.username}", - publicKeyPem: user.public_key - ), - tag: Enum.map(user.tags, fn tag -> + publicKey: + Jason.OrderedObject.new( + id: "https://#{domain}/@#{user.username}#main-key", + owner: "https://#{domain}/@#{user.username}", + publicKeyPem: user.public_key + ), + tag: + Enum.map(user.tags, fn tag -> Jason.OrderedObject.new( type: "Hashtag", href: "https://#{domain}/tags/#{tag}", name: "##{tag}" ) end), - attachment: Enum.map(user.fields, fn {name, value} -> + attachment: + Enum.map(user.fields, fn {name, value} -> Jason.OrderedObject.new( type: "PropertyValue", name: name, value: value ) end), - endpoints: Jason.OrderedObject.new( - sharedInbox: "https://#{domain}/inbox" - ), - icon: Jason.OrderedObject.new( - type: "Image", - mediaType: MIME.from_path(user.avatar), - url: "https://#{domain}#{user.avatar}" - ), - image: Jason.OrderedObject.new( - type: "Image", - mediaType: MIME.from_path(user.banner), - url: "https://#{domain}#{user.banner}" - ), + endpoints: Jason.OrderedObject.new(sharedInbox: "https://#{domain}/inbox"), + icon: + Jason.OrderedObject.new( + type: "Image", + mediaType: MIME.from_path(user.avatar), + url: "https://#{domain}#{user.avatar}" + ), + image: + Jason.OrderedObject.new( + type: "Image", + mediaType: MIME.from_path(user.banner), + url: "https://#{domain}#{user.banner}" + ), "vcard:bday": user.birthday, "vcard:Address": user.location - ]) + ) end @spec note(String.t(), Nulla.Models.User.t(), Nulla.Models.Note.t()) :: Jason.OrderedObject.t() def note(domain, user, note) do - Jason.OrderedObject.new([ + Jason.OrderedObject.new( "@context": [ "https://www.w3.org/ns/activitystreams", - Jason.OrderedObject.new( - sensitive: "as:sensitive" - ) + Jason.OrderedObject.new(sensitive: "as:sensitive") ], id: "https://#{domain}/@#{user.username}/#{note.id}", type: "Note", @@ -106,16 +107,25 @@ defmodule Nulla.ActivityPub do ], sensetive: false, content: note.content, - contentMap: Jason.OrderedObject.new( - "#{note.language}": "

@rf@mastodonsocial.ru Вниманию новичкам!

Вам небольшое руководство о том, как импротировать пост, которого нет в вашей ленте.

" - ), + contentMap: Jason.OrderedObject.new("#{note.language}": note.content), attachment: [ Jason.OrderedObject.new( type: "Document", - mediaType: "video/mp4", - url: "https://mastodon.ml/system/media_attachments/files/000/040/494/original/8c06de179c11daea.mp4" + mediaType: "#{note.media_attachment.mime_type}", + url: "https://#{domain}/files/#{note.media_attachment.file}" ) ] - ]) + ) + end + + def activity(domain, action) do + Jason.OrderedObject.new( + "@context": "https://www.w3.org/ns/activitystreams", + id: "https://#{domain}/activities/#{action.id}", + type: action.type, + actor: action.actor, + object: action.object, + to: action.to + ) end end diff --git a/lib/nulla/models/activity.ex b/lib/nulla/models/activity.ex new file mode 100644 index 0000000..bf0101e --- /dev/null +++ b/lib/nulla/models/activity.ex @@ -0,0 +1,21 @@ +defmodule Nulla.Models.Activity do + use Ecto.Schema + import Ecto.Changeset + + schema "activities" do + field :type, :string + field :actor, :string + field :object, :map + field :cc, {:array, :string}, default: [] + + timestamps() + end + + @doc false + def changeset(activity, attrs) do + activity + |> cast(attrs, [:type, :actor, :object, :to]) + |> validate_required([:type, :actor, :object]) + |> validate_inclusion(:type, ~w(Create Update Delete Undo Like Announce Follow Accept Reject)) + end +end diff --git a/lib/nulla/models/instance_settings.ex b/lib/nulla/models/instance_settings.ex index 96280c1..5ed8860 100644 --- a/lib/nulla/models/instance_settings.ex +++ b/lib/nulla/models/instance_settings.ex @@ -18,8 +18,26 @@ defmodule Nulla.Models.InstanceSettings do @doc false def changeset(instance_settings, attrs) do instance_settings - |> cast(attrs, [:name, :description, :domain, :registration, :max_characters, :max_upload_size, :public_key, :private_key]) - |> validate_required([:name, :description, :domain, :registration, :max_characters, :max_upload_size, :public_key, :private_key]) + |> cast(attrs, [ + :name, + :description, + :domain, + :registration, + :max_characters, + :max_upload_size, + :public_key, + :private_key + ]) + |> validate_required([ + :name, + :description, + :domain, + :registration, + :max_characters, + :max_upload_size, + :public_key, + :private_key + ]) end def get_instance_settings!, do: Repo.one!(InstanceSettings) diff --git a/lib/nulla/models/note.ex b/lib/nulla/models/note.ex index f4d5960..965e642 100644 --- a/lib/nulla/models/note.ex +++ b/lib/nulla/models/note.ex @@ -7,7 +7,11 @@ defmodule Nulla.Models.Note do schema "notes" do field :content, :string - field :visibility, Ecto.Enum, values: [:public, :unlisted, :followers, :private], default: :public + + field :visibility, Ecto.Enum, + values: [:public, :unlisted, :followers, :private], + default: :public + field :sensitive, :boolean, default: false field :language, :string field :in_reply_to, :string diff --git a/lib/nulla/models/user.ex b/lib/nulla/models/user.ex index d350ad0..cd20f48 100644 --- a/lib/nulla/models/user.ex +++ b/lib/nulla/models/user.ex @@ -35,8 +35,46 @@ defmodule Nulla.Models.User do @doc false def changeset(user, attrs) do user - |> cast(attrs, [:username, :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]) - |> validate_required([:username, :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]) + |> cast(attrs, [ + :username, + :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 + ]) + |> validate_required([ + :username, + :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 + ]) end def get_user_by_username!(username), do: Repo.get_by!(User, username: username) diff --git a/lib/nulla/uploader.ex b/lib/nulla/uploader.ex index 9704674..2abd925 100644 --- a/lib/nulla/uploader.ex +++ b/lib/nulla/uploader.ex @@ -11,7 +11,7 @@ defmodule Nulla.Uploader do |> Enum.chunk_every(3) |> Enum.map(&Enum.join/1) - filename = String.slice(hash, 15..-1) <> file_type + filename = String.slice(hash, 15..-1//1) <> file_type relative_path = Path.join(segments) <> "/" <> filename dest_path = Path.join(["priv/static/files", relative_path]) diff --git a/lib/nulla_web/controllers/inbox_controller.ex b/lib/nulla_web/controllers/inbox_controller.ex new file mode 100644 index 0000000..e4ed4c4 --- /dev/null +++ b/lib/nulla_web/controllers/inbox_controller.ex @@ -0,0 +1,25 @@ +defmodule NullaWeb.InboxController do + use NullaWeb, :controller + + 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"}) + end +end diff --git a/lib/nulla_web/controllers/user_controller.ex b/lib/nulla_web/controllers/user_controller.ex index 0f078e0..4c93d99 100644 --- a/lib/nulla_web/controllers/user_controller.ex +++ b/lib/nulla_web/controllers/user_controller.ex @@ -15,7 +15,7 @@ defmodule NullaWeb.UserController do if accept in ["application/activity+json", "application/ld+json"] do conn |> put_resp_content_type("application/activity+json") - |> json(ActivityPub.ap_user(domain, user)) + |> json(ActivityPub.user(domain, user)) else render(conn, :show, domain: domain, user: user, notes: notes, layout: false) end diff --git a/lib/nulla_web/controllers/user_html.ex b/lib/nulla_web/controllers/user_html.ex index fe6103e..2f1ec2f 100644 --- a/lib/nulla_web/controllers/user_html.ex +++ b/lib/nulla_web/controllers/user_html.ex @@ -25,12 +25,19 @@ defmodule NullaWeb.UserHTML do relative = cond do - diff == 0 -> "today" - diff == 1 -> "1 day ago" - diff < 30 -> "#{diff} days ago" + diff == 0 -> + "today" + + diff == 1 -> + "1 day ago" + + diff < 30 -> + "#{diff} days ago" + diff < 365 -> months = Timex.diff(now, date, :months) if months == 1, do: "1 month ago", else: "#{months} months ago" + true -> years = Timex.diff(now, date, :years) if years == 1, do: "1 year ago", else: "#{years} years ago" @@ -46,7 +53,7 @@ defmodule NullaWeb.UserHTML do def format_note_datetime_diff(datetime) do now = Timex.now() diff = Timex.diff(now, datetime, :seconds) - + cond do diff < 60 -> "now" @@ -59,15 +66,15 @@ defmodule NullaWeb.UserHTML do hours = div(diff, 3600) "#{hours}h ago" - diff < 518400 -> + diff < 518_400 -> days = div(diff, 86400) "#{days}d ago" - diff < 2419200 -> - weeks = div(diff, 604800) + diff < 2_419_200 -> + weeks = div(diff, 604_800) "#{weeks}w ago" - diff < 28512000 -> + diff < 28_512_000 -> months = Timex.diff(now, datetime, :months) "#{months}mo ago" diff --git a/lib/nulla_web/controllers/user_html/show.html.heex b/lib/nulla_web/controllers/user_html/show.html.heex index 9ce9f87..9184f8b 100644 --- a/lib/nulla_web/controllers/user_html/show.html.heex +++ b/lib/nulla_web/controllers/user_html/show.html.heex @@ -1,8 +1,14 @@
- +
- +
@@ -12,8 +18,13 @@
- - + +
@@ -27,28 +38,28 @@
<.icon name="hero-map-pin" class="mt-0.5 h-5 w-5 flex-none" />
-
<%= @user.location %>
+
{@user.location}
<% end %> <%= if @user.birthday do %>
<.icon name="hero-cake" class="mt-0.5 h-5 w-5 flex-none" />
-
<%= format_birthdate(@user.birthday) %>
+
{format_birthdate(@user.birthday)}
<% end %>
<.icon name="hero-calendar" class="mt-0.5 h-5 w-5 flex-none" />
-
<%= format_registration_date(@user.inserted_at) %>
+
{format_registration_date(@user.inserted_at)}
<%= if @user.fields do %>
<%= for {key, value} <- @user.fields do %> -
<%= key %>
+
{key}
<%= if Regex.match?(~r{://}, value) do %> - <%= Regex.replace(~r{^\w+://}, value, "") %> + {Regex.replace(~r{^\w+://}, value, "")} <% else %> - <%= value %> + {value} <% end %>
<% end %> @@ -70,15 +81,15 @@ <%= for note <- @notes do %>
- +
- <%= @user.realname %> + {@user.realname} - @<%= @user.username %>@<%= @domain %> + @{@user.username}@{@domain}
@@ -92,11 +103,11 @@ <% :private -> %> <.icon name="hero-at-symbol" class="h-5 w-5" /> <% end %> - <%= format_note_datetime_diff(note.inserted_at) %> + {format_note_datetime_diff(note.inserted_at)}
-

<%= note.content %>

+

{note.content}

@@ -110,8 +121,6 @@
-
- -
+
diff --git a/lib/nulla_web/router.ex b/lib/nulla_web/router.ex index 4f956cd..5af267b 100644 --- a/lib/nulla_web/router.ex +++ b/lib/nulla_web/router.ex @@ -11,13 +11,16 @@ defmodule NullaWeb.Router do end pipeline :api do - plug :accepts, ["json"] + plug :accepts, ["activity+json", "ld+json"] + + post "/inbox", InboxController, :receive + post "/@:username/inbox", InboxController, :receive + get "/@:username/outbox", OutboxController, :index end scope "/", NullaWeb do pipe_through :browser - get "/", PageController, :home get "/@:username", UserController, :show resources "/users", UserController end diff --git a/priv/repo/migrations/20250607124601_create_activities.exs b/priv/repo/migrations/20250607124601_create_activities.exs new file mode 100644 index 0000000..0ed3cf2 --- /dev/null +++ b/priv/repo/migrations/20250607124601_create_activities.exs @@ -0,0 +1,17 @@ +defmodule Nulla.Repo.Migrations.CreateActivities do + use Ecto.Migration + + def change do + create table(:activities) do + add :type, :string, null: false + add :actor, :string, null: false + add :object, :map, null: false + add :to, {:array, :string}, default: [] + + timestamps() + end + + create index(:activities, [:actor]) + create index(:activities, [:type]) + end +end