Merge pull request #990 from MrPetovan/task/share-block-guid
[friendica-addons.git/.git] / twitter / twitter.php
index da8c3b3..f743d96 100644 (file)
@@ -81,6 +81,8 @@ use Friendica\Model\Conversation;
 use Friendica\Model\Group;
 use Friendica\Model\Item;
 use Friendica\Model\ItemContent;
+use Friendica\Model\ItemURI;
+use Friendica\Model\Tag;
 use Friendica\Model\User;
 use Friendica\Protocol\Activity;
 use Friendica\Util\ConfigFileLoader;
@@ -108,29 +110,11 @@ function twitter_install()
        Hook::register('expire'                 , __FILE__, 'twitter_expire');
        Hook::register('prepare_body'           , __FILE__, 'twitter_prepare_body');
        Hook::register('check_item_notification', __FILE__, 'twitter_check_item_notification');
+       Hook::register('probe_detect'           , __FILE__, 'twitter_probe_detect');
        Logger::info("installed twitter");
 }
 
-function twitter_uninstall()
-{
-       Hook::unregister('load_config'            , __FILE__, 'twitter_load_config');
-       Hook::unregister('connector_settings'     , __FILE__, 'twitter_settings');
-       Hook::unregister('connector_settings_post', __FILE__, 'twitter_settings_post');
-       Hook::unregister('hook_fork'              , __FILE__, 'twitter_hook_fork');
-       Hook::unregister('post_local'             , __FILE__, 'twitter_post_local');
-       Hook::unregister('notifier_normal'        , __FILE__, 'twitter_post_hook');
-       Hook::unregister('jot_networks'           , __FILE__, 'twitter_jot_nets');
-       Hook::unregister('cron'                   , __FILE__, 'twitter_cron');
-       Hook::unregister('follow'                 , __FILE__, 'twitter_follow');
-       Hook::unregister('expire'                 , __FILE__, 'twitter_expire');
-       Hook::unregister('prepare_body'           , __FILE__, 'twitter_prepare_body');
-       Hook::unregister('check_item_notification', __FILE__, 'twitter_check_item_notification');
-
-       // old setting - remove only
-       Hook::unregister('post_local_end'     , __FILE__, 'twitter_post_hook');
-       Hook::unregister('addon_settings'     , __FILE__, 'twitter_settings');
-       Hook::unregister('addon_settings_post', __FILE__, 'twitter_settings_post');
-}
+// Hook functions
 
 function twitter_load_config(App $a, ConfigFileLoader $loader)
 {
@@ -179,14 +163,14 @@ function twitter_follow(App $a, array &$contact)
        $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
        $connection->post('friendships/create', ['screen_name' => $nickname]);
 
-       twitter_fetchuser($a, $uid, $nickname);
+       $user = twitter_fetchuser($nickname);
 
-       $r = q("SELECT name,nick,url,addr,batch,notify,poll,request,confirm,poco,photo,priority,network,alias,pubkey
-               FROM `contact` WHERE `uid` = %d AND `nick` = '%s'",
-                               intval($uid),
-                               DBA::escape($nickname));
-       if (DBA::isResult($r)) {
-               $contact["contact"] = $r[0];
+       $contact_id = twitter_fetch_contact($uid, $user, true);
+
+       $contact = Contact::getById($contact_id, ['name', 'nick', 'url', 'addr', 'batch', 'notify', 'poll', 'request', 'confirm', 'poco', 'photo', 'priority', 'network', 'alias', 'pubkey']);
+
+       if (DBA::isResult($contact)) {
+               $contact["contact"] = $contact;
        }
 }
 
@@ -469,6 +453,33 @@ function twitter_post_local(App $a, array &$b)
        $b['postopts'] .= 'twitter';
 }
 
