diff --git a/pnut/pnut.php b/pnut/pnut.php index f5c40794..1bae3869 100644 --- a/pnut/pnut.php +++ b/pnut/pnut.php @@ -2,7 +2,7 @@ /** * Name: Pnut Connector * Description: Post to pnut.io - * Version: 0.1.2 + * Version: 0.2.0 * Author: Morgan McMillian * Status: In Development */ @@ -11,14 +11,19 @@ require_once 'addon/pnut/lib/phpnut.php'; require_once 'addon/pnut/lib/phpnutException.php'; use Friendica\Content\Text\BBCode; +use Friendica\Content\Text\HTML; use Friendica\Content\Text\Plaintext; -use Friendica\Core\Config\Util\ConfigFileManager; +use Friendica\Content\Text\Markdown; use Friendica\Core\Hook; use Friendica\Core\Renderer; use Friendica\Core\System; use Friendica\DI; +use Friendica\Model\Contact; +use Friendica\Model\Circle; +use Friendica\Util\Strings; use Friendica\Model\Item; use Friendica\Model\Photo; +use Friendica\Core\Protocol; use phpnut\phpnutException; const PNUT_LIMIT = 256; @@ -32,6 +37,9 @@ function pnut_install() Hook::register('jot_networks', __FILE__, 'pnut_jot_nets'); Hook::register('connector_settings', __FILE__, 'pnut_settings'); Hook::register('connector_settings_post', __FILE__, 'pnut_settings_post'); + Hook::register('cron', __FILE__, 'pnut_cron'); + Hook::register('prepare_body', __FILE__, 'pnut_prepare_body'); + Hook::register('check_item_notification', __FILE__, 'pnut_check_item_notification'); } function pnut_module() {} @@ -78,6 +86,10 @@ function pnut_connect() DI::logger()->debug('Got Token', [$token]); $o = DI::l10n()->t('You are now authenticated with pnut.io.'); DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'pnut', 'access_token', $token); + + // Get user info and set ownid + $userdata = $nut->getUser(); + DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'pnut', 'ownid', $userdata['id']); } catch (phpnutException $e) { $o = DI::l10n()->t('Error fetching token. Please try again.', ['code' => $e->getCode(), 'message' => $e->getMessage()]); } @@ -87,9 +99,15 @@ function pnut_connect() return $o; } -function pnut_load_config(ConfigFileManager $loader) +function pnut_check_item_notification($a, &$notification_data) { - DI::appHelper()->getConfigCache()->load($loader->loadAddonConfig('pnut'), \Friendica\Core\Config\ValueObject\Cache::SOURCE_STATIC); + $own_id = DI::pConfig()->get($notification_data["uid"], 'pnut', 'ownid'); + + $own_user = DI::dba()->p("SELECT `url` FROM `contact` WHERE `uid` = ? AND `alias` = ?", $notification_data["uid"], "pnut::" . $own_id); + + if ($own_user) { + $notification_data["profiles"][] = $own_user[0]["url"]; + } } function pnut_addon_admin(string &$o) @@ -123,9 +141,11 @@ function pnut_settings(array &$data) $enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'pnut', 'post') ?? false; $def_enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'pnut', 'post_by_default') ?? false; + $importenabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'pnut', 'import') ?? false; $client_id = DI::config()->get('pnut', 'client_id'); $client_secret = DI::config()->get('pnut', 'client_secret'); $token = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'pnut', 'access_token'); + $ownid = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'pnut', 'ownid'); $user_client = empty($client_id) || empty($client_secret); if ($user_client) { @@ -133,6 +153,8 @@ function pnut_settings(array &$data) $client_secret = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'pnut', 'client_secret'); } + $o = ''; + if (!empty($client_id) && !empty($client_secret) && empty($token)) { $nut = new phpnut\phpnut($client_id, $client_secret); $authorize_url = $nut->getAuthUrl($redirectUri, $scope); @@ -140,14 +162,22 @@ function pnut_settings(array &$data) } if (!empty($token)) { - $disconn_btn = DI::l10n()->t('Disconnect'); + $nut = new phpnut\phpnut($token); + try { + $userdata = $nut->getUser(); + if ($ownid != $userdata["id"]) { + DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'pnut', 'ownid', $userdata["id"]); + } + $o .= '

