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 .htconfig.php or use the admin panel.
53 * $a->config['twitter']['consumerkey'] = 'your consumer_key here';
54 * $a->config['twitter']['consumersecret'] = 'your consumer_secret here';
56 * To activate the addon itself add it to the $a->config['system']['addon']
57 * setting. After this, your user can configure their Twitter account settings
58 * from "Settings -> Addon Settings".
60 * Requirements: PHP5, curl
63 use Abraham\TwitterOAuth\TwitterOAuth;
65 use Friendica\Content\OEmbed;
66 use Friendica\Content\Text\BBCode;
67 use Friendica\Content\Text\Plaintext;
68 use Friendica\Core\Addon;
69 use Friendica\Core\Config;
70 use Friendica\Core\L10n;
71 use Friendica\Core\PConfig;
72 use Friendica\Core\Worker;
73 use Friendica\Model\GContact;
74 use Friendica\Model\Group;
75 use Friendica\Model\Item;
76 use Friendica\Model\Photo;
77 use Friendica\Model\Queue;
78 use Friendica\Model\User;
79 use Friendica\Object\Image;
80 use Friendica\Util\Network;
81 use Friendica\Util\Temporal;
83 require_once 'boot.php';
84 require_once 'include/dba.php';
85 require_once 'include/enotify.php';
86 require_once 'include/text.php';
88 require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
90 define('TWITTER_DEFAULT_POLL_INTERVAL', 5); // given in minutes
92 function twitter_install()
94 // we need some hooks, for the configuration and for sending tweets
95 Addon::registerHook('connector_settings', 'addon/twitter/twitter.php', 'twitter_settings');
96 Addon::registerHook('connector_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
97 Addon::registerHook('post_local', 'addon/twitter/twitter.php', 'twitter_post_local');
98 Addon::registerHook('notifier_normal', 'addon/twitter/twitter.php', 'twitter_post_hook');
99 Addon::registerHook('jot_networks', 'addon/twitter/twitter.php', 'twitter_jot_nets');
100 Addon::registerHook('cron', 'addon/twitter/twitter.php', 'twitter_cron');
101 Addon::registerHook('queue_predeliver', 'addon/twitter/twitter.php', 'twitter_queue_hook');
102 Addon::registerHook('follow', 'addon/twitter/twitter.php', 'twitter_follow');
103 Addon::registerHook('expire', 'addon/twitter/twitter.php', 'twitter_expire');
104 Addon::registerHook('prepare_body', 'addon/twitter/twitter.php', 'twitter_prepare_body');
105 Addon::registerHook('check_item_notification', 'addon/twitter/twitter.php', 'twitter_check_item_notification');
106 logger("installed twitter");
109 function twitter_uninstall()
111 Addon::unregisterHook('connector_settings', 'addon/twitter/twitter.php', 'twitter_settings');
112 Addon::unregisterHook('connector_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
113 Addon::unregisterHook('post_local', 'addon/twitter/twitter.php', 'twitter_post_local');
114 Addon::unregisterHook('notifier_normal', 'addon/twitter/twitter.php', 'twitter_post_hook');
115 Addon::unregisterHook('jot_networks', 'addon/twitter/twitter.php', 'twitter_jot_nets');
116 Addon::unregisterHook('cron', 'addon/twitter/twitter.php', 'twitter_cron');
117 Addon::unregisterHook('queue_predeliver', 'addon/twitter/twitter.php', 'twitter_queue_hook');
118 Addon::unregisterHook('follow', 'addon/twitter/twitter.php', 'twitter_follow');
119 Addon::unregisterHook('expire', 'addon/twitter/twitter.php', 'twitter_expire');
120 Addon::unregisterHook('prepare_body', 'addon/twitter/twitter.php', 'twitter_prepare_body');
121 Addon::unregisterHook('check_item_notification', 'addon/twitter/twitter.php', 'twitter_check_item_notification');
123 // old setting - remove only
124 Addon::unregisterHook('post_local_end', 'addon/twitter/twitter.php', 'twitter_post_hook');
125 Addon::unregisterHook('addon_settings', 'addon/twitter/twitter.php', 'twitter_settings');
126 Addon::unregisterHook('addon_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
129 function twitter_check_item_notification(App $a, &$notification_data)
131 $own_id = PConfig::get($notification_data["uid"], 'twitter', 'own_id');
133 $own_user = q("SELECT `url` FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
134 intval($notification_data["uid"]),
135 dbesc("twitter::".$own_id)
139 $notification_data["profiles"][] = $own_user[0]["url"];
143 function twitter_follow(App $a, &$contact)
145 logger("twitter_follow: Check if contact is twitter contact. " . $contact["url"], LOGGER_DEBUG);
147 if (!strstr($contact["url"], "://twitter.com") && !strstr($contact["url"], "@twitter.com")) {
151 // contact seems to be a twitter contact, so continue
152 $nickname = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $contact["url"]);
153 $nickname = str_replace("@twitter.com", "", $nickname);
155 $uid = $a->user["uid"];
157 $ckey = Config::get('twitter', 'consumerkey');
158 $csecret = Config::get('twitter', 'consumersecret');
159 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
160 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
162 // If the addon is not configured (general or for this user) quit here
163 if (empty($ckey) || empty($csecret) || empty($otoken) || empty($osecret)) {
168 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
169 $connection->post('friendships/create', ['screen_name' => $nickname]);
171 twitter_fetchuser($a, $uid, $nickname);
173 $r = q("SELECT name,nick,url,addr,batch,notify,poll,request,confirm,poco,photo,priority,network,alias,pubkey
174 FROM `contact` WHERE `uid` = %d AND `nick` = '%s'",
178 $contact["contact"] = $r[0];
182 function twitter_jot_nets(App $a, &$b)
188 $tw_post = PConfig::get(local_user(), 'twitter', 'post');
189 if (intval($tw_post) == 1) {
190 $tw_defpost = PConfig::get(local_user(), 'twitter', 'post_by_default');
191 $selected = ((intval($tw_defpost) == 1) ? ' checked="checked" ' : '');
192 $b .= '<div class="profile-jot-net"><input type="checkbox" name="twitter_enable"' . $selected . ' value="1" /> '
193 . L10n::t('Post to Twitter') . '</div>';
197 function twitter_settings_post(App $a, $post)
202 // don't check twitter settings if twitter submit button is not clicked
203 if (!x($_POST, 'twitter-submit')) {
207 if (isset($_POST['twitter-disconnect'])) {
209 * if the twitter-disconnect checkbox is set, clear the OAuth key/secret pair
210 * from the user configuration
212 PConfig::delete(local_user(), 'twitter', 'consumerkey');
213 PConfig::delete(local_user(), 'twitter', 'consumersecret');
214 PConfig::delete(local_user(), 'twitter', 'oauthtoken');
215 PConfig::delete(local_user(), 'twitter', 'oauthsecret');
216 PConfig::delete(local_user(), 'twitter', 'post');
217 PConfig::delete(local_user(), 'twitter', 'post_by_default');
218 PConfig::delete(local_user(), 'twitter', 'lastid');
219 PConfig::delete(local_user(), 'twitter', 'mirror_posts');
220 PConfig::delete(local_user(), 'twitter', 'import');
221 PConfig::delete(local_user(), 'twitter', 'create_user');
222 PConfig::delete(local_user(), 'twitter', 'own_id');
224 if (isset($_POST['twitter-pin'])) {
225 // if the user supplied us with a PIN from Twitter, let the magic of OAuth happen
226 logger('got a Twitter PIN');
227 $ckey = Config::get('twitter', 'consumerkey');
228 $csecret = Config::get('twitter', 'consumersecret');
229 // the token and secret for which the PIN was generated were hidden in the settings
230 // form as token and token2, we need a new connection to Twitter using these token
231 // and secret to request a Access Token with the PIN
232 $connection = new TwitterOAuth($ckey, $csecret, $_POST['twitter-token'], $_POST['twitter-token2']);
233 $token = $connection->oauth("oauth/access_token", ["oauth_verifier" => $_POST['twitter-pin']]);
234 // ok, now that we have the Access Token, save them in the user config
235 PConfig::set(local_user(), 'twitter', 'oauthtoken', $token['oauth_token']);
236 PConfig::set(local_user(), 'twitter', 'oauthsecret', $token['oauth_token_secret']);
237 PConfig::set(local_user(), 'twitter', 'post', 1);
238 // reload the Addon Settings page, if we don't do it see Bug #42
239 goaway('settings/connectors');
241 // if no PIN is supplied in the POST variables, the user has changed the setting
242 // to post a tweet for every new __public__ posting to the wall
243 PConfig::set(local_user(), 'twitter', 'post', intval($_POST['twitter-enable']));
244 PConfig::set(local_user(), 'twitter', 'post_by_default', intval($_POST['twitter-default']));
245 PConfig::set(local_user(), 'twitter', 'mirror_posts', intval($_POST['twitter-mirror']));
246 PConfig::set(local_user(), 'twitter', 'import', intval($_POST['twitter-import']));
247 PConfig::set(local_user(), 'twitter', 'create_user', intval($_POST['twitter-create_user']));
249 if (!intval($_POST['twitter-mirror'])) {
250 PConfig::delete(local_user(), 'twitter', 'lastid');
253 info(L10n::t('Twitter settings updated.') . EOL);
258 function twitter_settings(App $a, &$s)
263 $a->page['htmlhead'] .= '<link rel="stylesheet" type="text/css" href="' . $a->get_baseurl() . '/addon/twitter/twitter.css' . '" media="all" />' . "\r\n";
265 * 1) Check that we have global consumer key & secret
266 * 2) If no OAuthtoken & stuff is present, generate button to get some
267 * 3) Checkbox for "Send public notices (280 chars only)
269 $ckey = Config::get('twitter', 'consumerkey');
270 $csecret = Config::get('twitter', 'consumersecret');
271 $otoken = PConfig::get(local_user(), 'twitter', 'oauthtoken');
272 $osecret = PConfig::get(local_user(), 'twitter', 'oauthsecret');
274 $enabled = intval(PConfig::get(local_user(), 'twitter', 'post'));
275 $defenabled = intval(PConfig::get(local_user(), 'twitter', 'post_by_default'));
276 $mirrorenabled = intval(PConfig::get(local_user(), 'twitter', 'mirror_posts'));
277 $importenabled = intval(PConfig::get(local_user(), 'twitter', 'import'));
278 $create_userenabled = intval(PConfig::get(local_user(), 'twitter', 'create_user'));
280 $css = (($enabled) ? '' : '-disabled');
282 $s .= '<span id="settings_twitter_inflated" class="settings-block fakelink" style="display: block;" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
283 $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
285 $s .= '<div id="settings_twitter_expanded" class="settings-block" style="display: none;">';
286 $s .= '<span class="fakelink" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
287 $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
290 if ((!$ckey) && (!$csecret)) {
292 * no global consumer keys
293 * display warning and skip personal config
295 $s .= '<p>' . L10n::t('No consumer key pair for Twitter found. Please contact your site administrator.') . '</p>';
298 * ok we have a consumer key pair now look into the OAuth stuff
300 if ((!$otoken) && (!$osecret)) {
302 * the user has not yet connected the account to twitter...
303 * get a temporary OAuth key/secret pair and display a button with
304 * which the user can request a PIN to connect the account to a
305 * account at Twitter.
307 $connection = new TwitterOAuth($ckey, $csecret);
308 $result = $connection->oauth('oauth/request_token', ['oauth_callback' => 'oob']);
310 * make some nice form
312 $s .= '<p>' . 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>';
313 $s .= '<a href="' . $connection->url('oauth/authorize', ['oauth_token' => $result->oauth_token]) . '" target="_twitter"><img src="addon/twitter/lighter.png" alt="' . L10n::t('Log in with Twitter') . '"></a>';
314 $s .= '<div id="twitter-pin-wrapper">';
315 $s .= '<label id="twitter-pin-label" for="twitter-pin">' . L10n::t('Copy the PIN from Twitter here') . '</label>';
316 $s .= '<input id="twitter-pin" type="text" name="twitter-pin" />';
317 $s .= '<input id="twitter-token" type="hidden" name="twitter-token" value="' . $result->oauth_token . '" />';
318 $s .= '<input id="twitter-token2" type="hidden" name="twitter-token2" value="' . $result->oauth_token_secret . '" />';
319 $s .= '</div><div class="clear"></div>';
320 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
323 * we have an OAuth key / secret pair for the user
324 * so let's give a chance to disable the postings to Twitter
326 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
327 $details = $connection->get('account/verify_credentials');
329 $field_checkbox = get_markup_template('field_checkbox.tpl');
331 $s .= '<div id="twitter-info" >
332 <p>' . L10n::t('Currently connected to: ') . '<a href="https://twitter.com/' . $details->screen_name . '" target="_twitter">' . $details->screen_name . '</a>
333 <button type="submit" name="twitter-disconnect" value="1">' . L10n::t('Disconnect') . '</button>
335 <p id="twitter-info-block">
336 <a href="https://twitter.com/' . $details->screen_name . '" target="_twitter"><img id="twitter-avatar" src="' . $details->profile_image_url . '" /></a>
337 <em>' . $details->description . '</em>
340 $s .= '<div class="clear"></div>';
342 $s .= replace_macros($field_checkbox, [
343 '$field' => ['twitter-enable', L10n::t('Allow posting to Twitter'), $enabled, 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.')]
345 if ($a->user['hidewall']) {
346 $s .= '<p>' . 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>';
348 $s .= replace_macros($field_checkbox, [
349 '$field' => ['twitter-default', L10n::t('Send public postings to Twitter by default'), $defenabled, '']
351 $s .= replace_macros($field_checkbox, [
352 '$field' => ['twitter-mirror', L10n::t('Mirror all posts from twitter that are no replies'), $mirrorenabled, '']
354 $s .= replace_macros($field_checkbox, [
355 '$field' => ['twitter-import', L10n::t('Import the remote timeline'), $importenabled, '']
357 $s .= replace_macros($field_checkbox, [
358 '$field' => ['twitter-create_user', L10n::t('Automatically create contacts'), $create_userenabled, '']
361 $s .= '<div class="clear"></div>';
362 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
365 $s .= '</div><div class="clear"></div>';
368 function twitter_post_local(App $a, &$b)
374 if (!local_user() || (local_user() != $b['uid'])) {
378 $twitter_post = intval(PConfig::get(local_user(), 'twitter', 'post'));
379 $twitter_enable = (($twitter_post && x($_REQUEST, 'twitter_enable')) ? intval($_REQUEST['twitter_enable']) : 0);
381 // if API is used, default to the chosen settings
382 if ($b['api_source'] && intval(PConfig::get(local_user(), 'twitter', 'post_by_default'))) {
386 if (!$twitter_enable) {
390 if (strlen($b['postopts'])) {
391 $b['postopts'] .= ',';
394 $b['postopts'] .= 'twitter';
397 function twitter_action(App $a, $uid, $pid, $action)
399 $ckey = Config::get('twitter', 'consumerkey');
400 $csecret = Config::get('twitter', 'consumersecret');
401 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
402 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
404 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
406 $post = ['id' => $pid];
408 logger("twitter_action '" . $action . "' ID: " . $pid . " data: " . print_r($post, true), LOGGER_DATA);
412 // To-Do: $result = $connection->post('statuses/destroy', $post);
415 $result = $connection->post('favorites/create', $post);
418 $result = $connection->post('favorites/destroy', $post);
421 logger("twitter_action '" . $action . "' send, result: " . print_r($result, true), LOGGER_DEBUG);
424 function twitter_post_hook(App $a, &$b)
427 if (!PConfig::get($b["uid"], 'twitter', 'import')
428 && ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))) {
432 if ($b['parent'] != $b['id']) {
433 logger("twitter_post_hook: parameter " . print_r($b, true), LOGGER_DATA);
435 // Looking if its a reply to a twitter post
436 if ((substr($b["parent-uri"], 0, 9) != "twitter::")
437 && (substr($b["extid"], 0, 9) != "twitter::")
438 && (substr($b["thr-parent"], 0, 9) != "twitter::"))
440 logger("twitter_post_hook: no twitter post " . $b["parent"]);
444 $r = q("SELECT * FROM item WHERE item.uri = '%s' AND item.uid = %d LIMIT 1",
445 dbesc($b["thr-parent"]),
449 logger("twitter_post_hook: no parent found " . $b["thr-parent"]);
457 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
458 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
459 $nicknameplain = "@" . $nicknameplain;
461 logger("twitter_post_hook: comparing " . $nickname . " and " . $nicknameplain . " with " . $b["body"], LOGGER_DEBUG);
462 if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) {
463 $b["body"] = $nickname . " " . $b["body"];
466 logger("twitter_post_hook: parent found " . print_r($orig_post, true), LOGGER_DATA);
470 if ($b['private'] || !strstr($b['postopts'], 'twitter')) {
474 // Dont't post if the post doesn't belong to us.
475 // This is a check for forum postings
476 $self = dba::selectFirst('contact', ['id'], ['uid' => $b['uid'], 'self' => true]);
477 if ($b['contact-id'] != $self['id']) {
482 if (($b['verb'] == ACTIVITY_POST) && $b['deleted']) {
483 twitter_action($a, $b["uid"], substr($orig_post["uri"], 9), "delete");
486 if ($b['verb'] == ACTIVITY_LIKE) {
487 logger("twitter_post_hook: parameter 2 " . substr($b["thr-parent"], 9), LOGGER_DEBUG);
489 twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "unlike");
491 twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "like");
497 if ($b['deleted'] || ($b['created'] !== $b['edited'])) {
501 // if post comes from twitter don't send it back
502 if ($b['extid'] == NETWORK_TWITTER) {
506 if ($b['app'] == "Twitter") {
510 logger('twitter post invoked');
512 PConfig::load($b['uid'], 'twitter');
514 $ckey = Config::get('twitter', 'consumerkey');
515 $csecret = Config::get('twitter', 'consumersecret');
516 $otoken = PConfig::get($b['uid'], 'twitter', 'oauthtoken');
517 $osecret = PConfig::get($b['uid'], 'twitter', 'oauthsecret');
519 if ($ckey && $csecret && $otoken && $osecret) {
520 logger('twitter: we have customer key and oauth stuff, going to send.', LOGGER_DEBUG);
522 // If it's a repeated message from twitter then do a native retweet and exit
523 if (twitter_is_retweet($a, $b['uid'], $b['body'])) {
527 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
530 $msgarr = BBCode::toPlaintext($b, $max_char, true, 8);
531 $msg = $msgarr["text"];
533 if (($msg == "") && isset($msgarr["title"])) {
534 $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
539 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
540 $msg .= "\n" . $msgarr["url"];
543 if (isset($msgarr["image"]) && ($msgarr["type"] != "video")) {
544 $image = $msgarr["image"];
547 // and now tweet it :-)
548 if (strlen($msg) && ($image != "")) {
549 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
550 $media = $connection->upload('media/upload', ['media' => $image]);
552 $post = ['status' => $msg, 'media_ids' => $media->media_id_string];
555 $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
558 $result = $connection->post('statuses/update', $post);
560 logger('twitter_post_with_media send, result: ' . print_r($result, true), LOGGER_DEBUG);
562 if ($result->source) {
563 Config::set("twitter", "application_name", strip_tags($result->source));
566 if ($result->errors || $result->error) {
567 logger('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
569 // Workaround: Remove the picture link so that the post can be reposted without it
570 $msg .= " " . $image;
572 } elseif ($iscomment) {
573 logger('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
574 q("UPDATE `item` SET `extid` = '%s', `body` = '%s' WHERE `id` = %d",
575 dbesc("twitter::" . $result->id_str),
576 dbesc($result->text),
582 if (strlen($msg) && ($image == "")) {
585 $msgarr = BBCode::toPlaintext($b, $max_char, true, 8);
586 $msg = $msgarr["text"];
588 if (($msg == "") && isset($msgarr["title"])) {
589 $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
592 if (isset($msgarr["url"])) {
593 $msg .= "\n" . $msgarr["url"];
596 $url = 'statuses/update';
597 $post = ['status' => $msg, 'weighted_character_count' => 'true'];
600 $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
603 $result = $connection->post($url, $post);
604 logger('twitter_post send, result: ' . print_r($result, true), LOGGER_DEBUG);
606 if ($result->source) {
607 Config::set("twitter", "application_name", strip_tags($result->source));
610 if ($result->errors) {
611 logger('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
613 $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `self`", intval($b['uid']));
615 $a->contact = $r[0]["id"];
618 $s = serialize(['url' => $url, 'item' => $b['id'], 'post' => $post]);
620 Queue::add($a->contact, NETWORK_TWITTER, $s);
621 notice(L10n::t('Twitter post failed. Queued for retry.') . EOL);
622 } elseif ($iscomment) {
623 logger('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
624 q("UPDATE `item` SET `extid` = '%s' WHERE `id` = %d",
625 dbesc("twitter::" . $result->id_str),
633 function twitter_addon_admin_post(App $a)
635 $consumerkey = x($_POST, 'consumerkey') ? notags(trim($_POST['consumerkey'])) : '';
636 $consumersecret = x($_POST, 'consumersecret') ? notags(trim($_POST['consumersecret'])) : '';
637 Config::set('twitter', 'consumerkey', $consumerkey);
638 Config::set('twitter', 'consumersecret', $consumersecret);
639 info(L10n::t('Settings updated.') . EOL);
642 function twitter_addon_admin(App $a, &$o)
644 $t = get_markup_template("admin.tpl", "addon/twitter/");
646 $o = replace_macros($t, [
647 '$submit' => L10n::t('Save Settings'),
648 // name, label, value, help, [extra values]
649 '$consumerkey' => ['consumerkey', L10n::t('Consumer key'), Config::get('twitter', 'consumerkey'), ''],
650 '$consumersecret' => ['consumersecret', L10n::t('Consumer secret'), Config::get('twitter', 'consumersecret'), ''],
654 function twitter_cron(App $a, $b)
656 $last = Config::get('twitter', 'last_poll');
658 $poll_interval = intval(Config::get('twitter', 'poll_interval'));
659 if (!$poll_interval) {
660 $poll_interval = TWITTER_DEFAULT_POLL_INTERVAL;
664 $next = $last + ($poll_interval * 60);
665 if ($next > time()) {
666 logger('twitter: poll intervall not reached');
670 logger('twitter: cron_start');
672 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'mirror_posts' AND `v` = '1'");
674 foreach ($r as $rr) {
675 logger('twitter: fetching for user ' . $rr['uid']);
676 Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
680 $abandon_days = intval(Config::get('system', 'account_abandon_days'));
681 if ($abandon_days < 1) {
685 $abandon_limit = date(Temporal::MYSQL, time() - $abandon_days * 86400);
687 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1'");
689 foreach ($r as $rr) {
690 if ($abandon_days != 0) {
691 $user = q("SELECT `login_date` FROM `user` WHERE uid=%d AND `login_date` >= '%s'", $rr['uid'], $abandon_limit);
693 logger('abandoned account: timeline from user ' . $rr['uid'] . ' will not be imported');
698 logger('twitter: importing timeline from user ' . $rr['uid']);
699 Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 2, (int) $rr['uid']);
702 // check for new contacts once a day
703 $last_contact_check = PConfig::get($rr['uid'],'pumpio','contact_check');
704 if($last_contact_check)
705 $next_contact_check = $last_contact_check + 86400;
707 $next_contact_check = 0;
709 if($next_contact_check <= time()) {
710 pumpio_getallusers($a, $rr["uid"]);
711 PConfig::set($rr['uid'],'pumpio','contact_check',time());
717 logger('twitter: cron_end');
719 Config::set('twitter', 'last_poll', time());
722 function twitter_expire(App $a, $b)
724 $days = Config::get('twitter', 'expire');
730 if (method_exists('dba', 'delete')) {
731 $r = dba::select('item', ['id'], ['deleted' => true, 'network' => NETWORK_TWITTER]);
732 while ($row = dba::fetch($r)) {
733 dba::delete('item', ['id' => $row['id']]);
737 $r = q("DELETE FROM `item` WHERE `deleted` AND `network` = '%s'", dbesc(NETWORK_TWITTER));
740 require_once "include/items.php";
742 logger('twitter_expire: expire_start');
744 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
746 foreach ($r as $rr) {
747 logger('twitter_expire: user ' . $rr['uid']);
748 Item::expire($rr['uid'], $days, NETWORK_TWITTER, true);
752 logger('twitter_expire: expire_end');
755 function twitter_prepare_body(App $a, &$b)
757 if ($b["item"]["network"] != NETWORK_TWITTER) {
764 $item["plink"] = $a->get_baseurl() . "/display/" . $a->user["nickname"] . "/" . $item["parent"];
766 $r = q("SELECT `author-link` FROM item WHERE item.uri = '%s' AND item.uid = %d LIMIT 1",
767 dbesc($item["thr-parent"]),
768 intval(local_user()));
773 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
774 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
775 $nicknameplain = "@" . $nicknameplain;
777 if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) {
778 $item["body"] = $nickname . " " . $item["body"];
782 $msgarr = BBCode::toPlaintext($item, $max_char, true, 8);
783 $msg = $msgarr["text"];
785 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
786 $msg .= " " . $msgarr["url"];
789 if (isset($msgarr["image"])) {
790 $msg .= " " . $msgarr["image"];
793 $b['html'] = nl2br(htmlspecialchars($msg));
798 * @brief Build the item array for the mirrored post
800 * @param App $a Application class
801 * @param integer $uid User id
802 * @param object $post Twitter object with the post
804 * @return array item data to be posted
806 function twitter_do_mirrorpost(App $a, $uid, $post)
808 $datarray["type"] = "wall";
809 $datarray["api_source"] = true;
810 $datarray["profile_uid"] = $uid;
811 $datarray["extid"] = NETWORK_TWITTER;
812 $datarray['message_id'] = item_new_uri($a->get_hostname(), $uid, NETWORK_TWITTER . ":" . $post->id);
813 $datarray['object'] = json_encode($post);
814 $datarray["title"] = "";
816 if (is_object($post->retweeted_status)) {
817 // We don't support nested shares, so we mustn't show quotes as shares on retweets
818 $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true);
820 $datarray['body'] = "\n" . share_header(
821 $item['author-name'],
822 $item['author-link'],
823 $item['author-avatar'],
829 $datarray['body'] .= $item['body'] . '[/share]';
831 $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false);
833 $datarray['body'] = $item['body'];
836 $datarray["source"] = $item['app'];
837 $datarray["verb"] = $item['verb'];
839 if (isset($item["location"])) {
840 $datarray["location"] = $item["location"];
843 if (isset($item["coord"])) {
844 $datarray["coord"] = $item["coord"];
850 function twitter_fetchtimeline(App $a, $uid)
852 $ckey = Config::get('twitter', 'consumerkey');
853 $csecret = Config::get('twitter', 'consumersecret');
854 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
855 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
856 $lastid = PConfig::get($uid, 'twitter', 'lastid');
858 $application_name = Config::get('twitter', 'application_name');
860 if ($application_name == "") {
861 $application_name = $a->get_hostname();
864 $has_picture = false;
866 require_once 'mod/item.php';
867 require_once 'include/items.php';
868 require_once 'mod/share.php';
870 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
872 $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
874 $first_time = ($lastid == "");
877 $parameters["since_id"] = $lastid;
880 $items = $connection->get('statuses/user_timeline', $parameters);
882 if (!is_array($items)) {
886 $posts = array_reverse($items);
889 foreach ($posts as $post) {
890 if ($post->id_str > $lastid) {
891 $lastid = $post->id_str;
892 PConfig::set($uid, 'twitter', 'lastid', $lastid);
899 if (!stristr($post->source, $application_name)) {
900 $_SESSION["authenticated"] = true;
901 $_SESSION["uid"] = $uid;
903 $_REQUEST = twitter_do_mirrorpost($a, $uid, $post);
905 logger('twitter: posting for user ' . $uid);
911 PConfig::set($uid, 'twitter', 'lastid', $lastid);
914 function twitter_queue_hook(App $a, &$b)
916 $qi = q("SELECT * FROM `queue` WHERE `network` = '%s'",
917 dbesc(NETWORK_TWITTER)
923 foreach ($qi as $x) {
924 if ($x['network'] !== NETWORK_TWITTER) {
928 logger('twitter_queue: run');
930 $r = q("SELECT `user`.* FROM `user` LEFT JOIN `contact` on `contact`.`uid` = `user`.`uid`
931 WHERE `contact`.`self` = 1 AND `contact`.`id` = %d LIMIT 1",
940 $ckey = Config::get('twitter', 'consumerkey');
941 $csecret = Config::get('twitter', 'consumersecret');
942 $otoken = PConfig::get($user['uid'], 'twitter', 'oauthtoken');
943 $osecret = PConfig::get($user['uid'], 'twitter', 'oauthsecret');
947 if ($ckey && $csecret && $otoken && $osecret) {
948 logger('twitter_queue: able to post');
950 $z = unserialize($x['content']);
952 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
953 $result = $connection->post($z['url'], $z['post']);
955 logger('twitter_queue: post result: ' . print_r($result, true), LOGGER_DEBUG);
957 if ($result->errors) {
958 logger('twitter_queue: Send to Twitter failed: "' . print_r($result->errors, true) . '"');
961 Queue::removeItem($x['id']);
964 logger("twitter_queue: Error getting tokens for user " . $user['uid']);
968 logger('twitter_queue: delayed');
969 Queue::updateTime($x['id']);
974 function twitter_fix_avatar($avatar)
976 $new_avatar = str_replace("_normal.", ".", $avatar);
978 $info = Image::getInfoFromURL($new_avatar);
980 $new_avatar = $avatar;
986 function twitter_fetch_contact($uid, $contact, $create_user)
988 if ($contact->id_str == "") {
992 $avatar = twitter_fix_avatar($contact->profile_image_url_https);
994 GContact::update(["url" => "https://twitter.com/" . $contact->screen_name,
995 "network" => NETWORK_TWITTER, "photo" => $avatar, "hide" => true,
996 "name" => $contact->name, "nick" => $contact->screen_name,
997 "location" => $contact->location, "about" => $contact->description,
998 "addr" => $contact->screen_name . "@twitter.com", "generation" => 2]);
1000 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1002 dbesc("twitter::" . $contact->id_str));
1004 if (!count($r) && !$create_user) {
1008 if (count($r) && ($r[0]["readonly"] || $r[0]["blocked"])) {
1009 logger("twitter_fetch_contact: Contact '" . $r[0]["nick"] . "' is blocked or readonly.", LOGGER_DEBUG);
1014 // create contact record
1015 q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `addr`, `alias`, `notify`, `poll`,
1016 `name`, `nick`, `photo`, `network`, `rel`, `priority`,
1017 `location`, `about`, `writable`, `blocked`, `readonly`, `pending`)
1018 VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', %d, 0, 0, 0)",
1020 dbesc(Temporal::utcNow()),
1021 dbesc("https://twitter.com/" . $contact->screen_name),
1022 dbesc(normalise_link("https://twitter.com/" . $contact->screen_name)),
1023 dbesc($contact->screen_name."@twitter.com"),
1024 dbesc("twitter::" . $contact->id_str),
1026 dbesc("twitter::" . $contact->id_str),
1027 dbesc($contact->name),
1028 dbesc($contact->screen_name),
1030 dbesc(NETWORK_TWITTER),
1031 intval(CONTACT_IS_FRIEND),
1033 dbesc($contact->location),
1034 dbesc($contact->description),
1038 $r = q("SELECT * FROM `contact` WHERE `alias` = '%s' AND `uid` = %d LIMIT 1",
1039 dbesc("twitter::".$contact->id_str),
1047 $contact_id = $r[0]['id'];
1049 Group::addMember(User::getDefaultGroup($uid), $contact_id);
1051 $photos = Photo::importProfilePhoto($avatar, $uid, $contact_id, true);
1054 q("UPDATE `contact` SET `photo` = '%s',
1059 `avatar-date` = '%s'
1064 dbesc(Temporal::utcNow()),
1065 dbesc(Temporal::utcNow()),
1066 dbesc(Temporal::utcNow()),
1071 // update profile photos once every two weeks as we have no notification of when they change.
1072 //$update_photo = (($r[0]['avatar-date'] < Temporal::convert('now -2 days', '', '', )) ? true : false);
1073 $update_photo = ($r[0]['avatar-date'] < Temporal::utc('now -12 hours'));
1075 // check that we have all the photos, this has been known to fail on occasion
1076 if ((!$r[0]['photo']) || (!$r[0]['thumb']) || (!$r[0]['micro']) || ($update_photo)) {
1077 logger("twitter_fetch_contact: Updating contact " . $contact->screen_name, LOGGER_DEBUG);
1079 $photos = Photo::importProfilePhoto($avatar, $uid, $r[0]['id'], true);
1082 q("UPDATE `contact` SET `photo` = '%s',
1087 `avatar-date` = '%s',
1099 dbesc(Temporal::utcNow()),
1100 dbesc(Temporal::utcNow()),
1101 dbesc(Temporal::utcNow()),
1102 dbesc("https://twitter.com/".$contact->screen_name),
1103 dbesc(normalise_link("https://twitter.com/".$contact->screen_name)),
1104 dbesc($contact->screen_name."@twitter.com"),
1105 dbesc($contact->name),
1106 dbesc($contact->screen_name),
1107 dbesc($contact->location),
1108 dbesc($contact->description),
1118 function twitter_fetchuser(App $a, $uid, $screen_name = "", $user_id = "")
1120 $ckey = Config::get('twitter', 'consumerkey');
1121 $csecret = Config::get('twitter', 'consumersecret');
1122 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1123 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1125 $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1136 if ($screen_name != "") {
1137 $parameters["screen_name"] = $screen_name;
1140 if ($user_id != "") {
1141 $parameters["user_id"] = $user_id;
1144 // Fetching user data
1145 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1146 $user = $connection->get('users/show', $parameters);
1148 if (!is_object($user)) {
1152 $contact_id = twitter_fetch_contact($uid, $user, true);
1157 function twitter_expand_entities(App $a, $body, $item, $no_tags = false, $picture)
1163 if (isset($item->entities->urls)) {
1169 foreach ($item->entities->urls as $url) {
1170 $plain = str_replace($url->url, '', $plain);
1172 if ($url->url && $url->expanded_url && $url->display_url) {
1173 $expanded_url = Network::finalUrl($url->expanded_url);
1175 $oembed_data = OEmbed::fetchURL($expanded_url);
1177 // Quickfix: Workaround for URL with "[" and "]" in it
1178 if (strpos($expanded_url, "[") || strpos($expanded_url, "]")) {
1179 $expanded_url = $url->url;
1183 $type = $oembed_data->type;
1186 if ($oembed_data->type == "video") {
1187 //$body = str_replace($url->url,
1188 // "[video]".$expanded_url."[/video]", $body);
1189 //$dontincludemedia = true;
1190 $type = $oembed_data->type;
1191 $footerurl = $expanded_url;
1192 $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1194 $body = str_replace($url->url, $footerlink, $body);
1195 //} elseif (($oembed_data->type == "photo") AND isset($oembed_data->url) AND !$dontincludemedia) {
1196 } elseif (($oembed_data->type == "photo") && isset($oembed_data->url)) {
1197 $body = str_replace($url->url, "[url=" . $expanded_url . "][img]" . $oembed_data->url . "[/img][/url]", $body);
1198 //$dontincludemedia = true;
1199 } elseif ($oembed_data->type != "link") {
1200 $body = str_replace($url->url, "[url=" . $expanded_url . "]" . $expanded_url . "[/url]", $body);
1202 $img_str = Network::fetchUrl($expanded_url, true, $redirects, 4);
1204 $tempfile = tempnam(get_temppath(), "cache");
1205 file_put_contents($tempfile, $img_str);
1206 $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1209 if (substr($mime, 0, 6) == "image/") {
1211 $body = str_replace($url->url, "[img]" . $expanded_url . "[/img]", $body);
1212 //$dontincludemedia = true;
1214 $type = $oembed_data->type;
1215 $footerurl = $expanded_url;
1216 $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1218 $body = str_replace($url->url, $footerlink, $body);
1224 if ($footerurl != "") {
1225 $footer = add_page_info($footerurl, false, $picture);
1228 if (($footerlink != "") && (trim($footer) != "")) {
1229 $removedlink = trim(str_replace($footerlink, "", $body));
1231 if (($removedlink == "") || strstr($body, $removedlink)) {
1232 $body = $removedlink;
1238 if (($footer == "") && ($picture != "")) {
1239 $body .= "\n\n[img]" . $picture . "[/img]\n";
1240 } elseif (($footer == "") && ($picture == "")) {
1241 $body = add_page_info_to_body($body);
1245 return ["body" => $body, "tags" => "", "plain" => $plain];
1250 foreach ($item->entities->hashtags AS $hashtag) {
1251 $url = "#[url=" . $a->get_baseurl() . "/search?tag=" . rawurlencode($hashtag->text) . "]" . $hashtag->text . "[/url]";
1252 $tags_arr["#" . $hashtag->text] = $url;
1253 $body = str_replace("#" . $hashtag->text, $url, $body);
1256 foreach ($item->entities->user_mentions AS $mention) {
1257 $url = "@[url=https://twitter.com/" . rawurlencode($mention->screen_name) . "]" . $mention->screen_name . "[/url]";
1258 $tags_arr["@" . $mention->screen_name] = $url;
1259 $body = str_replace("@" . $mention->screen_name, $url, $body);
1262 // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
1263 $tags = get_tags($body);
1266 foreach ($tags as $tag) {
1267 if (strstr(trim($tag), " ")) {
1271 if (strpos($tag, '#') === 0) {
1272 if (strpos($tag, '[url=')) {
1276 // don't link tags that are already embedded in links
1277 if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
1280 if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
1284 $basetag = str_replace('_', ' ', substr($tag, 1));
1285 $url = '#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1286 $body = str_replace($tag, $url, $body);
1287 $tags_arr["#" . $basetag] = $url;
1288 } elseif (strpos($tag, '@') === 0) {
1289 if (strpos($tag, '[url=')) {
1293 $basetag = substr($tag, 1);
1294 $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1295 $body = str_replace($tag, $url, $body);
1296 $tags_arr["@" . $basetag] = $url;
1301 $tags = implode($tags_arr, ",");
1303 return ["body" => $body, "tags" => $tags, "plain" => $plain];
1307 * @brief Fetch media entities and add media links to the body
1309 * @param object $post Twitter object with the post
1310 * @param array $postarray Array of the item that is about to be posted
1312 * @return $picture string Returns a a single picture string if it isn't a media post
1314 function twitter_media_entities($post, &$postarray)
1316 // There are no media entities? So we quit.
1317 if (!is_array($post->extended_entities->media)) {
1321 // When the post links to an external page, we only take one picture.
1322 // We only do this when there is exactly one media.
1323 if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
1325 foreach ($post->extended_entities->media AS $medium) {
1326 if (isset($medium->media_url_https)) {
1327 $picture = $medium->media_url_https;
1328 $postarray['body'] = str_replace($medium->url, "", $postarray['body']);
1334 // This is a pure media post, first search for all media urls
1336 foreach ($post->extended_entities->media AS $medium) {
1337 switch ($medium->type) {
1339 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1340 $postarray['object-type'] = ACTIVITY_OBJ_IMAGE;
1343 case 'animated_gif':
1344 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1345 $postarray['object-type'] = ACTIVITY_OBJ_VIDEO;
1346 if (is_array($medium->video_info->variants)) {
1348 // We take the video with the highest bitrate
1349 foreach ($medium->video_info->variants AS $variant) {
1350 if (($variant->content_type == "video/mp4") && ($variant->bitrate >= $bitrate)) {
1351 $media[$medium->url] = "\n[video]" . $variant->url . "[/video]";
1352 $bitrate = $variant->bitrate;
1357 // The following code will only be activated for test reasons
1359 // $postarray['body'] .= print_r($medium, true);
1363 // Now we replace the media urls.
1364 foreach ($media AS $key => $value) {
1365 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1370 function twitter_createpost(App $a, $uid, $post, $self, $create_user, $only_existing_contact, $noquote)
1373 $postarray['network'] = NETWORK_TWITTER;
1374 $postarray['gravity'] = 0;
1375 $postarray['uid'] = $uid;
1376 $postarray['wall'] = 0;
1377 $postarray['uri'] = "twitter::" . $post->id_str;
1378 $postarray['object'] = json_encode($post);
1380 // Don't import our own comments
1381 $r = q("SELECT * FROM `item` WHERE `extid` = '%s' AND `uid` = %d LIMIT 1",
1382 dbesc($postarray['uri']),
1387 logger("Item with extid " . $postarray['uri'] . " found.", LOGGER_DEBUG);
1393 if ($post->in_reply_to_status_id_str != "") {
1394 $parent = "twitter::" . $post->in_reply_to_status_id_str;
1396 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1401 $postarray['thr-parent'] = $r[0]["uri"];
1402 $postarray['parent-uri'] = $r[0]["parent-uri"];
1403 $postarray['parent'] = $r[0]["parent"];
1404 $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1406 $r = q("SELECT * FROM `item` WHERE `extid` = '%s' AND `uid` = %d LIMIT 1",
1411 $postarray['thr-parent'] = $r[0]['uri'];
1412 $postarray['parent-uri'] = $r[0]['parent-uri'];
1413 $postarray['parent'] = $r[0]['parent'];
1414 $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1416 $postarray['thr-parent'] = $postarray['uri'];
1417 $postarray['parent-uri'] = $postarray['uri'];
1418 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1423 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1425 if ($post->user->id_str == $own_id) {
1426 $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1430 $contactid = $r[0]["id"];
1432 $postarray['owner-name'] = $r[0]["name"];
1433 $postarray['owner-link'] = $r[0]["url"];
1434 $postarray['owner-avatar'] = $r[0]["photo"];
1436 logger("No self contact for user " . $uid, LOGGER_DEBUG);
1440 // Don't create accounts of people who just comment something
1441 $create_user = false;
1443 $postarray['parent-uri'] = $postarray['uri'];
1444 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1447 if ($contactid == 0) {
1448 $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
1450 $postarray['owner-name'] = $post->user->name;
1451 $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
1452 $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
1455 if (($contactid == 0) && !$only_existing_contact) {
1456 $contactid = $self['id'];
1457 } elseif ($contactid <= 0) {
1458 logger("Contact ID is zero or less than zero.", LOGGER_DEBUG);
1462 $postarray['contact-id'] = $contactid;
1464 $postarray['verb'] = ACTIVITY_POST;
1465 $postarray['author-name'] = $postarray['owner-name'];
1466 $postarray['author-link'] = $postarray['owner-link'];
1467 $postarray['author-avatar'] = $postarray['owner-avatar'];
1468 $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
1469 $postarray['app'] = strip_tags($post->source);
1471 if ($post->user->protected) {
1472 $postarray['private'] = 1;
1473 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1476 if (is_string($post->full_text)) {
1477 $postarray['body'] = $post->full_text;
1479 $postarray['body'] = $post->text;
1482 // When the post contains links then use the correct object type
1483 if (count($post->entities->urls) > 0) {
1484 $postarray['object-type'] = ACTIVITY_OBJ_BOOKMARK;
1487 // Search for media links
1488 $picture = twitter_media_entities($post, $postarray);
1490 $converted = twitter_expand_entities($a, $postarray['body'], $post, false, $picture);
1491 $postarray['body'] = $converted["body"];
1492 $postarray['tag'] = $converted["tags"];
1493 $postarray['created'] = Temporal::utc($post->created_at);
1494 $postarray['edited'] = Temporal::utc($post->created_at);
1496 $statustext = $converted["plain"];
1498 if (is_string($post->place->name)) {
1499 $postarray["location"] = $post->place->name;
1501 if (is_string($post->place->full_name)) {
1502 $postarray["location"] = $post->place->full_name;
1504 if (is_array($post->geo->coordinates)) {
1505 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1507 if (is_array($post->coordinates->coordinates)) {
1508 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1510 if (is_object($post->retweeted_status)) {
1511 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1513 $retweet['object'] = $postarray['object'];
1514 $retweet['private'] = $postarray['private'];
1515 $retweet['allow_cid'] = $postarray['allow_cid'];
1516 $retweet['contact-id'] = $postarray['contact-id'];
1517 $retweet['owner-name'] = $postarray['owner-name'];
1518 $retweet['owner-link'] = $postarray['owner-link'];
1519 $retweet['owner-avatar'] = $postarray['owner-avatar'];
1521 $postarray = $retweet;
1524 if (is_object($post->quoted_status) && !$noquote) {
1525 $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
1527 $postarray['body'] = $statustext;
1529 $postarray['body'] .= "\n" . share_header(
1530 $quoted['author-name'],
1531 $quoted['author-link'],
1532 $quoted['author-avatar'],
1538 $postarray['body'] .= $quoted['body'] . '[/share]';
1544 function twitter_checknotification(App $a, $uid, $own_id, $top_item, $postarray)
1546 /// TODO: this whole function doesn't seem to work. Needs complete check
1547 $user = q("SELECT * FROM `contact` WHERE `uid` = %d AND `self` LIMIT 1",
1551 if (!count($user)) {
1556 if (link_compare($user[0]["url"], $postarray['author-link'])) {
1560 $own_user = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1562 dbesc("twitter::".$own_id)
1565 if (!count($own_user)) {
1569 // Is it me from twitter?
1570 if (link_compare($own_user[0]["url"], $postarray['author-link'])) {
1574 $myconv = q("SELECT `author-link`, `author-avatar`, `parent` FROM `item` WHERE `parent-uri` = '%s' AND `uid` = %d AND `parent` != 0 AND `deleted` = 0",
1575 dbesc($postarray['parent-uri']),
1579 if (count($myconv)) {
1580 foreach ($myconv as $conv) {
1581 // now if we find a match, it means we're in this conversation
1582 if (!link_compare($conv['author-link'], $user[0]["url"]) && !link_compare($conv['author-link'], $own_user[0]["url"])) {
1586 require_once 'include/enotify.php';
1588 $conv_parent = $conv['parent'];
1591 'type' => NOTIFY_COMMENT,
1592 'notify_flags' => $user[0]['notify-flags'],
1593 'language' => $user[0]['language'],
1594 'to_name' => $user[0]['username'],
1595 'to_email' => $user[0]['email'],
1596 'uid' => $user[0]['uid'],
1597 'item' => $postarray,
1598 'link' => $a->get_baseurl() . '/display/' . urlencode(Item::getGuidById($top_item)),
1599 'source_name' => $postarray['author-name'],
1600 'source_link' => $postarray['author-link'],
1601 'source_photo' => $postarray['author-avatar'],
1602 'verb' => ACTIVITY_POST,
1604 'parent' => $conv_parent,
1607 // only send one notification
1613 function twitter_fetchparentposts(App $a, $uid, $post, $connection, $self, $own_id)
1615 logger("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, LOGGER_DEBUG);
1619 while ($post->in_reply_to_status_id_str != "") {
1620 $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str];
1622 $post = $connection->get('statuses/show', $parameters);
1624 if (!count($post)) {
1625 logger("twitter_fetchparentposts: Can't fetch post " . $parameters->id, LOGGER_DEBUG);
1629 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1630 dbesc("twitter::".$post->id_str),
1641 logger("twitter_fetchparentposts: Fetching " . count($posts) . " parents", LOGGER_DEBUG);
1643 $posts = array_reverse($posts);
1645 if (count($posts)) {
1646 foreach ($posts as $post) {
1647 $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1649 if (trim($postarray['body']) == "") {
1653 $item = Item::insert($postarray);
1654 $postarray["id"] = $item;
1656 logger('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1658 if ($item && !function_exists("check_item_notification")) {
1659 twitter_checknotification($a, $uid, $own_id, $item, $postarray);
1665 function twitter_fetchhometimeline(App $a, $uid)
1667 $ckey = Config::get('twitter', 'consumerkey');
1668 $csecret = Config::get('twitter', 'consumersecret');
1669 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1670 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1671 $create_user = PConfig::get($uid, 'twitter', 'create_user');
1672 $mirror_posts = PConfig::get($uid, 'twitter', 'mirror_posts');
1674 logger("twitter_fetchhometimeline: Fetching for user " . $uid, LOGGER_DEBUG);
1676 $application_name = Config::get('twitter', 'application_name');
1678 if ($application_name == "") {
1679 $application_name = $a->get_hostname();
1682 require_once 'include/items.php';
1684 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1686 $own_contact = twitter_fetch_own_contact($a, $uid);
1688 $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1689 intval($own_contact),
1693 $own_id = $r[0]["nick"];
1695 logger("twitter_fetchhometimeline: Own twitter contact not found for user " . $uid, LOGGER_DEBUG);
1699 $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1705 logger("twitter_fetchhometimeline: Own contact not found for user " . $uid, LOGGER_DEBUG);
1709 $u = q("SELECT * FROM user WHERE uid = %d LIMIT 1",
1712 logger("twitter_fetchhometimeline: Own user not found for user " . $uid, LOGGER_DEBUG);
1716 $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
1717 //$parameters["count"] = 200;
1718 // Fetching timeline
1719 $lastid = PConfig::get($uid, 'twitter', 'lasthometimelineid');
1721 $first_time = ($lastid == "");
1723 if ($lastid != "") {
1724 $parameters["since_id"] = $lastid;
1727 $items = $connection->get('statuses/home_timeline', $parameters);
1729 if (!is_array($items)) {
1730 logger("twitter_fetchhometimeline: Error fetching home timeline: " . print_r($items, true), LOGGER_DEBUG);
1734 $posts = array_reverse($items);
1736 logger("twitter_fetchhometimeline: Fetching timeline for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1738 if (count($posts)) {
1739 foreach ($posts as $post) {
1740 if ($post->id_str > $lastid) {
1741 $lastid = $post->id_str;
1742 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1749 if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1750 logger("twitter_fetchhometimeline: Skip previously sended post", LOGGER_DEBUG);
1754 if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1755 logger("twitter_fetchhometimeline: Skip post that will be mirrored", LOGGER_DEBUG);
1759 if ($post->in_reply_to_status_id_str != "") {
1760 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1763 $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1765 if (trim($postarray['body']) == "") {
1769 $item = Item::insert($postarray);
1770 $postarray["id"] = $item;
1772 logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted home timeline item ' . $item);
1774 if ($item && !function_exists("check_item_notification")) {
1775 twitter_checknotification($a, $uid, $own_id, $item, $postarray);
1779 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1781 // Fetching mentions
1782 $lastid = PConfig::get($uid, 'twitter', 'lastmentionid');
1784 $first_time = ($lastid == "");
1786 if ($lastid != "") {
1787 $parameters["since_id"] = $lastid;
1790 $items = $connection->get('statuses/mentions_timeline', $parameters);
1792 if (!is_array($items)) {
1793 logger("twitter_fetchhometimeline: Error fetching mentions: " . print_r($items, true), LOGGER_DEBUG);
1797 $posts = array_reverse($items);
1799 logger("twitter_fetchhometimeline: Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1801 if (count($posts)) {
1802 foreach ($posts as $post) {
1803 if ($post->id_str > $lastid) {
1804 $lastid = $post->id_str;
1811 if ($post->in_reply_to_status_id_str != "") {
1812 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1815 $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1817 if (trim($postarray['body']) == "") {
1821 $item = Item::insert($postarray);
1822 $postarray["id"] = $item;
1824 if ($item && function_exists("check_item_notification")) {
1825 check_item_notification($item, $uid, NOTIFY_TAGSELF);
1828 if (!isset($postarray["parent"]) || ($postarray["parent"] == 0)) {
1829 $postarray["parent"] = $item;
1832 logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted mention timeline item ' . $item);
1835 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1836 dbesc($postarray['uri']),
1840 $item = $r[0]['id'];
1841 $parent_id = $r[0]['parent'];
1844 $parent_id = $postarray['parent'];
1847 if (($item != 0) && !function_exists("check_item_notification")) {
1848 require_once 'include/enotify.php';
1850 'type' => NOTIFY_TAGSELF,
1851 'notify_flags' => $u[0]['notify-flags'],
1852 'language' => $u[0]['language'],
1853 'to_name' => $u[0]['username'],
1854 'to_email' => $u[0]['email'],
1855 'uid' => $u[0]['uid'],
1856 'item' => $postarray,
1857 'link' => $a->get_baseurl() . '/display/' . urlencode(Item::getGuidById($item)),
1858 'source_name' => $postarray['author-name'],
1859 'source_link' => $postarray['author-link'],
1860 'source_photo' => $postarray['author-avatar'],
1861 'verb' => ACTIVITY_TAG,
1863 'parent' => $parent_id
1869 PConfig::set($uid, 'twitter', 'lastmentionid', $lastid);
1872 function twitter_fetch_own_contact(App $a, $uid)
1874 $ckey = Config::get('twitter', 'consumerkey');
1875 $csecret = Config::get('twitter', 'consumersecret');
1876 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1877 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1879 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1883 if ($own_id == "") {
1884 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1886 // Fetching user data
1887 $user = $connection->get('account/verify_credentials');
1889 PConfig::set($uid, 'twitter', 'own_id', $user->id_str);
1891 $contact_id = twitter_fetch_contact($uid, $user, true);
1893 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1895 dbesc("twitter::" . $own_id));
1897 $contact_id = $r[0]["id"];
1899 PConfig::delete($uid, 'twitter', 'own_id');
1906 function twitter_is_retweet(App $a, $uid, $body)
1908 $body = trim($body);
1910 // Skip if it isn't a pure repeated messages
1911 // Does it start with a share?
1912 if (strpos($body, "[share") > 0) {
1916 // Does it end with a share?
1917 if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1921 $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1922 // Skip if there is no shared message in there
1923 if ($body == $attributes) {
1928 preg_match("/link='(.*?)'/ism", $attributes, $matches);
1929 if ($matches[1] != "") {
1930 $link = $matches[1];
1933 preg_match('/link="(.*?)"/ism', $attributes, $matches);
1934 if ($matches[1] != "") {
1935 $link = $matches[1];
1938 $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
1943 logger('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, LOGGER_DEBUG);
1945 $ckey = Config::get('twitter', 'consumerkey');
1946 $csecret = Config::get('twitter', 'consumersecret');
1947 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1948 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1950 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1951 $result = $connection->post('statuses/retweet/' . $id);
1953 logger('twitter_is_retweet: result ' . print_r($result, true), LOGGER_DEBUG);
1955 return !isset($result->errors);