1
/* Copyright 2009-2013 Yorba Foundation
3
* This software is licensed under the GNU Lesser General Public License
4
* (version 2.1 or later). See the COPYING file in this distribution.
7
public class FacebookService : Object, Spit.Pluggable, Spit.Publishing.Service {
8
private const string ICON_FILENAME = "facebook.png";
10
private static Gdk.Pixbuf[] icon_pixbuf_set = null;
12
public FacebookService (GLib.File resource_directory) {
13
if (icon_pixbuf_set == null)
14
icon_pixbuf_set = Resources.load_icon_set (resource_directory.get_child (ICON_FILENAME));
17
public int get_pluggable_interface (int min_host_interface, int max_host_interface) {
18
return Spit.negotiate_interfaces (min_host_interface, max_host_interface,
19
Spit.Publishing.CURRENT_INTERFACE);
22
public unowned string get_id () {
23
return "org.pantheon.photos.publishing.facebook";
26
public unowned string get_pluggable_name () {
30
public void get_info (ref Spit.PluggableInfo info) {
31
info.authors = "Lucas Beeler";
32
info.copyright = _ ("Copyright 2009-2013 Yorba Foundation");
33
info.translators = Resources.TRANSLATORS;
34
info.version = _VERSION;
35
info.website_name = Resources.WEBSITE_NAME;
36
info.website_url = Resources.WEBSITE_URL;
37
info.is_license_wordwrapped = false;
38
info.license = Resources.LICENSE;
39
info.icons = icon_pixbuf_set;
42
public void activation (bool enabled) {
45
public Spit.Publishing.Publisher create_publisher (Spit.Publishing.PluginHost host) {
46
return new Publishing.Facebook.FacebookPublisher (this, host);
49
public Spit.Publishing.Publisher.MediaType get_supported_media () {
50
return (Spit.Publishing.Publisher.MediaType.PHOTO |
51
Spit.Publishing.Publisher.MediaType.VIDEO);
55
namespace Publishing.Facebook {
56
// global parameters for the Facebook publishing plugin -- don't touch these (unless you really,
57
// truly, deep-down know what you're doing)
58
public const string SERVICE_NAME = "facebook";
59
internal const string USER_VISIBLE_NAME = "Facebook";
60
internal const string APPLICATION_ID = "162702932093";
61
internal const string DEFAULT_ALBUM_NAME = _ ("Shotwell Connect");
62
internal const string SERVICE_WELCOME_MESSAGE =
63
_ ("You are not currently logged into Facebook.\n\nIf you don't yet have a Facebook account, you can create one during the login process. During login, Shotwell Connect may ask you for permission to upload photos and publish to your feed. These permissions are required for Shotwell Connect to function.");
64
internal const string RESTART_ERROR_MESSAGE =
65
_ ("You have already logged in and out of Facebook during this Shotwell session.\nTo continue publishing to Facebook, quit and restart Shotwell, then try publishing again.");
66
internal const string USER_AGENT = "Java/1.6.0_16";
67
internal const int EXPIRED_SESSION_STATUS_CODE = 400;
69
internal class Album {
73
public Album (string name, string id) {
79
internal enum Resolution {
83
public string get_name () {
86
return _ ("Standard (720 pixels)");
89
return _ ("Large (2048 pixels)");
92
error ("Unknown resolution %s", this.to_string ());
96
public int get_pixels () {
105
error ("Unknown resolution %s", this.to_string ());
110
internal class PublishingParameters {
111
public const int UNKNOWN_ALBUM = -1;
113
public bool strip_metadata;
114
public Album[] albums;
115
public int target_album;
116
public string? new_album_name; // the name of the new album being created during this
117
// publishing interaction or null if publishing to an existing
120
public string? privacy_object; // a serialized JSON object encoding the privacy settings of the
121
// published resources
122
public Resolution resolution;
124
public PublishingParameters () {
126
this.privacy_object = null;
127
this.target_album = UNKNOWN_ALBUM;
128
this.new_album_name = null;
129
this.strip_metadata = false;
130
this.resolution = Resolution.HIGH;
133
public void add_album (string name, string id) {
135
albums = new Album[0];
137
Album new_album = new Album (name, id);
141
public void set_target_album_by_name (string? name) {
143
target_album = UNKNOWN_ALBUM;
147
for (int i = 0; i < albums.length; i++) {
149
if (albums[i].name == name) {
155
target_album = UNKNOWN_ALBUM;
158
public string? get_target_album_name () {
159
if (albums == null || target_album == UNKNOWN_ALBUM)
162
return albums[target_album].name;
165
public string? get_target_album_id () {
166
if (albums == null || target_album == UNKNOWN_ALBUM)
169
return albums[target_album].id;
173
public class FacebookPublisher : Spit.Publishing.Publisher, GLib.Object {
174
private PublishingParameters publishing_params;
175
private weak Spit.Publishing.PluginHost host = null;
176
private WebAuthenticationPane web_auth_pane = null;
177
private Spit.Publishing.ProgressCallback progress_reporter = null;
178
private weak Spit.Publishing.Service service = null;
179
private bool running = false;
180
private GraphSession graph_session;
181
private PublishingOptionsPane? publishing_options_pane = null;
182
private Uploader? uploader = null;
183
private string? uid = null;
184
private string? username = null;
186
public FacebookPublisher (Spit.Publishing.Service service,
187
Spit.Publishing.PluginHost host) {
188
debug ("FacebookPublisher instantiated.");
190
this.service = service;
193
this.publishing_params = new PublishingParameters ();
195
this.graph_session = new GraphSession ();
196
graph_session.authenticated.connect (on_session_authenticated);
199
private bool is_persistent_session_valid () {
200
string? token = get_persistent_access_token ();
203
debug ("existing Facebook session found in configuration database (access_token = %s).",
206
debug ("no existing Facebook session available.");
208
return token != null;
211
private string? get_persistent_access_token () {
212
return host.get_config_string ("access_token", null);
215
private bool get_persistent_strip_metadata () {
216
return host.get_config_bool ("strip_metadata", false);
219
private void set_persistent_access_token (string access_token) {
220
host.set_config_string ("access_token", access_token);
223
private void set_persistent_strip_metadata (bool strip_metadata) {
224
host.set_config_bool ("strip_metadata", strip_metadata);
227
// Part of the fix for #3232. These have to be
228
// public so the legacy options pane may use them.
229
public int get_persistent_default_size () {
230
return host.get_config_int ("default_size", 0);
233
public void set_persistent_default_size (int size) {
234
host.set_config_int ("default_size", size);
237
private void invalidate_persistent_session () {
238
debug ("invalidating saved Facebook session.");
240
set_persistent_access_token ("");
243
private void do_show_service_welcome_pane () {
244
debug ("ACTION: showing service welcome pane.");
246
host.install_welcome_pane (SERVICE_WELCOME_MESSAGE, on_login_clicked);
247
host.set_service_locked (false);
250
private void do_test_connection_to_endpoint () {
251
debug ("ACTION: testing connection to Facebook endpoint.");
252
host.set_service_locked (true);
254
host.install_static_message_pane (_ ("Testing connection to Facebook..."));
256
GraphMessage endpoint_test_message = graph_session.new_endpoint_test ();
257
endpoint_test_message.completed.connect (on_endpoint_test_completed);
258
endpoint_test_message.failed.connect (on_endpoint_test_error);
260
graph_session.send_message (endpoint_test_message);
263
private void do_fetch_user_info () {
264
debug ("ACTION: fetching user information.");
266
host.set_service_locked (true);
267
host.install_account_fetch_wait_pane ();
269
GraphMessage user_info_message = graph_session.new_query ("/me");
271
user_info_message.completed.connect (on_fetch_user_info_completed);
272
user_info_message.failed.connect (on_fetch_user_info_error);
274
graph_session.send_message (user_info_message);
277
private void do_fetch_album_descriptions () {
278
debug ("ACTION: fetching album list.");
280
host.set_service_locked (true);
281
host.install_account_fetch_wait_pane ();
283
GraphMessage albums_message = graph_session.new_query ("/%s/albums".printf (uid));
285
albums_message.completed.connect (on_fetch_albums_completed);
286
albums_message.failed.connect (on_fetch_albums_error);
288
graph_session.send_message (albums_message);
291
private void do_extract_user_info_from_json (string json) {
292
debug ("ACTION: extracting user info from JSON response.");
295
Json.Parser parser = new Json.Parser ();
296
parser.load_from_data (json);
298
Json.Node root = parser.get_root ();
299
Json.Object response_object = root.get_object ();
300
uid = response_object.get_string_member ("id");
301
username = response_object.get_string_member ("name");
302
} catch (Error error) {
303
host.post_error (new Spit.Publishing.PublishingError.MALFORMED_RESPONSE (error.message));
307
on_user_info_extracted ();
310
private void do_extract_albums_from_json (string json) {
311
debug ("ACTION: extracting album info from JSON response.");
314
Json.Parser parser = new Json.Parser ();
315
parser.load_from_data (json);
317
Json.Node root = parser.get_root ();
318
Json.Object response_object = root.get_object ();
319
Json.Array album_list = response_object.get_array_member ("data");
321
publishing_params.albums = new Album[0];
323
for (int i = 0; i < album_list.get_length (); i++) {
324
Json.Object current_album = album_list.get_object_element (i);
325
string album_id = current_album.get_string_member ("id");
326
string album_name = current_album.get_string_member ("name");
328
// Note that we are completely ignoring the "can_upload" flag in the list of albums
329
// that we pulled from facebook eariler -- effectively, we add every album to the
330
// publishing_params album list regardless of the value of its can_upload flag. In
331
// the future we may wish to make adding to the publishing_params album list
332
// conditional on the value of the can_upload flag being true
333
publishing_params.add_album (album_name, album_id);
335
} catch (Error error) {
336
host.post_error (new Spit.Publishing.PublishingError.MALFORMED_RESPONSE (error.message));
340
on_albums_extracted ();
343
private void do_create_new_album () {
344
debug ("ACTION: creating a new album named \"%s\".\n", publishing_params.new_album_name);
346
host.set_service_locked (true);
347
host.install_static_message_pane (_ ("Creating album..."));
349
GraphMessage create_album_message = graph_session.new_create_album (
350
publishing_params.new_album_name, publishing_params.privacy_object);
352
create_album_message.completed.connect (on_create_album_completed);
353
create_album_message.failed.connect (on_create_album_error);
355
graph_session.send_message (create_album_message);
358
private void do_show_publishing_options_pane () {
359
debug ("ACTION: showing publishing options pane.");
361
host.set_service_locked (false);
362
Gtk.Builder builder = new Gtk.Builder ();
365
// the trailing get_path () is required, since add_from_file can't cope
366
// with File objects directly and expects a pathname instead.
367
builder.add_from_file (
368
host.get_module_file ().get_parent ().
369
get_child ("facebook_publishing_options_pane.glade").get_path ());
371
warning ("Could not parse UI file! Error: %s.", e.message);
373
new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR (
374
_ ("A file required for publishing is unavailable. Publishing to Facebook can't continue.")));
378
publishing_options_pane = new PublishingOptionsPane (username, publishing_params.albums,
379
host.get_publishable_media_type (), this, builder, get_persistent_strip_metadata ());
380
publishing_options_pane.logout.connect (on_publishing_options_pane_logout);
381
publishing_options_pane.publish.connect (on_publishing_options_pane_publish);
382
host.install_dialog_pane (publishing_options_pane,
383
Spit.Publishing.PluginHost.ButtonMode.CANCEL);
386
private void do_logout () {
387
debug ("ACTION: clearing persistent session information and restaring interaction.");
389
invalidate_persistent_session ();
395
private void do_add_new_local_album_from_json (string album_name, string json) {
397
Json.Parser parser = new Json.Parser ();
398
parser.load_from_data (json);
400
Json.Node root = parser.get_root ();
401
Json.Object response_object = root.get_object ();
402
string album_id = response_object.get_string_member ("id");
404
publishing_params.add_album (album_name, album_id);
405
} catch (Error error) {
406
host.post_error (new Spit.Publishing.PublishingError.MALFORMED_RESPONSE (error.message));
410
publishing_params.set_target_album_by_name (album_name);
414
private void do_hosted_web_authentication () {
415
debug ("ACTION: doing hosted web authentication.");
417
host.set_service_locked (false);
419
web_auth_pane = new WebAuthenticationPane ();
420
web_auth_pane.login_succeeded.connect (on_web_auth_pane_login_succeeded);
421
web_auth_pane.login_failed.connect (on_web_auth_pane_login_failed);
423
host.install_dialog_pane (web_auth_pane,
424
Spit.Publishing.PluginHost.ButtonMode.CANCEL);
428
private void do_authenticate_session (string good_login_uri) {
429
debug ("ACTION: preparing to extract session information encoded in uri = '%s'",
432
// the raw uri is percent-encoded, so decode it
433
string decoded_uri = Soup.URI.decode (good_login_uri);
435
// locate the access token within the URI
436
string? access_token = null;
437
int index = decoded_uri.index_of ("#access_token=");
439
access_token = decoded_uri[index:decoded_uri.length];
440
if (access_token == null) {
441
host.post_error (new Spit.Publishing.PublishingError.MALFORMED_RESPONSE (
442
"Server redirect URL contained no access token"));
446
// remove any trailing parameters from the session description string
447
string? trailing_params = null;
448
index = access_token.index_of_char ('&');
450
trailing_params = access_token[index:access_token.length];
451
if (trailing_params != null)
452
access_token = access_token.replace (trailing_params, "");
454
// remove the key from the session description string
455
access_token = access_token.replace ("#access_token=", "");
457
// we've got an access token!
458
graph_session.authenticated.connect (on_session_authenticated);
459
graph_session.authenticate (access_token);
462
private void do_save_session_information () {
463
debug ("ACTION: saving session information to configuration system.");
465
set_persistent_access_token (graph_session.get_access_token ());
468
private void do_upload () {
469
debug ("ACTION: uploading photos to album '%s'",
470
publishing_params.target_album == PublishingParameters.UNKNOWN_ALBUM ? "(none)" :
471
publishing_params.get_target_album_name ());
473
host.set_service_locked (true);
475
progress_reporter = host.serialize_publishables (publishing_params.resolution.get_pixels (),
476
publishing_params.strip_metadata);
478
// Serialization is a long and potentially cancellable operation, so before we use
479
// the publishables, make sure that the publishing interaction is still running. If it
480
// isn't the publishing environment may be partially torn down so do a short-circuit
485
Spit.Publishing.Publishable[] publishables = host.get_publishables ();
486
uploader = new Uploader (graph_session, publishing_params, publishables);
488
uploader.upload_complete.connect (on_upload_complete);
489
uploader.upload_error.connect (on_upload_error);
491
uploader.upload (on_upload_status_updated);
494
private void do_show_success_pane () {
495
debug ("ACTION: showing success pane.");
497
host.set_service_locked (false);
498
host.install_success_pane ();
501
private void on_generic_error (Spit.Publishing.PublishingError error) {
502
if (error is Spit.Publishing.PublishingError.EXPIRED_SESSION)
505
host.post_error (error);
508
private void on_login_clicked () {
512
debug ("EVENT: user clicked 'Login' on welcome pane.");
514
do_test_connection_to_endpoint ();
517
private void on_endpoint_test_completed (GraphMessage message) {
518
message.completed.disconnect (on_endpoint_test_completed);
519
message.failed.disconnect (on_endpoint_test_error);
524
debug ("EVENT: endpoint test transaction detected that the Facebook endpoint is alive.");
526
do_hosted_web_authentication ();
529
private void on_endpoint_test_error (GraphMessage message,
530
Spit.Publishing.PublishingError error) {
531
message.completed.disconnect (on_endpoint_test_completed);
532
message.failed.disconnect (on_endpoint_test_error);
537
debug ("EVENT: endpoint test transaction failed to detect a connection to the Facebook " +
540
on_generic_error (error);
543
private void on_web_auth_pane_login_succeeded (string success_url) {
547
debug ("EVENT: hosted web login succeeded.");
549
do_authenticate_session (success_url);
554
private void on_web_auth_pane_login_failed () {
558
debug ("EVENT: hosted web login failed.");
560
// In this case, "failed" doesn't mean that the user didn't enter the right username and
561
// password -- Facebook handles that case inside the Facebook Connect web control. Instead,
562
// it means that no session was initiated in response to our login request. The only
563
// way this happens is if the user clicks the "Cancel" button that appears inside
564
// the web control. In this case, the correct behavior is to return the user to the
565
// service welcome pane so that they can start the web interaction again.
566
do_show_service_welcome_pane ();
569
private void on_session_authenticated () {
570
graph_session.authenticated.disconnect (on_session_authenticated);
575
assert (graph_session.is_authenticated ());
576
debug ("EVENT: an authenticated session has become available.");
578
do_save_session_information ();
579
do_fetch_user_info ();
582
private void on_fetch_user_info_completed (GraphMessage message) {
583
message.completed.disconnect (on_fetch_user_info_completed);
584
message.failed.disconnect (on_fetch_user_info_error);
589
debug ("EVENT: user info fetch completed; response = '%s'.", message.get_response_body ());
591
do_extract_user_info_from_json (message.get_response_body ());
594
private void on_fetch_user_info_error (GraphMessage message,
595
Spit.Publishing.PublishingError error) {
596
message.completed.disconnect (on_fetch_user_info_completed);
597
message.failed.disconnect (on_fetch_user_info_error);
602
debug ("EVENT: fetching user info generated and error.");
604
on_generic_error (error);
607
private void on_user_info_extracted () {
611
debug ("EVENT: user info extracted from JSON response: uid = %s; name = %s.", uid, username);
613
do_fetch_album_descriptions ();
616
private void on_fetch_albums_completed (GraphMessage message) {
617
message.completed.disconnect (on_fetch_albums_completed);
618
message.failed.disconnect (on_fetch_albums_error);
623
debug ("EVENT: album descriptions fetch transaction completed; response = '%s'.",
624
message.get_response_body ());
626
do_extract_albums_from_json (message.get_response_body ());
629
private void on_fetch_albums_error (GraphMessage message,
630
Spit.Publishing.PublishingError err) {
631
message.completed.disconnect (on_fetch_albums_completed);
632
message.failed.disconnect (on_fetch_albums_error);
637
debug ("EVENT: album description fetch attempt generated an error.");
639
on_generic_error (err);
642
private void on_albums_extracted () {
646
debug ("EVENT: successfully extracted %d albums from JSON response",
647
publishing_params.albums.length);
649
do_show_publishing_options_pane ();
652
private void on_publishing_options_pane_logout () {
653
publishing_options_pane.publish.disconnect (on_publishing_options_pane_publish);
654
publishing_options_pane.logout.disconnect (on_publishing_options_pane_logout);
659
debug ("EVENT: user clicked 'Logout' in publishing options pane.");
664
private void on_publishing_options_pane_publish (string? target_album, string privacy_setting,
665
Resolution resolution, bool strip_metadata) {
666
publishing_options_pane.publish.disconnect (on_publishing_options_pane_publish);
667
publishing_options_pane.logout.disconnect (on_publishing_options_pane_logout);
672
debug ("EVENT: user clicked 'Publish' in publishing options pane.");
674
publishing_params.strip_metadata = strip_metadata;
675
set_persistent_strip_metadata (strip_metadata);
676
publishing_params.resolution = resolution;
677
set_persistent_default_size (resolution);
678
publishing_params.privacy_object = privacy_setting;
680
if (target_album != null) {
681
// we are publishing at least one photo so we need the name of an album to which
682
// we'll upload the photo(s)
683
publishing_params.set_target_album_by_name (target_album);
684
if (publishing_params.target_album != PublishingParameters.UNKNOWN_ALBUM) {
687
publishing_params.new_album_name = target_album;
688
do_create_new_album ();
691
// we're publishing only videos and we don't need an album name
696
private void on_create_album_completed (GraphMessage message) {
697
message.completed.disconnect (on_create_album_completed);
698
message.failed.disconnect (on_create_album_error);
700
assert (publishing_params.new_album_name != null);
705
debug ("EVENT: created new album resource on remote host; response body = %s.\n",
706
message.get_response_body ());
708
do_add_new_local_album_from_json (publishing_params.new_album_name,
709
message.get_response_body ());
712
private void on_create_album_error (GraphMessage message, Spit.Publishing.PublishingError err) {
713
message.completed.disconnect (on_create_album_completed);
714
message.failed.disconnect (on_create_album_error);
719
debug ("EVENT: attempt to create new album generated an error.");
721
on_generic_error (err);
724
private void on_upload_status_updated (int file_number, double completed_fraction) {
728
debug ("EVENT: uploader reports upload %.2f percent complete.", 100.0 * completed_fraction);
730
assert (progress_reporter != null);
732
progress_reporter (file_number, completed_fraction);
735
private void on_upload_complete (Uploader uploader, int num_published) {
736
uploader.upload_complete.disconnect (on_upload_complete);
737
uploader.upload_error.disconnect (on_upload_error);
742
debug ("EVENT: uploader reports upload complete; %d items published.", num_published);
744
do_show_success_pane ();
747
private void on_upload_error (Uploader uploader, Spit.Publishing.PublishingError err) {
748
uploader.upload_complete.disconnect (on_upload_complete);
749
uploader.upload_error.disconnect (on_upload_error);
754
debug ("EVENT: uploader reports upload error = '%s'.", err.message);
756
host.post_error (err);
759
public Spit.Publishing.Service get_service () {
763
public string get_service_name () {
767
public string get_user_visible_name () {
768
return USER_VISIBLE_NAME;
771
public void start () {
775
debug ("FacebookPublisher: starting interaction.");
779
// reset all publishing parameters to their default values -- in case this start is
780
// actually a restart
781
publishing_params = new PublishingParameters ();
783
// Do we have saved user credentials? If so, go ahead and authenticate the session
784
// with the saved credentials and proceed with the publishing interaction. Otherwise, show
786
if (is_persistent_session_valid ()) {
787
graph_session.authenticate (get_persistent_access_token ());
789
if (WebAuthenticationPane.is_cache_dirty ()) {
790
host.set_service_locked (false);
791
host.install_static_message_pane (RESTART_ERROR_MESSAGE,
792
Spit.Publishing.PluginHost.ButtonMode.CANCEL);
794
do_show_service_welcome_pane ();
799
public void stop () {
800
debug ("FacebookPublisher: stop( ) invoked.");
802
if (graph_session != null)
803
graph_session.stop_transactions ();
809
public bool is_running () {
814
internal class WebAuthenticationPane : Spit.Publishing.DialogPane, Object {
815
private WebKit.WebView webview = null;
816
private Gtk.Box pane_widget = null;
817
private Gtk.ScrolledWindow webview_frame = null;
818
private static bool cache_dirty = false;
820
public signal void login_succeeded (string success_url);
821
public signal void login_failed ();
823
public WebAuthenticationPane () {
824
pane_widget = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
826
webview_frame = new Gtk.ScrolledWindow (null, null);
827
webview_frame.set_shadow_type (Gtk.ShadowType.ETCHED_IN);
828
webview_frame.set_policy (Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
830
webview = new WebKit.WebView ();
831
webview.get_settings ().enable_plugins = false;
832
webview.get_settings ().enable_default_context_menu = false;
834
webview.load_finished.connect (on_page_load);
835
webview.load_started.connect (on_load_started);
837
webview_frame.add (webview);
838
pane_widget.pack_start (webview_frame, true, true, 0);
841
private class LocaleLookup {
842
public string prefix;
843
public string translation;
844
public string? exception_code;
845
public string? exception_translation;
846
public string? exception_code_2;
847
public string? exception_translation_2;
849
public LocaleLookup (string prefix, string translation, string? exception_code = null,
850
string? exception_translation = null, string? exception_code_2 = null,
851
string? exception_translation_2 = null) {
852
this.prefix = prefix;
853
this.translation = translation;
854
this.exception_code = exception_code;
855
this.exception_translation = exception_translation;
856
this.exception_code_2 = exception_code_2;
857
this.exception_translation_2 = exception_translation_2;
862
private LocaleLookup[] locale_lookup_table = {
863
new LocaleLookup ( "es", "es-la", "ES", "es-es" ),
864
new LocaleLookup ( "en", "en-gb", "US", "en-us" ),
865
new LocaleLookup ( "fr", "fr-fr", "CA", "fr-ca" ),
866
new LocaleLookup ( "pt", "pt-br", "PT", "pt-pt" ),
867
new LocaleLookup ( "zh", "zh-cn", "HK", "zh-hk", "TW", "zh-tw" ),
868
new LocaleLookup ( "af", "af-za" ),
869
new LocaleLookup ( "ar", "ar-ar" ),
870
new LocaleLookup ( "nb", "nb-no" ),
871
new LocaleLookup ( "no", "nb-no" ),
872
new LocaleLookup ( "id", "id-id" ),
873
new LocaleLookup ( "ms", "ms-my" ),
874
new LocaleLookup ( "ca", "ca-es" ),
875
new LocaleLookup ( "cs", "cs-cz" ),
876
new LocaleLookup ( "cy", "cy-gb" ),
877
new LocaleLookup ( "da", "da-dk" ),
878
new LocaleLookup ( "de", "de-de" ),
879
new LocaleLookup ( "tl", "tl-ph" ),
880
new LocaleLookup ( "ko", "ko-kr" ),
881
new LocaleLookup ( "hr", "hr-hr" ),
882
new LocaleLookup ( "it", "it-it" ),
883
new LocaleLookup ( "lt", "lt-lt" ),
884
new LocaleLookup ( "hu", "hu-hu" ),
885
new LocaleLookup ( "nl", "nl-nl" ),
886
new LocaleLookup ( "ja", "ja-jp" ),
887
new LocaleLookup ( "nb", "nb-no" ),
888
new LocaleLookup ( "no", "nb-no" ),
889
new LocaleLookup ( "pl", "pl-pl" ),
890
new LocaleLookup ( "ro", "ro-ro" ),
891
new LocaleLookup ( "ru", "ru-ru" ),
892
new LocaleLookup ( "sk", "sk-sk" ),
893
new LocaleLookup ( "sl", "sl-si" ),
894
new LocaleLookup ( "sv", "sv-se" ),
895
new LocaleLookup ( "th", "th-th" ),
896
new LocaleLookup ( "vi", "vi-vn" ),
897
new LocaleLookup ( "tr", "tr-tr" ),
898
new LocaleLookup ( "el", "el-gr" ),
899
new LocaleLookup ( "bg", "bg-bg" ),
900
new LocaleLookup ( "sr", "sr-rs" ),
901
new LocaleLookup ( "he", "he-il" ),
902
new LocaleLookup ( "hi", "hi-in" ),
903
new LocaleLookup ( "bn", "bn-in" ),
904
new LocaleLookup ( "pa", "pa-in" ),
905
new LocaleLookup ( "ta", "ta-in" ),
906
new LocaleLookup ( "te", "te-in" ),
907
new LocaleLookup ( "ml", "ml-in" )
910
private string get_system_locale_as_facebook_locale () {
911
unowned string? raw_system_locale = Intl.setlocale (LocaleCategory.ALL, "");
912
if (raw_system_locale == null || raw_system_locale == "")
915
string system_locale = raw_system_locale.split (".")[0];
917
foreach (LocaleLookup locale_lookup in locale_lookup_table) {
918
if (!system_locale.has_prefix (locale_lookup.prefix))
921
if (locale_lookup.exception_code != null) {
922
assert (locale_lookup.exception_translation != null);
924
if (system_locale.contains (locale_lookup.exception_code))
925
return locale_lookup.exception_translation;
928
if (locale_lookup.exception_code_2 != null) {
929
assert (locale_lookup.exception_translation_2 != null);
931
if (system_locale.contains (locale_lookup.exception_code_2))
932
return locale_lookup.exception_translation_2;
935
return locale_lookup.translation;
942
private string get_login_url () {
943
string facebook_locale = get_system_locale_as_facebook_locale ();
945
return "https://%s.facebook.com/dialog/oauth?client_id=%s&redirect_uri=https://www.facebook.com/connect/login_success.html&scope=publish_actions,user_photos,user_videos&response_type=token".printf (facebook_locale, APPLICATION_ID);
948
private void on_page_load (WebKit.WebFrame origin_frame) {
949
pane_widget.get_window ().set_cursor (new Gdk.Cursor (Gdk.CursorType.LEFT_PTR));
951
string loaded_url = origin_frame.get_uri ().dup ();
953
// strip parameters from the loaded url
954
if (loaded_url.contains ("?")) {
955
int index = loaded_url.index_of_char ('?');
956
string params = loaded_url[index:loaded_url.length];
957
loaded_url = loaded_url.replace (params, "");
960
// were we redirected to the facebook login success page?
961
if (loaded_url.contains ("login_success")) {
963
login_succeeded (origin_frame.get_uri ());
967
// were we redirected to the login total failure page?
968
if (loaded_url.contains ("login_failure")) {
974
private void on_load_started (WebKit.WebFrame frame) {
975
pane_widget.get_window ().set_cursor (new Gdk.Cursor (Gdk.CursorType.WATCH));
978
public static bool is_cache_dirty () {
982
public Gtk.Widget get_widget () {
986
public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry () {
987
return Spit.Publishing.DialogPane.GeometryOptions.NONE;
990
public void on_pane_installed () {
991
webview.open (get_login_url ());
994
public void on_pane_uninstalled () {
998
internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object {
999
private Gtk.Builder builder;
1000
private Gtk.Box pane_widget = null;
1001
private Gtk.RadioButton use_existing_radio = null;
1002
private Gtk.RadioButton create_new_radio = null;
1003
private Gtk.ComboBoxText existing_albums_combo = null;
1004
private Gtk.ComboBoxText visibility_combo = null;
1005
private Gtk.Entry new_album_entry = null;
1006
private Gtk.CheckButton strip_metadata_check = null;
1007
private Gtk.Button publish_button = null;
1008
private Gtk.Button logout_button = null;
1009
private Gtk.Label how_to_label = null;
1010
private Album[] albums = null;
1011
private FacebookPublisher publisher = null;
1012
private PrivacyDescription[] privacy_descriptions;
1014
private Resolution[] possible_resolutions;
1015
private Gtk.ComboBoxText resolution_combo = null;
1017
private Spit.Publishing.Publisher.MediaType media_type;
1019
private const string HEADER_LABEL_TEXT = _ ("You are logged into Facebook as %s.\n\n");
1020
private const string PHOTOS_LABEL_TEXT = _ ("Where would you like to publish the selected photos?");
1021
private const string RESOLUTION_LABEL_TEXT = _ ("Upload _size:");
1022
private const int CONTENT_GROUP_SPACING = 32;
1023
private const int STANDARD_ACTION_BUTTON_WIDTH = 128;
1025
public signal void logout ();
1026
public signal void publish (string? target_album, string privacy_setting,
1027
Resolution target_resolution, bool strip_metadata);
1029
private class PrivacyDescription {
1030
public string description;
1031
public string privacy_setting;
1033
public PrivacyDescription (string description, string privacy_setting) {
1034
this.description = description;
1035
this.privacy_setting = privacy_setting;
1039
public PublishingOptionsPane (string username, Album[] albums,
1040
Spit.Publishing.Publisher.MediaType media_type, FacebookPublisher publisher,
1041
Gtk.Builder builder, bool strip_metadata) {
1043
this.builder = builder;
1044
assert (builder != null);
1045
assert (builder.get_objects ().length () > 0);
1047
this.albums = albums;
1048
this.privacy_descriptions = create_privacy_descriptions ();
1050
this.possible_resolutions = create_resolution_list ();
1051
this.publisher = publisher;
1053
// we'll need to know if the user is importing video or not when sorting out visibility.
1054
this.media_type = media_type;
1056
pane_widget = (Gtk.Box) builder.get_object ("facebook_pane_box");
1057
pane_widget.set_border_width (16);
1059
use_existing_radio = (Gtk.RadioButton) this.builder.get_object ("use_existing_radio");
1060
create_new_radio = (Gtk.RadioButton) this.builder.get_object ("create_new_radio");
1061
existing_albums_combo = (Gtk.ComboBoxText) this.builder.get_object ("existing_albums_combo");
1062
visibility_combo = (Gtk.ComboBoxText) this.builder.get_object ("visibility_combo");
1063
publish_button = (Gtk.Button) this.builder.get_object ("publish_button");
1064
logout_button = (Gtk.Button) this.builder.get_object ("logout_button");
1065
new_album_entry = (Gtk.Entry) this.builder.get_object ("new_album_entry");
1066
resolution_combo = (Gtk.ComboBoxText) this.builder.get_object ("resolution_combo");
1067
how_to_label = (Gtk.Label) this.builder.get_object ("how_to_label");
1068
strip_metadata_check = (Gtk.CheckButton) this.builder.get_object ("strip_metadata_check");
1070
create_new_radio.clicked.connect (on_create_new_toggled);
1071
use_existing_radio.clicked.connect (on_use_existing_toggled);
1073
string label_text = HEADER_LABEL_TEXT.printf (username);
1074
if ((media_type & Spit.Publishing.Publisher.MediaType.PHOTO) != 0)
1075
label_text += PHOTOS_LABEL_TEXT;
1076
how_to_label.set_label (label_text);
1077
strip_metadata_check.set_active (strip_metadata);
1079
setup_visibility_combo ();
1080
visibility_combo.set_active (0);
1082
publish_button.clicked.connect (on_publish_button_clicked);
1083
logout_button.clicked.connect (on_logout_button_clicked);
1085
setup_resolution_combo ();
1086
resolution_combo.set_active (publisher.get_persistent_default_size ());
1087
resolution_combo.changed.connect (on_size_changed);
1089
// Ticket #3175, part 2: make sure this widget starts out sensitive
1090
// if it needs to by checking whether we're starting with a video
1091
// or a new gallery.
1092
visibility_combo.set_sensitive (
1093
(create_new_radio != null && create_new_radio.active) ||
1094
((media_type & Spit.Publishing.Publisher.MediaType.VIDEO) != 0));
1096
// if publishing only videos, disable all photo-specific controls
1097
if (media_type == Spit.Publishing.Publisher.MediaType.VIDEO) {
1098
strip_metadata_check.set_active (false);
1099
strip_metadata_check.set_sensitive (false);
1100
resolution_combo.set_sensitive (false);
1101
use_existing_radio.set_sensitive (false);
1102
create_new_radio.set_sensitive (false);
1103
existing_albums_combo.set_sensitive (false);
1104
new_album_entry.set_sensitive (false);
1108
private bool publishing_photos () {
1109
return (media_type & Spit.Publishing.Publisher.MediaType.PHOTO) != 0;
1112
private void setup_visibility_combo () {
1113
foreach (PrivacyDescription p in privacy_descriptions)
1114
visibility_combo.append_text (p.description);
1117
private void setup_resolution_combo () {
1118
foreach (Resolution res in possible_resolutions)
1119
resolution_combo.append_text (res.get_name ());
1122
private void on_use_existing_toggled () {
1123
if (use_existing_radio.active) {
1124
existing_albums_combo.set_sensitive (true);
1125
new_album_entry.set_sensitive (false);
1127
// Ticket #3175 - if we're not adding a new gallery
1128
// or a video, then we shouldn't be allowed tof
1129
// choose visibility, since it has no effect.
1130
visibility_combo.set_sensitive ((media_type & Spit.Publishing.Publisher.MediaType.VIDEO) != 0);
1132
existing_albums_combo.grab_focus ();
1136
private void on_create_new_toggled () {
1137
if (create_new_radio.active) {
1138
existing_albums_combo.set_sensitive (false);
1139
new_album_entry.set_sensitive (true);
1140
new_album_entry.grab_focus ();
1142
// Ticket #3175 - if we're creating a new gallery, make sure this is
1143
// active, since it may have possibly been set inactive.
1144
visibility_combo.set_sensitive (true);
1148
private void on_size_changed () {
1149
publisher.set_persistent_default_size (resolution_combo.get_active ());
1152
private void on_logout_button_clicked () {
1156
private void on_publish_button_clicked () {
1158
string privacy_setting = privacy_descriptions[visibility_combo.get_active ()].privacy_setting;
1160
Resolution resolution_setting;
1162
if (publishing_photos ()) {
1163
resolution_setting = possible_resolutions[resolution_combo.get_active ()];
1164
if (use_existing_radio.active) {
1165
album_name = existing_albums_combo.get_active_text ();
1167
album_name = new_album_entry.get_text ();
1170
resolution_setting = Resolution.STANDARD;
1174
publish (album_name, privacy_setting, resolution_setting, strip_metadata_check.get_active ());
1177
private PrivacyDescription[] create_privacy_descriptions () {
1178
PrivacyDescription[] result = new PrivacyDescription[0];
1180
result += new PrivacyDescription (_ ("Just me"), "{ 'value' : 'SELF' }");
1181
result += new PrivacyDescription (_ ("Friends"), "{ 'value' : 'ALL_FRIENDS' }");
1182
result += new PrivacyDescription (_ ("Everyone"), "{ 'value' : 'EVERYONE' }");
1187
private Resolution[] create_resolution_list () {
1188
Resolution[] result = new Resolution[0];
1190
result += Resolution.STANDARD;
1191
result += Resolution.HIGH;
1196
public void installed () {
1197
if (publishing_photos ()) {
1198
if (albums.length == 0) {
1199
create_new_radio.set_active (true);
1200
new_album_entry.set_text (DEFAULT_ALBUM_NAME);
1201
existing_albums_combo.set_sensitive (false);
1202
use_existing_radio.set_sensitive (false);
1204
int default_album_seq_num = -1;
1206
foreach (Album album in albums) {
1207
existing_albums_combo.append_text (album.name);
1208
if (album.name == DEFAULT_ALBUM_NAME)
1209
default_album_seq_num = ticker;
1212
if (default_album_seq_num != -1) {
1213
existing_albums_combo.set_active (default_album_seq_num);
1214
use_existing_radio.set_active (true);
1215
new_album_entry.set_sensitive (false);
1217
create_new_radio.set_active (true);
1218
existing_albums_combo.set_active (0);
1219
existing_albums_combo.set_sensitive (false);
1220
new_album_entry.set_text (DEFAULT_ALBUM_NAME);
1225
publish_button.grab_focus ();
1228
private void notify_logout () {
1232
private void notify_publish (string? target_album, string privacy_setting, Resolution target_resolution) {
1233
publish (target_album, privacy_setting, target_resolution, strip_metadata_check.get_active ());
1236
public Gtk.Widget get_widget () {
1240
public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry () {
1241
return Spit.Publishing.DialogPane.GeometryOptions.NONE;
1244
public void on_pane_installed () {
1245
logout.connect (notify_logout);
1246
publish.connect (notify_publish);
1251
public void on_pane_uninstalled () {
1252
logout.disconnect (notify_logout);
1253
publish.disconnect (notify_publish);
1257
internal enum Endpoint {
1262
public string to_uri () {
1265
return "https://graph.facebook.com/";
1268
return "https://graph-video.facebook.com/";
1270
case TEST_CONNECTION:
1271
return "https://www.facebook.com/";
1274
assert_not_reached ();
1279
internal abstract class GraphMessage {
1280
public signal void completed ();
1281
public signal void failed (Spit.Publishing.PublishingError err);
1282
public signal void data_transmitted (int bytes_sent_so_far, int total_bytes);
1284
public abstract string get_uri ();
1285
public abstract string get_response_body ();
1288
internal class GraphSession {
1289
private abstract class GraphMessageImpl : GraphMessage {
1290
public Publishing.RESTSupport.HttpMethod method;
1292
public string access_token;
1293
public Soup.Message soup_message;
1294
public weak GraphSession host_session;
1295
public int bytes_so_far;
1297
public GraphMessageImpl (GraphSession host_session, Publishing.RESTSupport.HttpMethod method,
1298
string relative_uri, string access_token, Endpoint endpoint = Endpoint.DEFAULT) {
1299
this.method = method;
1300
this.access_token = access_token;
1301
this.host_session = host_session;
1302
this.bytes_so_far = 0;
1304
string endpoint_uri = endpoint.to_uri ();
1306
Regex starting_slashes = new Regex ("^/+");
1307
this.uri = endpoint_uri + starting_slashes.replace (relative_uri, -1, 0, "");
1308
} catch (RegexError err) {
1309
assert_not_reached ();
1313
public virtual bool prepare_for_transmission () {
1317
public override string get_uri () {
1321
public override string get_response_body () {
1322
return (string) soup_message.response_body.data;
1325
public void on_wrote_body_data (Soup.Buffer chunk) {
1326
bytes_so_far += (int) chunk.length;
1328
data_transmitted (bytes_so_far, (int) soup_message.request_body.length);
1332
private class GraphQueryMessage : GraphMessageImpl {
1333
public GraphQueryMessage (GraphSession host_session, string relative_uri,
1334
string access_token) {
1335
base (host_session, Publishing.RESTSupport.HttpMethod.GET, relative_uri, access_token);
1337
Soup.URI destination_uri = new Soup.URI (uri + "?access_token=" + access_token);
1338
soup_message = new Soup.Message.from_uri (method.to_string (), destination_uri);
1339
soup_message.wrote_body_data.connect (on_wrote_body_data);
1343
private class GraphEndpointProbeMessage : GraphMessageImpl {
1344
public GraphEndpointProbeMessage (GraphSession host_session) {
1345
base (host_session, Publishing.RESTSupport.HttpMethod.GET, "/", "",
1346
Endpoint.TEST_CONNECTION);
1348
soup_message = new Soup.Message.from_uri (method.to_string (), new Soup.URI (uri));
1349
soup_message.wrote_body_data.connect (on_wrote_body_data);
1353
private class GraphUploadMessage : GraphMessageImpl {
1354
private MappedFile mapped_file = null;
1355
private Spit.Publishing.Publishable publishable;
1357
public GraphUploadMessage (GraphSession host_session, string access_token,
1358
string relative_uri, Spit.Publishing.Publishable publishable,
1359
bool suppress_titling, string? resource_privacy = null) {
1360
base (host_session, Publishing.RESTSupport.HttpMethod.POST, relative_uri, access_token,
1361
(publishable.get_media_type () == Spit.Publishing.Publisher.MediaType.VIDEO) ?
1362
Endpoint.VIDEO : Endpoint.DEFAULT);
1364
// Video uploads require a privacy string at the per-resource level. Since they aren't
1365
// placed in albums, they can't inherit their privacy settings from their containing
1366
// album like photos do
1367
assert (publishable.get_media_type () != Spit.Publishing.Publisher.MediaType.VIDEO ||
1368
resource_privacy != null);
1370
this.publishable = publishable;
1372
// attempt to map the binary payload from disk into memory
1374
this.mapped_file = new MappedFile (publishable.get_serialized_file ().get_path (),
1376
} catch (FileError e) {
1380
this.soup_message = new Soup.Message.from_uri (method.to_string (), new Soup.URI (uri));
1381
soup_message.wrote_body_data.connect (on_wrote_body_data);
1383
unowned uint8[] payload = (uint8[]) mapped_file.get_contents ();
1384
payload.length = (int) mapped_file.get_length ();
1386
Soup.Buffer image_data = new Soup.Buffer (Soup.MemoryUse.TEMPORARY, payload);
1388
Soup.Multipart mp_envelope = new Soup.Multipart ("multipart/form-data");
1390
mp_envelope.append_form_string ("access_token", access_token);
1392
if (publishable.get_media_type () == Spit.Publishing.Publisher.MediaType.VIDEO)
1393
mp_envelope.append_form_string ("privacy", resource_privacy);
1395
// get photo title and post it as message on FB API
1396
string publishable_title = publishable.get_param_string("title");
1397
if (!suppress_titling && publishable_title != null)
1398
mp_envelope.append_form_string ("name", publishable_title);
1400
// set 'message' data field with EXIF comment field. Title has precedence.
1401
string publishable_comment = publishable.get_param_string("comment");
1402
if (!suppress_titling && publishable_comment != null)
1403
mp_envelope.append_form_string("message", publishable_comment);
1405
// set correct date of the picture
1406
if (!suppress_titling)
1407
mp_envelope.append_form_string("backdated_time", publishable.get_exposure_date_time().to_string());
1409
string source_file_mime_type =
1410
(publishable.get_media_type () == Spit.Publishing.Publisher.MediaType.VIDEO) ?
1411
"video" : "image/jpeg";
1412
mp_envelope.append_form_file ("source", publishable.get_serialized_file ().get_basename (),
1413
source_file_mime_type, image_data);
1415
mp_envelope.to_message (soup_message.request_headers, soup_message.request_body);
1418
public override bool prepare_for_transmission () {
1419
if (mapped_file == null) {
1420
failed (new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR (
1421
"File %s is unavailable.".printf (publishable.get_serialized_file ().get_path ())));
1429
private class GraphCreateAlbumMessage : GraphMessageImpl {
1430
public GraphCreateAlbumMessage (GraphSession host_session, string access_token,
1431
string album_name, string album_privacy) {
1432
base (host_session, Publishing.RESTSupport.HttpMethod.POST, "/me/albums", access_token);
1434
assert (album_privacy != null && album_privacy != "");
1436
this.soup_message = new Soup.Message.from_uri (method.to_string (), new Soup.URI (uri));
1438
Soup.Multipart mp_envelope = new Soup.Multipart ("multipart/form-data");
1440
mp_envelope.append_form_string ("access_token", access_token);
1441
mp_envelope.append_form_string ("name", album_name);
1442
mp_envelope.append_form_string ("privacy", album_privacy);
1444
mp_envelope.to_message (soup_message.request_headers, soup_message.request_body);
1448
public signal void authenticated ();
1450
private Soup.Session soup_session;
1451
private string? access_token;
1452
private GraphMessage? current_message;
1454
public GraphSession () {
1455
this.soup_session = new Soup.SessionAsync ();
1456
this.soup_session.request_unqueued.connect (on_request_unqueued);
1457
this.soup_session.timeout = 15;
1458
this.access_token = null;
1459
this.current_message = null;
1463
soup_session.request_unqueued.disconnect (on_request_unqueued);
1466
private void manage_message (GraphMessage msg) {
1467
assert (current_message == null);
1469
current_message = msg;
1472
private void unmanage_message (GraphMessage msg) {
1473
assert (current_message != null);
1475
current_message = null;
1478
private void on_request_unqueued (Soup.Message msg) {
1479
assert (current_message != null);
1480
GraphMessageImpl real_message = (GraphMessageImpl) current_message;
1481
assert (real_message.soup_message == msg);
1483
// these error types are always recoverable given the unique behavior of the Facebook
1484
// endpoint, so try again
1485
if (msg.status_code == Soup.KnownStatusCode.IO_ERROR ||
1486
msg.status_code == Soup.KnownStatusCode.MALFORMED ||
1487
msg.status_code == Soup.KnownStatusCode.TRY_AGAIN) {
1488
real_message.bytes_so_far = 0;
1489
soup_session.queue_message (msg, null);
1493
unmanage_message (real_message);
1494
msg.wrote_body_data.disconnect (real_message.on_wrote_body_data);
1496
Spit.Publishing.PublishingError? error = null;
1497
switch (msg.status_code) {
1498
case Soup.KnownStatusCode.OK:
1499
case Soup.KnownStatusCode.CREATED: // HTTP code 201 (CREATED) signals that a new
1500
// resource was created in response to a PUT
1504
case EXPIRED_SESSION_STATUS_CODE:
1505
error = new Spit.Publishing.PublishingError.EXPIRED_SESSION (
1506
"OAuth Access Token has Expired. Logout user.");
1509
case Soup.KnownStatusCode.CANT_RESOLVE:
1510
case Soup.KnownStatusCode.CANT_RESOLVE_PROXY:
1511
error = new Spit.Publishing.PublishingError.NO_ANSWER (
1512
"Unable to resolve %s (error code %u)", real_message.get_uri (), msg.status_code);
1515
case Soup.KnownStatusCode.CANT_CONNECT:
1516
case Soup.KnownStatusCode.CANT_CONNECT_PROXY:
1517
error = new Spit.Publishing.PublishingError.NO_ANSWER (
1518
"Unable to connect to %s (error code %u)", real_message.get_uri (), msg.status_code);
1522
// status codes below 100 are used by Soup, 100 and above are defined HTTP
1524
if (msg.status_code >= 100) {
1525
error = new Spit.Publishing.PublishingError.NO_ANSWER (
1526
"Service %s returned HTTP status code %u %s", real_message.get_uri (),
1527
msg.status_code, msg.reason_phrase);
1529
error = new Spit.Publishing.PublishingError.NO_ANSWER (
1530
"Failure communicating with %s (error code %u)", real_message.get_uri (),
1536
// All valid communication with Facebook involves body data in the response
1538
if (msg.response_body.data == null || msg.response_body.data.length == 0)
1539
error = new Spit.Publishing.PublishingError.MALFORMED_RESPONSE (
1540
"No response data from %s", real_message.get_uri ());
1543
real_message.completed ();
1545
real_message.failed (error);
1548
public void authenticate (string access_token) {
1549
this.access_token = access_token;
1553
public bool is_authenticated () {
1554
return access_token != null;
1557
public string get_access_token () {
1558
assert (is_authenticated ());
1559
return access_token;
1562
public GraphMessage new_endpoint_test () {
1563
return new GraphEndpointProbeMessage (this);
1566
public GraphMessage new_query (string resource_path) {
1567
return new GraphQueryMessage (this, resource_path, access_token);
1570
public GraphMessage new_upload (string resource_path, Spit.Publishing.Publishable publishable,
1571
bool suppress_titling, string? resource_privacy = null) {
1572
return new GraphUploadMessage (this, access_token, resource_path, publishable,
1573
suppress_titling, resource_privacy);
1576
public GraphMessage new_create_album (string album_name, string privacy) {
1577
return new GraphSession.GraphCreateAlbumMessage (this, access_token, album_name, privacy);
1580
public void send_message (GraphMessage message) {
1581
GraphMessageImpl real_message = (GraphMessageImpl) message;
1583
debug ("making HTTP request to URI: " + real_message.soup_message.uri.to_string (false));
1585
if (real_message.prepare_for_transmission ()) {
1586
manage_message (message);
1587
soup_session.queue_message (real_message.soup_message, null);
1591
public void stop_transactions () {
1592
soup_session.abort ();
1596
internal class Uploader {
1597
private int current_file;
1598
private Spit.Publishing.Publishable[] publishables;
1599
private GraphSession session;
1600
private PublishingParameters publishing_params;
1601
private unowned Spit.Publishing.ProgressCallback? status_updated = null;
1603
public signal void upload_complete (int num_photos_published);
1604
public signal void upload_error (Spit.Publishing.PublishingError err);
1606
public Uploader (GraphSession session, PublishingParameters publishing_params,
1607
Spit.Publishing.Publishable[] publishables) {
1608
this.current_file = 0;
1609
this.publishables = publishables;
1610
this.session = session;
1611
this.publishing_params = publishing_params;
1614
private void send_current_file () {
1615
Spit.Publishing.Publishable publishable = publishables[current_file];
1616
GLib.File? file = publishable.get_serialized_file ();
1618
// if the current publishable hasn't been serialized, then skip it
1624
string resource_uri =
1625
(publishable.get_media_type () == Spit.Publishing.Publisher.MediaType.PHOTO) ?
1626
"/%s/photos".printf (publishing_params.get_target_album_id ()) : "/me/videos";
1627
string? resource_privacy =
1628
(publishable.get_media_type () == Spit.Publishing.Publisher.MediaType.VIDEO) ?
1629
publishing_params.privacy_object : null;
1630
GraphMessage upload_message = session.new_upload (resource_uri, publishable,
1631
publishing_params.strip_metadata, resource_privacy);
1633
upload_message.data_transmitted.connect (on_chunk_transmitted);
1634
upload_message.completed.connect (on_message_completed);
1635
upload_message.failed.connect (on_message_failed);
1637
session.send_message (upload_message);
1640
private void send_files () {
1642
send_current_file ();
1645
private void on_chunk_transmitted (int bytes_written_so_far, int total_bytes) {
1646
double file_span = 1.0 / publishables.length;
1647
double this_file_fraction_complete = ((double) bytes_written_so_far) / total_bytes;
1648
double fraction_complete = (current_file * file_span) + (this_file_fraction_complete *
1651
if (status_updated != null)
1652
status_updated (current_file + 1, fraction_complete);
1655
private void on_message_completed (GraphMessage message) {
1656
message.data_transmitted.disconnect (on_chunk_transmitted);
1657
message.completed.disconnect (on_message_completed);
1658
message.failed.disconnect (on_message_failed);
1661
if (current_file < publishables.length) {
1662
send_current_file ();
1664
upload_complete (current_file);
1668
private void on_message_failed (GraphMessage message, Spit.Publishing.PublishingError error) {
1669
message.data_transmitted.disconnect (on_chunk_transmitted);
1670
message.completed.disconnect (on_message_completed);
1671
message.failed.disconnect (on_message_failed);
1673
upload_error (error);
1676
public void upload (Spit.Publishing.ProgressCallback? status_updated = null) {
1677
this.status_updated = status_updated;
1679
if (publishables.length > 0)