Merge pull request #990 from MrPetovan/task/share-block-guid
[friendica-addons.git/.git] / twitter / twitter.php
index d846b1a..f743d96 100644 (file)
@@ -69,24 +69,25 @@ use Friendica\App;
 use Friendica\Content\OEmbed;
 use Friendica\Content\Text\BBCode;
 use Friendica\Content\Text\Plaintext;
-use Friendica\Core\Config;
 use Friendica\Core\Hook;
-use Friendica\Core\L10n;
 use Friendica\Core\Logger;
-use Friendica\Core\PConfig;
 use Friendica\Core\Protocol;
 use Friendica\Core\Renderer;
 use Friendica\Core\Worker;
 use Friendica\Database\DBA;
+use Friendica\DI;
 use Friendica\Model\Contact;
 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\Object\Image;
+use Friendica\Protocol\Activity;
 use Friendica\Util\ConfigFileLoader;
 use Friendica\Util\DateTimeFormat;
+use Friendica\Util\Images;
 use Friendica\Util\Network;
 use Friendica\Util\Strings;
 
@@ -109,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');
-       Logger::log("installed twitter");
+       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)
 {
@@ -140,7 +123,7 @@ function twitter_load_config(App $a, ConfigFileLoader $loader)
 
 function twitter_check_item_notification(App $a, array &$notification_data)
 {
-       $own_id = PConfig::get($notification_data["uid"], 'twitter', 'own_id');
+       $own_id = DI::pConfig()->get($notification_data["uid"], 'twitter', 'own_id');
 
        $own_user = q("SELECT `url` FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
                        intval($notification_data["uid"]),
@@ -154,7 +137,7 @@ function twitter_check_item_notification(App $a, array &$notification_data)
 
 function twitter_follow(App $a, array &$contact)
 {
-       Logger::log("twitter_follow: Check if contact is twitter contact. " . $contact["url"], Logger::DEBUG);
+       Logger::info('Check if contact is twitter contact', ['url' => $contact["url"]]);
 
        if (!strstr($contact["url"], "://twitter.com") && !strstr($contact["url"], "@twitter.com")) {
                return;
@@ -166,10 +149,10 @@ function twitter_follow(App $a, array &$contact)
 
        $uid = $a->user["uid"];
 
-       $ckey = Config::get('twitter', 'consumerkey');
-       $csecret = Config::get('twitter', 'consumersecret');
-       $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
-       $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
+       $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');
 
        // If the addon is not configured (general or for this user) quit here
        if (empty($ckey) || empty($csecret) || empty($otoken) || empty($osecret)) {
@@ -180,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;
        }
 }
 
@@ -197,13 +180,13 @@ function twitter_jot_nets(App $a, array &$jotnets_fields)
                return;
        }
 
-       if (PConfig::get(local_user(), 'twitter', 'post')) {
+       if (DI::pConfig()->get(local_user(), 'twitter', 'post')) {
                $jotnets_fields[] = [
                        'type' => 'checkbox',
                        'field' => [
                                'twitter_enable',
-                               L10n::t('Post to Twitter'),
-                               PConfig::get(local_user(), 'twitter', 'post_by_default')
+                               DI::l10n()->t('Post to Twitter'),
+                               DI::pConfig()->get(local_user(), 'twitter', 'post_by_default')
                        ]
                ];
        }
@@ -225,58 +208,58 @@ function twitter_settings_post(App $a)
                 * if the twitter-disconnect checkbox is set, clear the OAuth key/secret pair
                 * from the user configuration
                 */
-               PConfig::delete(local_user(), 'twitter', 'consumerkey');
-               PConfig::delete(local_user(), 'twitter', 'consumersecret');
-               PConfig::delete(local_user(), 'twitter', 'oauthtoken');
-               PConfig::delete(local_user(), 'twitter', 'oauthsecret');
-               PConfig::delete(local_user(), 'twitter', 'post');
-               PConfig::delete(local_user(), 'twitter', 'post_by_default');
-               PConfig::delete(local_user(), 'twitter', 'lastid');
-               PConfig::delete(local_user(), 'twitter', 'mirror_posts');
-               PConfig::delete(local_user(), 'twitter', 'import');
-               PConfig::delete(local_user(), 'twitter', 'create_user');
-               PConfig::delete(local_user(), 'twitter', 'own_id');
+               DI::pConfig()->delete(local_user(), 'twitter', 'consumerkey');
+               DI::pConfig()->delete(local_user(), 'twitter', 'consumersecret');
+               DI::pConfig()->delete(local_user(), 'twitter', 'oauthtoken');
+               DI::pConfig()->delete(local_user(), 'twitter', 'oauthsecret');
+               DI::pConfig()->delete(local_user(), 'twitter', 'post');
+               DI::pConfig()->delete(local_user(), 'twitter', 'post_by_default');
+               DI::pConfig()->delete(local_user(), 'twitter', 'lastid');
+               DI::pConfig()->delete(local_user(), 'twitter', 'mirror_posts');
+               DI::pConfig()->delete(local_user(), 'twitter', 'import');
+               DI::pConfig()->delete(local_user(), 'twitter', 'create_user');
+               DI::pConfig()->delete(local_user(), 'twitter', 'own_id');
        } else {
                if (isset($_POST['twitter-pin'])) {
                        //  if the user supplied us with a PIN from Twitter, let the magic of OAuth happen
-                       Logger::log('got a Twitter PIN');
-                       $ckey    = Config::get('twitter', 'consumerkey');
-                       $csecret = Config::get('twitter', 'consumersecret');
+                       Logger::notice('got a Twitter PIN');
+                       $ckey    = DI::config()->get('twitter', 'consumerkey');
+                       $csecret = DI::config()->get('twitter', 'consumersecret');
                        //  the token and secret for which the PIN was generated were hidden in the settings
                        //  form as token and token2, we need a new connection to Twitter using these token
                        //  and secret to request a Access Token with the PIN
                        try {
                                if (empty($_POST['twitter-pin'])) {
-                                       throw new Exception(L10n::t('You submitted an empty PIN, please Sign In with Twitter again to get a new one.'));
+                                       throw new Exception(DI::l10n()->t('You submitted an empty PIN, please Sign In with Twitter again to get a new one.'));
                                }
 
                                $connection = new TwitterOAuth($ckey, $csecret, $_POST['twitter-token'], $_POST['twitter-token2']);
                                $token = $connection->oauth("oauth/access_token", ["oauth_verifier" => $_POST['twitter-pin']]);
                                //  ok, now that we have the Access Token, save them in the user config
-                               PConfig::set(local_user(), 'twitter', 'oauthtoken', $token['oauth_token']);
-                               PConfig::set(local_user(), 'twitter', 'oauthsecret', $token['oauth_token_secret']);
-                               PConfig::set(local_user(), 'twitter', 'post', 1);
+                               DI::pConfig()->set(local_user(), 'twitter', 'oauthtoken', $token['oauth_token']);
+                               DI::pConfig()->set(local_user(), 'twitter', 'oauthsecret', $token['oauth_token_secret']);
+                               DI::pConfig()->set(local_user(), 'twitter', 'post', 1);
                        } catch(Exception $e) {
                                info($e->getMessage());
                        } catch(TwitterOAuthException $e) {
                                info($e->getMessage());
                        }
                        //  reload the Addon Settings page, if we don't do it see Bug #42
-                       $a->internalRedirect('settings/connectors');
+                       DI::baseUrl()->redirect('settings/connectors');
                } else {
                        //  if no PIN is supplied in the POST variables, the user has changed the setting
                        //  to post a tweet for every new __public__ posting to the wall
-                       PConfig::set(local_user(), 'twitter', 'post', intval($_POST['twitter-enable']));
-                       PConfig::set(local_user(), 'twitter', 'post_by_default', intval($_POST['twitter-default']));
-                       PConfig::set(local_user(), 'twitter', 'mirror_posts', intval($_POST['twitter-mirror']));
-                       PConfig::set(local_user(), 'twitter', 'import', intval($_POST['twitter-import']));
-                       PConfig::set(local_user(), 'twitter', 'create_user', intval($_POST['twitter-create_user']));
+                       DI::pConfig()->set(local_user(), 'twitter', 'post', intval($_POST['twitter-enable']));
+                       DI::pConfig()->set(local_user(), 'twitter', 'post_by_default', intval($_POST['twitter-default']));
+                       DI::pConfig()->set(local_user(), 'twitter', 'mirror_posts', intval($_POST['twitter-mirror']));
+                       DI::pConfig()->set(local_user(), 'twitter', 'import', intval($_POST['twitter-import']));
+                       DI::pConfig()->set(local_user(), 'twitter', 'create_user', intval($_POST['twitter-create_user']));
 
                        if (!intval($_POST['twitter-mirror'])) {
-                               PConfig::delete(local_user(), 'twitter', 'lastid');
+                               DI::pConfig()->delete(local_user(), 'twitter', 'lastid');
                        }
 
-                       info(L10n::t('Twitter settings updated.') . EOL);
+                       info(DI::l10n()->t('Twitter settings updated.') . EOL);
                }
        }
 }
@@ -286,38 +269,38 @@ function twitter_settings(App $a, &$s)
        if (!local_user()) {
                return;
        }
-       $a->page['htmlhead'] .= '<link rel="stylesheet"  type="text/css" href="' . $a->getBaseURL() . '/addon/twitter/twitter.css' . '" media="all" />' . "\r\n";
+       DI::page()['htmlhead'] .= '<link rel="stylesheet"  type="text/css" href="' . DI::baseUrl()->get() . '/addon/twitter/twitter.css' . '" media="all" />' . "\r\n";
        /*       * *
         * 1) Check that we have global consumer key & secret
         * 2) If no OAuthtoken & stuff is present, generate button to get some
         * 3) Checkbox for "Send public notices (280 chars only)
         */
-       $ckey    = Config::get('twitter', 'consumerkey');
-       $csecret = Config::get('twitter', 'consumersecret');
-       $otoken  = PConfig::get(local_user(), 'twitter', 'oauthtoken');
-       $osecret = PConfig::get(local_user(), 'twitter', 'oauthsecret');
+       $ckey    = DI::config()->get('twitter', 'consumerkey');
+       $csecret = DI::config()->get('twitter', 'consumersecret');
+       $otoken  = DI::pConfig()->get(local_user(), 'twitter', 'oauthtoken');
+       $osecret = DI::pConfig()->get(local_user(), 'twitter', 'oauthsecret');
 
-       $enabled            = intval(PConfig::get(local_user(), 'twitter', 'post'));
-       $defenabled         = intval(PConfig::get(local_user(), 'twitter', 'post_by_default'));
-       $mirrorenabled      = intval(PConfig::get(local_user(), 'twitter', 'mirror_posts'));
-       $importenabled      = intval(PConfig::get(local_user(), 'twitter', 'import'));
-       $create_userenabled = intval(PConfig::get(local_user(), 'twitter', 'create_user'));
+       $enabled            = intval(DI::pConfig()->get(local_user(), 'twitter', 'post'));
+       $defenabled         = intval(DI::pConfig()->get(local_user(), 'twitter', 'post_by_default'));
+       $mirrorenabled      = intval(DI::pConfig()->get(local_user(), 'twitter', 'mirror_posts'));
+       $importenabled      = intval(DI::pConfig()->get(local_user(), 'twitter', 'import'));
+       $create_userenabled = intval(DI::pConfig()->get(local_user(), 'twitter', 'create_user'));
 
        $css = (($enabled) ? '' : '-disabled');
 
        $s .= '<span id="settings_twitter_inflated" class="settings-block fakelink" style="display: block;" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
-       $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
+       $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . DI::l10n()->t('Twitter Import/Export/Mirror') . '</h3>';
        $s .= '</span>';
        $s .= '<div id="settings_twitter_expanded" class="settings-block" style="display: none;">';
        $s .= '<span class="fakelink" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
-       $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
+       $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . DI::l10n()->t('Twitter Import/Export/Mirror') . '</h3>';
        $s .= '</span>';
 
        if ((!$ckey) && (!$csecret)) {
                /* no global consumer keys
                 * display warning and skip personal config
                 */
-               $s .= '<p>' . L10n::t('No consumer key pair for Twitter found. Please contact your site administrator.') . '</p>';
+               $s .= '<p>' . DI::l10n()->t('No consumer key pair for Twitter found. Please contact your site administrator.') . '</p>';
        } else {
                // ok we have a consumer key pair now look into the OAuth stuff
                if ((!$otoken) && (!$osecret)) {
@@ -329,17 +312,17 @@ function twitter_settings(App $a, &$s)
                        $connection = new TwitterOAuth($ckey, $csecret);
                        try {
                                $result = $connection->oauth('oauth/request_token', ['oauth_callback' => 'oob']);
-                               $s .= '<p>' . L10n::t('At this Friendica instance the Twitter addon was enabled but you have not yet connected your account to your Twitter account. To do so click the button below to get a PIN from Twitter which you have to copy into the input box below and submit the form. Only your <strong>public</strong> posts will be posted to Twitter.') . '</p>';
-                               $s .= '<a href="' . $connection->url('oauth/authorize', ['oauth_token' => $result['oauth_token']]) . '" target="_twitter"><img src="addon/twitter/lighter.png" alt="' . L10n::t('Log in with Twitter') . '"></a>';
+                               $s .= '<p>' . DI::l10n()->t('At this Friendica instance the Twitter addon was enabled but you have not yet connected your account to your Twitter account. To do so click the button below to get a PIN from Twitter which you have to copy into the input box below and submit the form. Only your <strong>public</strong> posts will be posted to Twitter.') . '</p>';
+                               $s .= '<a href="' . $connection->url('oauth/authorize', ['oauth_token' => $result['oauth_token']]) . '" target="_twitter"><img src="addon/twitter/lighter.png" alt="' . DI::l10n()->t('Log in with Twitter') . '"></a>';
                                $s .= '<div id="twitter-pin-wrapper">';
-                               $s .= '<label id="twitter-pin-label" for="twitter-pin">' . L10n::t('Copy the PIN from Twitter here') . '</label>';
+                               $s .= '<label id="twitter-pin-label" for="twitter-pin">' . DI::l10n()->t('Copy the PIN from Twitter here') . '</label>';
                                $s .= '<input id="twitter-pin" type="text" name="twitter-pin" />';
                                $s .= '<input id="twitter-token" type="hidden" name="twitter-token" value="' . $result['oauth_token'] . '" />';
                                $s .= '<input id="twitter-token2" type="hidden" name="twitter-token2" value="' . $result['oauth_token_secret'] . '" />';
                                $s .= '</div><div class="clear"></div>';
-                               $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
+                               $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . DI::l10n()->t('Save Settings') . '" /></div>';
                        } catch (TwitterOAuthException $e) {
-                               $s .= '<p>' . L10n::t('An error occured: ') . $e->getMessage() . '</p>';
+                               $s .= '<p>' . DI::l10n()->t('An error occured: ') . $e->getMessage() . '</p>';
                        }
                } else {
                        /*                       * *
@@ -356,8 +339,8 @@ function twitter_settings(App $a, &$s)
                                    property_exists($details, 'description') &&
                                    property_exists($details, 'profile_image_url')) {
                                        $s .= '<div id="twitter-info" >
-                                       <p>' . L10n::t('Currently connected to: ') . '<a href="https://twitter.com/' . $details->screen_name . '" target="_twitter">' . $details->screen_name . '</a>
-                                               <button type="submit" name="twitter-disconnect" value="1">' . L10n::t('Disconnect') . '</button>
+                                       <p>' . DI::l10n()->t('Currently connected to: ') . '<a href="https://twitter.com/' . $details->screen_name . '" target="_twitter">' . $details->screen_name . '</a>
+                                               <button type="submit" name="twitter-disconnect" value="1">' . DI::l10n()->t('Disconnect') . '</button>
                                        </p>
                                        <p id="twitter-info-block">
                                                <a href="https://twitter.com/' . $details->screen_name . '" target="_twitter"><img id="twitter-avatar" src="' . $details->profile_image_url . '" /></a>
@@ -373,27 +356,27 @@ function twitter_settings(App $a, &$s)
                                $s .= '<div class="clear"></div>';
 
                                $s .= Renderer::replaceMacros($field_checkbox, [
-                                       '$field' => ['twitter-enable', L10n::t('Allow posting to Twitter'), $enabled, L10n::t('If enabled all your <strong>public</strong> postings can be posted to the associated Twitter account. You can choose to do so by default (here) or for every posting separately in the posting options when writing the entry.')]
+                                       '$field' => ['twitter-enable', DI::l10n()->t('Allow posting to Twitter'), $enabled, DI::l10n()->t('If enabled all your <strong>public</strong> postings can be posted to the associated Twitter account. You can choose to do so by default (here) or for every posting separately in the posting options when writing the entry.')]
                                ]);
                                if ($a->user['hidewall']) {
-                                       $s .= '<p>' . L10n::t('<strong>Note</strong>: Due to your privacy settings (<em>Hide your profile details from unknown viewers?</em>) the link potentially included in public postings relayed to Twitter will lead the visitor to a blank page informing the visitor that the access to your profile has been restricted.') . '</p>';
+                                       $s .= '<p>' . DI::l10n()->t('<strong>Note</strong>: Due to your privacy settings (<em>Hide your profile details from unknown viewers?</em>) the link potentially included in public postings relayed to Twitter will lead the visitor to a blank page informing the visitor that the access to your profile has been restricted.') . '</p>';
                                }
                                $s .= Renderer::replaceMacros($field_checkbox, [
-                                       '$field' => ['twitter-default', L10n::t('Send public postings to Twitter by default'), $defenabled, '']
+                                       '$field' => ['twitter-default', DI::l10n()->t('Send public postings to Twitter by default'), $defenabled, '']
                                ]);
                                $s .= Renderer::replaceMacros($field_checkbox, [
-                                       '$field' => ['twitter-mirror', L10n::t('Mirror all posts from twitter that are no replies'), $mirrorenabled, '']
+                                       '$field' => ['twitter-mirror', DI::l10n()->t('Mirror all posts from twitter that are no replies'), $mirrorenabled, '']
                                ]);
                                $s .= Renderer::replaceMacros($field_checkbox, [
-                                       '$field' => ['twitter-import', L10n::t('Import the remote timeline'), $importenabled, '']
+                                       '$field' => ['twitter-import', DI::l10n()->t('Import the remote timeline'), $importenabled, '']
                                ]);
                                $s .= Renderer::replaceMacros($field_checkbox, [
-                                       '$field' => ['twitter-create_user', L10n::t('Automatically create contacts'), $create_userenabled, L10n::t('This will automatically create a contact in Friendica as soon as you receive a message from an existing contact via the Twitter network. If you do not enable this, you need to manually add those Twitter contacts in Friendica from whom you would like to see posts here. However if enabled, you cannot merely remove a twitter contact from the Friendica contact list, as it will recreate this contact when they post again.')]
+                                       '$field' => ['twitter-create_user', DI::l10n()->t('Automatically create contacts'), $create_userenabled, DI::l10n()->t('This will automatically create a contact in Friendica as soon as you receive a message from an existing contact via the Twitter network. If you do not enable this, you need to manually add those Twitter contacts in Friendica from whom you would like to see posts here. However if enabled, you cannot merely remove a twitter contact from the Friendica contact list, as it will recreate this contact when they post again.')]
                                ]);
                                $s .= '<div class="clear"></div>';
-                               $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
+                               $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . DI::l10n()->t('Save Settings') . '" /></div>';
                        } catch (TwitterOAuthException $e) {
-                               $s .= '<p>' . L10n::t('An error occured: ') . $e->getMessage() . '</p>';
+                               $s .= '<p>' . DI::l10n()->t('An error occured: ') . $e->getMessage() . '</p>';
                        }
                }
        }
@@ -425,10 +408,10 @@ function twitter_hook_fork(App $a, array &$b)
                return;
        }
 
-       if (PConfig::get($post['uid'], 'twitter', 'import')) {
+       if (DI::pConfig()->get($post['uid'], 'twitter', 'import')) {
                // Don't fork if it isn't a reply to a twitter post
                if (($post['parent'] != $post['id']) && !Item::exists(['id' => $post['parent'], 'network' => Protocol::TWITTER])) {
-                       Logger::log('No twitter parent found for item ' . $post['id']);
+                       Logger::notice('No twitter parent found', ['item' => $post['id']]);
                        $b['execute'] = false;
                        return;
                }
@@ -451,11 +434,11 @@ function twitter_post_local(App $a, array &$b)
                return;
        }
 
-       $twitter_post = intval(PConfig::get(local_user(), 'twitter', 'post'));
+       $twitter_post = intval(DI::pConfig()->get(local_user(), 'twitter', 'post'));
        $twitter_enable = (($twitter_post && !empty($_REQUEST['twitter_enable'])) ? intval($_REQUEST['twitter_enable']) : 0);
 
        // if API is used, default to the chosen settings
-       if ($b['api_source'] && intval(PConfig::get(local_user(), 'twitter', 'post_by_default'))) {
+       if ($b['api_source'] && intval(DI::pConfig()->get(local_user(), 'twitter', 'post_by_default'))) {
                $twitter_enable = 1;
        }
 
@@ -470,41 +453,75 @@ 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 = Config::get('twitter', 'consumerkey');
-       $csecret = Config::get('twitter', 'consumersecret');
-       $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
-       $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
+       $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');
 
        $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
 
        $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)
 {
        // Post to Twitter
-       if (!PConfig::get($b["uid"], 'twitter', 'import')
+       if (!DI::pConfig()->get($b["uid"], 'twitter', 'import')
                && ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))) {
                return;
        }
@@ -556,11 +573,11 @@ function twitter_post_hook(App $a, array &$b)
                }
        }
 
-       if (($b['verb'] == ACTIVITY_POST) && $b['deleted']) {
+       if (($b['verb'] == Activity::POST) && $b['deleted']) {
                twitter_action($a, $b["uid"], substr($orig_post["uri"], 9), "delete");
        }
 
-       if ($b['verb'] == ACTIVITY_LIKE) {
+       if ($b['verb'] == Activity::LIKE) {
                Logger::log("twitter_post_hook: parameter 2 " . substr($b["thr-parent"], 9), Logger::DEBUG);
                if ($b['deleted']) {
                        twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "unlike");
@@ -586,12 +603,12 @@ function twitter_post_hook(App $a, array &$b)
 
        Logger::notice('twitter post invoked', ['id' => $b['id'], 'guid' => $b['guid']]);
 
-       PConfig::load($b['uid'], 'twitter');
+       DI::pConfig()->load($b['uid'], 'twitter');
 
-       $ckey    = Config::get('twitter', 'consumerkey');
-       $csecret = Config::get('twitter', 'consumersecret');
-       $otoken  = PConfig::get($b['uid'], 'twitter', 'oauthtoken');
-       $osecret = PConfig::get($b['uid'], 'twitter', 'oauthsecret');
+       $ckey    = DI::config()->get('twitter', 'consumerkey');
+       $csecret = DI::config()->get('twitter', 'consumersecret');
+       $otoken  = DI::pConfig()->get($b['uid'], 'twitter', 'oauthtoken');
+       $osecret = DI::pConfig()->get($b['uid'], 'twitter', 'oauthsecret');
 
        if ($ckey && $csecret && $otoken && $osecret) {
                Logger::log('twitter: we have customer key and oauth stuff, going to send.', Logger::DEBUG);
@@ -622,15 +639,15 @@ function twitter_post_hook(App $a, array &$b)
 
                $b['body'] = twitter_update_mentions($b['body']);
 
-               $msgarr = ItemContent::getPlaintextPost($b, $max_char, true, 8);
-               Logger::info('Got plaintext', $msgarr);
+               $msgarr = ItemContent::getPlaintextPost($b, $max_char, true, BBCode::TWITTER);
+               Logger::info('Got plaintext', ['id' => $b['id'], 'message' => $msgarr]);
                $msg = $msgarr["text"];
 
                if (($msg == "") && isset($msgarr["title"])) {
                        $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
                }
 
-               if (($msgarr['url'] == $b['plink']) && !empty($msgarr['images']) && (count($msgarr['images']) <= 4)) {
+               if (!empty($msgarr['url']) && ($msgarr['url'] == $b['plink']) && !empty($msgarr['images']) && (count($msgarr['images']) <= 4)) {
                        $url_added = false;
                } elseif (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
                        $msg .= "\n" . $msgarr["url"];
@@ -640,6 +657,7 @@ function twitter_post_hook(App $a, array &$b)
                }
 
                if (empty($msg)) {
+                       Logger::info('Empty message', ['id' => $b['id']]);
                        return;
                }
 
@@ -647,6 +665,7 @@ function twitter_post_hook(App $a, array &$b)
                $post = [];
 
                if (!empty($msgarr['images'])) {
+                       Logger::info('Got images', ['id' => $b['id'], 'images' => $msgarr['images']]);
                        try {
                                $media_ids = [];
                                foreach ($msgarr['images'] as $image) {
@@ -659,6 +678,7 @@ function twitter_post_hook(App $a, array &$b)
                                        $tempfile = tempnam(get_temppath(), 'cache');
                                        file_put_contents($tempfile, $img_str);
 
+                                       Logger::info('Uploading', ['id' => $b['id'], 'image' => $image['url']]);
                                        $media = $connection->upload('media/upload', ['media' => $tempfile]);
 
                                        unlink($tempfile);
@@ -670,10 +690,10 @@ function twitter_post_hook(App $a, array &$b)
                                                        $data = ['media_id' => $media->media_id_string,
                                                                'alt_text' => ['text' => substr($image['description'], 0, 420)]];
                                                        $ret = $cb->media_metadata_create($data);
-                                                       Logger::info('Metadata create', ['data' => $data, 'return' => json_encode($ret)]);
+                                                       Logger::info('Metadata create', ['id' => $b['id'], 'data' => $data, 'return' => json_encode($ret)]);
                                                }
                                        } else {
-                                               throw new Exception('Failed upload of ' . $image['url']);
+                                               throw new Exception('Failed upload', ['id' => $b['id'], 'image' => $image['url']]);
                                        }
                                }
                                $post['media_ids'] = implode(',', $media_ids);
@@ -681,7 +701,7 @@ function twitter_post_hook(App $a, array &$b)
                                        unset($post['media_ids']);
                                }
                        } catch (Exception $e) {
-                               Logger::log('Exception when trying to send to Twitter: ' . $e->getMessage());
+                               Logger::info('Exception when trying to send to Twitter', ['id' => $b['id'], 'message' => $e->getMessage()]);
                        }
                }
 
@@ -693,17 +713,17 @@ function twitter_post_hook(App $a, array &$b)
 
                $url = 'statuses/update';
                $result = $connection->post($url, $post);
-               Logger::log('twitter_post send, result: ' . print_r($result, true), Logger::DEBUG);
+               Logger::info('twitter_post send', ['id' => $b['id'], 'result' => $result]);
 
                if (!empty($result->source)) {
-                       Config::set("twitter", "application_name", strip_tags($result->source));
+                       DI::config()->set("twitter", "application_name", strip_tags($result->source));
                }
 
                if (!empty($result->errors)) {
-                       Logger::log('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
+                       Logger::info('Send to Twitter failed', ['id' => $b['id'], 'error' => $result->errors]);
                        Worker::defer();
                } elseif ($iscomment) {
-                       Logger::log('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
+                       Logger::info('Update extid', ['id' => $b['id'], 'extid' => $result->id_str]);
                        Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $b['id']]);
                }
        }
@@ -713,9 +733,9 @@ function twitter_addon_admin_post(App $a)
 {
        $consumerkey    = !empty($_POST['consumerkey'])    ? Strings::escapeTags(trim($_POST['consumerkey']))    : '';
        $consumersecret = !empty($_POST['consumersecret']) ? Strings::escapeTags(trim($_POST['consumersecret'])) : '';
-       Config::set('twitter', 'consumerkey', $consumerkey);
-       Config::set('twitter', 'consumersecret', $consumersecret);
-       info(L10n::t('Settings updated.') . EOL);
+       DI::config()->set('twitter', 'consumerkey', $consumerkey);
+       DI::config()->set('twitter', 'consumersecret', $consumersecret);
+       info(DI::l10n()->t('Settings updated.') . EOL);
 }
 
 function twitter_addon_admin(App $a, &$o)
@@ -723,18 +743,18 @@ function twitter_addon_admin(App $a, &$o)
        $t = Renderer::getMarkupTemplate("admin.tpl", "addon/twitter/");
 
        $o = Renderer::replaceMacros($t, [
-               '$submit' => L10n::t('Save Settings'),
+               '$submit' => DI::l10n()->t('Save Settings'),
                // name, label, value, help, [extra values]
-               '$consumerkey' => ['consumerkey', L10n::t('Consumer key'), Config::get('twitter', 'consumerkey'), ''],
-               '$consumersecret' => ['consumersecret', L10n::t('Consumer secret'), Config::get('twitter', 'consumersecret'), ''],
+               '$consumerkey' => ['consumerkey', DI::l10n()->t('Consumer key'), DI::config()->get('twitter', 'consumerkey'), ''],
+               '$consumersecret' => ['consumersecret', DI::l10n()->t('Consumer secret'), DI::config()->get('twitter', 'consumersecret'), ''],
        ]);
 }
 
 function twitter_cron(App $a)
 {
-       $last = Config::get('twitter', 'last_poll');
+       $last = DI::config()->get('twitter', 'last_poll');
 
-       $poll_interval = intval(Config::get('twitter', 'poll_interval'));
+       $poll_interval = intval(DI::config()->get('twitter', 'poll_interval'));
        if (!$poll_interval) {
                $poll_interval = TWITTER_DEFAULT_POLL_INTERVAL;
        }
@@ -742,21 +762,21 @@ function twitter_cron(App $a)
        if ($last) {
                $next = $last + ($poll_interval * 60);
                if ($next > time()) {
-                       Logger::log('twitter: poll intervall not reached');
+                       Logger::notice('twitter: poll intervall not reached');
                        return;
                }
        }
-       Logger::log('twitter: cron_start');
+       Logger::notice('twitter: cron_start');
 
        $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'mirror_posts' AND `v` = '1'");
        if (DBA::isResult($r)) {
                foreach ($r as $rr) {
-                       Logger::log('twitter: fetching for user ' . $rr['uid']);
+                       Logger::notice('Fetching', ['user' => $rr['uid']]);
                        Worker::add(['priority' => PRIORITY_MEDIUM, 'force_priority' => true], "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
                }
        }
 
-       $abandon_days = intval(Config::get('system', 'account_abandon_days'));
+       $abandon_days = intval(DI::config()->get('system', 'account_abandon_days'));
        if ($abandon_days < 1) {
                $abandon_days = 0;
        }
@@ -769,17 +789,17 @@ function twitter_cron(App $a)
                        if ($abandon_days != 0) {
                                $user = q("SELECT `login_date` FROM `user` WHERE uid=%d AND `login_date` >= '%s'", $rr['uid'], $abandon_limit);
                                if (!DBA::isResult($user)) {
-                                       Logger::log('abandoned account: timeline from user ' . $rr['uid'] . ' will not be imported');
+                                       Logger::notice('abandoned account: timeline from user will not be imported', ['user' => $rr['uid']]);
                                        continue;
                                }
                        }
 
-                       Logger::log('twitter: importing timeline from user ' . $rr['uid']);
+                       Logger::notice('importing timeline', ['user' => $rr['uid']]);
                        Worker::add(['priority' => PRIORITY_MEDIUM, 'force_priority' => true], "addon/twitter/twitter_sync.php", 2, (int) $rr['uid']);
                        /*
                          // To-Do
                          // check for new contacts once a day
-                         $last_contact_check = PConfig::get($rr['uid'],'pumpio','contact_check');
+                         $last_contact_check = DI::pConfig()->get($rr['uid'],'pumpio','contact_check');
                          if($last_contact_check)
                          $next_contact_check = $last_contact_check + 86400;
                          else
@@ -787,42 +807,43 @@ function twitter_cron(App $a)
 
                          if($next_contact_check <= time()) {
                          pumpio_getallusers($a, $rr["uid"]);
-                         PConfig::set($rr['uid'],'pumpio','contact_check',time());
+                         DI::pConfig()->set($rr['uid'],'pumpio','contact_check',time());
                          }
                         */
                }
        }
 
-       Logger::log('twitter: cron_end');
+       Logger::notice('twitter: cron_end');
 
-       Config::set('twitter', 'last_poll', time());
+       DI::config()->set('twitter', 'last_poll', time());
 }
 
 function twitter_expire(App $a)
 {
-       $days = Config::get('twitter', 'expire');
+       $days = DI::config()->get('twitter', 'expire');
 
        if ($days == 0) {
                return;
        }
 
-       $r = Item::select(['id'], ['deleted' => true, 'network' => Protocol::TWITTER]);
+       $r = Item::select(['id', 'guid'], ['deleted' => true, 'network' => Protocol::TWITTER]);
        while ($row = DBA::fetch($r)) {
+               Logger::info('[twitter] Delete expired item', ['id' => $row['id'], 'guid' => $row['guid'], 'callstack' => \Friendica\Core\System::callstack()]);
                DBA::delete('item', ['id' => $row['id']]);
        }
        DBA::close($r);
 
-       Logger::log('twitter_expire: expire_start');
+       Logger::notice('twitter_expire: expire_start');
 
        $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
        if (DBA::isResult($r)) {
                foreach ($r as $rr) {
-                       Logger::log('twitter_expire: user ' . $rr['uid']);
+                       Logger::notice('twitter_expire', ['user' => $rr['uid']]);
                        Item::expire($rr['uid'], $days, Protocol::TWITTER, true);
                }
        }
 
-       Logger::log('twitter_expire: expire_end');
+       Logger::notice('twitter_expire: expire_end');
 }
 
 function twitter_prepare_body(App $a, array &$b)
@@ -834,7 +855,7 @@ function twitter_prepare_body(App $a, array &$b)
        if ($b["preview"]) {
                $max_char = 280;
                $item = $b["item"];
-               $item["plink"] = $a->getBaseURL() . "/display/" . $item["guid"];
+               $item["plink"] = DI::baseUrl()->get() . "/display/" . $item["guid"];
 
                $condition = ['uri' => $item["thr-parent"], 'uid' => local_user()];
                $orig_post = Item::selectFirst(['author-link'], $condition);
@@ -848,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")) {
@@ -884,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 [];
@@ -926,16 +946,16 @@ function twitter_do_mirrorpost(App $a, $uid, $post)
 
 function twitter_fetchtimeline(App $a, $uid)
 {
-       $ckey    = Config::get('twitter', 'consumerkey');
-       $csecret = Config::get('twitter', 'consumersecret');
-       $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
-       $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
-       $lastid  = PConfig::get($uid, 'twitter', 'lastid');
+       $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');
+       $lastid  = DI::pConfig()->get($uid, 'twitter', 'lastid');
 
-       $application_name = Config::get('twitter', 'application_name');
+       $application_name = DI::config()->get('twitter', 'application_name');
 
        if ($application_name == "") {
-               $application_name = $a->getHostName();
+               $application_name = DI::baseUrl()->getHostname();
        }
 
        $has_picture = false;
@@ -956,12 +976,12 @@ function twitter_fetchtimeline(App $a, $uid)
        try {
                $items = $connection->get('statuses/user_timeline', $parameters);
        } catch (TwitterOAuthException $e) {
-               Logger::log('Error fetching timeline for user ' . $uid . ': ' . $e->getMessage());
+               Logger::notice('Error fetching timeline', ['user' => $uid, 'message' => $e->getMessage()]);
                return;
        }
 
        if (!is_array($items)) {
-               Logger::log('No items for user ' . $uid, Logger::INFO);
+               Logger::notice('No items', ['user' => $uid]);
                return;
        }
 
@@ -973,7 +993,7 @@ function twitter_fetchtimeline(App $a, $uid)
                foreach ($posts as $post) {
                        if ($post->id_str > $lastid) {
                                $lastid = $post->id_str;
-                               PConfig::set($uid, 'twitter', 'lastid', $lastid);
+                               DI::pConfig()->set($uid, 'twitter', 'lastid', $lastid);
                        }
 
                        if ($first_time) {
@@ -998,7 +1018,7 @@ function twitter_fetchtimeline(App $a, $uid)
                        }
                }
        }
-       PConfig::set($uid, 'twitter', 'lastid', $lastid);
+       DI::pConfig()->set($uid, 'twitter', 'lastid', $lastid);
        Logger::log('Last ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
 }
 
@@ -1006,7 +1026,7 @@ function twitter_fix_avatar($avatar)
 {
        $new_avatar = str_replace("_normal.", ".", $avatar);
 
-       $info = Image::getInfoFromURL($new_avatar);
+       $info = Images::getInfoFromURLCached($new_avatar);
        if (!$info) {
                $new_avatar = $avatar;
        }
@@ -1014,21 +1034,100 @@ function twitter_fix_avatar($avatar)
        return $new_avatar;
 }
 
-function twitter_fetch_contact($uid, $data, $create_user)
+function twitter_get_relation($uid, $target, $contact = [])
+{
+       if (isset($contact['rel'])) {
+               $relation = $contact['rel'];
+       } else {
+               $relation = 0;
+       }
+
+       $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');
+       $own_id = DI::pConfig()->get($uid, 'twitter', 'own_id');
+
+       $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
+       $parameters = ['source_id' => $own_id, 'target_screen_name' => $target];
+
+       try {
+               $status = $connection->get('friendships/show', $parameters);
+               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;
+
+               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()]);
+       }
+
+       return $relation;
+}
+
+/**
+ * @param $data
+ * @return array
+ */
+function twitter_user_to_contact($data)
 {
        if (empty($data->id_str)) {
+               return [];
+       }
+
+       $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;
+}
+
+function twitter_fetch_contact($uid, $data, $create_user)
+{
+       $fields = twitter_user_to_contact($data);
+
+       if (empty($fields)) {
                return -1;
        }
 
-       $avatar = twitter_fix_avatar($data->profile_image_url_https);
-       $url = "https://twitter.com/" . $data->screen_name;
-       $addr = $data->screen_name . "@twitter.com";
+       // photo comes from twitter_user_to_contact but shouldn't be saved directly in the contact row
+       $avatar = $fields['photo'];
+       unset($fields['photo']);
 
-       $fields = ['url' => $url, 'network' => Protocol::TWITTER,
-               'name' => $data->name, 'nick' => $data->screen_name, 'addr' => $addr,
-                'location' => $data->location, 'about' => $data->description];
+       // 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($fields['url'], 0, true, $fields);
+       }
 
