Wednesday, August 15, 2007

Erlang Picasa API, it works, and here's some reason why...

The Post request to the picasa web service:

request_picasa(AuthToken, User, ImgName, Data) ->
Body = iolist_to_binary(Data),
Authorization = <<"GoogleLogin auth=", AuthToken/binary>>,
Url = "http://picasaweb.google.com/data/feed/api/user/" ++ User ++ "/album/blog",
case http:request(post,
{ Url,
[ { "Authorization", binary_to_list(Authorization) },
{ "Slug", ImgName }
],
"image/jpeg", % Content-type
Body % Body
},
[ {timeout, 30000}, {sync, false} ], % HTTPOptions
[ {body_format, binary} ]) of % Options

{ok, Result} ->
case Result of
{{_, 201, _}, Headers, Response} ->
{ok, Headers, Response};

{_,_,Response} ->
Response
end;

{error, Reason} ->
io:format("Error: ~p~n", [Reason])
end.

And what's you must note is the following:
  • a new url to Post your image.
  • a new header named 'Slug' holding the value of the filename you want for your image
  • a timeout of 30 seconds, letting enough time for the upload process to complete.
Other headers are mandatory, and hopefully already set by 'http:request':
  • content-length holding the size of the binary data of your image
  • content-type holding the image type, here 'image/jpeg'
I choose to use the 'prim_file:load_file/1' fun to retrieve a binary stream of octet composing the image I want to upload:

{ok, File} = prim_file:read_file(Image),
{value, {_, Auth}} = lists:keysearch(picasa, 1, State#state.auth),
case request_picasa(Auth, User, ImgName, File ) of
The variable File holds the binary data, the Auth variable holds the 'correct' Picasa AuthToken, so the 'request_picasa/4' fun can be called correctly...

I'll upgrade later the project http://code.google.com/p/googerl, because I want the 'request_picasa' fun to return {ok, ImageUrl}, and to do that I need to parse the atom response sent by Google... And I'm asking myself wether I doit using xmerl or leex ;]

Tuesday, August 14, 2007

Picasa API for erlang, within the googerl project...

I'm working on the Picasa module for googerl, and was unable to get something else than:

Token Invalid


While scouting my network traffic to really see what I sent:

POST /data/feed/api/user/XXXXXXX/album/blog HTTP/1.1.
content-type: image/jpeg.
content-length: 24425.
te: .
host: picasaweb.google.com.
authorization: GoogleLogin auth=DQAAAIAAAAA5xws0PFfzZEzJ9JsboXhEtKXCdV06ZobagrB61zPWy9lU4j9dOQEK247yR8aQMS83mr3AmPPjkGLTnk3aGUjWsaYmUjmyw20gWrgrZUMcLijbRJUyg5J_t1x45qbglqn7Z07NtIHXudh8GpmnWkTE9T-sghc7AokEiDMzOFmGRg.
connection: keep-alive.
slug: test-v.jpg.
.
......JFIF.....H.H......Exif..MM.*.....................

I realize that my post was correct !
So why this error ?

The answer is in the Picasa Documentation, and it's hard to find but here it is:

