Синхронизация/кросспостинг тем на разных сайтах Discourse

У меня сейчас есть тема на одном сайте, которую я хотел бы иметь также на другом. Хотя я мог бы просто добавить гиперссылку, мне бы очень хотелось иметь возможность редактировать её на любом из сайтов, чтобы изменения отображались на обоих. Это избавит меня от необходимости поддерживать одну версию темы, которая быстро устаревает, и позволит держать обе темы постоянно актуальными. Кроме того, это даст мне возможность децентрализовать информацию со своего сайта.

Некоторые идеи по функциональности:

  • В качестве отправной точки один сайт остаётся хостом/владельцем темы, а остальные по сути её зеркалят. В дальнейшем даже интересно подумать, можно ли каким-то образом передать тему зеркальному сайту, если оригинал будет удалён.
  • Тема должна сохранять возможность быть скрытой, закрытой и т.п. на зеркальных сайтах.
  • Ответы не должны синхронизироваться — у каждого сайта своя аудитория, поэтому я не вижу, как можно было бы синхронизировать ответы.

Я понимаю, что реализация такой функции далеко не проста, но мне интересно, рассматривалось ли это ранее и какие уже есть результаты или эксперименты?

2 лайка

Дублирование контента на нескольких сайтах — это большой запрет для SEO. Вероятнее всего, такая функция поддерживаться не будет.

Какой у этого сценарий использования или цель? Вы, по сути, хотите, чтобы другие сайты были резервной копией основного?

2 лайка

Не могли бы вы привести немного больше информации по этому поводу? С моей точки зрения, это, наоборот, уменьшит количество дублирующегося контента.

Создание резервных копий — не цель. Речь идет о создании единого источника истины для тем, которые уместны на нескольких сайтах.

Для наглядного примера: я занимаюсь оформлением визы. У меня есть тема в моем личном Discourse, которая по сути представляет собой чек-лист необходимых действий. Однако мои друзья тоже могут найти это полезным, поэтому я создаю такую же тему на нашем общем Discourse. Проблема в том, что мне нужно синхронизировать информацию в обеих темах отдельно, вместо того чтобы обновлять одну. Из-за этого часто в одной из тем не хватает ключевой информации.

Полагаю, это можно реализовать даже просто с помощью API-ключа для другого сайта? Возможно, что-то вроде кнопки или раздела в редакторе, где можно создать список API-ключей и URL-адресов для целевых тем. При внесении изменений в исходную тему вы могли бы нажать кнопку вроде «отправить изменения в клоны этой темы». Всё, что это сделает, — отправит обновление на темы в других инстансах.

2 лайка

Разместите информацию в одном единственном месте, где её сможет увидеть каждый. Ссылка — это способ сделать это.

Но у вас есть секретная информация, которую вы хотите сделать доступной в другом месте. Это уже другая история. Это вполне реализуемо с помощью плагина. Это тот тип проблемы, когда надуманное решение требует в 10 раз больше работы, чем сама проблема. (Я, например, часто трачу часы на автоматизацию задачи, которая должна выполняться лишь один раз.)

Но тогда вам нужно разместить его в таком месте, где он будет доступен только пользователю, который его опубликовал. Или сделать это общедоступным для всего сайта?

Снова же, они должны быть привязаны к каждому пользователю и обрабатываться в сериализаторе только для текущего пользователя (или, возможно, хранить их в профиле пользователя?). И вам потребуется структура данных для сопоставления API-ключей с различными сайтами. Похоже на задачу, которую, как мне кажется, я мог бы решить за 2–5 часов.

Таким образом, вам где-то нужно хранить URL-адреса для других сайтов, которые должны содержать эту тему. Создание такого поста тоже может быть сложным; самый простой способ — создать его вручную и включить URL этой темы с исходного сайта. Вероятно, это можно хранить в сыром тексте поста в виде какого-то BB-кода или чего-то подобного. Это позволит создать компонент, который сгенерирует кнопку и ссылку для каждого из них, а затем у вас будет код на Rails, который поставит в очередь задачу для отправки этого на другой сайт (сайты). Но принимающим сайтам не понадобится никакого кода — вы можете использовать API, чтобы отправить редактирование поста.

