From 6a8ebc8639507404be07bb7d6fd5a37189e46563 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 15 Sep 2018 18:54:45 +0000 Subject: [PATCH] Contact follow and unfollow workd partially --- mod/dfrn_confirm.php | 40 +++----- mod/follow.php | 3 +- src/Model/Contact.php | 43 +++++---- src/Model/Item.php | 67 +++++++++++++ src/Protocol/ActivityPub.php | 177 +++++++++++++++++++++++++++++++---- 5 files changed, 268 insertions(+), 62 deletions(-) diff --git a/mod/dfrn_confirm.php b/mod/dfrn_confirm.php index 41b5e0ef54..2a872dd414 100644 --- a/mod/dfrn_confirm.php +++ b/mod/dfrn_confirm.php @@ -28,6 +28,7 @@ use Friendica\Model\Group; use Friendica\Model\User; use Friendica\Network\Probe; use Friendica\Protocol\Diaspora; +use Friendica\Protocol\ActivityPub; use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; use Friendica\Util\Network; @@ -335,10 +336,17 @@ function dfrn_confirm_post(App $a, $handsfree = null) intval($contact_id) ); } else { + if ($network == Protocol::ACTIVITYPUB) { + ActivityPub::transmitContactActivity('Accept', $contact['url'], $contact['hub-verify'], $uid); + $pending = true; + } else { + $pending = false; + } + // $network !== Protocol::DFRN $network = defaults($contact, 'network', Protocol::OSTATUS); - $arr = Probe::uri($contact['url']); + $arr = Probe::uri($contact['url'], $network); $notify = defaults($contact, 'notify' , $arr['notify']); $poll = defaults($contact, 'poll' , $arr['poll']); @@ -362,30 +370,12 @@ function dfrn_confirm_post(App $a, $handsfree = null) DBA::delete('intro', ['id' => $intro_id]); - $r = q("UPDATE `contact` SET `name-date` = '%s', - `uri-date` = '%s', - `addr` = '%s', - `notify` = '%s', - `poll` = '%s', - `blocked` = 0, - `pending` = 0, - `network` = '%s', - `writable` = %d, - `hidden` = %d, - `rel` = %d - WHERE `id` = %d - ", - DBA::escape(DateTimeFormat::utcNow()), - DBA::escape(DateTimeFormat::utcNow()), - DBA::escape($addr), - DBA::escape($notify), - DBA::escape($poll), - DBA::escape($network), - intval($writable), - intval($hidden), - intval($new_relation), - intval($contact_id) - ); + $fields = ['name-date' => DateTimeFormat::utcNow(), + 'uri-date' => DateTimeFormat::utcNow(), 'addr' => $addr, + 'notify' => $notify, 'poll' => $poll, 'blocked' => false, + 'pending' => $pending, 'network' => $network, + 'writable' => $writable, 'hidden' => $hidden, 'rel' => $new_relation]; + DBA::update('contact', $fields, ['id' => $contact_id]); } if (!DBA::isResult($r)) { diff --git a/mod/follow.php b/mod/follow.php index 627ab52033..65028a70e0 100644 --- a/mod/follow.php +++ b/mod/follow.php @@ -31,7 +31,8 @@ function follow_post(App $a) // This is just a precaution if maybe this page is called somewhere directly via POST $_SESSION['fastlane'] = $url; - $result = Contact::createFromProbe($uid, $url, true); + $result = Contact::createFromProbe($uid, $url, true, Protocol::ACTIVITYPUB); +// $result = Contact::createFromProbe($uid, $url, true); if ($result['success'] == false) { if ($result['message']) { diff --git a/src/Model/Contact.php b/src/Model/Contact.php index ab804ab7f1..2bcb86327b 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -16,6 +16,7 @@ use Friendica\Database\DBA; use Friendica\Model\Profile; use Friendica\Network\Probe; use Friendica\Object\Image; +use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Diaspora; use Friendica\Protocol\DFRN; use Friendica\Protocol\OStatus; @@ -555,6 +556,8 @@ class Contact extends BaseObject } } elseif ($contact['network'] == Protocol::DIASPORA) { Diaspora::sendUnshare($user, $contact); + } elseif ($contact['network'] == Protocol::ACTIVITYPUB) { + ActivityPub::transmitContactActivity('Undo', $contact['url'], '', $user['uid']); } } @@ -1054,7 +1057,6 @@ class Contact extends BaseObject if (!x($contact, 'avatar')) { $update_contact = true; } - if (!$update_contact || $no_update) { return $contact_id; } @@ -1495,10 +1497,11 @@ class Contact extends BaseObject } /** - * @param integer $id contact id + * @param integer $id contact id + * @param string $network Optional network we are probing for * @return boolean */ - public static function updateFromProbe($id) + public static function updateFromProbe($id, $network = '') { /* Warning: Never ever fetch the public key via Probe::uri and write it into the contacts. @@ -1511,10 +1514,10 @@ class Contact extends BaseObject return false; } - $ret = Probe::uri($contact["url"]); + $ret = Probe::uri($contact["url"], $network); // If Probe::uri fails the network code will be different - if ($ret["network"] != $contact["network"]) { + if (($ret["network"] != $contact["network"]) && ($ret["network"] != $network)) { return false; } @@ -1537,14 +1540,15 @@ class Contact extends BaseObject DBA::update( 'contact', [ - 'url' => $ret['url'], - 'nurl' => normalise_link($ret['url']), - 'addr' => $ret['addr'], - 'alias' => $ret['alias'], - 'batch' => $ret['batch'], - 'notify' => $ret['notify'], - 'poll' => $ret['poll'], - 'poco' => $ret['poco'] + 'url' => $ret['url'], + 'nurl' => normalise_link($ret['url']), + 'network' => $ret['network'], + 'addr' => $ret['addr'], + 'alias' => $ret['alias'], + 'batch' => $ret['batch'], + 'notify' => $ret['notify'], + 'poll' => $ret['poll'], + 'poco' => $ret['poco'] ], ['id' => $id] ); @@ -1686,7 +1690,7 @@ class Contact extends BaseObject $hidden = (($ret['network'] === Protocol::MAIL) ? 1 : 0); - if (in_array($ret['network'], [Protocol::MAIL, Protocol::DIASPORA])) { + if (in_array($ret['network'], [Protocol::MAIL, Protocol::DIASPORA, Protocol::ACTIVITYPUB])) { $writeable = 1; } @@ -1766,6 +1770,9 @@ class Contact extends BaseObject } elseif ($contact['network'] == Protocol::DIASPORA) { $ret = Diaspora::sendShare($a->user, $contact); logger('share returns: ' . $ret); + } elseif ($contact['network'] == Protocol::ACTIVITYPUB) { + $ret = ActivityPub::transmitActivity('Follow', $contact['url'], $uid); + logger('Follow returns: ' . $ret); } } @@ -1814,7 +1821,7 @@ class Contact extends BaseObject return $contact; } - public static function addRelationship($importer, $contact, $datarray, $item, $sharing = false) { + public static function addRelationship($importer, $contact, $datarray, $item = '', $sharing = false) { // Should always be set if (empty($datarray['author-id'])) { return; @@ -1839,13 +1846,17 @@ class Contact extends BaseObject DBA::update('contact', ['rel' => self::FRIEND, 'writable' => true], ['id' => $contact['id'], 'uid' => $importer['uid']]); } + + if ($contact['network'] == Protocol::ACTIVITYPUB) { + ActivityPub::transmitContactActivity('Accept', $contact['url'], $contact['hub-verify'], $importer['uid']); + } + // send email notification to owner? } else { if (DBA::exists('contact', ['nurl' => normalise_link($url), 'uid' => $importer['uid'], 'pending' => true])) { logger('ignoring duplicated connection request from pending contact ' . $url); return; } - // create contact record q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`, `blocked`, `readonly`, `pending`, `writable`) diff --git a/src/Model/Item.php b/src/Model/Item.php index 69a3783bbd..68ebc690e8 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -2071,6 +2071,47 @@ class Item extends BaseObject $users = []; + $owner = DBA::selectFirst('contact', ['url', 'nurl', 'alias'], ['id' => $parent['owner-id']]); + if (!DBA::isResult($owner)) { + return; + } + + $condition = ['nurl' => $owner['nurl'], 'rel' => [Contact::SHARING, Contact::FRIEND]]; + $contacts = DBA::select('contact', ['uid'], $condition); + while ($contact = DBA::fetch($contacts)) { + if ($contact['uid'] == 0) { + continue; + } + + $users[$contact['uid']] = $contact['uid']; + } + DBA::close($contacts); + + $condition = ['alias' => $owner['url'], 'rel' => [Contact::SHARING, Contact::FRIEND]]; + $contacts = DBA::select('contact', ['uid'], $condition); + while ($contact = DBA::fetch($contacts)) { + if ($contact['uid'] == 0) { + continue; + } + + $users[$contact['uid']] = $contact['uid']; + } + DBA::close($contacts); + + if (!empty($owner['alias'])) { + $condition = ['url' => $owner['alias'], 'rel' => [Contact::SHARING, Contact::FRIEND]]; + $contacts = DBA::select('contact', ['uid'], $condition); + while ($contact = DBA::fetch($contacts)) { + if ($contact['uid'] == 0) { + continue; + } + + $users[$contact['uid']] = $contact['uid']; + } + DBA::close($contacts); + } +/* + $condition = ["`nurl` IN (SELECT `nurl` FROM `contact` WHERE `id` = ?) AND `uid` != 0 AND NOT `blocked` AND `rel` IN (?, ?)", $parent['owner-id'], Contact::SHARING, Contact::FRIEND]; @@ -2080,6 +2121,32 @@ class Item extends BaseObject $users[$contact['uid']] = $contact['uid']; } + DBA::close($contacts); + + // And the same with the alias in the user contacts + $condition = ["`alias` IN (SELECT `url` FROM `contact` WHERE `id` = ?) AND `uid` != 0 AND NOT `blocked` AND `rel` IN (?, ?)", + $parent['owner-id'], Contact::SHARING, Contact::FRIEND]; + + $contacts = DBA::select('contact', ['uid'], $condition); + + while ($contact = DBA::fetch($contacts)) { + $users[$contact['uid']] = $contact['uid']; + } + + DBA::close($contacts); + + // And vice versa + $condition = ["`url` IN (SELECT `alias` FROM `contact` WHERE `id` = ?) AND `uid` != 0 AND NOT `blocked` AND `rel` IN (?, ?)", + $parent['owner-id'], Contact::SHARING, Contact::FRIEND]; + + $contacts = DBA::select('contact', ['uid'], $condition); + + while ($contact = DBA::fetch($contacts)) { + $users[$contact['uid']] = $contact['uid']; + } + + DBA::close($contacts); +*/ $origin_uid = 0; if ($item['uri'] != $item['parent-uri']) { diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index e80568487b..78d5d44daf 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -33,14 +33,17 @@ use Friendica\Network\Probe; * Digest: https://tools.ietf.org/html/rfc5843 * https://tools.ietf.org/html/draft-cavage-http-signatures-10#ref-15 * + * Part of the code for HTTP signing is taken from the Osada project. + * + * * To-do: * * Receiver: - * - Activities: Follow, Accept, Update + * - Activities: Undo, Update * - Object Types: Person, Tombstome * * Transmitter: - * - Activities: Like, Dislike, Follow, Accept, Update + * - Activities: Like, Dislike, Update, Undo * - Object Tyoes: Article, Announce, Person, Tombstone * * General: @@ -77,7 +80,7 @@ class ActivityPub Network::post($target, $content, $headers); $return_code = BaseObject::getApp()->get_curl_code(); - echo $return_code."\n"; + logger('Transmit to ' . $target . ' returned ' . $return_code); } /** @@ -206,6 +209,28 @@ class ActivityPub 'actor' => $owner['url'], 'object' => $profile['url']]; + logger('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid, LOGGER_DEBUG); + return self::transmit($data, $profile['notify'], $uid); + } + + public static function transmitContactActivity($activity, $target, $id, $uid) + { + $profile = Probe::uri($target, Protocol::ACTIVITYPUB); + + if (empty($id)) { + $id = 'https://pirati.ca/activity/' . System::createGUID(); + } + + $owner = User::getOwnerDataById($uid); + $data = ['@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://pirati.ca/activity/' . System::createGUID(), + 'type' => $activity, + 'actor' => $owner['url'], + 'object' => ['id' => $id, 'type' => 'Follow', + 'actor' => $owner['url'], + 'object' => $profile['url']]]; + + logger('Sending ' . $activity . ' to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); return self::transmit($data, $profile['notify'], $uid); } @@ -604,14 +629,24 @@ class ActivityPub logger('Receivers: ' . json_encode($receivers), LOGGER_DEBUG); - if (!in_array($activity['type'], ['Like', 'Dislike'])) { + logger('Processing activity: ' . $activity['type'], LOGGER_DEBUG); + + // Fetch the content only on activities where this matters + if (in_array($activity['type'], ['Create', 'Update', 'Announce'])) { $item = self::fetchObject($object_url, $activity['object']); if (empty($item)) { logger("Object data couldn't be processed", LOGGER_DEBUG); return; } } else { - $item['object'] = $object_url; + if (in_array($activity['type'], ['Accept'])) { + $item['object'] = self::processElement($activity, 'object', 'actor', 'type', 'Follow'); + } elseif (in_array($activity['type'], ['Undo'])) { + $item['object'] = self::processElement($activity, 'object', 'object', 'type', 'Follow'); + } else { + $item['object'] = $object_url; + } + $item['id'] = $activity['id']; $item['receiver'] = []; $item['type'] = $activity['type']; } @@ -622,8 +657,6 @@ class ActivityPub $item['receiver'] = array_merge($item['receiver'], $receivers); - logger('Processing activity: ' . $activity['type'], LOGGER_DEBUG); - switch ($activity['type']) { case 'Create': case 'Update': @@ -632,11 +665,25 @@ class ActivityPub break; case 'Like': + self::likeItem($item, $body); + break; + case 'Dislike': - self::activityItem($item, $body); + break; + + case 'Delete': break; case 'Follow': + self::followUser($item); + break; + + case 'Accept': + self::acceptFollowUser($item); + break; + + case 'Undo': + self::undoFollowUser($item); break; default: @@ -962,18 +1009,42 @@ class ActivityPub } private static function createItem($activity, $body) + { + $item = []; + $item['verb'] = ACTIVITY_POST; + $item['parent-uri'] = $activity['reply-to-uri']; + + if ($activity['reply-to-uri'] == $activity['uri']) { + $item['gravity'] = GRAVITY_PARENT; + $item['object-type'] = ACTIVITY_OBJ_NOTE; + } else { + $item['gravity'] = GRAVITY_COMMENT; + $item['object-type'] = ACTIVITY_OBJ_COMMENT; + } + + self::postItem($activity, $item, $body); + } + + private static function likeItem($activity, $body) + { + $item = []; + $item['verb'] = ACTIVITY_LIKE; + $item['parent-uri'] = $activity['object']; + $item['gravity'] = GRAVITY_ACTIVITY; + $item['object-type'] = ACTIVITY_OBJ_NOTE; + + self::postItem($activity, $item, $body); + } + + private static function postItem($activity, $item, $body) { /// @todo What to do with $activity['context']? - $item = []; $item['network'] = Protocol::ACTIVITYPUB; $item['private'] = !in_array(0, $activity['receiver']); $item['author-id'] = Contact::getIdForURL($activity['author'], 0, true); $item['owner-id'] = Contact::getIdForURL($activity['owner'], 0, true); $item['uri'] = $activity['uri']; - $item['parent-uri'] = $activity['reply-to-uri']; - $item['verb'] = ACTIVITY_POST; - $item['object-type'] = ACTIVITY_OBJ_NOTE; /// Todo? $item['created'] = $activity['published']; $item['edited'] = $activity['updated']; $item['guid'] = $activity['uuid']; @@ -1012,16 +1083,82 @@ class ActivityPub } } - private static function activityItem($data) + private static function followUser($activity) { - logger('Activity "' . $data['type'] . '" for ' . $data['object']); - $items = Item::select(['id'], ['uri' => $data['object']]); - while ($item = Item::fetch($items)) { - logger('Activity ' . $data['type'] . ' for item ' . $item['id'], LOGGER_DEBUG); - Item::performLike($item['id'], strtolower($data['type'])); + if (empty($activity['receiver'][$activity['object']])) { + return; } - DBA::close($item); - logger('Activity done', LOGGER_DEBUG); + + $uid = $activity['receiver'][$activity['object']]; + $owner = User::getOwnerDataById($uid); + + $cid = Contact::getIdForURL($activity['owner'], $uid); + if (!empty($cid)) { + $contact = DBA::selectFirst('contact', [], ['id' => $cid]); + } else { + $contact = false; + } + + $item = ['author-id' => Contact::getIdForURL($activity['owner'])]; + + Contact::addRelationship($owner, $contact, $item); + + $cid = Contact::getIdForURL($activity['owner'], $uid); + if (empty($cid)) { + return; + } + + $contact = DBA::selectFirst('contact', ['network'], ['id' => $cid]); + if ($contact['network'] != Protocol::ACTIVITYPUB) { + Contact::updateFromProbe($cid, Protocol::ACTIVITYPUB); + } + + DBA::update('contact', ['hub-verify' => $activity['id']], ['id' => $cid]); + logger('Follow user ' . $uid . ' from contact ' . $cid . ' with id ' . $activity['id']); } + private static function acceptFollowUser($activity) + { + if (empty($activity['receiver'][$activity['object']])) { + return; + } + + $uid = $activity['receiver'][$activity['object']]; + $owner = User::getOwnerDataById($uid); + + $cid = Contact::getIdForURL($activity['owner'], $uid); + if (empty($cid)) { + logger('No contact found for ' . $activity['owner'], LOGGER_DEBUG); + return; + } + + $fields = ['pending' => false]; + $condition = ['id' => $cid, 'pending' => true]; + DBA::update('comtact', $fields, $condition); + logger('Accept contact request from contact ' . $cid . ' for user ' . $uid, LOGGER_DEBUG); + } + + private static function undoFollowUser($activity) + { + if (empty($activity['receiver'][$activity['object']])) { + return; + } + + $uid = $activity['receiver'][$activity['object']]; + $owner = User::getOwnerDataById($uid); + + $cid = Contact::getIdForURL($activity['owner'], $uid); + if (empty($cid)) { + logger('No contact found for ' . $activity['owner'], LOGGER_DEBUG); + return; + } + + $contact = DBA::selectFirst('contact', [], ['id' => $cid]); + if (!DBA::isResult($contact)) { + return; + } + + Contact::removeFollower($owner, $contact); + logger('Undo following request from contact ' . $cid . ' for user ' . $uid, LOGGER_DEBUG); + } }