Comments on news posts: one forum discussion per post
oc:pages command in the forum extension, tools/forum_sync.py and 'make forum-sync' (run by deploy) keep data/forum_pages.json up to date; post pages end with their discussion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDRjtmAiNpo5mK5N3d7QwZ
This commit is contained in:
@@ -2,10 +2,11 @@
|
||||
|
||||
use Flarum\Extend;
|
||||
use Oc\Museum\Console\ImportCommand;
|
||||
use Oc\Museum\Console\PagesCommand;
|
||||
use Oc\Museum\DisplayName;
|
||||
|
||||
return [
|
||||
(new Extend\Console())->command(ImportCommand::class),
|
||||
(new Extend\Console())->command(ImportCommand::class)->command(PagesCommand::class),
|
||||
|
||||
// selected by oc:import (setting display_name_driver = museum); needs the Nicknames extension
|
||||
(new Extend\User())->displayNameDriver('museum', DisplayName::class),
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
// One discussion per commentable page of the site other than machines (news posts);
|
||||
// page_key is the page's content file, e.g. "post/2026-10-11-we-are-back.md".
|
||||
return Migration::createTable('oc_page_threads', function (Blueprint $table) {
|
||||
$table->string('page_key', 190)->primary();
|
||||
$table->unsignedInteger('discussion_id')->unique();
|
||||
});
|
||||
@@ -43,7 +43,7 @@ class ImportCommand extends AbstractCommand
|
||||
'Pongs' => ['#15803d', 'fas fa-table-tennis-paddle-ball'],
|
||||
];
|
||||
|
||||
public function __construct(private ConnectionInterface $db, private SettingsRepositoryInterface $settings)
|
||||
public function __construct(protected ConnectionInterface $db, protected SettingsRepositoryInterface $settings)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -75,7 +75,7 @@ class ImportCommand extends AbstractCommand
|
||||
$this->settings->set('display_name_driver', 'museum');
|
||||
|
||||
$site = rtrim((string) $this->input->getOption('site-url'), '/');
|
||||
$archive = $this->archiveUser();
|
||||
$archive = $this->systemUser();
|
||||
$this->group = $this->group();
|
||||
$this->loadAuthors();
|
||||
$tags = $this->tags();
|
||||
@@ -138,7 +138,7 @@ class ImportCommand extends AbstractCommand
|
||||
return $discussion->id;
|
||||
}
|
||||
|
||||
private function post(Discussion $discussion, User $user, Carbon $when, string $markdown): CommentPost
|
||||
protected function post(Discussion $discussion, User $user, Carbon $when, string $markdown): CommentPost
|
||||
{
|
||||
$post = new CommentPost();
|
||||
$post->discussion_id = $discussion->id;
|
||||
@@ -202,7 +202,7 @@ class ImportCommand extends AbstractCommand
|
||||
/** --replace: forget imported threads in which only archive accounts have posted. */
|
||||
private function removeUntouchedThreads(): void
|
||||
{
|
||||
$archived = $this->group->users()->pluck('users.id')->push($this->archiveUser()->id)->all();
|
||||
$archived = $this->group->users()->pluck('users.id')->push($this->systemUser()->id)->all();
|
||||
$removed = $kept = 0;
|
||||
foreach ($this->db->table('oc_threads')->pluck('discussion_id', 'oc_id') as $ocId => $discussionId) {
|
||||
$discussion = Discussion::find($discussionId);
|
||||
@@ -232,14 +232,14 @@ class ImportCommand extends AbstractCommand
|
||||
return $group;
|
||||
}
|
||||
|
||||
private function archiveUser(): User
|
||||
protected function systemUser(string $name = self::ARCHIVE_USER): User
|
||||
{
|
||||
$user = User::query()->where('username', self::ARCHIVE_USER)->first();
|
||||
$user = User::query()->where('username', $name)->first();
|
||||
if (! $user) {
|
||||
$user = new User();
|
||||
$user->username = self::ARCHIVE_USER;
|
||||
$user->email = 'archive@invalid.invalid';
|
||||
$user->password = Str::random(40); // nobody logs in as the archive
|
||||
$user->username = $name;
|
||||
$user->email = Str::lower($name).'@invalid.invalid';
|
||||
$user->password = Str::random(40); // nobody logs in as a system account
|
||||
$user->is_email_confirmed = true;
|
||||
$user->joined_at = Carbon::now();
|
||||
$user->save();
|
||||
@@ -252,19 +252,24 @@ class ImportCommand extends AbstractCommand
|
||||
private function tags(): array
|
||||
{
|
||||
$tags = [];
|
||||
$position = (int) Tag::query()->max('position');
|
||||
foreach (self::TAGS as $name => [$color, $icon]) {
|
||||
$tag = Tag::query()->where('slug', Str::slug($name))->first();
|
||||
if (! $tag) {
|
||||
// hidden from "All discussions": one thread per machine would bury the forum topics
|
||||
$tag = Tag::build($name, Str::slug($name), "Comments on the $name pages of the museum", $color, $icon, true);
|
||||
$tag->is_primary = true;
|
||||
$tag->position = ++$position;
|
||||
$tag->save();
|
||||
}
|
||||
$tags[$name] = $tag;
|
||||
// hidden from "All discussions": one thread per machine would bury the forum topics
|
||||
$tags[$name] = $this->tag($name, "Comments on the $name pages of the museum", $color, $icon, true);
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
protected function tag(string $name, string $description, string $color, string $icon, bool $hidden): Tag
|
||||
{
|
||||
$tag = Tag::query()->where('slug', Str::slug($name))->first();
|
||||
if (! $tag) {
|
||||
$tag = Tag::build($name, Str::slug($name), $description, $color, $icon, $hidden);
|
||||
$tag->is_primary = true;
|
||||
$tag->position = (int) Tag::query()->max('position') + 1;
|
||||
$tag->save();
|
||||
}
|
||||
|
||||
return $tag;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Oc\Museum\Console;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Creates the discussion in which a page of the site is commented, for pages that are not
|
||||
* museum machines: the news posts. Input is the list written by tools/forum_sync.py of the
|
||||
* website repository. Pages that already have a discussion (table oc_page_threads) are skipped.
|
||||
*/
|
||||
class PagesCommand extends ImportCommand
|
||||
{
|
||||
private const SYSTEM_USER = 'News';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('oc:pages')
|
||||
->setDescription('Create the comment discussions of the site\'s news posts')
|
||||
->addArgument('file', InputArgument::REQUIRED, 'pages.json written by tools/forum_sync.py')
|
||||
->addOption('site-url', null, InputOption::VALUE_REQUIRED, 'Address of the static site, e.g. https://example.org/oldcomputers', '')
|
||||
->addOption('map', null, InputOption::VALUE_REQUIRED, 'Where to write the page -> discussion map (forum_pages.json)');
|
||||
}
|
||||
|
||||
protected function fire(): int
|
||||
{
|
||||
$pages = json_decode((string) file_get_contents($this->input->getArgument('file')), true);
|
||||
if (! is_array($pages)) {
|
||||
$this->error('Cannot read the pages file.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
$site = rtrim((string) $this->input->getOption('site-url'), '/');
|
||||
$user = $this->systemUser(self::SYSTEM_USER);
|
||||
// unlike the machine threads, news discussions are listed with the other forum topics
|
||||
$tag = $this->tag('News', 'Comments on the news posts of the site', '#b45309', 'fas fa-newspaper', false);
|
||||
$done = $this->db->table('oc_page_threads')->pluck('discussion_id', 'page_key');
|
||||
$created = 0;
|
||||
|
||||
foreach ($pages as $page) {
|
||||
if (isset($done[$page['key']])) {
|
||||
continue;
|
||||
}
|
||||
$this->db->transaction(function () use ($page, $site, $user, $tag, &$done) {
|
||||
$when = Carbon::parse($page['date'] ?: 'now');
|
||||
$discussion = Discussion::start($page['title'], $user);
|
||||
$discussion->created_at = $when;
|
||||
$discussion->save();
|
||||
$first = $this->post($discussion, $user, $when,
|
||||
"Comments on the news post [{$page['title']}]({$site}{$page['path']}).");
|
||||
$discussion->setFirstPost($first)->refreshCommentCount()->refreshLastPost()->refreshParticipantCount()->save();
|
||||
$discussion->tags()->sync([$tag->id]);
|
||||
$this->db->table('oc_page_threads')->insert(['page_key' => $page['key'], 'discussion_id' => $discussion->id]);
|
||||
$done[$page['key']] = $discussion->id;
|
||||
});
|
||||
$created++;
|
||||
}
|
||||
|
||||
$user->refreshCommentCount()->refreshDiscussionCount()->save();
|
||||
$tag->discussion_count = $tag->discussions()->count();
|
||||
$tag->refreshLastPostedDiscussion()->save();
|
||||
if ($map = $this->input->getOption('map')) {
|
||||
file_put_contents($map, json_encode($done, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n");
|
||||
}
|
||||
$this->info("$created discussions created; ".count($done).' pages mapped.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user