Похоже на задачу, которую, как мне кажется, я мог бы решить за 5–10 часов, но на деле это, скорее всего, займет в два раза больше времени. Если это для вас интересно, то это может стать крутым проектом.

4 лайка

Это также можно реализовать с помощью небольшого внешнего приложения, которое будет отслеживать вебхуки, отправляемые при редактировании на вашем исходном сайте, а затем использовать API для публикации на зеркальном сайте.

4 лайка

Я снова обдумывал этот вопрос.

Плагин мог бы добавить пользовательское поле темы для URL-адреса источника основного документа. (Полагаю, также потребовались бы поля для удалённого имени пользователя и API-ключа, если основной документ должен быть скрыт, как, кажется, требуется в вашем случае, но эту часть можно отложить. Или, возможно, они могли бы находиться в пользовательском поле профиля. Ответственность за обеспечение того, чтобы API-ключ имел права только на чтение, лежала бы на том, кто его создал).

При создании темы вы вводили бы что-то вроде «remote: https://meta.discourse.org/t/synchronising-crossposting-topics-across-different-discourse-sites/263269», и при создании темы Discourse извлекал бы исходный текст удалённой темы, вставлял его в поле raw как правку и создавал запись в topic_custom_field с удалённым URL, возможно, добавляя вверху фразу «скопировано с url».

На этом этапе вы скопировали удалённую тему локально и сохранили запись о ней.

Затем могла бы появиться кнопка «Проверить источник», которая извлекала бы удалённую тему и сохраняла значение updated_at удалённой темы, а возможно, и её raw, в других пользовательских полях (это также мог бы делать фоновый процесс периодически, что немного улучшило бы UX). Затем можно было бы добавить кнопку «Обновить», которая заменяла бы существующее поле raw на удалённое, оформляя это как правку.

Если основной сайт общедоступен, то эта часть реализуется очень легко. Добавление API-ключа для доступа к приватному сайту усложняет задачу, а управление набором API-ключей на нескольких сайтах — ещё больше. Если нужно было бы заменить первоисточник, возможно, это можно было бы сделать с помощью задачи remap в rake, или добавить возможность редактирования пользовательского поля с удалённым URL при необходимости.

Эта часть предоставляется автоматически, поскольку в данном решении вторичные сайты извлекают данные с основного.

Верно. И можно добавить ссылку обратно на сайт-источник, чтобы люди могли перейти туда и посмотреть эти комментарии, или, возможно, даже встроить их через Embed comments from Discourse in your single page app.

Если у вас есть хоть какой-то бюджет на это, не стесняйтесь связаться со мной.

1 лайк

Привет, Джей,

Эта мысль давно у меня в голове, но до недавнего времени она почти не развивалась. К сожалению, синхронизация между Discourse и Discourse потеряла для меня актуальность (речь шла лишь о нескольких темах), однако возросла потребность в общей синхронизации Markdown-файлов с других платформ.

Нашим основным сценарием использования в компании было размещение README-файлов и вики-страниц из проектов GitLab внутри Discourse (для обратной связи и поиска), при этом файлы в GitLab оставались единственным источником истины. Из-за отсутствия знаний Ruby я написал скрипт на Python, который, безусловно, избыточен по своей реализации, но вполне удовлетворителен по функционалу. Первая рабочая версия уже реализует часть того, что ты описал. Вот некоторые возможности:

  • содержит ссылку на оригинальный источник (файл в GitLab);
  • содержит ссылку на конкретную ревизию (номер коммита в GitLab);
  • обрабатывает изображения и ссылки на изображения:
    • загружает изображения из репозитория GitLab;
    • выгружает их в Discourse;
    • заменяет исходный URL изображения на короткую ссылку на загруженный файл;
  • добавляет тег “synced_with_gitlab”, чтобы их было легко найти.

Это работало примерно так же и с GitHub. У обеих платформ довольно схожий стиль Markdown, что делает процесс очень удобным.

Я бы с радостью сделал этот проект открытым, но сначала мне нужно уточнить юридические аспекты. Кроме того, текущая реализация на Python всё ещё выглядит немного кривой. В планах — переписать это в виде плагина на Ruby, но мне нужно будет посмотреть, смогу ли я найти на это время.

3 лайка

