Showing posts with label blogger. Show all posts
Showing posts with label blogger. Show all posts

Tuesday, July 15, 2008

From Googerl to googleatom_app

Hi, (a quick post)
I've been busy this weekend working on a more generic layer for google API. Here comes the 'googleatom_app'.

I've uploaded it in the googerl project.
The application needs 'crypto, ssl, inets' to work.
[ application:start(X) || X <- [crypto, ssl, inets, googleatom] ]
should start the application.
There's currently not much documentation, this is still at a very early stage. Here's a list of what you can do now:

  • Post an article into blogger
  • Post a photo into picasa, taken from a file or from a URL (inets download the image and send it to picasa)


Once the application has started, you'll see two new registered processes 'blogger_srv' and 'picasa_srv'. The first thing you need to do before being able to use thoses process is setting your google credentials:
picasa_srv:auth(Username, Password).
Once the call returns you'll receive a google token string. If not you'll have to investigate a little :).
Ok now that you're authenticated you can post a nice photo into your gallery:
picasa_srv:new(Username, Album, "http://www.aniceimageservier.com/superimage.jpg", "Fake Image Name").

Since the process of posting images takes time, picasa_srv uses 'gen_server:cast', and now the success is indicated by some 'error_logger:info_msg' ... (xmerl is used to extract various new urls of your posted photo).

That's it for this really quick introduction, I'll will update files within days, actually some funs are not available everywhere, I'll fix this.

I'll post a googleatom_app tutorial later this week...

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...

Tuesday, July 31, 2007

Connecting Erlang to Blogger (Part 2) - Adding an entry

For this second part, we start where we left the last time.
We were able to read the response of a succesful login, data was three lines of key value pairs.
The last line holds the final 'AuthToken' we need to send to the blogger atom post service...

So here's the code to extract the line that begins with the 'Auth' keyword and store the value after the '=' and before the end of line:

extract_auth(<<>>) ->
{error, not_found};
extract_auth(<<"Error=", Rest/binary>>) ->
Size = size(Rest) - 1,
<<Msg:Size/binary, _/binary>> = Rest,
{error, binary_to_list(Msg)};

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

Note that we are also able to read 'Error' lines, those lines are sent in case of an failed login attempt...
I can describe what 'extract_auth/1' do like this:
  1. if binary in empty returns {error, not_found}
  2. if binary begins with 'Error' catch it and returns its content with the tuple {error, Msg}
  3. if binary begins with the 'Auth' keyword extract everything till the end of the binary
  4. in any other case extract one character and parse the rest of the binary


To conclude, upon successful login this fun will return:

{ok, "AAAAAQAARAFA..."}

This is, of course our Blogger AuthToken...

Now how can we use it ? This is Simple !
Open a new file named blogger.erl and write something like this:

-module(blogger).
-export([ new/3, post/3, test/2, template/3 ]).

post(AuthToken, Title, Content) ->
Data = template(Title, Content),
request(AuthToken, iolist_to_binary(Data)).

template(Title, Content) when is_list(Content) ->
template(Title, Content, {"none", "none"}).

template(Title, Content, Author) when is_list(Content) ->
template(Title, list_to_binary(Content), Author);

