feat: Ingest posts from followers
This commit is contained in:
parent
a4af27e5b7
commit
87c5ce66cd
7 changed files with 145 additions and 16 deletions
|
|
@ -3,6 +3,8 @@ defmodule Postland.Activities do
|
|||
|
||||
require Logger
|
||||
|
||||
alias Ecto.Multi
|
||||
|
||||
alias Postland.Activity
|
||||
alias Postland.Repo
|
||||
alias Postland.Follows
|
||||
|
|
@ -38,7 +40,37 @@ defmodule Postland.Activities do
|
|||
]
|
||||
}
|
||||
|> Postland.Activity.changeset()
|
||||
|> Repo.insert()
|
||||
|> record_create_activity()
|
||||
|
||||
# TODO: ActivityPub out this activity (only for our own posts obviously)
|
||||
end
|
||||
|
||||
def record_create_activity(changeset) do
|
||||
Multi.new()
|
||||
|> Multi.insert(:activity, changeset)
|
||||
|> record_objects(changeset)
|
||||
|> Repo.transaction()
|
||||
end
|
||||
|
||||
def record_objects(multi, activity_changeset) do
|
||||
activity_changeset
|
||||
|> Ecto.Changeset.get_field(:data)
|
||||
|> Map.get("object")
|
||||
|> List.wrap()
|
||||
|> Enum.group_by(fn object ->
|
||||
{Map.get(object, "id"), Map.get(object, "type")}
|
||||
end)
|
||||
|> Enum.reduce(multi, fn {{id, type}, note_as_list_of_types}, multi ->
|
||||
Multi.insert(
|
||||
multi,
|
||||
"#{id}.#{type}",
|
||||
Postland.Object.changeset(%{
|
||||
"id" => id,
|
||||
"type" => type,
|
||||
"items" => note_as_list_of_types
|
||||
})
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
def process_activity(params) do
|
||||
|
|
@ -57,6 +89,8 @@ defmodule Postland.Activities do
|
|||
|> Repo.insert()
|
||||
end
|
||||
|
||||
# TODO: add effects for CRUD notes
|
||||
|
||||
def cause_effects(
|
||||
%Activity{type: "Accept", data: %{"object" => %{"type" => "Follow", "id" => follow_id}}} =
|
||||
activity
|
||||
|
|
@ -85,6 +119,14 @@ defmodule Postland.Activities do
|
|||
{:ok, activity}
|
||||
end
|
||||
|
||||
def cause_effects(%Activity{type: "Create"} = activity) do
|
||||
changeset = Activity.changeset(activity.data)
|
||||
|
||||
Multi.new()
|
||||
|> record_objects(changeset)
|
||||
|> Repo.transaction()
|
||||
end
|
||||
|
||||
def cause_effects(
|
||||
%Activity{actor_id: actor_id, type: "Follow", data: %{"object" => object}} = activity
|
||||
) do
|
||||
|
|
|
|||
|
|
@ -11,6 +11,20 @@ defmodule Postland.Follows do
|
|||
|
||||
alias Ecto.Multi
|
||||
|
||||
def all_following() do
|
||||
my_actor_id = Postland.my_actor_id()
|
||||
|
||||
from(f in Follow, where: f.follower == ^my_actor_id, where: not is_nil(f.confirmed_at))
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def all_followers() do
|
||||
my_actor_id = Postland.my_actor_id()
|
||||
|
||||
from(f in Follow, where: f.followee == ^my_actor_id, where: not is_nil(f.confirmed_at))
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def record_and_send_acceptance(request) do
|
||||
Multi.new()
|
||||
|> Multi.run(:confirm_timestamp, fn _data, _repo -> confirm_request(request) end)
|
||||
|
|
|
|||
27
lib/postland/object.ex
Normal file
27
lib/postland/object.ex
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
defmodule Postland.Object do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: false}
|
||||
schema "objects" do
|
||||
field :type, :string
|
||||
field :data, :map
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(attrs) do
|
||||
attrs = %{
|
||||
"id" => Map.get(attrs, "id", Ecto.UUID.autogenerate()),
|
||||
"type" => Map.get(attrs, "type"),
|
||||
"data" => attrs
|
||||
}
|
||||
|
||||
%__MODULE__{}
|
||||
|> cast(attrs, [:id, :type, :data])
|
||||
|> validate_required(:id)
|
||||
|> validate_required(:data)
|
||||
|> validate_required(:type)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,17 +1,25 @@
|
|||
defmodule Postland.Timeline do
|
||||
alias Postland.Activity
|
||||
alias Postland.Object
|
||||
alias Postland.Repo
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
def html_content(activity) do
|
||||
case Map.get(activity.data, "object") do
|
||||
get_from_html_item(activity, "content")
|
||||
end
|
||||
|
||||
def attribution(activity) do
|
||||
get_from_html_item(activity, "attributedTo")
|
||||
end
|
||||
|
||||
defp get_from_html_item(activity, field_name) do
|
||||
case Map.get(activity.data, "items") do
|
||||
map when is_map(map) ->
|
||||
Map.get(map, "content")
|
||||
Map.get(map, field_name)
|
||||
|
||||
list when is_list(list) ->
|
||||
Enum.find_value(list, fn map ->
|
||||
Map.get(map, "mediaType") == "text/html" && Map.get(map, "content")
|
||||
Map.get(map, "mediaType", "text/html") == "text/html" && Map.get(map, field_name)
|
||||
end) || ""
|
||||
|
||||
nil ->
|
||||
|
|
@ -19,10 +27,12 @@ defmodule Postland.Timeline do
|
|||
end
|
||||
end
|
||||
|
||||
def attribution(map) do
|
||||
end
|
||||
|
||||
def timeline do
|
||||
from(a in Activity, where: a.type == "Create", order_by: [desc: a.inserted_at])
|
||||
# Only accounts I'm following + myself
|
||||
# Only notes
|
||||
from(a in Object, where: a.type == "Note", order_by: [desc: a.inserted_at])
|
||||
# TODO: Only accounts I'm following + myself
|
||||
|> Repo.all()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -66,13 +66,21 @@ defmodule PostlandWeb.CoreComponents do
|
|||
|
||||
def post_card(assigns) do
|
||||
~H"""
|
||||
<div class="divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<%= {:safe, Postland.Timeline.html_content(@post)} %>
|
||||
<div class="flex items-start space-x-4 w-full">
|
||||
<div class="flex-shrink-0">
|
||||
<img class="inline-block h-10 w-10 rounded-full" alt="" src="/images/avatar.png" />
|
||||
</div>
|
||||
<div class="px-4 py-4 sm:px-6">
|
||||
<!-- Content goes here -->
|
||||
<!-- We use less vertical padding on card footers at all sizes than on headers or body sections -->
|
||||
<div class="divide-y flex-grow divide-gray-200 overflow-hidden rounded-lg bg-white shadow">
|
||||
<div class="px-4 py-4 sm:px-6">
|
||||
<%= Postland.Timeline.attribution(@post) %>
|
||||
</div>
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<%= {:safe, Postland.Timeline.html_content(@post)} %>
|
||||
</div>
|
||||
<div class="px-4 py-4 sm:px-6">
|
||||
<!-- Content goes here -->
|
||||
<!-- We use less vertical padding on card footers at all sizes than on headers or body sections -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -17,9 +17,24 @@ defmodule PostlandWeb.TimelineLive do
|
|||
end
|
||||
|
||||
def handle_event("create_post", %{"post" => post}, socket) do
|
||||
{:ok, post} = Postland.Activities.record_markdown_post(post)
|
||||
{:ok, results} = Postland.Activities.record_markdown_post(post)
|
||||
|
||||
{:noreply, socket |> assign(:post, "") |> stream_insert(:posts, post, at: 0)}
|
||||
new_posts =
|
||||
results
|
||||
|> Enum.filter(fn {_key, value} ->
|
||||
is_struct(value, Postland.Object)
|
||||
end)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:post, "")
|
||||
|> then(fn socket ->
|
||||
Enum.reduce(new_posts, socket, fn {_id, post}, socket ->
|
||||
stream_insert(socket, :posts, post, at: 0)
|
||||
end)
|
||||
end)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("change_post", %{"post" => post}, socket) do
|
||||
|
|
|
|||
13
priv/repo/migrations/20241021231131_add_objects.exs
Normal file
13
priv/repo/migrations/20241021231131_add_objects.exs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
defmodule Postland.Repo.Migrations.AddObjects do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table("objects", primary_key: false) do
|
||||
add :id, :text, primary_key: true
|
||||
add :type, :text, null: false
|
||||
add :data, :map
|
||||
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue