~elementary-apps/pantheon-photos/master

« back to all changes in this revision

Viewing changes to plugins/shotwell-publishing/FacebookPublishing.vala

  • Committer: RabbitBot
  • Author(s): Corentin Noël
  • Date: 2016-01-14 14:48:54 UTC
  • mfrom: (2879.1.3)
  • Revision ID: git-v1:938bcfdd68fed20a4d47ccfceeaa15f75f7eff12
Ported to CMake. Removed very old retro-compatibility.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
/* Copyright 2009-2013 Yorba Foundation
2
 
 *
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.
5
 
 */
6
 
 
7
 
public class FacebookService : Object, Spit.Pluggable, Spit.Publishing.Service {
8
 
    private const string ICON_FILENAME = "facebook.png";
9
 
 
10
 
    private static Gdk.Pixbuf[] icon_pixbuf_set = null;
11
 
 
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));
15
 
    }
16
 
 
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);
20
 
    }
21
 
 
22
 
    public unowned string get_id () {
23
 
        return "org.pantheon.photos.publishing.facebook";
24
 
    }
25
 
 
26
 
    public unowned string get_pluggable_name () {
27
 
        return "Facebook";
28
 
    }
29
 
 
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;
40
 
    }
41
 
 
42
 
    public void activation (bool enabled) {
43
 
    }
44
 
 
45
 
    public Spit.Publishing.Publisher create_publisher (Spit.Publishing.PluginHost host) {
46
 
        return new Publishing.Facebook.FacebookPublisher (this, host);
47
 
    }
48
 
 
49
 
    public Spit.Publishing.Publisher.MediaType get_supported_media () {
50
 
        return (Spit.Publishing.Publisher.MediaType.PHOTO |
51
 
                Spit.Publishing.Publisher.MediaType.VIDEO);
52
 
    }
53
 
}
54
 
 
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;
68
 
 
69
 
internal class Album {
70
 
    public string name;
71
 
    public string id;
72
 
 
73
 
    public Album (string name, string id) {
74
 
        this.name = name;
75
 
        this.id = id;
76
 
    }
77
 
}
78
 
 
79
 
internal enum Resolution {
80
 
    STANDARD,
81
 
    HIGH;
82
 
 
83
 
    public string get_name () {
84
 
        switch (this) {
85
 
        case STANDARD:
86
 
            return _ ("Standard (720 pixels)");
87
 
 
88
 
        case HIGH:
89
 
            return _ ("Large (2048 pixels)");
90
 
 
91
 
        default:
92
 
            error ("Unknown resolution %s", this.to_string ());
93
 
        }
94
 
    }
95
 
 
96
 
    public int get_pixels () {
97
 
        switch (this) {
98
 
        case STANDARD:
99
 
            return 720;
100
 
 
101
 
        case HIGH:
102
 
            return 2048;
103
 
 
104
 
        default:
105
 
            error ("Unknown resolution %s", this.to_string ());
106
 
        }
107
 
    }
108
 
}
109
 
 
110
 
internal class PublishingParameters {
111
 
    public const int UNKNOWN_ALBUM = -1;
112
 
 
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
118
 
    // album
119
 
 
120
 
    public string? privacy_object;  // a serialized JSON object encoding the privacy settings of the
121
 
    // published resources
122
 
    public Resolution resolution;
123
 
 
124
 
    public PublishingParameters () {
125
 
        this.albums = null;
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;
131
 
    }
132
 
 
133
 
    public void add_album (string name, string id) {
134
 
        if (albums == null)
135
 
            albums = new Album[0];
136
 
 
137
 
        Album new_album = new Album (name, id);
138
 
        albums += new_album;
139
 
    }
140
 
 
141
 
    public void set_target_album_by_name (string? name) {
142
 
        if (name == null) {
143
 
            target_album = UNKNOWN_ALBUM;
144
 
            return;
145
 
        }
146
 
 
147
 
        for (int i = 0; i < albums.length; i++) {
148
 
 
149
 
            if (albums[i].name == name) {
150
 
                target_album = i;
151
 
                return;
152
 
            }
153
 
        }
154
 
 
155
 
        target_album = UNKNOWN_ALBUM;
156
 
    }
157
 
 
158
 
    public string? get_target_album_name () {
159
 
        if (albums == null || target_album == UNKNOWN_ALBUM)
160
 
            return null;
161
 
 
162
 
        return albums[target_album].name;
163
 
    }
164
 
 
165
 
    public string? get_target_album_id () {
166
 
        if (albums == null || target_album == UNKNOWN_ALBUM)
167
 
            return null;
168
 
 
169
 
        return albums[target_album].id;
170
 
    }
171
 
}
172
 
 
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;
185
 
 
186
 
    public FacebookPublisher (Spit.Publishing.Service service,
187
 
                              Spit.Publishing.PluginHost host) {
188
 
        debug ("FacebookPublisher instantiated.");
189
 
 
190
 
        this.service = service;
191
 
        this.host = host;
192
 
 
193
 
        this.publishing_params = new PublishingParameters ();
194
 
 
195
 
        this.graph_session = new GraphSession ();
196
 
        graph_session.authenticated.connect (on_session_authenticated);
197
 
    }
198
 
 
199
 
    private bool is_persistent_session_valid () {
200
 
        string? token = get_persistent_access_token ();
201
 
 
202
 
        if (token != null)
203
 
            debug ("existing Facebook session found in configuration database (access_token = %s).",
204
 
                   token);
205
 
        else
206
 
            debug ("no existing Facebook session available.");
207
 
 
208
 
        return token != null;
209
 
    }
