再见 Firefish (Firefish 迁移 Sharkey 指北)

目录

我是去年的 11 月知道的 Firefish。它是我当时找到的最美丽的联邦宇宙软件,相比于死板的 Mastodon,过于倾向于日语又太娱乐的 Misskey,它同时兼具美观大方与功能丰富。管理员可以自由配置最大帖子长度,Nodejs的后端也让它比 Ruby on Rails 编写的 Mastodon 对性能要求更低。我欣然安装上了它,并把它作为自己的主要联邦宇宙软件。

可惜,当时我还不知道它的状态已经岌岌可危。就在当时,原有的主要贡献者大多已经离开,panos 和 namekuji 离开了这摊烂摊子开启了新项目 Catodon。数个月后,Firefish 的 maintainer Kainoa 就直接撒手不管,在没有事先告知的情况下将maintainer更改给 naskya。naskya 非常负责且具有热爱,在伊的代理下 Firefish 回光返照了一段时间 —— 我也是那个时候尝试了为 Firefish 进行贡献。

可惜 maintainer 的名头为伊带来了巨大的压力与恶心的网络骚扰者。 naskya 的身体和精神状态都无法负担这样的项目,不到一年之后,Firefish 再次被抛弃。这次,大约再也没人能救 Firefish 了。

Goodbye Firefish

我很难不把 Kainoa 的行为称为一种残忍,他将一团糟的 Firefish 项目丢给毫无准备的 naskya ,此时伊甚至还没有过什么编程经验。但 naskya 的确是一个非常负责的人。伊接手 Firefish 的时候就知道自己的身体和精神状态糟糕,但还是努力维护了 Firefish 一段时间。

naskya 留给 Firefish 最后的体面。 伊维护了 Firefish 代码库中的 downgrade.sql (/docs/downgrade.sql),这份文件提供了 Firefish 从当前版本降级到 Firefish 1.05 rc-1 的能力,也为迁移提供了可能。没有这份文件,目前运营 Firefish 的服务器很可能都会再也无法迁移到其他版本。

这篇笔记也将告诉你如何用这份文件迁移你的 Firefish 到 Sharkey.

Sharkey 是基于 Misskey 的另一个 Fork,其功能与 Firefish 相似。

本笔记适用于 Firefish 1.05 (即 v20240206 )以上。如果您的 Firefish 版本低于 1.05,可以直接按照其他软件的官方说明迁移。您完全不升级的吗?那还迁移什么呢?

准备工作

在接下来的操作前,请务必备份你的数据库。 迁移前请务必备份你的数据库,因为这是一个非常非常危险的操作,一旦失误会弄坏你的数据库。

请先在本地测试环境中按该笔记进行操作,再在生产环境操作。 原因同上。

本教程基于 Manual 安装而非 Docker 安装,如果你使用的是 Docker 安装,关于数据库的操作很可能有区别。抱歉我无法提供这方面的帮助,请自行将下面的命令翻译为 Docker 上的数据库操作。

备份

如果你要正式开始迁移工作,请务必在备份前停止 Firefish。 但如果你只是现在本地环境中进行测试,就不必了。

使用 pg_dump 将你的数据库进行备份。命令大约如下:

pg_dump {your_database_name} -f {backupname}.sql

例如,如果你的数据库名是 firefish,可以这样:

pg_dump firefish -f firefish_20240906.sql

对于整篇笔记,我们假定您的数据库名是 firefish 。数据库用户名也是 firefish, 密码是 firefish_pswd。请在下文自行替换对应名称。

数据库的用户名和密码可以在 Firefish 目录下的 .config/default.yml 找到

你可以考虑在导出 sql 的时候不导出权限和用户名,这样可以方便迁移到其他用户和其他数据库名上。否则,如果你不是原地迁移。后面的迁移可能会遇到权限问题。如果遇到了这样的问题,你可以看看 附录:权限错误与解决方法

  • --no-owner:不导出对象所有者信息。
  • --no-privileges--no-acl:不导出对象的权限(访问控制列表)。

还原

如果您在本地测试环境,或者其他服务器中尝试接下来的步骤,您现在已经可以将您的备份文件下载到对应环境了。

或者,如果您的 Firefish 还在使用 Postgres 12 甚至以下,数据库的版本已经过旧,您可以趁机升级一下数据库版本。