Верно. Я снова приступаю к этому проекту и сейчас рассматриваю создание базы данных со следующей структурой:

  1. Одна таблица на платформу (таблица Discourse, таблица GitLab и т.д.), чтобы учесть возможные нюансы.
  2. Каждая таблица платформы должна поддерживать как вебхуки, так и опрос через API-ключ.
  3. Шифрование базы данных или API-ключей — сейчас я считаю, что лучше шифровать всю базу данных и взаимодействовать с ней через скрипт и парольную фразу.

Таблица Discourse выглядела бы так:

Тип Интервал (мин) Последний запуск (мин) src_domain src_post_ID src_usr src_key tgt_domain tgt_post_id tgt_usr tgt_key
Вебхук - 120 meta.discourse.com 1280952 - - discourse.mysite.com 120 Tris xyz12345
Опрос 60 40 meta.discourse.com 1280953 Tris20 12345xyz discourse.mysite.com 121 Tris xyz12345
Опрос 60 35 meta.discourse.com 1750968 Tris20 12345xyz discourse.mysite.com 221 Tris xyz12345
Опрос 60 40 meta.discourse.com 1123292 Tris20 12345xyz discourse.mysite.com 131 Tris xyz12345
Вебхук - 4800 meta.discourse.com 1283678 - - discourse.mysite.com 129 Tris xyz12345

Не кажется ли это безумным и излишне усложнённым? Часть меня хочет создать для этого полноценное решение, что потребует учёта обоих вариантов — вебхуков и опроса.

Также я буду очень признателен за предложения по обеспечению безопасности содержимого базы данных. Сейчас я думаю о шифровании базы данных с помощью парольной фразы, которая должна быть передана в качестве аргумента при запуске скрипта, например:

discourse-sync run password123

В качестве небольшого вдохновения: вебхук может работать очень быстро:


Здесь показаны две темы на двух разных экземплярах. Тема слева — исходная, тема справа — целевая.

В Rails есть способ зашифровать одно поле. Именно это я и сделал в своей панели управления.

Смотрите: Active Record Encryption — Ruby on Rails Guides

Наличие и опроса, и вебхуков кажется избыточным. Я бы выбрал один из этих подходов.

1 лайк

Думаю, что плагин ActivityPub в ближайшее время получит возможность федерации между инстансами Discourse, если это вообще возможно?

3 лайка

Кстати, эта идея тоже не раз приходила мне в голову. Не уверен, что у нас уже есть отдельная тема об этом, но если нет, то нам стоит её создать!

2 лайка

Теоретически согласен, но корпоративный мир всё усложняет:

  1. Вебхуки в нашей компании невозможны из-за политик ИТ-безопасности (мы размещены за пределами компании через CDCK, и настройка проброса портов наружу требует сложных процедур) — поэтому работа через API обязательна.
  2. Вебхуки быстры, удобны и идеально подходят для всех остальных, поэтому имеет смысл поддерживать и их тоже :slight_smile:

Так что я уже реализовал грубую версию этого решения некоторое время назад. Работает отлично, но не масштабируемо. Плохой дизайн с моей стороны: я экспериментировал с идеей использовать тему с таблицей Markdown в качестве входных данных. Отлично работает, пока записей немного, но при 30+ записях всё превращается в кашу.

Один из сценариев использования Discourse-to-Discourse, который я вижу, — это единый источник истины для документации (Meta), синхронизирующийся с соответствующими постами на других инстансах. Это означает, что если команда изменит Core и обновит документацию пользователя в Meta, у меня будет актуальная версия этой документации на моём инстансе, доступная всем пользователям.

Для версии 2 я планирую использовать базу данных SQLite в качестве входных данных, как указано выше, и, вероятно, писать на Rust вместо Python.

Ниже приведён примерный набросок схемы.