+function twitter_probe_detect(App $a, array &$hookData)
+{
+       // Don't overwrite an existing result
+       if ($hookData['result']) {
+               return;
+       }
+
+       // Avoid a lookup for the wrong network
+       if (!in_array($hookData['network'], ['', Protocol::TWITTER])) {
+               return;
+       }
+
+       if (preg_match('=(.*)@twitter.com=i', $hookData['uri'], $matches)) {
+               $nick = $matches[1];
+       } elseif (preg_match('=https?://(?:mobile\.)?twitter.com/(.*)=i', $hookData['uri'], $matches)) {
+               $nick = $matches[1];
+       } else {
+               return;
+       }
+
+       $user = twitter_fetchuser($nick);
+
+       if ($user) {
+               $hookData['result'] = twitter_user_to_contact($user);
+       }
+}
+
 function twitter_action(App $a, $uid, $pid, $action)
 {
        $ckey = DI::config()->get('twitter', 'consumerkey');
@@ -480,24 +491,31 @@ function twitter_action(App $a, $uid, $pid, $action)
 
        $post = ['id' => $pid];
 
-       Logger::log("twitter_action '" . $action . "' ID: " . $pid . " data: " . print_r($post, true), Logger::DATA);
+       Logger::debug('before action', ['action' => $action, 'pid' => $pid, 'data' => $post]);
 
        switch ($action) {
-               case "delete":
+               case 'delete':
                        // To-Do: $result = $connection->post('statuses/destroy', $post);
                        $result = [];
                        break;
-               case "like":
+               case 'like':
                        $result = $connection->post('favorites/create', $post);
+                       if ($connection->getLastHttpCode() != 200) {
+                               Logger::error('Unable to create favorite', ['result' => $result]);
+                       }
                        break;
-               case "unlike":
+               case 'unlike':
                        $result = $connection->post('favorites/destroy', $post);
+                       if ($connection->getLastHttpCode() != 200) {
+                               Logger::error('Unable to destroy favorite', ['result' => $result]);
+                       }
                        break;
                default:
-                       Logger::log('Unhandled action ' . $action, Logger::DEBUG);
+                       Logger::warning('Unhandled action', ['action' => $action]);
                        $result = [];
        }
-       Logger::log("twitter_action '" . $action . "' send, result: " . print_r($result, true), Logger::DEBUG);
+
+       Logger::info('after action', ['action' => $action, 'result' => $result]);
 }
 
 function twitter_post_hook(App $a, array &$b)
@@ -621,7 +639,7 @@ function twitter_post_hook(App $a, array &$b)
 
                $b['body'] = twitter_update_mentions($b['body']);
 
-               $msgarr = ItemContent::getPlaintextPost($b, $max_char, true, 8);
+               $msgarr = ItemContent::getPlaintextPost($b, $max_char, true, BBCode::TWITTER);
                Logger::info('Got plaintext', ['id' => $b['id'], 'message' => $msgarr]);
                $msg = $msgarr["text"];
 
@@ -851,7 +869,7 @@ function twitter_prepare_body(App $a, array &$b)
                        }
                }
 
-               $msgarr = ItemContent::getPlaintextPost($item, $max_char, true, 8);
+               $msgarr = ItemContent::getPlaintextPost($item, $max_char, true, BBCode::TWITTER);
                $msg = $msgarr["text"];
 
                if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
@@ -887,24 +905,23 @@ function twitter_do_mirrorpost(App $a, $uid, $post)
 
        if (!empty($post->retweeted_status)) {
                // We don't support nested shares, so we mustn't show quotes as shares on retweets
-               $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true);
+               $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true, -1);
 
                if (empty($item['body'])) {
                        return [];
                }
 
