facebookclient.php 35 KB

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