add support for bluesky syndication
This commit is contained in:
parent
36a1cdfab4
commit
8e3b231465
19 changed files with 342 additions and 17 deletions
.formatter.exs
lib
mix.exsmix.lockpriv/repo/migrations
|
@ -1,5 +1,5 @@
|
||||||
[
|
[
|
||||||
import_deps: [:oban, :ecto, :ecto_sql, :phoenix],
|
import_deps: [:oban, :ecto, :ecto_sql, :phoenix, :tesla],
|
||||||
subdirectories: ["priv/*/migrations"],
|
subdirectories: ["priv/*/migrations"],
|
||||||
plugins: [Phoenix.LiveView.HTMLFormatter],
|
plugins: [Phoenix.LiveView.HTMLFormatter],
|
||||||
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
|
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
|
||||||
|
|
|
@ -88,9 +88,9 @@ defmodule Core.Posts do
|
||||||
with {:ok, post} <-
|
with {:ok, post} <-
|
||||||
post
|
post
|
||||||
|> Post.publish_changeset(published_at)
|
|> Post.publish_changeset(published_at)
|
||||||
|> Core.Repo.update() do
|
|> Core.Repo.update(),
|
||||||
Core.Syndication.syndicate_to_mastodon(post)
|
{:ok, post} <- Core.Syndication.syndicate_to_mastodon(post),
|
||||||
|
{:ok, post} <- Core.Syndication.syndicate_to_bluesky(post) do
|
||||||
{:ok, post}
|
{:ok, post}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,8 @@ defmodule Core.Posts.Post do
|
||||||
:body,
|
:body,
|
||||||
:deleted_at,
|
:deleted_at,
|
||||||
:published_at,
|
:published_at,
|
||||||
:syndicate_to_mastodon
|
:syndicate_to_mastodon,
|
||||||
|
:syndicate_to_bluesky
|
||||||
])
|
])
|
||||||
|> validate_required([:kind], message: "must have a kind")
|
|> validate_required([:kind], message: "must have a kind")
|
||||||
|> validate_required([:body], message: "must have a body")
|
|> validate_required([:body], message: "must have a body")
|
||||||
|
@ -90,7 +91,9 @@ defmodule Core.Posts.Post do
|
||||||
as: :posts,
|
as: :posts,
|
||||||
left_join: mp in assoc(p, :mastodon_post),
|
left_join: mp in assoc(p, :mastodon_post),
|
||||||
as: :mastodon_posts,
|
as: :mastodon_posts,
|
||||||
preload: [mastodon_post: mp]
|
left_join: bp in assoc(p, :bluesky_post),
|
||||||
|
as: :bluesky_posts,
|
||||||
|
preload: [mastodon_post: mp, bluesky_post: bp]
|
||||||
end
|
end
|
||||||
|
|
||||||
def current(query \\ base()) do
|
def current(query \\ base()) do
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
defmodule Core.Syndication do
|
defmodule Core.Syndication do
|
||||||
alias __MODULE__
|
alias __MODULE__
|
||||||
|
|
||||||
|
## mastodon
|
||||||
|
|
||||||
@mastodon_instance Application.compile_env!(:sloanely_but_surely, [
|
@mastodon_instance Application.compile_env!(:sloanely_but_surely, [
|
||||||
Core.Syndication,
|
Core.Syndication,
|
||||||
:mastodon_instance
|
:mastodon_instance
|
||||||
|
@ -33,13 +35,13 @@ defmodule Core.Syndication do
|
||||||
})
|
})
|
||||||
|> Core.Repo.insert()
|
|> Core.Repo.insert()
|
||||||
|
|
||||||
post
|
{:ok, post}
|
||||||
else
|
else
|
||||||
post
|
{:ok, post}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_mastodon_access_token do
|
defp get_mastodon_access_token! do
|
||||||
mastodon_account = Core.Repo.one!(Schema.MastodonAccount)
|
mastodon_account = Core.Repo.one!(Schema.MastodonAccount)
|
||||||
mastodon_account.access_token
|
mastodon_account.access_token
|
||||||
end
|
end
|
||||||
|
@ -47,7 +49,93 @@ defmodule Core.Syndication do
|
||||||
defp build_mastodon_client_conn do
|
defp build_mastodon_client_conn do
|
||||||
%MastodonClient.Conn{
|
%MastodonClient.Conn{
|
||||||
instance: @mastodon_instance,
|
instance: @mastodon_instance,
|
||||||
access_token: get_mastodon_access_token()
|
access_token: get_mastodon_access_token!()
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
## bluesky
|
||||||
|
|
||||||
|
def get_bluesky_account(%Schema.User{} = user) do
|
||||||
|
Core.Repo.get_by(Schema.BlueskyAccount, user_id: user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_bluesky_account(user, identifier, password) do
|
||||||
|
with {:ok, session_resp} <-
|
||||||
|
Syndication.BlueskyClient.create_session(identifier, password),
|
||||||
|
{:ok, attrs} <- parse_bluesky_token_resp(session_resp) do
|
||||||
|
user
|
||||||
|
|> Ecto.build_assoc(:bluesky_account)
|
||||||
|
|> Syndication.BlueskyAccount.create_changeset(attrs)
|
||||||
|
|> Core.Repo.insert(
|
||||||
|
conflict_target: [:user_id],
|
||||||
|
on_conflict: {:replace_all_except, [:id, :inserted_at]}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_bluesky_account(%Schema.BlueskyAccount{} = bluesky_account) do
|
||||||
|
with {:ok, refresh_resp} <-
|
||||||
|
Syndication.BlueskyClient.refresh_session(bluesky_account),
|
||||||
|
{:ok, attrs} <- parse_bluesky_token_resp(refresh_resp) do
|
||||||
|
bluesky_account
|
||||||
|
|> Syndication.BlueskyAccount.refresh_changeset(attrs)
|
||||||
|
|> Core.Repo.update()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def syndicate_to_bluesky(%Schema.Post{} = post) do
|
||||||
|
post = Core.Repo.preload(post, [:bluesky_post])
|
||||||
|
|
||||||
|
if post.syndicate_to_bluesky && is_nil(post.bluesky_post) do
|
||||||
|
bluesky_account = get_bluesky_account!()
|
||||||
|
|
||||||
|
with {:ok, resp} <- Syndication.BlueskyClient.post_status(bluesky_account, post.body) do
|
||||||
|
post
|
||||||
|
|> Ecto.build_assoc(:bluesky_post)
|
||||||
|
|> Syndication.BlueskyPost.changeset(resp.body)
|
||||||
|
|> Core.Repo.insert()
|
||||||
|
|
||||||
|
{:ok, post}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{:ok, post}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def bsky_app_url(%Schema.BlueskyPost{uri: uri}) do
|
||||||
|
[did, id] = Regex.run(~r|at://(did:.*)/app.bsky.feed.post/(.*)|, uri, capture: :all_but_first)
|
||||||
|
|
||||||
|
"https://bsky.app/profile/#{did}/post/#{id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_bluesky_token_resp(resp) do
|
||||||
|
with {:ok, %{iat: access_jwt_iat, exp: access_jwt_exp}} <-
|
||||||
|
parse_bluesky_jwt(resp.body["accessJwt"]),
|
||||||
|
{:ok, %{iat: refresh_jwt_iat, exp: refresh_jwt_exp}} <-
|
||||||
|
parse_bluesky_jwt(resp.body["refreshJwt"]) do
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
handle: resp.body["handle"],
|
||||||
|
did: resp.body["did"],
|
||||||
|
access_jwt: resp.body["accessJwt"],
|
||||||
|
access_jwt_iat: access_jwt_iat,
|
||||||
|
access_jwt_exp: access_jwt_exp,
|
||||||
|
refresh_jwt: resp.body["refreshJwt"],
|
||||||
|
refresh_jwt_iat: refresh_jwt_iat,
|
||||||
|
refresh_jwt_exp: refresh_jwt_exp
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_bluesky_jwt(jwt) do
|
||||||
|
with {:ok, claims} <- Joken.peek_claims(jwt),
|
||||||
|
{:ok, iat} <- DateTime.from_unix(claims["iat"]),
|
||||||
|
{:ok, exp} <- DateTime.from_unix(claims["exp"]) do
|
||||||
|
{:ok, %{iat: iat, exp: exp}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_bluesky_account! do
|
||||||
|
Core.Repo.one!(Schema.BlueskyAccount)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
47
lib/core/syndication/bluesky_account.ex
Normal file
47
lib/core/syndication/bluesky_account.ex
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
defmodule Core.Syndication.BlueskyAccount do
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
def create_changeset(%Schema.BlueskyAccount{} = bluesky_account, attrs) do
|
||||||
|
bluesky_account
|
||||||
|
|> cast(attrs, [
|
||||||
|
:handle,
|
||||||
|
:did,
|
||||||
|
:access_jwt,
|
||||||
|
:access_jwt_iat,
|
||||||
|
:access_jwt_exp,
|
||||||
|
:refresh_jwt,
|
||||||
|
:refresh_jwt_iat,
|
||||||
|
:refresh_jwt_exp
|
||||||
|
])
|
||||||
|
|> validate_required([
|
||||||
|
:handle,
|
||||||
|
:did,
|
||||||
|
:access_jwt,
|
||||||
|
:access_jwt_iat,
|
||||||
|
:access_jwt_exp,
|
||||||
|
:refresh_jwt,
|
||||||
|
:refresh_jwt_iat,
|
||||||
|
:refresh_jwt_exp
|
||||||
|
])
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_changeset(%Schema.BlueskyAccount{} = bluesky_account, attrs) do
|
||||||
|
bluesky_account
|
||||||
|
|> cast(attrs, [
|
||||||
|
:access_jwt,
|
||||||
|
:access_jwt_iat,
|
||||||
|
:access_jwt_exp,
|
||||||
|
:refresh_jwt,
|
||||||
|
:refresh_jwt_iat,
|
||||||
|
:refresh_jwt_exp
|
||||||
|
])
|
||||||
|
|> validate_required([
|
||||||
|
:access_jwt,
|
||||||
|
:access_jwt_iat,
|
||||||
|
:access_jwt_exp,
|
||||||
|
:refresh_jwt,
|
||||||
|
:refresh_jwt_iat,
|
||||||
|
:refresh_jwt_exp
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
50
lib/core/syndication/bluesky_client.ex
Normal file
50
lib/core/syndication/bluesky_client.ex
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
defmodule Core.Syndication.BlueskyClient do
|
||||||
|
@middleware [
|
||||||
|
Tesla.Middleware.Logger,
|
||||||
|
{Tesla.Middleware.BaseUrl, "https://bsky.social/xrpc"},
|
||||||
|
Tesla.Middleware.JSON
|
||||||
|
]
|
||||||
|
|
||||||
|
def new(opts \\ []) do
|
||||||
|
middleware = @middleware
|
||||||
|
|
||||||
|
middleware =
|
||||||
|
case Keyword.fetch(opts, :token) do
|
||||||
|
{:ok, token} ->
|
||||||
|
middleware ++ [{Tesla.Middleware.BearerAuth, token: token}]
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
middleware
|
||||||
|
end
|
||||||
|
|
||||||
|
Tesla.client(middleware)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_session(identifier, password) do
|
||||||
|
client = new()
|
||||||
|
|
||||||
|
Tesla.post(client, "/com.atproto.server.createSession", %{
|
||||||
|
identifier: identifier,
|
||||||
|
password: password
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_session(%Schema.BlueskyAccount{refresh_jwt: token}) do
|
||||||
|
client = new(token: token)
|
||||||
|
|
||||||
|
Tesla.post(client, "/com.atproto.server.refreshSession", nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_status(%Schema.BlueskyAccount{} = bluesky_account, text) do
|
||||||
|
client = new(token: bluesky_account.access_jwt)
|
||||||
|
|
||||||
|
Tesla.post(client, "/com.atproto.repo.createRecord", %{
|
||||||
|
repo: bluesky_account.did,
|
||||||
|
collection: "app.bsky.feed.post",
|
||||||
|
record: %{
|
||||||
|
text: text,
|
||||||
|
createdAt: DateTime.utc_now() |> DateTime.to_iso8601()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
9
lib/core/syndication/bluesky_post.ex
Normal file
9
lib/core/syndication/bluesky_post.ex
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Core.Syndication.BlueskyPost do
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
def changeset(%Schema.BlueskyPost{} = bluesky_post, attrs) do
|
||||||
|
bluesky_post
|
||||||
|
|> cast(attrs, [:cid, :uri, :commit])
|
||||||
|
|> validate_required([:cid, :uri, :commit])
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,8 @@
|
||||||
defmodule Schema do
|
defmodule Schema do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Boundary, deps: [], exports: [Post, User, UserToken, MastodonAccount]
|
use Boundary,
|
||||||
|
deps: [],
|
||||||
|
exports: [Post, User, UserToken, MastodonAccount, BlueskyAccount, BlueskyPost]
|
||||||
|
|
||||||
defmacro __using__(_) do
|
defmacro __using__(_) do
|
||||||
quote do
|
quote do
|
||||||
|
|
20
lib/schema/bluesky_account.ex
Normal file
20
lib/schema/bluesky_account.ex
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
defmodule Schema.BlueskyAccount do
|
||||||
|
use Schema
|
||||||
|
|
||||||
|
schema "bluesky_accounts" do
|
||||||
|
field :handle, :string
|
||||||
|
field :did, :string
|
||||||
|
|
||||||
|
field :access_jwt, :string, redact: true
|
||||||
|
field :access_jwt_iat, :utc_datetime
|
||||||
|
field :access_jwt_exp, :utc_datetime
|
||||||
|
|
||||||
|
field :refresh_jwt, :string, redact: true
|
||||||
|
field :refresh_jwt_iat, :utc_datetime
|
||||||
|
field :refresh_jwt_exp, :utc_datetime
|
||||||
|
|
||||||
|
belongs_to :user, Schema.User
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
end
|
13
lib/schema/bluesky_post.ex
Normal file
13
lib/schema/bluesky_post.ex
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
defmodule Schema.BlueskyPost do
|
||||||
|
use Schema
|
||||||
|
|
||||||
|
schema "bluesky_posts" do
|
||||||
|
field :cid, :string
|
||||||
|
field :uri, :string
|
||||||
|
field :commit, :map
|
||||||
|
|
||||||
|
belongs_to :post, Schema.Post
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
end
|
|
@ -29,8 +29,10 @@ defmodule Schema.Post do
|
||||||
field :deleted_at, :utc_datetime_usec
|
field :deleted_at, :utc_datetime_usec
|
||||||
|
|
||||||
field :syndicate_to_mastodon, :boolean
|
field :syndicate_to_mastodon, :boolean
|
||||||
|
field :syndicate_to_bluesky, :boolean
|
||||||
|
|
||||||
has_one :mastodon_post, Schema.MastodonPost
|
has_one :mastodon_post, Schema.MastodonPost
|
||||||
|
has_one :bluesky_post, Schema.BlueskyPost
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,6 +9,7 @@ defmodule Schema.User do
|
||||||
field :current_password, :string, virtual: true, redact: true
|
field :current_password, :string, virtual: true, redact: true
|
||||||
|
|
||||||
has_one :mastodon_account, Schema.MastodonAccount
|
has_one :mastodon_account, Schema.MastodonAccount
|
||||||
|
has_one :bluesky_account, Schema.BlueskyAccount
|
||||||
|
|
||||||
timestamps(type: :utc_datetime_usec)
|
timestamps(type: :utc_datetime_usec)
|
||||||
end
|
end
|
||||||
|
|
|
@ -55,11 +55,22 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-x-2">
|
<div class="flex flex-row gap-x-2">
|
||||||
<%= if @status.mastodon_post do %>
|
<.link
|
||||||
<.link href={@status.mastodon_post.url} target="_blank" class="u-syndication">
|
:if={@status.mastodon_post}
|
||||||
tech.lgbt
|
href={@status.mastodon_post.url}
|
||||||
</.link>
|
target="_blank"
|
||||||
<% end %>
|
class="u-syndication"
|
||||||
|
>
|
||||||
|
tech.lgbt
|
||||||
|
</.link>
|
||||||
|
<.link
|
||||||
|
:if={@status.bluesky_post}
|
||||||
|
href={Core.Syndication.bsky_app_url(@status.bluesky_post)}
|
||||||
|
target="_blank"
|
||||||
|
class="u-syndication"
|
||||||
|
>
|
||||||
|
bsky.app
|
||||||
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -31,6 +31,12 @@
|
||||||
class="self-end"
|
class="self-end"
|
||||||
field={@form[:syndicate_to_mastodon]}
|
field={@form[:syndicate_to_mastodon]}
|
||||||
/>
|
/>
|
||||||
|
<.input
|
||||||
|
type="checkbox"
|
||||||
|
label="syndicate to bluesky"
|
||||||
|
class="self-end"
|
||||||
|
field={@form[:syndicate_to_bluesky]}
|
||||||
|
/>
|
||||||
<%= if @post.deleted_at do %>
|
<%= if @post.deleted_at do %>
|
||||||
<.button phx-click="undelete" class="self-end">undelete</.button>
|
<.button phx-click="undelete" class="self-end">undelete</.button>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|
|
@ -3,11 +3,26 @@ defmodule Web.AdminSyndicationLive do
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
mastodon_account = Core.Syndication.get_mastodon_account(socket.assigns.current_user)
|
mastodon_account = Core.Syndication.get_mastodon_account(socket.assigns.current_user)
|
||||||
|
bluesky_account = Core.Syndication.get_bluesky_account(socket.assigns.current_user)
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(:mastodon_account, mastodon_account)
|
|> assign(:mastodon_account, mastodon_account)
|
||||||
|
|> assign(:bluesky_account, bluesky_account)
|
||||||
|
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event(
|
||||||
|
"save_bluesky_account",
|
||||||
|
%{"bluesky_account" => %{"identifier" => identifier, "password" => password}},
|
||||||
|
socket
|
||||||
|
) do
|
||||||
|
{:ok, bluesky_account} =
|
||||||
|
Core.Syndication.save_bluesky_account(socket.assigns.current_user, identifier, password)
|
||||||
|
|
||||||
|
socket = assign(socket, bluesky_account: bluesky_account)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,7 +31,18 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Bluesky: </strong>Coming soon!
|
<strong>Bluesky: </strong>
|
||||||
|
<%= if @bluesky_account do %>
|
||||||
|
<.link href={"https://bsky.app/profile/#{@bluesky_account.handle}"} target="_blank">
|
||||||
|
@{@bluesky_account.handle}
|
||||||
|
</.link>
|
||||||
|
<% else %>
|
||||||
|
<.form :let={f} for={%{}} as={:bluesky_account} phx-submit="save_bluesky_account">
|
||||||
|
<.input field={f[:identifier]} label="email" />
|
||||||
|
<.input field={f[:password]} type="password" />
|
||||||
|
<.button type="submit">Connect account</.button>
|
||||||
|
</.form>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
1
mix.exs
1
mix.exs
|
@ -70,6 +70,7 @@ defmodule SlaonelyButSurely.MixProject do
|
||||||
{:ueberauth_mastodon, "~> 0.3.0"},
|
{:ueberauth_mastodon, "~> 0.3.0"},
|
||||||
{:tesla, "~> 1.14"},
|
{:tesla, "~> 1.14"},
|
||||||
{:mint, "~> 1.7"},
|
{:mint, "~> 1.7"},
|
||||||
|
{:joken, "~> 2.6"},
|
||||||
|
|
||||||
# Added dev and/or test dependencies
|
# Added dev and/or test dependencies
|
||||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||||
|
|
2
mix.lock
2
mix.lock
|
@ -31,6 +31,8 @@
|
||||||
"igniter": {:hex, :igniter, "0.5.47", "7a1041d5e38303e526fa6b6de37c9e78013f5cb573833ed51183d18e3a152f10", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "53a900909e20f217a25d15a34fef629c562b4822c1fb39cfa5d6999bc72992ed"},
|
"igniter": {:hex, :igniter, "0.5.47", "7a1041d5e38303e526fa6b6de37c9e78013f5cb573833ed51183d18e3a152f10", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "53a900909e20f217a25d15a34fef629c562b4822c1fb39cfa5d6999bc72992ed"},
|
||||||
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
|
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
|
||||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||||
|
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
|
||||||
|
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
|
||||||
"mastodon_client": {:hex, :mastodon_client, "0.1.0", "7f1a9e54367d0e126c76d0bb1097de346fb9c6da6d682a6b57bc936c92d643eb", [:mix], [{:hackney, "~> 1.18", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:tesla, "~> 1.4", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "3daa37fc7a95430eb0b77cdb168af58b8db84efea6e0f3244d15d45f39a87160"},
|
"mastodon_client": {:hex, :mastodon_client, "0.1.0", "7f1a9e54367d0e126c76d0bb1097de346fb9c6da6d682a6b57bc936c92d643eb", [:mix], [{:hackney, "~> 1.18", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:tesla, "~> 1.4", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "3daa37fc7a95430eb0b77cdb168af58b8db84efea6e0f3244d15d45f39a87160"},
|
||||||
"mdex": {:hex, :mdex, "0.5.0", "252c83cebc6a089801dfc1e142b4d98c9c358378ec7096a94796bce8bd13b0fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: false]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "73e3ddee03130267e3be6aaf47a7f423c6f86add4bb5c62b352465cd9fb87d95"},
|
"mdex": {:hex, :mdex, "0.5.0", "252c83cebc6a089801dfc1e142b4d98c9c358378ec7096a94796bce8bd13b0fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: false]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "73e3ddee03130267e3be6aaf47a7f423c6f86add4bb5c62b352465cd9fb87d95"},
|
||||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||||
|
|
44
priv/repo/migrations/20250502113428_add_bluesky_tables.exs
Normal file
44
priv/repo/migrations/20250502113428_add_bluesky_tables.exs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
defmodule Core.Repo.Migrations.AddBlueskyTables do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:bluesky_accounts, primary_key: false) do
|
||||||
|
add :id, :uuid, primary_key: true
|
||||||
|
|
||||||
|
add :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false
|
||||||
|
|
||||||
|
add :handle, :text, null: false
|
||||||
|
add :did, :text, null: false
|
||||||
|
|
||||||
|
add :access_jwt, :text, null: false
|
||||||
|
add :access_jwt_iat, :utc_datetime, null: false
|
||||||
|
add :access_jwt_exp, :utc_datetime, null: false
|
||||||
|
|
||||||
|
add :refresh_jwt, :text, null: false
|
||||||
|
add :refresh_jwt_iat, :utc_datetime, null: false
|
||||||
|
add :refresh_jwt_exp, :utc_datetime, null: false
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime_usec)
|
||||||
|
end
|
||||||
|
|
||||||
|
create unique_index(:bluesky_accounts, [:user_id])
|
||||||
|
|
||||||
|
create table(:bluesky_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 :cid, :text, null: false
|
||||||
|
add :uri, :text, null: false
|
||||||
|
add :commit, :jsonb, null: false
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime_usec)
|
||||||
|
end
|
||||||
|
|
||||||
|
create unique_index(:bluesky_posts, [:post_id])
|
||||||
|
|
||||||
|
alter table(:posts) do
|
||||||
|
add :syndicate_to_bluesky, :boolean, default: false, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue