diff --git a/config/config.exs b/config/config.exs index 5ac0ae6a7..ad4e62b76 100644 --- a/config/config.exs +++ b/config/config.exs @@ -28,7 +28,11 @@ config :algora, AlgoraWeb.Endpoint, config :algora, Oban, repo: Algora.Repo, - queues: [event_consumers: 1, comment_consumers: 1] + queues: [ + event_consumers: 1, + comment_consumers: 1, + github_og_image: 5 + ] # Configures the mailer # diff --git a/lib/algora/workspace/jobs/update_repository_og_image.ex b/lib/algora/workspace/jobs/update_repository_og_image.ex new file mode 100644 index 000000000..6df0bf714 --- /dev/null +++ b/lib/algora/workspace/jobs/update_repository_og_image.ex @@ -0,0 +1,50 @@ +defmodule Algora.Workspace.Jobs.UpdateRepositoryOgImage do + @moduledoc false + use Oban.Worker, queue: :github_og_image + + alias Algora.Repo + alias Algora.Workspace.Repository + + require Logger + + @impl Oban.Worker + def perform(%Oban.Job{args: %{"repository_id" => repository_id}}) do + repository = + Repository + |> Repo.get(repository_id) + |> Repo.preload(:user) + + case repository do + nil -> {:error, :not_found} + repository -> update_og_image(repository) + end + end + + defp update_og_image(repository) do + repo_owner = repository.user.provider_login + repo_name = repository.name + object = "repositories/#{repo_owner}/#{repo_name}/og.png" + + req = Finch.build(:get, repository.og_image_url) + + with {:ok, %Finch.Response{body: body}} <- Finch.request(req, Algora.Finch), + {:ok, _} <- Algora.S3.upload(body, object, content_type: "image/png"), + url = Algora.S3.bucket_url() <> "/" <> object, + {:ok, updated_repository} <- update_repository_url(repository, url) do + {:ok, updated_repository} + else + error -> + Logger.error("Failed to fetch/upload image for #{repo_owner}/#{repo_name}: #{inspect(error)}") + error + end + end + + defp update_repository_url(repository, url) do + repository + |> Ecto.Changeset.change(%{ + og_image_url: url, + og_image_updated_at: DateTime.utc_now() + }) + |> Repo.update() + end +end diff --git a/lib/algora/workspace/schemas/repository.ex b/lib/algora/workspace/schemas/repository.ex index c0968e1d9..cbdeaff5f 100644 --- a/lib/algora/workspace/schemas/repository.ex +++ b/lib/algora/workspace/schemas/repository.ex @@ -6,31 +6,45 @@ defmodule Algora.Workspace.Repository do @derive {Inspect, except: [:provider_meta]} typed_schema "repositories" do - field :provider, :string - field :provider_id, :string - field :provider_meta, :map + field :provider, :string, null: false + field :provider_id, :string, null: false + field :provider_meta, :map, null: false - field :name, :string - field :url, :string + field :name, :string, null: false + field :url, :string, null: false + field :description, :string + field :og_image_url, :string, null: false + field :og_image_updated_at, :utc_datetime_usec has_many :tickets, Algora.Workspace.Ticket - belongs_to :user, Algora.Accounts.User + belongs_to :user, Algora.Accounts.User, null: false timestamps() end + defp og_image_base_url, do: "https://opengraph.githubassets.com" + + def has_default_og_image?(%Repository{} = repository), + do: String.starts_with?(repository.og_image_url, og_image_base_url()) + + def default_og_image_url(repo_owner, repo_name), do: "#{og_image_base_url()}/0/#{repo_owner}/#{repo_name}" + def github_changeset(meta, user) do params = %{ provider_id: to_string(meta["id"]), name: meta["name"], + description: meta["description"], + og_image_url: default_og_image_url(meta["owner"]["login"], meta["name"]), + og_image_updated_at: DateTime.utc_now(), url: meta["html_url"], user_id: user.id } %Repository{provider: "github", provider_meta: meta} - |> cast(params, [:provider_id, :name, :url, :user_id]) + |> cast(params, [:provider_id, :name, :url, :description, :og_image_url, :og_image_updated_at, :user_id]) |> generate_id() |> validate_required([:provider_id, :name, :url, :user_id]) + |> foreign_key_constraint(:user_id) |> unique_constraint([:provider, :provider_id]) end end diff --git a/lib/algora/workspace/workspace.ex b/lib/algora/workspace/workspace.ex index 3d044592a..cf41007e5 100644 --- a/lib/algora/workspace/workspace.ex +++ b/lib/algora/workspace/workspace.ex @@ -6,6 +6,7 @@ defmodule Algora.Workspace do alias Algora.Github alias Algora.Repo alias Algora.Workspace.Installation + alias Algora.Workspace.Jobs alias Algora.Workspace.Repository alias Algora.Workspace.Ticket @@ -49,10 +50,34 @@ defmodule Algora.Workspace do where: u.provider_login == ^owner ) - case Repo.one(repository_query) do - %Repository{} = repository -> {:ok, repository} - nil -> create_repository_from_github(token, owner, repo) + res = + case Repo.one(repository_query) do + %Repository{} = repository -> {:ok, repository} + nil -> create_repository_from_github(token, owner, repo) + end + + case res do + {:ok, repository} -> maybe_schedule_og_image_update(repository) + error -> error + end + + res + end + + defp maybe_schedule_og_image_update(%Repository{} = repository) do + one_day_ago = DateTime.add(DateTime.utc_now(), -1, :day) + + needs_update? = + Repository.has_default_og_image?(repository) || + (repository.og_image_updated_at && DateTime.before?(repository.og_image_updated_at, one_day_ago)) + + if needs_update? do + %{repository_id: repository.id} + |> Jobs.UpdateRepositoryOgImage.new() + |> Oban.insert() end + + :ok end def create_repository_from_github(token, owner, repo) do diff --git a/priv/repo/migrations/20250105152749_add_repo_fields.exs b/priv/repo/migrations/20250105152749_add_repo_fields.exs new file mode 100644 index 000000000..ea060c2e1 --- /dev/null +++ b/priv/repo/migrations/20250105152749_add_repo_fields.exs @@ -0,0 +1,23 @@ +defmodule Algora.Repo.Migrations.AddRepoFields do + use Ecto.Migration + + def change do + alter table(:repositories) do + add :description, :text + add :og_image_url, :string + add :og_image_updated_at, :utc_datetime_usec + end + + # Backfill existing repositories with default values + execute """ + UPDATE repositories + SET og_image_url = REPLACE(url, 'https://github.com', 'https://opengraph.githubassets.com/0') + WHERE og_image_url IS NULL + """ + + # Make columns non-nullable after backfill + alter table(:repositories) do + modify :og_image_url, :string, null: false + end + end +end