Skip to content

Commit

Permalink
Add fetching of update notifications from GitHub
Browse files Browse the repository at this point in the history
  • Loading branch information
wmnnd committed Dec 15, 2024
1 parent 879e51e commit 22214ba
Show file tree
Hide file tree
Showing 16 changed files with 285 additions and 2 deletions.
5 changes: 5 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,11 @@ if config_env() == :prod do
# Enable billing
config :keila, Keila.Billing,
enabled: System.get_env("ENABLE_BILLING") in [1, "1", "true", "TRUE"]
# Enable update check
config :keila,
:update_checks_enabled,
System.get_env("DISABLE_UPDATE_CHECKS") not in [nil, "", "0", "false", "FALSE"]


paddle_vendor = System.get_env("PADDLE_VENDOR")

Expand Down
9 changes: 9 additions & 0 deletions lib/keila/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ defmodule Keila.Application do
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Keila.Supervisor]

Supervisor.start_link(children, opts)
|> tap(fn _ ->
maybe_fetch_updates()
end)
end

# Tell Phoenix to update the endpoint configuration
Expand All @@ -53,4 +57,9 @@ defmodule Keila.Application do
Keila.ReleaseTasks.init()
end
end

defp maybe_fetch_updates() do
Keila.Instance.UpdateCronWorker.new(%{})
|> Oban.insert()
end
end
83 changes: 83 additions & 0 deletions lib/keila/instance.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
defmodule Keila.Instance do
alias __MODULE__.Instance
alias __MODULE__.Release

use Keila.Repo
require Logger

@moduledoc """
This module provides easier access to the properties of the current Keila instance.
"""

@doc """
Returns the `Instance` struct that is persisted in the database.
If it doesn't exist, creates a new `Instance` and returns it.
"""
@spec get_instance() :: Instance.t()
def get_instance() do
case Repo.one(Instance) do
nil -> Repo.insert!(%Instance{})
instance -> instance
end
end

@doc """
Returns the version of the current Keila instance as a string.
"""
@spec current_version() :: String.t()
def current_version() do
Application.spec(:keila, :vsn) |> to_string()
end

@doc """
Returns `true` if the update check has been enabled for the current instance.
"""
@spec update_checks_enabled? :: boolean()
def update_checks_enabled? do
Application.get_env(:keila, :update_checks_enabled, true)
end

@doc """
Returns `true` if updates are available.
"""
@spec updates_available? :: boolean()
def updates_available? do
update_checks_enabled?() &&
Repo.exists?(
from i in Instance, where: fragment("jsonb_array_length(?) > 0", i.available_updates)
)
end

@doc """
Fetches updates from the Keila GitHub update releases.
"""
@release_url "https://api.github.com/repos/pentacent/keila/releases"
@spec fetch_updates() :: [__MODULE__.Release.t()]
def fetch_updates() do
with {:ok, response} <- HTTPoison.get(@release_url, recv_timeout: 5_000),
{:ok, release_attrs} when is_list(release_attrs) <- Jason.decode(response.body) do
current_version = current_version() |> Version.parse!()

release_attrs
|> Enum.map(fn %{"tag_name" => version, "published_at" => published_at, "body" => changelog} ->
%{version: version, published_at: published_at, changelog: changelog}
end)
|> Enum.map(&Release.new!/1)
|> Enum.filter(fn %{version: version} ->
version |> Version.parse!() |> Version.compare(current_version) == :gt
end)
else
other ->
Logger.info("Unable to fetch updates. Got: #{inspect(other)}")
[]
end
end

def get_available_updates() do
if update_checks_enabled?() do
Repo.one(from i in Instance, select: i.available_updates)
else
[]
end
end
end
7 changes: 7 additions & 0 deletions lib/keila/instance/schemas/instance.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Keila.Instance.Instance do
use Keila.Schema

schema "instance" do
embeds_many :available_updates, Keila.Instance.Release, on_replace: :delete
end
end
25 changes: 25 additions & 0 deletions lib/keila/instance/schemas/release.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule Keila.Instance.Release do
use Keila.Schema

embedded_schema do
field :version, :string
field :published_at, :utc_datetime
field :changelog, :string
end

def changeset(struct \\ %__MODULE__{}, params) do
struct
|> cast(params, [:version, :published_at, :changelog])
|> validate_required([:version, :published_at, :changelog])
|> validate_change(:version, fn :version, version ->
case Version.parse(version) do
{:ok, _} -> []
_ -> [version: "invalid version format"]
end
end)
end

def new!(params) do
params |> changeset() |> Ecto.Changeset.apply_action!(:insert)
end
end
25 changes: 25 additions & 0 deletions lib/keila/instance/update_cron_worker.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule Keila.Instance.UpdateCronWorker do
use Oban.Worker,
queue: :periodic,
unique: [
period: :infinity,
states: [:available, :scheduled, :executing]
]

use Keila.Repo
alias Keila.Instance

@impl true
def perform(_job) do
if Instance.update_checks_enabled?() do
releases = Instance.fetch_updates()

Instance.get_instance()
|> change()
|> put_embed(:available_updates, releases)
|> Repo.update()
else
:ok
end
end
end
32 changes: 32 additions & 0 deletions lib/keila_web/controllers/instance_admin_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule KeilaWeb.InstanceAdminController do
use KeilaWeb, :controller

alias Keila.Instance

plug :authorize

