-
- $abandon_days = intval(DI::config()->get('system', 'account_abandon_days'));
- if ($abandon_days < 1) {
- $abandon_days = 0;
- }
-
- $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400);
-
- $pconfigs = DBA::selectToArray('pconfig', [], ['cat' => 'twitter', 'k' => 'import', 'v' => true]);
- foreach ($pconfigs as $rr) {
- if ($abandon_days != 0) {
- if (!DBA::exists('user', ["`uid` = ? AND `login_date` >= ?", $rr['uid'], $abandon_limit])) {
- Logger::notice('abandoned account: timeline from user will not be imported', ['user' => $rr['uid']]);
- continue;
- }
- }
-
- Logger::notice('importing timeline', ['user' => $rr['uid']]);
- Worker::add(['priority' => PRIORITY_MEDIUM, 'force_priority' => true], "addon/twitter/twitter_sync.php", 2, (int) $rr['uid']);
- /*
- // To-Do
- // check for new contacts once a day
- $last_contact_check = DI::pConfig()->get($rr['uid'],'pumpio','contact_check');
- if($last_contact_check)
- $next_contact_check = $last_contact_check + 86400;
- else
- $next_contact_check = 0;
-
- if($next_contact_check <= time()) {
- pumpio_getallusers($a, $rr["uid"]);
- DI::pConfig()->set($rr['uid'],'pumpio','contact_check',time());
- }
- */
- }
-
- Logger::notice('twitter: cron_end');
-
- DI::config()->set('twitter', 'last_poll', time());
-}
-
-function twitter_expire(App $a)
-{
- $days = DI::config()->get('twitter', 'expire');
-
- if ($days == 0) {
- return;
- }
-
- 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()]);
- Item::markForDeletionById($row['id']);
- }
- DBA::close($r);
-
- Logger::notice('End deleting expired posts');
-
- Logger::notice('Start expiry');
-
- $pconfigs = DBA::selectToArray('pconfig', [], ['cat' => 'twitter', 'k' => 'import', 'v' => true]);
- foreach ($pconfigs as $rr) {
- Logger::notice('twitter_expire', ['user' => $rr['uid']]);
- Item::expire($rr['uid'], $days, Protocol::TWITTER, true);
- }
-
- Logger::notice('End expiry');
-}
-
-function twitter_prepare_body(App $a, array &$b)
-{
- if ($b["item"]["network"] != Protocol::TWITTER) {
- return;
- }
-
- if ($b["preview"]) {
- $max_char = 280;
- $item = $b["item"];
- $item["plink"] = DI::baseUrl()->get() . "/display/" . $item["guid"];
-
- $condition = ['uri' => $item["thr-parent"], 'uid' => local_user()];
- $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]";
- $nicknameplain = "@" . $nicknameplain;
-
- if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) {
- $item["body"] = $nickname . " " . $item["body"];
- }
- }
-
- $msgarr = Plaintext::getPost($item, $max_char, true, BBCode::TWITTER);
- $msg = $msgarr["text"];
-
- if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
- $msg .= " " . $msgarr["url"];
- }
-
- if (isset($msgarr["image"])) {
- $msg .= " " . $msgarr["image"];
- }
-
- $b['html'] = nl2br(htmlspecialchars($msg));
- }
-}
-
-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
- *
- * @param App $a Application class
- * @param integer $uid User id
- * @param object $post Twitter object with the post
- *
- * @return array item data to be posted
- */
-function twitter_do_mirrorpost(App $a, $uid, $post)
-{
- $datarray['uid'] = $uid;
- $datarray['extid'] = 'twitter::' . $post->id;
- $datarray['title'] = '';
-
- if (!empty($post->retweeted_status)) {
- // We don't support nested shares, so we mustn't show quotes as shares on retweets
- $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true, -1);
-
- if (empty($item['body'])) {
- return [];
- }
-
- $datarray['body'] = "\n" . BBCode::getShareOpeningTag(
- $item['author-name'],
- $item['author-link'],
- $item['author-avatar'],
- $item['plink'],
- $item['created']
- );
-
- $datarray['body'] .= $item['body'] . '[/share]';
- } else {
- $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false, -1);
-
- if (empty($item['body'])) {
- return [];
- }
-
- $datarray['body'] = $item['body'];
- }
-
- $datarray['app'] = $item['app'];
- $datarray['verb'] = $item['verb'];
-
- if (isset($item['location'])) {
- $datarray['location'] = $item['location'];
- }
-
- if (isset($item['coord'])) {
- $datarray['coord'] = $item['coord'];
- }
-
- return $datarray;
-}
-
-function twitter_fetchtimeline(App $a, $uid)
-{
- $ckey = DI::config()->get('twitter', 'consumerkey');
- $csecret = DI::config()->get('twitter', 'consumersecret');
- $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
- $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
- $lastid = DI::pConfig()->get($uid, 'twitter', 'lastid');
-
- $application_name = DI::config()->get('twitter', 'application_name');
-
- if ($application_name == "") {
- $application_name = DI::baseUrl()->getHostname();
- }
-
- $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 == "");
-
- if ($lastid != "") {
- $parameters["since_id"] = $lastid;
- }
-
- try {
- $items = $connection->get('statuses/user_timeline', $parameters);
- } catch (TwitterOAuthException $e) {
- Logger::warning('Error fetching timeline', ['uid' => $uid, 'message' => $e->getMessage()]);
- return;
- }
-
- if (!is_array($items)) {
- Logger::notice('No items', ['user' => $uid]);
- return;
- }
-
- $posts = array_reverse($items);
-
- Logger::notice('Start processing posts', ['from' => $lastid, 'user' => $uid, 'count' => count($posts)]);
-
- if (count($posts)) {
- foreach ($posts as $post) {
- if ($post->id_str > $lastid) {
- $lastid = $post->id_str;
- DI::pConfig()->set($uid, 'twitter', 'lastid', $lastid);
- }
-
- if ($first_time) {
- continue;
- }
-
- if (!stristr($post->source, $application_name)) {
- Logger::info('Preparing mirror post', ['twitter-id' => $post->id_str, 'uid' => $uid]);
-
- $mirrorpost = twitter_do_mirrorpost($a, $uid, $post);
-
- if (empty($mirrorpost['body'])) {
- continue;
- }
-
- Logger::info('Posting mirror post', ['twitter-id' => $post->id_str, 'uid' => $uid]);
-
- Post\Delayed::add($mirrorpost['extid'], $mirrorpost, PRIORITY_MEDIUM, Post\Delayed::UNPREPARED);
- }
- }
- }
- DI::pConfig()->set($uid, 'twitter', 'lastid', $lastid);
- Logger::info('Last ID for user ' . $uid . ' is now ' . $lastid);
-}
-
-function twitter_fix_avatar($avatar)
-{
- $new_avatar = str_replace("_normal.", "_400x400.", $avatar);
-
- $info = Images::getInfoFromURLCached($new_avatar);
- if (!$info) {
- $new_avatar = $avatar;
- }
-
- return $new_avatar;
-}
-
-function twitter_get_relation($uid, $target, $contact = [])
-{
- if (isset($contact['rel'])) {
- $relation = $contact['rel'];
- } else {
- $relation = 0;
- }
-
- $ckey = DI::config()->get('twitter', 'consumerkey');
- $csecret = DI::config()->get('twitter', 'consumersecret');
- $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
- $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
- $own_id = DI::pConfig()->get($uid, 'twitter', 'own_id');
-
- $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
- $parameters = ['source_id' => $own_id, 'target_screen_name' => $target];
-
- try {
- $status = $connection->get('friendships/show', $parameters);
- if ($connection->getLastHttpCode() !== 200) {
- throw new Exception($status->errors[0]->message ?? 'HTTP response code ' . $connection->getLastHttpCode(), $status->errors[0]->code ?? $connection->getLastHttpCode());
- }
-
- $following = $status->relationship->source->following;
- $followed = $status->relationship->source->followed_by;
-
- if ($following && !$followed) {
- $relation = Contact::SHARING;
- } elseif (!$following && $followed) {
- $relation = Contact::FOLLOWER;
- } elseif ($following && $followed) {
- $relation = Contact::FRIEND;
- } elseif (!$following && !$followed) {
- $relation = 0;
- }
-
- Logger::info('Fetched friendship relation', ['user' => $uid, 'target' => $target, 'relation' => $relation]);
- } catch (Throwable $e) {
- Logger::warning('Error fetching friendship status', ['uid' => $uid, 'target' => $target, 'message' => $e->getMessage()]);
- }
-
- return $relation;
-}
-
-/**
- * @param $data
- * @return array
- */
-function twitter_user_to_contact($data)
-{
- if (empty($data->id_str)) {
- return [];
- }
-
- $baseurl = 'https://twitter.com';
- $url = $baseurl . '/' . $data->screen_name;
- $addr = $data->screen_name . '@twitter.com';
-
- $fields = [
- 'url' => $url,
- 'nurl' => Strings::normaliseLink($url),
- 'uri-id' => ItemURI::getIdByURI($url),
- 'network' => Protocol::TWITTER,
- 'alias' => 'twitter::' . $data->id_str,
- 'baseurl' => $baseurl,
- 'name' => $data->name,
- 'nick' => $data->screen_name,
- 'addr' => $addr,
- '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;
-}
-
-function twitter_get_contact($data, int $uid = 0)
-{
- $contact = DBA::selectFirst('contact', ['id'], ['uid' => $uid, 'alias' => "twitter::" . $data->id_str]);
- if (DBA::isResult($contact)) {
- return $contact['id'];
- } else {
- return twitter_fetch_contact($uid, $data, false);
- }
-}
-
-function twitter_fetch_contact($uid, $data, $create_user)
-{
- $fields = twitter_user_to_contact($data);
-
- if (empty($fields)) {
- return -1;
- }
-
- // photo comes from twitter_user_to_contact but shouldn't be saved directly in the contact row
- $avatar = $fields['photo'];
- unset($fields['photo']);
-
- // Update the public contact
- $pcontact = DBA::selectFirst('contact', ['id'], ['uid' => 0, 'alias' => "twitter::" . $data->id_str]);
- if (DBA::isResult($pcontact)) {
- $cid = $pcontact['id'];
- } else {
- $cid = Contact::getIdForURL($fields['url'], 0, false, $fields);
- }
-
- if (!empty($cid)) {
- Contact::update($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) && 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)) {
- $relation = twitter_get_relation($uid, $data->screen_name);
-
- // create contact record
- $fields['uid'] = $uid;
- $fields['created'] = DateTimeFormat::utcNow();
- $fields['poll'] = 'twitter::' . $data->id_str;
- $fields['rel'] = $relation;
- $fields['priority'] = 1;
- $fields['writable'] = true;
- $fields['blocked'] = false;
- $fields['readonly'] = false;
- $fields['pending'] = false;
-
- if (!Contact::insert($fields)) {
- return false;
- }
-
- $contact_id = DBA::lastInsertId();
-
- Group::addMember(User::getDefaultGroup($uid), $contact_id);
- } else {
- if ($contact["readonly"] || $contact["blocked"]) {
- Logger::notice('Contact is blocked or readonly.', ['nickname' => $contact["nick"]]);
- return -1;
- }
-
- $contact_id = $contact['id'];
- $update = false;
-
- // Update the contact relation once per day
- if ($contact['updated'] < DateTimeFormat::utc('now -24 hours')) {
- $fields['rel'] = twitter_get_relation($uid, $data->screen_name, $contact);
- $update = true;
- }
-
- if ($contact['name'] != $data->name) {
- $fields['name-date'] = $fields['uri-date'] = DateTimeFormat::utcNow();
- $update = true;
- }
-
- if ($contact['nick'] != $data->screen_name) {
- $fields['uri-date'] = DateTimeFormat::utcNow();
- $update = true;
- }
-
- if (($contact['location'] != $data->location) || ($contact['about'] != $data->description)) {
- $update = true;
- }
-
- if ($update) {
- $fields['updated'] = DateTimeFormat::utcNow();
- Contact::update($fields, ['id' => $contact['id']]);
- Logger::info('Updated contact', ['id' => $contact['id'], 'nick' => $data->screen_name]);
- }
- }
-
- Contact::updateAvatar($contact_id, $avatar);
-
- return $contact_id;
-}
-
-/**
- * @param string $screen_name
- * @return stdClass|null
- * @throws Exception
- */
-function twitter_fetchuser($screen_name)
-{
- $ckey = DI::config()->get('twitter', 'consumerkey');
- $csecret = DI::config()->get('twitter', 'consumersecret');
-
- try {
- // Fetching user data
- $connection = new TwitterOAuth($ckey, $csecret);
- $parameters = ['screen_name' => $screen_name];
- $user = $connection->get('users/show', $parameters);
- } catch (TwitterOAuthException $e) {
- Logger::warning('Error fetching user', ['user' => $screen_name, 'message' => $e->getMessage()]);
- return null;
- }
-
- if (!is_object($user)) {
- return null;
- }
-
- return $user;
-}
-
-/**
- * Replaces Twitter entities with Friendica-friendly links.
- *
- * The Twitter API gives indices for each entity, which allows for fine-grained replacement.
- *
- * First, we need to collect everything that needs to be replaced, what we will replace it with, and the start index.
- * Then we sort the indices decreasingly, and we replace from the end of the body to the start in order for the next
- * index to be correct even after the last replacement.
- *
- * @param string $body
- * @param stdClass $status
- * @return array
- * @throws \Friendica\Network\HTTPException\InternalServerErrorException
- */
-function twitter_expand_entities($body, stdClass $status)
-{
- $plain = $body;
- $contains_urls = false;
-
- $taglist = [];
-
- $replacementList = [];
-
- foreach ($status->entities->hashtags AS $hashtag) {
- $replace = '#[url=' . DI::baseUrl()->get() . '/search?tag=' . $hashtag->text . ']' . $hashtag->text . '[/url]';
- $taglist['#' . $hashtag->text] = ['#', $hashtag->text, ''];
-
- $replacementList[$hashtag->indices[0]] = [
- 'replace' => $replace,
- 'length' => $hashtag->indices[1] - $hashtag->indices[0],
- ];
- }
-
- foreach ($status->entities->user_mentions AS $mention) {
- $replace = '@[url=https://twitter.com/' . rawurlencode($mention->screen_name) . ']' . $mention->screen_name . '[/url]';
- $taglist['@' . $mention->screen_name] = ['@', $mention->screen_name, 'https://twitter.com/' . rawurlencode($mention->screen_name)];
-
- $replacementList[$mention->indices[0]] = [
- 'replace' => $replace,
- 'length' => $mention->indices[1] - $mention->indices[0],
- ];
- }
-
- 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
- ) {
- $replacementList[$url->indices[0]] = [
- 'replace' => '',
- 'length' => $url->indices[1] - $url->indices[0],
- ];
- continue;
- }
-
- $contains_urls = true;
-
- $expanded_url = $url->expanded_url;
-
- // Quickfix: Workaround for URL with '[' and ']' in it
- if (strpos($expanded_url, '[') || strpos($expanded_url, ']')) {
- $expanded_url = $url->url;
- }
-
- $replacementList[$url->indices[0]] = [
- 'replace' => '[url=' . $expanded_url . ']' . $url->display_url . '[/url]',
- 'length' => $url->indices[1] - $url->indices[0],
- ];
- }
- }
-
- krsort($replacementList);
-
- foreach ($replacementList as $startIndex => $parameters) {
- $body = Strings::substringReplace($body, $parameters['replace'], $startIndex, $parameters['length']);
- }
-
- $body = trim($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]);
- }
- }
- }
-
- 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
- * @param integer $uriid URI Id used to store tags. -1 = don't store tags for this post.
- */
-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;
- }
-
- // This is a pure media post, first search for all media urls
- $media = [];
- foreach ($post->extended_entities->media AS $medium) {
- if (!isset($media[$medium->url])) {
- $media[$medium->url] = '';
- }
- switch ($medium->type) {
- case 'photo':
- if (!empty($medium->ext_alt_text)) {
- Logger::info('Got text description', ['alt_text' => $medium->ext_alt_text]);
- $media[$medium->url] .= "\n[img=" . $medium->media_url_https .']' . $medium->ext_alt_text . '[/img]';
- } else {
- $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
- }
-
- $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]);
- $media[$medium->url] .= "\n[img=" . $medium->media_url_https .']' . $medium->ext_alt_text . '[/img]';
- } else {
- $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
- }
-
- $postarray['object-type'] = Activity\ObjectType::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)) {
- $media[$medium->url] = "\n[video]" . $variant->url . '[/video]';
- $bitrate = $variant->bitrate;
- }
- }
- }
- break;
- }
- }
-
- 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']);
- }
-}
-
-/**
- * Undocumented function
- *
- * @param App $a
- * @param integer $uid User ID
- * @param object $post Incoming Twitter post
- * @param array $self
- * @param bool $create_user Should users be created?
- * @param bool $only_existing_contact Only import existing contacts if set to "true"
- * @param bool $noquote
- * @param integer $uriid URI Id used to store tags. 0 = create a new one; -1 = don't store tags for this post.
- * @return array item array
- */
-function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $only_existing_contact, $noquote, int $uriid = 0)
-{
- $postarray = [];
- $postarray['network'] = Protocol::TWITTER;
- $postarray['uid'] = $uid;
- $postarray['wall'] = 0;
- $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 (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 != "") {
- $thr_parent = "twitter::" . $post->in_reply_to_status_id_str;
-
- $item = Post::selectFirst(['uri'], ['uri' => $thr_parent, 'uid' => $uid]);
- if (!DBA::isResult($item)) {
- $item = Post::selectFirst(['uri'], ['extid' => $thr_parent, 'uid' => $uid, 'gravity' => GRAVITY_COMMENT]);
- }
-
- if (DBA::isResult($item)) {
- $postarray['thr-parent'] = $item['uri'];
- $postarray['object-type'] = Activity\ObjectType::COMMENT;
- } else {
- $postarray['object-type'] = Activity\ObjectType::NOTE;
- }
-
- // Is it me?
- $own_id = DI::pConfig()->get($uid, 'twitter', 'own_id');
-
- if ($post->user->id_str == $own_id) {
- $self = Contact::selectFirst(['id', 'name', 'url', 'photo'], ['self' => true, 'uid' => $uid]);
- if (DBA::isResult($self)) {
- $contactid = $self['id'];
-
- $postarray['owner-name'] = $self['name'];
- $postarray['owner-link'] = $self['url'];
- $postarray['owner-avatar'] = $self['photo'];
- } else {
- Logger::error('No self contact found', ['uid' => $uid]);
- return [];
- }
- }
- // Don't create accounts of people who just comment something
- $create_user = false;
- } else {
- $postarray['object-type'] = Activity\ObjectType::NOTE;
- }
-
- if ($contactid == 0) {
- $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
-
- $postarray['owner-id'] = twitter_get_contact($post->user);
- $postarray['owner-name'] = $post->user->name;
- $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
- $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
- }
-
- if (($contactid == 0) && !$only_existing_contact) {
- $contactid = $self['id'];
- } elseif ($contactid <= 0) {
- Logger::info('Contact ID is zero or less than zero.');
- return [];
- }
-
- $postarray['contact-id'] = $contactid;
- $postarray['verb'] = Activity::POST;
- $postarray['author-id'] = $postarray['owner-id'];
- $postarray['author-name'] = $postarray['owner-name'];
- $postarray['author-link'] = $postarray['owner-link'];
- $postarray['author-avatar'] = $postarray['owner-avatar'];
- $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
- $postarray['app'] = strip_tags($post->source);
-
- if ($post->user->protected) {
- $postarray['private'] = Item::PRIVATE;
- $postarray['allow_cid'] = '<' . $self['id'] . '>';
- } else {
- $postarray['private'] = Item::UNLISTED;
- $postarray['allow_cid'] = '';
- }
-
- if (!empty($post->full_text)) {
- $postarray['body'] = $post->full_text;
- } else {
- $postarray['body'] = $post->text;
- }
-
- // When the post contains links then use the correct object type
- if (count($post->entities->urls) > 0) {
- $postarray['object-type'] = Activity\ObjectType::BOOKMARK;
- }
-
- // Search for media links
- 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;
- }
-
- $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);
- }
-
- if (!empty($post->place->name)) {
- $postarray["location"] = $post->place->name;
- }
- if (!empty($post->place->full_name)) {
- $postarray["location"] = $post->place->full_name;
- }
- if (!empty($post->geo->coordinates)) {
- $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
- }
- if (!empty($post->coordinates->coordinates)) {
- $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
- }
- if (!empty($post->retweeted_status)) {
- $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
-
- if (empty($retweet['body'])) {
- return [];
- }
-
- if (!$noquote) {
- // Store the original tweet
- Item::insert($retweet);
-
- // CHange the other post into a reshare activity
- $postarray['verb'] = Activity::ANNOUNCE;
- $postarray['gravity'] = GRAVITY_ACTIVITY;
- $postarray['object-type'] = Activity\ObjectType::NOTE;
-
- $postarray['thr-parent'] = $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'];
- $retweet['owner-id'] = $postarray['owner-id'];
- $retweet['owner-name'] = $postarray['owner-name'];
- $retweet['owner-link'] = $postarray['owner-link'];
- $retweet['owner-avatar'] = $postarray['owner-avatar'];
-
- $postarray = $retweet;
- }
- }
-
- if (!empty($post->quoted_status)) {
- if ($noquote) {
- // 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, 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'],
- $post['guid'] ?? ''
- );
-
- $postarray['body'] .= $quoted['body'] . '[/share]';
- } else {
- // Quoted post author is blocked/ignored, so 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;
- }
- }
- }
-
- return $postarray;
-}
-
-/**
- * Store tags and mentions
- *
- * @param integer $uriid
- * @param array $taglist
- */
-function twitter_store_tags(int $uriid, array $taglist)
-{
- foreach ($taglist as $tag) {
- Tag::storeByHash($uriid, $tag[0], $tag[1], $tag[2]);
- }
-}
-
-function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection, array $self)
-{
- Logger::info('Fetching parent posts', ['user' => $uid, 'post' => $post->id_str]);
-
- $posts = [];
-
- while (!empty($post->in_reply_to_status_id_str)) {
- try {
- $post = twitter_statuses_show($post->in_reply_to_status_id_str, $connection);
- } catch (TwitterOAuthException $e) {
- Logger::warning('Error fetching parent post', ['uid' => $uid, 'post' => $post->id_str, 'message' => $e->getMessage()]);
- break;
- }
-
- if (empty($post)) {
- Logger::info("twitter_fetchparentposts: Can't fetch post");
- break;
- }
-
- if (empty($post->id_str)) {
- Logger::info("twitter_fetchparentposts: This is not a post", ['post' => $post]);
- break;
- }
-
- if (Post::exists(['uri' => 'twitter::' . $post->id_str, 'uid' => $uid])) {
- break;
- }
-
- $posts[] = $post;
- }
-
- Logger::info("twitter_fetchparentposts: Fetching " . count($posts) . " parents");
-
- $posts = array_reverse($posts);
-
- if (!empty($posts)) {
- foreach ($posts as $post) {
- $postarray = twitter_createpost($a, $uid, $post, $self, false, !DI::pConfig()->get($uid, 'twitter', 'create_user'), false);
-
- if (empty($postarray['body'])) {
- continue;
- }
-
- $item = Item::insert($postarray);
-
- $postarray["id"] = $item;
-
- Logger::notice('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
- }
- }
-}
-
-function twitter_fetchhometimeline(App $a, $uid)
-{
- $ckey = DI::config()->get('twitter', 'consumerkey');
- $csecret = DI::config()->get('twitter', 'consumersecret');
- $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
- $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
- $create_user = DI::pConfig()->get($uid, 'twitter', 'create_user');
- $mirror_posts = DI::pConfig()->get($uid, 'twitter', 'mirror_posts');
-
- Logger::info('Fetching timeline', ['uid' => $uid]);
-
- $application_name = DI::config()->get('twitter', 'application_name');
-
- if ($application_name == "") {
- $application_name = DI::baseUrl()->getHostname();
- }
-
- $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
-
- try {
- $own_contact = twitter_fetch_own_contact($a, $uid);
- } catch (TwitterOAuthException $e) {
- Logger::warning('Error fetching own contact', ['uid' => $uid, 'message' => $e->getMessage()]);
- return;
- }
-
- $contact = Contact::selectFirst(['nick'], ['id' => $own_contact, 'uid' => $uid]);
- if (DBA::isResult($contact)) {
- $own_id = $contact['nick'];
- } else {
- Logger::warning('Own twitter contact not found', ['uid' => $uid]);
- return;
- }
-
- $self = User::getOwnerDataById($uid);
- if ($self === false) {
- Logger::warning('Own contact not found', ['uid' => $uid]);
- return;
- }
-
- $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended", "include_ext_alt_text" => true];
- //$parameters["count"] = 200;
- // Fetching timeline
- $lastid = DI::pConfig()->get($uid, 'twitter', 'lasthometimelineid');
-
- $first_time = ($lastid == "");
-
- if ($lastid != "") {
- $parameters["since_id"] = $lastid;
- }
-
- try {
- $items = $connection->get('statuses/home_timeline', $parameters);
- } catch (TwitterOAuthException $e) {
- Logger::warning('Error fetching home timeline', ['uid' => $uid, 'message' => $e->getMessage()]);
- return;
- }
-
- if (!is_array($items)) {
- Logger::warning('home timeline is no array', ['items' => $items]);
- return;
- }
-
- if (empty($items)) {
- Logger::notice('No new timeline content', ['uid' => $uid]);
- return;
- }
-
- $posts = array_reverse($items);
-
- Logger::notice('Processing timeline', ['lastid' => $lastid, 'uid' => $uid, 'count' => count($posts)]);
-
- if (count($posts)) {
- foreach ($posts as $post) {
- if ($post->id_str > $lastid) {
- $lastid = $post->id_str;
- DI::pConfig()->set($uid, 'twitter', 'lasthometimelineid', $lastid);
- }
-
- if ($first_time) {
- continue;
- }
-
- if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
- Logger::info("Skip previously sent post");
- continue;
- }
-
- if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
- Logger::info("Skip post that will be mirrored");
- continue;
- }
-
- if ($post->in_reply_to_status_id_str != "") {
- twitter_fetchparentposts($a, $uid, $post, $connection, $self);
- }
-
- Logger::info('Preparing post ' . $post->id_str . ' for user ' . $uid);
-
- $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
-
- if (empty($postarray['body']) || trim($postarray['body']) == "") {
- Logger::info('Empty body for post ' . $post->id_str . ' and user ' . $uid);
- continue;
- }
-
- $notify = false;
-
- if (empty($postarray['thr-parent'])) {
- $contact = DBA::selectFirst('contact', [], ['id' => $postarray['contact-id'], 'self' => false]);
- if (DBA::isResult($contact) && Item::isRemoteSelf($contact, $postarray)) {
- $notify = PRIORITY_MEDIUM;
- }
- }
-
- $item = Item::insert($postarray, $notify);
- $postarray["id"] = $item;
-
- Logger::notice('User ' . $uid . ' posted home timeline item ' . $item);
- }
- }
- DI::pConfig()->set($uid, 'twitter', 'lasthometimelineid', $lastid);
-
- Logger::info('Last timeline ID for user ' . $uid . ' is now ' . $lastid);
-
- // Fetching mentions
- $lastid = DI::pConfig()->get($uid, 'twitter', 'lastmentionid');
-
- $first_time = ($lastid == "");
-
- if ($lastid != "") {
- $parameters["since_id"] = $lastid;
- }
-
- try {
- $items = $connection->get('statuses/mentions_timeline', $parameters);
- } catch (TwitterOAuthException $e) {
- Logger::warning('Error fetching mentions', ['uid' => $uid, 'message' => $e->getMessage()]);
- return;
- }
-
- if (!is_array($items)) {
- Logger::warning("mentions are no arrays", ['items' => $items]);
- return;
- }
-
- $posts = array_reverse($items);
-
- Logger::info("Fetching mentions for user " . $uid . " " . sizeof($posts) . " items");
-
- if (count($posts)) {
- foreach ($posts as $post) {
- if ($post->id_str > $lastid) {
- $lastid = $post->id_str;
- }
-
- if ($first_time) {
- continue;
- }
-
- if ($post->in_reply_to_status_id_str != "") {
- twitter_fetchparentposts($a, $uid, $post, $connection, $self);
- }
-
- $postarray = twitter_createpost($a, $uid, $post, $self, false, !$create_user, false);
-
- if (empty($postarray['body'])) {
- continue;
- }
-
- $item = Item::insert($postarray);
-
- Logger::notice('User ' . $uid . ' posted mention timeline item ' . $item);
- }
- }
-
- DI::pConfig()->set($uid, 'twitter', 'lastmentionid', $lastid);
-
- Logger::info('Last mentions ID for user ' . $uid . ' is now ' . $lastid);
-}
-
-function twitter_fetch_own_contact(App $a, $uid)
-{
- $ckey = DI::config()->get('twitter', 'consumerkey');
- $csecret = DI::config()->get('twitter', 'consumersecret');
- $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
- $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
-
- $own_id = DI::pConfig()->get($uid, 'twitter', 'own_id');
-
- $contact_id = 0;
-
- if ($own_id == "") {
- $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
-
- // Fetching user data
- // get() may throw TwitterOAuthException, but we will catch it later
- $user = $connection->get('account/verify_credentials');
- if (empty($user->id_str)) {
- return false;
- }
-
- DI::pConfig()->set($uid, 'twitter', 'own_id', $user->id_str);
-
- $contact_id = twitter_fetch_contact($uid, $user, true);
- } else {
- $contact = Contact::selectFirst(['id'], ['uid' => $uid, 'alias' => 'twitter::' . $own_id]);
- if (DBA::isResult($contact)) {
- $contact_id = $contact['id'];
- } else {
- DI::pConfig()->delete($uid, 'twitter', 'own_id');
- }
- }
-
- return $contact_id;
-}
-
-function twitter_is_retweet(App $a, $uid, $body)
-{
- $body = trim($body);
-
- // Skip if it isn't a pure repeated messages
- // Does it start with a share?
- if (strpos($body, "[share") > 0) {
- return false;
- }
-
- // Does it end with a share?
- if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
- return false;
- }
-
- $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
- // Skip if there is no shared message in there
- if ($body == $attributes) {
- return false;
- }
-
- $link = "";
- preg_match("/link='(.*?)'/ism", $attributes, $matches);
- if (!empty($matches[1])) {
- $link = $matches[1];
- }
-
- preg_match('/link="(.*?)"/ism', $attributes, $matches);
- if (!empty($matches[1])) {
- $link = $matches[1];
- }
-
- $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
- if ($id == $link) {
- return false;
- }
- return twitter_retweet($uid, $id);
-}
-
-function twitter_retweet(int $uid, int $id, int $item_id = 0)
-{
- Logger::info('Retweeting', ['user' => $uid, 'id' => $id]);
-
- $result = twitter_api_post('statuses/retweet', $id, $uid);
-
- 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);
-}
-
-function twitter_update_mentions($body)
-{
- $URLSearchString = "^\[\]";
- $return = preg_replace_callback(
- "/@\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
- function ($matches) {
- if (strpos($matches[1], 'twitter.com')) {
- $return = '@' . substr($matches[1], strrpos($matches[1], '/') + 1);
- } else {
- $return = $matches[2] . ' (' . $matches[1] . ')';
- }
-
- return $return;
- },
- $body
- );
-
- return $return;
-}
-
-function twitter_convert_share(array $attributes, array $author_contact, $content, $is_quote_share)
-{
- if (empty($author_contact)) {
- return $content . "\n\n" . $attributes['link'];
- }
-
- if (!empty($author_contact['network']) && ($author_contact['network'] == Protocol::TWITTER)) {
- $mention = '@' . $author_contact['nick'];
- } else {
- $mention = $author_contact['addr'];
- }
-
- return ($is_quote_share ? "\n\n" : '' ) . 'RT ' . $mention . ': ' . $content . "\n\n" . $attributes['link'];