%%% @author Sergey Prokhorov %%% @copyright (C) 2016, Sergey Prokhorov %%% @doc %%% Main API module. %%% @end %%% Created : 18 May 2016 by Sergey Prokhorov -module(pe4kin). -export([api_call/2, api_call/3, download_file/2]). -export([launch_bot/3]). -export([get_me/1, send_message/2, forward_message/2, send_photo/2, send_audio/2, send_document/2, send_sticker/2, send_video/2, send_voice/2, send_location/2, send_venue/2, send_contact/2, send_chat_action/2, get_user_profile_photos/2, get_file/2, kick_chat_member/2, unban_chat_member/2, answer_callback_query/2, get_updates_sync/2]). -export_type([bot_name/0, update/0]). -export_type([json_object/0, json_value/0]). -type bot_name() :: binary(). % bot name without leading "@" -type update() :: json_object(). -type input_file() :: {file, Name :: binary(), ContentType :: binary(), Payload :: iodata()} | {file_path, file:name()} | integer(). -type json_literal() :: null | true | false | json_string() | json_number(). -type json_value() :: json_literal() | json_object() | json_array(). -type json_array() :: [json_value()]. -type json_string() :: atom() | binary(). -type json_number() :: integer() | float(). -type json_object() :: #{json_string() => json_value()}. -type api_body() :: undefined | {json, json_object()} | {query, #{json_string() => json_literal()}} | {multipart, #{json_string() => json_literal() | input_file()}}. -define(HACKNEY_POOL, ?MODULE). get_token(Bot) -> {ok, Token} = application:get_env(?MODULE, {Bot, token}), Token. launch_bot(Bot, Token, Opts) -> application:set_env(?MODULE, {Bot, token}, Token), case Opts of #{receiver := true} -> %% pe4kin_receiver_sup:start_receiver( pe4kin_receiver:start_link( Bot, Token, maps:remove(receiver, Opts)); _ -> ok end. %% Api methods get_me(Bot) -> api_call(Bot, <<"getMe">>). send_message(Bot, #{chat_id := _, text := _} = Message) -> api_call(Bot, <<"sendMessage">>, {json, Message}). forward_message(Bot, #{chat_id := _, from_chat_id := _, message_id := _} = Req) -> api_call(Bot, <<"forwardMessage">>, {json, Req}). send_photo(Bot, #{chat_id := _, photo := _} = Req) -> api_call(Bot, <<"sendPhoto">>, body_with_file([photo], Req)). send_audio(Bot, #{chat_id := _, audio := _} = Req) -> api_call(Bot, <<"sendAudio">>, body_with_file([audio], Req)). send_document(Bot, #{chat_id := _, document := _} = Req) -> api_call(Bot, <<"sendDocument">>, body_with_file([document], Req)). send_sticker(Bot, #{chat_id := _, sticker := _} = Req) -> api_call(Bot, <<"sendSticker">>, body_with_file([sticker], Req)). send_video(Bot, #{chat_id := _, video := _} = Req) -> api_call(Bot, <<"sendVideo">>, body_with_file([video], Req)). send_voice(Bot, #{chat_id := _, voice := _} = Req) -> api_call(Bot, <<"sendVoice">>, body_with_file([voice], Req)). send_location(Bot, #{chat_id := _, latitude := _, longitude := _} = Req) -> api_call(Bot, <<"sendLocation">>, {json, Req}). send_venue(Bot, #{chat_id := _, latitude := _, longitude := _, title := _, address := _} = Req) -> api_call(Bot, <<"sendVenue">>, {json, Req}). send_contact(Bot, #{chat_id := _, phone_number := _, first_name := _} = Req) -> api_call(Bot, <<"sendContact">>, {json, Req}). send_chat_action(Bot, #{chat_id := _, action := _} = Req) -> api_call(Bot, <<"sendChatAction">>, {json, Req}). get_user_profile_photos(Bot, #{user_id := _} = Req) -> api_call(Bot, <<"getUserProfilePhotos">>, {json, Req}). get_file(Bot, #{file_id := _} = Req) -> api_call(Bot, <<"getFile">>, {json, Req}). kick_chat_member(Bot, #{chat_id := _, user_id := _} = Req) -> api_call(Bot, <<"kickChatMember">>, {json, Req}). unban_chat_member(Bot, #{chat_id := _, user_id := _} = Req) -> api_call(Bot, <<"unbanChatMember">>, {json, Req}). answer_callback_query(Bot, #{callback_query_id := _} = Req) -> api_call(Bot, <<"answerCallbackQuery">>, {json, Req}). %% @doc %% This API is for testing purposes get_updates_sync(Bot, Opts) -> Opts1 = Opts#{timeout => 0}, QS = hackney_url:qs([{atom_to_binary(Key, utf8), integer_to_binary(Val)} || {Key, Val} <- maps:to_list(Opts1)]), api_call(Bot, <<"getUpdates?", QS/binary>>). %% Generic API methods -spec download_file(bot_name(), json_object()) -> {ok, Headers :: [{binary(), binary()}], Body :: binary()}. download_file(Bot, #{<<"file_id">> := _, <<"file_path">> := FilePath}) -> Endpoint = application:get_env(pe4kin, api_server_endpoint, <<"https://api.telegram.org">>), Token = get_token(Bot), Url = <>, {ok, 200, Headers, BodyRef} = do_api_call(Url, undefined), {ok, Body} = hackney:body(BodyRef), {ok, Headers, Body}. -spec api_call(bot_name(), binary()) -> {ok, json_value()} | {error, Type :: atom(), term()}. api_call(Bot, Method) -> api_call(Bot, Method, undefined). -spec api_call(bot_name(), binary(), api_body()) -> {ok, json_value()} | {error, Type :: atom(), term()}. api_call(Bot, Method, Payload) -> Endpoint = application:get_env(pe4kin, api_server_endpoint, <<"https://api.telegram.org">>), Token = get_token(Bot), api_call({Endpoint, Token}, Bot, Method, Payload). api_call({ApiServerEndpoint, Token}, _Bot, Method, Payload) -> Url = <>, case do_api_call(Url, Payload) of {ok, Code, Hdrs, BodyRef} -> ContentType = hackney_headers:parse(<<"content-type">>, Hdrs), case {hackney:body(BodyRef), ContentType, Code} of {{ok, <<>>}, _, 200} -> ok; {{ok, Body}, {<<"application">>, <<"json">>, _}, _} -> case jiffy:decode(Body, [return_maps]) of #{<<"ok">> := true, <<"result">> := Result} when Code == 200 -> {ok, Result}; #{<<"ok">> := false, <<"description">> := ErrDescription, <<"error_code">> := ErrCode} when Code =/= 200 -> {error, telegram, {ErrCode, Code, ErrDescription}} end; {{error, ErrBody}, _, _} -> {error, hackney_body, ErrBody} end; {error, ErrReason} -> {error, hackney, ErrReason} end. do_api_call(Url, undefined) -> hackney:request(<<"GET">>, Url, [], [], [{pool, ?HACKNEY_POOL}]); do_api_call(Url, {json, Payload}) -> Json = jiffy:encode(Payload), Headers = [{<<"Content-Type">>, <<"application/json">>}, {<<"Accept">>, <<"application/json">>}], hackney:request(<<"POST">>, Url, Headers, Json, [{pool, ?HACKNEY_POOL}]); do_api_call(Url, {query, Payload}) -> Headers = [{<<"Content-Type">>, <<"application/x-www-form-urlencoded; encoding=utf-8">>}, {<<"Accept">>, <<"application/json">>}], hackney:request(<<"POST">>, Url, Headers, {form, maps:to_list(Payload)}, [{pool, ?HACKNEY_POOL}]); do_api_call(Url, {multipart, Payload}) -> Headers = [{<<"Content-Type">>, <<"multipart/form-data">>}, {<<"Accept">>, <<"application/json">>}], hackney:request(<<"POST">>, Url, Headers, {multipart, Payload}, [{pool, ?HACKNEY_POOL}]). body_with_file(FileFields, Payload) -> body_with_file_(FileFields, json, Payload). body_with_file_([Key | Keys] = AllKeys, json, Map) -> case maps:get(Key, Map) of Bin when is_binary(Bin) -> body_with_file_(Keys, json, Map); File when (element(1, File) == file) orelse (element(1, File) == file_path) -> body_with_file_(AllKeys, multipart, maps:to_list(Map)) end; body_with_file_([Key | Keys], multipart, List) -> case lists:keyfind(Key, 1, List) of {Key, Bin} when is_binary(Bin) -> %% file ID body_with_file_(Keys, multipart, List); {Key, File} when is_tuple(File) -> %% `file' or `file_name' tuple body_with_file_(Keys, multipart, lists:keyreplace(Key, 1, List, file2multipart(Key, File))) end; body_with_file_([], multipart, Load) -> ToBin = fun(V) when is_integer(V) -> integer_to_binary(V); (V) when is_atom(V) -> atom_to_binary(V, utf8); (V) -> V end, BinLoad = lists:map(fun({K, V}) -> {ToBin(K), ToBin(V)}; (File) -> File end, Load), {multipart, BinLoad}; body_with_file_([], Enctype, Load) -> %% json {Enctype, Load}. file2multipart(Key, {file, FileName, ContentType, Payload}) -> {atom_to_binary(Key, utf8), Payload, {<<"form-data">>, [{<<"name">>, atom_to_binary(Key, utf8)}, {<<"filename">>, FileName}]}, [{<<"Content-Type">>, ContentType}]}; file2multipart(Key, {file_path, Path}) -> {file, Path, {<<"form-data">>, [{<<"name">>, atom_to_binary(Key, utf8)}, {<<"filename">>, filename:basename(Path)}]}, []}.