2 namespace Friendica\Protocol;
5 * @file include/diaspora.php
6 * @brief The implementation of the diaspora protocol
8 * The new protocol is described here: http://diaspora.github.io/diaspora_federation/index.html
9 * This implementation here interprets the old and the new protocol and sends the new one.
10 * In the future we will remove most stuff from "valid_posting" and interpret only the new protocol.
14 use Friendica\Core\System;
15 use Friendica\Core\Config;
16 use Friendica\Core\PConfig;
17 use Friendica\Core\Worker;
18 use Friendica\Database\DBM;
19 use Friendica\Network\Probe;
25 require_once 'include/items.php';
26 require_once 'include/bb2diaspora.php';
27 require_once 'include/Contact.php';
28 require_once 'include/Photo.php';
29 require_once 'include/socgraph.php';
30 require_once 'include/group.php';
31 require_once 'include/datetime.php';
32 require_once 'include/queue_fn.php';
33 require_once 'include/cache.php';
36 * @brief This class contain functions to create and send Diaspora XML files
42 * @brief Return a list of relay servers
44 * This is an experimental Diaspora feature.
46 * @return array of relay servers
48 public static function relay_list() {
50 $serverdata = Config::get("system", "relay_server");
51 if ($serverdata == "")
56 $servers = explode(",", $serverdata);
58 foreach ($servers AS $server) {
59 $server = trim($server);
60 $addr = "relay@".str_replace("http://", "", normalise_link($server));
61 $batch = $server."/receive/public";
63 $relais = q("SELECT `batch`, `id`, `name`,`network` FROM `contact` WHERE `uid` = 0 AND `batch` = '%s' AND `addr` = '%s' AND `nurl` = '%s' LIMIT 1",
64 dbesc($batch), dbesc($addr), dbesc(normalise_link($server)));
67 $r = q("INSERT INTO `contact` (`uid`, `created`, `name`, `nick`, `addr`, `url`, `nurl`, `batch`, `network`, `rel`, `blocked`, `pending`, `writable`, `name-date`, `uri-date`, `avatar-date`)
68 VALUES (0, '%s', '%s', 'relay', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, '%s', '%s', '%s')",
73 dbesc(normalise_link($server)),
75 dbesc(NETWORK_DIASPORA),
76 intval(CONTACT_IS_FOLLOWER),
77 dbesc(datetime_convert()),
78 dbesc(datetime_convert()),
79 dbesc(datetime_convert())
82 $relais = q("SELECT `batch`, `id`, `name`,`network` FROM `contact` WHERE `uid` = 0 AND `batch` = '%s' LIMIT 1", dbesc($batch));
84 $relay[] = $relais[0];
86 $relay[] = $relais[0];
93 * @brief repairs a signature that was double encoded
95 * The function is unused at the moment. It was copied from the old implementation.
97 * @param string $signature The signature
98 * @param string $handle The handle of the signature owner
99 * @param integer $level This value is only set inside this function to avoid endless loops
101 * @return string the repaired signature
103 private static function repair_signature($signature, $handle = "", $level = 1) {
105 if ($signature == "")
108 if (base64_encode(base64_decode(base64_decode($signature))) == base64_decode($signature)) {
109 $signature = base64_decode($signature);
110 logger("Repaired double encoded signature from Diaspora/Hubzilla handle ".$handle." - level ".$level, LOGGER_DEBUG);
112 // Do a recursive call to be able to fix even multiple levels
114 $signature = self::repair_signature($signature, $handle, ++$level);
121 * @brief verify the envelope and return the verified data
123 * @param string $envelope The magic envelope
125 * @return string verified data
127 private static function verify_magic_envelope($envelope) {
129 $basedom = parse_xml_string($envelope);
131 if (!is_object($basedom)) {
132 logger("Envelope is no XML file");
136 $children = $basedom->children('http://salmon-protocol.org/ns/magic-env');
138 if (sizeof($children) == 0) {
139 logger("XML has no children");
145 $data = base64url_decode($children->data);
146 $type = $children->data->attributes()->type[0];
148 $encoding = $children->encoding;
150 $alg = $children->alg;
152 $sig = base64url_decode($children->sig);
153 $key_id = $children->sig->attributes()->key_id[0];
155 $handle = base64url_decode($key_id);
157 $b64url_data = base64url_encode($data);
158 $msg = str_replace(array("\n", "\r", " ", "\t"), array("", "", "", ""), $b64url_data);
160 $signable_data = $msg.".".base64url_encode($type).".".base64url_encode($encoding).".".base64url_encode($alg);
162 $key = self::key($handle);
164 $verify = rsa_verify($signable_data, $sig, $key);
166 logger('Message did not verify. Discarding.');
174 * @brief encrypts data via AES
176 * @param string $key The AES key
177 * @param string $iv The IV (is used for CBC encoding)
178 * @param string $data The data that is to be encrypted
180 * @return string encrypted data
182 private static function aes_encrypt($key, $iv, $data) {
183 return openssl_encrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
187 * @brief decrypts data via AES
189 * @param string $key The AES key
190 * @param string $iv The IV (is used for CBC encoding)
191 * @param string $encrypted The encrypted data
193 * @return string decrypted data
195 private static function aes_decrypt($key, $iv, $encrypted) {
196 return openssl_decrypt($encrypted,'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA,str_pad($iv, 16, "\0"));
200 * @brief: Decodes incoming Diaspora message in the new format
202 * @param array $importer Array of the importer user
203 * @param string $raw raw post message
206 * 'message' -> decoded Diaspora XML message
207 * 'author' -> author diaspora handle
208 * 'key' -> author public key (converted to pkcs#8)
210 public static function decode_raw($importer, $raw) {
211 $data = json_decode($raw);
213 // Is it a private post? Then decrypt the outer Salmon
214 if (is_object($data)) {
215 $encrypted_aes_key_bundle = base64_decode($data->aes_key);
216 $ciphertext = base64_decode($data->encrypted_magic_envelope);
218 $outer_key_bundle = '';
219 @openssl_private_decrypt($encrypted_aes_key_bundle, $outer_key_bundle, $importer['prvkey']);
220 $j_outer_key_bundle = json_decode($outer_key_bundle);
222 if (!is_object($j_outer_key_bundle)) {
223 logger('Outer Salmon did not verify. Discarding.');
224 http_status_exit(400);
227 $outer_iv = base64_decode($j_outer_key_bundle->iv);
228 $outer_key = base64_decode($j_outer_key_bundle->key);
230 $xml = self::aes_decrypt($outer_key, $outer_iv, $ciphertext);
235 $basedom = parse_xml_string($xml);
237 if (!is_object($basedom)) {
238 logger('Received data does not seem to be an XML. Discarding. '.$xml);
239 http_status_exit(400);
242 $base = $basedom->children(NAMESPACE_SALMON_ME);
244 // Not sure if this cleaning is needed
245 $data = str_replace(array(" ", "\t", "\r", "\n"), array("", "", "", ""), $base->data);
247 // Build the signed data
248 $type = $base->data[0]->attributes()->type[0];
249 $encoding = $base->encoding;
251 $signed_data = $data.'.'.base64url_encode($type).'.'.base64url_encode($encoding).'.'.base64url_encode($alg);
253 // This is the signature
254 $signature = base64url_decode($base->sig);
256 // Get the senders' public key
257 $key_id = $base->sig[0]->attributes()->key_id[0];
258 $author_addr = base64_decode($key_id);
259 $key = self::key($author_addr);
261 $verify = rsa_verify($signed_data, $signature, $key);
263 logger('Message did not verify. Discarding.');
264 http_status_exit(400);
267 return array('message' => (string)base64url_decode($base->data),
268 'author' => unxmlify($author_addr),
269 'key' => (string)$key);
273 * @brief: Decodes incoming Diaspora message in the deprecated format
275 * @param array $importer Array of the importer user
276 * @param string $xml urldecoded Diaspora salmon
279 * 'message' -> decoded Diaspora XML message
280 * 'author' -> author diaspora handle
281 * 'key' -> author public key (converted to pkcs#8)
283 public static function decode($importer, $xml) {
286 $basedom = parse_xml_string($xml);
288 if (!is_object($basedom)) {
289 logger("XML is not parseable.");
292 $children = $basedom->children('https://joindiaspora.com/protocol');
294 if ($children->header) {
296 $author_link = str_replace('acct:','',$children->header->author_id);
298 // This happens with posts from a relais
300 logger("This is no private post in the old format", LOGGER_DEBUG);
304 $encrypted_header = json_decode(base64_decode($children->encrypted_header));
306 $encrypted_aes_key_bundle = base64_decode($encrypted_header->aes_key);
307 $ciphertext = base64_decode($encrypted_header->ciphertext);
309 $outer_key_bundle = '';
310 openssl_private_decrypt($encrypted_aes_key_bundle,$outer_key_bundle,$importer['prvkey']);
312 $j_outer_key_bundle = json_decode($outer_key_bundle);
314 $outer_iv = base64_decode($j_outer_key_bundle->iv);
315 $outer_key = base64_decode($j_outer_key_bundle->key);
317 $decrypted = self::aes_decrypt($outer_key, $outer_iv, $ciphertext);
319 logger('decrypted: '.$decrypted, LOGGER_DEBUG);
320 $idom = parse_xml_string($decrypted);
322 $inner_iv = base64_decode($idom->iv);
323 $inner_aes_key = base64_decode($idom->aes_key);
325 $author_link = str_replace('acct:','',$idom->author_id);
328 $dom = $basedom->children(NAMESPACE_SALMON_ME);
330 // figure out where in the DOM tree our data is hiding
332 if ($dom->provenance->data)
333 $base = $dom->provenance;
334 elseif ($dom->env->data)
340 logger('unable to locate salmon data in xml');
341 http_status_exit(400);
345 // Stash the signature away for now. We have to find their key or it won't be good for anything.
346 $signature = base64url_decode($base->sig);
350 // strip whitespace so our data element will return to one big base64 blob
351 $data = str_replace(array(" ","\t","\r","\n"),array("","","",""),$base->data);
354 // stash away some other stuff for later
356 $type = $base->data[0]->attributes()->type[0];
357 $keyhash = $base->sig[0]->attributes()->keyhash[0];
358 $encoding = $base->encoding;
362 $signed_data = $data.'.'.base64url_encode($type).'.'.base64url_encode($encoding).'.'.base64url_encode($alg);
366 $data = base64url_decode($data);
370 $inner_decrypted = $data;
373 // Decode the encrypted blob
375 $inner_encrypted = base64_decode($data);
376 $inner_decrypted = self::aes_decrypt($inner_aes_key, $inner_iv, $inner_encrypted);
380 logger('Could not retrieve author URI.');
381 http_status_exit(400);
383 // Once we have the author URI, go to the web and try to find their public key
384 // (first this will look it up locally if it is in the fcontact cache)
385 // This will also convert diaspora public key from pkcs#1 to pkcs#8
387 logger('Fetching key for '.$author_link);
388 $key = self::key($author_link);
391 logger('Could not retrieve author key.');
392 http_status_exit(400);
395 $verify = rsa_verify($signed_data,$signature,$key);
398 logger('Message did not verify. Discarding.');
399 http_status_exit(400);
402 logger('Message verified.');
404 return array('message' => (string)$inner_decrypted,
405 'author' => unxmlify($author_link),
406 'key' => (string)$key);
411 * @brief Dispatches public messages and find the fitting receivers
413 * @param array $msg The post that will be dispatched
415 * @return int The message id of the generated message, "true" or "false" if there was an error
417 public static function dispatch_public($msg) {
419 $enabled = intval(Config::get("system", "diaspora_enabled"));
421 logger("diaspora is disabled");
425 if (!($postdata = self::valid_posting($msg))) {
426 logger("Invalid posting");
430 $fields = $postdata['fields'];
432 // Is it a an action (comment, like, ...) for our own post?
433 if (isset($fields->parent_guid) && !$postdata["relayed"]) {
434 $guid = notags(unxmlify($fields->parent_guid));
435 $importer = self::importer_for_guid($guid);
436 if (is_array($importer)) {
437 logger("delivering to origin: ".$importer["name"]);
438 $message_id = self::dispatch($importer, $msg, $fields);
443 // Process item retractions. This has to be done separated from the other stuff,
444 // since retractions for comments could come even from non followers.
445 if (!empty($fields) && in_array($fields->getName(), array('retraction'))) {
446 $target = notags(unxmlify($fields->target_type));
447 if (in_array($target, array("Comment", "Like", "Post", "Reshare", "StatusMessage"))) {
448 logger('processing retraction for '.$target, LOGGER_DEBUG);
449 $importer = array("uid" => 0, "page-flags" => PAGE_FREELOVE);
450 $message_id = self::dispatch($importer, $msg, $fields);
455 // Now distribute it to the followers
456 $r = q("SELECT `user`.* FROM `user` WHERE `user`.`uid` IN
457 (SELECT `contact`.`uid` FROM `contact` WHERE `contact`.`network` = '%s' AND `contact`.`addr` = '%s')
458 AND NOT `account_expired` AND NOT `account_removed`",
459 dbesc(NETWORK_DIASPORA),
460 dbesc($msg["author"])
463 if (DBM::is_result($r)) {
464 foreach ($r as $rr) {
465 logger("delivering to: ".$rr["username"]);
466 self::dispatch($rr, $msg, $fields);
468 } elseif (!Config::get('system', 'relay_subscribe', false)) {
469 logger("Unwanted message from ".$msg["author"]." send by ".$_SERVER["REMOTE_ADDR"]." with ".$_SERVER["HTTP_USER_AGENT"].": ".print_r($msg, true), LOGGER_DEBUG);
471 // Use a dummy importer to import the data for the public copy
472 $importer = array("uid" => 0, "page-flags" => PAGE_FREELOVE);
473 $message_id = self::dispatch($importer, $msg, $fields);
480 * @brief Dispatches the different message types to the different functions
482 * @param array $importer Array of the importer user
483 * @param array $msg The post that will be dispatched
484 * @param object $fields SimpleXML object that contains the message
486 * @return int The message id of the generated message, "true" or "false" if there was an error
488 public static function dispatch($importer, $msg, $fields = null) {
490 // The sender is the handle of the contact that sent the message.
491 // This will often be different with relayed messages (for example "like" and "comment")
492 $sender = $msg["author"];
494 // This is only needed for private postings since this is already done for public ones before
495 if (is_null($fields)) {
496 if (!($postdata = self::valid_posting($msg))) {
497 logger("Invalid posting");
500 $fields = $postdata['fields'];
503 $type = $fields->getName();
505 logger("Received message type ".$type." from ".$sender." for user ".$importer["uid"], LOGGER_DEBUG);
508 case "account_migration":
509 return self::receiveAccountMigration($importer, $fields);
511 case "account_deletion":
512 return self::receive_account_deletion($importer, $fields);
515 return self::receive_comment($importer, $sender, $fields, $msg["message"]);
518 return self::receive_contact_request($importer, $fields);
521 return self::receive_conversation($importer, $msg, $fields);
524 return self::receive_like($importer, $sender, $fields);
527 return self::receive_message($importer, $fields);
529 case "participation": // Not implemented
530 return self::receive_participation($importer, $fields);
532 case "photo": // Not implemented
533 return self::receive_photo($importer, $fields);
535 case "poll_participation": // Not implemented
536 return self::receive_poll_participation($importer, $fields);
539 return self::receive_profile($importer, $fields);
542 return self::receive_reshare($importer, $fields, $msg["message"]);
545 return self::receive_retraction($importer, $sender, $fields);
547 case "status_message":
548 return self::receive_status_message($importer, $fields, $msg["message"]);
551 logger("Unknown message type ".$type);
559 * @brief Checks if a posting is valid and fetches the data fields.
561 * This function does not only check the signature.
562 * It also does the conversion between the old and the new diaspora format.
564 * @param array $msg Array with the XML, the sender handle and the sender signature
566 * @return bool|array If the posting is valid then an array with an SimpleXML object is returned
568 private static function valid_posting($msg) {
570 $data = parse_xml_string($msg["message"]);
572 if (!is_object($data)) {
573 logger("No valid XML ".$msg["message"], LOGGER_DEBUG);
577 $first_child = $data->getName();
579 // Is this the new or the old version?
580 if ($data->getName() == "XML") {
582 foreach ($data->post->children() as $child)
589 $type = $element->getName();
592 logger("Got message type ".$type.": ".$msg["message"], LOGGER_DATA);
594 // All retractions are handled identically from now on.
595 // In the new version there will only be "retraction".
596 if (in_array($type, array("signed_retraction", "relayable_retraction")))
597 $type = "retraction";
599 if ($type == "request")
602 $fields = new SimpleXMLElement("<".$type."/>");
606 foreach ($element->children() AS $fieldname => $entry) {
608 // Translation for the old XML structure
609 if ($fieldname == "diaspora_handle") {
610 $fieldname = "author";
612 if ($fieldname == "participant_handles") {
613 $fieldname = "participants";
615 if (in_array($type, array("like", "participation"))) {
616 if ($fieldname == "target_type") {
617 $fieldname = "parent_type";
620 if ($fieldname == "sender_handle") {
621 $fieldname = "author";
623 if ($fieldname == "recipient_handle") {
624 $fieldname = "recipient";
626 if ($fieldname == "root_diaspora_id") {
627 $fieldname = "root_author";
629 if ($type == "status_message") {
630 if ($fieldname == "raw_message") {
634 if ($type == "retraction") {
635 if ($fieldname == "post_guid") {
636 $fieldname = "target_guid";
638 if ($fieldname == "type") {
639 $fieldname = "target_type";
644 if (($fieldname == "author_signature") && ($entry != ""))
645 $author_signature = base64_decode($entry);
646 elseif (($fieldname == "parent_author_signature") && ($entry != ""))
647 $parent_author_signature = base64_decode($entry);
648 elseif (!in_array($fieldname, array("author_signature", "parent_author_signature", "target_author_signature"))) {
649 if ($signed_data != "") {
651 $signed_data_parent .= ";";
654 $signed_data .= $entry;
656 if (!in_array($fieldname, array("parent_author_signature", "target_author_signature")) ||
657 ($orig_type == "relayable_retraction"))
658 xml::copy($entry, $fields, $fieldname);
661 // This is something that shouldn't happen at all.
662 if (in_array($type, array("status_message", "reshare", "profile")))
663 if ($msg["author"] != $fields->author) {
664 logger("Message handle is not the same as envelope sender. Quitting this message.");
668 // Only some message types have signatures. So we quit here for the other types.
669 if (!in_array($type, array("comment", "like"))) {
670 return array("fields" => $fields, "relayed" => false);
672 // No author_signature? This is a must, so we quit.
673 if (!isset($author_signature)) {
674 logger("No author signature for type ".$type." - Message: ".$msg["message"], LOGGER_DEBUG);
678 if (isset($parent_author_signature)) {
681 $key = self::key($msg["author"]);
683 if (!rsa_verify($signed_data, $parent_author_signature, $key, "sha256")) {
684 logger("No valid parent author signature for parent author ".$msg["author"]. " in type ".$type." - signed data: ".$signed_data." - Message: ".$msg["message"]." - Signature ".$parent_author_signature, LOGGER_DEBUG);
691 $key = self::key($fields->author);
693 if (!rsa_verify($signed_data, $author_signature, $key, "sha256")) {
694 logger("No valid author signature for author ".$fields->author. " in type ".$type." - signed data: ".$signed_data." - Message: ".$msg["message"]." - Signature ".$author_signature, LOGGER_DEBUG);
697 return array("fields" => $fields, "relayed" => $relayed);
702 * @brief Fetches the public key for a given handle
704 * @param string $handle The handle
706 * @return string The public key
708 private static function key($handle) {
709 $handle = strval($handle);
711 logger("Fetching diaspora key for: ".$handle);
713 $r = self::person_by_handle($handle);
721 * @brief Fetches data for a given handle
723 * @param string $handle The handle
725 * @return array the queried data
727 public static function person_by_handle($handle) {
729 $r = q("SELECT * FROM `fcontact` WHERE `network` = '%s' AND `addr` = '%s' LIMIT 1",
730 dbesc(NETWORK_DIASPORA),
735 logger("In cache ".print_r($r,true), LOGGER_DEBUG);
737 // update record occasionally so it doesn't get stale
738 $d = strtotime($person["updated"]." +00:00");
739 if ($d < strtotime("now - 14 days"))
742 if ($person["guid"] == "")
746 if (!$person || $update) {
747 logger("create or refresh", LOGGER_DEBUG);
748 $r = Probe::uri($handle, NETWORK_DIASPORA);
750 // Note that Friendica contacts will return a "Diaspora person"
751 // if Diaspora connectivity is enabled on their server
752 if ($r && ($r["network"] === NETWORK_DIASPORA)) {
753 self::add_fcontact($r, $update);
761 * @brief Updates the fcontact table
763 * @param array $arr The fcontact data
764 * @param bool $update Update or insert?
766 * @return string The id of the fcontact entry
768 private static function add_fcontact($arr, $update = false) {
771 $r = q("UPDATE `fcontact` SET
785 WHERE `url` = '%s' AND `network` = '%s'",
787 dbesc($arr["photo"]),
788 dbesc($arr["request"]),
790 dbesc(strtolower($arr["addr"])),
792 dbesc($arr["batch"]),
793 dbesc($arr["notify"]),
795 dbesc($arr["confirm"]),
796 dbesc($arr["alias"]),
797 dbesc($arr["pubkey"]),
798 dbesc(datetime_convert()),
800 dbesc($arr["network"])
803 $r = q("INSERT INTO `fcontact` (`url`,`name`,`photo`,`request`,`nick`,`addr`, `guid`,
804 `batch`, `notify`,`poll`,`confirm`,`network`,`alias`,`pubkey`,`updated`)
805 VALUES ('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s')",
808 dbesc($arr["photo"]),
809 dbesc($arr["request"]),
813 dbesc($arr["batch"]),
814 dbesc($arr["notify"]),
816 dbesc($arr["confirm"]),
817 dbesc($arr["network"]),
818 dbesc($arr["alias"]),
819 dbesc($arr["pubkey"]),
820 dbesc(datetime_convert())
828 * @brief get a handle (user@domain.tld) from a given contact id or gcontact id
830 * @param int $contact_id The id in the contact table
831 * @param int $gcontact_id The id in the gcontact table
833 * @return string the handle
835 public static function handle_from_contact($contact_id, $gcontact_id = 0) {
838 logger("contact id is ".$contact_id." - gcontact id is ".$gcontact_id, LOGGER_DEBUG);
840 if ($gcontact_id != 0) {
841 $r = q("SELECT `addr` FROM `gcontact` WHERE `id` = %d AND `addr` != ''",
842 intval($gcontact_id));
844 if (DBM::is_result($r)) {
845 return strtolower($r[0]["addr"]);
849 $r = q("SELECT `network`, `addr`, `self`, `url`, `nick` FROM `contact` WHERE `id` = %d",
850 intval($contact_id));
852 if (DBM::is_result($r)) {
855 logger("contact 'self' = ".$contact['self']." 'url' = ".$contact['url'], LOGGER_DEBUG);
857 if ($contact['addr'] != "") {
858 $handle = $contact['addr'];
860 $baseurl_start = strpos($contact['url'],'://') + 3;
861 $baseurl_length = strpos($contact['url'],'/profile') - $baseurl_start; // allows installations in a subdirectory--not sure how Diaspora will handle
862 $baseurl = substr($contact['url'], $baseurl_start, $baseurl_length);
863 $handle = $contact['nick'].'@'.$baseurl;
867 return strtolower($handle);
871 * @brief get a url (scheme://domain.tld/u/user) from a given Diaspora*
874 * @param mixed $fcontact_guid Hexadecimal string guid
876 * @return string the contact url or null
878 public static function url_from_contact_guid($fcontact_guid) {
879 logger("fcontact guid is ".$fcontact_guid, LOGGER_DEBUG);
881 $r = q("SELECT `url` FROM `fcontact` WHERE `url` != '' AND `network` = '%s' AND `guid` = '%s'",
882 dbesc(NETWORK_DIASPORA),
883 dbesc($fcontact_guid)
886 if (DBM::is_result($r)) {
894 * @brief Get a contact id for a given handle
896 * @param int $uid The user id
897 * @param string $handle The handle in the format user@domain.tld
899 * @return The contact id
901 private static function contact_by_handle($uid, $handle) {
903 // First do a direct search on the contact table
904 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `addr` = '%s' LIMIT 1",
909 if (DBM::is_result($r)) {
913 * We haven't found it?
914 * We use another function for it that will possibly create a contact entry.
916 $cid = get_contact($handle, $uid);
919 /// @TODO Contact retrieval should be encapsulated into an "entity" class like `Contact`
920 $r = q("SELECT * FROM `contact` WHERE `id` = %d LIMIT 1", intval($cid));
922 if (DBM::is_result($r)) {
928 $handle_parts = explode("@", $handle);
929 $nurl_sql = "%%://".$handle_parts[1]."%%/profile/".$handle_parts[0];
930 $r = q("SELECT * FROM `contact` WHERE `network` = '%s' AND `uid` = %d AND `nurl` LIKE '%s' LIMIT 1",
935 if (DBM::is_result($r)) {
939 logger("Haven't found contact for user ".$uid." and handle ".$handle, LOGGER_DEBUG);
944 * @brief Check if posting is allowed for this contact
946 * @param array $importer Array of the importer user
947 * @param array $contact The contact that is checked
948 * @param bool $is_comment Is the check for a comment?
950 * @return bool is the contact allowed to post?
952 private static function post_allow($importer, $contact, $is_comment = false) {
955 * Perhaps we were already sharing with this person. Now they're sharing with us.
956 * That makes us friends.
957 * Normally this should have handled by getting a request - but this could get lost
959 if ($contact["rel"] == CONTACT_IS_FOLLOWER && in_array($importer["page-flags"], array(PAGE_FREELOVE))) {
960 dba::update('contact', array('rel' => CONTACT_IS_FRIEND, 'writable' => true),
961 array('id' => $contact["id"], 'uid' => $contact["uid"]));
963 $contact["rel"] = CONTACT_IS_FRIEND;
964 logger("defining user ".$contact["nick"]." as friend");
967 // We don't seem to like that person
968 if ($contact["blocked"] || $contact["readonly"] || $contact["archive"]) {
969 // Maybe blocked, don't accept.
971 // We are following this person?
972 } elseif (($contact["rel"] == CONTACT_IS_SHARING) || ($contact["rel"] == CONTACT_IS_FRIEND)) {
973 // Yes, then it is fine.
975 // Is it a post to a community?
976 } elseif (($contact["rel"] == CONTACT_IS_FOLLOWER) && ($importer["page-flags"] == PAGE_COMMUNITY)) {
979 // Is the message a global user or a comment?
980 } elseif (($importer["uid"] == 0) || $is_comment) {
981 // Messages for the global users and comments are always accepted
989 * @brief Fetches the contact id for a handle and checks if posting is allowed
991 * @param array $importer Array of the importer user
992 * @param string $handle The checked handle in the format user@domain.tld
993 * @param bool $is_comment Is the check for a comment?
995 * @return array The contact data
997 private static function allowed_contact_by_handle($importer, $handle, $is_comment = false) {
998 $contact = self::contact_by_handle($importer["uid"], $handle);
1000 logger("A Contact for handle ".$handle." and user ".$importer["uid"]." was not found");
1001 // If a contact isn't found, we accept it anyway if it is a comment
1009 if (!self::post_allow($importer, $contact, $is_comment)) {
1010 logger("The handle: ".$handle." is not allowed to post to user ".$importer["uid"]);
1017 * @brief Does the message already exists on the system?
1019 * @param int $uid The user id
1020 * @param string $guid The guid of the message
1022 * @return int|bool message id if the message already was stored into the system - or false.
1024 private static function message_exists($uid, $guid) {
1025 $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `guid` = '%s' LIMIT 1",
1030 if (DBM::is_result($r)) {
1031 logger("message ".$guid." already exists for user ".$uid);
1039 * @brief Checks for links to posts in a message
1041 * @param array $item The item array
1043 private static function fetch_guid($item) {
1044 $expression = "=diaspora://.*?/post/([0-9A-Za-z\-_@.:]{15,254}[0-9A-Za-z])=ism";
1045 preg_replace_callback($expression,
1046 function ($match) use ($item) {
1047 return self::fetch_guid_sub($match, $item);
1050 preg_replace_callback("&\[url=/posts/([^\[\]]*)\](.*)\[\/url\]&Usi",
1051 function ($match) use ($item) {
1052 return self::fetch_guid_sub($match, $item);
1057 * @brief Checks for relative /people/* links in an item body to match local
1058 * contacts or prepends the remote host taken from the author link.
1060 * @param string $body The item body to replace links from
1061 * @param string $author_link The author link for missing local contact fallback
1063 * @return the replaced string
1065 public static function replace_people_guid($body, $author_link) {
1066 $return = preg_replace_callback("&\[url=/people/([^\[\]]*)\](.*)\[\/url\]&Usi",
1067 function ($match) use ($author_link) {
1069 // 0 => '[url=/people/0123456789abcdef]Foo Bar[/url]'
1070 // 1 => '0123456789abcdef'
1072 $handle = self::url_from_contact_guid($match[1]);
1075 $return = '@[url='.$handle.']'.$match[2].'[/url]';
1077 // No local match, restoring absolute remote URL from author scheme and host
1078 $author_url = parse_url($author_link);
1079 $return = '[url='.$author_url['scheme'].'://'.$author_url['host'].'/people/'.$match[1].']'.$match[2].'[/url]';
1089 * @brief sub function of "fetch_guid" which checks for links in messages
1091 * @param array $match array containing a link that has to be checked for a message link
1092 * @param array $item The item array
1094 private static function fetch_guid_sub($match, $item) {
1095 if (!self::store_by_guid($match[1], $item["author-link"]))
1096 self::store_by_guid($match[1], $item["owner-link"]);
1100 * @brief Fetches an item with a given guid from a given server
1102 * @param string $guid the message guid
1103 * @param string $server The server address
1104 * @param int $uid The user id of the user
1106 * @return int the message id of the stored message or false
1108 private static function store_by_guid($guid, $server, $uid = 0) {
1109 $serverparts = parse_url($server);
1110 $server = $serverparts["scheme"]."://".$serverparts["host"];
1112 logger("Trying to fetch item ".$guid." from ".$server, LOGGER_DEBUG);
1114 $msg = self::message($guid, $server);
1119 logger("Successfully fetched item ".$guid." from ".$server, LOGGER_DEBUG);
1121 // Now call the dispatcher
1122 return self::dispatch_public($msg);
1126 * @brief Fetches a message from a server
1128 * @param string $guid message guid
1129 * @param string $server The url of the server
1130 * @param int $level Endless loop prevention
1133 * 'message' => The message XML
1134 * 'author' => The author handle
1135 * 'key' => The public key of the author
1137 private static function message($guid, $server, $level = 0) {
1142 // This will work for new Diaspora servers and Friendica servers from 3.5
1143 $source_url = $server."/fetch/post/".urlencode($guid);
1145 logger("Fetch post from ".$source_url, LOGGER_DEBUG);
1147 $envelope = fetch_url($source_url);
1149 logger("Envelope was fetched.", LOGGER_DEBUG);
1150 $x = self::verify_magic_envelope($envelope);
1152 logger("Envelope could not be verified.", LOGGER_DEBUG);
1154 logger("Envelope was verified.", LOGGER_DEBUG);
1158 // This will work for older Diaspora and Friendica servers
1160 $source_url = $server."/p/".urlencode($guid).".xml";
1161 logger("Fetch post from ".$source_url, LOGGER_DEBUG);
1163 $x = fetch_url($source_url);
1168 $source_xml = parse_xml_string($x);
1170 if (!is_object($source_xml))
1173 if ($source_xml->post->reshare) {
1174 // Reshare of a reshare - old Diaspora version
1175 logger("Message is a reshare", LOGGER_DEBUG);
1176 return self::message($source_xml->post->reshare->root_guid, $server, ++$level);
1177 } elseif ($source_xml->getName() == "reshare") {
1178 // Reshare of a reshare - new Diaspora version
1179 logger("Message is a new reshare", LOGGER_DEBUG);
1180 return self::message($source_xml->root_guid, $server, ++$level);
1185 // Fetch the author - for the old and the new Diaspora version
1186 if ($source_xml->post->status_message->diaspora_handle)
1187 $author = (string)$source_xml->post->status_message->diaspora_handle;
1188 elseif ($source_xml->author && ($source_xml->getName() == "status_message"))
1189 $author = (string)$source_xml->author;
1191 // If this isn't a "status_message" then quit
1193 logger("Message doesn't seem to be a status message", LOGGER_DEBUG);
1197 $msg = array("message" => $x, "author" => $author);
1199 $msg["key"] = self::key($msg["author"]);
1205 * @brief Fetches the item record of a given guid
1207 * @param int $uid The user id
1208 * @param string $guid message guid
1209 * @param string $author The handle of the item
1210 * @param array $contact The contact of the item owner
1212 * @return array the item record
1214 private static function parent_item($uid, $guid, $author, $contact) {
1215 $r = q("SELECT `id`, `parent`, `body`, `wall`, `uri`, `guid`, `private`, `origin`,
1216 `author-name`, `author-link`, `author-avatar`,
1217 `owner-name`, `owner-link`, `owner-avatar`
1218 FROM `item` WHERE `uid` = %d AND `guid` = '%s' LIMIT 1",
1219 intval($uid), dbesc($guid));
1222 $result = self::store_by_guid($guid, $contact["url"], $uid);
1225 $person = self::person_by_handle($author);
1226 $result = self::store_by_guid($guid, $person["url"], $uid);
1230 logger("Fetched missing item ".$guid." - result: ".$result, LOGGER_DEBUG);
1232 $r = q("SELECT `id`, `body`, `wall`, `uri`, `private`, `origin`,
1233 `author-name`, `author-link`, `author-avatar`,
1234 `owner-name`, `owner-link`, `owner-avatar`
1235 FROM `item` WHERE `uid` = %d AND `guid` = '%s' LIMIT 1",
1236 intval($uid), dbesc($guid));
1241 logger("parent item not found: parent: ".$guid." - user: ".$uid);
1244 logger("parent item found: parent: ".$guid." - user: ".$uid);
1250 * @brief returns contact details
1252 * @param array $contact The default contact if the person isn't found
1253 * @param array $person The record of the person
1254 * @param int $uid The user id
1257 * 'cid' => contact id
1258 * 'network' => network type
1260 private static function author_contact_by_url($contact, $person, $uid) {
1262 $r = q("SELECT `id`, `network`, `url` FROM `contact` WHERE `nurl` = '%s' AND `uid` = %d LIMIT 1",
1263 dbesc(normalise_link($person["url"])), intval($uid));
1266 $network = $r[0]["network"];
1268 // We are receiving content from a user that possibly is about to be terminated
1269 // This means the user is vital, so we remove a possible termination date.
1270 unmark_for_death($r[0]);
1272 $cid = $contact["id"];
1273 $network = NETWORK_DIASPORA;
1276 return array("cid" => $cid, "network" => $network);
1280 * @brief Is the profile a hubzilla profile?
1282 * @param string $url The profile link
1284 * @return bool is it a hubzilla server?
1286 public static function is_redmatrix($url) {
1287 return(strstr($url, "/channel/"));
1291 * @brief Generate a post link with a given handle and message guid
1293 * @param string $addr The user handle
1294 * @param string $guid message guid
1296 * @return string the post link
1298 private static function plink($addr, $guid, $parent_guid = '') {
1299 $r = q("SELECT `url`, `nick`, `network` FROM `fcontact` WHERE `addr`='%s' LIMIT 1", dbesc($addr));
1302 if (!DBM::is_result($r)) {
1303 if ($parent_guid != '') {
1304 return "https://".substr($addr,strpos($addr,"@") + 1)."/posts/".$parent_guid."#".$guid;
1306 return "https://".substr($addr,strpos($addr,"@") + 1)."/posts/".$guid;
1310 // Friendica contacts are often detected as Diaspora contacts in the "fcontact" table
1311 // So we try another way as well.
1312 $s = q("SELECT `network` FROM `gcontact` WHERE `nurl`='%s' LIMIT 1", dbesc(normalise_link($r[0]["url"])));
1313 if (DBM::is_result($s)) {
1314 $r[0]["network"] = $s[0]["network"];
1317 if ($r[0]["network"] == NETWORK_DFRN) {
1318 return str_replace("/profile/".$r[0]["nick"]."/", "/display/".$guid, $r[0]["url"]."/");
1321 if (self::is_redmatrix($r[0]["url"])) {
1322 return $r[0]["url"]."/?f=&mid=".$guid;
1325 if ($parent_guid != '') {
1326 return "https://".substr($addr,strpos($addr,"@")+1)."/posts/".$parent_guid."#".$guid;
1328 return "https://".substr($addr,strpos($addr,"@")+1)."/posts/".$guid;
1333 * @brief Receives account migration
1335 * @param array $importer Array of the importer user
1336 * @param object $data The message object
1338 * @return bool Success
1340 private static function receiveAccountMigration($importer, $data) {
1341 $old_handle = notags(unxmlify($data->author));
1342 $new_handle = notags(unxmlify($data->profile->author));
1343 $signature = notags(unxmlify($data->signature));
1345 $contact = self::contact_by_handle($importer["uid"], $old_handle);
1347 logger("cannot find contact for sender: ".$old_handle." and user ".$importer["uid"]);
1351 logger("Got migration for ".$old_handle.", to ".$new_handle." with user ".$importer["uid"]);
1354 $signed_text = 'AccountMigration:'.$old_handle.':'.$new_handle;
1355 $key = self::key($old_handle);
1356 if (!rsa_verify($signed_text, $signature, $key, "sha256")) {
1357 logger('No valid signature for migration.');
1361 // Update the profile
1362 self::receive_profile($importer, $data->profile);
1364 // change the technical stuff in contact and gcontact
1365 $data = Probe::uri($new_handle);
1366 if ($data['network'] == NETWORK_PHANTOM) {
1367 logger('Account for '.$new_handle." couldn't be probed.");
1371 $fields = array('url' => $data['url'], 'nurl' => normalise_link($data['url']),
1372 'name' => $data['name'], 'nick' => $data['nick'],
1373 'addr' => $data['addr'], 'batch' => $data['batch'],
1374 'notify' => $data['notify'], 'poll' => $data['poll'],
1375 'network' => $data['network']);
1377 dba::update('contact', $fields, array('addr' => $old_handle));
1379 $fields = array('url' => $data['url'], 'nurl' => normalise_link($data['url']),
1380 'name' => $data['name'], 'nick' => $data['nick'],
1381 'addr' => $data['addr'], 'connect' => $data['addr'],
1382 'notify' => $data['notify'], 'photo' => $data['photo'],
1383 'server_url' => $data['baseurl'], 'network' => $data['network']);
1385 dba::update('gcontact', $fields, array('addr' => $old_handle));
1387 logger('Contacts are updated.');
1390 /// @todo This is an extreme performance killer
1392 'owner-link' => array($contact["url"], $data["url"]),
1393 'author-link' => array($contact["url"], $data["url"]),
1395 foreach ($fields as $n=>$f) {
1396 $r = q("SELECT `id` FROM `item` WHERE `%s` = '%s' AND `uid` = %d LIMIT 1",
1398 intval($importer["uid"]));
1400 if (DBM::is_result($r)) {
1401 $x = q("UPDATE `item` SET `%s` = '%s' WHERE `%s` = '%s' AND `uid` = %d",
1404 intval($importer["uid"]));
1412 logger('Items are updated.');
1418 * @brief Processes an account deletion
1420 * @param array $importer Array of the importer user
1421 * @param object $data The message object
1423 * @return bool Success
1425 private static function receive_account_deletion($importer, $data) {
1427 /// @todo Account deletion should remove the contact from the global contacts as well
1429 $author = notags(unxmlify($data->author));
1431 $contact = self::contact_by_handle($importer["uid"], $author);
1433 logger("cannot find contact for author: ".$author);
1437 // We now remove the contact
1438 contact_remove($contact["id"]);
1443 * @brief Fetch the uri from our database if we already have this item (maybe from ourselves)
1445 * @param string $author Author handle
1446 * @param string $guid Message guid
1447 * @param boolean $onlyfound Only return uri when found in the database
1449 * @return string The constructed uri or the one from our database
1451 private static function get_uri_from_guid($author, $guid, $onlyfound = false) {
1453 $r = q("SELECT `uri` FROM `item` WHERE `guid` = '%s' LIMIT 1", dbesc($guid));
1454 if (DBM::is_result($r)) {
1455 return $r[0]["uri"];
1456 } elseif (!$onlyfound) {
1457 return $author.":".$guid;
1464 * @brief Fetch the guid from our database with a given uri
1466 * @param string $author Author handle
1467 * @param string $uri Message uri
1469 * @return string The post guid
1471 private static function get_guid_from_uri($uri, $uid) {
1473 $r = q("SELECT `guid` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($uri), intval($uid));
1474 if (DBM::is_result($r)) {
1475 return $r[0]["guid"];
1482 * @brief Find the best importer for a comment, like, ...
1484 * @param string $guid The guid of the item
1486 * @return array|boolean the origin owner of that post - or false
1488 private static function importer_for_guid($guid) {
1489 $item = dba::fetch_first("SELECT `uid` FROM `item` WHERE `origin` AND `guid` = ? LIMIT 1", $guid);
1491 if (DBM::is_result($item)) {
1492 logger("Found user ".$item['uid']." as owner of item ".$guid, LOGGER_DEBUG);
1493 $contact = dba::fetch_first("SELECT * FROM `contact` WHERE `self` AND `uid` = ?", $item['uid']);
1494 if (DBM::is_result($contact)) {
1502 * @brief Processes an incoming comment
1504 * @param array $importer Array of the importer user
1505 * @param string $sender The sender of the message
1506 * @param object $data The message object
1507 * @param string $xml The original XML of the message
1509 * @return int The message id of the generated comment or "false" if there was an error
1511 private static function receive_comment($importer, $sender, $data, $xml) {
1512 $author = notags(unxmlify($data->author));
1513 $guid = notags(unxmlify($data->guid));
1514 $parent_guid = notags(unxmlify($data->parent_guid));
1515 $text = unxmlify($data->text);
1517 if (isset($data->created_at)) {
1518 $created_at = datetime_convert("UTC", "UTC", notags(unxmlify($data->created_at)));
1520 $created_at = datetime_convert();
1523 if (isset($data->thread_parent_guid)) {
1524 $thread_parent_guid = notags(unxmlify($data->thread_parent_guid));
1525 $thr_uri = self::get_uri_from_guid("", $thread_parent_guid, true);
1530 $contact = self::allowed_contact_by_handle($importer, $sender, true);
1535 $message_id = self::message_exists($importer["uid"], $guid);
1540 $parent_item = self::parent_item($importer["uid"], $parent_guid, $author, $contact);
1541 if (!$parent_item) {
1545 $person = self::person_by_handle($author);
1546 if (!is_array($person)) {
1547 logger("unable to find author details");
1551 // Fetch the contact id - if we know this contact
1552 $author_contact = self::author_contact_by_url($contact, $person, $importer["uid"]);
1554 $datarray = array();
1556 $datarray["uid"] = $importer["uid"];
1557 $datarray["contact-id"] = $author_contact["cid"];
1558 $datarray["network"] = $author_contact["network"];
1560 $datarray["author-name"] = $person["name"];
1561 $datarray["author-link"] = $person["url"];
1562 $datarray["author-avatar"] = ((x($person,"thumb")) ? $person["thumb"] : $person["photo"]);
1564 $datarray["owner-name"] = $contact["name"];
1565 $datarray["owner-link"] = $contact["url"];
1566 $datarray["owner-avatar"] = ((x($contact,"thumb")) ? $contact["thumb"] : $contact["photo"]);
1568 $datarray["guid"] = $guid;
1569 $datarray["uri"] = self::get_uri_from_guid($author, $guid);
1571 $datarray["type"] = "remote-comment";
1572 $datarray["verb"] = ACTIVITY_POST;
1573 $datarray["gravity"] = GRAVITY_COMMENT;
1575 if ($thr_uri != "") {
1576 $datarray["parent-uri"] = $thr_uri;
1578 $datarray["parent-uri"] = $parent_item["uri"];
1581 $datarray["object-type"] = ACTIVITY_OBJ_COMMENT;
1583 $datarray["protocol"] = PROTOCOL_DIASPORA;
1584 $datarray["source"] = $xml;
1586 $datarray["changed"] = $datarray["created"] = $datarray["edited"] = $created_at;
1588 $datarray["plink"] = self::plink($author, $guid, $parent_item['guid']);
1590 $body = diaspora2bb($text);
1592 $datarray["body"] = self::replace_people_guid($body, $person["url"]);
1594 self::fetch_guid($datarray);
1596 $message_id = item_store($datarray);
1598 if ($message_id <= 0) {
1603 logger("Stored comment ".$datarray["guid"]." with message id ".$message_id, LOGGER_DEBUG);
1606 // If we are the origin of the parent we store the original data and notify our followers
1607 if ($message_id && $parent_item["origin"]) {
1609 // Formerly we stored the signed text, the signature and the author in different fields.
1610 // We now store the raw data so that we are more flexible.
1611 dba::insert('sign', array('iid' => $message_id, 'signed_text' => json_encode($data)));
1614 Worker::add(PRIORITY_HIGH, "notifier", "comment-import", $message_id);
1621 * @brief processes and stores private messages
1623 * @param array $importer Array of the importer user
1624 * @param array $contact The contact of the message
1625 * @param object $data The message object
1626 * @param array $msg Array of the processed message, author handle and key
1627 * @param object $mesg The private message
1628 * @param array $conversation The conversation record to which this message belongs
1630 * @return bool "true" if it was successful
1632 private static function receive_conversation_message($importer, $contact, $data, $msg, $mesg, $conversation) {
1633 $author = notags(unxmlify($data->author));
1634 $guid = notags(unxmlify($data->guid));
1635 $subject = notags(unxmlify($data->subject));
1637 // "diaspora_handle" is the element name from the old version
1638 // "author" is the element name from the new version
1639 if ($mesg->author) {
1640 $msg_author = notags(unxmlify($mesg->author));
1641 } elseif ($mesg->diaspora_handle) {
1642 $msg_author = notags(unxmlify($mesg->diaspora_handle));
1647 $msg_guid = notags(unxmlify($mesg->guid));
1648 $msg_conversation_guid = notags(unxmlify($mesg->conversation_guid));
1649 $msg_text = unxmlify($mesg->text);
1650 $msg_created_at = datetime_convert("UTC", "UTC", notags(unxmlify($mesg->created_at)));
1652 if ($msg_conversation_guid != $guid) {
1653 logger("message conversation guid does not belong to the current conversation.");
1657 $body = diaspora2bb($msg_text);
1658 $message_uri = $msg_author.":".$msg_guid;
1660 $person = self::person_by_handle($msg_author);
1664 $r = q("SELECT `id` FROM `mail` WHERE `guid` = '%s' AND `uid` = %d LIMIT 1",
1666 intval($importer["uid"])
1668 if (DBM::is_result($r)) {
1669 logger("duplicate message already delivered.", LOGGER_DEBUG);
1673 q("INSERT INTO `mail` (`uid`, `guid`, `convid`, `from-name`,`from-photo`,`from-url`,`contact-id`,`title`,`body`,`seen`,`reply`,`uri`,`parent-uri`,`created`)
1674 VALUES (%d, '%s', %d, '%s', '%s', '%s', %d, '%s', '%s', %d, %d, '%s','%s','%s')",
1675 intval($importer["uid"]),
1677 intval($conversation["id"]),
1678 dbesc($person["name"]),
1679 dbesc($person["photo"]),
1680 dbesc($person["url"]),
1681 intval($contact["id"]),
1686 dbesc($message_uri),
1687 dbesc($author.":".$guid),
1688 dbesc($msg_created_at)
1693 dba::update('conv', array('updated' => datetime_convert()), array('id' => $conversation["id"]));
1696 "type" => NOTIFY_MAIL,
1697 "notify_flags" => $importer["notify-flags"],
1698 "language" => $importer["language"],
1699 "to_name" => $importer["username"],
1700 "to_email" => $importer["email"],
1701 "uid" =>$importer["uid"],
1702 "item" => array("subject" => $subject, "body" => $body),
1703 "source_name" => $person["name"],
1704 "source_link" => $person["url"],
1705 "source_photo" => $person["thumb"],
1706 "verb" => ACTIVITY_POST,
1713 * @brief Processes new private messages (answers to private messages are processed elsewhere)
1715 * @param array $importer Array of the importer user
1716 * @param array $msg Array of the processed message, author handle and key
1717 * @param object $data The message object
1719 * @return bool Success
1721 private static function receive_conversation($importer, $msg, $data) {
1722 $author = notags(unxmlify($data->author));
1723 $guid = notags(unxmlify($data->guid));
1724 $subject = notags(unxmlify($data->subject));
1725 $created_at = datetime_convert("UTC", "UTC", notags(unxmlify($data->created_at)));
1726 $participants = notags(unxmlify($data->participants));
1728 $messages = $data->message;
1730 if (!count($messages)) {
1731 logger("empty conversation");
1735 $contact = self::allowed_contact_by_handle($importer, $msg["author"], true);
1739 $conversation = null;
1741 $c = q("SELECT * FROM `conv` WHERE `uid` = %d AND `guid` = '%s' LIMIT 1",
1742 intval($importer["uid"]),
1746 $conversation = $c[0];
1748 $r = q("INSERT INTO `conv` (`uid`, `guid`, `creator`, `created`, `updated`, `subject`, `recips`)
1749 VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s')",
1750 intval($importer["uid"]),
1754 dbesc(datetime_convert()),
1756 dbesc($participants)
1759 $c = q("SELECT * FROM `conv` WHERE `uid` = %d AND `guid` = '%s' LIMIT 1",
1760 intval($importer["uid"]),
1765 $conversation = $c[0];
1767 if (!$conversation) {
1768 logger("unable to create conversation.");
1772 foreach ($messages as $mesg)
1773 self::receive_conversation_message($importer, $contact, $data, $msg, $mesg, $conversation);
1779 * @brief Creates the body for a "like" message
1781 * @param array $contact The contact that send us the "like"
1782 * @param array $parent_item The item array of the parent item
1783 * @param string $guid message guid
1785 * @return string the body
1787 private static function construct_like_body($contact, $parent_item, $guid) {
1788 $bodyverb = t('%1$s likes %2$s\'s %3$s');
1790 $ulink = "[url=".$contact["url"]."]".$contact["name"]."[/url]";
1791 $alink = "[url=".$parent_item["author-link"]."]".$parent_item["author-name"]."[/url]";
1792 $plink = "[url=".System::baseUrl()."/display/".urlencode($guid)."]".t("status")."[/url]";
1794 return sprintf($bodyverb, $ulink, $alink, $plink);
1798 * @brief Creates a XML object for a "like"
1800 * @param array $importer Array of the importer user
1801 * @param array $parent_item The item array of the parent item
1803 * @return string The XML
1805 private static function construct_like_object($importer, $parent_item) {
1806 $objtype = ACTIVITY_OBJ_NOTE;
1807 $link = '<link rel="alternate" type="text/html" href="'.System::baseUrl()."/display/".$importer["nickname"]."/".$parent_item["id"].'" />';
1808 $parent_body = $parent_item["body"];
1810 $xmldata = array("object" => array("type" => $objtype,
1812 "id" => $parent_item["uri"],
1815 "content" => $parent_body));
1817 return xml::from_array($xmldata, $xml, true);
1821 * @brief Processes "like" messages
1823 * @param array $importer Array of the importer user
1824 * @param string $sender The sender of the message
1825 * @param object $data The message object
1827 * @return int The message id of the generated like or "false" if there was an error
1829 private static function receive_like($importer, $sender, $data) {
1830 $author = notags(unxmlify($data->author));
1831 $guid = notags(unxmlify($data->guid));
1832 $parent_guid = notags(unxmlify($data->parent_guid));
1833 $parent_type = notags(unxmlify($data->parent_type));
1834 $positive = notags(unxmlify($data->positive));
1836 // likes on comments aren't supported by Diaspora - only on posts
1837 // But maybe this will be supported in the future, so we will accept it.
1838 if (!in_array($parent_type, array("Post", "Comment")))
1841 $contact = self::allowed_contact_by_handle($importer, $sender, true);
1845 $message_id = self::message_exists($importer["uid"], $guid);
1849 $parent_item = self::parent_item($importer["uid"], $parent_guid, $author, $contact);
1853 $person = self::person_by_handle($author);
1854 if (!is_array($person)) {
1855 logger("unable to find author details");
1859 // Fetch the contact id - if we know this contact
1860 $author_contact = self::author_contact_by_url($contact, $person, $importer["uid"]);
1862 // "positive" = "false" would be a Dislike - wich isn't currently supported by Diaspora
1863 // We would accept this anyhow.
1864 if ($positive == "true")
1865 $verb = ACTIVITY_LIKE;
1867 $verb = ACTIVITY_DISLIKE;
1869 $datarray = array();
1871 $datarray["protocol"] = PROTOCOL_DIASPORA;
1873 $datarray["uid"] = $importer["uid"];
1874 $datarray["contact-id"] = $author_contact["cid"];
1875 $datarray["network"] = $author_contact["network"];
1877 $datarray["author-name"] = $person["name"];
1878 $datarray["author-link"] = $person["url"];
1879 $datarray["author-avatar"] = ((x($person,"thumb")) ? $person["thumb"] : $person["photo"]);
1881 $datarray["owner-name"] = $contact["name"];
1882 $datarray["owner-link"] = $contact["url"];
1883 $datarray["owner-avatar"] = ((x($contact,"thumb")) ? $contact["thumb"] : $contact["photo"]);
1885 $datarray["guid"] = $guid;
1886 $datarray["uri"] = self::get_uri_from_guid($author, $guid);
1888 $datarray["type"] = "activity";
1889 $datarray["verb"] = $verb;
1890 $datarray["gravity"] = GRAVITY_LIKE;
1891 $datarray["parent-uri"] = $parent_item["uri"];
1893 $datarray["object-type"] = ACTIVITY_OBJ_NOTE;
1894 $datarray["object"] = self::construct_like_object($importer, $parent_item);
1896 $datarray["body"] = self::construct_like_body($contact, $parent_item, $guid);
1898 $message_id = item_store($datarray);
1900 if ($message_id <= 0) {
1905 logger("Stored like ".$datarray["guid"]." with message id ".$message_id, LOGGER_DEBUG);
1908 // like on comments have the comment as parent. So we need to fetch the toplevel parent
1909 if ($parent_item["id"] != $parent_item["parent"]) {
1910 $toplevel = dba::select('item', array('origin'), array('id' => $parent_item["parent"]), array('limit' => 1));
1911 $origin = $toplevel["origin"];
1913 $origin = $parent_item["origin"];
1916 // If we are the origin of the parent we store the original data and notify our followers
1917 if ($message_id && $origin) {
1919 // Formerly we stored the signed text, the signature and the author in different fields.
1920 // We now store the raw data so that we are more flexible.
1921 dba::insert('sign', array('iid' => $message_id, 'signed_text' => json_encode($data)));
1924 Worker::add(PRIORITY_HIGH, "notifier", "comment-import", $message_id);
1931 * @brief Processes private messages
1933 * @param array $importer Array of the importer user
1934 * @param object $data The message object
1936 * @return bool Success?
1938 private static function receive_message($importer, $data) {
1939 $author = notags(unxmlify($data->author));
1940 $guid = notags(unxmlify($data->guid));
1941 $conversation_guid = notags(unxmlify($data->conversation_guid));
1942 $text = unxmlify($data->text);
1943 $created_at = datetime_convert("UTC", "UTC", notags(unxmlify($data->created_at)));
1945 $contact = self::allowed_contact_by_handle($importer, $author, true);
1950 $conversation = null;
1952 $c = q("SELECT * FROM `conv` WHERE `uid` = %d AND `guid` = '%s' LIMIT 1",
1953 intval($importer["uid"]),
1954 dbesc($conversation_guid)
1957 $conversation = $c[0];
1959 logger("conversation not available.");
1963 $message_uri = $author.":".$guid;
1965 $person = self::person_by_handle($author);
1967 logger("unable to find author details");
1971 $body = diaspora2bb($text);
1973 $body = self::replace_people_guid($body, $person["url"]);
1977 $r = q("SELECT `id` FROM `mail` WHERE `guid` = '%s' AND `uid` = %d LIMIT 1",
1979 intval($importer["uid"])
1981 if (DBM::is_result($r)) {
1982 logger("duplicate message already delivered.", LOGGER_DEBUG);
1986 q("INSERT INTO `mail` (`uid`, `guid`, `convid`, `from-name`,`from-photo`,`from-url`,`contact-id`,`title`,`body`,`seen`,`reply`,`uri`,`parent-uri`,`created`)
1987 VALUES ( %d, '%s', %d, '%s', '%s', '%s', %d, '%s', '%s', %d, %d, '%s','%s','%s')",
1988 intval($importer["uid"]),
1990 intval($conversation["id"]),
1991 dbesc($person["name"]),
1992 dbesc($person["photo"]),
1993 dbesc($person["url"]),
1994 intval($contact["id"]),
1995 dbesc($conversation["subject"]),
1999 dbesc($message_uri),
2000 dbesc($author.":".$conversation["guid"]),
2006 dba::update('conv', array('updated' => datetime_convert()), array('id' => $conversation["id"]));
2011 * @brief Processes participations - unsupported by now
2013 * @param array $importer Array of the importer user
2014 * @param object $data The message object
2016 * @return bool always true
2018 private static function receive_participation($importer, $data) {
2019 // I'm not sure if we can fully support this message type
2024 * @brief Processes photos - unneeded
2026 * @param array $importer Array of the importer user
2027 * @param object $data The message object
2029 * @return bool always true
2031 private static function receive_photo($importer, $data) {
2032 // There doesn't seem to be a reason for this function, since the photo data is transmitted in the status message as well
2037 * @brief Processes poll participations - unssupported
2039 * @param array $importer Array of the importer user
2040 * @param object $data The message object
2042 * @return bool always true
2044 private static function receive_poll_participation($importer, $data) {
2045 // We don't support polls by now
2050 * @brief Processes incoming profile updates
2052 * @param array $importer Array of the importer user
2053 * @param object $data The message object
2055 * @return bool Success
2057 private static function receive_profile($importer, $data) {
2058 $author = strtolower(notags(unxmlify($data->author)));
2060 $contact = self::contact_by_handle($importer["uid"], $author);
2064 $name = unxmlify($data->first_name).((strlen($data->last_name)) ? " ".unxmlify($data->last_name) : "");
2065 $image_url = unxmlify($data->image_url);
2066 $birthday = unxmlify($data->birthday);
2067 $gender = unxmlify($data->gender);
2068 $about = diaspora2bb(unxmlify($data->bio));
2069 $location = diaspora2bb(unxmlify($data->location));
2070 $searchable = (unxmlify($data->searchable) == "true");
2071 $nsfw = (unxmlify($data->nsfw) == "true");
2072 $tags = unxmlify($data->tag_string);
2074 $tags = explode("#", $tags);
2076 $keywords = array();
2077 foreach ($tags as $tag) {
2078 $tag = trim(strtolower($tag));
2083 $keywords = implode(", ", $keywords);
2085 $handle_parts = explode("@", $author);
2086 $nick = $handle_parts[0];
2089 $name = $handle_parts[0];
2091 if ( preg_match("|^https?://|", $image_url) === 0)
2092 $image_url = "http://".$handle_parts[1].$image_url;
2094 update_contact_avatar($image_url, $importer["uid"], $contact["id"]);
2096 // Generic birthday. We don't know the timezone. The year is irrelevant.
2098 $birthday = str_replace("1000", "1901", $birthday);
2100 if ($birthday != "")
2101 $birthday = datetime_convert("UTC", "UTC", $birthday, "Y-m-d");
2103 // this is to prevent multiple birthday notifications in a single year
2104 // if we already have a stored birthday and the 'm-d' part hasn't changed, preserve the entry, which will preserve the notify year
2106 if (substr($birthday,5) === substr($contact["bd"],5))
2107 $birthday = $contact["bd"];
2109 $r = q("UPDATE `contact` SET `name` = '%s', `nick` = '%s', `addr` = '%s', `name-date` = '%s', `bd` = '%s',
2110 `location` = '%s', `about` = '%s', `keywords` = '%s', `gender` = '%s' WHERE `id` = %d AND `uid` = %d",
2114 dbesc(datetime_convert()),
2120 intval($contact["id"]),
2121 intval($importer["uid"])
2124 $gcontact = array("url" => $contact["url"], "network" => NETWORK_DIASPORA, "generation" => 2,
2125 "photo" => $image_url, "name" => $name, "location" => $location,
2126 "about" => $about, "birthday" => $birthday, "gender" => $gender,
2127 "addr" => $author, "nick" => $nick, "keywords" => $keywords,
2128 "hide" => !$searchable, "nsfw" => $nsfw);
2130 $gcid = update_gcontact($gcontact);
2132 link_gcontact($gcid, $importer["uid"], $contact["id"]);
2134 logger("Profile of contact ".$contact["id"]." stored for user ".$importer["uid"], LOGGER_DEBUG);
2140 * @brief Processes incoming friend requests
2142 * @param array $importer Array of the importer user
2143 * @param array $contact The contact that send the request
2145 private static function receive_request_make_friend($importer, $contact) {
2149 if ($contact["rel"] == CONTACT_IS_SHARING) {
2150 dba::update('contact', array('rel' => CONTACT_IS_FRIEND, 'writable' => true),
2151 array('id' => $contact["id"], 'uid' => $importer["uid"]));
2153 // send notification
2155 $r = q("SELECT `hide-friends` FROM `profile` WHERE `uid` = %d AND `is-default` = 1 LIMIT 1",
2156 intval($importer["uid"])
2159 if ($r && !$r[0]["hide-friends"] && !$contact["hidden"] && intval(PConfig::get($importer["uid"], "system", "post_newfriend"))) {
2161 $self = q("SELECT * FROM `contact` WHERE `self` AND `uid` = %d LIMIT 1",
2162 intval($importer["uid"])
2165 // they are not CONTACT_IS_FOLLOWER anymore but that's what we have in the array
2167 if ($self && $contact["rel"] == CONTACT_IS_FOLLOWER) {
2170 $arr["protocol"] = PROTOCOL_DIASPORA;
2171 $arr["uri"] = $arr["parent-uri"] = item_new_uri($a->get_hostname(), $importer["uid"]);
2172 $arr["uid"] = $importer["uid"];
2173 $arr["contact-id"] = $self[0]["id"];
2175 $arr["type"] = 'wall';
2176 $arr["gravity"] = 0;
2178 $arr["author-name"] = $arr["owner-name"] = $self[0]["name"];
2179 $arr["author-link"] = $arr["owner-link"] = $self[0]["url"];
2180 $arr["author-avatar"] = $arr["owner-avatar"] = $self[0]["thumb"];
2181 $arr["verb"] = ACTIVITY_FRIEND;
2182 $arr["object-type"] = ACTIVITY_OBJ_PERSON;
2184 $A = "[url=".$self[0]["url"]."]".$self[0]["name"]."[/url]";
2185 $B = "[url=".$contact["url"]."]".$contact["name"]."[/url]";
2186 $BPhoto = "[url=".$contact["url"]."][img]".$contact["thumb"]."[/img][/url]";
2187 $arr["body"] = sprintf(t("%1$s is now friends with %2$s"), $A, $B)."\n\n\n".$Bphoto;
2189 $arr["object"] = self::construct_new_friend_object($contact);
2191 $arr["last-child"] = 1;
2193 $arr["allow_cid"] = $user[0]["allow_cid"];
2194 $arr["allow_gid"] = $user[0]["allow_gid"];
2195 $arr["deny_cid"] = $user[0]["deny_cid"];
2196 $arr["deny_gid"] = $user[0]["deny_gid"];
2198 $i = item_store($arr);
2200 Worker::add(PRIORITY_HIGH, "notifier", "activity", $i);
2206 * @brief Creates a XML object for a "new friend" message
2208 * @param array $contact Array of the contact
2210 * @return string The XML
2212 private static function construct_new_friend_object($contact) {
2213 $objtype = ACTIVITY_OBJ_PERSON;
2214 $link = '<link rel="alternate" type="text/html" href="'.$contact["url"].'" />'."\n".
2215 '<link rel="photo" type="image/jpeg" href="'.$contact["thumb"].'" />'."\n";
2217 $xmldata = array("object" => array("type" => $objtype,
2218 "title" => $contact["name"],
2219 "id" => $contact["url"]."/".$contact["name"],
2222 return xml::from_array($xmldata, $xml, true);
2226 * @brief Processes incoming sharing notification
2228 * @param array $importer Array of the importer user
2229 * @param object $data The message object
2231 * @return bool Success
2233 private static function receive_contact_request($importer, $data) {
2234 $author = unxmlify($data->author);
2235 $recipient = unxmlify($data->recipient);
2237 if (!$author || !$recipient) {
2241 // the current protocol version doesn't know these fields
2242 // That means that we will assume their existance
2243 if (isset($data->following)) {
2244 $following = (unxmlify($data->following) == "true");
2249 if (isset($data->sharing)) {
2250 $sharing = (unxmlify($data->sharing) == "true");
2255 $contact = self::contact_by_handle($importer["uid"],$author);
2257 // perhaps we were already sharing with this person. Now they're sharing with us.
2258 // That makes us friends.
2261 logger("Author ".$author." (Contact ".$contact["id"].") wants to follow us.", LOGGER_DEBUG);
2262 self::receive_request_make_friend($importer, $contact);
2264 // refetch the contact array
2265 $contact = self::contact_by_handle($importer["uid"],$author);
2267 // If we are now friends, we are sending a share message.
2268 // Normally we needn't to do so, but the first message could have been vanished.
2269 if (in_array($contact["rel"], array(CONTACT_IS_FRIEND))) {
2270 $u = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1", intval($importer["uid"]));
2272 logger("Sending share message to author ".$author." - Contact: ".$contact["id"]." - User: ".$importer["uid"], LOGGER_DEBUG);
2273 $ret = self::send_share($u[0], $contact);
2278 logger("Author ".$author." doesn't want to follow us anymore.", LOGGER_DEBUG);
2279 lose_follower($importer, $contact);
2284 if (!$following && $sharing && in_array($importer["page-flags"], array(PAGE_SOAPBOX, PAGE_NORMAL))) {
2285 logger("Author ".$author." wants to share with us - but doesn't want to listen. Request is ignored.", LOGGER_DEBUG);
2287 } elseif (!$following && !$sharing) {
2288 logger("Author ".$author." doesn't want anything - and we don't know the author. Request is ignored.", LOGGER_DEBUG);
2290 } elseif (!$following && $sharing) {
2291 logger("Author ".$author." wants to share with us.", LOGGER_DEBUG);
2292 } elseif ($following && $sharing) {
2293 logger("Author ".$author." wants to have a bidirectional conection.", LOGGER_DEBUG);
2294 } elseif ($following && !$sharing) {
2295 logger("Author ".$author." wants to listen to us.", LOGGER_DEBUG);
2298 $ret = self::person_by_handle($author);
2300 if (!$ret || ($ret["network"] != NETWORK_DIASPORA)) {
2301 logger("Cannot resolve diaspora handle ".$author." for ".$recipient);
2305 $batch = (($ret["batch"]) ? $ret["batch"] : implode("/", array_slice(explode("/", $ret["url"]), 0, 3))."/receive/public");
2307 $r = q("INSERT INTO `contact` (`uid`, `network`,`addr`,`created`,`url`,`nurl`,`batch`,`name`,`nick`,`photo`,`pubkey`,`notify`,`poll`,`blocked`,`priority`)
2308 VALUES (%d, '%s', '%s', '%s', '%s','%s','%s','%s','%s','%s','%s','%s','%s',%d,%d)",
2309 intval($importer["uid"]),
2310 dbesc($ret["network"]),
2311 dbesc($ret["addr"]),
2314 dbesc(normalise_link($ret["url"])),
2316 dbesc($ret["name"]),
2317 dbesc($ret["nick"]),
2318 dbesc($ret["photo"]),
2319 dbesc($ret["pubkey"]),
2320 dbesc($ret["notify"]),
2321 dbesc($ret["poll"]),
2326 // find the contact record we just created
2328 $contact_record = self::contact_by_handle($importer["uid"],$author);
2330 if (!$contact_record) {
2331 logger("unable to locate newly created contact record.");
2335 logger("Author ".$author." was added as contact number ".$contact_record["id"].".", LOGGER_DEBUG);
2337 $def_gid = get_default_group($importer['uid'], $ret["network"]);
2339 if (intval($def_gid))
2340 group_add_member($importer["uid"], "", $contact_record["id"], $def_gid);
2342 update_contact_avatar($ret["photo"], $importer['uid'], $contact_record["id"], true);
2344 if ($importer["page-flags"] == PAGE_NORMAL) {
2346 logger("Sending intra message for author ".$author.".", LOGGER_DEBUG);
2348 $hash = random_string().(string)time(); // Generate a confirm_key
2350 $ret = q("INSERT INTO `intro` (`uid`, `contact-id`, `blocked`, `knowyou`, `note`, `hash`, `datetime`)
2351 VALUES (%d, %d, %d, %d, '%s', '%s', '%s')",
2352 intval($importer["uid"]),
2353 intval($contact_record["id"]),
2356 dbesc(t("Sharing notification from Diaspora network")),
2358 dbesc(datetime_convert())
2362 // automatic friend approval
2364 logger("Does an automatic friend approval for author ".$author.".", LOGGER_DEBUG);
2366 update_contact_avatar($contact_record["photo"],$importer["uid"],$contact_record["id"]);
2368 // technically they are sharing with us (CONTACT_IS_SHARING),
2369 // but if our page-type is PAGE_COMMUNITY or PAGE_SOAPBOX
2370 // we are going to change the relationship and make them a follower.
2372 if (($importer["page-flags"] == PAGE_FREELOVE) && $sharing && $following)
2373 $new_relation = CONTACT_IS_FRIEND;
2374 elseif (($importer["page-flags"] == PAGE_FREELOVE) && $sharing)
2375 $new_relation = CONTACT_IS_SHARING;
2377 $new_relation = CONTACT_IS_FOLLOWER;
2379 $r = q("UPDATE `contact` SET `rel` = %d,
2387 intval($new_relation),
2388 dbesc(datetime_convert()),
2389 dbesc(datetime_convert()),
2390 intval($contact_record["id"])
2393 $u = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1", intval($importer["uid"]));
2395 logger("Sending share message (Relation: ".$new_relation.") to author ".$author." - Contact: ".$contact_record["id"]." - User: ".$importer["uid"], LOGGER_DEBUG);
2396 $ret = self::send_share($u[0], $contact_record);
2398 // Send the profile data, maybe it weren't transmitted before
2399 self::send_profile($importer["uid"], array($contact_record));
2407 * @brief Fetches a message with a given guid
2409 * @param string $guid message guid
2410 * @param string $orig_author handle of the original post
2411 * @param string $author handle of the sharer
2413 * @return array The fetched item
2415 private static function original_item($guid, $orig_author, $author) {
2417 // Do we already have this item?
2418 $r = q("SELECT `body`, `tag`, `app`, `created`, `object-type`, `uri`, `guid`,
2419 `author-name`, `author-link`, `author-avatar`
2420 FROM `item` WHERE `guid` = '%s' AND `visible` AND NOT `deleted` AND `body` != '' LIMIT 1",
2423 if (DBM::is_result($r)) {
2424 logger("reshared message ".$guid." already exists on system.");
2426 // Maybe it is already a reshared item?
2427 // Then refetch the content, if it is a reshare from a reshare.
2428 // If it is a reshared post from another network then reformat to avoid display problems with two share elements
2429 if (self::is_reshare($r[0]["body"], true)) {
2431 } elseif (self::is_reshare($r[0]["body"], false) || strstr($r[0]["body"], "[share")) {
2432 $r[0]["body"] = diaspora2bb(bb2diaspora($r[0]["body"]));
2434 $r[0]["body"] = self::replace_people_guid($r[0]["body"], $r[0]["author-link"]);
2436 // Add OEmbed and other information to the body
2437 $r[0]["body"] = add_page_info_to_body($r[0]["body"], false, true);
2445 if (!DBM::is_result($r)) {
2446 $server = "https://".substr($orig_author, strpos($orig_author, "@") + 1);
2447 logger("1st try: reshared message ".$guid." will be fetched via SSL from the server ".$server);
2448 $item_id = self::store_by_guid($guid, $server);
2451 $server = "http://".substr($orig_author, strpos($orig_author, "@") + 1);
2452 logger("2nd try: reshared message ".$guid." will be fetched without SLL from the server ".$server);
2453 $item_id = self::store_by_guid($guid, $server);
2457 $r = q("SELECT `body`, `tag`, `app`, `created`, `object-type`, `uri`, `guid`,
2458 `author-name`, `author-link`, `author-avatar`
2459 FROM `item` WHERE `id` = %d AND `visible` AND NOT `deleted` AND `body` != '' LIMIT 1",
2462 if (DBM::is_result($r)) {
2463 // If it is a reshared post from another network then reformat to avoid display problems with two share elements
2464 if (self::is_reshare($r[0]["body"], false)) {
2465 $r[0]["body"] = diaspora2bb(bb2diaspora($r[0]["body"]));
2466 $r[0]["body"] = self::replace_people_guid($r[0]["body"], $r[0]["author-link"]);
2478 * @brief Processes a reshare message
2480 * @param array $importer Array of the importer user
2481 * @param object $data The message object
2482 * @param string $xml The original XML of the message
2484 * @return int the message id
2486 private static function receive_reshare($importer, $data, $xml) {
2487 $author = notags(unxmlify($data->author));
2488 $guid = notags(unxmlify($data->guid));
2489 $created_at = datetime_convert("UTC", "UTC", notags(unxmlify($data->created_at)));
2490 $root_author = notags(unxmlify($data->root_author));
2491 $root_guid = notags(unxmlify($data->root_guid));
2492 /// @todo handle unprocessed property "provider_display_name"
2493 $public = notags(unxmlify($data->public));
2495 $contact = self::allowed_contact_by_handle($importer, $author, false);
2500 $message_id = self::message_exists($importer["uid"], $guid);
2505 $original_item = self::original_item($root_guid, $root_author, $author);
2506 if (!$original_item) {
2510 $orig_url = System::baseUrl()."/display/".$original_item["guid"];
2512 $datarray = array();
2514 $datarray["uid"] = $importer["uid"];
2515 $datarray["contact-id"] = $contact["id"];
2516 $datarray["network"] = NETWORK_DIASPORA;
2518 $datarray["author-name"] = $contact["name"];
2519 $datarray["author-link"] = $contact["url"];
2520 $datarray["author-avatar"] = ((x($contact,"thumb")) ? $contact["thumb"] : $contact["photo"]);
2522 $datarray["owner-name"] = $datarray["author-name"];
2523 $datarray["owner-link"] = $datarray["author-link"];
2524 $datarray["owner-avatar"] = $datarray["author-avatar"];
2526 $datarray["guid"] = $guid;
2527 $datarray["uri"] = $datarray["parent-uri"] = self::get_uri_from_guid($author, $guid);
2529 $datarray["verb"] = ACTIVITY_POST;
2530 $datarray["gravity"] = GRAVITY_PARENT;
2532 $datarray["protocol"] = PROTOCOL_DIASPORA;
2533 $datarray["source"] = $xml;
2535 $prefix = share_header($original_item["author-name"], $original_item["author-link"], $original_item["author-avatar"],
2536 $original_item["guid"], $original_item["created"], $orig_url);
2537 $datarray["body"] = $prefix.$original_item["body"]."[/share]";
2539 $datarray["tag"] = $original_item["tag"];
2540 $datarray["app"] = $original_item["app"];
2542 $datarray["plink"] = self::plink($author, $guid);
2543 $datarray["private"] = (($public == "false") ? 1 : 0);
2544 $datarray["changed"] = $datarray["created"] = $datarray["edited"] = $created_at;
2546 $datarray["object-type"] = $original_item["object-type"];
2548 self::fetch_guid($datarray);
2549 $message_id = item_store($datarray);
2552 logger("Stored reshare ".$datarray["guid"]." with message id ".$message_id, LOGGER_DEBUG);
2560 * @brief Processes retractions
2562 * @param array $importer Array of the importer user
2563 * @param array $contact The contact of the item owner
2564 * @param object $data The message object
2566 * @return bool success
2568 private static function item_retraction($importer, $contact, $data) {
2569 $author = notags(unxmlify($data->author));
2570 $target_guid = notags(unxmlify($data->target_guid));
2571 $target_type = notags(unxmlify($data->target_type));
2573 $person = self::person_by_handle($author);
2574 if (!is_array($person)) {
2575 logger("unable to find author detail for ".$author);
2579 if (empty($contact["url"])) {
2580 $contact["url"] = $person["url"];
2583 // Fetch items that are about to be deleted
2584 $fields = array('uid', 'id', 'parent', 'parent-uri', 'author-link');
2586 // When we receive a public retraction, we delete every item that we find.
2587 if ($importer['uid'] == 0) {
2588 $condition = array("`guid` = ? AND NOT `file` LIKE '%%[%%' AND NOT `deleted`", $target_guid);
2590 $condition = array("`guid` = ? AND `uid` = ? AND NOT `file` LIKE '%%[%%' AND NOT `deleted`", $target_guid, $importer['uid']);
2592 $r = dba::select('item', $fields, $condition);
2593 if (!DBM::is_result($r)) {
2594 logger("Target guid ".$target_guid." was not found on this system for user ".$importer['uid'].".");
2598 while ($item = dba::fetch($r)) {
2599 // Fetch the parent item
2600 $parent = dba::select('item', array('author-link', 'origin'), array('id' => $item["parent"]), array('limit' => 1));
2602 // Only delete it if the parent author really fits
2603 if (!link_compare($parent["author-link"], $contact["url"]) && !link_compare($item["author-link"], $contact["url"])) {
2604 logger("Thread author ".$parent["author-link"]." and item author ".$item["author-link"]." don't fit to expected contact ".$contact["url"], LOGGER_DEBUG);
2608 // Currently we don't have a central deletion function that we could use in this case. The function "item_drop" doesn't work for that case
2609 dba::update('item', array('deleted' => true, 'title' => '', 'body' => '',
2610 'edited' => datetime_convert(), 'changed' => datetime_convert()),
2611 array('id' => $item["id"]));
2613 // Delete the thread - if it is a starting post and not a comment
2614 if ($target_type != 'Comment') {
2615 delete_thread($item["id"], $item["parent-uri"]);
2618 logger("Deleted target ".$target_guid." (".$item["id"].") from user ".$item["uid"]." parent: ".$item["parent"], LOGGER_DEBUG);
2620 // Now check if the retraction needs to be relayed by us
2621 if ($parent["origin"]) {
2623 Worker::add(PRIORITY_HIGH, "notifier", "drop", $item["id"]);
2631 * @brief Receives retraction messages
2633 * @param array $importer Array of the importer user
2634 * @param string $sender The sender of the message
2635 * @param object $data The message object
2637 * @return bool Success
2639 private static function receive_retraction($importer, $sender, $data) {
2640 $target_type = notags(unxmlify($data->target_type));
2642 $contact = self::contact_by_handle($importer["uid"], $sender);
2643 if (!$contact && (in_array($target_type, array("Contact", "Person")))) {
2644 logger("cannot find contact for sender: ".$sender." and user ".$importer["uid"]);
2648 logger("Got retraction for ".$target_type.", sender ".$sender." and user ".$importer["uid"], LOGGER_DEBUG);
2650 switch ($target_type) {
2655 case "StatusMessage":
2656 return self::item_retraction($importer, $contact, $data);
2660 /// @todo What should we do with an "unshare"?
2661 // Removing the contact isn't correct since we still can read the public items
2662 contact_remove($contact["id"]);
2666 logger("Unknown target type ".$target_type);
2673 * @brief Receives status messages
2675 * @param array $importer Array of the importer user
2676 * @param object $data The message object
2677 * @param string $xml The original XML of the message
2679 * @return int The message id of the newly created item
2681 private static function receive_status_message($importer, $data, $xml) {
2682 $author = notags(unxmlify($data->author));
2683 $guid = notags(unxmlify($data->guid));
2684 $created_at = datetime_convert("UTC", "UTC", notags(unxmlify($data->created_at)));
2685 $public = notags(unxmlify($data->public));
2686 $text = unxmlify($data->text);
2687 $provider_display_name = notags(unxmlify($data->provider_display_name));
2689 $contact = self::allowed_contact_by_handle($importer, $author, false);
2694 $message_id = self::message_exists($importer["uid"], $guid);
2700 if ($data->location) {
2701 foreach ($data->location->children() AS $fieldname => $data) {
2702 $address[$fieldname] = notags(unxmlify($data));
2706 $body = diaspora2bb($text);
2708 $datarray = array();
2710 // Attach embedded pictures to the body
2712 foreach ($data->photo AS $photo) {
2713 $body = "[img]".unxmlify($photo->remote_photo_path).
2714 unxmlify($photo->remote_photo_name)."[/img]\n".$body;
2717 $datarray["object-type"] = ACTIVITY_OBJ_IMAGE;
2719 $datarray["object-type"] = ACTIVITY_OBJ_NOTE;
2721 // Add OEmbed and other information to the body
2722 if (!self::is_redmatrix($contact["url"])) {
2723 $body = add_page_info_to_body($body, false, true);
2727 /// @todo enable support for polls
2728 //if ($data->poll) {
2729 // foreach ($data->poll AS $poll)
2734 /// @todo enable support for events
2736 $datarray["uid"] = $importer["uid"];
2737 $datarray["contact-id"] = $contact["id"];
2738 $datarray["network"] = NETWORK_DIASPORA;
2740 $datarray["author-name"] = $contact["name"];
2741 $datarray["author-link"] = $contact["url"];
2742 $datarray["author-avatar"] = ((x($contact,"thumb")) ? $contact["thumb"] : $contact["photo"]);
2744 $datarray["owner-name"] = $datarray["author-name"];
2745 $datarray["owner-link"] = $datarray["author-link"];
2746 $datarray["owner-avatar"] = $datarray["author-avatar"];
2748 $datarray["guid"] = $guid;
2749 $datarray["uri"] = $datarray["parent-uri"] = self::get_uri_from_guid($author, $guid);
2751 $datarray["verb"] = ACTIVITY_POST;
2752 $datarray["gravity"] = GRAVITY_PARENT;
2754 $datarray["protocol"] = PROTOCOL_DIASPORA;
2755 $datarray["source"] = $xml;
2757 $datarray["body"] = self::replace_people_guid($body, $contact["url"]);
2759 if ($provider_display_name != "") {
2760 $datarray["app"] = $provider_display_name;
2763 $datarray["plink"] = self::plink($author, $guid);
2764 $datarray["private"] = (($public == "false") ? 1 : 0);
2765 $datarray["changed"] = $datarray["created"] = $datarray["edited"] = $created_at;
2767 if (isset($address["address"])) {
2768 $datarray["location"] = $address["address"];
2771 if (isset($address["lat"]) && isset($address["lng"])) {
2772 $datarray["coord"] = $address["lat"]." ".$address["lng"];
2775 self::fetch_guid($datarray);
2776 $message_id = item_store($datarray);
2779 logger("Stored item ".$datarray["guid"]." with message id ".$message_id, LOGGER_DEBUG);
2786 /* ************************************************************************************** *
2787 * Here are all the functions that are needed to transmit data with the Diaspora protocol *
2788 * ************************************************************************************** */
2791 * @brief returnes the handle of a contact
2793 * @param array $me contact array
2795 * @return string the handle in the format user@domain.tld
2797 private static function my_handle($contact) {
2798 if ($contact["addr"] != "") {
2799 return $contact["addr"];
2802 // Normally we should have a filled "addr" field - but in the past this wasn't the case
2803 // So - just in case - we build the the address here.
2804 if ($contact["nickname"] != "") {
2805 $nick = $contact["nickname"];
2807 $nick = $contact["nick"];
2810 return $nick."@".substr(System::baseUrl(), strpos(System::baseUrl(),"://") + 3);
2815 * @brief Creates the data for a private message in the new format
2817 * @param string $msg The message that is to be transmitted
2818 * @param array $user The record of the sender
2819 * @param array $contact Target of the communication
2820 * @param string $prvkey The private key of the sender
2821 * @param string $pubkey The public key of the receiver
2823 * @return string The encrypted data
2825 public static function encode_private_data($msg, $user, $contact, $prvkey, $pubkey) {
2827 logger("Message: ".$msg, LOGGER_DATA);
2829 // without a public key nothing will work
2831 logger("pubkey missing: contact id: ".$contact["id"]);
2835 $aes_key = openssl_random_pseudo_bytes(32);
2836 $b_aes_key = base64_encode($aes_key);
2837 $iv = openssl_random_pseudo_bytes(16);
2838 $b_iv = base64_encode($iv);
2840 $ciphertext = self::aes_encrypt($aes_key, $iv, $msg);
2842 $json = json_encode(array("iv" => $b_iv, "key" => $b_aes_key));
2844 $encrypted_key_bundle = "";
2845 openssl_public_encrypt($json, $encrypted_key_bundle, $pubkey);
2847 $json_object = json_encode(array("aes_key" => base64_encode($encrypted_key_bundle),
2848 "encrypted_magic_envelope" => base64_encode($ciphertext)));
2850 return $json_object;
2854 * @brief Creates the envelope for the "fetch" endpoint and for the new format
2856 * @param string $msg The message that is to be transmitted
2857 * @param array $user The record of the sender
2859 * @return string The envelope
2861 public static function build_magic_envelope($msg, $user) {
2863 $b64url_data = base64url_encode($msg);
2864 $data = str_replace(array("\n", "\r", " ", "\t"), array("", "", "", ""), $b64url_data);
2866 $key_id = base64url_encode(self::my_handle($user));
2867 $type = "application/xml";
2868 $encoding = "base64url";
2869 $alg = "RSA-SHA256";
2870 $signable_data = $data.".".base64url_encode($type).".".base64url_encode($encoding).".".base64url_encode($alg);
2872 // Fallback if the private key wasn't transmitted in the expected field
2873 if ($user['uprvkey'] == "")
2874 $user['uprvkey'] = $user['prvkey'];
2876 $signature = rsa_sign($signable_data, $user["uprvkey"]);
2877 $sig = base64url_encode($signature);
2879 $xmldata = array("me:env" => array("me:data" => $data,
2880 "@attributes" => array("type" => $type),
2881 "me:encoding" => $encoding,
2884 "@attributes2" => array("key_id" => $key_id)));
2886 $namespaces = array("me" => "http://salmon-protocol.org/ns/magic-env");
2888 return xml::from_array($xmldata, $xml, false, $namespaces);
2892 * @brief Create the envelope for a message
2894 * @param string $msg The message that is to be transmitted
2895 * @param array $user The record of the sender
2896 * @param array $contact Target of the communication
2897 * @param string $prvkey The private key of the sender
2898 * @param string $pubkey The public key of the receiver
2899 * @param bool $public Is the message public?
2901 * @return string The message that will be transmitted to other servers
2903 private static function build_message($msg, $user, $contact, $prvkey, $pubkey, $public = false) {
2905 // The message is put into an envelope with the sender's signature
2906 $envelope = self::build_magic_envelope($msg, $user);
2908 // Private messages are put into a second envelope, encrypted with the receivers public key
2910 $envelope = self::encode_private_data($envelope, $user, $contact, $prvkey, $pubkey);
2917 * @brief Creates a signature for a message
2919 * @param array $owner the array of the owner of the message
2920 * @param array $message The message that is to be signed
2922 * @return string The signature
2924 private static function signature($owner, $message) {
2926 unset($sigmsg["author_signature"]);
2927 unset($sigmsg["parent_author_signature"]);
2929 $signed_text = implode(";", $sigmsg);
2931 return base64_encode(rsa_sign($signed_text, $owner["uprvkey"], "sha256"));
2935 * @brief Transmit a message to a target server
2937 * @param array $owner the array of the item owner
2938 * @param array $contact Target of the communication
2939 * @param string $envelope The message that is to be transmitted
2940 * @param bool $public_batch Is it a public post?
2941 * @param bool $queue_run Is the transmission called from the queue?
2942 * @param string $guid message guid
2944 * @return int Result of the transmission
2946 public static function transmit($owner, $contact, $envelope, $public_batch, $queue_run=false, $guid = "") {
2950 $enabled = intval(Config::get("system", "diaspora_enabled"));
2954 $logid = random_string(4);
2955 $dest_url = (($public_batch) ? $contact["batch"] : $contact["notify"]);
2957 logger("no url for contact: ".$contact["id"]." batch mode =".$public_batch);
2961 logger("transmit: ".$logid."-".$guid." ".$dest_url);
2963 if (!$queue_run && was_recently_delayed($contact["id"])) {
2966 if (!intval(Config::get("system", "diaspora_test"))) {
2967 $content_type = (($public_batch) ? "application/magic-envelope+xml" : "application/json");
2969 post_url($dest_url."/", $envelope, array("Content-Type: ".$content_type));
2970 $return_code = $a->get_curl_code();
2972 logger("test_mode");
2977 logger("transmit: ".$logid."-".$guid." returns: ".$return_code);
2979 if (!$return_code || (($return_code == 503) && (stristr($a->get_curl_headers(), "retry-after")))) {
2980 logger("queue message");
2982 $r = q("SELECT `id` FROM `queue` WHERE `cid` = %d AND `network` = '%s' AND `content` = '%s' AND `batch` = %d LIMIT 1",
2983 intval($contact["id"]),
2984 dbesc(NETWORK_DIASPORA),
2986 intval($public_batch)
2989 logger("add_to_queue ignored - identical item already in queue");
2991 // queue message for redelivery
2992 add_to_queue($contact["id"], NETWORK_DIASPORA, $envelope, $public_batch);
2994 // The message could not be delivered. We mark the contact as "dead"
2995 mark_for_death($contact);
2997 } elseif (($return_code >= 200) && ($return_code <= 299)) {
2998 // We successfully delivered a message, the contact is alive
2999 unmark_for_death($contact);
3002 return(($return_code) ? $return_code : (-1));
3007 * @brief Build the post xml
3009 * @param string $type The message type
3010 * @param array $message The message data
3012 * @return string The post XML
3014 public static function build_post_xml($type, $message) {
3016 $data = array($type => $message);
3018 return xml::from_array($data, $xml);
3022 * @brief Builds and transmit messages
3024 * @param array $owner the array of the item owner
3025 * @param array $contact Target of the communication
3026 * @param string $type The message type
3027 * @param array $message The message data
3028 * @param bool $public_batch Is it a public post?
3029 * @param string $guid message guid
3030 * @param bool $spool Should the transmission be spooled or transmitted?
3032 * @return int Result of the transmission
3034 private static function build_and_transmit($owner, $contact, $type, $message, $public_batch = false, $guid = "", $spool = false) {
3036 $msg = self::build_post_xml($type, $message);
3038 logger('message: '.$msg, LOGGER_DATA);
3039 logger('send guid '.$guid, LOGGER_DEBUG);
3041 // Fallback if the private key wasn't transmitted in the expected field
3042 if ($owner['uprvkey'] == "")
3043 $owner['uprvkey'] = $owner['prvkey'];
3045 $envelope = self::build_message($msg, $owner, $contact, $owner['uprvkey'], $contact['pubkey'], $public_batch);
3048 add_to_queue($contact['id'], NETWORK_DIASPORA, $envelope, $public_batch);
3051 $return_code = self::transmit($owner, $contact, $envelope, $public_batch, false, $guid);
3053 logger("guid: ".$item["guid"]." result ".$return_code, LOGGER_DEBUG);
3055 return $return_code;
3059 * @brief sends an account migration
3061 * @param array $owner the array of the item owner
3062 * @param array $contact Target of the communication
3063 * @param int $uid User ID
3065 * @return int The result of the transmission
3067 public static function sendAccountMigration($owner, $contact, $uid) {
3069 $old_handle = PConfig::get($uid, 'system', 'previous_addr');
3070 $profile = self::createProfileData($uid);
3072 $signed_text = 'AccountMigration:'.$old_handle.':'.$profile['author'];
3073 $signature = base64_encode(rsa_sign($signed_text, $owner["uprvkey"], "sha256"));
3075 $message = array("author" => $old_handle,
3076 "profile" => $profile,
3077 "signature" => $signature);
3079 logger("Send account migration ".print_r($message, true), LOGGER_DEBUG);
3081 return self::build_and_transmit($owner, $contact, "account_migration", $message);
3085 * @brief Sends a "share" message
3087 * @param array $owner the array of the item owner
3088 * @param array $contact Target of the communication
3090 * @return int The result of the transmission
3092 public static function send_share($owner, $contact) {
3095 * @todo support the different possible combinations of "following" and "sharing"
3096 * Currently, Diaspora only interprets the "sharing" field
3098 * Before switching this code productive, we have to check all "send_share" calls if "rel" is set correctly
3102 switch ($contact["rel"]) {
3103 case CONTACT_IS_FRIEND:
3106 case CONTACT_IS_SHARING:
3109 case CONTACT_IS_FOLLOWER:
3115 $message = array("author" => self::my_handle($owner),
3116 "recipient" => $contact["addr"],
3117 "following" => "true",
3118 "sharing" => "true");
3120 logger("Send share ".print_r($message, true), LOGGER_DEBUG);
3122 return self::build_and_transmit($owner, $contact, "contact", $message);
3126 * @brief sends an "unshare"
3128 * @param array $owner the array of the item owner
3129 * @param array $contact Target of the communication
3131 * @return int The result of the transmission
3133 public static function send_unshare($owner, $contact) {
3135 $message = array("author" => self::my_handle($owner),
3136 "recipient" => $contact["addr"],
3137 "following" => "false",
3138 "sharing" => "false");
3140 logger("Send unshare ".print_r($message, true), LOGGER_DEBUG);
3142 return self::build_and_transmit($owner, $contact, "contact", $message);
3146 * @brief Checks a message body if it is a reshare
3148 * @param string $body The message body that is to be check
3149 * @param bool $complete Should it be a complete check or a simple check?
3151 * @return array|bool Reshare details or "false" if no reshare
3153 public static function is_reshare($body, $complete = true) {
3154 $body = trim($body);
3156 // Skip if it isn't a pure repeated messages
3157 // Does it start with a share?
3158 if ((strpos($body, "[share") > 0) && $complete)
3161 // Does it end with a share?
3162 if (strlen($body) > (strrpos($body, "[/share]") + 8))
3165 $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$1",$body);
3166 // Skip if there is no shared message in there
3167 if ($body == $attributes)
3170 // If we don't do the complete check we quit here
3175 preg_match("/guid='(.*?)'/ism", $attributes, $matches);
3176 if ($matches[1] != "")
3177 $guid = $matches[1];
3179 preg_match('/guid="(.*?)"/ism', $attributes, $matches);
3180 if ($matches[1] != "")
3181 $guid = $matches[1];
3184 $r = q("SELECT `contact-id` FROM `item` WHERE `guid` = '%s' AND `network` IN ('%s', '%s') LIMIT 1",
3185 dbesc($guid), NETWORK_DFRN, NETWORK_DIASPORA);
3188 $ret["root_handle"] = self::handle_from_contact($r[0]["contact-id"]);
3189 $ret["root_guid"] = $guid;
3195 preg_match("/profile='(.*?)'/ism", $attributes, $matches);
3196 if ($matches[1] != "")
3197 $profile = $matches[1];
3199 preg_match('/profile="(.*?)"/ism', $attributes, $matches);
3200 if ($matches[1] != "")
3201 $profile = $matches[1];
3205 $ret["root_handle"] = preg_replace("=https?://(.*)/u/(.*)=ism", "$2@$1", $profile);
3206 if (($ret["root_handle"] == $profile) || ($ret["root_handle"] == ""))
3210 preg_match("/link='(.*?)'/ism", $attributes, $matches);
3211 if ($matches[1] != "")
3212 $link = $matches[1];
3214 preg_match('/link="(.*?)"/ism', $attributes, $matches);
3215 if ($matches[1] != "")
3216 $link = $matches[1];
3218 $ret["root_guid"] = preg_replace("=https?://(.*)/posts/(.*)=ism", "$2", $link);
3219 if (($ret["root_guid"] == $link) || (trim($ret["root_guid"]) == ""))
3226 * @brief Create an event array
3228 * @param integer $event_id The id of the event
3230 * @return array with event data
3232 private static function build_event($event_id) {
3234 $r = q("SELECT `guid`, `uid`, `start`, `finish`, `nofinish`, `summary`, `desc`, `location`, `adjust` FROM `event` WHERE `id` = %d", intval($event_id));
3235 if (!DBM::is_result($r)) {
3241 $eventdata = array();
3243 $r = q("SELECT `timezone` FROM `user` WHERE `uid` = %d", intval($event['uid']));
3244 if (!DBM::is_result($r)) {
3250 $r = q("SELECT `addr`, `nick` FROM `contact` WHERE `uid` = %d AND `self`", intval($event['uid']));
3251 if (!DBM::is_result($r)) {
3257 $eventdata['author'] = self::my_handle($owner);
3259 if ($event['guid']) {
3260 $eventdata['guid'] = $event['guid'];
3263 $mask = 'Y-m-d\TH:i:s\Z';
3265 /// @todo - establish "all day" events in Friendica
3266 $eventdata["all_day"] = "false";
3268 if (!$event['adjust']) {
3269 $eventdata['timezone'] = $user['timezone'];
3271 if ($eventdata['timezone'] == "") {
3272 $eventdata['timezone'] = 'UTC';
3276 if ($event['start']) {
3277 $eventdata['start'] = datetime_convert($eventdata['timezone'], "UTC", $event['start'], $mask);
3279 if ($event['finish'] && !$event['nofinish']) {
3280 $eventdata['end'] = datetime_convert($eventdata['timezone'], "UTC", $event['finish'], $mask);
3282 if ($event['summary']) {
3283 $eventdata['summary'] = html_entity_decode(bb2diaspora($event['summary']));
3285 if ($event['desc']) {
3286 $eventdata['description'] = html_entity_decode(bb2diaspora($event['desc']));
3288 if ($event['location']) {
3289 $location = array();
3290 $location["address"] = html_entity_decode(bb2diaspora($event['location']));
3291 $location["lat"] = 0;
3292 $location["lng"] = 0;
3293 $eventdata['location'] = $location;
3300 * @brief Create a post (status message or reshare)
3302 * @param array $item The item that will be exported
3303 * @param array $owner the array of the item owner
3306 * 'type' -> Message type ("status_message" or "reshare")
3307 * 'message' -> Array of XML elements of the status
3309 public static function build_status($item, $owner) {
3311 $cachekey = "diaspora:build_status:".$item['guid'];
3313 $result = Cache::get($cachekey);
3314 if (!is_null($result)) {
3318 $myaddr = self::my_handle($owner);
3320 $public = (($item["private"]) ? "false" : "true");
3322 $created = datetime_convert("UTC", "UTC", $item["created"], 'Y-m-d\TH:i:s\Z');
3324 // Detect a share element and do a reshare
3325 if (!$item['private'] && ($ret = self::is_reshare($item["body"]))) {
3326 $message = array("author" => $myaddr,
3327 "guid" => $item["guid"],
3328 "created_at" => $created,
3329 "root_author" => $ret["root_handle"],
3330 "root_guid" => $ret["root_guid"],
3331 "provider_display_name" => $item["app"],
3332 "public" => $public);
3336 $title = $item["title"];
3337 $body = $item["body"];
3339 // convert to markdown
3340 $body = html_entity_decode(bb2diaspora($body));
3344 $body = "## ".html_entity_decode($title)."\n\n".$body;
3346 if ($item["attach"]) {
3347 $cnt = preg_match_all('/href=\"(.*?)\"(.*?)title=\"(.*?)\"/ism', $item["attach"], $matches, PREG_SET_ORDER);
3349 $body .= "\n".t("Attachments:")."\n";
3350 foreach ($matches as $mtch)
3351 $body .= "[".$mtch[3]."](".$mtch[1].")\n";
3355 $location = array();
3357 if ($item["location"] != "")
3358 $location["address"] = $item["location"];
3360 if ($item["coord"] != "") {
3361 $coord = explode(" ", $item["coord"]);
3362 $location["lat"] = $coord[0];
3363 $location["lng"] = $coord[1];
3366 $message = array("author" => $myaddr,
3367 "guid" => $item["guid"],
3368 "created_at" => $created,
3369 "public" => $public,
3371 "provider_display_name" => $item["app"],
3372 "location" => $location);
3374 // Diaspora rejects messages when they contain a location without "lat" or "lng"
3375 if (!isset($location["lat"]) || !isset($location["lng"])) {
3376 unset($message["location"]);
3379 if ($item['event-id'] > 0) {
3380 $event = self::build_event($item['event-id']);
3381 if (count($event)) {
3382 $message['event'] = $event;
3384 /// @todo Once Diaspora supports it, we will remove the body
3385 // $message['text'] = '';
3389 $type = "status_message";
3392 $msg = array("type" => $type, "message" => $message);
3394 Cache::set($cachekey, $msg, CACHE_QUARTER_HOUR);
3400 * @brief Sends a post
3402 * @param array $item The item that will be exported
3403 * @param array $owner the array of the item owner
3404 * @param array $contact Target of the communication
3405 * @param bool $public_batch Is it a public post?
3407 * @return int The result of the transmission
3409 public static function send_status($item, $owner, $contact, $public_batch = false) {
3411 $status = self::build_status($item, $owner);
3413 return self::build_and_transmit($owner, $contact, $status["type"], $status["message"], $public_batch, $item["guid"]);
3417 * @brief Creates a "like" object
3419 * @param array $item The item that will be exported
3420 * @param array $owner the array of the item owner
3422 * @return array The data for a "like"
3424 private static function construct_like($item, $owner) {
3426 $p = q("SELECT `guid`, `uri`, `parent-uri` FROM `item` WHERE `uri` = '%s' LIMIT 1",
3427 dbesc($item["thr-parent"]));
3428 if (!DBM::is_result($p))
3433 $target_type = ($parent["uri"] === $parent["parent-uri"] ? "Post" : "Comment");
3434 if ($item['verb'] === ACTIVITY_LIKE) {
3436 } elseif ($item['verb'] === ACTIVITY_DISLIKE) {
3437 $positive = "false";
3440 return(array("author" => self::my_handle($owner),
3441 "guid" => $item["guid"],
3442 "parent_guid" => $parent["guid"],
3443 "parent_type" => $target_type,
3444 "positive" => $positive,
3445 "author_signature" => ""));
3449 * @brief Creates an "EventParticipation" object
3451 * @param array $item The item that will be exported
3452 * @param array $owner the array of the item owner
3454 * @return array The data for an "EventParticipation"
3456 private static function construct_attend($item, $owner) {
3458 $p = q("SELECT `guid`, `uri`, `parent-uri` FROM `item` WHERE `uri` = '%s' LIMIT 1",
3459 dbesc($item["thr-parent"]));
3460 if (!DBM::is_result($p))
3465 switch ($item['verb']) {
3466 case ACTIVITY_ATTEND:
3467 $attend_answer = 'accepted';
3469 case ACTIVITY_ATTENDNO:
3470 $attend_answer = 'declined';
3472 case ACTIVITY_ATTENDMAYBE:
3473 $attend_answer = 'tentative';
3476 logger('Unknown verb '.$item['verb'].' in item '.$item['guid']);
3480 return(array("author" => self::my_handle($owner),
3481 "guid" => $item["guid"],
3482 "parent_guid" => $parent["guid"],
3483 "status" => $attend_answer,
3484 "author_signature" => ""));
3488 * @brief Creates the object for a comment
3490 * @param array $item The item that will be exported
3491 * @param array $owner the array of the item owner
3493 * @return array The data for a comment
3495 private static function construct_comment($item, $owner) {
3497 $cachekey = "diaspora:construct_comment:".$item['guid'];
3499 $result = Cache::get($cachekey);
3500 if (!is_null($result)) {
3504 $p = q("SELECT `guid` FROM `item` WHERE `parent` = %d AND `id` = %d LIMIT 1",
3505 intval($item["parent"]),
3506 intval($item["parent"])
3509 if (!DBM::is_result($p))
3514 $text = html_entity_decode(bb2diaspora($item["body"]));
3515 $created = datetime_convert("UTC", "UTC", $item["created"], 'Y-m-d\TH:i:s\Z');
3517 $comment = array("author" => self::my_handle($owner),
3518 "guid" => $item["guid"],
3519 "created_at" => $created,
3520 "parent_guid" => $parent["guid"],
3522 "author_signature" => "");
3524 // Send the thread parent guid only if it is a threaded comment
3525 if ($item['thr-parent'] != $item['parent-uri']) {
3526 $comment['thread_parent_guid'] = self::get_guid_from_uri($item['thr-parent'], $item['uid']);
3529 Cache::set($cachekey, $comment, CACHE_QUARTER_HOUR);
3535 * @brief Send a like or a comment
3537 * @param array $item The item that will be exported
3538 * @param array $owner the array of the item owner
3539 * @param array $contact Target of the communication
3540 * @param bool $public_batch Is it a public post?
3542 * @return int The result of the transmission
3544 public static function send_followup($item,$owner,$contact,$public_batch = false) {
3546 if (in_array($item['verb'], array(ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE))) {
3547 $message = self::construct_attend($item, $owner);
3548 $type = "event_participation";
3549 } elseif (in_array($item["verb"], array(ACTIVITY_LIKE, ACTIVITY_DISLIKE))) {
3550 $message = self::construct_like($item, $owner);
3553 $message = self::construct_comment($item, $owner);
3560 $message["author_signature"] = self::signature($owner, $message);
3562 return self::build_and_transmit($owner, $contact, $type, $message, $public_batch, $item["guid"]);
3566 * @brief Creates a message from a signature record entry
3568 * @param array $item The item that will be exported
3569 * @param array $signature The entry of the "sign" record
3571 * @return string The message
3573 private static function message_from_signature($item, $signature) {
3575 // Split the signed text
3576 $signed_parts = explode(";", $signature['signed_text']);
3578 if ($item["deleted"]) {
3579 $message = array("author" => $signature['signer'],
3580 "target_guid" => $signed_parts[0],
3581 "target_type" => $signed_parts[1]);
3582 } elseif (in_array($item["verb"], array(ACTIVITY_LIKE, ACTIVITY_DISLIKE))) {
3583 $message = array("author" => $signed_parts[4],
3584 "guid" => $signed_parts[1],
3585 "parent_guid" => $signed_parts[3],
3586 "parent_type" => $signed_parts[2],
3587 "positive" => $signed_parts[0],
3588 "author_signature" => $signature['signature'],
3589 "parent_author_signature" => "");
3591 // Remove the comment guid
3592 $guid = array_shift($signed_parts);
3594 // Remove the parent guid
3595 $parent_guid = array_shift($signed_parts);
3597 // Remove the handle
3598 $handle = array_pop($signed_parts);
3600 // Glue the parts together
3601 $text = implode(";", $signed_parts);
3603 $message = array("author" => $handle,
3605 "parent_guid" => $parent_guid,
3606 "text" => implode(";", $signed_parts),
3607 "author_signature" => $signature['signature'],
3608 "parent_author_signature" => "");
3614 * @brief Relays messages (like, comment, retraction) to other servers if we are the thread owner
3616 * @param array $item The item that will be exported
3617 * @param array $owner the array of the item owner
3618 * @param array $contact Target of the communication
3619 * @param bool $public_batch Is it a public post?
3621 * @return int The result of the transmission
3623 public static function send_relay($item, $owner, $contact, $public_batch = false) {
3625 if ($item["deleted"]) {
3626 return self::send_retraction($item, $owner, $contact, $public_batch, true);
3627 } elseif (in_array($item["verb"], array(ACTIVITY_LIKE, ACTIVITY_DISLIKE))) {
3633 logger("Got relayable data ".$type." for item ".$item["guid"]." (".$item["id"].")", LOGGER_DEBUG);
3635 // fetch the original signature
3637 $r = q("SELECT `signed_text`, `signature`, `signer` FROM `sign` WHERE `iid` = %d LIMIT 1",
3638 intval($item["id"]));
3641 logger("Couldn't fetch signatur for item ".$item["guid"]." (".$item["id"].")", LOGGER_DEBUG);
3647 // Old way - is used by the internal Friendica functions
3648 /// @todo Change all signatur storing functions to the new format
3649 if ($signature['signed_text'] && $signature['signature'] && $signature['signer'])
3650 $message = self::message_from_signature($item, $signature);
3652 $msg = json_decode($signature['signed_text'], true);
3655 if (is_array($msg)) {
3656 foreach ($msg AS $field => $data) {
3657 if (!$item["deleted"]) {
3658 if ($field == "diaspora_handle") {
3661 if ($field == "target_type") {
3662 $field = "parent_type";
3666 $message[$field] = $data;
3669 logger("Signature text for item ".$item["guid"]." (".$item["id"].") couldn't be extracted: ".$signature['signed_text'], LOGGER_DEBUG);
3672 $message["parent_author_signature"] = self::signature($owner, $message);
3674 logger("Relayed data ".print_r($message, true), LOGGER_DEBUG);
3676 return self::build_and_transmit($owner, $contact, $type, $message, $public_batch, $item["guid"]);
3680 * @brief Sends a retraction (deletion) of a message, like or comment
3682 * @param array $item The item that will be exported
3683 * @param array $owner the array of the item owner
3684 * @param array $contact Target of the communication
3685 * @param bool $public_batch Is it a public post?
3686 * @param bool $relay Is the retraction transmitted from a relay?
3688 * @return int The result of the transmission
3690 public static function send_retraction($item, $owner, $contact, $public_batch = false, $relay = false) {
3692 $itemaddr = self::handle_from_contact($item["contact-id"], $item["gcontact-id"]);
3694 $msg_type = "retraction";
3696 if ($item['id'] == $item['parent']) {
3697 $target_type = "Post";
3698 } elseif (in_array($item["verb"], array(ACTIVITY_LIKE, ACTIVITY_DISLIKE))) {
3699 $target_type = "Like";
3701 $target_type = "Comment";
3704 $message = array("author" => $itemaddr,
3705 "target_guid" => $item['guid'],
3706 "target_type" => $target_type);
3708 logger("Got message ".print_r($message, true), LOGGER_DEBUG);
3710 return self::build_and_transmit($owner, $contact, $msg_type, $message, $public_batch, $item["guid"]);
3714 * @brief Sends a mail
3716 * @param array $item The item that will be exported
3717 * @param array $owner The owner
3718 * @param array $contact Target of the communication
3720 * @return int The result of the transmission
3722 public static function send_mail($item, $owner, $contact) {
3724 $myaddr = self::my_handle($owner);
3726 $r = q("SELECT * FROM `conv` WHERE `id` = %d AND `uid` = %d LIMIT 1",
3727 intval($item["convid"]),
3728 intval($item["uid"])
3731 if (!DBM::is_result($r)) {
3732 logger("conversation not found.");
3738 "author" => $cnv["creator"],
3739 "guid" => $cnv["guid"],
3740 "subject" => $cnv["subject"],
3741 "created_at" => datetime_convert("UTC", "UTC", $cnv['created'], 'Y-m-d\TH:i:s\Z'),
3742 "participants" => $cnv["recips"]
3745 $body = bb2diaspora($item["body"]);
3746 $created = datetime_convert("UTC", "UTC", $item["created"], 'Y-m-d\TH:i:s\Z');
3749 "author" => $myaddr,
3750 "guid" => $item["guid"],
3751 "conversation_guid" => $cnv["guid"],
3753 "created_at" => $created,
3756 if ($item["reply"]) {
3761 "author" => $cnv["creator"],
3762 "guid" => $cnv["guid"],
3763 "subject" => $cnv["subject"],
3764 "created_at" => datetime_convert("UTC", "UTC", $cnv['created'], 'Y-m-d\TH:i:s\Z'),
3765 "participants" => $cnv["recips"],
3768 $type = "conversation";
3771 return self::build_and_transmit($owner, $contact, $type, $message, false, $item["guid"]);
3775 * @brief Create profile data
3777 * @param int $uid The user id
3779 * @return array The profile data
3781 private static function createProfileData($uid) {
3782 $r = q("SELECT `profile`.`uid` AS `profile_uid`, `profile`.* , `user`.*, `user`.`prvkey` AS `uprvkey`, `contact`.`addr`
3784 INNER JOIN `user` ON `profile`.`uid` = `user`.`uid`
3785 INNER JOIN `contact` ON `profile`.`uid` = `contact`.`uid`
3786 WHERE `user`.`uid` = %d AND `profile`.`is-default` AND `contact`.`self` LIMIT 1",
3796 $handle = $profile["addr"];
3797 $first = ((strpos($profile['name'],' ')
3798 ? trim(substr($profile['name'],0,strpos($profile['name'],' '))) : $profile['name']));
3799 $last = (($first === $profile['name']) ? '' : trim(substr($profile['name'], strlen($first))));
3800 $large = System::baseUrl().'/photo/custom/300/'.$profile['uid'].'.jpg';
3801 $medium = System::baseUrl().'/photo/custom/100/'.$profile['uid'].'.jpg';
3802 $small = System::baseUrl().'/photo/custom/50/' .$profile['uid'].'.jpg';
3803 $searchable = (($profile['publish'] && $profile['net-publish']) ? 'true' : 'false');
3805 if ($searchable === 'true') {
3806 $dob = '1000-00-00';
3808 if (($profile['dob']) && ($profile['dob'] > '0001-01-01'))
3809 $dob = ((intval($profile['dob'])) ? intval($profile['dob']) : '1000') .'-'. datetime_convert('UTC','UTC',$profile['dob'],'m-d');
3811 $about = $profile['about'];
3812 $about = strip_tags(bbcode($about));
3814 $location = formatted_location($profile);
3816 if ($profile['pub_keywords']) {
3817 $kw = str_replace(',',' ',$profile['pub_keywords']);
3818 $kw = str_replace(' ',' ',$kw);
3819 $arr = explode(' ',$profile['pub_keywords']);
3821 for ($x = 0; $x < 5; $x ++) {
3823 $tags .= '#'. trim($arr[$x]) .' ';
3827 $tags = trim($tags);
3830 return array("author" => $handle,
3831 "first_name" => $first,
3832 "last_name" => $last,
3833 "image_url" => $large,
3834 "image_url_medium" => $medium,
3835 "image_url_small" => $small,
3837 "gender" => $profile['gender'],
3839 "location" => $location,
3840 "searchable" => $searchable,
3842 "tag_string" => $tags);
3846 * @brief Sends profile data
3848 * @param int $uid The user id
3850 public static function send_profile($uid, $recips = false) {
3856 $recips = q("SELECT `id`,`name`,`network`,`pubkey`,`notify` FROM `contact` WHERE `network` = '%s'
3857 AND `uid` = %d AND `rel` != %d",
3858 dbesc(NETWORK_DIASPORA),
3860 intval(CONTACT_IS_SHARING)
3865 $message = self::createProfileData($uid);
3867 foreach ($recips as $recip) {
3868 logger("Send updated profile data for user ".$uid." to contact ".$recip["id"], LOGGER_DEBUG);
3869 self::build_and_transmit($profile, $recip, "profile", $message, false, "", true);
3874 * @brief Stores the signature for likes that are created on our system
3876 * @param array $contact The contact array of the "like"
3877 * @param int $post_id The post id of the "like"
3879 * @return bool Success
3881 public static function store_like_signature($contact, $post_id) {
3883 // Is the contact the owner? Then fetch the private key
3884 if (!$contact['self'] || ($contact['uid'] == 0)) {
3885 logger("No owner post, so not storing signature", LOGGER_DEBUG);
3889 $r = q("SELECT `prvkey` FROM `user` WHERE `uid` = %d LIMIT 1", intval($contact['uid']));
3890 if (!DBM::is_result($r)) {
3894 $contact["uprvkey"] = $r[0]['prvkey'];
3896 $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1", intval($post_id));
3897 if (!DBM::is_result($r)) {
3901 if (!in_array($r[0]["verb"], array(ACTIVITY_LIKE, ACTIVITY_DISLIKE))) {
3905 $message = self::construct_like($r[0], $contact);
3906 $message["author_signature"] = self::signature($contact, $message);
3909 * Now store the signature more flexible to dynamically support new fields.
3910 * This will break Diaspora compatibility with Friendica versions prior to 3.5.
3912 dba::insert('sign', array('iid' => $post_id, 'signed_text' => json_encode($message)));
3914 logger('Stored diaspora like signature');
3919 * @brief Stores the signature for comments that are created on our system
3921 * @param array $item The item array of the comment
3922 * @param array $contact The contact array of the item owner
3923 * @param string $uprvkey The private key of the sender
3924 * @param int $message_id The message id of the comment
3926 * @return bool Success
3928 public static function store_comment_signature($item, $contact, $uprvkey, $message_id) {
3930 if ($uprvkey == "") {
3931 logger('No private key, so not storing comment signature', LOGGER_DEBUG);
3935 $contact["uprvkey"] = $uprvkey;
3937 $message = self::construct_comment($item, $contact);
3938 $message["author_signature"] = self::signature($contact, $message);
3941 * Now store the signature more flexible to dynamically support new fields.
3942 * This will break Diaspora compatibility with Friendica versions prior to 3.5.
3944 dba::insert('sign', array('iid' => $message_id, 'signed_text' => json_encode($message)));
3946 logger('Stored diaspora comment signature');