-               $datarray['body'] = "\n" . share_header(
+               $datarray['body'] = "\n" . BBCode::getShareOpeningTag(
                        $item['author-name'],
                        $item['author-link'],
                        $item['author-avatar'],
-                       '',
-                       $item['created'],
-                       $item['plink']
+                       $item['plink'],
+                       $item['created']
                );
 
                $datarray['body'] .= $item['body'] . '[/share]';
        } else {
-               $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false);
+               $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false, -1);
 
                if (empty($item['body'])) {
                        return [];
@@ -1036,50 +1053,79 @@ function twitter_get_relation($uid, $target, $contact = [])
 
        try {
                $status = $connection->get('friendships/show', $parameters);
-       } catch (TwitterOAuthException $e) {
-               Logger::info('Error fetching friendship status', ['user' => $uid, 'target' => $target, 'message' => $e->getMessage()]);
-               return $relation;
-       }
+               if ($connection->getLastHttpCode() !== 200) {
+                       throw new Exception($status->errors[0]->message ?? 'HTTP response code ' . $connection->getLastHttpCode(), $status->errors[0]->code ?? $connection->getLastHttpCode());
+               }
 
-       $following = $status->relationship->source->following;
-       $followed = $status->relationship->source->followed_by;
+               $following = $status->relationship->source->following;
+               $followed = $status->relationship->source->followed_by;
+
+               if ($following && !$followed) {
+                       $relation = Contact::SHARING;
+               } elseif (!$following && $followed) {
+                       $relation = Contact::FOLLOWER;
+               } elseif ($following && $followed) {
+                       $relation = Contact::FRIEND;
+               } elseif (!$following && !$followed) {
+                       $relation = 0;
+               }
 
-       if ($following && !$followed) {
-               $relation = Contact::SHARING;
-       } elseif (!$following && $followed) {
-               $relation = Contact::FOLLOWER;
-       } elseif ($following && $followed) {
-               $relation = Contact::FRIEND;
-       } elseif (!$following && !$followed) {
-               $relation = 0;
+               Logger::info('Fetched friendship relation', ['user' => $uid, 'target' => $target, 'relation' => $relation]);
+       } catch (Throwable $e) {
+               Logger::error('Error fetching friendship status', ['user' => $uid, 'target' => $target, 'message' => $e->getMessage()]);
        }
 
-       Logger::info('Fetched friendship relation', ['user' => $uid, 'target' => $target, 'relation' => $relation]);
-
        return $relation;
 }
 
-function twitter_fetch_contact($uid, $data, $create_user)
+/**
+ * @param $data
+ * @return array
+ */
+function twitter_user_to_contact($data)
 {
        if (empty($data->id_str)) {
-               return -1;
+               return [];
        }
 
-       $avatar = twitter_fix_avatar($data->profile_image_url_https);
-       $url = "https://twitter.com/" . $data->screen_name;
-       $addr = $data->screen_name . "@twitter.com";
+       $baseurl = 'https://twitter.com';
+       $url = $baseurl . '/' . $data->screen_name;
+       $addr = $data->screen_name . '@twitter.com';
+
+       $fields = [
+               'url'      => $url,
+               'network'  => Protocol::TWITTER,
+               'alias'    => 'twitter::' . $data->id_str,
+               'baseurl'  => $baseurl,
+               'name'     => $data->name,
+               'nick'     => $data->screen_name,
+               'addr'     => $addr,
+               'location' => $data->location,
+               'about'    => $data->description,
+               'photo'    => twitter_fix_avatar($data->profile_image_url_https),
+       ];
+
+       return $fields;
+}
 
