use Abraham\TwitterOAuth\TwitterOAuthException;
use Codebird\Codebird;
use Friendica\App;
-use Friendica\Content\OEmbed;
-use Friendica\Content\PageInfo;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\Plaintext;
use Friendica\Core\Hook;
use Friendica\Model\Conversation;
use Friendica\Model\Group;
use Friendica\Model\Item;
-use Friendica\Model\ItemContent;
use Friendica\Model\ItemURI;
+use Friendica\Model\Post;
use Friendica\Model\Tag;
use Friendica\Model\User;
use Friendica\Protocol\Activity;
Hook::register('prepare_body' , __FILE__, 'twitter_prepare_body');
Hook::register('check_item_notification', __FILE__, 'twitter_check_item_notification');
Hook::register('probe_detect' , __FILE__, 'twitter_probe_detect');
+ Hook::register('parse_link' , __FILE__, 'twitter_parse_link');
Logger::info("installed twitter");
}
} else {
$s .= '<div id="twitter-info" >
<p>Invalid Twitter info</p>
- </div>';
- Logger::info('Invalid twitter info (verify credentials).', ['auth' => TwitterOAuth::class]);
+ <button type="submit" name="twitter-disconnect" value="1">' . DI::l10n()->t('Disconnect') . '</button>
+ </div>';
+ Logger::notice('Invalid twitter info (verify credentials).', ['auth' => TwitterOAuth::class]);
}
$s .= '<div class="clear"></div>';
}
// if post comes from twitter don't send it back
- if ($post['extid'] == Protocol::TWITTER) {
+ if (($post['extid'] == Protocol::TWITTER) || twitter_get_id($post['extid'])) {
$b['execute'] = false;
return;
}
- if ($post['app'] == 'Twitter') {
+ if (substr($post['app'], 0, 7) == 'Twitter') {
$b['execute'] = false;
return;
}
if (DI::pConfig()->get($post['uid'], 'twitter', 'import')) {
// Don't fork if it isn't a reply to a twitter post
- if (($post['parent'] != $post['id']) && !Item::exists(['id' => $post['parent'], 'network' => Protocol::TWITTER])) {
+ if (($post['parent'] != $post['id']) && !Post::exists(['id' => $post['parent'], 'network' => Protocol::TWITTER])) {
Logger::notice('No twitter parent found', ['item' => $post['id']]);
$b['execute'] = false;
return;
function twitter_action(App $a, $uid, $pid, $action)
{
+ if (empty($pid)) {
+ return;
+ }
+
$ckey = DI::config()->get('twitter', 'consumerkey');
$csecret = DI::config()->get('twitter', 'consumersecret');
$otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
Logger::debug('before action', ['action' => $action, 'pid' => $pid, 'data' => $post]);
- switch ($action) {
- case 'delete':
- // To-Do: $result = $connection->post('statuses/destroy', $post);
- $result = [];
- break;
- case 'like':
- $result = $connection->post('favorites/create', $post);
- if ($connection->getLastHttpCode() != 200) {
- Logger::error('Unable to create favorite', ['result' => $result]);
- }
- break;
- case 'unlike':
- $result = $connection->post('favorites/destroy', $post);
- if ($connection->getLastHttpCode() != 200) {
- Logger::error('Unable to destroy favorite', ['result' => $result]);
- }
- break;
- default:
- Logger::warning('Unhandled action', ['action' => $action]);
- $result = [];
+ try {
+ switch ($action) {
+ case 'delete':
+ // To-Do: $result = $connection->post('statuses/destroy', $post);
+ $result = [];
+ break;
+ case 'like':
+ $result = $connection->post('favorites/create', $post);
+ if ($connection->getLastHttpCode() != 200) {
+ Logger::warning('Unable to create favorite', ['result' => $result]);
+ }
+ break;
+ case 'unlike':
+ $result = $connection->post('favorites/destroy', $post);
+ if ($connection->getLastHttpCode() != 200) {
+ Logger::warning('Unable to destroy favorite', ['result' => $result]);
+ }
+ break;
+ default:
+ Logger::warning('Unhandled action', ['action' => $action]);
+ $result = [];
+ }
+ } catch (TwitterOAuthException $twitterOAuthException) {
+ Logger::warning('Unable to communicate with twitter', ['action' => $action, 'data' => $post, 'code' => $twitterOAuthException->getCode(), 'exception' => $twitterOAuthException]);
}
Logger::info('after action', ['action' => $action, 'result' => $result]);
}
+function twitter_get_id(string $uri)
+{
+ if ((substr($uri, 0, 9) != 'twitter::') || (strlen($uri) <= 9)) {
+ return 0;
+ }
+
+ $id = substr($uri, 9);
+ if (!is_numeric($id)) {
+ return 0;
+ }
+
+ return (int)$id;
+}
+
function twitter_post_hook(App $a, array &$b)
{
// Post to Twitter
return;
}
+ $b['body'] = Post\Media::addAttachmentsToBody($b['uri-id'], $b['body']);
+
+ $thr_parent = null;
+
if ($b['parent'] != $b['id']) {
- Logger::log("twitter_post_hook: parameter " . print_r($b, true), Logger::DATA);
+ Logger::debug('Got comment', ['item' => $b]);
// Looking if its a reply to a twitter post
- if ((substr($b["parent-uri"], 0, 9) != "twitter::")
- && (substr($b["extid"], 0, 9) != "twitter::")
- && (substr($b["thr-parent"], 0, 9) != "twitter::"))
- {
- Logger::log("twitter_post_hook: no twitter post " . $b["parent"]);
+ if (!twitter_get_id($b["parent-uri"]) &&
+ !twitter_get_id($b["extid"]) &&
+ !twitter_get_id($b["thr-parent"])) {
+ Logger::info('No twitter post', ['parent' => $b["parent"]]);
return;
}
$condition = ['uri' => $b["thr-parent"], 'uid' => $b["uid"]];
- $orig_post = Item::selectFirst([], $condition);
- if (!DBA::isResult($orig_post)) {
- Logger::log("twitter_post_hook: no parent found " . $b["thr-parent"]);
+ $thr_parent = Post::selectFirst(['uri', 'extid', 'author-link', 'author-nick', 'author-network'], $condition);
+ if (!DBA::isResult($thr_parent)) {
+ Logger::warning('No parent found', ['thr-parent' => $b["thr-parent"]]);
return;
- } else {
- $iscomment = true;
}
+ if ($thr_parent['author-network'] == Protocol::TWITTER) {
+ $nickname = '@[url=' . $thr_parent['author-link'] . ']' . $thr_parent['author-nick'] . '[/url]';
+ $nicknameplain = '@' . $thr_parent['author-nick'];
- $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
- $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
- $nicknameplain = "@" . $nicknameplain;
-
- Logger::log("twitter_post_hook: comparing " . $nickname . " and " . $nicknameplain . " with " . $b["body"], Logger::DEBUG);
- if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) {
- $b["body"] = $nickname . " " . $b["body"];
+ Logger::info('Comparing', ['nickname' => $nickname, 'nicknameplain' => $nicknameplain, 'body' => $b["body"]]);
+ if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) {
+ $b["body"] = $nickname . " " . $b["body"];
+ }
}
- Logger::log("twitter_post_hook: parent found " . print_r($orig_post, true), Logger::DATA);
+ Logger::debug('Parent found', ['parent' => $thr_parent]);
} else {
- $iscomment = false;
-
if ($b['private'] || !strstr($b['postopts'], 'twitter')) {
return;
}
}
if (($b['verb'] == Activity::POST) && $b['deleted']) {
- twitter_action($a, $b["uid"], substr($orig_post["uri"], 9), "delete");
+ twitter_action($a, $b['uid'], twitter_get_id($thr_parent['uri']), 'delete');
}
if ($b['verb'] == Activity::LIKE) {
- Logger::log("twitter_post_hook: parameter 2 " . substr($b["thr-parent"], 9), Logger::DEBUG);
+ Logger::info('Like', ['uid' => $b['uid'], 'id' => twitter_get_id($b["thr-parent"])]);
+ if ($b['deleted']) {
+ twitter_action($a, $b["uid"], twitter_get_id($b["thr-parent"]), "unlike");
+ } else {
+ twitter_action($a, $b["uid"], twitter_get_id($b["thr-parent"]), "like");
+ }
+
+ return;
+ }
+
+ if ($b['verb'] == Activity::ANNOUNCE) {
+ Logger::info('Retweet', ['uid' => $b['uid'], 'id' => twitter_get_id($b["thr-parent"])]);
if ($b['deleted']) {
- twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "unlike");
+ twitter_action($a, $b['uid'], twitter_get_id($thr_parent['extid']), 'delete');
} else {
- twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "like");
+ twitter_retweet($b["uid"], twitter_get_id($b["thr-parent"]));
}
return;
}
// if post comes from twitter don't send it back
- if ($b['extid'] == Protocol::TWITTER) {
+ if (($b['extid'] == Protocol::TWITTER) || twitter_get_id($b['extid'])) {
return;
}
$osecret = DI::pConfig()->get($b['uid'], 'twitter', 'oauthsecret');
if ($ckey && $csecret && $otoken && $osecret) {
- Logger::log('twitter: we have customer key and oauth stuff, going to send.', Logger::DEBUG);
+ Logger::info('We have customer key and oauth stuff, going to send.');
// If it's a repeated message from twitter then do a native retweet and exit
if (twitter_is_retweet($a, $b['uid'], $b['body'])) {
$b['body'] = twitter_update_mentions($b['body']);
- $msgarr = ItemContent::getPlaintextPost($b, $max_char, true, BBCode::TWITTER);
+ $msgarr = Plaintext::getPost($b, $max_char, true, BBCode::TWITTER);
Logger::info('Got plaintext', ['id' => $b['id'], 'message' => $msgarr]);
$msg = $msgarr["text"];
$msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
}
- if (!empty($msgarr['url']) && ($msgarr['url'] == $b['plink']) && !empty($msgarr['images']) && (count($msgarr['images']) <= 4)) {
- $url_added = false;
- } elseif (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
- $msg .= "\n" . $msgarr["url"];
- $url_added = true;
- } else {
- $url_added = false;
+ // Add the link to the body if the type isn't a photo or there are more than 4 images in the post
+ if (!empty($msgarr['url']) && (($msgarr['type'] != 'photo') || empty($msgarr['images']) || (count($msgarr['images']) > 4))) {
+ $msg .= "\n" . $msgarr['url'];
}
if (empty($msg)) {
- Logger::info('Empty message', ['id' => $b['id']]);
+ Logger::notice('Empty message', ['id' => $b['id']]);
return;
}
$data = ['media_id' => $media->media_id_string,
'alt_text' => ['text' => substr($image['description'], 0, 420)]];
$ret = $cb->media_metadata_create($data);
- Logger::info('Metadata create', ['id' => $b['id'], 'data' => $data, 'return' => json_encode($ret)]);
+ Logger::info('Metadata create', ['id' => $b['id'], 'data' => $data, 'return' => $ret]);
}
} else {
- Logger::error('Failed upload', ['id' => $b['id'], 'image' => $image['url']]);
+ Logger::error('Failed upload', ['id' => $b['id'], 'image' => $image['url'], 'return' => $media]);
throw new Exception('Failed upload of ' . $image['url']);
}
}
unset($post['media_ids']);
}
} catch (Exception $e) {
- Logger::info('Exception when trying to send to Twitter', ['id' => $b['id'], 'message' => $e->getMessage()]);
+ Logger::warning('Exception when trying to send to Twitter', ['id' => $b['id'], 'message' => $e->getMessage()]);
}
}
$post['status'] = $msg;
- if ($iscomment) {
- $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
+ if ($thr_parent) {
+ $post['in_reply_to_status_id'] = twitter_get_id($thr_parent['uri']);
}
$url = 'statuses/update';
}
if (!empty($result->errors)) {
- Logger::info('Send to Twitter failed', ['id' => $b['id'], 'error' => $result->errors]);
+ Logger::error('Send to Twitter failed', ['id' => $b['id'], 'error' => $result->errors]);
Worker::defer();
- } elseif ($iscomment) {
- Logger::info('Update extid', ['id' => $b['id'], 'extid' => $result->id_str]);
+ } elseif ($thr_parent) {
+ Logger::notice('Post send, updating extid', ['id' => $b['id'], 'extid' => $result->id_str]);
Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $b['id']]);
}
}
return;
}
- $r = Item::select(['id', 'guid'], ['deleted' => true, 'network' => Protocol::TWITTER]);
- while ($row = DBA::fetch($r)) {
+ Logger::notice('Start deleting expired posts');
+
+ $r = Post::select(['id', 'guid'], ['deleted' => true, 'network' => Protocol::TWITTER]);
+ while ($row = Post::fetch($r)) {
Logger::info('[twitter] Delete expired item', ['id' => $row['id'], 'guid' => $row['guid'], 'callstack' => \Friendica\Core\System::callstack()]);
- DBA::delete('item', ['id' => $row['id']]);
+ Item::markForDeletionById($row['id']);
}
DBA::close($r);
- Logger::notice('twitter_expire: expire_start');
+ Logger::notice('End deleting expired posts');
+
+ Logger::notice('Start expiry');
$r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
if (DBA::isResult($r)) {
}
}
- Logger::notice('twitter_expire: expire_end');
+ Logger::notice('End expiry');
}
function twitter_prepare_body(App $a, array &$b)
$item["plink"] = DI::baseUrl()->get() . "/display/" . $item["guid"];
$condition = ['uri' => $item["thr-parent"], 'uid' => local_user()];
- $orig_post = Item::selectFirst(['author-link'], $condition);
+ $orig_post = Post::selectFirst(['author-link'], $condition);
if (DBA::isResult($orig_post)) {
$nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
$nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
}
}
- $msgarr = ItemContent::getPlaintextPost($item, $max_char, true, BBCode::TWITTER);
+ $msgarr = Plaintext::getPost($item, $max_char, true, BBCode::TWITTER);
$msg = $msgarr["text"];
if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
}
}
+function twitter_statuses_show(string $id, TwitterOAuth $twitterOAuth = null)
+{
+ if ($twitterOAuth === null) {
+ $ckey = DI::config()->get('twitter', 'consumerkey');
+ $csecret = DI::config()->get('twitter', 'consumersecret');
+
+ if (empty($ckey) || empty($csecret)) {
+ return new stdClass();
+ }
+
+ $twitterOAuth = new TwitterOAuth($ckey, $csecret);
+ }
+
+ $parameters = ['trim_user' => false, 'tweet_mode' => 'extended', 'id' => $id, 'include_ext_alt_text' => true];
+
+ return $twitterOAuth->get('statuses/show', $parameters);
+}
+
+/**
+ * Parse Twitter status URLs since Twitter removed OEmbed
+ *
+ * @param App $a
+ * @param array $b Expected format:
+ * [
+ * 'url' => [URL to parse],
+ * 'format' => 'json'|'',
+ * 'text' => Output parameter
+ * ]
+ * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+ */
+function twitter_parse_link(App $a, array &$b)
+{
+ // Only handle Twitter status URLs
+ if (!preg_match('#^https?://(?:mobile\.|www\.)?twitter.com/[^/]+/status/(\d+).*#', $b['url'], $matches)) {
+ return;
+ }
+
+ $status = twitter_statuses_show($matches[1]);
+
+ if (empty($status->id)) {
+ return;
+ }
+
+ $item = twitter_createpost($a, 0, $status, [], true, false, true);
+
+ if ($b['format'] == 'json') {
+ $images = [];
+ foreach ($status->extended_entities->media ?? [] as $media) {
+ if (!empty($media->media_url_https)) {
+ $images[] = [
+ 'src' => $media->media_url_https,
+ 'width' => $media->sizes->thumb->w,
+ 'height' => $media->sizes->thumb->h,
+ ];
+ }
+ }
+
+ $b['text'] = [
+ 'data' => [
+ 'type' => 'link',
+ 'url' => $item['plink'],
+ 'title' => DI::l10n()->t('%s on Twitter', $status->user->name),
+ 'text' => BBCode::toPlaintext($item['body'], false),
+ 'images' => $images,
+ ],
+ 'contentType' => 'attachment',
+ 'success' => true,
+ ];
+ } else {
+ $b['text'] = BBCode::getShareOpeningTag(
+ $item['author-name'],
+ $item['author-link'],
+ $item['author-avatar'],
+ $item['plink'],
+ $item['created']
+ );
+ $b['text'] .= $item['body'] . '[/share]';
+ }
+}
+
+
+/*********************
+ *
+ * General functions
+ *
+ *********************/
+
+
/**
* @brief Build the item array for the mirrored post
*
*/
function twitter_do_mirrorpost(App $a, $uid, $post)
{
- $datarray['api_source'] = true;
- $datarray['profile_uid'] = $uid;
- $datarray['extid'] = Protocol::TWITTER;
- $datarray['message_id'] = Item::newURI($uid, Protocol::TWITTER . ':' . $post->id);
- $datarray['protocol'] = Conversation::PARCEL_TWITTER;
- $datarray['source'] = json_encode($post);
+ $datarray['uid'] = $uid;
+ $datarray['extid'] = 'twitter::' . $post->id;
$datarray['title'] = '';
if (!empty($post->retweeted_status)) {
$datarray['body'] = $item['body'];
}
- $datarray['source'] = $item['app'];
+ $datarray['app'] = $item['app'];
$datarray['verb'] = $item['verb'];
if (isset($item['location'])) {
$application_name = DI::baseUrl()->getHostname();
}
- $has_picture = false;
-
- require_once 'mod/item.php';
- require_once 'mod/share.php';
-
$connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
+ // Ensure to have the own contact
+ try {
+ twitter_fetch_own_contact($a, $uid);
+ } catch (TwitterOAuthException $e) {
+ Logger::warning('Error fetching own contact', ['uid' => $uid, 'message' => $e->getMessage()]);
+ return;
+ }
+
$parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended", "include_ext_alt_text" => true];
$first_time = ($lastid == "");
try {
$items = $connection->get('statuses/user_timeline', $parameters);
} catch (TwitterOAuthException $e) {
- Logger::notice('Error fetching timeline', ['user' => $uid, 'message' => $e->getMessage()]);
+ Logger::warning('Error fetching timeline', ['uid' => $uid, 'message' => $e->getMessage()]);
return;
}
$posts = array_reverse($items);
- Logger::log('Starting from ID ' . $lastid . ' for user ' . $uid, Logger::DEBUG);
+ Logger::notice('Start processing posts', ['from' => $lastid, 'user' => $uid, 'count' => count($posts)]);
if (count($posts)) {
foreach ($posts as $post) {
}
if (!stristr($post->source, $application_name)) {
- $_SESSION["authenticated"] = true;
- $_SESSION["uid"] = $uid;
+ Logger::info('Preparing mirror post', ['twitter-id' => $post->id_str, 'uid' => $uid]);
- Logger::log('Preparing Twitter ID ' . $post->id_str . ' for user ' . $uid, Logger::DEBUG);
+ $mirrorpost = twitter_do_mirrorpost($a, $uid, $post);
- $_REQUEST = twitter_do_mirrorpost($a, $uid, $post);
-
- if (empty($_REQUEST['body'])) {
+ if (empty($mirrorpost['body'])) {
continue;
}
- Logger::log('Posting Twitter ID ' . $post->id_str . ' for user ' . $uid);
+ Logger::info('Posting mirror post', ['twitter-id' => $post->id_str, 'uid' => $uid]);
- item_post($a);
+ Post\Delayed::add($mirrorpost['extid'], $mirrorpost, PRIORITY_MEDIUM, true);
}
}
}
Logger::info('Fetched friendship relation', ['user' => $uid, 'target' => $target, 'relation' => $relation]);
} catch (Throwable $e) {
- Logger::error('Error fetching friendship status', ['user' => $uid, 'target' => $target, 'message' => $e->getMessage()]);
+ Logger::warning('Error fetching friendship status', ['uid' => $uid, 'target' => $target, 'message' => $e->getMessage()]);
}
return $relation;
'location' => $data->location,
'about' => $data->description,
'photo' => twitter_fix_avatar($data->profile_image_url_https),
+ 'header' => $data->profile_banner_url ?? $data->profile_background_image_url_https,
];
return $fields;
if (!empty($cid)) {
DBA::update('contact', $fields, ['id' => $cid]);
Contact::updateAvatar($cid, $avatar);
+ } else {
+ Logger::warning('No contact found', ['fields' => $fields]);
}
$contact = DBA::selectFirst('contact', [], ['uid' => $uid, 'alias' => "twitter::" . $data->id_str]);
- if (!DBA::isResult($contact) && !$create_user) {
- Logger::info('User contact not found', ['uid' => $uid, 'twitter-id' => $data->id_str]);
+ if (!DBA::isResult($contact) && empty($cid)) {
+ Logger::warning('User contact not found', ['uid' => $uid, 'twitter-id' => $data->id_str]);
return 0;
+ } elseif (!$create_user) {
+ return $cid;
}
if (!DBA::isResult($contact)) {
Contact::updateAvatar($contact_id, $avatar);
} else {
if ($contact["readonly"] || $contact["blocked"]) {
- Logger::log("twitter_fetch_contact: Contact '" . $contact["nick"] . "' is blocked or readonly.", Logger::DEBUG);
+ Logger::notice('Contact is blocked or readonly.', ['nickname' => $contact["nick"]]);
return -1;
}
$parameters = ['screen_name' => $screen_name];
$user = $connection->get('users/show', $parameters);
} catch (TwitterOAuthException $e) {
- Logger::log('twitter_fetchuser: Error fetching user ' . $screen_name . ': ' . $e->getMessage());
+ Logger::warning('Error fetching user', ['user' => $screen_name, 'message' => $e->getMessage()]);
return null;
}
*
* @param string $body
* @param stdClass $status
- * @param string $picture
* @return array
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
-function twitter_expand_entities($body, stdClass $status, $picture)
+function twitter_expand_entities($body, stdClass $status)
{
$plain = $body;
+ $contains_urls = false;
$taglist = [];
];
}
- // This URL if set will be used to add an attachment at the bottom of the post
- $attachmentUrl = '';
-
foreach ($status->entities->urls ?? [] as $url) {
$plain = str_replace($url->url, '', $plain);
if ($url->url && $url->expanded_url && $url->display_url) {
-
// Quote tweet, we just remove the quoted tweet URL from the body, the share block will be added later.
if (!empty($status->quoted_status) && isset($status->quoted_status_id_str)
&& substr($url->expanded_url, -strlen($status->quoted_status_id_str)) == $status->quoted_status_id_str
continue;
}
- $expanded_url = $url->expanded_url;
-
- $final_url = DI::httpRequest()->finalUrl($url->expanded_url);
-
- $oembed_data = OEmbed::fetchURL($final_url);
+ $contains_urls = true;
- if (empty($oembed_data) || empty($oembed_data->type)) {
- continue;
- }
+ $expanded_url = $url->expanded_url;
// Quickfix: Workaround for URL with '[' and ']' in it
if (strpos($expanded_url, '[') || strpos($expanded_url, ']')) {
$expanded_url = $url->url;
}
- if ($oembed_data->type == 'video') {
- $attachmentUrl = $expanded_url;
- $replace = '';
- } elseif (($oembed_data->type == 'photo') && isset($oembed_data->url)) {
- $replace = '[url=' . $expanded_url . '][img]' . $oembed_data->url . '[/img][/url]';
- } elseif ($oembed_data->type != 'link') {
- $replace = '[url=' . $expanded_url . ']' . $url->display_url . '[/url]';
- } else {
- $img_str = DI::httpRequest()->fetch($final_url, true, 4);
-
- $tempfile = tempnam(get_temppath(), 'cache');
- file_put_contents($tempfile, $img_str);
-
- // See http://php.net/manual/en/function.exif-imagetype.php#79283
- if (filesize($tempfile) > 11) {
- $mime = image_type_to_mime_type(exif_imagetype($tempfile));
- } else {
- $mime = false;
- }
-
- unlink($tempfile);
-
- if (substr($mime, 0, 6) == 'image/') {
- $replace = '[img]' . $final_url . '[/img]';
- } else {
- $attachmentUrl = $expanded_url;
- $replace = '';
- }
- }
-
$replacementList[$url->indices[0]] = [
- 'replace' => $replace,
+ 'replace' => '[url=' . $expanded_url . ']' . $url->display_url . '[/url]',
'length' => $url->indices[1] - $url->indices[0],
];
}
$body = Strings::substringReplace($body, $parameters['replace'], $startIndex, $parameters['length']);
}
- // Footer will be taken care of with a share block in the case of a quote
- if (empty($status->quoted_status)) {
- $footer = '';
- if ($attachmentUrl) {
- $footer = "\n" . PageInfo::getFooterFromUrl($attachmentUrl, false, $picture);
- }
+ $body = trim($body);
- if (trim($footer)) {
- $body .= $footer;
- } elseif ($picture) {
- $body .= "\n\n[img]" . $picture . "[/img]\n";
- } else {
- $body = PageInfo::searchAndAppendToBody($body);
+ return ['body' => trim($body), 'plain' => trim($plain), 'taglist' => $taglist, 'urls' => $contains_urls];
+}
+
+/**
+ * Store entity attachments
+ *
+ * @param integer $uriid
+ * @param object $post Twitter object with the post
+ */
+function twitter_store_attachments(int $uriid, $post)
+{
+ if (!empty($post->extended_entities->media)) {
+ foreach ($post->extended_entities->media AS $medium) {
+ switch ($medium->type) {
+ case 'photo':
+ $attachment = ['uri-id' => $uriid, 'type' => Post\Media::IMAGE];
+
+ $attachment['url'] = $medium->media_url_https . '?name=large';
+ $attachment['width'] = $medium->sizes->large->w;
+ $attachment['height'] = $medium->sizes->large->h;
+
+ if ($medium->sizes->small->w != $attachment['width']) {
+ $attachment['preview'] = $medium->media_url_https . '?name=small';
+ $attachment['preview-width'] = $medium->sizes->small->w;
+ $attachment['preview-height'] = $medium->sizes->small->h;
+ }
+
+ $attachment['name'] = $medium->display_url ?? null;
+ $attachment['description'] = $medium->ext_alt_text ?? null;
+ Logger::debug('Photo attachment', ['attachment' => $attachment]);
+ Post\Media::insert($attachment);
+ break;
+ case 'video':
+ case 'animated_gif':
+ $attachment = ['uri-id' => $uriid, 'type' => Post\Media::VIDEO];
+ if (is_array($medium->video_info->variants)) {
+ $bitrate = 0;
+ // We take the video with the highest bitrate
+ foreach ($medium->video_info->variants AS $variant) {
+ if (($variant->content_type == 'video/mp4') && ($variant->bitrate >= $bitrate)) {
+ $attachment['url'] = $variant->url;
+ $bitrate = $variant->bitrate;
+ }
+ }
+ }
+
+ $attachment['name'] = $medium->display_url ?? null;
+ $attachment['preview'] = $medium->media_url_https . ':small';
+ $attachment['preview-width'] = $medium->sizes->small->w;
+ $attachment['preview-height'] = $medium->sizes->small->h;
+ $attachment['description'] = $medium->ext_alt_text ?? null;
+ Logger::debug('Video attachment', ['attachment' => $attachment]);
+ Post\Media::insert($attachment);
+ break;
+ default:
+ Logger::notice('Unknown media type', ['medium' => $medium]);
+ }
}
}
- return ['body' => $body, 'plain' => trim($plain), 'taglist' => $taglist];
+ if (!empty($post->entities->urls)) {
+ foreach ($post->entities->urls as $url) {
+ $attachment = ['uri-id' => $uriid, 'type' => Post\Media::UNKNOWN, 'url' => $url->expanded_url, 'name' => $url->display_url];
+ Logger::debug('Attached link', ['attachment' => $attachment]);
+ Post\Media::insert($attachment);
+ }
+ }
}
/**
* @brief Fetch media entities and add media links to the body
*
- * @param object $post Twitter object with the post
- * @param array $postarray Array of the item that is about to be posted
- *
- * @return $picture string Image URL or empty string
+ * @param object $post Twitter object with the post
+ * @param array $postarray Array of the item that is about to be posted
+ * @param integer $uriid URI Id used to store tags. -1 = don't store tags for this post.
*/
-function twitter_media_entities($post, array &$postarray)
+function twitter_media_entities($post, array &$postarray, int $uriid = -1)
{
// There are no media entities? So we quit.
if (empty($post->extended_entities->media)) {
- return '';
- }
-
- // When the post links to an external page, we only take one picture.
- // We only do this when there is exactly one media.
- if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
- $medium = $post->extended_entities->media[0];
- $picture = '';
- foreach ($post->entities->urls as $link) {
- // Let's make sure the external link url matches the media url
- if ($medium->url == $link->url && isset($medium->media_url_https)) {
- $picture = $medium->media_url_https;
- $postarray['body'] = str_replace($medium->url, '', $postarray['body']);
- return $picture;
- }
- }
+ return;
}
// This is a pure media post, first search for all media urls
}
$postarray['object-type'] = Activity\ObjectType::IMAGE;
+ $postarray['post-type'] = Item::PT_IMAGE;
break;
case 'video':
+ // Currently deactivated, since this causes the video to be display before the content
+ // We have to figure out a better way for declaring the post type and the display style.
+ //$postarray['post-type'] = Item::PT_VIDEO;
case 'animated_gif':
if (!empty($medium->ext_alt_text)) {
Logger::info('Got text description', ['alt_text' => $medium->ext_alt_text]);
}
}
break;
- // The following code will only be activated for test reasons
- //default:
- // $postarray['body'] .= print_r($medium, true);
}
}
+ if ($uriid != -1) {
+ foreach ($media AS $key => $value) {
+ $postarray['body'] = str_replace($key, '', $postarray['body']);
+ }
+ return;
+ }
+
// Now we replace the media urls.
foreach ($media AS $key => $value) {
$postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
}
-
- return '';
}
/**
$postarray['uri'] = "twitter::" . $post->id_str;
$postarray['protocol'] = Conversation::PARCEL_TWITTER;
$postarray['source'] = json_encode($post);
+ $postarray['direction'] = Conversation::PULL;
if (empty($uriid)) {
$uriid = $postarray['uri-id'] = ItemURI::insert(['uri' => $postarray['uri']]);
}
// Don't import our own comments
- if (Item::exists(['extid' => $postarray['uri'], 'uid' => $uid])) {
- Logger::log("Item with extid " . $postarray['uri'] . " found.", Logger::DEBUG);
+ if (Post::exists(['extid' => $postarray['uri'], 'uid' => $uid])) {
+ Logger::info('Item found', ['extid' => $postarray['uri']]);
return [];
}
$contactid = 0;
if ($post->in_reply_to_status_id_str != "") {
- $parent = "twitter::" . $post->in_reply_to_status_id_str;
+ $thr_parent = "twitter::" . $post->in_reply_to_status_id_str;
- $fields = ['uri', 'parent-uri', 'parent'];
- $parent_item = Item::selectFirst($fields, ['uri' => $parent, 'uid' => $uid]);
- if (!DBA::isResult($parent_item)) {
- $parent_item = Item::selectFirst($fields, ['extid' => $parent, 'uid' => $uid]);
+ $item = Post::selectFirst(['uri'], ['uri' => $thr_parent, 'uid' => $uid]);
+ if (!DBA::isResult($item)) {
+ $item = Post::selectFirst(['uri'], ['extid' => $thr_parent, 'uid' => $uid]);
}
- if (DBA::isResult($parent_item)) {
- $postarray['thr-parent'] = $parent_item['uri'];
- $postarray['parent-uri'] = $parent_item['parent-uri'];
- $postarray['parent'] = $parent_item['parent'];
+ if (DBA::isResult($item)) {
+ $postarray['thr-parent'] = $item['uri'];
$postarray['object-type'] = Activity\ObjectType::COMMENT;
} else {
- $postarray['thr-parent'] = $postarray['uri'];
- $postarray['parent-uri'] = $postarray['uri'];
$postarray['object-type'] = Activity\ObjectType::NOTE;
}
$postarray['owner-link'] = $r[0]["url"];
$postarray['owner-avatar'] = $r[0]["photo"];
} else {
- Logger::log("No self contact for user " . $uid, Logger::DEBUG);
+ Logger::error('No self contact found', ['uid' => $uid]);
return [];
}
}
// Don't create accounts of people who just comment something
$create_user = false;
} else {
- $postarray['parent-uri'] = $postarray['uri'];
$postarray['object-type'] = Activity\ObjectType::NOTE;
}
if (($contactid == 0) && !$only_existing_contact) {
$contactid = $self['id'];
} elseif ($contactid <= 0) {
- Logger::log("Contact ID is zero or less than zero.", Logger::DEBUG);
+ Logger::info('Contact ID is zero or less than zero.');
return [];
}
$postarray['app'] = strip_tags($post->source);
if ($post->user->protected) {
- $postarray['private'] = 1;
+ $postarray['private'] = Item::PRIVATE;
$postarray['allow_cid'] = '<' . $self['id'] . '>';
} else {
- $postarray['private'] = 0;
+ $postarray['private'] = Item::UNLISTED;
$postarray['allow_cid'] = '';
}
}
// Search for media links
- $picture = twitter_media_entities($post, $postarray);
+ twitter_media_entities($post, $postarray, $uriid);
+
+ $converted = twitter_expand_entities($postarray['body'], $post);
+
+ // When the post contains external links then images or videos are just "decorations".
+ if (!empty($converted['urls'])) {
+ $postarray['post-type'] = Item::PT_NOTE;
+ }
- $converted = twitter_expand_entities($postarray['body'], $post, $picture);
$postarray['body'] = $converted['body'];
$postarray['created'] = DateTimeFormat::utc($post->created_at);
$postarray['edited'] = DateTimeFormat::utc($post->created_at);
if ($uriid > 0) {
twitter_store_tags($uriid, $converted['taglist']);
+ twitter_store_attachments($uriid, $post);
}
- $statustext = $converted["plain"];
-
if (!empty($post->place->name)) {
$postarray["location"] = $post->place->name;
}
$postarray['object-type'] = Activity\ObjectType::NOTE;
$postarray['thr-parent'] = $retweet['uri'];
- $postarray['parent-uri'] = $retweet['uri'];
} else {
$retweet['source'] = $postarray['source'];
+ $retweet['direction'] = $postarray['direction'];
$retweet['private'] = $postarray['private'];
$retweet['allow_cid'] = $postarray['allow_cid'];
$retweet['contact-id'] = $postarray['contact-id'];
// To avoid recursive share blocks we just provide the link to avoid removing quote context.
$postarray['body'] .= "\n\nhttps://twitter.com/" . $post->quoted_status->user->screen_name . "/status/" . $post->quoted_status->id_str;
} else {
- $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true, $uriid);
+ $quoted = twitter_createpost($a, 0, $post->quoted_status, $self, false, false, true);
if (!empty($quoted['body'])) {
+ Item::insert($quoted);
+ $post = Post::selectFirst(['guid', 'uri-id'], ['uri' => $quoted['uri'], 'uid' => 0]);
+ Logger::info('Stored quoted post', ['uid' => $uid, 'uri-id' => $uriid, 'post' => $post]);
+
$postarray['body'] .= "\n" . BBCode::getShareOpeningTag(
$quoted['author-name'],
$quoted['author-link'],
$quoted['author-avatar'],
$quoted['plink'],
- $quoted['created']
+ $quoted['created'],
+ $post['guid'] ?? ''
);
$postarray['body'] .= $quoted['body'] . '[/share]';
function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection, array $self)
{
- Logger::log("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, Logger::DEBUG);
+ Logger::info('Fetching parent posts', ['user' => $uid, 'post' => $post->id_str]);
$posts = [];
while (!empty($post->in_reply_to_status_id_str)) {
- $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str, "include_ext_alt_text" => true];
-
try {
- $post = $connection->get('statuses/show', $parameters);
+ $post = twitter_statuses_show($post->in_reply_to_status_id_str, $connection);
} catch (TwitterOAuthException $e) {
- Logger::log('twitter_fetchparentposts: Error fetching for user ' . $uid . ' and post ' . $post->id_str . ': ' . $e->getMessage());
+ Logger::warning('Error fetching parent post', ['uid' => $uid, 'post' => $post->id_str, 'message' => $e->getMessage()]);
break;
}
if (empty($post)) {
- Logger::log("twitter_fetchparentposts: Can't fetch post " . $parameters['id'], Logger::DEBUG);
+ Logger::info("twitter_fetchparentposts: Can't fetch post");
break;
}
if (empty($post->id_str)) {
- Logger::log("twitter_fetchparentposts: This is not a post " . json_encode($post), Logger::DEBUG);
+ Logger::info("twitter_fetchparentposts: This is not a post", ['post' => $post]);
break;
}
- if (Item::exists(['uri' => 'twitter::' . $post->id_str, 'uid' => $uid])) {
+ if (Post::exists(['uri' => 'twitter::' . $post->id_str, 'uid' => $uid])) {
break;
}
$create_user = DI::pConfig()->get($uid, 'twitter', 'create_user');
$mirror_posts = DI::pConfig()->get($uid, 'twitter', 'mirror_posts');
- Logger::log("Fetching timeline for user " . $uid, Logger::DEBUG);
+ Logger::info('Fetching timeline', ['uid' => $uid]);
$application_name = DI::config()->get('twitter', 'application_name');
try {
$own_contact = twitter_fetch_own_contact($a, $uid);
} catch (TwitterOAuthException $e) {
- Logger::log('Error fetching own contact for user ' . $uid . ': ' . $e->getMessage());
+ Logger::warning('Error fetching own contact', ['uid' => $uid, 'message' => $e->getMessage()]);
return;
}
if (DBA::isResult($r)) {
$own_id = $r[0]["nick"];
} else {
- Logger::log("Own twitter contact not found for user " . $uid);
+ Logger::warning('Own twitter contact not found', ['uid' => $uid]);
return;
}
$self = User::getOwnerDataById($uid);
if ($self === false) {
- Logger::log("Own contact not found for user " . $uid);
+ Logger::warning('Own contact not found', ['uid' => $uid]);
return;
}
try {
$items = $connection->get('statuses/home_timeline', $parameters);
} catch (TwitterOAuthException $e) {
- Logger::log('Error fetching home timeline for user ' . $uid . ': ' . $e->getMessage());
+ Logger::warning('Error fetching home timeline', ['uid' => $uid, 'message' => $e->getMessage()]);
return;
}
if (!is_array($items)) {
- Logger::log('No array while fetching home timeline for user ' . $uid . ': ' . print_r($items, true));
+ Logger::warning('home timeline is no array', ['items' => $items]);
return;
}
if (empty($items)) {
- Logger::log('No new timeline content for user ' . $uid, Logger::INFO);
+ Logger::notice('No new timeline content', ['uid' => $uid]);
return;
}
$posts = array_reverse($items);
- Logger::log('Fetching timeline from ID ' . $lastid . ' for user ' . $uid . ' ' . sizeof($posts) . ' items', Logger::DEBUG);
+ Logger::notice('Processing timeline', ['lastid' => $lastid, 'uid' => $uid, 'count' => count($posts)]);
if (count($posts)) {
foreach ($posts as $post) {
}
if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
- Logger::log("Skip previously sent post", Logger::DEBUG);
+ Logger::info("Skip previously sent post");
continue;
}
if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
- Logger::log("Skip post that will be mirrored", Logger::DEBUG);
+ Logger::info("Skip post that will be mirrored");
continue;
}
$notify = false;
- if (($postarray['uri'] == $postarray['parent-uri']) && ($postarray['author-link'] == $postarray['owner-link'])) {
+ if (empty($postarray['thr-parent'])) {
$contact = DBA::selectFirst('contact', [], ['id' => $postarray['contact-id'], 'self' => false]);
- if (DBA::isResult($contact)) {
- $notify = Item::isRemoteSelf($contact, $postarray);
+ if (DBA::isResult($contact) && Item::isRemoteSelf($contact, $postarray)) {
+ $notify = PRIORITY_MEDIUM;
}
}
try {
$items = $connection->get('statuses/mentions_timeline', $parameters);
} catch (TwitterOAuthException $e) {
- Logger::log('Error fetching mentions: ' . $e->getMessage());
+ Logger::warning('Error fetching mentions', ['uid' => $uid, 'message' => $e->getMessage()]);
return;
}
if (!is_array($items)) {
- Logger::log("Error fetching mentions: " . print_r($items, true), Logger::DEBUG);
+ Logger::warning("mentions are no arrays", ['items' => $items]);
return;
}
if ($id == $link) {
return false;
}
+ return twitter_retweet($uid, $id);
+}
- Logger::log('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, Logger::DEBUG);
+function twitter_retweet(int $uid, int $id, int $item_id = 0)
+{
+ Logger::info('Retweeting', ['user' => $uid, 'id' => $id]);
$ckey = DI::config()->get('twitter', 'consumerkey');
$csecret = DI::config()->get('twitter', 'consumersecret');
$connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
$result = $connection->post('statuses/retweet/' . $id);
- Logger::log('twitter_is_retweet: result ' . print_r($result, true), Logger::DEBUG);
+ Logger::info('Retweeted', ['user' => $uid, 'id' => $id, 'result' => $result]);
+
+ if (!empty($item_id) && !empty($result->id_str)) {
+ Logger::notice('Update extid', ['id' => $item_id, 'extid' => $result->id_str]);
+ Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $item_id]);
+ }
return !isset($result->errors);
}