您接下来可以使用这样的命令恢复数据库:

psql postgres

进入 postgres。

如果你是还原备份,此时已经存在 firefish 数据库,请把它删掉:

DROP DATABASE firefish;

此后,执行:

CREATE DATABASE firefish;
exit

创建 firefish 数据库。

现在,执行这样的命令:

psql -U {your_username} -d {your_database} -f {your_backup}.sql

例如,对 firefish 用户还原 firefish 数据库,你可以:

psql -U firefish -d firefish -f path/to/backup.sql

降级

该内容来自 Firefish 官方降级文档,有使用自己经验的修改。

不要升级你现在使用的 Firefish 版本。在 firefish 软件的根目录原地进行此操作:

psql --file=docs/downgrade.sql --user=your_user_name --dbname=your_database_name

例如,本案例中是

psql --file=docs/downgrade.sql --user=firefish --dbname=firefish

If you get the FATAL: Peer authentication failed error, you also need to provide the --host option (you will be asked the password):

psql --file=docs/downgrade.sql --user=your_user_name --dbname=your_database_name --host=127.0.0.1

因为你是要迁移到 Sharkey,成功执行到这里后就不必按官方降级文档的继续了。到这里,你可以对 Firefish say bye-bye了,后面不再需要用到它。

安装 Sharkey 仓库

选你中意的位置,跳过新建用户的步骤(因为你的 Firefish 已经有用户了!)安装 Sharkey 直到 initialize 数据库的步骤:

git clone --recurse-submodules -b stable https://activitypub.software/TransFem-org/Sharkey.git
cd Sharkey
pnpm install --frozen-lockfile
cp .config/example.yml .config/default.yml

编辑 .config/default.yml 使其与你的 Firefish 使用相同的数据库

vim .config/default.yml

Build

pnpm run build

接下来按照迁移说明进行操作,这里为了 Manual install 做了修改:

psql 到你的 firefish 数据库,执行这样的 SQL:

-- start a transaction, so we won't leave the db in a halfway state if
-- things go wrong
BEGIN;

-- we need to add back some columns that Firefish removed, but that
-- Sharkey migrations expect
ALTER TABLE "user_profile" ADD "integrations" JSONB NOT NULL DEFAULT '{}';
ALTER TABLE "meta" ADD "twitterConsumerSecret" VARCHAR(128);
ALTER TABLE "meta" ADD "twitterConsumerKey" VARCHAR(128);
ALTER TABLE "meta" ADD "enableTwitterIntegration" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "meta" ADD "enableGithubIntegration" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "meta" ADD "githubClientId" VARCHAR(128);
ALTER TABLE "meta" ADD "githubClientSecret" VARCHAR(128);
ALTER TABLE "meta" ADD "enableDiscordIntegration" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "meta" ADD "discordClientId" VARCHAR(128);
ALTER TABLE "meta" ADD "discordClientSecret" VARCHAR(128);

-- also an extra table, for the same reasons
CREATE TABLE antenna_note();

-- Misskey used to have a Reversi game, Firefish dropped the tables,
-- now Misskey uses them again
CREATE TABLE "reversi_game" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "startedAt" TIMESTAMP WITH TIME ZONE, "user1Id" character varying(32) NOT NULL, "user2Id" character varying(32) NOT NULL, "user1Accepted" boolean NOT NULL DEFAULT false, "user2Accepted" boolean NOT NULL DEFAULT false, "black" integer, "isStarted" boolean NOT NULL DEFAULT false, "isEnded" boolean NOT NULL DEFAULT false, "winnerId" character varying(32), "surrendered" character varying(32), "logs" jsonb NOT NULL DEFAULT '[]', "map" character varying(64) array NOT NULL, "bw" character varying(32) NOT NULL, "isLlotheo" boolean NOT NULL DEFAULT false, "canPutEverywhere" boolean NOT NULL DEFAULT false, "loopedBoard" boolean NOT NULL DEFAULT false, "form1" jsonb DEFAULT null, "form2" jsonb DEFAULT null, "crc32" character varying(32), CONSTRAINT "PK_76b30eeba71b1193ad7c5311c3f" PRIMARY KEY ("id"));
CREATE INDEX "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt");
CREATE TABLE "reversi_matching" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "parentId" character varying(32) NOT NULL, "childId" character varying(32) NOT NULL, CONSTRAINT "PK_880bd0afbab232f21c8b9d146cf" PRIMARY KEY ("id"));
CREATE INDEX "IDX_b604d92d6c7aec38627f6eaf16" ON "reversi_matching" ("createdAt");
CREATE INDEX "IDX_3b25402709dd9882048c2bbade" ON "reversi_matching" ("parentId");
CREATE INDEX "IDX_e247b23a3c9b45f89ec1299d06" ON "reversi_matching" ("childId");

