facebookclient.php 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133
  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. try {
  64. $this->flink = Foreign_link::getByUserID($profile_id, FACEBOOK_SERVICE);
  65. $this->user = $this->flink->getUser();
  66. } catch (NoResultException $e) {
  67. // at least $this->flink could've gotten set to something,
  68. // but the logic that was here before didn't care, so let's not care either
  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. try {
  802. $fuser = Foreign_user::getForeignUser($fbuser->id, FACEBOOK_SERVICE);
  803. $result = $fuser->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. } catch (NoResultException $e) {
  816. // no old foreign users exist for this id
  817. }
  818. $fuser = new Foreign_user();
  819. $fuser->nickname = $fbuser->username;
  820. $fuser->uri = $fbuser->link;
  821. $fuser->id = $fbuser->id;
  822. $fuser->service = FACEBOOK_SERVICE;
  823. $fuser->created = common_sql_now();
  824. $result = $fuser->insert();
  825. if (empty($result)) {
  826. common_log(
  827. LOG_WARNING,
  828. sprintf(
  829. 'Failed to add new Facebook user: %s, fbuid %d',
  830. $fbuser->username,
  831. $fbuser->id
  832. ),
  833. __FILE__
  834. );
  835. common_log_db_error($fuser, 'INSERT', __FILE__);
  836. } else {
  837. common_log(
  838. LOG_INFO,
  839. sprintf(
  840. 'Added new Facebook user: %s, fbuid %d',
  841. $fbuser->name,
  842. $fbuser->id
  843. ),
  844. __FILE__
  845. );
  846. }
  847. return $result;
  848. }
  849. /*
  850. * Remove an item from a Facebook user's feed if we have a mapping
  851. * for it.
  852. */
  853. function streamRemove()
  854. {
  855. $n2i = Notice_to_item::getKV('notice_id', $this->notice->id);
  856. if (!empty($this->flink) && !empty($n2i)) {
  857. try {
  858. $result = $this->facebook->api(
  859. array(
  860. 'method' => 'stream.remove',
  861. 'post_id' => $n2i->item_id,
  862. 'uid' => $this->flink->foreign_id
  863. )
  864. );
  865. if (!empty($result) && result == true) {
  866. common_log(
  867. LOG_INFO,
  868. sprintf(
  869. 'Deleted Facebook item: %s for %s (%d), fbuid %d',
  870. $n2i->item_id,
  871. $this->user->nickname,
  872. $this->user->id,
  873. $this->flink->foreign_id
  874. ),
  875. __FILE__
  876. );
  877. $n2i->delete();
  878. } else {
  879. throw new FaceboookApiException(var_export($result, true));
  880. }
  881. } catch (FacebookApiException $e) {
  882. common_log(
  883. LOG_WARNING,
  884. sprintf(
  885. 'Could not deleted Facebook item: %s for %s (%d), '
  886. . 'fbuid %d - (API error: %s) item already deleted '
  887. . 'on Facebook? ',
  888. $n2i->item_id,
  889. $this->user->nickname,
  890. $this->user->id,
  891. $this->flink->foreign_id,
  892. $e
  893. ),
  894. __FILE__
  895. );
  896. }
  897. }
  898. }
  899. /*
  900. * Like an item in a Facebook user's feed if we have a mapping
  901. * for it.
  902. */
  903. function like()
  904. {
  905. $n2i = Notice_to_item::getKV('notice_id', $this->notice->id);
  906. if (!empty($this->flink) && !empty($n2i)) {
  907. try {
  908. $result = $this->facebook->api(
  909. array(
  910. 'method' => 'stream.addlike',
  911. 'post_id' => $n2i->item_id,
  912. 'uid' => $this->flink->foreign_id
  913. )
  914. );
  915. if (!empty($result) && result == true) {
  916. common_log(
  917. LOG_INFO,
  918. sprintf(
  919. 'Added like for item: %s for %s (%d), fbuid %d',
  920. $n2i->item_id,
  921. $this->user->nickname,
  922. $this->user->id,
  923. $this->flink->foreign_id
  924. ),
  925. __FILE__
  926. );
  927. } else {
  928. throw new FacebookApiException(var_export($result, true));
  929. }
  930. } catch (FacebookApiException $e) {
  931. common_log(
  932. LOG_WARNING,
  933. sprintf(
  934. 'Could not like Facebook item: %s for %s (%d), '
  935. . 'fbuid %d (API error: %s)',
  936. $n2i->item_id,
  937. $this->user->nickname,
  938. $this->user->id,
  939. $this->flink->foreign_id,
  940. $e
  941. ),
  942. __FILE__
  943. );
  944. }
  945. }
  946. }
  947. /*
  948. * Unlike an item in a Facebook user's feed if we have a mapping
  949. * for it.
  950. */
  951. function unLike()
  952. {
  953. $n2i = Notice_to_item::getKV('notice_id', $this->notice->id);
  954. if (!empty($this->flink) && !empty($n2i)) {
  955. try {
  956. $result = $this->facebook->api(
  957. array(
  958. 'method' => 'stream.removeLike',
  959. 'post_id' => $n2i->item_id,
  960. 'uid' => $this->flink->foreign_id
  961. )
  962. );
  963. if (!empty($result) && result == true) {
  964. common_log(
  965. LOG_INFO,
  966. sprintf(
  967. 'Removed like for item: %s for %s (%d), fbuid %d',
  968. $n2i->item_id,
  969. $this->user->nickname,
  970. $this->user->id,
  971. $this->flink->foreign_id
  972. ),
  973. __FILE__
  974. );
  975. } else {
  976. throw new FacebookApiException(var_export($result, true));
  977. }
  978. } catch (FacebookApiException $e) {
  979. common_log(
  980. LOG_WARNING,
  981. sprintf(
  982. 'Could not remove like for Facebook item: %s for %s '
  983. . '(%d), fbuid %d (API error: %s)',
  984. $n2i->item_id,
  985. $this->user->nickname,
  986. $this->user->id,
  987. $this->flink->foreign_id,
  988. $e
  989. ),
  990. __FILE__
  991. );
  992. }
  993. }
  994. }
  995. }