Array instead of a string
[friendica-addons.git/.git] / twitter / twitter.php
index a51d374..ac23b84 100644 (file)
  *     we do not need "Twitter as login". When you've registered the app you get the
  *     OAuth Consumer key and secret pair for your application/site.
  *
- *     Add this key pair to your global config/addon.ini.php or use the admin panel.
+ *     Add this key pair to your global config/addon.config.php or use the admin panel.
  *
- *     [twitter]
- *     consumerkey = your consumer_key here
- *     consumersecret = your consumer_secret here
+ *             'twitter' => [
+ *                 'consumerkey' => '',
+ *             'consumersecret' => '',
+ *      ],
  *
- *     To activate the addon itself add it to the [system] addon
+ *     To activate the addon itself add it to the system.addon
  *     setting. After this, your user can configure their Twitter account settings
  *     from "Settings -> Addon Settings".
  *
 
 use Abraham\TwitterOAuth\TwitterOAuth;
 use Abraham\TwitterOAuth\TwitterOAuthException;
+use Codebird\Codebird;
 use Friendica\App;
 use Friendica\Content\OEmbed;
+use Friendica\Content\Text\BBCode;
 use Friendica\Content\Text\Plaintext;
-use Friendica\Core\Addon;
 use Friendica\Core\Config;
+use Friendica\Core\Hook;
 use Friendica\Core\L10n;
 use Friendica\Core\Logger;
 use Friendica\Core\PConfig;
@@ -77,22 +80,16 @@ use Friendica\Core\Worker;
 use Friendica\Database\DBA;
 use Friendica\Model\Contact;
 use Friendica\Model\Conversation;
-use Friendica\Model\GContact;
 use Friendica\Model\Group;
 use Friendica\Model\Item;
 use Friendica\Model\ItemContent;
-use Friendica\Model\Queue;
 use Friendica\Model\User;
 use Friendica\Object\Image;
+use Friendica\Util\ConfigFileLoader;
 use Friendica\Util\DateTimeFormat;
 use Friendica\Util\Network;
 use Friendica\Util\Strings;
 
-require_once 'boot.php';
-require_once 'include/dba.php';
-require_once 'include/enotify.php';
-require_once 'include/text.php';
-
 require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
 
 define('TWITTER_DEFAULT_POLL_INTERVAL', 5); // given in minutes
@@ -100,45 +97,45 @@ define('TWITTER_DEFAULT_POLL_INTERVAL', 5); // given in minutes
 function twitter_install()
 {
        //  we need some hooks, for the configuration and for sending tweets
-       Addon::registerHook('load_config'            , __FILE__, 'twitter_load_config');
-       Addon::registerHook('connector_settings'     , __FILE__, 'twitter_settings');
-       Addon::registerHook('connector_settings_post', __FILE__, 'twitter_settings_post');
-       Addon::registerHook('post_local'             , __FILE__, 'twitter_post_local');
-       Addon::registerHook('notifier_normal'        , __FILE__, 'twitter_post_hook');
-       Addon::registerHook('jot_networks'           , __FILE__, 'twitter_jot_nets');
-       Addon::registerHook('cron'                   , __FILE__, 'twitter_cron');
-       Addon::registerHook('queue_predeliver'       , __FILE__, 'twitter_queue_hook');
-       Addon::registerHook('follow'                 , __FILE__, 'twitter_follow');
-       Addon::registerHook('expire'                 , __FILE__, 'twitter_expire');
-       Addon::registerHook('prepare_body'           , __FILE__, 'twitter_prepare_body');
-       Addon::registerHook('check_item_notification', __FILE__, 'twitter_check_item_notification');
+       Hook::register('load_config'            , __FILE__, 'twitter_load_config');
+       Hook::register('connector_settings'     , __FILE__, 'twitter_settings');
+       Hook::register('connector_settings_post', __FILE__, 'twitter_settings_post');
+       Hook::register('hook_fork'              , __FILE__, 'twitter_hook_fork');
+       Hook::register('post_local'             , __FILE__, 'twitter_post_local');
+       Hook::register('notifier_normal'        , __FILE__, 'twitter_post_hook');
+       Hook::register('jot_networks'           , __FILE__, 'twitter_jot_nets');
+       Hook::register('cron'                   , __FILE__, 'twitter_cron');
+       Hook::register('follow'                 , __FILE__, 'twitter_follow');
+       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");
 }
 
 function twitter_uninstall()
 {
-       Addon::unregisterHook('load_config'            , __FILE__, 'twitter_load_config');
-       Addon::unregisterHook('connector_settings'     , __FILE__, 'twitter_settings');
-       Addon::unregisterHook('connector_settings_post', __FILE__, 'twitter_settings_post');
-       Addon::unregisterHook('post_local'             , __FILE__, 'twitter_post_local');
-       Addon::unregisterHook('notifier_normal'        , __FILE__, 'twitter_post_hook');
-       Addon::unregisterHook('jot_networks'           , __FILE__, 'twitter_jot_nets');
-       Addon::unregisterHook('cron'                   , __FILE__, 'twitter_cron');
-       Addon::unregisterHook('queue_predeliver'       , __FILE__, 'twitter_queue_hook');
-       Addon::unregisterHook('follow'                 , __FILE__, 'twitter_follow');
-       Addon::unregisterHook('expire'                 , __FILE__, 'twitter_expire');
-       Addon::unregisterHook('prepare_body'           , __FILE__, 'twitter_prepare_body');
-       Addon::unregisterHook('check_item_notification', __FILE__, 'twitter_check_item_notification');
+       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
-       Addon::unregisterHook('post_local_end'     , __FILE__, 'twitter_post_hook');
-       Addon::unregisterHook('addon_settings'     , __FILE__, 'twitter_settings');
-       Addon::unregisterHook('addon_settings_post', __FILE__, 'twitter_settings_post');
+       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');
 }
 
