Packages
Robust API call planning and execution for Torn City
Current section
Files
Jump to
Current section
Files
lib/api.ex
# Copyright 2024-2025 tiksan
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
defmodule Tornex.API do
@moduledoc """
Core API functionality.
`Tornex.API` provides the core Torn API-related functionality to make the API calls.
## Configuration
Tornex uses compile-time configuration for certain API-related functionality:
- `base_url` (default: "https://api.torn.com") => base of the URL the remainder of the request URL is built off of
- `comment` (default: "tex/#{Mix.Project.config()[:version]}") => Comment used in the API calls
To customize one of these, include the following in a `config/*.exs`:
config :tornex,
base_url: "https://tornium.com/api/v1",
comment: "Tornium"
"""
use Tesla
@type return :: list() | map()
@type error :: {:error, :timeout | :cf_challenge | :unknown | any()}
@user_agent [{"User-agent", "tornex" <> Mix.Project.config()[:version]}]
@base_url Application.compile_env(:tornex, :base_url, "https://api.torn.com")
@comment Application.compile_env(:tornex, :comment, "tex/" <> Mix.Project.config()[:version])
plug(Tesla.Middleware.BaseUrl, @base_url)
plug(Tesla.Middleware.Headers, @user_agent)
plug(Tesla.Middleware.JSON)
@doc """
Converts a `Tornex.Query` to the URL required to make the HTTP request.
iex> query = %Tornex.Query{
...> resource: "user",
...> resource_id: 1,
...> key: "apikey",
...> key_owner: "apikey",
...> nice: 10,
...> selections: ["basic", "discord"]
...> }
iex> Tornex.API.query_to_url(query)
"https://api.torn.com/user/1?selections=basic,discord&key=apikey&comment=tex/#{Mix.Project.config()[:version]}"
"""
@spec query_to_url(Tornex.Query.t()) :: String.t()
def query_to_url(%Tornex.Query{} = query) do
("/" <> query.resource <> "/")
|> append_resource_id(query)
|> Tesla.build_url(
[
selections: Enum.join(query.selections || [], ","),
from: query.from || [],
to: query.to || [],
timestamp: query.timestamp || [],
key: query.key,
comment: @comment
] ++ (query.params || [])
)
end
@doc """
Performs a blocking HTTP GET request against the Torn API for the `Tornex.Query`.
## Examples
iex> query = %Tornex.Query{resource: "user", resource_id: 1, key: "", key_owner: 1, nice: 0}
iex> Tornex.API.torn_get(query)
...
"""
@spec torn_get(Tornex.Query.t()) :: map() | list() | error()
def torn_get(%Tornex.Query{} = query) do
:telemetry.execute([:tornex, :api, :start], %{}, %{
resource: query.resource,
resource_id: query.resource_id,
selections: query.selections,
user: query.key_owner
})
# TODO: Validate query
# TODO: Replace Tesla.get with some other library (e.g. Req)
# TODO: Switch `torn_get` to `get` and deprecate `torn_get`
{latency, response} = :timer.tc(&get/1, [query_to_url(query)])
:telemetry.execute([:tornex, :api, :finish], %{latency: latency}, %{
resource: query.resource,
resource_id: query.resource_id,
selections: query.selections,
user: query.key_owner
})
handle_response(query, response)
end
@spec handle_response(query :: Tornex.Query.t(), response :: tuple()) :: {:ok, return()} | error()
defp handle_response(%Tornex.Query{} = _query, {:ok, %Tesla.Env{status: 403} = response}) do
if Enum.member?(response.headers, {"cf-mitigated", "challenge"}) do
{:error, :cf_challenge}
else
response.body
end
end
defp handle_response(%Tornex.Query{} = _query, {:ok, %Tesla.Env{} = response}) do
response.body
end
defp handle_response(%Tornex.Query{} = query, {:error, :timeout}) do
:telemetry.execute([:tornex, :api, :timeout], %{}, %{
resource: query.resource,
resource_id: query.resource_id,
selections: query.selections,
user: query.key_owner
})
{:error, :timeout}
end
defp handle_response(%Tornex.Query{} = _query, {:error, reason}) do
IO.inspect(reason, label: "Unknown Tornex error")
{:error, :unknown}
end
@spec append_resource_id(String.t(), Tornex.Query.t()) :: String.t()
defp append_resource_id(uri_string, %Tornex.Query{resource_id: nil} = _) do
uri_string
end
defp append_resource_id(uri_string, query) when is_binary(query.resource_id) do
uri_string <> query.resource_id
end
defp append_resource_id(uri_string, query) when is_integer(query.resource_id) do
uri_string <> Integer.to_string(query.resource_id)
end
# NOTE: Disable the docs on all the functions originating from Tesla
# TODO: Disable docs on type `option()`
@doc false
def delete(_client, _url, _opts)
@doc false
def delete!(_client, _url, _opts)
@doc false
def get(_client, _url, _opts)
@doc false
def get!(_client, _url, _opts)
@doc false
def head(_client, _url, _opts)
@doc false
def head!(_client, _url, _opts)
@doc false
def options(_client, _url, _opts)
@doc false
def options!(_client, _url, _opts)
@doc false
def patch(_client, _url, _body, _opts)
@doc false
def patch!(_client, _url, _body, _opts)
@doc false
def post(_client, _url, _body, _opts)
@doc false
def post!(_client, _url, _body, _opts)
@doc false
def put(_client, _url, _body, _opts)
@doc false
def put!(_client, _url, _body, _opts)
@doc false
def request(_client, _options)
@doc false
def request!(_client, _options)
@doc false
def trace(_client, _url, _opts)
@doc false
def trace!(_client, _url, _opts)
end