]> git.mxchange.org Git - friendica-addons.git/blob - tumblr/tumblr.php
Tumblr/Bluesky: Store the subscribed feed/tag
[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                                 $post = Post::selectFirst(['uri-id'], ['id' => $id]);
738                                 $stored = Post\Category::storeFileByURIId($post['uri-id'], $uid, Post\Category::SUBCRIPTION, $tag);
739                                 Logger::debug('Stored tag subscription for user', ['uri-id' => $post['uri-id'], 'uid' => $uid, 'tag' => $tag, 'stored' => $stored]);
740                         }
741                 }
742         }
743 }
744
745 /**
746  * Fetch the dashboard (timeline) for the given user
747  *
748  * @param integer $uid
749  * @return void
750  */
751 function tumblr_fetch_dashboard(int $uid)
752 {
753         $parameters = ['reblog_info' => false, 'notes_info' => false, 'npf' => false];
754
755         $last = DI::pConfig()->get($uid, 'tumblr', 'last_id');
756         if (!empty($last)) {
757                 $parameters['since_id'] = $last;
758         }
759
760         $dashboard = tumblr_get($uid, 'user/dashboard', $parameters);
761         if ($dashboard->meta->status > 399) {
762                 Logger::notice('Error fetching dashboard', ['meta' => $dashboard->meta, 'response' => $dashboard->response, 'errors' => $dashboard->errors]);
763                 return [];
764         }
765
766         if (empty($dashboard->response->posts)) {
767                 return;
768         }
769
770         foreach (array_reverse($dashboard->response->posts) as $post) {
771                 if ($post->id > $last) {
772                         $last = $post->id;
773                 }
774
775                 Logger::debug('Importing post', ['uid' => $uid, 'created' => date(DateTimeFormat::MYSQL, $post->timestamp), 'id' => $post->id_string]);
776
777                 tumblr_process_post($post, $uid, Item::PR_NONE);
778
779                 DI::pConfig()->set($uid, 'tumblr', 'last_id', $last);
780         }
781 }
782
783 function tumblr_process_post(stdClass $post, int $uid, int $post_reason): int
784 {
785         $uri = 'tumblr::' . $post->id_string . ':' . $post->reblog_key;
786
787         if (Post::exists(['uri' => $uri, 'uid' => $uid]) || ($post->blog->uuid == tumblr_get_page($uid))) {
788                 return 0;
789         }
790
791         $item = tumblr_get_header($post, $uri, $uid);
792
793         $item = tumblr_get_content($item, $post);
794
795         $item['post-reason'] = $post_reason;
796
797         if (!empty($post->followed)) {
798                 $item['post-reason'] = Item::PR_FOLLOWER;
799         }
800
801         $id = item::insert($item);
802
803         if ($id) {
804                 $stored = Post::selectFirst(['uri-id'], ['id' => $id]);
805
806                 if (!empty($post->tags)) {
807                         foreach ($post->tags as $tag) {
808                                 Tag::store($stored['uri-id'], Tag::HASHTAG, $tag);
809                         }
810                 }
811         }
812         return $id;
813 }
814
815 /**
816  * Sets the initial data for the item array
817  *
818  * @param stdClass $post
819  * @param string $uri
820  * @param integer $uid
821  * @return array
822  */
823 function tumblr_get_header(stdClass $post, string $uri, int $uid): array
824 {
825         $contact = tumblr_get_contact($post->blog, $uid);
826         $item = [
827                 'network'       => Protocol::TUMBLR,
828                 'uid'           => $uid,
829                 'wall'          => false,
830                 'uri'           => $uri,
831                 'private'       => Item::UNLISTED,
832                 'verb'          => Activity::POST,
833                 'contact-id'    => $contact['id'],
834                 'author-name'   => $contact['name'],
835                 'author-link'   => $contact['url'],
836                 'author-avatar' => $contact['avatar'],
837                 'plink'         => $post->post_url,
838                 'created'       => date(DateTimeFormat::MYSQL, $post->timestamp)
839         ];
840
841         $item['owner-name']   = $item['author-name'];
842         $item['owner-link']   = $item['author-link'];
843         $item['owner-avatar'] = $item['author-avatar'];
844
845         return $item;
846 }
847
848 /**
849  * Set the body according the given content type
850  *
851  * @param array $item
852  * @param stdClass $post
853  * @return array
854  */
855 function tumblr_get_content(array $item, stdClass $post): array
856 {
857         switch ($post->type) {
858                 case 'text':
859                         $item['title'] = $post->title;
860                         $item['body'] = HTML::toBBCode(tumblr_add_npf_data($post->body, $post->post_url));
861                         break;
862
863                 case 'quote':
864                         if (empty($post->text)) {
865                                 $body = HTML::toBBCode($post->text) . "\n";
866                         } else {
867                                 $body = '';
868                         }
869                         if (!empty($post->source_title) && !empty($post->source_url)) {
870                                 $body .= '[url=' . $post->source_url . ']' . $post->source_title . "[/url]:\n";
871                         } elseif (!empty($post->source_title)) {
872                                 $body .= $post->source_title . ":\n";
873                         }
874                         $body .= '[quote]' . HTML::toBBCode($post->source) . '[/quote]';
875                         $item['body'] = $body;
876                         break;
877
878                 case 'link':
879                         $item['body'] = HTML::toBBCode($post->description) . "\n" . PageInfo::getFooterFromUrl($post->url);
880                         break;
881
882                 case 'answer':
883                         if (!empty($post->asking_name) && !empty($post->asking_url)) {
884                                 $body = '[url=' . $post->asking_url . ']' . $post->asking_name . "[/url]:\n";
885                         } elseif (!empty($post->asking_name)) {
886                                 $body = $post->asking_name . ":\n";
887                         } else {
888                                 $body = '';
889                         }
890                         $body .= '[quote]' . HTML::toBBCode($post->question) . "[/quote]\n" . HTML::toBBCode($post->answer);
891                         $item['body'] = $body;
892                         break;
893
894                 case 'video':
895                         $item['body'] = HTML::toBBCode($post->caption);
896                         if (!empty($post->video_url)) {
897                                 $item['body'] .= "\n[video]" . $post->video_url . "[/video]\n";
898                         } elseif (!empty($post->thumbnail_url)) {
899                                 $item['body'] .= "\n[url=" . $post->permalink_url . "][img]" . $post->thumbnail_url . "[/img][/url]\n";
900                         } elseif (!empty($post->permalink_url)) {
901                                 $item['body'] .= "\n[url]" . $post->permalink_url . "[/url]\n";
902                         } elseif (!empty($post->source_url) && !empty($post->source_title)) {
903                                 $item['body'] .= "\n[url=" . $post->source_url . "]" . $post->source_title . "[/url]\n";
904                         } elseif (!empty($post->source_url)) {
905                                 $item['body'] .= "\n[url]" . $post->source_url . "[/url]\n";
906                         }
907                         break;
908
909                 case 'audio':
910                         $item['body'] = HTML::toBBCode($post->caption);
911                         if (!empty($post->source_url) && !empty($post->source_title)) {
912                                 $item['body'] .= "\n[url=" . $post->source_url . "]" . $post->source_title . "[/url]\n";
913                         } elseif (!empty($post->source_url)) {
914                                 $item['body'] .= "\n[url]" . $post->source_url . "[/url]\n";
915                         }
916                         break;
917
918                 case 'photo':
919                         $item['body'] = HTML::toBBCode($post->caption);
920                         foreach ($post->photos as $photo) {
921                                 if (!empty($photo->original_size)) {
922                                         $item['body'] .= "\n[img]" . $photo->original_size->url . "[/img]";
923                                 } elseif (!empty($photo->alt_sizes)) {
924                                         $item['body'] .= "\n[img]" . $photo->alt_sizes[0]->url . "[/img]";
925                                 }
926                         }
927                         break;
928
929                 case 'chat':
930                         $item['title'] = $post->title;
931                         $item['body']  = "\n[ul]";
932                         foreach ($post->dialogue as $line) {
933                                 $item['body'] .= "\n[li]" . $line->label . " " . $line->phrase . "[/li]";
934                         }
935                         $item['body'] .= "[/ul]\n";
936                         break;
937         }
938         return $item;
939 }
940
941 function tumblr_add_npf_data(string $html, string $plink): string
942 {
943         $doc = new DOMDocument();
944
945         $doc->formatOutput = true;
946         @$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
947         $xpath = new DomXPath($doc);
948         $list = $xpath->query('//p[@class="npf_link"]');
949         foreach ($list as $node) {
950                 $data = tumblr_get_npf_data($node);
951                 if (empty($data)) {
952                         continue;
953                 }
954
955                 tumblr_replace_with_npf($doc, $node, tumblr_get_type_replacement($data, $plink));
956         }
957
958         $list = $xpath->query('//div[@data-npf]');
959         foreach ($list as $node) {
960                 $data = tumblr_get_npf_data($node);
961                 if (empty($data)) {
962                         continue;
963                 }
964
965                 tumblr_replace_with_npf($doc, $node, tumblr_get_type_replacement($data, $plink));
966         }
967
968         $list = $xpath->query('//figure[@data-provider="youtube"]');
969         foreach ($list as $node) {
970                 $attributes = tumblr_get_attributes($node);
971                 if (empty($attributes['data-url'])) {
972                         continue;
973                 }
974                 tumblr_replace_with_npf($doc, $node, '[youtube]' . $attributes['data-url'] . '[/youtube]');
975         }
976
977         $list = $xpath->query('//figure[@data-npf]');
978         foreach ($list as $node) {
979                 $data = tumblr_get_npf_data($node);
980                 if (empty($data)) {
981                         continue;
982                 }
983                 tumblr_replace_with_npf($doc, $node, tumblr_get_type_replacement($data, $plink));
984         }
985
986         return $doc->saveHTML();
987 }
988
989 function tumblr_replace_with_npf(DOMDocument $doc, DOMNode $node, string $replacement)
990 {
991         if (empty($replacement)) {
992                 return;
993         }
994         $replace = $doc->createTextNode($replacement);
995         $node->parentNode->insertBefore($replace, $node);
996         $node->parentNode->removeChild($node);
997 }
998
999 function tumblr_get_npf_data(DOMNode $node): array
1000 {
1001         $attributes = tumblr_get_attributes($node);
1002         if (empty($attributes['data-npf'])) {
1003                 return [];
1004         }
1005
1006         return json_decode($attributes['data-npf'], true);
1007 }
1008
1009 function tumblr_get_attributes($node): array
1010 {
1011         if (empty($node->attributes)) {
1012                 return [];
1013         }
1014
1015         $attributes = [];
1016         foreach ($node->attributes as $key => $attribute) {
1017                 $attributes[$key] = trim($attribute->value);
1018         }
1019         return $attributes;
1020 }
1021
1022 function tumblr_get_type_replacement(array $data, string $plink): string
1023 {
1024         switch ($data['type']) {
1025                 case 'poll':
1026                         $body = '[p][url=' . $plink . ']' . $data['question'] . '[/url][/p][ul]';
1027                         foreach ($data['answers'] as $answer) {
1028                                 $body .= '[li]' . $answer['answer_text'] . '[/li]';
1029                         }
1030                         $body .= '[/ul]';
1031                         break;
1032
1033                 case 'link':
1034                         $body = PageInfo::getFooterFromUrl(str_replace('https://href.li/?', '', $data['url']));
1035                         break;
1036
1037                 case 'video':
1038                         if (!empty($data['url']) && ($data['provider'] == 'tumblr')) {
1039                                 $body = '[video]' . $data['url'] . '[/video]';
1040                                 break;
1041                         }
1042
1043                 default:
1044                         Logger::notice('Unknown type', ['type' => $data['type'], 'data' => $data, 'plink' => $plink]);
1045                         $body = '';
1046         }
1047
1048         return $body;
1049 }
1050
1051 /**
1052  * Get a contact array for the given blog
1053  *
1054  * @param stdClass $blog
1055  * @param integer $uid
1056  * @return array
1057  */
1058 function tumblr_get_contact(stdClass $blog, int $uid): array
1059 {
1060         $condition = ['network' => Protocol::TUMBLR, 'uid' => 0, 'poll' => 'tumblr::' . $blog->uuid];
1061         $contact = Contact::selectFirst(['id', 'updated'], $condition);
1062
1063         $update = empty($contact) || $contact['updated'] < DateTimeFormat::utc('now -24 hours');
1064
1065         $public_fields = $fields = tumblr_get_contact_fields($blog, $uid, $update);
1066
1067         $avatar = $fields['avatar'] ?? '';
1068         unset($fields['avatar']);
1069         unset($public_fields['avatar']);
1070
1071         $public_fields['uid'] = 0;
1072         $public_fields['rel'] = Contact::NOTHING;
1073
1074         if (empty($contact)) {
1075                 $cid = Contact::insert($public_fields);
1076         } else {
1077                 $cid = $contact['id'];
1078                 Contact::update($public_fields, ['id' => $cid], true);
1079         }
1080
1081         if ($uid != 0) {
1082                 $condition = ['network' => Protocol::TUMBLR, 'uid' => $uid, 'poll' => 'tumblr::' . $blog->uuid];
1083
1084                 $contact = Contact::selectFirst(['id', 'rel', 'uid'], $condition);
1085                 if (!isset($fields['rel']) && isset($contact['rel'])) {
1086                         $fields['rel'] = $contact['rel'];
1087                 } elseif (!isset($fields['rel'])) {
1088                         $fields['rel'] = Contact::NOTHING;
1089                 }
1090         }
1091
1092         if (($uid != 0) && ($fields['rel'] != Contact::NOTHING)) {
1093                 if (empty($contact)) {
1094                         $cid = Contact::insert($fields);
1095                 } else {
1096                         $cid = $contact['id'];
1097                         Contact::update($fields, ['id' => $cid], true);
1098                 }
1099                 Logger::debug('Get user contact', ['id' => $cid, 'uid' => $uid, 'update' => $update]);
1100         } else {
1101                 Logger::debug('Get public contact', ['id' => $cid, 'uid' => $uid, 'update' => $update]);
1102         }
1103
1104         if (!empty($avatar)) {
1105                 Contact::updateAvatar($cid, $avatar);
1106         }
1107
1108         return Contact::getById($cid);
1109 }
1110
1111 function tumblr_get_contact_fields(stdClass $blog, int $uid, bool $update): array
1112 {
1113         $baseurl = 'https://tumblr.com';
1114         $url     = $baseurl . '/' . $blog->name;
1115
1116         $fields = [
1117                 'uid'      => $uid,
1118                 'network'  => Protocol::TUMBLR,
1119                 'poll'     => 'tumblr::' . $blog->uuid,
1120                 'baseurl'  => $baseurl,
1121                 'priority' => 1,
1122                 'writable' => true,
1123                 'blocked'  => false,
1124                 'readonly' => false,
1125                 'pending'  => false,
1126                 'url'      => $url,
1127                 'nurl'     => Strings::normaliseLink($url),
1128                 'alias'    => $blog->url,
1129                 'name'     => $blog->title ?: $blog->name,
1130                 'nick'     => $blog->name,
1131                 'addr'     => $blog->name . '@tumblr.com',
1132                 'about'    => HTML::toBBCode($blog->description),
1133                 'updated'  => date(DateTimeFormat::MYSQL, $blog->updated)
1134         ];
1135
1136         if (!$update) {
1137                 Logger::debug('Got contact fields', ['uid' => $uid, 'url' => $fields['url']]);
1138                 return $fields;
1139         }
1140
1141         $info = tumblr_get($uid, 'blog/' . $blog->uuid . '/info');
1142         if ($info->meta->status > 399) {
1143                 Logger::notice('Error fetching blog info', ['meta' => $info->meta, 'response' => $info->response, 'errors' => $info->errors]);
1144                 return $fields;
1145         }
1146
1147         $avatar = $info->response->blog->avatar;
1148         if (!empty($avatar)) {
1149                 $fields['avatar'] = $avatar[0]->url;
1150         }
1151
1152         if ($info->response->blog->followed && $info->response->blog->subscribed) {
1153                 $fields['rel'] = Contact::FRIEND;
1154         } elseif ($info->response->blog->followed && !$info->response->blog->subscribed) {
1155                 $fields['rel'] = Contact::SHARING;
1156         } elseif (!$info->response->blog->followed && $info->response->blog->subscribed) {
1157                 $fields['rel'] = Contact::FOLLOWER;
1158         } else {
1159                 $fields['rel'] = Contact::NOTHING;
1160         }
1161
1162         $fields['header'] = $info->response->blog->theme->header_image_focused;
1163
1164         Logger::debug('Got updated contact fields', ['uid' => $uid, 'url' => $fields['url']]);
1165         return $fields;
1166 }
1167
1168 /**
1169  * Get the default page for posting. Detects the value if not provided or has got a bad value.
1170  *
1171  * @param integer $uid
1172  * @param array $blogs
1173  * @return string
1174  */
1175 function tumblr_get_page(int $uid, array $blogs = []): string
1176 {
1177         $page = DI::pConfig()->get($uid, 'tumblr', 'page');
1178
1179         if (!empty($page) && (strpos($page, '/') === false)) {
1180                 return $page;
1181         }
1182
1183         if (empty($blogs)) {
1184                 $blogs = tumblr_get_blogs($uid);
1185         }
1186
1187         if (!empty($blogs)) {
1188                 $page = array_key_first($blogs);
1189                 DI::pConfig()->set($uid, 'tumblr', 'page', $page);
1190                 return $page;
1191         }
1192
1193         return '';
1194 }
1195
1196 /**
1197  * Get an array of blogs for the given user
1198  *
1199  * @param integer $uid
1200  * @return array
1201  */
1202 function tumblr_get_blogs(int $uid): array
1203 {
1204         $userinfo = tumblr_get($uid, 'user/info');
1205         if ($userinfo->meta->status > 299) {
1206                 Logger::notice('Error fetching blogs', ['meta' => $userinfo->meta, 'response' => $userinfo->response, 'errors' => $userinfo->errors]);
1207                 return [];
1208         }
1209
1210         $blogs = [];
1211         foreach ($userinfo->response->user->blogs as $blog) {
1212                 $blogs[$blog->uuid] = $blog->name;
1213         }
1214         return $blogs;
1215 }
1216
1217 function tumblr_enabled_for_user(int $uid)
1218 {
1219         return !empty($uid) && !empty(DI::pConfig()->get($uid, 'tumblr', 'access_token')) &&
1220                 !empty(DI::pConfig()->get($uid, 'tumblr', 'refresh_token')) &&
1221                 !empty(DI::config()->get('tumblr', 'consumer_key')) &&
1222                 !empty(DI::config()->get('tumblr', 'consumer_secret'));
1223 }
1224
1225 /**
1226  * Get a contact array from a Tumblr url
1227  *
1228  * @param string $url
1229  * @return array|null
1230  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1231  */
1232 function tumblr_get_contact_by_url(string $url): ?array
1233 {
1234         $consumer_key = DI::config()->get('tumblr', 'consumer_key');
1235         if (empty($consumer_key)) {
1236                 return null;
1237         }
1238
1239         if (!preg_match('#^https?://tumblr.com/(.+)#', $url, $matches) && !preg_match('#^https?://www\.tumblr.com/(.+)#', $url, $matches) && !preg_match('#^https?://(.+)\.tumblr.com#', $url, $matches)) {
1240                 try {
1241                         $curlResult = DI::httpClient()->get($url);
1242                 } catch (\Exception $e) {
1243                         return null;
1244                 }
1245                 $html = $curlResult->getBody();
1246                 if (empty($html)) {
1247                         return null;
1248                 }
1249                 $doc = new DOMDocument();
1250                 @$doc->loadHTML($html);
1251                 $xpath = new DomXPath($doc);
1252                 $body = $xpath->query('body');
1253                 $attributes = tumblr_get_attributes($body->item(0));
1254                 $blog = $attributes['data-urlencoded-name'] ?? '';
1255         } else {
1256                 $blogs = explode('/', $matches[1]);
1257                 $blog = $blogs[0] ?? '';
1258         }
1259
1260         if (empty($blog)) {
1261                 return null;
1262         }
1263
1264         Logger::debug('Update Tumblr blog data', ['url' => $url]);
1265
1266         $curlResult = DI::httpClient()->get('https://api.tumblr.com/v2/blog/' . $blog . '/info?api_key=' . $consumer_key);
1267         $body = $curlResult->getBody();
1268         $data = json_decode($body);
1269         if (empty($data)) {
1270                 return null;
1271         }
1272
1273         if (is_array($data->response->blog) || empty($data->response->blog)) {
1274                 Logger::warning('Unexpected blog format', ['blog' => $blog, 'data' => $data]);
1275                 return null;
1276         }
1277
1278         $baseurl = 'https://tumblr.com';
1279         $url     = $baseurl . '/' . $data->response->blog->name;
1280
1281         return [
1282                 'url'      => $url,
1283                 'nurl'     => Strings::normaliseLink($url),
1284                 'addr'     => $data->response->blog->name . '@tumblr.com',
1285                 'alias'    => $data->response->blog->url,
1286                 'batch'    => '',
1287                 'notify'   => '',
1288                 'poll'     => 'tumblr::' . $data->response->blog->uuid,
1289                 'poco'     => '',
1290                 'name'     => $data->response->blog->title ?: $data->response->blog->name,
1291                 'nick'     => $data->response->blog->name,
1292                 'network'  => Protocol::TUMBLR,
1293                 'baseurl'  => $baseurl,
1294                 'pubkey'   => '',
1295                 'priority' => 0,
1296                 'guid'     => $data->response->blog->uuid,
1297                 'about'    => HTML::toBBCode($data->response->blog->description),
1298                 'photo'    => $data->response->blog->avatar[0]->url,
1299                 'header'   => $data->response->blog->theme->header_image_focused,
1300         ];
1301 }
1302
1303 /**
1304  * Perform an OAuth2 GET request
1305  *
1306  * @param integer $uid
1307  * @param string $url
1308  * @param array $parameters
1309  * @return stdClass
1310  */
1311 function tumblr_get(int $uid, string $url, array $parameters = []): stdClass
1312 {
1313         $url = 'https://api.tumblr.com/v2/' . $url;
1314
1315         if (!empty($parameters)) {
1316                 $url .= '?' . http_build_query($parameters);
1317         }
1318
1319         $curlResult = DI::httpClient()->get($url, HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . tumblr_get_token($uid)]]]);
1320         return tumblr_format_result($curlResult);
1321 }
1322
1323 /**
1324  * Perform an OAuth2 POST request
1325  *
1326  * @param integer $uid
1327  * @param string $url
1328  * @param array $parameters
1329  * @return stdClass
1330  */
1331 function tumblr_post(int $uid, string $url, array $parameters): stdClass
1332 {
1333         $url = 'https://api.tumblr.com/v2/' . $url;
1334
1335         $curlResult = DI::httpClient()->post($url, $parameters, ['Authorization' => ['Bearer ' . tumblr_get_token($uid)]]);
1336         return tumblr_format_result($curlResult);
1337 }
1338
1339 /**
1340  * Perform an OAuth2 DELETE request
1341  *
1342  * @param integer $uid
1343  * @param string $url
1344  * @param array $parameters
1345  * @return stdClass
1346  */
1347 function tumblr_delete(int $uid, string $url, array $parameters): stdClass
1348 {
1349         $url = 'https://api.tumblr.com/v2/' . $url;
1350
1351         $opts = [
1352                 HttpClientOptions::HEADERS     => ['Authorization' => ['Bearer ' . tumblr_get_token($uid)]],
1353                 HttpClientOptions::FORM_PARAMS => $parameters
1354         ];
1355
1356         $curlResult = DI::httpClient()->request('delete', $url, $opts);
1357         return tumblr_format_result($curlResult);
1358 }
1359
1360 /**
1361  * Format the get/post result value
1362  *
1363  * @param ICanHandleHttpResponses $curlResult
1364  * @return stdClass
1365  */
1366 function tumblr_format_result(ICanHandleHttpResponses $curlResult): stdClass
1367 {
1368         $result = json_decode($curlResult->getBody());
1369         if (empty($result) || empty($result->meta)) {
1370                 $result               = new stdClass;
1371                 $result->meta         = new stdClass;
1372                 $result->meta->status = 500;
1373                 $result->meta->msg    = '';
1374                 $result->response     = [];
1375                 $result->errors       = [];
1376         }
1377         return $result;
1378 }
1379
1380 /**
1381  * Fetch the OAuth token, update it if needed
1382  *
1383  * @param integer $uid
1384  * @param string $code
1385  * @return string
1386  */
1387 function tumblr_get_token(int $uid, string $code = ''): string
1388 {
1389         $access_token  = DI::pConfig()->get($uid, 'tumblr', 'access_token');
1390         $expires_at    = DI::pConfig()->get($uid, 'tumblr', 'expires_at');
1391         $refresh_token = DI::pConfig()->get($uid, 'tumblr', 'refresh_token');
1392
1393         if (empty($code) && !empty($access_token) && ($expires_at > (time()))) {
1394                 Logger::debug('Got token', ['uid' => $uid, 'expires_at' => date('c', $expires_at)]);
1395                 return $access_token;
1396         }
1397
1398         $consumer_key    = DI::config()->get('tumblr', 'consumer_key');
1399         $consumer_secret = DI::config()->get('tumblr', 'consumer_secret');
1400
1401         $parameters = ['client_id' => $consumer_key, 'client_secret' => $consumer_secret];
1402
1403         if (empty($refresh_token) && empty($code)) {
1404                 $result = tumblr_exchange_token($uid);
1405                 if (empty($result->refresh_token)) {
1406                         Logger::info('Invalid result while exchanging token', ['uid' => $uid]);
1407                         return '';
1408                 }
1409                 $expires_at = time() + $result->expires_in;
1410                 Logger::debug('Updated token from OAuth1 to OAuth2', ['uid' => $uid, 'expires_at' => date('c', $expires_at)]);
1411         } else {
1412                 if (!empty($code)) {
1413                         $parameters['code']       = $code;
1414                         $parameters['grant_type'] = 'authorization_code';
1415                 } else {
1416                         $parameters['refresh_token'] = $refresh_token;
1417                         $parameters['grant_type']    = 'refresh_token';
1418                 }
1419
1420                 $curlResult = DI::httpClient()->post('https://api.tumblr.com/v2/oauth2/token', $parameters);
1421                 if (!$curlResult->isSuccess()) {
1422                         Logger::info('Error fetching token', ['uid' => $uid, 'code' => $code, 'result' => $curlResult->getBody(), 'parameters' => $parameters]);
1423                         return '';
1424                 }
1425
1426                 $result = json_decode($curlResult->getBody());
1427                 if (empty($result)) {
1428                         Logger::info('Invalid result when updating token', ['uid' => $uid]);
1429                         return '';
1430                 }
1431
1432                 $expires_at = time() + $result->expires_in;
1433                 Logger::debug('Renewed token', ['uid' => $uid, 'expires_at' => date('c', $expires_at)]);
1434         }
1435
1436         DI::pConfig()->set($uid, 'tumblr', 'access_token', $result->access_token);
1437         DI::pConfig()->set($uid, 'tumblr', 'expires_at', $expires_at);
1438         DI::pConfig()->set($uid, 'tumblr', 'refresh_token', $result->refresh_token);
1439
1440         return $result->access_token;
1441 }
1442
1443 /**
1444  * Create an OAuth2 token out of an OAuth1 token
1445  *
1446  * @param int $uid
1447  * @return stdClass
1448  */
1449 function tumblr_exchange_token(int $uid): stdClass
1450 {
1451         $oauth_token        = DI::pConfig()->get($uid, 'tumblr', 'oauth_token');
1452         $oauth_token_secret = DI::pConfig()->get($uid, 'tumblr', 'oauth_token_secret');
1453
1454         $consumer_key    = DI::config()->get('tumblr', 'consumer_key');
1455         $consumer_secret = DI::config()->get('tumblr', 'consumer_secret');
1456
1457         $stack = HandlerStack::create();
1458
1459         $middleware = new Oauth1([
1460                 'consumer_key'    => $consumer_key,
1461                 'consumer_secret' => $consumer_secret,
1462                 'token'           => $oauth_token,
1463                 'token_secret'    => $oauth_token_secret
1464         ]);
1465
1466         $stack->push($middleware);
1467
1468         try {
1469                 $client = new Client([
1470                         'base_uri' => 'https://api.tumblr.com/v2/',
1471                         'handler' => $stack
1472                 ]);
1473
1474                 $response = $client->post('oauth2/exchange', ['auth' => 'oauth']);
1475                 return json_decode($response->getBody()->getContents());
1476         } catch (RequestException $exception) {
1477                 Logger::notice('Exchange failed', ['code' => $exception->getCode(), 'message' => $exception->getMessage()]);
1478                 return new stdClass;
1479         }
1480 }