4.9 KiB · text History 4b6fe3b
defmodule OhMyExOhIrcd.Release do
  @moduledoc """
  Used for executing release tasks when run in production without Mix
  installed.

  The `mix invites.*` / `mix opers.*` tasks are dev-only (Mix is not
  shipped in the release). These functions are their prod equivalents —
  run them with `eval` so the Repo is started on demand and output lands
  in your terminal:

      bin/oh_my_ex_oh_ircd eval 'OhMyExOhIrcd.Release.create_invite(note: "for alice")'

  Prefer `eval` over `rpc` here: `rpc` runs on the live node, so any
  printed output goes to the *server's* console, not yours.
  """
  @app :oh_my_ex_oh_ircd

  def migrate do
    load_app()

    for repo <- repos() do
      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
    end
  end

  def rollback(repo, version) do
    load_app()
    {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
  end

  ## Registration invites — prod equivalents of `mix invites.*`.

  @doc """
  Mint a single-use registration invite and print the token + signup URL.

      bin/oh_my_ex_oh_ircd eval \
        'OhMyExOhIrcd.Release.create_invite(note: "for alice", expires_in_days: 7)'

  Accepts `:note` and `:expires_in_days` (mirrors `mix invites.create`).
  """
  def create_invite(opts \\ []) do
    opts =
      case Keyword.pop(opts, :expires_in_days) do
        {nil, rest} -> rest
        {days, rest} -> Keyword.put(rest, :expires_in_seconds, days * 24 * 3600)
      end

    with_started_repo(fn ->
      case OhMyExOhIrcd.Accounts.create_invite(opts) do
        {:ok, invite} ->
          IO.puts("Token:  #{invite.token}")
          IO.puts("URL:    #{base_url()}/users/register?invite=#{invite.token}")
          if invite.expires_at, do: IO.puts("Expires: #{invite.expires_at}")
          :ok

        {:error, cs} ->
          IO.puts("Couldn't create invite: #{inspect(cs.errors)}")
          {:error, cs}
      end
    end)
  end

  @doc "List active (unconsumed) invites. Prod equivalent of `mix invites.list`."
  def list_invites do
    with_started_repo(fn ->
      case OhMyExOhIrcd.Accounts.list_active_invites() do
        [] ->
          IO.puts("(no active invites)")

        invites ->
          Enum.each(invites, fn i ->
            IO.puts(
              "#{String.slice(i.token, 0, 8)}…  #{i.note || ""}  expires=#{i.expires_at || "never"}"
            )
          end)
      end
    end)
  end

  ## IRC operators — prod equivalents of `mix opers.*`.

  @doc "Flag an account (by email) as an IRC operator. See `mix opers.grant`."
  def grant_oper(email), do: set_oper(email, true)

  @doc "Clear an account's IRC-operator flag. See `mix opers.revoke`."
  def revoke_oper(email), do: set_oper(email, false)

  @doc "List accounts flagged as IRC operators. See `mix opers.list`."
  def list_opers do
    import Ecto.Query

    with_started_repo(fn ->
      OhMyExOhIrcd.Repo.all(
        from u in OhMyExOhIrcd.Accounts.User,
          where: u.is_oper == true,
          order_by: [asc: u.email]
      )
      |> case do
        [] -> IO.puts("(no IRC operators flagged)")
        users -> Enum.each(users, &IO.puts(&1.email))
      end
    end)
  end

  defp set_oper(email, flag) do
    with_started_repo(fn ->
      case OhMyExOhIrcd.Accounts.get_user_by_email(email) do
        nil ->
          IO.puts("No account with email #{email}")
          {:error, :not_found}

        user ->
          case OhMyExOhIrcd.Accounts.set_oper(user, flag) do
            {:ok, updated} ->
              IO.puts("#{updated.email}: is_oper=#{updated.is_oper}")
              :ok

            {:error, cs} ->
              IO.puts("Update failed: #{inspect(cs.errors)}")
              {:error, cs}
          end
      end
    end)
  end

  # Build the public base URL from the Endpoint's `:url` config without
  # starting the Endpoint process (we run under `eval`, where it isn't up).
  defp base_url do
    url = Application.get_env(@app, OhMyExOhIrcdWeb.Endpoint, []) |> Keyword.get(:url, [])
    scheme = Keyword.get(url, :scheme, "http")
    host = Keyword.get(url, :host, "localhost")
    port = Keyword.get(url, :port, if(scheme == "https", do: 443, else: 80))
    default = if scheme == "https", do: 443, else: 80
    if port == default, do: "#{scheme}://#{host}", else: "#{scheme}://#{host}:#{port}"
  end

  # Start just the Repo (and its deps) for the duration of `fun`, then
  # shut it back down — the same trick `migrate/0` uses, so these helpers
  # work under `eval` on a node that isn't running the full app.
  defp with_started_repo(fun) do
    load_app()
    {:ok, result, _apps} = Ecto.Migrator.with_repo(OhMyExOhIrcd.Repo, fn _repo -> fun.() end)
    result
  end

  defp repos do
    Application.fetch_env!(@app, :ecto_repos)
  end

  defp load_app do
    # Many platforms require SSL when connecting to the database
    Application.ensure_all_started(:ssl)
    Application.ensure_loaded(@app)
  end
end