]> git.mxchange.org Git - friendica-addons.git/blob - tumblr/tumblr.php
audon/audon.php aktualisiert
[friendica-addons.git] / tumblr / tumblr.php
1 <?php
2 /**
3  * Name: Tumblr Post Connector
4  * Description: Post to Tumblr
5  * Version: 2.0
6  * Author: Mike Macgirvin <http://macgirvin.com/profile/mike>
7  * Author: Michael Vogel <https://pirati.ca/profile/heluecht>
8  */
9
10 use Friendica\Content\PageInfo;
11 use Friendica\Content\Text\BBCode;
12 use Friendica\Content\Text\HTML;
13 use Friendica\Content\Text\NPF;
14 use Friendica\Core\Cache\Enum\Duration;
15 use Friendica\Core\Config\Util\ConfigFileManager;
16 use Friendica\Core\Hook;
17 use Friendica\Core\Logger;
18 use Friendica\Core\Protocol;
19 use Friendica\Core\Renderer;
20 use Friendica\Core\System;
21 use Friendica\Core\Worker;
22 use Friendica\Database\DBA;
23 use Friendica\DI;
24 use Friendica\Model\Contact;
25 use Friendica\Model\Item;
26 use Friendica\Model\Photo;
27 use Friendica\Model\Post;
28 use Friendica\Model\Tag;
29 use Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses;
30 use Friendica\Network\HTTPClient\Client\HttpClientAccept;
31 use Friendica\Network\HTTPClient\Client\HttpClientOptions;
32 use Friendica\Protocol\Activity;
33 use Friendica\Util\DateTimeFormat;
34 use Friendica\Util\Network;
35 use Friendica\Util\Strings;
36 use GuzzleHttp\Client;
37 use GuzzleHttp\Exception\RequestException;
38 use GuzzleHttp\HandlerStack;
39 use GuzzleHttp\Subscriber\Oauth\Oauth1;
40
41 define('TUMBLR_DEFAULT_POLL_INTERVAL', 10); // given in minutes
42 define('TUMBLR_DEFAULT_MAXIMUM_TAGS', 10);
43
44 function tumblr_install()
45 {
46         Hook::register('load_config',             __FILE__, 'tumblr_load_config');
47         Hook::register('hook_fork',               __FILE__, 'tumblr_hook_fork');
48         Hook::register('post_local',              __FILE__, 'tumblr_post_local');
49         Hook::register('notifier_normal',         __FILE__, 'tumblr_send');
50         Hook::register('jot_networks',            __FILE__, 'tumblr_jot_nets');
51         Hook::register('connector_settings',      __FILE__, 'tumblr_settings');
52         Hook::register('connector_settings_post', __FILE__, 'tumblr_settings_post');
53         Hook::register('cron',                    __FILE__, 'tumblr_cron');
54         Hook::register('support_follow',          __FILE__, 'tumblr_support_follow');
55         Hook::register('support_probe',           __FILE__, 'tumblr_support_probe');
56         Hook::register('follow',                  __FILE__, 'tumblr_follow');
57         Hook::register('unfollow',                __FILE__, 'tumblr_unfollow');
58         Hook::register('block',                   __FILE__, 'tumblr_block');
59         Hook::register('unblock',                 __FILE__, 'tumblr_unblock');
60         Hook::register('check_item_notification', __FILE__, 'tumblr_check_item_notification');
61         Hook::register('probe_detect',            __FILE__, 'tumblr_probe_detect');
62         Hook::register('item_by_link',            __FILE__, 'tumblr_item_by_link');
63         Logger::info('installed tumblr');
64 }
65
66 function tumblr_load_config(ConfigFileManager $loader)
67 {
68         DI::app()->getConfigCache()->load($loader->loadAddonConfig('tumblr'), \Friendica\Core\Config\ValueObject\Cache::SOURCE_STATIC);
69 }
70
71 function tumblr_check_item_notification(array &$notification_data)
72 {
73         if (!tumblr_enabled_for_user($notification_data['uid'])) {
74                 return;
75         }
76
77         $page = tumblr_get_page($notification_data['uid']);
78         if (empty($page)) {
79                 return;
80         }
81
82         $own_user = Contact::selectFirst(['url', 'alias'], ['network' => Protocol::TUMBLR, 'uid' => [0, $notification_data['uid']], 'poll' => 'tumblr::' . $page]);
83         if ($own_user) {
84                 $notification_data['profiles'][] = $own_user['url'];
85                 $notification_data['profiles'][] = $own_user['alias'];
86         }
87 }
88
89 function tumblr_probe_detect(array &$hookData)
90 {
91         // Don't overwrite an existing result
92         if (isset($hookData['result'])) {
93                 return;
94         }
95
96         // Avoid a lookup for the wrong network
97         if (!in_array($hookData['network'], ['', Protocol::TUMBLR])) {
98                 return;
99         }
100
101         $hookData['result'] = tumblr_get_contact_by_url($hookData['uri']);
102
103         // Authoritative probe should set the result even if the probe was unsuccessful
104         if ($hookData['network'] == Protocol::TUMBLR && empty($hookData['result'])) {
105                 $hookData['result'] = [];
106         }
107 }
108
109 function tumblr_item_by_link(array &$hookData)
110 {
111         // Don't overwrite an existing result
112         if (isset($hookData['item_id'])) {
113                 return;
114         }
115
116         if (!tumblr_enabled_for_user($hookData['uid'])) {
117                 return;
118         }
119
120         if (!preg_match('#^https?://www\.tumblr.com/blog/view/(.+)/(\d+).*#', $hookData['uri'], $matches) && !preg_match('#^https?://www\.tumblr.com/(.+)/(\d+).*#', $hookData['uri'], $matches)) {
121                 return;
122         }
123
124         Logger::debug('Found tumblr post', ['url' => $hookData['uri'], 'blog' => $matches[1], 'id' => $matches[2]]);
125
126         $parameters = ['id' => $matches[2], 'reblog_info' => false, 'notes_info' => false, 'npf' => false];
127         $result = tumblr_get($hookData['uid'], 'blog/' . $matches[1] . '/posts', $parameters);
128         if ($result->meta->status > 399) {
129                 Logger::notice('Error fetching status', ['meta' => $result->meta, 'response' => $result->response, 'errors' => $result->errors, 'blog' => $matches[1], 'id' => $matches[2]]);
130                 return [];
131         }
132
133         Logger::debug('Got post', ['blog' => $matches[1], 'id' => $matches[2], 'result' => $result->response->posts]);
134         if (!empty($result->response->posts)) {
135                 $hookData['item_id'] = tumblr_process_post($result->response->posts[0], $hookData['uid'], Item::PR_FETCHED);
136         }
137 }
138
139 function tumblr_support_follow(array &$data)
140 {
141         if ($data['protocol'] == Protocol::TUMBLR) {
142                 $data['result'] = true;
143         }
144 }
145
146 function tumblr_support_probe(array &$data)
147 {
148         if ($data['protocol'] == Protocol::TUMBLR) {
149                 $data['result'] = true;
150         }
151 }
152
153 function tumblr_follow(array &$hook_data)
154 {
155         $uid = DI::userSession()->getLocalUserId();
156
157         if (!tumblr_enabled_for_user($uid)) {
158                 return;
159         }
160
161         Logger::debug('Check if contact is Tumblr', ['url' => $hook_data['url']]);
162
163         $fields = tumblr_get_contact_by_url($hook_data['url']);
164         if (empty($fields)) {
165                 Logger::debug('Contact is not a Tumblr contact', ['url' => $hook_data['url']]);
166                 return;
167         }
168
169         $result = tumblr_post($uid, 'user/follow', ['url' => $fields['url']]);
170         if ($result->meta->status <= 399) {
171                 $hook_data['contact'] = $fields;
172                 Logger::debug('Successfully start following', ['url' => $fields['url']]);
173         } else {
174                 Logger::notice('Following failed', ['meta' => $result->meta, 'response' => $result->response, 'errors' => $result->errors, 'url' => $fields['url']]);
175         }
176 }
177
178 function tumblr_unfollow(array &$hook_data)
179 {
180         if (!tumblr_enabled_for_user($hook_data['uid'])) {
181                 return;
182         }
183
184         if (!tumblr_get_contact_uuid($hook_data['contact'])) {
185                 return;
186         }
187         $result = tumblr_post($hook_data['uid'], 'user/unfollow', ['url' => $hook_data['contact']['url']]);
188         $hook_data['result'] = ($result->meta->status <= 399);
189 }
190
191 function tumblr_block(array &$hook_data)
192 {
193         if (!tumblr_enabled_for_user($hook_data['uid'])) {
194                 return;
195         }
196
197         $uuid = tumblr_get_contact_uuid($hook_data['contact']);
198         if (!$uuid) {
199                 return;
200         }
201
202         $result = tumblr_post($hook_data['uid'], 'blog/' . tumblr_get_page($hook_data['uid']) . '/blocks', ['blocked_tumblelog' => $uuid]);
203         $hook_data['result'] = ($result->meta->status <= 399);
204
205         if ($hook_data['result']) {
206                 $cdata = Contact::getPublicAndUserContactID($hook_data['contact']['id'], $hook_data['uid']);
207                 if (!empty($cdata['user'])) {
208                         Contact::remove($cdata['user']);
209                 }
210         }
211 }
212
213 function tumblr_unblock(array &$hook_data)
214 {
215         if (!tumblr_enabled_for_user($hook_data['uid'])) {
216                 return;
217         }
218
219         $uuid = tumblr_get_contact_uuid($hook_data['contact']);
220         if (!$uuid) {
221                 return;
222         }
223
224         $result = tumblr_delete($hook_data['uid'], 'blog/' . tumblr_get_page($hook_data['uid']) . '/blocks', ['blocked_tumblelog' => $uuid]);
225         $hook_data['result'] = ($result->meta->status <= 399);
226 }
227
228 function tumblr_get_contact_uuid(array $contact): string
229 {
230         if (($contact['network'] != Protocol::TUMBLR) || (substr($contact['poll'], 0, 8) != 'tumblr::')) {
231                 return '';
232         }
233         return substr($contact['poll'], 8);
234 }
235
236 /**
237  * This is a statement rather than an actual function definition. The simple
238  * existence of this method is checked to figure out if the addon offers a
239  * module.
240  */
241 function tumblr_module()
242 {
243 }
244
245 function tumblr_content()
246 {
247         if (!DI::userSession()->getLocalUserId()) {
248                 DI::sysmsg()->addNotice(DI::l10n()->t('Permission denied.'));
249                 return;
250         }
251
252         switch (DI::args()->getArgv()[1] ?? '') {
253                 case 'connect':
254                         tumblr_connect();
255                         break;
256
257                 case 'redirect':
258                         tumblr_redirect();
259                         break;
260         }
261         DI::baseUrl()->redirect('settings/connectors/tumblr');
262 }
263
264 function tumblr_redirect()
265 {
266         if (($_REQUEST['state'] ?? '') != DI::session()->get('oauth_state')) {
267                 return;
268         }
269
270         tumblr_get_token(DI::userSession()->getLocalUserId(), $_REQUEST['code'] ?? '');
271 }
272
273 function tumblr_connect()
274 {
275         // Define the needed keys
276         $consumer_key    = DI::config()->get('tumblr', 'consumer_key');
277         $consumer_secret = DI::config()->get('tumblr', 'consumer_secret');
278
279         if (empty($consumer_key) || empty($consumer_secret)) {
280                 return;
281         }
282
283         $state = base64_encode(random_bytes(20));
284         DI::session()->set('oauth_state', $state);
285
286         $parameters = [
287                 'client_id'     => $consumer_key,
288                 'response_type' => 'code',
289                 'scope'         => 'basic write offline_access',
290                 'state'         => $state
291         ];
292
293         System::externalRedirect('https://www.tumblr.com/oauth2/authorize?' . http_build_query($parameters));
294 }
295
296 function tumblr_addon_admin(string &$o)
297 {
298         $t = Renderer::getMarkupTemplate('admin.tpl', 'addon/tumblr/');
299
300         $o = Renderer::replaceMacros($t, [
301                 '$submit' => DI::l10n()->t('Save Settings'),
302                 '$consumer_key'    => ['consumer_key', DI::l10n()->t('Consumer Key'), DI::config()->get('tumblr', 'consumer_key'), ''],
303                 '$consumer_secret' => ['consumer_secret', DI::l10n()->t('Consumer Secret'), DI::config()->get('tumblr', 'consumer_secret'), ''],
304                 '$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.')],
305         ]);
306 }
307
308 function tumblr_addon_admin_post()
309 {
310         DI::config()->set('tumblr', 'consumer_key', trim($_POST['consumer_key'] ?? ''));
311         DI::config()->set('tumblr', 'consumer_secret', trim($_POST['consumer_secret'] ?? ''));
312         DI::config()->set('tumblr', 'max_tags', max(0, intval($_POST['max_tags'] ?? '')));
313 }
314
315 function tumblr_settings(array &$data)
316 {
317         if (!DI::userSession()->getLocalUserId()) {
318                 return;
319         }
320
321         $enabled     = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'post') ?? false;
322         $def_enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'post_by_default') ?? false;
323         $import      = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'import') ?? false;
324         $tags        = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'tags') ?? [];
325
326         $max_tags = DI::config()->get('tumblr', 'max_tags') ?? TUMBLR_DEFAULT_MAXIMUM_TAGS;
327
328         $tags_str = implode(', ', $tags);
329         $cachekey = 'tumblr-blogs-' . DI::userSession()->getLocalUserId();
330         $blogs = DI::cache()->get($cachekey);
331         if (empty($blogs)) {
332                 $blogs = tumblr_get_blogs(DI::userSession()->getLocalUserId());
333                 if (!empty($blogs)) {
334                         DI::cache()->set($cachekey, $blogs, Duration::HALF_HOUR);
335                 }
336         }
337
338         if (!empty($blogs)) {
339                 $page = tumblr_get_page(DI::userSession()->getLocalUserId(), $blogs);
340
341                 $page_select = ['tumblr_page', DI::l10n()->t('Post to page:'), $page, '', $blogs];
342         }
343
344         $t    = Renderer::getMarkupTemplate('connector_settings.tpl', 'addon/tumblr/');
345         $html = Renderer::replaceMacros($t, [
346                 '$l10n' => [
347                         'connect'   => DI::l10n()->t('(Re-)Authenticate your tumblr page'),
348                         'noconnect' => DI::l10n()->t('You are not authenticated to tumblr'),
349                 ],
350
351                 '$authenticate_url' => DI::baseUrl() . '/tumblr/connect',
352
353                 '$enable'      => ['tumblr', DI::l10n()->t('Enable Tumblr Post Addon'), $enabled],
354                 '$bydefault'   => ['tumblr_bydefault', DI::l10n()->t('Post to Tumblr by default'), $def_enabled],
355                 '$import'      => ['tumblr_import', DI::l10n()->t('Import the remote timeline'), $import],
356                 '$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)],
357                 '$page_select' => $page_select ?? '',
358         ]);
359
360         $data = [
361                 'connector' => 'tumblr',
362                 'title'     => DI::l10n()->t('Tumblr Import/Export'),
363                 'image'     => 'images/tumblr.png',
364                 'enabled'   => $enabled,
365                 'html'      => $html,
366         ];
367 }
368
369 function tumblr_jot_nets(array &$jotnets_fields)
370 {
371         if (!DI::userSession()->getLocalUserId()) {
372                 return;
373         }
374
375         if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'post')) {
376                 $jotnets_fields[] = [
377                         'type' => 'checkbox',
378                         'field' => [
379                                 'tumblr_enable',
380                                 DI::l10n()->t('Post to Tumblr'),
381                                 DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'post_by_default')
382                         ]
383                 ];
384         }
385 }
386
387 function tumblr_settings_post(array &$b)
388 {
389         if (!empty($_POST['tumblr-submit'])) {
390                 DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'tumblr', 'post',            intval($_POST['tumblr']));
391                 DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'tumblr', 'page',            $_POST['tumblr_page']);
392                 DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'tumblr', 'post_by_default', intval($_POST['tumblr_bydefault']));
393                 DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'tumblr', 'import',          intval($_POST['tumblr_import']));
394
395                 $max_tags = DI::config()->get('tumblr', 'max_tags') ?? TUMBLR_DEFAULT_MAXIMUM_TAGS;
396                 $tags     = [];
397                 foreach (explode(',', $_POST['tags']) as $tag) {
398                         if (count($tags) < $max_tags) {
399                                 $tags[] = trim($tag, ' #');
400                         }
401                 }
402
403                 DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'tumblr', 'tags', $tags);
404         }
405 }
406
407 function tumblr_cron()
408 {
409         $last = DI::keyValue()->get('tumblr_last_poll');
410
411         $poll_interval = intval(DI::config()->get('tumblr', 'poll_interval'));
412         if (!$poll_interval) {
413                 $poll_interval = TUMBLR_DEFAULT_POLL_INTERVAL;
414         }
415
416         if ($last) {
417                 $next = $last + ($poll_interval * 60);
418                 if ($next > time()) {
419                         Logger::notice('poll interval not reached');
420                         return;
421                 }
422         }
423         Logger::notice('cron_start');
424
425         $abandon_days = intval(DI::config()->get('system', 'account_abandon_days'));
426         if ($abandon_days < 1) {
427                 $abandon_days = 0;
428         }
429
430         $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400);
431
432         $pconfigs = DBA::selectToArray('pconfig', [], ['cat' => 'tumblr', 'k' => 'import', 'v' => true]);
433         foreach ($pconfigs as $pconfig) {
434                 if ($abandon_days != 0) {
435                         if (!DBA::exists('user', ["`uid` = ? AND `login_date` >= ?", $pconfig['uid'], $abandon_limit])) {
436                                 Logger::notice('abandoned account: timeline from user will not be imported', ['user' => $pconfig['uid']]);
437                                 continue;
438                         }
439                 }
440
441                 Logger::notice('importing timeline - start', ['user' => $pconfig['uid']]);
442                 tumblr_fetch_dashboard($pconfig['uid']);
443                 tumblr_fetch_tags($pconfig['uid']);
444                 Logger::notice('importing timeline - done', ['user' => $pconfig['uid']]);
445         }
446
447         $last_clean = DI::keyValue()->get('tumblr_last_clean');
448         if (empty($last_clean) || ($last_clean + 86400 < time())) {
449                 Logger::notice('Start contact cleanup');
450                 $contacts = DBA::select('account-user-view', ['id', 'pid'], ["`network` = ? AND `uid` != ? AND `rel` = ?", Protocol::TUMBLR, 0, Contact::NOTHING]);
451                 while ($contact = DBA::fetch($contacts)) {
452                         Worker::add(Worker::PRIORITY_LOW, 'MergeContact', $contact['pid'], $contact['id'], 0);
453                 }
454                 DBA::close($contacts);
455                 DI::keyValue()->set('tumblr_last_clean', time());
456                 Logger::notice('Contact cleanup done');
457         }
458
459         Logger::notice('cron_end');
460
461         DI::keyValue()->set('tumblr_last_poll', time());
462 }
463
464 function tumblr_hook_fork(array &$b)
465 {
466         if ($b['name'] != 'notifier_normal') {
467                 return;
468         }
469
470         $post = $b['data'];
471
472         // Editing is not supported by the addon
473         if (($post['created'] !== $post['edited']) && !$post['deleted']) {
474                 DI::logger()->info('Editing is not supported by the addon');
475                 $b['execute'] = false;
476                 return;
477         }
478
479         if (DI::pConfig()->get($post['uid'], 'tumblr', 'import')) {
480                 // Don't post if it isn't a reply to a tumblr post
481                 if (($post['parent'] != $post['id']) && !Post::exists(['id' => $post['parent'], 'network' => Protocol::TUMBLR])) {
482                         Logger::notice('No tumblr parent found', ['item' => $post['id']]);
483                         $b['execute'] = false;
484                         return;
485                 }
486         } elseif (!strstr($post['postopts'] ?? '', 'tumblr') || ($post['parent'] != $post['id']) || $post['private']) {
487                 DI::logger()->info('Activities are never exported when we don\'t import the tumblr timeline', ['uid' => $post['uid']]);
488                 $b['execute'] = false;
489                 return;
490         }
491 }
492
493 function tumblr_post_local(array &$b)
494 {
495         if ($b['edit']) {
496                 return;
497         }
498
499         if (!DI::userSession()->getLocalUserId() || (DI::userSession()->getLocalUserId() != $b['uid'])) {
500                 return;
501         }
502
503         if ($b['private'] || $b['parent']) {
504                 return;
505         }
506
507         $tmbl_post   = intval(DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'post'));
508         $tmbl_enable = (($tmbl_post && !empty($_REQUEST['tumblr_enable'])) ? intval($_REQUEST['tumblr_enable']) : 0);
509
510         // if API is used, default to the chosen settings
511         if ($b['api_source'] && intval(DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'tumblr', 'post_by_default'))) {
512                 $tmbl_enable = 1;
513         }
514
515         if (!$tmbl_enable) {
516                 return;
517         }
518
519         if (strlen($b['postopts'])) {
520                 $b['postopts'] .= ',';
521         }
522
523         $b['postopts'] .= 'tumblr';
524 }
525
526 function tumblr_send(array &$b)
527 {
528         if (($b['created'] !== $b['edited']) && !$b['deleted']) {
529                 return;
530         }
531
532         if ($b['gravity'] != Item::GRAVITY_PARENT) {
533                 Logger::debug('Got comment', ['item' => $b]);
534
535                 $parent = tumblr_get_post_from_uri($b['thr-parent']);
536                 if (empty($parent)) {
537                         Logger::notice('No tumblr post', ['thr-parent' => $b['thr-parent']]);
538                         return;
539                 }
540
541                 Logger::debug('Parent found', ['parent' => $parent]);
542
543                 $page = tumblr_get_page($b['uid']);
544
545                 if ($b['gravity'] == Item::GRAVITY_COMMENT) {
546                         Logger::notice('Commenting is not supported (yet)');
547                 } else {
548                         if (($b['verb'] == Activity::LIKE) && !$b['deleted']) {
549                                 $params = ['id' => $parent['id'], 'reblog_key' => $parent['reblog_key']];
550                                 $result = tumblr_post($b['uid'], 'user/like', $params);
551                         } elseif (($b['verb'] == Activity::LIKE) && $b['deleted']) {
552                                 $params = ['id' => $parent['id'], 'reblog_key' => $parent['reblog_key']];
553                                 $result = tumblr_post($b['uid'], 'user/unlike', $params);
554                         } elseif (($b['verb'] == Activity::ANNOUNCE) && !$b['deleted']) {
555                                 $params = ['id' => $parent['id'], 'reblog_key' => $parent['reblog_key']];
556                                 $result = tumblr_post($b['uid'], 'blog/' . $page . '/post/reblog', $params);
557                         } elseif (($b['verb'] == Activity::ANNOUNCE) && $b['deleted']) {
558                                 $announce = tumblr_get_post_from_uri($b['extid']);
559                                 if (empty($announce)) {
560                                         return;
561                                 }
562                                 $params = ['id' => $announce['id']];
563                                 $result = tumblr_post($b['uid'], 'blog/' . $page . '/post/delete', $params);
564                         } else {
565                                 // Unsupported activity
566                                 return;
567                         }
568
569                         if ($result->meta->status < 400) {
570                                 Logger::info('Successfully performed activity', ['verb' => $b['verb'], 'deleted' => $b['deleted'], 'meta' => $result->meta, 'response' => $result->response]);
571                                 if (!$b['deleted'] && !empty($result->response->id_string)) {
572                                         Item::update(['extid' => 'tumblr::' . $result->response->id_string], ['id' => $b['id']]);
573                                 }
574                         } else {
575                                 Logger::notice('Error while performing activity', ['verb' => $b['verb'], 'deleted' => $b['deleted'], 'meta' => $result->meta, 'response' => $result->response, 'errors' => $result->errors, 'params' => $params]);
576                         }
577                 }
578                 return;
579         } elseif ($b['private'] || !strstr($b['postopts'], 'tumblr')) {
580                 return;
581         }
582
583         if (!tumblr_send_npf($b)) {
584                 tumblr_send_legacy($b);
585         }
586 }
587
588 function tumblr_send_legacy(array $b)
589 {
590         $b['body'] = BBCode::removeAttachment($b['body']);
591
592         $title = trim($b['title']);
593
594         $media = Post\Media::getByURIId($b['uri-id'], [Post\Media::HTML, Post\Media::AUDIO, Post\Media::VIDEO, Post\Media::IMAGE]);
595
596         $photo = array_search(Post\Media::IMAGE, array_column($media, 'type'));
597         $link  = array_search(Post\Media::HTML, array_column($media, 'type'));
598         $audio = array_search(Post\Media::AUDIO, array_column($media, 'type'));
599         $video = array_search(Post\Media::VIDEO, array_column($media, 'type'));
600
601         $params = [
602                 'state'  => 'published',
603                 'tags'   => implode(',', array_column(Tag::getByURIId($b['uri-id']), 'name')),
604                 'tweet'  => 'off',
605                 'format' => 'html',
606         ];
607
608         $body = BBCode::removeShareInformation($b['body']);
609         $body = Post\Media::removeFromEndOfBody($body);
610
611         if ($photo !== false) {
612                 $params['type'] = 'photo';
613                 $params['caption'] = BBCode::convertForUriId($b['uri-id'], $body, BBCode::CONNECTORS);
614                 $params['data'] = [];
615                 foreach ($media as $photo) {
616                         if ($photo['type'] == Post\Media::IMAGE) {
617                                 if (Network::isLocalLink($photo['url']) && ($data = Photo::getResourceData($photo['url']))) {
618                                         $photo = Photo::selectFirst([], ["`resource-id` = ? AND `scale` > ?", $data['guid'], 0]);
619                                         if (!empty($photo)) {
620                                                 $params['data'][] = Photo::getImageDataForPhoto($photo);
621                                         }
622                                 }
623                         }
624                 }
625         } elseif ($link !== false) {
626                 $params['type']        = 'link';
627                 $params['title']       = $media[$link]['name'];
628                 $params['url']         = $media[$link]['url'];
629                 $params['description'] = BBCode::convertForUriId($b['uri-id'], $body, BBCode::CONNECTORS);
630
631                 if (!empty($media[$link]['preview'])) {
632                         $params['thumbnail'] = $media[$link]['preview'];
633                 }
634                 if (!empty($media[$link]['description'])) {
635                         $params['excerpt'] = $media[$link]['description'];
636                 }
637                 if (!empty($media[$link]['author-name'])) {
638                         $params['author'] = $media[$link]['author-name'];
639                 }
640         } elseif ($audio !== false) {
641                 $params['type']         = 'audio';
642                 $params['external_url'] = $media[$audio]['url'];
643                 $params['caption']      = BBCode::convertForUriId($b['uri-id'], $body, BBCode::CONNECTORS);
644         } elseif ($video !== false) {
645                 $params['type']    = 'video';
646                 $params['embed']   = $media[$video]['url'];
647                 $params['caption'] = BBCode::convertForUriId($b['uri-id'], $body, BBCode::CONNECTORS);
648         } else {
649                 $params['type']  = 'text';
650                 $params['title'] = $title;
651                 $params['body']  = BBCode::convertForUriId($b['uri-id'], $b['body'], BBCode::CONNECTORS);
652         }
653
654         if (isset($params['caption']) && (trim($title) != '')) {
655                 $params['caption'] = '<h1>' . $title . '</h1>' .
656                         '<p>' . $params['caption'] . '</p>';
657         }
658
659         $page = tumblr_get_page($b['uid']);
660
661         $result = tumblr_post($b['uid'], 'blog/' . $page . '/post', $params);
662
663         if ($result->meta->status < 400) {
664                 Logger::info('Success (legacy)', ['blog' => $page, 'meta' => $result->meta, 'response' => $result->response]);
665         } else {
666                 Logger::notice('Error posting blog (legacy)', ['blog' => $page, 'meta' => $result->meta, 'response' => $result->response, 'errors' => $result->errors, 'params' => $params]);
667         }
668 }
669
670 function tumblr_send_npf(array $post): bool
671 {
672         $page = tumblr_get_page($post['uid']);
673
674         if (empty($page)) {
675                 Logger::notice('Missing page, post will not be send to Tumblr.', ['uid' => $post['uid'], 'page' => $page, 'id' => $post['id']]);
676                 // "true" is returned, since the legacy function will fail as well.
677                 return true;
678         }
679
680         $post['body'] = Post\Media::addAttachmentsToBody($post['uri-id'], $post['body']);
681         if (!empty($post['title'])) {
682                 $post['body'] = '[h1]' . $post['title'] . "[/h1]\n" . $post['body'];
683         }
684
685         $params = [
686                 'content'                => NPF::fromBBCode($post['body'], $post['uri-id']),
687                 'state'                  => 'published',
688                 'date'                   => DateTimeFormat::utc($post['created'], DateTimeFormat::ATOM),
689                 'tags'                   => implode(',', array_column(Tag::getByURIId($post['uri-id']), 'name')),
690                 'is_private'             => false,
691                 'interactability_reblog' => 'everyone'
692         ];
693
694         $result = tumblr_post($post['uid'], 'blog/' . $page . '/posts', $params);
695
696         if ($result->meta->status < 400) {
697                 Logger::info('Success (NPF)', ['blog' => $page, 'meta' => $result->meta, 'response' => $result->response]);
698                 return true;
699         } else {
700                 Logger::notice('Error posting blog (NPF)', ['blog' => $page, 'meta' => $result->meta, 'response' => $result->response, 'errors' => $result->errors, 'params' => $params]);
701                 return false;
702         }
703 }
704
705 function tumblr_get_post_from_uri(string $uri): array
706 {
707         $parts = explode(':', $uri);
708         if (($parts[0] != 'tumblr') || empty($parts[2])) {
709                 return [];
710         }
711
712         $post['id']        = $parts[2];
713         $post['reblog_key'] = $parts[3] ?? '';
714
715         $post['reblog_key'] = str_replace('@t', '', $post['reblog_key']); // Temp
716         return $post;
717 }
718
719 /**
720  * Fetch posts for user defined hashtags for the given user
721  *
722  * @param integer $uid
723  * @return void
724  */
725 function tumblr_fetch_tags(int $uid)
726 {
727         if (!DI::config()->get('tumblr', 'max_tags') ?? TUMBLR_DEFAULT_MAXIMUM_TAGS) {
728                 return;
729         }
730
731         foreach (DI::pConfig()->get($uid, 'tumblr', 'tags') ?? [] as $tag) {
732                 $data = tumblr_get($uid, 'tagged', ['tag' => $tag]);
733                 foreach (array_reverse($data->response) as $post) {
734                         $id = tumblr_process_post($post, $uid, Item::PR_TAG);
735                         if (!empty($id)) {
736                                 Logger::debug('Tag post imported', ['tag' => $tag, 'id' => $id]);
737                         }
738                 }
739         }
740 }
741
742 /**
743  * Fetch the dashboard (timeline) for the given user
744  *
745  * @param integer $uid
746  * @return void
747  */
748 function tumblr_fetch_dashboard(int $uid)
749 {
750         $parameters = ['reblog_info' => false, 'notes_info' => false, 'npf' => false];
751
752         $last = DI::pConfig()->get($uid, 'tumblr', 'last_id');
753         if (!empty($last)) {
754                 $parameters['since_id'] = $last;
755         }
756
757         $dashboard = tumblr_get($uid, 'user/dashboard', $parameters);
758         if ($dashboard->meta->status > 399) {
759                 Logger::notice('Error fetching dashboard', ['meta' => $dashboard->meta, 'response' => $dashboard->response, 'errors' => $dashboard->errors]);
760                 return [];
761         }
762
763         if (empty($dashboard->response->posts)) {
764                 return;
765         }
766
767         foreach (array_reverse($dashboard->response->posts) as $post) {
768                 if ($post->id > $last) {
769                         $last = $post->id;
770                 }
771
772                 Logger::debug('Importing post', ['uid' => $uid, 'created' => date(DateTimeFormat::MYSQL, $post->timestamp), 'id' => $post->id_string]);
773
774                 tumblr_process_post($post, $uid, Item::PR_NONE);
775
776                 DI::pConfig()->set($uid, 'tumblr', 'last_id', $last);
777         }
778 }
779
780 function tumblr_process_post(stdClass $post, int $uid, int $post_reason): int
781 {
782         $uri = 'tumblr::' . $post->id_string . ':' . $post->reblog_key;
783
784         if (Post::exists(['uri' => $uri, 'uid' => $uid]) || ($post->blog->uuid == tumblr_get_page($uid))) {
785                 return 0;
786         }
787
788         $item = tumblr_get_header($post, $uri, $uid);
789
790         $item = tumblr_get_content($item, $post);
791
792         $item['post-reason'] = $post_reason;
793
794         if (!empty($post->followed)) {
795                 $item['post-reason'] = Item::PR_FOLLOWER;
796         }
797
798         $id = item::insert($item);
799
800         if ($id) {
801                 $stored = Post::selectFirst(['uri-id'], ['id' => $id]);
802
803                 if (!empty($post->tags)) {
804                         foreach ($post->tags as $tag) {
805                                 Tag::store($stored['uri-id'], Tag::HASHTAG, $tag);
806                         }
807                 }
808         }
809         return $id;
810 }
811
812 /**
813  * Sets the initial data for the item array
814  *
815  * @param stdClass $post
816  * @param string $uri
817  * @param integer $uid
818  * @return array
819  */
820 function tumblr_get_header(stdClass $post, string $uri, int $uid): array
821 {
822         $contact = tumblr_get_contact($post->blog, $uid);
823         $item = [
824                 'network'       => Protocol::TUMBLR,
825                 'uid'           => $uid,
826                 'wall'          => false,
827                 'uri'           => $uri,
828                 'private'       => Item::UNLISTED,
829                 'verb'          => Activity::POST,
830                 'contact-id'    => $contact['id'],
831                 'author-name'   => $contact['name'],
832                 'author-link'   => $contact['url'],
833                 'author-avatar' => $contact['avatar'],
834                 'plink'         => $post->post_url,
835                 'created'       => date(DateTimeFormat::MYSQL, $post->timestamp)
836         ];
837
838         $item['owner-name']   = $item['author-name'];
839         $item['owner-link']   = $item['author-link'];
840         $item['owner-avatar'] = $item['author-avatar'];
841
842         return $item;
843 }
844
845 /**
846  * Set the body according the given content type
847  *
848  * @param array $item
849  * @param stdClass $post
850  * @return array
851  */
852 function tumblr_get_content(array $item, stdClass $post): array
853 {
854         switch ($post->type) {
855                 case 'text':
856                         $item['title'] = $post->title;
857                         $item['body'] = HTML::toBBCode(tumblr_add_npf_data($post->body, $post->post_url));
858                         break;
859
860                 case 'quote':
861                         if (empty($post->text)) {
862                                 $body = HTML::toBBCode($post->text) . "\n";
863                         } else {
864                                 $body = '';
865                         }
866                         if (!empty($post->source_title) && !empty($post->source_url)) {
867                                 $body .= '[url=' . $post->source_url . ']' . $post->source_title . "[/url]:\n";
868                         } elseif (!empty($post->source_title)) {
869                                 $body .= $post->source_title . ":\n";
870                         }
871                         $body .= '[quote]' . HTML::toBBCode($post->source) . '[/quote]';
872                         $item['body'] = $body;
873                         break;
874
875                 case 'link':
876                         $item['body'] = HTML::toBBCode($post->description) . "\n" . PageInfo::getFooterFromUrl($post->url);
877                         break;
878
879                 case 'answer':
880                         if (!empty($post->asking_name) && !empty($post->asking_url)) {
881                                 $body = '[url=' . $post->asking_url . ']' . $post->asking_name . "[/url]:\n";
882                         } elseif (!empty($post->asking_name)) {
883                                 $body = $post->asking_name . ":\n";
884                         } else {
885                                 $body = '';
886                         }
887                         $body .= '[quote]' . HTML::toBBCode($post->question) . "[/quote]\n" . HTML::toBBCode($post->answer);
888                         $item['body'] = $body;
889                         break;
890
891                 case 'video':
892                         $item['body'] = HTML::toBBCode($post->caption);
893                         if (!empty($post->video_url)) {
894                                 $item['body'] .= "\n[video]" . $post->video_url . "[/video]\n";
895                         } elseif (!empty($post->thumbnail_url)) {
896                                 $item['body'] .= "\n[url=" . $post->permalink_url . "][img]" . $post->thumbnail_url . "[/img][/url]\n";
897                         } elseif (!empty($post->permalink_url)) {
898                                 $item['body'] .= "\n[url]" . $post->permalink_url . "[/url]\n";
899                         } elseif (!empty($post->source_url) && !empty($post->source_title)) {
900                                 $item['body'] .= "\n[url=" . $post->source_url . "]" . $post->source_title . "[/url]\n";
901                         } elseif (!empty($post->source_url)) {
902                                 $item['body'] .= "\n[url]" . $post->source_url . "[/url]\n";
903                         }
904                         break;
905
906                 case 'audio':
907                         $item['body'] = HTML::toBBCode($post->caption);
908                         if (!empty($post->source_url) && !empty($post->source_title)) {
909                                 $item['body'] .= "\n[url=" . $post->source_url . "]" . $post->source_title . "[/url]\n";
910                         } elseif (!empty($post->source_url)) {
911                                 $item['body'] .= "\n[url]" . $post->source_url . "[/url]\n";
912                         }
913                         break;
914
915                 case 'photo':
916                         $item['body'] = HTML::toBBCode($post->caption);
917                         foreach ($post->photos as $photo) {
918                                 if (!empty($photo->original_size)) {
919                                         $item['body'] .= "\n[img]" . $photo->original_size->url . "[/img]";
920                                 } elseif (!empty($photo->alt_sizes)) {
921                                         $item['body'] .= "\n[img]" . $photo->alt_sizes[0]->url . "[/img]";
922                                 }
923                         }
924                         break;
925
926                 case 'chat':
927                         $item['title'] = $post->title;
928                         $item['body']  = "\n[ul]";
929                         foreach ($post->dialogue as $line) {
930                                 $item['body'] .= "\n[li]" . $line->label . " " . $line->phrase . "[/li]";
931                         }
932                         $item['body'] .= "[/ul]\n";
933                         break;
934         }
935         return $item;
936 }
937
938 function tumblr_add_npf_data(string $html, string $plink): string
939 {
940         $doc = new DOMDocument();
941
942         $doc->formatOutput = true;
943         @$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
944         $xpath = new DomXPath($doc);
945         $list = $xpath->query('//p[@class="npf_link"]');
946         foreach ($list as $node) {
947                 $data = tumblr_get_npf_data($node);
948                 if (empty($data)) {
949                         continue;
950                 }
951
952                 tumblr_replace_with_npf($doc, $node, tumblr_get_type_replacement($data, $plink));
953         }
954
955         $list = $xpath->query('//div[@data-npf]');
956         foreach ($list as $node) {
957                 $data = tumblr_get_npf_data($node);
958                 if (empty($data)) {
959                         continue;
960                 }
961
962                 tumblr_replace_with_npf($doc, $node, tumblr_get_type_replacement($data, $plink));
963         }
964
965         $list = $xpath->query('//figure[@data-provider="youtube"]');
966         foreach ($list as $node) {
967                 $attributes = tumblr_get_attributes($node);
968                 if (empty($attributes['data-url'])) {
969                         continue;
970                 }
971                 tumblr_replace_with_npf($doc, $node, '[youtube]' . $attributes['data-url'] . '[/youtube]');
972         }
973
974         $list = $xpath->query('//figure[@data-npf]');
975         foreach ($list as $node) {
976                 $data = tumblr_get_npf_data($node);
977                 if (empty($data)) {
978                         continue;
979                 }
980                 tumblr_replace_with_npf($doc, $node, tumblr_get_type_replacement($data, $plink));
981         }
982
983         return $doc->saveHTML();
984 }
985
986 function tumblr_replace_with_npf(DOMDocument $doc, DOMNode $node, string $replacement)
987 {
988         if (empty($replacement)) {
989                 return;
990         }
991         $replace = $doc->createTextNode($replacement);
992         $node->parentNode->insertBefore($replace, $node);
993         $node->parentNode->removeChild($node);
994 }
995
996 function tumblr_get_npf_data(DOMNode $node): array
997 {
998         $attributes = tumblr_get_attributes($node);
999         if (empty($attributes['data-npf'])) {
1000                 return [];
1001         }
1002
1003         return json_decode($attributes['data-npf'], true);
1004 }
1005
1006 function tumblr_get_attributes($node): array
1007 {
1008         if (empty($node->attributes)) {
1009                 return [];
1010         }
1011
1012         $attributes = [];
1013         foreach ($node->attributes as $key => $attribute) {
1014                 $attributes[$key] = trim($attribute->value);
1015         }
1016         return $attributes;
1017 }
1018
1019 function tumblr_get_type_replacement(array $data, string $plink): string
1020 {
1021         switch ($data['type']) {
1022                 case 'poll':
1023                         $body = '[p][url=' . $plink . ']' . $data['question'] . '[/url][/p][ul]';
1024                         foreach ($data['answers'] as $answer) {
1025                                 $body .= '[li]' . $answer['answer_text'] . '[/li]';
1026                         }
1027                         $body .= '[/ul]';
1028                         break;
1029
1030                 case 'link':
1031                         $body = PageInfo::getFooterFromUrl(str_replace('https://href.li/?', '', $data['url']));
1032                         break;
1033
1034                 case 'video':
1035                         if (!empty($data['url']) && ($data['provider'] == 'tumblr')) {
1036                                 $body = '[video]' . $data['url'] . '[/video]';
1037                                 break;
1038                         }
1039
1040                 default:
1041                         Logger::notice('Unknown type', ['type' => $data['type'], 'data' => $data, 'plink' => $plink]);
1042                         $body = '';
1043         }
1044
1045         return $body;
1046 }
1047
1048 /**
1049  * Get a contact array for the given blog
1050  *
1051  * @param stdClass $blog
1052  * @param integer $uid
1053  * @return array
1054  */
1055 function tumblr_get_contact(stdClass $blog, int $uid): array
1056 {
1057         $condition = ['network' => Protocol::TUMBLR, 'uid' => 0, 'poll' => 'tumblr::' . $blog->uuid];
1058         $contact = Contact::selectFirst(['id', 'updated'], $condition);
1059
1060         $update = empty($contact) || $contact['updated'] < DateTimeFormat::utc('now -24 hours');
1061
1062         $public_fields = $fields = tumblr_get_contact_fields($blog, $uid, $update);
1063
1064         $avatar = $fields['avatar'] ?? '';
1065         unset($fields['avatar']);
1066         unset($public_fields['avatar']);
1067
1068         $public_fields['uid'] = 0;
1069         $public_fields['rel'] = Contact::NOTHING;
1070
1071         if (empty($contact)) {
1072                 $cid = Contact::insert($public_fields);
1073         } else {
1074                 $cid = $contact['id'];
1075                 Contact::update($public_fields, ['id' => $cid], true);
1076         }
1077
1078         if ($uid != 0) {
1079                 $condition = ['network' => Protocol::TUMBLR, 'uid' => $uid, 'poll' => 'tumblr::' . $blog->uuid];
1080
1081                 $contact = Contact::selectFirst(['id', 'rel', 'uid'], $condition);
1082                 if (!isset($fields['rel']) && isset($contact['rel'])) {
1083                         $fields['rel'] = $contact['rel'];
1084                 } elseif (!isset($fields['rel'])) {
1085                         $fields['rel'] = Contact::NOTHING;
1086                 }
1087         }
1088
1089         if (($uid != 0) && ($fields['rel'] != Contact::NOTHING)) {
1090                 if (empty($contact)) {
1091                         $cid = Contact::insert($fields);
1092                 } else {
1093                         $cid = $contact['id'];
1094                         Contact::update($fields, ['id' => $cid], true);
1095                 }
1096                 Logger::debug('Get user contact', ['id' => $cid, 'uid' => $uid, 'update' => $update]);
1097         } else {
1098                 Logger::debug('Get public contact', ['id' => $cid, 'uid' => $uid, 'update' => $update]);
1099         }
1100
1101         if (!empty($avatar)) {
1102                 Contact::updateAvatar($cid, $avatar);
1103         }
1104
1105         return Contact::getById($cid);
1106 }
1107
1108 function tumblr_get_contact_fields(stdClass $blog, int $uid, bool $update): array
1109 {
1110         $baseurl = 'https://tumblr.com';
1111         $url     = $baseurl . '/' . $blog->name;
1112
1113         $fields = [
1114                 'uid'      => $uid,
1115                 'network'  => Protocol::TUMBLR,
1116                 'poll'     => 'tumblr::' . $blog->uuid,
1117                 'baseurl'  => $baseurl,
1118                 'priority' => 1,
1119                 'writable' => true,
1120                 'blocked'  => false,
1121                 'readonly' => false,
1122                 'pending'  => false,
1123                 'url'      => $url,
1124                 'nurl'     => Strings::normaliseLink($url),
1125                 'alias'    => $blog->url,
1126                 'name'     => $blog->title ?: $blog->name,
1127                 'nick'     => $blog->name,
1128                 'addr'     => $blog->name . '@tumblr.com',
1129                 'about'    => HTML::toBBCode($blog->description),
1130                 'updated'  => date(DateTimeFormat::MYSQL, $blog->updated)
1131         ];
1132
1133         if (!$update) {
1134                 Logger::debug('Got contact fields', ['uid' => $uid, 'url' => $fields['url']]);
1135                 return $fields;
1136         }
1137
1138         $info = tumblr_get($uid, 'blog/' . $blog->uuid . '/info');
1139         if ($info->meta->status > 399) {
1140                 Logger::notice('Error fetching blog info', ['meta' => $info->meta, 'response' => $info->response, 'errors' => $info->errors]);
1141                 return $fields;
1142         }
1143
1144         $avatar = $info->response->blog->avatar;
1145         if (!empty($avatar)) {
1146                 $fields['avatar'] = $avatar[0]->url;
1147         }
1148
1149         if ($info->response->blog->followed && $info->response->blog->subscribed) {
1150                 $fields['rel'] = Contact::FRIEND;
1151         } elseif ($info->response->blog->followed && !$info->response->blog->subscribed) {
1152                 $fields['rel'] = Contact::SHARING;
1153         } elseif (!$info->response->blog->followed && $info->response->blog->subscribed) {
1154                 $fields['rel'] = Contact::FOLLOWER;
1155         } else {
1156                 $fields['rel'] = Contact::NOTHING;
1157         }
1158
1159         $fields['header'] = $info->response->blog->theme->header_image_focused;
1160
1161         Logger::debug('Got updated contact fields', ['uid' => $uid, 'url' => $fields['url']]);
1162         return $fields;
1163 }
1164
1165 /**
1166  * Get the default page for posting. Detects the value if not provided or has got a bad value.
1167  *
1168  * @param integer $uid
1169  * @param array $blogs
1170  * @return string
1171  */
1172 function tumblr_get_page(int $uid, array $blogs = []): string
1173 {
1174         $page = DI::pConfig()->get($uid, 'tumblr', 'page');
1175
1176         if (!empty($page) && (strpos($page, '/') === false)) {
1177                 return $page;
1178         }
1179
1180         if (empty($blogs)) {
1181                 $blogs = tumblr_get_blogs($uid);
1182         }
1183
1184         if (!empty($blogs)) {
1185                 $page = array_key_first($blogs);
1186                 DI::pConfig()->set($uid, 'tumblr', 'page', $page);
1187                 return $page;
1188         }
1189
1190         return '';
1191 }
1192
1193 /**
1194  * Get an array of blogs for the given user
1195  *
1196  * @param integer $uid
1197  * @return array
1198  */
1199 function tumblr_get_blogs(int $uid): array
1200 {
1201         $userinfo = tumblr_get($uid, 'user/info');
1202         if ($userinfo->meta->status > 299) {
1203                 Logger::notice('Error fetching blogs', ['meta' => $userinfo->meta, 'response' => $userinfo->response, 'errors' => $userinfo->errors]);
1204                 return [];
1205         }
1206
1207         $blogs = [];
1208         foreach ($userinfo->response->user->blogs as $blog) {
1209                 $blogs[$blog->uuid] = $blog->name;
1210         }
1211         return $blogs;
1212 }
1213
1214 function tumblr_enabled_for_user(int $uid)
1215 {
1216         return !empty($uid) && !empty(DI::pConfig()->get($uid, 'tumblr', 'access_token')) &&
1217                 !empty(DI::pConfig()->get($uid, 'tumblr', 'refresh_token')) &&
1218                 !empty(DI::config()->get('tumblr', 'consumer_key')) &&
1219                 !empty(DI::config()->get('tumblr', 'consumer_secret'));
1220 }
1221
1222 /**
1223  * Get a contact array from a Tumblr url
1224  *
1225  * @param string $url
1226  * @return array|null
1227  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1228  */
1229 function tumblr_get_contact_by_url(string $url): ?array
1230 {
1231         $consumer_key = DI::config()->get('tumblr', 'consumer_key');
1232         if (empty($consumer_key)) {
1233                 return null;
1234         }
1235
1236         if (!preg_match('#^https?://tumblr.com/(.+)#', $url, $matches) && !preg_match('#^https?://www\.tumblr.com/(.+)#', $url, $matches) && !preg_match('#^https?://(.+)\.tumblr.com#', $url, $matches)) {
1237                 try {
1238                         $curlResult = DI::httpClient()->get($url);
1239                 } catch (\Exception $e) {
1240                         return null;
1241                 }
1242                 $html = $curlResult->getBody();
1243                 if (empty($html)) {
1244                         return null;
1245                 }
1246                 $doc = new DOMDocument();
1247                 @$doc->loadHTML($html);
1248                 $xpath = new DomXPath($doc);
1249                 $body = $xpath->query('body');
1250                 $attributes = tumblr_get_attributes($body->item(0));
1251                 $blog = $attributes['data-urlencoded-name'] ?? '';
1252         } else {
1253                 $blogs = explode('/', $matches[1]);
1254                 $blog = $blogs[0] ?? '';
1255         }
1256
1257         if (empty($blog)) {
1258                 return null;
1259         }
1260
1261         Logger::debug('Update Tumblr blog data', ['url' => $url]);
1262
1263         $curlResult = DI::httpClient()->get('https://api.tumblr.com/v2/blog/' . $blog . '/info?api_key=' . $consumer_key);
1264         $body = $curlResult->getBody();
1265         $data = json_decode($body);
1266         if (empty($data)) {
1267                 return null;
1268         }
1269
1270         if (is_array($data->response->blog) || empty($data->response->blog)) {
1271                 Logger::warning('Unexpected blog format', ['blog' => $blog, 'data' => $data]);
1272                 return null;
1273         }
1274
1275         $baseurl = 'https://tumblr.com';
1276         $url     = $baseurl . '/' . $data->response->blog->name;
1277
1278         return [
1279                 'url'      => $url,
1280                 'nurl'     => Strings::normaliseLink($url),
1281                 'addr'     => $data->response->blog->name . '@tumblr.com',
1282                 'alias'    => $data->response->blog->url,
1283                 'batch'    => '',
1284                 'notify'   => '',
1285                 'poll'     => 'tumblr::' . $data->response->blog->uuid,
1286                 'poco'     => '',
1287                 'name'     => $data->response->blog->title ?: $data->response->blog->name,
1288                 'nick'     => $data->response->blog->name,
1289                 'network'  => Protocol::TUMBLR,
1290                 'baseurl'  => $baseurl,
1291                 'pubkey'   => '',
1292                 'priority' => 0,
1293                 'guid'     => $data->response->blog->uuid,
1294                 'about'    => HTML::toBBCode($data->response->blog->description),
1295                 'photo'    => $data->response->blog->avatar[0]->url,
1296                 'header'   => $data->response->blog->theme->header_image_focused,
1297         ];
1298 }
1299
1300 /**
1301  * Perform an OAuth2 GET request
1302  *
1303  * @param integer $uid
1304  * @param string $url
1305  * @param array $parameters
1306  * @return stdClass
1307  */
1308 function tumblr_get(int $uid, string $url, array $parameters = []): stdClass
1309 {
1310         $url = 'https://api.tumblr.com/v2/' . $url;
1311
1312         if (!empty($parameters)) {
1313                 $url .= '?' . http_build_query($parameters);
1314         }
1315
1316         $curlResult = DI::httpClient()->get($url, HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . tumblr_get_token($uid)]]]);
1317         return tumblr_format_result($curlResult);
1318 }
1319
1320 /**
1321  * Perform an OAuth2 POST request
1322  *
1323  * @param integer $uid
1324  * @param string $url
1325  * @param array $parameters
1326  * @return stdClass
1327  */
1328 function tumblr_post(int $uid, string $url, array $parameters): stdClass
1329 {
1330         $url = 'https://api.tumblr.com/v2/' . $url;
1331
1332         $curlResult = DI::httpClient()->post($url, $parameters, ['Authorization' => ['Bearer ' . tumblr_get_token($uid)]]);
1333         return tumblr_format_result($curlResult);
1334 }
1335
1336 /**
1337  * Perform an OAuth2 DELETE request
1338  *
1339  * @param integer $uid
1340  * @param string $url
1341  * @param array $parameters
1342  * @return stdClass
1343  */
1344 function tumblr_delete(int $uid, string $url, array $parameters): stdClass
1345 {
1346         $url = 'https://api.tumblr.com/v2/' . $url;
1347
1348         $opts = [
1349                 HttpClientOptions::HEADERS     => ['Authorization' => ['Bearer ' . tumblr_get_token($uid)]],
1350                 HttpClientOptions::FORM_PARAMS => $parameters
1351         ];
1352
1353         $curlResult = DI::httpClient()->request('delete', $url, $opts);
1354         return tumblr_format_result($curlResult);
1355 }
1356
1357 /**
1358  * Format the get/post result value
1359  *
1360  * @param ICanHandleHttpResponses $curlResult
1361  * @return stdClass
1362  */
1363 function tumblr_format_result(ICanHandleHttpResponses $curlResult): stdClass
1364 {
1365         $result = json_decode($curlResult->getBody());
1366         if (empty($result) || empty($result->meta)) {
1367                 $result               = new stdClass;
1368                 $result->meta         = new stdClass;
1369                 $result->meta->status = 500;
1370                 $result->meta->msg    = '';
1371                 $result->response     = [];
1372                 $result->errors       = [];
1373         }
1374         return $result;
1375 }
1376
1377 /**
1378  * Fetch the OAuth token, update it if needed
1379  *
1380  * @param integer $uid
1381  * @param string $code
1382  * @return string
1383  */
1384 function tumblr_get_token(int $uid, string $code = ''): string
1385 {
1386         $access_token  = DI::pConfig()->get($uid, 'tumblr', 'access_token');
1387         $expires_at    = DI::pConfig()->get($uid, 'tumblr', 'expires_at');
1388         $refresh_token = DI::pConfig()->get($uid, 'tumblr', 'refresh_token');
1389
1390         if (empty($code) && !empty($access_token) && ($expires_at > (time()))) {
1391                 Logger::debug('Got token', ['uid' => $uid, 'expires_at' => date('c', $expires_at)]);
1392                 return $access_token;
1393         }
1394
1395         $consumer_key    = DI::config()->get('tumblr', 'consumer_key');
1396         $consumer_secret = DI::config()->get('tumblr', 'consumer_secret');
1397
1398         $parameters = ['client_id' => $consumer_key, 'client_secret' => $consumer_secret];
1399
1400         if (empty($refresh_token) && empty($code)) {
1401                 $result = tumblr_exchange_token($uid);
1402                 if (empty($result->refresh_token)) {
1403                         Logger::info('Invalid result while exchanging token', ['uid' => $uid]);
1404                         return '';
1405                 }
1406                 $expires_at = time() + $result->expires_in;
1407                 Logger::debug('Updated token from OAuth1 to OAuth2', ['uid' => $uid, 'expires_at' => date('c', $expires_at)]);
1408         } else {
1409                 if (!empty($code)) {
1410                         $parameters['code']       = $code;
1411                         $parameters['grant_type'] = 'authorization_code';
1412                 } else {
1413                         $parameters['refresh_token'] = $refresh_token;
1414                         $parameters['grant_type']    = 'refresh_token';
1415                 }
1416
1417                 $curlResult = DI::httpClient()->post('https://api.tumblr.com/v2/oauth2/token', $parameters);
1418                 if (!$curlResult->isSuccess()) {
1419                         Logger::info('Error fetching token', ['uid' => $uid, 'code' => $code, 'result' => $curlResult->getBody(), 'parameters' => $parameters]);
1420                         return '';
1421                 }
1422
1423                 $result = json_decode($curlResult->getBody());
1424                 if (empty($result)) {
1425                         Logger::info('Invalid result when updating token', ['uid' => $uid]);
1426                         return '';
1427                 }
1428
1429                 $expires_at = time() + $result->expires_in;
1430                 Logger::debug('Renewed token', ['uid' => $uid, 'expires_at' => date('c', $expires_at)]);
1431         }
1432
1433         DI::pConfig()->set($uid, 'tumblr', 'access_token', $result->access_token);
1434         DI::pConfig()->set($uid, 'tumblr', 'expires_at', $expires_at);
1435         DI::pConfig()->set($uid, 'tumblr', 'refresh_token', $result->refresh_token);
1436
1437         return $result->access_token;
1438 }
1439
1440 /**
1441  * Create an OAuth2 token out of an OAuth1 token
1442  *
1443  * @param int $uid
1444  * @return stdClass
1445  */
1446 function tumblr_exchange_token(int $uid): stdClass
1447 {
1448         $oauth_token        = DI::pConfig()->get($uid, 'tumblr', 'oauth_token');
1449         $oauth_token_secret = DI::pConfig()->get($uid, 'tumblr', 'oauth_token_secret');
1450
1451         $consumer_key    = DI::config()->get('tumblr', 'consumer_key');
1452         $consumer_secret = DI::config()->get('tumblr', 'consumer_secret');
1453
1454         $stack = HandlerStack::create();
1455
1456         $middleware = new Oauth1([
1457                 'consumer_key'    => $consumer_key,
1458                 'consumer_secret' => $consumer_secret,
1459                 'token'           => $oauth_token,
1460                 'token_secret'    => $oauth_token_secret
1461         ]);
1462
1463         $stack->push($middleware);
1464
1465         try {
1466                 $client = new Client([
1467                         'base_uri' => 'https://api.tumblr.com/v2/',
1468                         'handler' => $stack
1469                 ]);
1470
1471                 $response = $client->post('oauth2/exchange', ['auth' => 'oauth']);
1472                 return json_decode($response->getBody()->getContents());
1473         } catch (RequestException $exception) {
1474                 Logger::notice('Exchange failed', ['code' => $exception->getCode(), 'message' => $exception->getMessage()]);
1475                 return new stdClass;
1476         }
1477 }