feat: Ingest posts from followers

This commit is contained in:
Ro 2024-10-22 10:50:26 -05:00
parent a4af27e5b7
commit 87c5ce66cd
7 changed files with 145 additions and 16 deletions

View file

@ -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

View file

@ -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
View 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

View file

@ -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

View file

@ -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>
"""

View file

@ -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

View 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