Author: Jim Winstead (jimwins) Committer: GitHub (web-flow) Pusher: jimwins Date: 2024-09-02T12:19:19-07:00
Commit: https://github.com/php/web-news/commit/b7d1acc70bd8e2cbdfd8ea2c4b16fd319fe3b70a Raw diff: https://github.com/php/web-news/commit/b7d1acc70bd8e2cbdfd8ea2c4b16fd319fe3b70a.diff Show thread information for each article (#24) Changed paths: A lib/ThreadTree.php M article.php M lib/Web/News/Nntp.php Diff: diff --git a/article.php b/article.php index 244f93b..a617e58 100644 --- a/article.php +++ b/article.php @@ -1,6 +1,7 @@ <?php require 'common.php'; +require 'lib/ThreadTree.php'; if (isset($_GET['article'])) { $article = (int)$_GET['article']; @@ -227,6 +228,36 @@ echo " </pre>\n"; echo " </blockquote>\n"; +try { + $overview = $nntpClient->getThreadOverview($group, $article); + + $threads = new \PhpWeb\ThreadTree($overview['articles']); + ?> + <blockquote> + <h2> + Thread (<?= sprintf("%d message%s", $count = $threads->count(), $count > 1 ? 's' : '') ?>) + </h2> + <div class="responsive-table"> + <table class="standard"> + <thead> + <tr> + <th>#</th> + <th>Subject</th> + <th>Author</th> + <th>Date</th> + </tr> + </thead> + <tbody> + <?php $threads->printRows($group, 'utf8'); ?> + </tbody> + </table> + </div> + </blockquote> + <?php +} catch (\Throwable $t) { + // We don't care if there's no thread. (There should be, though.) +} + // Does not check existence of next, so consider this the super duper fast [broken] version // Based off navbar() in group.php $group = htmlspecialchars($group, ENT_QUOTES, "UTF-8"); diff --git a/lib/ThreadTree.php b/lib/ThreadTree.php new file mode 100644 index 0000000..f9c09c3 --- /dev/null +++ b/lib/ThreadTree.php @@ -0,0 +1,97 @@ +<?php + +namespace PhpWeb; + +class ThreadTree +{ + public $articles; + public $root; + public $tree = []; + public $articleNumbers = []; + public $extraRootChildren = []; + + public function __construct(array $articles) + { + $this->articles = $articles; + + /* + * We need to build a tree of the articles. We know they are in article + * number order, we assume that this means that parents come before + * children. There may end up being some posts that appear unattached + * and we just assume they are replies to the root of the tree. + */ + foreach ($this->articles as $articleNumber => $details) { + $messageId = $details['messageId']; + + if (!isset($this->root)) { + $this->root = $messageId; + } + + $this->articleNumbers[$messageId] = $articleNumber; + + if ($details['references']) { + /* Parent is the last reference. */ + if (preg_match('/.*(<.+?>)$/', $details['references'], $matches)) { + $parent = $matches[1]; + $this->tree[$parent][] = $messageId; + if (!array_key_exists($parent, $this->articleNumbers)) { + $this->extraRootChildren[] = $parent; + } + } + } else { + if ($this->root && $this->root != $messageId) { + $this->extraRootChildren[] = $messageId; + } + } + } + } + + public function count() + { + return count($this->articleNumbers); + } + + protected function printArticleAndChildren($messageId, $group, $charset, $depth = 0) + { + if (array_key_exists($messageId, $this->articleNumbers)) { + $articleNumber = $this->articleNumbers[$messageId]; + + # for debugging that we've actually handled all articles + #unset($this->articleNumbers[$messageId]); + + $details = $this->articles[$articleNumber]; + + echo " <tr>\n"; + echo " <td align=\"center\"><a href=\"/$group/$articleNumber\">$articleNumber</a></td>\n"; + echo " <td>"; + echo str_repeat(" ", $depth ?? 0); + echo "<a href=\"/$group/$articleNumber\">"; + echo format_subject($details['subject'], $charset); + echo "</a></td>\n"; + echo " <td class=\"vcard\">" . format_author($details['author'], $charset) . "</td>\n"; + echo " <td class=\"align-center\"><span class='monospace mod-small'>" . + format_date($details['date']) . "</span></td>\n"; + echo " </tr>\n"; + } + + // bail out if things are too deep + if ($depth > 40) { + error_log("Tree was too deep, didn't print children of {$messageId})"); + return; + } + + if (array_key_exists($messageId, $this->tree)) { + foreach ($this->tree[$messageId] as $child) { + $this->printArticleAndChildren($child, $group, $charset, $depth + 1); + } + } + } + + public function printRows($group, $charset = 'utf8') + { + $this->printArticleAndChildren($this->root, $group, $charset); + foreach ($this->extraRootChildren as $root) { + $this->printArticleAndChildren($root, $group, $charset, 1); + } + } +} diff --git a/lib/Web/News/Nntp.php b/lib/Web/News/Nntp.php index e76902d..b6587f3 100644 --- a/lib/Web/News/Nntp.php +++ b/lib/Web/News/Nntp.php @@ -189,6 +189,48 @@ public function getArticlesOverview($group, $start, $pageSize = 20) return $overview; } + /** + * Returns an overview of the articles in the same thread as the specified + * message + * + * @param string $group The name of the group to select + * @param int $article The number of an article in the thread + * @return array + */ + public function getThreadOverview($group, $article) + { + $groupDetails = $this->selectGroup($group); + + $overview = [ + 'group' => $groupDetails, + 'articles' => [], + ]; + + $response = $this->sendCommand("XTHREAD {$article}", 224); + + while ($line = fgets($this->connection)) { + if ($line == ".\r\n") { + break; + } + + $line = rtrim($line); + list($n, $subject, $author, $date, $messageId, $references, $lines, $extra) = explode("\t", $line, 9); + + $overview['articles'][$n] = [ + 'subject' => $subject, + 'author' => $author, + 'date' => $date, + 'messageId' => $messageId, + 'references' => $references, + 'lines' => $lines, + 'extra' => $extra, + ]; + + } + + return $overview; + } + /** * Returns the full content of the specified article (headers and body) *