[ 'peer_name' => 'generic-server', 'verify_peer' => FALSE, 'verify_peer_name' => FALSE, 'allow_self_signed' => TRUE ] ] ); curl_setopt($curl, CURLOPT_SSLVERSION, 4); */ /** * The function searches for a key-value pair in a string and returns the value if found. * * @param keyToFind The key we want to find in the string. * @param string The string in which to search for the key-value pair. * * @return the value of the key that matches the given keyToFind in the given string. If a match is * found, the function returns the value of the key as a string after trimming any whitespace. If no * match is found, the function returns null. */ function getSingleParameter($keyToFind, $string) { if (!str_contains($string, $keyToFind)) { return null; } $pattern = '/\s*(? = " preg_match($pattern, $string, $matches); if (isset($matches[1]) && isset($matches[2])) { $result = array($matches[1], $matches[2]); return $result; } return null; } function getReplyHashFromTwt(string $twtString): string { // Extract the text between parentheses using regular expressions $pattern = '/\(#([^\)]+)\)/'; // Matches "(#)" preg_match($pattern, $twtString, $matches); if (isset($matches[1])) { $textBetweenParentheses = $matches[1]; return $textBetweenParentheses; } return ''; } function getImagesFromTwt(string $twtString) { $pattern = '/(]+>)/i'; preg_match_all($pattern, $twtString, $matches, PREG_SET_ORDER); $result = array(); foreach ($matches as $match) { $result[] = array($match[0]); } return $result; } function getTagsFromTwt(string $twtString) { //$pattern = '/(?]+)\s([^>]+)>/'; // Matches "@" preg_match_all($pattern, $twtString, $matches, PREG_SET_ORDER); $result = array(); foreach ($matches as $match) { $nick = $match[1]; $url = $match[2]; $result[] = array("nick" => $nick, "url" => $url); } return $result; } function replaceMentionsFromTwt(string $twtString): string { // Example input: 'Hello @, how are you? @'; // Example output: Hello @eapl.mx@eapl.mx/twtxt.txt, how are you? @nick@server.com/something/twtxt.txt $pattern = '/@<([^ ]+)\s([^>]+)>/'; //$replacement = '@$1'; $replacement = '@$1'; //$replacement .= ''; // Adds a hidden link direcly to the twtxt.txt of the mentioned target #$twtString = '@'; #$pattern = '/@<([^ ]+) ([^>]+)>/'; #$replacement = '@$1'; /* $pattern_only_url = '/@<(https?:\/\/[^>]+)>/'; if (preg_match($pattern_only_url, $twtString)) { $replacement = '@$1'; } */ $result = preg_replace($pattern, $replacement, $twtString); return $result; // from https://github.com/hxii/picoblog/blob/master/picoblog.php //$pattern = '/\@<([a-zA-Z0-9\.]+)\W+(https?:\/\/[^>]+)>/'; //return preg_replace($pattern,'@$1',$twtString); } function replaceLinksFromTwt(string $twtString) { // TODO: Make this NOT match with `inline code` to avoid links in code-snippets // 1. Look into how yarnd handles this // Regular expression pattern to match URLs //$pattern = '/(?)(?\s]+)/is'; // Replace URLs with clickable links $replacement = '$1'; $result = preg_replace($pattern, $replacement, $twtString); return $result; } function replaceMarkdownLinksFromTwt(string $twtString) { $pattern = '/\[([^\]]+)\]\(([^)]+)\)/'; $replacement = '$1'; $result = preg_replace($pattern, $replacement, $twtString); return $result; } function replaceImagesFromTwt(string $twtString) { $pattern = '/!\[(.*?)\]\((.*?)\)/'; //$replacement = '$1'; $replacement = '$1'; $result = preg_replace($pattern, $replacement, $twtString); return $result; } function replaceTagsFromTwt(string $twtString) { //$pattern = '/#(\w+)?/'; //$pattern = '/(?<=\s)#(\w+)/'; $pattern = '/(?<=\B)#([\p{L}\p{N}_]+)/u'; //$replacement = '#\1'; // Dummy link $replacement = '#${1}'; $result = preg_replace($pattern, $replacement, $twtString); return $result; } function embedYoutubeFromTwt(string $twtString) { // original regex source: https://gist.github.com/afeld/1254889#gistcomment-1253992 $pattern = '/(?:youtube(?:-nocookie)?\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/mi'; if (preg_match_all($pattern, $twtString, $youtubeLinks)) { $youtubeLinks = array_unique($youtubeLinks[1]); // Remove dublicate cause by raw URLs conceverter to links foreach ($youtubeLinks as $videoID) { $twtString .= ''; } } $result = $twtString; return $result; } function getTimeElapsedString($timestamp, $full = false) { $now = new DateTime; $ago = new DateTime; $ago->setTimestamp($timestamp); $agoText = 'ago'; if ($now < $ago) { $agoText = 'in the future'; } $diff = $now->diff($ago); //$diff->w = floor($diff->d / 7); $w = floor($diff->d / 7); $d = $diff->d - ($w * 7); //$diff->d -= $diff->w * 7; $string = array( 'y' => 'year', 'm' => 'month', 'w' => 'week', 'd' => 'day', 'h' => 'hour', 'i' => 'minute', 's' => 'second', ); foreach ($string as $k => &$v) { // k is key, and v is value... Obviously if ($k === 'w') { if ($w) { $v = $w . ' ' . $v . ($w > 1 ? 's' : ''); } else { unset($string[$k]); } } else { if ($diff->$k) { $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : ''); } else { unset($string[$k]); } } } if (!$full) $string = array_slice($string, 0, 1); return $string ? implode(', ', $string) . " $agoText" : 'just now'; } function getCachedFileContentsOrUpdate($fileURL, $cacheDurationSecs = 15) { # TODO: Process the Warning # Warning: file_get_contents(https://eapl.mx/twtxt.net): # failed to open stream: HTTP request failed! HTTP/1.1 404 Not Found in $cacheFilePath = getCachedFileName($fileURL); // Check if cache file exists and it's not expired if (file_exists($cacheFilePath) && (time() - filemtime($cacheFilePath)) < $cacheDurationSecs) { return file_get_contents($cacheFilePath); } // File doesn't exist in cache or has expired, so fetch and cache it $contents = file_get_contents($fileURL); file_put_contents($cacheFilePath, $contents); return $contents; } function getCachedFileContents($filePath) { $cacheFile = getCachedFileName($filePath); // Check if cache file exists and it's not expired if (file_exists($cacheFile)) { return file_get_contents($cacheFile); } return null; } function updateCachedFile($filePath) { $cacheFilePath = getCachedFileName($filePath); # TODO: Report down URLs and stop loading them after a few tries # Get the last modification time of the local file $lastModifiedTime = file_exists($cacheFilePath) ? filemtime($cacheFilePath) : false; $lastModifiedHeader = $lastModifiedTime ? gmdate('D, d M Y H:i:s', $lastModifiedTime) . ' GMT' : null; #echo "lastModifiedHeader: $lastModifiedHeader
\n"; global $config; global $agentVersion; # TODO: Check this from the main page, not in this function if (!array_key_exists('public_txt_url', $config) || !array_key_exists('public_nick', $config)) { die("Check your config.ini file. 'public_txt_url' or 'public_nick' missing"); } $url = $config['public_txt_url']; $nick = $config['public_nick']; if (filter_var($url, FILTER_VALIDATE_URL) === FALSE) { die("Check your config.ini file. 'public_txt_url' not valid"); } # TODO: Add a validation for the nickname. For example at least 1 character. // if ($nick === "") { // die("Check your config.ini file. 'public_nick' not valid"); // } $agentName = 'timeline'; $userAgentHeader = "User-Agent: $agentName/$agentVersion (+$url; @$nick)\r\n"; $header = $lastModifiedHeader ? "If-Modified-Since: $lastModifiedHeader\r\n" : ''; $header .= $userAgentHeader; # Set up the HTTP context with the 'If-Modified-Since' header $options = [ 'http' => [ 'method' => 'GET', 'header' => $header, ] ]; $context = stream_context_create($options); $response = @file_get_contents($filePath, false, $context); # Check if HTTP headers are available, usually when the server is available if (!isset($http_response_header)) { # echo "Failed to fetch headers. No HTTP request was made.\n"; return; } if ($http_response_header) { # var_dump($http_response_header); foreach ($http_response_header as $header) { # Look for the Last-Modified header if (preg_match('/^Last-Modified:\s*(.+)$/i', $header, $matches)) { $dateString = $matches[1]; // Extracted date # echo "Extracted Date: $dateString\n"; // Convert to Unix timestamp $lastModifiedTimestamp = strtotime($dateString); if ($lastModifiedTimestamp > $lastModifiedTime) { # echo "Remote file is newer. Load it!
\n"; } else { # echo "Not modified since last request. No update needed.
\n"; return; } } } } # Save the content if it was successfully retrieved if ($response !== false) { file_put_contents($cacheFilePath, $response); #echo "File updated successfully.\n"; } } function getTwtsFromTwtxtString($url) { $fileContent = getCachedFileContents($url); if (is_null($fileContent)) { return null; } $fileContent = mb_convert_encoding($fileContent, 'UTF-8'); $fileLines = explode("\n", $fileContent); $twtxtData = new TwtxtFile(); foreach ($fileLines as $currentLine) { // Remove empty lines if (empty($currentLine)) { continue; } if (str_starts_with($currentLine, '#')) { // Check if comments (starting with #) have some metadata if (!is_null(getSingleParameter('url', $currentLine))) { $currentURL = getSingleParameter('url', $currentLine); if (empty($twtxtData->URLs)) { $twtxtData->mainURL = $currentURL; } $twtxtData->URLs[] = $currentURL; } if (!is_null(getSingleParameter('nick', $currentLine))) { $twtxtData->nick = getSingleParameter('nick', $currentLine); } if (!is_null(getSingleParameter('avatar', $currentLine))) { $twtxtData->avatar = getSingleParameter('avatar', $currentLine); } if (!is_null(getSingleParameter('emoji', $currentLine))) { $twtxtData->emoji = getSingleParameter('emoji', $currentLine); } if (!is_null(getSingleParameter('lang', $currentLine))) { $twtxtData->lang = getSingleParameter('lang', $currentLine); } if (!is_null(getSingleParameter('description', $currentLine))) { $twtxtData->description = getSingleParameter('description', $currentLine); // TODO - FIX BUG: only takes first word! } if (!is_null(getSingleParameter('follow', $currentLine))) { $twtxtData->following[] = getSingleParameter('follow', $currentLine); } } // Clean up nick if set to something like `@soren@darch.dk` instead of just `soren` - mosty for (re)feeds from Mastodon etc. if (str_contains($twtxtData->nick, "@")) { $str = $twtxtData->nick; $str = ltrim($str, "@"); $twtxtData->nick = explode("@", $str)[0]; // take the first [0] from splitting the nick at "@" } // Fallback for nick and url if not set in twtxt.txt if ($twtxtData->nick === "") { $str = parse_url($url, PHP_URL_HOST); $str = str_replace("www.", "", $str); //$str = explode(".", $str)[0]; // take the first [0] from splitting the host at "." $twtxtData->nick = $str; } if ($twtxtData->mainURL === "") { $twtxtData->mainURL = $url; } // Use only nick as handle if nick and domain is the same $twtxtData->domain = parse_url($twtxtData->mainURL, PHP_URL_HOST); if ($twtxtData->nick === $twtxtData->domain) { $twtxtData->domain = ""; } else { $twtxtData->domain = "@".$twtxtData->domain; } if (!str_starts_with($currentLine, '#')) { $explodedLine = explode("\t", $currentLine); if (count($explodedLine) >= 2) { $dateStr = $explodedLine[0]; $twtContent = $explodedLine[1]; $twtContent = replaceMentionsFromTwt($twtContent); // Convert HTML problematic characters //$twtContent = htmlentities($twtContent); // TODO: Messing up rendering of @mentions #BUG // Replace the Line separator character (U+2028) // \u2028 is \xE2 \x80 \xA8 in UTF-8 // Check here: https://www.mclean.net.nz/ucf/ //$twtContent = str_replace("\xE2\x80\xA8", "
\n", $twtContent); // For some reason I was having trouble finding this nomenclature // that's why I leave the UTF-8 representation for future reference //$twtContent = str_replace("\u{2028}", "\n
\n", $twtContent); $twtContent = str_replace("\u{2028}", "\n", $twtContent); $twtContent = embedYoutubeFromTwt($twtContent); // Get and remove the hash $hash = getReplyHashFromTwt($twtContent); if ($hash) { $twtContent = str_replace("(#$hash)", '', $twtContent); } // Convert HTML entities to their corresponding characters $twtContent = html_entity_decode($twtContent); // TODO: Make ?tag= filtering feature $twtContent = replaceTagsFromTwt($twtContent); // Interpret the content as markdown $Parsedown = new Parsedown(); $twtContent = $Parsedown->text($twtContent); // TODO: Remove obserlete fuctions, or build our own simpler markdown parser? //$twtContent = replaceMarkdownLinksFromTwt($twtContent); //$twtContent = replaceImagesFromTwt($twtContent); //$twtContent = replaceLinksFromTwt($twtContent); // TODO: Get mentions $mentions = getMentionsFromTwt($twtContent); // Get Lang metadata if (($timestamp = strtotime($dateStr)) === false) { //echo "The string ($dateStr) is incorrect"; // Incorrect date string, skip this twt continue; } else { $displayDate = getTimeElapsedString($timestamp); } // TODO: Only 1 twt by second is allowed here $twt = new Twt(); $twt->originalTwtStr = $currentLine; $twt->hash = getHashFromTwt($currentLine, $twtxtData->mainURL); $twt->timestamp = $timestamp; $twt->fullDate = date('j F Y h:i:s A', $timestamp) . ' (UTC)'; $twt->displayDate = $displayDate; $twt->content = $twtContent; $twt->replyToHash = $hash; $twt->mentions = $mentions; $twt->avatar = $twtxtData->avatar; $twt->emoji = $twtxtData->emoji; $twt->nick = $twtxtData->nick; $twt->mainURL = $twtxtData->mainURL; $twt->domain = $twtxtData->domain; $twtxtData->twts[$timestamp] = $twt; } } } return $twtxtData; } function insertFollowingURL($urlString) { // Check if it's a valid URL // Retrieve the nickname, if didn't find a nick, ask for one $originalCode = ' Lorem ipsum dolor sit amet, #~~~# consectetur adipiscing elit.'; $text = '#~~~#'; $newText = '123' . PHP_EOL . $text; $result = str_replace('#~~~#', $newText, $originalCode); echo $result; } function getCachedFileName($filePath) { return __DIR__ . '/../private/cache/' . hash('sha256', $filePath); // TODO: make better path } if (!function_exists('str_starts_with')) { function str_starts_with($haystack, $needle) { return (string)$needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0; } } if (!function_exists('str_ends_with')) { function str_ends_with($haystack, $needle) { return $needle !== '' && substr($haystack, -strlen($needle)) === (string)$needle; } } if (!function_exists('str_contains')) { function str_contains($haystack, $needle) { return $needle !== '' && mb_strpos($haystack, $needle) !== false; } }