OStatusPlugin.php 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389
  1. <?php
  2. /*
  3. * StatusNet - the distributed open-source microblogging tool
  4. * Copyright (C) 2009-2010, StatusNet, Inc.
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. /**
  20. * OStatusPlugin implementation for GNU Social
  21. *
  22. * Depends on: WebFinger plugin
  23. *
  24. * @package OStatusPlugin
  25. * @maintainer Brion Vibber <brion@status.net>
  26. */
  27. if (!defined('GNUSOCIAL')) { exit(1); }
  28. set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/phpseclib');
  29. class FeedSubException extends Exception
  30. {
  31. function __construct($msg=null)
  32. {
  33. $type = get_class($this);
  34. if ($msg) {
  35. parent::__construct("$type: $msg");
  36. } else {
  37. parent::__construct($type);
  38. }
  39. }
  40. }
  41. class OStatusPlugin extends Plugin
  42. {
  43. /**
  44. * Hook for RouterInitialized event.
  45. *
  46. * @param URLMapper $m path-to-action mapper
  47. * @return boolean hook return
  48. */
  49. public function onRouterInitialized(URLMapper $m)
  50. {
  51. // Discovery actions
  52. $m->connect('main/ostatustag',
  53. array('action' => 'ostatustag'));
  54. $m->connect('main/ostatustag?nickname=:nickname',
  55. array('action' => 'ostatustag'), array('nickname' => '[A-Za-z0-9_-]+'));
  56. $m->connect('main/ostatus/nickname/:nickname',
  57. array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
  58. $m->connect('main/ostatus/group/:group',
  59. array('action' => 'ostatusinit'), array('group' => '[A-Za-z0-9_-]+'));
  60. $m->connect('main/ostatus/peopletag/:peopletag/tagger/:tagger',
  61. array('action' => 'ostatusinit'), array('tagger' => '[A-Za-z0-9_-]+',
  62. 'peopletag' => '[A-Za-z0-9_-]+'));
  63. $m->connect('main/ostatus',
  64. array('action' => 'ostatusinit'));
  65. // Remote subscription actions
  66. $m->connect('main/ostatussub',
  67. array('action' => 'ostatussub'));
  68. $m->connect('main/ostatusgroup',
  69. array('action' => 'ostatusgroup'));
  70. $m->connect('main/ostatuspeopletag',
  71. array('action' => 'ostatuspeopletag'));
  72. // PuSH actions
  73. $m->connect('main/push/hub', array('action' => 'pushhub'));
  74. $m->connect('main/push/callback/:feed',
  75. array('action' => 'pushcallback'),
  76. array('feed' => '[0-9]+'));
  77. // Salmon endpoint
  78. $m->connect('main/salmon/user/:id',
  79. array('action' => 'usersalmon'),
  80. array('id' => '[0-9]+'));
  81. $m->connect('main/salmon/group/:id',
  82. array('action' => 'groupsalmon'),
  83. array('id' => '[0-9]+'));
  84. $m->connect('main/salmon/peopletag/:id',
  85. array('action' => 'peopletagsalmon'),
  86. array('id' => '[0-9]+'));
  87. return true;
  88. }
  89. /**
  90. * Set up queue handlers for outgoing hub pushes
  91. * @param QueueManager $qm
  92. * @return boolean hook return
  93. */
  94. function onEndInitializeQueueManager(QueueManager $qm)
  95. {
  96. // Prepare outgoing distributions after notice save.
  97. $qm->connect('ostatus', 'OStatusQueueHandler');
  98. // Outgoing from our internal PuSH hub
  99. $qm->connect('hubconf', 'HubConfQueueHandler');
  100. $qm->connect('hubprep', 'HubPrepQueueHandler');
  101. $qm->connect('hubout', 'HubOutQueueHandler');
  102. // Outgoing Salmon replies (when we don't need a return value)
  103. $qm->connect('salmon', 'SalmonQueueHandler');
  104. // Incoming from a foreign PuSH hub
  105. $qm->connect('pushin', 'PushInQueueHandler');
  106. return true;
  107. }
  108. /**
  109. * Put saved notices into the queue for pubsub distribution.
  110. */
  111. function onStartEnqueueNotice($notice, &$transports)
  112. {
  113. if ($notice->inScope(null)) {
  114. // put our transport first, in case there's any conflict (like OMB)
  115. array_unshift($transports, 'ostatus');
  116. $this->log(LOG_INFO, "Notice {$notice->id} queued for OStatus processing");
  117. } else {
  118. // FIXME: we don't do privacy-controlled OStatus updates yet.
  119. // once that happens, finer grain of control here.
  120. $this->log(LOG_NOTICE, "Not queueing notice {$notice->id} for OStatus because of privacy; scope = {$notice->scope}");
  121. }
  122. return true;
  123. }
  124. /**
  125. * Set up a PuSH hub link to our internal link for canonical timeline
  126. * Atom feeds for users and groups.
  127. */
  128. function onStartApiAtom($feed)
  129. {
  130. $id = null;
  131. if ($feed instanceof AtomUserNoticeFeed) {
  132. $salmonAction = 'usersalmon';
  133. $user = $feed->getUser();
  134. $id = $user->id;
  135. $profile = $user->getProfile();
  136. } else if ($feed instanceof AtomGroupNoticeFeed) {
  137. $salmonAction = 'groupsalmon';
  138. $group = $feed->getGroup();
  139. $id = $group->id;
  140. } else if ($feed instanceof AtomListNoticeFeed) {
  141. $salmonAction = 'peopletagsalmon';
  142. $peopletag = $feed->getList();
  143. $id = $peopletag->id;
  144. } else {
  145. return true;
  146. }
  147. if (!empty($id)) {
  148. $hub = common_config('ostatus', 'hub');
  149. if (empty($hub)) {
  150. // Updates will be handled through our internal PuSH hub.
  151. $hub = common_local_url('pushhub');
  152. }
  153. $feed->addLink($hub, array('rel' => 'hub'));
  154. // Also, we'll add in the salmon link
  155. $salmon = common_local_url($salmonAction, array('id' => $id));
  156. $feed->addLink($salmon, array('rel' => Salmon::REL_SALMON));
  157. // XXX: these are deprecated, but StatusNet only looks for NS_REPLIES
  158. $feed->addLink($salmon, array('rel' => Salmon::NS_REPLIES));
  159. $feed->addLink($salmon, array('rel' => Salmon::NS_MENTIONS));
  160. }
  161. return true;
  162. }
  163. /**
  164. * Add in an OStatus subscribe button
  165. */
  166. function onStartProfileRemoteSubscribe($output, $profile)
  167. {
  168. $this->onStartProfileListItemActionElements($output, $profile);
  169. return false;
  170. }
  171. function onStartGroupSubscribe($widget, $group)
  172. {
  173. $cur = common_current_user();
  174. if (empty($cur)) {
  175. $widget->out->elementStart('li', 'entity_subscribe');
  176. $url = common_local_url('ostatusinit',
  177. array('group' => $group->nickname));
  178. $widget->out->element('a', array('href' => $url,
  179. 'class' => 'entity_remote_subscribe'),
  180. // TRANS: Link to subscribe to a remote entity.
  181. _m('Subscribe'));
  182. $widget->out->elementEnd('li');
  183. return false;
  184. }
  185. return true;
  186. }
  187. function onStartSubscribePeopletagForm($output, $peopletag)
  188. {
  189. $cur = common_current_user();
  190. if (empty($cur)) {
  191. $output->elementStart('li', 'entity_subscribe');
  192. $profile = $peopletag->getTagger();
  193. $url = common_local_url('ostatusinit',
  194. array('tagger' => $profile->nickname, 'peopletag' => $peopletag->tag));
  195. $output->element('a', array('href' => $url,
  196. 'class' => 'entity_remote_subscribe'),
  197. // TRANS: Link to subscribe to a remote entity.
  198. _m('Subscribe'));
  199. $output->elementEnd('li');
  200. return false;
  201. }
  202. return true;
  203. }
  204. function onStartTagProfileAction($action, $profile)
  205. {
  206. $err = null;
  207. $uri = $action->trimmed('uri');
  208. if (!$profile && $uri) {
  209. try {
  210. if (Validate::email($uri)) {
  211. $oprofile = Ostatus_profile::ensureWebfinger($uri);
  212. } else if (Validate::uri($uri)) {
  213. $oprofile = Ostatus_profile::ensureProfileURL($uri);
  214. } else {
  215. // TRANS: Exception in OStatus when invalid URI was entered.
  216. throw new Exception(_m('Invalid URI.'));
  217. }
  218. // redirect to the new profile.
  219. common_redirect(common_local_url('tagprofile', array('id' => $oprofile->profile_id)), 303);
  220. } catch (Exception $e) {
  221. // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
  222. // TRANS: and example.net, as these are official standard domain names for use in examples.
  223. $err = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname.");
  224. }
  225. $action->showForm($err);
  226. return false;
  227. }
  228. return true;
  229. }
  230. /*
  231. * If the field being looked for is URI look for the profile
  232. */
  233. function onStartProfileCompletionSearch($action, $profile, $search_engine) {
  234. if ($action->field == 'uri') {
  235. $profile->joinAdd(array('id', 'user:id'));
  236. $profile->whereAdd('uri LIKE "%' . $profile->escape($q) . '%"');
  237. $profile->query();
  238. if ($profile->N == 0) {
  239. try {
  240. if (Validate::email($q)) {
  241. $oprofile = Ostatus_profile::ensureWebfinger($q);
  242. } else if (Validate::uri($q)) {
  243. $oprofile = Ostatus_profile::ensureProfileURL($q);
  244. } else {
  245. // TRANS: Exception in OStatus when invalid URI was entered.
  246. throw new Exception(_m('Invalid URI.'));
  247. }
  248. return $this->filter(array($oprofile->localProfile()));
  249. } catch (Exception $e) {
  250. // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
  251. // TRANS: and example.net, as these are official standard domain names for use in examples.
  252. $this->msg = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname.");
  253. return array();
  254. }
  255. }
  256. return false;
  257. }
  258. return true;
  259. }
  260. /**
  261. * Find any explicit remote mentions. Accepted forms:
  262. * Webfinger: @user@example.com
  263. * Profile link: @example.com/mublog/user
  264. * @param Profile $sender
  265. * @param string $text input markup text
  266. * @param array &$mention in/out param: set of found mentions
  267. * @return boolean hook return value
  268. */
  269. function onEndFindMentions(Profile $sender, $text, &$mentions)
  270. {
  271. $matches = array();
  272. // Webfinger matches: @user@example.com
  273. if (preg_match_all('!(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\-?\w+\.)*\w+(?:\w+\-\w+)*\.\w+)!',
  274. $text,
  275. $wmatches,
  276. PREG_OFFSET_CAPTURE)) {
  277. foreach ($wmatches[1] as $wmatch) {
  278. list($target, $pos) = $wmatch;
  279. $this->log(LOG_INFO, "Checking webfinger '$target'");
  280. try {
  281. $oprofile = Ostatus_profile::ensureWebfinger($target);
  282. if ($oprofile instanceof Ostatus_profile && !$oprofile->isGroup()) {
  283. $profile = $oprofile->localProfile();
  284. $matches[$pos] = array('mentioned' => array($profile),
  285. 'type' => 'mention',
  286. 'text' => $target,
  287. 'position' => $pos,
  288. 'url' => $profile->getUrl());
  289. }
  290. } catch (Exception $e) {
  291. $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage());
  292. }
  293. }
  294. }
  295. // Profile matches: @example.com/mublog/user
  296. if (preg_match_all('!(?:^|\s+)@((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)!',
  297. $text,
  298. $wmatches,
  299. PREG_OFFSET_CAPTURE)) {
  300. foreach ($wmatches[1] as $wmatch) {
  301. list($target, $pos) = $wmatch;
  302. $schemes = array('http', 'https');
  303. foreach ($schemes as $scheme) {
  304. $url = "$scheme://$target";
  305. $this->log(LOG_INFO, "Checking profile address '$url'");
  306. try {
  307. $oprofile = Ostatus_profile::ensureProfileURL($url);
  308. if ($oprofile instanceof Ostatus_profile && !$oprofile->isGroup()) {
  309. $profile = $oprofile->localProfile();
  310. $matches[$pos] = array('mentioned' => array($profile),
  311. 'type' => 'mention',
  312. 'text' => $target,
  313. 'position' => $pos,
  314. 'url' => $profile->getUrl());
  315. break;
  316. }
  317. } catch (Exception $e) {
  318. $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage());
  319. }
  320. }
  321. }
  322. }
  323. foreach ($mentions as $i => $other) {
  324. // If we share a common prefix with a local user, override it!
  325. $pos = $other['position'];
  326. if (isset($matches[$pos])) {
  327. $mentions[$i] = $matches[$pos];
  328. unset($matches[$pos]);
  329. }
  330. }
  331. foreach ($matches as $mention) {
  332. $mentions[] = $mention;
  333. }
  334. return true;
  335. }
  336. /**
  337. * Allow remote profile references to be used in commands:
  338. * sub update@status.net
  339. * whois evan@identi.ca
  340. * reply http://identi.ca/evan hey what's up
  341. *
  342. * @param Command $command
  343. * @param string $arg
  344. * @param Profile &$profile
  345. * @return hook return code
  346. */
  347. function onStartCommandGetProfile($command, $arg, &$profile)
  348. {
  349. $oprofile = $this->pullRemoteProfile($arg);
  350. if ($oprofile instanceof Ostatus_profile && !$oprofile->isGroup()) {
  351. try {
  352. $profile = $oprofile->localProfile();
  353. } catch (NoProfileException $e) {
  354. // No locally stored profile found for remote profile
  355. return true;
  356. }
  357. return false;
  358. } else {
  359. return true;
  360. }
  361. }
  362. /**
  363. * Allow remote group references to be used in commands:
  364. * join group+statusnet@identi.ca
  365. * join http://identi.ca/group/statusnet
  366. * drop identi.ca/group/statusnet
  367. *
  368. * @param Command $command
  369. * @param string $arg
  370. * @param User_group &$group
  371. * @return hook return code
  372. */
  373. function onStartCommandGetGroup($command, $arg, &$group)
  374. {
  375. $oprofile = $this->pullRemoteProfile($arg);
  376. if ($oprofile instanceof Ostatus_profile && $oprofile->isGroup()) {
  377. $group = $oprofile->localGroup();
  378. return false;
  379. } else {
  380. return true;
  381. }
  382. }
  383. protected function pullRemoteProfile($arg)
  384. {
  385. $oprofile = null;
  386. if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) {
  387. // webfinger lookup
  388. try {
  389. return Ostatus_profile::ensureWebfinger($arg);
  390. } catch (Exception $e) {
  391. common_log(LOG_ERR, 'Webfinger lookup failed for ' .
  392. $arg . ': ' . $e->getMessage());
  393. }
  394. }
  395. // Look for profile URLs, with or without scheme:
  396. $urls = array();
  397. if (preg_match('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
  398. $urls[] = $arg;
  399. }
  400. if (preg_match('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
  401. $schemes = array('http', 'https');
  402. foreach ($schemes as $scheme) {
  403. $urls[] = "$scheme://$arg";
  404. }
  405. }
  406. foreach ($urls as $url) {
  407. try {
  408. return Ostatus_profile::ensureProfileURL($url);
  409. } catch (Exception $e) {
  410. common_log(LOG_ERR, 'Profile lookup failed for ' .
  411. $arg . ': ' . $e->getMessage());
  412. }
  413. }
  414. return null;
  415. }
  416. /**
  417. * Make sure necessary tables are filled out.
  418. */
  419. function onCheckSchema() {
  420. $schema = Schema::get();
  421. $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef());
  422. $schema->ensureTable('ostatus_source', Ostatus_source::schemaDef());
  423. $schema->ensureTable('feedsub', FeedSub::schemaDef());
  424. $schema->ensureTable('hubsub', HubSub::schemaDef());
  425. $schema->ensureTable('magicsig', Magicsig::schemaDef());
  426. return true;
  427. }
  428. public function onEndShowStylesheets(Action $action) {
  429. $action->cssLink($this->path('theme/base/css/ostatus.css'));
  430. return true;
  431. }
  432. function onEndShowStatusNetScripts($action) {
  433. $action->script($this->path('js/ostatus.js'));
  434. return true;
  435. }
  436. /**
  437. * Override the "from ostatus" bit in notice lists to link to the
  438. * original post and show the domain it came from.
  439. *
  440. * @param Notice in $notice
  441. * @param string out &$name
  442. * @param string out &$url
  443. * @param string out &$title
  444. * @return mixed hook return code
  445. */
  446. function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
  447. {
  448. // If we don't handle this, keep the event handler going
  449. if ($notice->source != 'ostatus') {
  450. return true;
  451. }
  452. try {
  453. $url = $notice->getUrl();
  454. // If getUrl() throws exception, $url is never set
  455. $bits = parse_url($url);
  456. $domain = $bits['host'];
  457. if (substr($domain, 0, 4) == 'www.') {
  458. $name = substr($domain, 4);
  459. } else {
  460. $name = $domain;
  461. }
  462. // TRANS: Title. %s is a domain name.
  463. $title = sprintf(_m('Sent from %s via OStatus'), $domain);
  464. // Abort event handler, we have a name and URL!
  465. return false;
  466. } catch (InvalidUrlException $e) {
  467. // This just means we don't have the notice source data
  468. return true;
  469. }
  470. }
  471. /**
  472. * Send incoming PuSH feeds for OStatus endpoints in for processing.
  473. *
  474. * @param FeedSub $feedsub
  475. * @param DOMDocument $feed
  476. * @return mixed hook return code
  477. */
  478. function onStartFeedSubReceive($feedsub, $feed)
  479. {
  480. $oprofile = Ostatus_profile::getKV('feeduri', $feedsub->uri);
  481. if ($oprofile instanceof Ostatus_profile) {
  482. $oprofile->processFeed($feed, 'push');
  483. } else {
  484. common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri");
  485. }
  486. }
  487. /**
  488. * Tell the FeedSub infrastructure whether we have any active OStatus
  489. * usage for the feed; if not it'll be able to garbage-collect the
  490. * feed subscription.
  491. *
  492. * @param FeedSub $feedsub
  493. * @param integer $count in/out
  494. * @return mixed hook return code
  495. */
  496. function onFeedSubSubscriberCount($feedsub, &$count)
  497. {
  498. $oprofile = Ostatus_profile::getKV('feeduri', $feedsub->uri);
  499. if ($oprofile instanceof Ostatus_profile) {
  500. $count += $oprofile->subscriberCount();
  501. }
  502. return true;
  503. }
  504. /**
  505. * When about to subscribe to a remote user, start a server-to-server
  506. * PuSH subscription if needed. If we can't establish that, abort.
  507. *
  508. * @fixme If something else aborts later, we could end up with a stray
  509. * PuSH subscription. This is relatively harmless, though.
  510. *
  511. * @param Profile $profile subscriber
  512. * @param Profile $other subscribee
  513. *
  514. * @return hook return code
  515. *
  516. * @throws Exception
  517. */
  518. function onStartSubscribe(Profile $profile, Profile $other)
  519. {
  520. if (!$profile->isLocal()) {
  521. return true;
  522. }
  523. $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
  524. if (!$oprofile instanceof Ostatus_profile) {
  525. return true;
  526. }
  527. $oprofile->subscribe();
  528. }
  529. /**
  530. * Having established a remote subscription, send a notification to the
  531. * remote OStatus profile's endpoint.
  532. *
  533. * @param Profile $profile subscriber
  534. * @param Profile $other subscribee
  535. *
  536. * @return hook return code
  537. *
  538. * @throws Exception
  539. */
  540. function onEndSubscribe(Profile $profile, Profile $other)
  541. {
  542. if (!$profile->isLocal()) {
  543. return true;
  544. }
  545. $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
  546. if (!$oprofile instanceof Ostatus_profile) {
  547. return true;
  548. }
  549. $sub = Subscription::pkeyGet(array('subscriber' => $profile->id,
  550. 'subscribed' => $other->id));
  551. $act = $sub->asActivity();
  552. $oprofile->notifyActivity($act, $profile);
  553. return true;
  554. }
  555. /**
  556. * Notify remote server and garbage collect unused feeds on unsubscribe.
  557. * @todo FIXME: Send these operations to background queues
  558. *
  559. * @param User $user
  560. * @param Profile $other
  561. * @return hook return value
  562. */
  563. function onEndUnsubscribe(Profile $profile, Profile $other)
  564. {
  565. if (!$profile->isLocal()) {
  566. return true;
  567. }
  568. $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
  569. if (!$oprofile instanceof Ostatus_profile) {
  570. return true;
  571. }
  572. // Drop the PuSH subscription if there are no other subscribers.
  573. $oprofile->garbageCollect();
  574. $act = new Activity();
  575. $act->verb = ActivityVerb::UNFOLLOW;
  576. $act->id = TagURI::mint('unfollow:%d:%d:%s',
  577. $profile->id,
  578. $other->id,
  579. common_date_iso8601(time()));
  580. $act->time = time();
  581. // TRANS: Title for unfollowing a remote profile.
  582. $act->title = _m('TITLE','Unfollow');
  583. // TRANS: Success message for unsubscribe from user attempt through OStatus.
  584. // TRANS: %1$s is the unsubscriber's name, %2$s is the unsubscribed user's name.
  585. $act->content = sprintf(_m('%1$s stopped following %2$s.'),
  586. $profile->getBestName(),
  587. $other->getBestName());
  588. $act->actor = $profile->asActivityObject();
  589. $act->object = $other->asActivityObject();
  590. $oprofile->notifyActivity($act, $profile);
  591. return true;
  592. }
  593. /**
  594. * When one of our local users tries to join a remote group,
  595. * notify the remote server. If the notification is rejected,
  596. * deny the join.
  597. *
  598. * @param User_group $group
  599. * @param Profile $profile
  600. *
  601. * @return mixed hook return value
  602. * @throws Exception of various kinds, some from $oprofile->subscribe();
  603. */
  604. function onStartJoinGroup($group, $profile)
  605. {
  606. $oprofile = Ostatus_profile::getKV('group_id', $group->id);
  607. if (!$oprofile instanceof Ostatus_profile) {
  608. return true;
  609. }
  610. $oprofile->subscribe();
  611. // NOTE: we don't use Group_member::asActivity() since that record
  612. // has not yet been created.
  613. $act = new Activity();
  614. $act->id = TagURI::mint('join:%d:%d:%s',
  615. $profile->id,
  616. $group->id,
  617. common_date_iso8601(time()));
  618. $act->actor = $profile->asActivityObject();
  619. $act->verb = ActivityVerb::JOIN;
  620. $act->object = $oprofile->asActivityObject();
  621. $act->time = time();
  622. // TRANS: Title for joining a remote groep.
  623. $act->title = _m('TITLE','Join');
  624. // TRANS: Success message for subscribe to group attempt through OStatus.
  625. // TRANS: %1$s is the member name, %2$s is the subscribed group's name.
  626. $act->content = sprintf(_m('%1$s has joined group %2$s.'),
  627. $profile->getBestName(),
  628. $oprofile->getBestName());
  629. if ($oprofile->notifyActivity($act, $profile)) {
  630. return true;
  631. } else {
  632. $oprofile->garbageCollect();
  633. // TRANS: Exception thrown when joining a remote group fails.
  634. throw new Exception(_m('Failed joining remote group.'));
  635. }
  636. }
  637. /**
  638. * When one of our local users leaves a remote group, notify the remote
  639. * server.
  640. *
  641. * @fixme Might be good to schedule a resend of the leave notification
  642. * if it failed due to a transitory error. We've canceled the local
  643. * membership already anyway, but if the remote server comes back up
  644. * it'll be left with a stray membership record.
  645. *
  646. * @param User_group $group
  647. * @param Profile $profile
  648. *
  649. * @return mixed hook return value
  650. */
  651. function onEndLeaveGroup($group, $profile)
  652. {
  653. $oprofile = Ostatus_profile::getKV('group_id', $group->id);
  654. if (!$oprofile instanceof Ostatus_profile) {
  655. return true;
  656. }
  657. // Drop the PuSH subscription if there are no other subscribers.
  658. $oprofile->garbageCollect();
  659. $member = $profile;
  660. $act = new Activity();
  661. $act->id = TagURI::mint('leave:%d:%d:%s',
  662. $member->id,
  663. $group->id,
  664. common_date_iso8601(time()));
  665. $act->actor = $member->asActivityObject();
  666. $act->verb = ActivityVerb::LEAVE;
  667. $act->object = $oprofile->asActivityObject();
  668. $act->time = time();
  669. // TRANS: Title for leaving a remote group.
  670. $act->title = _m('TITLE','Leave');
  671. // TRANS: Success message for unsubscribe from group attempt through OStatus.
  672. // TRANS: %1$s is the member name, %2$s is the unsubscribed group's name.
  673. $act->content = sprintf(_m('%1$s has left group %2$s.'),
  674. $member->getBestName(),
  675. $oprofile->getBestName());
  676. $oprofile->notifyActivity($act, $member);
  677. }
  678. /**
  679. * When one of our local users tries to subscribe to a remote peopletag,
  680. * notify the remote server. If the notification is rejected,
  681. * deny the subscription.
  682. *
  683. * @param Profile_list $peopletag
  684. * @param User $user
  685. *
  686. * @return mixed hook return value
  687. * @throws Exception of various kinds, some from $oprofile->subscribe();
  688. */
  689. function onStartSubscribePeopletag($peopletag, $user)
  690. {
  691. $oprofile = Ostatus_profile::getKV('peopletag_id', $peopletag->id);
  692. if (!$oprofile instanceof Ostatus_profile) {
  693. return true;
  694. }
  695. $oprofile->subscribe();
  696. $sub = $user->getProfile();
  697. $tagger = Profile::getKV($peopletag->tagger);
  698. $act = new Activity();
  699. $act->id = TagURI::mint('subscribe_peopletag:%d:%d:%s',
  700. $sub->id,
  701. $peopletag->id,
  702. common_date_iso8601(time()));
  703. $act->actor = $sub->asActivityObject();
  704. $act->verb = ActivityVerb::FOLLOW;
  705. $act->object = $oprofile->asActivityObject();
  706. $act->time = time();
  707. // TRANS: Title for following a remote list.
  708. $act->title = _m('TITLE','Follow list');
  709. // TRANS: Success message for remote list follow through OStatus.
  710. // TRANS: %1$s is the subscriber name, %2$s is the list, %3$s is the lister's name.
  711. $act->content = sprintf(_m('%1$s is now following people listed in %2$s by %3$s.'),
  712. $sub->getBestName(),
  713. $oprofile->getBestName(),
  714. $tagger->getBestName());
  715. if ($oprofile->notifyActivity($act, $sub)) {
  716. return true;
  717. } else {
  718. $oprofile->garbageCollect();
  719. // TRANS: Exception thrown when subscription to remote list fails.
  720. throw new Exception(_m('Failed subscribing to remote list.'));
  721. }
  722. }
  723. /**
  724. * When one of our local users unsubscribes to a remote peopletag, notify the remote
  725. * server.
  726. *
  727. * @param Profile_list $peopletag
  728. * @param User $user
  729. *
  730. * @return mixed hook return value
  731. */
  732. function onEndUnsubscribePeopletag($peopletag, $user)
  733. {
  734. $oprofile = Ostatus_profile::getKV('peopletag_id', $peopletag->id);
  735. if (!$oprofile instanceof Ostatus_profile) {
  736. return true;
  737. }
  738. // Drop the PuSH subscription if there are no other subscribers.
  739. $oprofile->garbageCollect();
  740. $sub = Profile::getKV($user->id);
  741. $tagger = Profile::getKV($peopletag->tagger);
  742. $act = new Activity();
  743. $act->id = TagURI::mint('unsubscribe_peopletag:%d:%d:%s',
  744. $sub->id,
  745. $peopletag->id,
  746. common_date_iso8601(time()));
  747. $act->actor = $member->asActivityObject();
  748. $act->verb = ActivityVerb::UNFOLLOW;
  749. $act->object = $oprofile->asActivityObject();
  750. $act->time = time();
  751. // TRANS: Title for unfollowing a remote list.
  752. $act->title = _m('Unfollow list');
  753. // TRANS: Success message for remote list unfollow through OStatus.
  754. // TRANS: %1$s is the subscriber name, %2$s is the list, %3$s is the lister's name.
  755. $act->content = sprintf(_m('%1$s stopped following the list %2$s by %3$s.'),
  756. $sub->getBestName(),
  757. $oprofile->getBestName(),
  758. $tagger->getBestName());
  759. $oprofile->notifyActivity($act, $user);
  760. }
  761. /**
  762. * Notify remote users when their notices get favorited.
  763. *
  764. * @param Profile or User $profile of local user doing the faving
  765. * @param Notice $notice being favored
  766. * @return hook return value
  767. */
  768. function onEndFavorNotice(Profile $profile, Notice $notice)
  769. {
  770. // Only distribute local users' favor actions, remote users
  771. // will have already distributed theirs.
  772. if (!$profile->isLocal()) {
  773. return true;
  774. }
  775. $oprofile = Ostatus_profile::getKV('profile_id', $notice->profile_id);
  776. if (!$oprofile instanceof Ostatus_profile) {
  777. return true;
  778. }
  779. $fav = Fave::pkeyGet(array('user_id' => $profile->id,
  780. 'notice_id' => $notice->id));
  781. if (!$fav instanceof Fave) {
  782. // That's weird.
  783. // TODO: Make pkeyGet throw exception, since this is a critical failure.
  784. return true;
  785. }
  786. $act = $fav->asActivity();
  787. $oprofile->notifyActivity($act, $profile);
  788. return true;
  789. }
  790. /**
  791. * Notify remote user it has got a new people tag
  792. * - tag verb is queued
  793. * - the subscription is done immediately if not present
  794. *
  795. * @param Profile_tag $ptag the people tag that was created
  796. * @return hook return value
  797. * @throws Exception of various kinds, some from $oprofile->subscribe();
  798. */
  799. function onEndTagProfile($ptag)
  800. {
  801. $oprofile = Ostatus_profile::getKV('profile_id', $ptag->tagged);
  802. if (!$oprofile instanceof Ostatus_profile) {
  803. return true;
  804. }
  805. $plist = $ptag->getMeta();
  806. if ($plist->private) {
  807. return true;
  808. }
  809. $act = new Activity();
  810. $tagger = $plist->getTagger();
  811. $tagged = Profile::getKV('id', $ptag->tagged);
  812. $act->verb = ActivityVerb::TAG;
  813. $act->id = TagURI::mint('tag_profile:%d:%d:%s',
  814. $plist->tagger, $plist->id,
  815. common_date_iso8601(time()));
  816. $act->time = time();
  817. // TRANS: Title for listing a remote profile.
  818. $act->title = _m('TITLE','List');
  819. // TRANS: Success message for remote list addition through OStatus.
  820. // TRANS: %1$s is the list creator's name, %2$s is the added list member, %3$s is the list name.
  821. $act->content = sprintf(_m('%1$s listed %2$s in the list %3$s.'),
  822. $tagger->getBestName(),
  823. $tagged->getBestName(),
  824. $plist->getBestName());
  825. $act->actor = $tagger->asActivityObject();
  826. $act->objects = array($tagged->asActivityObject());
  827. $act->target = ActivityObject::fromPeopletag($plist);
  828. $oprofile->notifyDeferred($act, $tagger);
  829. // initiate a PuSH subscription for the person being tagged
  830. $oprofile->subscribe();
  831. return true;
  832. }
  833. /**
  834. * Notify remote user that a people tag has been removed
  835. * - untag verb is queued
  836. * - the subscription is undone immediately if not required
  837. * i.e garbageCollect()'d
  838. *
  839. * @param Profile_tag $ptag the people tag that was deleted
  840. * @return hook return value
  841. */
  842. function onEndUntagProfile($ptag)
  843. {
  844. $oprofile = Ostatus_profile::getKV('profile_id', $ptag->tagged);
  845. if (!$oprofile instanceof Ostatus_profile) {
  846. return true;
  847. }
  848. $plist = $ptag->getMeta();
  849. if ($plist->private) {
  850. return true;
  851. }
  852. $act = new Activity();
  853. $tagger = $plist->getTagger();
  854. $tagged = Profile::getKV('id', $ptag->tagged);
  855. $act->verb = ActivityVerb::UNTAG;
  856. $act->id = TagURI::mint('untag_profile:%d:%d:%s',
  857. $plist->tagger, $plist->id,
  858. common_date_iso8601(time()));
  859. $act->time = time();
  860. // TRANS: Title for unlisting a remote profile.
  861. $act->title = _m('TITLE','Unlist');
  862. // TRANS: Success message for remote list removal through OStatus.
  863. // TRANS: %1$s is the list creator's name, %2$s is the removed list member, %3$s is the list name.
  864. $act->content = sprintf(_m('%1$s removed %2$s from the list %3$s.'),
  865. $tagger->getBestName(),
  866. $tagged->getBestName(),
  867. $plist->getBestName());
  868. $act->actor = $tagger->asActivityObject();
  869. $act->objects = array($tagged->asActivityObject());
  870. $act->target = ActivityObject::fromPeopletag($plist);
  871. $oprofile->notifyDeferred($act, $tagger);
  872. // unsubscribe to PuSH feed if no more required
  873. $oprofile->garbageCollect();
  874. return true;
  875. }
  876. /**
  877. * Notify remote users when their notices get de-favorited.
  878. *
  879. * @param Profile $profile Profile person doing the de-faving
  880. * @param Notice $notice Notice being favored
  881. *
  882. * @return hook return value
  883. */
  884. function onEndDisfavorNotice(Profile $profile, Notice $notice)
  885. {
  886. // Only distribute local users' disfavor actions, remote users
  887. // will have already distributed theirs.
  888. if (!$profile->isLocal()) {
  889. return true;
  890. }
  891. $oprofile = Ostatus_profile::getKV('profile_id', $notice->profile_id);
  892. if (!$oprofile instanceof Ostatus_profile) {
  893. return true;
  894. }
  895. $act = new Activity();
  896. $act->verb = ActivityVerb::UNFAVORITE;
  897. $act->id = TagURI::mint('disfavor:%d:%d:%s',
  898. $profile->id,
  899. $notice->id,
  900. common_date_iso8601(time()));
  901. $act->time = time();
  902. // TRANS: Title for unliking a remote notice.
  903. $act->title = _m('Unlike');
  904. // TRANS: Success message for remove a favorite notice through OStatus.
  905. // TRANS: %1$s is the unfavoring user's name, %2$s is URI to the no longer favored notice.
  906. $act->content = sprintf(_m('%1$s no longer likes %2$s.'),
  907. $profile->getBestName(),
  908. $notice->getUrl());
  909. $act->actor = $profile->asActivityObject();
  910. $act->object = $notice->asActivityObject();
  911. $oprofile->notifyActivity($act, $profile);
  912. return true;
  913. }
  914. function onStartGetProfileUri($profile, &$uri)
  915. {
  916. $oprofile = Ostatus_profile::getKV('profile_id', $profile->id);
  917. if ($oprofile instanceof Ostatus_profile) {
  918. $uri = $oprofile->uri;
  919. return false;
  920. }
  921. return true;
  922. }
  923. function onStartUserGroupHomeUrl($group, &$url)
  924. {
  925. return $this->onStartUserGroupPermalink($group, $url);
  926. }
  927. function onStartUserGroupPermalink($group, &$url)
  928. {
  929. $oprofile = Ostatus_profile::getKV('group_id', $group->id);
  930. if ($oprofile instanceof Ostatus_profile) {
  931. // @fixme this should probably be in the user_group table
  932. // @fixme this uri not guaranteed to be a profile page
  933. $url = $oprofile->uri;
  934. return false;
  935. }
  936. }
  937. function onStartShowSubscriptionsContent($action)
  938. {
  939. $this->showEntityRemoteSubscribe($action);
  940. return true;
  941. }
  942. function onStartShowUserGroupsContent($action)
  943. {
  944. $this->showEntityRemoteSubscribe($action, 'ostatusgroup');
  945. return true;
  946. }
  947. function onEndShowSubscriptionsMiniList($action)
  948. {
  949. $this->showEntityRemoteSubscribe($action);
  950. return true;
  951. }
  952. function onEndShowGroupsMiniList($action)
  953. {
  954. $this->showEntityRemoteSubscribe($action, 'ostatusgroup');
  955. return true;
  956. }
  957. function showEntityRemoteSubscribe($action, $target='ostatussub')
  958. {
  959. $user = common_current_user();
  960. if ($user && ($user->id == $action->profile->id)) {
  961. $action->elementStart('div', 'entity_actions');
  962. $action->elementStart('p', array('id' => 'entity_remote_subscribe',
  963. 'class' => 'entity_subscribe'));
  964. $action->element('a', array('href' => common_local_url($target),
  965. 'class' => 'entity_remote_subscribe'),
  966. // TRANS: Link text for link to remote subscribe.
  967. _m('Remote'));
  968. $action->elementEnd('p');
  969. $action->elementEnd('div');
  970. }
  971. }
  972. /**
  973. * Ping remote profiles with updates to this profile.
  974. * Salmon pings are queued for background processing.
  975. */
  976. function onEndBroadcastProfile(Profile $profile)
  977. {
  978. $user = User::getKV('id', $profile->id);
  979. // Find foreign accounts I'm subscribed to that support Salmon pings.
  980. //
  981. // @fixme we could run updates through the PuSH feed too,
  982. // in which case we can skip Salmon pings to folks who
  983. // are also subscribed to me.
  984. $sql = "SELECT * FROM ostatus_profile " .
  985. "WHERE profile_id IN " .
  986. "(SELECT subscribed FROM subscription WHERE subscriber=%d) " .
  987. "OR group_id IN " .
  988. "(SELECT group_id FROM group_member WHERE profile_id=%d)";
  989. $oprofile = new Ostatus_profile();
  990. $oprofile->query(sprintf($sql, $profile->id, $profile->id));
  991. if ($oprofile->N == 0) {
  992. common_log(LOG_DEBUG, "No OStatus remote subscribees for $profile->nickname");
  993. return true;
  994. }
  995. $act = new Activity();
  996. $act->verb = ActivityVerb::UPDATE_PROFILE;
  997. $act->id = TagURI::mint('update-profile:%d:%s',
  998. $profile->id,
  999. common_date_iso8601(time()));
  1000. $act->time = time();
  1001. // TRANS: Title for activity.
  1002. $act->title = _m('Profile update');
  1003. // TRANS: Ping text for remote profile update through OStatus.
  1004. // TRANS: %s is user that updated their profile.
  1005. $act->content = sprintf(_m('%s has updated their profile page.'),
  1006. $profile->getBestName());
  1007. $act->actor = $profile->asActivityObject();
  1008. $act->object = $act->actor;
  1009. while ($oprofile->fetch()) {
  1010. $oprofile->notifyDeferred($act, $profile);
  1011. }
  1012. return true;
  1013. }
  1014. function onStartProfileListItemActionElements($item, $profile=null)
  1015. {
  1016. if (!common_logged_in()) {
  1017. $profileUser = User::getKV('id', $item->profile->id);
  1018. if (!empty($profileUser)) {
  1019. if ($item instanceof Action) {
  1020. $output = $item;
  1021. $profile = $item->profile;
  1022. } else {
  1023. $output = $item->out;
  1024. }
  1025. // Add an OStatus subscribe
  1026. $output->elementStart('li', 'entity_subscribe');
  1027. $url = common_local_url('ostatusinit',
  1028. array('nickname' => $profileUser->nickname));
  1029. $output->element('a', array('href' => $url,
  1030. 'class' => 'entity_remote_subscribe'),
  1031. // TRANS: Link text for a user to subscribe to an OStatus user.
  1032. _m('Subscribe'));
  1033. $output->elementEnd('li');
  1034. $output->elementStart('li', 'entity_tag');
  1035. $url = common_local_url('ostatustag',
  1036. array('nickname' => $profileUser->nickname));
  1037. $output->element('a', array('href' => $url,
  1038. 'class' => 'entity_remote_tag'),
  1039. // TRANS: Link text for a user to list an OStatus user.
  1040. _m('List'));
  1041. $output->elementEnd('li');
  1042. }
  1043. }
  1044. return true;
  1045. }
  1046. function onPluginVersion(&$versions)
  1047. {
  1048. $versions[] = array('name' => 'OStatus',
  1049. 'version' => GNUSOCIAL_VERSION,
  1050. 'author' => 'Evan Prodromou, James Walker, Brion Vibber, Zach Copley',
  1051. 'homepage' => 'http://status.net/wiki/Plugin:OStatus',
  1052. // TRANS: Plugin description.
  1053. 'rawdescription' => _m('Follow people across social networks that implement '.
  1054. '<a href="http://ostatus.org/">OStatus</a>.'));
  1055. return true;
  1056. }
  1057. /**
  1058. * Utility function to check if the given URI is a canonical group profile
  1059. * page, and if so return the ID number.
  1060. *
  1061. * @param string $url
  1062. * @return mixed int or false
  1063. */
  1064. public static function localGroupFromUrl($url)
  1065. {
  1066. $group = User_group::getKV('uri', $url);
  1067. if ($group instanceof User_group) {
  1068. if ($group->isLocal()) {
  1069. return $group->id;
  1070. }
  1071. } else {
  1072. // To find local groups which haven't had their uri fields filled out...
  1073. // If the domain has changed since a subscriber got the URI, it'll
  1074. // be broken.
  1075. $template = common_local_url('groupbyid', array('id' => '31337'));
  1076. $template = preg_quote($template, '/');
  1077. $template = str_replace('31337', '(\d+)', $template);
  1078. if (preg_match("/$template/", $url, $matches)) {
  1079. return intval($matches[1]);
  1080. }
  1081. }
  1082. return false;
  1083. }
  1084. public function onStartProfileGetAtomFeed($profile, &$feed)
  1085. {
  1086. $oprofile = Ostatus_profile::getKV('profile_id', $profile->id);
  1087. if (!$oprofile instanceof Ostatus_profile) {
  1088. return true;
  1089. }
  1090. $feed = $oprofile->feeduri;
  1091. return false;
  1092. }
  1093. function onStartGetProfileFromURI($uri, &$profile)
  1094. {
  1095. // Don't want to do Web-based discovery on our own server,
  1096. // so we check locally first.
  1097. $user = User::getKV('uri', $uri);
  1098. if (!empty($user)) {
  1099. $profile = $user->getProfile();
  1100. return false;
  1101. }
  1102. // Now, check remotely
  1103. try {
  1104. $oprofile = Ostatus_profile::ensureProfileURI($uri);
  1105. $profile = $oprofile->localProfile();
  1106. return !($profile instanceof Profile); // localProfile won't throw exception but can return null
  1107. } catch (Exception $e) {
  1108. return true; // It's not an OStatus profile as far as we know, continue event handling
  1109. }
  1110. }
  1111. function onEndWebFingerNoticeLinks(XML_XRD $xrd, Notice $target)
  1112. {
  1113. $author = $target->getProfile();
  1114. $profiletype = $this->profileTypeString($author);
  1115. $salmon_url = common_local_url("{$profiletype}salmon", array('id' => $author->id));
  1116. $xrd->links[] = new XML_XRD_Element_Link(Salmon::REL_SALMON, $salmon_url);
  1117. return true;
  1118. }
  1119. function onEndWebFingerProfileLinks(XML_XRD $xrd, Profile $target)
  1120. {
  1121. if ($target->getObjectType() === ActivityObject::PERSON) {
  1122. $this->addWebFingerPersonLinks($xrd, $target);
  1123. }
  1124. // Salmon
  1125. $profiletype = $this->profileTypeString($target);
  1126. $salmon_url = common_local_url("{$profiletype}salmon", array('id' => $target->id));
  1127. $xrd->links[] = new XML_XRD_Element_Link(Salmon::REL_SALMON, $salmon_url);
  1128. // XXX: these are deprecated, but StatusNet only looks for NS_REPLIES
  1129. $xrd->links[] = new XML_XRD_Element_Link(Salmon::NS_REPLIES, $salmon_url);
  1130. $xrd->links[] = new XML_XRD_Element_Link(Salmon::NS_MENTIONS, $salmon_url);
  1131. // TODO - finalize where the redirect should go on the publisher
  1132. $xrd->links[] = new XML_XRD_Element_Link('http://ostatus.org/schema/1.0/subscribe',
  1133. common_local_url('ostatussub') . '?profile={uri}',
  1134. null, // type not set
  1135. true); // isTemplate
  1136. return true;
  1137. }
  1138. protected function profileTypeString(Profile $target)
  1139. {
  1140. // This is just used to have a definitive string response to "USERsalmon" or "GROUPsalmon"
  1141. switch ($target->getObjectType()) {
  1142. case ActivityObject::PERSON:
  1143. return 'user';
  1144. case ActivityObject::GROUP:
  1145. return 'group';
  1146. default:
  1147. throw new ServerException('Unknown profile type for WebFinger profile links');
  1148. }
  1149. }
  1150. protected function addWebFingerPersonLinks(XML_XRD $xrd, Profile $target)
  1151. {
  1152. $xrd->links[] = new XML_XRD_Element_Link(Discovery::UPDATESFROM,
  1153. common_local_url('ApiTimelineUser',
  1154. array('id' => $target->id, 'format' => 'atom')),
  1155. 'application/atom+xml');
  1156. // Get this profile's keypair
  1157. $magicsig = Magicsig::getKV('user_id', $target->id);
  1158. if (!$magicsig instanceof Magicsig && $target->isLocal()) {
  1159. $magicsig = Magicsig::generate($target->getUser());
  1160. }
  1161. if ($magicsig instanceof Magicsig) {
  1162. $xrd->links[] = new XML_XRD_Element_Link(Magicsig::PUBLICKEYREL,
  1163. 'data:application/magic-public-key,'. $magicsig->toString());
  1164. $xrd->links[] = new XML_XRD_Element_Link(Magicsig::DIASPORA_PUBLICKEYREL,
  1165. base64_encode($magicsig->exportPublicKey()));
  1166. }
  1167. }
  1168. public function onGetLocalAttentions(Profile $actor, array $attention_uris, array &$mentions, array &$groups)
  1169. {
  1170. list($mentions, $groups) = Ostatus_profile::filterAttention($actor, $attention_uris);
  1171. }
  1172. // FIXME: Maybe this shouldn't be so authoritative that it breaks other remote profile lookups?
  1173. static public function onCheckActivityAuthorship(Activity $activity, Profile &$profile)
  1174. {
  1175. try {
  1176. $oprofile = Ostatus_profile::ensureProfileURL($profile->getUrl());
  1177. $profile = $oprofile->checkAuthorship($activity);
  1178. } catch (Exception $e) {
  1179. common_log(LOG_ERR, 'Could not get a profile or check authorship ('.get_class($e).': "'.$e->getMessage().'") for activity ID: '.$activity->id);
  1180. $profile = null;
  1181. return false;
  1182. }
  1183. return true;
  1184. }
  1185. public function onProfileDeleteRelated($profile, &$related)
  1186. {
  1187. // Ostatus_profile has a 'profile_id' property, which will be used to find the object
  1188. $related[] = 'Ostatus_profile';
  1189. // Magicsig has a "user_id" column instead, so we have to delete it more manually:
  1190. $magicsig = Magicsig::getKV('user_id', $profile->id);
  1191. if ($magicsig instanceof Magicsig) {
  1192. $magicsig->delete();
  1193. }
  1194. return true;
  1195. }
  1196. }