TwitterBridgePlugin.php 19 KB

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