diff --git a/Makefile b/Makefile index 3dce5b67..a1a74f14 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,13 @@ PAGEFIND ?= npx -y pagefind@1 # test server: the web share of barbel-lescar, mounted from the file manager (smb://barbel-lescar/web) DEPLOY_DIR ?= /run/user/$(shell id -u)/gvfs/smb-share:server=barbel-lescar,share=web/oldcomputers DEPLOY_URL ?= https://barbel.synology.me/oldcomputers/ +# forum on the server (comment discussions of the news posts, see forum-sync): FORUM_RUN runs a +# shell line where the forum lives; empty = no forum, deploy then skips the sync +FORUM_RUN ?= ssh -p 27 yannick@192.168.1.98 +FORUM_DIR ?= /volume2/web/oldcomputers-forum +FORUM_PHP ?= /usr/local/bin/php84 -d memory_limit=512M -.PHONY: content build check serve deploy post forum-up forum-import forum-down forum-bundle +.PHONY: content build check serve deploy post forum-up forum-import forum-down forum-bundle forum-sync content: ## regenerate content/ and static/media/ from the extraction repo python3 $(OC)/scripts/05_hugo.py --target $(CURDIR) $(if $(wildcard data/forum.json),--forum) @@ -27,7 +32,12 @@ post: ## new post: make post NAME=my-title, then edit content/post//embed/` (Flarum's Embed extension) in a frame that follows the height of the thread. +## News posts + +`php flarum oc:pages` creates one discussion per news post under the tag News (listed with the +other forum topics, unlike the machine threads), opened by the system account `News` with a +link to the post. `make forum-sync` in the website repository drives it: `tools/forum_sync.py` +sends the list of published posts to the forum host (`FORUM_RUN`, `FORUM_DIR`, `FORUM_PHP` in +the Makefile) and stores the answer in `data/forum_pages.json`. `make deploy` runs it first, so +a new post gets its discussion when it is published. A post that is renamed gets a new +discussion; a retitled one keeps its discussion and its old title there. + ## Local test make forum-up # http://localhost:8081, admin / adminadmin (first start takes a minute) diff --git a/forum/extension/extend.php b/forum/extension/extend.php index 68929936..63a07699 100644 --- a/forum/extension/extend.php +++ b/forum/extension/extend.php @@ -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), diff --git a/forum/extension/migrations/2026_10_12_000000_create_oc_page_threads_table.php b/forum/extension/migrations/2026_10_12_000000_create_oc_page_threads_table.php new file mode 100644 index 00000000..2527a176 --- /dev/null +++ b/forum/extension/migrations/2026_10_12_000000_create_oc_page_threads_table.php @@ -0,0 +1,11 @@ +string('page_key', 190)->primary(); + $table->unsignedInteger('discussion_id')->unique(); +}); diff --git a/forum/extension/src/Console/ImportCommand.php b/forum/extension/src/Console/ImportCommand.php index eba22109..ca136aed 100644 --- a/forum/extension/src/Console/ImportCommand.php +++ b/forum/extension/src/Console/ImportCommand.php @@ -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; + } } diff --git a/forum/extension/src/Console/PagesCommand.php b/forum/extension/src/Console/PagesCommand.php new file mode 100644 index 00000000..a8f30d53 --- /dev/null +++ b/forum/extension/src/Console/PagesCommand.php @@ -0,0 +1,72 @@ +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; + } +} diff --git a/layouts/_partials/comments.html b/layouts/_partials/comments.html index 52f09129..51f52c83 100644 --- a/layouts/_partials/comments.html +++ b/layouts/_partials/comments.html @@ -5,13 +5,7 @@ {{ if and $p.forum_thread site.Params.forum.url }}

Comments

- {{ $thread := printf "%s/embed/%d" (strings.TrimSuffix "/" site.Params.forum.url) (int $p.forum_thread) }} -

The first comments were left by visitors of the original site. Log in to the forum to add yours. Open this discussion in the forum.

- - {{ with resources.Get "js/iframeResizer.min.js" | fingerprint }} - - - {{ end }} + {{ partial "forum-thread.html" (dict "id" $p.forum_thread "title" .Title "note" "The first comments were left by visitors of the original site. Log in to the forum to add yours.") }}
{{ else if $p.comments }}
diff --git a/layouts/_partials/forum-thread.html b/layouts/_partials/forum-thread.html new file mode 100644 index 00000000..7d78e882 --- /dev/null +++ b/layouts/_partials/forum-thread.html @@ -0,0 +1,9 @@ +{{/* A forum discussion shown in a frame sized to its content. + dict "id" "title" "note" */}} +{{ $thread := printf "%s/embed/%d" (strings.TrimSuffix "/" site.Params.forum.url) (int .id) }} +

{{ .note }} Open this discussion in the forum.

+ +{{ with resources.Get "js/iframeResizer.min.js" | fingerprint }} + + +{{ end }} diff --git a/layouts/post/single.html b/layouts/post/single.html index 894d2b26..a60698ca 100644 --- a/layouts/post/single.html +++ b/layouts/post/single.html @@ -5,6 +5,13 @@

{{ if not .Date.IsZero }}{{ end }}{{ with .GetTerms "author" }}{{ if not $.Date.IsZero }} · {{ end }}{{ partial "terms.html" (dict "page" $ "taxonomy" "author") }}{{ end }}

{{ with partial "banner.html" (dict "page" . "width" 1000) }}{{ end }}
{{ .Content }}
+ {{/* comments: the post's forum discussion, created by `make forum-sync` (data/forum_pages.json) */}} + {{ with and site.Params.forum.url .File (index (hugo.Data.forum_pages | default dict) .File.Path) }} +
+

Comments

+ {{ partial "forum-thread.html" (dict "id" . "title" $.Title "note" "Log in to the forum to comment.") }} +
+ {{ end }}