210
 
 
211
 
    private string? get_persistent_access_token () {
212
 
        return host.get_config_string ("access_token", null);
213
 
    }
214
 
 
215
 
    private bool get_persistent_strip_metadata () {
216
 
        return host.get_config_bool ("strip_metadata", false);
217
 
    }
218
 
 
219
 
    private void set_persistent_access_token (string access_token) {
220
 
        host.set_config_string ("access_token", access_token);
221
 
    }
222
 
 
223
 
    private void set_persistent_strip_metadata (bool strip_metadata) {
224
 
        host.set_config_bool ("strip_metadata", strip_metadata);
225
 
    }
226
 
 
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);
231
 
    }
232
 
 
233
 
    public void set_persistent_default_size (int size) {
234
 
        host.set_config_int ("default_size", size);
235
 
    }
236
 
 
237
 
    private void invalidate_persistent_session () {
238
 
        debug ("invalidating saved Facebook session.");
239
 
 
240
 
        set_persistent_access_token ("");
241
 
    }
242
 
 
243
 
    private void do_show_service_welcome_pane () {
244
 
        debug ("ACTION: showing service welcome pane.");
245
 
 
246
 
        host.install_welcome_pane (SERVICE_WELCOME_MESSAGE, on_login_clicked);
247
 
        host.set_service_locked (false);
248
 
    }
249
 
 
250
 
    private void do_test_connection_to_endpoint () {
251
 
        debug ("ACTION: testing connection to Facebook endpoint.");
252
 
        host.set_service_locked (true);
253
 
 
254
 
        host.install_static_message_pane (_ ("Testing connection to Facebook..."));
255
 
 
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);
259
 
 
260
 
        graph_session.send_message (endpoint_test_message);
261
 
    }
262
 
 
263
 
    private void do_fetch_user_info () {
264
 
        debug ("ACTION: fetching user information.");
265
 
 
266
 
        host.set_service_locked (true);
267
 
        host.install_account_fetch_wait_pane ();
268
 
 
269
 
        GraphMessage user_info_message = graph_session.new_query ("/me");
270
 
 
271
 
        user_info_message.completed.connect (on_fetch_user_info_completed);
272
 
        user_info_message.failed.connect (on_fetch_user_info_error);
273
 
 
274
 
        graph_session.send_message (user_info_message);
275
 
    }
276
 
 
277
 
    private void do_fetch_album_descriptions () {
278
 
        debug ("ACTION: fetching album list.");
279
 
 
280
 
        host.set_service_locked (true);
281
 
        host.install_account_fetch_wait_pane ();
282
 
 
283
 
        GraphMessage albums_message = graph_session.new_query ("/%s/albums".printf (uid));
284
 
 
285
 
        albums_message.completed.connect (on_fetch_albums_completed);
286
 
        albums_message.failed.connect (on_fetch_albums_error);
287
 
 
288
 
        graph_session.send_message (albums_message);
289
 
    }
290
 
 
291
 
    private void do_extract_user_info_from_json (string json) {
292
 
        debug ("ACTION: extracting user info from JSON response.");
293
 
 
294
 
        try {
295
 
            Json.Parser parser = new Json.Parser ();
296
 
            parser.load_from_data (json);
297
 
 
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));
304
 
            return;
305
 
        }
306
 
 
307
 
        on_user_info_extracted ();
308
 
    }
309
 
 
310
 
    private void do_extract_albums_from_json (string json) {
311
 
        debug ("ACTION: extracting album info from JSON response.");
312
 
 
313
 
        try {
314
 
            Json.Parser parser = new Json.Parser ();
315
 
            parser.load_from_data (json);
316
 
 
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");
320
 
 
321
 
            publishing_params.albums = new Album[0];
322
 
 
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");
327
 
 
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);
334
 
            }
335
 
        } catch (Error error) {
336
 
            host.post_error (new Spit.Publishing.PublishingError.MALFORMED_RESPONSE (error.message));
337
 
            return;
338
 
        }
339
 
 
340
 
        on_albums_extracted ();
341
 
    }
342
 
 
343
 
    private void do_create_new_album () {
344
 
        debug ("ACTION: creating a new album named \"%s\".\n", publishing_params.new_album_name);
345
 
 
346
 
        host.set_service_locked (true);
347
 
        host.install_static_message_pane (_ ("Creating album..."));
348
 
 
349
 
        GraphMessage create_album_message = graph_session.new_create_album (
350
 
                                                publishing_params.new_album_name, publishing_params.privacy_object);
351
 
 
352
 
        create_album_message.completed.connect (on_create_album_completed);
353
 
        create_album_message.failed.connect (on_create_album_error);
354
 
 
355
 
        graph_session.send_message (create_album_message);
356
 
    }
357
 
 
358
 
    private void do_show_publishing_options_pane () {
359
 
        debug ("ACTION: showing publishing options pane.");
360
 
 
361
 
        host.set_service_locked (false);
362
 
        Gtk.Builder builder = new Gtk.Builder ();
363
 
 
364
 
        try {
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 ());
370
 
        } catch (Error e) {
371
 
            warning ("Could not parse UI file! Error: %s.", e.message);
372
 
            host.post_error (
373
 
                new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR (
374
 
                    _ ("A file required for publishing is unavailable. Publishing to Facebook can't continue.")));
375
 
            return;
376
 
        }
