]> git.mxchange.org Git - friendica-addons.git/blobdiff - tumblr/tumblr.php
Changed hook parameter / more languages added
[friendica-addons.git] / tumblr / tumblr.php
index d8ab10fca111e10e2c88adc31770b13c8be0cb86..a7a0db4c0657a3fe9bee56d79827a1a1ef3d0443 100644 (file)
@@ -18,11 +18,11 @@ use Friendica\Core\Logger;
 use Friendica\Core\Protocol;
 use Friendica\Core\Renderer;
 use Friendica\Core\System;
+use Friendica\Core\Worker;
 use Friendica\Database\DBA;
 use Friendica\DI;
 use Friendica\Model\Contact;
 use Friendica\Model\Item;
-use Friendica\Model\ItemURI;
 use Friendica\Model\Photo;
 use Friendica\Model\Post;
 use Friendica\Model\Tag;
@@ -39,6 +39,7 @@ use GuzzleHttp\HandlerStack;
 use GuzzleHttp\Subscriber\Oauth\Oauth1;
 
 define('TUMBLR_DEFAULT_POLL_INTERVAL', 10); // given in minutes
+define('TUMBLR_DEFAULT_MAXIMUM_TAGS', 10);
 
 function tumblr_install()
 {
@@ -51,6 +52,7 @@ function tumblr_install()
        Hook::register('connector_settings_post', __FILE__, 'tumblr_settings_post');
        Hook::register('cron',                    __FILE__, 'tumblr_cron');
        Hook::register('support_follow',          __FILE__, 'tumblr_support_follow');
+       Hook::register('support_probe',           __FILE__, 'tumblr_support_probe');
        Hook::register('follow',                  __FILE__, 'tumblr_follow');
        Hook::register('unfollow',                __FILE__, 'tumblr_unfollow');
        Hook::register('block',                   __FILE__, 'tumblr_block');
@@ -68,7 +70,7 @@ function tumblr_load_config(ConfigFileManager $loader)
 
 function tumblr_check_item_notification(array &$notification_data)
 {
-       if (!tumblr_enabled_for_user($notification_data['uid'])) { 
+       if (!tumblr_enabled_for_user($notification_data['uid'])) {
                return;
        }
 
@@ -77,7 +79,7 @@ function tumblr_check_item_notification(array &$notification_data)
                return;
        }
 
-       $own_user = Contact::selectFirst(['url', 'alias'], ['uid' => $notification_data['uid'], 'poll' => 'tumblr::'.$page]);
+       $own_user = Contact::selectFirst(['url', 'alias'], ['network' => Protocol::TUMBLR, 'uid' => [0, $notification_data['uid']], 'poll' => 'tumblr::' . $page]);
        if ($own_user) {
                $notification_data['profiles'][] = $own_user['url'];
                $notification_data['profiles'][] = $own_user['alias'];
@@ -96,9 +98,12 @@ function tumblr_probe_detect(array &$hookData)
                return;
        }
 
-       Logger::debug('Search for tumblr blog', ['url' => $hookData['uri']]);
-
        $hookData['result'] = tumblr_get_contact_by_url($hookData['uri']);
+
+       // Authoritative probe should set the result even if the probe was unsuccessful
+       if ($hookData['network'] == Protocol::TUMBLR && empty($hookData['result'])) {
+               $hookData['result'] = [];
+       }
 }
 
 function tumblr_item_by_link(array &$hookData)
@@ -115,7 +120,7 @@ function tumblr_item_by_link(array &$hookData)
        if (!preg_match('#^https?://www\.tumblr.com/blog/view/(.+)/(\d+).*#', $hookData['uri'], $matches) && !preg_match('#^https?://www\.tumblr.com/(.+)/(\d+).*#', $hookData['uri'], $matches)) {
                return;
        }
-       
+
        Logger::debug('Found tumblr post', ['url' => $hookData['uri'], 'blog' => $matches[1], 'id' => $matches[2]]);
 
        $parameters = ['id' => $matches[2], 'reblog_info' => false, 'notes_info' => false, 'npf' => false];
@@ -127,7 +132,7 @@ function tumblr_item_by_link(array &$hookData)
 
        Logger::debug('Got post', ['blog' => $matches[1], 'id' => $matches[2], 'result' => $result->response->posts]);
        if (!empty($result->response->posts)) {
-               $hookData['item_id'] = tumblr_process_post($result->response->posts[0], $hookData['uid']);
+               $hookData['item_id'] = tumblr_process_post($result->response->posts[0], $hookData['uid'], Item::PR_FETCHED);
        }
 }
 
