]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/FacebookSSO/lib/facebookclient.php
- Still send notices to Facebook from existing Facebook app users
[quix0rs-gnu-social.git] / plugins / FacebookSSO / lib / facebookclient.php
1 <?php
2 /**
3  * StatusNet, the distributed open-source microblogging tool
4  *
5  * Class for communicating with Facebook
6  *
7  * PHP version 5
8  *
9  * LICENCE: This program is free software: you can redistribute it and/or modify
10  * it under the terms of the GNU Affero General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU Affero General Public License for more details.
18  *
19  * You should have received a copy of the GNU Affero General Public License
20  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21  *
22  * @category  Plugin
23  * @package   StatusNet
24  * @author    Craig Andrews <candrews@integralblue.com>
25  * @author    Zach Copley <zach@status.net>
26  * @copyright 2009-2010 StatusNet, Inc.
27  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
28  * @link      http://status.net/
29  */
30
31 if (!defined('STATUSNET')) {
32     exit(1);
33 }
34
35 /**
36  * Class for communication with Facebook
37  *
38  * @category Plugin
39  * @package  StatusNet
40  * @author   Zach Copley <zach@status.net>
41  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
42  * @link     http://status.net/
43  */
44 class Facebookclient
45 {
46     protected $facebook      = null; // Facebook Graph client obj
47     protected $flink         = null; // Foreign_link StatusNet -> Facebook
48     protected $notice        = null; // The user's notice
49     protected $user          = null; // Sender of the notice
50     //protected $oldRestClient = null; // Old REST API client
51
52     function __construct($notice)
53     {
54         $this->facebook = self::getFacebook();
55         $this->notice   = $notice;
56
57         $this->flink = Foreign_link::getByUserID(
58             $notice->profile_id,
59             FACEBOOK_SERVICE
60         );
61
62         $this->user = $this->flink->getUser();
63     }
64
65     /*
66      * Get an instance of the Facebook Graph SDK object
67      *
68      * @param string $appId     Application
69      * @param string $secret    Facebook API secret
70      *
71      * @return Facebook A Facebook SDK obj
72      */
73     static function getFacebook($appId = null, $secret = null)
74     {
75         // Check defaults and configuration for application ID and secret
76         if (empty($appId)) {
77             $appId = common_config('facebook', 'appid');
78         }
79
80         if (empty($secret)) {
81             $secret = common_config('facebook', 'secret');
82         }
83
84         // If there's no app ID and secret set in the local config, look
85         // for a global one
86         if (empty($appId) || empty($secret)) {
87             $appId  = common_config('facebook', 'global_appid');
88             $secret = common_config('facebook', 'global_secret');
89         }
90
91         return new Facebook(
92             array(
93                'appId'  => $appId,
94                'secret' => $secret,
95                'cookie' => true
96             )
97         );
98     }
99
100     /*
101      * Broadcast a notice to Facebook
102      *
103      * @param Notice $notice    the notice to send
104      */
105     static function facebookBroadcastNotice($notice)
106     {
107         common_debug('Facebook broadcast');
108         $client = new Facebookclient($notice);
109         return $client->sendNotice();
110     }
111
112     /*
113      * Should the notice go to Facebook?
114      */
115     function isFacebookBound() {
116
117         if (empty($this->flink)) {
118             common_log(
119                 LOG_WARN,
120                 sprintf(
121                     "No Foreign_link to Facebook for the author of notice %d.",
122                     $this->notice->id
123                 ),
124                 __FILE__
125             );
126             return false;
127         }
128
129         // Avoid a loop
130         if ($this->notice->source == 'Facebook') {
131             common_log(
132                 LOG_INFO,
133                 sprintf(
134                     'Skipping notice %d because its source is Facebook.',
135                     $this->notice->id
136                 ),
137                 __FILE__
138             );
139             return false;
140         }
141
142         // If the user does not want to broadcast to Facebook, move along
143         if (!($this->flink->noticesync & FOREIGN_NOTICE_SEND == FOREIGN_NOTICE_SEND)) {
144             common_log(
145                 LOG_INFO,
146                 sprintf(
147                     'Skipping notice %d because user has FOREIGN_NOTICE_SEND bit off.',
148                     $this->notice->id
149                 ),
150                 __FILE__
151             );
152             return false;
153         }
154
155         // If it's not a reply, or if the user WANTS to send @-replies,
156         // then, yeah, it can go to Facebook.
157         if (!preg_match('/@[a-zA-Z0-9_]{1,15}\b/u', $this->notice->content) ||
158             ($this->flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
159             return true;
160         }
161
162         return false;
163     }
164
165     /*
166      * Determine whether we should send this notice using the Graph API or the
167      * old REST API and then dispatch
168      */
169     function sendNotice()
170     {
171         // If there's nothing in the credentials field try to send via
172         // the Old Rest API
173
174         if ($this->isFacebookBound()) {
175             common_debug("notice is facebook bound", __FILE__);
176             if (empty($this->flink->credentials)) {
177                 $this->sendOldRest();
178             } else {
179
180                 // Otherwise we most likely have an access token
181                 $this->sendGraph();
182             }
183
184         } else {
185             common_debug(
186                 sprintf(
187                     "Skipping notice %d - not bound for Facebook",
188                     $this->notice->id,
189                     __FILE__
190                 )
191             );
192         }
193     }
194
195     /*
196      * Send a notice to Facebook using the Graph API
197      */
198     function sendGraph()
199     {
200         try {
201
202             $fbuid = $this->flink->foreign_id;
203
204             common_debug(
205                 sprintf(
206                     "Attempting use Graph API to post notice %d as a stream item for %s (%d), fbuid %s",
207                     $this->notice->id,
208                     $this->user->nickname,
209                     $this->user->id,
210                     $fbuid
211                 ),
212                 __FILE__
213             );
214
215             $params = array(
216                 'access_token' => $this->flink->credentials,
217                 'message'      => $this->notice->content
218             );
219
220             $attachments = $this->notice->attachments();
221
222             if (!empty($attachments)) {
223
224                 // We can only send one attachment with the Graph API
225
226                 $first = array_shift($attachments);
227
228                 if (substr($first->mimetype, 0, 6) == 'image/'
229                     || in_array(
230                         $first->mimetype,
231                         array('application/x-shockwave-flash', 'audio/mpeg' ))) {
232
233                    $params['picture'] = $first->url;
234                    $params['caption'] = 'Click for full size';
235                    $params['source']  = $first->url;
236                 }
237
238             }
239
240             $result = $this->facebook->api(
241                 sprintf('/%s/feed', $fbuid), 'post', $params
242             );
243
244         } catch (FacebookApiException $e) {
245             return $this->handleFacebookError($e);
246         }
247
248         return true;
249     }
250
251     /*
252      * Send a notice to Facebook using the deprecated Old REST API. We need this
253      * for backwards compatibility. Users who signed up for Facebook bridging
254      * using the old Facebook Canvas application do not have an OAuth 2.0
255      * access token.
256      */
257     function sendOldRest()
258     {
259         try {
260
261             $canPublish = $this->checkPermission('publish_stream');
262             $canUpdate  = $this->checkPermission('status_update');
263
264             // We prefer to use stream.publish, because it can handle
265             // attachments and returns the ID of the published item
266
267             if ($canPublish == 1) {
268                 $this->restPublishStream();
269             } else if ($canUpdate == 1) {
270                 // as a last resort we can just update the user's "status"
271                 $this->restStatusUpdate();
272             } else {
273
274                 $msg = 'Not sending notice %d to Facebook because user %s '
275                      . '(%d), fbuid %s,  does not have \'status_update\' '
276                      . 'or \'publish_stream\' permission.';
277
278                 common_log(
279                     LOG_WARNING,
280                     sprintf(
281                         $msg,
282                         $this->notice->id,
283                         $this->user->nickname,
284                         $this->user->id,
285                         $this->flink->foreign_id
286                     ),
287                     __FILE__
288                 );
289             }
290
291         } catch (FacebookApiException $e) {
292             return $this->handleFacebookError($e);
293         }
294
295         return true;
296     }
297
298     /*
299      * Query Facebook to to see if a user has permission
300      *
301      *
302      *
303      * @param $permission the permission to check for - must be either
304      *                    public_stream or status_update
305      *
306      * @return boolean result
307      */
308     function checkPermission($permission)
309     {
310         if (!in_array($permission, array('publish_stream', 'status_update'))) {
311              throw new ServerException("No such permission!");
312         }
313
314         $fbuid = $this->flink->foreign_id;
315
316         common_debug(
317             sprintf(
318                 'Checking for %s permission for user %s (%d), fbuid %s',
319                 $permission,
320                 $this->user->nickname,
321                 $this->user->id,
322                 $fbuid
323             ),
324             __FILE__
325         );
326
327         $hasPermission = $this->facebook->api(
328             array(
329                 'method'   => 'users.hasAppPermission',
330                 'ext_perm' => $permission,
331                 'uid'      => $fbuid
332             )
333         );
334
335         if ($hasPermission == 1) {
336
337             common_debug(
338                 sprintf(
339                     '%s (%d), fbuid %s has %s permission',
340                     $permission,
341                     $this->user->nickname,
342                     $this->user->id,
343                     $fbuid
344                 ),
345                 __FILE__
346             );
347
348             return true;
349
350         } else {
351
352             $logMsg = '%s (%d), fbuid $fbuid does NOT have %s permission.'
353                     . 'Facebook returned: %s';
354
355             common_debug(
356                 sprintf(
357                     $logMsg,
358                     $this->user->nickname,
359                     $this->user->id,
360                     $permission,
361                     $fbuid,
362                     var_export($result, true)
363                 ),
364                 __FILE__
365             );
366
367             return false;
368
369         }
370     }
371
372     /*
373      * Handle a Facebook API Exception
374      *
375      * @param FacebookApiException $e the exception
376      *
377      */
378     function handleFacebookError($e)
379     {
380         $fbuid  = $this->flink->foreign_id;
381         $errmsg = $e->getMessage();
382         $code   = $e->getCode();
383
384         // The Facebook PHP SDK seems to always set the code attribute
385         // of the Exception to 0; they put the real error code it in
386         // the message. Gar!
387         if ($code == 0) {
388             preg_match('/^\(#(?<code>\d+)\)/', $errmsg, $matches);
389             $code = $matches['code'];
390         }
391
392         // XXX: Check for any others?
393         switch($code) {
394          case 100: // Invalid parameter
395             $msg = 'Facebook claims notice %d was posted with an invalid '
396                  . 'parameter (error code 100 - %s) Notice details: '
397                  . '[nickname=%s, user id=%d, fbuid=%d, content="%s"]. '
398                  . 'Dequeing.';
399             common_log(
400                 LOG_ERR, sprintf(
401                     $msg,
402                     $this->notice->id,
403                     $errmsg,
404                     $this->user->nickname,
405                     $this->user->id,
406                     $fbuid,
407                     $this->notice->content
408                 ),
409                 __FILE__
410             );
411             return true;
412             break;
413          case 200: // Permissions error
414          case 250: // Updating status requires the extended permission status_update
415             $this->disconnect();
416             return true; // dequeue
417             break;
418          case 341: // Feed action request limit reached
419                 $msg = '%s (userid=%d, fbuid=%d) has exceeded his/her limit '
420                      . 'for posting notices to Facebook today. Dequeuing '
421                      . 'notice %d';
422                 common_log(
423                     LOG_INFO, sprintf(
424                         $msg,
425                         $user->nickname,
426                         $user->id,
427                         $fbuid,
428                         $this->notice->id
429                     ),
430                     __FILE__
431                 );
432             // @fixme: We want to rety at a later time when the throttling has expired
433             // instead of just giving up.
434             return true;
435             break;
436          default:
437             $msg = 'Facebook returned an error we don\'t know how to deal with '
438                  . 'when posting notice %d. Error code: %d, error message: "%s"'
439                  . ' Notice details: [nickname=%s, user id=%d, fbuid=%d, '
440                  . 'notice content="%s"]. Dequeing.';
441             common_log(
442                 LOG_ERR, sprintf(
443                     $msg,
444                     $this->notice->id,
445                     $code,
446                     $errmsg,
447                     $this->user->nickname,
448                     $this->user->id,
449                     $fbuid,
450                     $this->notice->content
451                 ),
452                 __FILE__
453             );
454             return true; // dequeue
455             break;
456         }
457     }
458
459     /*
460      * Publish a notice to Facebook as a status update
461      *
462      * This is the least preferable way to send a notice to Facebook because
463      * it doesn't support attachments and the API method doesn't return
464      * the ID of the post on Facebook.
465      *
466      */
467     function restStatusUpdate()
468     {
469         $fbuid = $this->flink->foreign_id;
470
471         common_debug(
472             sprintf(
473                 "Attempting to post notice %d as a status update for %s (%d), fbuid %s",
474                 $this->notice->id,
475                 $this->user->nickname,
476                 $this->user->id,
477                 $fbuid
478             ),
479             __FILE__
480         );
481
482         $result = $this->facebook->api(
483             array(
484                 'method'               => 'users.setStatus',
485                 'status'               => $this->notice->content,
486                 'status_includes_verb' => true,
487                 'uid'                  => $fbuid
488             )
489         );
490
491         common_log(
492             LOG_INFO,
493             sprintf(
494                 "Posted notice %s as a status update for %s (%d), fbuid %s",
495                 $this->notice->id,
496                 $this->user->nickname,
497                 $this->user->id,
498                 $fbuid
499             ),
500             __FILE__
501         );
502
503     }
504
505     /*
506      * Publish a notice to a Facebook user's stream using the old REST API
507      */
508     function restPublishStream()
509     {
510         $fbuid = $this->flink->foreign_id;
511
512         common_debug(
513             sprintf(
514                 'Attempting to post notice %d as stream item for %s (%d) fbuid %s',
515                 $this->notice->id,
516                 $this->user->nickname,
517                 $this->user->id,
518                 $fbuid
519             ),
520             __FILE__
521         );
522
523         $fbattachment = $this->formatAttachments();
524
525         $result = $this->facebook->api(
526             array(
527                 'method'     => 'stream.publish',
528                 'message'    => $this->notice->content,
529                 'attachment' => $fbattachment,
530                 'uid'        => $fbuid
531             )
532         );
533
534         common_log(
535             LOG_INFO,
536             sprintf(
537                 'Posted notice %d as a %s for %s (%d), fbuid %s',
538                 $this->notice->id,
539                 empty($fbattachment) ? 'stream item' : 'stream item with attachment',
540                 $this->user->nickname,
541                 $this->user->id,
542                 $fbuid
543             ),
544             __FILE__
545         );
546
547     }
548
549     /*
550      * Format attachments for the old REST API stream.publish method
551      *
552      * Note: Old REST API supports multiple attachments per post
553      *
554      */
555     function formatAttachments()
556     {
557
558         $attachments = $this->notice->attachments();
559
560         $fbattachment          = array();
561         $fbattachment['media'] = array();
562
563         foreach($attachments as $attachment)
564         {
565             if($enclosure = $attachment->getEnclosure()){
566                 $fbmedia = $this->getFacebookMedia($enclosure);
567             }else{
568                 $fbmedia = $this->getFacebookMedia($attachment);
569             }
570             if($fbmedia){
571                 $fbattachment['media'][]=$fbmedia;
572             }else{
573                 $fbattachment['name'] = ($attachment->title ?
574                                       $attachment->title : $attachment->url);
575                 $fbattachment['href'] = $attachment->url;
576             }
577         }
578         if(count($fbattachment['media'])>0){
579             unset($fbattachment['name']);
580             unset($fbattachment['href']);
581         }
582         return $fbattachment;
583     }
584
585     /**
586      * given a File objects, returns an associative array suitable for Facebook media
587      */
588     function getFacebookMedia($attachment)
589     {
590         $fbmedia    = array();
591
592         if (strncmp($attachment->mimetype, 'image/', strlen('image/')) == 0) {
593             $fbmedia['type']         = 'image';
594             $fbmedia['src']          = $attachment->url;
595             $fbmedia['href']         = $attachment->url;
596         } else if ($attachment->mimetype == 'audio/mpeg') {
597             $fbmedia['type']         = 'mp3';
598             $fbmedia['src']          = $attachment->url;
599         }else if ($attachment->mimetype == 'application/x-shockwave-flash') {
600             $fbmedia['type']         = 'flash';
601
602             // http://wiki.developers.facebook.com/index.php/Attachment_%28Streams%29
603             // says that imgsrc is required... but we have no value to put in it
604             // $fbmedia['imgsrc']='';
605
606             $fbmedia['swfsrc']       = $attachment->url;
607         }else{
608             return false;
609         }
610         return $fbmedia;
611     }
612
613     /*
614      * Disconnect a user from Facebook by deleting his Foreign_link.
615      * Notifies the user his account has been disconnected by email.
616      */
617     function disconnect()
618     {
619         $fbuid = $this->flink->foreign_id;
620
621         common_log(
622             LOG_INFO,
623             sprintf(
624                 'Removing Facebook link for %s (%d), fbuid %s',
625                 $this->user->nickname,
626                 $this->user->id,
627                 $fbuid
628             ),
629             __FILE__
630         );
631
632         $result = $this->flink->delete();
633
634         if (empty($result)) {
635             common_log(
636                 LOG_ERR,
637                 sprintf(
638                     'Could not remove Facebook link for %s (%d), fbuid %s',
639                     $this->user->nickname,
640                     $this->user->id,
641                     $fbuid
642                 ),
643                 __FILE__
644             );
645             common_log_db_error($flink, 'DELETE', __FILE__);
646         }
647
648         // Notify the user that we are removing their Facebook link
649
650         $result = $this->mailFacebookDisconnect();
651
652         if (!$result) {
653
654             $msg = 'Unable to send email to notify %s (%d), fbuid %s '
655                  . 'about his/her Facebook link being removed.';
656
657             common_log(
658                 LOG_WARNING,
659                 sprintf(
660                     $msg,
661                     $this->user->nickname,
662                     $this->user->id,
663                     $fbuid
664                 ),
665                 __FILE__
666             );
667         }
668     }
669
670     /**
671      * Send a mail message to notify a user that her Facebook link
672      * has been terminated.
673      *
674      * @return boolean success flag
675      */
676     function mailFacebookDisconnect()
677     {
678         $profile = $user->getProfile();
679
680         $siteName = common_config('site', 'name');
681
682         common_switch_locale($user->language);
683
684         $subject = sprintf(
685             _m('Your Facebook connection has been removed'),
686             $siteName
687         );
688
689         $msg = <<<BODY
690 Hi, %1$s. We're sorry to inform you we are unable to publish your notice to
691 Facebook, and have removed the connection between your %2$s account and Facebook.
692
693 This may have happened because you have removed permission for %2$s to post on
694 your behalf, or perhaps you have deactivated your Facebook account. You can
695 reconnect your %s account to Facebook at any time by logging in with Facebook
696 again.
697 BODY;
698         $body = sprintf(
699             _m($msg),
700             $this->user->nickname,
701             $siteName
702         );
703
704         common_switch_locale();
705
706         return mail_to_user($this->user, $subject, $body);
707     }
708
709 }