奈芙莲 • 糯可

终于有一天轮到我了吗?

前文略,总而言之这篇笔记将告诉你如何迁移你的 Firefish 到 Sharkey.

准备工作

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

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

本教程基于 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 版本到最新版本。执行 pnpm run migrate 后在 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;
$$;

必须是类型 notificationtypeenum 的属主

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

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 的问题。这时可以进入后台作业队列,如果发现像这样的错误:

null value in column "id" of relation "__chart_xxxxxx" violates not-null constraint

可以去 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; 提交这些更改。等待一天,帖子数和用户数就会重新显示了。

升级到了 Writefreely v0.15.1

虽然不知道有些什么区别()

中继站经常会传入大量的,其实用户根本就不会去看一眼的帖子

如果我们认为这样的帖子是对用户而言没什么用的:

  • 不是被本站用户,或者本站用户关注或者被关注的人发的
  • 且不是被上述用户直接或者间接回复(在同一个 thread),也没有被转发或者转发
  • 没有被本站用户或者远程用户收藏或点赞或者投票(等任何方式“交互”)
  • 已经在数据库中呆了15天没有改过了

那么极大概率这些帖子完全不会被用到,理论上说可以从数据库中安全删掉。

写了个脚本找到这些帖子。( should_keep_notes_ids_uniq 则是相反,必须要保留的帖子)

WITH 
should_keep_users AS (
  SELECT id FROM "user" WHERE host is NULL
  UNION
  SELECT "followeeId" as id FROM "following"
  UNION
  SELECT "followerId" as id FROM "following"
),
should_keep_notes_data AS (
  SELECT n.id as nid, n."threadId" as ntid, n."renoteId" as nrid
  FROM should_keep_users su
  JOIN "note" n
  ON n."userId" = su.id
),
should_keep_notes_ids_not_uniq AS (
  SELECT nid as id FROM should_keep_notes_data
  UNION ALL
  SELECT n.id as id
  FROM "note" n
  WHERE n."threadId" IN ( SELECT ntid as id FROM should_keep_notes_data WHERE ntid IS NOT NULL )
  UNION ALL
  SELECT n.id as id
  FROM "note" n
  WHERE n."id" IN ( SELECT nrid as id FROM should_keep_notes_data WHERE nrid IS NOT NULL )
  UNION ALL 
  SELECT n.id as id
  FROM "note" n
  WHERE n."renoteId" IN ( SELECT nid as id FROM should_keep_notes_data ) 
  UNION ALL 
  SELECT n.id as id 
  FROM "note" n
  WHERE n."renoteId" IN ( SELECT nrid as id FROM should_keep_notes_data WHERE nrid IS NOT NULL )

  UNION ALL
  (select "noteId" as id from note_reaction) UNION ALL
  (select "noteId" as id from user_note_pining) UNION ALL
  (select "noteId" as id from note_unread) UNION ALL
  (select "noteId" as id from note_watching) UNION ALL
  (select "noteId" as id from note_favorite) UNION ALL
  (select "noteId" as id from poll_vote) UNION ALL
  (select "noteId" as id from poll)
),
should_keep_notes_ids_uniq AS (
  SELECT DISTINCT id from should_keep_notes_ids_not_uniq
),
should_delete_notes_ids AS (
  (
    SELECT id
    FROM "note" n
    WHERE n."userHost" IS NOT NULL
    AND "updatedAt" < (current_timestamp - interval '15 day')
  )
  EXCEPT
  (
    SELECT id FROM should_keep_notes_ids_uniq
  )
)
SELECT count(*) from should_delete_notes_ids;

Facebook在性别选择框里放了97个选项以后就一直有人对此津津乐道。以前我一直懒得看相关的东西,毕竟大家都知道这只是跨性别恐惧症(transphobia)的狗哨而已。只要稍微看其中的内容,就能意识到这个硬凑的97种性别有多恶意。

Facebook通过搜集小众语言和小众文化里的同义词,强行凑出97种“性别”,让不懂的人以为这是跨性别者的创造。今日闲来无事,我也来构造2936种性别,谈谈Facebook是如何利用同义词放大人们的跨性别恐惧症的。

