diff --git a/config/config.exs b/config/config.exs index d8d5fa0..41e0957 100644 --- a/config/config.exs +++ b/config/config.exs @@ -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" diff --git a/lib/core/posts.ex b/lib/core/posts.ex index cd5e84a..ba4754c 100644 --- a/lib/core/posts.ex +++ b/lib/core/posts.ex @@ -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 diff --git a/lib/core/posts/post.ex b/lib/core/posts/post.ex index 45cb7e1..cab8544 100644 --- a/lib/core/posts/post.ex +++ b/lib/core/posts/post.ex @@ -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 diff --git a/lib/core/syndication.ex b/lib/core/syndication.ex index fcf06d4..02bc49d 100644 --- a/lib/core/syndication.ex +++ b/lib/core/syndication.ex @@ -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 diff --git a/lib/core/syndication/mastodon_post.ex b/lib/core/syndication/mastodon_post.ex new file mode 100644 index 0000000..5ee71b3 --- /dev/null +++ b/lib/core/syndication/mastodon_post.ex @@ -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 diff --git a/lib/schema/mastodon_post.ex b/lib/schema/mastodon_post.ex new file mode 100644 index 0000000..12acc3d --- /dev/null +++ b/lib/schema/mastodon_post.ex @@ -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 diff --git a/lib/schema/post.ex b/lib/schema/post.ex index 94ed07f..e913fb4 100644 --- a/lib/schema/post.ex +++ b/lib/schema/post.ex @@ -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 diff --git a/lib/web/components/core_components.ex b/lib/web/components/core_components.ex index 43cf78e..437777e 100644 --- a/lib/web/components/core_components.ex +++ b/lib/web/components/core_components.ex @@ -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]}> diff --git a/lib/web/controllers/status_html/show.html.heex b/lib/web/controllers/status_html/show.html.heex index bcd4982..bb4743d 100644 --- a/lib/web/controllers/status_html/show.html.heex +++ b/lib/web/controllers/status_html/show.html.heex @@ -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> diff --git a/lib/web/live/admin_post_live.html.heex b/lib/web/live/admin_post_live.html.heex index b8144bc..92408bf 100644 --- a/lib/web/live/admin_post_live.html.heex +++ b/lib/web/live/admin_post_live.html.heex @@ -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 %> diff --git a/priv/repo/migrations/20250429165843_add_mastodon_posts_table.exs b/priv/repo/migrations/20250429165843_add_mastodon_posts_table.exs new file mode 100644 index 0000000..c0df6d3 --- /dev/null +++ b/priv/repo/migrations/20250429165843_add_mastodon_posts_table.exs @@ -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