-       $cid = Contact::getIdForURL($url, 0, true, $fields);
        if (!empty($cid)) {
                DBA::update('contact', $fields, ['id' => $cid]);
                Contact::updateAvatar($avatar, 0, $cid);
@@ -1036,17 +1135,19 @@ function twitter_fetch_contact($uid, $data, $create_user)
 
        $contact = DBA::selectFirst('contact', [], ['uid' => $uid, 'alias' => "twitter::" . $data->id_str]);
        if (!DBA::isResult($contact) && !$create_user) {
+               Logger::info('User contact not found', ['uid' => $uid, 'twitter-id' => $data->id_str]);
                return 0;
        }
 
        if (!DBA::isResult($contact)) {
+               $relation = twitter_get_relation($uid, $data->screen_name);
+
                // create contact record
                $fields['uid'] = $uid;
                $fields['created'] = DateTimeFormat::utcNow();
-               $fields['nurl'] = Strings::normaliseLink($url);
-               $fields['alias'] = 'twitter::' . $data->id_str;
+               $fields['nurl'] = Strings::normaliseLink($fields['url']);
                $fields['poll'] = 'twitter::' . $data->id_str;
-               $fields['rel'] = Contact::FRIEND;
+               $fields['rel'] = $relation;
                $fields['priority'] = 1;
                $fields['writable'] = true;
                $fields['blocked'] = false;
@@ -1069,229 +1170,204 @@ function twitter_fetch_contact($uid, $data, $create_user)
                }
 
                $contact_id = $contact['id'];
+               $update = false;
 
-               // update profile photos once every twelve hours as we have no notification of when they change.
-               $update_photo = ($contact['avatar-date'] < DateTimeFormat::utc('now -12 hours'));
+               // Update the contact relation once per day
+               if ($contact['updated'] < DateTimeFormat::utc('now -24 hours')) {
+                       $fields['rel'] = twitter_get_relation($uid, $data->screen_name, $contact);
+                       $update = true;
+               }
 
-               // check that we have all the photos, this has been known to fail on occasion
-               if (empty($contact['photo']) || empty($contact['thumb']) || empty($contact['micro']) || $update_photo) {
-                       Logger::log("twitter_fetch_contact: Updating contact " . $data->screen_name, Logger::DEBUG);
+               Contact::updateAvatar($avatar, $uid, $contact['id']);
 
-                       Contact::updateAvatar($avatar, $uid, $contact['id']);
+               if ($contact['name'] != $data->name) {
+                       $fields['name-date'] = $fields['uri-date'] = DateTimeFormat::utcNow();
+                       $update = true;
+               }
 
-                       $fields['name-date'] = DateTimeFormat::utcNow();
+               if ($contact['nick'] != $data->screen_name) {
                        $fields['uri-date'] = DateTimeFormat::utcNow();
+                       $update = true;
+               }
 
+               if (($contact['location'] != $data->location) || ($contact['about'] != $data->description)) {
+                       $update = true;
+               }
+
+               if ($update) {
+                       $fields['updated'] = DateTimeFormat::utcNow();
                        DBA::update('contact', $fields, ['id' => $contact['id']]);
+                       Logger::info('Updated contact', ['id' => $contact['id'], 'nick' => $data->screen_name]);
                }
        }
 
        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 = Config::get('twitter', 'consumerkey');
-       $csecret = Config::get('twitter', 'consumersecret');
-       $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
-       $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
+       $ckey = DI::config()->get('twitter', 'consumerkey');
+       $csecret = DI::config()->get('twitter', 'consumersecret');
 
-       $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 = [];
+       $taglist = [];
 
-       foreach ($item->entities->hashtags AS $hashtag) {
-               $url = '#[url=' . $a->getBaseURL() . '/search?tag=' . $hashtag->text . ']' . $hashtag->text . '[/url]';
-               $tags_arr['#' . $hashtag->text] = $url;
-               $body = str_replace('#' . $hashtag->text, $url, $body);
-       }
+       $replacementList = [];
 
-       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);
-       }
+       foreach ($status->entities->hashtags AS $hashtag) {
+               $replace = '#[url=' . DI::baseUrl()->get() . '/search?tag=' . $hashtag->text . ']' . $hashtag->text . '[/url]';
+               $taglist['#' . $hashtag->text] = ['#', $hashtag->text, ''];
 
-       if (isset($item->entities->urls)) {
-               $type = '';
-               $footerurl = '';
-               $footerlink = '';
-               $footer = '';
-
-               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 (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=' . $a->getBaseURL() . '/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];
 }
 
 /**
@@ -1339,7 +1415,7 @@ function twitter_media_entities($post, array &$postarray)
                                        $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
                                }
 
-                               $postarray['object-type'] = ACTIVITY_OBJ_IMAGE;
+                               $postarray['object-type'] = Activity\ObjectType::IMAGE;
                                break;
                        case 'video':
                        case 'animated_gif':
@@ -1350,7 +1426,7 @@ function twitter_media_entities($post, array &$postarray)
                                        $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
                                }
 
-                               $postarray['object-type'] = ACTIVITY_OBJ_VIDEO;
+                               $postarray['object-type'] = Activity\ObjectType::VIDEO;
                                if (is_array($medium->video_info->variants)) {
                                        $bitrate = 0;
                                        // We take the video with the highest bitrate
@@ -1376,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;
@@ -1386,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);
@@ -1407,15 +1500,15 @@ function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $onl
                        $postarray['thr-parent'] = $parent_item['uri'];
                        $postarray['parent-uri'] = $parent_item['parent-uri'];
                        $postarray['parent'] = $parent_item['parent'];
-                       $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
+                       $postarray['object-type'] = Activity\ObjectType::COMMENT;
                } else {
                        $postarray['thr-parent'] = $postarray['uri'];
                        $postarray['parent-uri'] = $postarray['uri'];
-                       $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
+                       $postarray['object-type'] = Activity\ObjectType::NOTE;
                }
 
                // Is it me?
-               $own_id = PConfig::get($uid, 'twitter', 'own_id');
+               $own_id = DI::pConfig()->get($uid, 'twitter', 'own_id');
 
                if ($post->user->id_str == $own_id) {
                        $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
@@ -1436,7 +1529,7 @@ function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $onl
                $create_user = false;
        } else {
                $postarray['parent-uri'] = $postarray['uri'];
-               $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
+               $postarray['object-type'] = Activity\ObjectType::NOTE;
        }
 
        if ($contactid == 0) {
@@ -1456,7 +1549,7 @@ function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $onl
 
        $postarray['contact-id'] = $contactid;
 
-       $postarray['verb'] = ACTIVITY_POST;
+       $postarray['verb'] = Activity::POST;
        $postarray['author-name'] = $postarray['owner-name'];
        $postarray['author-link'] = $postarray['owner-link'];
        $postarray['author-avatar'] = $postarray['owner-avatar'];
@@ -1479,18 +1572,21 @@ function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $onl
 
        // When the post contains links then use the correct object type
        if (count($post->entities->urls) > 0) {
-               $postarray['object-type'] = ACTIVITY_OBJ_BOOKMARK;
+               $postarray['object-type'] = Activity\ObjectType::BOOKMARK;
        }
 
        // 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)) {
@@ -1517,9 +1613,9 @@ function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $onl
                        Item::insert($retweet);
 
                        // CHange the other post into a reshare activity
-                       $postarray['verb'] = ACTIVITY2_ANNOUNCE;
+                       $postarray['verb'] = Activity::ANNOUNCE;
                        $postarray['gravity'] = GRAVITY_ACTIVITY;
-                       $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
+                       $postarray['object-type'] = Activity\ObjectType::NOTE;
 
                        $postarray['thr-parent'] = $retweet['uri'];
                        $postarray['parent-uri'] = $retweet['uri'];
@@ -1536,28 +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'])) {
-                       return [];
+       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;
+                       }
                }
-
-               $postarray['body'] .= "\n" . share_header(
-                       $quoted['author-name'],
-                       $quoted['author-link'],
-                       $quoted['author-avatar'],
-                       "",
-                       $quoted['created'],
-                       $quoted['plink']
-               );
-
-               $postarray['body'] .= $quoted['body'] . '[/share]';
        }
 
        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);
@@ -1597,7 +1710,7 @@ function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection,
 
        if (!empty($posts)) {
                foreach ($posts as $post) {
-                       $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
+                       $postarray = twitter_createpost($a, $uid, $post, $self, false, !DI::pConfig()->get($uid, 'twitter', 'create_user'), false);
 
                        if (empty($postarray['body'])) {
                                continue;
@@ -1614,19 +1727,19 @@ function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection,
 
 function twitter_fetchhometimeline(App $a, $uid)
 {
-       $ckey    = Config::get('twitter', 'consumerkey');
-       $csecret = Config::get('twitter', 'consumersecret');
-       $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
-       $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
-       $create_user = PConfig::get($uid, 'twitter', 'create_user');
-       $mirror_posts = PConfig::get($uid, 'twitter', 'mirror_posts');
+       $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');
+       $create_user = DI::pConfig()->get($uid, 'twitter', 'create_user');
+       $mirror_posts = DI::pConfig()->get($uid, 'twitter', 'mirror_posts');
 
        Logger::log("Fetching timeline for user " . $uid, Logger::DEBUG);
 
-       $application_name = Config::get('twitter', 'application_name');
+       $application_name = DI::config()->get('twitter', 'application_name');
 
        if ($application_name == "") {
-               $application_name = $a->getHostName();
+               $application_name = DI::baseUrl()->getHostname();
        }
 
        $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
@@ -1658,7 +1771,7 @@ function twitter_fetchhometimeline(App $a, $uid)
        $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended", "include_ext_alt_text" => true];
        //$parameters["count"] = 200;
        // Fetching timeline
-       $lastid = PConfig::get($uid, 'twitter', 'lasthometimelineid');
+       $lastid = DI::pConfig()->get($uid, 'twitter', 'lasthometimelineid');
 
        $first_time = ($lastid == "");
 
@@ -1691,7 +1804,7 @@ function twitter_fetchhometimeline(App $a, $uid)
                foreach ($posts as $post) {
                        if ($post->id_str > $lastid) {
                                $lastid = $post->id_str;
-                               PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
+                               DI::pConfig()->set($uid, 'twitter', 'lasthometimelineid', $lastid);
                        }
 
                        if ($first_time) {
@@ -1730,18 +1843,18 @@ 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);
                }
        }
-       PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
+       DI::pConfig()->set($uid, 'twitter', 'lasthometimelineid', $lastid);
 
        Logger::log('Last timeline ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
 
        // Fetching mentions
-       $lastid = PConfig::get($uid, 'twitter', 'lastmentionid');
+       $lastid = DI::pConfig()->get($uid, 'twitter', 'lastmentionid');
 
        $first_time = ($lastid == "");
 
@@ -1779,7 +1892,7 @@ function twitter_fetchhometimeline(App $a, $uid)
                                twitter_fetchparentposts($a, $uid, $post, $connection, $self);
                        }
 
-                       $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
+                       $postarray = twitter_createpost($a, $uid, $post, $self, false, !$create_user, false);
 
                        if (empty($postarray['body'])) {
                                continue;
@@ -1791,19 +1904,19 @@ function twitter_fetchhometimeline(App $a, $uid)
                }
        }
 
-       PConfig::set($uid, 'twitter', 'lastmentionid', $lastid);
+       DI::pConfig()->set($uid, 'twitter', 'lastmentionid', $lastid);
 
        Logger::log('Last mentions ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
 }
 
 function twitter_fetch_own_contact(App $a, $uid)
 {
-       $ckey    = Config::get('twitter', 'consumerkey');
-       $csecret = Config::get('twitter', 'consumersecret');
-       $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
-       $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
+       $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');
 
-       $own_id = PConfig::get($uid, 'twitter', 'own_id');
+       $own_id = DI::pConfig()->get($uid, 'twitter', 'own_id');
 
        $contact_id = 0;
 
@@ -1813,11 +1926,11 @@ 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;
                }
 
-               PConfig::set($uid, 'twitter', 'own_id', $user->id_str);
+               DI::pConfig()->set($uid, 'twitter', 'own_id', $user->id_str);
 
                $contact_id = twitter_fetch_contact($uid, $user, true);
        } else {
@@ -1827,7 +1940,7 @@ function twitter_fetch_own_contact(App $a, $uid)
                if (DBA::isResult($r)) {
                        $contact_id = $r[0]["id"];
                } else {
-                       PConfig::delete($uid, 'twitter', 'own_id');
+                       DI::pConfig()->delete($uid, 'twitter', 'own_id');
                }
        }
 
@@ -1873,10 +1986,10 @@ function twitter_is_retweet(App $a, $uid, $body)
 
        Logger::log('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, Logger::DEBUG);
 
-       $ckey    = Config::get('twitter', 'consumerkey');
-       $csecret = Config::get('twitter', 'consumersecret');
-       $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
-       $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
+       $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');
 
        $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
        $result = $connection->post('statuses/retweet/' . $id);
@@ -1908,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'];