@@ -138,6 +143,13 @@ function tumblr_support_follow(array &$data)
        }
 }
 
+function tumblr_support_probe(array &$data)
+{
+       if ($data['protocol'] == Protocol::TUMBLR) {
+               $data['result'] = true;
+       }
+}
+
 function tumblr_follow(array &$hook_data)
 {
        $uid = DI::userSession()->getLocalUserId();
@@ -289,6 +301,7 @@ function tumblr_addon_admin(string &$o)
                '$submit' => DI::l10n()->t('Save Settings'),
                '$consumer_key'    => ['consumer_key', DI::l10n()->t('Consumer Key'), DI::config()->get('tumblr', 'consumer_key'), ''],
                '$consumer_secret' => ['consumer_secret', DI::l10n()->t('Consumer Secret'), DI::config()->get('tumblr', 'consumer_secret'), ''],
+               '$max_tags'        => ['max_tags', DI::l10n()->t('Maximum tags'), DI::config()->get('tumblr', 'max_tags') ?? TUMBLR_DEFAULT_MAXIMUM_TAGS, DI::l10n()->t('Maximum number of tags that a user can follow. Enter 0 to deactivate the feature.')],
        ]);
 }
 
@@ -296,6 +309,7 @@ function tumblr_addon_admin_post()
 {
        DI::config()->set('tumblr', 'consumer_key', trim($_POST['consumer_key'] ?? ''));
        DI::config()->set('tumblr', 'consumer_secret', trim($_POST['consumer_secret'] ?? ''));
+       DI::config()->set('tumblr', 'max_tags', max(0, intval($_POST['max_tags'] ?? '')));
 }
 
 function tumblr_settings(array &$data)
@@ -304,10 +318,14 @@ function tumblr_settings(array &$data)
                return;
        }
 
-       $enabled     = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'post', false);
-       $def_enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'post_by_default', false);
-       $import      = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'import', false);
+       $enabled     = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'post') ?? false;
+       $def_enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'post_by_default') ?? false;
+       $import      = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'import') ?? false;
+       $tags        = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'tags') ?? [];
+
+       $max_tags = DI::config()->get('tumblr', 'max_tags') ?? TUMBLR_DEFAULT_MAXIMUM_TAGS;
 
+       $tags_str = implode(', ', $tags);
        $cachekey = 'tumblr-blogs-' . DI::userSession()->getLocalUserId();
        $blogs = DI::cache()->get($cachekey);
        if (empty($blogs)) {
@@ -335,6 +353,7 @@ function tumblr_settings(array &$data)
                '$enable'      => ['tumblr', DI::l10n()->t('Enable Tumblr Post Addon'), $enabled],
                '$bydefault'   => ['tumblr_bydefault', DI::l10n()->t('Post to Tumblr by default'), $def_enabled],
                '$import'      => ['tumblr_import', DI::l10n()->t('Import the remote timeline'), $import],
+               '$tags'        => ['tags', DI::l10n()->t('Subscribed tags'), $tags_str, DI::l10n()->t('Comma separated list of up to %d tags that will be imported additionally to the timeline', $max_tags)],
                '$page_select' => $page_select ?? '',
        ]);
 
@@ -372,6 +391,16 @@ function tumblr_settings_post(array &$b)
                DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'tumblr', 'page',            $_POST['tumblr_page']);
                DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'tumblr', 'post_by_default', intval($_POST['tumblr_bydefault']));
                DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'tumblr', 'import',          intval($_POST['tumblr_import']));
+
+               $max_tags = DI::config()->get('tumblr', 'max_tags') ?? TUMBLR_DEFAULT_MAXIMUM_TAGS;
+               $tags     = [];
+               foreach (explode(',', $_POST['tags']) as $tag) {
+                       if (count($tags) < $max_tags) {
+                               $tags[] = trim($tag, ' #');
+                       }
+               }
+
+               DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'tumblr', 'tags', $tags);
        }
 }
 
