mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-12-04 17:42:46 +00:00
Merge branch 'main' into domain_permission_subscriptions
This commit is contained in:
commit
d11aee04b6
|
@ -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`.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1642,6 +1642,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
|
||||
|
@ -1822,6 +1826,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
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 标头解析下一页与上一页查询。
|
||||
|
||||
示例:
|
||||
```
|
||||
<https://example.org/api/v1/admin/domain_permission_drafts?limit=20&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/domain_permission_drafts?limit=20&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; 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 标头解析下一页与上一页查询。
|
||||
示例:
|
||||
```
|
||||
<https://example.org/api/v1/admin/domain_permission_excludes?limit=20&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/domain_permission_excludes?limit=20&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; 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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 直接提供的,设置面板也是如此,但大多数交互都是通过应用程序完成的。
|
||||
|
||||
## 为什么我的贴文没有显示在我的账户页面上?
|
||||
|
||||
|
|
|
@ -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 连接通过。
|
||||
|
||||
|
|
|
@ -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`。
|
||||
|
||||
|
|
|
@ -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 将是轻而易举的。
|
||||
|
|
|
@ -36,6 +36,9 @@ GoToSocial 为贴文提供 Mastodon 风格的隐私设置。从最私密到最
|
|||
|
||||
### 互关可见
|
||||
|
||||
!!! warning
|
||||
目前暂时无法将帖文可见性设为“互关可见”。
|
||||
|
||||
`互关可见` 的贴文只会显示给贴文作者和与作者*互相关注*的人。换句话说,只有在满足两个条件时,其他人才能看到:
|
||||
|
||||
1. 其他账户关注贴文作者。
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 229 KiB |
4
go.mod
4
go.mod
|
@ -41,7 +41,7 @@ require (
|
|||
codeberg.org/gruf/go-sched v1.2.4
|
||||
codeberg.org/gruf/go-storage v0.2.0
|
||||
codeberg.org/gruf/go-structr v0.8.11
|
||||
codeberg.org/superseriousbusiness/exif-terminator v0.9.0
|
||||
codeberg.org/superseriousbusiness/exif-terminator v0.9.1
|
||||
github.com/DmitriyVTitov/size v1.5.0
|
||||
github.com/KimMachineGun/automemlimit v0.6.1
|
||||
github.com/buckket/go-blurhash v1.1.0
|
||||
|
@ -60,7 +60,7 @@ 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/oklog/ulid v1.3.1
|
||||
|
|
8
go.sum
generated
8
go.sum
generated
|
@ -72,8 +72,8 @@ codeberg.org/gruf/go-storage v0.2.0 h1:mKj3Lx6AavEkuXXtxqPhdq+akW9YwrnP16yQBF7K5
|
|||
codeberg.org/gruf/go-storage v0.2.0/go.mod h1:o3GzMDE5QNUaRnm/daUzFqvuAaC4utlgXDXYO79sWKU=
|
||||
codeberg.org/gruf/go-structr v0.8.11 h1:I3cQCHpK3fQSXWaaUfksAJRN4+efULiuF11Oi/m8c+o=
|
||||
codeberg.org/gruf/go-structr v0.8.11/go.mod h1:zkoXVrAnKosh8VFAsbP/Hhs8FmLBjbVVy5w/Ngm8ApM=
|
||||
codeberg.org/superseriousbusiness/exif-terminator v0.9.0 h1:/EfyGI6HIrbkhFwgXGSjZ9o1kr/+k8v4mKdfXTH02Go=
|
||||
codeberg.org/superseriousbusiness/exif-terminator v0.9.0/go.mod h1:gCWKduudUWFzsnixoMzu0FYVdxHWG+AbXnZ50DqxsUE=
|
||||
codeberg.org/superseriousbusiness/exif-terminator v0.9.1 h1:8Pss29AVuvljHAYLnZUyoqJp/8IN1cD3Jz30bJbxme8=
|
||||
codeberg.org/superseriousbusiness/exif-terminator v0.9.1/go.mod h1:gCWKduudUWFzsnixoMzu0FYVdxHWG+AbXnZ50DqxsUE=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
|
@ -413,8 +413,8 @@ github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
|||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.80 h1:2mdUHXEykRdY/BigLt3Iuu1otL0JTogT0Nmltg0wujk=
|
||||
github.com/minio/minio-go/v7 v7.0.80/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0=
|
||||
github.com/minio/minio-go/v7 v7.0.81 h1:SzhMN0TQ6T/xSBu6Nvw3M5M8voM+Ht8RH3hE8S7zxaA=
|
||||
github.com/minio/minio-go/v7 v7.0.81/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
"github.com/uptrace/bun/dialect/pgdialect"
|
||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
// DBService satisfies the DB interface
|
||||
|
@ -131,18 +132,18 @@ func doMigration(ctx context.Context, db *bun.DB) error {
|
|||
// NewBunDBService returns a bunDB derived from the provided config, which implements the go-fed DB interface.
|
||||
// Under the hood, it uses https://github.com/uptrace/bun to create and maintain a database connection.
|
||||
func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
||||
var db *bun.DB
|
||||
var sqldb *sql.DB
|
||||
var dialect func() schema.Dialect
|
||||
var err error
|
||||
t := strings.ToLower(config.GetDbType())
|
||||
|
||||
switch t {
|
||||
switch t := strings.ToLower(config.GetDbType()); t {
|
||||
case "postgres":
|
||||
db, err = pgConn(ctx)
|
||||
sqldb, dialect, err = pgConn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "sqlite":
|
||||
db, err = sqliteConn(ctx)
|
||||
sqldb, dialect, err = sqliteConn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -150,34 +151,20 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
|||
return nil, fmt.Errorf("database type %s not supported for bundb", t)
|
||||
}
|
||||
|
||||
// Add database query hooks.
|
||||
db.AddQueryHook(queryHook{})
|
||||
if config.GetTracingEnabled() {
|
||||
db.AddQueryHook(tracing.InstrumentBun())
|
||||
}
|
||||
if config.GetMetricsEnabled() {
|
||||
db.AddQueryHook(metrics.InstrumentBun())
|
||||
}
|
||||
|
||||
// table registration is needed for many-to-many, see:
|
||||
// https://bun.uptrace.dev/orm/many-to-many-relation/
|
||||
for _, t := range []interface{}{
|
||||
>smodel.AccountToEmoji{},
|
||||
>smodel.ConversationToStatus{},
|
||||
>smodel.StatusToEmoji{},
|
||||
>smodel.StatusToTag{},
|
||||
>smodel.ThreadToStatus{},
|
||||
} {
|
||||
db.RegisterModel(t)
|
||||
}
|
||||
|
||||
// perform any pending database migrations: this includes
|
||||
// the very first 'migration' on startup which just creates
|
||||
// necessary tables
|
||||
if err := doMigration(ctx, db); err != nil {
|
||||
// perform any pending database migrations: this includes the first
|
||||
// 'migration' on startup which just creates necessary db tables.
|
||||
//
|
||||
// Note this uses its own instance of bun.DB as bun will automatically
|
||||
// store in-memory reflect type schema of any Go models passed to it,
|
||||
// and we still maintain lots of old model versions in the migrations.
|
||||
if err := doMigration(ctx, bunDB(sqldb, dialect)); err != nil {
|
||||
return nil, fmt.Errorf("db migration error: %s", err)
|
||||
}
|
||||
|
||||
// Wrap sql.DB as bun.DB type,
|
||||
// adding any connection hooks.
|
||||
db := bunDB(sqldb, dialect)
|
||||
|
||||
ps := &DBService{
|
||||
Account: &accountDB{
|
||||
db: db,
|
||||
|
@ -319,17 +306,47 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
|||
return ps, nil
|
||||
}
|
||||
|
||||
func pgConn(ctx context.Context) (*bun.DB, error) {
|
||||
// bunDB returns a new bun.DB for given sql.DB connection pool and dialect
|
||||
// function. This can be used to apply any necessary opts / hooks as we
|
||||
// initialize a bun.DB object both before and after performing migrations.
|
||||
func bunDB(sqldb *sql.DB, dialect func() schema.Dialect) *bun.DB {
|
||||
db := bun.NewDB(sqldb, dialect())
|
||||
|
||||
// Add our SQL connection hooks.
|
||||
db.AddQueryHook(queryHook{})
|
||||
if config.GetTracingEnabled() {
|
||||
db.AddQueryHook(tracing.InstrumentBun())
|
||||
}
|
||||
if config.GetMetricsEnabled() {
|
||||
db.AddQueryHook(metrics.InstrumentBun())
|
||||
}
|
||||
|
||||
// table registration is needed for many-to-many, see:
|
||||
// https://bun.uptrace.dev/orm/many-to-many-relation/
|
||||
for _, t := range []interface{}{
|
||||
>smodel.AccountToEmoji{},
|
||||
>smodel.ConversationToStatus{},
|
||||
>smodel.StatusToEmoji{},
|
||||
>smodel.StatusToTag{},
|
||||
>smodel.ThreadToStatus{},
|
||||
} {
|
||||
db.RegisterModel(t)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func pgConn(ctx context.Context) (*sql.DB, func() schema.Dialect, error) {
|
||||
opts, err := deriveBunDBPGOptions() //nolint:contextcheck
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create bundb postgres options: %w", err)
|
||||
return nil, nil, fmt.Errorf("could not create bundb postgres options: %w", err)
|
||||
}
|
||||
|
||||
cfg := stdlib.RegisterConnConfig(opts)
|
||||
|
||||
sqldb, err := sql.Open("pgx-gts", cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open postgres db: %w", err)
|
||||
return nil, nil, fmt.Errorf("could not open postgres db: %w", err)
|
||||
}
|
||||
|
||||
// Tune db connections for postgres, see:
|
||||
|
@ -339,22 +356,20 @@ func pgConn(ctx context.Context) (*bun.DB, error) {
|
|||
sqldb.SetMaxIdleConns(2) // assume default 2; if max idle is less than max open, it will be automatically adjusted
|
||||
sqldb.SetConnMaxLifetime(5 * time.Minute) // fine to kill old connections
|
||||
|
||||
db := bun.NewDB(sqldb, pgdialect.New())
|
||||
|
||||
// ping to check the db is there and listening
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return nil, fmt.Errorf("postgres ping: %w", err)
|
||||
if err := sqldb.PingContext(ctx); err != nil {
|
||||
return nil, nil, fmt.Errorf("postgres ping: %w", err)
|
||||
}
|
||||
|
||||
log.Info(ctx, "connected to POSTGRES database")
|
||||
return db, nil
|
||||
return sqldb, func() schema.Dialect { return pgdialect.New() }, nil
|
||||
}
|
||||
|
||||
func sqliteConn(ctx context.Context) (*bun.DB, error) {
|
||||
func sqliteConn(ctx context.Context) (*sql.DB, func() schema.Dialect, error) {
|
||||
// validate db address has actually been set
|
||||
address := config.GetDbAddress()
|
||||
if address == "" {
|
||||
return nil, fmt.Errorf("'%s' was not set when attempting to start sqlite", config.DbAddressFlag())
|
||||
return nil, nil, fmt.Errorf("'%s' was not set when attempting to start sqlite", config.DbAddressFlag())
|
||||
}
|
||||
|
||||
// Build SQLite connection address with prefs.
|
||||
|
@ -363,7 +378,7 @@ func sqliteConn(ctx context.Context) (*bun.DB, error) {
|
|||
// Open new DB instance
|
||||
sqldb, err := sql.Open("sqlite-gts", address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open sqlite db with address %s: %w", address, err)
|
||||
return nil, nil, fmt.Errorf("could not open sqlite db with address %s: %w", address, err)
|
||||
}
|
||||
|
||||
// Tune db connections for sqlite, see:
|
||||
|
@ -379,16 +394,14 @@ func sqliteConn(ctx context.Context) (*bun.DB, error) {
|
|||
sqldb.SetConnMaxLifetime(5 * time.Minute)
|
||||
}
|
||||
|
||||
db := bun.NewDB(sqldb, sqlitedialect.New())
|
||||
|
||||
// ping to check the db is there and listening
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return nil, fmt.Errorf("sqlite ping: %w", err)
|
||||
if err := sqldb.PingContext(ctx); err != nil {
|
||||
return nil, nil, fmt.Errorf("sqlite ping: %w", err)
|
||||
}
|
||||
|
||||
log.Infof(ctx, "connected to SQLITE database with address %s", address)
|
||||
|
||||
return db, nil
|
||||
return sqldb, func() schema.Dialect { return sqlitedialect.New() }, nil
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -517,15 +530,12 @@ func buildSQLiteAddress(addr string) (string, bool) {
|
|||
//
|
||||
// - SQLite by itself supports setting a subset of its configuration options
|
||||
// via URI query arguments in the connection. Namely `mode` and `cache`.
|
||||
// This is the same situation for the directly transpiled C->Go code in
|
||||
// modernc.org/sqlite, i.e. modernc.org/sqlite/lib, NOT the Go SQL driver.
|
||||
// This is the same situation for our supported SQLite implementations.
|
||||
//
|
||||
// - `modernc.org/sqlite` has a "shim" around it to allow the directly
|
||||
// transpiled C code to be usable with a more native Go API. This is in
|
||||
// the form of a `database/sql/driver.Driver{}` implementation that calls
|
||||
// through to the transpiled C code.
|
||||
// - Both implementations have a "shim" around them in the form of a
|
||||
// `database/sql/driver.Driver{}` implementation.
|
||||
//
|
||||
// - The SQLite shim we interface with adds support for setting ANY of the
|
||||
// - The SQLite shims we interface with add support for setting ANY of the
|
||||
// configuration options via query arguments, through using a special `_pragma`
|
||||
// query key that specifies SQLite PRAGMAs to set upon opening each connection.
|
||||
// As such you will see below that most config is set with the `_pragma` key.
|
||||
|
@ -551,12 +561,6 @@ func buildSQLiteAddress(addr string) (string, bool) {
|
|||
// reached. And for whatever reason (:shrug:) SQLite is very particular about
|
||||
// setting this BEFORE the `journal_mode` is set, otherwise you can end up
|
||||
// running into more of these `SQLITE_BUSY` return codes than you might expect.
|
||||
//
|
||||
// - One final thing (I promise!): `SQLITE_BUSY` is only handled by the internal
|
||||
// `busy_timeout` handler in the case that a data race occurs contending for
|
||||
// table locks. THERE ARE STILL OTHER SITUATIONS IN WHICH THIS MAY BE RETURNED!
|
||||
// As such, we use our wrapping DB{} and Tx{} types (in "db.go") which make use
|
||||
// of our own retry-busy handler.
|
||||
|
||||
// Drop anything fancy from DB address
|
||||
addr = strings.Split(addr, "?")[0] // drop any provided query strings
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -227,6 +227,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 {
|
||||
|
|
|
@ -217,18 +217,23 @@ func (f *federate) CreatePollVote(ctx context.Context, poll *gtsmodel.Poll, vote
|
|||
return err
|
||||
}
|
||||
|
||||
// Convert vote to AS Create with vote choices as Objects.
|
||||
create, err := f.converter.PollVoteToASCreate(ctx, vote)
|
||||
// Convert vote to AS Creates with vote choices as Objects.
|
||||
creates, err := f.converter.PollVoteToASCreates(ctx, vote)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error converting to notes: %w", err)
|
||||
}
|
||||
|
||||
// Send the Create via the Actor's outbox.
|
||||
if _, err := f.FederatingActor().Send(ctx, outboxIRI, create); err != nil {
|
||||
return gtserror.Newf("error sending Create activity via outbox %s: %w", outboxIRI, err)
|
||||
var errs gtserror.MultiError
|
||||
|
||||
// Send each create activity.
|
||||
actor := f.FederatingActor()
|
||||
for _, create := range creates {
|
||||
if _, err := actor.Send(ctx, outboxIRI, create); err != nil {
|
||||
errs.Appendf("error sending Create activity via outbox %s: %w", outboxIRI, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (f *federate) DeleteStatus(ctx context.Context, status *gtsmodel.Status) error {
|
||||
|
|
|
@ -1701,10 +1701,14 @@ func (c *Converter) ReportToASFlag(ctx context.Context, r *gtsmodel.Report) (voc
|
|||
// PollVoteToASCreate converts a vote on a poll into a Create
|
||||
// activity, suitable for federation, with each choice in the
|
||||
// vote appended as a Note to the Create's Object field.
|
||||
func (c *Converter) PollVoteToASCreate(
|
||||
//
|
||||
// TODO: as soon as other AP server implementations support
|
||||
// the use of multiple objects in a single create, update this
|
||||
// to return just the one create event again.
|
||||
func (c *Converter) PollVoteToASCreates(
|
||||
ctx context.Context,
|
||||
vote *gtsmodel.PollVote,
|
||||
) (vocab.ActivityStreamsCreate, error) {
|
||||
) ([]vocab.ActivityStreamsCreate, error) {
|
||||
if len(vote.Choices) == 0 {
|
||||
panic("no vote.Choices")
|
||||
}
|
||||
|
@ -1743,22 +1747,25 @@ func (c *Converter) PollVoteToASCreate(
|
|||
return nil, gtserror.Newf("invalid account uri: %w", err)
|
||||
}
|
||||
|
||||
// Allocate Create activity and address 'To' poll author.
|
||||
create := streams.NewActivityStreamsCreate()
|
||||
ap.AppendTo(create, pollAuthorIRI)
|
||||
// Parse each choice to a Note and add it to the list of Creates.
|
||||
creates := make([]vocab.ActivityStreamsCreate, len(vote.Choices))
|
||||
for i, choice := range vote.Choices {
|
||||
|
||||
// Create ID formatted as: {$voterIRI}/activity#vote/{$statusIRI}.
|
||||
id := author.URI + "/activity#vote/" + poll.Status.URI
|
||||
ap.MustSet(ap.SetJSONLDIdStr, ap.WithJSONLDId(create), id)
|
||||
// Allocate Create activity and address 'To' poll author.
|
||||
create := streams.NewActivityStreamsCreate()
|
||||
ap.AppendTo(create, pollAuthorIRI)
|
||||
|
||||
// Set Create actor appropriately.
|
||||
ap.AppendActorIRIs(create, authorIRI)
|
||||
// Create ID formatted as: {$voterIRI}/activity#vote{$index}/{$statusIRI}.
|
||||
createID := fmt.Sprintf("%s/activity#vote%d/%s", author.URI, i, poll.Status.URI)
|
||||
ap.MustSet(ap.SetJSONLDIdStr, ap.WithJSONLDId(create), createID)
|
||||
|
||||
// Set publish time for activity.
|
||||
ap.SetPublished(create, vote.CreatedAt)
|
||||
// Set Create actor appropriately.
|
||||
ap.AppendActorIRIs(create, authorIRI)
|
||||
|
||||
// Parse each choice to a Note and add it to the Create.
|
||||
for _, choice := range vote.Choices {
|
||||
// Set publish time for activity.
|
||||
ap.SetPublished(create, vote.CreatedAt)
|
||||
|
||||
// Allocate new note to hold the vote.
|
||||
note := streams.NewActivityStreamsNote()
|
||||
|
||||
// For AP IRI generate from author URI + poll ID + vote choice.
|
||||
|
@ -1775,11 +1782,14 @@ func (c *Converter) PollVoteToASCreate(
|
|||
ap.AppendInReplyTo(note, statusIRI)
|
||||
ap.AppendTo(note, pollAuthorIRI)
|
||||
|
||||
// Append this note as Create Object.
|
||||
// Append this note to the Create Object.
|
||||
appendStatusableToActivity(create, note, false)
|
||||
|
||||
// Set create in slice.
|
||||
creates[i] = create
|
||||
}
|
||||
|
||||
return create, nil
|
||||
return creates, nil
|
||||
}
|
||||
|
||||
// populateValuesForProp appends the given PolicyValues
|
||||
|
|
|
@ -1104,43 +1104,55 @@ func (suite *InternalToASTestSuite) TestPinnedStatusesToASOneItem() {
|
|||
func (suite *InternalToASTestSuite) TestPollVoteToASCreate() {
|
||||
vote := suite.testPollVotes["remote_account_1_status_2_poll_vote_local_account_1"]
|
||||
|
||||
create, err := suite.typeconverter.PollVoteToASCreate(context.Background(), vote)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
creates, err := suite.typeconverter.PollVoteToASCreates(context.Background(), vote)
|
||||
suite.NoError(err)
|
||||
suite.Len(creates, 2)
|
||||
|
||||
createI, err := ap.Serialize(create)
|
||||
createI0, err := ap.Serialize(creates[0])
|
||||
suite.NoError(err)
|
||||
|
||||
bytes, err := json.MarshalIndent(createI, "", " ")
|
||||
createI1, err := ap.Serialize(creates[1])
|
||||
suite.NoError(err)
|
||||
|
||||
bytes0, err := json.MarshalIndent(createI0, "", " ")
|
||||
suite.NoError(err)
|
||||
|
||||
bytes1, err := json.MarshalIndent(createI1, "", " ")
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": "http://localhost:8080/users/the_mighty_zork",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/activity#vote/http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6",
|
||||
"object": [
|
||||
{
|
||||
"attributedTo": "http://localhost:8080/users/the_mighty_zork",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork#01HEN2R65468ZG657C4ZPHJ4EX/votes/1",
|
||||
"inReplyTo": "http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6",
|
||||
"name": "tissues",
|
||||
"to": "http://fossbros-anonymous.io/users/foss_satan",
|
||||
"type": "Note"
|
||||
},
|
||||
{
|
||||
"attributedTo": "http://localhost:8080/users/the_mighty_zork",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork#01HEN2R65468ZG657C4ZPHJ4EX/votes/2",
|
||||
"inReplyTo": "http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6",
|
||||
"name": "financial times",
|
||||
"to": "http://fossbros-anonymous.io/users/foss_satan",
|
||||
"type": "Note"
|
||||
}
|
||||
],
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/activity#vote0/http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6",
|
||||
"object": {
|
||||
"attributedTo": "http://localhost:8080/users/the_mighty_zork",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork#01HEN2R65468ZG657C4ZPHJ4EX/votes/1",
|
||||
"inReplyTo": "http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6",
|
||||
"name": "tissues",
|
||||
"to": "http://fossbros-anonymous.io/users/foss_satan",
|
||||
"type": "Note"
|
||||
},
|
||||
"published": "2021-09-11T11:45:37+02:00",
|
||||
"to": "http://fossbros-anonymous.io/users/foss_satan",
|
||||
"type": "Create"
|
||||
}`, string(bytes))
|
||||
}`, string(bytes0))
|
||||
|
||||
suite.Equal(`{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": "http://localhost:8080/users/the_mighty_zork",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/activity#vote1/http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6",
|
||||
"object": {
|
||||
"attributedTo": "http://localhost:8080/users/the_mighty_zork",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork#01HEN2R65468ZG657C4ZPHJ4EX/votes/2",
|
||||
"inReplyTo": "http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6",
|
||||
"name": "financial times",
|
||||
"to": "http://fossbros-anonymous.io/users/foss_satan",
|
||||
"type": "Note"
|
||||
},
|
||||
"published": "2021-09-11T11:45:37+02:00",
|
||||
"to": "http://fossbros-anonymous.io/users/foss_satan",
|
||||
"type": "Create"
|
||||
}`, string(bytes1))
|
||||
}
|
||||
|
||||
func (suite *InternalToASTestSuite) TestInteractionReqToASAcceptAnnounce() {
|
||||
|
|
|
@ -1534,6 +1534,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,
|
||||
|
@ -1674,6 +1675,7 @@ 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),
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -54,7 +54,7 @@ func (m *Module) aboutGETHandler(c *gin.Context) {
|
|||
Template: "about.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Stylesheets: []string{cssAbout},
|
||||
Stylesheets: []string{cssAbout, instanceCustomCSSPath},
|
||||
Extra: map[string]any{
|
||||
"showStrap": true,
|
||||
"blocklistExposed": config.GetInstanceExposeSuspendedWeb(),
|
||||
|
|
|
@ -127,8 +127,9 @@ func (m *Module) confirmEmailPOSTHandler(c *gin.Context) {
|
|||
// Serve page informing user that their
|
||||
// email address is now confirmed.
|
||||
page := apiutil.WebPage{
|
||||
Template: "confirmed_email.tmpl",
|
||||
Instance: instance,
|
||||
Template: "confirmed_email.tmpl",
|
||||
Instance: instance,
|
||||
Stylesheets: []string{instanceCustomCSSPath},
|
||||
Extra: map[string]any{
|
||||
"email": user.Email,
|
||||
"username": user.Account.Username,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ func (m *Module) domainBlockListGETHandler(c *gin.Context) {
|
|||
Template: "domain-blocklist.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Stylesheets: []string{cssFA},
|
||||
Stylesheets: []string{cssFA, instanceCustomCSSPath},
|
||||
Javascript: []string{jsFrontend},
|
||||
Extra: map[string]any{"blocklist": domainBlocks},
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ func (m *Module) indexHandler(c *gin.Context) {
|
|||
Template: "index.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Stylesheets: []string{cssAbout, cssIndex},
|
||||
Stylesheets: []string{cssAbout, cssIndex, instanceCustomCSSPath},
|
||||
Extra: map[string]any{"showStrap": true},
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
@ -142,6 +142,7 @@ func (m *Module) profileGETHandler(c *gin.Context) {
|
|||
cssStatus,
|
||||
cssThread,
|
||||
cssProfile,
|
||||
instanceCustomCSSPath,
|
||||
}...,
|
||||
)
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ func (m *Module) SettingsPanelHandler(c *gin.Context) {
|
|||
cssProfile, // Used for rendering stub/fake profiles.
|
||||
cssStatus, // Used for rendering stub/fake statuses.
|
||||
cssSettings,
|
||||
instanceCustomCSSPath,
|
||||
},
|
||||
Javascript: []string{jsSettings},
|
||||
}
|
||||
|
|
|
@ -126,9 +126,10 @@ func (m *Module) signupPOSTHandler(c *gin.Context) {
|
|||
// Serve a page informing the
|
||||
// user that they've signed up.
|
||||
page := apiutil.WebPage{
|
||||
Template: "signed-up.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Template: "signed-up.tmpl",
|
||||
Instance: instance,
|
||||
Stylesheets: []string{instanceCustomCSSPath},
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Extra: map[string]any{
|
||||
"email": user.UnconfirmedEmail,
|
||||
"username": user.Account.Username,
|
||||
|
|
|
@ -59,7 +59,7 @@ func (m *Module) tagGETHandler(c *gin.Context) {
|
|||
Template: "tag.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Stylesheets: []string{cssFA, cssThread, cssTag},
|
||||
Stylesheets: []string{cssFA, cssThread, cssTag, instanceCustomCSSPath},
|
||||
Extra: map[string]any{"tagName": tagName},
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
@ -131,6 +131,7 @@ func (m *Module) threadGETHandler(c *gin.Context) {
|
|||
if theme := targetAccount.Theme; theme != "" {
|
||||
stylesheets = append(
|
||||
stylesheets,
|
||||
instanceCustomCSSPath,
|
||||
themesPathPrefix+"/"+theme,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
16
vendor/codeberg.org/superseriousbusiness/exif-terminator/jpeg.go
generated
vendored
16
vendor/codeberg.org/superseriousbusiness/exif-terminator/jpeg.go
generated
vendored
|
@ -109,17 +109,17 @@ func (v *jpegVisitor) writeSegment(s *jpegstructure.Segment) error {
|
|||
|
||||
sizeLen, found := markerLen[s.MarkerId]
|
||||
if !found || sizeLen == 2 {
|
||||
sizeLen = 2
|
||||
l := uint16(len(s.Data) + sizeLen)
|
||||
|
||||
if err := binary.Write(w, binary.BigEndian, &l); err != nil {
|
||||
l := uint16(len(s.Data) + 2)
|
||||
b := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(b, l)
|
||||
if _, err := w.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
} else if sizeLen == 4 {
|
||||
l := uint32(len(s.Data) + sizeLen)
|
||||
|
||||
if err := binary.Write(w, binary.BigEndian, &l); err != nil {
|
||||
l := uint32(len(s.Data) + 4)
|
||||
b := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(b, l)
|
||||
if _, err := w.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if sizeLen != 0 {
|
||||
|
|
94
vendor/codeberg.org/superseriousbusiness/exif-terminator/webp.go
generated
vendored
94
vendor/codeberg.org/superseriousbusiness/exif-terminator/webp.go
generated
vendored
|
@ -25,17 +25,16 @@
|
|||
)
|
||||
|
||||
const (
|
||||
riffHeaderSize = 4 * 3
|
||||
riffHeader = "RIFF"
|
||||
webpHeader = "WEBP"
|
||||
exifFourcc = "EXIF"
|
||||
xmpFourcc = "XMP "
|
||||
)
|
||||
|
||||
var (
|
||||
riffHeader = [4]byte{'R', 'I', 'F', 'F'}
|
||||
webpHeader = [4]byte{'W', 'E', 'B', 'P'}
|
||||
exifFourcc = [4]byte{'E', 'X', 'I', 'F'}
|
||||
xmpFourcc = [4]byte{'X', 'M', 'P', ' '}
|
||||
|
||||
errNoRiffHeader = errors.New("no RIFF header")
|
||||
errNoWebpHeader = errors.New("not a WEBP file")
|
||||
errInvalidChunk = errors.New("invalid chunk")
|
||||
)
|
||||
|
||||
type webpVisitor struct {
|
||||
|
@ -43,59 +42,68 @@ type webpVisitor struct {
|
|||
doneHeader bool
|
||||
}
|
||||
|
||||
func fourCC(b []byte) [4]byte {
|
||||
return [4]byte{b[0], b[1], b[2], b[3]}
|
||||
}
|
||||
|
||||
func (v *webpVisitor) split(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
// parse/write the header first
|
||||
if !v.doneHeader {
|
||||
if len(data) < riffHeaderSize {
|
||||
// need the full header
|
||||
|
||||
// const rifHeaderSize = 12
|
||||
if len(data) < 12 {
|
||||
if atEOF {
|
||||
err = errNoRiffHeader
|
||||
}
|
||||
return
|
||||
}
|
||||
if fourCC(data) != riffHeader {
|
||||
|
||||
if string(data[:4]) != riffHeader {
|
||||
err = errNoRiffHeader
|
||||
return
|
||||
}
|
||||
if fourCC(data[8:]) != webpHeader {
|
||||
|
||||
if string(data[8:12]) != webpHeader {
|
||||
err = errNoWebpHeader
|
||||
return
|
||||
}
|
||||
if _, err = v.writer.Write(data[:riffHeaderSize]); err != nil {
|
||||
|
||||
if _, err = v.writer.Write(data[:12]); err != nil {
|
||||
return
|
||||
}
|
||||
advance += riffHeaderSize
|
||||
data = data[riffHeaderSize:]
|
||||
|
||||
advance += 12
|
||||
data = data[12:]
|
||||
v.doneHeader = true
|
||||
}
|
||||
|
||||
// need enough for fourcc and size
|
||||
if len(data) < 8 {
|
||||
return
|
||||
}
|
||||
size := int64(binary.LittleEndian.Uint32(data[4:]))
|
||||
if (size & 1) != 0 {
|
||||
// odd chunk size - extra padding byte
|
||||
size++
|
||||
}
|
||||
// wait until there is enough
|
||||
if int64(len(data)-8) < size {
|
||||
return
|
||||
}
|
||||
|
||||
fourcc := fourCC(data)
|
||||
rawChunkData := data[8 : 8+size]
|
||||
if fourcc == exifFourcc || fourcc == xmpFourcc {
|
||||
// replace exif/xmp with blank
|
||||
rawChunkData = make([]byte, size)
|
||||
}
|
||||
|
||||
if _, err = v.writer.Write(data[:8]); err == nil {
|
||||
if _, err = v.writer.Write(rawChunkData); err == nil {
|
||||
advance += 8 + int(size)
|
||||
for {
|
||||
// need enough for
|
||||
// fourcc and size
|
||||
if len(data) < 8 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
size := int64(binary.LittleEndian.Uint32(data[4:]))
|
||||
|
||||
if (size & 1) != 0 {
|
||||
// odd chunk size:
|
||||
// extra padding byte
|
||||
size++
|
||||
}
|
||||
|
||||
// wait until there is enough
|
||||
if int64(len(data)) < 8+size {
|
||||
return
|
||||
}
|
||||
|
||||
// replace exif/xmp with blank
|
||||
switch string(data[:4]) {
|
||||
case exifFourcc, xmpFourcc:
|
||||
clear(data[8 : 8+size])
|
||||
}
|
||||
|
||||
if _, err = v.writer.Write(data[:8+size]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
advance += 8 + int(size)
|
||||
data = data[8+size:]
|
||||
}
|
||||
}
|
||||
|
|
78
vendor/github.com/minio/minio-go/v7/api-prompt-object.go
generated
vendored
Normal file
78
vendor/github.com/minio/minio-go/v7/api-prompt-object.go
generated
vendored
Normal file
|
@ -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
|
||||
}
|
84
vendor/github.com/minio/minio-go/v7/api-prompt-options.go
generated
vendored
Normal file
84
vendor/github.com/minio/minio-go/v7/api-prompt-options.go
generated
vendored
Normal file
|
@ -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
|
||||
}
|
5
vendor/github.com/minio/minio-go/v7/api-put-object-fan-out.go
generated
vendored
5
vendor/github.com/minio/minio-go/v7/api-put-object-fan-out.go
generated
vendored
|
@ -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 {
|
||||
|
|
2
vendor/github.com/minio/minio-go/v7/api.go
generated
vendored
2
vendor/github.com/minio/minio-go/v7/api.go
generated
vendored
|
@ -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.
|
||||
|
|
1907
vendor/github.com/minio/minio-go/v7/functional_tests.go
generated
vendored
1907
vendor/github.com/minio/minio-go/v7/functional_tests.go
generated
vendored
File diff suppressed because it is too large
Load diff
7
vendor/github.com/minio/minio-go/v7/pkg/credentials/sts_web_identity.go
generated
vendored
7
vendor/github.com/minio/minio-go/v7/pkg/credentials/sts_web_identity.go
generated
vendored
|
@ -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
|
||||
|
|
71
vendor/github.com/minio/minio-go/v7/post-policy.go
generated
vendored
71
vendor/github.com/minio/minio-go/v7/post-policy.go
generated
vendored
|
@ -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
|
||||
|
|
10
vendor/github.com/minio/minio-go/v7/retry-continous.go
generated
vendored
10
vendor/github.com/minio/minio-go/v7/retry-continous.go
generated
vendored
|
@ -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<<uint(attempt))
|
||||
if sleep > cap {
|
||||
sleep = cap
|
||||
// sleep = random_between(0, min(maxSleep, base * 2 ** attempt))
|
||||
sleep := baseSleep * time.Duration(1<<uint(attempt))
|
||||
if sleep > maxSleep {
|
||||
sleep = maxSleep
|
||||
}
|
||||
if jitter != NoJitter {
|
||||
sleep -= time.Duration(c.random.Float64() * float64(sleep) * jitter)
|
||||
|
|
10
vendor/github.com/minio/minio-go/v7/retry.go
generated
vendored
10
vendor/github.com/minio/minio-go/v7/retry.go
generated
vendored
|
@ -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<<uint(attempt))
|
||||
if sleep > cap {
|
||||
sleep = cap
|
||||
// sleep = random_between(0, min(maxSleep, base * 2 ** attempt))
|
||||
sleep := baseSleep * time.Duration(1<<uint(attempt))
|
||||
if sleep > maxSleep {
|
||||
sleep = maxSleep
|
||||
}
|
||||
if jitter != NoJitter {
|
||||
sleep -= time.Duration(c.random.Float64() * float64(sleep) * jitter)
|
||||
|
|
4
vendor/modules.txt
vendored
4
vendor/modules.txt
vendored
|
@ -66,7 +66,7 @@ codeberg.org/gruf/go-storage/s3
|
|||
# codeberg.org/gruf/go-structr v0.8.11
|
||||
## explicit; go 1.21
|
||||
codeberg.org/gruf/go-structr
|
||||
# codeberg.org/superseriousbusiness/exif-terminator v0.9.0
|
||||
# codeberg.org/superseriousbusiness/exif-terminator v0.9.1
|
||||
## explicit; go 1.21
|
||||
codeberg.org/superseriousbusiness/exif-terminator
|
||||
# github.com/DmitriyVTitov/size v1.5.0
|
||||
|
@ -488,7 +488,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
|
||||
|
|
10
web/assets/themes/blurple-auto.css
Normal file
10
web/assets/themes/blurple-auto.css
Normal file
|
@ -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);
|
10
web/assets/themes/brutalist-auto.css
Normal file
10
web/assets/themes/brutalist-auto.css
Normal file
|
@ -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);
|
10
web/assets/themes/solarized-auto.css
Normal file
10
web/assets/themes/solarized-auto.css
Normal file
|
@ -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);
|
|
@ -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;
|
||||
|
|
|
@ -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,6 +195,15 @@ function InstanceSettingsForm({ data: instance }: InstanceSettingsFormProps) {
|
|||
type="email"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
field={form.customCSS}
|
||||
label={"Custom CSS"}
|
||||
className="monospace"
|
||||
rows={8}
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
|
||||
<MutationButton label="Save" result={result} disabled={false} />
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -29,27 +29,27 @@
|
|||
<ul class="applist nodot" role="group">
|
||||
<li class="applist-entry">
|
||||
<div class="applist-text">
|
||||
<p><strong>Semaphore</strong> is a web client designed for speed and simplicity.</p>
|
||||
<p><strong>Pinafore</strong> is a web client designed for speed and simplicity.</p>
|
||||
<a
|
||||
href="https://semaphore.social/"
|
||||
href="https://pinafore.social/"
|
||||
rel="nofollow noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Use Semaphore
|
||||
Use Pinafore
|
||||
</a>
|
||||
</div>
|
||||
<svg
|
||||
role="img"
|
||||
aria-labelledby="semaphore-title semaphore-desc"
|
||||
aria-labelledby="pinafore-title pinafore-desc"
|
||||
class="applist-logo redraw"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 146 120"
|
||||
viewBox="0 0 10000 10000"
|
||||
width="100"
|
||||
height="100"
|
||||
>
|
||||
<title id="semaphore-title">The Semaphore logo</title>
|
||||
<desc id="semaphore-desc">A waving flag</desc>
|
||||
<path d="M68.13 0C53.94 0 42.81 20 13.9 27.1l-2.23-5.29a6.5 6.5 0 0 0-5.17-10.4 6.5 6.5 0 0 0-.81 12.95L46.2 120l5.99-2.5-14.42-33.33c22.8-6.86 32.51-22.16 49.83-20.58 9.9.9 4.87 19.56 8.11 17.93 16.22-8.15 32.44-11.41 50.29-11.41-7.96-9.78-17.38-20.55-22.71-31.74L120.8 32c-2.32-7.33-2.56-14.75.87-22.22-9.74-3.26-21.1 0-32.45 4.9C82.2 9.77 79.5 0 68.13 0zM15.26 30.42c8.95 6.63 13.63 13.86 16.07 20.94l1.62 6.32c1.24 6.58 1.07 12.8 1.27 18.03z"></path>
|
||||
<title id="pinafore-title">The Pinafore logo</title>
|
||||
<desc id="pinafore-desc">A sailboat</desc>
|
||||
<path d="M9212 5993H5987V823c1053 667 2747 2177 3225 5170zM3100 2690A12240 12240 0 01939 6035h2161zm676 7210h2448a3067 3067 0 003067-3067H5052V627a527 527 0 00-1052 0v6206H709a3067 3067 0 003067 3067z"></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li class="applist-entry">
|
||||
|
|
Loading…
Reference in a new issue