template(Title, Content, Author) ->
{AuthorName, AuthorEmail} = Author,
[ <<"<entry xmlns=\"http://www.w3.org/2005/Atom\">\n<title type=\"text\">\n">>,
list_to_binary(Title),
% <<"</title><content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>">>,
<<"</title>\n<content type=\"text\">">>,
Content,
<<"</content>\n<author><name>">>,
list_to_binary(AuthorName),
<<"</name>">>,
list_to_binary(AuthorEmail),
<<"<
</author>\n</entry>\n">> ].

request(AuthToken, Data) when is_binary(AuthToken) ->
request(binary_to_list(AuthToken), Data);

request(AuthToken, Data) ->
io:format("Sending: ~nContent-length: ~p~nBody:~n~s~n", [ size(Data), Data ]),
Authorization = "GoogleLogin auth=" ++ AuthToken,
Url = "http://www.blogger.com/feeds/199963XXXX081936700/posts/default", % Put your BlogID, this one is invalid
case http:request(post,
{ Url,
[ { "Authorization", Authorization } ],
"application/atom+xml; charset=utf-8", Data},
[ {timeout, 3000}, {sync, false} ],
[ {body_format, binary} ]) of

{ok, Result} ->
%io:format("Received: ~p~n", [Result]),
{_,_,Body} = Result,
Body;

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


Once you're done, you'll be pleased to found that this doesn't work :/.
Yep, I wasn't able to post anything !
May be I've missed something in the documentation, but all I get is an nice SAXexception...

Testing my Erlang Blogger API ...

I'm stuck on this error message !

<<"org.xml.sax.SAXParseException: Content is not allowed in prolog.">>


I'm simply using this code:

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

{ok, Result} ->
%io:format("Received: ~p~n", [Result]),
{_,_,Body} = Result,
Body;

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



And the resulting Body is always the error:

<<"org.xml.sax.SAXParseException: Content is not allowed in prolog.">>


Help Meeee !

Monday, July 30, 2007

Connecting Erlang to Blogger (Part 1) - Auth with ClientLogin

With the Gdata API from google you can connect your application to some nice services... Calendar, Blogger etc.
Since this is completly REST based you can of course use your 'http:request' to connect and exploit those services. Let's begin with the ClientLogin process.

For this article we will focus on the Blogger API, the main purpose is of course create an Erlang client for Blogger :)

Connecting to google is as simple as sending something like this:

accountType=HOSTED_OR_GOOGLE&Email=YOURGOOGLEACOUNT&Passwd=YOURPASSWORD&source=SelfCo-TestApp-1&service=blogger


Now we can do it in Erlang too !. First we need to build the query string, second we need to send it to the ClientLogin service using 'http:request'.


auth(Username, Password, Application) ->
Sep = <<"&">>,
Post = [
<<"accountType=HOSTED_OR_GOOGLE&">>,
<<"Email=">>, list_to_binary(Username), Sep,
<<"Passwd=">>, list_to_binary(Password), Sep,
<<"source=">>, list_to_binary(Application), Sep,
<<"service=blogger">> ],
request(erlang:iolist_to_binary(Post)).



The fun 'erlang:iolist_to_binary/1' transforms the list of binaries to a simple binary, this is not really necessary but this will ease yourself later for debugging...

Now we can send this query string to the google ClientLogin process:

request(Data) ->
case http:request(post,
{"https://www.google.com/accounts/ClientLogin", [],
"application/x-www-form-urlencoded", Data},
[ {timeout, 3000} ], [{stream, "/tmp/google.test"}, {body_format, binary}]) of

{ok, saved_to_file} ->
io:format("Saved to file~n");

{ok, Result} ->
io:format("Received: ~p~n", [Result]);

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


  • This is a POST query
  • The service is https://www.google.com/accounts/ClientLogin
  • The content-type is application/x-www-form-urlencoded
  • We sets the timeout to 3 seconds
  • We store the result (if successful to '/tmp/google.test')


Let's try this code:

65> google:auth("test@gmail.com", "secretcode").
Received: {{"HTTP/1.1",403,"Forbidden"},
[{"cache-control","no-cache"},
{"date","Sun, 29 Jul 2007 20:44:20 GMT"},
{"pragma","no-cache"},
{"server","GFE/1.3"},
{"content-length","24"},
{"content-type","text/plain"}],
<<"Error=BadAuthentication\n">>}
ok

The connection fails, so let's try with a valid user account:

70> google:auth("validaccount@gmail.com", "validpassword").
Saved to file
ok

Success !

The content of '/tmp/google.test':

SID=DQAAAG8AAACuATb7YJxMdqQhp0LIf546SWLfDNfTlANffRc0B6OGbTat4Ebdj89s6hVEzfNZRL...
LSID=DQAAAHEAAAAG1iqBgOrgzrY5cdgpBv9y42HxkvjNuUaYKImw6yH7xh0GtL5EG19C9GkGdPEb1...
Auth=DQAAAHAAAAAG1iqBgOrgzrY5cdgpBv9y42HxkvjNuUaYKImw6yH7xh0GtL5EG19C9GkGdPEb1...


The final token we need is the 'Auth=' one, this string will be passed with every new query as an 'Authorization' header:

Authorization: GoogleLogin auth=DQAAAHAAAA...


Next Time in Part 2, I'll show you how we'll use this AuthToken and how we will be able to post a message to our blog !

Sticky