' . DI::l10n()->t('Currently connected to: ') . '' . $userdata["username"] . '
' . $userdata["description"]["text"] . '

'; + } catch (phpnutException $e) { + $o .= DI::l10n()->t("

Error fetching user profile. Please clear the configuration and try again.

"); + } } $t = Renderer::getMarkupTemplate('connector_settings.tpl', 'addon/pnut/'); $html = Renderer::replaceMacros($t, [ '$enable' => ['pnut', DI::l10n()->t('Enable Pnut Post Addon'), $enabled], - '$bydefault' => ['pnut_bydefault', DI::l10n()->t('Post to Pnut by default'), $def_enabled], - '$client_id' => ['pnut_client_id', DI::l10n()->t('Client ID'), $client_id], + '$bydefault' => ['pnut_bydefault', DI::l10n()->t('Post to Pnut by default'), $def_enabled], '$import' => ['pnut_import', DI::l10n()->t('Import the remote timeline'), $importenabled], '$client_id' => ['pnut_client_id', DI::l10n()->t('Client ID'), $client_id], '$client_secret' => ['pnut_client_secret', DI::l10n()->t('Client Secret'), $client_secret], '$access_token' => ['pnut_access_token', DI::l10n()->t('Access Token'), $token, '', '', 'readonly'], '$authorize_url' => $authorize_url ?? '', @@ -174,12 +204,14 @@ function pnut_settings_post(array &$b) if (!empty($_POST['pnut-disconnect'])) { DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'pnut', 'post'); DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'pnut', 'post_by_default'); + DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'pnut', 'import'); DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'pnut', 'client_id'); DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'pnut', 'client_secret'); DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'pnut', 'access_token'); } else { DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'pnut', 'post', intval($_POST['pnut'])); DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'pnut', 'post_by_default', intval($_POST['pnut_bydefault'])); + DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'pnut', 'import', intval($_POST['pnut_import'])); if (!empty($_POST['pnut_client_id'])) { DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'pnut', 'client_id', $_POST['pnut_client_id']); } @@ -275,6 +307,59 @@ function pnut_post_hook(array &$b) $token = DI::pConfig()->get($b['uid'], 'pnut', 'access_token'); $nut = new phpnut\phpnut($token); + $iscomment = false; + $islike = false; + + if ($b['parent'] != $b['id']) { + // Looking if its a reply to a pnut post + if ((substr($b["parent-uri"], 0, 6) != "pnut::") && (substr($b["extid"], 0, 6) != "pnut::") && (substr($b["thr-parent"], 0, 6) != "pnut::")) { + DI::logger()->debug("PNUT post: no pnut post " . $b["parent"]); + return; + } + + $r = DI::dba()->p("SELECT * FROM item WHERE item.uri = ? AND item.uid = ? LIMIT 1", $b["thr-parent"], intval($b["uid"])); + + if (!count($r)) { + DI::logger()->debug("PNUT post: no parent found " . $b["thr-parent"]); + return; + } else { + $iscomment = true; + $orig_post = $r[0]; + } + + $nicknameplain = preg_replace("=https?://pnut.io/(.*)=ism", "$1", $orig_post["author-link"]); + $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]"; + $nicknameplain = "@" . $nicknameplain; + + DI::logger()->debug('PNUT post: comparing ' . $nickname . ' and ' . $nicknameplain . ' with ' . $b["body"]); + if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) { + $b["body"] = $nickname . " " . $b["body"]; + } + + DI::logger()->debug('PNUT post: parent found ' . print_r($orig_post, true)); + } elseif ($b['verb'] == 'http://activitystrea.ms/schema/1.0/like') { + DI::logger()->debug('PNUT post: like detected'); + $islike = true; + } + + if (($b['verb'] == 'http://activitystrea.ms/schema/1.0/post') && $b['deleted']) { + pnut_action($b["uid"], substr($orig_post["uri"], 6), "delete"); + return; + } + + if ($b['verb'] == 'http://activitystrea.ms/schema/1.0/like') { + if ($b['deleted']) { + pnut_action($b["uid"], substr($b["thr-parent"], 6), "unlike"); + } else { + pnut_action($b["uid"], substr($b["thr-parent"], 6), "like"); + } + return; + } + + if ($b['deleted'] || ($b['created'] !== $b['edited'])) { + return; + } + $msgarr = Plaintext::getPost($b, PNUT_LIMIT, true, BBCode::EXTERNAL); $text = $msgarr['text']; $raw = []; @@ -319,7 +404,647 @@ function pnut_post_hook(array &$b) } $raw['io.pnut.core.crosspost'][] = ['canonical_url' => $b['plink']]; - $nut->createPost($text, ['raw' => $raw]); + + $data = ['raw' => $raw]; + if ($iscomment) { + $data['reply_to'] = substr($orig_post["uri"], 6); + } + + $nut->createPost($text, $data); DI::logger()->debug('PNUT post complete', ['id' => $b['id'], 'text' => $text, 'raw' => $raw]); } + +function pnut_cron($a, $b) +{ + $last = DI::config()->get('pnut', 'last_poll'); + + $poll_interval = intval(DI::config()->get('pnut', 'poll_interval')); + if (!$poll_interval) { + $poll_interval = 5; // default 5 minutes + } + + if ($last) { + $next = $last + ($poll_interval * 60); + if ($next > time()) { + DI::logger()->debug('PNUT cron: poll interval not reached'); + return; + } + } + + DI::logger()->debug('PNUT cron: cron_start'); + + $abandon_days = intval(DI::config()->get('system', 'account_abandon_days', 0)); + if ($abandon_days < 1) { + $abandon_days = 0; + } + + $abandon_limit = date("Y-m-d H:i:s", time() - $abandon_days * 86400); + + $r = DI::dba()->p("SELECT * FROM `pconfig` WHERE `cat` = 'pnut' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()"); + if (count($r)) { + foreach ($r as $rr) { + if ($abandon_days != 0) { + $user = DI::dba()->p("SELECT `login_date` FROM `user` WHERE uid = ? AND `login_date` >= ?", $rr['uid'], $abandon_limit); + if (!count($user)) { + DI::logger()->debug('abandoned account: timeline from user ' . $rr['uid'] . ' will not be imported'); + continue; + } + } + + DI::logger()->debug('PNUT cron: importing timeline from user ' . $rr['uid']); + pnut_fetchstream($rr["uid"]); + } + } + + DI::logger()->debug('PNUT cron: cron_end'); + + DI::config()->set('pnut', 'last_poll', time()); +} + +function pnut_fetchstream($uid) +{ + $token = DI::pConfig()->get($uid, 'pnut', 'access_token'); + $nut = new phpnut\phpnut($token); + + $r = DI::dba()->p("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = ? LIMIT 1", intval($uid)); + + if (count($r)) { + $me = $r[0]; + } else { + DI::logger()->debug("PNUT fetchstream: Own contact not found for user " . $uid); + return; + } + + $user = DI::dba()->p("SELECT * FROM `user` WHERE `uid` = ? AND `account_expired` = 0 LIMIT 1", intval($uid)); + + if (count($user)) { + $user = $user[0]; + } else { + DI::logger()->debug("PNUT fetchstream: Own user not found for user " . $uid); + return; + } + + $ownid = DI::pConfig()->get($uid, 'pnut', 'ownid'); + + // Fetch stream + $param = array("count" => 200, "include_deleted" => false, "include_directed_posts" => true, + "include_html" => false, "include_post_annotations" => true); + + $lastid = DI::pConfig()->get($uid, 'pnut', 'laststreamid'); + + if ($lastid <> "") { + $param["since_id"] = $lastid; + } + + try { + $stream = $nut->getUserStream($param); + } catch (phpnutException $e) { + DI::logger()->debug("PNUT fetchstream: Error fetching stream for user " . $uid . " " . $e->getMessage()); + return; + } + + if (!is_array($stream)) { + $stream = array(); + } + + $stream = array_reverse($stream); + foreach ($stream as $post) { + $postarray = pnut_createpost($uid, $post, $me, $user, $ownid, true); + + $item = Item::insert($postarray); + $postarray["id"] = $item; + + DI::logger()->debug('PNUT fetchstream: User ' . $uid . ' posted stream item ' . $item); + + $lastid = $post["id"]; + } + + DI::pConfig()->set($uid, 'pnut', 'laststreamid', $lastid); + + // Fetch mentions + $param = array("count" => 200, "include_deleted" => false, "include_directed_posts" => true, + "include_html" => false, "include_post_annotations" => true); + + $lastid = DI::pConfig()->get($uid, 'pnut', 'lastmentionid'); + + if ($lastid <> "") { + $param["since_id"] = $lastid; + } + + try { + $mentions = $nut->getUserMentions("me", $param); + } catch (phpnutException $e) { + DI::logger()->debug("PNUT fetchstream: Error fetching mentions for user " . $uid . " " . $e->getMessage()); + return; + } + + if (!is_array($mentions)) { + $mentions = array(); + } + + $mentions = array_reverse($mentions); + foreach ($mentions as $post) { + $postarray = pnut_createpost($uid, $post, $me, $user, $ownid, false); + + if (isset($postarray["id"])) { + $item = $postarray["id"]; + $parent_id = $postarray['parent']; + } elseif (isset($postarray["body"])) { + $item = Item::insert($postarray); + $postarray["id"] = $item; + + $parent_id = 0; + DI::logger()->debug('PNUT fetchstream: User ' . $uid . ' posted mention item ' . $item); + } else { + $item = 0; + $parent_id = 0; + } + + // Fetch the parent and id + if (($parent_id == 0) && ($postarray['uri'] != "")) { + $r = DI::dba()->p("SELECT `id`, `parent` FROM `item` WHERE `uri` = ? AND `uid` = ? LIMIT 1", $postarray['uri'], intval($uid)); + + if (count($r)) { + $item = $r[0]['id']; + $parent_id = $r[0]['parent']; + } + } + + $lastid = $post["id"]; + } + + DI::pConfig()->set($uid, 'pnut', 'lastmentionid', $lastid); +} + +function pnut_createpost($uid, $post, $me, $user, $ownid, $createuser, $threadcompletion = true, $nodupcheck = false) +{ + if ($post["machine_only"]) { + return; + } + + if ($post["is_deleted"]) { + return; + } + + $postarray = array(); + $postarray['gravity'] = 0; + $postarray['uid'] = $uid; + $postarray['wall'] = 0; + $postarray['verb'] = 'http://activitystrea.ms/schema/1.0/post'; + $postarray['network'] = Protocol::PNUT; // Assuming Protocol::PNUT is defined, else use a string + if (is_array($post["repost_of"])) { + // You can't reply to reposts. So use the original id and thread-id + $postarray['uri'] = "pnut::" . $post["repost_of"]["id"]; + $postarray['parent-uri'] = "pnut::" . $post["repost_of"]["thread_id"]; + } else { + $postarray['uri'] = "pnut::" . $post["id"]; + $postarray['parent-uri'] = "pnut::" . $post["thread_id"]; + } + + if (!$nodupcheck) { + $r = DI::dba()->p("SELECT * FROM `item` WHERE `uri` = ? AND `uid` = ? LIMIT 1", $postarray['uri'], intval($uid)); + + if (count($r)) { + return $r[0]; + } + + $r = DI::dba()->p("SELECT * FROM `item` WHERE `extid` = ? AND `uid` = ? LIMIT 1", $postarray['uri'], intval($uid)); + + if (count($r)) { + return $r[0]; + } + } + + if (isset($post["reply_to"]) && ($post["reply_to"] != "")) { + $postarray['thr-parent'] = "pnut::" . $post["reply_to"]; + + // Complete the thread (if the parent doesn't exists) + if ($threadcompletion) { + DI::logger()->debug("PNUT createpost: completing thread " . $post["thread_id"] . " for user " . $uid); + + $token = DI::pConfig()->get($uid, 'pnut', 'access_token'); + $nut = new phpnut\phpnut($token); + + $param = array("count" => 200, "include_deleted" => false, "include_directed_posts" => true, + "include_html" => false, "include_post_annotations" => true); + try { + $thread = []; + } catch (phpnutException $e) { + DI::logger()->debug("PNUT createpost: Error fetching thread for user " . $uid . " " . $e->getMessage()); + } + $thread = array_reverse($thread); + + DI::logger()->debug("PNUT createpost: fetched " . count($thread) . " items for thread " . $post["thread_id"] . " for user " . $uid); + + foreach ($thread as $tpost) { + $threadpost = pnut_createpost($uid, $tpost, $me, $user, $ownid, false, false); + $item = Item::insert($threadpost); + $threadpost["id"] = $item; + + DI::logger()->debug("PNUT createpost: stored post " . $post["id"] . " thread " . $post["thread_id"] . " in item " . $item); + } + } + // Don't create accounts of people who just comment something + $createuser = false; + + $postarray['object-type'] = 'http://activitystrea.ms/schema/1.0/comment'; + } else { + $postarray['thr-parent'] = $postarray['uri']; + $postarray['object-type'] = 'http://activitystrea.ms/schema/1.0/note'; + } + + if (($post["user"]["id"] != $ownid) || ($postarray['thr-parent'] == $postarray['uri'])) { + $postarray['owner-name'] = $post["user"]["name"]; + $postarray['owner-link'] = $post["user"]["canonical_url"]; + $postarray['owner-avatar'] = $post["user"]["avatar_image"]["url"]; + $postarray['contact-id'] = pnut_fetchcontact($uid, $post["user"], $me, $createuser); + } else { + $postarray['owner-name'] = $me["name"]; + $postarray['owner-link'] = $me["url"]; + $postarray['owner-avatar'] = $me["thumb"]; + $postarray['contact-id'] = $me["id"]; + } + + if (is_array($post["repost_of"])) { + $postarray['author-name'] = $post["repost_of"]["user"]["name"]; + $postarray['author-link'] = $post["repost_of"]["user"]["canonical_url"]; + $postarray['author-avatar'] = $post["repost_of"]["user"]["avatar_image"]["url"]; + + $content = $post["repost_of"]; + } else { + $postarray['author-name'] = $postarray['owner-name']; + $postarray['author-link'] = $postarray['owner-link']; + $postarray['author-avatar'] = $postarray['owner-avatar']; + + $content = $post; + } + + $postarray['plink'] = $content["canonical_url"]; + + if (is_array($content["entities"])) { + $converted = pnut_expand_entities($content["text"], $content["entities"]); + $postarray['body'] = $converted["body"]; + $postarray['tag'] = $converted["tags"]; + } else { + $postarray['body'] = $content["text"]; + } + + if (sizeof($content["entities"]["links"])) { + foreach ($content["entities"]["links"] as $link) { + $url = Strings::normaliseLink($link["url"]); + $links[$url] = $link["url"]; + } + } + + $page_info = ""; + + if (is_array($content["annotations"])) { + $photo = pnut_expand_annotations($content["annotations"]); + if (($photo["large"] != "") && ($photo["url"] != "")) { + $page_info = "\n[url=" . $photo["url"] . "][img]" . $photo["large"] . "[/img][/url]"; + } elseif ($photo["url"] != "") { + $page_info = "\n[img]" . $photo["url"] . "[/img]"; + } + + if ($photo["url"] != "") { + $postarray['object-type'] = 'http://activitystrea.ms/schema/1.0/image'; + } + } else { + $photo = array("url" => "", "large" => ""); + } + + if (sizeof($links)) { + $link = array_pop($links); + $url = str_replace(array('/', '.'), array('\/', '\.'), $link); + + $page_info = ""; + + if (trim($page_info) != "") { + $removedlink = preg_replace("/\[url\=" . $url . "\](.*?)\[\/url\]/ism", '', $postarray['body']); + if (($removedlink == "") || strstr($postarray['body'], $removedlink)) { + $postarray['body'] = $removedlink; + } + } + } + + $postarray['body'] .= $page_info; + + $postarray['created'] = (new DateTime($post["created_at"]))->format(DateTime::ATOM); + $postarray['edited'] = (new DateTime($post["created_at"]))->format(DateTime::ATOM); + + $postarray['app'] = $post["source"]["name"]; + + return $postarray; +} + +function pnut_expand_entities($body, $entities) +{ + if (!function_exists('substr_unicode')) { + function substr_unicode($str, $s, $l = null) { + return join("", array_slice( + preg_split("//u", $str, -1, PREG_SPLIT_NO_EMPTY), $s, $l)); + } + } + + $tags_arr = array(); + $replace = array(); + + foreach ($entities["mentions"] as $mention) { + $url = "@[url=https://pnut.io/" . rawurlencode($mention["name"]) . "]" . $mention["name"] . "[/url]"; + $tags_arr["@" . $mention["name"]] = $url; + $replace[$mention["pos"]] = array("pos" => $mention["pos"], "len" => $mention["len"], "replace" => $url); + } + + foreach ($entities["hashtags"] as $hashtag) { + $url = "#[url=" . DI::baseUrl() . "/search?tag=" . rawurlencode($hashtag["name"]) . "]" . $hashtag["name"] . "[/url]"; + $tags_arr["#" . $hashtag["name"]] = $url; + $replace[$hashtag["pos"]] = array("pos" => $hashtag["pos"], "len" => $hashtag["len"], "replace" => $url); + } + + foreach ($entities["links"] as $links) { + $url = "[url=" . $links["url"] . "]" . $links["text"] . "[/url]"; + if (isset($links["amended_len"]) && ($links["amended_len"] > $links["len"])) { + $replace[$links["pos"]] = array("pos" => $links["pos"], "len" => $links["amended_len"], "replace" => $url); + } else { + $replace[$links["pos"]] = array("pos" => $links["pos"], "len" => $links["len"], "replace" => $url); + } + } + + if (sizeof($replace)) { + krsort($replace); + foreach ($replace as $entity) { + $pre = substr_unicode($body, 0, $entity["pos"]); + $post = substr_unicode($body, $entity["pos"] + $entity["len"]); + $body = $pre . $entity["replace"] . $post; + } + } + + return array("body" => $body, "tags" => implode(",", $tags_arr)); +} + +function pnut_expand_annotations($annotations) +{ + $photo = array("url" => "", "large" => ""); + foreach ($annotations as $annotation) { + if (($annotation["type"] == "io.pnut.core.oembed") && + ($annotation["value"]["type"] == "photo")) { + if ($annotation["value"]["url"] != "") { + $photo["url"] = $annotation["value"]["url"]; + } + + if ($annotation["value"]["thumbnail_large_url"] != "") { + $photo["large"] = $annotation["value"]["thumbnail_large_url"]; + } + } + } + return $photo; +} + +function pnut_action($uid, $post_id, $action) +{ + // TODO: Implement pnut action API call + DI::logger()->info('PNUT action', ['uid' => $uid, 'post_id' => $post_id, 'action' => $action]); +} + +function pnut_fetchcontact($uid, $contact, $me, $create_user) +{ + $r = DI::dba()->p("SELECT * FROM `contact` WHERE `uid` = ? AND `alias` = ? LIMIT 1", intval($uid), "pnut::" . $contact["id"]); + + if (!count($r) && !$create_user) { + return $me["id"]; + } + + if ($contact["canonical_url"] == "") { + return $me["id"]; + } + + if (count($r) && ($r[0]["readonly"] || $r[0]["blocked"])) { + DI::logger()->debug("PNUT fetchcontact: Contact '" . $r[0]["nick"] . "' is blocked or readonly."); + return -1; + } + + if (!count($r)) { + + if ($contact["name"] == "") { + $contact["name"] = $contact["username"]; + } + + if ($contact["username"] == "") { + $contact["username"] = $contact["name"]; + } + + // create contact record + DI::dba()->p("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `addr`, `alias`, `notify`, `poll`, + `name`, `nick`, `photo`, `network`, `rel`, `priority`, + `about`, `writable`, `blocked`, `readonly`, `pending` ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0 )", + intval($uid), + new DateTime('now', new DateTimeZone('UTC')), + $contact["canonical_url"], + Strings::normaliseLink($contact["canonical_url"]), + $contact["username"] . "@pnut.io", + "pnut::" . $contact["id"], + '', + "pnut::" . $contact["id"], + $contact["name"], + $contact["username"], + $contact["avatar_image"]["url"], + Protocol::PNUT, + intval(Contact::SHARING), + intval(1), + $contact["description"]["text"], + intval(1) + ); + + $r = DI::dba()->p("SELECT * FROM `contact` WHERE `alias` = ? AND `uid` = ? LIMIT 1", + "pnut::" . $contact["id"], intval($uid)); + + if (!count($r)) { + return false; + } + + $contact_id = $r[0]['id']; + + $g = DI::dba()->p("SELECT def_gid FROM user WHERE uid = ? LIMIT 1", intval($uid)); + + if ($g && intval($g[0]['def_gid'])) { + require_once('include/group.php'); + Circle::addMember($uid, '', $contact_id, $g[0]['def_gid']); + } + + $photos = Photo::importProfilePhoto($contact["avatar_image"]["url"], $uid, $contact_id); + + DI::dba()->p("UPDATE `contact` SET `photo` = ?, `thumb` = ?, `micro` = ?, + `name-date` = ?, `uri-date` = ?, `avatar-date` = ? WHERE `id` = ?", + $photos[0], + $photos[1], + $photos[2], + new DateTime('now', new DateTimeZone('UTC')), + new DateTime('now', new DateTimeZone('UTC')), + new DateTime('now', new DateTimeZone('UTC')), + intval($contact_id) + ); + } else { + // update profile photos once every two weeks as we have no notification of when they change. + + $update_photo = ($r[0]['avatar-date'] < (new DateTime('now', new DateTimeZone('UTC')))->modify('-12 hours')); + + // check that we have all the photos, this has been known to fail on occasion + + if ((!$r[0]['photo']) || (!$r[0]['thumb']) || (!$r[0]['micro']) || ($update_photo)) { + + DI::logger()->debug("PNUT fetchcontact: Updating contact " . $contact["username"]); + + $photos = Photo::importProfilePhoto($contact["avatar_image"]["url"], $uid, $r[0]['id']); + + DI::dba()->p("UPDATE `contact` SET `photo` = ?, `thumb` = ?, `micro` = ?, + `name-date` = ?, `uri-date` = ?, `avatar-date` = ?, + `url` = ?, `nurl` = ?, `addr` = ?, `name` = ?, `nick` = ?, `about` = ? WHERE `id` = ?", + $photos[0], + $photos[1], + $photos[2], + new DateTime('now', new DateTimeZone('UTC')), + new DateTime('now', new DateTimeZone('UTC')), + new DateTime('now', new DateTimeZone('UTC')), + $contact["canonical_url"], + Strings::normaliseLink($contact["canonical_url"]), + $contact["username"] . "@pnut.io", + $contact["name"], + $contact["username"], + $contact["description"]["text"], + intval($r[0]['id']) + ); + } + } + + return $r[0]["id"]; +} + +function pnut_prepare_body($a, &$b) +{ + if ($b["item"]["network"] != Protocol::PNUT) { + return; + } + + if ($b["preview"]) { + $max_char = 256; + require_once("include/plaintext.php"); + $item = $b["item"]; + $item["plink"] = DI::baseUrl() . "/display/" . $a->user["nickname"] . "/" . $item["parent"]; + + $r = DI::dba()->p("SELECT `author-link` FROM item WHERE item.uri = ? AND item.uid = ? LIMIT 1", + $item["thr-parent"], intval(DI::userSession()->getLocalUserId())); + + if (count($r)) { + $orig_post = $r[0]; + + $nicknameplain = preg_replace("=https?://pnut.io/(.*)=ism", "$1", $orig_post["author-link"]); + $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]"; + $nicknameplain = "@" . $nicknameplain; + + if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) { + $item["body"] = $nickname . " " . $item["body"]; + } + } + + $msgarr = Plaintext::getPost($a, $item, $max_char, true); + $msg = pnut_create_entities($a, $item, $msgarr); + + $msg = Markdown::convert($msg); + + $b['html'] = $msg; + } +} + +function pnut_create_entities($a, $b, $postdata) +{ + require_once("include/bbcode.php"); + require_once("include/plaintext.php"); + + $bbcode = $b["body"]; + $bbcode = BBCode::removeShareInformation($bbcode, false, true); + + // Change pure links in text to bbcode uris + $bbcode = preg_replace("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", '$1[url=$2]$2[/url]', $bbcode); + + $URLSearchString = "^\[\]"; + + $bbcode = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",'#$2',$bbcode); + $bbcode = preg_replace("/@\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",'@$2',$bbcode); + $bbcode = preg_replace("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",'[url=$1]$2[/url]',$bbcode); + $bbcode = preg_replace("/\[video\](.*?)\[\/video\]/ism",'[url=$1]$1[/url]',$bbcode); + $bbcode = preg_replace("/\[youtube\]https?:\/\/(.*?)\[\/youtube\]/ism",'[url=https://$1]https://$1[/url]',$bbcode); + $bbcode = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", + '[url=https://www.youtube.com/watch?v=$1]https://www.youtube.com/watch?v=$1[/url]', $bbcode); + $bbcode = preg_replace("/\[vimeo\]https?:\/\/(.*?)\[\/vimeo\]/ism",'[url=https://$1]https://$1[/url]',$bbcode); + $bbcode = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", + '[url=https://vimeo.com/$1]https://vimeo.com/$1[/url]', $bbcode); + $bbcode = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $bbcode); + + preg_match_all("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", $bbcode, $urls, PREG_SET_ORDER); + + $bbcode = preg_replace("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",'$1',$bbcode); + + $b["body"] = $bbcode; + + $plaintext = Plaintext::getPost($a, $b, 0, false, 6); + + $text = $plaintext["text"]; + + $start = 0; + $entities = array(); + + foreach ($urls as $url) { + $lenurl = iconv_strlen($url[1], "UTF-8"); + $len = iconv_strlen($url[2], "UTF-8"); + $pos = iconv_strpos($text, $url[1], $start, "UTF-8"); + $pre = iconv_substr($text, 0, $pos, "UTF-8"); + $post = iconv_substr($text, $pos + $lenurl, 1000000, "UTF-8"); + + $mid = $url[2]; + $html = BBCode::convert($mid, false, false, 6); + $mid = HTML::toPlaintext($html, 0, true); + + $mid = trim(html_entity_decode($mid, ENT_QUOTES, 'UTF-8')); + + $text = $pre . $mid . $post; + + if ($mid != "") { + $entities[] = array("pos" => $pos, "len" => $len, "url" => $url[1], "text" => $mid); + } + + $start = $pos + 1; + } + + if (isset($postdata["url"]) && isset($postdata["title"]) && ($postdata["type"] != "photo")) { + $postdata["title"] = Plaintext::shorten($postdata["title"], 90); + $max = 256 - strlen($postdata["title"]); + $text = Plaintext::shorten($text, $max); + $text .= "\n[" . $postdata["title"] . "](" . $postdata["url"] . ")"; + } elseif (isset($postdata["url"]) && ($postdata["type"] != "photo")) { + $postdata["url"] = Strings::normaliseLink($postdata["url"]); + $max = 240; + $text = Plaintext::shorten($text, $max); + $text .= " [" . $postdata["url"] . "](" . $postdata["url"] . ")"; + } else { + $max = 256; + $text = Plaintext::shorten($text, $max); + } + + if (iconv_strlen($text, "UTF-8") < $max) { + $max = iconv_strlen($text, "UTF-8"); + } + + krsort($entities); + foreach ($entities as $entity) { + if (($entity["pos"] + $entity["len"]) <= $max) { + $pre = iconv_substr($text, 0, $entity["pos"], "UTF-8"); + $post = iconv_substr($text, $entity["pos"] + $entity["len"], 1000000, "UTF-8"); + + $text = $pre . "[" . $entity["text"] . "](" . $entity["url"] . ")" . $post; + } + } + + return $text; +} diff --git a/pnut/templates/connector_settings.tpl b/pnut/templates/connector_settings.tpl index 6662a5ae..7e830c94 100644 --- a/pnut/templates/connector_settings.tpl +++ b/pnut/templates/connector_settings.tpl @@ -1,6 +1,7 @@

{{$status}}

{{include file="field_checkbox.tpl" field=$enable}} {{include file="field_checkbox.tpl" field=$bydefault}} +{{include file="field_checkbox.tpl" field=$import}} {{if $user_client}} {{include file="field_input.tpl" field=$client_id}} {{include file="field_input.tpl" field=$client_secret}}