-function twitter_load_config(App $a)
+function twitter_load_config(App $a, ConfigFileLoader $loader)
 {
-       $a->loadConfigFile(__DIR__ . '/config/twitter.ini.php');
+       $a->getConfigCache()->load($loader->loadAddonConfig('twitter'));
 }
 
 function twitter_check_item_notification(App $a, array &$notification_data)
@@ -194,21 +191,25 @@ function twitter_follow(App $a, array &$contact)
        }
 }
 
-function twitter_jot_nets(App $a, &$b)
+function twitter_jot_nets(App $a, array &$jotnets_fields)
 {
        if (!local_user()) {
                return;
        }
 
-       $tw_post = PConfig::get(local_user(), 'twitter', 'post');
-       if (intval($tw_post) == 1) {
-               $tw_defpost = PConfig::get(local_user(), 'twitter', 'post_by_default');
-               $selected = ((intval($tw_defpost) == 1) ? ' checked="checked" ' : '');
-               $b .= '<div class="profile-jot-net"><input type="checkbox" name="twitter_enable"' . $selected . ' value="1" /> '
-                       . L10n::t('Post to Twitter') . '</div>';
+       if (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')
+                       ]
+               ];
        }
 }
 
+
 function twitter_settings_post(App $a)
 {
        if (!local_user()) {
@@ -390,6 +391,47 @@ function twitter_settings(App $a, &$s)
        $s .= '</div><div class="clear"></div>';
 }
 
+function twitter_hook_fork(App $a, array &$b)
+{
+       if ($b['name'] != 'notifier_normal') {
+               return;
+       }
+
+       $post = $b['data'];
+
+       // Deleting and editing is not supported by the addon (deleting could, but isn't by now)
+       if ($post['deleted'] || ($post['created'] !== $post['edited'])) {
+               $b['execute'] = false;
+               return;
+       }
+
+       // if post comes from twitter don't send it back
+       if ($post['extid'] == Protocol::TWITTER) {
+               $b['execute'] = false;
+               return;
+       }
+
+       if ($post['app'] == 'Twitter') {
+               $b['execute'] = false;
+               return;
+       }
+
+       if (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']);
+                       $b['execute'] = false;
+                       return;
+               }
+       } else {
+               // Comments are never exported when we don't import the twitter timeline
+               if (!strstr($post['postopts'], 'twitter') || ($post['parent'] != $post['id']) || $post['private']) {
+                       $b['execute'] = false;
+                       return;
+               }
+        }
+}
+
 function twitter_post_local(App $a, array &$b)
 {
        if ($b['edit']) {
@@ -401,7 +443,7 @@ function twitter_post_local(App $a, array &$b)
        }
 
        $twitter_post = intval(PConfig::get(local_user(), 'twitter', 'post'));
-       $twitter_enable = (($twitter_post && x($_REQUEST, 'twitter_enable')) ? intval($_REQUEST['twitter_enable']) : 0);
+       $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'))) {
@@ -533,7 +575,7 @@ function twitter_post_hook(App $a, array &$b)
                return;
        }
 