377
 
 
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);
384
 
    }
385
 
 
386
 
    private void do_logout () {
387
 
        debug ("ACTION: clearing persistent session information and restaring interaction.");
388
 
 
389
 
        invalidate_persistent_session ();
390
 
 
391
 
        running = false;
392
 
        start ();
393
 
    }
394
 
 
395
 
    private void do_add_new_local_album_from_json (string album_name, string json) {
396
 
        try {
397
 
            Json.Parser parser = new Json.Parser ();
398
 
            parser.load_from_data (json);
399
 
 
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");
403
 
 
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));
407
 
            return;
408
 
        }
409
 
 
410
 
        publishing_params.set_target_album_by_name (album_name);
411
 
        do_upload ();
412
 
    }
413
 
 
414
 
    private void do_hosted_web_authentication () {
415
 
        debug ("ACTION: doing hosted web authentication.");
416
 
 
417
 
        host.set_service_locked (false);
418
 
 
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);
422
 
 
423
 
        host.install_dialog_pane (web_auth_pane,
424
 
                                  Spit.Publishing.PluginHost.ButtonMode.CANCEL);
425
 
 
426
 
    }
427
 
 
428
 
    private void do_authenticate_session (string good_login_uri) {
429
 
        debug ("ACTION: preparing to extract session information encoded in uri = '%s'",
430
 
               good_login_uri);
431
 
 
432
 
        // the raw uri is percent-encoded, so decode it
433
 
        string decoded_uri = Soup.URI.decode (good_login_uri);
434
 
 
435
 
        // locate the access token within the URI
436
 
        string? access_token = null;
437
 
        int index = decoded_uri.index_of ("#access_token=");
438
 
        if (index >= 0)
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"));
443
 
            return;
444
 
        }
445
 
 
446
 
        // remove any trailing parameters from the session description string
447
 
        string? trailing_params = null;
448
 
        index = access_token.index_of_char ('&');
449
 
        if (index >= 0)
450
 
            trailing_params = access_token[index:access_token.length];
451
 
        if (trailing_params != null)
452
 
            access_token = access_token.replace (trailing_params, "");
453
 
 
454
 
        // remove the key from the session description string
455
 
        access_token = access_token.replace ("#access_token=", "");
456
 
 
457
 
        // we've got an access token!
458
 
        graph_session.authenticated.connect (on_session_authenticated);
459
 
        graph_session.authenticate (access_token);
460
 
    }
461
 
 
462
 
    private void do_save_session_information () {
463
 
        debug ("ACTION: saving session information to configuration system.");
464
 
 
465
 
        set_persistent_access_token (graph_session.get_access_token ());
466
 
    }
467
 
 
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 ());
472
 
 
473
 
        host.set_service_locked (true);
474
 
 
475
 
        progress_reporter = host.serialize_publishables (publishing_params.resolution.get_pixels (),
476
 
                            publishing_params.strip_metadata);
477
 
 
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
481
 
        // return
482
 
        if (!is_running ())
483
 
            return;
484
 
 
485
 
        Spit.Publishing.Publishable[] publishables = host.get_publishables ();
486
 
        uploader = new Uploader (graph_session, publishing_params, publishables);
487
 
 
488
 
        uploader.upload_complete.connect (on_upload_complete);
489
 
        uploader.upload_error.connect (on_upload_error);
490
 
 
491
 
        uploader.upload (on_upload_status_updated);
492
 
    }
493
 
 
494
 
    private void do_show_success_pane () {
495
 
        debug ("ACTION: showing success pane.");
496
 
 
497
 
        host.set_service_locked (false);
498
 
        host.install_success_pane ();
499
 
    }
500
 
 
501
 
    private void on_generic_error (Spit.Publishing.PublishingError error) {
502
 
        if (error is Spit.Publishing.PublishingError.EXPIRED_SESSION)
503
 
            do_logout ();
504
 
        else
505
 
            host.post_error (error);
506
 
    }
507
 
 
508
 
    private void on_login_clicked () {
509
 
        if (!is_running ())
510
 
            return;
511
 
 
512
 
        debug ("EVENT: user clicked 'Login' on welcome pane.");
513
 
 
514
 
        do_test_connection_to_endpoint ();
515
 
    }
516
 
 
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);
520
 
 
521
 
        if (!is_running ())
522
 
            return;
523
 
 
524
 
        debug ("EVENT: endpoint test transaction detected that the Facebook endpoint is alive.");
525
 
 
526
 
        do_hosted_web_authentication ();
527
 
    }
528
 
 
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);
533
 
 
534
 
        if (!is_running ())
535
 
            return;
536
 
 
537
 
        debug ("EVENT: endpoint test transaction failed to detect a connection to the Facebook " +
538
 
               "endpoint");
539
 
 
540
 
        on_generic_error (error);
541
 
    }
542
 
 
543
 
    private void on_web_auth_pane_login_succeeded (string success_url) {
544
 
        if (!is_running ())
545
 
            return;
546
 
 
547
 
        debug ("EVENT: hosted web login succeeded.");
548
 
 
549
 
        do_authenticate_session (success_url);
550
 
    }
551
 
 
552
 
 
553
 
 
554
 
    private void on_web_auth_pane_login_failed () {
555
 
        if (!is_running ())
556
 
            return;
557
 
 
558
 
        debug ("EVENT: hosted web login failed.");
559
 
 
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 ();
567
 
    }
