live_beats/lib/live_beats/github.ex
Chris McCord 6b02cfc614 Touchup
2022-08-03 09:40:48 -04:00

135 lines
3.5 KiB
Elixir

defmodule LiveBeats.Github do
def authorize_url() do
state = random_string()
"https://github.com/login/oauth/authorize?client_id=#{client_id()}&state=#{state}&scope=user:email"
end
def exchange_access_token(opts) do
code = Keyword.fetch!(opts, :code)
state = Keyword.fetch!(opts, :state)
state
|> fetch_exchange_response(code)
|> fetch_user_info()
|> fetch_emails()
end
defp fetch_exchange_response(state, code) do
resp =
http(
"github.com",
"POST",
"/login/oauth/access_token",
[state: state, code: code, client_secret: secret()],
[{"accept", "application/json"}]
)
with {:ok, resp} <- resp,
%{"access_token" => token} <- Jason.decode!(resp) do
{:ok, token}
else
{:error, _reason} = err -> err
%{} = resp -> {:error, {:bad_response, resp}}
end
end
defp fetch_user_info({:error, _reason} = error), do: error
defp fetch_user_info({:ok, token}) do
resp =
http(
"api.github.com",
"GET",
"/user",
[],
[{"accept", "application/vnd.github.v3+json"}, {"Authorization", "token #{token}"}]
)
case resp do
{:ok, info} -> {:ok, %{info: Jason.decode!(info), token: token}}
{:error, _reason} = err -> err
end
end
defp fetch_emails({:error, _} = err), do: err
defp fetch_emails({:ok, user}) do
resp =
http(
"api.github.com",
"GET",
"/user/emails",
[],
[{"accept", "application/vnd.github.v3+json"}, {"Authorization", "token #{user.token}"}]
)
case resp do
{:ok, info} ->
emails = Jason.decode!(info)
{:ok, Map.merge(user, %{primary_email: primary_email(emails), emails: emails})}
{:error, _reason} = err ->
err
end
end
def random_string do
binary = <<
System.system_time(:nanosecond)::64,
:erlang.phash2({node(), self()})::16,
:erlang.unique_integer()::16
>>
binary
|> Base.url_encode64()
|> String.replace(["/", "+"], "-")
end
defp client_id, do: LiveBeats.config([:github, :client_id])
defp secret, do: LiveBeats.config([:github, :client_secret])
defp http(host, method, path, query, headers, body \\ "") do
{:ok, conn} = Mint.HTTP.connect(:https, host, 443)
path = path <> "?" <> URI.encode_query([{:client_id, client_id()} | query])
{:ok, conn, ref} =
Mint.HTTP.request(
conn,
method,
path,
headers,
body
)
receive_resp(conn, ref, nil, nil, false)
end
defp receive_resp(conn, ref, status, data, done?) do
receive do
message ->
{:ok, conn, responses} = Mint.HTTP.stream(conn, message)
{new_status, new_data, done?} =
Enum.reduce(responses, {status, data, done?}, fn
{:status, ^ref, new_status}, {_old_status, data, done?} -> {new_status, data, done?}
{:headers, ^ref, _headers}, acc -> acc
{:data, ^ref, binary}, {status, nil, done?} -> {status, binary, done?}
{:data, ^ref, binary}, {status, data, done?} -> {status, data <> binary, done?}
{:done, ^ref}, {status, data, _done?} -> {status, data, true}
end)
cond do
done? and new_status == 200 -> {:ok, new_data}
done? -> {:error, {new_status, new_data}}
!done? -> receive_resp(conn, ref, new_status, new_data, done?)
end
end
end
defp primary_email(emails) do
Enum.find(emails, fn email -> email["primary"] end)["email"] || Enum.at(emails, 0)
end
end