feat: mastodon microblog syndication

This commit is contained in:
sloane 2025-04-29 12:57:55 -04:00
parent 96e778ff91
commit 9c01aef829
Signed by: sloanelybutsurely
SSH key fingerprint: SHA256:8SBnwhl+RY3oEyQxy1a9wByPzxWM0x+/Ejc+sIlY5qQ
11 changed files with 153 additions and 15 deletions

View file

@ -61,12 +61,16 @@ config :flop, repo: Core.Repo
config :tesla, adapter: Tesla.Adapter.Mint
mastodon_instance = "https://tech.lgbt"
config :sloanely_but_surely, Core.Syndication, mastodon_instance: mastodon_instance
config :ueberauth, Ueberauth,
providers: [
mastodon:
{Ueberauth.Strategy.Mastodon,
[
instance: "https://tech.lgbt",
instance: mastodon_instance,
client_id: {System, :get_env, ["MASTODON_CLIENT_ID"]},
client_secret: {System, :get_env, ["MASTODON_CLIENT_SECRET"]},
scope: "read write push"

View file

@ -85,9 +85,14 @@ defmodule Core.Posts do
end
def publish_post(%Schema.Post{} = post, published_at \\ DateTime.utc_now()) do
post
|> Post.publish_changeset(published_at)
|> Core.Repo.update()
with {:ok, post} <-
post
|> Post.publish_changeset(published_at)
|> Core.Repo.update() do
Core.Syndication.syndicate_to_mastodon(post)
{:ok, post}
end
end
def delete_post(%Schema.Post{} = post, deleted_at \\ DateTime.utc_now()) do

View file

@ -6,7 +6,16 @@ defmodule Core.Posts.Post do
def content_changeset(%Schema.Post{} = post, attrs) do
changeset =
post
|> cast(attrs, [:tid, :kind, :slug, :title, :body, :deleted_at, :published_at])
|> cast(attrs, [
:tid,
:kind,
:slug,
:title,
:body,
:deleted_at,
:published_at,
:syndicate_to_mastodon
])
|> validate_required([:kind], message: "must have a kind")
|> validate_required([:body], message: "must have a body")
@ -77,7 +86,11 @@ defmodule Core.Posts.Post do
import Ecto.Query
def base do
from _ in Schema.Post, as: :posts
from p in Schema.Post,
as: :posts,
join: mp in assoc(p, :mastodon_post),
as: :mastodon_posts,
preload: [mastodon_post: mp]
end
def current(query \\ base()) do

View file

@ -1,6 +1,11 @@
defmodule Core.Syndication do
alias __MODULE__
@mastodon_instance Application.compile_env!(:sloanely_but_surely, [
Core.Syndication,
:mastodon_instance
])
def get_mastodon_account(user) do
Core.Repo.get_by(Schema.MastodonAccount, user_id: user.id)
end
@ -11,4 +16,38 @@ defmodule Core.Syndication do
|> Syndication.MastodonAccount.changeset(attrs)
|> Core.Repo.insert()
end
def syndicate_to_mastodon(%Schema.Post{} = post) do
post = Core.Repo.preload(post, [:mastodon_post])
if post.syndicate_to_mastodon and is_nil(post.mastodon_post) do
conn = build_mastodon_client_conn()
{:ok, resp} = MastodonClient.post(conn, "/api/v1/statuses", %{status: post.body})
post
|> Ecto.build_assoc(:mastodon_post)
|> Syndication.MastodonPost.changeset(%{
status_id: resp.body["id"],
url: resp.body["url"]
})
|> Core.Repo.insert()
post
else
post
end
end
defp get_mastodon_access_token do
mastodon_account = Core.Repo.one!(Schema.MastodonAccount)
mastodon_account.access_token
end
defp build_mastodon_client_conn do
%MastodonClient.Conn{
instance: @mastodon_instance,
access_token: get_mastodon_access_token()
}
end
end

View file

@ -0,0 +1,10 @@
defmodule Core.Syndication.MastodonPost do
import Ecto.Changeset
def changeset(mastodon_post, attrs) do
mastodon_post
|> cast(attrs, [:status_id, :url])
|> validate_required([:status_id, :url])
|> unique_constraint([:post_id])
end
end

View file

@ -0,0 +1,11 @@
defmodule Schema.MastodonPost do
use Schema
schema "mastodon_posts" do
field :status_id, :string
field :url, :string
belongs_to :post, Schema.Post
timestamps()
end
end

View file

@ -28,6 +28,10 @@ defmodule Schema.Post do
field :published_at, :utc_datetime_usec
field :deleted_at, :utc_datetime_usec
field :syndicate_to_mastodon, :boolean
has_one :mastodon_post, Schema.MastodonPost
timestamps()
end

View file

@ -11,7 +11,7 @@ defmodule Web.CoreComponents do
attr :label, :string, default: nil
attr :value, :any
attr :class, :string, default: nil
attr :type, :string, default: "text", values: ~w[text password textarea datetime-local]
attr :type, :string, default: "text", values: ~w[text password textarea datetime-local checkbox]
attr :field, FormField
attr :errors, :list, default: []
attr :rest, :global, include: ~w[disabled form pattern placeholder readonly required]
@ -35,6 +35,23 @@ defmodule Web.CoreComponents do
|> input()
end
def input(%{type: "checkbox"} = assigns) do
~H"""
<div class={["flex flex-row items-center gap-x-2", @class]}>
<.label for={@id}>{@label}</.label>
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
<input
id={@id}
type={@type}
name={@name}
value="true"
checked={Phoenix.HTML.Form.normalize_value(@type, @value)}
{@rest}
/>
</div>
"""
end
def input(%{type: "textarea"} = assigns) do
~H"""
<div class={["flex flex-col", @class]}>

View file

@ -44,14 +44,23 @@
<.markdown content={@status.body} />
</div>
<footer class="mt-4 border-t border-gray-200 pt-2 text-sm text-gray-500">
<%= if @status.published_at do %>
<.timex
value={Core.Posts.publish_date_time(@status)}
format="{Mfull} {D}, {YYYY} at {h12}:{m} {AM}"
class="dt-published"
/>
<% end %>
<footer class="mt-4 border-t border-gray-200 pt-2 text-sm text-gray-500 flex flex-row justify-between">
<div>
<%= if @status.published_at do %>
<.timex
value={Core.Posts.publish_date_time(@status)}
format="{Mfull} {D}, {YYYY} at {h12}:{m} {AM}"
class="dt-published"
/>
<% end %>
</div>
<div class="flex flex-row gap-x-2">
<%= if @status.mastodon_post do %>
<.link href={@status.mastodon_post.url} target="_blank" class="u-syndication">
tech.lgbt
</.link>
<% end %>
</div>
</footer>
</article>
</div>

View file

@ -25,6 +25,12 @@
<.button phx-click="publish" class="self-end">publish</.button>
<% end %>
<.input type="datetime-local" label="deleted (utc)" field={@form[:deleted_at]} />
<.input
type="checkbox"
label="syndicate to mastodon"
class="self-end"
field={@form[:syndicate_to_mastodon]}
/>
<%= if @post.deleted_at do %>
<.button phx-click="undelete" class="self-end">undelete</.button>
<% else %>

View file

@ -0,0 +1,20 @@
defmodule Core.Repo.Migrations.AddMastodonPostsTable do
use Ecto.Migration
def change do
create table(:mastodon_posts, primary_key: false) do
add :id, :uuid, primary_key: true
add :post_id, references(:posts, type: :uuid, on_delete: :delete_all), null: false
add :status_id, :text, null: false
add :url, :text, null: false
timestamps(type: :utc_datetime_usec)
end
create unique_index(:mastodon_posts, [:post_id])
alter table(:posts) do
add :syndicate_to_mastodon, :boolean, default: false, null: false
end
end
end