Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2300 lines
79KB

  1. <?php
  2. /**
  3. * @file include/ostatus.php
  4. */
  5. use Friendica\App;
  6. require_once("include/Contact.php");
  7. require_once("include/threads.php");
  8. require_once("include/html2bbcode.php");
  9. require_once("include/bbcode.php");
  10. require_once("include/items.php");
  11. require_once("mod/share.php");
  12. require_once("include/enotify.php");
  13. require_once("include/socgraph.php");
  14. require_once("include/Photo.php");
  15. require_once("include/Scrape.php");
  16. require_once("include/follow.php");
  17. require_once("include/api.php");
  18. require_once("mod/proxy.php");
  19. require_once("include/xml.php");
  20. /**
  21. * @brief This class contain functions for the OStatus protocol
  22. *
  23. */
  24. class ostatus {
  25. const OSTATUS_DEFAULT_POLL_INTERVAL = 30; // given in minutes
  26. const OSTATUS_DEFAULT_POLL_TIMEFRAME = 1440; // given in minutes
  27. const OSTATUS_DEFAULT_POLL_TIMEFRAME_MENTIONS = 14400; // given in minutes
  28. /**
  29. * @brief Mix two paths together to possibly fix missing parts
  30. *
  31. * @param string $avatar Path to the avatar
  32. * @param string $base Another path that is hopefully complete
  33. *
  34. * @return string fixed avatar path
  35. */
  36. public static function fix_avatar($avatar, $base) {
  37. $base_parts = parse_url($base);
  38. // Remove all parts that could create a problem
  39. unset($base_parts['path']);
  40. unset($base_parts['query']);
  41. unset($base_parts['fragment']);
  42. $avatar_parts = parse_url($avatar);
  43. // Now we mix them
  44. $parts = array_merge($base_parts, $avatar_parts);
  45. // And put them together again
  46. $scheme = isset($parts['scheme']) ? $parts['scheme'] . '://' : '';
  47. $host = isset($parts['host']) ? $parts['host'] : '';
  48. $port = isset($parts['port']) ? ':' . $parts['port'] : '';
  49. $path = isset($parts['path']) ? $parts['path'] : '';
  50. $query = isset($parts['query']) ? '?' . $parts['query'] : '';
  51. $fragment = isset($parts['fragment']) ? '#' . $parts['fragment'] : '';
  52. $fixed = $scheme.$host.$port.$path.$query.$fragment;
  53. logger('Base: '.$base.' - Avatar: '.$avatar.' - Fixed: '.$fixed, LOGGER_DATA);
  54. return $fixed;
  55. }
  56. /**
  57. * @brief Fetches author data
  58. *
  59. * @param object $xpath The xpath object
  60. * @param object $context The xml context of the author detals
  61. * @param array $importer user record of the importing user
  62. * @param array $contact Called by reference, will contain the fetched contact
  63. * @param bool $onlyfetch Only fetch the header without updating the contact entries
  64. *
  65. * @return array Array of author related entries for the item
  66. */
  67. private function fetchauthor($xpath, $context, $importer, &$contact, $onlyfetch) {
  68. $author = array();
  69. $author["author-link"] = $xpath->evaluate('atom:author/atom:uri/text()', $context)->item(0)->nodeValue;
  70. $author["author-name"] = $xpath->evaluate('atom:author/atom:name/text()', $context)->item(0)->nodeValue;
  71. $aliaslink = $author["author-link"];
  72. $alternate = $xpath->query("atom:author/atom:link[@rel='alternate']", $context)->item(0)->attributes;
  73. if (is_object($alternate))
  74. foreach($alternate AS $attributes)
  75. if ($attributes->name == "href")
  76. $author["author-link"] = $attributes->textContent;
  77. $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `nurl` IN ('%s', '%s') AND `network` != '%s'",
  78. intval($importer["uid"]), dbesc(normalise_link($author["author-link"])),
  79. dbesc(normalise_link($aliaslink)), dbesc(NETWORK_STATUSNET));
  80. if ($r) {
  81. $contact = $r[0];
  82. $author["contact-id"] = $r[0]["id"];
  83. } else
  84. $author["contact-id"] = $contact["id"];
  85. $avatarlist = array();
  86. $avatars = $xpath->query("atom:author/atom:link[@rel='avatar']", $context);
  87. foreach($avatars AS $avatar) {
  88. $href = "";
  89. $width = 0;
  90. foreach($avatar->attributes AS $attributes) {
  91. if ($attributes->name == "href")
  92. $href = $attributes->textContent;
  93. if ($attributes->name == "width")
  94. $width = $attributes->textContent;
  95. }
  96. if (($width > 0) AND ($href != ""))
  97. $avatarlist[$width] = $href;
  98. }
  99. if (count($avatarlist) > 0) {
  100. krsort($avatarlist);
  101. $author["author-avatar"] = self::fix_avatar(current($avatarlist), $author["author-link"]);
  102. }
  103. $displayname = $xpath->evaluate('atom:author/poco:displayName/text()', $context)->item(0)->nodeValue;
  104. if ($displayname != "")
  105. $author["author-name"] = $displayname;
  106. $author["owner-name"] = $author["author-name"];
  107. $author["owner-link"] = $author["author-link"];
  108. $author["owner-avatar"] = $author["author-avatar"];
  109. // Only update the contacts if it is an OStatus contact
  110. if ($r AND !$onlyfetch AND ($contact["network"] == NETWORK_OSTATUS)) {
  111. // Update contact data
  112. // This query doesn't seem to work
  113. // $value = $xpath->query("atom:link[@rel='salmon']", $context)->item(0)->nodeValue;
  114. // if ($value != "")
  115. // $contact["notify"] = $value;
  116. // This query doesn't seem to work as well - I hate these queries
  117. // $value = $xpath->query("atom:link[@rel='self' and @type='application/atom+xml']", $context)->item(0)->nodeValue;
  118. // if ($value != "")
  119. // $contact["poll"] = $value;
  120. $value = $xpath->evaluate('atom:author/atom:uri/text()', $context)->item(0)->nodeValue;
  121. if ($value != "")
  122. $contact["alias"] = $value;
  123. $value = $xpath->evaluate('atom:author/poco:displayName/text()', $context)->item(0)->nodeValue;
  124. if ($value != "")
  125. $contact["name"] = $value;
  126. $value = $xpath->evaluate('atom:author/poco:preferredUsername/text()', $context)->item(0)->nodeValue;
  127. if ($value != "")
  128. $contact["nick"] = $value;
  129. $value = $xpath->evaluate('atom:author/poco:note/text()', $context)->item(0)->nodeValue;
  130. if ($value != "")
  131. $contact["about"] = html2bbcode($value);
  132. $value = $xpath->evaluate('atom:author/poco:address/poco:formatted/text()', $context)->item(0)->nodeValue;
  133. if ($value != "")
  134. $contact["location"] = $value;
  135. if (($contact["name"] != $r[0]["name"]) OR ($contact["nick"] != $r[0]["nick"]) OR ($contact["about"] != $r[0]["about"]) OR
  136. ($contact["alias"] != $r[0]["alias"]) OR ($contact["location"] != $r[0]["location"])) {
  137. logger("Update contact data for contact ".$contact["id"], LOGGER_DEBUG);
  138. q("UPDATE `contact` SET `name` = '%s', `nick` = '%s', `alias` = '%s', `about` = '%s', `location` = '%s', `name-date` = '%s' WHERE `id` = %d",
  139. dbesc($contact["name"]), dbesc($contact["nick"]), dbesc($contact["alias"]),
  140. dbesc($contact["about"]), dbesc($contact["location"]),
  141. dbesc(datetime_convert()), intval($contact["id"]));
  142. }
  143. if (isset($author["author-avatar"]) AND ($author["author-avatar"] != $r[0]['avatar'])) {
  144. logger("Update profile picture for contact ".$contact["id"], LOGGER_DEBUG);
  145. update_contact_avatar($author["author-avatar"], $importer["uid"], $contact["id"]);
  146. }
  147. // Ensure that we are having this contact (with uid=0)
  148. $cid = get_contact($author["author-link"], 0);
  149. if ($cid) {
  150. // Update it with the current values
  151. q("UPDATE `contact` SET `url` = '%s', `name` = '%s', `nick` = '%s', `alias` = '%s',
  152. `about` = '%s', `location` = '%s',
  153. `success_update` = '%s', `last-update` = '%s'
  154. WHERE `id` = %d",
  155. dbesc($author["author-link"]), dbesc($contact["name"]), dbesc($contact["nick"]),
  156. dbesc($contact["alias"]), dbesc($contact["about"]), dbesc($contact["location"]),
  157. dbesc(datetime_convert()), dbesc(datetime_convert()), intval($cid));
  158. // Update the avatar
  159. update_contact_avatar($author["author-avatar"], 0, $cid);
  160. }
  161. $contact["generation"] = 2;
  162. $contact["hide"] = false; // OStatus contacts are never hidden
  163. $contact["photo"] = $author["author-avatar"];
  164. $gcid = update_gcontact($contact);
  165. link_gcontact($gcid, $contact["uid"], $contact["id"]);
  166. }
  167. return($author);
  168. }
  169. /**
  170. * @brief Fetches author data from a given XML string
  171. *
  172. * @param string $xml The XML
  173. * @param array $importer user record of the importing user
  174. *
  175. * @return array Array of author related entries for the item
  176. */
  177. public static function salmon_author($xml, $importer) {
  178. if ($xml == "")
  179. return;
  180. $doc = new DOMDocument();
  181. @$doc->loadXML($xml);
  182. $xpath = new DomXPath($doc);
  183. $xpath->registerNamespace('atom', NAMESPACE_ATOM1);
  184. $xpath->registerNamespace('thr', NAMESPACE_THREAD);
  185. $xpath->registerNamespace('georss', NAMESPACE_GEORSS);
  186. $xpath->registerNamespace('activity', NAMESPACE_ACTIVITY);
  187. $xpath->registerNamespace('media', NAMESPACE_MEDIA);
  188. $xpath->registerNamespace('poco', NAMESPACE_POCO);
  189. $xpath->registerNamespace('ostatus', NAMESPACE_OSTATUS);
  190. $xpath->registerNamespace('statusnet', NAMESPACE_STATUSNET);
  191. $entries = $xpath->query('/atom:entry');
  192. foreach ($entries AS $entry) {
  193. // fetch the author
  194. $author = self::fetchauthor($xpath, $entry, $importer, $contact, true);
  195. return $author;
  196. }
  197. }
  198. /**
  199. * @brief Read attributes from element
  200. *
  201. * @param object $element Element object
  202. *
  203. * @return array attributes
  204. */
  205. private static function read_attributes($element) {
  206. $attribute = array();
  207. foreach ($element->attributes AS $attributes) {
  208. $attribute[$attributes->name] = $attributes->textContent;
  209. }
  210. return $attribute;
  211. }
  212. /**
  213. * @brief Imports an XML string containing OStatus elements
  214. *
  215. * @param string $xml The XML
  216. * @param array $importer user record of the importing user
  217. * @param $contact
  218. * @param array $hub Called by reference, returns the fetched hub data
  219. */
  220. public static function import($xml,$importer,&$contact, &$hub) {
  221. /// @todo this function is too long. It has to be split in many parts
  222. logger("Import OStatus message", LOGGER_DEBUG);
  223. if ($xml == "") {
  224. return;
  225. }
  226. //$tempfile = tempnam(get_temppath(), "import");
  227. //file_put_contents($tempfile, $xml);
  228. $doc = new DOMDocument();
  229. @$doc->loadXML($xml);
  230. $xpath = new DomXPath($doc);
  231. $xpath->registerNamespace('atom', NAMESPACE_ATOM1);
  232. $xpath->registerNamespace('thr', NAMESPACE_THREAD);
  233. $xpath->registerNamespace('georss', NAMESPACE_GEORSS);
  234. $xpath->registerNamespace('activity', NAMESPACE_ACTIVITY);
  235. $xpath->registerNamespace('media', NAMESPACE_MEDIA);
  236. $xpath->registerNamespace('poco', NAMESPACE_POCO);
  237. $xpath->registerNamespace('ostatus', NAMESPACE_OSTATUS);
  238. $xpath->registerNamespace('statusnet', NAMESPACE_STATUSNET);
  239. $gub = "";
  240. $hub_attributes = $xpath->query("/atom:feed/atom:link[@rel='hub']")->item(0)->attributes;
  241. if (is_object($hub_attributes)) {
  242. foreach ($hub_attributes AS $hub_attribute) {
  243. if ($hub_attribute->name == "href") {
  244. $hub = $hub_attribute->textContent;
  245. logger("Found hub ".$hub, LOGGER_DEBUG);
  246. }
  247. }
  248. }
  249. $header = array();
  250. $header["uid"] = $importer["uid"];
  251. $header["network"] = NETWORK_OSTATUS;
  252. $header["type"] = "remote";
  253. $header["wall"] = 0;
  254. $header["origin"] = 0;
  255. $header["gravity"] = GRAVITY_PARENT;
  256. // it could either be a received post or a post we fetched by ourselves
  257. // depending on that, the first node is different
  258. $first_child = $doc->firstChild->tagName;
  259. if ($first_child == "feed") {
  260. $entries = $xpath->query('/atom:feed/atom:entry');
  261. $header["protocol"] = PROTOCOL_OSTATUS_FEED;
  262. } else {
  263. $entries = $xpath->query('/atom:entry');
  264. $header["protocol"] = PROTOCOL_OSTATUS_SALMON;
  265. }
  266. $conversation = "";
  267. $conversationlist = array();
  268. $item_id = 0;
  269. // Reverse the order of the entries
  270. $entrylist = array();
  271. foreach ($entries AS $entry) {
  272. $entrylist[] = $entry;
  273. }
  274. foreach (array_reverse($entrylist) AS $entry) {
  275. $mention = false;
  276. // fetch the author
  277. if ($first_child == "feed") {
  278. $author = self::fetchauthor($xpath, $doc->firstChild, $importer, $contact, false);
  279. } else {
  280. $author = self::fetchauthor($xpath, $entry, $importer, $contact, false);
  281. }
  282. $value = $xpath->evaluate('atom:author/poco:preferredUsername/text()', $context)->item(0)->nodeValue;
  283. if ($value != "") {
  284. $nickname = $value;
  285. } else {
  286. $nickname = $author["author-name"];
  287. }
  288. $item = array_merge($header, $author);
  289. // Now get the item
  290. $item["uri"] = $xpath->query('atom:id/text()', $entry)->item(0)->nodeValue;
  291. $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s'",
  292. intval($importer["uid"]), dbesc($item["uri"]));
  293. if (dbm::is_result($r)) {
  294. logger("Item with uri ".$item["uri"]." for user ".$importer["uid"]." already existed under id ".$r[0]["id"], LOGGER_DEBUG);
  295. continue;
  296. }
  297. $item["body"] = add_page_info_to_body(html2bbcode($xpath->query('atom:content/text()', $entry)->item(0)->nodeValue));
  298. $item["object-type"] = $xpath->query('activity:object-type/text()', $entry)->item(0)->nodeValue;
  299. $item["verb"] = $xpath->query('activity:verb/text()', $entry)->item(0)->nodeValue;
  300. // Mastodon Content Warning
  301. if (($item["verb"] == ACTIVITY_POST) AND $xpath->evaluate('boolean(atom:summary)', $entry)) {
  302. $clear_text = $xpath->query('atom:summary/text()', $entry)->item(0)->nodeValue;
  303. $item["body"] = html2bbcode($clear_text) . '[spoiler]' . $item["body"] . '[/spoiler]';
  304. }
  305. if (($item["object-type"] == ACTIVITY_OBJ_BOOKMARK) OR ($item["object-type"] == ACTIVITY_OBJ_EVENT)) {
  306. $item["title"] = $xpath->query('atom:title/text()', $entry)->item(0)->nodeValue;
  307. $item["body"] = $xpath->query('atom:summary/text()', $entry)->item(0)->nodeValue;
  308. } elseif ($item["object-type"] == ACTIVITY_OBJ_QUESTION) {
  309. $item["title"] = $xpath->query('atom:title/text()', $entry)->item(0)->nodeValue;
  310. }
  311. $item["source"] = $xml;
  312. /// @TODO
  313. /// Delete a message
  314. if ($item["verb"] == "qvitter-delete-notice" || $item["verb"] == ACTIVITY_DELETE) {
  315. // ignore "Delete" messages (by now)
  316. logger("Ignore delete message ".print_r($item, true));
  317. continue;
  318. }
  319. if ($item["verb"] == ACTIVITY_JOIN) {
  320. // ignore "Join" messages
  321. logger("Ignore join message ".print_r($item, true));
  322. continue;
  323. }
  324. if ($item["verb"] == ACTIVITY_FOLLOW) {
  325. new_follower($importer, $contact, $item, $nickname);
  326. continue;
  327. }
  328. if ($item["verb"] == NAMESPACE_OSTATUS."/unfollow") {
  329. lose_follower($importer, $contact, $item, $dummy);
  330. continue;
  331. }
  332. if ($item["verb"] == ACTIVITY_FAVORITE) {
  333. $orig_uri = $xpath->query("activity:object/atom:id", $entry)->item(0)->nodeValue;
  334. logger("Favorite ".$orig_uri." ".print_r($item, true));
  335. $item["verb"] = ACTIVITY_LIKE;
  336. $item["parent-uri"] = $orig_uri;
  337. $item["gravity"] = GRAVITY_LIKE;
  338. }
  339. if ($item["verb"] == NAMESPACE_OSTATUS."/unfavorite") {
  340. // Ignore "Unfavorite" message
  341. logger("Ignore unfavorite message ".print_r($item, true));
  342. continue;
  343. }
  344. // http://activitystrea.ms/schema/1.0/rsvp-yes
  345. if (!in_array($item["verb"], array(ACTIVITY_POST, ACTIVITY_LIKE, ACTIVITY_SHARE))) {
  346. logger("Unhandled verb ".$item["verb"]." ".print_r($item, true));
  347. }
  348. $item["created"] = $xpath->query('atom:published/text()', $entry)->item(0)->nodeValue;
  349. $item["edited"] = $xpath->query('atom:updated/text()', $entry)->item(0)->nodeValue;
  350. $conversation = $xpath->query('ostatus:conversation/text()', $entry)->item(0)->nodeValue;
  351. $item['conversation-uri'] = $conversation;
  352. $conv = $xpath->query('ostatus:conversation', $entry);
  353. if (is_object($conv->item(0))) {
  354. foreach ($conv->item(0)->attributes AS $attributes) {
  355. if ($attributes->name == "ref") {
  356. $item['conversation-uri'] = $attributes->textContent;
  357. }
  358. if ($attributes->name == "href") {
  359. $item['conversation-href'] = $attributes->textContent;
  360. }
  361. }
  362. }
  363. $related = "";
  364. $inreplyto = $xpath->query('thr:in-reply-to', $entry);
  365. if (is_object($inreplyto->item(0))) {
  366. foreach ($inreplyto->item(0)->attributes AS $attributes) {
  367. if ($attributes->name == "ref") {
  368. $item["parent-uri"] = $attributes->textContent;
  369. }
  370. if ($attributes->name == "href") {
  371. $related = $attributes->textContent;
  372. }
  373. }
  374. }
  375. $georsspoint = $xpath->query('georss:point', $entry);
  376. if ($georsspoint) {
  377. $item["coord"] = $georsspoint->item(0)->nodeValue;
  378. }
  379. $categories = $xpath->query('atom:category', $entry);
  380. if ($categories) {
  381. foreach ($categories AS $category) {
  382. foreach ($category->attributes AS $attributes) {
  383. if ($attributes->name == "term") {
  384. $term = $attributes->textContent;
  385. if(strlen($item["tag"])) {
  386. $item["tag"] .= ',';
  387. }
  388. $item["tag"] .= "#[url=".App::get_baseurl()."/search?tag=".$term."]".$term."[/url]";
  389. }
  390. }
  391. }
  392. }
  393. $self = "";
  394. $enclosure = "";
  395. $links = $xpath->query('atom:link', $entry);
  396. if ($links) {
  397. foreach ($links AS $link) {
  398. $attribute = self::read_attributes($link);
  399. if (($attribute['rel'] != "") AND ($attribute['href'] != "")) {
  400. switch ($attribute['rel']) {
  401. case "alternate":
  402. $item["plink"] = $attribute['href'];
  403. if (($item["object-type"] == ACTIVITY_OBJ_QUESTION) OR
  404. ($item["object-type"] == ACTIVITY_OBJ_EVENT)) {
  405. $item["body"] .= add_page_info($attribute['href']);
  406. }
  407. break;
  408. case "ostatus:conversation":
  409. $conversation = $attribute['href'];
  410. $item['conversation-href'] = $conversation;
  411. if (!isset($item['conversation-uri'])) {
  412. $item['conversation-uri'] = $item['conversation-href'];
  413. }
  414. break;
  415. case "enclosure":
  416. $enclosure = $attribute['href'];
  417. if (strlen($item["attach"])) {
  418. $item["attach"] .= ',';
  419. }
  420. if (!isset($attribute['length'])) {
  421. $attribute['length'] = "0";
  422. }
  423. $item["attach"] .= '[attach]href="'.$attribute['href'].'" length="'.$attribute['length'].'" type="'.$attribute['type'].'" title="'.$attribute['title'].'"[/attach]';
  424. break;
  425. case "related":
  426. if ($item["object-type"] != ACTIVITY_OBJ_BOOKMARK) {
  427. if (!isset($item["parent-uri"])) {
  428. $item["parent-uri"] = $attribute['href'];
  429. }
  430. if ($related == "") {
  431. $related = $attribute['href'];
  432. }
  433. } else {
  434. $item["body"] .= add_page_info($attribute['href']);
  435. }
  436. break;
  437. case "self":
  438. $self = $attribute['href'];
  439. break;
  440. case "mentioned":
  441. // Notification check
  442. if ($importer["nurl"] == normalise_link($attribute['href'])) {
  443. $mention = true;
  444. }
  445. break;
  446. }
  447. }
  448. }
  449. }
  450. $local_id = "";
  451. $repeat_of = "";
  452. $notice_info = $xpath->query('statusnet:notice_info', $entry);
  453. if ($notice_info AND ($notice_info->length > 0)) {
  454. foreach ($notice_info->item(0)->attributes AS $attributes) {
  455. if ($attributes->name == "source") {
  456. $item["app"] = strip_tags($attributes->textContent);
  457. }
  458. if ($attributes->name == "local_id") {
  459. $local_id = $attributes->textContent;
  460. }
  461. if ($attributes->name == "repeat_of") {
  462. $repeat_of = $attributes->textContent;
  463. }
  464. }
  465. }
  466. // Is it a repeated post?
  467. if (($repeat_of != "") OR ($item["verb"] == ACTIVITY_SHARE)) {
  468. $activityobjects = $xpath->query('activity:object', $entry)->item(0);
  469. if (is_object($activityobjects)) {
  470. $orig_uri = $xpath->query("activity:object/atom:id", $activityobjects)->item(0)->nodeValue;
  471. if (!isset($orig_uri)) {
  472. $orig_uri = $xpath->query('atom:id/text()', $activityobjects)->item(0)->nodeValue;
  473. }
  474. $orig_links = $xpath->query("activity:object/atom:link[@rel='alternate']", $activityobjects);
  475. if ($orig_links AND ($orig_links->length > 0)) {
  476. foreach ($orig_links->item(0)->attributes AS $attributes) {
  477. if ($attributes->name == "href") {
  478. $orig_link = $attributes->textContent;
  479. }
  480. }
  481. }
  482. if (!isset($orig_link)) {
  483. $orig_link = $xpath->query("atom:link[@rel='alternate']", $activityobjects)->item(0)->nodeValue;
  484. }
  485. if (!isset($orig_link)) {
  486. $orig_link = self::convert_href($orig_uri);
  487. }
  488. $orig_body = $xpath->query('activity:object/atom:content/text()', $activityobjects)->item(0)->nodeValue;
  489. if (!isset($orig_body)) {
  490. $orig_body = $xpath->query('atom:content/text()', $activityobjects)->item(0)->nodeValue;
  491. }
  492. $orig_created = $xpath->query('atom:published/text()', $activityobjects)->item(0)->nodeValue;
  493. $orig_edited = $xpath->query('atom:updated/text()', $activityobjects)->item(0)->nodeValue;
  494. $orig_contact = $contact;
  495. $orig_author = self::fetchauthor($xpath, $activityobjects, $importer, $orig_contact, false);
  496. $item["author-name"] = $orig_author["author-name"];
  497. $item["author-link"] = $orig_author["author-link"];
  498. $item["author-avatar"] = $orig_author["author-avatar"];
  499. $item["body"] = add_page_info_to_body(html2bbcode($orig_body));
  500. $item["created"] = $orig_created;
  501. $item["edited"] = $orig_edited;
  502. $item["uri"] = $orig_uri;
  503. if (!isset($item["plink"])) {
  504. $item["plink"] = $orig_link;
  505. }
  506. $item["verb"] = $xpath->query('activity:verb/text()', $activityobjects)->item(0)->nodeValue;
  507. $item["object-type"] = $xpath->query('activity:object/activity:object-type/text()', $activityobjects)->item(0)->nodeValue;
  508. if (!isset($item["object-type"])) {
  509. $item["object-type"] = $xpath->query('activity:object-type/text()', $activityobjects)->item(0)->nodeValue;
  510. }
  511. $enclosures = $xpath->query("atom:link[@rel='alternate']", $activityobjects);
  512. if ($enclosures) {
  513. foreach ($enclosures AS $link) {
  514. $attribute = self::read_attributes($link);
  515. if ($href != "") {
  516. $enclosure = $attribute['href'];
  517. if (strlen($item["attach"])) {
  518. $item["attach"] .= ',';
  519. }
  520. if (!isset($attribute['length'])) {
  521. $attribute['length'] = "0";
  522. }
  523. $item["attach"] .= '[attach]href="'.$attribute['href'].'" length="'.$attribute['length'].'" type="'.$attribute['type'].'" title="'.$attribute['title'].'"[/attach]';
  524. }
  525. }
  526. }
  527. }
  528. }
  529. //if ($enclosure != "")
  530. // $item["body"] .= add_page_info($enclosure);
  531. if (isset($item["parent-uri"])) {
  532. $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s'",
  533. intval($importer["uid"]), dbesc($item["parent-uri"]));
  534. // Only fetch missing stuff if it is a comment or reshare.
  535. if (in_array($item["verb"], array(ACTIVITY_POST, ACTIVITY_SHARE)) AND
  536. !dbm::is_result($r) AND ($related != "")) {
  537. $reply_path = str_replace("/notice/", "/api/statuses/show/", $related).".atom";
  538. if ($reply_path != $related) {
  539. logger("Fetching related items for user ".$importer["uid"]." from ".$reply_path, LOGGER_DEBUG);
  540. $reply_xml = fetch_url($reply_path);
  541. $reply_contact = $contact;
  542. self::import($reply_xml,$importer,$reply_contact, $reply_hub);
  543. // After the import try to fetch the parent item again
  544. $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s'",
  545. intval($importer["uid"]), dbesc($item["parent-uri"]));
  546. }
  547. }
  548. if (dbm::is_result($r)) {
  549. $item["type"] = 'remote-comment';
  550. $item["gravity"] = GRAVITY_COMMENT;
  551. }
  552. } else {
  553. $item["parent-uri"] = $item["uri"];
  554. }
  555. $item_id = self::completion($conversation, $importer["uid"], $item, $self);
  556. if (!$item_id) {
  557. logger("Error storing item", LOGGER_DEBUG);
  558. continue;
  559. }
  560. logger("Item was stored with id ".$item_id, LOGGER_DEBUG);
  561. }
  562. }
  563. /**
  564. * @brief Create an url out of an uri
  565. *
  566. * @param string $href URI in the format "parameter1:parameter1:..."
  567. *
  568. * @return string URL in the format http(s)://....
  569. */
  570. public static function convert_href($href) {
  571. $elements = explode(":",$href);
  572. if ((count($elements) <= 2) OR ($elements[0] != "tag"))
  573. return $href;
  574. $server = explode(",", $elements[1]);
  575. $conversation = explode("=", $elements[2]);
  576. if ((count($elements) == 4) AND ($elements[2] == "post"))
  577. return "http://".$server[0]."/notice/".$elements[3];
  578. if ((count($conversation) != 2) OR ($conversation[1] ==""))
  579. return $href;
  580. if ($elements[3] == "objectType=thread")
  581. return "http://".$server[0]."/conversation/".$conversation[1];
  582. else
  583. return "http://".$server[0]."/notice/".$conversation[1];
  584. return $href;
  585. }
  586. /**
  587. * @brief Checks if there are entries in conversations that aren't present on our side
  588. *
  589. * @param bool $mentions Fetch conversations where we are mentioned
  590. * @param bool $override Override the interval setting
  591. */
  592. public static function check_conversations($mentions = false, $override = false) {
  593. $last = get_config('system','ostatus_last_poll');
  594. $poll_interval = intval(get_config('system','ostatus_poll_interval'));
  595. if (!$poll_interval) {
  596. $poll_interval = self::OSTATUS_DEFAULT_POLL_INTERVAL;
  597. }
  598. // Don't poll if the interval is set negative
  599. if (($poll_interval < 0) AND !$override) {
  600. return;
  601. }
  602. if (!$mentions) {
  603. $poll_timeframe = intval(get_config('system','ostatus_poll_timeframe'));
  604. if (!$poll_timeframe) {
  605. $poll_timeframe = self::OSTATUS_DEFAULT_POLL_TIMEFRAME;
  606. }
  607. } else {
  608. $poll_timeframe = intval(get_config('system','ostatus_poll_timeframe'));
  609. if (!$poll_timeframe) {
  610. $poll_timeframe = self::OSTATUS_DEFAULT_POLL_TIMEFRAME_MENTIONS;
  611. }
  612. }
  613. if ($last AND !$override) {
  614. $next = $last + ($poll_interval * 60);
  615. if ($next > time()) {
  616. logger('poll interval not reached');
  617. return;
  618. }
  619. }
  620. logger('cron_start');
  621. $start = date("Y-m-d H:i:s", time() - ($poll_timeframe * 60));
  622. if ($mentions) {
  623. $conversations = q("SELECT `term`.`oid`, `term`.`url`, `term`.`uid` FROM `term`
  624. STRAIGHT_JOIN `thread` ON `thread`.`iid` = `term`.`oid` AND `thread`.`uid` = `term`.`uid`
  625. WHERE `term`.`type` = 7 AND `term`.`term` > '%s' AND `thread`.`mention`
  626. GROUP BY `term`.`url`, `term`.`uid`, `term`.`oid`, `term`.`term` ORDER BY `term`.`term` DESC", dbesc($start));
  627. } else {
  628. $conversations = q("SELECT `oid`, `url`, `uid` FROM `term`
  629. WHERE `type` = 7 AND `term` > '%s'
  630. GROUP BY `url`, `uid`, `oid`, `term` ORDER BY `term` DESC", dbesc($start));
  631. }
  632. foreach ($conversations AS $conversation) {
  633. self::completion($conversation['url'], $conversation['uid']);
  634. }
  635. logger('cron_end');
  636. set_config('system','ostatus_last_poll', time());
  637. }
  638. /**
  639. * @brief Updates the gcontact table with actor data from the conversation
  640. *
  641. * @param object $actor The actor object that contains the contact data
  642. */
  643. private function conv_fetch_actor($actor) {
  644. // We set the generation to "3" since the data here is not as reliable as the data we get on other occasions
  645. $contact = array("network" => NETWORK_OSTATUS, "generation" => 3);
  646. if (isset($actor->url))
  647. $contact["url"] = $actor->url;
  648. if (isset($actor->displayName))
  649. $contact["name"] = $actor->displayName;
  650. if (isset($actor->portablecontacts_net->displayName))
  651. $contact["name"] = $actor->portablecontacts_net->displayName;
  652. if (isset($actor->portablecontacts_net->preferredUsername))
  653. $contact["nick"] = $actor->portablecontacts_net->preferredUsername;
  654. if (isset($actor->id))
  655. $contact["alias"] = $actor->id;
  656. if (isset($actor->summary))
  657. $contact["about"] = $actor->summary;
  658. if (isset($actor->portablecontacts_net->note))
  659. $contact["about"] = $actor->portablecontacts_net->note;
  660. if (isset($actor->portablecontacts_net->addresses->formatted))
  661. $contact["location"] = $actor->portablecontacts_net->addresses->formatted;
  662. if (isset($actor->image->url))
  663. $contact["photo"] = $actor->image->url;
  664. if (isset($actor->image->width))
  665. $avatarwidth = $actor->image->width;
  666. if (is_array($actor->status_net->avatarLinks))
  667. foreach ($actor->status_net->avatarLinks AS $avatar) {
  668. if ($avatarsize < $avatar->width) {
  669. $contact["photo"] = $avatar->url;
  670. $avatarsize = $avatar->width;
  671. }
  672. }
  673. $contact["hide"] = false; // OStatus contacts are never hidden
  674. update_gcontact($contact);
  675. }
  676. /**
  677. * @brief Fetches the conversation url for a given item link or conversation id
  678. *
  679. * @param string $self The link to the posting
  680. * @param string $conversation_id The conversation id
  681. *
  682. * @return string The conversation url
  683. */
  684. private function fetch_conversation($self, $conversation_id = "") {
  685. if ($conversation_id != "") {
  686. $elements = explode(":", $conversation_id);
  687. if ((count($elements) <= 2) OR ($elements[0] != "tag"))
  688. return $conversation_id;
  689. }
  690. if ($self == "")
  691. return "";
  692. $json = str_replace(".atom", ".json", $self);
  693. $raw = fetch_url($json);
  694. if ($raw == "")
  695. return "";
  696. $data = json_decode($raw);
  697. if (!is_object($data))
  698. return "";
  699. $conversation_id = $data->statusnet_conversation_id;
  700. $pos = strpos($self, "/api/statuses/show/");
  701. $base_url = substr($self, 0, $pos);
  702. return $base_url."/conversation/".$conversation_id;
  703. }
  704. /**
  705. * @brief Fetches actor details of a given actor and user id
  706. *
  707. * @param string $actor The actor url
  708. * @param int $uid The user id
  709. * @param int $contact_id The default contact-id
  710. *
  711. * @return array Array with actor details
  712. */
  713. private function get_actor_details($actor, $uid, $contact_id) {
  714. $details = array();
  715. $contact = q("SELECT `id`, `rel`, `network` FROM `contact` WHERE `uid` = %d AND `nurl` = '%s' AND `network` != '%s'",
  716. $uid, normalise_link($actor), NETWORK_STATUSNET);
  717. if (!$contact)
  718. $contact = q("SELECT `id`, `rel`, `network` FROM `contact` WHERE `uid` = %d AND `alias` IN ('%s', '%s') AND `network` != '%s'",
  719. $uid, $actor, normalise_link($actor), NETWORK_STATUSNET);
  720. if ($contact) {
  721. logger("Found contact for url ".$actor, LOGGER_DEBUG);
  722. $details["contact_id"] = $contact[0]["id"];
  723. $details["network"] = $contact[0]["network"];
  724. $details["not_following"] = !in_array($contact[0]["rel"], array(CONTACT_IS_SHARING, CONTACT_IS_FRIEND));
  725. } else {
  726. logger("No contact found for user ".$uid." and url ".$actor, LOGGER_DEBUG);
  727. // Adding a global contact
  728. /// @TODO Use this data for the post
  729. $details["global_contact_id"] = get_contact($actor, 0);
  730. logger("Global contact ".$global_contact_id." found for url ".$actor, LOGGER_DEBUG);
  731. $details["contact_id"] = $contact_id;
  732. $details["network"] = NETWORK_OSTATUS;
  733. $details["not_following"] = true;
  734. }
  735. return $details;
  736. }
  737. /**
  738. * @brief Stores an item and completes the thread
  739. *
  740. * @param string $conversation_url The URI of the conversation
  741. * @param integer $uid The user id
  742. * @param array $item Data of the item that is to be posted
  743. *
  744. * @return integer The item id of the posted item array
  745. */
  746. private function completion($conversation_url, $uid, $item = array(), $self = "") {
  747. /// @todo This function is totally ugly and has to be rewritten totally
  748. // Import all threads or only threads that were started by our followers?
  749. $all_threads = !get_config('system','ostatus_full_threads');
  750. $item_stored = -1;
  751. $conversation_url = self::fetch_conversation($self, $conversation_url);
  752. // If the thread shouldn't be completed then store the item and go away
  753. // Don't do a completion on liked content
  754. if (((intval(get_config('system','ostatus_poll_interval')) == -2) AND (count($item) > 0)) OR
  755. ($item["verb"] == ACTIVITY_LIKE) OR ($conversation_url == "")) {
  756. $item_stored = item_store($item, $all_threads);
  757. return $item_stored;
  758. } elseif (count($item) > 0) {
  759. $item = store_conversation($item);
  760. }
  761. // Get the parent
  762. $parents = q("SELECT `item`.`id`, `item`.`parent`, `item`.`uri`, `item`.`contact-id`, `item`.`type`,
  763. `item`.`verb`, `item`.`visible` FROM `term`
  764. STRAIGHT_JOIN `item` AS `thritem` ON `thritem`.`parent` = `term`.`oid`
  765. STRAIGHT_JOIN `item` ON `item`.`parent` = `thritem`.`parent`
  766. WHERE `term`.`uid` = %d AND `term`.`otype` = %d AND `term`.`type` = %d AND `term`.`url` = '%s'",
  767. intval($uid), intval(TERM_OBJ_POST), intval(TERM_CONVERSATION), dbesc($conversation_url));
  768. /* 2016-10-23: The old query will be kept until we are sure that the query above is a good and fast replacement
  769. $parents = q("SELECT `id`, `parent`, `uri`, `contact-id`, `type`, `verb`, `visible` FROM `item` WHERE `id` IN
  770. (SELECT `parent` FROM `item` WHERE `id` IN
  771. (SELECT `oid` FROM `term` WHERE `uid` = %d AND `otype` = %d AND `type` = %d AND `url` = '%s'))",
  772. intval($uid), intval(TERM_OBJ_POST), intval(TERM_CONVERSATION), dbesc($conversation_url));
  773. */
  774. if ($parents)
  775. $parent = $parents[0];
  776. elseif (count($item) > 0) {
  777. $parent = $item;
  778. $parent["type"] = "remote";
  779. $parent["verb"] = ACTIVITY_POST;
  780. $parent["visible"] = 1;
  781. } else {
  782. // Preset the parent
  783. $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid`=%d", $uid);
  784. if (!$r)
  785. return(-2);
  786. $parent = array();
  787. $parent["id"] = 0;
  788. $parent["parent"] = 0;
  789. $parent["uri"] = "";
  790. $parent["contact-id"] = $r[0]["id"];
  791. $parent["type"] = "remote";
  792. $parent["verb"] = ACTIVITY_POST;
  793. $parent["visible"] = 1;
  794. }
  795. $conv = str_replace("/conversation/", "/api/statusnet/conversation/", $conversation_url).".as";
  796. $pageno = 1;
  797. $items = array();
  798. logger('fetching conversation url '.$conv.' (Self: '.$self.') for user '.$uid);
  799. do {
  800. $conv_arr = z_fetch_url($conv."?page=".$pageno);
  801. // If it is a non-ssl site and there is an error, then try ssl or vice versa
  802. if (!$conv_arr["success"] AND (substr($conv, 0, 7) == "http://")) {
  803. $conv = str_replace("http://", "https://", $conv);
  804. $conv_as = fetch_url($conv."?page=".$pageno);
  805. } elseif (!$conv_arr["success"] AND (substr($conv, 0, 8) == "https://")) {
  806. $conv = str_replace("https://", "http://", $conv);
  807. $conv_as = fetch_url($conv."?page=".$pageno);
  808. } else
  809. $conv_as = $conv_arr["body"];
  810. $conv_as = str_replace(',"statusnet:notice_info":', ',"statusnet_notice_info":', $conv_as);
  811. $conv_as = json_decode($conv_as);
  812. $no_of_items = sizeof($items);
  813. if (@is_array($conv_as->items))
  814. foreach ($conv_as->items AS $single_item)
  815. $items[$single_item->id] = $single_item;
  816. if ($no_of_items == sizeof($items))
  817. break;
  818. $pageno++;
  819. } while (true);
  820. logger('fetching conversation done. Found '.count($items).' items');
  821. if (!sizeof($items)) {
  822. if (count($item) > 0) {
  823. $item_stored = item_store($item, $all_threads);
  824. if ($item_stored) {
  825. logger("Conversation ".$conversation_url." couldn't be fetched. Item uri ".$item["uri"]." stored: ".$item_stored, LOGGER_DEBUG);
  826. self::store_conversation($item_id, $conversation_url);
  827. }
  828. return($item_stored);
  829. } else
  830. return(-3);
  831. }
  832. $items = array_reverse($items);
  833. $r = q("SELECT `nurl` FROM `contact` WHERE `uid` = %d AND `self`", intval($uid));
  834. $importer = $r[0];
  835. $new_parent = true;
  836. foreach ($items as $single_conv) {
  837. // Update the gcontact table
  838. self::conv_fetch_actor($single_conv->actor);
  839. // Test - remove before flight
  840. //$tempfile = tempnam(get_temppath(), "conversation");
  841. //file_put_contents($tempfile, json_encode($single_conv));
  842. $mention = false;
  843. if (isset($single_conv->object->id))
  844. $single_conv->id = $single_conv->object->id;
  845. $plink = self::convert_href($single_conv->id);
  846. if (isset($single_conv->object->url))
  847. $plink = self::convert_href($single_conv->object->url);
  848. if (@!$single_conv->id)
  849. continue;
  850. logger("Got id ".$single_conv->id, LOGGER_DEBUG);
  851. if ($first_id == "") {
  852. $first_id = $single_conv->id;
  853. // The first post of the conversation isn't our first post. There are three options:
  854. // 1. Our conversation hasn't the "real" thread starter
  855. // 2. This first post is a post inside our thread
  856. // 3. This first post is a post inside another thread
  857. if (($first_id != $parent["uri"]) AND ($parent["uri"] != "")) {
  858. $new_parent = true;
  859. $new_parents = q("SELECT `id`, `parent`, `uri`, `contact-id`, `type`, `verb`, `visible` FROM `item` WHERE `id` IN
  860. (SELECT `parent` FROM `item`
  861. WHERE `uid` = %d AND `uri` = '%s' AND `network` IN ('%s','%s')) LIMIT 1",
  862. intval($uid), dbesc($first_id), dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN));
  863. if ($new_parents) {
  864. if ($new_parents[0]["parent"] == $parent["parent"]) {
  865. // Option 2: This post is already present inside our thread - but not as thread starter
  866. logger("Option 2: uri present in our thread: ".$first_id, LOGGER_DEBUG);
  867. $first_id = $parent["uri"];
  868. } else {
  869. // Option 3: Not so good. We have mixed parents. We have to see how to clean this up.
  870. // For now just take the new parent.
  871. $parent = $new_parents[0];
  872. $first_id = $parent["uri"];
  873. logger("Option 3: mixed parents for uri ".$first_id, LOGGER_DEBUG);
  874. }
  875. } else {
  876. // Option 1: We hadn't got the real thread starter
  877. // We have to clean up our existing messages.
  878. $parent["id"] = 0;
  879. $parent["uri"] = $first_id;
  880. logger("Option 1: we have a new parent: ".$first_id, LOGGER_DEBUG);
  881. }
  882. } elseif ($parent["uri"] == "") {
  883. $parent["id"] = 0;
  884. $parent["uri"] = $first_id;
  885. }
  886. }
  887. $parent_uri = $parent["uri"];
  888. // "context" only seems to exist on older servers
  889. if (isset($single_conv->context->inReplyTo->id)) {
  890. $parent_exists = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s' AND `network` IN ('%s','%s') LIMIT 1",
  891. intval($uid), dbesc($single_conv->context->inReplyTo->id), dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN));
  892. if ($parent_exists)
  893. $parent_uri = $single_conv->context->inReplyTo->id;
  894. }
  895. // This is the current way
  896. if (isset($single_conv->object->inReplyTo->id)) {
  897. $parent_exists = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s' AND `network` IN ('%s','%s') LIMIT 1",
  898. intval($uid), dbesc($single_conv->object->inReplyTo->id), dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN));
  899. if ($parent_exists)
  900. $parent_uri = $single_conv->object->inReplyTo->id;
  901. }
  902. $message_exists = q("SELECT `id`, `parent`, `uri` FROM `item` WHERE `uid` = %d AND `uri` = '%s' AND `network` IN ('%s','%s') LIMIT 1",
  903. intval($uid), dbesc($single_conv->id),
  904. dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN));
  905. if ($message_exists) {
  906. logger("Message ".$single_conv->id." already existed on the system", LOGGER_DEBUG);
  907. if ($parent["id"] != 0) {
  908. $existing_message = $message_exists[0];
  909. // We improved the way we fetch OStatus messages, this shouldn't happen very often now
  910. /// @TODO We have to change the shadow copies as well. This way here is really ugly.
  911. if ($existing_message["parent"] != $parent["id"]) {
  912. logger('updating id '.$existing_message["id"].' with parent '.$existing_message["parent"].' to parent '.$parent["id"].' uri '.$parent["uri"].' thread '.$parent_uri, LOGGER_DEBUG);
  913. // Update the parent id of the selected item
  914. $r = q("UPDATE `item` SET `parent` = %d, `parent-uri` = '%s' WHERE `id` = %d",
  915. intval($parent["id"]), dbesc($parent["uri"]), intval($existing_message["id"]));
  916. // Update the parent uri in the thread - but only if it points to itself
  917. $r = q("UPDATE `item` SET `thr-parent` = '%s' WHERE `id` = %d AND `uri` = `thr-parent`",
  918. dbesc($parent_uri), intval($existing_message["id"]));
  919. // try to change all items of the same parent
  920. $r = q("UPDATE `item` SET `parent` = %d, `parent-uri` = '%s' WHERE `parent` = %d",
  921. intval($parent["id"]), dbesc($parent["uri"]), intval($existing_message["parent"]));
  922. // Update the parent uri in the thread - but only if it points to itself
  923. $r = q("UPDATE `item` SET `thr-parent` = '%s' WHERE (`parent` = %d) AND (`uri` = `thr-parent`)",
  924. dbesc($parent["uri"]), intval($existing_message["parent"]));
  925. // Now delete the thread
  926. delete_thread($existing_message["parent"]);
  927. }
  928. }
  929. // The item we are having on the system is the one that we wanted to store via the item array
  930. if (isset($item["uri"]) AND ($item["uri"] == $existing_message["uri"])) {
  931. $item = array();
  932. $item_stored = 0;
  933. }
  934. continue;
  935. }
  936. if (is_array($single_conv->to))
  937. foreach($single_conv->to AS $to)
  938. if ($importer["nurl"] == normalise_link($to->id))
  939. $mention = true;
  940. $actor = $single_conv->actor->id;
  941. if (isset($single_conv->actor->url))
  942. $actor = $single_conv->actor->url;
  943. $details = self::get_actor_details($actor, $uid, $parent["contact-id"]);
  944. // Do we only want to import threads that were started by our contacts?
  945. if ($details["not_following"] AND $new_parent AND get_config('system','ostatus_full_threads')) {
  946. logger("Don't import uri ".$first_id." because user ".$uid." doesn't follow the person ".$actor, LOGGER_DEBUG);
  947. continue;
  948. }
  949. $arr = array();
  950. $arr["network"] = $details["network"];
  951. $arr["uri"] = $single_conv->id;
  952. $arr["plink"] = $plink;
  953. $arr["uid"] = $uid;
  954. $arr["contact-id"] = $details["contact_id"];
  955. $arr["parent-uri"] = $parent_uri;
  956. $arr["created"] = $single_conv->published;
  957. $arr["edited"] = $single_conv->published;
  958. $arr["owner-name"] = $single_conv->actor->displayName;
  959. if ($arr["owner-name"] == '')
  960. $arr["owner-name"] = $single_conv->actor->contact->displayName;
  961. if ($arr["owner-name"] == '')
  962. $arr["owner-name"] = $single_conv->actor->portablecontacts_net->displayName;
  963. $arr["owner-link"] = $actor;
  964. $arr["owner-avatar"] = self::fix_avatar($single_conv->actor->image->url, $arr["owner-link"]);
  965. $arr["author-name"] = $arr["owner-name"];
  966. $arr["author-link"] = $arr["owner-link"];
  967. $arr["author-avatar"] = $arr["owner-avatar"];
  968. $arr["body"] = add_page_info_to_body(html2bbcode($single_conv->content));
  969. if (isset($single_conv->status_net->conversation)) {
  970. $arr['conversation-uri'] = $single_conv->status_net->conversation;
  971. }
  972. if (isset($single_conv->status_net->notice_info->source))
  973. $arr["app"] = strip_tags($single_conv->status_net->notice_info->source);
  974. elseif (isset($single_conv->statusnet->notice_info->source))
  975. $arr["app"] = strip_tags($single_conv->statusnet->notice_info->source);
  976. elseif (isset($single_conv->statusnet_notice_info->source))
  977. $arr["app"] = strip_tags($single_conv->statusnet_notice_info->source);
  978. elseif (isset($single_conv->provider->displayName))
  979. $arr["app"] = $single_conv->provider->displayName;
  980. else
  981. $arr["app"] = "OStatus";
  982. $arr["source"] = json_encode($single_conv);
  983. $arr["protocol"] = PROTOCOL_GS_CONVERSATION;
  984. $arr["verb"] = $parent["verb"];
  985. $arr["visible"] = $parent["visible"];
  986. $arr["location"] = $single_conv->location->displayName;
  987. $arr["coord"] = trim($single_conv->location->lat." ".$single_conv->location->lon);
  988. // Is it a reshared item?
  989. if (isset($single_conv->verb) AND ($single_conv->verb == "share") AND isset($single_conv->object)) {
  990. if (is_array($single_conv->object))
  991. $single_conv->object = $single_conv->object[0];
  992. logger("Found reshared item ".$single_conv->object->id);
  993. // $single_conv->object->context->conversation;
  994. if (isset($single_conv->object->object->id))
  995. $arr["uri"] = $single_conv->object->object->id;
  996. else
  997. $arr["uri"] = $single_conv->object->id;
  998. if (isset($single_conv->object->object->url))
  999. $plink = self::convert_href($single_conv->object->object->url);
  1000. else
  1001. $plink = self::convert_href($single_conv->object->url);
  1002. if (isset($single_conv->object->object->content))
  1003. $arr["body"] = add_page_info_to_body(html2bbcode($single_conv->object->object->content));
  1004. else
  1005. $arr["body"] = add_page_info_to_body(html2bbcode($single_conv->object->content));
  1006. $arr["plink"] = $plink;
  1007. $arr["created"] = $single_conv->object->published;
  1008. $arr["edited"] = $single_conv->object->published;
  1009. $arr["author-name"] = $single_conv->object->actor->displayName;
  1010. if ($arr["owner-name"] == '') {
  1011. $arr["author-name"] = $single_conv->object->actor->contact->displayName;
  1012. }
  1013. $arr["author-link"] = $single_conv->object->actor->url;
  1014. $arr["author-avatar"] = self::fix_avatar($single_conv->object->actor->image->url, $arr["author-link"]);
  1015. $arr["app"] = $single_conv->object->provider->displayName."#";
  1016. //$arr["verb"] = $single_conv->object->verb;
  1017. $arr["location"] = $single_conv->object->location->displayName;
  1018. $arr["coord"] = trim($single_conv->object->location->lat." ".$single_conv->object->location->lon);
  1019. }
  1020. if ($arr["location"] == "")
  1021. unset($arr["location"]);
  1022. if ($arr["coord"] == "")
  1023. unset($arr["coord"]);
  1024. // Copy fields from given item array
  1025. if (isset($item["uri"]) AND (($item["uri"] == $arr["uri"]) OR ($item["uri"] == $single_conv->id))) {
  1026. $copy_fields = array("owner-name", "owner-link", "owner-avatar", "author-name", "author-link", "author-avatar",
  1027. "gravity", "body", "object-type", "object", "verb", "created", "edited", "coord", "tag",
  1028. "title", "attach", "app", "type", "location", "contact-id", "uri");
  1029. foreach ($copy_fields AS $field)
  1030. if (isset($item[$field]))
  1031. $arr[$field] = $item[$field];
  1032. }
  1033. $newitem = item_store($arr);
  1034. if (!$newitem) {
  1035. logger("Item wasn't stored ".print_r($arr, true), LOGGER_DEBUG);
  1036. continue;
  1037. }
  1038. if (isset($item["uri"]) AND ($item["uri"] == $arr["uri"])) {
  1039. $item = array();
  1040. $item_stored = $newitem;
  1041. }
  1042. logger('Stored new item '.$plink.' for parent '.$arr["parent-uri"].' under id '.$newitem, LOGGER_DEBUG);
  1043. // Add the conversation entry (but don't fetch the whole conversation)
  1044. self::store_conversation($newitem, $conversation_url);
  1045. // If the newly created item is the top item then change the parent settings of the thread
  1046. // This shouldn't happen anymore. This is supposed to be absolote.
  1047. if ($arr["uri"] == $first_id) {
  1048. logger('setting new parent to id '.$newitem);
  1049. $new_parents = q("SELECT `id`, `uri`, `contact-id`, `type`, `verb`, `visible` FROM `item` WHERE `uid` = %d AND `id` = %d LIMIT 1",
  1050. intval($uid), intval($newitem));
  1051. if ($new_parents)
  1052. $parent = $new_parents[0];
  1053. }
  1054. }
  1055. if (($item_stored < 0) AND (count($item) > 0)) {
  1056. if (get_config('system','ostatus_full_threads')) {
  1057. $details = self::get_actor_details($item["owner-link"], $uid, $item["contact-id"]);
  1058. if ($details["not_following"]) {
  1059. logger("Don't import uri ".$item["uri"]." because user ".$uid." doesn't follow the person ".$item["owner-link"], LOGGER_DEBUG);
  1060. return false;
  1061. }
  1062. }
  1063. $item_stored = item_store($item, $all_threads);
  1064. if ($item_stored) {
  1065. logger("Uri ".$item["uri"]." wasn't found in conversation ".$conversation_url, LOGGER_DEBUG);
  1066. self::store_conversation($item_stored, $conversation_url);
  1067. }
  1068. }
  1069. return($item_stored);
  1070. }
  1071. /**
  1072. * @brief Stores conversation data into the database
  1073. *
  1074. * @param integer $itemid The id of the item
  1075. * @param string $conversation_url The uri of the conversation
  1076. */
  1077. private function store_conversation($itemid, $conversation_url) {
  1078. $conversation_url = self::convert_href($conversation_url);
  1079. $messages = q("SELECT `uid`, `parent`, `created`, `received`, `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($itemid));
  1080. if (!$messages)
  1081. return;
  1082. $message = $messages[0];
  1083. // Store conversation url if not done before
  1084. $conversation = q("SELECT `url` FROM `term` WHERE `uid` = %d AND `oid` = %d AND `otype` = %d AND `type` = %d",
  1085. intval($message["uid"]), intval($itemid), intval(TERM_OBJ_POST), intval(TERM_CONVERSATION));
  1086. if (!$conversation) {
  1087. $r = q("INSERT INTO `term` (`uid`, `oid`, `otype`, `type`, `term`, `url`, `created`, `received`, `guid`) VALUES (%d, %d, %d, %d, '%s', '%s', '%s', '%s', '%s')",
  1088. intval($message["uid"]), intval($itemid), intval(TERM_OBJ_POST), intval(TERM_CONVERSATION),
  1089. dbesc($message["created"]), dbesc($conversation_url), dbesc($message["created"]), dbesc($message["received"]), dbesc($message["guid"]));
  1090. logger('Storing conversation url '.$conversation_url.' for id '.$itemid);
  1091. }
  1092. }
  1093. /**
  1094. * @brief Checks if the current post is a reshare
  1095. *
  1096. * @param array $item The item array of thw post
  1097. *
  1098. * @return string The guid if the post is a reshare
  1099. */
  1100. private function get_reshared_guid($item) {
  1101. $body = trim($item["body"]);
  1102. // Skip if it isn't a pure repeated messages
  1103. // Does it start with a share?
  1104. if (strpos($body, "[share") > 0)
  1105. return("");
  1106. // Does it end with a share?
  1107. if (strlen($body) > (strrpos($body, "[/share]") + 8))
  1108. return("");
  1109. $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$1",$body);
  1110. // Skip if there is no shared message in there
  1111. if ($body == $attributes)
  1112. return(false);
  1113. $guid = "";
  1114. preg_match("/guid='(.*?)'/ism", $attributes, $matches);
  1115. if ($matches[1] != "")
  1116. $guid = $matches[1];
  1117. preg_match('/guid="(.*?)"/ism', $attributes, $matches);
  1118. if ($matches[1] != "")
  1119. $guid = $matches[1];
  1120. return $guid;
  1121. }
  1122. /**
  1123. * @brief Cleans the body of a post if it contains picture links
  1124. *
  1125. * @param string $body The body
  1126. *
  1127. * @return string The cleaned body
  1128. */
  1129. private function format_picture_post($body) {
  1130. $siteinfo = get_attached_data($body);
  1131. if (($siteinfo["type"] == "photo")) {
  1132. if (isset($siteinfo["preview"]))
  1133. $preview = $siteinfo["preview"];
  1134. else
  1135. $preview = $siteinfo["image"];
  1136. // Is it a remote picture? Then make a smaller preview here
  1137. $preview = proxy_url($preview, false, PROXY_SIZE_SMALL);
  1138. // Is it a local picture? Then make it smaller here
  1139. $preview = str_replace(array("-0.jpg", "-0.png"), array("-2.jpg", "-2.png"), $preview);
  1140. $preview = str_replace(array("-1.jpg", "-1.png"), array("-2.jpg", "-2.png"), $preview);
  1141. if (isset($siteinfo["url"]))
  1142. $url = $siteinfo["url"];
  1143. else
  1144. $url = $siteinfo["image"];
  1145. $body = trim($siteinfo["text"])." [url]".$url."[/url]\n[img]".$preview."[/img]";
  1146. }
  1147. return $body;
  1148. }
  1149. /**
  1150. * @brief Adds the header elements to the XML document
  1151. *
  1152. * @param object $doc XML document
  1153. * @param array $owner Contact data of the poster
  1154. *
  1155. * @return object header root element
  1156. */
  1157. private function add_header($doc, $owner) {
  1158. $a = get_app();
  1159. $root = $doc->createElementNS(NAMESPACE_ATOM1, 'feed');
  1160. $doc->appendChild($root);
  1161. $root->setAttribute("xmlns:thr", NAMESPACE_THREAD);
  1162. $root->setAttribute("xmlns:georss", NAMESPACE_GEORSS);
  1163. $root->setAttribute("xmlns:activity", NAMESPACE_ACTIVITY);
  1164. $root->setAttribute("xmlns:media", NAMESPACE_MEDIA);
  1165. $root->setAttribute("xmlns:poco", NAMESPACE_POCO);
  1166. $root->setAttribute("xmlns:ostatus", NAMESPACE_OSTATUS);
  1167. $root->setAttribute("xmlns:statusnet", NAMESPACE_STATUSNET);
  1168. $root->setAttribute("xmlns:mastodon", NAMESPACE_MASTODON);
  1169. $attributes = array("uri" => "https://friendi.ca", "version" => FRIENDICA_VERSION."-".DB_UPDATE_VERSION);
  1170. xml::add_element($doc, $root, "generator", FRIENDICA_PLATFORM, $attributes);
  1171. xml::add_element($doc, $root, "id", App::get_baseurl()."/profile/".$owner["nick"]);
  1172. xml::add_element($doc, $root, "title", sprintf("%s timeline", $owner["name"]));
  1173. xml::add_element($doc, $root, "subtitle", sprintf("Updates from %s on %s", $owner["name"], $a->config["sitename"]));
  1174. xml::add_element($doc, $root, "logo", $owner["photo"]);
  1175. xml::add_element($doc, $root, "updated", datetime_convert("UTC", "UTC", "now", ATOM_TIME));
  1176. $author = self::add_author($doc, $owner);
  1177. $root->appendChild($author);
  1178. $attributes = array("href" => $owner["url"], "rel" => "alternate", "type" => "text/html");
  1179. xml::add_element($doc, $root, "link", "", $attributes);
  1180. /// @TODO We have to find out what this is
  1181. /// $attributes = array("href" => App::get_baseurl()."/sup",
  1182. /// "rel" => "http://api.friendfeed.com/2008/03#sup",
  1183. /// "type" => "application/json");
  1184. /// xml::add_element($doc, $root, "link", "", $attributes);
  1185. self::hublinks($doc, $root);
  1186. $attributes = array("href" => App::get_baseurl()."/salmon/".$owner["nick"], "rel" => "salmon");
  1187. xml::add_element($doc, $root, "link", "", $attributes);
  1188. $attributes = array("href" => App::get_baseurl()."/salmon/".$owner["nick"], "rel" => "http://salmon-protocol.org/ns/salmon-replies");
  1189. xml::add_element($doc, $root, "link", "", $attributes);
  1190. $attributes = array("href" => App::get_baseurl()."/salmon/".$owner["nick"], "rel" => "http://salmon-protocol.org/ns/salmon-mention");
  1191. xml::add_element($doc, $root, "link", "", $attributes);
  1192. $attributes = array("href" => App::get_baseurl()."/api/statuses/user_timeline/".$owner["nick"].".atom",
  1193. "rel" => "self", "type" => "application/atom+xml");
  1194. xml::add_element($doc, $root, "link", "", $attributes);
  1195. return $root;
  1196. }
  1197. /**
  1198. * @brief Add the link to the push hubs to the XML document
  1199. *
  1200. * @param object $doc XML document
  1201. * @param object $root XML root element where the hub links are added
  1202. */
  1203. public static function hublinks($doc, $root) {
  1204. $hub = get_config('system','huburl');
  1205. $hubxml = '';
  1206. if(strlen($hub)) {
  1207. $hubs = explode(',', $hub);
  1208. if(count($hubs)) {
  1209. foreach($hubs as $h) {
  1210. $h = trim($h);
  1211. if(! strlen($h))
  1212. continue;
  1213. if ($h === '[internal]')
  1214. $h = App::get_baseurl() . '/pubsubhubbub';
  1215. xml::add_element($doc, $root, "link", "", array("href" => $h, "rel" => "hub"));
  1216. }
  1217. }
  1218. }
  1219. }
  1220. /**
  1221. * @brief Adds attachement data to the XML document
  1222. *
  1223. * @param object $doc XML document
  1224. * @param object $root XML root element where the hub links are added
  1225. * @param array $item Data of the item that is to be posted
  1226. */
  1227. private function get_attachment($doc, $root, $item) {
  1228. $o = "";
  1229. $siteinfo = get_attached_data($item["body"]);
  1230. switch ($siteinfo["type"]) {
  1231. case 'photo':
  1232. $imgdata = get_photo_info($siteinfo["image"]);
  1233. $attributes = array("rel" => "enclosure",
  1234. "href" => $siteinfo["image"],
  1235. "type" => $imgdata["mime"],
  1236. "length" => intval($imgdata["size"]));
  1237. xml::add_element($doc, $root, "link", "", $attributes);
  1238. break;
  1239. case 'video':
  1240. $attributes = array("rel" => "enclosure",
  1241. "href" => $siteinfo["url"],
  1242. "type" => "text/html; charset=UTF-8",
  1243. "length" => "",
  1244. "title" => $siteinfo["title"]);
  1245. xml::add_element($doc, $root, "link", "", $attributes);
  1246. break;
  1247. default:
  1248. break;
  1249. }
  1250. if (($siteinfo["type"] != "photo") AND isset($siteinfo["image"])) {
  1251. $imgdata = get_photo_info($siteinfo["image"]);
  1252. $attributes = array("rel" => "enclosure",
  1253. "href" => $siteinfo["image"],
  1254. "type" => $imgdata["mime"],
  1255. "length" => intval($imgdata["size"]));
  1256. xml::add_element($doc, $root, "link", "", $attributes);
  1257. }
  1258. $arr = explode('[/attach],', $item['attach']);
  1259. if (count($arr)) {
  1260. foreach ($arr as $r) {
  1261. $matches = false;
  1262. $cnt = preg_match('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\" title=\"(.*?)\"|', $r, $matches);
  1263. if ($cnt) {
  1264. $attributes = array("rel" => "enclosure",
  1265. "href" => $matches[1],
  1266. "type" => $matches[3]);
  1267. if (intval($matches[2])) {
  1268. $attributes["length"] = intval($matches[2]);
  1269. }
  1270. if (trim($matches[4]) != "") {
  1271. $attributes["title"] = trim($matches[4]);
  1272. }
  1273. xml::add_element($doc, $root, "link", "", $attributes);
  1274. }
  1275. }
  1276. }
  1277. }
  1278. /**
  1279. * @brief Adds the author element to the XML document
  1280. *
  1281. * @param object $doc XML document
  1282. * @param array $owner Contact data of the poster
  1283. *
  1284. * @return object author element
  1285. */
  1286. private function add_author($doc, $owner) {
  1287. $r = q("SELECT `homepage`, `publish` FROM `profile` WHERE `uid` = %d AND `is-default` LIMIT 1", intval($owner["uid"]));
  1288. if ($r)
  1289. $profile = $r[0];
  1290. $author = $doc->createElement("author");
  1291. xml::add_element($doc, $author, "id", $owner["url"]);
  1292. xml::add_element($doc, $author, "activity:object-type", ACTIVITY_OBJ_PERSON);
  1293. xml::add_element($doc, $author, "uri", $owner["url"]);
  1294. xml::add_element($doc, $author, "name", $owner["nick"]);
  1295. xml::add_element($doc, $author, "email", $owner["addr"]);
  1296. xml::add_element($doc, $author, "summary", bbcode($owner["about"], false, false, 7));
  1297. $attributes = array("rel" => "alternate", "type" => "text/html", "href" => $owner["url"]);
  1298. xml::add_element($doc, $author, "link", "", $attributes);
  1299. $attributes = array(
  1300. "rel" => "avatar",
  1301. "type" => "image/jpeg", // To-Do?
  1302. "media:width" => 175,
  1303. "media:height" => 175,
  1304. "href" => $owner["photo"]);
  1305. xml::add_element($doc, $author, "link", "", $attributes);
  1306. if (isset($owner["thumb"])) {
  1307. $attributes = array(
  1308. "rel" => "avatar",
  1309. "type" => "image/jpeg", // To-Do?
  1310. "media:width" => 80,
  1311. "media:height" => 80,
  1312. "href" => $owner["thumb"]);
  1313. xml::add_element($doc, $author, "link", "", $attributes);
  1314. }
  1315. xml::add_element($doc, $author, "poco:preferredUsername", $owner["nick"]);
  1316. xml::add_element($doc, $author, "poco:displayName", $owner["name"]);
  1317. xml::add_element($doc, $author, "poco:note", bbcode($owner["about"], false, false, 7));
  1318. if (trim($owner["location"]) != "") {
  1319. $element = $doc->createElement("poco:address");
  1320. xml::add_element($doc, $element, "poco:formatted", $owner["location"]);
  1321. $author->appendChild($element);
  1322. }
  1323. if (trim($profile["homepage"]) != "") {
  1324. $urls = $doc->createElement("poco:urls");
  1325. xml::add_element($doc, $urls, "poco:type", "homepage");
  1326. xml::add_element($doc, $urls, "poco:value", $profile["homepage"]);
  1327. xml::add_element($doc, $urls, "poco:primary", "true");
  1328. $author->appendChild($urls);
  1329. }
  1330. if (count($profile)) {
  1331. xml::add_element($doc, $author, "followers", "", array("url" => App::get_baseurl()."/viewcontacts/".$owner["nick"]));
  1332. xml::add_element($doc, $author, "statusnet:profile_info", "", array("local_id" => $owner["uid"]));
  1333. }
  1334. if ($profile["publish"]) {
  1335. xml::add_element($doc, $author, "mastodon:scope", "public");
  1336. }
  1337. return $author;
  1338. }
  1339. /**
  1340. * @TODO Picture attachments should look like this:
  1341. * <a href="https://status.pirati.ca/attachment/572819" title="https://status.pirati.ca/file/heluecht-20151202T222602-rd3u49p.gif"
  1342. * class="attachment thumbnail" id="attachment-572819" rel="nofollow external">https://status.pirati.ca/attachment/572819</a>
  1343. *
  1344. */
  1345. /**
  1346. * @brief Returns the given activity if present - otherwise returns the "post" activity
  1347. *
  1348. * @param array $item Data of the item that is to be posted
  1349. *
  1350. * @return string activity
  1351. */
  1352. function construct_verb($item) {
  1353. if ($item['verb'])
  1354. return $item['verb'];
  1355. return ACTIVITY_POST;
  1356. }
  1357. /**
  1358. * @brief Returns the given object type if present - otherwise returns the "note" object type
  1359. *
  1360. * @param array $item Data of the item that is to be posted
  1361. *
  1362. * @return string Object type
  1363. */
  1364. function construct_objecttype($item) {
  1365. if (in_array($item['object-type'], array(ACTIVITY_OBJ_NOTE, ACTIVITY_OBJ_COMMENT)))
  1366. return $item['object-type'];
  1367. return ACTIVITY_OBJ_NOTE;
  1368. }
  1369. /**
  1370. * @brief Adds an entry element to the XML document
  1371. *
  1372. * @param object $doc XML document
  1373. * @param array $item Data of the item that is to be posted
  1374. * @param array $owner Contact data of the poster
  1375. * @param bool $toplevel
  1376. *
  1377. * @return object Entry element
  1378. */
  1379. private function entry($doc, $item, $owner, $toplevel = false) {
  1380. $repeated_guid = self::get_reshared_guid($item);
  1381. if ($repeated_guid != "")
  1382. $xml = self::reshare_entry($doc, $item, $owner, $repeated_guid, $toplevel);
  1383. if ($xml)
  1384. return $xml;
  1385. if ($item["verb"] == ACTIVITY_LIKE) {
  1386. return self::like_entry($doc, $item, $owner, $toplevel);
  1387. } elseif (in_array($item["verb"], array(ACTIVITY_FOLLOW, NAMESPACE_OSTATUS."/unfollow"))) {
  1388. return self::follow_entry($doc, $item, $owner, $toplevel);
  1389. } else {
  1390. return self::note_entry($doc, $item, $owner, $toplevel);
  1391. }
  1392. }
  1393. /**
  1394. * @brief Adds a source entry to the XML document
  1395. *
  1396. * @param object $doc XML document
  1397. * @param array $contact Array of the contact that is added
  1398. *
  1399. * @return object Source element
  1400. */
  1401. private function source_entry($doc, $contact) {
  1402. $source = $doc->createElement("source");
  1403. xml::add_element($doc, $source, "id", $contact["poll"]);
  1404. xml::add_element($doc, $source, "title", $contact["name"]);
  1405. xml::add_element($doc, $source, "link", "", array("rel" => "alternate",
  1406. "type" => "text/html",
  1407. "href" => $contact["alias"]));
  1408. xml::add_element($doc, $source, "link", "", array("rel" => "self",
  1409. "type" => "application/atom+xml",
  1410. "href" => $contact["poll"]));
  1411. xml::add_element($doc, $source, "icon", $contact["photo"]);
  1412. xml::add_element($doc, $source, "updated", datetime_convert("UTC","UTC",$contact["success_update"]."+00:00",ATOM_TIME));
  1413. return $source;
  1414. }
  1415. /**
  1416. * @brief Fetches contact data from the contact or the gcontact table
  1417. *
  1418. * @param string $url URL of the contact
  1419. * @param array $owner Contact data of the poster
  1420. *
  1421. * @return array Contact array
  1422. */
  1423. private function contact_entry($url, $owner) {
  1424. $r = q("SELECT * FROM `contact` WHERE `nurl` = '%s' AND `uid` IN (0, %d) ORDER BY `uid` DESC LIMIT 1",
  1425. dbesc(normalise_link($url)), intval($owner["uid"]));
  1426. if ($r) {
  1427. $contact = $r[0];
  1428. $contact["uid"] = -1;
  1429. }
  1430. if (!$r) {
  1431. $r = q("SELECT * FROM `gcontact` WHERE `nurl` = '%s' LIMIT 1",
  1432. dbesc(normalise_link($url)));
  1433. if ($r) {
  1434. $contact = $r[0];
  1435. $contact["uid"] = -1;
  1436. $contact["success_update"] = $contact["updated"];
  1437. }
  1438. }
  1439. if (!$r)
  1440. $contact = owner;
  1441. if (!isset($contact["poll"])) {
  1442. $data = probe_url($url);
  1443. $contact["poll"] = $data["poll"];
  1444. if (!$contact["alias"])
  1445. $contact["alias"] = $data["alias"];
  1446. }
  1447. if (!isset($contact["alias"]))
  1448. $contact["alias"] = $contact["url"];
  1449. return $contact;
  1450. }
  1451. /**
  1452. * @brief Adds an entry element with reshared content
  1453. *
  1454. * @param object $doc XML document
  1455. * @param array $item Data of the item that is to be posted
  1456. * @param array $owner Contact data of the poster
  1457. * @param $repeated_guid
  1458. * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
  1459. *
  1460. * @return object Entry element
  1461. */
  1462. private function reshare_entry($doc, $item, $owner, $repeated_guid, $toplevel) {
  1463. if (($item["id"] != $item["parent"]) AND (normalise_link($item["author-link"]) != normalise_link($owner["url"]))) {
  1464. logger("OStatus entry is from author ".$owner["url"]." - not from ".$item["author-link"].". Quitting.", LOGGER_DEBUG);
  1465. }
  1466. $title = self::entry_header($doc, $entry, $owner, $toplevel);
  1467. $r = q("SELECT * FROM `item` WHERE `uid` = %d AND `guid` = '%s' AND NOT `private` AND `network` IN ('%s', '%s', '%s') LIMIT 1",
  1468. intval($owner["uid"]), dbesc($repeated_guid),
  1469. dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS));
  1470. if ($r)
  1471. $repeated_item = $r[0];
  1472. else
  1473. return false;
  1474. $contact = self::contact_entry($repeated_item['author-link'], $owner);
  1475. $parent_item = (($item['thr-parent']) ? $item['thr-parent'] : $item['parent-uri']);
  1476. $title = $owner["nick"]." repeated a notice by ".$contact["nick"];
  1477. self::entry_content($doc, $entry, $item, $owner, $title, ACTIVITY_SHARE, false);
  1478. $as_object = $doc->createElement("activity:object");
  1479. xml::add_element($doc, $as_object, "activity:object-type", NAMESPACE_ACTIVITY_SCHEMA."activity");
  1480. self::entry_content($doc, $as_object, $repeated_item, $owner, "", "", false);
  1481. $author = self::add_author($doc, $contact);
  1482. $as_object->appendChild($author);
  1483. $as_object2 = $doc->createElement("activity:object");
  1484. xml::add_element($doc, $as_object2, "activity:object-type", self::construct_objecttype($repeated_item));
  1485. $title = sprintf("New comment by %s", $contact["nick"]);
  1486. self::entry_content($doc, $as_object2, $repeated_item, $owner, $title);
  1487. $as_object->appendChild($as_object2);
  1488. self::entry_footer($doc, $as_object, $item, $owner, false);
  1489. $source = self::source_entry($doc, $contact);
  1490. $as_object->appendChild($source);
  1491. $entry->appendChild($as_object);
  1492. self::entry_footer($doc, $entry, $item, $owner);
  1493. return $entry;
  1494. }
  1495. /**
  1496. * @brief Adds an entry element with a "like"
  1497. *
  1498. * @param object $doc XML document
  1499. * @param array $item Data of the item that is to be posted
  1500. * @param array $owner Contact data of the poster
  1501. * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
  1502. *
  1503. * @return object Entry element with "like"
  1504. */
  1505. private function like_entry($doc, $item, $owner, $toplevel) {
  1506. if (($item["id"] != $item["parent"]) AND (normalise_link($item["author-link"]) != normalise_link($owner["url"]))) {
  1507. logger("OStatus entry is from author ".$owner["url"]." - not from ".$item["author-link"].". Quitting.", LOGGER_DEBUG);
  1508. }
  1509. $title = self::entry_header($doc, $entry, $owner, $toplevel);
  1510. $verb = NAMESPACE_ACTIVITY_SCHEMA."favorite";
  1511. self::entry_content($doc, $entry, $item, $owner, "Favorite", $verb, false);
  1512. $as_object = $doc->createElement("activity:object");
  1513. $parent = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d",
  1514. dbesc($item["thr-parent"]), intval($item["uid"]));
  1515. $parent_item = (($item['thr-parent']) ? $item['thr-parent'] : $item['parent-uri']);
  1516. xml::add_element($doc, $as_object, "activity:object-type", self::construct_objecttype($parent[0]));
  1517. self::entry_content($doc, $as_object, $parent[0], $owner, "New entry");
  1518. $entry->appendChild($as_object);
  1519. self::entry_footer($doc, $entry, $item, $owner);
  1520. return $entry;
  1521. }
  1522. /**
  1523. * @brief Adds the person object element to the XML document
  1524. *
  1525. * @param object $doc XML document
  1526. * @param array $owner Contact data of the poster
  1527. * @param array $contact Contact data of the target
  1528. *
  1529. * @return object author element
  1530. */
  1531. private function add_person_object($doc, $owner, $contact) {
  1532. $object = $doc->createElement("activity:object");
  1533. xml::add_element($doc, $object, "activity:object-type", ACTIVITY_OBJ_PERSON);
  1534. if ($contact['network'] == NETWORK_PHANTOM) {
  1535. xml::add_element($doc, $object, "id", $contact['url']);
  1536. return $object;
  1537. }
  1538. xml::add_element($doc, $object, "id", $contact["alias"]);
  1539. xml::add_element($doc, $object, "title", $contact["nick"]);
  1540. $attributes = array("rel" => "alternate", "type" => "text/html", "href" => $contact["url"]);
  1541. xml::add_element($doc, $object, "link", "", $attributes);
  1542. $attributes = array(
  1543. "rel" => "avatar",
  1544. "type" => "image/jpeg", // To-Do?
  1545. "media:width" => 175,
  1546. "media:height" => 175,
  1547. "href" => $contact["photo"]);
  1548. xml::add_element($doc, $object, "link", "", $attributes);
  1549. xml::add_element($doc, $object, "poco:preferredUsername", $contact["nick"]);
  1550. xml::add_element($doc, $object, "poco:displayName", $contact["name"]);
  1551. if (trim($contact["location"]) != "") {
  1552. $element = $doc->createElement("poco:address");
  1553. xml::add_element($doc, $element, "poco:formatted", $contact["location"]);
  1554. $object->appendChild($element);
  1555. }
  1556. return $object;
  1557. }
  1558. /**
  1559. * @brief Adds a follow/unfollow entry element
  1560. *
  1561. * @param object $doc XML document
  1562. * @param array $item Data of the follow/unfollow message
  1563. * @param array $owner Contact data of the poster
  1564. * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
  1565. *
  1566. * @return object Entry element
  1567. */
  1568. private function follow_entry($doc, $item, $owner, $toplevel) {
  1569. $item["id"] = $item["parent"] = 0;
  1570. $item["created"] = $item["edited"] = date("c");
  1571. $item["private"] = true;
  1572. $contact = Probe::uri($item['follow']);
  1573. if ($contact['alias'] == '') {
  1574. $contact['alias'] = $contact["url"];
  1575. } else {
  1576. $item['follow'] = $contact['alias'];
  1577. }
  1578. $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `nurl` = '%s'",
  1579. intval($owner['uid']), dbesc(normalise_link($contact["url"])));
  1580. if (dbm::is_result($r)) {
  1581. $connect_id = $r[0]['id'];
  1582. } else {
  1583. $connect_id = 0;
  1584. }
  1585. if ($item['verb'] == ACTIVITY_FOLLOW) {
  1586. $message = t('%s is now following %s.');
  1587. $title = t('following');
  1588. $action = "subscription";
  1589. } else {
  1590. $message = t('%s stopped following %s.');
  1591. $title = t('stopped following');
  1592. $action = "unfollow";
  1593. }
  1594. $item["uri"] = $item['parent-uri'] = $item['thr-parent'] =
  1595. 'tag:'.get_app()->get_hostname().
  1596. ','.date('Y-m-d').':'.$action.':'.$owner['uid'].
  1597. ':person:'.$connect_id.':'.$item['created'];
  1598. $item["body"] = sprintf($message, $owner["nick"], $contact["nick"]);
  1599. self::entry_header($doc, $entry, $owner, $toplevel);
  1600. self::entry_content($doc, $entry, $item, $owner, $title);
  1601. $object = self::add_person_object($doc, $owner, $contact);
  1602. $entry->appendChild($object);
  1603. self::entry_footer($doc, $entry, $item, $owner);
  1604. return $entry;
  1605. }
  1606. /**
  1607. * @brief Adds a regular entry element
  1608. *
  1609. * @param object $doc XML document
  1610. * @param array $item Data of the item that is to be posted
  1611. * @param array $owner Contact data of the poster
  1612. * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
  1613. *
  1614. * @return object Entry element
  1615. */
  1616. private function note_entry($doc, $item, $owner, $toplevel) {
  1617. if (($item["id"] != $item["parent"]) AND (normalise_link($item["author-link"]) != normalise_link($owner["url"]))) {
  1618. logger("OStatus entry is from author ".$owner["url"]." - not from ".$item["author-link"].". Quitting.", LOGGER_DEBUG);
  1619. }
  1620. $title = self::entry_header($doc, $entry, $owner, $toplevel);
  1621. xml::add_element($doc, $entry, "activity:object-type", ACTIVITY_OBJ_NOTE);
  1622. self::entry_content($doc, $entry, $item, $owner, $title);
  1623. self::entry_footer($doc, $entry, $item, $owner);
  1624. return $entry;
  1625. }
  1626. /**
  1627. * @brief Adds a header element to the XML document
  1628. *
  1629. * @param object $doc XML document
  1630. * @param object $entry The entry element where the elements are added
  1631. * @param array $owner Contact data of the poster
  1632. * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
  1633. *
  1634. * @return string The title for the element
  1635. */
  1636. private function entry_header($doc, &$entry, $owner, $toplevel) {
  1637. /// @todo Check if this title stuff is really needed (I guess not)
  1638. if (!$toplevel) {
  1639. $entry = $doc->createElement("entry");
  1640. $title = sprintf("New note by %s", $owner["nick"]);
  1641. } else {
  1642. $entry = $doc->createElementNS(NAMESPACE_ATOM1, "entry");
  1643. $entry->setAttribute("xmlns:thr", NAMESPACE_THREAD);
  1644. $entry->setAttribute("xmlns:georss", NAMESPACE_GEORSS);
  1645. $entry->setAttribute("xmlns:activity", NAMESPACE_ACTIVITY);
  1646. $entry->setAttribute("xmlns:media", NAMESPACE_MEDIA);
  1647. $entry->setAttribute("xmlns:poco", NAMESPACE_POCO);
  1648. $entry->setAttribute("xmlns:ostatus", NAMESPACE_OSTATUS);
  1649. $entry->setAttribute("xmlns:statusnet", NAMESPACE_STATUSNET);
  1650. $entry->setAttribute("xmlns:mastodon", NAMESPACE_MASTODON);
  1651. $author = self::add_author($doc, $owner);
  1652. $entry->appendChild($author);
  1653. $title = sprintf("New comment by %s", $owner["nick"]);
  1654. }
  1655. return $title;
  1656. }
  1657. /**
  1658. * @brief Adds elements to the XML document
  1659. *
  1660. * @param object $doc XML document
  1661. * @param object $entry Entry element where the content is added
  1662. * @param array $item Data of the item that is to be posted
  1663. * @param array $owner Contact data of the poster
  1664. * @param string $title Title for the post
  1665. * @param string $verb The activity verb
  1666. * @param bool $complete Add the "status_net" element?
  1667. */
  1668. private function entry_content($doc, $entry, $item, $owner, $title, $verb = "", $complete = true) {
  1669. if ($verb == "")
  1670. $verb = self::construct_verb($item);
  1671. xml::add_element($doc, $entry, "id", $item["uri"]);
  1672. xml::add_element($doc, $entry, "title", $title);
  1673. $body = self::format_picture_post($item['body']);
  1674. if ($item['title'] != "")
  1675. $body = "[b]".$item['title']."[/b]\n\n".$body;
  1676. $body = bbcode($body, false, false, 7);
  1677. xml::add_element($doc, $entry, "content", $body, array("type" => "html"));
  1678. xml::add_element($doc, $entry, "link", "", array("rel" => "alternate", "type" => "text/html",
  1679. "href" => App::get_baseurl()."/display/".$item["guid"]));
  1680. if ($complete AND ($item["id"] > 0))
  1681. xml::add_element($doc, $entry, "status_net", "", array("notice_id" => $item["id"]));
  1682. xml::add_element($doc, $entry, "activity:verb", $verb);
  1683. xml::add_element($doc, $entry, "published", datetime_convert("UTC","UTC",$item["created"]."+00:00",ATOM_TIME));
  1684. xml::add_element($doc, $entry, "updated", datetime_convert("UTC","UTC",$item["edited"]."+00:00",ATOM_TIME));
  1685. }
  1686. /**
  1687. * @brief Adds the elements at the foot of an entry to the XML document
  1688. *
  1689. * @param object $doc XML document
  1690. * @param object $entry The entry element where the elements are added
  1691. * @param array $item Data of the item that is to be posted
  1692. * @param array $owner Contact data of the poster
  1693. * @param $complete
  1694. */
  1695. private function entry_footer($doc, $entry, $item, $owner, $complete = true) {
  1696. $mentioned = array();
  1697. if (($item['parent'] != $item['id']) OR ($item['parent-uri'] !== $item['uri']) OR (($item['thr-parent'] !== '') AND ($item['thr-parent'] !== $item['uri']))) {
  1698. $parent = q("SELECT `guid`, `author-link`, `owner-link` FROM `item` WHERE `id` = %d", intval($item["parent"]));
  1699. $parent_item = (($item['thr-parent']) ? $item['thr-parent'] : $item['parent-uri']);
  1700. $thrparent = q("SELECT `guid`, `author-link`, `owner-link`, `plink` FROM `item` WHERE `uid` = %d AND `uri` = '%s'",
  1701. intval($owner["uid"]),
  1702. dbesc($parent_item));
  1703. if ($thrparent) {
  1704. $mentioned[$thrparent[0]["author-link"]] = $thrparent[0]["author-link"];
  1705. $mentioned[$thrparent[0]["owner-link"]] = $thrparent[0]["owner-link"];
  1706. $parent_plink = $thrparent[0]["plink"];
  1707. } else {
  1708. $mentioned[$parent[0]["author-link"]] = $parent[0]["author-link"];
  1709. $mentioned[$parent[0]["owner-link"]] = $parent[0]["owner-link"];
  1710. $parent_plink = App::get_baseurl()."/display/".$parent[0]["guid"];
  1711. }
  1712. $attributes = array(
  1713. "ref" => $parent_item,
  1714. "href" => $parent_plink);
  1715. xml::add_element($doc, $entry, "thr:in-reply-to", "", $attributes);
  1716. $attributes = array(
  1717. "rel" => "related",
  1718. "href" => $parent_plink);
  1719. xml::add_element($doc, $entry, "link", "", $attributes);
  1720. }
  1721. if (intval($item["parent"]) > 0) {
  1722. $conversation_href = App::get_baseurl()."/display/".$owner["nick"]."/".$item["parent"];
  1723. $conversation_uri = $conversation_href;
  1724. if (isset($parent_item)) {
  1725. $r = dba::fetch_first("SELECT `conversation-uri`, `conversation-href` FROM `conversation` WHERE `item-uri` = ?", $parent_item);
  1726. if (dbm::is_result($r)) {
  1727. if ($r['conversation-uri'] != '') {
  1728. $conversation_uri = $r['conversation-uri'];
  1729. }
  1730. if ($r['conversation-href'] != '') {
  1731. $conversation_href = $r['conversation-href'];
  1732. }
  1733. }
  1734. }
  1735. xml::add_element($doc, $entry, "link", "", array("rel" => "ostatus:conversation", "href" => $conversation_href));
  1736. $attributes = array(
  1737. "href" => $conversation_href,
  1738. "local_id" => $item["parent"],
  1739. "ref" => $conversation_uri);
  1740. xml::add_element($doc, $entry, "ostatus:conversation", $conversation_uri, $attributes);
  1741. }
  1742. $tags = item_getfeedtags($item);
  1743. if(count($tags))
  1744. foreach($tags as $t)
  1745. if ($t[0] == "@")
  1746. $mentioned[$t[1]] = $t[1];
  1747. // Make sure that mentions are accepted (GNU Social has problems with mixing HTTP and HTTPS)
  1748. $newmentions = array();
  1749. foreach ($mentioned AS $mention) {
  1750. $newmentions[str_replace("http://", "https://", $mention)] = str_replace("http://", "https://", $mention);
  1751. $newmentions[str_replace("https://", "http://", $mention)] = str_replace("https://", "http://", $mention);
  1752. }
  1753. $mentioned = $newmentions;
  1754. foreach ($mentioned AS $mention) {
  1755. $r = q("SELECT `forum`, `prv` FROM `contact` WHERE `uid` = %d AND `nurl` = '%s'",
  1756. intval($owner["uid"]),
  1757. dbesc(normalise_link($mention)));
  1758. if ($r[0]["forum"] OR $r[0]["prv"])
  1759. xml::add_element($doc, $entry, "link", "", array("rel" => "mentioned",
  1760. "ostatus:object-type" => ACTIVITY_OBJ_GROUP,
  1761. "href" => $mention));
  1762. else
  1763. xml::add_element($doc, $entry, "link", "", array("rel" => "mentioned",
  1764. "ostatus:object-type" => ACTIVITY_OBJ_PERSON,
  1765. "href" => $mention));
  1766. }
  1767. if (!$item["private"]) {
  1768. xml::add_element($doc, $entry, "link", "", array("rel" => "ostatus:attention",
  1769. "href" => "http://activityschema.org/collection/public"));
  1770. xml::add_element($doc, $entry, "link", "", array("rel" => "mentioned",
  1771. "ostatus:object-type" => "http://activitystrea.ms/schema/1.0/collection",
  1772. "href" => "http://activityschema.org/collection/public"));
  1773. xml::add_element($doc, $entry, "mastodon:scope", "public");
  1774. }
  1775. if(count($tags))
  1776. foreach($tags as $t)
  1777. if ($t[0] != "@")
  1778. xml::add_element($doc, $entry, "category", "", array("term" => $t[2]));
  1779. self::get_attachment($doc, $entry, $item);
  1780. if ($complete AND ($item["id"] > 0)) {
  1781. $app = $item["app"];
  1782. if ($app == "")
  1783. $app = "web";
  1784. $attributes = array("local_id" => $item["id"], "source" => $app);
  1785. if (isset($parent["id"]))
  1786. $attributes["repeat_of"] = $parent["id"];
  1787. if ($item["coord"] != "")
  1788. xml::add_element($doc, $entry, "georss:point", $item["coord"]);
  1789. xml::add_element($doc, $entry, "statusnet:notice_info", "", $attributes);
  1790. }
  1791. }
  1792. /**
  1793. * @brief Creates the XML feed for a given nickname
  1794. *
  1795. * @param App $a The application class
  1796. * @param string $owner_nick Nickname of the feed owner
  1797. * @param string $last_update Date of the last update
  1798. *
  1799. * @return string XML feed
  1800. */
  1801. public static function feed(App $a, $owner_nick, $last_update) {
  1802. $r = q("SELECT `contact`.*, `user`.`nickname`, `user`.`timezone`, `user`.`page-flags`
  1803. FROM `contact` INNER JOIN `user` ON `user`.`uid` = `contact`.`uid`
  1804. WHERE `contact`.`self` AND `user`.`nickname` = '%s' LIMIT 1",
  1805. dbesc($owner_nick));
  1806. if (!$r)
  1807. return;
  1808. $owner = $r[0];
  1809. if(!strlen($last_update))
  1810. $last_update = 'now -30 days';
  1811. $check_date = datetime_convert('UTC','UTC',$last_update,'Y-m-d H:i:s');
  1812. $authorid = get_contact($owner["url"], 0);
  1813. $items = q("SELECT `item`.*, `item`.`id` AS `item_id` FROM `item` USE INDEX (`uid_contactid_created`)
  1814. STRAIGHT_JOIN `thread` ON `thread`.`iid` = `item`.`parent`
  1815. WHERE `item`.`uid` = %d AND `item`.`contact-id` = %d AND
  1816. `item`.`author-id` = %d AND `item`.`created` > '%s' AND
  1817. NOT `item`.`deleted` AND NOT `item`.`private` AND
  1818. `thread`.`network` IN ('%s', '%s')
  1819. ORDER BY `item`.`created` DESC LIMIT 300",
  1820. intval($owner["uid"]), intval($owner["id"]),
  1821. intval($authorid), dbesc($check_date),
  1822. dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN));
  1823. /* 2016-10-23: The old query will be kept until we are sure that the query above is a good and fast replacement
  1824. $items = q("SELECT `item`.*, `item`.`id` AS `item_id` FROM `item`
  1825. STRAIGHT_JOIN `thread` ON `thread`.`iid` = `item`.`parent`
  1826. LEFT JOIN `item` AS `thritem` ON `thritem`.`uri`=`item`.`thr-parent` AND `thritem`.`uid`=`item`.`uid`
  1827. WHERE `item`.`uid` = %d AND `item`.`received` > '%s' AND NOT `item`.`private` AND NOT `item`.`deleted`
  1828. AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = '' AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = ''
  1829. AND ((`item`.`wall` AND (`item`.`parent` = `item`.`id`))
  1830. OR (`item`.`network` = '%s' AND ((`thread`.`network` IN ('%s', '%s')) OR (`thritem`.`network` IN ('%s', '%s')))) AND `thread`.`mention`)
  1831. AND ((`item`.`owner-link` IN ('%s', '%s') AND (`item`.`parent` = `item`.`id`))
  1832. OR (`item`.`author-link` IN ('%s', '%s')))
  1833. ORDER BY `item`.`id` DESC
  1834. LIMIT 0, 300",
  1835. intval($owner["uid"]), dbesc($check_date), dbesc(NETWORK_DFRN),
  1836. //dbesc(NETWORK_OSTATUS), dbesc(NETWORK_OSTATUS),
  1837. //dbesc(NETWORK_OSTATUS), dbesc(NETWORK_OSTATUS),
  1838. dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN),
  1839. dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN),
  1840. dbesc($owner["nurl"]), dbesc(str_replace("http://", "https://", $owner["nurl"])),
  1841. dbesc($owner["nurl"]), dbesc(str_replace("http://", "https://", $owner["nurl"]))
  1842. );
  1843. */
  1844. $doc = new DOMDocument('1.0', 'utf-8');
  1845. $doc->formatOutput = true;
  1846. $root = self::add_header($doc, $owner);
  1847. foreach ($items AS $item) {
  1848. $entry = self::entry($doc, $item, $owner);
  1849. $root->appendChild($entry);
  1850. }
  1851. return(trim($doc->saveXML()));
  1852. }
  1853. /**
  1854. * @brief Creates the XML for a salmon message
  1855. *
  1856. * @param array $item Data of the item that is to be posted
  1857. * @param array $owner Contact data of the poster
  1858. *
  1859. * @return string XML for the salmon
  1860. */
  1861. public static function salmon($item,$owner) {
  1862. $doc = new DOMDocument('1.0', 'utf-8');
  1863. $doc->formatOutput = true;
  1864. $entry = self::entry($doc, $item, $owner, true);
  1865. $doc->appendChild($entry);
  1866. return(trim($doc->saveXML()));
  1867. }
  1868. }