PageRenderTime 40ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/src/clickatell.erl

http://erl-clickatell.googlecode.com/
Erlang | 249 lines | 202 code | 39 blank | 8 comment | 0 complexity | 631cda85feb4337b2694af9bb8723dc5 MD5 | raw file
  1. -module(clickatell).
  2. -behaviour(gen_server).
  3. -include("../include/clickatell.hrl").
  4. -export([start_link/3,
  5. stop/0]).
  6. -export([balance/0,
  7. check/1,
  8. cost/1,
  9. reply/2,
  10. send/1,
  11. status/1]).
  12. -export([init/1,
  13. handle_call/3,
  14. handle_cast/2,
  15. handle_info/2,
  16. terminate/2,
  17. code_change/3]).
  18. -export([arg_to_sms/1,
  19. handle/2]).
  20. -record(state, {session_id, callback_ets}).
  21. -define(URL, "https://api.clickatell.com").
  22. -define(TIMEOUT, timer:seconds(15)).
  23. -define(PING_WAIT, timer:minutes(5)).
  24. %% Starting
  25. start_link(User, Pass, API) ->
  26. gen_server:start_link({local, ?MODULE}, ?MODULE, [User, Pass, API], []).
  27. stop() ->
  28. gen_server:cast(?MODULE, stop).
  29. %% Interface
  30. balance() ->
  31. gen_server:call(?MODULE, {balance}, ?TIMEOUT).
  32. check(To) ->
  33. gen_server:call(?MODULE, {check, To}, ?TIMEOUT).
  34. cost(MessageID) ->
  35. gen_server:call(?MODULE, {cost, MessageID}, ?TIMEOUT).
  36. send(SMS) ->
  37. gen_server:call(?MODULE, {send, SMS}, ?TIMEOUT).
  38. reply(#sms{to = To, from = From}, Text) ->
  39. send(#sms{to = From, from = To, text = Text}).
  40. status(MessageID) ->
  41. gen_server:call(?MODULE, {status, MessageID}, ?TIMEOUT).
  42. %% Server
  43. init([User, Pass, API]) ->
  44. process_flag(trap_exit, true),
  45. case login(User, Pass, API) of
  46. {ok, SessionID} ->
  47. spawn_link(fun() -> ping_loop(?PING_WAIT, SessionID) end),
  48. {ok, #state{session_id = SessionID, callback_ets = ets:new(callbacks, [])}};
  49. {error, Error} ->
  50. {stop, Error}
  51. end.
  52. handle_call({balance}, From, State) ->
  53. {ok, RequestID, Callback} = call_balance(State#state.session_id),
  54. ets:insert(State#state.callback_ets, {RequestID, From, Callback}),
  55. {noreply, State};
  56. handle_call({check, To}, From, State) ->
  57. {ok, RequestID, Callback} = call_check(To, State#state.session_id),
  58. ets:insert(State#state.callback_ets, {RequestID, From, Callback}),
  59. {noreply, State};
  60. handle_call({cost, MessageID}, From, State) ->
  61. {ok, RequestID, Callback} = call_cost(MessageID, State#state.session_id),
  62. ets:insert(State#state.callback_ets, {RequestID, From, Callback}),
  63. {noreply, State};
  64. handle_call({send, SMS}, From, State) ->
  65. {ok, RequestID, Callback} = call_send(SMS, State#state.session_id),
  66. ets:insert(State#state.callback_ets, {RequestID, From, Callback}),
  67. {noreply, State};
  68. handle_call({status, MessageID}, From, State) ->
  69. {ok, RequestID, Callback} = call_status(MessageID, State#state.session_id),
  70. ets:insert(State#state.callback_ets, {RequestID, From, Callback}),
  71. {noreply, State}.
  72. handle_cast(stop, State) ->
  73. {stop, normal, State}.
  74. handle_info({http, {RequestID, {error, Error}}}, State) ->
  75. {stop, {http_error, Error, RequestID}, State};
  76. handle_info({http, {RequestID, HTTPResponse}}, State) ->
  77. [{_, From, Callback}] = ets:lookup(State#state.callback_ets, RequestID),
  78. gen_server:reply(From, apply(Callback, [HTTPResponse])),
  79. ets:delete(State#state.callback_ets, RequestID),
  80. {noreply, State};
  81. handle_info({'EXIT', _Pid, {ping_error, Error}}, State) ->
  82. {stop, {ping_error, Error}, State}.
  83. terminate({ping_error, Error}, State) ->
  84. error_logger:error_msg("Failed to ping ~s because ~p", [State#state.session_id, Error]),
  85. ok;
  86. terminate(_Reason, _State) ->
  87. ok.
  88. code_change(_OldVsn, State, _Extra) ->
  89. {ok, State}.
  90. %% Sessions
  91. login(User, Pass, API) ->
  92. {ok, HTTPResponse} = call("/http/auth", [{user, User}, {password, Pass}, {api_id, API}]),
  93. case parse_response(HTTPResponse) of
  94. {ok, PropList} -> {ok, proplists:get_value(ok, PropList)};
  95. {error, Error} -> {stop, Error}
  96. end.
  97. ping_loop(Time, SessionID) ->
  98. error_logger:info_msg("Pinging ~s...~n", [SessionID]),
  99. {ok, HTTPResponse} = call("/http/ping", [{session_id, SessionID}]),
  100. case parse_response(HTTPResponse) of
  101. {ok, _} -> timer:sleep(Time), ping_loop(Time, SessionID);
  102. {error, Error} -> exit({ping_error, Error})
  103. end.
  104. %% Commands
  105. call_balance(SessionID) ->
  106. callback("/http/getbalance", [{session_id, SessionID}], fun handle_balance/1).
  107. handle_balance(HTTPResponse) ->
  108. case parse_response(HTTPResponse) of
  109. {ok, PropList} -> list_to_float(proplists:get_value(credit, PropList));
  110. {error, Error} -> {error, Error}
  111. end.
  112. call_check(To, SessionID) ->
  113. callback("/utils/routeCoverage.php", [{msisdn, To}, {session_id, SessionID}], fun handle_check/1).
  114. handle_check(HTTPResponse) ->
  115. case parse_response(HTTPResponse) of
  116. {ok, _} -> true;
  117. {error, _} -> false
  118. end.
  119. call_cost(MessageID, SessionID) ->
  120. callback("/http/getmsgcharge", [{apimsgid, MessageID}, {session_id, SessionID}], fun handle_cost/1).
  121. handle_cost(HTTPResponse) ->
  122. case parse_response(HTTPResponse) of
  123. {ok, PropList} -> str_to_number(proplists:get_value(charge, PropList));
  124. {error, Error} -> {error, Error}
  125. end.
  126. call_send(#sms{to = To, from = From, text = Text, options = Opts}, SessionID) ->
  127. callback("/http/sendmsg", [{to, To}, {from, From}, {text, Text}, {session_id, SessionID}] ++ Opts, fun handle_send/1).
  128. handle_send(HTTPResponse) ->
  129. case parse_response(HTTPResponse) of
  130. {ok, PropList} -> proplists:get_value(id, PropList);
  131. {error, Error} -> {error, Error}
  132. end.
  133. call_status(MessageID, SessionID) ->
  134. callback("/http/querymsg", [{apimsgid, MessageID}, {session_id, SessionID}], fun handle_status/1).
  135. handle_status(HTTPResponse) ->
  136. case parse_response(HTTPResponse) of
  137. {ok, PropList} -> str_to_number(proplists:get_value(status, PropList));
  138. {error, Error} -> {error, Error}
  139. end.
  140. %% Sending
  141. call(Path, PropList) ->
  142. call(Path, PropList, true).
  143. call(Path, PropList, Sync) ->
  144. URL = ?URL ++ Path,
  145. Headers = [{"User-Agent", "erl-clickatell"}],
  146. Payload = proplist_to_params(PropList),
  147. ContentType = "application/x-www-form-urlencoded",
  148. HTTPOptions = [],
  149. Options = [{sync, Sync}, {body_format, binary}],
  150. http:request(post, {URL, Headers, ContentType, Payload}, HTTPOptions, Options).
  151. callback(Path, PropList, Callback) when is_function(Callback) ->
  152. {ok, RequestID} = call(Path, PropList, false),
  153. {ok, RequestID, Callback}.
  154. %% Receiving
  155. arg_to_sms(Arg) ->
  156. PropList = arg_to_proplist(Arg),
  157. To = list_to_integer(proplists:get_value("to", PropList)),
  158. From = list_to_integer(proplists:get_value("from", PropList)),
  159. Text = proplists:get_value("text", PropList),
  160. {ok, #sms{to = To, from = From, text = Text, options = PropList}}.
  161. handle(Arg, {M, F}) ->
  162. case arg_to_sms(Arg) of
  163. {ok, SMS} -> M:F(SMS);
  164. {error, Error} -> M:F({error, Error})
  165. end,
  166. ok.
  167. %% Utils
  168. str_to_number(Str) ->
  169. try list_to_integer(Str)
  170. catch error:_ -> list_to_float(Str)
  171. end.
  172. proplist_to_params(PropList) ->
  173. join("&", lists:map(fun({Key, Val}) ->
  174. yaws_api:url_encode(yaws:to_string(Key)) ++ "=" ++ yaws_api:url_encode(yaws:to_string(Val))
  175. end, PropList)).
  176. arg_to_proplist(Arg) ->
  177. lists:map(fun({Key,Val}) -> {list_to_atom(Key), Val} end, yaws_api:parse_query(Arg)).
  178. parse_response(HTTPResponse) ->
  179. case HTTPResponse of
  180. {{_, 200, _}, _, ResponseBinary} ->
  181. Str = binary_to_list(ResponseBinary),
  182. {match, Matches} = regexp:matches(Str, "[A-Za-z]+:"),
  183. PropList = parse_response_loop(Str, Matches, []),
  184. case proplists:get_value(err, PropList) of
  185. undefined -> {ok, PropList};
  186. Error -> {error, Error}
  187. end;
  188. {{_, Error, _}, _, _} ->
  189. {error, Error}
  190. end.
  191. parse_response_loop(Str, Matches, PropList) ->
  192. case Matches of
  193. [{NextPt, PtLen}] ->
  194. lists:reverse([{parse_left (string:substr(Str, NextPt, PtLen)),
  195. parse_right(string:substr(Str, NextPt + PtLen))}
  196. | PropList]);
  197. [{NextPt1, PtLen1}, {NextPt2, PtLen2} | RemMatches] ->
  198. parse_response_loop(Str,
  199. [{NextPt2, PtLen2} | RemMatches],
  200. [{parse_left (string:substr(Str, NextPt1, PtLen1)),
  201. parse_right(string:substr(Str, NextPt1 + PtLen1,
  202. NextPt2 - (NextPt1 + PtLen1)))}
  203. | PropList])
  204. end.
  205. parse_left(Str) ->
  206. list_to_atom(string:strip(http_util:to_lower(lists:delete($:, Str)))).
  207. parse_right(Str) ->
  208. string:strip(Str).
  209. join(Sep, List) ->
  210. lists:foldl(fun(A, "") -> A; (A, Acc) -> Acc ++ Sep ++ A end, "", List).