From ef7a69fffe3cc60f4dd8ff376733a20ae4fdaec1 Mon Sep 17 00:00:00 2001 From: Raphael Nepomuceno <58113327+rphln@users.noreply.github.com> Date: Wed, 10 Feb 2021 21:13:48 -0300 Subject: [PATCH] Fix Pixiv. See . --- config/config.exs | 9 +- config/releases.exs | 3 +- lib/katsuragi/application.ex | 5 - lib/katsuragi/commands/pixiv.ex | 84 +++++++++----- lib/katsuragi/commands/pixiv/auth_server.ex | 57 ---------- lib/katsuragi/commands/pixiv/authenticator.ex | 106 ------------------ lib/katsuragi/commands/pixiv/constants.ex | 53 +++------ lib/katsuragi/commands/pixiv/tokens.ex | 72 ------------ lib/katsuragi/commands/pixiv/work.ex | 37 ++---- lib/katsuragi/routes.ex | 2 +- mix.lock | 40 ++++--- 11 files changed, 107 insertions(+), 361 deletions(-) delete mode 100644 lib/katsuragi/commands/pixiv/auth_server.ex delete mode 100644 lib/katsuragi/commands/pixiv/authenticator.ex delete mode 100644 lib/katsuragi/commands/pixiv/tokens.ex diff --git a/config/config.exs b/config/config.exs index cdb2a53..4be2ad4 100644 --- a/config/config.exs +++ b/config/config.exs @@ -8,9 +8,11 @@ config :logger, config :nostrum, token: System.get_env("DISCORD_TOKEN") +config :mojito, + timeout: :infinity + config :katsuragi, - pixiv_username: System.get_env("PIXIV_USERNAME"), - pixiv_password: System.get_env("PIXIV_PASSWORD") + pixiv_session_token: System.get_env("PIXIV_SESSION_TOKEN") config :extwitter, :oauth, consumer_key: System.get_env("TWITTER_CONSUMER_KEY"), @@ -20,8 +22,7 @@ config :extwitter, :oauth, config :katsuragi, Katsuragi.Scheduler, jobs: [ - {"20 19 * * *", {Katsuragi.Twitter, :send, []}}, - {"*/45 * * * *", {Katsuragi.Commands.Pixiv.AuthServer, :refresh, []}} + {"20 19 * * *", {Katsuragi.Twitter, :send, []}} ] if Mix.env() in [:dev, :test] do diff --git a/config/releases.exs b/config/releases.exs index ce154da..5e752d3 100644 --- a/config/releases.exs +++ b/config/releases.exs @@ -6,5 +6,4 @@ config :nostrum, token: System.get_env("DISCORD_TOKEN") config :katsuragi, - pixiv_username: System.get_env("PIXIV_USERNAME"), - pixiv_password: System.get_env("PIXIV_PASSWORD") + pixiv_session_token: System.get_env("PIXIV_SESSION_TOKEN") diff --git a/lib/katsuragi/application.ex b/lib/katsuragi/application.ex index bbd44d1..dfe0fed 100644 --- a/lib/katsuragi/application.ex +++ b/lib/katsuragi/application.ex @@ -6,15 +6,10 @@ defmodule Katsuragi.Application do use Application alias Katsuragi.Consumer - alias Katsuragi.Commands.Pixiv @spec start(any, any) :: {:error, any} | {:ok, pid} def start(_type, _args) do - username = Application.get_env(:katsuragi, :pixiv_username) - password = Application.get_env(:katsuragi, :pixiv_password) - children = [ - # {Pixiv.AuthServer, username: username, password: password}, Katsuragi.Scheduler ] diff --git a/lib/katsuragi/commands/pixiv.ex b/lib/katsuragi/commands/pixiv.ex index 70aab53..cc8af05 100644 --- a/lib/katsuragi/commands/pixiv.ex +++ b/lib/katsuragi/commands/pixiv.ex @@ -9,7 +9,7 @@ defmodule Katsuragi.Commands.Pixiv do alias Nostrum.Struct.Embed alias Katsuragi.Commands.Pixiv.Work - alias Katsuragi.Commands.Pixiv.AuthServer + alias Katsuragi.Commands.Pixiv.Constants @pattern ~r"pixiv\S*?/artworks/(\d+)"i @@ -17,44 +17,66 @@ defmodule Katsuragi.Commands.Pixiv do ["pixiv"] end + def reply(message, gallery_id) do + with {:ok, work} <- Work.get(gallery_id), + {:ok, file} <- Work.download(work["urls"]["regular"]) do + name = Path.basename(work["urls"]["regular"]) + + description = + work["tags"]["tags"] + |> Enum.map(&(&1["translation"]["en"] || &1["tag"])) + |> Enum.join(" • ") + + embed = + %Embed{} + |> Embed.put_color(0x0086E0) + |> Embed.put_title(work["title"]) + |> Embed.put_author(work["userName"], Work.author_link_for(work), nil) + |> Embed.put_image("attachment://#{name}") + |> Embed.put_description("`#{description}`") + |> Embed.put_footer(""" + Gallery with #{work["pageCount"]} page(s). + Shared by #{message.author.username}. + """) + |> Embed.put_timestamp(Work.updated_at!(work)) + |> Embed.put_url(Work.link_for(work)) + + file = %{body: file.body, name: name} + + Api.create_message(message, file: file, embed: embed) + end + end + def call(%Request{message: message} = request) do matches = for [_match, gallery_id] <- Regex.scan(@pattern, message.content) do - tokens = AuthServer.refresh_and_get() - - with {:ok, work} <- Work.get(tokens, gallery_id), - {:ok, file} <- Work.download(tokens, work["image_urls"]["medium"]) do - name = Path.basename(work["image_urls"]["medium"]) - - description = - work["tags"] - |> Enum.map(&(&1["translated_name"] || &1["name"])) - |> Enum.join(" • ") - - embed = - %Embed{} - |> Embed.put_color(0x0086E0) - |> Embed.put_title(work["title"]) - |> Embed.put_author(work["user"]["name"], Work.author_link_for(work), nil) - |> Embed.put_image("attachment://#{name}") - |> Embed.put_description("`#{description}`") - |> Embed.put_footer(""" - Gallery with #{work["page_count"]} page(s). - Shared by #{message.author.username}. - """) - |> Embed.put_timestamp(Work.updated_at!(work)) - |> Embed.put_url(Work.link_for(work)) - - file = %{body: file.body, name: name} - - Api.create_message!(message, file: file, embed: embed) - Process.sleep(2000) + task = Task.async(fn -> reply(message, gallery_id) end) + + # Wait for a while to avoid triggering the rate limiter. + Process.sleep(2000) + + case Task.await(task, :infinity) do + {:ok, response} -> + {:ok, response} + + _error -> + {:error, "Failed to create preview for #{Constants.gallery_url(gallery_id)}"} end end Request.register_after_send(request, fn _request -> unless Enum.empty?(matches) do - Api.delete_message(message) + errors = + Enum.filter(matches, fn + {:ok, _response} -> false + {:error, _reason} -> true + end) + + if Enum.empty?(errors) do + Api.delete_message!(message) + else + Enum.each(errors, fn {:error, reason} -> Api.create_message!(message, reason) end) + end end end) end diff --git a/lib/katsuragi/commands/pixiv/auth_server.ex b/lib/katsuragi/commands/pixiv/auth_server.ex deleted file mode 100644 index c8e5afb..0000000 --- a/lib/katsuragi/commands/pixiv/auth_server.ex +++ /dev/null @@ -1,57 +0,0 @@ -defmodule Katsuragi.Commands.Pixiv.AuthServer do - @moduledoc """ - Storage mechanism for upkeeping tokens. - """ - - use Agent - - alias Katsuragi.Commands.Pixiv.Tokens - alias Katsuragi.Commands.Pixiv.Authenticator - - @doc """ - Starts a tokens server. - """ - def start_link(options) - - def start_link(tokens) when is_list(tokens) do - start_link(Authenticator.login!(tokens[:username], tokens[:password])) - end - - def start_link(%Tokens{} = tokens) do - Agent.start_link(fn -> tokens end, name: __MODULE__) - end - - @doc """ - Gets the tokens as currently stored. - - Note that this function may return stale tokens. - """ - @spec get() :: Tokens.t() - def get do - Agent.get(__MODULE__, & &1) - end - - @doc """ - Forces a refresh on the tokens. - """ - def refresh do - Agent.update(__MODULE__, &Authenticator.refresh!/1) - end - - @doc """ - Refreshes and returns the stored tokens. - """ - @spec refresh_and_get() :: Tokens.t() - def refresh_and_get do - Agent.get_and_update(__MODULE__, fn tokens -> - tokens = - if Tokens.expired?(tokens) do - Authenticator.refresh!(tokens) - else - tokens - end - - {tokens, tokens} - end) - end -end diff --git a/lib/katsuragi/commands/pixiv/authenticator.ex b/lib/katsuragi/commands/pixiv/authenticator.ex deleted file mode 100644 index 53f78e8..0000000 --- a/lib/katsuragi/commands/pixiv/authenticator.ex +++ /dev/null @@ -1,106 +0,0 @@ -defmodule Katsuragi.Commands.Pixiv.Authenticator do - @moduledoc """ - Handles authentication requests, responses and expiration. - """ - - alias Katsuragi.Commands.Pixiv.Tokens - alias Katsuragi.Commands.Pixiv.Constants - - @doc """ - Authenticates the user using a valid `username` and `password`. - """ - @spec login(String.t(), String.t()) :: {:ok, Tokens.t()} | {:error, String.t()} - def login(username, password) do - authenticate(%{ - grant_type: "password", - username: username, - password: password - }) - end - - @doc """ - Authenticates the user using a valid `username` and `password`. - """ - @spec login!(String.t(), String.t()) :: Tokens.t() - def login!(username, password) do - {:ok, tokens} = login(username, password) - tokens - end - - @doc """ - Attempts to refresh the `t:Katsuragi.Commands.Pixiv.Tokens.t/0` access token using the - refresh token. - """ - @spec refresh(Tokens.t()) :: {:ok, Tokens.t()} | {:error, String.t()} - def refresh(%Tokens{refresh_token: token}) do - authenticate(%{ - grant_type: "refresh_token", - refresh_token: token - }) - end - - @doc """ - Attempts to refresh the `t:Katsuragi.Commands.Pixiv.Tokens.t/0` access token using the - refresh token. - """ - @spec refresh!(Tokens.t()) :: Tokens.t() - def refresh!(tokens) do - {:ok, tokens} = refresh(tokens) - tokens - end - - defp authenticate(form) do - form = - form - |> Map.merge(%{ - client_id: Constants.client_id(), - client_secret: Constants.client_secret(), - get_secure_url: 1 - }) - |> URI.encode_query() - - # TODO: Replace with `strftime`. - time = now() - - hash = :crypto.hash(:md5, [time, Constants.hash_secret()]) - hash = Base.encode16(hash, case: :lower) - - headers = [ - {"User-Agent", Constants.user_agent()}, - {"X-Client-Hash", hash}, - {"X-Client-Time", time}, - {"Content-Type", "application/x-www-form-urlencoded"} - ] - - with {:ok, %{status_code: 200, body: body}} <- - Mojito.post(Constants.auth_url(), headers, form) do - %{"response" => response} = Jason.decode!(body) - - token = - Tokens.new( - response["access_token"], - response["refresh_token"], - response["expires_in"] - ) - - {:ok, token} - else - {:ok, %{status_code: 400}} -> - {:error, "Authentication failed"} - - _ -> - {:error, "Unknown error"} - end - end - - defp now do - time = NaiveDateTime.utc_now() - - [year, month, day, hour, minute, second] = - [time.year, time.month, time.day, time.hour, time.minute, time.second] - |> Enum.map(&to_string/1) - |> Enum.map(&String.pad_leading(&1, 2, "0")) - - "#{year}-#{month}-#{day}T#{hour}:#{minute}:#{second}+00:00" - end -end diff --git a/lib/katsuragi/commands/pixiv/constants.ex b/lib/katsuragi/commands/pixiv/constants.ex index 8f943ed..30530be 100644 --- a/lib/katsuragi/commands/pixiv/constants.ex +++ b/lib/katsuragi/commands/pixiv/constants.ex @@ -3,42 +3,18 @@ defmodule Katsuragi.Commands.Pixiv.Constants do Constants used to access the Pixiv API. """ - alias Katsuragi.Commands.Pixiv.Tokens - @doc """ Base URL for the public API. """ @spec base_url() :: String.t() - def base_url, do: "https://app-api.pixiv.net/v1" - - @doc """ - Endpoint for requesting and refreshing authentication tokens. - """ - @spec auth_url() :: String.t() - def auth_url, do: "https://oauth.secure.pixiv.net/auth/token" - - @doc """ - Public identifier used by OAuth2. - """ - @spec client_id() :: String.t() - def client_id, do: "MOBrBDS8blbauoSck0ZfDbtuzpyT" - - @doc """ - Private identifier used by OAuth2. - """ - @spec client_secret() :: String.t() - def client_secret, do: "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj" - - @doc """ - Salt used to generate the `X-Client-Hash` header. - """ - @spec hash_secret() :: String.t() - def hash_secret, do: "28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c" + def base_url, do: "https://www.pixiv.net/ajax" @doc """ Value to be used in the `User-Agent` header. """ - def user_agent, do: "PixivIOSApp/6.4.0" + def user_agent, + do: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36" @doc """ Value to be used in the `Accept-Language` header. Determines whether tag @@ -46,26 +22,25 @@ defmodule Katsuragi.Commands.Pixiv.Constants do """ def accept_language, do: "en" + @doc """ + Authentication token, obtained by manually logging in to Pixiv and getting the `PHPSESSID` + cookie value. + """ + def session_token, do: Application.fetch_env!(:katsuragi, :pixiv_session_token) + @doc """ Request headers required to access Pixiv. """ @spec headers() :: [{String.t(), String.t()}] def headers do [ - {"Referer", auth_url()}, - {"User-Agent", user_agent()}, - {"Accept-Language", accept_language()} + {"Accept-Language", accept_language()}, + {"PHPSESSID", session_token()}, + {"Referer", base_url()}, + {"User-Agent", user_agent()} ] end - @doc """ - Request headers required to access Pixiv with authentication. - """ - @spec headers(Tokens.t()) :: [{String.t(), String.t()}] - def headers(%Tokens{access_token: access_token}) do - [{"Authorization", "Bearer #{access_token}"} | headers()] - end - @doc """ Returns the gallery URL for `id`. """ diff --git a/lib/katsuragi/commands/pixiv/tokens.ex b/lib/katsuragi/commands/pixiv/tokens.ex deleted file mode 100644 index 504bf9e..0000000 --- a/lib/katsuragi/commands/pixiv/tokens.ex +++ /dev/null @@ -1,72 +0,0 @@ -defmodule Katsuragi.Commands.Pixiv.Tokens do - @moduledoc """ - A struct to hold credential tokens from Pixiv. - - These consist of an access token, used to authenticate requests to the web - API, a refresh token, used to request a new access token when it expires, as - well the expiration timestamp itself for the token. - - Note that accessing the expiration timestamp directly is considered undefined - behaviour; methods such as `expired?/1` or `expires_in/1` should be used - instead. - """ - - @typedoc "An API token." - @type token :: String.t() - - @type t :: %__MODULE__{ - access_token: token | nil, - expires_in: integer, - last_refresh: integer, - refresh_token: token - } - - defstruct [:access_token, :refresh_token, :expires_in, :last_refresh] - - @doc """ - Builds a `Katsuragi.Commands.Pixiv.Tokens.t/0` from the given arguments. - """ - @spec new(token, token, integer) :: t - def new(access_token, refresh_token, expires_in) do - %__MODULE__{ - access_token: access_token, - refresh_token: refresh_token, - expires_in: expires_in, - last_refresh: time() - } - end - - @doc """ - Builds a `Katsuragi.Commands.Pixiv.Tokens.t/0` from the given refresh token. The resulting - credential is treated as expired by default. - """ - @spec from_refresh_token(token) :: t - def from_refresh_token(refresh_token) do - %__MODULE__{ - access_token: nil, - expires_in: 0, - last_refresh: time(), - refresh_token: refresh_token - } - end - - @doc """ - Returns how many seconds are left until the token expires. - """ - @spec expires_in(t) :: integer - def expires_in(%__MODULE__{expires_in: expires_in, last_refresh: last_refresh}) do - expires_in + last_refresh - time() - end - - @doc """ - Returns whether a `Pixiv.Tokens` struct is expired. - """ - @spec expired?(t) :: boolean - def expired?(%__MODULE__{expires_in: expires_in} = credentials) do - is_nil(expires_in) or expires_in(credentials) <= 0 - end - - defp time do - System.monotonic_time(:second) - end -end diff --git a/lib/katsuragi/commands/pixiv/work.ex b/lib/katsuragi/commands/pixiv/work.ex index 3bb8efc..02f3c8a 100644 --- a/lib/katsuragi/commands/pixiv/work.ex +++ b/lib/katsuragi/commands/pixiv/work.ex @@ -12,17 +12,11 @@ defmodule Katsuragi.Commands.Pixiv.Work do @doc """ Fetches metadata for a single gallery. """ - @spec get(Token.t(), integer) :: {:ok, gallery} | {:error, term} - def get(tokens, id) do - url = - "#{Constants.base_url()}/illust/detail" - |> URI.parse() - |> Map.put(:query, URI.encode_query(%{"illust_id" => id})) - |> URI.to_string() - - with {:ok, response} <- download(tokens, url) do + @spec get(integer) :: {:ok, gallery} | {:error, term} + def get(id) do + with {:ok, response} <- download("#{Constants.base_url()}/illust/#{id}") do case Jason.decode!(response.body) do - %{"illust" => work} -> + %{"error" => false, "body" => work} -> {:ok, work} _ -> @@ -31,11 +25,8 @@ defmodule Katsuragi.Commands.Pixiv.Work do end end - @doc """ - TODO: Write this. - """ - def download(tokens, url) do - case Mojito.get(url, Constants.headers(tokens)) do + def download(url) do + case Mojito.get(url, Constants.headers()) do {:ok, %{status_code: 200} = response} -> {:ok, response} @@ -52,15 +43,7 @@ defmodule Katsuragi.Commands.Pixiv.Work do """ @spec updated_at!(gallery) :: String.t() def updated_at!(gallery) do - Map.get(gallery, "reuploaded_time", gallery["created_time"]) - end - - @doc """ - Whether the given gallery is animated. - """ - @spec animated?(gallery) :: boolean - def animated?(gallery) do - gallery["type"] == "ugoira" + Map.get(gallery, "uploadDate", gallery["createDate"]) end @doc """ @@ -68,7 +51,7 @@ defmodule Katsuragi.Commands.Pixiv.Work do """ @spec multipage?(gallery) :: boolean def multipage?(gallery) do - gallery["page_count"] > 1 + gallery["pageCount"] > 1 end @doc """ @@ -76,7 +59,7 @@ defmodule Katsuragi.Commands.Pixiv.Work do """ @spec link_for(gallery) :: String.t() def link_for(gallery) do - Constants.gallery_url(gallery["id"]) + Constants.gallery_url(gallery["illustId"]) end @doc """ @@ -84,6 +67,6 @@ defmodule Katsuragi.Commands.Pixiv.Work do """ @spec author_link_for(gallery) :: String.t() def author_link_for(gallery) do - Constants.member_url(gallery["user"]["id"]) + Constants.member_url(gallery["userId"]) end end diff --git a/lib/katsuragi/routes.ex b/lib/katsuragi/routes.ex index b007b92..93f04f7 100644 --- a/lib/katsuragi/routes.ex +++ b/lib/katsuragi/routes.ex @@ -12,7 +12,7 @@ defmodule Katsuragi.Routes do Katsuragi.Commands.Stamp, Katsuragi.Commands.Choose, Katsuragi.Commands.Random, - # Katsuragi.Commands.Pixiv, + Katsuragi.Commands.Pixiv, Katsuragi.Commands.Sadpanda, Katsuragi.Commands.Riot, Katsuragi.Commands.Whale diff --git a/mix.lock b/mix.lock index 71764b7..b1b3e55 100644 --- a/mix.lock +++ b/mix.lock @@ -1,29 +1,35 @@ %{ - "castore": {:hex, :castore, "0.1.6", "2da0dccb3eacb67841d11790598ff03cd5caee861e01fad61dce1376b5da28e6", [:mix], [], "hexpm", "f874c510b720d31dd6334e9ae5c859a06a3c9e67dfe1a195c512e57588556d3f"}, - "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, - "cowlib": {:hex, :cowlib, "2.6.0", "8aa629f81a0fc189f261dc98a42243fa842625feea3c7ec56c48f4ccdb55490f", [:rebar3], [], "hexpm", "45a1a08e05e4c66f2af665295955e337d52c2d33b1f1cf24d353cadeddf34992"}, + "castore": {:hex, :castore, "0.1.9", "eb08a94c12ebff92a92d844c6ccd90728dc7662aab9bdc8b3b785ba653c499d5", [:mix], [], "hexpm", "99c3a38ad9c0bab03fee1418c98390da1a31f3b85e317db5840d51a1443d26c8"}, + "certifi": {:hex, :certifi, "2.5.3", "70bdd7e7188c804f3a30ee0e7c99655bc35d8ac41c23e12325f36ab449b70651", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "ed516acb3929b101208a9d700062d520f3953da3b6b918d866106ffa980e1c10"}, + "chacha20": {:hex, :chacha20, "1.0.2", "73c5e96eba5e94917603a43c5c7c6b049436a5d71d9d3182781c345d87d28c8b", [:mix], [], "hexpm", "549b89314cbffa0893ef923d999d625c227cab5f1301133598793225f02a3963"}, + "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm", "1e1a3d176d52daebbecbbcdfd27c27726076567905c2a9d7398c54da9d225761"}, "crontab": {:hex, :crontab, "1.1.10", "dc9bb1f4299138d47bce38341f5dcbee0aa6c205e864fba7bc847f3b5cb48241", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "1347d889d1a0eda997990876b4894359e34bfbbd688acbb0ba28a2795ca40685"}, - "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, - "extwitter": {:hex, :extwitter, "0.12.0", "c3078598cf53a50535b0e5f68a612fa836d1b5141731e8334c7a2964c147468d", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:oauther, "~> 1.1", [hex: :oauther, repo: "hexpm", optional: false]}], "hexpm", "8b8997ec79803327e7ddbf3be2174aa070ad447f4c0c4ee7c4d060a12933b067"}, - "gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm", "8453e2289d94c3199396eb517d65d6715ef26bcae0ee83eb5ff7a84445458d76"}, - "gun": {:hex, :gun, "1.3.2", "542064cbb9f613650b8a8100b3a927505f364fbe198b7a5a112868ff43f3e477", [:rebar3], [{:cowlib, "~> 2.6.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "ba323f0a5fd8abac379a3e1fe6d8ce570c4a12c7fd1c68f4994b53447918e462"}, - "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, - "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"}, - "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, - "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, + "curve25519": {:hex, :curve25519, "1.0.4", "e570561b832c29b3ce4fd8b9fcd9c9546916188860568f1c1fc9428d7cb00894", [:mix], [], "hexpm", "1a068bf9646e7067bf6aa5bf802b404002734e09bb5300f098109df03e31f9f5"}, + "ed25519": {:hex, :ed25519, "1.3.2", "e3a2d4badf57f0799279cf09925bd761ec38df6df3696e266585626280b5c0ad", [:mix], [], "hexpm", "2290e46e0e23717adbe20632c6dd29aa71a46ca6e153ef7ba41fe1204f66f859"}, + "equivalex": {:hex, :equivalex, "1.0.2", "b9a9aaf79f2556288f514218653beaddb15afa2af407bfec37c5c4906e39f514", [:mix], [], "hexpm", "f7f8127c59be715ee6288f8c59fa8fc40e6428fb5c9bd2a001de2c9b1ff3f1c2"}, + "extwitter": {:hex, :extwitter, "0.12.2", "2cb458404e8b22f8c0b6f6180edc09101a41657ec41f29b3e2f7954cd45478ee", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:oauther, "~> 1.1", [hex: :oauther, repo: "hexpm", optional: false]}], "hexpm", "ad9d09ab2e59f09df98c0382976b0191bb90486608e658407f00ea9277c72616"}, + "gen_stage": {:hex, :gen_stage, "1.1.0", "dd0c0f8d2f3b993fdbd3d58e94abbe65380f4e78bdee3fa93d5618d7d14abe60", [:mix], [], "hexpm", "7f2b36a6d02f7ef2ba410733b540ec423af65ec9c99f3d1083da508aca3b9305"}, + "gun": {:hex, :gun, "1.3.3", "cf8b51beb36c22b9c8df1921e3f2bc4d2b1f68b49ad4fbc64e91875aa14e16b4", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "3106ce167f9c9723f849e4fb54ea4a4d814e3996ae243a1c828b256e749041e0"}, + "hackney": {:hex, :hackney, "1.17.0", "717ea195fd2f898d9fe9f1ce0afcc2621a41ecfe137fae57e7fe6e9484b9aa99", [:rebar3], [{:certifi, "~>2.5", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "64c22225f1ea8855f584720c0e5b3cd14095703af1c9fbc845ba042811dc671c"}, + "httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, + "kcl": {:hex, :kcl, "1.3.2", "40ca3e6c6421781b6db8ba2e5354e64c975f19a3073aed62f632e6edcb714148", [:mix], [{:curve25519, ">= 1.0.4", [hex: :curve25519, repo: "hexpm", optional: false]}, {:ed25519, "~> 1.3", [hex: :ed25519, repo: "hexpm", optional: false]}, {:poly1305, "~> 1.0", [hex: :poly1305, repo: "hexpm", optional: false]}, {:salsa20, "~> 1.0", [hex: :salsa20, repo: "hexpm", optional: false]}], "hexpm", "f66d34ef4c9be59fa439e8ca861daed074e6542306a81dabe8e3ab2be9dc78fd"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "mint": {:hex, :mint, "1.1.0", "1fd0189edd9e3ffdbd7fcd8bc3835902b987a63ec6c4fd1aa8c2a56e2165f252", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bfd316c3789340b682d5679a8116bcf2112e332447bdc20c1d62909ee45f48d"}, - "mojito": {:hex, :mojito, "0.7.3", "7356f3b7697d79520a243b48cf0bf8bd1152b2e9cdb6ff7cf22cd0769f32dd40", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "433479d8ef1c882fafe864ac6d7b08249f321fc46bdcc8db78691bc1ddcf234a"}, - "nostrum": {:hex, :nostrum, "0.4.3", "393f22d9ede8f24aded2eaef9d9637c67512a6c6579c23a5c45a37e1466f7f1b", [:mix], [{:gen_stage, "~> 0.11", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.7", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "aef02c1a5de6ae05a533a14d849ab559738e84de95b012bcb56fc673c9357249"}, - "number": {:hex, :number, "1.0.3", "932c8a2d478a181c624138958ca88a78070332191b8061717270d939778c9857", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "dd397bbc096b2ca965a6a430126cc9cf7b9ef7421130def69bcf572232ca0f18"}, + "mint": {:hex, :mint, "1.2.1", "369cc8fecc54afd170e11740aa7efd066709e5ef3b5a2c63f0a47d1542cbd56a", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "053fe2f48c965f31878a16272478d9299fa412bc4df86dee2678986f2e40e018"}, + "mojito": {:hex, :mojito, "0.7.7", "23fc76342227d554405628877c883ad8a42ef32a47dabefb632048d83b67e03f", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.1", [hex: :mint, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b95592a52606709f040cf5ee91fa9d96da2a6763f5381f284043cbc2b6777653"}, + "nostrum": {:hex, :nostrum, "0.4.6", "b5e93e4f99d47d29c5c0471dfa61c51cef4a043f0692c0710396cb945a19c1ba", [:mix], [{:gen_stage, "~> 0.11 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.7", [hex: :httpoison, repo: "hexpm", optional: false]}, {:kcl, "~> 1.3", [hex: :kcl, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}, {:porcelain, "~> 2.0", [hex: :porcelain, repo: "hexpm", optional: false]}], "hexpm", "ff9346cba9a7e7f3b1e29b0338f7b4e7dbd5b57c48c5402ce72804ba4f848f91"}, "oauther": {:hex, :oauther, "1.1.1", "7d8b16167bb587ecbcddd3f8792beb9ec3e7b65c1f8ebd86b8dd25318d535752", [:mix], [], "hexpm", "9374f4302045321874cccdc57eb975893643bd69c3b22bf1312dab5f06e5788e"}, - "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, + "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "percussion": {:git, "https://github.com/rphln/Percussion.git", "7b8d669f05ce1482c7b7129f95a5709980eebe61", []}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, + "poly1305": {:hex, :poly1305, "1.0.2", "c6bb74cbe79747cc12aa1580791c2bd8e0f062bc8faaf117b756e675cfaea03d", [:mix], [{:chacha20, "~> 1.0", [hex: :chacha20, repo: "hexpm", optional: false]}, {:equivalex, "~> 1.0", [hex: :equivalex, repo: "hexpm", optional: false]}], "hexpm", "d0a0f8be324e7bfdd61e8e52215024a025816f3a7f665c274ad5bea154480c2b"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, + "porcelain": {:hex, :porcelain, "2.0.3", "2d77b17d1f21fed875b8c5ecba72a01533db2013bd2e5e62c6d286c029150fdc", [:mix], [], "hexpm", "dc996ab8fadbc09912c787c7ab8673065e50ea1a6245177b0c24569013d23620"}, "quantum": {:hex, :quantum, "3.3.0", "e8f6b9479728774288c5f426b11a6e3e8f619f3c226163a7e18bccfe543b714d", [:mix], [{:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.14 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3b83ef137ab3887e783b013418b5ce3e847d66b71c4ef0f233b0321c84b72f67"}, + "salsa20": {:hex, :salsa20, "1.0.2", "558fddb4169b1f90b1748d42e9221ba8d1354c414a03361308cc7bc0fd2f45c7", [:mix], [], "hexpm", "8d0d394c67da8d44073cbf2c030c4e0e04e7bed92967761be60419d8d46df18d"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, }