-       Logger::log('twitter post invoked');
+       Logger::notice('twitter post invoked', ['id' => $b['id'], 'guid' => $b['guid']]);
 
        PConfig::load($b['uid'], 'twitter');
 
@@ -550,6 +592,10 @@ function twitter_post_hook(App $a, array &$b)
                        return;
                }
 
+               Codebird::setConsumerKey($ckey, $csecret);
+               $cb = Codebird::getInstance();
+               $cb->setToken($otoken, $osecret);
+
                $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
 
                // Set the timeout for upload to 30 seconds
@@ -568,25 +614,22 @@ 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);
                $msg = $msgarr["text"];
 
                if (($msg == "") && isset($msgarr["title"])) {
                        $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
                }
 
-               $image = "";
-
-               if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
+               if (($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"];
                        $url_added = true;
                } else {
                        $url_added = false;
                }
 
-               if (isset($msgarr["image"]) && ($msgarr["type"] != "video")) {
-                       $image = $msgarr["image"];
-               }
-
                if (empty($msg)) {
                        return;
                }
@@ -594,32 +637,42 @@ function twitter_post_hook(App $a, array &$b)
                // and now tweet it :-)
                $post = [];
 
-               if (!empty($image)) {
+               if (!empty($msgarr['images'])) {
                        try {
-                               $img_str = Network::fetchUrl($image);
+                               $media_ids = [];
+                               foreach ($msgarr['images'] as $image) {
+                                       if (count($media_ids) == 4) {
+                                               continue;
+                                       }
 
-                               $tempfile = tempnam(get_temppath(), 'cache');
-                               file_put_contents($tempfile, $img_str);
+                                       $img_str = Network::fetchUrl($image['url']);
 
-                               $media = $connection->upload('media/upload', ['media' => $tempfile]);
+                                       $tempfile = tempnam(get_temppath(), 'cache');
+                                       file_put_contents($tempfile, $img_str);
 
-                               unlink($tempfile);
+                                       $media = $connection->upload('media/upload', ['media' => $tempfile]);
 
-                               if (isset($media->media_id_string)) {
-                                       $post['media_ids'] = $media->media_id_string;
-                               } else {
-                                       throw new Exception('Failed upload of ' . $image);
+                                       unlink($tempfile);
+
+                                       if (isset($media->media_id_string)) {
+                                               $media_ids[] = $media->media_id_string;
+
+                                               if (!empty($image['description'])) {
+                                                       $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)]);
+                                               }
+                                       } else {
+                                               throw new Exception('Failed upload of ' . $image['url']);
+                                       }
+                               }
+                               $post['media_ids'] = implode(',', $media_ids);
+                               if (empty($post['media_ids'])) {
+                                       unset($post['media_ids']);
                                }
                        } catch (Exception $e) {
                                Logger::log('Exception when trying to send to Twitter: ' . $e->getMessage());
-
-                               // Workaround: Remove the picture link so that the post can be reposted without it
-                               // When there is another url already added, a second url would be superfluous.
-                               if (!$url_added) {
-                                       $msg .= "\n" . $image;
-                               }
-
-                               $image = "";
                        }
                }
 
