/* Copyright 2012 BJA Electronics * Author: Jeroen Arnoldus (b.j.arnoldus@bja-electronics.nl) * * This software is licensed under the GNU Lesser General Public License * (version 2.1 or later). See the COPYING file in this distribution. */ public class TumblrService : Object, Spit.Pluggable, Spit.Publishing.Service { private const string ICON_FILENAME = "tumblr.png"; private static Gdk.Pixbuf[] icon_pixbuf_set = null; public TumblrService(GLib.File resource_directory) { if (icon_pixbuf_set == null) icon_pixbuf_set = Resources.load_from_resource(Resources.RESOURCE_PATH + "/" + ICON_FILENAME); } public int get_pluggable_interface(int min_host_interface, int max_host_interface) { return Spit.negotiate_interfaces(min_host_interface, max_host_interface, Spit.Publishing.CURRENT_INTERFACE); } public unowned string get_id() { return "org.yorba.shotwell.publishing.tumblr"; } public unowned string get_pluggable_name() { return "Tumblr"; } public void get_info(ref Spit.PluggableInfo info) { info.authors = "Jeroen Arnoldus"; info.copyright = _("Copyright 2012 BJA Electronics"); info.translators = Resources.TRANSLATORS; info.version = _VERSION; info.website_name = Resources.WEBSITE_NAME; info.website_url = Resources.WEBSITE_URL; info.is_license_wordwrapped = false; info.license = Resources.LICENSE; info.icons = icon_pixbuf_set; } public void activation(bool enabled) { } public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) { return new Publishing.Tumblr.TumblrPublisher(this, host); } public Spit.Publishing.Publisher.MediaType get_supported_media() { return (Spit.Publishing.Publisher.MediaType.PHOTO | Spit.Publishing.Publisher.MediaType.VIDEO); } } namespace Publishing.Tumblr { internal const string SERVICE_NAME = "Tumblr"; internal const string ENDPOINT_URL = "http://www.tumblr.com/"; internal const string API_KEY = "NdXvXQuKVccOsCOj0H4k9HUJcbcjDBYSo2AkaHzXFECHGNuP9k"; internal const string API_SECRET = "BN0Uoig0MwbeD27OgA0IwYlp3Uvonyfsrl9pf1cnnMj1QoEUvi"; internal const string ENCODE_RFC_3986_EXTRA = "!*'();:@&=+$,/?%#[] \\"; internal const int ORIGINAL_SIZE = -1; private class BlogEntry { public string blog; public string url; public BlogEntry(string creator_blog, string creator_url) { blog = creator_blog; url = creator_url; } } private class SizeEntry { public string title; public int size; public SizeEntry(string creator_title, int creator_size) { title = creator_title; size = creator_size; } } public class TumblrPublisher : Spit.Publishing.Publisher, GLib.Object { private Spit.Publishing.Service service; private Spit.Publishing.PluginHost host; private Spit.Publishing.ProgressCallback progress_reporter = null; private bool running = false; private bool was_started = false; private Session session = null; private PublishingOptionsPane publishing_options_pane = null; private SizeEntry[] sizes = null; private BlogEntry[] blogs = null; private string username = ""; private SizeEntry[] create_sizes() { SizeEntry[] result = new SizeEntry[0]; result += new SizeEntry(_("500 × 375 pixels"), 500); result += new SizeEntry(_("1024 × 768 pixels"), 1024); result += new SizeEntry(_("1280 × 853 pixels"), 1280); //Larger images make no sense for Tumblr // result += new SizeEntry(_("2048 × 1536 pixels"), 2048); // result += new SizeEntry(_("4096 × 3072 pixels"), 4096); // result += new SizeEntry(_("Original size"), ORIGINAL_SIZE); return result; } private BlogEntry[] create_blogs() { BlogEntry[] result = new BlogEntry[0]; return result; } public TumblrPublisher(Spit.Publishing.Service service, Spit.Publishing.PluginHost host) { debug("TumblrPublisher instantiated."); this.service = service; this.host = host; this.session = new Session(); this.sizes = this.create_sizes(); this.blogs = this.create_blogs(); session.authenticated.connect(on_session_authenticated); } ~TumblrPublisher() { session.authenticated.disconnect(on_session_authenticated); } private void invalidate_persistent_session() { set_persistent_access_phase_token(""); set_persistent_access_phase_token_secret(""); } // Publisher interface implementation public Spit.Publishing.Service get_service() { return service; } public Spit.Publishing.PluginHost get_host() { return host; } public bool is_running() { return running; } private bool is_persistent_session_valid() { string? access_phase_token = get_persistent_access_phase_token(); string? access_phase_token_secret = get_persistent_access_phase_token_secret(); bool valid = ((access_phase_token != null) && (access_phase_token_secret != null)); if (valid) debug("existing Tumblr session found in configuration database; using it."); else debug("no persisted Tumblr session exists."); return valid; } public string? get_persistent_access_phase_token() { return host.get_config_string("token", null); } private void set_persistent_access_phase_token(string? token) { host.set_config_string("token", token); } public string? get_persistent_access_phase_token_secret() { return host.get_config_string("token_secret", null); } private void set_persistent_access_phase_token_secret(string? token_secret) { host.set_config_string("token_secret", token_secret); } internal int get_persistent_default_size() { return host.get_config_int("default_size", 1); } internal void set_persistent_default_size(int size) { host.set_config_int("default_size", size); } internal int get_persistent_default_blog() { return host.get_config_int("default_blog", 0); } internal void set_persistent_default_blog(int blog) { host.set_config_int("default_blog", blog); } // Actions and events implementation /** * Action that shows the authentication pane. * * This action method shows the authentication pane. It is shown at the * very beginning of the interaction when no persistent parameters are found * or after a failed login attempt using persisted parameters. It can be * given a mode flag to specify whether it should be displayed in initial * mode or in any of the error modes that it supports. * * @param mode the mode for the authentication pane */ private void do_show_authentication_pane(AuthenticationPane.Mode mode = AuthenticationPane.Mode.INTRO) { debug("ACTION: installing authentication pane"); host.set_service_locked(false); AuthenticationPane authentication_pane = new AuthenticationPane(this, mode); authentication_pane.login.connect(on_authentication_pane_login_clicked); host.install_dialog_pane(authentication_pane, Spit.Publishing.PluginHost.ButtonMode.CLOSE); host.set_dialog_default_widget(authentication_pane.get_default_widget()); } /** * Event triggered when the login button in the authentication panel is * clicked. * * This event is triggered when the login button in the authentication * panel is clicked. It then triggers a network login interaction. * * @param username the name of the Tumblr user as entered in the dialog * @param password the password of the Tumblr as entered in the dialog */ private void on_authentication_pane_login_clicked( string username, string password ) { debug("EVENT: on_authentication_pane_login_clicked"); if (!running) return; do_network_login(username, password); } /** * Action to perform a network login to a Tumblr blog. * * This action performs a network login a Tumblr blog specified the given user name and password as credentials. * * @param username the name of the Tumblr user used to login * @param password the password of the Tumblr user used to login */ private void do_network_login(string username, string password) { debug("ACTION: logging in"); host.set_service_locked(true); host.install_login_wait_pane(); AccessTokenFetchTransaction txn = new AccessTokenFetchTransaction(session,username,password); txn.completed.connect(on_auth_request_txn_completed); txn.network_error.connect(on_auth_request_txn_error); try { txn.execute(); } catch (Spit.Publishing.PublishingError err) { host.post_error(err); } } private void on_auth_request_txn_completed(Publishing.RESTSupport.Transaction txn) { txn.completed.disconnect(on_auth_request_txn_completed); txn.network_error.disconnect(on_auth_request_txn_error); if (!is_running()) return; debug("EVENT: OAuth authentication request transaction completed; response = '%s'", txn.get_response()); do_parse_token_info_from_auth_request(txn.get_response()); } private void on_auth_request_txn_error(Publishing.RESTSupport.Transaction txn, Spit.Publishing.PublishingError err) { txn.completed.disconnect(on_auth_request_txn_completed); txn.network_error.disconnect(on_auth_request_txn_error); if (!is_running()) return; debug("EVENT: OAuth authentication request transaction caused a network error"); host.post_error(err); } private void do_parse_token_info_from_auth_request(string response) { debug("ACTION: parsing authorization request response '%s' into token and secret", response); string? oauth_token = null; string? oauth_token_secret = null; string[] key_value_pairs = response.split("&"); foreach (string pair in key_value_pairs) { string[] split_pair = pair.split("="); if (split_pair.length != 2) host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( _("“%s” isn’t a valid response to an OAuth authentication request"), response)); if (split_pair[0] == "oauth_token") oauth_token = split_pair[1]; else if (split_pair[0] == "oauth_token_secret") oauth_token_secret = split_pair[1]; } if (oauth_token == null || oauth_token_secret == null) host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( _("“%s” isn’t a valid response to an OAuth authentication request"), response)); session.set_access_phase_credentials(oauth_token, oauth_token_secret); } private void on_session_authenticated() { if (!is_running()) return; debug("EVENT: a fully authenticated session has become available"); set_persistent_access_phase_token(session.get_access_phase_token()); set_persistent_access_phase_token_secret(session.get_access_phase_token_secret()); do_get_blogs(); } private void do_get_blogs() { debug("ACTION: obtain all blogs of the tumblr user"); UserInfoFetchTransaction txn = new UserInfoFetchTransaction(session); txn.completed.connect(on_info_request_txn_completed); txn.network_error.connect(on_info_request_txn_error); try { txn.execute(); } catch (Spit.Publishing.PublishingError err) { host.post_error(err); } } private void on_info_request_txn_completed(Publishing.RESTSupport.Transaction txn) { txn.completed.disconnect(on_info_request_txn_completed); txn.network_error.disconnect(on_info_request_txn_error); if (!is_running()) return; debug("EVENT: user info request transaction completed; response = '%s'", txn.get_response()); do_parse_token_info_from_user_request(txn.get_response()); do_show_publishing_options_pane(); } private void do_parse_token_info_from_user_request(string response) { debug("ACTION: parsing info request response '%s' into list of available blogs", response); try { var parser = new Json.Parser(); parser.load_from_data (response, -1); var root_object = parser.get_root().get_object(); this.username = root_object.get_object_member("response").get_object_member("user").get_string_member ("name"); debug("Got user name: %s",username); foreach (var blognode in root_object.get_object_member("response").get_object_member("user").get_array_member("blogs").get_elements ()) { var blog = blognode.get_object (); string name = blog.get_string_member ("name"); string url = blog.get_string_member ("url").replace("http://","").replace("/",""); debug("Got blog name: %s and url: %s", name, url); this.blogs += new BlogEntry(name,url); } } catch (Error err) { host.post_error(err); } } private void on_info_request_txn_error(Publishing.RESTSupport.Transaction txn, Spit.Publishing.PublishingError err) { txn.completed.disconnect(on_info_request_txn_completed); txn.network_error.disconnect(on_info_request_txn_error); if (!is_running()) return; session.deauthenticate(); invalidate_persistent_session(); debug("EVENT: user info request transaction caused a network error"); host.post_error(err); } private void do_show_publishing_options_pane() { debug("ACTION: displaying publishing options pane"); host.set_service_locked(false); PublishingOptionsPane publishing_options_pane = new PublishingOptionsPane(this, host.get_publishable_media_type(), this.sizes, this.blogs, this.username); publishing_options_pane.publish.connect(on_publishing_options_pane_publish); publishing_options_pane.logout.connect(on_publishing_options_pane_logout); host.install_dialog_pane(publishing_options_pane); } private void on_publishing_options_pane_publish() { if (publishing_options_pane != null) { publishing_options_pane.publish.disconnect(on_publishing_options_pane_publish); publishing_options_pane.logout.disconnect(on_publishing_options_pane_logout); } if (!is_running()) return; debug("EVENT: user clicked the 'Publish' button in the publishing options pane"); do_publish(); } private void on_publishing_options_pane_logout() { if (publishing_options_pane != null) { publishing_options_pane.publish.disconnect(on_publishing_options_pane_publish); publishing_options_pane.logout.disconnect(on_publishing_options_pane_logout); } if (!is_running()) return; debug("EVENT: user clicked the 'Logout' button in the publishing options pane"); do_logout(); } public static int tumblr_date_time_compare_func(Spit.Publishing.Publishable a, Spit.Publishing.Publishable b) { return a.get_exposure_date_time().compare(b.get_exposure_date_time()); } private void do_publish() { debug("ACTION: uploading media items to remote server."); host.set_service_locked(true); progress_reporter = host.serialize_publishables(sizes[get_persistent_default_size()].size); // Serialization is a long and potentially cancellable operation, so before we use // the publishables, make sure that the publishing interaction is still running. If it // isn't the publishing environment may be partially torn down so do a short-circuit // return if (!is_running()) return; // Sort publishables in reverse-chronological order. Spit.Publishing.Publishable[] publishables = host.get_publishables(); Gee.ArrayList sorted_list = new Gee.ArrayList(); foreach (Spit.Publishing.Publishable p in publishables) { debug("ACTION: add publishable"); sorted_list.add(p); } sorted_list.sort(tumblr_date_time_compare_func); string blog_url = this.blogs[get_persistent_default_blog()].url; Uploader uploader = new Uploader(session, sorted_list.to_array(),blog_url); uploader.upload_complete.connect(on_upload_complete); uploader.upload_error.connect(on_upload_error); uploader.upload(on_upload_status_updated); } private void do_show_success_pane() { debug("ACTION: showing success pane."); host.set_service_locked(false); host.install_success_pane(); } private void on_upload_status_updated(int file_number, double completed_fraction) { if (!is_running()) return; debug("EVENT: uploader reports upload %.2f percent complete.", 100.0 * completed_fraction); assert(progress_reporter != null); progress_reporter(file_number, completed_fraction); } private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader, int num_published) { if (!is_running()) return; debug("EVENT: uploader reports upload complete; %d items published.", num_published); uploader.upload_complete.disconnect(on_upload_complete); uploader.upload_error.disconnect(on_upload_error); do_show_success_pane(); } private void on_upload_error(Publishing.RESTSupport.BatchUploader uploader, Spit.Publishing.PublishingError err) { if (!is_running()) return; debug("EVENT: uploader reports upload error = '%s'.", err.message); uploader.upload_complete.disconnect(on_upload_complete); uploader.upload_error.disconnect(on_upload_error); host.post_error(err); } private void do_logout() { debug("ACTION: logging user out, deauthenticating session, and erasing stored credentials"); session.deauthenticate(); invalidate_persistent_session(); running = false; attempt_start(); } public void attempt_start() { if (is_running()) return; debug("TumblrPublisher: starting interaction."); running = true; if (is_persistent_session_valid()) { debug("attempt start: a persistent session is available; using it"); session.authenticate_from_persistent_credentials(get_persistent_access_phase_token(), get_persistent_access_phase_token_secret()); } else { debug("attempt start: no persistent session available; showing login welcome pane"); do_show_authentication_pane(); } } public void start() { if (is_running()) return; if (was_started) error(_("TumblrPublisher: start( ): can’t start; this publisher is not restartable.")); debug("TumblrPublisher: starting interaction."); attempt_start(); } public void stop() { debug("TumblrPublisher: stop( ) invoked."); // if (session != null) // session.stop_transactions(); running = false; } // UI elements /** * The authentication pane used when asking service URL, user name and password * from the user. */ internal class AuthenticationPane : Spit.Publishing.DialogPane, Object { public enum Mode { INTRO, FAILED_RETRY_USER } private static string INTRO_MESSAGE = _("Enter the username and password associated with your Tumblr account."); private static string FAILED_RETRY_USER_MESSAGE = _("Username and/or password invalid. Please try again"); private Gtk.Box pane_widget = null; private Gtk.Builder builder; private Gtk.Entry username_entry; private Gtk.Entry password_entry; private Gtk.Button login_button; public signal void login(string user, string password); public AuthenticationPane(TumblrPublisher publisher, Mode mode = Mode.INTRO) { this.pane_widget = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); try { builder = new Gtk.Builder(); builder.add_from_resource (Resources.RESOURCE_PATH + "/tumblr_authentication_pane.ui"); builder.connect_signals(null); var content = builder.get_object ("content") as Gtk.Widget; Gtk.Label message_label = builder.get_object("message_label") as Gtk.Label; switch (mode) { case Mode.INTRO: message_label.set_text(INTRO_MESSAGE); break; case Mode.FAILED_RETRY_USER: message_label.set_markup("%s\n\n%s".printf(_( "Invalid User Name or Password"), FAILED_RETRY_USER_MESSAGE)); break; } username_entry = builder.get_object ("username_entry") as Gtk.Entry; password_entry = builder.get_object ("password_entry") as Gtk.Entry; login_button = builder.get_object("login_button") as Gtk.Button; username_entry.changed.connect(on_user_changed); password_entry.changed.connect(on_password_changed); login_button.clicked.connect(on_login_button_clicked); content.parent.remove (content); pane_widget.add (content); publisher.get_host().set_dialog_default_widget(login_button); } catch (Error e) { warning(_("Could not load UI: %s"), e.message); } } public Gtk.Widget get_default_widget() { return login_button; } private void on_login_button_clicked() { login(username_entry.get_text(), password_entry.get_text()); } private void on_user_changed() { update_login_button_sensitivity(); } private void on_password_changed() { update_login_button_sensitivity(); } private void update_login_button_sensitivity() { login_button.set_sensitive(username_entry.text_length > 0 && password_entry.text_length > 0); } public Gtk.Widget get_widget() { return pane_widget; } public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() { return Spit.Publishing.DialogPane.GeometryOptions.NONE; } public void on_pane_installed() { username_entry.grab_focus(); password_entry.set_activates_default(true); login_button.can_default = true; update_login_button_sensitivity(); } public void on_pane_uninstalled() { } } /** * The publishing options pane. */ internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object { private Gtk.Builder builder; private Gtk.Box pane_widget = null; private Gtk.Label upload_info_label = null; private Gtk.Label size_label = null; private Gtk.Label blog_label = null; private Gtk.Button logout_button = null; private Gtk.Button publish_button = null; private Gtk.ComboBoxText size_combo = null; private Gtk.ComboBoxText blog_combo = null; private SizeEntry[] sizes = null; private BlogEntry[] blogs = null; private string username = ""; private TumblrPublisher publisher = null; private Spit.Publishing.Publisher.MediaType media_type; public signal void publish(); public signal void logout(); public PublishingOptionsPane(TumblrPublisher publisher, Spit.Publishing.Publisher.MediaType media_type, SizeEntry[] sizes, BlogEntry[] blogs, string username) { this.pane_widget = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); this.username = username; this.publisher = publisher; this.media_type = media_type; this.sizes = sizes; this.blogs=blogs; try { builder = new Gtk.Builder(); builder.add_from_resource (Resources.RESOURCE_PATH + "/tumblr_publishing_options_pane.ui"); builder.connect_signals(null); // pull in the necessary widgets from the glade file pane_widget = (Gtk.Box) this.builder.get_object("tumblr_pane"); upload_info_label = (Gtk.Label) this.builder.get_object("upload_info_label"); logout_button = (Gtk.Button) this.builder.get_object("logout_button"); publish_button = (Gtk.Button) this.builder.get_object("publish_button"); size_combo = (Gtk.ComboBoxText) this.builder.get_object("size_combo"); size_label = (Gtk.Label) this.builder.get_object("size_label"); blog_combo = (Gtk.ComboBoxText) this.builder.get_object("blog_combo"); blog_label = (Gtk.Label) this.builder.get_object("blog_label"); string upload_label_text = _("You are logged into Tumblr as %s.\n\n").printf(this.username); upload_info_label.set_label(upload_label_text); populate_blog_combo(); blog_combo.changed.connect(on_blog_changed); if ((media_type != Spit.Publishing.Publisher.MediaType.VIDEO)) { populate_size_combo(); size_combo.changed.connect(on_size_changed); } else { // publishing -only- video - don't let the user manipulate the photo size choices. size_combo.set_sensitive(false); size_label.set_sensitive(false); } logout_button.clicked.connect(on_logout_clicked); publish_button.clicked.connect(on_publish_clicked); } catch (Error e) { warning(_("Could not load UI: %s"), e.message); } } private void on_logout_clicked() { logout(); } private void on_publish_clicked() { publish(); } private void populate_blog_combo() { if (blogs != null) { foreach (BlogEntry b in blogs) blog_combo.append_text(b.blog); blog_combo.set_active(publisher.get_persistent_default_blog()); } } private void on_blog_changed() { publisher.set_persistent_default_blog(blog_combo.get_active()); } private void populate_size_combo() { if (sizes != null) { foreach (SizeEntry e in sizes) size_combo.append_text(e.title); size_combo.set_active(publisher.get_persistent_default_size()); } } private void on_size_changed() { publisher.set_persistent_default_size(size_combo.get_active()); } protected void notify_publish() { publish(); } protected void notify_logout() { logout(); } public Gtk.Widget get_widget() { return pane_widget; } public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() { return Spit.Publishing.DialogPane.GeometryOptions.NONE; } public void on_pane_installed() { publish.connect(notify_publish); logout.connect(notify_logout); } public void on_pane_uninstalled() { publish.disconnect(notify_publish); logout.disconnect(notify_logout); } } // REST support classes internal class Transaction : Publishing.RESTSupport.Transaction { public Transaction(Session session, Publishing.RESTSupport.HttpMethod method = Publishing.RESTSupport.HttpMethod.POST) { base(session, method); } public Transaction.with_uri(Session session, string uri, Publishing.RESTSupport.HttpMethod method = Publishing.RESTSupport.HttpMethod.POST) { base.with_endpoint_url(session, uri, method); add_argument("oauth_nonce", session.get_oauth_nonce()); add_argument("oauth_signature_method", "HMAC-SHA1"); add_argument("oauth_version", "1.0"); add_argument("oauth_timestamp", session.get_oauth_timestamp()); add_argument("oauth_consumer_key", API_KEY); if (session.get_access_phase_token() != null) { add_argument("oauth_token", session.get_access_phase_token()); } } public override void execute() throws Spit.Publishing.PublishingError { ((Session) get_parent_session()).sign_transaction(this); base.execute(); } } internal class AccessTokenFetchTransaction : Transaction { public AccessTokenFetchTransaction(Session session, string username, string password) { base.with_uri(session, "https://www.tumblr.com/oauth/access_token", Publishing.RESTSupport.HttpMethod.POST); add_argument("x_auth_username", Soup.URI.encode(username, ENCODE_RFC_3986_EXTRA)); add_argument("x_auth_password", password); add_argument("x_auth_mode", "client_auth"); } } internal class UserInfoFetchTransaction : Transaction { public UserInfoFetchTransaction(Session session) { base.with_uri(session, "http://api.tumblr.com/v2/user/info", Publishing.RESTSupport.HttpMethod.POST); } } internal class UploadTransaction : Publishing.RESTSupport.UploadTransaction { private Session session; private Publishing.RESTSupport.Argument[] auth_header_fields; //Workaround for Soup.URI.encode() to support binary data (i.e. string with \0) private string encode( uint8[] data ){ var s = new StringBuilder(); char[] bytes = new char[2]; bytes[1] = 0; foreach( var byte in data ) { if(byte == 0) { s.append( "%00" ); } else { bytes[0] = (char)byte; s.append( Soup.URI.encode((string) bytes, ENCODE_RFC_3986_EXTRA) ); } } return s.str; } public UploadTransaction(Session session,Spit.Publishing.Publishable publishable, string blog_url) { debug("Init upload transaction"); base.with_endpoint_url(session, publishable,"http://api.tumblr.com/v2/blog/%s/post".printf(blog_url) ); this.session = session; } public void add_authorization_header_field(string key, string value) { auth_header_fields += new Publishing.RESTSupport.Argument(key, value); } public Publishing.RESTSupport.Argument[] get_authorization_header_fields() { return auth_header_fields; } public string get_authorization_header_string() { string result = "OAuth "; for (int i = 0; i < auth_header_fields.length; i++) { result += auth_header_fields[i].key; result += "="; result += ("\"" + auth_header_fields[i].value + "\""); if (i < auth_header_fields.length - 1) result += ", "; } return result; } public override void execute() throws Spit.Publishing.PublishingError { add_authorization_header_field("oauth_nonce", session.get_oauth_nonce()); add_authorization_header_field("oauth_signature_method", "HMAC-SHA1"); add_authorization_header_field("oauth_version", "1.0"); add_authorization_header_field("oauth_timestamp", session.get_oauth_timestamp()); add_authorization_header_field("oauth_consumer_key", API_KEY); add_authorization_header_field("oauth_token", session.get_access_phase_token()); string payload; size_t payload_length; try { FileUtils.get_contents(base.publishable.get_serialized_file().get_path(), out payload, out payload_length); string reqdata = this.encode(payload.data[0:payload_length]); add_argument("data[0]", reqdata); add_argument("type", "photo"); string[] keywords = base.publishable.get_publishing_keywords(); string tags = ""; if (keywords != null) { tags = string.joinv (",", keywords); } add_argument("tags", Soup.URI.encode(tags, ENCODE_RFC_3986_EXTRA)); } catch (FileError e) { throw new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( _("A temporary file needed for publishing is unavailable")); } session.sign_transaction(this); string authorization_header = get_authorization_header_string(); debug("executing upload transaction: authorization header string = '%s'", authorization_header); add_header("Authorization", authorization_header); Publishing.RESTSupport.Argument[] request_arguments = get_arguments(); assert(request_arguments.length > 0); string request_data = ""; for (int i = 0; i < request_arguments.length; i++) { request_data += (request_arguments[i].key + "=" + request_arguments[i].value); if (i < request_arguments.length - 1) request_data += "&"; } Soup.Message outbound_message = new Soup.Message( "POST", get_endpoint_url()); outbound_message.set_request("application/x-www-form-urlencoded", Soup.MemoryUse.COPY, request_data.data); // TODO: there must be a better way to iterate over a map Gee.MapIterator i = base.message_headers.map_iterator(); bool cont = i.next(); while(cont) { outbound_message.request_headers.append(i.get_key(), i.get_value()); cont = i.next(); } set_message(outbound_message); set_is_executed(true); send(); } } internal class Uploader : Publishing.RESTSupport.BatchUploader { private string blog_url = ""; public Uploader(Session session, Spit.Publishing.Publishable[] publishables, string blog_url) { base(session, publishables); this.blog_url=blog_url; } protected override Publishing.RESTSupport.Transaction create_transaction( Spit.Publishing.Publishable publishable) { debug("Create upload transaction"); return new UploadTransaction((Session) get_session(), get_current_publishable(), this.blog_url); } } /** * Session class that keeps track of the authentication status and of the * user token tumblr. */ internal class Session : Publishing.RESTSupport.Session { private string? access_phase_token = null; private string? access_phase_token_secret = null; public Session() { base(ENDPOINT_URL); } public override bool is_authenticated() { return (access_phase_token != null && access_phase_token_secret != null); } public void authenticate_from_persistent_credentials(string token, string secret) { this.access_phase_token = token; this.access_phase_token_secret = secret; authenticated(); } public void deauthenticate() { access_phase_token = null; access_phase_token_secret = null; } public void sign_transaction(Publishing.RESTSupport.Transaction txn) { string http_method = txn.get_method().to_string(); debug("signing transaction with parameters:"); debug("HTTP method = " + http_method); string? signing_key = null; if (access_phase_token_secret != null) { debug("access phase token secret available; using it as signing key"); signing_key = API_SECRET + "&" + this.get_access_phase_token_secret(); } else { debug("Access phase token secret not available; using API " + "key as signing key"); signing_key = API_SECRET + "&"; } Publishing.RESTSupport.Argument[] base_string_arguments = txn.get_arguments(); UploadTransaction? upload_txn = txn as UploadTransaction; if (upload_txn != null) { debug("this transaction is an UploadTransaction; including Authorization header " + "fields in signature base string"); Publishing.RESTSupport.Argument[] auth_header_args = upload_txn.get_authorization_header_fields(); foreach (Publishing.RESTSupport.Argument arg in auth_header_args) base_string_arguments += arg; } Publishing.RESTSupport.Argument[] sorted_args = Publishing.RESTSupport.Argument.sort(base_string_arguments); string arguments_string = ""; for (int i = 0; i < sorted_args.length; i++) { arguments_string += (sorted_args[i].key + "=" + sorted_args[i].value); if (i < sorted_args.length - 1) arguments_string += "&"; } string signature_base_string = http_method + "&" + Soup.URI.encode( txn.get_endpoint_url(), ENCODE_RFC_3986_EXTRA) + "&" + Soup.URI.encode(arguments_string, ENCODE_RFC_3986_EXTRA); debug("signature base string = '%s'", signature_base_string); debug("signing key = '%s'", signing_key); // compute the signature string signature = Publishing.RESTSupport.hmac_sha1(signing_key, signature_base_string); debug("signature = '%s'", signature); signature = Soup.URI.encode(signature, ENCODE_RFC_3986_EXTRA); debug("signature after RFC encode = '%s'", signature); if (upload_txn != null) upload_txn.add_authorization_header_field("oauth_signature", signature); else txn.add_argument("oauth_signature", signature); } public void set_access_phase_credentials(string token, string secret) { this.access_phase_token = token; this.access_phase_token_secret = secret; authenticated(); } public string get_access_phase_token() { return access_phase_token; } public string get_access_phase_token_secret() { return access_phase_token_secret; } public string get_oauth_nonce() { TimeVal currtime = TimeVal(); currtime.get_current_time(); return Checksum.compute_for_string(ChecksumType.MD5, currtime.tv_sec.to_string() + currtime.tv_usec.to_string()); } public string get_oauth_timestamp() { return GLib.get_real_time().to_string().substring(0, 10); } } } //class TumblrPublisher } //namespace Publishing.Tumblr