├── .gitignore ├── Makefile ├── README.markdown ├── ebin └── twitter_client.app ├── include └── twitter_client.hrl ├── src ├── Makefile ├── twitter_client.erl ├── twitter_client_utils.erl └── twitterbot.erl ├── support └── include.mk └── t └── 001-load.t /.gitignore: -------------------------------------------------------------------------------- 1 | *beam 2 | erlang_twitter* -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LIBDIR=`erl -eval 'io:format("~s~n", [code:lib_dir()])' -s init stop -noshell` 2 | VERSION=0.4.3 3 | 4 | all: 5 | mkdir -p ebin/ 6 | (cd src;$(MAKE)) 7 | 8 | test: all 9 | prove -v t/*.t 10 | 11 | clean: 12 | (cd src;$(MAKE) clean) 13 | rm -rf erl_crash.dump *.beam *.hrl erlang_twitter-$(VERSION).tgz 14 | 15 | package: clean 16 | @mkdir erlang_twitter-$(VERSION)/ && cp -rf include src support t Makefile README.markdown erlang_twitter-$(VERSION) 17 | @COPYFILE_DISABLE=true tar zcf erlang_twitter-$(VERSION).tgz erlang_twitter-$(VERSION) 18 | @rm -rf erlang_twitter-$(VERSION)/ 19 | 20 | install: 21 | for d in ebin include; do mkdir -p $(prefix)/$(LIBDIR)/erlang_twitter-$(VERSION)/$$d ; done 22 | for i in include/*.hrl ebin/*.beam ebin/*.app; do install $$i $(prefix)/$(LIBDIR)/erlang_twitter-$(VERSION)/$$i ; done 23 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | 2 | ## About 3 | 4 | erlang\_twitter is a client library to the Twitter API. Using it is simple: 5 | 6 | 1> inets:start(). 7 | ... 8 | 2> Auth = {"ngerakines", "secretpassword!"}. 9 | 3> twitter_client:status_mentions(Auth, []). 10 | twitter_client:status_mentions({"ngerakines", "secretpassword"}, []). 11 | [{status,"Mon Nov 16 13:07:54 +0000 2009","5764367829", 12 | "@ngerakines Have a safe trip back. Great seeing you & meeting @jacobvorreuter", 13 | "web","false","5763249258","10590","false", 14 | {user,"15592821","Francesco Cesarini","FrancescoC", 15 | ... 16 | 17 | The module layout is relatively simple and self explanatory. Each of the Twitter API methods map directly to a module function. For example, the Twitter API "statuses/friends\_timeline.xml" can be accessed using twitter\_client:status\_friends\_timeline/4. 18 | 19 | Each API method function has the same function parameters. They are a string representing the root API url, the login and password for the account and then a list of API method specific arguments. API methods that do not use certain arguments ignore them. 20 | 21 | The _status_ and _user_ records as defined in twitter\_client.hrl represent statuses and users as returned by API requests. 22 | 23 | ## TODO 24 | 25 | * Add support for search. 26 | * Add support for trends. 27 | * Add support for lists. 28 | * Document existing OAuth support. 29 | * Add support for the streaming API. 30 | 31 | ## Contributions 32 | 33 | * Harish Mallipeddi 34 | * Joshua Miller 35 | -------------------------------------------------------------------------------- /ebin/twitter_client.app: -------------------------------------------------------------------------------- 1 | %%% -*- mode:erlang -*- 2 | {application, erlang_twitter, [ 3 | {description, "An Erlang-native Twitter client."}, 4 | {vsn, "0.5"}, 5 | {modules, [twitter_client, twitter_client_utils]}, 6 | {registered, []}, 7 | {applications, [kernel, stdlib, inets]}, 8 | {env, []} 9 | ]}. 10 | -------------------------------------------------------------------------------- /include/twitter_client.hrl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2008 Nick Gerakines 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person 4 | %% obtaining a copy of this software and associated documentation 5 | %% files (the "Software"), to deal in the Software without 6 | %% restriction, including without limitation the rights to use, 7 | %% copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | %% copies of the Software, and to permit persons to whom the 9 | %% Software is furnished to do so, subject to the following 10 | %% conditions: 11 | %% 12 | %% The above copyright notice and this permission notice shall be 13 | %% included in all copies or substantial portions of the Software. 14 | %% 15 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | %% OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -record(status, {created_at, id, text, source, truncated, in_reply_to_status_id, in_reply_to_user_id, favorited, user}). 25 | -record(message, {created_at, id, text, sender_id, recipient_id, sender_screen_name, recipient_screen_name, sender, recipient}). 26 | -record(user, {id, name, screen_name, location, description, profile_image_url, url, protected, followers_count, status, profile_background_color, profile_text_color, profile_link_color, profile_sidebar_fill_color, profile_sidebar_border_color, friends_count, created_at, favourites_count, utc_offset, time_zone, following, notifications, statuses_count}). 27 | -record(rate_limit, {reset_time, reset_time_in_seconds, remaining_hits, hourly_limit}). 28 | -record(list, {id, name, full_name, slug, description, subscriber_count, member_count, uri, mode, user}). 29 | 30 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | include ../support/include.mk 2 | 3 | all: $(EBIN_FILES) 4 | 5 | debug: 6 | $(MAKE) DEBUG=-DDEBUG 7 | 8 | clean: 9 | rm -rf $(EBIN_FILES) erl_crash.dump 10 | -------------------------------------------------------------------------------- /src/twitter_client.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2008 Nick Gerakines 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person 4 | %% obtaining a copy of this software and associated documentation 5 | %% files (the "Software"), to deal in the Software without 6 | %% restriction, including without limitation the rights to use, 7 | %% copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | %% copies of the Software, and to permit persons to whom the 9 | %% Software is furnished to do so, subject to the following 10 | %% conditions: 11 | %% 12 | %% The above copyright notice and this permission notice shall be 13 | %% included in all copies or substantial portions of the Software. 14 | %% 15 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | %% OTHER DEALINGS IN THE SOFTWARE. 23 | %% 24 | %% @author Nick Gerakines 25 | %% @copyright 2008-2009 Nick Gerakines 26 | %% @version 0.4 27 | %% @doc Provides access to the Twitter web service. Mostly through the 28 | %% clever use of REST-like requests and XML parsing. 29 | %% 30 | %% This module attempts to provide complete access to the Twitter API. In 31 | %% addition, it provides a simple gen_server process template to allow calls 32 | %% to be made on behalf of a named user without having to send a username 33 | %% and password each time. 34 | %% 35 | %% When the gen_server feature is used to make Twitter API calls for a user, 36 | %% a gen_server process is spawned locally and its name is prefixed to 37 | %% prevent named process collision. 38 | %% 39 | %% Make sure you start inets (inets:start().) before you do 40 | %% anything. 41 | %% 42 | %%

Quick start

43 | %%

 44 | %% 1> inets:start().
 45 | %% 2> twitter_client:start("myname", "pass").
 46 | %% 3> twitter_client:account_verify_credentials("myname", "pass", []).
 47 | %%   OR
 48 | %% 3> twitter_client:call("myname", account_verify_credentials).
 49 | %% 4> twitter_client:call("myname", user_timeline).
 50 | %% 5> twitter_client:call("myname", status_update, [{"status", "Testing the erlang_twitter twitter_client.erl library."}]).
 51 | %% 6> twitter_client:call("myname", user_timeline).
 52 | %% 
53 | -module(twitter_client). 54 | 55 | -author("Nick Gerakines "). 56 | -version("0.5"). 57 | 58 | -export([ 59 | status_friends_timeline/2, 60 | status_home_timeline/2, 61 | status_user_timeline/2, 62 | status_mentions/2, 63 | status_show/2, 64 | status_update/2, 65 | status_replies/2, 66 | status_destroy/2, 67 | account_archive/2, collect_account_archive/4, 68 | account_update_location/2, 69 | account_update_delivery_device/2, 70 | account_rate_limit_status/2, 71 | direct_messages/2, collect_direct_messages/4, 72 | direct_new/2, 73 | direct_sent/2, 74 | direct_destroy/2, 75 | favorites/2, collect_favorites/3, 76 | favorites_create/2, 77 | favorites_destroy/2, 78 | friendship_create/2, 79 | friendship_destroy/2, 80 | user_followers/2, collect_user_followers/3, 81 | user_friends/2, collect_user_friends/3, 82 | user_featured/2, 83 | user_show/2, 84 | user_list_memberships/2, 85 | user_list_subscriptions/2, 86 | list/2, 87 | lists/2, 88 | list_statuses/2, 89 | list_members/2, 90 | notification_follow/2, 91 | notification_leave/2, 92 | block_create/2, 93 | block_destroy/2, 94 | help_test/2, 95 | social_graph_follower_ids/2, 96 | social_graph_friend_ids/2, 97 | friendship_exists/2, 98 | account_end_session/2, 99 | account_verify_credentials/2, 100 | headers/2, 101 | parse_status/1, parse_statuses/1, parse_user/1, parse_users/1, request_url/5, 102 | text_or_default/3, 103 | build_url/2 104 | ]). 105 | 106 | -include("twitter_client.hrl"). 107 | -include_lib("xmerl/include/xmerl.hrl"). 108 | 109 | -define(BASE_URL(X), "http://www.twitter.com/" ++ X). 110 | 111 | status_home_timeline(Auth, Args) when is_tuple(Auth), is_list(Args) -> 112 | Url = build_url("statuses/home_timeline.xml", Args), 113 | request_url(get, Url, Auth, [], fun(X) -> parse_statuses(X) end). 114 | 115 | status_friends_timeline(Auth, Args) when is_tuple(Auth), is_list(Args) -> 116 | Url = case lists:keytake("id", 1, Args) of 117 | false -> build_url("statuses/friends_timeline" ++ ".xml", Args); 118 | {_, {"id", Id}, RetArgs} -> build_url("statuses/friends_timeline" ++ "/" ++ Id ++ ".xml", RetArgs) 119 | end, 120 | request_url(get, Url, Auth, [], fun(X) -> parse_statuses(X) end). 121 | 122 | status_user_timeline(Auth, Args) -> 123 | Url = case lists:keytake("id", 1, Args) of 124 | false -> build_url("statuses/user_timeline" ++ ".xml", Args); 125 | {_, {"id", Id}, RetArgs} -> build_url("statuses/user_timeline" ++ "/" ++ Id ++ ".xml", RetArgs) 126 | end, 127 | request_url(get, Url, Auth, [], fun(X) -> parse_statuses(X) end). 128 | 129 | status_mentions(Auth, Args) -> 130 | Url = build_url("statuses/mentions.xml", Args), 131 | request_url(get, Url, Auth, [], fun(X) -> parse_statuses(X) end). 132 | 133 | status_show(Auth, [{"id", Id}]) -> 134 | Url = build_url("statuses/show/" ++ Id ++ ".xml", []), 135 | request_url(get, Url, Auth, [], fun(X) -> parse_status(X) end). 136 | 137 | status_update(Auth, Args) -> 138 | request_url(post, "statuses/update.xml", Auth, Args, fun(X) -> parse_status(X) end). 139 | 140 | status_replies(Auth, Args) -> 141 | Url = build_url("statuses/replies.xml", Args), 142 | request_url(get, Url, Auth, [], fun(X) -> parse_statuses(X) end). 143 | 144 | status_destroy(Auth, [{"id", Id}]) -> 145 | Url = build_url("statuses/destroy/" ++ Id ++ ".xml", []), 146 | request_url(get, Url, Auth, [], fun(X) -> parse_statuses(X) end). 147 | 148 | account_end_session(Auth, _) -> 149 | Url = build_url("account/end_session", []), 150 | request_url(get, Url, Auth, [], fun(_) -> ok end). 151 | 152 | account_archive(Auth, Args) -> 153 | Url = build_url("account/archive.xml", Args), 154 | request_url(get, Url, Auth, [], fun(X) -> parse_statuses(X) end). 155 | 156 | collect_account_archive(Auth, Page, Args, Acc) -> 157 | NArgs = [{"page", integer_to_list(Page)} ] ++ Args, 158 | Messages = twitter_client:account_archive(Auth, NArgs), 159 | %% NKG: Assert that `Messages` is a list? 160 | case length(Messages) of 161 | 80 -> collect_account_archive(Auth, Page + 1, Args, [Messages | Acc]); 162 | 0 -> lists:flatten(Acc); 163 | _ -> lists:flatten([Messages | Acc]) 164 | end. 165 | 166 | account_update_location(Auth, Args) -> 167 | Url = build_url("account/update_location.xml", Args), 168 | request_url(get, Url, Auth, [], fun(X) -> parse_user(X) end). 169 | 170 | account_update_delivery_device(Auth, Args) -> 171 | Url = build_url("account/update_delivery_device.xml", Args), 172 | request_url(get, Url, Auth, [], fun(X) -> parse_user(X) end). 173 | 174 | account_rate_limit_status(Auth, Args) -> 175 | Url = build_url("account/rate_limit_status.xml", Args), 176 | request_url(get, Url, Auth, [], fun(X) -> parse_rate_limit(X) end). 177 | 178 | direct_messages(Auth, Args) -> 179 | Url = build_url("direct_messages.xml", Args), 180 | request_url(get, Url, Auth, [], fun(X) -> parse_messages(X) end). 181 | 182 | collect_direct_messages(Auth, Page, LowID, Acc) -> 183 | Args = [{"page", integer_to_list(Page)}, {"since_id", integer_to_list(LowID)}], 184 | Messages = twitter_client:direct_messages(Auth, Args), 185 | %% NKG: Assert that `Messages` is a list? 186 | case length(Messages) of 187 | 20 -> collect_direct_messages(Auth, Page + 1, LowID, [Messages | Acc]); 188 | 0 -> lists:flatten(Acc); 189 | _ -> lists:flatten([Messages | Acc]) 190 | end. 191 | 192 | direct_new(Auth, Args) -> 193 | request_url(post, "direct_messages/new.xml", Auth, Args, fun(Body) -> parse_message(Body) end). 194 | 195 | direct_sent(Auth, Args) -> 196 | Url = build_url("direct_messages/sent.xml", Args), 197 | request_url(get, Url, Auth, [], fun(Body) -> parse_messages(Body) end). 198 | 199 | direct_destroy(Auth, [{"id", Id}]) -> 200 | Url = build_url("direct_messages/destroy/" ++ Id ++ ".xml", []), 201 | request_url(get, Url, Auth, [], fun(Body) -> parse_status(Body) end). 202 | 203 | favorites(Auth, Args) -> 204 | Url = case lists:keytake("id", 1, Args) of 205 | false -> build_url("favorites" ++ ".xml", Args); 206 | {value, {"id", Id}, RetArgs} -> build_url("favorites" ++ "/" ++ Id ++ ".xml", RetArgs) 207 | end, 208 | request_url(get, Url, Auth, [], fun(Body) -> parse_statuses(Body) end). 209 | 210 | collect_favorites(Auth, Page, Acc) -> 211 | Messages = favorites(Auth, [{"page", integer_to_list(Page)}]), 212 | case length(Messages) of 213 | 20 -> collect_favorites(Auth, Page + 1, [Messages | Acc]); 214 | 0 -> lists:flatten(Acc); 215 | _ -> lists:flatten([Messages | Acc]) 216 | end. 217 | 218 | favorites_create(Auth, [{"id", Id}]) -> 219 | Url = build_url("favorites/create/" ++ Id ++ ".xml", []), 220 | request_url(get, Url, Auth, [], fun(Body) -> parse_status(Body) end). 221 | 222 | favorites_destroy(Auth, [{"id", Id}]) -> 223 | Url = build_url("favorites/destroy/" ++ Id ++ ".xml", []), 224 | request_url(get, Url, Auth, [], fun(Body) -> parse_status(Body) end). 225 | 226 | friendship_exists(Auth, Args) -> 227 | Url = build_url("friendships/exists.xml", Args), 228 | request_url(get, Url, Auth, [], fun(Body) -> Body == "true" end). 229 | 230 | friendship_create(Auth, [{"id", Id}]) -> 231 | Url = "friendships/create/" ++ Id ++ ".xml", 232 | request_url(post, Url, Auth, [], fun(Body) -> parse_user(Body) end). 233 | 234 | friendship_destroy(Auth, [{"id", Id}]) -> 235 | Url = build_url("friendships/destroy/" ++ Id ++ ".xml", []), 236 | request_url(get, Url, Auth, [], fun(Body) -> parse_user(Body) end). 237 | 238 | user_friends(Auth, Args) -> 239 | Url = case lists:keytake("id", 1, Args) of 240 | false -> build_url("statuses/friends" ++ ".xml", Args); 241 | {_, {"id", Id}, RetArgs} -> build_url("statuses/friends" ++ "/" ++ Id ++ ".xml", RetArgs) 242 | end, 243 | request_url(get, Url, Auth, [], fun(Body) -> parse_users(Body) end). 244 | 245 | collect_user_friends(Auth, Page, Acc) -> 246 | Friends = user_friends(Auth, [{"page", integer_to_list(Page)}, {"lite", "true"}]), 247 | case length(Friends) of 248 | 100 -> collect_user_friends(Auth, Page + 1, [Friends | Acc]); 249 | 0 -> lists:flatten(Acc); 250 | _ -> lists:flatten([Friends | Acc]) 251 | end. 252 | 253 | user_followers(Auth, Args) -> 254 | Url = build_url("statuses/followers.xml", Args), 255 | request_url(get, Url, Auth, [], fun(Body) -> parse_users(Body) end). 256 | 257 | collect_user_followers(Auth, Page, Acc) -> 258 | Followers = user_followers(Auth, [{"page", integer_to_list(Page)}, {"lite", "true"}]), 259 | case length(Followers) of 260 | 100 -> collect_user_followers(Auth, Page + 1, [Followers | Acc]); 261 | 0 -> lists:flatten(Acc); 262 | _ -> lists:flatten([Followers | Acc]) 263 | end. 264 | 265 | user_featured(_, _) -> 266 | Url = build_url("statuses/featured.xml", []), 267 | request_url(get, Url, {nil, nil}, [], fun(Body) -> parse_users(Body) end). 268 | 269 | user_show(Auth, Args) -> 270 | Url = case lists:keytake("id", 1, Args) of 271 | false -> build_url("users/show" ++ ".xml", Args); 272 | {value, {"id", Id}, RetArgs} -> build_url("users/show" ++ "/" ++ Id ++ ".xml", RetArgs) 273 | end, 274 | request_url(get, Url, Auth, [], fun(Body) -> parse_user(Body) end). 275 | 276 | user_list_memberships(Auth, Args) -> 277 | Login = case Auth of {X, _} -> X; {X, _, _, _} -> X end, 278 | Url = case lists:keytake("id", 1, Args) of 279 | false -> build_url("/" ++ Login ++ "/lists/memberships.xml", []); 280 | {_, {"id", Id}, RetArgs} -> build_url("/" ++ Id ++ "/lists/memberships.xml", RetArgs) 281 | end, 282 | request_url(get, Url, Auth, [], fun(Body) -> parse_lists(Body) end). 283 | 284 | user_list_subscriptions(Auth, Args) -> 285 | Login = case Auth of {X, _} -> X; {X, _, _, _} -> X end, 286 | Url = case lists:keytake("id", 1, Args) of 287 | false -> build_url("/" ++ Login ++ "/lists/subscriptions.xml", []); 288 | {_, {"id", Id}, RetArgs} -> build_url("/" ++ Id ++ "/lists/subscriptions.xml", RetArgs) 289 | end, 290 | request_url(get, Url, Auth, [], fun(Body) -> parse_lists(Body) end). 291 | 292 | list(Auth, Args) -> 293 | Login = case lists:keytake("id", 1, Args) of 294 | false -> case Auth of {X, _} -> X; {X, _, _, _} -> X end; 295 | {_, {"id", Id}, _} -> Id end, 296 | Url = case lists:keytake("listid", 1, Args) of 297 | {_, {"listid", ListId}, RetArgs} -> build_url("/" ++ Login ++ "/lists/" ++ ListId ++ ".xml", RetArgs) 298 | end, 299 | request_url(get, Url, Auth, [], fun(Body) -> parse_list(Body) end). 300 | 301 | lists(Auth, Args) -> 302 | Login = case Auth of {X, _} -> X; {X, _, _, _} -> X end, 303 | Url = case lists:keytake("id", 1, Args) of 304 | false -> build_url("/" ++ Login ++ "/lists.xml", []); 305 | {_, {"id", Id}, RetArgs} -> build_url("/" ++ Id ++ "/lists.xml", RetArgs) 306 | end, 307 | request_url(get, Url, Auth, [], fun(Body) -> parse_lists(Body) end). 308 | 309 | list_statuses(Auth, Args) -> 310 | Login = case lists:keytake("id", 1, Args) of 311 | false -> case Auth of {X, _} -> X; {X, _, _, _} -> X end; 312 | {_, {"id", Id}, _} -> Id end, 313 | Url = case lists:keytake("listid", 1, Args) of 314 | {_, {"listid", ListId}, RetArgs} -> build_url("/" ++ Login ++ "/lists/" ++ ListId ++ "/statuses.xml", RetArgs) 315 | end, 316 | request_url(get, Url, Auth, [], fun(Body) -> parse_statuses(Body) end). 317 | 318 | list_members(Auth, Args) -> 319 | Login = case lists:keytake("id", 1, Args) of 320 | false -> case Auth of {X, _} -> X; {X, _, _, _} -> X end; 321 | {_, {"id", Id}, _} -> Id end, 322 | Url = case lists:keytake("listid", 1, Args) of 323 | {_, {"listid", ListId}, RetArgs} -> build_url("/" ++ Login ++ "/" ++ ListId ++ "/members.xml", RetArgs) 324 | end, 325 | request_url(get, Url, Auth, [], fun(Body) -> parse_list_users(Body) end). 326 | 327 | notification_follow(Auth, [{"id", Id}]) -> 328 | Url = build_url("notifications/follow/" ++ Id ++ ".xml", []), 329 | request_url(get, Url, Auth, [], fun(Body) -> 330 | case parse_user(Body) of [#user{ screen_name = Id }] -> true; _ -> false end 331 | end). 332 | 333 | notification_leave(Auth, [{"id", Id}]) -> 334 | Url = build_url("notifications/leave/" ++ Id ++ ".xml", []), 335 | request_url(get, Url, Auth, [], fun(Body) -> 336 | case parse_user(Body) of [#user{ screen_name = Id }] -> true; _ -> false end 337 | end). 338 | 339 | block_create(Auth, [{"id", Id}]) -> 340 | Url = build_url("blocks/create/" ++ Id ++ ".xml", []), 341 | request_url(get, Url, Auth, [], fun(Body) -> 342 | case parse_user(Body) of [#user{ screen_name = Id }] -> true; _ -> false end 343 | end). 344 | 345 | block_destroy(Auth, [{"id", Id}]) -> 346 | Url = build_url("blocks/destroy/" ++ Id ++ ".xml", []), 347 | request_url(get, Url, Auth, [], fun(Body) -> 348 | case parse_user(Body) of [#user{ screen_name = Id }] -> true; _ -> false end 349 | end). 350 | 351 | help_test(_, _) -> 352 | Url = build_url("help/test.xml", []), 353 | request_url(get, Url, {nil, nil}, [], fun(Body) -> Body == "true" end). 354 | 355 | social_graph_friend_ids(Auth, _) -> 356 | Login = case Auth of {X, _} -> X; {X, _, _, _} -> X end, 357 | Url = build_url("friends/ids/" ++ twitter_client_utils:url_encode(Login) ++ ".xml", []), 358 | request_url(get, Url, Auth, [], fun(Body) -> parse_ids(Body) end). 359 | 360 | social_graph_follower_ids(Auth, _) -> 361 | Login = case Auth of {X, _} -> X; {X, _, _, _} -> X end, 362 | Url = build_url("followers/ids/" ++ twitter_client_utils:url_encode(Login) ++ ".xml", []), 363 | request_url(get, Url, Auth, [], fun(Body) -> parse_ids(Body) end). 364 | 365 | account_verify_credentials({Login, Password}, _) -> 366 | Url = build_url("account/verify_credentials.xml", []), 367 | case httpc:request(get, {Url, headers(Login, Password)}, [], []) of 368 | {ok, {{_HTTPVersion, 200, _Text}, _Headers, _Body}} -> true; 369 | {ok, {{_HTTPVersion, 401, _Text}, _Headers, _Body}} -> false; 370 | _ -> {error} 371 | end; 372 | account_verify_credentials({Consumer, Token, Secret}, _) -> 373 | Url = build_url("account/verify_credentials.xml", []), 374 | case oauth:get(Url, [], Consumer, Token, Secret) of 375 | {ok, {{_HTTPVersion, 200, _Text}, _Headers, _Body}} -> true; 376 | {ok, {{_HTTPVersion, 401, _Text}, _Headers, _Body}} -> false; 377 | _ -> {error} 378 | end. 379 | 380 | build_url(Url, []) -> Url; 381 | build_url(Url, Args) -> 382 | Url ++ "?" ++ lists:concat( 383 | lists:foldl( 384 | fun (Rec, []) -> [Rec]; (Rec, Ac) -> [Rec, "&" | Ac] end, [], 385 | [K ++ "=" ++ twitter_client_utils:url_encode(V) || {K, V} <- Args] 386 | ) 387 | ). 388 | 389 | request_url(get, Url, {Login, Pass}, _, Fun) -> 390 | case httpc:request(get, {?BASE_URL(Url), headers(Login, Pass)}, [{timeout, 6000}], []) of 391 | {ok, {_, _, Body}} -> Fun(Body); 392 | Other -> {error, Other} 393 | end; 394 | request_url(post, Url, {Login, Pass}, Args, Fun) -> 395 | Body = twitter_client_utils:compose_body(Args), 396 | case httpc:request(post, {?BASE_URL(Url), headers(Login, Pass), "application/x-www-form-urlencoded", Body} , [{timeout, 6000}], []) of 397 | {ok, {_, _, Body2}} -> Fun(Body2); 398 | Other -> {error, Other} 399 | end; 400 | request_url(get, Url, {Consumer, Token, Secret}, Args, Fun) -> 401 | case oauth:get(?BASE_URL(Url), Args, Consumer, Token, Secret) of 402 | {ok, {_, _, "Failed to validate oauth signature or token"}} -> {oauth_error, "Failed to validate oauth signature or token"}; 403 | {ok, {_, _, Body}} -> Fun(Body); 404 | Other -> Other 405 | end; 406 | request_url(post, Url, {Consumer, Token, Secret}, Args, Fun) -> 407 | case oauth:post(?BASE_URL(Url), Args, Consumer, Token, Secret) of 408 | {ok, {_, _, "Failed to validate oauth signature or token"}} -> {oauth_error, "Failed to validate oauth signature or token"}; 409 | {ok, {_, _, Body}} -> Fun(Body); 410 | Other -> Other 411 | end. 412 | 413 | headers(nil, nil) -> [{"User-Agent", "ErlangTwitterClient/0.1"}]; 414 | headers(User, Pass) when is_binary(User) -> 415 | headers(binary_to_list(User), Pass); 416 | headers(User, Pass) when is_binary(Pass) -> 417 | headers(User, binary_to_list(Pass)); 418 | headers(User, Pass) -> 419 | Basic = "Basic " ++ binary_to_list(base64:encode(User ++ ":" ++ Pass)), 420 | [{"User-Agent", "ErlangTwitterClient/0.1"}, {"Authorization", Basic}, {"Host", "twitter.com"}]. 421 | 422 | parse_statuses(Body) -> 423 | parse_generic(Body, fun(Xml) -> 424 | [status_rec(Node) || Node <- lists:flatten([xmerl_xpath:string("/statuses/status", Xml), xmerl_xpath:string("/direct-messages/direct_message", Xml)])] 425 | end). 426 | 427 | parse_ids(Body) -> 428 | parse_generic(Body, fun(Xml) -> 429 | [parse_id(Node) || Node <- xmerl_xpath:string("/ids/id", Xml)] 430 | end). 431 | 432 | parse_status(Body) when is_list(Body) -> 433 | parse_generic(Body, fun(Xml) -> 434 | [status_rec(Node) || Node <- xmerl_xpath:string("/status", Xml)] 435 | end). 436 | 437 | parse_messages(Body) -> 438 | parse_generic(Body, fun(Xml) -> 439 | [message_rec(Node) || Node <- lists:flatten([xmerl_xpath:string("/direct-messages/direct_message", Xml)])] 440 | end). 441 | 442 | parse_message(Body) when is_list(Body) -> 443 | parse_generic(Body, fun(Xml) -> 444 | [message_rec(Node) || Node <- xmerl_xpath:string("/direct_message", Xml)] 445 | end). 446 | 447 | parse_users(Body) -> 448 | parse_generic(Body, fun(Xml) -> 449 | [user_rec(Node) || Node <- xmerl_xpath:string("/users/user", Xml)] 450 | end). 451 | 452 | parse_user(Body) when is_list(Body) -> 453 | parse_generic(Body, fun(Xml) -> [user_rec(Node) || Node <- xmerl_xpath:string("/user", Xml)] end). 454 | 455 | parse_list(Body) -> 456 | parse_generic(Body, fun(Xml) -> [list_rec(Node) || Node <- xmerl_xpath:string("/list", Xml)] end). 457 | 458 | parse_lists(Body) -> 459 | parse_generic(Body, fun(Xml) -> 460 | [list_rec(Node) || Node <- xmerl_xpath:string("/lists_list/lists/list", Xml)] 461 | end). 462 | 463 | parse_list_users(Body) -> 464 | parse_generic(Body, fun(Xml) -> 465 | [user_rec(Node) || Node <- xmerl_xpath:string("/users_list/users/user", Xml)] 466 | end). 467 | 468 | status_rec(Node) when is_tuple(Node) -> 469 | Status = #status{ 470 | created_at = text_or_default(Node, ["/status/created_at/text()", "/direct_message/created_at/text()"], ""), 471 | id = text_or_default(Node, ["/status/id/text()", "/direct_message/id/text()"], ""), 472 | text = text_or_default(Node, ["/status/text/text()", "/direct_message/text/text()"], ""), 473 | source = text_or_default(Node, ["/status/source/text()", "/direct_message/source/text()"], ""), 474 | truncated = text_or_default(Node, ["/status/truncated/text()", "/direct_message/truncated/text()"], ""), 475 | in_reply_to_status_id = text_or_default(Node, ["/status/in_reply_to_status_id/text()", "/direct_message/in_reply_to_status_id/text()"], ""), 476 | in_reply_to_user_id = text_or_default(Node, ["/status/in_reply_to_user_id/text()", "/direct_message/in_reply_to_user_id/text()"], ""), 477 | favorited = text_or_default(Node, ["/status/favorited/text()", "/direct_message/favorited/text()"], "") 478 | }, 479 | case xmerl_xpath:string("/status/user|/direct_message/sender", Node) of 480 | [] -> Status; 481 | [UserNode] -> Status#status{ user = user_rec(UserNode) } 482 | end. 483 | 484 | message_rec(Node) when is_tuple(Node) -> 485 | #message{ 486 | created_at = text_or_default(Node, ["/direct_message/created_at/text()"], ""), 487 | id = text_or_default(Node, ["/direct_message/id/text()"], ""), 488 | text = text_or_default(Node, ["/direct_message/text/text()"], ""), 489 | sender_id = text_or_default(Node, ["/direct_message/sender_id/text()"], ""), 490 | recipient_id = text_or_default(Node, ["/direct_message/recipient_id/text()"], ""), 491 | sender_screen_name = text_or_default(Node, ["/direct_message/sender_screen_name/text()"], ""), 492 | recipient_screen_name = text_or_default(Node, ["/direct_message/recipient_screen_name/text()"], ""), 493 | sender = case xmerl_xpath:string("/direct_message/sender", Node) of 494 | [] -> ""; 495 | [SenderNode] -> user_rec(SenderNode) 496 | end, 497 | recipient = case xmerl_xpath:string("/direct_message/recipient", Node) of 498 | [] -> ""; 499 | [RecipientNode] -> user_rec(RecipientNode) 500 | end 501 | }. 502 | 503 | user_rec(Node) when is_tuple(Node) -> 504 | UserRec = #user{ 505 | id = text_or_default(Node, ["/user/id/text()", "/sender/id/text()"], ""), 506 | name = text_or_default(Node, ["/user/name/text()", "/sender/name/text()"], ""), 507 | screen_name = text_or_default(Node, ["/user/screen_name/text()", "/sender/screen_name/text()"], ""), 508 | location = text_or_default(Node, ["/user/location/text()", "/sender/location/text()"], ""), 509 | description = text_or_default(Node, ["/user/description/text()", "/sender/description/text()"], ""), 510 | profile_image_url = text_or_default(Node, ["/user/profile_image_url/text()", "/sender/profile_image_url/text()"], ""), 511 | url = text_or_default(Node, ["/user/url/text()", "/sender/url/text()"], ""), 512 | protected = text_or_default(Node, ["/user/protected/text()", "/sender/protected/text()"], ""), 513 | followers_count = text_or_default(Node, ["/user/followers_count/text()", "/sender/followers_count/text()"], ""), 514 | profile_background_color = text_or_default(Node, ["/user/profile_background_color/text()"], ""), 515 | profile_text_color = text_or_default(Node, ["/user/profile_text_color/text()"], ""), 516 | profile_link_color = text_or_default(Node, ["/user/profile_link_color/text()"], ""), 517 | profile_sidebar_fill_color = text_or_default(Node, ["/user/profile_sidebar_fill_color/text()"], ""), 518 | profile_sidebar_border_color = text_or_default(Node, ["/user/profile_sidebar_border_color/text()"], ""), 519 | friends_count = text_or_default(Node, ["/user/friends_count/text()"], ""), 520 | created_at = text_or_default(Node, ["/user/created_at/text()"], ""), 521 | favourites_count = text_or_default(Node, ["/user/favourites_count/text()"], ""), 522 | utc_offset = text_or_default(Node, ["/user/utc_offset/text()"], ""), 523 | time_zone = text_or_default(Node, ["/user/time_zone/text()"], ""), 524 | following = text_or_default(Node, ["/user/following/text()"], ""), 525 | notifications = text_or_default(Node, ["/user/notifications/text()"], ""), 526 | statuses_count = text_or_default(Node, ["/user/statuses_count/text()"], "") 527 | }, 528 | case xmerl_xpath:string("/user/status", Node) of 529 | [] -> UserRec; 530 | [StatusNode] -> UserRec#user{ status = status_rec(StatusNode) } 531 | end. 532 | 533 | list_rec(Node) when is_tuple(Node) -> 534 | ListRec = #list{ 535 | id = text_or_default(Node, ["id/text()"], ""), 536 | name = text_or_default(Node, ["name/text()"], ""), 537 | full_name = text_or_default(Node, ["full_name/text()"], ""), 538 | slug = text_or_default(Node, ["slug/text()"], ""), 539 | description = text_or_default(Node, ["description/text()"], ""), 540 | subscriber_count = text_or_default(Node, ["subscriber_count/text()"], ""), 541 | member_count = text_or_default(Node, ["member_count/text()"], ""), 542 | uri = text_or_default(Node, ["uri/text()"], ""), 543 | mode = text_or_default(Node, ["name/text()"], "") 544 | }, 545 | case xmerl_xpath:string("/list/user", Node) of 546 | [] -> ListRec; 547 | [UserNode] -> ListRec#list{ user = user_rec(UserNode) } 548 | end. 549 | 550 | parse_rate_limit(Node) when is_tuple(Node) -> 551 | #rate_limit{ 552 | reset_time = text_or_default(Node, ["/hash/reset-time/text()"], ""), 553 | reset_time_in_seconds = int_or_default(Node, ["/hash/reset-time-in-seconds/text()"], ""), 554 | remaining_hits = int_or_default(Node, ["/hash/remaining-hits/text()"], ""), 555 | hourly_limit = int_or_default(Node, ["/hash/hourly-limit/text()"], "") 556 | }; 557 | 558 | parse_rate_limit(Body) when is_list(Body) -> 559 | parse_generic(Body, fun(Xml) -> [parse_rate_limit(Node) || Node <- xmerl_xpath:string("/hash", Xml)] end). 560 | 561 | parse_generic(Body, Fun) -> 562 | try xmerl_scan:string(Body, [{quiet, true}]) of 563 | Result -> 564 | {Xml, _Rest} = Result, 565 | Fun(Xml) 566 | catch _:_ -> {error, Body} end. 567 | 568 | parse_id(Node) -> 569 | Text = text_or_default(Node, ["/id/text()"], "0"), 570 | twitter_client_utils:string_to_int(Text). 571 | 572 | text_or_default(_, [], Default) -> Default; 573 | text_or_default(Xml, [Xpath | Tail], Default) -> 574 | Res = lists:foldr( 575 | fun (#xmlText{value = Val}, Acc) -> lists:append(Val, Acc); (_, Acc) -> Acc end, 576 | Default, 577 | xmerl_xpath:string(Xpath, Xml) 578 | ), 579 | text_or_default(Xml, Tail, Res). 580 | 581 | int_or_default(_Xml, [], Default) -> Default; 582 | int_or_default(Xml, Xpath, Default) -> 583 | twitter_client_utils:string_to_int(text_or_default(Xml, Xpath, Default)). 584 | 585 | -------------------------------------------------------------------------------- /src/twitter_client_utils.erl: -------------------------------------------------------------------------------- 1 | -module(twitter_client_utils). 2 | 3 | -export([url_encode/1, string_to_int/1, compose_body/1]). 4 | 5 | %% some utility functions stolen from yaws_api module 6 | 7 | url_encode([H|T]) -> 8 | if 9 | H >= $a, $z >= H -> 10 | [H|url_encode(T)]; 11 | H >= $A, $Z >= H -> 12 | [H|url_encode(T)]; 13 | H >= $0, $9 >= H -> 14 | [H|url_encode(T)]; 15 | H == $_; H == $.; H == $-; H == $/; H == $: -> % FIXME: more.. 16 | [H|url_encode(T)]; 17 | true -> 18 | case integer_to_hex(H) of 19 | [X, Y] -> 20 | [$%, X, Y | url_encode(T)]; 21 | [X] -> 22 | [$%, $0, X | url_encode(T)] 23 | end 24 | end; 25 | 26 | url_encode([]) -> []. 27 | 28 | integer_to_hex(I) -> 29 | case catch erlang:integer_to_list(I, 16) of 30 | {'EXIT', _} -> 31 | old_integer_to_hex(I); 32 | Int -> 33 | Int 34 | end. 35 | 36 | old_integer_to_hex(I) when I<10 -> 37 | integer_to_list(I); 38 | old_integer_to_hex(I) when I<16 -> 39 | [I-10+$A]; 40 | old_integer_to_hex(I) when I>=16 -> 41 | N = trunc(I/16), 42 | old_integer_to_hex(N) ++ old_integer_to_hex(I rem 16). 43 | 44 | string_to_int(S) -> 45 | case string:to_integer(S) of 46 | {Int,[]} -> Int; 47 | {error,no_integer} -> null 48 | end. 49 | 50 | compose_body(Args) -> 51 | lists:concat( 52 | lists:foldl( 53 | fun (Rec, []) -> [Rec]; (Rec, Ac) -> [Rec, "&" | Ac] end, 54 | [], 55 | [K ++ "=" ++ twitter_client_utils:url_encode(V) || {K, V} <- Args] 56 | ) 57 | ). 58 | -------------------------------------------------------------------------------- /src/twitterbot.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2008 Nick Gerakines 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person 4 | %% obtaining a copy of this software and associated documentation 5 | %% files (the "Software"), to deal in the Software without 6 | %% restriction, including without limitation the rights to use, 7 | %% copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | %% copies of the Software, and to permit persons to whom the 9 | %% Software is furnished to do so, subject to the following 10 | %% conditions: 11 | %% 12 | %% The above copyright notice and this permission notice shall be 13 | %% included in all copies or substantial portions of the Software. 14 | %% 15 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | %% OTHER DEALINGS IN THE SOFTWARE. 23 | %% @author Nick Gerakines 24 | %% @copyright 2008 Nick Gerakines 25 | %% @version 0.1a 26 | %% @doc A Twitter bot template/example. 27 | %% 28 | %% This module is still in development and should be considered to be in 29 | %% ALPHA stage. For your own sake do not put this into 30 | %% production. 31 | %% 32 | %%

Quick start

33 | %%

 34 | %% 1> inets:start().
 35 | %% 2> twitterbot:start("mybot", "mybotpassword").
 36 | %% 3> gen_server:call(twitterbot:clean_name("mybot"), {info}).
 37 | %% 4> gen_server:call(twitterbot:clean_name("mybot"), {start_dmloop}).
 38 | %% 5> gen_server:call(twitterbot:clean_name("mybot"), {start_flwloop}).
 39 | %% 6> gen_server:call(twitterbot:clean_name("mybot"), {followers}).
 40 | %% 
41 | -module(twitterbot). 42 | -behaviour(gen_server). 43 | 44 | -author("Nick Gerakines "). 45 | -version("0.1a"). 46 | 47 | -compile(export_all). 48 | 49 | -export([ 50 | init/1, terminate/2, code_change/3, 51 | handle_call/3, handle_cast/2, handle_info/2 52 | ]). 53 | 54 | -include("twitter_client.hrl"). 55 | -include_lib("xmerl/include/xmerl.hrl"). 56 | -include_lib("stdlib/include/qlc.hrl"). 57 | 58 | -define(UNIQUEPREFIX,"twitterbot_"). 59 | 60 | -record(state, {login, password, usertable, datatable, dmloop, flwloop, lastcheck, checkinterval}). 61 | -record(bucket, {id, name, date, total, count}). 62 | 63 | clean_name(Name) -> list_to_atom(lists:concat([?UNIQUEPREFIX, Name])). 64 | 65 | start(Login, Password) -> 66 | twitter_client:start(Login, Password), 67 | gen_server:start_link({local, twitterbot:clean_name(Login)}, ?MODULE, [Login, Password], []). 68 | 69 | call(Client, Method) -> 70 | twitter_client:call(Client, Method, []). 71 | 72 | call(Client, Method, Args) -> 73 | gen_server:call(twitterbot:clean_name(Client), {Method, Args}). 74 | 75 | followers_loop(Name) -> 76 | io:format("Checking for new followers.~n", []), 77 | 78 | Followers = twitter_client:call("treasury", collect_user_followers), 79 | [begin 80 | gen_server:call(twitterbot:clean_name(Name), {follower, Follower}, infinity) 81 | end || Follower <- Followers], 82 | 83 | SleepTime = gen_server:call(twitterbot:clean_name(Name), {checkinterval}, infinity), 84 | timer:sleep(SleepTime), 85 | twitterbot:followers_loop(Name). 86 | 87 | direct_message_loop(Name) -> 88 | LowId = gen_server:call(twitterbot:clean_name(Name), {lastcheck}, infinity), 89 | io:format("Checking for new messages: lowid ~p~n", [LowId]), 90 | 91 | NewMessages = twitter_client:call(Name, collect_direct_messages, LowId), 92 | case length(NewMessages) of 93 | 0 -> ok; 94 | _ -> 95 | [LastId | _] = lists:usort([Status#status.id || Status <- NewMessages]), 96 | gen_server:call(twitterbot:clean_name(Name), {lastcheck, list_to_integer(LastId)}, infinity), 97 | twitterbot:process_direct_messages(NewMessages, Name) 98 | end, 99 | 100 | SleepTime = gen_server:call(twitterbot:clean_name(Name), {checkinterval}, infinity), 101 | timer:sleep(SleepTime), 102 | twitterbot:direct_message_loop(Name). 103 | 104 | process_direct_messages([], _) -> ok; 105 | process_direct_messages([Message | Messages], Name) -> 106 | MessageUser = Message#status.user, 107 | twitterbot:parse_direct_message(Name, MessageUser#user.screen_name, Message#status.text), 108 | twitterbot:process_direct_messages(Messages, Name). 109 | 110 | parse_direct_message(BotName, Name, "i " ++ Message) -> 111 | TableName = gen_server:call(twitterbot:clean_name(BotName), {datatable}, infinity), 112 | {Date, _} = calendar:universal_time(), 113 | NewBucket = lists:foldl( 114 | fun ("$" ++ X, OldRec) -> OldRec#bucket{ total = clean_price(X) }; 115 | ([X | Y], OldRec) when X > 47, X < 58 -> 116 | OldRec#bucket{ total = clean_price([X | Y]) }; 117 | ("price:$" ++ X, OldRec) -> OldRec#bucket{ total = clean_price(X) }; 118 | ("price:" ++ X, OldRec) -> OldRec#bucket{ total = clean_price(X) }; 119 | ("bucket:" ++ X, OldRec) -> OldRec#bucket{ name = X }; 120 | ("date:" ++ X, OldRec) -> OldRec#bucket{ date = clean_date(X) }; 121 | (Bucket, OldRec) -> OldRec#bucket{ name = Bucket } 122 | end, 123 | #bucket{ date = Date, count = 0, name = "default"}, 124 | string:tokens(Message, " ") 125 | ), 126 | BucketId = bucket_key(Name, NewBucket#bucket.date), 127 | case dets:lookup(TableName, BucketId) of 128 | [_] -> 129 | dets:update_counter(TableName, BucketId, {4, NewBucket#bucket.total}), 130 | dets:update_counter(TableName, BucketId, {5, 1}), 131 | ok; 132 | _ -> 133 | dets:insert(TableName, [{BucketId, NewBucket#bucket.name, NewBucket#bucket.date, NewBucket#bucket.total, 1}]), 134 | ok 135 | end; 136 | parse_direct_message(_, _, Message) -> 137 | io:format("Direct message (no match): ~p~n", [Message]), 138 | ok. 139 | 140 | bucket_key(Name, {YY, MM, DD}) -> 141 | lists:flatten(io_lib:format("~s-~w-~w-~w",[Name, YY, MM, DD])). 142 | 143 | clean_date(Str) -> 144 | case string:tokens(Str, "/") of 145 | [A, B, C] -> list_to_tuple([ list_to_integer(X) || X <- [A, B, C]]); 146 | _ -> {Date, _} = calendar:universal_time(), Date 147 | end. 148 | 149 | clean_price(Str) when is_integer(Str) -> Str; 150 | clean_price(Str) when is_list(Str) -> 151 | case string:chr(Str, $.) of 152 | 0 -> list_to_integer(Str) + 0.0; 153 | _ -> list_to_float(Str) 154 | end. 155 | 156 | %% - 157 | %% gen_server functions 158 | 159 | %% todo: Add the process to a pg2 pool 160 | init([Login, Password]) -> 161 | {ok, UserTable} = dets:open_file(list_to_atom(Login ++ "_users"), []), 162 | {ok, DataTable} = dets:open_file(list_to_atom(Login ++ "_data"), []), 163 | State = #state{ 164 | login = Login, 165 | password = Password, 166 | usertable = UserTable, 167 | datatable = DataTable, 168 | checkinterval = 60000 * 5, 169 | lastcheck = 5000 170 | }, 171 | {ok, State}. 172 | 173 | handle_call({info}, _From, State) -> {reply, State, State}; 174 | 175 | handle_call({checkinterval}, _From, State) -> {reply, State#state.checkinterval, State}; 176 | 177 | handle_call({checkinterval, X}, _From, State) -> {reply, ok, State#state{ checkinterval = X }}; 178 | 179 | handle_call({datatable}, _From, State) -> {reply, State#state.datatable, State}; 180 | 181 | handle_call({lastcheck}, _From, State) -> {reply, State#state.lastcheck, State}; 182 | 183 | handle_call({lastcheck, X}, _From, State) -> {reply, ok, State#state{ lastcheck = X }}; 184 | 185 | handle_call({follower, User}, _From, State) -> 186 | io:format("Follow: ~p~n", [User]), 187 | dets:insert(State#state.usertable, [{User#user.id, User#user.name, User#user.screen_name}]), 188 | {reply, ok, State}; 189 | 190 | handle_call({followers}, _From, State) -> 191 | Response = dets:foldl(fun(R, Acc) -> [R | Acc] end, [], State#state.usertable), 192 | {reply, Response, State}; 193 | 194 | %% dmloop, flwloop 195 | handle_call({check_dmloop}, _From, State) -> 196 | Response = case erlang:is_pid(State#state.dmloop) of 197 | false -> false; 198 | true -> erlang:is_process_alive(State#state.dmloop) 199 | end, 200 | {reply, Response, State}; 201 | 202 | handle_call({start_dmloop}, _From, State) -> 203 | case erlang:is_pid(State#state.dmloop) of 204 | false -> ok; 205 | true -> 206 | case erlang:is_process_alive(State#state.dmloop) of 207 | false -> ok; 208 | true -> erlang:exit(State#state.dmloop, kill) 209 | end 210 | end, 211 | NewState = State#state{ 212 | dmloop = spawn(twitterbot, direct_message_loop, [State#state.login]) 213 | }, 214 | {reply, ok, NewState}; 215 | 216 | handle_call({check_flwloop}, _From, State) -> 217 | Response = case erlang:is_pid(State#state.flwloop) of 218 | false -> false; 219 | true -> erlang:is_process_alive(State#state.flwloop) 220 | end, 221 | {reply, Response, State}; 222 | 223 | handle_call({start_flwloop}, _From, State) -> 224 | case erlang:is_pid(State#state.flwloop) of 225 | false -> ok; 226 | true -> 227 | case erlang:is_process_alive(State#state.flwloop) of 228 | false -> ok; 229 | true -> erlang:exit(State#state.flwloop, kill) 230 | end 231 | end, 232 | NewState = State#state{ 233 | flwloop = spawn(twitterbot, followers_loop, [State#state.login]) 234 | }, 235 | {reply, ok, NewState}; 236 | 237 | handle_call({stop}, _From, State) -> {stop, normalStop, State}; 238 | 239 | handle_call(stop, _From, State) -> {stop, normalStop, State}; 240 | 241 | handle_call(_, _From, State) -> {noreply, ok, State}. 242 | 243 | handle_cast(_Msg, State) -> {noreply, State}. 244 | 245 | handle_info(_Info, State) -> {noreply, State}. 246 | 247 | terminate(_Reason, _State) -> ok. 248 | 249 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 250 | -------------------------------------------------------------------------------- /support/include.mk: -------------------------------------------------------------------------------- 1 | ## -*- makefile -*- 2 | 3 | ERL := erl 4 | ERLC := $(ERL)c 5 | 6 | INCLUDE_DIRS := ../include $(wildcard ../deps/*/include) 7 | EBIN_DIRS := $(wildcard ../deps/*/ebin) 8 | ERLC_FLAGS := -W $(INCLUDE_DIRS:../%=-I ../%) $(EBIN_DIRS:%=-pa %) 9 | 10 | ifndef no_debug_info 11 | ERLC_FLAGS += +debug_info 12 | endif 13 | 14 | ifdef debug 15 | ERLC_FLAGS += -Ddebug 16 | endif 17 | 18 | EBIN_DIR := ../ebin 19 | DOC_DIR := ../doc 20 | EMULATOR := beam 21 | 22 | ERL_SOURCES := $(wildcard *.erl) 23 | ERL_HEADERS := $(wildcard *.hrl) $(wildcard ../include/*.hrl) 24 | ERL_OBJECTS := $(ERL_SOURCES:%.erl=$(EBIN_DIR)/%.beam) 25 | ERL_OBJECTS_LOCAL := $(ERL_SOURCES:%.erl=./%.$(EMULATOR)) 26 | APP_FILES := $(wildcard *.app) 27 | EBIN_FILES = $(ERL_OBJECTS) $(APP_FILES:%.app=../ebin/%.app) $(ERL_TEMPLATES) 28 | MODULES = $(ERL_SOURCES:%.erl=%) 29 | 30 | ../ebin/%.app: %.app 31 | cp $< $@ 32 | 33 | $(EBIN_DIR)/%.$(EMULATOR): %.erl 34 | $(ERLC) $(ERLC_FLAGS) -o $(EBIN_DIR) $< 35 | 36 | ./%.$(EMULATOR): %.erl 37 | $(ERLC) $(ERLC_FLAGS) -o . $< 38 | -------------------------------------------------------------------------------- /t/001-load.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %% -*- erlang -*- 3 | %%! -pa ./ebin -sasl errlog_type error -boot start_sasl -noshell 4 | 5 | main(_) -> 6 | etap:plan(2), 7 | etap_can:loaded_ok(twitter_client, "module 'twitter_client' loaded"), 8 | etap_can:loaded_ok(twitter_client, "module 'twitter_client_utils' loaded"), 9 | etap:end_tests(). 10 | --------------------------------------------------------------------------------