让我们从最基础的开始。几乎所有人都会承认世界上至少有三种性别:女性,男性,和同时不属于二者的广义的非二元性别。如果连非二元性别都不承认,那我只能说你是一个无可救药的二极管,已经可以不看了。好的,让我们列出这三种。

  • female
  • male
  • non-binary

然后,如果我们要细分的话,女性又分为顺性别女性(Cisgender)和跨性别女性(Transgender)。非二元也有不同的种类,比如同时具有男女两种认同的人(Bigender),觉得自己既不是男性也不是女性的人(Agender),不认为男性但不完全认为自己是女性的半女(demigirl),和不认为自己是女性但不完全认为自己是男性的半男(demiboy)。

所以新的列表大概变成了这样:

  • female
  • male
  • non-binary
  • cisgender female
  • transgender female
  • cisgender male
  • transgender male
  • agender
  • bigender
  • demigirl
  • demiboy

等一下,上面的女性对于顺性别女性和跨性别女性不是包含关系吗?为什么它们会出现在一起?这样不是重复了吗?

这你就要问Facebook了。我也不知道它们是不是故意的,总而言之它们把大范围的集合和小集合放在了一起同时作为不同的“性别”。比如让我们看看Facebook光女性就划分了多少种:

  1. Cis-gender woman 顺性别女,所谓顺性别,指的是性别认同与出生时指派性别相同的人。即出生时指派性别为女,自我认知为女。
  2. Woman 广义上的女人。
  3. Transgender Woman 跨性别女。跨性别指的是性别认同或性别表现与他们出生时指派性别不同的人。跨性别女即出生时指派性别为男,性别认同为女。
  4. Woman of Trans experience 有跨性别经验的女性。
  5. Woman with a history of gender transition 有性别转换史的女性。
  6. Transfeminine 女性倾向跨性别者。字面意思,偏女的跨性别者
  7. Feminine-of-center 中心女性化,用于描述一个人认为或表现出,相比于男性更多是女性化的人。
  8. MTF male-to-female的缩写。跨性别女性的别名
  9. Transgirl 年轻的跨性别女性。
  10. T-girl 9的缩写
  11. Sistergirl 还是跨性别女性,但是是来自太平洋土著的俚语。

在这里之前不懂的读者大概也意识到不对劲了。上述连续分了11个性别,实际上翻来覆去讲的只有两种:顺性别女性,或者跨性别女性。剩下的全都是对跨性别女性的别称、子集或者缩写。

如果你选了跨性别女孩,实际上你同时还是 T-girl(跨性别女孩的缩写),实际上你同时还是跨性别女性因为显然女孩就是年轻女性,实际上你还是Sistergirl因为这是土著的别称,实际上你还是transfeminine因为这又是一个别称,当然你还是Woman of Trans experience毕竟你有跨性别经验,最后你还是个Woman。这下你同时拥有7个性别了(笑)

Facebook将同义词重复地放在这个列表中实际上就是一种无耻的行径。说真的,谁会在乎T-girl还是Transgirl啊?是字母太多了写不下吗?Facebook此举实际上意味非常明显,就是“你看跨性别多荒谬,搞出这么多个性别”——那你怎么不说中国古代人荒谬啊?中国古代的自称有多少让我们看看:

吾、余、在下、敝人、鄙人、不肖、不才、老子、爷、某、依、仆、乃公、人家、我等、我辈、我依、吾、吾们、吾济、吾辈、吾曹、小辈、小生、小人、小子、小可、小的、余、予、在下、洒家、咱、朕、寡人、孤、我

《震惊!中国人竟然有37种自我!这究竟是人性的扭曲还是道德的沦丧!》

放狗屁吧这些全都是一个意思,有的是谦辞有的是不礼貌的词有的是普通词而已。

按Facebook的玩法,我们的性别表单一下子就指数级增长了:你瞧,female可以按年龄分为woman和girl,transgender可以缩写为trans和T-,cisgender当然也能缩写成cis。跨性别还能细分为有跨性别经历的,有激素治疗的(HRT),有手术的(SRS),顺性别还能分成曾经以为自己是跨性别但是后来退坑的(detrans)和坚定的顺性别(firmly),瞬间表单暴涨好多倍嘛!