@@ -387,7 +416,7 @@ function tumblr_cron()
        if ($last) {
                $next = $last + ($poll_interval * 60);
                if ($next > time()) {
-                       Logger::notice('poll intervall not reached');
+                       Logger::notice('poll interval not reached');
                        return;
                }
        }
@@ -411,9 +440,22 @@ function tumblr_cron()
 
                Logger::notice('importing timeline - start', ['user' => $pconfig['uid']]);
                tumblr_fetch_dashboard($pconfig['uid']);
+               tumblr_fetch_tags($pconfig['uid']);
                Logger::notice('importing timeline - done', ['user' => $pconfig['uid']]);
        }
 
+       $last_clean = DI::keyValue()->get('tumblr_last_clean');
+       if (empty($last_clean) || ($last_clean + 86400 < time())) {
+               Logger::notice('Start contact cleanup');
+               $contacts = DBA::select('account-user-view', ['id', 'pid'], ["`network` = ? AND `uid` != ? AND `rel` = ?", Protocol::TUMBLR, 0, Contact::NOTHING]);
+               while ($contact = DBA::fetch($contacts)) {
+                       Worker::add(Worker::PRIORITY_LOW, 'MergeContact', $contact['pid'], $contact['id'], 0);
+               }
+               DBA::close($contacts);
+               DI::keyValue()->set('tumblr_last_clean', time());
+               Logger::notice('Contact cleanup done');
+       }
+
        Logger::notice('cron_end');
 
        DI::keyValue()->set('tumblr_last_poll', time());
@@ -674,6 +716,32 @@ function tumblr_get_post_from_uri(string $uri): array
        return $post;
 }
 
+/**
+ * Fetch posts for user defined hashtags for the given user
+ *
+ * @param integer $uid
+ * @return void
+ */
+function tumblr_fetch_tags(int $uid)
+{
+       if (!DI::config()->get('tumblr', 'max_tags') ?? TUMBLR_DEFAULT_MAXIMUM_TAGS) {
+               return;
+       }
+
+       foreach (DI::pConfig()->get($uid, 'tumblr', 'tags') ?? [] as $tag) {
+               $data = tumblr_get($uid, 'tagged', ['tag' => $tag]);
+               foreach (array_reverse($data->response) as $post) {
+                       $id = tumblr_process_post($post, $uid, Item::PR_TAG);
+                       if (!empty($id)) {
+                               Logger::debug('Tag post imported', ['tag' => $tag, 'id' => $id]);
+                               $post = Post::selectFirst(['uri-id'], ['id' => $id]);
+                               $stored = Post\Category::storeFileByURIId($post['uri-id'], $uid, Post\Category::SUBCRIPTION, $tag);
+                               Logger::debug('Stored tag subscription for user', ['uri-id' => $post['uri-id'], 'uid' => $uid, 'tag' => $tag, 'stored' => $stored]);
+                       }
+               }
+       }
+}
+
 /**
  * Fetch the dashboard (timeline) for the given user
  *
@@ -682,8 +750,6 @@ function tumblr_get_post_from_uri(string $uri): array
  */
 function tumblr_fetch_dashboard(int $uid)
 {
-       $page = tumblr_get_page($uid);
-
        $parameters = ['reblog_info' => false, 'notes_info' => false, 'npf' => false];
 
        $last = DI::pConfig()->get($uid, 'tumblr', 'last_id');
@@ -702,36 +768,36 @@ function tumblr_fetch_dashboard(int $uid)
        }
 
        foreach (array_reverse($dashboard->response->posts) as $post) {
-               $uri = 'tumblr::' . $post->id_string . ':' . $post->reblog_key;
-
                if ($post->id > $last) {
                        $last = $post->id;
                }
 
-               Logger::debug('Importing post', ['uid' => $uid, 'created' => date(DateTimeFormat::MYSQL, $post->timestamp), 'uri' => $uri]);
-
-               if (Post::exists(['uri' => $uri, 'uid' => $uid]) || ($post->blog->uuid == $page)) {
-                       DI::pConfig()->set($uid, 'tumblr', 'last_id', $last);
-                       continue;
-               }
-
-               tumblr_process_post($post, $uid, $uri);
+               Logger::debug('Importing post', ['uid' => $uid, 'created' => date(DateTimeFormat::MYSQL, $post->timestamp), 'id' => $post->id_string]);
 
+               tumblr_process_post($post, $uid, Item::PR_NONE);
 
                DI::pConfig()->set($uid, 'tumblr', 'last_id', $last);
        }
 }
 