-       $fields = ['url' => $url, 'network' => Protocol::TWITTER,
-               'alias' => 'twitter::' . $data->id_str,
-               'name' => $data->name, 'nick' => $data->screen_name, 'addr' => $addr,
-                'location' => $data->location, 'about' => $data->description];
+function twitter_fetch_contact($uid, $data, $create_user)
+{
+       $fields = twitter_user_to_contact($data);
+
+       if (empty($fields)) {
+               return -1;
+       }
+
+       // photo comes from twitter_user_to_contact but shouldn't be saved directly in the contact row
+       $avatar = $fields['photo'];
+       unset($fields['photo']);
 
        // Update the public contact
        $pcontact = DBA::selectFirst('contact', ['id'], ['uid' => 0, 'alias' => "twitter::" . $data->id_str]);
        if (DBA::isResult($pcontact)) {
                $cid = $pcontact['id'];
        } else {
-               $cid = Contact::getIdForURL($url, 0, true, $fields);
+               $cid = Contact::getIdForURL($fields['url'], 0, true, $fields);
        }
 
        if (!empty($cid)) {
@@ -1099,7 +1145,7 @@ function twitter_fetch_contact($uid, $data, $create_user)
                // create contact record
                $fields['uid'] = $uid;
                $fields['created'] = DateTimeFormat::utcNow();
-               $fields['nurl'] = Strings::normaliseLink($url);
+               $fields['nurl'] = Strings::normaliseLink($fields['url']);
                $fields['poll'] = 'twitter::' . $data->id_str;
                $fields['rel'] = $relation;
                $fields['priority'] = 1;
@@ -1158,209 +1204,170 @@ function twitter_fetch_contact($uid, $data, $create_user)
        return $contact_id;
 }
 
-function twitter_fetchuser(App $a, $uid, $screen_name = "", $user_id = "")
+/**
+ * @param string $screen_name
+ * @return stdClass|null
+ * @throws Exception
+ */
+function twitter_fetchuser($screen_name)
 {
        $ckey = DI::config()->get('twitter', 'consumerkey');
        $csecret = DI::config()->get('twitter', 'consumersecret');
-       $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
-       $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
-
-       $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
-               intval($uid));
-
-       if (DBA::isResult($r)) {
-               $self = $r[0];
-       } else {
-               return;
-       }
-
-       $parameters = [];
-
-       if ($screen_name != "") {
-               $parameters["screen_name"] = $screen_name;
-       }
-
-       if ($user_id != "") {
-               $parameters["user_id"] = $user_id;
-       }
 
-       // Fetching user data
-       $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
        try {
+               // Fetching user data
+               $connection = new TwitterOAuth($ckey, $csecret);
+               $parameters = ['screen_name' => $screen_name];
                $user = $connection->get('users/show', $parameters);
        } catch (TwitterOAuthException $e) {
-               Logger::log('twitter_fetchuser: Error fetching user ' . $uid . ': ' . $e->getMessage());
-               return;
+               Logger::log('twitter_fetchuser: Error fetching user ' . $screen_name . ': ' . $e->getMessage());
+               return null;
        }
 
        if (!is_object($user)) {
-               return;
+               return null;
        }
 
-       $contact_id = twitter_fetch_contact($uid, $user, true);
-
-       return $contact_id;
+       return $user;
 }
 