-- move aside some FireFish columns; Sharkey migrations will
-- re-create them; we don't `DROP` them because we want to keep the data
ALTER TABLE "user" RENAME COLUMN "movedToUri" TO "ff_movedToUri";
ALTER TABLE "user" RENAME COLUMN "alsoKnownAs" TO "ff_alsoKnownAs";
ALTER TABLE "user" RENAME COLUMN "isIndexable" TO "ff_isIndexable";
ALTER TABLE "user" RENAME COLUMN "speakAsCat" TO "ff_speakAsCat";
ALTER TABLE "user_profile" RENAME COLUMN "preventAiLearning" TO "ff_preventAiLearning";
ALTER TABLE "meta" RENAME COLUMN "silencedHosts" TO "ff_silencedHosts";

-- this column was added by both Firefish and Misskey, but with
-- different names, let's fix it
ALTER TABLE "meta" RENAME COLUMN "ToSUrl" TO "termsOfServiceUrl";

-- update antenna types, this is only needed on some instances but
-- recommend to run anyway
--
-- this *removes* any antennas of types not supported by Sharkey!
CREATE TYPE public.new_antenna_src_enum AS ENUM ('home', 'all', 'list');
ALTER TABLE antenna ADD COLUMN new_src public.new_antenna_src_enum;
DELETE FROM antenna WHERE src NOT IN ('home', 'all', 'list');
ALTER TABLE antenna DROP COLUMN src;
ALTER TABLE antenna RENAME COLUMN new_src TO src;
DROP TYPE public.antenna_src_enum;
ALTER TYPE new_antenna_src_enum RENAME TO antenna_src_enum;

-- optional but recommended: delete all empty moderation log entries
DELETE FROM moderation_log WHERE info = '{}';

-- only needed on some instances, run this if
-- `\dT+ user_profile_mutingnotificationtypes_enum`
-- does not show `note` in the "elements" section
ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" ADD VALUE 'note';

如果有任何报错信息,请停止迁移!除非你真的知道自己在干什么,不要乱动数据库。去 Sharkey 提供的 Matrix or Discord 求助。

如果没有报错,你就可以接着输入

COMMIT;

提交这些变更。

好了,现在你可以启动 Sharkey 的 migrations 了。在 Sharkey 的目录:

pnpm run migrate
pnpm run start

如果两条命令都没有报错的完成了,并且 Sharkey 说它正在监听端口, Ctrl+C 关闭 Sharkey。现在,还要对数据库进行一些小小的按摩(?)

仍然是

psql firefish

到你的数据库:

BEGIN;

-- all existing users are approved, because Firefish doesn't have a
-- concept of approvals
UPDATE "user" SET approved = true;

-- now we put back the data we moved aside
UPDATE "user" SET "movedToUri" = "ff_movedToUri" WHERE "ff_movedToUri" IS NOT NULL;
UPDATE "user" SET "alsoKnownAs" = "ff_alsoKnownAs" WHERE "ff_alsoKnownAs" IS NOT NULL;
UPDATE "user" SET "noindex" = NOT (COALESCE("ff_isIndexable", true));
UPDATE "user" SET "speakAsCat" = COALESCE("ff_speakAsCat", false);
UPDATE "user_profile" SET "preventAiLearning" = COALESCE("ff_preventAiLearning", true);
UPDATE "meta" SET "silencedHosts" = COALESCE("ff_silencedHosts",'{}');

ALTER TABLE "user" DROP COLUMN "ff_movedToUri";
ALTER TABLE "user" DROP COLUMN "ff_alsoKnownAs";
ALTER TABLE "user" DROP COLUMN "ff_isIndexable";
ALTER TABLE "user" DROP COLUMN "ff_speakAsCat";
ALTER TABLE "user_profile" DROP COLUMN "ff_preventAiLearning";
ALTER TABLE "meta" DROP COLUMN "ff_silencedHosts";