568
 
 
569
 
    private void on_session_authenticated () {
570
 
        graph_session.authenticated.disconnect (on_session_authenticated);
571
 
 
572
 
        if (!is_running ())
573
 
            return;
574
 
 
575
 
        assert (graph_session.is_authenticated ());
576
 
        debug ("EVENT: an authenticated session has become available.");
577
 
 
578
 
        do_save_session_information ();
579
 
        do_fetch_user_info ();
580
 
    }
581
 
 
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);
585
 
 
586
 
        if (!is_running ())
587
 
            return;
588
 
 
589
 
        debug ("EVENT: user info fetch completed; response = '%s'.", message.get_response_body ());
590
 
 
591
 
        do_extract_user_info_from_json (message.get_response_body ());
592
 
    }
593
 
 
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);
598
 
 
599
 
        if (!is_running ())
600
 
            return;
601
 
 
602
 
        debug ("EVENT: fetching user info generated and error.");
603
 
 
604
 
        on_generic_error (error);
605
 
    }
606
 
 
607
 
    private void on_user_info_extracted () {
608
 
        if (!is_running ())
609
 
            return;
610
 
 
611
 
        debug ("EVENT: user info extracted from JSON response: uid = %s; name = %s.", uid, username);
612
 
 
613
 
        do_fetch_album_descriptions ();
614
 
    }
615
 
 
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);
619
 
 
620
 
        if (!is_running ())
621
 
            return;
622
 
 
623
 
        debug ("EVENT: album descriptions fetch transaction completed; response = '%s'.",
624
 
               message.get_response_body ());
625
 
 
626
 
        do_extract_albums_from_json (message.get_response_body ());
627
 
    }
628
 
 
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);
633
 
 
634
 
        if (!is_running ())
635
 
            return;
636
 
 
637
 
        debug ("EVENT: album description fetch attempt generated an error.");
638
 
 
639
 
        on_generic_error (err);
640
 
    }
641
 
 
642
 
    private void on_albums_extracted () {
643
 
        if (!is_running ())
644
 
            return;
645
 
 
646
 
        debug ("EVENT: successfully extracted %d albums from JSON response",
647
 
               publishing_params.albums.length);
648
 
 
649
 
        do_show_publishing_options_pane ();
650
 
    }
651
 
 
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);
655
 
 
656
 
        if (!is_running ())
657
 
            return;
658
 
 
659
 
        debug ("EVENT: user clicked 'Logout' in publishing options pane.");
660
 
 
661
 
        do_logout ();
662
 
    }
663
 
 
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);
668
 
 
669
 
        if (!is_running ())
670
 
            return;
671
 
 
672
 
        debug ("EVENT: user clicked 'Publish' in publishing options pane.");
673
 
 
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;
679
 
 
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) {
685
 
                do_upload ();
686
 
            } else {
687
 
                publishing_params.new_album_name = target_album;
688
 
                do_create_new_album ();
689
 
            }
690
 
        } else {
691
 
            // we're publishing only videos and we don't need an album name
692
 
            do_upload ();
693
 
        }
694
 
    }
695
 
 
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);
699
 
 
700
 
        assert (publishing_params.new_album_name != null);
701
 
 
702
 
        if (!is_running ())
703
 
            return;
704
 
 
705
 
        debug ("EVENT: created new album resource on remote host; response body = %s.\n",
706
 
               message.get_response_body ());
707
 
 
708
 
        do_add_new_local_album_from_json (publishing_params.new_album_name,
709
 
                                          message.get_response_body ());
710
 
    }
711
 
 
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);
715
 
 
716
 
        if (!is_running ())
717
 
            return;
718
 
 
719
 
        debug ("EVENT: attempt to create new album generated an error.");
720
 
 
721
 
        on_generic_error (err);
722
 
    }
723
 
 
724
 
    private void on_upload_status_updated (int file_number, double completed_fraction) {
725
 
        if (!is_running ())
726
 
            return;
727
 
 
728
 
        debug ("EVENT: uploader reports upload %.2f percent complete.", 100.0 * completed_fraction);
729
 
 
730
 
        assert (progress_reporter != null);
731
 
 
732
 
        progress_reporter (file_number, completed_fraction);
733
 
    }
734
 
 
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);
738
 
 
739
 
        if (!is_running ())
740
 
            return;
741
 
 
742
 
        debug ("EVENT: uploader reports upload complete; %d items published.", num_published);
743
 
 
744
 
        do_show_success_pane ();
745
 
    }
746
 
 
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);
750
 
 
751
 
        if (!is_running ())
752
 
            return;
753
 
 
754
 
        debug ("EVENT: uploader reports upload error = '%s'.", err.message);
755
 
 
756
 
        host.post_error (err);
757
 
    }
758
 
 
759
 
    public Spit.Publishing.Service get_service () {
760
 
        return service;
761
 
    }
762
 
 
763
 
    public string get_service_name () {
764
 
        return SERVICE_NAME;
765
 
    }
766
 
 
767
 
    public string get_user_visible_name () {
768
 
        return USER_VISIBLE_NAME;
769
 
    }
770
 
 
771
 
    public void start () {
772
 
        if (is_running ())
773
 
            return;
774
 
 
775
 
        debug ("FacebookPublisher: starting interaction.");
776
 
 
777
 
        running = true;
778
 
 
779
 
        // reset all publishing parameters to their default values -- in case this start is
780
 
        // actually a restart
781
 
        publishing_params = new PublishingParameters ();
782
 
 
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
785
 
        // the Welcome pane
786
 
        if (is_persistent_session_valid ()) {
787
 
            graph_session.authenticate (get_persistent_access_token ());
788
 
        } else {
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);
793
 
            } else {
794
 
                do_show_service_welcome_pane ();
795
 
            }