def show(conn, _) do
update_checks_enabled? = Instance.update_checks_enabled?()
available_updates = Instance.get_available_updates()
current_version = Instance.current_version()

host =
Routes.project_url(conn, :index)
|> String.replace(~r"https?://", "")
|> String.replace("/", "")

conn
|> assign(:update_checks_enabled?, update_checks_enabled?)
|> assign(:available_updates, available_updates)
|> assign(:current_version, current_version)
|> assign(:host, host)
|> render("show.html")
end

defp authorize(conn, _) do
case conn.assigns.is_admin? do
true -> conn
false -> conn |> put_status(404) |> halt()
end
end
end
19 changes: 19 additions & 0 deletions lib/keila_web/helpers/instance_info_plug.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule KeilaWeb.InstanceInfoPlug do
@behaviour Plug
import Plug.Conn

@impl true
def call(conn, _opts) do
if conn.assigns[:is_admin?] do
updates_available? = Keila.Instance.updates_available?()

conn
|> assign(:instance_updates_available?, updates_available?)
else
conn
end
end

@impl true
def init(opts), do: opts
end
3 changes: 3 additions & 0 deletions lib/keila_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule KeilaWeb.Router do
plug KeilaWeb.Meta.Plug
plug KeilaWeb.AuthSession.Plug
plug KeilaWeb.PutLocalePlug
plug KeilaWeb.InstanceInfoPlug
end

# Non-authenticated Routes
Expand Down Expand Up @@ -78,6 +79,8 @@ defmodule KeilaWeb.Router do

resources "/admin/shared-senders", SharedSenderAdminController
get "/admin/shared-senders/:id/delete", SharedSenderAdminController, :delete_confirmation

get "/admin/instance", InstanceAdminController, :show
end

# Authenticated Routes within a Project context
Expand Down
44 changes: 44 additions & 0 deletions lib/keila_web/templates/instance_admin/show.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<div class="container flex py-8 sm:py-11 mb-4">
<div class="flex-grow gap-4 flex flex-col sm:flex-row sm:items-center">
<h1 class="text-2xl sm:text-3xl text-gray-100">
<%= dgettext("admin", "You are currently running Keila %{version} on %{host}.",
version: @current_version,
host: @host
) %>
</h1>
</div>
</div>

<div class="container mb-4">
<%= if @update_checks_enabled? do %>
<%= if Enum.any?(@available_updates) do %>
<div class="flex gap-4 items-center text-3xl max-w-xl">
<div class="flex size-16">
<%= render_icon(:newspaper) %>
</div>
There are updates available for your Keila instance!
</div>
<br /><br />
<%= for release <- @available_updates do %>
<div class="mb-4 max-w-prose">
<p class="p-4 italic bg-gray-700">
<%= local_datetime_tag(release.published_at) %>
</p>
<div class="bg-gray-800 p-4 prose prose-lg prose-invert">
<%= raw(Earmark.as_html!(release.changelog)) %>
</div>
</div>
<% end %>
<% else %>
<div class="flex gap-4 items-center text-3xl max-w-xl">
<div class="flex size-16">
<%= render_icon(:check_circle) %>
</div>
You seem to be running the most recent version of Keila!
</div>
<br />
<% end %>
<% else %>
<p>Update checks are disabled.</p>
<% end %>
</div>
6 changes: 6 additions & 0 deletions lib/keila_web/templates/layout/_menu.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@
icon: :speakerphone
) %>
<% end %>
<%= menu_link(
@conn,
Routes.instance_admin_path(@conn, :show),
dgettext("admin", "System Info"),
icon: :information_circle
) %>
<%= menu_link(@conn, Routes.auth_path(@conn, :logout), gettext("Sign out"), icon: :logout) %>
</nav>
</div>
Expand Down
10 changes: 10 additions & 0 deletions lib/keila_web/templates/layout/app.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,15 @@
<p class="container py-5 my-0"><%= get_flash(@conn, :error) %></p>
</div>
<% end %>
<%= if assigns[:is_admin?] && assigns[:instance_updates_available?] do %>
<div class="bg-amber-200 text-black" role="alert">
<p class="container py-5 my-0">
<a href={Routes.instance_admin_path(@conn, :show)}>
<%= dgettext("admin", "A new version of Keila is available.") %>
<span class="underline"><%= dgettext("admin", "Read more") %></span>
</a>
</p>
</div>
<% end %>
<%= @inner_content %>
</main>
3 changes: 3 additions & 0 deletions lib/keila_web/views/instance_admin_view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule KeilaWeb.InstanceAdminView do
use KeilaWeb, :view
end
2 changes: 1 addition & 1 deletion priv/repo/migrations/20201229080000_auth .exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule PreluApi.Repo.Migrations.Auth do
defmodule Keila.Repo.Migrations.Auth do
use Ecto.Migration

def change do
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule PreluApi.Repo.Migrations.SubscriptionUpserts do
defmodule Keila.Repo.Migrations.SubscriptionUpserts do
use Ecto.Migration

def up do
Expand Down
12 changes: 12 additions & 0 deletions priv/repo/migrations/20241117001430_create_instance.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Keila.Repo.Migrations.CreateInstance do
use Ecto.Migration

def change do
create table("instance") do
add :available_updates, :jsonb, default: "[]"
end

execute "CREATE UNIQUE INDEX instance_singleton ON instance ((true));", ""
execute "INSERT INTO instance DEFAULT VALUES;", ""
end
end

0 comments on commit 22214ba

Please sign in to comment.