-function twitter_expand_entities(App $a, $body, $item, $picture)
+/**
+ * Replaces Twitter entities with Friendica-friendly links.
+ *
+ * The Twitter API gives indices for each entity, which allows for fine-grained replacement.
+ *
+ * First, we need to collect everything that needs to be replaced, what we will replace it with, and the start index.
+ * Then we sort the indices decreasingly, and we replace from the end of the body to the start in order for the next
+ * index to be correct even after the last replacement.
+ *
+ * @param string   $body
+ * @param stdClass $status
+ * @param string   $picture
+ * @return array
+ * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+ */
+function twitter_expand_entities($body, stdClass $status, $picture)
 {
        $plain = $body;
 
-       $tags_arr = [];
-
-       foreach ($item->entities->hashtags AS $hashtag) {
-               $url = '#[url=' . DI::baseUrl()->get() . '/search?tag=' . $hashtag->text . ']' . $hashtag->text . '[/url]';
-               $tags_arr['#' . $hashtag->text] = $url;
-               $body = str_replace('#' . $hashtag->text, $url, $body);
-       }
+       $taglist = [];
 
-       foreach ($item->entities->user_mentions AS $mention) {
-               $url = '@[url=https://twitter.com/' . rawurlencode($mention->screen_name) . ']' . $mention->screen_name . '[/url]';
-               $tags_arr['@' . $mention->screen_name] = $url;
-               $body = str_replace('@' . $mention->screen_name, $url, $body);
-       }
+       $replacementList = [];
 
-       if (isset($item->entities->urls)) {
-               $type = '';
-               $footerurl = '';
-               $footerlink = '';
-               $footer = '';
+       foreach ($status->entities->hashtags AS $hashtag) {
+               $replace = '#[url=' . DI::baseUrl()->get() . '/search?tag=' . $hashtag->text . ']' . $hashtag->text . '[/url]';
+               $taglist['#' . $hashtag->text] = ['#', $hashtag->text, ''];
 
-               foreach ($item->entities->urls as $url) {
-                       $plain = str_replace($url->url, '', $plain);
+               $replacementList[$hashtag->indices[0]] = [
+                       'replace' => $replace,
+                       'length' => $hashtag->indices[1] - $hashtag->indices[0],
+               ];
+       }
 
-                       if ($url->url && $url->expanded_url && $url->display_url) {
-                               // Quote tweet, we just remove the quoted tweet URL from the body, the share block will be added later.
-                               if (!empty($item->quoted_status) && isset($item->quoted_status_id_str)
-                                       && substr($url->expanded_url, -strlen($item->quoted_status_id_str)) == $item->quoted_status_id_str ) {
-                                       $body = str_replace($url->url, '', $body);
-                                       continue;
-                               }
+       foreach ($status->entities->user_mentions AS $mention) {
+               $replace = '@[url=https://twitter.com/' . rawurlencode($mention->screen_name) . ']' . $mention->screen_name . '[/url]';
+               $taglist['@' . $mention->screen_name] = ['@', $mention->screen_name, 'https://twitter.com/' . rawurlencode($mention->screen_name)];
 
-                               $expanded_url = $url->expanded_url;
+               $replacementList[$mention->indices[0]] = [
+                       'replace' => $replace,
+                       'length' => $mention->indices[1] - $mention->indices[0],
+               ];
+       }
 
-                               $final_url = Network::finalUrl($url->expanded_url);
+       // This URL if set will be used to add an attachment at the bottom of the post
+       $attachmentUrl = '';
 
-                               $oembed_data = OEmbed::fetchURL($final_url);
+       foreach ($status->entities->urls ?? [] as $url) {
+               $plain = str_replace($url->url, '', $plain);
 
-                               if (empty($oembed_data) || empty($oembed_data->type)) {
-                                       continue;
-                               }
+               if ($url->url && $url->expanded_url && $url->display_url) {
 
-                               // Quickfix: Workaround for URL with '[' and ']' in it
-                               if (strpos($expanded_url, '[') || strpos($expanded_url, ']')) {
-                                       $expanded_url = $url->url;
-                               }
+                       // Quote tweet, we just remove the quoted tweet URL from the body, the share block will be added later.
+                       if (!empty($status->quoted_status) && isset($status->quoted_status_id_str)
+                               && substr($url->expanded_url, -strlen($status->quoted_status_id_str)) == $status->quoted_status_id_str
+                       ) {
+                               $replacementList[$url->indices[0]] = [
+                                       'replace' => '',
+                                       'length' => $url->indices[1] - $url->indices[0],
+                               ];
+                               continue;
+                       }
 
-                               if ($type == '') {
-                                       $type = $oembed_data->type;
-                               }
+                       $expanded_url = $url->expanded_url;
 
-                               if ($oembed_data->type == 'video') {
-                                       $type = $oembed_data->type;
-                                       $footerurl = $expanded_url;
-                                       $footerlink = '[url=' . $expanded_url . ']' . $url->display_url . '[/url]';
+                       $final_url = Network::finalUrl($url->expanded_url);
 
-                                       $body = str_replace($url->url, $footerlink, $body);
-                               } elseif (($oembed_data->type == 'photo') && isset($oembed_data->url)) {
-                                       $body = str_replace($url->url, '[url=' . $expanded_url . '][img]' . $oembed_data->url . '[/img][/url]', $body);
-                               } elseif ($oembed_data->type != 'link') {
-                                       $body = str_replace($url->url, '[url=' . $expanded_url . ']' . $url->display_url . '[/url]', $body);
-                               } else {
-                                       $img_str = Network::fetchUrl($final_url, true, 4);
+                       $oembed_data = OEmbed::fetchURL($final_url);
 
-                                       $tempfile = tempnam(get_temppath(), 'cache');
-                                       file_put_contents($tempfile, $img_str);
+                       if (empty($oembed_data) || empty($oembed_data->type)) {
+                               continue;
+                       }
 
-                                       // See http://php.net/manual/en/function.exif-imagetype.php#79283
-                                       if (filesize($tempfile) > 11) {
-                                               $mime = image_type_to_mime_type(exif_imagetype($tempfile));
-                                       } else {
-                                               $mime = false;
-                                       }
+                       // Quickfix: Workaround for URL with '[' and ']' in it
+                       if (strpos($expanded_url, '[') || strpos($expanded_url, ']')) {
+                               $expanded_url = $url->url;
+                       }
 
-                                       unlink($tempfile);
+                       if ($oembed_data->type == 'video') {
+                               $attachmentUrl = $expanded_url;
+                               $replace = '';
+                       } elseif (($oembed_data->type == 'photo') && isset($oembed_data->url)) {
+                               $replace = '[url=' . $expanded_url . '][img]' . $oembed_data->url . '[/img][/url]';
+                       } elseif ($oembed_data->type != 'link') {
+                               $replace = '[url=' . $expanded_url . ']' . $url->display_url . '[/url]';
+                       } else {
+                               $img_str = Network::fetchUrl($final_url, true, 4);
 
-                                       if (substr($mime, 0, 6) == 'image/') {
-                                               $type = 'photo';
-                                               $body = str_replace($url->url, '[img]' . $final_url . '[/img]', $body);
-                                       } else {
-                                               $type = $oembed_data->type;
-                                               $footerurl = $expanded_url;
-                                               $footerlink = '[url=' . $expanded_url . ']' . $url->display_url . '[/url]';
+                               $tempfile = tempnam(get_temppath(), 'cache');
+                               file_put_contents($tempfile, $img_str);
 
-                                               $body = str_replace($url->url, $footerlink, $body);
-                                       }
+                               // See http://php.net/manual/en/function.exif-imagetype.php#79283
+                               if (filesize($tempfile) > 11) {
+                                       $mime = image_type_to_mime_type(exif_imagetype($tempfile));
+                               } else {
+                                       $mime = false;
                                }
-                       }
-               }
 
-               // Footer will be taken care of with a share block in the case of a quote
-               if (empty($item->quoted_status)) {
-                       if ($footerurl != '') {
-                               $footer = add_page_info($footerurl, false, $picture);
-                       }
-
-                       if (($footerlink != '') && (trim($footer) != '')) {
-                               $removedlink = trim(str_replace($footerlink, '', $body));
+                               unlink($tempfile);
 
-                               if (($removedlink == '') || strstr($body, $removedlink)) {
-                                       $body = $removedlink;
+                               if (substr($mime, 0, 6) == 'image/') {
+                                       $replace = '[img]' . $final_url . '[/img]';
+                               } else {
+                                       $attachmentUrl = $expanded_url;
+                                       $replace = '';
                                }
-
-                               $body .= $footer;
                        }
 
-                       if ($footer == '' && $picture != '') {
-                               $body .= "\n\n[img]" . $picture . "[/img]\n";
-                       } elseif ($footer == '' && $picture == '') {
-                               $body = add_page_info_to_body($body);
-                       }
+                       $replacementList[$url->indices[0]] = [
+                               'replace' => $replace,
+                               'length' => $url->indices[1] - $url->indices[0],
+                       ];
                }
        }
 
-       // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
-       $tags = BBCode::getTags($body);
+       krsort($replacementList);
 
-       if (count($tags)) {
-               foreach ($tags as $tag) {
-                       if (strstr(trim($tag), ' ')) {
-                               continue;
-                       }
-
-                       if (strpos($tag, '#') === 0) {
-                               if (strpos($tag, '[url=')) {
-                                       continue;
-                               }
-
-                               // don't link tags that are already embedded in links
-                               if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
-                                       continue;
-                               }
-                               if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
-                                       continue;
-                               }
+       foreach ($replacementList as $startIndex => $parameters) {
+               $body = Strings::substringReplace($body, $parameters['replace'], $startIndex, $parameters['length']);
+       }
 
-                               $basetag = str_replace('_', ' ', substr($tag, 1));
-                               $url = '#[url=' . DI::baseUrl()->get() . '/search?tag=' . $basetag . ']' . $basetag . '[/url]';
-                               $body = str_replace($tag, $url, $body);
-                               $tags_arr['#' . $basetag] = $url;
-                       } elseif (strpos($tag, '@') === 0) {
-                               if (strpos($tag, '[url=')) {
-                                       continue;
-                               }
+       // Footer will be taken care of with a share block in the case of a quote
+       if (empty($status->quoted_status)) {
+               $footer = '';
+               if ($attachmentUrl) {
+                       $footer = add_page_info($attachmentUrl, false, $picture);
+               }
 
-                               $basetag = substr($tag, 1);
-                               $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
-                               $body = str_replace($tag, $url, $body);
-                               $tags_arr['@' . $basetag] = $url;
-                       }
+               if (trim($footer)) {
+                       $body .= $footer;
+               } elseif ($picture) {
+                       $body .= "\n\n[img]" . $picture . "[/img]\n";
+               } else {
+                       $body = add_page_info_to_body($body);
                }
        }
 
-       $tags = implode($tags_arr, ',');
-
-       return ['body' => $body, 'tags' => $tags, 'plain' => $plain];
+       return ['body' => $body, 'plain' => $plain, 'taglist' => $taglist];
 }
 
 /**
@@ -1445,7 +1452,20 @@ function twitter_media_entities($post, array &$postarray)
        return '';
 }
 
-function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $only_existing_contact, $noquote)
+/**
+ * Undocumented function
+ *
+ * @param App $a
+ * @param integer $uid User ID
+ * @param object $post Incoming Twitter post
+ * @param array $self
+ * @param bool $create_user Should users be created?
+ * @param bool $only_existing_contact Only import existing contacts if set to "true"
+ * @param bool $noquote
+ * @param integer $uriid URI Id used to store tags. 0 = create a new one; -1 = don't store tags for this post.
+ * @return array item array
+ */
+function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $only_existing_contact, $noquote, int $uriid = 0)
 {
        $postarray = [];
        $postarray['network'] = Protocol::TWITTER;
@@ -1455,6 +1475,10 @@ function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $onl
        $postarray['protocol'] = Conversation::PARCEL_TWITTER;
        $postarray['source'] = json_encode($post);
 
+       if (empty($uriid)) {
+               $uriid = $postarray['uri-id'] = ItemURI::insert(['uri' => $postarray['uri']]);
+       }
+
        // Don't import our own comments
        if (Item::exists(['extid' => $postarray['uri'], 'uid' => $uid])) {
                Logger::log("Item with extid " . $postarray['uri'] . " found.", Logger::DEBUG);
@@ -1554,12 +1578,15 @@ function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $onl
        // Search for media links
        $picture = twitter_media_entities($post, $postarray);
 
-       $converted = twitter_expand_entities($a, $postarray['body'], $post, $picture);
-       $postarray['body'] = $converted["body"];
-       $postarray['tag'] = $converted["tags"];
+       $converted = twitter_expand_entities($postarray['body'], $post, $picture);
+       $postarray['body'] = $converted['body'];
        $postarray['created'] = DateTimeFormat::utc($post->created_at);
        $postarray['edited'] = DateTimeFormat::utc($post->created_at);
 
+       if ($uriid > 0) {
+               twitter_store_tags($uriid, $converted['taglist']);
+       }
+
        $statustext = $converted["plain"];
 
        if (!empty($post->place->name)) {
@@ -1605,29 +1632,45 @@ function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $onl
                }
        }
 
-       if (!empty($post->quoted_status) && !$noquote) {
-               $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
-
-               if (!empty($quoted['body'])) {
-                       $postarray['body'] .= "\n" . share_header(
-                               $quoted['author-name'],
-                               $quoted['author-link'],
-                               $quoted['author-avatar'],
-                               "",
-                               $quoted['created'],
-                               $quoted['plink']
-                       );
-
-                       $postarray['body'] .= $quoted['body'] . '[/share]';
-               } else {
-                       // Quoted post author is blocked/ignored, so we just provide the link to avoid removing quote context.
+       if (!empty($post->quoted_status)) {
+               if ($noquote) {
+                       // To avoid recursive share blocks we just provide the link to avoid removing quote context.
                        $postarray['body'] .= "\n\nhttps://twitter.com/" . $post->quoted_status->user->screen_name . "/status/" . $post->quoted_status->id_str;
+               } else {
+                       $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true, $uriid);
+                       if (!empty($quoted['body'])) {
+                               $postarray['body'] .= "\n" . BBCode::getShareOpeningTag(
+                                               $quoted['author-name'],
+                                               $quoted['author-link'],
+                                               $quoted['author-avatar'],
+                                               $quoted['plink'],
+                                               $quoted['created']
+                                       );
+
+                               $postarray['body'] .= $quoted['body'] . '[/share]';
+                       } else {
+                               // Quoted post author is blocked/ignored, so we just provide the link to avoid removing quote context.
+                               $postarray['body'] .= "\n\nhttps://twitter.com/" . $post->quoted_status->user->screen_name . "/status/" . $post->quoted_status->id_str;
+                       }
                }
        }
 
        return $postarray;
 }
 
+/**
+ * Store tags and mentions
+ *
+ * @param integer $uriid
+ * @param array $taglist
+ */
+function twitter_store_tags(int $uriid, array $taglist)
+{
+       foreach ($taglist as $tag) {
+               Tag::storeByHash($uriid, $tag[0], $tag[1], $tag[2]);
+       }
+}
+
 function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection, array $self)
 {
        Logger::log("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, Logger::DEBUG);
@@ -1800,7 +1843,7 @@ function twitter_fetchhometimeline(App $a, $uid)
                                }
                        }
 
-                       $item = Item::insert($postarray, false, $notify);
+                       $item = Item::insert($postarray, $notify);
                        $postarray["id"] = $item;
 
                        Logger::log('User ' . $uid . ' posted home timeline item ' . $item);
@@ -1883,7 +1926,7 @@ function twitter_fetch_own_contact(App $a, $uid)
                // Fetching user data
                // get() may throw TwitterOAuthException, but we will catch it later
                $user = $connection->get('account/verify_credentials');
-               if (empty($user) || empty($user->id_str)) {
+               if (empty($user->id_str)) {
                        return false;
                }
 
@@ -1978,7 +2021,11 @@ function twitter_update_mentions($body)
 
 function twitter_convert_share(array $attributes, array $author_contact, $content, $is_quote_share)
 {
-       if ($author_contact['network'] == Protocol::TWITTER) {
+       if (empty($author_contact)) {
+               return $content . "\n\n" . $attributes['link'];
+       }
+
+       if (!empty($author_contact['network']) && ($author_contact['network'] == Protocol::TWITTER)) {
                $mention = '@' . $author_contact['nick'];
        } else {
                $mention = $author_contact['addr'];