@@ -639,16 +692,7 @@ function twitter_post_hook(App $a, array &$b)
 
                if (!empty($result->errors)) {
                        Logger::log('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
-
-                       $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `self`", intval($b['uid']));
-                       if (DBA::isResult($r)) {
-                               $a->contact = $r[0]["id"];
-                       }
-
-                       $s = serialize(['url' => $url, 'item' => $b['id'], 'post' => $post]);
-
-                       Queue::add($a->contact, Protocol::TWITTER, $s);
-                       notice(L10n::t('Twitter post failed. Queued for retry.') . EOL);
+                       Worker::defer();
                } elseif ($iscomment) {
                        Logger::log('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
                        Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $b['id']]);
@@ -658,8 +702,8 @@ function twitter_post_hook(App $a, array &$b)
 
 function twitter_addon_admin_post(App $a)
 {
-       $consumerkey    = x($_POST, 'consumerkey')    ? Strings::escapeTags(trim($_POST['consumerkey']))    : '';
-       $consumersecret = x($_POST, 'consumersecret') ? Strings::escapeTags(trim($_POST['consumersecret'])) : '';
+       $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);
@@ -699,7 +743,7 @@ function twitter_cron(App $a)
        if (DBA::isResult($r)) {
                foreach ($r as $rr) {
                        Logger::log('twitter: fetching for user ' . $rr['uid']);
-                       Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
+                       Worker::add(['priority' => PRIORITY_MEDIUM, 'force_priority' => true], "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
                }
        }
 
@@ -722,7 +766,7 @@ function twitter_cron(App $a)
                        }
 
                        Logger::log('twitter: importing timeline from user ' . $rr['uid']);
-                       Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 2, (int) $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
@@ -759,8 +803,6 @@ function twitter_expire(App $a)
        }
        DBA::close($r);
 
-       require_once "include/items.php";
-
        Logger::log('twitter_expire: expire_start');
 
        $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
