Permalink
Browse files

* Removed my custom pid registry in favor of gproc

* Moved the source code to the apps/ directory for easier releases (I hope)
* Added some tests for twitterlinks_delicious
  • Loading branch information...
1 parent b97df92 commit 01ecd99aa5065e15ecf2daea48c88dd681702401 @ericmoritz committed May 15, 2012
Showing with 1,758 additions and 900 deletions.
  1. +3 −0 Makefile
  2. BIN apps/twitterlinks/.eunit/mochijson.beam
  3. 0 {src → apps/twitterlinks/.eunit}/mochijson.erl
  4. BIN apps/twitterlinks/.eunit/mochinum.beam
  5. 0 {src → apps/twitterlinks/.eunit}/mochinum.erl
  6. BIN apps/twitterlinks/.eunit/twitterlinks.beam
  7. +12 −4 {src → apps/twitterlinks/.eunit}/twitterlinks.erl
  8. BIN apps/twitterlinks/.eunit/twitterlinks_app.beam
  9. +33 −0 apps/twitterlinks/.eunit/twitterlinks_app.erl
  10. BIN apps/twitterlinks/.eunit/twitterlinks_delicious.beam
  11. +216 −0 apps/twitterlinks/.eunit/twitterlinks_delicious.erl
  12. BIN apps/twitterlinks/.eunit/twitterlinks_delicious_sup.beam
  13. 0 {src → apps/twitterlinks/.eunit}/twitterlinks_delicious_sup.erl
  14. BIN apps/twitterlinks/.eunit/twitterlinks_misc.beam
  15. +26 −13 {src → apps/twitterlinks/.eunit}/twitterlinks_misc.erl
  16. BIN apps/twitterlinks/.eunit/twitterlinks_sup.beam
  17. 0 {src → apps/twitterlinks/.eunit}/twitterlinks_sup.erl
  18. BIN apps/twitterlinks/.eunit/twitterlinks_twitter.beam
  19. +12 −13 {src → apps/twitterlinks/.eunit}/twitterlinks_twitter.erl
  20. BIN apps/twitterlinks/.eunit/twitterlinks_twitter_sup.beam
  21. 0 {src → apps/twitterlinks/.eunit}/twitterlinks_twitter_sup.erl
  22. +529 −0 apps/twitterlinks/src/mochijson.erl
  23. +354 −0 apps/twitterlinks/src/mochinum.erl
  24. +1 −0 { → apps/twitterlinks}/src/twitterlinks.app.src
  25. +38 −0 apps/twitterlinks/src/twitterlinks.erl
  26. +33 −0 apps/twitterlinks/src/twitterlinks_app.erl
  27. +218 −0 apps/twitterlinks/src/twitterlinks_delicious.erl
  28. +22 −0 apps/twitterlinks/src/twitterlinks_delicious_sup.erl
  29. +101 −0 apps/twitterlinks/src/twitterlinks_misc.erl
  30. +32 −0 apps/twitterlinks/src/twitterlinks_sup.erl
  31. +92 −0 apps/twitterlinks/src/twitterlinks_twitter.erl
  32. +22 −0 apps/twitterlinks/src/twitterlinks_twitter_sup.erl
  33. BIN rebar
  34. +12 −5 rebar.config
  35. +0 −34 rel/files/erl
  36. +0 −44 rel/files/install_upgrade.escript
  37. +0 −138 rel/files/nodetool
  38. +0 −39 rel/files/start_erl.cmd
  39. +0 −15 rel/files/sys.config
  40. +0 −258 rel/files/twitterlinks
  41. +0 −84 rel/files/twitterlinks.cmd
  42. +0 −19 rel/files/vm.args
  43. +0 −47 rel/reltool.config
  44. +0 −32 src/twitterlinks_app.erl
  45. +0 −111 src/twitterlinks_delicious.erl
  46. +0 −21 src/twitterlinks_delicious_store.erl
  47. +0 −21 src/twitterlinks_twitter_store.erl
  48. +2 −2 test.config