Include the relevant parameters in the body of the POST request, as described in the ClientLogin documentation. Use lh2 as the service name.
(extracted from http://code.google.com/apis/picasaweb/gdata.html#Add_Album_Manual_Installed)
I must retrieve a token for the 'lh2' service ! The one I've sent was for the 'blogger' service.

So there's a problem with my current implementation, since my 'google.erl' module is only storing one AuthToken... I'll have to fix this, I'll use a list of AuthToken tuple:

[ {picasa, "authtoken"}, {blogger, "authtoken"} ]

So the code will be heavily modified, and for the time being doesn't work anymore :p
I'll come back soon with good news for everyone !

Saturday, August 11, 2007

Managing lists of binaries or strings...

When you want to deal with binaries, the preserve speed and 'normal' memory footprint there's one fun that you must be aware !

This is 'iolist_to_binary' and it's brother 'iolist_size'.

With theses you can do whatever you need to manager large array of strings, or manage small strings or small binaries... In fact you can do everything.

From my blogger module I need a fun to give me back only binary stuff, for example this fun that just returns a valid 'div' element:

test(Title) ->
[ <<"<div class='title'>">>, iolist_to_binary(Title), <<"</div">> ].

With the 'iolist_to_binary' fun I don't have to deal with guards... ie: I don't need to call 'list_to_binary' if the argument is a list...
Here's some tests:

35> blogger_utils:test("blah"). %this is a string
[<<'"<div class='title'>">>,<<"blah">>,<<"</div">>]

36> blogger_utils:test(<<"blah">>). %this is a binary
[<<"<div class='title'>">>,<<"blah">>,<<"</div">>]

37> blogger_utils:test([<<"blah">>, "bli"]). %this is a list of mixed types
[<<"<div class='title'>">>,<<"blahbli">>,<<"</div">>]

Convenient isn't it ?

Tuesday, August 7, 2007

Blogger_srv.erl version 0.1 is here !

Here's the link to find the 'blogger_srv' you're waiting for: blogger_srv.erl.

This is the first version of the code, I think I'll add other features later:
  • auto reauth whenever the authtoken gets invalid
  • add some support for posting binary data, like images..
  • replace those antislashed double quotes by simple quotes ;)
For the time now, the code works, and all you need to do is starting the server:
blogger_srv:init().

Call the 'auth' fun to obtain a valid 'AuthToken':
blogger_srv:auth("youremail@gmail.com", "yourpass").

Finally call the 'new/4' or 'new/5' fun to post a message:
blogger_srv:new(yourBlogId, Title, [Tags], <<Content>>).
blogger_srv:new(yourBlogId, Title, [Tags], <<Content>>, AuthorName, AuthorEmail).

When the post is successfull, 'new/4' or 'new/5' will returns:
{ok, NewPostId}

That's all for now !

Blogger gen_server, a running session

Here's a screenshots of a sample session using 'blogger_srv'.
You can see some of available funs 'auth', 'snap' and others...

Here's the link of the google project.

Monday, August 6, 2007

Blogger API gen_server !

I'm proud to announce the start of the Blogger API gen_server !

I'll use the code I already wrote for testing the blogger API, and put some OTP requirement around it... For the moment there's three gen_server call:
  • new: to create a new post
  • reset: to reset credentials, i.e. retrieve another AuthToken
  • auth: to retrieve the AuthToken (recomputing it if needed)

Extracted from the 'blogger_srv.erl' file:

auth(Username, Password) ->
gen_server:call(?MODULE, {auth, Username, Password}).

new(BlogId, Title, Tags, Content, {AuthorName, AuthorEmail} ) ->
gen_server:call(?MODULE, {post, BlogId, Title, Tags, Content, AuthorName, AuthorEmail}).

reset() ->
gen_server:cast(?MODULE, reset).


For example, the 'post' fun:

handle_call({post, BlogId, Title, Tags, Content, {AuthorName, AuthorEmail}}, _Node, State) ->
Requests = State#state.requests,
Auth = State#state.auth,
Data = entry_new(Title, {AuthorName, AuthorEmail}, Content, Tags),
case request(Auth, BlogId, Data) of
{ok, PostId} ->
{reply, {ok, PostId}, State#state{ requests = Requests + 1 } };

Msg ->
{reply, {err, Msg}, State#state{ requests = Requests + 1 } }
end;

The final request the post your Atom message.

The final request to post the message in the atom format:

request(AuthToken, BlogId, Data) ->
Body = iolist_to_binary(Data),
io:format("Sending: ~nContent-length: ~p~nBody:~n~s~n", [ size(Body), Body ]),
Authorization = "GoogleLogin auth=" ++ AuthToken,
Url = "http://www.blogger.com/feeds/" ++ BlogId ++ "/posts/default",
case http:request(post, % Method ;)
{
Url, % URL
[ { "Authorization", Authorization } ], % Headers
"application/atom+xml; charset=utf-8", % Content-type
Body % Body
},
[ {timeout, 3000}, {sync, false} ], % HTTPOptions
[ {body_format, binary} ]) of % Options

{ok, Result} ->
case Result of
{{_, 201, _}, Headers, _Response} ->
PostId = get_postid(Headers),
{ok, PostId};

{_,_,Response} ->
Response
end;

{error, Reason} ->
io:format("Error: ~p~n", [Reason])
end.

The thing that's cool overthere is the 'get_postid/1' to retrieve the postId generated by the blogger api... This fun will be called only when the HTTP 'status' code will be "201", which means "created".

I parse response headers and search for 'location' string, and once found I extract the last part of the url:

get_postid([]) ->
"none";
get_postid(Headers) ->
case lists:keysearch("location", 1, Headers) of
{value, {_, Value}} ->
lists:last( string:tokens(Value, "/") );

_ ->
"none"
end.

I've look at the Zend Framework, and found that it parse the entire response body to extract the same value... I think that my method is simpler and works better. That's also why erlang is for smarter people than php (gratuitous troll :p)

More things about the blogger API...

I've reworked many times the atom template I use to post things to blogger, and what I'll describe here is a design I found simple and efficient, so here's the code :

This is the main function:

entry_new(Title, Author, { Content, ContentType }, Tags) when is_list(Tags) ->
NTitle = post_title(Title),
NContent = post_content(Content, ContentType),
NTags = post_tags(Tags),
NAuthor = post_author(Author),
[
entry_header(),
NTitle,
NContent,
NAuthor,
NTags,
entry_footer()
];

The main idea is to use a list of binary, using this simple methods we don't mess with multiple copies or padding or strings management... This is quick and clean.

Here's some little function to create simple tempates:

entry_new(Title, Author, Content, Tags) ->
entry_new(Title, Author, {Content, text}, Tags).

entry_header() ->
<<"<entry xmlns=\"http://www.w3.org/2005/Atom\">">>.

entry_footer() ->
<<"</entry>">>.

post_title(Title) ->
[ <<"<title type=\"text\">">>, list_to_binary(Title), <<"</title>">> ].

post_author({ AuthorName, AuthorEmail }) ->
[
<<"<author><name>">>, list_to_binary(AuthorName), <<"</name><email>">>,
list_to_binary(AuthorEmail), <<"</email></author>">> ];
post_author(AuthorEmail) ->
post_author({ "", AuthorEmail }).



Here's some other function to carefully set the content-type with the post content:

post_content(Content, ContentType) when is_list(Content) ->
post_content(list_to_binary(Content), ContentType);
post_content(Content, html) ->
post_content(Content, "html");
post_content(Content, xhtml) ->
post_content(Content, "xhtml");
post_content(Content, text) ->
post_content(Content, "text");
post_content(Content, ContentType) ->
[ <<"<content type=\"">>, list_to_binary(ContentType), <<"\">">>,
Content,
<<"</content>">> ].


And now the code the post 'tags':

post_tags([]) ->
<<>>;
post_tags(List) ->
lists:map(
fun(X) ->
[ <<"<category scheme='http://www.blogger.com/atom/ns#' term='">>,
list_to_binary(X) ,
<<"'/>">> ]
end,
List).

The magic stands in the 'category' atom tag.

Thursday, August 2, 2007

Blogger API, posting a message (with tags)

Here's some code from the current version:

Data = entry_new("Test no1", {"tonio", "ako@gmail.com"}, <<"TestContent">>, ["erlang", "test"]),
request(Auth, iolist_to_binary(Data)).
  • The Title,
  • The author, a tuple with authorName and authorEmail
  • A binary holding the content of the post
  • A list of tags
I think I'm on the right track...
More news later...

Erlang Blogger API is working !!!

I've finally manage to get it working !
The solution was in the AuthToken from my code, I didn't squeezed the final '\n' character !

Later when the http request was generated, headers were split, making the GFE returning 400 Bad Request.

This is the corrected 'extract_auth/1':

extract_auth(<<"Auth=", Rest/binary>>) ->
Size = size(Rest) - 1,
<<Auth:Size/binary, _/binary>> = Rest,
{ok, Auth};


I squeeze the final character !

Here's a link to the google groups discussion.

Wednesday, August 1, 2007

Blogger API, xml sample

Here's the XML code is used as test:

simple_post(AuthToken) ->
Data =
<<"<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>
<entry xmlns=\"http://www.w3.org/2005/Atom\">
<title type=\"text\">UberKwl</title>
<content type=\"xhtml\">
<div xmlns=\"http://www.w3.org/1999/xhtml\">
<p>test post</p>
</div>
</content>
<author> <name> Gautier </name> <email> gauth@gmail.com </email>
</author>
</entry>">>,
request(AuthToken, Data).


Really, really simple, and this was extracted from a previous post somewhere in the google Blogger API groups...

I've 'xmllint'ed it and of course it was correct...

Sticky