["female","girl","woman","male","boy","man","non-binary","cisgender female","cisgender female firmly","cisgender female detrans","cis female","cis female firmly","cis female detrans","cisgender girl","cisgender girl firmly","cisgender girl detrans","cis girl","cis girl firmly","cis girl detrans","cisgender woman","cisgender woman firmly","cisgender woman detrans","cis woman","cis woman firmly","cis woman detrans","transgender female","trans female","T-female","transgender female with trans experience","trans female with trans experience","T-female with trans experience","transgender female with HRT","trans female with HRT","T-female with HRT","transgender female with SRS","trans female with SRS","T-female with SRS","transgender girl","trans girl","T-girl","transgender girl with trans experience","trans girl with trans experience","T-girl with trans experience","transgender girl with HRT","trans girl with HRT","T-girl with HRT","transgender girl with SRS","trans girl with SRS","T-girl with SRS","transgender woman","trans woman","T-woman","transgender woman with trans experience","trans woman with trans experience","T-woman with trans experience","transgender woman with HRT","trans woman with HRT","T-woman with HRT","transgender woman with SRS","trans woman with SRS","T-woman with SRS","cisgender male","cisgender male firmly","cisgender male detrans","cis male","cis male firmly","cis male detrans","cisgender boy","cisgender boy firmly","cisgender boy detrans","cis boy","cis boy firmly","cis boy detrans","cisgender man","cisgender man firmly","cisgender man detrans","cis man","cis man firmly","cis man detrans","transgender male","trans male","T-male","transgender male with trans experience","trans male with trans experience","T-male with trans experience","transgender male with HRT","trans male with HRT","T-male with HRT","transgender male with SRS","trans male with SRS","T-male with SRS","transgender boy","trans boy","T-boy","transgender boy with trans experience","trans boy with trans experience","T-boy with trans experience","transgender boy with HRT","trans boy with HRT","T-boy with HRT","transgender boy with SRS","trans boy with SRS","T-boy with SRS","transgender man","trans man","T-man","transgender man with trans experience","trans man with trans experience","T-man with trans experience","transgender man with HRT","trans man with HRT","T-man with HRT","transgender man with SRS","trans man with SRS","T-man with SRS","agender","bigender","demigirl","demiboy"]

这不是,119种性别一下子就到手了。

我们还有AFAB(A female at birth出生时指派为女的人)和AMAB(出生时指派为男的人)。当然,跨性别我们还能合二为一统称为 transgender 或者 trans 或者 transexual。这可不是我在玩抽象,是Facebook自己添加进它们的性别列表的,看:

  1. Trans 跨性别。Transgender man 和 transgender woman的统称。
  2. Transgender 还是跨性别
  3. Transsexual 还是跨性别
  4. Genderqueer 性别酷儿,意思差不多就是跨性别总称

我还是很难像facebook这样无耻,还把女同性恋也写成一种性别,性取向和性别没什么关系,所以Femme这类的词我就不加进去了。

所以我决定玩点抽象的:这不是还有染色体吗,实际上Y染色体上的SRY基因才是决定性别分化的开关,丢失了SRY基因的Y染色体没法让人发育成男性,被插入了SRY基因的X染色体反而能让人发育成男性。这不是我说胡话,这两种分别叫Swyer综合征和XX男性综合征,都是大约两万份之一的概率出现,因此全球加起来80多万人呢。同时,还有俗称超雄和超雌综合征的XYY和XXX,还有克氏综合征的XXY。以上这些统称性别分化障碍(DSD),那没有这种障碍的当然也要有自己单独的性别啦(non-DSD)。让我们把这些染色体都乘进去。

现在我们已经有了1112种性别,虽然这份列表翻来覆去讲的全都是一样的东西,不妨碍我取个超级震撼性大新闻 《震惊!有人声称世界上有1112种性别!》了。

让我们再看看facebook干了什么:

70.Palao’ana 马里亚纳群岛的查莫罗文化中指第三性别。 71.Ashtime 埃塞俄比亚语中用来指代第三性别。 72.Mashoga 斯瓦希里语的第三性别。 73.Mangaiko 刚果语用来指代第三性别。 74.Chibados 恩东戈王国用来指代第三性别。

我操,它把同一个词语在不同语言中的写法也扔进去了。欺负老实人呢?