View
@@ -12,3 +12,6 @@ release: compile
clean:
./rebar clean
+
+demo:
+ erl -pa deps/*/ebin apps/*/ebin -boot start_sasl -s twitterlinks -config test
Binary file not shown.
File renamed without changes.
Binary file not shown.
File renamed without changes.
Binary file not shown.
@@ -1,9 +1,17 @@
-module(twitterlinks).
--export([add_account/3, del_account/1, publish_tweet/2]).
+-export([start/0, add_account/3, del_account/1, publish_tweet/3]).
%% public API
+start() ->
+ application:start(crypto),
+ application:start(public_key),
+ application:start(ssl),
+ application:start(inets),
+ application:start(gproc),
+ application:start(twitterlinks).
+
add_account(AccountId, {TwitUser, TwitPass, TwitUID},
{DelUser, DelPass}) ->
twitterlinks_delicious:create(AccountId, DelUser, DelPass),
@@ -15,8 +23,8 @@ del_account(AccountId) ->
twitterlinks_delicious:stop(AccountId),
ok.
-publish_tweet(AccountId, TweetJSONBin) ->
- Links = twitterlinks_misc:translate_tweet(TweetJSONBin),
+publish_tweet(AccountId, UserId, TweetJSONBin) ->
+ Links = twitterlinks_misc:translate_tweet(UserId, TweetJSONBin),
publish_links(AccountId, Links).
%% Internal functions
@@ -27,4 +35,4 @@ publish_links(AccountId, [Link|Rest]) ->
{Url, Description, TagList} = Link,
twitterlinks_delicious:publish_url(AccountId, Url, Description, TagList),
publish_links(AccountId, Rest).
-
+
Binary file not shown.
@@ -0,0 +1,33 @@
+-module(twitterlinks_app).
+
+-behaviour(application).
+
+%% Application callbacks
+-export([start/2, stop/1]).
+
+%% ===================================================================
+%% Application callbacks
+%% ===================================================================
+
+start(_StartType, _StartArgs) ->
+ {ok, Pid} = twitterlinks_sup:start_link(),
+ {ok, Accounts} = application:get_env(twitterlinks, accounts),
+
+ % starts the configured account pairs
+ start_accounts(Accounts),
+
+ {ok, Pid}.
+
+start_accounts(Accounts) ->
+ lists:foreach(fun(Account) ->
+ {AccountId, Props} = Account,
+ TwitterConfig = proplists:get_value(twitter, Props),
+ DeliciousConfig = proplists:get_value(delicious,
+ Props),
+
+ twitterlinks:add_account(AccountId, TwitterConfig,
+ DeliciousConfig)
+ end, Accounts).
+
+stop(_State) ->
+ ok.
Binary file not shown.
@@ -0,0 +1,216 @@
+-module(twitterlinks_delicious).
+
+-behaviour(gen_server).
+
+-define(SERVER, ?MODULE).
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+-endif.
+
+%% ------------------------------------------------------------------
+%% API Function Exports
+%% ------------------------------------------------------------------
+
+-export([start_link/3, create/3, publish_url/4, pid_for/1]).
+
+%% ------------------------------------------------------------------
+%% gen_server Function Exports
+%% ------------------------------------------------------------------
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-record(state, {account_id, username, password}).
+
+%% ------------------------------------------------------------------
+%% API Function Definitions
+%% ------------------------------------------------------------------
+
+start_link(AccountId, Username, Password) ->
+ gen_server:start_link(?MODULE, [AccountId, Username, Password], []).
+
+create(AccountId, Username, Password) ->
+ twitterlinks_delicious_sup:start_child(AccountId, Username, Password).
+
+publish_url(AccountId, Url, Description, TagList) ->
+ case pid_for(AccountId) of
+ {ok, Pid} ->
+ gen_server:call(Pid, {publish_url, {Url, Description, TagList}});
+ {error, not_found} ->
+ % This could happen if the delicous service dies
+ {error, delicious_service_down}
+ end.
+
+pid_for(AccountId) ->
+ case gproc:lookup_pid({n,l, {account_id, AccountId}}) of
+ undefined ->
+ {error, not_found};
+ Pid ->
+ {ok, Pid}
+ end.
+
+%% ------------------------------------------------------------------
+%% gen_server Function Definitions
+%% ------------------------------------------------------------------
+
+init([AccountId, Username, Password]) ->
+ gproc:reg({n, l, {account_id, AccountId}}, self()),
+ {ok, #state{account_id=AccountId, username=Username, password=Password}}.
+
+handle_call({publish_url, {Url, Description, TagList}}, _From, State) ->
+ Request = build_add_request(Url, Description, TagList,
+ State#state.username, State#state.password),
+ Reply = case httpc:request(post, Request, [],[]) of
+ {ok, {{_HTTPVsn, 200, _Reason}, _Headers, _Body}} ->
+ ok;
+ {ok, {{_HTTPVsn, Status, Reason}, _Headers, _Body}} ->
+ {error, {http, Status, Reason}};
+ {error, Reason} -> {error, Reason}
+ end,
+ {reply, Reply, State};
+handle_call(_Msg, _From, State) ->
+ {noreply, State}.
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%% ------------------------------------------------------------------
+%% Internal Function Definitions
+%% ------------------------------------------------------------------
+
+urlencode_list(Params) ->
+ string:join(lists:map(fun({Key, Value}) -> Key ++ "=" ++ http_uri:encode(Value) end,
+ Params), "&").
+
+build_add_request(Url, Description, TagList, Username, Password) ->
+ ServiceUrl = "https://api.del.icio.us/v1/posts/add",
+ Headers = [twitterlinks_misc:http_auth_header(basic, Username, Password)],
+ Body = urlencode_list([{"url", Url},
+ {"description", Description},
+ {"tags", string:join(TagList, ",")}]),
+ {ServiceUrl, Headers, "application/x-www-form-urlencoded", Body}.
+
+
+-ifdef(TEST).
+
+urlencode_list_test() ->
+ Expected = "foo=this%20is%20a%20test&bar=other",
+ Result = urlencode_list([{"foo", "this is a test"},
+ {"bar", "other"}]),
+ ?assertEqual(Expected, Result).
+
+build_add_request_test() ->
+ Expected = {"https://api.del.icio.us/v1/posts/add",
+ [{"Authorization", "Basic ZXJpY21vcml0ejp0ZXN0"}],
+ "application/x-www-form-urlencoded",
+ "url=http%3A%2F%2Fexample.com&description=this%20is%20the%20description&tags=foo%2Cbar"},
+
+ Result = build_add_request("http://example.com",
+ "this is the description",
+ ["foo", "bar"], "ericmoritz", "test"),
+ ?assertEqual(Expected, Result).
+
+init_test_() ->
+ {setup,
+ fun() -> application:start(gproc) end,
+ fun(_Arg) -> application:stop(gproc) end,
+ ?_test(begin
+ {ok, Result} = init([ericmoritz, username, password]),
+
+ Expected = #state{account_id=ericmoritz,
+ username=username, password=password},
+
+ ?assertEqual(Expected, Result),
+
+ ?assertEqual(gproc:get_value({n, l, {account_id, ericmoritz}}),
+ self()),
+ ok end)}.
+
+handle_publish_url_200_test_() ->
+ {setup,
+ fun() -> meck:new(httpc),
+ meck:expect(httpc, request, fun(post, _Request, [], []) ->
+ {ok, {{'_HTTPVsn',
+ 200,
+ '_Reason'},
+ '_Headers',
+ '_Body'}}
+ end) end,
+ fun(_Arg) -> meck:unload(httpc) end,
+ ?_test(begin
+ State = #state{username="username",
+ password="password"},
+ UrlTuple = {"http://example.com",
+ "this is the description", ["foo", "bar"]},
+
+ ?assertEqual({reply, ok, State},
+ handle_call({publish_url, UrlTuple},
+ self(), State)),
+ ok
+ end)
+ }.
+
+handle_publish_url_non200_test_() ->
+ {setup,
+ fun() -> meck:new(httpc),
+ meck:expect(httpc, request, fun(post, _Request, [], []) ->
+ {ok, {{'_HTTPVsn',
+ 404,
+ "Not Found"},
+ '_Headers',
+ '_Body'}}
+ end) end,
+ fun(_Arg) -> meck:unload(httpc) end,
+ ?_test(begin
+ State = #state{username="username",
+ password="password"},
+ UrlTuple = {"http://example.com",
+ "this is the description", ["foo", "bar"]},
+
+ ?assertEqual({reply, {error, {http, 404, "Not Found"}}, State},
+ handle_call({publish_url, UrlTuple},
+ self(), State)),
+ ok
+ end)
+ }.
+
+handle_publish_url_httpc_error_test_() ->
+ {setup,
+ fun() -> meck:new(httpc),
+ meck:expect(httpc, request, fun(post, _Request, [], []) ->
+ {error, reason}
+ end) end,
+ fun(_Arg) -> meck:unload(httpc) end,
+ ?_test(begin
+ State = #state{username="username",
+ password="password"},
+ UrlTuple = {"http://example.com",
+ "this is the description", ["foo", "bar"]},
+
+ ?assertEqual({reply, {error, reason}, State},
+ handle_call({publish_url, UrlTuple},
+ self(), State)),
+ ok
+ end)
+ }.
+
+pid_for_test_() ->
+ {setup,
+ fun() -> application:start(gproc) end,
+ fun(_) -> application:stop(gproc) end,
+ ?_test(begin
+ {ok, Pid} = start_link(account_id, "username", "password"),
+ {ok, Pid} = pid_for(account_id)
+ end)}.
+
+-endif.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,5 @@
-module(twitterlinks_misc).
--export([http_auth_header/3, translate_tweet/1]).
+-export([http_auth_header/3, translate_tweet/2]).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
@@ -11,15 +11,23 @@ http_auth_header(basic, Username, Password) ->
Encoded = base64:encode_to_string(lists:append([Username, ":", Password])),
{"Authorization", "Basic " ++ Encoded}.
-translate_tweet(TweetBin) when is_binary(TweetBin) ->
- translate_tweet(mochijson:decode(TweetBin));
-translate_tweet(Tweet) ->
- Description = struct:get_value("text", Tweet),
- Tags = tweet_to_taglist(Tweet),
- Links = tweet_to_linklist(Tweet),
-
- [{Url, Description, Tags} || Url <- Links].
-
+translate_tweet(UserId, TweetBin) when is_binary(TweetBin) ->
+ translate_tweet(UserId, mochijson:decode(TweetBin));
+translate_tweet(UserId, Tweet) ->
+ case struct:get_value({"user", "id"}, Tweet) of
+ UserId ->
+ % If the tweet's author id matches the user id we are
+ % listening too, process the tweet for links
+ Description = struct:get_value("text", Tweet),
+ Tags = tweet_to_taglist(Tweet),
+ Links = tweet_to_linklist(Tweet),
+
+ [{Url, Description, Tags} || Url <- Links];
+ _ ->
+ % Ignore links tweeted by anyone but the User we're
+ % listening to
+ []
+ end.
%% Internal functions
@@ -44,6 +52,7 @@ tweet_to_linklist(Tweet) ->
-define(FIXTURE, <<"{
\"text\": \"https://t.co/ZkG6Whj0 #json #erlang Jiffy JSON encoder, decoder.\",
+ \"user\": {\"id\": 12345},
\"entities\": {
\"user_mentions\": [],
\"hashtags\": [
@@ -81,8 +90,12 @@ translate_tweet_test() ->
"https://t.co/ZkG6Whj0 #json #erlang Jiffy JSON encoder, decoder.",
["json", "erlang"]}],
- Tweet = parse_tweet(?FIXTURE),
- Result = translate_tweet(Tweet),
- ?assertEqual(Expected, Result).
+ Result = translate_tweet(12345, ?FIXTURE),
+ ?assertEqual(Expected, Result),
+
+ Result2 = translate_tweet(12346, ?FIXTURE),
+ ?assertEqual([], Result2),
+
+ ok.
-endif.
Binary file not shown.
Binary file not shown.
Oops, something went wrong.

0 comments on commit 01ecd99

Please sign in to comment.