diff --git a/README.md b/README.md index 836ce3387..644f25140 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ GoToSocial is an [ActivityPub](https://activitypub.rocks/) social network server With GoToSocial, you can keep in touch with your friends, post, read, and share images and articles. All without being tracked or advertised to!

- +

**GoToSocial is still [BETA SOFTWARE](https://en.wikipedia.org/wiki/Software_release_life_cycle#Beta)**. It is already deployable and useable, and it federates cleanly with many other Fediverse servers (not yet all). However, many things are not yet implemented, and there are plenty of bugs! We left alpha stage around September/October 2024, and we intend to exit beta some time around 2026. @@ -19,7 +19,7 @@ To build from source, check the [CONTRIBUTING.md](https://github.com/superseriou Here's a screenshot of the instance landing page! -![Screenshot of the landing page for the GoToSocial instance goblin.technology. It shows basic information about the instance; number of users and posts etc.](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/instancesplash.png) +![Screenshot of the landing page for the GoToSocial instance goblin.technology. It shows basic information about the instance; number of users and posts etc.](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/overrides/public/instancesplash.png) ## Table of Contents @@ -72,7 +72,7 @@ GoToSocial provides a lightweight, customizable, and safety-focused entryway int If you've ever used something like Twitter or Tumblr (or even Myspace!) GoToSocial will probably feel familiar to you: You can follow people and have followers, you make posts which people can favourite and reply to and share, and you scroll through posts from people you follow using a timeline. You can write long posts or short posts, or just post images, it's up to you. You can also, of course, block people or otherwise limit interactions that you don't want by posting just to your friends. -![Screenshot of the web view of a profile in GoToSocial, showing header and avatar, bio, and numbers of followers/following.](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/profile1.png) +![Screenshot of the web view of a profile in GoToSocial, showing header and avatar, bio, and numbers of followers/following.](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/overrides/public/profile1.png) **GoToSocial does NOT use recommendation algorithms or collect data about you to suggest content or 'improve your experience'**. The timeline is chronological: whatever you see at the top of your timeline is there because it's *just been posted*, not because it's been selected as interesting (or controversial) based on your personal profile. @@ -84,7 +84,7 @@ GoToSocial doesn't claim to be *better* than any other application, but it offer Because GoToSocial uses [ActivityPub](https://activitypub.rocks/), you can hang out not just with people on your home server, but with people all over the [Fediverse](https://en.wikipedia.org/wiki/Fediverse), seamlessly. -![the activitypub logo](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/ap_logo.svg) +![the activitypub logo](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/overrides/public/ap_logo.svg) Federation means that your home server is part of a network of servers all over the world that all communicate using the same protocol. Your data is no longer centralized on one company's servers, but resides on your own server and is shared — as you see fit — across a resilient web of servers run by other people. @@ -128,7 +128,7 @@ GoToSocial offers public, unlisted/unlocked, followers-only, and direct posts (s GoToSocial lets you choose who can reply to your posts, via [interaction policies](https://docs.gotosocial.org/en/latest/user_guide/settings/#default-interaction-policies). You can choose to let anyone reply to your posts, let only your friends reply, and more. -![interaction policies settings](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/user-settings-interaction-policy-1.png) +![interaction policies settings](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/overrides/public/user-settings-interaction-policy-1.png) ### Local-only posting @@ -142,7 +142,7 @@ GoToSocial lets you opt-in to exposing your profile as an RSS feed, so that peop With GoToSocial, you can write posts using the popular, easy-to-use Markdown markup language, which lets you produce rich HTML posts with support for blockquotes, syntax-highlighted code blocks, lists, inline links, and more. -![markdown-formatted post](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/markdown-post.png) +![markdown-formatted post](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/overrides/public/markdown-post.png) ### Themes and custom CSS @@ -153,61 +153,61 @@ It's also easy for admins to [add their own custom themes](https://docs.gotosoci
Show theme examples
- +
Blurple dark

- +
Blurple light

- +
Brutalist light

- +
Brutalist dark

- +
Ecks pee

- +
Midnight trip
- +
Moonlight hunt

- +
Rainforest

- +
Soft

- +
Solarized dark

- +
Solarized light

- +
Sunset

@@ -217,7 +217,7 @@ It's also easy for admins to [add their own custom themes](https://docs.gotosoci GoToSocial uses only about 250-350MiB of RAM, and requires very little CPU power, so it plays nice with single-board computers, old laptops and tiny $5/month VPSes. -![Grafana graph showing GoToSocial heap in use hovering around 250MB and spiking occasionally to 400MB-500MB.](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/getting-started-memory-graph.png) +![Grafana graph showing GoToSocial heap in use hovering around 250MB and spiking occasionally to 400MB-500MB.](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/overrides/public/getting-started-memory-graph.png) No external dependencies apart from a database (or just use SQLite!). diff --git a/docs/admin/federation_modes.md b/docs/admin/federation_modes.md index 8313d8af2..dac1e178f 100644 --- a/docs/admin/federation_modes.md +++ b/docs/admin/federation_modes.md @@ -33,7 +33,7 @@ When your instance encounters a mention or an announce of a status or account it It is possible to both block and allow the same domain, and the effect of combining these two things depends on which federation mode your instance is currently using. -![A flow chart diagram showing how the two different federation modes treat incoming requests.](../assets/diagrams/federation_modes.png) +![A flow chart diagram showing how the two different federation modes treat incoming requests.](../public/diagrams/federation_modes.png) ### In blocklist mode diff --git a/docs/admin/settings.md b/docs/admin/settings.md index 5e1a906e3..0efb5bf45 100644 --- a/docs/admin/settings.md +++ b/docs/admin/settings.md @@ -20,13 +20,13 @@ Instance moderation settings. ### Reports -![List of reports for testing, showing one open report.](../assets/admin-settings-reports.png) +![List of reports for testing, showing one open report.](../public/admin-settings-reports.png) The reports section shows a list of reports, originating from your local users, or remote instances (shown anonymously as just the name of the instance, without specific username). Clicking a report shows if it was resolved (with the reasoning if available), more information, and a list of reported toots if selected by the reporting user. You can also use this view to mark a report as resolved, and fill in a comment. Whatever comment you enter here will be visible to the user that created the report, if that user is from your instance. -![The detailed view of an open report, showing the reported status and the reason for the report.](../assets/admin-settings-report-detail.png) +![The detailed view of an open report, showing the reported status and the reason for the report.](../public/admin-settings-report-detail.png) Clicking on the username of the reported account opens that account in the 'Accounts' view, allowing you to perform moderation actions on it. @@ -36,7 +36,7 @@ You can use this section to search for an account and perform moderation actions ### Federation -![List of suspended instances, with a field to filter/add new blocks. Below is a link to the bulk import/export interface](../assets/admin-settings-federation.png) +![List of suspended instances, with a field to filter/add new blocks. Below is a link to the bulk import/export interface](../public/admin-settings-federation.png) In the federation section you can create, delete, and review explicit domain blocks and domain allows. @@ -56,7 +56,7 @@ The domain allows section works much like the domain blocks section, described a Through the link at the bottom of the Federation section (or going to `/settings/admin/federation/import-export`) you can do bulk import/export of blocklists and allowlists. -![List of domains included in an import, providing ways to select some or all of them, change their domains, and update the use of subdomains.](../assets/admin-settings-federation-import-export.png) +![List of domains included in an import, providing ways to select some or all of them, change their domains, and update the use of subdomains.](../public/admin-settings-federation-import-export.png) Upon importing a list, either through the input field or from a file, you can review the entries in the list before importing a subset. You'll also be warned for entries that use subdomains, providing an easy way to change them to the main domain. @@ -86,7 +86,7 @@ Custom Emoji will be automatically fetched when included in remote toots, but to #### Local -![Local custom emoji section, showing an overview of custom emoji sorted by category. There are a lot of garfields.](../assets/admin-settings-emoji-local.png) +![Local custom emoji section, showing an overview of custom emoji sorted by category. There are a lot of garfields.](../public/admin-settings-emoji-local.png) This section shows an overview of all the custom emoji enabled on your instance, sorted by their category. Clicking an emoji shows it's details, and provides options to change the category or image, or delete it completely. The shortcode cannot be updated here, you would have to upload it with the new shortcode yourself (and optionally delete the old one). @@ -94,7 +94,7 @@ Below the overview you can upload your own custom emoji, after previewing how th #### Remote -![Remote custom emoji section, showing a list of 3 emoji parsed from the entered toot, garfield, blobfoxbox and blobhajmlem. They can be selected, their shortcode can be tweaked, and they can be assigned to a category, before submitting as a copy or delete operation](../assets/admin-settings-emoji-remote.png) +![Remote custom emoji section, showing a list of 3 emoji parsed from the entered toot, garfield, blobfoxbox and blobhajmlem. They can be selected, their shortcode can be tweaked, and they can be assigned to a category, before submitting as a copy or delete operation](../public/admin-settings-emoji-remote.png) Through the 'remote' section, you can look up a link to any remote toots (provided the instance isn't suspended). If they use any custom emoji they will be listed, providing an easy way to copy them to the local emoji (for use in your own toots), or disable them ( hiding them from toots). @@ -102,7 +102,7 @@ Through the 'remote' section, you can look up a link to any remote toots (provid ### Instance Settings -![Screenshot of the GoToSocial admin panel, showing the fields to change an instance's settings](../assets/admin-settings-instance.png) +![Screenshot of the GoToSocial admin panel, showing the fields to change an instance's settings](../public/admin-settings-instance.png) Here you can set various metadata for your instance, like the displayed name/title, thumbnail image, (short) description, and contact info. diff --git a/docs/admin/signups.md b/docs/admin/signups.md index db2010cf7..78513380c 100644 --- a/docs/admin/signups.md +++ b/docs/admin/signups.md @@ -17,7 +17,7 @@ You can open new account sign-ups for your instance by changing the variable `ac A sign-up form for your instance will be available at the `/signup` endpoint. For example, `https://your-instance.example.org/signup`. -![Sign-up form, showing email, password, username, and reason fields.](../assets/signup-form.png) +![Sign-up form, showing email, password, username, and reason fields.](../public/signup-form.png) Also, your instance homepage and "about" pages will be updated to reflect that registrations are open. @@ -29,11 +29,11 @@ In the meantime, admins and moderators on your instance will receive an email an Instance admins and moderators can handle a new sign-up by either approving or rejecting it via the "accounts" -> "pending" section in the admin panel. -![Admin settings panel open to "accounts" -> "pending", showing one account in a list.](../assets/signup-pending.png) +![Admin settings panel open to "accounts" -> "pending", showing one account in a list.](../public/signup-pending.png) If you have no sign-ups, the list pictured above will be empty. If you have a pending account sign-up, however, you can click on it to open that account in the account details screen: -![Details of a new pending account, giving options to approve or reject the sign-up.](../assets/signup-account.png) +![Details of a new pending account, giving options to approve or reject the sign-up.](../public/signup-account.png) At the bottom, you will find actions that let you approve or reject the sign-up. diff --git a/docs/advanced/tracing.md b/docs/advanced/tracing.md index 6e8c7dbf3..c06b449c3 100644 --- a/docs/advanced/tracing.md +++ b/docs/advanced/tracing.md @@ -39,6 +39,6 @@ If you wanted to see all GoToSocial traces, you could instead run: Once you select a trace, a second panel will open up visualising the span. You can drill down from there, by clicking into every sub-span to see what it was doing. -![Grafana showing a trace for the /api/v1/instance endpoint](../assets/tracing.png) +![Grafana showing a trace for the /api/v1/instance endpoint](../public/tracing.png) [traceql]: https://grafana.com/docs/tempo/latest/traceql/ diff --git a/docs/configuration/storage.md b/docs/configuration/storage.md index 539898e11..697c2c88b 100644 --- a/docs/configuration/storage.md +++ b/docs/configuration/storage.md @@ -1,5 +1,7 @@ # Storage +When configuring an object storage backend, the `storage-s3-endpoint` **must not** include the bucket name. That's what `s3-bucket-name` is for. Using subfolders in a bucket isn't currently supported. + ## Settings ```yaml diff --git a/docs/federation/posts.md b/docs/federation/posts.md index c599587a7..b03bfe40a 100644 --- a/docs/federation/posts.md +++ b/docs/federation/posts.md @@ -914,7 +914,7 @@ Now, `remote_1` boosts/reblogs a post from a third account, `remote_2`, residing `local_account` does not follow `remote_2`, and neither does anybody else on `our.server`, which means that `our.server` has not seen this post by `remote_2` before. -![A diagram of the conversation thread, showing the post from remote_2, and possible ancestor and descendant posts](../assets/diagrams/conversation_thread.png) +![A diagram of the conversation thread, showing the post from remote_2, and possible ancestor and descendant posts](../public/diagrams/conversation_thread.png) What GoToSocial will do now, is 'dereference' the post by `remote_2` to check if it is part of a thread and, if so, whether any other parts of the thread can be obtained. diff --git a/docs/getting_started/index.md b/docs/getting_started/index.md index d8b96439b..21a7b234a 100644 --- a/docs/getting_started/index.md +++ b/docs/getting_started/index.md @@ -20,7 +20,7 @@ You can find more detail on system requirements below, but in short you should a For a small instance (1-20 active users), GoToSocial will likely hover consistently between 250MB and 350MB of RAM usage once the internal caches are hydrated: -![Grafana graph showing GoToSocial heap in use hovering around 250MB and spiking occasionally to 400MB-500MB.](../assets/getting-started-memory-graph.png) +![Grafana graph showing GoToSocial heap in use hovering around 250MB and spiking occasionally to 400MB-500MB.](../public/getting-started-memory-graph.png) In the graph above you can see that RAM usage spikes during periods of load. This happens, for example, when when a status gets boosted by someone with many followers, or when the embedded `ffmpeg` binary is decoding or reencoding media files into thumbnails (especially larger video files). diff --git a/docs/locales/zh/.readthedocs.yaml b/docs/locales/zh/.readthedocs.yaml new file mode 100644 index 000000000..bfeb50cf3 --- /dev/null +++ b/docs/locales/zh/.readthedocs.yaml @@ -0,0 +1,17 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "mambaforge-22.9" # https://docs.readthedocs.io/en/stable/guides/conda.html#making-builds-faster-with-mamba + +mkdocs: + configuration: "docs/locales/zh/mkdocs.yml" + +conda: + environment: "docs/environment.yml" diff --git a/docs/locales/zh/admin/backup_and_restore.md b/docs/locales/zh/admin/backup_and_restore.md new file mode 100644 index 000000000..2a5b50bf1 --- /dev/null +++ b/docs/locales/zh/admin/backup_and_restore.md @@ -0,0 +1,223 @@ +# 备份和恢复 + +由于 GoToSocial 数据库包含了实例以及所有用户的签名密钥,备份是至关重要的。如果这些密钥丢失,您将无法再从此域名进行联合与交流。请记住加密备份,以确保数据在静置时的安全。 + +除了灾难恢复之外,还有其他一些保持备份的好理由。您可以考虑以下几种可能的场景: + +* 您想关闭实例,但可能会在以后重新创建,并且不希望破坏联合功能。 +* 出于某种原因,您需要迁移到不同的数据库(从 Postgres 到 SQLite 或反之亦然)。 +* 您准备对实例进行一些调整,希望快速备份以防出错导致数据丢失。 + +## 需要备份的内容 + +### 数据库 + +大多数备份工具都内置对常见的数据库的支持,如 PostgreSQL 和 SQLite。请确保首先查看他们的文档,因为它们通常会详细说明备份成功完成和恢复所需满足的某些注意事项和条件。 + +### 媒体文件 + +本站媒体文件应被备份。您可以使用 [GoToSocial CLI](cli.md#gotosocial-admin-media-list-local) 列出属于您的实例及其用户的所有媒体文件。 + +外站媒体无需备份。这可以有效地控制备份大小。外站媒体会从原始实例获取,就像因为媒体保留而被修剪掉后再次获取一样。 + +## 如何备份 + +您可以通过以下几种方式进行备份: + +* 为实例和数据库运行所在的 VM/机器制作镜像 +* 使用 CLI 转储 GoToSocial 的状态 +* 备份数据库和媒体文件 +* 使用备份软件 + +尽管设置备份软件可能需要更多工作,但这无疑是最佳选择。它确保了一致和加密的备份,并可以抵御文件系统损坏,而磁盘快照和复制原始数据库及媒体文件无法做到这一点。 + +### 镜像您的磁盘 + +如果您在 VPS(云端的远程机器)上运行 GoToSocial,制作附加到 VPS 的磁盘镜像可能是保存所有数据库条目和媒体的最简单方法。这将保留整个磁盘。许多 VPS 提供商提供定时自动创建备份的选项,因此即使数据丢失,您也可以随时恢复。 + +优点: + +* 相对容易操作。 +* 易于自动化(视具体的 VPS 而定)。 +* 保留完整的媒体和数据库条目。 + +缺点: + +* 可能会根据您的 VPS 产生额外费用。 +* 可能也会保留您不需要的其他程序运行的数据。 +* 与供应商绑定,数据难以迁移。 + +### 使用 GoToSocial CLI + +GoToSocial CLI 工具还提供了从实例备份和恢复数据的命令,这将保留所有必要的最少数据以备份和恢复您的实例,而不破坏与其他实例的联邦。 + +将**保留**的内容有: + +* 所有本站账户条目,包括私钥和公钥。 +* 关注/被关注的外站账户,包括公钥。 +* 关注/关注请求。 +* 实例屏蔽列表。 +* 账户屏蔽列表。 +* 账户封禁列表。 +* 用户名和密码条目,电子邮件地址。 + +将**丢弃**的: + +* 所有贴文。 +* 媒体。 +* 收藏。 +* 书签。 +* 置顶。 +* 应用程序。 +* 令牌。 + +生成的备份文件将是一个以行分隔的 JSON 对象序列(而不是 JSON 数组!)。例如: + +```json +{"type":"account","id":"01F8MH5NBDF2MV7CTC4Q5128HF","createdAt":"2021-08-31T12:00:53.985645Z","username":"1happyturtle","locked":true,"language":"en","uri":"http://localhost:8080/users/1happyturtle","url":"http://localhost:8080/@1happyturtle","inboxURI":"http://localhost:8080/users/1happyturtle/inbox","outboxURI":"http://localhost:8080/users/1happyturtle/outbox","followingUri":"http://localhost:8080/users/1happyturtle/following","followersUri":"http://localhost:8080/users/1happyturtle/followers","featuredCollectionUri":"http://localhost:8080/users/1happyturtle/collections/featured","actorType":"Person","privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAzLP7oyyR+BU9ejn0CN9K+WpX3L37pxUcCgZAGH5lf3cGPZjz\nausfsFME94OjVyzw3K5M2beDkZ4E+Fak46NLtakLB1yovy9jKtj4Y4txHoMvRJLz\neUPxdfeXtpx2d3FDj++Uq4DEE0BhbePXhTGJWaNdC9MQmWKghJnCS5mrnFkdpEFx\njUz9l0UHl2Z4wppxPdpt7FyevcdfKqzGsAA3BxTM0dg47ZJWjtcvfCiSYpAKFNJY\nfKhKn9T3ezZgrLsF+o0IpD23KxWe1X4d5lgJRU9T4FmLmbvyJKUnfgYXbSEvLUcq\n79WbhgRcWwxWubjmWXgPGwzULVhhpYlwd2Cv3wIDAQABAoIBAGF+MxHjD15VV2NY\nKKb1GjMx98i1Xx6TijgoA+zmfha4LGu35e79Lql+0LXFp0zEpa6lAQsMQQhgd0OD\nmKKmSk+pxAvskJ4FxrhIf/yBFA4RMrj5OCaAOocRtdsOJ8n5UtFBrNAF0tzMY9q/\nkgzoq97aVF1mV9iFxaeBx6zT8ozSdqBq1PK/3w1dVg89S5tfKYc7Q0lQ00SfsTnd\niTDClKyqurebo9Pt6M7gXavgg3tvBlmwwr6XHs34Leng3oiN9mW8DVzaBMPzn+rE\nxF2eqs3v9vVpj8es88OwCh5P+ff8vJYvhu7Fcr/bJ8BItBQwfb8QBDATg/MXU2BI\n2ssW6AECgYEA4wmIyYGeu9+hzDa/J3Vh8GnlVNUCohHcChQdOsWsFXUgpVlUIHrX\neKHn42vD4Rzy52/YzJts4NkZTM9sL+kEXIEcpMG/S9xIIud7U0m/hMSAlmnJK/9j\niEXws3o4jo0E77jnRcBdIjpG4K5Eekm0DSR3SFhtZfEdN2DWPvu7K98CgYEA5tER\n/qJwFMc51AobMU87ZjXON7hI2U1WY/pVF62jSl0IcSsnj2riEKWLrs+GRG+HUg+U\naFSqAHcxaVHA0h0AYR8RopAhDdVKh0kvB8biLo+IEzNjPv2vyn0yRN5YSfXdGzyJ\nUjVU6kWdQOwmzy86nHgFaqEx7eofHIaGZzJK/AECgYEAu2VNQHX63TuzQuoVUa5z\nzoq5vhGsALYZF0CO98ndRkDNV22qIL0ESQ/qZS64GYFZhWouWoQXlGfdmCbFN65v\n6SKwz9UT3rvN1vGWO6Ltr9q6AG0EnYpJT1vbV2kUcaU4Y94NFue2d9/+TMnKv91B\n/m8Q/efvNGuWH/WQIaCKV6UCgYBz89WhYMMDfS4M2mLcu5vwddk53qciGxrqMMjs\nkzsz0Va7W12NS7lzeWaZlAE0gf6t98urOdUJVNeKvBoss4sMP0phqxwf0eWV3ur0\ncjIQB+TpGGikLVdRVuGY/UXHKe9AjoHBva8B3aTpB3lbnbNJBXZbIc1uYq3sa5w7\nXWWUAQKBgH3yW73RRpQNcc9hTUssomUsnQQgHxpfWx5tNxqod36Ytd9EKBh3NqUZ\nvPcH6gdh7mcnNaVNTtQOHLHsbPfBK/pqvb3MAsdlokJcQz8MQJ9SGBBPY6PaGw8z\nq/ambaQykER6dwlXTIlU20uXY0bttOL/iYjKmgo3vA66qfzS6nsg\n-----END RSA PRIVATE KEY-----\n","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAzLP7oyyR+BU9ejn0CN9K+WpX3L37pxUcCgZAGH5lf3cGPZjzausf\nsFME94OjVyzw3K5M2beDkZ4E+Fak46NLtakLB1yovy9jKtj4Y4txHoMvRJLzeUPx\ndfeXtpx2d3FDj++Uq4DEE0BhbePXhTGJWaNdC9MQmWKghJnCS5mrnFkdpEFxjUz9\nl0UHl2Z4wppxPdpt7FyevcdfKqzGsAA3BxTM0dg47ZJWjtcvfCiSYpAKFNJYfKhK\nn9T3ezZgrLsF+o0IpD23KxWe1X4d5lgJRU9T4FmLmbvyJKUnfgYXbSEvLUcq79Wb\nhgRcWwxWubjmWXgPGwzULVhhpYlwd2Cv3wIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://localhost:8080/users/1happyturtle#main-key"} +{"type":"account","id":"01F8MH0BBE4FHXPH513MBVFHB0","createdAt":"2021-09-08T10:00:53.985634Z","username":"weed_lord420","locked":true,"language":"en","uri":"http://localhost:8080/users/weed_lord420","url":"http://localhost:8080/@weed_lord420","inboxURI":"http://localhost:8080/users/weed_lord420/inbox","outboxURI":"http://localhost:8080/users/weed_lord420/outbox","followingUri":"http://localhost:8080/users/weed_lord420/following","followersUri":"http://localhost:8080/users/weed_lord420/followers","featuredCollectionUri":"http://localhost:8080/users/weed_lord420/collections/featured","actorType":"Person","privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAzsCcTHzwIgpWKVvut0Q/t1bFwnbj9hO6Ic6k0KXCXbf6qi0b\nMIyLRZr8DS61mD+SPSO2QKEL647xxyW2D8YGtwN6Cc6MpWETsWJkNtS8t7tDL//P\nceYpo5LiqKgn0TXj0Pq8Lvb7rqpH8QJ2EVm14SK+elhKZW/Bi5ZOEwfL8pw6EHI4\nus6VxCNQ099dksu++kbdD7zxqEKnk/4zOttYt0whlVrxzkibTjlKdlSlTYpIstU+\nfNyYVE0xWvrn+yF7jVlEwZYOFGfZbpELadrdOr2k1hvAk7upkrpKmLqYfwqD/xPc\nqwtx0iS6AEnmkSiTcAvju5vLkoLFRU7Of4AZ2wIDAQABAoIBAEAA4GHNS4k+Ke4j\nx4J0XkUjV5UbuPY0pSpSDjOJHOJmUfLcg85Ds9mYYO6zxwOaqmrC42ieclI5rh84\nTWQUqX9+VAk1J9UKeE4xZ1SSBtnZ3rK9PjrERZ+dmQ0dATaCuEO5Wwgu7Trk++Bg\nIqy8WNGZL94v9tfwALp1jTXW9AvmQoNdCFBP62vcmYW4YLjnggxLCFTA8YKfdePa\nTuxxY6uLkeBbxzWpbRU2+bmlxd5OnCkiRSMHIX+6JdtCu2JdWpUTCnWrFi2n1TZz\nZQx9z5rvowK1O785jGMFum5vBWpjIU8sJcXmPjGMU25zzmrhzfmkJsTXER3CXoUo\nSqSPqgECgYEA78OR7bY5KKQQ7Lyz6dru4Fct5P/OXTQoOg5aS7TKb95LVWj+TANn\n5djwIbLmAUV30z0Id9VgiZOL0Hny8+3VV9eU088Z408pAy5WQrL3dB8tZLUJSq5c\n5k6X15/VjWOOZKppDxShzoV3mcohrnwVwkv4fhPFQQOJJBYz6xurWs0CgYEA3MDE\nsDMd9ahzO0dl62ynojkkA8ZTcn2UdyvLpGj9UxT5j9vWF3CfqitXgcpNiVSIbxqQ\nbo/pBch7c/2Xakv5zkdcrJj5/6gyr+m1/tK2o7+CjDaSE4SYwufXx+qkl03Zpyzt\nKdOi7Hz/b2tdjump7ECEDE45mG2ea8oSnPgXl0cCgYBkGGFzu/9g2B24t47ksmHH\nhp3CXIjqoDurARLxSCi7SzJoFc0ULtfRPSAC8YzUOwwrQ++lF4+V3+MexcqHy2Kl\nqXqYcn18SC/3BAE/Fzf3Yoyw3mNiqihefbEmc7PTsxxfKkVx5ksmzNGBgsFM9sCe\nvNigyeAvpCo8xogmPwbqgQKBgE34mIBTzcUzFmBdu5YH7r3RyPK8XkUWLhZZlbgg\njTmHMw6o61mkIgENBf+F4RUckoQLsfAbTIcKZPB3JcAZzcYaVpVwAv1V/3E671lu\nO6xivE2iCL50GzDcis7GBhSbHsF5kNsxMV6uV9qW5ZjQ13/m2b0u9BDuxwHzgdeH\nmW2JAoGAIUOYniuEwdygxWVnYatpr3NPjT3BOKoV5i9zkeJRu1hFpwQM6vQ4Ds5p\nGC5vbMKAv9Cwuw62e2HvqTun3+U2Y5Uived3XCpgM/50BFrFHCfuqXEnu1bEzk5z\n9mIhp8uXPxzC5N7tRQfb3/eU1IUcb6T6ksbr2P81z0j03J55erg=\n-----END RSA PRIVATE KEY-----\n","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAzsCcTHzwIgpWKVvut0Q/t1bFwnbj9hO6Ic6k0KXCXbf6qi0bMIyL\nRZr8DS61mD+SPSO2QKEL647xxyW2D8YGtwN6Cc6MpWETsWJkNtS8t7tDL//PceYp\no5LiqKgn0TXj0Pq8Lvb7rqpH8QJ2EVm14SK+elhKZW/Bi5ZOEwfL8pw6EHI4us6V\nxCNQ099dksu++kbdD7zxqEKnk/4zOttYt0whlVrxzkibTjlKdlSlTYpIstU+fNyY\nVE0xWvrn+yF7jVlEwZYOFGfZbpELadrdOr2k1hvAk7upkrpKmLqYfwqD/xPcqwtx\n0iS6AEnmkSiTcAvju5vLkoLFRU7Of4AZ2wIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://localhost:8080/users/weed_lord420#main-key"} +{"type":"account","id":"01F8MH17FWEB39HZJ76B6VXSKF","createdAt":"2021-09-05T10:00:53.985641Z","username":"admin","locked":true,"language":"en","uri":"http://localhost:8080/users/admin","url":"http://localhost:8080/@admin","inboxURI":"http://localhost:8080/users/admin/inbox","outboxURI":"http://localhost:8080/users/admin/outbox","followingUri":"http://localhost:8080/users/admin/following","followersUri":"http://localhost:8080/users/admin/followers","featuredCollectionUri":"http://localhost:8080/users/admin/collections/featured","actorType":"Person","privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxr2e1pqfLwwUCwHUdx56Mxnq5Kzc2EBwqN6jIPjiqVaG5eVq\nhujDhdqwMq0hnpBSPzLnvjiOtEh7Bwhx0MjuC/GRPTM9oNWPYD4PcjX5ofrubyLR\nBI97qD0SbyzUWzeyBi6R5tpW8LK1MJXNbnYlz5WouEiC4mY77ulri0EN2hCq80wg\nfvtEjEvELcKBqIytKH3rutIzfAyqXD7LSQ8UDoNh9GHyIfq8Zj32gWVk2MiPI3+G\n8kQJDmD8CKEasnrGVdSJBQUg3xDAtOibPXLP+07AIsKYMon35hVNvQNQPS7ru/Bk\nRhhGp2R44zqj6L9mxYbSrhFAaKDedu8oVe1aLQIDAQABAoIBAGK0aIADOU4ffJDe\n7sveiih5Fc1PATwx/QIR2QkWM1SREdx6LYclcX44V8xDanAbE44p1SkHY/CsEtYy\nXnyoXnn2FwFDQrdveY7+I6PApOPLAcKWkyLltC+hbVdj92/6YGNrm7EA/a77wruH\nmwjiivLnTG2CLecNiXSl33DA9YU4Yz+2Tza3IpTdjt8c/dz/BKKaxaWV+i9ew5VR\nioo5v51B+J8PrneCM/p8LGiLV148Njr0JqV6eFy1JuzItYMYdc3Fp+YnMzsuMZEA\n1akMcoln/ucVJyOFnCn6jx47nIoPZLl1KxX3aRDRfvrejm6W4yAkkTmR5voSRqax\njPL3rI0CgYEA9Acu4TO8xJ3uGaUad0N9JTYQVSmtAaE/g+df9LGMSzoj8X95S4xE\nQsGPqNGDm2VWADJjK4P05twZ+LfsfSKQ86wbp4/gbgnXpqB1P5Lty/B7KxiTnNwt\nwb1WGWTCukxfUSL3PRyf8uylkrg72RxKiBx4zKO3WVSLWOZWrFtn0qMCgYEA0H2p\nJs9Nv20ADOOX5tQ7+ruS6/B/Fhyj5fhflSYCAtOW7aME7+zQKJyqSQZ4b2Aub3Tp\nGIaUbRIGzjHyuTultFFWvjU3H5aI/0g1G9WKaBhNkyTIYVmMKtYyhXNvouWing8x\noraWx8TTBP8Cdnnk+QgdR2fpug8cghKupp5wvO8CgYA1JFtRL7MsHjh73TimQExA\njkWARlMmx7bNQtXis8eZmk+5h8kiaqly4DQoz3eZn7fa0x5Fm7b5j3UYdPVLSvvG\nFPTwyKRXUk1kPA1MivK+NuCbwf5jao+MYW8emJLPf1JCmRq+dD1g6aglC3n9Dewt\nOAYWipCjI4Y1FfRKFJ3HgQKBgEAb47+DTyzln3ZXJYZdDHR06SCTuwBZnixAy2NZ\nZJTp6yb3UbVU5E0Yn2QFEVNuB9lN4b8g4tMHEACnazN6G+HugPXL9z9HUqjs0yfT\n6dNIZdIxJUyJ9IfXhYFzlYhJhE+F7IVUD9kttJV8tI0pvja1QAuM8Fm9+84jYIDr\nh08RAoGAMYbjKHbtejcHBwt1kIcSss0cDmlZbBleJo8tdmdg4ndf5GE9N4/EL7tq\nm2zYSfr7OVdnOwRhoO+xF/6d1L7+TR1wz+k2fuMsI71aM5Ocp1nYTutjIkBTcldZ\nZzvjOgZWng5icuRLQQiDSKG5uqazqL/xGXkijb4kp4WW6myWY3c=\n-----END RSA PRIVATE KEY-----\n","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAxr2e1pqfLwwUCwHUdx56Mxnq5Kzc2EBwqN6jIPjiqVaG5eVqhujD\nhdqwMq0hnpBSPzLnvjiOtEh7Bwhx0MjuC/GRPTM9oNWPYD4PcjX5ofrubyLRBI97\nqD0SbyzUWzeyBi6R5tpW8LK1MJXNbnYlz5WouEiC4mY77ulri0EN2hCq80wgfvtE\njEvELcKBqIytKH3rutIzfAyqXD7LSQ8UDoNh9GHyIfq8Zj32gWVk2MiPI3+G8kQJ\nDmD8CKEasnrGVdSJBQUg3xDAtOibPXLP+07AIsKYMon35hVNvQNQPS7ru/BkRhhG\np2R44zqj6L9mxYbSrhFAaKDedu8oVe1aLQIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://localhost:8080/users/admin#main-key"} +{"type":"account","id":"01F8MH1H7YV1Z7D2C8K2730QBF","createdAt":"2021-09-06T10:00:53.985643Z","username":"the_mighty_zork","locked":true,"language":"en","uri":"http://localhost:8080/users/the_mighty_zork","url":"http://localhost:8080/@the_mighty_zork","inboxURI":"http://localhost:8080/users/the_mighty_zork/inbox","outboxURI":"http://localhost:8080/users/the_mighty_zork/outbox","followingUri":"http://localhost:8080/users/the_mighty_zork/following","followersUri":"http://localhost:8080/users/the_mighty_zork/followers","featuredCollectionUri":"http://localhost:8080/users/the_mighty_zork/collections/featured","actorType":"Person","privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEApBmF8U+or+E0mgUMH3LE4uRIWzeV9rhYnvSMm9OpOsxwJiss\n5mEA/NtPHvQlq2UwrqXX89Wvu94K9EzZ4VyWYQGdxaiPpt17vRqUfsHUnXkY0pvC\nC9zt/aNlJtdt2xm+7PTC0YQd4+E1FX3aaoUPJL8MXzNlpJzaUtuwLZe1iBmFfatZ\nFHptEgc4nlf6TNLTzj3Yw1/7zIGVS8Vi7VquHc0Xo8dRiL2RxCGzLWnwL6GlrxY1\ntMhsUg467XeoiwegFCpcIhAhPFREKoTnCEksL/N0rpXl7m6CAy5uqBGs5mMXnXlq\nefr58l0j2dU6zc60LCHH9TJC+roXsKJhy9sx/QIDAQABAoIBAFa+UypbFG1cW2Tr\nNBxPm7ngOEtXl8MicV4dIVKh0TwOo13ZxtNFBbOj7jALmPn/9HrtmbkABPQHDL1U\n/nt9aNSAeTjpwH3RaD5vFX3n0g8n2zJBOZLxxzAjNi4RBLYj5uP1AiKkdvRlsJza\nuSFDkty2zMBqN9mLPHE+RePj5Qa6tjYfIQqQzu/+YnYMlXHoC2yHNKsvz6S5FhVj\nv5zATv2JlJQH3RSmhuPOah73iQnKCLzYYEAHleawKrCg/rZ3ht37Guvabeq7MqQN\nvi9pJdAA+RMxPsboHajskePjOTYJgKQSxEAMRTMfBR40aZxklxQL0EoBd1Y3CHXh\nfMg0xWECgYEA0ORrpJ1A2WNQwKcDDeBBsaJqWF4EraoFzYrugKZrAYEeVyuGD0zq\nARUaWkZTZ1f6wQ10i1WxAuKlBEds7QsLdZzLsA4um4JlBroCZiYfPnmTtb8op1LY\nFqeYTByvAmnfWWTuOI67GX9ruLg8tEGuz38kuQVSxYs51its3tScNPUCgYEAyRst\nwRbqpOqnwoRoS6pxv0Vpc3nUcfaVYwsg/qobJkiwAdlUYeE7alvEY926VW4cvU/X\nhy3L1punAqnyLI7uuqCefXEbNxO0Cebyy4Kv2Ye1uzl0OHsJczSNdfpNqfAIKwtN\nHLCYDGCsluQhz+I/5Pd0dT+JDPPW9hKS2HG7o+kCgYBqugn1VRLo/sEnbS02TbnC\n1ESZWY/yWsgUOEObH2vUnO+vgeFAt/9nBi0sqnm6d0z6jbFZ7zI9UycUhJm2ksoM\nEUxQay6M7ZZIVYkcP6X++YbqePyAYOdey8oYOR+BkC45MkQ0SVh2so+LFTaOsnBq\nO3+7uGiN3ZBzSESbpO0acQKBgQCONrsXZeZO82XpB4tdns3LbgGRWKEkajTgEnml\nvZNvck2NMSwb/5PttbFe0ei4CyMluPV4MamJPQ9Qse+BFR67OWR63uZY/4T8z6X4\nxpUmZnLcUFfgrRlUr+AtgvEy8HxGPDquxC7x6deC6RcEFEIM3/UqCOEZGMJ1x1Ky\n31LLKQKBgGCKwVgQ8+4JyHZFkie3YdHhxJDokgY+Opb0HNnoBY/lZ54UMCCJQPS2\n0XPSu651j/3adr3RQneU04gF6U2/D5JzFEV0kUsqZ4Zy2EEU0LU4ibus0gyomSpK\niWhU4QrC/M4ELxYZinlNu3ThPWNQ/PMNteVWfdgOcV7uUWl0ViFp\n-----END RSA PRIVATE KEY-----\n","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEApBmF8U+or+E0mgUMH3LE4uRIWzeV9rhYnvSMm9OpOsxwJiss5mEA\n/NtPHvQlq2UwrqXX89Wvu94K9EzZ4VyWYQGdxaiPpt17vRqUfsHUnXkY0pvCC9zt\n/aNlJtdt2xm+7PTC0YQd4+E1FX3aaoUPJL8MXzNlpJzaUtuwLZe1iBmFfatZFHpt\nEgc4nlf6TNLTzj3Yw1/7zIGVS8Vi7VquHc0Xo8dRiL2RxCGzLWnwL6GlrxY1tMhs\nUg467XeoiwegFCpcIhAhPFREKoTnCEksL/N0rpXl7m6CAy5uqBGs5mMXnXlqefr5\n8l0j2dU6zc60LCHH9TJC+roXsKJhy9sx/QIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://localhost:8080/users/the_mighty_zork#main-key"} +{"type":"block","id":"01FEXXET6XXMF7G2V3ASZP3YQW","createdAt":"2021-09-08T09:00:53.965362Z","uri":"http://localhost:8080/users/1happyturtle/blocks/01FEXXET6XXMF7G2V3ASZP3YQW","accountId":"01F8MH5NBDF2MV7CTC4Q5128HF","targetAccountId":"01F8MH5ZK5VRH73AKHQM6Y9VNX"} +{"type":"account","id":"01F8MH5ZK5VRH73AKHQM6Y9VNX","createdAt":"2021-08-31T12:00:53.985646Z","username":"foss_satan","domain":"fossbros-anonymous.io","locked":true,"language":"en","uri":"http://fossbros-anonymous.io/users/foss_satan","url":"http://fossbros-anonymous.io/@foss_satan","inboxURI":"http://fossbros-anonymous.io/users/foss_satan/inbox","outboxURI":"http://fossbros-anonymous.io/users/foss_satan/outbox","followingUri":"http://fossbros-anonymous.io/users/foss_satan/following","followersUri":"http://fossbros-anonymous.io/users/foss_satan/followers","featuredCollectionUri":"http://fossbros-anonymous.io/users/foss_satan/collections/featured","actorType":"Person","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEA2OyVgkaIL9VohXKYTh319j4OouHRX/8QC7piXj71k7q5RDzEyvis\nVZBc5/C1/crCpxt895i0Ai2CiXQx+dISV7s/JBhAGl8s7TQ8jLlMuptrI0+sdkBC\nlu8pU0qQmoeXVnlquOzNmqGufUxIDtLXLZDN17qf/7vWA23q4d0tG5KQhGGGKiVM\n61Ufvr9MmgPBSpyUvYMAulFlz1264L49aGWeVgOz3qUQzqtxjrP0kaIbeyt56miP\nKr5AqkRgSsXci+FAo6suxR5gzo9NgleNkbZWF9MQyKlawukPwZUDSh396vtNQMee\n/4mto7mAXw8iio0IacrYO3F7iyewXnmI/QIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://fossbros-anonymous.io/users/foss_satan/main-key"} +{"type":"follow","id":"01F8PYDCE8XE23GRE5DPZJDZDP","createdAt":"2021-09-08T09:00:54.749465Z","uri":"http://localhost:8080/users/the_mighty_zork/follow/01F8PYDCE8XE23GRE5DPZJDZDP","accountId":"01F8MH1H7YV1Z7D2C8K2730QBF","targetAccountId":"01F8MH5NBDF2MV7CTC4Q5128HF"} +{"type":"follow","id":"01F8PY8RHWRQZV038T4E8T9YK8","createdAt":"2021-09-06T12:00:54.749459Z","uri":"http://localhost:8080/users/the_mighty_zork/follow/01F8PY8RHWRQZV038T4E8T9YK8","accountId":"01F8MH1H7YV1Z7D2C8K2730QBF","targetAccountId":"01F8MH17FWEB39HZJ76B6VXSKF"} +{"type":"domainBlock","id":"01FF22EQM7X8E3RX1XGPN7S87D","createdAt":"2021-09-08T10:00:53.968971Z","domain":"replyguys.com","createdByAccountID":"01F8MH17FWEB39HZJ76B6VXSKF","privateComment":"i blocked this domain because they keep replying with pushy + unwarranted linux advice","publicComment":"reply-guying to tech posts","obfuscate":false} +{"type":"user","id":"01F8MGYG9E893WRHW0TAEXR8GJ","createdAt":"2021-09-08T10:00:53.97247Z","accountID":"01F8MH0BBE4FHXPH513MBVFHB0","encryptedPassword":"$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS","locale":"en","lastEmailedAt":"0001-01-01T00:00:00Z","confirmationToken":"a5a280bd-34be-44a3-8330-a57eaf61b8dd","confirmationTokenSentAt":"2021-09-08T10:00:53.972472Z","unconfirmedEmail":"weed_lord420@example.org","moderator":false,"admin":false,"disabled":false,"approved":false} +{"type":"user","id":"01F8MGWYWKVKS3VS8DV1AMYPGE","createdAt":"2021-09-05T10:00:53.972475Z","email":"admin@example.org","accountID":"01F8MH17FWEB39HZJ76B6VXSKF","encryptedPassword":"$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS","currentSignInAt":"2021-09-08T09:50:53.972477Z","lastSignInAt":"2021-09-08T08:00:53.972477Z","chosenLanguages":["en"],"locale":"en","lastEmailedAt":"2021-09-08T09:30:53.972478Z","confirmedAt":"2021-09-05T10:00:53.972478Z","moderator":true,"admin":true,"disabled":false,"approved":true} +{"type":"user","id":"01F8MGVGPHQ2D3P3X0454H54Z5","createdAt":"2021-09-06T22:00:53.97248Z","email":"zork@example.org","accountID":"01F8MH1H7YV1Z7D2C8K2730QBF","encryptedPassword":"$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS","currentSignInAt":"2021-09-08T09:30:53.972481Z","lastSignInAt":"2021-09-08T08:00:53.972481Z","chosenLanguages":["en"],"locale":"en","lastEmailedAt":"2021-09-08T09:05:53.972482Z","confirmationTokenSentAt":"2021-09-06T22:00:53.972483Z","confirmedAt":"2021-09-07T00:00:53.972482Z","moderator":false,"admin":false,"disabled":false,"approved":true} +{"type":"user","id":"01F8MH1VYJAE00TVVGMM5JNJ8X","createdAt":"2021-09-06T22:00:53.972485Z","email":"tortle.dude@example.org","accountID":"01F8MH5NBDF2MV7CTC4Q5128HF","encryptedPassword":"$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS","currentSignInAt":"2021-09-08T09:30:53.972485Z","lastSignInAt":"2021-09-08T08:00:53.972486Z","chosenLanguages":["en"],"locale":"en","lastEmailedAt":"2021-09-08T09:05:53.972487Z","confirmationTokenSentAt":"2021-09-06T22:00:53.972487Z","confirmedAt":"2021-09-07T00:00:53.972487Z","moderator":false,"admin":false,"disabled":false,"approved":true} +{"type":"instance","id":"01BZDDRPAB8J645ABY31HHF68Y","createdAt":"2021-09-08T10:00:54.763912Z","domain":"localhost:8080","title":"localhost:8080","uri":"http://localhost:8080","reputation":0} +``` + +有关如何使用命令导入/导出的信息,参见[此处](cli.md#gotosocial-admin-export)。尽管 `export` 命令不会备份媒体,但可以使用 [`media list-local`](cli.md#gotosocial-admin-media-list-local) 命令来确定应该保留哪些媒体文件。 + +优势: + +* 数据库无关:导出的数据采用了一种通用格式,可以使用 `import` 命令将这些数据插入 Postgres 或 SQLite 数据库中。 +* 轻量级:仅保留必要的内容,因此备份文件可以非常小(甚至小到可以通过电子邮件发送)。备份/导入命令只需几秒钟即可运行。 +* 易读格式:输出是 JSON 格式。 + +劣势: + +* 贴文/收藏等的丢失:除非你愿意丢失某些内容,否则不要这样进行备份/恢复。 +* 需要使用 GoToSocial CLI 工具将数据插回数据库,除非你为此编写自定义工具。 + +### 备份你的数据库文件和媒体 + +无论你是使用 PostgreSQL 还是 SQLite 作为 GoToSocial 数据库,都可以直接使用类似 [rclone](https://rclone.org/) 的工具来备份数据库文件,或者遵循 [Postgres 数据备份的最佳实践](https://www.postgresql.org/docs/15/backup.html) 或 [SQLite 数据备份的最佳实践](https://sqlite.org/backup.html)。 + +使用 GoToSocial CLI 的媒体 [`list-attachments`](cli.md#gotosocial-admin-media-list-attachments) 和 [`list-emojis`](cli.md#gotosocial-admin-media-list-emojis) 命令来获取需要保护的媒体文件列表。 + +优势: + +* 备份相对便携 - 可以将数据从一台机器迁移到另一台。 +* 有大量文档和工具可供参考。 +* 可以根据需要有多种备份方式。 + +劣势: + +* 初次设置可能有点复杂。 +* 需要确定备份的存放位置。 +* 从备份中恢复可能比较麻烦。 +* 除非同时备份媒体,否则数据库中的媒体附件引用将会中断。 + +### 备份软件 + +备份软件专为帮助你创建、管理和恢复备份而设计。它通常知道如何安全地备份数据库,因此你不用成为 PostgreSQL 或 SQLite 备份专家。它也可以从文件系统中进行备份。 + +尽管与直接备份数据库文件的方法大致具有相同的优点和缺点,这种方法还有一些额外的好处: + +* 备份高度便携,可以从零开始恢复数据库。 +* 备份按照常规计划进行,并有可配置的保留策略。 +* 备份是增量和压缩的,以节省存储和带宽。 +* 备份是加密的。 +* 内置工具可以列出快照并从中恢复。 + +!!! tip + [Rsync.net](https://rsync.net/)、[BorgBase](https://www.borgbase.com/) 和 [Hetzner Storage](https://www.hetzner.com/storage/storage-box) 提供了可用于备份的经济实惠的存储。Rsync.net 有一种专门为 Borg 设计的备份产品,比他们的常规存储产品便宜得多。如果你只想使用 Borg 管理的备份,请在[此处注册](https://www.rsync.net/products/borg.html)。 + +#### Borgmatic + +[Borgmatic](https://torsion.org/borgmatic/) 是一个帮助使用 [Borg](https://www.borgbackup.org/) 进行备份的工具。它通过使用 YAML 的声明性配置文件驱动。BorgBase、Rsync.net 和 Hetzner 都支持 Borg。 + +!!! warning + 初始化 Borg 仓库时,确保使用强加密密钥进行设置,并将密钥安全地存放在某处。否则将无法在将来解密备份。ArchWiki 上关于 Borgmatic 的条目解释了如何安全地将你的加密密钥传递给 Borgmatic,而不在配置文件中以明文形式存储它。 + +如何使用 Borgmatic 备份数据库有其[单独的文档页面](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/),你应当在备份前查看一下。对于使用 SQLite 的 GoToSocial,Borgmatic 的简单 `config.yaml` 如下: + + +```yaml +location: + repositories: + - path: ssh://<在你的提供商控制面板中查找ssh地址> + label: <可以是任意内容,但通常是提供商名称,例如 borgbase> + patterns_from: + - /etc/borgmatic/gotosocial_patterns + +storage: + compression: auto,zstd + archive_name_format: '{hostname}-{now:%Y-%m-%d-%H%M%S}' + retries: 5 + retry_wait: 30 + +retention: + keep_daily: 7 + keep_weekly: 6 + keep_monthly: 12 + +hooks: + before_backup: + - /usr/bin/systemctl stop gotosocial + after_backup: + - /usr/bin/systemctl start gotosocial + sqlite_databases: + - name: gotosocial + path: /path/to/sqlite.db +``` + +对于 PostgreSQL,你应该使用 `postgresql_databases`。 + +`patterns_from` 中提到的文件可以通过转换 GoToSocial CLI 媒体命令 [`list-attachments`](cli.md#gotosocial-admin-media-list-attachments) 和 [`list-emojis`](cli.md#gotosocial-admin-media-list-emojis) 的输出来创建。要生成正确的模式,您可以使用 [`media-to-borg-patterns.py`](https://github.com/superseriousbusiness/gotosocial/tree/main/example/borgmatic/media-to-borg-patterns.py) 脚本。有关 Borg 模式如何工作的详情,参见 [他们的文档](https://man.archlinux.org/man/borg-patterns.1)。 + +您需要将该文件放在您的 GoToSocial 实例上,并确保该文件是可执行的。它需要 Python 3,安装 Borg 和 Borgmatic 后您应该已经具备。它仅依赖于 Python 标准库。 + +!!! note + 为了确保可靠运行,您应确保 GoToSocial 配置中的 [storage-local-base-path](../configuration/storage.md) 使用的是绝对路径。否则您将需要自己调整路径。 + +```sh +$ gotosocial admin media list-attachments --local-only | \ + /path/to/media-to-borg-patterns.py \ + +``` + +这将在控制台上输出一个类似于以下内容的模式集: + +``` +R ++ pp:/ +- /* +``` + +!!! tip + 你可以通过向 `media-to-borg-patterns.py` 传递 `--help` 来查看帮助。通过将文件位置作为脚本的最后一个参数,也可以将输出直接写入文件。 + +给定这组模式,Borg 将从 `` 开始寻找文件。任何匹配路径前缀 `pp:` 的都会被包括进去。其他的则会匹配最后一个模式,从存档中排除。 + +在单用户实例中,你可以运行此命令一次,并直接在 Borgmatic 配置中内联模式 [使用 `patterns` 键](https://torsion.org/borgmatic/docs/reference/configuration/)。在多用户实例中,你应该在用户注册后运行此命令。或者,每次备份前都可以运行它。 + +如果你将 Borgmatic 作为 systemd 服务运行,可以为 `borgmatic.service` [创建一个 drop-in](https://wiki.archlinux.org/title/systemd#Drop-in_files),在备份开始前运行模式生成: + + +```ini +[Service] +ExecStartPre=/path/to/gotosocial admin media list-attachments --local-only | /path/to/media-to-borg-patterns.py /etc/borgmatic/gotosocial_patterns +``` + +建议查看的文档: + +* Borgmatic [配置参考](https://torsion.org/borgmatic/docs/reference/configuration/) +* ArchWiki 关于 [Borgmatic 的条目](https://wiki.archlinux.org/title/Borgmatic) +* ArchWiki 关于 [Borg 的条目](https://wiki.archlinux.org/title/Borg_backup) +* BorgBase [文档](https://docs.borgbase.com/) +* Hetzner 社区指南关于 [设置 Borgmatic](https://community.hetzner.com/tutorials/install-and-configure-borgmatic) diff --git a/docs/locales/zh/admin/cli.md b/docs/locales/zh/admin/cli.md new file mode 100644 index 000000000..24ea8745b --- /dev/null +++ b/docs/locales/zh/admin/cli.md @@ -0,0 +1,465 @@ +# GtS CLI 工具 + +GoToSocial 编译为二进制可执行文件。 + +使用此二进制文件的标准方法是通过运行 `gotosocial server start` 命令启动服务器。 + +不过,此二进制文件也可以作为管理工具和调试工具使用。 + +以下是 `gotosocial --help` 的完整输出,不包括全局配置选项的大列表。 + +```text +GoToSocial - 一个联邦制社交媒体服务器 + +帮助文档参见:https://docs.gotosocial.org。 + +代码仓库:https://github.com/superseriousbusiness/gotosocial + +用法: + gotosocial [command] + +可用命令: + admin gotosocial 管理相关任务 + debug gotosocial 调试相关任务 + help 获取任何命令的帮助 + server gotosocial 服务器相关任务 +``` + +在 `可用命令` 下,可以看到标准的 `server` 命令。但是也有处理管理和调试的命令,这些将在本文档中进行解释。 + +!!! Info "将全局配置传递给 CLI" + + 对于所有这些命令,你仍然需要正确设置全局选项,以便 CLI 工具知道如何连接到你的数据库,以及使用哪个数据库、哪个主机和账户域等。 + + 你可以使用环境变量设置这些选项,通过 CLI 标志传递它们(例如,`gotosocial [commands] --host example.org`),或者只需将 CLI 工具指向你的配置文件(例如,`gotosocial --config-path ./config.yaml [commands]`)。 + +!!! Info + + 运行 CLI 命令时,你将会看到如下输出: + + ```text + time=XXXX level=info msg=connected to SQLITE database + time=XXXX level=info msg=there are no new migrations to run func=doMigration + time=XXXX level=info msg=closing db connection + ``` + + 这是正常的,表示命令已按预期运行。 + +!!! Warning "运行管理命令后重启 GtS" + + 由于 GoToSocial 的内部缓存机制,你可能需要在运行某些命令后重启 GoToSocial,以使命令的效果“生效”。我们仍在寻找一种无需重启的方法。在此期间,需要在运行命令后重启的命令将在下文中突出显示。 + +## gotosocial admin + +包含 `account`、`export`、`import` 和 `media` 子命令。 + +### gotosocial admin account create + +此命令可用于在你的实例上创建新账户。 + +`gotosocial admin account create --help`: + +```text +创建一个新的本站账户 + +用法: + gotosocial admin account create [flags] + +标志: + --email string 该账户的电子邮件地址 + -h, --help 获取创建命令的帮助 + --password string 为该账户设置的密码 + --username string 要创建/删除等的用户名 +``` + +示例: + +```bash +gotosocial admin account create \ + --username some_username \ + --email someuser@example.org \ + --password 'somelongandcomplicatedpassword' \ + --config-path config.yaml +``` + +### gotosocial admin account confirm + +此命令可用于确认你的实例上的用户+账户,允许他们登录并使用账户。 + +!!! Info + + 如果账户是使用 `admin account create` 创建的,则不必在账户上运行 `confirm`,它将已被确认。 + +`gotosocial admin account confirm --help`: + +```text +手动确认现有本站账户,从而跳过电子邮件确认 + +用法: + gotosocial admin account confirm [flags] + +标志: + -h, --help 获取确认命令的帮助 + --username string 要创建/删除等的用户名 +``` + +示例: + +```bash +gotosocial admin account confirm --username some_username --config-path config.yaml +``` + +### gotosocial admin account promote + +此命令可用于将用户提升为管理员。 + +!!! Warning "需要重启服务器" + + 为使更改生效,此命令需要在运行命令后重启 GoToSocial。 + +`gotosocial admin account promote --help`: + +```text +将本站账户提升为管理员 + +用法: + gotosocial admin account promote [flags] + +标志: + -h, --help 获取提升命令的帮助 + --username string 要创建/删除等的用户名 +``` + +示例: + +```bash +gotosocial admin account promote --username some_username --config-path config.yaml +``` + +### gotosocial admin account demote + +此命令可用于将用户从管理员降级为普通用户。 + +!!! Warning "需要重启服务器" + + 为使更改生效,此命令需要在运行命令后重启 GoToSocial。 + +`gotosocial admin account demote --help`: + +```text +将本站账户从管理员降级为普通用户 + +用法: + gotosocial admin account demote [flags] + +标志: + -h, --help 获取降级命令的帮助 + --username string 要创建/删除等的用户名 +``` + +示例: + +```bash +gotosocial admin account demote --username some_username --config-path config.yaml +``` + +### gotosocial admin account disable + +此命令可用于在你的实例上禁用一个账户:禁止其登录或执行任何操作,但不删除数据。 + +!!! Warning "需要重启服务器" + + 为使更改生效,此命令需要在运行命令后重启 GoToSocial。 + +`gotosocial admin account disable --help`: + +```text +将本站账户的 `disabled` 设置为 true,以防止其登录或发布等,但不删除任何内容 + +用法: + gotosocial admin account disable [flags] + +标志: + -h, --help 获取禁用命令的帮助 + --username string 要创建/删除等的用户名 +``` + +示例: + +```bash +gotosocial admin account disable --username some_username --config-path config.yaml +``` + +### gotosocial admin account enable + +此命令可用于重新启用你实例上的账户,撤销之前的 `disable` 命令。 + +!!! Warning "需要重启服务器" + + 为使更改生效,此命令需要在运行命令后重启 GoToSocial。 + +`gotosocial admin account enable --help`: + +```text +通过将本站账户的 `disabled` 设置为 false,撤销之前的禁用命令 + +用法: + gotosocial admin account enable [flags] + +标志: + -h, --help 获取启用命令的帮助 + --username string 要创建/删除等的用户名 +``` + +示例: + +```bash +gotosocial admin account enable --username some_username --config-path config.yaml +``` + +### gotosocial admin account password + +此命令可用于为指定的本站账户设置新密码。 + +!!! Warning "需要重启服务器" + + 为使更改生效,此命令需要在运行命令后重启 GoToSocial。 + +`gotosocial admin account password --help`: + +```text +为指定的本站账户设置新密码 + +用法: + gotosocial admin account password [flags] + +标志: + -h, --help 获取密码命令的帮助 + --password string 为该账户设置的密码 + --username string 要创建/删除等的用户名 +``` + +示例: + +```bash +gotosocial admin account password --username some_username --password some_really_good_password --config-path config.yaml +``` + +### gotosocial admin export + +此命令可用于将你的 GoToSocial 实例中的数据导出到文件,以便备份/存储。 + +文件格式将是一系列以换行符分隔的 JSON 对象。 + +`gotosocial admin export --help`: + +```text +将数据从数据库导出到指定路径的文件 + +用法: + gotosocial admin export [flags] + +标志: + -h, --help 获取导出命令的帮助 + --path string 导入/导出文件的路径 +``` + +Example: + +```bash +gotosocial admin export --path example.json --config-path config.yaml +``` + +`example.json`: + +```json +{"type":"account","id":"01F8MH5NBDF2MV7CTC4Q5128HF","createdAt":"2021-08-31T12:00:53.985645Z","username":"1happyturtle","locked":true,"language":"en","uri":"http://localhost:8080/users/1happyturtle","url":"http://localhost:8080/@1happyturtle","inboxURI":"http://localhost:8080/users/1happyturtle/inbox","outboxURI":"http://localhost:8080/users/1happyturtle/outbox","followingUri":"http://localhost:8080/users/1happyturtle/following","followersUri":"http://localhost:8080/users/1happyturtle/followers","featuredCollectionUri":"http://localhost:8080/users/1happyturtle/collections/featured","actorType":"Person","privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAzLP7oyyR+BU9ejn0CN9K+WpX3L37pxUcCgZAGH5lf3cGPZjz\nausfsFME94OjVyzw3K5M2beDkZ4E+Fak46NLtakLB1yovy9jKtj4Y4txHoMvRJLz\neUPxdfeXtpx2d3FDj++Uq4DEE0BhbePXhTGJWaNdC9MQmWKghJnCS5mrnFkdpEFx\njUz9l0UHl2Z4wppxPdpt7FyevcdfKqzGsAA3BxTM0dg47ZJWjtcvfCiSYpAKFNJY\nfKhKn9T3ezZgrLsF+o0IpD23KxWe1X4d5lgJRU9T4FmLmbvyJKUnfgYXbSEvLUcq\n79WbhgRcWwxWubjmWXgPGwzULVhhpYlwd2Cv3wIDAQABAoIBAGF+MxHjD15VV2NY\nKKb1GjMx98i1Xx6TijgoA+zmfha4LGu35e79Lql+0LXFp0zEpa6lAQsMQQhgd0OD\nmKKmSk+pxAvskJ4FxrhIf/yBFA4RMrj5OCaAOocRtdsOJ8n5UtFBrNAF0tzMY9q/\nkgzoq97aVF1mV9iFxaeBx6zT8ozSdqBq1PK/3w1dVg89S5tfKYc7Q0lQ00SfsTnd\niTDClKyqurebo9Pt6M7gXavgg3tvBlmwwr6XHs34Leng3oiN9mW8DVzaBMPzn+rE\nxF2eqs3v9vVpj8es88OwCh5P+ff8vJYvhu7Fcr/bJ8BItBQwfb8QBDATg/MXU2BI\n2ssW6AECgYEA4wmIyYGeu9+hzDa/J3Vh8GnlVNUCohHcChQdOsWsFXUgpVlUIHrX\neKHn42vD4Rzy52/YzJts4NkZTM9sL+kEXIEcpMG/S9xIIud7U0m/hMSAlmnJK/9j\niEXws3o4jo0E77jnRcBdIjpG4K5Eekm0DSR3SFhtZfEdN2DWPvu7K98CgYEA5tER\n/qJwFMc51AobMU87ZjXON7hI2U1WY/pVF62jSl0IcSsnj2riEKWLrs+GRG+HUg+U\naFSqAHcxaVHA0h0AYR8RopAhDdVKh0kvB8biLo+IEzNjPv2vyn0yRN5YSfXdGzyJ\nUjVU6kWdQOwmzy86nHgFaqEx7eofHIaGZzJK/AECgYEAu2VNQHX63TuzQuoVUa5z\nzoq5vhGsALYZF0CO98ndRkDNV22qIL0ESQ/qZS64GYFZhWouWoQXlGfdmCbFN65v\n6SKwz9UT3rvN1vGWO6Ltr9q6AG0EnYpJT1vbV2kUcaU4Y94NFue2d9/+TMnKv91B\n/m8Q/efvNGuWH/WQIaCKV6UCgYBz89WhYMMDfS4M2mLcu5vwddk53qciGxrqMMjs\nkzsz0Va7W12NS7lzeWaZlAE0gf6t98urOdUJVNeKvBoss4sMP0phqxwf0eWV3ur0\ncjIQB+TpGGikLVdRVuGY/UXHKe9AjoHBva8B3aTpB3lbnbNJBXZbIc1uYq3sa5w7\nXWWUAQKBgH3yW73RRpQNcc9hTUssomUsnQQgHxpfWx5tNxqod36Ytd9EKBh3NqUZ\nvPcH6gdh7mcnNaVNTtQOHLHsbPfBK/pqvb3MAsdlokJcQz8MQJ9SGBBPY6PaGw8z\nq/ambaQykER6dwlXTIlU20uXY0bttOL/iYjKmgo3vA66qfzS6nsg\n-----END RSA PRIVATE KEY-----\n","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAzLP7oyyR+BU9ejn0CN9K+WpX3L37pxUcCgZAGH5lf3cGPZjzausf\nsFME94OjVyzw3K5M2beDkZ4E+Fak46NLtakLB1yovy9jKtj4Y4txHoMvRJLzeUPx\ndfeXtpx2d3FDj++Uq4DEE0BhbePXhTGJWaNdC9MQmWKghJnCS5mrnFkdpEFxjUz9\nl0UHl2Z4wppxPdpt7FyevcdfKqzGsAA3BxTM0dg47ZJWjtcvfCiSYpAKFNJYfKhK\nn9T3ezZgrLsF+o0IpD23KxWe1X4d5lgJRU9T4FmLmbvyJKUnfgYXbSEvLUcq79Wb\nhgRcWwxWubjmWXgPGwzULVhhpYlwd2Cv3wIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://localhost:8080/users/1happyturtle#main-key"} +{"type":"account","id":"01F8MH0BBE4FHXPH513MBVFHB0","createdAt":"2021-09-08T10:00:53.985634Z","username":"weed_lord420","locked":true,"language":"en","uri":"http://localhost:8080/users/weed_lord420","url":"http://localhost:8080/@weed_lord420","inboxURI":"http://localhost:8080/users/weed_lord420/inbox","outboxURI":"http://localhost:8080/users/weed_lord420/outbox","followingUri":"http://localhost:8080/users/weed_lord420/following","followersUri":"http://localhost:8080/users/weed_lord420/followers","featuredCollectionUri":"http://localhost:8080/users/weed_lord420/collections/featured","actorType":"Person","privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAzsCcTHzwIgpWKVvut0Q/t1bFwnbj9hO6Ic6k0KXCXbf6qi0b\nMIyLRZr8DS61mD+SPSO2QKEL647xxyW2D8YGtwN6Cc6MpWETsWJkNtS8t7tDL//P\nceYpo5LiqKgn0TXj0Pq8Lvb7rqpH8QJ2EVm14SK+elhKZW/Bi5ZOEwfL8pw6EHI4\nus6VxCNQ099dksu++kbdD7zxqEKnk/4zOttYt0whlVrxzkibTjlKdlSlTYpIstU+\nfNyYVE0xWvrn+yF7jVlEwZYOFGfZbpELadrdOr2k1hvAk7upkrpKmLqYfwqD/xPc\nqwtx0iS6AEnmkSiTcAvju5vLkoLFRU7Of4AZ2wIDAQABAoIBAEAA4GHNS4k+Ke4j\nx4J0XkUjV5UbuPY0pSpSDjOJHOJmUfLcg85Ds9mYYO6zxwOaqmrC42ieclI5rh84\nTWQUqX9+VAk1J9UKeE4xZ1SSBtnZ3rK9PjrERZ+dmQ0dATaCuEO5Wwgu7Trk++Bg\nIqy8WNGZL94v9tfwALp1jTXW9AvmQoNdCFBP62vcmYW4YLjnggxLCFTA8YKfdePa\nTuxxY6uLkeBbxzWpbRU2+bmlxd5OnCkiRSMHIX+6JdtCu2JdWpUTCnWrFi2n1TZz\nZQx9z5rvowK1O785jGMFum5vBWpjIU8sJcXmPjGMU25zzmrhzfmkJsTXER3CXoUo\nSqSPqgECgYEA78OR7bY5KKQQ7Lyz6dru4Fct5P/OXTQoOg5aS7TKb95LVWj+TANn\n5djwIbLmAUV30z0Id9VgiZOL0Hny8+3VV9eU088Z408pAy5WQrL3dB8tZLUJSq5c\n5k6X15/VjWOOZKppDxShzoV3mcohrnwVwkv4fhPFQQOJJBYz6xurWs0CgYEA3MDE\nsDMd9ahzO0dl62ynojkkA8ZTcn2UdyvLpGj9UxT5j9vWF3CfqitXgcpNiVSIbxqQ\nbo/pBch7c/2Xakv5zkdcrJj5/6gyr+m1/tK2o7+CjDaSE4SYwufXx+qkl03Zpyzt\nKdOi7Hz/b2tdjump7ECEDE45mG2ea8oSnPgXl0cCgYBkGGFzu/9g2B24t47ksmHH\nhp3CXIjqoDurARLxSCi7SzJoFc0ULtfRPSAC8YzUOwwrQ++lF4+V3+MexcqHy2Kl\nqXqYcn18SC/3BAE/Fzf3Yoyw3mNiqihefbEmc7PTsxxfKkVx5ksmzNGBgsFM9sCe\nvNigyeAvpCo8xogmPwbqgQKBgE34mIBTzcUzFmBdu5YH7r3RyPK8XkUWLhZZlbgg\njTmHMw6o61mkIgENBf+F4RUckoQLsfAbTIcKZPB3JcAZzcYaVpVwAv1V/3E671lu\nO6xivE2iCL50GzDcis7GBhSbHsF5kNsxMV6uV9qW5ZjQ13/m2b0u9BDuxwHzgdeH\nmW2JAoGAIUOYniuEwdygxWVnYatpr3NPjT3BOKoV5i9zkeJRu1hFpwQM6vQ4Ds5p\nGC5vbMKAv9Cwuw62e2HvqTun3+U2Y5Uived3XCpgM/50BFrFHCfuqXEnu1bEzk5z\n9mIhp8uXPxzC5N7tRQfb3/eU1IUcb6T6ksbr2P81z0j03J55erg=\n-----END RSA PRIVATE KEY-----\n","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAzsCcTHzwIgpWKVvut0Q/t1bFwnbj9hO6Ic6k0KXCXbf6qi0bMIyL\nRZr8DS61mD+SPSO2QKEL647xxyW2D8YGtwN6Cc6MpWETsWJkNtS8t7tDL//PceYp\no5LiqKgn0TXj0Pq8Lvb7rqpH8QJ2EVm14SK+elhKZW/Bi5ZOEwfL8pw6EHI4us6V\nxCNQ099dksu++kbdD7zxqEKnk/4zOttYt0whlVrxzkibTjlKdlSlTYpIstU+fNyY\nVE0xWvrn+yF7jVlEwZYOFGfZbpELadrdOr2k1hvAk7upkrpKmLqYfwqD/xPcqwtx\n0iS6AEnmkSiTcAvju5vLkoLFRU7Of4AZ2wIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://localhost:8080/users/weed_lord420#main-key"} +{"type":"account","id":"01F8MH17FWEB39HZJ76B6VXSKF","createdAt":"2021-09-05T10:00:53.985641Z","username":"admin","locked":true,"language":"en","uri":"http://localhost:8080/users/admin","url":"http://localhost:8080/@admin","inboxURI":"http://localhost:8080/users/admin/inbox","outboxURI":"http://localhost:8080/users/admin/outbox","followingUri":"http://localhost:8080/users/admin/following","followersUri":"http://localhost:8080/users/admin/followers","featuredCollectionUri":"http://localhost:8080/users/admin/collections/featured","actorType":"Person","privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxr2e1pqfLwwUCwHUdx56Mxnq5Kzc2EBwqN6jIPjiqVaG5eVq\nhujDhdqwMq0hnpBSPzLnvjiOtEh7Bwhx0MjuC/GRPTM9oNWPYD4PcjX5ofrubyLR\nBI97qD0SbyzUWzeyBi6R5tpW8LK1MJXNbnYlz5WouEiC4mY77ulri0EN2hCq80wg\nfvtEjEvELcKBqIytKH3rutIzfAyqXD7LSQ8UDoNh9GHyIfq8Zj32gWVk2MiPI3+G\n8kQJDmD8CKEasnrGVdSJBQUg3xDAtOibPXLP+07AIsKYMon35hVNvQNQPS7ru/Bk\nRhhGp2R44zqj6L9mxYbSrhFAaKDedu8oVe1aLQIDAQABAoIBAGK0aIADOU4ffJDe\n7sveiih5Fc1PATwx/QIR2QkWM1SREdx6LYclcX44V8xDanAbE44p1SkHY/CsEtYy\nXnyoXnn2FwFDQrdveY7+I6PApOPLAcKWkyLltC+hbVdj92/6YGNrm7EA/a77wruH\nmwjiivLnTG2CLecNiXSl33DA9YU4Yz+2Tza3IpTdjt8c/dz/BKKaxaWV+i9ew5VR\nioo5v51B+J8PrneCM/p8LGiLV148Njr0JqV6eFy1JuzItYMYdc3Fp+YnMzsuMZEA\n1akMcoln/ucVJyOFnCn6jx47nIoPZLl1KxX3aRDRfvrejm6W4yAkkTmR5voSRqax\njPL3rI0CgYEA9Acu4TO8xJ3uGaUad0N9JTYQVSmtAaE/g+df9LGMSzoj8X95S4xE\nQsGPqNGDm2VWADJjK4P05twZ+LfsfSKQ86wbp4/gbgnXpqB1P5Lty/B7KxiTnNwt\nwb1WGWTCukxfUSL3PRyf8uylkrg72RxKiBx4zKO3WVSLWOZWrFtn0qMCgYEA0H2p\nJs9Nv20ADOOX5tQ7+ruS6/B/Fhyj5fhflSYCAtOW7aME7+zQKJyqSQZ4b2Aub3Tp\nGIaUbRIGzjHyuTultFFWvjU3H5aI/0g1G9WKaBhNkyTIYVmMKtYyhXNvouWing8x\noraWx8TTBP8Cdnnk+QgdR2fpug8cghKupp5wvO8CgYA1JFtRL7MsHjh73TimQExA\njkWARlMmx7bNQtXis8eZmk+5h8kiaqly4DQoz3eZn7fa0x5Fm7b5j3UYdPVLSvvG\nFPTwyKRXUk1kPA1MivK+NuCbwf5jao+MYW8emJLPf1JCmRq+dD1g6aglC3n9Dewt\nOAYWipCjI4Y1FfRKFJ3HgQKBgEAb47+DTyzln3ZXJYZdDHR06SCTuwBZnixAy2NZ\nZJTp6yb3UbVU5E0Yn2QFEVNuB9lN4b8g4tMHEACnazN6G+HugPXL9z9HUqjs0yfT\n6dNIZdIxJUyJ9IfXhYFzlYhJhE+F7IVUD9kttJV8tI0pvja1QAuM8Fm9+84jYIDr\nh08RAoGAMYbjKHbtejcHBwt1kIcSss0cDmlZbBleJo8tdmdg4ndf5GE9N4/EL7tq\nm2zYSfr7OVdnOwRhoO+xF/6d1L7+TR1wz+k2fuMsI71aM5Ocp1nYTutjIkBTcldZ\nZzvjOgZWng5icuRLQQiDSKG5uqazqL/xGXkijb4kp4WW6myWY3c=\n-----END RSA PRIVATE KEY-----\n","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAxr2e1pqfLwwUCwHUdx56Mxnq5Kzc2EBwqN6jIPjiqVaG5eVqhujD\nhdqwMq0hnpBSPzLnvjiOtEh7Bwhx0MjuC/GRPTM9oNWPYD4PcjX5ofrubyLRBI97\nqD0SbyzUWzeyBi6R5tpW8LK1MJXNbnYlz5WouEiC4mY77ulri0EN2hCq80wgfvtE\njEvELcKBqIytKH3rutIzfAyqXD7LSQ8UDoNh9GHyIfq8Zj32gWVk2MiPI3+G8kQJ\nDmD8CKEasnrGVdSJBQUg3xDAtOibPXLP+07AIsKYMon35hVNvQNQPS7ru/BkRhhG\np2R44zqj6L9mxYbSrhFAaKDedu8oVe1aLQIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://localhost:8080/users/admin#main-key"} +{"type":"account","id":"01F8MH1H7YV1Z7D2C8K2730QBF","createdAt":"2021-09-06T10:00:53.985643Z","username":"the_mighty_zork","locked":true,"language":"en","uri":"http://localhost:8080/users/the_mighty_zork","url":"http://localhost:8080/@the_mighty_zork","inboxURI":"http://localhost:8080/users/the_mighty_zork/inbox","outboxURI":"http://localhost:8080/users/the_mighty_zork/outbox","followingUri":"http://localhost:8080/users/the_mighty_zork/following","followersUri":"http://localhost:8080/users/the_mighty_zork/followers","featuredCollectionUri":"http://localhost:8080/users/the_mighty_zork/collections/featured","actorType":"Person","privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEApBmF8U+or+E0mgUMH3LE4uRIWzeV9rhYnvSMm9OpOsxwJiss\n5mEA/NtPHvQlq2UwrqXX89Wvu94K9EzZ4VyWYQGdxaiPpt17vRqUfsHUnXkY0pvC\nC9zt/aNlJtdt2xm+7PTC0YQd4+E1FX3aaoUPJL8MXzNlpJzaUtuwLZe1iBmFfatZ\nFHptEgc4nlf6TNLTzj3Yw1/7zIGVS8Vi7VquHc0Xo8dRiL2RxCGzLWnwL6GlrxY1\ntMhsUg467XeoiwegFCpcIhAhPFREKoTnCEksL/N0rpXl7m6CAy5uqBGs5mMXnXlq\nefr58l0j2dU6zc60LCHH9TJC+roXsKJhy9sx/QIDAQABAoIBAFa+UypbFG1cW2Tr\nNBxPm7ngOEtXl8MicV4dIVKh0TwOo13ZxtNFBbOj7jALmPn/9HrtmbkABPQHDL1U\n/nt9aNSAeTjpwH3RaD5vFX3n0g8n2zJBOZLxxzAjNi4RBLYj5uP1AiKkdvRlsJza\nuSFDkty2zMBqN9mLPHE+RePj5Qa6tjYfIQqQzu/+YnYMlXHoC2yHNKsvz6S5FhVj\nv5zATv2JlJQH3RSmhuPOah73iQnKCLzYYEAHleawKrCg/rZ3ht37Guvabeq7MqQN\nvi9pJdAA+RMxPsboHajskePjOTYJgKQSxEAMRTMfBR40aZxklxQL0EoBd1Y3CHXh\nfMg0xWECgYEA0ORrpJ1A2WNQwKcDDeBBsaJqWF4EraoFzYrugKZrAYEeVyuGD0zq\nARUaWkZTZ1f6wQ10i1WxAuKlBEds7QsLdZzLsA4um4JlBroCZiYfPnmTtb8op1LY\nFqeYTByvAmnfWWTuOI67GX9ruLg8tEGuz38kuQVSxYs51its3tScNPUCgYEAyRst\nwRbqpOqnwoRoS6pxv0Vpc3nUcfaVYwsg/qobJkiwAdlUYeE7alvEY926VW4cvU/X\nhy3L1punAqnyLI7uuqCefXEbNxO0Cebyy4Kv2Ye1uzl0OHsJczSNdfpNqfAIKwtN\nHLCYDGCsluQhz+I/5Pd0dT+JDPPW9hKS2HG7o+kCgYBqugn1VRLo/sEnbS02TbnC\n1ESZWY/yWsgUOEObH2vUnO+vgeFAt/9nBi0sqnm6d0z6jbFZ7zI9UycUhJm2ksoM\nEUxQay6M7ZZIVYkcP6X++YbqePyAYOdey8oYOR+BkC45MkQ0SVh2so+LFTaOsnBq\nO3+7uGiN3ZBzSESbpO0acQKBgQCONrsXZeZO82XpB4tdns3LbgGRWKEkajTgEnml\nvZNvck2NMSwb/5PttbFe0ei4CyMluPV4MamJPQ9Qse+BFR67OWR63uZY/4T8z6X4\nxpUmZnLcUFfgrRlUr+AtgvEy8HxGPDquxC7x6deC6RcEFEIM3/UqCOEZGMJ1x1Ky\n31LLKQKBgGCKwVgQ8+4JyHZFkie3YdHhxJDokgY+Opb0HNnoBY/lZ54UMCCJQPS2\n0XPSu651j/3adr3RQneU04gF6U2/D5JzFEV0kUsqZ4Zy2EEU0LU4ibus0gyomSpK\niWhU4QrC/M4ELxYZinlNu3ThPWNQ/PMNteVWfdgOcV7uUWl0ViFp\n-----END RSA PRIVATE KEY-----\n","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEApBmF8U+or+E0mgUMH3LE4uRIWzeV9rhYnvSMm9OpOsxwJiss5mEA\n/NtPHvQlq2UwrqXX89Wvu94K9EzZ4VyWYQGdxaiPpt17vRqUfsHUnXkY0pvCC9zt\n/aNlJtdt2xm+7PTC0YQd4+E1FX3aaoUPJL8MXzNlpJzaUtuwLZe1iBmFfatZFHpt\nEgc4nlf6TNLTzj3Yw1/7zIGVS8Vi7VquHc0Xo8dRiL2RxCGzLWnwL6GlrxY1tMhs\nUg467XeoiwegFCpcIhAhPFREKoTnCEksL/N0rpXl7m6CAy5uqBGs5mMXnXlqefr5\n8l0j2dU6zc60LCHH9TJC+roXsKJhy9sx/QIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://localhost:8080/users/the_mighty_zork#main-key"} +{"type":"block","id":"01FEXXET6XXMF7G2V3ASZP3YQW","createdAt":"2021-09-08T09:00:53.965362Z","uri":"http://localhost:8080/users/1happyturtle/blocks/01FEXXET6XXMF7G2V3ASZP3YQW","accountId":"01F8MH5NBDF2MV7CTC4Q5128HF","targetAccountId":"01F8MH5ZK5VRH73AKHQM6Y9VNX"} +{"type":"account","id":"01F8MH5ZK5VRH73AKHQM6Y9VNX","createdAt":"2021-08-31T12:00:53.985646Z","username":"foss_satan","domain":"fossbros-anonymous.io","locked":true,"language":"en","uri":"http://fossbros-anonymous.io/users/foss_satan","url":"http://fossbros-anonymous.io/@foss_satan","inboxURI":"http://fossbros-anonymous.io/users/foss_satan/inbox","outboxURI":"http://fossbros-anonymous.io/users/foss_satan/outbox","followingUri":"http://fossbros-anonymous.io/users/foss_satan/following","followersUri":"http://fossbros-anonymous.io/users/foss_satan/followers","featuredCollectionUri":"http://fossbros-anonymous.io/users/foss_satan/collections/featured","actorType":"Person","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEA2OyVgkaIL9VohXKYTh319j4OouHRX/8QC7piXj71k7q5RDzEyvis\nVZBc5/C1/crCpxt895i0Ai2CiXQx+dISV7s/JBhAGl8s7TQ8jLlMuptrI0+sdkBC\nlu8pU0qQmoeXVnlquOzNmqGufUxIDtLXLZDN17qf/7vWA23q4d0tG5KQhGGGKiVM\n61Ufvr9MmgPBSpyUvYMAulFlz1264L49aGWeVgOz3qUQzqtxjrP0kaIbeyt56miP\nKr5AqkRgSsXci+FAo6suxR5gzo9NgleNkbZWF9MQyKlawukPwZUDSh396vtNQMee\n/4mto7mAXw8iio0IacrYO3F7iyewXnmI/QIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://fossbros-anonymous.io/users/foss_satan/main-key"} +{"type":"follow","id":"01F8PYDCE8XE23GRE5DPZJDZDP","createdAt":"2021-09-08T09:00:54.749465Z","uri":"http://localhost:8080/users/the_mighty_zork/follow/01F8PYDCE8XE23GRE5DPZJDZDP","accountId":"01F8MH1H7YV1Z7D2C8K2730QBF","targetAccountId":"01F8MH5NBDF2MV7CTC4Q5128HF"} +{"type":"follow","id":"01F8PY8RHWRQZV038T4E8T9YK8","createdAt":"2021-09-06T12:00:54.749459Z","uri":"http://localhost:8080/users/the_mighty_zork/follow/01F8PY8RHWRQZV038T4E8T9YK8","accountId":"01F8MH1H7YV1Z7D2C8K2730QBF","targetAccountId":"01F8MH17FWEB39HZJ76B6VXSKF"} +{"type":"domainBlock","id":"01FF22EQM7X8E3RX1XGPN7S87D","createdAt":"2021-09-08T10:00:53.968971Z","domain":"replyguys.com","createdByAccountID":"01F8MH17FWEB39HZJ76B6VXSKF","privateComment":"i blocked this domain because they keep replying with pushy + unwarranted linux advice","publicComment":"reply-guying to tech posts","obfuscate":false} +{"type":"user","id":"01F8MGYG9E893WRHW0TAEXR8GJ","createdAt":"2021-09-08T10:00:53.97247Z","accountID":"01F8MH0BBE4FHXPH513MBVFHB0","encryptedPassword":"$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS","locale":"en","lastEmailedAt":"0001-01-01T00:00:00Z","confirmationToken":"a5a280bd-34be-44a3-8330-a57eaf61b8dd","confirmationTokenSentAt":"2021-09-08T10:00:53.972472Z","unconfirmedEmail":"weed_lord420@example.org","moderator":false,"admin":false,"disabled":false,"approved":false} +{"type":"user","id":"01F8MGWYWKVKS3VS8DV1AMYPGE","createdAt":"2021-09-05T10:00:53.972475Z","email":"admin@example.org","accountID":"01F8MH17FWEB39HZJ76B6VXSKF","encryptedPassword":"$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS","currentSignInAt":"2021-09-08T09:50:53.972477Z","lastSignInAt":"2021-09-08T08:00:53.972477Z","chosenLanguages":["en"],"locale":"en","lastEmailedAt":"2021-09-08T09:30:53.972478Z","confirmedAt":"2021-09-05T10:00:53.972478Z","moderator":true,"admin":true,"disabled":false,"approved":true} +{"type":"user","id":"01F8MGVGPHQ2D3P3X0454H54Z5","createdAt":"2021-09-06T22:00:53.97248Z","email":"zork@example.org","accountID":"01F8MH1H7YV1Z7D2C8K2730QBF","encryptedPassword":"$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS","currentSignInAt":"2021-09-08T09:30:53.972481Z","lastSignInAt":"2021-09-08T08:00:53.972481Z","chosenLanguages":["en"],"locale":"en","lastEmailedAt":"2021-09-08T09:05:53.972482Z","confirmationTokenSentAt":"2021-09-06T22:00:53.972483Z","confirmedAt":"2021-09-07T00:00:53.972482Z","moderator":false,"admin":false,"disabled":false,"approved":true} +{"type":"user","id":"01F8MH1VYJAE00TVVGMM5JNJ8X","createdAt":"2021-09-06T22:00:53.972485Z","email":"tortle.dude@example.org","accountID":"01F8MH5NBDF2MV7CTC4Q5128HF","encryptedPassword":"$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS","currentSignInAt":"2021-09-08T09:30:53.972485Z","lastSignInAt":"2021-09-08T08:00:53.972486Z","chosenLanguages":["en"],"locale":"en","lastEmailedAt":"2021-09-08T09:05:53.972487Z","confirmationTokenSentAt":"2021-09-06T22:00:53.972487Z","confirmedAt":"2021-09-07T00:00:53.972487Z","moderator":false,"admin":false,"disabled":false,"approved":true} +{"type":"instance","id":"01BZDDRPAB8J645ABY31HHF68Y","createdAt":"2021-09-08T10:00:54.763912Z","domain":"localhost:8080","title":"localhost:8080","uri":"http://localhost:8080","reputation":0} +``` + +### gotosocial admin import + +此命令可用于将文件中的数据导入到你的 GoToSocial 数据库中。 + +如果数据库中尚未存在 GoToSocial 表,它们将被创建。 + +如果在导入过程中出现任何冲突(例如尝试导入特定帐户时已经存在),则进程将中止。 + +文件格式应为一系列以换行符分隔的 JSON 对象(参见上文)。 + +`gotosocial admin import --help`: + +```text +从文件中导入数据到数据库 + +用法: + gotosocial admin import [选项] + +选项: + -h, --help 导入命令的帮助信息 + --path string 要导入/导出文件的路径 +``` + +示例: + +```bash +gotosocial admin import --path example.json --config-path config.yaml +``` + +### gotosocial admin media list-attachments + +可用于列出实例上的本站、外站或所有媒体附件的存储路径(包括头像和头图)。 + +`local-only` 和 `remote-only` 可用作过滤器;它们不能同时被设置。 + +如果既未设置 `local-only` 也未设置 `remote-only`,则将列出实例上的所有媒体附件。 + +你可能希望在运行此命令时将 `GTS_LOG_LEVEL` 设置为 `warn` 或 `error`,否则会记录大量你可能不需要的信息日志。 + +`gotosocial admin media list-attachments --help`: + +```text +列出本站、外站或所有附件 + +用法: + gotosocial admin media list-attachments [选项] + +选项: + -h, --help list-attachments 命令的帮助信息 + --local-only 仅列出本站附件/表情;如果指定,则 remote-only 不能为 true + --remote-only 仅列出外站附件/表情;如果指定,则 local-only 不能为 true +``` + +示例输出: + +```text +/gotosocial/062G5WYKY35KKD12EMSM3F8PJ8/attachment/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg +/gotosocial/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg +/gotosocial/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg +/gotosocial/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg +/gotosocial/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH7TDVANYKWVE8VVKFPJTJ.gif +/gotosocial/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg +/gotosocial/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH58A357CV5K7R7TJMSH6S.jpg +/gotosocial/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01CDR64G398ADCHXK08WWTHEZ5.gif +``` + +### gotosocial admin media list-emojis + +用于列出您实例上的本站、外站或所有表情符号的存储路径。 + +`local-only` 和 `remote-only` 可用作过滤器;它们不能同时设置。 + +如果未设置 `local-only` 或 `remote-only`,将列出您实例上的所有表情符号。 + +您可能需要在运行时将 `GTS_LOG_LEVEL` 设置为 `warn` 或 `error`,否则将记录许多您可能不需要的信息消息。 + +`gotosocial admin media list-emojis --help`: + +```text +列出本站、外站或所有表情符号 + +用法: + gotosocial admin media list-emojis [标志] + +标志: + -h, --help 获取 list-emojis 帮助信息 + --local-only 仅列出本站附件/表情符号;如果指定,则 remote-only 不能为真 + --remote-only 仅列出外站附件/表情符号;如果指定,则 local-only 不能为真 +``` + +示例输出: + +```text +/gotosocial/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01GD5KP5CQEE1R3X43Y1EHS2CW.png +/gotosocial/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png +``` + +### gotosocial admin media prune orphaned + +此命令可用于删除您 GoToSocial 中的孤立媒体。 + +孤立媒体定义为存储中使用 GoToSocial 格式的键存在,但没有相应数据库条目的媒体。这对于删除可能在以前安装中遗留的文件,或错误放置在存储中的文件非常有用。 + +!!! warning "需要停止服务器" + + 此命令仅在 GoToSocial 未运行时起作用,因为它需要获取存储的独占锁。 + + 在运行此命令之前,请先停止 GoToSocial! + +```text +删除存储中的孤立媒体 + +用法: + gotosocial admin media prune orphaned [标志] + +标志: + --dry-run 执行试运行,仅记录可删除项目的数量(默认值为 true) + -h, --help 获取 orphaned 帮助信息 +``` + +默认情况下,此命令执行试运行,将记录可以删除的项目数量。要真正执行删除,请在命令中添加 `--dry-run=false`。 + +示例(试运行): + +```bash +gotosocial admin media prune orphaned +``` + +示例(实际执行): + +```bash +gotosocial admin media prune orphaned --dry-run=false +``` + +### gotosocial admin media prune remote + +此命令可用于删除您 GoToSocial 中未使用/过时的外站媒体。 + +过时媒体是指外站实例中早于 `media-remote-cache-days` 的头像/头图/状态附件。 + +如果需要,这些项目将会在之后按需重新获取。 + +未使用媒体是指当前账号或状态未使用的头像/头图/状态附件。 + +!!! warning "需要停止服务器" + + 此命令仅在 GoToSocial 未运行时起作用,因为它需要获取存储的独占锁。 + + 在运行此命令之前,请先停止 GoToSocial! + +```text +从存储中删除未使用/过时的外站媒体,时间早于指定天数 + +用法: + gotosocial admin media prune remote [标志] + +标志: + --dry-run 执行试运行,仅记录可删除项目的数量(默认值为 true) + -h, --help 获取 remote 帮助信息 +``` + +默认情况下,此命令执行试运行,将记录可以删除的项目数量。要真正执行删除,请在命令中添加 `--dry-run=false`。 + +示例(试运行): + +```bash +gotosocial admin media prune remote +``` + +示例(实际执行): + +```bash +gotosocial admin media prune remote --dry-run=false +``` diff --git a/docs/locales/zh/admin/database_maintenance.md b/docs/locales/zh/admin/database_maintenance.md new file mode 100644 index 000000000..36b0159bb --- /dev/null +++ b/docs/locales/zh/admin/database_maintenance.md @@ -0,0 +1,55 @@ +# 数据库维护 + +无论你选择使用 SQLite 还是 Postgres 来运行 GoToSocial,可能都需要偶尔执行一些维护工作,以保持数据库的良好运作。 + +!!! tip + + 尽管此处提供的维护建议旨在不破坏现有数据,你还是应该在手动执行维护操作之前备份数据库。这样,如果输入错误或意外运行了不当命令,可以恢复备份并重试。 + +!!! danger + + **强烈不建议**手动创建、删除或更新 GoToSocial 数据库中的条目,这里不会提供相关命令。即使你认为自己知道在做什么,运行 `DELETE` 等语句可能会引入非常难以排查的问题。以下维护建议旨在帮助你的实例平稳运行;如果你手动进入数据库并对条目、表和索引进行修改,它们不会拯救你的数据。 + +## SQLite + +要进行手动 SQLite 维护,你首先应该在存储 GoToSocial sqlite.db 文件的机器上安装 SQLite 命令行工具 `sqlite3`。有关 `sqlite3` 的详细信息,请参见[此处](https://sqlite.org/cli.html)。 + +### 分析/优化 + +按照 [SQLite 最佳实践](https://sqlite.org/lang_analyze.html#recommended_usage_pattern),GoToSocial 在关闭数据库连接时运行 `optimize` SQLite pragma,`analysis_limit=1000`,以保持索引信息的更新。 + +在每次数据库迁移后(例如,启动新版本的 GoToSocial 时),GoToSocial 将运行 `ANALYZE`,以确保查询计划器正确考虑迁移新增或删除的索引。 + +`ANALYZE` 命令可能需要大约 10 分钟,具体时间取决于硬件和数据库文件的大小。 + +由于上述自动化步骤,正常情况下你不需要针对 SQLite 数据库文件手动运行 `ANALYZE` 命令。 + +然而,如果你中断了之前的 `ANALYZE` 命令,并发现查询运行缓慢,可能是因为 SQLite 内部表中存储的索引元数据已被删除或不当修改。 + +如果是这种情况,可以尝试手动运行完整的 `ANALYZE` 命令,步骤如下: + +1. 停止 GoToSocial。 +2. 在 `sqlite3` shell 中连接到你的 GoToSocial 数据库文件,运行 `PRAGMA analysis_limit=0; ANALYZE;`(这可能需要几分钟)。 +3. 启动 GoToSocial。 + +[查看更多信息](https://sqlite.org/lang_analyze.html#approximate_analyze_for_large_databases). + +### 清理(Vacuum) + +GoToSocial 当前未启用 SQLite 的自动清理(auto-vacuum)。要将数据库文件重新打包到最佳大小,你可能需要定期(例如每几个月)在 SQLite 数据库上运行 `VACUUM` 命令。 + +可以在[此处](https://sqlite.org/lang_vacuum.html)查看有关 `VACUUM` 命令的详细信息。 + +基本步骤如下: + +1. 停止 GoToSocial。 +2. 在 `sqlite3` shell 中连接到你的 GoToSocial 数据库文件,运行 `VACUUM;`(这可能需要几分钟)。 +3. 启动 GoToSocial。 + +### 副本 + +为数据库设置副本等保护措施是常见做法。SQLite 可以使用外部软件进行副本创建。基本步骤描述在 [配置 SQLite 副本](../advanced/replicating-sqlite.md) 页面。 + +## Postgres + +待完成:Postgres 的维护建议。 diff --git a/docs/locales/zh/admin/domain_blocks.md b/docs/locales/zh/admin/domain_blocks.md new file mode 100644 index 000000000..00be4d3f8 --- /dev/null +++ b/docs/locales/zh/admin/domain_blocks.md @@ -0,0 +1,73 @@ +# 域名屏蔽 + +GoToSocial 支持屏蔽/封禁那些你不想与你的实例联合的域名。在我们的文档中,“屏蔽”和“封禁”这两个术语在涉及域名时可以互换使用,因为它们的意思相同:屏蔽你的实例与目标域名上的实例相互通信,有效地切断两个实例之间的联合。 + +你可以使用[实例管理面板](./settings.md#联合)查看、创建和移除域名屏蔽和域名允许。 + +本文档重点说明域名屏蔽实际*作用*是什么,以及创建新域名屏蔽时会产生哪些副作用。 + +## 域名屏蔽如何工作 + +域名屏蔽通过两种方式工作: + +首先,它指示你的实例拒绝来自目标域名的任何请求: + +- 从被屏蔽域名到你的实例的所有传入请求将以 HTTP 状态码 `403 Forbidden` 响应。 +- 这使目标域名上的帐户无法与你实例上的帐户或该帐户创建的任何贴文进行互动,因为你的实例会简单地拒绝处理请求。 +- 这也延伸到 GET 请求:你的实例将不再对被屏蔽实例的请求提供 ActivityPub 响应,例如获取帐户简介或置顶贴文等。 +- 你的实例上的帐户的贴文转发也不应对被屏蔽的实例可见,因为那些实例将无法获取已转发贴文的内容。 + +其次,域名屏蔽指示你的实例不再向目标实例发出任何请求。这意味着: + +- 你的实例不会向被屏蔽域名上的实例发送任何消息。 +- 也不会从该实例获取贴文、帐户、媒体或表情符号。 + +## 安全顾虑 + +### 屏蔽规避 + +域名屏蔽并不完全严密。GoToSocial *可以* 确保自身既不响应来自被屏蔽域名的请求,也不向这些实例发出请求。不幸的是,它*无法*保证你的实例上的帐户不会以任何方式对被屏蔽实例的用户可见。请考虑以下情况,这些都代表了一种[屏蔽规避](https://en.wikipedia.org/wiki/Block_(Internet)#Evasion): + +- 你屏蔽了 `blocked.instance.org`。`blocked.instance.org` 上的用户在 `not-blocked.domain` 上创建了一个帐户,以便他们可以使用新帐户与你的帖子互动或向你发送消息。他们可能会直接跳脸,告诉你他们是谁,或者使用假身份。 +- 你屏蔽了 `blocked.instance.org`。`not-blocked.domain` 上的用户截屏了你的贴文并将其发送给 `blocked.instance.org` 上的某人。 +- 你屏蔽了 `blocked.instance.org`。`blocked.instance.org` 上的用户通过浏览器访问你的个人资料,以查看你的公开贴文。 +- 你屏蔽了 `blocked.instance.org`。你的个人资料启用了 RSS。`blocked.instance.org` 上的用户订阅了你的 RSS feed 以阅读你的公开贴文。 + +在上述情况下,`blocked.instance.org` 依然被屏蔽,但该实例的用户可能仍有其他方式查看你的贴文并可能联系到你。 + +考虑到这一点,你应始终将域名屏蔽视为隐私保护的*一个层次*。也就是说,域名屏蔽应该与其他层次一起部署,以实现你所满意的隐私水平。这应包括不公开发布敏感信息、不在照片中意外暴露个人信息等。 + +### 屏蔽公告机器人 + +不幸的是,联邦宇宙中有一些恶意用户,他们将域名屏蔽视为敌人而试图打破。为达到此目的,他们通常会针对那些使用域名屏蔽来保护用户的实例。 + +因此,联邦宇宙中有机器人抓取实例域名屏蔽,并向机器人的关注者宣布任何发现的屏蔽,从而使屏蔽实例的管理员可能面临骚扰。这些机器人使用 `api/v1/instance/peers?filter=suspended` 端点来收集域名屏蔽信息。 + +默认情况下,GoToSocial 不会公开此端点,因此你的实例将不会被这种方式抓取。然而,如果你在 config.yaml 文件中将 `instance-expose-suspended` 设置为 `true`,你可能会发现此端点偶尔会被抓取,并且你的屏蔽可能会被恶意机器人宣布。 + +## 创建域名屏蔽的副作用 + +当你创建新的域名屏蔽(或重新提交现有的域名屏蔽)时,你的实例将处理该屏蔽的副作用。这些副作用是: + +1. 将数据库中存储自目标域的所有帐户标记为已封禁,并删除被标记帐户的大多数信息(简介、显示名称、字段等)。 +2. 清除本地帐户与封禁帐户之间的所有互关或单方面关系(关注、被关注、关注请求、收藏等)。 +3. 删除封禁帐户的所有贴文。 +4. 删除封禁帐户及其贴文的所有媒体,包括媒体附件、头像、头图和表情符号。 + +!!! danger + 目前,上述大多数副作用是**不可逆**的。如果你在屏蔽后取消屏蔽一个域名,该域名上的所有帐户将不再被标记为已封禁,并且你将能够再次与他们互动,但所有关系仍将被清除,所有贴文和媒体将被删除。 + + 在屏蔽一个域名之前请仔细考虑。 + +## 屏蔽一个域名及其所有子域 + +当你添加一个新的域名屏蔽时,GoToSocial 也将屏蔽该域名的所有子域。如果你不信任域名所有者,你可以选择屏蔽某些特定子域,或者更一般地屏蔽整个域名。 + +一些例子: + +1. 你屏蔽 `example.org`。这将屏蔽以下域名(非详尽列表):`example.org`,`subdomain.example.org`,`another-subdomain.example.org`,`sub.sub.sub.domain.example.org`。 +2. 你屏蔽 `baddies.example.org`。这将屏蔽以下域名(非详尽列表):`baddies.example.org`,`really-bad.baddies.example.org`。然而,以下域名不会被屏蔽(非详尽列表):`example.org`,`subdomain.example.org`,`not-baddies.example.org`。 + +一个更实际的例子: + +某个家伙拥有域名 `fossbros-anonymous.io`。他们不仅在 `mastodon.fossbros-anonymous.io` 运行 Mastodon 实例,还在 `gts.fossbros-anonymous.io` 运行 GoToSocial 实例,以及在 `akko.fossbros-anonymous.io` 运行 Akkoma 实例。你希望一次性屏蔽他们的所有这些实例(以及他们可能在未来创建的任何实例,例如 `pl.fossbros-anonymous.io` 等)。你可以通过简单地为 `fossbros-anonymous.io` 创建域名屏蔽来实现。子域上的任何实例将无法与你的实例通信。搞定! diff --git a/docs/locales/zh/admin/federation_modes.md b/docs/locales/zh/admin/federation_modes.md new file mode 100644 index 000000000..abe9fa095 --- /dev/null +++ b/docs/locales/zh/admin/federation_modes.md @@ -0,0 +1,62 @@ +# 联合模式 + +GoToSocial 当前提供“黑名单”和“白名单”联合模式,可以通过在 `config.yaml` 中设置 `instance-federation-mode`,或者使用环境变量 `GTS_INSTANCE_FEDERATION_MODE` 来配置。这些模式如下所述。 + +## 黑名单联合模式(默认) + +当 `instance-federation-mode` 设置为 `blocklist` 时,你的实例将与其他实例自由联合,没有限制,但你在设置面板中明确创建的屏蔽的实例除外。 + +当你的实例收到来自不在黑名单内的实例的新请求时,如果请求有效,并且请求者被允许查看所请求的资源(考虑贴文的可见性和任何用户级屏蔽),实例将处理该请求。 + +当你的实例遇到它以前未见过的贴文或账户的提及或公告时,如果该资源的域未通过域屏蔽条目被屏蔽,它将会去获取该资源。 + +!!! info + 黑名单联合模式是 GoToSocial 的默认联合模式。它也是大多数其他 ActivityPub 服务器实现的默认联合模式。 + +## 白名单联合模式 + +!!! warning + 白名单联合模式仍然被认为是“实验性”的,我们正在研究其在实际中的表现。它应该如其名称所示,但可能会在其他地方导致错误或出现边缘情况,我们还不确定! + +当 `instance-federation-mode` 设置为 `allowlist` 时,你的实例将仅与通过设置面板明确设为允许的实例联合,并限制任何未被允许的实例的访问。 + +当你的实例收到来自白名单之外实例的新请求时,它将拒绝处理该请求。如果请求来自白名单中的域名,你的实例将处理该请求(考虑贴文的可见性和任何用户级别的屏蔽)。 + +当你的实例遇到它以前未见过的贴文或账户的提及或公告时,它只会在资源所属域名被明确允许时才去获取资源。 + +!!! tip + 白名单联合模式在你希望仅与选择的“可信”实例联合的情况下非常有用。然而,这会影响发现过程。在黑名单联合模式下,你会通过转发和回复自然地遇到未知实例的贴文和账户,但在白名单联合模式下,这样的偶然发现不会发生。 + + 因此,建议你要么先从黑名单联合模式开始,然后在确定喜欢哪些其他实例后切换到白名单联合模式,要么从白名单联合模式开始,并在首次启动实例后准备好并导入白名单,以便“启动”它。 + +## 结合屏蔽与允许 + +可以同时屏蔽和允许同一个域,结合这两者的效果取决于你的实例当前使用的联合模式。 + +![一个流程图,显示两种不同联合模式如何处理传入的请求。](../public/diagrams/federation_modes.png) + +### 在黑名单模式下 + +如图所示,在黑名单模式下(图的左侧),显式添加允许条目可以用来覆盖域名屏蔽。 + +这在你从其他人处导入黑名单,但导入的黑名单中包含了一些你实际上不想屏蔽的实例时很有用。为了避免屏蔽这些实例,你可以先为这些实例显式创建允许条目。然后,当你导入黑名单时,显式允许的域将不会被屏蔽,并且创建屏蔽所导致的副作用(删除贴文、媒体、关系等)将不会被处理。 + +如果你以后移除对于同时存在屏蔽的域的显式允许,该实例将被屏蔽,并且将处理屏蔽创建的相关影响。 + +相反,如果你为被屏蔽的域添加显式允许,将处理解除屏蔽的相关影响。 + +### 在白名单模式下 + +如图所示,在白名单模式下(图的右侧),显式域名屏蔽条目会优先于显式域名允许条目。在运行白名单模式时,必须满足以下两个条件才能允许一个实例通过: + +1. 实例没有存在对应的显式域名屏蔽。 +2. 实例存在对应的显式域名允许。 + +如果上述任何条件不满足,请求将被拒绝。 + +!!! danger + 结合屏蔽和允许是一项棘手的工作! + + 在导入允许和黑名单时,你应该始终手动审核列表,以确保不会无意中屏蔽你不想屏蔽的实例,因为这可能会有**非常烦人的副作用**,例如移除关注/被关注、贴文等。 + + 有疑问时,请始终首先添加显式允许作为保险策略! diff --git a/docs/locales/zh/admin/media_caching.md b/docs/locales/zh/admin/media_caching.md new file mode 100644 index 000000000..d33e73358 --- /dev/null +++ b/docs/locales/zh/admin/media_caching.md @@ -0,0 +1,57 @@ +# 媒体缓存 + +GoToSocial 使用配置的[存储后端](https://docs.gotosocial.org/zh-cn/latest/configuration/storage/)来存储由本站用户上传到实例的媒体(图像、视频等),并缓存从外站实例联合过来的贴文和个人资料中附带的媒体。 + +由本站用户上传的媒体将会永久保存在存储中(除非其所属的贴文或账户被删除),以便能够随时响应来自外站的请求。 + +另一方面,外站媒体仅会被临时缓存。经过一段时间(见下文)后,它将从存储中移除,以帮助缓解存储空间的使用。通过这种方式被移除缓存的外站媒体,如果再次需要,将自动从外站重新获取。 + +!!! info "为什么要缓存?" + 你可能会认为应该完全不缓存外站媒体,因为它始终可以在原始服务器上获取。为什么不完全放弃缓存,而依赖外站根据需求提供服务呢? + + 虽然这是节省存储空间的一种简单方法,但它可能会引发其他问题,并且通常被认为是不够礼貌的做法。 + + 例如,假设某个小实例的用户发布了一条带有图片的有趣贴文。该贴文被一个拥有1000名跨5个不同实例(每个实例200人)关注者的账号转发。这1000人便会同时在时间线上看到这个图片。 + + 如果没有外站媒体缓存,可能会导致多达1000个请求同时冲击小实例,因为每个接收者的浏览器必须单独请求从小实例获取该图片。这会导致小实例的流量激增。在极端情况下,可能导致实例无响应或崩溃,本质上是对其进行分布式拒绝服务攻击(DDOS)。 + + 然而,通过启用外站媒体缓存,将一条贴文转发给1000名来自5个不同实例的用户仅会向小实例发出5个请求:每个实例1个请求。然后,每个实例会从缓存的外站图片版本为其本站用户提供200个请求,有效地分散了负载,保护了较小的实例。 + +## 清理 + +外站媒体缓存的清理是一个计划的后台进程,管理员无需手动干预。根据服务器速度、配置的存储速度和待处理的媒体数量,清理时间大约在5到30分钟之间。 + +GoToSocial 提供了三个变量,让你(管理员)可以调节何时以及如何进行这些操作:`media-remote-cache-days`、`media-cleanup-from` 和 `media-cleanup-every`。 + +默认情况下,这些变量设置如下: + +| 变量名称 | 默认值 | 含义 | +|-----------------------------|--------------|----------| +| `media-remote-cache-days` | `7` | 7天 | +| `media-cleanup-from` | `"00:00"` | 午夜 | +| `media-cleanup-every` | `"24h"` | 每日 | + +换句话说,默认设置意味着每晚午夜,超过一周的外站媒体将被清除并从存储中移除。 + +你可以通过调节这些变量实现不同的效果。例如,如果你希望在凌晨4:30而不是午夜进行清理,你可以将 `media-cleanup-from` 改为 `"04:30"`。 + +如果你只想每隔几天而不是每晚进行清理,可以将 `media-cleanup-every` 设置为更高的值,如 `"48h"` 或 `"72h"`。 + +如果你想采用更积极的清理策略以尽量减少存储使用,可以设置以下值: + +| 变量名称 | 设置值 | 含义 | +|-----------------------------|--------------|-----------------| +| `media-remote-cache-days` | `1` | 1天 | +| `media-cleanup-from` | `"00:00"` | 午夜 | +| `media-cleanup-every` | `"8h"` | 每8小时 | + +上述设置意味着从午夜开始每8小时,GoToSocial 将清除任何缓存超过1天(24小时)的媒体。清理任务将在 00:00、08:00 和 16:00,即午夜、上午8点和下午4点运行。使用此配置,你可能将外站媒体在存储中保留的最长时间约为32小时。 + +!!! tip + 将 `media-remote-cache-days` 设置为0或更小意味着外站媒体将永不被清除。然而,本站孤立媒体的清理任务和其他一致性检查仍将按其他变量定义的计划运行。 + +!!! tip + 如果你愿意,你也可以通过管理面板手动执行一次性清理操作([查看文档](./settings.md#媒体))。 + +!!! warning + 将 `media-cleanup-every` 设置为非常小的值,如 `"30m"` 或更小,可能会导致你的实例不断遍历附件,导致数据使用率高而效益甚微。我们不建议将该值设置为小于约 `"8h"`,即便如此,可能也显得过度。 diff --git a/docs/locales/zh/admin/request_filtering_modes.md b/docs/locales/zh/admin/request_filtering_modes.md new file mode 100644 index 000000000..435fa9386 --- /dev/null +++ b/docs/locales/zh/admin/request_filtering_modes.md @@ -0,0 +1,31 @@ +# HTTP 请求头过滤模式 + +GoToSocial 当前提供“屏蔽”、“允许”和禁用的 HTTP 请求头过滤模式,可以通过在 config.yaml 中设置 `advanced-header-filter-mode`,或使用环境变量 `GTS_ADVANCED_HEADER_FILTER_MODE` 来配置。这些模式的具体说明如下。 + +!!! warning + HTTP 请求头过滤是一个进阶设置。如果你不熟悉 HTTP 请求头的使用和复杂性,修改这些设置可能会导致联合功能中断,甚至无法访问你自己的实例。 + + HTTP 请求头过滤仍被视为“实验性”功能。它应该能如预期工作,但可能会导致其他地方出现错误或边缘情况,这点我们尚不确定! + +## 禁用请求头过滤模式(默认) + +当 `advanced-header-filter-mode` 设置为 `""`(即空字符串)时,将禁用所有请求头过滤。 + +## 屏蔽过滤模式 + +当 `advanced-header-filter-mode` 设置为 `"block"` 时,你的实例将正常接受 HTTP 请求(需进行 API 令牌检查、HTTP 签名检查等),但会拒绝符合你通过设置面板明确创建的屏蔽头过滤规则的请求。 + +在屏蔽模式中,可以使用允许头过滤规则来覆盖现有的屏蔽过滤规则,以提供更细致的控制。 + +在屏蔽模式下,请求将被接受,前提是该请求被明确允许或未被明确屏蔽。 + +## 允许过滤模式 + +当 `advanced-header-filter-mode` 设置为 `"allow"` 时,你的实例只会接受那些与通过设置面板明确创建的允许头过滤规则相匹配的 HTTP 请求。所有其他请求将被拒绝。 + +在允许模式中,可以使用屏蔽头过滤规则来覆盖现有的允许过滤规则,以提供更细致的控制。 + +在允许模式下,请求只有在被明确允许且未被明确屏蔽的情况下才会被接受。 + +!!! danger + 允许过滤模式是一个极为严格的模式,几乎肯定会阻止许多(合法的)客户端访问你的实例,包括你自己。只有在完全明确你的目标时才应启用此模式。 diff --git a/docs/locales/zh/admin/robots.md b/docs/locales/zh/admin/robots.md new file mode 100644 index 000000000..89a3d4c2d --- /dev/null +++ b/docs/locales/zh/admin/robots.md @@ -0,0 +1,13 @@ +# Robots.txt + +GoToSocial 在主域名上提供一个 `robots.txt` 文件。该文件包含试图屏蔽已知 AI 爬虫的一些规则,以及其他一些索引器。它还包括一些规则,以确保诸如 API 端点之类的内容不会被搜索引擎索引,因为这些内容没有被索引的必要。 + +## AI 爬虫 + +AI 爬虫来自一个[社区维护的仓库][airobots]。目前是手动保持同步的。如果你知道有任何遗漏的爬虫,请给他们提交一个 PR! + +已知有许多 AI 爬虫即便明确匹配其 User-Agent,也会忽略 `robots.txt` 中的条目。这意味着 `robots.txt` 文件并不是确保 AI 爬虫不抓取你的内容的万无一失的方法。 + +如果你想完全屏蔽这些爬虫,需要在反向代理中根据 User-Agent 头进行屏蔽,直到 GoToSocial 能够根据 User-Agent 头过滤请求。 + +[airobots]: https://github.com/ai-robots-txt/ai.robots.txt/ diff --git a/docs/locales/zh/admin/settings.md b/docs/locales/zh/admin/settings.md new file mode 100644 index 000000000..ea300a432 --- /dev/null +++ b/docs/locales/zh/admin/settings.md @@ -0,0 +1,169 @@ +# 管理设置面板 + +GoToSocial 管理设置面板使用 [管理 API](https://docs.gotosocial.org/zh-cn/latest/api/swagger/#operations-tag-admin) 来管理你的实例。它与 [用户设置面板](../user_guide/settings.md) 结合使用,并采用与普通客户端相同的 OAuth 机制(范围:admin)。 + +## 设置管理员账户权限和登录 + +要使用管理设置面板,你的账户必须被提升为管理员: + +```bash +./gotosocial --config-path ./config.yaml admin account promote --username 你的用户名 +``` + +为了使提权生效,可能需要在运行命令后重启你的实例。 + +之后,你可以访问 `https://[your-instance-name.org]/settings`,在登录字段中输入你的域名,然后像使用其他客户端一样登录。现在,你应该可以看到管理设置。 + +## 管理 + +实例管理设置。 + +### 举报 + +![一个展示未解决举报的举报列表。](../public/admin-settings-reports.png) + +举报部分显示来自本站用户或外站(匿名显示,仅显示实例名称,不显示具体用户名)的举报列表。 + +点击举报可以查看其是否已解决(若有理由则显示),更多信息,以及由举报用户选定的被举报贴文列表。你也可以在此视图中将举报标记为已解决,并填写评论。如果该用户来自你的实例,你在此处输入的任何评论都会对创建举报的用户可见。 + +![待处理的举报的详细视图,显示被举报的贴文和举报理由。](../public/admin-settings-report-detail.png) + +点击被举报账户的用户名会在“账户”视图中打开该账户,从而允许你对其执行管理操作。 + +### 账户 + +你可以使用此部分搜索账户并对其执行管理操作。 + +### 联合 + +![已封禁实例列表,有一个字段用于过滤/添加新的屏蔽。下面是批量导入/导出界面的链接](../public/admin-settings-federation.png) + +在联合部分,你可以创建、删除和审核明确的域名屏蔽和域名允许。 + +关于联合设置的更多详细信息,特别是域名允许和域名屏蔽如何结合使用,请参阅 [联合模式部分](./federation_modes.md) 和 [域名屏蔽部分](./domain_blocks.md)。 + +#### 域名屏蔽 + +你可以在搜索字段中输入一个要封禁的域名,这将过滤列表以显示你是否已有该域名的屏蔽条目。 + +点击“封禁”会显示一个表单,允许你添加公开和/或私人评论,并提交以添加屏蔽。添加封禁后,该实例上的所有已知账户将被封禁,并阻止与该被屏蔽实例上的任何用户的新互动。 + +#### 域名允许 + +域名允许部分的工作方式与域名屏蔽部分类似,只是用于明确的域名允许而不是域名屏蔽。 + +#### 批量导入/导出 + +通过联合部分底部的链接(或访问 `/settings/admin/federation/import-export`),你可以批量导入/导出屏蔽列表和允许列表。 + +![导入中包含的域列表,提供选择某些或全部域的方法,更改其域,以及更新子域使用方法。](../public/admin-settings-federation-import-export.png) + +通过输入字段或文件导入列表后,你可以在导入子集之前查看列表中的条目。你还会在使用子域的条目中收到警告,此处还提供一种轻松将其更改为主域的方法。 + +## 管理 + +实例管理设置。 + +### 操作 + +运行一次性管理操作。 + +#### 电子邮件 + +你可以使用此部分向指定的电子邮件地址发送测试邮件,并附加可选的测试信息。 + +#### 媒体 + +你可以使用此部分运行清理外站媒体缓存的操作,可以指定天数。超过指定天数的媒体将从存储中删除(s3 或本地)。以这种方式删除的媒体将未来需要时重新尝试获取。此操作在功能上与自动运行的媒体清理相同。 + +#### 密钥 + +你可以使用此部分使来自特定外站实例的公钥过期/失效。下次你的实例收到使用过期密钥的签名请求时,它将尝试重新获取和存储公钥。 + +### 自定义表情 + +包含在外站贴文中的自定义表情将自动获取,但要在你的帖子中使用它们,必须在你的实例上启用。 + +#### 本站 + +![本站自定义表情部分,显示按类别排序的自定义表情概览。有很多加菲猫表情。](../public/admin-settings-emoji-local.png) + +此部分显示你的实例上启用的所有自定义表情的概览,按类别排序。点击某个表情可显示其详细信息,并提供更改类别或图像的选项,或完全删除它。这里无法更新短代码,你需要自己上传带有新短代码的表情(可以选择删除旧的表情)。 + +在概览下方,你可以在预览表情在贴文中的效果后上传自己的自定义表情。支持 PNG 和(动画)GIF 格式。 + +#### 外站 + +![外站自定义表情部分,显示从输入的贴文中解析的 3 个表情的列表: blobcat、blobfoxbox 和 blobhajmlem。可以选择它们,微调短代码,并在提交复制或删除操作前为其分配类别](../public/admin-settings-emoji-remote.png) + +通过“外站”部分,你可以查找任何外站贴文的链接(前提是该实例未被封禁)。如果使用了任何自定义表情,它们将被列出,这样就提供了一种轻松复制到本站表情的方法(供你自己在贴文中使用),或者也可以禁止它们(从贴文中隐藏)。 + +**注意:**由于 testrig 服务器未进行联合,此功能在开发过程中无法使用(500:内部服务器错误)。 + +### 实例设置 + +![GoToSocial 管理面板的截图,显示了更改实例设置的字段](../public/admin-settings-instance.png) + +在这里,你可以为你的实例设置各种元数据,如显示名称/标题、缩略图、(简短)描述和联系信息。 + +#### 实例外观 + +这些设置主要影响你的实例在网络和他人眼中的显示方式。 + +你的 **实例标题** 将显示在你实例每个网页的顶部,并在 OpenGraph 元标签中出现,所以选择一个能代表你实例氛围的名称。 + +**实例头像** 类似于你实例的吉祥物。它将出现在每个网页顶上的实例标题旁边,并作为浏览器标签、OpenGraph 链接等的预览图像。 + +如果你设置了实例头像,我们强烈建议同时设置 **头像描述**。这将为你设置为头像的图片提供替代文字,帮助屏幕阅读器用户理解图片中描绘的内容。替代文本应保持简短明了。 + +#### 实例描述 + +你可以使用这些字段设置实例的简短和完整描述,并为当前和潜在用户提供实例使用条款。 + +**简短描述** 将显示在实例主页的顶部附近,以及响应 `/api/v1/instance` 查询时显示。 + +可以提供一些精辟的内容,以便访问你的实例的访客对你的实例有一个第一印象。例如: + +> 这是一个 ACG 爱好者的实例! +> +> 不管磕什么都可以来注册。 + +或者: + +> 这是一个单用户实例,只属于我! +> +> 这是我的主页:@your_username + +**完整描述** 将显示在你的实例的 /about 页面上,并在响应 `/api/v1/instance` 查询时显示。 + +你可以用它来提供如下信息: + +- 你的实例的历史、理念、态度和氛围 +- 你实例上的居民倾向于发布的内容类型 +- 如何在你的实例上获得账户(如果可能的话) +- 一个拥有账户的用户列表,希望更容易被找到 + +**使用条款** 框也会出现在你的实例的 /about 页面上,并在响应 `/api/v1/instance` 查询时显示。 + +用它来填写如下内容: + +- 法律术语(版权、GDPR 或相关链接) +- 联合政策 +- 数据政策 +- 账户删除/封禁政策 + +以上所有字段都接受 **markdown** 输入,因此你可以编写合适的列表、代码块、水平线、引用块或任何你喜欢的内容。 + +你也可以使用标准 `@user[@domain]` 格式提及账户。 + +查看 [markdown 速查表](https://markdownguide.offshoot.io/cheat-sheet/) 以了解可以做些什么。 + +### 实例联系信息 + +在此部分中,你可以向访问你实例的用户提供一种方便的方法,以联系你的实例管理员。 + +设置好的联系人账户和/或电子邮件地址的链接将出现在实例的每个网页底部、/about 页面的“联系”部分,以及响应 `/api/v1/instance` 查询时显示。 + +选择的 **联系人用户** 必须是实例上的活跃(未封禁)的管理员和/或站务。 + +如果你是在单用户实例上并将管理员权限授予你的主账户,你只需在此处填写自己的用户名即可;无需为此专门创建管理账户。 diff --git a/docs/locales/zh/admin/signups.md b/docs/locales/zh/admin/signups.md new file mode 100644 index 000000000..7b94a6910 --- /dev/null +++ b/docs/locales/zh/admin/signups.md @@ -0,0 +1,59 @@ +# 新账户注册 + +如果你希望你的实例不仅限于你自己,还可以让其他人注册账户,你可以开放你的实例供新账户注册。 + +注意,作为实例管理员,无论你是否愿意,你都需对在你的实例上发布的内容负责。如果你的实例用户在联合网上骚扰或烦扰他人,可能会导致你的实例名誉受损,并被其他人屏蔽。妥善管理一个社区需要付出努力。因此,你应仔细考虑是否愿意且有能力进行管理,及是否只接受朋友和你非常信任的人注册账户。 + +!!! warning + 为使注册流程正常运作,你的实例应[配置电子邮件发件服务](../configuration/smtp.md)。 + + 如下所述,在注册流程中,会向你(作为管理员/站务)和申请人发送几封邮件,包括要求对方确认邮箱地址的邮件。 + + 如果他们无法收到此邮件(因为你的实例未配置电子邮件发件服务),你将需要通过[使用 CLI 工具](../admin/cli.md#gotosocial-admin-account-confirm)手动确认账户。 + +## 开放注册 + +你可以通过在[配置文件](../configuration/accounts.md)中将变量 `accounts-registration-open` 修改为 `true`,并重启你的 GoToSocial 实例来开放新账户注册。 + +你的实例将会在 `/signup` 端点提供注册表单。例如,`https://your-instance.example.org/signup`。 + +![注册表单,显示电子邮件、密码、用户名和理由字段。](../public/signup-form.png) + +此外,你的实例主页和“关于”页面将更新,以反映注册现已开放。 + +当有人提交新注册申请时,他们会在提供的电子邮件地址收到一封邮件,其中包含一个链接,用于确认该地址确实属于他们。 + +同时,你实例上的管理员和站务会收到一封邮件和一条通知,告知有新的注册申请提交。 + +## 处理注册 + +实例管理员和版主可以通过管理面板中的“账户” -> “待处理”部分来审批或拒绝新注册。 + +![管理员设置面板打开到“账户” -> “待处理”,显示列表中有一个账户。](../public/signup-pending.png) + +如果没有注册申请,以上列表将为空。如果有待处理的注册申请,你可以点击打开账户详情页: + +![新待处理账户详情,提供批准或拒绝注册的选项。](../public/signup-account.png) + +在底部,你会看到批准或拒绝注册的操作选项。 + +如果你**批准**注册,账户将被标记为“已批准”,并会向申请人发送一封邮件,通知其注册已获批准,并提醒他们确认电子邮件地址(如果尚未确认)。如果已经确认,他们就可以登录并开始使用他们的账户。 + +如果你**拒绝**注册,可以选择通知申请人注册被拒,你可以通过勾选“发送邮件”复选框来实现。这将向申请人发送一封简短邮件,告知其被拒。如果需要,还可以添加自定义消息,该消息将添加在邮件底部。你还可以添加仅供其他管理员查看的私人备注。 + +!!! warning + 你可能希望等申请人确认他们的电子邮件地址后再批准注册,以防申请时输入错误或提供不是他们的电子邮件地址。如果他们不能确认电子邮件地址,将无法登录和使用账户。 + +## 注册限制 + +为了避免注册积压过多使管理员和版主不堪重负,GoToSocial 将待处理注册积压限制为 20 个账户。一旦积压中有 20 个账户等待管理员或版主处理,新注册将不能通过表单提交。 + +如果过去 24 小时内已批准 10 个或以上新账户注册,新的注册也将不能通过表单提交,以避免实例规模快速扩张超出管理能力。 + +在这两种情况下,申请人将看到一条错误信息,解释无法提交表单的原因,并邀请他们稍后再试。 + +为了防止垃圾账户,GoToSocial 的账户注册**始终**需要管理员手动批准,并且申请人**始终**需确认其电子邮件地址后才能登录和发布贴文。 + +## 通过邀请注册 + +尚未实现: 在未来的更新中,管理员和版主将能够创建和发送邀请,即使公共注册关闭时也允许创建账户,并可预先批准通过邀请创建的账户,和/或允许其绕过上述注册限制。 diff --git a/docs/locales/zh/admin/spam.md b/docs/locales/zh/admin/spam.md new file mode 100644 index 000000000..7af7062b3 --- /dev/null +++ b/docs/locales/zh/admin/spam.md @@ -0,0 +1,23 @@ +# 骚扰信息过滤 + +为了让管理员在应对来自开放注册实例的骚扰信息时稍微轻松一些,GoToSocial 提供了一个实验性的骚扰信息过滤选项。 + +如果你或你的用户受到骚扰信息的轰炸,可以尝试在 `config.yaml` 中将选项 `instance-federation-spam-filter` 设置为 true。你可以在[实例配置页面](../configuration/instance.md)了解有关使用的启发算法的更多信息。 + +被认为是骚扰信息的消息将不会存储在你的本站实例上,也不会生成通知。 + +!!! warning + 骚扰信息过滤器必然是不完美的工具,因为它们可能会误判一些合法的信息为垃圾,或者确实未能抓住一些*确实*是垃圾的信息。 + + 启用 `instance-federation-spam-filter` 应被视为当联合网络遭遇骚扰信息攻击时的一种“加固”选项。在正常情况下,你可能希望将其关闭,以避免意外过滤掉合法信息。 + +!!! tip + 如果你想检查骚扰信息过滤器捕获了哪些内容(如果有的话),可以在日志中搜索 `looked like spam`。 + + 如果你[将 GoToSocial 作为 systemd 服务运行](../getting_started/installation/metal.md#optional-enable-the-systemd-service),可以使用以下命令: + + ```bash + journalctl -u gotosocial --no-pager | grep 'looked like spam' + ``` + + 如果没有输出,说明过滤器中没有捕获到骚扰信息。否则,你将看到一行或多行日志,其中包含已被过滤并丢弃的贴文链接。 diff --git a/docs/locales/zh/admin/themes.md b/docs/locales/zh/admin/themes.md new file mode 100644 index 000000000..baa79ad8a --- /dev/null +++ b/docs/locales/zh/admin/themes.md @@ -0,0 +1,13 @@ +# 主题 + +你在本站的用户可以从 `web/assets/themes` 目录中的任何 CSS 文件中选择一个主题来装饰他们的个人资料。 + +GoToSocial 自带了一些主题文件,但你可以通过以下方式添加更多: + +1. 在 `web/assets/themes` 中创建一个文件,例如 `new-theme.css`。 +2. (可选)在你的主题文件顶部加入以下注释来给你的主题命名和描述: + ```css + /* + theme-title: 新主题 + theme-description: 这是一个示例主题。 + */ diff --git a/docs/locales/zh/advanced/builds/nowasm.md b/docs/locales/zh/advanced/builds/nowasm.md new file mode 100644 index 000000000..33e6fad22 --- /dev/null +++ b/docs/locales/zh/advanced/builds/nowasm.md @@ -0,0 +1,27 @@ +# 无 Wazero / WASM 版构建 + +!!! danger "不受支持" + 我们不提供对使用本节描述的 `nowasm` 标签构建的 GoToSocial 部署的任何支持。这样的构建在任何情况下都应被视为实验性构建,任何运行时出现的问题与我们无关!请不要在存储库中提交寻求 `nowasm` 构建的调试帮助的相关问题。 + +在[支持的平台](../../getting_started/releases.md#支持的平台)上,GoToSocial 使用 WebAssembly 运行时 [Wazero](https://wazero.io/) 对 `ffmpeg`、`ffprobe` 和 `sqlite3` WebAssembly 二进制文件进行沙盒化,使这些应用程序可以被打包并在 GoToSocial 二进制文件中运行,无需管理员安装和管理任何外部依赖。 + +这使得管理员更容易维护他们的 GoToSocial 实例,因为他们的 GtS 二进制文件完全与系统安装的 `ffmpeg`、`ffprobe` 和 `sqlite` 的更改隔离开来。以这种方式运行 `ffmpeg` 也更安全一些,因为 GoToSocial 将 `ffmpeg` 二进制文件封装在一个非常受限的文件系统中,该系统不允许 `ffmpeg` 二进制文件访问除正在解码和重新编码的文件以外的任何文件。换句话说,在受支持的平台上,GoToSocial 提供了 `ffmpeg` 等的大多数功能,而不存在一些麻烦。 + +然而,并不是所有的平台都能在速度更快的“编译器”模式下运行 Wazero,因此必须使用非常慢(且资源占用大的)“解释器”模式。有关符合性的详细信息,请参考 Wazero 的[此表](https://github.com/tetratelabs/wazero?tab=readme-ov-file#conformance)。 + +“解释器”模式的运行性能非常差,以至于在不是 64 位 Linux 或 64 位 FreeBSD 的平台上运行 GoToSocial 实例是不切实际的,因为所有的内存和 CPU 都被媒体处理消耗殆尽。 + +但是!为了让用户能够运行**实验性、不受支持的 GoToSocial 部署**,我们开放了 `nowasm` 构建标签,该标签可用于编译完全不使用 Wazero 或 WASM 的 GoToSocial 构建。 + +使用 `nowasm` 构建的 GoToSocial 二进制文件将使用 [modernc 版本的 SQLite](https://pkg.go.dev/modernc.org/sqlite) 而不是 WASM 版本,并将在系统上使用 `ffmpeg` 和 `ffprobe` 二进制文件进行媒体处理。 + +要使用 `nowasm` 标签构建 GoToSocial,可以像这样将标签传入我们的便利 `build.sh` 脚本: + +```bash +GO_BUILDTAGS=nowasm ./scripts/build.sh +``` + +要运行以此方式构建的 GoToSocial 版本,你必须确保在主机上安装了 `ffmpeg` 和 `ffprobe`。这通常只需运行类似 `doas -u root pkg_add ffmpeg`(OpenBSD)或 `sudo apt install ffmpeg`(Debian 等)的命令即可。 + +!!! danger "确实不受支持" + 再次强调,如果在你的操作系统/架构组合上运行 `nowasm` 构建的 GoToSocial 有效,那很好,但我们不会为这样的构建提供支持,也无法帮助调试为何某些功能不起作用。 diff --git a/docs/locales/zh/advanced/caching/api.md b/docs/locales/zh/advanced/caching/api.md new file mode 100644 index 000000000..56117e5cd --- /dev/null +++ b/docs/locales/zh/advanced/caching/api.md @@ -0,0 +1,84 @@ +# 缓存 API 响应 + +可以缓存某些 API 响应,以减少 GoToSocial 处理所有请求的负担。我们不建议缓存 `/api` 下请求的响应。 + +在使用[分域](../host-account-domain.md)部署方式时,你需要确保在主机域上配置缓存。账号域应仅发出重定向到主机域的指令,客户端会自动记住这些指令。 + +## 端点 + +### Webfinger 和 hostmeta + +对 `/.well-known/webfinger` 和 `/.well-known/host-meta` 的请求可以安全地缓存。注意确保任何缓存策略都考虑到 webfinger 请求的查询参数,因为对该端点的请求形式为 `?resource=acct:@username@domain.tld`。 + +### 公钥 + +许多实现将定期请求用户的公钥,以验证收到消息的签名。这将在消息联合的过程中发生。这些密钥是长期存在的,因此可以用长时间缓存。 + +## 配置代码片段 + +=== "nginx" + +请先在 nginx 中配置一个缓存区。该缓存区必须在 `http` 节内创建,而非 `server` 或 `location` 内。 + +```nginx +http { + ... + proxy_cache_path /var/cache/nginx keys_zone=gotosocial_ap_public_responses:10m inactive=1w; +} +``` + +这配置了一个 10MB 的缓存,其条目将在一周内未被访问时保留。 + +该区域命名为 `gotosocial_ap_public_responses`,你可以自行更改名称。10MB 可以容纳大量缓存键;在小实例上可以使用更小的值。 + +其次,我们需要更新 GoToSocial 的 nginx 配置,以便真正使用我们想要缓存的端点的缓存。 + +```nginx +server { + server_name social.example.org; + + location ~ /.well-known/(webfinger|host-meta)$ { + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_cache gotosocial_ap_public_responses; + proxy_cache_background_update on; + proxy_cache_key $scheme://$host$uri$is_args$query_string; + proxy_cache_valid 200 10m; + proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504 http_429; + proxy_cache_lock on; + add_header X-Cache-Status $upstream_cache_status; + + proxy_pass http://localhost:8080; + } + + location ~ ^\/users\/(?:[a-z0-9_\.]+)\/main-key$ { + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_cache gotosocial_ap_public_responses; + proxy_cache_background_update on; + proxy_cache_key $scheme://$host$uri; + proxy_cache_valid 200 604800s; + proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504 http_429; + proxy_cache_lock on; + add_header X-Cache-Status $upstream_cache_status; + + proxy_pass http://localhost:8080; + } +} +``` + +`proxy_pass` 和 `proxy_set_header` 大致相同,但 `proxy_cache*` 条目需要一些说明: + +- `proxy_cache gotosocial_ap_public_responses` 告诉 nginx 使用我们之前创建的 `gotosocial_ap_public_responses` 缓存区。如果你用的是其他名称,需要更改此值。 +- `proxy_cache_background_update on` 表示 nginx 会尝试在后台刷新即将过期的缓存资源,以确保磁盘上有最新副本。 +- `proxy_cache_key` 的配置确保缓存时考虑到查询字符串。所以请求 `.well-known/webfinger?acct=user1@example.org` 和 `.well-known/webfinger?acct=user2@example.org` 被视为不同请求。 +- `proxy_cache_valid 200 10m;` 意味着我们只缓存来自 GTS 的 200 响应,时间为 10 分钟。你可以添加类似 `proxy_cache_valid 404 1m;` 的其他行,来缓存 404 响应 1 分钟。 +- `proxy_cache_use_stale` 告诉 nginx 允许在某些情况下使用过期的缓存条目(超过 10 分钟)。 +- `proxy_cache_lock on` 表示如果资源未缓存且有多个并发请求,则查询将排队,以便只有一个请求通过,其他请求则从缓存中获取答案。 +- `add_header X-Cache-Status $upstream_cache_status` 将 `X-Cache-Status` 头添加到响应中,以便你可以检查是否正在缓存。你可以删除此项。 + +上述配置将在代理到 GoToSocial 时出错、连接到 GoToSocial 时超时、GoToSocial 返回 `5xx` 状态码或 GoToSocial 返回 429(请求过多)时提供过期响应。`updating` 值表示允许在 nginx 刷新缓存时提供过期的条目。因为我们在 `proxy_cache_path` 指令中配置了 `inactive=1w`,所以如果满足 `proxy_cache_use_stale` 中的条件,nginx 可以提供最长一周的缓存响应。 diff --git a/docs/locales/zh/advanced/caching/assets-media.md b/docs/locales/zh/advanced/caching/assets-media.md new file mode 100644 index 000000000..2ff522897 --- /dev/null +++ b/docs/locales/zh/advanced/caching/assets-media.md @@ -0,0 +1,132 @@ +# 缓存资源与媒体 + +当你配置 GoToSocial 实例使用本地存储媒体时,可以使用你的[反向代理](../../getting_started/reverse_proxy/index.md)直接提供这些文件并进行缓存。这样可以避免频繁请求 GoToSocial,同时反向代理通常能比 GoToSocial 更快地提供资源。 + +你还可以使用反向代理来缓存 GoToSocial Web UI 的资源,比如其使用的 CSS 和图片。 + +当使用[分域](../host-account-domain.md)部署方式时,你需要确保在主机域上配置资源和媒体的缓存。 + +!!! warning "媒体修剪" + 如果你配置了媒体修剪,必须确保当磁盘上找不到媒体时,仍然将请求发送到 GoToSocial。这将保证从外站实例重新获取该媒体,之后的请求将再次由你的反向代理处理。 + +## 端点 + +有两个端点提供可服务和缓存的资源: + +* `/assets` 包含字体、CSS、图像等 Web UI 的资源 +* `/fileserver` 在使用本地存储后端时,服务于贴文的附件 + +`/assets` 的文件系统位置由 [`web-asset-base-dir`](../../configuration/web.md) 配置选项定义。`/fileserver` 下的文件从 [`storage-local-base-path`](../../configuration/storage.md) 获取。 + +## 配置 + +=== "apache2" + + `Cache-Control` 头手动设置,合并配置和 `expires` 指令的值,以避免因为两个头行而导致错误。默认情况下 `Header set` 为 `onsuccess`,因此它也不会添加到错误响应中。 + + 假设你的 GtS 安装在 `/opt/GtS` 根目录下,并有一个 `storage` 子目录,且 Web 服务器已被授予访问权限,可以在 vhost 中添加以下部分: + + ```apacheconf + + Options None + AllowOverride None + Require all granted + ExpiresActive on + ExpiresDefault A300 + Header set Cache-Control "public, max-age=300" + + RewriteRule "^/assets/(.*)$" "/opt/GtS/web/assets/$1" [L] + + + Options None + AllowOverride None + Require all granted + ExpiresActive on + ExpiresDefault A604800 + Header set Cache-Control "private, immutable, max-age=604800" + + RewriteCond "/opt/GtS/storage/$1" -f + RewriteRule "^/fileserver/(.*)$" "/opt/GtS/storage/$1" [L] + ``` + + 这里的技巧是在基于 Apache 2 的反向代理设置中… + + ```apacheconf + RewriteEngine On + + RewriteCond %{HTTP:Upgrade} websocket [NC] + RewriteCond %{HTTP:Connection} upgrade [NC] + RewriteRule ^/?(.*) "ws://localhost:8980/$1" [P,L] + + ProxyIOBufferSize 65536 + ProxyTimeout 120 + + ProxyPreserveHost On + + ProxyPass http://127.0.0.1:8980/ + ProxyPassReverse http://127.0.0.1:8980/ + + ``` + + … 默认情况下所有的请求都是通过代理的,`RewriteRule` 通过指定文件系统路径来绕过代理以重定向到特定 URL 前缀,而 `RewriteCond` 确保只有在文件确实存在时才禁用 `/fileserver/` 代理。 + + 你还需要运行以下命令(假设使用类似 Debian 的设置)来启用使用的模块: + + ``` + $ sudo a2enmod expires + $ sudo a2enmod headers + $ sudo a2enmod rewrite + ``` + + 然后(在测试配置后)重启 Apache。 + +=== "nginx" + + 以下是你需要在现有的 nginx 配置中添加的三个位置块的示例: + + ```nginx + server { + server_name social.example.org; + + location /assets/ { + alias web-asset-base-dir/; + autoindex off; + expires 5m; + add_header Cache-Control "public"; + } + + location @fileserver { + proxy_pass http://localhost:8080; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /fileserver/ { + alias storage-local-base-path/; + autoindex off; + expires 1w; + add_header Cache-Control "private, immutable"; + try_files $uri @fileserver; + } + } + ``` + + `/fileserver` 位置有点特殊。当我们无法从磁盘获取媒体时,我们希望将请求代理到 GoToSocial,以便它尝试获取。`try_files` 指令本身不能使用 `proxy_pass`,所以我们创建了命名的 `@fileserver` 位置,在 `try_files` 中最后传递给它。 + + !!! bug "尾部斜杠" + `location` 指令和 `alias` 中的尾部斜杠很重要,不要移除它们。 + + `expires` 指令添加了必要的头信息,以告知客户端可以缓存资源的时间: + + * 对于资源,因为可能在每次发布时更改,所以在此示例中使用了 5 分钟 + * 对于附件,因为一旦创建后永远不会更改,所以当前使用一周 + + 有关其他选项,请参阅 [nginx 的 `expires` 指令](https://nginx.org/en/docs/http/ngx_http_headers_module.html#expires)文档。 + + Nginx 不会为 4xx 或 5xx 响应代码添加缓存头,因此抓取资源失败时不会被客户端缓存。`autoindex off` 指令告诉 nginx 不提供目录列表。这应该是默认设置,但明确设置不会有害。添加的 `add_header` 行为 `Cache-Control` 头设置了额外的选项: + + * `public` 用于指示任何人都可以缓存此资源 + * `immutable` 用于指示该资源在其新鲜期内(在 `expires` 之前)绝不会更改,允许客户端在此期间忽略条件请求以重新验证资源。 diff --git a/docs/locales/zh/advanced/caching/index.md b/docs/locales/zh/advanced/caching/index.md new file mode 100644 index 000000000..85b2292b5 --- /dev/null +++ b/docs/locales/zh/advanced/caching/index.md @@ -0,0 +1,11 @@ +# 缓存 + +本节涵盖了多种缓存技术,这些技术可以提高 GoToSocial 在高流量情况下的稳定性,并减轻 GoToSocial 实例的一部分工作负担。 + +!!! note + 这些指南仅在你运行[反向代理](../../getting_started/reverse_proxy/index.md)时才有意义。 + +## 指南 + +* [缓存 API 响应](api.md) +* [缓存资源与媒体](assets-media.md) diff --git a/docs/locales/zh/advanced/certificates.md b/docs/locales/zh/advanced/certificates.md new file mode 100644 index 000000000..2b2d0fa0c --- /dev/null +++ b/docs/locales/zh/advanced/certificates.md @@ -0,0 +1,108 @@ +# 配置 TLS 证书 + +如[部署注意事项](../getting_started/index.md)中所述,联合需要使用 TLS,因为大多数实例拒绝通过未加密的传输进行联合。 + +GoToSocial 内置了通过 Lets Encrypt 进行 TLS 证书的配置和更新支持。本指南介绍如何独立于 GoToSocial 配置证书。如果你想完全控制证书的配置方式,或者因为你正在使用执行 TLS 终止的[反向代理](../getting_started/reverse_proxy/index.md),这将很有用。 + +获取 TLS 证书的方式有几种: + +* 从供应商购买,通常有效期为 2 年 +* 从云提供商获取,具体有效期取决于其产品限制 +* 从像 Lets Encrypt 这样的[ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment)兼容提供商处获取,通常有效期为 3 个月 + +在本指南中,我们只讨论第三种,有关 ACME 兼容提供商的选项。 + +## 一般方法 + +通过 Lets Encrypt 配置证书的方法是: + +* 在你的服务器上安装 ACME 客户端 +* 配置 ACME 客户端来配置你的证书 +* 配置一个软件使用这些证书 +* 启用定时器/cron 定期续订证书 +* 通知必要的应用程序重新加载或重启以获取新证书 + +证书是通过[使用质询](https://letsencrypt.org/sv/docs/challenge-types/)来配置的,这是一种验证你为自己控制的域请求证书的方法。你通常会使用以下之一: + +* HTTP 质询 +* DNS 质询 + +HTTP 质询要求在所请求证书的域上的 80 端口下提供某些文件,路径为 `/.well-known/acme/`。这是默认质询类型。 + +DNS 质询完全在服务器外进行,但需要你更新 DNS TXT 记录。此方法只有在你的 DNS 注册商提供 API,使你的 ACME 客户端完成此质询时才可行。 + +## 客户端 + +官方的 Lets Encrypt 客户端是 [certbot](https://certbot.eff.org/),通常在你选择的[(Linux)发行版](https://repology.org/project/certbot/versions)中打包。某些反向代理如 Caddy 和 Traefik 内置了使用 ACME 协议配置证书的支持。 + +你可以考虑使用的其他一些客户端包括: + +* [acme-client](https://man.openbsd.org/acme-client.1),适用于 OpenBSD,使用平台的特权分离功能 +* [lacme](https://git.guilhem.org/lacme/about/),以进程隔离和最低特权为构建目标,类似于 acme-client 但适用于 Linux +* [Lego](https://github.com/go-acme/lego),用 Go 编写的 ACME 客户端和库 +* [mod_md](https://httpd.apache.org/docs/2.4/mod/mod_md.html),适用于 Apache 2.4.30+ + +### DNS 质询 + +对于 DNS 质询,你的注册商的 API 需要被你的 ACME 客户端支持。尽管 certbot 对一些流行提供商有一些插件,但你可能想查看 [dns-multi](https://github.com/alexzorin/certbot-dns-multi) 插件。它在幕后使用 [Lego](https://github.com/go-acme/lego),支持更广泛的供应商。 + +## 配置 + +有三个重要的配置选项: + +* [`letsencrypt-enabled`](../configuration/tls.md) 控制 GoToSocial 是否尝试配置自己的证书 +* [`tls-certificate-chain`](../configuration/tls.md) 文件系统路径,GoToSocial 可以在此找到 TLS 证书链和公钥 +* [`tls-certificate-key`](../configuration/tls.md) 文件系统路径,GoToSocial 可以在此找到关联的 TLS 私钥 + +### 不使用反向代理 + +当直接将 GoToSocial 暴露到互联网,但仍想使用自己的证书时,可以设置以下选项: + +```yaml +letsencrypt-enabled: false +tls-certificate-chain: "/path/to/combined-certificate-chain-public.key" +tls-certificate-key: "/path/to/private.key" +``` + +这将禁用通过 Lets Encrypt 内置的证书配置,并指示 GoToSocial 在磁盘上找到证书。 + +!!! tip + 在续订证书后应重启 GoToSocial。它在这种情况下不会自动监测证书的更换。 + +### 使用反向代理 + +当在执行 TLS 终止的[反向代理](../getting_started/reverse_proxy/index.md)后运行 GoToSocial 时,你需要如下设置: + +```yaml +letsencrypt-enabled: false +tls-certificate-chain: "" +tls-certificate-key: "" +``` + +确保 `tls-certificate-*` 选项未设置或设置为空字符串。否则,GoToSocial 将尝试自行处理 TLS。 + +!!! danger "协议配置选项" + **不要**将 [`protocol`](../configuration/general.md) 配置选项更改为 `http`。此选项仅应在开发环境中设置为 `http`。即使在 TLS 终止的反向代理后运行,也需要设置为 `https`。 + +你还需要更改 GoToSocial 绑定的[`port`](../configuration/general.md),以便它不再尝试使用 443 端口。 + +要在反向代理中配置 TLS,请参考其文档: + +* [nginx](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-http/) +* [apache](https://httpd.apache.org/docs/2.4/ssl/ssl_howto.html) +* [Traefik](https://doc.traefik.io/traefik/https/tls/) +* [Caddy](https://caddyserver.com/docs/caddyfile/directives/tls) + +!!! tip + 在你的反向代理中配置 TLS 时,请确保你配置了一组较现代的兼容版本和加密套件。可以使用 [Mozilla SSL Configuration Generator](https://ssl-config.mozilla.org/) 的“中级”配置。 + + 检查你的反向代理文档,以了解在证书更改后是否需要重新加载或重启它。并非所有的反向代理都会自动检测到这一点。 + +## 指南 + +网上有许多优质资源解释如何设置这些内容。 + +* [ArchWiki 条目](https://wiki.archlinux.org/title/certbot)关于 certbot +* [Gentoo wiki 条目](https://wiki.gentoo.org/wiki/Let%27s_Encrypt)关于 Lets Encrypt +* [Linode 指南](https://www.linode.com/docs/guides/enabling-https-using-certbot-with-nginx-on-fedora/)关于 Fedora、RHEL/CentOS、Debian 和 Ubuntu 上的 certbot +* Digital Ocean 指南关于在 Ubuntu 22.04 上用 [nginx](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-22-04) 或 [apache](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-22-04)使用 Lets Encrypt diff --git a/docs/locales/zh/advanced/healthchecks.md b/docs/locales/zh/advanced/healthchecks.md new file mode 100644 index 000000000..e3063842a --- /dev/null +++ b/docs/locales/zh/advanced/healthchecks.md @@ -0,0 +1,48 @@ +# 健康检查 + +GoToSocial 提供了两个健康检查 HTTP 端点:`/readyz` 和 `/livez`。 + +这些端点可以用来检查 GoToSocial 是否可访问,并能够进行简单的数据库查询。 + +`/livez` 会始终返回 200 OK 响应且无内容,支持 GET 和 HEAD 请求。这用于检查 GoToSocial 服务是否存活。 + +如果 GoToSocial 能够对配置的数据库后台执行一个非常简单的 SELECT 查询,`/readyz` 会在 GET 和 HEAD 请求下返回 200 OK 响应且无内容。如果执行 SELECT 时发生错误,错误会被记录,并返回 500 Internal Server Error,但无内容。 + +你可以使用上述端点在容器运行时/编排系统中实现健康检查。 + +例如,在 Docker 设置中,你可以在 docker-compose.yaml 中添加以下内容: + +```yaml +healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:8080/readyz || exit 1 + interval: 120s + retries: 5 + start_period: 30s + timeout: 10s +``` + +上述健康检查将在 30 秒后开始,每两分钟检查一次服务是否可用,通过对 `/readyz` 进行 HEAD 请求。如果检查连续失败五次,服务将被标记为不健康。你可以在使用的编排系统中利用此功能强制重启容器。 + +!!! warning + 在慢速硬件上进行数据库迁移时,迁移可能会超过上述健康检查所允许的 10 分钟。 + + 在这样的系统上,你可能需要增加健康检查的间隔或重试次数,以确保不会在迁移中途停止 GoToSocial(这会很糟糕!)。 + +!!! tip + 尽管健康检查端点不透露任何敏感信息,并且只运行非常简单的查询,你可能希望避免将它们暴露给外部世界。你可以在 nginx 中通过在 `server` 段中添加以下代码片段来实现: + + ```nginx + location /livez { + return 404; + } + location /readyz { + return 404; + } + ``` + + 这样会导致 nginx 在请求传递给 GoToSocial 之前拦截这些请求,并直接返回 404 Not Found。 + +参考资料: + +- [Dockerfile 参考](https://docs.docker.com/reference/dockerfile/#healthcheck) +- [Compose 文件参考](https://docs.docker.com/compose/compose-file/compose-file-v3/#healthcheck) diff --git a/docs/locales/zh/advanced/host-account-domain.md b/docs/locales/zh/advanced/host-account-domain.md new file mode 100644 index 000000000..5b758a99c --- /dev/null +++ b/docs/locales/zh/advanced/host-account-domain.md @@ -0,0 +1,116 @@ +# 分域部署 + +本指南解释了如何使用 `@me@example.org` 这样的用户名,但将 GoToSocial 实例本身运行在例如 `social.example.org` 这样的子域名的方法。这种部署布局的配置**必须**在第一次启动 GoToSocial 前完成。 + +!!! danger + 一旦与他人联合后就无法更改域名布局。服务器会因此产生混淆,而你需要说服每个与你联合的实例管理员修改其数据库来解决问题。同时,你还需要在本地重新生成数据库,创建一个新的实例账户和加密密钥对。 + +## 背景 + +ActivityPub 实现通过一个称为 [webfinger](https://www.rfc-editor.org/rfc/rfc7033) 的协议来发现如何将你的账户域映射到你的主机域。这种映射通常会被服务器缓存,因此在事后无法更改。 + +它的工作原理是请求 `https://<账户域>/.well-known/webfinger?resource=acct:@me@example.org`。此时,服务器可以返回重定向到实际的 webfinger 端点 `https://<主机域>/.well-known/webfinger?resource=acct:@me@example.org` 或直接响应。返回的 JSON 文档告知应查询的用户端点: + +```json +{ + "subject": "acct:me@example.org", + "aliases": [ + "https://social.example.org/users/me", + "https://social.example.org/@me" + ], + "links": [ + { + "rel": "http://webfinger.net/rel/profile-page", + "type": "text/html", + "href": "https://social.example.org/@me" + }, + { + "rel": "self", + "type": "application/activity+json", + "href": "https://social.example.org/users/me" + } + ] +} +``` + +ActivityPub 客户端和服务器将使用 `links` 数组中 `rel` 为 `self` 和 `type` 为 `application/activity+json` 的条目来查询更多信息,比如在哪里找到 `inbox` 以进行联合消息的传递。 + +## 配置 + +你需要关注两个配置设置: + +* `host`,API 运行的域名,以及客户端和服务器与实例通信时使用的域 +* `account-domain`,用户账户所属的域名 + +为了实现引言中描述的设置,你需要相应地设置这两个配置选项: + +```yaml +host: social.example.org +account-domain: example.org +``` + +!!! info + `host` 必须始终是运行 GoToSocial 实例的 DNS 名称。它不影响 GoToSocial 实例绑定的 IP 地址。该地址由 `bind-address` 控制。 + +## 反向代理 + +使用[反向代理](../getting_started/reverse_proxy/index.md)时,需要确保能够处理这两个域的流量。你需要将一些端点从账户域重定向到主机域。 + +重定向通常用于客户端侧检测域变化。需要从账户域重定向到主机域的端点是: + +* `/.well-known/webfinger` +* `/.well-known/host-meta` +* `/.well-known/nodeinfo` + +!!! tip + 不要将 API 端点 `/api/...` 的请求从账户域代理或重定向到主机域。这会混淆某些客户端用来检测分域部署的启发式方法,导致登录流程中断及其他异常行为。 + +### nginx + +为了配置重定向,你需要在账户域上进行配置。假设账户域为 `example.org`,主机域为 `social.example.org`,以下配置代码展示了如何做到这一点: + +```nginx +server { + server_name example.org; # account-domain + + location /.well-known/webfinger { + rewrite ^.*$ https://social.example.org/.well-known/webfinger permanent; # host + } + + location /.well-known/host-meta { + rewrite ^.*$ https://social.example.org/.well-known/host-meta permanent; # host + } + + location /.well-known/nodeinfo { + rewrite ^.*$ https://social.example.org/.well-known/nodeinfo permanent; # host + } +} +``` + +### Traefik + +如果 `example.org` 运行在 [Traefik](https://doc.traefik.io/traefik/) 上,可以使用类似以下的标签设置重定向。 + +```yaml +myservice: + image: foo + # 其他配置 + labels: + - 'traefik.http.routers.myservice.rule=Host(`example.org`)' # account-domain + - 'traefik.http.middlewares.myservice-gts.redirectregex.permanent=true' + - 'traefik.http.middlewares.myservice-gts.redirectregex.regex=^https://(.*)/.well-known/(webfinger|nodeinfo|host-meta)(\?.*)?' # host + - 'traefik.http.middlewares.myservice-gts.redirectregex.replacement=https://social.${1}/.well-known/${2}${3}' # host + - 'traefik.http.routers.myservice.middlewares=myservice-gts@docker' +``` + +### Caddy 2 + +确保在你的 `Caddyfile` 中在账户域上配置重定向。以下示例假设账户域为 `example.com`,主机域为 `social.example.com`。 + +``` +example.com { # account-domain + redir /.well-known/host-meta* https://social.example.com{uri} permanent # host + redir /.well-known/webfinger* https://social.example.com{uri} permanent # host + redir /.well-known/nodeinfo* https://social.example.com{uri} permanent # host +} +``` diff --git a/docs/locales/zh/advanced/index.md b/docs/locales/zh/advanced/index.md new file mode 100644 index 000000000..152a2cf16 --- /dev/null +++ b/docs/locales/zh/advanced/index.md @@ -0,0 +1,19 @@ +# 进阶 + +在本节中,我们将讨论多个进阶主题,主要涉及 GoToSocial 的构建、部署、操作和调优。 + +我们将这些主题视为进阶主题,因为不正确地应用它们可能导致客户端和联合问题。如果你不了解所做的更改,应用其中的任何配置更改也可能使调试你的 GoToSocial 实例问题变得更困难。 + +## 指南 + +* [分域部署(API 与账户域名)](host-account-domain.md) +* [使用 HTTP 代理进行客户端/外部请求](outgoing-proxy.md) +* [配置 TLS 证书](certificates.md) +* [缓存 API 响应](caching/api.md) +* [缓存资源及媒体](caching/assets-media.md) +* [进程沙箱](security/sandboxing.md) +* [配置防火墙](security/firewall.md) +* [追踪](tracing.md) +* [指标](metrics.md) +* [配置 SQLite 副本](replicating-sqlite.md) +* [网络存储上的 SQLite](sqlite-networked-storage.md) diff --git a/docs/locales/zh/advanced/metrics.md b/docs/locales/zh/advanced/metrics.md new file mode 100644 index 000000000..a9110bf91 --- /dev/null +++ b/docs/locales/zh/advanced/metrics.md @@ -0,0 +1,57 @@ +# 指标 + +GoToSocial 提供了基于 [OpenTelemetry][otel] 的指标。这些指标使用 [Prometheus 暴露格式][prom],通过 `/metrics` 路径展示。配置设置在 [可观察性配置参考][obs] 中有详细说明。 + +当前收集的指标包括: + +* Go 性能和运行时指标 +* Gin (HTTP) 指标 +* Bun (数据库) 指标 + +可以通过以下配置启用指标: + +```yaml +metrics-enabled: true +``` + +虽然指标不包含任何隐私敏感信息,但你可能不希望随便让任何人查看和抓取你的实例的运营指标。 + +## 启用基本身份验证 + +你可以为指标端点启用基本身份验证。在 GoToSocial 上,你需要以下配置: + +```yaml +metrics-auth-enabled: true +metrics-auth-username: some_username +metrics-auth-password: some_password +``` + +你可以使用 Prometheus 实例通过以下 `scrape_configs` 配置抓取该端点: + +```yaml +- job_name: gotosocial + metrics_path: /metrics + scheme: https + basic_auth: + username: some_username + password: some_password + static_configs: + - targets: + - example.org +``` + +## 屏蔽外部抓取 + +当使用反向代理运行时,可以利用它来屏蔽对指标的外部访问。如果你的 Prometheus 抓取器在与 GoToSocial 实例相同的机器上运行,并可以内部访问它,可以使用这种方法。 + +例如使用 nginx,通过返回 404 来屏蔽 `/metrics` 端点: + +```nginx +location /metrics { + return 404; +} +``` + +[otel]: https://opentelemetry.io/ +[prom]: https://prometheus.io/docs/instrumenting/exposition_formats/ +[obs]: ../configuration/observability.md \ No newline at end of file diff --git a/docs/locales/zh/advanced/outgoing-proxy.md b/docs/locales/zh/advanced/outgoing-proxy.md new file mode 100644 index 000000000..86fbfea91 --- /dev/null +++ b/docs/locales/zh/advanced/outgoing-proxy.md @@ -0,0 +1,21 @@ +# 出站 HTTP 代理 + +GoToSocial 支持配置 HTTP 代理使用的标准环境变量,用于出站请求: + +* `HTTP_PROXY` +* `HTTPS_PROXY` +* `NO_PROXY` + +这些环境变量的小写版本也同样被识别。在处理 https 请求时,`HTTPS_PROXY` 的优先级高于 `HTTP_PROXY`。 + +环境变量的值可以是完整的 URL 或 `host[:port]`,在这种情况下默认使用 "http" 协议。支持的协议包括 "http"、"https" 和 "socks5"。 + +## systemd + +使用 systemd 运行时,可以在 `Service` 部分使用 `Environment` 选项添加必要的环境变量。 + +如何操作可以参考 [`systemd.exec` 手册](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Environment)。 + +## 容器运行时 + +可以在 compose 文件的 `environment` 键下设置环境变量。你也可以在命令行中使用 `-e KEY=VALUE` 或 `--env KEY=VALUE` 传递给 Docker 或 Podman 的 `run` 命令。 diff --git a/docs/locales/zh/advanced/replicating-sqlite.md b/docs/locales/zh/advanced/replicating-sqlite.md new file mode 100644 index 000000000..13361fc35 --- /dev/null +++ b/docs/locales/zh/advanced/replicating-sqlite.md @@ -0,0 +1,102 @@ +# 配置 SQLite 副本 + +除了常规的[备份方法](../admin/backup_and_restore.md)之外,你可能还想设置副本功能,以便在发生灾难时恢复到另一个路径或外部主机。 + +为了使其正常工作,SQLite 需要将日志模式配置为 `WAL` 模式,且同步模式设置为 `NORMAL`。这是 GoToSocial 的默认配置。 + +你可以在配置文件中检查你的设置。日志模式在 `db-sqlite-journal-mode` 中设置,同步模式在 `db-sqlite-synchronous` 中设置。 + +## Linux 下的 Litestream + +使用 [Litestream](https://litestream.io) 是设置 SQLite 副本的一种相对轻量且快速的方法。它可以很容易地配置,并支持不同的后端,比如基于文件的副本、兼容 S3 的存储和许多其他设置。 + +你可以通过在 Linux 上使用 deb 文件安装预构建的软件包,或在其他发行版上从源代码构建。 + +在 Linux 上使用 .deb 包: + +转到 [releases page](https://github.com/benbjohnson/litestream/releases/latest),下载最新版本(确保为下面的 wget 命令选择合适的平台)。 + +```bash +wget https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.deb +sudo dpkg -i litestream-*.deb +``` + +## 配置 Litestream + +通过编辑配置文件进行配置。文件位于 /etc/litestream.yml。 + +### 配置基于文件的副本 + +```yaml +dbs: + - path: /gotosocial/sqlite.db + - path: /backup/sqlite.db +``` + +### 配置基于 S3 的副本 + +设置一个用于副本的桶,并确保将其设置为私有。 +确保用你仪表板中的正确值替换示例中的 `access-key-id` 和 `secret-access-key`。 + +```yaml +access-key-id: AKIAJSIE27KKMHXI3BJQ +secret-access-key: 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 + +dbs: + - path: /gotosocial/sqlite.db + - url: s3://my.bucket.com/db +``` + +使用兼容 S3 的存储提供商时,你需要设置一个端点。 +例如,对 minio 可以使用以下配置。 + +```yaml +access-key-id: miniouser +secret-access-key: miniopassword + +dbs: + - path: /gotosocial/sqlite.db + - type: s3 + bucket: mybucket + path: sqlite.db + endpoint: minio:9000 +``` + +## 启用副本 + +你可以通过启用 Litestream 服务在 Linux 上启用副本。 + +```bash +sudo systemctl enable litestream +sudo systemctl start litestream +``` + +使用 `sudo journalctl -u litestream -f` 检查其是否正常运行。 + +如果你需要更改配置文件,请重启 Litestream: + +```bash +sudo systemctl restart litestream +``` + +### 从配置的后端恢复 + +你可以使用以下简单命令从存储的后端拉取恢复文件。 + +```bash +sudo litestream restore +``` + +如果你配置了多个文件备份或有多个副本,请指定你要执行的操作。 + +对于基于文件的副本: + +```bash +sudo litestream restore -o /gotosocial/sqlite.db /backup/sqlite.db +``` + +对于基于 S3 的副本: + +```bash +sudo litestream restore -o /gotosocial/sqlite.db s3://bucketname/db +``` diff --git a/docs/locales/zh/advanced/security/firewall.md b/docs/locales/zh/advanced/security/firewall.md new file mode 100644 index 000000000..bf78cad76 --- /dev/null +++ b/docs/locales/zh/advanced/security/firewall.md @@ -0,0 +1,174 @@ +# 防火墙 + +你应该在你的实例上部署防火墙,以关闭任何开放端口,并提供一个机制来封禁可能行为不端的客户端。许多防火墙前端还会自动安装一些规则来屏蔽明显的恶意数据包。 + +部署工具来监控日志文件中的某些趋势,并自动封禁表现出某种行为的客户端是很有帮助的。这可以用于监控你的 SSH 和 Web 服务器访问日志,以应对如 SSH 暴力破解攻击。 + +## 端口 + +对于 GoToSocial,你需要确保端口 `443` 保持开放。没有它,任何人都无法访问你的实例。联合将失败,客户端应用程序将无法正常工作。 + +如果你使用 ACME 或 GoToSocial 的内置 Lets Encrypt 支持[配置 TLS 证书](../certificates.md),你还需要开放端口 `80`。 + +为了通过 SSH 访问你的实例,你还需要保持 SSH 守护进程绑定的端口开放。默认情况下,SSH 端口是 `22`。 + +## ICMP + +[ICMP](https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol) 是在机器之间交换数据,以检测某些网络条件或排除故障的协议。许多防火墙倾向于完全屏蔽 ICMP,但这并不理想。应该允许一些 ICMP 类型,你可以使用防火墙为它们配置速率限制。 + +### IPv4 + +为了确保功能可靠,你的防火墙必须允许: + +* ICMP 类型 3:"目标不可达",并有助于路径 MTU 发现 +* ICMP 类型 4:"源抑制" + +如果你希望能够 ping 或被 ping,还应允许: + +* ICMP 类型 0:"回显应答" +* ICMP 类型 8:"回显请求" + +为了 traceroute 能够工作,还可以允许: + +* ICMP 类型 11:"时间超限" + +### IPv6 + +IPv6 协议栈的所有部分非常依赖 ICMP,屏蔽它会导致难以调试的问题。[RFC 4890](https://www.rfc-editor.org/rfc/rfc4890) 专门为此而写,值得查看。 + +简单来说,你必须始终允许: + +* ICMP 类型 1:"目标不可达" +* ICMP 类型 2:"数据包过大" +* ICMP 类型 3,代码 0:"时间超限" +* ICMP 类型 4,代码 1, 2:"参数问题" + +对于 ping,你应该允许: + +* ICMP 类型 128:"回显请求" +* ICMP 类型 129:"回显应答" + +## 防火墙配置 + +在 Linux 上,通常使用 [iptables](https://en.wikipedia.org/wiki/Iptables) 或更现代、更快的 [nftables](https://en.wikipedia.org/wiki/Nftables) 作为后端进行防火墙配置。大多数发行版正在转向使用 nftables,许多防火墙前端可以配置为使用 nftables。你需要参考发行版的文档,但通常会有一个 `iptables` 或 `nftables` 服务,可以通过预定义的位置加载防火墙规则。 + +手动使用原始的 iptables 或 nftables 规则提供了最大的控制精度,但如果不熟悉这些系统,这样做可能会有挑战。为了帮助解决这个问题,存在许多配置前端可以使用。 + +在 Debian 和 Ubuntu 以及 openSUSE 系列的发行版中,通常使用 UFW。它是一个简单的防火墙前端,许多针对这些发行版的教程都会使用它。 + +对于 Red Hat/CentOS 系列的发行版,通常使用 firewalld。它是一个更高级的防火墙配置工具,也有桌面 GUI 和 [Cockpit 集成](https://cockpit-project.org/)。 + +尽管发行版有各自偏好,你可以在任何 Linux 发行版中使用 UFW、firewalld 或其他完全不同的工具。 + +* [Ubuntu Wiki](https://wiki.ubuntu.com/UncomplicatedFirewall?action=show&redirect=UbuntuFirewall) 关于 UFW 的介绍 +* [ArchWiki](https://wiki.archlinux.org/title/Uncomplicated_Firewall) 关于 UFW 的介绍 +* DigitalOcean 指南 [在 Ubuntu 22.04 上使用 UFW 建立防火墙](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-with-ufw-on-ubuntu-22-04) +* [firewalld](https://firewalld.org/) 项目主页及文档 +* [ArchWiki](https://wiki.archlinux.org/title/firewalld) 关于 firewalld 的介绍 +* [使用和配置 firewalld](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html/configuring_firewalls_and_packet_filters/using-and-configuring-firewalld_firewall-packet-filters) 的 Red Hat 文档 +* Linode 指南 [如何使用 firewalld](https://www.linode.com/docs/guides/introduction-to-firewalld-on-centos/) + +## 暴力攻击防护 + +[fail2ban](https://www.fail2ban.org) 和 [SSHGuard](https://www.sshguard.net/) 可以配置以监控日志文件中暴力破解登录和其他恶意行为的尝试。它们可以配置为自动插入防火墙规则,以屏蔽恶意 IP 地址,屏蔽可以是暂时的,也可以是永久的。 + +SSHGuard 最初只为 SSH 设计,但现在支持多种服务。Fail2ban 往往支持任何可生成一致日志行的服务,而 SSHGuard 的签名方法可以捕获更复杂或隐蔽的攻击,因为它随着时间的推移计算攻击分数。 + +SSHGuard 和 fail2ban 都带有后端,可以直接针对 iptables 和 nftables,或与你选择的前端如 UFW 或 firewalld 在 Linux 上工作,在 \*BSD 系统上可以使用 pf。确保查看其文档以正确配置。 + +* [ArchWiki](https://wiki.archlinux.org/title/Fail2ban) 关于 fail2ban 的介绍 +* DigitalOcean 指南如何在 Ubuntu 上使用 [fail2ban 保护 SSH](https://www.digitalocean.com/community/tutorial_collections/how-to-protect-ssh-with-fail2ban) +* Linode 指南如何使用 [fail2ban 保护服务器](https://www.linode.com/docs/guides/using-fail2ban-to-secure-your-server-a-tutorial/) +* [ArchWiki](https://wiki.archlinux.org/title/sshguard) 关于 sshguard 的介绍 +* [FreeBSD 手册](https://man.freebsd.org/cgi/man.cgi?query=sshguard&sektion=8&manpath=FreeBSD+13.2-RELEASE+and+Ports) sshguard 的介绍 +* [SSHGuard 设置](https://manpages.ubuntu.com/manpages/lunar/en/man7/sshguard-setup.7.html) 的 Ubuntu 手册 + +对于 fail2ban,可以使用以下正则表达式,该正则表达式在身份验证失败时触发 fail2ban,而不是其他“未经授权”的错误(例如 API): + +```regex +statusCode=401 path=/auth/sign_in clientIP= .* msg=\"Unauthorized: +``` + +## IP 屏蔽 + +GoToSocial 实现了速率限制,以保护你的实例不被单个主体占用所有处理能力。然而,如果你知道这不是合法流量,或者来自你不想与之联邦的实例,你可以屏蔽流量来源的 IP,以节省 GoToSocial 的处理能力。 + +### Linux + +屏蔽 IP 是通过 iptables 或 nftables 实现的。如果你使用 UFW 或 firewalld 等防火墙前端,请使用其功能来屏蔽 IP。 + +在 iptables 中,人们倾向于在 `filter` 表的 `INPUT` 链中为 IP 添加一个 `DROP` 规则。在 nftables 中,通常在一个具有 `ip` 或 `ip6` 地址族的链的表中完成。在这些情况下,内核已经对传入流量进行了大量不必要的处理,然后再通过 IP 匹配进行屏蔽。 + +使用 iptables 时,可以更有效地使用 `mangle` 表和 `PREROUTING` 链。你可以查看这篇博客文章,[了解它在 iptables 中的工作原理][iptblock]。对于 nftables,你可能会想要使用 [`netdev` family][nftnetdev] 进行屏蔽。 + +[iptblock]: https://javapipe.com/blog/iptables-ddos-protection/ +[nftnetdev]: https://wiki.nftables.org/wiki-nftables/index.php/Nftables_families#netdev + +#### iptables + +使用 `iptables` 屏蔽 IP 的示例: + +``` +iptables -t mangle -A PREROUTING -s 1.0.0.0/8 -j DROP +ip6tables -t mangle -A PREROUTING -s fc00::/7 -j DROP +``` + +当使用 iptables 时,添加许多规则会显著降低速度,包括在添加/删除规则时重新加载防火墙。由于你可能希望屏蔽许多 IP 地址,请使用 [ipset 模块][ipset] 并为集合添加单个屏蔽规则。 + +[ipset]: https://ipset.netfilter.org/ipset.man.html + +首先创建你的集合并添加一些 IP: + +``` +ipset create baddiesv4 hash:ip family inet +ipset create baddiesv6 hash:ip family inet6 + +ipset add baddiesv4 1.0.0.0/8 +ipset add baddiesv6 fc00::/7 +``` + +然后,更新你的 iptables 规则以针对该集合: + +``` +iptables -t mangle -A PREROUTING -m set --match-set baddiesv4 src -j DROP +ip6tables -t mangle -A PREROUTING -m set --match-set baddiesv6 src -j DROP +``` + +#### nftables + +对于 nftables,你可以使用如下配置: + +``` +table netdev filter { + chain ingress { + set baddiesv4 { + type ipv4_addr + flags interval + elements = { \ + 1.0.0.0/8, \ + 2.2.2.2/32 \ + } + } + set baddiesv6 { + type ipv6_addr + flags interval + elements = { \ + 2620:4f:8000::/48, \ + fc00::/7 \ + } + } + + type filter hook ingress device priority -500; + ip saddr @baddiesv4 drop + ip6 saddr @baddiesv6 drop + } +} +``` + +### BSDs + +使用 pf 时,你可以创建一个通常命名为 `` 的持久化表,将需要屏蔽的 IP 地址添加到该表中。表格还可以从其他文件读取,因此可以将 IP 列表保存在主 `pf.conf` 之外。 + +有关如何执行此操作的示例,可以在 [pf 手册][manpf] 中找到。 + +[manpf]: https://man.openbsd.org/pf.conf#TABLES diff --git a/docs/locales/zh/advanced/security/index.md b/docs/locales/zh/advanced/security/index.md new file mode 100644 index 000000000..bdd80b912 --- /dev/null +++ b/docs/locales/zh/advanced/security/index.md @@ -0,0 +1,11 @@ +# 安全加固措施 + +这些指南涵盖如何提高你的 GoToSocial 部署的安全状况。它们不涉及调整 GoToSocial 的设置,而是指出一些你可以做的额外措施,以更好地保护你的实例。 + +!!! note + 这些指南中的任何内容旨在增强你的 GoToSocial 部署的安全性;它们不能替代良好的安全实践,比如保持你的系统定期得到修补和更新。 + +## 指南 + +* [对 GoToSocial 可执行文件进行沙盒处理](sandboxing.md) +* [配置防火墙](firewall.md) diff --git a/docs/locales/zh/advanced/security/sandboxing.md b/docs/locales/zh/advanced/security/sandboxing.md new file mode 100644 index 000000000..ac4b88723 --- /dev/null +++ b/docs/locales/zh/advanced/security/sandboxing.md @@ -0,0 +1,63 @@ +# 对 GoToSocial 可执行文件进行沙盒处理 + +通过对 GoToSocial 二进制文件进行沙盒化,可以控制 GoToSocial 能访问系统的哪些部分,并限制其读写权限。这有助于确保即使在 GoToSocial 出现安全问题时,攻击者也很难提升权限,进而在系统上立足。 + +不同发行版有其偏好的沙盒机制: + +* **AppArmor** 适用于 Debian 或 Ubuntu 系列及 OpenSuSE,包括在 Docker 中的运行时 +* **SELinux** 适用于 Red Hat/Fedora/CentOS 系列或 Gentoo + +## AppArmor + +我们提供了一个 GoToSocial 的 AppArmor 示例策略,你可以按以下步骤获取并安装: + +```sh +$ curl -LO 'https://github.com/superseriousbusiness/gotosocial/raw/main/example/apparmor/gotosocial' +$ sudo install -o root -g root gotosocial /etc/apparmor.d/gotosocial +$ sudo apparmor_parser -Kr /etc/apparmor.d/gotosocial +``` + +安装策略后,你需要配置系统以使用该策略来限制 GoToSocial 的权限。 + +你可以这样禁用该策略: + +```sh +$ sudo apparmor_parser -R /etc/apparmor.d/gotosocial +$ sudo rm -vi /etc/apparmor.d/gotosocial +``` +别忘了回滚你所做的任何加载 AppArmor 策略的配置更改。 + +### systemd + +在 systemd 服务中添加以下内容,或创建一条覆盖规则: + +```ini +[Service] +... +AppArmorProfile=gotosocial +``` + +重载 systemd 并重新启动 GoToSocial: + +```sh +$ systemctl daemon-reload +$ systemctl restart gotosocial +``` + +### 容器 + +使用我们的示例 Compose 文件时,可以通过以下方式告知其加载 AppArmor 策略: + +```yaml +services: + gotosocial: + ... + security_opt: + - apparmor=gotosocial +``` + +在使用 `docker run` 或 `podman run` 启动容器时,需要使用 `--security-opt="apparmor=gotosocial"` 命令行标志。 + +## SELinux + +SELinux 策略由社区在 GitHub 上的 [`lzap/gotosocial-selinux`](https://github.com/lzap/gotosocial-selinux) 仓库维护。请务必阅读其文档,在使用前查看策略,并使用其问题跟踪器获取有关 SELinux 策略的支持请求。 diff --git a/docs/locales/zh/advanced/sqlite-networked-storage.md b/docs/locales/zh/advanced/sqlite-networked-storage.md new file mode 100644 index 000000000..a319aa20a --- /dev/null +++ b/docs/locales/zh/advanced/sqlite-networked-storage.md @@ -0,0 +1,35 @@ +# 网络存储上的 SQLite + +SQLite 的运行模式假定数据库和使用它的进程或应用程序位于同一主机上。在运行 WAL 模式(GoToSocial 的默认模式)时,它依赖于进程之间的共享内存来确保数据库完整性。 + +!!! quote + 所有使用数据库的进程必须在同一台主机计算机上;WAL 不能在网络文件系统上工作。这是因为 WAL 需要所有进程共享少量内存,而在不同主机上的进程显然不能相互共享内存。 + + — SQLite.org [写前日志](https://www.sqlite.org/wal.html) + +这也意味着访问数据库的任何其他进程需要在相同的命名空间或容器上下文中运行。 + +理论上,可以通过 Samba、NFS、iSCSI 或其他形式的网络访问文件系统运行 SQLite。但无论是否使用写前日志模式,SQLite 维护者都不推荐或不支持这样做。这样做会使你的数据库面临损坏的风险。长期以来,网络存储在其锁定原语中存在同步问题,实现的保证也比本地存储更弱。 + +你的云供应商的外部卷,如 Hetzner 云存储卷、AWS EBS、GCP 持久磁盘等,也可能导致问题,并增加不确定的延迟。这往往会严重降低 SQLite 的性能。 + +如果你打算通过网络访问数据库,最好使用具有客户端-服务器架构的数据库。GoToSocial 支持这种用例的 Postgres。 + +如果想要在耐久的长期存储上保留 SQLite 数据库的副本,请参阅 [SQLite 流式副本](replicating-sqlite.md)。请记住,无论是还是副本使用网络文件系统都不能替代[备份](../admin/backup_and_restore.md)。 + +## 设置 + +!!! danger "数据库损坏" + 我们不支持在网络文件系统上使用 SQLite 运行 GoToSocial,如果你因此损坏了数据库,我们将无法帮助你。 + +如果你确实想冒这个风险,你需要调整 SQLite 的 [synchronous][sqlite-sync] 模式和 [journal][sqlite-journal] 模式以适应文件系统的限制。 + +[sqlite-sync]: https://www.sqlite.org/pragma.html#pragma_synchronous +[sqlite-journal]: https://www.sqlite.org/pragma.html#pragma_journal_mode + +你需要更新以下设置: + +* `db-sqlite-journal-mode` +* `db-sqlite-synchronous` + +我们不提供任何建议,因为这将根据你使用的解决方案而有所不同。请参阅 [此问题](https://github.com/superseriousbusiness/gotosocial/issues/3360#issuecomment-2380332027)以了解你可能设置的值。 diff --git a/docs/locales/zh/advanced/tracing.md b/docs/locales/zh/advanced/tracing.md new file mode 100644 index 000000000..dc502b9e4 --- /dev/null +++ b/docs/locales/zh/advanced/tracing.md @@ -0,0 +1,44 @@ +# 追踪 + +GoToSocial 内置了基于 [OpenTelemetry][otel] 的追踪功能。虽然并没有贯穿每个函数,但我们的 HTTP 处理程序和数据库库会创建跨度。在 [可观测性配置参考][obs] 中解释了如何配置追踪。 + +为了接收这些追踪,你需要一些工具来摄取并可视化它们。有很多选项,包括自托管和商业选项。 + +我们提供了一个示例,说明如何使用 [Grafana Tempo][tempo] 来抓取数据跨度,并使用 [Grafana][grafana] 来检索它们。请注意,我们提供的配置不适合生产环境。可以安全地用于本地开发,并可为设置你自己的追踪基础设施提供一个良好的起点。 + +你需要获取 [`example/tracing`][ext] 中的文件。获取这些文件后,你可以运行 `docker-compose up -d` 来启动 Tempo 和 Grafana。在两个服务运行后,可以将以下内容添加到 GoToSocial 配置中,并重新启动你的实例: + +```yaml +tracing-enabled: true +tracing-transport: "grpc" +tracing-endpoint: "localhost:4317" +tracing-insecure-transport: true +``` + +[otel]: https://opentelemetry.io/ +[obs]: ../configuration/observability.md +[tempo]: https://grafana.com/oss/tempo/ +[grafana]: https://grafana.com/oss/grafana/ +[ext]: https://github.com/superseriousbusiness/gotosocial/tree/main/example/tracing + +## 查询和可视化追踪 + +在对你的实例执行几个查询后,你可以在 Grafana 中找到它们。你可以使用 Explore 选项卡并选择 Tempo 作为数据源。由于我们的 Grafana 示例配置启用了 [TraceQL][traceql],Explore 选项卡将默认选择 TraceQL 查询类型。你可以改为选择“搜索”,并在“GoToSocial”服务名称下找到所有 GoToSocial 发出的追踪。 + +使用 TraceQL 时,一个简单的查询来查找与 `/api/v1/instance` 请求相关的所有追踪可以这样写: + +``` +{.http.route = "/api/v1/instance"} +``` + +如果你想查看所有 GoToSocial 追踪,可以运行: + +``` +{.service.name = "GoToSocial"} +``` + +选择一个追踪后,将打开第二个面板,显示对应数据跨度的可视化视图。你可以从那里深入浏览,通过点击每个子跨度查看其执行的操作。 + +![Grafana 显示 /api/v1/instance 端点的追踪](../public/tracing.png) + +[traceql]: https://grafana.com/docs/tempo/latest/traceql/ diff --git a/docs/locales/zh/api/authentication.md b/docs/locales/zh/api/authentication.md new file mode 100644 index 000000000..156c11f31 --- /dev/null +++ b/docs/locales/zh/api/authentication.md @@ -0,0 +1,149 @@ +# 使用 API 进行身份验证 + +使用客户端 API 需要进行身份验证。本页记录了如何获取身份验证令牌的通用流程,并提供了使用 `curl` 在命令行界面进行操作的示例。 + +## 创建新应用 + +我们需要注册一个新应用,以便请求 OAuth 令牌。这可以通过向 `/api/v1/apps` 端点发送 `POST` 请求来完成。注意将下面命令中的 `your_app_name` 替换为你想使用的应用名称: + +```bash +curl \ + -X POST \ + -H 'Content-Type:application/json' \ + -d '{ + "client_name": "your_app_name", + "redirect_uris": "urn:ietf:wg:oauth:2.0:oob", + "scopes": "read" + }' \ + 'https://example.org/api/v1/apps' +``` + +字符串 `urn:ietf:wg:oauth:2.0:oob` 表示一种称为带外身份验证的技术,这是一种用于多因素身份验证的技术,旨在减少恶意行为者干扰身份验证过程的途径。在此情况下,它允许我们查看并手动复制生成的令牌以便继续使用。 + +注意,`scopes` 可以是以下任意空格分隔的组合: + +- `read` +- `write` +- `admin` + +!!! warning + GoToSocial 目前不支持范围授权令牌,因此在此过程中获得的任何令牌都可以代表你执行所有操作,包括如果你的账户具有管理员权限时的管理员操作。然而,始终以最低权限授予你的应用是一个好习惯。例如,如果你的应用不会发布贴文,请使用 scope=read。 + + 本着这种精神,上述示例使用了`read`,这意味着当未来支持范围令牌时,应用将仅限于执行`read`操作。 + + 你可以在[此处](https://github.com/superseriousbusiness/gotosocial/issues/2232)阅读更多关于计划中 OAuth 安全功能的信息。 + +成功调用会返回一个带有 `client_id` 和 `client_secret` 的响应,我们将在后续流程中需要使用这些信息。它看起来像这样: + +```json +{ + "id": "01J1CYJ4QRNFZD6WHQMZV7248G", + "name": "your_app_name", + "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", + "client_id": "YOUR_CLIENT_ID", + "client_secret": "YOUR_CLIENT_SECRET" +} +``` + +!!! tip + 确保将 `client_id` 和 `client_secret` 的值保存到某个位置,以便在需要时参考。 + +## 授权你的应用代表你操作 + +我们已经在 GoToSocial 注册了一个新应用,但它尚未与你的账户连接。现在,我们需要告知 GoToSocial 这个新应用将代表你操作。为此,我们需要通过浏览器进行实例认证,以启动登录和权限授予过程。 + +创建一个带查询字符串的 URL,如下所示,将 `YOUR_CLIENT_ID` 替换为你在上一步收到的 `client_id`,然后将 URL 粘贴到浏览器中: + +```text +https://example.org/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=read +``` + +!!! tip + 如果你在注册应用时使用了不同的范围,在上面的 URL 中将 `scope=read` 替换为你注册时使用的加号分隔的范围列表。例如,如果你注册你的应用时使用了 `scopes` 值 `read write`,那么你应该将上面的 `scope=read` 改为 `scope=read+write`。 + +将 URL 粘贴到浏览器后,你会被引导到实例的登录表单,提示你输入邮箱地址和密码以将应用连接到你的账户。 + +提交凭据后,你会到达一个页面,上面写着类似这样的内容: + +``` +嗨嗨,`your_username`! + +应用 `your_app_name` 申请以你的名义执行操作,申请的权限范围是 *`read`*. +如果选择允许,应用将跳转到: urn:ietf:wg:oauth:2.0:oob 继续操作 +``` + +点击 `允许`,你将到达这样一个页面: + +```text +Here's your out-of-band token with scope "read", use it wisely: +YOUR_AUTHORIZATION_TOKEN +``` + +复制带外授权令牌到某个地方,因为你将在下一步中需要它。 + +## 获取访问令牌 + +下一步是用刚刚收到的带外授权令牌交换一个可重用的访问令牌,该令牌可以在以后所有的 API 请求中发送。 + +你可以通过另一个 `POST` 请求来完成这项操作,如下所示: + +```bash +curl \ + -X POST \ + -H 'Content-Type: application/json' \ + -d '{ + "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", + "client_id": "YOUR_CLIENT_ID", + "client_secret": "YOUR_CLIENT_SECRET", + "grant_type": "authorization_code", + "code": "YOUR_AUTHORIZATION_TOKEN" + }' \ + 'https://example.org/oauth/token' +``` + +确保替换: + +- `YOUR_CLIENT_ID` 为第一步中收到的客户端 ID。 +- `YOUR_CLIENT_SECRET` 为第一步中收到的客户端密钥。 +- `YOUR_AUTHORIZATION_TOKEN` 为在第二步中收到的带外授权令牌。 + +你会收到一个包含访问令牌的响应,看起来像这样: + +```json +{ + "access_token": "YOUR_ACCESS_TOKEN", + "created_at": 1719577950, + "scope": "read", + "token_type": "Bearer" +} +``` + +将你的访问令牌复制并安全保存。 + +## 验证 + +为了确保一切正常,尝试查询 `/api/v1/verify_credentials` 端点,在请求头中添加你的访问令牌作为 `Authorization: Bearer YOUR_ACCESS_TOKEN`。 + +请参考以下示例: + +```bash +curl \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ + 'https://example.org/api/v1/accounts/verify_credentials' +``` + +如果一切顺利,你应该会得到用户资料的 JSON 响应。 + +## 最后说明 + +现在你拥有了访问令牌,可以在每次 API 请求中重复使用该令牌进行授权。你不需要每次都执行整个令牌交换过程! + +例如,你可以使用相同的访问令牌向 API 发送另一个 `GET` 请求以获取通知,如下所示: + +```bash +curl \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ + 'https://example.org/api/v1/notifications' +``` diff --git a/docs/locales/zh/api/ratelimiting.md b/docs/locales/zh/api/ratelimiting.md new file mode 100644 index 000000000..a9533fa01 --- /dev/null +++ b/docs/locales/zh/api/ratelimiting.md @@ -0,0 +1,41 @@ +# 请求速率限制 + +为减轻对你的实例的滥用和抓取,系统使用了基于 IP 的 HTTP 速率限制。 + +不同的端点组有单独的速率限制规则。换句话说,一个部分的 API 被速率限制了,并不意味着其他部分也会被限制。以下列表中的每个项目都有单独的速率限制规则: + +- `/users/*` 和 `/emoji/*` - ActivityPub (s2s) 端点。 +- `/auth/*` 和 `/oauth/*` - 登录和 OAUTH 令牌请求。 +- `/fileserver/*` - 媒体附件、表情符号等。 +- `/nodeinfo/*` - NodeInfo 端点。 +- `/.well-known/*` - webfinger 和 nodeinfo 请求。 + +默认情况下,每个速率限制规则允许在 5 分钟内最多进行 300 次请求:每个客户端 IP 地址每秒 1 次请求。 + +每个响应将包含速率限制的当前状态,具体表现为以下头信息: + +- `X-Ratelimit-Limit`: 每个时间段允许的最大请求数。 +- `X-Ratelimit-Remaining`: 在剩余时间内仍然可以进行的请求数量。 +- `X-Ratelimit-Reset`: 表示速率限制何时重置的 ISO8601 时间戳。 + +如果超过速率限制,将返回 [HTTP 429 Too Many Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) 错误给请求者。 + +## 速率限制常见问题 + +### 我总是超出速率限制!为什么? + +如果你发现自己的速率限制在正常使用时经常被超出(对于你自己和其他请求者也是如此),这可能是因为 GoToSocial 无法通过 IP 地址区分客户端。你可以通过查看实例的日志来调查这个问题。如果(几乎)所有记录的 IP 地址似乎都是相同的 IP 地址(类似于 `172.x.x.x`),那么速率限制将导致问题。 + +这种情况通常发生在你的服务器运行在 NAT(端口转发)中,或者在没有正确配置的 HTTP 代理之后,导致你的实例将所有传入 IP 地址视为相同的地址:即你的反向代理或网关的 IP 地址。这意味着所有传入请求*共享同一个速率限制*,而不是按 IP 正确分开。 + +如果你正在使用 HTTP 代理,那么很可能你的 `trusted-proxies` 未正确配置。如果是这种情况,尝试将反向代理的 IP 地址添加到 `trusted-proxies` 列表中,并重启你的实例。 + +如果没有使用 HTTP 代理,那么很可能是由 NAT 引起的。在这种情况下,你应该完全禁用速率限制。 + +### 我可以配置速率限制吗?可以关闭吗? + +可以!在配置中设置 `advanced-rate-limit-requests: 0`。 + +### 我可以将一个或多个 IP 地址排除在速率限制之外,而保持其他的限制吗? + +可以!在配置中设置 `advanced-rate-limit-exceptions`。 diff --git a/docs/locales/zh/api/swagger.md b/docs/locales/zh/api/swagger.md new file mode 100644 index 000000000..ad069d4d7 --- /dev/null +++ b/docs/locales/zh/api/swagger.md @@ -0,0 +1,21 @@ +# 路由和方法 + +GoToSocial 使用 [go-swagger](https://github.com/go-swagger/go-swagger) 从代码注释生成一个 V2 [OpenAPI 规范](https://swagger.io/specification/v2/)文档。 + +生成的 API 文档如下所示。请注意,本文档仅供参考。你将无法使用以下小部件内置的授权功能实际连接到实例或进行 API 调用。相反,你应该使用像 curl、Postman 等工具。 + +大多数 GoToSocial API 端点需要用户级别的 OAuth 令牌。有关如何使用 OAuth 令牌进行 API 认证的指南,请参阅[认证文档](./authentication.md)。 + +!!! tip + 如果你想更多地使用该规范,还可以直接查看 [swagger.yaml](./swagger.yaml),然后将其粘贴到 [Swagger Editor](https://editor.swagger.io/) 等工具中。这样你可以尝试自动生成不同语言的 GoToSocial API 客户端(不支持,但可以尝试),或者将文档转换为 JSON 或 OpenAPI v3 规范等。更多信息请参见[这里](https://swagger.io/tools/open-source/getting-started/)。 + +!!! info "注意事项:上传文件" + 当使用涉及提交表单上传文件的 API 端点时(例如,媒体附件端点或表情符号上传端点等),请注意,在表单字段中 `filename` 是必需的,这是由于 GoToSocial 用于解析表单的依赖关系以及 Go 的某些特性导致的。 + + 有关更多背景信息,请参见以下问题: + + - [#1958](https://github.com/superseriousbusiness/gotosocial/issues/1958) + - [#1944](https://github.com/superseriousbusiness/gotosocial/issues/1944) + - [#2641](https://github.com/superseriousbusiness/gotosocial/issues/2641) + + diff --git a/docs/locales/zh/api/swagger.yaml b/docs/locales/zh/api/swagger.yaml new file mode 100644 index 000000000..7751c47e3 --- /dev/null +++ b/docs/locales/zh/api/swagger.yaml @@ -0,0 +1,11393 @@ +basePath: / +definitions: + FilterAction: + title: FilterAction + description: FilterAction 是针对与过滤规则匹配的贴文所执行的操作。 + type: string + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + InstanceConfigurationEmojis: + properties: + emoji_size_limit: + description: 最大自定义表情文件大小(字节)。 + example: 51200 + format: int64 + type: integer + x-go-name: EmojiSizeLimit + title: InstanceConfigurationEmojis + description: InstanceConfigurationEmojis 结构体包含有关自定义表情的配置信息。 + type: object + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + Link: + description: Link 代表针对查询请求返回的链接组中的一个“链接”。详见 https://webfinger.net/ 和 https://www.rfc-editor.org/rfc/rfc6415.html#section-3.1 + properties: + href: + type: string + x-go-name: Href + rel: + type: string + x-go-name: Rel + template: + type: string + x-go-name: Template + type: + type: string + x-go-name: Type + title: Link + type: object + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + Mention: + properties: + acct: + description: |- + 通过webfinger发现的帐户URI。 + 对于本站用户,等于"用户名",对于外站用户,等于"用户名@域名"。 + example: some_user@example.org + type: string + x-go-name: Acct + id: + description: 被提及的帐户的ID。 + example: 01FBYJHQWQZAVWFRK9PDYTKGMB + type: string + x-go-name: ID + url: + description: 被提及的账户的Web资料页。 + example: https://example.org/@some_user + type: string + x-go-name: URL + username: + description: 被提及账户的用户名。 + example: some_user + type: string + x-go-name: Username + title: Mention + description: Mention 表示对另一个账户的一次提及。 + type: object + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + NodeInfoServices: + properties: + inbound: + items: + type: string + type: array + x-go-name: Inbound + outbound: + items: + type: string + type: array + x-go-name: Outbound + title: NodeInfoServices + description: 表示此节点对入站和出站连接提供的服务。 + type: object + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + NodeInfoSoftware: + properties: + name: + example: gotosocial + type: string + x-go-name: Name + version: + example: 0.1.2 1234567 + type: string + x-go-name: Version + title: NodeInfoSoftware + description: NodeInfoSoftware 表示此节点软件的名称和版本号。 + type: object + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + NodeInfoUsage: + properties: + localPosts: + format: int64 + type: integer + x-go-name: LocalPosts + users: + $ref: '#/definitions/NodeInfoUsers' + title: NodeInfoUsage + description: 表示有关此服务器的使用信息,例如用户数量。 + type: object + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + NodeInfoUsers: + properties: + total: + format: int64 + type: integer + x-go-name: Total + title: NodeInfoUsers + description: NodeInfoUsers 表示有关服务器上用户的聚合信息。 + type: object + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + Source: + description: Source 表示用户自己账户的内容展示或发布偏好。在验证和更新凭据时,作为 Account 的一个属性返回的附加实体。 + properties: + also_known_as_uris: + description: |- + 要设置此值,请调用 `/api/v1/accounts/alias`。 + + 若为空或为设置,将从json中省略。 + items: + type: string + type: array + x-go-name: AlsoKnownAsURIs + fields: + description: 此账户的元数据。 + items: + $ref: '#/definitions/field' + type: array + x-go-name: Fields + follow_requests_count: + description: 待处理的关注请求数量。 + format: int64 + type: integer + x-go-name: FollowRequestsCount + language: + description: 新贴文的默认发布语言。 + type: string + x-go-name: Language + note: + description: 个人资料简介。 + type: string + x-go-name: Note + privacy: + description: |- + 新贴文的默认可见性。 + public = 公开贴文 + unlisted = 未列出/不公开/悄悄公开贴文 + private = 仅粉丝可见的贴文 + direct = 私信贴文 + type: string + x-go-name: Privacy + sensitive: + description: 是否将新贴文默认标记为敏感。 + type: boolean + x-go-name: Sensitive + status_content_type: + description: 新贴文的默认内容格式。 + type: string + x-go-name: StatusContentType + web_visibility: + description: |- + 通过网页端API显示的该账户下贴文的最低可见性。 + "public" = 默认值,此时仅显示公开贴文。 + "unlisted" = 显示公开贴文 *和* 未列出的贴文。 + "none" = 不在网页端显示任何贴文,即使贴文为公开贴文。 + type: string + x-go-name: WebVisibility + title: Source + type: object + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + TimelineMarker: + properties: + last_read_id: + description: 最近查看的内容实体的ID。 + type: string + x-go-name: LastReadID + updated_at: + description: 设定标记时的时间戳 (ISO 8601 Datetime)。 + type: string + x-go-name: UpdatedAt + version: + description: 用于锁定以防止写冲突。 + format: int64 + type: integer + x-go-name: Version + title: TimelineMarker + description: TimelineMarker 包含有关用户在特定时间线上的阅读进度的信息。 + type: object + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + account: + description: Account 是一个 Fediverse 账户的抽象模型。被抽象的账户可以是本站账户,也可以是外站账户。 + properties: + acct: + description: |- + 通过 WebFinger 发现的帐户 URI。 + 对于本站用户,等于“用户名”,对于外站用户,等于“用户名@域名”。 + example: some_user@example.org + type: string + x-go-name: Acct + avatar: + description: 账户头像的网络地址。 + example: https://example.org/media/some_user/avatar/original/avatar.jpeg + type: string + x-go-name: Avatar + avatar_description: + description: 该账户头像的文字描述,用于提供 Alt 文本。 + example: 一只微笑的树懒的可爱图画。 + type: string + x-go-name: AvatarDescription + avatar_media_id: + description: |- + 此账户的头像对应的媒体附件的数据库 ID。 + 如果此账户没有上传头像(即默认头像),则省略。 + example: 01JAJ3XCD66K3T99JZESCR137W + type: string + x-go-name: AvatarMediaID + avatar_static: + description: |- + 该账户头像的静态版本的网络地址。 + 仅在账户的主头像是视频或 gif 时才起实际作用。 + example: https://example.org/media/some_user/avatar/static/avatar.png + type: string + x-go-name: AvatarStatic + bot: + description: 表示账户是一个机器人。 + type: boolean + x-go-name: Bot + created_at: + description: 账户创建时间 (ISO 8601 Datetime)。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: CreatedAt + custom_css: + description: 渲染该账户的资料页或贴文页时要包含的自定义 CSS。 + type: string + x-go-name: CustomCSS + discoverable: + description: 表示账户已选择加入内容发现功能。 + type: boolean + x-go-name: Discoverable + display_name: + description: 该账户的昵称 + example: 树懒一号 + type: string + x-go-name: DisplayName + emojis: + description: |- + 该账户的简介或昵称中使用的自定义表情组成的数组。 + 如果账户被屏蔽,则始终为空。 + items: + $ref: '#/definitions/emoji' + type: array + x-go-name: Emojis + enable_rss: + description: |- + 账户已启用 RSS 订阅。 + 如果为 false,则省略键/值。 + type: boolean + x-go-name: EnableRSS + fields: + description: |- + 账户设置的附加元数据。 + 如果账户被屏蔽,则始终为空。 + items: + $ref: '#/definitions/field' + type: array + x-go-name: Fields + followers_count: + description: 该账户的粉丝数量(数据来源于本站)。 + format: int64 + type: integer + x-go-name: FollowersCount + following_count: + description: 该账户关注的账户数量(数据来源于本站)。 + format: int64 + type: integer + x-go-name: FollowingCount + header: + description: 该账户的资料卡横幅背景图片的网络地址。 + example: https://example.org/media/some_user/header/original/header.jpeg + type: string + x-go-name: Header + header_description: + description: 该账户的资料卡横幅背景图片的文字描述,用于提供 Alt 文本。 + example: 一只树懒和一只小象在一起的可爱图画。 + type: string + x-go-name: HeaderDescription + header_media_id: + description: |- + 此账户的资料卡横幅背景图片对应的媒体附件的数据库 ID。 + 如果此账户没有上传资料卡横幅背景图(即使用默认横幅背景),则省略。 + example: 01JAJ3XCD66K3T99JZESCR137W + type: string + x-go-name: HeaderMediaID + header_static: + description: |- + 该账户的资料卡横幅背景图片的静态版本的网络地址。 + 仅在账户的资料卡横幅背景图片是视频或 gif 时才起实际作用。 + example: https://example.org/media/some_user/header/static/header.png + type: string + x-go-name: HeaderStatic + hide_collections: + description: |- + 账户选择隐藏其粉丝/关注数据。 + 如果为 false,则省略键/值。 + type: boolean + x-go-name: HideCollections + id: + description: 账户的 ID。 + example: 01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: ID + last_status_at: + description: 账户最近一条贴文的发布时间 (ISO 8601 Date)。 + example: "2021-07-30" + type: string + x-go-name: LastStatusAt + locked: + description: 账户选择手动批准关注请求。 + type: boolean + x-go-name: Locked + moved: + $ref: '#/definitions/account' + note: + description: 账户的简介。 + type: string + x-go-name: Note + role: + $ref: '#/definitions/accountRole' + roles: + description: |- + Roles 列出了账户在本站上的公开身份组。 + 与 Role 不同,Roles 始终可用,但从不包含权限细节。 + 对于外站账户,键/值被省略。 + items: + $ref: '#/definitions/accountDisplayRole' + type: array + x-go-name: Roles + source: + $ref: '#/definitions/Source' + statuses_count: + description: Number of statuses posted by this account, according to our instance. 账户发布的贴文数量 (数据来源于本站)。 + format: int64 + type: integer + x-go-name: StatusesCount + suspended: + description: 账户已被本站封禁。 + type: boolean + x-go-name: Suspended + theme: + description: 用户选择的 CSS 主题文件名,用于在渲染此账户的资料页或贴文页时包含。 + type: string + x-go-name: Theme + url: + description: 账户资料页的 Web 地址。 + example: https://example.org/@some_user + type: string + x-go-name: URL + username: + description: 账户的用户名,不包括域名。 + example: some_user + type: string + x-go-name: Username + title: Account + type: object + x-go-name: Account + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + accountDisplayRole: + description: AccountDisplayRole 是账户的公开、可显示身份组的抽象模型。它是 AccountRole 的一个子集。 + properties: + color: + description: |- + Color 是一个带有前导 `#` 的 6 位 CSS 风格十六进制颜色码,如果此身份组没有设置颜色,则为空字符串。 + GotoSocial 不使用身份组颜色,因此我们将其留空。 + type: string + x-go-name: Color + id: + description: |- + 身份组的 ID。 + GotoSocial 不使用该属性,但我们将其设置为身份组名称,以防客户端期望唯一 ID。 + type: string + x-go-name: ID + name: + description: 身份组的名称。 + type: string + x-go-name: Name + title: AccountDisplayRole + type: object + x-go-name: AccountDisplayRole + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + accountExportStats: + description: |- + AccountExportStats 专门用于在 /api/v1/exports/stats 端点上通知有关导出大小的账户统计信息。 + properties: + blocks_count: + description: 被此账户屏蔽的账户数量。 + example: 15 + format: int64 + type: integer + x-go-name: BlocksCount + followers_count: + description: 此账户的粉丝数量。 + example: 50 + format: int64 + type: integer + x-go-name: FollowersCount + following_count: + description: 此账户关注的账户数量。 + example: 50 + format: int64 + type: integer + x-go-name: FollowingCount + lists_count: + description: 此账户创建的列表数量。 + example: 10 + format: int64 + type: integer + x-go-name: ListsCount + media_storage: + description: 'TODO: 此账户使用的媒体存储空间大小的字符串表示。' + example: 500MB + type: string + x-go-name: MediaStorage + mutes_count: + description: 此账户静音/隐藏的账户数量。 + example: 11 + format: int64 + type: integer + x-go-name: MutesCount + statuses_count: + description: 此账户发布的贴文数量。 + example: 81986 + format: int64 + type: integer + x-go-name: StatusesCount + type: object + x-go-name: AccountExportStats + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + accountRelationship: + properties: + blocked_by: + description: 你被此账户屏蔽。 + type: boolean + x-go-name: BlockedBy + blocking: + description: 你屏蔽了此账户。 + type: boolean + x-go-name: Blocking + domain_blocking: + description: 你屏蔽了此账户所在的实例。 + type: boolean + x-go-name: DomainBlocking + endorsed: + description: 你在你的资料页上展示了此账户。 + type: boolean + x-go-name: Endorsed + followed_by: + description: 此账户关注了你。 + type: boolean + x-go-name: FollowedBy + following: + description: 你关注了此账户。 + type: boolean + x-go-name: Following + id: + description: 账户 ID。 + example: 01FBW9XGEP7G6K88VY4S9MPE1R + type: string + x-go-name: ID + muting: + description: 你静音/隐藏了此账户。 + type: boolean + x-go-name: Muting + muting_notifications: + description: 你静音/隐藏了此账户的通知。 + type: boolean + x-go-name: MutingNotifications + note: + description: 你对此账户的备注。 + type: string + x-go-name: Note + notifying: + description: 你在此账户发布贴文时收到通知。 + type: boolean + x-go-name: Notifying + requested: + description: 你向此账户发送了关注请求,请求正在等待对方处理。 + type: boolean + x-go-name: Requested + requested_by: + description: 此账户请求关注你,请求正在等待你处理。 + type: boolean + x-go-name: RequestedBy + showing_reblogs: + description: 你选择在主页时间线上看到此账户转发的贴文。 + type: boolean + x-go-name: ShowingReblogs + title: Relationship + description: Relationship 表示账户之间的关系。 + type: object + x-go-name: Relationship + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + accountRole: + properties: + color: + description: |- + Color 是一个带有 `#` 前缀的 6 位 CSS 风格十六进制颜色码,如果此身份组没有设置颜色,则为空字符串。 + GotoSocial 不使用身份组颜色,因此我们将其留空。 + type: string + x-go-name: Color + highlighted: + description: |- + Highlighted 表示该身份组是否在用户资料页上公开显示。 + 对于 GotoSocial 内置的管理员和站务身份组,此值始终为 true,否则为 false。 + type: boolean + x-go-name: Highlighted + id: + description: |- + 该身份组的 ID。 + GotoSocial 不使用该属性,但我们将其设置为身份组名称,以防客户端期望唯一 ID。 + type: string + x-go-name: ID + name: + description: 身份组的名称。 + type: string + x-go-name: Name + permissions: + description: Permissions 是一个按位序列化的数字字符串,指示用户可以执行哪些管理员/站务操作。 + type: string + x-go-name: Permissions + title: AccountRole + description: AccountRole 是账户的身份组的抽象模型。 + type: object + x-go-name: AccountRole + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + adminAccountInfo: + properties: + account: + $ref: '#/definitions/account' + approved: + description: 账户是否已被批准。 + type: boolean + x-go-name: Approved + confirmed: + description: 账户是否已验证其电子邮件地址。 + type: boolean + x-go-name: Confirmed + created_at: + description: 账户首次被发现的时间 (ISO 8601 Datetime)。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: CreatedAt + created_by_application_id: + description: 创建此账户的应用程序的 ID。 + type: string + x-go-name: CreatedByApplicationID + disabled: + description: 账户目前是否已被停用。 + type: boolean + x-go-name: Disabled + domain: + description: |- + 账户所在实例的域名。 + 对于本站账户,为 null。 + example: example.org + type: string + x-go-name: Domain + email: + description: |- + 与账户关联的电子邮件地址。 + 对于外站账户或没有已知电子邮件地址的账户,为空字符串。 + example: someone@somewhere.com + type: string + x-go-name: Email + id: + description: 该账户在数据库中的 ID。 + example: 01GQ4PHNT622DQ9X95XQX4KKNR + type: string + x-go-name: ID + invite_request: + description: |- + 账户填写的注册理由。 + 如果未提供理由或为外站账户,则为 null。 + example: 快给爷通过a!! + type: string + x-go-name: InviteRequest + invited_by_account_id: + description: 邀请此用户的账户的 ID。 + type: string + x-go-name: InvitedByAccountID + ip: + description: |- + 此账户上次登录的 IP 地址。 + 如果未知,则为 null。 + example: 192.0.2.1 + type: string + x-go-name: IP + ips: + description: |- + 与此账户关联的所有已知 IP 地址。 + 未实现(将始终为空数组)。 + example: [] + items: {} + type: array + x-go-name: IPs + locale: + description: The locale of the account. (ISO 639 Part 1 two-letter language code) 账户的语言偏好。 (ISO 639 Part 1 两字母语言代码) + example: zh + type: string + x-go-name: Locale + role: + $ref: '#/definitions/accountRole' + silenced: + description: 此账户是否被静音/隐藏。 + type: boolean + x-go-name: Silenced + suspended: + description: 账户目前是否已被封禁。 + type: boolean + x-go-name: Suspended + username: + description: 账户的用户名。 + example: dril + type: string + x-go-name: Username + title: AdminAccountInfo + description: AdminAccountInfo 是管理员视图下账户详情的抽象模型。 + type: object + x-go-name: AdminAccountInfo + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + adminActionResponse: + description: |- + AdminActionResponse 是服务器对管理员操作的响应的抽象模型。 + properties: + action_id: + description: 操作的内部 ID。 + example: 01H9QG6TZ9W5P0402VFRVM17TH + type: string + x-go-name: ActionID + type: object + x-go-name: AdminActionResponse + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + adminEmoji: + properties: + category: + description: 于在表情选择器中对自定义表情进行排序。 + example: blobcats + type: string + x-go-name: Category + content_type: + description: 该表情的 MIME 内容类型。 + example: image/png + type: string + x-go-name: ContentType + disabled: + description: 若此表情已被管理员操作禁用,则为 true。 + example: false + type: boolean + x-go-name: Disabled + domain: + description: 该表情的来源实例域名。仅用于外站域名,否则键/值将不会设置。 + example: example.org + type: string + x-go-name: Domain + id: + description: 该表情的 ID。 + example: 01GEM7SFDZ7GZNRXFVZ3X4E4N1 + type: string + x-go-name: ID + shortcode: + description: 自定义表情的名称。 + example: blobcat_uwu + type: string + x-go-name: Shortcode + static_url: + description: 一个指向此自定义表情的静态副本的链接。 + example: https://example.org/fileserver/emojis/blogcat_uwu.png + type: string + x-go-name: StaticURL + total_file_size: + description: 表情占用的总文件大小(字节),包括静态和动画版本。 + example: 69420 + format: int64 + type: integer + x-go-name: TotalFileSize + updated_at: + description: 该表情的最后更新时间。 + example: "2022-10-05T09:21:26.419Z" + type: string + x-go-name: UpdatedAt + uri: + description: 该表情的 ActivityPub URI。 + example: https://example.org/emojis/016T5Q3SQKBT337DAKVSKNXXW1 + type: string + x-go-name: URI + url: + description: 该自定义表情的 Web URL。 + example: https://example.org/fileserver/emojis/blogcat_uwu.gif + type: string + x-go-name: URL + visible_in_picker: + description: 该表情是否在实例的表情选择器中可见。 + example: true + type: boolean + x-go-name: VisibleInPicker + title: AdminEmoji + description: AdminEmoji 是管理员视图下自定义表情的抽象模型。 + type: object + x-go-name: AdminEmoji + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + adminReport: + properties: + account: + $ref: '#/definitions/adminAccountInfo' + action_taken: + description: 管理员是否已对该举报采取行动。 + example: false + type: boolean + x-go-name: ActionTaken + action_taken_at: + description: |- + 若已采取行动,在何时采取的行动? (ISO 8601 Datetime) + 如果未设置/尚未采取行动,则为null。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: ActionTakenAt + action_taken_by_account: + $ref: '#/definitions/adminAccountInfo' + action_taken_comment: + description: |- + 若已采取行动,管理员对采取的行动做了什么评论? + 如果未设置/尚未采取行动,则为null。 + example: 账户已被封禁。 + type: string + x-go-name: ActionTakenComment + assigned_account: + $ref: '#/definitions/adminAccountInfo' + category: + description: 该举报的类别是? + example: spam + type: string + x-go-name: Category + comment: + description: |- + 该举报创建时提交的评论。 + 如果未提交评论,则为空。 + example: 此人一直在骚扰我。 + type: string + x-go-name: Comment + created_at: + description: 举报创建时间 (ISO 8601 Datetime)。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: CreatedAt + forwarded: + description: 用于指示是否应将举报抄送外站实例的布尔值。 + example: true + type: boolean + x-go-name: Forwarded + id: + description: 举报的 ID。 + example: 01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: ID + rules: + description: |- + 该举报中提交的违反的规则数组。 + 如果未提交规则 ID,则为空。 + items: + $ref: '#/definitions/instanceRule' + type: array + x-go-name: Rules + statuses: + description: |- + 该举报中提交的贴文数组。 + 如果未提交贴文 ID,则为空。 + items: + $ref: '#/definitions/status' + type: array + x-go-name: Statuses + target_account: + $ref: '#/definitions/adminAccountInfo' + updated_at: + description: 针对此举报的最后一次操作的操作时间 (ISO 8601 Datetime)。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: UpdatedAt + title: AdminReport + description: AdminReport 是管理员视图下举报的抽象模型。 + type: object + x-go-name: AdminReport + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + application: + properties: + client_id: + description: 应用程序关联的客户端 ID。 + type: string + x-go-name: ClientID + client_secret: + description: 应用程序关联的客户端密钥。 + type: string + x-go-name: ClientSecret + id: + description: 应用程序的 ID。 + example: 01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: ID + name: + description: 应用程序的名称。 + example: Moshidon + type: string + x-go-name: Name + redirect_uri: + description: 授权后重定向 URI,用于应用程序 (OAuth2)。 + example: https://example.org/callback?some=query + type: string + x-go-name: RedirectURI + vapid_key: + description: 用于推送 API 的密钥。 + type: string + x-go-name: VapidKey + website: + description: 与应用程序关联的网站 (url)。 + example: https://tusky.app + type: string + x-go-name: Website + title: Application + description: Application 是对 API 应用程序的抽象模型。 + type: object + x-go-name: Application + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + attachment: + properties: + blurhash: + description: |- + 通过 BlurHash 算法计算的哈希,用于在媒体尚未下载时生成彩色预览缩略图。 + 参见 https://github.com/woltapp/blurhash + type: string + x-go-name: Blurhash + description: + description: 用于描述媒体附件内容的 Alt 文本。 + example: 一只橘猫在阳光下打盹。 + type: string + x-go-name: Description + id: + description: 媒体附件的 ID。 + example: 01FC31DZT1AYWDZ8XTCRWRBYRK + type: string + x-go-name: ID + meta: + $ref: '#/definitions/mediaMeta' + preview_remote_url: + description: |- + 外站服务器上的媒体附件缩略图地址。 + 仅在非本站实例上定义。 + example: https://some-other-server.org/attachments/small/ahhhhh.jpeg + type: string + x-go-name: PreviewRemoteURL + preview_url: + description: 该媒体附件的缩略图地址。 + example: https://example.org/fileserver/some_id/attachments/some_id/small/attachment.jpeg + type: string + x-go-name: PreviewURL + remote_url: + description: |- + 外站服务器上的媒体附件原图地址。 + 仅在非本站实例上定义。 + example: https://some-other-server.org/attachments/original/ahhhhh.jpeg + type: string + x-go-name: RemoteURL + text_url: + description: |- + 该媒体附件的短链接。 + 对于 GoToSocial,该值与 URL 相同,因为我们不创建较短的 URL。 + type: string + x-go-name: TextURL + type: + description: 附件的类型。 + example: image + type: string + x-go-name: Type + url: + description: 附件的原图地址。 + example: https://example.org/fileserver/some_id/attachments/some_id/original/attachment.jpeg + type: string + x-go-name: URL + title: Attachment + description: Attachment 是媒体附件的抽象模型。 + type: object + x-go-name: Attachment + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + card: + properties: + author_name: + description: 原始资源的作者。 + example: weewee@buzzfeed.com + type: string + x-go-name: AuthorName + author_url: + description: 指向原始资源作者的链接。 + example: https://buzzfeed.com/authors/weewee + type: string + x-go-name: AuthorURL + blurhash: + description: 通过 BlurHash 算法计算的哈希,用于在媒体尚未下载时生成彩色预览缩略图。 + type: string + x-go-name: Blurhash + description: + description: 预览的描述。 + example: 水是湿的吗?这很不好说。在这篇文章中,我们请教了一位专家... + type: string + x-go-name: Description + embed_url: + description: 用于照片嵌入,而非自定义 HTML。 + type: string + x-go-name: EmbedURL + height: + description: 预览的高度,以像素为单位。 + format: int64 + type: integer + x-go-name: Height + html: + description: 用于生成预览卡片的 HTML。 + type: string + x-go-name: HTML + image: + description: 预览缩略图。 + example: https://example.org/fileserver/preview/thumb.jpg + type: string + x-go-name: Image + provider_name: + description: 原始资源的提供者。 + example: 惊爆新闻 + type: string + x-go-name: ProviderName + provider_url: + description: 一个指向原始资源提供者的链接。 + example: https://buzzfeed.com + type: string + x-go-name: ProviderURL + title: + description: 链接到的资源的标题。 + example: 惊爆新闻:水是湿的吗? + type: string + x-go-name: Title + type: + description: 预览卡片的类型。 + example: link + type: string + x-go-name: Type + url: + description: 链接指向的资源的地址。 + example: https://buzzfeed.com/some/fuckin/buzzfeed/article + type: string + x-go-name: URL + width: + description: 预览的宽度,单位为像素。 + format: int64 + type: integer + x-go-name: Width + title: Card + description: Card 表示使用 OpenGraph 标签从 URL 生成的丰富预览卡片。 + type: object + x-go-name: Card + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + conversation: + description: |- + Conversation 表示具有“私信”可见性的对话。 + properties: + accounts: + description: |- + 对话的参与者。 + 如果这是没有目标账户的对话(即仅自己可见的私信), + 则仅包含请求账户本身。否则,将包括对话中的所有其他账户,但不包括请求账户。 + items: + $ref: '#/definitions/account' + type: array + x-go-name: Accounts + id: + description: 对话在本地数据库中的 ID。 + type: string + x-go-name: ID + last_status: + $ref: '#/definitions/status' + unread: + description: 该对话当前是否被标记为未读? + type: boolean + x-go-name: Unread + type: object + x-go-name: Conversation + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + debugAPUrlResponse: + description: |- + DebugAPUrlResponse 提供对 AP URL 解引用请求的详细调试信息。 + properties: + request_headers: + additionalProperties: + items: + type: string + type: array + description: 发送请求时使用的 HTTP 标头。 + type: object + x-go-name: RequestHeaders + request_url: + description: 请求的外站 AP URL。 + type: string + x-go-name: RequestURL + response_body: + description: |- + 外站实例返回的 Body。 + 将是字符串化的字节;可能是 JSON,可能是错误文本,也可能同时是以上两者! + type: string + x-go-name: ResponseBody + response_code: + description: 外站实例返回的 HTTP 状态码。 + format: int64 + type: integer + x-go-name: ResponseCode + response_headers: + additionalProperties: + items: + type: string + type: array + description: 外站实例返回的 HTTP 标头。 + type: object + x-go-name: ResponseHeaders + type: object + x-go-name: DebugAPUrlResponse + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + defaultPolicies: + properties: + direct: + $ref: '#/definitions/interactionPolicy' + private: + $ref: '#/definitions/interactionPolicy' + public: + $ref: '#/definitions/interactionPolicy' + unlisted: + $ref: '#/definitions/interactionPolicy' + title: 发起请求的账户的新贴文的默认互动规则。 + type: object + x-go-name: DefaultPolicies + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + domain: + description: Domain 表示一个外站实例 + properties: + domain: + description: 实例的主机名。 + example: example.org + type: string + x-go-name: Domain + public_comment: + description: 若实例被屏蔽,公开的屏蔽原因是什么。 + example: 它们被骚扰账号攻陷了。 + type: string + x-go-name: PublicComment + silenced_at: + description: 实例被静音/隐藏的时间。对于开放实例,此键将不会存在。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: SilencedAt + suspended_at: + description: 实例被屏蔽的时间。对于开放实例,此键将不会存在。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: SuspendedAt + type: object + x-go-name: Domain + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + domainPermission: + properties: + created_at: + description: 此权限条目创建的时间 (ISO 8601 Datetime)。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: CreatedAt + created_by: + description: 创建此实例权限条目的账户的 ID。 + example: 01FBW2758ZB6PBR200YPDDJK4C + type: string + x-go-name: CreatedBy + domain: + description: 此实例的主机名。 + example: example.org + type: string + x-go-name: Domain + id: + description: 此实例权限条目的 ID。 + example: 01FBW21XJA09XYX51KV5JVBW0F + readOnly: true + type: string + x-go-name: ID + obfuscate: + description: 是否在公开此实例权限条目时对域名进行模糊处理。 + example: false + type: boolean + x-go-name: Obfuscate + private_comment: + description: 对此权限条目的私密评论,仅对本站管理员可见。 + example: 它们管理员都是啥b啊 + type: string + x-go-name: PrivateComment + public_comment: + description: 若实例被屏蔽,公开的屏蔽原因是什么。 + example: 它们被骚扰账号攻陷了。 + type: string + x-go-name: PublicComment + silenced_at: + description: 该实例被静音/隐藏的时间。对于开放实例,此键将不会存在。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: SilencedAt + subscription_id: + description: 如果可用,导致此实例权限条目被创建的订阅的 ID。 + example: 01FBW25TF5J67JW3HFHZCSD23K + type: string + x-go-name: SubscriptionID + suspended_at: + description: 此实例被屏蔽的时间。对于开放实例,此键将不会存在。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: SuspendedAt + title: DomainPermission + description: DomainPermission 表示应用于某个实例的权限(显式阻止/允许)。 + type: object + x-go-name: DomainPermission + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + emoji: + properties: + category: + description: 用于在表情选择器中对自定义表情进行排序。 + example: blobcats + type: string + x-go-name: Category + shortcode: + description: 自定义表情的名称。 + example: blobcat_uwu + type: string + x-go-name: Shortcode + static_url: + description: 该自定义表情的静态副本链接。 + example: https://example.org/fileserver/emojis/blogcat_uwu.png + type: string + x-go-name: StaticURL + url: + description: 该自定义表情的 Web URL。 + example: https://example.org/fileserver/emojis/blogcat_uwu.gif + type: string + x-go-name: URL + visible_in_picker: + description: 该表情是否在实例的表情选择器中可见。 + example: true + type: boolean + x-go-name: VisibleInPicker + title: Emoji + description: Emoji 表示一个自定义表情。 + type: object + x-go-name: Emoji + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + emojiCategory: + properties: + id: + description: 自定义表情类别的 ID。 + type: string + x-go-name: ID + name: + description: 自定义表情类别的名称。 + type: string + x-go-name: Name + title: EmojiCategory + description: EmojiCategory 表示自定义表情的类别。 + type: object + x-go-name: EmojiCategory + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + field: + properties: + name: + description: 该附加字段的键/名称。 + example: 人称代词 + type: string + x-go-name: Name + value: + description: 该附加字段的值。 + example: they/them + type: string + x-go-name: Value + verified_at: + description: 如果此字段已被验证,是何时验证的? (ISO 8601 Datetime) + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: VerifiedAt + title: Field + description: Field 表示要在账户资料上显示的名称/值对。 + type: object + x-go-name: Field + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + filterContext: + description: FilterContext 表示过滤规则要应用到的上下文。Filter API 的 v1 和 v2 使用相同的上下文集合。 + title: FilterContext + type: string + x-go-name: FilterContext + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + filterKeyword: + properties: + id: + description: 数据库中过滤关键词条目的 ID。 + type: string + x-go-name: ID + keyword: + description: 被过滤的文本。 + example: 挂人 + type: string + x-go-name: Keyword + whole_word: + description: 该过滤关键词是否应考虑单词边界?(整词匹配) + example: true + type: boolean + x-go-name: WholeWord + title: FilterKeyword + description: FilterKeyword 表示 v2 过滤规则中要过滤的关键词文本。 + type: object + x-go-name: FilterKeyword + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + filterResult: + properties: + filter: + $ref: '#/definitions/filterV2' + keyword_matches: + description: 该过滤规则中被匹配到的关键词。 + items: + type: string + type: array + x-go-name: KeywordMatches + status_matches: + description: 命中该过滤规则的贴文 ID。 + items: + type: string + type: array + x-go-name: StatusMatches + title: FilterResult + description: FilterResult 与被过滤的贴文一起返回,以解释为什么被过滤。 + type: object + x-go-name: FilterResult + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + filterStatus: + properties: + id: + description: 数据库中过滤贴文条目的 ID。 + type: string + x-go-name: ID + phrase: + description: 被过滤的贴文 ID。 + type: string + x-go-name: StatusID + title: FilterStatus + description: FilterStatus 表示 v2 过滤规则中被过滤的贴文 ID。 + type: object + x-go-name: FilterStatus + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + filterV1: + description: |- + FilterV1 表示用户定义的过滤规则,用于确定哪些贴文不应显示给用户。 + 注意,v1 过滤规则在内部处理时会映射到 v2 过滤规则和 v2 过滤关键词。 + 若 whole_word 为 true,则客户端应执行: + 为您的应用程序定义“单词组成字符 (word constituent character)”。在官方实现中,它是 [A-Za-z0-9_] (JavaScript) 和 [[:word:]] (Ruby)。 + Ruby 使用 POSIX 字符类 (Letter | Mark | Decimal_Number | Connector_Punctuation)。 + 如果短语以单词字符开头,并且匹配范围之前的前一个字符是单词字符,则应将其匹配范围视为不匹配。 + 如果短语以单词字符结尾,并且匹配范围之后的下一个字符是单词字符,则应将其匹配范围视为不匹配。 + 请查看 Mastodon 源代码中的 app/javascript/mastodon/selectors/index.js 和 app/lib/feed_manager.rb 以获取更多详细信息。 + properties: + context: + description: 该过滤规则被应用到的上下文。 + example: + - home + - public + items: + $ref: '#/definitions/filterContext' + minItems: 1 + type: array + uniqueItems: true + x-go-name: Context + expires_at: + description: 过滤规则不再生效的时间。如果过滤规则不会过期,则为 null。 + example: "2024-02-01T02:57:49Z" + type: string + x-go-name: ExpiresAt + id: + description: 数据库中过滤规则条目的 ID。 + type: string + x-go-name: ID + irreversible: + description: 命中的条目是否应从用户的时间线/视图中移除,而不是隐藏? + example: false + type: boolean + x-go-name: Irreversible + phrase: + description: 被过滤的文本。 + example: 挂人 + type: string + x-go-name: Phrase + whole_word: + description: 过滤规则是否应考虑单词边界? + example: true + type: boolean + x-go-name: WholeWord + title: FilterV1 + type: object + x-go-name: FilterV1 + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + filterV2: + description: FilterV2 表示用户定义的过滤规则,用于确定哪些贴文不应显示给用户。v2 过滤规则具有名称,并且可以包含多个短语和贴文 ID 以进行过滤。 + properties: + context: + description: 过滤规则应用的上下文。 + example: + - home + - public + items: + $ref: '#/definitions/filterContext' + minItems: 1 + type: array + uniqueItems: true + x-go-name: Context + expires_at: + description: 过滤规则不再生效的时间。如果过滤规则不会过期,则为 null。 + example: "2024-02-01T02:57:49Z" + type: string + x-go-name: ExpiresAt + filter_action: + $ref: '#/definitions/FilterAction' + id: + description: 数据库中过滤规则条目的 ID。 + type: string + x-go-name: ID + keywords: + description: 此过滤规则下的关键词。 + items: + $ref: '#/definitions/filterKeyword' + type: array + x-go-name: Keywords + statuses: + description: 此过滤规则下的贴文。 + items: + $ref: '#/definitions/filterStatus' + type: array + x-go-name: Statuses + title: + description: 此过滤规则的名称。 + example: Linux 相关 + type: string + x-go-name: Title + title: FilterV2 + type: object + x-go-name: FilterV2 + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + headerFilter: + properties: + created_at: + description: 标头过滤规则创建时间 (ISO 8601 Datetime)。 + example: "2021-07-30T09:20:25+00:00" + readOnly: true + type: string + x-go-name: CreatedAt + created_by: + description: 创建此标头过滤规则的管理员账户的 ID。 + example: 01FBW2758ZB6PBR200YPDDJK4C + readOnly: true + type: string + x-go-name: CreatedBy + header: + description: 此标头过滤规则匹配的 HTTP 标头。 + example: User-Agent + type: string + x-go-name: Header + id: + description: 此标头过滤规则的 ID。 + example: 01FBW21XJA09XYX51KV5JVBW0F + readOnly: true + type: string + x-go-name: ID + regex: + description: 此标头过滤规则的正则表达式。 + example: .*Firefox.* + type: string + x-go-name: Regex + title: HeaderFilter + description: HeaderFilter 表示应用于特定 HTTP 标头的正则过滤规则(允许/阻止)。 + type: object + x-go-name: HeaderFilter + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + hostmeta: + description: 'HostMeta 表示一份 hostmeta 文档。参见: https://www.rfc-editor.org/rfc/rfc6415.html#section-3' + properties: + Link: + items: + $ref: '#/definitions/Link' + type: array + XMLNS: + type: string + XMLName: {} + title: HostMeta + type: object + x-go-name: HostMeta + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceConfigurationAccounts: + properties: + allow_custom_css: + description: 此实例是否允许账户上传用于资料页和贴文页的自定义 CSS。 + example: false + type: boolean + x-go-name: AllowCustomCSS + max_featured_tags: + description: |- + 此实例允许每个账户设置的最大特色标签数。 + format: int64 + type: integer + x-go-name: MaxFeaturedTags + max_profile_fields: + description: |- + 此实例允许每个账户设置的最大附加字段数。 + 目前不可配置,硬编码为 6。详见 (https://github.com/superseriousbusiness/gotosocial/issues/1876) + format: int64 + type: integer + x-go-name: MaxProfileFields + title: InstanceConfigurationAccounts + description: InstanceConfigurationAccounts 是实例账户配置参数的抽象模型。 + type: object + x-go-name: InstanceConfigurationAccounts + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceConfigurationMediaAttachments: + properties: + image_matrix_limit: + description: |- + 图像的最大尺寸,以像素为单位,高度*宽度。 + + GtS 不对此设置限制,但为了保持兼容性,我们在这里给出 Mastodon 的 4096x4096px 值。 + example: 16777216 + format: int64 + type: integer + x-go-name: ImageMatrixLimit + image_size_limit: + description: 图像的最大大小,以字节为单位。 + example: 2097152 + format: int64 + type: integer + x-go-name: ImageSizeLimit + supported_mime_types: + description: 此实例支持上传的 MIME 类型列表。 + example: + - image/jpeg + - image/gif + items: + type: string + type: array + x-go-name: SupportedMimeTypes + video_frame_rate_limit: + description: 此实例支持的最大视频帧率。 + example: 60 + format: int64 + type: integer + x-go-name: VideoFrameRateLimit + video_matrix_limit: + description: |- + 此实例支持的最大视频尺寸,以像素为单位,高度*宽度。 + + GtS 不对此设置限制,但为了保持兼容性,我们在这里给出 Mastodon 的 4096x4096px 值。 + example: 16777216 + format: int64 + type: integer + x-go-name: VideoMatrixLimit + video_size_limit: + description: 视频的最大大小,以字节为单位。 + example: 10485760 + format: int64 + type: integer + x-go-name: VideoSizeLimit + title: InstanceConfigurationMediaAttachments + description: InstanceConfigurationMediaAttachments 是实例媒体附件配置参数的抽象模型。 + type: object + x-go-name: InstanceConfigurationMediaAttachments + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceConfigurationPolls: + properties: + max_characters_per_option: + description: 投票选项中允许的最大字符数。 + example: 50 + format: int64 + type: integer + x-go-name: MaxCharactersPerOption + max_expiration: + description: 投票的最大时长,以秒为单位。 + example: 2629746 + format: int64 + type: integer + x-go-name: MaxExpiration + max_options: + description: 此实例允许的最大投票选项数。 + example: 4 + format: int64 + type: integer + x-go-name: MaxOptions + min_expiration: + description: 投票的最小时长,以秒为单位。 + example: 300 + format: int64 + type: integer + x-go-name: MinExpiration + title: InstanceConfigurationPolls + description: InstanceConfigurationPolls 是实例投票配置参数的抽象模型。 + type: object + x-go-name: InstanceConfigurationPolls + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceConfigurationStatuses: + properties: + characters_reserved_per_url: + description: 客户端应假设 URL 占用的字符数。 + example: 25 + format: int64 + type: integer + x-go-name: CharactersReservedPerURL + max_characters: + description: 此实例允许的最大贴文长度,以字符为单位。 + example: 5000 + format: int64 + type: integer + x-go-name: MaxCharacters + max_media_attachments: + description: 此实例允许的最大媒体附件数。 + example: 4 + format: int64 + type: integer + x-go-name: MaxMediaAttachments + supported_mime_types: + description: 此实例的贴文可用的 MIME 类型列表。 + example: + - text/plain + - text/markdown + items: + type: string + type: array + x-go-name: SupportedMimeTypes + title: InstanceConfigurationStatuses + description: InstanceConfigurationStatuses 是实例贴文配置参数的抽象模型。 + type: object + x-go-name: InstanceConfigurationStatuses + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceRule: + properties: + id: + type: string + x-go-name: ID + text: + type: string + x-go-name: Text + title: InstanceRule + description: InstanceRule 表示一条实例规则。 + type: object + x-go-name: InstanceRule + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceV1: + properties: + account_domain: + description: |- + 此实例上的账户的域名。 + 这不一定与 URI 的 Host 部分相同。 + example: example.org + type: string + x-go-name: AccountDomain + approval_required: + description: 新账户注册需要管理员批准。 + type: boolean + x-go-name: ApprovalRequired + configuration: + $ref: '#/definitions/instanceV1Configuration' + contact_account: + $ref: '#/definitions/account' + debug: + description: 此实例是否在 DEBUG 模式下运行。如果为 false,则省略。 + type: boolean + x-go-name: Debug + description: + description: |- + 此实例的描述。 + + 应为 HTML 格式,但可能是纯文本。 + + 这应该显示在实例的“关于”页面上。 + type: string + x-go-name: Description + description_text: + description: 描述的原始(未解析)版本。 + type: string + x-go-name: DescriptionText + email: + description: 用于咨询的电子邮件地址。 + example: social@example.org + type: string + x-go-name: Email + invites_enabled: + description: 此实例是否允许邀请注册。 + type: boolean + x-go-name: InvitesEnabled + languages: + description: 此实例的主要语言。 + example: + - zh + items: + type: string + type: array + x-go-name: Languages + max_toot_chars: + description: |- + 此实例允许的最大贴文长度,以字符为单位。 + + 这是为了与 Tusky 和其他应用程序兼容而提供的。 + example: 5000 + format: uint64 + type: integer + x-go-name: MaxTootChars + registrations: + description: 此实例上启用了新账户注册。 + type: boolean + x-go-name: Registrations + rules: + description: 此实例的规则的条目化列表。 + items: + $ref: '#/definitions/instanceRule' + type: array + x-go-name: Rules + short_description: + description: |- + 此实例的简要描述。 + + 应为 HTML 格式,但可能是纯文本。 + + 这应该显示在实例的首页上。 + type: string + x-go-name: ShortDescription + short_description_text: + description: 短描述的原始(未解析)版本。 + type: string + x-go-name: ShortDescriptionText + stats: + additionalProperties: + format: int64 + type: integer + description: |- + 此实例的统计信息:贴文数、账户数等。 + 值为指针,因为我们不希望在通过 Web 模板渲染统计信息时跳过 0 值。 + type: object + x-go-name: Stats + terms: + description: 此实例的条款与条件。 + type: string + x-go-name: Terms + terms_text: + description: 条款与条件的原始(未解析)版本。 + type: string + x-go-name: TermsRaw + thumbnail: + description: 此实例的头像/横幅图像的 URL。 + example: https://example.org/files/instance/thumbnail.jpeg + type: string + x-go-name: Thumbnail + thumbnail_description: + description: 此实例的头像描述。 + example: 一张可爱的卡通小懒 + type: string + x-go-name: ThumbnailDescription + thumbnail_static: + description: 此实例头像/横幅图的静态版本的 URL。 + example: https://example.org/files/instance/static/thumbnail.webp + type: string + x-go-name: ThumbnailStatic + thumbnail_static_type: + description: 此实例的头像/横幅图的静态版本的 MIME 类型。 + example: image/webp + type: string + x-go-name: ThumbnailStaticType + thumbnail_type: + description: 此实例头像的 MIME 类型。 + example: image/png + type: string + x-go-name: ThumbnailType + title: + description: 实例的标题。 + example: GoToSocial 演示实例 + type: string + x-go-name: Title + uri: + description: 实例的 URI。 + example: https://gts.example.org + type: string + x-go-name: URI + urls: + $ref: '#/definitions/instanceV1URLs' + version: + description: |- + 此实例部署的 GoToSocial 版本。 + + 至少包含一个语义版本号。 + + 它还可能包含当前软件的 git 提交短 ID。 + example: 0.1.1 cb85f65 + type: string + x-go-name: Version + title: InstanceV1 + description: InstanceV1 是关于此实例的信息的抽象模型。 + type: object + x-go-name: InstanceV1 + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceV1Configuration: + properties: + accounts: + $ref: '#/definitions/instanceConfigurationAccounts' + emojis: + $ref: '#/definitions/InstanceConfigurationEmojis' + media_attachments: + $ref: '#/definitions/instanceConfigurationMediaAttachments' + oidc_enabled: + description: 若实例使用 OIDC 作为身份验证/身份后端,则为 true,否则省略。 + type: boolean + x-go-name: OIDCEnabled + polls: + $ref: '#/definitions/instanceConfigurationPolls' + statuses: + $ref: '#/definitions/instanceConfigurationStatuses' + title: InstanceV1Configuration + description: InstanceV1Configuration 是实例配置参数的抽象模型。 + type: object + x-go-name: InstanceV1Configuration + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceV1URLs: + properties: + streaming_api: + description: 用于贴文和通知的流式传输的 Websockets 地址。 + example: wss://example.org + type: string + x-go-name: StreamingAPI + title: InstanceV1URLs + description: InstanceV1URLs 是客户端应用程序使用的与实例相关的 URL 的抽象模型。 + type: object + x-go-name: InstanceV1URLs + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceV2: + properties: + account_domain: + description: |- + 此实例上的账户的域名。 + 这不一定与 URI 的 Host 部分相同。 + example: example.org + type: string + x-go-name: AccountDomain + configuration: + $ref: '#/definitions/instanceV2Configuration' + contact: + $ref: '#/definitions/instanceV2Contact' + debug: + description: 此实例是否在 DEBUG 模式下运行。如果为 false,则省略。 + type: boolean + x-go-name: Debug + description: + description: |- + 此实例的描述。 + + 应为 HTML 格式,但可能是纯文本。 + + 这应该显示在实例的“关于”页面上。 + type: string + x-go-name: Description + description_text: + description: 描述的原始(未解析)版本。 + type: string + x-go-name: DescriptionText + domain: + description: 实例的域名。 + example: gts.example.org + type: string + x-go-name: Domain + languages: + description: 实例和实例站务/管理员的主要语言。 + example: + - zh + items: + type: string + type: array + x-go-name: Languages + registrations: + $ref: '#/definitions/instanceV2Registrations' + rules: + description: 该实例的规则的条目化列表。 + items: + $ref: '#/definitions/instanceRule' + type: array + x-go-name: Rules + source_url: + description: 本实例部署的软件的源代码 URL,应 AGPL 许可要求提供。 + example: https://github.com/superseriousbusiness/gotosocial + type: string + x-go-name: SourceURL + terms: + description: 本实例的账户条款与条件。 + type: string + x-go-name: Terms + terms_text: + description: 条款与条件的原始(未解析)版本。 + type: string + x-go-name: TermsText + thumbnail: + $ref: '#/definitions/instanceV2Thumbnail' + title: + description: 实例的标题。 + example: GoToSocial 演示实例 + type: string + x-go-name: Title + usage: + $ref: '#/definitions/instanceV2Usage' + version: + description: |- + 此实例部署的 GoToSocial 版本。 + + 至少包含一个语义版本号。 + + 它还可能包含当前软件的 git 提交短 ID。 + example: 0.1.1 cb85f65 + type: string + x-go-name: Version + title: InstanceV2 + description: InstanceV2 是关于此实例的信息的抽象模型。 + type: object + x-go-name: InstanceV2 + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceV2Configuration: + properties: + accounts: + $ref: '#/definitions/instanceConfigurationAccounts' + emojis: + $ref: '#/definitions/InstanceConfigurationEmojis' + media_attachments: + $ref: '#/definitions/instanceConfigurationMediaAttachments' + oidc_enabled: + description: 若实例使用 OIDC 作为身份验证/身份后端,则为 true,否则省略。 + type: boolean + x-go-name: OIDCEnabled + polls: + $ref: '#/definitions/instanceConfigurationPolls' + statuses: + $ref: '#/definitions/instanceConfigurationStatuses' + translation: + $ref: '#/definitions/instanceV2ConfigurationTranslation' + urls: + $ref: '#/definitions/instanceV2URLs' + title: InstanceV2Configuration + description: 此实例的配置值和限制。 + type: object + x-go-name: InstanceV2Configuration + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceV2ConfigurationTranslation: + properties: + enabled: + description: |- + 此实例是否支持翻译 API。 + 由于尚未实现,因此此值始终为 false。 + type: boolean + x-go-name: Enabled + title: InstanceV2ConfigurationTranslation + description: 关于翻译功能的提示。 + type: object + x-go-name: InstanceV2ConfigurationTranslation + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceV2Contact: + properties: + account: + $ref: '#/definitions/account' + email: + description: |- + 可用于咨询和问题反馈的电子邮件地址。 + 如果未设置电子邮件地址,则为空字符串。 + example: someone@example.org + type: string + x-go-name: Email + title: InstanceV2Contact + description: 此实例的联系信息。 + type: object + x-go-name: InstanceV2Contact + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceV2Registrations: + properties: + approval_required: + description: 此实例是否需要管理员批准新账户注册。 + example: true + type: boolean + x-go-name: ApprovalRequired + enabled: + description: 是否开放注册。 + example: false + type: boolean + x-go-name: Enabled + message: + description: |- + 一条自定义消息(HTML 字符串),用于在关闭注册时显示。 + example:

由于骚扰猖獗,example.org 目前关闭了注册,请联系管理员以获取帮助。

+ type: string + x-go-name: Message + title: InstanceV2Registrations + description: 此实例有关注册的信息。 + type: object + x-go-name: InstanceV2Registrations + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceV2Thumbnail: + properties: + blurhash: + description: |- + 通过 BlurHash 算法计算的哈希,用于在媒体尚未下载时生成彩色预览缩略图。 + example: UeKUpFxuo~R%0nW;WCnhF6RjaJt757oJodS$ + type: string + x-go-name: Blurhash + static_url: + description: 缩略图图像的静态版本的 URL。 + example: https://example.org/fileserver/01BPSX2MKCRVMD4YN4D71G9CP5/attachment/static/01H88X0KQ2DFYYDSWYP93VDJZA.webp + type: string + x-go-name: StaticURL + thumbnail_description: + description: |- + 实例缩略图的描述。 + 如果没有描述可用,则不设置键/值。 + example: 一只可爱的小懒 + type: string + x-go-name: Description + thumbnail_static_type: + description: |- + 实例缩略图的静态版本的 MIME 类型。 + 如果类型未知,则不设置键/值。 + example: image/png + type: string + x-go-name: StaticType + thumbnail_type: + description: |- + 实例缩略图的 MIME 类型。 + 如果类型未知,则不设置键/值。 + example: image/png + type: string + x-go-name: Type + url: + description: 缩略图的 URL。 + example: https://example.org/fileserver/01BPSX2MKCRVMD4YN4D71G9CP5/attachment/original/01H88X0KQ2DFYYDSWYP93VDJZA.png + type: string + x-go-name: URL + versions: + $ref: '#/definitions/instanceV2ThumbnailVersions' + title: InstanceV2Thumbnail + description: 代表此实例的图像。 + type: object + x-go-name: InstanceV2Thumbnail + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceV2ThumbnailVersions: + properties: + '@1x': + description: |- + 此缩略图的 1 倍分辨率缩放版本的 URL。 + 如果未提供此缩放版本,则不设置键/值。 + type: string + x-go-name: Size1URL + '@2x': + description: |- + 此缩略图的 2 倍分辨率缩放版本的 URL。 + 如果未提供此缩放版本,则不设置键/值。 + type: string + x-go-name: Size2URL + title: InstanceV2ThumbnailVersions + description: 缩略图的高分辨率版本,用于高 DPI 屏幕。 + type: object + x-go-name: InstanceV2ThumbnailVersions + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceV2URLs: + properties: + streaming: + description: 用于流式传输贴文和通知的 Websockets 地址。 + example: wss://example.org + type: string + x-go-name: Streaming + title: InstanceV2URLs + description: InstanceV2URLs 是客户端应用程序使用的与实例相关的 URL 的抽象模型。 + type: object + x-go-name: InstanceV2URLs + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceV2Usage: + properties: + users: + $ref: '#/definitions/instanceV2Users' + title: InstanceV2Usage + description: InstanceV2Usage 是关于此实例的使用数据的抽象模型。 + type: object + x-go-name: InstanceV2Usage + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceV2Users: + properties: + active_month: + description: |- + 此实例过去 4 周内的活跃用户数。 + 目前未实现:将始终为 0。 + example: 0 + format: int64 + type: integer + x-go-name: ActiveMonth + title: InstanceV2Users + description: 此实例的用户使用数据。 + type: object + x-go-name: InstanceV2Users + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + interactionPolicy: + properties: + can_favourite: + $ref: '#/definitions/interactionPolicyRules' + can_reblog: + $ref: '#/definitions/interactionPolicyRules' + can_reply: + $ref: '#/definitions/interactionPolicyRules' + title: InteractionPolicy + description: 某条贴文的互动规则。 + type: object + x-go-name: InteractionPolicy + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + interactionPolicyRules: + properties: + always: + description: 始终可以执行此类互动的账户的规则条目。 + items: + $ref: '#/definitions/interactionPolicyValue' + type: array + x-go-name: Always + with_approval: + description: 执行此类互动需要批准的账户的规则条目。 + items: + $ref: '#/definitions/interactionPolicyValue' + type: array + x-go-name: WithApproval + title: InteractionPolicyRules + description: 某类互动的规则。 + type: object + x-go-name: PolicyRules + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + interactionPolicyValue: + description: |- + 一条贴文的互动规则条目。 + 可以是以下内部关键字之一,也可以是一个完整的 ActivityPub Actor URI,如 "https://example.org/users/some_user"。 + + 内部关键字: + + public - 公开,即任何可以根据其可见性级别看到此贴文的人。 + followers - 贴文作者的粉丝。 + following - 贴文作者关注的人。 + mutuals - 贴文作者的互相关注(保留,未使用)。 + mentioned - 在贴文中提到的账户,或者在贴文中回复的账户。 + author - 贴文作者本人。 + me - 如果请求是由授权用户发出的,则 "me" 代表发出请求的、现在正在查询此互动策略的用户。 + title: InteractionPolicyValue + type: string + x-go-name: PolicyValue + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + interactionRequest: + properties: + accepted_at: + description: The timestamp that the interaction request was accepted (ISO 8601 Datetime). Field omitted if request not accepted (yet). 互动请求被接受的时间戳 (ISO 8601 Datetime)。如果请求尚未被接受,则省略此字段。 + type: string + x-go-name: AcceptedAt + account: + $ref: '#/definitions/account' + created_at: + description: 互动请求的时间戳 (ISO 8601 Datetime) + type: string + x-go-name: CreatedAt + id: + description: 互动请求在数据库中的 ID。 + type: string + x-go-name: ID + rejected_at: + description: 互动请求被拒绝的时间戳 (ISO 8601 Datetime)。如果请求尚未被拒绝,则省略此字段。 + type: string + x-go-name: RejectedAt + reply: + $ref: '#/definitions/status' + status: + $ref: '#/definitions/status' + type: + description: |- + 该互动请求所涉及的互动类型。 + + `favourite` - 有人点赞了一条贴文。 + `reply` - 有人回复了一条贴文。 + `reblog` - 有人转发了一条贴文。 + type: string + x-go-name: Type + uri: + description: 接受或拒绝的 URI。仅在设置了 accepted_at 或 rejected_at 时设置,否则省略。 + type: string + x-go-name: URI + title: InteractionRequest + description: InteractionRequest 表示待处理、已批准或已拒绝的点赞、回复或转发互动。 + type: object + x-go-name: InteractionRequest + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + list: + properties: + exclusive: + description: |- + 此列表是否为单独列表。 + 如果为 true,则在主页时间线中隐藏此列表的成员的贴文。 + type: boolean + x-go-name: Exclusive + id: + description: 列表的 ID。 + type: string + x-go-name: ID + replies_policy: + description: |- + 此列表的回复显示规则。 + followed = 显示任何对已关注用户的回复 + list = 显示对列表成员的回复 + none = 不显示任何回复 + type: string + x-go-name: RepliesPolicy + title: + description: 用户设定的列表名称。 + type: string + x-go-name: Title + title: List + description: List 表示用户为关注的账户创建的列表。 + type: object + x-go-name: List + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + markers: + properties: + home: + $ref: '#/definitions/TimelineMarker' + notifications: + $ref: '#/definitions/TimelineMarker' + title: Marker + description: Marker 表示用户时间线中的上次阅读位置。 + type: object + x-go-name: Marker + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + mediaDimensions: + properties: + aspect: + description: |- + 媒体附件的宽高比。 + 等于宽度 / 高度。 + example: 1.777777778 + format: float + type: number + x-go-name: Aspect + bitrate: + description: 媒体附件的比特率,以每秒位数为单位。 + example: 1000000 + format: uint64 + type: integer + x-go-name: Bitrate + duration: + description: |- + 媒体的时长(秒)。 + 仅对视频和音频设置。 + example: 5.43 + format: float + type: number + x-go-name: Duration + frame_rate: + description: |- + 媒体的帧率。 + 仅对视频和 GIF 设置。 + example: "30" + type: string + x-go-name: FrameRate + height: + description: |- + 媒体的高度(像素)。 + 对于音频不设置。 + example: 1080 + format: int64 + type: integer + x-go-name: Height + size: + description: |- + 媒体的尺寸,格式为 `[width]x[height]`。 + 对于音频不设置。 + example: 1920x1080 + type: string + x-go-name: Size + width: + description: |- + 媒体的宽度(像素)。 + 对于音频不设置。 + example: 1920 + format: int64 + type: integer + x-go-name: Width + title: MediaDimensions + description: MediaDimensions 是一份媒体附件的详细属性的抽象模型。 + type: object + x-go-name: MediaDimensions + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + mediaFocus: + properties: + x: + description: |- + 焦点的 x 轴位置,应在 -1 和 1 之间 + format: float + type: number + x-go-name: X + "y": + description: |- + 焦点的 y 轴位置,应在 -1 和 1 之间 + format: float + type: number + x-go-name: "Y" + title: MediaFocus + description: MediaFocus 是媒体附件的焦点的抽象模型。 + type: object + x-go-name: MediaFocus + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + mediaMeta: + description: MediaMeta 是媒体附件的元数据的抽象模型。这可以是有关图像、音频文件、视频等的元数据。 + properties: + focus: + $ref: '#/definitions/mediaFocus' + original: + $ref: '#/definitions/mediaDimensions' + small: + $ref: '#/definitions/mediaDimensions' + title: MediaMeta + type: object + x-go-name: MediaMeta + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + mutedAccount: + properties: + acct: + description: |- + 通过 webfinger 发现的账户 URI。 + 对于本地用户,等于用户名;对于远程用户,等于用户名@域名。 + example: some_user@example.org + type: string + x-go-name: Acct + avatar: + description: 该账户的头像的 Web 地址。 + example: https://example.org/media/some_user/avatar/original/avatar.jpeg + type: string + x-go-name: Avatar + avatar_description: + description: 该账户的头像描述,用于 alt 文本。 + example: 一张微笑的树懒的可爱图画。 + type: string + x-go-name: AvatarDescription + avatar_media_id: + description: |- + 此账户的头像对应的媒体附件的数据库 ID。 + 如果此账户没有上传头像(即默认头像),则省略。 + example: 01JAJ3XCD66K3T99JZESCR137W + type: string + x-go-name: AvatarMediaID + avatar_static: + description: |- + 账户头像的静态版本的 Web 地址。 + 仅在账户的主头像是视频或 gif 时才有用。 + example: https://example.org/media/some_user/avatar/static/avatar.png + type: string + x-go-name: AvatarStatic + bot: + description: 账户被标记为机器人。 + type: boolean + x-go-name: Bot + created_at: + description: 该账户创建的时间 (ISO 8601 Datetime)。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: CreatedAt + custom_css: + description: 渲染此账户的资料页或贴文页时包含的自定义 CSS。 + type: string + x-go-name: CustomCSS + discoverable: + description: 账户选择加入发现功能。 + type: boolean + x-go-name: Discoverable + display_name: + description: 账户的昵称。 + example: big jeff (he/him) + type: string + x-go-name: DisplayName + emojis: + description: |- + 账户的简介或显示名称中使用的自定义表情符号的数组。 + 对于被屏蔽的账户为空。 + items: + $ref: '#/definitions/emoji' + type: array + x-go-name: Emojis + enable_rss: + description: |- + 账户已启用 RSS 订阅。 + 如果为 false,则省略键/值。 + type: boolean + x-go-name: EnableRSS + fields: + description: |- + 账户资料中的附加元数据。 + 对于被屏蔽的账户为空。 + items: + $ref: '#/definitions/field' + type: array + x-go-name: Fields + followers_count: + description: 该账户的粉丝数,数据来源于本站实例。 + format: int64 + type: integer + x-go-name: FollowersCount + following_count: + description: 该账户关注的账户数,数据来源于本站实例。 + format: int64 + type: integer + x-go-name: FollowingCount + header: + description: 账户资料卡横幅背景图的 Web 地址。 + example: https://example.org/media/some_user/header/original/header.jpeg + type: string + x-go-name: Header + header_description: + description: 账户资料卡横幅背景图的描述,用于 alt 文本。 + example: 花开富贵。 + type: string + x-go-name: HeaderDescription + header_media_id: + description: |- + 此账户的资料卡横幅背景图片对应的媒体附件的数据库 ID。 + 如果此账户没有上传资料卡横幅背景图(即使用默认横幅背景),则省略。 + example: 01JAJ3XCD66K3T99JZESCR137W + type: string + x-go-name: HeaderMediaID + header_static: + description: |- + 账户资料卡横幅背景图的静态版本的 Web 地址。 + 仅在账户的主横幅是视频或 gif 时才有用。 + example: https://example.org/media/some_user/header/static/header.png + type: string + x-go-name: HeaderStatic + hide_collections: + description: |- + 账户选择隐藏他们的粉丝/关注数据。 + 如果为 false,则省略键/值。 + type: boolean + x-go-name: HideCollections + id: + description: 账户 ID。 + example: 01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: ID + last_status_at: + description: 账户上次发帖的时间 (ISO 8601 Date)。 + example: "2021-07-30" + type: string + x-go-name: LastStatusAt + locked: + description: 账户手动批准关注请求。 + type: boolean + x-go-name: Locked + moved: + $ref: '#/definitions/account' + mute_expires_at: + description: |- + 如果此账户被静音/隐藏,静音/隐藏将在何时到期 (ISO 8601 Datetime)。 + 如果静音/隐藏是永久的,值将为 null。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: MuteExpiresAt + note: + description: 该账户的简介/描述。 + type: string + x-go-name: Note + role: + $ref: '#/definitions/accountRole' + roles: + description: |- + Roles 列出了账户在本实例上的公开身份。 + 与 Role 不同,此属性始终可用的,但从不包含权限细节。 + items: + $ref: '#/definitions/accountDisplayRole' + type: array + x-go-name: Roles + source: + $ref: '#/definitions/Source' + statuses_count: + description: 该账户发布的贴文数,数据来源于本站实例。 + format: int64 + type: integer + x-go-name: StatusesCount + suspended: + description: 账户已被本站实例封禁。 + type: boolean + x-go-name: Suspended + theme: + description: 用户选择的 CSS 主题的文件名,用于在渲染此账户的资料页或贴文页时包含。例如,`blurple-light.css`。 + type: string + x-go-name: Theme + url: + description: 该账户资料页的 Web 地址。 + example: https://example.org/@some_user + type: string + x-go-name: URL + username: + description: 账户的用户名,不包括域名。 + example: some_user + type: string + x-go-name: Username + title: MutedAccount + description: MutedAccount 扩展了 Account,其中包含仅静音/隐藏用户列表使用的字段。 + type: object + x-go-name: MutedAccount + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + nodeinfo: + description: 'NodeInfo 表示版本 2.1 或版本 2.0 的 nodeinfo 数据结构。参见: https://nodeinfo.diaspora.software/schema.html' + properties: + metadata: + additionalProperties: {} + description: 自由形式的键值对,用于软件特定值。客户端不应依赖于任何特定的键存在。 + type: object + x-go-name: Metadata + openRegistrations: + description: 此服务器是否开放自主注册。 + example: false + type: boolean + x-go-name: OpenRegistrations + protocols: + description: 服务器支持的协议。 + items: + type: string + type: array + x-go-name: Protocols + services: + $ref: '#/definitions/NodeInfoServices' + software: + $ref: '#/definitions/NodeInfoSoftware' + usage: + $ref: '#/definitions/NodeInfoUsage' + version: + description: NodeInfo 数据结构版本。 + example: "2.0" + type: string + x-go-name: Version + title: Nodeinfo + type: object + x-go-name: Nodeinfo + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + notification: + properties: + account: + $ref: '#/definitions/account' + created_at: + description: 通知的时间戳 (ISO 8601 Datetime) + type: string + x-go-name: CreatedAt + id: + description: 通知在数据库中的 ID。 + type: string + x-go-name: ID + status: + $ref: '#/definitions/status' + type: + description: |- + 触发通知的事件类型。 + follow = 有人关注了你。`account` 将被设置。 + follow_request = 有人请求关注你。`account` 将被设置。 + mention = 有人在他们的贴文中提到了你。`status` 将被设置。`account` 将被设置。 + reblog = 有人转发了你的一条贴文。`status` 将被设置。`account` 将被设置。 + favourite = 有人点赞了你的一条贴文。`status` 将被设置。`account` 将被设置。 + poll = 你参与或创建的一次投票已结束。`status` 将被设置。`account` 将被设置。 + status = 你设置的接收发帖通知的某个账户发布了一条贴文。`status` 将被设置。`account` 将被设置。 + admin.sign_up = 有人在实例上注册了一个新账户。`account` 将被设置。 + type: string + x-go-name: Type + title: Notification + description: Notification 表示与用户相关的事件的通知。 + type: object + x-go-name: Notification + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + oauthToken: + properties: + access_token: + description: 用于身份验证的访问令牌。 + type: string + x-go-name: AccessToken + created_at: + description: 生成 OAuth 令牌的时间 (UNIX 时间戳格式的秒)。 + example: 1627644520 + format: int64 + type: integer + x-go-name: CreatedAt + scope: + description: 此令牌被授予的 OAuth 作用域,以空格分隔。 + example: read write admin + type: string + x-go-name: Scope + token_type: + description: OAuth 令牌类型。将始终为 'Bearer'。 + example: bearer + type: string + x-go-name: TokenType + title: Token + description: Token 表示用于 GoToSocial API 身份验证和执行操作的 OAuth 令牌。 + type: object + x-go-name: Token + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + poll: + properties: + emojis: + description: 渲染投票选项时使用的自定义表情符号。 + items: + $ref: '#/definitions/emoji' + type: array + x-go-name: Emojis + expired: + description: 投票是否已结束? + type: boolean + x-go-name: Expired + expires_at: + description: 投票结束时间 (ISO 8601 Datetime)。 + type: string + x-go-name: ExpiresAt + id: + description: 数据库中投票的 ID。 + example: 01FBYKMD1KBMJ0W6JF1YZ3VY5D + type: string + x-go-name: ID + multiple: + description: 投票是否允许多选? + type: boolean + x-go-name: Multiple + options: + description: 投票的选项。 + items: + $ref: '#/definitions/pollOption' + type: array + x-go-name: Options + own_votes: + description: |- + 当使用用户令牌调用时,令牌关联的授权用户选择了哪些选项?包含选项的索引值数组。 + + 未提供用户令牌时省略。 + items: + format: int64 + type: integer + type: array + x-go-name: OwnVotes + voted: + description: |- + 当使用用户令牌调用时,令牌关联的授权用户是否已投票? + + 未提供用户令牌时省略。 + type: boolean + x-go-name: Voted + voters_count: + description: 多选投票中有多少个独立账户投票。 + format: int64 + type: integer + x-go-name: VotersCount + votes_count: + description: 收到了多少票。 + format: int64 + type: integer + x-go-name: VotesCount + title: Poll + description: Poll 表示一个附加到贴文的投票。 + type: object + x-go-name: Poll + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + pollOption: + properties: + title: + description: 投票选项的文本值。格式为字符串。 + type: string + x-go-name: Title + votes_count: + description: 该选项收到的票数。 + format: int64 + type: integer + x-go-name: VotesCount + title: PollOption + description: PollOption 表示不同投票选项的当前投票计数。 + type: object + x-go-name: PollOption + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + report: + properties: + action_taken: + description: 管理员是否已对此举报采取行动。 + example: false + type: boolean + x-go-name: ActionTaken + action_taken_at: + description: |- + 若已采取行动,在何时采取行动?(ISO 8601 Datetime) + 如果未设置/尚未采取行动,则为null。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: ActionTakenAt + action_taken_comment: + description: |- + 若已采取行动,管理员在采取行动时发表了什么评论? + 如果未设置/尚未采取行动,则为null。 + example: 账户已被封禁。 + type: string + x-go-name: ActionTakenComment + category: + description: 此举报是在哪个类别下创建的? + example: spam + type: string + x-go-name: Category + comment: + description: |- + 提交举报时附带的评论。 + 如果未提交评论,则为空。 + example: 此人一直在骚扰我。 + type: string + x-go-name: Comment + created_at: + description: 举报被创建的时间 (ISO 8601 Datetime)。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: CreatedAt + forwarded: + description: 用于指示是否应将举报抄送到外站实例的布尔值。 + example: true + type: boolean + x-go-name: Forwarded + id: + description: 举报的 ID。 + example: 01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: ID + rule_ids: + description: |- + 与该举报一并提交的实例规则的 ID 数组。 + 如果未提交规则 ID,则为空。 + example: + - 01GPBN5YDY6JKBWE44H7YQBDCQ + - 01GPBN65PDWSBPWVDD0SQCFFY3 + items: + type: string + type: array + x-go-name: RuleIDs + status_ids: + description: |- + 与此举报一起提交的贴文的 ID 数组。 + 如果未提交贴文 ID,则为空。 + example: + - 01GPBN5YDY6JKBWE44H7YQBDCQ + - 01GPBN65PDWSBPWVDD0SQCFFY3 + items: + type: string + type: array + x-go-name: StatusIDs + target_account: + $ref: '#/definitions/account' + title: Report + description: Report 是提交给实例的一份举报的抽象模型,可以通过客户端 API 或联合 API 提交。 + type: object + x-go-name: Report + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + searchResult: + properties: + accounts: + items: + $ref: '#/definitions/account' + type: array + x-go-name: Accounts + hashtags: + description: 若为 API v1,则为字符串切片;若为 API v2,则为话题标签切片。 + items: {} + type: array + x-go-name: Hashtags + statuses: + items: + $ref: '#/definitions/status' + type: array + x-go-name: Statuses + title: SearchResult + description: SearchResult 是一次搜索结果的抽象模型。 + type: object + x-go-name: SearchResult + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + status: + properties: + account: + $ref: '#/definitions/account' + application: + $ref: '#/definitions/application' + bookmarked: + description: 此贴文已被查看的账户收藏。 + type: boolean + x-go-name: Bookmarked + card: + $ref: '#/definitions/card' + content: + description: 此贴文的内容。应为 HTML,但在某些情况下也可能为纯文本。 + example:

你好,我是一条嘟文。

+ type: string + x-go-name: Content + created_at: + description: 此贴文的创建时间 (ISO 8601 Datetime)。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: CreatedAt + emojis: + description: 此贴文内容中使用的自定义表情符号。 + items: + $ref: '#/definitions/emoji' + type: array + x-go-name: Emojis + favourited: + description: 此贴文已被查看它的账户点赞。 + type: boolean + x-go-name: Favourited + favourites_count: + description: 此贴文收到的点赞数,数据来源于本站实例。 + format: int64 + type: integer + x-go-name: FavouritesCount + filtered: + description: 此贴文命中的过滤规则列表,如果有命中的过滤规则,还会包含命中原因。 + items: + $ref: '#/definitions/filterResult' + type: array + x-go-name: Filtered + id: + description: 此贴文的 ID。 + example: 01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: ID + in_reply_to_account_id: + description: 此贴文回复的账户的 ID。 + example: 01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: InReplyToAccountID + in_reply_to_id: + description: 此贴文回复的贴文的 ID。 + example: 01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: InReplyToID + interaction_policy: + $ref: '#/definitions/interactionPolicy' + language: + description: |- + 此贴文的主要语言 (ISO 639 Part 1 两字母语言代码)。当语言未知时将为 null。 + example: en + type: string + x-go-name: Language + local_only: + description: 若贴文不参与联合,则设置为 "true";否则省略。 + type: boolean + x-go-name: LocalOnly + media_attachments: + description: 附于到此贴文的媒体。 + items: + $ref: '#/definitions/attachment' + type: array + x-go-name: MediaAttachments + mentions: + description: 此贴文内容中提及的用户。 + items: + $ref: '#/definitions/Mention' + type: array + x-go-name: Mentions + muted: + description: 此贴文下的回复已被查看它的账户静音。 + type: boolean + x-go-name: Muted + pinned: + description: 此贴文已被查看它的账户置顶(仅适用于您自己的贴文)。 + type: boolean + x-go-name: Pinned + poll: + $ref: '#/definitions/poll' + reblog: + $ref: '#/definitions/statusReblogged' + reblogged: + description: 此贴文已被查看它的账户转发。 + type: boolean + x-go-name: Reblogged + reblogs_count: + description: 此贴文被转发的次数,数据来源于本站实例。 + format: int64 + type: integer + x-go-name: ReblogsCount + replies_count: + description: 此贴文的回复数,数据来源于本站实例。 + format: int64 + type: integer + x-go-name: RepliesCount + sensitive: + description: 此贴文包含敏感内容。 + example: false + type: boolean + x-go-name: Sensitive + spoiler_text: + description: 此贴文的主题、摘要或内容警告。 + example: NSFW 警告 + type: string + x-go-name: SpoilerText + tags: + description: 贴文内容中使用的话题标签。 + items: + $ref: '#/definitions/tag' + type: array + x-go-name: Tags + text: + description: |- + 贴文的纯文本内容。当贴文被删除时返回,以便用户可以从源文本重新起草,而无需客户端从 HTML 内容中逆向出原始文本。 + type: string + x-go-name: Text + uri: + description: 此贴文的 ActivityPub URI。等同于贴文的 activitypub ID。 + example: https://example.org/users/some_user/statuses/01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: URI + url: + description: 此贴文的公开 Web URL。仅当贴文的可见性为 'public' 时,此链接才有效。 + example: https://example.org/@some_user/statuses/01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: URL + visibility: + description: 此贴文的可见性。 + example: unlisted + type: string + x-go-name: Visibility + title: Status + description: Status 表示一条贴文或帖子。 + type: object + x-go-name: Status + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + statusEdit: + description: |- + StatusEdit 表示一条贴文的一个历史修订版本,包含该修订版本的状态的部分信息。 + properties: + account: + $ref: '#/definitions/account' + content: + description: |- + 本次修订的贴文内容。 + 应为 HTML,但在某些情况下也可能为纯文本。 + example:

这是一条嘟文。

+ type: string + x-go-name: Content + created_at: + description: 此修订版本创建的时间 (ISO 8601 Datetime)。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: CreatedAt + emojis: + description: 渲染此贴文时用到的自定义表情符号。 + items: + $ref: '#/definitions/emoji' + type: array + x-go-name: Emojis + media_attachments: + description: 此贴文随附的媒体。 + items: + $ref: '#/definitions/attachment' + type: array + x-go-name: MediaAttachments + poll: + $ref: '#/definitions/poll' + sensitive: + description: 贴文在本次编辑中被标记为敏感。 + example: false + type: boolean + x-go-name: Sensitive + spoiler_text: + description: 本次编辑中设定的贴文主题、摘要或内容警告。 + example: warning nsfw + type: string + x-go-name: SpoilerText + type: object + x-go-name: StatusEdit + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + statusReblogged: + properties: + account: + $ref: '#/definitions/account' + application: + $ref: '#/definitions/application' + bookmarked: + description: 此贴文已被查看它的账户收藏。 + type: boolean + x-go-name: Bookmarked + card: + $ref: '#/definitions/card' + content: + description: 此贴文的内容。应为 HTML,但在某些情况下也可能为纯文本。 + example:

这是一条嘟文。

+ type: string + x-go-name: Content + created_at: + description: 此贴文的创建时间 (ISO 8601 Datetime)。 + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: CreatedAt + emojis: + description: 渲染此贴文时使用的自定义表情符号。 + items: + $ref: '#/definitions/emoji' + type: array + x-go-name: Emojis + favourited: + description: 此贴文已被查看它的账户点赞。 + type: boolean + x-go-name: Favourited + favourites_count: + description: 此贴文收到的点赞数,数据来源于本站实例。 + format: int64 + type: integer + x-go-name: FavouritesCount + filtered: + description: 此贴文命中的过滤规则列表,如果有命中的过滤规则,还会包含命中原因。 + items: + $ref: '#/definitions/filterResult' + type: array + x-go-name: Filtered + id: + description: 贴文的 ID。 + example: 01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: ID + in_reply_to_account_id: + description: 贴文回复的账户的 ID。 + example: 01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: InReplyToAccountID + in_reply_to_id: + description: 此贴文回复的贴文的 ID。 + example: 01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: InReplyToID + interaction_policy: + $ref: '#/definitions/interactionPolicy' + language: + description: |- + 此贴文的主要语言 (ISO 639 Part 1 两字母语言代码)。 + 当语言未知时将为 null。 + example: zh + type: string + x-go-name: Language + local_only: + description: 若贴文不参与联合,则设置为 "true";否则省略。 + type: boolean + x-go-name: LocalOnly + media_attachments: + description: 随附到此贴文的媒体。 + items: + $ref: '#/definitions/attachment' + type: array + x-go-name: MediaAttachments + mentions: + description: 此贴文内容中提及的用户。 + items: + $ref: '#/definitions/Mention' + type: array + x-go-name: Mentions + muted: + description: 此贴文下的回复已被查看它的账户静音。 + type: boolean + x-go-name: Muted + pinned: + description: 此贴文已被查看它的账户置顶(仅适用于您自己的贴文)。 + type: boolean + x-go-name: Pinned + poll: + $ref: '#/definitions/poll' + reblog: + $ref: '#/definitions/statusReblogged' + reblogged: + description: 此贴文已被查看它的账户转发。 + type: boolean + x-go-name: Reblogged + reblogs_count: + description: 此贴文被转发的次数,数据来源于本站实例。 + format: int64 + type: integer + x-go-name: ReblogsCount + replies_count: + description: 此贴文的回复数,数据来源于本站实例。 + format: int64 + type: integer + x-go-name: RepliesCount + sensitive: + description: 贴文包含敏感内容。 + example: false + type: boolean + x-go-name: Sensitive + spoiler_text: + description: 此贴文的主题、摘要或内容警告。 + example: NSFW + type: string + x-go-name: SpoilerText + tags: + description: 此贴文内容中使用的话题标签。 + items: + $ref: '#/definitions/tag' + type: array + x-go-name: Tags + text: + description: |- + 此贴文的纯文本内容。当贴文被删除时返回,以便用户可以从源文本重新起草,而无需客户端从 HTML 内容中逆向出原始文本。 + type: string + x-go-name: Text + uri: + description: 此贴文的 ActivityPub URI。等同于贴文的 activitypub ID。 + example: https://example.org/users/some_user/statuses/01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: URI + url: + description: 此贴文的公开 Web URL。仅当贴文的可见性为 'public' 时,此链接才有效。 + example: https://example.org/@some_user/statuses/01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: URL + visibility: + description: 此贴文的可见性。 + example: unlisted + type: string + x-go-name: Visibility + title: StatusReblogged + description: StatusReblogged 表示一条被转发的贴文。 + type: object + x-go-name: StatusReblogged + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + statusSource: + description: |- + StatusSource 表示创建贴文时提交给 API 的贴文源文本。 + properties: + id: + description: 贴文的 ID。 + example: 01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: ID + spoiler_text: + description: 贴文折叠提示的纯文本版本。 + type: string + x-go-name: SpoilerText + text: + description: 贴文的源文本。 + type: string + x-go-name: Text + type: object + x-go-name: StatusSource + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + swaggerCollection: + properties: + '@context': + description: |- + ActivityStreams JSON-LD 上下文。 + 字符串或字符串数组,或更复杂的嵌套项。 + example: https://www.w3.org/ns/activitystreams + x-go-name: Context + first: + $ref: '#/definitions/swaggerCollectionPage' + id: + description: ActivityStreams ID. + example: https://example.org/users/some_user/statuses/106717595988259568/replies + type: string + x-go-name: ID + last: + $ref: '#/definitions/swaggerCollectionPage' + type: + description: ActivityStreams 类型。 + example: Collection + type: string + x-go-name: Type + title: SwaggerCollection + description: SwaggerCollection 表示一个 ActivityPub 集合。 + type: object + x-go-name: SwaggerCollection + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users + swaggerCollectionPage: + properties: + id: + description: ActivityStreams ID. + example: https://example.org/users/some_user/statuses/106717595988259568/replies?page=true + type: string + x-go-name: ID + items: + description: 此页面上的条目。 + example: + - https://example.org/users/some_other_user/statuses/086417595981111564 + - https://another.example.com/users/another_user/statuses/01FCN8XDV3YG7B4R42QA6YQZ9R + items: + type: string + type: array + x-go-name: Items + next: + description: 指向下一页的链接。 + example: https://example.org/users/some_user/statuses/106717595988259568/replies?only_other_accounts=true&page=true + type: string + x-go-name: Next + partOf: + description: 此页面所属的集合。 + example: https://example.org/users/some_user/statuses/106717595988259568/replies + type: string + x-go-name: PartOf + type: + description: ActivityStreams 类型。 + example: CollectionPage + type: string + x-go-name: Type + title: SwaggerCollectionPage + description: SwaggerCollectionPage 表示一个 ActivityPub 集合的一页。 + type: object + x-go-name: SwaggerCollectionPage + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users + swaggerFeaturedCollection: + properties: + '@context': + description: |- + ActivityStreams JSON-LD 上下文。 + 字符串或字符串数组,或更复杂的嵌套项。 + example: https://www.w3.org/ns/activitystreams + x-go-name: Context + TotalItems: + description: 此集合中的项目数。 + example: 2 + format: int64 + type: integer + id: + description: ActivityStreams ID. + example: https://example.org/users/some_user/collections/featured + type: string + x-go-name: ID + items: + description: 贴文 URI 的列表。 + example: + - https://example.org/users/some_user/statuses/01GSZ0F7Q8SJKNRF777GJD271R + - https://example.org/users/some_user/statuses/01GSZ0G012CBQ7TEKX689S3QRE + items: + type: string + type: array + x-go-name: Items + type: + description: ActivityStreams 类型。 + example: OrderedCollection + type: string + x-go-name: Type + title: SwaggerFeaturedCollection + description: SwaggerFeaturedCollection 表示一个有序 ActivityPub 集合。 + type: object + x-go-name: SwaggerFeaturedCollection + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users + tag: + properties: + following: + description: |- + Following 在当用户关注此标签时为 true,不关注时为 false,如果没有当前认证用户则省略。 + type: boolean + x-go-name: Following + history: + description: |- + 此话题标签的使用历史。 + 目前只是一个存根,如果提供将始终为空数组。 + example: [] + items: {} + type: array + x-go-name: History + name: + description: '此话题标签的名称,不包含 # 符号。' + example: helloworld + type: string + x-go-name: Name + url: + description: 此话题标签的网页链接。 + example: https://example.org/tags/helloworld + type: string + x-go-name: URL + title: Tag + description: Tag 表示贴文内容中使用的某个话题标签。 + type: object + x-go-name: Tag + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + theme: + properties: + description: + description: 此主题面向用户的描述。 + type: string + x-go-name: Description + file_name: + description: 此主题在主题目录中的文件名。 + type: string + x-go-name: FileName + title: + description: 此主题的用户可见标题。 + type: string + x-go-name: Title + title: Theme + description: Theme 表示一个用户可选的预设 CSS 主题。 + type: object + x-go-name: Theme + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + threadContext: + description: |- + ThreadContext 是围绕给定贴文的贴文树/贴文串的抽象模型。 + properties: + ancestors: + description: 此贴文的祖先。 + items: + $ref: '#/definitions/status' + type: array + x-go-name: Ancestors + descendants: + description: 此贴文的后代。 + items: + $ref: '#/definitions/status' + type: array + x-go-name: Descendants + type: object + x-go-name: ThreadContext + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + user: + properties: + admin: + description: 用户为管理员。 + example: false + type: boolean + x-go-name: Admin + approved: + description: 用户已被管理员批准。 + example: true + type: boolean + x-go-name: Approved + confirmation_sent_at: + description: 上次发送“请确认您的电子邮件地址”电子邮件的时间(如果有的话)。 (ISO 8601 Datetime) + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: ConfirmationSentAt + confirmed_at: + description: 用户填写的电子邮件地址被确认的时间,如果有的话。 (ISO 8601 Datetime) + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: ConfirmedAt + created_at: + description: 用户创建时间。 (ISO 8601 Datetime) + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: CreatedAt + disabled: + description: 用户的帐户已被停用。 + example: false + type: boolean + x-go-name: Disabled + email: + description: 用户确认的电子邮件地址(如果用户设置了电子邮件地址)。 + example: someone@example.org + type: string + x-go-name: Email + id: + description: 此用户的数据库 ID。 + example: 01FBVD42CQ3ZEEVMW180SBX03B + type: string + x-go-name: ID + last_emailed_at: + description: 上次给此用户发送电子邮件的时间,如果有的话。 (ISO 8601 Datetime) + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: LastEmailedAt + moderator: + description: 用户为站务/监察员。 + example: false + type: boolean + x-go-name: Moderator + reason: + description: 用户注册原因(如果用户注册时填写了原因)。 + example: Please! Pretty please! + type: string + x-go-name: Reason + reset_password_sent_at: + description: 上次发送“请重置您的密码”电子邮件的时间(如果有的话)。 (ISO 8601 Datetime) + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: ResetPasswordSentAt + unconfirmed_email: + description: 此用户的未确认电子邮件地址(如果用户设置了电子邮件地址)。 + example: someone.else@somewhere.else.example.org + type: string + x-go-name: UnconfirmedEmail + title: User + description: User 是单个用户的相关字段的抽象模型。 + type: object + x-go-name: User + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + wellKnownResponse: + properties: + aliases: + items: + type: string + type: array + x-go-name: Aliases + links: + items: + $ref: '#/definitions/Link' + type: array + x-go-name: Links + subject: + type: string + x-go-name: Subject + title: WellKnownResponse + description: |- + WellKnownResponse 表示对“acct”资源的 webfinger 请求或对 nodeinfo 请求的响应。 + 例如,访问 https://example.org/.well-known/webfinger?resource=acct:some_username@example.org 时将返回一个此类型的响应。 + 参见 https://webfinger.net/ + type: object + x-go-name: WellKnownResponse + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model +host: example.org +info: + contact: + email: admin@gotosocial.org + name: 全体 GotoSocial 开发者 + license: + name: AGPL3 + url: https://www.gnu.org/licenses/agpl-3.0.zh-cn.html + title: GoToSocial Swagger 文档 + version: 0.17.2-SNAPSHOT-8a93300a +paths: + /.well-known/host-meta: + get: + description: '响应 web 主机元数据查询,返回符合规范的 hostmeta 响应。参见: https://www.rfc-editor.org/rfc/rfc6415.html' + operationId: hostMetaGet + produces: + - application/xrd+xml" + responses: + "200": + description: "" + schema: + $ref: '#/definitions/hostmeta' + summary: 获取 host-meta + tags: + - .well-known + /.well-known/nodeinfo: + get: + description: |- + 返回一个 well-known 响应,将调用者重定向到 `/nodeinfo/2.0`。 + 例如: `{"links":[{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.0","href":"http://example.org/nodeinfo/2.0"}]}` + 参见: https://nodeinfo.diaspora.software/protocol.html + operationId: nodeInfoWellKnownGet + produces: + - application/json + responses: + "200": + description: "" + schema: + $ref: '#/definitions/wellKnownResponse' + summary: 获取 nodeinfo + tags: + - .well-known + /.well-known/webfinger: + get: + description: |- + 处理 webfinger 账户查询请求。 + 例如,对 `https://example.org/.well-known/webfinger?resource=acct:admin@example.org` 发送 GET 请求,将返回: + ``` + {"subject":"acct:admin@example.org","aliases":["https://example.org/users/admin","https://example.org/@admin"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://example.org/@admin"},{"rel":"self","type":"application/activity+json","href":"https://example.org/users/admin"}]} + ``` + 参见: https://webfinger.net/ + operationId: webfingerGet + produces: + - application/jrd+json + responses: + "200": + description: "" + schema: + $ref: '#/definitions/wellKnownResponse' + summary: Webfinger 账户查询 + tags: + - .well-known + /api/{api_version}/media: + post: + consumes: + - multipart/form-data + operationId: mediaCreate + parameters: + - description: 要使用的 API 版本。必须是 `v1` 或 `v2`。 + in: path + name: api_version + required: true + type: string + - description: 媒体附件的 alt 文本描述。这对于使用屏幕阅读器的用户非常有用!根据您的实例设置,可能需要也可能不需要。 + in: formData + name: description + type: string + - default: 0,0 + description: '媒体附件的焦点。如果存在,它应该是两个介于 -1 和 1 之间的逗号分隔的浮点数。例如:`-0.5,0.25`。' + in: formData + name: focus + type: string + - description: 要上传的媒体附件。 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 新创建的媒体附件。 + schema: + $ref: '#/definitions/attachment' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "422": + description: unprocessable 无法处理 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:media + summary: 上传新附件 + description: 上传新的媒体附件。 + tags: + - media + /api/{api_version}/search: + get: + description: 在本站或其他地方搜索贴文、帐户或话题标签。如果结果中包含贴文,则它们将按时间顺序(最新的在前)返回,带有连续的 ID(更大 = 更新)。 + operationId: searchGet + parameters: + - description: 必须是 `v1` 或 `v2`。如果使用 v1,Hashtag 结果将是字符串切片。如果使用 v2,Hashtag 结果将是 apimodel 标签切片。 + in: path + name: api_version + required: true + type: string + - description: 仅返回 *早于* 给定的 max ID 的项目。当前仅在将 type 设置为特定类型时使用。 + in: query + name: max_id + type: string + - description: 仅返回 *晚于且紧邻* 给定的 min ID 的项目。当前仅在将 type 设置为特定类型时使用。 + in: query + name: min_id + type: string + - default: 20 + description: 要返回的每种项目的数量。 + in: query + maximum: 40 + minimum: 1 + name: limit + type: integer + - default: 0 + description: 要返回的结果的页数(从 0 开始)。该参数目前未使用,请通过选择特定的查询类型并使用 maxID 和 minID 来分页。 + in: query + maximum: 10 + minimum: 0 + name: offset + type: integer + - description: |- + 要搜索的关键词。这可以是以下形式之一: + - `@[username]` -- 搜索任何实例上具有给定用户名的帐户。可以返回多个结果。 + - `@[username]@[domain]` -- 精确匹配给定的用户名和实例域名的外站帐户。最多只会返回 1 个结果。 + - `https://example.org/some/arbitrary/url` -- 搜索具有给定 URL 的账户或贴文。最多只会返回 1 个结果。 + - `#[hashtag_name]` -- 搜索具有给定话题标签名称或以给定话题标签名称开头的话题标签。不区分大小写。可以返回多个结果。 + - 任意字符串 -- 搜索包含给定字符串的帐户或贴文。可以返回多个结果。 + + 任意字符串搜索可以包括以下运算符: + - `from:localuser`,`from:remoteuser@instance.tld`:将结果限制为由指定帐户创建的贴文。 + in: query + name: q + required: true + type: string + - description: |- + 要返回的项目类型。可以是以下之一: + - `` -- 空字符串;返回任何/所有结果。 + - `accounts` -- 仅返回帐户。 + - `statuses` -- 仅返回贴文。 + - `hashtags` -- 仅返回话题标签。 + 如果指定了 `type`,则可以使用 max_id 和 min_id 参数进行分页。 + 如果未指定 `type`,使用 `offset` 参数进行分页。 + in: query + name: type + type: string + - default: false + description: 若搜索关键词是 `@[username]@[domain]` 或 URL,则允许 GoToSocial 实例通过外站调用(webfinger、ActivityPub 等)来解析搜索关键词。 + in: query + name: resolve + type: boolean + - default: false + description: 若搜索类型包括帐户,并且搜索关键词是任意字符串,则仅显示请求帐户关注的帐户。如果设置为 `true`,则 GoToSocial 将在用户名和昵称之外同时检索账户信息来增强搜索。 + in: query + name: following + type: boolean + - default: false + description: 若搜索类型包括话题标签,并且搜索关键词是任意字符串,则排除尚未被实例管理员批准的话题标签。目前此参数未使用。 + in: query + name: exclude_unreviewed + type: boolean + - description: 将搜索范围限制到由指定帐户创建的贴文。 + in: query + name: account_id + type: string + produces: + - application/json + responses: + "200": + description: 搜索结果。 + schema: + $ref: '#/definitions/searchResult' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:search + summary: 搜索 + tags: + - search + /api/v1/accounts: + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + description: |- + 使用应用程序令牌创建新帐户。 + 若 Content-Type 为 `application/json`,则参数也可在请求体中以 JSON 形式给出。 + 若 Content-Type 为 `application/xml`,则参数也可在请求体中以 XML 形式给出。 + operationId: accountCreate + parameters: + - description: 若注册需要手动批准,则此文本将由管理员审核。 + in: query + name: reason + type: string + x-go-name: Reason + - description: 账户设置的用户名。 + in: query + name: username + type: string + x-go-name: Username + - description: 用于登录的电子邮件地址。 + in: query + name: email + type: string + x-go-name: Email + - description: 用于登录的密码。将在存储之前进行哈希处理。 + in: query + name: password + type: string + x-go-name: Password + - description: 用户同意实例的条款、条件和政策。 + in: query + name: agreement + type: boolean + x-go-name: Agreement + - description: 确认邮件的语言。 + in: query + name: locale + type: string + x-go-name: Locale + produces: + - application/json + responses: + "200": + description: 为新创建的帐户提供的 OAuth2 访问令牌。 + schema: + $ref: '#/definitions/oauthToken' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "422": + description: unprocessable 无法处理. 你的帐户创建请求无法处理,因为在过去的 24 小时内在此实例上创建了太多帐户,或者待处理的帐户积压已满。 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Application: + - write:accounts + summary: 创建新帐户 + tags: + - accounts + /api/v1/accounts/{id}: + get: + operationId: accountGet + parameters: + - description: 请求的帐户 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的帐户。 + schema: + $ref: '#/definitions/account' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:accounts + summary: 获取账户详情 + description: 获取具有给定 ID 的帐户的信息。 + tags: + - accounts + /api/v1/accounts/{id}/block: + post: + operationId: accountBlock + parameters: + - description: 要屏蔽的帐户 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 你与帐户的关系(relationship)。 + schema: + $ref: '#/definitions/accountRelationship' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:blocks + summary: 屏蔽帐户 + description: 屏蔽具有给定 ID 的帐户。 + tags: + - accounts + /api/v1/accounts/{id}/follow: + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + description: |- + 关注具有给定 ID 的帐户。 + + 若 Content-Type 为 `application/json`,则参数也可在请求体中以 JSON 形式给出。 + 若 Content-Type 为 `application/xml`,则参数也可在请求体中以 XML 形式给出。 + + 若您已经关注了给定的帐户,则关注(请求)将被更新,不会使用 `reblogs` 和 `notify` 参数。 + operationId: accountFollow + parameters: + - description: 要关注的帐户 ID。 + in: path + name: id + required: true + type: string + - default: true + description: 显示此帐户的转发。 + in: formData + name: reblogs + type: boolean + - default: false + description: 该账户发帖时通知。 + in: formData + name: notify + type: boolean + produces: + - application/json + responses: + "200": + description: 你与此帐户的关系(relationship)。 + schema: + $ref: '#/definitions/accountRelationship' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:follows + summary: 关注帐户 + tags: + - accounts + /api/v1/accounts/{id}/followers: + get: + description: |- + 查看关注此帐户的帐户。 + 下一个/上一个查询可以从返回的 Link 标头中解析。 + 例如: + + ``` + ; rel="next", ; rel="prev" + ``` + 若帐户 `hide_collections` 为 true,且请求帐户 != 目标帐户,则不会返回结果。 + operationId: accountFollowers + parameters: + - description: 帐户 ID。 + in: path + name: id + required: true + type: string + - description: '仅返回早于给定的max ID 的粉丝帐户。具有指定 ID 的粉丝帐户不会包含在响应中。注意:ID 是内部关注的 ID,而不是任何返回的帐户的 ID。' + in: query + name: max_id + type: string + - description: '仅返回晚于给定的 since ID 的粉丝帐户。具有指定 ID 的粉丝帐户不会包含在响应中。注意:ID 是内部关注的 ID,而不是任何返回的帐户的 ID。' + in: query + name: since_id + type: string + - description: '仅返回 *晚于且紧邻* 给定的 min ID 的粉丝帐户。具有指定 ID 的粉丝帐户不会包含在响应中。注意:ID 是内部关注的 ID,而不是任何返回的帐户的 ID。' + in: query + name: min_id + type: string + - default: 40 + description: 要返回的粉丝帐户数量。 + in: query + maximum: 80 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: 一个关注此帐户的帐户的数组。 + headers: + Link: + description: 指向下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/account' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:accounts + summary: 查看粉丝 + tags: + - accounts + /api/v1/accounts/{id}/following: + get: + description: |- + 查询此ID对应的帐户关注的帐户。 + 下一个/上一个查询可以从返回的 Link 标头中解析。 + 示例: + ``` + ; rel="next", ; rel="prev" + ``` + 若帐户 `hide_collections` 为 true,且请求帐户 != 目标帐户,则不会返回结果。 + operationId: accountFollowing + parameters: + - description: Account ID. + in: path + name: id + required: true + type: string + - description: '仅返回早于给定的 max ID 的关注帐户。具有指定 ID 的关注帐户不会包含在响应中。注意:ID 是内部关注的 ID,而不是任何返回的帐户的 ID。' + in: query + name: max_id + type: string + - description: '仅返回晚于给定的 since ID 的关注帐户。具有指定 ID 的关注帐户不会包含在响应中。注意:ID 是内部关注的 ID,而不是任何返回的帐户的 ID。' + in: query + name: since_id + type: string + - description: '仅返回 *晚于且紧邻* 给定的 min ID 的关注帐户。具有指定 ID 的关注帐户不会包含在响应中。注意:ID 是内部关注的 ID,而不是任何返回的帐户的 ID。' + in: query + name: min_id + type: string + - default: 40 + description: 要返回的关注帐户数量。 + in: query + maximum: 80 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: 此账户关注的账户数组。 + headers: + Link: + description: 指向下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/account' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:accounts + summary: 查看关注 + tags: + - accounts + /api/v1/accounts/{id}/lists: + get: + operationId: accountLists + parameters: + - description: 账户 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 一个包含此帐户的所有列表的数组。 + schema: + items: + $ref: '#/definitions/list' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:lists + summary: 查看列表 + description: 查看你的列表中包含此帐户的所有列表。 + tags: + - accounts + /api/v1/accounts/{id}/mute: + post: + description: 根据 ID 静音/隐藏帐户。若此账户已被静音/隐藏,则无论如何都会成功。这可以用来更新静音/隐藏的细节。 + operationId: accountMute + parameters: + - description: 要静音/隐藏的帐户 ID。 + in: path + name: id + required: true + type: string + - default: false + description: 在静音/隐藏贴文的同时也静音/隐藏通知。 + in: formData + name: notifications + type: boolean + - default: 0 + description: 静音/隐藏持续时间,以秒为单位。如果为 0 或未提供,则静音/隐藏持续时间无限。 + in: formData + name: duration + type: number + produces: + - application/json + responses: + "200": + description: 你与此帐户的关系(relationship)。 + schema: + $ref: '#/definitions/accountRelationship' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: 禁止对已迁移的账户设置静音/隐藏 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:mutes + summary: 静音/隐藏帐户 + tags: + - accounts + /api/v1/accounts/{id}/note: + post: + consumes: + - multipart/form-data + operationId: accountNote + parameters: + - description: 备注要添加到的帐户的 ID。 + in: path + name: id + required: true + type: string + - default: "" + description: 备注的文本。省略此参数或发送空字符串以清除备注。 + in: formData + name: comment + type: string + produces: + - application/json + responses: + "200": + description: 你与此账户的关系(relationship)。 + schema: + $ref: '#/definitions/accountRelationship' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:accounts + summary: 设置备注 + description: 为具有给定 ID 的帐户设置私人备注。 + tags: + - accounts + /api/v1/accounts/{id}/statuses: + get: + description: 查看指定账户的贴文。贴文将按时间顺序(最新的在前)返回,带有连续的 ID(更大 = 更新)。 + operationId: accountStatuses + parameters: + - description: 账户 ID。 + in: path + name: id + required: true + type: string + - default: 30 + description: 要返回的贴文数量。 + in: query + name: limit + type: integer + - default: false + description: 排除回复。 + in: query + name: exclude_replies + type: boolean + - default: false + description: 排除转发。 + in: query + name: exclude_reblogs + type: boolean + - description: 仅返回 *早于* 给定的 max status ID 的项目。当前仅在将 type 设置为特定类型时使用。 + in: query + name: max_id + type: string + - description: 仅返回 *晚于* 给定的 min status ID 的项目。当前仅在将 type 设置为特定类型时使用。 + in: query + name: min_id + type: string + - default: false + description: 仅显示置顶的贴文。即排除未置顶到给定帐户 ID 的贴文。 + in: query + name: pinned + type: boolean + - default: false + description: 仅显示带有媒体附件的贴文。 + in: query + name: only_media + type: boolean + - default: false + description: 仅显示具有“公开”可见性的贴文。 + in: query + name: only_public + type: boolean + produces: + - application/json + responses: + "200": + description: 贴文的数组。 + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/status' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:accounts + summary: 查看贴文 + tags: + - accounts + /api/v1/accounts/{id}/unblock: + post: + operationId: accountUnblock + parameters: + - description: 要解除屏蔽的帐户 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 你与此账户的关系(relationship)。 + schema: + $ref: '#/definitions/accountRelationship' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:blocks + summary: 解除屏蔽账户 + description: 解除对给定 ID 的帐户的屏蔽。 + tags: + - accounts + /api/v1/accounts/{id}/unfollow: + post: + operationId: accountUnfollow + parameters: + - description: 要取消关注的帐户 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 你与此账户的关系(relationship)。 + schema: + $ref: '#/definitions/accountRelationship' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:follows + summary: 取关账户 + description: 取消关注具有给定 ID 的帐户。 + tags: + - accounts + /api/v1/accounts/{id}/unmute: + post: + description: 解除对给定 ID 的帐户的静音/隐藏。若账户已被解除静音/隐藏,则无论如何都会成功。 + operationId: accountUnmute + parameters: + - description: 要解除静音/隐藏的帐户 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 你与此账户的关系(relationship)。 + schema: + $ref: '#/definitions/accountRelationship' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:mutes + summary: 取消静音/隐藏账户 + tags: + - accounts + /api/v1/accounts/alias: + post: + consumes: + - multipart/form-data + description: |- + 通过将 alsoKnownAs 设置为给定的 URI,将此账户设置为另一个账户的别名。 + + 在需要将其它账户迁移到此账户时很有用。 + + 在这种情况下,你应该将此账户的 alsoKnownAs 设置为要发起迁移的账户的 URI。 + operationId: accountAlias + parameters: + - description: |- + 要设为别名的账户的 ActivityPub URI/ID。例如,`["https://example.org/users/some_account"]`。 + 使用空数组也可以清除 alsoKnownAs,清除账户别名。 + in: formData + name: also_known_as_uris + required: true + type: string + responses: + "200": + description: 更新后的账户。 + schema: + $ref: '#/definitions/account' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "422": + description: unprocessable 无法处理。请查看响应正文以获取更多详细信息。 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:accounts + summary: 设置账户别名 + tags: + - accounts + /api/v1/accounts/delete: + post: + consumes: + - multipart/form-data + operationId: accountDelete + parameters: + - description: 账户的密码,用于确认。 + in: formData + name: password + required: true + type: string + responses: + "202": + description: 账户删除请求已被接受,账户将被删除。 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:accounts + summary: 删除账户 + tags: + - accounts + /api/v1/accounts/lookup: + get: + operationId: accountLookupGet + parameters: + - description: 要查询的用户名或 Webfinger 地址。 + in: query + name: acct + required: true + type: string + produces: + - application/json + responses: + "200": + description: 查询结果。 + schema: + $ref: '#/definitions/account' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:accounts + summary: 快速查找账户 + description: 快速查找用户名以查看其是否可用,跳过 WebFinger 解析。 + tags: + - accounts + /api/v1/accounts/move: + post: + consumes: + - multipart/form-data + operationId: accountMove + parameters: + - description: 账户的密码,用于确认。 + in: formData + name: password + required: true + type: string + - description: 目标账户的 ActivityPub URI/ID。例如,`https://example.org/users/some_account`。目标账户必须也是请求账户的 alsoKnownAs,才能成功迁移。 + in: formData + name: moved_to_uri + required: true + type: string + responses: + "202": + description: 账户迁移请求已被接受,账户将被迁移。 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "422": + description: unprocessable 无法处理。请查看响应正文以获取更多详细信息。 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:accounts + summary: 迁出账户 + description: 将此账户迁移到另一个账户。 + tags: + - accounts + /api/v1/accounts/relationships: + get: + operationId: accountRelationships + parameters: + - collectionFormat: multi + description: 账户 ID。 + in: query + items: + type: string + name: id[] + required: true + type: array + produces: + - application/json + responses: + "200": + description: 账户关系(relationship)的数组。 + schema: + items: + $ref: '#/definitions/accountRelationship' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:accounts + summary: 查询账户关系 + description: 查看你的账户与给定账户 ID 的关系。 + tags: + - accounts + /api/v1/accounts/search: + get: + operationId: accountSearchGet + parameters: + - default: 40 + description: 要返回的结果数量。 + in: query + maximum: 80 + minimum: 1 + name: limit + type: integer + - default: 0 + description: 要返回的结果所在的页数(从 0 开始)。此参数目前未使用,偏移量大于 0 将始终返回 0 个结果。 + in: query + maximum: 10 + minimum: 0 + name: offset + type: integer + - description: |- + 要查找的关键词。可以是以下形式之一: + - `@[username]` -- 在任何实例上搜索具有给定用户名的帐户。可以返回多个结果。 + - `@[username]@[domain]` -- 搜索确切的用户名和域名的外站帐户。最多只返回 1 个结果。 + - 任意字符串 -- 搜索用户名或昵称中包含给定字符串的帐户。可以返回多个结果。 + in: query + name: q + required: true + type: string + - default: false + description: 若关键词为 `@[username]@[domain]` 或 URL,允许 GoToSocial 实例通过调用外站实例(webfinger、ActivityPub 等)来解析搜索。 + in: query + name: resolve + type: boolean + - default: false + description: 只显示发起请求的账户关注的账户。如果设置为 `true`,则 GoToSocial 实例将在用户名和昵称之外,同时在账户信息中发起搜索,来增强搜索结果。 + in: query + name: following + type: boolean + produces: + - application/json + responses: + "200": + description: 搜索结果。 + schema: + items: + $ref: '#/definitions/account' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:accounts + summary: 搜索账户 + description: 搜索具有给定用户名或昵称的帐户。 + tags: + - accounts + /api/v1/accounts/themes: + get: + operationId: accountThemes + produces: + - application/json + responses: + "200": + description: 主题的数组。 + schema: + items: + $ref: '#/definitions/theme' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:accounts + summary: 查看主题 + description: 查看此实例上可用于账户的预设 CSS 主题。 + tags: + - accounts + /api/v1/accounts/update_credentials: + patch: + consumes: + - multipart/form-data + - application/x-www-form-urlencoded + - application/json + operationId: accountUpdate + parameters: + - description: 账户是否可被发现,并在用户名录中显示。 + in: formData + name: discoverable + type: boolean + - description: 账户被标记为机器人。 + in: formData + name: bot + type: boolean + - allowEmptyValue: true + description: 账户的昵称。 + in: formData + name: display_name + type: string + - allowEmptyValue: true + description: 账户的简介。 + in: formData + name: note + type: string + - description: 账户的头像。 + in: formData + name: avatar + type: file + - allowEmptyValue: true + description: 账户的头像描述,用于 alt 文本。 + in: formData + name: avatar_description + type: string + - description: 用户的资料页横幅背景。 + in: formData + name: header + type: file + - allowEmptyValue: true + description: 用户资料页横幅背景的描述,用于 alt 文本。 + in: formData + name: header_description + type: string + - description: 关注请求需要手动批准。 + in: formData + name: locked + type: boolean + - description: 撰写的贴文的默认可见性。 + in: formData + name: source[privacy] + type: string + - description: 将撰写的贴文默认标记为敏感。 + in: formData + name: source[sensitive] + type: boolean + - description: 撰写的贴文的默认语言 (ISO 6391)。 + in: formData + name: source[language] + type: string + - description: 撰写的贴文的默认内容类型 (text/plain 或 text/markdown)。 + in: formData + name: source[status_content_type] + type: string + - description: 渲染此帐户的资料页或贴文时要使用的主题的文件名。主题必须存在于此服务器上,如 /api/v1/accounts/themes 所示。空字符串意味着取消主题偏好并回退到 GoToSocial 默认主题。 + in: formData + name: theme + type: string + - description: 渲染此帐户的资料页或贴文时要使用的自定义 CSS。字符串长度不能超过 5,000 个字符(~5kb)。 + in: formData + name: custom_css + type: string + - description: 为此帐户的公开贴文启用 RSS 订阅,端点为 `/[username]/feed.rss`。 + in: formData + name: enable_rss + type: boolean + - description: 隐藏此帐户的关注/粉丝信息。 + in: formData + name: hide_collections + type: boolean + - description: |- + Posts to show on the web view of the account. + "public": default, show only Public visibility posts on the web. + "unlisted": show Public *和* Unlisted visibility posts on the web. + "none": show no posts on the web, not even Public ones. + 要在网页端展示的贴文范围。 + "public": 默认,只在网页上显示公开的贴文。 + "unlisted": 在网页上显示可见性为公开 *和* 不列出的贴文。 + "none": 在网页上不显示任何贴文,甚至不显示公开的贴文。 + in: formData + name: web_visibility + type: string + - description: 要添加到此帐户的资料页的第一个资料字段的名称。 (索引可以是任何字符串;添加更多索引以发送更多字段。) + in: formData + name: fields_attributes[0][name] + type: string + - description: 要添加到此帐户的资料页的第一个资料字段的值。 (索引可以是任何字符串;添加更多索引以发送更多字段。) + in: formData + name: fields_attributes[0][value] + type: string + - description: 要添加到此帐户的资料页的第二个资料字段的名称。 + in: formData + name: fields_attributes[1][name] + type: string + - description: 要添加到此帐户的资料页的第二个资料字段的值。 + in: formData + name: fields_attributes[1][value] + type: string + - description: 要添加到此帐户的资料页的第三个资料字段的名称。 + in: formData + name: fields_attributes[2][name] + type: string + - description: 要添加到此帐户的资料页的第三个资料字段的值。 + in: formData + name: fields_attributes[2][value] + type: string + - description: 要添加到此帐户的资料页的第四个资料字段的名称。 + in: formData + name: fields_attributes[3][name] + type: string + - description: 要添加到此帐户的资料页的第四个资料字段的值。 + in: formData + name: fields_attributes[3][value] + type: string + - description: 要添加到此帐户的资料页的第五个资料字段的名称。 + in: formData + name: fields_attributes[4][name] + type: string + - description: 要添加到此帐户的资料页的第五个资料字段的值。 + in: formData + name: fields_attributes[4][value] + type: string + - description: 要添加到此帐户的资料页的第六个资料字段的名称。 + in: formData + name: fields_attributes[5][name] + type: string + - description: 要添加到此帐户的资料页的第六个资料字段的值。 + in: formData + name: fields_attributes[5][value] + type: string + produces: + - application/json + responses: + "200": + description: 更新后的账户。 + schema: + $ref: '#/definitions/account' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:accounts + summary: 更新账户信息 + description: 更新你的账户信息。 + tags: + - accounts + /api/v1/accounts/verify_credentials: + get: + operationId: accountVerify + produces: + - application/json + responses: + "200": + description: "" + schema: + $ref: '#/definitions/account' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:accounts + summary: 验证令牌 + description: 验证令牌并返回与之相关的账户详细信息。 + tags: + - accounts + /api/v1/admin/accounts: + get: + description: |- + 使用特定的过滤规则查看 + 分页浏览已知的账户。 + + 返回的账户将按域名 + 用户名的字母顺序(a-z)排序。 + + 下一个和上一个查询可以从返回的 Link 标头中解析出来。 + 例如: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: adminAccountsGetV1 + parameters: + - default: false + description: 只显示本站账户的过滤规则。 + in: query + name: local + type: boolean + - default: false + description: 只显示外站账户的过滤规则。 + in: query + name: remote + type: boolean + - default: false + description: 只显示当前活跃的账户的过滤规则。 + in: query + name: active + type: boolean + - default: false + description: 只显示当前待处理的账户的过滤规则。 + in: query + name: pending + type: boolean + - default: false + description: 只显示当前被禁用的账户的过滤规则。 + in: query + name: disabled + type: boolean + - default: false + description: 只显示当前被静音的账户的过滤规则。 + in: query + name: silenced + type: boolean + - default: false + description: 只显示当前被封禁的账户的过滤规则。 + in: query + name: suspended + type: boolean + - default: false + description: 只显示当前被强制标记为敏感的账户的过滤规则。 + in: query + name: sensitized + type: boolean + - description: 搜索给定的用户名。 + in: query + name: username + type: string + - description: 搜索给定的昵称。 + in: query + name: display_name + type: string + - description: 搜索给定的域名。 + in: query + name: by_domain + type: string + - description: 搜索具有给定电子邮箱的用户。 + in: query + name: email + type: string + - description: 搜索具有给定 IP 地址的用户。 + in: query + name: ip + type: string + - default: false + description: 搜索工作人员账户。 + in: query + name: staff + type: boolean + - description: "`[domain]/@[username]` 中的 `max_id`。返回的所有结果都将在 `[domain]/@[username]` 之后的字母顺序中。例如,如果 `max_id` = `example.org/@someone`,则返回的条目可能包含 `example.org/@someone_else`、`later.example.org/@someone` 等。在此形式中,本站帐户 ID 使用空字符串作为 `[domain]` 部分,例如具有用户名 `someone` 的本地帐户将是 `/@someone`。" + in: query + name: max_id + type: string + - description: "`[domain]/@[username]` 中的 `min_id`。返回的所有结果都将在 `[domain]/@[username]` 之前的字母顺序中。例如,如果 `min_id` = `example.org/@someone`,则返回的条目可能包含 `example.org/@earlier_account`、`earlier.example.org/@someone` 等。在此形式中,本站帐户 ID 使用空字符串作为 `[domain]` 部分,例如具有用户名 `someone` 的本地帐户将是 `/@someone`。" + in: query + name: min_id + type: string + - default: 50 + description: 要返回的最大结果数。 + in: query + maximum: 200 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: "" + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/adminAccountInfo' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看账户 + tags: + - admin + /api/v1/admin/accounts/{id}: + get: + operationId: adminAccountGet + parameters: + - description: 账户的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/adminAccountInfo' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看单个账户 + description: 查看一个账户。 + tags: + - admin + /api/v1/admin/accounts/{id}/action: + post: + consumes: + - multipart/form-data + operationId: adminAccountAction + parameters: + - description: 账户的 ID。 + in: path + name: id + required: true + type: string + - description: 要执行的操作类型,目前仅支持 `suspend`。 + in: formData + name: type + required: true + type: string + - description: 描述执行此操作的原因的文本,可不填。 + in: formData + name: text + type: string + produces: + - application/json + responses: + "200": + description: OK + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "409": + description: '冲突: 已经有一个与此操作冲突的管理员操作正在执行。查看响应正文中的错误消息以获取更多信息。这是一个临时错误; 如果稍后再试一次,应该可以处理此操作。' + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 执行管理操作 + description: 对某个账户执行管理员操作。 + tags: + - admin + /api/v1/admin/accounts/{id}/approve: + post: + operationId: adminAccountApprove + parameters: + - description: 账户的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 被批准后的账户。 + schema: + $ref: '#/definitions/adminAccountInfo' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 批准账户 + description: 批准待处理的账户。 + tags: + - admin + /api/v1/admin/accounts/{id}/reject: + post: + operationId: adminAccountReject + parameters: + - description: 账户的 ID。 + in: path + name: id + required: true + type: string + - description: 用于说明为什么拒绝此账户的评论。评论仅对管理员可见。 + in: formData + name: private_comment + type: string + - description: 要包含在发送给申请人的电子邮件中的消息。仅在 send_email 为 true 时包含。 + in: formData + name: message + type: string + - description: 向申请人发送一封电子邮件,告知他们的注册已被拒绝。 + in: formData + name: send_email + type: boolean + produces: + - application/json + responses: + "200": + description: 被拒绝的账户。 + schema: + $ref: '#/definitions/adminAccountInfo' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 拒绝账户 + description: 拒绝待处理的账户。 + tags: + - admin + /api/v1/admin/custom_emojis: + get: + description: |- + 下一个/上一个查询可以从返回的 Link 标头中解析。 + 例如: + + `; rel="next", ; rel="prev"` + operationId: emojisGet + parameters: + - default: domain:all + description: |- + 查看本站表情和已知的外站表情。 + + 要应用到结果中的过滤规则列表,以逗号分隔。可被识别的过滤规则有: + + `domain:[domain]` -- 显示给定域名下的表情,例如 `?filter=domain:example.org` 将只显示 `example.org` 的表情。 + 除了给定特定域名外,还可以给出关键字 `local` 或 `all`,以仅显示本站表情 (`domain:local`) 或显示所有实例的所有表情 (`domain:all`)。 + 注意:`domain:*` 等同于 `domain:all`(包括本站)。 + 如果未提供域名过滤规则,则将假定 `domain:all`。 + + `disabled` -- 包括已禁用的表情。 + + `enabled` -- 包括已启用的表情。 + + `shortcode:[shortcode]` -- 仅显示给定的表情代码,例如 `?filter=shortcode:blob_cat_uwu` 将仅显示表情代码为 `blob_cat_uwu` 的表情(区分大小写)。 + + 如果未提供 `disabled` 或 `enabled`,则将同时显示已禁用和已启用的表情。 + + 如果未提供过滤规则,则将使用默认的 `domain:all`,它将显示所有实例的所有表情。 + in: query + name: filter + type: string + - default: 50 + description: 要返回的表情数量。小于 1 或未设置表示无限制(所有表情)。 + in: query + maximum: 200 + minimum: 0 + name: limit + type: integer + - description: |- + 只返回比给定 `[shortcode]@[domain]` *更低*(按字母顺序)的表情。例如,如果 `max_shortcode_domain=beep@example.org`,则返回的值可能包括 `[shortcode]@[domain]` 为 `car@example.org`, `debian@aaa.com`, `test@` (本站表情) 等。 + 具有给定 `[shortcode]@[domain]` 的表情将不包含在结果集中。 + in: query + name: max_shortcode_domain + type: string + - description: |- + 只返回比给定 `[shortcode]@[domain]` *更高*(按字母顺序)的表情。例如,如果 `max_shortcode_domain=beep@example.org`, 则返回的值可能包括 `[shortcode]@[domain]` 为 `arse@test.com`, `0101_binary@hackers.net`, `bee@` (本站表情) 等。 + 具有给定 `[shortcode]@[domain]` 的表情将不包含在结果集中。 + in: query + name: min_shortcode_domain + type: string + produces: + - application/json + responses: + "200": + description: 表情数组,按表情代码和域名字母顺序排列。 + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/adminEmoji' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + summary: 查看表情 + tags: + - admin + post: + consumes: + - multipart/form-data + operationId: emojiCreate + parameters: + - description: 此表情的代码,将被实例居民用于选定对应表情。此代码在实例上必须是唯一的。 + in: formData + name: shortcode + pattern: \w{2,30} + required: true + type: string + - description: 此表情的 png 或 gif 图像。动画 png 也可以!为了确保与其他 fedi 实现的兼容性,默认情况下表情大小限制为 50kb。 + in: formData + name: image + required: true + type: file + - description: 此表情所属的类别。如果留空,表情将不属于任何类别。如果给定的类别名称尚不存在,则将创建该类别。 + in: formData + name: category + type: string + produces: + - application/json + responses: + "200": + description: 新创建的表情。 + schema: + $ref: '#/definitions/emoji' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "409": + description: 冲突 -- 此表情的代码已被使用 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 创建表情 + description: 上传和创建一个新的实例表情。 + tags: + - admin + /api/v1/admin/custom_emojis/{id}: + delete: + description: |- + 在此实例删除一个 **本地** 表情。 + + 给定 ID 的表情将不再在此实例上可用。 + + 如果您只想更新表情图像,请使用 `/api/v1/admin/custom_emojis/{id}` PATCH 路由。 + + 要禁用 **外站** 实例的表情,请使用 `/api/v1/admin/custom_emojis/{id}` PATCH 路由。 + operationId: emojiDelete + parameters: + - description: 表情的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 被删除的表情将被返回给调用者,以便进一步处理。 + schema: + $ref: '#/definitions/adminEmoji' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 删除表情 + tags: + - admin + get: + operationId: emojiGet + parameters: + - description: 表情的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 单个表情。 + schema: + $ref: '#/definitions/adminEmoji' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + summary: 查看表情 + description: 在管理视图中查看单个表情。 + tags: + - admin + patch: + consumes: + - multipart/form-data + description: |- + 对一个 **本站** 或 **外站** 表情执行管理操作。 + + 要执行的操作取决于提供的操作类型 `type` 参数。 + + `disable`: 禁用一个 **外站** 表情,使其无法在此实例上使用/显示。不适用于本站表情。 + + `copy`: 将 **外站** 表情复制到此实例。执行此操作时,必须提供一个表情代码,并且它必须在此实例上已有的表情中是唯一的。可以提供一个类别,然后复制的表情将被放入提供的类别中。 + + `modify`: 修改一个 **本站** 表情。您可以为表情提供一个新的图像和/或更新类别。 + + 本站表情不能使用此端点删除。要删除本站表情,请查看 DELETE /api/v1/admin/custom_emojis/{id}。 + operationId: emojiUpdate + parameters: + - description: 表情的 ID。 + in: path + name: id + required: true + type: string + - description: |- + 要执行的操作类型。需为下列其中之一:(`disable`, `copy`, `modify`)。 + 对于 **外站** 表情,支持 `copy` 或 `disable`。 + 对于 **本站** 表情,仅支持 `modify`。 + enum: + - copy + - disable + - modify + in: formData + name: type + required: true + type: string + - description: 用于表情的代码,将被实例居民用于选定表情。此代码在实例上必须是唯一的。仅适用于 `copy` 操作类型。 + in: formData + name: shortcode + pattern: \w{2,30} + type: string + - description: 此表情的新 png 或 gif 图像。动画 png 也可以!为了确保与其他 fedi 实现的兼容性,默认情况下表情大小限制为 50kb。仅适用于 **本站** 表情。 + in: formData + name: image + type: file + - description: 表情所属的类别。如果尚不存在具有给定名称的类别,则将创建该类别。仅适用于 `copy` 和 `modify` 操作类型。 + in: formData + name: category + type: string + produces: + - application/json + responses: + "200": + description: 更新后的表情。 + schema: + $ref: '#/definitions/adminEmoji' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 修改表情 + tags: + - admin + /api/v1/admin/custom_emojis/categories: + get: + operationId: emojiCategoriesGet + produces: + - application/json + responses: + "200": + description: 现有表情类别的数组。 + schema: + items: + $ref: '#/definitions/emojiCategory' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + summary: 查看表情类别 + description: 获取现有表情类别的列表。 + tags: + - admin + /api/v1/admin/debug/apurl: + get: + description: 对指定的 ActivityPub URL 执行 GET 请求,并返回详细的调试信息。仅当 GoToSocial 使用 DEBUG=1 标志构建和运行时才启用/公开。 + operationId: debugAPUrl + parameters: + - description: 要解析的 URL 或 ActivityPub ID。 如 `https://example.org/users/someone` + in: query + name: url + required: true + type: string + produces: + - application/json + responses: + "200": + description: "" + schema: + $ref: '#/definitions/debugAPUrlResponse' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 解析 ActivityPub URL + tags: + - debug + /api/v1/admin/debug/caches/clear: + post: + description: 清除所有内存中的缓存。仅当 GoToSocial 使用 DEBUG=1 标志构建和运行时才启用/公开。 + operationId: debugClearCaches + produces: + - application/json + responses: + "200": + description: 所有缓存都已清除。 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 清除缓存 + tags: + - debug + /api/v1/admin/domain_allows: + get: + operationId: domainAllowsGet + parameters: + - description: 如果设为 `true`,则返回的实例白名单列表中的每个条目将仅包含字段 `domain` 和 `public_comment`。这对于当您想要保存和共享您实例上允许的所有域名的列表时非常有用,以便其他人可以轻松导入它们,但您不希望他们看到您的允许的数据库 ID,或私人评论等。 + in: query + name: export + type: boolean + produces: + - application/json + responses: + "200": + description: 目前所有被允许的域名。 + schema: + items: + $ref: '#/definitions/domainPermission' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看实例白名单 + description: 查看当前所有被允许联合的域名。 + tags: + - admin + post: + consumes: + - multipart/form-data + description: |- + 从字符串或文件中创建一个或多个实例白名单。 + + 在使用此端点时,您有两个选项:要么将 `import` 设置为 `true` 并上传一个包含多个要允许的域名的 JSON 文件,要么将 `import` 设置为 `false`,并添加一条要允许的域名。 + + JSON 文件的格式应该类似于:`[{"domain":"example.org"},{"domain":"whatever.com","public_comment":"它们是我的好厚米"}]` + operationId: domainAllowCreate + parameters: + - default: false + description: 表示正在导入一个包含多个要允许的域名的文件。如果设置为 `true`,则 `domains` 必须作为 JSON 格式的文件存在。如果设置为 `false`,则 `domains` 将被忽略,`domain` 必须存在。 + in: query + name: import + type: boolean + - description: JSON 格式的实例白名单列表。仅在 `import` 设置为 `true` 时使用。 + in: formData + name: domains + type: file + - description: 要允许的单个域名。仅在 `import` 不是 `true` 时使用。 + in: formData + name: domain + type: string + - description: 在公开时对域名的名称进行混淆。例如,`example.org` 变成类似于 `ex***e.org`。仅在 `import` 不是 `true` 时使用。 + in: formData + name: obfuscate + type: boolean + - description: 有关此实例白名单的公共评论。如果选择共享允许,则此评论将显示在实例白名单旁边。仅在 `import` 不是 `true` 时使用。 + in: formData + name: public_comment + type: string + - description: 有关此实例白名单的私人评论。仅显示给其他管理员,因此这是一个有用的内部方法,用于跟踪为什么某个域名最终被允许。仅在 `import` 不是 `true` 时使用。 + in: formData + name: private_comment + type: string + produces: + - application/json + responses: + "200": + description: 如果 `import` != `true`,则返回新创建的实例白名单。如果导入了列表,则将返回新创建的实例白名单的 `array` (数组)。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "409": + description: '冲突: 已经有一个与此操作冲突的管理员操作正在执行。查看响应正文中的错误消息以获取更多信息。这是一个临时错误; 如果稍后再试一次,应该可以处理此操作。' + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 创建实例白名单 + tags: + - admin + /api/v1/admin/domain_allows/{id}: + delete: + operationId: domainAllowDelete + parameters: + - description: 实例白名单条目的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 刚刚删除的实例白名单条目。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "409": + description: '冲突: 已经有一个与此操作冲突的管理员操作正在执行。查看响应正文中的错误消息以获取更多信息。这是一个临时错误; 如果稍后再试一次,应该可以处理此操作。' + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 删除实例白名单条目 + description: 删除具有给定 ID 的实例白名单条目。 + tags: + - admin + get: + operationId: domainAllowGet + parameters: + - description: 实例白名单条目的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的实例白名单条目。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看实例白名单条目 + description: 查看具有给定 ID 的实例白名单条目。 + tags: + - admin + /api/v1/admin/domain_blocks: + get: + operationId: domainBlocksGet + parameters: + - description: 如果设为 `true`,则返回的实例黑名单的每个条目将仅包含字段 `domain` 和 `public_comment`。这对于当您想要保存和共享您实例上阻止的所有实例的列表时非常有用,以便其他人可以轻松导入它们,但您不希望他们看到您的黑名单的数据库 ID,或私人评论等。 + in: query + name: export + type: boolean + produces: + - application/json + responses: + "200": + description: 目前所有的实例黑名单。 + schema: + items: + $ref: '#/definitions/domainPermission' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看实例黑名单 + description: 查看当前所有实例黑名单。 + tags: + - admin + post: + consumes: + - multipart/form-data + description: |- + 从字符串或文件中创建一个或多个实例黑名单条目。 + + 使用此端点时,您有两个选项:要么将 `import` 设置为 `true` 并上传一个包含多个要阻止联合的实例的 JSON 文件,要么将 `import` 设置为 `false`,并添加一个要阻止的实例。 + + JSON 文件的格式应该类似于:`[{"domain":"example.org"},{"domain":"whatever.com","public_comment":"它们不是我的好厚米"}]` + operationId: domainBlockCreate + parameters: + - default: false + description: 表示正在导入一个包含多个要阻止联合的实例的文件。如果设置为 `true`,则 `domains` 必须作为 JSON 格式的文件存在。如果设置为 `false`,则 `domains` 将被忽略,`domain` 必须存在。 + in: query + name: import + type: boolean + - description: 要导入的 JSON 格式的实例黑名单。仅在 `import` 设置为 `true` 时使用。 + in: formData + name: domains + type: file + - description: 单个要阻止联合的实例。仅在 `import` 不是 `true` 时使用。 + in: formData + name: domain + type: string + - description: 对外公开时对域名进行混淆。例如,`example.org` 变成类似于 `ex***e.org`。仅在 `import` 不是 `true` 时使用。 + in: formData + name: obfuscate + type: boolean + - description: 对此实例黑名单条目的公开评论。如果选择共享阻止,此评论将显示在实例黑名单条目旁边。仅在 `import` 不是 `true` 时使用。 + in: formData + name: public_comment + type: string + - description: 对此实例黑名单条目的私人评论。仅显示给其他管理员,因此这是一个有用的内部方法,用于跟踪为什么某个域名最终被阻止。仅在 `import` 不是 `true` 时使用。 + in: formData + name: private_comment + type: string + produces: + - application/json + responses: + "200": + description: 如果 `import` != `true`,则返回新创建的实例黑名单条目。如果导入了列表,则将返回新创建的实例黑名单的 `array` (数组)。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "409": + description: '冲突: 已经有一个与此操作冲突的管理员操作正在执行。查看响应正文中的错误消息以获取更多信息。这是一个临时错误; 如果稍后再试一次,应该可以处理此操作。' + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 创建实例黑名单 + tags: + - admin + /api/v1/admin/domain_blocks/{id}: + delete: + operationId: domainBlockDelete + parameters: + - description: 实例黑名单条目的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 刚刚删除的实例黑名单条目。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "409": + description: '冲突: 已经有一个与此操作冲突的管理员操作正在执行。查看响应正文中的错误消息以获取更多信息。这是一个临时错误; 如果稍后再试一次,应该可以处理此操作。' + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 删除实例黑名单条目 + description: 删除具有给定 ID 的实例黑名单条目。 + tags: + - admin + get: + operationId: domainBlockGet + parameters: + - description: 实例黑名单条目的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的实例黑名单条目。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看实例黑名单条目 + description: 查看具有给定 ID 的实例黑名单条目。 + tags: + - admin + /api/v1/admin/domain_keys_expire: + post: + consumes: + - multipart/form-data + description: |- + 将指定域名的所有帐户的缓存公钥强制吊销,这些公钥存储在您的数据库中。 + + 当外站实例不得不轮转其密钥时(出于安全问题、数据泄漏、例行安全程序等原因),此功能非常有用,您的实例无法使用缓存的密钥与其正常通信。以这种方式标记为过期的密钥将在下次由该密钥所有者签名的请求发送到您的实例时进行懒惰重新获取,因此不需要进一步操作即可重新建立与该域的通信。 + + 此端点务必不能用于轮转您自己的密钥,它仅适用于外站实例。 + + 使用此端点来吊销尚未轮转所有密钥的实例的密钥既不会有害,也不会破坏联合,但是毫无意义,会导致不必要的请求执行。 + operationId: domainKeysExpire + parameters: + - description: |- + 要吊销密钥的实例域名。 + 例如:example.org + in: formData + name: domain + type: string + x-go-name: Domain + produces: + - application/json + responses: + "202": + description: 请求已接受并将被处理。检查日志以查看进度/错误。 + schema: + $ref: '#/definitions/adminActionResponse' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "409": + description: '冲突: 已经有一个与此操作冲突的管理员操作正在执行。查看响应正文中的错误消息以获取更多信息。这是一个临时错误; 如果稍后再试一次,应该可以处理此操作。' + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 吊销实例密钥 + tags: + - admin + /api/v1/admin/email/test: + post: + consumes: + - multipart/form-data + description: |- + 向指定的电子邮件地址发送一封通用的测试电子邮件。 + + 这可以用来验证实例的 SMTP 配置,并调试任何潜在问题。 + + 如果 SMTP 连接返回错误,此处理程序将返回代码 422,以指示请求无法处理,并将 SMTP 错误返回给调用者。 + operationId: testEmailSend + parameters: + - description: 要发送测试电子邮件的电子邮件地址。 + in: formData + name: email + required: true + type: string + - description: 要包含在电子邮件中的消息,可不填。 + in: formData + name: message + type: string + produces: + - application/json + responses: + "202": + description: 测试邮件已发送。 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "422": + description: 尝试发件时发生了一个 smtp 错误。检查返回的 json 以获取更多信息。将包含 smtp 错误,以帮助您调试与 smtp 服务器的通信。 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 测试电子邮件 + tags: + - admin + /api/v1/admin/header_allows: + get: + operationId: headerFilterAllowsGet + responses: + "200": + description: 目前所有的标头 "allow" (放行) 过滤规则。 + schema: + items: + $ref: '#/definitions/headerFilter' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看标头放行规则 + description: 查看当前所有 "allow" (放行) 过滤规则。 + tags: + - admin + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + description: |- + 创建一条新的 "allow" (放行) HTTP 请求标头过滤规则。 + 若 Content-Type 为 'application/json',则参数也可以在请求体中以 JSON 形式给出。 + 若 Content-Type 为 'application/xml',则参数也可以在请求体中以 XML 形式给出。 + operationId: headerFilterAllowCreate + parameters: + - description: 要匹配的 HTTP 标头 (例如 User-Agent)。 + in: formData + name: header + required: true + type: string + x-go-name: Header + - description: 要匹配的标头值的正则表达式。 + in: formData + name: regex + required: true + type: string + x-go-name: Regex + produces: + - application/json + responses: + "200": + description: 新创建的 "allow" (放行) 标头过滤规则。 + schema: + $ref: '#/definitions/headerFilter' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 创建标头放行规则 + tags: + - admin + /api/v1/admin/header_allows/{id}: + delete: + operationId: headerFilterAllowDelete + parameters: + - description: 目标标头过滤规则 ID。 + in: path + name: id + required: true + type: string + responses: + "202": + description: Accepted 已接受 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 删除标头放行规则 + description: 删除具有给定 ID 的 "allow" (放行) 标头过滤规则。 + tags: + - admin + get: + operationId: headerFilterAllowGet + parameters: + - description: 目标标头过滤规则 ID。 + in: path + name: id + required: true + type: string + responses: + "200": + description: 请求的 "allow" (放行) 标头过滤规则。 + schema: + $ref: '#/definitions/headerFilter' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 获取单个标头放行规则 + description: 获取具有给定 ID 的 "allow" (放行) 标头过滤规则。 + tags: + - admin + /api/v1/admin/header_blocks: + get: + operationId: headerFilterBlocksGet + responses: + "200": + description: 目前所有的 "block" (阻止) 标头过滤规则。 + schema: + items: + $ref: '#/definitions/headerFilter' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看标头阻止规则 + description: 获取目前所有的 "block" (阻止) 标头过滤规则。 + tags: + - admin + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + description: |- + 创建一条新的 "block" (阻止) HTTP 请求标头过滤规则。 + 若 Content-Type 为 'application/json',则参数也可以在请求体中以 JSON 形式给出。 + 若 Content-Type 为 'application/xml',则参数也可以在请求体中以 XML 形式给出。 + operationId: headerFilterBlockCreate + parameters: + - description: 要匹配的 HTTP 标头 (例如 User-Agent)。 + in: formData + name: header + required: true + type: string + x-go-name: Header + - description: 要匹配的标头值的正则表达式。 + in: formData + name: regex + required: true + type: string + x-go-name: Regex + produces: + - application/json + responses: + "200": + description: 新创建的 "block" (阻止) 标头过滤规则。 + schema: + $ref: '#/definitions/headerFilter' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 创建标头阻止规则 + tags: + - admin + /api/v1/admin/header_blocks/{id}: + delete: + operationId: headerFilterBlockDelete + parameters: + - description: 目标标头过滤规则 ID。 + in: path + name: id + required: true + type: string + responses: + "202": + description: Accepted 已接受 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 删除标头阻止规则 + description: 删除具有给定 ID 的 "block" (阻止) 标头过滤规则。 + tags: + - admin + get: + operationId: headerFilterBlockGet + parameters: + - description: 目标标头过滤规则 ID。 + in: path + name: id + required: true + type: string + responses: + "200": + description: 请求的 "block" (阻止) 标头过滤规则。 + schema: + $ref: '#/definitions/headerFilter' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 获取单个标头阻止规则 + description: 获取具有给定 ID 的 "block" (阻止) 标头过滤规则。 + tags: + - admin + /api/v1/admin/instance/rules: + post: + consumes: + - multipart/form-data + operationId: ruleCreate + parameters: + - description: 实例规则的文本内容,纯文本。 + in: formData + name: text + required: true + type: string + x-go-name: Text + produces: + - application/json + responses: + "200": + description: 新创建的实例规则。 + schema: + $ref: '#/definitions/instanceRule' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 创建实例规则 + description: 创建一条新的实例规则。 + tags: + - admin + /api/v1/admin/instance/rules/{id}: + delete: + consumes: + - multipart/form-data + operationId: ruleDelete + parameters: + - description: 要删除的规则的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 被删除的实例规则。 + schema: + $ref: '#/definitions/instanceRule' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 删除实例规则 + description: 删除一条现有的实例规则。 + tags: + - admin + patch: + consumes: + - multipart/form-data + operationId: ruleUpdate + parameters: + - description: 要更新的规则的 ID。 + in: path + name: id + required: true + type: string + x-go-name: ID + - description: 更新的实例规则的文本内容,纯文本。 + in: formData + name: text + required: true + type: string + x-go-name: Text + produces: + - application/json + responses: + "200": + description: 更新后的实例规则。 + schema: + $ref: '#/definitions/instanceRule' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 更新实例规则 + description: 更新一条现有的实例规则。 + tags: + - admin + /api/v1/admin/media_cleanup: + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + description: 删除早于给定日期的外站媒体。也会清理未使用的头像和横幅背景头图,并从存储中删除孤立的项目。 + operationId: mediaCleanup + parameters: + - description: |- + 要保留的外站媒体的天数。本地(Native)值将被视为 0。 + 若未指定值,则将使用服务器配置中的 media-remote-cache-days 的值。 + format: int64 + in: query + name: remote_cache_days + type: integer + x-go-name: RemoteCacheDays + produces: + - application/json + responses: + "200": + description: 返回请求的天数。清理将在请求完成后异步执行。 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 清理媒体 + tags: + - admin + /api/v1/admin/media_refetch: + post: + description: |- + 重新获取数据库中指定但在存储中丢失的媒体。 + 当前,仅包括外站表情。 + 当数据丢失时,您可以使用此端点尝试恢复到工作状态。 + operationId: mediaRefetch + parameters: + - description: 要重新获取媒体的实例。如果为空,则将重新获取所有实例的媒体。 + in: query + name: domain + type: string + produces: + - application/json + responses: + "202": + description: 请求已接受并将被处理。检查日志以查看进度/错误。 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 重新获取媒体 + tags: + - admin + /api/v1/admin/reports: + get: + description: |- + 查看发给管理员的用户举报。 + + 举报将按时间顺序(最新的在前)返回,带有连续的 ID(更大 = 更新)。 + + 下一个和上一个查询可以从返回的Link标头中解析。 + + 例子: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: adminReports + parameters: + - description: 如果设为 true,则只返回已解决的举报。如果设为 false,则只返回未解决的举报。如果未设置,则不会根据其解决状态过滤举报。 + in: query + name: resolved + type: boolean + - description: 只返回由给定 ID 的账户创建的举报。 + in: query + name: account_id + type: string + - description: 只返回针对给定 ID 的账户的举报。 + in: query + name: target_account_id + type: string + - description: 只返回比给定的 max ID *旧* 的举报(用于向下翻页)。具有指定 ID 的举报不会包含在响应中。 + in: query + name: max_id + type: string + - description: 只返回比给定的 since ID *新* 的举报。具有指定 ID 的举报不会包含在响应中。 + in: query + name: since_id + type: string + - description: 只返回比给定的 min ID 相邻且*更新* 的举报(用于向上翻页)。具有指定 ID 的举报不会包含在响应中。 + in: query + name: min_id + type: string + - default: 20 + description: 要返回的举报数量。 + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: 举报数组。 + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/adminReport' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看用户举报 + tags: + - admin + /api/v1/admin/reports/{id}: + get: + operationId: adminReportGet + parameters: + - description: 举报的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的举报。 + schema: + $ref: '#/definitions/adminReport' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看单个用户举报 + description: 查看具有给定 ID 的用户举报。 + tags: + - admin + /api/v1/admin/reports/{id}/resolve: + post: + consumes: + - application/json + - application/xml + - multipart/form-data + operationId: adminReportResolve + parameters: + - description: 举报的 ID。 + in: path + name: id + required: true + type: string + - description: |- + 管理员对此举报采取的行动的评论,可不填。在将举报标记为已处理之前,提供关于采取的行动(如果有)的解释非常有用。这将对创建举报的用户可见! + in: formData + name: action_taken_comment + type: string + produces: + - application/json + responses: + "200": + description: 已处理的举报。 + schema: + $ref: '#/definitions/adminReport' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 处理用户举报 + description: 将具有给定 ID 的用户举报标记为已处理。 + tags: + - admin + /api/v1/admin/rules: + get: + description: 查看实例规则及其 ID。实例规则将按顺序返回(按 Order 升序排序)。 + operationId: adminsRuleGet + produces: + - application/json + responses: + "200": + description: 本站实例的所有规则组成的数组。 + schema: + items: + $ref: '#/definitions/instanceRule' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看实例规则 + tags: + - admin + /api/v1/admin/rules/{id}: + get: + operationId: adminRuleGet + parameters: + - description: 实例规则的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的规则。 + schema: + $ref: '#/definitions/instanceRule' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看单个实例规则 + description: 查看具有给定 ID 的实例规则。 + tags: + - admin + /api/v1/apps: + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + description: |- + 在本实例注册一个新的应用程序。 + 注册的应用程序可用于获取应用程序令牌。 + 然后可以使用此令牌注册新帐户,或(通过用户身份验证)获取访问令牌。 + + 若 Content-Type 为 'application/json',则参数也可以在请求体中以 JSON 形式给出。 + 若 Content-Type 为 'application/xml',则参数也可以在请求体中以 XML 形式给出。 + operationId: appCreate + parameters: + - description: 应用程序的名称。 + in: formData + name: client_name + required: true + type: string + x-go-name: ClientName + - description: |- + 用户在授权后应重定向到何处。 + + 若要向用户显示授权代码而不是重定向到网页,请在此参数中使用 `urn:ietf:wg:oauth:2.0:oob`。 + in: formData + name: redirect_uris + required: true + type: string + x-go-name: RedirectURIs + - description: |- + 授权范围,以空格分隔的列表。 + + 如果未提供范围,则默认为 `read`。 + in: formData + name: scopes + type: string + x-go-name: Scopes + - description: 此应用程序的网页 URL(可选)。 + in: formData + name: website + type: string + x-go-name: Website + produces: + - application/json + responses: + "200": + description: 新创建的应用程序。 + schema: + $ref: '#/definitions/application' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + summary: 创建应用程序 + tags: + - apps + /api/v1/blocks: + get: + description: |- + 获取发起请求的账户已屏蔽的账户数组。 + 下一个和上一个查询可以从返回的Link标头中解析。 + 示例: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: blocksGet + parameters: + - description: '仅返回早于给定的 max ID 的已屏蔽账户。具有指定 ID 的已屏蔽账户不会包含在响应中。注意:ID 是内部屏蔽的 ID,而不是任何返回的账户的 ID。' + in: query + name: max_id + type: string + - description: '仅返回晚于给定的 since ID 的已屏蔽账户。具有指定 ID 的已屏蔽账户不会包含在响应中。注意:ID 是内部屏蔽的 ID,而不是任何返回的账户的 ID。' + in: query + name: since_id + type: string + - description: '仅返回比给定的 min ID *相邻且更新* 的已屏蔽账户。具有指定 ID 的已屏蔽账户不会包含在响应中。注意:ID 是内部屏蔽的 ID,而不是任何返回的账户的 ID。' + in: query + name: min_id + type: string + - default: 40 + description: 要返回的已屏蔽账户数量。 + in: query + maximum: 80 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: "" + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/account' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:blocks + summary: 获取已屏蔽账户 + tags: + - blocks + /api/v1/bookmarks: + get: + description: 获取在本站收藏的贴文的数组。 + operationId: bookmarksGet + parameters: + - default: 30 + description: 要返回的贴文数量。 + in: query + name: limit + type: integer + - description: 仅返回早于给定的收藏 ID 的已收藏贴文。具有相应收藏 ID 的贴文不会包含在响应中。 + in: query + name: max_id + type: string + - description: 仅返回比给定的收藏 ID *新* 的已收藏贴文。具有相应收藏 ID 的贴文不会包含在响应中。 + in: query + name: min_id + type: string + produces: + - application/json + responses: + "200": + description: 已收藏贴文的数组。 + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/status' + type: array + "401": + description: unauthorized 未授权 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:bookmarks + summary: 获取收藏列表 + tags: + - bookmarks + /api/v1/conversation/{id}/read: + post: + operationId: conversationRead + parameters: + - description: 对话的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 更新后的对话。 + schema: + $ref: '#/definitions/conversation' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "422": + description: unprocessable 无法处理 content + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:conversations + summary: 标记对话为已读 + description: 将具有给定 ID 的对话标记为已读。 + tags: + - conversations + /api/v1/conversations: + get: + description: |- + 获取发起请求的账户参与的对话(私信)数组。 + 下一个和上一个查询可以从返回的Link标头中解析。 + 示例: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: conversationsGet + parameters: + - description: '仅返回最后贴文早于给定的 max ID 的对话。具有相应贴文 ID 的对话不会包含在响应中。注意:ID 是贴文 ID。使用Link标头进行分页。' + in: query + name: max_id + type: string + - description: '仅返回最后贴文晚于给定的 since ID 的对话。具有相应贴文 ID 的对话不会包含在响应中。注意:ID 是贴文 ID。使用Link标头进行分页。' + in: query + name: since_id + type: string + - description: '仅返回最后贴文 **相邻且晚于** 给定的 since ID 的对话。具有相应贴文 ID 的对话不会包含在响应中。注意:ID 是贴文 ID。使用Link标头进行分页。' + in: query + name: min_id + type: string + - default: 40 + description: 要返回的对话数量。 + in: query + maximum: 80 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: "" + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/conversation' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:statuses + summary: 获取对话列表 + tags: + - conversations + /api/v1/conversations/{id}: + delete: + description: |- + 删除具有给定 ID 的单个对话。 + 这不会删除对话包含的实际贴文,也不会阻止参与者稍后从相同的贴文串和参与者创建新对话。 + operationId: conversationDelete + parameters: + - description: 对话的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 对话已删除 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:conversations + summary: 删除对话 + tags: + - conversations + /api/v1/custom_emojis: + get: + operationId: customEmojisGet + produces: + - application/json + responses: + "200": + description: 自定义表情数组。 + schema: + items: + $ref: '#/definitions/emoji' + type: array + "401": + description: unauthorized 未授权 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:custom_emojis + summary: 获取自定义表情 + description: 获取本实例可用的自定义表情数组。 + tags: + - custom_emojis + /api/v1/exports/blocks.csv: + get: + operationId: exportBlocks + produces: + - text/csv + responses: + "200": + description: 你屏蔽的账户的 CSV 文件。 + "401": + description: unauthorized 未授权 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:blocks + summary: 导出屏蔽列表 + description: 导出你屏蔽的账户的 CSV 文件。 + tags: + - import-export + /api/v1/exports/followers.csv: + get: + operationId: exportFollowers + produces: + - text/csv + responses: + "200": + description: 你的粉丝的 CSV 文件。 + "401": + description: unauthorized 未授权 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:follows + summary: 导出粉丝列表 + description: 导出你的粉丝的 CSV 文件。 + tags: + - import-export + /api/v1/exports/following.csv: + get: + operationId: exportFollowing + produces: + - text/csv + responses: + "200": + description: 你关注的账户的 CSV 文件。 + "401": + description: unauthorized 未授权 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:follows + summary: 导出关注列表 + description: 导出你关注的账户的 CSV 文件。 + tags: + - import-export + /api/v1/exports/lists.csv: + get: + operationId: exportLists + produces: + - text/csv + responses: + "200": + description: 列表的 CSV 文件。 + "401": + description: unauthorized 未授权 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:lists + summary: 导出所有列表 + description: 导出你创建的列表的 CSV 文件。 + tags: + - import-export + /api/v1/exports/mutes.csv: + get: + operationId: exportMutes + produces: + - text/csv + responses: + "200": + description: 你屏蔽的账户的 CSV 文件。 + "401": + description: unauthorized 未授权 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:mutes + summary: 导出静音列表 + description: 导出你静音/隐藏的账户的 CSV 文件。 + tags: + - import-export + /api/v1/exports/stats: + get: + operationId: exportStats + produces: + - application/json + responses: + "200": + description: 导出统计信息 + schema: + $ref: '#/definitions/accountExportStats' + "401": + description: unauthorized 未授权 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:account + summary: 查看导出统计信息 + description: 返回对应账户有关可以导出的项目数量的统计信息。 + tags: + - import-export + /api/v1/favourites: + get: + description: |- + 查看发起请求的账户已点赞的贴文数组。 + 下一个和上一个查询可以从返回的Link标头中解析。 + 示例: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: favouritesGet + parameters: + - default: 20 + description: 要返回的贴文数量。 + in: query + name: limit + type: integer + - description: 仅返回比给定的点赞 ID *旧* 的贴文。具有相应点赞 ID 的贴文不会包含在响应中。 + in: query + name: max_id + type: string + - description: 仅返回比给定的点赞 ID *新* 的贴文。具有相应点赞 ID 的贴文不会包含在响应中。 + in: query + name: min_id + type: string + produces: + - application/json + responses: + "200": + description: "" + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/status' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:favourites + summary: 查看点赞贴文 + tags: + - favourites + /api/v1/featured_tags: + get: + description: '获取你当前在个人资料上展示的所有话题标签的数组。**此端点尚未完全实现**:它将始终返回一个空数组。' + operationId: getFeaturedTags + produces: + - application/json + responses: + "200": + description: "" + schema: + items: + type: object + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:accounts + summary: 获取精选话题标签 + tags: + - tags + /api/v1/filters: + get: + operationId: filtersV1Get + produces: + - application/json + responses: + "200": + description: 请求的过滤规则。 + schema: + items: + $ref: '#/definitions/filterV1' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:filters + summary: 获取V1过滤规则 + description: 获取发起请求的账户的所有过滤规则。 + tags: + - filters + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + operationId: filterV1Post + parameters: + - description: |- + 要过滤的文本。 + + 示例:挂人 + in: formData + maxLength: 40 + minLength: 1 + name: phrase + required: true + type: string + - collectionFormat: multi + description: |- + 此过滤规则要应用的上下文。 + + 示例: home, public + enum: + - home + - notifications + - public + - thread + - account + in: formData + items: + type: string + minItems: 1 + name: context[] + required: true + type: array + uniqueItems: true + - description: |- + 此过滤规则的过期时间(秒)。如果省略,则过滤规则永不过期。 + + 示例:86400 + in: formData + name: expires_in + type: number + - default: false + description: |- + 被命中的条目是否应该从用户的时间线/视图中移除,而不是隐藏?目前不支持。 + + 示例:false + in: formData + name: irreversible + type: boolean + - default: false + description: |- + 此过滤规则是否应考虑单词边界(整词匹配)? + + 示例:true + in: formData + name: whole_word + type: boolean + produces: + - application/json + responses: + "200": + description: 新过滤规则。 + schema: + $ref: '#/definitions/filterV1' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: 禁止对已迁移的账户执行此操作 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "409": + description: conflict (duplicate keyword) + "422": + description: unprocessable 无法处理 content + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:filters + summary: 创建V1过滤规则 + description: 创建一条新的V1过滤规则。 + tags: + - filters + /api/v1/filters/{id}: + delete: + operationId: filterV1Delete + parameters: + - description: 过滤规则的 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 过滤规则已删除 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:filters + summary: 删除V1过滤规则 + description: 删除具有给定 ID 的单个过滤规则。 + tags: + - filters + get: + operationId: filterV1Get + parameters: + - description: 过滤规则的 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的过滤规则。 + schema: + $ref: '#/definitions/filterV1' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:filters + summary: 获取单个V1过滤规则 + description: 获取具有给定 ID 的单个V1过滤规则。 + tags: + - filters + put: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + operationId: filterV1Put + parameters: + - description: 过滤规则的 ID. + in: path + name: id + required: true + type: string + - description: |- + 要过滤的文本。 + + 示例: 挂人 + in: formData + maxLength: 40 + minLength: 1 + name: phrase + required: true + type: string + - collectionFormat: multi + description: |- + 此过滤规则要应用的上下文。 + + 示例: home, public + enum: + - home + - notifications + - public + - thread + - account + in: formData + items: + type: string + minItems: 1 + name: context[] + required: true + type: array + uniqueItems: true + - description: |- + 此过滤规则的过期时间(秒)。如果省略,则过滤规则永不过期。 + + 示例: 86400 + in: formData + name: expires_in + type: number + - default: false + description: |- + 被命中的条目是否应该从用户的时间线/视图中移除,而不是隐藏?目前不支持。 + + 示例: false + in: formData + name: irreversible + type: boolean + - default: false + description: |- + 此过滤规则是否应考虑单词边界(整词匹配)? + + 示例: true + in: formData + name: whole_word + type: boolean + produces: + - application/json + responses: + "200": + description: 更新后的过滤规则。 + schema: + $ref: '#/definitions/filterV1' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: 禁止对已迁移的账户执行此操作 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "409": + description: conflict (duplicate keyword) + "422": + description: unprocessable 无法处理 content + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:filters + summary: 更新单个V1过滤规则 + description: 更新具有给定 ID 的单个过滤规则。 + tags: + - filters + /api/v1/follow_requests: + get: + description: |- + 获取向你发起关注请求的账户数组。 + 下一个和上一个查询可以从返回的Link标头中解析。 + 示例: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: getFollowRequests + parameters: + - description: '仅返回比给定的 max ID *旧* 的关注请求账户。具有指定 ID 的关注请求账户不会包含在响应中。注意:ID 是内部关注请求的 ID,而不是任何返回的账户的 ID。' + in: query + name: max_id + type: string + - description: '仅返回比给定的 since ID *新* 的关注请求账户。具有指定 ID 的关注请求账户不会包含在响应中。注意:ID 是内部关注请求的 ID,而不是任何返回的账户的 ID。' + in: query + name: since_id + type: string + - description: '仅返回比给定的 min ID *相邻且更新* 的关注请求账户。具有指定 ID 的关注请求账户不会包含在响应中。注意:ID 是内部关注请求的 ID,而不是任何返回的账户的 ID。' + in: query + name: min_id + type: string + - default: 40 + description: 要返回的关注请求账户数量。 + in: query + maximum: 80 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: "" + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/account' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:follows + summary: 获取关注请求列表 + tags: + - follow_requests + /api/v1/follow_requests/{account_id}/authorize: + post: + description: 接受/批准来自给定账户 ID 的关注请求,并将请求账户放入你的“粉丝”列表。 + operationId: authorizeFollowRequest + parameters: + - description: 要接受的账户的 ID。 + in: path + name: account_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 你与此账户的关系(relationship)。 + schema: + $ref: '#/definitions/accountRelationship' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:follows + summary: 接受关注请求 + tags: + - follow_requests + /api/v1/follow_requests/{account_id}/reject: + post: + operationId: rejectFollowRequest + parameters: + - description: 要拒绝的账户的 ID。 + in: path + name: account_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 你与此账户的关系(relationship)。 + schema: + $ref: '#/definitions/accountRelationship' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:follows + summary: 拒绝关注请求 + description: 拒绝来自给定账户 ID 的关注请求。 + tags: + - follow_requests + /api/v1/followed_tags: + get: + operationId: getFollowedTags + parameters: + - description: '仅返回比给定的 max ID *旧* 的已关注话题标签。具有相应 max ID 的已关注话题标签不会包含在响应中。注意:ID 是已关注话题标签的内部 ID,而不是任何返回的话题标签的 ID。' + in: query + name: max_id + type: string + - description: '仅返回比给定的 since ID *新* 的已关注话题标签。具有相应 since ID 的已关注话题标签不会包含在响应中。注意:ID 是已关注话题标签的内部 ID,而不是任何返回的话题标签的 ID。' + in: query + name: since_id + type: string + - description: '仅返回比给定的 min ID *相邻且更新* 的已关注话题标签。具有相应 min ID 的已关注话题标签不会包含在响应中。注意:ID 是已关注话题标签的内部 ID,而不是任何返回的话题标签的 ID。' + in: query + name: min_id + type: string + - default: 100 + description: 要返回的已关注话题标签数量。 + in: query + maximum: 200 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: "" + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/tag' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:follows + summary: 获取已关注话题标签 + description: 获取你当前关注的所有话题标签的数组。 + tags: + - tags + /api/v1/import: + post: + consumes: + - multipart/form-data + description: |- + 将 CSV 数据文件上传到你的账户。 + 这可以用于从 Mastodon 兼容的 CSV 文件迁移数据到 GoToSocial 账户。 + + 上传的数据将异步处理,并且可能不会处理所有条目,具体取决于实例屏蔽、用户级屏蔽、引用账户和状态的网络可用性等。 + operationId: importData + parameters: + - description: 要上传的 CSV 数据文件。 + in: formData + name: data + required: true + type: file + - description: |- + 此数据文件中包含的条目类型: + - `following` - 要关注的账户。 - `blocks` - 要屏蔽的账户。 + in: formData + name: type + required: true + type: string + - default: merge + description: |- + 根据数据文件创建对应的条目时使用的模式: + - `merge` 将数据文件中的条目与现有条目合并。 - `overwrite` 用数据文件中的条目替换现有条目。 + in: formData + name: mode + type: string + produces: + - application/json + responses: + "202": + description: 上传的数据文件已被接受。 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:accounts + summary: 导入数据 + tags: + - import-export + /api/v1/instance: + get: + operationId: instanceGetV1 + produces: + - application/json + responses: + "200": + description: 实例信息 + schema: + $ref: '#/definitions/instanceV1' + "406": + description: not acceptable 不可接受 + "500": + description: internal error + summary: 查看实例信息 + tags: + - instance + patch: + consumes: + - multipart/form-data + description: 更新实例信息和/或上传新的头像/横幅头图。此端点需要实例管理员权限。 + operationId: instanceUpdate + parameters: + - allowEmptyValue: true + description: 实例的标题。 + in: formData + maxLength: 40 + name: title + type: string + - allowEmptyValue: true + description: 联系账户的用户名。这必须是实例管理员的用户名。 + in: formData + name: contact_username + type: string + - allowEmptyValue: true + description: 实例联系邮箱。 + in: formData + name: contact_email + type: string + - allowEmptyValue: true + description: 实例的简短描述。 + in: formData + maxLength: 500 + name: short_description + type: string + - allowEmptyValue: true + description: 实例的详细描述。 + in: formData + maxLength: 5000 + name: description + type: string + - allowEmptyValue: true + description: 实例的条款和条件。 + in: formData + maxLength: 5000 + name: terms + type: string + - description: 实例的缩略图。 + in: formData + name: thumbnail + type: file + - description: 提交的实例缩略图的图像描述。 + in: formData + name: thumbnail_description + type: string + - description: 实例的头部背景横幅。 + in: formData + name: header + type: file + produces: + - application/json + responses: + "200": + description: 更新后的实例信息。 + schema: + $ref: '#/definitions/instanceV1' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 更新实例信息 + tags: + - instance + /api/v1/instance/peers: + get: + operationId: instancePeersGet + parameters: + - default: open + description: |- + 要应用于结果的过滤规则列表,以逗号分隔。可被识别的过滤规则包括: + - `open` -- 包括未被禁止或静音的同伴实例 + - `suspended` -- 包括已被禁止的同伴实例。 + + 如果过滤规则是 `open`,则只会返回未被禁止或静音的实例。 + + 如果过滤规则是 `suspended`,则只会显示已被禁止的实例。 + + 如果过滤规则是 `open,suspended`,则将返回所有已知实例。 + + 如果过滤规则是空字符串或未设置,则将默认为 `open`。 + in: query + name: filter + type: string + produces: + - application/json + responses: + "200": + description: |- + 如果未提供 filter 参数,或 filter 为空,则将返回一个遗留的、与 Mastodon-API 兼容的响应。这将仅包含一个类似 `["example.com", "example.org"]` 的字符串数组,对应于此实例的同伴实例的域名。 + + 如果提供了 filter 参数,则将返回一个对象数组,每个对象都至少包含一个 `domain` 键。 + + 被静音或被禁止的域名还将有一个包含 iso8601 date 格式字符串的 `suspended_at` 或 `silenced_at` 键。如果域名对象不包含这两个键,则它是开放的。在某些情况下,已被禁止的实例可能会被混淆,这意味着它们的一些字母将被 `*` 替换,以使恶意用户更难以针对实例进行骚扰。 + + 无论返回的是扁平响应还是更详细的响应,域名都将按主机名字母顺序排序。 + schema: + items: + $ref: '#/definitions/domain' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + tags: + - instance + /api/v1/instance/rules: + get: + description: 实例规则将按顺序返回(按 Order 升序排序)。 + operationId: rules + produces: + - application/json + responses: + "200": + description: 本站实例的所有规则组成的数组。 + schema: + items: + $ref: '#/definitions/instanceRule' + type: array + "400": + description: bad request 无效请求 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + summary: 查看实例规则(公开) + tags: + - instance + /api/v1/interaction_policies/defaults: + get: + operationId: policiesDefaultsGet + produces: + - application/json + responses: + "200": + description: 一个默认的策略对象,包含每种贴文可见性的互动规则。 + schema: + $ref: '#/definitions/defaultPolicies' + "401": + description: unauthorized 未授权 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:accounts + summary: 获取默认互动规则 + description: 获取你创建的新贴文的默认互动规则。 + tags: + - interaction_policies + patch: + consumes: + - multipart/form-data + - application/x-www-form-urlencoded + - application/json + description: |- + 为你创建的不同可见性级别的新贴文更新默认互动策略。 + + 如果使用表单数据提交,请使用以下范式: + + `VISIBILITY[INTERACTION_TYPE][CONDITION][INDEX]=Value` + + 例如:`public[can_reply][always][0]=author` + + 使用 `curl` 调用时可以为如下形式: + + `curl -F 'public[can_reply][always][0]=author' -F 'public[can_reply][always][1]=followers'` + + 其 JSON 等效形式为: + + `curl -H 'Content-Type: application/json' -d '{"public":{"can_reply":{"always":["author","followers"]}}}'` + + 请求体中未指定的任何可见性级别将被重置为默认值。 + + 例如,在上面的示例中,“public” 将被更新,但“unlisted”、“private” 和“direct” 将被重置为默认值。 + + 服务器将对提交的策略执行一些规范化,以免您提交完全无效的策略。 + operationId: policiesDefaultsUpdate + parameters: + - description: public.can_favourite.always 的第 N 个条目。 + in: formData + name: public[can_favourite][always][0] + type: string + - description: public.can_favourite.with_approval 的第 N 个条目。 + in: formData + name: public[can_favourite][with_approval][0] + type: string + - description: public.can_reply.always 的第 N 个条目。 + in: formData + name: public[can_reply][always][0] + type: string + - description: public.can_reply.with_approval 的第 N 个条目。 + in: formData + name: public[can_reply][with_approval][0] + type: string + - description: public.can_reblog.always 的第 N 个条目。 + in: formData + name: public[can_reblog][always][0] + type: string + - description: public.can_reblog.with_approval 的第 N 个条目。 + in: formData + name: public[can_reblog][with_approval][0] + type: string + - description: unlisted.can_favourite.always 的第 N 个条目。 + in: formData + name: unlisted[can_favourite][always][0] + type: string + - description: unlisted.can_favourite.with_approval 的第 N 个条目。 + in: formData + name: unlisted[can_favourite][with_approval][0] + type: string + - description: unlisted.can_reply.always 的第 N 个条目。 + in: formData + name: unlisted[can_reply][always][0] + type: string + - description: unlisted.can_reply.with_approval 的第 N 个条目。 + in: formData + name: unlisted[can_reply][with_approval][0] + type: string + - description: unlisted.can_reblog.always 的第 N 个条目。 + in: formData + name: unlisted[can_reblog][always][0] + type: string + - description: unlisted.can_reblog.with_approval 的第 N 个条目。 + in: formData + name: unlisted[can_reblog][with_approval][0] + type: string + - description: private.can_favourite.always 的第 N 个条目。 + in: formData + name: private[can_favourite][always][0] + type: string + - description: private.can_favourite.with_approval 的第 N 个条目。 + in: formData + name: private[can_favourite][with_approval][0] + type: string + - description: private.can_reply.always 的第 N 个条目。 + in: formData + name: private[can_reply][always][0] + type: string + - description: private.can_reply.with_approval 的第 N 个条目。 + in: formData + name: private[can_reply][with_approval][0] + type: string + - description: private.can_reblog.always 的第 N 个条目。 + in: formData + name: private[can_reblog][always][0] + type: string + - description: private.can_reblog.with_approval 的第 N 个条目。 + in: formData + name: private[can_reblog][with_approval][0] + type: string + - description: direct.can_favourite.always 的第 N 个条目。 + in: formData + name: direct[can_favourite][always][0] + type: string + - description: direct.can_favourite.with_approval 的第 N 个条目。 + in: formData + name: direct[can_favourite][with_approval][0] + type: string + - description: direct.can_reply.always 的第 N 个条目。 + in: formData + name: direct[can_reply][always][0] + type: string + - description: direct.can_reply.with_approval 的第 N 个条目。 + in: formData + name: direct[can_reply][with_approval][0] + type: string + - description: direct.can_reblog.always 的第 N 个条目。 + in: formData + name: direct[can_reblog][always][0] + type: string + - description: direct.can_reblog.with_approval 的第 N 个条目。 + in: formData + name: direct[can_reblog][with_approval][0] + type: string + produces: + - application/json + responses: + "200": + description: 更新后的默认策略对象,包含每种贴文可见性的互动规则。 + schema: + $ref: '#/definitions/defaultPolicies' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "406": + description: not acceptable 不可接受 + "422": + description: unprocessable 无法处理 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:accounts + summary: 更新默认互动规则 + tags: + - interaction_policies + /api/v1/interaction_requests: + get: + description: |- + 获取其他账户对你的贴文发起的,等待你批准的互动请求的数组。 + ``` + ; rel="next", ; rel="prev" + ```` + operationId: getInteractionRequests + parameters: + - description: 若设置,则只有针对给定 status_id 的互动请求将包含在结果中。 + in: query + name: status_id + type: string + - default: true + description: 如果为 true 或未设置,则将在结果中包含待批准的点赞。favourites, replies 和 reblogs 中至少有一个必须为 true。 + in: query + name: favourites + type: boolean + - default: true + description: 如果为 true 或未设置,则将在结果中包含待批准的回复。favourites, replies 和 reblogs 中至少有一个必须为 true。 + in: query + name: replies + type: boolean + - default: true + description: 如果为 true 或未设置,则将在结果中包含待批准的转发。favourites, replies 和 reblogs 中至少有一个必须为 true。 + in: query + name: reblogs + type: boolean + - description: 仅返回比给定的 max ID *旧* 的互动请求。具有指定 ID 的互动请求不会包含在响应中。 + in: query + name: max_id + type: string + - description: 仅返回比给定的 since ID *新* 的互动请求。具有指定 ID 的互动请求不会包含在响应中。 + in: query + name: since_id + type: string + - description: 仅返回比给定的 min ID *相邻且更新* 的互动请求。具有指定 ID 的互动请求不会包含在响应中。 + in: query + name: min_id + type: string + - default: 40 + description: 要返回的互动请求数量。 + in: query + maximum: 80 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: "" + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/interactionRequest' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:notifications + summary: 获取互动请求列表 + tags: + - interaction_requests + /api/v1/interaction_requests/{id}: + get: + operationId: getInteractionRequest + parameters: + - description: 对你发出的互动请求的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 互动请求。 + schema: + $ref: '#/definitions/interactionRequest' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:notifications + summary: 获取单个互动请求 + description: 获取具有给定 ID 的互动请求。 + tags: + - interaction_requests + /api/v1/interaction_requests/{id}/authorize: + post: + operationId: authorizeInteractionRequest + parameters: + - description: 对你发出的互动请求的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 被批准后的互动请求。 + schema: + $ref: '#/definitions/interactionRequest' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:statuses + summary: 接受互动请求 + description: 接受/授权/批准具有给定 ID 的互动请求。 + tags: + - interaction_requests + /api/v1/interaction_requests/{id}/reject: + post: + operationId: rejectInteractionRequest + parameters: + - description: 对你发出的互动请求的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 被拒绝的互动请求。 + schema: + $ref: '#/definitions/interactionRequest' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:statuses + summary: 拒绝互动请求 + description: 拒绝具有给定 ID 的互动请求。 + tags: + - interaction_requests + /api/v1/lists: + get: + operationId: lists + produces: + - application/json + responses: + "200": + description: 发起请求的用户拥有的所有列表的数组。 + schema: + items: + $ref: '#/definitions/list' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:lists + summary: 获取所有列表 + description: 获取授权用户拥有的所有列表。 + tags: + - lists + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + operationId: listCreate + parameters: + - description: |- + 列表的标题。 + 示例: 大佬 + in: formData + name: title + required: true + type: string + x-go-name: Title + - default: list + description: |- + 此列表的回复策略。 + followed = 显示对任何关注的用户的回复 + list = 显示对列表成员的回复 + none = 不显示回复 + 示例: list + enum: + - followed + - list + - none + in: formData + name: replies_policy + type: string + x-go-name: RepliesPolicy + - default: false + description: 将此列表的成员的贴文从你的主页时间线中隐藏。 + in: formData + name: exclusive + type: boolean + x-go-name: Exclusive + produces: + - application/json + responses: + "200": + description: 新创建的列表。 + schema: + $ref: '#/definitions/list' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:lists + summary: 创建列表 + tags: + - lists + /api/v1/lists/{id}: + delete: + operationId: listDelete + parameters: + - description: 列表的 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 列表已删除 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:lists + summary: 删除列表 + description: 删除具有给定 ID 的列表。 + tags: + - lists + get: + operationId: list + parameters: + - description: 列表的 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的列表。 + schema: + $ref: '#/definitions/list' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:lists + summary: 获取列表 + description: 获取具有给定 ID 的列表。 + tags: + - lists + put: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + operationId: listUpdate + parameters: + - description: 列表的 ID + in: path + name: id + required: true + type: string + - description: |- + 列表的标题。 + 示例: 大佬 + in: formData + name: title + type: string + - description: |- + 列表的回复展示规则。 + followed = 显示对任何关注的用户的回复 + list = 显示对列表成员的回复 + none = 不显示回复 + 示例: list + enum: + - followed + - list + - none + in: formData + name: replies_policy + type: string + - description: 将此列表的成员的贴文从你的主页时间线中隐藏。 + in: formData + name: exclusive + type: boolean + produces: + - application/json + responses: + "200": + description: 更新后的列表。 + schema: + $ref: '#/definitions/list' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:lists + summary: 更新列表 + tags: + - lists + /api/v1/lists/{id}/accounts: + delete: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + operationId: removeListAccounts + parameters: + - description: 列表的 ID + in: path + name: id + required: true + type: string + - collectionFormat: multi + description: 要编辑的账户 ID 数组。每个账户 ID 必须对应于一个发起请求的账户关注的账户。 + in: formData + items: + type: string + name: account_ids[] + required: true + type: array + produces: + - application/json + responses: + "200": + description: 列表账户已更新 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:lists + summary: 从列表删除账户 + description: 从给定列表中删除一个或多个账户。 + tags: + - lists + get: + description: |- + 分页浏览给定列表中的账户。 + 返回的 Link 标头可用于在时间线向上或向下滚动时生成上一个和下一个查询。 + + 示例: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: listAccounts + parameters: + - description: 列表的 ID + in: path + name: id + required: true + type: string + - description: 仅返回比给定的 max ID *旧* 的列表条目。具有指定 ID 的列表条目对应的账户不会包含在响应中。 + in: query + name: max_id + type: string + - description: 仅返回比给定的 since ID *新* 的列表条目。具有指定 ID 的列表条目对应的账户不会包含在响应中。 + in: query + name: since_id + type: string + - description: 仅返回比给定的 min ID *相邻且更新* 的列表条目。具有指定 ID 的列表条目对应的账户不会包含在响应中。 + in: query + name: min_id + type: string + - default: 40 + description: '要返回的账户数量。如果显式设置为 0,则列表中的所有账户将被返回,并且不会使用分页标头。这是针对 Mastodon API 的一个特殊情况的解决方法:https://docs.joinmastodon.org/methods/lists/#query-parameters。' + in: query + maximum: 80 + minimum: 0 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: 账户数组。 + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/account' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:lists + summary: 查看列表中的账户 + tags: + - lists + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + operationId: addListAccounts + parameters: + - description: 列表的 ID + in: path + name: id + required: true + type: string + - collectionFormat: multi + description: 要编辑的账户 ID 数组。每个账户 ID 必须对应于一个发起请求的账户关注的账户。 + in: formData + items: + type: string + name: account_ids[] + required: true + type: array + produces: + - application/json + responses: + "200": + description: 列表账户已更新 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:lists + summary: 向列表添加账户 + description: 向给定列表中添加一个或多个账户。 + tags: + - lists + /api/v1/markers: + get: + description: 根据名称获取时间线标记 + operationId: markersGet + parameters: + - description: 要检索的时间线。 + in: query + items: + enum: + - home + - notifications + type: string + name: timeline + type: array + produces: + - application/json + responses: + "200": + description: 请求的标记 + schema: + $ref: '#/definitions/markers' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:statuses + tags: + - markers + post: + consumes: + - multipart/form-data + description: 根据名称更新时间线标记 + operationId: markersPost + parameters: + - description: 在主页时间线上最后阅读的贴文 ID。 + in: formData + name: home[last_read_id] + type: string + - description: 在通知时间线上最后阅读的通知 ID。 + in: formData + name: notifications[last_read_id] + type: string + produces: + - application/json + responses: + "200": + description: 请求的标记 + schema: + $ref: '#/definitions/markers' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "409": + description: 冲突(当两个客户端尝试同时更新同一时间线时) + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:statuses + tags: + - markers + /api/v1/media/{id}: + get: + operationId: mediaGet + parameters: + - description: 附件的 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的媒体附件。 + schema: + $ref: '#/definitions/attachment' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:media + summary: 获取单个媒体附件 + description: 获取一个你拥有的媒体附件。 + tags: + - media + put: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + description: |- + 你必须拥有这个媒体附件,并且这个附件还没有被附加到任何贴文上。 + + 若 Content-Type 为 'application/json',则参数也可以在请求体中以 JSON 形式给出。 + 若 Content-Type 为 'application/xml',则参数也可以在请求体中以 XML 形式给出。 + operationId: mediaUpdate + parameters: + - description: 要更新的附件的 ID + in: path + name: id + required: true + type: string + - allowEmptyValue: true + description: 图片或媒体描述,用作附件的 alt 文本。这对于使用屏幕阅读器的用户非常有用!根据实例设置,可能必填,也可能选填。 + in: formData + name: description + type: string + - allowEmptyValue: true + default: 0,0 + description: '媒体文件的焦点。如果存在,它应该是两个介于 -1 和 1 之间的逗号分隔的浮点数。例如:`-0.5,0.25`。' + in: formData + name: focus + type: string + produces: + - application/json + responses: + "200": + description: 更新后的媒体附件。 + schema: + $ref: '#/definitions/attachment' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:media + summary: 更新单个媒体附件 + tags: + - media + /api/v1/mutes: + get: + description: |- + 获取发起请求的账户静音的账户列表。 + 下一个和上一个查询可以从返回的Link标头中解析。 + 示例: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: mutesGet + parameters: + - description: '仅返回比给定的 max ID *旧* 的被静音账户。具有指定 ID 的被静音账户不会包含在响应中。' + in: query + name: max_id + type: string + - description: '仅返回比给定的 since ID *新* 的被静音账户。具有指定 ID 的被静音账户不会包含在响应中。' + in: query + name: since_id + type: string + - description: '仅返回比给定的 min ID *相邻且更新* 的被静音账户。具有指定 ID 的被静音账户不会包含在响应中。' + in: query + name: min_id + type: string + - default: 40 + description: 要返回的被静音账户数量。 + in: query + maximum: 80 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: 被静音账户的列表,包括他们的静音到期时间(如果适用)。 + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/mutedAccount' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:mutes + summary: 获取静音账户列表 + tags: + - mutes + /api/v1/notification/{id}: + get: + operationId: notification + parameters: + - description: 通知的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的通知。 + schema: + $ref: '#/definitions/notification' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:notifications + summary: 获取单个通知 + description: 获取单个具有给定 ID 的通知。 + tags: + - notifications + /api/v1/notifications: + get: + description: |- + 获取当前授权用户的通知。 + 通知将按照时间顺序(最新的在前)返回,具有连续的 ID(更大 = 更新)。 + + 下一个和上一个查询可以从返回的Link标头中解析。 + 示例: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: notifications + parameters: + - description: 仅返回比给定的 max ID *旧* 的通知。具有指定 ID 的通知不会包含在响应中。 + in: query + name: max_id + type: string + - description: 仅返回比给定的 since ID *新* 的通知。具有指定 ID 的通知不会包含在响应中。 + in: query + name: since_id + type: string + - description: 仅返回比给定的 since ID *相邻且更新* 的通知。具有指定 ID 的通知不会包含在响应中。 + in: query + name: min_id + type: string + - default: 20 + description: 要返回的通知数量。 + in: query + name: limit + type: integer + - description: 要包含的通知类型。如果未提供,则将包含所有通知类型。 + in: query + items: + enum: + - follow + - follow_request + - mention + - reblog + - favourite + - poll + - status + - admin.sign_up + type: string + name: types[] + type: array + - description: 要排除的通知类型。 + in: query + items: + enum: + - follow + - follow_request + - mention + - reblog + - favourite + - poll + - status + - admin.sign_up + type: string + name: exclude_types[] + type: array + produces: + - application/json + responses: + "200": + description: 通知数组。 + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/notification' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:notifications + summary: 获取通知列表 + tags: + - notifications + /api/v1/notifications/clear: + post: + description: 清空/删除当前授权用户的所有通知。如果成功,将返回一个空对象 `{}`。 + operationId: clearNotifications + produces: + - application/json + responses: + "200": + description: "" + schema: + type: object + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:notifications + summary: 清空通知 + tags: + - notifications + /api/v1/polls/{id}: + get: + operationId: poll + parameters: + - description: 目标投票 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的投票。 + schema: + $ref: '#/definitions/poll' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:statuses + summary: 获取投票 + description: 获取具有给定 ID 的投票。 + tags: + - polls + /api/v1/polls/{id}/votes: + post: + operationId: pollVote + parameters: + - description: 目标投票 ID。 + in: path + name: id + required: true + type: string + - description: 用户选择的投票项索引。 + in: formData + items: + type: integer + name: choices + required: true + type: array + produces: + - application/json + responses: + "200": + description: 更新后的投票和用户投票选择。 + schema: + $ref: '#/definitions/poll' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "422": + description: unprocessable 无法处理 entity + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:statuses + summary: 参与投票 + description: 对给定投票进行投票。 + tags: + - polls + /api/v1/preferences: + get: + description: |- + 返回包含用户偏好设置的对象。 + 示例: + + ``` + + { + "posting:default:visibility": "public", + "posting:default:sensitive": false, + "posting:default:language": "en", + "reading:expand:media": "default", + "reading:expand:spoilers": false, + "reading:autoplay:gifs": false + } + + ```` + operationId: preferencesGet + produces: + - application/json + responses: + "200": + description: "" + schema: + type: object + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:accounts + summary: 获取用户偏好设置 + tags: + - preferences + /api/v1/profile/avatar: + delete: + description: 删除当前认证账户的头像。如果账户没有头像,调用也会成功。 + operationId: accountAvatarDelete + produces: + - application/json + responses: + "200": + description: 更新后的账户,包括个人资料源信息。 + schema: + $ref: '#/definitions/account' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 删除头像 + tags: + - accounts + /api/v1/profile/header: + delete: + description: 删除当前认证账户的横幅背景头图。如果账户没有横幅背景头图,调用也会成功。 + operationId: accountHeaderDelete + produces: + - application/json + responses: + "200": + description: 更新后的账户,包括个人资料源信息。 + schema: + $ref: '#/definitions/account' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 删除横幅背景 + tags: + - accounts + /api/v1/reports: + get: + description: |- + 查看发起请求的账户创建的举报。 + + 举报将按时间顺序(最新的在前)返回,具有连续的 ID(更大 = 更新)。 + + 下一个和上一个查询可以从返回的Link标头中解析。 + + 示例: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: reports + parameters: + - description: 如果设置为 true,只返回已解决的举报。如果设置为 false,只返回未解决的举报。如果未设置,举报将不会根据其解决状态进行过滤。 + in: query + name: resolved + type: boolean + - description: 仅返回针对目标账户 ID 的举报。 + in: query + name: target_account_id + type: string + - description: 仅返回比给定的 max ID *旧* 的举报。具有指定 ID 的举报不会包含在响应中。 + in: query + name: max_id + type: string + - description: 仅返回比给定的 since ID *新* 的举报。具有指定 ID 的举报不会包含在响应中。 + in: query + name: since_id + type: string + - description: 仅返回比给定的 min ID *相邻且更新* 的举报。具有指定 ID 的举报不会包含在响应中。 + in: query + name: min_id + type: string + - default: 20 + description: 要返回的举报数量。 + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: 举报的数组。 + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/report' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:reports + summary: 获取举报列表 + tags: + - reports + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + operationId: reportCreate + parameters: + - description: |- + 要举报的账户 ID。 + 示例: 01GPE75FXSH2EGFBF85NXPH3KP + in: formData + name: account_id + required: true + type: string + x-go-name: AccountID + - description: |- + 要附加到举报中以提供额外上下文的贴文的 ID。 + 示例: ["01GPE76N4SBVRZ8K24TW51ZZQ4","01GPE76WN9JZE62EPT3Q9FRRD4"] + in: formData + items: + type: string + name: status_ids + type: array + x-go-name: StatusIDs + - description: |- + 举报的原因。默认最大 1000 个字符。 + 示例: 未经允许公开他人隐私信息 + in: formData + name: comment + type: string + x-go-name: Comment + - default: false + description: |- + 如果账户为外站账户,是否应将举报转发给外站管理员? + 示例: true + in: formData + name: forward + type: boolean + x-go-name: Forward + - default: other + description: |- + 指定举报属于骚扰、违反特定实例规则还是其他原因。 + 目前仅支持 'other'。 + 示例: other + in: formData + name: category + type: string + x-go-name: Category + - description: |- + 举报人提供的被违反的实例规则的 ID。 + 示例: ["01GPBN5YDY6JKBWE44H7YQBDCQ","01GPBN65PDWSBPWVDD0SQCFFY3"] + in: formData + items: + type: string + name: rule_ids + type: array + x-go-name: RuleIDs + produces: + - application/json + responses: + "200": + description: 创建的举报。 + schema: + $ref: '#/definitions/report' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:reports + summary: 创建举报 + description: 根据给定参数创建一个新的用户举报。 + tags: + - reports + /api/v1/reports/{id}: + get: + operationId: reportGet + parameters: + - description: 举报的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的举报。 + schema: + $ref: '#/definitions/report' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:reports + summary: 获取单条举报 + description: 获取具有给定 ID 的举报。 + tags: + - reports + /api/v1/statuses: + post: + consumes: + - application/json + - application/x-www-form-urlencoded + description: |- + 按给定的表单字段参数创建一个新的贴文。 + 若 Content-Type 为 'application/json',则参数也可以在请求体中以 JSON 形式给出。 + + `interaction_policy` 字段可用于为此贴文设置交互策略。 + + 如果使用表单数据提交,请使用以下模式设置交互策略: + + `interaction_policy[INTERACTION_TYPE][CONDITION][INDEX]=Value` + + 例如:`interaction_policy[can_reply][always][0]=author` + + 使用 `curl` 可以按如下所示: + + `curl -F 'interaction_policy[can_reply][always][0]=author' -F 'interaction_policy[can_reply][always][1]=followers' [... other form fields ...]` + + JSON 等效形式为: + + `curl -H 'Content-Type: application/json' -d '{"interaction_policy":{"can_reply":{"always":["author","followers"]}} [... other json fields ...]}'` + + 服务器将对提交的策略执行一些规范化处理,以确保您无法提交完全无效的内容。 + operationId: statusCreate + parameters: + - description: |- + 贴文的文字内容。 + 如果提供了 media_ids,此项变为可选。 + 当提供贴文时,附加投票将变为可选。 + in: formData + name: status + type: string + x-go-name: Status + - description: |- + 要附加的媒体附件 ID 数组。 + 如果提供了 media_ids,status 将变为可选,poll 不能使用。 + + 如果贴文以表单形式提交,应使用的键为 'media_ids[]',但如果是 JSON 或 XML,则键为 'media_ids'。 + in: formData + items: + type: string + name: media_ids + type: array + x-go-name: MediaIDs + - description: |- + 投票选项的数组。 + 如果提供了投票选项,media_ids 将不能使用,poll[expires_in] 必须提供。 + in: formData + items: + type: string + name: poll[options][] + type: array + x-go-name: PollOptions + - description: |- + 投票的开放时长,单位为秒。 + 如果提供了此项,media_ids 不能使用,poll[options] 必须提供。 + format: int64 + in: formData + name: poll[expires_in] + type: integer + x-go-name: PollExpiresIn + - default: false + description: 在此投票上允许多选。 + in: formData + name: poll[multiple] + type: boolean + x-go-name: PollMultiple + - default: true + description: 在投票结束前隐藏投票计数。 + in: formData + name: poll[hide_totals] + type: boolean + x-go-name: PollHideTotals + - description: 被回复的贴文 ID(如果要发送的贴文是回复)。 + in: formData + name: in_reply_to_id + type: string + x-go-name: InReplyToID + - description: 贴文和附加媒体应标记为敏感。 + in: formData + name: sensitive + type: boolean + x-go-name: Sensitive + - description: |- + 在贴文内容之前显示的警告或主题文本。 + 贴文通常被折叠在此字段身后。 + in: formData + name: spoiler_text + type: string + x-go-name: SpoilerText + - description: 贴文的可见性。 + enum: + - public + - unlisted + - private + - mutuals_only + - direct + in: formData + name: visibility + type: string + x-go-name: Visibility + - default: false + description: 如果设置为 true,此状态将仅在本站可见,不会传播到本站以外的时间线。如果设置为 false(默认),此状态将传播到您的所有关注者,以及本站时间线外的时间线。 + in: formData + name: local_only + type: boolean + x-go-name: LocalOnly + - description: '***已弃用***。仅用于后向兼容。仅在 local_only 尚未设置且此字段被明确设置时使用。如果设置为 true,此贴文将在本站时间线之外传播。如果设置为 false,此贴文将不会在本站时间线之外传播。' + in: formData + name: federated + type: boolean + x-go-name: Federated + - description: |- + 此贴文的计划发布时间,格式为 ISO 8601 Datetime。 + 提供此参数将导致返回 ScheduledStatus 而不是 Status。 + 必须至少在未来 5 分钟之后。 + + 此功能尚未实现;尝试设置它将返回 501 Not Implemented。 + in: formData + name: scheduled_at + type: string + x-go-name: ScheduledAt + - description: 此贴文的 ISO 639 语言代码。 + in: formData + name: language + type: string + x-go-name: Language + - description: 解析此贴文时使用的内容类型。 + enum: + - text/plain + - text/markdown + in: formData + name: content_type + type: string + x-go-name: ContentType + - description: interaction_policy.can_favourite.always 的第 N 个条目。 + in: formData + name: interaction_policy[can_favourite][always][0] + type: string + - description: interaction_policy.can_favourite.with_approval 的第 N 个条目。 + in: formData + name: interaction_policy[can_favourite][with_approval][0] + type: string + - description: interaction_policy.can_reply.always 的第 N 个条目。 + in: formData + name: interaction_policy[can_reply][always][0] + type: string + - description: interaction_policy.can_reply.with_approval 的第 N 个条目。 + in: formData + name: interaction_policy[can_reply][with_approval][0] + type: string + - description: interaction_policy.can_reblog.always 的第 N 个条目。 + in: formData + name: interaction_policy[can_reblog][always][0] + type: string + - description: interaction_policy.can_reblog.with_approval 的第 N 个条目。 + in: formData + name: interaction_policy[can_reblog][with_approval][0] + type: string + produces: + - application/json + responses: + "200": + description: 新创建的贴文。 + schema: + $ref: '#/definitions/status' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + "501": + description: scheduled_at 被设置,但此功能尚未实现 + security: + - OAuth2 Bearer: + - write:statuses + summary: 创建新贴文 + tags: + - statuses + /api/v1/statuses/{id}: + delete: + description: |- + 删除给定 ID 的贴文。贴文必须属于您。 + 删除的贴文将在响应中返回。`text` 字段将包含贴文的原始文本,就像它被提交时一样。这在执行“删除并重新撰写”类型操作时很有用。 + operationId: statusDelete + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 刚刚被删除的贴文。 + schema: + $ref: '#/definitions/status' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:statuses + summary: 删除贴文 + tags: + - statuses + get: + operationId: statusGet + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的贴文。 + schema: + $ref: '#/definitions/status' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:statuses + summary: 查看贴文 + description: 查看具有给定 ID 的贴文。 + tags: + - statuses + /api/v1/statuses/{id}/bookmark: + post: + operationId: statusBookmark + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 对应的贴文。 + schema: + $ref: '#/definitions/status' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:statuses + summary: 收藏贴文 + description: 收藏具有给定 ID 的贴文。 + tags: + - statuses + /api/v1/statuses/{id}/context: + get: + description: 返回给定贴文的祖先和后代。返回的贴文将按贴文串结构排序,因此它们适合按返回顺序显示。 + operationId: threadContext + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 贴文串上下文对象。 + schema: + $ref: '#/definitions/threadContext' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:statuses + summary: 查看贴文串上下文 + tags: + - statuses + /api/v1/statuses/{id}/favourite: + post: + operationId: statusFave + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 被点赞的贴文。 + schema: + $ref: '#/definitions/status' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:statuses + summary: 点赞贴文 + description: 点赞给定 ID 的贴文(如果允许)。 + tags: + - statuses + /api/v1/statuses/{id}/favourited_by: + get: + operationId: statusFavedBy + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: "" + schema: + items: + $ref: '#/definitions/account' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:accounts + summary: 查看贴文点赞列表 + description: 查看点赞给定 ID 的贴文的账户。 + tags: + - statuses + /api/v1/statuses/{id}/history: + get: + description: '查看给定 ID 的贴文的编辑历史。 **未实现**:当前此端点将始终返回一个长度为 1 的数组,仅包含贴文的最新/当前版本。' + operationId: statusHistoryGet + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: "" + schema: + items: + $ref: '#/definitions/statusEdit' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:statuses + summary: 查看贴文编辑历史 + tags: + - statuses + /api/v1/statuses/{id}/mute: + post: + description: |- + 静音某条贴文的贴文串,防止未来串中的回复、点赞、转发等行为产生通知。 + + 目标贴文必须属于您或提及了您。 + + 贴文串静音和取消静音是幂等的。如果您已经静音了一个串,再次静音它只是意味着它保持静音,您将得到 200 OK 返回。 + operationId: statusMute + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 被静音的贴文。 + schema: + $ref: '#/definitions/status' + "400": + description: bad request 无效请求; 你不是目标贴文串的参与者 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:mutes + summary: 静音贴文串 + tags: + - statuses + /api/v1/statuses/{id}/pin: + post: + description: |- + 将贴文在个人资料页置顶,并将其添加到你的特色 ActivityPub 集合中。 + + 你只能将原创贴文(非转发)固定到自己的个人资料上。 + + 支持的固定贴文的隐私级别有 public、unlisted 和 private/followers-only, + 但只有公开贴文会出现在您个人资料的网页版本上。 + operationId: statusPin + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 被固定的贴文。 + schema: + $ref: '#/definitions/status' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:accounts + summary: 置顶贴文 + tags: + - statuses + /api/v1/statuses/{id}/reblog: + post: + description: |- + 转发给定 ID 的贴文。 + 如果目标贴文是可转发的,它将被分享给您的粉丝。 + 这相当于一个 ActivityPub 'Announce' 活动。 + operationId: statusReblog + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 被转发的贴文。 + schema: + $ref: '#/definitions/status' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:statuses + summary: 转发贴文 + tags: + - statuses + /api/v1/statuses/{id}/reblogged_by: + get: + operationId: statusBoostedBy + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: "" + schema: + items: + $ref: '#/definitions/account' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + security: + - OAuth2 Bearer: + - read:accounts + summary: 查看贴文转发列表 + description: 查看转发给定 ID 的贴文的账户。 + tags: + - statuses + /api/v1/statuses/{id}/source: + get: + operationId: statusSourceGet + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: "" + schema: + items: + $ref: '#/definitions/statusSource' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:statuses + summary: 查看贴文源文本 + description: 查看给定 ID 的贴文的源文本。请求者必须拥有该贴文。 + tags: + - statuses + /api/v1/statuses/{id}/unbookmark: + post: + operationId: statusUnbookmark + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 对应的贴文。 + schema: + $ref: '#/definitions/status' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:statuses + summary: 取消收藏贴文 + description: 取消收藏给定 ID 的贴文。 + tags: + - statuses + /api/v1/statuses/{id}/unfavourite: + post: + operationId: statusUnfave + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 被取消点赞的贴文。 + schema: + $ref: '#/definitions/status' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:statuses + summary: 取消点赞贴文 + description: 取消点赞给定 ID 的贴文。 + tags: + - statuses + /api/v1/statuses/{id}/unmute: + post: + description: |- + 取消静音某条贴文的贴文串。这将重新启用串中未来回复、点赞、转发等行为的通知。 + + 目标贴文必须属于您或提及了您。 + + 贴文串静音和取消静音是幂等的。如果您已经取消静音了一个串,再次取消静音它只是意味着它保持取消静音,您将得到 200 OK 的返回状态。 + operationId: statusUnmute + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 被取消静音的贴文。 + schema: + $ref: '#/definitions/status' + "400": + description: bad request 无效请求; 你不是目标贴文串的参与者 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:mutes + summary: 取消静音贴文串 + tags: + - statuses + /api/v1/statuses/{id}/unpin: + post: + operationId: statusUnpin + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 贴文。 + schema: + $ref: '#/definitions/status' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:accounts + summary: 取消置顶贴文 + description: 取消置顶某条已经置顶的贴文。 + tags: + - statuses + /api/v1/statuses/{id}/unreblog: + post: + operationId: statusUnreblog + parameters: + - description: 目标贴文 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 被取消转发的贴文。 + schema: + $ref: '#/definitions/status' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:statuses + summary: 取消转发贴文 + description: 取消转发给定 ID 的贴文。 + tags: + - statuses + /api/v1/streaming: + get: + description: |- + 发起一个用于实时流式传输贴文和通知的 websocket 连接。 + 使用的协议应始终为 `wss`。流式传输根路径可以在 `/api/v1/instance` 查看。 + + 在成功连接时,将返回代码 `101`,表示正在将连接升级为安全的 websocket 连接。 + + 只要连接保持打开状态,就会将各种消息类型流式传输到其中。 + + GoToSocial 将每 30 秒对连接进行 ping,以检查客户端是否仍在接收。 + + 如果 ping 失败,或者在传输过程中发生其他问题,则连接将被断开,并且客户端将被要求重新启动。 + operationId: streamGet + parameters: + - description: 发起请求的账户的访问令牌。 + in: query + name: access_token + required: true + type: string + - description: |- + 请求的流类型。 + + 可用选项有: + + `user`: 接收账户的主页时间线更新。 + `public`: 接收公共时间线更新。 + `public:local`: 接收本站时间线更新。 + `hashtag`: 接收给定话题标签的更新。 + `hashtag:local`: 接收给定话题标签的本站更新。 + `list`: 接收一个列表中的账户的更新。 + `direct`: 接收私信的更新。 + in: query + name: stream + required: true + type: string + - description: |- + 要订阅的列表的 ID。 + 仅在流类型为 'list' 时使用。 + in: query + name: list + type: string + - description: |- + 要订阅的话题标签的名称。 + 仅在流类型为 'hashtag' 或 'hashtag:local' 时使用。 + in: query + name: tag + type: string + produces: + - application/json + responses: + "101": + description: "" + schema: + properties: + event: + description: |- + 要接收的事件类型。 + + `update`:收到新贴文。 + `notification`:收到新通知。 + `delete`:贴文已被删除。 + `filters_changed`:过滤规则(包括关键字和贴文)已更改。 + enum: + - update + - notification + - delete + - filters_changed + type: string + payload: + description: |- + 流式传输消息的有效负载。 + 根据 `event` 类型不同而不同。 + + 如果存在,应解析为字符串。 + + 如果 `event` = `update`,则负载将是一个贴文的 JSON 字符串。 + 如果 `event` = `notification`,则负载将是一个通知的 JSON 字符串。 + 如果 `event` = `delete`,则负载将是一个贴文 ID。 + 如果 `event` = `filters_changed`,则没有负载。 + example: '{"id":"01FC3TZ5CFG6H65GCKCJRKA669","created_at":"2021-08-02T16:25:52Z","sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"https://gts.superseriousbusiness.org/users/dumpsterqueer/statuses/01FC3TZ5CFG6H65GCKCJRKA669","url":"https://gts.superseriousbusiness.org/@dumpsterqueer/statuses/01FC3TZ5CFG6H65GCKCJRKA669","replies_count":0,"reblogs_count":0,"favourites_count":0,"favourited":false,"reblogged":false,"muted":false,"bookmarked":fals…//gts.superseriousbusiness.org/fileserver/01JNN207W98SGG3CBJ76R5MVDN/header/original/019036W043D8FXPJKSKCX7G965.png","header_static":"https://gts.superseriousbusiness.org/fileserver/01JNN207W98SGG3CBJ76R5MVDN/header/small/019036W043D8FXPJKSKCX7G965.png","followers_count":33,"following_count":28,"statuses_count":126,"last_status_at":"2021-08-02T16:25:52Z","emojis":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null,"text":"a"}' + type: string + stream: + items: + enum: + - user + - public + - public:local + - hashtag + - hashtag:local + - list + - direct + type: string + type: array + type: object + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + schemes: + - wss + security: + - OAuth2 Bearer: + - read:streaming + summary: 流式传输 + tags: + - streaming + /api/v1/tags/{tag_name}: + get: + description: 获取话题标签详情,包括您当前是否关注它。如果话题不存在,此方法不会在数据库中创建它。 + operationId: getTag + parameters: + - description: 话题标签的名称(不包含 `#`)。 + in: path + name: tag_name + required: true + type: string + produces: + - application/json + responses: + "200": + description: 话题标签详情。 + schema: + $ref: '#/definitions/tag' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:follows + summary: 获取话题标签 + tags: + - tags + /api/v1/tags/{tag_name}/follow: + post: + description: '此端点是幂等的:如果您已经关注了话题标签,此调用仍将成功。' + operationId: followTag + parameters: + - description: 话题标签的名称(不包含 `#`)。 + in: path + name: tag_name + required: true + type: string + produces: + - application/json + responses: + "200": + description: 话题标签详情。 + schema: + $ref: '#/definitions/tag' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:follows + summary: 关注话题标签 + tags: + - tags + /api/v1/tags/{tag_name}/unfollow: + post: + description: '此端点是幂等的:如果您没有关注话题标签,此调用仍将成功。' + operationId: unfollowTag + parameters: + - description: 话题标签的名称(不包含 `#`)。 + in: path + name: tag_name + required: true + type: string + produces: + - application/json + responses: + "200": + description: 话题标签详情。 + schema: + $ref: '#/definitions/tag' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: unauthorized 未授权 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:follows + summary: 取消关注话题标签 + tags: + - tags + /api/v1/timelines/home: + get: + description: |- + 查看你关注的账户发布的贴文。 + 贴文将按时间顺序(最新的在前)返回,带有连续的 ID(较大 = 较新)。 + + 返回的 Link 标头可用于在时间线向上或向下滚动时生成上一个和下一个查询。 + + 示例: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: homeTimeline + parameters: + - description: 只返回早于给定的最大贴文 ID 的贴文。具有指定 ID 的贴文不会包含在响应中。 + in: query + name: max_id + type: string + - description: 只返回新于给定的最小贴文 ID 的贴文。具有指定 ID 的贴文不会包含在响应中。 + in: query + name: since_id + type: string + - description: 只返回比给定的最小贴文 ID **相邻且更新** 的贴文。具有指定 ID 的贴文不会包含在响应中。 + in: query + name: min_id + type: string + - default: 20 + description: 要返回的贴文数量。 + in: query + name: limit + type: integer + - default: false + description: 仅显示本站账户发布的贴文。 + in: query + name: local + type: boolean + produces: + - application/json + responses: + "200": + description: 贴文数组。 + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/status' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + security: + - OAuth2 Bearer: + - read:statuses + summary: 查看主页时间线 + tags: + - timelines + /api/v1/timelines/list/{id}: + get: + description: |- + 查看给定列表的时间线贴文。 + + 贴文将按时间顺序(最新的在前)返回,带有连续的 ID(较大 = 较新)。 + + 返回的 Link 标头可用于在时间线向上或向下滚动时生成上一个和下一个查询。 + + 示例: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: listTimeline + parameters: + - description: 列表的 ID + in: path + name: id + required: true + type: string + - description: 只返回早于给定的最大贴文 ID 的贴文。具有指定 ID 的贴文不会包含在响应中。 + in: query + name: max_id + type: string + - description: 只返回新于给定的最小贴文 ID 的贴文。具有指定 ID 的贴文不会包含在响应中。 + in: query + name: since_id + type: string + - description: 只返回比给定的最小贴文 ID **相邻且更新** 的贴文。具有指定 ID 的贴文不会包含在响应中。 + in: query + name: min_id + type: string + - default: 20 + description: 要返回的贴文数量。 + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: 贴文数组。 + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/status' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + security: + - OAuth2 Bearer: + - read:lists + summary: 查看列表时间线 + tags: + - timelines + /api/v1/timelines/public: + get: + description: |- + 查看你所在的实例已知的公开贴文。 + + 贴文将按时间顺序(最新的在前)返回,带有连续的 ID(较大 = 较新)。 + + 返回的 Link 标头可用于在时间线向上或向下滚动时生成上一个和下一个查询。 + + 示例: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: publicTimeline + parameters: + - description: 只返回早于给定的最大贴文 ID 的贴文。具有指定 ID 的贴文不会包含在响应中。 + in: query + name: max_id + type: string + - description: 只返回新于给定的最小贴文 ID 的贴文。具有指定 ID 的贴文不会包含在响应中。 + in: query + name: since_id + type: string + - description: 只返回比给定的最小贴文 ID **相邻且更新** 的贴文。具有指定 ID 的贴文不会包含在响应中。 + in: query + name: min_id + type: string + - default: 20 + description: 要返回的贴文数量。 + in: query + name: limit + type: integer + - default: false + description: 仅显示本站账户发布的贴文。 + in: query + name: local + type: boolean + produces: + - application/json + responses: + "200": + description: 贴文数组。 + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/status' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + security: + - OAuth2 Bearer: + - read:statuses + summary: 查看跨站时间线 + tags: + - timelines + /api/v1/timelines/tag/{tag_name}: + get: + description: |- + 查看给定话题标签的公开贴文 (不区分大小写) + + 贴文将按时间顺序(最新的在前)返回,带有连续的 ID(较大 = 较新)。 + + 返回的 Link 标头可用于在时间线向上或向下滚动时生成上一个和下一个查询。 + + 示例: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: tagTimeline + parameters: + - description: Name of the tag + in: path + name: tag_name + required: true + type: string + - description: 只返回早于给定的最大贴文 ID 的贴文。具有指定 ID 的贴文不会包含在响应中。 + in: query + name: max_id + type: string + - description: 只返回新于给定的最小贴文 ID 的贴文。具有指定 ID 的贴文不会包含在响应中。 + in: query + name: since_id + type: string + - description: 只返回比给定的最小贴文 ID **相邻且更新** 的贴文。具有指定 ID 的贴文不会包含在响应中。 + in: query + name: min_id + type: string + - default: 20 + description: 要返回的贴文数量。 + in: query + maximum: 40 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: 贴文数组。 + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/status' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + security: + - OAuth2 Bearer: + - read:statuses + summary: 查看话题标签时间线 + tags: + - timelines + /api/v1/user: + get: + operationId: getUser + produces: + - application/json + responses: + "200": + description: 请求的用户。 + schema: + $ref: '#/definitions/user' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "406": + description: not acceptable 不可接受 + "500": + description: internal error + security: + - OAuth2 Bearer: + - read:user + summary: 获取当前用户 + description: 获取自己的用户模型。 + tags: + - user + /api/v1/user/email_change: + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + operationId: userEmailChange + parameters: + - description: 用户当前密码,用于验证。 + in: formData + name: password + required: true + type: string + x-go-name: Password + - description: 新邮箱地址。 + in: formData + name: new_email + required: true + type: string + x-go-name: NewEmail + produces: + - application/json + responses: + "202": + description: '已接受:正在处理邮箱更改;请检查您的收件箱以确认新地址。' + schema: + $ref: '#/definitions/user' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "406": + description: not acceptable 不可接受 + "409": + description: '冲突: 提供的邮箱地址已被使用' + "500": + description: internal error + security: + - OAuth2 Bearer: + - write:user + summary: 更改邮箱地址 + description: 请求更改当前用户的邮箱地址。 + tags: + - user + /api/v1/user/password_change: + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + description: |- + 修改当前用户的密码。 + 若 Content-Type 为 'application/json',则参数也可以在请求体中以 JSON 形式给出。 + 若 Content-Type 为 'application/xml',则参数也可以在请求体中以 XML 形式给出。 + operationId: userPasswordChange + parameters: + - description: 用户的旧密码。 + in: formData + name: old_password + required: true + type: string + x-go-name: OldPassword + - description: |- + 用户提供的新密码。 + 如果密码的熵不够高,将被拒绝。 + 参见 https://github.com/wagslane/go-password-validator + in: formData + name: new_password + required: true + type: string + x-go-name: NewPassword + produces: + - application/json + responses: + "200": + description: 密码更改成功 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "406": + description: not acceptable 不可接受 + "422": + description: 无法处理请求,因为实例正在使用 OIDC 后端 + "500": + description: internal error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:user + summary: 更改密码 + tags: + - user + /api/v2/admin/accounts: + get: + description: |- + 按给定过滤规则查看并按页浏览已知账户。 + + 返回的账户将按域名 + 用户名的字母顺序(a-z)排序。 + + 下一个和上一个查询可以从返回的Link标头中解析。 + 示例: + + ``` + ; rel="next", ; rel="prev" + ```` + operationId: adminAccountsGetV2 + parameters: + - description: 针对 `local` 或 `remote` 类型的账户进行过滤。 + in: query + name: origin + type: string + - description: 针对 `active`, `pending`, `disabled`, `silenced`, 或 `suspended` 类型的账户进行过滤。 + in: query + name: status + type: string + - description: 针对具有站务权限(可以管理举报)的账户进行过滤。 + in: query + name: permissions + type: string + - description: 针对具有对应身份组的账户进行过滤。 + in: query + items: + type: string + name: role_ids[] + type: array + - description: 查找被对应 ID 的账户邀请的用户。 + in: query + name: invited_by + type: string + - description: 搜索给定用户名。 + in: query + name: username + type: string + - description: 搜索给定昵称。 + in: query + name: display_name + type: string + - description: 针对给定实例域名进行过滤。 + in: query + name: by_domain + type: string + - description: 查找具有给定电子邮箱的用户。 + in: query + name: email + type: string + - description: 查找具有此 IP 地址的用户。 + in: query + name: ip + type: string + - description: "`[domain]/@[username]` 形式的 max_id。返回的所有结果都将在 `[domain]/@[username]` 之后的字母顺序中。例如,如果 max_id = `example.org/@someone`,则返回的条目可能包含 `example.org/@someone_else`,`later.example.org/@someone` 等。本站账户 ID 使用空字符串作为 `[domain]` 部分,例如具有用户名 `someone` 的本站账户将是 `/@someone`。" + in: query + name: max_id + type: string + - description: "`[domain]/@[username]` 形式的 min_id。返回的所有结果都将在 `[domain]/@[username]` 之前的字母顺序中。例如,如果 min_id = `example.org/@someone`,则返回的条目可能包含 `example.org/@earlier_account`,`earlier.example.org/@someone` 等。本站账户 ID 使用空字符串作为 `[domain]` 部分,例如具有用户名 `someone` 的本站账户将是 `/@someone`。" + in: query + name: min_id + type: string + - default: 50 + description: 返回的最大结果数量。 + in: query + maximum: 200 + minimum: 1 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: "" + headers: + Link: + description: 下一/上一查询的链接。 + type: string + schema: + items: + $ref: '#/definitions/adminAccountInfo' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 查看账户列表 + tags: + - admin + /api/v2/filters: + get: + operationId: filtersV2Get + produces: + - application/json + responses: + "200": + description: 请求的过滤规则。 + schema: + items: + $ref: '#/definitions/filterV2' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:filters + summary: 获取V2过滤规则 + description: 获取当前账户的所有V2过滤规则。 + tags: + - filters + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + operationId: filterV2Post + parameters: + - description: |- + 过滤规则的名称。 + + 示例: 挂人 + in: formData + maxLength: 200 + minLength: 1 + name: title + required: true + type: string + - collectionFormat: multi + description: |- + 此过滤规则要应用的上下文。 + + 示例: home, public + enum: + - home + - notifications + - public + - thread + - account + in: formData + items: + type: string + minItems: 1 + name: context[] + required: true + type: array + uniqueItems: true + - description: |- + 此过滤规则的持续时间(以秒为单位)。若省略,则过滤规则永不过期。 + + 示例:86400 + in: formData + name: expires_in + type: number + - default: warn + description: |- + 贴文命中过滤规则时的操作。 + + 示例: warn + enum: + - warn + - hide + in: formData + name: filter_action + type: string + - collectionFormat: multi + description: 要添加 (如果不使用 id 参数) 或更新 (如果使用 id 参数) 的关键词。 + in: formData + items: + type: string + name: keywords_attributes[][keyword] + type: array + - collectionFormat: multi + description: 匹配关键词时是否考虑单词边界 (整词匹配)? + in: formData + items: + type: boolean + name: keywords_attributes[][whole_word] + type: array + - collectionFormat: multi + description: 要添加到过滤规则的贴文(status)。 + in: formData + items: + type: string + name: statuses_attributes[][status_id] + type: array + produces: + - application/json + responses: + "200": + description: 新过滤规则。 + schema: + $ref: '#/definitions/filterV2' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: 禁止对已迁移的账户执行此操作 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突(重复的标题、关键词或贴文) + "422": + description: 无法处理的内容 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:filters + summary: 创建V2过滤规则 + tags: + - filters + /api/v2/filters/{id}: + delete: + operationId: filterV2Delete + parameters: + - description: 过滤规则的 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 过滤规则已删除 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:filters + summary: 删除V2过滤规则 + description: 删除给定 ID 的单个V2过滤规则。 + tags: + - filters + get: + operationId: filterV2Get + parameters: + - description: 过滤规则的 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的过滤规则。 + schema: + $ref: '#/definitions/filterV2' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:filters + summary: 获取单个V2过滤规则 + description: 获取给定 ID 的单个V2过滤规则。 + tags: + - filters + put: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + description: |- + 更新具有给定 ID 的单个V2过滤规则。 + 注意,这实际上更接近于 PATCH 操作:只有提供的字段将被更新,而省略的字段将保持为以前的值。 + operationId: filterV2Put + parameters: + - description: 过滤规则的 ID. + in: path + name: id + required: true + type: string + - description: |- + 过滤规则的标题。 + + 示例: 挂人 + in: formData + maxLength: 200 + minLength: 1 + name: title + required: true + type: string + - collectionFormat: multi + description: 要添加到此过滤规则的关键词。 + in: formData + items: + type: string + name: keywords_attributes[][keyword] + type: array + - collectionFormat: multi + description: 匹配关键词时是否考虑单词边界 (整词匹配)? + in: formData + items: + type: boolean + name: keywords_attributes[][whole_word] + type: array + - collectionFormat: multi + description: 要添加到过滤规则的贴文。 + in: formData + items: + type: string + name: statuses_attributes[][status_id] + type: array + - collectionFormat: multi + description: |- + 此过滤规则要应用的上下文。 + + 示例: home, public + enum: + - home + - notifications + - public + - thread + - account + in: formData + items: + type: string + minItems: 1 + name: context[] + required: true + type: array + uniqueItems: true + - description: |- + 此过滤规则的持续时间(以秒为单位)。若省略,则过滤规则永不过期。 + + 示例: 86400 + in: formData + name: expires_in + type: number + - description: |- + 贴文命中过滤规则时的操作。 + + 示例: warn + enum: + - warn + - hide + in: formData + name: filter_action + type: string + produces: + - application/json + responses: + "200": + description: 更新后的过滤规则。 + schema: + $ref: '#/definitions/filterV2' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: 禁止对已迁移的账户执行此操作 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突(重复的标题、关键词或贴文) + "422": + description: unprocessable 无法处理的内容 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:filters + summary: 更新V2过滤规则 + tags: + - filters + /api/v2/filters/{id}/keywords: + get: + operationId: filterKeywordsGet + parameters: + - description: 过滤规则的 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的过滤规则关键词 + schema: + items: + $ref: '#/definitions/filterKeyword' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:filters + summary: 获取过滤规则关键词 + description: 获取给定 ID 的过滤规则的所有关键词。 + tags: + - filters + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + operationId: filterKeywordPost + parameters: + - description: 要将贴文添加到的过滤规则的 ID。 + in: path + name: id + required: true + type: string + - description: |- + 要被过滤的文本 + + 示例:挂人 + in: formData + maxLength: 40 + minLength: 1 + name: keyword + required: true + type: string + - default: false + description: |- + 此过滤规则是否应考虑单词边界(整词匹配)? + + 示例:true + in: formData + name: whole_word + type: boolean + produces: + - application/json + responses: + "200": + description: 新的过滤规则关键词。 + schema: + $ref: '#/definitions/filterKeyword' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: 禁止对已迁移的账户执行此操作 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突(重复的关键词) + "422": + description: unprocessable 无法处理的内容 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:filters + summary: 添加关键词 + description: 向给定 ID 的过滤规则添加关键词。 + tags: + - filters + /api/v2/filters/{id}/statuses: + get: + operationId: filterStatusesGet + parameters: + - description: 过滤规则的 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的过滤规则贴文。 + schema: + items: + $ref: '#/definitions/filterStatus' + type: array + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:filters + summary: 获取过滤规则贴文 + description: 获取给定过滤规则下的所有贴文。 + tags: + - filters + post: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + operationId: filterStatusPost + parameters: + - description: 要将贴文添加到的过滤规则的 ID。 + in: path + name: id + required: true + type: string + - description: |- + 要过滤的贴文的 ID。 + + 示例:01HXA2NE0K8T1C70K90E74GYD0 + in: formData + name: status_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 新的过滤规则贴文。 + schema: + $ref: '#/definitions/filterStatus' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: 禁止对已迁移的账户执行此操作 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突(重复的贴文) + "422": + description: unprocessable 无法处理的内容 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:filters + summary: 添加贴文 + description: 向已有过滤规则添加一条贴文。 + tags: + - filters + /api/v2/filters/keywords/{id}: + delete: + operationId: filterKeywordDelete + parameters: + - description: 过滤规则关键词的 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 过滤规则关键词已删除 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:filters + summary: 删除过滤规则关键词 + description: 通过指定 ID 删除某个过滤规则关键词。 + tags: + - filters + get: + operationId: filterKeywordGet + parameters: + - description: 过滤规则关键词的 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的过滤规则关键词。 + schema: + $ref: '#/definitions/filterKeyword' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:filters + summary: 获取过滤规则关键词 + description: 获取具有给定 ID 的单个过滤规则关键词。 + tags: + - filters + /api/v2/filters/keywords{id}: + put: + consumes: + - application/json + - application/xml + - application/x-www-form-urlencoded + operationId: filterKeywordPut + parameters: + - description: 要更新的过滤规则关键词的 ID。 + in: path + name: id + required: true + type: string + - description: |- + 要过滤的文本 + + 示例:挂人 + in: formData + maxLength: 40 + minLength: 1 + name: keyword + required: true + type: string + - description: |- + 过滤时是否考虑单词边界(整词匹配)? + + 示例:true + in: formData + name: whole_word + type: boolean + produces: + - application/json + responses: + "200": + description: 更新后的过滤关键词。 + schema: + $ref: '#/definitions/filterKeyword' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: 禁止对已迁移的账户执行此操作 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突(重复的关键词) + "422": + description: unprocessable 无法处理 content + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:filters + summary: 更新过滤规则关键词 + description: 更新具有给定 ID 的单个过滤规则关键词。 + tags: + - filters + /api/v2/filters/statuses/{id}: + delete: + operationId: filterStatusDelete + parameters: + - description: 过滤规则贴文的 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 过滤规则贴文已删除 + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - write:filters + summary: 删除过滤规则贴文 + description: 删除具有给定 ID 的单个过滤规则贴文。 + tags: + - filters + get: + operationId: filterStatusGet + parameters: + - description: 过滤规则贴文的 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 请求的过滤规则贴文。 + schema: + $ref: '#/definitions/filterStatus' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - read:filters + summary: 获取过滤规则贴文 + description: 获取具有给定 ID 的单个过滤规则贴文。 + tags: + - filters + /api/v2/instance: + get: + operationId: instanceGetV2 + produces: + - application/json + responses: + "200": + description: 实例信息。 + schema: + $ref: '#/definitions/instanceV2' + "406": + description: not acceptable 不可接受 + "500": + description: internal error + summary: 查看实例信息 + tags: + - instance + /livez: + get: + operationId: liveGet + responses: + "200": + description: OK + summary: 服务在线检测 + description: 若 GoToSocial 服务“在线” (即能够响应HTTP请求),则返回无响应体的 200 状态码。 + tags: + - health + head: + operationId: liveHead + responses: + "200": + description: OK + summary: 服务在线检测 + description: 若 GoToSocial 服务“在线” (即能够响应HTTP请求),则返回 200 状态码。 + tags: + - health + /nodeinfo/2.0: + get: + description: '对 nodeinfo 查询返回符合规范的 nodeinfo 响应。参见: https://nodeinfo.diaspora.software/schema.html' + operationId: nodeInfoGet + produces: + - application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#" + responses: + "200": + description: "" + schema: + $ref: '#/definitions/nodeinfo' + summary: NodeInfo 2.0 + tags: + - nodeinfo + /readyz: + get: + description: 若 GoToSocial 服务“就绪” (即能够连接到数据库后端并执行简单的 SELECT),则返回无响应体的 200 状态码。若 GtS 尚未准备就绪,则在日志中记录错误(但不返回给调用方,以避免泄露内部信息)。 + operationId: readyGet + responses: + "200": + description: OK + "500": + description: 尚未就绪。查看日志以获取错误信息。 + summary: 服务就绪检测 + tags: + - health + head: + description: 若 GoToSocial 服务“就绪” (即能够连接到数据库后端并执行简单的 SELECT),则返回无响应体的 200 状态码。若 GtS 尚未准备就绪,则在日志中记录错误(但不返回给调用方,以避免泄露内部信息)。 + operationId: readyHead + responses: + "200": + description: OK + summary: 服务就绪检测 + tags: + - health + /users/{username}/collections/featured: + get: + description: |- + 获取某个用户的精选集合(置顶贴文)。 + 响应将包含 `items` 属性中的 Note URI 的有序集合。 + + 由调用方决定是否解析提供的 Note URIs(如果它们已经缓存,则不解析)。 + + 请求需要 HTTP 签名。 + operationId: s2sFeaturedCollectionGet + parameters: + - description: 用户的账户名 + in: path + name: username + required: true + type: string + produces: + - application/activity+json + responses: + "200": + description: "" + schema: + $ref: '#/definitions/swaggerFeaturedCollection' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + summary: 获取用户精选集合 + tags: + - s2s/federation + /users/{username}/outbox: + get: + description: |- + 获取用户的公开发件箱集合。 + + 注意,如果 `page` 为 `false`,则响应将是一个带有 `first` 页的 Collection,如下所示。 + + 如果 `page` 为 `true`,则响应将是一个不带 `Collection` 包装的单个 `CollectionPage`。 + + 请求需要 HTTP 签名。 + operationId: s2sOutboxGet + parameters: + - description: 账户的用户名。 + in: path + name: username + required: true + type: string + - default: false + description: 按 CollectionPage 返回响应。 + in: query + name: page + type: boolean + - description: 下一贴文的最小 ID,用于分页。 + in: query + name: min_id + type: string + - description: 下一贴文的最大 ID,用于分页。 + in: query + name: max_id + type: string + produces: + - application/activity+json + responses: + "200": + description: "" + schema: + $ref: '#/definitions/swaggerCollection' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + summary: 获取用户发件箱 + tags: + - s2s/federation + /users/{username}/statuses/{status}/replies: + get: + description: |- + 获取贴文的回复集合。 + + 注意,如果 `page` 为 `false`,则响应将是一个带有 `first` 页的 Collection,如下所示。 + + 如果 `page` 为 `true`,则响应将是一个不带 `Collection` 包装的单个 `CollectionPage`。 + + 请求需要 HTTP 签名。 + operationId: s2sRepliesGet + parameters: + - description: 账户的用户名。 + in: path + name: username + required: true + type: string + - description: 贴文的 ID。 + in: path + name: status + required: true + type: string + - default: false + description: 按 CollectionPage 返回响应。 + in: query + name: page + type: boolean + - default: false + description: 返回仅来自贴文所有者以外的账户的回复。 + in: query + name: only_other_accounts + type: boolean + - description: 下一贴文的最小 ID,用于分页。 + in: query + name: min_id + type: string + produces: + - application/activity+json + responses: + "200": + description: "" + schema: + $ref: '#/definitions/swaggerCollection' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止 + "404": + description: not found 未找到 + summary: 获取贴文回复 + tags: + - s2s/federation +schemes: + - https + - http +securityDefinitions: + OAuth2 Application: + flow: application + scopes: + write:accounts: grants write access to accounts 授予对账户的写入权限 + tokenUrl: https://example.org/oauth/token + type: oauth2 + OAuth2 Bearer: + authorizationUrl: https://example.org/oauth/authorize + flow: accessCode + scopes: + admin: 授予对所有内容的管理权限 + admin:accounts: 授予对账户的管理权限 + read: 授予对所有内容的读取权限 + read:accounts: 授予对账户的读取权限 + read:blocks: 授予对屏蔽的读取权限 + read:custom_emojis: 授予对自定义表情的读取权限 + read:favourites: 授予对点赞的读取权限 + read:filters: 授予对过滤规则的读取权限 + read:follows: 授予对关注的读取权限 + read:lists: 授予对列表的读取权限 + read:media: 授予对媒体的读取权限 + read:mutes: 授予对静音的读取权限 + read:notifications: 授予对通知的读取权限 + read:search: 授予对搜索的读取权限 + read:statuses: 授予对贴文的读取权限 + read:streaming: 授予对流式 API 的读取权限 + read:user: 授予对用户级信息的读取权限 + write: 授予对所有内容的写入权限 + write:accounts: 授予对账户的写入权限 + write:blocks: 授予对屏蔽的写入权限 + write:filters: 授予对过滤规则的写入权限 + write:follows: 授予对关注的写入权限 + write:lists: 授予对列表的写入权限 + write:media: 授予对媒体的写入权限 + write:mutes: 授予对静音的写入权限 + write:statuses: 授予对贴文的写入权限 + write:user: 授予对用户级信息的写入权限 + tokenUrl: https://example.org/oauth/token + type: oauth2 +swagger: "2.0" diff --git a/docs/locales/zh/api/throttling.md b/docs/locales/zh/api/throttling.md new file mode 100644 index 000000000..ec1485f96 --- /dev/null +++ b/docs/locales/zh/api/throttling.md @@ -0,0 +1,35 @@ +# 请求限流 + +GoToSocial 使用请求限流来限制与你的实例 API 的开放连接数。这是为了防止在一个账户含有成千上万粉丝的情况下,贴文被转发或回复时,避免你的实例意外被 DDOS 攻击(即所谓的[死亡之拥](https://en.wikipedia.org/wiki/Slashdot_effect))。 + +限流意味着只有有限数量的 HTTP 请求可同时处理,以便为每个请求提供快速响应,迅速完成。其原理是,快速处理较少的请求比同时尝试处理所有传入请求并每个用时多秒要好。 + +限流应用于不同的路由组,类似于[速率限制](./ratelimiting.md)的组织方式,因此,如果 API 的某个部分正处于限流状态,并不意味着所有部分都如此。 + +限流限制基于 GoToSocial 可用的 CPU 数量和配置值 `advanced-throttling-multiplier` 来计算。其计算方式如下: + +- 处理中的队列限制 = CPU 数量 * CPU 乘数。 +- 待处理队列限制 = 处理中的队列限制 * CPU 乘数。 + +对于默认乘数(8),得出以下值: + +```text +1 个 CPU = 08 处理中,064 待处理 +2 个 CPU = 16 处理中,128 待处理 +4 个 CPU = 32 处理中,256 待处理 +8 个 CPU = 64 处理中,512 待处理 +``` + +新请求如果超过处理中的限制将被放入待处理队列,并在有空位时进行处理(即当前处理中的请求完成时)。无法处理且无法放入待处理队列的请求将收到 HTTP 代码 [503 - 服务不可用](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503),并设置 `Retry-After` 头为 `30`(秒),以表示调用者稍后重试。 + +请求不会无限期地保留在待处理队列中:如果待处理中的请求无法在 30 秒内处理,它们也将收到 503 代码和 30 秒的重试等待。 + +## 限流常见问题 + +### 我可以调节请求限流吗? + +可以。只需根据你的 CPU 性能更改 `advanced-throttling-multiplier` 的值,CPU 性能强可调高,性能相对较弱可调低。 + +### 我可以禁用请求限流吗? + +可以。只需将 `advanced-throttling-multiplier` 设置为 `0` 或更小。这样将完全禁用 HTTP 请求限流,并尝试同时处理所有传入请求。如果你想使用外部服务或反向代理进行请求限流,并且不希望 GoToSocial 干扰你的设置,这是很有用的。 diff --git a/docs/locales/zh/configuration/accounts.md b/docs/locales/zh/configuration/accounts.md new file mode 100644 index 000000000..8e80d511b --- /dev/null +++ b/docs/locales/zh/configuration/accounts.md @@ -0,0 +1,44 @@ +# 账户 + +## 设置 + +```yaml +########################### +##### 账户配置 ##### +########################### + +# 服务器上账户创建与维护的配置,以及新账户的默认设置。 + +# 布尔值。允许人们通过 /signup 表单提交新的注册请求。 +# +# 选项: [true, false] +# 默认: false +accounts-registration-open: false + +# 布尔值。注册请求是否需要提交请求理由(例如,解释他们为何想加入此实例)? +# 选项: [true, false] +# 默认: true +accounts-reason-required: true + +# 布尔值。允许此实例上的账户为其个人资料页面和贴文设置自定义 CSS。 +# 启用此设置将允许账户通过 /user 设置页面上传自定义 CSS, +# 然后这些 CSS 将在账户的个人资料和贴文的网页视图中呈现。 +# +# 对于允许公开注册的实例,**强烈建议**将此设置保持为 'false', +# 因为设置为 true 允许恶意账户使其个人资料页面具有误导性、不可用 +# 或对访问者甚至危险。换句话说,只有在你信任实例上的用户不会产生有害 CSS 时, +# 才应启用此设置。 +# +# 无论此值设置为何,任何上传的 CSS 都不会联合到其他实例,仅在*本*实例上的个人资料和贴文中显示。 +# +# 选项: [true, false] +# 默认: false +accounts-allow-custom-css: false + +# 整数值。如果 accounts-allow-custom-css 为 true,则为此实例上账户上传的 +# CSS 允许的字符长度。如果 accounts-allow-custom-css 为 false,则无效。 +# +# 示例: [500, 5000, 9999] +# 默认: 10000 +accounts-custom-css-length: 10000 +``` diff --git a/docs/locales/zh/configuration/advanced.md b/docs/locales/zh/configuration/advanced.md new file mode 100644 index 000000000..c41f3e47d --- /dev/null +++ b/docs/locales/zh/configuration/advanced.md @@ -0,0 +1,152 @@ +# 进阶设置 + +提供进阶设置选项是为了让管理员能够根据自己的喜好调整实例。 + +这些设置已设置为合理的默认值,所以大多数服务器管理员不需要更改或考虑它们。 + +**如果你不知道自己在做什么,修改这些设置可能会导致实例出错**。 + +## 设置 + +```yaml +############################# +##### 进阶设置 ##### +############################# + +# 与HTTP超时、安全性、Cookie等相关的进阶设置。 +# +# 只有在你了解自己在做什么的情况下才调整这些设置! +# +# 大多数用户不需要(也不应该)修改这些设置,因为它们被设为合理的默认值,改变可能导致问题。 +# +# 不过,这些设置提供给服务器管理员用于性能或安全原因的调整。 + +# 字符串。GoToSocial设置的Cookie的SameSite属性值。 +# 默认设置为 'lax' 以确保OIDC流程不会中断,这通常是可以的。 +# 如果你希望加强实例对抗CSRF攻击,并且不介意某些登录相关操作可能中断,可以将其设置为 'strict'。 +# +# 关于此设置的概述,请参见: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite +# +# 选项: ["lax", "strict"] +# 默认: "lax" +advanced-cookies-samesite: "lax" + +# 整数。允许单个IP地址在5分钟内对每个路由分组的请求数量。 +# 如果超出此数量,将返回429 HTTP错误代码。 +# +# 如果你发现需要调整此限制,是因为它经常被超出,你应首先验证 `trusted-proxies` 配置是否正确。 +# 在许多情况下,超出速率限制是因为你的实例将所有传入请求视为来自*相同的IP地址*(你可以通过查看实例日志中的客户端IP来验证)。 +# 如果是这种情况,尝试在调整此速率限制设置*之前*将该IP地址添加到你的`trusted-proxies`中! +# +# 如果将此设置为0或更少,则完全禁用速率限制。 +# +# 示例: [1000, 500, 0] +# 默认: 300 +advanced-rate-limit-requests: 300 + +# 字符串数组。要从速率限制中排除的CIDR范围。 +# CIDR范围内的任何IP的请求将不受速率限制,并且这些请求不会设置速率限制头。 +# +# 对于IPv6,我们只考虑到/64的子网。如果你想开放更大的前缀,你需要列出多个前缀。 +# +# 在以下示例情况下(可能还有很多其他情况),这可能很有用: +# +# 1. 你已设置使用API的自动化服务,但它频繁被限速,你信任它没有滥用实例。资源 +# +# 2. 你和多人共用同一路由器/NAT登录同一实例,所以你们都有相同的IP地址,并且不断相互限速。 +# +# 3. 你主要使用自己的家庭网络访问实例,并希望豁免家庭网络的速率限制。 +# +# 调整此设置时需要小心,因为如果设置范围过宽,可能会使速率限制变得无用。如果不确定,建议宁少勿多,并根据需要调整。 +# +# 示例: ["192.168.0.0/16", "2001:DB8:FACE:CAFE::/64"] +# 默认: [] +advanced-rate-limit-exceptions: [] + +# 整数。每个CPU、每个路由分组允许的开放请求数量,以应用HTTP请求限制。 +# 超出计算限制的请求将被保留在一个等待队列中,最长30秒然后处理或超时。 +# 不在等待队列中的请求将返回状态503,并设置“Retry-After”头为30秒。 +# +# 开放请求限制为可用CPU * 乘数;等待队列限制为限制 * 乘数。 +# +# 乘数为8的示例值: +# +# 1 cpu = 08 开放, 064 等待 +# 2 cpu = 16 开放, 128 等待 +# 4 cpu = 32 开放, 256 等待 +# +# 乘数为4的示例值: +# +# 1 cpu = 04 开放, 016 等待 +# 2 cpu = 08 开放, 032 等待 +# 4 cpu = 16 开放, 064 等待 +# +# 乘数为8是合理的默认值,但对于运行在性能非常高的硬件上的实例,你可能希望增加它;对于使用非常慢的CPU的实例,你可能希望减少它。 +# +# 如果将此设置为0或更少,将完全禁用HTTP请求限制。 +# +# 示例: [8, 4, 9, 0] +# 默认: 8 +advanced-throttling-multiplier: 8 + +# 持续时间。用于响应限速请求的“retry-after”头值的时间段。 +# 最小分辨率为1秒。 +# +# 示例: [30s, 10s, 5s, 1m] +# 默认: "30s" +advanced-throttling-retry-after: "30s" + +# 整数。用于通过ActivityPub发送消息的固定协程数量的CPU倍数。 +# 消息将被批量处理并推送到单一队列,倍数 * CPU数的协程将提取对垒中的消息并尝试发送。 +# 这可以用于限制对外站收件箱的并发发布,防止当有很多关注者的账户发布贴文时实例CPU使用率激增。 +# +# 如果将此设置为0或更少,无论CPU数量如何,都只会使用1个发送者。这可能在你有非常严格的网络或CPU限制时有用。 +# +# 乘数为2的示例值(默认): +# +# 1 cpu = 2 个并发发送者 +# 2 cpu = 4 个并发发送者 +# 4 cpu = 8 个并发发送者 +# +# 乘数为4的示例值: +# +# 1 cpu = 4 个并发发送者 +# 2 cpu = 8 个并发发送者 +# 4 cpu = 16 个并发发送者 +# +# 乘数<1的示例值: +# +# 1 cpu = 1 个并发发送者 +# 2 cpu = 1 个并发发送者 +# 4 cpu = 1 个并发发送者 +advanced-sender-multiplier: 2 + +# 字符串数组。为实例设置Content-Security-Policy头时,要添加到'img-src'和'media-src'中的额外URI。 +# +# 这可以用于在浏览器中查看实例页面和个人资料时,允许加载来自额外来源(如S3桶等)的资源。 +# +# 由于非代理的S3存储将在实例启动时被探测以生成正确的Content-Security-Policy,你可能永远都不需要修改此设置,但把它包括在内是因为“可配置项(通常)越多越好”。 +# +# 参见: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP +# +# 示例: ["s3.example.org", "some-bucket-name.s3.example.org"] +# 默认: [] +advanced-csp-extra-uris: [] + +# 字符串。用于此实例的HTTP请求头过滤模式。 +# +# "block" -- 只有明确被请求头过滤规则阻止的请求会被拒绝(除非它们被明确允许)。 +# +# "allow" -- 只有明确被请求头过滤规则允许的请求会被接受(除非它们被明确阻止)。 +# 此模式被视为实验性功能,并且几乎肯定会破坏对你实例的访问,除非非常小心。 +# +# "" -- 请求头过滤禁用。 +# +# 有关阻止和允许模式的更多详细信息,请查看文档: +# https://docs.gotosocial.org/zh-cn/latest/admin/request_filtering_modes +# +# 选项: ["block", "allow", ""] +# 默认: "" +advanced-header-filter-mode: "" +``` \ No newline at end of file diff --git a/docs/locales/zh/configuration/database.md b/docs/locales/zh/configuration/database.md new file mode 100644 index 000000000..cddd1d186 --- /dev/null +++ b/docs/locales/zh/configuration/database.md @@ -0,0 +1,192 @@ +# 数据库 + +GoToSocial 将贴文、账号等存储在数据库中。可以选择使用 [SQLite](https://sqlite.org/index.html) 或 [Postgres](https://www.postgresql.org/)。 + +默认情况下,GoToSocial 使用 Postgres,但可以轻松更改。 + +## SQLite + +顾名思义,SQLite 是 GoToSocial 可用的最轻量级的数据库类型。它以简单的文件格式存储条目,通常与 GoToSocial 二进制文件位于同一目录。SQLite 非常适合小规模实例和单板计算机,使用专用数据库对它们来说过于复杂。 + +要配置 GoToSocial 使用 SQLite,将 `db-type` 更改为 `sqlite`。此时 `address` 设置将是一个文件名而不是地址,所以你需要将其更改为 `sqlite.db` 或类似名称。 + +注意,`:memory:` 设置将使用 *内存数据库*,当你的 GoToSocial 实例停止运行时,内存将被清除。这仅用于测试,绝不适用于运行正式实例,因此*不要这样做*。 + +## Postgres + +Postgres 是较重的数据库格式,适用于需要扩展性能的大型实例,或者需要在 GoToSocial 实例之外的专用计算机上运行数据库的情况(或运行数据库集群等复杂应用)。 + +你可以使用 Unix 套接字连接或 TCP 连接到 Postgres,这取决于你设置的 `db-address` 值。 + +GoToSocial 还支持使用 SSL/TLS 通过 TCP 连接到 Postgres。如果你在不同的计算机上运行 Postgres,并通过 IP 地址或主机名连接它(而不是仅在本地运行),那么 SSL/TLS **至关重要**,以防止数据泄露! + +使用 Postgres 时,GoToSocial 期望你已经在数据库中创建了 `db-user` 并拥有 `db-database` 的所有权。 + +例如,如果你设置了: + +```text +db: + [...] + user: "gotosocial" + password: "some_really_good_password" + database: "gotosocial" +``` + +那么你应该已经在 Postgres 中创建了数据库 `gotosocial`,并将其所有权授予 `gotosocial` 用户。 + +执行这些操作的 psql 命令如下: + +```psql +create database gotosocial with locale 'C.UTF-8' template template0; +create user gotosocial with password 'some_really_good_password'; +grant all privileges on database gotosocial to gotosocial; +``` + +GoToSocial 使用 ULIDs(全局唯一且按字典顺序可排序的标识符),这在非英文排序环境中不起作用。因此,创建数据库时使用 `C.UTF-8` 地区设置很重要。在已经使用非 C 地区初始化的系统上,必须使用 `template0` 原始数据库模板才能进行。 + +如果你希望使用特定选项连接到 Postgres,可以使用 `db-postgres-connection-string` 定义连接字符串。如果 `db-postgres-connection-string` 已定义,则所有其他与数据库相关的配置字段将被忽略。例如,可以使用 `db-postgres-connection-string` 连接到 `mySchema`,用户名为 `myUser`,密码为 `myPass`,在 `localhost` 上,数据库名称为 `db`: + +```yaml +db-postgres-connection-string: 'postgres://myUser:myPass@localhost/db?search_path=mySchema' +``` + +## 设置 + +```yaml +############################ +##### 数据库配置 ###### +############################ + +# GoToSocial 数据库连接的相关配置 + +# 字符串。数据库类型。 +# 选项: ["postgres","sqlite"] +# 默认: "postgres" +db-type: "postgres" + +# 字符串。数据库地址或参数。 +# +# 对于 Postgres,这应该是数据库可以访问的地址或套接字。 +# +# 对于 Sqlite,这应该是你的 sqlite 数据库文件的路径。比如,/opt/gotosocial/sqlite.db。 +# 如果在指定路径不存在该文件,会自动创建。 +# 如果只提供了文件名(没有目录),那么数据库将创建在 GoToSocial 二进制文件的同一目录中。 +# 如果 `address` 设置为 :memory:,将使用内存数据库(没有文件)。 +# 警告: :memory: 应该仅用于测试目的,不应在其他情况下使用。 +# +# 示例: ["localhost","my.db.host","127.0.0.1","192.111.39.110",":memory:", "sqlite.db"] +# 默认: "" +db-address: "" + +# 整数。数据库连接的端口。 +# 示例: [5432, 1234, 6969] +# 默认: 5432 +db-port: 5432 + +# 字符串。数据库连接的用户名。 +# 示例: ["mydbuser","postgres","gotosocial"] +# 默认: "" +db-user: "" + +# 字符串。数据库连接使用的密码 +# 示例: ["password123","verysafepassword","postgres"] +# 默认: "" +db-password: "" + +# 字符串。要在提供的数据库类型中使用的数据库名称。 +# 示例: ["mydb","postgres","gotosocial"] +# 默认: "gotosocial" +db-database: "gotosocial" + +# 字符串。禁用、启用或要求数据库的 SSL/TLS 连接。 +# 如果为 "disable",则不会尝试 TLS 连接。 +# 如果为 "enable",则会尝试 TLS,但不会检查数据库证书(适用于自签名证书)。 +# 如果为 "require",则需要 TLS 进行连接,并且必须提供有效证书。 +# 选项: ["disable", "enable", "require"] +# 默认: "disable" +db-tls-mode: "disable" + +# 字符串。用于数据库证书验证的主机的 CA 证书路径。 +# 如果留空,仅使用主机证书。 +# 如果填写,则会加载证书并添加到主机证书中。 +# 示例: ["/path/to/some/cert.crt"] +# 默认: "" +db-tls-ca-cert: "" + +# 整数。乘以 CPU 数量以设置允许总数的打开数据库连接(使用和空闲)。 +# 你可以使用此设置来调整你的数据库连接行为,但大多数管理员不需要更改它。 +# +# 乘数 8 的示例值: +# +# 1 cpu = 08 打开的连接 +# 2 cpu = 16 打开的连接 +# 4 cpu = 32 打开的连接 +# +# 乘数 4 的示例值: +# +# 1 cpu = 04 打开的连接 +# 2 cpu = 08 打开的连接 +# 4 cpu = 16 打开的连接 +# +# 乘数 8 是一个合理的默认值,但你可能希望为在非常高性能硬件上运行的实例增加此值,或为使用非常慢的 CPU 的实例减少此值。 +# +# 请注意!!:此设置目前仅适用于 Postgres。SQLite 将始终使用 1 个连接,无论此处设置为何。这种行为将在实现更好的 SQLITE_BUSY 处理时更改。 +# 更多详情请参见 https://github.com/superseriousbusiness/gotosocial/issues/1407。 +# +# 示例: [16, 8, 10, 2] +# 默认: 8 +db-max-open-conns-multiplier: 8 + +# 字符串。SQLite 日志模式。 +# 仅适用于 SQLite -- 否则不使用。 +# 如果设置为空字符串,则使用 sqlite 默认值。 +# 参见: https://www.sqlite.org/pragma.html#pragma_journal_mode +# 示例: ["DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"] +# 默认: "WAL" +db-sqlite-journal-mode: "WAL" + +# 字符串。SQLite 同步模式。 +# 仅适用于 SQLite -- 否则不使用。 +# 如果设置为空字符串,则使用 sqlite 默认值。 +# 参见: https://www.sqlite.org/pragma.html#pragma_synchronous +# 示例: ["OFF", "NORMAL", "FULL", "EXTRA"] +# 默认: "NORMAL" +db-sqlite-synchronous: "NORMAL" + +# 字节大小。SQlite 缓存大小。 +# 仅适用于 SQLite -- 否则不使用。 +# 如果设置为空字符串或零,则使用 sqlite 默认值(2MiB)。 +# 参见: https://www.sqlite.org/pragma.html#pragma_cache_size +# +# 缓存并非越大越好。它们需要针对工作负载进行调整。默认设置对于大多数实例应该已足够,不应该更改。 +# 如果你确实更改它,请确保在 GoToSocial 帮助频道求助时提到这一点。 +# +# 示例: ["0", "2MiB", "8MiB", "64MiB"] +# 默认: "8MiB" +db-sqlite-cache-size: "8MiB" + +# 持续时间。SQlite 忙等待时间。 +# 仅适用于 SQLite -- 否则不使用。 +# 如果设置为空字符串或零,则使用 sqlite 默认值。 +# 参见: https://www.sqlite.org/pragma.html#pragma_busy_timeout +# 示例: ["0s", "1s", "30s", "1m", "5m"] +# 默认: "30m" +db-sqlite-busy-timeout: "30m" + +# 字符串。完整的数据库连接字符串 +# +# 此连接字符串仅适用于 Postgres。当定义此字段时,所有其他与数据库相关的配置字段将被忽略。 +# 此字段允许你微调与 Postgres 的连接。 +# +# 示例: ["postgres://user:pass@localhost/db?search_path=gotosocial", "postgres://user:pass@localhost:9999/db"] +# 默认: "" +db-postgres-connection-string: "" + +cache: + # cache.memory-target 设置一个目标限制, + # 应用程序将尝试将其缓存保持在此限制内。 + # 这是基于内存对象的估计大小,因此绝对不精确。 + # 示例: ["100MiB", "200MiB", "500MiB", "1GiB"] + # 默认: "100MiB" + memory-target: "100MiB" +``` \ No newline at end of file diff --git a/docs/locales/zh/configuration/general.md b/docs/locales/zh/configuration/general.md new file mode 100644 index 000000000..56b1388d4 --- /dev/null +++ b/docs/locales/zh/configuration/general.md @@ -0,0 +1,121 @@ +# 基础配置 + +GoToSocial 的基础配置,包括域名、端口、绑定地址和传输协议等基本内容。 + +这里*真正*需要设置的只有 `host`,也就是你实例可以访问的域名,可能还需要设置 `port`。 + +## 设置 + +```yaml +########################### +##### 通用配置 ###### +########################### + +# 字符串。应用程序使用的日志级别,必须是小写。 +# 选项: ["trace","debug","info","warn","error","fatal"] +# 默认: "info" +log-level: "info" + +# 布尔值。当日志级别设置为 debug 或 trace 时记录数据库查询。 +# 这一设置会产生详细的日志,因此最好在你尝试定位问题时才启用。 +# 选项: [true, false] +# 默认: false +log-db-queries: false + +# 布尔值。在日志行中包含客户端 IP。 +# 选项: [true, false] +# 默认: true +log-client-ip: true + +# 字符串。日志行中时间戳的格式。 +# 如果设置为空字符串,则时间戳将完全从日志中省略。 +# +# 该格式必须符合 Go 的 time.Layout 规定, +# 详见 https://pkg.go.dev/time#pkg-constants。 +# +# 示例: ["2006-01-02T15:04:05.000Z07:00", ""] +# 默认: "02/01/2006 15:04:05.000" +log-timestamp-format: "02/01/2006 15:04:05.000" + +# 字符串。内部使用的应用程序名称。 +# 示例: ["My Application","gotosocial"] +# 默认: "gotosocial" +application-name: "gotosocial" + +# 字符串。在首页显示的用户。如果没有设置用户,将显示默认的首页。 +# 示例: "admin" +# 默认: "" +landing-page-user: "" + +# 字符串。可以访问到本实例的主机名。默认值为用于本地测试的 localhost, +# 但在实际运行时你*绝对*应该更改此设置,否则你的服务器将无法正常工作。 +# 在你的实例已经运行过一次后,请不要更改此项,否则会导致问题! +# 示例: ["gts.example.org","some.server.com"] +# 默认: "localhost" +host: "localhost" + +# 字符串。在交换账户信息时使用的域名。当你希望服务器位于 +# "gts.example.org",但希望账户域名为 "example.org" 时,这会更好看, +# 或更加简短易记。 +# +# 为使此设置正常工作,你需要将 "example.org/.well-known/webfinger" 的请求 +# 重定向到 "gts.example.org/.well-known/webfinger",以便 GtS 正常处理它们。 +# +# 你还应该以同样的方式重定向 "example.org/.well-known/nodeinfo" 的请求。 +# +# 你还应该以同样的方式重定向 "example.org/.well-known/host-meta" 的请求。 +# 这个端点被许多客户端用于在主机名和账户域名不同时发现 API 端点。 +# +# 空字符串(即,未设置)表示将使用 'host' 的相同值。 +# +# 在你的服务器已经运行过一次后请不要更改此项,否则会导致问题! +# +# 在更改此设置前,请阅读安装指南的相应部分: +# https://docs.gotosocial.org/zh-cn/latest/advanced/host-account-domain/ +# +# 示例: ["example.org","server.com"] +# 默认: "" +account-domain: "" + +# 字符串。服务器从外界可访问的协议。 +# +# 仅在本地测试时,才需将其更改为 HTTP!在 99.99% 的情况下你不应该更改此项! +# +# 这应该是你的服务器实际可以访问的 URI 的协议部分。 +# 因此,即使你在处理 SSL 证书的反向代理之后运行 GoToSocial, +# 而不是使用内置的 letsencrypt,它仍然应该是 https,而不是 http。 +# +# 再次强调,仅在本地测试时才需将其更改为 HTTP!如果你将其设置为 `http`,启动实例, +# 然后再更改为 `https`,你的实例上已有的用户的 URI 生成过程将被破坏。在 100% 知道自己在做什么时才更改此设置。 +# +# 选项: ["http","https"] +# 默认: "https" +protocol: "https" + +# 字符串。GoToSocial 服务器绑定的地址。 +# 可以是 IPv4 地址或 IPv6 地址(用方括号括起来),或者是主机名。 +# 默认值为绑定到所有接口,使服务器可以被其他机器访问。在大多数场景中无需更改此项。 +# 如果你在与代理同一台机器上使用反向代理设置 GoToSocial, +# 建议将其设置为 "localhost" 或等效值,以防止代理被绕过。 +# 示例: ["0.0.0.0", "172.128.0.16", "localhost", "[::]", "[2001:db8::fed1]"] +# 默认: "0.0.0.0" +bind-address: "0.0.0.0" + +# 整数。GoToSocial 网页服务器和 API 的监听端口。如果你在反向代理和/或 Docker 容器中运行, +# 请将其设置为任意值(或保留默认值),并确保正确转发。 +# 如果你启用了内建 letsencrypt 并在本机直接运行 GoToSocial, +# 可能希望将其设置为 443(标准 https 端口),除非你有其他服务正在使用该端口。 +# 此项*不得*与下面指定的 letsencrypt 端口相同,除非禁用 letsencrypt。 +# 示例: [443, 6666, 8080] +# 默认: 8080 +port: 8080 + +# 字符串数组。用于通过反向代理确定真实客户端 IP 的受信任代理的 CIDR 或 IP 地址。 +# 如果你的实例在 Docker 容器中运行,且位于 Traefik 或 Nginx 后,请添加你的 Docker 网络的子网, +# 或 Docker 网络的网关,和/或反向代理的地址(如果不是运行在本机上)。 +# 示例: ["127.0.0.1/32", "172.20.0.1"] +# 默认: ["127.0.0.1/32", "::1"] (本地主机 ipv4 + ipv6) +trusted-proxies: + - "127.0.0.1/32" + - "::1" +``` \ No newline at end of file diff --git a/docs/locales/zh/configuration/httpclient.md b/docs/locales/zh/configuration/httpclient.md new file mode 100644 index 000000000..b04cfa487 --- /dev/null +++ b/docs/locales/zh/configuration/httpclient.md @@ -0,0 +1,67 @@ +# HTTP 客户端 + +## 设置 + +```yaml +################################ +##### HTTP 客户端设置 ##### +################################ + +# GoToSocial 用于向外站资源发送请求的 HTTP 客户端连接设置 +# (如贴文获取、媒体获取、向对方收件箱发信等)。 + +http-client: + + # 持续时间。对外 HTTP 请求的超时时长。 + # 如果超时,连接到外站服务器的请求将被中断。 + # 设置为 0s 表示没有超时:不建议这样做! + # 示例: ["5s", "10s", "0s"] + # 默认: "10s" + timeout: "10s" + + ######################################## + #### 保留的例外 IP 范围 ###### + ######################################## + # + # 在提供的 IPv4/v6 CIDR 范围内显式允许或屏蔽出站连接。 + # + # 默认情况下,作为基本的安全预防措施,GoToSocial 屏蔽大多数“特殊用途” + # IP 范围内的出站连接。然而,具有更复杂设置(代理、特殊 NAT 环境等)的管理员 + # 可能希望显式覆盖一个或多个被屏蔽的范围。 + # + # 以下每个允许/屏蔽配置选项接受一个 IPv4 和/或 IPv6 CIDR 字符串数组。 + # 例如,要覆盖本地站的 IPv4 和 IPv6 建立连接的硬编码屏蔽,请设置: + # + # allow-ips: ["127.0.0.1/32", "::1/128"]. + # + # 你也可以使用 YAML 多行数组来定义这些,但要注意缩进。 + # + # 建立连接时,GoToSocial 将首先检查目标是否在显式允许的 IP 范围内, + # 然后检查显式屏蔽的 IP 范围,再检查默认(硬编码)屏蔽的 IP 范围, + # 首次命中允许匹配项时返回 OK,首次命中屏蔽匹配项时返回不 OK, + # 或如果没有命中,则默认返回 OK。 + # + # 和所有安全设置一样,最好从最严格的配置开始,根据用例放宽, + # 而不是从最宽松的配置开始,然后再试图亡羊补牢。记住这一点: + # - 除非你有充分的理由,否则不要修改这些设置,只在你知道自己在做什么的情况下修改。 + # - 添加显式允许的例外 IP 段时,尽可能使用最小 CIDR。 + # + # 有关保留/特殊范围,请参阅: + # - https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml + # - https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml + # + # allow-ips 和 block-ips 默认都是空数组。 + allow-ips: [] + block-ips: [] + + # 布尔值。禁用对外站服务器 TLS 证书的验证。 + # 设置为 'true' 时,当外站服务器提供无效或自签名证书时, + # GoToSocial 不会报错。 + # + # 该设置仅用于测试!如果你在生产环境中启用, + # 就会让你的服务器容易受到中间人攻击!不要更改此设置, + # 除非你非常清楚自己在做什么以及为什么这么做。 + # + # 默认: false + tls-insecure-skip-verify: false +``` diff --git a/docs/locales/zh/configuration/index.md b/docs/locales/zh/configuration/index.md new file mode 100644 index 000000000..7eba92186 --- /dev/null +++ b/docs/locales/zh/configuration/index.md @@ -0,0 +1,138 @@ +# 配置概述 + +GoToSocial 力求尽可能让所有属性可配置,以适应多种不同的使用场景。 + +我们尽量提供合理的默认值,但在运行 GoToSocial 实例时,你需要进行*一些*配置管理。 + +## 配置方法 + +配置 GoToSocial 实例有三种不同的方法,这些方法可以根据你的设置进行组合。 + +### 配置文件 + +配置 GoToSocial 最简单的方法是将配置文件传递给 `gotosocial server start` 命令,例如: + +```bash +gotosocial --config-path ./config.yaml server start +``` + +该命令需要一个 [YAML](https://en.wikipedia.org/wiki/YAML) 或 [JSON](https://en.wikipedia.org/wiki/JSON) 格式的文件。 + +可以在[这里](https://github.com/superseriousbusiness/gotosocial/blob/main/example/config.yaml)找到示例配置文件,其中包含每个配置字段的解释、默认值和示例值。此示例文件也包含在每个发行版的下载资源中。 + +建议创建你自己的配置文件,只更改你需要改变的设置。这可以确保在每次发布时,你不必合并默认值的更改或者增删未从默认值更改的配置设置。 + +#### 在容器中挂载 + +你可能需要在容器中挂载一个 `config.yaml`,因为某些设置不容易通过环境变量或命令行标志管理。 + +为此,请在主机上创建一个 `config.yaml`,将其挂载到容器中,然后告诉 GoToSocial 读取该配置文件。可以通过将容器的运行命令设置为 `--config-path /path/inside/container/to/config.yaml` 或使用 `GTS_CONFIG_PATH` 环境变量来实现这一点。 + +对于 docker compose,可以这样修改配置: + +```yaml +services: + gotosocial: + command: ["--config-path", "/gotosocial/config.yaml"] + volumes: + - type: bind + source: /path/on/the/host/to/config.yaml + target: /gotosocial/config.yaml + read_only: true +``` + +或者,通过环境变量来修改配置: + +```yaml +services: + gotosocial: + environment: + GTS_CONFIG_PATH: /gotosocial/config.yaml + volumes: + - type: bind + source: /path/on/the/host/to/config.yaml + target: /gotosocial/config.yaml + read_only: true +``` + +对于 Docker 或 Podman 命令行,需要传递一个 [符合规范的挂载参数](https://docs.podman.io/en/latest/markdown/podman-run.1.html#mount-type-type-type-specific-option)。 + +在使用 `docker run` 或 `podman run` 时,传递 `--config-path /gotosocial/config.yaml` 作为命令,例如: + +```sh +podman run \ + --mount type=bind,source=/path/on/the/host/to/config.yaml,destination=/gotosocial/config.yaml,readonly \ + docker.io/superseriousbusiness/gotosocial:latest \ + --config-path /gotosocial/config.yaml +``` + +使用 `GTS_CONFIG_PATH` 环境变量: + +```sh +podman run \ + --mount type=bind,source=/path/on/the/host/to/config.yaml,destination=/gotosocial/config.yaml,readonly \ + --env 'GTS_CONFIG_PATH=/gotosocial/config.yaml' \ + docker.io/superseriousbusiness/gotosocial:latest +``` + +### 环境变量 + +你也可以通过设置[环境变量](https://en.wikipedia.org/wiki/Environment_variable)来配置 GoToSocial。这些环境变量遵循的格式为: + +1. 在配置标志前加上 `GTS_`。 +2. 全部使用大写。 +3. 将短横线(`-`)替换为下划线(`_`)。 + +例如,如果不想在 config.yaml 中设置 `media-image-max-size` 为 `2097152`,你可以改为设置环境变量: + +```text +GTS_MEDIA_IMAGE_MAX_SIZE=2097152 +``` + +如果对于环境变量名称有疑问,只需查看你正在使用的子命令的 `--help`。 + +### 命令行标志 + +最后,你可以使用命令行标志来设置配置值,这些标志是在运行 `gotosocial` 命令时直接传递的。例如,不在 config.yaml 或环境变量中设置 `media-image-max-size`,你可以直接通过命令行传递值: + +```bash +gotosocial server start --media-image-max-size 2097152 +``` + +如果不确定哪些标志可用,请检查 `gotosocial --help`。 + +## 优先级 + +上述配置方法按列出的顺序相互覆盖。 + +```text +命令行标志 > 环境变量 > 配置文件 +``` + +也就是说,如果你在配置文件中将 `media-image-max-size` 设置为 `2097152`,但*也*设置了环境变量 `GTS_MEDIA_MAX_IMAGE_SIZE=9999999`,则最终值将为 `9999999`,因为环境变量比 config.yaml 中设置的值具有*更高的优先级*。 + +命令行标志具有最高优先级,因此如果你设置了 `--media-image-max-size 13121312`,无论你在其他地方设置了什么,最终值都将为 `13121312`。 + +这意味着在你只想尝试改变一件事,但不想编辑配置文件的情况下,可以临时使用环境变量或命令行标志来设置那个东西。 + +## 默认值 + +*大多数*配置参数都提供了合理的默认值,除了必须自定义值的情况。 + +请查看[示例配置文件](https://github.com/superseriousbusiness/gotosocial/blob/main/example/config.yaml)以获取默认值,或运行 `gotosocial --help`。 + +## `GTS_WAZERO_COMPILATION_CACHE` + +启动时,GoToSocial 会将嵌入的 WebAssembly `ffmpeg` 和 `ffprobe` 二进制文件编译为 [Wazero](https://wazero.io/) 兼容模块,这些模块用于媒体处理,无需任何外部依赖。 + +为了加快 GoToSocial 的启动时间,你可以在首次启动时缓存已编译的模块,这样 GoToSocial 就不必在每次启动时从头开始编译它们。 + +你可以通过将环境变量 `GTS_WAZERO_COMPILATION_CACHE` 设置为一个目录来指示 GoToSocial 存储 Wazero 工件,该目录将由 GtS 用于存储两个大小约为 ~50MiB 的小型工件(总计约 ~100MiB)。 + +要了解此方法的示例,请参见 [docker-compose.yaml](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/example/docker-compose/docker-compose.yaml) 和 [gotosocial.service](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/example/gotosocial.service) 示例文件。 + +如果你希望在 systemd 或 Docker 之外为 GtS 提供此值,可以在启动 GtS 服务器时通过以下方式进行: + +```bash +GTS_WAZERO_COMPILATION_CACHE=~/gotosocial/.cache ./gotosocial --config-path ./config.yaml server start +``` diff --git a/docs/locales/zh/configuration/instance.md b/docs/locales/zh/configuration/instance.md new file mode 100644 index 000000000..e1c8c8251 --- /dev/null +++ b/docs/locales/zh/configuration/instance.md @@ -0,0 +1,115 @@ +# 站点 + +## 设置 + +```yaml +########################### +##### 站点配置 ##### +########################### + +# 与实例间联合、隐藏/显示页面等相关的配置。 + +# 字符串数组。BCP47 语言标签,用于指示本站用户的首选语言。 +# +# 如果你提供了这些标签,请按照从最优先到最次优先的顺序提供。 +# 但请注意,从此数组中省略某种语言并不意味着该语言不能在本站使用, +# 而只是表示不会将其作为为本站的首选语言展示。 +# +# 这里可以不提供任何条目;这样的话,你的站点将没有特定的首选语言。 +# +# 常用标签参考此处:https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags +# 所有当前标签参考此处:https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry +# +# 示例: ["zh", "zh-Hans", "zh-Hans-CN", "zh-Hant", "zh-Hant-TW", "en"] +# 默认值: [] +instance-languages: [] + +# 字符串。用于本站的联合模式。 +# +# "blocklist" -- 默认开放联合。只有被明确屏蔽的站点会被拒绝(除非它们被另外明确设定的允许规则放行)。 +# +# "allowlist" -- 默认关闭联合。只有被明确允许的站点才能与本站互动。 +# +# 关于屏蔽列表和允许列表模式的更多详细信息,请查阅文档: +# https://docs.gotosocial.org/zh-cn/latest/admin/federation_modes +# +# 选项: ["blocklist", "allowlist"] +# 默认值: "blocklist" +instance-federation-mode: "blocklist" + +# 布尔值。启用针对通过联合 API 进入本站的消息的启发式垃圾过滤。无论你在此处设置什么, +# 都仍将执行基本的相关性检查,但如果你被其他站点的垃圾信息骚扰,并希望更严格地过滤掉垃圾信息, +# 可以尝试启用此设置。 +# +# 这是一个实验性设置,可能会过滤掉合法信息,或未能过滤掉垃圾信息。建议仅在 +# Fediverse 出现垃圾信息潮时启用此设置,以保持站点的可用性。 +# +# 判断消息是否为垃圾信息的决策基于以下启发规则,依次进行,接收者 = 收到消息的账户,发送者 = 发送消息的外站账户。 +# +# 首先执行基本相关性检查 +# +# 1. 接收者关注了发送者。返回 OK。 +# 2. 贴文未提及接收者。返回 NotRelevant。 +# +# 如果 instance-federation-spam-filter = false,则现在返回 OK。 +# 否则进一步检查: +# +# 3. 接收者已锁定账号并被发送者关注。返回 OK。 +# 4. 提及五人或以上。返回 Spam。 +# 5. 接收者关注(请求关注)一个被提及账户。返回 OK。 +# 6. 贴文有媒体附件。返回 Spam。 +# 7. 贴文包含非提及、非话题标签的链接。返回 Spam。 +# +# 被识别为垃圾的信息将从本站删除,不会插入数据库或主页时间线或通知中。 +# +# 选项: [true, false] +# 默认值: false +instance-federation-spam-filter: false + +# 布尔值。允许未经认证的用户查询 /api/v1/instance/peers?filter=open 以查看与本站联合的站点列表。 +# 即使设置为 'false',认证用户(本站成员)仍然可以查询该端点。 +# 选项: [true, false] +# 默认值: false +instance-expose-peers: false + +# 布尔值。允许未经认证的用户查询 /api/v1/instance/peers?filter=suspended 以查看被本站屏蔽/封禁的站点列表。 +# 即使设置为 'false',认证用户(本站成员)仍然可以查询该端点。 +# +# 警告:将此变量设置为 'true' 可能导致你的站点被屏蔽列表爬虫攻击。 +# 参考: https://docs.gotosocial.org/zh-cn/latest/admin/domain_blocks/#block-announce-bots +# +# 选项: [true, false] +# 默认值: false +instance-expose-suspended: false + +# 布尔值。允许未经认证的用户查看 /about/suspended, +# 显示本站屏蔽/封禁站点的 HTML 渲染列表。 +# 选项: [true, false] +# 默认值: false +instance-expose-suspended-web: false + +# 布尔值。允许未经认证的用户查询 /api/v1/timelines/public 以查看本站的公共贴文列表。 +# 即使设置为 'false',认证用户(本站成员)仍然可以查询该端点。 +# 选项: [true, false] +# 默认值: false +instance-expose-public-timeline: false + +# 布尔值。此标志是否将 GoToSocial 的 ActivityPub 消息发送到收件人的共享收件箱(如果可用), +# 而不是将每条消息分别发送给应接收消息的每个主体。 +# +# 当发送给共享收件箱的多个收件人时,共享收件箱传递能显著减少网络负载(例如,大型 Mastodon 实例)。 +# +# 参考: https://www.w3.org/TR/activitypub/#shared-inbox-delivery +# +# 选项: [true, false] +# 默认值: true +instance-deliver-to-shared-inboxes: true + +# 布尔值。此标志将在 /api/v1/instance 中包含的版本字段中注入一个 Mastodon 版本。 +# 这个版本通常被 Mastodon 客户端用于 API 功能检测。通过注入一个与 Mastodon 兼容的版本, +# 可以促使那些客户端在 GoToSocial 上正常运行。 +# +# 选项: [true, false] +# 默认值: false +instance-inject-mastodon-version: false +``` diff --git a/docs/locales/zh/configuration/media.md b/docs/locales/zh/configuration/media.md new file mode 100644 index 000000000..399a13f44 --- /dev/null +++ b/docs/locales/zh/configuration/media.md @@ -0,0 +1,87 @@ +# 媒体 + +## 设置 + +```yaml +######################## +##### 媒体配置 ###### +######################## + +# 关于媒体上传(媒体,图片描述,表情符号)的配置。 + +# 大小。通过 API 上传的媒体最大大小(字节)。 +# +# 增大此限制可能导致其他服务器无法获取贴文中的媒体。 +# +# 示例: [2097152, 10485760, 40MB, 40MiB] +# 默认: 40MiB (41943040 字节) +media-local-max-size: 40MiB + +# 大小。从其他实例下载媒体的最大大小(字节)。 +# +# 降低此限制可能导致你的实例无法获取贴文中的媒体。 +# +# 示例: [2097152, 10485760, 40MB, 40MiB] +# 默认: 40MiB (41943040 字节) +media-remote-max-size: 40MiB + +# 整数。图像或视频描述所需的最小字符数。 +# 示例: [500, 1000, 1500] +# 默认: 0 (无要求) +media-description-min-chars: 0 + +# 整数。图像或视频描述允许的最大字符数。 +# 示例: [1000, 1500, 3000] +# 默认: 1500 +media-description-max-chars: 1500 + +# 大小。通过管理员 API 上传到本站的表情最大大小(字节)。 +# +# 默认值与 Mastodon 表情大小限制相同(50kb),这有助于实现良好的互操作性。提高此限制可能会影响表情在其他实例间的联合,需谨慎。 +# +# 示例: [51200, 102400, 50KB, 50KiB] +# 默认: 50KiB (51200 字节) +media-emoji-local-max-size: 50KiB + +# 大小。从其他实例下载表情的最大大小(字节)。 +# +# 默认值为 100kb,是 media-emoji-local-max-size 默认值的两倍。这在较高表情大小限制的实例间保持良好的互操作性,并且不会占用太多存储空间。 +# +# 示例: [51200, 102400, 100KB, 100KiB] +# 默认: 100KiB (102400 字节) +media-emoji-remote-max-size: 100KiB + +# 整数。添加到媒体处理池中的 ffmpeg+ffprobe 实例数量。 +# +# 增加此数量会加快并发媒体处理速度,但每增加一个实例将消耗约 250MB 的(峰值)内存。 +# +# 如果你有多余的 RAM,并且/或者你为超过 50 人提供服务(他们发布/查看大量媒体),你可以增加这个数值,但单用户实例或在受限(低内存)环境中运行 GoToSocial 时应保持为 1。 +# +# 如果将此数值设为 0 或更少,则会根据 GOMAXPROCS x 1 进行缩放,通常会在主机的每个 CPU 核上生成一个 ffmpeg 实例和一个 ffprobe 实例。 +# +# 示例: [1, 2, -1, 8] +# 默认: 1 +media-ffmpeg-pool-size: 1 + +# 以下媒体清理设置允许管理员自定义什么时候运行媒体清理 + 修剪作业及执行相关作业的频率,默认设置为相对合理(每晚午夜)。有关这些设置的具体操作及一些自定义示例,请参见文档: +# https://docs.gotosocial.org/zh-cn/latest/admin/media_caching#cleanup + +# 整数。从外站实例缓存媒体的天数,到期之后它们将从缓存中移除。当外站媒体从缓存中移除时,它会从存储中删除,但媒体的数据库条目将保留,以便用户请求时可以重新获取。 +# +# 如果设置为 0,外站实例的媒体将无限期缓存。 +# +# 示例: [30, 60, 7, 0] +# 默认: 7 +media-remote-cache-days: 7 + +# 字符串。24 小时格式的时间,格式为 hh:mm。 +# 示例: ["14:30", "00:00", "04:00"] +# 默认: "00:00" (午夜)。 +media-cleanup-from: "00:00" + +# 时长。媒体清理运行之间的间隔。 +# 每 24 小时多次清理不推荐并且可能是不必要的。将此值设置得过低(如每 10 分钟一次)可能会导致队列滞后并可能产生其他问题。 +# 示例: ["24h", "72h", "12h"] +# 默认: "24h"(每天一次)。 +media-cleanup-every: "24h" +``` diff --git a/docs/locales/zh/configuration/observability.md b/docs/locales/zh/configuration/observability.md new file mode 100644 index 000000000..c26b2b351 --- /dev/null +++ b/docs/locales/zh/configuration/observability.md @@ -0,0 +1,53 @@ +# 可观测性 + +这些设置允许你调整和配置某些与可观测性相关的行为。 + +## 指标 + +在启用指标之前,[请阅读指南](../advanced/metrics.md),并确保你已为设置采取适当的安全措施。 + +## 设置 + +```yaml +################################## +##### 可观测性设置 ##### +################################## + +# 字符串。用于提取请求或跟踪ID的请求头名称。通常由负载均衡器或代理设置。 +# 默认值: "X-Request-Id" +request-id-header: "X-Request-Id" + +# 布尔值。启用基于OpenTelemetry的跟踪支持。 +# 默认值: false +tracing-enabled: false + +# 字符串。设置跟踪系统的传输协议。可以是 "grpc" 表示OTLP gRPC,或 "http" 表示OTLP HTTP。 +# 选项: ["grpc", "http"] +# 默认值: "grpc" +tracing-transport: "grpc" + +# 字符串。跟踪收集器的端点。使用gRPC或HTTP传输时,应提供不含协议方案的地址/端口组合。 +# 示例: ["localhost:4317"] +# 默认值: "" +tracing-endpoint: "" + +# 布尔值。禁用gRPC和HTTP传输协议的TLS。 +# 默认值: false +tracing-insecure-transport: false + +# 布尔值。启用基于OpenTelemetry的指标支持。 +# 默认值: false +metrics-enabled: false + +# 布尔值。为Prometheus指标端点启用HTTP基本认证。 +# 默认值: false +metrics-auth-enabled: false + +# 字符串。Prometheus指标端点的用户名。 +# 默认值: "" +metrics-auth-username: "" + +# 字符串。Prometheus指标端点的密码。 +# 默认值: "" +metrics-auth-password: "" +``` diff --git a/docs/locales/zh/configuration/oidc.md b/docs/locales/zh/configuration/oidc.md new file mode 100644 index 000000000..f4c6ef9bf --- /dev/null +++ b/docs/locales/zh/configuration/oidc.md @@ -0,0 +1,156 @@ +# OpenID Connect (OIDC) + +GoToSocial 支持 [OpenID Connect](https://openid.net/connect/),这是一种基于 [OAuth 2.0](https://oauth.net/2/) 构建的身份验证协议,OAuth 2.0 是授权协议的行业标准协议之一。 + +这意味着你可以将 GoToSocial 连接到外部 OIDC 提供商,如 [Gitlab](https://docs.gitlab.com/ee/integration/openid_connect_provider.html)、[Google](https://cloud.google.com/identity-platform/docs/web/oidc)、[Keycloak](https://www.keycloak.org/) 或 [Dex](https://dexidp.io/),并允许用户使用其凭据登录 GoToSocial。 + +在以下情况下,这非常方便: + +- 你在一个平台上运行多个服务(Matrix、Peertube、GoToSocial),并希望用户可以使用相同的登录页面登录所有服务。 +- 你希望将用户、账户、密码等的管理委托给外部服务,以简化管理。 +- 你已经在外部系统中有很多用户,不想在 GoToSocial 中手动重新创建他们。 + +!!! tip + 如果用户尚不存在,且你的 IdP 没有返回非空的 `email` 作为 claims 的一部分,登录将会失败。这个 email 需要在此实例中是唯一的。尽管我们使用 `sub` claim 将外部身份与 GtS 用户关联,但创建用户时需要一个与之关联的 email。 + +## 设置 + +GoToSocial 为 OIDC 提供以下配置设置,以下显示的是其默认值。 + +```yaml +####################### +##### OIDC CONFIG ##### +####################### + +# 配置与外部 OIDC 提供商(如 Dex、Google、Auth0 等)的身份验证。 + +# 布尔值。启用与外部 OIDC 提供商的身份验证。如果设置为 true,则其他 OIDC 选项也必须设置。 +# 如果设置为 false,则使用标准的内部 OAuth 流程,用户使用用户名/密码登录 GtS。 +# 选项: [true, false] +# 默认值: false +oidc-enabled: false + +# 字符串。oidc idp(身份提供商)的名称。这将在用户登录时显示。 +# 示例: ["Google", "Dex", "Auth0"] +# 默认值: "" +oidc-idp-name: "" + +# 布尔值。跳过对从 OIDC 提供商返回的令牌的正常验证流程,即不检查过期或签名。 +# 这应仅用于调试或测试,绝对不要在生产环境中使用,因为这极其不安全! +# 选项: [true, false] +# 默认值: false +oidc-skip-verification: false + +# 字符串。OIDC 提供商 URI。这是 GtS 将用户重定向到的登录地址。 +# 通常这看起来像是一个标准的网页 URL。 +# 示例: ["https://auth.example.org", "https://example.org/auth"] +# 默认值: "" +oidc-issuer: "" + +# 字符串。在 OIDC 提供商处注册的此客户端的 ID。 +# 示例: ["some-client-id", "fda3772a-ad35-41c9-9a59-f1943ad18f54"] +# 默认值: "" +oidc-client-id: "" + +# 字符串。在 OIDC 提供商处注册的此客户端的密钥。 +# 示例: ["super-secret-business", "79379cf5-8057-426d-bb83-af504d98a7b0"] +# 默认值: "" +oidc-client-secret: "" + +# 字符串数组。向 OIDC 提供商请求的范围。返回的值将用于填充在 GtS 中创建的用户。 +# 'openid' 和 'email' 是必需的。 +# 'profile' 用于提取新创建用户的用户名。 +# 'groups' 是可选的,可以用于根据 oidc-admin-groups 确定用户是否为管理员。 +# 示例: 见 eg., https://auth0.com/docs/scopes/openid-connect-scopes +# 默认值: ["openid", "email", "profile", "groups"] +oidc-scopes: + - "openid" + - "email" + - "profile" + - "groups" + +# 布尔值。将通过 OIDC 进行身份验证的用户与现有用户基于其电子邮件地址进行关联。 +# 这主要用于迁移目的,即从使用不稳定 `email` claim 进行唯一用户标识的旧版 GtS 迁移。对于大多数用例,应设置为 false。 +# 选项: [true, false] +# 默认值: false +oidc-link-existing: false + +# 字符串数组。如果返回的 ID 令牌包含与 oidc-allowed-groups 中的某个组匹配的 'groups' claim,则该用户将在 GtS 实例上被授予访问权限。 +# 如果数组为空,则授予所有组权限。 +# 默认值: [] +oidc-allowed-groups: [] + +# 字符串数组。如果返回的 ID 令牌包含与 oidc-admin-groups 中的某个组匹配的 'groups' claim,则该用户将在 GtS 实例上被授予管理员权限。 +# 默认值: [] +oidc-admin-groups: [] +``` + +## 行为 + +在 GoToSocial 上启用 OIDC 后,默认登录页面会自动重定向到 OIDC 提供商的登录页面。 + +这意味着 OIDC 本质上 *替代* 了正常的 GtS 邮箱/密码登录流程。 + +由于 ActivityPub 标准的工作方式,你 _不能_ 在设置用户名后更改它。这与 OIDC 规范冲突,该规范不保证 `preferred_username` 字段是稳定的。 + +为了解决这个问题,我们要求用户在首次登录尝试时提供用户名。此字段预先填入 `preferred_username` claim 的值。 + +在认证后,GtS 存储由 OIDC 提供商提供的 `sub` claim。在后续的身份验证尝试中,这个 claim 被用作唯一的用户查找方式。 + +这使你可以在提供商级别更改用户名而不丢失对 GtS 账户的访问。 + +### 群组成员身份 + +大多数 OIDC 提供商允许在返回的 claims 中包含群组和群组成员身份的概念。GoToSocial 可以使用群组成员身份来确定从 OIDC 流中返回的用户是否应创建为管理员账户。 + +如果返回的 OIDC 群组信息中包含配置在 `oidc-admin-groups` 中的群组成员身份,则该用户将被创建/登录为管理员。 + +## 从旧版本迁移 + +如果你从使用不稳定 `email` claim 进行唯一用户标识的旧版 GtS 迁移过来,可以将 `oidc-link-existing` 配置设置为 `true`。如果无法为提供商返回的 ID 找到用户,则会根据 `email` claim 进行查找。如果成功,稳定 ID 将被添加到匹配的用户数据库中。 + +你应仅在有限时间内使用此功能,以避免恶意账户盗取。 + +## 提供商示例 + +### Dex + +[Dex](https://dexidp.io/) 是一个可以自行托管的开源 OIDC 提供商。安装 Dex 的过程不在本文档范围内,你可以在 [这里](https://dexidp.io/docs/) 查看 Dex 文档。 + +Dex 的优势在于它也用 Go 编写,像 GoToSocial 一样,这意味着它体积小、运行快,在低功耗系统上也能很好地运行。 + +要配置 Dex 和 GoToSocial 一起工作,在 Dex 配置的 `staticClients` 部分创建以下客户端: + +```yaml +staticClients: + - id: 'gotosocial_client' + redirectURIs: + - 'https://gotosocial.example.org/auth/callback' + name: 'GoToSocial' + secret: 'some-client-secret' +``` + +确保将 `gotosocial_client` 替换为你想要的客户端 ID,并将 `secret` 替换为一个合理长且安全的密钥(例如 UUID)。你还应该将 `gotosocial.example.org` 替换为 GtS 实例的 `host`,但保留 `/auth/callback` 不变。 + +然后,编辑 GoToSocial config.yaml 中的 `oidc` 部分如下: + +```yaml +oidc: + enabled: true + idpName: "Dex" + skipVerification: false + issuer: "https://auth.example.org" + clientID: "gotosocial_client" + clientSecret: "some-client-secret" + scopes: + - "openid" + - "email" + - "profile" + - "groups" +``` + +确保将 `issuer` 变量替换为你的 Dex 提供商设置。这应该是你的 Dex 实例的可访问到的确切 URI。 + +现在,重启 GoToSocial 和 Dex,以便新设置生效。 + +当你下次登录 GtS 时,你应该会被重定向到 Dex 的登录页面。登录成功后,你将返回到 GoToSocial。 diff --git a/docs/locales/zh/configuration/smtp.md b/docs/locales/zh/configuration/smtp.md new file mode 100644 index 000000000..0b8062a09 --- /dev/null +++ b/docs/locales/zh/configuration/smtp.md @@ -0,0 +1,78 @@ +# 邮件配置 (smtp) + +GoToSocial 支持通过[简单邮件传输协议](https://wikipedia.org/wiki/Simple_Mail_Transfer_Protocol)(即 **smtp**)向用户发送邮件。 + +配置 GoToSocial 发送邮件不是必需的,但它在发送确认邮件、通知以及处理密码重置请求等方面非常有用。 + +要使 GoToSocial 可以发送邮件,你需要一项支持 smtp 的邮件服务,可以在 GoToSocial 所运行的同一台机器上运行SMTP服务器,也可以使用像 [Mailgun](https://mailgun.com) 这样的外部服务。如果你的邮件提供商支持 smtp(请咨询他们—大多数都支持),也可能使用免费的个人电子邮件地址发送邮件,但如果发送大量邮件,可能会遇到问题。 + +要验证你的配置,你可以使用设置面板中的“管理 -> 操作 -> 邮件”部分来发送测试邮件。 + +## 设置 + +以下是 smtp 的配置选项: + +```yaml +####################### +##### SMTP 配置 ##### +####################### + +# 配置通过 smtp 服务器发送邮件。详情请见 https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol + +# 字符串。你想使用的 smtp 服务器的主机名。 +# 如果未设置,将不使用 smtp 发送邮件,你可以忽略其他设置。 +# 示例: ["mail.example.org", "localhost"] +# 默认值: "" +smtp-host: "" + +# 整数。用于连接 smtp 服务器的端口。 +# 在大多数情况下,你应使用端口 587。 +# 示例: [] +# 默认值: 0 +smtp-port: 0 + +# 字符串。与 smtp 服务器进行身份验证时使用的用户名。 +# 你的 smtp 服务应已提供给你。 +# 这通常是(但不总是)一个电子邮件地址。 +# 示例: ["maillord@example.org"] +# 默认值: "" +smtp-username: "" + +# 字符串。与 smtp 服务器进行身份验证时使用的密码。 +# 你的 smtp 服务应已提供给你。 +# 示例: ["1234", "password"] +# 默认值: "" +smtp-password: "" + +# 字符串。发送邮件的‘发件人’地址。 +# 示例: ["mail@example.org"] +# 默认值: "" +smtp-from: "" + +# 布尔值。如果为 true,当一封邮件有多个收件人时,每个收件人都将包含在收件人字段中,以便每个收件人可以看到其他谁收到了邮件,并且如果他们愿意,可以“回复所有人”。 +# +# 如果为 false,邮件将发送给未公开的收件人,并且每个收件人将看不到其他收件人。 +# +# 如果你希望通过“回复所有人”来讨论新的举报,可能需要将此设置更改为 'true'。 +# 默认值: false +smtp-disclose-recipients: false +``` + +请注意,如果你没有设置 `Host`,则 smtp 发送邮件将被禁用,其他设置将被忽略。GoToSocial 仍会记录(跟踪级别)如果启用 smtp 本可以发送的邮件。 + +## 什么时候发送电子邮件? + +目前,电子邮件在以下场景中发送: + +- 向新用户通过注册页面或 API 创建新账户时,向其提交的电子邮件地址发送确认请求。 +- 当新账户通过上述渠道创建账户时,向实例管理员发送通知。 +- 当收到新的举报时,向所有活跃的实例管理员+站务发送通知。默认情况下,收件地址将位于密送列表中,但你可以通过设置 `smtp-disclose-recipients` 更改此行为。 +- 当举报被管理员关闭时,向举报创建者(若为本站用户)发送邮件。 + +## HTML 与纯文本 + +默认情况下,邮件以纯文本形式发送。目前,没有选项可以发送 html 格式的邮件,但如果有足够的需求,这可能会在以后添加。 + +## 自定义 + +如果你愿意,可以自定义用于生成邮件的模板。请按照 `web/templates` 中的示例进行操作。 diff --git a/docs/locales/zh/configuration/statuses.md b/docs/locales/zh/configuration/statuses.md new file mode 100644 index 000000000..c16abcc04 --- /dev/null +++ b/docs/locales/zh/configuration/statuses.md @@ -0,0 +1,38 @@ +# 贴文 + +## 设置 + +```yaml +########################### +##### 贴文配置 ##### +########################### + +# 有关创建贴文的配置和允许的限制。 + +# 整数值。新贴文允许的最大字符数, +# 包括内容警告(如果设置)。 +# +# 请注意,大幅高于默认值可能会影响联合。 +# +# 示例: [140, 500, 5000] +# 默认: 5000 +statuses-max-chars: 5000 + +# 整数值。创建新投票时允许的最大选项数量。 +# 请注意,大幅高于默认值可能会影响联合。 +# 示例: [4, 6, 10] +# 默认: 6 +statuses-poll-max-options: 6 + +# 整数值。创建新投票时每个选项允许的最大字符数。 +# 请注意,大幅高于默认值可能会影响联合。 +# 示例: [50, 100, 150] +# 默认: 50 +statuses-poll-option-max-chars: 50 + +# 整数值。新贴文可以附加的最大媒体文件数量。 +# 请注意,大幅高于默认值可能会影响联合。 +# 示例: [4, 6, 10] +# 默认: 6 +statuses-media-max-files: 6 +``` diff --git a/docs/locales/zh/configuration/storage.md b/docs/locales/zh/configuration/storage.md new file mode 100644 index 000000000..24bb40d45 --- /dev/null +++ b/docs/locales/zh/configuration/storage.md @@ -0,0 +1,189 @@ +# 存储 + +## 设置 + +```yaml +########################## +##### 存储配置指南 ##### +########################## + +# 用户创建上传内容(如视频、图片等)的存储配置。 + +# 字符串。要使用的存储后端类型。 +# 示例: ["local", "s3"] +# 默认: "local"(存储在本地磁盘上) +storage-backend: "local" + +# 字符串。用于存储文件的根目录。 +# 确保运行 GoToSocial 的用户/组有权限访问此目录,并能在其中创建新的子目录和文件。 +# 仅在使用本地存储后端时需要。 +# 示例: ["/home/gotosocial/storage", "/opt/gotosocial/datastorage"] +# 默认: "/gotosocial/storage" +storage-local-base-path: "/gotosocial/storage" + +# 字符串。S3 兼容服务的 API 端点。 +# 仅在使用 s3 存储后端时需要。 +# 示例: ["minio:9000", "s3.nl-ams.scw.cloud", "s3.us-west-002.backblazeb2.com"] +# GoToSocial 使用“DNS 风格”访问桶。 +# 如果你使用 Scaleways 对象存储,请移除端点地址中的“桶名称” +# 默认: "" +storage-s3-endpoint: "" + +# 布尔值。如果 S3 中存储的数据应通过 GoToSocial 代理而不是转发到预签名 URL,请将其设置为 true。 +# +# 在大多数情况下,你无需更改此设置,但如果你的桶提供商无法生成预签名 URL, +# 或者你的桶无法暴露给更广泛的互联网,这可能有用。 +# +# 默认: false +storage-s3-proxy: false + +# 字符串。用于重定向传入媒体请求的基本 URL。 +# +# 必须以“http://”或“https://”开头,并以无尾斜杠结尾。 +# +# 除非有正当理由,否则不要设置此值!对“常规”s3 使用没有必要,大多数管理员可以忽略此设置。 +# +# 如果设置,那么对实例的媒体文件服务器请求将被重定向到此 URL 而不是你的桶 URL,保留相关路径部分。 +# +# 如果你在 S3 桶前使用 CDN 代理,并希望通过 CDN 提供媒体,而不是直接从 S3 桶提供媒体,这会很有用。 +# +# 例如,如果你的 storage-s3-endpoint 值设置为“s3.my-storage.example.org”, +# 并且你设置了一个 CDN 以代理你的桶,从“cdn.some-fancy-host.org”提供服务, +# 那么你应该将 storage-s3-redirect-url 设置为“https://cdn.some-fancy-host.org”。 +# +# 这将允许你的 GoToSocial 实例*上传*数据到“s3.my-storage.example.org”, +# 但引导调用者从“https://cdn.some-fancy-host.org” *下载* 这些数据。 +# +# 如果 storage-backend 不是 s3,或者 storage-s3-proxy 为 true,则忽略此值。 +# +# 示例: ["https://cdn.some-fancy-host.org"] +# 默认: "" +storage-s3-redirect-url: "" + +# 布尔值。使用 SSL 进行 S3 连接。 +# +# 仅在本地测试时将此设置为 'false'。 +# +# 默认: true +storage-s3-use-ssl: true + +# 字符串。S3 凭证的访问密钥部分。 +# 请考虑使用环境变量设置此值,以避免通过配置文件泄露 +# 仅在使用 s3 存储后端时需要。 +# 示例: ["AKIAJSIE27KKMHXI3BJQ","miniouser"] +# 默认: "" +storage-s3-access-key: "" + +# 字符串。S3 凭证的秘密密钥部分。 +# 请考虑使用环境变量设置此值,以避免通过配置文件泄露 +# 仅在使用 s3 存储后端时需要。 +# 示例: ["5bEYu26084qjSFyclM/f2pz4gviSfoOg+mFwBH39","miniopassword"] +# 默认: "" +storage-s3-secret-key: "" + +# 字符串。存储桶的名称。 +# +# 如果你已经在 storage-s3-endpoint 中包含了你的桶名称, +# 此值将用作包含你数据的目录。 +# +# 存储桶必须在启动 GoToSocial 之前就已存在 +# +# 仅在使用 s3 存储后端时需要。 +# 示例: ["gts","cool-instance"] +# 默认: "" +storage-s3-bucket: "" +``` + +## AWS S3 配置 + +### 创建一个桶 + +GoToSocial 默认创建签名 URL,这意味着我们不需要在桶的策略上做重大更改。 + +1. 登录 AWS -> 选择 S3 作为服务。 +2. 点击创建桶 +3. 提供一个唯一名称,避免在名称中添加`.` +4. 不要更改公开访问设置(保持“屏蔽公开访问”模式) + +### IAM 配置 + +1. 创建一个具有程序化 API 访问权限的[新用户](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html) +2. 在此用户上添加在线政策,将 `` 替换为你的桶名称 + ```json + { + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:ListAllMyBuckets", + "Resource": "arn:aws:s3:::*" + }, + { + "Effect": "Allow", + "Action": "s3:*", + "Resource": [ + "arn:aws:s3:::", + "arn:aws:s3:::/*" + ] + } + ] + } + ``` +3. 为此用户创建一个[访问密钥](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) +4. 在上方配置中提供值 + * `storage-s3-endpoint` -> 你所在区域的 S3 API 端点,例如: `s3.ap-southeast-1.amazonaws.com` + * `storage-s3-access-key` -> 你为上面创建的用户获取的访问密钥 ID + * `storage-s3-secret-key` -> 你为上面创建的用户获取的秘密密钥 + * `storage-s3-bucket` -> 你刚刚创建的 `` + +### `storage-s3-redirect-url` + +如果你在 S3 桶前使用 CDN,并希望通过 CDN 提供媒体,而不是直接从 S3 桶提供媒体,你应将 `storage-s3-redirect-url` 设置为 CDN URL。 + +例如,如果你的 `storage-s3-endpoint` 值设置为 `s3.my-storage.example.org`,并且你设置了一个 CDN 来代理你的桶,从 `cdn.some-fancy-host.org` 提供服务,那么你应该将 `storage-s3-redirect-url` 设置为 `https://cdn.some-fancy-host.org`。 + +这将允许你的 GoToSocial 实例*上传*数据到 `s3.my-storage.example.org`,但引导调用者从 `https://cdn.some-fancy-host.org` *下载* 那些数据。 + +## 存储迁移 + +可以自由地在后端之间迁移。要做到这一点,你只需在不同的实现之间移动目录(及其内容)。 + +从一个后端迁移到另一个后端时,数据库中的外站账户的头像和资料卡横幅背景引用仍然指向旧存储后端,这可能导致它们在客户端中无法正确加载。这将在一段时间后自行解决,但你可以在下一次与外站账户交互时强制 GoToSocial 重新获取头像和封面。当 GoToSocial 不运行时,你可以在数据库上执行以下指令(执行后重启实例也可)。这将确保缓存被清除。 + +```sql +UPDATE accounts SET (avatar_media_attachment_id, avatar_remote_url, header_media_attachment_id, header_remote_url, fetched_at) = (null, null, null, null, null) WHERE domain IS NOT null; +``` + +### 从本地到 AWS S3 + +有多种工具可帮助你将数据从文件系统复制到 AWS S3 桶。 + +#### AWS CLI + +使用官方 [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide) + +```sh +aws s3 sync s3:// +``` + +#### s3cmd + +使用 [s3cmd](https://github.com/s3tools/s3cmd),可以使用以下命令: + +```sh +s3cmd sync --add-header="Cache-Control:public, max-age=315576000, immutable" s3:// +``` + +### 从本地到 S3 兼容存储 + +这适用于任何 S3 兼容的存储,包括 AWS S3 本身。 + +#### Minio CLI + +你可以使用 [MinIO 客户端](https://docs.min.io/docs/minio-client-complete-guide.html)。要执行迁移,你需要使用客户端注册你的 S3 兼容后端,然后让它复制文件: + +```sh +mc alias set scw https://s3.nl-ams.scw.cloud +mc mirror scw/example-bucket/ +``` + +如果你想迁移回来,请交换 `mc mirror` 命令的参数顺序。 diff --git a/docs/locales/zh/configuration/syslog.md b/docs/locales/zh/configuration/syslog.md new file mode 100644 index 000000000..4e0ef75ae --- /dev/null +++ b/docs/locales/zh/configuration/syslog.md @@ -0,0 +1,40 @@ +# Syslog + +GoToSocial 可以将日志镜像到 [syslog](https://en.wikipedia.org/wiki/Syslog),支持通过 udp/tcp 协议传输日志,或者直接连接到本地 syslog(例如,`/var/log/syslog`)。 + +如果你希望通过守护进程管理 GtS 并不想自行处理日志轮转等工作,使用此功能是非常有用的,因为它依赖于经过验证的实现。 + +在 syslog 中的日志看起来会像这样: + +```text +Dec 12 17:44:03 dilettante ./gotosocial[246860]: time=2021-12-12T17:44:03+01:00 level=info msg=connected to SQLITE database +Dec 12 17:44:03 dilettante ./gotosocial[246860]: time=2021-12-12T17:44:03+01:00 level=info msg=there are no new migrations to run func=doMigration +``` + +## 设置 + +```yaml +######################### +##### SYSLOG CONFIG ##### +######################### + +# 额外的 syslog 日志钩子的配置。请参阅 https://en.wikipedia.org/wiki/Syslog, +# 和 https://github.com/sirupsen/logrus/tree/master/hooks/syslog。 +# +# 当需要通过守护进程管理 GtS 并将日志发送到特定位置时(无论是发送到本地位置还是 syslog 服务器),这些设置都很有用。 +# 大多数用户不需要修改这些设置。 + +# 布尔值。启用 syslog 日志钩子。日志将被镜像到配置的目标。 +# 选项: [true, false] +# 默认值: false +syslog-enabled: false + +# 字符串。指定将日志发送到 syslog 时使用的协议。留空以连接到本地 syslog。 +# 选项: ["udp", "tcp", ""] +# 默认值: "udp" +syslog-protocol: "udp" + +# 字符串。发送 syslog 日志的目标地址和端口。留空以连接到本地 syslog。 +# 默认值: "localhost:514" +syslog-address: "localhost:514" +``` diff --git a/docs/locales/zh/configuration/tls.md b/docs/locales/zh/configuration/tls.md new file mode 100644 index 000000000..5d92c9c5d --- /dev/null +++ b/docs/locales/zh/configuration/tls.md @@ -0,0 +1,64 @@ +# TLS + +你可以通过以下两种方式配置 TLS 支持: +* 内置支持 Lets Encrypt / ACME 兼容供应商 +* 从磁盘加载 TLS 文件 + +不能同时启用这两种方法。 + +注意,当使用从磁盘加载的 TLS 文件时,你需要在文件更改时重新启动实例。文件不会自动重新加载。 + +## 设置 + +```yaml +############################## +##### LETSENCRYPT 配置 ##### +############################## + +# 与自动获取和使用 LetsEncrypt HTTPS 证书相关的配置。 + +# 布尔值。是否为服务器启用 letsencrypt。 +# 如果为 false,这里的其余设置将被忽略。 +# 如果你的 GoToSocial 服务部署在 nginx 或 traefik 这样的反向代理后,请保持关闭状态。 +# 如果没有,请开启以便可以使用 https。 +# 选项:[true, false] +# 默认值:false +letsencrypt-enabled: false + +# 整数。监听 letsencrypt 证书挑战的端口。 +# 如果启用了 letsencrypt,则该端口必须可达,否则你将无法获取证书。 +# 如果没有启用 letsencrypt,则该端口将不会使用。 +# 这 *不能* 与上面指定的 webserver/API 端口相同。 +# 例子:[80, 8000, 1312] +# 默认值:80 +letsencrypt-port: 80 + +# 字符串。存储 LetsEncrypt 证书的目录。 +# 最好将其设置为存储目录中的子路径,以便于备份, +# 但如果其他服务也需要访问这些证书,你可能希望将它们移到别的地方。 +# 无论如何,请确保 GoToSocial 有权限写入/读取此目录。 +# 例子:["/home/gotosocial/storage/certs", "/acmecerts"] +# 默认值:"/gotosocial/storage/certs" +letsencrypt-cert-dir: "/gotosocial/storage/certs" + +# 字符串。注册 LetsEncrypt 证书时使用的电子邮件地址。 +# 此电子邮件地址很可能是实例管理员的地址。 +# LetsEncrypt 将发送关于证书到期等的通知到此地址。 +# 例子:["admin@example.org"] +# 默认值:"" +letsencrypt-email-address: "" + +############################## +##### 手动 TLS 配置 ##### +############################## + +# 字符串。磁盘上 PEM 编码文件的路径,包含证书链和公钥。 +# 例子:["/gotosocial/storage/certs/chain.pem"] +# 默认值:"" +tls-certificate-chain: "" + +# 字符串。磁盘上 PEM 编码文件的路径,包含与 tls-certificate-chain 相关的私钥。 +# 例子:["/gotosocial/storage/certs/private.pem"] +# 默认值:"" +tls-certificate-key: "" +``` diff --git a/docs/locales/zh/configuration/web.md b/docs/locales/zh/configuration/web.md new file mode 100644 index 000000000..553e511bb --- /dev/null +++ b/docs/locales/zh/configuration/web.md @@ -0,0 +1,21 @@ +# Web + +## 设置 + +```yaml +###################### +##### WEB CONFIG ##### +###################### + +# 与网页模板和发送邮件通知等相关的配置 + +# 字符串。GoToSocial 尝试加载 HTML 模板 (.tmpl 文件) 的目录。 +# 示例: ["/some/absolute/path/", "./relative/path/", "../../some/weird/path/"] +# 默认值: "./web/template/" +web-template-base-dir: "./web/template/" + +# 字符串。GoToSocial 试图提供静态网页资源(图片,脚本)的目录。 +# 示例: ["/some/absolute/path/", "./relative/path/", "../../some/weird/path/"] +# 默认值: "./web/assets/" +web-asset-base-dir: "./web/assets/" +``` diff --git a/docs/locales/zh/faq.md b/docs/locales/zh/faq.md new file mode 100644 index 000000000..8383d7462 --- /dev/null +++ b/docs/locales/zh/faq.md @@ -0,0 +1,41 @@ +# 常见问题 + +## 用户界面在哪? + +GoToSocial 的大部分内容只是一个裸服务端,用于通过第三方应用程序进行使用。可通过 [Semaphore](https://semaphore.social/) 在浏览器使用,可通过 [Tusky](https://tusky.app/) 在 Android 使用,可通过 [Feditext](https://github.com/feditext/feditext) 在 iOS、iPadOS 和 macOS 使用。这些应用程序兼容性最好。任何由 Mastodon API 提供的实例功能都应该可以工作,除非它们是 GoToSocial 尚不具备的功能。永久链接和个账户页是通过 GoToSocial 直接提供的,设置面板也是如此,但大多数交互都是通过应用程序完成的。 + +## 为什么我的贴文没有显示在我的账户页面上? + +与 Mastodon 不同,GoToSocial 的默认贴文可见性是“不列出”。如果你希望某个内容在个人资料页面上可见,贴文必须设为公开可见。 + +## 为什么我的贴文没有在其他实例上显示? + +首先检查上面提到的可见性。TODO: 解释如何调试常见的联合问题 + +## 为什么我频繁收到 HTTP 429 错误响应? + +GoToSocial 默认配置了基于 IP 的[限流规则](./api/ratelimiting.md),但在某些情况下无法准确识别外部 IP,会将所有连接视为来自相同位置。在这些情况下,需要禁用或重新配置限流。 + +## 为什么我频繁收到 HTTP 503 错误响应? + +当你的实例负载过重且请求被限制时,会返回代码 503。可以根据需要调整此行为,或完全关闭,请参见[此处](./api/throttling.md)。 + +## 我总是收到 400 Bad Request 错误,且已经按照错误信息中的建议操作。该怎么办? + +请确认 `host` 配置与 GoToSocial 所服务的域名(用户用来访问服务器的域名)匹配。 + +## 我在服务器日志中不断看到 'dial within blocked / reserved IP range',无法从我的实例连接到一些实例,我该怎么办? + +外站实例的 IP 地址可能位于 GoToSocial 出于安全原因而硬编码屏蔽的“特殊用途”IP 范围内。如果需要,你可以在配置文件中覆盖此设置。查看[HTTP 客户端文档](./configuration/httpclient.md),并仔细阅读其中的警告!如果添加了明确的 IP 允许条目,需要重启 GoToSocial 实例以使配置生效。 + +## 我已部署实例并登录到客户端,但时间线为空,这是怎么回事? + +要查看贴文,你需要开始关注其他人!一旦你关注了几个人,并且他们发布或转发了内容,你就会在时间线上看到这些内容。目前,GoToSocial 没有“回填”贴文的方法,即尚不能从其他实例获取之前的贴文,所以你只能看到你关注的人的新贴文。如果你想与他们的旧贴文互动,可以从他们的账户页中复制贴文链接,并将其粘贴到客户端的搜索栏中。 + +## 如何在某个实例上注册? + +我们在 v0.16.0 中引入了注册流程。你想注册的实例必须手动启用注册功能,具体详见[此处](./admin/signups.md)。 + +## 为什么还在 Beta 阶段? + +查看[当前 bug 列表](https://github.com/superseriousbusiness/gotosocial/issues?q=is%3Aissue+is%3Aopen+label%3Abug)和[路线图](https://github.com/superseriousbusiness/gotosocial/blob/main/docs/locales/zh/repo/ROADMAP.md)以获取更详细的信息。 diff --git a/docs/locales/zh/federation/access_control.md b/docs/locales/zh/federation/access_control.md new file mode 100644 index 000000000..6d5439b30 --- /dev/null +++ b/docs/locales/zh/federation/access_control.md @@ -0,0 +1,11 @@ +# 访问控制 + +GoToSocial 使用访问控制限制来保护用户和资源免受外站账户和实例的不必要互动。 + +如[HTTP 签名](#http-signatures)部分所示,GoToSocial 要求所有来自外站服务器的传入 `GET` 和 `POST` 请求必须签名。未签名的请求将被拒绝,并返回 http 代码 `401 Unauthorized`。 + +访问控制限制通过检查签名的 `keyId` (即谁拥有发出请求的公钥/私钥对)来实现。 + +首先,会将 `keyId` URI 的主机值与 GoToSocial 实例的已屏蔽(取消联合)的域列表进行检查。如果域名被检测到位于屏蔽列表,则 http 请求将立即以 http 代码 `403 Forbidden` 中止。 + +接下来,GoToSocial 将检查发出 http 请求的公钥所有者与请求目标资源的所有者之间是否存在(任一方向的)屏蔽。如果 GoToSocial 用户屏蔽了发出请求的外站账户,则请求将以 http 代码 `403 Forbidden` 中止。 diff --git a/docs/locales/zh/federation/actors.md b/docs/locales/zh/federation/actors.md new file mode 100644 index 000000000..b801a262f --- /dev/null +++ b/docs/locales/zh/federation/actors.md @@ -0,0 +1,368 @@ +# 行为体与行为体属性 + +## 收件箱 + +GoToSocial 按照 [ActivityPub 规范](https://www.w3.org/TR/activitypub/#inbox),为行为体实现了收件箱。 + +如规范所述,[外站](https://www.w3.org/TR/activitypub/#delivery) 应通过向活动目标受众的每个收件箱发送 HTTP POST 请求,将活动传送到 GoToSocial 服务器。 + +GoToSocial 帐号目前没有实现 [共享收件箱](https://www.w3.org/TR/activitypub/#shared-inbox-delivery) 端点,但这可能会有所改变。当 GoToSocial 服务器上有多个行为体是活动受众时,对已传送活动的去重由 GoToSocial 处理。 + +对 GoToSocial 行为体收件箱的 POST 请求必须由发起活动的行为体进行正确地 [HTTP 签名](#http-signatures)。 + +可被接受的收件箱 POST `Content-Type` 头为: + +- `application/activity+json` +- `application/activity+json; charset=utf-8` +- `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` + +未使用上述 `Content-Type` 头之一的收件箱 POST 请求将被拒绝,并返回 HTTP 状态码 [406 - Not Acceptable](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406)。 + +有关可接受内容类型的更多信息,请参阅 ActivityPub 协议的 [服务器间交互](https://www.w3.org/TR/activitypub/#server-to-server-interactions) 部分。 + +对格式正确且已签名的收件箱 POST 请求,GoToSocial 将返回 HTTP 状态码 [202 - Accepted](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/202)。 + +对格式错误的收件箱 POST 请求,将返回 HTTP 状态码 [400 - Bad Request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400)。响应正文可能包含有关 GoToSocial 服务器为何认为请求内容格式错误的更多信息。对于代码 `400` 的回应,其他服务器不应重试交付活动。 + +即使 GoToSocial 返回 `202` 状态码,也可能不继续处理已传送的活动,具体取决于活动的发起者、目标和活动类型。ActivityPub 是一个广泛的协议,GoToSocial 并未涵盖每种活动和对象的组合。 + +## 发件箱 + +GoToSocial 按照 [ActivityPub 规范](https://www.w3.org/TR/activitypub/#outbox),为行为体(即实例账户)实现了发件箱。 + +要获取某行为体最近发布的活动 [OrderedCollection](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection),外站可以对用户的发件箱进行 `GET` 请求。其地址类似于 `https://example.org/users/whatever/outbox`。 + +服务器将返回以下结构的 OrderedCollection: + +```json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.org/users/whatever/outbox", + "type": "OrderedCollection", + "first": "https://example.org/users/whatever/outbox?page=true" +} +``` + +请注意,`OrderedCollection` 本身不包含项目。调用者必须解引用 `first` 页面以开始获取项目。例如,对 `https://example.org/users/whatever/outbox?page=true` 的 `GET` 请求将生成如下内容: + +```json +{ + "id": "https://example.org/users/whatever/outbox?page=true", + "type": "OrderedCollectionPage", + "next": "https://example.org/users/whatever/outbox?max_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true", + "prev": "https://example.org/users/whatever/outbox?min_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true", + "partOf": "https://example.org/users/whatever/outbox", + "orderedItems": [ + { + "id": "https://example.org/users/whatever/statuses/01FJC1MKPVX2VMWP2ST93Q90K7/activity", + "type": "Create", + "actor": "https://example.org/users/whatever", + "published": "2021-10-18T20:06:18Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://example.org/users/whatever/followers" + ], + "object": "https://example.org/users/whatever/statuses/01FJC1MKPVX2VMWP2ST93Q90K7" + } + ] +} +``` + +`orderedItems` 数组最多包含 30 个条目。要获取超过此数量的条目,调用者可以使用响应中提供的 `next` 链接。 + +请注意,在返回的 `orderedItems` 中,所有活动类型都将是 `Create`。在每个活动中,`object` 字段将是由拥有发件箱的行为体创建的原始公共贴文的 AP URI(即 `Note`,`to` 字段中包含 `https://www.w3.org/ns/activitystreams#Public`,且不是对另一个贴文的回复)。调用者可以使用返回的 AP URI 来解引用这些 `Note` 的内容。 + +## 粉丝与关注集合 + +GoToSocial 将粉丝和关注的集合实现为 `OrderedCollection`。例如,对行为体的关注集合进行正确签名的 `GET` 请求将返回如下内容: + +```json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "first": "https://example.org/users/someone/following?limit=40", + "id": "https://example.org/users/someone/following", + "totalItems": 397, + "type": "OrderedCollection" +} +``` + +从这里开始,你可以使用 `first` 页面开始获取项目。例如,对 `https://example.org/users/someone/following?limit=40` 的 `GET` 请求将产生如下内容: + +```json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.org/users/someone/following?limit=40", + "next": "https://example.org/users/someone/following?limit=40&max_id=01V1AY4ZJT4JK1NT271SH2WMGH", + "orderedItems": [ + "https://example.org/users/someone_else", + "https://somewhere.else.example.org/users/another_account", + [... 38 more entries here ...] + ], + "partOf": "https://example.org/users/someone/following", + "prev": "https://example.org/users/someone/following?limit=40&since_id=021HKBY346X7BPFYANPPJN493P", + "totalItems": 397, + "type": "OrderedCollectionPage" +} +``` + +然后,你可以使用 `next` 和 `prev` 端点在 OrderedCollection 中向下和向上翻页。 + +## 个人资料字段 + +与 Mastodon 和其他联邦宇宙软件类似,GoToSocial 允许用户在其个人资料上设置键/值对;这对于传达简短的信息如链接、代词、年龄等很有用。 + +为了与其他实现兼容,GoToSocial 使用与 Mastodon 相同的 schema.org PropertyValue 扩展,作为设置字段的行为体上的 `attachment` 数组值。例如,以下 JSON 显示了两个 PropertyValue 字段的账户: + +```json +{ + "@context": [ + "http://joinmastodon.org/ns", + "https://w3id.org/security/v1", + "https://www.w3.org/ns/activitystreams", + "http://schema.org" + ], + "attachment": [ + { + "name": "接受关注", + "type": "PropertyValue", + "value": "纯看个人心情" + }, + { + "name": "年龄", + "type": "PropertyValue", + "value": "120" + } + ], + "discoverable": false, + "featured": "http://example.org/users/flyingsloth/collections/featured", + "followers": "http://example.org/users/flyingsloth/followers", + "following": "http://example.org/users/flyingsloth/following", + "id": "http://example.org/users/flyingsloth", + "inbox": "http://example.org/users/flyingsloth/inbox", + "manuallyApprovesFollowers": true, + "name": "飞翔的树懒 :3", + "outbox": "http://example.org/users/flyingsloth/outbox", + "preferredUsername": "flyingsloth", + "publicKey": { + "id": "http://example.org/users/flyingsloth#main-key", + "owner": "http://example.org/users/flyingsloth", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtTc6Jpg6LrRPhVQG4KLz\n2+YqEUUtZPd4YR+TKXuCnwEG9ZNGhgP046xa9h3EWzrZXaOhXvkUQgJuRqPrAcfN\nvc8jBHV2xrUeD8pu/MWKEabAsA/tgCv3nUC47HQ3/c12aHfYoPz3ufWsGGnrkhci\nv8PaveJ3LohO5vjCn1yZ00v6osMJMViEZvZQaazyE9A8FwraIexXabDpoy7tkHRg\nA1fvSkg4FeSG1XMcIz2NN7xyUuFACD+XkuOk7UqzRd4cjPUPLxiDwIsTlcgGOd3E\nUFMWVlPxSGjY2hIKa3lEHytaYK9IMYdSuyCsJshd3/yYC9LqxZY2KdlKJ80VOVyh\nyQIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "summary": "\u003cp\u003e只是一只普通树懒\u003c/p\u003e", + "tag": [], + "type": "Person", + "url": "http://example.org/@flyingsloth" +} +``` + +对于没有 `PropertyValue` 字段的行为体,`attachment` 属性将不设置。即,`attachment` 键值不会在行为体中出现(即使是空数组或 null 值也不会)。 + +尽管 `attachment` 在规范上不是一个有序集合,GoToSocial(还是为了与其他实现保持一致)仍会按应显示的顺序呈现 `attachment` 的 `PropertyValue` 字段。 + +GoToSocial 还将解析 GoToSocial 实例发现的外站行为体的 PropertyValue 字段,以便可以把它们展示给 GoToSocial 实例上的用户。 + +GoToSocial 默认允许设置最多 6 个 `PropertyValue` 字段,而 Mastodon 的默认值为 4 个。 + +## 置顶/特色贴文 + +GoToSocial 允许用户在他们的个人资料上置顶贴文。 + +在 ActivityPub 术语中,GoToSocial 在行为体的 [featured](https://docs.joinmastodon.org/spec/activitypub/#featured) 字段中指定的端点提供这些置顶贴文,格式为 [OrderedCollection](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection) 。该字段的值将被设置为类似 `https://example.org/users/some_user/collections/featured`。 + +通过向此端点发送经过签名的 GET 请求,外站实例可以解引用特色贴文集合,这将返回带有 `orderedItems` 字段,其中包含贴文 URI 列表的 `OrderedCollection`。 + +置顶多条 `Note` 的用户的 featured collection 示例: + +```json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.org/users/some_user/collections/featured", + "orderedItems": [ + "https://example.org/users/some_user/statuses/01GS7VTYH0S77NNXTP6W4G9EAG", + "https://example.org/users/some_user/statuses/01GSFY2SZK9TPCJFQ1WCCPGDRT", + "https://example.org/users/some_user/statuses/01GSCXY70MZCBFMH5EKJW9ENC8" + ], + "totalItems": 3, + "type": "OrderedCollection" +} +``` + +置顶单条 `Note` 的用户的 featured collection 示例: + +```json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.org/users/some_user/collections/featured", + "orderedItems": [ + "https://example.org/users/some_user/statuses/01GS7VTYH0S77NNXTP6W4G9EAG" + ], + "totalItems": 1, + "type": "OrderedCollection" +} +``` + +没有置顶 `Note` 的示例: + +```json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.org/users/some_user/collections/featured", + "orderedItems": [], + "totalItems": 0, + "type": "OrderedCollection" +} +``` + +与 Mastodon 和一些其他实现不同,GoToSocial 不会将在 `orderedItems` 的值中提供完整的 `Note` 表示。相反,它仅提供每个 `Note` 的 URI,外站服务器可以自行进一步解引用(如果它们已经在本地缓存了该 `Note` 则可以不执行此操作)。 + +作为集合一部分提供的一些 URI 可能指向仅限粉丝可见性的贴文,请求的 `Actor` 不一定有权限查看。外站服务器应确保进行适当的过滤(与其他任何类型的贴文一样),以确保这些贴文仅显示给有权查看的用户。 + +GoToSocial 和其他服务器实现之间的另一个区别是,当用户置顶或取消置顶贴文时,GoToSocial 不会向外站服务器发送更新。Mastodon 会通过发送 [Add](https://www.w3.org/TR/activitypub/#add-activity-inbox) 和 [Remove](https://www.w3.org/TR/activitypub/#remove-activity-inbox) 活动类型来进行,`object` 是被置顶或取消置顶的贴文,`target` 是发送 `Actor` 的 `featured` 集合。尽管在概念上这很合理,但这与 ActivityPub 协议建议不一致,因为活动的 `target`“不属于接收服务器,因此他们不能更新它”。 + +相反,建议外站仅定期轮询 GoToSocial 行为体的 `featured` 集合,并根据需要在其缓存表示中添加/删除贴文,以构建和更新 GoToSocial 用户置顶贴文的视图。 + +## 行为体迁移 / 别名 + +GoToSocial 支持通过 `Move` 活动以及行为体对象属性 `alsoKnownAs` 和 `movedTo` 从一个实例/服务器迁移到另一个。 + +### `alsoKnownAs` + +GoToSocial 支持使用 `alsoKnownAs` 行为体属性进行帐户别名,这是一个 [公认的 ActivityPub 扩展](https://www.w3.org/wiki/Activity_Streams_extensions#as:alsoKnownAs_property)。 + +#### 传入 + +在传入的 AP 消息中,GoToSocial 将查找行为体上的 `alsoKnownAs` 属性,该属性是行为体也可以通过的其他活动 IDs/URIs 构成的数组。 + +例如: + +```json +{ + "@context": [ + "http://joinmastodon.org/ns", + "https://w3id.org/security/v1", + "https://www.w3.org/ns/activitystreams", + "http://schema.org" + ], + "featured": "http://example.org/users/flyingsloth/collections/featured", + "followers": "http://example.org/users/flyingsloth/followers", + "following": "http://example.org/users/flyingsloth/following", + "id": "http://example.org/users/flyingsloth", + "inbox": "http://example.org/users/flyingsloth/inbox", + "manuallyApprovesFollowers": true, + "name": "飞翔的树懒 :3", + "outbox": "http://example.org/users/flyingsloth/outbox", + "preferredUsername": "flyingsloth", + "publicKey": {...}, + "summary": "\u003cp\u003e只是一只普通树懒\u003c/p\u003e", + "type": "Person", + "url": "http://example.org/@flyingsloth", + "alsoKnownAs": [ + "https://another-server.com/users/flyingsloth", + "https://somewhere-else.org/users/originalsloth" + ] +} +``` + +在上述 AP JSON 中,行为体 `http://example.org/users/flyingsloth` 已设置别名为其他行为体 `https://another-server.com/users/flyingsloth` 和 `https://somewhere-else.org/users/originalsloth`。 + +GoToSocial 将传入的 `alsoKnownAs` URI 存储在数据库中,但(当前)不会使用它们,除非用于验证 `Move` 活动(见下文)。 + +#### 传出 + +GoToSocial 用户可以通过 GoToSocial 客户端 API 在其账户上设置多个 `alsoKnownAs` URI。GoToSocial 会在存入数据库并在传出 AP 消息序列化之前验证这些 `alsoKnownAs` 别名是否为有效的行为体 URI。 + +然而,GoToSocial 并不验证用户在设置别名之前对那些 `alsoKnownAs` URI 的*所有权*;它期望外站自行进行验证,然后再采信任何传入的 `alsoKnownAs` 值。 + +例如,GoToSocial 实例中的用户 `http://example.org/users/flyingsloth` 可能会在他们的账户上设置 `alsoKnownAs: [ "https://unrelated-server.com/users/someone_else" ]`,GoToSocial 会如实传输此别名到其他服务器。 + +在这种情况下,`https://unrelated-server.com/users/someone_else` 或许不是 `flyingsloth`。`flyingsloth` 可能无意或恶意地设置了此别名。为了正确验证 `someone_else` 的所有权,外站应检查行为体 `https://unrelated-server.com/users/someone_else` 的 `alsoKnownAs` 属性是否包含 `http://example.org/users/flyingsloth` 条目。 + +换句话说,外站不应默认信任 `alsoKnownAs` 别名,而应确保在将别名视为有效之前,行为体之间存在**双向别名**。 + +### `movedTo` + +GoToSocial 使用 `movedTo` 属性标记账户已迁移。与 `alsoKnownAs` 不同,这不是一个被接受的 ActivityPub 扩展,但它已被 Mastodon 广泛推广,也在 `Move` 活动中使用。[参见 Mastodon 文档获取更多信息](https://documentation.sig.gy/spec/activitypub/#namespaces)。 + +#### 传入 + +对于传入的 AP 消息,GoToSocial 查找行为体上的 `movedTo` 属性,该属性设置为单个 ActivityPub 行为体 URI/ID。 + +例如: + +```json +{ + "@context": [ + "http://joinmastodon.org/ns", + "https://w3id.org/security/v1", + "https://www.w3.org/ns/activitystreams", + "http://schema.org" + ], + "featured": "http://example.org/users/flyingsloth/collections/featured", + "followers": "http://example.org/users/flyingsloth/followers", + "following": "http://example.org/users/flyingsloth/following", + "id": "http://example.org/users/flyingsloth", + "inbox": "http://example.org/users/flyingsloth/inbox", + "manuallyApprovesFollowers": true, + "name": "飞翔的树懒 :3", + "outbox": "http://example.org/users/flyingsloth/outbox", + "preferredUsername": "flyingsloth", + "publicKey": {...}, + "summary": "\u003cp\u003e只是一只普通树懒\u003c/p\u003e", + "type": "Person", + "url": "http://example.org/@flyingsloth", + "alsoKnownAs": [ + "https://another-server.com/users/flyingsloth" + ], + "movedTo": "https://another-server.com/users/flyingsloth" +} +``` + +在上述 JSON 中,行为体 `http://example.org/users/flyingsloth` 已设置别名为行为体 `https://another-server.com/users/flyingsloth` 并已迁移/转向该账户。 + +GoToSocial 将传入的 `movedTo` 值存储在数据库中,但除非行为体在进行移动之前发送了一个 `Move` 活动,否则不会认为帐户迁移已处理(见下文)。 + +### `Move` 活动 + +为了实际触发账户迁移,GoToSocial 使用 `Move` 活动,并将行为体 URI 作为对象和目标,例如: + +```json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.org/users/flyingsloth/moves/01HR9FDFCAGM7JYPMWNTFRDQE9", + "actor": "https://example.org/users/flyingsloth", + "type": "Move", + "object": "https://example.org/users/flyingsloth", + "target": "https://another-server.com/users/my_new_account_hurray", + "to": "https://example.org/users/flyingsloth/followers" +} +``` + +在上述 `Move` 中,行为体 `https://example.org/users/flyingsloth` 指示其账户正在迁移到 URI `https://another-server.com/users/my_new_account_hurray`。 + +### 迁入 + +在收到行为体收件箱中的 `Move` 活动时,GoToSocial 将首先通过以下检查验证 `Move`: + +1. 请求由 `actor` 签名。 +2. `actor` 和 `object` 字段相同(你不能迁移其他人的账户)。 +3. `actor` 尚未迁移到其他地方。 +4. `target` 是有效的行为体 URI:可检索、未封禁、未迁移,且不在接收到此 `Move` 的 GoToSocial 实例屏蔽的实例上。 +5. `target` 将 `alsoKnownAs` 设置为发送 `Move` 的 `actor`。在此示例中,`https://another-server.com/users/my_new_account_hurray` 必须具有 `alsoKnownAs` 值,其中包括 `https://example.org/users/flyingsloth`。 + +如果检查通过,则 GoToSocial 将通过将粉丝重定向到新账户来处理 `Move`: + +1. 选择此 GoToSocial 实例上执行 `Move` 的 `actor` 的所有粉丝。 +2. 对于以这种方式选择的每个本站粉丝,从该粉丝的账户发送关注请求到 `Move` 的 `target`。 +3. 删除针对“旧” `actor` 的所有关注。 + +这样做的最终结果是,在接收实例上 `https://example.org/users/flyingsloth` 的所有粉丝现在将关注 `https://another-server.com/users/my_new_account_hurray`。 + +GoToSocial 还会删除由执行 `Move` 的 `actor` 拥有的所有关注和待关注请求;由 `target` 帐户再次发送关注请求。 + +为了防止潜在的 DoS 向量,GoToSocial 对 `Move` 强制进行 7 天冷却期。一旦帐户成功转移,GoToSocial 将在上次迁移后的 7 天内不处理来自新帐户的进一步迁移活动。 + +#### 迁出 + +发送帐户迁移时,GoToSocial 以类似方式使用 `Move` 活动。当 GoToSocial 实例上的行为体想要执行 `Move` 时,GoToSocial 将首先检查和验证 `Move` 目标,并确保它具有等于执行 `Move` 的行为体的 `alsoKnownAs` 条目。在成功验证后,将向所有发起迁移的行为体的粉丝发送 `Move` 活动,为其指示 `Move` 的 `target`。GoToSocial 期望外站将相应的追随者迁移到 `target` 名下。 diff --git a/docs/locales/zh/federation/glossary.md b/docs/locales/zh/federation/glossary.md new file mode 100644 index 000000000..5214b0745 --- /dev/null +++ b/docs/locales/zh/federation/glossary.md @@ -0,0 +1,73 @@ +# 术语表 + +本文档描述了有关联合的一些常用术语。 + +## `ActivityPub` + +一种基于 ActivityStreams 数据格式的去中心化社交网络协议。参见 [这里](https://www.w3.org/TR/activitypub/)。 + +GoToSocial 在 与其它 GtS 服务器和其它联邦宇宙服务器(如 Mastodon, Pixelfed 等)通信时使用 ActivityPub 协议。 + +## `ActivityStreams` + +使用 JSON 表示潜在活动和已完成活动的模型/数据格式。参见 [这里](https://www.w3.org/TR/activitystreams-core/)。 + +GoToSocial 使用 ActivityStreams 数据模型与其他服务器进行 ActivityPub 通信。 + +## `Actor` (行为体) + +Actor 是一个可以执行某些活动(例如关注、点赞、创建贴文、转发等)的 ActivityStreams 对象。参见 [这里](https://www.w3.org/TR/activitypub/#actors)。 + +在 GoToSocial 中,每个账号/用户都是一个 actor。 + +## `Dereference` (解引用) + +“解引用”一个贴文或账户意味着向托管该贴文或账户的服务器发出 HTTP 请求,以获取其 ActivityStreams 表示。 + +GoToSocial 对外站服务器解引用贴文和账户,以将它们转换为 GoToSocial 可以理解和处理的模型。 + +以下是一些示例的详细说明: + +假设有人在 ActivityPub 服务器上搜索用户名 `@tobi@goblin.technology`。 + +他们的服务器会在 `goblin.technology` 上通过以下 URL 进行 webfinger 查询: + +```text +https://goblin.technology/.well-known/webfinger?resource=acct:tobi@goblin.technology +``` + +`goblin.technology` 服务器会返回一些 JSON 作为响应,类似于: + +```json +{ + "subject": "acct:tobi@goblin.technology", + "aliases": [ + "https://goblin.technology/users/tobi", + "https://goblin.technology/@tobi" + ], + "links": [ + { + "rel": "http://webfinger.net/rel/profile-page", + "type": "text/html", + "href": "https://goblin.technology/@tobi" + }, + { + "rel": "self", + "type": "application/activity+json", + "href": "https://goblin.technology/users/tobi" + } + ] +} +``` + +在链接部分,请求服务器会寻找类型为 `application/activity+json` 的链接,这表示用户的 ActivityStreams 表示。在此情况下,URL 是: + +```text +https://goblin.technology/users/tobi +``` + +上述 URL 是 `tobi` 在 `goblin.technology` 实例上的 用户/行为体 的 activitypub 表示的*引用*。它之所以被称为引用,是因为它不包含关于该用户的所有信息,只是信息所在位置的参考点。 + +现在,请求服务器将向该 URL 发送请求,以获得 `@tobi@goblin.technology` 的更完整表示,以符合 ActivityPub 规范。换句话说,服务器现在通过一个*引用*来获取它所引用的内容。这使得它*不再是一个引用*,因此称为*解引用*。 + +作为类比,考虑在书的目录中查找某些内容时的情况:首先你获得你感兴趣的材料所在的页码,这是一个引用。然后你翻到引用的页面查看内容,这就是解引用。 diff --git a/docs/locales/zh/federation/http_signatures.md b/docs/locales/zh/federation/http_signatures.md new file mode 100644 index 000000000..b4f2a17e9 --- /dev/null +++ b/docs/locales/zh/federation/http_signatures.md @@ -0,0 +1,85 @@ +# HTTP 签名 + +GoToSocial 要求所有发送到 ActivityPub 服务器的 `GET` 和 `POST` 请求都必须附带有效的 HTTP 签名。 + +GoToSocial 也会为其向其他服务器发送的所有 `GET` 和 `POST` 请求签名。 + +这种行为与 Mastodon 的 [AUTHORIZED_FETCH / "安全模式"](https://docs.joinmastodon.org/admin/config/#authorized_fetch) 等效。 + +GoToSocial 使用 [superseriousbusiness/httpsig](https://github.com/superseriousbusiness/httpsig) 库(从 go-fed 派生)来为发出的请求签名,并解析和验证传入请求的签名。该库严格遵循 [Cavage HTTP Signature RFC](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12),这是其他实现(如 Mastodon、Pixelfed、Akkoma/Pleroma 等)使用的同一份 RFC。(此 RFC 后来被 [httpbis HTTP Signature RFC](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures) 取代,但尚未广泛实施。) + +## 查询参数 + +关于是否应该在用于生成和验证签名的 URL 中包含查询参数,HTTP 签名规范并无明确规定。 + +在历史上,GoToSocial 在签名中包含了查询参数,而大多数其他实现则没有。这导致在对 Collection 端点进行签名 GET 请求或验证签名的 GET 请求时(通常使用查询参数进行分页),出现了兼容性问题。 + +从 0.14 开始,GoToSocial 尝试同时签署和验证携带和不携带查询参数请求,以确保与其他实现更好的兼容性。 + +发送请求时,GtS 将首先尝试包含查询参数的情况。当收到外站服务器的 `401` 响应时,它将尝试在不包含查询参数的情况下重新发送请求。 + +接收请求时,GtS 将首先尝试验证包含查询参数的签名。如果签名验证失败,它将尝试在不包含查询参数的情况下重新验证签名。 + +详细信息请参见 [#894](https://github.com/superseriousbusiness/gotosocial/issues/894)。 + +## 传入请求 + +GoToSocial 的请求签名验证在 [internal/federation](https://github.com/superseriousbusiness/gotosocial/blob/main/internal/federation/authenticate.go) 中实现。 + +GoToSocial 将尝试按以下算法顺序解析签名,成功后将停止: + +```text +RSA_SHA256 +RSA_SHA512 +ED25519 +``` + +## 发出请求 + +GoToSocial 的请求签名在 [internal/transport](https://github.com/superseriousbusiness/gotosocial/blob/main/internal/transport/signing.go) 中实现。 + +一旦解决了 https://github.com/superseriousbusiness/gotosocial/issues/2991 ,GoToSocial 将使用 `(created)` 伪标头代替 `date`。 + +然而,目前在组装签名时: + +- 发出的 `GET` 请求使用 `(request-target) host date` +- 发出的 `POST` 请求使用 `(request-target) host date digest` + +GoToSocial 在签名中将 `algorithm` 字段设置为 `hs2019`,这一般意味着“从与 keyId 关联的元数据中导出算法”。生成签名时使用的实际算法是 `RSA_SHA256`,这与其他 ActivityPub 实现一致。在验证 GoToSocial 的 HTTP 签名时,外站服务器可以安全地假设该签名是使用 `sha256` 生成的。 + +## 特点 + +GoToSocial 在 `Signature` 标头中使用的 `keyId` 格式如下所示: + +```text +https://example.org/users/example_user/main-key +``` + +这不同于大多数其他实现,它们通常在 `keyId` URI 中使用片段 (`#`)。例如,在 Mastodon 上,用户的密钥会这样表示: + +```text +https://example.org/users/example_user#main-key +``` + +对于 Mastodon,用户的公钥作为该用户的 Actor 表示的一部分提供。GoToSocial 在提供用户的公钥时模仿了这种行为,但并不在 `main-key` 端点返回完整的 Actor(这可能包含敏感字段),而是仅返回 Actor 的部分存根。它如下所示: + +```json +{ + "@context": [ + "https://w3id.org/security/v1", + "https://www.w3.org/ns/activitystreams" + ], + "id": "https://example.org/users/example_user", + "preferredUsername": "example_user", + "publicKey": { + "id": "https://example.org/users/example_user/main-key", + "owner": "https://example.org/users/example_user", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzGB3yDvMl+8p+ViutVRG\nVDl9FO7ZURYXnwB3TedSfG13jyskoiMDNvsbLoUQM9ajZPB0zxJPZUlB/W3BWHRC\nNFQglE5DkB30GjTClNZoOrx64vLRT5wAEwIOjklKVNk9GJi1hFFxrgj931WtxyML\nBvo+TdEblBcoru6MKAov8IU4JjQj5KUmjnW12Rox8dj/rfGtdaH8uJ14vLgvlrAb\neQbN5Ghaxh9DGTo1337O9a9qOsir8YQqazl8ahzS2gvYleV+ou09RDhS75q9hdF2\nLI+1IvFEQ2ZO2tLk3umUP1ioa+5CWKsWD0GAXbQu9uunAV0VoExP4+/9WYOuP0ei\nKwIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "type": "Person" +} +``` + +与 GoToSocial 联合的外站服务器应从 `publicKey` 字段提取公钥。然后,它们应该使用公钥的 `owner` 字段签名 `GET` 请求,进一步解引用 Actor 的完整版本。 + +这种行为是为了避免外站服务器对完整 Actor 端点进行未签名的 `GET` 请求引入的。然而,由于不合规且引发问题,此行为可能会在未来发生变化。在 [此问题](https://github.com/superseriousbusiness/gotosocial/issues/1186) 中进行跟踪。 diff --git a/docs/locales/zh/federation/index.md b/docs/locales/zh/federation/index.md new file mode 100644 index 000000000..1f1afde9d --- /dev/null +++ b/docs/locales/zh/federation/index.md @@ -0,0 +1,7 @@ +# 概述 + +本节文档包含与 GoToSocial 联合所需的各个 (ActivityPub/ActivityStreams) 元素的信息。 + +!!! info "Post 与 Status 的区别" + + 在文档中,post 与 status 这两个术语可以互换使用,指的是由用户创建的相同内容:一个(微型)博客条目,可能包含文本、媒体附件等。在中文文档中,我们统一使用“贴文”一词来指代这种内容。 diff --git a/docs/locales/zh/federation/moderation.md b/docs/locales/zh/federation/moderation.md new file mode 100644 index 000000000..ce506568a --- /dev/null +++ b/docs/locales/zh/federation/moderation.md @@ -0,0 +1,41 @@ +# 管理 + +## 举报 / 标记 + +与其他微博 ActivityPub 实现类似,GoToSocial 使用 [Flag](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-flag) 活动类型来向其他服务器传达用户举报信息。 + +### 发送举报 + +发送的 GoToSocial `Flag` 的 JSON 格式如下: + +```json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "actor": "http://example.org/users/example.org", + "content": "此用户频繁骚扰其它用户", + "id": "http://example.org/reports/01GP3AWY4CRDVRNZKW0TEAMB5R", + "object": [ + "http://fossbros-anonymous.io/users/foss_satan", + "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M" + ], + "type": "Flag" +} +``` + +`Flag` 的 `actor` 始终是创建该 `Flag` 的 GoToSocial 实例的实例行为体。这样做是为了保护创建举报的用户,实现部分的匿名性,以防止他们成为骚扰目标。 + +`Flag` 的 `content` 是由创建 `Flag` 的用户提交的一段文本,该文本应该向外站管理员提供创建举报的理由。如果用户没有提交理由,`content` 可能是空字符串,也可能不存在于 JSON 中。 + +`Flag` 的 `object` 字段的值可以是一个字符串(被举报用户的 ActivityPub `id`),或者是一个字符串数组,其中数组的第一个条目是被举报用户的 `id`,后续条目是一个或多个被举报的 `Note` / 贴文的 `id`。 + +`Flag` 活动会原样发送到被举报用户的 `inbox`(或共享收件箱)。它不会被包装在 `Create` 活动中。 + +### 接收举报 + +GoToSocial 假设接收到的举报会作为 `Flag` 活动发送到被举报用户的 `inbox`。它将按照创建发送 `Flag` 的相同方式解析接收到的 `Flag`,但有一个不同之处:它会尝试从 `object` 字段和 Misskey/Calckey 格式的 `content` 值中解析贴文的 URL,这些值包含内联的贴文 URL。 + +GoToSocial 不会假设接收到的 `Flag` 活动中会设置 `to` 字段。相反,它假定外站使用 `bto` 将 `Flag` 发送给其接收者。 + +接收到的有效 `Flag` 活动将作为举报提供给接收到举报的 GoToSocial 实例管理员,以便他们对被举报用户采取必要的管理措施。 + +除非 GtS 管理员选择通过其他渠道与被举报用户分享此信息,被举报用户本人不会看到举报信息,也不会收到被举报的通知。 diff --git a/docs/locales/zh/federation/posts.md b/docs/locales/zh/federation/posts.md new file mode 100644 index 000000000..d536c9cae --- /dev/null +++ b/docs/locales/zh/federation/posts.md @@ -0,0 +1,924 @@ +# 帖文及其属性 + +## 话题标签 + +GoToSocial 用户可以在贴文中包含话题标签,用于向其他实例表明该用户希望将其贴文与其他使用相同话题标签的贴文加入同一分组,以便于发现。 + +GoToSocial 默认期望只有公开地址的贴文会按话题标签分组,这与其他 ActivityPub 服务器实现一致。 + +为了实现话题标签的联合,GoToSocial 在对象的 `tag` 属性中使用被广泛采用的 [ActivityStreams `Hashtag` 类型扩展](https://www.w3.org/wiki/Activity_Streams_extensions#as:Hashtag_type)。 + +以下是一条外发信息中的 `tag` 属性示例,包含自定义表情和一个话题标签: + +```json +"tag": [ + { + "icon": { + "mediaType": "image/png", + "type": "Image", + "url": "https://example.org/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png" + }, + "id": "https://example.org/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ", + "name": ":rainbow:", + "type": "Emoji", + "updated": "2021-09-20T10:40:37Z" + }, + { + "href": "https://example.org/tags/welcome", + "name": "#welcome", + "type": "Hashtag" + } +] +``` + +当仅有一个话题标签时,`tag` 属性将是一个对象而非数组,如下所示: + +```json +"tag": { + "href": "https://example.org/tags/welcome", + "name": "#welcome", + "type": "Hashtag" +} +``` + +### 话题标签的 `href` 属性 + +GoToSocial 外发话题标签提供的 `href` URL 指向一个提供 `text/html` 的网页。 + +GoToSocial 对给定 `text/html` 的内容不做任何保证,外站不应该将 URL 解释为规范的 ActivityPub ID/URI 属性。`href` URL 仅作为可能包含该话题标签更多信息的一个端点。 + +## 提及 + +GoToSocial 用户可以在贴文中使用 `@[用户名]@[域名]` 格式提及其他用户。例如,如果一个 GoToSocial 用户想提及实例 `example.org` 上的用户 `someone`,可以在贴文中包含 `@someone@example.org`。 + +!!! info "提及与活动地址" + + 提及的表示不仅仅是美观问题,它们也会影响活动的地址。 + + 如果 GoToSocial 用户在 Note 中明确提及另一个用户,该用户的 URI 将始终包含在 Note 的 Create 活动的 `To` 或 `Cc` 属性中。 + + 如果 Note 的可见性为私信(即,不发送给公众或粉丝),每个提及的目标 URI 将位于活动包装的 `To` 属性中。 + + 在所有其他情况下,提及将包含在活动包装的 `Cc` 属性中。 + +### 外发 + +当 GoToSocial 用户提及另一个账户时,提及会作为 `tag` 属性中的一个条目包含在外发的联合消息中。 + +例如,一个 GoToSocial 实例上的用户提及 `@someone@example.org`,外发 Note 的 `tag` 属性可能如下: + +```json +"tag": { + "href": "http://example.org/users/someone", + "name": "@someone@example.org", + "type": "Mention" +} +``` + +如果用户提及同一实例内的本站用户,他们的完整 `name` 仍会被包含。 + +例如,一个 GoToSocial 用户在域 `some.gotosocial.instance` 上提及同一实例内的用户 `user2`。他们还提及了 `@someone@example.org`。外发 Note 的 `tag` 属性如下: + +```json +"tag": [ + { + "href": "http://example.org/users/someone", + "name": "@someone@example.org", + "type": "Mention" + }, + { + "href": "http://some.gotosocial.instance/users/user2", + "name": "@user2@some.gotosocial.instance", + "type": "Mention" + } +] +``` + +为了方便外站,GoToSocial 始终在外发的提及中提供 `href` 和 `name` 属性。GoToSocial 使用的 `href` 属性始终是目标账户的 ActivityPub ID/URI,而不是网页 URL。 + +### 传入 + +GoToSocial 尝试以与发送出相同的方式解析传入提及:作为 `tag` 属性中的 `Mention` 类型条目。然而,解析传入提及时,对于必须设置哪些属性的要求会更宽松些。 + +GoToSocial 更偏好 `href` 属性,它可以是目标的 ActivityPub ID/URI 或网页 URL;如果 `href` 不存在,将使用 `name` 属性作为回退。如果两个属性都不存在,提及将被视为无效并被丢弃。 + +## 内容、内容映射和语言 + +与其他 ActivityPub 实现一致,GoToSocial 在 `Objects` 中使用 `content` 和 `contentMap` 字段来推断传入贴文的内容和语言,并在外发贴文中设置内容和语言。 + +### 外发 + +如果一个外发 `Object`(通常是 `Note`)有内容,它将以在 `content` 字段中以被字符串化的 HTML 形式给出。 + +如果 `content` 关联特定用户选择的语言,那么 `Object` 还将设置 `contentMap` 属性为单条目键/值映射,其中键是 BCP47 语言话题标签,值是与 `content` 字段相同的内容。 + +例如,一篇用英语 (`en`) 写的贴文将如下所示: + +```json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Note", + "attributedTo": "http://example.org/users/i_p_freely", + "to": "https://www.w3.org/ns/activitystreams#Public", + "cc": "http://example.org/users/i_p_freely/followers", + "id": "http://example.org/users/i_p_freely/statuses/01FF25D5Q0DH7CHD57CTRS6WK0", + "url": "http://example.org/@i_p_freely/statuses/01FF25D5Q0DH7CHD57CTRS6WK0", + "published": "2021-11-20T13:32:16Z", + "content": "

This is an example note.

", + "contentMap": { + "en": "

This is an example note.

" + }, + "attachment": [], + "replies": {...}, + "sensitive": false, + "summary": "", + "tag": {...} +} +``` + +如果贴文有内容,GoToSocial 会始终设置 `content` 字段,但是如果使用的 GoToSocial 版本较旧、用户未设置语言,或设置的语言不是公认的 BCP47 语言标签,则可能不会始终设置 `contentMap` 字段。 + +### 传入 + +GoToSocial 在解析传入的 `Object` 时使用 `content` 和 `contentMap` 属性来确定内容并推断该内容的主要语言。它使用以下算法: + +#### 仅设置 `content` + +仅采用该内容并将语言标记为未知。 + +#### 同时设置 `content` 和 `contentMap` + +在 `contentMap` 中查找键,作为语言标签,要查找的键的值与 `content` 中的 HTML 字符串匹配。 + +如果找到匹配项,将其用作贴文的语言。 + +如果未找到匹配项,则保留 `content` 中的内容并将语言标记为未知。 + +#### 仅设置 `contentMap` + +如果 `contentMap` 只有一个条目,则将语言标签和内容(值)作为“主要”语言和内容。 + +如果 `contentMap` 有多个条目,则无法确定贴文的意图内容和语言,因为映射顺序不可预测。在这种情况下,尝试从 GoToSocial 实例的[配置语言](../configuration/instance.md)中选择与其中一种语言匹配的语言和内容条目。如果无法通过这种方式匹配语言,则从 `contentMap` 中随机选择一个语言和内容条目作为“主要”语言和内容。 + +!!! Note + 在上述所有情况下,如果推断的语言无法解析为有效的 BCP47 语言话题标签,则语言将回退为未知。 + +## 互动规则 + +GoToSocial 使用 `interactionPolicy` 属性告知外站给定帖文允许的互动类型(有前提)。 + +!!! danger + + 互动规则旨在限制用户贴文上用户不希望的回复和其他互动的有害影响(例如,“回复家(reply guys)” —— 不请自来地发表冒失回复的人)。 + + 然而,这远远不能满足这一目的,因为仍然有许多“额外”方式可以分发或回复贴文,进而超出用户的初衷或意图。 + + 例如,用户可能创建一个附有非常严格互动规则的贴文,却发现其他服务器软件不尊重该规则,而其他实例上的用户从他们的实例视角进行讨论并回复该贴文。原始发布者的实例将自动从视图中删除这些用户不想要的互动,但外站实例可能仍会显示它们。 + + 另一个例子:有人可能会看到一个指定没人可以回复的贴文,但截屏该贴文,将截屏作为新帖文发布,并将提及原作者或只是附上原贴文的 URL。在这种情况下,他们成功通过新创建的贴文串来达到“回复”该贴文的目的。 + + 无论好坏,GoToSocial 只能为这一部分问题提供尽最大努力的部分技术解决方案,这更多的是一个社会行为和边界的问题。 + +### 概述 + +`interactionPolicy` 是贴文类 `Object`(如 `Note`、`Article`、`Question` 等)附带的一个属性,其格式如下: + +```json +{ + [...], + "interactionPolicy": { + "canLike": { + "always": [ "始终可进行此操作的零个或多个 URI" ], + "approvalRequired": [ "需要批准才能进行此操作的零个或多个 URI" ] + }, + "canReply": { + "always": [ "始终可进行此操作的零个或多个 URI" ], + "approvalRequired": [ "需要批准才能进行此操作的零个或多个 URI" ] + }, + "canAnnounce": { + "always": [ "始终可进行此操作的零个或多个 URI" ], + "approvalRequired": [ "需要批准才能进行此操作的零个或多个 URI" ] + } + }, + [...] +} +``` + +在此对象中: + +- `canLike` 指定可创建 `Like` 并将帖文 URI 作为 `Like` 的 `Object` 的人。 +- `canReply` 指定可创建 `inReplyTo` 设置为帖文 URI 的帖文的人。 +- `canAnnounce` 指定可创建 `Announce` 并将帖文 URI 作为 `Announce` 的 `Object` 的人。 + +并且: + +- `always` 是一个 ActivityPub URI/ID 的 `Actor` 或 `Actor` 的 `Collection`,无需 `Accept` 即可进行互动分发到其粉丝。 +- `approvalRequired` 是一个 ActivityPub URI/ID 的 `Actor` 或 `Actor` 的 `Collection`,可以互动,但在将互动分发给其粉丝之前需要获得 `Accept`。 + +`always` 和 `approvalRequired` 的有效 URI 条目包括 magic ActivityStreams 公共 URI `https://www.w3.org/ns/activitystreams#Public`,贴文创建者的 `Following` 和/或 `Followers` 集合的 URI,以及个人请求体的 URI。例如: + +```json +[ + "https://www.w3.org/ns/activitystreams#Public", + "https://example.org/users/someone/followers", + "https://example.org/users/someone/following", + "https://example.org/users/someone_else", + "https://somewhere.else.example.org/users/someone_on_a_different_instance" +] +``` + +### 指定无人能进行的操作 + +!!! note + 即使规则指定无人可互动,GoToSocial 仍做出默认假设。参见[默认假设](#默认假设)。 + +空数组或缺少/空的键表示无人能进行此互动。 + +例如,以下 `canLike` 指定无人能 `Like` 该贴文: + +```json +"canLike": { + "always": [], + "approvalRequired": [] +}, +``` + +类似的,`canLike` 值为 `null` 也表示无人能 `Like` 该帖: + +```json +"canLike": null +``` + +或 + +```json +"canLike": { + "always": null, + "approvalRequired": null +} +``` + +缺失的 `canLike` 值同样表达了相同的意思: + +```json +{ + [...], + "interactionPolicy": { + "canReply": { + "always": [ "始终可进行此操作的零个或多个 URI" ], + "approvalRequired": [ "需要批准才能进行此操作的零个或多个 URI" ] + }, + "canAnnounce": { + "always": [ "始终可进行此操作的零个或多个 URI" ], + "approvalRequired": [ "需要批准才能进行此操作的零个或多个 URI" ] + } + }, + [...] +} +``` + +### 冲突/重复值 + +在用户位于集合 URI 中, 且也通过 URI 被显式指定的情况下,**更具体的值**优先。 + +例如: + +```json +[...], +"canReply": { + "always": [ + "https://example.org/users/someone" + ], + "approvalRequired": [ + "https://www.w3.org/ns/activitystreams#Public" + ] +}, +[...] +``` + +在此情形下,`@someone@example.org` 位于 `always` 数组中,并且也存在于 `approvalRequired` 数组中的 magic ActivityStreams 公共集合中。在这种情况下,他们可以始终回复,因为 `always` 值更为明确。 + +另一个例子: + +```json +[...], +"canReply": { + "always": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "approvalRequired": [ + "https://example.org/users/someone" + ] +}, +[...] +``` + +在此,`@someone@example.org` 位于 `approvalRequired` 数组中,但也隐含地存在于 `always` 数组中的 magic ActivityStreams 公共集合中。在这种情况下,除了 `@someone@example.org` 需要批准外,其他人都可以无需批准进行回复。 + +在相同 URI 存在于 `always` 和 `approvalRequired` 两者中时,**最高级别的权限**优先(即 `always` 中的 URI 优先于 `approvalRequired` 中的相同 URI)。 + +### 默认假设 + +GoToSocial 对 `interactionPolicy` 做出若干默认假设。 + +**首先**,无论贴文的可见性和 `interactionPolicy` 如何,被[提及](#提及)或回复的用户应**始终**能够回复该贴而无需批准,**除非**提及或回复他们的贴文本身正在等待批准。 + +这是为了防止潜在的骚扰者在辱骂贴文中提及某人,并不给被提及的用户任何回复的机会。 + +因此,当发送互动规则时,GoToSocial 始终将提及用户的 URI 添加到 `canReply.always` 数组中,除非它们已被 magic ActivityStreams 公共 URI 覆盖。 + +同样,在执行接收到的互动规则时,即使用户 URI 不存在于 `canReply.always` 数组中,GoToSocial 也将被提及用户的 URI 视作已存在。 + +**其次**,用户应**始终**能够回复自己的贴文,点赞自己的贴文,并转发自己的贴文而无需批准,**除非**该贴文本身正在等待批准。 + +因此,当发送互动规则时,GoToSocial 始终将贴文作者的 URI 添加到 `canLike.always`、`canReply.always` 和 `canAnnounce.always` 数组中,除非它们已被 magic ActivityStreams 公共 URI 覆盖。 + +同样,在执行接收到的互动规则时,即使贴文作者 URI 不存在于这些 `always` 数组中,GoToSocial 也始终将贴文作者 URI 视为已存在。 + +### 默认值 + +当贴文上没有 `interactionPolicy` 属性时,GoToSocial 会根据贴文可见级别和发帖作者为该帖假定默认的 `interactionPolicy`。 + +对于 `@someone@example.org` 的**公开**或**未列出**贴文,默认 `interactionPolicy` 为: + +```json +{ + [...], + "interactionPolicy": { + "canLike": { + "always": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "approvalRequired": [] + }, + "canReply": { + "always": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "approvalRequired": [] + }, + "canAnnounce": { + "always": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "approvalRequired": [] + } + }, + [...] +} +``` + +对于 `@someone@example.org` 的**仅限粉丝**贴文,假定的 `interactionPolicy` 为: + +```json +{ + [...], + "interactionPolicy": { + "canLike": { + "always": [ + "https://example.org/users/someone", + "https://example.org/users/someone/followers", + [...提及的用户的 URI...] + ], + "approvalRequired": [] + }, + "canReply": { + "always": [ + "https://example.org/users/someone", + "https://example.org/users/someone/followers", + [...提及的用户的 URI...] + ], + "approvalRequired": [] + }, + "canAnnounce": { + "always": [ + "https://example.org/users/someone" + ], + "approvalRequired": [] + } + }, + [...] +} +``` + +对于 `@someone@example.org` 的**私信**贴文,假定的 `interactionPolicy` 为: + +```json +{ + [...], + "interactionPolicy": { + "canLike": { + "always": [ + "https://example.org/users/someone", + [...提及的用户的 URI...] + ], + "approvalRequired": [] + }, + "canReply": { + "always": [ + "https://example.org/users/someone", + [...提及的用户的 URI...] + ], + "approvalRequired": [] + }, + "canAnnounce": { + "always": [ + "https://example.org/users/someone" + ], + "approvalRequired": [] + } + }, + [...] +} +``` + +### 示例 1 - 限制对话范围 + +在此示例中,用户 `@the_mighty_zork` 想开始与用户 `@booblover6969` 和 `@hodor` 之间的对话。 + +为了避免讨论被他人打断,他们希望来自三名参与者以外的用户的回复仅在获得 `@the_mighty_zork` 批准后才被允许。 + +此外,他们希望将贴文转发/`Announce` 的权利限制为仅限于他们自己的粉丝和三个对话参与者。 + +然而,任何人都可以 `Like` `@the_mighty_zork` 的贴文。 + +这可以通过以下 `interactionPolicy` 来实现,它附加在可见性为公开的帖文上: + +```json +{ + [...], + "interactionPolicy": { + "canLike": { + "always": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "approvalRequired": [] + }, + "canReply": { + "always": [ + "https://example.org/users/the_mighty_zork", + "https://example.org/users/booblover6969", + "https://example.org/users/hodor" + ], + "approvalRequired": [ + "https://www.w3.org/ns/activitystreams#Public" + ] + }, + "canAnnounce": { + "always": [ + "https://example.org/users/the_mighty_zork", + "https://example.org/users/the_mighty_zork/followers", + "https://example.org/users/booblover6969", + "https://example.org/users/hodor" + ], + "approvalRequired": [] + } + }, + [...] +} +``` + +### 示例 2 - 长独白贴文串 + +在此示例中,用户 `@the_mighty_zork` 想写一个长篇独白。 + +他们不介意别人转发和点赞贴文,但不想收到任何回复,因为他们没有精力去管理讨论;他们只是想通过发泄自己的想法去表达自我。 + +这可以通过在贴文串中的每个贴文上设置以下 `interactionPolicy` 来实现: + +```json +{ + [...], + "interactionPolicy": { + "canLike": { + "always": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "approvalRequired": [] + }, + "canReply": { + "always": [ + "https://example.org/users/the_mighty_zork" + ], + "approvalRequired": [] + }, + "canAnnounce": { + "always": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "approvalRequired": [] + } + }, + [...] +} +``` + +在这里,任何人都可以点赞或转发,但无人能够回复(除了 `@the_mighty_zork` 自己)。 + +### 示例 3 - 完全开放 + +在此示例中,`@the_mighty_zork` 想写一篇完全开放的贴文,任何能看到此帖的人都可以进行回复、转发或点赞(即解锁和公开贴文默认行为): + +```json +{ + [...], + "interactionPolicy": { + "canLike": { + "always": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "approvalRequired": [] + }, + "canReply": { + "always": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "approvalRequired": [] + }, + "canAnnounce": { + "always": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "approvalRequired": [] + } + }, + [...] +} +``` + +### 请求、获得和验证批准 + +当用户的 URI 在需要获得批准的互动的 `approvalRequired` 数组中时,如果他们希望获得批准以分发互动,应该执行以下步骤: + +1. 像往常一样撰写互动 `Activity`(即 `Like`、`Create` (回复)或 `Announce`)。 +2. 像往常一样将 `Activity` 的 `to` 和 `cc` 地址设为预期的收件人。 +3. 将 `Activity` **仅**发送到要互动帖的作者的 `Inbox`(或 `sharedInbox`)。 +4. **此时不要进一步分发 `Activity`**。 + +此时,互动可视为等待批准,并不应该显示在被互动的贴文的回复或点赞集合等中。 + +可以向发送互动的用户显示“互动待批准”状态,但理想情况下不应该向与该用户共享实例的其他用户显示。 + +从这里开始,可能会出现以下三种情况之一: + +#### 拒绝 + +在这种情况下,互动目标贴文的作者发回一个 `Reject` `Activity`,该活动的 `Object` 属性带有待拒绝互动活动的 URI/ID。 + +例如,以下 JSON 对象 `Reject` 了 `@someone@somewhere.else.example.org` 回复 `@post_author@example.org` 贴文的尝试: + +```json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "actor": "https://example.org/users/post_author", + "to": "https://somewhere.else.example.org/users/someone", + "id": "https://example.org/users/post_author/activities/reject/01J0K2YXP9QCT5BE1JWQSAM3B6", + "object": "https://somewhere.else.example.org/users/someone/statuses/01J17XY2VXGMNNPH1XR7BG2524", + "type": "Reject" +} +``` + +如果发生这种情况,`@someone@somewhere.else.example.org`(及其实例)应视交互为被拒绝。该实例应从其内部存储(即数据库)中删除该活动,或以其他方式表明它已被拒绝,并且不应进一步分发该 `Activity` 或重试该交互。 + +#### 无响应 + +在这种情况下,正在互动的贴文的作者从不发送 `Reject` 或 `Accept` `Activity`。在这种情况下,交互被视为永久“待处理”。实例可能希望实现某种清理功能,达到一定时间期限的已发送且待处理交互应被视为过期,然后按照上述方式被处理为 `Rejected` 并删除。 + +#### 接受 + +在这种情况下,正在互动的贴文的作者发回一个`Accept` `Activity`,该活动的 `Object` 属性带有待批准互动活动的 URI/ID。 + +例如,以下 JSON 对象 `Accept` 了 `@someone@somewhere.else.example.org` 回复 `@post_author@example.org` 贴文的尝试: + +```json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "actor": "https://example.org/users/post_author", + "cc": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://example.org/users/post_author/followers" + ], + "to": "https://somewhere.else.example.org/users/someone", + "id": "https://example.org/users/post_author/activities/accept/01J0K2YXP9QCT5BE1JWQSAM3B6", + "object": "https://somewhere.else.example.org/users/someone/statuses/01J17XY2VXGMNNPH1XR7BG2524", + "type": "Accept" +} +``` + +如果发生这种情况,`@someone@somewhere.else.example.org`(及其实例)应视为交互已被批准/接受。然后,该实例可以自由地将此交互 `Activity` 分发给所有由 `to`、`cc` 等目标的接收者,并附加属性 `approvedBy`。 + +### 验证在粉丝/关注中是否存在 + +如果一个 `Actor` 在其互动规则的 `always` 字段中因为存在于 `Followers` 或 `Following` 集合中而被允许进行交互(例如 `Like`、`inReplyTo` 或 `Announce`),则其服务器仍应等待来自目标帐户服务器的 `Accept`,然后才更广泛地分发交互,并将 `approvedBy` 属性设置为 `Accept` 的 URI。 + +这样可以防止第三方服务器被迫以某种方式验证互动的 `Actor` 是否存在于接收互动的用户的 `Followers` 或 `Following` 集合中。让目标服务器进行验证,并采信其 `Accept` ,将其视为交互 `Actor` 存在于相关集合中的证明,更为简单。 + +同样,当接收到一个具有匹配 `Following` 或 `Followers` 集合的 `Actor` 的互动请求时,接收互动的 `Actor` 的服务器应确保尽快发送出 `Accept`,以便交互 `Actor` 服务器可以带着适当的接受证明发送出 `Activity`。 + +这个过程应绕过通常的“待批准”阶段,因此没有必要通知 `Actor` 待批准的交互,因为他们已明确同意。在 GoToSocial 代码库中,这被称为“预批准”。 + +### `approvedBy` + +`approvedBy` 是一个附加属性,添加到 `Like` 和 `Announce` 活动以及任何被视为“贴文”的 `Object`(如 `Note`、`Article`)中。 + +`approvedBy` 的存在表明贴文的作者接受了由 `Activity` 作为目标或由 `Object` 所回复的互动,并现在可以分发给其预期观众。 + +`approvedBy` 的值应为创建 `Accept` `Activity` 的接收交互贴文作者的 URI。 + +例如,以下 `Announce` `Activity` 的 `approvedBy` 表示它已被 `@post_author@example.org` `Accept`: + +```json +{ + "actor": "https://somewhere.else.example.org/users/someone", + "to": [ + "https://somewhere.else.example.org/users/someone/followers" + ], + "cc": [ + "https://example.org/users/post_author" + ], + "id": "https://somewhere.else.example.org/users/someone/activities/announce/01J0K2YXP9QCT5BE1JWQSAM3B6", + "object": "https://example.org/users/post_author/statuses/01J17ZZFK6W82K9MJ9SYQ33Y3D", + "approvedBy": "https://example.org/users/post_author/activities/accept/01J18043HGECBDZQPT09CP6F2X", + "type": "Announce" +} +``` + +接收到一个带有 `approvedBy` 值的 `Activity` 时,外站实例应解引用字段的 URI 值以获取 `Accept` `Activity`。 + +然后,他们应验证 `Accept` `Activity` 的 `object` 值是否等于交互 `Activity` 或 `Object` 的 `id`,并验证 `actor` 值是否等于接收交互的贴文的作者。 + +此外,他们应确保解引用的 `Accept` 的 URL 域名等于接收交互贴文的作者的 URL 域名。 + +如果无法解引用 `Accept` 或未通过有效性检查,则交互应被视为无效并丢弃。 + +由于这种验证机制,实例应确保他们对涉及 `interactionPolicy` 的 `Accept` URI 的解引用响应提供一个有效的 ActivityPub 对象。如果不这样做,他们会无意中限制外站实例分发其贴文的能力。 + +### 后续回复/范围扩展 + +对话中的每个后续回复将有其自己的互动规则,由创建回复的用户选择。换句话说,整个*对话*或*贴文串*并不由一个 `interactionPolicy` 控制,而是贴文串中的每个后续贴文可以由贴文作者设置不同的规则。 + +不幸的是,这意味着即使有 `interactionPolicy`,贴文串的范围也可能不小心超出第一个贴文作者的意图。 + +例如,在上述[示例 1](#示例-1---限制对话范围)中,`@the_mighty_zork` 在第一个贴文中指定了 `canReply.always` 值为 + +```json +[ + "https://example.org/users/the_mighty_zork", + "https://example.org/users/booblover6969", + "https://example.org/users/hodor" +] +``` + +在后续回复中,`@booblover6969` 无意或有意地将 `canReply.always` 值设为: + +```json +[ + "https://www.w3.org/ns/activitystreams#Public" +] +``` + +这扩大了对话的范围,因为现在任何人都可以回复 `@booblover6969` 的贴文,并可能也在该回复中标记 `@the_mighty_zork`。 + +为了避免这个问题,建议外站实例防止用户能够扩大范围(具体机制待定)。 + +同时,实例应将任何与仍处于待批准状态的贴文或贴文类似的 `Object` 的交互视作待批准。 + +换句话说,只要某条贴文处于待批准状态,实例应将该贴文下的所有互动标记为待批准,无论此贴文的互动规则通常允许什么。 + +这可避免有用户回复贴文,且在回复尚未得到批准的情况下继续回复*他们自己的回复*并将其标记为允许(作为贴文回复的作者,他们默认拥有对贴文回复的[回复权限](#默认假设))。 + +## 投票 + +为了联合投票状态,GoToSocial 使用广泛采用的 [ActivityStreams `Question` 类型](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-question)。然而,第一个由 Mastodon 引入和推广的这个类型略微偏离 ActivityStreams 规范。在规范中,Question 类型被标记为 `IntransitiveActivity` 的扩展,此扩展是一个应当不带 `Object` 且所有详细信息应被默认包含的 `Activity` 扩展。但在具体实现中,它作为 `Object` 通过 `Create` 或 `Update` 活动传递。 + +值得注意的是,虽然 GoToSocial 内部可能将投票视作贴文附件的一种类型,但 ActivityStreams 表示法将贴文和带投票的贴文视为两种不同的 `Object` 类型。贴文以 `Note` 类型联合,投票以 `Question` 类型联合。 + +GoToSocial 传输(和期望接收)的 `Question` 类型包含所有常见的 `Note` 属性,外加一些附加内容。它们期望以下附加的(伪)JSON: + +```json +{ + "@context":[ + { + // toot:votersCount 扩展,用于添加 votersCount 属性。 + "toot":"http://joinmastodon.org/ns#", + "votersCount":"toot:votersCount" + } + ], + + // oneOf / anyOf 包含投票选项 + // 本身。只有其中之一会被设置, + // 其中 "oneOf" 表示单选投票, + // "anyOf" 表示多选投票。 + // + // 任一属性都包含一个 “Notes” 数组, + // 特殊的是它们包含一个 “name” 且未设置 + // “content”,其中 “name” 代表实际 + // 投票选项字符串。此外,它们包含 + // 一个 “Collection” 类型的 “replies” 属性, + // 通过 “totalItems” 表示每个投票选项当前已知的投票数。 + "oneOf": [ // 或 "anyOf" + { + "type": "Note", + "name": "选项 1", + "replies": { + "type": "Collection", + "totalItems": 0 + } + }, + { + "type": "Note", + "name": "选项 2", + "replies": { + "type": "Collection", + "totalItems": 0 + } + } + ], + + // endTime 指示此投票将何时结束。 + // 某些服务器实现支持永不结束的投票, + // 或使用 “closed” 来暗示 “endTime”,因此该项可能不会总是被设置。 + "endTime": "2023-01-01T20:04:45Z", + + // closed 指示此投票结束的时间。 + // 在来到此时间之前,该项将不会被设置。 + "closed": "2023-01-01T20:04:45Z", + + // votersCount 表示参与者的总数, + // 这在多选投票的情况下很有用。 + "votersCount": 10 +} +``` + +### 外发 + +你可以期望从 GoToSocial 接收到一个 `Question` 形式的投票,投票在 `Create` 或 `Update` 活动中作为对象属性传递。在 `Update` 活动的情形下,如果投票中除了 `votersCount`、`replies.totalItems` 或 `closed` 之外的任何内容发生了变化,那么就表明包裹的贴文以需要重新创建的方式进行了编辑,因此需要重置。你可以期望在以下时间收到这些活动: + +- "Create":刚刚创建了带有附加投票的贴文 + +- "Update":投票/投票人数发生了变化,或者投票刚刚结束 + +你可以期望的,由 GoToSocial 生成的 `Question` 可以在上面的伪 JSON 中看到。在此 JSON 中,"endTime" 字段将始终被设置(因为我们不支持创建无尽投票),而 "closed" 字段只有在投票结束时才会设置。 + +### 传入 + +GoToSocial 期望以与发出投票几乎相同的方式接收投票,在解析 `Question` 对象时采用略显宽容的规则。 + +- 如果提供 "closed" 而不提供 "endTime",那么这也将被视为 "endTime" 的值 + +- 如果既没有提供 "closed" 也没有 "endTime",则认为投票是永不结束的投票 + +- 任何情况下,若一个带有 `Question` 的 `Update` 活动提供了一个 `closed` 时间,而之前的活动没有提供,则假定投票刚刚关闭。这将在本站参与投票的用户的客户端触发通知 + +## 投票行动 + +为了联合投状态票,GoToSocial 使用特殊格式化 [ActivityStreams "Note" 类型](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note)。这被 ActivityPub 服务器广泛接受为联合投票的方式,仅将投票作为 "Create" 活动的 "Object" 附加对象。 + +GoToSocial 传输的 "Note" 类型(以及期望接收到的)包含内容有: +- "name": [确切的投票选项文本] +- "content": [未设置] +- "inReplyTo": [指向 AS Question 的 IRI] +- "attributedTo": [投票作者的 IRI] +- "to": [投票作者的 IRI] + +例如: + +```json +{ + "type": "Note", + "name": "选项 1", + "inReplyTo": "https://example.org/users/bobby_tables/statuses/123456", + "attributedTo": "https://sample.com/users/willy_nilly", + "to": "https://example.org/users/bobby_tables" +} +``` + +### 外发 + +你可以期望以上面特定描述形式接收到来自 GoToSocial 的投票。投票仅作为附属于 "Create" 活动的对象发送。 + +特别地,如上节所述,GoToSocial 会在 `name` 字段中提供选项文本,不设置 `content` 字段,在 `inReplyTo` 字段提供一个 IRI,指向你的实例上的带投票贴文。 + +以下是一个 `Create` 示例,其中用户 `https://sample.com/users/willy_nilly` 在用户 `https://example.org/users/bobby_tables` 创建的多选投票中投票: + +```json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "actor": "https://sample.com/users/willy_nilly", + "id": "https://sample.com/users/willy_nilly/activity#vote/https://example.org/users/bobby_tables/statuses/123456", + "object": [ + { + "attributedTo": "https://sample.com/users/willy_nilly", + "id": "https://sample.com/users/willy_nilly#01HEN2R65468ZG657C4ZPHJ4EX/votes/1", + "inReplyTo": "https://example.org/users/bobby_tables/statuses/123456", + "name": "纸巾", + "to": "https://example.org/users/bobby_tables", + "type": "Note" + }, + { + "attributedTo": "https://sample.com/users/willy_nilly", + "id": "https://sample.com/users/willy_nilly#01HEN2R65468ZG657C4ZPHJ4EX/votes/2", + "inReplyTo": "https://example.org/users/bobby_tables/statuses/123456", + "name": "金融时报", + "to": "https://example.org/users/bobby_tables", + "type": "Note" + } + ], + "published": "2021-09-11T11:45:37+02:00", + "to": "https://example.org/users/bobby_tables", + "type": "Create" +} +``` + +### 传入 + +GoToSocial 期望以与发送投票的几乎相同形式接收投票。即只会期望把投票作为 "Create" 活动的一部分接收。 + +特别地,GoToSocial 将 votes 识别为不同于其他 "Note" 对象,因为其包含一个 "name" 字段,省略 "content" 字段,且 "inReplyTo" 字段是指向带附有投票的贴文的 URI。 如果满足这些条件,GoToSocial 将把提供的 "Note" 视为格式不正确的贴文对象。 + +## 贴文删除 + +GoToSocial 允许用户删除他们创建的贴文。这些删除操作将会向其他实例进行联合,其他实例也应删除其缓存的贴文。 + +### 外发 + +当 GoToSocial 用户删除贴文时,服务器会向其他实例发送一个 `Delete` 活动。 + +`Delete` 活动的 `Object` 条目会设置为该贴文的 ActivityPub URI。 + +`to` 和 `cc` 将根据原始贴文的可见性以及任何被提及/回复的用户进行设置。 + +如果原始贴文不是私信,ActivityPub `Public` URI 将在 `to` 中注明。否则,只会涉及被提及和回复的用户。 + +在以下示例中,'admin' 用户删除了一篇公开贴文,其中提到了 'foss_satan' 用户: + +```json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "actor": "http://example.org/users/admin", + "cc": [ + "http://example.org/users/admin/followers", + "http://fossbros-anonymous.io/users/foss_satan" + ], + "object": "http://example.org/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0", + "to": "https://www.w3.org/ns/activitystreams#Public", + "type": "Delete" +} +``` + +在下一个示例中,'1happyturtle' 用户删除了一条原本发给 'the_mighty_zork' 用户的直接消息。 + +```json +{ + "@context": "https://www.w3.org/ns/activitystreams", + "actor": "http://example.org/users/1happyturtle", + "cc": [], + "object": "http://example.org/users/1happyturtle/statuses/01FN3VJGFH10KR7S2PB0GFJZYG", + "to": "http://somewhere.com/users/the_mighty_zork", + "type": "Delete" +} +``` + +要处理从 GoToSocial 实例发出的 `Delete` 活动,外站实例应检查其是否根据提供的 URI 存储了 `Object`。如果有,它们应从本站缓存中删除该对象。如果没有,那么无需执行任何操作,因为它们从未存储过现在被删除的贴文。 + +### 接收 + +GoToSocial 尽可能彻底地处理来自外站实例的 `Delete` 活动,以尊重其他用户的隐私。 + +当 GoToSocial 实例收到 `Delete` 时,它会尝试从 `Object` 字段中提取被删除的贴文 URI。如果 `Object` 仅是一个 URI,则使用该 URI。如果 `Object` 是一个 `Note` 或其他常用于表示贴文的类型,则会从中提取 URI。 + +然后,GoToSocial 将检查其是否存储了具有给定 URI 的贴文。如果有,它将在数据库和所有用户时间线上完全删除。 + +GoToSocial 仅在确认原帖是被 `Delete` 所属的 `actor` 所拥有的情况下才会删除对应贴文。 + +## 贴文串 + +由于去中心化和联合的特性,Fediverse 上的任何一个服务器几乎不可能知道给定贴文串中的每篇贴文。 + +即便如此,也可以尽力对贴文串进行解引用,从不同的外站实例拉取回复,以更充分地展现整个对话。 + +GoToSocial 通过在对话贴文串上下迭代,尽可能获取外站贴文,来实现这一点。 + +假设我们有两个账户:`local_account` 在 `our.server` 上,`remote_1` 在 `remote.1` 上。 + +在这种情况下,`local_account` 关注了 `remote_1`,所以 `remote_1` 的贴文会出现在 `local_account` 的主页时间线上。 + +现在,`remote_1` 转发/转贴了来自第三方账户 `remote_2` 的一篇贴文,该账户在服务器 `remote.2` 上。 + +`local_account` 未关注 `remote_2`,`our.server` 上也没有其他人关注,因此 `our.server` 未曾见过 `remote_2` 的这篇贴文。 + +![贴文串的示意图,展示了来自 remote_2 的贴文,以及可能的祖先和后代贴文](../public/diagrams/conversation_thread.png) + +此时,GoToSocial 会对 `remote_2` 的贴文进行“解引用”,检查其是否属于某个贴文串,以及贴文串的其他部分是否可以获取。 + +GtS 首先检查贴文的 `inReplyTo` 属性,该属性在贴文回复其他贴文时设置。[见此处](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-inreplyto)。如果设置了 `inReplyTo`,GoToSocial 会解引用被回复的贴文。如果 *这篇* 贴文也设置了 `inReplyTo`,那么 GoToSocial 也会对此进行解引用,如此反复。 + +一旦获取到贴文的所有 **祖先** 后,GtS 将开始处理贴文的 **后代**。 + +这种情况下通过检查解引用贴文的 `replies` 属性,依次处理回复及回复的回复。[见此处](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies)。 + +这个贴文串解引用的过程可能需要进行多次 HTTP 请求到不同的服务器,尤其是在贴文串长且复杂的情况下。 + +解引用的最终结果是,假设由 `remote_2` 转发的贴文属于一个贴文串,那么 `local_account` 在主页时间线上打开贴文时,现在应该能够看到贴文串中的贴文。换句话说,他们将看到来自其他服务器账户的回复(他们可能尚未相遇),以及由 `remote_2` 发布的贴文串之前和之后的贴文。 + +这为 `local_account` 提供更完整的对话视图,而不仅仅是孤立和断章取义地看到被转发的贴文。此外,这还为 `local_account` 提供了根据对 `remote_2` 的回复发现新账户以进行关注的机会。 diff --git a/docs/locales/zh/federation/ratelimiting.md b/docs/locales/zh/federation/ratelimiting.md new file mode 100644 index 000000000..f724b0c18 --- /dev/null +++ b/docs/locales/zh/federation/ratelimiting.md @@ -0,0 +1,7 @@ +# 请求限流与速率限制 + +GoToSocial 对 ActivityPub API 端点(收件箱、用户端点、表情符号等)应用了 HTTP 请求限流和速率限制。 + +这可确保外站服务器不能用虚假请求淹没 GoToSocial 实例。外站服务器在对 ActivityPub API 端点进行 GET 或 POST 请求时,应尊重 429 和 503 HTTP 状态码,并考虑 `retry-after` HTTP 响应头。 + +有关请求限流和速率限制行为的更多详细信息,请参阅 [限流](../api/throttling.md) 和 [速率限制](../api/ratelimiting.md) 文档。 diff --git a/docs/locales/zh/getting_started/index.md b/docs/locales/zh/getting_started/index.md new file mode 100644 index 000000000..68c948f11 --- /dev/null +++ b/docs/locales/zh/getting_started/index.md @@ -0,0 +1,185 @@ +# 部署注意事项 + +在部署 GoToSocial 之前,有几个关键点需要你仔细考虑,因为这些选择将影响你如何运行和管理 GoToSocial。 + +!!! danger + + 在同一域名上切换不同实现是不被 Fediverse 支持的。这意味着如果你在 example.org 上运行 GoToSocial,而尝试切换到其他实现如 Pleroma/Akkoma、Misskey/Calckey 等,你会遇到联合问题。 + + 同理,如果你已经在 example.org 上运行了其他 ActivityPub 实现,就不应该尝试在那个域名上切换到 GoToSocial。 + +## 服务器 / VPS 系统要求 + +GoToSocial 致力于为在小型设备上运行的使用场景优化,因此我们尽量确保系统需求处于合理低位,同时仍提供人们期望的社交媒体服务器功能。 + +下面是系统需求的详细信息,但简而言之,你应该选择至少有 **1 个 CPU 核心**、大约 **1GB 内存**(可能会因操作系统而异),和 **15GB-20GB 的存储空间**(一般可满足前几年的使用)。 + +### 内存 + +**简短结论:系统上总共 1GB 的 RAM 应该足够,但你可能降至 512MB 也能运行。** + +对于小型站点(1-20 个活跃用户),GoToSocial 在内部缓存被填满的情况下,大约会占用 250MB 到 350MB 的 RAM: + +在上图中,你可以看到 RAM 的使用会在负载期间突增。这种情况会在例如某个贴文被一个拥有众多粉丝的人转发时,或嵌入的 `ffmpeg` 二进制文件在解码或重新编码媒体文件为缩略图时(特别是较大视频文件)出现。 + +你应该在这种情况下预留一些余量,若有需要,可以[配置一些交换内存](#交换内存)。 + +!!! tip + 在内存受限的环境中,你可以将 `cache.memory-target` 设置为低于默认的 100MB (查看数据库配置选项[这里](../configuration/database.md#settings))。设置为 50MB 已被证明可以正常运行。 + + 这将使总内存使用稍微降低,但代价是某些请求的延迟略高,因为 GtS 需要更频繁地访问数据库。 + +!!! info "为什么 `htop` 显示的内存使用比图中高?" + 如果你在服务器上运行 `top` 或 `htop` 或其他系统资源监测工具,GoToSocial 显示的保留内存可能比图中高。然而,这并不总是反映 GoToSocial 实际*使用*的内存。这种差异是由于 Go 运行时会比较保守地将内存释放回操作系统,因为与立即释放并在稍后需要时重新请求相比,保留空闲内存通常更划算。 + +### CPU + +**简短结论:1 个不错的 CPU 核心应该足够。** + +CPU 负载主要在处理媒体时(尤其是编码 blurhash)和/或同时处理大量联合请求时较高。只要不在同一台机器上运行其他 CPU 密集型任务,1 个 CPU 核心就能胜任。 + +### 存储 + +**简短结论:15GB-20GB 可用存储空间应足够使用几年。** + +GoToSocial 使用存储来保存其数据库文件,以及存储和服务媒体文件,例如头像和附件等。你可以[配置 GtS 使用 S3 存储桶来存储媒体](../configuration/storage.md)。 + +对于媒体存储,以及[缓存的外站媒体文件存储](../admin/media_caching.md),你应该预算大约 5GB-10GB 的空间。GoToSocial 会自动执行自我清理,在一段时间后从缓存中删除未使用的外站媒体。如果存储空间是个问题,你可以[调整媒体清理行为](../admin/media_caching.md#清理)以更频繁地清理和/或减少外站媒体的缓存时间。 + +!!! info + 如果你的 sqlite.db 文件或 Postgres 容量在一开始增长很快,请不要惊慌,这是正常的。当你首次部署实例并开始联合时,你的实例会迅速发现并存储来自其他实例的账号和贴文。然而,随着实例的长期部署,这种增长会逐渐减缓,因为你会自然而然地看到更少的新账号(即,你的实例尚未见过并因此尚未在数据库中存储的账号)。 + +### 单板计算机 + +GoToSocial 的轻量系统要求意味着它在配置良好的单板计算机上运行良好。如果在单板计算机上运行,你应该确保 GoToSocial 使用 USB 驱动器(最好是 SSD)来存储其数据库文件和媒体,而不是 SD 卡存储,因为后者通常太慢,不适合运行数据库。 + +### VPS + +如果你决定使用 VPS,可以为自己建立一个便宜的运行 Linux 的服务器。大多数每月 €2-€5 的 VPS 能够为个人 GoToSocial 实例提供出色的性能。 + +[Hostwinds](https://www.hostwinds.com/) 是一个不错的选择:价格便宜,而且他们免费提供静态 IP 地址。 + +[Greenhost](https://greenhost.net) 也是一个好选择:它完全无 CO2 排放,但价格稍高。他们的 1GB、1 个 CPU 的 VPS 对于单个用户或小型实例来说效果很好。 + +!!! warning "云存储卷" + 并非所有的云 VPS 存储都相同,声称基于 SSD 的存储并不一定适合作为 GoToSocial 实例的运行环境。 + + [Hetzner 云卷的性能](https://github.com/superseriousbusiness/gotosocial/issues/2471#issuecomment-1891098323)没有保证,且延迟波动较大。这会导致你的 GoToSocial 实例表现不佳。 + +!!! danger "Oracle 免费套餐" + 如果你打算与多个其他实例和用户联合,[Oracle 云免费套餐](https://www.oracle.com/cloud/free/) 服务器不适合用于 GoToSocial 部署。 + + 在 Oracle 云免费套餐上运行的 GoToSocial 管理员报告说,他们的实例在中等负载期间非常慢或无响应。这很可能是由于内存或存储延迟,导致即使是简单的数据库查询也要很长时间才能运行。 + +### 发行版系统要求 + +请务必检查你的发行版的系统需求,特别是内存。许多发行版有基线要求,在不满足它们的系统上运行会造成问题,除非你进行进一步的调整和优化。 + +Linux: + +* [Arch Linux][archreq]: `512MB` RAM +* [Debian][debreq]: `786MB` RAM +* [Ubuntu][ubireq]: `1GB` RAM +* [RHEL 8+][rhelreq] 及其衍生版本: `1.5GB` RAM +* [Fedora][fedorareq]: `2GB` RAM + +BSD 家族的发行版在内存要求方面记录较少,但普遍预期 `128MB` 以上就足够。 + +[archreq]: https://wiki.archlinux.org/title/installation_guide +[debreq]: https://www.debian.org/releases/stable/amd64/ch02s05.en.html +[ubireq]: https://ubuntu.com/server/docs/installation +[rhelreq]: https://access.redhat.com/articles/rhel-limits#minimum-required-memory-3 +[fedorareq]: https://docs.fedoraproject.org/en-US/fedora/latest/release-notes/welcome/Hardware_Overview/#hardware_overview-specs + +## 数据库 + +GoToSocial 支持 SQLite 和 Postgres 作为数据库驱动。尽管理论上可以在 SQLite 和 Postgres 之间切换,但我们目前没有工具支持这项操作,因此你在开始时应慎重考虑数据库的选择。 + +SQLite 是默认的驱动,并已被证明在 1-30 用户范围内的实例表现出色。 + +!!! danger "网络存储上的 SQLite" + 不要将你的 SQLite 数据库放在外部存储上,无论是 NFS/Samba、iSCSI 卷,如 Ceph/Gluster,或者你的云供应商的网络卷存储解决方案。 + + 更多信息参见[网络存储上的 SQLite](../advanced/sqlite-networked-storage.md)。 + +如果你计划在一个实例上托管更多用户,你可能希望改用 Postgres,因为它提供了数据库集群和冗余的可能性,尽管这会增加一些复杂性。 + +无论你选择哪种数据库驱动,为了获得良好的性能,它们都应在快速、稳定的低延迟存储上运行。虽然可以在网络附加存储上运行数据库,但这会增加可变延迟和网络拥堵,还有源存储上的潜在 I/O 争用。 + +!!! tip + 请[备份你的数据库](../admin/backup_and_restore.md)。数据库包含实例和任何用户账户的加密密钥。如果丢失这些密钥,你将无法再次从同一域进行联合! + +## 域名 + +为了和其他实例进行联合,你需要一个域名,如 `example.org`。你可以通过任何域名注册商注册域名,例如 [Namecheap](https://www.namecheap.com/)。确保你选择的注册商也允许你管理 DNS 条目,以便将你的域指向运行 GoToSocial 实例的服务器 IP。 + +用户通常会出现在顶级域下,例如 `@me@example.org`,但这不是必须的。完全可以在 `@me@social.example.org` 下创建用户。很多人更喜欢在顶级域下创建用户,因为输入起来更短,但你可以使用任何你控制的(子域)。 + +可以拥有形如 `@me@example.org` 的用户名,但让 GoToSocial 运行在 `social.example.org`。这通过区分 API 域(称为“实例域名”)和用户名用的域(称为“账号域名”)来实现。 + +如果你打算这样部署 GoToSocial 实例,请阅读[分域部署](../advanced/host-account-domain.md)文档以了解详细信息。 + +!!! danger + 无法在联合已经事实发生后安全地更改实例域名和账号域名。这需要重新生成数据库,并在任何已联合的服务器造成混乱情况。一旦你的实例域名和账号域名设置好,便不可更改。 + +## TLS + +为了实现联合,你必须使用 TLS。大多数实现,包括 GoToSocial,通常会拒绝通过未加密的传输进行联合。 + +GoToSocial 内置 Lets Encrypt 证书配置支持。它也可以从磁盘加载证书。如果你有连接到 GoToSocial 的反向代理,可以在代理层处理 TLS。 + +!!! tip + 请确保配置使用现代版本的 TLS,TLSv1.2 及更高版本,以确保服务器和客户端之间的通信安全。当 GoToSocial 处理 TLS 终端时,这会自动为你配置。如果使用反向代理,请使用 [Mozilla SSL 配置生成器](https://ssl-config.mozilla.org/)。 + +## 端口 + +GoToSocial 需要开放端口 `80` 和 `443`。 + +* `80` 用于 Lets Encrypt。因此,如果不使用内置的 Lets Encrypt 配置,则不需要开放。 +* `443` 用于通过 TLS 服务 API,并且是与其联合的任何实例尝试连接的端口。 + +如果你无法在机器上开放 `443` 和 `80` 端口,不要担心!你可以在 GoToSocial 中配置这些端口,但还需要配置端口转发,以将 `443` 和 `80` 上的流量准确转发到你选择的端口。 + +!!! tip + 你应该在机器上配置防火墙,并配置一些防范暴力 SSH 登录尝试的保护措施。参阅我们的[防火墙文档](../advanced/security/firewall.md)以获取配置建议和可帮助你的工具。 + +## 集群 / 多节点部署 + +GoToSocial 不支持[集群或任何形式的多节点部署](https://github.com/superseriousbusiness/gotosocial/issues/1749)。 + +尽管多个 GtS 实例可以使用相同的 Postgres 数据库和共享的本地存储或相同的对象桶,但 GtS 依赖于大量的内部缓存以保持高效。没有同步这些实例缓存的机制。没有它,你会得到各种奇怪和不一致的行为。不要这样做! + +## 调优 + +除了[示例配置文件](https://github.com/superseriousbusiness/gotosocial/blob/main/example/config.yaml)中的众多实例调优选项之外,你还可以对运行 GoToSocial 实例的机器进行额外的调优。 + +### 交换内存 + +除非你在进行这种调优并处理由不使用交换内存可能产生的问题方面有经验,否则你应该按照你的发行版本或主机供应商的建议配置适量的交换内存。如果你的发行版本或主机供应商没有提供指导,你可以使用以下经验法则为服务器配置交换内存: + +* 小于 2GB 的 RAM:交换内存 = RAM × 2 +* 大于 2GB 的 RAM:交换内存 = RAM,最高可达 8G + +!!! tip "配置交换内存活跃度" + Linux 的内存交换得很早。这在服务器上通常是不必要的,并且在数据库的情况下可能导致不必要的延迟。虽然在需要时让系统进行交换是好的,但可以通过告诉它在早期交换时保守一些来帮助提升性能。这个在 Linux 上的配置是通过更改 `vm.swappiness` [sysctl][sysctl] 值完成的。 + + 默认值是 `60`。你可以将其降低到 `10` 作为起点并留意观察。运行更低的值也是可能的,但这可能没有必要。要使该值持久化,你需要在 `/etc/sysctl.d/` 中放置一个配置文件。 + +虽然可以在没有交换内存的情况下运行系统,但为了安全地做到这一点并确保一致的性能和服务可用性,你需要相应调整内核、系统和工作负载。这需要对内核的内存管理系统及你所运行的工作负载的内存使用模式有良好的理解。 + +!!! tip + 交换内存用于确保内核可以高效地回收内存。这在系统没有经历内存争用时也很有用,比如在进程启动时仅使用过的内存腾出。这允许更多活跃使用的东西被缓存于内存中。内存交换不是让你的程序变慢的原因。内存争用才是造成缓慢的原因。 + +[sysctl]: https://man7.org/linux/man-pages/man8/sysctl.8.html + +### 内存和 CPU 限制 + +可以限制 GoToSocial 实例可以消耗的内存或 CPU 的数量。在 Linux 上可以使用 [CGroups v2 资源控制器][cgv2] 来做到这一点。 + +你可以使用 [systemd 资源控制设置][systemdcgv2]、[OpenRC cgroup 支持][openrccgv2] 或 [libcgroup CLI][libcg] 为进程配置限制。如果你想在系统经历内存压力时保护 GoToSocial,可以查看 [`memory.low`][cgv2mem]。 + +[cgv2]: https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html +[systemdcgv2]: https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html +[openrccgv2]: https://wiki.gentoo.org/wiki/OpenRC/CGroups +[libcg]: https://github.com/libcgroup/libcgroup/ +[cgv2mem]: https://docs.kernel.org/admin-guide/cgroup-v2.html#memory-interface-files diff --git a/docs/locales/zh/getting_started/installation/container.md b/docs/locales/zh/getting_started/installation/container.md new file mode 100644 index 000000000..43c526896 --- /dev/null +++ b/docs/locales/zh/getting_started/installation/container.md @@ -0,0 +1,164 @@ +# 容器 + +本指南将指引你通过我们发布的官方容器镜像运行 GoToSocial。在本例中,我们将直接使用 [Docker Compose](https://docs.docker.com/compose) 和 SQLite 作为数据库。 + +你也可以使用容器编排系统(如 [Kubernetes](https://kubernetes.io/) 或 [Nomad](https://www.nomadproject.io/))运行 GoToSocial,但这超出了本指南的范围。 + +## 创建工作目录 + +你需要一个工作目录来存放你的 docker-compose 文件,以及一个目录来存储 GoToSocial 的数据。请使用以下命令创建这些目录: + +```bash +mkdir -p ~/gotosocial/data +``` + +现在切换到你创建的工作目录: + +```bash +cd ~/gotosocial +``` + +## 获取最新的 docker-compose.yaml + +使用 `wget` 下载最新的 [docker-compose.yaml](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/example/docker-compose/docker-compose.yaml) 示例,我们将根据需要进行自定义: + +```bash +wget https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/example/docker-compose/docker-compose.yaml +``` + +## 编辑 docker-compose.yaml + +由于 GoToSocial 可以使用[环境变量](../../configuration/index.md#环境变量)进行配置,我们可以跳过在容器中挂载 config.yaml 文件,使配置更为简单。只需编辑 docker-compose.yaml 文件以更改一些内容。 + +首先在你的编辑器中打开 docker-compose.yaml 文件。例如: + +```bash +nano docker-compose.yaml +``` + +### 版本 + +如果需要,更新 GoToSocial Docker 镜像标签到你想要使用的 GtS 版本: + +* `latest`:默认值。这指向最新的稳定版本的 GoToSocial。 +* `snapshot`:指向当前在主分支上的代码。不保证稳定,可能经常出错。谨慎使用。 +* `vX.Y.Z`:发布标签。这指向 GoToSocial 的特定、稳定的版本。 + +!!! tip + `latest` 和 `snapshot` 标签是动态标签,而 `vX.Y.Z` 标签是固定的。拉取动态标签的结果可能每天都会变化。同一系统上的 `latest` 可能与不同系统上的 `latest` 不同。建议使用 `vX.Y.Z` 标签,以便你始终确切知道运行的是 GoToSocial 的哪个版本。发布列表可以在[这里](https://github.com/superseriousbusiness/gotosocial/releases)找到,最新的发布在顶部。 + +### 主机 + +更改 `GTS_HOST` 环境变量为你运行 GoToSocial 的域名。 + +### 服务器时区(可选但推荐) + +为确保你的 GoToSocial 服务器在贴文和日志中显示正确的时间,你可以通过编辑 `TZ` 环境变量设置服务器的时区。 + +1. 删除环境部分中 `TZ: UTC` 前面的 `#`。 +2. 将 `UTC` 部分更改为你的时区标识符。有关这些标识符的列表,请参阅 https://en.wikipedia.org/wiki/List_of_tz_database_time_zones。 + +例如,如果你在明斯克运行服务器,你可以设置 `TZ: Europe/Minsk`,日本设置为 `TZ: Japan`,迪拜设置为 `TZ: Asia/Dubai`,等等。 + +如果不设置,将使用默认的 `UTC`。 + +### 用户(可选/可能不必要) + +默认情况下,Docker 化的 GoToSocial 以 Linux 用户/组 `1000:1000` 运行,这在大多数情况下是可以的。如果你想以不同的用户/组运行,应相应地更改 docker-compose.yaml 中的 `user` 字段。 + +例如,假设你为 id 为 `1001` 的用户和组创建了 `~/gotosocial/data` 目录。如果现在不更改 `user` 字段就尝试运行 GoToSocial,将会遇到权限错误,无法在目录中打开数据库文件。在这种情况下,你需要将 docker compose 文件的 `user` 字段更改为 `1001:1001`。 + +### LetsEncrypt(可选) + +如果你想为 TLS 证书(https)使用 [LetsEncrypt](../../configuration/tls.md),你还应该: + +1. 将 `GTS_LETSENCRYPT_ENABLED` 的值更改为 `"true"`。 +2. 删除 `ports` 部分中 `- "80:80"` 前面的 `#`。 +3. (可选)将 `GTS_LETSENCRYPT_EMAIL_ADDRESS` 设置为有效的电子邮件地址,以接收证书过期警告等。 + +!!! info "可选配置" + + config.yaml 文件中记录了许多其他配置选项,你可以使用这些选项进一步自定义你的 GoToSocial 实例的行为。尽可能使用合理的默认设置,因此不一定需要立即对它们进行更改,但以下几个可能会感兴趣: + + - `GTS_INSTANCE_LANGUAGES`:确定你实例首选语言的 [BCP47 语言标签](https://en.wikipedia.org/wiki/IETF_language_tag)数组。 + - `GTS_MEDIA_REMOTE_CACHE_DAYS`:在存储中保持外站媒体缓存的天数。 + - `GTS_SMTP_*`:允许你的 GoToSocial 实例连接到电子邮件服务器并发送通知电子邮件的设置。 + + 如果你决定稍后设置/更改这些变量,请确保在更改后重新创建 GoToSocial 实例容器。 + + +!!! tip + + 有关将 config.yaml 文件中的变量名称转换为环境变量的帮助,请参阅[配置部分](../../configuration/index.md#environment-variables)。 + +### Wazero 编译缓存(可选) + +启动时,GoToSocial 会将嵌入的 WebAssembly `ffmpeg` 和 `ffprobe` 二进制文件编译为 [Wazero](https://wazero.io/) 兼容模块,用于媒体处理而无需任何外部依赖。 + +要加快 GoToSocial 的启动时间,你可以在重启之间缓存已编译的模块,这样 GoToSocial 就不必在每次启动时从头编译它们。 + +如果你希望在 Docker 容器中进行此操作,首先在工作文件夹中创建一个 `.cache` 目录以存储模块: + +```bash +mkdir -p ~/gotosocial/.cache +``` + +然后,取消注释 docker-compose.yaml 文件中第二个卷的前面的 `#` 符号,使其从 + +```yaml +#- ~/gotosocial/.cache:/gotosocial/.cache +``` + +变为 + +```yaml +- ~/gotosocial/.cache:/gotosocial/.cache +``` + +这将指示 Docker 在 Docker 容器中将 `~/gotosocial/.cache` 目录挂载到 `/gotosocial/.cache`。 + +## 启动 GoToSocial + +完成这些小改动后,您现在可以使用以下命令启动 GoToSocial: + +```shell +docker-compose up -d +``` + +运行此命令后,你应该会看到如下输出: + +```text +Creating network "gotosocial_gotosocial" with the default driver +Creating gotosocial ... done +``` + +如果你想跟踪 GoToSocial 的日志,可以使用: + +```bash +docker logs -f gotosocial +``` + +如果一切正常,你应该会看到类似以下的内容: + +```text +time=2022-04-19T09:48:35Z level=info msg=connected to SQLITE database +time=2022-04-19T09:48:35Z level=info msg=MIGRATED DATABASE TO group #1 (20211113114307, 20220214175650, 20220305130328, 20220315160814) func=doMigration +time=2022-04-19T09:48:36Z level=info msg=instance account example.org CREATED with id 01EXX0TJ9PPPXF2C4N2MMMVK50 +time=2022-04-19T09:48:36Z level=info msg=created instance instance example.org with id 01PQT31C7BZJ1Q2Z4BMEV90ZCV +time=2022-04-19T09:48:36Z level=info msg=media manager cron logger: start[] +time=2022-04-19T09:48:36Z level=info msg=media manager cron logger: schedule[now 2022-04-19 09:48:36.096127852 +0000 UTC entry 1 next 2022-04-20 00:00:00 +0000 UTC] +time=2022-04-19T09:48:36Z level=info msg=started media manager remote cache cleanup job: will run next at 2022-04-20 00:00:00 +0000 UTC +time=2022-04-19T09:48:36Z level=info msg=listening on 0.0.0.0:8080 +``` + +## 创建你的第一个用户 + +现在 GoToSocial 已在运行,你应该至少为自己创建一个用户。如何创建用户可以在我们的[创建用户](../user_creation.md)指南中找到。 + +### 完成 + +GoToSocial 现在应该在你的机器上运行!要验证这一点,打开浏览器,导航到你设置的 `GTS_HOST` 值。你应该会看到 GoToSocial 的登陆页面。干得不错! + +## (可选)反向代理 + +如果你想在 443 端口上运行其他网络服务器或想增加额外的安全层,你可能需要使用[反向代理](../reverse_proxy/index.md)。我们为几个流行的开源选项提供了指南,并乐意接受更多的拉取请求以增加新的指南。 diff --git a/docs/locales/zh/getting_started/installation/index.md b/docs/locales/zh/getting_started/installation/index.md new file mode 100644 index 000000000..d1160f3b8 --- /dev/null +++ b/docs/locales/zh/getting_started/installation/index.md @@ -0,0 +1,14 @@ +# 安装 + +正如我们在[版本发布](../releases.md)中提到的那样,我们发布了官方的二进制版本和容器版本。我们提供了一些指南,用于说明如何以这种方式部署你自己的 GoToSocial 实例。 + +在继续安装之前,请确保你已阅读[部署注意事项](../index.md),并准备好了域名和服务器。 + +另外,花一点时间熟悉[如何配置](../../configuration/index.md)GoToSocial。 + +## 指南 + +对于第三方发布版本,我们不提供使用指南。需要参考他们自己的文档。我们的指南可能仍然有助于你熟悉需要设置和调整哪些配置选项。 + +* [裸机](metal.md) +* [容器](container.md) diff --git a/docs/locales/zh/getting_started/installation/metal.md b/docs/locales/zh/getting_started/installation/metal.md new file mode 100644 index 000000000..0419e8cc4 --- /dev/null +++ b/docs/locales/zh/getting_started/installation/metal.md @@ -0,0 +1,164 @@ +# 裸机 + +本指南将引导你在裸机上使用官方二进制发行版来运行 GoToSocial。 + +## 准备 VPS + +在 VPS 或你的家庭服务器终端中,创建 GoToSocial 运行的目录,它将用作存储的目录,以及存放 LetsEncrypt 证书的目录。 + +这意味着我们需要以下目录结构: + +``` +. +└── gotosocial + └── storage + └── certs +``` + +你可以通过以下命令一步创建所有目录: + +```bash +mkdir -p /gotosocial/storage/certs +``` + +如果你在机器上没有 root 权限,请使用类似 `~/gotosocial` 的路径。 + +## 下载发行版 + +在 VPS 或你的家庭服务器终端中,进入你刚创建的 GoToSocial 根目录: + +```bash +cd /gotosocial +``` + +现在,下载与你运行的操作系统和架构相对应的最新 GoToSocial 发行版压缩包。 + +!!! tip + 你可以在[这里](https://github.com/superseriousbusiness/gotosocial/releases)找到按发布时间排列的发布列表,最新的发行版位于最上面。 + +例如,下载适用于 64 位 Linux 的版本: + +```bash +GTS_VERSION=X.Y.Z # 替换此处 +GTS_TARGET=linux_amd64 +wget https://github.com/superseriousbusiness/gotosocial/releases/download/v${GTS_VERSION}/gotosocial_${GTS_VERSION}_${GTS_TARGET}.tar.gz +``` + +然后解压: + +```bash +tar -xzf gotosocial_${GTS_VERSION}_${GTS_TARGET}.tar.gz +``` + +这将在你的当前目录放置 `gotosocial` 二进制文件,以及包含网页前端资源的 `web` 文件夹和包含示例配置文件的 `example` 文件夹。 + +!!! danger + 如果你想使用基于当前主分支代码的 GoToSocial 快照构建,可以从[这里](https://minio.s3.superseriousbusiness.org/browser/gotosocial-snapshots)下载最近的二进制 .tar.gz 文件(基于提交哈希)。仅在你很清楚自己的操作时使用,否则请使用稳定版。 + +## 编辑配置文件 + +基于 `example` 文件夹中的 `config.yaml` 创建一个新的配置文件。你可以复制整个文件,但请确保仅保留已更改的设置。这让检查发布升级时的配置变化更加容易。 + +你可能需要更改以下设置: + +- 设置 `host` 为你要运行服务器的域名(例如 `example.org`)。 +- 设置 `port` 为 `443`。 +- 设置 `db-type` 为 `sqlite`。 +- 设置 `db-address` 为 `sqlite.db`。 +- 设置 `storage-local-base-path` 为你上面创建的存储目录(例如 `/gotosocial/storage`)。 +- 设置 `letsencrypt-enabled` 为 `true`。 +- 设置 `letsencrypt-cert-dir` 为你上面创建的证书存储目录(例如 `/gotosocial/storage/certs`)。 + +上述选项假设你使用 SQLite 作为数据库。如果你想使用 Postgres,请参阅[这里](../../configuration/database.md)获取配置选项。 + +!!! info "可选配置" + + `config.yaml` 文件中记录了许多其他配置选项,可以进一步自定义你的 GoToSocial 实例的行为。这些选项在可能的情况下使用合理的默认值,因此现在不必对此进行任何更改,但以下是一些你可能感兴趣的选项: + + - `instance-languages`: 确定实例首选语言的 [BCP47 语言标签](https://en.wikipedia.org/wiki/IETF_language_tag)数组。 + - `media-remote-cache-days`: 保存在存储中外站媒体的缓存天数。 + - `smtp-*`: 允许你的 GoToSocial 实例连接到邮件服务器并发送通知邮件的设置。 + + 如果你决定稍后设置/更改这些变量,请确保在进行更改后重新启动你的 GoToSocial 实例。 + +## 运行二进制文件 + +你现在可以运行二进制文件了。 + +使用以下命令启动 GoToSocial 服务器: + +```bash +./gotosocial --config-path ./config.yaml server start +``` + +服务器应该现在启动,并且你应该能通过浏览器访问你的域名的启动页面。请注意,首次创建 LetsEncrypt 证书可能需要最多一分钟的时间,因此如有必要请多次刷新页面。 + +注意在本例中我们假设可以运行在端口 443(标准 HTTPS 端口),并且没有其他进程运行在该端口。 + +## 创建你的用户 + +你可以使用 GoToSocial 二进制文件来创建并提权你的用户账户。所有这些过程在我们的[创建用户](../user_creation.md)指南中均有记录。 + +## 登录 + +你现在应该可以使用刚创建的账户的电子邮件地址和密码登录到你的实例。 + +## (可选)启用 systemd 服务 + +如果你不喜欢每次启动时手动启动 GoToSocial,你可能希望创建一个 systemd 服务为你启动。 + +首先,停止你的 GoToSocial 实例。 + +然后为你的 GoToSocial 安装创建一个新用户和用户组: + +```bash +sudo useradd -r gotosocial +sudo groupadd gotosocial +sudo usermod -a -G gotosocial gotosocial +``` + +然后使其成为你的 GoToSocial 安装目录的所有者,因为它们需要在其中进行读写: + +```bash +sudo chown -R gotosocial:gotosocial /gotosocial +``` + +你可以在 [GitHub](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/example/gotosocial.service) 或你的安装文件夹中的 `example` 文件夹中找到一个 `gotosocial.service` 文件。 + +将它复制到 `/etc/systemd/system/gotosocial.service`: + +```bash +sudo cp /gotosocial/example/gotosocial.service /etc/systemd/system/ +``` + +然后使用 `sudoedit /etc/systemd/system/gotosocial.service` 在编辑器中打开文件。如果你在与本指南中使用的 `/gotosocial` 路径不同的目录中安装了 GoToSocial,请根据你的安装修改 `ExecStart=` 和 `WorkingDirectory=` 行。 + +!!! info "运行在端口 80 和 443" + + 如果你完全遵循本指南,你的 GoToSocial 实例将配置为绑定到端口 443 和 80,它们是已知的特权端口。要允许 GoToSocial 用户绑定到这些端口,你需要通过删除前导 `#` 来取消注释服务文件中的 `CAP_NET_BIND_SERVICE` 行。 + + 修改前: + + ``` + #AmbientCapabilities=CAP_NET_BIND_SERVICE + ``` + + 修改后: + + ``` + AmbientCapabilities=CAP_NET_BIND_SERVICE + ``` + + 如果你以后决定使用反向代理运行 GoToSocial(见下文),你可能希望重新注释此行以移除权限,因为反向代理将绑定到特权端口。 + +编辑完成后,保存并关闭文件,运行以下命令以启用服务: + +```bash +sudo systemctl enable --now gotosocial.service +``` + +GoToSocial 现在应该已启动并运行。 + +## (可选)反向代理 + +如果你想在端口 443 上运行其他网络服务器或想添加额外的安全层,你可能希望使用[反向代理](../reverse_proxy/index.md)。我们提供了几个流行开源选项的指南,并非常欢迎提供更多指南的 pull requests。 diff --git a/docs/locales/zh/getting_started/releases.md b/docs/locales/zh/getting_started/releases.md new file mode 100644 index 000000000..01dbdbb94 --- /dev/null +++ b/docs/locales/zh/getting_started/releases.md @@ -0,0 +1,11 @@ +# 发行版 + +GoToSocial 可以通过多种方式进行安装。我们发布官方的二进制文件以及容器镜像。 + +有许多第三方软件包由不同的发行版维护,一些用户还创建了额外的部署工具,以便你可以轻松地自行部署 GoToSocial。 + +{% + include "../repo/README.md" + start='' + end='' +%} \ No newline at end of file diff --git a/docs/locales/zh/getting_started/reverse_proxy/apache-httpd.md b/docs/locales/zh/getting_started/reverse_proxy/apache-httpd.md new file mode 100644 index 000000000..f371178bc --- /dev/null +++ b/docs/locales/zh/getting_started/reverse_proxy/apache-httpd.md @@ -0,0 +1,261 @@ +# Apache HTTP 服务器 + +要将 Apache 用作 GoToSocial 的反向代理,你需要在服务器上安装它。如果你还希望 Apache 处理 TLS,就需要[配置 TLS 证书](../../advanced/certificates.md)。 + +Apache 已被[打包用于许多发行版](https://repology.org/project/apache/versions)。你很可能可以使用发行版的包管理器来安装它。你还可以使用发布到 Docker Hub 的[官方 Apache 镜像](https://hub.docker.com/_/httpd)通过容器运行 Apache。 + +本指南还将展示如何使用 certbot 来配置 TLS 证书。它同样被[打包在许多发行版](https://repology.org/project/certbot/versions)中,但许多发行版往往附带较旧版本的 certbot。如果遇到问题,可以考虑使用[容器镜像](https://hub.docker.com/r/certbot/certbot)。 + +## 配置 GoToSocial + +我们将让 Apache 处理 LetsEncrypt 证书,所以你需要在 GoToSocial 配置中关闭内置的 LetsEncrypt 支持。 + +首先在文本编辑器中打开文件: + +```bash +sudoedit /gotosocial/config.yaml +``` + +然后设置 `letsencrypt-enabled: false`。 + +如果反向代理将在同一台机器上运行,请将 `bind-address` 设置为 `"localhost"`,这样 GoToSocial 服务器仅通过环回才可以访问。否则可能会直接连接到 GoToSocial 以绕过你的代理,这是我们不希望的。 + +如果 GoToSocial 已经在运行,请重启它。 + +```bash +sudo systemctl restart gotosocial.service +``` + +或者,如果你没有配置 systemd 服务,只需手动重启。 + +## 设置 Apache + +### 所需的 Apache 模块 + +你需要确保安装并启用了多个 Apache 模块。这些模块应该都在你的发行版的 Apache 包中,但可能被拆分成单独的包。 + +你可以通过 `apachectl -M` 查看已安装哪些模块。 + +你需要加载以下模块: + +* `proxy_http` 来代理 HTTP 请求到 GoToSocial +* `ssl` 来处理 SSL/TLS +* `headers` 来操作 HTTP 请求和响应头 +* `rewrite` 来重写 HTTP 请求 +* `md` 用于 Lets Encrypt,自 2.4.30 开始可用 + +在 Debian、Ubuntu 和 openSUSE 中,可以使用 [`a2enmod`](https://manpages.debian.org/bookworm/apache2/a2enmod.8.en.html) 工具加载任何额外的模块。对于 Red Hat/CentOS 系列发行版,你需要在 Apache 配置中添加 [`LoadModule` 指令](https://httpd.apache.org/docs/2.4/mod/mod_so.html#loadmodule)。 + +### 使用 mod_md 启用 TLS + +!!! note + `mod_md` 自 Apache 2.4.30 开始可用,仍被视为实验性的。实际上,它在实践中表现良好,是最便捷的方法。 + +现在我们将配置 Apache HTTP 服务器来处理 GoToSocial 请求。 + +首先,我们将在 `/etc/apache2/sites-available` 中为 Apache HTTP 服务器编写配置: + +```bash +sudo mkdir -p /etc/apache2/sites-available/ +sudoedit /etc/apache2/sites-available/example.com.conf +``` + +在上述 `sudoedit` 命令中,将 `example.com` 替换为你的 GoToSocial 服务器的域名。 + +你将创建的文件应如下所示: + +=== "2.4.47+" + ```apache + MDomain example.com auto + MDCertificateAgreement accepted + + + ServerName example.com + + + + ServerName example.com + + SSLEngine On + ProxyPreserveHost On + # 设置为 127.0.0.1 而不是 localhost 以解决 https://stackoverflow.com/a/52550758 + ProxyPass / http://127.0.0.1:8080/ upgrade=websocket + ProxyPassReverse / http://127.0.0.1:8080/ + + RequestHeader set "X-Forwarded-Proto" expr=https + + ``` + +=== "旧版本" + ```apache + MDomain example.com auto + MDCertificateAgreement accepted + + + ServerName example.com + + + + ServerName example.com + + RewriteEngine On + RewriteCond %{HTTP:Upgrade} websocket [NC] + RewriteCond %{HTTP:Connection} upgrade [NC] + # 设置为 127.0.0.1 而不是 localhost 以解决 https://stackoverflow.com/a/52550758 + RewriteRule ^/?(.*) "ws://127.0.0.1:8080/$1" [P,L] + + SSLEngine On + ProxyPreserveHost On + # 设置为 127.0.0.1 而不是 localhost 以解决 https://stackoverflow.com/a/52550758 + ProxyPass / http://127.0.0.1:8080/ + ProxyPassReverse / http://127.0.0.1:8080/ + + RequestHeader set "X-Forwarded-Proto" expr=https + + ``` + +同样,将上述配置文件中的 `example.com` 替换为你的 GoToSocial 服务器的域名。如果你的域名是 `gotosocial.example.com`,那么用 `gotosocial.example.com` 作为正确的值。 + +你还应该将 `http://127.0.0.1:8080` 更改为 GoToSocial 服务器的正确地址和端口(如果它不在 `127.0.0.1:8080` 上)。例如,如果你在另一台机器上以 `192.168.178.69` 的本地 IP 运行 GoToSocial,并且端口为 `8080`,那么 `http://192.168.178.69:8080/` 就是正确的值。 + +需要 `Rewrite*` 指令以确保 Websocket 流连接正常工作。有关更多信息,请参阅 [websocket](./websocket.md) 文档。 + +`ProxyPreserveHost On` 是必要的:它保证代理和 GoToSocial 使用相同的服务器名称。否则,GoToSocial 会构建错误的身份验证标头,所有联合尝试将被拒绝并返回 401 未授权。 + +默认情况下,Apache 会在转发的请求中设置 `X-Forwarded-For`。为了使这个设置和限速工作,设置 `trusted-proxies` 配置变量。请参阅[限速](../../api/ratelimiting.md)和[基础配置](../../configuration/general.md)文档。 + +保存并关闭配置文件。 + +现在,我们需要将刚创建的文件链接到 Apache HTTP 服务器读取已激活站点配置的文件夹中。 + +```bash +sudo mkdir /etc/apache2/sites-enabled +sudo ln -s /etc/apache2/sites-available/example.com.conf /etc/apache2/sites-enabled/ +``` + +在上述 `ln` 命令中,将 `example.com` 替换为你的 GoToSocial 服务器的域名。 + +现在检查配置错误。 + +```bash +sudo apachectl -t +``` + +如果一切正常,你应该看到以下输出: + +```text +Syntax OK +``` + +一切正常?太好了!然后重启 Apache HTTP 服务器以加载新的配置文件。 + +```bash +sudo systemctl restart apache2 +``` + +现在,观测日志以查看新 LetsEncrypt 证书何时送达(`tail -F /var/log/apache2/error.log`),然后使用上述 `systemctl restart` 命令再次重载 Apache。之后,你应该就可以开始了! + +每当 `mod_md` 获取新证书时,需要重启(或重载)Apache HTTP 服务器;请参阅该模块的文档以了解[更多信息](https://github.com/icing/mod_md#how-to-manage-server-reloads)。 + +根据你使用的 Apache HTTP 服务器版本,可能会看到以下错误:`error (specific information not available): acme problem urn:ietf:params:acme:error:invalidEmail: Error creating new account :: contact email "webmaster@localhost" has invalid domain : Domain name needs at least one dot` + +如果发生这种情况,你需要进行以下操作之一(或全部): + +1. 更新 `/etc/apache2/sites-enabled/000-default.conf` 并将 `ServerAdmin` 值更改为有效的电子邮件地址(然后重载 Apache HTTP 服务器)。 +2. 在 `/etc/apache2/sites-available/example.com.conf` 的 `MDomain` 行下添加行 `MDContactEmail your.email.address@whatever.com`,将 `your.email.address@whatever.com` 替换为有效的电子邮件地址,并将 `example.com` 替换为你的 GoToSocial 域名。 + +### 使用外部管理证书启用 TLS + +!!! note + 我们有关于如何[配置 TLS 证书](../../advanced/certificates.md)的额外文档,其中还提供了不同发行版的其他内容和教程链接,可能值得查看。 + +如果你更喜欢手动设置或使用不同服务(如 Certbot)来管理 SSL,可以为你的 Apache HTTP 服务器使用更简单的设置。 + +首先,我们将在 `/etc/apache2/sites-available` 中为 Apache HTTP 服务器编写配置: + +```bash +sudo mkdir -p /etc/apache2/sites-available/ +sudoedit /etc/apache2/sites-available/example.com.conf +``` + +在上述 `sudoedit` 命令中,将 `example.com` 替换为你的 GoToSocial 服务器的域名。 + +你将创建的文件最初应如下所示,针对 80(必需)和 443 端口(可选): + +=== "2.4.47+" + ```apache + + ServerName example.com + + ProxyPreserveHost On + # 设置为 127.0.0.1 而不是 localhost 以解决 https://stackoverflow.com/a/52550758 + ProxyPass / http://127.0.0.1:8080/ upgrade=websocket + ProxyPassReverse / http://127.0.0.1:8080/ + + ``` + +=== "旧版本" + ```apache + + ServerName example.com + + RewriteEngine On + RewriteCond %{HTTP:Upgrade} websocket [NC] + RewriteCond %{HTTP:Connection} upgrade [NC] + # 设置为 127.0.0.1 而不是 localhost 以解决 https://stackoverflow.com/a/52550758 + RewriteRule ^/?(.*) "ws://127.0.0.1:8080/$1" [P,L] + + ProxyPreserveHost On + # 设置为 127.0.0.1 而不是 localhost 以解决 https://stackoverflow.com/a/52550758 + ProxyPass / http://127.0.0.1:8080/ + ProxyPassReverse / http://127.0.0.1:8080/ + + + ``` + +同样,将上述配置文件中的 `example.com` 替换为你的 GoToSocial 服务器的域名。如果你的域名是 `gotosocial.example.com`,那么用 `gotosocial.example.com` 作为正确的值。 + +你还应该将 `http://127.0.0.1:8080` 更改为 GoToSocial 服务器的正确地址和端口(如果它不在 `127.0.0.1:8080` 上)。例如,如果你在另一台机器上以 `192.168.178.69` 的本地 IP 运行 GoToSocial,并且端口为 `8080`,那么 `http://192.168.178.69:8080/` 就是正确的值。 + +需要 `Rewrite*` 指令以确保 Websocket 流连接正常工作。有关更多信息,请参阅 [websocket](websocket.md) 文档。 + +`ProxyPreserveHost On` 是必需的:它保证代理和 GoToSocial 使用相同的服务器名称。否则,GoToSocial 会构建错误的身份验证头,所有联合尝试将被拒绝并返回 401 未授权。 + +在443端口提供初始设置以供外部工具进行附加管理时,你可以使用服务器提供的默认证书,你可以在 `/etc/apache2/sites-available/` 的 `default-ssl.conf` 文件中找到引用。 + +保存并关闭配置文件。 + +现在,我们需要将刚创建的文件链接到 Apache HTTP 服务器读取已激活站点配置的文件夹中。 + +```bash +sudo mkdir /etc/apache2/sites-enabled +sudo ln -s /etc/apache2/sites-available/example.com.conf /etc/apache2/sites-enabled/ +``` + +在上述 `ln` 命令中,将 `example.com` 替换为你的 GoToSocial 服务器的域名。 + +现在检查配置错误。 + +```bash +sudo apachectl -t +``` + +如果一切正常,你应该看到以下输出: + +```text +Syntax OK +``` + +一切正常?太好了!然后重启 Apache HTTP 服务器以加载新的配置文件。 + +```bash +sudo systemctl restart apache2 +``` + +## 故障排除 + +如果无法在浏览器中连接到站点,则反向代理设置不起作用。比较 Apache 日志文件(`tail -F /var/log/apache2/access.log`)和 GoToSocial 日志文件。发出的请求必须在两个地方中都显示出来。仔细检查 `ProxyPass` 设置。 + +如果可以连接,但贴文未能联合且账户无法从其他地方找到,请检查日志。如果你看到尝试读取你的个人资料(比如 `level=INFO … method=GET statusCode=401 path=/users/your_username msg="Unauthorized: …"`)或向你的收件箱发送贴文的信息(比如 `level=INFO … method=POST statusCode=404 path=/your_username/inbox msg="Not Found: …"`),则联合已被中断。仔细检查 `ProxyPreserveHost` 设置。 + +如果可以连接但无法在 Mastodon 客户端应用中授权账户,请确保从正确的域名启动登录。当使用[分域](../../advanced/host-account-domain.md)设置时,必须从 `host` 域启动登录,而不是 `account-domain`。GoToSocial 设置了 `Content-Security-Policy` 头,以抵御 XSS 和数据注入攻击。该头应保持不变,确保你的反向代理没有修改、覆盖或取消设置它。 diff --git a/docs/locales/zh/getting_started/reverse_proxy/caddy.md b/docs/locales/zh/getting_started/reverse_proxy/caddy.md new file mode 100644 index 000000000..3dec6f44b --- /dev/null +++ b/docs/locales/zh/getting_started/reverse_proxy/caddy.md @@ -0,0 +1,110 @@ +# Caddy 2 + +## 要求 + +在此指南中,你需要使用 [Caddy 2](https://caddyserver.com/),无需其他依赖。Caddy 管理 Lets Encrypt 证书及其续订。 + +Caddy 可以通过大多数流行的包管理器获取,或者你可以获取一个静态二进制文件。最新的安装指南请参考[他们的手册](https://caddyserver.com/docs/install)。 + +### Debian, Ubuntu, Raspbian + +```bash +# 为其自定义仓库添加密钥环。 +sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list + +# 更新软件包并安装 +sudo apt update +sudo apt install caddy +``` + +### Fedora, Redhat, Centos + +```bash +dnf install 'dnf-command(copr)' +dnf copr enable @caddy/caddy +dnf install caddy +``` + +### Arch + +```bash +pacman -Syu caddy +``` + +### FreeBSD + +```bash +sudo pkg install caddy +``` + +## 配置 GoToSocial + +如果 GoToSocial 已经在运行,先停止它。 + +```bash +sudo systemctl stop gotosocial +``` + +在你的 GoToSocial 配置中,通过将 `letsencrypt-enabled` 设置为 `false` 来关闭 Lets Encrypt。 + +如果你之前在 443 端口运行 GoToSocial,需将 `port` 值改回默认的 `8080`。 + +如果反向代理将在同一台机器上运行,将 `bind-address` 设置为 `"localhost"`,这样 GoToSocial 服务器只能通过回环地址访问。否则可能会有人直接连接到 GoToSocial 以绕过你的代理,这是不安全的。 + +## 设置 Caddy + +我们将配置 Caddy 2 来在主域名 example.org 上使用 GoToSocial。由于 Caddy 负责获取 Lets Encrypt 证书,我们只需正确配置它一次。 + +在最简单的使用场景中,Caddy 默认使用名为 Caddyfile 的文件。它可以在更改时重新加载,或者通过 HTTP API 配置以实现零停机,但这超出了我们当前的讨论范围。 + +```bash +sudo mkdir -p /etc/caddy +sudo vim /etc/caddy/Caddyfile +``` + +在编辑上述文件时,你应将 'example.org' 替换为你的域名。你的域名应在当前配置中出现两次。如果你为 GoToSocial 选择了端口号 8080 以外的端口,请在反向代理行中更改端口号以匹配它。 + +你即将创建的文件应如下所示: + +```Caddyfile +example.org { + # 可选,但推荐,使用适当的协议压缩流量 + encode zstd gzip + + # 实际的代理配置为端口 8080(除非你选择了其他端口号) + reverse_proxy * http://127.0.0.1:8080 { + # 立即刷新,以防止缓冲响应给客户端 + flush_interval -1 + } +} +``` + +默认情况下,caddy 在转发请求中设置 `X-Forwarded-For`。为了使其与速率限制配合使用,请设置 `trusted-proxies` 配置变量。详见[速率限制](../../api/ratelimiting.md)和[通用配置](../../configuration/general.md)文档。 + +有关进阶配置,请查看 Caddy 文档中的[反向代理指令](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy)。 + +现在检查配置错误。 + +```bash +sudo caddy validate +``` + +如果一切正常,你将看到一些信息行作为输出。除非前面标有 *[err]* 的行,否则你就准备好了。 + +一切正常吗?太好了!然后重启 caddy 以加载你的新配置文件。 + +```bash +sudo systemctl restart caddy +``` + +如果一切顺利,你现在就可以享受你的 GoToSocial 实例,所以我们将再次启动它。 + +```bash +sudo systemctl start gotosocial +``` + +## 结果 + +你现在应该能够在浏览器中打开你的实例的启动页面,并会看到它在 HTTPS 下运行! diff --git a/docs/locales/zh/getting_started/reverse_proxy/index.md b/docs/locales/zh/getting_started/reverse_proxy/index.md new file mode 100644 index 000000000..c0aa0df4f --- /dev/null +++ b/docs/locales/zh/getting_started/reverse_proxy/index.md @@ -0,0 +1,43 @@ +# 反向代理 + +GoToSocial 可以直接暴露到互联网上。不过,许多人更愿意使用反向代理来处理外部连接。这也可以使你对 TLS 配置有更大的控制权,并启用一些更复杂的场景,比如资源缓存。 + +## 一般步骤 + +要使用反向代理,通常需要做以下几件事: + +* 配置某种方式获取主机域名的 TLS 证书 +* 将 GoToSocial 绑定到一个本地 IP 而不是公网 IP,并使用非特权端口。调整 `bind-address` 和 `port` 配置选项 +* 如果你使用了 Lets Encrypt,在 GoToSocial 中禁用它。将 `letsencrypt-enabled` 设置为 `false` +* 配置反向代理以处理 TLS 并将请求代理到 GoToSocial + +!!! warning + 不要更改 `host` 配置选项的值。这必须保持为其他实例在互联网上看到的实际域名。相反,改变 `bind-address` 并更新 `port` 和 `trusted-proxies`。 + +### 容器 + +当你使用我们的[Docker Compose 示例指南](../installation/container.md)部署 GoToSocial 时,它默认绑定到端口 `443`,假设你希望直接将其暴露到互联网上。要在反向代理后运行它,你需要更改这些设置。 + +在 Compose 文件中: + +* 注释掉 `ports` 定义中的 `- "443:8080"` 行 +* 如果你启用了 Lets Encrypt 支持: + * 注释掉 `ports` 定义中的 `- "80:80"` 行 + * 将 `GTS_LETSENCRYPT_ENABLED` 设置回 `"false"` 或注释掉 +* 改为取消注释 `- "127.0.0.1:8080:8080"` 行 + +这将导致 Docker 仅在 `127.0.0.1` 的端口 `8080` 上转发连接到容器,有效地将其与外界隔离。你现在可以指示反向代理将请求发送到那里。 + +## 指南 + +我们为以下服务器提供了指南: + +* [nginx](nginx.md) +* [Apache httpd](apache-httpd.md) +* [Caddy 2](caddy.md) + +## WebSockets + +使用反向代理时,必须特别注意允许 WebSockets 正常工作。因为许多客户端应用程序使用 WebSockets 来流式传输你的时间线。WebSockets 不用于联合。 + +请确保阅读 [WebSocket](websocket.md) 文档,并相应地配置你的反向代理。 diff --git a/docs/locales/zh/getting_started/reverse_proxy/nginx.md b/docs/locales/zh/getting_started/reverse_proxy/nginx.md new file mode 100644 index 000000000..62b36f7a8 --- /dev/null +++ b/docs/locales/zh/getting_started/reverse_proxy/nginx.md @@ -0,0 +1,186 @@ +# NGINX + +要使用 NGINX 作为 GoToSocial 的反向代理,你需要在服务器上安装它。如果你打算让 NGINX 处理 TLS,你还需要[配置 TLS 证书](../../advanced/certificates.md)。 + +!!! tip + 通过在 `server` 块中包含 `http2 on;` 来启用 NGINX 的 HTTP/2。这样可以加快客户端的体验。请参阅 [ngx_http_v2_module 文档](https://nginx.org/en/docs/http/ngx_http_v2_module.html#example)。 + +NGINX 已为[多个发行版打包](https://repology.org/project/nginx/versions)。你很可能可以使用发行版的包管理器来安装它。你也可以使用 Docker Hub 上发布的[官方 NGINX 镜像](https://hub.docker.com/_/nginx)通过容器运行 NGINX。 + +在本指南中,我们还将展示如何使用 certbot 配置 TLS 证书。它也在[许多发行版中打包](https://repology.org/project/certbot/versions),但许多发行版往往提供的 certbot 版本较旧。如果遇到问题,可以考虑使用[容器镜像](https://hub.docker.com/r/certbot/certbot)。 + +## 配置 GoToSocial + +如果 GoToSocial 已在运行,先停止它。 + +```bash +sudo systemctl stop gotosocial +``` + +或者如果你没有 systemd 服务,只需手动停止它。 + +这样调整你的 GoToSocial 配置: + +```yaml +letsencrypt-enabled: false +port: 8080 +bind-address: 127.0.0.1 +``` + +第一个设置禁用了内置的 TLS 证书配置。由于 NGINX 现在将处理这些流量,GoToSocial 不再需要绑定到 443 端口或任何特权端口。 + +通过将 `bind-address` 设置为 `127.0.0.1`,GoToSocial 将不再能直接从外部访问。如果你的 NGINX 和 GoToSocial 实例不在同一台服务器上,你需要绑定一个允许你的反向代理访问你的 GoToSocial 实例的 IP 地址。绑定到私有 IP 地址可以确保只有通过 NGINX 才能访问 GoToSocial。 + +## 设置 NGINX + +我们首先设置 NGINX 为 GoToSocial 提供不安全的 http 服务,然后使用 Certbot 自动升级为 https 服务。 + +请勿在此完成之前尝试使用,否则你将有泄露密码的风险,或破坏联合。 + +首先,我们将为 NGINX 编写一个配置文件,并将其放入 `/etc/nginx/sites-available` 中。 + +```bash +sudo mkdir -p /etc/nginx/sites-available +sudoedit /etc/nginx/sites-available/yourgotosocial.url.conf +``` + +在上述命令中,将 `yourgotosocial.url` 替换为你的实际 GoToSocial 主机值。所以如果你的 `host` 设置为 `example.org`,那么文件应该命名为 `/etc/nginx/sites-available/example.org.conf` + +你要创建的文件应该如下所示: + +```nginx +server { + listen 80; + listen [::]:80; + server_name example.org; + location / { + # 设置为 127.0.0.1 而不是 localhost 以解决 https://stackoverflow.com/a/52550758 + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + } + client_max_body_size 40M; +} +``` + +将 `proxy_pass` 改为你实际运行 GoToSocial 的 IP 和端口(如果不是 `127.0.0.1:8080`),并将 `server_name` 改为你自己的域名。 + +如果你的域名是 `example.org`,那么 `server_name example.org;` 就是正确的值。 + +如果你在另一台本地 IP 为 192.168.178.69 的机器上运行 GoToSocial,并在端口 8080 上,那么 `proxy_pass http://192.168.178.69:8080;` 就是正确的值。 + +**注意**:如果你的服务器不支持 IPv6,可以删除 `listen [::]:80;` 这一行。 + +**注意**:`proxy_set_header Host $host;` 必不可少。它确保代理和 GoToSocial 使用相同的服务器名称。如果没有,GoToSocial 将构建错误的身份验证标头,导致所有的联合尝试以 401 被拒绝。 + +**注意**:`Connection` 和 `Upgrade` 头用于 WebSocket 连接。请参阅 [WebSocket 文档](websocket.md)。 + +**注意**:本例中 `client_max_body_size` 设置为 40M,这是 GoToSocial 的默认视频上传大小。根据需要你可以将此值设置得更大或更小。nginx 的默认值仅为 1M,太小了。 + +**注意**:为了使 `X-Forwarded-For` 和限流生效,请设置 `trusted-proxies` 配置变量。请参阅[限流](../../api/ratelimiting.md)和[通用配置](../../configuration/general.md)文档。 + +接下来我们需要将刚创建的文件链接到 nginx 从中读取活动站点配置的文件夹中。 + +```bash +sudo mkdir -p /etc/nginx/sites-enabled +sudo ln -s /etc/nginx/sites-available/yourgotosocial.url.conf /etc/nginx/sites-enabled/ +``` + +再次将 `yourgotosocial.url` 替换为你的实际 GoToSocial 主机值。 + +现在检查配置错误。 + +```bash +sudo nginx -t +``` + +如果一切正常,你应该会看到以下输出: + +```text +nginx: the configuration file /etc/nginx/nginx.conf syntax is ok +nginx: configuration file /etc/nginx/nginx.conf test is successful +``` + +一切正常吗?太好了!然后重启 nginx 以加载新的配置文件。 + +```bash +sudo systemctl restart nginx +``` + +## 设置 TLS + +!!! warning + 我们有关于如何[配置 TLS 证书](../../advanced/certificates.md)的附加文档,还提供了有关不同发行版的附加内容和教程链接,值得一看。 + +你现在可以运行 certbot,它将引导你完成启用 https 的步骤。 + +```bash +sudo certbot --nginx +``` + +完成后,它应该自动编辑你的配置文件以启用 https。 + +最后再次重新加载 NGINX: + +```bash +sudo systemctl restart nginx +``` + +现在重新启动 GoToSocial: + +```bash +sudo systemctl start gotosocial +``` + +## 安全加固 + +如果你想通过进阶配置选项加强 NGINX 部署,网上有很多指南([例如这个](https://beaglesecurity.com/blog/article/nginx-server-security.html))。请尝试找到最新的指南。Mozilla 还[在此处](https://ssl-config.mozilla.org/)发布了最佳实践 SSL 配置。 + +## 结果 + +你现在应该可以在浏览器中打开实例的启动页面,并看到它运行在 https 下! + +如果你再次打开 NGINX 配置,你会发现 Certbot 添加了一些额外的行。 + +!!! warning + 根据你设置 Certbot 时选择的选项,以及使用的 NGINX 版本,可能会有所不同。 + +```nginx +server { + server_name example.org; + location / { + # 设置为 127.0.0.1 而不是 localhost 以解决 https://stackoverflow.com/a/52550758 + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + } + client_max_body_size 40M; + + listen [::]:443 ssl; # 由 Certbot 管理 + listen 443 ssl; # 由 Certbot 管理 + http2 on; # 由 Certbot 管理 + ssl_certificate /etc/letsencrypt/live/example.org/fullchain.pem; # 由 Certbot 管理 + ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem; # 由 Certbot 管理 + include /etc/letsencrypt/options-ssl-nginx.conf; # 由 Certbot 管理 + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # 由 Certbot 管理 +} + +server { + if ($host = example.org) { + return 301 https://$host$request_uri; + } # 由 Certbot 管理 + + listen 80; + listen [::]:80; + server_name example.org; + return 404; # 由 Certbot 管理 +} +``` + +关于 nginx 的其他配置选项(包括静态资源服务和缓存),请参阅文档的[进阶配置部分](../../advanced/index.md)。 diff --git a/docs/locales/zh/getting_started/reverse_proxy/websocket.md b/docs/locales/zh/getting_started/reverse_proxy/websocket.md new file mode 100644 index 000000000..e1391ec45 --- /dev/null +++ b/docs/locales/zh/getting_started/reverse_proxy/websocket.md @@ -0,0 +1,43 @@ +# WebSocket + +GoToSocial 使用安全的 [WebSocket 协议](https://en.wikipedia.org/wiki/WebSocket)(即 `wss`)来通过客户端应用程序(如 Semaphore)实现贴文和通知的实时更新。 + +为了使用此功能,你需要确保配置 GoToSocial 所在的代理允许 WebSocket 连接通过。 + +WebSocket 端点位于 `wss://example.org/api/v1/streaming`,其中 `example.org` 是你的 GoToSocial 实例的域名。 + +WebSocket 端点使用在[通用配置](../../configuration/general.md)的 `port` 部分中配置的相同端口。 + +典型的 WebSocket **请求**头,如 Pinafore 所发送的如下所示: + +```text +Host: example.org +User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/99.0 +Accept: */* +Accept-Language: en-US,en;q=0.5 +Accept-Encoding: gzip, deflate, br +Sec-WebSocket-Version: 13 +Origin: https://pinafore.social +Sec-WebSocket-Protocol: null +Sec-WebSocket-Extensions: permessage-deflate +Sec-WebSocket-Key: YWFhYWFhYm9vYmllcwo= +DNT: 1 +Connection: keep-alive, Upgrade +Sec-Fetch-Dest: websocket +Sec-Fetch-Mode: websocket +Sec-Fetch-Site: cross-site +Pragma: no-cache +Cache-Control: no-cache +Upgrade: websocket +``` + +典型的 WebSocket **响应**头,如 GoToSocial 返回的如下所示: + +```text +HTTP/1.1 101 Switching Protocols +Upgrade: websocket +Connection: Upgrade +Sec-WebSocket-Accept: WVdGaFlXRmhZbTl2WW1sbGN3bz0K +``` + +无论你的设置如何,你都需要确保这些头在你的反向代理中被允许,这可能需要根据所使用的具体反向代理进行额外配置。 diff --git a/docs/locales/zh/getting_started/user_creation.md b/docs/locales/zh/getting_started/user_creation.md new file mode 100644 index 000000000..6b2479adc --- /dev/null +++ b/docs/locales/zh/getting_started/user_creation.md @@ -0,0 +1,49 @@ +# 创建用户 + +无论使用哪种安装方法,你都需要创建一些用户。GoToSocial 目前还没有通过网页 UI 创建用户或让人们通过网页 UI 注册的功能。 + +在此期间,你可以使用 CLI 创建用户: + +```sh +./gotosocial --config-path /path/to/config.yaml \ + admin account create \ + --username some_username \ + --email some_email@whatever.org \ + --password 'SOME_PASSWORD' +``` + +在上述命令中,将 `some_username` 替换为你想要的用户名,将 `some_email@whatever.org` 替换为你想关联到用户的电子邮件地址,将 `SOME_PASSWORD` 替换为一个安全的密码。 + +如果你想让用户拥有管理员权限,可以使用类似的命令提升他们: + +```sh +./gotosocial --config-path /path/to/config.yaml \ + admin account promote --username some_username +``` + +将 `some_username` 替换为你刚创建的账户的用户名。 + +!!! warning "提权需要重启服务器" + + 由于 GoToSocial 的缓存机制,某些管理员 CLI 命令在执行后需要重启服务器才能使更改生效。 + + 例如,将用户提升为管理员后,你需要重启 GoToSocial 服务器,以便从数据库加载新值。 + +!!! tip + + 要查看其他可用的 CLI 命令,请点击[这里](../admin/cli.md)。 + +## 容器 + +当从容器运行 GoToSocial 时,你需要在容器中执行上述命令。如何操作取决于你的容器运行时,但对于 Docker 来说,应该像这样: + +```sh +docker exec -it CONTAINER_NAME_OR_ID \ + /gotosocial/gotosocial \ + admin account create \ + --username some_username \ + --email someone@example.org \ + --password 'some_very_good_password' +``` + +如果你遵循我们的 Docker 指南,容器名应该为 `gotosocial`。你可以通过 `docker ps` 获取名称或 ID。 diff --git a/docs/locales/zh/index.md b/docs/locales/zh/index.md new file mode 100644 index 000000000..818225dea --- /dev/null +++ b/docs/locales/zh/index.md @@ -0,0 +1,17 @@ +{% + include "./repo/README.md" + start='' + end='' +%} + +{% + include "./repo/README.md" + start='' + end='' +%} + +{% + include "./repo/README.md" + start='' + end='' +%} diff --git a/docs/locales/zh/mkdocs.yml b/docs/locales/zh/mkdocs.yml new file mode 100644 index 000000000..8de9afad6 --- /dev/null +++ b/docs/locales/zh/mkdocs.yml @@ -0,0 +1,111 @@ +INHERIT: ../../../mkdocs.yml +site_name: "GoToSocial 文档" +theme: + language: zh + custom_dir: ../../overrides + palette: + - scheme: slate + toggle: + icon: material/brightness-7 + name: 切换到浅色模式 + - scheme: default + toggle: + icon: material/brightness-4 + name: 切换到深色模式 +docs_dir: . +edit_uri: edit/main/docs/locales/zh/ +copyright: GoToSocial 以 GNU AGPL v3 许可授权。版权所有 (C) 全体 GoToSocial 开发者 admin@gotosocial.org + +exclude_docs: | + repo/** + +nav: + - "主页": "index.md" + - "FAQ": "faq.md" + - "使用指南": + - "user_guide/settings.md" + - "user_guide/posts.md" + - "user_guide/search.md" + - "user_guide/custom_css.md" + - "user_guide/password_management.md" + - "user_guide/rss.md" + - "user_guide/migration.md" + - "开始部署": + - "getting_started/index.md" + - "getting_started/releases.md" + - "安装": + - "getting_started/installation/index.md" + - "getting_started/installation/metal.md" + - "getting_started/installation/container.md" + - "反向代理": + - "getting_started/reverse_proxy/index.md" + - "getting_started/reverse_proxy/nginx.md" + - "getting_started/reverse_proxy/apache-httpd.md" + - "getting_started/reverse_proxy/caddy.md" + - "getting_started/reverse_proxy/websocket.md" + - "getting_started/user_creation.md" + - "配置": + - "configuration/index.md" + - "configuration/general.md" + - "configuration/database.md" + - "configuration/web.md" + - "configuration/instance.md" + - "configuration/accounts.md" + - "configuration/media.md" + - "configuration/storage.md" + - "configuration/statuses.md" + - "configuration/tls.md" + - "configuration/oidc.md" + - "configuration/smtp.md" + - "configuration/syslog.md" + - "configuration/httpclient.md" + - "configuration/advanced.md" + - "configuration/observability.md" + - "进阶": + - "概述": "advanced/index.md" + - "advanced/host-account-domain.md" + - "advanced/outgoing-proxy.md" + - "缓存": + - "advanced/caching/index.md" + - "advanced/caching/api.md" + - "advanced/caching/assets-media.md" + - "advanced/certificates.md" + - "安全加固": + - "advanced/security/index.md" + - "advanced/security/sandboxing.md" + - "advanced/security/firewall.md" + - "advanced/healthchecks.md" + - "advanced/tracing.md" + - "advanced/metrics.md" + - "advanced/replicating-sqlite.md" + - "advanced/sqlite-networked-storage.md" + - "适用进阶场景的构建": + - "advanced/builds/nowasm.md" + + - "管理": + - "admin/settings.md" + - "admin/signups.md" + - "admin/federation_modes.md" + - "admin/domain_blocks.md" + - "admin/request_filtering_modes.md" + - "admin/robots.md" + - "admin/cli.md" + - "admin/backup_and_restore.md" + - "admin/media_caching.md" + - "admin/spam.md" + - "admin/database_maintenance.md" + - "admin/themes.md" + - "联合": + - "federation/index.md" + - "federation/http_signatures.md" + - "federation/access_control.md" + - "federation/ratelimiting.md" + - "federation/actors.md" + - "federation/posts.md" + - "federation/moderation.md" + - "federation/glossary.md" + - "客户端 API 文档": + - "api/authentication.md" + - "api/swagger.md" + - "api/ratelimiting.md" + - "api/throttling.md" diff --git a/docs/locales/zh/repo/CODE_OF_CONDUCT.md b/docs/locales/zh/repo/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..1cc1d38fe --- /dev/null +++ b/docs/locales/zh/repo/CODE_OF_CONDUCT.md @@ -0,0 +1,11 @@ +# 行为准则 + +此文档当前为草稿版本。 + +在写出更完整的行为准则之前,此处声明一些基本准则: + +1. 我们对右翼分子、纳粹分子、法西斯分子、跨性别恐惧者、同性恋恐惧者、种族主义者、骚扰者、施虐者、白人至上主义者、厌女症者或资本主义支持者的意见不感兴趣。上述名单并不详尽。意见是指包括 PR(Pull Request)、问题(issues)及任何其他形式的交流。请你走开! +2. 我们不会接受使用"人工智能"工具生成的修改(无论是代码还是其他内容)。"人工智能"模型的训练是建立在低薪工人过滤不良内容的基础上,并且对输入内容的所有者不尊重。在伦理上,这是不可接受的。 +3. 我们不会接受任何使 GoToSocial 转向企业化、监视、或其他有害资本主义行为的修改(包括代码或其他内容)。 +4. 我们不会接受促进社交媒体上有害行为的任何修改(代码或其他内容)。显然,这一条还存有争议,因为整个人类尚在探索如何安全地参与社交媒体活动。 +5. 禁止发送骚扰与垃圾信息! diff --git a/docs/locales/zh/repo/CONTRIBUTING.md b/docs/locales/zh/repo/CONTRIBUTING.md new file mode 100644 index 000000000..4d05b180e --- /dev/null +++ b/docs/locales/zh/repo/CONTRIBUTING.md @@ -0,0 +1,546 @@ +# 贡献指引 + +你好!欢迎阅读 GoToSocial 的 CONTRIBUTING.md :) 感谢你的关注,为你点赞。 + +这些贡献指引借鉴并受到了 Gitea 的启发 (https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md)。感谢 Gitea! + +## 目录 + +- [介绍](#介绍) +- [错误报告与功能请求](#错误报告与功能请求) +- [合并请求](#合并请求) + - [代码](#代码) + - [文档](#文档) +- [开发](#开发) + - [Golang 的分支特点](#golang-的分支特点) + - [构建 GoToSocial](#构建-gotosocial) + - [二进制文件](#二进制文件) + - [Docker](#docker) + - [使用 GoReleaser](#使用-goreleaser) + - [手动构建](#手动构建) + - [样式表 / Web开发](#样式表--web开发) + - [实时加载](#实时加载) + - [项目结构](#项目结构) + - [浏览代码结构](#浏览代码结构) + - [风格/代码检查/格式化](#风格代码检查格式化) + - [测试](#测试) + - [独立测试环境与 Semaphore](#独立测试环境与-semaphore) + - [运行自动化测试](#运行自动化测试) + - [SQLite](#sqlite) + - [Postgres](#postgres) + - [CLI 测试](#cli-测试) + - [联合](#联合) + - [更新 Swagger 文档](#更新-swagger-文档) + - [CI/CD 配置](#cicd-配置) + - [发布检查清单](#发布检查清单) + - [如果出问题了怎么办?](#如果出问题了怎么办) + +## 介绍 + +本文件包含一些重要信息,帮助你成功向 GoToSocial 提交的贡献。请在开启合并请求前仔细阅读! + +## 错误报告与功能请求 + +目前,我们使用 Github 的问题追踪系统来管理错误报告与功能请求。 + +你可以在[此处](https://github.com/superseriousbusiness/gotosocial/issues "GoToSocial 的 Github 问题页")查看所有开放的问题。 + +在创建新问题之前,不论是错误还是功能请求,**请现仔细搜索所有仍处于打开状态和已被关闭的问题,以确保它尚未被解决过**。你可以使用 Github 的关键字搜索来进行此操作。如果你的问题与已有问题重复,它将被关闭。 + +在打开功能请求之前,请考虑以下几点: + +- 这个功能是否符合 GoToSocial 的范围?由于我们是小团队,我们对维护可能导致问题的[功能蔓延](https://en.wikipedia.org/wiki/Feature_creep "关于功能蔓延的维基百科文章")保持警惕。 +- 这个功能是否对软件的许多用户普遍有用,还是仅适合非常具体的用例? +- 这个功能是否会对软件性能产生负面影响?如果是,这种权衡是否值得? +- 这个功能是否需要放宽 API 的安全限制?如果是,需要合理的理由。 +- 这个功能是否属于 GoToSocial 的服务器后端,还是应该由客户端实现? + +我们倾向于优先考虑与无障碍性、联合互通性和客户端兼容性相关的功能请求。 + +## 合并请求 + +我们欢迎新老贡献者的合并请求,但需注意以下几点: + +- 你已阅读并同意我们的[行为准则](./CODE_OF_CONDUCT.md)。 +- 合并请求应解决现有问题或错误(请在请求中链接相关问题),或者与文档有关。 +- 如果你的合并请求引入了大量的代码或架构变更,你会愿意对这些变更的代码与架构进行一些维护工作,并解决错误。我们不欢迎引入大量维护负担的一次性合并请求! +- 合并请求质量合格。我们是小团队,时间有限,无法帮助指导合并请求或解决基本编程问题。如果你不确定,不要承担太多任务:从一个小功能或错误修复开始,将其作为你的第一个合并请求,然后逐步提高。 + +如果在合并请求过程中有小问题或评论,你可以[加入我们的 Matrix 空间](https://matrix.to/#/#gotosocial-space:superseriousbusiness.org "GoToSocial Matrix 空间"),地址为 `#gotosocial-space:superseriousbusiness.org`。 + +请阅读下面适合你计划开启的合并请求类型的相应部分。 + +### 代码 + +为了方便维护者管理,针对 GoToSocial 库的合并请求流程大致如下: + +1. 为你将要解决的功能、错误或问题打开一个问题,或者在现有问题上发表评论,让大家知道你想处理它。 +2. 利用开放的问题与我们讨论你的设计,收集反馈,并解决关于实现的任何问题。 +3. 编写代码!确保所有现有测试通过。适当添加测试。运行代码格式化工具并更新文档。 +4. 打开合并请求。如果希望对正在实现中的代码收集更多反馈,可以作为草稿提交。 +5. 当你的合并请求已准备好接受审核时通知我们。 +6. 等待审核。 +7. 处理审核反馈,适当修改代码。如果你有合理的理由,可以对审核评论提出异议——我们都是在学习,毕竟——但请务必耐心和有礼貌。 + +为方便审核,请尝试将你的合并请求拆分为合理大小的提交,但不要过于追求完美:我们总是进行合并压缩。 + +如果你的合并请求过大,请考虑将其拆分为更小的独立合并请求以便于审核和理解。 + +确保你的合并请求仅包含与你尝试实现的功能或解决的错误相关的代码。不要在请求中包含对无关代码的重构:请为其创建单独的合并请求! + +如果你在未遵循上述流程的情况下开启了代码合并请求,我们可能会关闭它,并要求你遵循流程。 + +### 文档 + +文档合并请求的流程比代码的稍微宽松一些。 + +如果你发现文档中有遗漏、错误或不明确的地方,可以自由开启合并请求进行更正;你不必先开启问题,但请在合并请求评论中解释你开启请求的原因。 + +我们支持基于 [Conda](https://docs.conda.io/en/latest/) 的工作流程,用于修改、构建和发布文档。以下是你可以在本地开始编辑的步骤: + +* 安装 [`miniconda`](https://docs.conda.io/en/latest/miniconda.html) +* 创建你的 conda 环境:`conda env create -f ./docs/environment.yml` +* 激活环境:`conda activate gotosocial-docs` +* 在本地运行:`mkdocs serve` + +然后你可以在浏览器中访问 [localhost:8000](http://127.0.0.1:8000/) 查看。 + +添加新页面时,需要在 [`mkdocs.yml`](mkdocs.yml) 中添加,以便它显示在侧栏的正确部分。 + +如果你不使用 Conda,可以阅读 `docs/environment.yml` 查看需要哪些依赖,并手动通过 `pip install` 安装这些依赖。建议在虚拟环境中进行此操作,你可以通过类似 `python3 -m venv /path-to/store-the-venv` 创建虚拟环境。之后可以调用 `/path-to/store-the-venv/bin/pip`、`/path-to/store-the-venv/bin/mkdocs` 等。 + +要更新依赖,在已激活的环境中使用 `conda update --update-all`。然后你可以使用以下命令更新 `environment.yml`: + +```sh +conda env export -n gotosocial-docs --from-history --override-channels -c conda-forge -c nodefaults -f ./docs/environment.yml +``` + +注意 `conda env export` 会在 environment.yml 文件中添加 `prefix` 条目,并删除 `pip` 依赖,因此请确保移除 prefix,并重新添加 `pip` 依赖。 + +## 开发 + +### Golang 的分支特点 + +Golang 的一个特点是,它所依赖的源代码管理路径与 `go.mod` 中使用的路径以及各 Go 文件中的包导入路径相同。这使得使用分支有些棘手。 + +假设你要将 GoToSocial 分支到 `github.com/yourgithubname/gotosocial`,然后将存储库克隆到 `~/go/src/github.com/yourgithubname/gotosocial`。你可能会在尝试运行测试或构建时遇到错误,因此你可能会更改 `go.mod` 文件,使模块名称为 `github.com/yourgithubname/gotosocial` 而不是 `github.com/superseriousbusiness/gotosocial`。但这样做会破坏项目中的所有导入路径。这简直是噩梦!于是,你不得不逐一在源代码文件中将 `github.com/superseriousbusiness/gotosocial` 替换为 `github.com/yourgithubname/gotosocial`。这样确实能行得通,但一旦你决定对原始存储库发起合并请求,所有路径变更都会被包含在内!哦不! + +正确的解决方案是先派生存储库,然后克隆上游存储库,并将上游存储库的 `origin` 设置为你分支的源。 + +有关更多细节,请参阅[这篇博客](https://blog.sgmansfield.com/2016/06/working-with-forks-in-go/)。 + +为防此文章消失,此处是步骤(有轻微修改): + +> +> 在 GitHub 上派生存储库或设置任何其他远程 git 存储库。在这种情况下,我会转到 GitHub 并分支存储库。 +> +> 现在克隆上游存储库(而非派生的存储库): +> +> `mkdir -p ~/go/src/github.com/superseriousbusiness && git clone git@github.com:superseriousbusiness/gotosocial ~/go/src/github.com/superseriousbusiness/gotosocial` +> +> 转到你的计算机上上游存储库的顶级目录: +> +> `cd ~/go/src/github.com/superseriousbusiness/gotosocial` +> +> 将当前的 origin 远程源重命名为 upstream: +> +> `git remote rename origin upstream` +> +> 把你的派生分支添加为 origin: +> +> `git remote add origin git@github.com/yourgithubname/gotosocial` +> + +在第一次构建项目之前,一定要运行 `git fetch`。 + +### 构建 GoToSocial + +#### 二进制文件 + +要开始构建,你需要先安装 Go。GtS 目前使用 Go 1.21,因此你也应该使用这个版本。安装指南见[此处](https://golang.org/doc/install)。 + +安装 go 后,将此存储库克隆到你的 Go 路径中。通常,此路径为 `~/go/src/github.com/superseriousbusiness/gotosocial`。 + +安装完上述环境与依赖后,可以尝试构建项目:`./scripts/build.sh`。此命令将构建 `gotosocial` 二进制文件。 + +如果没有错误,太好了,你准备好了! + +如果看到错误 `fatal: No names found, cannot describe anything.`,需要运行 `git fetch`。 + +在开发过程中,为了自动重新编译,可以使用 [nodemon](https://www.npmjs.com/package/nodemon): + +```bash +nodemon -e go --signal SIGTERM --exec "go run ./cmd/gotosocial --host localhost testrig start || exit 1" +``` + +#### Docker + +对于以下两种方法,你需要安装 [Docker buildx](https://docs.docker.com/buildx/working-with-buildx/)。 + +##### 使用 GoReleaser + +GoToSocial 使用发布工具 [GoReleaser](https://goreleaser.com/intro/) 使多架构 + Docker 构建变得简单。 + +GoReleaser 还被 GoToSocial 用于构建和推送 Docker 镜像。 + +通常,这些过程由 Drone (参见 CI/CD 部分) 处理。不过,你也可以手动调用 GoReleaser 来构建快照版。 + +为此,首先[安装 GoReleaser](https://goreleaser.com/install/)。 + +然后按照[Swagger 部分](#更新-swagger-文档)的说明安装 GoSwagger。 + +接着按[样式表 / Web开发](#样式表--web开发)的说明安装 Node 和 Yarn。 + +最后,创建快照构建,执行: + +```bash +goreleaser release --clean --snapshot +``` + +如果一切按计划进行,现在你应该会在 `./dist` 文件夹中找到多个架构的二进制文件和 tar,终端输出中应显示构建的快照 Docker 镜像的版本。 + +##### 手动构建 + +如果你更喜欢以简单方法构建 Docker 容器,使用更少的依赖(go-swagger, Node, Yarn),也可以这样构建: + +```bash +./scripts/build.sh && docker buildx build -t superseriousbusiness/gotosocial:latest . +``` + +上述命令首先构建 `gotosocial` 二进制文件,然后调用 Docker buildx 构建容器镜像。 + +如果想为不同 CPU 架构构建 docker 镜像而不设置 buildx(例如 ARMv7 aka 32-bit ARM),首先需要通过添加以下几行到 Dockerfile 顶部来修改 Dockerfile(但不要提交此更改!): + +```dockerfile +# 使用 buildx 时,这些变量将由工具设定: +# https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope +# 但是,将它们声明为全局构建参数,允许手动使用 `--build-arg` 设置它们。 +ARG BUILDPLATFORM +ARG TARGETPLATFORM +``` + +然后,可以使用以下命令: + +```bash +GOOS=linux GOARCH=arm ./scripts/build.sh && docker build --build-arg BUILDPLATFORM=linux/amd64 --build-arg TARGETPLATFORM=linux/arm/v7 -t superseriousbusiness/gotosocial:latest . +``` + +另请参阅:[GOOS 和 GOARCH 值的详尽列表](https://gist.github.com/lizkes/975ab2d1b5f9d5fdee5d3fa665bcfde6) + +以及:[docker 的 `--platform` 可能值的详尽列表](https://github.com/tonistiigi/binfmt/#build-test-image) + +### 样式表 / Web开发 + +GoToSocial 使用存放于 `web/template` 文件夹下的 Gin 模板。静态资源存储于 `web/assets`。样式表和 JS 包(用于前端增强和设置界面)的源文件存储于 `web/source`,并从那里捆绑到 git 忽略的 `web/assets/dist` 文件夹。 + +要捆绑更改,需要 [Node.js](https://nodejs.org/en/download/) 和 [Yarn](https://classic.yarnpkg.com/en/docs/install)。 + +使用 [NVM](https://github.com/nvm-sh/nvm) 是安装它们的一种方便方式,还支持管理不同的 Node 版本。 + +安装前端依赖: + +```bash +yarn --cwd ./web/source install && yarn --cwd ./web/source ts-patch install +``` + +`ts-patch` 步骤是必要的,因为我们使用 Typia 进行一些类型验证:参见 [Typia 安装文档](https://typia.io/docs/setup/#manual-setup)。 + +重新编译前端包到 `web/assets/dist`: + +```bash +yarn --cwd ./web/source build +``` + +#### 实时加载 + +为了更方便的开发环境,可以在 [testrig](#测试) 中运行一个实时加载的捆绑器(bundler)。 + +首先用 DEBUG=1 构建 GtS 二进制文件以启用 testrig: + +``` bash +DEBUG=1 ./scripts/build.sh +``` + +现在打开两个终端。 + +在第一个终端中,使用你刚构建的二进制文件在端口 8081 上运行 testrig: + +```bash +DEBUG=1 GTS_PORT=8081 ./gotosocial testrig start +``` + +然后启动捆绑器(bundler),它将在端口 8080 上运行,并在需要时将请求代理到 testrig 实例。 + +``` bash +NODE_ENV=development yarn --cwd ./web/source dev +``` + +然后你可以在 `http://localhost:8080/settings` 登录 GoToSocial 设置面板,并查看实时更新反映的更改。 + +实时加载捆绑器(bundler)*不会*更改 `dist/` 中的捆绑资源,因此完成更改并想在某处部署时,必须运行 `node web/source` 生成准备就绪的生产环境包。 + +### 项目结构 + +对于项目结构,GoToSocial 遵循 [在此处定义的标准且被广泛接受的项目布局](https://github.com/golang-standards/project-layout)。正如作者所写: + +> 这是 Go 应用项目的基本布局。它不是核心 Go 开发团队定义的正式标准;然而,它是在 Go 生态系统中常见的历史和新兴项目布局模式。 + +在可能的情况下,我们更倾向于更短和更多的文件和包,对应用逻辑的可定义模块进行更明显的划分,而不是更少但更长的文件:如果一个 `.go` 文件接近 1000 行代码,可能就太长了。 + +#### 浏览代码结构 + +应用程序的大部分核心业务逻辑位于 `internal` 目录的各个包和子包中。以下是每个包的简要说明: + +`internal/ap` - ActivityPub 工具函数和接口。 + +`internal/api` - 客户端与联合 (ActivityPub) API 的模型、路由和工具。在此处可以为路由器添加路由。 + +`internal/concurrency` - 处理器和其他队列使用的工作模式。 + +`internal/config` - 配置标志、CLI 标志解析及配置获取/设置的代码。 + +`internal/db` - 用于与 sqlite/postgres 数据库交互的数据库接口。数据库迁移代码在 `internal/db/bundb/migrations`。 + +`internal/email` - 通过 SMTP 发送电子邮件的功能。 + +`internal/federation` - ActivityPub 联合代码;实现 `go-fed` 接口。 + +`internal/federation/federatingdb` - 实现 `go-fed` 的数据库接口。 + +`internal/federation/dereferencing` - 用于从外站实例获取资源的 HTTP 调用代码。 + +`internal/gotosocial` - GoToSocial 服务器启动/关闭逻辑。 + +`internal/gtserror` - 错误模型。 + +`internal/gtsmodel` - 数据库和内部模型。此处包含 `bundb` 注解。 + +`internal/httpclient` - GoToSocial 用于发请求到外站资源的 HTTP 客户端。 + +`internal/id` - 生成数据库模型 ID (ULIDs) 的代码。 + +`internal/log` - 日志实现。 + +`internal/media` - 管理和处理媒体附件的代码:图像、视频、表情等。 + +`internal/messages` - 用于封装工作消息的模型。 + +`internal/middleware` - Gin Gonic 路由中间件:HTTP 签名检查、缓存控制、令牌检查等。 + +`internal/netutil` - HTTP/网络请求验证代码。 + +`internal/oauth` - OAuth 服务器实现的封装代码/接口。 + +`internal/oidc` - OIDC 声明和回调的封装代码/接口。 + +`internal/processing` - 处理联合或客户端 API 产生的消息的逻辑。GoToSocial 的核心业务逻辑大多在此处。 + +`internal/regexes` - 用于解析文本和匹配 URL、标签、提及的正则表达式。 + +`internal/router` - Gin HTTP 路由器的封装。此处包含核心 HTTP 逻辑。此路由器暴露用于附加路由的函数,由 `internal/api` 中的处理程序代码使用。 + +`internal/storage` - `codeberg.org/gruf/go-store` 实现的封装。此处包含本地文件存储和 S3 逻辑。 + +`internal/stream` - Websocket 流逻辑。 + +`internal/text` - 文本解析与转换。包含贴文解析逻辑——支持纯文本和 markdown。 + +`internal/timeline` - 贴文时间线管理代码。 + +`internal/trans` - 将模型导出到数据库的 JSON 备份文件,并从备份 JSON 文件导入到数据库的代码。 + +`internal/transport` - HTTP 传输代码和工具。 + +`internal/typeutils` - 在内部数据库模型和 JSON 之间进行转换,从 ActivityPub 格式到内部数据库模型格式及其反向转换的代码。基本上是序列化与反序列化。 + +`internal/uris` - 用于生成 GoToSocial 中使用的 URI 的工具。 + +`internal/util` - 零碎的工具函数,用于多个包。 + +`internal/validate` - 模型验证代码——目前并未真正使用。 + +`internal/visibility` - 贴文可见性检查和过滤。 + +`internal/web` - Web UI 处理程序,专门用于提供网页、登录页面、设置面板。 + +### 风格/代码检查/格式化 + +在提交代码前,建议阅读官方的简短文档 [Effective Go](https://golang.org/doc/effective_go):这份文档是许多风格指南的基础,GoToSocial 基本遵循其建议。 + +我们还试图遵循的另一个风格指南是:[这个](https://github.com/bahlo/go-styleguide)。 + +此外,此处列举有一些符合 GtS 风格的 Uber 的 Go 风格指南亮点: + +- [分组相似的声明](https://github.com/uber-go/guide/blob/master/style.md#group-similar-declarations)。 +- [减少嵌套](https://github.com/uber-go/guide/blob/master/style.md#reduce-nesting)。 +- [不必要的 Else](https://github.com/uber-go/guide/blob/master/style.md#unnecessary-else)。 +- [局部变量声明](https://github.com/uber-go/guide/blob/master/style.md#local-variable-declarations)。 +- [减少变量作用域](https://github.com/uber-go/guide/blob/master/style.md#reduce-scope-of-variables)。 +- [初始化结构体](https://github.com/uber-go/guide/blob/master/style.md#initializing-structs)。 + +在提交代码之前,请确保执行 `go fmt ./...` 以更新空格和其他格式设置。 + +我们使用 [golangci-lint](https://golangci-lint.run/) 进行代码检查,通过静态代码分析捕获风格不一致和潜在的错误或安全问题。 + +如果你提交的 PR 未通过代码检查,将会被拒绝。因此,最好在推送或打开 PR 之前本地运行代码检查。 + +要做到这一点,请首先按照 [此处](https://golangci-lint.run/welcome/install/) 的说明安装代码检查工具。 + +然后,可以用以下命令运行代码检查: + +```bash +golangci-lint run +``` + +如果没有输出,太好了!这说明检查通过了 :) + +### 测试 + +GoToSocial 提供了一个 [testrig](https://github.com/superseriousbusiness/gotosocial/tree/main/testrig),包含一些可以用于集成测试的模拟包。 + +没有模拟的一个东西是数据库接口,因为使用内存中的 SQLite 数据库比模拟所有东西要简单得多。 + +#### 独立测试环境与 Semaphore + +你可以启动一个在本地主机运行的独立测试服务器 testrig,可以通过 [Semaphore](https://github.com/NickColley/semaphore/) 连接。 + +要做到这一点,首先用 `DEBUG=1 ./scripts/build.sh` 构建 gotosocial 二进制文件。 + +然后,通过设置 `DEBUG` 环境变量启动 testrig,如下调用二进制文件: + +```bash +DEBUG=1 ./gotosocial testrig start +``` + +要在本地开发模式下运行 Semaphore,首先克隆 [Semaphore](https://github.com/NickColley/semaphore/) 存储库,然后在克隆的目录中运行以下命令: + +```bash +yarn # 安装依赖 +yarn run dev +``` + +Semaphore 实例将在 `localhost:4002` 上启动。 + +要连接到 testrig,导航至 `http://localhost:4002`,并将在实例域名栏输入 `localhost:8080`。 + +在登录界面,输入电子邮件地址 `zork@example.org` 和密码 `password`。你会看到一个确认提示。接受后,你将以 Zork 身份登录。 + +请注意以下限制: + +- 由于 testrig 使用内存数据库,因此当 testrig 停止时,数据库将被销毁。 +- 如果你停止 testrig 并重新启动,则在测试期间创建的任何令牌或应用程序也会被删除。因此,你需要每次停止/启动 rig 时重新登录。 +- testrig 不会进行任何实际的外部 HTTP 调用,因此联合功能无法在 testrig 工作。 + +#### 运行自动化测试 + +测试可以在 SQLite 和 Postgres 上运行。 + +##### SQLite + +如果你想尽快运行测试,使用内存中的 SQLite 数据库,请使用: + +```bash +go test ./... +``` + +##### Postgres + +如果你想在本地运行针对 Postgres 数据库的测试,请运行: + +```bash +GTS_DB_TYPE="postgres" GTS_DB_ADDRESS="localhost" go test -p 1 ./... +``` + +在上面的命令中,假设你使用的是默认的 Postgres 密码 `postgres`。 + +在 Postgres 上运行时,我们设置 `-p 1` 因为它需要串行而不是并行运行测试。 + +#### CLI 测试 + +在 [./test/envparsing.sh](./test/envparsing.sh) 中有一个测试,用于确保 CLI 标志、配置和环境变量按预期解析。 + +虽然此测试是 CI/CD 测试过程的一部分,但除非你在修改 `cmd/gotosocial` 中的 `main` 包或者 `internal/config` 中的 `config` 包内的代码,否则你可能不需要过多担心自行运行它。 + +#### 联合 + +通过使用从磁盘加载 TLS 文件的支持,可以启动两个或多个本地实例,其 TLS 允许(手动)测试联合。 + +你需要设置以下配置选项: + +- `GTS_TLS_CERTIFICATE_CHAIN`:指向包含公钥证书的 PEM 编码证书链。 +- `GTS_TLS_CERTIFICATE_KEY`:指向 PEM 编码的私钥。 + +此外,为了让 Go HTTP 客户端认可自定义 CA 签发的证书为有效,你需要设置下列变量之一: + +- `SSL_CERT_FILE`:指向你的自定义 CA 的公钥。 +- `SSL_CERT_DIR`:一个以 `:` 分隔的目录列表,用于加载 CA 证书。 + +上述 `SSL_CERT` 变量仅适用于类 Unix 系统,不包括 Mac。请参阅 https://pkg.go.dev/crypto/x509#SystemCertPool。如果你在不支持设置上述变量的架构上运行测试,可以在 `config.yaml` 文件中将 `http-client.tls-insecure-skip-verify` 设置为 `true`,以完全禁用 HTTP 客户端的 TLS 证书验证。 + +你还需要为两个实例名称提供功能正常的 DNS,可以通过在 `/etc/hosts` 中添加条目或运行像 [dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html) 这样的本地 DNS 服务器来实现。 + +### 更新 Swagger 文档 + +GoToSocial 使用 [go-swagger](https://goswagger.io) 根据代码注释生成 Swagger API 文档。 + +你可以遵循 [此处](https://goswagger.io/install.html) 的说明安装 go-swagger。 + +如果你更改了任何 API 路径上的 Swagger 注释,可以通过运行以下命令在 `./docs/api/swagger.yaml` 生成一个新的 Swagger 文件: + +```bash +swagger generate spec --scan-models --exclude-deps -o docs/api/swagger.yaml +``` + +### CI/CD 配置 + +GoToSocial 使用 [Drone](https://www.drone.io/) 进行 CI/CD 任务,如运行测试、代码检查和构建 Docker 容器。 + +这些运行与 GitHub 集成,在打开拉取请求或合并到主干时执行。 + +GoToSocial 的 Drone 实例在 [此处](https://drone.superseriousbusiness.org/superseriousbusiness/gotosocial)。 + +`drone.yml` 文件在 [此处](../../../../.drone.yml) —— 它定义了 Drone 如何运行及何时运行。Drone 的文档在 [此处](https://docs.drone.io/)。 + +值得注意的是,`drone.yml` 文件必须由 Drone 管理员帐户签名后才被视为有效。每次修改该文件时都必须这样做。这是为了防止篡改和劫持 Drone 实例。请参阅 [此处](https://docs.drone.io/signature/)。 + +要签署文件,请首先安装并设置 [drone cli 工具](https://docs.drone.io/cli/install/)。然后,运行: + +```bash +drone -t PUT_YOUR_DRONE_ADMIN_TOKEN_HERE -s https://drone.superseriousbusiness.org sign superseriousbusiness/gotosocial --save +``` + +### 发布检查清单 + +首先:如果这是一个安全修复,我们可能会加急处理此清单,并在几天后发布包含此修复的版本。 + +现在,解决完安全问题后,此处是我们的清单。 + +GoToSocial 遵循 [语义化版本控制](https://semver.org/)。 +因此,清单上的首要问题是: + +- 我们正在发布哪个版本? + +接下来我们需要检查: + +- 这些资源是否需要重新构建并提交到存储库。 +- Swagger 文档是否需要重新生成? + +在项目管理方面: + +- 是否有需要移动到其他里程碑的问题? +- [路线图](./ROADMAP.md) 上是否有可以勾掉的事情? + +一旦我们对清单满意,我们就可以创建标签并推送它。 +剩下的事情 [是自动化](../../../../.drone.yml)。 + +然后我们可以前往 GitHub,为发布说明增添个性。 +最后,我们在所有渠道上发布公告,宣布发布已完成! + +#### 如果出问题了怎么办? + +有时事情会出错。 +我们发布了有 Bug 的版本,或者忘记了什么重要的东西。 + +如果该版本不可用,甚至对很大一部分用户而言是危险的,我们可以删除标签。 + +无论怎样,一旦我们解决了问题,我们就重新开始这个清单。版本号并不昂贵,可以随意更改。 diff --git a/docs/locales/zh/repo/README.md b/docs/locales/zh/repo/README.md new file mode 100644 index 000000000..82761a4b5 --- /dev/null +++ b/docs/locales/zh/repo/README.md @@ -0,0 +1,520 @@ + +# GoToSocial + +**有关企业赞助的更新:我们欢迎与符合我们价值观的组织建立赞助关系;请查看下述条件** + +GoToSocial 是一个用 Golang 编写的 [ActivityPub](https://activitypub.rocks/) 社交网络服务端。 + +通过 GoToSocial,你可以与朋友保持联系,发帖、阅读和分享图片及文章,且不会被追踪或广告打扰! + +

+ +

+ +**GoToSocial 仍然是 [BETA 软件](https://en.wikipedia.org/wiki/Software_release_life_cycle#Beta)**。它已经可被部署和使用,并能与许多其他 Fediverse 服务端顺利联合(但还不是与所有服务端)。然而,许多功能尚未实现,而且还有不少漏洞!我们在 2024 年 9 月/10 月离开了 Alpha 阶段,并计划于 2026 年结束 Beta。 + +文档位于 [docs.gotosocial.org](https://docs.gotosocial.org/zh-cn/)。你可以直接跳至 [API 文档](https://docs.gotosocial.org/zh-cn/latest/api/swagger/)。 + +要从源代码构建,请查看 [CONTRIBUTING.md](https://github.com/superseriousbusiness/gotosocial/blob/main/docs/locales/zh/repo/CONTRIBUTING.md) 文件。 + +这是实例首页的截图! + +![GoToSocial 实例 goblin.technology 的首页截图。它展示了实例的基本信息,如用户数和贴文数等。](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/overrides/public/instancesplash.png) + + +## 目录 + +- [什么是 GoToSocial?](#什么是-gotosocial) + - [联合](#联合) + - [历史与现状](#历史与现状) +- [功能](#功能) + - [兼容 Mastodon API](#兼容-mastodon-api) + - [精细的贴文可见性设置](#精细的贴文可见性设置) + - [回复控制](#回复控制) + - [仅本站贴文](#仅本站贴文) + - [RSS 源](#rss-源) + - [富文本格式化](#富文本格式化) + - [主题与自定义 CSS](#主题与自定义-css) + - [易于运行](#易于运行) + - [隐私+安全功能](#隐私安全功能) + - [多种联合模式](#多种联合模式) + - [OIDC 集成](#oidc-集成) + - [后端优先设计](#后端优先设计) +- [已知问题](#已知问题) +- [安装 GoToSocial](#安装-gotosocial) + - [支持的平台](#支持的平台) + - [FreeBSD](#freebsd) + - [32位](#32位) + - [OpenBSD](#openbsd) + - [稳定版本](#稳定版本) + - [快照版本](#快照版本) + - [Docker](#docker) + - [二进制发布 .tar.gz](#二进制发布-targz) + - [从源代码构建](#从源代码构建) + - [第三方打包](#第三方打包) +- [参与贡献](#参与贡献) +- [联系我们](#联系我们) +- [致谢](#致谢) + - [库](#库) + - [图像归属与许可](#图像归属与许可) + - [团队成员](#团队成员) + - [特别鸣谢](#特别鸣谢) +- [赞助与资金支持](#赞助与资金支持) + - [众筹](#众筹) + - [企业赞助](#企业赞助) + - [NLnet](#nlnet) +- [许可](#许可) + + +## 什么是 GoToSocial? + +GoToSocial 提供了一个轻量级、可定制且注重安全的进入 [联邦宇宙](https://en.wikipedia.org/wiki/Fediverse) 的入口,它类似但不同于像 [Mastodon](https://joinmastodon.org/)、[Pleroma](https://pleroma.social/)、[Friendica](https://friendi.ca) 和 [PixelFed](https://pixelfed.org/) 这样的现有项目。 + +如果你曾使用过 Twitter 或 Tumblr(甚至是 Myspace)等服务,GoToSocial 可能会让你感到熟悉:你可以关注他人并拥有粉丝,发布贴文,点赞、回复和分享他人的帖子,并通过时间线浏览你关注的人的贴文。你可以撰写长篇或短篇贴文,或者仅发布图片,一切随你选择。当然,你也可以屏蔽他人,或通过选择仅向朋友发布来限制不想要的互动。 + +![GoToSocial 中的网页版账户页截图,显示了头像、简介和粉丝/关注人数。](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/overrides/public/profile1.png) + +**GoToSocial 不使用推荐算法,也不收集你的数据来推荐内容或“改善你的体验”**。时间线是按时间顺序排列的:你在时间线顶部看到的内容是*刚刚发布的*,而不是根据你的个人资料选择的“有趣”或“有争议”的内容。 + +GoToSocial 并不是为拥有成千上万粉丝的“必追”网红设计的,也不是设计被用来让人上瘾的。你的时间线和体验由你关注的人和你与他人的互动方式决定,而不是你的参与度的相关指标! + +GoToSocial 不会宣称比其他应用更“好”,但它提供了一些可能特别*适合你*的东西。 + +### 联合 + +因为 GoToSocial 使用 [ActivityPub](https://activitypub.rocks/),你不仅可以与本站上的人交流,还可以无缝与 [联邦宇宙](https://en.wikipedia.org/wiki/Fediverse) 上的人交流。 + +![activitypub 标志](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/overrides/public/ap_logo.svg) + +联合意味着你的实例是一个遍布世界的、使用相同协议通信的服务器网络的一部分。你的数据不再集中在一家公司服务器上,而是在你自己的服务器上,根据你的意愿,跨越由其他人运行的服务器组成的弹性网络实现共享。 + +这种联合方式也意味着你不必受制于可能远在千里之外的庞大公司设定的任意规则。你的实例有自己的规则和文化;你的实例的居民是你的网上邻居;你很可能会认识你的服务器管理员和站务,或者自己成为管理员。 + +GoToSocial 的愿景是让许多小而特别的实例遍布联邦宇宙,让人们感到宾至如归,而不是让联邦宇宙被少数大的通用的实例占据,在那里一个人的声音可能会在大量其它账号的声音中迷失。 + +### 历史与现状 + +该项目于 2021 年 2/3 月因对其他联合式微博/社交媒体应用的安全和隐私功能的不满而起步,并希望实现一些不同的东西。 + +它最初是一个个人项目,然后随着更多开发者的兴趣和加入而加速发展。 + +我们在 2021 年 11 月进行了首次 Alpha 发布。我们于 2024 年 9 月/10 月离开 Alpha,进入 Beta 阶段。 + +要详细了解已实现和未实现的内容,以及 [稳定发布](https://en.wikipedia.org/wiki/Software_release_life_cycle#Stable_release) 的进展,请查看 [路线图](https://github.com/superseriousbusiness/gotosocial/blob/main/docs/locales/zh/repo/ROADMAP.md)。 + +--- + +## 功能 + +### 兼容 Mastodon API + +Mastodon API 已成为客户端与联邦宇宙服务端通信的事实标准,因此 GoToSocial 实现并在自定义功能上扩展了该 API。 + +大多数实现 Mastodon API 的应用程序都应该可以使用 GoToSocial,但以下这些优秀的应用程序已经过测试,可与 GoToSocial 可靠地配合使用: + +* [Tusky](https://tusky.app/) 适用于 Android +* [Semaphore](https://semaphore.social/) 适用于浏览器 +* [Feditext](https://github.com/feditext/feditext) (beta) 适用于 iOS, iPadOS 和 macOS + +如果你之前通过第三方应用来使用 Mastodon,使用 GoToSocial 将是轻而易举的。 + +### 精细的贴文可见性设置 + +发布内容时,选择谁能看到很重要。 + +GoToSocial 提供公开、不列出/悄悄公开、仅粉丝和私信(最好让对方事先同意)的贴文选项。 + +### 回复控制 + +GoToSocial 允许你通过 [互动规则](https://docs.gotosocial.org/zh-cn/latest/user_guide/settings/#default-interaction-policies) 选择谁可以回复你的贴文。你可以选择允许任何人回复贴文,仅允许朋友回复,等等。 + +![互动规则设置](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/overrides/public/user-settings-interaction-policy-1.png) + +### 仅本站贴文 + +有时你只想与同一实例中的人们交流。GoToSocial 通过仅本站可见贴文支持这一点,确保贴文仅保留在你的实例中。(当前,仅本站可见贴文能否使用取决于客户端支持。) + +### RSS 源 + +GoToSocial 允许你选择将个人资料暴露为 RSS 订阅源,这样人们可以订阅你的公开源而不会错过任何贴文。 + +### 富文本格式化 + +使用 GoToSocial,你可以使用流行且易用的 Markdown 标记语言来撰写帖子,从而生成丰富的 HTML 贴文,支持引用段落、语法高亮代码块、列表、内嵌链接等。 + +![markdown 格式化贴文](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/overrides/public/markdown-post.png) + +### 主题与自定义 CSS + +用户可以为他们的账户页 [选择多种有趣的主题](https://docs.gotosocial.org/zh-cn/latest/user_guide/settings/#select-theme),或甚至编写自己的 [自定义 CSS](https://docs.gotosocial.org/zh-cn/latest/user_guide/settings/#custom-css)。 + +管理员也可以轻松地为用户 [添加自定义主题](https://docs.gotosocial.org/zh-cn/latest/admin/themes/) 供用户选择。 + +
+显示主题示例 +
+ +
Blurple dark
+
+
+
+ +
Blurple light
+
+
+
+ +
Brutalist light
+
+
+
+ +
Brutalist dark
+
+
+
+ +
Ecks pee
+
+
+
+ +
Midnight trip
+
+
+ +
Moonlight hunt
+
+
+
+ +
Rainforest
+
+
+
+ +
Soft
+
+
+
+ +
Solarized dark
+
+
+
+ +
Solarized light
+
+
+
+ +
Sunset
+
+
+
+ +### 易于运行 + +GoToSocial 仅需约 250-350MiB 的 RAM,并且只要求极少的 CPU 频率,因此非常适合单板计算机、旧笔记本和每月 5 美元的小 VPS。 + +![Grafana 图标显示 GoToSocial 堆占用约为 250MB,偶尔飙升至 400MB-500MB。](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/overrides/public/getting-started-memory-graph.png) + +除数据库外无需其他依赖(也可以仅使用 SQLite!)。 + +只需下载二进制文件和对应资源(或 Docker 镜像),调整配置并运行。 + +### 隐私+安全功能 + +- 内置 [Let's Encrypt](https://letsencrypt.org/) 的自动使用 HTTPS 支持。 +- 严格执行贴文可见性和屏蔽逻辑。 +- 导入与导出允许联合实例列表和拒绝联合实例列表。订阅社区创建的屏蔽列表(类似于用于实例间联合的广告拦截器!)(功能仍在进行中)。 +- HTTP 签名认证:GoToSocial 在发送和接收消息时要求 [HTTP 签名](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12),以确保消息不能被篡改,身份不能被伪造。 + +### 多种联合模式 + +GoToSocial 对联合并不采取一刀切的方法。你的实例应该与谁联合应由你决定。 + +- “屏蔽列表”模式(默认):发现新实例;屏蔽你不喜欢的实例。 +- “允许列表”模式(实验性);只选择与信任的实例联合。 +- “零”联合模式;保持你的服务器私密(尚未实现)。 + +[查看文档了解更多信息](https://docs.gotosocial.org/zh-cn/latest/admin/federation_modes)。 + +### OIDC 集成 + +GoToSocial 支持 [OpenID Connect (OIDC)](https://openid.net/connect/) 身份提供商,这意味着你可以将其与现有的用户管理服务(如 [Auth0](https://auth0.com/)、[Gitlab](https://docs.gitlab.com/ee/integration/openid_connect_provider.html) 等)集成,或者部署你自己的 OIDC 服务并与之相连(我们推荐使用 [Dex](https://dexidp.io/))。 + +### 后端优先设计 + +与其他联邦宇宙服务端项目不同,GoToSocial 不附带集成的客户端前端(例如,网页端应用)。 + +相反,与 Matrix.org 的 [Synapse](https://github.com/matrix-org/synapse) 项目类似,它提供了一个相对通用的后端服务器实现,一些用于展示账户和贴文的美观的页面,以及一个[具有完善文档的 API](https://docs.gotosocial.org/zh-cn/latest/api/swagger/)。 + +在该 API 基础上,GoToSocial 鼓励开发者构建任何他们想要的前端实现或移动应用,无论它们是类似于 Tumblr、Facebook、Twitter,还是完全不同的东西。 + +--- + +## 已知问题 + +由于 GoToSocial 仍处于测试阶段,存在很多错误。我们使用 [GitHub issues](https://github.com/superseriousbusiness/gotosocial/issues?q=is%3Aissue+is%3Aopen+label%3Abug) 跟踪这些问题。 + +由于每个 ActivityPub 服务端实现对协议的解释略有不同,有些服务端尚未与 GoToSocial 正常联合。我们在 [这个项目](https://github.com/superseriousbusiness/gotosocial/projects/4) 中跟踪这些问题。最终,我们希望确保任何可以与 Mastodon 正确联合的 ActivityPub 实现也能够与 GoToSocial 联合。 + +--- + +## 安装 GoToSocial + +查看我们的 [入门文档](https://docs.gotosocial.org/zh-cn/latest/getting_started/),并浏览我们的 [发布页面](https://github.com/superseriousbusiness/gotosocial/releases)。 + + +### 支持的平台 + +虽然我们尽力支持合理数量的架构和操作系统,但由于库的限制或性能问题,对特定平台的支持有时是不可能实现的。 + +某些平台不被我们正式支持,但仍*可能*工作,我们无法测试或保证其性能或稳定性。 + +以下是 GoToSocial 当前针对不同平台的支持状态(如果某个平台未列出,则表示我们尚未检查,因此我们不清楚): + +| 操作系统 | 架构 | 支持程度 | 二进制文件 | Docker 容器 | +| ------- | --------------------- | ---------------------------------- | ---------- | --------------- | +| Linux | x86-64/AMD64 (64位) | 🟢 完全支持 | 是 | 是 | +| Linux | Armv8/ARM64 (64位) | 🟢 完全支持 | 是 | 是 | +| FreeBSD | x86-64/AMD64 (64位) | 🟢 完全支持[1](#freebsd) | 是 | 否 | +| Linux | x86-32/i386 (32位) | 🟡 部分支持[2](#32-bit) | 是 | 是 | +| Linux | Armv7/ARM32 (32位) | 🟡 部分支持[2](#32-bit) | 是 | 是 | +| Linux | Armv6/ARM32 (32位) | 🟡 部分支持[2](#32-bit) | 是 | 是 | +| OpenBSD | 任何架构 | 🔴 不支持[3](#openbsd) | 否 | 否 | + +#### FreeBSD + +大多数情况下可用,只是在 WASM SQLite 上有一些问题;在 FreeBSD 上安装时请仔细查看发行说明。如果使用 Postgres,则不应出现问题。 + +#### 32位 + +GtS 在像 i386 或 Armv6/v7 这样的 32 位系统上表现不佳,这主要是媒体解码性能的问题。 + +我们不建议在 32 位系统上运行 GtS,但你可以尝试关闭外站媒体处理功能,或使用完全**不受支持、实验性**的 [nowasm](https://docs.gotosocial.org/zh-cn/latest/advanced/builds/nowasm/) 标签自行构建二进制文件。 + +有关更多指导,请在尝试在 32 位系统上安装时检查发行说明。 + +#### OpenBSD + +由于性能问题(空闲时的高内存占用,在处理媒体时崩溃),此系统被标记为不支持。 + +虽然我们不支持在 OpenBSD 上运行 GtS,但你可以尝试使用完全**不受支持、实验性**的 [nowasm](https://docs.gotosocial.org/zh-cn/latest/advanced/builds/nowasm/) 标签自行构建二进制文件。 + +### 稳定版本 + +我们为二进制构建和 Docker 容器打包稳定版本,这样你就不需要自己从源代码构建。 + +Docker 镜像 `superseriousbusiness/gotosocial:latest` 始终对应于最新稳定版本。由于此标签经常被覆盖,你可能希望使用 Docker CLI 标志 `--pull always` 确保每次运行此标签时都有最新的镜像,或者也可在使用前手动运行 `docker pull superseriousbusiness/gotosocial:latest`。 + +### 快照版本 + +我们还会在每次将代码合并到主分支时进行快照版的构建,因此如果你愿意,可以从主分支的代码运行。 + +请注意,风险自负!我们会尝试确保主分支正常工作,但不能做出任何保证。如果不确定,请选择稳定版。 + +#### Docker + +要使用 Docker 从主分支运行,请使用 `snapshot` Docker 标签。Docker 镜像 `superseriousbusiness/gotosocial:snapshot` 始终对应主分支上的最新提交。由于此标签经常被覆盖,你可能希望使用 Docker CLI 标志 `--pull always` 确保每次运行此标签时都有最新的镜像,或者也可在使用前手动运行 `docker pull superseriousbusiness/gotosocial:snapshot`。 + +#### 二进制发布 .tar.gz + +要使用二进制发布从主分支运行,请从我们的 [自托管 Minio S3 仓库](https://minio.s3.superseriousbusiness.org/browser/gotosocial-snapshots)下载适合你架构的 .tar.gz 文件。 + +S3 存储桶中的快照版二进制发布由 Github 提交哈希控制。要获取最新的,请按上次修改时间排序,或者查看 [这里的提交列表](https://github.com/superseriousbusiness/gotosocial/commits/main),复制最新的 SHA,并在 Minio 控制台过滤器中粘贴。快照二进制发布会在 28 天后过期,以降低我们的托管成本。 + +### 从源代码构建 + +有关从源代码构建 GoToSocial 的说明,请参见 [CONTRIBUTING.md](https://github.com/superseriousbusiness/gotosocial/blob/main/docs/locales/zh/repo/CONTRIBUTING.md) 文件。 + +### 第三方打包 + +非常感谢那些将时间和精力投入到打包 GoToSocial 的人! + +这些包不是由 GoToSocial 维护的,因此请将问题和反馈发往对应的存储库维护者(并考虑向他们捐款!)。 + +[![打包状态](https://repology.org/badge/vertical-allrepos/gotosocial.svg)](https://repology.org/project/gotosocial/versions) + +你还可以通过以下方式部署自己的 GoToSocial 实例: + +- [YunoHost 上的 GoToSocial 打包](https://github.com/YunoHost-Apps/gotosocial_ynh):作者 [OniriCorpe](https://github.com/OniriCorpe)。 +- [Ansible Playbook (MASH)](https://github.com/mother-of-all-self-hosting/mash-playbook):该 Playbook 支持包括 GoToSocial 在内的多项服务。[文档](https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/services/gotosocial.md) +- [GoToSocial Helm Chart](https://github.com/fSocietySocial/charts/tree/main/charts/gotosocial):作者 [0hlov3](https://github.com/0hlov3)。 + + +--- + +## 参与贡献 + +你想为 GtS 作出贡献吗?太好了!❤️❤️❤️ 请查看问题页面,看看是否有你想参与的内容,并阅读 [CONTRIBUTING.md](https://github.com/superseriousbusiness/gotosocial/blob/main/docs/locales/zh/repo/CONTRIBUTING.md) 文件以获取指南并配置开发环境。 + +--- + +## 联系我们 + +如果你有问题或反馈,可以[加入我们的 Matrix 空间](https://matrix.to/#/#gotosocial-space:superseriousbusiness.org),地址是 `#gotosocial-space:superseriousbusiness.org`。这是联系开发人员的最快方式。你也可以发送邮件至 [admin@gotosocial.org](mailto:admin@gotosocial.org)。 + +对于错误和功能请求,请先查看是否[已有相应问题](https://github.com/superseriousbusiness/gotosocial/issues),如果没有,可以开一个新问题工单(issue),或者使用上述渠道提出请求(如果你没有 Github 账户的话)。 + +--- + +## 致谢 + + +### 库 + +GoToSocial 使用以下开源库、框架和工具,在此声明并致谢 💕 + +- [buckket/go-blurhash](https://github.com/buckket/go-blurhash); 用于生成图像模糊哈希。 [GPL-3.0 许可证](https://spdx.org/licenses/GPL-3.0-only.html)。 +- [coreos/go-oidc](https://github.com/coreos/go-oidc); OIDC 客户端库。 [Apache-2.0 许可证](https://spdx.org/licenses/Apache-2.0.html)。 +- [DmitriyVTitov/size](https://github.com/DmitriyVTitov/size); 运行时模型内存大小计算。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- Gin: + - [gin-contrib/cors](https://github.com/gin-contrib/cors); Gin CORS 中间件。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [gin-contrib/gzip](https://github.com/gin-contrib/gzip); Gin gzip 中间件。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [gin-contrib/sessions](https://github.com/gin-contrib/sessions); Gin 会话中间件。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [gin-gonic/gin](https://github.com/gin-gonic/gin); 高速路由引擎。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- [google/uuid](https://github.com/google/uuid); UUID 生成。 [BSD-3-Clause 许可证](https://spdx.org/licenses/BSD-3-Clause.html)。 +- Go-Playground: + - [go-playground/form](https://github.com/go-playground/form); 表单映射支持。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [go-playground/validator](https://github.com/go-playground/validator); 结构验证。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- Gorilla: + - [gorilla/feeds](https://github.com/gorilla/feeds); RSS + Atom 提要生成。 [BSD-2-Clause 许可证](https://spdx.org/licenses/BSD-2-Clause.html)。 + - [gorilla/websocket](https://github.com/gorilla/websocket); WebSocket 连接。 [BSD-2-Clause 许可证](https://spdx.org/licenses/BSD-2-Clause.html)。 +- [go-swagger/go-swagger](https://github.com/go-swagger/go-swagger); Swagger OpenAPI 规范生成。 [Apache-2.0 许可证](https://spdx.org/licenses/Apache-2.0.html)。 +- gruf: + - [gruf/go-bytesize](https://codeberg.org/gruf/go-bytesize); 字节大小解析/格式化。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [gruf/go-cache](https://codeberg.org/gruf/go-cache); LRU 和 TTL 缓存。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [gruf/go-debug](https://codeberg.org/gruf/go-debug); 调试构建标记。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [gruf/go-errors](https://codeberg.org/gruf/go-errors); 类似上下文的错误与值包装。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [gruf/go-fastcopy](https://codeberg.org/gruf/go-fastcopy); 高性能 I/O 复制(缓冲池)。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [gruf/go-ffmpreg](https://codeberg.org/gruf/go-ffmpreg); 嵌入式 ffmpeg / ffprobe WASM 二进制文件。 [GPL-3.0 许可证](https://spdx.org/licenses/GPL-3.0-only.html)。 + - [gruf/go-kv](https://codeberg.org/gruf/go-kv); 日志字段格式化。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [gruf/go-list](https://codeberg.org/gruf/go-list); 通用双向链表。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [gruf/go-mutexes](https://codeberg.org/gruf/go-mutexes); 安全互斥锁和互斥图。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [gruf/go-runners](https://codeberg.org/gruf/go-runners); 同步工具。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [gruf/go-sched](https://codeberg.org/gruf/go-sched); 任务调度器。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [gruf/go-storage](https://codeberg.org/gruf/go-storage); 文件存储后端(本地及 s3)。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [gruf/go-structr](https://codeberg.org/gruf/go-structr); 结构缓存+队列及按字段索引。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- jackc: + - [jackc/pgconn](https://github.com/jackc/pgconn); Postgres 驱动程序。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + - [jackc/pgx](https://github.com/jackc/pgx); Postgres 驱动程序及工具包。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- [KimMachineGun/automemlimit](https://github.com/KimMachineGun/automemlimit); cgroups 内存限制检查。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- [k3a/html2text](https://github.com/k3a/html2text); HTML 转文本转换。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- [mcuadros/go-syslog](https://github.com/mcuadros/go-syslog); Syslog 服务器库。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- [microcosm-cc/bluemonday](https://github.com/microcosm-cc/bluemonday); HTML 用户输入清理。 [BSD-3-Clause 许可证](https://spdx.org/licenses/BSD-3-Clause.html)。 +- [miekg/dns](https://github.com/miekg/dns); DNS 工具。 [Go 许可证](https://go.dev/LICENSE)。 +- [minio/minio-go](https://github.com/minio/minio-go); S3 客户端 SDK。 [Apache-2.0 许可证](https://spdx.org/licenses/Apache-2.0.html)。 +- [mitchellh/mapstructure](https://github.com/mitchellh/mapstructure); Go 接口 => 结构解析。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- [modernc.org/sqlite](https://gitlab.com/cznic/sqlite); 简明的 SQLite。 [其他许可证](https://gitlab.com/cznic/sqlite/-/blob/master/LICENSE)。 +- [mvdan.cc/xurls](https://github.com/mvdan/xurls); URL 解析正则表达式。 [BSD-3-Clause 许可证](https://spdx.org/licenses/BSD-3-Clause.html)。 +- [oklog/ulid](https://github.com/oklog/ulid); 顺序友好的数据库 ID 生成。 [Apache-2.0 许可证](https://spdx.org/licenses/Apache-2.0.html)。 +- [open-telemetry/opentelemetry-go](https://github.com/open-telemetry/opentelemetry-go); OpenTelemetry API + SDK。 [Apache-2.0 许可证](https://spdx.org/licenses/Apache-2.0.html)。 +- spf13: + - [spf13/cobra](https://github.com/spf13/cobra); 命令行工具。 [Apache-2.0 许可证](https://spdx.org/licenses/Apache-2.0.html)。 + - [spf13/viper](https://github.com/spf13/viper); 配置管理。 [Apache-2.0 许可证](https://spdx.org/licenses/Apache-2.0.html)。 +- [stretchr/testify](https://github.com/stretchr/testify); 测试框架。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- superseriousbusiness: + - [superseriousbusiness/activity](https://github.com/superseriousbusiness/activity) 从 [go-fed/activity](https://github.com/go-fed/activity) 派生; Golang ActivityPub/ActivityStreams 库。 [BSD-3-Clause 许可证](https://spdx.org/licenses/BSD-3-Clause.html)。 + - [superseriousbusiness/exif-terminator](https://codeberg.org/superseriousbusiness/exif-terminator); EXIF 数据擦除。 [GNU AGPL v3 许可证](https://spdx.org/licenses/AGPL-3.0-or-later.html)。 + - [superseriousbusiness/httpsig](https://github.com/superseriousbusiness/httpsig) 从 [go-fed/httpsig](https://github.com/go-fed/httpsig) 派生; 安全 HTTP 签名库。 [BSD-3-Clause 许可证](https://spdx.org/licenses/BSD-3-Clause.html)。 + - [superseriousbusiness/oauth2](https://github.com/superseriousbusiness/oauth2) 从 [go-oauth2/oauth2](https://github.com/go-oauth2/oauth2) 派生; OAuth 服务器框架和令牌处理。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- [tdewolff/minify](https://github.com/tdewolff/minify); Markdown 帖文的 HTML 压缩。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- [uber-go/automaxprocs](https://github.com/uber-go/automaxprocs); GOMAXPROCS 自动化。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- [ulule/limiter](https://github.com/ulule/limiter); http 流量限制中间件。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- [uptrace/bun](https://github.com/uptrace/bun); 数据库 ORM。 [BSD-2-Clause 许可证](https://spdx.org/licenses/BSD-2-Clause.html)。 +- [wagslane/go-password-validator](https://github.com/wagslane/go-password-validator); 密码强度验证。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 +- [yuin/goldmark](https://github.com/yuin/goldmark); Markdown 解析器。 [MIT 许可证](https://spdx.org/licenses/MIT.html)。 + + +### 图像归属与许可 + +树懒标志由 [Anna Abramek](https://abramek.art/) 设计。 + +Creative Commons License
GoToSocial 的树懒吉祥物采用 知识共享署名-相同方式共享 4.0 国际许可协议。 + +该许可具体适用于以下存储库内的文件和子目录: + +- [树懒标志 png](https://github.com/superseriousbusiness/gotosocial/blob/main/web/assets/logo.png) +- [树懒标志 webp](https://github.com/superseriousbusiness/gotosocial/blob/main/web/assets/logo.webp) +- [树懒标志 svg](https://github.com/superseriousbusiness/gotosocial/blob/main/web/assets/logo.svg) +- [所有默认头像](https://github.com/superseriousbusiness/gotosocial/blob/main/web/assets/default_avatars) + +根据许可证条款,你可以: + +- 分享 — 在任何媒介或格式中复制、传播上述材料。 +- 演绎 — 混合、转换与再创作上述材料,并用于任何目的,包括商业用途。 + +### 团队成员 + +按字母顺序(... 和气味顺序)排列: + +- daenney +- f0x \[[通过 liberapay 捐赠](https://liberapay.com/f0x)\] +- kim \[在 @ [codeberg](https://codeberg.org/gruf) 查看我的代码, 或在 @ [@kim](https://k.iim.gay/@kim) 找到我\] +- tobi \[[通过 liberapay 捐赠](https://liberapay.com/GoToSocial/)\] +- vyr + +### 特别鸣谢 + +特别感谢来自 [go-fed](https://github.com/go-fed/activity) 的 CJ:没有你的工作,GoToSocial 不可能实现。 + +感谢所有使用 GtS 的人,包括提交问题的,提出改进建议的,提供资金支持的,以及以其他方式支持或鼓励该项目的人! + +--- + +## 赞助与资金支持 + +**有关企业赞助的更新:我们欢迎与符合我们价值观的组织进行赞助合作;请参阅以下条件** + +### 众筹 + +![open collective 标准树懒 徽章](https://opencollective.com/gotosocial/tiers/standard-sloth/badge.svg?label=Standard%20Sloth&color=brightgreen) ![open collective 稳定树懒 徽章](https://opencollective.com/gotosocial/tiers/stable-sloth/badge.svg?label=Stable%20Sloth&color=green) ![open collective 特别树懒 徽章](https://opencollective.com/gotosocial/tiers/special-sloth/badge.svg?label=Special%20Sloth&color=yellowgreen) ![open collective 糖果树懒 徽章](https://opencollective.com/gotosocial/tiers/sugar-sloth/badge.svg?label=Sugar%20Sloth&color=blue) + +如果你希望为 GoToSocial 捐款以支持开发,[你可以通过我们的 OpenCollective 捐助](https://opencollective.com/gotosocial#support)! + +![LiberaPay 赞助人](https://img.shields.io/liberapay/patrons/GoToSocial.svg?logo=liberapay) ![通过 LiberaPay 接收捐赠](https://img.shields.io/liberapay/receives/GoToSocial.svg?logo=liberapay) + +如果你喜欢通过 LiberaPay 赞助,我们也有一个 LiberaPay 帐户!你可以在[这里找到我们](https://liberapay.com/GoToSocial/)。 + +通过我们 OpenCollective 和 Liberapay 账户的众筹捐款将用于支付核心团队的工资、服务器成本以及 GtS 的艺术、设计等其他开支。 + +💕 🦥 💕 谢谢你们! + +### 企业赞助 + +GoToSocial 欢迎与符合我们价值观的组织进行合作。在此对您的支持表示感谢,我们将在存储库和文档中展示你的 Logo、网站及简短的标语。赞助有以下限制: + +1. GoToSocial 的项目方向始终由核心团队完全掌控,永远不会受到企业赞助的支配或影响。这是不可商量的。当然,企业同样可以像任何其他用户一样建议/请求功能,但不会获得特殊待遇。 + +2. 企业赞助取决于你的组织是否符合我们团队的伦理准则。这不是一套具体的规则,而是“你的公司是否造成了伤害?”的问题。例如,国防行业的不需要申请,因为答案显然是肯定的! + +如果在阅读后您仍有兴趣赞助我们,那太好了!请通过 admin@gotosocial.org 与我们联系以进一步讨论 :) + +### NLnet + +NGIZero logo + +结合以上众筹来源,2023 年 GoToSocial Alpha 阶段的开发得到了 [NGI0 Entrust Fund](https://nlnet.nl/entrust/) 旗下的 [NLnet](https://nlnet.nl/) 提供的 50,000 欧元资助。详情请见[此处](https://nlnet.nl/project/GoToSocial/#ack)。成功的资助申请存档在[此处](https://github.com/superseriousbusiness/gotosocial/blob/main/archive/nlnet/2022-next-generation-internet-zero.md)。 + +2024 年 GoToSocial Beta 阶段的开发将从 [NGI0 Entrust Fund](https://nlnet.nl/entrust/) 旗下的 [NLnet](https://nlnet.nl/) 那里再获得 50,000 欧元的资助。 + +--- + +## 许可 + +![GNU AGPL 徽标](https://www.gnu.org/graphics/agplv3-155x51.png) + +GoToSocial 是自由软件,采用 [GNU AGPL v3 许可](https://github.com/superseriousbusiness/gotosocial/blob/main/LICENSE)。我们鼓励你对代码进行派生和修改,进行各种实验。 + +有关 AGPL 和 GPL 许可之间的区别,请参阅[这里](https://www.gnu.org/licenses/why-affero-gpl.html),关于 GPL 许可(包括 AGPL)的常见问题解答,请参阅[这里](https://www.gnu.org/licenses/gpl-faq.html)。 + +如果你修改了 GoToSocial 的源码,并以网络可访问的方式运行修改后的代码,你*必须*根据许可的指引提供你对源码的修改副本: + +> 如果你修改了程序,并且你的修改版本支持通过计算机网络与用户进行远程交互,你的版本必须显著地向所有这些用户提供获得你的版本对应源码的机会,方式需为通过网络服务器以不收费的方式,或通过某种标准或习惯方式提供以便于复制软件。 + +版权所有 (C) 全体 GoToSocial 开发者 + + + diff --git a/docs/locales/zh/repo/ROADMAP.md b/docs/locales/zh/repo/ROADMAP.md new file mode 100644 index 000000000..28d54752e --- /dev/null +++ b/docs/locales/zh/repo/ROADMAP.md @@ -0,0 +1,99 @@ +# Beta 版路线图 + +本文档包含了 GoToSocial 为其首个正式稳定版本发布而制定的路线图。 + +文档中的所有信息仅为预测。这为参与开发的人提供了粗略的时间表,但过程中难免会有变动;请不要对文档中的任何事项抱有太强烈的期望! + +感谢 [NLnet](https://nlnet.nl) 对 GoToSocial alpha 与 beta 阶段开发的资助! + +非常感谢我们所有的 [Open Collective](https://opencollective.com/gotosocial) 和 [Liberapay](https://liberapay.com/gotosocial) 赞助者们,他们的赞助使 GoToSocial 项目能够持续前行! 💕 + +## 目录 + +- [Beta 目标](#beta-目标) +- [时间节点](#时间节点) + - [2023 年中](#2023-年中) + - [2023 年中到年底](#2023-年中到年底) + - [2024 年初](#2024-年初) + - [BETA 里程碑](#beta-里程碑) + - [2024 年余下时间至 2025 年初](#2024-年余下时间至-2025-年初) + - [BETA 发布到稳定版发布期间](#beta-发布到稳定版发布期间) +- [愿望单](#愿望单) + +## Beta 目标 + +每个软件项目对“beta”都有不同的理解。对于我们来说,GoToSocial 的 beta 版本应提供一套与现有流行的 ActivityPub 服务端实现大致相当的功能集。 + +换句话说,你应该能使用 GoToSocial 的 beta 版本作为你的主要社交实例,关注他人、发布动态,而不会遇到功能缺失或工作不正常的情况。 + +我们的 beta 目标还包括一些我们认为对用户安全与健康至关重要的功能,如关闭评论区、黑名单订阅、白名单模式支持等。 + +一旦我们实现了足以使 GoToSocial 进入 “beta” 的功能,我们将利用 beta 阶段来修复漏洞、调整性能,并新增一些需要在稳定基础上实现的额外功能。 + +我们希望在进入 beta 阶段后,客户端 API 能保持相对稳定,以便开发者能自信地基于 GoToSocial 构建应用,而无需担心 API 发生重大变化。 + +我们预计在 2024 年初进入 beta 阶段,但这个时间点只是预计,可能会更改。 + +## 时间节点 + +以下是我们迈向 beta 的功能开发大致时间表。时间表的推演基于以下假设: + +- 我们的开发速度将与过去两年类似。 +- 我们的总工作量大致相当于一个人全职参与该项目。 +- 一个独立的“功能”需要一个人 2-4 周的时间来开发和测试,具体取决于功能的复杂度。 +- 在实现各种功能的过程中还需要修复其他 bug,因此不应安排过于密集的功能计划。 + +**这只是预估的时间节点,具体功能发布的顺序并未固定。根据我们遇到的挑战和社区贡献的代码数量,开发速度可能会更快或更慢。此时间线也未包含实现新功能之外的任务,如管理、完善现有功能、重构代码、版本管理及确保与其他 AP 实现的兼容性。** + +### 2023 年中 + +- [x] **话题标签** -- 实现话题标签的联合与查看,让用户发现他们可能感兴趣的帖文。(完成! https://github.com/superseriousbusiness/gotosocial/pull/2032)。 + +### 2023 年中到年底 + +- [x] **投票** -- 实现对投票的解析、创建和参与功能。(完成! https://github.com/superseriousbusiness/gotosocial/pull/2330) +- [x] **静音帖文/贴文串** -- 取消订阅贴文串的回复通知;不在时间线上显示特定帖文。(完成! https://github.com/superseriousbusiness/gotosocial/pull/2278) +- [x] **有限联合/白名单** -- 允许实例管理员默认阻止与其他实例的联合。(完成! https://github.com/superseriousbusiness/gotosocial/pull/2200) + +### 2024 年初 + +- [x] **账户迁移** -- 使用 ActivityPub 的 `Move` 活动支持用户账户在服务器之间的迁移。 +- [x] **注册流** -- 允许用户提交注册申请;允许管理员审核注册请求。 + +### BETA 里程碑 + +完成以上所有功能即表明我们进入了 GoToSocial 的 BETA 阶段。我们预计在 2024 年 2 月到 3 月之间实现这一阶段。编辑:最终在 2024 年 9 月到 10 月之间实现,抱歉! + +### 2024 年余下时间至 2025 年初 + +这些功能按无特定顺序提供。 + +- [x] **v2 过滤规则** -- 实现过滤器 API 的第二版。 +- [x] **静音账户** -- 静音账户以防止其帖文出现在主页时间线上(可选:限制时间段)。 +- [x] **无评论区的帖文** -- 设计无评论区帖文的相关逻辑,让用户创建无评论区的帖文。 +- [ ] **屏蔽/允许列表订阅** -- 允许实例管理员订阅纯文本的示例屏蔽/允许列表。(大部分工作已经完成) +- [x] **私信对话视图** -- 让用户能够轻松浏览他们参与的所有私信对话。 +- [ ] **Oauth 令牌管理** -- 通过设置面板创建/查看/吊销 OAuth 令牌。 +- [ ] **贴文编辑支持** -- 编辑已创建的贴文,而无需删除并重新编辑。并正确地将编辑传播出去。 +- [ ] **Fediverse 中继支持** -- 与中继通信,发布和接收帖文。 +- [ ] **两步验证 (2fa)** -- 允许用户通过设置面板为其账户启用 2FA,并在登录时实施 2FA。 +- [ ] **管理:附加内容警告/将所有内容标记为敏感内容**。 + +更多内容待定! + +### BETA 发布到稳定版发布期间 + +待定。 + +## 愿望单 + +如果时间允许,我们将实现以下这些很酷的功能(因为我们真的很想要): + +- **群组** 与群组发帖! +- 基于声誉的“慢速”联合。 +- 联合及管理操作的社区决策。 +- 用户可选择自定义模板来渲染公开帖文: + - 推特风格 + - 博客帖文 + - 图库 + - 等其它风格 diff --git a/docs/locales/zh/user_guide/custom_css.md b/docs/locales/zh/user_guide/custom_css.md new file mode 100644 index 000000000..265ebba39 --- /dev/null +++ b/docs/locales/zh/user_guide/custom_css.md @@ -0,0 +1,73 @@ +# 自定义 CSS(进阶) + +CSS(级联样式表)是一种与 HTML 一起使用的编码语言,它决定了网页在浏览器中的外观: + +> HTML 用于定义内容的结构和语义,CSS 用于对其进行样式化和布局。例如,你可以使用 CSS 来更改内容的字体、颜色、大小和间距,分成多列,或添加动画和其他装饰功能。 +> +> -- [学习 CSS (Mozilla)](https://developer.mozilla.org/zh-CN/docs/Learn/CSS) + +根据你的 GoToSocial 实例管理员配置的设置,你可以通过用户设置面板上传自定义 CSS 到你的账户。 + +这允许你为使用网络浏览器访问你的 GoToSocial 账户页的用户自定义页面外观。 + +## 示例 - 更改背景颜色 + +这是一个标准的 GoToSocial 账户页面: + +![一个 GoToSocial 测试账户页面。标准配色方案是灰色、蓝色和橙色。](./../public/cssstandard.png) + +假设我们想将背景颜色改为黑色而不是灰色。 + +在用户设置面板中,我们在自定义 CSS 字段中输入以下 CSS 代码: + +```css +.page { + background: black; +} +``` + +然后我们点击保存账户信息。 + +如果我们返回到账户页面并刷新页面,现在它看起来是这样的: + +![同一个 GoToSocial 测试账户页面。背景现在是黑色。](./../public/cssblack.png) + +如果我们想要更炫一点,可以使用下面的 CSS 代码为背景添加渐变效果: + +```css +.page { + background: linear-gradient(crimson, purple); +} +``` + +保存 CSS 并刷新账户页面后,页面现在看起来是这样的: + +![同一个 GoToSocial 测试账户页面。背景现在从深红色开始,向下渐变为紫色。](./../public/cssgradient.png) + +## 可访问性 + +可访问的 HTML 和 CSS 的重要性不容忽视。以下节选自 W3: + +> 网络的基本设计理念是为所有人服务,无论他们的硬件、软件、语言、位置或能力如何。当网络达到这一目标时,它对具有不同的听力、行动、视力和认知能力的人来说都是可访问的。 +> +> 因此,残疾带来的影响在网络上有根本性的不同,因为网络消除了许多人在物理世界中面临的交流和互动障碍。然而,当网站、应用程序、技术或工具设计不佳时,它们可能会带来把人排除在网络之外的新障碍。 +> +> 对于希望创建高质量网站和网络工具,而不希望把使用其产品和服务的部分人群排除在外的开发者和组织来说,可访问性是必不可少的。 +> +> -- [网络可访问性介绍](https://www.w3.org/WAI/fundamentals/accessibility-intro/) + +标准的 GoToSocial 主题在设计中考虑了网络可访问性,特别是在布局、颜色对比、字体大小等方面。 + +如果你为账户编写自定义 CSS,非常重要的一点是确保它保持可读,并按预期运行。按钮应看起来像按钮,链接应看起来像链接,文本应以可读的字体呈现,元素不应在页面上跳动等。网页可以做到漂亮且令人兴奋,而不必牺牲可读性或让事情过于复杂。 + +如果你更改你的配色方案,最好验证新颜色,以确保它们具有足够的对比度以便视觉障碍(如色盲)的人可以阅读。一旦更新了 CSS,试着将你的账户 URL 输入对比度检查工具中,如[颜色对比度可访问性验证器](https://color.a11y.com/Contrast)。你还可以使用网络浏览器的“可访问性”选项卡检查是否存在任何问题。 + +以可访问性为中心进行样式设置可以让网络对所有人更友好!查看下面的链接以获取更多信息。 + +## 有用链接 + +- [学习 CSS (Mozilla)](https://developer.mozilla.org/zh-CN/docs/Learn/CSS) +- [CSS 教程 (W3 Schools)](https://www.w3schools.com/Css/default.asp) +- [CSS 和 JavaScript 可访问性最佳实践 (Mozilla)](https://developer.mozilla.org/en-US/docs/Learn/Accessibility/CSS_and_JavaScript#css) +- [WAVE 网页可访问性评估工具](https://wave.webaim.org/) +- [颜色对比度可访问性验证器](https://color.a11y.com/Contrast) diff --git a/docs/locales/zh/user_guide/migration.md b/docs/locales/zh/user_guide/migration.md new file mode 100644 index 000000000..9e519159c --- /dev/null +++ b/docs/locales/zh/user_guide/migration.md @@ -0,0 +1,78 @@ +# 迁移 + +GoToSocial 支持使用 `Move` 活动进行账号迁移。 + +这允许你将账号迁移到你的 GoToSocial 账号,或者将你的 GoToSocial 账号迁移到其他账号。 + +迁移是软件无关的,因此你可以将账号迁移到其它软件或从任何支持 `Move` 活动的软件发起迁移,无论具体的软件是什么。例如,你可以将 GoToSocial 账号迁移到 Mastodon 账号,将 Mastodon 账号迁移到 GoToSocial 账号,将 GoToSocial 账号迁移到或从 Akkoma、Misskey、GoToSocial 等。 + +!!! tip + 根据目标账号所在软件的不同,目标账号的 URI(用于别名和迁移)应该类似于 `https://mastodon.example.org/users/account_you_are_moving_to`。如果你不确定使用哪种格式,请咨询你要迁移或设置别名的实例管理员。 + +!!! warning + GoToSocial 要求 7 天的账号迁移冷却期,以防止过度切换实例(以及潜在的屏蔽规避风险)。 + + 如果任何一个发起新迁移尝试的账号在最近七天内已迁移,GoToSocial 将拒绝进行迁移,直到上一次迁移过去七天位置。 + +## 将你的 GoToSocial 账号迁移到其他账号(从 GoToSocial 迁移) + +使用迁移账号设置,你可以将你的 GoToSocial 账号迁移到给定的目标账号 URI。 + +为使迁移成功: + +1. 目标账号(你要迁移到的账号)必须反向别名到你当前的账号(你要从中迁移的账号)。 +2. 目标账号必须可从你当前账号访问,即不被你屏蔽,不屏蔽你,未被封禁,不在你当前实例的屏蔽列表中。 + +迁移你的账号将从你当前账号向粉丝发送一条消息,指示他们关注目标账号。根据你的粉丝使用的服务器软件,他们可能会自动向目标账号发送关注请求,并取消关注你当前账号。 + +目前,**只有你的粉丝会转移到新账号**。其他如关注列表、贴文、媒体、书签、点赞、屏蔽等不会转移。 + +一旦你的账号迁移完成,你当前的(旧的)账号的网页视图将显示已迁移的信息,以及迁移的目标账号。 + +除非手动删除,否则旧账号的贴文和媒体仍可在这个已迁移的账号的网页视图中看到。如果你愿意,可以请求你迁出的实例管理员在迁移完成后封禁/删除你的账号。 + +如有必要,你可以使用相同的目标账号 URI 重试账号迁移。这将再次发送迁移消息。这在你的粉丝由于网络问题或其他临时故障未收到迁移消息的情况下很有用。 + +!!! danger "账号迁移是不可逆的永久操作!" + + 从 GoToSocial 触发账号迁移的那一刻起,你将仅对已迁移的账号拥有基本读取和删除权限。 + + 你仍然可以登录旧账号,查看自己的贴文、点赞、收藏、屏蔽和列表。 + + 你也可以编辑个人资料,删除和/或取消置顶自己的贴文,以及取消转发、取消点赞和取消收藏。 + + 但是,你将无法执行任何涉及创建内容的操作,如创建贴文、转发、添加收藏或点赞、关注他人、上传媒体、创建列表等。 + + 此外,你将无法查看任何时间线(主页标签、公共列表),或使用搜索功能。 + +## 将账号迁移到你的 GoToSocial 账号(迁移到 GoToSocial) + +要成功从其他账号向你的 GoToSocial 账号触发迁移,你必须首先创建一个**别名**,将你的 GoToSocial 账号链接回你要发起迁移的账号,以表明你也拥有要迁移到的 GoToSocial 账号。 + +为此,你必须首先使用你的 GoToSocial 账号登录 GoToSocial 设置面板。例如,如果你的 GoToSocial 实例位于 `https://example.org`,你应登录设置面板 `https://example.org/settings`。 + +然后,进入“迁移”部分,查看“别名账号”子部分: + +![展示已填写账号别名的别名账号子部分。](../public/migration-aliasing.png) + +在第一个还未填写账号别名框中,输入你希望**发起迁移**的账号的 URL。这表示你要发起迁移的账号属于你,即你“也被称为”该账号。 + +例如,如果你要从实例 `ondergrond.org` 上的账号 `@dumpsterqueer` 迁移,应输入 `https://ondergrond.org/@dumpsterqueer` 或 `https://ondergrond.org/users/dumpsterqueer` 作为账号别名,如上图所示。 + +输入别名后,点击“保存账号别名”按钮。如果一切顺利,按钮上会显示一个勾。如果不行,会显示一个错误帮助你判断出错的原因。 + +一旦你从 GoToSocial 账号创建了指向要发起迁移的账号的别名,你可以在另一账号所在实例的设置面板发起到你的 GoToSocial 账号的迁移。 + +在 Mastodon 上,“账号迁移”设置部分看起来如下: + +![Mastodon “账号迁移”设置页面。](../public/migration-mastodon.png) + +如果你正在从 Mastodon 账号迁移到 GoToSocial 账号,你会在“新账号代号”字段中填写 GoToSocial 账号的 `@[username]@[domain]` 值。例如,如果你的 GoToSocial 账号用户名是 `@someone`,并且在实例 `example.org` 上,那么在此处输入 `@someone@example.org`。 + +一旦触发从其他账号到 GoToSocial 账号的迁移,你唯一需要做的就是在新(GoToSocial)账号上接受来自旧账号粉丝的关注请求。 + +!!! tip + 为了省去麻烦,可以考虑在触发迁移前将 GoToSocial 账号设置为不需要批准新的关注请求。迁移完成后再开启关注请求审核。否则,你将需要手动批准每个从旧账号迁移的粉丝。 + +!!! tip + 迁移账号后,可能需要将之前账号的关注列表导入 GoToSocial 账号。[在此查看](./settings.md#import)如何通过设置面板完成此操作的详细信息。 diff --git a/docs/locales/zh/user_guide/password_management.md b/docs/locales/zh/user_guide/password_management.md new file mode 100644 index 000000000..0f72e1bfe --- /dev/null +++ b/docs/locales/zh/user_guide/password_management.md @@ -0,0 +1,19 @@ +# 密码管理 + +## 更改你的密码 + +你可以使用[用户设置面板](./settings.md)来更改密码。只需登录用户设置面板,滑动到页面底部,输入你的旧密码和想要的新密码即可。 + +如果你提供的新密码不够长或不够复杂,设置面板会报错并提示你尝试其他密码。 + +如果你的实例使用 OIDC(即通过 Google 或其他外部提供商登录),你需要通过 OIDC 提供商,而不是通过用户设置面板更改密码。 + +## 密码存储 + +GoToSocial 使用安全的 [bcrypt](https://en.wikipedia.org/wiki/Bcrypt) 函数,通过 [Go 标准库](https://pkg.go.dev/golang.org/x/crypto/bcrypt)在数据库中存储用户密码的哈希值。 + +这意味着,即使你的 GoToSocial 实例数据库遭到破坏,你的密码明文也是安全的。这也意味着你的实例管理员无法访问你的密码。 + +为了在接受前检查密码是否足够安全,GoToSocial 使用[这个库](https://github.com/wagslane/go-password-validator),其熵值设置为 60。这意味着像 `password` 这样的密码会被拒绝,但像 `verylongandsecurepasswordhahaha` 这样的密码会被接受,即使没有特殊字符/大写和小写字母等。 + +我们建议遵循 EFF 关于[创建强密码](https://ssd.eff.org/en/module/creating-strong-passwords)的指南。 diff --git a/docs/locales/zh/user_guide/posts.md b/docs/locales/zh/user_guide/posts.md new file mode 100644 index 000000000..8c41dcd77 --- /dev/null +++ b/docs/locales/zh/user_guide/posts.md @@ -0,0 +1,300 @@ +# 贴文 + +## 隐私设置 + +GoToSocial 为贴文提供 Mastodon 风格的隐私设置。从最私密到最不私密的顺序是: + +* 私信 +* 互关可见 +* 私密/仅粉丝可见 +* 不列出 +* 公开 + +无论为贴文选择哪种隐私设置,GoToSocial 都会尽力确保你的贴文不会出现在你已屏蔽的实例或你直接屏蔽的用户面前。 + +与一些其他联邦宇宙服务端实现不同,GoToSocial 对新账户使用 `不列出` 作为默认的贴文设置,而不是使用 `公开`。我们的理念是,将某条贴文公开始终应该是一个明确作出的决定,而不是默认选择。 + +请注意,尽管 GoToSocial 非常严格地遵循这些隐私设置,但其他服务端实现不一定可靠:联合网络中存在不良行为者。与任何社交媒体一样,你应该仔细考虑你发布的内容及其受众。 + +### 私信 + +`私信` 可见性的贴文只会显示给贴文作者和贴文中提到的用户。以下是一个示例: + +```text +嘿 @whoever@example.org,这是一条私信!只有咱们能看到! +``` + +如果这条消息是由 `@someone@server.com` 写的,那么只有 `@whoever@example.org` 和 `@someone@server.com` 能看到。 + +顾名思义,`私信` 贴文用于你希望与一个或多个特定人交流的情况。 + +然而,`私信` 贴文并不是替代端到端加密消息(如 [Signal](https://signal.org/) 和 [Matrix](https://matrix.org/))的合适选择。如果你要直接交流,但不是传递敏感信息,那么 `私信` 贴文是可以胜任的。如果你需要进行敏感且安全的交流,请使用其他工具! + +私信贴文可以被点赞,但不能被转发。 + +私信贴文在你的 GoToSocial 实例上无法通过网页 URL 访问。 + +### 互关可见 + +`互关可见` 的贴文只会显示给贴文作者和与作者*互相关注*的人。换句话说,只有在满足两个条件时,其他人才能看到: + +1. 其他账户关注贴文作者。 +2. 贴文作者也关注其他账户。 + +这在你希望只有朋友能看到某些内容时很有用。 + +互关可见贴文可以被点赞,但不能被转发。 + +互关可见贴文在你的 GoToSocial 实例上无法通过网页 URL 访问。 + +### 私密/仅粉丝可见 + +`仅粉丝` 贴文只对贴文作者和关注贴文作者的人可见。与 `互关可见` 类似,但只需满足第一个条件;贴文作者不需要回关其他账户。 + +这在你想向粉丝发布公告,或分享一些比 `互关可见` 稍微不那么私密的内容时很有用。 + +私密/仅粉丝可见贴文可以被点赞,但不能被转发。 + +私密/仅粉丝可见贴文在你的 GoToSocial 实例上无法通过网页 URL 访问。 + +### 不列出 + +`不列出`(有时称为 `未锁定/悄悄公开`)的贴文是半公开的。它们会被发送给关注你的人,并可以转发到未关注你的人的时间线中,但不会出现在跨站或本站时间线上,也不会出现在你的公开资料页中。 + +不列出贴文适用于你希望某个贴文传播,但不想被所有人立即看到的情况。也可以用于发布相对公开的贴文,同时不让跨站/本站时间线被占用。 + +不列出贴文可以被点赞,也可以被转发。 + +与 Mastodon 不同,不列出贴文在你的 GoToSocial 实例上无法通过网页 URL 访问! + +### 公开 + +`公开` 可见性的贴文是*完全*公开的。它们可以通过网络看到,出现在本地和联合时间线上,并且完全可以被转发。`公开` 是将贴文广泛传播和易于分发的终极设置,适用于你希望某些内容可被广泛访问的情况。 + +公开贴文可以被点赞,也可以被转发。 + +**公开贴文可以在你的 GoToSocial 实例上通过网页 URL 访问!** + +## 输入类型 + +GoToSocial 当前接受两种不同类型的贴文(以及用户简介)输入。你可以在[用户设置页面](./settings.md)在这两种类型之间进行选择。它们分别是: + +* `plain` +* `Markdown` + +纯文本(`plain`)是默认的发帖方式:GtS 接受一些简单的文本,通过解析链接和提及等将其转化为优雅的 HTML。如果你习惯于使用 Mastodon 或 Twitter 或大多数其他社交媒体平台,这种发帖方式会让你一见如故。 + +Markdown 是一种更复杂的组织文本的方式,在如何解析和格式化文本方面给你更多的控制权。 + +GoToSocial 支持[基本 Markdown 语法](https://www.markdownguide.org/basic-syntax),以及部分[扩展 Markdown 语法](https://www.markdownguide.org/extended-syntax/),包括独立代码块、脚注、删除线、下标、上标和自动 URL 链接。 + +你还可以在你的 markdown 中包含基本 HTML 片段! + +关于 Markdown 的更多信息,请参阅[Markdown 指南](https://www.markdownguide.org/)。 + +关于 Markdown 语法的速查表,请参阅[Markdown 速查表](https://www.markdownguide.org/cheat-sheet)。 + +## 媒体附件 + +GoToSocial 允许你在贴文中附加媒体文件,大多数客户端会在贴文底部以画廊视图呈现这些文件。默认情况下,你可以向贴文附加 6 个媒体文件,但这可能会根据你使用的客户端和实例配置而有所不同。 + +目前支持以下文件类型: + +- image/jpeg +- image/gif +- image/png +- image/webp +- video/mp4(大多数类型) + +默认情况下,上传媒体的大小限制为 40MB,但这可能会因实例配置而有所不同。 + +### 图片描述(alt 文本) + +当你在贴文中附加图片或视频等媒体时,大多数客户端会提供选项,让你为图片或视频的内容撰写描述。这个描述将作为所有用户查看媒体时的 alt 文本出现。这对所有人,尤其是对盲人或视力部分受损的人来说有用。如果没有图片描述,对方可能难以理解媒体中包含的内容以及为何你将其附加到特定贴文中。 + +撰写好的图片描述可能很难,但这样做非常值得! + +> 图片描述是一种表示关心的行为,也是无障碍的基本组成部分。没有它们,内容对盲人/视力低下的人来说将完全不可用。通过撰写图片描述,我们展现了对跨残疾团结及运动团结的支持。 + +-- Alex Chen,[如何撰写图片描述](https://uxdesign.cc/how-to-write-an-image-description-2f30d3bf5546)。 + +### Exif 数据 + +当照片或视频拍摄时,大多数传统相机和手机相机会将 [Exif 数据标签](https://en.wikipedia.org/wiki/Exif) 编码为结果媒体的元数据。此 Exif 数据包含如下内容: + +- 相机的品牌和型号。 +- 图片或视频的颜色和像素信息。 +- 图片或视频的尺寸和方向。 +- 日期和时间信息。 +- 位置信息(如果启用)。 + +一般来说,这些 Exif 数据点用于摄影师帮助整理他们自己的图片。然而,遗憾的是,它们也带来了[隐私和安全影响](https://en.wikipedia.org/wiki/Exif#Privacy_and_security),特别是在涉及位置信息时。如果你曾在网上平台(如 Facebook)发布图片,你可能会想知道 Facebook 是如何知道图片的拍摄地点和时间的;这很大程度上归因于 Exif 数据中嵌入的位置信息和时间戳,Facebook 从中读取图片信息,以组装一条“你曾去过的地方”的时间线。 + +为了避免泄漏你的位置信息,GoToSocial 努力在上传媒体时通过清零 Exif 数据点移除 Exif 信息。 + +!!! danger + 为了方便和保护隐私,GoToSocial 在上传图片文件时会自动移除 Exif 标签。然而,**无法自动移除 mp4 视频的 Exif 数据**(参见 [#2577](https://github.com/superseriousbusiness/gotosocial/issues/2577))。 + + 在你将视频上传至 GoToSocial 之前,建议确保该视频的 Exif 数据标签已经被移除。你可以在线找到多种工具和服务来做到这一点。 + + 为防止 Exif 位置信息在一开始被写入图片或视频中,你还可以关闭设备摄像头应用中的位置标记(通常称为地理标记)。 + +!!! tip + 即使你在上传图片或视频之前已完全移除所有 Exif 元数据,恶意用户仍然可以通过媒体本身的内容推断出你的位置信息。 + + 如果你属于在生产中有保密需要的组织,或正在被跟踪或监视,你可能需要考虑不要发布任何可能含有你位置线索的媒体。 + +## 格式化 + +当贴文以 `plain`(纯文本) 格式提交时,GoToSocial 会自动进行一些整理和格式化,将其转换为 HTML,如下所述。 + +### 空格 + +任何开头或结尾的空格和换行都会从贴文中去除。因此,例如: + +```text + + +这个贴文以换行开头 +``` + +将变为: + +```text +这个贴文以换行开头 +``` + +### 包裹 + +整个贴文将被 `

` 包裹。 + +因此以下文本: + +```text +你好,这是一条很短的贴文! +``` + +将变为: + +```html +

你好,这是一条很短的贴文!

+``` + +### 换行 + +任何换行符都将被替换为 `
` + +继续上述示例: + +```text +你好,这是一条很短的贴文! + +这是另一行。 +``` + +将变为: + +```html +

你好,这是一条很短的贴文!

这是另一行。

+``` + +### 链接 + +任何可识别的链接将在文本中被缩短并转换为适当的超链接,还会添加一些其他属性。 + +例如: + +```text +这里是某个链接:https://example.org/some/link/address +``` + +将变为: + +```html +这里是某个链接:example.org/some/link/address +``` + +呈现为: + +> 这里是某个链接:[example.org/some/link/address](https://example.org/some/link/address) + +注意这仅对 `http` 和 `https` 链接有效;其他协议不支持。 + +### 提及 + +你可以通过以下方式提及其他账户: + +> @some_account@example.org + +在这个例子中,`some_account` 是你要提及的账户的用户名,`example.org` 是托管他们账户的域名。 + +被提及的账户将收到你提到他们的通知,并能够看到提及他们的贴文。 + +提及的格式类似于链接,所以: + +```text +嗨 @some_account@example.org 最近怎么样? +``` + +将变为: + +```html +嗨 @some_account 最近怎么样? +``` + +呈现为: + +> 嗨 @some_account 最近怎么样? + +当提及本站账户(即你的实例上的账户)时,提及第二部分是不必要的。如果在你的实例上有一个名为 `local_account_person` 的账户,你可以通过写: + +```text +嘿 @local_account_person 你是我的网上邻居 +``` + +变为: + +```html +嘿 @local_account_person 你是我的网上邻居 +``` + +呈现为: + +> 嘿 @local_account_person 你是我的网上邻居 + +### 话题标签 + +你可以在贴文中使用一个或多个话题标签来指示贴文主题,并允许贴文与其他使用相同话题标签的贴文被归入同一分组,以帮助你的贴文被他人发现。 + +大多数 ActivityPub 服务端实现,如 Mastodon 等,只会通过它们使用的话题标签对**公开**贴文进行分组,但这并不是绝对的。一般来说,最好只对那些你希望能比其他情况下更广泛传播的公开可见贴文使用话题标签。这方面的一个好例子是 `#introduction` 话题标签,通常用于新账户想要向联邦宇宙介绍自己时使用! + +在贴文中包含话题标签的方式类似于大多数其他社交媒体软件:只需在你想用作话题标签的词前加上 `#` 符号。 + +一些示例: + +* `#introduction` +* `#Mosstodon` +* `#LichenSubscribe` + +在 GoToSocial 中,话题标签不区分大小写,因此无论你在书写话题标签时使用大写、小写或两者混合,都会被视为相同的话题标签。例如,`#Introduction` 和 `#introduction` 会被视为完全相同。 + +出于可访问性原因,在书写话题标签时,考虑使用大驼峰式(即每个单词的首字母大写)是更好的。换句话说:要把 `#thisisahashtag` 替换为 `#ThisIsAHashtag`。这样不仅视觉上更易读,屏幕阅读器也更容易朗读。 + +你可以在 GoToSocial 贴文中包含任意数量的话题标签,而且每个话题标签的长度限制为 100 个字符。 + +## 输入净化 + +为了不传播脚本、漏洞以及不稳定的 HTML,GoToSocial 执行以下类型的输入净化: + +`plain` 输入类型: + +* 在解析前,会完全移除贴文正文和内容警告字段中的已有 HTML。 +* 在解析后,所有生成的 HTML 都会通过清理器处理以移除有害元素。 + +`Markdown` 输入类型: + +* 在解析前,会完全移除内容警告字段中的已有 HTML。 +* 在解析前,贴文正文中的现有 HTML 会通过清理器处理以移除有害元素。 +* 在解析后,所有生成的 HTML 都会通过清理器处理以移除有害元素。 + +GoToSocial 使用 [bluemonday](https://github.com/microcosm-cc/bluemonday) 进行 HTML 清理。 diff --git a/docs/locales/zh/user_guide/rss.md b/docs/locales/zh/user_guide/rss.md new file mode 100644 index 000000000..eb7f78ab3 --- /dev/null +++ b/docs/locales/zh/user_guide/rss.md @@ -0,0 +1,15 @@ +# RSS + +RSS 是[简易信息聚合](https://en.wikipedia.org/wiki/RSS)的缩写。这是一种在网络上共享内容的非常成熟的标准。你可能在你喜欢的新闻网站和博客上看到过这个活泼的橙色 RSS 标志: + +![橙色 RSS 图标](../public/rss.svg) + +如果你愿意,可以配置你的 GoToSocial 账户,以将你的贴文以 RSS 订阅源的形式发布到网上。这让没有 Fediverse 账户的人也能定期获取你的贴文更新。这非常适合使用 GoToSocial 发布长篇博客形式的贴文,并希望任何人都能轻松阅读它们的情况。 + +GoToSocial 账户的 RSS 源默认是关闭的。你可以通过[用户设置](./settings.md)在 `https://[你的实例域名]/settings` 启用它。 + +启用后,你的账户的 RSS 订阅将可在 `https://[你的实例域名]/@[你的用户名]/feed.rss` 获取。如果你使用 RSS 阅读器,可以用其打开这个地址以检查 RSS 是否正常工作。 + +## 哪些贴文会通过 RSS 分享? + +只有你最近的 20 篇公开贴文会通过 RSS 分享。回复和转发不包括在内。不列出的贴文也不包括在内。换句话说,通过 RSS 可见的贴文将仅是通过浏览器打开你的账户页时可见的贴文。 diff --git a/docs/locales/zh/user_guide/search.md b/docs/locales/zh/user_guide/search.md new file mode 100644 index 000000000..b9d4d2bc5 --- /dev/null +++ b/docs/locales/zh/user_guide/search.md @@ -0,0 +1,20 @@ +# 搜索 + +## 查询格式 + +GoToSocial 接受多种搜索查询格式: + +- `@用户名`:在所有已知实例中搜索给定用户名的账户。可能返回多个结果。 +- `@用户名@域`:搜索具有特定用户名和域名的外站账户。最多只会返回1个结果。 +- `https://example.org/some/arbitrary/url`:搜索具有给定 URL 的账户或贴文。如果账户或贴文尚未联合到 GoToSocial,将尝试获取它。最多只会返回1个结果。 +- `#标签名`:搜索具有给定标签名或以其开头的话题标签。大小写不敏感。可能返回多个结果。 +- `任意文本`:搜索包含该文本的贴文、包含该文本的话题标签,以及用户名、显示名或简介中含有该文本的账户。将搜索由你撰写的贴文以及回复你的贴文。仅在你关注的账户中搜索账户简介。可能返回多个结果。 + +## 搜索运算符 + +搜索关键词可以包含以下搜索运算符: + +- `from:用户名`:将结果限制为由指定**本站**账户创建的贴文。 +- `from:用户名@域`:将结果限制为由指定外站账户创建的贴文。 + +例如,你可以搜索 `sloth from:你的用户名` 来查找你关于树懒的贴文。 diff --git a/docs/locales/zh/user_guide/settings.md b/docs/locales/zh/user_guide/settings.md new file mode 100644 index 000000000..37ca049a7 --- /dev/null +++ b/docs/locales/zh/user_guide/settings.md @@ -0,0 +1,268 @@ +# 设置 + +GoToSocial 提供了一个设置界面,你可以在这里更新你的贴文和账户设置,添加头像和横幅背景图,为你的账户撰写简介等。 + +你可以通过访问自己 GoToSocial 实例的 `https://my-instance.example.com/settings` 来访问设置。设置面板同样使用 OAuth 机制进行身份验证。 + +在提供实例 URL 后,你会看到提示,要求你使用电子邮件地址和密码登录。 + +## 账户 + +![用户设置界面的账户部分截图,显示头像、横幅背景图和昵称的预览,并提供更改它们的表单字段](../public/user-settings-profile-info.png) + +在账户部分,你可以更改昵称、头像和横幅背景图。你还可以选择启用手动批准关注请求,并选择提供公开的贴文 RSS 源。 + +### 设置头像/横幅背景图 + +要设置头像或横幅背景图,请在相应部分点击 `浏览` 按钮,并使用文件浏览器选择图像。 + +当前支持的图像格式有 `gif`、`png`、`webp` 和 `jpeg`/`jpg`。 + +页面底部会显示图像在你的账户中的预览。如果你对选择满意,点击页面底部的 `保存账户信息` 按钮。 + +如果你转到自己的账户页并刷新页面,页面将会显示新的头像/横幅背景图。这个更新可能需要一些时间才能传播到其他外站实例。 + +### 选择主题 + +GoToSocial 提供主题供你选择,以更改账户的外观和氛围。 + +要选择主题,只需在账户设置页面中选择,然后点击页面底部的 `保存账户信息`。用浏览器查看账户页(可能需要刷新页面),你会看到新主题已被应用,访问你账户的其他人也会看到。 + +!!! tip "添加更多主题" + 实例管理员可以通过将 CSS 文件放入 `web/assets/themes` 文件夹中来添加更多主题。有关详细信息,请参阅管理员文档中的[主题](../admin/themes.md)部分。 + +### 基本信息 + +#### 昵称 + +昵称是与你的用户名一起显示在账户上的简短标识。 + +尽管创建后无法更改用户名,但昵称可以更改。 + +昵称可以包含空格、大写字母、表情符号等。 + +这是放网名或全名的好地方。例如,如果你的用户名是 `@miranda`,昵称可以是 `Miranda Priestly`。 + +#### 简介 + +你的简介是介绍你的账户和自己的较长文本。适合用于: + +- 提示你贴文的内容。 +- 提及大致年龄/位置。 +- 链接到你的其他账户或账户。 +- 说明与他人互动时的边界和偏好。 +- 链接你经常使用的标签。 + +简介支持 `纯文本` 或 `markdown` 格式。默认贴文格式设置如[贴文设置](#贴文设置)中所述。 + +#### 资料字段 + +资料字段是一系列名称/值对,将显示在账户上,并传播到其他外站实例。 + +此处适合放置的信息包括: + +- 你的网站链接 +- 众筹/捐助页面的链接 +- 你的年龄 +- 人称代词 + +一些示例: + +- 别名:汉德尔·沃尔特 +- 我的网站:https://example.org +- 年龄:99 +- 代词:she/her/她 +- 我的其他账户:@someone@somewhere.com + +### 可见性和隐私 + +#### 个人资料上显示的贴文可见性级别 + +使用此下拉菜单,你可以选择在你的网页版账户页、贴文以及 RSS 源(如果你已启用 RSS)上显示的贴文可见性级别。 + +**默认情况下,GoToSocial 仅在网页版账户页上显示公开可见的贴文,而不显示不列出的贴文。** 你可以调整此设置,以显示不列出的贴文,这类似于其他 ActivityPub 软件如 Mastodon 的默认设置。 + +你还可以选择在 GoToSocial 的网页视图上完全不显示任何贴文。这样,你可以安心发表贴文,而无需担心有人通过网页浏览你的个人资料并查看你的贴文。 + +此设置仅适用于你自己的贴文的可见性。其他用户的“不列出”贴文永远不会显示。 + +此设置不会影响你的贴文在 ActivityPub 协议和客户端中的可见性,因此即便你选择不在网页版账户页显示任何贴文,只要他人是你的粉丝、你的贴文被转发到他们的时间线,或使用链接搜索你的某个贴文,他们仍然可以看到的贴文。 + +!!! warning + 请注意,此设置的更改也会应用于之前的贴文。 + + 也就是说,如果你之前发布了一条“不列出”可见性的贴文,而当时你的网页版账户页被设置为仅显示公开贴文,此时如果你更改此设置为一并显示公开和不列出,那你之前发布的“不列出”贴文将会与公开贴文一起显示在你的网页版账户页上。 + + 同样地,如果你选择不显示任何贴文,那么所有贴文将从你的网页版账户页中隐藏,无论它们是在何时创建,也无论当时此选项被设置为什么。这种情况将持续直到你再次更改此设置。 + +!!! tip + 结合(域名)屏蔽,如果有人通过公开贴文骚扰你,这是一种很好的“紧急”设置。虽然它不会阻止在 ActivityPub 客户端中可以看到你的贴文的人,但至少会防止他们无需身份验证就通过浏览器点击查看你的贴文,并通过 URL 轻松与他人分享。 + +#### 手动批准关注请求(即锁定帐户) + +此复选框允许你决定是否希望手动审核账户的关注请求。 + +当此选项**未勾选**时,新关注请求会被自动批准,无需你的干预。对于更面向公众的账户或不怎么发布敏感信息的情况而言,这很有用。 + +当其**已勾选**时,你必须手动批准新关注请求,并可以拒绝你不想被关注的账号的关注请求。这对仅向粉丝发布私人内容的私人账户很有用。 + +在联邦宇宙中,一般将此选项称为“锁定”账户。 + +勾选或取消勾选复选框后,务必点击底部的 `保存账户信息` 按钮以保存新设置。 + +#### 将账户标记为可被搜索引擎和目录发现 + +此设置用于更新账户上的“可发现”标记。 + +选中账户的可发现性框可执行以下操作: + +- 更新账户的 robots 元信息标记,允许其被搜索引擎索引并出现在搜索引擎结果中。 +- 向外站指示账户可包含在公共目录和索引中。 + +将可发现性标记打开可能需要一周或更长时间才会生效,账户不会立即出现在搜索引擎结果中。 + +!!! tip + 为了避免暴露给爬虫,新帐户的可发现性默认为 false。但对于希望被抓取的面向公众的帐户,将其设置为 true 是有用的。 + +!!! info + 可发现性设置是关于**账户的可发现性**,而不是贴文的可被搜索性。这与 Mastodon 实例或其他使用全文搜索的实例的贴文索引无关! + +#### 启用公开贴文的 RSS 源 + +用户的 RSS 源默认情况下是禁用的,但可以通过此复选框选择。有关更多信息,请参阅 [RSS](./rss.md)。 + +此源仅包括设置为“公开”的贴文(参见 [隐私设置](./posts.md#隐私设置))。 + +!!! warning + 公开您的 RSS 源允许*任何人*匿名订阅您公开贴文的更新,绕过关注和关注请求。 + +#### 隐藏你关注/被关注的人 + +默认情况下,GoToSocial 会在你的公开网络资料上显示你的关注/粉丝数量,并允许其他人查看你关注的和关注你的人。这对于账户发现可能很有用。然而,出于隐私和安全原因,你可能希望隐藏这些信息,并隐藏其他账户的关注/粉丝清单。你可以通过勾选此框来做到这一点。 + +勾选此框后,你的关注/粉丝数量将从你的公开网络资料中隐藏,其他人将无法浏览你的关注/粉丝清单。 + +### 进阶 + +#### 自定义 CSS + +如果实例管理员允许,自定义 CSS 可以让你进一步自定义账户在浏览器中的外观。 + +如果此设置未被实例管理员启用,文本输入框将为只读状态,自定义 CSS 将不会应用。 + +请参阅 [自定义 CSS](./custom_css.md) 页面,了解有关为账户编写自定义 CSS 的一些提示。 + +!!! tip + 你在此框中添加的任何自定义 CSS 都将在*选择主题之后*应用,因此你可以选择一个喜欢的预设主题,然后进行自己的调整! + +## 贴文 + +### 贴文设置 + +默认贴文语言设置允许你向其他用户声明你的贴文通常使用哪种语言。这对说其它语言(例如韩语)并希望过滤掉其他语言的贴文的用户很有帮助。 + +默认贴文可见性设置允许你设置新贴文的默认可见性。当你通常发布公开或只对粉丝可见的贴文,但不想每次发贴时都设置隐私时会很有用。请记住,这只是默认设置:无论你在此处设置什么,仍然可以根据需要单独设置新贴文的隐私。有关贴文隐私设置的更多信息,请参阅[贴文页面](./posts.md)。 + +默认贴文格式设置允许你选择在解析贴文时使用哪个文本解释器。 + +plain(默认)设置提供标准贴文格式,类似于许多其他联邦宇宙服务端使用的格式。这非常适合一般目的的发布:你可以写简短的推特风格的贴文,或多段文章,插入链接,并使用用户名提及其他账户。 + +markdown 设置表示你的贴文应被按 Markdown 格式解析,这是一种标记语言,提供更多选项来自定义贴文的布局和外观。有关 plain 和 markdown 贴文格式之间差异的更多信息,请参阅 [贴文页面](posts.md)。 + +更新贴文设置后,请记得点击该部分底部的 `保存设置` 按钮以保存更改。 + +### 默认互动规则 + +通过此部分,你可以为新贴文设置每个可见级别的默认互动规则。这允许你精确控制他人如何与你的贴文互动。 + +这使你能够做以下事情: + +- 创建只有你自己可以互动的贴文。 +- 创建仅粉丝/你关注的人可以互动的贴文。 +- 创建任何人都可以点赞或转发,但只有特定人可以回复的贴文。 +- 等等。 + +例如,下图显示了一个公开可见性贴文的默认互动规则,允许任何人点赞或转发,但仅允许粉丝和你关注的人回复。 + +![互动规则,图中显示“谁可以点赞” = “任何人”,“谁可以回复” = “粉丝”和“关注的人”,“谁可以转发” = “任何人”。](../public/user-settings-interaction-policy-1.png) + +请记住,互动规则不具备前向可追溯性。应用默认互动规则之后创建的贴文将默认使用新设置的规则,但在此之前创建的任何贴文将使用创建时的默认规则。 + +无论在贴文上设置了什么规则,首先被考虑的仍是可见性设置和账户屏蔽情况。例如,如果你将某种类型的互动范围设置为“任何人”,这仍然会排除你屏蔽的账户,或你实例屏蔽的域名域下的账户。“任何人”在这种情况下基本上意味着“任何通常能看到贴文的人”。 + +最后,请注意,无论为贴文设置了什么规则,贴文中提到的任何账户将**始终**能够回复该贴文。 + +更新互动规则设置后,请记得点击该部分底部的 `保存规则` 按钮以保存更改。 + +如果你想将所有规则重置为初始默认值,可以点击 `重置为默认值` 按钮。 + +!!! danger + 虽然 GoToSocial 尊重互动规则,但不能保证其他服务端软件也会这样做,即使你的实例禁止某些互动,其他服务器上的账户可能仍会向其粉丝发送(被禁止的)贴文回复和转发。 + + 随着更多 ActivityPub 服务端推出互动规则支持,这个问题有望减少,但在此期间,GoToSocial 只能在“尽力而为”范围内进行尝试,以根据你设定的规则限制与贴文的互动。 + +## 电子邮箱和密码 + +### 更改电子邮箱 + +你可以使用面板的更改电子邮箱部分更改账户的电子邮箱地址。出于安全原因,你必须提供当前密码以验证更改。 + +输入新电子邮箱地址,并点击“更改电子邮箱地址”后,必须打开新电子邮件地址的收件箱,并通过提供的链接确认地址。完成后,你的电子邮箱地址更改将被确认。 + +!!! info + 如果你的实例使用 OIDC 作为授权/身份提供商,你可以通过设置面板更改电子邮箱地址,但只会影响 GoToSocial 用于联系你的电子邮箱地址,而不会更改用于登录账户的电子邮箱地址。要更改此项,应联系你的 OIDC 提供商。 + +### 更改密码 + +你可以使用面板的更改密码部分为账户设置新密码。出于安全原因,你必须提供当前密码以验证更改。 + +!!! info + 如果你的实例使用 OIDC 作为授权/身份提供商,你将无法通过 GoToSocial 设置面板更改密码,此时应联系你的 OIDC 提供商。 + +有关 GoToSocial 如何管理密码的更多信息,请参阅[密码管理文档](./password_management.md)。 + +## 迁移 + +在迁移部分,你可以管理与与账户别名、迁移到其他账户或从其他账户迁移相关的设置。 + +有关移动账户的更多信息,请参阅[迁移文档](./migration.md)。 + +## 导出和导入 + +在导出和导入部分,你可以从 GoToSocial 账户导出数据或将数据导入账户。 + +![导出/导入页面。](../public/user-settings-export-import.png) + +### 导出 + +要导出你的关注、粉丝、账户列表、账户屏蔽列表或账户静音列表,你可以使用此页面上的按钮。 + +所有导出都将以 Mastodon 导出格式兼容的 CSV 格式提供,因此如果有需要,可以将其导入 Mastodon 或另一个 GoToSocial 实例。 + +### 导入 + +你可以使用导入部分,将其他账户的数据导入到 GoToSocial 账户中,使用从其他账户导出的 CSV 文件。 + +这在你已将账户[迁移](./migration.md)到 GoToSocial 账户,并希望保留在以前的账户上的关注列表和屏蔽列表时很有用。 + +要将数据导入账户,首先点击“浏览”并选择从 Mastodon 或其他兼容实例导出的与 Mastodon 导出格式兼容的 CSV 文件。 + +然后,使用下拉菜单选择通过 CSV 文件上传的数据类型。 + +!!! warning + 在选择“类型”时要小心,否则可能会意外封禁你计划关注的一堆账户,反之亦然! + +然后,选择是要**合并**新数据到 GoToSocial 账户中该类型的现有数据,还是要用 CSV 文件中包含的数据**覆盖**现有数据。 + +如果选择**合并**,则 CSV 文件中包含的任何数据都将添加到现有数据中,而不会删除任何现有数据。 + +例如,如果你的 GoToSocial 账户关注 `account1` 和 `account2`,并且正在上传一个包含 `account3` 和 `account4` 的关注 CSV 文件,并使用模式 **合并**,那么在导入结束时,你将关注 `account1`、`account2`、`account3` 和 `account4`。 + +如果选择**覆盖**,则 CSV 文件中包含的任何数据将*替换*现有数据,删除 CSV 文件中未包含的条目。 + +例如,如果你的 GoToSocial 账户关注 `account1` 和 `account2`,并上传一个包含 `account3` 和 `account4` 的关注 CSV 文件,并使用模式 **覆盖**,那么导入结束时,你将关注 `account3` 和 `account4`。你对 `account1` 和 `account2` 的关注将被移除。 + +合并和覆盖操作都是幂等的,这通常意味着现有数据和 CSV 文件中的重复条目不会产生问题,如果需要重试导入,可以多次导入相同的数据。 + +!!! info + 由于各种原因,通过导入不可能一定会重新创建上传的 CSV 文件中的每个条目。例如,假设你试图导入包含 `example_account` 的关注 CSV,但 `example_account` 的实例已下线,或者它们的实例封禁了你的实例,或你的实例封禁了它们的实例等。在这种情况下,将无法创建对 `example_account` 的关注。 diff --git a/docs/assets/admin-settings-emoji-local.png b/docs/overrides/public/admin-settings-emoji-local.png similarity index 100% rename from docs/assets/admin-settings-emoji-local.png rename to docs/overrides/public/admin-settings-emoji-local.png diff --git a/docs/assets/admin-settings-emoji-remote.png b/docs/overrides/public/admin-settings-emoji-remote.png similarity index 100% rename from docs/assets/admin-settings-emoji-remote.png rename to docs/overrides/public/admin-settings-emoji-remote.png diff --git a/docs/assets/admin-settings-federation-import-export.png b/docs/overrides/public/admin-settings-federation-import-export.png similarity index 100% rename from docs/assets/admin-settings-federation-import-export.png rename to docs/overrides/public/admin-settings-federation-import-export.png diff --git a/docs/assets/admin-settings-federation.png b/docs/overrides/public/admin-settings-federation.png similarity index 100% rename from docs/assets/admin-settings-federation.png rename to docs/overrides/public/admin-settings-federation.png diff --git a/docs/assets/admin-settings-instance.png b/docs/overrides/public/admin-settings-instance.png similarity index 100% rename from docs/assets/admin-settings-instance.png rename to docs/overrides/public/admin-settings-instance.png diff --git a/docs/assets/admin-settings-report-detail.png b/docs/overrides/public/admin-settings-report-detail.png similarity index 100% rename from docs/assets/admin-settings-report-detail.png rename to docs/overrides/public/admin-settings-report-detail.png diff --git a/docs/assets/admin-settings-reports.png b/docs/overrides/public/admin-settings-reports.png similarity index 100% rename from docs/assets/admin-settings-reports.png rename to docs/overrides/public/admin-settings-reports.png diff --git a/docs/assets/ap_logo.svg b/docs/overrides/public/ap_logo.svg similarity index 100% rename from docs/assets/ap_logo.svg rename to docs/overrides/public/ap_logo.svg diff --git a/docs/assets/css/colours.css b/docs/overrides/public/css/colours.css similarity index 100% rename from docs/assets/css/colours.css rename to docs/overrides/public/css/colours.css diff --git a/docs/assets/cssblack.png b/docs/overrides/public/cssblack.png similarity index 100% rename from docs/assets/cssblack.png rename to docs/overrides/public/cssblack.png diff --git a/docs/assets/cssgradient.png b/docs/overrides/public/cssgradient.png similarity index 100% rename from docs/assets/cssgradient.png rename to docs/overrides/public/cssgradient.png diff --git a/docs/assets/cssstandard.png b/docs/overrides/public/cssstandard.png similarity index 100% rename from docs/assets/cssstandard.png rename to docs/overrides/public/cssstandard.png diff --git a/docs/assets/diagrams/conversation_thread.drawio b/docs/overrides/public/diagrams/conversation_thread.drawio similarity index 100% rename from docs/assets/diagrams/conversation_thread.drawio rename to docs/overrides/public/diagrams/conversation_thread.drawio diff --git a/docs/assets/diagrams/conversation_thread.png b/docs/overrides/public/diagrams/conversation_thread.png similarity index 100% rename from docs/assets/diagrams/conversation_thread.png rename to docs/overrides/public/diagrams/conversation_thread.png diff --git a/docs/assets/diagrams/federation_modes.drawio b/docs/overrides/public/diagrams/federation_modes.drawio similarity index 100% rename from docs/assets/diagrams/federation_modes.drawio rename to docs/overrides/public/diagrams/federation_modes.drawio diff --git a/docs/assets/diagrams/federation_modes.png b/docs/overrides/public/diagrams/federation_modes.png similarity index 100% rename from docs/assets/diagrams/federation_modes.png rename to docs/overrides/public/diagrams/federation_modes.png diff --git a/docs/assets/getting-started-memory-graph.png b/docs/overrides/public/getting-started-memory-graph.png similarity index 100% rename from docs/assets/getting-started-memory-graph.png rename to docs/overrides/public/getting-started-memory-graph.png diff --git a/docs/assets/instancesplash.png b/docs/overrides/public/instancesplash.png similarity index 100% rename from docs/assets/instancesplash.png rename to docs/overrides/public/instancesplash.png diff --git a/docs/assets/markdown-post.png b/docs/overrides/public/markdown-post.png similarity index 100% rename from docs/assets/markdown-post.png rename to docs/overrides/public/markdown-post.png diff --git a/docs/assets/migration-aliasing.png b/docs/overrides/public/migration-aliasing.png similarity index 100% rename from docs/assets/migration-aliasing.png rename to docs/overrides/public/migration-aliasing.png diff --git a/docs/assets/migration-mastodon.png b/docs/overrides/public/migration-mastodon.png similarity index 100% rename from docs/assets/migration-mastodon.png rename to docs/overrides/public/migration-mastodon.png diff --git a/docs/assets/pinafore.png b/docs/overrides/public/pinafore.png similarity index 100% rename from docs/assets/pinafore.png rename to docs/overrides/public/pinafore.png diff --git a/docs/assets/profile1.png b/docs/overrides/public/profile1.png similarity index 100% rename from docs/assets/profile1.png rename to docs/overrides/public/profile1.png diff --git a/docs/assets/rss.svg b/docs/overrides/public/rss.svg similarity index 100% rename from docs/assets/rss.svg rename to docs/overrides/public/rss.svg diff --git a/docs/assets/signup-account.png b/docs/overrides/public/signup-account.png similarity index 100% rename from docs/assets/signup-account.png rename to docs/overrides/public/signup-account.png diff --git a/docs/assets/signup-form.png b/docs/overrides/public/signup-form.png similarity index 100% rename from docs/assets/signup-form.png rename to docs/overrides/public/signup-form.png diff --git a/docs/assets/signup-pending.png b/docs/overrides/public/signup-pending.png similarity index 100% rename from docs/assets/signup-pending.png rename to docs/overrides/public/signup-pending.png diff --git a/docs/assets/sloth.webp b/docs/overrides/public/sloth.webp similarity index 100% rename from docs/assets/sloth.webp rename to docs/overrides/public/sloth.webp diff --git a/docs/assets/theme-blurple-dark.png b/docs/overrides/public/theme-blurple-dark.png similarity index 100% rename from docs/assets/theme-blurple-dark.png rename to docs/overrides/public/theme-blurple-dark.png diff --git a/docs/assets/theme-blurple-light.png b/docs/overrides/public/theme-blurple-light.png similarity index 100% rename from docs/assets/theme-blurple-light.png rename to docs/overrides/public/theme-blurple-light.png diff --git a/docs/assets/theme-brutalist-dark.png b/docs/overrides/public/theme-brutalist-dark.png similarity index 100% rename from docs/assets/theme-brutalist-dark.png rename to docs/overrides/public/theme-brutalist-dark.png diff --git a/docs/assets/theme-brutalist-light.png b/docs/overrides/public/theme-brutalist-light.png similarity index 100% rename from docs/assets/theme-brutalist-light.png rename to docs/overrides/public/theme-brutalist-light.png diff --git a/docs/assets/theme-ecks-pee.png b/docs/overrides/public/theme-ecks-pee.png similarity index 100% rename from docs/assets/theme-ecks-pee.png rename to docs/overrides/public/theme-ecks-pee.png diff --git a/docs/assets/theme-midnight-trip.png b/docs/overrides/public/theme-midnight-trip.png similarity index 100% rename from docs/assets/theme-midnight-trip.png rename to docs/overrides/public/theme-midnight-trip.png diff --git a/docs/assets/theme-moonlight-hunt.png b/docs/overrides/public/theme-moonlight-hunt.png similarity index 100% rename from docs/assets/theme-moonlight-hunt.png rename to docs/overrides/public/theme-moonlight-hunt.png diff --git a/docs/assets/theme-rainforest.png b/docs/overrides/public/theme-rainforest.png similarity index 100% rename from docs/assets/theme-rainforest.png rename to docs/overrides/public/theme-rainforest.png diff --git a/docs/assets/theme-soft.png b/docs/overrides/public/theme-soft.png similarity index 100% rename from docs/assets/theme-soft.png rename to docs/overrides/public/theme-soft.png diff --git a/docs/assets/theme-solarized-dark.png b/docs/overrides/public/theme-solarized-dark.png similarity index 100% rename from docs/assets/theme-solarized-dark.png rename to docs/overrides/public/theme-solarized-dark.png diff --git a/docs/assets/theme-solarized-light.png b/docs/overrides/public/theme-solarized-light.png similarity index 100% rename from docs/assets/theme-solarized-light.png rename to docs/overrides/public/theme-solarized-light.png diff --git a/docs/assets/theme-sunset.png b/docs/overrides/public/theme-sunset.png similarity index 100% rename from docs/assets/theme-sunset.png rename to docs/overrides/public/theme-sunset.png diff --git a/docs/assets/tracing.png b/docs/overrides/public/tracing.png similarity index 100% rename from docs/assets/tracing.png rename to docs/overrides/public/tracing.png diff --git a/docs/assets/user-settings-export-import.png b/docs/overrides/public/user-settings-export-import.png similarity index 100% rename from docs/assets/user-settings-export-import.png rename to docs/overrides/public/user-settings-export-import.png diff --git a/docs/assets/user-settings-interaction-policy-1.png b/docs/overrides/public/user-settings-interaction-policy-1.png similarity index 100% rename from docs/assets/user-settings-interaction-policy-1.png rename to docs/overrides/public/user-settings-interaction-policy-1.png diff --git a/docs/assets/user-settings-profile-info.png b/docs/overrides/public/user-settings-profile-info.png similarity index 100% rename from docs/assets/user-settings-profile-info.png rename to docs/overrides/public/user-settings-profile-info.png diff --git a/docs/assets/user-settings-settings.png b/docs/overrides/public/user-settings-settings.png similarity index 100% rename from docs/assets/user-settings-settings.png rename to docs/overrides/public/user-settings-settings.png diff --git a/docs/user_guide/custom_css.md b/docs/user_guide/custom_css.md index 513074c62..679240bca 100644 --- a/docs/user_guide/custom_css.md +++ b/docs/user_guide/custom_css.md @@ -14,7 +14,7 @@ This allows you to customize the appearance of your GoToSocial profile for users Here's a standard GoToSocial profile page: -![A GoToSocial test profile page. The standard color scheme of grey, blue, and orange.](./../assets/cssstandard.png) +![A GoToSocial test profile page. The standard color scheme of grey, blue, and orange.](./../public/cssstandard.png) Let's say we want the background color to be black instead of grey. @@ -30,7 +30,7 @@ We then click on Save Profile Info. If we go back to our profile page and refresh the page, it now looks like this: -![The same GoToSocial test profile page. The background is now black.](./../assets/cssblack.png) +![The same GoToSocial test profile page. The background is now black.](./../public/cssblack.png) If we want to get really fancy, we can add an ombre effect to the background, by using the following CSS code instead: @@ -42,7 +42,7 @@ If we want to get really fancy, we can add an ombre effect to the background, by After saving the css and refreshing the profile page, the profile now looks like this: -![The same GoToSocial test profile page. The background now starts dark red and fades to purple further down the page.](./../assets/cssgradient.png) +![The same GoToSocial test profile page. The background now starts dark red and fades to purple further down the page.](./../public/cssgradient.png) ## Accessibility diff --git a/docs/user_guide/migration.md b/docs/user_guide/migration.md index a2d2a4c36..a00fcdbf0 100644 --- a/docs/user_guide/migration.md +++ b/docs/user_guide/migration.md @@ -53,7 +53,7 @@ To do this, you must first log in to the GoToSocial settings panel with your GoT From there, go to the "Migration" section, and look at the "Alias Account" subsection: -![The Alias Account subsection, showing a filled-in account alias.](../assets/migration-aliasing.png) +![The Alias Account subsection, showing a filled-in account alias.](../public/migration-aliasing.png) In the first free account alias box, enter the URL of the account you wish to move **from**. This indicates that the account you wish to move from belongs to you, ie., you are "also known as" the account. @@ -65,7 +65,7 @@ Once you have created the account alias from your GoToSocial account, pointing b On Mastodon, the "Account migration" settings section looks something like this: -![The Mastodon "Account migration" settings page.](../assets/migration-mastodon.png) +![The Mastodon "Account migration" settings page.](../public/migration-mastodon.png) If you were moving to a GoToSocial account from a Mastodon account, you would fill in the "Handle of the new account" field with the `@[username]@[domain]` value of your GoToSocial account. For example, if your GoToSocial account has username "@someone" and it's on the instance "example.org", you would enter `@someone@example.org` here. diff --git a/docs/user_guide/rss.md b/docs/user_guide/rss.md index a45b5286d..1ec543cfb 100644 --- a/docs/user_guide/rss.md +++ b/docs/user_guide/rss.md @@ -2,7 +2,7 @@ RSS stands for [Really Simple Syndication](https://en.wikipedia.org/wiki/RSS). It's a very well established standard for sharing content on the web. You might recognize the jolly orange RSS logo from your favorite news websites and blogs: -![The orange RSS icon](../assets/rss.svg) +![The orange RSS icon](../public/rss.svg) If you like, you can configure your GoToSocial account to expose an RSS feed of your posts to the web. This allows people to get regular updates about your posts even when they don't have a Fediverse account. This is great when you're using GoToSocial to create longer-form, blog style posts, and you want anyone to be able to read them easily. diff --git a/docs/user_guide/settings.md b/docs/user_guide/settings.md index 6c51b34d4..17f2c5962 100644 --- a/docs/user_guide/settings.md +++ b/docs/user_guide/settings.md @@ -8,7 +8,7 @@ You will be prompted to log in with your email address and password after provid ## Profile -![Screenshot of the profile section of the user settings interface, showing a preview of the avatar, header and display name, and providing form fields to change them](../assets/user-settings-profile-info.png) +![Screenshot of the profile section of the user settings interface, showing a preview of the avatar, header and display name, and providing form fields to change them](../public/user-settings-profile-info.png) In the profile section you can change your display name, avatar and header images. You can also choose to enable manually approving follow requests, and opt-in to providing a public RSS feed of your posts. @@ -184,7 +184,7 @@ This allows you to do things like: For example, the below image shows a policy for Public visibility posts that allows anyone to like or boost, but only allows followers, and people you follow, to reply. -![Policy showing "Who can like" = "anyone", "Who can reply" = "followers" and "following", and "Who can boost" = "anyone".](../assets/user-settings-interaction-policy-1.png) +![Policy showing "Who can like" = "anyone", "Who can reply" = "followers" and "following", and "Who can boost" = "anyone".](../public/user-settings-interaction-policy-1.png) Bear in mind that policies do not apply retroactively. Posts created after you've applied a default interaction policy will use that policy, but any post created before then will use whatever policy was the default when the post was created. @@ -231,7 +231,7 @@ Please see the [migration document](./migration.md) for more information on movi In the export & import section, you can export data from your GoToSocial account, or import data into it. -![The export/import page.](../assets/user-settings-export-import.png) +![The export/import page.](../public/user-settings-export-import.png) ### Export diff --git a/go.mod b/go.mod index 2b7ab98fd..9e6db71f1 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,24 @@ module github.com/superseriousbusiness/gotosocial go 1.22.2 +// Replace modernc/sqlite with our version that fixes the concurrency INTERRUPT issue replace modernc.org/sqlite => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.33.1-concurrency-workaround +// Below pin otel libraries to v1.29.0 until we can figure out issues +replace go.opentelemetry.io/otel => go.opentelemetry.io/otel v1.29.0 + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 + +replace go.opentelemetry.io/otel/metric => go.opentelemetry.io/otel/metric v1.29.0 + +replace go.opentelemetry.io/otel/sdk => go.opentelemetry.io/otel/sdk v1.29.0 + +replace go.opentelemetry.io/otel/sdk/metric => go.opentelemetry.io/otel/sdk/metric v1.29.0 + +replace go.opentelemetry.io/otel/trace => go.opentelemetry.io/otel/trace v1.29.0 + require ( codeberg.org/gruf/go-bytes v1.0.2 codeberg.org/gruf/go-bytesize v1.0.3 @@ -12,7 +28,7 @@ require ( codeberg.org/gruf/go-debug v1.3.0 codeberg.org/gruf/go-errors/v2 v2.3.2 codeberg.org/gruf/go-fastcopy v1.1.3 - codeberg.org/gruf/go-ffmpreg v0.4.2 + codeberg.org/gruf/go-ffmpreg v0.6.0 codeberg.org/gruf/go-iotools v0.0.0-20240710125620-934ae9c654cf codeberg.org/gruf/go-kv v1.6.5 codeberg.org/gruf/go-list v0.0.0-20240425093752-494db03d641f @@ -43,7 +59,7 @@ require ( github.com/miekg/dns v1.1.62 github.com/minio/minio-go/v7 v7.0.80 github.com/mitchellh/mapstructure v1.5.0 - github.com/ncruces/go-sqlite3 v0.20.0 + github.com/ncruces/go-sqlite3 v0.20.2 github.com/oklog/ulid v1.3.1 github.com/prometheus/client_golang v1.20.5 github.com/spf13/cobra v1.8.1 @@ -57,26 +73,26 @@ require ( github.com/tetratelabs/wazero v1.8.1 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 github.com/ulule/limiter/v3 v3.11.2 - github.com/uptrace/bun v1.2.1 - github.com/uptrace/bun/dialect/pgdialect v1.2.1 - github.com/uptrace/bun/dialect/sqlitedialect v1.2.1 - github.com/uptrace/bun/extra/bunotel v1.2.1 + github.com/uptrace/bun v1.2.5 + github.com/uptrace/bun/dialect/pgdialect v1.2.5 + github.com/uptrace/bun/dialect/sqlitedialect v1.2.5 + github.com/uptrace/bun/extra/bunotel v1.2.5 github.com/wagslane/go-password-validator v0.3.0 github.com/yuin/goldmark v1.7.8 - go.opentelemetry.io/otel v1.29.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 + go.opentelemetry.io/otel v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 go.opentelemetry.io/otel/exporters/prometheus v0.51.0 - go.opentelemetry.io/otel/metric v1.29.0 - go.opentelemetry.io/otel/sdk v1.29.0 - go.opentelemetry.io/otel/sdk/metric v1.29.0 - go.opentelemetry.io/otel/trace v1.29.0 + go.opentelemetry.io/otel/metric v1.32.0 + go.opentelemetry.io/otel/sdk v1.32.0 + go.opentelemetry.io/otel/sdk/metric v1.32.0 + go.opentelemetry.io/otel/trace v1.32.0 go.uber.org/automaxprocs v1.6.0 - golang.org/x/crypto v0.28.0 - golang.org/x/image v0.21.0 - golang.org/x/net v0.30.0 - golang.org/x/oauth2 v0.23.0 - golang.org/x/text v0.19.0 + golang.org/x/crypto v0.29.0 + golang.org/x/image v0.22.0 + golang.org/x/net v0.31.0 + golang.org/x/oauth2 v0.24.0 + golang.org/x/text v0.20.0 gopkg.in/mcuadros/go-syslog.v2 v2.3.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v0.0.0-00010101000000-000000000000 @@ -181,6 +197,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.59.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect @@ -201,7 +218,7 @@ require ( github.com/toqueteos/webbrowser v1.2.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - github.com/uptrace/opentelemetry-go-extra/otelsql v0.2.4 // indirect + github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect @@ -211,8 +228,8 @@ require ( golang.org/x/arch v0.8.0 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/mod v0.18.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.26.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect diff --git a/go.sum b/go.sum index 72e252234..f00a34326 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ codeberg.org/gruf/go-fastcopy v1.1.3 h1:Jo9VTQjI6KYimlw25PPc7YLA3Xm+XMQhaHwKnM7x codeberg.org/gruf/go-fastcopy v1.1.3/go.mod h1:GDDYR0Cnb3U/AIfGM3983V/L+GN+vuwVMvrmVABo21s= codeberg.org/gruf/go-fastpath/v2 v2.0.0 h1:iAS9GZahFhyWEH0KLhFEJR+txx1ZhMXxYzu2q5Qo9c0= codeberg.org/gruf/go-fastpath/v2 v2.0.0/go.mod h1:3pPqu5nZjpbRrOqvLyAK7puS1OfEtQvjd6342Cwz56Q= -codeberg.org/gruf/go-ffmpreg v0.4.2 h1:HKkPapm/PWkxsnUdjyQOGpwl5Qoa2EBrUQ09s4R4/FA= -codeberg.org/gruf/go-ffmpreg v0.4.2/go.mod h1:Ar5nbt3tB2Wr0uoaqV3wDBNwAx+H+AB/mV7Kw7NlZTI= +codeberg.org/gruf/go-ffmpreg v0.6.0 h1:/cfUJ9bFKEoXT9LDYZy3eZ0HF60YWcO+0nGciepJKMw= +codeberg.org/gruf/go-ffmpreg v0.6.0/go.mod h1:Ar5nbt3tB2Wr0uoaqV3wDBNwAx+H+AB/mV7Kw7NlZTI= codeberg.org/gruf/go-iotools v0.0.0-20240710125620-934ae9c654cf h1:84s/ii8N6lYlskZjHH+DG6jyia8w2mXMZlRwFn8Gs3A= codeberg.org/gruf/go-iotools v0.0.0-20240710125620-934ae9c654cf/go.mod h1:zZAICsp5rY7+hxnws2V0ePrWxE0Z2Z/KXcN3p/RQCfk= codeberg.org/gruf/go-kv v1.6.5 h1:ttPf0NA8F79pDqBttSudPTVCZmGncumeNIxmeM9ztz0= @@ -432,8 +432,8 @@ github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/ncruces/go-sqlite3 v0.20.0 h1:/nBLvYxj7sk9S6y57nmMFvoQ/KJtGo0pNi8J80s8oJU= -github.com/ncruces/go-sqlite3 v0.20.0/go.mod h1:yL4ZNWGsr1/8pcLfpPW1RT1WFdvyeHonrgIwwi4rvkg= +github.com/ncruces/go-sqlite3 v0.20.2 h1:cMLIwrLZQuCWVCEOowSqlIlpzgbag3jnYVW4NM5u01M= +github.com/ncruces/go-sqlite3 v0.20.2/go.mod h1:yL4ZNWGsr1/8pcLfpPW1RT1WFdvyeHonrgIwwi4rvkg= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= @@ -469,6 +469,8 @@ github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJ github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= +github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc= github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -578,16 +580,16 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA= github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI= -github.com/uptrace/bun v1.2.1 h1:2ENAcfeCfaY5+2e7z5pXrzFKy3vS8VXvkCag6N2Yzfk= -github.com/uptrace/bun v1.2.1/go.mod h1:cNg+pWBUMmJ8rHnETgf65CEvn3aIKErrwOD6IA8e+Ec= -github.com/uptrace/bun/dialect/pgdialect v1.2.1 h1:ceP99r03u+s8ylaDE/RzgcajwGiC76Jz3nS2ZgyPQ4M= -github.com/uptrace/bun/dialect/pgdialect v1.2.1/go.mod h1:mv6B12cisvSc6bwKm9q9wcrr26awkZK8QXM+nso9n2U= -github.com/uptrace/bun/dialect/sqlitedialect v1.2.1 h1:IprvkIKUjEjvt4VKpcmLpbMIucjrsmUPJOSlg19+a0Q= -github.com/uptrace/bun/dialect/sqlitedialect v1.2.1/go.mod h1:mMQf4NUpgY8bnOanxGmxNiHCdALOggS4cZ3v63a9D/o= -github.com/uptrace/bun/extra/bunotel v1.2.1 h1:5oTy3Jh7Q1bhCd5vnPszBmJgYouw+PuuZ8iSCm+uNCQ= -github.com/uptrace/bun/extra/bunotel v1.2.1/go.mod h1:SWW3HyjiXPYM36q0QSpdtTP8v21nWHnTCxu4lYkpO90= -github.com/uptrace/opentelemetry-go-extra/otelsql v0.2.4 h1:x3omFAG2XkvWFg1hvXRinY2ExAL1Aacl7W9ZlYjo6gc= -github.com/uptrace/opentelemetry-go-extra/otelsql v0.2.4/go.mod h1:qMKJr5fTnY0p7hqCQMNrAk62bCARWR5rAbTrGUFRuh4= +github.com/uptrace/bun v1.2.5 h1:gSprL5xiBCp+tzcZHgENzJpXnmQwRM/A6s4HnBF85mc= +github.com/uptrace/bun v1.2.5/go.mod h1:vkQMS4NNs4VNZv92y53uBSHXRqYyJp4bGhMHgaNCQpY= +github.com/uptrace/bun/dialect/pgdialect v1.2.5 h1:dWLUxpjTdglzfBks2x+U2WIi+nRVjuh7Z3DLYVFswJk= +github.com/uptrace/bun/dialect/pgdialect v1.2.5/go.mod h1:stwnlE8/6x8cuQ2aXcZqwDK/d+6jxgO3iQewflJT6C4= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.5 h1:liDvMaIWrN8DrHcxVbviOde/VDss9uhcqpcTSL3eJjc= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.5/go.mod h1:Mw6IDL/jNUL5ozcREAezOJSZ9Jm4LJlfoaXxBEfNBlM= +github.com/uptrace/bun/extra/bunotel v1.2.5 h1:kkuuTbrG9d5leYZuSBKhq2gtq346lIrxf98Mig2y128= +github.com/uptrace/bun/extra/bunotel v1.2.5/go.mod h1:rCHLszRZwppWE9cGDodO2FCI1qCrLwDjONp38KD3bA8= +github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c= +github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTXKkiKNGmLQS7vRsqRxIQTFZpYpHK3IQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.14.0/go.mod h1:ol1PCaL0dX20wC0htZ7sYCsvCYmrouYra0zHzaclZhE= @@ -666,8 +668,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -682,8 +684,8 @@ golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUF golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= -golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= +golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g= +golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -737,16 +739,16 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -756,8 +758,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -798,13 +800,13 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -812,8 +814,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/api/client/accounts/mute.go b/internal/api/client/accounts/mute.go index affb0f055..c9a57a348 100644 --- a/internal/api/client/accounts/mute.go +++ b/internal/api/client/accounts/mute.go @@ -19,9 +19,7 @@ import ( "errors" - "fmt" "net/http" - "strconv" "github.com/gin-gonic/gin" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -140,25 +138,15 @@ func normalizeCreateUpdateMute(form *apimodel.UserMuteCreateUpdateRequest) error // Apply defaults for missing fields. form.Notifications = util.Ptr(util.PtrOrValue(form.Notifications, false)) - // Normalize mute duration if necessary. - // If we parsed this as JSON, expires_in - // may be either a float64 or a string. - if ei := form.DurationI; ei != nil { - switch e := ei.(type) { - case float64: - form.Duration = util.Ptr(int(e)) - - case string: - duration, err := strconv.Atoi(e) - if err != nil { - return fmt.Errorf("could not parse duration value %s as integer: %w", e, err) - } - - form.Duration = &duration - - default: - return fmt.Errorf("could not parse expires_in type %T as integer", ei) + // Normalize duration if necessary. + if form.DurationI != nil { + // If we parsed this as JSON, duration + // may be either a float64 or a string. + duration, err := apiutil.ParseDuration(form.DurationI, "duration") + if err != nil { + return err } + form.Duration = duration } // Interpret zero as indefinite duration. diff --git a/internal/api/client/filters/v1/validate.go b/internal/api/client/filters/v1/validate.go index cce00fdc4..9e876c8cf 100644 --- a/internal/api/client/filters/v1/validate.go +++ b/internal/api/client/filters/v1/validate.go @@ -19,15 +19,14 @@ import ( "errors" - "fmt" - "strconv" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/internal/validate" ) -func validateNormalizeCreateUpdateFilter(form *model.FilterCreateUpdateRequestV1) error { +func validateNormalizeCreateUpdateFilter(form *apimodel.FilterCreateUpdateRequestV1) error { if err := validate.FilterKeyword(form.Phrase); err != nil { return err } @@ -48,25 +47,23 @@ func validateNormalizeCreateUpdateFilter(form *model.FilterCreateUpdateRequestV1 } // Normalize filter expiry if necessary. - // If we parsed this as JSON, expires_in - // may be either a float64 or a string. - if ei := form.ExpiresInI; ei != nil { - switch e := ei.(type) { - case float64: - form.ExpiresIn = util.Ptr(int(e)) - - case string: - expiresIn, err := strconv.Atoi(e) - if err != nil { - return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err) - } - - form.ExpiresIn = &expiresIn - - default: - return fmt.Errorf("could not parse expires_in type %T as integer", ei) + if form.ExpiresInI != nil { + // If we parsed this as JSON, expires_in + // may be either a float64 or a string. + var err error + form.ExpiresIn, err = apiutil.ParseDuration( + form.ExpiresInI, + "expires_in", + ) + if err != nil { + return err } } + // Interpret zero as indefinite duration. + if form.ExpiresIn != nil && *form.ExpiresIn == 0 { + form.ExpiresIn = nil + } + return nil } diff --git a/internal/api/client/filters/v2/filterpost.go b/internal/api/client/filters/v2/filterpost.go index 13270b1e5..632c4402f 100644 --- a/internal/api/client/filters/v2/filterpost.go +++ b/internal/api/client/filters/v2/filterpost.go @@ -18,9 +18,7 @@ package v2 import ( - "fmt" "net/http" - "strconv" "github.com/gin-gonic/gin" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -228,26 +226,24 @@ func validateNormalizeCreateFilter(form *apimodel.FilterCreateRequestV2) error { form.FilterAction = util.Ptr(action) // Normalize filter expiry if necessary. - // If we parsed this as JSON, expires_in - // may be either a float64 or a string. - if ei := form.ExpiresInI; ei != nil { - switch e := ei.(type) { - case float64: - form.ExpiresIn = util.Ptr(int(e)) - - case string: - expiresIn, err := strconv.Atoi(e) - if err != nil { - return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err) - } - - form.ExpiresIn = &expiresIn - - default: - return fmt.Errorf("could not parse expires_in type %T as integer", ei) + if form.ExpiresInI != nil { + // If we parsed this as JSON, expires_in + // may be either a float64 or a string. + var err error + form.ExpiresIn, err = apiutil.ParseDuration( + form.ExpiresInI, + "expires_in", + ) + if err != nil { + return err } } + // Interpret zero as indefinite duration. + if form.ExpiresIn != nil && *form.ExpiresIn == 0 { + form.ExpiresIn = nil + } + // Normalize and validate new keywords and statuses. for i, formKeyword := range form.Keywords { if err := validate.FilterKeyword(formKeyword.Keyword); err != nil { diff --git a/internal/api/client/filters/v2/filterput.go b/internal/api/client/filters/v2/filterput.go index 24f7e7567..cde03360d 100644 --- a/internal/api/client/filters/v2/filterput.go +++ b/internal/api/client/filters/v2/filterput.go @@ -19,9 +19,7 @@ import ( "errors" - "fmt" "net/http" - "strconv" "github.com/gin-gonic/gin" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -272,26 +270,24 @@ func validateNormalizeUpdateFilter(form *apimodel.FilterUpdateRequestV2) error { } // Normalize filter expiry if necessary. - // If we parsed this as JSON, expires_in - // may be either a float64 or a string. - if ei := form.ExpiresInI; ei != nil { - switch e := ei.(type) { - case float64: - form.ExpiresIn = util.Ptr(int(e)) - - case string: - expiresIn, err := strconv.Atoi(e) - if err != nil { - return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err) - } - - form.ExpiresIn = &expiresIn - - default: - return fmt.Errorf("could not parse expires_in type %T as integer", ei) + if form.ExpiresInI != nil { + // If we parsed this as JSON, expires_in + // may be either a float64 or a string. + var err error + form.ExpiresIn, err = apiutil.ParseDuration( + form.ExpiresInI, + "expires_in", + ) + if err != nil { + return err } } + // Interpret zero as indefinite duration. + if form.ExpiresIn != nil && *form.ExpiresIn == 0 { + form.ExpiresIn = nil + } + // Normalize and validate updates. for i, formKeyword := range form.Keywords { if formKeyword.Keyword != nil { diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go index 3a362f27c..9c4a9118a 100644 --- a/internal/api/client/instance/instancepatch_test.go +++ b/internal/api/client/instance/instancepatch_test.go @@ -130,10 +130,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() { "video/x-matroska" ], "image_size_limit": 41943040, - "image_matrix_limit": 9223372036854775807, + "image_matrix_limit": 2147483647, "video_size_limit": 41943040, - "video_frame_rate_limit": 9223372036854775807, - "video_matrix_limit": 9223372036854775807 + "video_frame_rate_limit": 2147483647, + "video_matrix_limit": 2147483647 }, "polls": { "max_options": 6, @@ -271,10 +271,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() { "video/x-matroska" ], "image_size_limit": 41943040, - "image_matrix_limit": 9223372036854775807, + "image_matrix_limit": 2147483647, "video_size_limit": 41943040, - "video_frame_rate_limit": 9223372036854775807, - "video_matrix_limit": 9223372036854775807 + "video_frame_rate_limit": 2147483647, + "video_matrix_limit": 2147483647 }, "polls": { "max_options": 6, @@ -412,10 +412,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() { "video/x-matroska" ], "image_size_limit": 41943040, - "image_matrix_limit": 9223372036854775807, + "image_matrix_limit": 2147483647, "video_size_limit": 41943040, - "video_frame_rate_limit": 9223372036854775807, - "video_matrix_limit": 9223372036854775807 + "video_frame_rate_limit": 2147483647, + "video_matrix_limit": 2147483647 }, "polls": { "max_options": 6, @@ -604,10 +604,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() { "video/x-matroska" ], "image_size_limit": 41943040, - "image_matrix_limit": 9223372036854775807, + "image_matrix_limit": 2147483647, "video_size_limit": 41943040, - "video_frame_rate_limit": 9223372036854775807, - "video_matrix_limit": 9223372036854775807 + "video_frame_rate_limit": 2147483647, + "video_matrix_limit": 2147483647 }, "polls": { "max_options": 6, @@ -767,10 +767,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() { "video/x-matroska" ], "image_size_limit": 41943040, - "image_matrix_limit": 9223372036854775807, + "image_matrix_limit": 2147483647, "video_size_limit": 41943040, - "video_frame_rate_limit": 9223372036854775807, - "video_matrix_limit": 9223372036854775807 + "video_frame_rate_limit": 2147483647, + "video_matrix_limit": 2147483647 }, "polls": { "max_options": 6, @@ -949,10 +949,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() { "video/x-matroska" ], "image_size_limit": 41943040, - "image_matrix_limit": 9223372036854775807, + "image_matrix_limit": 2147483647, "video_size_limit": 41943040, - "video_frame_rate_limit": 9223372036854775807, - "video_matrix_limit": 9223372036854775807 + "video_frame_rate_limit": 2147483647, + "video_matrix_limit": 2147483647 }, "polls": { "max_options": 6, diff --git a/internal/api/client/statuses/statuscreate.go b/internal/api/client/statuses/statuscreate.go index 48d11f363..8198d5358 100644 --- a/internal/api/client/statuses/statuscreate.go +++ b/internal/api/client/statuses/statuscreate.go @@ -21,7 +21,6 @@ "errors" "fmt" "net/http" - "strconv" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" @@ -474,25 +473,19 @@ func validateStatusPoll(form *apimodel.StatusCreateRequest) gtserror.WithCode { } // Normalize poll expiry if necessary. - // If we parsed this as JSON, expires_in - // may be either a float64 or a string. - if ei := form.Poll.ExpiresInI; ei != nil { - switch e := ei.(type) { - case float64: - form.Poll.ExpiresIn = int(e) + if form.Poll.ExpiresInI != nil { + // If we parsed this as JSON, expires_in + // may be either a float64 or a string. + expiresIn, err := apiutil.ParseDuration( + form.Poll.ExpiresInI, + "expires_in", + ) + if err != nil { + return gtserror.NewErrorBadRequest(err, err.Error()) + } - case string: - expiresIn, err := strconv.Atoi(e) - if err != nil { - text := fmt.Sprintf("could not parse expires_in value %s as integer: %v", e, err) - return gtserror.NewErrorBadRequest(errors.New(text), text) - } - - form.Poll.ExpiresIn = expiresIn - - default: - text := fmt.Sprintf("could not parse expires_in type %T as integer", ei) - return gtserror.NewErrorBadRequest(errors.New(text), text) + if expiresIn != nil { + form.Poll.ExpiresIn = *expiresIn } } diff --git a/internal/api/util/parseform.go b/internal/api/util/parseform.go new file mode 100644 index 000000000..19e24189f --- /dev/null +++ b/internal/api/util/parseform.go @@ -0,0 +1,70 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package util + +import ( + "fmt" + "strconv" +) + +// ParseDuration parses the given raw interface belonging to +// the given fieldName as an integer duration. +// +// Will return nil, nil if rawI is the zero value of its type. +func ParseDuration(rawI any, fieldName string) (*int, error) { + var ( + asInteger int + err error + ) + + switch raw := rawI.(type) { + case float64: + // Submitted as JSON number + // (casts to float64 by default). + asInteger = int(raw) + + case string: + // Submitted as JSON string or form field. + asInteger, err = strconv.Atoi(raw) + if err != nil { + err = fmt.Errorf( + "could not parse %s value %s as integer: %w", + fieldName, raw, err, + ) + } + + default: + // Submitted as god-knows-what. + err = fmt.Errorf( + "could not parse %s type %T as integer", + fieldName, rawI, + ) + } + + if err != nil { + return nil, err + } + + // Someone submitted 0, + // don't point to this. + if asInteger == 0 { + return nil, nil + } + + return &asInteger, nil +} diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go index 16c82c08f..f054b1412 100644 --- a/internal/db/bundb/account.go +++ b/internal/db/bundb/account.go @@ -36,6 +36,7 @@ "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/state" "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect" ) @@ -86,7 +87,7 @@ func(uncached []string) ([]*gtsmodel.Account, error) { // Reorder the statuses by their // IDs to ensure in correct order. getID := func(a *gtsmodel.Account) string { return a.ID } - util.OrderBy(accounts, ids, getID) + xslices.OrderBy(accounts, ids, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. diff --git a/internal/db/bundb/application.go b/internal/db/bundb/application.go index fda0ba602..cbba499b0 100644 --- a/internal/db/bundb/application.go +++ b/internal/db/bundb/application.go @@ -22,7 +22,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -169,7 +169,7 @@ func(uncached []string) ([]*gtsmodel.Token, error) { // Reoroder the tokens by their // IDs to ensure in correct order. getID := func(t *gtsmodel.Token) string { return t.ID } - util.OrderBy(tokens, tokenIDs, getID) + xslices.OrderBy(tokens, tokenIDs, getID) return tokens, nil } diff --git a/internal/db/bundb/conversation.go b/internal/db/bundb/conversation.go index 22ff4fd79..354463111 100644 --- a/internal/db/bundb/conversation.go +++ b/internal/db/bundb/conversation.go @@ -31,7 +31,7 @@ "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect" ) @@ -209,7 +209,7 @@ func(accountID string, uncached []string) ([]*gtsmodel.Conversation, error) { // Reorder the conversations by their last status IDs to ensure correct order. getID := func(b *gtsmodel.Conversation) string { return b.ID } - util.OrderBy(conversations, conversationLastStatusIDs, getID) + xslices.OrderBy(conversations, conversationLastStatusIDs, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. @@ -558,7 +558,7 @@ func (c *conversationDB) DeleteStatusFromConversations(ctx context.Context, stat // Invalidate cache entries. updatedConversationIDs = append(updatedConversationIDs, deletedConversationIDs...) - updatedConversationIDs = util.Deduplicate(updatedConversationIDs) + updatedConversationIDs = xslices.Deduplicate(updatedConversationIDs) c.state.Caches.DB.Conversation.InvalidateIDs("ID", updatedConversationIDs) return nil diff --git a/internal/db/bundb/emoji.go b/internal/db/bundb/emoji.go index db9daf0aa..ee564317e 100644 --- a/internal/db/bundb/emoji.go +++ b/internal/db/bundb/emoji.go @@ -31,7 +31,7 @@ "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect" ) @@ -597,7 +597,7 @@ func(uncached []string) ([]*gtsmodel.Emoji, error) { // Reorder the emojis by their // IDs to ensure in correct order. getID := func(e *gtsmodel.Emoji) string { return e.ID } - util.OrderBy(emojis, ids, getID) + xslices.OrderBy(emojis, ids, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. @@ -661,7 +661,7 @@ func(uncached []string) ([]*gtsmodel.EmojiCategory, error) { // Reorder the categories by their // IDs to ensure in correct order. getID := func(c *gtsmodel.EmojiCategory) string { return c.ID } - util.OrderBy(categories, ids, getID) + xslices.OrderBy(categories, ids, getID) return categories, nil } diff --git a/internal/db/bundb/filter.go b/internal/db/bundb/filter.go index e68a0bcd0..fe23bb405 100644 --- a/internal/db/bundb/filter.go +++ b/internal/db/bundb/filter.go @@ -27,7 +27,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -99,7 +99,7 @@ func(uncached []string) ([]*gtsmodel.Filter, error) { } // Put the filter structs in the same order as the filter IDs. - util.OrderBy(filters, filterIDs, func(filter *gtsmodel.Filter) string { return filter.ID }) + xslices.OrderBy(filters, filterIDs, func(filter *gtsmodel.Filter) string { return filter.ID }) if gtscontext.Barebones(ctx) { return filters, nil diff --git a/internal/db/bundb/filterkeyword.go b/internal/db/bundb/filterkeyword.go index 8a006d10f..0e1d8daeb 100644 --- a/internal/db/bundb/filterkeyword.go +++ b/internal/db/bundb/filterkeyword.go @@ -26,7 +26,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -140,7 +140,7 @@ func(uncached []string) ([]*gtsmodel.FilterKeyword, error) { } // Put the filter keyword structs in the same order as the filter keyword IDs. - util.OrderBy(filterKeywords, filterKeywordIDs, func(filterKeyword *gtsmodel.FilterKeyword) string { + xslices.OrderBy(filterKeywords, filterKeywordIDs, func(filterKeyword *gtsmodel.FilterKeyword) string { return filterKeyword.ID }) diff --git a/internal/db/bundb/filterstatus.go b/internal/db/bundb/filterstatus.go index 95919bd2c..1cd924d13 100644 --- a/internal/db/bundb/filterstatus.go +++ b/internal/db/bundb/filterstatus.go @@ -25,7 +25,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -116,7 +116,7 @@ func(uncached []string) ([]*gtsmodel.FilterStatus, error) { } // Put the filter status structs in the same order as the filter status IDs. - util.OrderBy(filterStatuses, filterStatusIDs, func(filterStatus *gtsmodel.FilterStatus) string { + xslices.OrderBy(filterStatuses, filterStatusIDs, func(filterStatus *gtsmodel.FilterStatus) string { return filterStatus.ID }) diff --git a/internal/db/bundb/interaction.go b/internal/db/bundb/interaction.go index a04e97905..9fbe00711 100644 --- a/internal/db/bundb/interaction.go +++ b/internal/db/bundb/interaction.go @@ -29,7 +29,7 @@ "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -113,7 +113,7 @@ func(uncached []string) ([]*gtsmodel.InteractionRequest, error) { // Reorder the requests by their // IDs to ensure in correct order. getID := func(r *gtsmodel.InteractionRequest) string { return r.ID } - util.OrderBy(requests, ids, getID) + xslices.OrderBy(requests, ids, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. diff --git a/internal/db/bundb/list.go b/internal/db/bundb/list.go index 03dff95e3..f81c59c42 100644 --- a/internal/db/bundb/list.go +++ b/internal/db/bundb/list.go @@ -31,7 +31,7 @@ "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -333,7 +333,7 @@ func(uncached []string) ([]*gtsmodel.List, error) { // Reorder the lists by their // IDs to ensure in correct order. getID := func(l *gtsmodel.List) string { return l.ID } - util.OrderBy(lists, ids, getID) + xslices.OrderBy(lists, ids, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. @@ -387,12 +387,12 @@ func (l *listDB) PutListEntries(ctx context.Context, entries []*gtsmodel.ListEnt } // Collect unique list IDs from the provided list entries. - listIDs := util.Collate(entries, func(e *gtsmodel.ListEntry) string { + listIDs := xslices.Collate(entries, func(e *gtsmodel.ListEntry) string { return e.ListID }) // Collect unique follow IDs from the provided list entries. - followIDs := util.Collate(entries, func(e *gtsmodel.ListEntry) string { + followIDs := xslices.Collate(entries, func(e *gtsmodel.ListEntry) string { return e.FollowID }) @@ -441,7 +441,7 @@ func (l *listDB) DeleteAllListEntriesByFollows(ctx context.Context, followIDs .. } // Deduplicate IDs before invalidate. - listIDs = util.Deduplicate(listIDs) + listIDs = xslices.Deduplicate(listIDs) // Invalidate all related list entry caches. l.invalidateEntryCaches(ctx, listIDs, followIDs) diff --git a/internal/db/bundb/media.go b/internal/db/bundb/media.go index de980a16a..453ad856a 100644 --- a/internal/db/bundb/media.go +++ b/internal/db/bundb/media.go @@ -28,7 +28,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -78,7 +78,7 @@ func(uncached []string) ([]*gtsmodel.MediaAttachment, error) { // Reorder the media by their // IDs to ensure in correct order. getID := func(m *gtsmodel.MediaAttachment) string { return m.ID } - util.OrderBy(media, ids, getID) + xslices.OrderBy(media, ids, getID) return media, nil } diff --git a/internal/db/bundb/mention.go b/internal/db/bundb/mention.go index ba8c0ba11..04aa5d76e 100644 --- a/internal/db/bundb/mention.go +++ b/internal/db/bundb/mention.go @@ -28,7 +28,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -91,7 +91,7 @@ func(uncached []string) ([]*gtsmodel.Mention, error) { // Reorder the mentions by their // IDs to ensure in correct order. getID := func(m *gtsmodel.Mention) string { return m.ID } - util.OrderBy(mentions, ids, getID) + xslices.OrderBy(mentions, ids, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. diff --git a/internal/db/bundb/notification.go b/internal/db/bundb/notification.go index 770e84c5c..ef2527637 100644 --- a/internal/db/bundb/notification.go +++ b/internal/db/bundb/notification.go @@ -29,7 +29,7 @@ "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -130,7 +130,7 @@ func(uncached []string) ([]*gtsmodel.Notification, error) { // Reorder the notifs by their // IDs to ensure in correct order. getID := func(n *gtsmodel.Notification) string { return n.ID } - util.OrderBy(notifs, ids, getID) + xslices.OrderBy(notifs, ids, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. diff --git a/internal/db/bundb/poll.go b/internal/db/bundb/poll.go index f5c33ce9b..b9384774b 100644 --- a/internal/db/bundb/poll.go +++ b/internal/db/bundb/poll.go @@ -29,7 +29,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -315,7 +315,7 @@ func(uncached []string) ([]*gtsmodel.PollVote, error) { // Reorder the poll votes by their // IDs to ensure in correct order. getID := func(v *gtsmodel.PollVote) string { return v.ID } - util.OrderBy(votes, voteIDs, getID) + xslices.OrderBy(votes, voteIDs, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. diff --git a/internal/db/bundb/relationship_block.go b/internal/db/bundb/relationship_block.go index 9738970e5..9578b0e3e 100644 --- a/internal/db/bundb/relationship_block.go +++ b/internal/db/bundb/relationship_block.go @@ -27,7 +27,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -127,7 +127,7 @@ func(uncached []string) ([]*gtsmodel.Block, error) { // Reorder the blocks by their // IDs to ensure in correct order. getID := func(b *gtsmodel.Block) string { return b.ID } - util.OrderBy(blocks, ids, getID) + xslices.OrderBy(blocks, ids, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. diff --git a/internal/db/bundb/relationship_follow.go b/internal/db/bundb/relationship_follow.go index 042d12f37..aea36f39c 100644 --- a/internal/db/bundb/relationship_follow.go +++ b/internal/db/bundb/relationship_follow.go @@ -28,7 +28,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -103,7 +103,7 @@ func(uncached []string) ([]*gtsmodel.Follow, error) { // Reorder the follows by their // IDs to ensure in correct order. getID := func(f *gtsmodel.Follow) string { return f.ID } - util.OrderBy(follows, ids, getID) + xslices.OrderBy(follows, ids, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. @@ -376,7 +376,7 @@ func (r *relationshipDB) DeleteAccountFollows(ctx context.Context, accountID str } // Gather the follow IDs that were deleted for removing related list entries. - followIDs := util.Gather(nil, deleted, func(follow *gtsmodel.Follow) string { + followIDs := xslices.Gather(nil, deleted, func(follow *gtsmodel.Follow) string { return follow.ID }) diff --git a/internal/db/bundb/relationship_follow_req.go b/internal/db/bundb/relationship_follow_req.go index fc0ca5c0a..030c99c58 100644 --- a/internal/db/bundb/relationship_follow_req.go +++ b/internal/db/bundb/relationship_follow_req.go @@ -28,7 +28,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -103,7 +103,7 @@ func(uncached []string) ([]*gtsmodel.FollowRequest, error) { // Reorder the requests by their // IDs to ensure in correct order. getID := func(f *gtsmodel.FollowRequest) string { return f.ID } - util.OrderBy(follows, ids, getID) + xslices.OrderBy(follows, ids, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. diff --git a/internal/db/bundb/relationship_mute.go b/internal/db/bundb/relationship_mute.go index 37d97a64f..b7b7e109e 100644 --- a/internal/db/bundb/relationship_mute.go +++ b/internal/db/bundb/relationship_mute.go @@ -28,7 +28,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/paging" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect" ) @@ -109,7 +109,7 @@ func(uncached []string) ([]*gtsmodel.UserMute, error) { // Reorder the mutes by their // IDs to ensure in correct order. getID := func(b *gtsmodel.UserMute) string { return b.ID } - util.OrderBy(mutes, ids, getID) + xslices.OrderBy(mutes, ids, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. diff --git a/internal/db/bundb/status.go b/internal/db/bundb/status.go index 5340b63cd..45e9864a3 100644 --- a/internal/db/bundb/status.go +++ b/internal/db/bundb/status.go @@ -29,7 +29,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -76,7 +76,7 @@ func(uncached []string) ([]*gtsmodel.Status, error) { // Reorder the statuses by their // IDs to ensure in correct order. getID := func(s *gtsmodel.Status) string { return s.ID } - util.OrderBy(statuses, ids, getID) + xslices.OrderBy(statuses, ids, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. diff --git a/internal/db/bundb/statusbookmark.go b/internal/db/bundb/statusbookmark.go index 1534050da..6cbd7f583 100644 --- a/internal/db/bundb/statusbookmark.go +++ b/internal/db/bundb/statusbookmark.go @@ -28,7 +28,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -95,7 +95,7 @@ func(uncached []string) ([]*gtsmodel.StatusBookmark, error) { // Reorder the bookmarks by their // IDs to ensure in correct order. getID := func(b *gtsmodel.StatusBookmark) string { return b.ID } - util.OrderBy(bookmarks, ids, getID) + xslices.OrderBy(bookmarks, ids, getID) // Populate all loaded bookmarks, removing those we fail // to populate (removes needing so many later nil checks). diff --git a/internal/db/bundb/statusfave.go b/internal/db/bundb/statusfave.go index cf20fbba3..c1fa375aa 100644 --- a/internal/db/bundb/statusfave.go +++ b/internal/db/bundb/statusfave.go @@ -31,7 +31,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -155,7 +155,7 @@ func(uncached []string) ([]*gtsmodel.StatusFave, error) { // Reorder the statuses by their // IDs to ensure in correct order. getID := func(f *gtsmodel.StatusFave) string { return f.ID } - util.OrderBy(faves, faveIDs, getID) + xslices.OrderBy(faves, faveIDs, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. @@ -339,7 +339,7 @@ func (s *statusFaveDB) DeleteStatusFaves(ctx context.Context, targetAccountID st } // Deduplicate determined status IDs. - statusIDs = util.Deduplicate(statusIDs) + statusIDs = xslices.Deduplicate(statusIDs) // Invalidate any cached status faves for this status ID. s.state.Caches.DB.StatusFave.InvalidateIDs("ID", statusIDs) diff --git a/internal/db/bundb/tag.go b/internal/db/bundb/tag.go index 6c3d870f6..dfb80e829 100644 --- a/internal/db/bundb/tag.go +++ b/internal/db/bundb/tag.go @@ -28,7 +28,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/uptrace/bun" ) @@ -102,7 +102,7 @@ func(uncached []string) ([]*gtsmodel.Tag, error) { // Reorder the tags by their // IDs to ensure in correct order. getID := func(t *gtsmodel.Tag) string { return t.ID } - util.OrderBy(tags, ids, getID) + xslices.OrderBy(tags, ids, getID) return tags, nil } @@ -301,5 +301,5 @@ func (t *tagDB) GetAccountIDsFollowingTagIDs(ctx context.Context, tagIDs []strin // Accounts might be following multiple tags in list, // but we only want to return each account once. - return util.Deduplicate(accountIDs), nil + return xslices.Deduplicate(accountIDs), nil } diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index fdd13f7f0..a953701f8 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -35,7 +35,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/uris" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" ) type errOtherIRIBlocked struct { @@ -162,7 +162,7 @@ func (f *Federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Reques // OtherIRIs will likely contain some // duplicate entries now, so remove them. - otherIRIs = util.DeduplicateFunc(otherIRIs, + otherIRIs = xslices.DeduplicateFunc(otherIRIs, (*url.URL).String, // serialized URL is 'key()' ) diff --git a/internal/gtsmodel/conversation.go b/internal/gtsmodel/conversation.go index d17cbe6fe..d3bdcbf1d 100644 --- a/internal/gtsmodel/conversation.go +++ b/internal/gtsmodel/conversation.go @@ -22,7 +22,7 @@ "strings" "time" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" ) // Conversation represents direct messages between the owner account and a set of other accounts. @@ -62,7 +62,7 @@ type Conversation struct { // ConversationOtherAccountsKey creates an OtherAccountsKey from a list of OtherAccountIDs. func ConversationOtherAccountsKey(otherAccountIDs []string) string { - otherAccountIDs = util.Deduplicate(otherAccountIDs) + otherAccountIDs = xslices.Deduplicate(otherAccountIDs) slices.Sort(otherAccountIDs) return strings.Join(otherAccountIDs, ",") } diff --git a/internal/log/log.go b/internal/log/log.go index bb2e561b3..52703ef28 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -22,11 +22,11 @@ "fmt" "log/syslog" "os" - "slices" "strings" "time" "codeberg.org/gruf/go-kv" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" ) var ( @@ -412,7 +412,10 @@ func logf(ctx context.Context, depth int, lvl LEVEL, fields []kv.Field, s string buf.B = append(buf.B, lvlstrs[lvl]...) buf.B = append(buf.B, ' ') - if ctx != nil { + if ctx != nil && len(ctxhooks) > 0 { + // Ensure fields have space for hooks (+1 for below). + fields = xslices.GrowJust(fields, len(ctxhooks)+1) + // Pass context through hooks. for _, hook := range ctxhooks { fields = hook(ctx, fields) @@ -420,9 +423,8 @@ func logf(ctx context.Context, depth int, lvl LEVEL, fields []kv.Field, s string } if s != "" { - // Append message to log fields. - fields = slices.Grow(fields, 1) - fields = append(fields, kv.Field{ + // Append message (if given) as final log field. + fields = xslices.AppendJust(fields, kv.Field{ K: "msg", V: fmt.Sprintf(s, a...), }) } diff --git a/internal/media/ffmpeg.go b/internal/media/ffmpeg.go index 1d7b01905..c225d4378 100644 --- a/internal/media/ffmpeg.go +++ b/internal/media/ffmpeg.go @@ -181,6 +181,10 @@ func ffmpeg(ctx context.Context, inpath string, outpath string, args ...string) } fscfg = fscfg.WithFSMount(shared, path.Dir(inpath)) + // Set anonymous module name. + modcfg = modcfg.WithName("") + + // Update with prepared fs config. return modcfg.WithFSConfig(fscfg) }, }) @@ -247,6 +251,10 @@ func ffprobe(ctx context.Context, filepath string) (*result, error) { } fscfg = fscfg.WithFSMount(in, path.Dir(filepath)) + // Set anonymous module name. + modcfg = modcfg.WithName("") + + // Update with prepared fs config. return modcfg.WithFSConfig(fscfg) }, }) diff --git a/internal/media/ffmpeg/ffmpeg.go b/internal/media/ffmpeg/ffmpeg.go index 0571c029a..b978201c1 100644 --- a/internal/media/ffmpeg/ffmpeg.go +++ b/internal/media/ffmpeg/ffmpeg.go @@ -21,6 +21,7 @@ import ( "context" + "errors" "codeberg.org/gruf/go-ffmpreg/wasm" ) @@ -35,12 +36,25 @@ // prepares the runner to only allow max given concurrent running instances. func InitFfmpeg(ctx context.Context, max int) error { ffmpegRunner.Init(max) - return compileFfmpeg(ctx) + return initWASM(ctx) } // Ffmpeg runs the given arguments with an instance of ffmpeg. func Ffmpeg(ctx context.Context, args Args) (uint32, error) { return ffmpegRunner.Run(ctx, func() (uint32, error) { - return wasm.Run(ctx, runtime, ffmpeg, args) + + // Load WASM rt and module. + ffmpreg := ffmpreg.Load() + if ffmpreg == nil { + return 0, errors.New("wasm not initialized") + } + + // Call into ffmpeg. + args.Name = "ffmpeg" + return wasm.Run(ctx, + ffmpreg.run, + ffmpreg.mod, + args, + ) }) } diff --git a/internal/media/ffmpeg/ffprobe.go b/internal/media/ffmpeg/ffprobe.go index ccd5072dd..410935d9c 100644 --- a/internal/media/ffmpeg/ffprobe.go +++ b/internal/media/ffmpeg/ffprobe.go @@ -21,6 +21,7 @@ import ( "context" + "errors" "codeberg.org/gruf/go-ffmpreg/wasm" ) @@ -35,12 +36,25 @@ // prepares the runner to only allow max given concurrent running instances. func InitFfprobe(ctx context.Context, max int) error { ffprobeRunner.Init(max) - return compileFfprobe(ctx) + return initWASM(ctx) } // Ffprobe runs the given arguments with an instance of ffprobe. func Ffprobe(ctx context.Context, args Args) (uint32, error) { return ffprobeRunner.Run(ctx, func() (uint32, error) { - return wasm.Run(ctx, runtime, ffprobe, args) + + // Load WASM rt and module. + ffmpreg := ffmpreg.Load() + if ffmpreg == nil { + return 0, errors.New("wasm not initialized") + } + + // Call into ffprobe. + args.Name = "ffprobe" + return wasm.Run(ctx, + ffmpreg.run, + ffmpreg.mod, + args, + ) }) } diff --git a/internal/media/ffmpeg/wasm.go b/internal/media/ffmpeg/wasm.go index b23809d93..a5612ba14 100644 --- a/internal/media/ffmpeg/wasm.go +++ b/internal/media/ffmpeg/wasm.go @@ -22,72 +22,27 @@ import ( "context" "os" + "sync/atomic" + "unsafe" - ffmpeglib "codeberg.org/gruf/go-ffmpreg/embed/ffmpeg" - ffprobelib "codeberg.org/gruf/go-ffmpreg/embed/ffprobe" + "codeberg.org/gruf/go-ffmpreg/embed" "codeberg.org/gruf/go-ffmpreg/wasm" "github.com/tetratelabs/wazero" ) -var ( - // shared WASM runtime instance. - runtime wazero.Runtime +// ffmpreg is a concurrency-safe pointer +// to our necessary WebAssembly runtime +// and compiled ffmpreg module instance. +var ffmpreg atomic.Pointer[struct { + run wazero.Runtime + mod wazero.CompiledModule +}] - // ffmpeg / ffprobe compiled WASM. - ffmpeg wazero.CompiledModule - ffprobe wazero.CompiledModule -) - -// compileFfmpeg ensures the ffmpeg WebAssembly has been -// pre-compiled into memory. If already compiled is a no-op. -func compileFfmpeg(ctx context.Context) error { - if ffmpeg != nil { - return nil - } - - // Ensure runtime already initialized. - if err := initRuntime(ctx); err != nil { - return err - } - - // Compile the ffmpeg WebAssembly module into memory. - cmod, err := runtime.CompileModule(ctx, ffmpeglib.B) - if err != nil { - return err - } - - // Set module. - ffmpeg = cmod - return nil -} - -// compileFfprobe ensures the ffprobe WebAssembly has been -// pre-compiled into memory. If already compiled is a no-op. -func compileFfprobe(ctx context.Context) error { - if ffprobe != nil { - return nil - } - - // Ensure runtime already initialized. - if err := initRuntime(ctx); err != nil { - return err - } - - // Compile the ffprobe WebAssembly module into memory. - cmod, err := runtime.CompileModule(ctx, ffprobelib.B) - if err != nil { - return err - } - - // Set module. - ffprobe = cmod - return nil -} - -// initRuntime initializes the global wazero.Runtime, -// if already initialized this function is a no-op. -func initRuntime(ctx context.Context) (err error) { - if runtime != nil { +// initWASM safely prepares new WebAssembly runtime +// and compiles ffmpreg module instance, if the global +// pointer has not been already. else, is a no-op. +func initWASM(ctx context.Context) error { + if ffmpreg.Load() != nil { return nil } @@ -105,7 +60,59 @@ func initRuntime(ctx context.Context) (err error) { cfg = cfg.WithCompilationCache(cache) } + var ( + run wazero.Runtime + mod wazero.CompiledModule + err error + set bool + ) + + defer func() { + if err == nil && set { + // Drop binary. + embed.B = nil + return + } + + // Close module. + if !isNil(mod) { + mod.Close(ctx) + } + + // Close runtime. + if !isNil(run) { + run.Close(ctx) + } + }() + // Initialize new runtime from config. - runtime, err = wasm.NewRuntime(ctx, cfg) - return + run, err = wasm.NewRuntime(ctx, cfg) + if err != nil { + return err + } + + // Compile ffmpreg WebAssembly into memory. + mod, err = run.CompileModule(ctx, embed.B) + if err != nil { + return err + } + + // Try set global WASM runtime and module, + // or if beaten to it defer will handle close. + set = ffmpreg.CompareAndSwap(nil, &struct { + run wazero.Runtime + mod wazero.CompiledModule + }{ + run: run, + mod: mod, + }) + + return nil +} + +// isNil will safely check if 'v' is nil without +// dealing with weird Go interface nil bullshit. +func isNil(i interface{}) bool { + type eface struct{ Type, Data unsafe.Pointer } + return (*eface)(unsafe.Pointer(&i)).Data == nil } diff --git a/internal/processing/account/alias.go b/internal/processing/account/alias.go index a11be0305..d7d4cf547 100644 --- a/internal/processing/account/alias.go +++ b/internal/processing/account/alias.go @@ -27,7 +27,7 @@ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" ) func (p *Processor) Alias( @@ -137,8 +137,8 @@ type uri struct { // Dedupe URIs + accounts, in case someone // provided both an account URL and an // account URI above, for the same account. - account.AlsoKnownAsURIs = util.Deduplicate(account.AlsoKnownAsURIs) - account.AlsoKnownAs = util.DeduplicateFunc( + account.AlsoKnownAsURIs = xslices.Deduplicate(account.AlsoKnownAsURIs) + account.AlsoKnownAs = xslices.DeduplicateFunc( account.AlsoKnownAs, func(a *gtsmodel.Account) string { return a.URI diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go index 55a78610f..fbc2dadf7 100644 --- a/internal/processing/status/create.go +++ b/internal/processing/status/create.go @@ -36,6 +36,7 @@ "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/uris" "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" ) // Create processes the given form to create a new status, returning the api model representation of that status if it's OK. @@ -536,9 +537,9 @@ func (p *Processor) processContent(ctx context.Context, parseMention gtsmodel.Pa } // Gather all the database IDs from each of the gathered status mentions, tags, and emojis. - status.MentionIDs = util.Gather(nil, status.Mentions, func(mention *gtsmodel.Mention) string { return mention.ID }) - status.TagIDs = util.Gather(nil, status.Tags, func(tag *gtsmodel.Tag) string { return tag.ID }) - status.EmojiIDs = util.Gather(nil, status.Emojis, func(emoji *gtsmodel.Emoji) string { return emoji.ID }) + status.MentionIDs = xslices.Gather(nil, status.Mentions, func(mention *gtsmodel.Mention) string { return mention.ID }) + status.TagIDs = xslices.Gather(nil, status.Tags, func(tag *gtsmodel.Tag) string { return tag.ID }) + status.EmojiIDs = xslices.Gather(nil, status.Emojis, func(emoji *gtsmodel.Emoji) string { return emoji.ID }) if status.ContentWarning != "" && len(status.AttachmentIDs) > 0 { // If a content-warning is set, and diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index cfc790bd4..ed8bc1d8d 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -36,7 +36,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/uris" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" ) // AccountToAS converts a gts model account into an activity streams person, suitable for federation @@ -1819,7 +1819,7 @@ func populateValuesForProp[T ap.WithIRI]( // Deduplicate the iri strings to // make sure we're not parsing + adding // the same string multiple times. - iriStrs = util.Deduplicate(iriStrs) + iriStrs = xslices.Deduplicate(iriStrs) // Append them to the property. for _, iriStr := range iriStrs { diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index f8ada4b33..5f919f014 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -1576,9 +1576,9 @@ func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins instance.Configuration.MediaAttachments.VideoSizeLimit = int(videoSz) // #nosec G115 -- Already validated. // we don't actually set any limits on these. set to max possible. - instance.Configuration.MediaAttachments.ImageMatrixLimit = math.MaxInt - instance.Configuration.MediaAttachments.VideoFrameRateLimit = math.MaxInt - instance.Configuration.MediaAttachments.VideoMatrixLimit = math.MaxInt + instance.Configuration.MediaAttachments.ImageMatrixLimit = math.MaxInt32 + instance.Configuration.MediaAttachments.VideoFrameRateLimit = math.MaxInt32 + instance.Configuration.MediaAttachments.VideoMatrixLimit = math.MaxInt32 instance.Configuration.Polls.MaxOptions = config.GetStatusesPollMaxOptions() instance.Configuration.Polls.MaxCharactersPerOption = config.GetStatusesPollOptionMaxChars() @@ -1739,9 +1739,9 @@ func (c *Converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins instance.Configuration.MediaAttachments.VideoSizeLimit = int(videoSz) // #nosec G115 -- Already validated. // we don't actually set any limits on these. set to max possible. - instance.Configuration.MediaAttachments.ImageMatrixLimit = math.MaxInt - instance.Configuration.MediaAttachments.VideoFrameRateLimit = math.MaxInt - instance.Configuration.MediaAttachments.VideoMatrixLimit = math.MaxInt + instance.Configuration.MediaAttachments.ImageMatrixLimit = math.MaxInt32 + instance.Configuration.MediaAttachments.VideoFrameRateLimit = math.MaxInt32 + instance.Configuration.MediaAttachments.VideoMatrixLimit = math.MaxInt32 instance.Configuration.Polls.MaxOptions = config.GetStatusesPollMaxOptions() instance.Configuration.Polls.MaxCharactersPerOption = config.GetStatusesPollOptionMaxChars() diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 6c318e851..b9cadb183 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -1968,10 +1968,10 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() { "video/x-matroska" ], "image_size_limit": 41943040, - "image_matrix_limit": 9223372036854775807, + "image_matrix_limit": 2147483647, "video_size_limit": 41943040, - "video_frame_rate_limit": 9223372036854775807, - "video_matrix_limit": 9223372036854775807 + "video_frame_rate_limit": 2147483647, + "video_matrix_limit": 2147483647 }, "polls": { "max_options": 6, @@ -2113,10 +2113,10 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV2ToFrontend() { "video/x-matroska" ], "image_size_limit": 41943040, - "image_matrix_limit": 9223372036854775807, + "image_matrix_limit": 2147483647, "video_size_limit": 41943040, - "video_frame_rate_limit": 9223372036854775807, - "video_matrix_limit": 9223372036854775807 + "video_frame_rate_limit": 2147483647, + "video_matrix_limit": 2147483647 }, "polls": { "max_options": 6, diff --git a/internal/util/slices.go b/internal/util/xslices/slices.go similarity index 78% rename from internal/util/slices.go rename to internal/util/xslices/slices.go index 955fe8830..1c1c159b2 100644 --- a/internal/util/slices.go +++ b/internal/util/xslices/slices.go @@ -15,12 +15,53 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package util +package xslices import ( "slices" ) +// GrowJust increases slice capacity to guarantee +// extra room 'size', where in the case that it does +// need to allocate more it ONLY allocates 'size' extra. +// This is different to typical slices.Grow behaviour, +// which simply guarantees extra through append() which +// may allocate more than necessary extra size. +func GrowJust[T any](in []T, size int) []T { + + if cap(in)-len(in) < size { + // Reallocate enough for in + size. + in2 := make([]T, len(in), len(in)+size) + _ = copy(in2, in) + in = in2 + } + + return in +} + +// AppendJust appends extra elements to slice, +// ONLY allocating at most len(extra) elements. This +// is different to the typical append behaviour which +// will append extra, in a manner to reduce the need +// for new allocations on every call to append. +func AppendJust[T any](in []T, extra ...T) []T { + l := len(in) + + if cap(in)-l < len(extra) { + // Reallocate enough for + extra. + in2 := make([]T, l+len(extra)) + _ = copy(in2, in) + in = in2 + } else { + // Reslice for + extra. + in = in[:l+len(extra)] + } + + // Copy extra into slice. + _ = copy(in[l:], extra) + return in +} + // Deduplicate deduplicates entries in the given slice. func Deduplicate[T comparable](in []T) []T { var ( diff --git a/internal/util/slices_test.go b/internal/util/xslices/slices_test.go similarity index 52% rename from internal/util/slices_test.go rename to internal/util/xslices/slices_test.go index c93e489f5..7c62ac77f 100644 --- a/internal/util/slices_test.go +++ b/internal/util/xslices/slices_test.go @@ -15,22 +15,90 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package util_test +package xslices_test import ( + "math/rand" "net/url" "slices" "testing" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/stretchr/testify/assert" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" ) -var ( - testURLSlice = []*url.URL{} -) +func TestGrowJust(t *testing.T) { + for _, l := range []int{0, 2, 4, 8, 16, 32, 64} { + for _, x := range []int{0, 2, 4, 8, 16, 32, 64} { + s := make([]int, l, l+x) + for _, g := range []int{0, 2, 4, 8, 16, 32, 64} { + s2 := xslices.GrowJust(s, g) + + // Slice length should not be different. + assert.Equal(t, len(s), len(s2)) + + switch { + // If slice already has capacity for + // 'g' then it should not be changed. + case cap(s) >= len(s)+g: + assert.Equal(t, cap(s), cap(s2)) + + // Else, returned slice should only + // have capacity for original length + // plus extra elements, NOTHING MORE. + default: + assert.Equal(t, cap(s2), len(s)+g) + } + } + } + } +} + +func TestAppendJust(t *testing.T) { + for _, l := range []int{0, 2, 4, 8, 16, 32, 64} { + for _, x := range []int{0, 2, 4, 8, 16, 32, 64} { + s := make([]int, l, l+x) + + // Randomize slice. + for i := range s { + s[i] = rand.Int() + } + + for _, a := range []int{0, 2, 4, 8, 16, 32, 64} { + toAppend := make([]int, a) + + // Randomize appended vals. + for i := range toAppend { + toAppend[i] = rand.Int() + } + + s2 := xslices.AppendJust(s, toAppend...) + + // Slice length should be as expected. + assert.Equal(t, len(s)+a, len(s2)) + + // Slice contents should be as expected. + assert.Equal(t, append(s, toAppend...), s2) + + switch { + // If slice already has capacity for + // 'toAppend' then it should not change. + case cap(s) >= len(s)+a: + assert.Equal(t, cap(s), cap(s2)) + + // Else, returned slice should only + // have capacity for original length + // plus extra elements, NOTHING MORE. + default: + assert.Equal(t, len(s)+a, cap(s2)) + } + } + } + } +} func TestGather(t *testing.T) { - out := util.Gather(nil, []*url.URL{ + out := xslices.Gather(nil, []*url.URL{ {Scheme: "https", Host: "google.com", Path: "/some-search"}, {Scheme: "http", Host: "example.com", Path: "/robots.txt"}, }, (*url.URL).String) @@ -41,7 +109,7 @@ func TestGather(t *testing.T) { t.Fatal("unexpected gather output") } - out = util.Gather([]string{ + out = xslices.Gather([]string{ "starting input string", "another starting input", }, []*url.URL{ @@ -59,7 +127,7 @@ func TestGather(t *testing.T) { } func TestGatherIf(t *testing.T) { - out := util.GatherIf(nil, []string{ + out := xslices.GatherIf(nil, []string{ "hello world", "not hello world", "hello world", @@ -73,7 +141,7 @@ func TestGatherIf(t *testing.T) { t.Fatal("unexpected gatherif output") } - out = util.GatherIf([]string{ + out = xslices.GatherIf([]string{ "starting input string", "another starting input", }, []string{ diff --git a/mkdocs.yml b/mkdocs.yml index 4f179f1a4..1b12db12b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,6 +4,7 @@ theme: name: material language: en font: false + custom_dir: docs/overrides features: - header.autohide - content.code.copy @@ -34,8 +35,17 @@ plugins: syntaxHighlightTheme: obsidian - include-markdown +extra: + alternate: + - name: English + link: /en/ + lang: en + - name: 中文 + link: /zh-cn/ + lang: zh + extra_css: - - assets/css/colours.css + - public/css/colours.css markdown_extensions: - admonition @@ -57,6 +67,9 @@ markdown_extensions: - pymdownx.tabbed: alternate_style: true +exclude_docs: | + locales/** + nav: - "Home": "index.md" - "FAQ": "faq.md" diff --git a/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffmpeg/ffmpeg.wasm b/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffmpeg/ffmpeg.wasm deleted file mode 100644 index 9d1faa3ed..000000000 Binary files a/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffmpeg/ffmpeg.wasm and /dev/null differ diff --git a/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffmpeg/lib.go b/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffmpeg/lib.go deleted file mode 100644 index abe32d7c1..000000000 --- a/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffmpeg/lib.go +++ /dev/null @@ -1,25 +0,0 @@ -package ffmpeg - -import ( - _ "embed" - "os" -) - -func init() { - // Check for WASM source file path. - path := os.Getenv("FFMPEG_WASM") - if path == "" { - return - } - - var err error - - // Read file into memory. - B, err = os.ReadFile(path) - if err != nil { - panic(err) - } -} - -//go:embed ffmpeg.wasm -var B []byte diff --git a/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffmpreg.wasm.gz b/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffmpreg.wasm.gz new file mode 100644 index 000000000..1545d58bc Binary files /dev/null and b/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffmpreg.wasm.gz differ diff --git a/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffprobe/ffprobe.wasm b/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffprobe/ffprobe.wasm deleted file mode 100644 index 0094c53f4..000000000 Binary files a/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffprobe/ffprobe.wasm and /dev/null differ diff --git a/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffprobe/lib.go b/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffprobe/lib.go deleted file mode 100644 index c3c3a3df1..000000000 --- a/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffprobe/lib.go +++ /dev/null @@ -1,25 +0,0 @@ -package ffprobe - -import ( - _ "embed" - "os" -) - -func init() { - // Check for WASM source file path. - path := os.Getenv("FFPROBE_WASM") - if path == "" { - return - } - - var err error - - // Read file into memory. - B, err = os.ReadFile(path) - if err != nil { - panic(err) - } -} - -//go:embed ffprobe.wasm -var B []byte diff --git a/vendor/codeberg.org/gruf/go-ffmpreg/embed/lib.go b/vendor/codeberg.org/gruf/go-ffmpreg/embed/lib.go new file mode 100644 index 000000000..7829b5524 --- /dev/null +++ b/vendor/codeberg.org/gruf/go-ffmpreg/embed/lib.go @@ -0,0 +1,39 @@ +package embed + +import ( + "bytes" + "compress/gzip" + _ "embed" + "io" + "os" +) + +func init() { + var err error + + if path := os.Getenv("FFMPREG_WASM"); path != "" { + // Read file into memory. + B, err = os.ReadFile(path) + if err != nil { + panic(err) + } + } + + // Wrap bytes in reader. + b := bytes.NewReader(B) + + // Create unzipper from reader. + gz, err := gzip.NewReader(b) + if err != nil { + panic(err) + } + + // Extract gzipped binary. + B, err = io.ReadAll(gz) + if err != nil { + panic(err) + } +} + +//go:embed ffmpreg.wasm.gz +var B []byte diff --git a/vendor/codeberg.org/gruf/go-ffmpreg/wasm/run.go b/vendor/codeberg.org/gruf/go-ffmpreg/wasm/run.go index 62ce2bc25..7b07d851d 100644 --- a/vendor/codeberg.org/gruf/go-ffmpreg/wasm/run.go +++ b/vendor/codeberg.org/gruf/go-ffmpreg/wasm/run.go @@ -14,6 +14,11 @@ // wazero.Runtime on module instantiation. type Args struct { + // Program name, depending on the + // module being run this may or may + // not be necessary. + Name string + // Optional further module configuration function. // (e.g. to mount filesystem dir, set env vars, etc). Config func(wazero.ModuleConfig) wazero.ModuleConfig @@ -39,7 +44,7 @@ func Run( // Prefix arguments with module name. cargs := make([]string, len(args.Args)+1) - cargs[0] = module.Name() + cargs[0] = args.Name copy(cargs[1:], args.Args) // Prepare new module configuration. diff --git a/vendor/github.com/ncruces/go-sqlite3/config.go b/vendor/github.com/ncruces/go-sqlite3/config.go index cf72cbda5..474f960a2 100644 --- a/vendor/github.com/ncruces/go-sqlite3/config.go +++ b/vendor/github.com/ncruces/go-sqlite3/config.go @@ -2,6 +2,7 @@ import ( "context" + "fmt" "strconv" "github.com/tetratelabs/wazero/api" @@ -70,6 +71,15 @@ func logCallback(ctx context.Context, mod api.Module, _, iCode, zMsg uint32) { } } +// Log writes a message into the error log established by [Conn.ConfigLog]. +// +// https://sqlite.org/c3ref/log.html +func (c *Conn) Log(code ExtendedErrorCode, format string, a ...any) { + if c.log != nil { + c.log(code, fmt.Sprintf(format, a...)) + } +} + // FileControl allows low-level control of database files. // Only a subset of opcodes are supported. // diff --git a/vendor/github.com/ncruces/go-sqlite3/context.go b/vendor/github.com/ncruces/go-sqlite3/context.go index be5dd92c5..86be214e2 100644 --- a/vendor/github.com/ncruces/go-sqlite3/context.go +++ b/vendor/github.com/ncruces/go-sqlite3/context.go @@ -89,6 +89,7 @@ func (ctx Context) ResultText(value string) { } // ResultRawText sets the text result of the function to a []byte. +// Returning a nil slice is the same as calling [Context.ResultNull]. // // https://sqlite.org/c3ref/result_blob.html func (ctx Context) ResultRawText(value []byte) { diff --git a/vendor/github.com/ncruces/go-sqlite3/error.go b/vendor/github.com/ncruces/go-sqlite3/error.go index 71238ef12..870aa3ab1 100644 --- a/vendor/github.com/ncruces/go-sqlite3/error.go +++ b/vendor/github.com/ncruces/go-sqlite3/error.go @@ -106,6 +106,11 @@ func (e ErrorCode) Temporary() bool { return e == BUSY } +// ExtendedCode returns the extended error code for this error. +func (e ErrorCode) ExtendedCode() ExtendedErrorCode { + return ExtendedErrorCode(e) +} + // Error implements the error interface. func (e ExtendedErrorCode) Error() string { return util.ErrorCodeString(uint32(e)) @@ -136,6 +141,11 @@ func (e ExtendedErrorCode) Timeout() bool { return e == BUSY_TIMEOUT } +// Code returns the primary error code for this error. +func (e ExtendedErrorCode) Code() ErrorCode { + return ErrorCode(e) +} + func errorCode(err error, def ErrorCode) (msg string, code uint32) { switch code := err.(type) { case nil: diff --git a/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap.go b/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap.go index 5788eeb24..613bb90b1 100644 --- a/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap.go +++ b/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap.go @@ -1,4 +1,4 @@ -//go:build unix && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_noshm || sqlite3_nosys) +//go:build unix && !sqlite3_nosys package util @@ -55,10 +55,10 @@ type MappedRegion struct { used bool } -func MapRegion(ctx context.Context, mod api.Module, f *os.File, offset int64, size int32, prot int) (*MappedRegion, error) { +func MapRegion(ctx context.Context, mod api.Module, f *os.File, offset int64, size int32, readOnly bool) (*MappedRegion, error) { s := ctx.Value(moduleKey{}).(*moduleState) r := s.new(ctx, mod, size) - err := r.mmap(f, offset, prot) + err := r.mmap(f, offset, readOnly) if err != nil { return nil, err } @@ -75,7 +75,11 @@ func (r *MappedRegion) Unmap() error { return err } -func (r *MappedRegion) mmap(f *os.File, offset int64, prot int) error { +func (r *MappedRegion) mmap(f *os.File, offset int64, readOnly bool) error { + prot := unix.PROT_READ + if !readOnly { + prot |= unix.PROT_WRITE + } _, err := unix.MmapPtr(int(f.Fd()), offset, r.addr, uintptr(r.size), prot, unix.MAP_SHARED|unix.MAP_FIXED) r.used = err == nil diff --git a/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_other.go b/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_other.go index a2fbf24df..e11f953a7 100644 --- a/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_other.go +++ b/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_other.go @@ -1,4 +1,4 @@ -//go:build !unix || !(386 || arm || amd64 || arm64 || riscv64 || ppc64le) || sqlite3_noshm || sqlite3_nosys +//go:build !unix || sqlite3_nosys package util diff --git a/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_windows.go b/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_windows.go new file mode 100644 index 000000000..fdf6f439a --- /dev/null +++ b/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_windows.go @@ -0,0 +1,53 @@ +//go:build !sqlite3_nosys + +package util + +import ( + "context" + "os" + "reflect" + "unsafe" + + "github.com/tetratelabs/wazero/api" + "golang.org/x/sys/windows" +) + +type MappedRegion struct { + windows.Handle + Data []byte + addr uintptr +} + +func MapRegion(ctx context.Context, mod api.Module, f *os.File, offset int64, size int32) (*MappedRegion, error) { + h, err := windows.CreateFileMapping(windows.Handle(f.Fd()), nil, windows.PAGE_READWRITE, 0, 0, nil) + if h == 0 { + return nil, err + } + + a, err := windows.MapViewOfFile(h, windows.FILE_MAP_WRITE, + uint32(offset>>32), uint32(offset), uintptr(size)) + if a == 0 { + windows.CloseHandle(h) + return nil, err + } + + res := &MappedRegion{Handle: h, addr: a} + // SliceHeader, although deprecated, avoids a go vet warning. + sh := (*reflect.SliceHeader)(unsafe.Pointer(&res.Data)) + sh.Len = int(size) + sh.Cap = int(size) + sh.Data = a + return res, nil +} + +func (r *MappedRegion) Unmap() error { + if r.Data == nil { + return nil + } + err := windows.UnmapViewOfFile(r.addr) + if err != nil { + return err + } + r.Data = nil + return windows.CloseHandle(r.Handle) +} diff --git a/vendor/github.com/ncruces/go-sqlite3/stmt.go b/vendor/github.com/ncruces/go-sqlite3/stmt.go index 9da2a2eaf..139dd3525 100644 --- a/vendor/github.com/ncruces/go-sqlite3/stmt.go +++ b/vendor/github.com/ncruces/go-sqlite3/stmt.go @@ -255,6 +255,7 @@ func (s *Stmt) BindText(param int, value string) error { // BindRawText binds a []byte to the prepared statement as text. // The leftmost SQL parameter has an index of 1. +// Binding a nil slice is the same as calling [Stmt.BindNull]. // // https://sqlite.org/c3ref/bind_blob.html func (s *Stmt) BindRawText(param int, value []byte) error { diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/README.md b/vendor/github.com/ncruces/go-sqlite3/vfs/README.md index 77991486b..cf0e3c30f 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/README.md +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/README.md @@ -15,24 +15,23 @@ The main differences are [file locking](#file-locking) and [WAL mode](#write-ahe POSIX advisory locks, which SQLite uses on Unix, are [broken by design](https://github.com/sqlite/sqlite/blob/b74eb0/src/os_unix.c#L1073-L1161). - -On Linux and macOS, this package uses +Instead, on Linux and macOS, this package uses [OFD locks](https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html) to synchronize access to database files. -OFD locks are fully compatible with POSIX advisory locks. This package can also use [BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2), albeit with reduced concurrency (`BEGIN IMMEDIATE` behaves like `BEGIN EXCLUSIVE`). -On BSD, macOS, and illumos, BSD locks are fully compatible with POSIX advisory locks; -on Linux and z/OS, they are fully functional, but incompatible; -elsewhere, they are very likely broken. BSD locks are the default on BSD and illumos, but you can opt into them with the `sqlite3_flock` build tag. On Windows, this package uses `LockFileEx` and `UnlockFileEx`, like SQLite. +You can also opt into a cross-platform locking implementation +with the `sqlite3_dotlk` build tag. +The only requirement is an atomic `os.Mkdir`. + Otherwise, file locking is not supported, and you must use [`nolock=1`](https://sqlite.org/uri.html#urinolock) (or [`immutable=1`](https://sqlite.org/uri.html#uriimmutable)) @@ -46,7 +45,7 @@ to check if your build supports file locking. ### Write-Ahead Logging -On little-endian Unix, this package uses `mmap` to implement +On Unix, this package may use `mmap` to implement [shared-memory for the WAL-index](https://sqlite.org/wal.html#implementation_of_shared_memory_for_the_wal_index), like SQLite. @@ -55,6 +54,11 @@ a WAL database can only be accessed by a single proccess. Other processes that attempt to access a database locked with BSD locks, will fail with the [`SQLITE_PROTOCOL`](https://sqlite.org/rescode.html#protocol) error code. +On Windows, this package may use `MapViewOfFile`, like SQLite. + +You can also opt into a cross-platform, in-process, memory sharing implementation +with the `sqlite3_dotlk` build tag. + Otherwise, [WAL support is limited](https://sqlite.org/wal.html#noshm), and `EXCLUSIVE` locking mode must be set to create, read, and write WAL databases. To use `EXCLUSIVE` locking mode with the @@ -67,7 +71,7 @@ to check if your build supports shared memory. ### Batch-Atomic Write -On 64-bit Linux, this package supports +On Linux, this package may support [batch-atomic writes](https://sqlite.org/cgi/src/technote/714) on the F2FS filesystem. @@ -86,27 +90,27 @@ The implementation is compatible with SQLite's ### Build Tags The VFS can be customized with a few build tags: -- `sqlite3_flock` forces the use of BSD locks; it can be used on z/OS to enable locking, - and elsewhere to test BSD locks. -- `sqlite3_nosys` prevents importing [`x/sys`](https://pkg.go.dev/golang.org/x/sys); - disables locking _and_ shared memory on all platforms. -- `sqlite3_noshm` disables shared memory on all platforms. +- `sqlite3_flock` forces the use of BSD locks. +- `sqlite3_dotlk` forces the use of dot-file locks. +- `sqlite3_nosys` prevents importing [`x/sys`](https://pkg.go.dev/golang.org/x/sys). > [!IMPORTANT] > The default configuration of this package is compatible with the standard > [Unix and Windows SQLite VFSes](https://sqlite.org/vfs.html#multiple_vfses); > `sqlite3_flock` builds are compatible with the -> [`unix-flock` VFS](https://sqlite.org/compile.html#enable_locking_style). +> [`unix-flock` VFS](https://sqlite.org/compile.html#enable_locking_style); +> `sqlite3_dotlk` builds are compatible with the +> [`unix-dotfile` VFS](https://sqlite.org/compile.html#enable_locking_style). > If incompatible file locking is used, accessing databases concurrently with > _other_ SQLite libraries will eventually corrupt data. ### Custom VFSes -- [`github.com/ncruces/go-sqlite3/vfs/adiantum`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/adiantum) - wraps a VFS to offer encryption at rest. - [`github.com/ncruces/go-sqlite3/vfs/memdb`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb) implements an in-memory VFS. - [`github.com/ncruces/go-sqlite3/vfs/readervfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs) implements a VFS for immutable databases. +- [`github.com/ncruces/go-sqlite3/vfs/adiantum`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/adiantum) + wraps a VFS to offer encryption at rest. - [`github.com/ncruces/go-sqlite3/vfs/xts`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/xts) - wraps a VFS to offer encryption at rest. \ No newline at end of file + wraps a VFS to offer encryption at rest. diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/const.go b/vendor/github.com/ncruces/go-sqlite3/vfs/const.go index e80437be6..0a8fee621 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/const.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/const.go @@ -234,4 +234,8 @@ func (e _ErrorCode) Error() string { _SHM_LOCK _ShmFlag = 2 _SHM_SHARED _ShmFlag = 4 _SHM_EXCLUSIVE _ShmFlag = 8 + + _SHM_NLOCK = 8 + _SHM_BASE = 120 + _SHM_DMS = _SHM_BASE + _SHM_NLOCK ) diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/file.go b/vendor/github.com/ncruces/go-sqlite3/vfs/file.go index ebd42e9ad..ba70aa14f 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/file.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/file.go @@ -35,10 +35,10 @@ func testSymlinks(path string) error { func (vfsOS) Delete(path string, syncDir bool) error { err := os.Remove(path) + if errors.Is(err, fs.ErrNotExist) { + return _IOERR_DELETE_NOENT + } if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return _IOERR_DELETE_NOENT - } return err } if runtime.GOOS != "windows" && syncDir { @@ -151,6 +151,7 @@ func (f *vfsFile) Close() error { if f.shm != nil { f.shm.Close() } + f.Unlock(LOCK_NONE) return f.File.Close() } @@ -206,10 +207,10 @@ func (f *vfsFile) HasMoved() (bool, error) { return false, err } pi, err := os.Stat(f.Name()) + if errors.Is(err, fs.ErrNotExist) { + return true, nil + } if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return true, nil - } return false, err } return !os.SameFile(fi, pi), nil diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/lock.go b/vendor/github.com/ncruces/go-sqlite3/vfs/lock.go index 890684169..22e320a81 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/lock.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/lock.go @@ -1,4 +1,4 @@ -//go:build (linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock) && !sqlite3_nosys +//go:build ((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk package vfs diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/lock_other.go b/vendor/github.com/ncruces/go-sqlite3/vfs/lock_other.go index c395f34a7..81aacc622 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/lock_other.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/lock_other.go @@ -1,4 +1,4 @@ -//go:build !(linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock) || sqlite3_nosys +//go:build !(((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk) package vfs diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/memdb/memdb.go b/vendor/github.com/ncruces/go-sqlite3/vfs/memdb/memdb.go index d313b45d1..686f8e9a7 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/memdb/memdb.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/memdb/memdb.go @@ -78,19 +78,15 @@ type memDB struct { // +checklocks:dataMtx data []*[sectorSize]byte - // +checklocks:dataMtx size int64 - // +checklocks:lockMtx - shared int32 - // +checklocks:lockMtx - reserved bool - // +checklocks:lockMtx - pending bool - // +checklocks:memoryMtx - refs int + refs int32 + + shared int32 // +checklocks:lockMtx + pending bool // +checklocks:lockMtx + reserved bool // +checklocks:lockMtx lockMtx sync.Mutex dataMtx sync.RWMutex @@ -253,12 +249,12 @@ func (m *memFile) Unlock(lock vfs.LockLevel) error { m.lockMtx.Lock() defer m.lockMtx.Unlock() - if m.pending && m.lock >= vfs.LOCK_PENDING { - m.pending = false - } - if m.reserved && m.lock >= vfs.LOCK_RESERVED { + if m.lock >= vfs.LOCK_RESERVED { m.reserved = false } + if m.lock >= vfs.LOCK_PENDING { + m.pending = false + } if lock < vfs.LOCK_SHARED { m.shared-- } diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_bsd.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_bsd.go index 1f54a6929..56713e359 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_bsd.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_bsd.go @@ -1,4 +1,4 @@ -//go:build (freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock) && !sqlite3_nosys +//go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && !(sqlite3_dotlk || sqlite3_nosys)) || sqlite3_flock package vfs diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_dotlk.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_dotlk.go new file mode 100644 index 000000000..1c1a49c11 --- /dev/null +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_dotlk.go @@ -0,0 +1,143 @@ +//go:build sqlite3_dotlk + +package vfs + +import ( + "errors" + "io/fs" + "os" + "sync" +) + +var ( + // +checklocks:vfsDotLocksMtx + vfsDotLocks = map[string]*vfsDotLocker{} + vfsDotLocksMtx sync.Mutex +) + +type vfsDotLocker struct { + shared int // +checklocks:vfsDotLocksMtx + pending *os.File // +checklocks:vfsDotLocksMtx + reserved *os.File // +checklocks:vfsDotLocksMtx +} + +func osGetSharedLock(file *os.File) _ErrorCode { + vfsDotLocksMtx.Lock() + defer vfsDotLocksMtx.Unlock() + + name := file.Name() + locker := vfsDotLocks[name] + if locker == nil { + err := os.Mkdir(name+".lock", 0777) + if errors.Is(err, fs.ErrExist) { + return _BUSY // Another process has the lock. + } + if err != nil { + return _IOERR_LOCK + } + locker = &vfsDotLocker{} + vfsDotLocks[name] = locker + } + + if locker.pending != nil { + return _BUSY + } + locker.shared++ + return _OK +} + +func osGetReservedLock(file *os.File) _ErrorCode { + vfsDotLocksMtx.Lock() + defer vfsDotLocksMtx.Unlock() + + name := file.Name() + locker := vfsDotLocks[name] + if locker == nil { + return _IOERR_LOCK + } + + if locker.reserved != nil && locker.reserved != file { + return _BUSY + } + locker.reserved = file + return _OK +} + +func osGetExclusiveLock(file *os.File, _ *LockLevel) _ErrorCode { + vfsDotLocksMtx.Lock() + defer vfsDotLocksMtx.Unlock() + + name := file.Name() + locker := vfsDotLocks[name] + if locker == nil { + return _IOERR_LOCK + } + + if locker.pending != nil && locker.pending != file { + return _BUSY + } + locker.pending = file + if locker.shared > 1 { + return _BUSY + } + return _OK +} + +func osDowngradeLock(file *os.File, _ LockLevel) _ErrorCode { + vfsDotLocksMtx.Lock() + defer vfsDotLocksMtx.Unlock() + + name := file.Name() + locker := vfsDotLocks[name] + if locker == nil { + return _IOERR_UNLOCK + } + + if locker.reserved == file { + locker.reserved = nil + } + if locker.pending == file { + locker.pending = nil + } + return _OK +} + +func osReleaseLock(file *os.File, state LockLevel) _ErrorCode { + vfsDotLocksMtx.Lock() + defer vfsDotLocksMtx.Unlock() + + name := file.Name() + locker := vfsDotLocks[name] + if locker == nil { + return _IOERR_UNLOCK + } + + if locker.shared == 1 { + err := os.Remove(name + ".lock") + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return _IOERR_UNLOCK + } + delete(vfsDotLocks, name) + } + + if locker.reserved == file { + locker.reserved = nil + } + if locker.pending == file { + locker.pending = nil + } + locker.shared-- + return _OK +} + +func osCheckReservedLock(file *os.File) (bool, _ErrorCode) { + vfsDotLocksMtx.Lock() + defer vfsDotLocksMtx.Unlock() + + name := file.Name() + locker := vfsDotLocks[name] + if locker == nil { + return false, _OK + } + return locker.reserved != nil, _OK +} diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_ofd.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_ofd.go index 15730fe62..b4f570f4d 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_ofd.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_ofd.go @@ -1,4 +1,4 @@ -//go:build (linux || darwin) && !(sqlite3_flock || sqlite3_nosys) +//go:build (linux || darwin) && !(sqlite3_flock || sqlite3_dotlk || sqlite3_nosys) package vfs diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_windows.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_windows.go index 4f6149ba3..b901f98aa 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_windows.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_windows.go @@ -1,4 +1,4 @@ -//go:build !sqlite3_nosys +//go:build !(sqlite3_dotlk || sqlite3_nosys) package vfs diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm.go index 402676afb..9d9dff1c4 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/shm.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm.go @@ -1,20 +1,7 @@ -//go:build (darwin || linux) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_flock || sqlite3_noshm || sqlite3_nosys) +//go:build ((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk package vfs -import ( - "context" - "io" - "os" - "sync" - "time" - - "github.com/tetratelabs/wazero/api" - "golang.org/x/sys/unix" - - "github.com/ncruces/go-sqlite3/internal/util" -) - // SupportsSharedMemory is false on platforms that do not support shared memory. // To use [WAL without shared-memory], you need to set [EXCLUSIVE locking mode]. // @@ -22,12 +9,6 @@ // [EXCLUSIVE locking mode]: https://sqlite.org/pragma.html#pragma_locking_mode const SupportsSharedMemory = true -const ( - _SHM_NLOCK = 8 - _SHM_BASE = 120 - _SHM_DMS = _SHM_BASE + _SHM_NLOCK -) - func (f *vfsFile) SharedMemory() SharedMemory { return f.shm } // NewSharedMemory returns a shared-memory WAL-index @@ -41,172 +22,5 @@ func NewSharedMemory(path string, flags OpenFlag) SharedMemory { if flags&OPEN_MAIN_DB == 0 || flags&(OPEN_DELETEONCLOSE|OPEN_MEMORY) != 0 { return nil } - return &vfsShm{ - path: path, - readOnly: flags&OPEN_READONLY != 0, - } -} - -var _ blockingSharedMemory = &vfsShm{} - -type vfsShm struct { - *os.File - path string - regions []*util.MappedRegion - readOnly bool - blocking bool - sync.Mutex -} - -func (s *vfsShm) shmOpen() _ErrorCode { - if s.File == nil { - var flag int - if s.readOnly { - flag = unix.O_RDONLY - } else { - flag = unix.O_RDWR - } - f, err := os.OpenFile(s.path, - flag|unix.O_CREAT|unix.O_NOFOLLOW, 0666) - if err != nil { - return _CANTOPEN - } - s.File = f - } - - // Dead man's switch. - if lock, rc := osTestLock(s.File, _SHM_DMS, 1); rc != _OK { - return _IOERR_LOCK - } else if lock == unix.F_WRLCK { - return _BUSY - } else if lock == unix.F_UNLCK { - if s.readOnly { - return _READONLY_CANTINIT - } - // Do not use a blocking lock here. - // If the lock cannot be obtained immediately, - // it means some other connection is truncating the file. - // And after it has done so, it will not release its lock, - // but only downgrade it to a shared lock. - // So no point in blocking here. - // The call below to obtain the shared DMS lock may use a blocking lock. - if rc := osWriteLock(s.File, _SHM_DMS, 1, 0); rc != _OK { - return rc - } - if err := s.Truncate(0); err != nil { - return _IOERR_SHMOPEN - } - } - if rc := osReadLock(s.File, _SHM_DMS, 1, time.Millisecond); rc != _OK { - return rc - } - return _OK -} - -func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (uint32, _ErrorCode) { - // Ensure size is a multiple of the OS page size. - if int(size)&(unix.Getpagesize()-1) != 0 { - return 0, _IOERR_SHMMAP - } - - if rc := s.shmOpen(); rc != _OK { - return 0, rc - } - - // Check if file is big enough. - o, err := s.Seek(0, io.SeekEnd) - if err != nil { - return 0, _IOERR_SHMSIZE - } - if n := (int64(id) + 1) * int64(size); n > o { - if !extend { - return 0, _OK - } - err := osAllocate(s.File, n) - if err != nil { - return 0, _IOERR_SHMSIZE - } - } - - var prot int - if s.readOnly { - prot = unix.PROT_READ - } else { - prot = unix.PROT_READ | unix.PROT_WRITE - } - r, err := util.MapRegion(ctx, mod, s.File, int64(id)*int64(size), size, prot) - if err != nil { - return 0, _IOERR_SHMMAP - } - s.regions = append(s.regions, r) - if s.readOnly { - return r.Ptr, _READONLY - } - return r.Ptr, _OK -} - -func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { - // Argument check. - if n <= 0 || offset < 0 || offset+n > _SHM_NLOCK { - panic(util.AssertErr()) - } - switch flags { - case - _SHM_LOCK | _SHM_SHARED, - _SHM_LOCK | _SHM_EXCLUSIVE, - _SHM_UNLOCK | _SHM_SHARED, - _SHM_UNLOCK | _SHM_EXCLUSIVE: - // - default: - panic(util.AssertErr()) - } - if n != 1 && flags&_SHM_EXCLUSIVE == 0 { - panic(util.AssertErr()) - } - - var timeout time.Duration - if s.blocking { - timeout = time.Millisecond - } - - switch { - case flags&_SHM_UNLOCK != 0: - return osUnlock(s.File, _SHM_BASE+int64(offset), int64(n)) - case flags&_SHM_SHARED != 0: - return osReadLock(s.File, _SHM_BASE+int64(offset), int64(n), timeout) - case flags&_SHM_EXCLUSIVE != 0: - return osWriteLock(s.File, _SHM_BASE+int64(offset), int64(n), timeout) - default: - panic(util.AssertErr()) - } -} - -func (s *vfsShm) shmUnmap(delete bool) { - if s.File == nil { - return - } - - // Unmap regions. - for _, r := range s.regions { - r.Unmap() - } - clear(s.regions) - s.regions = s.regions[:0] - - // Close the file. - if delete { - os.Remove(s.path) - } - s.Close() - s.File = nil -} - -func (s *vfsShm) shmBarrier() { - s.Lock() - //lint:ignore SA2001 memory barrier. - s.Unlock() -} - -func (s *vfsShm) shmEnableBlocking(block bool) { - s.blocking = block + return &vfsShm{path: path} } diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_bsd.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_bsd.go index 8dc6ec922..d4e046369 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_bsd.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_bsd.go @@ -1,4 +1,4 @@ -//go:build (freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_noshm || sqlite3_nosys) +//go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_dotlk || sqlite3_nosys)) || sqlite3_flock package vfs @@ -14,44 +14,14 @@ "github.com/ncruces/go-sqlite3/internal/util" ) -// SupportsSharedMemory is false on platforms that do not support shared memory. -// To use [WAL without shared-memory], you need to set [EXCLUSIVE locking mode]. -// -// [WAL without shared-memory]: https://sqlite.org/wal.html#noshm -// [EXCLUSIVE locking mode]: https://sqlite.org/pragma.html#pragma_locking_mode -const SupportsSharedMemory = true - -const _SHM_NLOCK = 8 - -func (f *vfsFile) SharedMemory() SharedMemory { return f.shm } - -// NewSharedMemory returns a shared-memory WAL-index -// backed by a file with the given path. -// It will return nil if shared-memory is not supported, -// or not appropriate for the given flags. -// Only [OPEN_MAIN_DB] databases may need a WAL-index. -// You must ensure all concurrent accesses to a database -// use shared-memory instances created with the same path. -func NewSharedMemory(path string, flags OpenFlag) SharedMemory { - if flags&OPEN_MAIN_DB == 0 || flags&(OPEN_DELETEONCLOSE|OPEN_MEMORY) != 0 { - return nil - } - return &vfsShm{ - path: path, - readOnly: flags&OPEN_READONLY != 0, - } -} - type vfsShmFile struct { *os.File info os.FileInfo - // +checklocks:vfsShmFilesMtx - refs int + refs int // +checklocks:vfsShmFilesMtx - // +checklocks:lockMtx - lock [_SHM_NLOCK]int16 - lockMtx sync.Mutex + lock [_SHM_NLOCK]int16 // +checklocks:Mutex + sync.Mutex } var ( @@ -62,10 +32,9 @@ type vfsShmFile struct { type vfsShm struct { *vfsShmFile - path string - lock [_SHM_NLOCK]bool - regions []*util.MappedRegion - readOnly bool + path string + lock [_SHM_NLOCK]bool + regions []*util.MappedRegion } func (s *vfsShm) Close() error { @@ -80,7 +49,7 @@ func (s *vfsShm) Close() error { s.shmLock(0, _SHM_NLOCK, _SHM_UNLOCK) // Decrease reference count. - if s.vfsShmFile.refs > 1 { + if s.vfsShmFile.refs > 0 { s.vfsShmFile.refs-- s.vfsShmFile = nil return nil @@ -97,7 +66,7 @@ func (s *vfsShm) Close() error { panic(util.AssertErr()) } -func (s *vfsShm) shmOpen() (rc _ErrorCode) { +func (s *vfsShm) shmOpen() _ErrorCode { if s.vfsShmFile != nil { return _OK } @@ -128,34 +97,29 @@ func (s *vfsShm) shmOpen() (rc _ErrorCode) { } } - // Lock and truncate the file, if not readonly. + // Lock and truncate the file. // The lock is only released by closing the file. - if s.readOnly { - rc = _READONLY_CANTINIT - } else { - if rc := osLock(f, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK); rc != _OK { - return rc - } - if err := f.Truncate(0); err != nil { - return _IOERR_SHMOPEN - } + if rc := osLock(f, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK); rc != _OK { + return rc + } + if err := f.Truncate(0); err != nil { + return _IOERR_SHMOPEN } // Add the new shared file. s.vfsShmFile = &vfsShmFile{ File: f, info: fi, - refs: 1, } f = nil // Don't close the file. for i, g := range vfsShmFiles { if g == nil { vfsShmFiles[i] = s.vfsShmFile - return rc + return _OK } } vfsShmFiles = append(vfsShmFiles, s.vfsShmFile) - return rc + return _OK } func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (uint32, _ErrorCode) { @@ -177,32 +141,22 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext if !extend { return 0, _OK } - err := osAllocate(s.File, n) - if err != nil { + if osAllocate(s.File, n) != nil { return 0, _IOERR_SHMSIZE } } - var prot int - if s.readOnly { - prot = unix.PROT_READ - } else { - prot = unix.PROT_READ | unix.PROT_WRITE - } - r, err := util.MapRegion(ctx, mod, s.File, int64(id)*int64(size), size, prot) + r, err := util.MapRegion(ctx, mod, s.File, int64(id)*int64(size), size, false) if err != nil { return 0, _IOERR_SHMMAP } s.regions = append(s.regions, r) - if s.readOnly { - return r.Ptr, _READONLY - } return r.Ptr, _OK } func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { - s.lockMtx.Lock() - defer s.lockMtx.Unlock() + s.Lock() + defer s.Unlock() switch { case flags&_SHM_UNLOCK != 0: @@ -224,7 +178,7 @@ func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { if s.lock[i] { panic(util.AssertErr()) } - if s.vfsShmFile.lock[i] < 0 { + if s.vfsShmFile.lock[i]+1 <= 0 { return _BUSY } } @@ -261,8 +215,7 @@ func (s *vfsShm) shmUnmap(delete bool) { for _, r := range s.regions { r.Unmap() } - clear(s.regions) - s.regions = s.regions[:0] + s.regions = nil // Close the file. if delete { @@ -272,7 +225,7 @@ func (s *vfsShm) shmUnmap(delete bool) { } func (s *vfsShm) shmBarrier() { - s.lockMtx.Lock() + s.Lock() //lint:ignore SA2001 memory barrier. - s.lockMtx.Unlock() + s.Unlock() } diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_copy.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_copy.go new file mode 100644 index 000000000..7a250523e --- /dev/null +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_copy.go @@ -0,0 +1,84 @@ +//go:build (windows && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_dotlk + +package vfs + +import ( + "unsafe" + + "github.com/ncruces/go-sqlite3/internal/util" +) + +const ( + _WALINDEX_HDR_SIZE = 136 + _WALINDEX_PGSZ = 32768 +) + +// This looks like a safe way of keeping the WAL-index in sync. +// +// The WAL-index file starts with a header, +// and the index doesn't meaningfully change if the header doesn't change. +// +// The header starts with two 48 byte, checksummed, copies of the same information, +// which are accessed independently between memory barriers. +// The checkpoint information that follows uses 4 byte aligned words. +// +// Finally, we have the WAL-index hash tables, +// which are only modified holding the exclusive WAL_WRITE_LOCK. +// +// Since all the data is either redundant+checksummed, +// 4 byte aligned, or modified under an exclusive lock, +// the copies below should correctly keep copies in sync. +// +// https://sqlite.org/walformat.html#the_wal_index_file_format + +func (s *vfsShm) shmAcquire() { + if len(s.ptrs) == 0 || shmUnmodified(s.shadow[0][:], s.shared[0][:]) { + return + } + // Copies modified words from shared to private memory. + for id, p := range s.ptrs { + shared := shmPage(s.shared[id][:]) + shadow := shmPage(s.shadow[id][:]) + privat := shmPage(util.View(s.mod, p, _WALINDEX_PGSZ)) + for i, shared := range shared { + if shadow[i] != shared { + shadow[i] = shared + privat[i] = shared + } + } + } +} + +func (s *vfsShm) shmRelease() { + if len(s.ptrs) == 0 || shmUnmodified(s.shadow[0][:], util.View(s.mod, s.ptrs[0], _WALINDEX_HDR_SIZE)) { + return + } + // Copies modified words from private to shared memory. + for id, p := range s.ptrs { + shared := shmPage(s.shared[id][:]) + shadow := shmPage(s.shadow[id][:]) + privat := shmPage(util.View(s.mod, p, _WALINDEX_PGSZ)) + for i, privat := range privat { + if shadow[i] != privat { + shadow[i] = privat + shared[i] = privat + } + } + } +} + +func (s *vfsShm) shmBarrier() { + s.Lock() + s.shmAcquire() + s.shmRelease() + s.Unlock() +} + +func shmPage(s []byte) *[_WALINDEX_PGSZ / 4]uint32 { + p := (*uint32)(unsafe.Pointer(unsafe.SliceData(s))) + return (*[_WALINDEX_PGSZ / 4]uint32)(unsafe.Slice(p, _WALINDEX_PGSZ/4)) +} + +func shmUnmodified(v1, v2 []byte) bool { + return *(*[_WALINDEX_HDR_SIZE]byte)(v1[:]) == *(*[_WALINDEX_HDR_SIZE]byte)(v2[:]) +} diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_dotlk.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_dotlk.go new file mode 100644 index 000000000..36e00a1cd --- /dev/null +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_dotlk.go @@ -0,0 +1,224 @@ +//go:build sqlite3_dotlk + +package vfs + +import ( + "context" + "errors" + "io/fs" + "os" + "sync" + + "github.com/ncruces/go-sqlite3/internal/util" + "github.com/tetratelabs/wazero/api" +) + +type vfsShmBuffer struct { + shared [][_WALINDEX_PGSZ]byte + refs int // +checklocks:vfsShmBuffersMtx + + lock [_SHM_NLOCK]int16 // +checklocks:Mutex + sync.Mutex +} + +var ( + // +checklocks:vfsShmBuffersMtx + vfsShmBuffers = map[string]*vfsShmBuffer{} + vfsShmBuffersMtx sync.Mutex +) + +type vfsShm struct { + *vfsShmBuffer + mod api.Module + alloc api.Function + free api.Function + path string + shadow [][_WALINDEX_PGSZ]byte + ptrs []uint32 + stack [1]uint64 + lock [_SHM_NLOCK]bool +} + +func (s *vfsShm) Close() error { + if s.vfsShmBuffer == nil { + return nil + } + + vfsShmBuffersMtx.Lock() + defer vfsShmBuffersMtx.Unlock() + + // Unlock everything. + s.shmLock(0, _SHM_NLOCK, _SHM_UNLOCK) + + // Decrease reference count. + if s.vfsShmBuffer.refs > 0 { + s.vfsShmBuffer.refs-- + s.vfsShmBuffer = nil + return nil + } + + err := os.Remove(s.path) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return _IOERR_UNLOCK + } + delete(vfsShmBuffers, s.path) + s.vfsShmBuffer = nil + return nil +} + +func (s *vfsShm) shmOpen() _ErrorCode { + if s.vfsShmBuffer != nil { + return _OK + } + + vfsShmBuffersMtx.Lock() + defer vfsShmBuffersMtx.Unlock() + + // Find a shared buffer, increase the reference count. + if g, ok := vfsShmBuffers[s.path]; ok { + s.vfsShmBuffer = g + g.refs++ + return _OK + } + + // Create a directory on disk to ensure only this process + // uses this path to register a shared memory. + err := os.Mkdir(s.path, 0777) + if errors.Is(err, fs.ErrExist) { + return _BUSY + } + if err != nil { + return _IOERR_LOCK + } + + // Add the new shared buffer. + s.vfsShmBuffer = &vfsShmBuffer{} + vfsShmBuffers[s.path] = s.vfsShmBuffer + return _OK +} + +func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (uint32, _ErrorCode) { + if size != _WALINDEX_PGSZ { + return 0, _IOERR_SHMMAP + } + if s.mod == nil { + s.mod = mod + s.free = mod.ExportedFunction("sqlite3_free") + s.alloc = mod.ExportedFunction("sqlite3_malloc64") + } + if rc := s.shmOpen(); rc != _OK { + return 0, rc + } + + s.Lock() + defer s.Unlock() + defer s.shmAcquire() + + // Extend shared memory. + if int(id) >= len(s.shared) { + if !extend { + return 0, _OK + } + s.shared = append(s.shared, make([][_WALINDEX_PGSZ]byte, int(id)-len(s.shared)+1)...) + } + + // Allocate shadow memory. + if int(id) >= len(s.shadow) { + s.shadow = append(s.shadow, make([][_WALINDEX_PGSZ]byte, int(id)-len(s.shadow)+1)...) + s.shadow[0][4] = 1 // force invalidation + } + + // Allocate local memory. + for int(id) >= len(s.ptrs) { + s.stack[0] = uint64(size) + if err := s.alloc.CallWithStack(ctx, s.stack[:]); err != nil { + panic(err) + } + if s.stack[0] == 0 { + panic(util.OOMErr) + } + clear(util.View(s.mod, uint32(s.stack[0]), _WALINDEX_PGSZ)) + s.ptrs = append(s.ptrs, uint32(s.stack[0])) + } + + return s.ptrs[id], _OK +} + +func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { + s.Lock() + defer s.Unlock() + + switch { + case flags&_SHM_LOCK != 0: + defer s.shmAcquire() + case flags&_SHM_EXCLUSIVE != 0: + s.shmRelease() + } + + switch { + case flags&_SHM_UNLOCK != 0: + for i := offset; i < offset+n; i++ { + if s.lock[i] { + if s.vfsShmBuffer.lock[i] == 0 { + panic(util.AssertErr()) + } + if s.vfsShmBuffer.lock[i] <= 0 { + s.vfsShmBuffer.lock[i] = 0 + } else { + s.vfsShmBuffer.lock[i]-- + } + s.lock[i] = false + } + } + case flags&_SHM_SHARED != 0: + for i := offset; i < offset+n; i++ { + if s.lock[i] { + panic(util.AssertErr()) + } + if s.vfsShmBuffer.lock[i]+1 <= 0 { + return _BUSY + } + } + for i := offset; i < offset+n; i++ { + s.vfsShmBuffer.lock[i]++ + s.lock[i] = true + } + case flags&_SHM_EXCLUSIVE != 0: + for i := offset; i < offset+n; i++ { + if s.lock[i] { + panic(util.AssertErr()) + } + if s.vfsShmBuffer.lock[i] != 0 { + return _BUSY + } + } + for i := offset; i < offset+n; i++ { + s.vfsShmBuffer.lock[i] = -1 + s.lock[i] = true + } + default: + panic(util.AssertErr()) + } + + return _OK +} + +func (s *vfsShm) shmUnmap(delete bool) { + if s.vfsShmBuffer == nil { + return + } + defer s.Close() + + s.Lock() + s.shmRelease() + defer s.Unlock() + + for _, p := range s.ptrs { + s.stack[0] = uint64(p) + if err := s.free.CallWithStack(context.Background(), s.stack[:]); err != nil { + panic(err) + } + } + s.ptrs = nil + s.shadow = nil +} diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_ofd.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_ofd.go new file mode 100644 index 000000000..75c8fbcfb --- /dev/null +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_ofd.go @@ -0,0 +1,168 @@ +//go:build (linux || darwin) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_flock || sqlite3_dotlk || sqlite3_nosys) + +package vfs + +import ( + "context" + "io" + "os" + "sync" + "time" + + "github.com/tetratelabs/wazero/api" + "golang.org/x/sys/unix" + + "github.com/ncruces/go-sqlite3/internal/util" +) + +type vfsShm struct { + *os.File + path string + regions []*util.MappedRegion + readOnly bool + blocking bool + sync.Mutex +} + +var _ blockingSharedMemory = &vfsShm{} + +func (s *vfsShm) shmOpen() _ErrorCode { + if s.File == nil { + f, err := os.OpenFile(s.path, + unix.O_RDWR|unix.O_CREAT|unix.O_NOFOLLOW, 0666) + if err != nil { + f, err = os.OpenFile(s.path, + unix.O_RDONLY|unix.O_CREAT|unix.O_NOFOLLOW, 0666) + s.readOnly = true + } + if err != nil { + return _CANTOPEN + } + s.File = f + } + + // Dead man's switch. + if lock, rc := osTestLock(s.File, _SHM_DMS, 1); rc != _OK { + return _IOERR_LOCK + } else if lock == unix.F_WRLCK { + return _BUSY + } else if lock == unix.F_UNLCK { + if s.readOnly { + return _READONLY_CANTINIT + } + // Do not use a blocking lock here. + // If the lock cannot be obtained immediately, + // it means some other connection is truncating the file. + // And after it has done so, it will not release its lock, + // but only downgrade it to a shared lock. + // So no point in blocking here. + // The call below to obtain the shared DMS lock may use a blocking lock. + if rc := osWriteLock(s.File, _SHM_DMS, 1, 0); rc != _OK { + return rc + } + if err := s.Truncate(0); err != nil { + return _IOERR_SHMOPEN + } + } + return osReadLock(s.File, _SHM_DMS, 1, time.Millisecond) +} + +func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (uint32, _ErrorCode) { + // Ensure size is a multiple of the OS page size. + if int(size)&(unix.Getpagesize()-1) != 0 { + return 0, _IOERR_SHMMAP + } + + if rc := s.shmOpen(); rc != _OK { + return 0, rc + } + + // Check if file is big enough. + o, err := s.Seek(0, io.SeekEnd) + if err != nil { + return 0, _IOERR_SHMSIZE + } + if n := (int64(id) + 1) * int64(size); n > o { + if !extend { + return 0, _OK + } + if s.readOnly || osAllocate(s.File, n) != nil { + return 0, _IOERR_SHMSIZE + } + } + + r, err := util.MapRegion(ctx, mod, s.File, int64(id)*int64(size), size, s.readOnly) + if err != nil { + return 0, _IOERR_SHMMAP + } + s.regions = append(s.regions, r) + if s.readOnly { + return r.Ptr, _READONLY + } + return r.Ptr, _OK +} + +func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { + // Argument check. + if n <= 0 || offset < 0 || offset+n > _SHM_NLOCK { + panic(util.AssertErr()) + } + switch flags { + case + _SHM_LOCK | _SHM_SHARED, + _SHM_LOCK | _SHM_EXCLUSIVE, + _SHM_UNLOCK | _SHM_SHARED, + _SHM_UNLOCK | _SHM_EXCLUSIVE: + // + default: + panic(util.AssertErr()) + } + if n != 1 && flags&_SHM_EXCLUSIVE == 0 { + panic(util.AssertErr()) + } + + var timeout time.Duration + if s.blocking { + timeout = time.Millisecond + } + + switch { + case flags&_SHM_UNLOCK != 0: + return osUnlock(s.File, _SHM_BASE+int64(offset), int64(n)) + case flags&_SHM_SHARED != 0: + return osReadLock(s.File, _SHM_BASE+int64(offset), int64(n), timeout) + case flags&_SHM_EXCLUSIVE != 0: + return osWriteLock(s.File, _SHM_BASE+int64(offset), int64(n), timeout) + default: + panic(util.AssertErr()) + } +} + +func (s *vfsShm) shmUnmap(delete bool) { + if s.File == nil { + return + } + + // Unmap regions. + for _, r := range s.regions { + r.Unmap() + } + s.regions = nil + + // Close the file. + if delete { + os.Remove(s.path) + } + s.Close() + s.File = nil +} + +func (s *vfsShm) shmBarrier() { + s.Lock() + //lint:ignore SA2001 memory barrier. + s.Unlock() +} + +func (s *vfsShm) shmEnableBlocking(block bool) { + s.blocking = block +} diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_other.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_other.go index 12012033e..9602dd0cd 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_other.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_other.go @@ -1,4 +1,4 @@ -//go:build !(darwin || linux || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock) || !(386 || arm || amd64 || arm64 || riscv64 || ppc64le) || sqlite3_noshm || sqlite3_nosys +//go:build !(((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk) package vfs diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_windows.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_windows.go new file mode 100644 index 000000000..218d8e2c7 --- /dev/null +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_windows.go @@ -0,0 +1,182 @@ +//go:build (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_dotlk || sqlite3_nosys) + +package vfs + +import ( + "context" + "io" + "os" + "sync" + "time" + + "github.com/tetratelabs/wazero/api" + "golang.org/x/sys/windows" + + "github.com/ncruces/go-sqlite3/internal/util" + "github.com/ncruces/go-sqlite3/util/osutil" +) + +type vfsShm struct { + *os.File + mod api.Module + alloc api.Function + free api.Function + path string + regions []*util.MappedRegion + shared [][]byte + shadow [][_WALINDEX_PGSZ]byte + ptrs []uint32 + stack [1]uint64 + blocking bool + sync.Mutex +} + +var _ blockingSharedMemory = &vfsShm{} + +func (s *vfsShm) Close() error { + // Unmap regions. + for _, r := range s.regions { + r.Unmap() + } + s.regions = nil + + // Close the file. + return s.File.Close() +} + +func (s *vfsShm) shmOpen() _ErrorCode { + if s.File == nil { + f, err := osutil.OpenFile(s.path, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + return _CANTOPEN + } + s.File = f + } + + // Dead man's switch. + if rc := osWriteLock(s.File, _SHM_DMS, 1, 0); rc == _OK { + err := s.Truncate(0) + osUnlock(s.File, _SHM_DMS, 1) + if err != nil { + return _IOERR_SHMOPEN + } + } + return osReadLock(s.File, _SHM_DMS, 1, time.Millisecond) +} + +func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (uint32, _ErrorCode) { + // Ensure size is a multiple of the OS page size. + if size != _WALINDEX_PGSZ || (windows.Getpagesize()-1)&_WALINDEX_PGSZ != 0 { + return 0, _IOERR_SHMMAP + } + if s.mod == nil { + s.mod = mod + s.free = mod.ExportedFunction("sqlite3_free") + s.alloc = mod.ExportedFunction("sqlite3_malloc64") + } + if rc := s.shmOpen(); rc != _OK { + return 0, rc + } + + defer s.shmAcquire() + + // Check if file is big enough. + o, err := s.Seek(0, io.SeekEnd) + if err != nil { + return 0, _IOERR_SHMSIZE + } + if n := (int64(id) + 1) * int64(size); n > o { + if !extend { + return 0, _OK + } + if osAllocate(s.File, n) != nil { + return 0, _IOERR_SHMSIZE + } + } + + // Maps regions into memory. + for int(id) >= len(s.shared) { + r, err := util.MapRegion(ctx, mod, s.File, int64(id)*int64(size), size) + if err != nil { + return 0, _IOERR_SHMMAP + } + s.regions = append(s.regions, r) + s.shared = append(s.shared, r.Data) + } + + // Allocate shadow memory. + if int(id) >= len(s.shadow) { + s.shadow = append(s.shadow, make([][_WALINDEX_PGSZ]byte, int(id)-len(s.shadow)+1)...) + s.shadow[0][4] = 1 // force invalidation + } + + // Allocate local memory. + for int(id) >= len(s.ptrs) { + s.stack[0] = uint64(size) + if err := s.alloc.CallWithStack(ctx, s.stack[:]); err != nil { + panic(err) + } + if s.stack[0] == 0 { + panic(util.OOMErr) + } + clear(util.View(s.mod, uint32(s.stack[0]), _WALINDEX_PGSZ)) + s.ptrs = append(s.ptrs, uint32(s.stack[0])) + } + + return s.ptrs[id], _OK +} + +func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { + switch { + case flags&_SHM_LOCK != 0: + defer s.shmAcquire() + case flags&_SHM_EXCLUSIVE != 0: + s.shmRelease() + } + + var timeout time.Duration + if s.blocking { + timeout = time.Millisecond + } + + switch { + case flags&_SHM_UNLOCK != 0: + return osUnlock(s.File, _SHM_BASE+uint32(offset), uint32(n)) + case flags&_SHM_SHARED != 0: + return osReadLock(s.File, _SHM_BASE+uint32(offset), uint32(n), timeout) + case flags&_SHM_EXCLUSIVE != 0: + return osWriteLock(s.File, _SHM_BASE+uint32(offset), uint32(n), timeout) + default: + panic(util.AssertErr()) + } +} + +func (s *vfsShm) shmUnmap(delete bool) { + if s.File == nil { + return + } + + s.shmRelease() + + // Free local memory. + for _, p := range s.ptrs { + s.stack[0] = uint64(p) + if err := s.free.CallWithStack(context.Background(), s.stack[:]); err != nil { + panic(err) + } + } + s.ptrs = nil + s.shadow = nil + s.shared = nil + + // Close the file. + s.Close() + s.File = nil + if delete { + os.Remove(s.path) + } +} + +func (s *vfsShm) shmEnableBlocking(block bool) { + s.blocking = block +} diff --git a/vendor/github.com/puzpuzpuz/xsync/v3/.gitignore b/vendor/github.com/puzpuzpuz/xsync/v3/.gitignore new file mode 100644 index 000000000..66fd13c90 --- /dev/null +++ b/vendor/github.com/puzpuzpuz/xsync/v3/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/vendor/github.com/puzpuzpuz/xsync/v3/BENCHMARKS.md b/vendor/github.com/puzpuzpuz/xsync/v3/BENCHMARKS.md new file mode 100644 index 000000000..aaa72fa86 --- /dev/null +++ b/vendor/github.com/puzpuzpuz/xsync/v3/BENCHMARKS.md @@ -0,0 +1,133 @@ +# xsync benchmarks + +If you're interested in `MapOf` comparison with some of the popular concurrent hash maps written in Go, check [this](https://github.com/cornelk/hashmap/pull/70) and [this](https://github.com/alphadose/haxmap/pull/22) PRs. + +The below results were obtained for xsync v2.3.1 on a c6g.metal EC2 instance (64 CPU, 128GB RAM) running Linux and Go 1.19.3. I'd like to thank [@felixge](https://github.com/felixge) who kindly ran the benchmarks. + +The following commands were used to run the benchmarks: +```bash +$ go test -run='^$' -cpu=1,2,4,8,16,32,64 -bench . -count=30 -timeout=0 | tee bench.txt +$ benchstat bench.txt | tee benchstat.txt +``` + +The below sections contain some of the results. Refer to [this gist](https://gist.github.com/puzpuzpuz/e62e38e06feadecfdc823c0f941ece0b) for the complete output. + +Please note that `MapOf` got a number of optimizations since v2.3.1, so the current result is likely to be different. + +### Counter vs. atomic int64 + +``` +name time/op +Counter 27.3ns ± 1% +Counter-2 27.2ns ±11% +Counter-4 15.3ns ± 8% +Counter-8 7.43ns ± 7% +Counter-16 3.70ns ±10% +Counter-32 1.77ns ± 3% +Counter-64 0.96ns ±10% +AtomicInt64 7.60ns ± 0% +AtomicInt64-2 12.6ns ±13% +AtomicInt64-4 13.5ns ±14% +AtomicInt64-8 12.7ns ± 9% +AtomicInt64-16 12.8ns ± 8% +AtomicInt64-32 13.0ns ± 6% +AtomicInt64-64 12.9ns ± 7% +``` + +Here `time/op` stands for average time spent on operation. If you divide `10^9` by the result in nanoseconds per operation, you'd get the throughput in operations per second. Thus, the ideal theoretical scalability of a concurrent data structure implies that the reported `time/op` decreases proportionally with the increased number of CPU cores. On the contrary, if the measured time per operation increases when run on more cores, it means performance degradation. + +### MapOf vs. sync.Map + +1,000 `[int, int]` entries with a warm-up, 100% Loads: +``` +IntegerMapOf_WarmUp/reads=100% 24.0ns ± 0% +IntegerMapOf_WarmUp/reads=100%-2 12.0ns ± 0% +IntegerMapOf_WarmUp/reads=100%-4 6.02ns ± 0% +IntegerMapOf_WarmUp/reads=100%-8 3.01ns ± 0% +IntegerMapOf_WarmUp/reads=100%-16 1.50ns ± 0% +IntegerMapOf_WarmUp/reads=100%-32 0.75ns ± 0% +IntegerMapOf_WarmUp/reads=100%-64 0.38ns ± 0% +IntegerMapStandard_WarmUp/reads=100% 55.3ns ± 0% +IntegerMapStandard_WarmUp/reads=100%-2 27.6ns ± 0% +IntegerMapStandard_WarmUp/reads=100%-4 16.1ns ± 3% +IntegerMapStandard_WarmUp/reads=100%-8 8.35ns ± 7% +IntegerMapStandard_WarmUp/reads=100%-16 4.24ns ± 7% +IntegerMapStandard_WarmUp/reads=100%-32 2.18ns ± 6% +IntegerMapStandard_WarmUp/reads=100%-64 1.11ns ± 3% +``` + +1,000 `[int, int]` entries with a warm-up, 99% Loads, 0.5% Stores, 0.5% Deletes: +``` +IntegerMapOf_WarmUp/reads=99% 31.0ns ± 0% +IntegerMapOf_WarmUp/reads=99%-2 16.4ns ± 1% +IntegerMapOf_WarmUp/reads=99%-4 8.42ns ± 0% +IntegerMapOf_WarmUp/reads=99%-8 4.41ns ± 0% +IntegerMapOf_WarmUp/reads=99%-16 2.38ns ± 2% +IntegerMapOf_WarmUp/reads=99%-32 1.37ns ± 4% +IntegerMapOf_WarmUp/reads=99%-64 0.85ns ± 2% +IntegerMapStandard_WarmUp/reads=99% 121ns ± 1% +IntegerMapStandard_WarmUp/reads=99%-2 109ns ± 3% +IntegerMapStandard_WarmUp/reads=99%-4 115ns ± 4% +IntegerMapStandard_WarmUp/reads=99%-8 114ns ± 2% +IntegerMapStandard_WarmUp/reads=99%-16 105ns ± 2% +IntegerMapStandard_WarmUp/reads=99%-32 97.0ns ± 3% +IntegerMapStandard_WarmUp/reads=99%-64 98.0ns ± 2% +``` + +1,000 `[int, int]` entries with a warm-up, 75% Loads, 12.5% Stores, 12.5% Deletes: +``` +IntegerMapOf_WarmUp/reads=75%-reads 46.2ns ± 1% +IntegerMapOf_WarmUp/reads=75%-reads-2 36.7ns ± 2% +IntegerMapOf_WarmUp/reads=75%-reads-4 22.0ns ± 1% +IntegerMapOf_WarmUp/reads=75%-reads-8 12.8ns ± 2% +IntegerMapOf_WarmUp/reads=75%-reads-16 7.69ns ± 1% +IntegerMapOf_WarmUp/reads=75%-reads-32 5.16ns ± 1% +IntegerMapOf_WarmUp/reads=75%-reads-64 4.91ns ± 1% +IntegerMapStandard_WarmUp/reads=75%-reads 156ns ± 0% +IntegerMapStandard_WarmUp/reads=75%-reads-2 177ns ± 1% +IntegerMapStandard_WarmUp/reads=75%-reads-4 197ns ± 1% +IntegerMapStandard_WarmUp/reads=75%-reads-8 221ns ± 2% +IntegerMapStandard_WarmUp/reads=75%-reads-16 242ns ± 1% +IntegerMapStandard_WarmUp/reads=75%-reads-32 258ns ± 1% +IntegerMapStandard_WarmUp/reads=75%-reads-64 264ns ± 1% +``` + +### MPMCQueue vs. Go channels + +Concurrent producers and consumers (1:1), queue/channel size 1,000, some work done by both producers and consumers: +``` +QueueProdConsWork100 252ns ± 0% +QueueProdConsWork100-2 206ns ± 5% +QueueProdConsWork100-4 136ns ±12% +QueueProdConsWork100-8 110ns ± 6% +QueueProdConsWork100-16 108ns ± 2% +QueueProdConsWork100-32 102ns ± 2% +QueueProdConsWork100-64 101ns ± 0% +ChanProdConsWork100 283ns ± 0% +ChanProdConsWork100-2 406ns ±21% +ChanProdConsWork100-4 549ns ± 7% +ChanProdConsWork100-8 754ns ± 7% +ChanProdConsWork100-16 828ns ± 7% +ChanProdConsWork100-32 810ns ± 8% +ChanProdConsWork100-64 832ns ± 4% +``` + +### RBMutex vs. sync.RWMutex + +The writer locks on each 100,000 iteration with some work in the critical section for both readers and the writer: +``` +RBMutexWorkWrite100000 146ns ± 0% +RBMutexWorkWrite100000-2 73.3ns ± 0% +RBMutexWorkWrite100000-4 36.7ns ± 0% +RBMutexWorkWrite100000-8 18.6ns ± 0% +RBMutexWorkWrite100000-16 9.83ns ± 3% +RBMutexWorkWrite100000-32 5.53ns ± 0% +RBMutexWorkWrite100000-64 4.04ns ± 3% +RWMutexWorkWrite100000 121ns ± 0% +RWMutexWorkWrite100000-2 128ns ± 1% +RWMutexWorkWrite100000-4 124ns ± 2% +RWMutexWorkWrite100000-8 101ns ± 1% +RWMutexWorkWrite100000-16 92.9ns ± 1% +RWMutexWorkWrite100000-32 89.9ns ± 1% +RWMutexWorkWrite100000-64 88.4ns ± 1% +``` diff --git a/vendor/github.com/puzpuzpuz/xsync/v3/LICENSE b/vendor/github.com/puzpuzpuz/xsync/v3/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/vendor/github.com/puzpuzpuz/xsync/v3/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/puzpuzpuz/xsync/v3/README.md b/vendor/github.com/puzpuzpuz/xsync/v3/README.md new file mode 100644 index 000000000..6fe04976f --- /dev/null +++ b/vendor/github.com/puzpuzpuz/xsync/v3/README.md @@ -0,0 +1,166 @@ +[![GoDoc reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/puzpuzpuz/xsync/v3) +[![GoReport](https://goreportcard.com/badge/github.com/puzpuzpuz/xsync/v3)](https://goreportcard.com/report/github.com/puzpuzpuz/xsync/v3) +[![codecov](https://codecov.io/gh/puzpuzpuz/xsync/branch/main/graph/badge.svg)](https://codecov.io/gh/puzpuzpuz/xsync) + +# xsync + +Concurrent data structures for Go. Aims to provide more scalable alternatives for some of the data structures from the standard `sync` package, but not only. + +Covered with tests following the approach described [here](https://puzpuzpuz.dev/testing-concurrent-code-for-fun-and-profit). + +## Benchmarks + +Benchmark results may be found [here](BENCHMARKS.md). I'd like to thank [@felixge](https://github.com/felixge) who kindly ran the benchmarks on a beefy multicore machine. + +Also, a non-scientific, unfair benchmark comparing Java's [j.u.c.ConcurrentHashMap](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ConcurrentHashMap.html) and `xsync.MapOf` is available [here](https://puzpuzpuz.dev/concurrent-map-in-go-vs-java-yet-another-meaningless-benchmark). + +## Usage + +The latest xsync major version is v3, so `/v3` suffix should be used when importing the library: + +```go +import ( + "github.com/puzpuzpuz/xsync/v3" +) +``` + +*Note for pre-v3 users*: v1 and v2 support is discontinued, so please upgrade to v3. While the API has some breaking changes, the migration should be trivial. + +### Counter + +A `Counter` is a striped `int64` counter inspired by the `j.u.c.a.LongAdder` class from the Java standard library. + +```go +c := xsync.NewCounter() +// increment and decrement the counter +c.Inc() +c.Dec() +// read the current value +v := c.Value() +``` + +Works better in comparison with a single atomically updated `int64` counter in high contention scenarios. + +### Map + +A `Map` is like a concurrent hash table-based map. It follows the interface of `sync.Map` with a number of valuable extensions like `Compute` or `Size`. + +```go +m := xsync.NewMap() +m.Store("foo", "bar") +v, ok := m.Load("foo") +s := m.Size() +``` + +`Map` uses a modified version of Cache-Line Hash Table (CLHT) data structure: https://github.com/LPD-EPFL/CLHT + +CLHT is built around the idea of organizing the hash table in cache-line-sized buckets, so that on all modern CPUs update operations complete with minimal cache-line transfer. Also, `Get` operations are obstruction-free and involve no writes to shared memory, hence no mutexes or any other sort of locks. Due to this design, in all considered scenarios `Map` outperforms `sync.Map`. + +One important difference with `sync.Map` is that only string keys are supported. That's because Golang standard library does not expose the built-in hash functions for `interface{}` values. + +`MapOf[K, V]` is an implementation with parametrized key and value types. While it's still a CLHT-inspired hash map, `MapOf`'s design is quite different from `Map`. As a result, less GC pressure and fewer atomic operations on reads. + +```go +m := xsync.NewMapOf[string, string]() +m.Store("foo", "bar") +v, ok := m.Load("foo") +``` + +Apart from CLHT, `MapOf` borrows ideas from Java's `j.u.c.ConcurrentHashMap` (immutable K/V pair structs instead of atomic snapshots) and C++'s `absl::flat_hash_map` (meta memory and SWAR-based lookups). It also has more dense memory layout when compared with `Map`. Long story short, `MapOf` should be preferred over `Map` when possible. + +An important difference with `Map` is that `MapOf` supports arbitrary `comparable` key types: + +```go +type Point struct { + x int32 + y int32 +} +m := NewMapOf[Point, int]() +m.Store(Point{42, 42}, 42) +v, ok := m.Load(point{42, 42}) +``` + +Both maps use the built-in Golang's hash function which has DDOS protection. This means that each map instance gets its own seed number and the hash function uses that seed for hash code calculation. However, for smaller keys this hash function has some overhead. So, if you don't need DDOS protection, you may provide a custom hash function when creating a `MapOf`. For instance, Murmur3 finalizer does a decent job when it comes to integers: + +```go +m := NewMapOfWithHasher[int, int](func(i int, _ uint64) uint64 { + h := uint64(i) + h = (h ^ (h >> 33)) * 0xff51afd7ed558ccd + h = (h ^ (h >> 33)) * 0xc4ceb9fe1a85ec53 + return h ^ (h >> 33) +}) +``` + +When benchmarking concurrent maps, make sure to configure all of the competitors with the same hash function or, at least, take hash function performance into the consideration. + +### MPMCQueue + +A `MPMCQueue` is a bounded multi-producer multi-consumer concurrent queue. + +```go +q := xsync.NewMPMCQueue(1024) +// producer inserts an item into the queue +q.Enqueue("foo") +// optimistic insertion attempt; doesn't block +inserted := q.TryEnqueue("bar") +// consumer obtains an item from the queue +item := q.Dequeue() // interface{} pointing to a string +// optimistic obtain attempt; doesn't block +item, ok := q.TryDequeue() +``` + +`MPMCQueueOf[I]` is an implementation with parametrized item type. It is available for Go 1.19 or later. + +```go +q := xsync.NewMPMCQueueOf[string](1024) +q.Enqueue("foo") +item := q.Dequeue() // string +``` + +The queue is based on the algorithm from the [MPMCQueue](https://github.com/rigtorp/MPMCQueue) C++ library which in its turn references D.Vyukov's [MPMC queue](https://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue). According to the following [classification](https://www.1024cores.net/home/lock-free-algorithms/queues), the queue is array-based, fails on overflow, provides causal FIFO, has blocking producers and consumers. + +The idea of the algorithm is to allow parallelism for concurrent producers and consumers by introducing the notion of tickets, i.e. values of two counters, one per producers/consumers. An atomic increment of one of those counters is the only noticeable contention point in queue operations. The rest of the operation avoids contention on writes thanks to the turn-based read/write access for each of the queue items. + +In essence, `MPMCQueue` is a specialized queue for scenarios where there are multiple concurrent producers and consumers of a single queue running on a large multicore machine. + +To get the optimal performance, you may want to set the queue size to be large enough, say, an order of magnitude greater than the number of producers/consumers, to allow producers and consumers to progress with their queue operations in parallel most of the time. + +### RBMutex + +A `RBMutex` is a reader-biased reader/writer mutual exclusion lock. The lock can be held by many readers or a single writer. + +```go +mu := xsync.NewRBMutex() +// reader lock calls return a token +t := mu.RLock() +// the token must be later used to unlock the mutex +mu.RUnlock(t) +// writer locks are the same as in sync.RWMutex +mu.Lock() +mu.Unlock() +``` + +`RBMutex` is based on a modified version of BRAVO (Biased Locking for Reader-Writer Locks) algorithm: https://arxiv.org/pdf/1810.01553.pdf + +The idea of the algorithm is to build on top of an existing reader-writer mutex and introduce a fast path for readers. On the fast path, reader lock attempts are sharded over an internal array based on the reader identity (a token in the case of Golang). This means that readers do not contend over a single atomic counter like it's done in, say, `sync.RWMutex` allowing for better scalability in terms of cores. + +Hence, by the design `RBMutex` is a specialized mutex for scenarios, such as caches, where the vast majority of locks are acquired by readers and write lock acquire attempts are infrequent. In such scenarios, `RBMutex` should perform better than the `sync.RWMutex` on large multicore machines. + +`RBMutex` extends `sync.RWMutex` internally and uses it as the "reader bias disabled" fallback, so the same semantics apply. The only noticeable difference is in the reader tokens returned from the `RLock`/`RUnlock` methods. + +Apart from blocking methods, `RBMutex` also has methods for optimistic locking: +```go +mu := xsync.NewRBMutex() +if locked, t := mu.TryRLock(); locked { + // critical reader section... + mu.RUnlock(t) +} +if mu.TryLock() { + // critical writer section... + mu.Unlock() +} +``` + +## License + +Licensed under MIT. diff --git a/vendor/github.com/puzpuzpuz/xsync/v3/counter.go b/vendor/github.com/puzpuzpuz/xsync/v3/counter.go new file mode 100644 index 000000000..4d4dc87d2 --- /dev/null +++ b/vendor/github.com/puzpuzpuz/xsync/v3/counter.go @@ -0,0 +1,99 @@ +package xsync + +import ( + "sync" + "sync/atomic" +) + +// pool for P tokens +var ptokenPool sync.Pool + +// a P token is used to point at the current OS thread (P) +// on which the goroutine is run; exact identity of the thread, +// as well as P migration tolerance, is not important since +// it's used to as a best effort mechanism for assigning +// concurrent operations (goroutines) to different stripes of +// the counter +type ptoken struct { + idx uint32 + //lint:ignore U1000 prevents false sharing + pad [cacheLineSize - 4]byte +} + +// A Counter is a striped int64 counter. +// +// Should be preferred over a single atomically updated int64 +// counter in high contention scenarios. +// +// A Counter must not be copied after first use. +type Counter struct { + stripes []cstripe + mask uint32 +} + +type cstripe struct { + c int64 + //lint:ignore U1000 prevents false sharing + pad [cacheLineSize - 8]byte +} + +// NewCounter creates a new Counter instance. +func NewCounter() *Counter { + nstripes := nextPowOf2(parallelism()) + c := Counter{ + stripes: make([]cstripe, nstripes), + mask: nstripes - 1, + } + return &c +} + +// Inc increments the counter by 1. +func (c *Counter) Inc() { + c.Add(1) +} + +// Dec decrements the counter by 1. +func (c *Counter) Dec() { + c.Add(-1) +} + +// Add adds the delta to the counter. +func (c *Counter) Add(delta int64) { + t, ok := ptokenPool.Get().(*ptoken) + if !ok { + t = new(ptoken) + t.idx = runtime_fastrand() + } + for { + stripe := &c.stripes[t.idx&c.mask] + cnt := atomic.LoadInt64(&stripe.c) + if atomic.CompareAndSwapInt64(&stripe.c, cnt, cnt+delta) { + break + } + // Give a try with another randomly selected stripe. + t.idx = runtime_fastrand() + } + ptokenPool.Put(t) +} + +// Value returns the current counter value. +// The returned value may not include all of the latest operations in +// presence of concurrent modifications of the counter. +func (c *Counter) Value() int64 { + v := int64(0) + for i := 0; i < len(c.stripes); i++ { + stripe := &c.stripes[i] + v += atomic.LoadInt64(&stripe.c) + } + return v +} + +// Reset resets the counter to zero. +// This method should only be used when it is known that there are +// no concurrent modifications of the counter. +func (c *Counter) Reset() { + for i := 0; i < len(c.stripes); i++ { + stripe := &c.stripes[i] + atomic.StoreInt64(&stripe.c, 0) + } +} diff --git a/vendor/github.com/puzpuzpuz/xsync/v3/map.go b/vendor/github.com/puzpuzpuz/xsync/v3/map.go new file mode 100644 index 000000000..6c5b6ebd6 --- /dev/null +++ b/vendor/github.com/puzpuzpuz/xsync/v3/map.go @@ -0,0 +1,873 @@ +package xsync + +import ( + "fmt" + "math" + "runtime" + "strings" + "sync" + "sync/atomic" + "unsafe" +) + +type mapResizeHint int + +const ( + mapGrowHint mapResizeHint = 0 + mapShrinkHint mapResizeHint = 1 + mapClearHint mapResizeHint = 2 +) + +const ( + // number of Map entries per bucket; 3 entries lead to size of 64B + // (one cache line) on 64-bit machines + entriesPerMapBucket = 3 + // threshold fraction of table occupation to start a table shrinking + // when deleting the last entry in a bucket chain + mapShrinkFraction = 128 + // map load factor to trigger a table resize during insertion; + // a map holds up to mapLoadFactor*entriesPerMapBucket*mapTableLen + // key-value pairs (this is a soft limit) + mapLoadFactor = 0.75 + // minimal table size, i.e. number of buckets; thus, minimal map + // capacity can be calculated as entriesPerMapBucket*defaultMinMapTableLen + defaultMinMapTableLen = 32 + // minimum counter stripes to use + minMapCounterLen = 8 + // maximum counter stripes to use; stands for around 4KB of memory + maxMapCounterLen = 32 +) + +var ( + topHashMask = uint64((1<<20)-1) << 44 + topHashEntryMasks = [3]uint64{ + topHashMask, + topHashMask >> 20, + topHashMask >> 40, + } +) + +// Map is like a Go map[string]interface{} but is safe for concurrent +// use by multiple goroutines without additional locking or +// coordination. It follows the interface of sync.Map with +// a number of valuable extensions like Compute or Size. +// +// A Map must not be copied after first use. +// +// Map uses a modified version of Cache-Line Hash Table (CLHT) +// data structure: https://github.com/LPD-EPFL/CLHT +// +// CLHT is built around idea to organize the hash table in +// cache-line-sized buckets, so that on all modern CPUs update +// operations complete with at most one cache-line transfer. +// Also, Get operations involve no write to memory, as well as no +// mutexes or any other sort of locks. Due to this design, in all +// considered scenarios Map outperforms sync.Map. +// +// One important difference with sync.Map is that only string keys +// are supported. That's because Golang standard library does not +// expose the built-in hash functions for interface{} values. +type Map struct { + totalGrowths int64 + totalShrinks int64 + resizing int64 // resize in progress flag; updated atomically + resizeMu sync.Mutex // only used along with resizeCond + resizeCond sync.Cond // used to wake up resize waiters (concurrent modifications) + table unsafe.Pointer // *mapTable + minTableLen int + growOnly bool +} + +type mapTable struct { + buckets []bucketPadded + // striped counter for number of table entries; + // used to determine if a table shrinking is needed + // occupies min(buckets_memory/1024, 64KB) of memory + size []counterStripe + seed uint64 +} + +type counterStripe struct { + c int64 + //lint:ignore U1000 prevents false sharing + pad [cacheLineSize - 8]byte +} + +type bucketPadded struct { + //lint:ignore U1000 ensure each bucket takes two cache lines on both 32 and 64-bit archs + pad [cacheLineSize - unsafe.Sizeof(bucket{})]byte + bucket +} + +type bucket struct { + next unsafe.Pointer // *bucketPadded + keys [entriesPerMapBucket]unsafe.Pointer + values [entriesPerMapBucket]unsafe.Pointer + // topHashMutex is a 2-in-1 value. + // + // It contains packed top 20 bits (20 MSBs) of hash codes for keys + // stored in the bucket: + // | key 0's top hash | key 1's top hash | key 2's top hash | bitmap for keys | mutex | + // | 20 bits | 20 bits | 20 bits | 3 bits | 1 bit | + // + // The least significant bit is used for the mutex (TTAS spinlock). + topHashMutex uint64 +} + +type rangeEntry struct { + key unsafe.Pointer + value unsafe.Pointer +} + +// MapConfig defines configurable Map/MapOf options. +type MapConfig struct { + sizeHint int + growOnly bool +} + +// WithPresize configures new Map/MapOf instance with capacity enough +// to hold sizeHint entries. The capacity is treated as the minimal +// capacity meaning that the underlying hash table will never shrink +// to a smaller capacity. If sizeHint is zero or negative, the value +// is ignored. +func WithPresize(sizeHint int) func(*MapConfig) { + return func(c *MapConfig) { + c.sizeHint = sizeHint + } +} + +// WithGrowOnly configures new Map/MapOf instance to be grow-only. +// This means that the underlying hash table grows in capacity when +// new keys are added, but does not shrink when keys are deleted. +// The only exception to this rule is the Clear method which +// shrinks the hash table back to the initial capacity. +func WithGrowOnly() func(*MapConfig) { + return func(c *MapConfig) { + c.growOnly = true + } +} + +// NewMap creates a new Map instance configured with the given +// options. +func NewMap(options ...func(*MapConfig)) *Map { + c := &MapConfig{ + sizeHint: defaultMinMapTableLen * entriesPerMapBucket, + } + for _, o := range options { + o(c) + } + + m := &Map{} + m.resizeCond = *sync.NewCond(&m.resizeMu) + var table *mapTable + if c.sizeHint <= defaultMinMapTableLen*entriesPerMapBucket { + table = newMapTable(defaultMinMapTableLen) + } else { + tableLen := nextPowOf2(uint32((float64(c.sizeHint) / entriesPerMapBucket) / mapLoadFactor)) + table = newMapTable(int(tableLen)) + } + m.minTableLen = len(table.buckets) + m.growOnly = c.growOnly + atomic.StorePointer(&m.table, unsafe.Pointer(table)) + return m +} + +// NewMapPresized creates a new Map instance with capacity enough to hold +// sizeHint entries. The capacity is treated as the minimal capacity +// meaning that the underlying hash table will never shrink to +// a smaller capacity. If sizeHint is zero or negative, the value +// is ignored. +// +// Deprecated: use NewMap in combination with WithPresize. +func NewMapPresized(sizeHint int) *Map { + return NewMap(WithPresize(sizeHint)) +} + +func newMapTable(minTableLen int) *mapTable { + buckets := make([]bucketPadded, minTableLen) + counterLen := minTableLen >> 10 + if counterLen < minMapCounterLen { + counterLen = minMapCounterLen + } else if counterLen > maxMapCounterLen { + counterLen = maxMapCounterLen + } + counter := make([]counterStripe, counterLen) + t := &mapTable{ + buckets: buckets, + size: counter, + seed: makeSeed(), + } + return t +} + +// Load returns the value stored in the map for a key, or nil if no +// value is present. +// The ok result indicates whether value was found in the map. +func (m *Map) Load(key string) (value interface{}, ok bool) { + table := (*mapTable)(atomic.LoadPointer(&m.table)) + hash := hashString(key, table.seed) + bidx := uint64(len(table.buckets)-1) & hash + b := &table.buckets[bidx] + for { + topHashes := atomic.LoadUint64(&b.topHashMutex) + for i := 0; i < entriesPerMapBucket; i++ { + if !topHashMatch(hash, topHashes, i) { + continue + } + atomic_snapshot: + // Start atomic snapshot. + vp := atomic.LoadPointer(&b.values[i]) + kp := atomic.LoadPointer(&b.keys[i]) + if kp != nil && vp != nil { + if key == derefKey(kp) { + if uintptr(vp) == uintptr(atomic.LoadPointer(&b.values[i])) { + // Atomic snapshot succeeded. + return derefValue(vp), true + } + // Concurrent update/remove. Go for another spin. + goto atomic_snapshot + } + } + } + bptr := atomic.LoadPointer(&b.next) + if bptr == nil { + return + } + b = (*bucketPadded)(bptr) + } +} + +// Store sets the value for a key. +func (m *Map) Store(key string, value interface{}) { + m.doCompute( + key, + func(interface{}, bool) (interface{}, bool) { + return value, false + }, + false, + false, + ) +} + +// LoadOrStore returns the existing value for the key if present. +// Otherwise, it stores and returns the given value. +// The loaded result is true if the value was loaded, false if stored. +func (m *Map) LoadOrStore(key string, value interface{}) (actual interface{}, loaded bool) { + return m.doCompute( + key, + func(interface{}, bool) (interface{}, bool) { + return value, false + }, + true, + false, + ) +} + +// LoadAndStore returns the existing value for the key if present, +// while setting the new value for the key. +// It stores the new value and returns the existing one, if present. +// The loaded result is true if the existing value was loaded, +// false otherwise. +func (m *Map) LoadAndStore(key string, value interface{}) (actual interface{}, loaded bool) { + return m.doCompute( + key, + func(interface{}, bool) (interface{}, bool) { + return value, false + }, + false, + false, + ) +} + +// LoadOrCompute returns the existing value for the key if present. +// Otherwise, it computes the value using the provided function and +// returns the computed value. The loaded result is true if the value +// was loaded, false if stored. +// +// This call locks a hash table bucket while the compute function +// is executed. It means that modifications on other entries in +// the bucket will be blocked until the valueFn executes. Consider +// this when the function includes long-running operations. +func (m *Map) LoadOrCompute(key string, valueFn func() interface{}) (actual interface{}, loaded bool) { + return m.doCompute( + key, + func(interface{}, bool) (interface{}, bool) { + return valueFn(), false + }, + true, + false, + ) +} + +// Compute either sets the computed new value for the key or deletes +// the value for the key. When the delete result of the valueFn function +// is set to true, the value will be deleted, if it exists. When delete +// is set to false, the value is updated to the newValue. +// The ok result indicates whether value was computed and stored, thus, is +// present in the map. The actual result contains the new value in cases where +// the value was computed and stored. See the example for a few use cases. +// +// This call locks a hash table bucket while the compute function +// is executed. It means that modifications on other entries in +// the bucket will be blocked until the valueFn executes. Consider +// this when the function includes long-running operations. +func (m *Map) Compute( + key string, + valueFn func(oldValue interface{}, loaded bool) (newValue interface{}, delete bool), +) (actual interface{}, ok bool) { + return m.doCompute(key, valueFn, false, true) +} + +// LoadAndDelete deletes the value for a key, returning the previous +// value if any. The loaded result reports whether the key was +// present. +func (m *Map) LoadAndDelete(key string) (value interface{}, loaded bool) { + return m.doCompute( + key, + func(value interface{}, loaded bool) (interface{}, bool) { + return value, true + }, + false, + false, + ) +} + +// Delete deletes the value for a key. +func (m *Map) Delete(key string) { + m.doCompute( + key, + func(value interface{}, loaded bool) (interface{}, bool) { + return value, true + }, + false, + false, + ) +} + +func (m *Map) doCompute( + key string, + valueFn func(oldValue interface{}, loaded bool) (interface{}, bool), + loadIfExists, computeOnly bool, +) (interface{}, bool) { + // Read-only path. + if loadIfExists { + if v, ok := m.Load(key); ok { + return v, !computeOnly + } + } + // Write path. + for { + compute_attempt: + var ( + emptyb *bucketPadded + emptyidx int + hintNonEmpty int + ) + table := (*mapTable)(atomic.LoadPointer(&m.table)) + tableLen := len(table.buckets) + hash := hashString(key, table.seed) + bidx := uint64(len(table.buckets)-1) & hash + rootb := &table.buckets[bidx] + lockBucket(&rootb.topHashMutex) + // The following two checks must go in reverse to what's + // in the resize method. + if m.resizeInProgress() { + // Resize is in progress. Wait, then go for another attempt. + unlockBucket(&rootb.topHashMutex) + m.waitForResize() + goto compute_attempt + } + if m.newerTableExists(table) { + // Someone resized the table. Go for another attempt. + unlockBucket(&rootb.topHashMutex) + goto compute_attempt + } + b := rootb + for { + topHashes := atomic.LoadUint64(&b.topHashMutex) + for i := 0; i < entriesPerMapBucket; i++ { + if b.keys[i] == nil { + if emptyb == nil { + emptyb = b + emptyidx = i + } + continue + } + if !topHashMatch(hash, topHashes, i) { + hintNonEmpty++ + continue + } + if key == derefKey(b.keys[i]) { + vp := b.values[i] + if loadIfExists { + unlockBucket(&rootb.topHashMutex) + return derefValue(vp), !computeOnly + } + // In-place update/delete. + // We get a copy of the value via an interface{} on each call, + // thus the live value pointers are unique. Otherwise atomic + // snapshot won't be correct in case of multiple Store calls + // using the same value. + oldValue := derefValue(vp) + newValue, del := valueFn(oldValue, true) + if del { + // Deletion. + // First we update the value, then the key. + // This is important for atomic snapshot states. + atomic.StoreUint64(&b.topHashMutex, eraseTopHash(topHashes, i)) + atomic.StorePointer(&b.values[i], nil) + atomic.StorePointer(&b.keys[i], nil) + leftEmpty := false + if hintNonEmpty == 0 { + leftEmpty = isEmptyBucket(b) + } + unlockBucket(&rootb.topHashMutex) + table.addSize(bidx, -1) + // Might need to shrink the table. + if leftEmpty { + m.resize(table, mapShrinkHint) + } + return oldValue, !computeOnly + } + nvp := unsafe.Pointer(&newValue) + if assertionsEnabled && vp == nvp { + panic("non-unique value pointer") + } + atomic.StorePointer(&b.values[i], nvp) + unlockBucket(&rootb.topHashMutex) + if computeOnly { + // Compute expects the new value to be returned. + return newValue, true + } + // LoadAndStore expects the old value to be returned. + return oldValue, true + } + hintNonEmpty++ + } + if b.next == nil { + if emptyb != nil { + // Insertion into an existing bucket. + var zeroedV interface{} + newValue, del := valueFn(zeroedV, false) + if del { + unlockBucket(&rootb.topHashMutex) + return zeroedV, false + } + // First we update the value, then the key. + // This is important for atomic snapshot states. + topHashes = atomic.LoadUint64(&emptyb.topHashMutex) + atomic.StoreUint64(&emptyb.topHashMutex, storeTopHash(hash, topHashes, emptyidx)) + atomic.StorePointer(&emptyb.values[emptyidx], unsafe.Pointer(&newValue)) + atomic.StorePointer(&emptyb.keys[emptyidx], unsafe.Pointer(&key)) + unlockBucket(&rootb.topHashMutex) + table.addSize(bidx, 1) + return newValue, computeOnly + } + growThreshold := float64(tableLen) * entriesPerMapBucket * mapLoadFactor + if table.sumSize() > int64(growThreshold) { + // Need to grow the table. Then go for another attempt. + unlockBucket(&rootb.topHashMutex) + m.resize(table, mapGrowHint) + goto compute_attempt + } + // Insertion into a new bucket. + var zeroedV interface{} + newValue, del := valueFn(zeroedV, false) + if del { + unlockBucket(&rootb.topHashMutex) + return newValue, false + } + // Create and append a bucket. + newb := new(bucketPadded) + newb.keys[0] = unsafe.Pointer(&key) + newb.values[0] = unsafe.Pointer(&newValue) + newb.topHashMutex = storeTopHash(hash, newb.topHashMutex, 0) + atomic.StorePointer(&b.next, unsafe.Pointer(newb)) + unlockBucket(&rootb.topHashMutex) + table.addSize(bidx, 1) + return newValue, computeOnly + } + b = (*bucketPadded)(b.next) + } + } +} + +func (m *Map) newerTableExists(table *mapTable) bool { + curTablePtr := atomic.LoadPointer(&m.table) + return uintptr(curTablePtr) != uintptr(unsafe.Pointer(table)) +} + +func (m *Map) resizeInProgress() bool { + return atomic.LoadInt64(&m.resizing) == 1 +} + +func (m *Map) waitForResize() { + m.resizeMu.Lock() + for m.resizeInProgress() { + m.resizeCond.Wait() + } + m.resizeMu.Unlock() +} + +func (m *Map) resize(knownTable *mapTable, hint mapResizeHint) { + knownTableLen := len(knownTable.buckets) + // Fast path for shrink attempts. + if hint == mapShrinkHint { + if m.growOnly || + m.minTableLen == knownTableLen || + knownTable.sumSize() > int64((knownTableLen*entriesPerMapBucket)/mapShrinkFraction) { + return + } + } + // Slow path. + if !atomic.CompareAndSwapInt64(&m.resizing, 0, 1) { + // Someone else started resize. Wait for it to finish. + m.waitForResize() + return + } + var newTable *mapTable + table := (*mapTable)(atomic.LoadPointer(&m.table)) + tableLen := len(table.buckets) + switch hint { + case mapGrowHint: + // Grow the table with factor of 2. + atomic.AddInt64(&m.totalGrowths, 1) + newTable = newMapTable(tableLen << 1) + case mapShrinkHint: + shrinkThreshold := int64((tableLen * entriesPerMapBucket) / mapShrinkFraction) + if tableLen > m.minTableLen && table.sumSize() <= shrinkThreshold { + // Shrink the table with factor of 2. + atomic.AddInt64(&m.totalShrinks, 1) + newTable = newMapTable(tableLen >> 1) + } else { + // No need to shrink. Wake up all waiters and give up. + m.resizeMu.Lock() + atomic.StoreInt64(&m.resizing, 0) + m.resizeCond.Broadcast() + m.resizeMu.Unlock() + return + } + case mapClearHint: + newTable = newMapTable(m.minTableLen) + default: + panic(fmt.Sprintf("unexpected resize hint: %d", hint)) + } + // Copy the data only if we're not clearing the map. + if hint != mapClearHint { + for i := 0; i < tableLen; i++ { + copied := copyBucket(&table.buckets[i], newTable) + newTable.addSizePlain(uint64(i), copied) + } + } + // Publish the new table and wake up all waiters. + atomic.StorePointer(&m.table, unsafe.Pointer(newTable)) + m.resizeMu.Lock() + atomic.StoreInt64(&m.resizing, 0) + m.resizeCond.Broadcast() + m.resizeMu.Unlock() +} + +func copyBucket(b *bucketPadded, destTable *mapTable) (copied int) { + rootb := b + lockBucket(&rootb.topHashMutex) + for { + for i := 0; i < entriesPerMapBucket; i++ { + if b.keys[i] != nil { + k := derefKey(b.keys[i]) + hash := hashString(k, destTable.seed) + bidx := uint64(len(destTable.buckets)-1) & hash + destb := &destTable.buckets[bidx] + appendToBucket(hash, b.keys[i], b.values[i], destb) + copied++ + } + } + if b.next == nil { + unlockBucket(&rootb.topHashMutex) + return + } + b = (*bucketPadded)(b.next) + } +} + +func appendToBucket(hash uint64, keyPtr, valPtr unsafe.Pointer, b *bucketPadded) { + for { + for i := 0; i < entriesPerMapBucket; i++ { + if b.keys[i] == nil { + b.keys[i] = keyPtr + b.values[i] = valPtr + b.topHashMutex = storeTopHash(hash, b.topHashMutex, i) + return + } + } + if b.next == nil { + newb := new(bucketPadded) + newb.keys[0] = keyPtr + newb.values[0] = valPtr + newb.topHashMutex = storeTopHash(hash, newb.topHashMutex, 0) + b.next = unsafe.Pointer(newb) + return + } + b = (*bucketPadded)(b.next) + } +} + +func isEmptyBucket(rootb *bucketPadded) bool { + b := rootb + for { + for i := 0; i < entriesPerMapBucket; i++ { + if b.keys[i] != nil { + return false + } + } + if b.next == nil { + return true + } + b = (*bucketPadded)(b.next) + } +} + +// Range calls f sequentially for each key and value present in the +// map. If f returns false, range stops the iteration. +// +// Range does not necessarily correspond to any consistent snapshot +// of the Map's contents: no key will be visited more than once, but +// if the value for any key is stored or deleted concurrently, Range +// may reflect any mapping for that key from any point during the +// Range call. +// +// It is safe to modify the map while iterating it, including entry +// creation, modification and deletion. However, the concurrent +// modification rule apply, i.e. the changes may be not reflected +// in the subsequently iterated entries. +func (m *Map) Range(f func(key string, value interface{}) bool) { + var zeroEntry rangeEntry + // Pre-allocate array big enough to fit entries for most hash tables. + bentries := make([]rangeEntry, 0, 16*entriesPerMapBucket) + tablep := atomic.LoadPointer(&m.table) + table := *(*mapTable)(tablep) + for i := range table.buckets { + rootb := &table.buckets[i] + b := rootb + // Prevent concurrent modifications and copy all entries into + // the intermediate slice. + lockBucket(&rootb.topHashMutex) + for { + for i := 0; i < entriesPerMapBucket; i++ { + if b.keys[i] != nil { + bentries = append(bentries, rangeEntry{ + key: b.keys[i], + value: b.values[i], + }) + } + } + if b.next == nil { + unlockBucket(&rootb.topHashMutex) + break + } + b = (*bucketPadded)(b.next) + } + // Call the function for all copied entries. + for j := range bentries { + k := derefKey(bentries[j].key) + v := derefValue(bentries[j].value) + if !f(k, v) { + return + } + // Remove the reference to avoid preventing the copied + // entries from being GCed until this method finishes. + bentries[j] = zeroEntry + } + bentries = bentries[:0] + } +} + +// Clear deletes all keys and values currently stored in the map. +func (m *Map) Clear() { + table := (*mapTable)(atomic.LoadPointer(&m.table)) + m.resize(table, mapClearHint) +} + +// Size returns current size of the map. +func (m *Map) Size() int { + table := (*mapTable)(atomic.LoadPointer(&m.table)) + return int(table.sumSize()) +} + +func derefKey(keyPtr unsafe.Pointer) string { + return *(*string)(keyPtr) +} + +func derefValue(valuePtr unsafe.Pointer) interface{} { + return *(*interface{})(valuePtr) +} + +func lockBucket(mu *uint64) { + for { + var v uint64 + for { + v = atomic.LoadUint64(mu) + if v&1 != 1 { + break + } + runtime.Gosched() + } + if atomic.CompareAndSwapUint64(mu, v, v|1) { + return + } + runtime.Gosched() + } +} + +func unlockBucket(mu *uint64) { + v := atomic.LoadUint64(mu) + atomic.StoreUint64(mu, v&^1) +} + +func topHashMatch(hash, topHashes uint64, idx int) bool { + if topHashes&(1<<(idx+1)) == 0 { + // Entry is not present. + return false + } + hash = hash & topHashMask + topHashes = (topHashes & topHashEntryMasks[idx]) << (20 * idx) + return hash == topHashes +} + +func storeTopHash(hash, topHashes uint64, idx int) uint64 { + // Zero out top hash at idx. + topHashes = topHashes &^ topHashEntryMasks[idx] + // Chop top 20 MSBs of the given hash and position them at idx. + hash = (hash & topHashMask) >> (20 * idx) + // Store the MSBs. + topHashes = topHashes | hash + // Mark the entry as present. + return topHashes | (1 << (idx + 1)) +} + +func eraseTopHash(topHashes uint64, idx int) uint64 { + return topHashes &^ (1 << (idx + 1)) +} + +func (table *mapTable) addSize(bucketIdx uint64, delta int) { + cidx := uint64(len(table.size)-1) & bucketIdx + atomic.AddInt64(&table.size[cidx].c, int64(delta)) +} + +func (table *mapTable) addSizePlain(bucketIdx uint64, delta int) { + cidx := uint64(len(table.size)-1) & bucketIdx + table.size[cidx].c += int64(delta) +} + +func (table *mapTable) sumSize() int64 { + sum := int64(0) + for i := range table.size { + sum += atomic.LoadInt64(&table.size[i].c) + } + return sum +} + +// MapStats is Map/MapOf statistics. +// +// Warning: map statistics are intented to be used for diagnostic +// purposes, not for production code. This means that breaking changes +// may be introduced into this struct even between minor releases. +type MapStats struct { + // RootBuckets is the number of root buckets in the hash table. + // Each bucket holds a few entries. + RootBuckets int + // TotalBuckets is the total number of buckets in the hash table, + // including root and their chained buckets. Each bucket holds + // a few entries. + TotalBuckets int + // EmptyBuckets is the number of buckets that hold no entries. + EmptyBuckets int + // Capacity is the Map/MapOf capacity, i.e. the total number of + // entries that all buckets can physically hold. This number + // does not consider the load factor. + Capacity int + // Size is the exact number of entries stored in the map. + Size int + // Counter is the number of entries stored in the map according + // to the internal atomic counter. In case of concurrent map + // modifications this number may be different from Size. + Counter int + // CounterLen is the number of internal atomic counter stripes. + // This number may grow with the map capacity to improve + // multithreaded scalability. + CounterLen int + // MinEntries is the minimum number of entries per a chain of + // buckets, i.e. a root bucket and its chained buckets. + MinEntries int + // MinEntries is the maximum number of entries per a chain of + // buckets, i.e. a root bucket and its chained buckets. + MaxEntries int + // TotalGrowths is the number of times the hash table grew. + TotalGrowths int64 + // TotalGrowths is the number of times the hash table shrinked. + TotalShrinks int64 +} + +// ToString returns string representation of map stats. +func (s *MapStats) ToString() string { + var sb strings.Builder + sb.WriteString("MapStats{\n") + sb.WriteString(fmt.Sprintf("RootBuckets: %d\n", s.RootBuckets)) + sb.WriteString(fmt.Sprintf("TotalBuckets: %d\n", s.TotalBuckets)) + sb.WriteString(fmt.Sprintf("EmptyBuckets: %d\n", s.EmptyBuckets)) + sb.WriteString(fmt.Sprintf("Capacity: %d\n", s.Capacity)) + sb.WriteString(fmt.Sprintf("Size: %d\n", s.Size)) + sb.WriteString(fmt.Sprintf("Counter: %d\n", s.Counter)) + sb.WriteString(fmt.Sprintf("CounterLen: %d\n", s.CounterLen)) + sb.WriteString(fmt.Sprintf("MinEntries: %d\n", s.MinEntries)) + sb.WriteString(fmt.Sprintf("MaxEntries: %d\n", s.MaxEntries)) + sb.WriteString(fmt.Sprintf("TotalGrowths: %d\n", s.TotalGrowths)) + sb.WriteString(fmt.Sprintf("TotalShrinks: %d\n", s.TotalShrinks)) + sb.WriteString("}\n") + return sb.String() +} + +// Stats returns statistics for the Map. Just like other map +// methods, this one is thread-safe. Yet it's an O(N) operation, +// so it should be used only for diagnostics or debugging purposes. +func (m *Map) Stats() MapStats { + stats := MapStats{ + TotalGrowths: atomic.LoadInt64(&m.totalGrowths), + TotalShrinks: atomic.LoadInt64(&m.totalShrinks), + MinEntries: math.MaxInt32, + } + table := (*mapTable)(atomic.LoadPointer(&m.table)) + stats.RootBuckets = len(table.buckets) + stats.Counter = int(table.sumSize()) + stats.CounterLen = len(table.size) + for i := range table.buckets { + nentries := 0 + b := &table.buckets[i] + stats.TotalBuckets++ + for { + nentriesLocal := 0 + stats.Capacity += entriesPerMapBucket + for i := 0; i < entriesPerMapBucket; i++ { + if atomic.LoadPointer(&b.keys[i]) != nil { + stats.Size++ + nentriesLocal++ + } + } + nentries += nentriesLocal + if nentriesLocal == 0 { + stats.EmptyBuckets++ + } + if b.next == nil { + break + } + b = (*bucketPadded)(atomic.LoadPointer(&b.next)) + stats.TotalBuckets++ + } + if nentries < stats.MinEntries { + stats.MinEntries = nentries + } + if nentries > stats.MaxEntries { + stats.MaxEntries = nentries + } + } + return stats +} diff --git a/vendor/github.com/puzpuzpuz/xsync/v3/mapof.go b/vendor/github.com/puzpuzpuz/xsync/v3/mapof.go new file mode 100644 index 000000000..4c4ad0867 --- /dev/null +++ b/vendor/github.com/puzpuzpuz/xsync/v3/mapof.go @@ -0,0 +1,694 @@ +package xsync + +import ( + "fmt" + "math" + "sync" + "sync/atomic" + "unsafe" +) + +const ( + // number of MapOf entries per bucket; 5 entries lead to size of 64B + // (one cache line) on 64-bit machines + entriesPerMapOfBucket = 5 + defaultMeta uint64 = 0x8080808080808080 + metaMask uint64 = 0xffffffffff + defaultMetaMasked uint64 = defaultMeta & metaMask + emptyMetaSlot uint8 = 0x80 +) + +// MapOf is like a Go map[K]V but is safe for concurrent +// use by multiple goroutines without additional locking or +// coordination. It follows the interface of sync.Map with +// a number of valuable extensions like Compute or Size. +// +// A MapOf must not be copied after first use. +// +// MapOf uses a modified version of Cache-Line Hash Table (CLHT) +// data structure: https://github.com/LPD-EPFL/CLHT +// +// CLHT is built around idea to organize the hash table in +// cache-line-sized buckets, so that on all modern CPUs update +// operations complete with at most one cache-line transfer. +// Also, Get operations involve no write to memory, as well as no +// mutexes or any other sort of locks. Due to this design, in all +// considered scenarios MapOf outperforms sync.Map. +// +// MapOf also borrows ideas from Java's j.u.c.ConcurrentHashMap +// (immutable K/V pair structs instead of atomic snapshots) +// and C++'s absl::flat_hash_map (meta memory and SWAR-based +// lookups). +type MapOf[K comparable, V any] struct { + totalGrowths int64 + totalShrinks int64 + resizing int64 // resize in progress flag; updated atomically + resizeMu sync.Mutex // only used along with resizeCond + resizeCond sync.Cond // used to wake up resize waiters (concurrent modifications) + table unsafe.Pointer // *mapOfTable + hasher func(K, uint64) uint64 + minTableLen int + growOnly bool +} + +type mapOfTable[K comparable, V any] struct { + buckets []bucketOfPadded + // striped counter for number of table entries; + // used to determine if a table shrinking is needed + // occupies min(buckets_memory/1024, 64KB) of memory + size []counterStripe + seed uint64 +} + +// bucketOfPadded is a CL-sized map bucket holding up to +// entriesPerMapOfBucket entries. +type bucketOfPadded struct { + //lint:ignore U1000 ensure each bucket takes two cache lines on both 32 and 64-bit archs + pad [cacheLineSize - unsafe.Sizeof(bucketOf{})]byte + bucketOf +} + +type bucketOf struct { + meta uint64 + entries [entriesPerMapOfBucket]unsafe.Pointer // *entryOf + next unsafe.Pointer // *bucketOfPadded + mu sync.Mutex +} + +// entryOf is an immutable map entry. +type entryOf[K comparable, V any] struct { + key K + value V +} + +// NewMapOf creates a new MapOf instance configured with the given +// options. +func NewMapOf[K comparable, V any](options ...func(*MapConfig)) *MapOf[K, V] { + return NewMapOfWithHasher[K, V](defaultHasher[K](), options...) +} + +// NewMapOfWithHasher creates a new MapOf instance configured with +// the given hasher and options. The hash function is used instead +// of the built-in hash function configured when a map is created +// with the NewMapOf function. +func NewMapOfWithHasher[K comparable, V any]( + hasher func(K, uint64) uint64, + options ...func(*MapConfig), +) *MapOf[K, V] { + c := &MapConfig{ + sizeHint: defaultMinMapTableLen * entriesPerMapOfBucket, + } + for _, o := range options { + o(c) + } + + m := &MapOf[K, V]{} + m.resizeCond = *sync.NewCond(&m.resizeMu) + m.hasher = hasher + var table *mapOfTable[K, V] + if c.sizeHint <= defaultMinMapTableLen*entriesPerMapOfBucket { + table = newMapOfTable[K, V](defaultMinMapTableLen) + } else { + tableLen := nextPowOf2(uint32((float64(c.sizeHint) / entriesPerMapOfBucket) / mapLoadFactor)) + table = newMapOfTable[K, V](int(tableLen)) + } + m.minTableLen = len(table.buckets) + m.growOnly = c.growOnly + atomic.StorePointer(&m.table, unsafe.Pointer(table)) + return m +} + +// NewMapOfPresized creates a new MapOf instance with capacity enough +// to hold sizeHint entries. The capacity is treated as the minimal capacity +// meaning that the underlying hash table will never shrink to +// a smaller capacity. If sizeHint is zero or negative, the value +// is ignored. +// +// Deprecated: use NewMapOf in combination with WithPresize. +func NewMapOfPresized[K comparable, V any](sizeHint int) *MapOf[K, V] { + return NewMapOf[K, V](WithPresize(sizeHint)) +} + +func newMapOfTable[K comparable, V any](minTableLen int) *mapOfTable[K, V] { + buckets := make([]bucketOfPadded, minTableLen) + for i := range buckets { + buckets[i].meta = defaultMeta + } + counterLen := minTableLen >> 10 + if counterLen < minMapCounterLen { + counterLen = minMapCounterLen + } else if counterLen > maxMapCounterLen { + counterLen = maxMapCounterLen + } + counter := make([]counterStripe, counterLen) + t := &mapOfTable[K, V]{ + buckets: buckets, + size: counter, + seed: makeSeed(), + } + return t +} + +// Load returns the value stored in the map for a key, or zero value +// of type V if no value is present. +// The ok result indicates whether value was found in the map. +func (m *MapOf[K, V]) Load(key K) (value V, ok bool) { + table := (*mapOfTable[K, V])(atomic.LoadPointer(&m.table)) + hash := m.hasher(key, table.seed) + h1 := h1(hash) + h2w := broadcast(h2(hash)) + bidx := uint64(len(table.buckets)-1) & h1 + b := &table.buckets[bidx] + for { + metaw := atomic.LoadUint64(&b.meta) + markedw := markZeroBytes(metaw^h2w) & metaMask + for markedw != 0 { + idx := firstMarkedByteIndex(markedw) + eptr := atomic.LoadPointer(&b.entries[idx]) + if eptr != nil { + e := (*entryOf[K, V])(eptr) + if e.key == key { + return e.value, true + } + } + markedw &= markedw - 1 + } + bptr := atomic.LoadPointer(&b.next) + if bptr == nil { + return + } + b = (*bucketOfPadded)(bptr) + } +} + +// Store sets the value for a key. +func (m *MapOf[K, V]) Store(key K, value V) { + m.doCompute( + key, + func(V, bool) (V, bool) { + return value, false + }, + false, + false, + ) +} + +// LoadOrStore returns the existing value for the key if present. +// Otherwise, it stores and returns the given value. +// The loaded result is true if the value was loaded, false if stored. +func (m *MapOf[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { + return m.doCompute( + key, + func(V, bool) (V, bool) { + return value, false + }, + true, + false, + ) +} + +// LoadAndStore returns the existing value for the key if present, +// while setting the new value for the key. +// It stores the new value and returns the existing one, if present. +// The loaded result is true if the existing value was loaded, +// false otherwise. +func (m *MapOf[K, V]) LoadAndStore(key K, value V) (actual V, loaded bool) { + return m.doCompute( + key, + func(V, bool) (V, bool) { + return value, false + }, + false, + false, + ) +} + +// LoadOrCompute returns the existing value for the key if present. +// Otherwise, it computes the value using the provided function and +// returns the computed value. The loaded result is true if the value +// was loaded, false if stored. +// +// This call locks a hash table bucket while the compute function +// is executed. It means that modifications on other entries in +// the bucket will be blocked until the valueFn executes. Consider +// this when the function includes long-running operations. +func (m *MapOf[K, V]) LoadOrCompute(key K, valueFn func() V) (actual V, loaded bool) { + return m.doCompute( + key, + func(V, bool) (V, bool) { + return valueFn(), false + }, + true, + false, + ) +} + +// Compute either sets the computed new value for the key or deletes +// the value for the key. When the delete result of the valueFn function +// is set to true, the value will be deleted, if it exists. When delete +// is set to false, the value is updated to the newValue. +// The ok result indicates whether value was computed and stored, thus, is +// present in the map. The actual result contains the new value in cases where +// the value was computed and stored. See the example for a few use cases. +// +// This call locks a hash table bucket while the compute function +// is executed. It means that modifications on other entries in +// the bucket will be blocked until the valueFn executes. Consider +// this when the function includes long-running operations. +func (m *MapOf[K, V]) Compute( + key K, + valueFn func(oldValue V, loaded bool) (newValue V, delete bool), +) (actual V, ok bool) { + return m.doCompute(key, valueFn, false, true) +} + +// LoadAndDelete deletes the value for a key, returning the previous +// value if any. The loaded result reports whether the key was +// present. +func (m *MapOf[K, V]) LoadAndDelete(key K) (value V, loaded bool) { + return m.doCompute( + key, + func(value V, loaded bool) (V, bool) { + return value, true + }, + false, + false, + ) +} + +// Delete deletes the value for a key. +func (m *MapOf[K, V]) Delete(key K) { + m.doCompute( + key, + func(value V, loaded bool) (V, bool) { + return value, true + }, + false, + false, + ) +} + +func (m *MapOf[K, V]) doCompute( + key K, + valueFn func(oldValue V, loaded bool) (V, bool), + loadIfExists, computeOnly bool, +) (V, bool) { + // Read-only path. + if loadIfExists { + if v, ok := m.Load(key); ok { + return v, !computeOnly + } + } + // Write path. + for { + compute_attempt: + var ( + emptyb *bucketOfPadded + emptyidx int + ) + table := (*mapOfTable[K, V])(atomic.LoadPointer(&m.table)) + tableLen := len(table.buckets) + hash := m.hasher(key, table.seed) + h1 := h1(hash) + h2 := h2(hash) + h2w := broadcast(h2) + bidx := uint64(len(table.buckets)-1) & h1 + rootb := &table.buckets[bidx] + rootb.mu.Lock() + // The following two checks must go in reverse to what's + // in the resize method. + if m.resizeInProgress() { + // Resize is in progress. Wait, then go for another attempt. + rootb.mu.Unlock() + m.waitForResize() + goto compute_attempt + } + if m.newerTableExists(table) { + // Someone resized the table. Go for another attempt. + rootb.mu.Unlock() + goto compute_attempt + } + b := rootb + for { + metaw := b.meta + markedw := markZeroBytes(metaw^h2w) & metaMask + for markedw != 0 { + idx := firstMarkedByteIndex(markedw) + eptr := b.entries[idx] + if eptr != nil { + e := (*entryOf[K, V])(eptr) + if e.key == key { + if loadIfExists { + rootb.mu.Unlock() + return e.value, !computeOnly + } + // In-place update/delete. + // We get a copy of the value via an interface{} on each call, + // thus the live value pointers are unique. Otherwise atomic + // snapshot won't be correct in case of multiple Store calls + // using the same value. + oldv := e.value + newv, del := valueFn(oldv, true) + if del { + // Deletion. + // First we update the hash, then the entry. + newmetaw := setByte(metaw, emptyMetaSlot, idx) + atomic.StoreUint64(&b.meta, newmetaw) + atomic.StorePointer(&b.entries[idx], nil) + rootb.mu.Unlock() + table.addSize(bidx, -1) + // Might need to shrink the table if we left bucket empty. + if newmetaw == defaultMeta { + m.resize(table, mapShrinkHint) + } + return oldv, !computeOnly + } + newe := new(entryOf[K, V]) + newe.key = key + newe.value = newv + atomic.StorePointer(&b.entries[idx], unsafe.Pointer(newe)) + rootb.mu.Unlock() + if computeOnly { + // Compute expects the new value to be returned. + return newv, true + } + // LoadAndStore expects the old value to be returned. + return oldv, true + } + } + markedw &= markedw - 1 + } + if emptyb == nil { + // Search for empty entries (up to 5 per bucket). + emptyw := metaw & defaultMetaMasked + if emptyw != 0 { + idx := firstMarkedByteIndex(emptyw) + emptyb = b + emptyidx = idx + } + } + if b.next == nil { + if emptyb != nil { + // Insertion into an existing bucket. + var zeroedV V + newValue, del := valueFn(zeroedV, false) + if del { + rootb.mu.Unlock() + return zeroedV, false + } + newe := new(entryOf[K, V]) + newe.key = key + newe.value = newValue + // First we update meta, then the entry. + atomic.StoreUint64(&emptyb.meta, setByte(emptyb.meta, h2, emptyidx)) + atomic.StorePointer(&emptyb.entries[emptyidx], unsafe.Pointer(newe)) + rootb.mu.Unlock() + table.addSize(bidx, 1) + return newValue, computeOnly + } + growThreshold := float64(tableLen) * entriesPerMapOfBucket * mapLoadFactor + if table.sumSize() > int64(growThreshold) { + // Need to grow the table. Then go for another attempt. + rootb.mu.Unlock() + m.resize(table, mapGrowHint) + goto compute_attempt + } + // Insertion into a new bucket. + var zeroedV V + newValue, del := valueFn(zeroedV, false) + if del { + rootb.mu.Unlock() + return newValue, false + } + // Create and append a bucket. + newb := new(bucketOfPadded) + newb.meta = setByte(defaultMeta, h2, 0) + newe := new(entryOf[K, V]) + newe.key = key + newe.value = newValue + newb.entries[0] = unsafe.Pointer(newe) + atomic.StorePointer(&b.next, unsafe.Pointer(newb)) + rootb.mu.Unlock() + table.addSize(bidx, 1) + return newValue, computeOnly + } + b = (*bucketOfPadded)(b.next) + } + } +} + +func (m *MapOf[K, V]) newerTableExists(table *mapOfTable[K, V]) bool { + curTablePtr := atomic.LoadPointer(&m.table) + return uintptr(curTablePtr) != uintptr(unsafe.Pointer(table)) +} + +func (m *MapOf[K, V]) resizeInProgress() bool { + return atomic.LoadInt64(&m.resizing) == 1 +} + +func (m *MapOf[K, V]) waitForResize() { + m.resizeMu.Lock() + for m.resizeInProgress() { + m.resizeCond.Wait() + } + m.resizeMu.Unlock() +} + +func (m *MapOf[K, V]) resize(knownTable *mapOfTable[K, V], hint mapResizeHint) { + knownTableLen := len(knownTable.buckets) + // Fast path for shrink attempts. + if hint == mapShrinkHint { + if m.growOnly || + m.minTableLen == knownTableLen || + knownTable.sumSize() > int64((knownTableLen*entriesPerMapOfBucket)/mapShrinkFraction) { + return + } + } + // Slow path. + if !atomic.CompareAndSwapInt64(&m.resizing, 0, 1) { + // Someone else started resize. Wait for it to finish. + m.waitForResize() + return + } + var newTable *mapOfTable[K, V] + table := (*mapOfTable[K, V])(atomic.LoadPointer(&m.table)) + tableLen := len(table.buckets) + switch hint { + case mapGrowHint: + // Grow the table with factor of 2. + atomic.AddInt64(&m.totalGrowths, 1) + newTable = newMapOfTable[K, V](tableLen << 1) + case mapShrinkHint: + shrinkThreshold := int64((tableLen * entriesPerMapOfBucket) / mapShrinkFraction) + if tableLen > m.minTableLen && table.sumSize() <= shrinkThreshold { + // Shrink the table with factor of 2. + atomic.AddInt64(&m.totalShrinks, 1) + newTable = newMapOfTable[K, V](tableLen >> 1) + } else { + // No need to shrink. Wake up all waiters and give up. + m.resizeMu.Lock() + atomic.StoreInt64(&m.resizing, 0) + m.resizeCond.Broadcast() + m.resizeMu.Unlock() + return + } + case mapClearHint: + newTable = newMapOfTable[K, V](m.minTableLen) + default: + panic(fmt.Sprintf("unexpected resize hint: %d", hint)) + } + // Copy the data only if we're not clearing the map. + if hint != mapClearHint { + for i := 0; i < tableLen; i++ { + copied := copyBucketOf(&table.buckets[i], newTable, m.hasher) + newTable.addSizePlain(uint64(i), copied) + } + } + // Publish the new table and wake up all waiters. + atomic.StorePointer(&m.table, unsafe.Pointer(newTable)) + m.resizeMu.Lock() + atomic.StoreInt64(&m.resizing, 0) + m.resizeCond.Broadcast() + m.resizeMu.Unlock() +} + +func copyBucketOf[K comparable, V any]( + b *bucketOfPadded, + destTable *mapOfTable[K, V], + hasher func(K, uint64) uint64, +) (copied int) { + rootb := b + rootb.mu.Lock() + for { + for i := 0; i < entriesPerMapOfBucket; i++ { + if b.entries[i] != nil { + e := (*entryOf[K, V])(b.entries[i]) + hash := hasher(e.key, destTable.seed) + bidx := uint64(len(destTable.buckets)-1) & h1(hash) + destb := &destTable.buckets[bidx] + appendToBucketOf(h2(hash), b.entries[i], destb) + copied++ + } + } + if b.next == nil { + rootb.mu.Unlock() + return + } + b = (*bucketOfPadded)(b.next) + } +} + +// Range calls f sequentially for each key and value present in the +// map. If f returns false, range stops the iteration. +// +// Range does not necessarily correspond to any consistent snapshot +// of the Map's contents: no key will be visited more than once, but +// if the value for any key is stored or deleted concurrently, Range +// may reflect any mapping for that key from any point during the +// Range call. +// +// It is safe to modify the map while iterating it, including entry +// creation, modification and deletion. However, the concurrent +// modification rule apply, i.e. the changes may be not reflected +// in the subsequently iterated entries. +func (m *MapOf[K, V]) Range(f func(key K, value V) bool) { + var zeroPtr unsafe.Pointer + // Pre-allocate array big enough to fit entries for most hash tables. + bentries := make([]unsafe.Pointer, 0, 16*entriesPerMapOfBucket) + tablep := atomic.LoadPointer(&m.table) + table := *(*mapOfTable[K, V])(tablep) + for i := range table.buckets { + rootb := &table.buckets[i] + b := rootb + // Prevent concurrent modifications and copy all entries into + // the intermediate slice. + rootb.mu.Lock() + for { + for i := 0; i < entriesPerMapOfBucket; i++ { + if b.entries[i] != nil { + bentries = append(bentries, b.entries[i]) + } + } + if b.next == nil { + rootb.mu.Unlock() + break + } + b = (*bucketOfPadded)(b.next) + } + // Call the function for all copied entries. + for j := range bentries { + entry := (*entryOf[K, V])(bentries[j]) + if !f(entry.key, entry.value) { + return + } + // Remove the reference to avoid preventing the copied + // entries from being GCed until this method finishes. + bentries[j] = zeroPtr + } + bentries = bentries[:0] + } +} + +// Clear deletes all keys and values currently stored in the map. +func (m *MapOf[K, V]) Clear() { + table := (*mapOfTable[K, V])(atomic.LoadPointer(&m.table)) + m.resize(table, mapClearHint) +} + +// Size returns current size of the map. +func (m *MapOf[K, V]) Size() int { + table := (*mapOfTable[K, V])(atomic.LoadPointer(&m.table)) + return int(table.sumSize()) +} + +func appendToBucketOf(h2 uint8, entryPtr unsafe.Pointer, b *bucketOfPadded) { + for { + for i := 0; i < entriesPerMapOfBucket; i++ { + if b.entries[i] == nil { + b.meta = setByte(b.meta, h2, i) + b.entries[i] = entryPtr + return + } + } + if b.next == nil { + newb := new(bucketOfPadded) + newb.meta = setByte(defaultMeta, h2, 0) + newb.entries[0] = entryPtr + b.next = unsafe.Pointer(newb) + return + } + b = (*bucketOfPadded)(b.next) + } +} + +func (table *mapOfTable[K, V]) addSize(bucketIdx uint64, delta int) { + cidx := uint64(len(table.size)-1) & bucketIdx + atomic.AddInt64(&table.size[cidx].c, int64(delta)) +} + +func (table *mapOfTable[K, V]) addSizePlain(bucketIdx uint64, delta int) { + cidx := uint64(len(table.size)-1) & bucketIdx + table.size[cidx].c += int64(delta) +} + +func (table *mapOfTable[K, V]) sumSize() int64 { + sum := int64(0) + for i := range table.size { + sum += atomic.LoadInt64(&table.size[i].c) + } + return sum +} + +func h1(h uint64) uint64 { + return h >> 7 +} + +func h2(h uint64) uint8 { + return uint8(h & 0x7f) +} + +// Stats returns statistics for the MapOf. Just like other map +// methods, this one is thread-safe. Yet it's an O(N) operation, +// so it should be used only for diagnostics or debugging purposes. +func (m *MapOf[K, V]) Stats() MapStats { + stats := MapStats{ + TotalGrowths: atomic.LoadInt64(&m.totalGrowths), + TotalShrinks: atomic.LoadInt64(&m.totalShrinks), + MinEntries: math.MaxInt32, + } + table := (*mapOfTable[K, V])(atomic.LoadPointer(&m.table)) + stats.RootBuckets = len(table.buckets) + stats.Counter = int(table.sumSize()) + stats.CounterLen = len(table.size) + for i := range table.buckets { + nentries := 0 + b := &table.buckets[i] + stats.TotalBuckets++ + for { + nentriesLocal := 0 + stats.Capacity += entriesPerMapOfBucket + for i := 0; i < entriesPerMapOfBucket; i++ { + if atomic.LoadPointer(&b.entries[i]) != nil { + stats.Size++ + nentriesLocal++ + } + } + nentries += nentriesLocal + if nentriesLocal == 0 { + stats.EmptyBuckets++ + } + if b.next == nil { + break + } + b = (*bucketOfPadded)(atomic.LoadPointer(&b.next)) + stats.TotalBuckets++ + } + if nentries < stats.MinEntries { + stats.MinEntries = nentries + } + if nentries > stats.MaxEntries { + stats.MaxEntries = nentries + } + } + return stats +} diff --git a/vendor/github.com/puzpuzpuz/xsync/v3/mpmcqueue.go b/vendor/github.com/puzpuzpuz/xsync/v3/mpmcqueue.go new file mode 100644 index 000000000..96584e698 --- /dev/null +++ b/vendor/github.com/puzpuzpuz/xsync/v3/mpmcqueue.go @@ -0,0 +1,137 @@ +package xsync + +import ( + "runtime" + "sync/atomic" + "unsafe" +) + +// A MPMCQueue is a bounded multi-producer multi-consumer concurrent +// queue. +// +// MPMCQueue instances must be created with NewMPMCQueue function. +// A MPMCQueue must not be copied after first use. +// +// Based on the data structure from the following C++ library: +// https://github.com/rigtorp/MPMCQueue +type MPMCQueue struct { + cap uint64 + head uint64 + //lint:ignore U1000 prevents false sharing + hpad [cacheLineSize - 8]byte + tail uint64 + //lint:ignore U1000 prevents false sharing + tpad [cacheLineSize - 8]byte + slots []slotPadded +} + +type slotPadded struct { + slot + //lint:ignore U1000 prevents false sharing + pad [cacheLineSize - unsafe.Sizeof(slot{})]byte +} + +type slot struct { + turn uint64 + item interface{} +} + +// NewMPMCQueue creates a new MPMCQueue instance with the given +// capacity. +func NewMPMCQueue(capacity int) *MPMCQueue { + if capacity < 1 { + panic("capacity must be positive number") + } + return &MPMCQueue{ + cap: uint64(capacity), + slots: make([]slotPadded, capacity), + } +} + +// Enqueue inserts the given item into the queue. +// Blocks, if the queue is full. +func (q *MPMCQueue) Enqueue(item interface{}) { + head := atomic.AddUint64(&q.head, 1) - 1 + slot := &q.slots[q.idx(head)] + turn := q.turn(head) * 2 + for atomic.LoadUint64(&slot.turn) != turn { + runtime.Gosched() + } + slot.item = item + atomic.StoreUint64(&slot.turn, turn+1) +} + +// Dequeue retrieves and removes the item from the head of the queue. +// Blocks, if the queue is empty. +func (q *MPMCQueue) Dequeue() interface{} { + tail := atomic.AddUint64(&q.tail, 1) - 1 + slot := &q.slots[q.idx(tail)] + turn := q.turn(tail)*2 + 1 + for atomic.LoadUint64(&slot.turn) != turn { + runtime.Gosched() + } + item := slot.item + slot.item = nil + atomic.StoreUint64(&slot.turn, turn+1) + return item +} + +// TryEnqueue inserts the given item into the queue. Does not block +// and returns immediately. The result indicates that the queue isn't +// full and the item was inserted. +func (q *MPMCQueue) TryEnqueue(item interface{}) bool { + head := atomic.LoadUint64(&q.head) + for { + slot := &q.slots[q.idx(head)] + turn := q.turn(head) * 2 + if atomic.LoadUint64(&slot.turn) == turn { + if atomic.CompareAndSwapUint64(&q.head, head, head+1) { + slot.item = item + atomic.StoreUint64(&slot.turn, turn+1) + return true + } + } else { + prevHead := head + head = atomic.LoadUint64(&q.head) + if head == prevHead { + return false + } + } + runtime.Gosched() + } +} + +// TryDequeue retrieves and removes the item from the head of the +// queue. Does not block and returns immediately. The ok result +// indicates that the queue isn't empty and an item was retrieved. +func (q *MPMCQueue) TryDequeue() (item interface{}, ok bool) { + tail := atomic.LoadUint64(&q.tail) + for { + slot := &q.slots[q.idx(tail)] + turn := q.turn(tail)*2 + 1 + if atomic.LoadUint64(&slot.turn) == turn { + if atomic.CompareAndSwapUint64(&q.tail, tail, tail+1) { + item = slot.item + ok = true + slot.item = nil + atomic.StoreUint64(&slot.turn, turn+1) + return + } + } else { + prevTail := tail + tail = atomic.LoadUint64(&q.tail) + if tail == prevTail { + return + } + } + runtime.Gosched() + } +} + +func (q *MPMCQueue) idx(i uint64) uint64 { + return i % q.cap +} + +func (q *MPMCQueue) turn(i uint64) uint64 { + return i / q.cap +} diff --git a/vendor/github.com/puzpuzpuz/xsync/v3/mpmcqueueof.go b/vendor/github.com/puzpuzpuz/xsync/v3/mpmcqueueof.go new file mode 100644 index 000000000..38a8fa3c6 --- /dev/null +++ b/vendor/github.com/puzpuzpuz/xsync/v3/mpmcqueueof.go @@ -0,0 +1,150 @@ +//go:build go1.19 +// +build go1.19 + +package xsync + +import ( + "runtime" + "sync/atomic" + "unsafe" +) + +// A MPMCQueueOf is a bounded multi-producer multi-consumer concurrent +// queue. It's a generic version of MPMCQueue. +// +// MPMCQueue instances must be created with NewMPMCQueueOf function. +// A MPMCQueueOf must not be copied after first use. +// +// Based on the data structure from the following C++ library: +// https://github.com/rigtorp/MPMCQueue +type MPMCQueueOf[I any] struct { + cap uint64 + head uint64 + //lint:ignore U1000 prevents false sharing + hpad [cacheLineSize - 8]byte + tail uint64 + //lint:ignore U1000 prevents false sharing + tpad [cacheLineSize - 8]byte + slots []slotOfPadded[I] +} + +type slotOfPadded[I any] struct { + slotOf[I] + // Unfortunately, proper padding like the below one: + // + // pad [cacheLineSize - (unsafe.Sizeof(slotOf[I]{}) % cacheLineSize)]byte + // + // won't compile, so here we add a best-effort padding for items up to + // 56 bytes size. + //lint:ignore U1000 prevents false sharing + pad [cacheLineSize - unsafe.Sizeof(atomic.Uint64{})]byte +} + +type slotOf[I any] struct { + // atomic.Uint64 is used here to get proper 8 byte alignment on + // 32-bit archs. + turn atomic.Uint64 + item I +} + +// NewMPMCQueueOf creates a new MPMCQueueOf instance with the given +// capacity. +func NewMPMCQueueOf[I any](capacity int) *MPMCQueueOf[I] { + if capacity < 1 { + panic("capacity must be positive number") + } + return &MPMCQueueOf[I]{ + cap: uint64(capacity), + slots: make([]slotOfPadded[I], capacity), + } +} + +// Enqueue inserts the given item into the queue. +// Blocks, if the queue is full. +func (q *MPMCQueueOf[I]) Enqueue(item I) { + head := atomic.AddUint64(&q.head, 1) - 1 + slot := &q.slots[q.idx(head)] + turn := q.turn(head) * 2 + for slot.turn.Load() != turn { + runtime.Gosched() + } + slot.item = item + slot.turn.Store(turn + 1) +} + +// Dequeue retrieves and removes the item from the head of the queue. +// Blocks, if the queue is empty. +func (q *MPMCQueueOf[I]) Dequeue() I { + var zeroedI I + tail := atomic.AddUint64(&q.tail, 1) - 1 + slot := &q.slots[q.idx(tail)] + turn := q.turn(tail)*2 + 1 + for slot.turn.Load() != turn { + runtime.Gosched() + } + item := slot.item + slot.item = zeroedI + slot.turn.Store(turn + 1) + return item +} + +// TryEnqueue inserts the given item into the queue. Does not block +// and returns immediately. The result indicates that the queue isn't +// full and the item was inserted. +func (q *MPMCQueueOf[I]) TryEnqueue(item I) bool { + head := atomic.LoadUint64(&q.head) + for { + slot := &q.slots[q.idx(head)] + turn := q.turn(head) * 2 + if slot.turn.Load() == turn { + if atomic.CompareAndSwapUint64(&q.head, head, head+1) { + slot.item = item + slot.turn.Store(turn + 1) + return true + } + } else { + prevHead := head + head = atomic.LoadUint64(&q.head) + if head == prevHead { + return false + } + } + runtime.Gosched() + } +} + +// TryDequeue retrieves and removes the item from the head of the +// queue. Does not block and returns immediately. The ok result +// indicates that the queue isn't empty and an item was retrieved. +func (q *MPMCQueueOf[I]) TryDequeue() (item I, ok bool) { + tail := atomic.LoadUint64(&q.tail) + for { + slot := &q.slots[q.idx(tail)] + turn := q.turn(tail)*2 + 1 + if slot.turn.Load() == turn { + if atomic.CompareAndSwapUint64(&q.tail, tail, tail+1) { + var zeroedI I + item = slot.item + ok = true + slot.item = zeroedI + slot.turn.Store(turn + 1) + return + } + } else { + prevTail := tail + tail = atomic.LoadUint64(&q.tail) + if tail == prevTail { + return + } + } + runtime.Gosched() + } +} + +func (q *MPMCQueueOf[I]) idx(i uint64) uint64 { + return i % q.cap +} + +func (q *MPMCQueueOf[I]) turn(i uint64) uint64 { + return i / q.cap +} diff --git a/vendor/github.com/puzpuzpuz/xsync/v3/rbmutex.go b/vendor/github.com/puzpuzpuz/xsync/v3/rbmutex.go new file mode 100644 index 000000000..4cbd9c41d --- /dev/null +++ b/vendor/github.com/puzpuzpuz/xsync/v3/rbmutex.go @@ -0,0 +1,188 @@ +package xsync + +import ( + "runtime" + "sync" + "sync/atomic" + "time" +) + +// slow-down guard +const nslowdown = 7 + +// pool for reader tokens +var rtokenPool sync.Pool + +// RToken is a reader lock token. +type RToken struct { + slot uint32 + //lint:ignore U1000 prevents false sharing + pad [cacheLineSize - 4]byte +} + +// A RBMutex is a reader biased reader/writer mutual exclusion lock. +// The lock can be held by an many readers or a single writer. +// The zero value for a RBMutex is an unlocked mutex. +// +// A RBMutex must not be copied after first use. +// +// RBMutex is based on a modified version of BRAVO +// (Biased Locking for Reader-Writer Locks) algorithm: +// https://arxiv.org/pdf/1810.01553.pdf +// +// RBMutex is a specialized mutex for scenarios, such as caches, +// where the vast majority of locks are acquired by readers and write +// lock acquire attempts are infrequent. In such scenarios, RBMutex +// performs better than sync.RWMutex on large multicore machines. +// +// RBMutex extends sync.RWMutex internally and uses it as the "reader +// bias disabled" fallback, so the same semantics apply. The only +// noticeable difference is in reader tokens returned from the +// RLock/RUnlock methods. +type RBMutex struct { + rslots []rslot + rmask uint32 + rbias int32 + inhibitUntil time.Time + rw sync.RWMutex +} + +type rslot struct { + mu int32 + //lint:ignore U1000 prevents false sharing + pad [cacheLineSize - 4]byte +} + +// NewRBMutex creates a new RBMutex instance. +func NewRBMutex() *RBMutex { + nslots := nextPowOf2(parallelism()) + mu := RBMutex{ + rslots: make([]rslot, nslots), + rmask: nslots - 1, + rbias: 1, + } + return &mu +} + +// TryRLock tries to lock m for reading without blocking. +// When TryRLock succeeds, it returns true and a reader token. +// In case of a failure, a false is returned. +func (mu *RBMutex) TryRLock() (bool, *RToken) { + if t := mu.fastRlock(); t != nil { + return true, t + } + // Optimistic slow path. + if mu.rw.TryRLock() { + if atomic.LoadInt32(&mu.rbias) == 0 && time.Now().After(mu.inhibitUntil) { + atomic.StoreInt32(&mu.rbias, 1) + } + return true, nil + } + return false, nil +} + +// RLock locks m for reading and returns a reader token. The +// token must be used in the later RUnlock call. +// +// Should not be used for recursive read locking; a blocked Lock +// call excludes new readers from acquiring the lock. +func (mu *RBMutex) RLock() *RToken { + if t := mu.fastRlock(); t != nil { + return t + } + // Slow path. + mu.rw.RLock() + if atomic.LoadInt32(&mu.rbias) == 0 && time.Now().After(mu.inhibitUntil) { + atomic.StoreInt32(&mu.rbias, 1) + } + return nil +} + +func (mu *RBMutex) fastRlock() *RToken { + if atomic.LoadInt32(&mu.rbias) == 1 { + t, ok := rtokenPool.Get().(*RToken) + if !ok { + t = new(RToken) + t.slot = runtime_fastrand() + } + // Try all available slots to distribute reader threads to slots. + for i := 0; i < len(mu.rslots); i++ { + slot := t.slot + uint32(i) + rslot := &mu.rslots[slot&mu.rmask] + rslotmu := atomic.LoadInt32(&rslot.mu) + if atomic.CompareAndSwapInt32(&rslot.mu, rslotmu, rslotmu+1) { + if atomic.LoadInt32(&mu.rbias) == 1 { + // Hot path succeeded. + t.slot = slot + return t + } + // The mutex is no longer reader biased. Roll back. + atomic.AddInt32(&rslot.mu, -1) + rtokenPool.Put(t) + return nil + } + // Contention detected. Give a try with the next slot. + } + } + return nil +} + +// RUnlock undoes a single RLock call. A reader token obtained from +// the RLock call must be provided. RUnlock does not affect other +// simultaneous readers. A panic is raised if m is not locked for +// reading on entry to RUnlock. +func (mu *RBMutex) RUnlock(t *RToken) { + if t == nil { + mu.rw.RUnlock() + return + } + if atomic.AddInt32(&mu.rslots[t.slot&mu.rmask].mu, -1) < 0 { + panic("invalid reader state detected") + } + rtokenPool.Put(t) +} + +// TryLock tries to lock m for writing without blocking. +func (mu *RBMutex) TryLock() bool { + if mu.rw.TryLock() { + if atomic.LoadInt32(&mu.rbias) == 1 { + atomic.StoreInt32(&mu.rbias, 0) + for i := 0; i < len(mu.rslots); i++ { + if atomic.LoadInt32(&mu.rslots[i].mu) > 0 { + // There is a reader. Roll back. + atomic.StoreInt32(&mu.rbias, 1) + mu.rw.Unlock() + return false + } + } + } + return true + } + return false +} + +// Lock locks m for writing. If the lock is already locked for +// reading or writing, Lock blocks until the lock is available. +func (mu *RBMutex) Lock() { + mu.rw.Lock() + if atomic.LoadInt32(&mu.rbias) == 1 { + atomic.StoreInt32(&mu.rbias, 0) + start := time.Now() + for i := 0; i < len(mu.rslots); i++ { + for atomic.LoadInt32(&mu.rslots[i].mu) > 0 { + runtime.Gosched() + } + } + mu.inhibitUntil = time.Now().Add(time.Since(start) * nslowdown) + } +} + +// Unlock unlocks m for writing. A panic is raised if m is not locked +// for writing on entry to Unlock. +// +// As with RWMutex, a locked RBMutex is not associated with a +// particular goroutine. One goroutine may RLock (Lock) a RBMutex and +// then arrange for another goroutine to RUnlock (Unlock) it. +func (mu *RBMutex) Unlock() { + mu.rw.Unlock() +} diff --git a/vendor/github.com/puzpuzpuz/xsync/v3/util.go b/vendor/github.com/puzpuzpuz/xsync/v3/util.go new file mode 100644 index 000000000..769270895 --- /dev/null +++ b/vendor/github.com/puzpuzpuz/xsync/v3/util.go @@ -0,0 +1,66 @@ +package xsync + +import ( + "math/bits" + "runtime" + _ "unsafe" +) + +// test-only assert()-like flag +var assertionsEnabled = false + +const ( + // cacheLineSize is used in paddings to prevent false sharing; + // 64B are used instead of 128B as a compromise between + // memory footprint and performance; 128B usage may give ~30% + // improvement on NUMA machines. + cacheLineSize = 64 +) + +// nextPowOf2 computes the next highest power of 2 of 32-bit v. +// Source: https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 +func nextPowOf2(v uint32) uint32 { + if v == 0 { + return 1 + } + v-- + v |= v >> 1 + v |= v >> 2 + v |= v >> 4 + v |= v >> 8 + v |= v >> 16 + v++ + return v +} + +func parallelism() uint32 { + maxProcs := uint32(runtime.GOMAXPROCS(0)) + numCores := uint32(runtime.NumCPU()) + if maxProcs < numCores { + return maxProcs + } + return numCores +} + +//go:noescape +//go:linkname runtime_fastrand runtime.fastrand +func runtime_fastrand() uint32 + +func broadcast(b uint8) uint64 { + return 0x101010101010101 * uint64(b) +} + +func firstMarkedByteIndex(w uint64) int { + return bits.TrailingZeros64(w) >> 3 +} + +// SWAR byte search: may produce false positives, e.g. for 0x0100, +// so make sure to double-check bytes found by this function. +func markZeroBytes(w uint64) uint64 { + return ((w - 0x0101010101010101) & (^w) & 0x8080808080808080) +} + +func setByte(w uint64, b uint8, idx int) uint64 { + shift := idx << 3 + return (w &^ (0xff << shift)) | (uint64(b) << shift) +} diff --git a/vendor/github.com/puzpuzpuz/xsync/v3/util_hash.go b/vendor/github.com/puzpuzpuz/xsync/v3/util_hash.go new file mode 100644 index 000000000..9aa65972d --- /dev/null +++ b/vendor/github.com/puzpuzpuz/xsync/v3/util_hash.go @@ -0,0 +1,77 @@ +package xsync + +import ( + "reflect" + "unsafe" +) + +// makeSeed creates a random seed. +func makeSeed() uint64 { + var s1 uint32 + for { + s1 = runtime_fastrand() + // We use seed 0 to indicate an uninitialized seed/hash, + // so keep trying until we get a non-zero seed. + if s1 != 0 { + break + } + } + s2 := runtime_fastrand() + return uint64(s1)<<32 | uint64(s2) +} + +// hashString calculates a hash of s with the given seed. +func hashString(s string, seed uint64) uint64 { + if s == "" { + return seed + } + strh := (*reflect.StringHeader)(unsafe.Pointer(&s)) + return uint64(runtime_memhash(unsafe.Pointer(strh.Data), uintptr(seed), uintptr(strh.Len))) +} + +//go:noescape +//go:linkname runtime_memhash runtime.memhash +func runtime_memhash(p unsafe.Pointer, h, s uintptr) uintptr + +// defaultHasher creates a fast hash function for the given comparable type. +// The only limitation is that the type should not contain interfaces inside +// based on runtime.typehash. +func defaultHasher[T comparable]() func(T, uint64) uint64 { + var zero T + + if reflect.TypeOf(&zero).Elem().Kind() == reflect.Interface { + return func(value T, seed uint64) uint64 { + iValue := any(value) + i := (*iface)(unsafe.Pointer(&iValue)) + return runtime_typehash64(i.typ, i.word, seed) + } + } else { + var iZero any = zero + i := (*iface)(unsafe.Pointer(&iZero)) + return func(value T, seed uint64) uint64 { + return runtime_typehash64(i.typ, unsafe.Pointer(&value), seed) + } + } +} + +// how interface is represented in memory +type iface struct { + typ uintptr + word unsafe.Pointer +} + +// same as runtime_typehash, but always returns a uint64 +// see: maphash.rthash function for details +func runtime_typehash64(t uintptr, p unsafe.Pointer, seed uint64) uint64 { + if unsafe.Sizeof(uintptr(0)) == 8 { + return uint64(runtime_typehash(t, p, uintptr(seed))) + } + + lo := runtime_typehash(t, p, uintptr(seed)) + hi := runtime_typehash(t, p, uintptr(seed>>32)) + return uint64(hi)<<32 | uint64(lo) +} + +//go:noescape +//go:linkname runtime_typehash runtime.typehash +func runtime_typehash(t uintptr, p unsafe.Pointer, h uintptr) uintptr diff --git a/vendor/github.com/uptrace/bun/CHANGELOG.md b/vendor/github.com/uptrace/bun/CHANGELOG.md index a5ae7761a..ded6f1f40 100644 --- a/vendor/github.com/uptrace/bun/CHANGELOG.md +++ b/vendor/github.com/uptrace/bun/CHANGELOG.md @@ -1,3 +1,70 @@ +## [1.2.5](https://github.com/uptrace/bun/compare/v1.2.3...v1.2.5) (2024-10-26) + + +### Bug Fixes + +* allow Limit() without Order() with MSSQL ([#1009](https://github.com/uptrace/bun/issues/1009)) ([1a46ddc](https://github.com/uptrace/bun/commit/1a46ddc0d3ca0bdc60ca8be5ad1886799d14c8b0)) +* copy bytes in mapModel.Scan ([#1030](https://github.com/uptrace/bun/issues/1030)) ([#1032](https://github.com/uptrace/bun/issues/1032)) ([39fda4e](https://github.com/uptrace/bun/commit/39fda4e3d341e59e4955f751cb354a939e57c1b1)) +* fix issue with has-many join and pointer fields ([#950](https://github.com/uptrace/bun/issues/950)) ([#983](https://github.com/uptrace/bun/issues/983)) ([cbc5177](https://github.com/uptrace/bun/commit/cbc517792ba6cdcef1828f3699d3d4dfe3c5e0eb)) +* restore explicit column: name override ([#984](https://github.com/uptrace/bun/issues/984)) ([169f258](https://github.com/uptrace/bun/commit/169f258a9460cad451f3025d2ef8df1bbd42a003)) +* return column option back ([#1036](https://github.com/uptrace/bun/issues/1036)) ([a3ccbea](https://github.com/uptrace/bun/commit/a3ccbeab39151d3eed6cb245fe15cfb5d71ba557)) +* sql.NullString mistaken as custom struct ([#1019](https://github.com/uptrace/bun/issues/1019)) ([87c77b8](https://github.com/uptrace/bun/commit/87c77b8911f2035b0ee8ea96356a2c7600b5b94d)) +* typos ([#1026](https://github.com/uptrace/bun/issues/1026)) ([760de7d](https://github.com/uptrace/bun/commit/760de7d0fad15dc761475670a4dde056aef9210d)) + + +### Features + +* add transaction isolation level support to pgdriver ([#1034](https://github.com/uptrace/bun/issues/1034)) ([3ef44ce](https://github.com/uptrace/bun/commit/3ef44ce1cdd969a21b76d6c803119cf12c375cb0)) + + +### Performance Improvements + +* refactor SelectQuery.ScanAndCount to optimize performance when there is no limit and offset ([#1035](https://github.com/uptrace/bun/issues/1035)) ([8638613](https://github.com/uptrace/bun/commit/86386135897485bbada6c50ec9a2743626111433)) + + + +## [1.2.4](https://github.com/uptrace/bun/compare/v1.2.3...v1.2.4) (2024-10-26) + + +### Bug Fixes + +* allow Limit() without Order() with MSSQL ([#1009](https://github.com/uptrace/bun/issues/1009)) ([1a46ddc](https://github.com/uptrace/bun/commit/1a46ddc0d3ca0bdc60ca8be5ad1886799d14c8b0)) +* copy bytes in mapModel.Scan ([#1030](https://github.com/uptrace/bun/issues/1030)) ([#1032](https://github.com/uptrace/bun/issues/1032)) ([39fda4e](https://github.com/uptrace/bun/commit/39fda4e3d341e59e4955f751cb354a939e57c1b1)) +* return column option back ([#1036](https://github.com/uptrace/bun/issues/1036)) ([a3ccbea](https://github.com/uptrace/bun/commit/a3ccbeab39151d3eed6cb245fe15cfb5d71ba557)) +* sql.NullString mistaken as custom struct ([#1019](https://github.com/uptrace/bun/issues/1019)) ([87c77b8](https://github.com/uptrace/bun/commit/87c77b8911f2035b0ee8ea96356a2c7600b5b94d)) +* typos ([#1026](https://github.com/uptrace/bun/issues/1026)) ([760de7d](https://github.com/uptrace/bun/commit/760de7d0fad15dc761475670a4dde056aef9210d)) + + +### Features + +* add transaction isolation level support to pgdriver ([#1034](https://github.com/uptrace/bun/issues/1034)) ([3ef44ce](https://github.com/uptrace/bun/commit/3ef44ce1cdd969a21b76d6c803119cf12c375cb0)) + + +### Performance Improvements + +* refactor SelectQuery.ScanAndCount to optimize performance when there is no limit and offset ([#1035](https://github.com/uptrace/bun/issues/1035)) ([8638613](https://github.com/uptrace/bun/commit/86386135897485bbada6c50ec9a2743626111433)) + + + +## [1.2.3](https://github.com/uptrace/bun/compare/v1.2.2...v1.2.3) (2024-08-31) + + + +## [1.2.2](https://github.com/uptrace/bun/compare/v1.2.1...v1.2.2) (2024-08-29) + + +### Bug Fixes + +* gracefully handle empty hstore in pgdialect ([#1010](https://github.com/uptrace/bun/issues/1010)) ([2f73d8a](https://github.com/uptrace/bun/commit/2f73d8a8e16c8718ebfc956036d9c9a01a0888bc)) +* number each unit test ([#974](https://github.com/uptrace/bun/issues/974)) ([b005dc2](https://github.com/uptrace/bun/commit/b005dc2a9034715c6f59dcfc8e76aa3b85df38ab)) + + +### Features + +* add ModelTableExpr to TruncateTableQuery ([#969](https://github.com/uptrace/bun/issues/969)) ([7bc330f](https://github.com/uptrace/bun/commit/7bc330f152cf0d9dc30956478e2731ea5816f012)) + + + ## [1.2.1](https://github.com/uptrace/bun/compare/v1.2.0...v1.2.1) (2024-04-02) @@ -14,7 +81,7 @@ ### Features -* Allow overiding of Warn and Deprecated loggers ([#952](https://github.com/uptrace/bun/issues/952)) ([0e9d737](https://github.com/uptrace/bun/commit/0e9d737e4ca2deb86930237ee32a39cf3f7e8157)) +* Allow overriding of Warn and Deprecated loggers ([#952](https://github.com/uptrace/bun/issues/952)) ([0e9d737](https://github.com/uptrace/bun/commit/0e9d737e4ca2deb86930237ee32a39cf3f7e8157)) * enable SNI ([#953](https://github.com/uptrace/bun/issues/953)) ([4071ffb](https://github.com/uptrace/bun/commit/4071ffb5bcb1b233cda239c92504d8139dcf1d2f)) * **idb:** add NewMerge method to IDB ([#966](https://github.com/uptrace/bun/issues/966)) ([664e2f1](https://github.com/uptrace/bun/commit/664e2f154f1153d2a80cd062a5074f1692edaee7)) @@ -100,7 +167,7 @@ ### Bug Fixes -* add support for inserting values with unicode encoding for mssql dialect ([e98c6c0](https://github.com/uptrace/bun/commit/e98c6c0f033b553bea3bbc783aa56c2eaa17718f)) +* add support for inserting values with Unicode encoding for mssql dialect ([e98c6c0](https://github.com/uptrace/bun/commit/e98c6c0f033b553bea3bbc783aa56c2eaa17718f)) * fix relation tag ([a3eedff](https://github.com/uptrace/bun/commit/a3eedff49700490d4998dcdcdc04f554d8f17166)) @@ -136,7 +203,7 @@ ### Bug Fixes -* addng dialect override for append-bool ([#695](https://github.com/uptrace/bun/issues/695)) ([338f2f0](https://github.com/uptrace/bun/commit/338f2f04105ad89e64530db86aeb387e2ad4789e)) +* adding dialect override for append-bool ([#695](https://github.com/uptrace/bun/issues/695)) ([338f2f0](https://github.com/uptrace/bun/commit/338f2f04105ad89e64530db86aeb387e2ad4789e)) * don't call hooks twice for whereExists ([9057857](https://github.com/uptrace/bun/commit/90578578e717f248e4b6eb114c5b495fd8d4ed41)) * don't lock migrations when running Migrate and Rollback ([69a7354](https://github.com/uptrace/bun/commit/69a7354d987ff2ed5338c9ef5f4ce320724299ab)) * **query:** make WhereDeleted compatible with ForceDelete ([299c3fd](https://github.com/uptrace/bun/commit/299c3fd57866aaecd127a8f219c95332898475db)), closes [#673](https://github.com/uptrace/bun/issues/673) @@ -304,7 +371,7 @@ recommended to upgrade to v1.0.24 before upgrading to v1.1.x. - append slice values ([4a65129](https://github.com/uptrace/bun/commit/4a651294fb0f1e73079553024810c3ead9777311)) -- check for nils when appeding driver.Value +- check for nils when appending driver.Value ([7bb1640](https://github.com/uptrace/bun/commit/7bb1640a00fceca1e1075fe6544b9a4842ab2b26)) - cleanup soft deletes for mssql ([e72e2c5](https://github.com/uptrace/bun/commit/e72e2c5d0a85f3d26c3fa22c7284c2de1dcfda8e)) @@ -323,7 +390,7 @@ recommended to upgrade to v1.0.24 before upgrading to v1.1.x. ### Deprecated -In the comming v1.1.x release, Bun will stop automatically adding `,pk,autoincrement` options on +In the coming v1.1.x release, Bun will stop automatically adding `,pk,autoincrement` options on `ID int64/int32` fields. This version (v1.0.23) only prints a warning when it encounters such fields, but the code will continue working as before. @@ -441,7 +508,7 @@ In v1.1.x, such options as `,nopk` and `,allowzero` will not be necessary and wi ([693f1e1](https://github.com/uptrace/bun/commit/693f1e135999fc31cf83b99a2530a695b20f4e1b)) - add model embedding via embed:prefix\_ ([9a2cedc](https://github.com/uptrace/bun/commit/9a2cedc8b08fa8585d4bfced338bd0a40d736b1d)) -- change the default logoutput to stderr +- change the default log output to stderr ([4bf5773](https://github.com/uptrace/bun/commit/4bf577382f19c64457cbf0d64490401450954654)), closes [#349](https://github.com/uptrace/bun/issues/349) diff --git a/vendor/github.com/uptrace/bun/Makefile b/vendor/github.com/uptrace/bun/Makefile index fc295561c..50a1903e7 100644 --- a/vendor/github.com/uptrace/bun/Makefile +++ b/vendor/github.com/uptrace/bun/Makefile @@ -15,7 +15,7 @@ go_mod_tidy: echo "go mod tidy in $${dir}"; \ (cd "$${dir}" && \ go get -u ./... && \ - go mod tidy -go=1.21); \ + go mod tidy); \ done fmt: diff --git a/vendor/github.com/uptrace/bun/README.md b/vendor/github.com/uptrace/bun/README.md index 07a01aa61..dbe5bc0b4 100644 --- a/vendor/github.com/uptrace/bun/README.md +++ b/vendor/github.com/uptrace/bun/README.md @@ -1,4 +1,4 @@ -# SQL-first Golang ORM for PostgreSQL, MySQL, MSSQL, and SQLite +# SQL-first Golang ORM for PostgreSQL, MySQL, MSSQL, SQLite and Oracle [![build workflow](https://github.com/uptrace/bun/actions/workflows/build.yml/badge.svg)](https://github.com/uptrace/bun/actions) [![PkgGoDev](https://pkg.go.dev/badge/github.com/uptrace/bun)](https://pkg.go.dev/github.com/uptrace/bun) @@ -19,6 +19,7 @@ [MySQL](https://bun.uptrace.dev/guide/drivers.html#mysql) (including MariaDB), [MSSQL](https://bun.uptrace.dev/guide/drivers.html#mssql), [SQLite](https://bun.uptrace.dev/guide/drivers.html#sqlite). + [Oracle](https://bun.uptrace.dev/guide/drivers.html#oracle). - [ORM-like](/example/basic/) experience using good old SQL. Bun supports structs, map, scalars, and slices of map/structs/scalars. - [Bulk inserts](https://bun.uptrace.dev/guide/query-insert.html). diff --git a/vendor/github.com/uptrace/bun/bun.go b/vendor/github.com/uptrace/bun/bun.go index 8f71db8fc..626f0bf4b 100644 --- a/vendor/github.com/uptrace/bun/bun.go +++ b/vendor/github.com/uptrace/bun/bun.go @@ -22,6 +22,10 @@ AfterScanRowHook = schema.AfterScanRowHook ) +func SafeQuery(query string, args ...interface{}) schema.QueryWithArgs { + return schema.SafeQuery(query, args) +} + type BeforeSelectHook interface { BeforeSelect(ctx context.Context, query *SelectQuery) error } @@ -70,7 +74,7 @@ type AfterDropTableHook interface { AfterDropTable(ctx context.Context, query *DropTableQuery) error } -// SetLogger overwriters default Bun logger. +// SetLogger overwrites default Bun logger. func SetLogger(logger internal.Logging) { internal.SetLogger(logger) } diff --git a/vendor/github.com/uptrace/bun/dialect/dialect.go b/vendor/github.com/uptrace/bun/dialect/dialect.go index 03b81fbbc..4dde63c92 100644 --- a/vendor/github.com/uptrace/bun/dialect/dialect.go +++ b/vendor/github.com/uptrace/bun/dialect/dialect.go @@ -12,6 +12,8 @@ func (n Name) String() string { return "mysql" case MSSQL: return "mssql" + case Oracle: + return "oracle" default: return "invalid" } @@ -23,4 +25,5 @@ func (n Name) String() string { SQLite MySQL MSSQL + Oracle ) diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/append.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/append.go index 7e9491abc..c95fa86e7 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/append.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/append.go @@ -2,12 +2,9 @@ import ( "database/sql/driver" - "encoding/hex" "fmt" "reflect" - "strconv" "time" - "unicode/utf8" "github.com/uptrace/bun/dialect" "github.com/uptrace/bun/schema" @@ -32,316 +29,10 @@ sliceTimeType = reflect.TypeOf([]time.Time(nil)) ) -func arrayAppend(fmter schema.Formatter, b []byte, v interface{}) []byte { - switch v := v.(type) { - case int64: - return strconv.AppendInt(b, v, 10) - case float64: - return dialect.AppendFloat64(b, v) - case bool: - return dialect.AppendBool(b, v) - case []byte: - return arrayAppendBytes(b, v) - case string: - return arrayAppendString(b, v) - case time.Time: - return fmter.Dialect().AppendTime(b, v) - default: - err := fmt.Errorf("pgdialect: can't append %T", v) - return dialect.AppendError(b, err) - } +func appendTime(buf []byte, tm time.Time) []byte { + return tm.UTC().AppendFormat(buf, "2006-01-02 15:04:05.999999-07:00") } -func arrayAppendStringValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { - return arrayAppendString(b, v.String()) -} - -func arrayAppendBytesValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { - return arrayAppendBytes(b, v.Bytes()) -} - -func arrayAppendDriverValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { - iface, err := v.Interface().(driver.Valuer).Value() - if err != nil { - return dialect.AppendError(b, err) - } - return arrayAppend(fmter, b, iface) -} - -//------------------------------------------------------------------------------ - -func (d *Dialect) arrayAppender(typ reflect.Type) schema.AppenderFunc { - kind := typ.Kind() - - switch kind { - case reflect.Ptr: - if fn := d.arrayAppender(typ.Elem()); fn != nil { - return schema.PtrAppender(fn) - } - case reflect.Slice, reflect.Array: - // ok: - default: - return nil - } - - elemType := typ.Elem() - - if kind == reflect.Slice { - switch elemType { - case stringType: - return appendStringSliceValue - case intType: - return appendIntSliceValue - case int64Type: - return appendInt64SliceValue - case float64Type: - return appendFloat64SliceValue - case timeType: - return appendTimeSliceValue - } - } - - appendElem := d.arrayElemAppender(elemType) - if appendElem == nil { - panic(fmt.Errorf("pgdialect: %s is not supported", typ)) - } - - return func(fmter schema.Formatter, b []byte, v reflect.Value) []byte { - kind := v.Kind() - switch kind { - case reflect.Ptr, reflect.Slice: - if v.IsNil() { - return dialect.AppendNull(b) - } - } - - if kind == reflect.Ptr { - v = v.Elem() - } - - b = append(b, '\'') - - b = append(b, '{') - ln := v.Len() - for i := 0; i < ln; i++ { - elem := v.Index(i) - b = appendElem(fmter, b, elem) - b = append(b, ',') - } - if v.Len() > 0 { - b[len(b)-1] = '}' // Replace trailing comma. - } else { - b = append(b, '}') - } - - b = append(b, '\'') - - return b - } -} - -func (d *Dialect) arrayElemAppender(typ reflect.Type) schema.AppenderFunc { - if typ.Implements(driverValuerType) { - return arrayAppendDriverValue - } - switch typ.Kind() { - case reflect.String: - return arrayAppendStringValue - case reflect.Slice: - if typ.Elem().Kind() == reflect.Uint8 { - return arrayAppendBytesValue - } - } - return schema.Appender(d, typ) -} - -func appendStringSliceValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { - ss := v.Convert(sliceStringType).Interface().([]string) - return appendStringSlice(b, ss) -} - -func appendStringSlice(b []byte, ss []string) []byte { - if ss == nil { - return dialect.AppendNull(b) - } - - b = append(b, '\'') - - b = append(b, '{') - for _, s := range ss { - b = arrayAppendString(b, s) - b = append(b, ',') - } - if len(ss) > 0 { - b[len(b)-1] = '}' // Replace trailing comma. - } else { - b = append(b, '}') - } - - b = append(b, '\'') - - return b -} - -func appendIntSliceValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { - ints := v.Convert(sliceIntType).Interface().([]int) - return appendIntSlice(b, ints) -} - -func appendIntSlice(b []byte, ints []int) []byte { - if ints == nil { - return dialect.AppendNull(b) - } - - b = append(b, '\'') - - b = append(b, '{') - for _, n := range ints { - b = strconv.AppendInt(b, int64(n), 10) - b = append(b, ',') - } - if len(ints) > 0 { - b[len(b)-1] = '}' // Replace trailing comma. - } else { - b = append(b, '}') - } - - b = append(b, '\'') - - return b -} - -func appendInt64SliceValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { - ints := v.Convert(sliceInt64Type).Interface().([]int64) - return appendInt64Slice(b, ints) -} - -func appendInt64Slice(b []byte, ints []int64) []byte { - if ints == nil { - return dialect.AppendNull(b) - } - - b = append(b, '\'') - - b = append(b, '{') - for _, n := range ints { - b = strconv.AppendInt(b, n, 10) - b = append(b, ',') - } - if len(ints) > 0 { - b[len(b)-1] = '}' // Replace trailing comma. - } else { - b = append(b, '}') - } - - b = append(b, '\'') - - return b -} - -func appendFloat64SliceValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { - floats := v.Convert(sliceFloat64Type).Interface().([]float64) - return appendFloat64Slice(b, floats) -} - -func appendFloat64Slice(b []byte, floats []float64) []byte { - if floats == nil { - return dialect.AppendNull(b) - } - - b = append(b, '\'') - - b = append(b, '{') - for _, n := range floats { - b = dialect.AppendFloat64(b, n) - b = append(b, ',') - } - if len(floats) > 0 { - b[len(b)-1] = '}' // Replace trailing comma. - } else { - b = append(b, '}') - } - - b = append(b, '\'') - - return b -} - -//------------------------------------------------------------------------------ - -func arrayAppendBytes(b []byte, bs []byte) []byte { - if bs == nil { - return dialect.AppendNull(b) - } - - b = append(b, `"\\x`...) - - s := len(b) - b = append(b, make([]byte, hex.EncodedLen(len(bs)))...) - hex.Encode(b[s:], bs) - - b = append(b, '"') - - return b -} - -func arrayAppendString(b []byte, s string) []byte { - b = append(b, '"') - for _, r := range s { - switch r { - case 0: - // ignore - case '\'': - b = append(b, "''"...) - case '"': - b = append(b, '\\', '"') - case '\\': - b = append(b, '\\', '\\') - default: - if r < utf8.RuneSelf { - b = append(b, byte(r)) - break - } - l := len(b) - if cap(b)-l < utf8.UTFMax { - b = append(b, make([]byte, utf8.UTFMax)...) - } - n := utf8.EncodeRune(b[l:l+utf8.UTFMax], r) - b = b[:l+n] - } - } - b = append(b, '"') - return b -} - -func appendTimeSliceValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { - ts := v.Convert(sliceTimeType).Interface().([]time.Time) - return appendTimeSlice(fmter, b, ts) -} - -func appendTimeSlice(fmter schema.Formatter, b []byte, ts []time.Time) []byte { - if ts == nil { - return dialect.AppendNull(b) - } - b = append(b, '\'') - b = append(b, '{') - for _, t := range ts { - b = append(b, '"') - b = t.UTC().AppendFormat(b, "2006-01-02 15:04:05.999999-07:00") - b = append(b, '"') - b = append(b, ',') - } - if len(ts) > 0 { - b[len(b)-1] = '}' // Replace trailing comma. - } else { - b = append(b, '}') - } - b = append(b, '\'') - return b -} - -//------------------------------------------------------------------------------ - var mapStringStringType = reflect.TypeOf(map[string]string(nil)) func (d *Dialect) hstoreAppender(typ reflect.Type) schema.AppenderFunc { diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/array.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/array.go index 281cff733..46b55659b 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/array.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/array.go @@ -2,9 +2,16 @@ import ( "database/sql" + "database/sql/driver" + "encoding/hex" "fmt" "reflect" + "strconv" + "time" + "unicode/utf8" + "github.com/uptrace/bun/dialect" + "github.com/uptrace/bun/internal" "github.com/uptrace/bun/schema" ) @@ -20,7 +27,7 @@ type ArrayValue struct { // // For struct fields you can use array tag: // -// Emails []string `bun:",array"` +// Emails []string `bun:",array"` func Array(vi interface{}) *ArrayValue { v := reflect.ValueOf(vi) if !v.IsValid() { @@ -63,3 +70,576 @@ func (a *ArrayValue) Value() interface{} { } return nil } + +//------------------------------------------------------------------------------ + +func (d *Dialect) arrayAppender(typ reflect.Type) schema.AppenderFunc { + kind := typ.Kind() + + switch kind { + case reflect.Ptr: + if fn := d.arrayAppender(typ.Elem()); fn != nil { + return schema.PtrAppender(fn) + } + case reflect.Slice, reflect.Array: + // continue below + default: + return nil + } + + elemType := typ.Elem() + + if kind == reflect.Slice { + switch elemType { + case stringType: + return appendStringSliceValue + case intType: + return appendIntSliceValue + case int64Type: + return appendInt64SliceValue + case float64Type: + return appendFloat64SliceValue + case timeType: + return appendTimeSliceValue + } + } + + appendElem := d.arrayElemAppender(elemType) + if appendElem == nil { + panic(fmt.Errorf("pgdialect: %s is not supported", typ)) + } + + return func(fmter schema.Formatter, b []byte, v reflect.Value) []byte { + kind := v.Kind() + switch kind { + case reflect.Ptr, reflect.Slice: + if v.IsNil() { + return dialect.AppendNull(b) + } + } + + if kind == reflect.Ptr { + v = v.Elem() + } + + b = append(b, "'{"...) + + ln := v.Len() + for i := 0; i < ln; i++ { + elem := v.Index(i) + if i > 0 { + b = append(b, ',') + } + b = appendElem(fmter, b, elem) + } + + b = append(b, "}'"...) + + return b + } +} + +func (d *Dialect) arrayElemAppender(typ reflect.Type) schema.AppenderFunc { + if typ.Implements(driverValuerType) { + return arrayAppendDriverValue + } + switch typ.Kind() { + case reflect.String: + return arrayAppendStringValue + case reflect.Slice: + if typ.Elem().Kind() == reflect.Uint8 { + return arrayAppendBytesValue + } + } + return schema.Appender(d, typ) +} + +func arrayAppend(fmter schema.Formatter, b []byte, v interface{}) []byte { + switch v := v.(type) { + case int64: + return strconv.AppendInt(b, v, 10) + case float64: + return dialect.AppendFloat64(b, v) + case bool: + return dialect.AppendBool(b, v) + case []byte: + return arrayAppendBytes(b, v) + case string: + return arrayAppendString(b, v) + case time.Time: + return fmter.Dialect().AppendTime(b, v) + default: + err := fmt.Errorf("pgdialect: can't append %T", v) + return dialect.AppendError(b, err) + } +} + +func arrayAppendStringValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { + return arrayAppendString(b, v.String()) +} + +func arrayAppendBytesValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { + return arrayAppendBytes(b, v.Bytes()) +} + +func arrayAppendDriverValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { + iface, err := v.Interface().(driver.Valuer).Value() + if err != nil { + return dialect.AppendError(b, err) + } + return arrayAppend(fmter, b, iface) +} + +func appendStringSliceValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { + ss := v.Convert(sliceStringType).Interface().([]string) + return appendStringSlice(b, ss) +} + +func appendStringSlice(b []byte, ss []string) []byte { + if ss == nil { + return dialect.AppendNull(b) + } + + b = append(b, '\'') + + b = append(b, '{') + for _, s := range ss { + b = arrayAppendString(b, s) + b = append(b, ',') + } + if len(ss) > 0 { + b[len(b)-1] = '}' // Replace trailing comma. + } else { + b = append(b, '}') + } + + b = append(b, '\'') + + return b +} + +func appendIntSliceValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { + ints := v.Convert(sliceIntType).Interface().([]int) + return appendIntSlice(b, ints) +} + +func appendIntSlice(b []byte, ints []int) []byte { + if ints == nil { + return dialect.AppendNull(b) + } + + b = append(b, '\'') + + b = append(b, '{') + for _, n := range ints { + b = strconv.AppendInt(b, int64(n), 10) + b = append(b, ',') + } + if len(ints) > 0 { + b[len(b)-1] = '}' // Replace trailing comma. + } else { + b = append(b, '}') + } + + b = append(b, '\'') + + return b +} + +func appendInt64SliceValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { + ints := v.Convert(sliceInt64Type).Interface().([]int64) + return appendInt64Slice(b, ints) +} + +func appendInt64Slice(b []byte, ints []int64) []byte { + if ints == nil { + return dialect.AppendNull(b) + } + + b = append(b, '\'') + + b = append(b, '{') + for _, n := range ints { + b = strconv.AppendInt(b, n, 10) + b = append(b, ',') + } + if len(ints) > 0 { + b[len(b)-1] = '}' // Replace trailing comma. + } else { + b = append(b, '}') + } + + b = append(b, '\'') + + return b +} + +func appendFloat64SliceValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { + floats := v.Convert(sliceFloat64Type).Interface().([]float64) + return appendFloat64Slice(b, floats) +} + +func appendFloat64Slice(b []byte, floats []float64) []byte { + if floats == nil { + return dialect.AppendNull(b) + } + + b = append(b, '\'') + + b = append(b, '{') + for _, n := range floats { + b = dialect.AppendFloat64(b, n) + b = append(b, ',') + } + if len(floats) > 0 { + b[len(b)-1] = '}' // Replace trailing comma. + } else { + b = append(b, '}') + } + + b = append(b, '\'') + + return b +} + +func appendTimeSliceValue(fmter schema.Formatter, b []byte, v reflect.Value) []byte { + ts := v.Convert(sliceTimeType).Interface().([]time.Time) + return appendTimeSlice(fmter, b, ts) +} + +func appendTimeSlice(fmter schema.Formatter, b []byte, ts []time.Time) []byte { + if ts == nil { + return dialect.AppendNull(b) + } + b = append(b, '\'') + b = append(b, '{') + for _, t := range ts { + b = append(b, '"') + b = appendTime(b, t) + b = append(b, '"') + b = append(b, ',') + } + if len(ts) > 0 { + b[len(b)-1] = '}' // Replace trailing comma. + } else { + b = append(b, '}') + } + b = append(b, '\'') + return b +} + +//------------------------------------------------------------------------------ + +func arrayScanner(typ reflect.Type) schema.ScannerFunc { + kind := typ.Kind() + + switch kind { + case reflect.Ptr: + if fn := arrayScanner(typ.Elem()); fn != nil { + return schema.PtrScanner(fn) + } + case reflect.Slice, reflect.Array: + // ok: + default: + return nil + } + + elemType := typ.Elem() + + if kind == reflect.Slice { + switch elemType { + case stringType: + return scanStringSliceValue + case intType: + return scanIntSliceValue + case int64Type: + return scanInt64SliceValue + case float64Type: + return scanFloat64SliceValue + } + } + + scanElem := schema.Scanner(elemType) + return func(dest reflect.Value, src interface{}) error { + dest = reflect.Indirect(dest) + if !dest.CanSet() { + return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type()) + } + + kind := dest.Kind() + + if src == nil { + if kind != reflect.Slice || !dest.IsNil() { + dest.Set(reflect.Zero(dest.Type())) + } + return nil + } + + if kind == reflect.Slice { + if dest.IsNil() { + dest.Set(reflect.MakeSlice(dest.Type(), 0, 0)) + } else if dest.Len() > 0 { + dest.Set(dest.Slice(0, 0)) + } + } + + b, err := toBytes(src) + if err != nil { + return err + } + + p := newArrayParser(b) + nextValue := internal.MakeSliceNextElemFunc(dest) + for p.Next() { + elem := p.Elem() + elemValue := nextValue() + if err := scanElem(elemValue, elem); err != nil { + return fmt.Errorf("scanElem failed: %w", err) + } + } + return p.Err() + } +} + +func scanStringSliceValue(dest reflect.Value, src interface{}) error { + dest = reflect.Indirect(dest) + if !dest.CanSet() { + return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type()) + } + + slice, err := decodeStringSlice(src) + if err != nil { + return err + } + + dest.Set(reflect.ValueOf(slice)) + return nil +} + +func decodeStringSlice(src interface{}) ([]string, error) { + if src == nil { + return nil, nil + } + + b, err := toBytes(src) + if err != nil { + return nil, err + } + + slice := make([]string, 0) + + p := newArrayParser(b) + for p.Next() { + elem := p.Elem() + slice = append(slice, string(elem)) + } + if err := p.Err(); err != nil { + return nil, err + } + return slice, nil +} + +func scanIntSliceValue(dest reflect.Value, src interface{}) error { + dest = reflect.Indirect(dest) + if !dest.CanSet() { + return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type()) + } + + slice, err := decodeIntSlice(src) + if err != nil { + return err + } + + dest.Set(reflect.ValueOf(slice)) + return nil +} + +func decodeIntSlice(src interface{}) ([]int, error) { + if src == nil { + return nil, nil + } + + b, err := toBytes(src) + if err != nil { + return nil, err + } + + slice := make([]int, 0) + + p := newArrayParser(b) + for p.Next() { + elem := p.Elem() + + if elem == nil { + slice = append(slice, 0) + continue + } + + n, err := strconv.Atoi(bytesToString(elem)) + if err != nil { + return nil, err + } + + slice = append(slice, n) + } + if err := p.Err(); err != nil { + return nil, err + } + return slice, nil +} + +func scanInt64SliceValue(dest reflect.Value, src interface{}) error { + dest = reflect.Indirect(dest) + if !dest.CanSet() { + return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type()) + } + + slice, err := decodeInt64Slice(src) + if err != nil { + return err + } + + dest.Set(reflect.ValueOf(slice)) + return nil +} + +func decodeInt64Slice(src interface{}) ([]int64, error) { + if src == nil { + return nil, nil + } + + b, err := toBytes(src) + if err != nil { + return nil, err + } + + slice := make([]int64, 0) + + p := newArrayParser(b) + for p.Next() { + elem := p.Elem() + + if elem == nil { + slice = append(slice, 0) + continue + } + + n, err := strconv.ParseInt(bytesToString(elem), 10, 64) + if err != nil { + return nil, err + } + + slice = append(slice, n) + } + if err := p.Err(); err != nil { + return nil, err + } + return slice, nil +} + +func scanFloat64SliceValue(dest reflect.Value, src interface{}) error { + dest = reflect.Indirect(dest) + if !dest.CanSet() { + return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type()) + } + + slice, err := scanFloat64Slice(src) + if err != nil { + return err + } + + dest.Set(reflect.ValueOf(slice)) + return nil +} + +func scanFloat64Slice(src interface{}) ([]float64, error) { + if src == -1 { + return nil, nil + } + + b, err := toBytes(src) + if err != nil { + return nil, err + } + + slice := make([]float64, 0) + + p := newArrayParser(b) + for p.Next() { + elem := p.Elem() + + if elem == nil { + slice = append(slice, 0) + continue + } + + n, err := strconv.ParseFloat(bytesToString(elem), 64) + if err != nil { + return nil, err + } + + slice = append(slice, n) + } + if err := p.Err(); err != nil { + return nil, err + } + return slice, nil +} + +func toBytes(src interface{}) ([]byte, error) { + switch src := src.(type) { + case string: + return stringToBytes(src), nil + case []byte: + return src, nil + default: + return nil, fmt.Errorf("bun: got %T, wanted []byte or string", src) + } +} + +//------------------------------------------------------------------------------ + +func arrayAppendBytes(b []byte, bs []byte) []byte { + if bs == nil { + return dialect.AppendNull(b) + } + + b = append(b, `"\\x`...) + + s := len(b) + b = append(b, make([]byte, hex.EncodedLen(len(bs)))...) + hex.Encode(b[s:], bs) + + b = append(b, '"') + + return b +} + +func arrayAppendString(b []byte, s string) []byte { + b = append(b, '"') + for _, r := range s { + switch r { + case 0: + // ignore + case '\'': + b = append(b, "''"...) + case '"': + b = append(b, '\\', '"') + case '\\': + b = append(b, '\\', '\\') + default: + if r < utf8.RuneSelf { + b = append(b, byte(r)) + break + } + l := len(b) + if cap(b)-l < utf8.UTFMax { + b = append(b, make([]byte, utf8.UTFMax)...) + } + n := utf8.EncodeRune(b[l:l+utf8.UTFMax], r) + b = b[:l+n] + } + } + b = append(b, '"') + return b +} diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/array_parser.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/array_parser.go index a8358337e..462f8d91d 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/array_parser.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/array_parser.go @@ -2,132 +2,92 @@ import ( "bytes" - "encoding/hex" "fmt" "io" ) type arrayParser struct { - *streamParser - err error + p pgparser + + elem []byte + err error } func newArrayParser(b []byte) *arrayParser { - p := &arrayParser{ - streamParser: newStreamParser(b, 1), - } + p := new(arrayParser) + if len(b) < 2 || b[0] != '{' || b[len(b)-1] != '}' { - p.err = fmt.Errorf("bun: can't parse array: %q", b) + p.err = fmt.Errorf("pgdialect: can't parse array: %q", b) + return p } + + p.p.Reset(b[1 : len(b)-1]) return p } -func (p *arrayParser) NextElem() ([]byte, error) { +func (p *arrayParser) Next() bool { if p.err != nil { - return nil, p.err + return false + } + p.err = p.readNext() + return p.err == nil +} + +func (p *arrayParser) Err() error { + if p.err != io.EOF { + return p.err + } + return nil +} + +func (p *arrayParser) Elem() []byte { + return p.elem +} + +func (p *arrayParser) readNext() error { + ch := p.p.Read() + if ch == 0 { + return io.EOF } - c, err := p.readByte() - if err != nil { - return nil, err - } - - switch c { + switch ch { case '}': - return nil, io.EOF + return io.EOF case '"': - b, err := p.readSubstring() + b, err := p.p.ReadSubstring(ch) if err != nil { - return nil, err + return err } - if p.peek() == ',' { - p.skipNext() + if p.p.Peek() == ',' { + p.p.Advance() } - return b, nil + p.elem = b + return nil + case '[', '(': + rng, err := p.p.ReadRange(ch) + if err != nil { + return err + } + + if p.p.Peek() == ',' { + p.p.Advance() + } + + p.elem = rng + return nil default: - b := p.readSimple() - if bytes.Equal(b, []byte("NULL")) { - b = nil + lit := p.p.ReadLiteral(ch) + if bytes.Equal(lit, []byte("NULL")) { + lit = nil } - if p.peek() == ',' { - p.skipNext() + if p.p.Peek() == ',' { + p.p.Advance() } - return b, nil + p.elem = lit + return nil } } - -func (p *arrayParser) readSimple() []byte { - p.unreadByte() - - if i := bytes.IndexByte(p.b[p.i:], ','); i >= 0 { - b := p.b[p.i : p.i+i] - p.i += i - return b - } - - b := p.b[p.i : len(p.b)-1] - p.i = len(p.b) - 1 - return b -} - -func (p *arrayParser) readSubstring() ([]byte, error) { - c, err := p.readByte() - if err != nil { - return nil, err - } - - p.buf = p.buf[:0] - for { - if c == '"' { - break - } - - next, err := p.readByte() - if err != nil { - return nil, err - } - - if c == '\\' { - switch next { - case '\\', '"': - p.buf = append(p.buf, next) - - c, err = p.readByte() - if err != nil { - return nil, err - } - default: - p.buf = append(p.buf, '\\') - c = next - } - continue - } - if c == '\'' && next == '\'' { - p.buf = append(p.buf, next) - c, err = p.readByte() - if err != nil { - return nil, err - } - continue - } - - p.buf = append(p.buf, c) - c = next - } - - if bytes.HasPrefix(p.buf, []byte("\\x")) && len(p.buf)%2 == 0 { - data := p.buf[2:] - buf := make([]byte, hex.DecodedLen(len(data))) - n, err := hex.Decode(buf, data) - if err != nil { - return nil, err - } - return buf[:n], nil - } - - return p.buf, nil -} diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/array_scan.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/array_scan.go index a8ff29715..6b8abda3d 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/array_scan.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/array_scan.go @@ -1,302 +1 @@ package pgdialect - -import ( - "fmt" - "io" - "reflect" - "strconv" - - "github.com/uptrace/bun/internal" - "github.com/uptrace/bun/schema" -) - -func arrayScanner(typ reflect.Type) schema.ScannerFunc { - kind := typ.Kind() - - switch kind { - case reflect.Ptr: - if fn := arrayScanner(typ.Elem()); fn != nil { - return schema.PtrScanner(fn) - } - case reflect.Slice, reflect.Array: - // ok: - default: - return nil - } - - elemType := typ.Elem() - - if kind == reflect.Slice { - switch elemType { - case stringType: - return scanStringSliceValue - case intType: - return scanIntSliceValue - case int64Type: - return scanInt64SliceValue - case float64Type: - return scanFloat64SliceValue - } - } - - scanElem := schema.Scanner(elemType) - return func(dest reflect.Value, src interface{}) error { - dest = reflect.Indirect(dest) - if !dest.CanSet() { - return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type()) - } - - kind := dest.Kind() - - if src == nil { - if kind != reflect.Slice || !dest.IsNil() { - dest.Set(reflect.Zero(dest.Type())) - } - return nil - } - - if kind == reflect.Slice { - if dest.IsNil() { - dest.Set(reflect.MakeSlice(dest.Type(), 0, 0)) - } else if dest.Len() > 0 { - dest.Set(dest.Slice(0, 0)) - } - } - - b, err := toBytes(src) - if err != nil { - return err - } - - p := newArrayParser(b) - nextValue := internal.MakeSliceNextElemFunc(dest) - for { - elem, err := p.NextElem() - if err != nil { - if err == io.EOF { - break - } - return err - } - - elemValue := nextValue() - if err := scanElem(elemValue, elem); err != nil { - return err - } - } - - return nil - } -} - -func scanStringSliceValue(dest reflect.Value, src interface{}) error { - dest = reflect.Indirect(dest) - if !dest.CanSet() { - return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type()) - } - - slice, err := decodeStringSlice(src) - if err != nil { - return err - } - - dest.Set(reflect.ValueOf(slice)) - return nil -} - -func decodeStringSlice(src interface{}) ([]string, error) { - if src == nil { - return nil, nil - } - - b, err := toBytes(src) - if err != nil { - return nil, err - } - - slice := make([]string, 0) - - p := newArrayParser(b) - for { - elem, err := p.NextElem() - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - slice = append(slice, string(elem)) - } - - return slice, nil -} - -func scanIntSliceValue(dest reflect.Value, src interface{}) error { - dest = reflect.Indirect(dest) - if !dest.CanSet() { - return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type()) - } - - slice, err := decodeIntSlice(src) - if err != nil { - return err - } - - dest.Set(reflect.ValueOf(slice)) - return nil -} - -func decodeIntSlice(src interface{}) ([]int, error) { - if src == nil { - return nil, nil - } - - b, err := toBytes(src) - if err != nil { - return nil, err - } - - slice := make([]int, 0) - - p := newArrayParser(b) - for { - elem, err := p.NextElem() - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - - if elem == nil { - slice = append(slice, 0) - continue - } - - n, err := strconv.Atoi(bytesToString(elem)) - if err != nil { - return nil, err - } - - slice = append(slice, n) - } - - return slice, nil -} - -func scanInt64SliceValue(dest reflect.Value, src interface{}) error { - dest = reflect.Indirect(dest) - if !dest.CanSet() { - return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type()) - } - - slice, err := decodeInt64Slice(src) - if err != nil { - return err - } - - dest.Set(reflect.ValueOf(slice)) - return nil -} - -func decodeInt64Slice(src interface{}) ([]int64, error) { - if src == nil { - return nil, nil - } - - b, err := toBytes(src) - if err != nil { - return nil, err - } - - slice := make([]int64, 0) - - p := newArrayParser(b) - for { - elem, err := p.NextElem() - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - - if elem == nil { - slice = append(slice, 0) - continue - } - - n, err := strconv.ParseInt(bytesToString(elem), 10, 64) - if err != nil { - return nil, err - } - - slice = append(slice, n) - } - - return slice, nil -} - -func scanFloat64SliceValue(dest reflect.Value, src interface{}) error { - dest = reflect.Indirect(dest) - if !dest.CanSet() { - return fmt.Errorf("bun: Scan(non-settable %s)", dest.Type()) - } - - slice, err := scanFloat64Slice(src) - if err != nil { - return err - } - - dest.Set(reflect.ValueOf(slice)) - return nil -} - -func scanFloat64Slice(src interface{}) ([]float64, error) { - if src == -1 { - return nil, nil - } - - b, err := toBytes(src) - if err != nil { - return nil, err - } - - slice := make([]float64, 0) - - p := newArrayParser(b) - for { - elem, err := p.NextElem() - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - - if elem == nil { - slice = append(slice, 0) - continue - } - - n, err := strconv.ParseFloat(bytesToString(elem), 64) - if err != nil { - return nil, err - } - - slice = append(slice, n) - } - - return slice, nil -} - -func toBytes(src interface{}) ([]byte, error) { - switch src := src.(type) { - case string: - return stringToBytes(src), nil - case []byte: - return src, nil - default: - return nil, fmt.Errorf("bun: got %T, wanted []byte or string", src) - } -} diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/dialect.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/dialect.go index f100e682c..358971f61 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/dialect.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/dialect.go @@ -89,9 +89,17 @@ func (d *Dialect) onField(field *schema.Field) { if field.Tag.HasOption("array") || strings.HasSuffix(field.UserSQLType, "[]") { field.Append = d.arrayAppender(field.StructField.Type) field.Scan = arrayScanner(field.StructField.Type) + return } - if field.DiscoveredSQLType == sqltype.HSTORE { + if field.Tag.HasOption("multirange") { + field.Append = d.arrayAppender(field.StructField.Type) + field.Scan = arrayScanner(field.StructField.Type) + return + } + + switch field.DiscoveredSQLType { + case sqltype.HSTORE: field.Append = d.hstoreAppender(field.StructField.Type) field.Scan = hstoreScanner(field.StructField.Type) } diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/hstore_parser.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/hstore_parser.go index 7a18b50b1..fec401786 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/hstore_parser.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/hstore_parser.go @@ -3,140 +3,98 @@ import ( "bytes" "fmt" + "io" ) type hstoreParser struct { - *streamParser - err error + p pgparser + + key string + value string + err error } func newHStoreParser(b []byte) *hstoreParser { - p := &hstoreParser{ - streamParser: newStreamParser(b, 0), - } - if len(b) < 6 || b[0] != '"' { - p.err = fmt.Errorf("bun: can't parse hstore: %q", b) + p := new(hstoreParser) + if len(b) != 0 && (len(b) < 6 || b[0] != '"') { + p.err = fmt.Errorf("pgdialect: can't parse hstore: %q", b) + return p } + p.p.Reset(b) return p } -func (p *hstoreParser) NextKey() (string, error) { +func (p *hstoreParser) Next() bool { if p.err != nil { - return "", p.err + return false } - - err := p.skipByte('"') - if err != nil { - return "", err - } - - key, err := p.readSubstring() - if err != nil { - return "", err - } - - const separator = "=>" - - for i := range separator { - err = p.skipByte(separator[i]) - if err != nil { - return "", err - } - } - - return string(key), nil + p.err = p.readNext() + return p.err == nil } -func (p *hstoreParser) NextValue() (string, error) { - if p.err != nil { - return "", p.err +func (p *hstoreParser) Err() error { + if p.err != io.EOF { + return p.err + } + return nil +} + +func (p *hstoreParser) Key() string { + return p.key +} + +func (p *hstoreParser) Value() string { + return p.value +} + +func (p *hstoreParser) readNext() error { + if !p.p.Valid() { + return io.EOF } - c, err := p.readByte() + if err := p.p.Skip('"'); err != nil { + return err + } + + key, err := p.p.ReadUnescapedSubstring('"') if err != nil { - return "", err + return err + } + p.key = string(key) + + if err := p.p.SkipPrefix([]byte("=>")); err != nil { + return err } - switch c { + ch, err := p.p.ReadByte() + if err != nil { + return err + } + + switch ch { case '"': - value, err := p.readSubstring() + value, err := p.p.ReadUnescapedSubstring(ch) if err != nil { - return "", err + return err } - - if p.peek() == ',' { - p.skipNext() - } - - if p.peek() == ' ' { - p.skipNext() - } - - return string(value), nil + p.skipComma() + p.value = string(value) + return nil default: - value := p.readSimple() + value := p.p.ReadLiteral(ch) if bytes.Equal(value, []byte("NULL")) { - value = nil + p.value = "" } - - if p.peek() == ',' { - p.skipNext() - } - - return string(value), nil + p.skipComma() + return nil } } -func (p *hstoreParser) readSimple() []byte { - p.unreadByte() - - if i := bytes.IndexByte(p.b[p.i:], ','); i >= 0 { - b := p.b[p.i : p.i+i] - p.i += i - return b +func (p *hstoreParser) skipComma() { + if p.p.Peek() == ',' { + p.p.Advance() + } + if p.p.Peek() == ' ' { + p.p.Advance() } - - b := p.b[p.i:len(p.b)] - p.i = len(p.b) - return b -} - -func (p *hstoreParser) readSubstring() ([]byte, error) { - c, err := p.readByte() - if err != nil { - return nil, err - } - - p.buf = p.buf[:0] - for { - if c == '"' { - break - } - - next, err := p.readByte() - if err != nil { - return nil, err - } - - if c == '\\' { - switch next { - case '\\', '"': - p.buf = append(p.buf, next) - - c, err = p.readByte() - if err != nil { - return nil, err - } - default: - p.buf = append(p.buf, '\\') - c = next - } - continue - } - - p.buf = append(p.buf, c) - c = next - } - - return p.buf, nil } diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/hstore_scan.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/hstore_scan.go index b10b06b8d..62ab89a3a 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/hstore_scan.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/hstore_scan.go @@ -2,7 +2,6 @@ import ( "fmt" - "io" "reflect" "github.com/uptrace/bun/schema" @@ -58,25 +57,11 @@ func decodeMapStringString(src interface{}) (map[string]string, error) { m := make(map[string]string) p := newHStoreParser(b) - for { - key, err := p.NextKey() - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - - value, err := p.NextValue() - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - - m[key] = value + for p.Next() { + m[p.Key()] = p.Value() + } + if err := p.Err(); err != nil { + return nil, err } - return m, nil } diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/range.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/range.go new file mode 100644 index 000000000..b942a068e --- /dev/null +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/range.go @@ -0,0 +1,240 @@ +package pgdialect + +import ( + "bytes" + "database/sql" + "encoding/hex" + "fmt" + "io" + "time" + + "github.com/uptrace/bun/internal" + "github.com/uptrace/bun/internal/parser" + "github.com/uptrace/bun/schema" +) + +type MultiRange[T any] []Range[T] + +type Range[T any] struct { + Lower, Upper T + LowerBound, UpperBound RangeBound +} + +type RangeBound byte + +const ( + RangeBoundInclusiveLeft RangeBound = '[' + RangeBoundInclusiveRight RangeBound = ']' + RangeBoundExclusiveLeft RangeBound = '(' + RangeBoundExclusiveRight RangeBound = ')' +) + +func NewRange[T any](lower, upper T) Range[T] { + return Range[T]{ + Lower: lower, + Upper: upper, + LowerBound: RangeBoundInclusiveLeft, + UpperBound: RangeBoundExclusiveRight, + } +} + +var _ sql.Scanner = (*Range[any])(nil) + +func (r *Range[T]) Scan(anySrc any) (err error) { + src := anySrc.([]byte) + + if len(src) == 0 { + return io.ErrUnexpectedEOF + } + r.LowerBound = RangeBound(src[0]) + src = src[1:] + + src, err = scanElem(&r.Lower, src) + if err != nil { + return err + } + + if len(src) == 0 { + return io.ErrUnexpectedEOF + } + if ch := src[0]; ch != ',' { + return fmt.Errorf("got %q, wanted %q", ch, ',') + } + src = src[1:] + + src, err = scanElem(&r.Upper, src) + if err != nil { + return err + } + + if len(src) == 0 { + return io.ErrUnexpectedEOF + } + r.UpperBound = RangeBound(src[0]) + src = src[1:] + + if len(src) > 0 { + return fmt.Errorf("unread data: %q", src) + } + return nil +} + +var _ schema.QueryAppender = (*Range[any])(nil) + +func (r *Range[T]) AppendQuery(fmt schema.Formatter, buf []byte) ([]byte, error) { + buf = append(buf, byte(r.LowerBound)) + buf = appendElem(buf, r.Lower) + buf = append(buf, ',') + buf = appendElem(buf, r.Upper) + buf = append(buf, byte(r.UpperBound)) + return buf, nil +} + +func appendElem(buf []byte, val any) []byte { + switch val := val.(type) { + case time.Time: + buf = append(buf, '"') + buf = appendTime(buf, val) + buf = append(buf, '"') + return buf + default: + panic(fmt.Errorf("unsupported range type: %T", val)) + } +} + +func scanElem(ptr any, src []byte) ([]byte, error) { + switch ptr := ptr.(type) { + case *time.Time: + src, str, err := readStringLiteral(src) + if err != nil { + return nil, err + } + + tm, err := internal.ParseTime(internal.String(str)) + if err != nil { + return nil, err + } + *ptr = tm + + return src, nil + default: + panic(fmt.Errorf("unsupported range type: %T", ptr)) + } +} + +func readStringLiteral(src []byte) ([]byte, []byte, error) { + p := newParser(src) + + if err := p.Skip('"'); err != nil { + return nil, nil, err + } + + str, err := p.ReadSubstring('"') + if err != nil { + return nil, nil, err + } + + src = p.Remaining() + return src, str, nil +} + +//------------------------------------------------------------------------------ + +type pgparser struct { + parser.Parser + buf []byte +} + +func newParser(b []byte) *pgparser { + p := new(pgparser) + p.Reset(b) + return p +} + +func (p *pgparser) ReadLiteral(ch byte) []byte { + p.Unread() + lit, _ := p.ReadSep(',') + return lit +} + +func (p *pgparser) ReadUnescapedSubstring(ch byte) ([]byte, error) { + return p.readSubstring(ch, false) +} + +func (p *pgparser) ReadSubstring(ch byte) ([]byte, error) { + return p.readSubstring(ch, true) +} + +func (p *pgparser) readSubstring(ch byte, escaped bool) ([]byte, error) { + ch, err := p.ReadByte() + if err != nil { + return nil, err + } + + p.buf = p.buf[:0] + for { + if ch == '"' { + break + } + + next, err := p.ReadByte() + if err != nil { + return nil, err + } + + if ch == '\\' { + switch next { + case '\\', '"': + p.buf = append(p.buf, next) + + ch, err = p.ReadByte() + if err != nil { + return nil, err + } + default: + p.buf = append(p.buf, '\\') + ch = next + } + continue + } + + if escaped && ch == '\'' && next == '\'' { + p.buf = append(p.buf, next) + ch, err = p.ReadByte() + if err != nil { + return nil, err + } + continue + } + + p.buf = append(p.buf, ch) + ch = next + } + + if bytes.HasPrefix(p.buf, []byte("\\x")) && len(p.buf)%2 == 0 { + data := p.buf[2:] + buf := make([]byte, hex.DecodedLen(len(data))) + n, err := hex.Decode(buf, data) + if err != nil { + return nil, err + } + return buf[:n], nil + } + + return p.buf, nil +} + +func (p *pgparser) ReadRange(ch byte) ([]byte, error) { + p.buf = p.buf[:0] + p.buf = append(p.buf, ch) + + for p.Valid() { + ch = p.Read() + p.buf = append(p.buf, ch) + if ch == ']' || ch == ')' { + break + } + } + + return p.buf, nil +} diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/sqltype.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/sqltype.go index dadea5c1c..fad84209d 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/sqltype.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/sqltype.go @@ -1,6 +1,7 @@ package pgdialect import ( + "database/sql" "encoding/json" "net" "reflect" @@ -27,14 +28,6 @@ pgTypeSerial = "SERIAL" // 4 byte autoincrementing integer pgTypeBigSerial = "BIGSERIAL" // 8 byte autoincrementing integer - // Character Types - pgTypeChar = "CHAR" // fixed length string (blank padded) - pgTypeText = "TEXT" // variable length string without limit - - // JSON Types - pgTypeJSON = "JSON" // text representation of json data - pgTypeJSONB = "JSONB" // binary representation of json data - // Binary Data Types pgTypeBytea = "BYTEA" // binary string ) @@ -43,6 +36,7 @@ ipType = reflect.TypeOf((*net.IP)(nil)).Elem() ipNetType = reflect.TypeOf((*net.IPNet)(nil)).Elem() jsonRawMessageType = reflect.TypeOf((*json.RawMessage)(nil)).Elem() + nullStringType = reflect.TypeOf((*sql.NullString)(nil)).Elem() ) func (d *Dialect) DefaultVarcharLen() int { @@ -78,12 +72,14 @@ func fieldSQLType(field *schema.Field) string { func sqlType(typ reflect.Type) string { switch typ { + case nullStringType: // typ.Kind() == reflect.Struct, test for exact match + return sqltype.VarChar case ipType: return pgTypeInet case ipNetType: return pgTypeCidr case jsonRawMessageType: - return pgTypeJSONB + return sqltype.JSONB } sqlType := schema.DiscoverSQLType(typ) @@ -93,16 +89,16 @@ func sqlType(typ reflect.Type) string { } switch typ.Kind() { - case reflect.Map, reflect.Struct: + case reflect.Map, reflect.Struct: // except typ == nullStringType, see above if sqlType == sqltype.VarChar { - return pgTypeJSONB + return sqltype.JSONB } return sqlType case reflect.Array, reflect.Slice: if typ.Elem().Kind() == reflect.Uint8 { return pgTypeBytea } - return pgTypeJSONB + return sqltype.JSONB } return sqlType diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/stream_parser.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/stream_parser.go deleted file mode 100644 index 7b9a15f62..000000000 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/stream_parser.go +++ /dev/null @@ -1,60 +0,0 @@ -package pgdialect - -import ( - "fmt" - "io" -) - -type streamParser struct { - b []byte - i int - - buf []byte -} - -func newStreamParser(b []byte, start int) *streamParser { - return &streamParser{ - b: b, - i: start, - } -} - -func (p *streamParser) valid() bool { - return p.i < len(p.b) -} - -func (p *streamParser) skipByte(skip byte) error { - c, err := p.readByte() - if err != nil { - return err - } - if c == skip { - return nil - } - p.unreadByte() - return fmt.Errorf("got %q, wanted %q", c, skip) -} - -func (p *streamParser) readByte() (byte, error) { - if p.valid() { - c := p.b[p.i] - p.i++ - return c, nil - } - return 0, io.EOF -} - -func (p *streamParser) unreadByte() { - p.i-- -} - -func (p *streamParser) peek() byte { - if p.valid() { - return p.b[p.i] - } - return 0 -} - -func (p *streamParser) skipNext() { - p.i++ -} diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/version.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/version.go index b5e5e3cb0..c06043647 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/version.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/version.go @@ -2,5 +2,5 @@ // Version is the current release version. func Version() string { - return "1.2.1" + return "1.2.5" } diff --git a/vendor/github.com/uptrace/bun/dialect/sqlitedialect/version.go b/vendor/github.com/uptrace/bun/dialect/sqlitedialect/version.go index 06e5bb1a4..e3cceaa77 100644 --- a/vendor/github.com/uptrace/bun/dialect/sqlitedialect/version.go +++ b/vendor/github.com/uptrace/bun/dialect/sqlitedialect/version.go @@ -2,5 +2,5 @@ // Version is the current release version. func Version() string { - return "1.2.1" + return "1.2.5" } diff --git a/vendor/github.com/uptrace/bun/extra/bunotel/README.md b/vendor/github.com/uptrace/bun/extra/bunotel/README.md index 50b3e6c48..1773ecf02 100644 --- a/vendor/github.com/uptrace/bun/extra/bunotel/README.md +++ b/vendor/github.com/uptrace/bun/extra/bunotel/README.md @@ -1,3 +1,3 @@ # OpenTelemetry instrumentation for Bun -See [example](../example/opentelemetry) for details. +See [example](../../example/opentelemetry) for details. diff --git a/vendor/github.com/uptrace/bun/extra/bunotel/unsafe.go b/vendor/github.com/uptrace/bun/extra/bunotel/unsafe.go index 23accd40e..67b687cbe 100644 --- a/vendor/github.com/uptrace/bun/extra/bunotel/unsafe.go +++ b/vendor/github.com/uptrace/bun/extra/bunotel/unsafe.go @@ -1,3 +1,4 @@ +//go:build !appengine // +build !appengine package bunotel @@ -5,14 +6,15 @@ import "unsafe" func bytesToString(b []byte) string { - return *(*string)(unsafe.Pointer(&b)) + if len(b) == 0 { + return "" + } + return unsafe.String(&b[0], len(b)) } func stringToBytes(s string) []byte { - return *(*[]byte)(unsafe.Pointer( - &struct { - string - Cap int - }{s, len(s)}, - )) + if s == "" { + return []byte{} + } + return unsafe.Slice(unsafe.StringData(s), len(s)) } diff --git a/vendor/github.com/uptrace/bun/internal/parser/parser.go b/vendor/github.com/uptrace/bun/internal/parser/parser.go index cdfc0be16..1f2704478 100644 --- a/vendor/github.com/uptrace/bun/internal/parser/parser.go +++ b/vendor/github.com/uptrace/bun/internal/parser/parser.go @@ -2,6 +2,8 @@ import ( "bytes" + "fmt" + "io" "strconv" "github.com/uptrace/bun/internal" @@ -22,23 +24,43 @@ func NewString(s string) *Parser { return New(internal.Bytes(s)) } +func (p *Parser) Reset(b []byte) { + p.b = b + p.i = 0 +} + func (p *Parser) Valid() bool { return p.i < len(p.b) } -func (p *Parser) Bytes() []byte { +func (p *Parser) Remaining() []byte { return p.b[p.i:] } +func (p *Parser) ReadByte() (byte, error) { + if p.Valid() { + ch := p.b[p.i] + p.Advance() + return ch, nil + } + return 0, io.ErrUnexpectedEOF +} + func (p *Parser) Read() byte { if p.Valid() { - c := p.b[p.i] + ch := p.b[p.i] p.Advance() - return c + return ch } return 0 } +func (p *Parser) Unread() { + if p.i > 0 { + p.i-- + } +} + func (p *Parser) Peek() byte { if p.Valid() { return p.b[p.i] @@ -50,19 +72,25 @@ func (p *Parser) Advance() { p.i++ } -func (p *Parser) Skip(skip byte) bool { - if p.Peek() == skip { +func (p *Parser) Skip(skip byte) error { + ch := p.Peek() + if ch == skip { p.Advance() - return true + return nil } - return false + return fmt.Errorf("got %q, wanted %q", ch, skip) } -func (p *Parser) SkipBytes(skip []byte) bool { - if len(skip) > len(p.b[p.i:]) { - return false +func (p *Parser) SkipPrefix(skip []byte) error { + if !bytes.HasPrefix(p.b[p.i:], skip) { + return fmt.Errorf("got %q, wanted prefix %q", p.b, skip) } - if !bytes.Equal(p.b[p.i:p.i+len(skip)], skip) { + p.i += len(skip) + return nil +} + +func (p *Parser) CutPrefix(skip []byte) bool { + if !bytes.HasPrefix(p.b[p.i:], skip) { return false } p.i += len(skip) diff --git a/vendor/github.com/uptrace/bun/internal/unsafe.go b/vendor/github.com/uptrace/bun/internal/unsafe.go index 4bc79701f..1a0331297 100644 --- a/vendor/github.com/uptrace/bun/internal/unsafe.go +++ b/vendor/github.com/uptrace/bun/internal/unsafe.go @@ -1,3 +1,4 @@ +//go:build !appengine // +build !appengine package internal @@ -6,15 +7,16 @@ // String converts byte slice to string. func String(b []byte) string { - return *(*string)(unsafe.Pointer(&b)) + if len(b) == 0 { + return "" + } + return unsafe.String(&b[0], len(b)) } // Bytes converts string to byte slice. func Bytes(s string) []byte { - return *(*[]byte)(unsafe.Pointer( - &struct { - string - Cap int - }{s, len(s)}, - )) + if s == "" { + return []byte{} + } + return unsafe.Slice(unsafe.StringData(s), len(s)) } diff --git a/vendor/github.com/uptrace/bun/migrate/migrations.go b/vendor/github.com/uptrace/bun/migrate/migrations.go index 289735270..1a7ea5668 100644 --- a/vendor/github.com/uptrace/bun/migrate/migrations.go +++ b/vendor/github.com/uptrace/bun/migrate/migrations.go @@ -96,10 +96,6 @@ func (m *Migrations) Discover(fsys fs.FS) error { } migration := m.getOrCreateMigration(name) - if err != nil { - return err - } - migration.Comment = comment migrationFunc := NewSQLMigrationFunc(fsys, path) diff --git a/vendor/github.com/uptrace/bun/migrate/migrator.go b/vendor/github.com/uptrace/bun/migrate/migrator.go index 33c5bd16f..e6d70e39f 100644 --- a/vendor/github.com/uptrace/bun/migrate/migrator.go +++ b/vendor/github.com/uptrace/bun/migrate/migrator.go @@ -362,7 +362,10 @@ func (m *Migrator) MarkUnapplied(ctx context.Context, migration *Migration) erro } func (m *Migrator) TruncateTable(ctx context.Context) error { - _, err := m.db.NewTruncateTable().TableExpr(m.table).Exec(ctx) + _, err := m.db.NewTruncateTable(). + Model((*Migration)(nil)). + ModelTableExpr(m.table). + Exec(ctx) return err } diff --git a/vendor/github.com/uptrace/bun/model_map.go b/vendor/github.com/uptrace/bun/model_map.go index 814d636e6..d7342576f 100644 --- a/vendor/github.com/uptrace/bun/model_map.go +++ b/vendor/github.com/uptrace/bun/model_map.go @@ -1,6 +1,7 @@ package bun import ( + "bytes" "context" "database/sql" "reflect" @@ -82,6 +83,8 @@ func (m *mapModel) Scan(src interface{}) error { return m.scanRaw(src) case reflect.Slice: if scanType.Elem().Kind() == reflect.Uint8 { + // Reference types such as []byte are only valid until the next call to Scan. + src := bytes.Clone(src.([]byte)) return m.scanRaw(src) } } diff --git a/vendor/github.com/uptrace/bun/model_table_has_many.go b/vendor/github.com/uptrace/bun/model_table_has_many.go index 3d8a5da6f..544cdf5d6 100644 --- a/vendor/github.com/uptrace/bun/model_table_has_many.go +++ b/vendor/github.com/uptrace/bun/model_table_has_many.go @@ -24,7 +24,7 @@ type hasManyModel struct { func newHasManyModel(j *relationJoin) *hasManyModel { baseTable := j.BaseModel.Table() joinModel := j.JoinModel.(*sliceTableModel) - baseValues := baseValues(joinModel, j.Relation.BaseFields) + baseValues := baseValues(joinModel, j.Relation.BasePKs) if len(baseValues) == 0 { return nil } @@ -92,9 +92,9 @@ func (m *hasManyModel) Scan(src interface{}) error { return err } - for _, f := range m.rel.JoinFields { + for _, f := range m.rel.JoinPKs { if f.Name == field.Name { - m.structKey = append(m.structKey, field.Value(m.strct).Interface()) + m.structKey = append(m.structKey, indirectFieldValue(field.Value(m.strct))) break } } @@ -103,6 +103,7 @@ func (m *hasManyModel) Scan(src interface{}) error { } func (m *hasManyModel) parkStruct() error { + baseValues, ok := m.baseValues[internal.NewMapKey(m.structKey)] if !ok { return fmt.Errorf( @@ -143,7 +144,19 @@ func baseValues(model TableModel, fields []*schema.Field) map[internal.MapKey][] func modelKey(key []interface{}, strct reflect.Value, fields []*schema.Field) []interface{} { for _, f := range fields { - key = append(key, f.Value(strct).Interface()) + key = append(key, indirectFieldValue(f.Value(strct))) } return key } + +// indirectFieldValue return the field value dereferencing the pointer if necessary. +// The value is then used as a map key. +func indirectFieldValue(field reflect.Value) interface{} { + if field.Kind() != reflect.Ptr { + return field.Interface() + } + if field.IsNil() { + return nil + } + return field.Elem().Interface() +} diff --git a/vendor/github.com/uptrace/bun/model_table_m2m.go b/vendor/github.com/uptrace/bun/model_table_m2m.go index 88d8a1268..14d385e62 100644 --- a/vendor/github.com/uptrace/bun/model_table_m2m.go +++ b/vendor/github.com/uptrace/bun/model_table_m2m.go @@ -24,7 +24,7 @@ type m2mModel struct { func newM2MModel(j *relationJoin) *m2mModel { baseTable := j.BaseModel.Table() joinModel := j.JoinModel.(*sliceTableModel) - baseValues := baseValues(joinModel, baseTable.PKs) + baseValues := baseValues(joinModel, j.Relation.BasePKs) if len(baseValues) == 0 { return nil } @@ -83,27 +83,21 @@ func (m *m2mModel) Scan(src interface{}) error { column := m.columns[m.scanIndex] m.scanIndex++ - field, ok := m.table.FieldMap[column] - if !ok { + // Base pks must come first. + if m.scanIndex <= len(m.rel.M2MBasePKs) { return m.scanM2MColumn(column, src) } - if err := field.ScanValue(m.strct, src); err != nil { - return err + if field, ok := m.table.FieldMap[column]; ok { + return field.ScanValue(m.strct, src) } - for _, fk := range m.rel.M2MBaseFields { - if fk.Name == field.Name { - m.structKey = append(m.structKey, field.Value(m.strct).Interface()) - break - } - } - - return nil + _, err := m.scanColumn(column, src) + return err } func (m *m2mModel) scanM2MColumn(column string, src interface{}) error { - for _, field := range m.rel.M2MBaseFields { + for _, field := range m.rel.M2MBasePKs { if field.Name == column { dest := reflect.New(field.IndirectType).Elem() if err := field.Scan(dest, src); err != nil { diff --git a/vendor/github.com/uptrace/bun/model_table_struct.go b/vendor/github.com/uptrace/bun/model_table_struct.go index a5c9a7bc3..a8860908e 100644 --- a/vendor/github.com/uptrace/bun/model_table_struct.go +++ b/vendor/github.com/uptrace/bun/model_table_struct.go @@ -242,7 +242,7 @@ func (m *structTableModel) ScanRows(ctx context.Context, rows *sql.Rows) (int, e n++ // And discard the rest. This is especially important for SQLite3, which can return - // a row like it was inserted sucessfully and then return an actual error for the next row. + // a row like it was inserted successfully and then return an actual error for the next row. // See issues/100. for rows.Next() { n++ diff --git a/vendor/github.com/uptrace/bun/package.json b/vendor/github.com/uptrace/bun/package.json index 331e4be8b..6a8d7082e 100644 --- a/vendor/github.com/uptrace/bun/package.json +++ b/vendor/github.com/uptrace/bun/package.json @@ -1,6 +1,6 @@ { "name": "gobun", - "version": "1.2.1", + "version": "1.2.5", "main": "index.js", "repository": "git@github.com:uptrace/bun.git", "author": "Vladimir Mihailenco ", diff --git a/vendor/github.com/uptrace/bun/query_base.go b/vendor/github.com/uptrace/bun/query_base.go index 2321a7537..8a26a4c8a 100644 --- a/vendor/github.com/uptrace/bun/query_base.go +++ b/vendor/github.com/uptrace/bun/query_base.go @@ -8,6 +8,7 @@ "fmt" "time" + "github.com/uptrace/bun/dialect" "github.com/uptrace/bun/dialect/feature" "github.com/uptrace/bun/internal" "github.com/uptrace/bun/schema" @@ -418,7 +419,11 @@ func (q *baseQuery) _appendTables( } else { b = fmter.AppendQuery(b, string(q.table.SQLNameForSelects)) if withAlias && q.table.SQLAlias != q.table.SQLNameForSelects { - b = append(b, " AS "...) + if q.db.dialect.Name() == dialect.Oracle { + b = append(b, ' ') + } else { + b = append(b, " AS "...) + } b = append(b, q.table.SQLAlias...) } } diff --git a/vendor/github.com/uptrace/bun/query_select.go b/vendor/github.com/uptrace/bun/query_select.go index c0e145110..5bb329143 100644 --- a/vendor/github.com/uptrace/bun/query_select.go +++ b/vendor/github.com/uptrace/bun/query_select.go @@ -538,6 +538,11 @@ func (q *SelectQuery) appendQuery( if count && !cteCount { b = append(b, "count(*)"...) } else { + // MSSQL: allows Limit() without Order() as per https://stackoverflow.com/a/36156953 + if q.limit > 0 && len(q.order) == 0 && fmter.Dialect().Name() == dialect.MSSQL { + b = append(b, "0 AS _temp_sort, "...) + } + b, err = q.appendColumns(fmter, b) if err != nil { return nil, err @@ -564,8 +569,8 @@ func (q *SelectQuery) appendQuery( return nil, err } - for _, j := range q.joins { - b, err = j.AppendQuery(fmter, b) + for _, join := range q.joins { + b, err = join.AppendQuery(fmter, b) if err != nil { return nil, err } @@ -793,6 +798,12 @@ func (q *SelectQuery) appendOrder(fmter schema.Formatter, b []byte) (_ []byte, e return b, nil } + + // MSSQL: allows Limit() without Order() as per https://stackoverflow.com/a/36156953 + if q.limit > 0 && fmter.Dialect().Name() == dialect.MSSQL { + return append(b, " ORDER BY _temp_sort"...), nil + } + return b, nil } @@ -856,52 +867,57 @@ func (q *SelectQuery) Exec(ctx context.Context, dest ...interface{}) (res sql.Re } func (q *SelectQuery) Scan(ctx context.Context, dest ...interface{}) error { + _, err := q.scanResult(ctx, dest...) + return err +} + +func (q *SelectQuery) scanResult(ctx context.Context, dest ...interface{}) (sql.Result, error) { if q.err != nil { - return q.err + return nil, q.err } model, err := q.getModel(dest) if err != nil { - return err + return nil, err } if q.table != nil { if err := q.beforeSelectHook(ctx); err != nil { - return err + return nil, err } } if err := q.beforeAppendModel(ctx, q); err != nil { - return err + return nil, err } queryBytes, err := q.AppendQuery(q.db.fmter, q.db.makeQueryBytes()) if err != nil { - return err + return nil, err } query := internal.String(queryBytes) res, err := q.scan(ctx, q, query, model, true) if err != nil { - return err + return nil, err } if n, _ := res.RowsAffected(); n > 0 { if tableModel, ok := model.(TableModel); ok { if err := q.selectJoins(ctx, tableModel.getJoins()); err != nil { - return err + return nil, err } } } if q.table != nil { if err := q.afterSelectHook(ctx); err != nil { - return err + return nil, err } } - return nil + return res, nil } func (q *SelectQuery) beforeSelectHook(ctx context.Context) error { @@ -946,6 +962,16 @@ func (q *SelectQuery) Count(ctx context.Context) (int, error) { } func (q *SelectQuery) ScanAndCount(ctx context.Context, dest ...interface{}) (int, error) { + if q.offset == 0 && q.limit == 0 { + // If there is no limit and offset, we can use a single query to get the count and scan + if res, err := q.scanResult(ctx, dest...); err != nil { + return 0, err + } else if n, err := res.RowsAffected(); err != nil { + return 0, err + } else { + return int(n), nil + } + } if _, ok := q.conn.(*DB); ok { return q.scanAndCountConc(ctx, dest...) } diff --git a/vendor/github.com/uptrace/bun/query_table_create.go b/vendor/github.com/uptrace/bun/query_table_create.go index 3d98da07b..aeb79cd37 100644 --- a/vendor/github.com/uptrace/bun/query_table_create.go +++ b/vendor/github.com/uptrace/bun/query_table_create.go @@ -9,6 +9,7 @@ "strconv" "strings" + "github.com/uptrace/bun/dialect" "github.com/uptrace/bun/dialect/feature" "github.com/uptrace/bun/dialect/sqltype" "github.com/uptrace/bun/internal" @@ -165,7 +166,7 @@ func (q *CreateTableQuery) AppendQuery(fmter schema.Formatter, b []byte) (_ []by b = append(b, field.SQLName...) b = append(b, " "...) b = q.appendSQLType(b, field) - if field.NotNull { + if field.NotNull && q.db.dialect.Name() != dialect.Oracle { b = append(b, " NOT NULL"...) } @@ -246,7 +247,11 @@ func (q *CreateTableQuery) appendSQLType(b []byte, field *schema.Field) []byte { return append(b, field.CreateTableSQLType...) } - b = append(b, sqltype.VarChar...) + if q.db.dialect.Name() == dialect.Oracle { + b = append(b, "VARCHAR2"...) + } else { + b = append(b, sqltype.VarChar...) + } b = append(b, "("...) b = strconv.AppendInt(b, int64(q.varchar), 10) b = append(b, ")"...) @@ -297,9 +302,9 @@ func (q *CreateTableQuery) appendFKConstraintsRel(fmter schema.Formatter, b []by b, err = q.appendFK(fmter, b, schema.QueryWithArgs{ Query: "(?) REFERENCES ? (?) ? ?", Args: []interface{}{ - Safe(appendColumns(nil, "", rel.BaseFields)), + Safe(appendColumns(nil, "", rel.BasePKs)), rel.JoinTable.SQLName, - Safe(appendColumns(nil, "", rel.JoinFields)), + Safe(appendColumns(nil, "", rel.JoinPKs)), Safe(rel.OnUpdate), Safe(rel.OnDelete), }, diff --git a/vendor/github.com/uptrace/bun/query_table_truncate.go b/vendor/github.com/uptrace/bun/query_table_truncate.go index a704b7b10..9ac5599d9 100644 --- a/vendor/github.com/uptrace/bun/query_table_truncate.go +++ b/vendor/github.com/uptrace/bun/query_table_truncate.go @@ -57,6 +57,11 @@ func (q *TruncateTableQuery) TableExpr(query string, args ...interface{}) *Trunc return q } +func (q *TruncateTableQuery) ModelTableExpr(query string, args ...interface{}) *TruncateTableQuery { + q.modelTableName = schema.SafeQuery(query, args) + return q +} + //------------------------------------------------------------------------------ func (q *TruncateTableQuery) ContinueIdentity() *TruncateTableQuery { diff --git a/vendor/github.com/uptrace/bun/relation_join.go b/vendor/github.com/uptrace/bun/relation_join.go index ba542666d..487f776ed 100644 --- a/vendor/github.com/uptrace/bun/relation_join.go +++ b/vendor/github.com/uptrace/bun/relation_join.go @@ -70,11 +70,11 @@ func (j *relationJoin) manyQuery(q *SelectQuery) *SelectQuery { } func (j *relationJoin) manyQueryCompositeIn(where []byte, q *SelectQuery) *SelectQuery { - if len(j.Relation.JoinFields) > 1 { + if len(j.Relation.JoinPKs) > 1 { where = append(where, '(') } - where = appendColumns(where, j.JoinModel.Table().SQLAlias, j.Relation.JoinFields) - if len(j.Relation.JoinFields) > 1 { + where = appendColumns(where, j.JoinModel.Table().SQLAlias, j.Relation.JoinPKs) + if len(j.Relation.JoinPKs) > 1 { where = append(where, ')') } where = append(where, " IN ("...) @@ -83,7 +83,7 @@ func (j *relationJoin) manyQueryCompositeIn(where []byte, q *SelectQuery) *Selec where, j.JoinModel.rootValue(), j.JoinModel.parentIndex(), - j.Relation.BaseFields, + j.Relation.BasePKs, ) where = append(where, ")"...) q = q.Where(internal.String(where)) @@ -104,8 +104,8 @@ func (j *relationJoin) manyQueryMulti(where []byte, q *SelectQuery) *SelectQuery where, j.JoinModel.rootValue(), j.JoinModel.parentIndex(), - j.Relation.BaseFields, - j.Relation.JoinFields, + j.Relation.BasePKs, + j.Relation.JoinPKs, j.JoinModel.Table().SQLAlias, ) @@ -175,10 +175,10 @@ func (j *relationJoin) m2mQuery(q *SelectQuery) *SelectQuery { q = q.Model(m2mModel) index := j.JoinModel.parentIndex() - baseTable := j.BaseModel.Table() if j.Relation.M2MTable != nil { - fields := append(j.Relation.M2MBaseFields, j.Relation.M2MJoinFields...) + // We only need base pks to park joined models to the base model. + fields := j.Relation.M2MBasePKs b := make([]byte, 0, len(fields)) b = appendColumns(b, j.Relation.M2MTable.SQLAlias, fields) @@ -193,7 +193,7 @@ func (j *relationJoin) m2mQuery(q *SelectQuery) *SelectQuery { join = append(join, " AS "...) join = append(join, j.Relation.M2MTable.SQLAlias...) join = append(join, " ON ("...) - for i, col := range j.Relation.M2MBaseFields { + for i, col := range j.Relation.M2MBasePKs { if i > 0 { join = append(join, ", "...) } @@ -202,13 +202,13 @@ func (j *relationJoin) m2mQuery(q *SelectQuery) *SelectQuery { join = append(join, col.SQLName...) } join = append(join, ") IN ("...) - join = appendChildValues(fmter, join, j.BaseModel.rootValue(), index, baseTable.PKs) + join = appendChildValues(fmter, join, j.BaseModel.rootValue(), index, j.Relation.BasePKs) join = append(join, ")"...) q = q.Join(internal.String(join)) joinTable := j.JoinModel.Table() - for i, m2mJoinField := range j.Relation.M2MJoinFields { - joinField := j.Relation.JoinFields[i] + for i, m2mJoinField := range j.Relation.M2MJoinPKs { + joinField := j.Relation.JoinPKs[i] q = q.Where("?.? = ?.?", joinTable.SQLAlias, joinField.SQLName, j.Relation.M2MTable.SQLAlias, m2mJoinField.SQLName) @@ -310,13 +310,13 @@ func (j *relationJoin) appendHasOneJoin( b = append(b, " ON "...) b = append(b, '(') - for i, baseField := range j.Relation.BaseFields { + for i, baseField := range j.Relation.BasePKs { if i > 0 { b = append(b, " AND "...) } b = j.appendAlias(fmter, b) b = append(b, '.') - b = append(b, j.Relation.JoinFields[i].SQLName...) + b = append(b, j.Relation.JoinPKs[i].SQLName...) b = append(b, " = "...) b = j.appendBaseAlias(fmter, b) b = append(b, '.') @@ -367,13 +367,13 @@ func appendChildValues( } // appendMultiValues is an alternative to appendChildValues that doesn't use the sql keyword ID -// but instead use a old style ((k1=v1) AND (k2=v2)) OR (...) of conditions. +// but instead uses old style ((k1=v1) AND (k2=v2)) OR (...) conditions. func appendMultiValues( fmter schema.Formatter, b []byte, v reflect.Value, index []int, baseFields, joinFields []*schema.Field, joinTable schema.Safe, ) []byte { // This is based on a mix of appendChildValues and query_base.appendColumns - // These should never missmatch in length but nice to know if it does + // These should never mismatch in length but nice to know if it does if len(joinFields) != len(baseFields) { panic("not reached") } diff --git a/vendor/github.com/uptrace/bun/schema/append_value.go b/vendor/github.com/uptrace/bun/schema/append_value.go index 9f0782e0f..a67b41e38 100644 --- a/vendor/github.com/uptrace/bun/schema/append_value.go +++ b/vendor/github.com/uptrace/bun/schema/append_value.go @@ -7,9 +7,9 @@ "reflect" "strconv" "strings" - "sync" "time" + "github.com/puzpuzpuz/xsync/v3" "github.com/uptrace/bun/dialect" "github.com/uptrace/bun/dialect/sqltype" "github.com/uptrace/bun/extra/bunjson" @@ -51,7 +51,7 @@ reflect.UnsafePointer: nil, } -var appenderMap sync.Map +var appenderCache = xsync.NewMapOf[reflect.Type, AppenderFunc]() func FieldAppender(dialect Dialect, field *Field) AppenderFunc { if field.Tag.HasOption("msgpack") { @@ -67,7 +67,7 @@ func FieldAppender(dialect Dialect, field *Field) AppenderFunc { } if fieldType.Kind() != reflect.Ptr { - if reflect.PtrTo(fieldType).Implements(driverValuerType) { + if reflect.PointerTo(fieldType).Implements(driverValuerType) { return addrAppender(appendDriverValue) } } @@ -79,14 +79,14 @@ func FieldAppender(dialect Dialect, field *Field) AppenderFunc { } func Appender(dialect Dialect, typ reflect.Type) AppenderFunc { - if v, ok := appenderMap.Load(typ); ok { - return v.(AppenderFunc) + if v, ok := appenderCache.Load(typ); ok { + return v } fn := appender(dialect, typ) - if v, ok := appenderMap.LoadOrStore(typ, fn); ok { - return v.(AppenderFunc) + if v, ok := appenderCache.LoadOrStore(typ, fn); ok { + return v } return fn } @@ -99,10 +99,10 @@ func appender(dialect Dialect, typ reflect.Type) AppenderFunc { return appendTimeValue case timePtrType: return PtrAppender(appendTimeValue) - case ipType: - return appendIPValue case ipNetType: return appendIPNetValue + case ipType, netipPrefixType, netipAddrType: + return appendStringer case jsonRawMessageType: return appendJSONRawMessageValue } @@ -123,7 +123,7 @@ func appender(dialect Dialect, typ reflect.Type) AppenderFunc { } if kind != reflect.Ptr { - ptr := reflect.PtrTo(typ) + ptr := reflect.PointerTo(typ) if ptr.Implements(queryAppenderType) { return addrAppender(appendQueryAppenderValue) } @@ -247,16 +247,15 @@ func appendTimeValue(fmter Formatter, b []byte, v reflect.Value) []byte { return fmter.Dialect().AppendTime(b, tm) } -func appendIPValue(fmter Formatter, b []byte, v reflect.Value) []byte { - ip := v.Interface().(net.IP) - return fmter.Dialect().AppendString(b, ip.String()) -} - func appendIPNetValue(fmter Formatter, b []byte, v reflect.Value) []byte { ipnet := v.Interface().(net.IPNet) return fmter.Dialect().AppendString(b, ipnet.String()) } +func appendStringer(fmter Formatter, b []byte, v reflect.Value) []byte { + return fmter.Dialect().AppendString(b, v.Interface().(fmt.Stringer).String()) +} + func appendJSONRawMessageValue(fmter Formatter, b []byte, v reflect.Value) []byte { bytes := v.Bytes() if bytes == nil { diff --git a/vendor/github.com/uptrace/bun/schema/dialect.go b/vendor/github.com/uptrace/bun/schema/dialect.go index 8814313f7..330293444 100644 --- a/vendor/github.com/uptrace/bun/schema/dialect.go +++ b/vendor/github.com/uptrace/bun/schema/dialect.go @@ -118,7 +118,7 @@ func (BaseDialect) AppendJSON(b, jsonb []byte) []byte { case '\000': continue case '\\': - if p.SkipBytes([]byte("u0000")) { + if p.CutPrefix([]byte("u0000")) { b = append(b, `\\u0000`...) } else { b = append(b, '\\') diff --git a/vendor/github.com/uptrace/bun/schema/reflect.go b/vendor/github.com/uptrace/bun/schema/reflect.go index 89be8eeb6..75980b102 100644 --- a/vendor/github.com/uptrace/bun/schema/reflect.go +++ b/vendor/github.com/uptrace/bun/schema/reflect.go @@ -4,6 +4,7 @@ "database/sql/driver" "encoding/json" "net" + "net/netip" "reflect" "time" ) @@ -14,6 +15,8 @@ timeType = timePtrType.Elem() ipType = reflect.TypeOf((*net.IP)(nil)).Elem() ipNetType = reflect.TypeOf((*net.IPNet)(nil)).Elem() + netipPrefixType = reflect.TypeOf((*netip.Prefix)(nil)).Elem() + netipAddrType = reflect.TypeOf((*netip.Addr)(nil)).Elem() jsonRawMessageType = reflect.TypeOf((*json.RawMessage)(nil)).Elem() driverValuerType = reflect.TypeOf((*driver.Valuer)(nil)).Elem() diff --git a/vendor/github.com/uptrace/bun/schema/relation.go b/vendor/github.com/uptrace/bun/schema/relation.go index 9eb74f7e9..f653cd7a3 100644 --- a/vendor/github.com/uptrace/bun/schema/relation.go +++ b/vendor/github.com/uptrace/bun/schema/relation.go @@ -13,21 +13,25 @@ ) type Relation struct { - Type int - Field *Field - JoinTable *Table - BaseFields []*Field - JoinFields []*Field - OnUpdate string - OnDelete string - Condition []string + // Base and Join can be explained with this query: + // + // SELECT * FROM base_table JOIN join_table + + Type int + Field *Field + JoinTable *Table + BasePKs []*Field + JoinPKs []*Field + OnUpdate string + OnDelete string + Condition []string PolymorphicField *Field PolymorphicValue string - M2MTable *Table - M2MBaseFields []*Field - M2MJoinFields []*Field + M2MTable *Table + M2MBasePKs []*Field + M2MJoinPKs []*Field } // References returns true if the table to which the Relation belongs needs to declare a foreign key constraint to create the relation. diff --git a/vendor/github.com/uptrace/bun/schema/scan.go b/vendor/github.com/uptrace/bun/schema/scan.go index 96b31caf3..4da160daf 100644 --- a/vendor/github.com/uptrace/bun/schema/scan.go +++ b/vendor/github.com/uptrace/bun/schema/scan.go @@ -8,9 +8,9 @@ "reflect" "strconv" "strings" - "sync" "time" + "github.com/puzpuzpuz/xsync/v3" "github.com/vmihailenco/msgpack/v5" "github.com/uptrace/bun/dialect/sqltype" @@ -53,7 +53,7 @@ func init() { } } -var scannerMap sync.Map +var scannerCache = xsync.NewMapOf[reflect.Type, ScannerFunc]() func FieldScanner(dialect Dialect, field *Field) ScannerFunc { if field.Tag.HasOption("msgpack") { @@ -72,14 +72,14 @@ func FieldScanner(dialect Dialect, field *Field) ScannerFunc { } func Scanner(typ reflect.Type) ScannerFunc { - if v, ok := scannerMap.Load(typ); ok { - return v.(ScannerFunc) + if v, ok := scannerCache.Load(typ); ok { + return v } fn := scanner(typ) - if v, ok := scannerMap.LoadOrStore(typ, fn); ok { - return v.(ScannerFunc) + if v, ok := scannerCache.LoadOrStore(typ, fn); ok { + return v } return fn } @@ -111,7 +111,7 @@ func scanner(typ reflect.Type) ScannerFunc { } if kind != reflect.Ptr { - ptr := reflect.PtrTo(typ) + ptr := reflect.PointerTo(typ) if ptr.Implements(scannerType) { return addrScanner(scanScanner) } diff --git a/vendor/github.com/uptrace/bun/schema/table.go b/vendor/github.com/uptrace/bun/schema/table.go index 0a23156a2..c8e71e38f 100644 --- a/vendor/github.com/uptrace/bun/schema/table.go +++ b/vendor/github.com/uptrace/bun/schema/table.go @@ -74,16 +74,7 @@ type structField struct { Table *Table } -func newTable( - dialect Dialect, typ reflect.Type, seen map[reflect.Type]*Table, canAddr bool, -) *Table { - if table, ok := seen[typ]; ok { - return table - } - - table := new(Table) - seen[typ] = table - +func (table *Table) init(dialect Dialect, typ reflect.Type, canAddr bool) { table.dialect = dialect table.Type = typ table.ZeroValue = reflect.New(table.Type).Elem() @@ -97,7 +88,7 @@ func newTable( table.Fields = make([]*Field, 0, typ.NumField()) table.FieldMap = make(map[string]*Field, typ.NumField()) - table.processFields(typ, seen, canAddr) + table.processFields(typ, canAddr) hooks := []struct { typ reflect.Type @@ -109,28 +100,15 @@ func newTable( {afterScanRowHookType, afterScanRowHookFlag}, } - typ = reflect.PtrTo(table.Type) + typ = reflect.PointerTo(table.Type) for _, hook := range hooks { if typ.Implements(hook.typ) { table.flags = table.flags.Set(hook.flag) } } - - return table } -func (t *Table) init() { - for _, field := range t.relFields { - t.processRelation(field) - } - t.relFields = nil -} - -func (t *Table) processFields( - typ reflect.Type, - seen map[reflect.Type]*Table, - canAddr bool, -) { +func (t *Table) processFields(typ reflect.Type, canAddr bool) { type embeddedField struct { prefix string index []int @@ -172,7 +150,7 @@ type embeddedField struct { continue } - subtable := newTable(t.dialect, sfType, seen, canAddr) + subtable := t.dialect.Tables().InProgress(sfType) for _, subfield := range subtable.allFields { embedded = append(embedded, embeddedField{ @@ -206,7 +184,7 @@ type embeddedField struct { t.TypeName, sf.Name, fieldType.Kind())) } - subtable := newTable(t.dialect, fieldType, seen, canAddr) + subtable := t.dialect.Tables().InProgress(fieldType) for _, subfield := range subtable.allFields { embedded = append(embedded, embeddedField{ prefix: prefix, @@ -229,7 +207,7 @@ type embeddedField struct { } t.StructMap[field.Name] = &structField{ Index: field.Index, - Table: newTable(t.dialect, field.IndirectType, seen, canAddr), + Table: t.dialect.Tables().InProgress(field.IndirectType), } } } @@ -423,6 +401,10 @@ func (t *Table) newField(sf reflect.StructField, tag tagparser.Tag) *Field { sqlName = tag.Name } + if s, ok := tag.Option("column"); ok { + sqlName = s + } + for name := range tag.Options { if !isKnownFieldOption(name) { internal.Warn.Printf("%s.%s has unknown tag option: %q", t.TypeName, sf.Name, name) @@ -490,6 +472,13 @@ func (t *Table) newField(sf reflect.StructField, tag tagparser.Tag) *Field { //--------------------------------------------------------------------------------------- +func (t *Table) initRelations() { + for _, field := range t.relFields { + t.processRelation(field) + } + t.relFields = nil +} + func (t *Table) processRelation(field *Field) { if rel, ok := field.Tag.Option("rel"); ok { t.initRelation(field, rel) @@ -577,7 +566,7 @@ func (t *Table) belongsToRelation(field *Field) *Relation { joinColumn := joinColumns[i] if f := t.FieldMap[baseColumn]; f != nil { - rel.BaseFields = append(rel.BaseFields, f) + rel.BasePKs = append(rel.BasePKs, f) } else { panic(fmt.Errorf( "bun: %s belongs-to %s: %s must have column %s", @@ -586,7 +575,7 @@ func (t *Table) belongsToRelation(field *Field) *Relation { } if f := joinTable.FieldMap[joinColumn]; f != nil { - rel.JoinFields = append(rel.JoinFields, f) + rel.JoinPKs = append(rel.JoinPKs, f) } else { panic(fmt.Errorf( "bun: %s belongs-to %s: %s must have column %s", @@ -597,17 +586,17 @@ func (t *Table) belongsToRelation(field *Field) *Relation { return rel } - rel.JoinFields = joinTable.PKs + rel.JoinPKs = joinTable.PKs fkPrefix := internal.Underscore(field.GoName) + "_" for _, joinPK := range joinTable.PKs { fkName := fkPrefix + joinPK.Name if fk := t.FieldMap[fkName]; fk != nil { - rel.BaseFields = append(rel.BaseFields, fk) + rel.BasePKs = append(rel.BasePKs, fk) continue } if fk := t.FieldMap[joinPK.Name]; fk != nil { - rel.BaseFields = append(rel.BaseFields, fk) + rel.BasePKs = append(rel.BasePKs, fk) continue } @@ -640,7 +629,7 @@ func (t *Table) hasOneRelation(field *Field) *Relation { baseColumns, joinColumns := parseRelationJoin(join) for i, baseColumn := range baseColumns { if f := t.FieldMap[baseColumn]; f != nil { - rel.BaseFields = append(rel.BaseFields, f) + rel.BasePKs = append(rel.BasePKs, f) } else { panic(fmt.Errorf( "bun: %s has-one %s: %s must have column %s", @@ -650,7 +639,7 @@ func (t *Table) hasOneRelation(field *Field) *Relation { joinColumn := joinColumns[i] if f := joinTable.FieldMap[joinColumn]; f != nil { - rel.JoinFields = append(rel.JoinFields, f) + rel.JoinPKs = append(rel.JoinPKs, f) } else { panic(fmt.Errorf( "bun: %s has-one %s: %s must have column %s", @@ -661,17 +650,17 @@ func (t *Table) hasOneRelation(field *Field) *Relation { return rel } - rel.BaseFields = t.PKs + rel.BasePKs = t.PKs fkPrefix := internal.Underscore(t.ModelName) + "_" for _, pk := range t.PKs { fkName := fkPrefix + pk.Name if f := joinTable.FieldMap[fkName]; f != nil { - rel.JoinFields = append(rel.JoinFields, f) + rel.JoinPKs = append(rel.JoinPKs, f) continue } if f := joinTable.FieldMap[pk.Name]; f != nil { - rel.JoinFields = append(rel.JoinFields, f) + rel.JoinPKs = append(rel.JoinPKs, f) continue } @@ -720,7 +709,7 @@ func (t *Table) hasManyRelation(field *Field) *Relation { } if f := t.FieldMap[baseColumn]; f != nil { - rel.BaseFields = append(rel.BaseFields, f) + rel.BasePKs = append(rel.BasePKs, f) } else { panic(fmt.Errorf( "bun: %s has-many %s: %s must have column %s", @@ -729,7 +718,7 @@ func (t *Table) hasManyRelation(field *Field) *Relation { } if f := joinTable.FieldMap[joinColumn]; f != nil { - rel.JoinFields = append(rel.JoinFields, f) + rel.JoinPKs = append(rel.JoinPKs, f) } else { panic(fmt.Errorf( "bun: %s has-many %s: %s must have column %s", @@ -738,7 +727,7 @@ func (t *Table) hasManyRelation(field *Field) *Relation { } } } else { - rel.BaseFields = t.PKs + rel.BasePKs = t.PKs fkPrefix := internal.Underscore(t.ModelName) + "_" if isPolymorphic { polymorphicColumn = fkPrefix + "type" @@ -747,12 +736,12 @@ func (t *Table) hasManyRelation(field *Field) *Relation { for _, pk := range t.PKs { joinColumn := fkPrefix + pk.Name if fk := joinTable.FieldMap[joinColumn]; fk != nil { - rel.JoinFields = append(rel.JoinFields, fk) + rel.JoinPKs = append(rel.JoinPKs, fk) continue } if fk := joinTable.FieldMap[pk.Name]; fk != nil { - rel.JoinFields = append(rel.JoinFields, fk) + rel.JoinPKs = append(rel.JoinPKs, fk) continue } @@ -852,12 +841,12 @@ func (t *Table) m2mRelation(field *Field) *Relation { } leftRel := m2mTable.belongsToRelation(leftField) - rel.BaseFields = leftRel.JoinFields - rel.M2MBaseFields = leftRel.BaseFields + rel.BasePKs = leftRel.JoinPKs + rel.M2MBasePKs = leftRel.BasePKs rightRel := m2mTable.belongsToRelation(rightField) - rel.JoinFields = rightRel.JoinFields - rel.M2MJoinFields = rightRel.BaseFields + rel.JoinPKs = rightRel.JoinPKs + rel.M2MJoinPKs = rightRel.BasePKs return rel } @@ -918,6 +907,7 @@ func isKnownFieldOption(name string) bool { "array", "hstore", "composite", + "multirange", "json_use_number", "msgpack", "notnull", diff --git a/vendor/github.com/uptrace/bun/schema/tables.go b/vendor/github.com/uptrace/bun/schema/tables.go index 19aff8606..985093421 100644 --- a/vendor/github.com/uptrace/bun/schema/tables.go +++ b/vendor/github.com/uptrace/bun/schema/tables.go @@ -4,22 +4,24 @@ "fmt" "reflect" "sync" + + "github.com/puzpuzpuz/xsync/v3" ) type Tables struct { dialect Dialect - tables sync.Map - mu sync.RWMutex - seen map[reflect.Type]*Table - inProgress map[reflect.Type]*tableInProgress + mu sync.Mutex + tables *xsync.MapOf[reflect.Type, *Table] + + inProgress map[reflect.Type]*Table } func NewTables(dialect Dialect) *Tables { return &Tables{ dialect: dialect, - seen: make(map[reflect.Type]*Table), - inProgress: make(map[reflect.Type]*tableInProgress), + tables: xsync.NewMapOf[reflect.Type, *Table](), + inProgress: make(map[reflect.Type]*Table), } } @@ -30,58 +32,26 @@ func (t *Tables) Register(models ...interface{}) { } func (t *Tables) Get(typ reflect.Type) *Table { - return t.table(typ, false) -} - -func (t *Tables) InProgress(typ reflect.Type) *Table { - return t.table(typ, true) -} - -func (t *Tables) table(typ reflect.Type, allowInProgress bool) *Table { typ = indirectType(typ) if typ.Kind() != reflect.Struct { panic(fmt.Errorf("got %s, wanted %s", typ.Kind(), reflect.Struct)) } if v, ok := t.tables.Load(typ); ok { - return v.(*Table) + return v } t.mu.Lock() + defer t.mu.Unlock() if v, ok := t.tables.Load(typ); ok { - t.mu.Unlock() - return v.(*Table) + return v } - var table *Table - - inProgress := t.inProgress[typ] - if inProgress == nil { - table = newTable(t.dialect, typ, t.seen, false) - inProgress = newTableInProgress(table) - t.inProgress[typ] = inProgress - } else { - table = inProgress.table - } - - t.mu.Unlock() - - if allowInProgress { - return table - } - - if !inProgress.init() { - return table - } - - t.mu.Lock() - delete(t.inProgress, typ) - t.tables.Store(typ, table) - t.mu.Unlock() + table := t.InProgress(typ) + table.initRelations() t.dialect.OnTable(table) - for _, field := range table.FieldMap { if field.UserSQLType == "" { field.UserSQLType = field.DiscoveredSQLType @@ -91,15 +61,27 @@ func (t *Tables) table(typ reflect.Type, allowInProgress bool) *Table { } } + t.tables.Store(typ, table) + return table +} + +func (t *Tables) InProgress(typ reflect.Type) *Table { + if table, ok := t.inProgress[typ]; ok { + return table + } + + table := new(Table) + t.inProgress[typ] = table + table.init(t.dialect, typ, false) + return table } func (t *Tables) ByModel(name string) *Table { var found *Table - t.tables.Range(func(key, value interface{}) bool { - t := value.(*Table) - if t.TypeName == name { - found = t + t.tables.Range(func(typ reflect.Type, table *Table) bool { + if table.TypeName == name { + found = table return false } return true @@ -109,34 +91,12 @@ func (t *Tables) ByModel(name string) *Table { func (t *Tables) ByName(name string) *Table { var found *Table - t.tables.Range(func(key, value interface{}) bool { - t := value.(*Table) - if t.Name == name { - found = t + t.tables.Range(func(typ reflect.Type, table *Table) bool { + if table.Name == name { + found = table return false } return true }) return found } - -type tableInProgress struct { - table *Table - - initOnce sync.Once -} - -func newTableInProgress(table *Table) *tableInProgress { - return &tableInProgress{ - table: table, - } -} - -func (inp *tableInProgress) init() bool { - var inited bool - inp.initOnce.Do(func() { - inp.table.init() - inited = true - }) - return inited -} diff --git a/vendor/github.com/uptrace/bun/schema/zerochecker.go b/vendor/github.com/uptrace/bun/schema/zerochecker.go index f24e51d30..7c1f088c1 100644 --- a/vendor/github.com/uptrace/bun/schema/zerochecker.go +++ b/vendor/github.com/uptrace/bun/schema/zerochecker.go @@ -60,7 +60,7 @@ func zeroChecker(typ reflect.Type) IsZeroerFunc { kind := typ.Kind() if kind != reflect.Ptr { - ptr := reflect.PtrTo(typ) + ptr := reflect.PointerTo(typ) if ptr.Implements(isZeroerType) { return addrChecker(isZeroInterface) } diff --git a/vendor/github.com/uptrace/bun/version.go b/vendor/github.com/uptrace/bun/version.go index be6c67f30..7f23c12c3 100644 --- a/vendor/github.com/uptrace/bun/version.go +++ b/vendor/github.com/uptrace/bun/version.go @@ -2,5 +2,5 @@ // Version is the current release version. func Version() string { - return "1.2.1" + return "1.2.5" } diff --git a/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/README.md b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/README.md index 64324c17f..c8886453f 100644 --- a/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/README.md +++ b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/README.md @@ -2,8 +2,9 @@ # database/sql instrumentation for OpenTelemetry Go -[database/sql OpenTelemetry instrumentation](https://uptrace.dev/getinstrument/opentelemetry-database-sql.html) -records database queries (including `Tx` and `Stmt` queries) and reports `DBStats` metrics. +[OpenTelemetry database/sql](https://uptrace.dev/get/instrument/opentelemetry-database-sql.html) +instrumentation records database queries (including `Tx` and `Stmt` queries) and reports `DBStats` +metrics. ## Installation diff --git a/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/version.go b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/version.go index 0eff84d26..2156ada32 100644 --- a/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/version.go +++ b/vendor/github.com/uptrace/opentelemetry-go-extra/otelsql/version.go @@ -2,5 +2,5 @@ // Version is the current release version. func Version() string { - return "0.2.4" + return "0.3.2" } diff --git a/vendor/golang.org/x/crypto/chacha20/chacha_noasm.go b/vendor/golang.org/x/crypto/chacha20/chacha_noasm.go index db42e6676..c709b7284 100644 --- a/vendor/golang.org/x/crypto/chacha20/chacha_noasm.go +++ b/vendor/golang.org/x/crypto/chacha20/chacha_noasm.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build (!arm64 && !s390x && !ppc64le) || !gc || purego +//go:build (!arm64 && !s390x && !ppc64 && !ppc64le) || !gc || purego package chacha20 diff --git a/vendor/golang.org/x/crypto/chacha20/chacha_ppc64le.go b/vendor/golang.org/x/crypto/chacha20/chacha_ppc64x.go similarity index 89% rename from vendor/golang.org/x/crypto/chacha20/chacha_ppc64le.go rename to vendor/golang.org/x/crypto/chacha20/chacha_ppc64x.go index 3a4287f99..bd183d9ba 100644 --- a/vendor/golang.org/x/crypto/chacha20/chacha_ppc64le.go +++ b/vendor/golang.org/x/crypto/chacha20/chacha_ppc64x.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build gc && !purego +//go:build gc && !purego && (ppc64 || ppc64le) package chacha20 diff --git a/vendor/golang.org/x/crypto/chacha20/chacha_ppc64le.s b/vendor/golang.org/x/crypto/chacha20/chacha_ppc64x.s similarity index 76% rename from vendor/golang.org/x/crypto/chacha20/chacha_ppc64le.s rename to vendor/golang.org/x/crypto/chacha20/chacha_ppc64x.s index c672ccf69..a660b4112 100644 --- a/vendor/golang.org/x/crypto/chacha20/chacha_ppc64le.s +++ b/vendor/golang.org/x/crypto/chacha20/chacha_ppc64x.s @@ -19,7 +19,7 @@ // The differences in this and the original implementation are // due to the calling conventions and initialization of constants. -//go:build gc && !purego +//go:build gc && !purego && (ppc64 || ppc64le) #include "textflag.h" @@ -36,32 +36,68 @@ // for VPERMXOR #define MASK R18 -DATA consts<>+0x00(SB)/8, $0x3320646e61707865 -DATA consts<>+0x08(SB)/8, $0x6b20657479622d32 -DATA consts<>+0x10(SB)/8, $0x0000000000000001 -DATA consts<>+0x18(SB)/8, $0x0000000000000000 -DATA consts<>+0x20(SB)/8, $0x0000000000000004 -DATA consts<>+0x28(SB)/8, $0x0000000000000000 -DATA consts<>+0x30(SB)/8, $0x0a0b08090e0f0c0d -DATA consts<>+0x38(SB)/8, $0x0203000106070405 -DATA consts<>+0x40(SB)/8, $0x090a0b080d0e0f0c -DATA consts<>+0x48(SB)/8, $0x0102030005060704 -DATA consts<>+0x50(SB)/8, $0x6170786561707865 -DATA consts<>+0x58(SB)/8, $0x6170786561707865 -DATA consts<>+0x60(SB)/8, $0x3320646e3320646e -DATA consts<>+0x68(SB)/8, $0x3320646e3320646e -DATA consts<>+0x70(SB)/8, $0x79622d3279622d32 -DATA consts<>+0x78(SB)/8, $0x79622d3279622d32 -DATA consts<>+0x80(SB)/8, $0x6b2065746b206574 -DATA consts<>+0x88(SB)/8, $0x6b2065746b206574 -DATA consts<>+0x90(SB)/8, $0x0000000100000000 -DATA consts<>+0x98(SB)/8, $0x0000000300000002 -DATA consts<>+0xa0(SB)/8, $0x5566774411223300 -DATA consts<>+0xa8(SB)/8, $0xddeeffcc99aabb88 -DATA consts<>+0xb0(SB)/8, $0x6677445522330011 -DATA consts<>+0xb8(SB)/8, $0xeeffccddaabb8899 +DATA consts<>+0x00(SB)/4, $0x61707865 +DATA consts<>+0x04(SB)/4, $0x3320646e +DATA consts<>+0x08(SB)/4, $0x79622d32 +DATA consts<>+0x0c(SB)/4, $0x6b206574 +DATA consts<>+0x10(SB)/4, $0x00000001 +DATA consts<>+0x14(SB)/4, $0x00000000 +DATA consts<>+0x18(SB)/4, $0x00000000 +DATA consts<>+0x1c(SB)/4, $0x00000000 +DATA consts<>+0x20(SB)/4, $0x00000004 +DATA consts<>+0x24(SB)/4, $0x00000000 +DATA consts<>+0x28(SB)/4, $0x00000000 +DATA consts<>+0x2c(SB)/4, $0x00000000 +DATA consts<>+0x30(SB)/4, $0x0e0f0c0d +DATA consts<>+0x34(SB)/4, $0x0a0b0809 +DATA consts<>+0x38(SB)/4, $0x06070405 +DATA consts<>+0x3c(SB)/4, $0x02030001 +DATA consts<>+0x40(SB)/4, $0x0d0e0f0c +DATA consts<>+0x44(SB)/4, $0x090a0b08 +DATA consts<>+0x48(SB)/4, $0x05060704 +DATA consts<>+0x4c(SB)/4, $0x01020300 +DATA consts<>+0x50(SB)/4, $0x61707865 +DATA consts<>+0x54(SB)/4, $0x61707865 +DATA consts<>+0x58(SB)/4, $0x61707865 +DATA consts<>+0x5c(SB)/4, $0x61707865 +DATA consts<>+0x60(SB)/4, $0x3320646e +DATA consts<>+0x64(SB)/4, $0x3320646e +DATA consts<>+0x68(SB)/4, $0x3320646e +DATA consts<>+0x6c(SB)/4, $0x3320646e +DATA consts<>+0x70(SB)/4, $0x79622d32 +DATA consts<>+0x74(SB)/4, $0x79622d32 +DATA consts<>+0x78(SB)/4, $0x79622d32 +DATA consts<>+0x7c(SB)/4, $0x79622d32 +DATA consts<>+0x80(SB)/4, $0x6b206574 +DATA consts<>+0x84(SB)/4, $0x6b206574 +DATA consts<>+0x88(SB)/4, $0x6b206574 +DATA consts<>+0x8c(SB)/4, $0x6b206574 +DATA consts<>+0x90(SB)/4, $0x00000000 +DATA consts<>+0x94(SB)/4, $0x00000001 +DATA consts<>+0x98(SB)/4, $0x00000002 +DATA consts<>+0x9c(SB)/4, $0x00000003 +DATA consts<>+0xa0(SB)/4, $0x11223300 +DATA consts<>+0xa4(SB)/4, $0x55667744 +DATA consts<>+0xa8(SB)/4, $0x99aabb88 +DATA consts<>+0xac(SB)/4, $0xddeeffcc +DATA consts<>+0xb0(SB)/4, $0x22330011 +DATA consts<>+0xb4(SB)/4, $0x66774455 +DATA consts<>+0xb8(SB)/4, $0xaabb8899 +DATA consts<>+0xbc(SB)/4, $0xeeffccdd GLOBL consts<>(SB), RODATA, $0xc0 +#ifdef GOARCH_ppc64 +#define BE_XXBRW_INIT() \ + LVSL (R0)(R0), V24 \ + VSPLTISB $3, V25 \ + VXOR V24, V25, V24 \ + +#define BE_XXBRW(vr) VPERM vr, vr, V24, vr +#else +#define BE_XXBRW_INIT() +#define BE_XXBRW(vr) +#endif + //func chaCha20_ctr32_vsx(out, inp *byte, len int, key *[8]uint32, counter *uint32) TEXT ·chaCha20_ctr32_vsx(SB),NOSPLIT,$64-40 MOVD out+0(FP), OUT @@ -94,6 +130,8 @@ TEXT ·chaCha20_ctr32_vsx(SB),NOSPLIT,$64-40 // Clear V27 VXOR V27, V27, V27 + BE_XXBRW_INIT() + // V28 LXVW4X (CONSTBASE)(R11), VS60 @@ -299,6 +337,11 @@ loop_vsx: VADDUWM V8, V18, V8 VADDUWM V12, V19, V12 + BE_XXBRW(V0) + BE_XXBRW(V4) + BE_XXBRW(V8) + BE_XXBRW(V12) + CMPU LEN, $64 BLT tail_vsx @@ -327,6 +370,11 @@ loop_vsx: VADDUWM V9, V18, V8 VADDUWM V13, V19, V12 + BE_XXBRW(V0) + BE_XXBRW(V4) + BE_XXBRW(V8) + BE_XXBRW(V12) + CMPU LEN, $64 BLT tail_vsx @@ -334,8 +382,8 @@ loop_vsx: LXVW4X (INP)(R8), VS60 LXVW4X (INP)(R9), VS61 LXVW4X (INP)(R10), VS62 - VXOR V27, V0, V27 + VXOR V27, V0, V27 VXOR V28, V4, V28 VXOR V29, V8, V29 VXOR V30, V12, V30 @@ -354,6 +402,11 @@ loop_vsx: VADDUWM V10, V18, V8 VADDUWM V14, V19, V12 + BE_XXBRW(V0) + BE_XXBRW(V4) + BE_XXBRW(V8) + BE_XXBRW(V12) + CMPU LEN, $64 BLT tail_vsx @@ -381,6 +434,11 @@ loop_vsx: VADDUWM V11, V18, V8 VADDUWM V15, V19, V12 + BE_XXBRW(V0) + BE_XXBRW(V4) + BE_XXBRW(V8) + BE_XXBRW(V12) + CMPU LEN, $64 BLT tail_vsx @@ -408,9 +466,9 @@ loop_vsx: done_vsx: // Increment counter by number of 64 byte blocks - MOVD (CNT), R14 + MOVWZ (CNT), R14 ADD BLOCKS, R14 - MOVD R14, (CNT) + MOVWZ R14, (CNT) RET tail_vsx: diff --git a/vendor/golang.org/x/crypto/internal/poly1305/mac_noasm.go b/vendor/golang.org/x/crypto/internal/poly1305/mac_noasm.go index 333da285b..bd896bdc7 100644 --- a/vendor/golang.org/x/crypto/internal/poly1305/mac_noasm.go +++ b/vendor/golang.org/x/crypto/internal/poly1305/mac_noasm.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build (!amd64 && !ppc64le && !s390x) || !gc || purego +//go:build (!amd64 && !ppc64le && !ppc64 && !s390x) || !gc || purego package poly1305 diff --git a/vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64le.go b/vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64x.go similarity index 95% rename from vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64le.go rename to vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64x.go index 4aec4874b..1a1679aaa 100644 --- a/vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64le.go +++ b/vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64x.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build gc && !purego +//go:build gc && !purego && (ppc64 || ppc64le) package poly1305 diff --git a/vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64le.s b/vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64x.s similarity index 89% rename from vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64le.s rename to vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64x.s index b3c1699bf..6899a1dab 100644 --- a/vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64le.s +++ b/vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64x.s @@ -2,15 +2,25 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build gc && !purego +//go:build gc && !purego && (ppc64 || ppc64le) #include "textflag.h" // This was ported from the amd64 implementation. +#ifdef GOARCH_ppc64le +#define LE_MOVD MOVD +#define LE_MOVWZ MOVWZ +#define LE_MOVHZ MOVHZ +#else +#define LE_MOVD MOVDBR +#define LE_MOVWZ MOVWBR +#define LE_MOVHZ MOVHBR +#endif + #define POLY1305_ADD(msg, h0, h1, h2, t0, t1, t2) \ - MOVD (msg), t0; \ - MOVD 8(msg), t1; \ + LE_MOVD (msg)( R0), t0; \ + LE_MOVD (msg)(R24), t1; \ MOVD $1, t2; \ ADDC t0, h0, h0; \ ADDE t1, h1, h1; \ @@ -50,10 +60,6 @@ ADDE t3, h1, h1; \ ADDZE h2 -DATA ·poly1305Mask<>+0x00(SB)/8, $0x0FFFFFFC0FFFFFFF -DATA ·poly1305Mask<>+0x08(SB)/8, $0x0FFFFFFC0FFFFFFC -GLOBL ·poly1305Mask<>(SB), RODATA, $16 - // func update(state *[7]uint64, msg []byte) TEXT ·update(SB), $0-32 MOVD state+0(FP), R3 @@ -66,6 +72,8 @@ TEXT ·update(SB), $0-32 MOVD 24(R3), R11 // r0 MOVD 32(R3), R12 // r1 + MOVD $8, R24 + CMP R5, $16 BLT bytes_between_0_and_15 @@ -94,7 +102,7 @@ flush_buffer: // Greater than 8 -- load the rightmost remaining bytes in msg // and put into R17 (h1) - MOVD (R4)(R21), R17 + LE_MOVD (R4)(R21), R17 MOVD $16, R22 // Find the offset to those bytes @@ -118,7 +126,7 @@ just1: BLT less8 // Exactly 8 - MOVD (R4), R16 + LE_MOVD (R4), R16 CMP R17, $0 @@ -133,7 +141,7 @@ less8: MOVD $0, R22 // shift count CMP R5, $4 BLT less4 - MOVWZ (R4), R16 + LE_MOVWZ (R4), R16 ADD $4, R4 ADD $-4, R5 MOVD $32, R22 @@ -141,7 +149,7 @@ less8: less4: CMP R5, $2 BLT less2 - MOVHZ (R4), R21 + LE_MOVHZ (R4), R21 SLD R22, R21, R21 OR R16, R21, R16 ADD $16, R22 diff --git a/vendor/golang.org/x/crypto/sha3/doc.go b/vendor/golang.org/x/crypto/sha3/doc.go index 7e0230907..bbf391fe6 100644 --- a/vendor/golang.org/x/crypto/sha3/doc.go +++ b/vendor/golang.org/x/crypto/sha3/doc.go @@ -5,6 +5,10 @@ // Package sha3 implements the SHA-3 fixed-output-length hash functions and // the SHAKE variable-output-length hash functions defined by FIPS-202. // +// All types in this package also implement [encoding.BinaryMarshaler], +// [encoding.BinaryAppender] and [encoding.BinaryUnmarshaler] to marshal and +// unmarshal the internal state of the hash. +// // Both types of hash function use the "sponge" construction and the Keccak // permutation. For a detailed specification see http://keccak.noekeon.org/ // diff --git a/vendor/golang.org/x/crypto/sha3/hashes.go b/vendor/golang.org/x/crypto/sha3/hashes.go index c544b29e5..31fffbe04 100644 --- a/vendor/golang.org/x/crypto/sha3/hashes.go +++ b/vendor/golang.org/x/crypto/sha3/hashes.go @@ -48,33 +48,52 @@ func init() { crypto.RegisterHash(crypto.SHA3_512, New512) } +const ( + dsbyteSHA3 = 0b00000110 + dsbyteKeccak = 0b00000001 + dsbyteShake = 0b00011111 + dsbyteCShake = 0b00000100 + + // rateK[c] is the rate in bytes for Keccak[c] where c is the capacity in + // bits. Given the sponge size is 1600 bits, the rate is 1600 - c bits. + rateK256 = (1600 - 256) / 8 + rateK448 = (1600 - 448) / 8 + rateK512 = (1600 - 512) / 8 + rateK768 = (1600 - 768) / 8 + rateK1024 = (1600 - 1024) / 8 +) + func new224Generic() *state { - return &state{rate: 144, outputLen: 28, dsbyte: 0x06} + return &state{rate: rateK448, outputLen: 28, dsbyte: dsbyteSHA3} } func new256Generic() *state { - return &state{rate: 136, outputLen: 32, dsbyte: 0x06} + return &state{rate: rateK512, outputLen: 32, dsbyte: dsbyteSHA3} } func new384Generic() *state { - return &state{rate: 104, outputLen: 48, dsbyte: 0x06} + return &state{rate: rateK768, outputLen: 48, dsbyte: dsbyteSHA3} } func new512Generic() *state { - return &state{rate: 72, outputLen: 64, dsbyte: 0x06} + return &state{rate: rateK1024, outputLen: 64, dsbyte: dsbyteSHA3} } // NewLegacyKeccak256 creates a new Keccak-256 hash. // // Only use this function if you require compatibility with an existing cryptosystem // that uses non-standard padding. All other users should use New256 instead. -func NewLegacyKeccak256() hash.Hash { return &state{rate: 136, outputLen: 32, dsbyte: 0x01} } +func NewLegacyKeccak256() hash.Hash { + return &state{rate: rateK512, outputLen: 32, dsbyte: dsbyteKeccak} +} // NewLegacyKeccak512 creates a new Keccak-512 hash. // // Only use this function if you require compatibility with an existing cryptosystem // that uses non-standard padding. All other users should use New512 instead. -func NewLegacyKeccak512() hash.Hash { return &state{rate: 72, outputLen: 64, dsbyte: 0x01} } +func NewLegacyKeccak512() hash.Hash { + return &state{rate: rateK1024, outputLen: 64, dsbyte: dsbyteKeccak} +} // Sum224 returns the SHA3-224 digest of the data. func Sum224(data []byte) (digest [28]byte) { diff --git a/vendor/golang.org/x/crypto/sha3/sha3.go b/vendor/golang.org/x/crypto/sha3/sha3.go index afedde5ab..6658c4447 100644 --- a/vendor/golang.org/x/crypto/sha3/sha3.go +++ b/vendor/golang.org/x/crypto/sha3/sha3.go @@ -4,6 +4,15 @@ package sha3 +import ( + "crypto/subtle" + "encoding/binary" + "errors" + "unsafe" + + "golang.org/x/sys/cpu" +) + // spongeDirection indicates the direction bytes are flowing through the sponge. type spongeDirection int @@ -14,16 +23,13 @@ spongeSqueezing ) -const ( - // maxRate is the maximum size of the internal buffer. SHAKE-256 - // currently needs the largest buffer. - maxRate = 168 -) - type state struct { - // Generic sponge components. - a [25]uint64 // main state of the hash - rate int // the number of bytes of state to use + a [1600 / 8]byte // main state of the hash + + // a[n:rate] is the buffer. If absorbing, it's the remaining space to XOR + // into before running the permutation. If squeezing, it's the remaining + // output to produce before running the permutation. + n, rate int // dsbyte contains the "domain separation" bits and the first bit of // the padding. Sections 6.1 and 6.2 of [1] separate the outputs of the @@ -39,10 +45,6 @@ type state struct { // Extendable-Output Functions (May 2014)" dsbyte byte - i, n int // storage[i:n] is the buffer, i is only used while squeezing - storage [maxRate]byte - - // Specific to SHA-3 and SHAKE. outputLen int // the default output size in bytes state spongeDirection // whether the sponge is absorbing or squeezing } @@ -61,7 +63,7 @@ func (d *state) Reset() { d.a[i] = 0 } d.state = spongeAbsorbing - d.i, d.n = 0, 0 + d.n = 0 } func (d *state) clone() *state { @@ -69,22 +71,25 @@ func (d *state) clone() *state { return &ret } -// permute applies the KeccakF-1600 permutation. It handles -// any input-output buffering. +// permute applies the KeccakF-1600 permutation. func (d *state) permute() { - switch d.state { - case spongeAbsorbing: - // If we're absorbing, we need to xor the input into the state - // before applying the permutation. - xorIn(d, d.storage[:d.rate]) - d.n = 0 - keccakF1600(&d.a) - case spongeSqueezing: - // If we're squeezing, we need to apply the permutation before - // copying more output. - keccakF1600(&d.a) - d.i = 0 - copyOut(d, d.storage[:d.rate]) + var a *[25]uint64 + if cpu.IsBigEndian { + a = new([25]uint64) + for i := range a { + a[i] = binary.LittleEndian.Uint64(d.a[i*8:]) + } + } else { + a = (*[25]uint64)(unsafe.Pointer(&d.a)) + } + + keccakF1600(a) + d.n = 0 + + if cpu.IsBigEndian { + for i := range a { + binary.LittleEndian.PutUint64(d.a[i*8:], a[i]) + } } } @@ -92,53 +97,36 @@ func (d *state) permute() { // the multi-bitrate 10..1 padding rule, and permutes the state. func (d *state) padAndPermute() { // Pad with this instance's domain-separator bits. We know that there's - // at least one byte of space in d.buf because, if it were full, + // at least one byte of space in the sponge because, if it were full, // permute would have been called to empty it. dsbyte also contains the // first one bit for the padding. See the comment in the state struct. - d.storage[d.n] = d.dsbyte - d.n++ - for d.n < d.rate { - d.storage[d.n] = 0 - d.n++ - } + d.a[d.n] ^= d.dsbyte // This adds the final one bit for the padding. Because of the way that // bits are numbered from the LSB upwards, the final bit is the MSB of // the last byte. - d.storage[d.rate-1] ^= 0x80 + d.a[d.rate-1] ^= 0x80 // Apply the permutation d.permute() d.state = spongeSqueezing - d.n = d.rate - copyOut(d, d.storage[:d.rate]) } // Write absorbs more data into the hash's state. It panics if any // output has already been read. -func (d *state) Write(p []byte) (written int, err error) { +func (d *state) Write(p []byte) (n int, err error) { if d.state != spongeAbsorbing { panic("sha3: Write after Read") } - written = len(p) + + n = len(p) for len(p) > 0 { - if d.n == 0 && len(p) >= d.rate { - // The fast path; absorb a full "rate" bytes of input and apply the permutation. - xorIn(d, p[:d.rate]) - p = p[d.rate:] - keccakF1600(&d.a) - } else { - // The slow path; buffer the input until we can fill the sponge, and then xor it in. - todo := d.rate - d.n - if todo > len(p) { - todo = len(p) - } - d.n += copy(d.storage[d.n:], p[:todo]) - p = p[todo:] + x := subtle.XORBytes(d.a[d.n:d.rate], d.a[d.n:d.rate], p) + d.n += x + p = p[x:] - // If the sponge is full, apply the permutation. - if d.n == d.rate { - d.permute() - } + // If the sponge is full, apply the permutation. + if d.n == d.rate { + d.permute() } } @@ -156,14 +144,14 @@ func (d *state) Read(out []byte) (n int, err error) { // Now, do the squeezing. for len(out) > 0 { - n := copy(out, d.storage[d.i:d.n]) - d.i += n - out = out[n:] - // Apply the permutation if we've squeezed the sponge dry. - if d.i == d.rate { + if d.n == d.rate { d.permute() } + + x := copy(out, d.a[d.n:d.rate]) + d.n += x + out = out[x:] } return @@ -183,3 +171,74 @@ func (d *state) Sum(in []byte) []byte { dup.Read(hash) return append(in, hash...) } + +const ( + magicSHA3 = "sha\x08" + magicShake = "sha\x09" + magicCShake = "sha\x0a" + magicKeccak = "sha\x0b" + // magic || rate || main state || n || sponge direction + marshaledSize = len(magicSHA3) + 1 + 200 + 1 + 1 +) + +func (d *state) MarshalBinary() ([]byte, error) { + return d.AppendBinary(make([]byte, 0, marshaledSize)) +} + +func (d *state) AppendBinary(b []byte) ([]byte, error) { + switch d.dsbyte { + case dsbyteSHA3: + b = append(b, magicSHA3...) + case dsbyteShake: + b = append(b, magicShake...) + case dsbyteCShake: + b = append(b, magicCShake...) + case dsbyteKeccak: + b = append(b, magicKeccak...) + default: + panic("unknown dsbyte") + } + // rate is at most 168, and n is at most rate. + b = append(b, byte(d.rate)) + b = append(b, d.a[:]...) + b = append(b, byte(d.n), byte(d.state)) + return b, nil +} + +func (d *state) UnmarshalBinary(b []byte) error { + if len(b) != marshaledSize { + return errors.New("sha3: invalid hash state") + } + + magic := string(b[:len(magicSHA3)]) + b = b[len(magicSHA3):] + switch { + case magic == magicSHA3 && d.dsbyte == dsbyteSHA3: + case magic == magicShake && d.dsbyte == dsbyteShake: + case magic == magicCShake && d.dsbyte == dsbyteCShake: + case magic == magicKeccak && d.dsbyte == dsbyteKeccak: + default: + return errors.New("sha3: invalid hash state identifier") + } + + rate := int(b[0]) + b = b[1:] + if rate != d.rate { + return errors.New("sha3: invalid hash state function") + } + + copy(d.a[:], b) + b = b[len(d.a):] + + n, state := int(b[0]), spongeDirection(b[1]) + if n > d.rate { + return errors.New("sha3: invalid hash state") + } + d.n = n + if state != spongeAbsorbing && state != spongeSqueezing { + return errors.New("sha3: invalid hash state") + } + d.state = state + + return nil +} diff --git a/vendor/golang.org/x/crypto/sha3/shake.go b/vendor/golang.org/x/crypto/sha3/shake.go index a01ef4357..a6b3a4281 100644 --- a/vendor/golang.org/x/crypto/sha3/shake.go +++ b/vendor/golang.org/x/crypto/sha3/shake.go @@ -16,9 +16,12 @@ // [2] https://doi.org/10.6028/NIST.SP.800-185 import ( + "bytes" "encoding/binary" + "errors" "hash" "io" + "math/bits" ) // ShakeHash defines the interface to hash functions that support @@ -50,41 +53,33 @@ type cshakeState struct { initBlock []byte } -// Consts for configuring initial SHA-3 state -const ( - dsbyteShake = 0x1f - dsbyteCShake = 0x04 - rate128 = 168 - rate256 = 136 -) - -func bytepad(input []byte, w int) []byte { - // leftEncode always returns max 9 bytes - buf := make([]byte, 0, 9+len(input)+w) - buf = append(buf, leftEncode(uint64(w))...) - buf = append(buf, input...) - padlen := w - (len(buf) % w) - return append(buf, make([]byte, padlen)...) +func bytepad(data []byte, rate int) []byte { + out := make([]byte, 0, 9+len(data)+rate-1) + out = append(out, leftEncode(uint64(rate))...) + out = append(out, data...) + if padlen := rate - len(out)%rate; padlen < rate { + out = append(out, make([]byte, padlen)...) + } + return out } -func leftEncode(value uint64) []byte { - var b [9]byte - binary.BigEndian.PutUint64(b[1:], value) - // Trim all but last leading zero bytes - i := byte(1) - for i < 8 && b[i] == 0 { - i++ +func leftEncode(x uint64) []byte { + // Let n be the smallest positive integer for which 2^(8n) > x. + n := (bits.Len64(x) + 7) / 8 + if n == 0 { + n = 1 } - // Prepend number of encoded bytes - b[i-1] = 9 - i - return b[i-1:] + // Return n || x with n as a byte and x an n bytes in big-endian order. + b := make([]byte, 9) + binary.BigEndian.PutUint64(b[1:], x) + b = b[9-n-1:] + b[0] = byte(n) + return b } func newCShake(N, S []byte, rate, outputLen int, dsbyte byte) ShakeHash { c := cshakeState{state: &state{rate: rate, outputLen: outputLen, dsbyte: dsbyte}} - - // leftEncode returns max 9 bytes - c.initBlock = make([]byte, 0, 9*2+len(N)+len(S)) + c.initBlock = make([]byte, 0, 9+len(N)+9+len(S)) // leftEncode returns max 9 bytes c.initBlock = append(c.initBlock, leftEncode(uint64(len(N))*8)...) c.initBlock = append(c.initBlock, N...) c.initBlock = append(c.initBlock, leftEncode(uint64(len(S))*8)...) @@ -111,6 +106,30 @@ func (c *state) Clone() ShakeHash { return c.clone() } +func (c *cshakeState) MarshalBinary() ([]byte, error) { + return c.AppendBinary(make([]byte, 0, marshaledSize+len(c.initBlock))) +} + +func (c *cshakeState) AppendBinary(b []byte) ([]byte, error) { + b, err := c.state.AppendBinary(b) + if err != nil { + return nil, err + } + b = append(b, c.initBlock...) + return b, nil +} + +func (c *cshakeState) UnmarshalBinary(b []byte) error { + if len(b) <= marshaledSize { + return errors.New("sha3: invalid hash state") + } + if err := c.state.UnmarshalBinary(b[:marshaledSize]); err != nil { + return err + } + c.initBlock = bytes.Clone(b[marshaledSize:]) + return nil +} + // NewShake128 creates a new SHAKE128 variable-output-length ShakeHash. // Its generic security strength is 128 bits against all attacks if at // least 32 bytes of its output are used. @@ -126,11 +145,11 @@ func NewShake256() ShakeHash { } func newShake128Generic() *state { - return &state{rate: rate128, outputLen: 32, dsbyte: dsbyteShake} + return &state{rate: rateK256, outputLen: 32, dsbyte: dsbyteShake} } func newShake256Generic() *state { - return &state{rate: rate256, outputLen: 64, dsbyte: dsbyteShake} + return &state{rate: rateK512, outputLen: 64, dsbyte: dsbyteShake} } // NewCShake128 creates a new instance of cSHAKE128 variable-output-length ShakeHash, @@ -143,7 +162,7 @@ func NewCShake128(N, S []byte) ShakeHash { if len(N) == 0 && len(S) == 0 { return NewShake128() } - return newCShake(N, S, rate128, 32, dsbyteCShake) + return newCShake(N, S, rateK256, 32, dsbyteCShake) } // NewCShake256 creates a new instance of cSHAKE256 variable-output-length ShakeHash, @@ -156,7 +175,7 @@ func NewCShake256(N, S []byte) ShakeHash { if len(N) == 0 && len(S) == 0 { return NewShake256() } - return newCShake(N, S, rate256, 64, dsbyteCShake) + return newCShake(N, S, rateK512, 64, dsbyteCShake) } // ShakeSum128 writes an arbitrary-length digest of data into hash. diff --git a/vendor/golang.org/x/crypto/sha3/xor.go b/vendor/golang.org/x/crypto/sha3/xor.go deleted file mode 100644 index 6ada5c957..000000000 --- a/vendor/golang.org/x/crypto/sha3/xor.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package sha3 - -import ( - "crypto/subtle" - "encoding/binary" - "unsafe" - - "golang.org/x/sys/cpu" -) - -// xorIn xors the bytes in buf into the state. -func xorIn(d *state, buf []byte) { - if cpu.IsBigEndian { - for i := 0; len(buf) >= 8; i++ { - a := binary.LittleEndian.Uint64(buf) - d.a[i] ^= a - buf = buf[8:] - } - } else { - ab := (*[25 * 64 / 8]byte)(unsafe.Pointer(&d.a)) - subtle.XORBytes(ab[:], ab[:], buf) - } -} - -// copyOut copies uint64s to a byte buffer. -func copyOut(d *state, b []byte) { - if cpu.IsBigEndian { - for i := 0; len(b) >= 8; i++ { - binary.LittleEndian.PutUint64(b, d.a[i]) - b = b[8:] - } - } else { - ab := (*[25 * 64 / 8]byte)(unsafe.Pointer(&d.a)) - copy(b, ab[:]) - } -} diff --git a/vendor/golang.org/x/crypto/ssh/client_auth.go b/vendor/golang.org/x/crypto/ssh/client_auth.go index b93961010..b86dde151 100644 --- a/vendor/golang.org/x/crypto/ssh/client_auth.go +++ b/vendor/golang.org/x/crypto/ssh/client_auth.go @@ -555,6 +555,7 @@ type initiateMsg struct { } gotMsgExtInfo := false + gotUserAuthInfoRequest := false for { packet, err := c.readPacket() if err != nil { @@ -585,6 +586,9 @@ type initiateMsg struct { if msg.PartialSuccess { return authPartialSuccess, msg.Methods, nil } + if !gotUserAuthInfoRequest { + return authFailure, msg.Methods, unexpectedMessageError(msgUserAuthInfoRequest, packet[0]) + } return authFailure, msg.Methods, nil case msgUserAuthSuccess: return authSuccess, nil, nil @@ -596,6 +600,7 @@ type initiateMsg struct { if err := Unmarshal(packet, &msg); err != nil { return authFailure, nil, err } + gotUserAuthInfoRequest = true // Manually unpack the prompt/echo pairs. rest := msg.Prompts diff --git a/vendor/golang.org/x/net/html/doc.go b/vendor/golang.org/x/net/html/doc.go index 3a7e5ab17..885c4c593 100644 --- a/vendor/golang.org/x/net/html/doc.go +++ b/vendor/golang.org/x/net/html/doc.go @@ -78,16 +78,11 @@ if err != nil { // ... } - var f func(*html.Node) - f = func(n *html.Node) { + for n := range doc.Descendants() { if n.Type == html.ElementNode && n.Data == "a" { // Do something with n... } - for c := n.FirstChild; c != nil; c = c.NextSibling { - f(c) - } } - f(doc) The relevant specifications include: https://html.spec.whatwg.org/multipage/syntax.html and diff --git a/vendor/golang.org/x/net/html/iter.go b/vendor/golang.org/x/net/html/iter.go new file mode 100644 index 000000000..54be8fd30 --- /dev/null +++ b/vendor/golang.org/x/net/html/iter.go @@ -0,0 +1,56 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.23 + +package html + +import "iter" + +// Ancestors returns an iterator over the ancestors of n, starting with n.Parent. +// +// Mutating a Node or its parents while iterating may have unexpected results. +func (n *Node) Ancestors() iter.Seq[*Node] { + _ = n.Parent // eager nil check + + return func(yield func(*Node) bool) { + for p := n.Parent; p != nil && yield(p); p = p.Parent { + } + } +} + +// ChildNodes returns an iterator over the immediate children of n, +// starting with n.FirstChild. +// +// Mutating a Node or its children while iterating may have unexpected results. +func (n *Node) ChildNodes() iter.Seq[*Node] { + _ = n.FirstChild // eager nil check + + return func(yield func(*Node) bool) { + for c := n.FirstChild; c != nil && yield(c); c = c.NextSibling { + } + } + +} + +// Descendants returns an iterator over all nodes recursively beneath +// n, excluding n itself. Nodes are visited in depth-first preorder. +// +// Mutating a Node or its descendants while iterating may have unexpected results. +func (n *Node) Descendants() iter.Seq[*Node] { + _ = n.FirstChild // eager nil check + + return func(yield func(*Node) bool) { + n.descendants(yield) + } +} + +func (n *Node) descendants(yield func(*Node) bool) bool { + for c := range n.ChildNodes() { + if !yield(c) || !c.descendants(yield) { + return false + } + } + return true +} diff --git a/vendor/golang.org/x/net/html/node.go b/vendor/golang.org/x/net/html/node.go index 1350eef22..77741a195 100644 --- a/vendor/golang.org/x/net/html/node.go +++ b/vendor/golang.org/x/net/html/node.go @@ -38,6 +38,10 @@ // that it looks like "a cc.idleTimeout + return cc.idleTimeout != 0 && !cc.lastIdle.IsZero() && cc.t.timeSince(cc.lastIdle.Round(0)) > cc.idleTimeout } // onIdleTimeout is called from a time.AfterFunc goroutine. It will @@ -1578,6 +1668,7 @@ func (cs *clientStream) cleanupWriteRequest(err error) { cs.reqBodyClosed = make(chan struct{}) } bodyClosed := cs.reqBodyClosed + closeOnIdle := cc.singleUse || cc.doNotReuse || cc.t.disableKeepAlives() || cc.goAway != nil cc.mu.Unlock() if mustCloseBody { cs.reqBody.Close() @@ -1602,16 +1693,40 @@ func (cs *clientStream) cleanupWriteRequest(err error) { if cs.sentHeaders { if se, ok := err.(StreamError); ok { if se.Cause != errFromPeer { - cc.writeStreamReset(cs.ID, se.Code, err) + cc.writeStreamReset(cs.ID, se.Code, false, err) } } else { - cc.writeStreamReset(cs.ID, ErrCodeCancel, err) + // We're cancelling an in-flight request. + // + // This could be due to the server becoming unresponsive. + // To avoid sending too many requests on a dead connection, + // we let the request continue to consume a concurrency slot + // until we can confirm the server is still responding. + // We do this by sending a PING frame along with the RST_STREAM + // (unless a ping is already in flight). + // + // For simplicity, we don't bother tracking the PING payload: + // We reset cc.pendingResets any time we receive a PING ACK. + // + // We skip this if the conn is going to be closed on idle, + // because it's short lived and will probably be closed before + // we get the ping response. + ping := false + if !closeOnIdle { + cc.mu.Lock() + if cc.pendingResets == 0 { + ping = true + } + cc.pendingResets++ + cc.mu.Unlock() + } + cc.writeStreamReset(cs.ID, ErrCodeCancel, ping, err) } } cs.bufPipe.CloseWithError(err) // no-op if already closed } else { if cs.sentHeaders && !cs.sentEndStream { - cc.writeStreamReset(cs.ID, ErrCodeNo, nil) + cc.writeStreamReset(cs.ID, ErrCodeNo, false, nil) } cs.bufPipe.CloseWithError(errRequestCanceled) } @@ -1633,12 +1748,17 @@ func (cs *clientStream) cleanupWriteRequest(err error) { // Must hold cc.mu. func (cc *ClientConn) awaitOpenSlotForStreamLocked(cs *clientStream) error { for { - cc.lastActive = time.Now() + if cc.closed && cc.nextStreamID == 1 && cc.streamsReserved == 0 { + // This is the very first request sent to this connection. + // Return a fatal error which aborts the retry loop. + return errClientConnNotEstablished + } + cc.lastActive = cc.t.now() if cc.closed || !cc.canTakeNewRequestLocked() { return errClientConnUnusable } cc.lastIdle = time.Time{} - if int64(len(cc.streams)) < int64(cc.maxConcurrentStreams) { + if cc.currentRequestCountLocked() < int(cc.maxConcurrentStreams) { return nil } cc.pendingRequests++ @@ -2180,10 +2300,10 @@ func (cc *ClientConn) forgetStreamID(id uint32) { if len(cc.streams) != slen-1 { panic("forgetting unknown stream id") } - cc.lastActive = time.Now() + cc.lastActive = cc.t.now() if len(cc.streams) == 0 && cc.idleTimer != nil { cc.idleTimer.Reset(cc.idleTimeout) - cc.lastIdle = time.Now() + cc.lastIdle = cc.t.now() } // Wake up writeRequestBody via clientStream.awaitFlowControl and // wake up RoundTrip if there is a pending request. @@ -2243,7 +2363,6 @@ func isEOFOrNetReadError(err error) bool { func (rl *clientConnReadLoop) cleanup() { cc := rl.cc - cc.t.connPool().MarkDead(cc) defer cc.closeConn() defer close(cc.readerDone) @@ -2267,6 +2386,24 @@ func (rl *clientConnReadLoop) cleanup() { } cc.closed = true + // If the connection has never been used, and has been open for only a short time, + // leave it in the connection pool for a little while. + // + // This avoids a situation where new connections are constantly created, + // added to the pool, fail, and are removed from the pool, without any error + // being surfaced to the user. + const unusedWaitTime = 5 * time.Second + idleTime := cc.t.now().Sub(cc.lastActive) + if atomic.LoadUint32(&cc.atomicReused) == 0 && idleTime < unusedWaitTime { + cc.idleTimer = cc.t.afterFunc(unusedWaitTime-idleTime, func() { + cc.t.connPool().MarkDead(cc) + }) + } else { + cc.mu.Unlock() // avoid any deadlocks in MarkDead + cc.t.connPool().MarkDead(cc) + cc.mu.Lock() + } + for _, cs := range cc.streams { select { case <-cs.peerClosed: @@ -2494,15 +2631,34 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra if f.StreamEnded() { return nil, errors.New("1xx informational response with END_STREAM flag") } - cs.num1xx++ - const max1xxResponses = 5 // arbitrary bound on number of informational responses, same as net/http - if cs.num1xx > max1xxResponses { - return nil, errors.New("http2: too many 1xx informational responses") - } if fn := cs.get1xxTraceFunc(); fn != nil { + // If the 1xx response is being delivered to the user, + // then they're responsible for limiting the number + // of responses. if err := fn(statusCode, textproto.MIMEHeader(header)); err != nil { return nil, err } + } else { + // If the user didn't examine the 1xx response, then we + // limit the size of all 1xx headers. + // + // This differs a bit from the HTTP/1 implementation, which + // limits the size of all 1xx headers plus the final response. + // Use the larger limit of MaxHeaderListSize and + // net/http.Transport.MaxResponseHeaderBytes. + limit := int64(cs.cc.t.maxHeaderListSize()) + if t1 := cs.cc.t.t1; t1 != nil && t1.MaxResponseHeaderBytes > limit { + limit = t1.MaxResponseHeaderBytes + } + for _, h := range f.Fields { + cs.totalHeaderSize += int64(h.Size()) + } + if cs.totalHeaderSize > limit { + if VerboseLogs { + log.Printf("http2: 1xx informational responses too large") + } + return nil, errors.New("header list too large") + } } if statusCode == 100 { traceGot100Continue(cs.trace) @@ -3046,6 +3202,11 @@ func (rl *clientConnReadLoop) processPing(f *PingFrame) error { close(c) delete(cc.pings, f.Data) } + if cc.pendingResets > 0 { + // See clientStream.cleanupWriteRequest. + cc.pendingResets = 0 + cc.cond.Broadcast() + } return nil } cc := rl.cc @@ -3068,13 +3229,20 @@ func (rl *clientConnReadLoop) processPushPromise(f *PushPromiseFrame) error { return ConnectionError(ErrCodeProtocol) } -func (cc *ClientConn) writeStreamReset(streamID uint32, code ErrCode, err error) { +// writeStreamReset sends a RST_STREAM frame. +// When ping is true, it also sends a PING frame with a random payload. +func (cc *ClientConn) writeStreamReset(streamID uint32, code ErrCode, ping bool, err error) { // TODO: map err to more interesting error codes, once the // HTTP community comes up with some. But currently for // RST_STREAM there's no equivalent to GOAWAY frame's debug // data, and the error codes are all pretty vague ("cancel"). cc.wmu.Lock() cc.fr.WriteRSTStream(streamID, code) + if ping { + var payload [8]byte + rand.Read(payload[:]) + cc.fr.WritePing(false, payload) + } cc.bw.Flush() cc.wmu.Unlock() } @@ -3228,7 +3396,7 @@ func traceGotConn(req *http.Request, cc *ClientConn, reused bool) { cc.mu.Lock() ci.WasIdle = len(cc.streams) == 0 && reused if ci.WasIdle && !cc.lastActive.IsZero() { - ci.IdleTime = time.Since(cc.lastActive) + ci.IdleTime = cc.t.timeSince(cc.lastActive) } cc.mu.Unlock() diff --git a/vendor/golang.org/x/net/http2/unencrypted.go b/vendor/golang.org/x/net/http2/unencrypted.go new file mode 100644 index 000000000..b2de21161 --- /dev/null +++ b/vendor/golang.org/x/net/http2/unencrypted.go @@ -0,0 +1,32 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package http2 + +import ( + "crypto/tls" + "errors" + "net" +) + +const nextProtoUnencryptedHTTP2 = "unencrypted_http2" + +// unencryptedNetConnFromTLSConn retrieves a net.Conn wrapped in a *tls.Conn. +// +// TLSNextProto functions accept a *tls.Conn. +// +// When passing an unencrypted HTTP/2 connection to a TLSNextProto function, +// we pass a *tls.Conn with an underlying net.Conn containing the unencrypted connection. +// To be extra careful about mistakes (accidentally dropping TLS encryption in a place +// where we want it), the tls.Conn contains a net.Conn with an UnencryptedNetConn method +// that returns the actual connection we want to use. +func unencryptedNetConnFromTLSConn(tc *tls.Conn) (net.Conn, error) { + conner, ok := tc.NetConn().(interface { + UnencryptedNetConn() net.Conn + }) + if !ok { + return nil, errors.New("http2: TLS conn unexpectedly found in unencrypted handoff") + } + return conner.UnencryptedNetConn(), nil +} diff --git a/vendor/golang.org/x/net/internal/socket/zsys_openbsd_ppc64.go b/vendor/golang.org/x/net/internal/socket/zsys_openbsd_ppc64.go index cebde7634..3c9576e2d 100644 --- a/vendor/golang.org/x/net/internal/socket/zsys_openbsd_ppc64.go +++ b/vendor/golang.org/x/net/internal/socket/zsys_openbsd_ppc64.go @@ -4,27 +4,27 @@ package socket type iovec struct { - Base *byte - Len uint64 + Base *byte + Len uint64 } type msghdr struct { - Name *byte - Namelen uint32 - Iov *iovec - Iovlen uint32 - Control *byte - Controllen uint32 - Flags int32 + Name *byte + Namelen uint32 + Iov *iovec + Iovlen uint32 + Control *byte + Controllen uint32 + Flags int32 } type cmsghdr struct { - Len uint32 - Level int32 - Type int32 + Len uint32 + Level int32 + Type int32 } const ( - sizeofIovec = 0x10 - sizeofMsghdr = 0x30 + sizeofIovec = 0x10 + sizeofMsghdr = 0x30 ) diff --git a/vendor/golang.org/x/net/internal/socket/zsys_openbsd_riscv64.go b/vendor/golang.org/x/net/internal/socket/zsys_openbsd_riscv64.go index cebde7634..3c9576e2d 100644 --- a/vendor/golang.org/x/net/internal/socket/zsys_openbsd_riscv64.go +++ b/vendor/golang.org/x/net/internal/socket/zsys_openbsd_riscv64.go @@ -4,27 +4,27 @@ package socket type iovec struct { - Base *byte - Len uint64 + Base *byte + Len uint64 } type msghdr struct { - Name *byte - Namelen uint32 - Iov *iovec - Iovlen uint32 - Control *byte - Controllen uint32 - Flags int32 + Name *byte + Namelen uint32 + Iov *iovec + Iovlen uint32 + Control *byte + Controllen uint32 + Flags int32 } type cmsghdr struct { - Len uint32 - Level int32 - Type int32 + Len uint32 + Level int32 + Type int32 } const ( - sizeofIovec = 0x10 - sizeofMsghdr = 0x30 + sizeofIovec = 0x10 + sizeofMsghdr = 0x30 ) diff --git a/vendor/golang.org/x/oauth2/README.md b/vendor/golang.org/x/oauth2/README.md index 781770c20..48dbb9d84 100644 --- a/vendor/golang.org/x/oauth2/README.md +++ b/vendor/golang.org/x/oauth2/README.md @@ -5,15 +5,6 @@ oauth2 package contains a client implementation for OAuth 2.0 spec. -## Installation - -~~~~ -go get golang.org/x/oauth2 -~~~~ - -Or you can manually git clone the repository to -`$(go env GOPATH)/src/golang.org/x/oauth2`. - See pkg.go.dev for further documentation and examples. * [pkg.go.dev/golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) @@ -33,7 +24,11 @@ The main issue tracker for the oauth2 repository is located at https://github.com/golang/oauth2/issues. This repository uses Gerrit for code changes. To learn how to submit changes to -this repository, see https://golang.org/doc/contribute.html. In particular: +this repository, see https://go.dev/doc/contribute. + +The git repository is https://go.googlesource.com/oauth2. + +Note: * Excluding trivial changes, all contributions should be connected to an existing issue. * API changes must go through the [change proposal process](https://go.dev/s/proposal-process) before they can be accepted. diff --git a/vendor/golang.org/x/sys/cpu/asm_darwin_x86_gc.s b/vendor/golang.org/x/sys/cpu/asm_darwin_x86_gc.s new file mode 100644 index 000000000..ec2acfe54 --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/asm_darwin_x86_gc.s @@ -0,0 +1,17 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && amd64 && gc + +#include "textflag.h" + +TEXT libc_sysctl_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_sysctl(SB) +GLOBL ·libc_sysctl_trampoline_addr(SB), RODATA, $8 +DATA ·libc_sysctl_trampoline_addr(SB)/8, $libc_sysctl_trampoline<>(SB) + +TEXT libc_sysctlbyname_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_sysctlbyname(SB) +GLOBL ·libc_sysctlbyname_trampoline_addr(SB), RODATA, $8 +DATA ·libc_sysctlbyname_trampoline_addr(SB)/8, $libc_sysctlbyname_trampoline<>(SB) diff --git a/vendor/golang.org/x/sys/cpu/cpu_darwin_x86.go b/vendor/golang.org/x/sys/cpu/cpu_darwin_x86.go new file mode 100644 index 000000000..b838cb9e9 --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/cpu_darwin_x86.go @@ -0,0 +1,61 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && amd64 && gc + +package cpu + +// darwinSupportsAVX512 checks Darwin kernel for AVX512 support via sysctl +// call (see issue 43089). It also restricts AVX512 support for Darwin to +// kernel version 21.3.0 (MacOS 12.2.0) or later (see issue 49233). +// +// Background: +// Darwin implements a special mechanism to economize on thread state when +// AVX512 specific registers are not in use. This scheme minimizes state when +// preempting threads that haven't yet used any AVX512 instructions, but adds +// special requirements to check for AVX512 hardware support at runtime (e.g. +// via sysctl call or commpage inspection). See issue 43089 and link below for +// full background: +// https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.1.10/osfmk/i386/fpu.c#L214-L240 +// +// Additionally, all versions of the Darwin kernel from 19.6.0 through 21.2.0 +// (corresponding to MacOS 10.15.6 - 12.1) have a bug that can cause corruption +// of the AVX512 mask registers (K0-K7) upon signal return. For this reason +// AVX512 is considered unsafe to use on Darwin for kernel versions prior to +// 21.3.0, where a fix has been confirmed. See issue 49233 for full background. +func darwinSupportsAVX512() bool { + return darwinSysctlEnabled([]byte("hw.optional.avx512f\x00")) && darwinKernelVersionCheck(21, 3, 0) +} + +// Ensure Darwin kernel version is at least major.minor.patch, avoiding dependencies +func darwinKernelVersionCheck(major, minor, patch int) bool { + var release [256]byte + err := darwinOSRelease(&release) + if err != nil { + return false + } + + var mmp [3]int + c := 0 +Loop: + for _, b := range release[:] { + switch { + case b >= '0' && b <= '9': + mmp[c] = 10*mmp[c] + int(b-'0') + case b == '.': + c++ + if c > 2 { + return false + } + case b == 0: + break Loop + default: + return false + } + } + if c != 2 { + return false + } + return mmp[0] > major || mmp[0] == major && (mmp[1] > minor || mmp[1] == minor && mmp[2] >= patch) +} diff --git a/vendor/golang.org/x/sys/cpu/cpu_gc_x86.go b/vendor/golang.org/x/sys/cpu/cpu_gc_x86.go index 910728fb1..32a44514e 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_gc_x86.go +++ b/vendor/golang.org/x/sys/cpu/cpu_gc_x86.go @@ -6,10 +6,10 @@ package cpu -// cpuid is implemented in cpu_x86.s for gc compiler +// cpuid is implemented in cpu_gc_x86.s for gc compiler // and in cpu_gccgo.c for gccgo. func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32) -// xgetbv with ecx = 0 is implemented in cpu_x86.s for gc compiler +// xgetbv with ecx = 0 is implemented in cpu_gc_x86.s for gc compiler // and in cpu_gccgo.c for gccgo. func xgetbv() (eax, edx uint32) diff --git a/vendor/golang.org/x/sys/cpu/cpu_x86.s b/vendor/golang.org/x/sys/cpu/cpu_gc_x86.s similarity index 94% rename from vendor/golang.org/x/sys/cpu/cpu_x86.s rename to vendor/golang.org/x/sys/cpu/cpu_gc_x86.s index 7d7ba33ef..ce208ce6d 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_x86.s +++ b/vendor/golang.org/x/sys/cpu/cpu_gc_x86.s @@ -18,7 +18,7 @@ TEXT ·cpuid(SB), NOSPLIT, $0-24 RET // func xgetbv() (eax, edx uint32) -TEXT ·xgetbv(SB),NOSPLIT,$0-8 +TEXT ·xgetbv(SB), NOSPLIT, $0-8 MOVL $0, CX XGETBV MOVL AX, eax+0(FP) diff --git a/vendor/golang.org/x/sys/cpu/cpu_gccgo_x86.go b/vendor/golang.org/x/sys/cpu/cpu_gccgo_x86.go index 99c60fe9f..170d21ddf 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_gccgo_x86.go +++ b/vendor/golang.org/x/sys/cpu/cpu_gccgo_x86.go @@ -23,9 +23,3 @@ func xgetbv() (eax, edx uint32) { gccgoXgetbv(&a, &d) return a, d } - -// gccgo doesn't build on Darwin, per: -// https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/gcc.rb#L76 -func darwinSupportsAVX512() bool { - return false -} diff --git a/vendor/golang.org/x/sys/cpu/cpu_linux_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_linux_arm64.go index 08f35ea17..f1caf0f78 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_linux_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_linux_arm64.go @@ -110,7 +110,6 @@ func doinit() { ARM64.HasASIMDFHM = isSet(hwCap, hwcap_ASIMDFHM) ARM64.HasDIT = isSet(hwCap, hwcap_DIT) - // HWCAP2 feature bits ARM64.HasSVE2 = isSet(hwCap2, hwcap2_SVE2) ARM64.HasI8MM = isSet(hwCap2, hwcap2_I8MM) diff --git a/vendor/golang.org/x/sys/cpu/cpu_other_x86.go b/vendor/golang.org/x/sys/cpu/cpu_other_x86.go new file mode 100644 index 000000000..a0fd7e2f7 --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/cpu_other_x86.go @@ -0,0 +1,11 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build 386 || amd64p32 || (amd64 && (!darwin || !gc)) + +package cpu + +func darwinSupportsAVX512() bool { + panic("only implemented for gc && amd64 && darwin") +} diff --git a/vendor/golang.org/x/sys/cpu/cpu_x86.go b/vendor/golang.org/x/sys/cpu/cpu_x86.go index c29f5e4c5..600a68078 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_x86.go +++ b/vendor/golang.org/x/sys/cpu/cpu_x86.go @@ -92,10 +92,8 @@ func archInit() { osSupportsAVX = isSet(1, eax) && isSet(2, eax) if runtime.GOOS == "darwin" { - // Darwin doesn't save/restore AVX-512 mask registers correctly across signal handlers. - // Since users can't rely on mask register contents, let's not advertise AVX-512 support. - // See issue 49233. - osSupportsAVX512 = false + // Darwin requires special AVX512 checks, see cpu_darwin_x86.go + osSupportsAVX512 = osSupportsAVX && darwinSupportsAVX512() } else { // Check if OPMASK and ZMM registers have OS support. osSupportsAVX512 = osSupportsAVX && isSet(5, eax) && isSet(6, eax) && isSet(7, eax) diff --git a/vendor/golang.org/x/sys/cpu/syscall_darwin_x86_gc.go b/vendor/golang.org/x/sys/cpu/syscall_darwin_x86_gc.go new file mode 100644 index 000000000..4d0888b0c --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/syscall_darwin_x86_gc.go @@ -0,0 +1,98 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Minimal copy of x/sys/unix so the cpu package can make a +// system call on Darwin without depending on x/sys/unix. + +//go:build darwin && amd64 && gc + +package cpu + +import ( + "syscall" + "unsafe" +) + +type _C_int int32 + +// adapted from unix.Uname() at x/sys/unix/syscall_darwin.go L419 +func darwinOSRelease(release *[256]byte) error { + // from x/sys/unix/zerrors_openbsd_amd64.go + const ( + CTL_KERN = 0x1 + KERN_OSRELEASE = 0x2 + ) + + mib := []_C_int{CTL_KERN, KERN_OSRELEASE} + n := unsafe.Sizeof(*release) + + return sysctl(mib, &release[0], &n, nil, 0) +} + +type Errno = syscall.Errno + +var _zero uintptr // Single-word zero for use when we need a valid pointer to 0 bytes. + +// from x/sys/unix/zsyscall_darwin_amd64.go L791-807 +func sysctl(mib []_C_int, old *byte, oldlen *uintptr, new *byte, newlen uintptr) error { + var _p0 unsafe.Pointer + if len(mib) > 0 { + _p0 = unsafe.Pointer(&mib[0]) + } else { + _p0 = unsafe.Pointer(&_zero) + } + if _, _, err := syscall_syscall6( + libc_sysctl_trampoline_addr, + uintptr(_p0), + uintptr(len(mib)), + uintptr(unsafe.Pointer(old)), + uintptr(unsafe.Pointer(oldlen)), + uintptr(unsafe.Pointer(new)), + uintptr(newlen), + ); err != 0 { + return err + } + + return nil +} + +var libc_sysctl_trampoline_addr uintptr + +// adapted from internal/cpu/cpu_arm64_darwin.go +func darwinSysctlEnabled(name []byte) bool { + out := int32(0) + nout := unsafe.Sizeof(out) + if ret := sysctlbyname(&name[0], (*byte)(unsafe.Pointer(&out)), &nout, nil, 0); ret != nil { + return false + } + return out > 0 +} + +//go:cgo_import_dynamic libc_sysctl sysctl "/usr/lib/libSystem.B.dylib" + +var libc_sysctlbyname_trampoline_addr uintptr + +// adapted from runtime/sys_darwin.go in the pattern of sysctl() above, as defined in x/sys/unix +func sysctlbyname(name *byte, old *byte, oldlen *uintptr, new *byte, newlen uintptr) error { + if _, _, err := syscall_syscall6( + libc_sysctlbyname_trampoline_addr, + uintptr(unsafe.Pointer(name)), + uintptr(unsafe.Pointer(old)), + uintptr(unsafe.Pointer(oldlen)), + uintptr(unsafe.Pointer(new)), + uintptr(newlen), + 0, + ); err != 0 { + return err + } + + return nil +} + +//go:cgo_import_dynamic libc_sysctlbyname sysctlbyname "/usr/lib/libSystem.B.dylib" + +// Implemented in the runtime package (runtime/sys_darwin.go) +func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno) + +//go:linkname syscall_syscall6 syscall.syscall6 diff --git a/vendor/golang.org/x/sys/unix/ioctl_linux.go b/vendor/golang.org/x/sys/unix/ioctl_linux.go index dbe680eab..7ca4fa12a 100644 --- a/vendor/golang.org/x/sys/unix/ioctl_linux.go +++ b/vendor/golang.org/x/sys/unix/ioctl_linux.go @@ -58,6 +58,102 @@ func IoctlGetEthtoolDrvinfo(fd int, ifname string) (*EthtoolDrvinfo, error) { return &value, err } +// IoctlGetEthtoolTsInfo fetches ethtool timestamping and PHC +// association for the network device specified by ifname. +func IoctlGetEthtoolTsInfo(fd int, ifname string) (*EthtoolTsInfo, error) { + ifr, err := NewIfreq(ifname) + if err != nil { + return nil, err + } + + value := EthtoolTsInfo{Cmd: ETHTOOL_GET_TS_INFO} + ifrd := ifr.withData(unsafe.Pointer(&value)) + + err = ioctlIfreqData(fd, SIOCETHTOOL, &ifrd) + return &value, err +} + +// IoctlGetHwTstamp retrieves the hardware timestamping configuration +// for the network device specified by ifname. +func IoctlGetHwTstamp(fd int, ifname string) (*HwTstampConfig, error) { + ifr, err := NewIfreq(ifname) + if err != nil { + return nil, err + } + + value := HwTstampConfig{} + ifrd := ifr.withData(unsafe.Pointer(&value)) + + err = ioctlIfreqData(fd, SIOCGHWTSTAMP, &ifrd) + return &value, err +} + +// IoctlSetHwTstamp updates the hardware timestamping configuration for +// the network device specified by ifname. +func IoctlSetHwTstamp(fd int, ifname string, cfg *HwTstampConfig) error { + ifr, err := NewIfreq(ifname) + if err != nil { + return err + } + ifrd := ifr.withData(unsafe.Pointer(cfg)) + return ioctlIfreqData(fd, SIOCSHWTSTAMP, &ifrd) +} + +// FdToClockID derives the clock ID from the file descriptor number +// - see clock_gettime(3), FD_TO_CLOCKID macros. The resulting ID is +// suitable for system calls like ClockGettime. +func FdToClockID(fd int) int32 { return int32((int(^fd) << 3) | 3) } + +// IoctlPtpClockGetcaps returns the description of a given PTP device. +func IoctlPtpClockGetcaps(fd int) (*PtpClockCaps, error) { + var value PtpClockCaps + err := ioctlPtr(fd, PTP_CLOCK_GETCAPS2, unsafe.Pointer(&value)) + return &value, err +} + +// IoctlPtpSysOffsetPrecise returns a description of the clock +// offset compared to the system clock. +func IoctlPtpSysOffsetPrecise(fd int) (*PtpSysOffsetPrecise, error) { + var value PtpSysOffsetPrecise + err := ioctlPtr(fd, PTP_SYS_OFFSET_PRECISE2, unsafe.Pointer(&value)) + return &value, err +} + +// IoctlPtpSysOffsetExtended returns an extended description of the +// clock offset compared to the system clock. The samples parameter +// specifies the desired number of measurements. +func IoctlPtpSysOffsetExtended(fd int, samples uint) (*PtpSysOffsetExtended, error) { + value := PtpSysOffsetExtended{Samples: uint32(samples)} + err := ioctlPtr(fd, PTP_SYS_OFFSET_EXTENDED2, unsafe.Pointer(&value)) + return &value, err +} + +// IoctlPtpPinGetfunc returns the configuration of the specified +// I/O pin on given PTP device. +func IoctlPtpPinGetfunc(fd int, index uint) (*PtpPinDesc, error) { + value := PtpPinDesc{Index: uint32(index)} + err := ioctlPtr(fd, PTP_PIN_GETFUNC2, unsafe.Pointer(&value)) + return &value, err +} + +// IoctlPtpPinSetfunc updates configuration of the specified PTP +// I/O pin. +func IoctlPtpPinSetfunc(fd int, pd *PtpPinDesc) error { + return ioctlPtr(fd, PTP_PIN_SETFUNC2, unsafe.Pointer(pd)) +} + +// IoctlPtpPeroutRequest configures the periodic output mode of the +// PTP I/O pins. +func IoctlPtpPeroutRequest(fd int, r *PtpPeroutRequest) error { + return ioctlPtr(fd, PTP_PEROUT_REQUEST2, unsafe.Pointer(r)) +} + +// IoctlPtpExttsRequest configures the external timestamping mode +// of the PTP I/O pins. +func IoctlPtpExttsRequest(fd int, r *PtpExttsRequest) error { + return ioctlPtr(fd, PTP_EXTTS_REQUEST2, unsafe.Pointer(r)) +} + // IoctlGetWatchdogInfo fetches information about a watchdog device from the // Linux watchdog API. For more information, see: // https://www.kernel.org/doc/html/latest/watchdog/watchdog-api.html. diff --git a/vendor/golang.org/x/sys/unix/mkerrors.sh b/vendor/golang.org/x/sys/unix/mkerrors.sh index ac54ecaba..6ab02b6c3 100644 --- a/vendor/golang.org/x/sys/unix/mkerrors.sh +++ b/vendor/golang.org/x/sys/unix/mkerrors.sh @@ -158,6 +158,16 @@ includes_Linux=' #endif #define _GNU_SOURCE +// See the description in unix/linux/types.go +#if defined(__ARM_EABI__) || \ + (defined(__mips__) && (_MIPS_SIM == _ABIO32)) || \ + (defined(__powerpc__) && (!defined(__powerpc64__))) +# ifdef _TIME_BITS +# undef _TIME_BITS +# endif +# define _TIME_BITS 32 +#endif + // is broken on powerpc64, as it fails to include definitions of // these structures. We just include them copied from . #if defined(__powerpc__) @@ -256,6 +266,7 @@ struct ltchars { #include #include #include +#include #include #include #include @@ -527,6 +538,7 @@ ccflags="$@" $2 ~ /^(AF|SOCK|SO|SOL|IPPROTO|IP|IPV6|TCP|MCAST|EVFILT|NOTE|SHUT|PROT|MAP|MREMAP|MFD|T?PACKET|MSG|SCM|MCL|DT|MADV|PR|LOCAL|TCPOPT|UDP)_/ || $2 ~ /^NFC_(GENL|PROTO|COMM|RF|SE|DIRECTION|LLCP|SOCKPROTO)_/ || $2 ~ /^NFC_.*_(MAX)?SIZE$/ || + $2 ~ /^PTP_/ || $2 ~ /^RAW_PAYLOAD_/ || $2 ~ /^[US]F_/ || $2 ~ /^TP_STATUS_/ || diff --git a/vendor/golang.org/x/sys/unix/syscall_linux.go b/vendor/golang.org/x/sys/unix/syscall_linux.go index f08abd434..230a94549 100644 --- a/vendor/golang.org/x/sys/unix/syscall_linux.go +++ b/vendor/golang.org/x/sys/unix/syscall_linux.go @@ -1860,6 +1860,7 @@ func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err e //sys ClockAdjtime(clockid int32, buf *Timex) (state int, err error) //sys ClockGetres(clockid int32, res *Timespec) (err error) //sys ClockGettime(clockid int32, time *Timespec) (err error) +//sys ClockSettime(clockid int32, time *Timespec) (err error) //sys ClockNanosleep(clockid int32, flags int, request *Timespec, remain *Timespec) (err error) //sys Close(fd int) (err error) //sys CloseRange(first uint, last uint, flags uint) (err error) diff --git a/vendor/golang.org/x/sys/unix/syscall_zos_s390x.go b/vendor/golang.org/x/sys/unix/syscall_zos_s390x.go index 312ae6ac1..7bf5c04bb 100644 --- a/vendor/golang.org/x/sys/unix/syscall_zos_s390x.go +++ b/vendor/golang.org/x/sys/unix/syscall_zos_s390x.go @@ -768,6 +768,15 @@ func Munmap(b []byte) (err error) { return mapper.Munmap(b) } +func MmapPtr(fd int, offset int64, addr unsafe.Pointer, length uintptr, prot int, flags int) (ret unsafe.Pointer, err error) { + xaddr, err := mapper.mmap(uintptr(addr), length, prot, flags, fd, offset) + return unsafe.Pointer(xaddr), err +} + +func MunmapPtr(addr unsafe.Pointer, length uintptr) (err error) { + return mapper.munmap(uintptr(addr), length) +} + //sys Gethostname(buf []byte) (err error) = SYS___GETHOSTNAME_A //sysnb Getgid() (gid int) //sysnb Getpid() (pid int) @@ -816,10 +825,10 @@ func Lstat(path string, stat *Stat_t) (err error) { // for checking symlinks begins with $VERSION/ $SYSNAME/ $SYSSYMR/ $SYSSYMA/ func isSpecialPath(path []byte) (v bool) { var special = [4][8]byte{ - [8]byte{'V', 'E', 'R', 'S', 'I', 'O', 'N', '/'}, - [8]byte{'S', 'Y', 'S', 'N', 'A', 'M', 'E', '/'}, - [8]byte{'S', 'Y', 'S', 'S', 'Y', 'M', 'R', '/'}, - [8]byte{'S', 'Y', 'S', 'S', 'Y', 'M', 'A', '/'}} + {'V', 'E', 'R', 'S', 'I', 'O', 'N', '/'}, + {'S', 'Y', 'S', 'N', 'A', 'M', 'E', '/'}, + {'S', 'Y', 'S', 'S', 'Y', 'M', 'R', '/'}, + {'S', 'Y', 'S', 'S', 'Y', 'M', 'A', '/'}} var i, j int for i = 0; i < len(special); i++ { @@ -3115,3 +3124,90 @@ func legacy_Mkfifoat(dirfd int, path string, mode uint32) (err error) { //sys Posix_openpt(oflag int) (fd int, err error) = SYS_POSIX_OPENPT //sys Grantpt(fildes int) (rc int, err error) = SYS_GRANTPT //sys Unlockpt(fildes int) (rc int, err error) = SYS_UNLOCKPT + +func fcntlAsIs(fd uintptr, cmd int, arg uintptr) (val int, err error) { + runtime.EnterSyscall() + r0, e2, e1 := CallLeFuncWithErr(GetZosLibVec()+SYS_FCNTL<<4, uintptr(fd), uintptr(cmd), arg) + runtime.ExitSyscall() + val = int(r0) + if int64(r0) == -1 { + err = errnoErr2(e1, e2) + } + return +} + +func Fcntl(fd uintptr, cmd int, op interface{}) (ret int, err error) { + switch op.(type) { + case *Flock_t: + err = FcntlFlock(fd, cmd, op.(*Flock_t)) + if err != nil { + ret = -1 + } + return + case int: + return FcntlInt(fd, cmd, op.(int)) + case *F_cnvrt: + return fcntlAsIs(fd, cmd, uintptr(unsafe.Pointer(op.(*F_cnvrt)))) + case unsafe.Pointer: + return fcntlAsIs(fd, cmd, uintptr(op.(unsafe.Pointer))) + default: + return -1, EINVAL + } + return +} + +func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err error) { + if raceenabled { + raceReleaseMerge(unsafe.Pointer(&ioSync)) + } + return sendfile(outfd, infd, offset, count) +} + +func sendfile(outfd int, infd int, offset *int64, count int) (written int, err error) { + // TODO: use LE call instead if the call is implemented + originalOffset, err := Seek(infd, 0, SEEK_CUR) + if err != nil { + return -1, err + } + //start reading data from in_fd + if offset != nil { + _, err := Seek(infd, *offset, SEEK_SET) + if err != nil { + return -1, err + } + } + + buf := make([]byte, count) + readBuf := make([]byte, 0) + var n int = 0 + for i := 0; i < count; i += n { + n, err := Read(infd, buf) + if n == 0 { + if err != nil { + return -1, err + } else { // EOF + break + } + } + readBuf = append(readBuf, buf...) + buf = buf[0:0] + } + + n2, err := Write(outfd, readBuf) + if err != nil { + return -1, err + } + + //When sendfile() returns, this variable will be set to the + // offset of the byte following the last byte that was read. + if offset != nil { + *offset = *offset + int64(n) + // If offset is not NULL, then sendfile() does not modify the file + // offset of in_fd + _, err := Seek(infd, originalOffset, SEEK_SET) + if err != nil { + return -1, err + } + } + return n2, nil +} diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux.go b/vendor/golang.org/x/sys/unix/zerrors_linux.go index de3b46248..ccba391c9 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux.go @@ -2625,6 +2625,28 @@ PR_UNALIGN_NOPRINT = 0x1 PR_UNALIGN_SIGBUS = 0x2 PSTOREFS_MAGIC = 0x6165676c + PTP_CLK_MAGIC = '=' + PTP_ENABLE_FEATURE = 0x1 + PTP_EXTTS_EDGES = 0x6 + PTP_EXTTS_EVENT_VALID = 0x1 + PTP_EXTTS_V1_VALID_FLAGS = 0x7 + PTP_EXTTS_VALID_FLAGS = 0x1f + PTP_EXT_OFFSET = 0x10 + PTP_FALLING_EDGE = 0x4 + PTP_MAX_SAMPLES = 0x19 + PTP_PEROUT_DUTY_CYCLE = 0x2 + PTP_PEROUT_ONE_SHOT = 0x1 + PTP_PEROUT_PHASE = 0x4 + PTP_PEROUT_V1_VALID_FLAGS = 0x0 + PTP_PEROUT_VALID_FLAGS = 0x7 + PTP_PIN_GETFUNC = 0xc0603d06 + PTP_PIN_GETFUNC2 = 0xc0603d0f + PTP_RISING_EDGE = 0x2 + PTP_STRICT_FLAGS = 0x8 + PTP_SYS_OFFSET_EXTENDED = 0xc4c03d09 + PTP_SYS_OFFSET_EXTENDED2 = 0xc4c03d12 + PTP_SYS_OFFSET_PRECISE = 0xc0403d08 + PTP_SYS_OFFSET_PRECISE2 = 0xc0403d11 PTRACE_ATTACH = 0x10 PTRACE_CONT = 0x7 PTRACE_DETACH = 0x11 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_386.go b/vendor/golang.org/x/sys/unix/zerrors_linux_386.go index 8aa6d77c0..0c00cb3f3 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_386.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_386.go @@ -237,6 +237,20 @@ PPPIOCUNBRIDGECHAN = 0x7434 PPPIOCXFERUNIT = 0x744e PR_SET_PTRACER_ANY = 0xffffffff + PTP_CLOCK_GETCAPS = 0x80503d01 + PTP_CLOCK_GETCAPS2 = 0x80503d0a + PTP_ENABLE_PPS = 0x40043d04 + PTP_ENABLE_PPS2 = 0x40043d0d + PTP_EXTTS_REQUEST = 0x40103d02 + PTP_EXTTS_REQUEST2 = 0x40103d0b + PTP_MASK_CLEAR_ALL = 0x3d13 + PTP_MASK_EN_SINGLE = 0x40043d14 + PTP_PEROUT_REQUEST = 0x40383d03 + PTP_PEROUT_REQUEST2 = 0x40383d0c + PTP_PIN_SETFUNC = 0x40603d07 + PTP_PIN_SETFUNC2 = 0x40603d10 + PTP_SYS_OFFSET = 0x43403d05 + PTP_SYS_OFFSET2 = 0x43403d0e PTRACE_GETFPREGS = 0xe PTRACE_GETFPXREGS = 0x12 PTRACE_GET_THREAD_AREA = 0x19 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go index da428f425..dfb364554 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go @@ -237,6 +237,20 @@ PPPIOCUNBRIDGECHAN = 0x7434 PPPIOCXFERUNIT = 0x744e PR_SET_PTRACER_ANY = 0xffffffffffffffff + PTP_CLOCK_GETCAPS = 0x80503d01 + PTP_CLOCK_GETCAPS2 = 0x80503d0a + PTP_ENABLE_PPS = 0x40043d04 + PTP_ENABLE_PPS2 = 0x40043d0d + PTP_EXTTS_REQUEST = 0x40103d02 + PTP_EXTTS_REQUEST2 = 0x40103d0b + PTP_MASK_CLEAR_ALL = 0x3d13 + PTP_MASK_EN_SINGLE = 0x40043d14 + PTP_PEROUT_REQUEST = 0x40383d03 + PTP_PEROUT_REQUEST2 = 0x40383d0c + PTP_PIN_SETFUNC = 0x40603d07 + PTP_PIN_SETFUNC2 = 0x40603d10 + PTP_SYS_OFFSET = 0x43403d05 + PTP_SYS_OFFSET2 = 0x43403d0e PTRACE_ARCH_PRCTL = 0x1e PTRACE_GETFPREGS = 0xe PTRACE_GETFPXREGS = 0x12 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go b/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go index bf45bfec7..d46dcf78a 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go @@ -234,6 +234,20 @@ PPPIOCUNBRIDGECHAN = 0x7434 PPPIOCXFERUNIT = 0x744e PR_SET_PTRACER_ANY = 0xffffffff + PTP_CLOCK_GETCAPS = 0x80503d01 + PTP_CLOCK_GETCAPS2 = 0x80503d0a + PTP_ENABLE_PPS = 0x40043d04 + PTP_ENABLE_PPS2 = 0x40043d0d + PTP_EXTTS_REQUEST = 0x40103d02 + PTP_EXTTS_REQUEST2 = 0x40103d0b + PTP_MASK_CLEAR_ALL = 0x3d13 + PTP_MASK_EN_SINGLE = 0x40043d14 + PTP_PEROUT_REQUEST = 0x40383d03 + PTP_PEROUT_REQUEST2 = 0x40383d0c + PTP_PIN_SETFUNC = 0x40603d07 + PTP_PIN_SETFUNC2 = 0x40603d10 + PTP_SYS_OFFSET = 0x43403d05 + PTP_SYS_OFFSET2 = 0x43403d0e PTRACE_GETCRUNCHREGS = 0x19 PTRACE_GETFDPIC = 0x1f PTRACE_GETFDPIC_EXEC = 0x0 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go index 71c67162b..3af3248a7 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go @@ -240,6 +240,20 @@ PROT_BTI = 0x10 PROT_MTE = 0x20 PR_SET_PTRACER_ANY = 0xffffffffffffffff + PTP_CLOCK_GETCAPS = 0x80503d01 + PTP_CLOCK_GETCAPS2 = 0x80503d0a + PTP_ENABLE_PPS = 0x40043d04 + PTP_ENABLE_PPS2 = 0x40043d0d + PTP_EXTTS_REQUEST = 0x40103d02 + PTP_EXTTS_REQUEST2 = 0x40103d0b + PTP_MASK_CLEAR_ALL = 0x3d13 + PTP_MASK_EN_SINGLE = 0x40043d14 + PTP_PEROUT_REQUEST = 0x40383d03 + PTP_PEROUT_REQUEST2 = 0x40383d0c + PTP_PIN_SETFUNC = 0x40603d07 + PTP_PIN_SETFUNC2 = 0x40603d10 + PTP_SYS_OFFSET = 0x43403d05 + PTP_SYS_OFFSET2 = 0x43403d0e PTRACE_PEEKMTETAGS = 0x21 PTRACE_POKEMTETAGS = 0x22 PTRACE_SYSEMU = 0x1f diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go index 9476628fa..292bcf028 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go @@ -238,6 +238,20 @@ PPPIOCUNBRIDGECHAN = 0x7434 PPPIOCXFERUNIT = 0x744e PR_SET_PTRACER_ANY = 0xffffffffffffffff + PTP_CLOCK_GETCAPS = 0x80503d01 + PTP_CLOCK_GETCAPS2 = 0x80503d0a + PTP_ENABLE_PPS = 0x40043d04 + PTP_ENABLE_PPS2 = 0x40043d0d + PTP_EXTTS_REQUEST = 0x40103d02 + PTP_EXTTS_REQUEST2 = 0x40103d0b + PTP_MASK_CLEAR_ALL = 0x3d13 + PTP_MASK_EN_SINGLE = 0x40043d14 + PTP_PEROUT_REQUEST = 0x40383d03 + PTP_PEROUT_REQUEST2 = 0x40383d0c + PTP_PIN_SETFUNC = 0x40603d07 + PTP_PIN_SETFUNC2 = 0x40603d10 + PTP_SYS_OFFSET = 0x43403d05 + PTP_SYS_OFFSET2 = 0x43403d0e PTRACE_SYSEMU = 0x1f PTRACE_SYSEMU_SINGLESTEP = 0x20 RLIMIT_AS = 0x9 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go index b9e85f3cf..782b7110f 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go @@ -234,6 +234,20 @@ PPPIOCUNBRIDGECHAN = 0x20007434 PPPIOCXFERUNIT = 0x2000744e PR_SET_PTRACER_ANY = 0xffffffff + PTP_CLOCK_GETCAPS = 0x40503d01 + PTP_CLOCK_GETCAPS2 = 0x40503d0a + PTP_ENABLE_PPS = 0x80043d04 + PTP_ENABLE_PPS2 = 0x80043d0d + PTP_EXTTS_REQUEST = 0x80103d02 + PTP_EXTTS_REQUEST2 = 0x80103d0b + PTP_MASK_CLEAR_ALL = 0x20003d13 + PTP_MASK_EN_SINGLE = 0x80043d14 + PTP_PEROUT_REQUEST = 0x80383d03 + PTP_PEROUT_REQUEST2 = 0x80383d0c + PTP_PIN_SETFUNC = 0x80603d07 + PTP_PIN_SETFUNC2 = 0x80603d10 + PTP_SYS_OFFSET = 0x83403d05 + PTP_SYS_OFFSET2 = 0x83403d0e PTRACE_GETFPREGS = 0xe PTRACE_GET_THREAD_AREA = 0x19 PTRACE_GET_THREAD_AREA_3264 = 0xc4 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go index a48b68a76..84973fd92 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go @@ -234,6 +234,20 @@ PPPIOCUNBRIDGECHAN = 0x20007434 PPPIOCXFERUNIT = 0x2000744e PR_SET_PTRACER_ANY = 0xffffffffffffffff + PTP_CLOCK_GETCAPS = 0x40503d01 + PTP_CLOCK_GETCAPS2 = 0x40503d0a + PTP_ENABLE_PPS = 0x80043d04 + PTP_ENABLE_PPS2 = 0x80043d0d + PTP_EXTTS_REQUEST = 0x80103d02 + PTP_EXTTS_REQUEST2 = 0x80103d0b + PTP_MASK_CLEAR_ALL = 0x20003d13 + PTP_MASK_EN_SINGLE = 0x80043d14 + PTP_PEROUT_REQUEST = 0x80383d03 + PTP_PEROUT_REQUEST2 = 0x80383d0c + PTP_PIN_SETFUNC = 0x80603d07 + PTP_PIN_SETFUNC2 = 0x80603d10 + PTP_SYS_OFFSET = 0x83403d05 + PTP_SYS_OFFSET2 = 0x83403d0e PTRACE_GETFPREGS = 0xe PTRACE_GET_THREAD_AREA = 0x19 PTRACE_GET_THREAD_AREA_3264 = 0xc4 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go index ea00e8522..6d9cbc3b2 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go @@ -234,6 +234,20 @@ PPPIOCUNBRIDGECHAN = 0x20007434 PPPIOCXFERUNIT = 0x2000744e PR_SET_PTRACER_ANY = 0xffffffffffffffff + PTP_CLOCK_GETCAPS = 0x40503d01 + PTP_CLOCK_GETCAPS2 = 0x40503d0a + PTP_ENABLE_PPS = 0x80043d04 + PTP_ENABLE_PPS2 = 0x80043d0d + PTP_EXTTS_REQUEST = 0x80103d02 + PTP_EXTTS_REQUEST2 = 0x80103d0b + PTP_MASK_CLEAR_ALL = 0x20003d13 + PTP_MASK_EN_SINGLE = 0x80043d14 + PTP_PEROUT_REQUEST = 0x80383d03 + PTP_PEROUT_REQUEST2 = 0x80383d0c + PTP_PIN_SETFUNC = 0x80603d07 + PTP_PIN_SETFUNC2 = 0x80603d10 + PTP_SYS_OFFSET = 0x83403d05 + PTP_SYS_OFFSET2 = 0x83403d0e PTRACE_GETFPREGS = 0xe PTRACE_GET_THREAD_AREA = 0x19 PTRACE_GET_THREAD_AREA_3264 = 0xc4 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go index 91c646871..5f9fedbce 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go @@ -234,6 +234,20 @@ PPPIOCUNBRIDGECHAN = 0x20007434 PPPIOCXFERUNIT = 0x2000744e PR_SET_PTRACER_ANY = 0xffffffff + PTP_CLOCK_GETCAPS = 0x40503d01 + PTP_CLOCK_GETCAPS2 = 0x40503d0a + PTP_ENABLE_PPS = 0x80043d04 + PTP_ENABLE_PPS2 = 0x80043d0d + PTP_EXTTS_REQUEST = 0x80103d02 + PTP_EXTTS_REQUEST2 = 0x80103d0b + PTP_MASK_CLEAR_ALL = 0x20003d13 + PTP_MASK_EN_SINGLE = 0x80043d14 + PTP_PEROUT_REQUEST = 0x80383d03 + PTP_PEROUT_REQUEST2 = 0x80383d0c + PTP_PIN_SETFUNC = 0x80603d07 + PTP_PIN_SETFUNC2 = 0x80603d10 + PTP_SYS_OFFSET = 0x83403d05 + PTP_SYS_OFFSET2 = 0x83403d0e PTRACE_GETFPREGS = 0xe PTRACE_GET_THREAD_AREA = 0x19 PTRACE_GET_THREAD_AREA_3264 = 0xc4 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go index 8cbf38d63..bb0026ee0 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go @@ -237,6 +237,20 @@ PPPIOCXFERUNIT = 0x2000744e PROT_SAO = 0x10 PR_SET_PTRACER_ANY = 0xffffffff + PTP_CLOCK_GETCAPS = 0x40503d01 + PTP_CLOCK_GETCAPS2 = 0x40503d0a + PTP_ENABLE_PPS = 0x80043d04 + PTP_ENABLE_PPS2 = 0x80043d0d + PTP_EXTTS_REQUEST = 0x80103d02 + PTP_EXTTS_REQUEST2 = 0x80103d0b + PTP_MASK_CLEAR_ALL = 0x20003d13 + PTP_MASK_EN_SINGLE = 0x80043d14 + PTP_PEROUT_REQUEST = 0x80383d03 + PTP_PEROUT_REQUEST2 = 0x80383d0c + PTP_PIN_SETFUNC = 0x80603d07 + PTP_PIN_SETFUNC2 = 0x80603d10 + PTP_SYS_OFFSET = 0x83403d05 + PTP_SYS_OFFSET2 = 0x83403d0e PTRACE_GETEVRREGS = 0x14 PTRACE_GETFPREGS = 0xe PTRACE_GETREGS64 = 0x16 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go index a2df73419..46120db5c 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go @@ -237,6 +237,20 @@ PPPIOCXFERUNIT = 0x2000744e PROT_SAO = 0x10 PR_SET_PTRACER_ANY = 0xffffffffffffffff + PTP_CLOCK_GETCAPS = 0x40503d01 + PTP_CLOCK_GETCAPS2 = 0x40503d0a + PTP_ENABLE_PPS = 0x80043d04 + PTP_ENABLE_PPS2 = 0x80043d0d + PTP_EXTTS_REQUEST = 0x80103d02 + PTP_EXTTS_REQUEST2 = 0x80103d0b + PTP_MASK_CLEAR_ALL = 0x20003d13 + PTP_MASK_EN_SINGLE = 0x80043d14 + PTP_PEROUT_REQUEST = 0x80383d03 + PTP_PEROUT_REQUEST2 = 0x80383d0c + PTP_PIN_SETFUNC = 0x80603d07 + PTP_PIN_SETFUNC2 = 0x80603d10 + PTP_SYS_OFFSET = 0x83403d05 + PTP_SYS_OFFSET2 = 0x83403d0e PTRACE_GETEVRREGS = 0x14 PTRACE_GETFPREGS = 0xe PTRACE_GETREGS64 = 0x16 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go index 247913792..5c951634f 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go @@ -237,6 +237,20 @@ PPPIOCXFERUNIT = 0x2000744e PROT_SAO = 0x10 PR_SET_PTRACER_ANY = 0xffffffffffffffff + PTP_CLOCK_GETCAPS = 0x40503d01 + PTP_CLOCK_GETCAPS2 = 0x40503d0a + PTP_ENABLE_PPS = 0x80043d04 + PTP_ENABLE_PPS2 = 0x80043d0d + PTP_EXTTS_REQUEST = 0x80103d02 + PTP_EXTTS_REQUEST2 = 0x80103d0b + PTP_MASK_CLEAR_ALL = 0x20003d13 + PTP_MASK_EN_SINGLE = 0x80043d14 + PTP_PEROUT_REQUEST = 0x80383d03 + PTP_PEROUT_REQUEST2 = 0x80383d0c + PTP_PIN_SETFUNC = 0x80603d07 + PTP_PIN_SETFUNC2 = 0x80603d10 + PTP_SYS_OFFSET = 0x83403d05 + PTP_SYS_OFFSET2 = 0x83403d0e PTRACE_GETEVRREGS = 0x14 PTRACE_GETFPREGS = 0xe PTRACE_GETREGS64 = 0x16 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go index d265f146e..11a84d5af 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go @@ -234,6 +234,20 @@ PPPIOCUNBRIDGECHAN = 0x7434 PPPIOCXFERUNIT = 0x744e PR_SET_PTRACER_ANY = 0xffffffffffffffff + PTP_CLOCK_GETCAPS = 0x80503d01 + PTP_CLOCK_GETCAPS2 = 0x80503d0a + PTP_ENABLE_PPS = 0x40043d04 + PTP_ENABLE_PPS2 = 0x40043d0d + PTP_EXTTS_REQUEST = 0x40103d02 + PTP_EXTTS_REQUEST2 = 0x40103d0b + PTP_MASK_CLEAR_ALL = 0x3d13 + PTP_MASK_EN_SINGLE = 0x40043d14 + PTP_PEROUT_REQUEST = 0x40383d03 + PTP_PEROUT_REQUEST2 = 0x40383d0c + PTP_PIN_SETFUNC = 0x40603d07 + PTP_PIN_SETFUNC2 = 0x40603d10 + PTP_SYS_OFFSET = 0x43403d05 + PTP_SYS_OFFSET2 = 0x43403d0e PTRACE_GETFDPIC = 0x21 PTRACE_GETFDPIC_EXEC = 0x0 PTRACE_GETFDPIC_INTERP = 0x1 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go b/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go index 3f2d64439..f78c4617c 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go @@ -234,6 +234,20 @@ PPPIOCUNBRIDGECHAN = 0x7434 PPPIOCXFERUNIT = 0x744e PR_SET_PTRACER_ANY = 0xffffffffffffffff + PTP_CLOCK_GETCAPS = 0x80503d01 + PTP_CLOCK_GETCAPS2 = 0x80503d0a + PTP_ENABLE_PPS = 0x40043d04 + PTP_ENABLE_PPS2 = 0x40043d0d + PTP_EXTTS_REQUEST = 0x40103d02 + PTP_EXTTS_REQUEST2 = 0x40103d0b + PTP_MASK_CLEAR_ALL = 0x3d13 + PTP_MASK_EN_SINGLE = 0x40043d14 + PTP_PEROUT_REQUEST = 0x40383d03 + PTP_PEROUT_REQUEST2 = 0x40383d0c + PTP_PIN_SETFUNC = 0x40603d07 + PTP_PIN_SETFUNC2 = 0x40603d10 + PTP_SYS_OFFSET = 0x43403d05 + PTP_SYS_OFFSET2 = 0x43403d0e PTRACE_DISABLE_TE = 0x5010 PTRACE_ENABLE_TE = 0x5009 PTRACE_GET_LAST_BREAK = 0x5006 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go index 5d8b727a1..aeb777c34 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go @@ -239,6 +239,20 @@ PPPIOCUNBRIDGECHAN = 0x20007434 PPPIOCXFERUNIT = 0x2000744e PR_SET_PTRACER_ANY = 0xffffffffffffffff + PTP_CLOCK_GETCAPS = 0x40503d01 + PTP_CLOCK_GETCAPS2 = 0x40503d0a + PTP_ENABLE_PPS = 0x80043d04 + PTP_ENABLE_PPS2 = 0x80043d0d + PTP_EXTTS_REQUEST = 0x80103d02 + PTP_EXTTS_REQUEST2 = 0x80103d0b + PTP_MASK_CLEAR_ALL = 0x20003d13 + PTP_MASK_EN_SINGLE = 0x80043d14 + PTP_PEROUT_REQUEST = 0x80383d03 + PTP_PEROUT_REQUEST2 = 0x80383d0c + PTP_PIN_SETFUNC = 0x80603d07 + PTP_PIN_SETFUNC2 = 0x80603d10 + PTP_SYS_OFFSET = 0x83403d05 + PTP_SYS_OFFSET2 = 0x83403d0e PTRACE_GETFPAREGS = 0x14 PTRACE_GETFPREGS = 0xe PTRACE_GETFPREGS64 = 0x19 diff --git a/vendor/golang.org/x/sys/unix/zsyscall_linux.go b/vendor/golang.org/x/sys/unix/zsyscall_linux.go index af30da557..5cc1e8eb2 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_linux.go +++ b/vendor/golang.org/x/sys/unix/zsyscall_linux.go @@ -592,6 +592,16 @@ func ClockGettime(clockid int32, time *Timespec) (err error) { // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func ClockSettime(clockid int32, time *Timespec) (err error) { + _, _, e1 := Syscall(SYS_CLOCK_SETTIME, uintptr(clockid), uintptr(unsafe.Pointer(time)), 0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func ClockNanosleep(clockid int32, flags int, request *Timespec, remain *Timespec) (err error) { _, _, e1 := Syscall6(SYS_CLOCK_NANOSLEEP, uintptr(clockid), uintptr(flags), uintptr(unsafe.Pointer(request)), uintptr(unsafe.Pointer(remain)), 0, 0) if e1 != 0 { diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go index 3a69e4549..8daaf3faf 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go @@ -1752,12 +1752,6 @@ type TpacketAuxdata struct { IFLA_IPVLAN_UNSPEC = 0x0 IFLA_IPVLAN_MODE = 0x1 IFLA_IPVLAN_FLAGS = 0x2 - NETKIT_NEXT = -0x1 - NETKIT_PASS = 0x0 - NETKIT_DROP = 0x2 - NETKIT_REDIRECT = 0x7 - NETKIT_L2 = 0x0 - NETKIT_L3 = 0x1 IFLA_NETKIT_UNSPEC = 0x0 IFLA_NETKIT_PEER_INFO = 0x1 IFLA_NETKIT_PRIMARY = 0x2 @@ -1796,6 +1790,7 @@ type TpacketAuxdata struct { IFLA_VXLAN_DF = 0x1d IFLA_VXLAN_VNIFILTER = 0x1e IFLA_VXLAN_LOCALBYPASS = 0x1f + IFLA_VXLAN_LABEL_POLICY = 0x20 IFLA_GENEVE_UNSPEC = 0x0 IFLA_GENEVE_ID = 0x1 IFLA_GENEVE_REMOTE = 0x2 @@ -1825,6 +1820,8 @@ type TpacketAuxdata struct { IFLA_GTP_ROLE = 0x4 IFLA_GTP_CREATE_SOCKETS = 0x5 IFLA_GTP_RESTART_COUNT = 0x6 + IFLA_GTP_LOCAL = 0x7 + IFLA_GTP_LOCAL6 = 0x8 IFLA_BOND_UNSPEC = 0x0 IFLA_BOND_MODE = 0x1 IFLA_BOND_ACTIVE_SLAVE = 0x2 @@ -1857,6 +1854,7 @@ type TpacketAuxdata struct { IFLA_BOND_AD_LACP_ACTIVE = 0x1d IFLA_BOND_MISSED_MAX = 0x1e IFLA_BOND_NS_IP6_TARGET = 0x1f + IFLA_BOND_COUPLED_CONTROL = 0x20 IFLA_BOND_AD_INFO_UNSPEC = 0x0 IFLA_BOND_AD_INFO_AGGREGATOR = 0x1 IFLA_BOND_AD_INFO_NUM_PORTS = 0x2 @@ -1925,6 +1923,7 @@ type TpacketAuxdata struct { IFLA_HSR_SEQ_NR = 0x5 IFLA_HSR_VERSION = 0x6 IFLA_HSR_PROTOCOL = 0x7 + IFLA_HSR_INTERLINK = 0x8 IFLA_STATS_UNSPEC = 0x0 IFLA_STATS_LINK_64 = 0x1 IFLA_STATS_LINK_XSTATS = 0x2 @@ -1977,6 +1976,15 @@ type TpacketAuxdata struct { IFLA_DSA_MASTER = 0x1 ) +const ( + NETKIT_NEXT = -0x1 + NETKIT_PASS = 0x0 + NETKIT_DROP = 0x2 + NETKIT_REDIRECT = 0x7 + NETKIT_L2 = 0x0 + NETKIT_L3 = 0x1 +) + const ( NF_INET_PRE_ROUTING = 0x0 NF_INET_LOCAL_IN = 0x1 @@ -4110,6 +4118,106 @@ type EthtoolDrvinfo struct { Regdump_len uint32 } +type EthtoolTsInfo struct { + Cmd uint32 + So_timestamping uint32 + Phc_index int32 + Tx_types uint32 + Tx_reserved [3]uint32 + Rx_filters uint32 + Rx_reserved [3]uint32 +} + +type HwTstampConfig struct { + Flags int32 + Tx_type int32 + Rx_filter int32 +} + +const ( + HWTSTAMP_FILTER_NONE = 0x0 + HWTSTAMP_FILTER_ALL = 0x1 + HWTSTAMP_FILTER_SOME = 0x2 + HWTSTAMP_FILTER_PTP_V1_L4_EVENT = 0x3 + HWTSTAMP_FILTER_PTP_V2_L4_EVENT = 0x6 + HWTSTAMP_FILTER_PTP_V2_L2_EVENT = 0x9 + HWTSTAMP_FILTER_PTP_V2_EVENT = 0xc +) + +const ( + HWTSTAMP_TX_OFF = 0x0 + HWTSTAMP_TX_ON = 0x1 + HWTSTAMP_TX_ONESTEP_SYNC = 0x2 +) + +type ( + PtpClockCaps struct { + Max_adj int32 + N_alarm int32 + N_ext_ts int32 + N_per_out int32 + Pps int32 + N_pins int32 + Cross_timestamping int32 + Adjust_phase int32 + Max_phase_adj int32 + Rsv [11]int32 + } + PtpClockTime struct { + Sec int64 + Nsec uint32 + Reserved uint32 + } + PtpExttsEvent struct { + T PtpClockTime + Index uint32 + Flags uint32 + Rsv [2]uint32 + } + PtpExttsRequest struct { + Index uint32 + Flags uint32 + Rsv [2]uint32 + } + PtpPeroutRequest struct { + StartOrPhase PtpClockTime + Period PtpClockTime + Index uint32 + Flags uint32 + On PtpClockTime + } + PtpPinDesc struct { + Name [64]byte + Index uint32 + Func uint32 + Chan uint32 + Rsv [5]uint32 + } + PtpSysOffset struct { + Samples uint32 + Rsv [3]uint32 + Ts [51]PtpClockTime + } + PtpSysOffsetExtended struct { + Samples uint32 + Rsv [3]uint32 + Ts [25][3]PtpClockTime + } + PtpSysOffsetPrecise struct { + Device PtpClockTime + Realtime PtpClockTime + Monoraw PtpClockTime + Rsv [4]uint32 + } +) + +const ( + PTP_PF_NONE = 0x0 + PTP_PF_EXTTS = 0x1 + PTP_PF_PEROUT = 0x2 + PTP_PF_PHYSYNC = 0x3 +) + type ( HIDRawReportDescriptor struct { Size uint32 diff --git a/vendor/golang.org/x/sys/unix/ztypes_zos_s390x.go b/vendor/golang.org/x/sys/unix/ztypes_zos_s390x.go index d9a13af46..2e5d5a443 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_zos_s390x.go +++ b/vendor/golang.org/x/sys/unix/ztypes_zos_s390x.go @@ -377,6 +377,12 @@ type Flock_t struct { Pid int32 } +type F_cnvrt struct { + Cvtcmd int32 + Pccsid int16 + Fccsid int16 +} + type Termios struct { Cflag uint32 Iflag uint32 diff --git a/vendor/golang.org/x/sys/windows/syscall_windows.go b/vendor/golang.org/x/sys/windows/syscall_windows.go index 5cee9a314..4510bfc3f 100644 --- a/vendor/golang.org/x/sys/windows/syscall_windows.go +++ b/vendor/golang.org/x/sys/windows/syscall_windows.go @@ -725,20 +725,12 @@ func DurationSinceBoot() time.Duration { } func Ftruncate(fd Handle, length int64) (err error) { - curoffset, e := Seek(fd, 0, 1) - if e != nil { - return e + type _FILE_END_OF_FILE_INFO struct { + EndOfFile int64 } - defer Seek(fd, curoffset, 0) - _, e = Seek(fd, length, 0) - if e != nil { - return e - } - e = SetEndOfFile(fd) - if e != nil { - return e - } - return nil + var info _FILE_END_OF_FILE_INFO + info.EndOfFile = length + return SetFileInformationByHandle(fd, FileEndOfFileInfo, (*byte)(unsafe.Pointer(&info)), uint32(unsafe.Sizeof(info))) } func Gettimeofday(tv *Timeval) (err error) { @@ -894,6 +886,11 @@ func WaitForMultipleObjects(handles []Handle, waitAll bool, waitMilliseconds uin //sys GetACP() (acp uint32) = kernel32.GetACP //sys MultiByteToWideChar(codePage uint32, dwFlags uint32, str *byte, nstr int32, wchar *uint16, nwchar int32) (nwrite int32, err error) = kernel32.MultiByteToWideChar //sys getBestInterfaceEx(sockaddr unsafe.Pointer, pdwBestIfIndex *uint32) (errcode error) = iphlpapi.GetBestInterfaceEx +//sys GetIfEntry2Ex(level uint32, row *MibIfRow2) (errcode error) = iphlpapi.GetIfEntry2Ex +//sys GetUnicastIpAddressEntry(row *MibUnicastIpAddressRow) (errcode error) = iphlpapi.GetUnicastIpAddressEntry +//sys NotifyIpInterfaceChange(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) = iphlpapi.NotifyIpInterfaceChange +//sys NotifyUnicastIpAddressChange(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) = iphlpapi.NotifyUnicastIpAddressChange +//sys CancelMibChangeNotify2(notificationHandle Handle) (errcode error) = iphlpapi.CancelMibChangeNotify2 // For testing: clients can set this flag to force // creation of IPv6 sockets to return EAFNOSUPPORT. @@ -1685,13 +1682,16 @@ func (s NTStatus) Error() string { // do not use NTUnicodeString, and instead UTF16PtrFromString should be used for // the more common *uint16 string type. func NewNTUnicodeString(s string) (*NTUnicodeString, error) { - var u NTUnicodeString - s16, err := UTF16PtrFromString(s) + s16, err := UTF16FromString(s) if err != nil { return nil, err } - RtlInitUnicodeString(&u, s16) - return &u, nil + n := uint16(len(s16) * 2) + return &NTUnicodeString{ + Length: n - 2, // subtract 2 bytes for the NULL terminator + MaximumLength: n, + Buffer: &s16[0], + }, nil } // Slice returns a uint16 slice that aliases the data in the NTUnicodeString. diff --git a/vendor/golang.org/x/sys/windows/types_windows.go b/vendor/golang.org/x/sys/windows/types_windows.go index 7b97a154c..51311e205 100644 --- a/vendor/golang.org/x/sys/windows/types_windows.go +++ b/vendor/golang.org/x/sys/windows/types_windows.go @@ -2203,6 +2203,132 @@ type IpAdapterDNSSuffix struct { IfOperStatusLowerLayerDown = 7 ) +const ( + IF_MAX_PHYS_ADDRESS_LENGTH = 32 + IF_MAX_STRING_SIZE = 256 +) + +// MIB_IF_ENTRY_LEVEL enumeration from netioapi.h or +// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getifentry2ex. +const ( + MibIfEntryNormal = 0 + MibIfEntryNormalWithoutStatistics = 2 +) + +// MIB_NOTIFICATION_TYPE enumeration from netioapi.h or +// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ne-netioapi-mib_notification_type. +const ( + MibParameterNotification = 0 + MibAddInstance = 1 + MibDeleteInstance = 2 + MibInitialNotification = 3 +) + +// MibIfRow2 stores information about a particular interface. See +// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_if_row2. +type MibIfRow2 struct { + InterfaceLuid uint64 + InterfaceIndex uint32 + InterfaceGuid GUID + Alias [IF_MAX_STRING_SIZE + 1]uint16 + Description [IF_MAX_STRING_SIZE + 1]uint16 + PhysicalAddressLength uint32 + PhysicalAddress [IF_MAX_PHYS_ADDRESS_LENGTH]uint8 + PermanentPhysicalAddress [IF_MAX_PHYS_ADDRESS_LENGTH]uint8 + Mtu uint32 + Type uint32 + TunnelType uint32 + MediaType uint32 + PhysicalMediumType uint32 + AccessType uint32 + DirectionType uint32 + InterfaceAndOperStatusFlags uint8 + OperStatus uint32 + AdminStatus uint32 + MediaConnectState uint32 + NetworkGuid GUID + ConnectionType uint32 + TransmitLinkSpeed uint64 + ReceiveLinkSpeed uint64 + InOctets uint64 + InUcastPkts uint64 + InNUcastPkts uint64 + InDiscards uint64 + InErrors uint64 + InUnknownProtos uint64 + InUcastOctets uint64 + InMulticastOctets uint64 + InBroadcastOctets uint64 + OutOctets uint64 + OutUcastPkts uint64 + OutNUcastPkts uint64 + OutDiscards uint64 + OutErrors uint64 + OutUcastOctets uint64 + OutMulticastOctets uint64 + OutBroadcastOctets uint64 + OutQLen uint64 +} + +// MIB_UNICASTIPADDRESS_ROW stores information about a unicast IP address. See +// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_unicastipaddress_row. +type MibUnicastIpAddressRow struct { + Address RawSockaddrInet6 // SOCKADDR_INET union + InterfaceLuid uint64 + InterfaceIndex uint32 + PrefixOrigin uint32 + SuffixOrigin uint32 + ValidLifetime uint32 + PreferredLifetime uint32 + OnLinkPrefixLength uint8 + SkipAsSource uint8 + DadState uint32 + ScopeId uint32 + CreationTimeStamp Filetime +} + +const ScopeLevelCount = 16 + +// MIB_IPINTERFACE_ROW stores interface management information for a particular IP address family on a network interface. +// See https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_ipinterface_row. +type MibIpInterfaceRow struct { + Family uint16 + InterfaceLuid uint64 + InterfaceIndex uint32 + MaxReassemblySize uint32 + InterfaceIdentifier uint64 + MinRouterAdvertisementInterval uint32 + MaxRouterAdvertisementInterval uint32 + AdvertisingEnabled uint8 + ForwardingEnabled uint8 + WeakHostSend uint8 + WeakHostReceive uint8 + UseAutomaticMetric uint8 + UseNeighborUnreachabilityDetection uint8 + ManagedAddressConfigurationSupported uint8 + OtherStatefulConfigurationSupported uint8 + AdvertiseDefaultRoute uint8 + RouterDiscoveryBehavior uint32 + DadTransmits uint32 + BaseReachableTime uint32 + RetransmitTime uint32 + PathMtuDiscoveryTimeout uint32 + LinkLocalAddressBehavior uint32 + LinkLocalAddressTimeout uint32 + ZoneIndices [ScopeLevelCount]uint32 + SitePrefixLength uint32 + Metric uint32 + NlMtu uint32 + Connected uint8 + SupportsWakeUpPatterns uint8 + SupportsNeighborDiscovery uint8 + SupportsRouterDiscovery uint8 + ReachableTime uint32 + TransmitOffload uint32 + ReceiveOffload uint32 + DisableDefaultRoutes uint8 +} + // Console related constants used for the mode parameter to SetConsoleMode. See // https://docs.microsoft.com/en-us/windows/console/setconsolemode for details. diff --git a/vendor/golang.org/x/sys/windows/zsyscall_windows.go b/vendor/golang.org/x/sys/windows/zsyscall_windows.go index 4c2e1bdc0..6f5252880 100644 --- a/vendor/golang.org/x/sys/windows/zsyscall_windows.go +++ b/vendor/golang.org/x/sys/windows/zsyscall_windows.go @@ -181,10 +181,15 @@ func errnoErr(e syscall.Errno) error { procDnsRecordListFree = moddnsapi.NewProc("DnsRecordListFree") procDwmGetWindowAttribute = moddwmapi.NewProc("DwmGetWindowAttribute") procDwmSetWindowAttribute = moddwmapi.NewProc("DwmSetWindowAttribute") + procCancelMibChangeNotify2 = modiphlpapi.NewProc("CancelMibChangeNotify2") procGetAdaptersAddresses = modiphlpapi.NewProc("GetAdaptersAddresses") procGetAdaptersInfo = modiphlpapi.NewProc("GetAdaptersInfo") procGetBestInterfaceEx = modiphlpapi.NewProc("GetBestInterfaceEx") procGetIfEntry = modiphlpapi.NewProc("GetIfEntry") + procGetIfEntry2Ex = modiphlpapi.NewProc("GetIfEntry2Ex") + procGetUnicastIpAddressEntry = modiphlpapi.NewProc("GetUnicastIpAddressEntry") + procNotifyIpInterfaceChange = modiphlpapi.NewProc("NotifyIpInterfaceChange") + procNotifyUnicastIpAddressChange = modiphlpapi.NewProc("NotifyUnicastIpAddressChange") procAddDllDirectory = modkernel32.NewProc("AddDllDirectory") procAssignProcessToJobObject = modkernel32.NewProc("AssignProcessToJobObject") procCancelIo = modkernel32.NewProc("CancelIo") @@ -1606,6 +1611,14 @@ func DwmSetWindowAttribute(hwnd HWND, attribute uint32, value unsafe.Pointer, si return } +func CancelMibChangeNotify2(notificationHandle Handle) (errcode error) { + r0, _, _ := syscall.SyscallN(procCancelMibChangeNotify2.Addr(), uintptr(notificationHandle)) + if r0 != 0 { + errcode = syscall.Errno(r0) + } + return +} + func GetAdaptersAddresses(family uint32, flags uint32, reserved uintptr, adapterAddresses *IpAdapterAddresses, sizePointer *uint32) (errcode error) { r0, _, _ := syscall.Syscall6(procGetAdaptersAddresses.Addr(), 5, uintptr(family), uintptr(flags), uintptr(reserved), uintptr(unsafe.Pointer(adapterAddresses)), uintptr(unsafe.Pointer(sizePointer)), 0) if r0 != 0 { @@ -1638,6 +1651,46 @@ func GetIfEntry(pIfRow *MibIfRow) (errcode error) { return } +func GetIfEntry2Ex(level uint32, row *MibIfRow2) (errcode error) { + r0, _, _ := syscall.SyscallN(procGetIfEntry2Ex.Addr(), uintptr(level), uintptr(unsafe.Pointer(row))) + if r0 != 0 { + errcode = syscall.Errno(r0) + } + return +} + +func GetUnicastIpAddressEntry(row *MibUnicastIpAddressRow) (errcode error) { + r0, _, _ := syscall.SyscallN(procGetUnicastIpAddressEntry.Addr(), uintptr(unsafe.Pointer(row))) + if r0 != 0 { + errcode = syscall.Errno(r0) + } + return +} + +func NotifyIpInterfaceChange(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) { + var _p0 uint32 + if initialNotification { + _p0 = 1 + } + r0, _, _ := syscall.SyscallN(procNotifyIpInterfaceChange.Addr(), uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle))) + if r0 != 0 { + errcode = syscall.Errno(r0) + } + return +} + +func NotifyUnicastIpAddressChange(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) { + var _p0 uint32 + if initialNotification { + _p0 = 1 + } + r0, _, _ := syscall.SyscallN(procNotifyUnicastIpAddressChange.Addr(), uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle))) + if r0 != 0 { + errcode = syscall.Errno(r0) + } + return +} + func AddDllDirectory(path *uint16) (cookie uintptr, err error) { r0, _, e1 := syscall.Syscall(procAddDllDirectory.Addr(), 1, uintptr(unsafe.Pointer(path)), 0, 0) cookie = uintptr(r0) diff --git a/vendor/modules.txt b/vendor/modules.txt index 10c1e595c..a5465ec96 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -24,10 +24,9 @@ codeberg.org/gruf/go-fastcopy # codeberg.org/gruf/go-fastpath/v2 v2.0.0 ## explicit; go 1.14 codeberg.org/gruf/go-fastpath/v2 -# codeberg.org/gruf/go-ffmpreg v0.4.2 +# codeberg.org/gruf/go-ffmpreg v0.6.0 ## explicit; go 1.22.0 -codeberg.org/gruf/go-ffmpreg/embed/ffmpeg -codeberg.org/gruf/go-ffmpreg/embed/ffprobe +codeberg.org/gruf/go-ffmpreg/embed codeberg.org/gruf/go-ffmpreg/wasm # codeberg.org/gruf/go-iotools v0.0.0-20240710125620-934ae9c654cf ## explicit; go 1.21 @@ -515,7 +514,7 @@ github.com/modern-go/reflect2 # github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 ## explicit github.com/munnerz/goautoneg -# github.com/ncruces/go-sqlite3 v0.20.0 +# github.com/ncruces/go-sqlite3 v0.20.2 ## explicit; go 1.21 github.com/ncruces/go-sqlite3 github.com/ncruces/go-sqlite3/driver @@ -573,6 +572,9 @@ github.com/prometheus/common/model github.com/prometheus/procfs github.com/prometheus/procfs/internal/fs github.com/prometheus/procfs/internal/util +# github.com/puzpuzpuz/xsync/v3 v3.4.0 +## explicit; go 1.18 +github.com/puzpuzpuz/xsync/v3 # github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b ## explicit github.com/quasoft/memstore @@ -916,8 +918,8 @@ github.com/ugorji/go/codec github.com/ulule/limiter/v3 github.com/ulule/limiter/v3/drivers/store/common github.com/ulule/limiter/v3/drivers/store/memory -# github.com/uptrace/bun v1.2.1 -## explicit; go 1.21 +# github.com/uptrace/bun v1.2.5 +## explicit; go 1.22 github.com/uptrace/bun github.com/uptrace/bun/dialect github.com/uptrace/bun/dialect/feature @@ -928,17 +930,17 @@ github.com/uptrace/bun/internal/parser github.com/uptrace/bun/internal/tagparser github.com/uptrace/bun/migrate github.com/uptrace/bun/schema -# github.com/uptrace/bun/dialect/pgdialect v1.2.1 -## explicit; go 1.21 +# github.com/uptrace/bun/dialect/pgdialect v1.2.5 +## explicit; go 1.22 github.com/uptrace/bun/dialect/pgdialect -# github.com/uptrace/bun/dialect/sqlitedialect v1.2.1 -## explicit; go 1.21 +# github.com/uptrace/bun/dialect/sqlitedialect v1.2.5 +## explicit; go 1.22 github.com/uptrace/bun/dialect/sqlitedialect -# github.com/uptrace/bun/extra/bunotel v1.2.1 -## explicit; go 1.21 +# github.com/uptrace/bun/extra/bunotel v1.2.5 +## explicit; go 1.22 github.com/uptrace/bun/extra/bunotel -# github.com/uptrace/opentelemetry-go-extra/otelsql v0.2.4 -## explicit; go 1.21 +# github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 +## explicit; go 1.22 github.com/uptrace/opentelemetry-go-extra/otelsql # github.com/vmihailenco/msgpack/v5 v5.4.1 ## explicit; go 1.19 @@ -972,7 +974,7 @@ go.mongodb.org/mongo-driver/bson/bsonrw go.mongodb.org/mongo-driver/bson/bsontype go.mongodb.org/mongo-driver/bson/primitive go.mongodb.org/mongo-driver/x/bsonx/bsoncore -# go.opentelemetry.io/otel v1.29.0 +# go.opentelemetry.io/otel v1.32.0 => go.opentelemetry.io/otel v1.29.0 ## explicit; go 1.21 go.opentelemetry.io/otel go.opentelemetry.io/otel/attribute @@ -996,14 +998,14 @@ go.opentelemetry.io/otel/semconv/v1.7.0 ## explicit; go 1.21 go.opentelemetry.io/otel/exporters/otlp/otlptrace go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform -# go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 +# go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 => go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 ## explicit; go 1.21 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/envconfig go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/retry -# go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 +# go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 => go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 ## explicit; go 1.21 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal @@ -1013,12 +1015,12 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/retry # go.opentelemetry.io/otel/exporters/prometheus v0.51.0 ## explicit; go 1.21 go.opentelemetry.io/otel/exporters/prometheus -# go.opentelemetry.io/otel/metric v1.29.0 +# go.opentelemetry.io/otel/metric v1.32.0 => go.opentelemetry.io/otel/metric v1.29.0 ## explicit; go 1.21 go.opentelemetry.io/otel/metric go.opentelemetry.io/otel/metric/embedded go.opentelemetry.io/otel/metric/noop -# go.opentelemetry.io/otel/sdk v1.29.0 +# go.opentelemetry.io/otel/sdk v1.32.0 => go.opentelemetry.io/otel/sdk v1.29.0 ## explicit; go 1.21 go.opentelemetry.io/otel/sdk go.opentelemetry.io/otel/sdk/instrumentation @@ -1026,7 +1028,7 @@ go.opentelemetry.io/otel/sdk/internal/env go.opentelemetry.io/otel/sdk/internal/x go.opentelemetry.io/otel/sdk/resource go.opentelemetry.io/otel/sdk/trace -# go.opentelemetry.io/otel/sdk/metric v1.29.0 +# go.opentelemetry.io/otel/sdk/metric v1.32.0 => go.opentelemetry.io/otel/sdk/metric v1.29.0 ## explicit; go 1.21 go.opentelemetry.io/otel/sdk/metric go.opentelemetry.io/otel/sdk/metric/internal @@ -1034,7 +1036,7 @@ go.opentelemetry.io/otel/sdk/metric/internal/aggregate go.opentelemetry.io/otel/sdk/metric/internal/exemplar go.opentelemetry.io/otel/sdk/metric/internal/x go.opentelemetry.io/otel/sdk/metric/metricdata -# go.opentelemetry.io/otel/trace v1.29.0 +# go.opentelemetry.io/otel/trace v1.32.0 => go.opentelemetry.io/otel/trace v1.29.0 ## explicit; go 1.21 go.opentelemetry.io/otel/trace go.opentelemetry.io/otel/trace/embedded @@ -1056,7 +1058,7 @@ go.uber.org/multierr # golang.org/x/arch v0.8.0 ## explicit; go 1.18 golang.org/x/arch/x86/x86asm -# golang.org/x/crypto v0.28.0 +# golang.org/x/crypto v0.29.0 ## explicit; go 1.20 golang.org/x/crypto/acme golang.org/x/crypto/acme/autocert @@ -1083,7 +1085,7 @@ golang.org/x/exp/slices golang.org/x/exp/slog golang.org/x/exp/slog/internal golang.org/x/exp/slog/internal/buffer -# golang.org/x/image v0.21.0 +# golang.org/x/image v0.22.0 ## explicit; go 1.18 golang.org/x/image/riff golang.org/x/image/vp8 @@ -1094,7 +1096,7 @@ golang.org/x/image/webp golang.org/x/mod/internal/lazyregexp golang.org/x/mod/module golang.org/x/mod/semver -# golang.org/x/net v0.30.0 +# golang.org/x/net v0.31.0 ## explicit; go 1.18 golang.org/x/net/bpf golang.org/x/net/context @@ -1114,21 +1116,21 @@ golang.org/x/net/ipv6 golang.org/x/net/proxy golang.org/x/net/publicsuffix golang.org/x/net/trace -# golang.org/x/oauth2 v0.23.0 +# golang.org/x/oauth2 v0.24.0 ## explicit; go 1.18 golang.org/x/oauth2 golang.org/x/oauth2/internal -# golang.org/x/sync v0.8.0 +# golang.org/x/sync v0.9.0 ## explicit; go 1.18 golang.org/x/sync/errgroup golang.org/x/sync/semaphore -# golang.org/x/sys v0.26.0 +# golang.org/x/sys v0.27.0 ## explicit; go 1.18 golang.org/x/sys/cpu golang.org/x/sys/unix golang.org/x/sys/windows golang.org/x/sys/windows/registry -# golang.org/x/text v0.19.0 +# golang.org/x/text v0.20.0 ## explicit; go 1.18 golang.org/x/text/cases golang.org/x/text/encoding @@ -1344,3 +1346,10 @@ modernc.org/token ## explicit; go 1.19 mvdan.cc/xurls/v2 # modernc.org/sqlite => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.33.1-concurrency-workaround +# go.opentelemetry.io/otel => go.opentelemetry.io/otel v1.29.0 +# go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 +# go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 +# go.opentelemetry.io/otel/metric => go.opentelemetry.io/otel/metric v1.29.0 +# go.opentelemetry.io/otel/sdk => go.opentelemetry.io/otel/sdk v1.29.0 +# go.opentelemetry.io/otel/sdk/metric => go.opentelemetry.io/otel/sdk/metric v1.29.0 +# go.opentelemetry.io/otel/trace => go.opentelemetry.io/otel/trace v1.29.0