如果没有报错,你就可以接着输入

COMMIT;

提交这些变更。

现在,Sharkey的迁移已经完成了。你可以继续 Sharkey 的启动,比如配置 Systemd 项目。如果你之前配了 S3, 请去管理面板查看一下这些设置项,因为可能会迁移出类似这样的 URL: https://https://yourdomain.com, 你可以修复它。

附录:权限错误与解决方法

通常发生于备份文件的用户名、数据库名、权限和你的数据库内的设置不一致的时候。

以下SQL全部需要 psql firefish (或对应的数据库名)后使用。需要将 your_user 替换成你的用户名。

error: 对表 xxx 权限不够

直接把 firefish 数据库内的所有表格权限授予给你的用户:

GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO your_user;

然后将所有表格的owner设置成你的用户

DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_schema NOT IN ('pg_catalog', 'information_schema') AND table_type = 'BASE TABLE'
LOOP
EXECUTE 'ALTER TABLE ' || r.table_schema || '.' || r.table_name || ' OWNER TO your_user';
END LOOP;
END;
$$;

必须是类型 notification_type_enum 的属主

同样,将数据库中所有自定义类型的所有者更改为你的用户

DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT n.nspname AS schema_name, t.typname AS type_name
FROM pg_type t
JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE t.typowner <> (SELECT oid FROM pg_roles WHERE rolname = 'firefish')
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
AND t.typtype = 'e' -- 仅针对枚举类型(自定义类型)
LOOP
EXECUTE 'ALTER TYPE ' || r.schema_name || '.' || r.type_name || ' OWNER TO your_user';
END LOOP;
END;
$$;

附录:作者修订

用户数和帖子数一直显示 0

迁移后,有概率出现用户数和帖子数一直显示 0 的问题。这时可以进入后台作业队列,如果发现像这样的错误:

可以去 psql 进入数据库。尝试输入:

\d __chart__federation

它理应返回

                                              数据表 "public.__chart__federation"
栏位 | 类型 | 校对规则 | 可空的 | 预设
----------------------------------+---------------------+----------+----------+-------------------------------------------------
id | integer | | not null | nextval('__chart__federation_id_seq'::regclass)
date | integer | | not null |
unique_temp___deliveredInstances | character varying[] | | not null | '{}'::character varying[]
___deliveredInstances | smallint | | not null | '0'::smallint
unique_temp___inboxInstances | character varying[] | | not null | '{}'::character varying[]
___inboxInstances | smallint | | not null | '0'::smallint
unique_temp___stalled | character varying[] | | not null | '{}'::character varying[]
___stalled | smallint | | not null | '0'::smallint
___sub | smallint | | not null | '0'::smallint
___pub | smallint | | not null | '0'::smallint
___pubsub | smallint | | not null | '0'::smallint
___subActive | smallint | | not null | '0'::smallint
___pubActive | smallint | | not null | '0'::smallint
索引:
"PK_b39dcd31a0fe1a7757e348e85fd" PRIMARY KEY, btree (id)
"IDX_36cb699c49580d4e6c2e6159f9" UNIQUE, btree (date)
"UQ_36cb699c49580d4e6c2e6159f97" UNIQUE CONSTRAINT, btree (date)

注意预设中的 nextval('__chart__federation_id_seq'::regclass)

你很可能在迁移中丢失了该预设。因此,你可以在 Sharkey 已经可以运行后尝试执行:

BEGIN;
ALTER TABLE public.__chart__active_users ALTER COLUMN id SET DEFAULT nextval('__chart__active_users_id_seq'::regclass);
ALTER TABLE public.__chart__ap_request ALTER COLUMN id SET DEFAULT nextval('__chart__ap_request_id_seq'::regclass);
ALTER TABLE public.__chart__drive ALTER COLUMN id SET DEFAULT nextval('__chart__drive_id_seq'::regclass);
ALTER TABLE public.__chart__federation ALTER COLUMN id SET DEFAULT nextval('__chart__federation_id_seq'::regclass);
ALTER TABLE public.__chart__hashtag ALTER COLUMN id SET DEFAULT nextval('__chart__hashtag_id_seq'::regclass);
ALTER TABLE public.__chart__instance ALTER COLUMN id SET DEFAULT nextval('__chart__instance_id_seq'::regclass);
ALTER TABLE public.__chart__network ALTER COLUMN id SET DEFAULT nextval('__chart__network_id_seq'::regclass);
ALTER TABLE public.__chart__notes ALTER COLUMN id SET DEFAULT nextval('__chart__notes_id_seq'::regclass);
ALTER TABLE public.__chart__per_user_drive ALTER COLUMN id SET DEFAULT nextval('__chart__per_user_drive_id_seq'::regclass);
ALTER TABLE public.__chart__per_user_following ALTER COLUMN id SET DEFAULT nextval('__chart__per_user_following_id_seq'::regclass);
ALTER TABLE public.__chart__per_user_notes ALTER COLUMN id SET DEFAULT nextval('__chart__per_user_notes_id_seq'::regclass);
ALTER TABLE public.__chart__per_user_pv ALTER COLUMN id SET DEFAULT nextval('__chart__per_user_pv_id_seq'::regclass);
ALTER TABLE public.__chart__per_user_reaction ALTER COLUMN id SET DEFAULT nextval('__chart__per_user_reaction_id_seq'::regclass);
ALTER TABLE public.__chart__test ALTER COLUMN id SET DEFAULT nextval('__chart__test_id_seq'::regclass);
ALTER TABLE public.__chart__test_grouped ALTER COLUMN id SET DEFAULT nextval('__chart__test_grouped_id_seq'::regclass);
ALTER TABLE public.__chart__test_unique ALTER COLUMN id SET DEFAULT nextval('__chart__test_unique_id_seq'::regclass);
ALTER TABLE public.__chart__users ALTER COLUMN id SET DEFAULT nextval('__chart__users_id_seq'::regclass);
ALTER TABLE public.__chart_day__active_users ALTER COLUMN id SET DEFAULT nextval('__chart_day__active_users_id_seq'::regclass);
ALTER TABLE public.__chart_day__ap_request ALTER COLUMN id SET DEFAULT nextval('__chart_day__ap_request_id_seq'::regclass);
ALTER TABLE public.__chart_day__drive ALTER COLUMN id SET DEFAULT nextval('__chart_day__drive_id_seq'::regclass);
ALTER TABLE public.__chart_day__federation ALTER COLUMN id SET DEFAULT nextval('__chart_day__federation_id_seq'::regclass);
ALTER TABLE public.__chart_day__hashtag ALTER COLUMN id SET DEFAULT nextval('__chart_day__hashtag_id_seq'::regclass);
ALTER TABLE public.__chart_day__instance ALTER COLUMN id SET DEFAULT nextval('__chart_day__instance_id_seq'::regclass);
ALTER TABLE public.__chart_day__network ALTER COLUMN id SET DEFAULT nextval('__chart_day__network_id_seq'::regclass);
ALTER TABLE public.__chart_day__notes ALTER COLUMN id SET DEFAULT nextval('__chart_day__notes_id_seq'::regclass);
ALTER TABLE public.__chart_day__per_user_drive ALTER COLUMN id SET DEFAULT nextval('__chart_day__per_user_drive_id_seq'::regclass);
ALTER TABLE public.__chart_day__per_user_following ALTER COLUMN id SET DEFAULT nextval('__chart_day__per_user_following_id_seq'::regclass);
ALTER TABLE public.__chart_day__per_user_notes ALTER COLUMN id SET DEFAULT nextval('__chart_day__per_user_notes_id_seq'::regclass);
ALTER TABLE public.__chart_day__per_user_pv ALTER COLUMN id SET DEFAULT nextval('__chart_day__per_user_pv_id_seq'::regclass);
ALTER TABLE public.__chart_day__per_user_reaction ALTER COLUMN id SET DEFAULT nextval('__chart_day__per_user_reaction_id_seq'::regclass);
ALTER TABLE public.__chart_day__users ALTER COLUMN id SET DEFAULT nextval('__chart_day__users_id_seq'::regclass);

为所有的表格重新设置预设。
如果没有错误提示,则可以 COMMIT; 提交这些更改。等待一天,帖子数和用户数就会重新显示了。