796
 
        }
797
 
    }
798
 
 
799
 
    public void stop () {
800
 
        debug ("FacebookPublisher: stop( ) invoked.");
801
 
 
802
 
        if (graph_session != null)
803
 
            graph_session.stop_transactions ();
804
 
 
805
 
        host = null;
806
 
        running = false;
807
 
    }
808
 
 
809
 
    public bool is_running () {
810
 
        return running;
811
 
    }
812
 
}
813
 
 
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;
819
 
 
820
 
    public signal void login_succeeded (string success_url);
821
 
    public signal void login_failed ();
822
 
 
823
 
    public WebAuthenticationPane () {
824
 
        pane_widget = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
825
 
 
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);
829
 
 
830
 
        webview = new WebKit.WebView ();
831
 
        webview.get_settings ().enable_plugins = false;
832
 
        webview.get_settings ().enable_default_context_menu = false;
833
 
 
834
 
        webview.load_finished.connect (on_page_load);
835
 
        webview.load_started.connect (on_load_started);
836
 
 
837
 
        webview_frame.add (webview);
838
 
        pane_widget.pack_start (webview_frame, true, true, 0);
839
 
    }
840
 
 
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;
848
 
 
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;
858
 
        }
859
 
 
860
 
    }
861
 
 
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" )
908
 
    };
909
 
 
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 == "")
913
 
            return "www";
914
 
 
915
 
        string system_locale = raw_system_locale.split (".")[0];
916
 
 
917
 
        foreach (LocaleLookup locale_lookup in locale_lookup_table) {
918
 
            if (!system_locale.has_prefix (locale_lookup.prefix))
919
 
                continue;
920
 
 
921
 
            if (locale_lookup.exception_code != null) {
922
 
                assert (locale_lookup.exception_translation != null);
923
 
 
924
 
                if (system_locale.contains (locale_lookup.exception_code))
925
 
                    return locale_lookup.exception_translation;
926
 
            }
927
 
 
928
 
            if (locale_lookup.exception_code_2 != null) {
929
 
                assert (locale_lookup.exception_translation_2 != null);
930
 
 
931
 
                if (system_locale.contains (locale_lookup.exception_code_2))
932
 
                    return locale_lookup.exception_translation_2;
933
 
            }
934
 
 
935
 
            return locale_lookup.translation;
936
 
        }
937
 
 
938
 
        // default
939
 
        return "www";
940
 
    }
941
 
 
942
 
    private string get_login_url () {
943
 
        string facebook_locale = get_system_locale_as_facebook_locale ();
944
 
 
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);
946
 
    }
947
 
 
948
 
    private void on_page_load (WebKit.WebFrame origin_frame) {
949
 
        pane_widget.get_window ().set_cursor (new Gdk.Cursor (Gdk.CursorType.LEFT_PTR));
950
 
 
951
 
        string loaded_url = origin_frame.get_uri ().dup ();
952
 
 
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, "");
958
 
        }
959
 
 
960
 
        // were we redirected to the facebook login success page?
961
 
        if (loaded_url.contains ("login_success")) {
962
 
            cache_dirty = true;
963
 
            login_succeeded (origin_frame.get_uri ());
964
 
            return;
965
 
        }
966
 
 
967
 
        // were we redirected to the login total failure page?
968
 
        if (loaded_url.contains ("login_failure")) {
969
 
            login_failed ();
970
 
            return;
971
 
        }
972
 
    }
973
 
 
974
 
    private void on_load_started (WebKit.WebFrame frame) {
975
 
        pane_widget.get_window ().set_cursor (new Gdk.Cursor (Gdk.CursorType.WATCH));
976
 
    }
977
 
 
978
 
    public static bool is_cache_dirty () {
979
 
        return cache_dirty;
980
 
    }
981
 
 
982
 
    public Gtk.Widget get_widget () {
983
 
        return pane_widget;
984
 
    }
985
 
 
986
 
    public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry () {
987
 
        return Spit.Publishing.DialogPane.GeometryOptions.NONE;
988
 
    }
989
 
 
990
 
    public void on_pane_installed () {
991
 
        webview.open (get_login_url ());
992
 
    }
993
 
 
994
 
    public void on_pane_uninstalled () {
995
 
    }
996
 
}
997
 
 
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;
1013
 
 
1014
 
    private Resolution[] possible_resolutions;
1015
 
    private Gtk.ComboBoxText resolution_combo = null;
1016
 
 
1017
 
    private Spit.Publishing.Publisher.MediaType media_type;
1018
 
 
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;
1024
 
 
1025
 
    public signal void logout ();
1026
 
    public signal void publish (string? target_album, string privacy_setting,
1027
 
                                Resolution target_resolution, bool strip_metadata);
1028
 
 
1029
 
    private class PrivacyDescription {
1030
 
        public string description;
1031
 
        public string privacy_setting;
1032
 
 
1033
 
        public PrivacyDescription (string description, string privacy_setting) {
1034
 
            this.description = description;
1035
 
            this.privacy_setting = privacy_setting;
1036
 
        }
1037
 
    }
