3 * Name: Twitter Connector
4 * Description: Bidirectional (posting, relaying and reading) connector for Twitter.
6 * Author: Tobias Diekershoff <https://f.diekershoff.de/profile/tobias>
7 * Author: Michael Vogel <https://pirati.ca/profile/heluecht>
8 * Maintainer: Hypolite Petovan <https://friendica.mrpetovan.com/profile/hypolite>
10 * Copyright (c) 2011-2013 Tobias Diekershoff, Michael Vogel, Hypolite Petovan
11 * All rights reserved.
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions are met:
15 * * Redistributions of source code must retain the above copyright notice,
16 * this list of conditions and the following disclaimer.
17 * * Redistributions in binary form must reproduce the above
18 * * copyright notice, this list of conditions and the following disclaimer in
19 * the documentation and/or other materials provided with the distribution.
20 * * Neither the name of the <organization> nor the names of its contributors
21 * may be used to endorse or promote products derived from this software
22 * without specific prior written permission.
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
28 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
29 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
30 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
32 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
33 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36 /* Twitter Addon for Friendica
38 * Author: Tobias Diekershoff
39 * tobias.diekershoff@gmx.net
41 * License:3-clause BSD license
44 * To use this addon you need a OAuth Consumer key pair (key & secret)
45 * you can get it from Twitter at https://twitter.com/apps
47 * Register your Friendica site as "Client" application with "Read & Write" access
48 * we do not need "Twitter as login". When you've registered the app you get the
49 * OAuth Consumer key and secret pair for your application/site.
51 * Add this key pair to your global config/addon.config.php or use the admin panel.
54 * 'consumerkey' => '',
55 * 'consumersecret' => '',
58 * To activate the addon itself add it to the system.addon
59 * setting. After this, your user can configure their Twitter account settings
60 * from "Settings -> Addon Settings".
62 * Requirements: PHP5, curl
65 use Abraham\TwitterOAuth\TwitterOAuth;
66 use Abraham\TwitterOAuth\TwitterOAuthException;
67 use Codebird\Codebird;
69 use Friendica\Content\Text\BBCode;
70 use Friendica\Content\Text\Plaintext;
71 use Friendica\Core\Hook;
72 use Friendica\Core\Logger;
73 use Friendica\Core\Protocol;
74 use Friendica\Core\Renderer;
75 use Friendica\Core\Worker;
76 use Friendica\Database\DBA;
78 use Friendica\Model\Contact;
79 use Friendica\Model\Conversation;
80 use Friendica\Model\Group;
81 use Friendica\Model\Item;
82 use Friendica\Model\ItemURI;
83 use Friendica\Model\Post;
84 use Friendica\Model\Tag;
85 use Friendica\Model\User;
86 use Friendica\Protocol\Activity;
87 use Friendica\Util\ConfigFileLoader;
88 use Friendica\Util\DateTimeFormat;
89 use Friendica\Util\Images;
90 use Friendica\Util\Strings;
92 require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
94 define('TWITTER_DEFAULT_POLL_INTERVAL', 5); // given in minutes
96 function twitter_install()
98 // we need some hooks, for the configuration and for sending tweets
99 Hook::register('load_config' , __FILE__, 'twitter_load_config');
100 Hook::register('connector_settings' , __FILE__, 'twitter_settings');
101 Hook::register('connector_settings_post', __FILE__, 'twitter_settings_post');
102 Hook::register('hook_fork' , __FILE__, 'twitter_hook_fork');
103 Hook::register('post_local' , __FILE__, 'twitter_post_local');
104 Hook::register('notifier_normal' , __FILE__, 'twitter_post_hook');
105 Hook::register('jot_networks' , __FILE__, 'twitter_jot_nets');
106 Hook::register('cron' , __FILE__, 'twitter_cron');
107 Hook::register('support_follow' , __FILE__, 'twitter_support_follow');
108 Hook::register('follow' , __FILE__, 'twitter_follow');
109 Hook::register('unfollow' , __FILE__, 'twitter_unfollow');
110 Hook::register('block' , __FILE__, 'twitter_block');
111 Hook::register('unblock' , __FILE__, 'twitter_unblock');
112 Hook::register('expire' , __FILE__, 'twitter_expire');
113 Hook::register('prepare_body' , __FILE__, 'twitter_prepare_body');
114 Hook::register('check_item_notification', __FILE__, 'twitter_check_item_notification');
115 Hook::register('probe_detect' , __FILE__, 'twitter_probe_detect');
116 Hook::register('parse_link' , __FILE__, 'twitter_parse_link');
117 Logger::info("installed twitter");
122 function twitter_load_config(App $a, ConfigFileLoader $loader)
124 $a->getConfigCache()->load($loader->loadAddonConfig('twitter'));
127 function twitter_check_item_notification(App $a, array &$notification_data)
129 $own_id = DI::pConfig()->get($notification_data['uid'], 'twitter', 'own_id');
131 $own_user = Contact::selectFirst(['url'], ['uid' => $notification_data['uid'], 'alias' => 'twitter::'.$own_id]);
133 $notification_data['profiles'][] = $own_user['url'];
137 function twitter_support_follow(App $a, array &$data)
139 if ($data['protocol'] == Protocol::TWITTER) {
140 $data['result'] = true;
144 function twitter_follow(App $a, array &$contact)
146 Logger::info('Check if contact is twitter contact', ['url' => $contact["url"]]);
148 if (!strstr($contact["url"], "://twitter.com") && !strstr($contact["url"], "@twitter.com")) {
152 // contact seems to be a twitter contact, so continue
153 $nickname = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $contact["url"]);
154 $nickname = str_replace("@twitter.com", "", $nickname);
156 $uid = $a->getLoggedInUserId();
158 twitter_api_contact('friendships/create', ['network' => Protocol::TWITTER, 'nick' => $nickname], $uid);
160 $user = twitter_fetchuser($nickname);
162 $contact_id = twitter_fetch_contact($uid, $user, true);
164 $contact = Contact::getById($contact_id, ['name', 'nick', 'url', 'addr', 'batch', 'notify', 'poll', 'request', 'confirm', 'poco', 'photo', 'priority', 'network', 'alias', 'pubkey']);
166 if (DBA::isResult($contact)) {
167 $contact["contact"] = $contact;
171 function twitter_unfollow(App $a, array &$hook_data)
173 $hook_data['result'] = twitter_api_contact('friendship/destroy', $hook_data['contact'], $hook_data['uid']);
176 function twitter_block(App $a, array &$hook_data)
178 $hook_data['result'] = twitter_api_contact('blocks/create', $hook_data['contact'], $hook_data['uid']);
181 function twitter_unblock(App $a, array &$hook_data)
183 $hook_data['result'] = twitter_api_contact('blocks/destroy', $hook_data['contact'], $hook_data['uid']);
186 function twitter_api_contact(string $apiPath, array $contact, int $uid): ?bool
188 if ($contact['network'] !== Protocol::TWITTER) {
192 $ckey = DI::config()->get('twitter', 'consumerkey');
193 $csecret = DI::config()->get('twitter', 'consumersecret');
194 $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
195 $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
197 // If the addon is not configured (general or for this user) quit here
198 if (empty($ckey) || empty($csecret) || empty($otoken) || empty($osecret)) {
203 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
204 $result = $connection->post($apiPath, ['screen_name' => $contact['nick']]);
205 Logger::info('[twitter] API call successful', ['apiPath' => $apiPath, 'result' => $result]);
207 } catch (Exception $e) {
208 Logger::notice('[twitter] API call failed', ['apiPath' => $apiPath, 'uid' => $uid, 'url' => $contact['url'], 'exception' => $e]);
213 function twitter_jot_nets(App $a, array &$jotnets_fields)
219 if (DI::pConfig()->get(local_user(), 'twitter', 'post')) {
220 $jotnets_fields[] = [
221 'type' => 'checkbox',
224 DI::l10n()->t('Post to Twitter'),
225 DI::pConfig()->get(local_user(), 'twitter', 'post_by_default')
232 function twitter_settings_post(App $a)
237 // don't check twitter settings if twitter submit button is not clicked
238 if (empty($_POST['twitter-disconnect']) && empty($_POST['twitter-submit'])) {
242 if (!empty($_POST['twitter-disconnect'])) {
244 * if the twitter-disconnect checkbox is set, clear the OAuth key/secret pair
245 * from the user configuration
247 DI::pConfig()->delete(local_user(), 'twitter', 'consumerkey');
248 DI::pConfig()->delete(local_user(), 'twitter', 'consumersecret');
249 DI::pConfig()->delete(local_user(), 'twitter', 'oauthtoken');
250 DI::pConfig()->delete(local_user(), 'twitter', 'oauthsecret');
251 DI::pConfig()->delete(local_user(), 'twitter', 'post');
252 DI::pConfig()->delete(local_user(), 'twitter', 'post_by_default');
253 DI::pConfig()->delete(local_user(), 'twitter', 'lastid');
254 DI::pConfig()->delete(local_user(), 'twitter', 'mirror_posts');
255 DI::pConfig()->delete(local_user(), 'twitter', 'import');
256 DI::pConfig()->delete(local_user(), 'twitter', 'create_user');
257 DI::pConfig()->delete(local_user(), 'twitter', 'own_id');
259 if (isset($_POST['twitter-pin'])) {
260 // if the user supplied us with a PIN from Twitter, let the magic of OAuth happen
261 Logger::notice('got a Twitter PIN');
262 $ckey = DI::config()->get('twitter', 'consumerkey');
263 $csecret = DI::config()->get('twitter', 'consumersecret');
264 // the token and secret for which the PIN was generated were hidden in the settings
265 // form as token and token2, we need a new connection to Twitter using these token
266 // and secret to request a Access Token with the PIN
268 if (empty($_POST['twitter-pin'])) {
269 throw new Exception(DI::l10n()->t('You submitted an empty PIN, please Sign In with Twitter again to get a new one.'));
272 $connection = new TwitterOAuth($ckey, $csecret, $_POST['twitter-token'], $_POST['twitter-token2']);
273 $token = $connection->oauth("oauth/access_token", ["oauth_verifier" => $_POST['twitter-pin']]);
274 // ok, now that we have the Access Token, save them in the user config
275 DI::pConfig()->set(local_user(), 'twitter', 'oauthtoken', $token['oauth_token']);
276 DI::pConfig()->set(local_user(), 'twitter', 'oauthsecret', $token['oauth_token_secret']);
277 DI::pConfig()->set(local_user(), 'twitter', 'post', 1);
278 } catch(Exception $e) {
279 notice($e->getMessage());
280 } catch(TwitterOAuthException $e) {
281 notice($e->getMessage());
283 // reload the Addon Settings page, if we don't do it see Bug #42
284 DI::baseUrl()->redirect('settings/connectors');
286 // if no PIN is supplied in the POST variables, the user has changed the setting
287 // to post a tweet for every new __public__ posting to the wall
288 DI::pConfig()->set(local_user(), 'twitter', 'post', intval($_POST['twitter-enable']));
289 DI::pConfig()->set(local_user(), 'twitter', 'post_by_default', intval($_POST['twitter-default']));
290 DI::pConfig()->set(local_user(), 'twitter', 'mirror_posts', intval($_POST['twitter-mirror']));
291 DI::pConfig()->set(local_user(), 'twitter', 'import', intval($_POST['twitter-import']));
292 DI::pConfig()->set(local_user(), 'twitter', 'create_user', intval($_POST['twitter-create_user']));
294 if (!intval($_POST['twitter-mirror'])) {
295 DI::pConfig()->delete(local_user(), 'twitter', 'lastid');
301 function twitter_settings(App $a, &$s)
307 $user = User::getById(local_user());
309 DI::page()['htmlhead'] .= '<link rel="stylesheet" type="text/css" href="' . DI::baseUrl()->get() . '/addon/twitter/twitter.css' . '" media="all" />' . "\r\n";
311 * 1) Check that we have global consumer key & secret
312 * 2) If no OAuthtoken & stuff is present, generate button to get some
313 * 3) Checkbox for "Send public notices (280 chars only)
315 $ckey = DI::config()->get('twitter', 'consumerkey');
316 $csecret = DI::config()->get('twitter', 'consumersecret');
317 $otoken = DI::pConfig()->get(local_user(), 'twitter', 'oauthtoken');
318 $osecret = DI::pConfig()->get(local_user(), 'twitter', 'oauthsecret');
320 $enabled = intval(DI::pConfig()->get(local_user(), 'twitter', 'post'));
321 $defenabled = intval(DI::pConfig()->get(local_user(), 'twitter', 'post_by_default'));
322 $mirrorenabled = intval(DI::pConfig()->get(local_user(), 'twitter', 'mirror_posts'));
323 $importenabled = intval(DI::pConfig()->get(local_user(), 'twitter', 'import'));
324 $create_userenabled = intval(DI::pConfig()->get(local_user(), 'twitter', 'create_user'));
326 $css = (($enabled) ? '' : '-disabled');
328 $s .= '<span id="settings_twitter_inflated" class="settings-block fakelink" style="display: block;" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
329 $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . DI::l10n()->t('Twitter Import/Export/Mirror') . '</h3>';
331 $s .= '<div id="settings_twitter_expanded" class="settings-block" style="display: none;">';
332 $s .= '<span class="fakelink" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
333 $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . DI::l10n()->t('Twitter Import/Export/Mirror') . '</h3>';
336 if ((!$ckey) && (!$csecret)) {
337 /* no global consumer keys
338 * display warning and skip personal config
340 $s .= '<p>' . DI::l10n()->t('No consumer key pair for Twitter found. Please contact your site administrator.') . '</p>';
342 // ok we have a consumer key pair now look into the OAuth stuff
343 if ((!$otoken) && (!$osecret)) {
344 /* the user has not yet connected the account to twitter...
345 * get a temporary OAuth key/secret pair and display a button with
346 * which the user can request a PIN to connect the account to a
347 * account at Twitter.
349 $connection = new TwitterOAuth($ckey, $csecret);
351 $result = $connection->oauth('oauth/request_token', ['oauth_callback' => 'oob']);
352 $s .= '<p>' . DI::l10n()->t('At this Friendica instance the Twitter addon was enabled but you have not yet connected your account to your Twitter account. To do so click the button below to get a PIN from Twitter which you have to copy into the input box below and submit the form. Only your <strong>public</strong> posts will be posted to Twitter.') . '</p>';
353 $s .= '<a href="' . $connection->url('oauth/authorize', ['oauth_token' => $result['oauth_token']]) . '" target="_twitter"><img src="addon/twitter/lighter.png" alt="' . DI::l10n()->t('Log in with Twitter') . '"></a>';
354 $s .= '<div id="twitter-pin-wrapper">';
355 $s .= '<label id="twitter-pin-label" for="twitter-pin">' . DI::l10n()->t('Copy the PIN from Twitter here') . '</label>';
356 $s .= '<input id="twitter-pin" type="text" name="twitter-pin" />';
357 $s .= '<input id="twitter-token" type="hidden" name="twitter-token" value="' . $result['oauth_token'] . '" />';
358 $s .= '<input id="twitter-token2" type="hidden" name="twitter-token2" value="' . $result['oauth_token_secret'] . '" />';
359 $s .= '</div><div class="clear"></div>';
360 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . DI::l10n()->t('Save Settings') . '" /></div>';
361 } catch (TwitterOAuthException $e) {
362 $s .= '<p>' . DI::l10n()->t('An error occured: ') . $e->getMessage() . '</p>';
366 * we have an OAuth key / secret pair for the user
367 * so let's give a chance to disable the postings to Twitter
369 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
371 $details = $connection->get('account/verify_credentials');
373 $field_checkbox = Renderer::getMarkupTemplate('field_checkbox.tpl');
375 if (property_exists($details, 'screen_name') &&
376 property_exists($details, 'description') &&
377 property_exists($details, 'profile_image_url')) {
378 $s .= '<div id="twitter-info" >
379 <p>' . DI::l10n()->t('Currently connected to: ') . '<a href="https://twitter.com/' . $details->screen_name . '" target="_twitter">' . $details->screen_name . '</a>
380 <button type="submit" name="twitter-disconnect" value="1">' . DI::l10n()->t('Disconnect') . '</button>
382 <p id="twitter-info-block">
383 <a href="https://twitter.com/' . $details->screen_name . '" target="_twitter"><img id="twitter-avatar" src="' . $details->profile_image_url . '" /></a>
384 <em>' . $details->description . '</em>
388 $s .= '<div id="twitter-info" >
389 <p>Invalid Twitter info</p>
390 <button type="submit" name="twitter-disconnect" value="1">' . DI::l10n()->t('Disconnect') . '</button>
392 Logger::notice('Invalid twitter info (verify credentials).', ['auth' => TwitterOAuth::class]);
394 $s .= '<div class="clear"></div>';
396 $s .= Renderer::replaceMacros($field_checkbox, [
397 '$field' => ['twitter-enable', DI::l10n()->t('Allow posting to Twitter'), $enabled, DI::l10n()->t('If enabled all your <strong>public</strong> postings can be posted to the associated Twitter account. You can choose to do so by default (here) or for every posting separately in the posting options when writing the entry.')]
399 if ($user['hidewall']) {
400 $s .= '<p>' . DI::l10n()->t('<strong>Note</strong>: Due to your privacy settings (<em>Hide your profile details from unknown viewers?</em>) the link potentially included in public postings relayed to Twitter will lead the visitor to a blank page informing the visitor that the access to your profile has been restricted.') . '</p>';
402 $s .= Renderer::replaceMacros($field_checkbox, [
403 '$field' => ['twitter-default', DI::l10n()->t('Send public postings to Twitter by default'), $defenabled, '']
405 $s .= Renderer::replaceMacros($field_checkbox, [
406 '$field' => ['twitter-mirror', DI::l10n()->t('Mirror all posts from twitter that are no replies'), $mirrorenabled, '']
408 $s .= Renderer::replaceMacros($field_checkbox, [
409 '$field' => ['twitter-import', DI::l10n()->t('Import the remote timeline'), $importenabled, '']
411 $s .= Renderer::replaceMacros($field_checkbox, [
412 '$field' => ['twitter-create_user', DI::l10n()->t('Automatically create contacts'), $create_userenabled, DI::l10n()->t('This will automatically create a contact in Friendica as soon as you receive a message from an existing contact via the Twitter network. If you do not enable this, you need to manually add those Twitter contacts in Friendica from whom you would like to see posts here. However if enabled, you cannot merely remove a twitter contact from the Friendica contact list, as it will recreate this contact when they post again.')]
414 $s .= '<div class="clear"></div>';
415 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . DI::l10n()->t('Save Settings') . '" /></div>';
416 } catch (TwitterOAuthException $e) {
417 $s .= '<p>' . DI::l10n()->t('An error occured: ') . $e->getMessage() . '</p>';
421 $s .= '</div><div class="clear"></div>';
424 function twitter_hook_fork(App $a, array &$b)
426 if ($b['name'] != 'notifier_normal') {
432 // Deleting and editing is not supported by the addon (deleting could, but isn't by now)
433 if ($post['deleted'] || ($post['created'] !== $post['edited'])) {
434 $b['execute'] = false;
438 // if post comes from twitter don't send it back
439 if (($post['extid'] == Protocol::TWITTER) || twitter_get_id($post['extid'])) {
440 $b['execute'] = false;
444 if (substr($post['app'], 0, 7) == 'Twitter') {
445 $b['execute'] = false;
449 if (DI::pConfig()->get($post['uid'], 'twitter', 'import')) {
450 // Don't fork if it isn't a reply to a twitter post
451 if (($post['parent'] != $post['id']) && !Post::exists(['id' => $post['parent'], 'network' => Protocol::TWITTER])) {
452 Logger::notice('No twitter parent found', ['item' => $post['id']]);
453 $b['execute'] = false;
457 // Comments are never exported when we don't import the twitter timeline
458 if (!strstr($post['postopts'], 'twitter') || ($post['parent'] != $post['id']) || $post['private']) {
459 $b['execute'] = false;
465 function twitter_post_local(App $a, array &$b)
471 if (!local_user() || (local_user() != $b['uid'])) {
475 $twitter_post = intval(DI::pConfig()->get(local_user(), 'twitter', 'post'));
476 $twitter_enable = (($twitter_post && !empty($_REQUEST['twitter_enable'])) ? intval($_REQUEST['twitter_enable']) : 0);
478 // if API is used, default to the chosen settings
479 if ($b['api_source'] && intval(DI::pConfig()->get(local_user(), 'twitter', 'post_by_default'))) {
483 if (!$twitter_enable) {
487 if (strlen($b['postopts'])) {
488 $b['postopts'] .= ',';
491 $b['postopts'] .= 'twitter';
494 function twitter_probe_detect(App $a, array &$hookData)
496 // Don't overwrite an existing result
497 if ($hookData['result']) {
501 // Avoid a lookup for the wrong network
502 if (!in_array($hookData['network'], ['', Protocol::TWITTER])) {
506 if (preg_match('=([^@]+)@(?:mobile\.)?twitter\.com$=i', $hookData['uri'], $matches)) {
508 } elseif (preg_match('=^https?://(?:mobile\.)?twitter\.com/(.+)=i', $hookData['uri'], $matches)) {
514 $user = twitter_fetchuser($nick);
517 $hookData['result'] = twitter_user_to_contact($user);
521 function twitter_api_post(string $apiPath, string $pid, int $uid)
527 $ckey = DI::config()->get('twitter', 'consumerkey');
528 $csecret = DI::config()->get('twitter', 'consumersecret');
529 $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
530 $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
532 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
534 $post = ['id' => $pid];
536 Logger::debug('before action', ['action' => $apiPath, 'pid' => $pid, 'data' => $post]);
539 $result = $connection->post($apiPath, $post);
540 if ($connection->getLastHttpCode() != 200) {
541 Logger::warning('[twitter] API call unsuccessful', ['apiPath' => $apiPath, 'post' => $post, 'result' => $result]);
543 } catch (TwitterOAuthException $twitterOAuthException) {
544 Logger::warning('Unable to communicate with twitter', ['apiPath' => $apiPath, 'data' => $post, 'code' => $twitterOAuthException->getCode(), 'exception' => $twitterOAuthException]);
548 Logger::info('after action', ['action' => $apiPath, 'result' => $result]);
553 function twitter_get_id(string $uri)
555 if ((substr($uri, 0, 9) != 'twitter::') || (strlen($uri) <= 9)) {
559 $id = substr($uri, 9);
560 if (!is_numeric($id)) {
567 function twitter_post_hook(App $a, array &$b)
570 if (!DI::pConfig()->get($b["uid"], 'twitter', 'import')
571 && ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))) {
575 $b['body'] = Post\Media::addAttachmentsToBody($b['uri-id'], $b['body']);
579 if ($b['parent'] != $b['id']) {
580 Logger::debug('Got comment', ['item' => $b]);
582 // Looking if its a reply to a twitter post
583 if (!twitter_get_id($b["parent-uri"]) &&
584 !twitter_get_id($b["extid"]) &&
585 !twitter_get_id($b["thr-parent"])) {
586 Logger::info('No twitter post', ['parent' => $b["parent"]]);
590 $condition = ['uri' => $b["thr-parent"], 'uid' => $b["uid"]];
591 $thr_parent = Post::selectFirst(['uri', 'extid', 'author-link', 'author-nick', 'author-network'], $condition);
592 if (!DBA::isResult($thr_parent)) {
593 Logger::warning('No parent found', ['thr-parent' => $b["thr-parent"]]);
597 if ($thr_parent['author-network'] == Protocol::TWITTER) {
598 $nickname = '@[url=' . $thr_parent['author-link'] . ']' . $thr_parent['author-nick'] . '[/url]';
599 $nicknameplain = '@' . $thr_parent['author-nick'];
601 Logger::info('Comparing', ['nickname' => $nickname, 'nicknameplain' => $nicknameplain, 'body' => $b["body"]]);
602 if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) {
603 $b["body"] = $nickname . " " . $b["body"];
607 Logger::debug('Parent found', ['parent' => $thr_parent]);
609 if ($b['private'] || !strstr($b['postopts'], 'twitter')) {
613 // Dont't post if the post doesn't belong to us.
614 // This is a check for forum postings
615 $self = DBA::selectFirst('contact', ['id'], ['uid' => $b['uid'], 'self' => true]);
616 if ($b['contact-id'] != $self['id']) {
622 * @TODO This can't work at the moment:
623 * - Posts created on Friendica and mirrored to Twitter don't have a Twitter ID
624 * - Posts created on Twitter and mirrored on Friendica do not trigger the notifier hook this is part of.
626 //if (($b['verb'] == Activity::POST) && $b['deleted']) {
627 // twitter_api_post('statuses/destroy', twitter_get_id($thr_parent['uri']), $b['uid']);
630 if ($b['verb'] == Activity::LIKE) {
631 Logger::info('Like', ['uid' => $b['uid'], 'id' => twitter_get_id($b["thr-parent"])]);
633 twitter_api_post($b['deleted'] ? 'favorite/destroy' : 'favorite/create', twitter_get_id($b["thr-parent"]), $b["uid"]);
638 if ($b['verb'] == Activity::ANNOUNCE) {
639 Logger::info('Retweet', ['uid' => $b['uid'], 'id' => twitter_get_id($b["thr-parent"])]);
642 * @TODO This can't work at the moment:
643 * - Twitter post reshare removal doesn't seem to trigger the notifier hook this is part of
645 //twitter_api_post('statuses/destroy', twitter_get_id($thr_parent['extid']), $b['uid']);
647 twitter_retweet($b["uid"], twitter_get_id($b["thr-parent"]));
653 if ($b['deleted'] || ($b['created'] !== $b['edited'])) {
657 // if post comes from twitter don't send it back
658 if (($b['extid'] == Protocol::TWITTER) || twitter_get_id($b['extid'])) {
662 if ($b['app'] == "Twitter") {
666 Logger::notice('twitter post invoked', ['id' => $b['id'], 'guid' => $b['guid']]);
668 DI::pConfig()->load($b['uid'], 'twitter');
670 $ckey = DI::config()->get('twitter', 'consumerkey');
671 $csecret = DI::config()->get('twitter', 'consumersecret');
672 $otoken = DI::pConfig()->get($b['uid'], 'twitter', 'oauthtoken');
673 $osecret = DI::pConfig()->get($b['uid'], 'twitter', 'oauthsecret');
675 if ($ckey && $csecret && $otoken && $osecret) {
676 Logger::info('We have customer key and oauth stuff, going to send.');
678 // If it's a repeated message from twitter then do a native retweet and exit
679 if (twitter_is_retweet($a, $b['uid'], $b['body'])) {
683 Codebird::setConsumerKey($ckey, $csecret);
684 $cb = Codebird::getInstance();
685 $cb->setToken($otoken, $osecret);
687 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
689 // Set the timeout for upload to 30 seconds
690 $connection->setTimeouts(10, 30);
694 // Handling non-native reshares
695 $b['body'] = Friendica\Content\Text\BBCode::convertShare(
697 function (array $attributes, array $author_contact, $content, $is_quote_share) {
698 return twitter_convert_share($attributes, $author_contact, $content, $is_quote_share);
702 $b['body'] = twitter_update_mentions($b['body']);
704 $msgarr = Plaintext::getPost($b, $max_char, true, BBCode::TWITTER);
705 Logger::info('Got plaintext', ['id' => $b['id'], 'message' => $msgarr]);
706 $msg = $msgarr["text"];
708 if (($msg == "") && isset($msgarr["title"])) {
709 $msg = Plaintext::shorten($msgarr["title"], $max_char - 50, $b['uid']);
712 // Add the link to the body if the type isn't a photo or there are more than 4 images in the post
713 if (!empty($msgarr['url']) && (strpos($msg, $msgarr['url']) === false) && (($msgarr['type'] != 'photo') || empty($msgarr['images']) || (count($msgarr['images']) > 4))) {
714 $msg .= "\n" . $msgarr['url'];
718 Logger::notice('Empty message', ['id' => $b['id']]);
722 // and now tweet it :-)
725 if (!empty($msgarr['images'])) {
726 Logger::info('Got images', ['id' => $b['id'], 'images' => $msgarr['images']]);
729 foreach ($msgarr['images'] as $image) {
730 if (count($media_ids) == 4) {
734 $img_str = DI::httpClient()->fetch($image['url']);
736 $tempfile = tempnam(get_temppath(), 'cache');
737 file_put_contents($tempfile, $img_str);
739 Logger::info('Uploading', ['id' => $b['id'], 'image' => $image['url']]);
740 $media = $connection->upload('media/upload', ['media' => $tempfile]);
744 if (isset($media->media_id_string)) {
745 $media_ids[] = $media->media_id_string;
747 if (!empty($image['description'])) {
748 $data = ['media_id' => $media->media_id_string,
749 'alt_text' => ['text' => substr($image['description'], 0, 420)]];
750 $ret = $cb->media_metadata_create($data);
751 Logger::info('Metadata create', ['id' => $b['id'], 'data' => $data, 'return' => $ret]);
754 Logger::error('Failed upload', ['id' => $b['id'], 'image' => $image['url'], 'return' => $media]);
755 throw new Exception('Failed upload of ' . $image['url']);
758 $post['media_ids'] = implode(',', $media_ids);
759 if (empty($post['media_ids'])) {
760 unset($post['media_ids']);
762 } catch (Exception $e) {
763 Logger::warning('Exception when trying to send to Twitter', ['id' => $b['id'], 'message' => $e->getMessage()]);
767 $post['status'] = $msg;
770 $post['in_reply_to_status_id'] = twitter_get_id($thr_parent['uri']);
773 $result = $connection->post('statuses/update', $post);
774 Logger::info('twitter_post send', ['id' => $b['id'], 'result' => $result]);
776 if (!empty($result->source)) {
777 DI::config()->set("twitter", "application_name", strip_tags($result->source));
780 if (!empty($result->errors)) {
781 Logger::error('Send to Twitter failed', ['id' => $b['id'], 'error' => $result->errors]);
783 } elseif ($thr_parent) {
784 Logger::notice('Post send, updating extid', ['id' => $b['id'], 'extid' => $result->id_str]);
785 Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $b['id']]);
790 function twitter_addon_admin_post(App $a)
792 $consumerkey = !empty($_POST['consumerkey']) ? Strings::escapeTags(trim($_POST['consumerkey'])) : '';
793 $consumersecret = !empty($_POST['consumersecret']) ? Strings::escapeTags(trim($_POST['consumersecret'])) : '';
794 DI::config()->set('twitter', 'consumerkey', $consumerkey);
795 DI::config()->set('twitter', 'consumersecret', $consumersecret);
798 function twitter_addon_admin(App $a, &$o)
800 $t = Renderer::getMarkupTemplate("admin.tpl", "addon/twitter/");
802 $o = Renderer::replaceMacros($t, [
803 '$submit' => DI::l10n()->t('Save Settings'),
804 // name, label, value, help, [extra values]
805 '$consumerkey' => ['consumerkey', DI::l10n()->t('Consumer key'), DI::config()->get('twitter', 'consumerkey'), ''],
806 '$consumersecret' => ['consumersecret', DI::l10n()->t('Consumer secret'), DI::config()->get('twitter', 'consumersecret'), ''],
810 function twitter_cron(App $a)
812 $last = DI::config()->get('twitter', 'last_poll');
814 $poll_interval = intval(DI::config()->get('twitter', 'poll_interval'));
815 if (!$poll_interval) {
816 $poll_interval = TWITTER_DEFAULT_POLL_INTERVAL;
820 $next = $last + ($poll_interval * 60);
821 if ($next > time()) {
822 Logger::notice('twitter: poll intervall not reached');
826 Logger::notice('twitter: cron_start');
828 $pconfigs = DBA::selectToArray('pconfig', [], ['cat' => 'twitter', 'k' => 'mirror_posts', 'v' => true]);
829 foreach ($pconfigs as $rr) {
830 Logger::notice('Fetching', ['user' => $rr['uid']]);
831 Worker::add(['priority' => PRIORITY_MEDIUM, 'force_priority' => true], "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
834 $abandon_days = intval(DI::config()->get('system', 'account_abandon_days'));
835 if ($abandon_days < 1) {
839 $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400);
841 $pconfigs = DBA::selectToArray('pconfig', [], ['cat' => 'twitter', 'k' => 'import', 'v' => true]);
842 foreach ($pconfigs as $rr) {
843 if ($abandon_days != 0) {
844 if (!DBA::exists('user', ["`uid` = ? AND `login_date` >= ?", $rr['uid'], $abandon_limit])) {
845 Logger::notice('abandoned account: timeline from user will not be imported', ['user' => $rr['uid']]);
850 Logger::notice('importing timeline', ['user' => $rr['uid']]);
851 Worker::add(['priority' => PRIORITY_MEDIUM, 'force_priority' => true], "addon/twitter/twitter_sync.php", 2, (int) $rr['uid']);
854 // check for new contacts once a day
855 $last_contact_check = DI::pConfig()->get($rr['uid'],'pumpio','contact_check');
856 if($last_contact_check)
857 $next_contact_check = $last_contact_check + 86400;
859 $next_contact_check = 0;
861 if($next_contact_check <= time()) {
862 pumpio_getallusers($a, $rr["uid"]);
863 DI::pConfig()->set($rr['uid'],'pumpio','contact_check',time());
868 Logger::notice('twitter: cron_end');
870 DI::config()->set('twitter', 'last_poll', time());
873 function twitter_expire(App $a)
875 $days = DI::config()->get('twitter', 'expire');
881 Logger::notice('Start deleting expired posts');
883 $r = Post::select(['id', 'guid'], ['deleted' => true, 'network' => Protocol::TWITTER]);
884 while ($row = Post::fetch($r)) {
885 Logger::info('[twitter] Delete expired item', ['id' => $row['id'], 'guid' => $row['guid'], 'callstack' => \Friendica\Core\System::callstack()]);
886 Item::markForDeletionById($row['id']);
890 Logger::notice('End deleting expired posts');
892 Logger::notice('Start expiry');
894 $pconfigs = DBA::selectToArray('pconfig', [], ['cat' => 'twitter', 'k' => 'import', 'v' => true]);
895 foreach ($pconfigs as $rr) {
896 Logger::notice('twitter_expire', ['user' => $rr['uid']]);
897 Item::expire($rr['uid'], $days, Protocol::TWITTER, true);
900 Logger::notice('End expiry');
903 function twitter_prepare_body(App $a, array &$b)
905 if ($b["item"]["network"] != Protocol::TWITTER) {
912 $item["plink"] = DI::baseUrl()->get() . "/display/" . $item["guid"];
914 $condition = ['uri' => $item["thr-parent"], 'uid' => local_user()];
915 $orig_post = Post::selectFirst(['author-link'], $condition);
916 if (DBA::isResult($orig_post)) {
917 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
918 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
919 $nicknameplain = "@" . $nicknameplain;
921 if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) {
922 $item["body"] = $nickname . " " . $item["body"];
926 $msgarr = Plaintext::getPost($item, $max_char, true, BBCode::TWITTER);
927 $msg = $msgarr["text"];
929 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
930 $msg .= " " . $msgarr["url"];
933 if (isset($msgarr["image"])) {
934 $msg .= " " . $msgarr["image"];
937 $b['html'] = nl2br(htmlspecialchars($msg));
941 function twitter_statuses_show(string $id, TwitterOAuth $twitterOAuth = null)
943 if ($twitterOAuth === null) {
944 $ckey = DI::config()->get('twitter', 'consumerkey');
945 $csecret = DI::config()->get('twitter', 'consumersecret');
947 if (empty($ckey) || empty($csecret)) {
948 return new stdClass();
951 $twitterOAuth = new TwitterOAuth($ckey, $csecret);
954 $parameters = ['trim_user' => false, 'tweet_mode' => 'extended', 'id' => $id, 'include_ext_alt_text' => true];
956 return $twitterOAuth->get('statuses/show', $parameters);
960 * Parse Twitter status URLs since Twitter removed OEmbed
963 * @param array $b Expected format:
965 * 'url' => [URL to parse],
966 * 'format' => 'json'|'',
967 * 'text' => Output parameter
969 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
971 function twitter_parse_link(App $a, array &$b)
973 // Only handle Twitter status URLs
974 if (!preg_match('#^https?://(?:mobile\.|www\.)?twitter.com/[^/]+/status/(\d+).*#', $b['url'], $matches)) {
978 $status = twitter_statuses_show($matches[1]);
980 if (empty($status->id)) {
984 $item = twitter_createpost($a, 0, $status, [], true, false, true);
986 if ($b['format'] == 'json') {
988 foreach ($status->extended_entities->media ?? [] as $media) {
989 if (!empty($media->media_url_https)) {
991 'src' => $media->media_url_https,
992 'width' => $media->sizes->thumb->w,
993 'height' => $media->sizes->thumb->h,
1001 'url' => $item['plink'],
1002 'title' => DI::l10n()->t('%s on Twitter', $status->user->name),
1003 'text' => BBCode::toPlaintext($item['body'], false),
1004 'images' => $images,
1006 'contentType' => 'attachment',
1010 $b['text'] = BBCode::getShareOpeningTag(
1011 $item['author-name'],
1012 $item['author-link'],
1013 $item['author-avatar'],
1017 $b['text'] .= $item['body'] . '[/share]';
1022 /*********************
1026 *********************/
1030 * @brief Build the item array for the mirrored post
1032 * @param App $a Application class
1033 * @param integer $uid User id
1034 * @param object $post Twitter object with the post
1036 * @return array item data to be posted
1038 function twitter_do_mirrorpost(App $a, $uid, $post)
1040 $datarray['uid'] = $uid;
1041 $datarray['extid'] = 'twitter::' . $post->id;
1042 $datarray['title'] = '';
1044 if (!empty($post->retweeted_status)) {
1045 // We don't support nested shares, so we mustn't show quotes as shares on retweets
1046 $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true, -1);
1048 if (empty($item['body'])) {
1052 $datarray['body'] = "\n" . BBCode::getShareOpeningTag(
1053 $item['author-name'],
1054 $item['author-link'],
1055 $item['author-avatar'],
1060 $datarray['body'] .= $item['body'] . '[/share]';
1062 $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false, -1);
1064 if (empty($item['body'])) {
1068 $datarray['body'] = $item['body'];
1071 $datarray['app'] = $item['app'];
1072 $datarray['verb'] = $item['verb'];
1074 if (isset($item['location'])) {
1075 $datarray['location'] = $item['location'];
1078 if (isset($item['coord'])) {
1079 $datarray['coord'] = $item['coord'];
1085 function twitter_fetchtimeline(App $a, $uid)
1087 $ckey = DI::config()->get('twitter', 'consumerkey');
1088 $csecret = DI::config()->get('twitter', 'consumersecret');
1089 $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
1090 $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
1091 $lastid = DI::pConfig()->get($uid, 'twitter', 'lastid');
1093 $application_name = DI::config()->get('twitter', 'application_name');
1095 if ($application_name == "") {
1096 $application_name = DI::baseUrl()->getHostname();
1099 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1101 // Ensure to have the own contact
1103 twitter_fetch_own_contact($a, $uid);
1104 } catch (TwitterOAuthException $e) {
1105 Logger::warning('Error fetching own contact', ['uid' => $uid, 'message' => $e->getMessage()]);
1109 $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended", "include_ext_alt_text" => true];
1111 $first_time = ($lastid == "");
1113 if ($lastid != "") {
1114 $parameters["since_id"] = $lastid;
1118 $items = $connection->get('statuses/user_timeline', $parameters);
1119 } catch (TwitterOAuthException $e) {
1120 Logger::warning('Error fetching timeline', ['uid' => $uid, 'message' => $e->getMessage()]);
1124 if (!is_array($items)) {
1125 Logger::notice('No items', ['user' => $uid]);
1129 $posts = array_reverse($items);
1131 Logger::notice('Start processing posts', ['from' => $lastid, 'user' => $uid, 'count' => count($posts)]);
1133 if (count($posts)) {
1134 foreach ($posts as $post) {
1135 if ($post->id_str > $lastid) {
1136 $lastid = $post->id_str;
1137 DI::pConfig()->set($uid, 'twitter', 'lastid', $lastid);
1144 if (!stristr($post->source, $application_name)) {
1145 Logger::info('Preparing mirror post', ['twitter-id' => $post->id_str, 'uid' => $uid]);
1147 $mirrorpost = twitter_do_mirrorpost($a, $uid, $post);
1149 if (empty($mirrorpost['body'])) {
1153 Logger::info('Posting mirror post', ['twitter-id' => $post->id_str, 'uid' => $uid]);
1155 Post\Delayed::add($mirrorpost['extid'], $mirrorpost, PRIORITY_MEDIUM, Post\Delayed::UNPREPARED);
1159 DI::pConfig()->set($uid, 'twitter', 'lastid', $lastid);
1160 Logger::log('Last ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
1163 function twitter_fix_avatar($avatar)
1165 $new_avatar = str_replace("_normal.", ".", $avatar);
1167 $info = Images::getInfoFromURLCached($new_avatar);
1169 $new_avatar = $avatar;
1175 function twitter_get_relation($uid, $target, $contact = [])
1177 if (isset($contact['rel'])) {
1178 $relation = $contact['rel'];
1183 $ckey = DI::config()->get('twitter', 'consumerkey');
1184 $csecret = DI::config()->get('twitter', 'consumersecret');
1185 $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
1186 $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
1187 $own_id = DI::pConfig()->get($uid, 'twitter', 'own_id');
1189 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1190 $parameters = ['source_id' => $own_id, 'target_screen_name' => $target];
1193 $status = $connection->get('friendships/show', $parameters);
1194 if ($connection->getLastHttpCode() !== 200) {
1195 throw new Exception($status->errors[0]->message ?? 'HTTP response code ' . $connection->getLastHttpCode(), $status->errors[0]->code ?? $connection->getLastHttpCode());
1198 $following = $status->relationship->source->following;
1199 $followed = $status->relationship->source->followed_by;
1201 if ($following && !$followed) {
1202 $relation = Contact::SHARING;
1203 } elseif (!$following && $followed) {
1204 $relation = Contact::FOLLOWER;
1205 } elseif ($following && $followed) {
1206 $relation = Contact::FRIEND;
1207 } elseif (!$following && !$followed) {
1211 Logger::info('Fetched friendship relation', ['user' => $uid, 'target' => $target, 'relation' => $relation]);
1212 } catch (Throwable $e) {
1213 Logger::warning('Error fetching friendship status', ['uid' => $uid, 'target' => $target, 'message' => $e->getMessage()]);
1223 function twitter_user_to_contact($data)
1225 if (empty($data->id_str)) {
1229 $baseurl = 'https://twitter.com';
1230 $url = $baseurl . '/' . $data->screen_name;
1231 $addr = $data->screen_name . '@twitter.com';
1235 'network' => Protocol::TWITTER,
1236 'alias' => 'twitter::' . $data->id_str,
1237 'baseurl' => $baseurl,
1238 'name' => $data->name,
1239 'nick' => $data->screen_name,
1241 'location' => $data->location,
1242 'about' => $data->description,
1243 'photo' => twitter_fix_avatar($data->profile_image_url_https),
1244 'header' => $data->profile_banner_url ?? $data->profile_background_image_url_https,
1250 function twitter_fetch_contact($uid, $data, $create_user)
1252 $fields = twitter_user_to_contact($data);
1254 if (empty($fields)) {
1258 // photo comes from twitter_user_to_contact but shouldn't be saved directly in the contact row
1259 $avatar = $fields['photo'];
1260 unset($fields['photo']);
1262 // Update the public contact
1263 $pcontact = DBA::selectFirst('contact', ['id'], ['uid' => 0, 'alias' => "twitter::" . $data->id_str]);
1264 if (DBA::isResult($pcontact)) {
1265 $cid = $pcontact['id'];
1267 $cid = Contact::getIdForURL($fields['url'], 0, false, $fields);
1271 Contact::update($fields, ['id' => $cid]);
1272 Contact::updateAvatar($cid, $avatar);
1274 Logger::warning('No contact found', ['fields' => $fields]);
1277 $contact = DBA::selectFirst('contact', [], ['uid' => $uid, 'alias' => "twitter::" . $data->id_str]);
1278 if (!DBA::isResult($contact) && empty($cid)) {
1279 Logger::warning('User contact not found', ['uid' => $uid, 'twitter-id' => $data->id_str]);
1281 } elseif (!$create_user) {
1285 if (!DBA::isResult($contact)) {
1286 $relation = twitter_get_relation($uid, $data->screen_name);
1288 // create contact record
1289 $fields['uid'] = $uid;
1290 $fields['created'] = DateTimeFormat::utcNow();
1291 $fields['nurl'] = Strings::normaliseLink($fields['url']);
1292 $fields['poll'] = 'twitter::' . $data->id_str;
1293 $fields['rel'] = $relation;
1294 $fields['priority'] = 1;
1295 $fields['writable'] = true;
1296 $fields['blocked'] = false;
1297 $fields['readonly'] = false;
1298 $fields['pending'] = false;
1300 if (!Contact::insert($fields)) {
1304 $contact_id = DBA::lastInsertId();
1306 Group::addMember(User::getDefaultGroup($uid), $contact_id);
1308 Contact::updateAvatar($contact_id, $avatar);
1310 if ($contact["readonly"] || $contact["blocked"]) {
1311 Logger::notice('Contact is blocked or readonly.', ['nickname' => $contact["nick"]]);
1315 $contact_id = $contact['id'];
1318 // Update the contact relation once per day
1319 if ($contact['updated'] < DateTimeFormat::utc('now -24 hours')) {
1320 $fields['rel'] = twitter_get_relation($uid, $data->screen_name, $contact);
1324 Contact::updateAvatar($contact['id'], $avatar);
1326 if ($contact['name'] != $data->name) {
1327 $fields['name-date'] = $fields['uri-date'] = DateTimeFormat::utcNow();
1331 if ($contact['nick'] != $data->screen_name) {
1332 $fields['uri-date'] = DateTimeFormat::utcNow();
1336 if (($contact['location'] != $data->location) || ($contact['about'] != $data->description)) {
1341 $fields['updated'] = DateTimeFormat::utcNow();
1342 Contact::update($fields, ['id' => $contact['id']]);
1343 Logger::info('Updated contact', ['id' => $contact['id'], 'nick' => $data->screen_name]);
1351 * @param string $screen_name
1352 * @return stdClass|null
1355 function twitter_fetchuser($screen_name)
1357 $ckey = DI::config()->get('twitter', 'consumerkey');
1358 $csecret = DI::config()->get('twitter', 'consumersecret');
1361 // Fetching user data
1362 $connection = new TwitterOAuth($ckey, $csecret);
1363 $parameters = ['screen_name' => $screen_name];
1364 $user = $connection->get('users/show', $parameters);
1365 } catch (TwitterOAuthException $e) {
1366 Logger::warning('Error fetching user', ['user' => $screen_name, 'message' => $e->getMessage()]);
1370 if (!is_object($user)) {
1378 * Replaces Twitter entities with Friendica-friendly links.
1380 * The Twitter API gives indices for each entity, which allows for fine-grained replacement.
1382 * First, we need to collect everything that needs to be replaced, what we will replace it with, and the start index.
1383 * Then we sort the indices decreasingly, and we replace from the end of the body to the start in order for the next
1384 * index to be correct even after the last replacement.
1386 * @param string $body
1387 * @param stdClass $status
1389 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1391 function twitter_expand_entities($body, stdClass $status)
1394 $contains_urls = false;
1398 $replacementList = [];
1400 foreach ($status->entities->hashtags AS $hashtag) {
1401 $replace = '#[url=' . DI::baseUrl()->get() . '/search?tag=' . $hashtag->text . ']' . $hashtag->text . '[/url]';
1402 $taglist['#' . $hashtag->text] = ['#', $hashtag->text, ''];
1404 $replacementList[$hashtag->indices[0]] = [
1405 'replace' => $replace,
1406 'length' => $hashtag->indices[1] - $hashtag->indices[0],
1410 foreach ($status->entities->user_mentions AS $mention) {
1411 $replace = '@[url=https://twitter.com/' . rawurlencode($mention->screen_name) . ']' . $mention->screen_name . '[/url]';
1412 $taglist['@' . $mention->screen_name] = ['@', $mention->screen_name, 'https://twitter.com/' . rawurlencode($mention->screen_name)];
1414 $replacementList[$mention->indices[0]] = [
1415 'replace' => $replace,
1416 'length' => $mention->indices[1] - $mention->indices[0],
1420 foreach ($status->entities->urls ?? [] as $url) {
1421 $plain = str_replace($url->url, '', $plain);
1423 if ($url->url && $url->expanded_url && $url->display_url) {
1424 // Quote tweet, we just remove the quoted tweet URL from the body, the share block will be added later.
1425 if (!empty($status->quoted_status) && isset($status->quoted_status_id_str)
1426 && substr($url->expanded_url, -strlen($status->quoted_status_id_str)) == $status->quoted_status_id_str
1428 $replacementList[$url->indices[0]] = [
1430 'length' => $url->indices[1] - $url->indices[0],
1435 $contains_urls = true;
1437 $expanded_url = $url->expanded_url;
1439 // Quickfix: Workaround for URL with '[' and ']' in it
1440 if (strpos($expanded_url, '[') || strpos($expanded_url, ']')) {
1441 $expanded_url = $url->url;
1444 $replacementList[$url->indices[0]] = [
1445 'replace' => '[url=' . $expanded_url . ']' . $url->display_url . '[/url]',
1446 'length' => $url->indices[1] - $url->indices[0],
1451 krsort($replacementList);
1453 foreach ($replacementList as $startIndex => $parameters) {
1454 $body = Strings::substringReplace($body, $parameters['replace'], $startIndex, $parameters['length']);
1457 $body = trim($body);
1459 return ['body' => trim($body), 'plain' => trim($plain), 'taglist' => $taglist, 'urls' => $contains_urls];
1463 * Store entity attachments
1465 * @param integer $uriid
1466 * @param object $post Twitter object with the post
1468 function twitter_store_attachments(int $uriid, $post)
1470 if (!empty($post->extended_entities->media)) {
1471 foreach ($post->extended_entities->media AS $medium) {
1472 switch ($medium->type) {
1474 $attachment = ['uri-id' => $uriid, 'type' => Post\Media::IMAGE];
1476 $attachment['url'] = $medium->media_url_https . '?name=large';
1477 $attachment['width'] = $medium->sizes->large->w;
1478 $attachment['height'] = $medium->sizes->large->h;
1480 if ($medium->sizes->small->w != $attachment['width']) {
1481 $attachment['preview'] = $medium->media_url_https . '?name=small';
1482 $attachment['preview-width'] = $medium->sizes->small->w;
1483 $attachment['preview-height'] = $medium->sizes->small->h;
1486 $attachment['name'] = $medium->display_url ?? null;
1487 $attachment['description'] = $medium->ext_alt_text ?? null;
1488 Logger::debug('Photo attachment', ['attachment' => $attachment]);
1489 Post\Media::insert($attachment);
1492 case 'animated_gif':
1493 $attachment = ['uri-id' => $uriid, 'type' => Post\Media::VIDEO];
1494 if (is_array($medium->video_info->variants)) {
1496 // We take the video with the highest bitrate
1497 foreach ($medium->video_info->variants AS $variant) {
1498 if (($variant->content_type == 'video/mp4') && ($variant->bitrate >= $bitrate)) {
1499 $attachment['url'] = $variant->url;
1500 $bitrate = $variant->bitrate;
1505 $attachment['name'] = $medium->display_url ?? null;
1506 $attachment['preview'] = $medium->media_url_https . ':small';
1507 $attachment['preview-width'] = $medium->sizes->small->w;
1508 $attachment['preview-height'] = $medium->sizes->small->h;
1509 $attachment['description'] = $medium->ext_alt_text ?? null;
1510 Logger::debug('Video attachment', ['attachment' => $attachment]);
1511 Post\Media::insert($attachment);
1514 Logger::notice('Unknown media type', ['medium' => $medium]);
1519 if (!empty($post->entities->urls)) {
1520 foreach ($post->entities->urls as $url) {
1521 $attachment = ['uri-id' => $uriid, 'type' => Post\Media::UNKNOWN, 'url' => $url->expanded_url, 'name' => $url->display_url];
1522 Logger::debug('Attached link', ['attachment' => $attachment]);
1523 Post\Media::insert($attachment);
1529 * @brief Fetch media entities and add media links to the body
1531 * @param object $post Twitter object with the post
1532 * @param array $postarray Array of the item that is about to be posted
1533 * @param integer $uriid URI Id used to store tags. -1 = don't store tags for this post.
1535 function twitter_media_entities($post, array &$postarray, int $uriid = -1)
1537 // There are no media entities? So we quit.
1538 if (empty($post->extended_entities->media)) {
1542 // This is a pure media post, first search for all media urls
1544 foreach ($post->extended_entities->media AS $medium) {
1545 if (!isset($media[$medium->url])) {
1546 $media[$medium->url] = '';
1548 switch ($medium->type) {
1550 if (!empty($medium->ext_alt_text)) {
1551 Logger::info('Got text description', ['alt_text' => $medium->ext_alt_text]);
1552 $media[$medium->url] .= "\n[img=" . $medium->media_url_https .']' . $medium->ext_alt_text . '[/img]';
1554 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
1557 $postarray['object-type'] = Activity\ObjectType::IMAGE;
1558 $postarray['post-type'] = Item::PT_IMAGE;
1561 // Currently deactivated, since this causes the video to be display before the content
1562 // We have to figure out a better way for declaring the post type and the display style.
1563 //$postarray['post-type'] = Item::PT_VIDEO;
1564 case 'animated_gif':
1565 if (!empty($medium->ext_alt_text)) {
1566 Logger::info('Got text description', ['alt_text' => $medium->ext_alt_text]);
1567 $media[$medium->url] .= "\n[img=" . $medium->media_url_https .']' . $medium->ext_alt_text . '[/img]';
1569 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
1572 $postarray['object-type'] = Activity\ObjectType::VIDEO;
1573 if (is_array($medium->video_info->variants)) {
1575 // We take the video with the highest bitrate
1576 foreach ($medium->video_info->variants AS $variant) {
1577 if (($variant->content_type == 'video/mp4') && ($variant->bitrate >= $bitrate)) {
1578 $media[$medium->url] = "\n[video]" . $variant->url . '[/video]';
1579 $bitrate = $variant->bitrate;
1588 foreach ($media AS $key => $value) {
1589 $postarray['body'] = str_replace($key, '', $postarray['body']);
1594 // Now we replace the media urls.
1595 foreach ($media AS $key => $value) {
1596 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1601 * Undocumented function
1604 * @param integer $uid User ID
1605 * @param object $post Incoming Twitter post
1606 * @param array $self
1607 * @param bool $create_user Should users be created?
1608 * @param bool $only_existing_contact Only import existing contacts if set to "true"
1609 * @param bool $noquote
1610 * @param integer $uriid URI Id used to store tags. 0 = create a new one; -1 = don't store tags for this post.
1611 * @return array item array
1613 function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $only_existing_contact, $noquote, int $uriid = 0)
1616 $postarray['network'] = Protocol::TWITTER;
1617 $postarray['uid'] = $uid;
1618 $postarray['wall'] = 0;
1619 $postarray['uri'] = "twitter::" . $post->id_str;
1620 $postarray['protocol'] = Conversation::PARCEL_TWITTER;
1621 $postarray['source'] = json_encode($post);
1622 $postarray['direction'] = Conversation::PULL;
1624 if (empty($uriid)) {
1625 $uriid = $postarray['uri-id'] = ItemURI::insert(['uri' => $postarray['uri']]);
1628 // Don't import our own comments
1629 if (Post::exists(['extid' => $postarray['uri'], 'uid' => $uid])) {
1630 Logger::info('Item found', ['extid' => $postarray['uri']]);
1636 if ($post->in_reply_to_status_id_str != "") {
1637 $thr_parent = "twitter::" . $post->in_reply_to_status_id_str;
1639 $item = Post::selectFirst(['uri'], ['uri' => $thr_parent, 'uid' => $uid]);
1640 if (!DBA::isResult($item)) {
1641 $item = Post::selectFirst(['uri'], ['extid' => $thr_parent, 'uid' => $uid]);
1644 if (DBA::isResult($item)) {
1645 $postarray['thr-parent'] = $item['uri'];
1646 $postarray['object-type'] = Activity\ObjectType::COMMENT;
1648 $postarray['object-type'] = Activity\ObjectType::NOTE;
1652 $own_id = DI::pConfig()->get($uid, 'twitter', 'own_id');
1654 if ($post->user->id_str == $own_id) {
1655 $self = Contact::selectFirst(['id', 'name', 'url', 'photo'], ['self' => true, 'uid' => $uid]);
1656 if (DBA::isResult($self)) {
1657 $contactid = $self['id'];
1659 $postarray['owner-name'] = $self['name'];
1660 $postarray['owner-link'] = $self['url'];
1661 $postarray['owner-avatar'] = $self['photo'];
1663 Logger::error('No self contact found', ['uid' => $uid]);
1667 // Don't create accounts of people who just comment something
1668 $create_user = false;
1670 $postarray['object-type'] = Activity\ObjectType::NOTE;
1673 if ($contactid == 0) {
1674 $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
1676 $postarray['owner-name'] = $post->user->name;
1677 $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
1678 $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
1681 if (($contactid == 0) && !$only_existing_contact) {
1682 $contactid = $self['id'];
1683 } elseif ($contactid <= 0) {
1684 Logger::info('Contact ID is zero or less than zero.');
1688 $postarray['contact-id'] = $contactid;
1690 $postarray['verb'] = Activity::POST;
1691 $postarray['author-name'] = $postarray['owner-name'];
1692 $postarray['author-link'] = $postarray['owner-link'];
1693 $postarray['author-avatar'] = $postarray['owner-avatar'];
1694 $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
1695 $postarray['app'] = strip_tags($post->source);
1697 if ($post->user->protected) {
1698 $postarray['private'] = Item::PRIVATE;
1699 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1701 $postarray['private'] = Item::UNLISTED;
1702 $postarray['allow_cid'] = '';
1705 if (!empty($post->full_text)) {
1706 $postarray['body'] = $post->full_text;
1708 $postarray['body'] = $post->text;
1711 // When the post contains links then use the correct object type
1712 if (count($post->entities->urls) > 0) {
1713 $postarray['object-type'] = Activity\ObjectType::BOOKMARK;
1716 // Search for media links
1717 twitter_media_entities($post, $postarray, $uriid);
1719 $converted = twitter_expand_entities($postarray['body'], $post);
1721 // When the post contains external links then images or videos are just "decorations".
1722 if (!empty($converted['urls'])) {
1723 $postarray['post-type'] = Item::PT_NOTE;
1726 $postarray['body'] = $converted['body'];
1727 $postarray['created'] = DateTimeFormat::utc($post->created_at);
1728 $postarray['edited'] = DateTimeFormat::utc($post->created_at);
1731 twitter_store_tags($uriid, $converted['taglist']);
1732 twitter_store_attachments($uriid, $post);
1735 if (!empty($post->place->name)) {
1736 $postarray["location"] = $post->place->name;
1738 if (!empty($post->place->full_name)) {
1739 $postarray["location"] = $post->place->full_name;
1741 if (!empty($post->geo->coordinates)) {
1742 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1744 if (!empty($post->coordinates->coordinates)) {
1745 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1747 if (!empty($post->retweeted_status)) {
1748 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1750 if (empty($retweet['body'])) {
1755 // Store the original tweet
1756 Item::insert($retweet);
1758 // CHange the other post into a reshare activity
1759 $postarray['verb'] = Activity::ANNOUNCE;
1760 $postarray['gravity'] = GRAVITY_ACTIVITY;
1761 $postarray['object-type'] = Activity\ObjectType::NOTE;
1763 $postarray['thr-parent'] = $retweet['uri'];
1765 $retweet['source'] = $postarray['source'];
1766 $retweet['direction'] = $postarray['direction'];
1767 $retweet['private'] = $postarray['private'];
1768 $retweet['allow_cid'] = $postarray['allow_cid'];
1769 $retweet['contact-id'] = $postarray['contact-id'];
1770 $retweet['owner-name'] = $postarray['owner-name'];
1771 $retweet['owner-link'] = $postarray['owner-link'];
1772 $retweet['owner-avatar'] = $postarray['owner-avatar'];
1774 $postarray = $retweet;
1778 if (!empty($post->quoted_status)) {
1780 // To avoid recursive share blocks we just provide the link to avoid removing quote context.
1781 $postarray['body'] .= "\n\nhttps://twitter.com/" . $post->quoted_status->user->screen_name . "/status/" . $post->quoted_status->id_str;
1783 $quoted = twitter_createpost($a, 0, $post->quoted_status, $self, false, false, true);
1784 if (!empty($quoted['body'])) {
1785 Item::insert($quoted);
1786 $post = Post::selectFirst(['guid', 'uri-id'], ['uri' => $quoted['uri'], 'uid' => 0]);
1787 Logger::info('Stored quoted post', ['uid' => $uid, 'uri-id' => $uriid, 'post' => $post]);
1789 $postarray['body'] .= "\n" . BBCode::getShareOpeningTag(
1790 $quoted['author-name'],
1791 $quoted['author-link'],
1792 $quoted['author-avatar'],
1798 $postarray['body'] .= $quoted['body'] . '[/share]';
1800 // Quoted post author is blocked/ignored, so we just provide the link to avoid removing quote context.
1801 $postarray['body'] .= "\n\nhttps://twitter.com/" . $post->quoted_status->user->screen_name . "/status/" . $post->quoted_status->id_str;
1810 * Store tags and mentions
1812 * @param integer $uriid
1813 * @param array $taglist
1815 function twitter_store_tags(int $uriid, array $taglist)
1817 foreach ($taglist as $tag) {
1818 Tag::storeByHash($uriid, $tag[0], $tag[1], $tag[2]);
1822 function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection, array $self)
1824 Logger::info('Fetching parent posts', ['user' => $uid, 'post' => $post->id_str]);
1828 while (!empty($post->in_reply_to_status_id_str)) {
1830 $post = twitter_statuses_show($post->in_reply_to_status_id_str, $connection);
1831 } catch (TwitterOAuthException $e) {
1832 Logger::warning('Error fetching parent post', ['uid' => $uid, 'post' => $post->id_str, 'message' => $e->getMessage()]);
1837 Logger::info("twitter_fetchparentposts: Can't fetch post");
1841 if (empty($post->id_str)) {
1842 Logger::info("twitter_fetchparentposts: This is not a post", ['post' => $post]);
1846 if (Post::exists(['uri' => 'twitter::' . $post->id_str, 'uid' => $uid])) {
1853 Logger::log("twitter_fetchparentposts: Fetching " . count($posts) . " parents", Logger::DEBUG);
1855 $posts = array_reverse($posts);
1857 if (!empty($posts)) {
1858 foreach ($posts as $post) {
1859 $postarray = twitter_createpost($a, $uid, $post, $self, false, !DI::pConfig()->get($uid, 'twitter', 'create_user'), false);
1861 if (empty($postarray['body'])) {
1865 $item = Item::insert($postarray);
1867 $postarray["id"] = $item;
1869 Logger::log('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1874 function twitter_fetchhometimeline(App $a, $uid)
1876 $ckey = DI::config()->get('twitter', 'consumerkey');
1877 $csecret = DI::config()->get('twitter', 'consumersecret');
1878 $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
1879 $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
1880 $create_user = DI::pConfig()->get($uid, 'twitter', 'create_user');
1881 $mirror_posts = DI::pConfig()->get($uid, 'twitter', 'mirror_posts');
1883 Logger::info('Fetching timeline', ['uid' => $uid]);
1885 $application_name = DI::config()->get('twitter', 'application_name');
1887 if ($application_name == "") {
1888 $application_name = DI::baseUrl()->getHostname();
1891 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1894 $own_contact = twitter_fetch_own_contact($a, $uid);
1895 } catch (TwitterOAuthException $e) {
1896 Logger::warning('Error fetching own contact', ['uid' => $uid, 'message' => $e->getMessage()]);
1900 $contact = Contact::selectFirst(['nick'], ['id' => $own_contact, 'uid' => $uid]);
1901 if (DBA::isResult($contact)) {
1902 $own_id = $contact['nick'];
1904 Logger::warning('Own twitter contact not found', ['uid' => $uid]);
1908 $self = User::getOwnerDataById($uid);
1909 if ($self === false) {
1910 Logger::warning('Own contact not found', ['uid' => $uid]);
1914 $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended", "include_ext_alt_text" => true];
1915 //$parameters["count"] = 200;
1916 // Fetching timeline
1917 $lastid = DI::pConfig()->get($uid, 'twitter', 'lasthometimelineid');
1919 $first_time = ($lastid == "");
1921 if ($lastid != "") {
1922 $parameters["since_id"] = $lastid;
1926 $items = $connection->get('statuses/home_timeline', $parameters);
1927 } catch (TwitterOAuthException $e) {
1928 Logger::warning('Error fetching home timeline', ['uid' => $uid, 'message' => $e->getMessage()]);
1932 if (!is_array($items)) {
1933 Logger::warning('home timeline is no array', ['items' => $items]);
1937 if (empty($items)) {
1938 Logger::notice('No new timeline content', ['uid' => $uid]);
1942 $posts = array_reverse($items);
1944 Logger::notice('Processing timeline', ['lastid' => $lastid, 'uid' => $uid, 'count' => count($posts)]);
1946 if (count($posts)) {
1947 foreach ($posts as $post) {
1948 if ($post->id_str > $lastid) {
1949 $lastid = $post->id_str;
1950 DI::pConfig()->set($uid, 'twitter', 'lasthometimelineid', $lastid);
1957 if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1958 Logger::info("Skip previously sent post");
1962 if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1963 Logger::info("Skip post that will be mirrored");
1967 if ($post->in_reply_to_status_id_str != "") {
1968 twitter_fetchparentposts($a, $uid, $post, $connection, $self);
1971 Logger::log('Preparing post ' . $post->id_str . ' for user ' . $uid, Logger::DEBUG);
1973 $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1975 if (empty($postarray['body']) || trim($postarray['body']) == "") {
1976 Logger::log('Empty body for post ' . $post->id_str . ' and user ' . $uid, Logger::DEBUG);
1982 if (empty($postarray['thr-parent'])) {
1983 $contact = DBA::selectFirst('contact', [], ['id' => $postarray['contact-id'], 'self' => false]);
1984 if (DBA::isResult($contact) && Item::isRemoteSelf($contact, $postarray)) {
1985 $notify = PRIORITY_MEDIUM;
1989 $item = Item::insert($postarray, $notify);
1990 $postarray["id"] = $item;
1992 Logger::log('User ' . $uid . ' posted home timeline item ' . $item);
1995 DI::pConfig()->set($uid, 'twitter', 'lasthometimelineid', $lastid);
1997 Logger::log('Last timeline ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
1999 // Fetching mentions
2000 $lastid = DI::pConfig()->get($uid, 'twitter', 'lastmentionid');
2002 $first_time = ($lastid == "");
2004 if ($lastid != "") {
2005 $parameters["since_id"] = $lastid;
2009 $items = $connection->get('statuses/mentions_timeline', $parameters);
2010 } catch (TwitterOAuthException $e) {
2011 Logger::warning('Error fetching mentions', ['uid' => $uid, 'message' => $e->getMessage()]);
2015 if (!is_array($items)) {
2016 Logger::warning("mentions are no arrays", ['items' => $items]);
2020 $posts = array_reverse($items);
2022 Logger::log("Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", Logger::DEBUG);
2024 if (count($posts)) {
2025 foreach ($posts as $post) {
2026 if ($post->id_str > $lastid) {
2027 $lastid = $post->id_str;
2034 if ($post->in_reply_to_status_id_str != "") {
2035 twitter_fetchparentposts($a, $uid, $post, $connection, $self);
2038 $postarray = twitter_createpost($a, $uid, $post, $self, false, !$create_user, false);
2040 if (empty($postarray['body'])) {
2044 $item = Item::insert($postarray);
2046 Logger::log('User ' . $uid . ' posted mention timeline item ' . $item);
2050 DI::pConfig()->set($uid, 'twitter', 'lastmentionid', $lastid);
2052 Logger::log('Last mentions ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
2055 function twitter_fetch_own_contact(App $a, $uid)
2057 $ckey = DI::config()->get('twitter', 'consumerkey');
2058 $csecret = DI::config()->get('twitter', 'consumersecret');
2059 $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
2060 $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
2062 $own_id = DI::pConfig()->get($uid, 'twitter', 'own_id');
2066 if ($own_id == "") {
2067 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
2069 // Fetching user data
2070 // get() may throw TwitterOAuthException, but we will catch it later
2071 $user = $connection->get('account/verify_credentials');
2072 if (empty($user->id_str)) {
2076 DI::pConfig()->set($uid, 'twitter', 'own_id', $user->id_str);
2078 $contact_id = twitter_fetch_contact($uid, $user, true);
2080 $contact = Contact::selectFirst(['id'], ['uid' => $uid, 'alias' => 'twitter::' . $own_id]);
2081 if (DBA::isResult($contact)) {
2082 $contact_id = $contact['id'];
2084 DI::pConfig()->delete($uid, 'twitter', 'own_id');
2091 function twitter_is_retweet(App $a, $uid, $body)
2093 $body = trim($body);
2095 // Skip if it isn't a pure repeated messages
2096 // Does it start with a share?
2097 if (strpos($body, "[share") > 0) {
2101 // Does it end with a share?
2102 if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
2106 $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
2107 // Skip if there is no shared message in there
2108 if ($body == $attributes) {
2113 preg_match("/link='(.*?)'/ism", $attributes, $matches);
2114 if (!empty($matches[1])) {
2115 $link = $matches[1];
2118 preg_match('/link="(.*?)"/ism', $attributes, $matches);
2119 if (!empty($matches[1])) {
2120 $link = $matches[1];
2123 $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
2127 return twitter_retweet($uid, $id);
2130 function twitter_retweet(int $uid, int $id, int $item_id = 0)
2132 Logger::info('Retweeting', ['user' => $uid, 'id' => $id]);
2134 $result = twitter_api_post('statuses/retweet', $id, $uid);
2136 Logger::info('Retweeted', ['user' => $uid, 'id' => $id, 'result' => $result]);
2138 if (!empty($item_id) && !empty($result->id_str)) {
2139 Logger::notice('Update extid', ['id' => $item_id, 'extid' => $result->id_str]);
2140 Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $item_id]);
2143 return !isset($result->errors);
2146 function twitter_update_mentions($body)
2148 $URLSearchString = "^\[\]";
2149 $return = preg_replace_callback(
2150 "/@\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
2151 function ($matches) {
2152 if (strpos($matches[1], 'twitter.com')) {
2153 $return = '@' . substr($matches[1], strrpos($matches[1], '/') + 1);
2155 $return = $matches[2] . ' (' . $matches[1] . ')';
2166 function twitter_convert_share(array $attributes, array $author_contact, $content, $is_quote_share)
2168 if (empty($author_contact)) {
2169 return $content . "\n\n" . $attributes['link'];
2172 if (!empty($author_contact['network']) && ($author_contact['network'] == Protocol::TWITTER)) {
2173 $mention = '@' . $author_contact['nick'];
2175 $mention = $author_contact['addr'];
2178 return ($is_quote_share ? "\n\n" : '' ) . 'RT ' . $mention . ': ' . $content . "\n\n" . $attributes['link'];