Current section
Files
Jump to
Current section
Files
lib/nadia.ex
defmodule Nadia do
@moduledoc """
Provides access to Telegram Bot API.
## Reference
https://core.telegram.org/bots/api#available-methods
## Explicit clients
Public Bot API wrappers accept a `%Nadia.Client{}` as the first argument when
a call should use a specific bot identity:
client = Nadia.Client.new(token: System.fetch_env!("TELEGRAM_BOT_TOKEN"))
Nadia.send_message(client, 123, "hello")
Legacy application config based calls remain supported:
Nadia.send_message(123, "hello")
"""
@moduledoc groups: [
"Updates And Files",
"Messages",
"Interactions And Editing",
"Chats And Administration",
"Pinned Messages",
"Stickers",
"Games",
"Payments",
"Bot Account",
"Business",
"Managed Bots",
"Gifts And Verification"
]
alias Nadia.Client
alias Nadia.Model.{
BotAccessSettings,
BotCommand,
BotDescription,
BotName,
BotShortDescription,
BusinessConnection,
ChatAdministratorRights,
ChatInviteLink,
Error,
File,
ForumTopic,
GameHighScore,
Gifts,
MenuButton,
Message,
MessageId,
OwnedGifts,
Poll,
PreparedInlineMessage,
PreparedKeyboardButton,
SentGuestMessage,
SentWebAppMessage,
StarAmount,
StarTransactions,
Story,
Sticker,
Update,
User,
UserChatBoosts,
UserProfileAudios,
UserProfilePhotos,
WebhookInfo
}
import Nadia.API
@behaviour Nadia.Behaviour
defp api_request(method), do: request(method)
defp api_request(%Client{} = client, method), do: request(client, method, [], nil)
defp api_request(method, options), do: request(method, options)
defp api_request(%Client{} = client, method, options), do: request(client, method, options, nil)
defp api_request(method, options, file_field), do: request(method, options, file_field)
defp api_request(%Client{} = client, method, options, file_field) do
request(client, method, options, file_field)
end
defp encode_permissions(permissions) when is_list(permissions) do
permissions
|> Map.new()
|> reject_nil_values()
|> Jason.encode!()
end
defp encode_permissions(%_{} = permissions) do
permissions
|> Map.from_struct()
|> reject_nil_values()
|> Jason.encode!()
end
defp encode_permissions(permissions) when is_map(permissions) do
permissions
|> reject_nil_values()
|> Jason.encode!()
end
defp encode_permissions(permissions), do: permissions
defp encode_json_payload(nil), do: nil
defp encode_json_payload(payload) when is_binary(payload), do: payload
defp encode_json_payload(payload) do
%Nadia.InputFile.JSONPayload{value: json_payload_value(payload)}
end
defp encode_json_array_payload(nil), do: nil
defp encode_json_array_payload(payload) when is_binary(payload), do: payload
defp encode_json_array_payload(payload) when is_list(payload) do
%Nadia.InputFile.JSONPayload{value: Enum.map(payload, &json_payload_value/1)}
end
defp encode_json_array_payload(payload), do: encode_json_payload(payload)
defp json_payload_value(payload) when is_list(payload) do
if Keyword.keyword?(payload) do
payload
|> Map.new()
|> json_payload_value()
else
Enum.map(payload, &json_payload_value/1)
end
end
defp json_payload_value(%Nadia.InputFile{} = payload), do: payload
defp json_payload_value(%Nadia.InputContactMessageContent{} = payload), do: payload
defp json_payload_value(%Nadia.InputInvoiceMessageContent{} = payload), do: payload
defp json_payload_value(%Nadia.InputLocationMessageContent{} = payload), do: payload
defp json_payload_value(%Nadia.InputMedia{} = payload), do: payload
defp json_payload_value(%Nadia.InputPaidMedia{} = payload), do: payload
defp json_payload_value(%Nadia.InputPollOption{} = payload), do: payload
defp json_payload_value(%Nadia.InputPollMedia{} = payload), do: payload
defp json_payload_value(%Nadia.InputProfilePhoto{} = payload), do: payload
defp json_payload_value(%Nadia.InputRichMessage{} = payload), do: payload
defp json_payload_value(%Nadia.InputRichMessageContent{} = payload), do: payload
defp json_payload_value(%Nadia.InputTextMessageContent{} = payload), do: payload
defp json_payload_value(%Nadia.InputVenueMessageContent{} = payload), do: payload
defp json_payload_value(%Nadia.LabeledPrice{} = payload), do: payload
defp json_payload_value(%Nadia.ReactionType{} = payload), do: payload
defp json_payload_value(%Nadia.InputSticker{} = payload), do: payload
defp json_payload_value(%Nadia.StoryArea{} = payload), do: payload
defp json_payload_value(%Nadia.InputStoryContent{} = payload), do: payload
defp json_payload_value(%_{} = payload) do
payload
|> Map.from_struct()
|> json_payload_value()
end
defp json_payload_value(payload) when is_map(payload) do
payload
|> reject_nil_values()
|> Map.new(fn {key, value} -> {key, json_payload_value(value)} end)
end
defp json_payload_value(payload), do: payload
defp encode_poll_options(params) when is_list(params) do
params
|> encode_json_array_option(:options)
|> encode_json_array_option(:question_entities)
|> encode_json_array_option(:country_codes)
|> encode_json_array_option(:correct_option_ids)
|> encode_json_array_option(:explanation_entities)
|> encode_json_array_option(:description_entities)
|> encode_json_option(:media)
|> encode_json_option(:explanation_media)
|> encode_json_option(:reply_parameters)
end
defp encode_poll_options(params) when is_map(params) do
params
|> encode_json_array_option(:options)
|> encode_json_array_option(:question_entities)
|> encode_json_array_option(:country_codes)
|> encode_json_array_option(:correct_option_ids)
|> encode_json_array_option(:explanation_entities)
|> encode_json_array_option(:description_entities)
|> encode_json_option(:media)
|> encode_json_option(:explanation_media)
|> encode_json_option(:reply_parameters)
end
defp validate_poll_options(params) do
with :ok <- validate_poll_media_option(option_value(params, :media), :description),
:ok <- validate_poll_explanation_media(params),
:ok <- validate_poll_option_values(params) do
:ok
end
end
defp validate_poll_explanation_media(params) do
explanation_media = option_value(params, :explanation_media)
if typed_poll_media?(explanation_media) do
case option_value(params, :type) do
type when type in ["quiz", :quiz] ->
validate_poll_media_option(explanation_media, :explanation)
_type ->
{:error, {:input_poll_media, :explanation_media_requires_quiz}}
end
else
:ok
end
end
defp validate_poll_option_values(params) do
options = option_value(params, :options)
with :ok <- validate_poll_option_list(options),
:ok <- validate_correct_option_ids(params, options) do
:ok
end
end
defp validate_poll_option_list([]),
do: {:error, {:input_poll_option, {:option_count, 0}}}
defp validate_poll_option_list(options) when is_list(options) do
if Keyword.keyword?(options) do
:ok
else
typed_options? = Enum.any?(options, &match?(%Nadia.InputPollOption{}, &1))
typed_media? = Enum.any?(options, &typed_poll_media?(poll_option_media(&1)))
cond do
typed_options? and length(options) not in 1..12 ->
{:error, {:input_poll_option, {:option_count, length(options)}}}
typed_media? and length(options) not in 1..12 ->
{:error, {:input_poll_media, {:poll_option_size, length(options)}}}
true ->
options
|> Enum.with_index()
|> Enum.reduce_while(:ok, fn {option, index}, :ok ->
case validate_poll_option(option, index) do
:ok -> {:cont, :ok}
{:error, _reason} = error -> {:halt, error}
end
end)
end
end
end
defp validate_poll_option_list(_options), do: :ok
defp validate_poll_option(%Nadia.InputPollOption{} = option, index) do
case Nadia.InputPollOption.to_map(option) do
{:ok, _map} -> :ok
{:error, reason} -> {:error, {:input_poll_option, {:option, index, reason}}}
end
end
defp validate_poll_option(option, _index) do
validate_poll_media_option(poll_option_media(option), :option)
end
defp validate_correct_option_ids(params, options) when is_list(options) do
if Keyword.keyword?(options) or
not Enum.any?(options, &match?(%Nadia.InputPollOption{}, &1)) do
:ok
else
ids = option_value(params, :correct_option_ids)
quiz? = option_value(params, :type) in ["quiz", :quiz]
cond do
is_binary(ids) ->
:ok
quiz? and ids in [nil, []] ->
{:error, {:input_poll_option, {:correct_option_ids, :required}}}
is_nil(ids) ->
:ok
not is_list(ids) ->
{:error, {:input_poll_option, {:correct_option_ids, :integer_list_required}}}
not Enum.all?(ids, &is_integer/1) ->
{:error, {:input_poll_option, {:correct_option_ids, :integer_list_required}}}
ids != Enum.sort(ids) or length(ids) != MapSet.size(MapSet.new(ids)) ->
{:error, {:input_poll_option, {:correct_option_ids, :not_strictly_increasing}}}
invalid = Enum.find(ids, &(&1 < 0 or &1 >= length(options))) ->
{:error,
{:input_poll_option, {:correct_option_ids, {:out_of_bounds, invalid, length(options)}}}}
true ->
:ok
end
end
end
defp validate_correct_option_ids(_params, _options), do: :ok
defp validate_poll_media_option(media, context) do
if typed_poll_media?(media) do
case Nadia.InputPollMedia.validate_context(media, context) do
:ok -> :ok
{:error, reason} -> {:error, {:input_poll_media, reason}}
end
else
:ok
end
end
defp typed_poll_media?(%Nadia.InputMedia{}), do: true
defp typed_poll_media?(%Nadia.InputPollMedia{}), do: true
defp typed_poll_media?(_media), do: false
defp poll_option_media(option) when is_list(option) do
if Keyword.keyword?(option), do: Keyword.get(option, :media), else: nil
end
defp poll_option_media(%_{} = option), do: option |> Map.from_struct() |> poll_option_media()
defp poll_option_media(option) when is_map(option) do
Map.get(option, :media, Map.get(option, "media"))
end
defp poll_option_media(_option), do: nil
defp validate_rich_message(%Nadia.InputRichMessage{} = rich_message, context) do
case Nadia.InputRichMessage.validate_context(rich_message, context) do
:ok -> :ok
{:error, reason} -> {:error, {:input_rich_message, reason}}
end
end
defp validate_rich_message(_rich_message, _context), do: :ok
defp validate_story_options(options) do
case Nadia.StoryArea.validate_areas(option_value(options, :areas)) do
:ok -> :ok
{:error, reason} -> {:error, {:story_area, reason}}
end
end
defp do_answer_guest_query(client, guest_query_id, result, options) do
args = [guest_query_id: guest_query_id, result: encode_json_payload(result)]
if client do
api_request(client, "answerGuestQuery", args ++ options)
else
api_request("answerGuestQuery", args ++ options)
end
end
defp do_save_prepared_inline_message(client, user_id, result, options) do
args = [user_id: user_id, result: encode_json_payload(result)]
if client do
api_request(client, "savePreparedInlineMessage", request_options(args, options))
else
api_request("savePreparedInlineMessage", request_options(args, options))
end
end
defp do_answer_inline_query(client, inline_query_id, results, options) do
args = [inline_query_id: inline_query_id, results: encode_json_array_payload(results)]
if client do
api_request(client, "answerInlineQuery", args ++ options)
else
api_request("answerInlineQuery", args ++ options)
end
end
defp request_options(required, options) when is_list(options), do: required ++ options
defp request_options(required, options) when is_map(options),
do: Map.merge(Map.new(required), options)
defp encode_json_option(options, key) when is_list(options) do
Keyword.update(options, key, nil, &encode_json_payload/1)
end
defp encode_json_option(options, key) when is_map(options) do
update_map_option(options, key, &encode_json_payload/1)
end
defp encode_json_option(options, _key), do: options
defp encode_json_array_option(options, key) when is_list(options) do
Keyword.update(options, key, nil, &encode_json_array_payload/1)
end
defp encode_json_array_option(options, key) when is_map(options) do
update_map_option(options, key, &encode_json_array_payload/1)
end
defp encode_json_array_option(options, _key), do: options
defp update_map_option(options, key, encoder) do
string_key = Atom.to_string(key)
cond do
Map.has_key?(options, key) -> Map.update!(options, key, encoder)
Map.has_key?(options, string_key) -> Map.update!(options, string_key, encoder)
true -> options
end
end
defp encode_invoice_options(options) do
options
|> encode_json_array_option(:suggested_tip_amounts)
|> encode_json_option(:provider_data)
|> encode_json_option(:suggested_post_parameters)
|> encode_json_option(:reply_parameters)
end
defp encode_paid_media_options(options) do
options
|> encode_json_array_option(:caption_entities)
|> encode_json_option(:suggested_post_parameters)
|> encode_json_option(:reply_parameters)
end
defp encode_rich_message_options(options) do
options
|> encode_json_option(:suggested_post_parameters)
|> encode_json_option(:reply_parameters)
end
defp encode_story_options(options) do
options
|> encode_json_array_option(:caption_entities)
|> encode_json_array_option(:areas)
end
defp encode_added_user_ids(options) do
Keyword.update(options, :added_user_ids, nil, &Jason.encode!/1)
end
defp encode_message_ids(message_ids), do: Jason.encode!(message_ids)
defp encode_reaction_option(options) do
Keyword.update(options, :reaction, nil, &encode_reaction_types/1)
end
defp encode_reaction_types(nil), do: nil
defp encode_reaction_types(reaction) when is_list(reaction) do
reaction
|> normalize_reaction_types()
|> Jason.encode!()
end
defp encode_reaction_types(reaction) do
[reaction]
|> normalize_reaction_types()
|> Jason.encode!()
end
defp normalize_reaction_types([]), do: []
defp normalize_reaction_types(reaction) when is_list(reaction) do
if Keyword.keyword?(reaction) do
[reaction_type_map(reaction)]
else
Enum.map(reaction, &reaction_type_map/1)
end
end
defp reaction_type_map(reaction) when is_list(reaction) do
reaction
|> Map.new()
|> reject_nil_values()
end
defp reaction_type_map(%_{} = reaction) do
reaction
|> Map.from_struct()
|> reject_nil_values()
end
defp reaction_type_map(reaction) when is_map(reaction) do
reject_nil_values(reaction)
end
defp reject_nil_values(map) do
for {key, value} <- map, value != nil, into: %{}, do: {key, value}
end
defp legacy_input_sticker(sticker, emojis, options) do
emoji_list = if is_list(emojis), do: emojis, else: [emojis]
sticker_options = []
sticker_options =
case option_value(options, :mask_position) do
nil -> sticker_options
mask_position -> Keyword.put(sticker_options, :mask_position, mask_position)
end
sticker_options =
case option_value(options, :keywords) do
nil -> sticker_options
keywords -> Keyword.put(sticker_options, :keywords, keywords)
end
Nadia.InputSticker.static(sticker, emoji_list, sticker_options)
end
defp current_sticker_set_options(options) do
options
|> delete_option(:mask_position)
|> delete_option(:keywords)
|> rename_legacy_mask_option()
end
defp current_sticker_options?(options) when is_map(options), do: true
defp current_sticker_options?(options) when is_list(options),
do: Keyword.keyword?(options)
defp current_sticker_options?(_options), do: false
defp rename_legacy_mask_option(options) do
case option_value(options, :contains_masks) do
true -> options |> delete_option(:contains_masks) |> put_option(:sticker_type, "mask")
_ -> delete_option(options, :contains_masks)
end
end
defp option_value(options, key) when is_list(options), do: Keyword.get(options, key)
defp option_value(options, key) when is_map(options),
do: Map.get(options, key, Map.get(options, Atom.to_string(key)))
defp delete_option(options, key) when is_list(options), do: Keyword.delete(options, key)
defp delete_option(options, key) when is_map(options), do: Map.delete(options, key)
defp put_option(options, key, value) when is_list(options), do: Keyword.put(options, key, value)
defp put_option(options, key, value) when is_map(options), do: Map.put(options, key, value)
use Nadia.Methods.BotAccount
use Nadia.Methods.GiftsAndVerification
use Nadia.Methods.Messages
use Nadia.Methods.Games
use Nadia.Methods.UpdatesAndFiles
use Nadia.Methods.Chats
use Nadia.Methods.Business
use Nadia.Methods.Payments
use Nadia.Methods.ManagedBots
use Nadia.Methods.Interactions
use Nadia.Methods.Stickers
use Nadia.Methods.PinnedMessages
end