TwitterBridgePlugin.php 19 KB

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