graph TB
    A[terminal] -- ~>discourse-sync run $PASSWORD --> B[Rust Script]
    B -- SQLCipher:Decrypt DB using Password --> C[( sqlite: sources-and-targets')]
    C -- 'Discourse' Table Data --> B
    B -.- D{Decision on Type}
    D -- Webhook --> E[Listen for Webhook Info]
    D -- Polling --> F[Polling API]
    E --> G[Receive New Information]
    F --> G
    G --> H[Parse and Process Data]
    H --> I[POST\n tgt_domain, tgt_usr, tgt_key, post_id]
    I --'raw' and images--> J[ Target Post ]

    subgraph Rust Script Operations
    B
    D
E
F
G
H
I
    end

Буду очень рад получить обратную связь и предложения по этому решению :slightly_smiling_face:

2 лайка

@angus, думаю, это будет вам интересно. Я считаю, что мы уже во многом решаем эту проблему с помощью плагина ActivityPub.

7 лайков

Да, плагин ActivityPub теперь действительно поддерживает эту функцию. Мы очень близки к тому, чтобы начать использовать его internally для синхронизации документации между meta и внутренним экземпляром; это, кстати, включено в мой список задач на следующую неделю.

8 лайков

Применяется ли это к обоим экземплярам? Например, если источник публичный, а цель приватная, будет ли это работать?

Насколько бы ни был хорош плагин ActivityPub, я боюсь, что он может не подойти для приватных экземпляров.

1 лайк

Это действительно относится к приватным экземплярам. В текущей версии плагина ActivityPub приватные экземпляры могут подписываться на категории в публичных экземплярах Discourse и, следовательно, получать опубликованную активность из этих экземпляров. (Однако контент в приватном экземпляре не публикуется, поэтому синхронизация осуществляется только в одном направлении: из публичного в приватный.)

4 лайка

Получается, что ActivityPub может работать следующим образом:

Публичный → Публичный :white_check_mark:
Публичный → Приватный :white_check_mark:

Приватный → Публичный :x:
Приватный → Приватный :x:

Это, безусловно, полезно во многих случаях, например, для трансляции документации от Meta. К сожалению, это пока не покрывает один из моих сценариев использования: публикацию с приватного экземпляра на другой приватный экземпляр. Судя по тому, что я нашёл, правильно ли я понимаю, что плагин ActivityPub вряд ли будет поддерживать такие сценарии в будущем? Мне кажется, что ActivityPub был разработан с расчётом на взаимодействие «публичный — публичный».

3 лайка

@Tris20, интересно! Спасибо, что поделились своими мыслями и некоторыми деталями по этому поводу.

То, что вы описали, — это одна из проблем, для решения которых и был создан ActivityPub. Не хочу слишком сильно охлаждать ваш энтузиазм, но, честно говоря, вам предстоит столкнуться с широким спектром задач, пытаясь реализовать это так, как вы описываете. Я не буду приводить исчерпывающий перечень всех препятствий, с которыми вам придётся справиться, но чтобы дать представление: плагин ActivityPub уже содержит почти 700 тестов rspec, и после года разработки он лишь недавно получил полноценную поддержку синхронизации тем между темами.

Я не вижу никаких внутренних ограничений на поддержку публикации «частный — публичный» и «частный — частный» через ActivityPub. Вопрос заключается в обеспечении соблюдения требований безопасности и контроля доступа при работе с частными экземплярами.

Позвольте предложить вам кое-что. Возможно, стоит подумать, как можно опираться на работу, уже проделанную плагином ActivityPub (и самим стандартом ActivityPub) в этом направлении. На самом деле уже существует решение для синхронизации контента между экземплярами Discourse, которое работает на данный момент. Оно пока не покрывает ваш сценарий, но решает большинство проблем, которые вам нужно будет преодолеть для достижения ваших целей.

Возможно, стоит поразмыслить о том, как может быть реализована синхронизация «частный — частный» в плагине, то есть как могут быть решены вопросы доступа и безопасности? Тогда, возможно, мы сможем вместе поработать над запросом на слияние (PR), чтобы добавить эту функцию. Может быть, вы дойдёте до точки, когда поймёте, что реализовать желаемое в контексте плагина (или стандарта ActivityPub) невозможно, но работа, проделанная вами для достижения этой точки, будет по сути такой же, как и для независимого решения, так что она не пропадёт даром.

В мире ActivityPub много умных людей, и меня бы не удивило, если бы подобная проблема (то есть публикация из частных источников) уже была глубоко рассмотрена ранее. Одним из мест, где можно найти соответствующие наработки, является SocialHub — основной форум сообщества ActivityPub (естественно, на платформе Discourse), который сейчас использует плагин Discourse ActivityPub :slight_smile:

8 лайков