diff --git a/.github/ISSUE_TEMPLATE/bug_frontend.yaml b/.github/ISSUE_TEMPLATE/bug_frontend.yaml index 7a6591763..23ddb4d31 100644 --- a/.github/ISSUE_TEMPLATE/bug_frontend.yaml +++ b/.github/ISSUE_TEMPLATE/bug_frontend.yaml @@ -1,6 +1,6 @@ name: Frontend Bug Report description: Report an issue related to the web frontend -title: "[bug] Issue Title" +title: "[bug/frontend] Issue Title" labels: ["bug", "frontend"] assignees: [] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b52d6b59..4fa148f37 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ These contribution guidelines were adapted from / inspired by those of Gitea (ht - [Finding your way around the code](#finding-your-way-around-the-code) - [Style / Linting / Formatting](#style--linting--formatting) - [Testing](#testing) - - [Standalone Testrig with Semaphore](#standalone-testrig-with-semaphore) + - [Standalone Testrig with Pinafore](#standalone-testrig-with-pinafore) - [Running automated tests](#running-automated-tests) - [SQLite](#sqlite) - [Postgres](#postgres) @@ -401,9 +401,9 @@ GoToSocial provides a [testrig](https://github.com/superseriousbusiness/gotosoci One thing that *isn't* mocked is the Database interface because it's just easier to use an in-memory SQLite database than to mock everything out. -#### Standalone Testrig with Semaphore +#### Standalone Testrig with Pinafore -You can launch a testrig as a standalone server running at localhost, which you can connect to using something like [Semaphore](https://github.com/NickColley/semaphore/). +You can launch a testrig as a standalone server running at localhost, which you can connect to using something like [Pinafore](https://github.com/nolanlawson/pinafore/). To do this, first build the gotosocial binary with `DEBUG=1 ./scripts/build.sh`. @@ -413,14 +413,14 @@ Then, launch the testrig with the `DEBUG` environment variable set by invoking t DEBUG=1 ./gotosocial testrig start ``` -To run Semaphore locally in dev mode, first clone the [Semaphore](https://github.com/NickColley/semaphore/) repository, and then run the following commands in the cloned directory: +To run Pinafore locally in dev mode, first clone the [Pinafore](https://github.com/nolanlawson/pinafore/) repository, and then run the following commands in the cloned directory: ```bash yarn # install dependencies yarn run dev ``` -The Semaphore instance will start running on `localhost:4002`. +The Pinafore instance will start running on `localhost:4002`. To connect to the testrig, navigate to `http://localhost:4002` and enter your instance name as `localhost:8080`. diff --git a/README.md b/README.md index c37af7533..3f9f357e2 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ The Mastodon API has become the de facto standard for client communication with Though most apps that implement the Mastodon API should work, GoToSocial is tested and works reliably with beautiful apps like: * [Tusky](https://tusky.app/) for Android -* [Semaphore](https://semaphore.social/) in the browser +* [Pinafore](https://pinafore.social/) in the browser * [Feditext](https://github.com/feditext/feditext) (beta) on iOS, iPadOS and macOS If you've used Mastodon with a third-party app before, you'll find using GoToSocial a breeze. diff --git a/cmd/gen-ulid/main.go b/cmd/gen-ulid/main.go new file mode 100644 index 000000000..f96df4415 --- /dev/null +++ b/cmd/gen-ulid/main.go @@ -0,0 +1,22 @@ +// 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 main + +import "github.com/superseriousbusiness/gotosocial/internal/id" + +func main() { println(id.NewULID()) } diff --git a/docs/admin/backup_and_restore.md b/docs/admin/backup_and_restore.md index 9ad430bbe..83059c9e6 100644 --- a/docs/admin/backup_and_restore.md +++ b/docs/admin/backup_and_restore.md @@ -186,7 +186,7 @@ You'll need to put that file on your GoToSocial instance and make sure the file For this to work reliably, you should ensure that the [storage-local-base-path](../configuration/storage.md) in your GoToSocial configuration uses an absolute path. Otherwise you'll have to tweak the paths yourself. ```sh -$ gotosocial admin media list-attachments --local-only | \ +$ gotosocial --config-path /path/to/config.yaml admin media list-attachments --local-only | \ /path/to/media-to-borg-patterns.py \ ``` @@ -210,7 +210,7 @@ If you're running Borgmatic as a systemd service, you can [create a drop-in](htt ```ini [Service] -ExecStartPre=/path/to/gotosocial admin media list-attachments --local-only | /path/to/media-to-borg-patterns.py /etc/borgmatic/gotosocial_patterns +ExecStartPre=/path/to/gotosocial --config-path /path/to/config.yaml admin media list-attachments --local-only | /path/to/media-to-borg-patterns.py /etc/borgmatic/gotosocial_patterns ``` Documentation that's good to review: diff --git a/docs/admin/settings.md b/docs/admin/settings.md index 0efb5bf45..170d07e6a 100644 --- a/docs/admin/settings.md +++ b/docs/admin/settings.md @@ -167,3 +167,11 @@ Links to the set contact account and/or email address will appear on the footer The selected **contact user** must be an active (not suspended) admin and/or moderator on the instance. If you're on a single-user instance and you give admin privileges to your main account, you can just fill in your own username here; you don't need to make a separate admin account just for this. + +### Instance Custom CSS + +custom CSS allows you to further customize the way your instance looks when visited through a browser. + +This custom CSS will be applied to all pages of your instance. Users themes and CSS still take precedence over this customization. + +See the [Custom CSS](./custom_css.md) page for some tips on writing custom CSS for your instance. diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index 8f22c783a..61fdb4bf3 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -1570,6 +1570,10 @@ definitions: $ref: '#/definitions/instanceV1Configuration' contact_account: $ref: '#/definitions/account' + custom_css: + description: Custom CSS for the instance. + type: string + x-go-name: CustomCSS debug: description: Whether or not instance is running in DEBUG mode. Omitted if false. type: boolean @@ -1750,6 +1754,10 @@ definitions: $ref: '#/definitions/instanceV2Configuration' contact: $ref: '#/definitions/instanceV2Contact' + custom_css: + description: Instance Custom Css + type: string + x-go-name: CustomCSS debug: description: Whether or not instance is running in DEBUG mode. Omitted if false. type: boolean @@ -2696,6 +2704,11 @@ definitions: example: "2021-07-30T09:20:25+00:00" type: string x-go-name: CreatedAt + edited_at: + description: Timestamp of when the status was last edited (ISO 8601 Datetime). + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: EditedAt emojis: description: Custom emoji to be used when rendering status content. items: @@ -2893,6 +2906,11 @@ definitions: example: "2021-07-30T09:20:25+00:00" type: string x-go-name: CreatedAt + edited_at: + description: Timestamp of when the status was last edited (ISO 8601 Datetime). + example: "2021-07-30T09:20:25+00:00" + type: string + x-go-name: EditedAt emojis: description: Custom emoji to be used when rendering status content. items: @@ -3767,6 +3785,41 @@ paths: summary: Block account with id. tags: - accounts + /api/v1/accounts/{id}/featured_tags: + get: + description: 'THIS ENDPOINT IS CURRENTLY NOT FULLY IMPLEMENTED: it will always return an empty array.' + operationId: accountsFeaturedTags + parameters: + - description: The id of the account. + in: path + name: id + required: true + type: string + 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: Get an array of target account's featured tags. + tags: + - accounts /api/v1/accounts/{id}/follow: post: consumes: @@ -6829,6 +6882,34 @@ paths: summary: View instance rule with the given id. tags: - admin + /api/v1/announcements: + get: + description: 'THIS ENDPOINT IS CURRENTLY NOT FULLY IMPLEMENTED: it will always return an empty array.' + operationId: announcementsGet + produces: + - application/json + responses: + "200": + description: "" + schema: + items: + type: object + maxItems: 0 + type: array + "400": + description: bad request + "401": + description: unauthorized + "406": + description: not acceptable + "500": + description: internal server error + security: + - OAuth2 Bearer: + - read:announcements + summary: Get an array of currently active announcements. + tags: + - announcements /api/v1/apps: post: consumes: @@ -9859,6 +9940,112 @@ paths: summary: Create a new status using the given form field parameters. tags: - statuses + put: + consumes: + - application/json + - application/x-www-form-urlencoded + description: The parameters can also be given in the body of the request, as JSON, if the content-type is set to 'application/json'. + operationId: statusEdit + parameters: + - description: |- + Text content of the status. + If media_ids is provided, this becomes optional. + Attaching a poll is optional while status is provided. + in: formData + name: status + type: string + x-go-name: Status + - description: |- + Array of Attachment ids to be attached as media. + If provided, status becomes optional, and poll cannot be used. + + If the status is being submitted as a form, the key is 'media_ids[]', + but if it's json or xml, the key is 'media_ids'. + in: formData + items: + type: string + name: media_ids + type: array + x-go-name: MediaIDs + - description: |- + Array of possible poll answers. + If provided, media_ids cannot be used, and poll[expires_in] must be provided. + in: formData + items: + type: string + name: poll[options][] + type: array + x-go-name: PollOptions + - description: |- + Duration the poll should be open, in seconds. + If provided, media_ids cannot be used, and poll[options] must be provided. + format: int64 + in: formData + name: poll[expires_in] + type: integer + x-go-name: PollExpiresIn + - default: false + description: Allow multiple choices on this poll. + in: formData + name: poll[multiple] + type: boolean + x-go-name: PollMultiple + - default: true + description: Hide vote counts until the poll ends. + in: formData + name: poll[hide_totals] + type: boolean + x-go-name: PollHideTotals + - description: Status and attached media should be marked as sensitive. + in: formData + name: sensitive + type: boolean + x-go-name: Sensitive + - description: |- + Text to be shown as a warning or subject before the actual content. + Statuses are generally collapsed behind this field. + in: formData + name: spoiler_text + type: string + x-go-name: SpoilerText + - description: ISO 639 language code for this status. + in: formData + name: language + type: string + x-go-name: Language + - description: Content type to use when parsing this status. + enum: + - text/plain + - text/markdown + in: formData + name: content_type + type: string + x-go-name: ContentType + produces: + - application/json + responses: + "200": + description: The latest status revision. + 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: Edit an existing status using the given form field parameters. + tags: + - statuses /api/v1/statuses/{id}: delete: description: |- diff --git a/docs/faq.md b/docs/faq.md index 521c97531..72fed557d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -2,7 +2,7 @@ ## Where's the user interface? -GoToSocial is just a bare server for the most part and is designed to be used thru external applications. [Semaphore](https://semaphore.social/) in the browser, [Tusky](https://tusky.app/) for Android and [Feditext](https://github.com/feditext/feditext) for iOS, iPadOS and macOS are the best-supported. Anything that supports the Mastodon API should work, other than the features GoToSocial doesn't yet have. Permalinks and profile pages are served directly through GoToSocial as well as the settings panel, but most interaction goes through the apps. +GoToSocial is just a bare server for the most part and is designed to be used thru external applications. [Pinafore](https://pinafore.social/) in the browser, [Tusky](https://tusky.app/) for Android and [Feditext](https://github.com/feditext/feditext) for iOS, iPadOS and macOS are the best-supported. Anything that supports the Mastodon API should work, other than the features GoToSocial doesn't yet have. Permalinks and profile pages are served directly through GoToSocial as well as the settings panel, but most interaction goes through the apps. ## Why aren't my posts showing up on my profile page? diff --git a/docs/getting_started/reverse_proxy/websocket.md b/docs/getting_started/reverse_proxy/websocket.md index ec7c107a9..68d28bc5c 100644 --- a/docs/getting_started/reverse_proxy/websocket.md +++ b/docs/getting_started/reverse_proxy/websocket.md @@ -1,6 +1,6 @@ # WebSocket -GoToSocial uses the secure [WebSocket protocol](https://en.wikipedia.org/wiki/WebSocket) (aka `wss`) to allow for streaming updates of statuses and notifications via client apps like Semaphore. +GoToSocial uses the secure [WebSocket protocol](https://en.wikipedia.org/wiki/WebSocket) (aka `wss`) to allow for streaming updates of statuses and notifications via client apps like Pinafore. In order to use this functionality, you need to ensure that whatever proxy you've configured GoToSocial to run behind allows WebSocket connections through. diff --git a/docs/locales/zh/api/swagger.yaml b/docs/locales/zh/api/swagger.yaml index 7751c47e3..070a9448c 100644 --- a/docs/locales/zh/api/swagger.yaml +++ b/docs/locales/zh/api/swagger.yaml @@ -4980,7 +4980,7 @@ paths: - description: 此表情的代码,将被实例居民用于选定对应表情。此代码在实例上必须是唯一的。 in: formData name: shortcode - pattern: \w{2,30} + pattern: \w{1,30} required: true type: string - description: 此表情的 png 或 gif 图像。动画 png 也可以!为了确保与其他 fedi 实现的兼容性,默认情况下表情大小限制为 50kb。 @@ -5130,7 +5130,7 @@ paths: - description: 用于表情的代码,将被实例居民用于选定表情。此代码在实例上必须是唯一的。仅适用于 `copy` 操作类型。 in: formData name: shortcode - pattern: \w{2,30} + pattern: \w{1,30} type: string - description: 此表情的新 png 或 gif 图像。动画 png 也可以!为了确保与其他 fedi 实现的兼容性,默认情况下表情大小限制为 50kb。仅适用于 **本站** 表情。 in: formData @@ -5639,6 +5639,417 @@ paths: summary: 吊销实例密钥 tags: - admin + /api/v1/admin/domain_permission_drafts: + get: + description: |- + 该端点将返回按时间倒序排序(最新优先),并带有连续 ID 的域名权限草案(ID 值越大,草稿越新)。可以通过返回的 Link 标头解析下一页与上一页查询。 + + 示例: + ``` + ; rel="next", ; rel="prev" + ```` + operationId: domainPermissionDraftsGet + parameters: + - description: 仅显示给定订阅 ID 创建的草案。 + in: query + name: subscription_id + type: string + - description: 仅显示针对特定域名的草案。 + in: query + name: domain + type: string + - description: 筛选“屏蔽”与“放行”类型的草案。 + in: query + name: permission_type + 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/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: 查看域名权限草案。 + tags: + - admin + post: + consumes: + - multipart/form-data + - application/json + operationId: domainPermissionDraftCreate + parameters: + - description: 该草案要针对的域名。 + in: formData + name: domain + type: string + - description: 草案类型为“放行”或“屏蔽”。 + in: formData + name: permission_type + type: string + - description: 对外公开展示时混淆具体域名。例如:`example.org` 将变为类似 `ex***e.org` 的字符串。 + in: formData + name: obfuscate + type: boolean + - description: 对此域名权限的公开评注。若您选择分享此权限设定,此评注将与权限条目一起显示。 + in: formData + name: public_comment + type: string + - description: 对此域名权限的私人评注。仅显示给其他管理员,因此这是一个可用于记录为什么某个域名最终被添加此权限设定的有用的内部手段。 + in: formData + name: private_comment + type: string + produces: + - application/json + responses: + "200": + description: 新创建的域名权限草案。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止访问 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 使用给定参数创建一条域名权限草案。 + tags: + - admin + /api/v1/admin/domain_permission_drafts/{id}: + get: + operationId: domainPermissionDraftGet + parameters: + - description: 域名权限草案的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 域名权限草案。 + schema: + $ref: '#/definitions/domainPermission' + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止访问 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 获取具有给定 ID 的域名权限草案。 + tags: + - admin + /api/v1/admin/domain_permission_drafts/{id}/accept: + post: + consumes: + - multipart/form-data + - application/json + operationId: domainPermissionDraftAccept + parameters: + - description: 域名权限草案的 ID。 + in: path + name: id + required: true + type: string + - default: false + description: 若已经存在一条具有相同域名与权限设定类型的草案,使用新草案的字段覆盖现有权限设定。 + in: formData + name: overwrite + type: boolean + produces: + - application/json + responses: + "200": + description: 新创建的域名权限草案。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止访问 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 接受一条域名权限草案,将其转换为会得到强制执行的域名权限。 + tags: + - admin + /api/v1/admin/domain_permission_drafts/{id}/remove: + post: + consumes: + - multipart/form-data + - application/json + operationId: domainPermissionDraftRemove + parameters: + - description: 域名权限草案的 ID。 + in: path + name: id + required: true + type: string + - default: false + description: 删除此域名权限草案时,为目标域名创建一个域名排除条目,以确保之后不会为此域名创建草案。 + in: formData + name: exclude_target + type: boolean + produces: + - application/json + responses: + "200": + description: 被移除的域名权限草案。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止访问 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 移除一条域名权限草案,可选择忽略所有之后的针对给定域名的草案。 + tags: + - admin + /api/v1/admin/domain_permission_excludes: + get: + description: |- + 返回按时间倒序排序(新创建的条目优先),并带有连续 ID 的域名权限排除条目(ID 值越大,排除条目越新)。可以通过返回的 Link 标头解析下一页与上一页查询。 + 示例: + ``` + ; rel="next", ; rel="prev" + ``` + operationId: domainPermissionExcludesGet + parameters: + - description: 仅返回针对给定域名的排除条目。 + in: query + name: domain + 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/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: 查看域名权限排除条目。 + tags: + - admin + post: + consumes: + - multipart/form-data + - application/json + description: |- + 被排除的域名(及其子域名)在导入或订阅域名权限列表时不会被自动屏蔽或放行。 + 您仍然可以为被排除的域名手动创建域名屏蔽条目或域名放行条目,被排除之后,与该域名关联的任何的已有或新创建的域名屏蔽条目或域名放行条目都将被继续执行。 + operationId: domainPermissionExcludeCreate + parameters: + - description: 要创建权限排除的域名。 + in: formData + name: domain + type: string + - description: 对该域名排除条目的私密评论。 + in: formData + name: private_comment + type: string + produces: + - application/json + responses: + "200": + description: 新创建的域名排除条目。 + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request 无效请求 + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止访问 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 使用给定参数创建一个域名权限排除条目。 + tags: + - admin + /api/v1/admin/domain_permission_excludes/{id}: + delete: + operationId: domainPermissionExcludeDelete + 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 禁止访问 + "406": + description: not acceptable 不可接受 + "409": + description: conflict 冲突 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 移除一个域名权限排除条目。 + tags: + - admin + get: + operationId: domainPermissionExcludeGet + parameters: + - description: 域名权限排除条目的 ID。 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 域名权限排除条目。 + schema: + $ref: '#/definitions/domainPermission' + "401": + description: unauthorized 未授权 + "403": + description: forbidden 禁止访问 + "404": + description: not found 未找到 + "406": + description: not acceptable 不可接受 + "500": + description: internal server error 服务器内部错误 + security: + - OAuth2 Bearer: + - admin + summary: 获取具有给定 ID 的域名权限排除。 + tags: + - admin /api/v1/admin/email/test: post: consumes: diff --git a/docs/locales/zh/configuration/storage.md b/docs/locales/zh/configuration/storage.md index 24bb40d45..c2f1c467f 100644 --- a/docs/locales/zh/configuration/storage.md +++ b/docs/locales/zh/configuration/storage.md @@ -1,5 +1,9 @@ # 存储 +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. + +配置对象存储后端时,`storage-s3-endpoint` **不得** 包含存储桶名称。`s3-bucket-name`负责配置存储桶名称。目前不支持使用特定存储桶的子目录作为存储后端。 + ## 设置 ```yaml diff --git a/docs/locales/zh/faq.md b/docs/locales/zh/faq.md index 8383d7462..a49272202 100644 --- a/docs/locales/zh/faq.md +++ b/docs/locales/zh/faq.md @@ -2,7 +2,7 @@ ## 用户界面在哪? -GoToSocial 的大部分内容只是一个裸服务端,用于通过第三方应用程序进行使用。可通过 [Semaphore](https://semaphore.social/) 在浏览器使用,可通过 [Tusky](https://tusky.app/) 在 Android 使用,可通过 [Feditext](https://github.com/feditext/feditext) 在 iOS、iPadOS 和 macOS 使用。这些应用程序兼容性最好。任何由 Mastodon API 提供的实例功能都应该可以工作,除非它们是 GoToSocial 尚不具备的功能。永久链接和个账户页是通过 GoToSocial 直接提供的,设置面板也是如此,但大多数交互都是通过应用程序完成的。 +GoToSocial 的大部分内容只是一个裸服务端,用于通过第三方应用程序进行使用。可通过 [Pinafore](https://pinafore.social/) 在浏览器使用,可通过 [Tusky](https://tusky.app/) 在 Android 使用,可通过 [Feditext](https://github.com/feditext/feditext) 在 iOS、iPadOS 和 macOS 使用。这些应用程序兼容性最好。任何由 Mastodon API 提供的实例功能都应该可以工作,除非它们是 GoToSocial 尚不具备的功能。永久链接和个账户页是通过 GoToSocial 直接提供的,设置面板也是如此,但大多数交互都是通过应用程序完成的。 ## 为什么我的贴文没有显示在我的账户页面上? diff --git a/docs/locales/zh/getting_started/reverse_proxy/websocket.md b/docs/locales/zh/getting_started/reverse_proxy/websocket.md index e1391ec45..f265edf37 100644 --- a/docs/locales/zh/getting_started/reverse_proxy/websocket.md +++ b/docs/locales/zh/getting_started/reverse_proxy/websocket.md @@ -1,6 +1,6 @@ # WebSocket -GoToSocial 使用安全的 [WebSocket 协议](https://en.wikipedia.org/wiki/WebSocket)(即 `wss`)来通过客户端应用程序(如 Semaphore)实现贴文和通知的实时更新。 +GoToSocial 使用安全的 [WebSocket 协议](https://en.wikipedia.org/wiki/WebSocket)(即 `wss`)来通过客户端应用程序(如 Pinafore)实现贴文和通知的实时更新。 为了使用此功能,你需要确保配置 GoToSocial 所在的代理允许 WebSocket 连接通过。 diff --git a/docs/locales/zh/repo/CONTRIBUTING.md b/docs/locales/zh/repo/CONTRIBUTING.md index 4d05b180e..4da713ad3 100644 --- a/docs/locales/zh/repo/CONTRIBUTING.md +++ b/docs/locales/zh/repo/CONTRIBUTING.md @@ -24,7 +24,7 @@ - [浏览代码结构](#浏览代码结构) - [风格/代码检查/格式化](#风格代码检查格式化) - [测试](#测试) - - [独立测试环境与 Semaphore](#独立测试环境与-semaphore) + - [独立测试环境与 Pinafore](#独立测试环境与-pinafore) - [运行自动化测试](#运行自动化测试) - [SQLite](#sqlite) - [Postgres](#postgres) @@ -400,9 +400,9 @@ GoToSocial 提供了一个 [testrig](https://github.com/superseriousbusiness/got 没有模拟的一个东西是数据库接口,因为使用内存中的 SQLite 数据库比模拟所有东西要简单得多。 -#### 独立测试环境与 Semaphore +#### 独立测试环境与 Pinafore -你可以启动一个在本地主机运行的独立测试服务器 testrig,可以通过 [Semaphore](https://github.com/NickColley/semaphore/) 连接。 +你可以启动一个在本地主机运行的独立测试服务器 testrig,可以通过 [Pinafore](https://github.com/NickColley/pinafore/) 连接。 要做到这一点,首先用 `DEBUG=1 ./scripts/build.sh` 构建 gotosocial 二进制文件。 @@ -412,14 +412,14 @@ GoToSocial 提供了一个 [testrig](https://github.com/superseriousbusiness/got DEBUG=1 ./gotosocial testrig start ``` -要在本地开发模式下运行 Semaphore,首先克隆 [Semaphore](https://github.com/NickColley/semaphore/) 存储库,然后在克隆的目录中运行以下命令: +要在本地开发模式下运行 Pinafore,首先克隆 [Pinafore](https://github.com/nolanlawson/pinafore/) 存储库,然后在克隆的目录中运行以下命令: ```bash yarn # 安装依赖 yarn run dev ``` -Semaphore 实例将在 `localhost:4002` 上启动。 +Pinafore 实例将在 `localhost:4002` 上启动。 要连接到 testrig,导航至 `http://localhost:4002`,并将在实例域名栏输入 `localhost:8080`。 diff --git a/docs/locales/zh/repo/README.md b/docs/locales/zh/repo/README.md index 82761a4b5..24b5591fc 100644 --- a/docs/locales/zh/repo/README.md +++ b/docs/locales/zh/repo/README.md @@ -113,7 +113,7 @@ Mastodon API 已成为客户端与联邦宇宙服务端通信的事实标准, 大多数实现 Mastodon API 的应用程序都应该可以使用 GoToSocial,但以下这些优秀的应用程序已经过测试,可与 GoToSocial 可靠地配合使用: * [Tusky](https://tusky.app/) 适用于 Android -* [Semaphore](https://semaphore.social/) 适用于浏览器 +* [Pinafore](https://pinafore.social/) 适用于浏览器 * [Feditext](https://github.com/feditext/feditext) (beta) 适用于 iOS, iPadOS 和 macOS 如果你之前通过第三方应用来使用 Mastodon,使用 GoToSocial 将是轻而易举的。 diff --git a/docs/locales/zh/user_guide/posts.md b/docs/locales/zh/user_guide/posts.md index 8c41dcd77..911eb2beb 100644 --- a/docs/locales/zh/user_guide/posts.md +++ b/docs/locales/zh/user_guide/posts.md @@ -36,6 +36,9 @@ GoToSocial 为贴文提供 Mastodon 风格的隐私设置。从最私密到最 ### 互关可见 +!!! warning + 目前暂时无法将帖文可见性设为“互关可见”。 + `互关可见` 的贴文只会显示给贴文作者和与作者*互相关注*的人。换句话说,只有在满足两个条件时,其他人才能看到: 1. 其他账户关注贴文作者。 diff --git a/docs/overrides/public/admin-settings-instance.png b/docs/overrides/public/admin-settings-instance.png index 181a35a7c..1203e8a41 100644 Binary files a/docs/overrides/public/admin-settings-instance.png and b/docs/overrides/public/admin-settings-instance.png differ diff --git a/docs/user_guide/posts.md b/docs/user_guide/posts.md index 1f718cfae..c45ad4bcb 100644 --- a/docs/user_guide/posts.md +++ b/docs/user_guide/posts.md @@ -285,6 +285,9 @@ For accessibility reasons, it is considerate to use upper camel case when you're You can include as many hashtags as you like within a GoToSocial post, and each hashtag has a length limit of 100 characters. +!!! tip + To end a hashtag, you can simply use a space, for example in the text `this #soup rules`, the hashtag is terminated by a space so `#soup` becomes the hashtag. However, you can also use a pipe character `|`, or the unicode characters `\u200B` (zero-width no-break space) or `\uFEFF` (zero-width space), to create "partial-word" hashtags. For example, with input text `this #so|up rules`, only the `#so` part becomes the hashtag. Likewise, with the input text `this #so​up rules`, which contains an invisible zero-width space after the o and before the u, only the `#so` part becomes the hashtag. See here for more information on zero-width spaces: https://en.wikipedia.org/wiki/Zero-width_space. + ## Input Sanitization In order not to spread scripts, vulnerabilities, and glitchy HTML all over the place, GoToSocial performs the following types of input sanitization: diff --git a/go.mod b/go.mod index 6ad90f6cc..277110373 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ go 1.23 replace github.com/go-swagger/go-swagger => github.com/superseriousbusiness/go-swagger v0.31.0-gts-go1.23-fix // 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 +replace modernc.org/sqlite => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.34.2-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 @@ -31,7 +31,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.6.0 + codeberg.org/gruf/go-ffmpreg v0.6.4 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 @@ -44,6 +44,7 @@ require ( codeberg.org/superseriousbusiness/exif-terminator v0.9.1 github.com/DmitriyVTitov/size v1.5.0 github.com/KimMachineGun/automemlimit v0.6.1 + github.com/SherClockHolmes/webpush-go v1.3.0 github.com/buckket/go-blurhash v1.1.0 github.com/coreos/go-oidc/v3 v3.11.0 github.com/gin-contrib/cors v1.7.2 @@ -60,12 +61,11 @@ require ( github.com/k3a/html2text v1.2.1 github.com/microcosm-cc/bluemonday v1.0.27 github.com/miekg/dns v1.1.62 - github.com/minio/minio-go/v7 v7.0.80 + github.com/minio/minio-go/v7 v7.0.81 github.com/mitchellh/mapstructure v1.5.0 - github.com/ncruces/go-sqlite3 v0.20.3 + github.com/ncruces/go-sqlite3 v0.21.3 github.com/oklog/ulid v1.3.1 github.com/prometheus/client_golang v1.20.5 - github.com/SherClockHolmes/webpush-go v1.3.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 @@ -92,11 +92,11 @@ require ( 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.29.0 - golang.org/x/image v0.22.0 - golang.org/x/net v0.31.0 + golang.org/x/crypto v0.31.0 + golang.org/x/image v0.23.0 + golang.org/x/net v0.32.0 golang.org/x/oauth2 v0.24.0 - golang.org/x/text v0.20.0 + golang.org/x/text v0.21.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 @@ -235,8 +235,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.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.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/internal/ap/interfaces.go b/internal/ap/interfaces.go index a721fa997..1f08fde37 100644 --- a/internal/ap/interfaces.go +++ b/internal/ap/interfaces.go @@ -25,8 +25,11 @@ // IsActivityable returns whether AS vocab type name is acceptable as Activityable. func IsActivityable(typeName string) bool { - return isActivity(typeName) || - isIntransitiveActivity(typeName) + return isActivity(typeName) + // See interfaces_test.go comment + // about intransitive activities: + // + // || isIntransitiveActivity(typeName) } // ToActivityable safely tries to cast vocab.Type as Activityable, also checking for expected AS type names. @@ -184,6 +187,7 @@ type Accountable interface { WithEndpoints WithTag WithPublished + WithUpdated } // Statusable represents the minimum activitypub interface for representing a 'status'. @@ -196,6 +200,7 @@ type Statusable interface { WithName WithInReplyTo WithPublished + WithUpdated WithURL WithAttributedTo WithTo diff --git a/internal/ap/interfaces_test.go b/internal/ap/interfaces_test.go new file mode 100644 index 000000000..d3248cb1d --- /dev/null +++ b/internal/ap/interfaces_test.go @@ -0,0 +1,93 @@ +// 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 ap_test + +import ( + "github.com/superseriousbusiness/activity/streams/vocab" + "github.com/superseriousbusiness/gotosocial/internal/ap" +) + +var ( + // NOTE: the below aren't actually tests that are run, + // we just move them into an _test.go file to declutter + // the main interfaces.go file, which is already long. + + // Compile-time checks for Activityable interface methods. + _ ap.Activityable = (vocab.ActivityStreamsAccept)(nil) + _ ap.Activityable = (vocab.ActivityStreamsTentativeAccept)(nil) + _ ap.Activityable = (vocab.ActivityStreamsAdd)(nil) + _ ap.Activityable = (vocab.ActivityStreamsCreate)(nil) + _ ap.Activityable = (vocab.ActivityStreamsDelete)(nil) + _ ap.Activityable = (vocab.ActivityStreamsFollow)(nil) + _ ap.Activityable = (vocab.ActivityStreamsIgnore)(nil) + _ ap.Activityable = (vocab.ActivityStreamsJoin)(nil) + _ ap.Activityable = (vocab.ActivityStreamsLeave)(nil) + _ ap.Activityable = (vocab.ActivityStreamsLike)(nil) + _ ap.Activityable = (vocab.ActivityStreamsOffer)(nil) + _ ap.Activityable = (vocab.ActivityStreamsInvite)(nil) + _ ap.Activityable = (vocab.ActivityStreamsReject)(nil) + _ ap.Activityable = (vocab.ActivityStreamsTentativeReject)(nil) + _ ap.Activityable = (vocab.ActivityStreamsRemove)(nil) + _ ap.Activityable = (vocab.ActivityStreamsUndo)(nil) + _ ap.Activityable = (vocab.ActivityStreamsUpdate)(nil) + _ ap.Activityable = (vocab.ActivityStreamsView)(nil) + _ ap.Activityable = (vocab.ActivityStreamsListen)(nil) + _ ap.Activityable = (vocab.ActivityStreamsRead)(nil) + _ ap.Activityable = (vocab.ActivityStreamsMove)(nil) + _ ap.Activityable = (vocab.ActivityStreamsAnnounce)(nil) + _ ap.Activityable = (vocab.ActivityStreamsBlock)(nil) + _ ap.Activityable = (vocab.ActivityStreamsFlag)(nil) + _ ap.Activityable = (vocab.ActivityStreamsDislike)(nil) + + // the below intransitive activities don't fit the interface definition because they're + // missing an attached object (as the activity itself contains the details), but we don't + // actually end up using them so it's simpler to just comment them out and not have to do + // a WithObject{} interface check on every single incoming activity: + // + // _ Activityable = (vocab.ActivityStreamsArrive)(nil) + // _ Activityable = (vocab.ActivityStreamsTravel)(nil) + // _ Activityable = (vocab.ActivityStreamsQuestion)(nil) + + // Compile-time checks for Accountable interface methods. + _ ap.Accountable = (vocab.ActivityStreamsPerson)(nil) + _ ap.Accountable = (vocab.ActivityStreamsApplication)(nil) + _ ap.Accountable = (vocab.ActivityStreamsOrganization)(nil) + _ ap.Accountable = (vocab.ActivityStreamsService)(nil) + _ ap.Accountable = (vocab.ActivityStreamsGroup)(nil) + + // Compile-time checks for Statusable interface methods. + _ ap.Statusable = (vocab.ActivityStreamsArticle)(nil) + _ ap.Statusable = (vocab.ActivityStreamsDocument)(nil) + _ ap.Statusable = (vocab.ActivityStreamsImage)(nil) + _ ap.Statusable = (vocab.ActivityStreamsVideo)(nil) + _ ap.Statusable = (vocab.ActivityStreamsNote)(nil) + _ ap.Statusable = (vocab.ActivityStreamsPage)(nil) + _ ap.Statusable = (vocab.ActivityStreamsEvent)(nil) + _ ap.Statusable = (vocab.ActivityStreamsPlace)(nil) + _ ap.Statusable = (vocab.ActivityStreamsProfile)(nil) + _ ap.Statusable = (vocab.ActivityStreamsQuestion)(nil) + + // Compile-time checks for Pollable interface methods. + _ ap.Pollable = (vocab.ActivityStreamsQuestion)(nil) + + // Compile-time checks for PollOptionable interface methods. + _ ap.PollOptionable = (vocab.ActivityStreamsNote)(nil) + + // Compile-time checks for Acceptable interface methods. + _ ap.Acceptable = (vocab.ActivityStreamsAccept)(nil) +) diff --git a/internal/ap/properties.go b/internal/ap/properties.go index 38e58ebc0..0a2564168 100644 --- a/internal/ap/properties.go +++ b/internal/ap/properties.go @@ -408,6 +408,25 @@ func SetPublished(with WithPublished, published time.Time) { publishProp.Set(published) } +// GetUpdated returns the time contained in the Updated property of 'with'. +func GetUpdated(with WithUpdated) time.Time { + updateProp := with.GetActivityStreamsUpdated() + if updateProp == nil || !updateProp.IsXMLSchemaDateTime() { + return time.Time{} + } + return updateProp.Get() +} + +// SetUpdated sets the given time on the Updated property of 'with'. +func SetUpdated(with WithUpdated, updated time.Time) { + updateProp := with.GetActivityStreamsUpdated() + if updateProp == nil { + updateProp = streams.NewActivityStreamsUpdatedProperty() + with.SetActivityStreamsUpdated(updateProp) + } + updateProp.Set(updated) +} + // GetEndTime returns the time contained in the EndTime property of 'with'. func GetEndTime(with WithEndTime) time.Time { endTimeProp := with.GetActivityStreamsEndTime() diff --git a/internal/api/activitypub/users/outboxget_test.go b/internal/api/activitypub/users/outboxget_test.go index cba1ef31d..2de3b0456 100644 --- a/internal/api/activitypub/users/outboxget_test.go +++ b/internal/api/activitypub/users/outboxget_test.go @@ -82,7 +82,7 @@ func (suite *OutboxGetTestSuite) TestGetOutbox() { "@context": "https://www.w3.org/ns/activitystreams", "first": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40", "id": "http://localhost:8080/users/the_mighty_zork/outbox", - "totalItems": 8, + "totalItems": 9, "type": "OrderedCollection" }`, dst.String()) @@ -142,6 +142,14 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() { "id": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40", "next": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40\u0026max_id=01F8MHAMCHF6Y650WCRSCP4WMY", "orderedItems": [ + { + "actor": "http://localhost:8080/users/the_mighty_zork", + "cc": "http://localhost:8080/users/the_mighty_zork/followers", + "id": "http://localhost:8080/users/the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR/activity#Create", + "object": "http://localhost:8080/users/the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR", + "to": "https://www.w3.org/ns/activitystreams#Public", + "type": "Create" + }, { "actor": "http://localhost:8080/users/the_mighty_zork", "cc": "http://localhost:8080/users/the_mighty_zork/followers", @@ -160,8 +168,8 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() { } ], "partOf": "http://localhost:8080/users/the_mighty_zork/outbox", - "prev": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40\u0026min_id=01HH9KYNQPA416TNJ53NSATP40", - "totalItems": 8, + "prev": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40\u0026min_id=01JDPZC707CKDN8N4QVWM4Z1NR", + "totalItems": 9, "type": "OrderedCollectionPage" }`, dst.String()) @@ -224,7 +232,7 @@ func (suite *OutboxGetTestSuite) TestGetOutboxNextPage() { "id": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40&max_id=01F8MHAMCHF6Y650WCRSCP4WMY", "orderedItems": [], "partOf": "http://localhost:8080/users/the_mighty_zork/outbox", - "totalItems": 8, + "totalItems": 9, "type": "OrderedCollectionPage" }`, dst.String()) diff --git a/internal/api/client.go b/internal/api/client.go index 23a5803fc..3112aeea5 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -23,6 +23,7 @@ "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api/client/accounts" "github.com/superseriousbusiness/gotosocial/internal/api/client/admin" + "github.com/superseriousbusiness/gotosocial/internal/api/client/announcements" "github.com/superseriousbusiness/gotosocial/internal/api/client/apps" "github.com/superseriousbusiness/gotosocial/internal/api/client/blocks" "github.com/superseriousbusiness/gotosocial/internal/api/client/bookmarks" @@ -67,6 +68,7 @@ type Client struct { accounts *accounts.Module // api/v1/accounts, api/v1/profile admin *admin.Module // api/v1/admin + announcements *announcements.Module // api/v1/announcements apps *apps.Module // api/v1/apps blocks *blocks.Module // api/v1/blocks bookmarks *bookmarks.Module // api/v1/bookmarks @@ -119,6 +121,7 @@ func (c *Client) Route(r *router.Router, m ...gin.HandlerFunc) { h := apiGroup.Handle c.accounts.Route(h) c.admin.Route(h) + c.announcements.Route(h) c.apps.Route(h) c.blocks.Route(h) c.bookmarks.Route(h) @@ -159,6 +162,7 @@ func NewClient(state *state.State, p *processing.Processor) *Client { accounts: accounts.New(p), admin: admin.New(state, p), + announcements: announcements.New(p), apps: apps.New(p), blocks: blocks.New(p), bookmarks: bookmarks.New(p), diff --git a/internal/api/client/accounts/accounts.go b/internal/api/client/accounts/accounts.go index 0dbc5ea53..f21d23185 100644 --- a/internal/api/client/accounts/accounts.go +++ b/internal/api/client/accounts/accounts.go @@ -40,6 +40,7 @@ BlockPath = BasePathWithID + "/block" DeletePath = BasePath + "/delete" + FeaturedTagsPath = BasePathWithID + "/featured_tags" FollowersPath = BasePathWithID + "/followers" FollowingPath = BasePathWithID + "/following" FollowPath = BasePathWithID + "/follow" @@ -98,6 +99,9 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H // get account's statuses attachHandler(http.MethodGet, StatusesPath, m.AccountStatusesGETHandler) + // get account's featured tags + attachHandler(http.MethodGet, FeaturedTagsPath, m.AccountFeaturedTagsGETHandler) + // get following or followers attachHandler(http.MethodGet, FollowersPath, m.AccountFollowersGETHandler) attachHandler(http.MethodGet, FollowingPath, m.AccountFollowingGETHandler) diff --git a/internal/api/client/accounts/accountverify_test.go b/internal/api/client/accounts/accountverify_test.go index 3f67cdefb..df5c21389 100644 --- a/internal/api/client/accounts/accountverify_test.go +++ b/internal/api/client/accounts/accountverify_test.go @@ -97,7 +97,7 @@ func (suite *AccountVerifyTestSuite) TestAccountVerifyGet() { suite.Equal("http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp", apimodelAccount.HeaderStatic) suite.Equal(2, apimodelAccount.FollowersCount) suite.Equal(2, apimodelAccount.FollowingCount) - suite.Equal(8, apimodelAccount.StatusesCount) + suite.Equal(9, apimodelAccount.StatusesCount) suite.EqualValues(apimodel.VisibilityPublic, apimodelAccount.Source.Privacy) suite.Equal(testAccount.Settings.Language, apimodelAccount.Source.Language) suite.Equal(testAccount.NoteRaw, apimodelAccount.Source.Note) diff --git a/internal/api/client/accounts/featuredtags.go b/internal/api/client/accounts/featuredtags.go new file mode 100644 index 000000000..312a92bcc --- /dev/null +++ b/internal/api/client/accounts/featuredtags.go @@ -0,0 +1,83 @@ +// 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 accounts + +import ( + "net/http" + + "github.com/gin-gonic/gin" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// AccountFeaturedTagsGETHandler swagger:operation GET /api/v1/accounts/{id}/featured_tags accountsFeaturedTags +// +// Get an array of target account's featured tags. +// +// THIS ENDPOINT IS CURRENTLY NOT FULLY IMPLEMENTED: it will always return an empty array. +// +// --- +// tags: +// - accounts +// +// produces: +// - application/json +// +// parameters: +// - +// name: id +// type: string +// description: The id of the account. +// in: path +// required: true +// +// security: +// - OAuth2 Bearer: +// - read:accounts +// +// responses: +// '200': +// schema: +// type: array +// items: +// type: object +// '400': +// description: bad request +// '401': +// description: unauthorized +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) AccountFeaturedTagsGETHandler(c *gin.Context) { + _, err := oauth.Authed(c, true, true, true, true) + if err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) + return + } + + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + apiutil.Data(c, http.StatusOK, apiutil.AppJSON, apiutil.EmptyJSONArray) +} diff --git a/internal/api/client/admin/accountsgetv2_test.go b/internal/api/client/admin/accountsgetv2_test.go index 77ca135eb..489a245d0 100644 --- a/internal/api/client/admin/accountsgetv2_test.go +++ b/internal/api/client/admin/accountsgetv2_test.go @@ -99,8 +99,8 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() { "header_description": "Flat gray background (default header).", "followers_count": 1, "following_count": 1, - "statuses_count": 8, - "last_status_at": "2021-07-28", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [ { @@ -262,8 +262,8 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() { "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", "followers_count": 2, "following_count": 2, - "statuses_count": 8, - "last_status_at": "2024-01-10", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [], "enable_rss": true @@ -403,8 +403,8 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } diff --git a/internal/api/client/admin/reportsget_test.go b/internal/api/client/admin/reportsget_test.go index 12a307836..255e32c3b 100644 --- a/internal/api/client/admin/reportsget_test.go +++ b/internal/api/client/admin/reportsget_test.go @@ -186,8 +186,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } @@ -232,8 +232,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() { "header_description": "Flat gray background (default header).", "followers_count": 1, "following_count": 1, - "statuses_count": 8, - "last_status_at": "2021-07-28", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [ { @@ -414,8 +414,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() { "header_description": "Flat gray background (default header).", "followers_count": 1, "following_count": 1, - "statuses_count": 8, - "last_status_at": "2021-07-28", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [ { @@ -473,8 +473,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } @@ -485,6 +485,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() { { "id": "01FVW7JHQFSFK166WWKR8CBA6M", "created_at": "2021-09-20T10:40:37.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": false, @@ -521,8 +522,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] }, @@ -667,8 +668,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() { "header_description": "Flat gray background (default header).", "followers_count": 1, "following_count": 1, - "statuses_count": 8, - "last_status_at": "2021-07-28", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [ { @@ -726,8 +727,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } @@ -738,6 +739,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() { { "id": "01FVW7JHQFSFK166WWKR8CBA6M", "created_at": "2021-09-20T10:40:37.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": false, @@ -774,8 +776,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] }, @@ -920,8 +922,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() { "header_description": "Flat gray background (default header).", "followers_count": 1, "following_count": 1, - "statuses_count": 8, - "last_status_at": "2021-07-28", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [ { @@ -979,8 +981,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } @@ -991,6 +993,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() { { "id": "01FVW7JHQFSFK166WWKR8CBA6M", "created_at": "2021-09-20T10:40:37.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": false, @@ -1027,8 +1030,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] }, diff --git a/internal/api/client/announcements/announcements.go b/internal/api/client/announcements/announcements.go new file mode 100644 index 000000000..611a1c53e --- /dev/null +++ b/internal/api/client/announcements/announcements.go @@ -0,0 +1,42 @@ +// 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 announcements + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/processing" +) + +// BasePath is the base path for this api module, excluding the api prefix +const BasePath = "/v1/announcements" + +type Module struct { + processor *processing.Processor +} + +func New(processor *processing.Processor) *Module { + return &Module{ + processor: processor, + } +} + +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, BasePath, m.AnnouncementsGETHandler) +} diff --git a/internal/api/client/announcements/announcementsget.go b/internal/api/client/announcements/announcementsget.go new file mode 100644 index 000000000..04bd5f285 --- /dev/null +++ b/internal/api/client/announcements/announcementsget.go @@ -0,0 +1,74 @@ +// 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 announcements + +import ( + "net/http" + + "github.com/gin-gonic/gin" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// AnnouncementsGETHandler swagger:operation GET /api/v1/announcements announcementsGet +// +// Get an array of currently active announcements. +// +// THIS ENDPOINT IS CURRENTLY NOT FULLY IMPLEMENTED: it will always return an empty array. +// +// --- +// tags: +// - announcements +// +// produces: +// - application/json +// +// security: +// - OAuth2 Bearer: +// - read:announcements +// +// responses: +// '200': +// schema: +// type: array +// items: +// type: object +// maxItems: 0 +// '400': +// description: bad request +// '401': +// description: unauthorized +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) AnnouncementsGETHandler(c *gin.Context) { + _, err := oauth.Authed(c, true, true, true, true) + if err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) + return + } + + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + apiutil.JSON(c, http.StatusOK, apiutil.EmptyJSONArray) +} diff --git a/internal/api/client/exports/exports_test.go b/internal/api/client/exports/exports_test.go index b6f990320..7047d0849 100644 --- a/internal/api/client/exports/exports_test.go +++ b/internal/api/client/exports/exports_test.go @@ -231,7 +231,7 @@ type testCase struct { "media_storage": "", "followers_count": 2, "following_count": 2, - "statuses_count": 8, + "statuses_count": 9, "lists_count": 1, "blocks_count": 0, "mutes_count": 0 diff --git a/internal/api/client/instance/instancepatch.go b/internal/api/client/instance/instancepatch.go index 64263caf6..5085399eb 100644 --- a/internal/api/client/instance/instancepatch.go +++ b/internal/api/client/instance/instancepatch.go @@ -175,6 +175,7 @@ func validateInstanceUpdate(form *apimodel.InstanceSettingsUpdateRequest) error form.ContactEmail == nil && form.ShortDescription == nil && form.Description == nil && + form.CustomCSS == nil && form.Terms == nil && form.Avatar == nil && form.AvatarDescription == nil && diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go index efcb3762f..f126ee6ae 100644 --- a/internal/api/client/instance/instancepatch_test.go +++ b/internal/api/client/instance/instancepatch_test.go @@ -155,7 +155,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() { }, "stats": { "domain_count": 2, - "status_count": 19, + "status_count": 21, "user_count": 4 }, "thumbnail": "http://localhost:8080/assets/logo.webp", @@ -296,7 +296,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() { }, "stats": { "domain_count": 2, - "status_count": 19, + "status_count": 21, "user_count": 4 }, "thumbnail": "http://localhost:8080/assets/logo.webp", @@ -437,7 +437,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() { }, "stats": { "domain_count": 2, - "status_count": 19, + "status_count": 21, "user_count": 4 }, "thumbnail": "http://localhost:8080/assets/logo.webp", @@ -629,7 +629,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() { }, "stats": { "domain_count": 2, - "status_count": 19, + "status_count": 21, "user_count": 4 }, "thumbnail": "http://localhost:8080/assets/logo.webp", @@ -792,7 +792,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() { }, "stats": { "domain_count": 2, - "status_count": 19, + "status_count": 21, "user_count": 4 }, "thumbnail": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+` @@ -974,7 +974,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() { }, "stats": { "domain_count": 2, - "status_count": 19, + "status_count": 21, "user_count": 4 }, "thumbnail": "http://localhost:8080/assets/logo.webp", diff --git a/internal/api/client/mutes/mutesget_test.go b/internal/api/client/mutes/mutesget_test.go index fa52c9aa9..13d826398 100644 --- a/internal/api/client/mutes/mutesget_test.go +++ b/internal/api/client/mutes/mutesget_test.go @@ -148,7 +148,7 @@ func (suite *MutesTestSuite) TestIndefinitelyMutedAccountSerializesMuteExpiratio // Fetch all muted accounts for the logged-in account. // The expected body contains `"mute_expires_at":null`. - _, err = suite.getMutedAccounts(http.StatusOK, `[{"id":"01F8MH5ZK5VRH73AKHQM6Y9VNX","username":"foss_satan","acct":"foss_satan@fossbros-anonymous.io","display_name":"big gerald","locked":false,"discoverable":true,"bot":false,"created_at":"2021-09-26T10:52:36.000Z","note":"i post about like, i dunno, stuff, or whatever!!!!","url":"http://fossbros-anonymous.io/@foss_satan","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.webp","header_static":"http://localhost:8080/assets/default_header.webp","header_description":"Flat gray background (default header).","followers_count":0,"following_count":0,"statuses_count":3,"last_status_at":"2021-09-11","emojis":[],"fields":[],"mute_expires_at":null}]`) + _, err = suite.getMutedAccounts(http.StatusOK, `[{"id":"01F8MH5ZK5VRH73AKHQM6Y9VNX","username":"foss_satan","acct":"foss_satan@fossbros-anonymous.io","display_name":"big gerald","locked":false,"discoverable":true,"bot":false,"created_at":"2021-09-26T10:52:36.000Z","note":"i post about like, i dunno, stuff, or whatever!!!!","url":"http://fossbros-anonymous.io/@foss_satan","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.webp","header_static":"http://localhost:8080/assets/default_header.webp","header_description":"Flat gray background (default header).","followers_count":0,"following_count":0,"statuses_count":4,"last_status_at":"2024-11-01","emojis":[],"fields":[],"mute_expires_at":null}]`) if err != nil { suite.FailNow(err.Error()) } diff --git a/internal/api/client/reports/reportget_test.go b/internal/api/client/reports/reportget_test.go index 8c9dfa1e5..afbcb2e28 100644 --- a/internal/api/client/reports/reportget_test.go +++ b/internal/api/client/reports/reportget_test.go @@ -130,8 +130,8 @@ func (suite *ReportGetTestSuite) TestGetReport1() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } diff --git a/internal/api/client/reports/reportsget_test.go b/internal/api/client/reports/reportsget_test.go index 0eb66e778..b5988e331 100644 --- a/internal/api/client/reports/reportsget_test.go +++ b/internal/api/client/reports/reportsget_test.go @@ -156,8 +156,8 @@ func (suite *ReportsGetTestSuite) TestGetReports() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } @@ -247,8 +247,8 @@ func (suite *ReportsGetTestSuite) TestGetReports4() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } @@ -322,8 +322,8 @@ func (suite *ReportsGetTestSuite) TestGetReports6() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } @@ -381,8 +381,8 @@ func (suite *ReportsGetTestSuite) TestGetReports7() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } diff --git a/internal/api/client/search/searchget_test.go b/internal/api/client/search/searchget_test.go index ab4f46689..2c4efd19c 100644 --- a/internal/api/client/search/searchget_test.go +++ b/internal/api/client/search/searchget_test.go @@ -916,7 +916,7 @@ func (suite *SearchGetTestSuite) TestSearchAAny() { } suite.Len(searchResult.Accounts, 5) - suite.Len(searchResult.Statuses, 7) + suite.Len(searchResult.Statuses, 8) suite.Len(searchResult.Hashtags, 0) } @@ -959,7 +959,7 @@ func (suite *SearchGetTestSuite) TestSearchAAnyFollowingOnly() { } suite.Len(searchResult.Accounts, 2) - suite.Len(searchResult.Statuses, 7) + suite.Len(searchResult.Statuses, 8) suite.Len(searchResult.Hashtags, 0) } @@ -1002,7 +1002,7 @@ func (suite *SearchGetTestSuite) TestSearchAStatuses() { } suite.Len(searchResult.Accounts, 0) - suite.Len(searchResult.Statuses, 7) + suite.Len(searchResult.Statuses, 8) suite.Len(searchResult.Hashtags, 0) } diff --git a/internal/api/client/statuses/status.go b/internal/api/client/statuses/status.go index 33af9c456..88b34cbf5 100644 --- a/internal/api/client/statuses/status.go +++ b/internal/api/client/statuses/status.go @@ -83,9 +83,10 @@ func New(processor *processing.Processor) *Module { } func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { - // create / get / delete status + // create / get / edit / delete status attachHandler(http.MethodPost, BasePath, m.StatusCreatePOSTHandler) attachHandler(http.MethodGet, BasePathWithID, m.StatusGETHandler) + attachHandler(http.MethodPut, BasePathWithID, m.StatusEditPUTHandler) attachHandler(http.MethodDelete, BasePathWithID, m.StatusDELETEHandler) // fave stuff diff --git a/internal/api/client/statuses/statusboost_test.go b/internal/api/client/statuses/statusboost_test.go index 1f92d8b3f..51b7d7652 100644 --- a/internal/api/client/statuses/statusboost_test.go +++ b/internal/api/client/statuses/statusboost_test.go @@ -100,6 +100,7 @@ func (suite *StatusBoostTestSuite) TestPostBoost() { "card": null, "content": "", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": true, "favourites_count": 0, @@ -145,6 +146,7 @@ func (suite *StatusBoostTestSuite) TestPostBoost() { "card": null, "content": "hello world! #welcome ! first post on the instance :rainbow: !", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [ { "category": "reactions", @@ -280,6 +282,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() { "card": null, "content": "", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, @@ -329,6 +332,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() { "card": null, "content": "hi!", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, @@ -494,6 +498,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostImplicitAccept() { "card": null, "content": "", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, @@ -539,6 +544,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostImplicitAccept() { "card": null, "content": "

Hi @1happyturtle, can I reply?

", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, diff --git a/internal/api/client/statuses/statuscreate.go b/internal/api/client/statuses/statuscreate.go index 8198d5358..c83cdbad7 100644 --- a/internal/api/client/statuses/statuscreate.go +++ b/internal/api/client/statuses/statuscreate.go @@ -27,11 +27,9 @@ "github.com/go-playground/form/v4" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" - "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/util" - "github.com/superseriousbusiness/gotosocial/internal/validate" ) // StatusCreatePOSTHandler swagger:operation POST /api/v1/statuses statusCreate @@ -272,9 +270,9 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { return } - form, err := parseStatusCreateForm(c) - if err != nil { - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + form, errWithCode := parseStatusCreateForm(c) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } @@ -287,11 +285,6 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { // } // form.Status += "\n\nsent from " + user + "'s iphone\n" - if errWithCode := validateStatusCreateForm(form); errWithCode != nil { - apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) - return - } - apiStatus, errWithCode := m.processor.Status().Create( c.Request.Context(), authed.Account, @@ -303,7 +296,7 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { return } - c.JSON(http.StatusOK, apiStatus) + apiutil.JSON(c, http.StatusOK, apiStatus) } // intPolicyFormBinding satisfies gin's binding.Binding interface. @@ -328,108 +321,69 @@ func (intPolicyFormBinding) Bind(req *http.Request, obj any) error { return decoder.Decode(obj, req.Form) } -func parseStatusCreateForm(c *gin.Context) (*apimodel.StatusCreateRequest, error) { +func parseStatusCreateForm(c *gin.Context) (*apimodel.StatusCreateRequest, gtserror.WithCode) { form := new(apimodel.StatusCreateRequest) switch ct := c.ContentType(); ct { case binding.MIMEJSON: // Just bind with default json binding. if err := c.ShouldBindWith(form, binding.JSON); err != nil { - return nil, err + return nil, gtserror.NewErrorBadRequest( + err, + err.Error(), + ) } case binding.MIMEPOSTForm: // Bind with default form binding first. if err := c.ShouldBindWith(form, binding.FormPost); err != nil { - return nil, err + return nil, gtserror.NewErrorBadRequest( + err, + err.Error(), + ) } // Now do custom binding. intReqForm := new(apimodel.StatusInteractionPolicyForm) if err := c.ShouldBindWith(intReqForm, intPolicyFormBinding{}); err != nil { - return nil, err + return nil, gtserror.NewErrorBadRequest( + err, + err.Error(), + ) } + form.InteractionPolicy = intReqForm.InteractionPolicy case binding.MIMEMultipartPOSTForm: // Bind with default form binding first. if err := c.ShouldBindWith(form, binding.FormMultipart); err != nil { - return nil, err + return nil, gtserror.NewErrorBadRequest( + err, + err.Error(), + ) } // Now do custom binding. intReqForm := new(apimodel.StatusInteractionPolicyForm) if err := c.ShouldBindWith(intReqForm, intPolicyFormBinding{}); err != nil { - return nil, err + return nil, gtserror.NewErrorBadRequest( + err, + err.Error(), + ) } + form.InteractionPolicy = intReqForm.InteractionPolicy default: - err := fmt.Errorf( - "content-type %s not supported for this endpoint; supported content-types are %s, %s, %s", - ct, binding.MIMEJSON, binding.MIMEPOSTForm, binding.MIMEMultipartPOSTForm, - ) - return nil, err - } - - return form, nil -} - -// validateStatusCreateForm checks the form for disallowed -// combinations of attachments, overlength inputs, etc. -// -// Side effect: normalizes the post's language tag. -func validateStatusCreateForm(form *apimodel.StatusCreateRequest) gtserror.WithCode { - var ( - chars = len([]rune(form.Status)) + len([]rune(form.SpoilerText)) - maxChars = config.GetStatusesMaxChars() - mediaFiles = len(form.MediaIDs) - maxMediaFiles = config.GetStatusesMediaMaxFiles() - hasMedia = mediaFiles != 0 - hasPoll = form.Poll != nil - ) - - if chars == 0 && !hasMedia && !hasPoll { - // Status must contain *some* kind of content. - const text = "no status content, content warning, media, or poll provided" - return gtserror.NewErrorBadRequest(errors.New(text), text) - } - - if chars > maxChars { - text := fmt.Sprintf( - "status too long, %d characters provided (including content warning) but limit is %d", - chars, maxChars, - ) - return gtserror.NewErrorBadRequest(errors.New(text), text) - } - - if mediaFiles > maxMediaFiles { - text := fmt.Sprintf( - "too many media files attached to status, %d attached but limit is %d", - mediaFiles, maxMediaFiles, - ) - return gtserror.NewErrorBadRequest(errors.New(text), text) - } - - if form.Poll != nil { - if errWithCode := validateStatusPoll(form); errWithCode != nil { - return errWithCode - } + text := fmt.Sprintf("content-type %s not supported for this endpoint; supported content-types are %s, %s, %s", + ct, binding.MIMEJSON, binding.MIMEPOSTForm, binding.MIMEMultipartPOSTForm) + return nil, gtserror.NewErrorNotAcceptable(errors.New(text), text) } + // Check not scheduled status. if form.ScheduledAt != "" { const text = "scheduled_at is not yet implemented" - return gtserror.NewErrorNotImplemented(errors.New(text), text) - } - - // Validate + normalize - // language tag if provided. - if form.Language != "" { - lang, err := validate.Language(form.Language) - if err != nil { - return gtserror.NewErrorBadRequest(err, err.Error()) - } - form.Language = lang + return nil, gtserror.NewErrorNotImplemented(errors.New(text), text) } // Check if the deprecated "federated" field was @@ -438,42 +392,9 @@ func validateStatusCreateForm(form *apimodel.StatusCreateRequest) gtserror.WithC form.LocalOnly = util.Ptr(!*form.Federated) // nolint:staticcheck } - return nil -} + // Normalize poll expiry time if a poll was given. + if form.Poll != nil && form.Poll.ExpiresInI != nil { -func validateStatusPoll(form *apimodel.StatusCreateRequest) gtserror.WithCode { - var ( - maxPollOptions = config.GetStatusesPollMaxOptions() - pollOptions = len(form.Poll.Options) - maxPollOptionChars = config.GetStatusesPollOptionMaxChars() - ) - - if pollOptions == 0 { - const text = "poll with no options" - return gtserror.NewErrorBadRequest(errors.New(text), text) - } - - if pollOptions > maxPollOptions { - text := fmt.Sprintf( - "too many poll options provided, %d provided but limit is %d", - pollOptions, maxPollOptions, - ) - return gtserror.NewErrorBadRequest(errors.New(text), text) - } - - for _, option := range form.Poll.Options { - optionChars := len([]rune(option)) - if optionChars > maxPollOptionChars { - text := fmt.Sprintf( - "poll option too long, %d characters provided but limit is %d", - optionChars, maxPollOptionChars, - ) - return gtserror.NewErrorBadRequest(errors.New(text), text) - } - } - - // Normalize poll expiry if necessary. - 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( @@ -481,13 +402,10 @@ func validateStatusPoll(form *apimodel.StatusCreateRequest) gtserror.WithCode { "expires_in", ) if err != nil { - return gtserror.NewErrorBadRequest(err, err.Error()) - } - - if expiresIn != nil { - form.Poll.ExpiresIn = *expiresIn + return nil, gtserror.NewErrorBadRequest(err, err.Error()) } + form.Poll.ExpiresIn = util.PtrOrZero(expiresIn) } - return nil + return form, nil } diff --git a/internal/api/client/statuses/statuscreate_test.go b/internal/api/client/statuses/statuscreate_test.go index 5f5386dd5..227e7d83e 100644 --- a/internal/api/client/statuses/statuscreate_test.go +++ b/internal/api/client/statuses/statuscreate_test.go @@ -102,6 +102,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() { "card": null, "content": "

this is a brand new status! #helloworld

", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, @@ -187,6 +188,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusIntPolicy() { "card": null, "content": "

this is a brand new status! #helloworld

", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, @@ -282,6 +284,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusIntPolicyJSON() { "card": null, "content": "

this is a brand new status! #helloworld

", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, @@ -407,6 +410,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() { "card": null, "content": "

Title

Smaller title

This is a post written in markdown

", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, @@ -490,6 +494,7 @@ func (suite *StatusCreateTestSuite) TestMentionUnknownAccount() { "card": null, "content": "

hello @brand_new_person

", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, @@ -567,6 +572,7 @@ func (suite *StatusCreateTestSuite) TestPostStatusWithLinksAndTags() { "card": null, "content": "

#test alright, should be able to post #links with fragments in them now, let's see........

https://docs.gotosocial.org/en/latest/user_guide/posts/#links

#gotosocial

(tobi remember to pull the docker image challenge)

", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, @@ -650,6 +656,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() { "card": null, "content": "

here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow:
here's an emoji that isn't in the db: :test_emoji:

", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [ { "category": "reactions", @@ -747,6 +754,7 @@ func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() { "card": null, "content": "

hello @1happyturtle this reply should work!

", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, @@ -829,6 +837,7 @@ func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() { "card": null, "content": "

here's an image attachment

", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, @@ -933,6 +942,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithNoncanonicalLanguageTag "card": null, "content": "

English? what's English? i speak American

", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, @@ -1007,6 +1017,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithPollForm() { "card": null, "content": "

this is a status with a poll!

", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, @@ -1103,6 +1114,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithPollJSON() { "card": null, "content": "

this is a status with a poll!

", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": false, "favourites_count": 0, diff --git a/internal/api/client/statuses/statusdelete.go b/internal/api/client/statuses/statusdelete.go index 7ee240dff..fa62d6893 100644 --- a/internal/api/client/statuses/statusdelete.go +++ b/internal/api/client/statuses/statusdelete.go @@ -95,5 +95,5 @@ func (m *Module) StatusDELETEHandler(c *gin.Context) { return } - c.JSON(http.StatusOK, apiStatus) + apiutil.JSON(c, http.StatusOK, apiStatus) } diff --git a/internal/api/client/statuses/statusedit.go b/internal/api/client/statuses/statusedit.go new file mode 100644 index 000000000..dfd7d651e --- /dev/null +++ b/internal/api/client/statuses/statusedit.go @@ -0,0 +1,249 @@ +// 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 statuses + +import ( + "errors" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +// StatusEditPUTHandler swagger:operation PUT /api/v1/statuses statusEdit +// +// Edit an existing status using the given form field parameters. +// +// The parameters can also be given in the body of the request, as JSON, if the content-type is set to 'application/json'. +// +// --- +// tags: +// - statuses +// +// consumes: +// - application/json +// - application/x-www-form-urlencoded +// +// parameters: +// - +// name: status +// x-go-name: Status +// description: |- +// Text content of the status. +// If media_ids is provided, this becomes optional. +// Attaching a poll is optional while status is provided. +// type: string +// in: formData +// - +// name: media_ids +// x-go-name: MediaIDs +// description: |- +// Array of Attachment ids to be attached as media. +// If provided, status becomes optional, and poll cannot be used. +// +// If the status is being submitted as a form, the key is 'media_ids[]', +// but if it's json or xml, the key is 'media_ids'. +// type: array +// items: +// type: string +// in: formData +// - +// name: poll[options][] +// x-go-name: PollOptions +// description: |- +// Array of possible poll answers. +// If provided, media_ids cannot be used, and poll[expires_in] must be provided. +// type: array +// items: +// type: string +// in: formData +// - +// name: poll[expires_in] +// x-go-name: PollExpiresIn +// description: |- +// Duration the poll should be open, in seconds. +// If provided, media_ids cannot be used, and poll[options] must be provided. +// type: integer +// format: int64 +// in: formData +// - +// name: poll[multiple] +// x-go-name: PollMultiple +// description: Allow multiple choices on this poll. +// type: boolean +// default: false +// in: formData +// - +// name: poll[hide_totals] +// x-go-name: PollHideTotals +// description: Hide vote counts until the poll ends. +// type: boolean +// default: true +// in: formData +// - +// name: sensitive +// x-go-name: Sensitive +// description: Status and attached media should be marked as sensitive. +// type: boolean +// in: formData +// - +// name: spoiler_text +// x-go-name: SpoilerText +// description: |- +// Text to be shown as a warning or subject before the actual content. +// Statuses are generally collapsed behind this field. +// type: string +// in: formData +// - +// name: language +// x-go-name: Language +// description: ISO 639 language code for this status. +// type: string +// in: formData +// - +// name: content_type +// x-go-name: ContentType +// description: Content type to use when parsing this status. +// type: string +// enum: +// - text/plain +// - text/markdown +// in: formData +// +// produces: +// - application/json +// +// security: +// - OAuth2 Bearer: +// - write:statuses +// +// responses: +// '200': +// description: "The latest status revision." +// 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 +func (m *Module) StatusEditPUTHandler(c *gin.Context) { + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) + return + } + + if authed.Account.IsMoving() { + apiutil.ForbiddenAfterMove(c) + return + } + + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + form, errWithCode := parseStatusEditForm(c) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + apiStatus, errWithCode := m.processor.Status().Edit( + c.Request.Context(), + authed.Account, + c.Param(IDKey), + form, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + apiutil.JSON(c, http.StatusOK, apiStatus) +} + +func parseStatusEditForm(c *gin.Context) (*apimodel.StatusEditRequest, gtserror.WithCode) { + form := new(apimodel.StatusEditRequest) + + switch ct := c.ContentType(); ct { + case binding.MIMEJSON: + // Just bind with default json binding. + if err := c.ShouldBindWith(form, binding.JSON); err != nil { + return nil, gtserror.NewErrorBadRequest( + err, + err.Error(), + ) + } + + case binding.MIMEPOSTForm: + // Bind with default form binding first. + if err := c.ShouldBindWith(form, binding.FormPost); err != nil { + return nil, gtserror.NewErrorBadRequest( + err, + err.Error(), + ) + } + + case binding.MIMEMultipartPOSTForm: + // Bind with default form binding first. + if err := c.ShouldBindWith(form, binding.FormMultipart); err != nil { + return nil, gtserror.NewErrorBadRequest( + err, + err.Error(), + ) + } + + default: + text := fmt.Sprintf("content-type %s not supported for this endpoint; supported content-types are %s, %s, %s", + ct, binding.MIMEJSON, binding.MIMEPOSTForm, binding.MIMEMultipartPOSTForm) + return nil, gtserror.NewErrorNotAcceptable(errors.New(text), text) + } + + // Normalize poll expiry time if a poll was given. + if form.Poll != nil && 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 nil, gtserror.NewErrorBadRequest(err, err.Error()) + } + form.Poll.ExpiresIn = util.PtrOrZero(expiresIn) + } + + return form, nil + +} diff --git a/internal/api/client/statuses/statusedit_test.go b/internal/api/client/statuses/statusedit_test.go new file mode 100644 index 000000000..43b283d6d --- /dev/null +++ b/internal/api/client/statuses/statusedit_test.go @@ -0,0 +1,32 @@ +// 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 statuses_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type StatusEditTestSuite struct { + StatusStandardTestSuite +} + +func TestStatusEditTestSuite(t *testing.T) { + suite.Run(t, new(StatusEditTestSuite)) +} diff --git a/internal/api/client/statuses/statusfave_test.go b/internal/api/client/statuses/statusfave_test.go index bd81c0cf9..8851b4d58 100644 --- a/internal/api/client/statuses/statusfave_test.go +++ b/internal/api/client/statuses/statusfave_test.go @@ -105,6 +105,7 @@ func (suite *StatusFaveTestSuite) TestPostFave() { "card": null, "content": "🐕🐕🐕🐕🐕", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": true, "favourites_count": 1, @@ -228,6 +229,7 @@ func (suite *StatusFaveTestSuite) TestPostFaveImplicitAccept() { "card": null, "content": "

Hi @1happyturtle, can I reply?

", "created_at": "right the hell just now babyee", + "edited_at": null, "emojis": [], "favourited": true, "favourites_count": 1, diff --git a/internal/api/client/statuses/statushistory_test.go b/internal/api/client/statuses/statushistory_test.go index aea666dbb..3878f54e4 100644 --- a/internal/api/client/statuses/statushistory_test.go +++ b/internal/api/client/statuses/statushistory_test.go @@ -116,8 +116,8 @@ func (suite *StatusHistoryTestSuite) TestGetHistory() { "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", "followers_count": 2, "following_count": 2, - "statuses_count": 8, - "last_status_at": "2024-01-10", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [], "enable_rss": true diff --git a/internal/api/client/statuses/statusmute_test.go b/internal/api/client/statuses/statusmute_test.go index 384761fc6..66bd4a420 100644 --- a/internal/api/client/statuses/statusmute_test.go +++ b/internal/api/client/statuses/statusmute_test.go @@ -91,6 +91,7 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() { suite.Equal(`{ "id": "01F8MHAMCHF6Y650WCRSCP4WMY", "created_at": "2021-10-20T10:40:37.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": true, @@ -134,8 +135,8 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() { "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", "followers_count": 2, "following_count": 2, - "statuses_count": 8, - "last_status_at": "2024-01-10", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [], "enable_rss": true @@ -178,6 +179,7 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() { suite.Equal(`{ "id": "01F8MHAMCHF6Y650WCRSCP4WMY", "created_at": "2021-10-20T10:40:37.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": true, @@ -221,8 +223,8 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() { "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", "followers_count": 2, "following_count": 2, - "statuses_count": 8, - "last_status_at": "2024-01-10", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [], "enable_rss": true diff --git a/internal/api/client/statuses/statussource_test.go b/internal/api/client/statuses/statussource_test.go index 28b1e6852..797a462ed 100644 --- a/internal/api/client/statuses/statussource_test.go +++ b/internal/api/client/statuses/statussource_test.go @@ -91,7 +91,7 @@ func (suite *StatusSourceTestSuite) TestGetSource() { suite.Equal(`{ "id": "01F8MHAMCHF6Y650WCRSCP4WMY", - "text": "**STATUS EDITS ARE NOT CURRENTLY SUPPORTED IN GOTOSOCIAL (coming in 2024)**\nYou can review the original text of your status below, but you will not be able to submit this edit.\n\n---\n\nhello everyone!", + "text": "hello everyone!", "spoiler_text": "introduction post" }`, dst.String()) } diff --git a/internal/api/model/attachment.go b/internal/api/model/attachment.go index f037a09aa..1d910343c 100644 --- a/internal/api/model/attachment.go +++ b/internal/api/model/attachment.go @@ -23,12 +23,15 @@ // // swagger: ignore type AttachmentRequest struct { + // Media file. File *multipart.FileHeader `form:"file" binding:"required"` + // Description of the media file. Optional. // This will be used as alt-text for users of screenreaders etc. // example: This is an image of some kittens, they are very cute and fluffy. Description string `form:"description"` + // Focus of the media file. Optional. // If present, it should be in the form of two comma-separated floats between -1 and 1. // example: -0.5,0.565 @@ -39,16 +42,38 @@ type AttachmentRequest struct { // // swagger:ignore type AttachmentUpdateRequest struct { + // Description of the media file. // This will be used as alt-text for users of screenreaders etc. // allowEmptyValue: true Description *string `form:"description" json:"description" xml:"description"` + // Focus of the media file. // If present, it should be in the form of two comma-separated floats between -1 and 1. // allowEmptyValue: true Focus *string `form:"focus" json:"focus" xml:"focus"` } +// AttachmentAttributesRequest models an edit request for attachment attributes. +// +// swagger:ignore +type AttachmentAttributesRequest struct { + + // The ID of the attachment. + // example: 01FC31DZT1AYWDZ8XTCRWRBYRK + ID string `form:"id" json:"id"` + + // Description of the media file. + // This will be used as alt-text for users of screenreaders etc. + // allowEmptyValue: true + Description string `form:"description" json:"description"` + + // Focus of the media file. + // If present, it should be in the form of two comma-separated floats between -1 and 1. + // allowEmptyValue: true + Focus string `form:"focus" json:"focus"` +} + // Attachment models a media attachment. // // swagger:model attachment diff --git a/internal/api/model/content.go b/internal/api/model/content.go index 7da389ed1..5af81b11b 100644 --- a/internal/api/model/content.go +++ b/internal/api/model/content.go @@ -19,7 +19,6 @@ import ( "io" - "time" "github.com/superseriousbusiness/gotosocial/internal/storage" ) @@ -30,8 +29,6 @@ type Content struct { ContentType string // ContentLength in bytes ContentLength int64 - // Time when the content was last updated. - ContentUpdated time.Time // Actual content Content io.ReadCloser // Resource URL to forward to if the file can be fetched from the storage directly (e.g signed S3 URL) diff --git a/internal/api/model/instance.go b/internal/api/model/instance.go index 5232e8d66..d59424fa5 100644 --- a/internal/api/model/instance.go +++ b/internal/api/model/instance.go @@ -33,6 +33,8 @@ type InstanceSettingsUpdateRequest struct { ShortDescription *string `form:"short_description" json:"short_description" xml:"short_description"` // Longer description of the instance, max 5,000 chars. HTML formatting accepted. Description *string `form:"description" json:"description" xml:"description"` + // Custom CSS for the instance. + CustomCSS *string `form:"custom_css" json:"custom_css,omitempty" xml:"custom_css"` // Terms and conditions of the instance, max 5,000 chars. HTML formatting accepted. Terms *string `form:"terms" json:"terms" xml:"terms"` // Image to use as the instance thumbnail. diff --git a/internal/api/model/instancev1.go b/internal/api/model/instancev1.go index efa6d6faa..6dedd04cc 100644 --- a/internal/api/model/instancev1.go +++ b/internal/api/model/instancev1.go @@ -38,6 +38,8 @@ type InstanceV1 struct { // // This should be displayed on the 'about' page for an instance. Description string `json:"description"` + // Custom CSS for the instance. + CustomCSS string `json:"custom_css,omitempty"` // Raw (unparsed) version of description. DescriptionText string `json:"description_text,omitempty"` // A shorter description of the instance. diff --git a/internal/api/model/instancev2.go b/internal/api/model/instancev2.go index dcbd14ec0..b3d11dee2 100644 --- a/internal/api/model/instancev2.go +++ b/internal/api/model/instancev2.go @@ -53,6 +53,8 @@ type InstanceV2 struct { Description string `json:"description"` // Raw (unparsed) version of description. DescriptionText string `json:"description_text,omitempty"` + // Instance Custom Css + CustomCSS string `json:"custom_css,omitempty"` // Basic anonymous usage data for this instance. Usage InstanceV2Usage `json:"usage"` // An image used to represent this instance. diff --git a/internal/api/model/status.go b/internal/api/model/status.go index c29ab3e82..ea9fbaa35 100644 --- a/internal/api/model/status.go +++ b/internal/api/model/status.go @@ -29,6 +29,10 @@ type Status struct { // The date when this status was created (ISO 8601 Datetime). // example: 2021-07-30T09:20:25+00:00 CreatedAt string `json:"created_at"` + // Timestamp of when the status was last edited (ISO 8601 Datetime). + // example: 2021-07-30T09:20:25+00:00 + // nullable: true + EditedAt *string `json:"edited_at"` // ID of the status being replied to. // example: 01FBVD42CQ3ZEEVMW180SBX03B // nullable: true @@ -193,36 +197,50 @@ type StatusReblogged struct { // // swagger:ignore type StatusCreateRequest struct { + // Text content of the status. // If media_ids is provided, this becomes optional. // Attaching a poll is optional while status is provided. Status string `form:"status" json:"status"` + // Array of Attachment ids to be attached as media. // If provided, status becomes optional, and poll cannot be used. MediaIDs []string `form:"media_ids[]" json:"media_ids"` + // Poll to include with this status. Poll *PollRequest `form:"poll" json:"poll"` + // ID of the status being replied to, if status is a reply. InReplyToID string `form:"in_reply_to_id" json:"in_reply_to_id"` + // Status and attached media should be marked as sensitive. Sensitive bool `form:"sensitive" json:"sensitive"` + // Text to be shown as a warning or subject before the actual content. // Statuses are generally collapsed behind this field. SpoilerText string `form:"spoiler_text" json:"spoiler_text"` + // Visibility of the posted status. Visibility Visibility `form:"visibility" json:"visibility"` - // Set to "true" if this status should not be federated, ie. it should be a "local only" status. + + // Set to "true" if this status should not be + // federated,ie. it should be a "local only" status. LocalOnly *bool `form:"local_only" json:"local_only"` + // Deprecated: Only used if LocalOnly is not set. Federated *bool `form:"federated" json:"federated"` + // ISO 8601 Datetime at which to schedule a status. // Providing this parameter will cause ScheduledStatus to be returned instead of Status. // Must be at least 5 minutes in the future. ScheduledAt string `form:"scheduled_at" json:"scheduled_at"` + // ISO 639 language code for this status. Language string `form:"language" json:"language"` + // Content type to use when parsing this status. ContentType StatusContentType `form:"content_type" json:"content_type"` + // Interaction policy to use for this status. InteractionPolicy *InteractionPolicy `form:"-" json:"interaction_policy"` } @@ -232,6 +250,7 @@ type StatusCreateRequest struct { // // swagger:ignore type StatusInteractionPolicyForm struct { + // Interaction policy to use for this status. InteractionPolicy *InteractionPolicy `form:"interaction_policy" json:"-"` } @@ -246,13 +265,18 @@ type StatusInteractionPolicyForm struct { // VisibilityNone is visible to nobody. This is only used for the visibility of web statuses. VisibilityNone Visibility = "none" // VisibilityPublic is visible to everyone, and will be available via the web even for nonauthenticated users. + VisibilityPublic Visibility = "public" + // VisibilityUnlisted is visible to everyone, but only on home timelines, lists, etc. VisibilityUnlisted Visibility = "unlisted" + // VisibilityPrivate is visible only to followers of the account that posted the status. VisibilityPrivate Visibility = "private" + // VisibilityMutualsOnly is visible only to mutual followers of the account that posted the status. VisibilityMutualsOnly Visibility = "mutuals_only" + // VisibilityDirect is visible only to accounts tagged in the status. It is equivalent to a direct message. VisibilityDirect Visibility = "direct" ) @@ -264,7 +288,8 @@ type StatusInteractionPolicyForm struct { // swagger:type string type StatusContentType string -// Content type to use when parsing submitted status into an html-formatted status +// Content type to use when parsing submitted +// status into an html-formatted status. const ( StatusContentTypePlain StatusContentType = "text/plain" StatusContentTypeMarkdown StatusContentType = "text/markdown" @@ -276,11 +301,14 @@ type StatusInteractionPolicyForm struct { // // swagger:model statusSource type StatusSource struct { + // ID of the status. // example: 01FBVD42CQ3ZEEVMW180SBX03B ID string `json:"id"` + // Plain-text source of a status. Text string `json:"text"` + // Plain-text version of spoiler text. SpoilerText string `json:"spoiler_text"` } @@ -290,27 +318,69 @@ type StatusSource struct { // // swagger:model statusEdit type StatusEdit struct { + // The content of this status at this revision. // Should be HTML, but might also be plaintext in some cases. // example:

Hey this is a status!

Content string `json:"content"` + // Subject, summary, or content warning for the status at this revision. // example: warning nsfw SpoilerText string `json:"spoiler_text"` + // Status marked sensitive at this revision. // example: false Sensitive bool `json:"sensitive"` + // The date when this revision was created (ISO 8601 Datetime). // example: 2021-07-30T09:20:25+00:00 CreatedAt string `json:"created_at"` + // The account that authored this status. Account *Account `json:"account"` + // The poll attached to the status at this revision. // Note that edits changing the poll options will be collapsed together into one edit, since this action resets the poll. // nullable: true Poll *Poll `json:"poll"` + // Media that is attached to this status. MediaAttachments []*Attachment `json:"media_attachments"` + // Custom emoji to be used when rendering status content. Emojis []Emoji `json:"emojis"` } + +// StatusEditRequest models status edit parameters. +// +// swagger:ignore +type StatusEditRequest struct { + + // Text content of the status. + // If media_ids is provided, this becomes optional. + // Attaching a poll is optional while status is provided. + Status string `form:"status" json:"status"` + + // Text to be shown as a warning or subject before the actual content. + // Statuses are generally collapsed behind this field. + SpoilerText string `form:"spoiler_text" json:"spoiler_text"` + + // Content type to use when parsing this status. + ContentType StatusContentType `form:"content_type" json:"content_type"` + + // Status and attached media should be marked as sensitive. + Sensitive bool `form:"sensitive" json:"sensitive"` + + // ISO 639 language code for this status. + Language string `form:"language" json:"language"` + + // Array of Attachment ids to be attached as media. + // If provided, status becomes optional, and poll cannot be used. + MediaIDs []string `form:"media_ids[]" json:"media_ids"` + + // Array of Attachment attributes to be updated in attached media. + MediaAttributes []AttachmentAttributesRequest `form:"media_attributes[]" json:"media_attributes"` + + // Poll to include with this status. + Poll *PollRequest `form:"poll" json:"poll"` +} diff --git a/internal/api/util/parseform.go b/internal/api/util/parseform.go index 3eab065f2..8bb10012c 100644 --- a/internal/api/util/parseform.go +++ b/internal/api/util/parseform.go @@ -18,13 +18,55 @@ package util import ( + "errors" "fmt" "strconv" + "strings" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/util" ) +// ParseFocus parses a media attachment focus parameters from incoming API string. +func ParseFocus(focus string) (focusx, focusy float32, errWithCode gtserror.WithCode) { + if focus == "" { + return + } + spl := strings.Split(focus, ",") + if len(spl) != 2 { + const text = "missing comma separator" + errWithCode = gtserror.NewErrorBadRequest( + errors.New(text), + text, + ) + return + } + xStr := spl[0] + yStr := spl[1] + fx, err := strconv.ParseFloat(xStr, 32) + if err != nil || fx > 1 || fx < -1 { + text := fmt.Sprintf("invalid x focus: %s", xStr) + errWithCode = gtserror.NewErrorBadRequest( + errors.New(text), + text, + ) + return + } + fy, err := strconv.ParseFloat(yStr, 32) + if err != nil || fy > 1 || fy < -1 { + text := fmt.Sprintf("invalid y focus: %s", xStr) + errWithCode = gtserror.NewErrorBadRequest( + errors.New(text), + text, + ) + return + } + focusx = float32(fx) + focusy = float32(fy) + return +} + // ParseDuration parses the given raw interface belonging // the given fieldName as an integer duration. func ParseDuration(rawI any, fieldName string) (*int, error) { diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 152ae33d7..0154c0ff0 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -105,6 +105,7 @@ func (c *Caches) Init() { c.initStatus() c.initStatusBookmark() c.initStatusBookmarkIDs() + c.initStatusEdit() c.initStatusFave() c.initStatusFaveIDs() c.initTag() diff --git a/internal/cache/db.go b/internal/cache/db.go index c264d5567..8638cce62 100644 --- a/internal/cache/db.go +++ b/internal/cache/db.go @@ -226,6 +226,9 @@ type DBCaches struct { // StatusBookmarkIDs provides access to the status bookmark IDs list database cache. StatusBookmarkIDs SliceCache[string] + // StatusEdit provides access to the gtsmodel StatusEdit database cache. + StatusEdit StructCache[*gtsmodel.StatusEdit] + // StatusFave provides access to the gtsmodel StatusFave database cache. StatusFave StructCache[*gtsmodel.StatusFave] @@ -1394,6 +1397,38 @@ func (c *Caches) initStatusBookmarkIDs() { c.DB.StatusBookmarkIDs.Init(0, cap) } +func (c *Caches) initStatusEdit() { + // Calculate maximum cache size. + cap := calculateResultCacheMax( + sizeofStatusEdit(), // model in-mem size. + config.GetCacheStatusEditMemRatio(), + ) + + log.Infof(nil, "cache size = %d", cap) + + copyF := func(s1 *gtsmodel.StatusEdit) *gtsmodel.StatusEdit { + s2 := new(gtsmodel.StatusEdit) + *s2 = *s1 + + // Don't include ptr fields that + // will be populated separately. + s2.Attachments = nil + + return s2 + } + + c.DB.StatusEdit.Init(structr.CacheConfig[*gtsmodel.StatusEdit]{ + Indices: []structr.IndexConfig{ + {Fields: "ID"}, + {Fields: "StatusID", Multiple: true}, + }, + MaxSize: cap, + IgnoreErr: ignoreErrors, + Copy: copyF, + Invalidate: c.OnInvalidateStatusEdit, + }) +} + func (c *Caches) initStatusFave() { // Calculate maximum cache size. cap := calculateResultCacheMax( diff --git a/internal/cache/invalidate.go b/internal/cache/invalidate.go index be3eaa735..555c73cd7 100644 --- a/internal/cache/invalidate.go +++ b/internal/cache/invalidate.go @@ -273,6 +273,11 @@ func (c *Caches) OnInvalidateStatusBookmark(bookmark *gtsmodel.StatusBookmark) { c.DB.StatusBookmarkIDs.Invalidate(bookmark.StatusID) } +func (c *Caches) OnInvalidateStatusEdit(edit *gtsmodel.StatusEdit) { + // Invalidate cache of related status model. + c.DB.Status.Invalidate("ID", edit.StatusID) +} + func (c *Caches) OnInvalidateStatusFave(fave *gtsmodel.StatusFave) { // Invalidate status fave ID list for this status. c.DB.StatusFaveIDs.Invalidate(fave.StatusID) diff --git a/internal/cache/size.go b/internal/cache/size.go index abed1e3b6..f5b2b4d5c 100644 --- a/internal/cache/size.go +++ b/internal/cache/size.go @@ -513,7 +513,6 @@ func sizeofMedia() uintptr { URL: exampleURI, RemoteURL: exampleURI, CreatedAt: exampleTime, - UpdatedAt: exampleTime, Type: gtsmodel.FileTypeImage, AccountID: exampleID, Description: exampleText, @@ -540,7 +539,6 @@ func sizeofMention() uintptr { ID: exampleURI, StatusID: exampleURI, CreatedAt: exampleTime, - UpdatedAt: exampleTime, OriginAccountID: exampleURI, OriginAccountURI: exampleURI, TargetAccountID: exampleID, @@ -682,6 +680,23 @@ func sizeofStatusBookmark() uintptr { })) } +func sizeofStatusEdit() uintptr { + return uintptr(size.Of(>smodel.StatusEdit{ + ID: exampleID, + Content: exampleText, + ContentWarning: exampleUsername, // similar length + Text: exampleText, + Language: "en", + Sensitive: func() *bool { ok := false; return &ok }(), + AttachmentIDs: []string{exampleID, exampleID, exampleID}, + Attachments: nil, + PollOptions: []string{exampleTextSmall, exampleTextSmall, exampleTextSmall, exampleTextSmall}, + PollVotes: []int{69, 420, 1337, 1969}, + StatusID: exampleID, + CreatedAt: exampleTime, + })) +} + func sizeofStatusFave() uintptr { return uintptr(size.Of(>smodel.StatusFave{ ID: exampleID, diff --git a/internal/config/config.go b/internal/config/config.go index d9491740e..fd6cc82db 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -238,6 +238,7 @@ type CacheConfiguration struct { StatusMemRatio float64 `name:"status-mem-ratio"` StatusBookmarkMemRatio float64 `name:"status-bookmark-mem-ratio"` StatusBookmarkIDsMemRatio float64 `name:"status-bookmark-ids-mem-ratio"` + StatusEditMemRatio float64 `name:"status-edit-mem-ratio"` StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"` StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"` TagMemRatio float64 `name:"tag-mem-ratio"` diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 0b28b9025..b0aed5422 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -199,6 +199,7 @@ StatusMemRatio: 5, StatusBookmarkMemRatio: 0.5, StatusBookmarkIDsMemRatio: 2, + StatusEditMemRatio: 2, StatusFaveMemRatio: 2, StatusFaveIDsMemRatio: 3, TagMemRatio: 2, diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go index 2c554d87a..9ec33e6d9 100644 --- a/internal/config/helpers.gen.go +++ b/internal/config/helpers.gen.go @@ -3912,6 +3912,31 @@ func GetCacheStatusBookmarkIDsMemRatio() float64 { return global.GetCacheStatusB // SetCacheStatusBookmarkIDsMemRatio safely sets the value for global configuration 'Cache.StatusBookmarkIDsMemRatio' field func SetCacheStatusBookmarkIDsMemRatio(v float64) { global.SetCacheStatusBookmarkIDsMemRatio(v) } +// GetCacheStatusEditMemRatio safely fetches the Configuration value for state's 'Cache.StatusEditMemRatio' field +func (st *ConfigState) GetCacheStatusEditMemRatio() (v float64) { + st.mutex.RLock() + v = st.config.Cache.StatusEditMemRatio + st.mutex.RUnlock() + return +} + +// SetCacheStatusEditMemRatio safely sets the Configuration value for state's 'Cache.StatusEditMemRatio' field +func (st *ConfigState) SetCacheStatusEditMemRatio(v float64) { + st.mutex.Lock() + defer st.mutex.Unlock() + st.config.Cache.StatusEditMemRatio = v + st.reloadToViper() +} + +// CacheStatusEditMemRatioFlag returns the flag name for the 'Cache.StatusEditMemRatio' field +func CacheStatusEditMemRatioFlag() string { return "cache-status-edit-mem-ratio" } + +// GetCacheStatusEditMemRatio safely fetches the value for global configuration 'Cache.StatusEditMemRatio' field +func GetCacheStatusEditMemRatio() float64 { return global.GetCacheStatusEditMemRatio() } + +// SetCacheStatusEditMemRatio safely sets the value for global configuration 'Cache.StatusEditMemRatio' field +func SetCacheStatusEditMemRatio(v float64) { global.SetCacheStatusEditMemRatio(v) } + // GetCacheStatusFaveMemRatio safely fetches the Configuration value for state's 'Cache.StatusFaveMemRatio' field func (st *ConfigState) GetCacheStatusFaveMemRatio() (v float64) { st.mutex.RLock() diff --git a/internal/db/bundb/account_test.go b/internal/db/bundb/account_test.go index 7dcc0f9e7..879250408 100644 --- a/internal/db/bundb/account_test.go +++ b/internal/db/bundb/account_test.go @@ -46,7 +46,7 @@ type AccountTestSuite struct { func (suite *AccountTestSuite) TestGetAccountStatuses() { statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, false, false, "", "", false, false) suite.NoError(err) - suite.Len(statuses, 8) + suite.Len(statuses, 9) } func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() { @@ -69,7 +69,7 @@ func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() { if err != nil { suite.FailNow(err.Error()) } - suite.Len(statuses, 2) + suite.Len(statuses, 3) // try to get the last page (should be empty) statuses, err = suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 3, false, false, statuses[len(statuses)-1].ID, "", false, false) @@ -80,13 +80,13 @@ func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() { func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesAndReblogs() { statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, true, true, "", "", false, false) suite.NoError(err) - suite.Len(statuses, 7) + suite.Len(statuses, 8) } func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesAndReblogsPublicOnly() { statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, true, true, "", "", false, true) suite.NoError(err) - suite.Len(statuses, 3) + suite.Len(statuses, 4) } // populateTestStatus adds mandatory fields to a partially populated status. @@ -173,7 +173,7 @@ func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesExcludesSelfR testAccount := suite.testAccounts["local_account_1"] statuses, err := suite.db.GetAccountStatuses(context.Background(), testAccount.ID, 20, true, true, "", "", false, false) suite.NoError(err) - suite.Len(statuses, 8) + suite.Len(statuses, 9) for _, status := range statuses { if status.InReplyToID != "" && status.InReplyToAccountID != testAccount.ID { suite.FailNowf("", "Status with ID %s is a non-self reply and should have been excluded", status.ID) diff --git a/internal/db/bundb/basic_test.go b/internal/db/bundb/basic_test.go index 56159dc25..e20aab765 100644 --- a/internal/db/bundb/basic_test.go +++ b/internal/db/bundb/basic_test.go @@ -114,7 +114,7 @@ func (suite *BasicTestSuite) TestGetAllStatuses() { s := []*gtsmodel.Status{} err := suite.db.GetAll(context.Background(), &s) suite.NoError(err) - suite.Len(s, 25) + suite.Len(s, 28) } func (suite *BasicTestSuite) TestGetAllNotNull() { diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go index c307e0356..c9dd7866d 100644 --- a/internal/db/bundb/bundb.go +++ b/internal/db/bundb/bundb.go @@ -81,6 +81,7 @@ type DBService struct { db.SinBinStatus db.Status db.StatusBookmark + db.StatusEdit db.StatusFave db.Tag db.Thread @@ -273,6 +274,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) { db: db, state: state, }, + StatusEdit: &statusEditDB{ + db: db, + state: state, + }, StatusFave: &statusFaveDB{ db: db, state: state, diff --git a/internal/db/bundb/bundb_test.go b/internal/db/bundb/bundb_test.go index e976199e4..2fcf61aed 100644 --- a/internal/db/bundb/bundb_test.go +++ b/internal/db/bundb/bundb_test.go @@ -57,6 +57,7 @@ type BunDBStandardTestSuite struct { testPolls map[string]*gtsmodel.Poll testPollVotes map[string]*gtsmodel.PollVote testInteractionRequests map[string]*gtsmodel.InteractionRequest + testStatusEdits map[string]*gtsmodel.StatusEdit } func (suite *BunDBStandardTestSuite) SetupSuite() { @@ -83,6 +84,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() { suite.testPolls = testrig.NewTestPolls() suite.testPollVotes = testrig.NewTestPollVotes() suite.testInteractionRequests = testrig.NewTestInteractionRequests() + suite.testStatusEdits = testrig.NewTestStatusEdits() } func (suite *BunDBStandardTestSuite) SetupTest() { diff --git a/internal/db/bundb/instance_test.go b/internal/db/bundb/instance_test.go index 4b8ec9962..1364bacc2 100644 --- a/internal/db/bundb/instance_test.go +++ b/internal/db/bundb/instance_test.go @@ -47,13 +47,13 @@ func (suite *InstanceTestSuite) TestCountInstanceUsersRemote() { func (suite *InstanceTestSuite) TestCountInstanceStatuses() { count, err := suite.db.CountInstanceStatuses(context.Background(), config.GetHost()) suite.NoError(err) - suite.Equal(19, count) + suite.Equal(21, count) } func (suite *InstanceTestSuite) TestCountInstanceStatusesRemote() { count, err := suite.db.CountInstanceStatuses(context.Background(), "fossbros-anonymous.io") suite.NoError(err) - suite.Equal(3, count) + suite.Equal(4, count) } func (suite *InstanceTestSuite) TestCountInstanceDomains() { diff --git a/internal/db/bundb/interaction_test.go b/internal/db/bundb/interaction_test.go index 37684f18c..1eb8154c1 100644 --- a/internal/db/bundb/interaction_test.go +++ b/internal/db/bundb/interaction_test.go @@ -59,11 +59,7 @@ func (suite *InteractionTestSuite) markInteractionsPending( // Put an interaction request // in the DB for this reply. - req, err := typeutils.StatusToInteractionRequest(ctx, reply) - if err != nil { - suite.FailNow(err.Error()) - } - + req := typeutils.StatusToInteractionRequest(reply) if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil { suite.FailNow(err.Error()) } @@ -90,11 +86,7 @@ func (suite *InteractionTestSuite) markInteractionsPending( // Put an interaction request // in the DB for this boost. - req, err := typeutils.StatusToInteractionRequest(ctx, boost) - if err != nil { - suite.FailNow(err.Error()) - } - + req := typeutils.StatusToInteractionRequest(boost) if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil { suite.FailNow(err.Error()) } @@ -121,11 +113,7 @@ func (suite *InteractionTestSuite) markInteractionsPending( // Put an interaction request // in the DB for this fave. - req, err := typeutils.StatusFaveToInteractionRequest(ctx, fave) - if err != nil { - suite.FailNow(err.Error()) - } - + req := typeutils.StatusFaveToInteractionRequest(fave) if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil { suite.FailNow(err.Error()) } diff --git a/internal/db/bundb/media.go b/internal/db/bundb/media.go index 453ad856a..09c8188f0 100644 --- a/internal/db/bundb/media.go +++ b/internal/db/bundb/media.go @@ -104,12 +104,6 @@ func (m *mediaDB) PutAttachment(ctx context.Context, media *gtsmodel.MediaAttach } func (m *mediaDB) UpdateAttachment(ctx context.Context, media *gtsmodel.MediaAttachment, columns ...string) error { - media.UpdatedAt = time.Now() - if len(columns) > 0 { - // If we're updating by column, ensure "updated_at" is included. - columns = append(columns, "updated_at") - } - return m.state.Caches.DB.Media.Store(media, func() error { _, err := m.db.NewUpdate(). Model(media). diff --git a/internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go b/internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go index 82c2b4016..a3fb8675e 100644 --- a/internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go +++ b/internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go @@ -93,11 +93,7 @@ func init() { // For each currently pending status, check whether it's a reply or // a boost, and insert a corresponding interaction request into the db. for _, pendingStatus := range pendingStatuses { - req, err := typeutils.StatusToInteractionRequest(ctx, pendingStatus) - if err != nil { - return err - } - + req := typeutils.StatusToInteractionRequest(pendingStatus) if _, err := tx. NewInsert(). Model(req). @@ -125,10 +121,7 @@ func init() { } for _, pendingFave := range pendingFaves { - req, err := typeutils.StatusFaveToInteractionRequest(ctx, pendingFave) - if err != nil { - return err - } + req := typeutils.StatusFaveToInteractionRequest(pendingFave) if _, err := tx. NewInsert(). diff --git a/internal/db/bundb/migrations/20240924222938_add_instance_custom_css.go b/internal/db/bundb/migrations/20240924222938_add_instance_custom_css.go new file mode 100644 index 000000000..14231927a --- /dev/null +++ b/internal/db/bundb/migrations/20240924222938_add_instance_custom_css.go @@ -0,0 +1,44 @@ +// 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 migrations + +import ( + "context" + "strings" + + "github.com/uptrace/bun" +) + +func init() { + up := func(ctx context.Context, db *bun.DB) error { + _, err := db.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? TEXT", bun.Ident("instances"), bun.Ident("custom_css")) + if err != nil && !(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) { + return err + } + return nil + } + + down := func(ctx context.Context, db *bun.DB) error { + _, err := db.ExecContext(ctx, "ALTER TABLE ? DROP COLUMN ?", bun.Ident("instances"), bun.Ident("custom_css")) + return err + } + + if err := Migrations.Register(up, down); err != nil { + panic(err) + } +} diff --git a/internal/db/bundb/migrations/20241113151042_remove_mention_updated_at.go b/internal/db/bundb/migrations/20241113151042_remove_mention_updated_at.go new file mode 100644 index 000000000..ba6e0bd3a --- /dev/null +++ b/internal/db/bundb/migrations/20241113151042_remove_mention_updated_at.go @@ -0,0 +1,59 @@ +// 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 migrations + +import ( + "context" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/uptrace/bun" +) + +func init() { + up := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + + // Check for 'updated_at' column on mentions table, else return. + exists, err := doesColumnExist(ctx, tx, "mentions", "updated_at") + if err != nil { + return err + } else if !exists { + return nil + } + + // Remove 'updated_at' column. + log.Info(ctx, "removing unused updated_at column from mentions to save space, please wait...") + _, err = tx.NewDropColumn(). + Model((*gtsmodel.Mention)(nil)). + Column("updated_at"). + Exec(ctx) + return err + }) + } + + down := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + return nil + }) + } + + if err := Migrations.Register(up, down); err != nil { + panic(err) + } +} diff --git a/internal/db/bundb/migrations/20241113152126_add_status_edits.go b/internal/db/bundb/migrations/20241113152126_add_status_edits.go new file mode 100644 index 000000000..5d4fb7b3e --- /dev/null +++ b/internal/db/bundb/migrations/20241113152126_add_status_edits.go @@ -0,0 +1,69 @@ +// 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 migrations + +import ( + "context" + "reflect" + + gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241113152126_add_status_edits" + "github.com/superseriousbusiness/gotosocial/internal/log" + + "github.com/uptrace/bun" +) + +func init() { + up := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + statusType := reflect.TypeOf((*gtsmodel.Status)(nil)) + + // Generate new Status.EditIDs column definition from bun. + colDef, err := getBunColumnDef(tx, statusType, "EditIDs") + if err != nil { + return err + } + + // Add EditIDs column to Status table. + log.Info(ctx, "adding edits column to statuses table...") + _, err = tx.NewAddColumn(). + Model((*gtsmodel.Status)(nil)). + ColumnExpr(colDef). + Exec(ctx) + if err != nil { + return err + } + + // Create the main StatusEdits table. + _, err = tx.NewCreateTable(). + IfNotExists(). + Model((*gtsmodel.StatusEdit)(nil)). + Exec(ctx) + return err + }) + } + + down := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + return nil + }) + } + + if err := Migrations.Register(up, down); err != nil { + panic(err) + } +} diff --git a/internal/db/bundb/migrations/20241113152126_add_status_edits/status.go b/internal/db/bundb/migrations/20241113152126_add_status_edits/status.go new file mode 100644 index 000000000..1b7d93f70 --- /dev/null +++ b/internal/db/bundb/migrations/20241113152126_add_status_edits/status.go @@ -0,0 +1,97 @@ +// 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 gtsmodel + +import ( + "time" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// Status represents a user-created 'post' or 'status' in the database, either remote or local +type Status struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created + UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated + FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched. + PinnedAt time.Time `bun:"type:timestamptz,nullzero"` // Status was pinned by owning account at this time. + URI string `bun:",unique,nullzero,notnull"` // activitypub URI of this status + URL string `bun:",nullzero"` // web url for viewing this status + Content string `bun:""` // content of this status; likely html-formatted but not guaranteed + AttachmentIDs []string `bun:"attachments,array"` // Database IDs of any media attachments associated with this status + Attachments []*gtsmodel.MediaAttachment `bun:"attached_media,rel:has-many"` // Attachments corresponding to attachmentIDs + TagIDs []string `bun:"tags,array"` // Database IDs of any tags used in this status + Tags []*gtsmodel.Tag `bun:"attached_tags,m2m:status_to_tags"` // Tags corresponding to tagIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation + MentionIDs []string `bun:"mentions,array"` // Database IDs of any mentions in this status + Mentions []*gtsmodel.Mention `bun:"attached_mentions,rel:has-many"` // Mentions corresponding to mentionIDs + EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this status + Emojis []*gtsmodel.Emoji `bun:"attached_emojis,m2m:status_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation + Local *bool `bun:",nullzero,notnull,default:false"` // is this status from a local account? + AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // which account posted this status? + Account *gtsmodel.Account `bun:"rel:belongs-to"` // account corresponding to accountID + AccountURI string `bun:",nullzero,notnull"` // activitypub uri of the owner of this status + InReplyToID string `bun:"type:CHAR(26),nullzero"` // id of the status this status replies to + InReplyToURI string `bun:",nullzero"` // activitypub uri of the status this status is a reply to + InReplyToAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that this status replies to + InReplyTo *Status `bun:"-"` // status corresponding to inReplyToID + InReplyToAccount *gtsmodel.Account `bun:"rel:belongs-to"` // account corresponding to inReplyToAccountID + BoostOfID string `bun:"type:CHAR(26),nullzero"` // id of the status this status is a boost of + BoostOfURI string `bun:"-"` // URI of the status this status is a boost of; field not inserted in the db, just for dereferencing purposes. + BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that owns the boosted status + BoostOf *Status `bun:"-"` // status that corresponds to boostOfID + BoostOfAccount *gtsmodel.Account `bun:"rel:belongs-to"` // account that corresponds to boostOfAccountID + ThreadID string `bun:"type:CHAR(26),nullzero"` // id of the thread to which this status belongs; only set for remote statuses if a local account is involved at some point in the thread, otherwise null + EditIDs []string `bun:"edits,array"` // + Edits []*StatusEdit `bun:"-"` // + PollID string `bun:"type:CHAR(26),nullzero"` // + Poll *gtsmodel.Poll `bun:"-"` // + ContentWarning string `bun:",nullzero"` // cw string for this status + Visibility Visibility `bun:",nullzero,notnull"` // visibility entry for this status + Sensitive *bool `bun:",nullzero,notnull,default:false"` // mark the status as sensitive? + Language string `bun:",nullzero"` // what language is this status written in? + CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"` // Which application was used to create this status? + CreatedWithApplication *gtsmodel.Application `bun:"rel:belongs-to"` // application corresponding to createdWithApplicationID + ActivityStreamsType string `bun:",nullzero,notnull"` // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!. + Text string `bun:""` // Original text of the status without formatting + Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s) + InteractionPolicy *gtsmodel.InteractionPolicy `bun:""` // InteractionPolicy for this status. If null then the default InteractionPolicy should be assumed for this status's Visibility. Always null for boost wrappers. + PendingApproval *bool `bun:",nullzero,notnull,default:false"` // If true then status is a reply or boost wrapper that must be Approved by the reply-ee or boost-ee before being fully distributed. + PreApproved bool `bun:"-"` // If true, then status is a reply to or boost wrapper of a status on our instance, has permission to do the interaction, and an Accept should be sent out for it immediately. Field not stored in the DB. + ApprovedByURI string `bun:",nullzero"` // URI of an Accept Activity that approves the Announce or Create Activity that this status was/will be attached to. +} + +// Visibility represents the visibility granularity of a status. +type Visibility string + +const ( + // VisibilityNone means nobody can see this. + // It's only used for web status visibility. + VisibilityNone Visibility = "none" + // VisibilityPublic means this status will be visible to everyone on all timelines. + VisibilityPublic Visibility = "public" + // VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists. + VisibilityUnlocked Visibility = "unlocked" + // VisibilityFollowersOnly means this status is viewable to followers only. + VisibilityFollowersOnly Visibility = "followers_only" + // VisibilityMutualsOnly means this status is visible to mutual followers only. + VisibilityMutualsOnly Visibility = "mutuals_only" + // VisibilityDirect means this status is visible only to mentioned recipients. + VisibilityDirect Visibility = "direct" + // VisibilityDefault is used when no other setting can be found. + VisibilityDefault Visibility = VisibilityUnlocked +) diff --git a/internal/db/bundb/migrations/20241113152126_add_status_edits/statusedit.go b/internal/db/bundb/migrations/20241113152126_add_status_edits/statusedit.go new file mode 100644 index 000000000..b27c3b343 --- /dev/null +++ b/internal/db/bundb/migrations/20241113152126_add_status_edits/statusedit.go @@ -0,0 +1,48 @@ +// 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 gtsmodel + +import ( + "time" +) + +// StatusEdit represents a **historical** view of a Status +// after a received edit. The Status itself will always +// contain the latest up-to-date information. +// +// Note that stored status edits may not exactly match that +// of the origin server, they are a best-effort by receiver +// to store version history. There is no AP history endpoint. +type StatusEdit struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database. + Content string `bun:""` // Content of status at time of edit; likely html-formatted but not guaranteed. + ContentWarning string `bun:",nullzero"` // Content warning of status at time of edit. + Text string `bun:""` // Original status text, without formatting, at time of edit. + Language string `bun:",nullzero"` // Status language at time of edit. + Sensitive *bool `bun:",nullzero,notnull,default:false"` // Status sensitive flag at time of edit. + AttachmentIDs []string `bun:"attachments,array"` // Database IDs of media attachments associated with status at time of edit. + AttachmentDescriptions []string `bun:",array"` // Previous media descriptions of media attachments associated with status at time of edit. + PollOptions []string `bun:",array"` // Poll options of status at time of edit, only set if status contains a poll. + PollVotes []int `bun:",array"` // Poll vote count at time of status edit, only set if poll votes were reset. + StatusID string `bun:"type:CHAR(26),nullzero,notnull"` // The originating status ID this is a historical edit of. + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // The creation time of this version of the status content (according to receiving server). + + // We don't bother having a *gtsmodel.Status model here + // as the StatusEdit is always just attached to a Status, + // so it doesn't need a self-reference back to it. +} diff --git a/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints.go b/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints.go index b6328c6b6..5f3eb1409 100644 --- a/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints.go +++ b/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints.go @@ -19,10 +19,8 @@ import ( "context" - "errors" old_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" new_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/util" @@ -71,7 +69,9 @@ func init() { // Before making changes to the visibility col // we must drop all indices that rely on it. + log.Info(ctx, "dropping old visibility indexes...") for _, index := range visIndices { + log.Info(ctx, "dropping old index %s...", index.name) if _, err := tx.NewDropIndex(). Index(index.name). Exec(ctx); err != nil { @@ -91,7 +91,9 @@ func init() { } // Recreate the visibility indices. + log.Info(ctx, "creating new visibility indexes...") for _, index := range visIndices { + log.Info(ctx, "creating new index %s...", index.name) q := tx.NewCreateIndex(). Table("statuses"). Index(index.name). @@ -128,97 +130,6 @@ func init() { } } -// convertEnums performs a transaction that converts -// a table's column of our old-style enums (strings) to -// more performant and space-saving integer types. -func convertEnums[OldType ~string, NewType ~int16]( - ctx context.Context, - tx bun.Tx, - table string, - column string, - mapping map[OldType]NewType, - defaultValue *NewType, -) error { - if len(mapping) == 0 { - return errors.New("empty mapping") - } - - // Generate new column name. - newColumn := column + "_new" - - log.Infof(ctx, "converting %s.%s enums; "+ - "this may take a while, please don't interrupt!", - table, column, - ) - - // Ensure a default value. - if defaultValue == nil { - var zero NewType - defaultValue = &zero - } - - // Add new column to database. - if _, err := tx.NewAddColumn(). - Table(table). - ColumnExpr("? SMALLINT NOT NULL DEFAULT ?", - bun.Ident(newColumn), - *defaultValue). - Exec(ctx); err != nil { - return gtserror.Newf("error adding new column: %w", err) - } - - // Get a count of all in table. - total, err := tx.NewSelect(). - Table(table). - Count(ctx) - if err != nil { - return gtserror.Newf("error selecting total count: %w", err) - } - - var updated int - for old, new := range mapping { - - // Update old to new values. - res, err := tx.NewUpdate(). - Table(table). - Where("? = ?", bun.Ident(column), old). - Set("? = ?", bun.Ident(newColumn), new). - Exec(ctx) - if err != nil { - return gtserror.Newf("error updating old column values: %w", err) - } - - // Count number items updated. - n, _ := res.RowsAffected() - updated += int(n) - } - - // Check total updated. - if total != updated { - log.Warnf(ctx, "total=%d does not match updated=%d", total, updated) - } - - // Drop the old column from table. - if _, err := tx.NewDropColumn(). - Table(table). - ColumnExpr("?", bun.Ident(column)). - Exec(ctx); err != nil { - return gtserror.Newf("error dropping old column: %w", err) - } - - // Rename new to old name. - if _, err := tx.NewRaw( - "ALTER TABLE ? RENAME COLUMN ? TO ?", - bun.Ident(table), - bun.Ident(newColumn), - bun.Ident(column), - ).Exec(ctx); err != nil { - return gtserror.Newf("error renaming new column: %w", err) - } - - return nil -} - // visibilityEnumMapping maps old Visibility enum values to their newer integer type. func visibilityEnumMapping[T ~string]() map[T]new_gtsmodel.Visibility { return map[T]new_gtsmodel.Visibility{ diff --git a/internal/db/bundb/migrations/20241203124608_remove_media_attachment_updated_at.go b/internal/db/bundb/migrations/20241203124608_remove_media_attachment_updated_at.go new file mode 100644 index 000000000..63e5d5f90 --- /dev/null +++ b/internal/db/bundb/migrations/20241203124608_remove_media_attachment_updated_at.go @@ -0,0 +1,59 @@ +// 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 migrations + +import ( + "context" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/uptrace/bun" +) + +func init() { + up := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + + // Check for 'updated_at' column on media attachments table, else return. + exists, err := doesColumnExist(ctx, tx, "media_attachments", "updated_at") + if err != nil { + return err + } else if !exists { + return nil + } + + // Remove 'updated_at' column. + log.Info(ctx, "removing unused updated_at column from media attachments to save space, please wait...") + _, err = tx.NewDropColumn(). + Model((*gtsmodel.MediaAttachment)(nil)). + Column("updated_at"). + Exec(ctx) + return err + }) + } + + down := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + return nil + }) + } + + if err := Migrations.Register(up, down); err != nil { + panic(err) + } +} diff --git a/internal/db/bundb/migrations/util.go b/internal/db/bundb/migrations/util.go index 47de09e23..edf7c1d05 100644 --- a/internal/db/bundb/migrations/util.go +++ b/internal/db/bundb/migrations/util.go @@ -19,11 +19,209 @@ import ( "context" + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "codeberg.org/gruf/go-byteutil" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect" + "github.com/uptrace/bun/dialect/feature" + "github.com/uptrace/bun/dialect/sqltype" + "github.com/uptrace/bun/schema" ) +// convertEnums performs a transaction that converts +// a table's column of our old-style enums (strings) to +// more performant and space-saving integer types. +func convertEnums[OldType ~string, NewType ~int16]( + ctx context.Context, + tx bun.Tx, + table string, + column string, + mapping map[OldType]NewType, + defaultValue *NewType, +) error { + if len(mapping) == 0 { + return errors.New("empty mapping") + } + + // Generate new column name. + newColumn := column + "_new" + + log.Infof(ctx, "converting %s.%s enums; "+ + "this may take a while, please don't interrupt!", + table, column, + ) + + // Ensure a default value. + if defaultValue == nil { + var zero NewType + defaultValue = &zero + } + + // Add new column to database. + if _, err := tx.NewAddColumn(). + Table(table). + ColumnExpr("? SMALLINT NOT NULL DEFAULT ?", + bun.Ident(newColumn), + *defaultValue). + Exec(ctx); err != nil { + return gtserror.Newf("error adding new column: %w", err) + } + + // Get a count of all in table. + total, err := tx.NewSelect(). + Table(table). + Count(ctx) + if err != nil { + return gtserror.Newf("error selecting total count: %w", err) + } + + var updated int + for old, new := range mapping { + + // Update old to new values. + res, err := tx.NewUpdate(). + Table(table). + Where("? = ?", bun.Ident(column), old). + Set("? = ?", bun.Ident(newColumn), new). + Exec(ctx) + if err != nil { + return gtserror.Newf("error updating old column values: %w", err) + } + + // Count number items updated. + n, _ := res.RowsAffected() + updated += int(n) + } + + // Check total updated. + if total != updated { + log.Warnf(ctx, "total=%d does not match updated=%d", total, updated) + } + + // Drop the old column from table. + if _, err := tx.NewDropColumn(). + Table(table). + ColumnExpr("?", bun.Ident(column)). + Exec(ctx); err != nil { + return gtserror.Newf("error dropping old column: %w", err) + } + + // Rename new to old name. + if _, err := tx.NewRaw( + "ALTER TABLE ? RENAME COLUMN ? TO ?", + bun.Ident(table), + bun.Ident(newColumn), + bun.Ident(column), + ).Exec(ctx); err != nil { + return gtserror.Newf("error renaming new column: %w", err) + } + + return nil +} + +// getBunColumnDef generates a column definition string for the SQL table represented by +// Go type, with the SQL column represented by the given Go field name. This ensures when +// adding a new column for table by migration that it will end up as bun would create it. +// +// NOTE: this function must stay in sync with (*bun.CreateTableQuery{}).AppendQuery(), +// specifically where it loops over table fields appending each column definition. +func getBunColumnDef(db bun.IDB, rtype reflect.Type, fieldName string) (string, error) { + d := db.Dialect() + f := d.Features() + + // Get bun schema definitions for Go type and its field. + field, table, err := getModelField(db, rtype, fieldName) + if err != nil { + return "", err + } + + // Start with reasonable buf. + buf := make([]byte, 0, 64) + + // Start with the SQL column name. + buf = append(buf, field.SQLName...) + buf = append(buf, " "...) + + // Append the SQL + // type information. + switch { + + // Most of the time these two will match, but for the cases where DiscoveredSQLType is dialect-specific, + // e.g. pgdialect would change sqltype.SmallInt to pgTypeSmallSerial for columns that have `bun:",autoincrement"` + case !strings.EqualFold(field.CreateTableSQLType, field.DiscoveredSQLType): + buf = append(buf, field.CreateTableSQLType...) + + // For all common SQL types except VARCHAR, both UserDefinedSQLType and DiscoveredSQLType specify the correct type, + // and we needn't modify it. For VARCHAR columns, we will stop to check if a valid length has been set in .Varchar(int). + case !strings.EqualFold(field.CreateTableSQLType, sqltype.VarChar): + buf = append(buf, field.CreateTableSQLType...) + + // All else falls back + // to a default varchar. + default: + if d.Name() == dialect.Oracle { + buf = append(buf, "VARCHAR2"...) + } else { + buf = append(buf, sqltype.VarChar...) + } + buf = append(buf, "("...) + buf = strconv.AppendInt(buf, int64(d.DefaultVarcharLen()), 10) + buf = append(buf, ")"...) + } + + // Append not null definition if field requires. + if field.NotNull && d.Name() != dialect.Oracle { + buf = append(buf, " NOT NULL"...) + } + + // Append autoincrement definition if field requires. + if field.Identity && f.Has(feature.GeneratedIdentity) || + (field.AutoIncrement && (f.Has(feature.AutoIncrement) || f.Has(feature.Identity))) { + buf = d.AppendSequence(buf, table, field) + } + + // Append any default value. + if field.SQLDefault != "" { + buf = append(buf, " DEFAULT "...) + buf = append(buf, field.SQLDefault...) + } + + return byteutil.B2S(buf), nil +} + +// getModelField returns the uptrace/bun schema details for given Go type and field name. +func getModelField(db bun.IDB, rtype reflect.Type, fieldName string) (*schema.Field, *schema.Table, error) { + + // Get the associated table for Go type. + table := db.Dialect().Tables().Get(rtype) + if table == nil { + return nil, nil, fmt.Errorf("no table found for type: %s", rtype) + } + + var field *schema.Field + + // Look for field matching Go name. + for i := range table.Fields { + if table.Fields[i].GoName == fieldName { + field = table.Fields[i] + break + } + } + + if field == nil { + return nil, nil, fmt.Errorf("no bun field found on %s with name: %s", rtype, fieldName) + } + + return field, table, nil +} + // doesColumnExist safely checks whether given column exists on table, handling both SQLite and PostgreSQL appropriately. func doesColumnExist(ctx context.Context, tx bun.Tx, table, col string) (bool, error) { var n int diff --git a/internal/db/bundb/poll.go b/internal/db/bundb/poll.go index b9384774b..e8c3e7e54 100644 --- a/internal/db/bundb/poll.go +++ b/internal/db/bundb/poll.go @@ -88,12 +88,15 @@ func (p *pollDB) getPoll(ctx context.Context, lookup string, dbQuery func(*gtsmo func (p *pollDB) GetOpenPolls(ctx context.Context) ([]*gtsmodel.Poll, error) { var pollIDs []string - // Select all polls with unset `closed_at` time. + // Select all polls with: + // - UNSET `closed_at` + // - SET `expires_at` if err := p.db.NewSelect(). Table("polls"). Column("polls.id"). Join("JOIN ? ON ? = ?", bun.Ident("statuses"), bun.Ident("polls.id"), bun.Ident("statuses.poll_id")). Where("? = true", bun.Ident("statuses.local")). + Where("? IS NOT NULL", bun.Ident("polls.expires_at")). Where("? IS NULL", bun.Ident("polls.closed_at")). Scan(ctx, &pollIDs); err != nil { return nil, err diff --git a/internal/db/bundb/status.go b/internal/db/bundb/status.go index 45e9864a3..fea5594dd 100644 --- a/internal/db/bundb/status.go +++ b/internal/db/bundb/status.go @@ -21,7 +21,6 @@ "context" "errors" "slices" - "time" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtscontext" @@ -181,7 +180,7 @@ func (s *statusDB) getStatus(ctx context.Context, lookup string, dbQuery func(*g func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status) error { var ( err error - errs = gtserror.NewMultiError(9) + errs gtserror.MultiError ) if status.Account == nil { @@ -257,7 +256,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status) if !status.AttachmentsPopulated() { // Status attachments are out-of-date with IDs, repopulate. status.Attachments, err = s.state.DB.GetAttachmentsByIDs( - ctx, // these are already barebones + gtscontext.SetBarebones(ctx), status.AttachmentIDs, ) if err != nil { @@ -268,7 +267,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status) if !status.TagsPopulated() { // Status tags are out-of-date with IDs, repopulate. status.Tags, err = s.state.DB.GetTags( - ctx, + gtscontext.SetBarebones(ctx), status.TagIDs, ) if err != nil { @@ -279,7 +278,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status) if !status.MentionsPopulated() { // Status mentions are out-of-date with IDs, repopulate. status.Mentions, err = s.state.DB.GetMentions( - ctx, // leave fully populated for now + ctx, // TODO: manually populate mentions for places expecting these populated status.MentionIDs, ) if err != nil { @@ -290,7 +289,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status) if !status.EmojisPopulated() { // Status emojis are out-of-date with IDs, repopulate. status.Emojis, err = s.state.DB.GetEmojisByIDs( - ctx, // these are already barebones + gtscontext.SetBarebones(ctx), status.EmojiIDs, ) if err != nil { @@ -301,7 +300,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status) if status.CreatedWithApplicationID != "" && status.CreatedWithApplication == nil { // Populate the status' expected CreatedWithApplication (not always set). status.CreatedWithApplication, err = s.state.DB.GetApplicationByID( - ctx, // these are already barebones + gtscontext.SetBarebones(ctx), status.CreatedWithApplicationID, ) if err != nil { @@ -312,6 +311,23 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status) return errs.Combine() } +func (s *statusDB) PopulateStatusEdits(ctx context.Context, status *gtsmodel.Status) error { + var err error + + if !status.EditsPopulated() { + // Status edits are out-of-date with IDs, repopulate. + status.Edits, err = s.state.DB.GetStatusEditsByIDs( + gtscontext.SetBarebones(ctx), + status.EditIDs, + ) + if err != nil { + return gtserror.Newf("error populating status edits: %w", err) + } + } + + return nil +} + func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) error { return s.state.Caches.DB.Status.Store(status, func() error { // It is safe to run this database transaction within cache.Store @@ -350,14 +366,14 @@ func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) error } } - // change the status ID of the media attachments to the new status + // change the status ID of the media + // attachments to the current status for _, a := range status.Attachments { a.StatusID = status.ID - a.UpdatedAt = time.Now() if _, err := tx. NewUpdate(). Model(a). - Column("status_id", "updated_at"). + Column("status_id"). Where("? = ?", bun.Ident("media_attachment.id"), a.ID). Exec(ctx); err != nil { if !errors.Is(err, db.ErrAlreadyExists) { @@ -384,19 +400,15 @@ func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) error } // Finally, insert the status - _, err := tx.NewInsert().Model(status).Exec(ctx) + _, err := tx.NewInsert(). + Model(status). + Exec(ctx) return err }) }) } func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, columns ...string) error { - status.UpdatedAt = time.Now() - if len(columns) > 0 { - // If we're updating by column, ensure "updated_at" is included. - columns = append(columns, "updated_at") - } - return s.state.Caches.DB.Status.Store(status, func() error { // It is safe to run this database transaction within cache.Store // as the cache does not attempt a mutex lock until AFTER hook. @@ -434,13 +446,14 @@ func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, co } } - // change the status ID of the media attachments to the new status + // change the status ID of the media + // attachments to the current status. for _, a := range status.Attachments { a.StatusID = status.ID - a.UpdatedAt = time.Now() if _, err := tx. NewUpdate(). Model(a). + Column("status_id"). Where("? = ?", bun.Ident("media_attachment.id"), a.ID). Exec(ctx); err != nil { if !errors.Is(err, db.ErrAlreadyExists) { @@ -467,8 +480,7 @@ func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, co } // Finally, update the status - _, err := tx. - NewUpdate(). + _, err := tx.NewUpdate(). Model(status). Column(columns...). Where("? = ?", bun.Ident("status.id"), status.ID). diff --git a/internal/db/bundb/statusedit.go b/internal/db/bundb/statusedit.go new file mode 100644 index 000000000..c932968fd --- /dev/null +++ b/internal/db/bundb/statusedit.go @@ -0,0 +1,198 @@ +// 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 bundb + +import ( + "context" + "errors" + "slices" + + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/state" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" + "github.com/uptrace/bun" +) + +type statusEditDB struct { + db *bun.DB + state *state.State +} + +func (s *statusEditDB) GetStatusEditByID(ctx context.Context, id string) (*gtsmodel.StatusEdit, error) { + // Fetch edit from database cache with loader callback. + edit, err := s.state.Caches.DB.StatusEdit.LoadOne("ID", + func() (*gtsmodel.StatusEdit, error) { + var edit gtsmodel.StatusEdit + + // Not cached, load edit + // from database by its ID. + if err := s.db.NewSelect(). + Model(&edit). + Where("? = ?", bun.Ident("id"), id). + Scan(ctx); err != nil { + return nil, err + } + + return &edit, nil + }, id, + ) + if err != nil { + return nil, err + } + + if gtscontext.Barebones(ctx) { + // no need to fully populate. + return edit, nil + } + + // Further populate the edit fields where applicable. + if err := s.PopulateStatusEdit(ctx, edit); err != nil { + return nil, err + } + + return edit, nil +} + +func (s *statusEditDB) GetStatusEditsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.StatusEdit, error) { + // Load status edits for IDs via cache loader callbacks. + edits, err := s.state.Caches.DB.StatusEdit.LoadIDs("ID", + ids, + func(uncached []string) ([]*gtsmodel.StatusEdit, error) { + // Preallocate expected length of uncached edits. + edits := make([]*gtsmodel.StatusEdit, 0, len(uncached)) + + // Perform database query scanning + // the remaining (uncached) edit IDs. + if err := s.db.NewSelect(). + Model(&edits). + Where("? IN (?)", bun.Ident("id"), bun.In(uncached)). + Scan(ctx); err != nil { + return nil, err + } + + return edits, nil + }, + ) + if err != nil { + return nil, err + } + + // Reorder the edits by their + // IDs to ensure in correct order. + getID := func(e *gtsmodel.StatusEdit) string { return e.ID } + xslices.OrderBy(edits, ids, getID) + + if gtscontext.Barebones(ctx) { + // no need to fully populate. + return edits, nil + } + + // Populate all loaded edits, removing those we fail to + // populate (removes needing so many nil checks everywhere). + edits = slices.DeleteFunc(edits, func(edit *gtsmodel.StatusEdit) bool { + if err := s.PopulateStatusEdit(ctx, edit); err != nil { + log.Errorf(ctx, "error populating edit %s: %v", edit.ID, err) + return true + } + return false + }) + + return edits, nil +} + +func (s *statusEditDB) PopulateStatusEdit(ctx context.Context, edit *gtsmodel.StatusEdit) error { + var err error + var errs gtserror.MultiError + + // For sub-models we only want + // barebones versions of them. + ctx = gtscontext.SetBarebones(ctx) + + if !edit.AttachmentsPopulated() { + // Fetch all attachments for status edit's IDs. + edit.Attachments, err = s.state.DB.GetAttachmentsByIDs( + ctx, + edit.AttachmentIDs, + ) + if err != nil { + errs.Appendf("error populating edit attachments: %w", err) + } + } + + return errs.Combine() +} + +func (s *statusEditDB) PutStatusEdit(ctx context.Context, edit *gtsmodel.StatusEdit) error { + return s.state.Caches.DB.StatusEdit.Store(edit, func() error { + _, err := s.db.NewInsert().Model(edit).Exec(ctx) + return err + }) +} + +func (s *statusEditDB) DeleteStatusEdits(ctx context.Context, ids []string) error { + // Gather necessary fields from + // deleted for cache invalidation. + deleted := make([]*gtsmodel.StatusEdit, 0, len(ids)) + + // Delete all edits with IDs pertaining + // to given slice, returning status IDs. + if _, err := s.db.NewDelete(). + Model(&deleted). + Where("? IN (?)", bun.Ident("id"), bun.In(ids)). + Returning("?", bun.Ident("status_id")). + Exec(ctx); err != nil && + !errors.Is(err, db.ErrNoEntries) { + return err + } + + // Check for no deletes. + if len(deleted) == 0 { + return nil + } + + // Invalidate all the cached status edits with IDs. + s.state.Caches.DB.StatusEdit.InvalidateIDs("ID", ids) + + // With each invalidate hook mark status ID of + // edit we just called for. We only want to call + // invalidate hooks of edits from unique statuses. + invalidated := make(map[string]struct{}, 1) + + // Invalidate the first delete manually, this + // opt negates need for initial hashmap lookup. + s.state.Caches.OnInvalidateStatusEdit(deleted[0]) + invalidated[deleted[0].StatusID] = struct{}{} + + for _, edit := range deleted { + // Check not already called for status. + _, ok := invalidated[edit.StatusID] + if ok { + continue + } + + // Manually call status edit invalidate hook. + s.state.Caches.OnInvalidateStatusEdit(edit) + invalidated[edit.StatusID] = struct{}{} + } + + return nil +} diff --git a/internal/db/bundb/statusedit_test.go b/internal/db/bundb/statusedit_test.go new file mode 100644 index 000000000..b6a15e825 --- /dev/null +++ b/internal/db/bundb/statusedit_test.go @@ -0,0 +1,168 @@ +// 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 bundb_test + +import ( + "context" + "errors" + "reflect" + "slices" + "testing" + "time" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type StatusEditTestSuite struct { + BunDBStandardTestSuite +} + +func (suite *StatusEditTestSuite) TestGetStatusEditBy() { + t := suite.T() + + // Create a new context for this test. + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + // Sentinel error to mark avoiding a test case. + sentinelErr := errors.New("sentinel") + + for _, edit := range suite.testStatusEdits { + for lookup, dbfunc := range map[string]func() (*gtsmodel.StatusEdit, error){ + "id": func() (*gtsmodel.StatusEdit, error) { + return suite.db.GetStatusEditByID(ctx, edit.ID) + }, + } { + // Clear database caches. + suite.state.Caches.Init() + + t.Logf("checking database lookup %q", lookup) + + // Perform database function. + checkEdit, err := dbfunc() + if err != nil { + if err == sentinelErr { + continue + } + + t.Errorf("error encountered for database lookup %q: %v", lookup, err) + continue + } + + // Check received account data. + if !areEditsEqual(edit, checkEdit) { + t.Errorf("edit does not contain expected data: %+v", checkEdit) + continue + } + } + } +} + +func (suite *StatusEditTestSuite) TestGetStatusEditsByIDs() { + t := suite.T() + + // Create a new context for this test. + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + // editsByStatus returns all test edits by the given status with ID. + editsByStatus := func(status *gtsmodel.Status) []*gtsmodel.StatusEdit { + var edits []*gtsmodel.StatusEdit + for _, edit := range suite.testStatusEdits { + if edit.StatusID == status.ID { + edits = append(edits, edit) + } + } + return edits + } + + for _, status := range suite.testStatuses { + // Get test status edit models + // that should be found for status. + check := editsByStatus(status) + + // Fetch edits for the slice of IDs attached to status from database. + edits, err := suite.state.DB.GetStatusEditsByIDs(ctx, status.EditIDs) + suite.NoError(err) + + // Ensure both slices + // sorted the same. + sortEdits(check) + sortEdits(edits) + + // Check whether slices of status edits match. + if !slices.EqualFunc(check, edits, areEditsEqual) { + t.Error("status edit slices do not match") + } + } +} + +func (suite *StatusEditTestSuite) TestDeleteStatusEdits() { + // Create a new context for this test. + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + for _, status := range suite.testStatuses { + // Delete all edits for status with given IDs from database. + err := suite.state.DB.DeleteStatusEdits(ctx, status.EditIDs) + suite.NoError(err) + + // Now attempt to fetch these edits from database, should be empty. + edits, err := suite.state.DB.GetStatusEditsByIDs(ctx, status.EditIDs) + suite.NoError(err) + suite.Empty(edits) + } +} + +func TestStatusEditTestSuite(t *testing.T) { + suite.Run(t, new(StatusEditTestSuite)) +} + +func areEditsEqual(e1, e2 *gtsmodel.StatusEdit) bool { + // Clone the 1st status edit. + e1Copy := new(gtsmodel.StatusEdit) + *e1Copy = *e1 + e1 = e1Copy + + // Clone the 2nd status edit. + e2Copy := new(gtsmodel.StatusEdit) + *e2Copy = *e2 + e2 = e2Copy + + // Clear populated sub-models. + e1.Attachments = nil + e2.Attachments = nil + + // Clear database-set fields. + e1.CreatedAt = time.Time{} + e2.CreatedAt = time.Time{} + + return reflect.DeepEqual(*e1, *e2) +} + +func sortEdits(edits []*gtsmodel.StatusEdit) { + slices.SortFunc(edits, func(a, b *gtsmodel.StatusEdit) int { + if a.CreatedAt.Before(b.CreatedAt) { + return +1 + } else if b.CreatedAt.Before(a.CreatedAt) { + return -1 + } + return 0 + }) +} diff --git a/internal/db/bundb/timeline.go b/internal/db/bundb/timeline.go index bcb7953d4..fcea0178a 100644 --- a/internal/db/bundb/timeline.go +++ b/internal/db/bundb/timeline.go @@ -123,13 +123,8 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI if maxID == "" || maxID >= id.Highest { const future = 24 * time.Hour - var err error - // don't return statuses more than 24hr in the future - maxID, err = id.NewULIDFromTime(time.Now().Add(future)) - if err != nil { - return nil, err - } + maxID = id.NewULIDFromTime(time.Now().Add(future)) } // return only statuses LOWER (ie., older) than maxID @@ -223,13 +218,8 @@ func (t *timelineDB) GetPublicTimeline(ctx context.Context, maxID string, sinceI if maxID == "" || maxID >= id.Highest { const future = 24 * time.Hour - var err error - // don't return statuses more than 24hr in the future - maxID, err = id.NewULIDFromTime(time.Now().Add(future)) - if err != nil { - return nil, err - } + maxID = id.NewULIDFromTime(time.Now().Add(future)) } // return only statuses LOWER (ie., older) than maxID @@ -409,13 +399,8 @@ func (t *timelineDB) GetListTimeline( if maxID == "" || maxID >= id.Highest { const future = 24 * time.Hour - var err error - // don't return statuses more than 24hr in the future - maxID, err = id.NewULIDFromTime(time.Now().Add(future)) - if err != nil { - return nil, err - } + maxID = id.NewULIDFromTime(time.Now().Add(future)) } // return only statuses LOWER (ie., older) than maxID @@ -508,13 +493,8 @@ func (t *timelineDB) GetTagTimeline( if maxID == "" || maxID >= id.Highest { const future = 24 * time.Hour - var err error - // don't return statuses more than 24hr in the future - maxID, err = id.NewULIDFromTime(time.Now().Add(future)) - if err != nil { - return nil, err - } + maxID = id.NewULIDFromTime(time.Now().Add(future)) } // return only statuses LOWER (ie., older) than maxID diff --git a/internal/db/bundb/timeline_test.go b/internal/db/bundb/timeline_test.go index 00df2b3a6..75a335512 100644 --- a/internal/db/bundb/timeline_test.go +++ b/internal/db/bundb/timeline_test.go @@ -37,10 +37,7 @@ type TimelineTestSuite struct { func getFutureStatus() *gtsmodel.Status { theDistantFuture := time.Now().Add(876600 * time.Hour) - id, err := id.NewULIDFromTime(theDistantFuture) - if err != nil { - panic(err) - } + id := id.NewULIDFromTime(theDistantFuture) return >smodel.Status{ ID: id, @@ -182,7 +179,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineIgnoreExclusive() { if err != nil { suite.FailNow(err.Error()) } - suite.checkStatuses(s, id.Highest, id.Lowest, 8) + suite.checkStatuses(s, id.Highest, id.Lowest, 9) // Remove admin account from the exclusive list. listEntry := suite.testListEntries["local_account_1_list_1_entry_2"] @@ -196,7 +193,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineIgnoreExclusive() { if err != nil { suite.FailNow(err.Error()) } - suite.checkStatuses(s, id.Highest, id.Lowest, 12) + suite.checkStatuses(s, id.Highest, id.Lowest, 13) } func (suite *TimelineTestSuite) TestGetHomeTimelineNoFollowing() { @@ -228,7 +225,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineNoFollowing() { suite.FailNow(err.Error()) } - suite.checkStatuses(s, id.Highest, id.Lowest, 8) + suite.checkStatuses(s, id.Highest, id.Lowest, 9) } func (suite *TimelineTestSuite) TestGetHomeTimelineWithFutureStatus() { @@ -281,8 +278,8 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineFromHighest() { } suite.checkStatuses(s, id.Highest, id.Lowest, 5) - suite.Equal("01J2M1HPFSS54S60Y0KYV23KJE", s[0].ID) - suite.Equal("01G36SF3V6Y6V5BF9P4R7PQG7G", s[len(s)-1].ID) + suite.Equal("01JDPZEZ77X1NX0TY9M10BK1HM", s[0].ID) + suite.Equal("01HEN2RZ8BG29Y5Z9VJC73HZW7", s[len(s)-1].ID) } func (suite *TimelineTestSuite) TestGetListTimelineNoParams() { @@ -296,7 +293,7 @@ func (suite *TimelineTestSuite) TestGetListTimelineNoParams() { suite.FailNow(err.Error()) } - suite.checkStatuses(s, id.Highest, id.Lowest, 12) + suite.checkStatuses(s, id.Highest, id.Lowest, 13) } func (suite *TimelineTestSuite) TestGetListTimelineMaxID() { @@ -311,8 +308,8 @@ func (suite *TimelineTestSuite) TestGetListTimelineMaxID() { } suite.checkStatuses(s, id.Highest, id.Lowest, 5) - suite.Equal("01HEN2PRXT0TF4YDRA64FZZRN7", s[0].ID) - suite.Equal("01FF25D5Q0DH7CHD57CTRS6WK0", s[len(s)-1].ID) + suite.Equal("01JDPZEZ77X1NX0TY9M10BK1HM", s[0].ID) + suite.Equal("01FN3VJGFH10KR7S2PB0GFJZYG", s[len(s)-1].ID) } func (suite *TimelineTestSuite) TestGetListTimelineMinID() { diff --git a/internal/db/db.go b/internal/db/db.go index b7e2b29bd..16796ae49 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -51,6 +51,7 @@ type DB interface { SinBinStatus Status StatusBookmark + StatusEdit StatusFave Tag Thread diff --git a/internal/db/status.go b/internal/db/status.go index ade900728..6bf9653c8 100644 --- a/internal/db/status.go +++ b/internal/db/status.go @@ -41,8 +41,12 @@ type Status interface { GetStatusBoost(ctx context.Context, boostOfID string, byAccountID string) (*gtsmodel.Status, error) // PopulateStatus ensures that all sub-models of a status are populated (e.g. mentions, attachments, etc). + // Except for edits, to fetch these please call PopulateStatusEdits() . PopulateStatus(ctx context.Context, status *gtsmodel.Status) error + // PopulateStatusEdits ensures that status' edits are fully popualted. + PopulateStatusEdits(ctx context.Context, status *gtsmodel.Status) error + // PutStatus stores one status in the database. PutStatus(ctx context.Context, status *gtsmodel.Status) error diff --git a/internal/db/statusedit.go b/internal/db/statusedit.go new file mode 100644 index 000000000..32e770fb9 --- /dev/null +++ b/internal/db/statusedit.go @@ -0,0 +1,43 @@ +// 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 db + +import ( + "context" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type StatusEdit interface { + + // GetStatusEditByID fetches the StatusEdit with given ID from the database. + GetStatusEditByID(ctx context.Context, id string) (*gtsmodel.StatusEdit, error) + + // GetStatusEditsByIDs fetches all StatusEdits with given IDs from database, + // this is optimized and faster than multiple calls to GetStatusEditByID. + GetStatusEditsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.StatusEdit, error) + + // PopulateStatusEdit ensures the given StatusEdit's sub-models are populated. + PopulateStatusEdit(ctx context.Context, edit *gtsmodel.StatusEdit) error + + // PutStatusEdit inserts the given new StatusEdit into the database. + PutStatusEdit(ctx context.Context, edit *gtsmodel.StatusEdit) error + + // DeleteStatusEdits deletes the StatusEdits with given IDs from the database. + DeleteStatusEdits(ctx context.Context, ids []string) error +} diff --git a/internal/federation/dereferencing/announce.go b/internal/federation/dereferencing/announce.go index a3eaf199d..eb949f159 100644 --- a/internal/federation/dereferencing/announce.go +++ b/internal/federation/dereferencing/announce.go @@ -87,7 +87,7 @@ func (d *Dereferencer) EnrichAnnounce( boost.Federated = target.Federated // Ensure this Announce is permitted by the Announcee. - permit, err := d.isPermittedStatus(ctx, requestUser, nil, boost) + permit, err := d.isPermittedStatus(ctx, requestUser, nil, boost, true) if err != nil { return nil, gtserror.Newf("error checking permitted status %s: %w", boost.URI, err) } @@ -99,10 +99,7 @@ func (d *Dereferencer) EnrichAnnounce( } // Generate an ID for the boost wrapper status. - boost.ID, err = id.NewULIDFromTime(boost.CreatedAt) - if err != nil { - return nil, gtserror.Newf("error generating id: %w", err) - } + boost.ID = id.NewULIDFromTime(boost.CreatedAt) // Store the boost wrapper status in database. switch err = d.state.DB.PutStatus(ctx, boost); { diff --git a/internal/federation/dereferencing/dereferencer.go b/internal/federation/dereferencing/dereferencer.go index 3bff0d1a2..5e7b2b9c0 100644 --- a/internal/federation/dereferencing/dereferencer.go +++ b/internal/federation/dereferencing/dereferencer.go @@ -66,7 +66,7 @@ // causing loads of dereferencing calls. Fresh = util.Ptr(FreshnessWindow(5 * time.Minute)) - // 10 seconds. + // 5 seconds. // // Freshest is useful when you want an // immediately up to date model of something @@ -74,7 +74,7 @@ // // Be careful using this one; it can cause // lots of unnecessary traffic if used unwisely. - Freshest = util.Ptr(FreshnessWindow(10 * time.Second)) + Freshest = util.Ptr(FreshnessWindow(5 * time.Second)) ) // Dereferencer wraps logic and functionality for doing dereferencing diff --git a/internal/federation/dereferencing/media.go b/internal/federation/dereferencing/media.go index 3bed4b198..d22eeb237 100644 --- a/internal/federation/dereferencing/media.go +++ b/internal/federation/dereferencing/media.go @@ -128,6 +128,7 @@ func (d *Dereferencer) RefreshMedia( // Check emoji is up-to-date // with provided extra info. switch { + case force: case info.Blurhash != nil && *info.Blurhash != attach.Blurhash: attach.Blurhash = *info.Blurhash diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go index c90730826..223389ad7 100644 --- a/internal/federation/dereferencing/status.go +++ b/internal/federation/dereferencing/status.go @@ -35,6 +35,7 @@ "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" ) // statusFresh returns true if the given status is still @@ -302,6 +303,7 @@ func (d *Dereferencer) enrichStatusSafely( uri, status, statusable, + isNew, ) // Check for a returned HTTP code via error. @@ -374,6 +376,7 @@ func (d *Dereferencer) enrichStatus( uri *url.URL, status *gtsmodel.Status, statusable ap.Statusable, + isNew bool, ) ( *gtsmodel.Status, ap.Statusable, @@ -476,8 +479,7 @@ func (d *Dereferencer) enrichStatus( // Ensure the final parsed status URI or URL matches // the input URI we fetched (or received) it as. - matches, err := util.URIMatches( - uri, + matches, err := util.URIMatches(uri, append( ap.GetURL(statusable), // status URL(s) ap.GetJSONLDId(statusable), // status URI @@ -497,21 +499,18 @@ func (d *Dereferencer) enrichStatus( ) } - var isNew bool - - // Based on the original provided - // status model, determine whether - // this is a new insert / update. - if isNew = (status.ID == ""); isNew { + if isNew { // Generate new status ID from the provided creation date. - latestStatus.ID, err = id.NewULIDFromTime(latestStatus.CreatedAt) - if err != nil { - log.Errorf(ctx, "invalid created at date (falling back to 'now'): %v", err) - latestStatus.ID = id.NewULID() // just use "now" - } + latestStatus.ID = id.NewULIDFromTime(latestStatus.CreatedAt) } else { + // Ensure that status isn't trying to re-date itself. + if !latestStatus.CreatedAt.Equal(status.CreatedAt) { + err := gtserror.Newf("status %s 'published' changed", uri) + return nil, nil, gtserror.SetMalformed(err) + } + // Reuse existing status ID. latestStatus.ID = status.ID } @@ -519,7 +518,6 @@ func (d *Dereferencer) enrichStatus( // Set latest fetch time and carry- // over some values from "old" status. latestStatus.FetchedAt = time.Now() - latestStatus.UpdatedAt = status.UpdatedAt latestStatus.Local = status.Local latestStatus.PinnedAt = status.PinnedAt @@ -538,8 +536,9 @@ func (d *Dereferencer) enrichStatus( } // Check if this is a permitted status we should accept. - // Function also sets "PendingApproval" bool as necessary. - permit, err := d.isPermittedStatus(ctx, requestUser, status, latestStatus) + // Function also sets "PendingApproval" bool as necessary, + // and handles removal of existing statuses no longer permitted. + permit, err := d.isPermittedStatus(ctx, requestUser, status, latestStatus, isNew) if err != nil { return nil, nil, gtserror.Newf("error checking permissibility for status %s: %w", uri, err) } @@ -550,59 +549,113 @@ func (d *Dereferencer) enrichStatus( return nil, nil, gtserror.SetNotPermitted(err) } - // Ensure the status' mentions are populated, and pass in existing to check for changes. - if err := d.fetchStatusMentions(ctx, requestUser, status, latestStatus); err != nil { + // Insert / update any attached status poll. + pollChanged, err := d.handleStatusPoll(ctx, + status, + latestStatus, + ) + if err != nil { + return nil, nil, gtserror.Newf("error handling poll for status %s: %w", uri, err) + } + + // Populate mentions associated with status, passing + // in existing status to reuse old where possible. + // (especially important here to reduce need to dereference). + mentionsChanged, err := d.fetchStatusMentions(ctx, + requestUser, + status, + latestStatus, + ) + if err != nil { return nil, nil, gtserror.Newf("error populating mentions for status %s: %w", uri, err) } - // Ensure the status' poll remains consistent, else reset the poll. - if err := d.fetchStatusPoll(ctx, status, latestStatus); err != nil { - return nil, nil, gtserror.Newf("error populating poll for status %s: %w", uri, err) + // Ensure status in a thread is connected. + threadChanged, err := d.threadStatus(ctx, + status, + latestStatus, + ) + if err != nil { + return nil, nil, gtserror.Newf("error handling threading for status %s: %w", uri, err) } - // Now that we know who this status replies to (handled by ASStatusToStatus) - // and who it mentions, we can add a ThreadID to it if necessary. - if err := d.threadStatus(ctx, latestStatus); err != nil { - return nil, nil, gtserror.Newf("error checking / creating threadID for status %s: %w", uri, err) - } - - // Ensure the status' tags are populated, (changes are expected / okay). - if err := d.fetchStatusTags(ctx, status, latestStatus); err != nil { + // Populate tags associated with status, passing + // in existing status to reuse old where possible. + tagsChanged, err := d.fetchStatusTags(ctx, + status, + latestStatus, + ) + if err != nil { return nil, nil, gtserror.Newf("error populating tags for status %s: %w", uri, err) } - // Ensure the status' media attachments are populated, passing in existing to check for changes. - if err := d.fetchStatusAttachments(ctx, requestUser, status, latestStatus); err != nil { + // Populate media attachments associated with status, + // passing in existing status to reuse old where possible + // (especially important here to reduce need to dereference). + mediaChanged, err := d.fetchStatusAttachments(ctx, + requestUser, + status, + latestStatus, + ) + if err != nil { return nil, nil, gtserror.Newf("error populating attachments for status %s: %w", uri, err) } - // Ensure the status' emoji attachments are populated, passing in existing to check for changes. - if err := d.fetchStatusEmojis(ctx, status, latestStatus); err != nil { + // Populate emoji associated with status, passing + // in existing status to reuse old where possible + // (especially important here to reduce need to dereference). + emojiChanged, err := d.fetchStatusEmojis(ctx, + status, + latestStatus, + ) + if err != nil { return nil, nil, gtserror.Newf("error populating emojis for status %s: %w", uri, err) } if isNew { - // This is new, put the status in the database. - err := d.state.DB.PutStatus(ctx, latestStatus) - if err != nil { - return nil, nil, gtserror.Newf("error putting in database: %w", err) + // Simplest case, insert this new status into the database. + if err := d.state.DB.PutStatus(ctx, latestStatus); err != nil { + return nil, nil, gtserror.Newf("error inserting new status %s: %w", uri, err) } } else { - // This is an existing status, update the model in the database. - if err := d.state.DB.UpdateStatus(ctx, latestStatus); err != nil { - return nil, nil, gtserror.Newf("error updating database: %w", err) + // Check for and handle any edits to status, inserting + // historical edit if necessary. Also determines status + // columns that need updating in below query. + cols, err := d.handleStatusEdit(ctx, + status, + latestStatus, + pollChanged, + mentionsChanged, + threadChanged, + tagsChanged, + mediaChanged, + emojiChanged, + ) + if err != nil { + return nil, nil, gtserror.Newf("error handling edit for status %s: %w", uri, err) + } + + // With returned changed columns, now update the existing status entry. + if err := d.state.DB.UpdateStatus(ctx, latestStatus, cols...); err != nil { + return nil, nil, gtserror.Newf("error updating existing status %s: %w", uri, err) } } return latestStatus, statusable, nil } +// fetchStatusMentions populates the mentions on 'status', creating +// new where needed, or using unchanged mentions from 'existing' status. func (d *Dereferencer) fetchStatusMentions( ctx context.Context, requestUser string, existing *gtsmodel.Status, status *gtsmodel.Status, -) error { +) ( + changed bool, + err error, +) { + // Allocate new slice to take the yet-to-be created mention IDs. status.MentionIDs = make([]string, len(status.Mentions)) @@ -610,7 +663,6 @@ func (d *Dereferencer) fetchStatusMentions( var ( mention = status.Mentions[i] alreadyExists bool - err error ) // Search existing status for a mention already stored, @@ -633,19 +685,16 @@ func (d *Dereferencer) fetchStatusMentions( continue } + // Mark status as + // having changed. + changed = true + // This mention didn't exist yet. - // Generate new ID according to status creation. - // TODO: update this to use "edited_at" when we add - // support for edited status revision history. - mention.ID, err = id.NewULIDFromTime(status.CreatedAt) - if err != nil { - log.Errorf(ctx, "invalid created at date (falling back to 'now'): %v", err) - mention.ID = id.NewULID() // just use "now" - } + // Generate new ID according to latest update. + mention.ID = id.NewULIDFromTime(status.UpdatedAt) // Set known further mention details. - mention.CreatedAt = status.CreatedAt - mention.UpdatedAt = status.UpdatedAt + mention.CreatedAt = status.UpdatedAt mention.OriginAccount = status.Account mention.OriginAccountID = status.AccountID mention.OriginAccountURI = status.AccountURI @@ -657,7 +706,7 @@ func (d *Dereferencer) fetchStatusMentions( // Place the new mention into the database. if err := d.state.DB.PutMention(ctx, mention); err != nil { - return gtserror.Newf("error putting mention in database: %w", err) + return changed, gtserror.Newf("error putting mention in database: %w", err) } // Set the *new* mention and ID. @@ -678,17 +727,42 @@ func (d *Dereferencer) fetchStatusMentions( i++ } - return nil + return changed, nil } -func (d *Dereferencer) threadStatus(ctx context.Context, status *gtsmodel.Status) error { - if status.InReplyTo != nil { - if parentThreadID := status.InReplyTo.ThreadID; parentThreadID != "" { - // Simplest case: parent status - // is threaded, so inherit threadID. - status.ThreadID = parentThreadID - return nil +// threadStatus ensures that given status is threaded correctly +// where necessary. that is it will inherit a thread ID from the +// existing copy if it is threaded correctly, else it will inherit +// a thread ID from a parent with existing thread, else it will +// generate a new thread ID if status mentions a local account. +func (d *Dereferencer) threadStatus( + ctx context.Context, + existing *gtsmodel.Status, + status *gtsmodel.Status, +) ( + changed bool, + err error, +) { + + // Check for existing status + // that is already threaded. + if existing.ThreadID != "" { + + // Existing is threaded correctly. + if existing.InReplyTo == nil || + existing.InReplyTo.ThreadID == existing.ThreadID { + status.ThreadID = existing.ThreadID + return false, nil } + + // TODO: delete incorrect thread + } + + // Check for existing parent to inherit threading from. + if inReplyTo := status.InReplyTo; inReplyTo != nil && + inReplyTo.ThreadID != "" { + status.ThreadID = inReplyTo.ThreadID + return true, nil } // Parent wasn't threaded. If this @@ -711,7 +785,7 @@ func(m *gtsmodel.Mention) bool { // Status doesn't mention a // local account, so we don't // need to thread it. - return nil + return false, nil } // Status mentions a local account. @@ -719,24 +793,30 @@ func(m *gtsmodel.Mention) bool { // it to the status. threadID := id.NewULID() - if err := d.state.DB.PutThread( - ctx, - >smodel.Thread{ - ID: threadID, - }, + // Insert new thread model into db. + if err := d.state.DB.PutThread(ctx, + >smodel.Thread{ID: threadID}, ); err != nil { - return gtserror.Newf("error inserting new thread in db: %w", err) + return false, gtserror.Newf("error inserting new thread in db: %w", err) } + // Set thread on latest status. status.ThreadID = threadID - return nil + return true, nil } +// fetchStatusTags populates the tags on 'status', fetching existing +// from the database and creating new where needed. 'existing' is used +// to fetch tags that have not changed since previous stored status. func (d *Dereferencer) fetchStatusTags( ctx context.Context, existing *gtsmodel.Status, status *gtsmodel.Status, -) error { +) ( + changed bool, + err error, +) { + // Allocate new slice to take the yet-to-be determined tag IDs. status.TagIDs = make([]string, len(status.Tags)) @@ -751,10 +831,14 @@ func (d *Dereferencer) fetchStatusTags( continue } + // Mark status as + // having changed. + changed = true + // Look for existing tag with name in the database. existing, err := d.state.DB.GetTagByName(ctx, tag.Name) if err != nil && !errors.Is(err, db.ErrNoEntries) { - return gtserror.Newf("db error getting tag %s: %w", tag.Name, err) + return changed, gtserror.Newf("db error getting tag %s: %w", tag.Name, err) } else if existing != nil { status.Tags[i] = existing status.TagIDs[i] = existing.ID @@ -788,106 +872,21 @@ func (d *Dereferencer) fetchStatusTags( i++ } - return nil -} - -func (d *Dereferencer) fetchStatusPoll( - ctx context.Context, - existing *gtsmodel.Status, - status *gtsmodel.Status, -) error { - var ( - // insertStatusPoll generates ID and inserts the poll attached to status into the database. - insertStatusPoll = func(ctx context.Context, status *gtsmodel.Status) error { - var err error - - // Generate new ID for poll from the status CreatedAt. - // TODO: update this to use "edited_at" when we add - // support for edited status revision history. - status.Poll.ID, err = id.NewULIDFromTime(status.CreatedAt) - if err != nil { - log.Errorf(ctx, "invalid created at date (falling back to 'now'): %v", err) - status.Poll.ID = id.NewULID() // just use "now" - } - - // Update the status<->poll links. - status.PollID = status.Poll.ID - status.Poll.StatusID = status.ID - status.Poll.Status = status - - // Insert this latest poll into the database. - err = d.state.DB.PutPoll(ctx, status.Poll) - if err != nil { - return gtserror.Newf("error putting in database: %w", err) - } - - return nil - } - - // deleteStatusPoll deletes the poll with ID, and all attached votes, from the database. - deleteStatusPoll = func(ctx context.Context, pollID string) error { - if err := d.state.DB.DeletePollByID(ctx, pollID); err != nil { - return gtserror.Newf("error deleting existing poll from database: %w", err) - } - return nil - } - ) - - switch { - case existing.Poll == nil && status.Poll == nil: - // no poll before or after, nothing to do. - return nil - - case existing.Poll == nil && status.Poll != nil: - // no previous poll, insert new poll! - return insertStatusPoll(ctx, status) - - case status.Poll == nil: - // existing poll has been deleted, remove this. - return deleteStatusPoll(ctx, existing.PollID) - - case pollChanged(existing.Poll, status.Poll): - // poll has changed since original, delete and reinsert new. - if err := deleteStatusPoll(ctx, existing.PollID); err != nil { - return err - } - return insertStatusPoll(ctx, status) - - case pollUpdated(existing.Poll, status.Poll): - // Since we last saw it, the poll has updated! - // Whether that be stats, or close time. - poll := existing.Poll - poll.Closing = pollJustClosed(existing.Poll, status.Poll) - poll.ClosedAt = status.Poll.ClosedAt - poll.Voters = status.Poll.Voters - poll.Votes = status.Poll.Votes - - // Update poll model in the database (specifically only the possible changed columns). - if err := d.state.DB.UpdatePoll(ctx, poll, "closed_at", "voters", "votes"); err != nil { - return gtserror.Newf("error updating poll: %w", err) - } - - // Update poll on status. - status.PollID = poll.ID - status.Poll = poll - return nil - - default: - // latest and existing - // polls are up to date. - poll := existing.Poll - status.PollID = poll.ID - status.Poll = poll - return nil - } + return changed, nil } +// fetchStatusAttachments populates the attachments on 'status', creating new database +// entries where needed and dereferencing it, or using unchanged from 'existing' status. func (d *Dereferencer) fetchStatusAttachments( ctx context.Context, requestUser string, existing *gtsmodel.Status, status *gtsmodel.Status, -) error { +) ( + changed bool, + err error, +) { + // Allocate new slice to take the yet-to-be fetched attachment IDs. status.AttachmentIDs = make([]string, len(status.Attachments)) @@ -897,9 +896,26 @@ func (d *Dereferencer) fetchStatusAttachments( // Look for existing media attachment with remote URL first. existing, ok := existing.GetAttachmentByRemoteURL(placeholder.RemoteURL) if ok && existing.ID != "" { + var info media.AdditionalMediaInfo - // Ensure the existing media attachment is up-to-date and cached. - existing, err := d.updateAttachment(ctx, requestUser, existing, placeholder) + // Look for any difference in stored media description. + diff := (existing.Description != placeholder.Description) + if diff { + info.Description = &placeholder.Description + } + + // If description changed, + // we mark media as changed. + changed = changed || diff + + // Store any attachment updates and + // ensure media is locally cached. + existing, err := d.RefreshMedia(ctx, + requestUser, + existing, + info, + diff, + ) if err != nil { log.Errorf(ctx, "error updating existing attachment: %v", err) @@ -915,9 +931,12 @@ func (d *Dereferencer) fetchStatusAttachments( continue } + // Mark status as + // having changed. + changed = true + // Load this new media attachment. - attachment, err := d.GetMedia( - ctx, + attachment, err := d.GetMedia(ctx, requestUser, status.AccountID, placeholder.RemoteURL, @@ -955,42 +974,316 @@ func (d *Dereferencer) fetchStatusAttachments( i++ } - return nil + return changed, nil } +// fetchStatusEmojis populates the emojis on 'status', creating new database entries +// where needed and dereferencing it, or using unchanged from 'existing' status. func (d *Dereferencer) fetchStatusEmojis( ctx context.Context, existing *gtsmodel.Status, status *gtsmodel.Status, -) error { +) ( + changed bool, + err error, +) { + // Fetch the updated emojis for our status. emojis, changed, err := d.fetchEmojis(ctx, existing.Emojis, status.Emojis, ) if err != nil { - return gtserror.Newf("error fetching emojis: %w", err) + return changed, gtserror.Newf("error fetching emojis: %w", err) } if !changed { // Use existing status emoji objects. status.EmojiIDs = existing.EmojiIDs status.Emojis = existing.Emojis - return nil + return false, nil } // Set latest emojis. status.Emojis = emojis - // Iterate over and set changed emoji IDs. + // Extract IDs from latest slice of emojis. status.EmojiIDs = make([]string, len(emojis)) for i, emoji := range emojis { status.EmojiIDs[i] = emoji.ID } + // Combine both old and new emojis, as statuses.emojis + // keeps track of emojis for both old and current edits. + status.EmojiIDs = append(status.EmojiIDs, existing.EmojiIDs...) + status.Emojis = append(status.Emojis, existing.Emojis...) + status.EmojiIDs = xslices.Deduplicate(status.EmojiIDs) + status.Emojis = xslices.DeduplicateFunc(status.Emojis, + func(e *gtsmodel.Emoji) string { return e.ID }, + ) + + return true, nil +} + +// handleStatusPoll handles both inserting of new status poll or the +// update of an existing poll. this handles the case of simple vote +// count updates (without being classified as a change of the poll +// itself), as well as full poll changes that delete existing instance. +func (d *Dereferencer) handleStatusPoll( + ctx context.Context, + existing *gtsmodel.Status, + status *gtsmodel.Status, +) ( + changed bool, + err error, +) { + switch { + case existing.Poll == nil && status.Poll == nil: + // no poll before or after, nothing to do. + return false, nil + + case existing.Poll == nil && status.Poll != nil: + // no previous poll, insert new status poll! + return true, d.insertStatusPoll(ctx, status) + + case status.Poll == nil: + // existing status poll has been deleted, remove this from the database. + if err = d.state.DB.DeletePollByID(ctx, existing.Poll.ID); err != nil { + err = gtserror.Newf("error deleting poll from database: %w", err) + } + return true, err + + case pollChanged(existing.Poll, status.Poll): + // existing status poll has been changed, remove this from the database. + if err = d.state.DB.DeletePollByID(ctx, existing.Poll.ID); err != nil { + return true, gtserror.Newf("error deleting poll from database: %w", err) + } + + // insert latest poll version into database. + return true, d.insertStatusPoll(ctx, status) + + case pollStateUpdated(existing.Poll, status.Poll): + // Since we last saw it, the poll has updated! + // Whether that be stats, or close time. + poll := existing.Poll + poll.Closing = pollJustClosed(existing.Poll, status.Poll) + poll.ClosedAt = status.Poll.ClosedAt + poll.Voters = status.Poll.Voters + poll.Votes = status.Poll.Votes + + // Update poll model in the database (specifically only the possible changed columns). + if err = d.state.DB.UpdatePoll(ctx, poll, "closed_at", "voters", "votes"); err != nil { + return false, gtserror.Newf("error updating poll: %w", err) + } + + // Update poll on status. + status.PollID = poll.ID + status.Poll = poll + return false, nil + + default: + // latest and existing + // polls are up to date. + poll := existing.Poll + status.PollID = poll.ID + status.Poll = poll + return false, nil + } +} + +// insertStatusPoll inserts an assumed new poll attached to status into the database, this +// also handles generating new ID for the poll and setting necessary fields on the status. +func (d *Dereferencer) insertStatusPoll(ctx context.Context, status *gtsmodel.Status) error { + var err error + + // Generate new ID for poll from latest updated time. + status.Poll.ID = id.NewULIDFromTime(status.UpdatedAt) + + // Update the status<->poll links. + status.PollID = status.Poll.ID + status.Poll.StatusID = status.ID + status.Poll.Status = status + + // Insert this latest poll into the database. + err = d.state.DB.PutPoll(ctx, status.Poll) + if err != nil { + return gtserror.Newf("error putting poll in database: %w", err) + } + return nil } +// handleStatusEdit compiles a list of changed status table columns between +// existing and latest status model, and where necessary inserts a historic +// edit of the status into the database to store its previous state. the +// returned slice is a list of columns requiring updating in the database. +func (d *Dereferencer) handleStatusEdit( + ctx context.Context, + existing *gtsmodel.Status, + status *gtsmodel.Status, + pollChanged bool, + mentionsChanged bool, + threadChanged bool, + tagsChanged bool, + mediaChanged bool, + emojiChanged bool, +) ( + cols []string, + err error, +) { + var edited bool + + // Preallocate max slice length. + cols = make([]string, 1, 13) + + // Always update `fetched_at`. + cols[0] = "fetched_at" + + // Check for edited status content. + if existing.Content != status.Content { + cols = append(cols, "content") + edited = true + } + + // Check for edited status content warning. + if existing.ContentWarning != status.ContentWarning { + cols = append(cols, "content_warning") + edited = true + } + + // Check for edited status sensitive flag. + if *existing.Sensitive != *status.Sensitive { + cols = append(cols, "sensitive") + edited = true + } + + // Check for edited status language tag. + if existing.Language != status.Language { + cols = append(cols, "language") + edited = true + } + + if pollChanged { + // Attached poll was changed. + cols = append(cols, "poll_id") + edited = true + } + + if mentionsChanged { + cols = append(cols, "mentions") // i.e. MentionIDs + + // Mentions changed doesn't necessarily + // indicate an edit, it may just not have + // been previously populated properly. + } + + if threadChanged { + cols = append(cols, "thread_id") + + // Thread changed doesn't necessarily + // indicate an edit, it may just now + // actually be included in a thread. + } + + if tagsChanged { + cols = append(cols, "tags") // i.e. TagIDs + + // Tags changed doesn't necessarily + // indicate an edit, it may just not have + // been previously populated properly. + } + + if mediaChanged { + // Attached media was changed. + cols = append(cols, "attachments") // i.e. AttachmentIDs + edited = true + } + + if emojiChanged { + // Attached emojis changed. + cols = append(cols, "emojis") // i.e. EmojiIDs + + // We specifically store both *new* AND *old* edit + // revision emojis in the statuses.emojis column. + emojiByID := func(e *gtsmodel.Emoji) string { return e.ID } + status.Emojis = append(status.Emojis, existing.Emojis...) + status.Emojis = xslices.DeduplicateFunc(status.Emojis, emojiByID) + status.EmojiIDs = xslices.Gather(status.EmojiIDs[:0], status.Emojis, emojiByID) + + // Emojis changed doesn't necessarily + // indicate an edit, it may just not have + // been previously populated properly. + } + + if edited { + // ensure that updated_at hasn't remained the same + // but an edit was received. manually intervene here. + if status.UpdatedAt.Equal(existing.UpdatedAt) || + status.CreatedAt.Equal(status.UpdatedAt) { + + // Simply use current fetching time. + status.UpdatedAt = status.FetchedAt + } + + // Status has been editted since last + // we saw it, take snapshot of existing. + var edit gtsmodel.StatusEdit + edit.ID = id.NewULIDFromTime(status.UpdatedAt) + edit.Content = existing.Content + edit.ContentWarning = existing.ContentWarning + edit.Text = existing.Text + edit.Language = existing.Language + edit.Sensitive = existing.Sensitive + edit.StatusID = status.ID + + // Copy existing attachments and descriptions. + edit.AttachmentIDs = existing.AttachmentIDs + edit.Attachments = existing.Attachments + if l := len(existing.Attachments); l > 0 { + edit.AttachmentDescriptions = make([]string, l) + for i, attach := range existing.Attachments { + edit.AttachmentDescriptions[i] = attach.Description + } + } + + // Edit creation is last update time. + edit.CreatedAt = existing.UpdatedAt + + if existing.Poll != nil { + // Poll only set if existing contained them. + edit.PollOptions = existing.Poll.Options + + if pollChanged || !*existing.Poll.HideCounts || + !existing.Poll.ClosedAt.IsZero() { + // If the counts are allowed to be + // shown, or poll has changed, then + // include poll vote counts in edit. + edit.PollVotes = existing.Poll.Votes + } + } + + // Insert this new edit of existing status into database. + if err := d.state.DB.PutStatusEdit(ctx, &edit); err != nil { + return nil, gtserror.Newf("error putting edit in database: %w", err) + } + + // Add edit to list of edits on the status. + status.EditIDs = append(status.EditIDs, edit.ID) + status.Edits = append(status.Edits, &edit) + + // Add edit to list of cols. + cols = append(cols, "edits") + } + + if !existing.UpdatedAt.Equal(status.UpdatedAt) { + // Whether status edited or not, + // updated_at column has changed. + cols = append(cols, "updated_at") + } + + return cols, nil +} + // getPopulatedMention tries to populate the given // mention with the correct TargetAccount and (if not // yet set) TargetAccountURI, returning the populated diff --git a/internal/federation/dereferencing/status_permitted.go b/internal/federation/dereferencing/status_permitted.go index 9ad425c2f..5d05c5de4 100644 --- a/internal/federation/dereferencing/status_permitted.go +++ b/internal/federation/dereferencing/status_permitted.go @@ -62,6 +62,7 @@ func (d *Dereferencer) isPermittedStatus( requestUser string, existing *gtsmodel.Status, status *gtsmodel.Status, + isNew bool, ) ( permitted bool, // is permitted? err error, @@ -98,7 +99,7 @@ func (d *Dereferencer) isPermittedStatus( permitted = true } - if !permitted && existing != nil { + if !permitted && !isNew { log.Infof(ctx, "deleting unpermitted: %s", existing.URI) // Delete existing status from database as it's no longer permitted. @@ -110,11 +111,13 @@ func (d *Dereferencer) isPermittedStatus( return } +// isPermittedReply ... func (d *Dereferencer) isPermittedReply( ctx context.Context, requestUser string, reply *gtsmodel.Status, ) (bool, error) { + var ( replyURI = reply.URI // Definitely set. inReplyToURI = reply.InReplyToURI // Definitely set. @@ -149,8 +152,7 @@ func (d *Dereferencer) isPermittedReply( // If this status's parent was rejected, // implicitly this reply should be too; // there's nothing more to check here. - return false, d.unpermittedByParent( - ctx, + return false, d.unpermittedByParent(ctx, reply, thisReq, parentReq, @@ -164,6 +166,7 @@ func (d *Dereferencer) isPermittedReply( // be approved, then we should just reject it // again, as nothing's changed since last time. if thisRejected && acceptIRI == "" { + // Nothing changed, // still rejected. return false, nil @@ -174,6 +177,7 @@ func (d *Dereferencer) isPermittedReply( // to be approved. Continue permission checks. if inReplyTo == nil { + // If we didn't have the replied-to status // in our database (yet), we can't check // right now if this reply is permitted. diff --git a/internal/federation/dereferencing/status_test.go b/internal/federation/dereferencing/status_test.go index 3b2c2bff2..4b3bd6d67 100644 --- a/internal/federation/dereferencing/status_test.go +++ b/internal/federation/dereferencing/status_test.go @@ -21,14 +21,21 @@ "context" "fmt" "testing" + "time" "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/testrig" ) +// instantFreshness is the shortest possible freshness window. +var instantFreshness = util.Ptr(dereferencing.FreshnessWindow(0)) + type StatusTestSuite struct { DereferencerStandardTestSuite } @@ -229,6 +236,219 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithNonMatchingURI() { suite.Nil(fetchedStatus) } +func (suite *StatusTestSuite) TestDereferencerRefreshStatusUpdated() { + // Create a new context for this test. + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + // The local account we will be fetching statuses as. + fetchingAccount := suite.testAccounts["local_account_1"] + + // The test status in question that we will be dereferencing from "remote". + testURIStr := "https://unknown-instance.com/users/brand_new_person/statuses/01FE4NTHKWW7THT67EF10EB839" + testURI := testrig.URLMustParse(testURIStr) + testStatusable := suite.client.TestRemoteStatuses[testURIStr] + + // Fetch the remote status first to load it into instance. + testStatus, statusable, err := suite.dereferencer.GetStatusByURI(ctx, + fetchingAccount.Username, + testURI, + ) + suite.NotNil(statusable) + suite.NoError(err) + + // Run through multiple possible edits. + for _, testCase := range []struct { + editedContent string + editedContentWarning string + editedLanguage string + editedSensitive bool + editedAttachmentIDs []string + editedPollOptions []string + editedPollVotes []int + editedAt time.Time + }{ + { + editedContent: "updated status content!", + editedContentWarning: "CW: edited status content", + editedLanguage: testStatus.Language, // no change + editedSensitive: *testStatus.Sensitive, // no change + editedAttachmentIDs: testStatus.AttachmentIDs, // no change + editedPollOptions: getPollOptions(testStatus), // no change + editedPollVotes: getPollVotes(testStatus), // no change + editedAt: time.Now(), + }, + } { + // Take a snapshot of current + // state of the test status. + testStatus = copyStatus(testStatus) + + // Edit the "remote" statusable obj. + suite.editStatusable(testStatusable, + testCase.editedContent, + testCase.editedContentWarning, + testCase.editedLanguage, + testCase.editedSensitive, + testCase.editedAttachmentIDs, + testCase.editedPollOptions, + testCase.editedPollVotes, + testCase.editedAt, + ) + + // Refresh with a given statusable to updated to edited copy. + latest, statusable, err := suite.dereferencer.RefreshStatus(ctx, + fetchingAccount.Username, + testStatus, + nil, // NOTE: can provide testStatusable here to test as being received (not deref'd) + instantFreshness, + ) + suite.NotNil(statusable) + suite.NoError(err) + + // verify updated status details. + suite.verifyEditedStatusUpdate( + + // the original status + // before any changes. + testStatus, + + // latest status + // being tested. + latest, + + // expected current state. + >smodel.StatusEdit{ + Content: testCase.editedContent, + ContentWarning: testCase.editedContentWarning, + Language: testCase.editedLanguage, + Sensitive: &testCase.editedSensitive, + AttachmentIDs: testCase.editedAttachmentIDs, + PollOptions: testCase.editedPollOptions, + PollVotes: testCase.editedPollVotes, + // createdAt never changes + }, + + // expected historic edit. + >smodel.StatusEdit{ + Content: testStatus.Content, + ContentWarning: testStatus.ContentWarning, + Language: testStatus.Language, + Sensitive: testStatus.Sensitive, + AttachmentIDs: testStatus.AttachmentIDs, + PollOptions: getPollOptions(testStatus), + PollVotes: getPollVotes(testStatus), + CreatedAt: testStatus.UpdatedAt, + }, + ) + } +} + +// editStatusable updates the given statusable attributes. +// note that this acts on the original object, no copying. +func (suite *StatusTestSuite) editStatusable( + statusable ap.Statusable, + content string, + contentWarning string, + language string, + sensitive bool, + attachmentIDs []string, // TODO: this will require some thinking as to how ... + pollOptions []string, // TODO: this will require changing statusable type to question + pollVotes []int, // TODO: this will require changing statusable type to question + editedAt time.Time, +) { + // simply reset all mentions / emojis / tags + statusable.SetActivityStreamsTag(nil) + + // Update the statusable content property + language (if set). + contentProp := streams.NewActivityStreamsContentProperty() + statusable.SetActivityStreamsContent(contentProp) + contentProp.AppendXMLSchemaString(content) + if language != "" { + contentProp.AppendRDFLangString(map[string]string{ + language: content, + }) + } + + // Update the statusable content-warning property. + summaryProp := streams.NewActivityStreamsSummaryProperty() + statusable.SetActivityStreamsSummary(summaryProp) + summaryProp.AppendXMLSchemaString(contentWarning) + + // Update the statusable sensitive property. + sensitiveProp := streams.NewActivityStreamsSensitiveProperty() + statusable.SetActivityStreamsSensitive(sensitiveProp) + sensitiveProp.AppendXMLSchemaBoolean(sensitive) + + // Update the statusable updated property. + ap.SetUpdated(statusable, editedAt) +} + +// verifyEditedStatusUpdate verifies that a given status has +// the expected number of historic edits, the 'current' status +// attributes (encapsulated as an edit for minimized no. args), +// and the last given 'historic' status edit attributes. +func (suite *StatusTestSuite) verifyEditedStatusUpdate( + testStatus *gtsmodel.Status, // the original model + status *gtsmodel.Status, // the status to check + current *gtsmodel.StatusEdit, // expected current state + historic *gtsmodel.StatusEdit, // historic edit we expect to have +) { + // don't use this func + // name in error msgs. + suite.T().Helper() + + // Check we have expected number of edits. + previousEdits := len(testStatus.Edits) + suite.Len(status.Edits, previousEdits+1) + suite.Len(status.EditIDs, previousEdits+1) + + // Check current state of status. + suite.Equal(current.Content, status.Content) + suite.Equal(current.ContentWarning, status.ContentWarning) + suite.Equal(current.Language, status.Language) + suite.Equal(*current.Sensitive, *status.Sensitive) + suite.Equal(current.AttachmentIDs, status.AttachmentIDs) + suite.Equal(current.PollOptions, getPollOptions(status)) + suite.Equal(current.PollVotes, getPollVotes(status)) + + // Check the latest historic edit matches expected. + latestEdit := status.Edits[len(status.Edits)-1] + suite.Equal(historic.Content, latestEdit.Content) + suite.Equal(historic.ContentWarning, latestEdit.ContentWarning) + suite.Equal(historic.Language, latestEdit.Language) + suite.Equal(*historic.Sensitive, *latestEdit.Sensitive) + suite.Equal(historic.AttachmentIDs, latestEdit.AttachmentIDs) + suite.Equal(historic.PollOptions, latestEdit.PollOptions) + suite.Equal(historic.PollVotes, latestEdit.PollVotes) + suite.Equal(historic.CreatedAt, latestEdit.CreatedAt) + + // The status creation date should never change. + suite.Equal(testStatus.CreatedAt, status.CreatedAt) +} + func TestStatusTestSuite(t *testing.T) { suite.Run(t, new(StatusTestSuite)) } + +// copyStatus returns a copy of the given status model (not including sub-structs). +func copyStatus(status *gtsmodel.Status) *gtsmodel.Status { + copy := new(gtsmodel.Status) + *copy = *status + return copy +} + +// getPollOptions extracts poll option strings from status (if poll is set). +func getPollOptions(status *gtsmodel.Status) []string { + if status.Poll != nil { + return status.Poll.Options + } + return nil +} + +// getPollVotes extracts poll vote counts from status (if poll is set). +func getPollVotes(status *gtsmodel.Status) []int { + if status.Poll != nil { + return status.Poll.Votes + } + return nil +} diff --git a/internal/federation/dereferencing/util.go b/internal/federation/dereferencing/util.go index 297e90adc..208117660 100644 --- a/internal/federation/dereferencing/util.go +++ b/internal/federation/dereferencing/util.go @@ -52,15 +52,15 @@ func emojiChanged(existing, latest *gtsmodel.Emoji) bool { // pollChanged returns whether a poll has changed in way that // indicates that this should be an entirely new poll. i.e. if -// the available options have changed, or the expiry has increased. +// the available options have changed, or the expiry has changed. func pollChanged(existing, latest *gtsmodel.Poll) bool { return !slices.Equal(existing.Options, latest.Options) || !existing.ExpiresAt.Equal(latest.ExpiresAt) } -// pollUpdated returns whether a poll has updated, i.e. if the +// pollStateUpdated returns whether a poll has updated, i.e. if // vote counts have changed, or if it has expired / been closed. -func pollUpdated(existing, latest *gtsmodel.Poll) bool { +func pollStateUpdated(existing, latest *gtsmodel.Poll) bool { return *existing.Voters != *latest.Voters || !slices.Equal(existing.Votes, latest.Votes) || !existing.ClosedAt.Equal(latest.ClosedAt) diff --git a/internal/federation/federatingdb/announce_test.go b/internal/federation/federatingdb/announce_test.go index 264279253..5bb2fc877 100644 --- a/internal/federation/federatingdb/announce_test.go +++ b/internal/federation/federatingdb/announce_test.go @@ -79,7 +79,7 @@ func (suite *AnnounceTestSuite) TestAnnounceTwice() { // Insert the boost-of status into the // DB cache to emulate processor handling - boost.ID, _ = id.NewULIDFromTime(boost.CreatedAt) + boost.ID = id.NewULIDFromTime(boost.CreatedAt) suite.state.Caches.DB.Status.Put(boost) // only the URI will be set for the boosted status diff --git a/internal/gtsmodel/instance.go b/internal/gtsmodel/instance.go index 027d8fba4..97c0268ce 100644 --- a/internal/gtsmodel/instance.go +++ b/internal/gtsmodel/instance.go @@ -34,6 +34,7 @@ type Instance struct { ShortDescriptionText string `bun:""` // Raw text version of short description (before parsing). Description string `bun:""` // Longer description of this instance. DescriptionText string `bun:""` // Raw text version of long description (before parsing). + CustomCSS string `bun:",nullzero"` // Custom CSS for the instance. Terms string `bun:""` // Terms and conditions of this instance. TermsText string `bun:""` // Raw text version of terms (before parsing). ContactEmail string `bun:""` // Contact email address for this instance diff --git a/internal/gtsmodel/mediaattachment.go b/internal/gtsmodel/mediaattachment.go index f4bfb5929..5cf6f60a6 100644 --- a/internal/gtsmodel/mediaattachment.go +++ b/internal/gtsmodel/mediaattachment.go @@ -26,7 +26,6 @@ type MediaAttachment struct { ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated StatusID string `bun:"type:CHAR(26),nullzero"` // ID of the status to which this is attached URL string `bun:",nullzero"` // Where can the attachment be retrieved on *this* server RemoteURL string `bun:",nullzero"` // Where can the attachment be retrieved on a remote server (empty for local media) diff --git a/internal/gtsmodel/mention.go b/internal/gtsmodel/mention.go index 24e83f904..180193f0f 100644 --- a/internal/gtsmodel/mention.go +++ b/internal/gtsmodel/mention.go @@ -26,7 +26,6 @@ type Mention struct { ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated StatusID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the status this mention originates from Status *Status `bun:"rel:belongs-to"` // status referred to by statusID OriginAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the mention creator account diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go index f8bd068ab..4c65d8a88 100644 --- a/internal/gtsmodel/status.go +++ b/internal/gtsmodel/status.go @@ -20,6 +20,8 @@ import ( "slices" "time" + + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" ) // Status represents a user-created 'post' or 'status' in the database, either remote or local @@ -55,6 +57,8 @@ type Status struct { BoostOf *Status `bun:"-"` // status that corresponds to boostOfID BoostOfAccount *Account `bun:"rel:belongs-to"` // account that corresponds to boostOfAccountID ThreadID string `bun:"type:CHAR(26),nullzero"` // id of the thread to which this status belongs; only set for remote statuses if a local account is involved at some point in the thread, otherwise null + EditIDs []string `bun:"edits,array"` // + Edits []*StatusEdit `bun:"-"` // PollID string `bun:"type:CHAR(26),nullzero"` // Poll *Poll `bun:"-"` // ContentWarning string `bun:",nullzero"` // cw string for this status @@ -92,7 +96,8 @@ func (s *Status) GetBoostOfAccountID() string { return s.BoostOfAccountID } -// AttachmentsPopulated returns whether media attachments are populated according to current AttachmentIDs. +// AttachmentsPopulated returns whether media attachments +// are populated according to current AttachmentIDs. func (s *Status) AttachmentsPopulated() bool { if len(s.AttachmentIDs) != len(s.Attachments) { // this is the quickest indicator. @@ -106,7 +111,8 @@ func (s *Status) AttachmentsPopulated() bool { return true } -// TagsPopulated returns whether tags are populated according to current TagIDs. +// TagsPopulated returns whether tags are +// populated according to current TagIDs. func (s *Status) TagsPopulated() bool { if len(s.TagIDs) != len(s.Tags) { // this is the quickest indicator. @@ -120,7 +126,8 @@ func (s *Status) TagsPopulated() bool { return true } -// MentionsPopulated returns whether mentions are populated according to current MentionIDs. +// MentionsPopulated returns whether mentions are +// populated according to current MentionIDs. func (s *Status) MentionsPopulated() bool { if len(s.MentionIDs) != len(s.Mentions) { // this is the quickest indicator. @@ -134,7 +141,8 @@ func (s *Status) MentionsPopulated() bool { return true } -// EmojisPopulated returns whether emojis are populated according to current EmojiIDs. +// EmojisPopulated returns whether emojis are +// populated according to current EmojiIDs. func (s *Status) EmojisPopulated() bool { if len(s.EmojiIDs) != len(s.Emojis) { // this is the quickest indicator. @@ -148,6 +156,21 @@ func (s *Status) EmojisPopulated() bool { return true } +// EditsPopulated returns whether edits are +// populated according to current EditIDs. +func (s *Status) EditsPopulated() bool { + if len(s.EditIDs) != len(s.Edits) { + // this is quickest indicator. + return false + } + for i, id := range s.EditIDs { + if s.Edits[i].ID != id { + return false + } + } + return true +} + // EmojissUpToDate returns whether status emoji attachments of receiving status are up-to-date // according to emoji attachments of the passed status, by comparing their emoji URIs. We don't // use IDs as this is used to determine whether there are new emojis to fetch. @@ -247,6 +270,35 @@ func (s *Status) IsLocalOnly() bool { return s.Federated == nil || !*s.Federated } +// AllAttachmentIDs gathers ALL media attachment IDs from both the +// receiving Status{}, and any historical Status{}.Edits. Note that +// this function will panic if Status{}.Edits is not populated. +func (s *Status) AllAttachmentIDs() []string { + var total int + + if len(s.EditIDs) != len(s.Edits) { + panic("status edits not populated") + } + + // Get count of attachment IDs. + total += len(s.Attachments) + for _, edit := range s.Edits { + total += len(edit.AttachmentIDs) + } + + // Start gathering of all IDs with *current* attachment IDs. + attachmentIDs := make([]string, len(s.AttachmentIDs), total) + copy(attachmentIDs, s.AttachmentIDs) + + // Append IDs of historical edits. + for _, edit := range s.Edits { + attachmentIDs = append(attachmentIDs, edit.AttachmentIDs...) + } + + // Deduplicate these IDs in case of shared media. + return xslices.Deduplicate(attachmentIDs) +} + // StatusToTag is an intermediate struct to facilitate the many2many relationship between a status and one or more tags. type StatusToTag struct { StatusID string `bun:"type:CHAR(26),unique:statustag,nullzero,notnull"` diff --git a/internal/gtsmodel/statusedit.go b/internal/gtsmodel/statusedit.go new file mode 100644 index 000000000..199d47736 --- /dev/null +++ b/internal/gtsmodel/statusedit.go @@ -0,0 +1,62 @@ +// 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 gtsmodel + +import "time" + +// StatusEdit represents a **historical** view of a Status +// after a received edit. The Status itself will always +// contain the latest up-to-date information. +// +// Note that stored status edits may not exactly match that +// of the origin server, they are a best-effort by receiver +// to store version history. There is no AP history endpoint. +type StatusEdit struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database. + Content string `bun:""` // Content of status at time of edit; likely html-formatted but not guaranteed. + ContentWarning string `bun:",nullzero"` // Content warning of status at time of edit. + Text string `bun:""` // Original status text, without formatting, at time of edit. + Language string `bun:",nullzero"` // Status language at time of edit. + Sensitive *bool `bun:",nullzero,notnull,default:false"` // Status sensitive flag at time of edit. + AttachmentIDs []string `bun:"attachments,array"` // Database IDs of media attachments associated with status at time of edit. + AttachmentDescriptions []string `bun:",array"` // Previous media descriptions of media attachments associated with status at time of edit. + Attachments []*MediaAttachment `bun:"-"` // Media attachments relating to .AttachmentIDs field (not always populated). + PollOptions []string `bun:",array"` // Poll options of status at time of edit, only set if status contains a poll. + PollVotes []int `bun:",array"` // Poll vote count at time of status edit, only set if poll votes were reset. + StatusID string `bun:"type:CHAR(26),nullzero,notnull"` // The originating status ID this is a historical edit of. + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // The creation time of this version of the status content (according to receiving server). + + // We don't bother having a *gtsmodel.Status model here + // as the StatusEdit is always just attached to a Status, + // so it doesn't need a self-reference back to it. +} + +// AttachmentsPopulated returns whether media attachments +// are populated according to current AttachmentIDs. +func (e *StatusEdit) AttachmentsPopulated() bool { + if len(e.AttachmentIDs) != len(e.Attachments) { + // this is the quickest indicator. + return false + } + for i, id := range e.AttachmentIDs { + if e.Attachments[i].ID != id { + return false + } + } + return true +} diff --git a/internal/id/ulid.go b/internal/id/ulid.go index 8de4cc4cc..8c0b1e94c 100644 --- a/internal/id/ulid.go +++ b/internal/id/ulid.go @@ -22,7 +22,9 @@ "math/big" "time" + "codeberg.org/gruf/go-kv" "github.com/oklog/ulid" + "github.com/superseriousbusiness/gotosocial/internal/log" ) const ( @@ -45,13 +47,19 @@ func NewULID() string { return ulid.String() } -// NewULIDFromTime returns a new ULID string using the given time, or an error if something goes wrong. -func NewULIDFromTime(t time.Time) (string, error) { - newUlid, err := ulid.New(ulid.Timestamp(t), rand.Reader) - if err != nil { - return "", err +// NewULIDFromTime returns a new ULID string using +// given time, or from current time on any error. +func NewULIDFromTime(t time.Time) string { + ts := ulid.Timestamp(t) + if ts > ulid.MaxTime() { + log.WarnKVs(nil, kv.Fields{ + {K: "caller", V: log.Caller(2)}, + {K: "value", V: t}, + {K: "msg", V: "invalid ulid time"}, + }...) + ts = ulid.Now() } - return newUlid.String(), nil + return ulid.MustNew(ts, rand.Reader).String() } // NewRandomULID returns a new ULID string using a random time in an ~80 year range around the current datetime, or an error if something goes wrong. diff --git a/internal/media/manager.go b/internal/media/manager.go index 2807848bd..6aa13c17b 100644 --- a/internal/media/manager.go +++ b/internal/media/manager.go @@ -118,15 +118,11 @@ func (m *Manager) CreateMedia( Header: util.Ptr(false), Cached: util.Ptr(false), CreatedAt: now, - UpdatedAt: now, } // Check if we were provided additional info // to add to the attachment, and overwrite // some of the attachment fields if so. - if info.CreatedAt != nil { - attachment.CreatedAt = *info.CreatedAt - } if info.StatusID != nil { attachment.StatusID = *info.StatusID } @@ -372,9 +368,6 @@ func (m *Manager) createOrUpdateEmoji( if info.URI != nil { emoji.URI = *info.URI } - if info.CreatedAt != nil { - emoji.CreatedAt = *info.CreatedAt - } if info.Domain != nil { emoji.Domain = *info.Domain } diff --git a/internal/media/manager_test.go b/internal/media/manager_test.go index e175369f5..5b6882100 100644 --- a/internal/media/manager_test.go +++ b/internal/media/manager_test.go @@ -109,7 +109,6 @@ func (suite *ManagerTestSuite) TestEmojiProcessRefresh() { emojiToUpdate, data, media.AdditionalEmojiInfo{ - CreatedAt: &emojiToUpdate.CreatedAt, Domain: &emojiToUpdate.Domain, ImageRemoteURL: &newImageRemoteURL, }, diff --git a/internal/media/types.go b/internal/media/types.go index 9631a15bd..827752941 100644 --- a/internal/media/types.go +++ b/internal/media/types.go @@ -20,7 +20,6 @@ import ( "context" "io" - "time" ) type Size string @@ -44,10 +43,6 @@ // should be added to attachment when processing a piece of media. type AdditionalMediaInfo struct { - // Time that this media was - // created; defaults to time.Now(). - CreatedAt *time.Time - // ID of the status to which this // media is attached; defaults to "". StatusID *string @@ -93,10 +88,6 @@ type AdditionalEmojiInfo struct { // this remote emoji. URI *string - // Time that this emoji was - // created; defaults to time.Now(). - CreatedAt *time.Time - // Domain the emoji originated from. Blank // for this instance's domain. Defaults to "". Domain *string diff --git a/internal/processing/account/rss_test.go b/internal/processing/account/rss_test.go index e4706d3b7..5606151c2 100644 --- a/internal/processing/account/rss_test.go +++ b/internal/processing/account/rss_test.go @@ -70,7 +70,7 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() { func (suite *GetRSSTestSuite) TestGetAccountRSSZork() { getFeed, lastModified, err := suite.accountProcessor.GetRSSFeedForUsername(context.Background(), "the_mighty_zork") suite.NoError(err) - suite.EqualValues(1704878640, lastModified.Unix()) + suite.EqualValues(1730451600, lastModified.Unix()) feed, err := getFeed() suite.NoError(err) @@ -79,13 +79,23 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZork() { Posts from @the_mighty_zork@localhost:8080 http://localhost:8080/@the_mighty_zork Posts from @the_mighty_zork@localhost:8080 - Wed, 10 Jan 2024 09:24:00 +0000 - Wed, 10 Jan 2024 09:24:00 +0000 + Fri, 01 Nov 2024 09:00:00 +0000 + Fri, 01 Nov 2024 09:00:00 +0000 http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp Avatar for @the_mighty_zork@localhost:8080 http://localhost:8080/@the_mighty_zork + + edited status + http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR + @the_mighty_zork@localhost:8080 made a new post: "this is the latest revision of the status, with a content-warning" + this is the latest revision of the status, with a content-warning

]]>
+ @the_mighty_zork@localhost:8080 + http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR + Fri, 01 Nov 2024 09:00:00 +0000 + http://localhost:8080/@the_mighty_zork/feed.rss +
HTML in post http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40 diff --git a/internal/processing/admin/rule.go b/internal/processing/admin/rule.go index d1ee63cc8..8134c21cd 100644 --- a/internal/processing/admin/rule.go +++ b/internal/processing/admin/rule.go @@ -27,6 +27,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -42,7 +43,7 @@ func (p *Processor) RulesGet( apiRules := make([]*apimodel.AdminInstanceRule, len(rules)) for i := range rules { - apiRules[i] = p.converter.InstanceRuleToAdminAPIRule(&rules[i]) + apiRules[i] = typeutils.InstanceRuleToAdminAPIRule(&rules[i]) } return apiRules, nil @@ -58,7 +59,7 @@ func (p *Processor) RuleGet(ctx context.Context, id string) (*apimodel.AdminInst return nil, gtserror.NewErrorInternalError(err) } - return p.converter.InstanceRuleToAdminAPIRule(rule), nil + return typeutils.InstanceRuleToAdminAPIRule(rule), nil } // RuleCreate adds a new rule to the instance. @@ -77,7 +78,7 @@ func (p *Processor) RuleCreate(ctx context.Context, form *apimodel.InstanceRuleC return nil, gtserror.NewErrorInternalError(err) } - return p.converter.InstanceRuleToAdminAPIRule(rule), nil + return typeutils.InstanceRuleToAdminAPIRule(rule), nil } // RuleUpdate updates text for an existing rule. @@ -99,7 +100,7 @@ func (p *Processor) RuleUpdate(ctx context.Context, id string, form *apimodel.In return nil, gtserror.NewErrorInternalError(err) } - return p.converter.InstanceRuleToAdminAPIRule(updatedRule), nil + return typeutils.InstanceRuleToAdminAPIRule(updatedRule), nil } // RuleDelete deletes an existing rule. @@ -120,5 +121,5 @@ func (p *Processor) RuleDelete(ctx context.Context, id string) (*apimodel.AdminI return nil, gtserror.NewErrorInternalError(err) } - return p.converter.InstanceRuleToAdminAPIRule(deletedRule), nil + return typeutils.InstanceRuleToAdminAPIRule(deletedRule), nil } diff --git a/internal/processing/common/status.go b/internal/processing/common/status.go index da5cf1290..01f2ab72d 100644 --- a/internal/processing/common/status.go +++ b/internal/processing/common/status.go @@ -31,6 +31,40 @@ "github.com/superseriousbusiness/gotosocial/internal/log" ) +// GetOwnStatus fetches the given status with ID, +// and ensures that it belongs to given requester. +func (p *Processor) GetOwnStatus( + ctx context.Context, + requester *gtsmodel.Account, + targetID string, +) ( + *gtsmodel.Status, + gtserror.WithCode, +) { + target, err := p.state.DB.GetStatusByID(ctx, targetID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("error getting from db: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + switch { + case target == nil: + const text = "target status not found" + return nil, gtserror.NewErrorNotFound( + errors.New(text), + text, + ) + + case target.AccountID != requester.ID: + return nil, gtserror.NewErrorNotFound( + errors.New("status does not belong to requester"), + "target status not found", + ) + } + + return target, nil +} + // GetTargetStatusBy fetches the target status with db load // function, given the authorized (or, nil) requester's // account. This returns an approprate gtserror.WithCode diff --git a/internal/processing/instance.go b/internal/processing/instance.go index a9be6db1d..2f4c40416 100644 --- a/internal/processing/instance.go +++ b/internal/processing/instance.go @@ -29,6 +29,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/text" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/internal/validate" ) @@ -133,7 +134,7 @@ func (p *Processor) InstanceGetRules(ctx context.Context) ([]apimodel.InstanceRu return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance: %s", err)) } - return p.converter.InstanceRulesToAPIRules(i.Rules), nil + return typeutils.InstanceRulesToAPIRules(i.Rules), nil } func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.InstanceV1, gtserror.WithCode) { @@ -227,6 +228,17 @@ func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe columns = append(columns, []string{"description", "description_text"}...) } + // validate & update site custom css if it's set on the form + if form.CustomCSS != nil { + customCSS := *form.CustomCSS + if err := validate.InstanceCustomCSS(customCSS); err != nil { + return nil, gtserror.NewErrorBadRequest(err, err.Error()) + } + + instance.CustomCSS = text.SanitizeToPlaintext(customCSS) + columns = append(columns, []string{"custom_css"}...) + } + // Validate & update site // terms if set on the form. if form.Terms != nil { diff --git a/internal/processing/media/create.go b/internal/processing/media/create.go index ca1f1c3c6..5ea630618 100644 --- a/internal/processing/media/create.go +++ b/internal/processing/media/create.go @@ -25,6 +25,7 @@ "codeberg.org/gruf/go-iotools" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -45,10 +46,21 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form } // Parse focus details from API form input. - focusX, focusY, err := parseFocus(form.Focus) - if err != nil { - text := fmt.Sprintf("could not parse focus value %s: %s", form.Focus, err) - return nil, gtserror.NewErrorBadRequest(errors.New(text), text) + focusX, focusY, errWithCode := apiutil.ParseFocus(form.Focus) + if errWithCode != nil { + return nil, errWithCode + } + + // If description provided, + // process and validate it. + // + // This may not yet be set as it + // is often set on status post. + if form.Description != "" { + form.Description, errWithCode = processDescription(form.Description) + if errWithCode != nil { + return nil, errWithCode + } } // Open multipart file reader. @@ -58,7 +70,7 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form return nil, gtserror.NewErrorInternalError(err) } - // Wrap the multipart file reader to ensure is limited to max. + // Wrap multipart file reader to ensure is limited to max size. rc, _, _ := iotools.UpdateReadCloserLimit(mpfile, maxszInt64) // Create local media and write to instance storage. diff --git a/internal/processing/media/getfile.go b/internal/processing/media/getfile.go index 6962601f2..11d8f7eb5 100644 --- a/internal/processing/media/getfile.go +++ b/internal/processing/media/getfile.go @@ -177,9 +177,7 @@ func (p *Processor) getAttachmentContent( } // Start preparing API content model. - apiContent := &apimodel.Content{ - ContentUpdated: attach.UpdatedAt, - } + apiContent := &apimodel.Content{} // Retrieve appropriate // size file from storage. diff --git a/internal/processing/media/unattach_test.go b/internal/processing/media/unattach_test.go index 051caa4d3..02d2c7077 100644 --- a/internal/processing/media/unattach_test.go +++ b/internal/processing/media/unattach_test.go @@ -20,7 +20,6 @@ import ( "context" "testing" - "time" "github.com/stretchr/testify/suite" ) @@ -42,8 +41,6 @@ func (suite *UnattachTestSuite) TestUnattachMedia() { dbAttachment, errWithCode := suite.db.GetAttachmentByID(ctx, a.ID) suite.NoError(errWithCode) - - suite.WithinDuration(dbAttachment.UpdatedAt, time.Now(), 1*time.Minute) suite.Empty(dbAttachment.StatusID) } diff --git a/internal/processing/media/update.go b/internal/processing/media/update.go index d3a9cfe61..c8592395f 100644 --- a/internal/processing/media/update.go +++ b/internal/processing/media/update.go @@ -23,6 +23,8 @@ "fmt" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -47,17 +49,27 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, media var updatingColumns []string if form.Description != nil { - attachment.Description = text.SanitizeToPlaintext(*form.Description) + // Sanitize and validate incoming description. + description, errWithCode := processDescription( + *form.Description, + ) + if errWithCode != nil { + return nil, errWithCode + } + + attachment.Description = description updatingColumns = append(updatingColumns, "description") } if form.Focus != nil { - focusx, focusy, err := parseFocus(*form.Focus) - if err != nil { - return nil, gtserror.NewErrorBadRequest(err) + // Parse focus details from API form input. + focusX, focusY, errWithCode := apiutil.ParseFocus(*form.Focus) + if errWithCode != nil { + return nil, errWithCode } - attachment.FileMeta.Focus.X = focusx - attachment.FileMeta.Focus.Y = focusy + + attachment.FileMeta.Focus.X = focusX + attachment.FileMeta.Focus.Y = focusY updatingColumns = append(updatingColumns, "focus_x", "focus_y") } @@ -72,3 +84,21 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, media return &a, nil } + +// processDescription will sanitize and valid description against server configuration. +func processDescription(description string) (string, gtserror.WithCode) { + description = text.SanitizeToPlaintext(description) + chars := len([]rune(description)) + + if min := config.GetMediaDescriptionMinChars(); chars < min { + text := fmt.Sprintf("media description less than min chars (%d)", min) + return "", gtserror.NewErrorBadRequest(errors.New(text), text) + } + + if max := config.GetMediaDescriptionMaxChars(); chars > max { + text := fmt.Sprintf("media description exceeds max chars (%d)", max) + return "", gtserror.NewErrorBadRequest(errors.New(text), text) + } + + return description, nil +} diff --git a/internal/processing/media/util.go b/internal/processing/media/util.go deleted file mode 100644 index 0ca2697fd..000000000 --- a/internal/processing/media/util.go +++ /dev/null @@ -1,62 +0,0 @@ -// 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 media - -import ( - "fmt" - "strconv" - "strings" -) - -func parseFocus(focus string) (focusx, focusy float32, err error) { - if focus == "" { - return - } - spl := strings.Split(focus, ",") - if len(spl) != 2 { - err = fmt.Errorf("improperly formatted focus %s", focus) - return - } - xStr := spl[0] - yStr := spl[1] - if xStr == "" || yStr == "" { - err = fmt.Errorf("improperly formatted focus %s", focus) - return - } - fx, err := strconv.ParseFloat(xStr, 32) - if err != nil { - err = fmt.Errorf("improperly formatted focus %s: %s", focus, err) - return - } - if fx > 1 || fx < -1 { - err = fmt.Errorf("improperly formatted focus %s", focus) - return - } - focusx = float32(fx) - fy, err := strconv.ParseFloat(yStr, 32) - if err != nil { - err = fmt.Errorf("improperly formatted focus %s: %s", focus, err) - return - } - if fy > 1 || fy < -1 { - err = fmt.Errorf("improperly formatted focus %s", focus) - return - } - focusy = float32(fy) - return -} diff --git a/internal/processing/status/common.go b/internal/processing/status/common.go new file mode 100644 index 000000000..3f2b7b6cb --- /dev/null +++ b/internal/processing/status/common.go @@ -0,0 +1,351 @@ +// 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 status + +import ( + "context" + "errors" + "fmt" + "time" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/text" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" + "github.com/superseriousbusiness/gotosocial/internal/validate" +) + +// validateStatusContent will validate the common +// content fields across status write endpoints against +// current server configuration (e.g. max char counts). +func validateStatusContent( + status string, + spoiler string, + mediaIDs []string, + poll *apimodel.PollRequest, +) gtserror.WithCode { + totalChars := len([]rune(status)) + + len([]rune(spoiler)) + + if totalChars == 0 && len(mediaIDs) == 0 && poll == nil { + const text = "status contains no text, media or poll" + return gtserror.NewErrorBadRequest(errors.New(text), text) + } + + if max := config.GetStatusesMaxChars(); totalChars > max { + text := fmt.Sprintf("text with spoiler exceed max chars (%d)", max) + return gtserror.NewErrorBadRequest(errors.New(text), text) + } + + if max := config.GetStatusesMediaMaxFiles(); len(mediaIDs) > max { + text := fmt.Sprintf("media files exceed max count (%d)", max) + return gtserror.NewErrorBadRequest(errors.New(text), text) + } + + if poll != nil { + switch max := config.GetStatusesPollMaxOptions(); { + case len(poll.Options) == 0: + const text = "poll cannot have no options" + return gtserror.NewErrorBadRequest(errors.New(text), text) + + case len(poll.Options) > max: + text := fmt.Sprintf("poll options exceed max count (%d)", max) + return gtserror.NewErrorBadRequest(errors.New(text), text) + } + + max := config.GetStatusesPollOptionMaxChars() + for i, option := range poll.Options { + switch l := len([]rune(option)); { + case l == 0: + const text = "poll option cannot be empty" + return gtserror.NewErrorBadRequest(errors.New(text), text) + + case l > max: + text := fmt.Sprintf("poll option %d exceed max chars (%d)", i, max) + return gtserror.NewErrorBadRequest(errors.New(text), text) + } + } + } + + return nil +} + +// statusContent encompasses the set of common processed +// status content fields from status write operations for +// an easily returnable type, without needing to allocate +// an entire gtsmodel.Status{} model. +type statusContent struct { + Content string + ContentWarning string + PollOptions []string + Language string + MentionIDs []string + Mentions []*gtsmodel.Mention + EmojiIDs []string + Emojis []*gtsmodel.Emoji + TagIDs []string + Tags []*gtsmodel.Tag +} + +func (p *Processor) processContent( + ctx context.Context, + author *gtsmodel.Account, + statusID string, + contentType string, + content string, + contentWarning string, + language string, + poll *apimodel.PollRequest, +) ( + *statusContent, + gtserror.WithCode, +) { + if language == "" { + // Ensure we have a status language. + language = author.Settings.Language + if language == "" { + const text = "account default language unset" + return nil, gtserror.NewErrorInternalError( + errors.New(text), + ) + } + } + + var err error + + // Validate + normalize determined language. + language, err = validate.Language(language) + if err != nil { + text := fmt.Sprintf("invalid language tag: %v", err) + return nil, gtserror.NewErrorBadRequest( + errors.New(text), + text, + ) + } + + // format is the currently set text formatting + // function, according to the provided content-type. + var format text.FormatFunc + + if contentType == "" { + // If content type wasn't specified, use + // the author's preferred content-type. + contentType = author.Settings.StatusContentType + } + + switch contentType { + + // Format status according to text/plain. + case "", string(apimodel.StatusContentTypePlain): + format = p.formatter.FromPlain + + // Format status according to text/markdown. + case string(apimodel.StatusContentTypeMarkdown): + format = p.formatter.FromMarkdown + + // Unknown. + default: + const text = "invalid status format" + return nil, gtserror.NewErrorBadRequest( + errors.New(text), + text, + ) + } + + // Allocate a structure to hold the + // majority of formatted content without + // needing to alloc a whole gtsmodel.Status{}. + var status statusContent + status.Language = language + + // formatInput is a shorthand function to format the given input string with the + // currently set 'formatFunc', passing in all required args and returning result. + formatInput := func(formatFunc text.FormatFunc, input string) *text.FormatResult { + return formatFunc(ctx, p.parseMention, author.ID, statusID, input) + } + + // Sanitize input status text and format. + contentRes := formatInput(format, content) + + // Gather results of formatted. + status.Content = contentRes.HTML + status.Mentions = contentRes.Mentions + status.Emojis = contentRes.Emojis + status.Tags = contentRes.Tags + + // From here-on-out just use emoji-only + // plain-text formatting as the FormatFunc. + format = p.formatter.FromPlainEmojiOnly + + // Sanitize content warning and format. + warning := text.SanitizeToPlaintext(contentWarning) + warningRes := formatInput(format, warning) + + // Gather results of the formatted. + status.ContentWarning = warningRes.HTML + status.Emojis = append(status.Emojis, warningRes.Emojis...) + + if poll != nil { + // Pre-allocate slice of poll options of expected length. + status.PollOptions = make([]string, len(poll.Options)) + for i, option := range poll.Options { + + // Sanitize each poll option and format. + option = text.SanitizeToPlaintext(option) + optionRes := formatInput(format, option) + + // Gather results of the formatted. + status.PollOptions[i] = optionRes.HTML + status.Emojis = append(status.Emojis, optionRes.Emojis...) + } + + // Also update options on the form. + poll.Options = status.PollOptions + } + + // We may have received multiple copies of the same emoji, deduplicate these first. + status.Emojis = xslices.DeduplicateFunc(status.Emojis, func(e *gtsmodel.Emoji) string { + return e.ID + }) + + // Gather up the IDs of mentions from parsed content. + status.MentionIDs = xslices.Gather(nil, status.Mentions, + func(m *gtsmodel.Mention) string { + return m.ID + }, + ) + + // Gather up the IDs of tags from parsed content. + status.TagIDs = xslices.Gather(nil, status.Tags, + func(t *gtsmodel.Tag) string { + return t.ID + }, + ) + + // Gather up the IDs of emojis in updated content. + status.EmojiIDs = xslices.Gather(nil, status.Emojis, + func(e *gtsmodel.Emoji) string { + return e.ID + }, + ) + + return &status, nil +} + +func (p *Processor) processMedia( + ctx context.Context, + authorID string, + statusID string, + mediaIDs []string, +) ( + []*gtsmodel.MediaAttachment, + gtserror.WithCode, +) { + // No media provided! + if len(mediaIDs) == 0 { + return nil, nil + } + + // Get configured min/max supported descr chars. + minChars := config.GetMediaDescriptionMinChars() + maxChars := config.GetMediaDescriptionMaxChars() + + // Pre-allocate slice of media attachments of expected length. + attachments := make([]*gtsmodel.MediaAttachment, len(mediaIDs)) + for i, id := range mediaIDs { + + // Look for media attachment by ID in database. + media, err := p.state.DB.GetAttachmentByID(ctx, id) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("error getting media from db: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + // Check media exists and is owned by author + // (this masks finding out media ownership info). + if media == nil || media.AccountID != authorID { + text := fmt.Sprintf("media not found: %s", id) + return nil, gtserror.NewErrorBadRequest(errors.New(text), text) + } + + // Check media isn't already attached to another status. + if (media.StatusID != "" && media.StatusID != statusID) || + (media.ScheduledStatusID != "" && media.ScheduledStatusID != statusID) { + text := fmt.Sprintf("media already attached to status: %s", id) + return nil, gtserror.NewErrorBadRequest(errors.New(text), text) + } + + // Check media description chars within range, + // this needs to be done here as lots of clients + // only update media description on status post. + switch chars := len([]rune(media.Description)); { + case chars < minChars: + text := fmt.Sprintf("media description less than min chars (%d)", minChars) + return nil, gtserror.NewErrorBadRequest(errors.New(text), text) + + case chars > maxChars: + text := fmt.Sprintf("media description exceeds max chars (%d)", maxChars) + return nil, gtserror.NewErrorBadRequest(errors.New(text), text) + } + + // Set media at index. + attachments[i] = media + } + + return attachments, nil +} + +func (p *Processor) processPoll( + ctx context.Context, + statusID string, + form *apimodel.PollRequest, + now time.Time, // used for expiry time +) ( + *gtsmodel.Poll, + gtserror.WithCode, +) { + var expiresAt time.Time + + // Set an expiry time if one given. + if in := form.ExpiresIn; in > 0 { + expiresIn := time.Duration(in) + expiresAt = now.Add(expiresIn * time.Second) + } + + // Create new poll model. + poll := >smodel.Poll{ + ID: id.NewULIDFromTime(now), + Multiple: &form.Multiple, + HideCounts: &form.HideTotals, + Options: form.Options, + StatusID: statusID, + ExpiresAt: expiresAt, + } + + // Insert the newly created poll model in the database. + if err := p.state.DB.PutPoll(ctx, poll); err != nil { + err := gtserror.Newf("error inserting poll in db: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + return poll, nil +} diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go index ef8f8aa56..af9831b9c 100644 --- a/internal/processing/status/create.go +++ b/internal/processing/status/create.go @@ -19,29 +19,22 @@ import ( "context" - "errors" - "fmt" "time" "github.com/superseriousbusiness/gotosocial/internal/ap" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/internal/text" "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. -// -// Precondition: the form's fields should have already been validated and normalized by the caller. +// Note this also handles validation of incoming form field data. func (p *Processor) Create( ctx context.Context, requester *gtsmodel.Account, @@ -51,7 +44,17 @@ func (p *Processor) Create( *apimodel.Status, gtserror.WithCode, ) { - // Ensure account populated; we'll need settings. + // Validate incoming form status content. + if errWithCode := validateStatusContent( + form.Status, + form.SpoilerText, + form.MediaIDs, + form.Poll, + ); errWithCode != nil { + return nil, errWithCode + } + + // Ensure account populated; we'll need their settings. if err := p.state.DB.PopulateAccount(ctx, requester); err != nil { log.Errorf(ctx, "error(s) populating account, will continue: %s", err) } @@ -59,6 +62,30 @@ func (p *Processor) Create( // Generate new ID for status. statusID := id.NewULID() + // Process incoming status content fields. + content, errWithCode := p.processContent(ctx, + requester, + statusID, + string(form.ContentType), + form.Status, + form.SpoilerText, + form.Language, + form.Poll, + ) + if errWithCode != nil { + return nil, errWithCode + } + + // Process incoming status attachments. + media, errWithCode := p.processMedia(ctx, + requester.ID, + statusID, + form.MediaIDs, + ) + if errWithCode != nil { + return nil, errWithCode + } + // Generate necessary URIs for username, to build status URIs. accountURIs := uris.GenerateURIsForAccount(requester.Username) @@ -78,33 +105,36 @@ func (p *Processor) Create( ActivityStreamsType: ap.ObjectNote, Sensitive: &form.Sensitive, CreatedWithApplicationID: application.ID, - Text: form.Status, + + // Set validated language. + Language: content.Language, + + // Set formatted status content. + Content: content.Content, + ContentWarning: content.ContentWarning, + Text: form.Status, // raw + + // Set gathered mentions. + MentionIDs: content.MentionIDs, + Mentions: content.Mentions, + + // Set gathered emojis. + EmojiIDs: content.EmojiIDs, + Emojis: content.Emojis, + + // Set gathered tags. + TagIDs: content.TagIDs, + Tags: content.Tags, + + // Set gathered media. + AttachmentIDs: form.MediaIDs, + Attachments: media, // Assume not pending approval; this may // change when permissivity is checked. PendingApproval: util.Ptr(false), } - if form.Poll != nil { - // Update the status AS type to "Question". - status.ActivityStreamsType = ap.ActivityQuestion - - // Create new poll for status from form. - secs := time.Duration(form.Poll.ExpiresIn) - status.Poll = >smodel.Poll{ - ID: id.NewULID(), - Multiple: &form.Poll.Multiple, - HideCounts: &form.Poll.HideTotals, - Options: form.Poll.Options, - StatusID: statusID, - Status: status, - ExpiresAt: now.Add(secs * time.Second), - } - - // Set poll ID on the status. - status.PollID = status.Poll.ID - } - // Check + attach in-reply-to status. if errWithCode := p.processInReplyTo(ctx, requester, @@ -118,10 +148,6 @@ func (p *Processor) Create( return nil, errWithCode } - if errWithCode := p.processMediaIDs(ctx, form, requester.ID, status); errWithCode != nil { - return nil, errWithCode - } - if err := p.processVisibility(ctx, form, requester.Settings.Privacy, status); err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -132,28 +158,49 @@ func (p *Processor) Create( return nil, errWithCode } - if err := processLanguage(form, requester.Settings.Language, status); err != nil { + if status.ContentWarning != "" && len(status.AttachmentIDs) > 0 { + // If a content-warning is set, and + // the status contains media, always + // set the status sensitive flag. + status.Sensitive = util.Ptr(true) + } + + if form.Poll != nil { + // Process poll, inserting into database. + poll, errWithCode := p.processPoll(ctx, + statusID, + form.Poll, + now, + ) + if errWithCode != nil { + return nil, errWithCode + } + + // Set poll and its ID + // on status before insert. + status.PollID = poll.ID + status.Poll = poll + poll.Status = status + + // Update the status' ActivityPub type to Question. + status.ActivityStreamsType = ap.ActivityQuestion + } + + // Insert this newly prepared status into the database. + if err := p.state.DB.PutStatus(ctx, status); err != nil { + err := gtserror.Newf("error inserting status in db: %w", err) return nil, gtserror.NewErrorInternalError(err) } - if err := p.processContent(ctx, p.parseMention, form, status); err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - if status.Poll != nil { - // Try to insert the new status poll in the database. - if err := p.state.DB.PutPoll(ctx, status.Poll); err != nil { - err := gtserror.Newf("error inserting poll in db: %w", err) - return nil, gtserror.NewErrorInternalError(err) + if status.Poll != nil && !status.Poll.ExpiresAt.IsZero() { + // Now that the status is inserted, attempt to + // schedule an expiry handler for the status poll. + if err := p.polls.ScheduleExpiry(ctx, status.Poll); err != nil { + log.Errorf(ctx, "error scheduling poll expiry: %v", err) } } - // Insert this new status in the database. - if err := p.state.DB.PutStatus(ctx, status); err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - // send it back to the client API worker for async side-effects. + // Send it to the client API worker for async side-effects. p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{ APObjectType: ap.ObjectNote, APActivityType: ap.ActivityCreate, @@ -161,14 +208,6 @@ func (p *Processor) Create( Origin: requester, }) - if status.Poll != nil { - // Now that the status is inserted, and side effects queued, - // attempt to schedule an expiry handler for the status poll. - if err := p.polls.ScheduleExpiry(ctx, status.Poll); err != nil { - log.Errorf(ctx, "error scheduling poll expiry: %v", err) - } - } - // If the new status replies to a status that // replies to us, use our reply as an implicit // accept of any pending interaction. @@ -312,53 +351,6 @@ func (p *Processor) processThreadID(ctx context.Context, status *gtsmodel.Status return nil } -func (p *Processor) processMediaIDs(ctx context.Context, form *apimodel.StatusCreateRequest, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode { - if form.MediaIDs == nil { - return nil - } - - // Get minimum allowed char descriptions. - minChars := config.GetMediaDescriptionMinChars() - - attachments := []*gtsmodel.MediaAttachment{} - attachmentIDs := []string{} - - for _, mediaID := range form.MediaIDs { - attachment, err := p.state.DB.GetAttachmentByID(ctx, mediaID) - if err != nil && !errors.Is(err, db.ErrNoEntries) { - err := gtserror.Newf("error fetching media from db: %w", err) - return gtserror.NewErrorInternalError(err) - } - - if attachment == nil { - text := fmt.Sprintf("media %s not found", mediaID) - return gtserror.NewErrorBadRequest(errors.New(text), text) - } - - if attachment.AccountID != thisAccountID { - text := fmt.Sprintf("media %s does not belong to account", mediaID) - return gtserror.NewErrorBadRequest(errors.New(text), text) - } - - if attachment.StatusID != "" || attachment.ScheduledStatusID != "" { - text := fmt.Sprintf("media %s already attached to status", mediaID) - return gtserror.NewErrorBadRequest(errors.New(text), text) - } - - if length := len([]rune(attachment.Description)); length < minChars { - text := fmt.Sprintf("media %s description too short, at least %d required", mediaID, minChars) - return gtserror.NewErrorBadRequest(errors.New(text), text) - } - - attachments = append(attachments, attachment) - attachmentIDs = append(attachmentIDs, attachment.ID) - } - - status.Attachments = attachments - status.AttachmentIDs = attachmentIDs - return nil -} - func (p *Processor) processVisibility( ctx context.Context, form *apimodel.StatusCreateRequest, @@ -454,99 +446,3 @@ func processInteractionPolicy( // setting it explicitly to save space. return nil } - -func processLanguage(form *apimodel.StatusCreateRequest, accountDefaultLanguage string, status *gtsmodel.Status) error { - if form.Language != "" { - status.Language = form.Language - } else { - status.Language = accountDefaultLanguage - } - if status.Language == "" { - return errors.New("no language given either in status create form or account default") - } - return nil -} - -func (p *Processor) processContent(ctx context.Context, parseMention gtsmodel.ParseMentionFunc, form *apimodel.StatusCreateRequest, status *gtsmodel.Status) error { - if form.ContentType == "" { - // If content type wasn't specified, use the author's preferred content-type. - contentType := apimodel.StatusContentType(status.Account.Settings.StatusContentType) - form.ContentType = contentType - } - - // format is the currently set text formatting - // function, according to the provided content-type. - var format text.FormatFunc - - // formatInput is a shorthand function to format the given input string with the - // currently set 'formatFunc', passing in all required args and returning result. - formatInput := func(formatFunc text.FormatFunc, input string) *text.FormatResult { - return formatFunc(ctx, parseMention, status.AccountID, status.ID, input) - } - - switch form.ContentType { - // None given / set, - // use default (plain). - case "": - fallthrough - - // Format status according to text/plain. - case apimodel.StatusContentTypePlain: - format = p.formatter.FromPlain - - // Format status according to text/markdown. - case apimodel.StatusContentTypeMarkdown: - format = p.formatter.FromMarkdown - - // Unknown. - default: - return fmt.Errorf("invalid status format: %q", form.ContentType) - } - - // Sanitize status text and format. - contentRes := formatInput(format, form.Status) - - // Collect formatted results. - status.Content = contentRes.HTML - status.Mentions = append(status.Mentions, contentRes.Mentions...) - status.Emojis = append(status.Emojis, contentRes.Emojis...) - status.Tags = append(status.Tags, contentRes.Tags...) - - // From here-on-out just use emoji-only - // plain-text formatting as the FormatFunc. - format = p.formatter.FromPlainEmojiOnly - - // Sanitize content warning and format. - spoiler := text.SanitizeToPlaintext(form.SpoilerText) - warningRes := formatInput(format, spoiler) - - // Collect formatted results. - status.ContentWarning = warningRes.HTML - status.Emojis = append(status.Emojis, warningRes.Emojis...) - - if status.Poll != nil { - for i := range status.Poll.Options { - // Sanitize each option title name and format. - option := text.SanitizeToPlaintext(status.Poll.Options[i]) - optionRes := formatInput(format, option) - - // Collect each formatted result. - status.Poll.Options[i] = optionRes.HTML - status.Emojis = append(status.Emojis, optionRes.Emojis...) - } - } - - // Gather all the database IDs from each of the gathered status mentions, tags, and emojis. - 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 - // the status contains media, always - // set the status sensitive flag. - status.Sensitive = util.Ptr(true) - } - - return nil -} diff --git a/internal/processing/status/create_test.go b/internal/processing/status/create_test.go index 84168880e..d0a5c7f92 100644 --- a/internal/processing/status/create_test.go +++ b/internal/processing/status/create_test.go @@ -170,7 +170,7 @@ func (suite *StatusCreateTestSuite) TestProcessMediaDescriptionTooShort() { } apiStatus, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm) - suite.EqualError(err, "media 01F8MH8RMYQ6MSNY3JM2XT1CQ5 description too short, at least 100 required") + suite.EqualError(err, "media description less than min chars (100)") suite.Nil(apiStatus) } diff --git a/internal/processing/status/edit.go b/internal/processing/status/edit.go new file mode 100644 index 000000000..d16092a57 --- /dev/null +++ b/internal/processing/status/edit.go @@ -0,0 +1,555 @@ +// 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 status + +import ( + "context" + "errors" + "fmt" + "slices" + "time" + + "github.com/superseriousbusiness/gotosocial/internal/ap" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/messages" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" +) + +// Edit ... +func (p *Processor) Edit( + ctx context.Context, + requester *gtsmodel.Account, + statusID string, + form *apimodel.StatusEditRequest, +) ( + *apimodel.Status, + gtserror.WithCode, +) { + // Fetch status and ensure it's owned by requesting account. + status, errWithCode := p.c.GetOwnStatus(ctx, requester, statusID) + if errWithCode != nil { + return nil, errWithCode + } + + // Ensure this isn't a boost. + if status.BoostOfID != "" { + return nil, gtserror.NewErrorNotFound( + errors.New("status is a boost wrapper"), + "target status not found", + ) + } + + // Ensure account populated; we'll need their settings. + if err := p.state.DB.PopulateAccount(ctx, requester); err != nil { + log.Errorf(ctx, "error(s) populating account, will continue: %s", err) + } + + // We need the status populated including all historical edits. + if err := p.state.DB.PopulateStatusEdits(ctx, status); err != nil { + err := gtserror.Newf("error getting status edits from db: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + // Time of edit. + now := time.Now() + + // Validate incoming form edit content. + if errWithCode := validateStatusContent( + form.Status, + form.SpoilerText, + form.MediaIDs, + form.Poll, + ); errWithCode != nil { + return nil, errWithCode + } + + // Process incoming status edit content fields. + content, errWithCode := p.processContent(ctx, + requester, + statusID, + string(form.ContentType), + form.Status, + form.SpoilerText, + form.Language, + form.Poll, + ) + if errWithCode != nil { + return nil, errWithCode + } + + // Process new status attachments to use. + media, errWithCode := p.processMedia(ctx, + requester.ID, + statusID, + form.MediaIDs, + ) + if errWithCode != nil { + return nil, errWithCode + } + + // Process incoming edits of any attached media. + mediaEdited, errWithCode := p.processMediaEdits(ctx, + media, + form.MediaAttributes, + ) + if errWithCode != nil { + return nil, errWithCode + } + + // Process incoming edits of any attached status poll. + poll, pollEdited, errWithCode := p.processPollEdit(ctx, + statusID, + status.Poll, + form.Poll, + now, + ) + if errWithCode != nil { + return nil, errWithCode + } + + // Check if new status poll was set. + pollChanged := (poll != status.Poll) + + // Determine whether there were any changes possibly + // causing a change to embedded mentions, tags, emojis. + contentChanged := (status.Content != content.Content) + warningChanged := (status.ContentWarning != content.ContentWarning) + languageChanged := (status.Language != content.Language) + anyContentChanged := contentChanged || warningChanged || + pollEdited // encapsulates pollChanged too + + // Check if status media attachments have changed. + mediaChanged := !slices.Equal(status.AttachmentIDs, + form.MediaIDs, + ) + + // Track status columns we + // need to update in database. + cols := make([]string, 2, 13) + cols[0] = "updated_at" + cols[1] = "edits" + + if contentChanged { + // Update status text. + // + // Note we don't update these + // status fields right away so + // we can save current version. + cols = append(cols, "content") + cols = append(cols, "text") + } + + if warningChanged { + // Update status content warning. + // + // Note we don't update these + // status fields right away so + // we can save current version. + cols = append(cols, "content_warning") + } + + if languageChanged { + // Update status language pref. + // + // Note we don't update these + // status fields right away so + // we can save current version. + cols = append(cols, "language") + } + + if *status.Sensitive != form.Sensitive { + // Update status sensitivity pref. + // + // Note we don't update these + // status fields right away so + // we can save current version. + cols = append(cols, "sensitive") + } + + if mediaChanged { + // Updated status media attachments. + // + // Note we don't update these + // status fields right away so + // we can save current version. + cols = append(cols, "attachments") + } + + if pollChanged { + // Updated attached status poll. + // + // Note we don't update these + // status fields right away so + // we can save current version. + cols = append(cols, "poll_id") + + if status.Poll == nil || poll == nil { + // Went from with-poll to without-poll + // or vice-versa. This changes AP type. + cols = append(cols, "activity_streams_type") + } + } + + if anyContentChanged { + if !slices.Equal(status.MentionIDs, content.MentionIDs) { + // Update attached status mentions. + cols = append(cols, "mentions") + status.MentionIDs = content.MentionIDs + status.Mentions = content.Mentions + } + + if !slices.Equal(status.TagIDs, content.TagIDs) { + // Updated attached status tags. + cols = append(cols, "tags") + status.TagIDs = content.TagIDs + status.Tags = content.Tags + } + + if !slices.Equal(status.EmojiIDs, content.EmojiIDs) { + // We specifically store both *new* AND *old* edit + // revision emojis in the statuses.emojis column. + emojiByID := func(e *gtsmodel.Emoji) string { return e.ID } + status.Emojis = append(status.Emojis, content.Emojis...) + status.Emojis = xslices.DeduplicateFunc(status.Emojis, emojiByID) + status.EmojiIDs = xslices.Gather(status.EmojiIDs[:0], status.Emojis, emojiByID) + + // Update attached status emojis. + cols = append(cols, "emojis") + } + } + + // If no status columns were updated, no media and + // no poll were edited, there's nothing to do! + if len(cols) == 2 && !mediaEdited && !pollEdited { + const text = "status was not changed" + return nil, gtserror.NewErrorUnprocessableEntity( + errors.New(text), + text, + ) + } + + // Create an edit to store a + // historical snapshot of status. + var edit gtsmodel.StatusEdit + edit.ID = id.NewULIDFromTime(now) + edit.Content = status.Content + edit.ContentWarning = status.ContentWarning + edit.Text = status.Text + edit.Language = status.Language + edit.Sensitive = status.Sensitive + edit.StatusID = status.ID + edit.CreatedAt = status.UpdatedAt + + // Copy existing media and descriptions. + edit.AttachmentIDs = status.AttachmentIDs + if l := len(status.Attachments); l > 0 { + edit.AttachmentDescriptions = make([]string, l) + for i, attach := range status.Attachments { + edit.AttachmentDescriptions[i] = attach.Description + } + } + + if status.Poll != nil { + // Poll only set if existed previously. + edit.PollOptions = status.Poll.Options + + if pollChanged || !*status.Poll.HideCounts || + !status.Poll.ClosedAt.IsZero() { + // If the counts are allowed to be + // shown, or poll has changed, then + // include poll vote counts in edit. + edit.PollVotes = status.Poll.Votes + } + } + + // Insert this new edit of existing status into database. + if err := p.state.DB.PutStatusEdit(ctx, &edit); err != nil { + err := gtserror.Newf("error putting edit in database: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + // Add edit to list of edits on the status. + status.EditIDs = append(status.EditIDs, edit.ID) + status.Edits = append(status.Edits, &edit) + + // Now historical status data is stored, + // update the other necessary status fields. + status.Content = content.Content + status.ContentWarning = content.ContentWarning + status.Text = form.Status + status.Language = content.Language + status.Sensitive = &form.Sensitive + status.AttachmentIDs = form.MediaIDs + status.Attachments = media + status.UpdatedAt = now + + if poll != nil { + // Set relevent fields for latest with poll. + status.ActivityStreamsType = ap.ActivityQuestion + status.PollID = poll.ID + status.Poll = poll + } else { + // Set relevant fields for latest without poll. + status.ActivityStreamsType = ap.ObjectNote + status.PollID = "" + status.Poll = nil + } + + // Finally update the existing status model in the database. + if err := p.state.DB.UpdateStatus(ctx, status, cols...); err != nil { + err := gtserror.Newf("error updating status in db: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + if pollChanged && status.Poll != nil && !status.Poll.ExpiresAt.IsZero() { + // Now the status is updated, attempt to schedule + // an expiry handler for the changed status poll. + if err := p.polls.ScheduleExpiry(ctx, status.Poll); err != nil { + log.Errorf(ctx, "error scheduling poll expiry: %v", err) + } + } + + // Send it to the client API worker for async side-effects. + p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{ + APObjectType: ap.ObjectNote, + APActivityType: ap.ActivityUpdate, + GTSModel: status, + Origin: requester, + }) + + // Return an API model of the updated status. + return p.c.GetAPIStatus(ctx, requester, status) +} + +// HistoryGet gets edit history for the target status, taking account of privacy settings and blocks etc. +func (p *Processor) HistoryGet(ctx context.Context, requester *gtsmodel.Account, targetStatusID string) ([]*apimodel.StatusEdit, gtserror.WithCode) { + target, errWithCode := p.c.GetVisibleTargetStatus(ctx, + requester, + targetStatusID, + nil, // default freshness + ) + if errWithCode != nil { + return nil, errWithCode + } + + if err := p.state.DB.PopulateStatusEdits(ctx, target); err != nil { + err := gtserror.Newf("error getting status edits from db: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + edits, err := p.converter.StatusToAPIEdits(ctx, target) + if err != nil { + err := gtserror.Newf("error converting status edits: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + return edits, nil +} + +func (p *Processor) processMediaEdits( + ctx context.Context, + attachs []*gtsmodel.MediaAttachment, + attrs []apimodel.AttachmentAttributesRequest, +) ( + bool, + gtserror.WithCode, +) { + var edited bool + + for _, attr := range attrs { + // Search the media attachments slice for index of media with attr.ID. + i := slices.IndexFunc(attachs, func(m *gtsmodel.MediaAttachment) bool { + return m.ID == attr.ID + }) + if i == -1 { + text := fmt.Sprintf("media not found: %s", attr.ID) + return false, gtserror.NewErrorBadRequest(errors.New(text), text) + } + + // Get attach at index. + attach := attachs[i] + + // Track which columns need + // updating in database query. + cols := make([]string, 0, 2) + + // Check for description change. + if attr.Description != attach.Description { + attach.Description = attr.Description + cols = append(cols, "description") + } + + if attr.Focus != "" { + // Parse provided media focus parameters from string. + fx, fy, errWithCode := apiutil.ParseFocus(attr.Focus) + if errWithCode != nil { + return false, errWithCode + } + + // Check for change in focus coords. + if attach.FileMeta.Focus.X != fx || + attach.FileMeta.Focus.Y != fy { + attach.FileMeta.Focus.X = fx + attach.FileMeta.Focus.Y = fy + cols = append(cols, "focus_x", "focus_y") + } + } + + if len(cols) > 0 { + // Media attachment was changed, update this in database. + err := p.state.DB.UpdateAttachment(ctx, attach, cols...) + if err != nil { + err := gtserror.Newf("error updating attachment in db: %w", err) + return false, gtserror.NewErrorInternalError(err) + } + + // Set edited. + edited = true + } + } + + return edited, nil +} + +func (p *Processor) processPollEdit( + ctx context.Context, + statusID string, + original *gtsmodel.Poll, + form *apimodel.PollRequest, + now time.Time, // used for expiry time +) ( + *gtsmodel.Poll, + bool, + gtserror.WithCode, +) { + if form == nil { + if original != nil { + // No poll was given but there's an existing poll, + // this indicates the original needs to be deleted. + if err := p.deletePoll(ctx, original); err != nil { + return nil, true, gtserror.NewErrorInternalError(err) + } + + // Existing was deleted. + return nil, true, nil + } + + // No change in poll. + return nil, false, nil + } + + switch { + // No existing poll. + case original == nil: + + // Any change that effects voting, i.e. options, allow multiple + // or re-opening a closed poll requires deleting the existing poll. + case !slices.Equal(form.Options, original.Options) || + (form.Multiple != *original.Multiple) || + (!original.ClosedAt.IsZero() && form.ExpiresIn != 0): + if err := p.deletePoll(ctx, original); err != nil { + return nil, true, gtserror.NewErrorInternalError(err) + } + + // Any other changes only require a model + // update, and at-most a new expiry handler. + default: + var cols []string + + // Check if the hide counts field changed. + if form.HideTotals != *original.HideCounts { + cols = append(cols, "hide_counts") + original.HideCounts = &form.HideTotals + } + + var expiresAt time.Time + + // Determine expiry time if given. + if in := form.ExpiresIn; in > 0 { + expiresIn := time.Duration(in) + expiresAt = now.Add(expiresIn * time.Second) + } + + // Check for expiry time. + if !expiresAt.IsZero() { + + if !original.ExpiresAt.IsZero() { + // Existing had expiry, cancel scheduled handler. + _ = p.state.Workers.Scheduler.Cancel(original.ID) + } + + // Since expiry is given as a duration + // we always treat > 0 as a change as + // we can't know otherwise unfortunately. + cols = append(cols, "expires_at") + original.ExpiresAt = expiresAt + } + + if len(cols) == 0 { + // Were no changes to poll. + return original, false, nil + } + + // Update the original poll model in the database with these columns. + if err := p.state.DB.UpdatePoll(ctx, original, cols...); err != nil { + err := gtserror.Newf("error updating poll.expires_at in db: %w", err) + return nil, true, gtserror.NewErrorInternalError(err) + } + + if !expiresAt.IsZero() { + // Updated poll has an expiry, schedule a new expiry handler. + if err := p.polls.ScheduleExpiry(ctx, original); err != nil { + log.Errorf(ctx, "error scheduling poll expiry: %v", err) + } + } + + // Existing poll was updated. + return original, true, nil + } + + // If we reached here then an entirely + // new status poll needs to be created. + poll, errWithCode := p.processPoll(ctx, + statusID, + form, + now, + ) + return poll, true, errWithCode +} + +func (p *Processor) deletePoll(ctx context.Context, poll *gtsmodel.Poll) error { + if !poll.ExpiresAt.IsZero() && !poll.ClosedAt.IsZero() { + // Poll has an expiry and has not yet closed, + // cancel any expiry handler before deletion. + _ = p.state.Workers.Scheduler.Cancel(poll.ID) + } + + // Delete the given poll from the database. + err := p.state.DB.DeletePollByID(ctx, poll.ID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return gtserror.Newf("error deleting poll from db: %w", err) + } + + return nil +} diff --git a/internal/processing/status/edit_test.go b/internal/processing/status/edit_test.go new file mode 100644 index 000000000..393c3efc2 --- /dev/null +++ b/internal/processing/status/edit_test.go @@ -0,0 +1,544 @@ +// 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 status_test + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/suite" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/util/xslices" +) + +type StatusEditTestSuite struct { + StatusStandardTestSuite +} + +func (suite *StatusEditTestSuite) TestSimpleEdit() { + // Create cancellable context to use for test. + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + // Get a local account to use as test requester. + requester := suite.testAccounts["local_account_1"] + requester, _ = suite.state.DB.GetAccountByID(ctx, requester.ID) + + // Get requester's existing status to perform an edit on. + status := suite.testStatuses["local_account_1_status_9"] + status, _ = suite.state.DB.GetStatusByID(ctx, status.ID) + + // Prepare a simple status edit. + form := &apimodel.StatusEditRequest{ + Status: "

this is some edited status text!

", + SpoilerText: "shhhhh", + Sensitive: true, + Language: "fr", // hoh hoh hoh + MediaIDs: nil, + MediaAttributes: nil, + Poll: nil, + } + + // Pass the prepared form to the status processor to perform the edit. + apiStatus, errWithCode := suite.status.Edit(ctx, requester, status.ID, form) + suite.NotNil(apiStatus) + suite.NoError(errWithCode) + + // Check response against input form data. + suite.Equal(form.Status, apiStatus.Text) + suite.Equal(form.SpoilerText, apiStatus.SpoilerText) + suite.Equal(form.Sensitive, apiStatus.Sensitive) + suite.Equal(form.Language, *apiStatus.Language) + suite.NotEqual(util.FormatISO8601(status.UpdatedAt), *apiStatus.EditedAt) + + // Fetched the latest version of edited status from the database. + latestStatus, err := suite.state.DB.GetStatusByID(ctx, status.ID) + suite.NoError(err) + + // Check latest status against input form data. + suite.Equal(form.Status, latestStatus.Text) + suite.Equal(form.SpoilerText, latestStatus.ContentWarning) + suite.Equal(form.Sensitive, *latestStatus.Sensitive) + suite.Equal(form.Language, latestStatus.Language) + suite.Equal(len(status.EditIDs)+1, len(latestStatus.EditIDs)) + suite.NotEqual(status.UpdatedAt, latestStatus.UpdatedAt) + + // Populate all historical edits for this status. + err = suite.state.DB.PopulateStatusEdits(ctx, latestStatus) + suite.NoError(err) + + // Check previous status edit matches original status content. + previousEdit := latestStatus.Edits[len(latestStatus.Edits)-1] + suite.Equal(status.Content, previousEdit.Content) + suite.Equal(status.Text, previousEdit.Text) + suite.Equal(status.ContentWarning, previousEdit.ContentWarning) + suite.Equal(*status.Sensitive, *previousEdit.Sensitive) + suite.Equal(status.Language, previousEdit.Language) + suite.Equal(status.UpdatedAt, previousEdit.CreatedAt) +} + +func (suite *StatusEditTestSuite) TestEditAddPoll() { + // Create cancellable context to use for test. + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + // Get a local account to use as test requester. + requester := suite.testAccounts["local_account_1"] + requester, _ = suite.state.DB.GetAccountByID(ctx, requester.ID) + + // Get requester's existing status to perform an edit on. + status := suite.testStatuses["local_account_1_status_9"] + status, _ = suite.state.DB.GetStatusByID(ctx, status.ID) + + // Prepare edit adding a status poll. + form := &apimodel.StatusEditRequest{ + Status: "

this is some edited status text!

", + SpoilerText: "", + Sensitive: true, + Language: "fr", // hoh hoh hoh + MediaIDs: nil, + MediaAttributes: nil, + Poll: &apimodel.PollRequest{ + Options: []string{"yes", "no", "spiderman"}, + ExpiresIn: int(time.Minute), + Multiple: true, + HideTotals: false, + }, + } + + // Pass the prepared form to the status processor to perform the edit. + apiStatus, errWithCode := suite.status.Edit(ctx, requester, status.ID, form) + suite.NotNil(apiStatus) + suite.NoError(errWithCode) + + // Check response against input form data. + suite.Equal(form.Status, apiStatus.Text) + suite.Equal(form.SpoilerText, apiStatus.SpoilerText) + suite.Equal(form.Sensitive, apiStatus.Sensitive) + suite.Equal(form.Language, *apiStatus.Language) + suite.NotEqual(util.FormatISO8601(status.UpdatedAt), *apiStatus.EditedAt) + suite.NotNil(apiStatus.Poll) + suite.Equal(form.Poll.Options, xslices.Gather(nil, apiStatus.Poll.Options, func(opt apimodel.PollOption) string { + return opt.Title + })) + + // Fetched the latest version of edited status from the database. + latestStatus, err := suite.state.DB.GetStatusByID(ctx, status.ID) + suite.NoError(err) + + // Check latest status against input form data. + suite.Equal(form.Status, latestStatus.Text) + suite.Equal(form.SpoilerText, latestStatus.ContentWarning) + suite.Equal(form.Sensitive, *latestStatus.Sensitive) + suite.Equal(form.Language, latestStatus.Language) + suite.Equal(len(status.EditIDs)+1, len(latestStatus.EditIDs)) + suite.NotEqual(status.UpdatedAt, latestStatus.UpdatedAt) + suite.NotNil(latestStatus.Poll) + suite.Equal(form.Poll.Options, latestStatus.Poll.Options) + + // Ensure that a poll expiry handler was scheduled on status edit. + expiryWorker := suite.state.Workers.Scheduler.Cancel(latestStatus.PollID) + suite.Equal(form.Poll.ExpiresIn > 0, expiryWorker) + + // Populate all historical edits for this status. + err = suite.state.DB.PopulateStatusEdits(ctx, latestStatus) + suite.NoError(err) + + // Check previous status edit matches original status content. + previousEdit := latestStatus.Edits[len(latestStatus.Edits)-1] + suite.Equal(status.Content, previousEdit.Content) + suite.Equal(status.Text, previousEdit.Text) + suite.Equal(status.ContentWarning, previousEdit.ContentWarning) + suite.Equal(*status.Sensitive, *previousEdit.Sensitive) + suite.Equal(status.Language, previousEdit.Language) + suite.Equal(status.UpdatedAt, previousEdit.CreatedAt) + suite.Equal(status.Poll != nil, len(previousEdit.PollOptions) > 0) +} + +func (suite *StatusEditTestSuite) TestEditAddPollNoExpiry() { + // Create cancellable context to use for test. + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + // Get a local account to use as test requester. + requester := suite.testAccounts["local_account_1"] + requester, _ = suite.state.DB.GetAccountByID(ctx, requester.ID) + + // Get requester's existing status to perform an edit on. + status := suite.testStatuses["local_account_1_status_9"] + status, _ = suite.state.DB.GetStatusByID(ctx, status.ID) + + // Prepare edit adding an endless poll. + form := &apimodel.StatusEditRequest{ + Status: "

this is some edited status text!

", + SpoilerText: "", + Sensitive: true, + Language: "fr", // hoh hoh hoh + MediaIDs: nil, + MediaAttributes: nil, + Poll: &apimodel.PollRequest{ + Options: []string{"yes", "no", "spiderman"}, + ExpiresIn: 0, + Multiple: true, + HideTotals: false, + }, + } + + // Pass the prepared form to the status processor to perform the edit. + apiStatus, errWithCode := suite.status.Edit(ctx, requester, status.ID, form) + suite.NotNil(apiStatus) + suite.NoError(errWithCode) + + // Check response against input form data. + suite.Equal(form.Status, apiStatus.Text) + suite.Equal(form.SpoilerText, apiStatus.SpoilerText) + suite.Equal(form.Sensitive, apiStatus.Sensitive) + suite.Equal(form.Language, *apiStatus.Language) + suite.NotEqual(util.FormatISO8601(status.UpdatedAt), *apiStatus.EditedAt) + suite.NotNil(apiStatus.Poll) + suite.Equal(form.Poll.Options, xslices.Gather(nil, apiStatus.Poll.Options, func(opt apimodel.PollOption) string { + return opt.Title + })) + + // Fetched the latest version of edited status from the database. + latestStatus, err := suite.state.DB.GetStatusByID(ctx, status.ID) + suite.NoError(err) + + // Check latest status against input form data. + suite.Equal(form.Status, latestStatus.Text) + suite.Equal(form.SpoilerText, latestStatus.ContentWarning) + suite.Equal(form.Sensitive, *latestStatus.Sensitive) + suite.Equal(form.Language, latestStatus.Language) + suite.Equal(len(status.EditIDs)+1, len(latestStatus.EditIDs)) + suite.NotEqual(status.UpdatedAt, latestStatus.UpdatedAt) + suite.NotNil(latestStatus.Poll) + suite.Equal(form.Poll.Options, latestStatus.Poll.Options) + + // Ensure that a poll expiry handler was *not* scheduled on status edit. + expiryWorker := suite.state.Workers.Scheduler.Cancel(latestStatus.PollID) + suite.Equal(form.Poll.ExpiresIn > 0, expiryWorker) + + // Populate all historical edits for this status. + err = suite.state.DB.PopulateStatusEdits(ctx, latestStatus) + suite.NoError(err) + + // Check previous status edit matches original status content. + previousEdit := latestStatus.Edits[len(latestStatus.Edits)-1] + suite.Equal(status.Content, previousEdit.Content) + suite.Equal(status.Text, previousEdit.Text) + suite.Equal(status.ContentWarning, previousEdit.ContentWarning) + suite.Equal(*status.Sensitive, *previousEdit.Sensitive) + suite.Equal(status.Language, previousEdit.Language) + suite.Equal(status.UpdatedAt, previousEdit.CreatedAt) + suite.Equal(status.Poll != nil, len(previousEdit.PollOptions) > 0) +} + +func (suite *StatusEditTestSuite) TestEditMediaDescription() { + // Create cancellable context to use for test. + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + // Get a local account to use as test requester. + requester := suite.testAccounts["local_account_1"] + requester, _ = suite.state.DB.GetAccountByID(ctx, requester.ID) + + // Get requester's existing status to perform an edit on. + status := suite.testStatuses["local_account_1_status_4"] + status, _ = suite.state.DB.GetStatusByID(ctx, status.ID) + + // Prepare edit changing media description. + form := &apimodel.StatusEditRequest{ + Status: "

this is some edited status text!

", + SpoilerText: "this status is now missing media", + Sensitive: true, + Language: "en", + MediaIDs: status.AttachmentIDs, + MediaAttributes: []apimodel.AttachmentAttributesRequest{ + {ID: status.AttachmentIDs[0], Description: "hello world!"}, + {ID: status.AttachmentIDs[1], Description: "media attachment numero two"}, + }, + } + + // Pass the prepared form to the status processor to perform the edit. + apiStatus, errWithCode := suite.status.Edit(ctx, requester, status.ID, form) + suite.NoError(errWithCode) + + // Check response against input form data. + suite.Equal(form.Status, apiStatus.Text) + suite.Equal(form.SpoilerText, apiStatus.SpoilerText) + suite.Equal(form.Sensitive, apiStatus.Sensitive) + suite.Equal(form.Language, *apiStatus.Language) + suite.NotEqual(util.FormatISO8601(status.UpdatedAt), *apiStatus.EditedAt) + suite.Equal(form.MediaIDs, xslices.Gather(nil, apiStatus.MediaAttachments, func(media *apimodel.Attachment) string { + return media.ID + })) + suite.Equal( + xslices.Gather(nil, form.MediaAttributes, func(attr apimodel.AttachmentAttributesRequest) string { + return attr.Description + }), + xslices.Gather(nil, apiStatus.MediaAttachments, func(media *apimodel.Attachment) string { + return *media.Description + }), + ) + + // Fetched the latest version of edited status from the database. + latestStatus, err := suite.state.DB.GetStatusByID(ctx, status.ID) + suite.NoError(err) + + // Check latest status against input form data. + suite.Equal(form.Status, latestStatus.Text) + suite.Equal(form.SpoilerText, latestStatus.ContentWarning) + suite.Equal(form.Sensitive, *latestStatus.Sensitive) + suite.Equal(form.Language, latestStatus.Language) + suite.Equal(len(status.EditIDs)+1, len(latestStatus.EditIDs)) + suite.NotEqual(status.UpdatedAt, latestStatus.UpdatedAt) + suite.Equal(form.MediaIDs, latestStatus.AttachmentIDs) + suite.Equal( + xslices.Gather(nil, form.MediaAttributes, func(attr apimodel.AttachmentAttributesRequest) string { + return attr.Description + }), + xslices.Gather(nil, latestStatus.Attachments, func(media *gtsmodel.MediaAttachment) string { + return media.Description + }), + ) + + // Populate all historical edits for this status. + err = suite.state.DB.PopulateStatusEdits(ctx, latestStatus) + suite.NoError(err) + + // Further populate edits to get attachments. + for _, edit := range latestStatus.Edits { + err = suite.state.DB.PopulateStatusEdit(ctx, edit) + suite.NoError(err) + } + + // Check previous status edit matches original status content. + previousEdit := latestStatus.Edits[len(latestStatus.Edits)-1] + suite.Equal(status.Content, previousEdit.Content) + suite.Equal(status.Text, previousEdit.Text) + suite.Equal(status.ContentWarning, previousEdit.ContentWarning) + suite.Equal(*status.Sensitive, *previousEdit.Sensitive) + suite.Equal(status.Language, previousEdit.Language) + suite.Equal(status.UpdatedAt, previousEdit.CreatedAt) + suite.Equal(status.AttachmentIDs, previousEdit.AttachmentIDs) + suite.Equal( + xslices.Gather(nil, status.Attachments, func(media *gtsmodel.MediaAttachment) string { + return media.Description + }), + previousEdit.AttachmentDescriptions, + ) +} + +func (suite *StatusEditTestSuite) TestEditAddMedia() { + // Create cancellable context to use for test. + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + // Get a local account to use as test requester. + requester := suite.testAccounts["local_account_1"] + requester, _ = suite.state.DB.GetAccountByID(ctx, requester.ID) + + // Get some of requester's existing media, and unattach from existing status. + media1 := suite.testAttachments["local_account_1_status_4_attachment_1"] + media2 := suite.testAttachments["local_account_1_status_4_attachment_2"] + media1.StatusID, media2.StatusID = "", "" + suite.NoError(suite.state.DB.UpdateAttachment(ctx, media1, "status_id")) + suite.NoError(suite.state.DB.UpdateAttachment(ctx, media2, "status_id")) + media1, _ = suite.state.DB.GetAttachmentByID(ctx, media1.ID) + media2, _ = suite.state.DB.GetAttachmentByID(ctx, media2.ID) + + // Get requester's existing status to perform an edit on. + status := suite.testStatuses["local_account_1_status_9"] + status, _ = suite.state.DB.GetStatusByID(ctx, status.ID) + + // Prepare edit addding status media. + form := &apimodel.StatusEditRequest{ + Status: "

this is some edited status text!

", + SpoilerText: "this status now has media", + Sensitive: true, + Language: "en", + MediaIDs: []string{media1.ID, media2.ID}, + MediaAttributes: nil, + } + + // Pass the prepared form to the status processor to perform the edit. + apiStatus, errWithCode := suite.status.Edit(ctx, requester, status.ID, form) + suite.NotNil(apiStatus) + suite.NoError(errWithCode) + + // Check response against input form data. + suite.Equal(form.Status, apiStatus.Text) + suite.Equal(form.SpoilerText, apiStatus.SpoilerText) + suite.Equal(form.Sensitive, apiStatus.Sensitive) + suite.Equal(form.Language, *apiStatus.Language) + suite.NotEqual(util.FormatISO8601(status.UpdatedAt), *apiStatus.EditedAt) + suite.Equal(form.MediaIDs, xslices.Gather(nil, apiStatus.MediaAttachments, func(media *apimodel.Attachment) string { + return media.ID + })) + + // Fetched the latest version of edited status from the database. + latestStatus, err := suite.state.DB.GetStatusByID(ctx, status.ID) + suite.NoError(err) + + // Check latest status against input form data. + suite.Equal(form.Status, latestStatus.Text) + suite.Equal(form.SpoilerText, latestStatus.ContentWarning) + suite.Equal(form.Sensitive, *latestStatus.Sensitive) + suite.Equal(form.Language, latestStatus.Language) + suite.Equal(len(status.EditIDs)+1, len(latestStatus.EditIDs)) + suite.NotEqual(status.UpdatedAt, latestStatus.UpdatedAt) + suite.Equal(form.MediaIDs, latestStatus.AttachmentIDs) + + // Populate all historical edits for this status. + err = suite.state.DB.PopulateStatusEdits(ctx, latestStatus) + suite.NoError(err) + + // Check previous status edit matches original status content. + previousEdit := latestStatus.Edits[len(latestStatus.Edits)-1] + suite.Equal(status.Content, previousEdit.Content) + suite.Equal(status.Text, previousEdit.Text) + suite.Equal(status.ContentWarning, previousEdit.ContentWarning) + suite.Equal(*status.Sensitive, *previousEdit.Sensitive) + suite.Equal(status.Language, previousEdit.Language) + suite.Equal(status.UpdatedAt, previousEdit.CreatedAt) + suite.Equal(status.AttachmentIDs, previousEdit.AttachmentIDs) +} + +func (suite *StatusEditTestSuite) TestEditRemoveMedia() { + // Create cancellable context to use for test. + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + // Get a local account to use as test requester. + requester := suite.testAccounts["local_account_1"] + requester, _ = suite.state.DB.GetAccountByID(ctx, requester.ID) + + // Get requester's existing status to perform an edit on. + status := suite.testStatuses["local_account_1_status_4"] + status, _ = suite.state.DB.GetStatusByID(ctx, status.ID) + + // Prepare edit removing status media. + form := &apimodel.StatusEditRequest{ + Status: "

this is some edited status text!

", + SpoilerText: "this status is now missing media", + Sensitive: true, + Language: "en", + MediaIDs: nil, + MediaAttributes: nil, + } + + // Pass the prepared form to the status processor to perform the edit. + apiStatus, errWithCode := suite.status.Edit(ctx, requester, status.ID, form) + suite.NotNil(apiStatus) + suite.NoError(errWithCode) + + // Check response against input form data. + suite.Equal(form.Status, apiStatus.Text) + suite.Equal(form.SpoilerText, apiStatus.SpoilerText) + suite.Equal(form.Sensitive, apiStatus.Sensitive) + suite.Equal(form.Language, *apiStatus.Language) + suite.NotEqual(util.FormatISO8601(status.UpdatedAt), *apiStatus.EditedAt) + suite.Equal(form.MediaIDs, xslices.Gather(nil, apiStatus.MediaAttachments, func(media *apimodel.Attachment) string { + return media.ID + })) + + // Fetched the latest version of edited status from the database. + latestStatus, err := suite.state.DB.GetStatusByID(ctx, status.ID) + suite.NoError(err) + + // Check latest status against input form data. + suite.Equal(form.Status, latestStatus.Text) + suite.Equal(form.SpoilerText, latestStatus.ContentWarning) + suite.Equal(form.Sensitive, *latestStatus.Sensitive) + suite.Equal(form.Language, latestStatus.Language) + suite.Equal(len(status.EditIDs)+1, len(latestStatus.EditIDs)) + suite.NotEqual(status.UpdatedAt, latestStatus.UpdatedAt) + suite.Equal(form.MediaIDs, latestStatus.AttachmentIDs) + + // Populate all historical edits for this status. + err = suite.state.DB.PopulateStatusEdits(ctx, latestStatus) + suite.NoError(err) + + // Check previous status edit matches original status content. + previousEdit := latestStatus.Edits[len(latestStatus.Edits)-1] + suite.Equal(status.Content, previousEdit.Content) + suite.Equal(status.Text, previousEdit.Text) + suite.Equal(status.ContentWarning, previousEdit.ContentWarning) + suite.Equal(*status.Sensitive, *previousEdit.Sensitive) + suite.Equal(status.Language, previousEdit.Language) + suite.Equal(status.UpdatedAt, previousEdit.CreatedAt) + suite.Equal(status.AttachmentIDs, previousEdit.AttachmentIDs) +} + +func (suite *StatusEditTestSuite) TestEditOthersStatus1() { + // Create cancellable context to use for test. + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + // Get a local account to use as test requester. + requester := suite.testAccounts["local_account_1"] + requester, _ = suite.state.DB.GetAccountByID(ctx, requester.ID) + + // Get remote accounts's status to attempt an edit on. + status := suite.testStatuses["remote_account_1_status_1"] + status, _ = suite.state.DB.GetStatusByID(ctx, status.ID) + + // Prepare an empty request form, this + // should be all we need to trigger it. + form := &apimodel.StatusEditRequest{} + + // Attempt to edit other remote account's status, this should return an error. + apiStatus, errWithCode := suite.status.Edit(ctx, requester, status.ID, form) + suite.Nil(apiStatus) + suite.Equal(http.StatusNotFound, errWithCode.Code()) + suite.Equal("status does not belong to requester", errWithCode.Error()) + suite.Equal("Not Found: target status not found", errWithCode.Safe()) +} + +func (suite *StatusEditTestSuite) TestEditOthersStatus2() { + // Create cancellable context to use for test. + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + // Get a local account to use as test requester. + requester := suite.testAccounts["local_account_1"] + requester, _ = suite.state.DB.GetAccountByID(ctx, requester.ID) + + // Get other local accounts's status to attempt edit on. + status := suite.testStatuses["local_account_2_status_1"] + status, _ = suite.state.DB.GetStatusByID(ctx, status.ID) + + // Prepare an empty request form, this + // should be all we need to trigger it. + form := &apimodel.StatusEditRequest{} + + // Attempt to edit other local account's status, this should return an error. + apiStatus, errWithCode := suite.status.Edit(ctx, requester, status.ID, form) + suite.Nil(apiStatus) + suite.Equal(http.StatusNotFound, errWithCode.Code()) + suite.Equal("status does not belong to requester", errWithCode.Error()) + suite.Equal("Not Found: target status not found", errWithCode.Safe()) +} + +func TestStatusEditTestSuite(t *testing.T) { + suite.Run(t, new(StatusEditTestSuite)) +} diff --git a/internal/processing/status/get.go b/internal/processing/status/get.go index 75a687db2..812f01683 100644 --- a/internal/processing/status/get.go +++ b/internal/processing/status/get.go @@ -19,47 +19,16 @@ import ( "context" + "errors" 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" ) -// HistoryGet gets edit history for the target status, taking account of privacy settings and blocks etc. -// TODO: currently this just returns the latest version of the status. -func (p *Processor) HistoryGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.StatusEdit, gtserror.WithCode) { - targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, - requestingAccount, - targetStatusID, - nil, // default freshness - ) - if errWithCode != nil { - return nil, errWithCode - } - - apiStatus, errWithCode := p.c.GetAPIStatus(ctx, requestingAccount, targetStatus) - if errWithCode != nil { - return nil, errWithCode - } - - return []*apimodel.StatusEdit{ - { - Content: apiStatus.Content, - SpoilerText: apiStatus.SpoilerText, - Sensitive: apiStatus.Sensitive, - CreatedAt: util.FormatISO8601(targetStatus.UpdatedAt), - Account: apiStatus.Account, - Poll: apiStatus.Poll, - MediaAttachments: apiStatus.MediaAttachments, - Emojis: apiStatus.Emojis, - }, - }, nil -} - // Get gets the given status, taking account of privacy settings and blocks etc. func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, + target, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID, nil, // default freshness @@ -67,44 +36,25 @@ func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account if errWithCode != nil { return nil, errWithCode } - - return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus) + return p.c.GetAPIStatus(ctx, requestingAccount, target) } // SourceGet returns the *apimodel.StatusSource version of the targetStatusID. // Status must belong to the requester, and must not be a boost. -func (p *Processor) SourceGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.StatusSource, gtserror.WithCode) { - targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, - requestingAccount, - targetStatusID, - nil, // default freshness - ) +func (p *Processor) SourceGet(ctx context.Context, requester *gtsmodel.Account, statusID string) (*apimodel.StatusSource, gtserror.WithCode) { + status, errWithCode := p.c.GetOwnStatus(ctx, requester, statusID) if errWithCode != nil { return nil, errWithCode } - - // Redirect to wrapped status if boost. - targetStatus, errWithCode = p.c.UnwrapIfBoost( - ctx, - requestingAccount, - targetStatus, - ) - if errWithCode != nil { - return nil, errWithCode - } - - if targetStatus.AccountID != requestingAccount.ID { - err := gtserror.Newf( - "status %s does not belong to account %s", - targetStatusID, requestingAccount.ID, + if status.BoostOfID != "" { + return nil, gtserror.NewErrorNotFound( + errors.New("status is a boost wrapper"), + "target status not found", ) - return nil, gtserror.NewErrorNotFound(err) } - - statusSource, err := p.converter.StatusToAPIStatusSource(ctx, targetStatus) - if err != nil { - err = gtserror.Newf("error converting status: %w", err) - return nil, gtserror.NewErrorInternalError(err) - } - return statusSource, nil + return &apimodel.StatusSource{ + ID: status.ID, + Text: status.Text, + SpoilerText: status.ContentWarning, + }, nil } diff --git a/internal/processing/stream/notification_test.go b/internal/processing/stream/notification_test.go index 169e4f5ce..5c89e1f40 100644 --- a/internal/processing/stream/notification_test.go +++ b/internal/processing/stream/notification_test.go @@ -79,8 +79,8 @@ func (suite *NotificationTestSuite) TestStreamNotification() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } diff --git a/internal/processing/stream/statusupdate_test.go b/internal/processing/stream/statusupdate_test.go index b61a9c623..6bf5e436c 100644 --- a/internal/processing/stream/statusupdate_test.go +++ b/internal/processing/stream/statusupdate_test.go @@ -54,6 +54,7 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() { suite.Equal(`{ "id": "01FVW7JHQFSFK166WWKR8CBA6M", "created_at": "2021-09-20T10:40:37.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": false, @@ -90,8 +91,8 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] }, diff --git a/internal/processing/timeline/public_test.go b/internal/processing/timeline/public_test.go index 6b01c9849..ab8e33429 100644 --- a/internal/processing/timeline/public_test.go +++ b/internal/processing/timeline/public_test.go @@ -102,8 +102,8 @@ func (suite *PublicTestSuite) TestPublicTimelineGetHideFiltered() { requester = suite.testAccounts["local_account_1"] maxID = "" sinceID = "" - minID = "01F8MHAAY43M6RJ473VQFCVH36" // 1 before filteredStatus - limit = 10 + minID = "" + limit = 100 local = false filteredStatus = suite.testStatuses["admin_account_status_2"] filteredStatusFound = false diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go index 0d6ec1836..096e285f6 100644 --- a/internal/processing/workers/fromfediapi.go +++ b/internal/processing/workers/fromfediapi.go @@ -762,7 +762,7 @@ func (p *fediAPI) UpdateAccount(ctx context.Context, fMsg *messages.FromFediAPI) account, apubAcc, - // Force refresh within 10s window. + // Force refresh within 5s window. // // Missing account updates could be // detrimental to federation if they @@ -917,17 +917,25 @@ func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg *messages.FromFediAPI) return gtserror.Newf("cannot cast %T -> *gtsmodel.Status", fMsg.GTSModel) } + var freshness *dereferencing.FreshnessWindow + // Cast the updated ActivityPub statusable object . apStatus, _ := fMsg.APObject.(ap.Statusable) + if apStatus != nil { + // If an AP object was provided, we + // allow very fast refreshes that likely + // indicate a status edit after post. + freshness = dereferencing.Freshest + } + // Fetch up-to-date attach status attachments, etc. status, _, err := p.federate.RefreshStatus( ctx, fMsg.Receiving.Username, existing, apStatus, - // Force refresh within 5min window. - dereferencing.Fresh, + freshness, ) if err != nil { log.Errorf(ctx, "error refreshing status: %v", err) diff --git a/internal/processing/workers/util.go b/internal/processing/workers/util.go index 62ea6c95c..b358dc951 100644 --- a/internal/processing/workers/util.go +++ b/internal/processing/workers/util.go @@ -75,6 +75,21 @@ func (u *utils) wipeStatus( } } + // Before handling media, ensure + // historic edits are populated. + if !status.EditsPopulated() { + var err error + + // Fetch all historical edits of status from database. + status.Edits, err = u.state.DB.GetStatusEditsByIDs( + gtscontext.SetBarebones(ctx), + status.EditIDs, + ) + if err != nil { + errs.Appendf("error getting status edits from database: %w", err) + } + } + // Either delete all attachments for this status, // or simply detach + clean them separately later. // @@ -83,20 +98,27 @@ func (u *utils) wipeStatus( // status immediately (in case of delete + redraft). if deleteAttachments { // todo:u.state.DB.DeleteAttachmentsForStatus - for _, id := range status.AttachmentIDs { + for _, id := range status.AllAttachmentIDs() { if err := u.media.Delete(ctx, id); err != nil { errs.Appendf("error deleting media: %w", err) } } } else { // todo:u.state.DB.UnattachAttachmentsForStatus - for _, id := range status.AttachmentIDs { + for _, id := range status.AllAttachmentIDs() { if _, err := u.media.Unattach(ctx, status.Account, id); err != nil { errs.Appendf("error unattaching media: %w", err) } } } + // Delete all historical edits of status. + if ids := status.EditIDs; len(ids) > 0 { + if err := u.state.DB.DeleteStatusEdits(ctx, ids); err != nil { + errs.Appendf("error deleting status edits: %w", err) + } + } + // Delete all mentions generated by this status. // todo:u.state.DB.DeleteMentionsForStatus for _, id := range status.MentionIDs { @@ -120,19 +142,20 @@ func (u *utils) wipeStatus( errs.Appendf("error deleting status faves: %w", err) } - if pollID := status.PollID; pollID != "" { + if id := status.PollID; id != "" { // Delete this poll by ID from the database. - if err := u.state.DB.DeletePollByID(ctx, pollID); err != nil { + if err := u.state.DB.DeletePollByID(ctx, id); err != nil { errs.Appendf("error deleting status poll: %w", err) } // Cancel any scheduled expiry task for poll. - _ = u.state.Workers.Scheduler.Cancel(pollID) + _ = u.state.Workers.Scheduler.Cancel(id) } // Get all boost of this status so that we can // delete those boosts + remove them from timelines. boosts, err := u.state.DB.GetStatusBoosts( + // We MUST set a barebones context here, // as depending on where it came from the // original BoostOf may already be gone. @@ -537,11 +560,7 @@ func (u *utils) requestFave( } // Create + store new interaction request. - req, err = typeutils.StatusFaveToInteractionRequest(ctx, fave) - if err != nil { - return gtserror.Newf("error creating interaction request: %w", err) - } - + req = typeutils.StatusFaveToInteractionRequest(fave) if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil { return gtserror.Newf("db error storing interaction request: %w", err) } @@ -584,11 +603,7 @@ func (u *utils) requestReply( } // Create + store interaction request. - req, err = typeutils.StatusToInteractionRequest(ctx, reply) - if err != nil { - return gtserror.Newf("error creating interaction request: %w", err) - } - + req = typeutils.StatusToInteractionRequest(reply) if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil { return gtserror.Newf("db error storing interaction request: %w", err) } @@ -631,11 +646,7 @@ func (u *utils) requestAnnounce( } // Create + store interaction request. - req, err = typeutils.StatusToInteractionRequest(ctx, boost) - if err != nil { - return gtserror.Newf("error creating interaction request: %w", err) - } - + req = typeutils.StatusToInteractionRequest(boost) if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil { return gtserror.Newf("db error storing interaction request: %w", err) } diff --git a/internal/text/plain_test.go b/internal/text/plain_test.go index 48280bb44..fac54a38e 100644 --- a/internal/text/plain_test.go +++ b/internal/text/plain_test.go @@ -36,6 +36,8 @@ moreComplexExpected = "

Another test @foss_satan

#Hashtag

Text

:rainbow:

" withUTF8Link = "here's a link with utf-8 characters in it: https://example.org/söme_url" withUTF8LinkExpected = "

here's a link with utf-8 characters in it: https://example.org/söme_url

" + withFunkyTags = "#hashtag1 pee #hashtag2\u200Bpee #hashtag3|poo #hashtag4\uFEFFpoo" + withFunkyTagsExpected = "

#hashtag1 pee #hashtag2\u200bpee #hashtag3|poo #hashtag4\ufeffpoo

" ) type PlainTestSuite struct { @@ -136,6 +138,17 @@ func (suite *PlainTestSuite) TestDeriveHashtagsOK() { suite.Equal("올빼미", tags[0].Name) } +func (suite *PlainTestSuite) TestFunkyTags() { + formatted := suite.FromPlain(withFunkyTags) + suite.Equal(withFunkyTagsExpected, formatted.HTML) + + tags := formatted.Tags + suite.Equal("hashtag1", tags[0].Name) + suite.Equal("hashtag2", tags[1].Name) + suite.Equal("hashtag3", tags[2].Name) + suite.Equal("hashtag4", tags[3].Name) +} + func (suite *PlainTestSuite) TestDeriveMultiple() { statusText := `Another test @foss_satan@fossbros-anonymous.io diff --git a/internal/text/util.go b/internal/text/util.go index 204c64838..af45cfaf0 100644 --- a/internal/text/util.go +++ b/internal/text/util.go @@ -38,8 +38,34 @@ func isPermittedInHashtag(r rune) bool { // is a recognized break character for before // or after a #hashtag. func isHashtagBoundary(r rune) bool { - return unicode.IsSpace(r) || - (unicode.IsPunct(r) && r != '_') + switch { + + // Zero width space. + case r == '\u200B': + return true + + // Zero width no-break space. + case r == '\uFEFF': + return true + + // Pipe character sometimes + // used as workaround. + case r == '|': + return true + + // Standard Unicode white space. + case unicode.IsSpace(r): + return true + + // Non-underscore punctuation. + case unicode.IsPunct(r) && r != '_': + return true + + // Not recognized + // hashtag boundary. + default: + return false + } } // isMentionBoundary returns true if rune r diff --git a/internal/timeline/get_test.go b/internal/timeline/get_test.go index 6b01ca812..91a456560 100644 --- a/internal/timeline/get_test.go +++ b/internal/timeline/get_test.go @@ -228,7 +228,7 @@ func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossible() { if err != nil { suite.FailNow(err.Error()) } - suite.checkStatuses(statuses, id.Highest, id.Lowest, 20) + suite.checkStatuses(statuses, id.Highest, id.Lowest, 22) } func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossiblePageUp() { @@ -255,7 +255,7 @@ func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossiblePageUp() { if err != nil { suite.FailNow(err.Error()) } - suite.checkStatuses(statuses, id.Highest, id.Lowest, 20) + suite.checkStatuses(statuses, id.Highest, id.Lowest, 22) } func (suite *GetTestSuite) TestGetNewTimelineNoFollowing() { @@ -284,7 +284,7 @@ func (suite *GetTestSuite) TestGetNewTimelineNoFollowing() { if err != nil { suite.FailNow(err.Error()) } - suite.checkStatuses(statuses, id.Highest, id.Lowest, 8) + suite.checkStatuses(statuses, id.Highest, id.Lowest, 9) for _, s := range statuses { if s.GetAccountID() != testAccount.ID { diff --git a/internal/timeline/prune_test.go b/internal/timeline/prune_test.go index e78db64e8..4b909540c 100644 --- a/internal/timeline/prune_test.go +++ b/internal/timeline/prune_test.go @@ -40,7 +40,7 @@ func (suite *PruneTestSuite) TestPrune() { pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength) suite.NoError(err) - suite.Equal(20, pruned) + suite.Equal(23, pruned) suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID)) } @@ -56,7 +56,7 @@ func (suite *PruneTestSuite) TestPruneTwice() { pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength) suite.NoError(err) - suite.Equal(20, pruned) + suite.Equal(23, pruned) suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID)) // Prune same again, nothing should be pruned this time. @@ -78,7 +78,7 @@ func (suite *PruneTestSuite) TestPruneTo0() { pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength) suite.NoError(err) - suite.Equal(25, pruned) + suite.Equal(28, pruned) suite.Equal(0, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID)) } @@ -95,7 +95,7 @@ func (suite *PruneTestSuite) TestPruneToInfinityAndBeyond() { pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength) suite.NoError(err) suite.Equal(0, pruned) - suite.Equal(25, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID)) + suite.Equal(28, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID)) } func TestPruneTestSuite(t *testing.T) { diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index cf0c0719a..a473317ff 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -22,6 +22,7 @@ "context" "errors" "net/url" + "time" "github.com/miekg/dns" "github.com/superseriousbusiness/gotosocial/internal/ap" @@ -111,6 +112,13 @@ func (c *Converter) ASRepresentationToAccount( acct.UpdatedAt = pub } + // Extract updated time if possible, i.e. last edited. + if upd := ap.GetUpdated(accountable); !upd.IsZero() { + acct.UpdatedAt = upd + } else { + acct.UpdatedAt = acct.CreatedAt + } + // Extract a preferred name (display name), fallback to username. if displayName := ap.ExtractName(accountable); displayName != "" { acct.DisplayName = displayName @@ -348,18 +356,31 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab // zero-time will fall back to db defaults. if pub := ap.GetPublished(statusable); !pub.IsZero() { status.CreatedAt = pub - status.UpdatedAt = pub } else { log.Warnf(ctx, "unusable published property on %s", uri) + status.CreatedAt = time.Now() + } + + // status.Updated + // + // Extract and validate update time for status. Defaults to published. + if upd := ap.GetUpdated(statusable); !upd.Before(status.CreatedAt) { + status.UpdatedAt = upd + } else if upd.IsZero() { + status.UpdatedAt = status.CreatedAt + } else { + + // This is a malformed status that will likely break our systems. + err := gtserror.Newf("status %s 'updated' predates 'published'", uri) + return nil, gtserror.SetMalformed(err) } // status.AccountURI // status.AccountID // status.Account // - // Account that created the status. Assume we have - // this in the db by the time this function is called, - // error if we don't. + // Account that created the status. Assume we have this + // in the db by the time this function is called, else error. status.Account, err = c.getASAttributedToAccount(ctx, status.URI, statusable, diff --git a/internal/typeutils/internal.go b/internal/typeutils/internal.go index ccde6a38f..573495e0a 100644 --- a/internal/typeutils/internal.go +++ b/internal/typeutils/internal.go @@ -104,14 +104,8 @@ func (c *Converter) StatusToBoost( return boost, nil } -func StatusToInteractionRequest( - ctx context.Context, - status *gtsmodel.Status, -) (*gtsmodel.InteractionRequest, error) { - reqID, err := id.NewULIDFromTime(status.CreatedAt) - if err != nil { - return nil, gtserror.Newf("error generating ID: %w", err) - } +func StatusToInteractionRequest(status *gtsmodel.Status) *gtsmodel.InteractionRequest { + reqID := id.NewULIDFromTime(status.CreatedAt) var ( targetID string @@ -154,17 +148,11 @@ func StatusToInteractionRequest( InteractionType: interactionType, Reply: reply, Announce: announce, - }, nil + } } -func StatusFaveToInteractionRequest( - ctx context.Context, - fave *gtsmodel.StatusFave, -) (*gtsmodel.InteractionRequest, error) { - reqID, err := id.NewULIDFromTime(fave.CreatedAt) - if err != nil { - return nil, gtserror.Newf("error generating ID: %w", err) - } +func StatusFaveToInteractionRequest(fave *gtsmodel.StatusFave) *gtsmodel.InteractionRequest { + reqID := id.NewULIDFromTime(fave.CreatedAt) return >smodel.InteractionRequest{ ID: reqID, @@ -178,7 +166,7 @@ func StatusFaveToInteractionRequest( InteractionURI: fave.URI, InteractionType: gtsmodel.InteractionLike, Like: fave, - }, nil + } } func (c *Converter) StatusToSinBinStatus( diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index a81e5d2c0..7d0c483dd 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -444,7 +444,7 @@ func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (ap.Stat poll := streams.NewActivityStreamsQuestion() // Add required status poll data to AS Question. - if err := c.addPollToAS(ctx, s.Poll, poll); err != nil { + if err := c.addPollToAS(s.Poll, poll); err != nil { return nil, gtserror.Newf("error converting poll: %w", err) } @@ -484,10 +484,9 @@ func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (ap.Stat status.SetActivityStreamsInReplyTo(inReplyToProp) } - // published - publishedProp := streams.NewActivityStreamsPublishedProperty() - publishedProp.Set(s.CreatedAt) - status.SetActivityStreamsPublished(publishedProp) + // Set created / updated at properties. + ap.SetPublished(status, s.CreatedAt) + ap.SetUpdated(status, s.UpdatedAt) // url if s.URL != "" { @@ -708,7 +707,7 @@ func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (ap.Stat return status, nil } -func (c *Converter) addPollToAS(ctx context.Context, poll *gtsmodel.Poll, dst ap.Pollable) error { +func (c *Converter) addPollToAS(poll *gtsmodel.Poll, dst ap.Pollable) error { var optionsProp interface { // the minimum interface for appending AS Notes // to an AS type options property of some kind. diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go index c847cfc93..9870c760a 100644 --- a/internal/typeutils/internaltoas_test.go +++ b/internal/typeutils/internaltoas_test.go @@ -499,6 +499,7 @@ func (suite *InternalToASTestSuite) TestStatusToAS() { "tag": [], "to": "https://www.w3.org/ns/activitystreams#Public", "type": "Note", + "updated": "2021-10-20T12:40:37+02:00", "url": "http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY" }`, string(bytes)) } @@ -598,6 +599,7 @@ func (suite *InternalToASTestSuite) TestStatusWithTagsToASWithIDs() { ], "to": "https://www.w3.org/ns/activitystreams#Public", "type": "Note", + "updated": "2021-10-20T11:36:45Z", "url": "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R" }`, string(bytes)) } @@ -698,6 +700,7 @@ func (suite *InternalToASTestSuite) TestStatusWithTagsToASFromDB() { ], "to": "https://www.w3.org/ns/activitystreams#Public", "type": "Note", + "updated": "2021-10-20T11:36:45Z", "url": "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R" }`, string(bytes)) } @@ -778,6 +781,7 @@ func (suite *InternalToASTestSuite) TestStatusToASWithMentions() { }, "to": "https://www.w3.org/ns/activitystreams#Public", "type": "Note", + "updated": "2021-11-20T13:32:16Z", "url": "http://localhost:8080/@admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0" }`, string(bytes)) } diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index a29d242a9..7b8a210fe 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -1222,21 +1222,6 @@ func (c *Converter) StatusToWebStatus( return webStatus, nil } -// StatusToAPIStatusSource returns the *apimodel.StatusSource of the given status. -// Callers should check beforehand whether a requester has permission to view the -// source of the status, and ensure they're passing only a local status into this function. -func (c *Converter) StatusToAPIStatusSource(ctx context.Context, s *gtsmodel.Status) (*apimodel.StatusSource, error) { - // TODO: remove this when edit support is added. - text := "**STATUS EDITS ARE NOT CURRENTLY SUPPORTED IN GOTOSOCIAL (coming in 2024)**\n" + - "You can review the original text of your status below, but you will not be able to submit this edit.\n\n---\n\n" + s.Text - - return &apimodel.StatusSource{ - ID: s.ID, - Text: text, - SpoilerText: s.ContentWarning, - }, nil -} - // statusToFrontend is a package internal function for // parsing a status into its initial frontend representation. // @@ -1404,18 +1389,16 @@ func (c *Converter) baseStatusToFrontend( InteractionPolicy: *apiInteractionPolicy, } - // Nullable fields. - if s.InReplyToID != "" { - apiStatus.InReplyToID = util.Ptr(s.InReplyToID) + // Only set edited_at if this is a non-boost-wrapper + // with an updated_at date different to creation date. + if !s.UpdatedAt.Equal(s.CreatedAt) && s.BoostOfID == "" { + timestamp := util.FormatISO8601(s.UpdatedAt) + apiStatus.EditedAt = util.Ptr(timestamp) } - if s.InReplyToAccountID != "" { - apiStatus.InReplyToAccountID = util.Ptr(s.InReplyToAccountID) - } - - if s.Language != "" { - apiStatus.Language = util.Ptr(s.Language) - } + apiStatus.InReplyToID = util.PtrIf(s.InReplyToID) + apiStatus.InReplyToAccountID = util.PtrIf(s.InReplyToAccountID) + apiStatus.Language = util.PtrIf(s.Language) if app := s.CreatedWithApplication; app != nil { apiStatus.Application, err = c.AppToAPIAppPublic(ctx, app) @@ -1482,6 +1465,149 @@ func (c *Converter) baseStatusToFrontend( return apiStatus, nil } +// StatusToAPIEdits converts a status and its historical edits (if any) to a slice of API model status edits. +func (c *Converter) StatusToAPIEdits(ctx context.Context, status *gtsmodel.Status) ([]*apimodel.StatusEdit, error) { + var media map[string]*gtsmodel.MediaAttachment + + // Gather attachments of status AND edits. + attachmentIDs := status.AllAttachmentIDs() + if len(attachmentIDs) > 0 { + + // Fetch all of the gathered status attachments from the database. + attachments, err := c.state.DB.GetAttachmentsByIDs(ctx, attachmentIDs) + if err != nil { + return nil, gtserror.Newf("error getting attachments from db: %w", err) + } + + // Generate a lookup map in 'media' of status attachments by their IDs. + media = util.KeyBy(attachments, func(m *gtsmodel.MediaAttachment) string { + return m.ID + }) + } + + // Convert the status author account to API model. + apiAccount, err := c.AccountToAPIAccountPublic(ctx, + status.Account, + ) + if err != nil { + return nil, gtserror.Newf("error converting account: %w", err) + } + + // Convert status emojis to their API models, + // this includes all status emojis both current + // and historic, so it gets passed to each edit. + apiEmojis, err := c.convertEmojisToAPIEmojis(ctx, + nil, + status.EmojiIDs, + ) + if err != nil { + return nil, gtserror.Newf("error converting emojis: %w", err) + } + + var votes []int + var options []string + + if status.Poll != nil { + // Extract status poll options. + options = status.Poll.Options + + // Show votes only if closed / allowed. + if !status.Poll.ClosedAt.IsZero() || + !*status.Poll.HideCounts { + votes = status.Poll.Votes + } + } + + // Append status itself to final slot in the edits + // so we can add its revision using the below loop. + edits := append(status.Edits, >smodel.StatusEdit{ //nolint:gocritic + Content: status.Content, + ContentWarning: status.ContentWarning, + Sensitive: status.Sensitive, + PollOptions: options, + PollVotes: votes, + AttachmentIDs: status.AttachmentIDs, + AttachmentDescriptions: nil, // no change from current + CreatedAt: status.UpdatedAt, + }) + + // Iterate through status edits, starting at newest. + apiEdits := make([]*apimodel.StatusEdit, 0, len(edits)) + for i := len(edits) - 1; i >= 0; i-- { + edit := edits[i] + + // Iterate through edit attachment IDs, getting model from 'media' lookup. + apiAttachments := make([]*apimodel.Attachment, 0, len(edit.AttachmentIDs)) + for _, id := range edit.AttachmentIDs { + attachment, ok := media[id] + if !ok { + continue + } + + // Convert each media attachment to frontend API model. + apiAttachment, err := c.AttachmentToAPIAttachment(ctx, + attachment, + ) + if err != nil { + log.Error(ctx, "error converting attachment: %v", err) + continue + } + + // Append converted media attachment to return slice. + apiAttachments = append(apiAttachments, &apiAttachment) + } + + // If media descriptions are set, update API model descriptions. + if len(edit.AttachmentIDs) == len(edit.AttachmentDescriptions) { + var j int + for i, id := range edit.AttachmentIDs { + descr := edit.AttachmentDescriptions[i] + for ; j < len(apiAttachments); j++ { + if apiAttachments[j].ID == id { + apiAttachments[j].Description = &descr + break + } + } + } + } + + // Attach status poll if set. + var apiPoll *apimodel.Poll + if len(edit.PollOptions) > 0 { + apiPoll = new(apimodel.Poll) + + // Iterate through poll options and attach to API poll model. + apiPoll.Options = make([]apimodel.PollOption, len(edit.PollOptions)) + for i, option := range edit.PollOptions { + apiPoll.Options[i] = apimodel.PollOption{ + Title: option, + } + } + + // If poll votes are attached, set vote counts. + if len(edit.PollVotes) == len(apiPoll.Options) { + for i, votes := range edit.PollVotes { + apiPoll.Options[i].VotesCount = &votes + } + } + } + + // Append this status edit to the return slice. + apiEdits = append(apiEdits, &apimodel.StatusEdit{ + CreatedAt: util.FormatISO8601(edit.CreatedAt), + Content: edit.Content, + SpoilerText: edit.ContentWarning, + Sensitive: util.PtrOrZero(edit.Sensitive), + Account: apiAccount, + Poll: apiPoll, + MediaAttachments: apiAttachments, + Emojis: apiEmojis, // same models used for whole status + all edits + }) + } + + return apiEdits, nil +} + // VisToAPIVis converts a gts visibility into its api equivalent func (c *Converter) VisToAPIVis(ctx context.Context, m gtsmodel.Visibility) apimodel.Visibility { switch m { @@ -1498,7 +1624,7 @@ func (c *Converter) VisToAPIVis(ctx context.Context, m gtsmodel.Visibility) apim } // InstanceRuleToAdminAPIRule converts a local instance rule into its api equivalent for serving at /api/v1/admin/instance/rules/:id -func (c *Converter) InstanceRuleToAPIRule(r gtsmodel.Rule) apimodel.InstanceRule { +func InstanceRuleToAPIRule(r gtsmodel.Rule) apimodel.InstanceRule { return apimodel.InstanceRule{ ID: r.ID, Text: r.Text, @@ -1506,18 +1632,16 @@ func (c *Converter) InstanceRuleToAPIRule(r gtsmodel.Rule) apimodel.InstanceRule } // InstanceRulesToAPIRules converts all local instance rules into their api equivalent for serving at /api/v1/instance/rules -func (c *Converter) InstanceRulesToAPIRules(r []gtsmodel.Rule) []apimodel.InstanceRule { +func InstanceRulesToAPIRules(r []gtsmodel.Rule) []apimodel.InstanceRule { rules := make([]apimodel.InstanceRule, len(r)) - for i, v := range r { - rules[i] = c.InstanceRuleToAPIRule(v) + rules[i] = InstanceRuleToAPIRule(v) } - return rules } // InstanceRuleToAdminAPIRule converts a local instance rule into its api equivalent for serving at /api/v1/admin/instance/rules/:id -func (c *Converter) InstanceRuleToAdminAPIRule(r *gtsmodel.Rule) *apimodel.AdminInstanceRule { +func InstanceRuleToAdminAPIRule(r *gtsmodel.Rule) *apimodel.AdminInstanceRule { return &apimodel.AdminInstanceRule{ ID: r.ID, CreatedAt: util.FormatISO8601(r.CreatedAt), @@ -1540,6 +1664,7 @@ func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins Title: i.Title, Description: i.Description, DescriptionText: i.DescriptionText, + CustomCSS: i.CustomCSS, ShortDescription: i.ShortDescription, ShortDescriptionText: i.ShortDescriptionText, Email: i.ContactEmail, @@ -1549,7 +1674,7 @@ func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins ApprovalRequired: true, // approval always required InvitesEnabled: false, // todo: not supported yet MaxTootChars: uint(config.GetStatusesMaxChars()), // #nosec G115 -- Already validated. - Rules: c.InstanceRulesToAPIRules(i.Rules), + Rules: InstanceRulesToAPIRules(i.Rules), Terms: i.Terms, TermsRaw: i.TermsText, } @@ -1680,9 +1805,10 @@ func (c *Converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins SourceURL: instanceSourceURL, Description: i.Description, DescriptionText: i.DescriptionText, + CustomCSS: i.CustomCSS, Usage: apimodel.InstanceV2Usage{}, // todo: not implemented Languages: config.GetInstanceLanguages().TagStrs(), - Rules: c.InstanceRulesToAPIRules(i.Rules), + Rules: InstanceRulesToAPIRules(i.Rules), Terms: i.Terms, TermsText: i.TermsText, } diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 11f878557..3b1aca8e5 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -68,8 +68,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() { "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", "followers_count": 2, "following_count": 2, - "statuses_count": 8, - "last_status_at": "2024-01-10", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [], "enable_rss": true @@ -120,8 +120,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved() "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", "followers_count": 2, "following_count": 2, - "statuses_count": 8, - "last_status_at": "2024-01-10", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [], "source": { @@ -163,8 +163,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved() "header_description": "Flat gray background (default header).", "followers_count": 1, "following_count": 1, - "statuses_count": 8, - "last_status_at": "2021-07-28", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [ { @@ -218,8 +218,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiStruct() "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", "followers_count": 2, "following_count": 2, - "statuses_count": 8, - "last_status_at": "2024-01-10", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [ { "shortcode": "rainbow", @@ -267,8 +267,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiIDs() { "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", "followers_count": 2, "following_count": 2, - "statuses_count": 8, - "last_status_at": "2024-01-10", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [ { "shortcode": "rainbow", @@ -312,8 +312,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() { "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", "followers_count": 2, "following_count": 2, - "statuses_count": 8, - "last_status_at": "2024-01-10", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [], "source": { @@ -464,6 +464,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() { suite.Equal(`{ "id": "01F8MH75CBF9JFX4ZAD54N0W0R", "created_at": "2021-10-20T11:36:45.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": false, @@ -642,6 +643,7 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredStatusToFrontend() { suite.Equal(`{ "id": "01F8MH75CBF9JFX4ZAD54N0W0R", "created_at": "2021-10-20T11:36:45.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": false, @@ -808,6 +810,7 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredBoostToFrontend() { suite.Equal(`{ "id": "01G36SF3V6Y6V5BF9P4R7PQG7G", "created_at": "2021-10-20T10:41:37.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": false, @@ -828,6 +831,7 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredBoostToFrontend() { "reblog": { "id": "01F8MH75CBF9JFX4ZAD54N0W0R", "created_at": "2021-10-20T11:36:45.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": false, @@ -871,8 +875,8 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredBoostToFrontend() { "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", "followers_count": 2, "following_count": 2, - "statuses_count": 8, - "last_status_at": "2024-01-10", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [], "enable_rss": true @@ -1219,6 +1223,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments suite.Equal(`{ "id": "01HE7XJ1CG84TBKH5V9XKBVGF5", "created_at": "2023-11-02T10:44:25.000Z", + "edited_at": null, "in_reply_to_id": "01F8MH75CBF9JFX4ZAD54N0W0R", "in_reply_to_account_id": "01F8MH17FWEB39HZJ76B6VXSKF", "sensitive": true, @@ -1351,6 +1356,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() { suite.Equal(`{ "id": "01HE7XJ1CG84TBKH5V9XKBVGF5", "created_at": "2023-11-02T10:44:25.000Z", + "edited_at": null, "in_reply_to_id": "01F8MH75CBF9JFX4ZAD54N0W0R", "in_reply_to_account_id": "01F8MH17FWEB39HZJ76B6VXSKF", "sensitive": true, @@ -1512,6 +1518,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownLanguage() suite.Equal(`{ "id": "01F8MH75CBF9JFX4ZAD54N0W0R", "created_at": "2021-10-20T11:36:45.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": false, @@ -1655,6 +1662,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendPartialInteraction suite.Equal(`{ "id": "01F8MHBBN8120SYH7D5S050MGK", "created_at": "2021-10-20T10:40:37.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": false, @@ -1698,8 +1706,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendPartialInteraction "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", "followers_count": 2, "following_count": 2, - "statuses_count": 8, - "last_status_at": "2024-01-10", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [], "enable_rss": true @@ -1765,6 +1773,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToAPIStatusPendingApproval() suite.Equal(`{ "id": "01J5QVB9VC76NPPRQ207GG4DRZ", "created_at": "2024-02-20T10:41:37.000Z", + "edited_at": null, "in_reply_to_id": "01F8MHC8VWDRBQR0N1BATDDEM5", "in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF", "sensitive": false, @@ -1994,7 +2003,7 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() { }, "stats": { "domain_count": 2, - "status_count": 19, + "status_count": 21, "user_count": 4 }, "thumbnail": "http://localhost:8080/assets/logo.webp", @@ -2288,8 +2297,8 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend1() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } @@ -2332,8 +2341,8 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend2() { "header_description": "Flat gray background (default header).", "followers_count": 1, "following_count": 1, - "statuses_count": 8, - "last_status_at": "2021-07-28", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [ { @@ -2409,8 +2418,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } @@ -2455,8 +2464,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() { "header_description": "Flat gray background (default header).", "followers_count": 1, "following_count": 1, - "statuses_count": 8, - "last_status_at": "2021-07-28", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [ { @@ -2647,8 +2656,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() { "header_description": "Flat gray background (default header).", "followers_count": 1, "following_count": 1, - "statuses_count": 8, - "last_status_at": "2021-07-28", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [ { @@ -2706,8 +2715,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } @@ -2718,6 +2727,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() { { "id": "01FVW7JHQFSFK166WWKR8CBA6M", "created_at": "2021-09-20T10:40:37.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": false, @@ -2754,8 +2764,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() { "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] }, @@ -2913,8 +2923,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca "header_description": "Flat gray background (default header).", "followers_count": 0, "following_count": 0, - "statuses_count": 3, - "last_status_at": "2021-09-11", + "statuses_count": 4, + "last_status_at": "2024-11-01", "emojis": [], "fields": [] } @@ -3225,6 +3235,7 @@ func (suite *InternalToFrontendTestSuite) TestIntReqToAPI() { "status": { "id": "01F8MHC8VWDRBQR0N1BATDDEM5", "created_at": "2021-10-20T10:40:37.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": true, @@ -3265,8 +3276,8 @@ func (suite *InternalToFrontendTestSuite) TestIntReqToAPI() { "header_description": "Flat gray background (default header).", "followers_count": 1, "following_count": 1, - "statuses_count": 8, - "last_status_at": "2021-07-28", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [ { @@ -3318,6 +3329,7 @@ func (suite *InternalToFrontendTestSuite) TestIntReqToAPI() { "reply": { "id": "01J5QVB9VC76NPPRQ207GG4DRZ", "created_at": "2024-02-20T10:41:37.000Z", + "edited_at": null, "in_reply_to_id": "01F8MHC8VWDRBQR0N1BATDDEM5", "in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF", "sensitive": false, @@ -3475,8 +3487,8 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPISelfConvo() { "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", "followers_count": 2, "following_count": 2, - "statuses_count": 8, - "last_status_at": "2024-01-10", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [], "enable_rss": true @@ -3485,6 +3497,7 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPISelfConvo() { "last_status": { "id": "01F8MHAMCHF6Y650WCRSCP4WMY", "created_at": "2021-10-20T10:40:37.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": true, @@ -3528,8 +3541,8 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPISelfConvo() { "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", "followers_count": 2, "following_count": 2, - "statuses_count": 8, - "last_status_at": "2024-01-10", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [], "enable_rss": true @@ -3630,8 +3643,8 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPI() { "header_description": "Flat gray background (default header).", "followers_count": 1, "following_count": 1, - "statuses_count": 8, - "last_status_at": "2021-07-28", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [ { @@ -3651,6 +3664,7 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPI() { "last_status": { "id": "01F8MHAMCHF6Y650WCRSCP4WMY", "created_at": "2021-10-20T10:40:37.000Z", + "edited_at": null, "in_reply_to_id": null, "in_reply_to_account_id": null, "sensitive": true, @@ -3694,8 +3708,8 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPI() { "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", "followers_count": 2, "following_count": 2, - "statuses_count": 8, - "last_status_at": "2024-01-10", + "statuses_count": 9, + "last_status_at": "2024-11-01", "emojis": [], "fields": [], "enable_rss": true @@ -3734,6 +3748,136 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPI() { }`, string(b)) } +func (suite *InternalToFrontendTestSuite) TestStatusToAPIEdits() { + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + statusID := suite.testStatuses["local_account_1_status_9"].ID + + status, err := suite.state.DB.GetStatusByID(ctx, statusID) + suite.NoError(err) + + err = suite.state.DB.PopulateStatusEdits(ctx, status) + suite.NoError(err) + + apiEdits, err := suite.typeconverter.StatusToAPIEdits(ctx, status) + suite.NoError(err) + + b, err := json.MarshalIndent(apiEdits, "", " ") + suite.NoError(err) + + suite.Equal(`[ + { + "content": "\u003cp\u003ethis is the latest revision of the status, with a content-warning\u003c/p\u003e", + "spoiler_text": "edited status", + "sensitive": false, + "created_at": "2024-11-01T09:02:00.000Z", + "account": { + "id": "01F8MH1H7YV1Z7D2C8K2730QBF", + "username": "the_mighty_zork", + "acct": "the_mighty_zork", + "display_name": "original zork (he/they)", + "locked": false, + "discoverable": true, + "bot": false, + "created_at": "2022-05-20T11:09:18.000Z", + "note": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e", + "url": "http://localhost:8080/@the_mighty_zork", + "avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", + "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp", + "avatar_description": "a green goblin looking nasty", + "avatar_media_id": "01F8MH58A357CV5K7R7TJMSH6S", + "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", + "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp", + "header_description": "A very old-school screenshot of the original team fortress mod for quake", + "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", + "followers_count": 2, + "following_count": 2, + "statuses_count": 9, + "last_status_at": "2024-11-01", + "emojis": [], + "fields": [], + "enable_rss": true + }, + "poll": null, + "media_attachments": [], + "emojis": [] + }, + { + "content": "\u003cp\u003ethis is the first status edit! now with content-warning\u003c/p\u003e", + "spoiler_text": "edited status", + "sensitive": false, + "created_at": "2024-11-01T09:01:00.000Z", + "account": { + "id": "01F8MH1H7YV1Z7D2C8K2730QBF", + "username": "the_mighty_zork", + "acct": "the_mighty_zork", + "display_name": "original zork (he/they)", + "locked": false, + "discoverable": true, + "bot": false, + "created_at": "2022-05-20T11:09:18.000Z", + "note": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e", + "url": "http://localhost:8080/@the_mighty_zork", + "avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", + "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp", + "avatar_description": "a green goblin looking nasty", + "avatar_media_id": "01F8MH58A357CV5K7R7TJMSH6S", + "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", + "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp", + "header_description": "A very old-school screenshot of the original team fortress mod for quake", + "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", + "followers_count": 2, + "following_count": 2, + "statuses_count": 9, + "last_status_at": "2024-11-01", + "emojis": [], + "fields": [], + "enable_rss": true + }, + "poll": null, + "media_attachments": [], + "emojis": [] + }, + { + "content": "\u003cp\u003ethis is the original status\u003c/p\u003e", + "spoiler_text": "", + "sensitive": false, + "created_at": "2024-11-01T09:00:00.000Z", + "account": { + "id": "01F8MH1H7YV1Z7D2C8K2730QBF", + "username": "the_mighty_zork", + "acct": "the_mighty_zork", + "display_name": "original zork (he/they)", + "locked": false, + "discoverable": true, + "bot": false, + "created_at": "2022-05-20T11:09:18.000Z", + "note": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e", + "url": "http://localhost:8080/@the_mighty_zork", + "avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", + "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp", + "avatar_description": "a green goblin looking nasty", + "avatar_media_id": "01F8MH58A357CV5K7R7TJMSH6S", + "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", + "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp", + "header_description": "A very old-school screenshot of the original team fortress mod for quake", + "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", + "followers_count": 2, + "following_count": 2, + "statuses_count": 9, + "last_status_at": "2024-11-01", + "emojis": [], + "fields": [], + "enable_rss": true + }, + "poll": null, + "media_attachments": [], + "emojis": [] + } +]`, string(b)) +} + func TestInternalToFrontendTestSuite(t *testing.T) { suite.Run(t, new(InternalToFrontendTestSuite)) } diff --git a/internal/typeutils/wrap_test.go b/internal/typeutils/wrap_test.go index 1085c8c66..c2c9c9464 100644 --- a/internal/typeutils/wrap_test.go +++ b/internal/typeutils/wrap_test.go @@ -131,6 +131,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreate() { "tag": [], "to": "https://www.w3.org/ns/activitystreams#Public", "type": "Note", + "updated": "2021-10-20T12:40:37+02:00", "url": "http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY" }, "published": "2021-10-20T12:40:37+02:00", diff --git a/internal/validate/formvalidation.go b/internal/validate/formvalidation.go index 207e8e05e..4de7636a5 100644 --- a/internal/validate/formvalidation.go +++ b/internal/validate/formvalidation.go @@ -189,6 +189,16 @@ func CustomCSS(customCSS string) error { return nil } +func InstanceCustomCSS(customCSS string) error { + + maximumCustomCSSLength := config.GetAccountsCustomCSSLength() + if length := len([]rune(customCSS)); length > maximumCustomCSSLength { + return fmt.Errorf("custom_css must be less than %d characters, but submitted custom_css was %d characters", maximumCustomCSSLength, length) + } + + return nil +} + // EmojiShortcode just runs the given shortcode through the regular expression // for emoji shortcodes, to figure out whether it's a valid shortcode, ie., 1-30 characters, // a-zA-Z, numbers, and underscores. diff --git a/internal/web/customcss.go b/internal/web/customcss.go index b4072f2a7..36ae9de55 100644 --- a/internal/web/customcss.go +++ b/internal/web/customcss.go @@ -55,3 +55,22 @@ func (m *Module) customCSSGETHandler(c *gin.Context) { c.Header(cacheControlHeader, cacheControlNoCache) c.Data(http.StatusOK, textCSSUTF8, []byte(customCSS)) } + +func (m *Module) instanceCustomCSSGETHandler(c *gin.Context) { + + if _, err := apiutil.NegotiateAccept(c, apiutil.TextCSS); err != nil { + apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + instanceV1, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + instanceCustomCSS := instanceV1.CustomCSS + + c.Header(cacheControlHeader, cacheControlNoCache) + c.Data(http.StatusOK, textCSSUTF8, []byte(instanceCustomCSS)) +} diff --git a/internal/web/profile.go b/internal/web/profile.go index 60157fd19..a6d96a9ea 100644 --- a/internal/web/profile.go +++ b/internal/web/profile.go @@ -132,7 +132,7 @@ func (m *Module) profileGETHandler(c *gin.Context) { } // Prepare stylesheets for profile. - stylesheets := make([]string, 0, 6) + stylesheets := make([]string, 0, 7) // Basic profile stylesheets. stylesheets = append( diff --git a/internal/web/thread.go b/internal/web/thread.go index d3ba6ea5e..60f7ac4d2 100644 --- a/internal/web/thread.go +++ b/internal/web/thread.go @@ -115,7 +115,7 @@ func (m *Module) threadGETHandler(c *gin.Context) { } // Prepare stylesheets for thread. - stylesheets := make([]string, 0, 5) + stylesheets := make([]string, 0, 6) // Basic thread stylesheets. stylesheets = append( diff --git a/internal/web/web.go b/internal/web/web.go index 185bf7120..35f8f21b0 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -36,20 +36,21 @@ ) const ( - confirmEmailPath = "/" + uris.ConfirmEmailPath - profileGroupPath = "/@:username" - statusPath = "/statuses/:" + apiutil.WebStatusIDKey // leave out the '/@:username' prefix as this will be served within the profile group - tagsPath = "/tags/:" + apiutil.TagNameKey - customCSSPath = profileGroupPath + "/custom.css" - rssFeedPath = profileGroupPath + "/feed.rss" - assetsPathPrefix = "/assets" - distPathPrefix = assetsPathPrefix + "/dist" - themesPathPrefix = assetsPathPrefix + "/themes" - settingsPathPrefix = "/settings" - settingsPanelGlob = settingsPathPrefix + "/*panel" - userPanelPath = settingsPathPrefix + "/user" - adminPanelPath = settingsPathPrefix + "/admin" - signupPath = "/signup" + confirmEmailPath = "/" + uris.ConfirmEmailPath + profileGroupPath = "/@:username" + statusPath = "/statuses/:" + apiutil.WebStatusIDKey // leave out the '/@:username' prefix as this will be served within the profile group + tagsPath = "/tags/:" + apiutil.TagNameKey + customCSSPath = profileGroupPath + "/custom.css" + instanceCustomCSSPath = "/custom.css" + rssFeedPath = profileGroupPath + "/feed.rss" + assetsPathPrefix = "/assets" + distPathPrefix = assetsPathPrefix + "/dist" + themesPathPrefix = assetsPathPrefix + "/themes" + settingsPathPrefix = "/settings" + settingsPanelGlob = settingsPathPrefix + "/*panel" + userPanelPath = settingsPathPrefix + "/user" + adminPanelPath = settingsPathPrefix + "/admin" + signupPath = "/signup" cacheControlHeader = "Cache-Control" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control cacheControlNoCache = "no-cache" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives @@ -114,6 +115,7 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) { r.AttachHandler(http.MethodGet, settingsPathPrefix, m.SettingsPanelHandler) r.AttachHandler(http.MethodGet, settingsPanelGlob, m.SettingsPanelHandler) r.AttachHandler(http.MethodGet, customCSSPath, m.customCSSGETHandler) + r.AttachHandler(http.MethodGet, instanceCustomCSSPath, m.instanceCustomCSSGETHandler) r.AttachHandler(http.MethodGet, rssFeedPath, m.rssFeedGETHandler) r.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler) r.AttachHandler(http.MethodPost, confirmEmailPath, m.confirmEmailPOSTHandler) diff --git a/test/envparsing.sh b/test/envparsing.sh index e5e69a710..7fed2a87d 100755 --- a/test/envparsing.sh +++ b/test/envparsing.sh @@ -64,6 +64,7 @@ EXPECT=$(cat << "EOF" "sin-bin-status-mem-ratio": 0.5, "status-bookmark-ids-mem-ratio": 2, "status-bookmark-mem-ratio": 0.5, + "status-edit-mem-ratio": 2, "status-fave-ids-mem-ratio": 3, "status-fave-mem-ratio": 2, "status-mem-ratio": 5, diff --git a/testrig/db.go b/testrig/db.go index c107b9b05..dd19c3648 100644 --- a/testrig/db.go +++ b/testrig/db.go @@ -53,6 +53,7 @@ >smodel.Status{}, >smodel.StatusToEmoji{}, >smodel.StatusToTag{}, + >smodel.StatusEdit{}, >smodel.StatusFave{}, >smodel.StatusBookmark{}, >smodel.Tag{}, @@ -104,7 +105,7 @@ func CreateTestTables(db db.DB) { ctx := context.Background() for _, m := range testModels { if err := db.CreateTable(ctx, m); err != nil { - log.Panicf(nil, "error creating table for %+v: %s", m, err) + log.Panicf(ctx, "error creating table for %+v: %s", m, err) } } } @@ -128,225 +129,225 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) { for _, v := range NewTestTokens() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestClients() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestApplications() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestBlocks() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestReports() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestRules() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestDomainBlocks() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestInstances() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestUsers() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } if accounts == nil { for _, v := range NewTestAccounts() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } } else { for _, v := range accounts { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } } for _, v := range NewTestAccountSettings() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestAttachments() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestStatuses() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestEmojis() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestEmojiCategories() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestStatusToEmojis() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestTags() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestStatusToTags() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestMentions() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestFaves() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestFollows() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestLists() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestListEntries() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestNotifications() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestTombstones() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestBookmarks() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestAccountNotes() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestMarkers() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestThreads() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestThreadToStatus() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestPolls() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestPollVotes() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestFilters() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestFilterKeywords() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestFilterStatuses() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } for _, v := range NewTestUserMutes() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } } @@ -358,16 +359,22 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) { for _, v := range NewTestInteractionRequests() { if err := db.Put(ctx, v); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) + } + } + + for _, v := range NewTestStatusEdits() { + if err := db.Put(ctx, v); err != nil { + log.Panic(ctx, err) } } if err := db.CreateInstanceAccount(ctx); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } if err := db.CreateInstanceInstance(ctx); err != nil { - log.Panic(nil, err) + log.Panic(ctx, err) } vapidKeyPair := >smodel.VAPIDKeyPair{} @@ -379,7 +386,7 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) { log.Panic(nil, err) } - log.Debug(nil, "testing db setup complete") + log.Debug(ctx, "testing db setup complete") } // StandardDBTeardown drops all the standard testing tables/models from the database to ensure it's clean for the next test. diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 6cadcba52..d8db8cc40 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -718,7 +718,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { URL: "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg", RemoteURL: "", CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"), - UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"), Type: gtsmodel.FileTypeImage, FileMeta: gtsmodel.FileMeta{ Original: gtsmodel.Original{ @@ -761,7 +760,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH7TDVANYKWVE8VVKFPJTJ.gif", RemoteURL: "", CreatedAt: TimeMustParse("2022-06-09T13:12:00Z"), - UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"), Type: gtsmodel.FileTypeImage, FileMeta: gtsmodel.FileMeta{ Original: gtsmodel.Original{ @@ -808,7 +806,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01CDR64G398ADCHXK08WWTHEZ5.mp4", RemoteURL: "", CreatedAt: TimeMustParse("2022-06-09T13:12:00Z"), - UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"), Type: gtsmodel.FileTypeVideo, FileMeta: gtsmodel.FileMeta{ Original: gtsmodel.Original{ @@ -858,7 +855,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg", RemoteURL: "", CreatedAt: TimeMustParse("2022-06-09T13:12:00Z"), - UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"), Type: gtsmodel.FileTypeImage, FileMeta: gtsmodel.FileMeta{ Original: gtsmodel.Original{ @@ -905,7 +901,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", RemoteURL: "", CreatedAt: TimeMustParse("2022-06-09T13:12:00Z"), - UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"), Type: gtsmodel.FileTypeImage, FileMeta: gtsmodel.FileMeta{ Original: gtsmodel.Original{ @@ -952,7 +947,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", RemoteURL: "", CreatedAt: TimeMustParse("2022-06-09T13:12:00Z"), - UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"), Type: gtsmodel.FileTypeImage, FileMeta: gtsmodel.FileMeta{ Original: gtsmodel.Original{ @@ -999,7 +993,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01J2M20K6K9XQC4WSB961YJHV6.mp3", RemoteURL: "", CreatedAt: TimeMustParse("2024-01-10T11:24:00+02:00"), - UpdatedAt: TimeMustParse("2024-01-10T11:24:00+02:00"), Type: gtsmodel.FileTypeAudio, FileMeta: gtsmodel.FileMeta{ Original: gtsmodel.Original{ @@ -1043,13 +1036,30 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { Header: util.Ptr(false), Cached: util.Ptr(true), }, + "local_account_2_status_9_attachment_1": { + ID: "01JDQ164HM08SGJ7ZEK9003Z4B", + StatusID: "01JDPZEZ77X1NX0TY9M10BK1HM", + URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE88YG74PVAB81PX2XA9F3FG.mp3", + RemoteURL: "http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE892Y8ZS68TQCNPX7J888P3.mp3", + CreatedAt: TimeMustParse("2024-11-01T10:01:00+02:00"), + Type: gtsmodel.FileTypeUnknown, + FileMeta: gtsmodel.FileMeta{}, + AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF", + Description: "Jolly salsa song, public domain.", + Blurhash: "", + Processing: gtsmodel.ProcessingStatusProcessed, + File: gtsmodel.File{}, + Thumbnail: gtsmodel.Thumbnail{RemoteURL: ""}, + Avatar: util.Ptr(false), + Header: util.Ptr(false), + Cached: util.Ptr(false), + }, "remote_account_1_status_1_attachment_1": { ID: "01FVW7RXPQ8YJHTEXYPE7Q8ZY0", StatusID: "01FVW7JHQFSFK166WWKR8CBA6M", URL: "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg", RemoteURL: "http://fossbros-anonymous.io/attachments/original/13bbc3f8-2b5e-46ea-9531-40b4974d9912.jpg", CreatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"), - UpdatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"), Type: gtsmodel.FileTypeImage, FileMeta: gtsmodel.FileMeta{ Original: gtsmodel.Original{ @@ -1095,7 +1105,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { URL: "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg", RemoteURL: "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg", CreatedAt: TimeMustParse("2022-06-09T13:12:00Z"), - UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"), Type: gtsmodel.FileTypeImage, FileMeta: gtsmodel.FileMeta{ Original: gtsmodel.Original{ @@ -1141,7 +1150,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7Y3C432WRSNS10EZM86SA5.jpg", RemoteURL: "http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE7Y6G0EMCKST3Q0914WW0MS.jpg", CreatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"), - UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"), Type: gtsmodel.FileTypeImage, FileMeta: gtsmodel.FileMeta{ Original: gtsmodel.Original{ @@ -1186,7 +1194,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7ZFX9GKA5ZZVD4FACABSS9.svg", RemoteURL: "http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE7ZGJYTSYMXF927GF9353KR.svg", CreatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"), - UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"), Type: gtsmodel.FileTypeUnknown, FileMeta: gtsmodel.FileMeta{}, AccountID: "01FHMQX3GAABWSM0S2VZEC2SWC", @@ -1205,7 +1212,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE88YG74PVAB81PX2XA9F3FG.mp3", RemoteURL: "http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE892Y8ZS68TQCNPX7J888P3.mp3", CreatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"), - UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"), Type: gtsmodel.FileTypeUnknown, FileMeta: gtsmodel.FileMeta{}, AccountID: "01FHMQX3GAABWSM0S2VZEC2SWC", @@ -1739,6 +1745,32 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, + "local_account_1_status_9": { + ID: "01JDPZC707CKDN8N4QVWM4Z1NR", + URI: "http://localhost:8080/users/the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR", + URL: "http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR", + Content: "

this is the latest revision of the status, with a content-warning

", + Text: "this is the latest revision of the status, with a content-warning", + ContentWarning: "edited status", + AttachmentIDs: nil, + CreatedAt: TimeMustParse("2024-11-01T11:00:00+02:00"), + UpdatedAt: TimeMustParse("2024-11-01T11:02:00+02:00"), + Local: util.Ptr(true), + AccountURI: "http://localhost:8080/users/the_mighty_zork", + AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", + InReplyToID: "", + InReplyToAccountID: "", + InReplyToURI: "", + BoostOfID: "", + ThreadID: "", + EditIDs: []string{"01JDPZCZ2Y9KSGZW0R7ZG8T8Y2", "01JDPZDADMD1T9HKF94RECF7PP"}, + Visibility: gtsmodel.VisibilityPublic, + Sensitive: util.Ptr(false), + Language: "en", + CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", + Federated: util.Ptr(true), + ActivityStreamsType: ap.ObjectNote, + }, "local_account_2_status_1": { ID: "01F8MHBQCBTDKN6X5VHGMMN4MA", URI: "http://localhost:8080/users/1happyturtle/statuses/01F8MHBQCBTDKN6X5VHGMMN4MA", @@ -1967,6 +1999,32 @@ func NewTestStatuses() map[string]*gtsmodel.Status { PollID: "01HEN2QB5NR4NCEHGYC3HN84K6", PendingApproval: util.Ptr(false), }, + "local_account_2_status_9": { + ID: "01JDPZEZ77X1NX0TY9M10BK1HM", + URI: "http://localhost:8080/users/1happyturtle/statuses/01JDPZEZ77X1NX0TY9M10BK1HM", + URL: "http://localhost:8080/@1happyturtle/statuses/01JDPZEZ77X1NX0TY9M10BK1HM", + Content: "

now edited to bring back the previous edit's media!

", + Text: "now edited to bring back the previous edit's media!", + ContentWarning: "edit with media attachments", + AttachmentIDs: []string{"01JDQ164HM08SGJ7ZEK9003Z4B"}, + CreatedAt: TimeMustParse("2024-11-01T10:00:00+02:00"), + UpdatedAt: TimeMustParse("2024-11-01T10:03:00+02:00"), + Local: util.Ptr(true), + AccountURI: "http://localhost:8080/users/the_mighty_zork", + AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF", + InReplyToID: "", + InReplyToAccountID: "", + InReplyToURI: "", + BoostOfID: "", + ThreadID: "", + EditIDs: []string{"01JDPZPBXAX0M02YSEPB21KX4R", "01JDPZPJHKP7E3M0YQXEXPS1YT", "01JDPZPY3F85Y7B78ETRXEMWD9"}, + Visibility: gtsmodel.VisibilityPublic, + Sensitive: util.Ptr(false), + Language: "en", + CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", + Federated: util.Ptr(true), + ActivityStreamsType: ap.ObjectNote, + }, "remote_account_1_status_1": { ID: "01FVW7JHQFSFK166WWKR8CBA6M", URI: "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M", @@ -2042,6 +2100,33 @@ func NewTestStatuses() map[string]*gtsmodel.Status { PollID: "01HEWV1GW2D49R919NPEDXPTZ5", PendingApproval: util.Ptr(false), }, + "remote_account_1_status_4": { + ID: "01JDQ07JZTX9CMDJP67CNA71YD", + URI: "http://fossbros-anonymous.io/users/foss_satan/statuses/______", + URL: "http://fossbros-anonymous.io/@foss_satan/statuses/______", + Content: "

this is the latest status edit without poll change

", + Text: "this is the latest status edit without poll change", + ContentWarning: "", + AttachmentIDs: nil, + CreatedAt: TimeMustParse("2024-11-01T09:00:00+02:00"), + UpdatedAt: TimeMustParse("2024-11-01T09:02:00+02:00"), + Local: util.Ptr(false), + AccountURI: "http://fossbros-anonymous.io/users/foss_satan", + AccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX", + InReplyToID: "", + InReplyToAccountID: "", + InReplyToURI: "", + BoostOfID: "", + ThreadID: "", + EditIDs: []string{"01JDQ07ZZ4FGP13YN8TF63P5A6", "01JDQ08AYQC0G6413VAHA51CV9"}, + PollID: "01JDQ0EZ5HM9T4WXRQ5WSVD40J", + Visibility: gtsmodel.VisibilityPublic, + Sensitive: util.Ptr(false), + Language: "en", + CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", + Federated: util.Ptr(true), + ActivityStreamsType: ap.ObjectNote, + }, "remote_account_2_status_1": { ID: "01HE7XJ1CG84TBKH5V9XKBVGF5", URI: "http://example.org/users/Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5", @@ -2125,6 +2210,19 @@ func NewTestPolls() map[string]*gtsmodel.Poll { ClosedAt: time.Time{}, Closing: false, }, + "remote_account_1_status_4_poll": { + ID: "01JDQ0EZ5HM9T4WXRQ5WSVD40J", + Multiple: util.Ptr(false), + HideCounts: util.Ptr(false), + Options: []string{"yes", "no", "maybe", "i don't know", "can you repeat the question"}, + Votes: []int{0, 0, 0, 0, 2}, + Voters: util.Ptr(2), + StatusID: "01JDQ07JZTX9CMDJP67CNA71YD", + // empty expiry AND closed date, i.e. no end + ExpiresAt: time.Time{}, + ClosedAt: time.Time{}, + Closing: false, + }, } } @@ -2184,6 +2282,24 @@ func NewTestPollVotes() map[string]*gtsmodel.PollVote { Poll: nil, CreatedAt: TimeMustParse("2021-09-11T11:47:37+02:00"), }, + "remote_account_1_status_4_poll_vote_local_account_1": { + ID: "01JDQ0SX9QVVFHS7P8M1PA3SVG", + Choices: []int{4}, + AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", + Account: nil, + PollID: "01JDQ0EZ5HM9T4WXRQ5WSVD40J", + Poll: nil, + CreatedAt: TimeMustParse("2024-11-01T09:01:30+02:00"), + }, + "remote_account_1_status_4_poll_vote_local_account_2": { + ID: "01JDQ0T3EEDN7SAVBQMQP4PR12", + Choices: []int{4}, + AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF", + Account: nil, + PollID: "01JDQ0EZ5HM9T4WXRQ5WSVD40J", + Poll: nil, + CreatedAt: TimeMustParse("2024-11-01T09:02:30+02:00"), + }, } } @@ -2341,7 +2457,6 @@ func NewTestMentions() map[string]*gtsmodel.Mention { ID: "01FCTA2Y6FGHXQA4ZE6N5NMNEX", StatusID: "01FCTA44PW9H1TB328S9AQXKDS", CreatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"), - UpdatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"), OriginAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", OriginAccountURI: "http://localhost:8080/users/the_mighty_zork", TargetAccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX", @@ -2353,7 +2468,6 @@ func NewTestMentions() map[string]*gtsmodel.Mention { ID: "01FDF2HM2NF6FSRZCDEDV451CN", StatusID: "01FCQSQ667XHJ9AV9T27SJJSX5", CreatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"), - UpdatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"), OriginAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF", OriginAccountURI: "http://localhost:8080/users/1happyturtle", TargetAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", @@ -2365,7 +2479,6 @@ func NewTestMentions() map[string]*gtsmodel.Mention { ID: "01FN3VKDEF4CN2W9TKX339BEHB", StatusID: "01FN3VJGFH10KR7S2PB0GFJZYG", CreatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"), - UpdatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"), OriginAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF", OriginAccountURI: "http://localhost:8080/users/1happyturtle", TargetAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", @@ -2377,7 +2490,6 @@ func NewTestMentions() map[string]*gtsmodel.Mention { ID: "01FF26A6BGEKCZFWNEHXB2ZZ6M", StatusID: "01FF25D5Q0DH7CHD57CTRS6WK0", CreatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"), - UpdatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"), OriginAccountID: "01F8MH17FWEB39HZJ76B6VXSKF", OriginAccountURI: "http://localhost:8080/users/admin", TargetAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", @@ -2389,7 +2501,6 @@ func NewTestMentions() map[string]*gtsmodel.Mention { ID: "01J5QVP69ANF1K4WHES6GA4WXP", StatusID: "01J5QVB9VC76NPPRQ207GG4DRZ", CreatedAt: TimeMustParse("2024-02-20T12:41:37+02:00"), - UpdatedAt: TimeMustParse("2024-02-20T12:41:37+02:00"), OriginAccountID: "01F8MH17FWEB39HZJ76B6VXSKF", OriginAccountURI: "http://localhost:8080/users/admin", TargetAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF", @@ -2401,7 +2512,6 @@ func NewTestMentions() map[string]*gtsmodel.Mention { ID: "01HE7XQNMKTVC8MNPCE1JGK4J3", StatusID: "01HE7XJ1CG84TBKH5V9XKBVGF5", CreatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"), - UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"), OriginAccountID: "01FHMQX3GAABWSM0S2VZEC2SWC", OriginAccountURI: "http://example.org/users/Some_User", TargetAccountID: "01F8MH17FWEB39HZJ76B6VXSKF", @@ -3518,6 +3628,102 @@ func NewTestInteractionRequests() map[string]*gtsmodel.InteractionRequest { } } +func NewTestStatusEdits() map[string]*gtsmodel.StatusEdit { + return map[string]*gtsmodel.StatusEdit{ + "local_account_1_status_9_edit_1": { + ID: "01JDPZCZ2Y9KSGZW0R7ZG8T8Y2", + Content: "

this is the original status

", + ContentWarning: "", + Text: "this is the original status", + Language: "en", + Sensitive: util.Ptr(false), + AttachmentIDs: nil, + PollOptions: nil, + PollVotes: nil, + StatusID: "01JDPZC707CKDN8N4QVWM4Z1NR", + CreatedAt: TimeMustParse("2024-11-01T11:00:00+02:00"), + }, + "local_account_1_status_9_edit_2": { + ID: "01JDPZDADMD1T9HKF94RECF7PP", + Content: "

this is the first status edit! now with content-warning

", + ContentWarning: "edited status", + Text: "this is the first status edit! now with content-warning", + Language: "en", + Sensitive: util.Ptr(false), + AttachmentIDs: nil, + PollOptions: nil, + PollVotes: nil, + StatusID: "01JDPZC707CKDN8N4QVWM4Z1NR", + CreatedAt: TimeMustParse("2024-11-01T11:01:00+02:00"), + }, + "local_account_2_status_9_edit_1": { + ID: "01JDPZPBXAX0M02YSEPB21KX4R", + Content: "

this is the original status

", + ContentWarning: "", + Text: "this is the original status", + Language: "en", + Sensitive: util.Ptr(false), + AttachmentIDs: nil, + PollOptions: nil, + PollVotes: nil, + StatusID: "01JDPZEZ77X1NX0TY9M10BK1HM", + CreatedAt: TimeMustParse("2024-11-01T10:00:00+02:00"), + }, + "local_account_2_status_9_edit_2": { + ID: "01JDPZPJHKP7E3M0YQXEXPS1YT", + Content: "

now edited to have some media!

", + ContentWarning: "edit with media attachments", + Text: "now edited to have some media!", + Language: "en", + Sensitive: util.Ptr(true), + AttachmentIDs: []string{"01JDQ164HM08SGJ7ZEK9003Z4B"}, + PollOptions: nil, + PollVotes: nil, + StatusID: "01JDPZEZ77X1NX0TY9M10BK1HM", + CreatedAt: TimeMustParse("2024-11-01T10:01:00+02:00"), + }, + "local_account_2_status_9_edit_3": { + ID: "01JDPZPY3F85Y7B78ETRXEMWD9", + Content: "

now edited to remove the media

", + ContentWarning: "edit missing previous media attachments", + Text: "now edited to remove the media", + Language: "en", + Sensitive: util.Ptr(false), + AttachmentIDs: nil, + PollOptions: nil, + PollVotes: nil, + StatusID: "01JDPZEZ77X1NX0TY9M10BK1HM", + CreatedAt: TimeMustParse("2024-11-01T10:02:00+02:00"), + }, + "remote_account_1_status_4_edit_1": { + ID: "01JDQ07ZZ4FGP13YN8TF63P5A6", + Content: "

this is the original status, with a poll!

", + ContentWarning: "", + Text: "this is the original status, with a poll!", + Language: "en", + Sensitive: util.Ptr(false), + AttachmentIDs: nil, + PollOptions: []string{"yes", "no", "spiderman"}, + PollVotes: []int{42, 42, 69}, + StatusID: "01JDQ07JZTX9CMDJP67CNA71YD", + CreatedAt: TimeMustParse("2024-11-01T09:00:00+02:00"), + }, + "remote_account_1_status_4_edit_2": { + ID: "01JDQ08AYQC0G6413VAHA51CV9", + Content: "

this is the first status edit! now with a different poll!

", + ContentWarning: "edited status", + Text: "this is the first status edit! now with a different poll!", + Language: "en", + Sensitive: util.Ptr(false), + AttachmentIDs: nil, + PollOptions: []string{"yes", "no", "maybe", "i don't know", "can you repeat the question"}, + PollVotes: []int{0, 0, 0, 0, 1}, + StatusID: "01JDQ07JZTX9CMDJP67CNA71YD", + CreatedAt: TimeMustParse("2024-11-01T09:01:00+02:00"), + }, + } +} + // GetSignatureForActivity prepares a mock HTTP request as if it were going to deliver activity to destination signed for privkey and pubKeyID, signs the request and returns the header values. func GetSignatureForActivity(activity pub.Activity, pubKeyID string, privkey *rsa.PrivateKey, destination *url.URL) (signatureHeader string, digestHeader string, dateHeader string) { // convert the activity into json bytes diff --git a/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffmpreg.wasm.gz b/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffmpreg.wasm.gz index 1545d58bc..116fcf802 100644 Binary files a/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffmpreg.wasm.gz and b/vendor/codeberg.org/gruf/go-ffmpreg/embed/ffmpreg.wasm.gz differ diff --git a/vendor/github.com/minio/minio-go/v7/api-prompt-object.go b/vendor/github.com/minio/minio-go/v7/api-prompt-object.go new file mode 100644 index 000000000..dac062a75 --- /dev/null +++ b/vendor/github.com/minio/minio-go/v7/api-prompt-object.go @@ -0,0 +1,78 @@ +/* + * MinIO Go Library for Amazon S3 Compatible Cloud Storage + * Copyright 2015-2024 MinIO, Inc. + * + * 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. + */ + +package minio + +import ( + "bytes" + "context" + "io" + "net/http" + + "github.com/goccy/go-json" + "github.com/minio/minio-go/v7/pkg/s3utils" +) + +// PromptObject performs language model inference with the prompt and referenced object as context. +// Inference is performed using a Lambda handler that can process the prompt and object. +// Currently, this functionality is limited to certain MinIO servers. +func (c *Client) PromptObject(ctx context.Context, bucketName, objectName, prompt string, opts PromptObjectOptions) (io.ReadCloser, error) { + // Input validation. + if err := s3utils.CheckValidBucketName(bucketName); err != nil { + return nil, ErrorResponse{ + StatusCode: http.StatusBadRequest, + Code: "InvalidBucketName", + Message: err.Error(), + } + } + if err := s3utils.CheckValidObjectName(objectName); err != nil { + return nil, ErrorResponse{ + StatusCode: http.StatusBadRequest, + Code: "XMinioInvalidObjectName", + Message: err.Error(), + } + } + + opts.AddLambdaArnToReqParams(opts.LambdaArn) + opts.SetHeader("Content-Type", "application/json") + opts.AddPromptArg("prompt", prompt) + promptReqBytes, err := json.Marshal(opts.PromptArgs) + if err != nil { + return nil, err + } + + // Execute POST on bucket/object. + resp, err := c.executeMethod(ctx, http.MethodPost, requestMetadata{ + bucketName: bucketName, + objectName: objectName, + queryValues: opts.toQueryValues(), + customHeader: opts.Header(), + contentSHA256Hex: sum256Hex(promptReqBytes), + contentBody: bytes.NewReader(promptReqBytes), + contentLength: int64(len(promptReqBytes)), + }) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + defer closeResponse(resp) + return nil, httpRespToErrorResponse(resp, bucketName, objectName) + } + + return resp.Body, nil +} diff --git a/vendor/github.com/minio/minio-go/v7/api-prompt-options.go b/vendor/github.com/minio/minio-go/v7/api-prompt-options.go new file mode 100644 index 000000000..4493a75d4 --- /dev/null +++ b/vendor/github.com/minio/minio-go/v7/api-prompt-options.go @@ -0,0 +1,84 @@ +/* + * MinIO Go Library for Amazon S3 Compatible Cloud Storage + * Copyright 2015-2024 MinIO, Inc. + * + * 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. + */ + +package minio + +import ( + "net/http" + "net/url" +) + +// PromptObjectOptions provides options to PromptObject call. +// LambdaArn is the ARN of the Prompt Lambda to be invoked. +// PromptArgs is a map of key-value pairs to be passed to the inference action on the Prompt Lambda. +// "prompt" is a reserved key and should not be used as a key in PromptArgs. +type PromptObjectOptions struct { + LambdaArn string + PromptArgs map[string]any + headers map[string]string + reqParams url.Values +} + +// Header returns the http.Header representation of the POST options. +func (o PromptObjectOptions) Header() http.Header { + headers := make(http.Header, len(o.headers)) + for k, v := range o.headers { + headers.Set(k, v) + } + return headers +} + +// AddPromptArg Add a key value pair to the prompt arguments where the key is a string and +// the value is a JSON serializable. +func (o *PromptObjectOptions) AddPromptArg(key string, value any) { + if o.PromptArgs == nil { + o.PromptArgs = make(map[string]any) + } + o.PromptArgs[key] = value +} + +// AddLambdaArnToReqParams adds the lambdaArn to the request query string parameters. +func (o *PromptObjectOptions) AddLambdaArnToReqParams(lambdaArn string) { + if o.reqParams == nil { + o.reqParams = make(url.Values) + } + o.reqParams.Add("lambdaArn", lambdaArn) +} + +// SetHeader adds a key value pair to the options. The +// key-value pair will be part of the HTTP POST request +// headers. +func (o *PromptObjectOptions) SetHeader(key, value string) { + if o.headers == nil { + o.headers = make(map[string]string) + } + o.headers[http.CanonicalHeaderKey(key)] = value +} + +// toQueryValues - Convert the reqParams in Options to query string parameters. +func (o *PromptObjectOptions) toQueryValues() url.Values { + urlValues := make(url.Values) + if o.reqParams != nil { + for key, values := range o.reqParams { + for _, value := range values { + urlValues.Add(key, value) + } + } + } + + return urlValues +} diff --git a/vendor/github.com/minio/minio-go/v7/api-put-object-fan-out.go b/vendor/github.com/minio/minio-go/v7/api-put-object-fan-out.go index 0ae9142e1..3023b949c 100644 --- a/vendor/github.com/minio/minio-go/v7/api-put-object-fan-out.go +++ b/vendor/github.com/minio/minio-go/v7/api-put-object-fan-out.go @@ -85,7 +85,10 @@ func (c *Client) PutObjectFanOut(ctx context.Context, bucket string, fanOutData policy.SetEncryption(fanOutReq.SSE) // Set checksum headers if any. - policy.SetChecksum(fanOutReq.Checksum) + err := policy.SetChecksum(fanOutReq.Checksum) + if err != nil { + return nil, err + } url, formData, err := c.PresignedPostPolicy(ctx, policy) if err != nil { diff --git a/vendor/github.com/minio/minio-go/v7/api.go b/vendor/github.com/minio/minio-go/v7/api.go index 380ec4fde..88e8d4347 100644 --- a/vendor/github.com/minio/minio-go/v7/api.go +++ b/vendor/github.com/minio/minio-go/v7/api.go @@ -133,7 +133,7 @@ type Options struct { // Global constants. const ( libraryName = "minio-go" - libraryVersion = "v7.0.80" + libraryVersion = "v7.0.81" ) // User Agent should always following the below style. diff --git a/vendor/github.com/minio/minio-go/v7/functional_tests.go b/vendor/github.com/minio/minio-go/v7/functional_tests.go index c0180b36b..43383d134 100644 --- a/vendor/github.com/minio/minio-go/v7/functional_tests.go +++ b/vendor/github.com/minio/minio-go/v7/functional_tests.go @@ -160,7 +160,7 @@ func logError(testName, function string, args map[string]interface{}, startTime } else { logFailure(testName, function, args, startTime, alert, message, err) if !isRunOnFail() { - panic(err) + panic(fmt.Sprintf("Test failed with message: %s, err: %v", message, err)) } } } @@ -393,6 +393,42 @@ func getFuncNameLoc(caller int) string { return strings.TrimPrefix(runtime.FuncForPC(pc).Name(), "main.") } +type ClientConfig struct { + // MinIO client configuration + TraceOn bool // Turn on tracing of HTTP requests and responses to stderr + CredsV2 bool // Use V2 credentials if true, otherwise use v4 + TrailingHeaders bool // Send trailing headers in requests +} + +func NewClient(config ClientConfig) (*minio.Client, error) { + // Instantiate new MinIO client + var creds *credentials.Credentials + if config.CredsV2 { + creds = credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), "") + } else { + creds = credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), "") + } + opts := &minio.Options{ + Creds: creds, + Transport: createHTTPTransport(), + Secure: mustParseBool(os.Getenv(enableHTTPS)), + TrailingHeaders: config.TrailingHeaders, + } + client, err := minio.New(os.Getenv(serverEndpoint), opts) + if err != nil { + return nil, err + } + + if config.TraceOn { + client.TraceOn(os.Stderr) + } + + // Set user agent. + client.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + + return client, nil +} + // Tests bucket re-create errors. func testMakeBucketError() { region := "eu-central-1" @@ -407,27 +443,12 @@ function := "MakeBucket(bucketName, region)" "region": region, } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - Transport: createHTTPTransport(), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -462,20 +483,12 @@ function := "PutObject(bucketName, objectName, reader, objectSize, opts)" "objectName": "", "opts.UserMetadata": "", } - rand.Seed(startTime.Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - Transport: createHTTPTransport(), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client creation failed", err) return } - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -531,27 +544,12 @@ function := "MakeBucket(bucketName, region)" "region": region, } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -598,27 +596,12 @@ function := "PutObject(bucketName, objectName, reader, opts)" "opts": "objectContentType", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -697,27 +680,12 @@ function := "ListObjectVersions(bucketName, prefix, recursive)" "recursive": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -817,27 +785,12 @@ func testStatObjectWithVersioning() { function := "StatObject" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -935,27 +888,12 @@ func testGetObjectWithVersioning() { function := "GetObject()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -1075,27 +1013,12 @@ func testPutObjectWithVersioning() { function := "GetObject()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -1223,28 +1146,12 @@ func testListMultipartUpload() { function := "GetObject()" args := map[string]interface{}{} - // Instantiate new minio client object. - opts := &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - } - c, err := minio.New(os.Getenv(serverEndpoint), opts) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - core, err := minio.NewCore(os.Getenv(serverEndpoint), opts) - if err != nil { - logError(testName, function, args, startTime, "", "MinIO core client object creation failed", err) - return - } - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + core := minio.Core{Client: c} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") @@ -1347,27 +1254,12 @@ func testCopyObjectWithVersioning() { function := "CopyObject()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -1485,27 +1377,12 @@ func testConcurrentCopyObjectWithVersioning() { function := "CopyObject()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -1646,27 +1523,12 @@ func testComposeObjectWithVersioning() { function := "ComposeObject()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -1787,27 +1649,12 @@ func testRemoveObjectWithVersioning() { function := "DeleteObject()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -1900,27 +1747,12 @@ func testRemoveObjectsWithVersioning() { function := "DeleteObjects()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -1996,27 +1828,12 @@ func testObjectTaggingWithVersioning() { function := "{Get,Set,Remove}ObjectTagging()" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -2164,27 +1981,12 @@ function := "PutObject(bucketName, objectName, reader,size, opts)" return } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -2230,7 +2032,7 @@ function := "PutObject(bucketName, objectName, reader,size, opts)" h := test.cs.Hasher() h.Reset() - // Test with Wrong CRC. + // Test with a bad CRC - we haven't called h.Write(b), so this is a checksum of empty data meta[test.cs.Key()] = base64.StdEncoding.EncodeToString(h.Sum(nil)) args["metadata"] = meta args["range"] = "false" @@ -2350,28 +2152,12 @@ function := "PutObject(bucketName, objectName, reader,size, opts)" return } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - TrailingHeaders: true, - }) + c, err := NewClient(ClientConfig{TrailingHeaders: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -2541,28 +2327,12 @@ function := "PutObject(bucketName, objectName, reader,size, opts)" return } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - TrailingHeaders: trailing, - }) + c, err := NewClient(ClientConfig{TrailingHeaders: trailing}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -2620,7 +2390,7 @@ function := "PutObject(bucketName, objectName, reader,size, opts)" cmpChecksum := func(got, want string) { if want != got { logError(testName, function, args, startTime, "", "checksum mismatch", fmt.Errorf("want %s, got %s", want, got)) - //fmt.Printf("want %s, got %s\n", want, got) + // fmt.Printf("want %s, got %s\n", want, got) return } } @@ -2741,25 +2511,12 @@ function := "PutObject(bucketName, objectName, reader,size, opts)" return } - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - TrailingHeaders: true, - }) + c, err := NewClient(ClientConfig{TrailingHeaders: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -2881,7 +2638,6 @@ function := "PutObject(bucketName, objectName, reader,size, opts)" test.ChecksumCRC32C = hashMultiPart(b, int(test.PO.PartSize), test.hasher) // Set correct CRC. - // c.TraceOn(os.Stderr) resp, err := c.PutObject(context.Background(), bucketName, objectName, bytes.NewReader(b), int64(bufSize), test.PO) if err != nil { logError(testName, function, args, startTime, "", "PutObject failed", err) @@ -2933,6 +2689,8 @@ function := "PutObject(bucketName, objectName, reader,size, opts)" delete(args, "metadata") } + + logSuccess(testName, function, args, startTime) } // Test PutObject with custom checksums. @@ -2952,25 +2710,12 @@ function := "PutObject(bucketName, objectName, reader,size, opts)" return } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - TrailingHeaders: true, - }) + c, err := NewClient(ClientConfig{TrailingHeaders: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -2997,8 +2742,6 @@ function := "PutObject(bucketName, objectName, reader,size, opts)" {header: "x-amz-checksum-crc32c", hasher: crc32.New(crc32.MakeTable(crc32.Castagnoli))}, } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) // defer c.TraceOff() for i, test := range tests { @@ -3108,20 +2851,12 @@ function := "GetObjectAttributes(ctx, bucketName, objectName, opts)" return } - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - TrailingHeaders: true, - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{TrailingHeaders: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName err = c.MakeBucket( @@ -3315,19 +3050,12 @@ function := "GetObjectAttributes(ctx, bucketName, objectName, opts)" return } - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - TrailingHeaders: true, - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - Transport: createHTTPTransport(), - }) + c, err := NewClient(ClientConfig{TrailingHeaders: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName err = c.MakeBucket( @@ -3401,19 +3129,12 @@ function := "GetObjectAttributes(ctx, bucketName, objectName, opts)" return } - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - TrailingHeaders: true, - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{TrailingHeaders: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) unknownBucket := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-bucket-") unknownObject := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-object-") @@ -3657,27 +3378,12 @@ function := "PutObject(bucketName, objectName, reader,size, opts)" return } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -3764,27 +3470,12 @@ function := "PutObject(bucketName, objectName, reader, size, opts)" "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -3834,27 +3525,12 @@ function := "PutObject(bucketName, objectName, reader,size,opts)" "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -3906,27 +3582,12 @@ func testGetObjectSeekEnd() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4029,27 +3690,12 @@ func testGetObjectClosedTwice() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4120,26 +3766,13 @@ function := "RemoveObjects(ctx, bucketName, objectsCh)" "bucketName": "", } - // Seed random based on current tie. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Enable tracing, write to stdout. - // c.TraceOn(os.Stderr) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4217,27 +3850,12 @@ function := "RemoveObjects(bucketName, objectsCh)" "bucketName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - - // Enable tracing, write to stdout. - // c.TraceOn(os.Stderr) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4301,27 +3919,12 @@ function := "RemoveObjects(bucketName, objectsCh)" "bucketName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - - // Enable tracing, write to stdout. - // c.TraceOn(os.Stderr) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4437,27 +4040,12 @@ function := "FPutObject(bucketName, objectName, fileName, opts)" "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4543,27 +4131,12 @@ function := "FPutObject(bucketName, objectName, fileName, opts)" "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") location := "us-east-1" @@ -4713,27 +4286,13 @@ function := "FPutObject(bucketName, objectName, fileName, opts)" "fileName": "", "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4814,27 +4373,13 @@ function := "FPutObjectContext(ctx, bucketName, objectName, fileName, opts)" "objectName": "", "opts": "minio.PutObjectOptions{ContentType:objectContentType}", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4919,24 +4464,12 @@ function := "PutObject(ctx, bucketName, objectName, fileName, opts)" "opts": "", } - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Make a new bucket. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -4989,27 +4522,12 @@ func testGetObjectS3Zip() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{"x-minio-extract": true} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -5173,27 +4691,12 @@ func testGetObjectReadSeekFunctional() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -5343,27 +4846,12 @@ func testGetObjectReadAtFunctional() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -5521,27 +5009,12 @@ func testGetObjectReadAtWhenEOFWasReached() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -5641,27 +5114,12 @@ function := "PresignedPostPolicy(policy)" "policy": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") @@ -5689,50 +5147,22 @@ function := "PresignedPostPolicy(policy)" return } - // Save the data - _, err = c.PutObject(context.Background(), bucketName, objectName, bytes.NewReader(buf), int64(len(buf)), minio.PutObjectOptions{ContentType: "binary/octet-stream"}) - if err != nil { - logError(testName, function, args, startTime, "", "PutObject failed", err) - return - } - policy := minio.NewPostPolicy() - - if err := policy.SetBucket(""); err == nil { - logError(testName, function, args, startTime, "", "SetBucket did not fail for invalid conditions", err) - return - } - if err := policy.SetKey(""); err == nil { - logError(testName, function, args, startTime, "", "SetKey did not fail for invalid conditions", err) - return - } - if err := policy.SetExpires(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)); err == nil { - logError(testName, function, args, startTime, "", "SetExpires did not fail for invalid conditions", err) - return - } - if err := policy.SetContentType(""); err == nil { - logError(testName, function, args, startTime, "", "SetContentType did not fail for invalid conditions", err) - return - } - if err := policy.SetContentLengthRange(1024*1024, 1024); err == nil { - logError(testName, function, args, startTime, "", "SetContentLengthRange did not fail for invalid conditions", err) - return - } - if err := policy.SetUserMetadata("", ""); err == nil { - logError(testName, function, args, startTime, "", "SetUserMetadata did not fail for invalid conditions", err) - return - } - policy.SetBucket(bucketName) policy.SetKey(objectName) policy.SetExpires(time.Now().UTC().AddDate(0, 0, 10)) // expires in 10 days policy.SetContentType("binary/octet-stream") policy.SetContentLengthRange(10, 1024*1024) policy.SetUserMetadata(metadataKey, metadataValue) + policy.SetContentEncoding("gzip") // Add CRC32C checksum := minio.ChecksumCRC32C.ChecksumBytes(buf) - policy.SetChecksum(checksum) + err = policy.SetChecksum(checksum) + if err != nil { + logError(testName, function, args, startTime, "", "SetChecksum failed", err) + return + } args["policy"] = policy.String() @@ -5828,7 +5258,7 @@ function := "PresignedPostPolicy(policy)" expectedLocation := scheme + os.Getenv(serverEndpoint) + "/" + bucketName + "/" + objectName expectedLocationBucketDNS := scheme + bucketName + "." + os.Getenv(serverEndpoint) + "/" + objectName - if !strings.Contains(expectedLocation, "s3.amazonaws.com/") { + if !strings.Contains(expectedLocation, ".amazonaws.com/") { // Test when not against AWS S3. if val, ok := res.Header["Location"]; ok { if val[0] != expectedLocation && val[0] != expectedLocationBucketDNS { @@ -5840,9 +5270,194 @@ function := "PresignedPostPolicy(policy)" return } } - want := checksum.Encoded() - if got := res.Header.Get("X-Amz-Checksum-Crc32c"); got != want { - logError(testName, function, args, startTime, "", fmt.Sprintf("Want checksum %q, got %q", want, got), nil) + wantChecksumCrc32c := checksum.Encoded() + if got := res.Header.Get("X-Amz-Checksum-Crc32c"); got != wantChecksumCrc32c { + logError(testName, function, args, startTime, "", fmt.Sprintf("Want checksum %q, got %q", wantChecksumCrc32c, got), nil) + return + } + + // Ensure that when we subsequently GetObject, the checksum is returned + gopts := minio.GetObjectOptions{Checksum: true} + r, err := c.GetObject(context.Background(), bucketName, objectName, gopts) + if err != nil { + logError(testName, function, args, startTime, "", "GetObject failed", err) + return + } + st, err := r.Stat() + if err != nil { + logError(testName, function, args, startTime, "", "Stat failed", err) + return + } + if st.ChecksumCRC32C != wantChecksumCrc32c { + logError(testName, function, args, startTime, "", fmt.Sprintf("Want checksum %s, got %s", wantChecksumCrc32c, st.ChecksumCRC32C), nil) + return + } + + logSuccess(testName, function, args, startTime) +} + +// testPresignedPostPolicyWrongFile tests that when we have a policy with a checksum, we cannot POST the wrong file +func testPresignedPostPolicyWrongFile() { + // initialize logging params + startTime := time.Now() + testName := getFuncName() + function := "PresignedPostPolicy(policy)" + args := map[string]interface{}{ + "policy": "", + } + + c, err := NewClient(ClientConfig{}) + if err != nil { + logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) + return + } + + // Generate a new random bucket name. + bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") + + // Make a new bucket in 'us-east-1' (source bucket). + err = c.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{Region: "us-east-1"}) + if err != nil { + logError(testName, function, args, startTime, "", "MakeBucket failed", err) + return + } + + defer cleanupBucket(bucketName, c) + + // Generate 33K of data. + reader := getDataReader("datafile-33-kB") + defer reader.Close() + + objectName := randString(60, rand.NewSource(time.Now().UnixNano()), "") + // Azure requires the key to not start with a number + metadataKey := randString(60, rand.NewSource(time.Now().UnixNano()), "user") + metadataValue := randString(60, rand.NewSource(time.Now().UnixNano()), "") + + buf, err := io.ReadAll(reader) + if err != nil { + logError(testName, function, args, startTime, "", "ReadAll failed", err) + return + } + + policy := minio.NewPostPolicy() + policy.SetBucket(bucketName) + policy.SetKey(objectName) + policy.SetExpires(time.Now().UTC().AddDate(0, 0, 10)) // expires in 10 days + policy.SetContentType("binary/octet-stream") + policy.SetContentLengthRange(10, 1024*1024) + policy.SetUserMetadata(metadataKey, metadataValue) + + // Add CRC32C of the 33kB file that the policy will explicitly allow. + checksum := minio.ChecksumCRC32C.ChecksumBytes(buf) + err = policy.SetChecksum(checksum) + if err != nil { + logError(testName, function, args, startTime, "", "SetChecksum failed", err) + return + } + + args["policy"] = policy.String() + + presignedPostPolicyURL, formData, err := c.PresignedPostPolicy(context.Background(), policy) + if err != nil { + logError(testName, function, args, startTime, "", "PresignedPostPolicy failed", err) + return + } + + // At this stage, we have a policy that allows us to upload datafile-33-kB. + // Test that uploading datafile-10-kB, with a different checksum, fails as expected + filePath := getMintDataDirFilePath("datafile-10-kB") + if filePath == "" { + // Make a temp file with 10 KB data. + file, err := os.CreateTemp(os.TempDir(), "PresignedPostPolicyTest") + if err != nil { + logError(testName, function, args, startTime, "", "TempFile creation failed", err) + return + } + if _, err = io.Copy(file, getDataReader("datafile-10-kB")); err != nil { + logError(testName, function, args, startTime, "", "Copy failed", err) + return + } + if err = file.Close(); err != nil { + logError(testName, function, args, startTime, "", "File Close failed", err) + return + } + filePath = file.Name() + } + fileReader := getDataReader("datafile-10-kB") + defer fileReader.Close() + buf10k, err := io.ReadAll(fileReader) + if err != nil { + logError(testName, function, args, startTime, "", "ReadAll failed", err) + return + } + otherChecksum := minio.ChecksumCRC32C.ChecksumBytes(buf10k) + + var formBuf bytes.Buffer + writer := multipart.NewWriter(&formBuf) + for k, v := range formData { + if k == "x-amz-checksum-crc32c" { + v = otherChecksum.Encoded() + } + writer.WriteField(k, v) + } + + // Add file to post request + f, err := os.Open(filePath) + defer f.Close() + if err != nil { + logError(testName, function, args, startTime, "", "File open failed", err) + return + } + w, err := writer.CreateFormFile("file", filePath) + if err != nil { + logError(testName, function, args, startTime, "", "CreateFormFile failed", err) + return + } + _, err = io.Copy(w, f) + if err != nil { + logError(testName, function, args, startTime, "", "Copy failed", err) + return + } + writer.Close() + + httpClient := &http.Client{ + Timeout: 30 * time.Second, + Transport: createHTTPTransport(), + } + args["url"] = presignedPostPolicyURL.String() + + req, err := http.NewRequest(http.MethodPost, presignedPostPolicyURL.String(), bytes.NewReader(formBuf.Bytes())) + if err != nil { + logError(testName, function, args, startTime, "", "HTTP request failed", err) + return + } + + req.Header.Set("Content-Type", writer.FormDataContentType()) + + // Make the POST request with the form data. + res, err := httpClient.Do(req) + if err != nil { + logError(testName, function, args, startTime, "", "HTTP request failed", err) + return + } + defer res.Body.Close() + if res.StatusCode != http.StatusForbidden { + logError(testName, function, args, startTime, "", "HTTP request unexpected status", errors.New(res.Status)) + return + } + + // Read the response body, ensure it has checksum failure message + resBody, err := io.ReadAll(res.Body) + if err != nil { + logError(testName, function, args, startTime, "", "ReadAll failed", err) + return + } + + // Normalize the response body, because S3 uses quotes around the policy condition components + // in the error message, MinIO does not. + resBodyStr := strings.ReplaceAll(string(resBody), `"`, "") + if !strings.Contains(resBodyStr, "Policy Condition failed: [eq, $x-amz-checksum-crc32c, aHnJMw==]") { + logError(testName, function, args, startTime, "", "Unexpected response body", errors.New(resBodyStr)) return } @@ -5857,27 +5472,12 @@ func testCopyObject() { function := "CopyObject(dst, src)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") @@ -6052,27 +5652,12 @@ func testSSECEncryptedGetObjectReadSeekFunctional() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -6235,27 +5820,12 @@ func testSSES3EncryptedGetObjectReadSeekFunctional() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -6416,27 +5986,12 @@ func testSSECEncryptedGetObjectReadAtFunctional() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -6600,27 +6155,12 @@ func testSSES3EncryptedGetObjectReadAtFunctional() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -6785,27 +6325,13 @@ function := "PutEncryptedObject(bucketName, objectName, reader, sse)" "objectName": "", "sse": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -6895,27 +6421,13 @@ function := "FPutEncryptedObject(bucketName, objectName, filePath, contentType, "contentType": "", "sse": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -7018,27 +6530,13 @@ function := "PutEncryptedObject(bucketName, objectName, reader, sse)" "objectName": "", "sse": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -7126,27 +6624,13 @@ function := "FPutEncryptedObject(bucketName, objectName, filePath, contentType, "contentType": "", "sse": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -7255,26 +6739,12 @@ function := "SetBucketNotification(bucketName)" return } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable to debug - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - bucketName := os.Getenv("NOTIFY_BUCKET") args["bucketName"] = bucketName @@ -7350,26 +6820,12 @@ function := "testFunctional()" functionAll := "" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, nil, startTime, "", "MinIO client object creation failed", err) return } - // Enable to debug - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") @@ -8029,24 +7485,12 @@ func testGetObjectModified() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Make a new bucket. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -8125,24 +7569,12 @@ function := "PutObject(bucketName, objectName, fileToUpload, contentType)" "contentType": "binary/octet-stream", } - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Make a new bucket. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -8245,27 +7677,12 @@ function := "MakeBucket(bucketName, region)" "region": "eu-west-1", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") region := "eu-west-1" @@ -8305,27 +7722,12 @@ function := "MakeBucket(bucketName, region)" "region": "eu-west-1", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -8396,27 +7798,12 @@ function := "FPutObject(bucketName, objectName, fileName, opts)" "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -8557,27 +7944,12 @@ function := "MakeBucket(bucketName, region)" "region": "eu-west-1", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -8620,27 +7992,12 @@ func testGetObjectReadSeekFunctionalV2() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -8775,27 +8132,12 @@ func testGetObjectReadAtFunctionalV2() { function := "GetObject(bucketName, objectName)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -8937,27 +8279,12 @@ func testCopyObjectV2() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") @@ -9156,13 +8483,7 @@ func testComposeObjectErrorCasesV2() { function := "ComposeObject(destination, sourceList)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9254,13 +8575,7 @@ func testCompose10KSourcesV2() { function := "ComposeObject(destination, sourceList)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9276,13 +8591,7 @@ func testEncryptedEmptyObject() { function := "PutObject(bucketName, objectName, reader, objectSize, opts)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return @@ -9430,7 +8739,7 @@ function := "CopyObject(destination, source)" dstEncryption = sseDst } // 3. get copied object and check if content is equal - coreClient := minio.Core{c} + coreClient := minio.Core{Client: c} reader, _, _, err := coreClient.GetObject(context.Background(), bucketName, "dstObject", minio.GetObjectOptions{ServerSideEncryption: dstEncryption}) if err != nil { logError(testName, function, args, startTime, "", "GetObject failed", err) @@ -9537,13 +8846,7 @@ func testUnencryptedToSSECCopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9552,7 +8855,6 @@ function := "CopyObject(destination, source)" bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") sseDst := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"dstObject")) - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, nil, sseDst) } @@ -9564,13 +8866,7 @@ func testUnencryptedToSSES3CopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9580,7 +8876,6 @@ function := "CopyObject(destination, source)" var sseSrc encrypt.ServerSide sseDst := encrypt.NewSSE() - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9592,13 +8887,7 @@ func testUnencryptedToUnencryptedCopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9607,7 +8896,6 @@ function := "CopyObject(destination, source)" bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") var sseSrc, sseDst encrypt.ServerSide - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9619,13 +8907,7 @@ func testEncryptedSSECToSSECCopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9635,7 +8917,6 @@ function := "CopyObject(destination, source)" sseSrc := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"srcObject")) sseDst := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"dstObject")) - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9647,13 +8928,7 @@ func testEncryptedSSECToSSES3CopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9663,7 +8938,6 @@ function := "CopyObject(destination, source)" sseSrc := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"srcObject")) sseDst := encrypt.NewSSE() - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9675,13 +8949,7 @@ func testEncryptedSSECToUnencryptedCopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9691,7 +8959,6 @@ function := "CopyObject(destination, source)" sseSrc := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"srcObject")) var sseDst encrypt.ServerSide - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9703,13 +8970,7 @@ func testEncryptedSSES3ToSSECCopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9719,7 +8980,6 @@ function := "CopyObject(destination, source)" sseSrc := encrypt.NewSSE() sseDst := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"dstObject")) - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9731,13 +8991,7 @@ func testEncryptedSSES3ToSSES3CopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9747,7 +9001,6 @@ function := "CopyObject(destination, source)" sseSrc := encrypt.NewSSE() sseDst := encrypt.NewSSE() - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9759,13 +9012,7 @@ func testEncryptedSSES3ToUnencryptedCopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9775,7 +9022,6 @@ function := "CopyObject(destination, source)" sseSrc := encrypt.NewSSE() var sseDst encrypt.ServerSide - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9787,13 +9033,7 @@ func testEncryptedCopyObjectV2() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9803,7 +9043,6 @@ function := "CopyObject(destination, source)" sseSrc := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"srcObject")) sseDst := encrypt.DefaultPBKDF([]byte("correct horse battery staple"), []byte(bucketName+"dstObject")) - // c.TraceOn(os.Stderr) testEncryptedCopyObjectWrapper(c, bucketName, sseSrc, sseDst) } @@ -9814,13 +9053,7 @@ func testDecryptedCopyObject() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return @@ -9874,26 +9107,14 @@ func testSSECMultipartEncryptedToSSECCopyObjectPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -10072,26 +9293,14 @@ func testSSECEncryptedToSSECCopyObjectPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -10250,26 +9459,14 @@ func testSSECEncryptedToUnencryptedCopyPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -10427,26 +9624,14 @@ func testSSECEncryptedToSSES3CopyObjectPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -10607,26 +9792,14 @@ func testUnencryptedToSSECCopyObjectPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -10782,26 +9955,14 @@ func testUnencryptedToUnencryptedCopyPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -10953,26 +10114,14 @@ func testUnencryptedToSSES3CopyObjectPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -11126,26 +10275,14 @@ func testSSES3EncryptedToSSECCopyObjectPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -11302,26 +10439,14 @@ func testSSES3EncryptedToUnencryptedCopyPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -11474,26 +10599,14 @@ func testSSES3EncryptedToSSES3CopyObjectPart() { function := "CopyObjectPart(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - client, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + client, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return } // Instantiate new core client object. - c := minio.Core{client} - - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + c := minio.Core{Client: client} // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test") @@ -11648,19 +10761,12 @@ func testUserMetadataCopying() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // c.TraceOn(os.Stderr) testUserMetadataCopyingWrapper(c) } @@ -11825,19 +10931,12 @@ func testUserMetadataCopyingV2() { function := "CopyObject(destination, source)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) return } - // c.TraceOn(os.Stderr) testUserMetadataCopyingWrapper(c) } @@ -11848,13 +10947,7 @@ function := "testStorageClassMetadataPutObject()" args := map[string]interface{}{} testName := getFuncName() - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return @@ -11936,13 +11029,7 @@ function := "testStorageClassInvalidMetadataPutObject()" args := map[string]interface{}{} testName := getFuncName() - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return @@ -11979,13 +11066,7 @@ function := "testStorageClassMetadataCopyObject()" args := map[string]interface{}{} testName := getFuncName() - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - Transport: createHTTPTransport(), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO v4 client object creation failed", err) return @@ -12106,27 +11187,12 @@ function := "PutObject(bucketName, objectName, reader, size, opts)" "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { - logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) + logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -12182,27 +11248,12 @@ function := "PutObject(bucketName, objectName, reader,size,opts)" "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { - logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) + logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -12273,27 +11324,12 @@ function := "PutObject(bucketName, objectName, reader, size, opts)" "opts": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { - logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) + logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -12338,13 +11374,7 @@ func testComposeObjectErrorCases() { function := "ComposeObject(destination, sourceList)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return @@ -12361,13 +11391,7 @@ func testCompose10KSources() { function := "ComposeObject(destination, sourceList)" args := map[string]interface{}{} - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return @@ -12385,26 +11409,12 @@ function := "testFunctionalV2()" functionAll := "" args := map[string]interface{}{} - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - Transport: createHTTPTransport(), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) return } - // Enable to debug - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") location := "us-east-1" @@ -12838,27 +11848,13 @@ function := "GetObject(ctx, bucketName, objectName)" "bucketName": "", "objectName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -12941,27 +11937,13 @@ function := "FGetObject(ctx, bucketName, objectName, fileName)" "objectName": "", "fileName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -13033,24 +12015,12 @@ function := "GetObject(ctx, bucketName, objectName, fileName)" defer cancel() rng := rand.NewSource(time.Now().UnixNano()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rng, "minio-go-test-") args["bucketName"] = bucketName @@ -13140,27 +12110,13 @@ function := "GetObjectACL(ctx, bucketName, objectName)" "bucketName": "", "objectName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -13318,24 +12274,12 @@ function := "PutObject(ctx, bucketName, objectName, reader, size, opts)" "size": "", "opts": "", } - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { - logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) + logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Make a new bucket. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -13390,27 +12334,13 @@ function := "GetObject(ctx, bucketName, objectName)" "bucketName": "", "objectName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { - logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) + logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -13491,27 +12421,13 @@ function := "FGetObject(ctx, bucketName, objectName,fileName)" "objectName": "", "fileName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV2(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{CredsV2: true}) if err != nil { - logError(testName, function, args, startTime, "", "MinIO client v2 object creation failed", err) + logError(testName, function, args, startTime, "", "MinIO v2 client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -13580,27 +12496,13 @@ function := "ListObjects(bucketName, objectPrefix, recursive, doneCh)" "objectPrefix": "", "recursive": "true", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -13684,24 +12586,12 @@ function := "SetBucketCors(bucketName, cors)" "cors": "", } - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Create or reuse a bucket that will get cors settings applied to it and deleted when done bucketName := os.Getenv("MINIO_GO_TEST_BUCKET_CORS") if bucketName == "" { @@ -14420,24 +13310,12 @@ function := "SetBucketCors(bucketName, cors)" "cors": "", } - // Instantiate new minio client object - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -14519,27 +13397,13 @@ function := "RemoveObjects(bucketName, objectsCh, opts)" "objectPrefix": "", "recursive": "true", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -14653,27 +13517,13 @@ function := "GetBucketTagging(bucketName)" args := map[string]interface{}{ "bucketName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -14709,27 +13559,13 @@ function := "SetBucketTagging(bucketName, tags)" "bucketName": "", "tags": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -14795,27 +13631,13 @@ function := "RemoveBucketTagging(bucketName)" args := map[string]interface{}{ "bucketName": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object. - c, err := minio.New(os.Getenv(serverEndpoint), - &minio.Options{ - Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), - Transport: createHTTPTransport(), - Secure: mustParseBool(os.Getenv(enableHTTPS)), - }) + c, err := NewClient(ClientConfig{}) if err != nil { logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) return } - // Enable tracing, write to stderr. - // c.TraceOn(os.Stderr) - - // Set user agent. - c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) - // Generate a new random bucket name. bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") args["bucketName"] = bucketName @@ -14961,6 +13783,7 @@ func main() { testGetObjectReadAtFunctional() testGetObjectReadAtWhenEOFWasReached() testPresignedPostPolicy() + testPresignedPostPolicyWrongFile() testCopyObject() testComposeObjectErrorCases() testCompose10KSources() diff --git a/vendor/github.com/minio/minio-go/v7/pkg/credentials/sts_web_identity.go b/vendor/github.com/minio/minio-go/v7/pkg/credentials/sts_web_identity.go index f1c76c78e..787f0a38d 100644 --- a/vendor/github.com/minio/minio-go/v7/pkg/credentials/sts_web_identity.go +++ b/vendor/github.com/minio/minio-go/v7/pkg/credentials/sts_web_identity.go @@ -58,9 +58,10 @@ type WebIdentityResult struct { // WebIdentityToken - web identity token with expiry. type WebIdentityToken struct { - Token string - AccessToken string - Expiry int + Token string + AccessToken string + RefreshToken string + Expiry int } // A STSWebIdentity retrieves credentials from MinIO service, and keeps track if diff --git a/vendor/github.com/minio/minio-go/v7/post-policy.go b/vendor/github.com/minio/minio-go/v7/post-policy.go index 19687e027..26bf441b5 100644 --- a/vendor/github.com/minio/minio-go/v7/post-policy.go +++ b/vendor/github.com/minio/minio-go/v7/post-policy.go @@ -85,7 +85,7 @@ func (p *PostPolicy) SetExpires(t time.Time) error { // SetKey - Sets an object name for the policy based upload. func (p *PostPolicy) SetKey(key string) error { - if strings.TrimSpace(key) == "" || key == "" { + if strings.TrimSpace(key) == "" { return errInvalidArgument("Object name is empty.") } policyCond := policyCondition{ @@ -118,7 +118,7 @@ func (p *PostPolicy) SetKeyStartsWith(keyStartsWith string) error { // SetBucket - Sets bucket at which objects will be uploaded to. func (p *PostPolicy) SetBucket(bucketName string) error { - if strings.TrimSpace(bucketName) == "" || bucketName == "" { + if strings.TrimSpace(bucketName) == "" { return errInvalidArgument("Bucket name is empty.") } policyCond := policyCondition{ @@ -135,7 +135,7 @@ func (p *PostPolicy) SetBucket(bucketName string) error { // SetCondition - Sets condition for credentials, date and algorithm func (p *PostPolicy) SetCondition(matchType, condition, value string) error { - if strings.TrimSpace(value) == "" || value == "" { + if strings.TrimSpace(value) == "" { return errInvalidArgument("No value specified for condition") } @@ -156,7 +156,7 @@ func (p *PostPolicy) SetCondition(matchType, condition, value string) error { // SetTagging - Sets tagging for the object for this policy based upload. func (p *PostPolicy) SetTagging(tagging string) error { - if strings.TrimSpace(tagging) == "" || tagging == "" { + if strings.TrimSpace(tagging) == "" { return errInvalidArgument("No tagging specified.") } _, err := tags.ParseObjectXML(strings.NewReader(tagging)) @@ -178,7 +178,7 @@ func (p *PostPolicy) SetTagging(tagging string) error { // SetContentType - Sets content-type of the object for this policy // based upload. func (p *PostPolicy) SetContentType(contentType string) error { - if strings.TrimSpace(contentType) == "" || contentType == "" { + if strings.TrimSpace(contentType) == "" { return errInvalidArgument("No content type specified.") } policyCond := policyCondition{ @@ -211,7 +211,7 @@ func (p *PostPolicy) SetContentTypeStartsWith(contentTypeStartsWith string) erro // SetContentDisposition - Sets content-disposition of the object for this policy func (p *PostPolicy) SetContentDisposition(contentDisposition string) error { - if strings.TrimSpace(contentDisposition) == "" || contentDisposition == "" { + if strings.TrimSpace(contentDisposition) == "" { return errInvalidArgument("No content disposition specified.") } policyCond := policyCondition{ @@ -226,27 +226,44 @@ func (p *PostPolicy) SetContentDisposition(contentDisposition string) error { return nil } +// SetContentEncoding - Sets content-encoding of the object for this policy +func (p *PostPolicy) SetContentEncoding(contentEncoding string) error { + if strings.TrimSpace(contentEncoding) == "" { + return errInvalidArgument("No content encoding specified.") + } + policyCond := policyCondition{ + matchType: "eq", + condition: "$Content-Encoding", + value: contentEncoding, + } + if err := p.addNewPolicy(policyCond); err != nil { + return err + } + p.formData["Content-Encoding"] = contentEncoding + return nil +} + // SetContentLengthRange - Set new min and max content length // condition for all incoming uploads. -func (p *PostPolicy) SetContentLengthRange(min, max int64) error { - if min > max { +func (p *PostPolicy) SetContentLengthRange(minLen, maxLen int64) error { + if minLen > maxLen { return errInvalidArgument("Minimum limit is larger than maximum limit.") } - if min < 0 { + if minLen < 0 { return errInvalidArgument("Minimum limit cannot be negative.") } - if max <= 0 { + if maxLen <= 0 { return errInvalidArgument("Maximum limit cannot be non-positive.") } - p.contentLengthRange.min = min - p.contentLengthRange.max = max + p.contentLengthRange.min = minLen + p.contentLengthRange.max = maxLen return nil } // SetSuccessActionRedirect - Sets the redirect success url of the object for this policy // based upload. func (p *PostPolicy) SetSuccessActionRedirect(redirect string) error { - if strings.TrimSpace(redirect) == "" || redirect == "" { + if strings.TrimSpace(redirect) == "" { return errInvalidArgument("Redirect is empty") } policyCond := policyCondition{ @@ -264,7 +281,7 @@ func (p *PostPolicy) SetSuccessActionRedirect(redirect string) error { // SetSuccessStatusAction - Sets the status success code of the object for this policy // based upload. func (p *PostPolicy) SetSuccessStatusAction(status string) error { - if strings.TrimSpace(status) == "" || status == "" { + if strings.TrimSpace(status) == "" { return errInvalidArgument("Status is empty") } policyCond := policyCondition{ @@ -282,10 +299,10 @@ func (p *PostPolicy) SetSuccessStatusAction(status string) error { // SetUserMetadata - Set user metadata as a key/value couple. // Can be retrieved through a HEAD request or an event. func (p *PostPolicy) SetUserMetadata(key, value string) error { - if strings.TrimSpace(key) == "" || key == "" { + if strings.TrimSpace(key) == "" { return errInvalidArgument("Key is empty") } - if strings.TrimSpace(value) == "" || value == "" { + if strings.TrimSpace(value) == "" { return errInvalidArgument("Value is empty") } headerName := fmt.Sprintf("x-amz-meta-%s", key) @@ -304,7 +321,7 @@ func (p *PostPolicy) SetUserMetadata(key, value string) error { // SetUserMetadataStartsWith - Set how an user metadata should starts with. // Can be retrieved through a HEAD request or an event. func (p *PostPolicy) SetUserMetadataStartsWith(key, value string) error { - if strings.TrimSpace(key) == "" || key == "" { + if strings.TrimSpace(key) == "" { return errInvalidArgument("Key is empty") } headerName := fmt.Sprintf("x-amz-meta-%s", key) @@ -321,11 +338,29 @@ func (p *PostPolicy) SetUserMetadataStartsWith(key, value string) error { } // SetChecksum sets the checksum of the request. -func (p *PostPolicy) SetChecksum(c Checksum) { +func (p *PostPolicy) SetChecksum(c Checksum) error { if c.IsSet() { p.formData[amzChecksumAlgo] = c.Type.String() p.formData[c.Type.Key()] = c.Encoded() + + policyCond := policyCondition{ + matchType: "eq", + condition: fmt.Sprintf("$%s", amzChecksumAlgo), + value: c.Type.String(), + } + if err := p.addNewPolicy(policyCond); err != nil { + return err + } + policyCond = policyCondition{ + matchType: "eq", + condition: fmt.Sprintf("$%s", c.Type.Key()), + value: c.Encoded(), + } + if err := p.addNewPolicy(policyCond); err != nil { + return err + } } + return nil } // SetEncryption - sets encryption headers for POST API diff --git a/vendor/github.com/minio/minio-go/v7/retry-continous.go b/vendor/github.com/minio/minio-go/v7/retry-continous.go index bfeea95f3..81fcf16f1 100644 --- a/vendor/github.com/minio/minio-go/v7/retry-continous.go +++ b/vendor/github.com/minio/minio-go/v7/retry-continous.go @@ -20,7 +20,7 @@ import "time" // newRetryTimerContinous creates a timer with exponentially increasing delays forever. -func (c *Client) newRetryTimerContinous(unit, cap time.Duration, jitter float64, doneCh chan struct{}) <-chan int { +func (c *Client) newRetryTimerContinous(baseSleep, maxSleep time.Duration, jitter float64, doneCh chan struct{}) <-chan int { attemptCh := make(chan int) // normalize jitter to the range [0, 1.0] @@ -39,10 +39,10 @@ func (c *Client) newRetryTimerContinous(unit, cap time.Duration, jitter float64, if attempt > maxAttempt { attempt = maxAttempt } - // sleep = random_between(0, min(cap, base * 2 ** attempt)) - sleep := unit * time.Duration(1< cap { - sleep = cap + // sleep = random_between(0, min(maxSleep, base * 2 ** attempt)) + sleep := baseSleep * time.Duration(1< maxSleep { + sleep = maxSleep } if jitter != NoJitter { sleep -= time.Duration(c.random.Float64() * float64(sleep) * jitter) diff --git a/vendor/github.com/minio/minio-go/v7/retry.go b/vendor/github.com/minio/minio-go/v7/retry.go index d15eb5901..4cc45920c 100644 --- a/vendor/github.com/minio/minio-go/v7/retry.go +++ b/vendor/github.com/minio/minio-go/v7/retry.go @@ -45,7 +45,7 @@ // newRetryTimer creates a timer with exponentially increasing // delays until the maximum retry attempts are reached. -func (c *Client) newRetryTimer(ctx context.Context, maxRetry int, unit, cap time.Duration, jitter float64) <-chan int { +func (c *Client) newRetryTimer(ctx context.Context, maxRetry int, baseSleep, maxSleep time.Duration, jitter float64) <-chan int { attemptCh := make(chan int) // computes the exponential backoff duration according to @@ -59,10 +59,10 @@ func (c *Client) newRetryTimer(ctx context.Context, maxRetry int, unit, cap time jitter = MaxJitter } - // sleep = random_between(0, min(cap, base * 2 ** attempt)) - sleep := unit * time.Duration(1< cap { - sleep = cap + // sleep = random_between(0, min(maxSleep, base * 2 ** attempt)) + sleep := baseSleep * time.Duration(1< maxSleep { + sleep = maxSleep } if jitter != NoJitter { sleep -= time.Duration(c.random.Float64() * float64(sleep) * jitter) diff --git a/vendor/github.com/ncruces/go-sqlite3/README.md b/vendor/github.com/ncruces/go-sqlite3/README.md index f5394ab22..b370e9638 100644 --- a/vendor/github.com/ncruces/go-sqlite3/README.md +++ b/vendor/github.com/ncruces/go-sqlite3/README.md @@ -10,7 +10,7 @@ as well as direct access to most of the [C SQLite API](https://sqlite.org/cintro It wraps a [Wasm](https://webassembly.org/) [build](embed/) of SQLite, and uses [wazero](https://wazero.io/) as the runtime.\ -Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ runtime dependencies. +Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ direct dependencies. ### Getting started @@ -74,7 +74,7 @@ This project aims for [high test coverage](https://github.com/ncruces/go-sqlite3 It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and [wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach) thorough testing. -Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Test-matrix) on +Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Support-matrix) on Linux (amd64/arm64/386/riscv64/ppc64le/s390x), macOS (amd64/arm64), Windows (amd64), FreeBSD (amd64), OpenBSD (amd64), NetBSD (amd64), DragonFly BSD (amd64), illumos (amd64), and Solaris (amd64). diff --git a/vendor/github.com/ncruces/go-sqlite3/conn.go b/vendor/github.com/ncruces/go-sqlite3/conn.go index 3ba4375b4..d1ce30556 100644 --- a/vendor/github.com/ncruces/go-sqlite3/conn.go +++ b/vendor/github.com/ncruces/go-sqlite3/conn.go @@ -4,6 +4,7 @@ "context" "fmt" "math" + "math/rand" "net/url" "strings" "time" @@ -24,7 +25,6 @@ type Conn struct { interrupt context.Context pending *Stmt stmts []*Stmt - timer *time.Timer busy func(context.Context, int) bool log func(xErrorCode, string) collation func(*Conn, string) @@ -36,7 +36,9 @@ type Conn struct { rollback func() arena arena - handle uint32 + busy1st time.Time + busylst time.Time + handle uint32 } // Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE] and [OPEN_URI]. @@ -389,38 +391,20 @@ func (c *Conn) BusyTimeout(timeout time.Duration) error { } func timeoutCallback(ctx context.Context, mod api.Module, count, tmout int32) (retry uint32) { + // https://fractaledmind.github.io/2024/04/15/sqlite-on-rails-the-how-and-why-of-optimal-performance/ if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.interrupt.Err() == nil { - const delays = "\x01\x02\x05\x0a\x0f\x14\x19\x19\x19\x32\x32\x64" - const totals = "\x00\x01\x03\x08\x12\x21\x35\x4e\x67\x80\xb2\xe4" - const ndelay = int32(len(delays) - 1) - - var delay, prior int32 - if count <= ndelay { - delay = int32(delays[count]) - prior = int32(totals[count]) - } else { - delay = int32(delays[ndelay]) - prior = int32(totals[ndelay]) + delay*(count-ndelay) + switch { + case count == 0: + c.busy1st = time.Now() + case time.Since(c.busy1st) >= time.Duration(tmout)*time.Millisecond: + return 0 } - - if delay = min(delay, tmout-prior); delay > 0 { - delay := time.Duration(delay) * time.Millisecond - if c.interrupt.Done() == nil { - time.Sleep(delay) - return 1 - } - if c.timer == nil { - c.timer = time.NewTimer(delay) - } else { - c.timer.Reset(delay) - } - select { - case <-c.interrupt.Done(): - c.timer.Stop() - case <-c.timer.C: - return 1 - } + if time.Since(c.busylst) < time.Millisecond { + const sleepIncrement = 2*1024*1024 - 1 // power of two, ~2ms + time.Sleep(time.Duration(rand.Int63() & sleepIncrement)) } + c.busylst = time.Now() + return 1 } return 0 } @@ -501,8 +485,12 @@ func (c *Conn) TableColumnMetadata(schema, table, column string) (declType, coll uint64(declTypePtr), uint64(collSeqPtr), uint64(notNullPtr), uint64(primaryKeyPtr), uint64(autoIncPtr)) if err = c.error(r); err == nil && column != "" { - declType = util.ReadString(c.mod, util.ReadUint32(c.mod, declTypePtr), _MAX_NAME) - collSeq = util.ReadString(c.mod, util.ReadUint32(c.mod, collSeqPtr), _MAX_NAME) + if ptr := util.ReadUint32(c.mod, declTypePtr); ptr != 0 { + declType = util.ReadString(c.mod, ptr, _MAX_NAME) + } + if ptr := util.ReadUint32(c.mod, collSeqPtr); ptr != 0 { + collSeq = util.ReadString(c.mod, ptr, _MAX_NAME) + } notNull = util.ReadUint32(c.mod, notNullPtr) != 0 autoInc = util.ReadUint32(c.mod, autoIncPtr) != 0 primaryKey = util.ReadUint32(c.mod, primaryKeyPtr) != 0 diff --git a/vendor/github.com/ncruces/go-sqlite3/driver/driver.go b/vendor/github.com/ncruces/go-sqlite3/driver/driver.go index 88c4c50db..742f308af 100644 --- a/vendor/github.com/ncruces/go-sqlite3/driver/driver.go +++ b/vendor/github.com/ncruces/go-sqlite3/driver/driver.go @@ -81,6 +81,7 @@ "fmt" "io" "net/url" + "reflect" "strings" "time" "unsafe" @@ -107,17 +108,17 @@ func init() { // The second callback is called before the driver closes a connection. // The [sqlite3.Conn] can be used to execute queries, register functions, etc. func Open(dataSourceName string, fn ...func(*sqlite3.Conn) error) (*sql.DB, error) { - var drv SQLite if len(fn) > 2 { return nil, sqlite3.MISUSE } + var init, term func(*sqlite3.Conn) error if len(fn) > 1 { - drv.term = fn[1] + term = fn[1] } if len(fn) > 0 { - drv.init = fn[0] + init = fn[0] } - c, err := drv.OpenConnector(dataSourceName) + c, err := newConnector(dataSourceName, init, term) if err != nil { return nil, err } @@ -125,10 +126,7 @@ func Open(dataSourceName string, fn ...func(*sqlite3.Conn) error) (*sql.DB, erro } // SQLite implements [database/sql/driver.Driver]. -type SQLite struct { - init func(*sqlite3.Conn) error - term func(*sqlite3.Conn) error -} +type SQLite struct{} var ( // Ensure these interfaces are implemented: @@ -137,7 +135,7 @@ type SQLite struct { // Open implements [database/sql/driver.Driver]. func (d *SQLite) Open(name string) (driver.Conn, error) { - c, err := d.newConnector(name) + c, err := newConnector(name, nil, nil) if err != nil { return nil, err } @@ -146,11 +144,11 @@ func (d *SQLite) Open(name string) (driver.Conn, error) { // OpenConnector implements [database/sql/driver.DriverContext]. func (d *SQLite) OpenConnector(name string) (driver.Connector, error) { - return d.newConnector(name) + return newConnector(name, nil, nil) } -func (d *SQLite) newConnector(name string) (*connector, error) { - c := connector{driver: d, name: name} +func newConnector(name string, init, term func(*sqlite3.Conn) error) (*connector, error) { + c := connector{name: name, init: init, term: term} var txlock, timefmt string if strings.HasPrefix(name, "file:") { @@ -190,7 +188,8 @@ func (d *SQLite) newConnector(name string) (*connector, error) { } type connector struct { - driver *SQLite + init func(*sqlite3.Conn) error + term func(*sqlite3.Conn) error name string txLock string tmRead sqlite3.TimeFormat @@ -199,7 +198,7 @@ type connector struct { } func (n *connector) Driver() driver.Driver { - return n.driver + return &SQLite{} } func (n *connector) Connect(ctx context.Context) (res driver.Conn, err error) { @@ -228,13 +227,13 @@ func (n *connector) Connect(ctx context.Context) (res driver.Conn, err error) { return nil, err } } - if n.driver.init != nil { - err = n.driver.init(c.Conn) + if n.init != nil { + err = n.init(c.Conn) if err != nil { return nil, err } } - if n.pragmas || n.driver.init != nil { + if n.pragmas || n.init != nil { s, _, err := c.Conn.Prepare(`PRAGMA query_only`) if err != nil { return nil, err @@ -250,9 +249,9 @@ func (n *connector) Connect(ctx context.Context) (res driver.Conn, err error) { return nil, err } } - if n.driver.term != nil { + if n.term != nil { err = c.Conn.Trace(sqlite3.TRACE_CLOSE, func(sqlite3.TraceEvent, any, any) error { - return n.driver.term(c.Conn) + return n.term(c.Conn) }) if err != nil { return nil, err @@ -275,6 +274,7 @@ func (n *connector) Connect(ctx context.Context) (res driver.Conn, err error) { // if err != nil { // log.Fatal(err) // } +// defer conn.Close() // // err = conn.Raw(func(driverConn any) error { // conn := driverConn.(driver.Conn) @@ -288,6 +288,8 @@ func (n *connector) Connect(ctx context.Context) (res driver.Conn, err error) { type Conn interface { Raw() *sqlite3.Conn driver.Conn + driver.ConnBeginTx + driver.ConnPrepareContext } type conn struct { @@ -301,10 +303,8 @@ type conn struct { var ( // Ensure these interfaces are implemented: - _ Conn = &conn{} - _ driver.ConnBeginTx = &conn{} - _ driver.ConnPrepareContext = &conn{} - _ driver.ExecerContext = &conn{} + _ Conn = &conn{} + _ driver.ExecerContext = &conn{} ) func (c *conn) Raw() *sqlite3.Conn { @@ -380,7 +380,7 @@ func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, e if err != nil { return nil, err } - if tail != "" { + if notWhitespace(tail) { s.Close() return nil, util.TailErr } @@ -581,8 +581,22 @@ type rows struct { names []string types []string nulls []bool + scans []scantype } +type scantype byte + +const ( + _ANY scantype = iota + _INT scantype = scantype(sqlite3.INTEGER) + _REAL scantype = scantype(sqlite3.FLOAT) + _TEXT scantype = scantype(sqlite3.TEXT) + _BLOB scantype = scantype(sqlite3.BLOB) + _NULL scantype = scantype(sqlite3.NULL) + _BOOL scantype = iota + _TIME +) + var ( // Ensure these interfaces are implemented: _ driver.RowsColumnTypeDatabaseTypeName = &rows{} @@ -606,21 +620,42 @@ func (r *rows) Columns() []string { return r.names } -func (r *rows) loadTypes() { +func (r *rows) loadColumnMetadata() { if r.nulls == nil { count := r.Stmt.ColumnCount() nulls := make([]bool, count) types := make([]string, count) + scans := make([]scantype, count) for i := range nulls { if col := r.Stmt.ColumnOriginName(i); col != "" { types[i], _, nulls[i], _, _, _ = r.Stmt.Conn().TableColumnMetadata( r.Stmt.ColumnDatabaseName(i), r.Stmt.ColumnTableName(i), col) + types[i] = strings.ToUpper(types[i]) + // These types are only used before we have rows, + // and otherwise as type hints. + // The first few ensure STRICT tables are strictly typed. + // The other two are type hints for booleans and time. + switch types[i] { + case "INT", "INTEGER": + scans[i] = _INT + case "REAL": + scans[i] = _REAL + case "TEXT": + scans[i] = _TEXT + case "BLOB": + scans[i] = _BLOB + case "BOOLEAN": + scans[i] = _BOOL + case "DATE", "TIME", "DATETIME", "TIMESTAMP": + scans[i] = _TIME + } } } r.nulls = nulls r.types = types + r.scans = scans } } @@ -637,7 +672,7 @@ func (r *rows) declType(index int) string { } func (r *rows) ColumnTypeDatabaseTypeName(index int) string { - r.loadTypes() + r.loadColumnMetadata() decltype := r.types[index] if len := len(decltype); len > 0 && decltype[len-1] == ')' { if i := strings.LastIndexByte(decltype, '('); i >= 0 { @@ -648,13 +683,57 @@ func (r *rows) ColumnTypeDatabaseTypeName(index int) string { } func (r *rows) ColumnTypeNullable(index int) (nullable, ok bool) { - r.loadTypes() + r.loadColumnMetadata() if r.nulls[index] { return false, true } return true, false } +func (r *rows) ColumnTypeScanType(index int) (typ reflect.Type) { + r.loadColumnMetadata() + scan := r.scans[index] + + if r.Stmt.Busy() { + // SQLite is dynamically typed and we now have a row. + // Always use the type of the value itself, + // unless the scan type is more specific + // and can scan the actual value. + val := scantype(r.Stmt.ColumnType(index)) + useValType := true + switch { + case scan == _TIME && val != _BLOB && val != _NULL: + t := r.Stmt.ColumnTime(index, r.tmRead) + useValType = t == time.Time{} + case scan == _BOOL && val == _INT: + i := r.Stmt.ColumnInt64(index) + useValType = i != 0 && i != 1 + case scan == _BLOB && val == _NULL: + useValType = false + } + if useValType { + scan = val + } + } + + switch scan { + case _INT: + return reflect.TypeOf(int64(0)) + case _REAL: + return reflect.TypeOf(float64(0)) + case _TEXT: + return reflect.TypeOf("") + case _BLOB: + return reflect.TypeOf([]byte{}) + case _BOOL: + return reflect.TypeOf(false) + case _TIME: + return reflect.TypeOf(time.Time{}) + default: + return reflect.TypeOf((*any)(nil)).Elem() + } +} + func (r *rows) Next(dest []driver.Value) error { old := r.Stmt.Conn().SetInterrupt(r.ctx) defer r.Stmt.Conn().SetInterrupt(old) @@ -667,7 +746,7 @@ func (r *rows) Next(dest []driver.Value) error { } data := unsafe.Slice((*any)(unsafe.SliceData(dest)), len(dest)) - err := r.Stmt.Columns(data) + err := r.Stmt.Columns(data...) for i := range dest { if t, ok := r.decodeTime(i, dest[i]); ok { dest[i] = t diff --git a/vendor/github.com/ncruces/go-sqlite3/driver/time.go b/vendor/github.com/ncruces/go-sqlite3/driver/time.go index 630a5b10b..b3ebdd263 100644 --- a/vendor/github.com/ncruces/go-sqlite3/driver/time.go +++ b/vendor/github.com/ncruces/go-sqlite3/driver/time.go @@ -1,8 +1,6 @@ package driver -import ( - "time" -) +import "time" // Convert a string in [time.RFC3339Nano] format into a [time.Time] // if it roundtrips back to the same string. diff --git a/vendor/github.com/ncruces/go-sqlite3/driver/util.go b/vendor/github.com/ncruces/go-sqlite3/driver/util.go index 033841157..987585576 100644 --- a/vendor/github.com/ncruces/go-sqlite3/driver/util.go +++ b/vendor/github.com/ncruces/go-sqlite3/driver/util.go @@ -12,3 +12,63 @@ func namedValues(args []driver.Value) []driver.NamedValue { } return named } + +func notWhitespace(sql string) bool { + const ( + code = iota + slash + minus + ccomment + sqlcomment + endcomment + ) + + state := code + for _, b := range ([]byte)(sql) { + if b == 0 { + break + } + + switch state { + case code: + switch b { + case '/': + state = slash + case '-': + state = minus + case ' ', ';', '\t', '\n', '\v', '\f', '\r': + continue + default: + return true + } + case slash: + if b != '*' { + return true + } + state = ccomment + case minus: + if b != '-' { + return true + } + state = sqlcomment + case ccomment: + if b == '*' { + state = endcomment + } + case sqlcomment: + if b == '\n' { + state = code + } + case endcomment: + switch b { + case '/': + state = code + case '*': + state = endcomment + default: + state = ccomment + } + } + } + return state == slash || state == minus +} diff --git a/vendor/github.com/ncruces/go-sqlite3/embed/README.md b/vendor/github.com/ncruces/go-sqlite3/embed/README.md index 7a7a52a49..edec63320 100644 --- a/vendor/github.com/ncruces/go-sqlite3/embed/README.md +++ b/vendor/github.com/ncruces/go-sqlite3/embed/README.md @@ -1,6 +1,6 @@ # Embeddable Wasm build of SQLite -This folder includes an embeddable Wasm build of SQLite 3.47.1 for use with +This folder includes an embeddable Wasm build of SQLite 3.47.2 for use with [`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3). The following optional features are compiled in: diff --git a/vendor/github.com/ncruces/go-sqlite3/embed/sqlite3.wasm b/vendor/github.com/ncruces/go-sqlite3/embed/sqlite3.wasm index 2e86b6f5d..c312aa62a 100644 Binary files a/vendor/github.com/ncruces/go-sqlite3/embed/sqlite3.wasm and b/vendor/github.com/ncruces/go-sqlite3/embed/sqlite3.wasm differ diff --git a/vendor/github.com/ncruces/go-sqlite3/go.work b/vendor/github.com/ncruces/go-sqlite3/go.work index 18e378592..ff5d6b514 100644 --- a/vendor/github.com/ncruces/go-sqlite3/go.work +++ b/vendor/github.com/ncruces/go-sqlite3/go.work @@ -3,4 +3,5 @@ go 1.21 use ( . ./gormlite + ./embed/bcw2 ) diff --git a/vendor/github.com/ncruces/go-sqlite3/go.work.sum b/vendor/github.com/ncruces/go-sqlite3/go.work.sum index c3936965c..ded9bda72 100644 --- a/vendor/github.com/ncruces/go-sqlite3/go.work.sum +++ b/vendor/github.com/ncruces/go-sqlite3/go.work.sum @@ -1,3 +1,4 @@ +github.com/ncruces/go-sqlite3 v0.21.0/go.mod h1:zxMOaSG5kFYVFK4xQa0pdwIszqxqJ0W0BxBgwdrNjuA= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= @@ -10,5 +11,6 @@ golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= diff --git a/vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_other.go b/vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_other.go index d9a3de224..b420acc45 100644 --- a/vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_other.go +++ b/vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_other.go @@ -1,4 +1,4 @@ -//go:build !(unix || windows) || sqlite3_nosys +//go:build !unix && !windows package alloc diff --git a/vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_unix.go b/vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_unix.go index 2948487f6..a00dbbf24 100644 --- a/vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_unix.go +++ b/vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_unix.go @@ -1,4 +1,4 @@ -//go:build unix && !sqlite3_nosys +//go:build unix package alloc diff --git a/vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_windows.go b/vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_windows.go index 8e67e0319..6bfc73a0c 100644 --- a/vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_windows.go +++ b/vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_windows.go @@ -1,5 +1,3 @@ -//go:build !sqlite3_nosys - package alloc import ( diff --git a/vendor/github.com/ncruces/go-sqlite3/internal/dotlk/dotlk.go b/vendor/github.com/ncruces/go-sqlite3/internal/dotlk/dotlk.go new file mode 100644 index 000000000..3c8d782d7 --- /dev/null +++ b/vendor/github.com/ncruces/go-sqlite3/internal/dotlk/dotlk.go @@ -0,0 +1,29 @@ +package dotlk + +import ( + "errors" + "io/fs" + "os" +) + +// LockShm creates a directory on disk to prevent SQLite +// from using this path for a shared memory file. +func LockShm(name string) error { + err := os.Mkdir(name, 0777) + if errors.Is(err, fs.ErrExist) { + s, err := os.Lstat(name) + if err == nil && s.IsDir() { + return nil + } + } + return err +} + +// Unlock removes the lock or shared memory file. +func Unlock(name string) error { + err := os.Remove(name) + if errors.Is(err, fs.ErrNotExist) { + return nil + } + return err +} diff --git a/vendor/github.com/ncruces/go-sqlite3/internal/dotlk/dotlk_other.go b/vendor/github.com/ncruces/go-sqlite3/internal/dotlk/dotlk_other.go new file mode 100644 index 000000000..5399a5f8a --- /dev/null +++ b/vendor/github.com/ncruces/go-sqlite3/internal/dotlk/dotlk_other.go @@ -0,0 +1,13 @@ +//go:build !unix + +package dotlk + +import "os" + +// TryLock returns nil if it acquired the lock, +// fs.ErrExist if another process has the lock. +func TryLock(name string) error { + f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) + f.Close() + return err +} diff --git a/vendor/github.com/ncruces/go-sqlite3/internal/dotlk/dotlk_unix.go b/vendor/github.com/ncruces/go-sqlite3/internal/dotlk/dotlk_unix.go new file mode 100644 index 000000000..177ab30bb --- /dev/null +++ b/vendor/github.com/ncruces/go-sqlite3/internal/dotlk/dotlk_unix.go @@ -0,0 +1,50 @@ +//go:build unix + +package dotlk + +import ( + "errors" + "io/fs" + "os" + "strconv" + + "golang.org/x/sys/unix" +) + +// TryLock returns nil if it acquired the lock, +// fs.ErrExist if another process has the lock. +func TryLock(name string) error { + for retry := true; retry; retry = false { + f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) + if err == nil { + f.WriteString(strconv.Itoa(os.Getpid())) + f.Close() + return nil + } + if !errors.Is(err, fs.ErrExist) { + return err + } + if !removeStale(name) { + break + } + } + return fs.ErrExist +} + +func removeStale(name string) bool { + buf, err := os.ReadFile(name) + if err != nil { + return errors.Is(err, fs.ErrNotExist) + } + + pid, err := strconv.Atoi(string(buf)) + if err != nil { + return false + } + if unix.Kill(pid, 0) == nil { + return false + } + + err = os.Remove(name) + return err == nil || errors.Is(err, fs.ErrNotExist) +} 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 e11f953a7..720977b8d 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 || sqlite3_nosys +//go:build !unix package util diff --git a/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap.go b/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_unix.go similarity index 91% rename from vendor/github.com/ncruces/go-sqlite3/internal/util/mmap.go rename to vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_unix.go index 613bb90b1..4ff056666 100644 --- a/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap.go +++ b/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_unix.go @@ -1,4 +1,4 @@ -//go:build unix && !sqlite3_nosys +//go:build unix package util @@ -39,13 +39,13 @@ func (s *mmapState) new(ctx context.Context, mod api.Module, size int32) *Mapped // Save the newly allocated region. ptr := uint32(stack[0]) buf := View(mod, ptr, uint64(size)) - addr := unsafe.Pointer(&buf[0]) - s.regions = append(s.regions, &MappedRegion{ + res := &MappedRegion{ Ptr: ptr, - addr: addr, size: size, - }) - return s.regions[len(s.regions)-1] + addr: unsafe.Pointer(&buf[0]), + } + s.regions = append(s.regions, res) + return res } type MappedRegion struct { 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 index fdf6f439a..efff1e733 100644 --- a/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_windows.go +++ b/vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_windows.go @@ -1,5 +1,3 @@ -//go:build !sqlite3_nosys - package util import ( diff --git a/vendor/github.com/ncruces/go-sqlite3/sqlite.go b/vendor/github.com/ncruces/go-sqlite3/sqlite.go index 2afe9971c..18a2c2a73 100644 --- a/vendor/github.com/ncruces/go-sqlite3/sqlite.go +++ b/vendor/github.com/ncruces/go-sqlite3/sqlite.go @@ -265,10 +265,11 @@ func (a *arena) mark() (reset func()) { ptrs := len(a.ptrs) next := a.next return func() { - for _, ptr := range a.ptrs[ptrs:] { + rest := a.ptrs[ptrs:] + for _, ptr := range a.ptrs[:ptrs] { a.sqlt.free(ptr) } - a.ptrs = a.ptrs[:ptrs] + a.ptrs = rest a.next = next } } diff --git a/vendor/github.com/ncruces/go-sqlite3/stmt.go b/vendor/github.com/ncruces/go-sqlite3/stmt.go index 139dd3525..f1648f076 100644 --- a/vendor/github.com/ncruces/go-sqlite3/stmt.go +++ b/vendor/github.com/ncruces/go-sqlite3/stmt.go @@ -582,7 +582,9 @@ func (s *Stmt) ColumnRawBlob(col int) []byte { func (s *Stmt) columnRawBytes(col int, ptr uint32) []byte { if ptr == 0 { r := s.c.call("sqlite3_errcode", uint64(s.c.handle)) - s.err = s.c.error(r) + if r != _ROW && r != _DONE { + s.err = s.c.error(r) + } return nil } @@ -637,7 +639,7 @@ func (s *Stmt) ColumnValue(col int) Value { // [TEXT] as string, and [BLOB] as []byte. // Any []byte are owned by SQLite and may be invalidated by // subsequent calls to [Stmt] methods. -func (s *Stmt) Columns(dest []any) error { +func (s *Stmt) Columns(dest ...any) error { defer s.c.arena.mark()() count := uint64(len(dest)) typePtr := s.c.arena.new(count) @@ -666,6 +668,10 @@ func (s *Stmt) Columns(dest []any) error { dest[i] = nil default: ptr := util.ReadUint32(s.c.mod, dataPtr+0) + if ptr == 0 { + dest[i] = []byte{} + continue + } len := util.ReadUint32(s.c.mod, dataPtr+4) buf := util.View(s.c.mod, ptr, uint64(len)) if types[i] == byte(TEXT) { diff --git a/vendor/github.com/ncruces/go-sqlite3/util/osutil/open_windows.go b/vendor/github.com/ncruces/go-sqlite3/util/osutil/open_windows.go index 417faa562..febaf846e 100644 --- a/vendor/github.com/ncruces/go-sqlite3/util/osutil/open_windows.go +++ b/vendor/github.com/ncruces/go-sqlite3/util/osutil/open_windows.go @@ -101,6 +101,9 @@ func syscallOpen(path string, mode int, perm uint32) (fd Handle, err error) { const _FILE_FLAG_WRITE_THROUGH = 0x80000000 attrs |= _FILE_FLAG_WRITE_THROUGH } + if mode&O_NONBLOCK != 0 { + attrs |= FILE_FLAG_OVERLAPPED + } return CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0) } diff --git a/vendor/github.com/ncruces/go-sqlite3/util/sql3util/parse/sql3parse_table.wasm b/vendor/github.com/ncruces/go-sqlite3/util/sql3util/parse/sql3parse_table.wasm index f0b3819c8..4d3357ea1 100644 Binary files a/vendor/github.com/ncruces/go-sqlite3/util/sql3util/parse/sql3parse_table.wasm and b/vendor/github.com/ncruces/go-sqlite3/util/sql3util/parse/sql3parse_table.wasm differ diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/README.md b/vendor/github.com/ncruces/go-sqlite3/vfs/README.md index 08777972e..4e987ce3f 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/README.md +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/README.md @@ -48,11 +48,6 @@ 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. -With [BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2) -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 @@ -91,7 +86,6 @@ The implementation is compatible with SQLite's The VFS can be customized with a few build tags: - `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 diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/lock.go b/vendor/github.com/ncruces/go-sqlite3/vfs/lock.go index 22e320a81..b28d83230 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_nosys) || sqlite3_flock || sqlite3_dotlk +//go:build linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk package vfs @@ -20,12 +20,10 @@ ) func (f *vfsFile) Lock(lock LockLevel) error { - // Argument check. SQLite never explicitly requests a pending lock. - if lock != LOCK_SHARED && lock != LOCK_RESERVED && lock != LOCK_EXCLUSIVE { - panic(util.AssertErr()) - } - switch { + case lock != LOCK_SHARED && lock != LOCK_RESERVED && lock != LOCK_EXCLUSIVE: + // Argument check. SQLite never explicitly requests a pending lock. + panic(util.AssertErr()) case f.lock < LOCK_NONE || f.lock > LOCK_EXCLUSIVE: // Connection state check. panic(util.AssertErr()) @@ -87,13 +85,12 @@ func (f *vfsFile) Lock(lock LockLevel) error { } func (f *vfsFile) Unlock(lock LockLevel) error { - // Argument check. - if lock != LOCK_NONE && lock != LOCK_SHARED { + switch { + case lock != LOCK_NONE && lock != LOCK_SHARED: + // Argument check. panic(util.AssertErr()) - } - - // Connection state check. - if f.lock < LOCK_NONE || f.lock > LOCK_EXCLUSIVE { + case f.lock < LOCK_NONE || f.lock > LOCK_EXCLUSIVE: + // Connection state check. panic(util.AssertErr()) } 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 81aacc622..9bdfa3cf2 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_nosys) || sqlite3_flock || sqlite3_dotlk) +//go:build !(linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk) package vfs 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 cc5da7cab..4f6fadef4 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_dotlk || sqlite3_nosys)) || sqlite3_flock +//go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && !sqlite3_dotlk) || sqlite3_flock package vfs @@ -9,11 +9,11 @@ ) func osGetSharedLock(file *os.File) _ErrorCode { - return osLock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK) + return osFlock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK) } func osGetReservedLock(file *os.File) _ErrorCode { - rc := osLock(file, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK) + rc := osFlock(file, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK) if rc == _BUSY { // The documentation states that a lock is upgraded by // releasing the previous lock, then acquiring the new lock. @@ -37,7 +37,7 @@ func osGetExclusiveLock(file *os.File, state *LockLevel) _ErrorCode { } func osDowngradeLock(file *os.File, _ LockLevel) _ErrorCode { - rc := osLock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK) + rc := osFlock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK) if rc == _BUSY { // The documentation states that a lock is downgraded by // releasing the previous lock then acquiring the new lock. @@ -66,7 +66,36 @@ func osCheckReservedLock(file *os.File) (bool, _ErrorCode) { return lock == unix.F_WRLCK, rc } -func osLock(file *os.File, how int, def _ErrorCode) _ErrorCode { +func osFlock(file *os.File, how int, def _ErrorCode) _ErrorCode { err := unix.Flock(int(file.Fd()), how) return osLockErrorCode(err, def) } + +func osReadLock(file *os.File, start, len int64) _ErrorCode { + return osLock(file, unix.F_RDLCK, start, len, _IOERR_RDLOCK) +} + +func osWriteLock(file *os.File, start, len int64) _ErrorCode { + return osLock(file, unix.F_WRLCK, start, len, _IOERR_LOCK) +} + +func osLock(file *os.File, typ int16, start, len int64, def _ErrorCode) _ErrorCode { + err := unix.FcntlFlock(file.Fd(), unix.F_SETLK, &unix.Flock_t{ + Type: typ, + Start: start, + Len: len, + }) + return osLockErrorCode(err, def) +} + +func osUnlock(file *os.File, start, len int64) _ErrorCode { + err := unix.FcntlFlock(file.Fd(), unix.F_SETLK, &unix.Flock_t{ + Type: unix.F_UNLCK, + Start: start, + Len: len, + }) + if err != nil { + return _IOERR_UNLOCK + } + return _OK +} diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_darwin.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_darwin.go index c8d84dc36..07de7c3d8 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_darwin.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_darwin.go @@ -1,4 +1,4 @@ -//go:build !(sqlite3_flock || sqlite3_nosys) +//go:build !sqlite3_flock package vfs @@ -56,16 +56,12 @@ func osAllocate(file *os.File, size int64) error { return file.Truncate(size) } -func osUnlock(file *os.File, start, len int64) _ErrorCode { - err := unix.FcntlFlock(file.Fd(), _F_OFD_SETLK, &unix.Flock_t{ - Type: unix.F_UNLCK, - Start: start, - Len: len, - }) - if err != nil { - return _IOERR_UNLOCK - } - return _OK +func osReadLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode { + return osLock(file, unix.F_RDLCK, start, len, timeout, _IOERR_RDLOCK) +} + +func osWriteLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode { + return osLock(file, unix.F_WRLCK, start, len, timeout, _IOERR_LOCK) } func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, def _ErrorCode) _ErrorCode { @@ -88,10 +84,14 @@ func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, d return osLockErrorCode(err, def) } -func osReadLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode { - return osLock(file, unix.F_RDLCK, start, len, timeout, _IOERR_RDLOCK) -} - -func osWriteLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode { - return osLock(file, unix.F_WRLCK, start, len, timeout, _IOERR_LOCK) +func osUnlock(file *os.File, start, len int64) _ErrorCode { + err := unix.FcntlFlock(file.Fd(), _F_OFD_SETLK, &unix.Flock_t{ + Type: unix.F_UNLCK, + Start: start, + Len: len, + }) + if err != nil { + return _IOERR_UNLOCK + } + return _OK } diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_dotlk.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_dotlk.go index b00a1865b..7a9c38897 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_dotlk.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_dotlk.go @@ -7,6 +7,8 @@ "io/fs" "os" "sync" + + "github.com/ncruces/go-sqlite3/internal/dotlk" ) var ( @@ -28,12 +30,10 @@ func osGetSharedLock(file *os.File) _ErrorCode { name := file.Name() locker := vfsDotLocks[name] if locker == nil { - f, err := os.OpenFile(name+".lock", os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) - f.Close() - if errors.Is(err, fs.ErrExist) { - return _BUSY // Another process has the lock. - } - if err != nil { + if err := dotlk.TryLock(name + ".lock"); err != nil { + if errors.Is(err, fs.ErrExist) { + return _BUSY // Another process has the lock. + } return _IOERR_LOCK } locker = &vfsDotLocker{} @@ -114,8 +114,7 @@ func osReleaseLock(file *os.File, state LockLevel) _ErrorCode { } if locker.shared == 1 { - err := os.Remove(name + ".lock") - if err != nil && !errors.Is(err, fs.ErrNotExist) { + if err := dotlk.Unlock(name + ".lock"); err != nil { return _IOERR_UNLOCK } delete(vfsDotLocks, name) diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_f2fs_linux.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_f2fs_linux.go index 07bf0a047..36614309d 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_f2fs_linux.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_f2fs_linux.go @@ -1,4 +1,4 @@ -//go:build (amd64 || arm64 || riscv64) && !sqlite3_nosys +//go:build amd64 || arm64 || riscv64 package vfs diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_linux.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_linux.go index e163e804d..6199c7b00 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_linux.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_linux.go @@ -1,9 +1,8 @@ -//go:build !(sqlite3_flock || sqlite3_nosys) +//go:build !sqlite3_flock package vfs import ( - "math/rand" "os" "time" @@ -22,6 +21,30 @@ func osAllocate(file *os.File, size int64) error { return unix.Fallocate(int(file.Fd()), 0, 0, size) } +func osReadLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode { + return osLock(file, unix.F_RDLCK, start, len, timeout, _IOERR_RDLOCK) +} + +func osWriteLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode { + return osLock(file, unix.F_WRLCK, start, len, timeout, _IOERR_LOCK) +} + +func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, def _ErrorCode) _ErrorCode { + lock := unix.Flock_t{ + Type: typ, + Start: start, + Len: len, + } + var err error + switch { + case timeout < 0: + err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLKW, &lock) + default: + err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &lock) + } + return osLockErrorCode(err, def) +} + func osUnlock(file *os.File, start, len int64) _ErrorCode { err := unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &unix.Flock_t{ Type: unix.F_UNLCK, @@ -33,40 +56,3 @@ func osUnlock(file *os.File, start, len int64) _ErrorCode { } return _OK } - -func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, def _ErrorCode) _ErrorCode { - lock := unix.Flock_t{ - Type: typ, - Start: start, - Len: len, - } - var err error - switch { - case timeout == 0: - err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &lock) - case timeout < 0: - err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLKW, &lock) - default: - before := time.Now() - for { - err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &lock) - if errno, _ := err.(unix.Errno); errno != unix.EAGAIN { - break - } - if time.Since(before) > timeout { - break - } - const sleepIncrement = 1024*1024 - 1 // power of two, ~1ms - time.Sleep(time.Duration(rand.Int63() & sleepIncrement)) - } - } - return osLockErrorCode(err, def) -} - -func osReadLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode { - return osLock(file, unix.F_RDLCK, start, len, timeout, _IOERR_RDLOCK) -} - -func osWriteLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode { - return osLock(file, unix.F_WRLCK, start, len, timeout, _IOERR_LOCK) -} 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 b4f570f4d..d93050e8a 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_dotlk || sqlite3_nosys) +//go:build (linux || darwin) && !(sqlite3_flock || sqlite3_dotlk) package vfs diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_std.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_std.go index 87ce58b67..a17893d2e 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_std.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_std.go @@ -1,4 +1,4 @@ -//go:build !unix || sqlite3_nosys +//go:build !unix package vfs diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_std_alloc.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_std_alloc.go index 60c92182c..4dd1bb32c 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_std_alloc.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_std_alloc.go @@ -1,4 +1,4 @@ -//go:build !(linux || darwin) || sqlite3_flock || sqlite3_nosys +//go:build !(linux || darwin) || sqlite3_flock package vfs diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_std_atomic.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_std_atomic.go index ecaff0245..10a0c8470 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_std_atomic.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_std_atomic.go @@ -1,4 +1,4 @@ -//go:build !linux || !(amd64 || arm64 || riscv64) || sqlite3_nosys +//go:build !linux || !(amd64 || arm64 || riscv64) package vfs diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_std_sync.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_std_sync.go index 84dbd23bc..b32e83e08 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_std_sync.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_std_sync.go @@ -1,4 +1,4 @@ -//go:build !(linux || darwin) || sqlite3_flock || sqlite3_nosys +//go:build !(linux || darwin) || sqlite3_flock package vfs diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/os_unix.go b/vendor/github.com/ncruces/go-sqlite3/vfs/os_unix.go index 7a540889b..6637c2922 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_unix.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_unix.go @@ -1,4 +1,4 @@ -//go:build unix && !sqlite3_nosys +//go:build unix 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 0b6e5d342..0398f4760 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/os_windows.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/os_windows.go @@ -1,9 +1,8 @@ -//go:build !(sqlite3_dotlk || sqlite3_nosys) +//go:build !sqlite3_dotlk package vfs import ( - "math/rand" "os" "time" @@ -46,7 +45,8 @@ func osGetExclusiveLock(file *os.File, state *LockLevel) _ErrorCode { osUnlock(file, _SHARED_FIRST, _SHARED_SIZE) // Acquire the EXCLUSIVE lock. - rc := osWriteLock(file, _SHARED_FIRST, _SHARED_SIZE, time.Millisecond) + // Can't wait here, because the file is not OVERLAPPED. + rc := osWriteLock(file, _SHARED_FIRST, _SHARED_SIZE, 0) if rc != _OK { // Reacquire the SHARED lock. @@ -107,6 +107,27 @@ func osCheckReservedLock(file *os.File) (bool, _ErrorCode) { return false, rc } +func osReadLock(file *os.File, start, len uint32, timeout time.Duration) _ErrorCode { + return osLock(file, 0, start, len, timeout, _IOERR_RDLOCK) +} + +func osWriteLock(file *os.File, start, len uint32, timeout time.Duration) _ErrorCode { + return osLock(file, windows.LOCKFILE_EXCLUSIVE_LOCK, start, len, timeout, _IOERR_LOCK) +} + +func osLock(file *os.File, flags, start, len uint32, timeout time.Duration, def _ErrorCode) _ErrorCode { + var err error + switch { + case timeout == 0: + err = osLockEx(file, flags|windows.LOCKFILE_FAIL_IMMEDIATELY, start, len) + case timeout < 0: + err = osLockEx(file, flags, start, len) + default: + err = osLockExTimeout(file, flags, start, len, timeout) + } + return osLockErrorCode(err, def) +} + func osUnlock(file *os.File, start, len uint32) _ErrorCode { err := windows.UnlockFileEx(windows.Handle(file.Fd()), 0, len, 0, &windows.Overlapped{Offset: start}) @@ -119,41 +140,40 @@ func osUnlock(file *os.File, start, len uint32) _ErrorCode { return _OK } -func osLock(file *os.File, flags, start, len uint32, timeout time.Duration, def _ErrorCode) _ErrorCode { - var err error - switch { - case timeout == 0: - err = osLockEx(file, flags|windows.LOCKFILE_FAIL_IMMEDIATELY, start, len) - case timeout < 0: - err = osLockEx(file, flags, start, len) - default: - before := time.Now() - for { - err = osLockEx(file, flags|windows.LOCKFILE_FAIL_IMMEDIATELY, start, len) - if errno, _ := err.(windows.Errno); errno != windows.ERROR_LOCK_VIOLATION { - break - } - if time.Since(before) > timeout { - break - } - const sleepIncrement = 1024*1024 - 1 // power of two, ~1ms - time.Sleep(time.Duration(rand.Int63() & sleepIncrement)) - } - } - return osLockErrorCode(err, def) -} - func osLockEx(file *os.File, flags, start, len uint32) error { return windows.LockFileEx(windows.Handle(file.Fd()), flags, 0, len, 0, &windows.Overlapped{Offset: start}) } -func osReadLock(file *os.File, start, len uint32, timeout time.Duration) _ErrorCode { - return osLock(file, 0, start, len, timeout, _IOERR_RDLOCK) -} +func osLockExTimeout(file *os.File, flags, start, len uint32, timeout time.Duration) error { + event, err := windows.CreateEvent(nil, 1, 0, nil) + if err != nil { + return err + } + defer windows.CloseHandle(event) -func osWriteLock(file *os.File, start, len uint32, timeout time.Duration) _ErrorCode { - return osLock(file, windows.LOCKFILE_EXCLUSIVE_LOCK, start, len, timeout, _IOERR_LOCK) + fd := windows.Handle(file.Fd()) + overlapped := &windows.Overlapped{ + Offset: start, + HEvent: event, + } + + err = windows.LockFileEx(fd, flags, 0, len, 0, overlapped) + if err != windows.ERROR_IO_PENDING { + return err + } + + ms := (timeout + time.Millisecond - 1) / time.Millisecond + rc, err := windows.WaitForSingleObject(event, uint32(ms)) + if rc == windows.WAIT_OBJECT_0 { + return nil + } + defer windows.CancelIoEx(fd, overlapped) + + if err != nil { + return err + } + return windows.Errno(rc) } func osLockErrorCode(err error, def _ErrorCode) _ErrorCode { @@ -165,8 +185,9 @@ func osLockErrorCode(err error, def _ErrorCode) _ErrorCode { switch errno { case windows.ERROR_LOCK_VIOLATION, + windows.ERROR_OPERATION_ABORTED, windows.ERROR_IO_PENDING, - windows.ERROR_OPERATION_ABORTED: + windows.WAIT_TIMEOUT: return _BUSY } } diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm.go index 9d9dff1c4..f28955289 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/shm.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm.go @@ -1,4 +1,4 @@ -//go:build ((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk +//go:build ((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le)) || sqlite3_flock || sqlite3_dotlk package vfs 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 07cabf7b5..5f4f5d108 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_bsd.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_bsd.go @@ -1,10 +1,12 @@ -//go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_dotlk || sqlite3_nosys)) || sqlite3_flock +//go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_dotlk) || sqlite3_flock package vfs import ( "context" + "errors" "io" + "io/fs" "os" "sync" @@ -20,7 +22,7 @@ type vfsShmParent struct { refs int // +checklocks:vfsShmListMtx - lock [_SHM_NLOCK]int16 // +checklocks:Mutex + lock [_SHM_NLOCK]int8 // +checklocks:Mutex sync.Mutex } @@ -71,23 +73,21 @@ func (s *vfsShm) shmOpen() _ErrorCode { return _OK } - // Always open file read-write, as it will be shared. - f, err := os.OpenFile(s.path, - unix.O_RDWR|unix.O_CREAT|unix.O_NOFOLLOW, 0666) - if err != nil { - return _CANTOPEN - } - // Closes file if it's not nil. + var f *os.File + // Close file on error. + // Keep this here to avoid confusing checklocks. defer func() { f.Close() }() - fi, err := f.Stat() - if err != nil { - return _IOERR_FSTAT - } - vfsShmListMtx.Lock() defer vfsShmListMtx.Unlock() + // Stat file without opening it. + // Closing it would release all POSIX locks on it. + fi, err := os.Stat(s.path) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return _IOERR_FSTAT + } + // Find a shared file, increase the reference count. for _, g := range vfsShmList { if g != nil && os.SameFile(fi, g.info) { @@ -97,13 +97,33 @@ func (s *vfsShm) shmOpen() _ErrorCode { } } - // Lock and truncate the file. - // The lock is only released by closing the file. - if rc := osLock(f, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK); rc != _OK { + // Always open file read-write, as it will be shared. + f, err = os.OpenFile(s.path, + os.O_RDWR|os.O_CREATE|_O_NOFOLLOW, 0666) + if err != nil { + return _CANTOPEN + } + + // Dead man's switch. + if lock, rc := osTestLock(f, _SHM_DMS, 1); rc != _OK { + return _IOERR_LOCK + } else if lock == unix.F_WRLCK { + return _BUSY + } else if lock == unix.F_UNLCK { + if rc := osWriteLock(f, _SHM_DMS, 1); rc != _OK { + return rc + } + if err := f.Truncate(0); err != nil { + return _IOERR_SHMOPEN + } + } + if rc := osReadLock(f, _SHM_DMS, 1); rc != _OK { return rc } - if err := f.Truncate(0); err != nil { - return _IOERR_SHMOPEN + + fi, err = f.Stat() + if err != nil { + return _IOERR_FSTAT } // Add the new shared file. @@ -157,7 +177,42 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { s.Lock() defer s.Unlock() - return s.shmMemLock(offset, n, flags) + + // Check if we could obtain/release the lock locally. + rc := s.shmMemLock(offset, n, flags) + if rc != _OK { + return rc + } + + // Obtain/release the appropriate file locks. + switch { + case flags&_SHM_UNLOCK != 0: + begin, end := offset, offset+n + for i := begin; i < end; i++ { + if s.vfsShmParent.lock[i] != 0 { + if i > begin { + rc |= osUnlock(s.File, _SHM_BASE+int64(begin), int64(i-begin)) + } + begin = i + 1 + } + } + if end > begin { + rc |= osUnlock(s.File, _SHM_BASE+int64(begin), int64(end-begin)) + } + return rc + case flags&_SHM_SHARED != 0: + rc = osReadLock(s.File, _SHM_BASE+int64(offset), int64(n)) + case flags&_SHM_EXCLUSIVE != 0: + rc = osWriteLock(s.File, _SHM_BASE+int64(offset), int64(n)) + default: + panic(util.AssertErr()) + } + + // Release the local lock we had acquired. + if rc != _OK { + s.shmMemLock(offset, n, flags^(_SHM_UNLOCK|_SHM_LOCK)) + } + return rc } func (s *vfsShm) shmUnmap(delete bool) { diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_copy.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_copy.go index e6007aa1c..db8ddb4b8 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_copy.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_copy.go @@ -1,4 +1,4 @@ -//go:build (windows && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_dotlk +//go:build (windows && (386 || arm || amd64 || arm64 || riscv64 || ppc64le)) || sqlite3_dotlk package vfs @@ -13,7 +13,7 @@ _WALINDEX_PGSZ = 32768 ) -// This looks like a safe way of keeping the WAL-index in sync. +// This seems 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. @@ -27,7 +27,7 @@ // // 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. +// the copies below should correctly keep memory in sync. // // https://sqlite.org/walformat.html#the_wal_index_file_format @@ -35,7 +35,7 @@ func (s *vfsShm) shmAcquire(ptr *_ErrorCode) { if ptr != nil && *ptr != _OK { return } - if len(s.ptrs) == 0 || shmUnmodified(s.shadow[0][:], s.shared[0][:]) { + if len(s.ptrs) == 0 || shmEqual(s.shadow[0][:], s.shared[0][:]) { return } // Copies modified words from shared to private memory. @@ -53,7 +53,7 @@ func (s *vfsShm) shmAcquire(ptr *_ErrorCode) { } func (s *vfsShm) shmRelease() { - if len(s.ptrs) == 0 || shmUnmodified(s.shadow[0][:], util.View(s.mod, s.ptrs[0], _WALINDEX_HDR_SIZE)) { + if len(s.ptrs) == 0 || shmEqual(s.shadow[0][:], util.View(s.mod, s.ptrs[0], _WALINDEX_HDR_SIZE)) { return } // Copies modified words from private to shared memory. @@ -82,6 +82,6 @@ func shmPage(s []byte) *[_WALINDEX_PGSZ / 4]uint32 { return (*[_WALINDEX_PGSZ / 4]uint32)(unsafe.Slice(p, _WALINDEX_PGSZ/4)) } -func shmUnmodified(v1, v2 []byte) bool { +func shmEqual(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 index 4c7f47dec..842bea8f5 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_dotlk.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_dotlk.go @@ -6,18 +6,19 @@ "context" "errors" "io/fs" - "os" "sync" - "github.com/ncruces/go-sqlite3/internal/util" "github.com/tetratelabs/wazero/api" + + "github.com/ncruces/go-sqlite3/internal/dotlk" + "github.com/ncruces/go-sqlite3/internal/util" ) type vfsShmParent struct { shared [][_WALINDEX_PGSZ]byte refs int // +checklocks:vfsShmListMtx - lock [_SHM_NLOCK]int16 // +checklocks:Mutex + lock [_SHM_NLOCK]int8 // +checklocks:Mutex sync.Mutex } @@ -57,8 +58,7 @@ func (s *vfsShm) Close() error { return nil } - err := os.Remove(s.path) - if err != nil && !errors.Is(err, fs.ErrNotExist) { + if err := dotlk.Unlock(s.path); err != nil { return _IOERR_UNLOCK } delete(vfsShmList, s.path) @@ -81,9 +81,8 @@ func (s *vfsShm) shmOpen() _ErrorCode { 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) + // Dead man's switch. + err := dotlk.LockShm(s.path) if errors.Is(err, fs.ErrExist) { return _BUSY } diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_memlk.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_memlk.go index dc7b91350..5c8071ebe 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_memlk.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_memlk.go @@ -1,4 +1,4 @@ -//go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk +//go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le)) || sqlite3_flock || sqlite3_dotlk package vfs @@ -10,9 +10,6 @@ func (s *vfsShm) shmMemLock(offset, n int32, flags _ShmFlag) _ErrorCode { case flags&_SHM_UNLOCK != 0: for i := offset; i < offset+n; i++ { if s.lock[i] { - if s.vfsShmParent.lock[i] == 0 { - panic(util.AssertErr()) - } if s.vfsShmParent.lock[i] <= 0 { s.vfsShmParent.lock[i] = 0 } else { @@ -23,20 +20,21 @@ func (s *vfsShm) shmMemLock(offset, n int32, flags _ShmFlag) _ErrorCode { } case flags&_SHM_SHARED != 0: for i := offset; i < offset+n; i++ { - if s.lock[i] { - panic(util.AssertErr()) - } - if s.vfsShmParent.lock[i]+1 <= 0 { + if !s.lock[i] && + s.vfsShmParent.lock[i]+1 <= 0 { return _BUSY } } for i := offset; i < offset+n; i++ { - s.vfsShmParent.lock[i]++ - s.lock[i] = true + if !s.lock[i] { + s.vfsShmParent.lock[i]++ + s.lock[i] = true + } } case flags&_SHM_EXCLUSIVE != 0: for i := offset; i < offset+n; i++ { if s.lock[i] { + // SQLite never requests an exclusive lock that it already holds. panic(util.AssertErr()) } if s.vfsShmParent.lock[i] != 0 { diff --git a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_ofd.go b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_ofd.go index 75c8fbcfb..dd3611193 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_ofd.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_ofd.go @@ -1,4 +1,4 @@ -//go:build (linux || darwin) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_flock || sqlite3_dotlk || sqlite3_nosys) +//go:build (linux || darwin) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_flock || sqlite3_dotlk) package vfs @@ -20,6 +20,7 @@ type vfsShm struct { path string regions []*util.MappedRegion readOnly bool + fileLock bool blocking bool sync.Mutex } @@ -29,10 +30,10 @@ type vfsShm struct { 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) + os.O_RDWR|os.O_CREATE|_O_NOFOLLOW, 0666) if err != nil { f, err = os.OpenFile(s.path, - unix.O_RDONLY|unix.O_CREAT|unix.O_NOFOLLOW, 0666) + os.O_RDONLY|os.O_CREATE|_O_NOFOLLOW, 0666) s.readOnly = true } if err != nil { @@ -40,6 +41,9 @@ func (s *vfsShm) shmOpen() _ErrorCode { } s.File = f } + if s.fileLock { + return _OK + } // Dead man's switch. if lock, rc := osTestLock(s.File, _SHM_DMS, 1); rc != _OK { @@ -64,7 +68,9 @@ func (s *vfsShm) shmOpen() _ErrorCode { return _IOERR_SHMOPEN } } - return osReadLock(s.File, _SHM_DMS, 1, time.Millisecond) + rc := osReadLock(s.File, _SHM_DMS, 1, time.Millisecond) + s.fileLock = rc == _OK + return rc } func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (uint32, _ErrorCode) { @@ -104,7 +110,12 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { // Argument check. - if n <= 0 || offset < 0 || offset+n > _SHM_NLOCK { + switch { + case n <= 0: + panic(util.AssertErr()) + case offset < 0 || offset+n > _SHM_NLOCK: + panic(util.AssertErr()) + case n != 1 && flags&_SHM_EXCLUSIVE == 0: panic(util.AssertErr()) } switch flags { @@ -117,9 +128,6 @@ func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { default: panic(util.AssertErr()) } - if n != 1 && flags&_SHM_EXCLUSIVE == 0 { - panic(util.AssertErr()) - } var timeout time.Duration if s.blocking { 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 9602dd0cd..69319f07d 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 !(((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk) +//go:build !(((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le)) || 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 index 374d491ac..1de57640c 100644 --- a/vendor/github.com/ncruces/go-sqlite3/vfs/shm_windows.go +++ b/vendor/github.com/ncruces/go-sqlite3/vfs/shm_windows.go @@ -1,4 +1,4 @@ -//go:build (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_dotlk || sqlite3_nosys) +//go:build (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_dotlk package vfs @@ -7,6 +7,7 @@ "io" "os" "sync" + "syscall" "time" "github.com/tetratelabs/wazero/api" @@ -27,6 +28,7 @@ type vfsShm struct { shadow [][_WALINDEX_PGSZ]byte ptrs []uint32 stack [1]uint64 + fileLock bool blocking bool sync.Mutex } @@ -46,12 +48,16 @@ func (s *vfsShm) Close() error { func (s *vfsShm) shmOpen() _ErrorCode { if s.File == nil { - f, err := osutil.OpenFile(s.path, os.O_RDWR|os.O_CREATE, 0666) + f, err := osutil.OpenFile(s.path, + os.O_RDWR|os.O_CREATE|syscall.O_NONBLOCK, 0666) if err != nil { return _CANTOPEN } s.File = f } + if s.fileLock { + return _OK + } // Dead man's switch. if rc := osWriteLock(s.File, _SHM_DMS, 1, 0); rc == _OK { @@ -61,7 +67,9 @@ func (s *vfsShm) shmOpen() _ErrorCode { return _IOERR_SHMOPEN } } - return osReadLock(s.File, _SHM_DMS, 1, time.Millisecond) + rc := osReadLock(s.File, _SHM_DMS, 1, time.Millisecond) + s.fileLock = rc == _OK + return rc } func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (_ uint32, rc _ErrorCode) { diff --git a/vendor/golang.org/x/crypto/ssh/server.go b/vendor/golang.org/x/crypto/ssh/server.go index c0d1c29e6..5b5ccd96f 100644 --- a/vendor/golang.org/x/crypto/ssh/server.go +++ b/vendor/golang.org/x/crypto/ssh/server.go @@ -149,7 +149,7 @@ func (s *ServerConfig) AddHostKey(key Signer) { } // cachedPubKey contains the results of querying whether a public key is -// acceptable for a user. +// acceptable for a user. This is a FIFO cache. type cachedPubKey struct { user string pubKeyData []byte @@ -157,7 +157,13 @@ type cachedPubKey struct { perms *Permissions } -const maxCachedPubKeys = 16 +// maxCachedPubKeys is the number of cache entries we store. +// +// Due to consistent misuse of the PublicKeyCallback API, we have reduced this +// to 1, such that the only key in the cache is the most recently seen one. This +// forces the behavior that the last call to PublicKeyCallback will always be +// with the key that is used for authentication. +const maxCachedPubKeys = 1 // pubKeyCache caches tests for public keys. Since SSH clients // will query whether a public key is acceptable before attempting to @@ -179,9 +185,10 @@ func (c *pubKeyCache) get(user string, pubKeyData []byte) (cachedPubKey, bool) { // add adds the given tuple to the cache. func (c *pubKeyCache) add(candidate cachedPubKey) { - if len(c.keys) < maxCachedPubKeys { - c.keys = append(c.keys, candidate) + if len(c.keys) >= maxCachedPubKeys { + c.keys = c.keys[1:] } + c.keys = append(c.keys, candidate) } // ServerConn is an authenticated SSH connection, as seen from the diff --git a/vendor/golang.org/x/net/http2/frame.go b/vendor/golang.org/x/net/http2/frame.go index 105c3b279..81faec7e7 100644 --- a/vendor/golang.org/x/net/http2/frame.go +++ b/vendor/golang.org/x/net/http2/frame.go @@ -1490,7 +1490,7 @@ func (mh *MetaHeadersFrame) checkPseudos() error { pf := mh.PseudoFields() for i, hf := range pf { switch hf.Name { - case ":method", ":path", ":scheme", ":authority": + case ":method", ":path", ":scheme", ":authority", ":protocol": isRequest = true case ":status": isResponse = true @@ -1498,7 +1498,7 @@ func (mh *MetaHeadersFrame) checkPseudos() error { return pseudoHeaderError(hf.Name) } // Check for duplicates. - // This would be a bad algorithm, but N is 4. + // This would be a bad algorithm, but N is 5. // And this doesn't allocate. for _, hf2 := range pf[:i] { if hf.Name == hf2.Name { diff --git a/vendor/golang.org/x/net/http2/http2.go b/vendor/golang.org/x/net/http2/http2.go index 7688c356b..c7601c909 100644 --- a/vendor/golang.org/x/net/http2/http2.go +++ b/vendor/golang.org/x/net/http2/http2.go @@ -34,10 +34,11 @@ ) var ( - VerboseLogs bool - logFrameWrites bool - logFrameReads bool - inTests bool + VerboseLogs bool + logFrameWrites bool + logFrameReads bool + inTests bool + disableExtendedConnectProtocol bool ) func init() { @@ -50,6 +51,9 @@ func init() { logFrameWrites = true logFrameReads = true } + if strings.Contains(e, "http2xconnect=0") { + disableExtendedConnectProtocol = true + } } const ( @@ -141,6 +145,10 @@ func (s Setting) Valid() error { if s.Val < 16384 || s.Val > 1<<24-1 { return ConnectionError(ErrCodeProtocol) } + case SettingEnableConnectProtocol: + if s.Val != 1 && s.Val != 0 { + return ConnectionError(ErrCodeProtocol) + } } return nil } @@ -150,21 +158,23 @@ func (s Setting) Valid() error { type SettingID uint16 const ( - SettingHeaderTableSize SettingID = 0x1 - SettingEnablePush SettingID = 0x2 - SettingMaxConcurrentStreams SettingID = 0x3 - SettingInitialWindowSize SettingID = 0x4 - SettingMaxFrameSize SettingID = 0x5 - SettingMaxHeaderListSize SettingID = 0x6 + SettingHeaderTableSize SettingID = 0x1 + SettingEnablePush SettingID = 0x2 + SettingMaxConcurrentStreams SettingID = 0x3 + SettingInitialWindowSize SettingID = 0x4 + SettingMaxFrameSize SettingID = 0x5 + SettingMaxHeaderListSize SettingID = 0x6 + SettingEnableConnectProtocol SettingID = 0x8 ) var settingName = map[SettingID]string{ - SettingHeaderTableSize: "HEADER_TABLE_SIZE", - SettingEnablePush: "ENABLE_PUSH", - SettingMaxConcurrentStreams: "MAX_CONCURRENT_STREAMS", - SettingInitialWindowSize: "INITIAL_WINDOW_SIZE", - SettingMaxFrameSize: "MAX_FRAME_SIZE", - SettingMaxHeaderListSize: "MAX_HEADER_LIST_SIZE", + SettingHeaderTableSize: "HEADER_TABLE_SIZE", + SettingEnablePush: "ENABLE_PUSH", + SettingMaxConcurrentStreams: "MAX_CONCURRENT_STREAMS", + SettingInitialWindowSize: "INITIAL_WINDOW_SIZE", + SettingMaxFrameSize: "MAX_FRAME_SIZE", + SettingMaxHeaderListSize: "MAX_HEADER_LIST_SIZE", + SettingEnableConnectProtocol: "ENABLE_CONNECT_PROTOCOL", } func (s SettingID) String() string { diff --git a/vendor/golang.org/x/net/http2/server.go b/vendor/golang.org/x/net/http2/server.go index 832414b45..b55547aec 100644 --- a/vendor/golang.org/x/net/http2/server.go +++ b/vendor/golang.org/x/net/http2/server.go @@ -932,14 +932,18 @@ func (sc *serverConn) serve(conf http2Config) { sc.vlogf("http2: server connection from %v on %p", sc.conn.RemoteAddr(), sc.hs) } + settings := writeSettings{ + {SettingMaxFrameSize, conf.MaxReadFrameSize}, + {SettingMaxConcurrentStreams, sc.advMaxStreams}, + {SettingMaxHeaderListSize, sc.maxHeaderListSize()}, + {SettingHeaderTableSize, conf.MaxDecoderHeaderTableSize}, + {SettingInitialWindowSize, uint32(sc.initialStreamRecvWindowSize)}, + } + if !disableExtendedConnectProtocol { + settings = append(settings, Setting{SettingEnableConnectProtocol, 1}) + } sc.writeFrame(FrameWriteRequest{ - write: writeSettings{ - {SettingMaxFrameSize, conf.MaxReadFrameSize}, - {SettingMaxConcurrentStreams, sc.advMaxStreams}, - {SettingMaxHeaderListSize, sc.maxHeaderListSize()}, - {SettingHeaderTableSize, conf.MaxDecoderHeaderTableSize}, - {SettingInitialWindowSize, uint32(sc.initialStreamRecvWindowSize)}, - }, + write: settings, }) sc.unackedSettings++ @@ -1801,6 +1805,9 @@ func (sc *serverConn) processSetting(s Setting) error { sc.maxFrameSize = int32(s.Val) // the maximum valid s.Val is < 2^31 case SettingMaxHeaderListSize: sc.peerMaxHeaderListSize = s.Val + case SettingEnableConnectProtocol: + // Receipt of this parameter by a server does not + // have any impact default: // Unknown setting: "An endpoint that receives a SETTINGS // frame with any unknown or unsupported identifier MUST @@ -2231,11 +2238,17 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res scheme: f.PseudoValue("scheme"), authority: f.PseudoValue("authority"), path: f.PseudoValue("path"), + protocol: f.PseudoValue("protocol"), + } + + // extended connect is disabled, so we should not see :protocol + if disableExtendedConnectProtocol && rp.protocol != "" { + return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol)) } isConnect := rp.method == "CONNECT" if isConnect { - if rp.path != "" || rp.scheme != "" || rp.authority == "" { + if rp.protocol == "" && (rp.path != "" || rp.scheme != "" || rp.authority == "") { return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol)) } } else if rp.method == "" || rp.path == "" || (rp.scheme != "https" && rp.scheme != "http") { @@ -2259,6 +2272,9 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res if rp.authority == "" { rp.authority = rp.header.Get("Host") } + if rp.protocol != "" { + rp.header.Set(":protocol", rp.protocol) + } rw, req, err := sc.newWriterAndRequestNoBody(st, rp) if err != nil { @@ -2285,6 +2301,7 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res type requestParam struct { method string scheme, authority, path string + protocol string header http.Header } @@ -2326,7 +2343,7 @@ func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp requestParam) (*r var url_ *url.URL var requestURI string - if rp.method == "CONNECT" { + if rp.method == "CONNECT" && rp.protocol == "" { url_ = &url.URL{Host: rp.authority} requestURI = rp.authority // mimic HTTP/1 server behavior } else { diff --git a/vendor/golang.org/x/net/http2/transport.go b/vendor/golang.org/x/net/http2/transport.go index f5968f440..090d0e1bd 100644 --- a/vendor/golang.org/x/net/http2/transport.go +++ b/vendor/golang.org/x/net/http2/transport.go @@ -368,25 +368,26 @@ type ClientConn struct { idleTimeout time.Duration // or 0 for never idleTimer timer - mu sync.Mutex // guards following - cond *sync.Cond // hold mu; broadcast on flow/closed changes - flow outflow // our conn-level flow control quota (cs.outflow is per stream) - inflow inflow // peer's conn-level flow control - doNotReuse bool // whether conn is marked to not be reused for any future requests - closing bool - closed bool - seenSettings bool // true if we've seen a settings frame, false otherwise - wantSettingsAck bool // we sent a SETTINGS frame and haven't heard back - goAway *GoAwayFrame // if non-nil, the GoAwayFrame we received - goAwayDebug string // goAway frame's debug data, retained as a string - streams map[uint32]*clientStream // client-initiated - streamsReserved int // incr by ReserveNewRequest; decr on RoundTrip - nextStreamID uint32 - pendingRequests int // requests blocked and waiting to be sent because len(streams) == maxConcurrentStreams - pings map[[8]byte]chan struct{} // in flight ping data to notification channel - br *bufio.Reader - lastActive time.Time - lastIdle time.Time // time last idle + mu sync.Mutex // guards following + cond *sync.Cond // hold mu; broadcast on flow/closed changes + flow outflow // our conn-level flow control quota (cs.outflow is per stream) + inflow inflow // peer's conn-level flow control + doNotReuse bool // whether conn is marked to not be reused for any future requests + closing bool + closed bool + seenSettings bool // true if we've seen a settings frame, false otherwise + seenSettingsChan chan struct{} // closed when seenSettings is true or frame reading fails + wantSettingsAck bool // we sent a SETTINGS frame and haven't heard back + goAway *GoAwayFrame // if non-nil, the GoAwayFrame we received + goAwayDebug string // goAway frame's debug data, retained as a string + streams map[uint32]*clientStream // client-initiated + streamsReserved int // incr by ReserveNewRequest; decr on RoundTrip + nextStreamID uint32 + pendingRequests int // requests blocked and waiting to be sent because len(streams) == maxConcurrentStreams + pings map[[8]byte]chan struct{} // in flight ping data to notification channel + br *bufio.Reader + lastActive time.Time + lastIdle time.Time // time last idle // Settings from peer: (also guarded by wmu) maxFrameSize uint32 maxConcurrentStreams uint32 @@ -396,6 +397,17 @@ type ClientConn struct { initialStreamRecvWindowSize int32 readIdleTimeout time.Duration pingTimeout time.Duration + extendedConnectAllowed bool + + // rstStreamPingsBlocked works around an unfortunate gRPC behavior. + // gRPC strictly limits the number of PING frames that it will receive. + // The default is two pings per two hours, but the limit resets every time + // the gRPC endpoint sends a HEADERS or DATA frame. See golang/go#70575. + // + // rstStreamPingsBlocked is set after receiving a response to a PING frame + // bundled with an RST_STREAM (see pendingResets below), and cleared after + // receiving a HEADERS or DATA frame. + rstStreamPingsBlocked bool // pendingResets is the number of RST_STREAM frames we have sent to the peer, // without confirming that the peer has received them. When we send a RST_STREAM, @@ -819,6 +831,7 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro peerMaxHeaderListSize: 0xffffffffffffffff, // "infinite", per spec. Use 2^64-1 instead. streams: make(map[uint32]*clientStream), singleUse: singleUse, + seenSettingsChan: make(chan struct{}), wantSettingsAck: true, readIdleTimeout: conf.SendPingTimeout, pingTimeout: conf.PingTimeout, @@ -1466,6 +1479,8 @@ func (cs *clientStream) doRequest(req *http.Request, streamf func(*clientStream) cs.cleanupWriteRequest(err) } +var errExtendedConnectNotSupported = errors.New("net/http: extended connect not supported by peer") + // writeRequest sends a request. // // It returns nil after the request is written, the response read, @@ -1481,12 +1496,31 @@ func (cs *clientStream) writeRequest(req *http.Request, streamf func(*clientStre return err } + // wait for setting frames to be received, a server can change this value later, + // but we just wait for the first settings frame + var isExtendedConnect bool + if req.Method == "CONNECT" && req.Header.Get(":protocol") != "" { + isExtendedConnect = true + } + // Acquire the new-request lock by writing to reqHeaderMu. // This lock guards the critical section covering allocating a new stream ID // (requires mu) and creating the stream (requires wmu). if cc.reqHeaderMu == nil { panic("RoundTrip on uninitialized ClientConn") // for tests } + if isExtendedConnect { + select { + case <-cs.reqCancel: + return errRequestCanceled + case <-ctx.Done(): + return ctx.Err() + case <-cc.seenSettingsChan: + if !cc.extendedConnectAllowed { + return errExtendedConnectNotSupported + } + } + } select { case cc.reqHeaderMu <- struct{}{}: case <-cs.reqCancel: @@ -1714,10 +1748,14 @@ func (cs *clientStream) cleanupWriteRequest(err error) { ping := false if !closeOnIdle { cc.mu.Lock() - if cc.pendingResets == 0 { - ping = true + // rstStreamPingsBlocked works around a gRPC behavior: + // see comment on the field for details. + if !cc.rstStreamPingsBlocked { + if cc.pendingResets == 0 { + ping = true + } + cc.pendingResets++ } - cc.pendingResets++ cc.mu.Unlock() } cc.writeStreamReset(cs.ID, ErrCodeCancel, ping, err) @@ -2030,7 +2068,7 @@ func (cs *clientStream) awaitFlowControl(maxBytes int) (taken int32, err error) func validateHeaders(hdrs http.Header) string { for k, vv := range hdrs { - if !httpguts.ValidHeaderFieldName(k) { + if !httpguts.ValidHeaderFieldName(k) && k != ":protocol" { return fmt.Sprintf("name %q", k) } for _, v := range vv { @@ -2046,6 +2084,10 @@ func validateHeaders(hdrs http.Header) string { var errNilRequestURL = errors.New("http2: Request.URI is nil") +func isNormalConnect(req *http.Request) bool { + return req.Method == "CONNECT" && req.Header.Get(":protocol") == "" +} + // requires cc.wmu be held. func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64) ([]byte, error) { cc.hbuf.Reset() @@ -2066,7 +2108,7 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail } var path string - if req.Method != "CONNECT" { + if !isNormalConnect(req) { path = req.URL.RequestURI() if !validPseudoPath(path) { orig := path @@ -2103,7 +2145,7 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail m = http.MethodGet } f(":method", m) - if req.Method != "CONNECT" { + if !isNormalConnect(req) { f(":path", path) f(":scheme", req.URL.Scheme) } @@ -2461,7 +2503,7 @@ func (rl *clientConnReadLoop) run() error { cc.vlogf("http2: Transport readFrame error on conn %p: (%T) %v", cc, err, err) } if se, ok := err.(StreamError); ok { - if cs := rl.streamByID(se.StreamID); cs != nil { + if cs := rl.streamByID(se.StreamID, notHeaderOrDataFrame); cs != nil { if se.Cause == nil { se.Cause = cc.fr.errDetail } @@ -2507,13 +2549,16 @@ func (rl *clientConnReadLoop) run() error { if VerboseLogs { cc.vlogf("http2: Transport conn %p received error from processing frame %v: %v", cc, summarizeFrame(f), err) } + if !cc.seenSettings { + close(cc.seenSettingsChan) + } return err } } } func (rl *clientConnReadLoop) processHeaders(f *MetaHeadersFrame) error { - cs := rl.streamByID(f.StreamID) + cs := rl.streamByID(f.StreamID, headerOrDataFrame) if cs == nil { // We'd get here if we canceled a request while the // server had its response still in flight. So if this @@ -2842,7 +2887,7 @@ func (b transportResponseBody) Close() error { func (rl *clientConnReadLoop) processData(f *DataFrame) error { cc := rl.cc - cs := rl.streamByID(f.StreamID) + cs := rl.streamByID(f.StreamID, headerOrDataFrame) data := f.Data() if cs == nil { cc.mu.Lock() @@ -2977,9 +3022,22 @@ func (rl *clientConnReadLoop) endStreamError(cs *clientStream, err error) { cs.abortStream(err) } -func (rl *clientConnReadLoop) streamByID(id uint32) *clientStream { +// Constants passed to streamByID for documentation purposes. +const ( + headerOrDataFrame = true + notHeaderOrDataFrame = false +) + +// streamByID returns the stream with the given id, or nil if no stream has that id. +// If headerOrData is true, it clears rst.StreamPingsBlocked. +func (rl *clientConnReadLoop) streamByID(id uint32, headerOrData bool) *clientStream { rl.cc.mu.Lock() defer rl.cc.mu.Unlock() + if headerOrData { + // Work around an unfortunate gRPC behavior. + // See comment on ClientConn.rstStreamPingsBlocked for details. + rl.cc.rstStreamPingsBlocked = false + } cs := rl.cc.streams[id] if cs != nil && !cs.readAborted { return cs @@ -3073,6 +3131,21 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error { case SettingHeaderTableSize: cc.henc.SetMaxDynamicTableSize(s.Val) cc.peerMaxHeaderTableSize = s.Val + case SettingEnableConnectProtocol: + if err := s.Valid(); err != nil { + return err + } + // If the peer wants to send us SETTINGS_ENABLE_CONNECT_PROTOCOL, + // we require that it do so in the first SETTINGS frame. + // + // When we attempt to use extended CONNECT, we wait for the first + // SETTINGS frame to see if the server supports it. If we let the + // server enable the feature with a later SETTINGS frame, then + // users will see inconsistent results depending on whether we've + // seen that frame or not. + if !cc.seenSettings { + cc.extendedConnectAllowed = s.Val == 1 + } default: cc.vlogf("Unhandled Setting: %v", s) } @@ -3090,6 +3163,7 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error { // connection can establish to our default. cc.maxConcurrentStreams = defaultMaxConcurrentStreams } + close(cc.seenSettingsChan) cc.seenSettings = true } @@ -3098,7 +3172,7 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error { func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error { cc := rl.cc - cs := rl.streamByID(f.StreamID) + cs := rl.streamByID(f.StreamID, notHeaderOrDataFrame) if f.StreamID != 0 && cs == nil { return nil } @@ -3127,7 +3201,7 @@ func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error { } func (rl *clientConnReadLoop) processResetStream(f *RSTStreamFrame) error { - cs := rl.streamByID(f.StreamID) + cs := rl.streamByID(f.StreamID, notHeaderOrDataFrame) if cs == nil { // TODO: return error if server tries to RST_STREAM an idle stream return nil @@ -3205,6 +3279,7 @@ func (rl *clientConnReadLoop) processPing(f *PingFrame) error { if cc.pendingResets > 0 { // See clientStream.cleanupWriteRequest. cc.pendingResets = 0 + cc.rstStreamPingsBlocked = true cc.cond.Broadcast() } return nil diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux.go b/vendor/golang.org/x/sys/unix/zerrors_linux.go index ccba391c9..6ebc48b3f 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux.go @@ -321,6 +321,9 @@ AUDIT_INTEGRITY_STATUS = 0x70a AUDIT_IPC = 0x517 AUDIT_IPC_SET_PERM = 0x51f + AUDIT_IPE_ACCESS = 0x58c + AUDIT_IPE_CONFIG_CHANGE = 0x58d + AUDIT_IPE_POLICY_LOAD = 0x58e AUDIT_KERNEL = 0x7d0 AUDIT_KERNEL_OTHER = 0x524 AUDIT_KERN_MODULE = 0x532 @@ -489,6 +492,7 @@ BPF_F_ID = 0x20 BPF_F_NETFILTER_IP_DEFRAG = 0x1 BPF_F_QUERY_EFFECTIVE = 0x1 + BPF_F_REDIRECT_FLAGS = 0x19 BPF_F_REPLACE = 0x4 BPF_F_SLEEPABLE = 0x10 BPF_F_STRICT_ALIGNMENT = 0x1 @@ -1166,6 +1170,7 @@ EXTA = 0xe EXTB = 0xf F2FS_SUPER_MAGIC = 0xf2f52010 + FALLOC_FL_ALLOCATE_RANGE = 0x0 FALLOC_FL_COLLAPSE_RANGE = 0x8 FALLOC_FL_INSERT_RANGE = 0x20 FALLOC_FL_KEEP_SIZE = 0x1 @@ -1799,6 +1804,8 @@ LANDLOCK_ACCESS_NET_BIND_TCP = 0x1 LANDLOCK_ACCESS_NET_CONNECT_TCP = 0x2 LANDLOCK_CREATE_RULESET_VERSION = 0x1 + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET = 0x1 + LANDLOCK_SCOPE_SIGNAL = 0x2 LINUX_REBOOT_CMD_CAD_OFF = 0x0 LINUX_REBOOT_CMD_CAD_ON = 0x89abcdef LINUX_REBOOT_CMD_HALT = 0xcdef0123 @@ -1924,6 +1931,7 @@ MNT_FORCE = 0x1 MNT_ID_REQ_SIZE_VER0 = 0x18 MNT_ID_REQ_SIZE_VER1 = 0x20 + MNT_NS_INFO_SIZE_VER0 = 0x10 MODULE_INIT_COMPRESSED_FILE = 0x4 MODULE_INIT_IGNORE_MODVERSIONS = 0x1 MODULE_INIT_IGNORE_VERMAGIC = 0x2 @@ -2970,6 +2978,7 @@ RWF_WRITE_LIFE_NOT_SET = 0x0 SCHED_BATCH = 0x3 SCHED_DEADLINE = 0x6 + SCHED_EXT = 0x7 SCHED_FIFO = 0x1 SCHED_FLAG_ALL = 0x7f SCHED_FLAG_DL_OVERRUN = 0x4 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 0c00cb3f3..c0d45e320 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_386.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_386.go @@ -109,6 +109,7 @@ HIDIOCGRAWINFO = 0x80084803 HIDIOCGRDESC = 0x90044802 HIDIOCGRDESCSIZE = 0x80044801 + HIDIOCREVOKE = 0x4004480d HUPCL = 0x400 ICANON = 0x2 IEXTEN = 0x8000 @@ -297,6 +298,8 @@ RTC_WIE_ON = 0x700f RTC_WKALM_RD = 0x80287010 RTC_WKALM_SET = 0x4028700f + SCM_DEVMEM_DMABUF = 0x4f + SCM_DEVMEM_LINEAR = 0x4e SCM_TIMESTAMPING = 0x25 SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a @@ -335,6 +338,9 @@ SO_CNX_ADVICE = 0x35 SO_COOKIE = 0x39 SO_DETACH_REUSEPORT_BPF = 0x44 + SO_DEVMEM_DMABUF = 0x4f + SO_DEVMEM_DONTNEED = 0x50 + SO_DEVMEM_LINEAR = 0x4e SO_DOMAIN = 0x27 SO_DONTROUTE = 0x5 SO_ERROR = 0x4 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 dfb364554..c731d24f0 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go @@ -109,6 +109,7 @@ HIDIOCGRAWINFO = 0x80084803 HIDIOCGRDESC = 0x90044802 HIDIOCGRDESCSIZE = 0x80044801 + HIDIOCREVOKE = 0x4004480d HUPCL = 0x400 ICANON = 0x2 IEXTEN = 0x8000 @@ -298,6 +299,8 @@ RTC_WIE_ON = 0x700f RTC_WKALM_RD = 0x80287010 RTC_WKALM_SET = 0x4028700f + SCM_DEVMEM_DMABUF = 0x4f + SCM_DEVMEM_LINEAR = 0x4e SCM_TIMESTAMPING = 0x25 SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a @@ -336,6 +339,9 @@ SO_CNX_ADVICE = 0x35 SO_COOKIE = 0x39 SO_DETACH_REUSEPORT_BPF = 0x44 + SO_DEVMEM_DMABUF = 0x4f + SO_DEVMEM_DONTNEED = 0x50 + SO_DEVMEM_LINEAR = 0x4e SO_DOMAIN = 0x27 SO_DONTROUTE = 0x5 SO_ERROR = 0x4 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 d46dcf78a..680018a4a 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go @@ -108,6 +108,7 @@ HIDIOCGRAWINFO = 0x80084803 HIDIOCGRDESC = 0x90044802 HIDIOCGRDESCSIZE = 0x80044801 + HIDIOCREVOKE = 0x4004480d HUPCL = 0x400 ICANON = 0x2 IEXTEN = 0x8000 @@ -303,6 +304,8 @@ RTC_WIE_ON = 0x700f RTC_WKALM_RD = 0x80287010 RTC_WKALM_SET = 0x4028700f + SCM_DEVMEM_DMABUF = 0x4f + SCM_DEVMEM_LINEAR = 0x4e SCM_TIMESTAMPING = 0x25 SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a @@ -341,6 +344,9 @@ SO_CNX_ADVICE = 0x35 SO_COOKIE = 0x39 SO_DETACH_REUSEPORT_BPF = 0x44 + SO_DEVMEM_DMABUF = 0x4f + SO_DEVMEM_DONTNEED = 0x50 + SO_DEVMEM_LINEAR = 0x4e SO_DOMAIN = 0x27 SO_DONTROUTE = 0x5 SO_ERROR = 0x4 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 3af3248a7..a63909f30 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go @@ -112,6 +112,7 @@ HIDIOCGRAWINFO = 0x80084803 HIDIOCGRDESC = 0x90044802 HIDIOCGRDESCSIZE = 0x80044801 + HIDIOCREVOKE = 0x4004480d HUPCL = 0x400 ICANON = 0x2 IEXTEN = 0x8000 @@ -205,6 +206,7 @@ PERF_EVENT_IOC_SET_BPF = 0x40042408 PERF_EVENT_IOC_SET_FILTER = 0x40082406 PERF_EVENT_IOC_SET_OUTPUT = 0x2405 + POE_MAGIC = 0x504f4530 PPPIOCATTACH = 0x4004743d PPPIOCATTCHAN = 0x40047438 PPPIOCBRIDGECHAN = 0x40047435 @@ -294,6 +296,8 @@ RTC_WIE_ON = 0x700f RTC_WKALM_RD = 0x80287010 RTC_WKALM_SET = 0x4028700f + SCM_DEVMEM_DMABUF = 0x4f + SCM_DEVMEM_LINEAR = 0x4e SCM_TIMESTAMPING = 0x25 SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a @@ -332,6 +336,9 @@ SO_CNX_ADVICE = 0x35 SO_COOKIE = 0x39 SO_DETACH_REUSEPORT_BPF = 0x44 + SO_DEVMEM_DMABUF = 0x4f + SO_DEVMEM_DONTNEED = 0x50 + SO_DEVMEM_LINEAR = 0x4e SO_DOMAIN = 0x27 SO_DONTROUTE = 0x5 SO_ERROR = 0x4 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 292bcf028..9b0a2573f 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go @@ -109,6 +109,7 @@ HIDIOCGRAWINFO = 0x80084803 HIDIOCGRDESC = 0x90044802 HIDIOCGRDESCSIZE = 0x80044801 + HIDIOCREVOKE = 0x4004480d HUPCL = 0x400 ICANON = 0x2 IEXTEN = 0x8000 @@ -290,6 +291,8 @@ RTC_WIE_ON = 0x700f RTC_WKALM_RD = 0x80287010 RTC_WKALM_SET = 0x4028700f + SCM_DEVMEM_DMABUF = 0x4f + SCM_DEVMEM_LINEAR = 0x4e SCM_TIMESTAMPING = 0x25 SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a @@ -328,6 +331,9 @@ SO_CNX_ADVICE = 0x35 SO_COOKIE = 0x39 SO_DETACH_REUSEPORT_BPF = 0x44 + SO_DEVMEM_DMABUF = 0x4f + SO_DEVMEM_DONTNEED = 0x50 + SO_DEVMEM_LINEAR = 0x4e SO_DOMAIN = 0x27 SO_DONTROUTE = 0x5 SO_ERROR = 0x4 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 782b7110f..958e6e064 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go @@ -108,6 +108,7 @@ HIDIOCGRAWINFO = 0x40084803 HIDIOCGRDESC = 0x50044802 HIDIOCGRDESCSIZE = 0x40044801 + HIDIOCREVOKE = 0x8004480d HUPCL = 0x400 ICANON = 0x2 IEXTEN = 0x100 @@ -296,6 +297,8 @@ RTC_WIE_ON = 0x2000700f RTC_WKALM_RD = 0x40287010 RTC_WKALM_SET = 0x8028700f + SCM_DEVMEM_DMABUF = 0x4f + SCM_DEVMEM_LINEAR = 0x4e SCM_TIMESTAMPING = 0x25 SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a @@ -334,6 +337,9 @@ SO_CNX_ADVICE = 0x35 SO_COOKIE = 0x39 SO_DETACH_REUSEPORT_BPF = 0x44 + SO_DEVMEM_DMABUF = 0x4f + SO_DEVMEM_DONTNEED = 0x50 + SO_DEVMEM_LINEAR = 0x4e SO_DOMAIN = 0x1029 SO_DONTROUTE = 0x10 SO_ERROR = 0x1007 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 84973fd92..50c7f25bd 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go @@ -108,6 +108,7 @@ HIDIOCGRAWINFO = 0x40084803 HIDIOCGRDESC = 0x50044802 HIDIOCGRDESCSIZE = 0x40044801 + HIDIOCREVOKE = 0x8004480d HUPCL = 0x400 ICANON = 0x2 IEXTEN = 0x100 @@ -296,6 +297,8 @@ RTC_WIE_ON = 0x2000700f RTC_WKALM_RD = 0x40287010 RTC_WKALM_SET = 0x8028700f + SCM_DEVMEM_DMABUF = 0x4f + SCM_DEVMEM_LINEAR = 0x4e SCM_TIMESTAMPING = 0x25 SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a @@ -334,6 +337,9 @@ SO_CNX_ADVICE = 0x35 SO_COOKIE = 0x39 SO_DETACH_REUSEPORT_BPF = 0x44 + SO_DEVMEM_DMABUF = 0x4f + SO_DEVMEM_DONTNEED = 0x50 + SO_DEVMEM_LINEAR = 0x4e SO_DOMAIN = 0x1029 SO_DONTROUTE = 0x10 SO_ERROR = 0x1007 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 6d9cbc3b2..ced21d66d 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go @@ -108,6 +108,7 @@ HIDIOCGRAWINFO = 0x40084803 HIDIOCGRDESC = 0x50044802 HIDIOCGRDESCSIZE = 0x40044801 + HIDIOCREVOKE = 0x8004480d HUPCL = 0x400 ICANON = 0x2 IEXTEN = 0x100 @@ -296,6 +297,8 @@ RTC_WIE_ON = 0x2000700f RTC_WKALM_RD = 0x40287010 RTC_WKALM_SET = 0x8028700f + SCM_DEVMEM_DMABUF = 0x4f + SCM_DEVMEM_LINEAR = 0x4e SCM_TIMESTAMPING = 0x25 SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a @@ -334,6 +337,9 @@ SO_CNX_ADVICE = 0x35 SO_COOKIE = 0x39 SO_DETACH_REUSEPORT_BPF = 0x44 + SO_DEVMEM_DMABUF = 0x4f + SO_DEVMEM_DONTNEED = 0x50 + SO_DEVMEM_LINEAR = 0x4e SO_DOMAIN = 0x1029 SO_DONTROUTE = 0x10 SO_ERROR = 0x1007 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 5f9fedbce..226c04419 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go @@ -108,6 +108,7 @@ HIDIOCGRAWINFO = 0x40084803 HIDIOCGRDESC = 0x50044802 HIDIOCGRDESCSIZE = 0x40044801 + HIDIOCREVOKE = 0x8004480d HUPCL = 0x400 ICANON = 0x2 IEXTEN = 0x100 @@ -296,6 +297,8 @@ RTC_WIE_ON = 0x2000700f RTC_WKALM_RD = 0x40287010 RTC_WKALM_SET = 0x8028700f + SCM_DEVMEM_DMABUF = 0x4f + SCM_DEVMEM_LINEAR = 0x4e SCM_TIMESTAMPING = 0x25 SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a @@ -334,6 +337,9 @@ SO_CNX_ADVICE = 0x35 SO_COOKIE = 0x39 SO_DETACH_REUSEPORT_BPF = 0x44 + SO_DEVMEM_DMABUF = 0x4f + SO_DEVMEM_DONTNEED = 0x50 + SO_DEVMEM_LINEAR = 0x4e SO_DOMAIN = 0x1029 SO_DONTROUTE = 0x10 SO_ERROR = 0x1007 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 bb0026ee0..3122737cd 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go @@ -108,6 +108,7 @@ HIDIOCGRAWINFO = 0x40084803 HIDIOCGRDESC = 0x50044802 HIDIOCGRDESCSIZE = 0x40044801 + HIDIOCREVOKE = 0x8004480d HUPCL = 0x4000 ICANON = 0x100 IEXTEN = 0x400 @@ -351,6 +352,8 @@ RTC_WIE_ON = 0x2000700f RTC_WKALM_RD = 0x40287010 RTC_WKALM_SET = 0x8028700f + SCM_DEVMEM_DMABUF = 0x4f + SCM_DEVMEM_LINEAR = 0x4e SCM_TIMESTAMPING = 0x25 SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a @@ -389,6 +392,9 @@ SO_CNX_ADVICE = 0x35 SO_COOKIE = 0x39 SO_DETACH_REUSEPORT_BPF = 0x44 + SO_DEVMEM_DMABUF = 0x4f + SO_DEVMEM_DONTNEED = 0x50 + SO_DEVMEM_LINEAR = 0x4e SO_DOMAIN = 0x27 SO_DONTROUTE = 0x5 SO_ERROR = 0x4 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 46120db5c..eb5d3467e 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go @@ -108,6 +108,7 @@ HIDIOCGRAWINFO = 0x40084803 HIDIOCGRDESC = 0x50044802 HIDIOCGRDESCSIZE = 0x40044801 + HIDIOCREVOKE = 0x8004480d HUPCL = 0x4000 ICANON = 0x100 IEXTEN = 0x400 @@ -355,6 +356,8 @@ RTC_WIE_ON = 0x2000700f RTC_WKALM_RD = 0x40287010 RTC_WKALM_SET = 0x8028700f + SCM_DEVMEM_DMABUF = 0x4f + SCM_DEVMEM_LINEAR = 0x4e SCM_TIMESTAMPING = 0x25 SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a @@ -393,6 +396,9 @@ SO_CNX_ADVICE = 0x35 SO_COOKIE = 0x39 SO_DETACH_REUSEPORT_BPF = 0x44 + SO_DEVMEM_DMABUF = 0x4f + SO_DEVMEM_DONTNEED = 0x50 + SO_DEVMEM_LINEAR = 0x4e SO_DOMAIN = 0x27 SO_DONTROUTE = 0x5 SO_ERROR = 0x4 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 5c951634f..e921ebc60 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go @@ -108,6 +108,7 @@ HIDIOCGRAWINFO = 0x40084803 HIDIOCGRDESC = 0x50044802 HIDIOCGRDESCSIZE = 0x40044801 + HIDIOCREVOKE = 0x8004480d HUPCL = 0x4000 ICANON = 0x100 IEXTEN = 0x400 @@ -355,6 +356,8 @@ RTC_WIE_ON = 0x2000700f RTC_WKALM_RD = 0x40287010 RTC_WKALM_SET = 0x8028700f + SCM_DEVMEM_DMABUF = 0x4f + SCM_DEVMEM_LINEAR = 0x4e SCM_TIMESTAMPING = 0x25 SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a @@ -393,6 +396,9 @@ SO_CNX_ADVICE = 0x35 SO_COOKIE = 0x39 SO_DETACH_REUSEPORT_BPF = 0x44 + SO_DEVMEM_DMABUF = 0x4f + SO_DEVMEM_DONTNEED = 0x50 + SO_DEVMEM_LINEAR = 0x4e SO_DOMAIN = 0x27 SO_DONTROUTE = 0x5 SO_ERROR = 0x4 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 11a84d5af..38ba81c55 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go @@ -108,6 +108,7 @@ HIDIOCGRAWINFO = 0x80084803 HIDIOCGRDESC = 0x90044802 HIDIOCGRDESCSIZE = 0x80044801 + HIDIOCREVOKE = 0x4004480d HUPCL = 0x400 ICANON = 0x2 IEXTEN = 0x8000 @@ -287,6 +288,8 @@ RTC_WIE_ON = 0x700f RTC_WKALM_RD = 0x80287010 RTC_WKALM_SET = 0x4028700f + SCM_DEVMEM_DMABUF = 0x4f + SCM_DEVMEM_LINEAR = 0x4e SCM_TIMESTAMPING = 0x25 SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a @@ -325,6 +328,9 @@ SO_CNX_ADVICE = 0x35 SO_COOKIE = 0x39 SO_DETACH_REUSEPORT_BPF = 0x44 + SO_DEVMEM_DMABUF = 0x4f + SO_DEVMEM_DONTNEED = 0x50 + SO_DEVMEM_LINEAR = 0x4e SO_DOMAIN = 0x27 SO_DONTROUTE = 0x5 SO_ERROR = 0x4 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 f78c4617c..71f040097 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go @@ -108,6 +108,7 @@ HIDIOCGRAWINFO = 0x80084803 HIDIOCGRDESC = 0x90044802 HIDIOCGRDESCSIZE = 0x80044801 + HIDIOCREVOKE = 0x4004480d HUPCL = 0x400 ICANON = 0x2 IEXTEN = 0x8000 @@ -359,6 +360,8 @@ RTC_WIE_ON = 0x700f RTC_WKALM_RD = 0x80287010 RTC_WKALM_SET = 0x4028700f + SCM_DEVMEM_DMABUF = 0x4f + SCM_DEVMEM_LINEAR = 0x4e SCM_TIMESTAMPING = 0x25 SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a @@ -397,6 +400,9 @@ SO_CNX_ADVICE = 0x35 SO_COOKIE = 0x39 SO_DETACH_REUSEPORT_BPF = 0x44 + SO_DEVMEM_DMABUF = 0x4f + SO_DEVMEM_DONTNEED = 0x50 + SO_DEVMEM_LINEAR = 0x4e SO_DOMAIN = 0x27 SO_DONTROUTE = 0x5 SO_ERROR = 0x4 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 aeb777c34..c44a31332 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go @@ -112,6 +112,7 @@ HIDIOCGRAWINFO = 0x40084803 HIDIOCGRDESC = 0x50044802 HIDIOCGRDESCSIZE = 0x40044801 + HIDIOCREVOKE = 0x8004480d HUPCL = 0x400 ICANON = 0x2 IEXTEN = 0x8000 @@ -350,6 +351,8 @@ RTC_WIE_ON = 0x2000700f RTC_WKALM_RD = 0x40287010 RTC_WKALM_SET = 0x8028700f + SCM_DEVMEM_DMABUF = 0x58 + SCM_DEVMEM_LINEAR = 0x57 SCM_TIMESTAMPING = 0x23 SCM_TIMESTAMPING_OPT_STATS = 0x38 SCM_TIMESTAMPING_PKTINFO = 0x3c @@ -436,6 +439,9 @@ SO_CNX_ADVICE = 0x37 SO_COOKIE = 0x3b SO_DETACH_REUSEPORT_BPF = 0x47 + SO_DEVMEM_DMABUF = 0x58 + SO_DEVMEM_DONTNEED = 0x59 + SO_DEVMEM_LINEAR = 0x57 SO_DOMAIN = 0x1029 SO_DONTROUTE = 0x10 SO_ERROR = 0x1007 diff --git a/vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go b/vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go index d003c3d43..17c53bd9b 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go @@ -462,11 +462,14 @@ type FdSet struct { const ( SizeofIfMsghdr = 0x70 + SizeofIfMsghdr2 = 0xa0 SizeofIfData = 0x60 + SizeofIfData64 = 0x80 SizeofIfaMsghdr = 0x14 SizeofIfmaMsghdr = 0x10 SizeofIfmaMsghdr2 = 0x14 SizeofRtMsghdr = 0x5c + SizeofRtMsghdr2 = 0x5c SizeofRtMetrics = 0x38 ) @@ -480,6 +483,20 @@ type IfMsghdr struct { Data IfData } +type IfMsghdr2 struct { + Msglen uint16 + Version uint8 + Type uint8 + Addrs int32 + Flags int32 + Index uint16 + Snd_len int32 + Snd_maxlen int32 + Snd_drops int32 + Timer int32 + Data IfData64 +} + type IfData struct { Type uint8 Typelen uint8 @@ -512,6 +529,34 @@ type IfData struct { Reserved2 uint32 } +type IfData64 struct { + Type uint8 + Typelen uint8 + Physical uint8 + Addrlen uint8 + Hdrlen uint8 + Recvquota uint8 + Xmitquota uint8 + Unused1 uint8 + Mtu uint32 + Metric uint32 + Baudrate uint64 + Ipackets uint64 + Ierrors uint64 + Opackets uint64 + Oerrors uint64 + Collisions uint64 + Ibytes uint64 + Obytes uint64 + Imcasts uint64 + Omcasts uint64 + Iqdrops uint64 + Noproto uint64 + Recvtiming uint32 + Xmittiming uint32 + Lastchange Timeval32 +} + type IfaMsghdr struct { Msglen uint16 Version uint8 @@ -557,6 +602,21 @@ type RtMsghdr struct { Rmx RtMetrics } +type RtMsghdr2 struct { + Msglen uint16 + Version uint8 + Type uint8 + Index uint16 + Flags int32 + Addrs int32 + Refcnt int32 + Parentflags int32 + Reserved int32 + Use int32 + Inits uint32 + Rmx RtMetrics +} + type RtMetrics struct { Locks uint32 Mtu uint32 diff --git a/vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go b/vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go index 0d45a941a..2392226a7 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go @@ -462,11 +462,14 @@ type FdSet struct { const ( SizeofIfMsghdr = 0x70 + SizeofIfMsghdr2 = 0xa0 SizeofIfData = 0x60 + SizeofIfData64 = 0x80 SizeofIfaMsghdr = 0x14 SizeofIfmaMsghdr = 0x10 SizeofIfmaMsghdr2 = 0x14 SizeofRtMsghdr = 0x5c + SizeofRtMsghdr2 = 0x5c SizeofRtMetrics = 0x38 ) @@ -480,6 +483,20 @@ type IfMsghdr struct { Data IfData } +type IfMsghdr2 struct { + Msglen uint16 + Version uint8 + Type uint8 + Addrs int32 + Flags int32 + Index uint16 + Snd_len int32 + Snd_maxlen int32 + Snd_drops int32 + Timer int32 + Data IfData64 +} + type IfData struct { Type uint8 Typelen uint8 @@ -512,6 +529,34 @@ type IfData struct { Reserved2 uint32 } +type IfData64 struct { + Type uint8 + Typelen uint8 + Physical uint8 + Addrlen uint8 + Hdrlen uint8 + Recvquota uint8 + Xmitquota uint8 + Unused1 uint8 + Mtu uint32 + Metric uint32 + Baudrate uint64 + Ipackets uint64 + Ierrors uint64 + Opackets uint64 + Oerrors uint64 + Collisions uint64 + Ibytes uint64 + Obytes uint64 + Imcasts uint64 + Omcasts uint64 + Iqdrops uint64 + Noproto uint64 + Recvtiming uint32 + Xmittiming uint32 + Lastchange Timeval32 +} + type IfaMsghdr struct { Msglen uint16 Version uint8 @@ -557,6 +602,21 @@ type RtMsghdr struct { Rmx RtMetrics } +type RtMsghdr2 struct { + Msglen uint16 + Version uint8 + Type uint8 + Index uint16 + Flags int32 + Addrs int32 + Refcnt int32 + Parentflags int32 + Reserved int32 + Use int32 + Inits uint32 + Rmx RtMetrics +} + type RtMetrics struct { Locks uint32 Mtu uint32 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go index 8daaf3faf..5537148dc 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go @@ -2594,8 +2594,8 @@ type ScmTimestamping struct { SOF_TIMESTAMPING_BIND_PHC = 0x8000 SOF_TIMESTAMPING_OPT_ID_TCP = 0x10000 - SOF_TIMESTAMPING_LAST = 0x10000 - SOF_TIMESTAMPING_MASK = 0x1ffff + SOF_TIMESTAMPING_LAST = 0x20000 + SOF_TIMESTAMPING_MASK = 0x3ffff SCM_TSTAMP_SND = 0x0 SCM_TSTAMP_SCHED = 0x1 @@ -3541,7 +3541,7 @@ type Nhmsg struct { type NexthopGrp struct { Id uint32 Weight uint8 - Resvd1 uint8 + High uint8 Resvd2 uint16 } @@ -3802,7 +3802,7 @@ type PPSKTime struct { ETHTOOL_MSG_PSE_GET = 0x24 ETHTOOL_MSG_PSE_SET = 0x25 ETHTOOL_MSG_RSS_GET = 0x26 - ETHTOOL_MSG_USER_MAX = 0x2c + ETHTOOL_MSG_USER_MAX = 0x2d ETHTOOL_MSG_KERNEL_NONE = 0x0 ETHTOOL_MSG_STRSET_GET_REPLY = 0x1 ETHTOOL_MSG_LINKINFO_GET_REPLY = 0x2 @@ -3842,7 +3842,7 @@ type PPSKTime struct { ETHTOOL_MSG_MODULE_NTF = 0x24 ETHTOOL_MSG_PSE_GET_REPLY = 0x25 ETHTOOL_MSG_RSS_GET_REPLY = 0x26 - ETHTOOL_MSG_KERNEL_MAX = 0x2c + ETHTOOL_MSG_KERNEL_MAX = 0x2e ETHTOOL_FLAG_COMPACT_BITSETS = 0x1 ETHTOOL_FLAG_OMIT_REPLY = 0x2 ETHTOOL_FLAG_STATS = 0x4 @@ -3850,7 +3850,7 @@ type PPSKTime struct { ETHTOOL_A_HEADER_DEV_INDEX = 0x1 ETHTOOL_A_HEADER_DEV_NAME = 0x2 ETHTOOL_A_HEADER_FLAGS = 0x3 - ETHTOOL_A_HEADER_MAX = 0x3 + ETHTOOL_A_HEADER_MAX = 0x4 ETHTOOL_A_BITSET_BIT_UNSPEC = 0x0 ETHTOOL_A_BITSET_BIT_INDEX = 0x1 ETHTOOL_A_BITSET_BIT_NAME = 0x2 @@ -4031,11 +4031,11 @@ type PPSKTime struct { ETHTOOL_A_CABLE_RESULT_UNSPEC = 0x0 ETHTOOL_A_CABLE_RESULT_PAIR = 0x1 ETHTOOL_A_CABLE_RESULT_CODE = 0x2 - ETHTOOL_A_CABLE_RESULT_MAX = 0x2 + ETHTOOL_A_CABLE_RESULT_MAX = 0x3 ETHTOOL_A_CABLE_FAULT_LENGTH_UNSPEC = 0x0 ETHTOOL_A_CABLE_FAULT_LENGTH_PAIR = 0x1 ETHTOOL_A_CABLE_FAULT_LENGTH_CM = 0x2 - ETHTOOL_A_CABLE_FAULT_LENGTH_MAX = 0x2 + ETHTOOL_A_CABLE_FAULT_LENGTH_MAX = 0x3 ETHTOOL_A_CABLE_TEST_NTF_STATUS_UNSPEC = 0x0 ETHTOOL_A_CABLE_TEST_NTF_STATUS_STARTED = 0x1 ETHTOOL_A_CABLE_TEST_NTF_STATUS_COMPLETED = 0x2 @@ -4200,7 +4200,8 @@ type HwTstampConfig struct { } PtpSysOffsetExtended struct { Samples uint32 - Rsv [3]uint32 + Clockid int32 + Rsv [2]uint32 Ts [25][3]PtpClockTime } PtpSysOffsetPrecise struct { @@ -4399,6 +4400,7 @@ type HwTstampConfig struct { type LandlockRulesetAttr struct { Access_fs uint64 Access_net uint64 + Scoped uint64 } type LandlockPathBeneathAttr struct { diff --git a/vendor/golang.org/x/sys/windows/syscall_windows.go b/vendor/golang.org/x/sys/windows/syscall_windows.go index 4510bfc3f..4a3254386 100644 --- a/vendor/golang.org/x/sys/windows/syscall_windows.go +++ b/vendor/golang.org/x/sys/windows/syscall_windows.go @@ -168,6 +168,8 @@ func NewCallbackCDecl(fn interface{}) uintptr { //sys CreateNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *SecurityAttributes) (handle Handle, err error) [failretval==InvalidHandle] = CreateNamedPipeW //sys ConnectNamedPipe(pipe Handle, overlapped *Overlapped) (err error) //sys DisconnectNamedPipe(pipe Handle) (err error) +//sys GetNamedPipeClientProcessId(pipe Handle, clientProcessID *uint32) (err error) +//sys GetNamedPipeServerProcessId(pipe Handle, serverProcessID *uint32) (err error) //sys GetNamedPipeInfo(pipe Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) //sys GetNamedPipeHandleState(pipe Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW //sys SetNamedPipeHandleState(pipe Handle, state *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32) (err error) = SetNamedPipeHandleState diff --git a/vendor/golang.org/x/sys/windows/types_windows.go b/vendor/golang.org/x/sys/windows/types_windows.go index 51311e205..9d138de5f 100644 --- a/vendor/golang.org/x/sys/windows/types_windows.go +++ b/vendor/golang.org/x/sys/windows/types_windows.go @@ -176,6 +176,7 @@ WAIT_FAILED = 0xFFFFFFFF // Access rights for process. + PROCESS_ALL_ACCESS = 0xFFFF PROCESS_CREATE_PROCESS = 0x0080 PROCESS_CREATE_THREAD = 0x0002 PROCESS_DUP_HANDLE = 0x0040 diff --git a/vendor/golang.org/x/sys/windows/zsyscall_windows.go b/vendor/golang.org/x/sys/windows/zsyscall_windows.go index 6f5252880..01c0716c2 100644 --- a/vendor/golang.org/x/sys/windows/zsyscall_windows.go +++ b/vendor/golang.org/x/sys/windows/zsyscall_windows.go @@ -280,8 +280,10 @@ func errnoErr(e syscall.Errno) error { procGetMaximumProcessorCount = modkernel32.NewProc("GetMaximumProcessorCount") procGetModuleFileNameW = modkernel32.NewProc("GetModuleFileNameW") procGetModuleHandleExW = modkernel32.NewProc("GetModuleHandleExW") + procGetNamedPipeClientProcessId = modkernel32.NewProc("GetNamedPipeClientProcessId") procGetNamedPipeHandleStateW = modkernel32.NewProc("GetNamedPipeHandleStateW") procGetNamedPipeInfo = modkernel32.NewProc("GetNamedPipeInfo") + procGetNamedPipeServerProcessId = modkernel32.NewProc("GetNamedPipeServerProcessId") procGetOverlappedResult = modkernel32.NewProc("GetOverlappedResult") procGetPriorityClass = modkernel32.NewProc("GetPriorityClass") procGetProcAddress = modkernel32.NewProc("GetProcAddress") @@ -1612,7 +1614,7 @@ func DwmSetWindowAttribute(hwnd HWND, attribute uint32, value unsafe.Pointer, si } func CancelMibChangeNotify2(notificationHandle Handle) (errcode error) { - r0, _, _ := syscall.SyscallN(procCancelMibChangeNotify2.Addr(), uintptr(notificationHandle)) + r0, _, _ := syscall.Syscall(procCancelMibChangeNotify2.Addr(), 1, uintptr(notificationHandle), 0, 0) if r0 != 0 { errcode = syscall.Errno(r0) } @@ -1652,7 +1654,7 @@ func GetIfEntry(pIfRow *MibIfRow) (errcode error) { } func GetIfEntry2Ex(level uint32, row *MibIfRow2) (errcode error) { - r0, _, _ := syscall.SyscallN(procGetIfEntry2Ex.Addr(), uintptr(level), uintptr(unsafe.Pointer(row))) + r0, _, _ := syscall.Syscall(procGetIfEntry2Ex.Addr(), 2, uintptr(level), uintptr(unsafe.Pointer(row)), 0) if r0 != 0 { errcode = syscall.Errno(r0) } @@ -1660,7 +1662,7 @@ func GetIfEntry2Ex(level uint32, row *MibIfRow2) (errcode error) { } func GetUnicastIpAddressEntry(row *MibUnicastIpAddressRow) (errcode error) { - r0, _, _ := syscall.SyscallN(procGetUnicastIpAddressEntry.Addr(), uintptr(unsafe.Pointer(row))) + r0, _, _ := syscall.Syscall(procGetUnicastIpAddressEntry.Addr(), 1, uintptr(unsafe.Pointer(row)), 0, 0) if r0 != 0 { errcode = syscall.Errno(r0) } @@ -1672,7 +1674,7 @@ func NotifyIpInterfaceChange(family uint16, callback uintptr, callerContext unsa if initialNotification { _p0 = 1 } - r0, _, _ := syscall.SyscallN(procNotifyIpInterfaceChange.Addr(), uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle))) + r0, _, _ := syscall.Syscall6(procNotifyIpInterfaceChange.Addr(), 5, uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle)), 0) if r0 != 0 { errcode = syscall.Errno(r0) } @@ -1684,7 +1686,7 @@ func NotifyUnicastIpAddressChange(family uint16, callback uintptr, callerContext if initialNotification { _p0 = 1 } - r0, _, _ := syscall.SyscallN(procNotifyUnicastIpAddressChange.Addr(), uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle))) + r0, _, _ := syscall.Syscall6(procNotifyUnicastIpAddressChange.Addr(), 5, uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle)), 0) if r0 != 0 { errcode = syscall.Errno(r0) } @@ -2446,6 +2448,14 @@ func GetModuleHandleEx(flags uint32, moduleName *uint16, module *Handle) (err er return } +func GetNamedPipeClientProcessId(pipe Handle, clientProcessID *uint32) (err error) { + r1, _, e1 := syscall.Syscall(procGetNamedPipeClientProcessId.Addr(), 2, uintptr(pipe), uintptr(unsafe.Pointer(clientProcessID)), 0) + if r1 == 0 { + err = errnoErr(e1) + } + return +} + func GetNamedPipeHandleState(pipe Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) { r1, _, e1 := syscall.Syscall9(procGetNamedPipeHandleStateW.Addr(), 7, uintptr(pipe), uintptr(unsafe.Pointer(state)), uintptr(unsafe.Pointer(curInstances)), uintptr(unsafe.Pointer(maxCollectionCount)), uintptr(unsafe.Pointer(collectDataTimeout)), uintptr(unsafe.Pointer(userName)), uintptr(maxUserNameSize), 0, 0) if r1 == 0 { @@ -2462,6 +2472,14 @@ func GetNamedPipeInfo(pipe Handle, flags *uint32, outSize *uint32, inSize *uint3 return } +func GetNamedPipeServerProcessId(pipe Handle, serverProcessID *uint32) (err error) { + r1, _, e1 := syscall.Syscall(procGetNamedPipeServerProcessId.Addr(), 2, uintptr(pipe), uintptr(unsafe.Pointer(serverProcessID)), 0) + if r1 == 0 { + err = errnoErr(e1) + } + return +} + func GetOverlappedResult(handle Handle, overlapped *Overlapped, done *uint32, wait bool) (err error) { var _p0 uint32 if wait { diff --git a/vendor/modernc.org/sqlite/CONTRIBUTORS b/vendor/modernc.org/sqlite/CONTRIBUTORS index 853cbb56e..e9ea3aff5 100644 --- a/vendor/modernc.org/sqlite/CONTRIBUTORS +++ b/vendor/modernc.org/sqlite/CONTRIBUTORS @@ -37,3 +37,4 @@ Steffen Butzer Toni Spets W. Michael Petullo Yaacov Akiba Slama +Prathyush PV diff --git a/vendor/modernc.org/sqlite/Makefile b/vendor/modernc.org/sqlite/Makefile index 67a56d389..8b7472ddd 100644 --- a/vendor/modernc.org/sqlite/Makefile +++ b/vendor/modernc.org/sqlite/Makefile @@ -57,7 +57,7 @@ clean: edit: @touch log - @if [ -f "Session.vim" ]; then novim -S & else novim -p Makefile go.mod builder.json all_test.go vendor_libsqlite3.go & fi + @if [ -f "Session.vim" ]; then gvim -S & else gvim -p Makefile go.mod builder.json all_test.go vendor_libsqlite3.go & fi editor: gofmt -l -s -w . 2>&1 | tee log-editor diff --git a/vendor/modernc.org/sqlite/README.md b/vendor/modernc.org/sqlite/README.md index f47ad3240..8f4acdb3b 100644 --- a/vendor/modernc.org/sqlite/README.md +++ b/vendor/modernc.org/sqlite/README.md @@ -1,81 +1,7 @@ -# sqlite +![logo](logo.png) -Package sqlite is a cgo-free port of SQLite. Although you could see mattn's driver (`github.com/mattn/go-sqlite3`) in go.mod file, we import it for tests only. +[![Go Reference](https://pkg.go.dev/badge/modernc.org/sqlite.svg)](https://pkg.go.dev/modernc.org/sqlite) -SQLite is an in-process implementation of a self-contained, serverless, -zero-configuration, transactional SQL database engine. - -## Thanks - -This project is sponsored by Schleibinger Geräte Teubert u. Greim GmbH by -allowing one of the maintainers to work on it also in office hours. - -## Installation - - $ go get modernc.org/sqlite - -## Documentation - -[pkg.go.dev/modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite) - -## Builders - -[modern-c.appspot.com/-/builder/?importpath=modernc.org%2fsqlite](https://modern-c.appspot.com/-/builder/?importpath=modernc.org%2fsqlite) - -## Speedtest1 - -Numbers for the pure Go version were produced by - - ~/src/modernc.org/sqlite/speedtest1$ go build && ./speedtest1 - -Numbers for the pure C version were produced by - - ~/src/modernc.org/sqlite/testdata/sqlite-src-3410200/test$ gcc speedtest1.c ../../sqlite-amalgamation-3410200/sqlite3.c -lpthread -ldl && ./a.out - -The results are from Go version 1.20.4 and GCC version 10.2.1 on a -Linux/amd64 machine, CPU: AMD Ryzen 9 3900X 12-Core Processor × 24, 128GB -RAM. Shown are the best of 3 runs. - - Go C - - -- Speedtest1 for SQLite 3.41.2 2023-03-22 11:56:21 0d1fc92f94cb6b76bffe3ec34d69 -- Speedtest1 for SQLite 3.41.2 2023-03-22 11:56:21 0d1fc92f94cb6b76bffe3ec34d69 - 100 - 50000 INSERTs into table with no index...................... 0.071s 100 - 50000 INSERTs into table with no index...................... 0.077s - 110 - 50000 ordered INSERTS with one index/PK..................... 0.114s 110 - 50000 ordered INSERTS with one index/PK..................... 0.082s - 120 - 50000 unordered INSERTS with one index/PK................... 0.137s 120 - 50000 unordered INSERTS with one index/PK................... 0.099s - 130 - 25 SELECTS, numeric BETWEEN, unindexed...................... 0.083s 130 - 25 SELECTS, numeric BETWEEN, unindexed...................... 0.091s - 140 - 10 SELECTS, LIKE, unindexed................................. 0.210s 140 - 10 SELECTS, LIKE, unindexed................................. 0.120s - 142 - 10 SELECTS w/ORDER BY, unindexed............................ 0.276s 142 - 10 SELECTS w/ORDER BY, unindexed............................ 0.182s - 145 - 10 SELECTS w/ORDER BY and LIMIT, unindexed.................. 0.183s 145 - 10 SELECTS w/ORDER BY and LIMIT, unindexed.................. 0.099s - 150 - CREATE INDEX five times..................................... 0.172s 150 - CREATE INDEX five times..................................... 0.127s - 160 - 10000 SELECTS, numeric BETWEEN, indexed..................... 0.080s 160 - 10000 SELECTS, numeric BETWEEN, indexed..................... 0.078s - 161 - 10000 SELECTS, numeric BETWEEN, PK.......................... 0.080s 161 - 10000 SELECTS, numeric BETWEEN, PK.......................... 0.078s - 170 - 10000 SELECTS, text BETWEEN, indexed........................ 0.187s 170 - 10000 SELECTS, text BETWEEN, indexed........................ 0.169s - 180 - 50000 INSERTS with three indexes............................ 0.196s 180 - 50000 INSERTS with three indexes............................ 0.154s - 190 - DELETE and REFILL one table................................. 0.200s 190 - DELETE and REFILL one table................................. 0.155s - 200 - VACUUM...................................................... 0.180s 200 - VACUUM...................................................... 0.142s - 210 - ALTER TABLE ADD COLUMN, and query........................... 0.004s 210 - ALTER TABLE ADD COLUMN, and query........................... 0.005s - 230 - 10000 UPDATES, numeric BETWEEN, indexed..................... 0.093s 230 - 10000 UPDATES, numeric BETWEEN, indexed..................... 0.080s - 240 - 50000 UPDATES of individual rows............................ 0.153s 240 - 50000 UPDATES of individual rows............................ 0.137s - 250 - One big UPDATE of the whole 50000-row table................. 0.024s 250 - One big UPDATE of the whole 50000-row table................. 0.019s - 260 - Query added column after filling............................ 0.004s 260 - Query added column after filling............................ 0.005s - 270 - 10000 DELETEs, numeric BETWEEN, indexed..................... 0.278s 270 - 10000 DELETEs, numeric BETWEEN, indexed..................... 0.263s - 280 - 50000 DELETEs of individual rows............................ 0.188s 280 - 50000 DELETEs of individual rows............................ 0.180s - 290 - Refill two 50000-row tables using REPLACE................... 0.411s 290 - Refill two 50000-row tables using REPLACE................... 0.359s - 300 - Refill a 50000-row table using (b&1)==(a&1)................. 0.175s 300 - Refill a 50000-row table using (b&1)==(a&1)................. 0.151s - 310 - 10000 four-ways joins....................................... 0.427s 310 - 10000 four-ways joins....................................... 0.365s - 320 - subquery in result set...................................... 0.440s 320 - subquery in result set...................................... 0.521s - 400 - 70000 REPLACE ops on an IPK................................. 0.125s 400 - 70000 REPLACE ops on an IPK................................. 0.106s - 410 - 70000 SELECTS on an IPK..................................... 0.081s 410 - 70000 SELECTS on an IPK..................................... 0.078s - 500 - 70000 REPLACE on TEXT PK.................................... 0.174s 500 - 70000 REPLACE on TEXT PK.................................... 0.116s - 510 - 70000 SELECTS on a TEXT PK.................................. 0.153s 510 - 70000 SELECTS on a TEXT PK.................................. 0.117s - 520 - 70000 SELECT DISTINCT....................................... 0.083s 520 - 70000 SELECT DISTINCT....................................... 0.067s - 980 - PRAGMA integrity_check...................................... 0.436s 980 - PRAGMA integrity_check...................................... 0.377s - 990 - ANALYZE..................................................... 0.107s 990 - ANALYZE..................................................... 0.038s - TOTAL....................................................... 5.525s TOTAL....................................................... 4.637s - -This particular test executes 16.1% faster in the C version. - -## Troubleshooting - -* Q: **How can I write to a database concurrently without getting the `database is locked` error (or `SQLITE_BUSY`)?** - * A: You can't. The C sqlite implementation does not allow concurrent writes, and this libary does not modify that behaviour. You can, however, use [DB.SetMaxOpenConns(1)](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns) so that only 1 connection is ever used by the `DB`, allowing concurrent access to DB without making the writes concurrent. More information on issues [#65](https://gitlab.com/cznic/sqlite/-/issues/65) and [#106](https://gitlab.com/cznic/sqlite/-/issues/106). +[![LiberaPay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/jnml/donate) +[![receives](https://img.shields.io/liberapay/receives/jnml.svg?logo=liberapay)](https://liberapay.com/jnml/donate) +[![patrons](https://img.shields.io/liberapay/patrons/jnml.svg?logo=liberapay)](https://liberapay.com/jnml/donate) diff --git a/vendor/modernc.org/sqlite/doc.go b/vendor/modernc.org/sqlite/doc.go index c09d18590..02820b86b 100644 --- a/vendor/modernc.org/sqlite/doc.go +++ b/vendor/modernc.org/sqlite/doc.go @@ -8,6 +8,14 @@ // SQLite is an in-process implementation of a self-contained, serverless, // zero-configuration, transactional SQL database engine. // +// # Fragile modernc.org/libc dependency +// +// When you import this package you should use in your go.mod file the exact +// same version of modernc.org/libc as seen in the go.mod file of this +// repository. +// +// See the discussion at https://gitlab.com/cznic/sqlite/-/issues/177 for more details. +// // # Thanks // // This project is sponsored by Schleibinger Geräte Teubert u. Greim GmbH by @@ -41,175 +49,98 @@ // // https://modern-c.appspot.com/-/builder/?importpath=modernc.org%2fsqlite // -// # Fragile modernc.org/libc dependency -// -// When you import this package you should use in your go.mod file the exact -// same version of modernc.org/libc as seen in the go.mod file of this -// repository. -// -// See the discussion at https://gitlab.com/cznic/sqlite/-/issues/177 for more details. -// // # Changelog // -// 2024-07-22: v1.31.0 +// - 2024-11-16 v1.34.0: Implement ResetSession and IsValid methods in connection // -// Support windows/386. +// - 2024-07-22 v1.31.0: Support windows/386. // -// 2024-06-04: v1.30.0 +// - 2024-06-04 v1.30.0: Upgrade to SQLite 3.46.0, release notes at +// https://sqlite.org/releaselog/3_46_0.html. // -// Upgrade to SQLite 3.46.0, release notes at https://sqlite.org/releaselog/3_46_0.html. +// - 2024-02-13 v1.29.0: Upgrade to SQLite 3.45.1, release notes at +// https://sqlite.org/releaselog/3_45_1.html. // -// 2024-02-13: v1.29.0 +// - 2023-12-14: v1.28.0: Add (*Driver).RegisterConnectionHook, +// ConnectionHookFn, ExecQuerierContext, RegisterConnectionHook. // -// Upgrade to SQLite 3.45.1, release notes at https://sqlite.org/releaselog/3_45_1.html. +// - 2023-08-03 v1.25.0: enable SQLITE_ENABLE_DBSTAT_VTAB. // -// 2023-12-14 v1.28.0: +// - 2023-07-11 v1.24.0: Add +// (*conn).{Serialize,Deserialize,NewBackup,NewRestore} methods, add Backup +// type. // -// (*Driver).RegisterConnectionHook: added -// ConnectionHookFn: added -// ExecQuerierContext: added -// RegisterConnectionHook: added +// - 2023-06-01 v1.23.0: Allow registering aggregate functions. // -// 2023-08-03 v1.25.0: enable SQLITE_ENABLE_DBSTAT_VTAB. +// - 2023-04-22 v1.22.0: Support linux/s390x. // -// 2023-07-11 v1.24.0: +// - 2023-02-23 v1.21.0: Upgrade to SQLite 3.41.0, release notes at +// https://sqlite.org/releaselog/3_41_0.html. // -// Add (*conn).{Serialize,Deserialize,NewBackup,NewRestore} methods, add Backup type. +// - 2022-11-28 v1.20.0: Support linux/ppc64le. // -// 2023-06-01 v1.23.0: +// - 2022-09-16 v1.19.0: Support frebsd/arm64. // -// Allow registering aggregate functions. +// - 2022-07-26 v1.18.0: Add support for Go fs.FS based SQLite virtual +// filesystems, see function New in modernc.org/sqlite/vfs and/or TestVFS in +// all_test.go // -// 2023-04-22 v1.22.0: +// - 2022-04-24 v1.17.0: Support windows/arm64. // -// Support linux/s390x. +// - 2022-04-04 v1.16.0: Support scalar application defined functions written +// in Go. See https://www.sqlite.org/appfunc.html // -// 2023-02-23 v1.21.0: +// - 2022-03-13 v1.15.0: Support linux/riscv64. // -// Upgrade to SQLite 3.41.0, release notes at https://sqlite.org/releaselog/3_41_0.html. +// - 2021-11-13 v1.14.0: Support windows/amd64. This target had previously +// only experimental status because of a now resolved memory leak. // -// 2022-11-28 v1.20.0 +// - 2021-09-07 v1.13.0: Support freebsd/amd64. // -// Support linux/ppc64le. +// - 2021-06-23 v1.11.0: Upgrade to use sqlite 3.36.0, release notes at +// https://www.sqlite.org/releaselog/3_36_0.html. // -// 2022-09-16 v1.19.0: +// - 2021-05-06 v1.10.6: Fixes a memory corruption issue +// (https://gitlab.com/cznic/sqlite/-/issues/53). Versions since v1.8.6 were +// affected and should be updated to v1.10.6. // -// Support frebsd/arm64. +// - 2021-03-14 v1.10.0: Update to use sqlite 3.35.0, release notes at +// https://www.sqlite.org/releaselog/3_35_0.html. // -// 2022-07-26 v1.18.0: +// - 2021-03-11 v1.9.0: Support darwin/arm64. // -// Adds support for Go fs.FS based SQLite virtual filesystems, see function New -// in modernc.org/sqlite/vfs and/or TestVFS in all_test.go +// - 2021-01-08 v1.8.0: Support darwin/amd64. // -// 2022-04-24 v1.17.0: +// - 2020-09-13 v1.7.0: Support linux/arm and linux/arm64. // -// Support windows/arm64. +// - 2020-09-08 v1.6.0: Support linux/386. // -// 2022-04-04 v1.16.0: +// - 2020-09-03 v1.5.0: This project is now completely CGo-free, including +// the Tcl tests. // -// Support scalar application defined functions written in Go. +// - 2020-08-26 v1.4.0: First stable release for linux/amd64. The +// database/sql driver and its tests are CGo free. Tests of the translated +// sqlite3.c library still require CGo. // -// https://www.sqlite.org/appfunc.html +// - 2020-07-26 v1.4.0-beta1: The project has reached beta status while +// supporting linux/amd64 only at the moment. The 'extraquick' Tcl testsuite +// reports // -// 2022-03-13 v1.15.0: +// - 2019-12-28 v1.2.0-alpha.3: Third alpha fixes issue #19. // -// Support linux/riscv64. +// - 2019-12-26 v1.1.0-alpha.2: Second alpha release adds support for +// accessing a database concurrently by multiple goroutines and/or processes. +// v1.1.0 is now considered feature-complete. Next planed release should be a +// beta with a proper test suite. // -// 2021-11-13 v1.14.0: +// - 2019-12-18 v1.1.0-alpha.1: First alpha release using the new cc/v3, +// gocc, qbe toolchain. Some primitive tests pass on linux_{amd64,386}. Not +// yet safe for concurrent access by multiple goroutines. Next alpha release +// is planed to arrive before the end of this year. // -// Support windows/amd64. This target had previously only experimental status -// because of a now resolved memory leak. +// - 2017-06-10: Windows/Intel no more uses the VM (thanks Steffen Butzer). // -// 2021-09-07 v1.13.0: -// -// Support freebsd/amd64. -// -// 2021-06-23 v1.11.0: -// -// Upgrade to use sqlite 3.36.0, release notes at https://www.sqlite.org/releaselog/3_36_0.html. -// -// 2021-05-06 v1.10.6: -// -// Fixes a memory corruption issue -// (https://gitlab.com/cznic/sqlite/-/issues/53). Versions since v1.8.6 were -// affected and should be updated to v1.10.6. -// -// 2021-03-14 v1.10.0: -// -// Update to use sqlite 3.35.0, release notes at https://www.sqlite.org/releaselog/3_35_0.html. -// -// 2021-03-11 v1.9.0: -// -// Support darwin/arm64. -// -// 2021-01-08 v1.8.0: -// -// Support darwin/amd64. -// -// 2020-09-13 v1.7.0: -// -// Support linux/arm and linux/arm64. -// -// 2020-09-08 v1.6.0: -// -// Support linux/386. -// -// 2020-09-03 v1.5.0: -// -// This project is now completely CGo-free, including the Tcl tests. -// -// 2020-08-26 v1.4.0: -// -// First stable release for linux/amd64. The database/sql driver and its tests -// are CGo free. Tests of the translated sqlite3.c library still require CGo. -// -// $ make full -// -// ... -// -// SQLite 2020-08-14 13:23:32 fca8dc8b578f215a969cd899336378966156154710873e68b3d9ac5881b0ff3f -// 0 errors out of 928271 tests on 3900x Linux 64-bit little-endian -// WARNING: Multi-threaded tests skipped: Linked against a non-threadsafe Tcl build -// All memory allocations freed - no leaks -// Maximum memory usage: 9156360 bytes -// Current memory usage: 0 bytes -// Number of malloc() : -1 calls -// --- PASS: TestTclTest (1785.04s) -// PASS -// ok modernc.org/sqlite 1785.041s -// $ -// -// 2020-07-26 v1.4.0-beta1: -// -// The project has reached beta status while supporting linux/amd64 only at the -// moment. The 'extraquick' Tcl testsuite reports -// -// 630 errors out of 200177 tests on Linux 64-bit little-endian -// -// and some memory leaks -// -// Unfreed memory: 698816 bytes in 322 allocations -// -// 2019-12-28 v1.2.0-alpha.3: Third alpha fixes issue #19. -// -// It also bumps the minor version as the repository was wrongly already tagged -// with v1.1.0 before. Even though the tag was deleted there are proxies that -// cached that tag. Thanks /u/garaktailor for detecting the problem and -// suggesting this solution. -// -// 2019-12-26 v1.1.0-alpha.2: Second alpha release adds support for accessing a -// database concurrently by multiple goroutines and/or processes. v1.1.0 is now -// considered feature-complete. Next planed release should be a beta with a -// proper test suite. -// -// 2019-12-18 v1.1.0-alpha.1: First alpha release using the new cc/v3, gocc, -// qbe toolchain. Some primitive tests pass on linux_{amd64,386}. Not yet safe -// for concurrent access by multiple goroutines. Next alpha release is planed -// to arrive before the end of this year. -// -// 2017-06-10 Windows/Intel no more uses the VM (thanks Steffen Butzer). -// -// 2017-06-05 Linux/Intel no more uses the VM (cznic/virtual). +// - 2017-06-05 Linux/Intel no more uses the VM (cznic/virtual). // // # Connecting to a database // diff --git a/vendor/modernc.org/sqlite/lib/sqlite_darwin_amd64.go b/vendor/modernc.org/sqlite/lib/sqlite_darwin_amd64.go index 827d9b06d..e856b001a 100644 --- a/vendor/modernc.org/sqlite/lib/sqlite_darwin_amd64.go +++ b/vendor/modernc.org/sqlite/lib/sqlite_darwin_amd64.go @@ -230897,4 +230897,3 @@ func __ccgo_fp(f interface{}) uintptr { type Sqlite3_module = sqlite3_module type Sqlite3_vtab = sqlite3_vtab type Sqlite3_vtab_cursor = sqlite3_vtab_cursor - diff --git a/vendor/modernc.org/sqlite/lib/sqlite_darwin_arm64.go b/vendor/modernc.org/sqlite/lib/sqlite_darwin_arm64.go index 97211c806..78f2aeb3e 100644 --- a/vendor/modernc.org/sqlite/lib/sqlite_darwin_arm64.go +++ b/vendor/modernc.org/sqlite/lib/sqlite_darwin_arm64.go @@ -230452,4 +230452,3 @@ func __ccgo_fp(f interface{}) uintptr { type Sqlite3_module = sqlite3_module type Sqlite3_vtab = sqlite3_vtab type Sqlite3_vtab_cursor = sqlite3_vtab_cursor - diff --git a/vendor/modernc.org/sqlite/lib/sqlite_freebsd_amd64.go b/vendor/modernc.org/sqlite/lib/sqlite_freebsd_amd64.go index 5b7a72932..695a5b8c9 100644 --- a/vendor/modernc.org/sqlite/lib/sqlite_freebsd_amd64.go +++ b/vendor/modernc.org/sqlite/lib/sqlite_freebsd_amd64.go @@ -225512,4 +225512,3 @@ func __ccgo_fp(f interface{}) uintptr { type Sqlite3_module = sqlite3_module type Sqlite3_vtab = sqlite3_vtab type Sqlite3_vtab_cursor = sqlite3_vtab_cursor - diff --git a/vendor/modernc.org/sqlite/lib/sqlite_freebsd_arm64.go b/vendor/modernc.org/sqlite/lib/sqlite_freebsd_arm64.go index ce9ed9ccb..1883ce6b3 100644 --- a/vendor/modernc.org/sqlite/lib/sqlite_freebsd_arm64.go +++ b/vendor/modernc.org/sqlite/lib/sqlite_freebsd_arm64.go @@ -225523,4 +225523,3 @@ func __ccgo_fp(f interface{}) uintptr { type Sqlite3_module = sqlite3_module type Sqlite3_vtab = sqlite3_vtab type Sqlite3_vtab_cursor = sqlite3_vtab_cursor - diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_386.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_386.go index a454f2371..a099d0369 100644 --- a/vendor/modernc.org/sqlite/lib/sqlite_linux_386.go +++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_386.go @@ -231929,4 +231929,3 @@ func __ccgo_fp(f interface{}) uintptr { type Sqlite3_module = sqlite3_module type Sqlite3_vtab = sqlite3_vtab type Sqlite3_vtab_cursor = sqlite3_vtab_cursor - diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_amd64.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_amd64.go index 172b9f296..e52d6a899 100644 --- a/vendor/modernc.org/sqlite/lib/sqlite_linux_amd64.go +++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_amd64.go @@ -225728,4 +225728,3 @@ func __ccgo_fp(f interface{}) uintptr { type Sqlite3_module = sqlite3_module type Sqlite3_vtab = sqlite3_vtab type Sqlite3_vtab_cursor = sqlite3_vtab_cursor - diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_arm.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_arm.go index 49eb1ec10..964a2676c 100644 --- a/vendor/modernc.org/sqlite/lib/sqlite_linux_arm.go +++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_arm.go @@ -232454,4 +232454,3 @@ func __ccgo_fp(f interface{}) uintptr { type Sqlite3_module = sqlite3_module type Sqlite3_vtab = sqlite3_vtab type Sqlite3_vtab_cursor = sqlite3_vtab_cursor - diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_arm64.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_arm64.go index 57386d724..15ba9a251 100644 --- a/vendor/modernc.org/sqlite/lib/sqlite_linux_arm64.go +++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_arm64.go @@ -231974,4 +231974,3 @@ func __ccgo_fp(f interface{}) uintptr { type Sqlite3_module = sqlite3_module type Sqlite3_vtab = sqlite3_vtab type Sqlite3_vtab_cursor = sqlite3_vtab_cursor - diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_loong64.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_loong64.go index 2888a1c36..83675cadf 100644 --- a/vendor/modernc.org/sqlite/lib/sqlite_linux_loong64.go +++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_loong64.go @@ -225817,4 +225817,3 @@ func __ccgo_fp(f interface{}) uintptr { type Sqlite3_module = sqlite3_module type Sqlite3_vtab = sqlite3_vtab type Sqlite3_vtab_cursor = sqlite3_vtab_cursor - diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_ppc64le.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_ppc64le.go index e4915c3b1..a851859b8 100644 --- a/vendor/modernc.org/sqlite/lib/sqlite_linux_ppc64le.go +++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_ppc64le.go @@ -231955,4 +231955,3 @@ func __ccgo_fp(f interface{}) uintptr { type Sqlite3_module = sqlite3_module type Sqlite3_vtab = sqlite3_vtab type Sqlite3_vtab_cursor = sqlite3_vtab_cursor - diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_riscv64.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_riscv64.go index 3aedd92be..dd2bf77e1 100644 --- a/vendor/modernc.org/sqlite/lib/sqlite_linux_riscv64.go +++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_riscv64.go @@ -231918,4 +231918,3 @@ func __ccgo_fp(f interface{}) uintptr { type Sqlite3_module = sqlite3_module type Sqlite3_vtab = sqlite3_vtab type Sqlite3_vtab_cursor = sqlite3_vtab_cursor - diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_s390x.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_s390x.go index 2c276e6c3..81f1426a6 100644 --- a/vendor/modernc.org/sqlite/lib/sqlite_linux_s390x.go +++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_s390x.go @@ -231852,4 +231852,3 @@ func __ccgo_fp(f interface{}) uintptr { type Sqlite3_module = sqlite3_module type Sqlite3_vtab = sqlite3_vtab type Sqlite3_vtab_cursor = sqlite3_vtab_cursor - diff --git a/vendor/modernc.org/sqlite/lib/sqlite_windows.go b/vendor/modernc.org/sqlite/lib/sqlite_windows.go index 49ff6d6d0..6afacb327 100644 --- a/vendor/modernc.org/sqlite/lib/sqlite_windows.go +++ b/vendor/modernc.org/sqlite/lib/sqlite_windows.go @@ -94277,7 +94277,7 @@ func Xsqlite3_str_vappendf(tls *libc.TLS, pAccum uintptr, fmt uintptr, ap Tva_li _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _ = bArgList, base, bufpt, c, cThousand, ch, ch1, cset, done, e2, escarg, exp, flag_alternateform, flag_altform2, flag_dp, flag_leftjustify, flag_long, flag_prefix, flag_rtz, flag_zeropad, i, i1, iRound, idx, ii, infop, isnull, ix, j, j1, k, length, longvalue, n, n1, nCopyBytes, nOut, nPad, nPrior, needQuote, nn, pArgList, pExpr, pItem, pSel, pToken, pre, precision, prefix, px, q, realvalue, szBufNeeded, v, width, wx, x, x1, xtype, z, zExtra, zOut, v10, v100, v101, v102, v103, v104, v106, v107, v108, v109, v11, v110, v111, v12, v14, v15, v16, v17, v18, v19, v2, v20, v21, v22, v23, v24, v3, v4, v45, v46, v47, v48, v49, v5, v51, v52, v54, v55, v56, v57, v58, v59, v6, v60, v61, v62, v64, v65, v66, v67, v68, v7, v70, v71, v72, v73, v74, v75, v76, v77, v78, v79, v8, v80, v81, v82, v83, v85, v86, v87, v88, v89, v9, v90, v91, v93, v94, v96, v97, v98, p92 /* Thousands separator for %d and %u */ xtype = uint8(etINVALID) /* Size of the rendering buffer */ zExtra = uintptr(0) /* True if trailing zeros should be removed */ - pArgList = uintptr(0) /* Conversion buffer */ + pArgList = uintptr(0) /* Conversion buffer */ /* pAccum never starts out with an empty buffer that was obtained from ** malloc(). This precondition is required by the mprintf("%z...") ** optimization. */ @@ -97796,14 +97796,14 @@ func _sqlite3AtoF(tls *libc.TLS, z uintptr, pResult uintptr, length int32, enc T var _ /* rr at bp+0 */ [2]float64 _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _ = d, e, eType, eValid, esign, i, incr, nDigit, r, s, s2, sign, zEnd, v2, v3, v4 /* sign * significand * (10 ^ (esign * exponent)) */ - sign = int32(1) /* sign of significand */ - s = uint64(0) /* significand */ - d = 0 /* adjust exponent for shifting decimal point */ - esign = int32(1) /* sign of exponent */ - e = 0 /* exponent */ - eValid = int32(1) /* True exponent is either not used or is well-formed */ - nDigit = 0 /* Number of digits processed */ - eType = int32(1) /* 1: pure integer, 2+: fractional -1 or less: bad UTF16 */ + sign = int32(1) /* sign of significand */ + s = uint64(0) /* significand */ + d = 0 /* adjust exponent for shifting decimal point */ + esign = int32(1) /* sign of exponent */ + e = 0 /* exponent */ + eValid = int32(1) /* True exponent is either not used or is well-formed */ + nDigit = 0 /* Number of digits processed */ + eType = int32(1) /* 1: pure integer, 2+: fractional -1 or less: bad UTF16 */ *(*float64)(unsafe.Pointer(pResult)) = float64(0) /* Default return value, in case of an error */ if length == 0 { return 0 @@ -103328,7 +103328,7 @@ func _winOpen(tls *libc.TLS, pVfs uintptr, zName uintptr, id uintptr, flags int3 ** a temporary file. Use this buffer to store the file name in. */ *(*uintptr)(unsafe.Pointer(bp + 8)) = uintptr(0) /* For temporary filename, if necessary. */ - rc = SQLITE_OK /* Function Return Code */ + rc = SQLITE_OK /* Function Return Code */ isExclusive = flags & int32(SQLITE_OPEN_EXCLUSIVE) isDelete = flags & int32(SQLITE_OPEN_DELETEONCLOSE) isCreate = flags & int32(SQLITE_OPEN_CREATE) @@ -111336,7 +111336,7 @@ func _readDbPage(tls *libc.TLS, pPg uintptr) (r int32) { _, _, _, _ = dbFileVers, iOffset, pPager, rc pPager = (*TPgHdr)(unsafe.Pointer(pPg)).FpPager /* Pager object associated with page pPg */ rc = SQLITE_OK /* Return code */ - *(*Tu32)(unsafe.Pointer(bp)) = uint32(0) /* Frame of WAL containing pgno */ + *(*Tu32)(unsafe.Pointer(bp)) = uint32(0) /* Frame of WAL containing pgno */ if (*TPager)(unsafe.Pointer(pPager)).FpWal != uintptr(0) { rc = _sqlite3WalFindFrame(tls, (*TPager)(unsafe.Pointer(pPager)).FpWal, (*TPgHdr)(unsafe.Pointer(pPg)).Fpgno, bp) if rc != 0 { @@ -130461,7 +130461,7 @@ func _btreeCreateTable(tls *libc.TLS, p uintptr, piTable uintptr, createTabFlags var _ /* pgnoRoot at bp+8 */ TPgno var _ /* rc at bp+12 */ int32 _, _ = pBt, ptfFlags - pBt = (*TBtree)(unsafe.Pointer(p)).FpBt /* Page-type flags for the root page of new table */ + pBt = (*TBtree)(unsafe.Pointer(p)).FpBt /* Page-type flags for the root page of new table */ if (*TBtShared)(unsafe.Pointer(pBt)).FautoVacuum != 0 { /* The page to move to. */ /* Creating a new table may probably require moving an existing database ** to make room for the new tables root page. In case this page turns @@ -136679,8 +136679,8 @@ func _sqlite3VdbeMakeReady(tls *libc.TLS, p uintptr, pParse uintptr) { ** opcode array. This extra memory will be reallocated for other elements ** of the prepared statement. */ - n = int32(libc.Uint64FromInt64(24) * uint64((*TVdbe)(unsafe.Pointer(p)).FnOp)) /* Bytes of opcode memory used */ - (*(*TReusableSpace)(unsafe.Pointer(bp + 8))).FpSpace = (*TVdbe)(unsafe.Pointer(p)).FaOp + uintptr(n) /* Unused opcode memory */ + n = int32(libc.Uint64FromInt64(24) * uint64((*TVdbe)(unsafe.Pointer(p)).FnOp)) /* Bytes of opcode memory used */ + (*(*TReusableSpace)(unsafe.Pointer(bp + 8))).FpSpace = (*TVdbe)(unsafe.Pointer(p)).FaOp + uintptr(n) /* Unused opcode memory */ (*(*TReusableSpace)(unsafe.Pointer(bp + 8))).FnFree = int64(((*TParse)(unsafe.Pointer(pParse)).FszOpAlloc - n) & ^libc.Int32FromInt32(7)) /* Bytes of unused memory */ _resolveP2Values(tls, p, bp) libc.SetBitFieldPtr16Uint32(p+200, uint32(libc.BoolUint8((*TParse)(unsafe.Pointer(pParse)).FisMultiWrite != 0 && (*TParse)(unsafe.Pointer(pParse)).FmayAbort != 0)), 5, 0x20) @@ -167288,7 +167288,7 @@ func _statInit(tls *libc.TLS, context uintptr, argc int32, argv uintptr) { nColUp = nCol nKeyCol = Xsqlite3_value_int(tls, *(*uintptr)(unsafe.Pointer(argv + 1*8))) /* Allocate the space required for the StatAccum object */ - n = int32(uint64(136) + uint64(8)*uint64(uint64(nColUp))) /* StatAccum.anDLt */ + n = int32(uint64(136) + uint64(8)*uint64(uint64(nColUp))) /* StatAccum.anDLt */ n = int32(uint64(n) + libc.Uint64FromInt64(8)*uint64(uint64(nColUp))) /* StatAccum.anEq */ if mxSample != 0 { n = int32(uint64(n) + (libc.Uint64FromInt64(8)*uint64(uint64(nColUp)) + libc.Uint64FromInt64(48)*uint64(nCol+mxSample) + libc.Uint64FromInt64(8)*libc.Uint64FromInt32(3)*uint64(uint64(nColUp))*uint64(nCol+mxSample))) @@ -174242,7 +174242,7 @@ func _sqlite3RefillIndex(tls *libc.TLS, pParse uintptr, pIndex uintptr, memRootP func _sqlite3AllocateIndexObject(tls *libc.TLS, db uintptr, nCol Ti16, nExtra int32, ppExtra uintptr) (r uintptr) { var nByte int32 var p, pExtra uintptr - _, _, _ = nByte, p, pExtra /* Bytes of space for Index object + arrays */ + _, _, _ = nByte, p, pExtra /* Bytes of space for Index object + arrays */ nByte = int32((libc.Uint64FromInt64(160)+libc.Uint64FromInt32(7))&uint64(^libc.Int32FromInt32(7)) + (uint64(8)*uint64(uint64(nCol))+uint64(7))&uint64(^libc.Int32FromInt32(7)) + (uint64(2)*uint64(int32(int32(nCol))+libc.Int32FromInt32(1))+uint64(2)*uint64(uint64(nCol))+uint64(1)*uint64(uint64(nCol))+uint64(7))&uint64(^libc.Int32FromInt32(7))) /* Index.aSortOrder */ p = _sqlite3DbMallocZero(tls, db, uint64(nByte+nExtra)) if p != 0 { @@ -183673,7 +183673,7 @@ func _autoIncBegin(tls *libc.TLS, pParse uintptr, iDb int32, pTab uintptr) (r in v3 = pToplevel + 56 *(*int32)(unsafe.Pointer(v3))++ v2 = *(*int32)(unsafe.Pointer(v3)) - (*TAutoincInfo)(unsafe.Pointer(pInfo)).FregCtr = v2 /* Max rowid register */ + (*TAutoincInfo)(unsafe.Pointer(pInfo)).FregCtr = v2 /* Max rowid register */ *(*int32)(unsafe.Pointer(pToplevel + 56)) += int32(2) /* Rowid in sqlite_sequence + orig max val */ } memId = (*TAutoincInfo)(unsafe.Pointer(pInfo)).FregCtr @@ -209650,7 +209650,7 @@ func _sqlite3WhereCodeOneLoopStart(tls *libc.TLS, pParse uintptr, v uintptr, pWI iCovCur = v44 v47 = pParse + 56 *(*int32)(unsafe.Pointer(v47))++ - v46 = *(*int32)(unsafe.Pointer(v47)) /* Cursor used for index scans (if any) */ + v46 = *(*int32)(unsafe.Pointer(v47)) /* Cursor used for index scans (if any) */ regReturn = v46 /* Register used with OP_Gosub */ regRowset = 0 /* Register for RowSet object */ regRowid = 0 /* Register holding rowid */ @@ -232092,14 +232092,14 @@ func _yy_reduce(tls *libc.TLS, yypParser uintptr, yyruleno uint32, yyLookahead i *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*24 + 8)) = _sqlite3SrcListAppend(tls, pParse, uintptr(0), yymsp+uintptr(-libc.Int32FromInt32(2))*24+8, yymsp+8) /*A-overwrites-X*/ goto _346 _138: - ; /* xfullname ::= nm DOT nm AS nm */ + ; /* xfullname ::= nm DOT nm AS nm */ *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(4))*24 + 8)) = _sqlite3SrcListAppend(tls, pParse, uintptr(0), yymsp+uintptr(-libc.Int32FromInt32(4))*24+8, yymsp+uintptr(-libc.Int32FromInt32(2))*24+8) /*A-overwrites-X*/ if *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(4))*24 + 8)) != 0 { (*(*TSrcItem)(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(4))*24 + 8)) + 8))).FzAlias = _sqlite3NameFromToken(tls, (*TParse)(unsafe.Pointer(pParse)).Fdb, yymsp+8) } goto _346 _139: - ; /* xfullname ::= nm AS nm */ + ; /* xfullname ::= nm AS nm */ *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*24 + 8)) = _sqlite3SrcListAppend(tls, pParse, uintptr(0), yymsp+uintptr(-libc.Int32FromInt32(2))*24+8, uintptr(0)) /*A-overwrites-X*/ if *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*24 + 8)) != 0 { (*(*TSrcItem)(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*24 + 8)) + 8))).FzAlias = _sqlite3NameFromToken(tls, (*TParse)(unsafe.Pointer(pParse)).Fdb, yymsp+8) @@ -232157,7 +232157,7 @@ func _yy_reduce(tls *libc.TLS, yypParser uintptr, yyruleno uint32, yyLookahead i _sqlite3ExprListSetSortOrder(tls, *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(4))*24 + 8)), *(*int32)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(1))*24 + 8)), *(*int32)(unsafe.Pointer(yymsp + 8))) goto _346 _152: - ; /* sortlist ::= expr sortorder nulls */ + ; /* sortlist ::= expr sortorder nulls */ *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*24 + 8)) = _sqlite3ExprListAppend(tls, pParse, uintptr(0), *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*24 + 8))) /*A-overwrites-Y*/ _sqlite3ExprListSetSortOrder(tls, *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*24 + 8)), *(*int32)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(1))*24 + 8)), *(*int32)(unsafe.Pointer(yymsp + 8))) goto _346 @@ -232736,7 +232736,7 @@ func _yy_reduce(tls *libc.TLS, yypParser uintptr, yyruleno uint32, yyLookahead i *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(4))*24 + 8)) = _parserAddExprIdListTerm(tls, pParse, *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(4))*24 + 8)), yymsp+uintptr(-libc.Int32FromInt32(2))*24+8, *(*int32)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(1))*24 + 8)), *(*int32)(unsafe.Pointer(yymsp + 8))) goto _346 _254: - ; /* eidlist ::= nm collate sortorder */ + ; /* eidlist ::= nm collate sortorder */ *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*24 + 8)) = _parserAddExprIdListTerm(tls, pParse, uintptr(0), yymsp+uintptr(-libc.Int32FromInt32(2))*24+8, *(*int32)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(1))*24 + 8)), *(*int32)(unsafe.Pointer(yymsp + 8))) /*A-overwrites-Y*/ goto _346 _255: @@ -232851,7 +232851,7 @@ func _yy_reduce(tls *libc.TLS, yypParser uintptr, yyruleno uint32, yyLookahead i *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(8))*24 + 8)) = *(*uintptr)(unsafe.Pointer(&*(*TYYMINORTYPE)(unsafe.Pointer(bp)))) goto _346 _281: - ; /* trigger_cmd ::= scanpt insert_cmd INTO trnm idlist_opt select upsert scanpt */ + ; /* trigger_cmd ::= scanpt insert_cmd INTO trnm idlist_opt select upsert scanpt */ *(*uintptr)(unsafe.Pointer(&*(*TYYMINORTYPE)(unsafe.Pointer(bp)))) = _sqlite3TriggerInsertStep(tls, pParse, yymsp+uintptr(-libc.Int32FromInt32(4))*24+8, *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(3))*24 + 8)), *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*24 + 8)), uint8(*(*int32)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(6))*24 + 8))), *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(1))*24 + 8)), *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(7))*24 + 8)), *(*uintptr)(unsafe.Pointer(yymsp + 8))) /*yylhsminor.yy427-overwrites-yymsp[-6].minor.yy144*/ *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(7))*24 + 8)) = *(*uintptr)(unsafe.Pointer(&*(*TYYMINORTYPE)(unsafe.Pointer(bp)))) goto _346 @@ -232980,7 +232980,7 @@ func _yy_reduce(tls *libc.TLS, yypParser uintptr, yyruleno uint32, yyLookahead i *(*Tu8)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*24 + 8)) = uint8(M10d_No) goto _346 _312: - ; /* wqitem ::= withnm eidlist_opt wqas LP select RP */ + ; /* wqitem ::= withnm eidlist_opt wqas LP select RP */ *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(5))*24 + 8)) = _sqlite3CteNew(tls, pParse, yymsp+uintptr(-libc.Int32FromInt32(5))*24+8, *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(4))*24 + 8)), *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(1))*24 + 8)), *(*Tu8)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(3))*24 + 8))) /*A-overwrites-X*/ goto _346 _313: @@ -232988,7 +232988,7 @@ func _yy_reduce(tls *libc.TLS, yypParser uintptr, yyruleno uint32, yyLookahead i (*TParse)(unsafe.Pointer(pParse)).FbHasWith = uint8(1) goto _346 _314: - ; /* wqlist ::= wqitem */ + ; /* wqlist ::= wqitem */ *(*uintptr)(unsafe.Pointer(yymsp + 8)) = _sqlite3WithAdd(tls, pParse, uintptr(0), *(*uintptr)(unsafe.Pointer(yymsp + 8))) /*A-overwrites-X*/ goto _346 _315: @@ -263572,7 +263572,7 @@ func _sessionSerializeValue(tls *libc.TLS, aBuf uintptr, pValue uintptr, pnWrite var _ /* i at bp+0 */ Tu64 var _ /* r at bp+8 */ float64 _, _, _, _, _ = eType, n, nByte, nVarint, z /* Size of serialized value in bytes */ - if pValue != 0 { /* Value type (SQLITE_NULL, TEXT etc.) */ + if pValue != 0 { /* Value type (SQLITE_NULL, TEXT etc.) */ eType = Xsqlite3_value_type(tls, pValue) if aBuf != 0 { *(*Tu8)(unsafe.Pointer(aBuf)) = uint8(uint8(eType)) @@ -291297,7 +291297,7 @@ func _sqlite3Fts5StorageOpen(tls *libc.TLS, pConfig uintptr, pIndex uintptr, bCr var nByte Tsqlite3_int64 var p, zCols, zDefn, v1 uintptr _, _, _, _, _, _, _, _, _ = i, iOff, nByte, nDefn, p, rc, zCols, zDefn, v1 - rc = SQLITE_OK /* Bytes of space to allocate */ + rc = SQLITE_OK /* Bytes of space to allocate */ nByte = int64(uint64(128) + uint64((*TFts5Config)(unsafe.Pointer(pConfig)).FnCol)*uint64(8)) /* Fts5Storage.aTotalSize[] */ v1 = Xsqlite3_malloc64(tls, uint64(uint64(nByte))) p = v1 @@ -300917,4 +300917,3 @@ func __ccgo_fp(f interface{}) uintptr { type Sqlite3_module = sqlite3_module type Sqlite3_vtab = sqlite3_vtab type Sqlite3_vtab_cursor = sqlite3_vtab_cursor - diff --git a/vendor/modernc.org/sqlite/lib/sqlite_windows_386.go b/vendor/modernc.org/sqlite/lib/sqlite_windows_386.go index 2b71621cd..c0d66b916 100644 --- a/vendor/modernc.org/sqlite/lib/sqlite_windows_386.go +++ b/vendor/modernc.org/sqlite/lib/sqlite_windows_386.go @@ -94062,7 +94062,7 @@ func Xsqlite3_str_vappendf(tls *libc.TLS, pAccum uintptr, fmt uintptr, ap Tva_li _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _ = bArgList, base, bufpt, c, cThousand, ch, ch1, cset, done, e2, escarg, exp, flag_alternateform, flag_altform2, flag_dp, flag_leftjustify, flag_long, flag_prefix, flag_rtz, flag_zeropad, i, i1, iRound, idx, ii, infop, isnull, ix, j, j1, k, length, longvalue, n, n1, nCopyBytes, nOut, nPad, nPrior, needQuote, nn, pArgList, pExpr, pItem, pSel, pToken, pre, precision, prefix, px, q, realvalue, szBufNeeded, v, width, wx, x, x1, xtype, z, zExtra, zOut, v10, v100, v101, v102, v103, v104, v106, v107, v108, v109, v11, v110, v111, v12, v14, v15, v16, v17, v18, v19, v2, v20, v21, v22, v23, v24, v3, v4, v45, v46, v47, v48, v49, v5, v51, v52, v54, v55, v56, v57, v58, v59, v6, v60, v61, v62, v64, v65, v66, v67, v68, v7, v70, v71, v72, v73, v74, v75, v76, v77, v78, v79, v8, v80, v81, v82, v83, v85, v86, v87, v88, v89, v9, v90, v91, v93, v94, v96, v97, v98, p92 /* Thousands separator for %d and %u */ xtype = uint8(etINVALID) /* Size of the rendering buffer */ zExtra = uintptr(0) /* True if trailing zeros should be removed */ - pArgList = uintptr(0) /* Conversion buffer */ + pArgList = uintptr(0) /* Conversion buffer */ /* pAccum never starts out with an empty buffer that was obtained from ** malloc(). This precondition is required by the mprintf("%z...") ** optimization. */ @@ -97585,14 +97585,14 @@ func _sqlite3AtoF(tls *libc.TLS, z uintptr, pResult uintptr, length int32, enc T var _ /* rr at bp+0 */ [2]float64 _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _ = d, e, eType, eValid, esign, i, incr, nDigit, r, s, s2, sign, zEnd, v2, v3, v4 /* sign * significand * (10 ^ (esign * exponent)) */ - sign = int32(1) /* sign of significand */ - s = uint64(0) /* significand */ - d = 0 /* adjust exponent for shifting decimal point */ - esign = int32(1) /* sign of exponent */ - e = 0 /* exponent */ - eValid = int32(1) /* True exponent is either not used or is well-formed */ - nDigit = 0 /* Number of digits processed */ - eType = int32(1) /* 1: pure integer, 2+: fractional -1 or less: bad UTF16 */ + sign = int32(1) /* sign of significand */ + s = uint64(0) /* significand */ + d = 0 /* adjust exponent for shifting decimal point */ + esign = int32(1) /* sign of exponent */ + e = 0 /* exponent */ + eValid = int32(1) /* True exponent is either not used or is well-formed */ + nDigit = 0 /* Number of digits processed */ + eType = int32(1) /* 1: pure integer, 2+: fractional -1 or less: bad UTF16 */ *(*float64)(unsafe.Pointer(pResult)) = float64(0) /* Default return value, in case of an error */ if length == 0 { return 0 @@ -103121,7 +103121,7 @@ func _winOpen(tls *libc.TLS, pVfs uintptr, zName uintptr, id uintptr, flags int3 ** a temporary file. Use this buffer to store the file name in. */ *(*uintptr)(unsafe.Pointer(bp + 8)) = uintptr(0) /* For temporary filename, if necessary. */ - rc = SQLITE_OK /* Function Return Code */ + rc = SQLITE_OK /* Function Return Code */ isExclusive = flags & int32(SQLITE_OPEN_EXCLUSIVE) isDelete = flags & int32(SQLITE_OPEN_DELETEONCLOSE) isCreate = flags & int32(SQLITE_OPEN_CREATE) @@ -111142,7 +111142,7 @@ func _readDbPage(tls *libc.TLS, pPg uintptr) (r int32) { _, _, _, _ = dbFileVers, iOffset, pPager, rc pPager = (*TPgHdr)(unsafe.Pointer(pPg)).FpPager /* Pager object associated with page pPg */ rc = SQLITE_OK /* Return code */ - *(*Tu32)(unsafe.Pointer(bp)) = uint32(0) /* Frame of WAL containing pgno */ + *(*Tu32)(unsafe.Pointer(bp)) = uint32(0) /* Frame of WAL containing pgno */ if (*TPager)(unsafe.Pointer(pPager)).FpWal != uintptr(0) { rc = _sqlite3WalFindFrame(tls, (*TPager)(unsafe.Pointer(pPager)).FpWal, (*TPgHdr)(unsafe.Pointer(pPg)).Fpgno, bp) if rc != 0 { @@ -130278,7 +130278,7 @@ func _btreeCreateTable(tls *libc.TLS, p uintptr, piTable uintptr, createTabFlags var _ /* pgnoRoot at bp+4 */ TPgno var _ /* rc at bp+8 */ int32 _, _ = pBt, ptfFlags - pBt = (*TBtree)(unsafe.Pointer(p)).FpBt /* Page-type flags for the root page of new table */ + pBt = (*TBtree)(unsafe.Pointer(p)).FpBt /* Page-type flags for the root page of new table */ if (*TBtShared)(unsafe.Pointer(pBt)).FautoVacuum != 0 { /* The page to move to. */ /* Creating a new table may probably require moving an existing database ** to make room for the new tables root page. In case this page turns @@ -136500,7 +136500,7 @@ func _sqlite3VdbeMakeReady(tls *libc.TLS, p uintptr, pParse uintptr) { */ n = int32((libc.Uint32FromInt64(20)*uint32((*TVdbe)(unsafe.Pointer(p)).FnOp) + libc.Uint32FromInt32(7)) & uint32(^libc.Int32FromInt32(7))) /* Bytes of opcode memory used */ (*(*TReusableSpace)(unsafe.Pointer(bp + 8))).FpSpace = (*TVdbe)(unsafe.Pointer(p)).FaOp + uintptr(n) /* Unused opcode memory */ - (*(*TReusableSpace)(unsafe.Pointer(bp + 8))).FnFree = int64(((*TParse)(unsafe.Pointer(pParse)).FszOpAlloc - n) & ^libc.Int32FromInt32(7)) /* Bytes of unused memory */ + (*(*TReusableSpace)(unsafe.Pointer(bp + 8))).FnFree = int64(((*TParse)(unsafe.Pointer(pParse)).FszOpAlloc - n) & ^libc.Int32FromInt32(7)) /* Bytes of unused memory */ _resolveP2Values(tls, p, bp) libc.SetBitFieldPtr16Uint32(p+152, uint32(libc.BoolUint8((*TParse)(unsafe.Pointer(pParse)).FisMultiWrite != 0 && (*TParse)(unsafe.Pointer(pParse)).FmayAbort != 0)), 5, 0x20) if (*TParse)(unsafe.Pointer(pParse)).Fexplain != 0 { @@ -167158,7 +167158,7 @@ func _statInit(tls *libc.TLS, context uintptr, argc int32, argv uintptr) { nColUp = nCol nKeyCol = Xsqlite3_value_int(tls, *(*uintptr)(unsafe.Pointer(argv + 1*4))) /* Allocate the space required for the StatAccum object */ - n = int32(uint32(120) + uint32(8)*uint32(uint32(nColUp))) /* StatAccum.anDLt */ + n = int32(uint32(120) + uint32(8)*uint32(uint32(nColUp))) /* StatAccum.anDLt */ n = int32(uint32(n) + libc.Uint32FromInt64(8)*uint32(uint32(nColUp))) /* StatAccum.anEq */ if mxSample != 0 { n = int32(uint32(n) + (libc.Uint32FromInt64(8)*uint32(uint32(nColUp)) + libc.Uint32FromInt64(40)*uint32(nCol+mxSample) + libc.Uint32FromInt64(8)*libc.Uint32FromInt32(3)*uint32(uint32(nColUp))*uint32(nCol+mxSample))) @@ -174112,7 +174112,7 @@ func _sqlite3RefillIndex(tls *libc.TLS, pParse uintptr, pIndex uintptr, memRootP func _sqlite3AllocateIndexObject(tls *libc.TLS, db uintptr, nCol Ti16, nExtra int32, ppExtra uintptr) (r uintptr) { var nByte int32 var p, pExtra uintptr - _, _, _ = nByte, p, pExtra /* Bytes of space for Index object + arrays */ + _, _, _ = nByte, p, pExtra /* Bytes of space for Index object + arrays */ nByte = int32((libc.Uint32FromInt64(104)+libc.Uint32FromInt32(7))&uint32(^libc.Int32FromInt32(7)) + (uint32(4)*uint32(uint32(nCol))+uint32(7))&uint32(^libc.Int32FromInt32(7)) + (uint32(2)*uint32(int32(int32(nCol))+libc.Int32FromInt32(1))+uint32(2)*uint32(uint32(nCol))+uint32(1)*uint32(uint32(nCol))+uint32(7))&uint32(^libc.Int32FromInt32(7))) /* Index.aSortOrder */ p = _sqlite3DbMallocZero(tls, db, uint64(nByte+nExtra)) if p != 0 { @@ -183549,7 +183549,7 @@ func _autoIncBegin(tls *libc.TLS, pParse uintptr, iDb int32, pTab uintptr) (r in v3 = pToplevel + 44 *(*int32)(unsafe.Pointer(v3))++ v2 = *(*int32)(unsafe.Pointer(v3)) - (*TAutoincInfo)(unsafe.Pointer(pInfo)).FregCtr = v2 /* Max rowid register */ + (*TAutoincInfo)(unsafe.Pointer(pInfo)).FregCtr = v2 /* Max rowid register */ *(*int32)(unsafe.Pointer(pToplevel + 44)) += int32(2) /* Rowid in sqlite_sequence + orig max val */ } memId = (*TAutoincInfo)(unsafe.Pointer(pInfo)).FregCtr @@ -209563,7 +209563,7 @@ func _sqlite3WhereCodeOneLoopStart(tls *libc.TLS, pParse uintptr, v uintptr, pWI iCovCur = v44 v47 = pParse + 44 *(*int32)(unsafe.Pointer(v47))++ - v46 = *(*int32)(unsafe.Pointer(v47)) /* Cursor used for index scans (if any) */ + v46 = *(*int32)(unsafe.Pointer(v47)) /* Cursor used for index scans (if any) */ regReturn = v46 /* Register used with OP_Gosub */ regRowset = 0 /* Register for RowSet object */ regRowid = 0 /* Register holding rowid */ @@ -232009,14 +232009,14 @@ func _yy_reduce(tls *libc.TLS, yypParser uintptr, yyruleno uint32, yyLookahead i *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*12 + 4)) = _sqlite3SrcListAppend(tls, pParse, uintptr(0), yymsp+uintptr(-libc.Int32FromInt32(2))*12+4, yymsp+4) /*A-overwrites-X*/ goto _346 _138: - ; /* xfullname ::= nm DOT nm AS nm */ + ; /* xfullname ::= nm DOT nm AS nm */ *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(4))*12 + 4)) = _sqlite3SrcListAppend(tls, pParse, uintptr(0), yymsp+uintptr(-libc.Int32FromInt32(4))*12+4, yymsp+uintptr(-libc.Int32FromInt32(2))*12+4) /*A-overwrites-X*/ if *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(4))*12 + 4)) != 0 { (*(*TSrcItem)(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(4))*12 + 4)) + 8))).FzAlias = _sqlite3NameFromToken(tls, (*TParse)(unsafe.Pointer(pParse)).Fdb, yymsp+4) } goto _346 _139: - ; /* xfullname ::= nm AS nm */ + ; /* xfullname ::= nm AS nm */ *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*12 + 4)) = _sqlite3SrcListAppend(tls, pParse, uintptr(0), yymsp+uintptr(-libc.Int32FromInt32(2))*12+4, uintptr(0)) /*A-overwrites-X*/ if *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*12 + 4)) != 0 { (*(*TSrcItem)(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*12 + 4)) + 8))).FzAlias = _sqlite3NameFromToken(tls, (*TParse)(unsafe.Pointer(pParse)).Fdb, yymsp+4) @@ -232074,7 +232074,7 @@ func _yy_reduce(tls *libc.TLS, yypParser uintptr, yyruleno uint32, yyLookahead i _sqlite3ExprListSetSortOrder(tls, *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(4))*12 + 4)), *(*int32)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(1))*12 + 4)), *(*int32)(unsafe.Pointer(yymsp + 4))) goto _346 _152: - ; /* sortlist ::= expr sortorder nulls */ + ; /* sortlist ::= expr sortorder nulls */ *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*12 + 4)) = _sqlite3ExprListAppend(tls, pParse, uintptr(0), *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*12 + 4))) /*A-overwrites-Y*/ _sqlite3ExprListSetSortOrder(tls, *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*12 + 4)), *(*int32)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(1))*12 + 4)), *(*int32)(unsafe.Pointer(yymsp + 4))) goto _346 @@ -232653,7 +232653,7 @@ func _yy_reduce(tls *libc.TLS, yypParser uintptr, yyruleno uint32, yyLookahead i *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(4))*12 + 4)) = _parserAddExprIdListTerm(tls, pParse, *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(4))*12 + 4)), yymsp+uintptr(-libc.Int32FromInt32(2))*12+4, *(*int32)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(1))*12 + 4)), *(*int32)(unsafe.Pointer(yymsp + 4))) goto _346 _254: - ; /* eidlist ::= nm collate sortorder */ + ; /* eidlist ::= nm collate sortorder */ *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*12 + 4)) = _parserAddExprIdListTerm(tls, pParse, uintptr(0), yymsp+uintptr(-libc.Int32FromInt32(2))*12+4, *(*int32)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(1))*12 + 4)), *(*int32)(unsafe.Pointer(yymsp + 4))) /*A-overwrites-Y*/ goto _346 _255: @@ -232768,7 +232768,7 @@ func _yy_reduce(tls *libc.TLS, yypParser uintptr, yyruleno uint32, yyLookahead i *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(8))*12 + 4)) = *(*uintptr)(unsafe.Pointer(&*(*TYYMINORTYPE)(unsafe.Pointer(bp)))) goto _346 _281: - ; /* trigger_cmd ::= scanpt insert_cmd INTO trnm idlist_opt select upsert scanpt */ + ; /* trigger_cmd ::= scanpt insert_cmd INTO trnm idlist_opt select upsert scanpt */ *(*uintptr)(unsafe.Pointer(&*(*TYYMINORTYPE)(unsafe.Pointer(bp)))) = _sqlite3TriggerInsertStep(tls, pParse, yymsp+uintptr(-libc.Int32FromInt32(4))*12+4, *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(3))*12 + 4)), *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*12 + 4)), uint8(*(*int32)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(6))*12 + 4))), *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(1))*12 + 4)), *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(7))*12 + 4)), *(*uintptr)(unsafe.Pointer(yymsp + 4))) /*yylhsminor.yy427-overwrites-yymsp[-6].minor.yy144*/ *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(7))*12 + 4)) = *(*uintptr)(unsafe.Pointer(&*(*TYYMINORTYPE)(unsafe.Pointer(bp)))) goto _346 @@ -232897,7 +232897,7 @@ func _yy_reduce(tls *libc.TLS, yypParser uintptr, yyruleno uint32, yyLookahead i *(*Tu8)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(2))*12 + 4)) = uint8(M10d_No) goto _346 _312: - ; /* wqitem ::= withnm eidlist_opt wqas LP select RP */ + ; /* wqitem ::= withnm eidlist_opt wqas LP select RP */ *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(5))*12 + 4)) = _sqlite3CteNew(tls, pParse, yymsp+uintptr(-libc.Int32FromInt32(5))*12+4, *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(4))*12 + 4)), *(*uintptr)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(1))*12 + 4)), *(*Tu8)(unsafe.Pointer(yymsp + uintptr(-libc.Int32FromInt32(3))*12 + 4))) /*A-overwrites-X*/ goto _346 _313: @@ -232905,7 +232905,7 @@ func _yy_reduce(tls *libc.TLS, yypParser uintptr, yyruleno uint32, yyLookahead i (*TParse)(unsafe.Pointer(pParse)).FbHasWith = uint8(1) goto _346 _314: - ; /* wqlist ::= wqitem */ + ; /* wqlist ::= wqitem */ *(*uintptr)(unsafe.Pointer(yymsp + 4)) = _sqlite3WithAdd(tls, pParse, uintptr(0), *(*uintptr)(unsafe.Pointer(yymsp + 4))) /*A-overwrites-X*/ goto _346 _315: @@ -263543,7 +263543,7 @@ func _sessionSerializeValue(tls *libc.TLS, aBuf uintptr, pValue uintptr, pnWrite var _ /* i at bp+0 */ Tu64 var _ /* r at bp+8 */ float64 _, _, _, _, _ = eType, n, nByte, nVarint, z /* Size of serialized value in bytes */ - if pValue != 0 { /* Value type (SQLITE_NULL, TEXT etc.) */ + if pValue != 0 { /* Value type (SQLITE_NULL, TEXT etc.) */ eType = Xsqlite3_value_type(tls, pValue) if aBuf != 0 { *(*Tu8)(unsafe.Pointer(aBuf)) = uint8(uint8(eType)) @@ -291384,7 +291384,7 @@ func _sqlite3Fts5StorageOpen(tls *libc.TLS, pConfig uintptr, pIndex uintptr, bCr var nByte Tsqlite3_int64 var p, zCols, zDefn, v1 uintptr _, _, _, _, _, _, _, _, _ = i, iOff, nByte, nDefn, p, rc, zCols, zDefn, v1 - rc = SQLITE_OK /* Bytes of space to allocate */ + rc = SQLITE_OK /* Bytes of space to allocate */ nByte = int64(uint32(72) + uint32((*TFts5Config)(unsafe.Pointer(pConfig)).FnCol)*uint32(8)) /* Fts5Storage.aTotalSize[] */ v1 = Xsqlite3_malloc64(tls, uint64(uint64(nByte))) p = v1 @@ -301017,4 +301017,3 @@ func __ccgo_fp(f interface{}) uintptr { type Sqlite3_module = sqlite3_module type Sqlite3_vtab = sqlite3_vtab type Sqlite3_vtab_cursor = sqlite3_vtab_cursor - diff --git a/vendor/modernc.org/sqlite/logo.png b/vendor/modernc.org/sqlite/logo.png new file mode 100644 index 000000000..bdac02dff Binary files /dev/null and b/vendor/modernc.org/sqlite/logo.png differ diff --git a/vendor/modernc.org/sqlite/sqlite.go b/vendor/modernc.org/sqlite/sqlite.go index a785c6af9..1af3eea7b 100644 --- a/vendor/modernc.org/sqlite/sqlite.go +++ b/vendor/modernc.org/sqlite/sqlite.go @@ -18,6 +18,7 @@ "net/url" "reflect" "runtime" + "sort" "strconv" "strings" "sync" @@ -819,7 +820,25 @@ func applyQueryParams(c *conn, query string) error { return err } + var a []string for _, v := range q["_pragma"] { + a = append(a, v) + } + // Push 'busy_timeout' first, the rest in lexicographic order, case insenstive. + // See https://gitlab.com/cznic/sqlite/-/issues/198#note_2233423463 for + // discussion. + sort.Slice(a, func(i, j int) bool { + x, y := strings.TrimSpace(strings.ToLower(a[i])), strings.TrimSpace(strings.ToLower(a[j])) + if strings.HasPrefix(x, "busy_timeout") { + return true + } + if strings.HasPrefix(y, "busy_timeout") { + return false + } + + return x < y + }) + for _, v := range a { cmd := "pragma " + v _, err := c.exec(context.Background(), cmd, nil) if err != nil { @@ -1390,6 +1409,27 @@ func (c *conn) closeV2(db uintptr) error { return nil } +// ResetSession is called prior to executing a query on the connection if the +// connection has been used before. If the driver returns ErrBadConn the +// connection is discarded. +func (c *conn) ResetSession(ctx context.Context) error { + if !c.usable() { + return driver.ErrBadConn + } + + return nil +} + +// IsValid is called prior to placing the connection into the connection pool. +// The connection will be discarded if false is returned. +func (c *conn) IsValid() bool { + return c.usable() +} + +func (c *conn) usable() bool { + return c.db != 0 && sqlite3.Xsqlite3_is_interrupted(c.tls, c.db) == 0 +} + // FunctionImpl describes an [application-defined SQL function]. If Scalar is // set, it is treated as a scalar function; otherwise, it is treated as an // aggregate function using MakeAggregate. diff --git a/vendor/modules.txt b/vendor/modules.txt index a97146255..0e2195c6a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -24,7 +24,7 @@ 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.6.0 +# codeberg.org/gruf/go-ffmpreg v0.6.4 ## explicit; go 1.22.0 codeberg.org/gruf/go-ffmpreg/embed codeberg.org/gruf/go-ffmpreg/wasm @@ -491,7 +491,7 @@ github.com/miekg/dns # github.com/minio/md5-simd v1.1.2 ## explicit; go 1.14 github.com/minio/md5-simd -# github.com/minio/minio-go/v7 v7.0.80 +# github.com/minio/minio-go/v7 v7.0.81 ## explicit; go 1.22 github.com/minio/minio-go/v7 github.com/minio/minio-go/v7/pkg/cors @@ -523,12 +523,13 @@ 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.3 +# github.com/ncruces/go-sqlite3 v0.21.3 ## explicit; go 1.21 github.com/ncruces/go-sqlite3 github.com/ncruces/go-sqlite3/driver github.com/ncruces/go-sqlite3/embed github.com/ncruces/go-sqlite3/internal/alloc +github.com/ncruces/go-sqlite3/internal/dotlk github.com/ncruces/go-sqlite3/internal/util github.com/ncruces/go-sqlite3/util/osutil github.com/ncruces/go-sqlite3/util/sql3util @@ -1072,7 +1073,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.29.0 +# golang.org/x/crypto v0.31.0 ## explicit; go 1.20 golang.org/x/crypto/acme golang.org/x/crypto/acme/autocert @@ -1100,7 +1101,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.22.0 +# golang.org/x/image v0.23.0 ## explicit; go 1.18 golang.org/x/image/riff golang.org/x/image/vp8 @@ -1111,7 +1112,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.31.0 +# golang.org/x/net v0.32.0 ## explicit; go 1.18 golang.org/x/net/bpf golang.org/x/net/context @@ -1133,17 +1134,17 @@ golang.org/x/net/trace ## explicit; go 1.18 golang.org/x/oauth2 golang.org/x/oauth2/internal -# golang.org/x/sync v0.9.0 +# golang.org/x/sync v0.10.0 ## explicit; go 1.18 golang.org/x/sync/errgroup golang.org/x/sync/semaphore -# golang.org/x/sys v0.27.0 +# golang.org/x/sys v0.28.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.20.0 +# golang.org/x/text v0.21.0 ## explicit; go 1.18 golang.org/x/text/cases golang.org/x/text/encoding @@ -1345,8 +1346,8 @@ modernc.org/mathutil # modernc.org/memory v1.8.0 ## explicit; go 1.18 modernc.org/memory -# modernc.org/sqlite v0.0.0-00010101000000-000000000000 => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.33.1-concurrency-workaround -## explicit; go 1.20 +# modernc.org/sqlite v0.0.0-00010101000000-000000000000 => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.34.2-concurrency-workaround +## explicit; go 1.21 modernc.org/sqlite modernc.org/sqlite/lib # modernc.org/strutil v1.2.0 @@ -1359,7 +1360,7 @@ modernc.org/token ## explicit; go 1.19 mvdan.cc/xurls/v2 # github.com/go-swagger/go-swagger => github.com/superseriousbusiness/go-swagger v0.31.0-gts-go1.23-fix -# modernc.org/sqlite => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.33.1-concurrency-workaround +# modernc.org/sqlite => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.34.2-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 diff --git a/web/assets/themes/blurple-auto.css b/web/assets/themes/blurple-auto.css new file mode 100644 index 000000000..817a07248 --- /dev/null +++ b/web/assets/themes/blurple-auto.css @@ -0,0 +1,10 @@ +/* + theme-title: Blurple (auto) + theme-description: Official blurple theme that adapts to system preferences +*/ + +/* Default to dark theme */ +@import url("blurple-dark.css"); + +@import url("blurple-light.css") screen and (prefers-color-scheme: light); +@import url("blurple-dark.css") screen and (prefers-color-scheme: dark); diff --git a/web/assets/themes/brutalist-auto.css b/web/assets/themes/brutalist-auto.css new file mode 100644 index 000000000..080360c87 --- /dev/null +++ b/web/assets/themes/brutalist-auto.css @@ -0,0 +1,10 @@ +/* + theme-title: Brutalist (auto) + theme-description: Official (Pseudo-)monochrome brutality theme that adapts to system preferences +*/ + +/* Default to brutalist theme */ +@import url("brutalist.css"); + +@import url("brutalist.css") screen and (prefers-color-scheme: light); +@import url("brutalist-dark.css") screen and (prefers-color-scheme: dark); diff --git a/web/assets/themes/solarized-auto.css b/web/assets/themes/solarized-auto.css new file mode 100644 index 000000000..8324ef5f7 --- /dev/null +++ b/web/assets/themes/solarized-auto.css @@ -0,0 +1,10 @@ +/* + theme-title: Solarized (auto) + theme-description: Solarized theme that adapts to system preferences +*/ + +/* Default to dark theme */ +@import url("solarized-dark.css"); + +@import url("solarized-light.css") screen and (prefers-color-scheme: light); +@import url("solarized-dark.css") screen and (prefers-color-scheme: dark); diff --git a/web/source/package.json b/web/source/package.json index 3c239419e..ea90137c8 100644 --- a/web/source/package.json +++ b/web/source/package.json @@ -20,7 +20,7 @@ "langs": "^2.0.0", "match-sorter": "^6.3.1", "modern-normalize": "^1.1.0", - "nanoid": "^4.0.0", + "nanoid": "^5.0.9", "object-to-formdata": "^4.4.2", "papaparse": "^5.3.2", "parse-link-header": "^2.0.0", diff --git a/web/source/settings/lib/types/instance.ts b/web/source/settings/lib/types/instance.ts index 11f75032c..9abdc6a96 100644 --- a/web/source/settings/lib/types/instance.ts +++ b/web/source/settings/lib/types/instance.ts @@ -25,6 +25,7 @@ export interface InstanceV1 { description_text?: string; short_description: string; short_description_text?: string; + custom_css: string; email: string; version: string; debug?: boolean; diff --git a/web/source/settings/views/admin/instance/settings.tsx b/web/source/settings/views/admin/instance/settings.tsx index c769b11ec..fd5ceb1ee 100644 --- a/web/source/settings/views/admin/instance/settings.tsx +++ b/web/source/settings/views/admin/instance/settings.tsx @@ -46,7 +46,7 @@ function InstanceSettingsForm({ data: instance }: InstanceSettingsFormProps) { const shortDescLimit = 500; const descLimit = 5000; const termsLimit = 5000; - + const form = { title: useTextInput("title", { source: instance, @@ -66,6 +66,10 @@ function InstanceSettingsForm({ data: instance }: InstanceSettingsFormProps) { valueSelector: (s: InstanceV1) => s.description_text, validator: (val: string) => val.length <= descLimit ? "" : `Instance description is ${val.length} characters; must be ${descLimit} characters or less` }), + customCSS: useTextInput("custom_css", { + source: instance, + valueSelector: (s: InstanceV1) => s.custom_css + }), terms: useTextInput("terms", { source: instance, // Select "raw" text version of parsed field for editing. @@ -191,7 +195,16 @@ function InstanceSettingsForm({ data: instance }: InstanceSettingsFormProps) { type="email" /> +