好吧,那我也试试。我们加上一些小小的世界语和拉丁语,再翻两倍!

现在我们有了2936种性别。

最后

所以我想表达什么呢?

我想表达的很简单,通过随意的排列组合和一些不要脸的行径,就能轻易构造出简中互联网上最大的跨性别恐惧症狗哨之一——97种性别。不仅如此,我还顺手给它翻了将近30倍,把它变成了2936种。

跨性别恐惧症极力通过像这样的行为污名化跨性别者。表面上“尊重”了性少数,实际上煽动了互联网的仇跨情绪。就是这么简单。

附录

2936种性别的清单

译者注:

本文是 Michał “rysiek” Woźniak 的博客 Mastodon monoculture problem 的翻译。 source: https://rys.io/en/168.html

对于不太懂文中的名词的人,译者额外做出一些解释:

  • 联邦宇宙:Fediverse,是使用一些协议彼此链接的一系列网站/服务(被称为“实例”)的总称。在联邦宇宙,你在任何一个实例上拥有账号,你就能访问几乎整个联邦宇宙的所有内容——实例有特殊规则的时候除外。
  • 实例:联邦宇宙上的单独一个网站/服务。
  • 实例软件:运行实例使用的软件,比如 Mastodon。用一种实例软件可以搭建很多个实例。
  • ActivityPub协议:一种旨在让不同的实例之间可以互相连接,共通账号的协议。联邦宇宙实例很多使用的就是ActivityPub协议。
  • Mastodon:连接到联邦宇宙的一个微博类社交媒体软件,有类似Twitter的设计。

