diff options
Diffstat (limited to 'plugins/shotwell-publishing-extras/YandexPublishing.vala')
-rw-r--r-- | plugins/shotwell-publishing-extras/YandexPublishing.vala | 665 |
1 files changed, 665 insertions, 0 deletions
diff --git a/plugins/shotwell-publishing-extras/YandexPublishing.vala b/plugins/shotwell-publishing-extras/YandexPublishing.vala new file mode 100644 index 0000000..36a3ede --- /dev/null +++ b/plugins/shotwell-publishing-extras/YandexPublishing.vala @@ -0,0 +1,665 @@ +/* Copyright 2010+ Evgeniy Polyakov <zbr@ioremap.net> + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +public class YandexService : Object, Spit.Pluggable, Spit.Publishing.Service { + 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.yandex-fotki"; + } + + public unowned string get_pluggable_name() { + return "Yandex.Fotki"; + } + + public void get_info(ref Spit.PluggableInfo info) { + info.authors = "Evgeniy Polyakov <zbr@ioremap.net>"; + info.copyright = _("Copyright 2010+ Evgeniy Polyakov <zbr@ioremap.net>"); + info.translators = Resources.TRANSLATORS; + info.version = _VERSION; + info.website_name = _("Visit the Yandex.Fotki web site"); + info.website_url = "http://fotki.yandex.ru/"; + info.is_license_wordwrapped = false; + info.license = Resources.LICENSE; + } + + public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) { + return new Publishing.Yandex.YandexPublisher(this, host); + } + + public Spit.Publishing.Publisher.MediaType get_supported_media() { + return (Spit.Publishing.Publisher.MediaType.PHOTO); + } + + public void activation(bool enabled) { + } +} + +namespace Publishing.Yandex { + +internal const string SERVICE_NAME = "Yandex.Fotki"; + +private const string client_id = "52be4756dee3438792c831a75d7cd360"; + +internal class Transaction: Publishing.RESTSupport.Transaction { + public Transaction.with_url(Session session, string url, Publishing.RESTSupport.HttpMethod method = Publishing.RESTSupport.HttpMethod.GET) { + base.with_endpoint_url(session, url, method); + add_headers(); + } + + private void add_headers() { + if (((Session) get_parent_session()).is_authenticated()) { + add_header("Authorization", "OAuth %s".printf(((Session) get_parent_session()).get_auth_token())); + add_header("Connection", "close"); + } + } + + public Transaction(Session session, Publishing.RESTSupport.HttpMethod method = Publishing.RESTSupport.HttpMethod.GET) { + base(session, method); + add_headers(); + } + + public void add_data(string type, string data) { + set_custom_payload(data, type); + } +} + +internal class Session : Publishing.RESTSupport.Session { + private string? auth_token = null; + + public Session() { + } + + public override bool is_authenticated() { + return (auth_token != null); + } + + public void deauthenticate() { + auth_token = null; + } + + public void set_auth_token(string token) { + this.auth_token = token; + } + + public string? get_auth_token() { + return auth_token; + } +} + +internal class WebAuthPane : Spit.Publishing.DialogPane, GLib.Object { + private WebKit.WebView webview = null; + private Gtk.Box pane_widget = null; + private Gtk.ScrolledWindow webview_frame = null; + + private Regex re; + private string? login_url = null; + + public signal void login_succeeded(string success_url); + public signal void login_failed(); + + public WebAuthPane(string login_url) { + this.login_url = login_url; + + try { + this.re = new Regex("(.*)#access_token=([a-zA-Z0-9]*)&"); + } catch (RegexError e) { + critical("%s", e.message); + } + + pane_widget = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); + + webview_frame = new Gtk.ScrolledWindow(null, null); + webview_frame.set_shadow_type(Gtk.ShadowType.ETCHED_IN); + webview_frame.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); + + webview = new WebKit.WebView(); + webview.get_settings().enable_plugins = false; + webview.get_settings().enable_default_context_menu = false; + + webview.load_finished.connect(on_page_load); + webview.load_started.connect(on_load_started); + webview.navigation_requested.connect(navigation_requested); + + webview_frame.add(webview); + pane_widget.pack_start(webview_frame, true, true, 0); + } + + private void on_page_load(WebKit.WebFrame origin_frame) { + pane_widget.get_window().set_cursor(new Gdk.Cursor(Gdk.CursorType.LEFT_PTR)); + } + + private WebKit.NavigationResponse navigation_requested (WebKit.WebFrame frame, WebKit.NetworkRequest req) { + debug("Navigating to '%s'", req.uri); + + MatchInfo info = null; + + if (re.match(req.uri, 0, out info)) { + string access_token = info.fetch_all()[2]; + + debug("Load completed: %s", access_token); + pane_widget.get_window().set_cursor(new Gdk.Cursor(Gdk.CursorType.LEFT_PTR)); + if (access_token != null) { + login_succeeded(access_token); + return WebKit.NavigationResponse.IGNORE; + } else + login_failed(); + } + return WebKit.NavigationResponse.ACCEPT; + } + + private void on_load_started(WebKit.WebFrame frame) { + pane_widget.get_window().set_cursor(new Gdk.Cursor(Gdk.CursorType.WATCH)); + } + + public Gtk.Widget get_widget() { + return pane_widget; + } + + public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() { + return Spit.Publishing.DialogPane.GeometryOptions.RESIZABLE; + } + + public void on_pane_installed() { + webview.open(login_url); + } + + public void on_pane_uninstalled() { + } +} + +internal class PublishOptions { + public bool disable_comments = false; + public bool hide_original = false; + public string access_type; + + public string destination_album = null; + public string destination_album_url = null; +} + +internal class PublishingOptionsPane: Spit.Publishing.DialogPane, GLib.Object { + private Gtk.Box box; + private Gtk.Builder builder; + private Gtk.Button logout_button; + private Gtk.Button publish_button; + private Gtk.ComboBoxText album_list; + + private weak PublishOptions options; + + public signal void publish(); + public signal void logout(); + + public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() { + return Spit.Publishing.DialogPane.GeometryOptions.NONE; + } + public void on_pane_installed() { + } + public void on_pane_uninstalled() { + } + public Gtk.Widget get_widget() { + return box; + } + + public PublishingOptionsPane(PublishOptions options, Gee.HashMap<string, string> list, + Spit.Publishing.PluginHost host) { + this.options = options; + + box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); + + File ui_file = host.get_module_file().get_parent().get_child("yandex_publish_model.glade"); + + try { + builder = new Gtk.Builder(); + builder.add_from_file(ui_file.get_path()); + builder.connect_signals(null); + Gtk.Alignment align = builder.get_object("alignment") as Gtk.Alignment; + + album_list = builder.get_object ("album_list") as Gtk.ComboBoxText; + foreach (string key in list.keys) + album_list.append_text(key); + + album_list.set_active(0); + + publish_button = builder.get_object("publish_button") as Gtk.Button; + logout_button = builder.get_object("logout_button") as Gtk.Button; + + publish_button.clicked.connect(on_publish_clicked); + logout_button.clicked.connect(on_logout_clicked); + + align.reparent(box); + box.set_child_packing(align, true, true, 0, Gtk.PackType.START); + } catch (Error e) { + warning("Could not load UI: %s", e.message); + } + } + + private void on_logout_clicked() { + logout(); + } + + private void on_publish_clicked() { + options.destination_album = album_list.get_active_text(); + + Gtk.CheckButton tmp = builder.get_object("hide_original_check") as Gtk.CheckButton; + options.hide_original = tmp.active; + + tmp = builder.get_object("disable_comments_check") as Gtk.CheckButton; + options.disable_comments = tmp.active; + + Gtk.ComboBoxText access_type = builder.get_object("access_type_list") as Gtk.ComboBoxText; + options.access_type = access_type.get_active_text(); + + publish(); + } +} + +private class Uploader: Publishing.RESTSupport.BatchUploader { + private weak PublishOptions options; + + public Uploader(Session session, PublishOptions options, Spit.Publishing.Publishable[] photos) { + base(session, photos); + + this.options = options; + } + + protected override Publishing.RESTSupport.Transaction create_transaction(Spit.Publishing.Publishable publishable) { + debug("create transaction"); + return new UploadTransaction(((Session) get_session()), options, get_current_publishable()); + } +} + +private class UploadTransaction: Transaction { + public UploadTransaction(Session session, PublishOptions options, Spit.Publishing.Publishable photo) { + base.with_url(session, options.destination_album_url, Publishing.RESTSupport.HttpMethod.POST); + + set_custom_payload("qwe", "image/jpeg", 1); + + debug("Uploading '%s' -> %s : %s", photo.get_publishing_name(), options.destination_album, options.destination_album_url); + + Soup.Multipart message_parts = new Soup.Multipart("multipart/form-data"); + message_parts.append_form_string("title", photo.get_publishing_name()); + message_parts.append_form_string("hide_original", options.hide_original.to_string()); + message_parts.append_form_string("disable_comments", options.disable_comments.to_string()); + message_parts.append_form_string("access", options.access_type.down()); + + string photo_data; + size_t data_length; + + try { + FileUtils.get_contents(photo.get_serialized_file().get_path(), out photo_data, out data_length); + } catch (GLib.FileError e) { + critical("Failed to read data file '%s': %s", photo.get_serialized_file().get_path(), e.message); + } + + int image_part_num = message_parts.get_length(); + + Soup.Buffer bindable_data = new Soup.Buffer(Soup.MemoryUse.COPY, photo_data.data[0:data_length]); + message_parts.append_form_file("", photo.get_serialized_file().get_path(), "image/jpeg", bindable_data); + + unowned Soup.MessageHeaders image_part_header; + unowned Soup.Buffer image_part_body; + message_parts.get_part(image_part_num, out image_part_header, out image_part_body); + + GLib.HashTable<string, string> result = new GLib.HashTable<string, string>(GLib.str_hash, GLib.str_equal); + result.insert("name", "image"); + result.insert("filename", "unused"); + + image_part_header.set_content_disposition("form-data", result); + + Soup.Message outbound_message = soup_form_request_new_from_multipart(get_endpoint_url(), message_parts); + outbound_message.request_headers.append("Authorization", ("OAuth %s").printf(session.get_auth_token())); + outbound_message.request_headers.append("Connection", "close"); + set_message(outbound_message); + } +} + +public class YandexPublisher : Spit.Publishing.Publisher, GLib.Object { + private weak Spit.Publishing.PluginHost host = null; + private Spit.Publishing.ProgressCallback progress_reporter = null; + private weak Spit.Publishing.Service service = null; + + private string service_url = null; + + private Gee.HashMap<string, string> album_list = null; + private PublishOptions options; + + private bool running = false; + + private WebAuthPane web_auth_pane = null; + + private Session session; + + public YandexPublisher(Spit.Publishing.Service service, Spit.Publishing.PluginHost host) { + this.service = service; + this.host = host; + this.session = new Session(); + this.album_list = new Gee.HashMap<string, string>(); + this.options = new PublishOptions(); + } + + internal string? get_persistent_auth_token() { + return host.get_config_string("auth_token", null); + } + + internal void set_persistent_auth_token(string auth_token) { + host.set_config_string("auth_token", auth_token); + } + + internal void invalidate_persistent_session() { + host.unset_config_key("auth_token"); + } + + internal bool is_persistent_session_available() { + return (get_persistent_auth_token() != null); + } + + public bool is_running() { + return running; + } + + public Spit.Publishing.Service get_service() { + return service; + } + + private new string? check_response(Publishing.RESTSupport.XmlDocument doc) { + return null; + } + + private void parse_album_entry(Xml.Node *e) throws Spit.Publishing.PublishingError { + string title = null; + string link = null; + + for (Xml.Node* c = e->children ; c != null; c = c->next) { + if (c->name == "title") + title = c->get_content(); + + if ((c->name == "link") && (c->get_prop("rel") == "photos")) + link = c->get_prop("href"); + + if (title != null && link != null) { + debug("Added album: '%s', link: %s", title, link); + album_list.set(title, link); + title = null; + link = null; + break; + } + } + } + + public void parse_album_creation(string data) throws Spit.Publishing.PublishingError { + Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string(data, check_response); + Xml.Node *root = doc.get_root_node(); + + parse_album_entry(root); + } + + public void parse_album_list(string data) throws Spit.Publishing.PublishingError { + Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string(data, check_response); + Xml.Node *root = doc.get_root_node(); + + for (Xml.Node *e = root->children ; e != null; e = e->next) { + if (e->name != "entry") + continue; + + parse_album_entry(e); + } + } + + private void album_creation_error(Publishing.RESTSupport.Transaction t, Spit.Publishing.PublishingError err) { + t.completed.disconnect(album_creation_complete); + t.network_error.disconnect(album_creation_error); + + warning("Album creation error: %s", err.message); + } + + private void album_creation_complete(Publishing.RESTSupport.Transaction t) { + t.completed.disconnect(album_creation_complete); + t.network_error.disconnect(album_creation_error); + + try { + parse_album_creation(t.get_response()); + } catch (Spit.Publishing.PublishingError err) { + host.post_error(err); + return; + } + + if (album_list.get(options.destination_album) != null) + start_upload(); + else + host.post_error(new Spit.Publishing.PublishingError.PROTOCOL_ERROR("Server did not create album")); + } + + private void create_destination_album() { + string album = options.destination_album; + string data = "<entry xmlns=\"http://www.w3.org/2005/Atom\" xmlns:f=\"yandex:fotki\"><title>%s</title></entry>".printf(album); + + Transaction t = new Transaction.with_url(session, service_url, Publishing.RESTSupport.HttpMethod.POST); + + t.add_data("application/atom+xml; charset=utf-8; type=entry", data); + + t.completed.connect(album_creation_complete); + t.network_error.connect(album_creation_error); + + try { + t.execute(); + } catch (Spit.Publishing.PublishingError err) { + host.post_error(err); + } + } + + private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader, int num_published) { + uploader.upload_complete.disconnect(on_upload_complete); + uploader.upload_error.disconnect(on_upload_error); + + if (num_published == 0) + host.post_error(new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR("")); + + host.set_service_locked(false); + + host.install_success_pane(); + } + + private void on_upload_error(Publishing.RESTSupport.BatchUploader uploader, Spit.Publishing.PublishingError err) { + uploader.upload_complete.disconnect(on_upload_complete); + uploader.upload_error.disconnect(on_upload_error); + + warning("Photo upload error: %s", err.message); + } + + private void on_upload_status_updated(int file_number, double completed_fraction) { + debug("EVENT: uploader reports upload %.2f percent complete.", 100.0 * completed_fraction); + + assert(progress_reporter != null); + + progress_reporter(file_number, completed_fraction); + } + + private void start_upload() { + host.set_service_locked(true); + + progress_reporter = host.serialize_publishables(0); + + options.destination_album_url = album_list.get(options.destination_album); + Spit.Publishing.Publishable[] publishables = host.get_publishables(); + Uploader uploader = new Uploader(session, options, publishables); + + uploader.upload_complete.connect(on_upload_complete); + uploader.upload_error.connect(on_upload_error); + uploader.upload(on_upload_status_updated); + } + + private void on_logout() { + if (!is_running()) + return; + + session.deauthenticate(); + invalidate_persistent_session(); + + running = false; + + start(); + } + + private void on_publish() { + debug("Going to publish to '%s' : %s", options.destination_album, album_list.get(options.destination_album)); + if (album_list.get(options.destination_album) == null) + create_destination_album(); + else + start_upload(); + } + + public void service_get_album_list_error(Publishing.RESTSupport.Transaction t, Spit.Publishing.PublishingError err) { + t.completed.disconnect(service_get_album_list_complete); + t.network_error.disconnect(service_get_album_list_error); + + invalidate_persistent_session(); + warning("Failed to get album list: %s", err.message); + } + + public void service_get_album_list_complete(Publishing.RESTSupport.Transaction t) { + t.completed.disconnect(service_get_album_list_complete); + t.network_error.disconnect(service_get_album_list_error); + + debug("service_get_album_list_complete: %s", t.get_response()); + try { + parse_album_list(t.get_response()); + } catch (Spit.Publishing.PublishingError err) { + host.post_error(err); + } + + PublishingOptionsPane publishing_options_pane = new PublishingOptionsPane(options, album_list, + host); + + publishing_options_pane.publish.connect(on_publish); + publishing_options_pane.logout.connect(on_logout); + host.install_dialog_pane(publishing_options_pane); + } + + public void service_get_album_list(string url) { + service_url = url; + + Transaction t = new Transaction.with_url(session, url); + t.completed.connect(service_get_album_list_complete); + t.network_error.connect(service_get_album_list_error); + + try { + t.execute(); + } catch (Spit.Publishing.PublishingError err) { + host.post_error(err); + } + } + + public void fetch_account_error(Publishing.RESTSupport.Transaction t, Spit.Publishing.PublishingError err) { + t.completed.disconnect(fetch_account_complete); + t.network_error.disconnect(fetch_account_error); + + warning("Failed to fetch account info: %s", err.message); + } + + public void fetch_account_complete(Publishing.RESTSupport.Transaction t) { + t.completed.disconnect(fetch_account_complete); + t.network_error.disconnect(fetch_account_error); + + debug("account info: %s", t.get_response()); + try { + Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string(t.get_response(), check_response); + Xml.Node* root = doc.get_root_node(); + + for (Xml.Node* work = root->children ; work != null; work = work->next) { + if (work->name != "workspace") + continue; + for (Xml.Node* c = work->children ; c != null; c = c->next) { + if (c->name != "collection") + continue; + + if (c->get_prop("id") == "album-list") { + string url = c->get_prop("href"); + + set_persistent_auth_token(session.get_auth_token()); + service_get_album_list(url); + break; + } + } + } + } catch (Spit.Publishing.PublishingError err) { + host.post_error(err); + } + } + + public void fetch_account_information(string auth_token) { + session.set_auth_token(auth_token); + + Transaction t = new Transaction.with_url(session, "http://api-fotki.yandex.ru/api/me/"); + t.completed.connect(fetch_account_complete); + t.network_error.connect(fetch_account_error); + + try { + t.execute(); + } catch (Spit.Publishing.PublishingError err) { + host.post_error(err); + } + } + + private void web_auth_login_succeeded(string access_token) { + debug("login succeeded with token %s", access_token); + + host.set_service_locked(true); + host.install_account_fetch_wait_pane(); + + fetch_account_information(access_token); + } + + private void web_auth_login_failed() { + debug("login failed"); + } + + private void start_web_auth() { + host.set_service_locked(false); + + web_auth_pane = new WebAuthPane(("http://oauth.yandex.ru/authorize?client_id=%s&response_type=token").printf(client_id)); + web_auth_pane.login_succeeded.connect(web_auth_login_succeeded); + web_auth_pane.login_failed.connect(web_auth_login_failed); + + host.install_dialog_pane(web_auth_pane, Spit.Publishing.PluginHost.ButtonMode.CANCEL); + } + + private void show_welcome_page() { + host.install_welcome_pane(_("You are not currently logged into Yandex.Fotki."), + start_web_auth); + } + + public void start() { + if (is_running()) + return; + + if (host == null) + error("YandexPublisher: start( ): can't start; this publisher is not restartable."); + + debug("YandexPublisher: starting interaction."); + + running = true; + + if (is_persistent_session_available()) { + session.set_auth_token(get_persistent_auth_token()); + + fetch_account_information(get_persistent_auth_token()); + } else { + show_welcome_page(); + } + } + + public void stop() { + debug("YandexPublisher: stop( ) invoked."); + + host = null; + running = false; + } +} + +} + |