/** * * consume_feed - process atom feed and update anything/everything we might need to update * * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds. * * $importer = the contact_record (joined to user_record) of the local user who owns this relationship. * It is this person's stuff that is going to be updated. * $contact = the person who is sending us stuff. If not set, we MAY be processing a "follow" activity * from an external network and MAY create an appropriate contact record. Otherwise, we MUST * have a contact record. * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or * might not) try and subscribe to it. * $datedir sorts in reverse order * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been * imported prior to its children being seen in the stream unless we are certain * of how the feed is arranged/ordered. * With $pass = 1, we only pull parent items out of the stream. * With $pass = 2, we only pull children (comments/likes). * * So running this twice, first with pass 1 and then with pass 2 will do the right * thing regardless of feed ordering. This won't be adequate in a fully-threaded * model where comments can have sub-threads. That would require some massive sorting * to get all the feed items into a mostly linear ordering, and might still require * recursion. */ function consume_feed($xml, $importer, &$contact, &$hub, $datedir = 0, $pass = 0) { if ($contact['network'] === NETWORK_OSTATUS) { if ($pass < 2) { // Test - remove before flight //$tempfile = tempnam(get_temppath(), "ostatus2"); //file_put_contents($tempfile, $xml); logger("Consume OStatus messages ", LOGGER_DEBUG); ostatus_import($xml, $importer, $contact, $hub); } return; } if ($contact['network'] === NETWORK_FEED) { if ($pass < 2) { logger("Consume feeds", LOGGER_DEBUG); feed_import($xml, $importer, $contact, $hub); } return; } require_once 'library/simplepie/simplepie.inc'; require_once 'include/contact_selectors.php'; if (!strlen($xml)) { logger('consume_feed: empty input'); return; } $feed = new SimplePie(); $feed->set_raw_data($xml); if ($datedir) { $feed->enable_order_by_date(true); } else { $feed->enable_order_by_date(false); } $feed->init(); if ($feed->error()) { logger('consume_feed: Error parsing XML: ' . $feed->error()); } $permalink = $feed->get_permalink(); // Check at the feed level for updated contact name and/or photo $name_updated = ''; $new_name = ''; $photo_timestamp = ''; $photo_url = ''; $birthday = ''; $contact_updated = ''; $hubs = $feed->get_links('hub'); logger('consume_feed: hubs: ' . print_r($hubs, true), LOGGER_DATA); if (count($hubs)) { $hub = implode(',', $hubs); } $rawtags = $feed->get_feed_tags(NAMESPACE_DFRN, 'owner'); if (!$rawtags) { $rawtags = $feed->get_feed_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'author'); } if ($rawtags) { $elems = $rawtags[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]; if ($elems['name'][0]['attribs'][NAMESPACE_DFRN]['updated']) { $name_updated = $elems['name'][0]['attribs'][NAMESPACE_DFRN]['updated']; $new_name = $elems['name'][0]['data']; // Manually checking for changed contact names if ($new_name != $contact['name'] and $new_name != "" and $name_updated <= $contact['name-date']) { $name_updated = date("c"); $photo_timestamp = date("c"); } } if (x($elems, 'link') && $elems['link'][0]['attribs']['']['rel'] === 'photo' && $elems['link'][0]['attribs'][NAMESPACE_DFRN]['updated']) { if ($photo_timestamp == "") { $photo_timestamp = datetime_convert('UTC', 'UTC', $elems['link'][0]['attribs'][NAMESPACE_DFRN]['updated']); } $photo_url = $elems['link'][0]['attribs']['']['href']; } if (x($rawtags[0]['child'], NAMESPACE_DFRN) && x($rawtags[0]['child'][NAMESPACE_DFRN], 'birthday')) { $birthday = datetime_convert('UTC', 'UTC', $rawtags[0]['child'][NAMESPACE_DFRN]['birthday'][0]['data']); } } if (is_array($contact) && $photo_timestamp && strlen($photo_url) && $photo_timestamp > $contact['avatar-date']) { logger('consume_feed: Updating photo for ' . $contact['name'] . ' from ' . $photo_url . ' uid: ' . $contact['uid']); $contact_updated = $photo_timestamp; require_once "include/Photo.php"; $photos = import_profile_photo($photo_url, $contact['uid'], $contact['id']); q("UPDATE `contact` SET `avatar-date` = '%s', `photo` = '%s', `thumb` = '%s', `micro` = '%s'\n\t\t\tWHERE `uid` = %d AND `id` = %d AND NOT `self`", dbesc(datetime_convert()), dbesc($photos[0]), dbesc($photos[1]), dbesc($photos[2]), intval($contact['uid']), intval($contact['id'])); } if (is_array($contact) && $name_updated && strlen($new_name) && $name_updated > $contact['name-date']) { if ($name_updated > $contact_updated) { $contact_updated = $name_updated; } $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `id` = %d LIMIT 1", intval($contact['uid']), intval($contact['id'])); $x = q("UPDATE `contact` SET `name` = '%s', `name-date` = '%s' WHERE `uid` = %d AND `id` = %d AND `name` != '%s' AND NOT `self`", dbesc(notags(trim($new_name))), dbesc(datetime_convert()), intval($contact['uid']), intval($contact['id']), dbesc(notags(trim($new_name)))); // do our best to update the name on content items if (count($r) and notags(trim($new_name)) != $r[0]['name']) { q("UPDATE `item` SET `author-name` = '%s' WHERE `author-name` = '%s' AND `author-link` = '%s' AND `uid` = %d AND `author-name` != '%s'", dbesc(notags(trim($new_name))), dbesc($r[0]['name']), dbesc($r[0]['url']), intval($contact['uid']), dbesc(notags(trim($new_name)))); } } if ($contact_updated and $new_name and $photo_url) { poco_check($contact['url'], $new_name, NETWORK_DFRN, $photo_url, "", "", "", "", "", $contact_updated, 2, $contact['id'], $contact['uid']); } if (strlen($birthday)) { if (substr($birthday, 0, 4) != $contact['bdyear']) { logger('consume_feed: updating birthday: ' . $birthday); /** * * Add new birthday event for this person * * $bdtext is just a readable placeholder in case the event is shared * with others. We will replace it during presentation to our $importer * to contain a sparkle link and perhaps a photo. * */ $bdtext = sprintf(t('%s\'s birthday'), $contact['name']); $bdtext2 = sprintf(t('Happy Birthday %s'), ' [url=' . $contact['url'] . ']' . $contact['name'] . '[/url]'); $r = q("INSERT INTO `event` (`uid`,`cid`,`created`,`edited`,`start`,`finish`,`summary`,`desc`,`type`)\n\t\t\t\tVALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) ", intval($contact['uid']), intval($contact['id']), dbesc(datetime_convert()), dbesc(datetime_convert()), dbesc(datetime_convert('UTC', 'UTC', $birthday)), dbesc(datetime_convert('UTC', 'UTC', $birthday . ' + 1 day ')), dbesc($bdtext), dbesc($bdtext2), dbesc('birthday')); // update bdyear q("UPDATE `contact` SET `bdyear` = '%s' WHERE `uid` = %d AND `id` = %d", dbesc(substr($birthday, 0, 4)), intval($contact['uid']), intval($contact['id'])); // This function is called twice without reloading the contact // Make sure we only create one event. This is why &$contact // is a reference var in this function $contact['bdyear'] = substr($birthday, 0, 4); } } $community_page = 0; $rawtags = $feed->get_feed_tags(NAMESPACE_DFRN, 'community'); if ($rawtags) { $community_page = intval($rawtags[0]['data']); } if (is_array($contact) && intval($contact['forum']) != $community_page) { q("update contact set forum = %d where id = %d", intval($community_page), intval($contact['id'])); $contact['forum'] = (string) $community_page; } // process any deleted entries $del_entries = $feed->get_feed_tags(NAMESPACE_TOMB, 'deleted-entry'); if (is_array($del_entries) && count($del_entries) && $pass != 2) { foreach ($del_entries as $dentry) { $deleted = false; if (isset($dentry['attribs']['']['ref'])) { $uri = $dentry['attribs']['']['ref']; $deleted = true; if (isset($dentry['attribs']['']['when'])) { $when = $dentry['attribs']['']['when']; $when = datetime_convert('UTC', 'UTC', $when, 'Y-m-d H:i:s'); } else { $when = datetime_convert('UTC', 'UTC', 'now', 'Y-m-d H:i:s'); } } if ($deleted && is_array($contact)) { $r = q("SELECT `item`.*, `contact`.`self` FROM `item` INNER JOIN `contact` on `item`.`contact-id` = `contact`.`id`\n\t\t\t\t\tWHERE `uri` = '%s' AND `item`.`uid` = %d AND `contact-id` = %d AND NOT `item`.`file` LIKE '%%[%%' LIMIT 1", dbesc($uri), intval($importer['uid']), intval($contact['id'])); if (count($r)) { $item = $r[0]; if (!$item['deleted']) { logger('consume_feed: deleting item ' . $item['id'] . ' uri=' . $item['uri'], LOGGER_DEBUG); } if ($item['object-type'] === ACTIVITY_OBJ_EVENT) { logger("Deleting event " . $item['event-id'], LOGGER_DEBUG); event_delete($item['event-id']); } if ($item['verb'] === ACTIVITY_TAG && $item['object-type'] === ACTIVITY_OBJ_TAGTERM) { $xo = parse_xml_string($item['object'], false); $xt = parse_xml_string($item['target'], false); if ($xt->type === ACTIVITY_OBJ_NOTE) { $i = q("select * from `item` where uri = '%s' and uid = %d limit 1", dbesc($xt->id), intval($importer['importer_uid'])); if (count($i)) { // For tags, the owner cannot remove the tag on the author's copy of the post. $owner_remove = $item['contact-id'] == $i[0]['contact-id'] ? true : false; $author_remove = $item['origin'] && $item['self'] ? true : false; $author_copy = $item['origin'] ? true : false; if ($owner_remove && $author_copy) { continue; } if ($author_remove || $owner_remove) { $tags = explode(',', $i[0]['tag']); $newtags = array(); if (count($tags)) { foreach ($tags as $tag) { if (trim($tag) !== trim($xo->body)) { $newtags[] = trim($tag); } } } q("update item set tag = '%s' where id = %d", dbesc(implode(',', $newtags)), intval($i[0]['id'])); create_tags_from_item($i[0]['id']); } } } } if ($item['uri'] == $item['parent-uri']) { $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s',\n\t\t\t\t\t\t\t`body` = '', `title` = ''\n\t\t\t\t\t\t\tWHERE `parent-uri` = '%s' AND `uid` = %d", dbesc($when), dbesc(datetime_convert()), dbesc($item['uri']), intval($importer['uid'])); create_tags_from_itemuri($item['uri'], $importer['uid']); create_files_from_itemuri($item['uri'], $importer['uid']); update_thread_uri($item['uri'], $importer['uid']); } else { $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s',\n\t\t\t\t\t\t\t`body` = '', `title` = ''\n\t\t\t\t\t\t\tWHERE `uri` = '%s' AND `uid` = %d", dbesc($when), dbesc(datetime_convert()), dbesc($uri), intval($importer['uid'])); create_tags_from_itemuri($uri, $importer['uid']); create_files_from_itemuri($uri, $importer['uid']); if ($item['last-child']) { // ensure that last-child is set in case the comment that had it just got wiped. q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ", dbesc(datetime_convert()), dbesc($item['parent-uri']), intval($item['uid'])); // who is the last child now? $r = q("SELECT `id` FROM `item` WHERE `parent-uri` = '%s' AND `type` != 'activity' AND `deleted` = 0 AND `moderated` = 0 AND `uid` = %d\n\t\t\t\t\t\t\t\tORDER BY `created` DESC LIMIT 1", dbesc($item['parent-uri']), intval($importer['uid'])); if (count($r)) { q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d", intval($r[0]['id'])); } } } } } } } // Now process the feed if ($feed->get_item_quantity()) { logger('consume_feed: feed item count = ' . $feed->get_item_quantity()); // in inverse date order if ($datedir) { $items = array_reverse($feed->get_items()); } else { $items = $feed->get_items(); } foreach ($items as $item) { $is_reply = false; $item_id = $item->get_id(); $rawthread = $item->get_item_tags(NAMESPACE_THREAD, 'in-reply-to'); if (isset($rawthread[0]['attribs']['']['ref'])) { $is_reply = true; $parent_uri = $rawthread[0]['attribs']['']['ref']; } if ($is_reply && is_array($contact)) { if ($pass == 1) { continue; } // not allowed to post if ($contact['rel'] == CONTACT_IS_FOLLOWER) { continue; } // Have we seen it? If not, import it. $item_id = $item->get_id(); $datarray = get_atom_elements($feed, $item, $contact); if (!x($datarray, 'author-name') && $contact['network'] != NETWORK_DFRN) { $datarray['author-name'] = $contact['name']; } if (!x($datarray, 'author-link') && $contact['network'] != NETWORK_DFRN) { $datarray['author-link'] = $contact['url']; } if (!x($datarray, 'author-avatar') && $contact['network'] != NETWORK_DFRN) { $datarray['author-avatar'] = $contact['thumb']; } if (!x($datarray, 'author-name') || !x($datarray, 'author-link')) { logger('consume_feed: no author information! ' . print_r($datarray, true)); continue; } $force_parent = false; if ($contact['network'] === NETWORK_OSTATUS || stristr($contact['url'], 'twitter.com')) { if ($contact['network'] === NETWORK_OSTATUS) { $force_parent = true; } if (strlen($datarray['title'])) { unset($datarray['title']); } $r = q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d", dbesc(datetime_convert()), dbesc($parent_uri), intval($importer['uid'])); $datarray['last-child'] = 1; update_thread_uri($parent_uri, $importer['uid']); } $r = q("SELECT `uid`, `last-child`, `edited`, `body` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($item_id), intval($importer['uid'])); // Update content if 'updated' changes if (count($r)) { if (edited_timestamp_is_newer($r[0], $datarray)) { // do not accept (ignore) an earlier edit than one we currently have. if (datetime_convert('UTC', 'UTC', $datarray['edited']) < $r[0]['edited']) { continue; } $r = q("UPDATE `item` SET `title` = '%s', `body` = '%s', `tag` = '%s', `edited` = '%s', `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d", dbesc($datarray['title']), dbesc($datarray['body']), dbesc($datarray['tag']), dbesc(datetime_convert('UTC', 'UTC', $datarray['edited'])), dbesc(datetime_convert()), dbesc($item_id), intval($importer['uid'])); create_tags_from_itemuri($item_id, $importer['uid']); update_thread_uri($item_id, $importer['uid']); } // update last-child if it changes $allow = $item->get_item_tags(NAMESPACE_DFRN, 'comment-allow'); if ($allow && $allow[0]['data'] != $r[0]['last-child']) { $r = q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d", dbesc(datetime_convert()), dbesc($parent_uri), intval($importer['uid'])); $r = q("UPDATE `item` SET `last-child` = %d , `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d", intval($allow[0]['data']), dbesc(datetime_convert()), dbesc($item_id), intval($importer['uid'])); update_thread_uri($item_id, $importer['uid']); } continue; } if ($contact['network'] === NETWORK_FEED || !strlen($contact['notify'])) { // one way feed - no remote comment ability $datarray['last-child'] = 0; } $datarray['parent-uri'] = $parent_uri; $datarray['uid'] = $importer['uid']; $datarray['contact-id'] = $contact['id']; if ($datarray['verb'] === ACTIVITY_LIKE || $datarray['verb'] === ACTIVITY_DISLIKE || $datarray['verb'] === ACTIVITY_ATTEND || $datarray['verb'] === ACTIVITY_ATTENDNO || $datarray['verb'] === ACTIVITY_ATTENDMAYBE) { $datarray['type'] = 'activity'; $datarray['gravity'] = GRAVITY_LIKE; // only one like or dislike per person // splitted into two queries for performance issues $r = q("select id from item where uid = %d and `contact-id` = %d and verb ='%s' and deleted = 0 and (`parent-uri` = '%s') limit 1", intval($datarray['uid']), intval($datarray['contact-id']), dbesc($datarray['verb']), dbesc($parent_uri)); if ($r && count($r)) { continue; } $r = q("select id from item where uid = %d and `contact-id` = %d and verb ='%s' and deleted = 0 and (`thr-parent` = '%s') limit 1", intval($datarray['uid']), intval($datarray['contact-id']), dbesc($datarray['verb']), dbesc($parent_uri)); if ($r && count($r)) { continue; } } if ($datarray['verb'] === ACTIVITY_TAG && $datarray['object-type'] === ACTIVITY_OBJ_TAGTERM) { $xo = parse_xml_string($datarray['object'], false); $xt = parse_xml_string($datarray['target'], false); if ($xt->type == ACTIVITY_OBJ_NOTE) { $r = q("select * from item where `uri` = '%s' AND `uid` = %d limit 1", dbesc($xt->id), intval($importer['importer_uid'])); if (!count($r)) { continue; } // extract tag, if not duplicate, add to parent item if ($xo->id && $xo->content) { $newtag = '#[url=' . $xo->id . ']' . $xo->content . '[/url]'; if (!stristr($r[0]['tag'], $newtag)) { q("UPDATE item SET tag = '%s' WHERE id = %d", dbesc($r[0]['tag'] . (strlen($r[0]['tag']) ? ',' : '') . $newtag), intval($r[0]['id'])); create_tags_from_item($r[0]['id']); } } } } $r = item_store($datarray, $force_parent); continue; } else { // Head post of a conversation. Have we seen it? If not, import it. $item_id = $item->get_id(); $datarray = get_atom_elements($feed, $item, $contact); if (is_array($contact)) { if (!x($datarray, 'author-name') && $contact['network'] != NETWORK_DFRN) { $datarray['author-name'] = $contact['name']; } if (!x($datarray, 'author-link') && $contact['network'] != NETWORK_DFRN) { $datarray['author-link'] = $contact['url']; } if (!x($datarray, 'author-avatar') && $contact['network'] != NETWORK_DFRN) { $datarray['author-avatar'] = $contact['thumb']; } } if (!x($datarray, 'author-name') || !x($datarray, 'author-link')) { logger('consume_feed: no author information! ' . print_r($datarray, true)); continue; } // special handling for events if (x($datarray, 'object-type') && $datarray['object-type'] === ACTIVITY_OBJ_EVENT) { $ev = bbtoevent($datarray['body']); if ((x($ev, 'desc') || x($ev, 'summary')) && x($ev, 'start')) { $ev['uid'] = $importer['uid']; $ev['uri'] = $item_id; $ev['edited'] = $datarray['edited']; $ev['private'] = $datarray['private']; $ev['guid'] = $datarray['guid']; if (is_array($contact)) { $ev['cid'] = $contact['id']; } $r = q("SELECT * FROM `event` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($item_id), intval($importer['uid'])); if (count($r)) { $ev['id'] = $r[0]['id']; } $xyz = event_store($ev); continue; } } if ($contact['network'] === NETWORK_OSTATUS || stristr($contact['url'], 'twitter.com')) { if (strlen($datarray['title'])) { unset($datarray['title']); } $datarray['last-child'] = 1; } $r = q("SELECT `uid`, `last-child`, `edited`, `body` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($item_id), intval($importer['uid'])); // Update content if 'updated' changes if (count($r)) { if (edited_timestamp_is_newer($r[0], $datarray)) { // do not accept (ignore) an earlier edit than one we currently have. if (datetime_convert('UTC', 'UTC', $datarray['edited']) < $r[0]['edited']) { continue; } $r = q("UPDATE `item` SET `title` = '%s', `body` = '%s', `tag` = '%s', `edited` = '%s', `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d", dbesc($datarray['title']), dbesc($datarray['body']), dbesc($datarray['tag']), dbesc(datetime_convert('UTC', 'UTC', $datarray['edited'])), dbesc(datetime_convert()), dbesc($item_id), intval($importer['uid'])); create_tags_from_itemuri($item_id, $importer['uid']); update_thread_uri($item_id, $importer['uid']); } // update last-child if it changes $allow = $item->get_item_tags(NAMESPACE_DFRN, 'comment-allow'); if ($allow && $allow[0]['data'] != $r[0]['last-child']) { $r = q("UPDATE `item` SET `last-child` = %d , `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d", intval($allow[0]['data']), dbesc(datetime_convert()), dbesc($item_id), intval($importer['uid'])); update_thread_uri($item_id, $importer['uid']); } continue; } if (activity_match($datarray['verb'], ACTIVITY_FOLLOW)) { logger('consume-feed: New follower'); new_follower($importer, $contact, $datarray, $item); return; } if (activity_match($datarray['verb'], ACTIVITY_UNFOLLOW)) { lose_follower($importer, $contact, $datarray, $item); return; } if (activity_match($datarray['verb'], ACTIVITY_REQ_FRIEND)) { logger('consume-feed: New friend request'); new_follower($importer, $contact, $datarray, $item, true); return; } if (activity_match($datarray['verb'], ACTIVITY_UNFRIEND)) { lose_sharer($importer, $contact, $datarray, $item); return; } if (!is_array($contact)) { return; } if ($contact['network'] === NETWORK_FEED || !strlen($contact['notify'])) { // one way feed - no remote comment ability $datarray['last-child'] = 0; } if ($contact['network'] === NETWORK_FEED) { $datarray['private'] = 2; } $datarray['parent-uri'] = $item_id; $datarray['uid'] = $importer['uid']; $datarray['contact-id'] = $contact['id']; if (!link_compare($datarray['owner-link'], $contact['url'])) { // The item owner info is not our contact. It's OK and is to be expected if this is a tgroup delivery, // but otherwise there's a possible data mixup on the sender's system. // the tgroup delivery code called from item_store will correct it if it's a forum, // but we're going to unconditionally correct it here so that the post will always be owned by our contact. logger('consume_feed: Correcting item owner.', LOGGER_DEBUG); $datarray['owner-name'] = $contact['name']; $datarray['owner-link'] = $contact['url']; $datarray['owner-avatar'] = $contact['thumb']; } // We've allowed "followers" to reach this point so we can decide if they are // posting an @-tag delivery, which followers are allowed to do for certain // page types. Now that we've parsed the post, let's check if it is legit. Otherwise ignore it. if ($contact['rel'] == CONTACT_IS_FOLLOWER && !tgroup_check($importer['uid'], $datarray)) { continue; } // This is my contact on another system, but it's really me. // Turn this into a wall post. $notify = item_is_remote_self($contact, $datarray); $r = item_store($datarray, false, $notify); logger('Stored - Contact ' . $contact['url'] . ' Notify ' . $notify . ' return ' . $r . ' Item ' . print_r($datarray, true), LOGGER_DEBUG); continue; } } } }
/** * * consume_feed - process atom feed and update anything/everything we might need to update * * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds. * * $importer = the contact_record (joined to user_record) of the local user who owns this relationship. * It is this person's stuff that is going to be updated. * $contact = the person who is sending us stuff. If not set, we MAY be processing a "follow" activity * from an external network and MAY create an appropriate contact record. Otherwise, we MUST * have a contact record. * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or * might not) try and subscribe to it. * $datedir sorts in reverse order * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been * imported prior to its children being seen in the stream unless we are certain * of how the feed is arranged/ordered. * With $pass = 1, we only pull parent items out of the stream. * With $pass = 2, we only pull children (comments/likes). * * So running this twice, first with pass 1 and then with pass 2 will do the right * thing regardless of feed ordering. This won't be adequate in a fully-threaded * model where comments can have sub-threads. That would require some massive sorting * to get all the feed items into a mostly linear ordering, and might still require * recursion. */ function consume_feed($xml, $importer, &$contact, &$hub, $datedir = 0, $pass = 0) { require_once 'library/simplepie/simplepie.inc'; if (!strlen($xml)) { logger('consume_feed: empty input'); return; } $feed = new SimplePie(); $feed->set_raw_data($xml); if ($datedir) { $feed->enable_order_by_date(true); } else { $feed->enable_order_by_date(false); } $feed->init(); if ($feed->error()) { logger('consume_feed: Error parsing XML: ' . $feed->error()); } $permalink = $feed->get_permalink(); // Check at the feed level for updated contact name and/or photo $name_updated = ''; $new_name = ''; $photo_timestamp = ''; $photo_url = ''; $birthday = ''; $hubs = $feed->get_links('hub'); if (count($hubs)) { $hub = implode(',', $hubs); } $rawtags = $feed->get_feed_tags(NAMESPACE_DFRN, 'owner'); if (!$rawtags) { $rawtags = $feed->get_feed_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'author'); } if ($rawtags) { $elems = $rawtags[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]; if ($elems['name'][0]['attribs'][NAMESPACE_DFRN]['updated']) { $name_updated = $elems['name'][0]['attribs'][NAMESPACE_DFRN]['updated']; $new_name = $elems['name'][0]['data']; } if (x($elems, 'link') && $elems['link'][0]['attribs']['']['rel'] === 'photo' && $elems['link'][0]['attribs'][NAMESPACE_DFRN]['updated']) { $photo_timestamp = datetime_convert('UTC', 'UTC', $elems['link'][0]['attribs'][NAMESPACE_DFRN]['updated']); $photo_url = $elems['link'][0]['attribs']['']['href']; } if (x($rawtags[0]['child'], NAMESPACE_DFRN) && x($rawtags[0]['child'][NAMESPACE_DFRN], 'birthday')) { $birthday = datetime_convert('UTC', 'UTC', $rawtags[0]['child'][NAMESPACE_DFRN]['birthday'][0]['data']); } } if (is_array($contact) && $photo_timestamp && strlen($photo_url) && $photo_timestamp > $contact['avatar-date']) { logger('consume_feed: Updating photo for ' . $contact['name']); require_once "Photo.php"; $photo_failure = false; $have_photo = false; $r = q("SELECT `resource-id` FROM `photo` WHERE `contact-id` = %d AND `uid` = %d LIMIT 1", intval($contact['id']), intval($contact['uid'])); if (count($r)) { $resource_id = $r[0]['resource-id']; $have_photo = true; } else { $resource_id = photo_new_resource(); } $img_str = fetch_url($photo_url, true); $img = new Photo($img_str); if ($img->is_valid()) { if ($have_photo) { q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `contact-id` = %d AND `uid` = %d", dbesc($resource_id), intval($contact['id']), intval($contact['uid'])); } $img->scaleImageSquare(175); $hash = $resource_id; $r = $img->store($contact['uid'], $contact['id'], $hash, basename($photo_url), 'Contact Photos', 4); $img->scaleImage(80); $r = $img->store($contact['uid'], $contact['id'], $hash, basename($photo_url), 'Contact Photos', 5); $img->scaleImage(48); $r = $img->store($contact['uid'], $contact['id'], $hash, basename($photo_url), 'Contact Photos', 6); $a = get_app(); q("UPDATE `contact` SET `avatar-date` = '%s', `photo` = '%s', `thumb` = '%s', `micro` = '%s' \n\t\t\t\tWHERE `uid` = %d AND `id` = %d LIMIT 1", dbesc(datetime_convert()), dbesc($a->get_baseurl() . '/photo/' . $hash . '-4.jpg'), dbesc($a->get_baseurl() . '/photo/' . $hash . '-5.jpg'), dbesc($a->get_baseurl() . '/photo/' . $hash . '-6.jpg'), intval($contact['uid']), intval($contact['id'])); } } if (is_array($contact) && $name_updated && strlen($new_name) && $name_updated > $contact['name-date']) { $r = q("select * from contact where uid = %d and id = %d limit 1", intval($contact['uid']), intval($contact['id'])); $x = q("UPDATE `contact` SET `name` = '%s', `name-date` = '%s' WHERE `uid` = %d AND `id` = %d LIMIT 1", dbesc(notags(trim($new_name))), dbesc(datetime_convert()), intval($contact['uid']), intval($contact['id'])); // do our best to update the name on content items if (count($r)) { q("update item set `author-name` = '%s' where `author-name` = '%s' and `author-link` = '%s' and uid = %d", dbesc(notags(trim($new_name))), dbesc($r[0]['name']), dbesc($r[0]['url']), intval($contact['uid'])); } } if (strlen($birthday)) { if (substr($birthday, 0, 4) != $contact['bdyear']) { logger('consume_feed: updating birthday: ' . $birthday); /** * * Add new birthday event for this person * * $bdtext is just a readable placeholder in case the event is shared * with others. We will replace it during presentation to our $importer * to contain a sparkle link and perhaps a photo. * */ $bdtext = t('Birthday:') . ' [url=' . $contact['url'] . ']' . $contact['name'] . '[/url]'; $r = q("INSERT INTO `event` (`uid`,`cid`,`created`,`edited`,`start`,`finish`,`desc`,`type`)\n\t\t\t\tVALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s' ) ", intval($contact['uid']), intval($contact['id']), dbesc(datetime_convert()), dbesc(datetime_convert()), dbesc(datetime_convert('UTC', 'UTC', $birthday)), dbesc(datetime_convert('UTC', 'UTC', $birthday . ' + 1 day ')), dbesc($bdtext), dbesc('birthday')); // update bdyear q("UPDATE `contact` SET `bdyear` = '%s' WHERE `uid` = %d AND `id` = %d LIMIT 1", dbesc(substr($birthday, 0, 4)), intval($contact['uid']), intval($contact['id'])); // This function is called twice without reloading the contact // Make sure we only create one event. This is why &$contact // is a reference var in this function $contact['bdyear'] = substr($birthday, 0, 4); } } $community_page = 0; $rawtags = $feed->get_feed_tags(NAMESPACE_DFRN, 'community'); if ($rawtags) { $community_page = intval($rawtags[0]['data']); } if (is_array($contact) && intval($contact['forum']) != $community_page) { q("update contact set forum = %d where id = %d limit 1", intval($community_page), intval($contact['id'])); $contact['forum'] = (string) $community_page; } // process any deleted entries $del_entries = $feed->get_feed_tags(NAMESPACE_TOMB, 'deleted-entry'); if (is_array($del_entries) && count($del_entries) && $pass != 2) { foreach ($del_entries as $dentry) { $deleted = false; if (isset($dentry['attribs']['']['ref'])) { $uri = $dentry['attribs']['']['ref']; $deleted = true; if (isset($dentry['attribs']['']['when'])) { $when = $dentry['attribs']['']['when']; $when = datetime_convert('UTC', 'UTC', $when, 'Y-m-d H:i:s'); } else { $when = datetime_convert('UTC', 'UTC', 'now', 'Y-m-d H:i:s'); } } if ($deleted && is_array($contact)) { $r = q("SELECT `item`.*, `contact`.`self` FROM `item` left join `contact` on `item`.`contact-id` = `contact`.`id` \n\t\t\t\t\tWHERE `uri` = '%s' AND `item`.`uid` = %d AND `contact-id` = %d AND NOT `item`.`file` LIKE '%%[%%' LIMIT 1", dbesc($uri), intval($importer['uid']), intval($contact['id'])); if (count($r)) { $item = $r[0]; if (!$item['deleted']) { logger('consume_feed: deleting item ' . $item['id'] . ' uri=' . $item['uri'], LOGGER_DEBUG); } if ($item['verb'] === ACTIVITY_TAG && $item['object-type'] === ACTIVITY_OBJ_TAGTERM) { $xo = parse_xml_string($item['object'], false); $xt = parse_xml_string($item['target'], false); if ($xt->type === ACTIVITY_OBJ_NOTE) { $i = q("select * from `item` where uri = '%s' and uid = %d limit 1", dbesc($xt->id), intval($importer['importer_uid'])); if (count($i)) { // For tags, the owner cannot remove the tag on the author's copy of the post. $owner_remove = $item['contact-id'] == $i[0]['contact-id'] ? true : false; $author_remove = $item['origin'] && $item['self'] ? true : false; $author_copy = $item['origin'] ? true : false; if ($owner_remove && $author_copy) { continue; } if ($author_remove || $owner_remove) { $tags = explode(',', $i[0]['tag']); $newtags = array(); if (count($tags)) { foreach ($tags as $tag) { if (trim($tag) !== trim($xo->body)) { $newtags[] = trim($tag); } } } q("update item set tag = '%s' where id = %d limit 1", dbesc(implode(',', $newtags)), intval($i[0]['id'])); } } } } if ($item['uri'] == $item['parent-uri']) { $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s',\n\t\t\t\t\t\t\t`body` = '', `title` = ''\n\t\t\t\t\t\t\tWHERE `parent-uri` = '%s' AND `uid` = %d", dbesc($when), dbesc(datetime_convert()), dbesc($item['uri']), intval($importer['uid'])); } else { $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s',\n\t\t\t\t\t\t\t`body` = '', `title` = '' \n\t\t\t\t\t\t\tWHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($when), dbesc(datetime_convert()), dbesc($uri), intval($importer['uid'])); if ($item['last-child']) { // ensure that last-child is set in case the comment that had it just got wiped. q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ", dbesc(datetime_convert()), dbesc($item['parent-uri']), intval($item['uid'])); // who is the last child now? $r = q("SELECT `id` FROM `item` WHERE `parent-uri` = '%s' AND `type` != 'activity' AND `deleted` = 0 AND `moderated` = 0 AND `uid` = %d \n\t\t\t\t\t\t\t\tORDER BY `created` DESC LIMIT 1", dbesc($item['parent-uri']), intval($importer['uid'])); if (count($r)) { q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d LIMIT 1", intval($r[0]['id'])); } } } } } } } // Now process the feed if ($feed->get_item_quantity()) { logger('consume_feed: feed item count = ' . $feed->get_item_quantity()); // in inverse date order if ($datedir) { $items = array_reverse($feed->get_items()); } else { $items = $feed->get_items(); } foreach ($items as $item) { $is_reply = false; $item_id = $item->get_id(); $rawthread = $item->get_item_tags(NAMESPACE_THREAD, 'in-reply-to'); if (isset($rawthread[0]['attribs']['']['ref'])) { $is_reply = true; $parent_uri = $rawthread[0]['attribs']['']['ref']; } if ($is_reply && is_array($contact)) { if ($pass == 1) { continue; } // Have we seen it? If not, import it. $item_id = $item->get_id(); $datarray = get_atom_elements($feed, $item); if (!x($datarray, 'author-name') && $contact['network'] != NETWORK_DFRN) { $datarray['author-name'] = $contact['name']; } if (!x($datarray, 'author-link') && $contact['network'] != NETWORK_DFRN) { $datarray['author-link'] = $contact['url']; } if (!x($datarray, 'author-avatar') && $contact['network'] != NETWORK_DFRN) { $datarray['author-avatar'] = $contact['thumb']; } if (!x($datarray, 'author-name') || !x($datarray, 'author-link')) { logger('consume_feed: no author information! ' . print_r($datarray, true)); continue; } $r = q("SELECT `uid`, `last-child`, `edited`, `body` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($item_id), intval($importer['uid'])); // Update content if 'updated' changes if (count($r)) { if (x($datarray, 'edited') !== false && datetime_convert('UTC', 'UTC', $datarray['edited']) !== $r[0]['edited']) { $r = q("UPDATE `item` SET `title` = '%s', `body` = '%s', `tag` = '%s', `edited` = '%s' WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($datarray['title']), dbesc($datarray['body']), dbesc($datarray['tag']), dbesc(datetime_convert('UTC', 'UTC', $datarray['edited'])), dbesc($item_id), intval($importer['uid'])); } // update last-child if it changes $allow = $item->get_item_tags(NAMESPACE_DFRN, 'comment-allow'); if ($allow && $allow[0]['data'] != $r[0]['last-child']) { $r = q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d", dbesc(datetime_convert()), dbesc($parent_uri), intval($importer['uid'])); $r = q("UPDATE `item` SET `last-child` = %d , `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", intval($allow[0]['data']), dbesc(datetime_convert()), dbesc($item_id), intval($importer['uid'])); } continue; } $force_parent = false; if ($contact['network'] === NETWORK_OSTATUS || stristr($contact['url'], 'twitter.com')) { if ($contact['network'] === NETWORK_OSTATUS) { $force_parent = true; } if (strlen($datarray['title'])) { unset($datarray['title']); } $r = q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d", dbesc(datetime_convert()), dbesc($parent_uri), intval($importer['uid'])); $datarray['last-child'] = 1; } if ($contact['network'] === NETWORK_FEED || !strlen($contact['notify'])) { // one way feed - no remote comment ability $datarray['last-child'] = 0; } $datarray['parent-uri'] = $parent_uri; $datarray['uid'] = $importer['uid']; $datarray['contact-id'] = $contact['id']; if (activity_match($datarray['verb'], ACTIVITY_LIKE) || activity_match($datarray['verb'], ACTIVITY_DISLIKE)) { $datarray['type'] = 'activity'; $datarray['gravity'] = GRAVITY_LIKE; // only one like or dislike per person $r = q("select id from item where uid = %d and `contact-id` = %d and verb ='%s' and deleted = 0 limit 1", intval($datarray['uid']), intval($datarray['contact-id']), dbesc($datarray['verb'])); if ($r && count($r)) { continue; } } if ($datarray['verb'] === ACTIVITY_TAG && $datarray['object-type'] === ACTIVITY_OBJ_TAGTERM) { $xo = parse_xml_string($datarray['object'], false); $xt = parse_xml_string($datarray['target'], false); if ($xt->type == ACTIVITY_OBJ_NOTE) { $r = q("select * from item where `uri` = '%s' AND `uid` = %d limit 1", dbesc($xt->id), intval($importer['importer_uid'])); if (!count($r)) { continue; } // extract tag, if not duplicate, add to parent item if ($xo->id && $xo->content) { $newtag = '#[url=' . $xo->id . ']' . $xo->content . '[/url]'; if (!stristr($r[0]['tag'], $newtag)) { q("UPDATE item SET tag = '%s' WHERE id = %d LIMIT 1", dbesc($r[0]['tag'] . (strlen($r[0]['tag']) ? ',' : '') . $newtag), intval($r[0]['id'])); } } } } $r = item_store($datarray, $force_parent); continue; } else { // Head post of a conversation. Have we seen it? If not, import it. $item_id = $item->get_id(); $datarray = get_atom_elements($feed, $item); if (is_array($contact)) { if (!x($datarray, 'author-name') && $contact['network'] != NETWORK_DFRN) { $datarray['author-name'] = $contact['name']; } if (!x($datarray, 'author-link') && $contact['network'] != NETWORK_DFRN) { $datarray['author-link'] = $contact['url']; } if (!x($datarray, 'author-avatar') && $contact['network'] != NETWORK_DFRN) { $datarray['author-avatar'] = $contact['thumb']; } } if (!x($datarray, 'author-name') || !x($datarray, 'author-link')) { logger('consume_feed: no author information! ' . print_r($datarray, true)); continue; } // special handling for events if (x($datarray, 'object-type') && $datarray['object-type'] === ACTIVITY_OBJ_EVENT) { $ev = bbtoevent($datarray['body']); if (x($ev, 'desc') && x($ev, 'start')) { $ev['uid'] = $importer['uid']; $ev['uri'] = $item_id; $ev['edited'] = $datarray['edited']; $ev['private'] = $datarray['private']; if (is_array($contact)) { $ev['cid'] = $contact['id']; } $r = q("SELECT * FROM `event` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($item_id), intval($importer['uid'])); if (count($r)) { $ev['id'] = $r[0]['id']; } $xyz = event_store($ev); continue; } } $r = q("SELECT `uid`, `last-child`, `edited`, `body` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($item_id), intval($importer['uid'])); // Update content if 'updated' changes if (count($r)) { if (x($datarray, 'edited') !== false && datetime_convert('UTC', 'UTC', $datarray['edited']) !== $r[0]['edited']) { $r = q("UPDATE `item` SET `title` = '%s', `body` = '%s', `tag` = '%s', `edited` = '%s' WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($datarray['title']), dbesc($datarray['body']), dbesc($datarray['tag']), dbesc(datetime_convert('UTC', 'UTC', $datarray['edited'])), dbesc($item_id), intval($importer['uid'])); } // update last-child if it changes $allow = $item->get_item_tags(NAMESPACE_DFRN, 'comment-allow'); if ($allow && $allow[0]['data'] != $r[0]['last-child']) { $r = q("UPDATE `item` SET `last-child` = %d , `changed` = '%s' WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", intval($allow[0]['data']), dbesc(datetime_convert()), dbesc($item_id), intval($importer['uid'])); } continue; } if (activity_match($datarray['verb'], ACTIVITY_FOLLOW)) { logger('consume-feed: New follower'); new_follower($importer, $contact, $datarray, $item); return; } if (activity_match($datarray['verb'], ACTIVITY_UNFOLLOW)) { lose_follower($importer, $contact, $datarray, $item); return; } if (activity_match($datarray['verb'], ACTIVITY_REQ_FRIEND)) { logger('consume-feed: New friend request'); new_follower($importer, $contact, $datarray, $item, true); return; } if (activity_match($datarray['verb'], ACTIVITY_UNFRIEND)) { lose_sharer($importer, $contact, $datarray, $item); return; } if (!is_array($contact)) { return; } if ($contact['network'] === NETWORK_OSTATUS || stristr($contact['url'], 'twitter.com')) { if (strlen($datarray['title'])) { unset($datarray['title']); } $datarray['last-child'] = 1; } if ($contact['network'] === NETWORK_FEED || !strlen($contact['notify'])) { // one way feed - no remote comment ability $datarray['last-child'] = 0; } // This is my contact on another system, but it's really me. // Turn this into a wall post. if ($contact['remote_self']) { $datarray['wall'] = 1; } $datarray['parent-uri'] = $item_id; $datarray['uid'] = $importer['uid']; $datarray['contact-id'] = $contact['id']; if (!link_compare($datarray['owner-link'], $contact['url'])) { // The item owner info is not our contact. It's OK and is to be expected if this is a tgroup delivery, // but otherwise there's a possible data mixup on the sender's system. // the tgroup delivery code called from item_store will correct it if it's a forum, // but we're going to unconditionally correct it here so that the post will always be owned by our contact. logger('consume_feed: Correcting item owner.', LOGGER_DEBUG); $datarray['owner-name'] = $contact['name']; $datarray['owner-link'] = $contact['url']; $datarray['owner-avatar'] = $contact['thumb']; } $r = item_store($datarray); continue; } } } }