-function tumblr_process_post(stdClass $post, int $uid, string $uri = ''): int
+function tumblr_process_post(stdClass $post, int $uid, int $post_reason): int
 {
-       if (empty($uri)) {
-               $uri = 'tumblr::' . $post->id_string . ':' . $post->reblog_key;
+       $uri = 'tumblr::' . $post->id_string . ':' . $post->reblog_key;
+
+       if (Post::exists(['uri' => $uri, 'uid' => $uid]) || ($post->blog->uuid == tumblr_get_page($uid))) {
+               return 0;
        }
 
        $item = tumblr_get_header($post, $uri, $uid);
 
        $item = tumblr_get_content($item, $post);
 
+       $item['post-reason'] = $post_reason;
+
+       if (!empty($post->followed)) {
+               $item['post-reason'] = Item::PR_FOLLOWER;
+       }
+
        $id = item::insert($item);
 
        if ($id) {
@@ -991,39 +1057,58 @@ function tumblr_get_type_replacement(array $data, string $plink): string
  */
 function tumblr_get_contact(stdClass $blog, int $uid): array
 {
-       $condition = ['network' => Protocol::TUMBLR, 'uid' => $uid, 'poll' => 'tumblr::' . $blog->uuid];
-       $contact = Contact::selectFirst([], $condition);
-       if (!empty($contact) && (strtotime($contact['updated']) >= $blog->updated)) {
-               return $contact;
-       }
+       $condition = ['network' => Protocol::TUMBLR, 'uid' => 0, 'poll' => 'tumblr::' . $blog->uuid];
+       $contact = Contact::selectFirst(['id', 'updated'], $condition);
+
+       $update = empty($contact) || $contact['updated'] < DateTimeFormat::utc('now -24 hours');
+
+       $public_fields = $fields = tumblr_get_contact_fields($blog, $uid, $update);
+
+       $avatar = $fields['avatar'] ?? '';
+       unset($fields['avatar']);
+       unset($public_fields['avatar']);
+
+       $public_fields['uid'] = 0;
+       $public_fields['rel'] = Contact::NOTHING;
+
        if (empty($contact)) {
-               $cid = tumblr_insert_contact($blog, $uid);
+               $cid = Contact::insert($public_fields);
        } else {
                $cid = $contact['id'];
+               Contact::update($public_fields, ['id' => $cid], true);
        }
 
-       $condition['uid'] = 0;
+       if ($uid != 0) {
+               $condition = ['network' => Protocol::TUMBLR, 'uid' => $uid, 'poll' => 'tumblr::' . $blog->uuid];
 
-       $contact = Contact::selectFirst([], $condition);
-       if (empty($contact)) {
-               $pcid = tumblr_insert_contact($blog, 0);
+               $contact = Contact::selectFirst(['id', 'rel', 'uid'], $condition);
+               if (!isset($fields['rel']) && isset($contact['rel'])) {
+                       $fields['rel'] = $contact['rel'];
+               } elseif (!isset($fields['rel'])) {
+                       $fields['rel'] = Contact::NOTHING;
+               }
+       }
+
+       if (($uid != 0) && ($fields['rel'] != Contact::NOTHING)) {
+               if (empty($contact)) {
+                       $cid = Contact::insert($fields);
+               } else {
+                       $cid = $contact['id'];
+                       Contact::update($fields, ['id' => $cid], true);
+               }
+               Logger::debug('Get user contact', ['id' => $cid, 'uid' => $uid, 'update' => $update]);
        } else {
-               $pcid = $contact['id'];
+               Logger::debug('Get public contact', ['id' => $cid, 'uid' => $uid, 'update' => $update]);
        }
 
-       tumblr_update_contact($blog, $uid, $cid, $pcid);
+       if (!empty($avatar)) {
+               Contact::updateAvatar($cid, $avatar);
+       }
 
        return Contact::getById($cid);
 }
 
-/**
- * Create a new contact
- *
- * @param stdClass $blog
- * @param integer $uid
- * @return void
- */
-function tumblr_insert_contact(stdClass $blog, int $uid)
+function tumblr_get_contact_fields(stdClass $blog, int $uid, bool $update): array
 {
        $baseurl = 'https://tumblr.com';
        $url     = $baseurl . '/' . $blog->name;
@@ -1041,69 +1126,43 @@ function tumblr_insert_contact(stdClass $blog, int $uid)
                'url'      => $url,
                'nurl'     => Strings::normaliseLink($url),
                'alias'    => $blog->url,
-               'name'     => $blog->title,
+               'name'     => $blog->title ?: $blog->name,
                'nick'     => $blog->name,
                'addr'     => $blog->name . '@tumblr.com',
                'about'    => HTML::toBBCode($blog->description),
                'updated'  => date(DateTimeFormat::MYSQL, $blog->updated)
        ];
-       return Contact::insert($fields);
-}
 
-/**
- * Updates the given contact for the given user and proviced contact ids
- *
- * @param stdClass $blog
- * @param integer $uid
- * @param integer $cid
- * @param integer $pcid
- * @return void
- */
-function tumblr_update_contact(stdClass $blog, int $uid, int $cid, int $pcid)
-{
+       if (!$update) {
+               Logger::debug('Got contact fields', ['uid' => $uid, 'url' => $fields['url']]);
+               return $fields;
+       }
+
        $info = tumblr_get($uid, 'blog/' . $blog->uuid . '/info');
        if ($info->meta->status > 399) {
-               Logger::notice('Error fetching dashboard', ['meta' => $info->meta, 'response' => $info->response, 'errors' => $info->errors]);
-               return;
+               Logger::notice('Error fetching blog info', ['meta' => $info->meta, 'response' => $info->response, 'errors' => $info->errors]);
+               return $fields;
        }
 
        $avatar = $info->response->blog->avatar;
        if (!empty($avatar)) {
-               Contact::updateAvatar($cid, $avatar[0]->url);
+               $fields['avatar'] = $avatar[0]->url;
        }
 
-       $baseurl = 'https://tumblr.com';
-       $url     = $baseurl . '/' . $info->response->blog->name;
-
        if ($info->response->blog->followed && $info->response->blog->subscribed) {
-               $rel = Contact::FRIEND;
+               $fields['rel'] = Contact::FRIEND;
        } elseif ($info->response->blog->followed && !$info->response->blog->subscribed) {
-               $rel = Contact::SHARING;
+               $fields['rel'] = Contact::SHARING;
        } elseif (!$info->response->blog->followed && $info->response->blog->subscribed) {
-               $rel = Contact::FOLLOWER;
+               $fields['rel'] = Contact::FOLLOWER;
        } else {
-               $rel = Contact::NOTHING;
+               $fields['rel'] = Contact::NOTHING;
        }
 
-       $uri_id = ItemURI::getIdByURI($url);
-       $fields = [
-               'url'     => $url,
-               'nurl'    => Strings::normaliseLink($url),
-               'uri-id'  => $uri_id,
-               'alias'   => $info->response->blog->url,
-               'name'    => $info->response->blog->title,
-               'nick'    => $info->response->blog->name,
-               'addr'    => $info->response->blog->name . '@tumblr.com',
-               'about'   => HTML::toBBCode($info->response->blog->description),
-               'updated' => date(DateTimeFormat::MYSQL, $info->response->blog->updated),
-               'header'  => $info->response->blog->theme->header_image_focused,
-               'rel'     => $rel,
-       ];
+       $fields['header'] = $info->response->blog->theme->header_image_focused;
 
-       Contact::update($fields, ['id' => $cid]);
-
-       $fields['rel'] = Contact::NOTHING;
-       Contact::update($fields, ['id' => $pcid]);
+       Logger::debug('Got updated contact fields', ['uid' => $uid, 'url' => $fields['url']]);
+       return $fields;
 }
 
 /**
@@ -1155,7 +1214,7 @@ function tumblr_get_blogs(int $uid): array
        return $blogs;
 }
 
-function tumblr_enabled_for_user(int $uid) 
+function tumblr_enabled_for_user(int $uid)
 {
        return !empty($uid) && !empty(DI::pConfig()->get($uid, 'tumblr', 'access_token')) &&
                !empty(DI::pConfig()->get($uid, 'tumblr', 'refresh_token')) &&
@@ -1167,24 +1226,25 @@ function tumblr_enabled_for_user(int $uid)
  * Get a contact array from a Tumblr url
  *
  * @param string $url
- * @return array
+ * @return array|null
+ * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  */
-function tumblr_get_contact_by_url(string $url): array
+function tumblr_get_contact_by_url(string $url): ?array
 {
        $consumer_key = DI::config()->get('tumblr', 'consumer_key');
        if (empty($consumer_key)) {
-               return [];
+               return null;
        }
 
        if (!preg_match('#^https?://tumblr.com/(.+)#', $url, $matches) && !preg_match('#^https?://www\.tumblr.com/(.+)#', $url, $matches) && !preg_match('#^https?://(.+)\.tumblr.com#', $url, $matches)) {
                try {
                        $curlResult = DI::httpClient()->get($url);
                } catch (\Exception $e) {
-                       return [];
+                       return null;
                }
                $html = $curlResult->getBody();
                if (empty($html)) {
-                       return [];
+                       return null;
                }
                $doc = new DOMDocument();
                @$doc->loadHTML($html);
@@ -1198,14 +1258,21 @@ function tumblr_get_contact_by_url(string $url): array
        }
 
        if (empty($blog)) {
-               return [];
+               return null;
        }
 
+       Logger::debug('Update Tumblr blog data', ['url' => $url]);
+
        $curlResult = DI::httpClient()->get('https://api.tumblr.com/v2/blog/' . $blog . '/info?api_key=' . $consumer_key);
        $body = $curlResult->getBody();
        $data = json_decode($body);
        if (empty($data)) {
-               return [];
+               return null;
+       }
+
+       if (is_array($data->response->blog) || empty($data->response->blog)) {
+               Logger::warning('Unexpected blog format', ['blog' => $blog, 'data' => $data]);
+               return null;
        }
 
        $baseurl = 'https://tumblr.com';
@@ -1220,7 +1287,7 @@ function tumblr_get_contact_by_url(string $url): array
                'notify'   => '',
                'poll'     => 'tumblr::' . $data->response->blog->uuid,
                'poco'     => '',
-               'name'     => $data->response->blog->title,
+               'name'     => $data->response->blog->title ?: $data->response->blog->name,
                'nick'     => $data->response->blog->name,
                'network'  => Protocol::TUMBLR,
                'baseurl'  => $baseurl,
@@ -1228,8 +1295,8 @@ function tumblr_get_contact_by_url(string $url): array
                'priority' => 0,
                'guid'     => $data->response->blog->uuid,
                'about'    => HTML::toBBCode($data->response->blog->description),
-       'photo'    => $data->response->blog->avatar[0]->url,
-       'header'   => $data->response->blog->theme->header_image_focused,
+               'photo'    => $data->response->blog->avatar[0]->url,
+               'header'   => $data->response->blog->theme->header_image_focused,
        ];
 }
 
@@ -1355,13 +1422,13 @@ function tumblr_get_token(int $uid, string $code = ''): string
                        Logger::info('Error fetching token', ['uid' => $uid, 'code' => $code, 'result' => $curlResult->getBody(), 'parameters' => $parameters]);
                        return '';
                }
-       
+
                $result = json_decode($curlResult->getBody());
                if (empty($result)) {
                        Logger::info('Invalid result when updating token', ['uid' => $uid]);
                        return '';
                }
-       
+
                $expires_at = time() + $result->expires_in;
                Logger::debug('Renewed token', ['uid' => $uid, 'expires_at' => date('c', $expires_at)]);
        }