1038
 
 
1039
 
    public PublishingOptionsPane (string username, Album[] albums,
1040
 
                                  Spit.Publishing.Publisher.MediaType media_type, FacebookPublisher publisher,
1041
 
                                  Gtk.Builder builder, bool strip_metadata) {
1042
 
 
1043
 
        this.builder = builder;
1044
 
        assert (builder != null);
1045
 
        assert (builder.get_objects ().length () > 0);
1046
 
 
1047
 
        this.albums = albums;
1048
 
        this.privacy_descriptions = create_privacy_descriptions ();
1049
 
 
1050
 
        this.possible_resolutions = create_resolution_list ();
1051
 
        this.publisher = publisher;
1052
 
 
1053
 
        // we'll need to know if the user is importing video or not when sorting out visibility.
1054
 
        this.media_type = media_type;
1055
 
 
1056
 
        pane_widget = (Gtk.Box) builder.get_object ("facebook_pane_box");
1057
 
        pane_widget.set_border_width (16);
1058
 
 
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");
1069
 
 
1070
 
        create_new_radio.clicked.connect (on_create_new_toggled);
1071
 
        use_existing_radio.clicked.connect (on_use_existing_toggled);
1072
 
 
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);
1078
 
 
1079
 
        setup_visibility_combo ();
1080
 
        visibility_combo.set_active (0);
1081
 
 
1082
 
        publish_button.clicked.connect (on_publish_button_clicked);
1083
 
        logout_button.clicked.connect (on_logout_button_clicked);
1084
 
 
1085
 
        setup_resolution_combo ();
1086
 
        resolution_combo.set_active (publisher.get_persistent_default_size ());
1087
 
        resolution_combo.changed.connect (on_size_changed);
1088
 
 
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));
1095
 
 
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);
1105
 
        }
1106
 
    }
1107
 
 
1108
 
    private bool publishing_photos () {
1109
 
        return (media_type & Spit.Publishing.Publisher.MediaType.PHOTO) != 0;
1110
 
    }
1111
 
 
1112
 
    private void setup_visibility_combo () {
1113
 
        foreach (PrivacyDescription p in privacy_descriptions)
1114
 
            visibility_combo.append_text (p.description);
1115
 
    }
1116
 
 
1117
 
    private void setup_resolution_combo () {
1118
 
        foreach (Resolution res in possible_resolutions)
1119
 
            resolution_combo.append_text (res.get_name ());
1120
 
    }
1121
 
 
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);
1126
 
 
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);
1131
 
 
1132
 
            existing_albums_combo.grab_focus ();
1133
 
        }
1134
 
    }
1135
 
 
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 ();
1141
 
 
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);
1145
 
        }
1146
 
    }
1147
 
 
1148
 
    private void on_size_changed () {
1149
 
        publisher.set_persistent_default_size (resolution_combo.get_active ());
1150
 
    }
1151
 
 
1152
 
    private void on_logout_button_clicked () {
1153
 
        logout ();
1154
 
    }
1155
 
 
1156
 
    private void on_publish_button_clicked () {
1157
 
        string album_name;
1158
 
        string privacy_setting = privacy_descriptions[visibility_combo.get_active ()].privacy_setting;
1159
 
 
1160
 
        Resolution resolution_setting;
1161
 
 
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 ();
1166
 
            } else {
1167
 
                album_name = new_album_entry.get_text ();
1168
 
            }
1169
 
        } else {
1170
 
            resolution_setting = Resolution.STANDARD;
1171
 
            album_name = null;
1172
 
        }
1173
 
 
1174
 
        publish (album_name, privacy_setting, resolution_setting, strip_metadata_check.get_active ());
1175
 
    }
1176
 
 
1177
 
    private PrivacyDescription[] create_privacy_descriptions () {
1178
 
        PrivacyDescription[] result = new PrivacyDescription[0];
1179
 
 
1180
 
        result += new PrivacyDescription (_ ("Just me"), "{ 'value' : 'SELF' }");
1181
 
        result += new PrivacyDescription (_ ("Friends"), "{ 'value' : 'ALL_FRIENDS' }");
1182
 
        result += new PrivacyDescription (_ ("Everyone"), "{ 'value' : 'EVERYONE' }");
1183
 
 
1184
 
        return result;
1185
 
    }
1186
 
 
1187
 
    private Resolution[] create_resolution_list () {
1188
 
        Resolution[] result = new Resolution[0];
1189
 
 
1190
 
        result += Resolution.STANDARD;
1191
 
        result += Resolution.HIGH;
1192
 
 
1193
 
        return result;
1194
 
    }
1195
 
 
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);
1203
 
            } else {
1204
 
                int default_album_seq_num = -1;
1205
 
                int ticker = 0;
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;
1210
 
                    ticker++;
1211
 
                }
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);
1216
 
                } else {
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);
1221
 
                }
1222
 
            }
1223
 
        }
1224
 
 
1225
 
        publish_button.grab_focus ();
1226
 
    }
1227
 
 
1228
 
    private void notify_logout () {
1229
 
        logout ();
1230
 
    }
1231
 
 
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 ());
1234
 
    }
1235
 
 
1236
 
    public Gtk.Widget get_widget () {
1237
 
        return pane_widget;
1238
 
    }
1239
 
 
1240
 
    public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry () {
1241
 
        return Spit.Publishing.DialogPane.GeometryOptions.NONE;
1242
 
    }
1243
 
 
1244
 
    public void on_pane_installed () {
1245
 
        logout.connect (notify_logout);
1246
 
        publish.connect (notify_publish);
1247
 
 
1248
 
        installed ();
1249
 
    }