@@ -783,7 +825,7 @@ function twitter_prepare_body(App $a, array &$b)
        if ($b["preview"]) {
                $max_char = 280;
                $item = $b["item"];
-               $item["plink"] = $a->getBaseURL() . "/display/" . $a->user["nickname"] . "/" . $item["parent"];
+               $item["plink"] = $a->getBaseURL() . "/display/" . $item["guid"];
 
                $condition = ['uri' => $item["thr-parent"], 'uid' => local_user()];
                $orig_post = Item::selectFirst(['author-link'], $condition);
@@ -890,12 +932,11 @@ function twitter_fetchtimeline(App $a, $uid)
        $has_picture = false;
 
        require_once 'mod/item.php';
-       require_once 'include/items.php';
        require_once 'mod/share.php';
 
        $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
 
-       $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
+       $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended", "include_ext_alt_text" => true];
 
        $first_time = ($lastid == "");
 
@@ -952,66 +993,6 @@ function twitter_fetchtimeline(App $a, $uid)
        Logger::log('Last ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
 }
 
-function twitter_queue_hook(App $a)
-{
-       $qi = q("SELECT * FROM `queue` WHERE `network` = '%s'",
-               DBA::escape(Protocol::TWITTER)
-       );
-       if (!DBA::isResult($qi)) {
-               return;
-       }
-
-       foreach ($qi as $x) {
-               if ($x['network'] !== Protocol::TWITTER) {
-                       continue;
-               }
-
-               Logger::log('twitter_queue: run');
-
-               $r = q("SELECT `user`.* FROM `user` LEFT JOIN `contact` on `contact`.`uid` = `user`.`uid`
-                       WHERE `contact`.`self` = 1 AND `contact`.`id` = %d LIMIT 1",
-                       intval($x['cid'])
-               );
-               if (!DBA::isResult($r)) {
-                       continue;
-               }
-
-               $user = $r[0];
-
-               $ckey    = Config::get('twitter', 'consumerkey');
-               $csecret = Config::get('twitter', 'consumersecret');
-               $otoken  = PConfig::get($user['uid'], 'twitter', 'oauthtoken');
-               $osecret = PConfig::get($user['uid'], 'twitter', 'oauthsecret');
-
-               $success = false;
-
-               if ($ckey && $csecret && $otoken && $osecret) {
-                       Logger::log('twitter_queue: able to post');
-
-                       $z = unserialize($x['content']);
-
-                       $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
-                       $result = $connection->post($z['url'], $z['post']);
-
-                       Logger::log('twitter_queue: post result: ' . print_r($result, true), Logger::DEBUG);
-
-                       if ($result->errors) {
-                               Logger::log('twitter_queue: Send to Twitter failed: "' . print_r($result->errors, true) . '"');
-                       } else {
-                               $success = true;
-                               Queue::removeItem($x['id']);
-                       }
-               } else {
-                       Logger::log("twitter_queue: Error getting tokens for user " . $user['uid']);
-               }
-
-               if (!$success) {
-                       Logger::log('twitter_queue: delayed');
-                       Queue::updateTime($x['id']);
-               }
-       }
-}
-
 function twitter_fix_avatar($avatar)
 {
        $new_avatar = str_replace("_normal.", ".", $avatar);
@@ -1034,12 +1015,6 @@ function twitter_fetch_contact($uid, $data, $create_user)
        $url = "https://twitter.com/" . $data->screen_name;
        $addr = $data->screen_name . "@twitter.com";
 
-       GContact::update(["url" => $url, "network" => Protocol::TWITTER,
-               "photo" => $avatar, "hide" => true,
-               "name" => $data->name, "nick" => $data->screen_name,
-               "location" => $data->location, "about" => $data->description,
-               "addr" => $addr, "generation" => 2]);
-
        $fields = ['url' => $url, 'network' => Protocol::TWITTER,
                'name' => $data->name, 'nick' => $data->screen_name, 'addr' => $addr,
                 'location' => $data->location, 'about' => $data->description];
@@ -1156,7 +1131,7 @@ function twitter_expand_entities(App $a, $body, $item, $picture)
        $tags_arr = [];
 
        foreach ($item->entities->hashtags AS $hashtag) {
-               $url = '#[url=' . $a->getBaseURL() . '/search?tag=' . rawurlencode($hashtag->text) . ']' . $hashtag->text . '[/url]';
+               $url = '#[url=' . $a->getBaseURL() . '/search?tag=' . $hashtag->text . ']' . $hashtag->text . '[/url]';
                $tags_arr['#' . $hashtag->text] = $url;
                $body = str_replace('#' . $hashtag->text, $url, $body);
        }
@@ -1184,9 +1159,11 @@ function twitter_expand_entities(App $a, $body, $item, $picture)
                                        continue;
                                }
 
-                               $expanded_url = Network::finalUrl($url->expanded_url);
+                               $expanded_url = $url->expanded_url;
+
+                               $final_url = Network::finalUrl($url->expanded_url);
 
-                               $oembed_data = OEmbed::fetchURL($expanded_url);
+                               $oembed_data = OEmbed::fetchURL($final_url);
 
                                if (empty($oembed_data) || empty($oembed_data->type)) {
                                        continue;
@@ -1212,7 +1189,7 @@ function twitter_expand_entities(App $a, $body, $item, $picture)
                                } elseif ($oembed_data->type != 'link') {
                                        $body = str_replace($url->url, '[url=' . $expanded_url . ']' . $url->display_url . '[/url]', $body);
                                } else {
-                                       $img_str = Network::fetchUrl($expanded_url, true, $redirects, 4);
+                                       $img_str = Network::fetchUrl($final_url, true, 4);
 
                                        $tempfile = tempnam(get_temppath(), 'cache');
                                        file_put_contents($tempfile, $img_str);
@@ -1228,7 +1205,7 @@ function twitter_expand_entities(App $a, $body, $item, $picture)
 
                                        if (substr($mime, 0, 6) == 'image/') {
                                                $type = 'photo';
-                                               $body = str_replace($url->url, '[img]' . $expanded_url . '[/img]', $body);
+                                               $body = str_replace($url->url, '[img]' . $final_url . '[/img]', $body);
                                        } else {
                                                $type = $oembed_data->type;
                                                $footerurl = $expanded_url;
@@ -1265,7 +1242,7 @@ function twitter_expand_entities(App $a, $body, $item, $picture)
        }
 
        // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
-       $tags = Strings::getTags($body);
+       $tags = BBCode::getTags($body);
 
        if (count($tags)) {
                foreach ($tags as $tag) {
@@ -1287,7 +1264,7 @@ function twitter_expand_entities(App $a, $body, $item, $picture)
                                }
 
                                $basetag = str_replace('_', ' ', substr($tag, 1));
-                               $url = '#[url=' . $a->getBaseURL() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
+                               $url = '#[url=' . $a->getBaseURL() . '/search?tag=' . $basetag . ']' . $basetag . '[/url]';
                                $body = str_replace($tag, $url, $body);
                                $tags_arr['#' . $basetag] = $url;
                        } elseif (strpos($tag, '@') === 0) {
@@ -1346,12 +1323,24 @@ function twitter_media_entities($post, array &$postarray)
                }
                switch ($medium->type) {
                        case 'photo':
-                               $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
+                               if (!empty($medium->ext_alt_text)) {
+                                       Logger::info('Got text description', ['alt_text' => $medium->ext_alt_text]);
+                                       $media[$medium->url] .= "\n[img=" . $medium->media_url_https .']' . $medium->ext_alt_text . '[/img]';
+                               } else {
+                                       $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
+                               }
+
                                $postarray['object-type'] = ACTIVITY_OBJ_IMAGE;
                                break;
                        case 'video':
                        case 'animated_gif':
-                               $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
+                               if (!empty($medium->ext_alt_text)) {
+                                       Logger::info('Got text description', ['alt_text' => $medium->ext_alt_text]);
+                                       $media[$medium->url] .= "\n[img=" . $medium->media_url_https .']' . $medium->ext_alt_text . '[/img]';
+                               } else {
+                                       $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
+                               }
+
                                $postarray['object-type'] = ACTIVITY_OBJ_VIDEO;
                                if (is_array($medium->video_info->variants)) {
                                        $bitrate = 0;
@@ -1514,15 +1503,28 @@ function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $onl
                        return [];
                }
 
-               $retweet['source'] = $postarray['source'];
-               $retweet['private'] = $postarray['private'];
-               $retweet['allow_cid'] = $postarray['allow_cid'];
-               $retweet['contact-id'] = $postarray['contact-id'];
-               $retweet['owner-name'] = $postarray['owner-name'];
-               $retweet['owner-link'] = $postarray['owner-link'];
-               $retweet['owner-avatar'] = $postarray['owner-avatar'];
+               if (!$noquote) {
+                       // Store the original tweet
+                       Item::insert($retweet);
 
-               $postarray = $retweet;
+                       // CHange the other post into a reshare activity
+                       $postarray['verb'] = ACTIVITY2_ANNOUNCE;
+                       $postarray['gravity'] = GRAVITY_ACTIVITY;
+                       $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
+
+                       $postarray['thr-parent'] = $retweet['uri'];
+                       $postarray['parent-uri'] = $retweet['uri'];
+               } else {
+                       $retweet['source'] = $postarray['source'];
+                       $retweet['private'] = $postarray['private'];
+                       $retweet['allow_cid'] = $postarray['allow_cid'];
+                       $retweet['contact-id'] = $postarray['contact-id'];
+                       $retweet['owner-name'] = $postarray['owner-name'];
+                       $retweet['owner-link'] = $postarray['owner-link'];
+                       $retweet['owner-avatar'] = $postarray['owner-avatar'];
+
+                       $postarray = $retweet;
+               }
        }
 
        if (!empty($post->quoted_status) && !$noquote) {
@@ -1554,7 +1556,7 @@ function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection,
        $posts = [];
 
        while (!empty($post->in_reply_to_status_id_str)) {
-               $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str];
+               $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str, "include_ext_alt_text" => true];
 
                try {
                        $post = $connection->get('statuses/show', $parameters);
@@ -1564,7 +1566,7 @@ function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection,
                }
 
                if (empty($post)) {
-                       Logger::log("twitter_fetchparentposts: Can't fetch post " . $parameters->id, Logger::DEBUG);
+                       Logger::log("twitter_fetchparentposts: Can't fetch post " . $parameters['id'], Logger::DEBUG);
                        break;
                }
 
@@ -1618,8 +1620,6 @@ function twitter_fetchhometimeline(App $a, $uid)
                $application_name = $a->getHostName();
        }
 
-       require_once 'include/items.php';
-
        $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
 
        try {
@@ -1646,7 +1646,7 @@ function twitter_fetchhometimeline(App $a, $uid)
                return;
        }
 
-       $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
+       $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');