TwitterBridgePlugin.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. <?php
  2. /**
  3. * StatusNet, the distributed open-source microblogging tool
  4. *
  5. * PHP version 5
  6. *
  7. * LICENCE: This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU Affero General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU Affero General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Affero General Public License
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. *
  20. * @category Plugin
  21. * @package StatusNet
  22. * @author Zach Copley <zach@status.net>
  23. * @author Julien C <chaumond@gmail.com>
  24. * @copyright 2009-2010 Control Yourself, Inc.
  25. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  26. * @link http://status.net/
  27. */
  28. if (!defined('STATUSNET')) {
  29. exit(1);
  30. }
  31. require_once __DIR__ . '/twitter.php';
  32. /**
  33. * Plugin for sending and importing Twitter statuses
  34. *
  35. * This class allows users to link their Twitter accounts
  36. *
  37. * Depends on Favorite plugin.
  38. *
  39. * @category Plugin
  40. * @package StatusNet
  41. * @author Zach Copley <zach@status.net>
  42. * @author Julien C <chaumond@gmail.com>
  43. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  44. * @link http://status.net/
  45. * @link http://twitter.com/
  46. */
  47. class TwitterBridgePlugin extends Plugin
  48. {
  49. const VERSION = GNUSOCIAL_VERSION;
  50. public $adminImportControl = false; // Should the 'import' checkbox be exposed in the admin panel?
  51. /**
  52. * Initializer for the plugin.
  53. */
  54. function initialize()
  55. {
  56. // Allow the key and secret to be passed in
  57. // Control panel will override
  58. if (isset($this->consumer_key)) {
  59. $key = common_config('twitter', 'consumer_key');
  60. if (empty($key)) {
  61. Config::save('twitter', 'consumer_key', $this->consumer_key);
  62. }
  63. }
  64. if (isset($this->consumer_secret)) {
  65. $secret = common_config('twitter', 'consumer_secret');
  66. if (empty($secret)) {
  67. Config::save(
  68. 'twitter',
  69. 'consumer_secret',
  70. $this->consumer_secret
  71. );
  72. }
  73. }
  74. }
  75. /**
  76. * Check to see if there is a consumer key and secret defined
  77. * for Twitter integration.
  78. *
  79. * @return boolean result
  80. */
  81. static function hasKeys()
  82. {
  83. $ckey = common_config('twitter', 'consumer_key');
  84. $csecret = common_config('twitter', 'consumer_secret');
  85. if (empty($ckey) && empty($csecret)) {
  86. $ckey = common_config('twitter', 'global_consumer_key');
  87. $csecret = common_config('twitter', 'global_consumer_secret');
  88. }
  89. if (!empty($ckey) && !empty($csecret)) {
  90. return true;
  91. }
  92. return false;
  93. }
  94. /**
  95. * Add Twitter-related paths to the router table
  96. *
  97. * Hook for RouterInitialized event.
  98. *
  99. * @param URLMapper $m path-to-action mapper
  100. *
  101. * @return boolean hook return
  102. */
  103. public function onRouterInitialized(URLMapper $m)
  104. {
  105. $m->connect('panel/twitter', array('action' => 'twitteradminpanel'));
  106. if (self::hasKeys()) {
  107. $m->connect(
  108. 'twitter/authorization',
  109. array('action' => 'twitterauthorization')
  110. );
  111. $m->connect(
  112. 'settings/twitter', array(
  113. 'action' => 'twittersettings'
  114. )
  115. );
  116. if (common_config('twitter', 'signin')) {
  117. $m->connect(
  118. 'main/twitterlogin',
  119. array('action' => 'twitterlogin')
  120. );
  121. }
  122. }
  123. return true;
  124. }
  125. /*
  126. * Add a login tab for 'Sign in with Twitter'
  127. *
  128. * @param Action $action the current action
  129. *
  130. * @return void
  131. */
  132. function onEndLoginGroupNav($action)
  133. {
  134. $action_name = $action->trimmed('action');
  135. if (self::hasKeys() && common_config('twitter', 'signin')) {
  136. $action->menuItem(
  137. common_local_url('twitterlogin'),
  138. // TRANS: Menu item in login navigation.
  139. _m('MENU','Twitter'),
  140. // TRANS: Title for menu item in login navigation.
  141. _m('Login or register using Twitter.'),
  142. 'twitterlogin' === $action_name
  143. );
  144. }
  145. return true;
  146. }
  147. /**
  148. * Add the Twitter Settings page to the Connect Settings menu
  149. *
  150. * @param Action $action The calling page
  151. *
  152. * @return boolean hook return
  153. */
  154. function onEndConnectSettingsNav($action)
  155. {
  156. if (self::hasKeys()) {
  157. $action_name = $action->trimmed('action');
  158. $action->menuItem(
  159. common_local_url('twittersettings'),
  160. // TRANS: Menu item in connection settings navigation.
  161. _m('MENU','Twitter'),
  162. // TRANS: Title for menu item in connection settings navigation.
  163. _m('Twitter integration options'),
  164. $action_name === 'twittersettings'
  165. );
  166. }
  167. return true;
  168. }
  169. /**
  170. * Add a Twitter queue item for each notice
  171. *
  172. * @param Notice $notice the notice
  173. * @param array &$transports the list of transports (queues)
  174. *
  175. * @return boolean hook return
  176. */
  177. function onStartEnqueueNotice($notice, &$transports)
  178. {
  179. if (self::hasKeys() && $notice->isLocal() && $notice->inScope(null)) {
  180. // Avoid a possible loop
  181. if ($notice->source != 'twitter') {
  182. array_push($transports, 'twitter');
  183. }
  184. }
  185. return true;
  186. }
  187. /**
  188. * Add Twitter bridge daemons to the list of daemons to start
  189. *
  190. * @param array $daemons the list fo daemons to run
  191. *
  192. * @return boolean hook return
  193. */
  194. function onGetValidDaemons(&$daemons)
  195. {
  196. if (self::hasKeys()) {
  197. array_push(
  198. $daemons,
  199. INSTALLDIR
  200. . '/plugins/TwitterBridge/daemons/synctwitterfriends.php'
  201. );
  202. if (common_config('twitterimport', 'enabled')) {
  203. array_push(
  204. $daemons,
  205. INSTALLDIR
  206. . '/plugins/TwitterBridge/daemons/twitterstatusfetcher.php'
  207. );
  208. }
  209. }
  210. return true;
  211. }
  212. /**
  213. * Register Twitter notice queue handler
  214. *
  215. * @param QueueManager $manager
  216. *
  217. * @return boolean hook return
  218. */
  219. function onEndInitializeQueueManager($manager)
  220. {
  221. if (self::hasKeys()) {
  222. // Outgoing notices -> twitter
  223. $manager->connect('twitter', 'TwitterQueueHandler');
  224. // Incoming statuses <- twitter
  225. $manager->connect('tweetin', 'TweetInQueueHandler');
  226. }
  227. return true;
  228. }
  229. /**
  230. * If the plugin's installed, this should be accessible to admins
  231. */
  232. function onAdminPanelCheck($name, &$isOK)
  233. {
  234. if ($name == 'twitter') {
  235. $isOK = true;
  236. return false;
  237. }
  238. return true;
  239. }
  240. /**
  241. * Add a Twitter tab to the admin panel
  242. *
  243. * @param Widget $nav Admin panel nav
  244. *
  245. * @return boolean hook value
  246. */
  247. function onEndAdminPanelNav($nav)
  248. {
  249. if (AdminPanelAction::canAdmin('twitter')) {
  250. $action_name = $nav->action->trimmed('action');
  251. $nav->out->menuItem(
  252. common_local_url('twitteradminpanel'),
  253. // TRANS: Menu item in administrative panel that leads to the Twitter bridge configuration.
  254. _m('Twitter'),
  255. // TRANS: Menu item title in administrative panel that leads to the Twitter bridge configuration.
  256. _m('Twitter bridge configuration page.'),
  257. $action_name == 'twitteradminpanel',
  258. 'nav_twitter_admin_panel'
  259. );
  260. }
  261. return true;
  262. }
  263. /**
  264. * Plugin version data
  265. *
  266. * @param array &$versions array of version blocks
  267. *
  268. * @return boolean hook value
  269. */
  270. function onPluginVersion(&$versions)
  271. {
  272. $versions[] = array(
  273. 'name' => 'TwitterBridge',
  274. 'version' => self::VERSION,
  275. 'author' => 'Zach Copley, Julien C, Jean Baptiste Favre',
  276. 'homepage' => 'http://status.net/wiki/Plugin:TwitterBridge',
  277. // TRANS: Plugin description.
  278. 'rawdescription' => _m('The Twitter "bridge" plugin allows integration ' .
  279. 'of a StatusNet instance with ' .
  280. '<a href="http://twitter.com/">Twitter</a>.'
  281. )
  282. );
  283. return true;
  284. }
  285. /**
  286. * Expose the adminImportControl setting to the administration panel code.
  287. * This allows us to disable the import bridge enabling checkbox for administrators,
  288. * since on a bulk farm site we can't yet automate the import daemon setup.
  289. *
  290. * @return boolean hook value;
  291. */
  292. function onTwitterBridgeAdminImportControl()
  293. {
  294. return (bool)$this->adminImportControl;
  295. }
  296. /**
  297. * When the site is set to ssl=sometimes mode, we should make sure our
  298. * various auth-related pages are on SSL to keep things looking happy.
  299. * Although we're not submitting passwords directly, we do link out to
  300. * an authentication source and it's a lot happier if we've got some
  301. * protection against MitM.
  302. *
  303. * @param string $action name
  304. * @param boolean $ssl outval to force SSL
  305. * @return mixed hook return value
  306. */
  307. function onSensitiveAction($action, &$ssl)
  308. {
  309. $sensitive = array('twitteradminpanel',
  310. 'twittersettings',
  311. 'twitterauthorization',
  312. 'twitterlogin');
  313. if (in_array($action, $sensitive)) {
  314. $ssl = true;
  315. return false;
  316. } else {
  317. return true;
  318. }
  319. }
  320. /**
  321. * Database schema setup
  322. *
  323. * We maintain a table mapping StatusNet notices to Twitter statuses
  324. *
  325. * @see Schema
  326. * @see ColumnDef
  327. *
  328. * @return boolean hook value; true means continue processing, false means stop.
  329. */
  330. function onCheckSchema()
  331. {
  332. $schema = Schema::get();
  333. // For saving the last-synched status of various timelines
  334. // home_timeline, messages (in), messages (out), ...
  335. $schema->ensureTable('twitter_synch_status', Twitter_synch_status::schemaDef());
  336. // For storing user-submitted flags on profiles
  337. $schema->ensureTable('notice_to_status', Notice_to_status::schemaDef());
  338. return true;
  339. }
  340. /**
  341. * If a notice gets deleted, remove the Notice_to_status mapping and
  342. * delete the status on Twitter.
  343. *
  344. * @param User $user The user doing the deleting
  345. * @param Notice $notice The notice getting deleted
  346. *
  347. * @return boolean hook value
  348. */
  349. function onStartDeleteOwnNotice(User $user, Notice $notice)
  350. {
  351. $n2s = Notice_to_status::getKV('notice_id', $notice->id);
  352. if (!empty($n2s)) {
  353. $flink = Foreign_link::getByUserID($notice->profile_id,
  354. TWITTER_SERVICE); // twitter service
  355. if (empty($flink)) {
  356. return true;
  357. }
  358. if (!TwitterOAuthClient::isPackedToken($flink->credentials)) {
  359. $this->log(LOG_INFO, "Skipping deleting notice for {$notice->id} since link is not OAuth.");
  360. return true;
  361. }
  362. try {
  363. $token = TwitterOAuthClient::unpackToken($flink->credentials);
  364. $client = new TwitterOAuthClient($token->key, $token->secret);
  365. $client->statusesDestroy($n2s->status_id);
  366. } catch (Exception $e) {
  367. common_log(LOG_ERR, "Error attempting to delete bridged notice from Twitter: " . $e->getMessage());
  368. }
  369. $n2s->delete();
  370. }
  371. return true;
  372. }
  373. /**
  374. * Notify remote users when their notices get favorited.
  375. *
  376. * @param Profile or User $profile of local user doing the faving
  377. * @param Notice $notice being favored
  378. * @return hook return value
  379. */
  380. function onEndFavorNotice(Profile $profile, Notice $notice)
  381. {
  382. $flink = Foreign_link::getByUserID($profile->id,
  383. TWITTER_SERVICE); // twitter service
  384. if (empty($flink)) {
  385. return true;
  386. }
  387. if (!TwitterOAuthClient::isPackedToken($flink->credentials)) {
  388. $this->log(LOG_INFO, "Skipping fave processing for {$profile->id} since link is not OAuth.");
  389. return true;
  390. }
  391. $status_id = twitter_status_id($notice);
  392. if (empty($status_id)) {
  393. return true;
  394. }
  395. try {
  396. $token = TwitterOAuthClient::unpackToken($flink->credentials);
  397. $client = new TwitterOAuthClient($token->key, $token->secret);
  398. $client->favoritesCreate($status_id);
  399. } catch (Exception $e) {
  400. common_log(LOG_ERR, "Error attempting to favorite bridged notice on Twitter: " . $e->getMessage());
  401. }
  402. return true;
  403. }
  404. /**
  405. * Notify remote users when their notices get de-favorited.
  406. *
  407. * @param Profile $profile Profile person doing the de-faving
  408. * @param Notice $notice Notice being favored
  409. *
  410. * @return hook return value
  411. */
  412. function onEndDisfavorNotice(Profile $profile, Notice $notice)
  413. {
  414. $flink = Foreign_link::getByUserID($profile->id,
  415. TWITTER_SERVICE); // twitter service
  416. if (empty($flink)) {
  417. return true;
  418. }
  419. if (!TwitterOAuthClient::isPackedToken($flink->credentials)) {
  420. $this->log(LOG_INFO, "Skipping fave processing for {$profile->id} since link is not OAuth.");
  421. return true;
  422. }
  423. $status_id = twitter_status_id($notice);
  424. if (empty($status_id)) {
  425. return true;
  426. }
  427. try {
  428. $token = TwitterOAuthClient::unpackToken($flink->credentials);
  429. $client = new TwitterOAuthClient($token->key, $token->secret);
  430. $client->favoritesDestroy($status_id);
  431. } catch (Exception $e) {
  432. common_log(LOG_ERR, "Error attempting to unfavorite bridged notice on Twitter: " . $e->getMessage());
  433. }
  434. return true;
  435. }
  436. function onStartGetProfileUri($profile, &$uri)
  437. {
  438. if (preg_match('!^https?://twitter.com/!', $profile->profileurl)) {
  439. $uri = $profile->profileurl;
  440. return false;
  441. }
  442. return true;
  443. }
  444. /**
  445. * Add links in the user's profile block to their Twitter profile URL.
  446. *
  447. * @param Profile $profile The profile being shown
  448. * @param Array &$links Writeable array of arrays (href, text, image).
  449. *
  450. * @return boolean hook value (true)
  451. */
  452. function onOtherAccountProfiles($profile, &$links)
  453. {
  454. $fuser = null;
  455. $flink = Foreign_link::getByUserID($profile->id, TWITTER_SERVICE);
  456. if (!empty($flink)) {
  457. $fuser = $flink->getForeignUser();
  458. if (!empty($fuser)) {
  459. $links[] = array("href" => $fuser->uri,
  460. "text" => sprintf(_("@%s on Twitter"), $fuser->nickname),
  461. "image" => $this->path("icons/twitter-bird-white-on-blue.png"));
  462. }
  463. }
  464. return true;
  465. }
  466. public function onEndShowHeadElements(Action $action)
  467. {
  468. if (!($action instanceof AttachmentAction)) {
  469. return true;
  470. }
  471. /* Twitter card support. See https://dev.twitter.com/docs/cards */
  472. /* @fixme: should we display twitter cards only for attachments posted
  473. * by local users ? Seems mandatory to display twitter:creator
  474. *
  475. * Author: jbfavre
  476. */
  477. switch ($action->attachment->mimetype) {
  478. case 'image/pjpeg':
  479. case 'image/jpeg':
  480. case 'image/jpg':
  481. case 'image/png':
  482. case 'image/gif':
  483. $action->element('meta', array('name' => 'twitter:card',
  484. 'content' => 'photo'),
  485. null);
  486. $action->element('meta', array('name' => 'twitter:url',
  487. 'content' => common_local_url('attachment',
  488. array('attachment' => $action->attachment->id))),
  489. null );
  490. $action->element('meta', array('name' => 'twitter:image',
  491. 'content' => $action->attachment->url));
  492. $action->element('meta', array('name' => 'twitter:title',
  493. 'content' => $action->attachment->title));
  494. $ns = new AttachmentNoticeSection($action);
  495. $notices = $ns->getNotices();
  496. $noticeArray = $notices->fetchAll();
  497. // Should not have more than 1 notice for this attachment.
  498. if( count($noticeArray) != 1 ) { break; }
  499. $post = $noticeArray[0];
  500. $flink = Foreign_link::getByUserID($post->profile_id, TWITTER_SERVICE);
  501. if( $flink ) { // Our local user has registered Twitter Gateway
  502. $fuser = Foreign_user::getForeignUser($flink->foreign_id, TWITTER_SERVICE);
  503. if( $fuser ) { // Got nickname for local user's Twitter account
  504. $action->element('meta', array('name' => 'twitter:creator',
  505. 'content' => '@'.$fuser->nickname));
  506. }
  507. }
  508. break;
  509. default: break;
  510. }
  511. return true;
  512. }
  513. }