1250
 
 
1251
 
    public void on_pane_uninstalled () {
1252
 
        logout.disconnect (notify_logout);
1253
 
        publish.disconnect (notify_publish);
1254
 
    }
1255
 
}
1256
 
 
1257
 
internal enum Endpoint {
1258
 
    DEFAULT,
1259
 
    VIDEO,
1260
 
    TEST_CONNECTION;
1261
 
 
1262
 
    public string to_uri () {
1263
 
        switch (this) {
1264
 
        case DEFAULT:
1265
 
            return "https://graph.facebook.com/";
1266
 
 
1267
 
        case VIDEO:
1268
 
            return "https://graph-video.facebook.com/";
1269
 
 
1270
 
        case TEST_CONNECTION:
1271
 
            return "https://www.facebook.com/";
1272
 
 
1273
 
        default:
1274
 
            assert_not_reached ();
1275
 
        }
1276
 
    }
1277
 
}
1278
 
 
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);
1283
 
 
1284
 
    public abstract string get_uri ();
1285
 
    public abstract string get_response_body ();
1286
 
}
1287
 
 
1288
 
internal class GraphSession {
1289
 
    private abstract class GraphMessageImpl : GraphMessage {
1290
 
        public Publishing.RESTSupport.HttpMethod method;
1291
 
        public string uri;
1292
 
        public string access_token;
1293
 
        public Soup.Message soup_message;
1294
 
        public weak GraphSession host_session;
1295
 
        public int bytes_so_far;
1296
 
 
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;
1303
 
 
1304
 
            string endpoint_uri = endpoint.to_uri ();
1305
 
            try {
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 ();
1310
 
            }
1311
 
        }
1312
 
 
1313
 
        public virtual bool prepare_for_transmission () {
1314
 
            return true;
1315
 
        }
1316
 
 
1317
 
        public override string get_uri () {
1318
 
            return uri;
1319
 
        }
1320
 
 
1321
 
        public override string get_response_body () {
1322
 
            return (string) soup_message.response_body.data;
1323
 
        }
1324
 
 
1325
 
        public void on_wrote_body_data (Soup.Buffer chunk) {
1326
 
            bytes_so_far += (int) chunk.length;
1327
 
 
1328
 
            data_transmitted (bytes_so_far, (int) soup_message.request_body.length);
1329
 
        }
1330
 
    }
1331
 
 
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);
1336
 
 
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);
1340
 
        }
1341
 
    }
1342
 
 
1343
 
    private class GraphEndpointProbeMessage : GraphMessageImpl {
1344
 
        public GraphEndpointProbeMessage (GraphSession host_session) {
1345
 
            base (host_session, Publishing.RESTSupport.HttpMethod.GET, "/", "",
1346
 
                  Endpoint.TEST_CONNECTION);
1347
 
 
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);
1350
 
        }
1351
 
    }
1352
 
 
1353
 
    private class GraphUploadMessage : GraphMessageImpl {
1354
 
        private MappedFile mapped_file = null;
1355
 
        private Spit.Publishing.Publishable publishable;
1356
 
 
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);
1363
 
 
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);
1369
 
 
1370
 
            this.publishable = publishable;
1371
 
 
1372
 
            // attempt to map the binary payload from disk into memory
1373
 
            try {
1374
 
                this.mapped_file = new MappedFile (publishable.get_serialized_file ().get_path (),
1375
 
                                                   false);
1376
 
            } catch (FileError e) {
1377
 
                return;
1378
 
            }
1379
 
 
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);
1382
 
 
1383
 
            unowned uint8[] payload = (uint8[]) mapped_file.get_contents ();
1384
 
            payload.length = (int) mapped_file.get_length ();
1385
 
 
1386
 
            Soup.Buffer image_data = new Soup.Buffer (Soup.MemoryUse.TEMPORARY, payload);
1387
 
 
1388
 
            Soup.Multipart mp_envelope = new Soup.Multipart ("multipart/form-data");
1389
 
 
1390
 
            mp_envelope.append_form_string ("access_token", access_token);
1391
 
 
1392
 
            if (publishable.get_media_type () == Spit.Publishing.Publisher.MediaType.VIDEO)
1393
 
                mp_envelope.append_form_string ("privacy", resource_privacy);
1394
 
 
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);
1399
 
 
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);
1404
 
 
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());
1408
 
 
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);
1414
 
 
1415
 
            mp_envelope.to_message (soup_message.request_headers, soup_message.request_body);
1416
 
        }
1417
 
 
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 ())));
1422
 
                return false;
1423
 
            } else {
1424
 
                return true;
1425
 
            }
1426
 
        }
1427
 
    }
1428
 
 
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);
1433
 
 
1434
 
            assert (album_privacy != null && album_privacy != "");
1435
 
 
1436
 
            this.soup_message = new Soup.Message.from_uri (method.to_string (), new Soup.URI (uri));
1437
 
 
1438
 
            Soup.Multipart mp_envelope = new Soup.Multipart ("multipart/form-data");
1439
 
 
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);
1443
 
 
1444
 
            mp_envelope.to_message (soup_message.request_headers, soup_message.request_body);
1445
 
        }
1446
 
    }
1447
 
 
1448
 
    public signal void authenticated ();
1449
 
 
1450
 
    private Soup.Session soup_session;
1451
 
    private string? access_token;
1452
 
    private GraphMessage? current_message;
1453
 
 
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;
1460
 
    }
