]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/FacebookBridge/lib/facebookclient.php
6037ad0f2da32f333979587e2b110a821fb77a9f
[quix0rs-gnu-social.git] / plugins / FacebookBridge / 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
51     function __construct($notice)
52     {
53         $this->facebook = self::getFacebook();
54
55         if (empty($this->facebook)) {
56             throw new FacebookApiException(
57                 "Could not create Facebook client! Bad application ID or secret?"
58             );
59         }
60
61         $this->notice = $notice;
62
63         $this->flink = Foreign_link::getByUserID(
64             $notice->profile_id,
65             FACEBOOK_SERVICE
66         );
67
68         if (!empty($this->flink)) {
69             $this->user = $this->flink->getUser();
70         }
71     }
72
73     /*
74      * Get an instance of the Facebook Graph SDK object
75      *
76      * @param string $appId     Application
77      * @param string $secret    Facebook API secret
78      *
79      * @return Facebook A Facebook SDK obj
80      */
81     static function getFacebook($appId = null, $secret = null)
82     {
83         // Check defaults and configuration for application ID and secret
84         if (empty($appId)) {
85             $appId = common_config('facebook', 'appid');
86         }
87
88         if (empty($secret)) {
89             $secret = common_config('facebook', 'secret');
90         }
91
92         // If there's no app ID and secret set in the local config, look
93         // for a global one
94         if (empty($appId) || empty($secret)) {
95             $appId  = common_config('facebook', 'global_appid');
96             $secret = common_config('facebook', 'global_secret');
97         }
98
99         if (empty($appId)) {
100             common_log(
101                 LOG_WARNING,
102                 "Couldn't find Facebook application ID!",
103                 __FILE__
104             );
105         }
106
107         if (empty($secret)) {
108             common_log(
109                 LOG_WARNING,
110                 "Couldn't find Facebook application ID!",
111                 __FILE__
112             );
113         }
114
115         return new Facebook(
116             array(
117                'appId'  => $appId,
118                'secret' => $secret,
119                'cookie' => true
120             )
121         );
122     }
123
124     /*
125      * Broadcast a notice to Facebook
126      *
127      * @param Notice $notice    the notice to send
128      */
129     static function facebookBroadcastNotice($notice)
130     {
131         common_debug("ZZZZZ facebookBroadcastNotice() - entered ", __FILE__);
132         $client = new Facebookclient($notice);
133         return $client->sendNotice();
134     }
135
136     /*
137      * Should the notice go to Facebook?
138      */
139     function isFacebookBound() {
140
141         common_debug("ZZZZZ isFacebookBound() - entered", __FILE__);
142
143         if (empty($this->flink)) {
144             // User hasn't setup bridging
145             return false;
146         }
147
148         // Avoid a loop
149         if ($this->notice->source == 'Facebook') {
150             common_log(
151                 LOG_INFO,
152                 sprintf(
153                     'Skipping notice %d because its source is Facebook.',
154                     $this->notice->id
155                 ),
156                 __FILE__
157             );
158             return false;
159         }
160
161         // If the user does not want to broadcast to Facebook, move along
162         if (!($this->flink->noticesync & FOREIGN_NOTICE_SEND == FOREIGN_NOTICE_SEND)) {
163             common_log(
164                 LOG_INFO,
165                 sprintf(
166                     'Skipping notice %d because user has FOREIGN_NOTICE_SEND bit off.',
167                     $this->notice->id
168                 ),
169                 __FILE__
170             );
171             return false;
172         }
173
174         // If it's not a reply, or if the user WANTS to send @-replies,
175         // then, yeah, it can go to Facebook.
176         if (!preg_match('/@[a-zA-Z0-9_]{1,15}\b/u', $this->notice->content) ||
177             ($this->flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
178             return true;
179         }
180
181         common_debug(
182             sprintf(
183                 "ZZZZZ isFacebookBound() - notice %d this notice does not go to Facebook",
184                 $this->notice->id
185             ),
186             __FILE__
187         );
188
189         return false;
190     }
191
192     /*
193      * Determine whether we should send this notice using the Graph API or the
194      * old REST API and then dispatch
195      */
196     function sendNotice()
197     {
198         // If there's nothing in the credentials field try to send via
199         // the Old Rest API
200
201         if ($this->isFacebookBound()) {
202             common_debug("notice is facebook bound", __FILE__);
203             if (empty($this->flink->credentials)) {
204                 return $this->sendOldRest();
205             } else {
206
207                 // Otherwise we most likely have an access token
208                 return $this->sendGraph();
209             }
210         }
211
212         // dequeue
213         return true;
214     }
215
216     /*
217      * Send a notice to Facebook using the Graph API
218      */
219     function sendGraph()
220     {
221         try {
222
223             $fbuid = $this->flink->foreign_id;
224
225             common_debug(
226                 sprintf(
227                     "Attempting use Graph API to post notice %d as a stream item for %s (%d), fbuid %d",
228                     $this->notice->id,
229                     $this->user->nickname,
230                     $this->user->id,
231                     $fbuid
232                 ),
233                 __FILE__
234             );
235
236             $params = array(
237                 'access_token' => $this->flink->credentials,
238                 // XXX: Need to worrry about length of the message?
239                 'message'      => $this->notice->content
240             );
241
242             $attachments = $this->notice->attachments();
243
244             if (!empty($attachments)) {
245
246                 // We can only send one attachment with the Graph API :(
247
248                 $first = array_shift($attachments);
249
250                 if (substr($first->mimetype, 0, 6) == 'image/'
251                     || in_array(
252                         $first->mimetype,
253                         array('application/x-shockwave-flash', 'audio/mpeg' ))) {
254
255                    $params['picture'] = $first->url;
256                    $params['caption'] = 'Click for full size';
257                    $params['source']  = $first->url;
258                 }
259
260             }
261
262             $result = $this->facebook->api(
263                 sprintf('/%s/feed', $fbuid), 'post', $params
264             );
265
266             // Save a mapping
267             Notice_to_item::saveNew($this->notice->id, $result['id']);
268
269             common_log(
270                 LOG_INFO,
271                 sprintf(
272                     "Posted notice %d as a stream item for %s (%d), fbuid %d",
273                     $this->notice->id,
274                     $this->user->nickname,
275                     $this->user->id,
276                     $fbuid
277                 ),
278                 __FILE__
279             );
280
281         } catch (FacebookApiException $e) {
282             return $this->handleFacebookError($e);
283         }
284
285         return true;
286     }
287
288     /*
289      * Send a notice to Facebook using the deprecated Old REST API. We need this
290      * for backwards compatibility. Users who signed up for Facebook bridging
291      * using the old Facebook Canvas application do not have an OAuth 2.0
292      * access token.
293      */
294     function sendOldRest()
295     {
296         try {
297
298             $canPublish = $this->checkPermission('publish_stream');
299             $canUpdate  = $this->checkPermission('status_update');
300
301             // We prefer to use stream.publish, because it can handle
302             // attachments and returns the ID of the published item
303
304             if ($canPublish == 1) {
305                 $this->restPublishStream();
306             } else if ($canUpdate == 1) {
307                 // as a last resort we can just update the user's "status"
308                 $this->restStatusUpdate();
309             } else {
310
311                 $msg = 'Not sending notice %d to Facebook because user %s '
312                      . '(%d), fbuid %d,  does not have \'status_update\' '
313                      . 'or \'publish_stream\' permission.';
314
315                 common_log(
316                     LOG_WARNING,
317                     sprintf(
318                         $msg,
319                         $this->notice->id,
320                         $this->user->nickname,
321                         $this->user->id,
322                         $this->flink->foreign_id
323                     ),
324                     __FILE__
325                 );
326             }
327
328         } catch (FacebookApiException $e) {
329             return $this->handleFacebookError($e);
330         }
331
332         return true;
333     }
334
335     /*
336      * Query Facebook to to see if a user has permission
337      *
338      *
339      *
340      * @param $permission the permission to check for - must be either
341      *                    public_stream or status_update
342      *
343      * @return boolean result
344      */
345     function checkPermission($permission)
346     {
347         if (!in_array($permission, array('publish_stream', 'status_update'))) {
348              throw new ServerException("No such permission!");
349         }
350
351         $fbuid = $this->flink->foreign_id;
352
353         common_debug(
354             sprintf(
355                 'Checking for %s permission for user %s (%d), fbuid %d',
356                 $permission,
357                 $this->user->nickname,
358                 $this->user->id,
359                 $fbuid
360             ),
361             __FILE__
362         );
363
364         $hasPermission = $this->facebook->api(
365             array(
366                 'method'   => 'users.hasAppPermission',
367                 'ext_perm' => $permission,
368                 'uid'      => $fbuid
369             )
370         );
371
372         if ($hasPermission == 1) {
373
374             common_debug(
375                 sprintf(
376                     '%s (%d), fbuid %d has %s permission',
377                     $permission,
378                     $this->user->nickname,
379                     $this->user->id,
380                     $fbuid
381                 ),
382                 __FILE__
383             );
384
385             return true;
386
387         } else {
388
389             $logMsg = '%s (%d), fbuid $fbuid does NOT have %s permission.'
390                     . 'Facebook returned: %s';
391
392             common_debug(
393                 sprintf(
394                     $logMsg,
395                     $this->user->nickname,
396                     $this->user->id,
397                     $permission,
398                     $fbuid,
399                     var_export($result, true)
400                 ),
401                 __FILE__
402             );
403
404             return false;
405
406         }
407     }
408
409     /*
410      * Handle a Facebook API Exception
411      *
412      * @param FacebookApiException $e the exception
413      *
414      */
415     function handleFacebookError($e)
416     {
417         $fbuid  = $this->flink->foreign_id;
418         $errmsg = $e->getMessage();
419         $code   = $e->getCode();
420
421         // The Facebook PHP SDK seems to always set the code attribute
422         // of the Exception to 0; they put the real error code in
423         // the message. Gar!
424         if ($code == 0) {
425             preg_match('/^\(#(?<code>\d+)\)/', $errmsg, $matches);
426             $code = $matches['code'];
427         }
428
429         // XXX: Check for any others?
430         switch($code) {
431          case 100: // Invalid parameter
432             $msg = 'Facebook claims notice %d was posted with an invalid '
433                  . 'parameter (error code 100 - %s) Notice details: '
434                  . '[nickname=%s, user id=%d, fbuid=%d, content="%s"]. '
435                  . 'Dequeing.';
436             common_log(
437                 LOG_ERR, sprintf(
438                     $msg,
439                     $this->notice->id,
440                     $errmsg,
441                     $this->user->nickname,
442                     $this->user->id,
443                     $fbuid,
444                     $this->notice->content
445                 ),
446                 __FILE__
447             );
448             return true;
449             break;
450          case 200: // Permissions error
451          case 250: // Updating status requires the extended permission status_update
452             $this->disconnect();
453             return true; // dequeue
454             break;
455          case 341: // Feed action request limit reached
456                 $msg = '%s (userid=%d, fbuid=%d) has exceeded his/her limit '
457                      . 'for posting notices to Facebook today. Dequeuing '
458                      . 'notice %d';
459                 common_log(
460                     LOG_INFO, sprintf(
461                         $msg,
462                         $user->nickname,
463                         $user->id,
464                         $fbuid,
465                         $this->notice->id
466                     ),
467                     __FILE__
468                 );
469             // @fixme: We want to rety at a later time when the throttling has expired
470             // instead of just giving up.
471             return true;
472             break;
473          default:
474             $msg = 'Facebook returned an error we don\'t know how to deal with '
475                  . 'when posting notice %d. Error code: %d, error message: "%s"'
476                  . ' Notice details: [nickname=%s, user id=%d, fbuid=%d, '
477                  . 'notice content="%s"]. Dequeing.';
478             common_log(
479                 LOG_ERR, sprintf(
480                     $msg,
481                     $this->notice->id,
482                     $code,
483                     $errmsg,
484                     $this->user->nickname,
485                     $this->user->id,
486                     $fbuid,
487                     $this->notice->content
488                 ),
489                 __FILE__
490             );
491             return true; // dequeue
492             break;
493         }
494     }
495
496     /*
497      * Publish a notice to Facebook as a status update
498      *
499      * This is the least preferable way to send a notice to Facebook because
500      * it doesn't support attachments and the API method doesn't return
501      * the ID of the post on Facebook.
502      *
503      */
504     function restStatusUpdate()
505     {
506         $fbuid = $this->flink->foreign_id;
507
508         common_debug(
509             sprintf(
510                 "Attempting to post notice %d as a status update for %s (%d), fbuid %d",
511                 $this->notice->id,
512                 $this->user->nickname,
513                 $this->user->id,
514                 $fbuid
515             ),
516             __FILE__
517         );
518
519         $result = $this->facebook->api(
520             array(
521                 'method'               => 'users.setStatus',
522                 'status'               => $this->formatMessage(),
523                 'status_includes_verb' => true,
524                 'uid'                  => $fbuid
525             )
526         );
527
528         if ($result == 1) { // 1 is success
529
530             common_log(
531                 LOG_INFO,
532                 sprintf(
533                     "Posted notice %s as a status update for %s (%d), fbuid %d",
534                     $this->notice->id,
535                     $this->user->nickname,
536                     $this->user->id,
537                     $fbuid
538                 ),
539                 __FILE__
540             );
541
542             // There is no item ID returned for status update so we can't
543             // save a Notice_to_item mapping
544
545         } else {
546
547             $msg = sprintf(
548                 "Error posting notice %s as a status update for %s (%d), fbuid %d - error code: %s",
549                 $this->notice->id,
550                 $this->user->nickname,
551                 $this->user->id,
552                 $fbuid,
553                 $result // will contain 0, or an error
554             );
555
556             throw new FacebookApiException($msg, $result);
557         }
558     }
559
560     /*
561      * Publish a notice to a Facebook user's stream using the old REST API
562      */
563     function restPublishStream()
564     {
565         $fbuid = $this->flink->foreign_id;
566
567         common_debug(
568             sprintf(
569                 'Attempting to post notice %d as stream item for %s (%d) fbuid %d',
570                 $this->notice->id,
571                 $this->user->nickname,
572                 $this->user->id,
573                 $fbuid
574             ),
575             __FILE__
576         );
577
578         $fbattachment = $this->formatAttachments();
579
580         $result = $this->facebook->api(
581             array(
582                 'method'     => 'stream.publish',
583                 'message'    => $this->formatMessage(),
584                 'attachment' => $fbattachment,
585                 'uid'        => $fbuid
586             )
587         );
588
589         if (!empty($result)) { // result will contain the item ID
590
591             // Save a mapping
592             Notice_to_item::saveNew($this->notice->id, $result);
593
594             common_log(
595                 LOG_INFO,
596                 sprintf(
597                     'Posted notice %d as a %s for %s (%d), fbuid %d',
598                     $this->notice->id,
599                     empty($fbattachment) ? 'stream item' : 'stream item with attachment',
600                     $this->user->nickname,
601                     $this->user->id,
602                     $fbuid
603                 ),
604                 __FILE__
605             );
606
607         } else {
608
609             $msg = sprintf(
610                 'Could not post notice %d as a %s for %s (%d), fbuid %d - error code: %s',
611                 $this->notice->id,
612                 empty($fbattachment) ? 'stream item' : 'stream item with attachment',
613                 $this->user->nickname,
614                 $this->user->id,
615                 $result, // result will contain an error code
616                 $fbuid
617             );
618
619             throw new FacebookApiException($msg, $result);
620         }
621     }
622
623     /*
624      * Format the text message of a stream item so it's appropriate for
625      * sending to Facebook. If the notice is too long, truncate it, and
626      * add a linkback to the original notice at the end.
627      *
628      * @return String $txt the formated message
629      */
630     function formatMessage()
631     {
632         // Start with the plaintext source of this notice...
633         $txt = $this->notice->content;
634
635         // Facebook has a 420-char hardcoded max.
636         if (mb_strlen($statustxt) > 420) {
637             $noticeUrl = common_shorten_url($this->notice->uri);
638             $urlLen = mb_strlen($noticeUrl);
639             $txt = mb_substr($statustxt, 0, 420 - ($urlLen + 3)) . ' â€¦ ' . $noticeUrl;
640         }
641
642         return $txt;
643     }
644
645     /*
646      * Format attachments for the old REST API stream.publish method
647      *
648      * Note: Old REST API supports multiple attachments per post
649      *
650      */
651     function formatAttachments()
652     {
653         $attachments = $this->notice->attachments();
654
655         $fbattachment          = array();
656         $fbattachment['media'] = array();
657
658         foreach($attachments as $attachment)
659         {
660             if($enclosure = $attachment->getEnclosure()){
661                 $fbmedia = $this->getFacebookMedia($enclosure);
662             }else{
663                 $fbmedia = $this->getFacebookMedia($attachment);
664             }
665             if($fbmedia){
666                 $fbattachment['media'][]=$fbmedia;
667             }else{
668                 $fbattachment['name'] = ($attachment->title ?
669                                       $attachment->title : $attachment->url);
670                 $fbattachment['href'] = $attachment->url;
671             }
672         }
673         if(count($fbattachment['media'])>0){
674             unset($fbattachment['name']);
675             unset($fbattachment['href']);
676         }
677         return $fbattachment;
678     }
679
680     /**
681      * given a File objects, returns an associative array suitable for Facebook media
682      */
683     function getFacebookMedia($attachment)
684     {
685         $fbmedia    = array();
686
687         if (strncmp($attachment->mimetype, 'image/', strlen('image/')) == 0) {
688             $fbmedia['type']         = 'image';
689             $fbmedia['src']          = $attachment->url;
690             $fbmedia['href']         = $attachment->url;
691         } else if ($attachment->mimetype == 'audio/mpeg') {
692             $fbmedia['type']         = 'mp3';
693             $fbmedia['src']          = $attachment->url;
694         }else if ($attachment->mimetype == 'application/x-shockwave-flash') {
695             $fbmedia['type']         = 'flash';
696
697             // http://wiki.developers.facebook.com/index.php/Attachment_%28Streams%29
698             // says that imgsrc is required... but we have no value to put in it
699             // $fbmedia['imgsrc']='';
700
701             $fbmedia['swfsrc']       = $attachment->url;
702         }else{
703             return false;
704         }
705         return $fbmedia;
706     }
707
708     /*
709      * Disconnect a user from Facebook by deleting his Foreign_link.
710      * Notifies the user his account has been disconnected by email.
711      */
712     function disconnect()
713     {
714         $fbuid = $this->flink->foreign_id;
715
716         common_log(
717             LOG_INFO,
718             sprintf(
719                 'Removing Facebook link for %s (%d), fbuid %d',
720                 $this->user->nickname,
721                 $this->user->id,
722                 $fbuid
723             ),
724             __FILE__
725         );
726
727         $result = $this->flink->delete();
728
729         if (empty($result)) {
730             common_log(
731                 LOG_ERR,
732                 sprintf(
733                     'Could not remove Facebook link for %s (%d), fbuid %d',
734                     $this->user->nickname,
735                     $this->user->id,
736                     $fbuid
737                 ),
738                 __FILE__
739             );
740             common_log_db_error($flink, 'DELETE', __FILE__);
741         }
742
743         // Notify the user that we are removing their Facebook link
744         if (!empty($this->user->email)) {
745             $result = $this->mailFacebookDisconnect();
746
747             if (!$result) {
748
749                 $msg = 'Unable to send email to notify %s (%d), fbuid %d '
750                      . 'about his/her Facebook link being removed.';
751
752                 common_log(
753                     LOG_WARNING,
754                     sprintf(
755                         $msg,
756                         $this->user->nickname,
757                         $this->user->id,
758                         $fbuid
759                     ),
760                     __FILE__
761                 );
762             }
763
764         } else {
765
766             $msg = 'Unable to send email to notify %s (%d), fbuid %d '
767                  . 'about his/her Facebook link being removed because the '
768                  . 'user has not set an email address.';
769
770             common_log(
771                 LOG_WARNING,
772                 sprintf(
773                     $msg,
774                     $this->user->nickname,
775                     $this->user->id,
776                     $fbuid
777                 ),
778                 __FILE__
779             );
780         }
781     }
782
783     /**
784      * Send a mail message to notify a user that her Facebook link
785      * has been terminated.
786      *
787      * @return boolean success flag
788      */
789     function mailFacebookDisconnect()
790     {
791         $profile = $this->user->getProfile();
792
793         $siteName = common_config('site', 'name');
794
795         common_switch_locale($this->user->language);
796
797         $subject = _m('Your Facebook connection has been removed');
798
799         $msg = <<<BODY
800 Hi %1$s,
801
802 We're sorry to inform you we are unable to publish your notice to
803 Facebook, and have removed the connection between your %2$s account and
804 Facebook.
805
806 This may have happened because you have removed permission for %2$s
807 to post on your behalf, or perhaps you have deactivated your Facebook
808 account. You can reconnect your %s account to Facebook at any time by
809 logging in with Facebook again.
810
811 Sincerely,
812
813 %2$s
814 BODY;
815         $body = sprintf(
816             _m($msg),
817             $this->user->nickname,
818             $siteName
819         );
820
821         common_switch_locale();
822
823         $result = mail_to_user($this->user, $subject, $body);
824
825         if (empty($this->user->password)) {
826             $result = self::emailWarn($this->user);
827         }
828
829         return $result;
830     }
831
832     /*
833      * Send the user an email warning that their account has been
834      * disconnected and he/she has no way to login and must contact
835      * the site administrator for help.
836      *
837      * @param User $user the deauthorizing user
838      *
839      */
840     static function emailWarn($user)
841     {
842         $profile = $user->getProfile();
843
844         $siteName  = common_config('site', 'name');
845         $siteEmail = common_config('site', 'email');
846
847         if (empty($siteEmail)) {
848             common_log(
849                 LOG_WARNING,
850                     "No site email address configured. Please set one."
851             );
852         }
853
854         common_switch_locale($user->language);
855
856         $subject = _m('Contact the %s administrator to retrieve your account');
857
858         $msg = <<<BODY
859 Hi %1$s,
860
861 We've noticed you have deauthorized the Facebook connection for your
862 %2$s account.  You have not set a password for your %2$s account yet, so
863 you will not be able to login. If you wish to continue using your %2$s
864 account, please contact the site administrator (%3$s) to set a password.
865
866 Sincerely,
867
868 %2$s
869 BODY;
870         $body = sprintf(
871             _m($msg),
872             $user->nickname,
873             $siteName,
874             $siteEmail
875         );
876
877         common_switch_locale();
878
879         if (mail_to_user($user, $subject, $body)) {
880             common_log(
881                 LOG_INFO,
882                 sprintf(
883                     'Sent account lockout warning to %s (%d)',
884                     $user->nickname,
885                     $user->id
886                 ),
887                 __FILE__
888             );
889         } else {
890             common_log(
891                 LOG_WARNING,
892                 sprintf(
893                     'Unable to send account lockout warning to %s (%d)',
894                     $user->nickname,
895                     $user->id
896                 ),
897                 __FILE__
898             );
899         }
900     }
901
902     /*
903      * Check to see if we have a mapping to a copy of this notice
904      * on Facebook
905      *
906      * @param Notice $notice the notice to check
907      *
908      * @return mixed null if it can't find one, or the id of the Facebook
909      *               stream item
910      */
911     static function facebookStatusId($notice)
912     {
913         $n2i = Notice_to_item::staticGet('notice_id', $notice->id);
914
915         if (empty($n2i)) {
916             return null;
917         } else {
918             return $n2i->item_id;
919         }
920     }
921
922     /*
923      * Save a Foreign_user record of a Facebook user
924      *
925      * @param object $fbuser a Facebook Graph API user obj
926      *                       See: http://developers.facebook.com/docs/reference/api/user
927      * @return mixed $result Id or key
928      *
929      */
930     static function addFacebookUser($fbuser)
931     {
932         // remove any existing, possibly outdated, record
933         $luser = Foreign_user::getForeignUser($fbuser['id'], FACEBOOK_SERVICE);
934
935         if (!empty($luser)) {
936
937             $result = $luser->delete();
938
939             if ($result != false) {
940                 common_log(
941                     LOG_INFO,
942                     sprintf(
943                         'Removed old Facebook user: %s, fbuid %d',
944                         $fbuid['name'],
945                         $fbuid['id']
946                     ),
947                     __FILE__
948                 );
949             }
950         }
951
952         $fuser = new Foreign_user();
953
954         $fuser->nickname = $fbuser['name'];
955         $fuser->uri      = $fbuser['link'];
956         $fuser->id       = $fbuser['id'];
957         $fuser->service  = FACEBOOK_SERVICE;
958         $fuser->created  = common_sql_now();
959
960         $result = $fuser->insert();
961
962         if (empty($result)) {
963             common_log(
964                 LOG_WARNING,
965                     sprintf(
966                         'Failed to add new Facebook user: %s, fbuid %d',
967                         $fbuser['name'],
968                         $fbuser['id']
969                     ),
970                     __FILE__
971             );
972
973             common_log_db_error($fuser, 'INSERT', __FILE__);
974         } else {
975             common_log(
976                 LOG_INFO,
977                 sprintf(
978                     'Added new Facebook user: %s, fbuid %d',
979                     $fbuser['name'],
980                     $fbuser['id']
981                 ),
982                 __FILE__
983             );
984         }
985
986         return $result;
987     }
988
989     /*
990      * Remove an item from a Facebook user's feed if we have a mapping
991      * for it.
992      */
993     function streamRemove()
994     {
995         $n2i = Notice_to_item::staticGet('notice_id', $this->notice->id);
996
997         if (!empty($this->flink) && !empty($n2i)) {
998
999             try {
1000
1001                 $result = $this->facebook->api(
1002                     array(
1003                         'method'  => 'stream.remove',
1004                         'post_id' => $n2i->item_id,
1005                         'uid'     => $this->flink->foreign_id
1006                     )
1007                 );
1008
1009                 if (!empty($result) && result == true) {
1010
1011                     common_log(
1012                       LOG_INFO,
1013                         sprintf(
1014                             'Deleted Facebook item: %s for %s (%d), fbuid %d',
1015                             $n2i->item_id,
1016                             $this->user->nickname,
1017                             $this->user->id,
1018                             $this->flink->foreign_id
1019                         ),
1020                         __FILE__
1021                     );
1022
1023                     $n2i->delete();
1024
1025                 } else {
1026                     throw new FaceboookApiException(var_export($result, true));
1027                 }
1028
1029             } catch (FacebookApiException $e) {
1030                 common_log(
1031                   LOG_WARNING,
1032                     sprintf(
1033                         'Could not deleted Facebook item: %s for %s (%d), '
1034                             . 'fbuid %d - (API error: %s) item already deleted '
1035                             . 'on Facebook? ',
1036                         $n2i->item_id,
1037                         $this->user->nickname,
1038                         $this->user->id,
1039                         $this->flink->foreign_id,
1040                         $e
1041                     ),
1042                     __FILE__
1043                 );
1044             }
1045         }
1046     }
1047
1048     /*
1049      * Like an item in a Facebook user's feed if we have a mapping
1050      * for it.
1051      */
1052     function like()
1053     {
1054         $n2i = Notice_to_item::staticGet('notice_id', $this->notice->id);
1055
1056         if (!empty($this->flink) && !empty($n2i)) {
1057
1058             try {
1059
1060                 $result = $this->facebook->api(
1061                     array(
1062                         'method'  => 'stream.addlike',
1063                         'post_id' => $n2i->item_id,
1064                         'uid'     => $this->flink->foreign_id
1065                     )
1066                 );
1067
1068                 if (!empty($result) && result == true) {
1069
1070                     common_log(
1071                       LOG_INFO,
1072                         sprintf(
1073                             'Added like for item: %s for %s (%d), fbuid %d',
1074                             $n2i->item_id,
1075                             $this->user->nickname,
1076                             $this->user->id,
1077                             $this->flink->foreign_id
1078                         ),
1079                         __FILE__
1080                     );
1081
1082                 } else {
1083                     throw new FacebookApiException(var_export($result, true));
1084                 }
1085
1086             } catch (FacebookApiException $e) {
1087                 common_log(
1088                   LOG_WARNING,
1089                     sprintf(
1090                         'Could not like Facebook item: %s for %s (%d), '
1091                             . 'fbuid %d (API error: %s)',
1092                         $n2i->item_id,
1093                         $this->user->nickname,
1094                         $this->user->id,
1095                         $this->flink->foreign_id,
1096                         $e
1097                     ),
1098                     __FILE__
1099                 );
1100             }
1101         }
1102     }
1103
1104     /*
1105      * Unlike an item in a Facebook user's feed if we have a mapping
1106      * for it.
1107      */
1108     function unLike()
1109     {
1110         $n2i = Notice_to_item::staticGet('notice_id', $this->notice->id);
1111
1112         if (!empty($this->flink) && !empty($n2i)) {
1113
1114             try {
1115
1116                 $result = $this->facebook->api(
1117                     array(
1118                         'method'  => 'stream.removeLike',
1119                         'post_id' => $n2i->item_id,
1120                         'uid'     => $this->flink->foreign_id
1121                     )
1122                 );
1123
1124                 if (!empty($result) && result == true) {
1125
1126                     common_log(
1127                       LOG_INFO,
1128                         sprintf(
1129                             'Removed like for item: %s for %s (%d), fbuid %d',
1130                             $n2i->item_id,
1131                             $this->user->nickname,
1132                             $this->user->id,
1133                             $this->flink->foreign_id
1134                         ),
1135                         __FILE__
1136                     );
1137
1138                 } else {
1139                     throw new FacebookApiException(var_export($result, true));
1140                 }
1141
1142             } catch (FacebookApiException $e) {
1143                   common_log(
1144                   LOG_WARNING,
1145                     sprintf(
1146                         'Could not remove like for Facebook item: %s for %s '
1147                           . '(%d), fbuid %d (API error: %s)',
1148                         $n2i->item_id,
1149                         $this->user->nickname,
1150                         $this->user->id,
1151                         $this->flink->foreign_id,
1152                         $e
1153                     ),
1154                     __FILE__
1155                 );
1156             }
1157         }
1158     }
1159
1160 }