依照 Michał “rysiek” Woźniak 的原文 (https://rys.io/en/168.html),本文以 CC-BY-SA 4.0 协议发布。您可以自由地共享、演绎本作品,但是必须署名、以相同方式共享 – 如果您再混合、转换或者基于本作品进行创作,您必须基于与原先许可协议相同的许可协议分发您贡献的作品。


Mastodon 非营利组织的首席执行官兼 Mastodon 软件的首席开发者 Eugen Rochko(在联邦宇宙上被称为 Gargron)最近的举动让一些人@[email protected]/110300834063046929">担心 Mastodon 该软件项目,同时也是该非盈利组织对联邦宇宙的其余部分造成的巨大影响

确实。我们就是应该担心。

到目前为止,联邦宇宙上大多数人都在使用 Mastodon 软件。 截至撰写本文时,最大的实例 mastodon.social 拥有超过 200,000 个活跃帐户。 这意味着这单独一个实例上涵括了整个联邦宇宙的大约 1/10。 更糟糕的是,Mastodon 软件经常被认为是整个社交网络,这掩盖了一个真相: Fediverse 是一个由更多样化的软件们组成的更广泛的系统

现在它就已经产生了糟糕的后果,而且以后可能会更糟。 让我困扰的还有,我以前也见过这样的情况:

正如 OStatus 宇宙所示

几年前,我在联邦宇宙的前身有一个账户。 它主要基于 StatusNet 软件(后来更名为 GNU Social)和 OStatus协议。 最大的实例是 identi.ca ——笔者在那里拥有自己的账户。 同时还有很多其他实例、其他软件项目也实现了 OStatus协议——例如 Friendica

出于本博文的需要,我们将该社交网络称为“OStatus 宇宙”。

与今天的联邦宇宙相比,OStatus 宇宙是微不足道的。 我没有具体的数字,但我的粗略估计是,即使在最活跃的时候,也只有大约100,000到200,000个活跃帐户(如果你有实际数字,请告诉我,我将很乐意更新这篇博客)。 我也没有 identi.ca 上用户数目的确切数字,但我粗略估计它有 10,000 到 20,000 个活跃帐户。

对,刚好也是整个社交网络的 1/10。

OStatus 宇宙虽小但很活跃。我们在上面有讨论、线程回复(threads)和话题标签(hashtags)。 它早在 Mastodon 软件项目实现 Group 的十年前就实现了群组。它有桌面应用程序——我仍然怀念 Choqok 的可用性! 甚至经过一番唠叨后,我还说服了波兰的一个政府部门在那里设立官方办事处。 据我所知,这是最早的政府机构在自由软件驱动的去中心化社交网络上拥有官方账户的例子。

Identipocalypse (identi的末日)

然后有一天,identi.ca 的管理员(也是 StatusNet 软件的原始创建者)Evan Prodromou 决定将其重构为一项新服务,即 pump.io。 他们希望新软件更好、更精简。 他们创建了一个新协议,因为 OStatus 协议有非常现实的限制。

只有一个问题:新协议与 OStatus 宇宙的其它部分不兼容。它把这个社交网络撕碎了。

拥有 identi.ca 帐户的用户与所有其他 OStatus 实例失去了连接。 在其它实例上拥有帐户的人与 identi.ca 上的人失去了联系,并且其中一些人在 OStatus 宇宙中非常受欢迎(听起来很熟悉?..)。

事实证明,如果一个实例占据了整个社交网络的 1/10,那么它就会承载太多的社交联系。尽管的确存在其他实例,但突然间大量活跃用户消失了。一些群组瞬间就变得安安静静。 即使有人在不同的实例上有一个帐户,并且在其他实例上有联系人,很多熟悉的面孔也会消失。 于是此后不久我就停止使用它了。

从笔者的角度来看,就这么一项行为,就使得我们在推广去中心化社交媒体方面至少倒退了五年甚至十年。 identi.ca 的重构不仅在社交关系意义上破坏了 OStatus 宇宙,而且在协议和开发者社区意义上也是。 正如 Pettter,一位 OStatus 资深人士所说:

我认为,这个巨变带来的影响是,它不仅切断了社交联系,还导致了协议变得支离破碎,一次又一次地让重建联合社交网络的基本架构的努力付诸东流。 也许这是他们重新聚集在一起设计 ActivityPub 的必要步骤,但我个人不这么认为。

当然,Evan 完全有权利这样做,毕竟这是他用自己的钱、按照自己的条款无偿经营的一项服务。 但这并不能改变它割裂了OStatus宇宙的事实。

我认为我们需要从这段历史中吸取教训。我们应该担心 mastodon.social 的庞大规模。 我们应该为联邦宇宙上 Mastodon 软件明显的单一文化感到担忧。 我们还应该担心将整个联邦宇宙与“Mastodon”等同起来。

做大的代价

发展到像 mastodon.social 这样的规模,需要付出相当的成本和风险。 这些成本,尤其是那些风险,既针对该实例本身,也针对更广泛的 Fediverse。

Fediverse 上的审核很大程度上以实例为中心。 单个巨大的实例很难有效地管理,特别是如果它开放注册(就像 mastodon.social 目前所做的那样)。 作为直接在官方移动应用程序中推广的旗舰实例,它吸引了大量新注册,其中包括不少不太友好的用户

同时,这也使得其他实例的管理员和版主更难以做出有关 mastodon.social 的审核决定。

如果其它实例的管理员认为 mastodon.social 出于某种原因缺乏管理,他们是否应该静音该实例,甚至将其屏蔽(显然,有些人已经这样做了),代价是让本站的用户无法联络许多在 mastodon.social 有账户的受欢迎的人?或者、他们就应该冒着让自己的社区面临潜在有害行为的风险,保留这种联络的可能性吗?

mastodon.social 的庞大规模使得任何其他实例的管理决策成为了一件大事。 这是某种形式上的特权:“当然,如果您不喜欢我们的管理方式,您可以封掉我们,但如果您实例上的用户无法访问整个联邦宇宙的 1/10,那将是一种耻辱!” 正如 GoToSocial 网站所说

我们也不认为拥有成千上万用户的旗舰实例对联邦宇宙来说很有好处,因为它们会导致中心化,并且很容易变得“太大而不敢屏蔽”。

请注意,我并不是说这种权力动态是有意识地、有目的地利用的! 但不可否认,它是存在的。

作为一个巨大的旗舰实例也意味着 mastodon.social 更有可能成为恶意行为的目标。 例如,在过去几个月中,它多次受到 DDoS 攻击,并且好几次都因为这个而无法访问。 联邦系统的可靠性依赖于消除大的故障点,而 mastodon.social 现在已经是一个巨大的故障点了。

该实例的规模让它成为了一个诱人的攻击目标,这也意味着它需要做出某些艰难的选择。 例如,由于经常成为 DDoS 的目标,它现在由 Fastly 保护。 从隐私角度和互联网基础设施中心化的角度,这是一个问题。 这也是较小的实例完全避免的一个问题,因为它们很小,很少有人会无聊到 DDoS 攻击它们。

(译者注:这方面译者非常存疑,事实上小实例也经常受到随机的DDoS攻击。)

明显的单一文化

虽然联邦宇宙并不完全是单一文化,但它太接近单一文化了,令人感到不舒服。 Mastodon 非营利组织对整个联邦机构有着巨大的影响力。 这让使用社交网络的人、Mastodon 软件和其他实例软件项目的开发人员以及实例管理员感到紧张。

Mastodon 既不是联邦宇宙上唯一的平台软件,也不是第一个。 例如,Friendica 已经存在了十五年了,早在 Mastodon 软件的第一次 git 提交之前就已经存在。一些现在的联邦宇宙中运行着的 Friendica 实例(例如 pirati.ca)在十年前曾是 OStatus 宇宙的一部分!

但是很多人在将整个联邦宇宙称为“Mastodon”,说的跟 Fediverse 上只存在 Mastodon 软件一样。 这导致人们经常要求 Mastodon 实现一些新功能,但其实这些功能已经由其他实例软件实现了。 Calckey 已经有引用嘟文(带评转发)功能了。 Friendica 也早就有线程对话和富文本

将 Mastodon 与整个联邦宇宙等同起来对于 Mastodon 软件开发人员来说也是不利的。 这导致他们面临着被要求实现不完全适合 Mastodon 软件的功能的压力。 或者,有时候他们不得不两群吵吵囔囔的用户打交道,一群人想要某个功能,另一群人又觉得这个功能太大,实现不了。通过清楚地划出一条界限,并引导人们使用可能更适合他们的用例的其他实例软件,许多此类情况可能会更容易处理。

最后,Mastodon 是目前为止(按活跃用户和实例数量衡量)最流行的 ActivityPub 协议实现。 每个实现都有其自己的特性。 随着时间的推移和新功能的实现,Mastodon 的实现可能会进一步偏离严格的规范。 毕竟,这很诱人:如果你怎么弄都是龙头老大,为什么要艰难的去实现标准协议呢?

如果这真的发生了,其他所有实现是否都必须跟随它,从而变得随波逐流,没有对于事实标准的话语权? 这是否会在 Mastodon 软件开发人员和其他实例软件项目的开发人员之间造成更多紧张关系?

“Mastodon 错过了 XX 功能”的最优解并不总是“Mastodon 应该实现 XX 功能”。 通常来说,最好使用更适合特定任务或社区的不同实例软件。或者开发一个扩展协议,允许尽可能多的实例可靠地实现特别流行的功能。

但这只有在每个人都清楚 Mastodon 只是更大的社交网络:联邦宇宙 的一部分的情况下才有效。 而且,对实例软件、单个实例以及移动应用程序而言,我们现在本来就有很多选择。

遗憾的是,这似乎与 Eugen 最近的决定背道而驰:它们打算导向自上而下(不是完全垂直整合,但倾向于垂直整合)的官方 Mastodon 移动应用程序模型,以推广他们最大的 mastodon.social 实例。 在我看来,这很值得担忧。

更好的方式

我想澄清的是,我在这里并不是主张停止 Mastodon 的开发并且从不实现任何新功能。 我也同意注册流程需要比以前更好、更简化,我也同意需要实施大量 UI/UX 更改。 但所有这一切都可以并且理应以提高联邦宇宙弹性的方式进行,而不是破坏它。

必要的更改

我觉得 Mastodon 和联邦宇宙必须要更改的地方是:

  1. 现在关闭 mastodon.social 上的注册 对于联邦宇宙的其他部分来说,这个实例已经太大了,所带来的风险也太大了。
  2. 使用户迁移更加容易,甚至可以跨实例迁移 在 Mastodon 上,个人资料迁移目前仅移动关注者。 您关注的人、收藏夹、屏蔽和隐藏列表都可以手动移动。 帖子和列表无法移动——这对很多人来说都是一个很大的问题,因此他们就被和他们注册的第一个实例绑在了一起。 这并不是无法克服的——笔者已经迁移了两次个人账户,感觉也很不错。但这种阻力还是太大了。 值得庆幸的是,其他一些实例软件项目也正在努力允许帖子迁移。 但这不会是一个快速而简单的解决方案,因为 ActivityPub 的设计使得在实例之间移动帖子变得非常困难。(译注:Sharkey和Firefish实现了这项功能)
  3. 默认情况下,官方应用程序应该随机从一些可信的实例中选一个推荐给新用户注册 至少其中一些实例不应由 Mastodon 非营利组织控制。 理想情况下,某些实例应该运行不同的实例软件,只要它使用兼容的客户端 API。

我能做什么?

作为联邦宇宙的一员,我们可以做以下事情:

  1. 如果您在 mastodon.social 上有帐户,请考虑迁移走 的确这是有点艰难的一大步,但也是你可以做的最直接有助于解决问题的事情。 几年前,我从 mastodon.social 迁移过来,再也没回去过。
  2. 考虑使用基于不同软件项目的实例 越多的人迁移到使用 Mastodon 软件以外的其他实例软件的实例,我们的联邦宇宙就越平衡越有弹性。 例如,我听说很多人都觉得 Calckey 不错。 GoToSocial 看起来也很有趣。
  3. 请记住,联邦宇宙不仅仅是 Mastodon 语言很重要。 当谈论联邦宇宙时,称其为“Mastodon”只会让我上面提到的问题更难处理。
  4. 如果可以的话,支持 Mastodon 官方项目以外的项目 至此,Mastodon软件项目已经拥有众多的贡献者、稳定的开发团队以及足够雄厚的资金,可以安全地持续很长一段时间。这那太棒了!但是,对于其他与联邦宇宙相关的项目,包括独立的移动应用程序或实例软件,它们没有那么受到关注。为了拥有一个多元化、有弹性的联邦宇宙,我们需要确保这些项目也在各个方面得到支持,比如金钱上。

结束语

首先,联邦宇宙是一个比任何中心化的孤岛更有弹性、更长期可行、更安全、更民主化的社交网络。 即使存在 Mastodon 单一文化问题,它仍然不是(并且不可能 )由任何单一公司或个人拥有或控制。 我也觉得它比只是在 cosplay 去中心化的社交网络,比如 BlueSky 是一个更好、更安全的选择。

从某种意义上来说,OStatus宇宙可以说是联邦宇宙的早期版本; 如前所述,当时属于其中的一些实例仍在运行,并且今天已成为联邦宇宙的一部分。 换句话说,联邦宇宙已经存在了十五年了,尽管它受到了严重的伤害,但它仍然在 identi.ca 的灾难中“幸存下来”,同时见证了 Google+ 的诞生和过早的消逝

我确实相信如今的联邦宇宙比 identi.ca 重新部署之前的OStatus宇宙更具弹性。 就用户群而言,联邦宇宙至少要多十倍,有数十个不同的实例软件项目和数以万计的活跃实例。还有一些认真的机构对其未来进行了投资。 我们不应该对我上面写的一切感到恐慌。 但我确实认为我们应该防患于未来。

我不会将恶意归因于 Eugen 最近的行为(比如让官方 Mastodon 应用程序将新人引向 mastodon.social),也不会归因于 Evan 过去的行为(在 pump.io 上重新部署 identi.ca )。 我认为任何人都不应该这样做。 做好这件事很难,我们都在边走边学,并努力利用有限的时间和有限的资源去做到最好。

Evan 后来成为 ActivityPub(联邦宇宙运行的协议)的主要创建者之一。 Eugen 发起了 Mastodon 软件项目,我坚信这个项目让联邦宇宙蓬勃发展到了今天的样子。 我真的很欣赏他们的工作,并认识到如果没有人发表意见,在社交媒体空间中做任何事情都是不可能的。

然而,这并不意味着我们不能仔细思考这些决定,也不应该对此有这些意见。


更新:我(原作者)犯了一个傻傻的错误,mastodon.social 由 Fastly 提供保护,我以为是 CloudFlare。修复了,感谢指出这个错误的人!

更新2:衷心感谢 Jorge Maldonado Ventura 提供了这篇博文的西班牙语翻译,并在 CC BY-SA 4.0 下发布。 谢谢!

Hello, world

我们星屑也有了自己的博客平台(