1461
 
 
1462
 
    ~GraphSession () {
1463
 
        soup_session.request_unqueued.disconnect (on_request_unqueued);
1464
 
    }
1465
 
 
1466
 
    private void manage_message (GraphMessage msg) {
1467
 
        assert (current_message == null);
1468
 
 
1469
 
        current_message = msg;
1470
 
    }
1471
 
 
1472
 
    private void unmanage_message (GraphMessage msg) {
1473
 
        assert (current_message != null);
1474
 
 
1475
 
        current_message = null;
1476
 
    }
1477
 
 
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);
1482
 
 
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);
1490
 
            return;
1491
 
        }
1492
 
 
1493
 
        unmanage_message (real_message);
1494
 
        msg.wrote_body_data.disconnect (real_message.on_wrote_body_data);
1495
 
 
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
1501
 
            // or POST
1502
 
            break;
1503
 
 
1504
 
        case EXPIRED_SESSION_STATUS_CODE:
1505
 
            error = new Spit.Publishing.PublishingError.EXPIRED_SESSION (
1506
 
                "OAuth Access Token has Expired. Logout user.");
1507
 
            break;
1508
 
 
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);
1513
 
            break;
1514
 
 
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);
1519
 
            break;
1520
 
 
1521
 
        default:
1522
 
            // status codes below 100 are used by Soup, 100 and above are defined HTTP
1523
 
            // codes
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);
1528
 
            } else {
1529
 
                error = new Spit.Publishing.PublishingError.NO_ANSWER (
1530
 
                    "Failure communicating with %s (error code %u)", real_message.get_uri (),
1531
 
                    msg.status_code);
1532
 
            }
1533
 
            break;
1534
 
        }
1535
 
 
1536
 
        // All valid communication with Facebook involves body data in the response
1537
 
        if (error == null)
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 ());
1541
 
 
1542
 
        if (error == null)
1543
 
            real_message.completed ();
1544
 
        else
1545
 
            real_message.failed (error);
1546
 
    }
1547
 
 
1548
 
    public void authenticate (string access_token) {
1549
 
        this.access_token = access_token;
1550
 
        authenticated ();
1551
 
    }
1552
 
 
1553
 
    public bool is_authenticated () {
1554
 
        return access_token != null;
1555
 
    }
1556
 
 
1557
 
    public string get_access_token () {
1558
 
        assert (is_authenticated ());
1559
 
        return access_token;
1560
 
    }
1561
 
 
1562
 
    public GraphMessage new_endpoint_test () {
1563
 
        return new GraphEndpointProbeMessage (this);
1564
 
    }
1565
 
 
1566
 
    public GraphMessage new_query (string resource_path) {
1567
 
        return new GraphQueryMessage (this, resource_path, access_token);
1568
 
    }
1569
 
 
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);
1574
 
    }
1575
 
 
1576
 
    public GraphMessage new_create_album (string album_name, string privacy) {
1577
 
        return new GraphSession.GraphCreateAlbumMessage (this, access_token, album_name, privacy);
1578
 
    }
1579
 
 
1580
 
    public void send_message (GraphMessage message) {
1581
 
        GraphMessageImpl real_message = (GraphMessageImpl) message;
1582
 
 
1583
 
        debug ("making HTTP request to URI: " + real_message.soup_message.uri.to_string (false));
1584
 
 
1585
 
        if (real_message.prepare_for_transmission ()) {
1586
 
            manage_message (message);
1587
 
            soup_session.queue_message (real_message.soup_message, null);
1588
 
        }
1589
 
    }
1590
 
 
1591
 
    public void stop_transactions () {
1592
 
        soup_session.abort ();
1593
 
    }
1594
 
}
1595
 
 
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;
1602
 
 
1603
 
    public signal void upload_complete (int num_photos_published);
1604
 
    public signal void upload_error (Spit.Publishing.PublishingError err);
1605
 
 
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;
1612
 
    }
1613
 
 
1614
 
    private void send_current_file () {
1615
 
        Spit.Publishing.Publishable publishable = publishables[current_file];
1616
 
        GLib.File? file = publishable.get_serialized_file ();
1617
 
 
1618
 
        // if the current publishable hasn't been serialized, then skip it
1619
 
        if (file == null) {
1620
 
            current_file++;
1621
 
            return;
1622
 
        }
1623
 
 
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);
1632
 
 
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);
1636
 
 
1637
 
        session.send_message (upload_message);
1638
 
    }
1639
 
 
1640
 
    private void send_files () {
1641
 
        current_file = 0;
1642
 
        send_current_file ();
1643
 
    }
1644
 
 
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 *
1649
 
                                   file_span);
1650
 
 
1651
 
        if (status_updated != null)
1652
 
            status_updated (current_file + 1, fraction_complete);
1653
 
    }
1654
 
 
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);
1659
 
 
1660
 
        current_file++;
1661
 
        if (current_file < publishables.length) {
1662
 
            send_current_file ();
1663
 
        } else {
1664
 
            upload_complete (current_file);
1665
 
        }
1666
 
    }
1667
 
 
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);
1672
 
 
1673
 
        upload_error (error);
1674
 
    }
1675
 
 
1676
 
    public void upload (Spit.Publishing.ProgressCallback? status_updated = null) {
1677
 
        this.status_updated = status_updated;
1678
 
 
1679
 
        if (publishables.length > 0)
1680
 
            send_files ();
1681
 
    }
1682
 
}
1683
 
 
1684
 
}