mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-26 05:36:38 +00:00
Compare commits
8 commits
90e153ada0
...
a0505ae6db
Author | SHA1 | Date | |
---|---|---|---|
a0505ae6db | |||
2c3f1f4ddb | |||
1e421cb912 | |||
40c33ccc49 | |||
90b773ae2a | |||
4b7d7f9b8b | |||
af5a766f62 | |||
d9e59820ed |
|
@ -284,6 +284,12 @@ definitions:
|
|||
example: https://example.org/media/some_user/header/static/header.png
|
||||
type: string
|
||||
x-go-name: HeaderStatic
|
||||
hide_boosts:
|
||||
description: |-
|
||||
Account has opted to hide boosts from their profile.
|
||||
Key/value omitted if false.
|
||||
type: boolean
|
||||
x-go-name: HideBoosts
|
||||
hide_collections:
|
||||
description: |-
|
||||
Account has opted to hide their followers/following collections.
|
||||
|
@ -2289,6 +2295,12 @@ definitions:
|
|||
example: https://example.org/media/some_user/header/static/header.png
|
||||
type: string
|
||||
x-go-name: HeaderStatic
|
||||
hide_boosts:
|
||||
description: |-
|
||||
Account has opted to hide boosts from their profile.
|
||||
Key/value omitted if false.
|
||||
type: boolean
|
||||
x-go-name: HideBoosts
|
||||
hide_collections:
|
||||
description: |-
|
||||
Account has opted to hide their followers/following collections.
|
||||
|
|
|
@ -569,6 +569,7 @@ For example, the following json object `Reject`s the attempt of `@someone@somewh
|
|||
|
||||
```json
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": "https://example.org/users/post_author",
|
||||
"to": "https://somewhere.else.example.org/users/someone",
|
||||
"id": "https://example.org/users/post_author/activities/reject/01J0K2YXP9QCT5BE1JWQSAM3B6",
|
||||
|
@ -591,7 +592,12 @@ For example, the following json object `Accept`s the attempt of `@someone@somewh
|
|||
|
||||
```json
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": "https://example.org/users/post_author",
|
||||
"cc": [
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
"https://example.org/users/post_author/followers"
|
||||
],
|
||||
"to": "https://somewhere.else.example.org/users/someone",
|
||||
"id": "https://example.org/users/post_author/activities/reject/01J0K2YXP9QCT5BE1JWQSAM3B6",
|
||||
"object": "https://somewhere.else.example.org/users/someone/statuses/01J17XY2VXGMNNPH1XR7BG2524",
|
||||
|
@ -601,6 +607,9 @@ For example, the following json object `Accept`s the attempt of `@someone@somewh
|
|||
|
||||
If this happens, `@someone@somewhere.else.example.org` (and their instance) should consider the interaction as having been approved / accepted. The instance can then feel free to distribute the interaction `Activity` to all of the recipients targed by `to`, `cc`, etc, with the additional property `approvedBy` ([see below](#approvedby)).
|
||||
|
||||
!!! Note
|
||||
In the above example, actor `https://example.org/users/post_author` addresses the `Accept` activity not just to the interacting actor `https://somewhere.else.example.org/users/someone`, but to their followers collection as well (and, implicitly, to the public). This allows followers of `https://example.org/users/post_author` on other servers to also mark the interaction as accepted, and to show the interaction alongside the interacted-with post.
|
||||
|
||||
### Validating presence in a Followers / Following collection
|
||||
|
||||
If an `Actor` interacting with an `Object` (via `Like`, `inReplyTo`, or `Announce`) is permitted to do that interaction based on their presence in a `Followers` or `Following` collection in the `always` field of an interaction policy, then their server should *still* wait for an `Accept` to be received from the server of the target account, before distributing the interaction more widely with the `approvedBy` property set to the URI of the `Accept`.
|
||||
|
|
|
@ -88,9 +88,9 @@ This setting does not affect visibility of your posts over the ActivityPub proto
|
|||
|
||||
!!! warning
|
||||
Be aware that changes to this setting also apply retroactively.
|
||||
|
||||
|
||||
That is, if you previously made a post on Unlisted visibility, while set to show only Public posts on your profile, and you change this setting to show Public and Unlisted, then the Unlisted post you previously made will be visible on your profile alongside your Public posts.
|
||||
|
||||
|
||||
Likewise, if you change this setting to show no posts, then all your posts will be hidden from your profile, regardless of when you created them, and what this option was set to at the time. This will apply until you change this setting again.
|
||||
|
||||
!!! tip
|
||||
|
@ -134,6 +134,11 @@ This feed only includes posts set as 'Public' (see [Privacy Settings](./posts.md
|
|||
!!! warning
|
||||
Exposing your RSS feed allows *anyone* to subscribe to updates on your Public posts anonymously, bypassing follows and follow requests.
|
||||
|
||||
#### Hide boosts from your public page
|
||||
|
||||
By default, GoToSocial will display posts boosted by you on your public web profile. If you do not wish to display them, You can hide them by checking this box.
|
||||
|
||||
|
||||
#### Hide Who You Follow / Are Followed By
|
||||
|
||||
By default, GoToSocial shows your following/followers counts on your public web profile, and allows others to see who you follow and are followed by. This can be useful for account discovery purposes. However, for privacy + safety reasons you may wish to hide these counts, and to hide your following/followers lists from other accounts. You can do this by checking this box.
|
||||
|
@ -196,7 +201,7 @@ If you want to reset all your policies to the initial defaults, you can click on
|
|||
|
||||
!!! danger
|
||||
While GoToSocial respects interaction policies, it is not guaranteed that other server softwares will, and it is possible that accounts on other servers will still send out replies and boosts of your post to their followers, even if your instance forbids these interactions.
|
||||
|
||||
|
||||
As more ActivityPub servers roll out support for interaction policies, this issue will hopefully diminish, but in the meantime GoToSocial can offer only a "best effort" attempt to restrict interactions with your posts according to the policies you have set.
|
||||
|
||||
## Email & Password
|
||||
|
|
2
go.mod
2
go.mod
|
@ -44,7 +44,7 @@ require (
|
|||
github.com/miekg/dns v1.1.62
|
||||
github.com/minio/minio-go/v7 v7.0.77
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/ncruces/go-sqlite3 v0.18.4
|
||||
github.com/ncruces/go-sqlite3 v0.19.0
|
||||
github.com/oklog/ulid v1.3.1
|
||||
github.com/prometheus/client_golang v1.20.4
|
||||
github.com/spf13/cobra v1.8.1
|
||||
|
|
4
go.sum
4
go.sum
|
@ -434,8 +434,8 @@ github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
|
|||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/ncruces/go-sqlite3 v0.18.4 h1:Je8o3y33MDwPYY/Cacas8yCsuoUzpNY/AgoSlN2ekyE=
|
||||
github.com/ncruces/go-sqlite3 v0.18.4/go.mod h1:4HLag13gq1k10s4dfGBhMfRVsssJRT9/5hYqVM9RUYo=
|
||||
github.com/ncruces/go-sqlite3 v0.19.0 h1:yebbD/cP8Gf+7nKoUin2ATjnqJK2VvyS30d3xsjRp5k=
|
||||
github.com/ncruces/go-sqlite3 v0.19.0/go.mod h1:yL4ZNWGsr1/8pcLfpPW1RT1WFdvyeHonrgIwwi4rvkg=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
|
|
|
@ -77,6 +77,10 @@
|
|||
// See https://www.w3.org/TR/activitystreams-vocabulary/#microsyntaxes
|
||||
// and https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tag
|
||||
TagHashtag = "Hashtag"
|
||||
|
||||
// Not in the AS spec, just used internally to indicate
|
||||
// that we don't *yet* know what type of Object something is.
|
||||
ObjectUnknown = "Unknown"
|
||||
)
|
||||
|
||||
// isActivity returns whether AS type name is of an Activity (NOT IntransitiveActivity).
|
||||
|
|
|
@ -348,6 +348,7 @@ func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest,
|
|||
form.Theme == nil &&
|
||||
form.CustomCSS == nil &&
|
||||
form.EnableRSS == nil &&
|
||||
form.HideBoosts == nil &&
|
||||
form.HideCollections == nil &&
|
||||
form.WebVisibility == nil) {
|
||||
return nil, errors.New("empty form submitted")
|
||||
|
|
|
@ -104,6 +104,9 @@ type Account struct {
|
|||
// Account has enabled RSS feed.
|
||||
// Key/value omitted if false.
|
||||
EnableRSS bool `json:"enable_rss,omitempty"`
|
||||
// Account has opted to hide boosts from their profile.
|
||||
// Key/value omitted if false.
|
||||
HideBoosts bool `json:"hide_boosts,omitempty"`
|
||||
// Account has opted to hide their followers/following collections.
|
||||
// Key/value omitted if false.
|
||||
HideCollections bool `json:"hide_collections,omitempty"`
|
||||
|
@ -225,6 +228,8 @@ type UpdateCredentialsRequest struct {
|
|||
CustomCSS *string `form:"custom_css" json:"custom_css"`
|
||||
// Enable RSS feed of public toots for this account at /@[username]/feed.rss
|
||||
EnableRSS *bool `form:"enable_rss" json:"enable_rss"`
|
||||
// Hide boosts from this account's profile page.
|
||||
HideBoosts *bool `form:"hide_boosts" json:"hide_boosts"`
|
||||
// Hide this account's following/followers collections.
|
||||
HideCollections *bool `form:"hide_collections" json:"hide_collections"`
|
||||
// Visibility of statuses to show via the web view.
|
||||
|
|
|
@ -118,6 +118,10 @@ type WebStatus struct {
|
|||
// Override API account with web account.
|
||||
Account *WebAccount `json:"account"`
|
||||
|
||||
// Account that reblogged the status.
|
||||
// needed to properly render reblogged statuses on profile pages.
|
||||
ReblogAccount *WebAccount `json:"reblog_account"`
|
||||
|
||||
// Web version of media
|
||||
// attached to this status.
|
||||
MediaAttachments []*WebAttachment `json:"media_attachments"`
|
||||
|
|
|
@ -1017,6 +1017,7 @@ func (a *accountDB) GetAccountWebStatuses(
|
|||
) ([]*gtsmodel.Status, error) {
|
||||
// Check for an easy case: account exposes no statuses via the web.
|
||||
webVisibility := account.Settings.WebVisibility
|
||||
hideBoosts := *account.Settings.HideBoosts
|
||||
if webVisibility == gtsmodel.VisibilityNone {
|
||||
return nil, db.ErrNoEntries
|
||||
}
|
||||
|
@ -1035,9 +1036,12 @@ func (a *accountDB) GetAccountWebStatuses(
|
|||
// Select only IDs from table
|
||||
Column("status.id").
|
||||
Where("? = ?", bun.Ident("status.account_id"), account.ID).
|
||||
// Don't show replies or boosts.
|
||||
Where("? IS NULL", bun.Ident("status.in_reply_to_uri")).
|
||||
Where("? IS NULL", bun.Ident("status.boost_of_id"))
|
||||
// Don't show replies.
|
||||
Where("? IS NULL", bun.Ident("status.in_reply_to_uri"))
|
||||
|
||||
if hideBoosts {
|
||||
q = q.Where("? IS NULL", bun.Ident("status.boost_of_id"))
|
||||
}
|
||||
|
||||
// Select statuses for this account according
|
||||
// to their web visibility preference.
|
||||
|
|
|
@ -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 ? BOOLEAN DEFAULT FALSE", bun.Ident("account_settings"), bun.Ident("hide_boosts"))
|
||||
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("account_settings"), bun.Ident("hide_boosts"))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -527,8 +527,9 @@ func (d *Dereferencer) enrichStatus(
|
|||
// serve statuses with the `approved_by` field, but we
|
||||
// might have marked a status as pre-approved on our side
|
||||
// based on the author's inclusion in a followers/following
|
||||
// collection. By carrying over previously-set values we
|
||||
// can avoid marking such statuses as "pending" again.
|
||||
// collection, or by providing pre-approval URI on the bare
|
||||
// status passed to RefreshStatus. By carrying over previously
|
||||
// set values we can avoid marking such statuses as "pending".
|
||||
//
|
||||
// If a remote has in the meantime retracted its approval,
|
||||
// the next call to 'isPermittedStatus' will catch that.
|
||||
|
|
|
@ -113,33 +113,17 @@ func (d *Dereferencer) isPermittedStatus(
|
|||
func (d *Dereferencer) isPermittedReply(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
status *gtsmodel.Status,
|
||||
reply *gtsmodel.Status,
|
||||
) (bool, error) {
|
||||
var (
|
||||
statusURI = status.URI // Definitely set.
|
||||
inReplyToURI = status.InReplyToURI // Definitely set.
|
||||
inReplyTo = status.InReplyTo // Might not yet be set.
|
||||
replyURI = reply.URI // Definitely set.
|
||||
inReplyToURI = reply.InReplyToURI // Definitely set.
|
||||
inReplyTo = reply.InReplyTo // Might not be set.
|
||||
acceptIRI = reply.ApprovedByURI // Might not be set.
|
||||
)
|
||||
|
||||
// Check if status with this URI has previously been rejected.
|
||||
req, err := d.state.DB.GetInteractionRequestByInteractionURI(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
statusURI,
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error getting interaction request: %w", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if req != nil && req.IsRejected() {
|
||||
// This status has been
|
||||
// rejected reviously, so
|
||||
// it's not permitted now.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check if replied-to status has previously been rejected.
|
||||
req, err = d.state.DB.GetInteractionRequestByInteractionURI(
|
||||
// Check if we have a stored interaction request for parent status.
|
||||
parentReq, err := d.state.DB.GetInteractionRequestByInteractionURI(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
inReplyToURI,
|
||||
)
|
||||
|
@ -148,71 +132,78 @@ func (d *Dereferencer) isPermittedReply(
|
|||
return false, err
|
||||
}
|
||||
|
||||
if req != nil && req.IsRejected() {
|
||||
// This status's parent was rejected, so
|
||||
// implicitly this reply should be rejected too.
|
||||
//
|
||||
// We know already that we haven't inserted
|
||||
// a rejected interaction request for this
|
||||
// status yet so do it before returning.
|
||||
id := id.NewULID()
|
||||
// Check if we have a stored interaction request for this reply.
|
||||
thisReq, err := d.state.DB.GetInteractionRequestByInteractionURI(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
replyURI,
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error getting interaction request: %w", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// To ensure the Reject chain stays coherent,
|
||||
// borrow fields from the up-thread rejection.
|
||||
// This collapses the chain beyond the first
|
||||
// rejected reply and allows us to avoid derefing
|
||||
// further replies we already know we don't want.
|
||||
statusID := req.StatusID
|
||||
targetAccountID := req.TargetAccountID
|
||||
parentRejected := (parentReq != nil && parentReq.IsRejected())
|
||||
thisRejected := (thisReq != nil && thisReq.IsRejected())
|
||||
|
||||
// As nobody is actually Rejecting the reply
|
||||
// directly, but it's an implicit Reject coming
|
||||
// from our internal logic, don't bother setting
|
||||
// a URI (it's not a required field anyway).
|
||||
uri := ""
|
||||
|
||||
rejection := >smodel.InteractionRequest{
|
||||
ID: id,
|
||||
StatusID: statusID,
|
||||
TargetAccountID: targetAccountID,
|
||||
InteractingAccountID: status.AccountID,
|
||||
InteractionURI: statusURI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
URI: uri,
|
||||
RejectedAt: time.Now(),
|
||||
}
|
||||
err := d.state.DB.PutInteractionRequest(ctx, rejection)
|
||||
if err != nil && !errors.Is(err, db.ErrAlreadyExists) {
|
||||
return false, gtserror.Newf("db error putting pre-rejected interaction request: %w", err)
|
||||
}
|
||||
if parentRejected {
|
||||
// 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,
|
||||
reply,
|
||||
thisReq,
|
||||
parentReq,
|
||||
)
|
||||
}
|
||||
|
||||
// Parent wasn't rejected. Check if this
|
||||
// reply itself was rejected previously.
|
||||
//
|
||||
// If it was, and it doesn't now claim to
|
||||
// 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
|
||||
}
|
||||
|
||||
// This reply wasn't rejected previously, or
|
||||
// it was rejected previously and now claims
|
||||
// to be approved. Continue permission checks.
|
||||
|
||||
if inReplyTo == nil {
|
||||
// We didn't have the replied-to status in
|
||||
// our database (yet) so we can't know if
|
||||
// this reply is permitted or not. For now
|
||||
// just return true; worst-case, the status
|
||||
// sticks around on the instance for a couple
|
||||
// hours until we try to dereference it again
|
||||
// and realize it should be forbidden.
|
||||
return true, 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.
|
||||
//
|
||||
// For now, just return permitted if reply
|
||||
// was not explicitly rejected before; worst-
|
||||
// case, the reply stays on the instance for
|
||||
// a couple hours until we try to deref it
|
||||
// again and realize it should be forbidden.
|
||||
return !thisRejected, nil
|
||||
}
|
||||
|
||||
// We have the replied-to status; ensure it's fully populated.
|
||||
if err := d.state.DB.PopulateStatus(ctx, inReplyTo); err != nil {
|
||||
return false, gtserror.Newf("error populating status %s: %w", reply.ID, err)
|
||||
}
|
||||
|
||||
// Make sure replied-to status is not
|
||||
// a boost wrapper, and make sure it's
|
||||
// actually visible to the requester.
|
||||
if inReplyTo.BoostOfID != "" {
|
||||
// We do not permit replies to
|
||||
// boost wrapper statuses. (this
|
||||
// shouldn't be able to happen).
|
||||
// We do not permit replies
|
||||
// to boost wrapper statuses.
|
||||
log.Info(ctx, "rejecting reply to boost wrapper status")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check visibility of local
|
||||
// inReplyTo to replying account.
|
||||
if inReplyTo.IsLocal() {
|
||||
visible, err := d.visFilter.StatusVisible(ctx,
|
||||
status.Account,
|
||||
reply.Account,
|
||||
inReplyTo,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -227,9 +218,26 @@ func (d *Dereferencer) isPermittedReply(
|
|||
}
|
||||
}
|
||||
|
||||
// Check interaction policy of inReplyTo.
|
||||
// If this reply claims to be approved,
|
||||
// validate this by dereferencing the
|
||||
// Accept and checking the return value.
|
||||
// No further checks are required.
|
||||
if acceptIRI != "" {
|
||||
return d.isPermittedByAcceptIRI(
|
||||
ctx,
|
||||
requestUser,
|
||||
reply,
|
||||
inReplyTo,
|
||||
thisReq,
|
||||
acceptIRI,
|
||||
)
|
||||
}
|
||||
|
||||
// Status doesn't claim to be approved.
|
||||
// Check interaction policy of inReplyTo
|
||||
// to see if it doesn't require approval.
|
||||
replyable, err := d.intFilter.StatusReplyable(ctx,
|
||||
status.Account,
|
||||
reply.Account,
|
||||
inReplyTo,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -238,93 +246,250 @@ func (d *Dereferencer) isPermittedReply(
|
|||
}
|
||||
|
||||
if replyable.Forbidden() {
|
||||
// Reply is not permitted.
|
||||
// Reply is not permitted according to policy.
|
||||
//
|
||||
// Insert a pre-rejected interaction request
|
||||
// into the db and return. This ensures that
|
||||
// replies to this now-rejected status aren't
|
||||
// inadvertently permitted.
|
||||
id := id.NewULID()
|
||||
rejection := >smodel.InteractionRequest{
|
||||
ID: id,
|
||||
StatusID: inReplyTo.ID,
|
||||
TargetAccountID: inReplyTo.AccountID,
|
||||
InteractingAccountID: status.AccountID,
|
||||
InteractionURI: statusURI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
URI: uris.GenerateURIForReject(inReplyTo.Account.Username, id),
|
||||
RejectedAt: time.Now(),
|
||||
}
|
||||
err := d.state.DB.PutInteractionRequest(ctx, rejection)
|
||||
if err != nil && !errors.Is(err, db.ErrAlreadyExists) {
|
||||
return false, gtserror.Newf("db error putting pre-rejected interaction request: %w", err)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
// Either insert a pre-rejected interaction
|
||||
// req into the db, or update the existing
|
||||
// one, and return. This ensures that replies
|
||||
// to this rejected reply also aren't permitted.
|
||||
return false, d.rejectedByPolicy(
|
||||
ctx,
|
||||
reply,
|
||||
inReplyTo,
|
||||
thisReq,
|
||||
)
|
||||
}
|
||||
|
||||
if replyable.Permitted() &&
|
||||
!replyable.MatchedOnCollection() {
|
||||
// Replier is permitted to do this
|
||||
// interaction, and didn't match on
|
||||
// a collection so we don't need to
|
||||
// do further checking.
|
||||
// Reply is permitted according to the interaction
|
||||
// policy set on the replied-to status (if any).
|
||||
|
||||
if !replyable.MatchedOnCollection() {
|
||||
// If we didn't match on a collection,
|
||||
// then we don't require an acceptIRI,
|
||||
// and we don't need to send an Accept;
|
||||
// just permit the reply full stop.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Replier is permitted to do this
|
||||
// interaction pending approval, or
|
||||
// permitted but matched on a collection.
|
||||
// Reply is permitted, but match was made based
|
||||
// on inclusion in a followers/following collection.
|
||||
//
|
||||
// Check if we can dereference
|
||||
// an Accept that grants approval.
|
||||
|
||||
if status.ApprovedByURI == "" {
|
||||
// Status doesn't claim to be approved.
|
||||
//
|
||||
// For replies to local statuses that's
|
||||
// fine, we can put it in the DB pending
|
||||
// approval, and continue processing it.
|
||||
//
|
||||
// If permission was granted based on a match
|
||||
// with a followers or following collection,
|
||||
// we can mark it as PreApproved so the processor
|
||||
// sends an accept out for it immediately.
|
||||
//
|
||||
// For replies to remote statuses, though
|
||||
// we should be polite and just drop it.
|
||||
if inReplyTo.IsLocal() {
|
||||
status.PendingApproval = util.Ptr(true)
|
||||
status.PreApproved = replyable.MatchedOnCollection()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
// If the status is ours, mark it as PreApproved
|
||||
// so the processor knows to create and send out
|
||||
// an Accept for it immediately.
|
||||
if inReplyTo.IsLocal() {
|
||||
reply.PendingApproval = util.Ptr(true)
|
||||
reply.PreApproved = true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Status claims to be approved, check
|
||||
// this by dereferencing the Accept and
|
||||
// inspecting the return value.
|
||||
if err := d.validateApprovedBy(
|
||||
// For replies to remote statuses, which matched
|
||||
// on a followers/following collection, but did not
|
||||
// include an acceptIRI, we should just drop it.
|
||||
// It's possible we'll get an Accept for it later
|
||||
// and we can check everything again.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// unpermittedByParent marks the given reply as rejected
|
||||
// based on the fact that its parent was rejected.
|
||||
//
|
||||
// This will create a rejected interaction request for
|
||||
// the status in the db, if one didn't exist already,
|
||||
// or update an existing interaction request instead.
|
||||
func (d *Dereferencer) unpermittedByParent(
|
||||
ctx context.Context,
|
||||
reply *gtsmodel.Status,
|
||||
thisReq *gtsmodel.InteractionRequest,
|
||||
parentReq *gtsmodel.InteractionRequest,
|
||||
) error {
|
||||
if thisReq != nil && thisReq.IsRejected() {
|
||||
// This interaction request is
|
||||
// already marked as rejected,
|
||||
// there's nothing more to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
if thisReq != nil {
|
||||
// Before we return, ensure interaction
|
||||
// request is marked as rejected.
|
||||
thisReq.RejectedAt = time.Now()
|
||||
thisReq.AcceptedAt = time.Time{}
|
||||
err := d.state.DB.UpdateInteractionRequest(
|
||||
ctx,
|
||||
thisReq,
|
||||
"rejected_at",
|
||||
"accepted_at",
|
||||
)
|
||||
if err != nil {
|
||||
return gtserror.Newf("db error updating interaction request: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// We haven't stored a rejected interaction
|
||||
// request for this status yet, do it now.
|
||||
rejectID := id.NewULID()
|
||||
|
||||
// To ensure the Reject chain stays coherent,
|
||||
// borrow fields from the up-thread rejection.
|
||||
// This collapses the chain beyond the first
|
||||
// rejected reply and allows us to avoid derefing
|
||||
// further replies we already know we don't want.
|
||||
inReplyToID := parentReq.StatusID
|
||||
targetAccountID := parentReq.TargetAccountID
|
||||
|
||||
// As nobody is actually Rejecting the reply
|
||||
// directly, but it's an implicit Reject coming
|
||||
// from our internal logic, don't bother setting
|
||||
// a URI (it's not a required field anyway).
|
||||
uri := ""
|
||||
|
||||
rejection := >smodel.InteractionRequest{
|
||||
ID: rejectID,
|
||||
StatusID: inReplyToID,
|
||||
TargetAccountID: targetAccountID,
|
||||
InteractingAccountID: reply.AccountID,
|
||||
InteractionURI: reply.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
URI: uri,
|
||||
RejectedAt: time.Now(),
|
||||
}
|
||||
err := d.state.DB.PutInteractionRequest(ctx, rejection)
|
||||
if err != nil && !errors.Is(err, db.ErrAlreadyExists) {
|
||||
return gtserror.Newf("db error putting pre-rejected interaction request: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isPermittedByAcceptIRI checks whether the given acceptIRI
|
||||
// permits the given reply to the given inReplyTo status.
|
||||
// If yes, then thisReq will be updated to reflect the
|
||||
// acceptance, if it's not nil.
|
||||
func (d *Dereferencer) isPermittedByAcceptIRI(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
reply *gtsmodel.Status,
|
||||
inReplyTo *gtsmodel.Status,
|
||||
thisReq *gtsmodel.InteractionRequest,
|
||||
acceptIRI string,
|
||||
) (bool, error) {
|
||||
permitted, err := d.isValidAccept(
|
||||
ctx,
|
||||
requestUser,
|
||||
status.ApprovedByURI,
|
||||
statusURI,
|
||||
acceptIRI,
|
||||
reply.URI,
|
||||
inReplyTo.AccountURI,
|
||||
); err != nil {
|
||||
|
||||
)
|
||||
if err != nil {
|
||||
// Error dereferencing means we couldn't
|
||||
// get the Accept right now or it wasn't
|
||||
// valid, so we shouldn't store this status.
|
||||
log.Errorf(ctx, "undereferencable ApprovedByURI: %v", err)
|
||||
err := gtserror.Newf("undereferencable ApprovedByURI: %w", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !permitted {
|
||||
// It's a no from
|
||||
// us, squirt.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Status has been approved.
|
||||
status.PendingApproval = util.Ptr(false)
|
||||
// Reply is permitted by this Accept.
|
||||
// If it was previously rejected or
|
||||
// pending approval, clear that now.
|
||||
reply.PendingApproval = util.Ptr(false)
|
||||
if thisReq != nil {
|
||||
thisReq.URI = acceptIRI
|
||||
thisReq.AcceptedAt = time.Now()
|
||||
thisReq.RejectedAt = time.Time{}
|
||||
err := d.state.DB.UpdateInteractionRequest(
|
||||
ctx,
|
||||
thisReq,
|
||||
"uri",
|
||||
"accepted_at",
|
||||
"rejected_at",
|
||||
)
|
||||
if err != nil {
|
||||
return false, gtserror.Newf("db error updating interaction request: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// All good!
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (d *Dereferencer) rejectedByPolicy(
|
||||
ctx context.Context,
|
||||
reply *gtsmodel.Status,
|
||||
inReplyTo *gtsmodel.Status,
|
||||
thisReq *gtsmodel.InteractionRequest,
|
||||
) error {
|
||||
var (
|
||||
rejectID string
|
||||
rejectURI string
|
||||
)
|
||||
|
||||
if thisReq != nil {
|
||||
// Reuse existing ID.
|
||||
rejectID = thisReq.ID
|
||||
} else {
|
||||
// Generate new ID.
|
||||
rejectID = id.NewULID()
|
||||
}
|
||||
|
||||
if inReplyTo.IsLocal() {
|
||||
// If this a reply to one of our statuses
|
||||
// we should generate a URI for the Reject,
|
||||
// else just use an implicit (empty) URI.
|
||||
rejectURI = uris.GenerateURIForReject(
|
||||
inReplyTo.Account.Username,
|
||||
rejectID,
|
||||
)
|
||||
}
|
||||
|
||||
if thisReq != nil {
|
||||
// Before we return, ensure interaction
|
||||
// request is marked as rejected.
|
||||
thisReq.RejectedAt = time.Now()
|
||||
thisReq.AcceptedAt = time.Time{}
|
||||
thisReq.URI = rejectURI
|
||||
err := d.state.DB.UpdateInteractionRequest(
|
||||
ctx,
|
||||
thisReq,
|
||||
"rejected_at",
|
||||
"accepted_at",
|
||||
"uri",
|
||||
)
|
||||
if err != nil {
|
||||
return gtserror.Newf("db error updating interaction request: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// We haven't stored a rejected interaction
|
||||
// request for this status yet, do it now.
|
||||
rejection := >smodel.InteractionRequest{
|
||||
ID: rejectID,
|
||||
StatusID: inReplyTo.ID,
|
||||
TargetAccountID: inReplyTo.AccountID,
|
||||
InteractingAccountID: reply.AccountID,
|
||||
InteractionURI: reply.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
URI: rejectURI,
|
||||
RejectedAt: time.Now(),
|
||||
}
|
||||
err := d.state.DB.PutInteractionRequest(ctx, rejection)
|
||||
if err != nil && !errors.Is(err, db.ErrAlreadyExists) {
|
||||
return gtserror.Newf("db error putting pre-rejected interaction request: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dereferencer) isPermittedBoost(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
|
@ -418,18 +583,22 @@ func (d *Dereferencer) isPermittedBoost(
|
|||
// Boost claims to be approved, check
|
||||
// this by dereferencing the Accept and
|
||||
// inspecting the return value.
|
||||
if err := d.validateApprovedBy(
|
||||
permitted, err := d.isValidAccept(
|
||||
ctx,
|
||||
requestUser,
|
||||
status.ApprovedByURI,
|
||||
status.URI,
|
||||
boostOf.AccountURI,
|
||||
); err != nil {
|
||||
|
||||
)
|
||||
if err != nil {
|
||||
// Error dereferencing means we couldn't
|
||||
// get the Accept right now or it wasn't
|
||||
// valid, so we shouldn't store this status.
|
||||
log.Errorf(ctx, "undereferencable ApprovedByURI: %v", err)
|
||||
err := gtserror.Newf("undereferencable ApprovedByURI: %w", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !permitted {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
@ -438,43 +607,59 @@ func (d *Dereferencer) isPermittedBoost(
|
|||
return true, nil
|
||||
}
|
||||
|
||||
// validateApprovedBy dereferences the activitystreams Accept at
|
||||
// the specified IRI, and checks the Accept for validity against
|
||||
// the provided expectedObject and expectedActor.
|
||||
// isValidAccept dereferences the activitystreams Accept at the
|
||||
// specified IRI, and checks the Accept for validity against the
|
||||
// provided expectedObject and expectedActor.
|
||||
//
|
||||
// Will return either nil if everything looked OK, or an error if
|
||||
// something went wrong during deref, or if the dereffed Accept
|
||||
// did not meet expectations.
|
||||
func (d *Dereferencer) validateApprovedBy(
|
||||
// Will return either (true, nil) if everything looked OK, an error
|
||||
// if something went wrong internally during deref, or (false, nil)
|
||||
// if the dereferenced Accept did not meet expectations.
|
||||
func (d *Dereferencer) isValidAccept(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
approvedByURIStr string, // Eg., "https://example.org/users/someone/accepts/01J2736AWWJ3411CPR833F6D03"
|
||||
acceptIRIStr string, // Eg., "https://example.org/users/someone/accepts/01J2736AWWJ3411CPR833F6D03"
|
||||
expectObjectURIStr string, // Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R"
|
||||
expectActorURIStr string, // Eg., "https://example.org/users/someone"
|
||||
) error {
|
||||
approvedByURI, err := url.Parse(approvedByURIStr)
|
||||
) (bool, error) {
|
||||
l := log.
|
||||
WithContext(ctx).
|
||||
WithField("acceptIRI", acceptIRIStr)
|
||||
|
||||
acceptIRI, err := url.Parse(acceptIRIStr)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error parsing approvedByURI: %w", err)
|
||||
return err
|
||||
// Real returnable error.
|
||||
err := gtserror.Newf("error parsing acceptIRI: %w", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Don't make calls to the remote if it's blocked.
|
||||
if blocked, err := d.state.DB.IsDomainBlocked(ctx, approvedByURI.Host); blocked || err != nil {
|
||||
err := gtserror.Newf("domain %s is blocked", approvedByURI.Host)
|
||||
return err
|
||||
// Don't make calls to the Accept IRI
|
||||
// if it's blocked, just return false.
|
||||
blocked, err := d.state.DB.IsDomainBlocked(ctx, acceptIRI.Host)
|
||||
if err != nil {
|
||||
// Real returnable error.
|
||||
err := gtserror.Newf("error checking domain block: %w", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if blocked {
|
||||
l.Info("Accept host is blocked")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
tsport, err := d.transportController.NewTransportForUsername(ctx, requestUser)
|
||||
if err != nil {
|
||||
// Real returnable error.
|
||||
err := gtserror.Newf("error creating transport: %w", err)
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Make the call to resolve into an Acceptable.
|
||||
rsp, err := tsport.Dereference(ctx, approvedByURI)
|
||||
// Log any error encountered here but don't
|
||||
// return it as it's not *our* error.
|
||||
rsp, err := tsport.Dereference(ctx, acceptIRI)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error dereferencing %s: %w", approvedByURIStr, err)
|
||||
return err
|
||||
l.Errorf("error dereferencing Accept: %v", err)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
acceptable, err := ap.ResolveAcceptable(ctx, rsp.Body)
|
||||
|
@ -483,66 +668,71 @@ func (d *Dereferencer) validateApprovedBy(
|
|||
_ = rsp.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error resolving Accept %s: %w", approvedByURIStr, err)
|
||||
return err
|
||||
l.Errorf("error resolving to Accept: %v", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Extract the URI/ID of the Accept.
|
||||
acceptURI := ap.GetJSONLDId(acceptable)
|
||||
acceptURIStr := acceptURI.String()
|
||||
acceptID := ap.GetJSONLDId(acceptable)
|
||||
acceptIDStr := acceptID.String()
|
||||
|
||||
// Check whether input URI and final returned URI
|
||||
// have changed (i.e. we followed some redirects).
|
||||
rspURL := rsp.Request.URL
|
||||
rspURLStr := rspURL.String()
|
||||
switch {
|
||||
case rspURLStr == approvedByURIStr:
|
||||
if rspURLStr != acceptIRIStr {
|
||||
// If rspURLStr != acceptIRIStr, make sure final
|
||||
// response URL is at least on the same host as
|
||||
// what we expected (ie., we weren't redirected
|
||||
// across domains), and make sure it's the same
|
||||
// as the ID of the Accept we were returned.
|
||||
switch {
|
||||
case rspURL.Host != acceptIRI.Host:
|
||||
l.Errorf(
|
||||
"final deref host %s did not match acceptIRI host",
|
||||
rspURL.Host,
|
||||
)
|
||||
return false, nil
|
||||
|
||||
// i.e. from here, rspURLStr != approvedByURIStr.
|
||||
//
|
||||
// Make sure it's at least on the same host as
|
||||
// what we expected (ie., we weren't redirected
|
||||
// across domains), and make sure it's the same
|
||||
// as the ID of the Accept we were returned.
|
||||
case rspURL.Host != approvedByURI.Host:
|
||||
return gtserror.Newf(
|
||||
"final dereference host %s did not match approvedByURI host %s",
|
||||
rspURL.Host, approvedByURI.Host,
|
||||
)
|
||||
case acceptURIStr != rspURLStr:
|
||||
return gtserror.Newf(
|
||||
"final dereference uri %s did not match returned Accept ID/URI %s",
|
||||
rspURLStr, acceptURIStr,
|
||||
)
|
||||
case acceptIDStr != rspURLStr:
|
||||
l.Errorf(
|
||||
"final deref uri %s did not match returned Accept ID %s",
|
||||
rspURLStr, acceptIDStr,
|
||||
)
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Response is superficially OK,
|
||||
// check in more detail now.
|
||||
|
||||
// Extract the actor IRI and string from Accept.
|
||||
actorIRIs := ap.GetActorIRIs(acceptable)
|
||||
actorIRI, actorIRIStr := extractIRI(actorIRIs)
|
||||
switch {
|
||||
case actorIRIStr == "":
|
||||
err := gtserror.New("missing Accept actor IRI")
|
||||
return gtserror.SetMalformed(err)
|
||||
l.Error("Accept missing actor IRI")
|
||||
return false, nil
|
||||
|
||||
// Ensure the Accept Actor is who we expect
|
||||
// it to be, and not someone else trying to
|
||||
// do an Accept for an interaction with a
|
||||
// statusable they don't own.
|
||||
case actorIRI.Host != acceptURI.Host:
|
||||
return gtserror.Newf(
|
||||
"Accept Actor %s was not the same host as Accept %s",
|
||||
actorIRIStr, acceptURIStr,
|
||||
// Ensure the Accept Actor is on
|
||||
// the instance hosting the Accept.
|
||||
case actorIRI.Host != acceptID.Host:
|
||||
l.Errorf(
|
||||
"actor %s not on the same host as Accept",
|
||||
actorIRIStr,
|
||||
)
|
||||
return false, nil
|
||||
|
||||
// Ensure the Accept Actor is who we expect
|
||||
// it to be, and not someone else trying to
|
||||
// do an Accept for an interaction with a
|
||||
// statusable they don't own.
|
||||
case actorIRIStr != expectActorURIStr:
|
||||
return gtserror.Newf(
|
||||
"Accept Actor %s was not the same as expected actor %s",
|
||||
l.Errorf(
|
||||
"actor %s was not the same as expected actor %s",
|
||||
actorIRIStr, expectActorURIStr,
|
||||
)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Extract the object IRI string from Accept.
|
||||
|
@ -550,20 +740,22 @@ func (d *Dereferencer) validateApprovedBy(
|
|||
_, objectIRIStr := extractIRI(objectIRIs)
|
||||
switch {
|
||||
case objectIRIStr == "":
|
||||
err := gtserror.New("missing Accept object IRI")
|
||||
return gtserror.SetMalformed(err)
|
||||
l.Error("missing Accept object IRI")
|
||||
return false, nil
|
||||
|
||||
// Ensure the Accept Object is what we expect
|
||||
// it to be, ie., it's Accepting the interaction
|
||||
// we need it to Accept, and not something else.
|
||||
case objectIRIStr != expectObjectURIStr:
|
||||
return gtserror.Newf(
|
||||
"resolved Accept Object uri %s was not the same as expected object %s",
|
||||
l.Errorf(
|
||||
"resolved Accept object IRI %s was not the same as expected object %s",
|
||||
objectIRIStr, expectObjectURIStr,
|
||||
)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return nil
|
||||
// Everything looks OK.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// extractIRI is shorthand to extract the first IRI
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
|
||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
|
@ -68,6 +69,20 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
|
|||
return gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Ensure requester is the same as the
|
||||
// Actor of the Accept; you can't Accept
|
||||
// something on someone else's behalf.
|
||||
actorURI, err := ap.ExtractActorURI(accept)
|
||||
if err != nil {
|
||||
const text = "Accept had empty or invalid actor property"
|
||||
return gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
|
||||
if requestingAcct.URI != actorURI.String() {
|
||||
const text = "Accept actor and requesting account were not the same"
|
||||
return gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Iterate all provided objects in the activity,
|
||||
// handling the ones we know how to handle.
|
||||
for _, object := range ap.ExtractObjects(accept) {
|
||||
|
@ -108,18 +123,6 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
|
|||
return err
|
||||
}
|
||||
|
||||
// ACCEPT STATUS (reply/boost)
|
||||
case uris.IsStatusesPath(objIRI):
|
||||
if err := f.acceptStatusIRI(
|
||||
ctx,
|
||||
activityID.String(),
|
||||
objIRI.String(),
|
||||
receivingAcct,
|
||||
requestingAcct,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ACCEPT LIKE
|
||||
case uris.IsLikePath(objIRI):
|
||||
if err := f.acceptLikeIRI(
|
||||
|
@ -132,9 +135,20 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
|
|||
return err
|
||||
}
|
||||
|
||||
// UNHANDLED
|
||||
// ACCEPT OTHER (reply? boost?)
|
||||
//
|
||||
// Don't check on IsStatusesPath
|
||||
// as this may be a remote status.
|
||||
default:
|
||||
log.Debugf(ctx, "unhandled iri type: %s", objIRI)
|
||||
if err := f.acceptOtherIRI(
|
||||
ctx,
|
||||
activityID,
|
||||
objIRI,
|
||||
receivingAcct,
|
||||
requestingAcct,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -276,39 +290,91 @@ func (f *federatingDB) acceptFollowIRI(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (f *federatingDB) acceptStatusIRI(
|
||||
func (f *federatingDB) acceptOtherIRI(
|
||||
ctx context.Context,
|
||||
activityID string,
|
||||
objectIRI string,
|
||||
activityID *url.URL,
|
||||
objectIRI *url.URL,
|
||||
receivingAcct *gtsmodel.Account,
|
||||
requestingAcct *gtsmodel.Account,
|
||||
) error {
|
||||
// Lock on this potential status
|
||||
// URI as we may be updating it.
|
||||
unlock := f.state.FedLocks.Lock(objectIRI)
|
||||
defer unlock()
|
||||
|
||||
// Get the status from the db.
|
||||
status, err := f.state.DB.GetStatusByURI(ctx, objectIRI)
|
||||
// See if we can get a status from the db.
|
||||
status, err := f.state.DB.GetStatusByURI(ctx, objectIRI.String())
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error getting status: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if status == nil {
|
||||
// We didn't have a status with
|
||||
// this URI, so nothing to do.
|
||||
// Just return.
|
||||
if status != nil {
|
||||
// We had a status stored with this
|
||||
// objectIRI, proceed to accept it.
|
||||
return f.acceptStoredStatus(
|
||||
ctx,
|
||||
activityID,
|
||||
status,
|
||||
receivingAcct,
|
||||
requestingAcct,
|
||||
)
|
||||
}
|
||||
|
||||
if objectIRI.Host == config.GetHost() ||
|
||||
objectIRI.Host == config.GetAccountDomain() {
|
||||
// Claims to be Accepting something of ours,
|
||||
// but we don't have a status stored for this
|
||||
// URI, so most likely it's been deleted in
|
||||
// the meantime, just bail.
|
||||
return nil
|
||||
}
|
||||
|
||||
if !status.IsLocal() {
|
||||
// We don't process Accepts of statuses
|
||||
// that weren't created on our instance.
|
||||
// Just return.
|
||||
// This must be an Accept of a remote Activity
|
||||
// or Object. Ensure relevance of this message
|
||||
// by checking that receiver follows requester.
|
||||
following, err := f.state.DB.IsFollowing(
|
||||
ctx,
|
||||
receivingAcct.ID,
|
||||
requestingAcct.ID,
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("db error checking following: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if !following {
|
||||
// If we don't follow this person, and
|
||||
// they're not Accepting something we know
|
||||
// about, then we don't give a good goddamn.
|
||||
return nil
|
||||
}
|
||||
|
||||
// This may be a reply, or it may be a boost,
|
||||
// we can't know yet without dereferencing it,
|
||||
// but let the processor worry about that.
|
||||
apObjectType := ap.ObjectUnknown
|
||||
|
||||
// Pass to the processor and let them handle side effects.
|
||||
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
||||
APObjectType: apObjectType,
|
||||
APActivityType: ap.ActivityAccept,
|
||||
APIRI: activityID,
|
||||
APObject: objectIRI,
|
||||
Receiving: receivingAcct,
|
||||
Requesting: requestingAcct,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *federatingDB) acceptStoredStatus(
|
||||
ctx context.Context,
|
||||
activityID *url.URL,
|
||||
status *gtsmodel.Status,
|
||||
receivingAcct *gtsmodel.Account,
|
||||
requestingAcct *gtsmodel.Account,
|
||||
) error {
|
||||
// Lock on this status URI
|
||||
// as we may be updating it.
|
||||
unlock := f.state.FedLocks.Lock(status.URI)
|
||||
defer unlock()
|
||||
|
||||
pendingApproval := util.PtrOrValue(status.PendingApproval, false)
|
||||
if !pendingApproval {
|
||||
// Status doesn't need approval or it's
|
||||
|
@ -317,14 +383,6 @@ func (f *federatingDB) acceptStatusIRI(
|
|||
return nil
|
||||
}
|
||||
|
||||
// Make sure the creator of the original status
|
||||
// is the same as the inbox processing the Accept;
|
||||
// this also ensures the status is local.
|
||||
if status.AccountID != receivingAcct.ID {
|
||||
const text = "status author account and inbox account were not the same"
|
||||
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Make sure the target of the interaction (reply/boost)
|
||||
// is the same as the account doing the Accept.
|
||||
if status.BoostOfAccountID != requestingAcct.ID &&
|
||||
|
@ -335,7 +393,7 @@ func (f *federatingDB) acceptStatusIRI(
|
|||
|
||||
// Mark the status as approved by this Accept URI.
|
||||
status.PendingApproval = util.Ptr(false)
|
||||
status.ApprovedByURI = activityID
|
||||
status.ApprovedByURI = activityID.String()
|
||||
if err := f.state.DB.UpdateStatus(
|
||||
ctx,
|
||||
status,
|
||||
|
|
|
@ -33,6 +33,7 @@ type AccountSettings struct {
|
|||
Theme string `bun:",nullzero"` // Preset CSS theme filename selected by this Account (empty string if nothing set).
|
||||
CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses.
|
||||
EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed
|
||||
HideBoosts *bool `bun:",nullzero,notnull,default:false"` // Hide boosts from this accounts profile page.
|
||||
HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections.
|
||||
WebVisibility Visibility `bun:",nullzero,notnull,default:public"` // Visibility level of statuses that visitors can view via the web profile.
|
||||
InteractionPolicyDirect *InteractionPolicy `bun:""` // Interaction policy to use for new direct visibility statuses by this account. If null, assume default policy.
|
||||
|
|
|
@ -69,25 +69,29 @@ type InteractionRequest struct {
|
|||
Like *StatusFave `bun:"-"` // Not stored in DB. Only set if InteractionType = InteractionLike.
|
||||
Reply *Status `bun:"-"` // Not stored in DB. Only set if InteractionType = InteractionReply.
|
||||
Announce *Status `bun:"-"` // Not stored in DB. Only set if InteractionType = InteractionAnnounce.
|
||||
URI string `bun:",nullzero,unique"` // ActivityPub URI of the Accept (if accepted) or Reject (if rejected). Null/empty if currently neither accepted not rejected.
|
||||
AcceptedAt time.Time `bun:"type:timestamptz,nullzero"` // If interaction request was accepted, time at which this occurred.
|
||||
RejectedAt time.Time `bun:"type:timestamptz,nullzero"` // If interaction request was rejected, time at which this occurred.
|
||||
|
||||
// ActivityPub URI of the Accept (if accepted) or Reject (if rejected).
|
||||
// Field may be empty if currently neither accepted not rejected, or if
|
||||
// acceptance/rejection was implicit (ie., not resulting from an Activity).
|
||||
URI string `bun:",nullzero,unique"`
|
||||
}
|
||||
|
||||
// IsHandled returns true if interaction
|
||||
// request has been neither accepted or rejected.
|
||||
func (ir *InteractionRequest) IsPending() bool {
|
||||
return ir.URI == "" && ir.AcceptedAt.IsZero() && ir.RejectedAt.IsZero()
|
||||
return !ir.IsAccepted() && !ir.IsRejected()
|
||||
}
|
||||
|
||||
// IsAccepted returns true if this
|
||||
// interaction request has been accepted.
|
||||
func (ir *InteractionRequest) IsAccepted() bool {
|
||||
return ir.URI != "" && !ir.AcceptedAt.IsZero()
|
||||
return !ir.AcceptedAt.IsZero()
|
||||
}
|
||||
|
||||
// IsRejected returns true if this
|
||||
// interaction request has been rejected.
|
||||
func (ir *InteractionRequest) IsRejected() bool {
|
||||
return ir.URI != "" && !ir.RejectedAt.IsZero()
|
||||
return !ir.RejectedAt.IsZero()
|
||||
}
|
||||
|
|
|
@ -42,6 +42,16 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() {
|
|||
<description>Posts from @admin@localhost:8080</description>
|
||||
<pubDate>Wed, 20 Oct 2021 10:41:37 +0000</pubDate>
|
||||
<lastBuildDate>Wed, 20 Oct 2021 10:41:37 +0000</lastBuildDate>
|
||||
<item>
|
||||
<title>introduction post</title>
|
||||
<link>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</link>
|
||||
<description>@the_mighty_zork@localhost:8080 made a new post: "hello everyone!"</description>
|
||||
<content:encoded><![CDATA[hello everyone!]]></content:encoded>
|
||||
<author>@the_mighty_zork@localhost:8080</author>
|
||||
<guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</guid>
|
||||
<pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate>
|
||||
<source>http://localhost:8080/@the_mighty_zork/feed.rss</source>
|
||||
</item>
|
||||
<item>
|
||||
<title>open to see some puppies</title>
|
||||
<link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link>
|
||||
|
|
|
@ -274,6 +274,11 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
|||
settingsColumns = append(settingsColumns, "enable_rss")
|
||||
}
|
||||
|
||||
if form.HideBoosts != nil {
|
||||
account.Settings.HideBoosts = form.HideBoosts
|
||||
settingsColumns = append(settingsColumns, "hide_boosts")
|
||||
}
|
||||
|
||||
if form.HideCollections != nil {
|
||||
account.Settings.HideCollections = form.HideCollections
|
||||
settingsColumns = append(settingsColumns, "hide_collections")
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"codeberg.org/gruf/go-kv"
|
||||
|
@ -144,6 +145,10 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
|
|||
// ACCEPT (pending) ANNOUNCE
|
||||
case ap.ActivityAnnounce:
|
||||
return p.fediAPI.AcceptAnnounce(ctx, fMsg)
|
||||
|
||||
// ACCEPT (remote) REPLY or ANNOUNCE
|
||||
case ap.ObjectUnknown:
|
||||
return p.fediAPI.AcceptRemoteStatus(ctx, fMsg)
|
||||
}
|
||||
|
||||
// REJECT SOMETHING
|
||||
|
@ -823,6 +828,60 @@ func (p *fediAPI) AcceptReply(ctx context.Context, fMsg *messages.FromFediAPI) e
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *fediAPI) AcceptRemoteStatus(ctx context.Context, fMsg *messages.FromFediAPI) error {
|
||||
// See if we can accept a remote
|
||||
// status we don't have stored yet.
|
||||
objectIRI, ok := fMsg.APObject.(*url.URL)
|
||||
if !ok {
|
||||
return gtserror.Newf("%T not parseable as *url.URL", fMsg.APObject)
|
||||
}
|
||||
|
||||
acceptIRI := fMsg.APIRI
|
||||
if acceptIRI == nil {
|
||||
return gtserror.New("acceptIRI was nil")
|
||||
}
|
||||
|
||||
// Assume we're accepting a status; create a
|
||||
// barebones status for dereferencing purposes.
|
||||
bareStatus := >smodel.Status{
|
||||
URI: objectIRI.String(),
|
||||
ApprovedByURI: acceptIRI.String(),
|
||||
}
|
||||
|
||||
// Call RefreshStatus() to process the provided
|
||||
// barebones status and insert it into the database,
|
||||
// if indeed it's actually a status URI we can fetch.
|
||||
//
|
||||
// This will also check whether the given AcceptIRI
|
||||
// actually grants permission for this status.
|
||||
status, _, err := p.federate.RefreshStatus(ctx,
|
||||
fMsg.Receiving.Username,
|
||||
bareStatus,
|
||||
nil, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error processing accepted status %s: %w", bareStatus.URI, err)
|
||||
}
|
||||
|
||||
// No error means it was indeed a remote status, and the
|
||||
// given acceptIRI permitted it. Timeline and notify it.
|
||||
if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
|
||||
log.Errorf(ctx, "error timelining and notifying status: %v", err)
|
||||
}
|
||||
|
||||
// Interaction counts changed on the interacted status;
|
||||
// uncache the prepared version from all timelines.
|
||||
if status.InReplyToID != "" {
|
||||
p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
|
||||
}
|
||||
|
||||
if status.BoostOfID != "" {
|
||||
p.surface.invalidateStatusFromTimelines(ctx, status.BoostOfID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *fediAPI) AcceptAnnounce(ctx context.Context, fMsg *messages.FromFediAPI) error {
|
||||
boost, ok := fMsg.GTSModel.(*gtsmodel.Status)
|
||||
if !ok {
|
||||
|
|
|
@ -1988,6 +1988,16 @@ func (c *Converter) InteractionReqToASAccept(
|
|||
return nil, gtserror.Newf("invalid interacting account uri: %w", err)
|
||||
}
|
||||
|
||||
publicIRI, err := url.Parse(pub.PublicActivityPubIRI)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("invalid public uri: %w", err)
|
||||
}
|
||||
|
||||
followersIRI, err := url.Parse(req.TargetAccount.FollowersURI)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("invalid followers uri: %w", err)
|
||||
}
|
||||
|
||||
// Set id to the URI of
|
||||
// interaction request.
|
||||
ap.SetJSONLDId(accept, acceptID)
|
||||
|
@ -2003,6 +2013,9 @@ func (c *Converter) InteractionReqToASAccept(
|
|||
// of interaction URI.
|
||||
ap.AppendTo(accept, toIRI)
|
||||
|
||||
// Cc to the actor's followers, and to Public.
|
||||
ap.AppendCc(accept, publicIRI, followersIRI)
|
||||
|
||||
return accept, nil
|
||||
}
|
||||
|
||||
|
@ -2034,6 +2047,16 @@ func (c *Converter) InteractionReqToASReject(
|
|||
return nil, gtserror.Newf("invalid interacting account uri: %w", err)
|
||||
}
|
||||
|
||||
publicIRI, err := url.Parse(pub.PublicActivityPubIRI)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("invalid public uri: %w", err)
|
||||
}
|
||||
|
||||
followersIRI, err := url.Parse(req.TargetAccount.FollowersURI)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("invalid followers uri: %w", err)
|
||||
}
|
||||
|
||||
// Set id to the URI of
|
||||
// interaction request.
|
||||
ap.SetJSONLDId(reject, rejectID)
|
||||
|
@ -2049,5 +2072,8 @@ func (c *Converter) InteractionReqToASReject(
|
|||
// of interaction URI.
|
||||
ap.AppendTo(reject, toIRI)
|
||||
|
||||
// Cc to the actor's followers, and to Public.
|
||||
ap.AppendCc(reject, publicIRI, followersIRI)
|
||||
|
||||
return reject, nil
|
||||
}
|
||||
|
|
|
@ -1181,6 +1181,10 @@ func (suite *InternalToASTestSuite) TestInteractionReqToASAccept() {
|
|||
suite.Equal(`{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": "http://localhost:8080/users/the_mighty_zork",
|
||||
"cc": [
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
"http://localhost:8080/users/the_mighty_zork/followers"
|
||||
],
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/accepts/01J1AKMZ8JE5NW0ZSFTRC1JJNE",
|
||||
"object": "https://fossbros-anonymous.io/users/foss_satan/statuses/01J1AKRRHQ6MDDQHV0TP716T2K",
|
||||
"to": "http://fossbros-anonymous.io/users/foss_satan",
|
||||
|
|
|
@ -302,7 +302,7 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
|||
// Bits that vary between remote + local accounts:
|
||||
// - Account (acct) string.
|
||||
// - Role.
|
||||
// - Settings things (enableRSS, theme, customCSS, hideCollections).
|
||||
// - Settings things (enableRSS, theme, customCSS, hideBoosts ,hideCollections).
|
||||
|
||||
var (
|
||||
acct string
|
||||
|
@ -310,6 +310,7 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
|||
enableRSS bool
|
||||
theme string
|
||||
customCSS string
|
||||
hideBoosts bool
|
||||
hideCollections bool
|
||||
)
|
||||
|
||||
|
@ -338,6 +339,7 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
|||
enableRSS = *a.Settings.EnableRSS
|
||||
theme = a.Settings.Theme
|
||||
customCSS = a.Settings.CustomCSS
|
||||
hideBoosts = *a.Settings.HideBoosts
|
||||
hideCollections = *a.Settings.HideCollections
|
||||
}
|
||||
|
||||
|
@ -380,6 +382,7 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
|||
Theme: theme,
|
||||
CustomCSS: customCSS,
|
||||
EnableRSS: enableRSS,
|
||||
HideBoosts: hideBoosts,
|
||||
HideCollections: hideCollections,
|
||||
Roles: roles,
|
||||
}
|
||||
|
@ -1092,7 +1095,15 @@ func (c *Converter) StatusToWebStatus(
|
|||
ctx context.Context,
|
||||
s *gtsmodel.Status,
|
||||
) (*apimodel.WebStatus, error) {
|
||||
apiStatus, err := c.statusToFrontend(ctx, s,
|
||||
|
||||
isBoost := s.BoostOf != nil
|
||||
status := s
|
||||
|
||||
if isBoost {
|
||||
status = s.BoostOf
|
||||
}
|
||||
|
||||
apiStatus, err := c.statusToFrontend(ctx, status,
|
||||
nil, // No authed requester.
|
||||
statusfilter.FilterContextNone, // No filters.
|
||||
nil, // No filters.
|
||||
|
@ -1103,7 +1114,7 @@ func (c *Converter) StatusToWebStatus(
|
|||
}
|
||||
|
||||
// Convert status author to web model.
|
||||
acct, err := c.AccountToWebAccount(ctx, s.Account)
|
||||
acct, err := c.AccountToWebAccount(ctx, status.Account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1113,6 +1124,14 @@ func (c *Converter) StatusToWebStatus(
|
|||
Account: acct,
|
||||
}
|
||||
|
||||
if isBoost {
|
||||
reblogAcct, err := c.AccountToWebAccount(ctx, s.Account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
webStatus.ReblogAccount = reblogAcct
|
||||
}
|
||||
|
||||
// Whack a newline before and after each "pre" to make it easier to outdent it.
|
||||
webStatus.Content = strings.ReplaceAll(webStatus.Content, "<pre>", "\n<pre>")
|
||||
webStatus.Content = strings.ReplaceAll(webStatus.Content, "</pre>", "</pre>\n")
|
||||
|
|
|
@ -1402,6 +1402,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
|
|||
"emojis": [],
|
||||
"fields": []
|
||||
},
|
||||
"reblog_account": null,
|
||||
"media_attachments": [
|
||||
{
|
||||
"id": "01HE7Y3C432WRSNS10EZM86SA5",
|
||||
|
|
|
@ -39,6 +39,12 @@
|
|||
func (c *Converter) StatusToRSSItem(ctx context.Context, s *gtsmodel.Status) (*feeds.Item, error) {
|
||||
// see https://cyber.harvard.edu/rss/rss.html
|
||||
|
||||
// If status is a boost,
|
||||
// display the boost instead.
|
||||
if s.BoostOf != nil {
|
||||
s = s.BoostOf
|
||||
}
|
||||
|
||||
// Title -- The title of the item.
|
||||
// example: Venice Film Festival Tries to Quit Sinking
|
||||
var title string
|
||||
|
|
|
@ -657,6 +657,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
|||
Sensitive: util.Ptr(false),
|
||||
Language: "en",
|
||||
EnableRSS: util.Ptr(false),
|
||||
HideBoosts: util.Ptr(false),
|
||||
HideCollections: util.Ptr(false),
|
||||
WebVisibility: gtsmodel.VisibilityPublic,
|
||||
},
|
||||
|
@ -668,6 +669,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
|||
Sensitive: util.Ptr(false),
|
||||
Language: "en",
|
||||
EnableRSS: util.Ptr(true),
|
||||
HideBoosts: util.Ptr(false),
|
||||
HideCollections: util.Ptr(false),
|
||||
WebVisibility: gtsmodel.VisibilityPublic,
|
||||
},
|
||||
|
@ -679,6 +681,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
|||
Sensitive: util.Ptr(false),
|
||||
Language: "en",
|
||||
EnableRSS: util.Ptr(true),
|
||||
HideBoosts: util.Ptr(false),
|
||||
HideCollections: util.Ptr(false),
|
||||
WebVisibility: gtsmodel.VisibilityUnlocked,
|
||||
},
|
||||
|
@ -690,6 +693,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
|||
Sensitive: util.Ptr(true),
|
||||
Language: "fr",
|
||||
EnableRSS: util.Ptr(false),
|
||||
HideBoosts: util.Ptr(false),
|
||||
HideCollections: util.Ptr(true),
|
||||
WebVisibility: gtsmodel.VisibilityPublic,
|
||||
},
|
||||
|
|
2
vendor/github.com/ncruces/go-sqlite3/blob.go
generated
vendored
2
vendor/github.com/ncruces/go-sqlite3/blob.go
generated
vendored
|
@ -31,7 +31,6 @@ type Blob struct {
|
|||
//
|
||||
// https://sqlite.org/c3ref/blob_open.html
|
||||
func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob, error) {
|
||||
c.checkInterrupt()
|
||||
defer c.arena.mark()()
|
||||
blobPtr := c.arena.new(ptrlen)
|
||||
dbPtr := c.arena.string(db)
|
||||
|
@ -43,6 +42,7 @@ func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob,
|
|||
flags = 1
|
||||
}
|
||||
|
||||
c.checkInterrupt(c.handle)
|
||||
r := c.call("sqlite3_blob_open", uint64(c.handle),
|
||||
uint64(dbPtr), uint64(tablePtr), uint64(columnPtr),
|
||||
uint64(row), flags, uint64(blobPtr))
|
||||
|
|
5
vendor/github.com/ncruces/go-sqlite3/config.go
generated
vendored
5
vendor/github.com/ncruces/go-sqlite3/config.go
generated
vendored
|
@ -284,7 +284,10 @@ func walCallback(ctx context.Context, mod api.Module, _, pDB, zSchema uint32, pa
|
|||
//
|
||||
// https://sqlite.org/c3ref/autovacuum_pages.html
|
||||
func (c *Conn) AutoVacuumPages(cb func(schema string, dbPages, freePages, bytesPerPage uint) uint) error {
|
||||
funcPtr := util.AddHandle(c.ctx, cb)
|
||||
var funcPtr uint32
|
||||
if cb != nil {
|
||||
funcPtr = util.AddHandle(c.ctx, cb)
|
||||
}
|
||||
r := c.call("sqlite3_autovacuum_pages_go", uint64(c.handle), uint64(funcPtr))
|
||||
return c.error(r)
|
||||
}
|
||||
|
|
91
vendor/github.com/ncruces/go-sqlite3/conn.go
generated
vendored
91
vendor/github.com/ncruces/go-sqlite3/conn.go
generated
vendored
|
@ -24,7 +24,7 @@ type Conn struct {
|
|||
pending *Stmt
|
||||
stmts []*Stmt
|
||||
timer *time.Timer
|
||||
busy func(int) bool
|
||||
busy func(context.Context, int) bool
|
||||
log func(xErrorCode, string)
|
||||
collation func(*Conn, string)
|
||||
wal func(*Conn, string, int) error
|
||||
|
@ -38,14 +38,20 @@ type Conn struct {
|
|||
handle uint32
|
||||
}
|
||||
|
||||
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE], [OPEN_URI] and [OPEN_NOFOLLOW].
|
||||
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE] and [OPEN_URI].
|
||||
func Open(filename string) (*Conn, error) {
|
||||
return newConn(filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI|OPEN_NOFOLLOW)
|
||||
return newConn(context.Background(), filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
|
||||
}
|
||||
|
||||
// OpenContext is like [Open] but includes a context,
|
||||
// which is used to interrupt the process of opening the connectiton.
|
||||
func OpenContext(ctx context.Context, filename string) (*Conn, error) {
|
||||
return newConn(ctx, filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
|
||||
}
|
||||
|
||||
// OpenFlags opens an SQLite database file as specified by the filename argument.
|
||||
//
|
||||
// If none of the required flags is used, a combination of [OPEN_READWRITE] and [OPEN_CREATE] is used.
|
||||
// If none of the required flags are used, a combination of [OPEN_READWRITE] and [OPEN_CREATE] is used.
|
||||
// If a URI filename is used, PRAGMA statements to execute can be specified using "_pragma":
|
||||
//
|
||||
// sqlite3.Open("file:demo.db?_pragma=busy_timeout(10000)")
|
||||
|
@ -55,25 +61,33 @@ func OpenFlags(filename string, flags OpenFlag) (*Conn, error) {
|
|||
if flags&(OPEN_READONLY|OPEN_READWRITE|OPEN_CREATE) == 0 {
|
||||
flags |= OPEN_READWRITE | OPEN_CREATE
|
||||
}
|
||||
return newConn(filename, flags)
|
||||
return newConn(context.Background(), filename, flags)
|
||||
}
|
||||
|
||||
type connKey struct{}
|
||||
|
||||
func newConn(filename string, flags OpenFlag) (conn *Conn, err error) {
|
||||
sqlite, err := instantiateSQLite()
|
||||
func newConn(ctx context.Context, filename string, flags OpenFlag) (res *Conn, _ error) {
|
||||
err := ctx.Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Conn{interrupt: ctx}
|
||||
c.sqlite, err = instantiateSQLite()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if conn == nil {
|
||||
sqlite.close()
|
||||
if res == nil {
|
||||
c.Close()
|
||||
c.sqlite.close()
|
||||
} else {
|
||||
c.interrupt = context.Background()
|
||||
}
|
||||
}()
|
||||
|
||||
c := &Conn{sqlite: sqlite}
|
||||
c.arena = c.newArena(1024)
|
||||
c.ctx = context.WithValue(c.ctx, connKey{}, c)
|
||||
c.arena = c.newArena(1024)
|
||||
c.handle, err = c.openDB(filename, flags)
|
||||
if err == nil {
|
||||
err = initExtensions(c)
|
||||
|
@ -98,6 +112,7 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
|
|||
return 0, err
|
||||
}
|
||||
|
||||
c.call("sqlite3_progress_handler_go", uint64(handle), 100)
|
||||
if flags|OPEN_URI != 0 && strings.HasPrefix(filename, "file:") {
|
||||
var pragmas strings.Builder
|
||||
if _, after, ok := strings.Cut(filename, "?"); ok {
|
||||
|
@ -109,6 +124,7 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
|
|||
}
|
||||
}
|
||||
if pragmas.Len() != 0 {
|
||||
c.checkInterrupt(handle)
|
||||
pragmaPtr := c.arena.string(pragmas.String())
|
||||
r := c.call("sqlite3_exec", uint64(handle), uint64(pragmaPtr), 0, 0, 0)
|
||||
if err := c.sqlite.error(r, handle, pragmas.String()); err != nil {
|
||||
|
@ -118,7 +134,6 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
c.call("sqlite3_progress_handler_go", uint64(handle), 100)
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
|
@ -160,10 +175,10 @@ func (c *Conn) Close() error {
|
|||
//
|
||||
// https://sqlite.org/c3ref/exec.html
|
||||
func (c *Conn) Exec(sql string) error {
|
||||
c.checkInterrupt()
|
||||
defer c.arena.mark()()
|
||||
sqlPtr := c.arena.string(sql)
|
||||
|
||||
c.checkInterrupt(c.handle)
|
||||
r := c.call("sqlite3_exec", uint64(c.handle), uint64(sqlPtr), 0, 0, 0)
|
||||
return c.error(r, sql)
|
||||
}
|
||||
|
@ -301,8 +316,7 @@ func (c *Conn) ReleaseMemory() error {
|
|||
return c.error(r)
|
||||
}
|
||||
|
||||
// GetInterrupt gets the context set with [Conn.SetInterrupt],
|
||||
// or nil if none was set.
|
||||
// GetInterrupt gets the context set with [Conn.SetInterrupt].
|
||||
func (c *Conn) GetInterrupt() context.Context {
|
||||
return c.interrupt
|
||||
}
|
||||
|
@ -322,9 +336,11 @@ func (c *Conn) GetInterrupt() context.Context {
|
|||
//
|
||||
// https://sqlite.org/c3ref/interrupt.html
|
||||
func (c *Conn) SetInterrupt(ctx context.Context) (old context.Context) {
|
||||
// Is it the same context?
|
||||
if ctx == c.interrupt {
|
||||
return ctx
|
||||
old = c.interrupt
|
||||
c.interrupt = ctx
|
||||
|
||||
if ctx == old || ctx.Done() == old.Done() {
|
||||
return old
|
||||
}
|
||||
|
||||
// A busy SQL statement prevents SQLite from ignoring an interrupt
|
||||
|
@ -333,32 +349,29 @@ func (c *Conn) SetInterrupt(ctx context.Context) (old context.Context) {
|
|||
defer c.arena.mark()()
|
||||
stmtPtr := c.arena.new(ptrlen)
|
||||
loopPtr := c.arena.string(`WITH RECURSIVE c(x) AS (VALUES(0) UNION ALL SELECT x FROM c) SELECT x FROM c`)
|
||||
c.call("sqlite3_prepare_v3", uint64(c.handle), uint64(loopPtr), math.MaxUint64, 0, uint64(stmtPtr), 0)
|
||||
c.call("sqlite3_prepare_v3", uint64(c.handle), uint64(loopPtr), math.MaxUint64,
|
||||
uint64(PREPARE_PERSISTENT), uint64(stmtPtr), 0)
|
||||
c.pending = &Stmt{c: c}
|
||||
c.pending.handle = util.ReadUint32(c.mod, stmtPtr)
|
||||
}
|
||||
|
||||
old = c.interrupt
|
||||
c.interrupt = ctx
|
||||
|
||||
if old != nil && old.Done() != nil && (ctx == nil || ctx.Err() == nil) {
|
||||
if old.Done() != nil && ctx.Err() == nil {
|
||||
c.pending.Reset()
|
||||
}
|
||||
if ctx != nil && ctx.Done() != nil {
|
||||
if ctx.Done() != nil {
|
||||
c.pending.Step()
|
||||
}
|
||||
return old
|
||||
}
|
||||
|
||||
func (c *Conn) checkInterrupt() {
|
||||
if c.interrupt != nil && c.interrupt.Err() != nil {
|
||||
c.call("sqlite3_interrupt", uint64(c.handle))
|
||||
func (c *Conn) checkInterrupt(handle uint32) {
|
||||
if c.interrupt.Err() != nil {
|
||||
c.call("sqlite3_interrupt", uint64(handle))
|
||||
}
|
||||
}
|
||||
|
||||
func progressCallback(ctx context.Context, mod api.Module, pDB uint32) (interrupt uint32) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB &&
|
||||
c.interrupt != nil && c.interrupt.Err() != nil {
|
||||
func progressCallback(ctx context.Context, mod api.Module, _ uint32) (interrupt uint32) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.interrupt.Err() != nil {
|
||||
interrupt = 1
|
||||
}
|
||||
return interrupt
|
||||
|
@ -373,9 +386,8 @@ func (c *Conn) BusyTimeout(timeout time.Duration) error {
|
|||
return c.error(r)
|
||||
}
|
||||
|
||||
func timeoutCallback(ctx context.Context, mod api.Module, pDB uint32, count, tmout int32) (retry uint32) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok &&
|
||||
(c.interrupt == nil || c.interrupt.Err() == nil) {
|
||||
func timeoutCallback(ctx context.Context, mod api.Module, count, tmout int32) (retry uint32) {
|
||||
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)
|
||||
|
@ -391,7 +403,7 @@ func timeoutCallback(ctx context.Context, mod api.Module, pDB uint32, count, tmo
|
|||
|
||||
if delay = min(delay, tmout-prior); delay > 0 {
|
||||
delay := time.Duration(delay) * time.Millisecond
|
||||
if c.interrupt == nil || c.interrupt.Done() == nil {
|
||||
if c.interrupt.Done() == nil {
|
||||
time.Sleep(delay)
|
||||
return 1
|
||||
}
|
||||
|
@ -414,7 +426,7 @@ func timeoutCallback(ctx context.Context, mod api.Module, pDB uint32, count, tmo
|
|||
// BusyHandler registers a callback to handle [BUSY] errors.
|
||||
//
|
||||
// https://sqlite.org/c3ref/busy_handler.html
|
||||
func (c *Conn) BusyHandler(cb func(count int) (retry bool)) error {
|
||||
func (c *Conn) BusyHandler(cb func(ctx context.Context, count int) (retry bool)) error {
|
||||
var enable uint64
|
||||
if cb != nil {
|
||||
enable = 1
|
||||
|
@ -428,9 +440,12 @@ func (c *Conn) BusyHandler(cb func(count int) (retry bool)) error {
|
|||
}
|
||||
|
||||
func busyCallback(ctx context.Context, mod api.Module, pDB uint32, count int32) (retry uint32) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.busy != nil &&
|
||||
(c.interrupt == nil || c.interrupt.Err() == nil) {
|
||||
if c.busy(int(count)) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.busy != nil {
|
||||
interrupt := c.interrupt
|
||||
if interrupt == nil {
|
||||
interrupt = context.Background()
|
||||
}
|
||||
if interrupt.Err() == nil && c.busy(interrupt, int(count)) {
|
||||
retry = 1
|
||||
}
|
||||
}
|
||||
|
|
2
vendor/github.com/ncruces/go-sqlite3/conn_iter.go
generated
vendored
2
vendor/github.com/ncruces/go-sqlite3/conn_iter.go
generated
vendored
|
@ -1,4 +1,4 @@
|
|||
//go:build (go1.23 || goexperiment.rangefunc) && !vet
|
||||
//go:build go1.23
|
||||
|
||||
package sqlite3
|
||||
|
||||
|
|
2
vendor/github.com/ncruces/go-sqlite3/conn_old.go
generated
vendored
2
vendor/github.com/ncruces/go-sqlite3/conn_old.go
generated
vendored
|
@ -1,4 +1,4 @@
|
|||
//go:build !(go1.23 || goexperiment.rangefunc) || vet
|
||||
//go:build !go1.23
|
||||
|
||||
package sqlite3
|
||||
|
||||
|
|
68
vendor/github.com/ncruces/go-sqlite3/driver/driver.go
generated
vendored
68
vendor/github.com/ncruces/go-sqlite3/driver/driver.go
generated
vendored
|
@ -40,14 +40,14 @@
|
|||
// When using a custom time struct, you'll have to implement
|
||||
// [database/sql/driver.Valuer] and [database/sql.Scanner].
|
||||
//
|
||||
// The Value method should ideally serialise to a time [format] supported by SQLite.
|
||||
// The Value method should ideally encode to a time [format] supported by SQLite.
|
||||
// This ensures SQL date and time functions work as they should,
|
||||
// and that your schema works with other SQLite tools.
|
||||
// [sqlite3.TimeFormat.Encode] may help.
|
||||
//
|
||||
// The Scan method needs to take into account that the value it receives can be of differing types.
|
||||
// It can already be a [time.Time], if the driver decoded the value according to "_timefmt" rules.
|
||||
// Or it can be a: string, int64, float64, []byte, nil,
|
||||
// Or it can be a: string, int64, float64, []byte, or nil,
|
||||
// depending on the column type and what whoever wrote the value.
|
||||
// [sqlite3.TimeFormat.Decode] may help.
|
||||
//
|
||||
|
@ -202,19 +202,19 @@ func (n *connector) Driver() driver.Driver {
|
|||
return n.driver
|
||||
}
|
||||
|
||||
func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
|
||||
func (n *connector) Connect(ctx context.Context) (res driver.Conn, err error) {
|
||||
c := &conn{
|
||||
txLock: n.txLock,
|
||||
tmRead: n.tmRead,
|
||||
tmWrite: n.tmWrite,
|
||||
}
|
||||
|
||||
c.Conn, err = sqlite3.Open(n.name)
|
||||
c.Conn, err = sqlite3.OpenContext(ctx, n.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if res == nil {
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
|
@ -239,6 +239,7 @@ func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.Close()
|
||||
if s.Step() && s.ColumnBool(0) {
|
||||
c.readOnly = '1'
|
||||
} else {
|
||||
|
@ -466,6 +467,7 @@ func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (drive
|
|||
defer s.Stmt.Conn().SetInterrupt(old)
|
||||
|
||||
err = s.Stmt.Exec()
|
||||
s.Stmt.ClearBindings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -488,7 +490,7 @@ func (s *stmt) setupBindings(args []driver.NamedValue) (err error) {
|
|||
if arg.Name == "" {
|
||||
ids = append(ids, arg.Ordinal)
|
||||
} else {
|
||||
for _, prefix := range []string{":", "@", "$"} {
|
||||
for _, prefix := range [...]string{":", "@", "$"} {
|
||||
if id := s.Stmt.BindIndex(prefix + arg.Name); id != 0 {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
@ -522,9 +524,9 @@ func (s *stmt) setupBindings(args []driver.NamedValue) (err error) {
|
|||
default:
|
||||
panic(util.AssertErr())
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -595,10 +597,11 @@ func (r *rows) Close() error {
|
|||
func (r *rows) Columns() []string {
|
||||
if r.names == nil {
|
||||
count := r.Stmt.ColumnCount()
|
||||
r.names = make([]string, count)
|
||||
for i := range r.names {
|
||||
r.names[i] = r.Stmt.ColumnName(i)
|
||||
names := make([]string, count)
|
||||
for i := range names {
|
||||
names[i] = r.Stmt.ColumnName(i)
|
||||
}
|
||||
r.names = names
|
||||
}
|
||||
return r.names
|
||||
}
|
||||
|
@ -606,26 +609,29 @@ func (r *rows) Columns() []string {
|
|||
func (r *rows) loadTypes() {
|
||||
if r.nulls == nil {
|
||||
count := r.Stmt.ColumnCount()
|
||||
r.nulls = make([]bool, count)
|
||||
r.types = make([]string, count)
|
||||
for i := range r.nulls {
|
||||
nulls := make([]bool, count)
|
||||
types := make([]string, count)
|
||||
for i := range nulls {
|
||||
if col := r.Stmt.ColumnOriginName(i); col != "" {
|
||||
r.types[i], _, r.nulls[i], _, _, _ = r.Stmt.Conn().TableColumnMetadata(
|
||||
types[i], _, nulls[i], _, _, _ = r.Stmt.Conn().TableColumnMetadata(
|
||||
r.Stmt.ColumnDatabaseName(i),
|
||||
r.Stmt.ColumnTableName(i),
|
||||
col)
|
||||
}
|
||||
}
|
||||
r.nulls = nulls
|
||||
r.types = types
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rows) declType(index int) string {
|
||||
if r.types == nil {
|
||||
count := r.Stmt.ColumnCount()
|
||||
r.types = make([]string, count)
|
||||
for i := range r.types {
|
||||
r.types[i] = strings.ToUpper(r.Stmt.ColumnDeclType(i))
|
||||
types := make([]string, count)
|
||||
for i := range types {
|
||||
types[i] = strings.ToUpper(r.Stmt.ColumnDeclType(i))
|
||||
}
|
||||
r.types = types
|
||||
}
|
||||
return r.types[index]
|
||||
}
|
||||
|
@ -665,27 +671,23 @@ func (r *rows) Next(dest []driver.Value) error {
|
|||
for i := range dest {
|
||||
if t, ok := r.decodeTime(i, dest[i]); ok {
|
||||
dest[i] = t
|
||||
continue
|
||||
}
|
||||
if s, ok := dest[i].(string); ok {
|
||||
t, ok := maybeTime(s)
|
||||
if ok {
|
||||
dest[i] = t
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rows) decodeTime(i int, v any) (_ time.Time, ok bool) {
|
||||
switch r.tmRead {
|
||||
case sqlite3.TimeFormatDefault, time.RFC3339Nano:
|
||||
// handled by maybeTime
|
||||
return
|
||||
}
|
||||
switch v.(type) {
|
||||
case int64, float64, string:
|
||||
switch v := v.(type) {
|
||||
case int64, float64:
|
||||
// could be a time value
|
||||
case string:
|
||||
if r.tmWrite != "" && r.tmWrite != time.RFC3339 && r.tmWrite != time.RFC3339Nano {
|
||||
break
|
||||
}
|
||||
t, ok := maybeTime(v)
|
||||
if ok {
|
||||
return t, true
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
|
1
vendor/github.com/ncruces/go-sqlite3/embed/README.md
generated
vendored
1
vendor/github.com/ncruces/go-sqlite3/embed/README.md
generated
vendored
|
@ -9,6 +9,7 @@ The following optional features are compiled in:
|
|||
- [JSON](https://sqlite.org/json1.html)
|
||||
- [R*Tree](https://sqlite.org/rtree.html)
|
||||
- [GeoPoly](https://sqlite.org/geopoly.html)
|
||||
- [Spellfix1](https://sqlite.org/spellfix1.html)
|
||||
- [soundex](https://sqlite.org/lang_corefunc.html#soundex)
|
||||
- [stat4](https://sqlite.org/compile.html#enable_stat4)
|
||||
- [base64](https://github.com/sqlite/sqlite/blob/master/ext/misc/base64.c)
|
||||
|
|
2
vendor/github.com/ncruces/go-sqlite3/embed/build.sh
generated
vendored
2
vendor/github.com/ncruces/go-sqlite3/embed/build.sh
generated
vendored
|
@ -14,7 +14,7 @@ trap 'rm -f sqlite3.tmp' EXIT
|
|||
-o sqlite3.wasm "$ROOT/sqlite3/main.c" \
|
||||
-I"$ROOT/sqlite3" \
|
||||
-mexec-model=reactor \
|
||||
-matomics -msimd128 -mmutable-globals \
|
||||
-matomics -msimd128 -mmutable-globals -mmultivalue \
|
||||
-mbulk-memory -mreference-types \
|
||||
-mnontrapping-fptoint -msign-ext \
|
||||
-fno-stack-protector -fno-stack-clash-protection \
|
||||
|
|
1
vendor/github.com/ncruces/go-sqlite3/embed/exports.txt
generated
vendored
1
vendor/github.com/ncruces/go-sqlite3/embed/exports.txt
generated
vendored
|
@ -51,6 +51,7 @@ sqlite3_create_collation_go
|
|||
sqlite3_create_function_go
|
||||
sqlite3_create_module_go
|
||||
sqlite3_create_window_function_go
|
||||
sqlite3_data_count
|
||||
sqlite3_database_file_object
|
||||
sqlite3_db_cacheflush
|
||||
sqlite3_db_config
|
||||
|
|
BIN
vendor/github.com/ncruces/go-sqlite3/embed/sqlite3.wasm
generated
vendored
BIN
vendor/github.com/ncruces/go-sqlite3/embed/sqlite3.wasm
generated
vendored
Binary file not shown.
28
vendor/github.com/ncruces/go-sqlite3/func.go
generated
vendored
28
vendor/github.com/ncruces/go-sqlite3/func.go
generated
vendored
|
@ -33,16 +33,23 @@ func (c *Conn) CollationNeeded(cb func(db *Conn, name string)) error {
|
|||
// one or more unknown collating sequences.
|
||||
func (c Conn) AnyCollationNeeded() error {
|
||||
r := c.call("sqlite3_anycollseq_init", uint64(c.handle), 0, 0)
|
||||
return c.error(r)
|
||||
if err := c.error(r); err != nil {
|
||||
return err
|
||||
}
|
||||
c.collation = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateCollation defines a new collating sequence.
|
||||
//
|
||||
// https://sqlite.org/c3ref/create_collation.html
|
||||
func (c *Conn) CreateCollation(name string, fn func(a, b []byte) int) error {
|
||||
var funcPtr uint32
|
||||
defer c.arena.mark()()
|
||||
namePtr := c.arena.string(name)
|
||||
funcPtr := util.AddHandle(c.ctx, fn)
|
||||
if fn != nil {
|
||||
funcPtr = util.AddHandle(c.ctx, fn)
|
||||
}
|
||||
r := c.call("sqlite3_create_collation_go",
|
||||
uint64(c.handle), uint64(namePtr), uint64(funcPtr))
|
||||
return c.error(r)
|
||||
|
@ -52,9 +59,12 @@ funcPtr := util.AddHandle(c.ctx, fn)
|
|||
//
|
||||
// https://sqlite.org/c3ref/create_function.html
|
||||
func (c *Conn) CreateFunction(name string, nArg int, flag FunctionFlag, fn ScalarFunction) error {
|
||||
var funcPtr uint32
|
||||
defer c.arena.mark()()
|
||||
namePtr := c.arena.string(name)
|
||||
funcPtr := util.AddHandle(c.ctx, fn)
|
||||
if fn != nil {
|
||||
funcPtr = util.AddHandle(c.ctx, fn)
|
||||
}
|
||||
r := c.call("sqlite3_create_function_go",
|
||||
uint64(c.handle), uint64(namePtr), uint64(nArg),
|
||||
uint64(flag), uint64(funcPtr))
|
||||
|
@ -71,10 +81,13 @@ funcPtr := util.AddHandle(c.ctx, fn)
|
|||
//
|
||||
// https://sqlite.org/c3ref/create_function.html
|
||||
func (c *Conn) CreateWindowFunction(name string, nArg int, flag FunctionFlag, fn func() AggregateFunction) error {
|
||||
var funcPtr uint32
|
||||
defer c.arena.mark()()
|
||||
call := "sqlite3_create_aggregate_function_go"
|
||||
namePtr := c.arena.string(name)
|
||||
funcPtr := util.AddHandle(c.ctx, fn)
|
||||
if fn != nil {
|
||||
funcPtr = util.AddHandle(c.ctx, fn)
|
||||
}
|
||||
call := "sqlite3_create_aggregate_function_go"
|
||||
if _, ok := fn().(WindowFunction); ok {
|
||||
call = "sqlite3_create_window_function_go"
|
||||
}
|
||||
|
@ -184,11 +197,12 @@ func callbackAggregate(db *Conn, pAgg, pApp uint32) (AggregateFunction, uint32)
|
|||
|
||||
// We need to create the aggregate.
|
||||
fn := util.GetHandle(db.ctx, pApp).(func() AggregateFunction)()
|
||||
handle := util.AddHandle(db.ctx, fn)
|
||||
if pAgg != 0 {
|
||||
handle := util.AddHandle(db.ctx, fn)
|
||||
util.WriteUint32(db.mod, pAgg, handle)
|
||||
return fn, handle
|
||||
}
|
||||
return fn, handle
|
||||
return fn, 0
|
||||
}
|
||||
|
||||
func callbackArgs(db *Conn, arg []Value, pArg uint32) {
|
||||
|
|
3
vendor/github.com/ncruces/go-sqlite3/go.work.sum
generated
vendored
3
vendor/github.com/ncruces/go-sqlite3/go.work.sum
generated
vendored
|
@ -1,10 +1,13 @@
|
|||
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=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
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/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
|
|
2
vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_unix.go
generated
vendored
2
vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_unix.go
generated
vendored
|
@ -47,7 +47,7 @@ func (m *mmappedMemory) Reallocate(size uint64) []byte {
|
|||
// Commit additional memory up to new bytes.
|
||||
err := unix.Mprotect(m.buf[com:new], unix.PROT_READ|unix.PROT_WRITE)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update committed memory.
|
||||
|
|
2
vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_windows.go
generated
vendored
2
vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_windows.go
generated
vendored
|
@ -56,7 +56,7 @@ func (m *virtualMemory) Reallocate(size uint64) []byte {
|
|||
// Commit additional memory up to new bytes.
|
||||
_, err := windows.VirtualAlloc(m.addr, uintptr(new), windows.MEM_COMMIT, windows.PAGE_READWRITE)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update committed memory.
|
||||
|
|
12
vendor/github.com/ncruces/go-sqlite3/internal/util/func.go
generated
vendored
12
vendor/github.com/ncruces/go-sqlite3/internal/util/func.go
generated
vendored
|
@ -26,6 +26,7 @@ func ExportFuncVI[T0 i32](mod wazero.HostModuleBuilder, name string, fn func(con
|
|||
type funcVII[T0, T1 i32] func(context.Context, api.Module, T0, T1)
|
||||
|
||||
func (fn funcVII[T0, T1]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
_ = stack[1] // prevent bounds check on every slice access
|
||||
fn(ctx, mod, T0(stack[0]), T1(stack[1]))
|
||||
}
|
||||
|
||||
|
@ -39,6 +40,7 @@ func ExportFuncVII[T0, T1 i32](mod wazero.HostModuleBuilder, name string, fn fun
|
|||
type funcVIII[T0, T1, T2 i32] func(context.Context, api.Module, T0, T1, T2)
|
||||
|
||||
func (fn funcVIII[T0, T1, T2]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
_ = stack[2] // prevent bounds check on every slice access
|
||||
fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]))
|
||||
}
|
||||
|
||||
|
@ -52,6 +54,7 @@ func ExportFuncVIII[T0, T1, T2 i32](mod wazero.HostModuleBuilder, name string, f
|
|||
type funcVIIII[T0, T1, T2, T3 i32] func(context.Context, api.Module, T0, T1, T2, T3)
|
||||
|
||||
func (fn funcVIIII[T0, T1, T2, T3]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
_ = stack[3] // prevent bounds check on every slice access
|
||||
fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]))
|
||||
}
|
||||
|
||||
|
@ -65,6 +68,7 @@ func ExportFuncVIIII[T0, T1, T2, T3 i32](mod wazero.HostModuleBuilder, name stri
|
|||
type funcVIIIII[T0, T1, T2, T3, T4 i32] func(context.Context, api.Module, T0, T1, T2, T3, T4)
|
||||
|
||||
func (fn funcVIIIII[T0, T1, T2, T3, T4]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
_ = stack[4] // prevent bounds check on every slice access
|
||||
fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]), T4(stack[4]))
|
||||
}
|
||||
|
||||
|
@ -78,6 +82,7 @@ func ExportFuncVIIIII[T0, T1, T2, T3, T4 i32](mod wazero.HostModuleBuilder, name
|
|||
type funcVIIIIJ[T0, T1, T2, T3 i32, T4 i64] func(context.Context, api.Module, T0, T1, T2, T3, T4)
|
||||
|
||||
func (fn funcVIIIIJ[T0, T1, T2, T3, T4]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
_ = stack[4] // prevent bounds check on every slice access
|
||||
fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]), T4(stack[4]))
|
||||
}
|
||||
|
||||
|
@ -104,6 +109,7 @@ func ExportFuncII[TR, T0 i32](mod wazero.HostModuleBuilder, name string, fn func
|
|||
type funcIII[TR, T0, T1 i32] func(context.Context, api.Module, T0, T1) TR
|
||||
|
||||
func (fn funcIII[TR, T0, T1]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
_ = stack[1] // prevent bounds check on every slice access
|
||||
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1])))
|
||||
}
|
||||
|
||||
|
@ -117,6 +123,7 @@ func ExportFuncIII[TR, T0, T1 i32](mod wazero.HostModuleBuilder, name string, fn
|
|||
type funcIIII[TR, T0, T1, T2 i32] func(context.Context, api.Module, T0, T1, T2) TR
|
||||
|
||||
func (fn funcIIII[TR, T0, T1, T2]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
_ = stack[2] // prevent bounds check on every slice access
|
||||
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2])))
|
||||
}
|
||||
|
||||
|
@ -130,6 +137,7 @@ func ExportFuncIIII[TR, T0, T1, T2 i32](mod wazero.HostModuleBuilder, name strin
|
|||
type funcIIIII[TR, T0, T1, T2, T3 i32] func(context.Context, api.Module, T0, T1, T2, T3) TR
|
||||
|
||||
func (fn funcIIIII[TR, T0, T1, T2, T3]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
_ = stack[3] // prevent bounds check on every slice access
|
||||
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3])))
|
||||
}
|
||||
|
||||
|
@ -143,6 +151,7 @@ func ExportFuncIIIII[TR, T0, T1, T2, T3 i32](mod wazero.HostModuleBuilder, name
|
|||
type funcIIIIII[TR, T0, T1, T2, T3, T4 i32] func(context.Context, api.Module, T0, T1, T2, T3, T4) TR
|
||||
|
||||
func (fn funcIIIIII[TR, T0, T1, T2, T3, T4]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
_ = stack[4] // prevent bounds check on every slice access
|
||||
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]), T4(stack[4])))
|
||||
}
|
||||
|
||||
|
@ -156,6 +165,7 @@ func ExportFuncIIIIII[TR, T0, T1, T2, T3, T4 i32](mod wazero.HostModuleBuilder,
|
|||
type funcIIIIIII[TR, T0, T1, T2, T3, T4, T5 i32] func(context.Context, api.Module, T0, T1, T2, T3, T4, T5) TR
|
||||
|
||||
func (fn funcIIIIIII[TR, T0, T1, T2, T3, T4, T5]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
_ = stack[5] // prevent bounds check on every slice access
|
||||
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]), T4(stack[4]), T5(stack[5])))
|
||||
}
|
||||
|
||||
|
@ -169,6 +179,7 @@ func ExportFuncIIIIIII[TR, T0, T1, T2, T3, T4, T5 i32](mod wazero.HostModuleBuil
|
|||
type funcIIIIJ[TR, T0, T1, T2 i32, T3 i64] func(context.Context, api.Module, T0, T1, T2, T3) TR
|
||||
|
||||
func (fn funcIIIIJ[TR, T0, T1, T2, T3]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
_ = stack[3] // prevent bounds check on every slice access
|
||||
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3])))
|
||||
}
|
||||
|
||||
|
@ -182,6 +193,7 @@ func ExportFuncIIIIJ[TR, T0, T1, T2 i32, T3 i64](mod wazero.HostModuleBuilder, n
|
|||
type funcIIJ[TR, T0 i32, T1 i64] func(context.Context, api.Module, T0, T1) TR
|
||||
|
||||
func (fn funcIIJ[TR, T0, T1]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
_ = stack[1] // prevent bounds check on every slice access
|
||||
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1])))
|
||||
}
|
||||
|
||||
|
|
9
vendor/github.com/ncruces/go-sqlite3/internal/util/handle.go
generated
vendored
9
vendor/github.com/ncruces/go-sqlite3/internal/util/handle.go
generated
vendored
|
@ -35,17 +35,22 @@ func DelHandle(ctx context.Context, id uint32) error {
|
|||
s := ctx.Value(moduleKey{}).(*moduleState)
|
||||
a := s.handles[^id]
|
||||
s.handles[^id] = nil
|
||||
s.holes++
|
||||
if l := uint32(len(s.handles)); l == ^id {
|
||||
s.handles = s.handles[:l-1]
|
||||
} else {
|
||||
s.holes++
|
||||
}
|
||||
if c, ok := a.(io.Closer); ok {
|
||||
return c.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func AddHandle(ctx context.Context, a any) (id uint32) {
|
||||
func AddHandle(ctx context.Context, a any) uint32 {
|
||||
if a == nil {
|
||||
panic(NilErr)
|
||||
}
|
||||
|
||||
s := ctx.Value(moduleKey{}).(*moduleState)
|
||||
|
||||
// Find an empty slot.
|
||||
|
|
50
vendor/github.com/ncruces/go-sqlite3/quote.go
generated
vendored
50
vendor/github.com/ncruces/go-sqlite3/quote.go
generated
vendored
|
@ -3,6 +3,7 @@
|
|||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -13,6 +14,9 @@
|
|||
|
||||
// Quote escapes and quotes a value
|
||||
// making it safe to embed in SQL text.
|
||||
// Strings with embedded NUL characters are truncated.
|
||||
//
|
||||
// https://sqlite.org/lang_corefunc.html#quote
|
||||
func Quote(value any) string {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
|
@ -42,8 +46,8 @@ func Quote(value any) string {
|
|||
return "'" + v.Format(time.RFC3339Nano) + "'"
|
||||
|
||||
case string:
|
||||
if strings.IndexByte(v, 0) >= 0 {
|
||||
break
|
||||
if i := strings.IndexByte(v, 0); i >= 0 {
|
||||
v = v[:i]
|
||||
}
|
||||
|
||||
buf := make([]byte, 2+len(v)+strings.Count(v, "'"))
|
||||
|
@ -57,13 +61,13 @@ func Quote(value any) string {
|
|||
buf[i] = b
|
||||
i += 1
|
||||
}
|
||||
buf[i] = '\''
|
||||
buf[len(buf)-1] = '\''
|
||||
return unsafe.String(&buf[0], len(buf))
|
||||
|
||||
case []byte:
|
||||
buf := make([]byte, 3+2*len(v))
|
||||
buf[0] = 'x'
|
||||
buf[1] = '\''
|
||||
buf[0] = 'x'
|
||||
i := 2
|
||||
for _, b := range v {
|
||||
const hex = "0123456789ABCDEF"
|
||||
|
@ -71,26 +75,50 @@ func Quote(value any) string {
|
|||
buf[i+1] = hex[b%16]
|
||||
i += 2
|
||||
}
|
||||
buf[i] = '\''
|
||||
buf[len(buf)-1] = '\''
|
||||
return unsafe.String(&buf[0], len(buf))
|
||||
|
||||
case ZeroBlob:
|
||||
if v > ZeroBlob(1e9-3)/2 {
|
||||
break
|
||||
}
|
||||
|
||||
buf := bytes.Repeat([]byte("0"), int(3+2*int64(v)))
|
||||
buf[0] = 'x'
|
||||
buf[1] = '\''
|
||||
buf[0] = 'x'
|
||||
buf[len(buf)-1] = '\''
|
||||
return unsafe.String(&buf[0], len(buf))
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(value)
|
||||
k := v.Kind()
|
||||
|
||||
if k == reflect.Interface || k == reflect.Pointer {
|
||||
if v.IsNil() {
|
||||
return "NULL"
|
||||
}
|
||||
v = v.Elem()
|
||||
k = v.Kind()
|
||||
}
|
||||
|
||||
switch {
|
||||
case v.CanInt():
|
||||
return strconv.FormatInt(v.Int(), 10)
|
||||
case v.CanUint():
|
||||
return strconv.FormatUint(v.Uint(), 10)
|
||||
case v.CanFloat():
|
||||
return Quote(v.Float())
|
||||
case k == reflect.Bool:
|
||||
return Quote(v.Bool())
|
||||
case k == reflect.String:
|
||||
return Quote(v.String())
|
||||
case (k == reflect.Slice || k == reflect.Array && v.CanAddr()) &&
|
||||
v.Type().Elem().Kind() == reflect.Uint8:
|
||||
return Quote(v.Bytes())
|
||||
}
|
||||
|
||||
panic(util.ValueErr)
|
||||
}
|
||||
|
||||
// QuoteIdentifier escapes and quotes an identifier
|
||||
// making it safe to embed in SQL text.
|
||||
// Strings with embedded NUL characters panic.
|
||||
func QuoteIdentifier(id string) string {
|
||||
if strings.IndexByte(id, 0) >= 0 {
|
||||
panic(util.ValueErr)
|
||||
|
@ -107,6 +135,6 @@ func QuoteIdentifier(id string) string {
|
|||
buf[i] = b
|
||||
i += 1
|
||||
}
|
||||
buf[i] = '"'
|
||||
buf[len(buf)-1] = '"'
|
||||
return unsafe.String(&buf[0], len(buf))
|
||||
}
|
||||
|
|
4
vendor/github.com/ncruces/go-sqlite3/sqlite.go
generated
vendored
4
vendor/github.com/ncruces/go-sqlite3/sqlite.go
generated
vendored
|
@ -131,7 +131,7 @@ func (sqlt *sqlite) error(rc uint64, handle uint32, sql ...string) error {
|
|||
err.msg = util.ReadString(sqlt.mod, uint32(r), _MAX_LENGTH)
|
||||
}
|
||||
|
||||
if sql != nil {
|
||||
if len(sql) != 0 {
|
||||
if r := sqlt.call("sqlite3_error_offset", uint64(handle)); r != math.MaxUint32 {
|
||||
err.sql = sql[0][r:]
|
||||
}
|
||||
|
@ -301,7 +301,7 @@ func (a *arena) string(s string) uint32 {
|
|||
|
||||
func exportCallbacks(env wazero.HostModuleBuilder) wazero.HostModuleBuilder {
|
||||
util.ExportFuncII(env, "go_progress_handler", progressCallback)
|
||||
util.ExportFuncIIII(env, "go_busy_timeout", timeoutCallback)
|
||||
util.ExportFuncIII(env, "go_busy_timeout", timeoutCallback)
|
||||
util.ExportFuncIII(env, "go_busy_handler", busyCallback)
|
||||
util.ExportFuncII(env, "go_commit_hook", commitCallback)
|
||||
util.ExportFuncVI(env, "go_rollback_hook", rollbackCallback)
|
||||
|
|
57
vendor/github.com/ncruces/go-sqlite3/stmt.go
generated
vendored
57
vendor/github.com/ncruces/go-sqlite3/stmt.go
generated
vendored
|
@ -30,12 +30,13 @@ func (s *Stmt) Close() error {
|
|||
}
|
||||
|
||||
r := s.c.call("sqlite3_finalize", uint64(s.handle))
|
||||
for i := range s.c.stmts {
|
||||
if s == s.c.stmts[i] {
|
||||
l := len(s.c.stmts) - 1
|
||||
s.c.stmts[i] = s.c.stmts[l]
|
||||
s.c.stmts[l] = nil
|
||||
s.c.stmts = s.c.stmts[:l]
|
||||
stmts := s.c.stmts
|
||||
for i := range stmts {
|
||||
if s == stmts[i] {
|
||||
l := len(stmts) - 1
|
||||
stmts[i] = stmts[l]
|
||||
stmts[l] = nil
|
||||
s.c.stmts = stmts[:l]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +106,7 @@ func (s *Stmt) Busy() bool {
|
|||
//
|
||||
// https://sqlite.org/c3ref/step.html
|
||||
func (s *Stmt) Step() bool {
|
||||
s.c.checkInterrupt()
|
||||
s.c.checkInterrupt(s.c.handle)
|
||||
r := s.c.call("sqlite3_step", uint64(s.handle))
|
||||
switch r {
|
||||
case _ROW:
|
||||
|
@ -376,6 +377,15 @@ func (s *Stmt) BindValue(param int, value Value) error {
|
|||
return s.c.error(r)
|
||||
}
|
||||
|
||||
// DataCount resets the number of columns in a result set.
|
||||
//
|
||||
// https://sqlite.org/c3ref/data_count.html
|
||||
func (s *Stmt) DataCount() int {
|
||||
r := s.c.call("sqlite3_data_count",
|
||||
uint64(s.handle))
|
||||
return int(int32(r))
|
||||
}
|
||||
|
||||
// ColumnCount returns the number of columns in a result set.
|
||||
//
|
||||
// https://sqlite.org/c3ref/column_count.html
|
||||
|
@ -630,7 +640,7 @@ func (s *Stmt) Columns(dest []any) error {
|
|||
defer s.c.arena.mark()()
|
||||
count := uint64(len(dest))
|
||||
typePtr := s.c.arena.new(count)
|
||||
dataPtr := s.c.arena.new(8 * count)
|
||||
dataPtr := s.c.arena.new(count * 8)
|
||||
|
||||
r := s.c.call("sqlite3_columns_go",
|
||||
uint64(s.handle), count, uint64(typePtr), uint64(dataPtr))
|
||||
|
@ -639,26 +649,31 @@ func (s *Stmt) Columns(dest []any) error {
|
|||
}
|
||||
|
||||
types := util.View(s.c.mod, typePtr, count)
|
||||
|
||||
// Avoid bounds checks on types below.
|
||||
if len(types) != len(dest) {
|
||||
panic(util.AssertErr())
|
||||
}
|
||||
|
||||
for i := range dest {
|
||||
switch types[i] {
|
||||
case byte(INTEGER):
|
||||
dest[i] = int64(util.ReadUint64(s.c.mod, dataPtr+8*uint32(i)))
|
||||
continue
|
||||
dest[i] = int64(util.ReadUint64(s.c.mod, dataPtr))
|
||||
case byte(FLOAT):
|
||||
dest[i] = util.ReadFloat64(s.c.mod, dataPtr+8*uint32(i))
|
||||
continue
|
||||
dest[i] = util.ReadFloat64(s.c.mod, dataPtr)
|
||||
case byte(NULL):
|
||||
dest[i] = nil
|
||||
continue
|
||||
}
|
||||
ptr := util.ReadUint32(s.c.mod, dataPtr+8*uint32(i)+0)
|
||||
len := util.ReadUint32(s.c.mod, dataPtr+8*uint32(i)+4)
|
||||
buf := util.View(s.c.mod, ptr, uint64(len))
|
||||
if types[i] == byte(TEXT) {
|
||||
dest[i] = string(buf)
|
||||
} else {
|
||||
dest[i] = buf
|
||||
default:
|
||||
ptr := util.ReadUint32(s.c.mod, dataPtr+0)
|
||||
len := util.ReadUint32(s.c.mod, dataPtr+4)
|
||||
buf := util.View(s.c.mod, ptr, uint64(len))
|
||||
if types[i] == byte(TEXT) {
|
||||
dest[i] = string(buf)
|
||||
} else {
|
||||
dest[i] = buf
|
||||
}
|
||||
}
|
||||
dataPtr += 8
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
3
vendor/github.com/ncruces/go-sqlite3/time.go
generated
vendored
3
vendor/github.com/ncruces/go-sqlite3/time.go
generated
vendored
|
@ -138,6 +138,9 @@ func (f TimeFormat) Encode(t time.Time) any {
|
|||
//
|
||||
// https://sqlite.org/lang_datefunc.html
|
||||
func (f TimeFormat) Decode(v any) (time.Time, error) {
|
||||
if t, ok := v.(time.Time); ok {
|
||||
return t, nil
|
||||
}
|
||||
switch f {
|
||||
// Numeric formats.
|
||||
case TimeFormatJulianDay:
|
||||
|
|
29
vendor/github.com/ncruces/go-sqlite3/txn.go
generated
vendored
29
vendor/github.com/ncruces/go-sqlite3/txn.go
generated
vendored
|
@ -3,7 +3,6 @@
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
@ -136,23 +135,21 @@ type Savepoint struct {
|
|||
//
|
||||
// https://sqlite.org/lang_savepoint.html
|
||||
func (c *Conn) Savepoint() Savepoint {
|
||||
// Names can be reused; this makes catching bugs more likely.
|
||||
name := saveptName() + "_" + strconv.Itoa(int(rand.Int31()))
|
||||
name := callerName()
|
||||
if name == "" {
|
||||
name = "sqlite3.Savepoint"
|
||||
}
|
||||
// Names can be reused, but this makes catching bugs more likely.
|
||||
name = QuoteIdentifier(name + "_" + strconv.Itoa(int(rand.Int31())))
|
||||
|
||||
err := c.txnExecInterrupted(fmt.Sprintf("SAVEPOINT %q;", name))
|
||||
err := c.txnExecInterrupted("SAVEPOINT " + name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return Savepoint{c: c, name: name}
|
||||
}
|
||||
|
||||
func saveptName() (name string) {
|
||||
defer func() {
|
||||
if name == "" {
|
||||
name = "sqlite3.Savepoint"
|
||||
}
|
||||
}()
|
||||
|
||||
func callerName() (name string) {
|
||||
var pc [8]uintptr
|
||||
n := runtime.Callers(3, pc[:])
|
||||
if n <= 0 {
|
||||
|
@ -189,7 +186,7 @@ func (s Savepoint) Release(errp *error) {
|
|||
if s.c.GetAutocommit() { // There is nothing to commit.
|
||||
return
|
||||
}
|
||||
*errp = s.c.Exec(fmt.Sprintf("RELEASE %q;", s.name))
|
||||
*errp = s.c.Exec("RELEASE " + s.name)
|
||||
if *errp == nil {
|
||||
return
|
||||
}
|
||||
|
@ -201,10 +198,8 @@ func (s Savepoint) Release(errp *error) {
|
|||
return
|
||||
}
|
||||
// ROLLBACK and RELEASE even if interrupted.
|
||||
err := s.c.txnExecInterrupted(fmt.Sprintf(`
|
||||
ROLLBACK TO %[1]q;
|
||||
RELEASE %[1]q;
|
||||
`, s.name))
|
||||
err := s.c.txnExecInterrupted("ROLLBACK TO " +
|
||||
s.name + "; RELEASE " + s.name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -217,7 +212,7 @@ func (s Savepoint) Release(errp *error) {
|
|||
// https://sqlite.org/lang_transaction.html
|
||||
func (s Savepoint) Rollback() error {
|
||||
// ROLLBACK even if interrupted.
|
||||
return s.c.txnExecInterrupted(fmt.Sprintf("ROLLBACK TO %q;", s.name))
|
||||
return s.c.txnExecInterrupted("ROLLBACK TO " + s.name)
|
||||
}
|
||||
|
||||
func (c *Conn) txnExecInterrupted(sql string) error {
|
||||
|
|
2
vendor/github.com/ncruces/go-sqlite3/util/osutil/open_windows.go
generated
vendored
2
vendor/github.com/ncruces/go-sqlite3/util/osutil/open_windows.go
generated
vendored
|
@ -15,7 +15,7 @@ func OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
|
|||
if name == "" {
|
||||
return nil, &os.PathError{Op: "open", Path: name, Err: ENOENT}
|
||||
}
|
||||
r, e := syscallOpen(name, flag, uint32(perm.Perm()))
|
||||
r, e := syscallOpen(name, flag|O_CLOEXEC, uint32(perm.Perm()))
|
||||
if e != nil {
|
||||
return nil, &os.PathError{Op: "open", Path: name, Err: e}
|
||||
}
|
||||
|
|
19
vendor/github.com/ncruces/go-sqlite3/vfs/file.go
generated
vendored
19
vendor/github.com/ncruces/go-sqlite3/vfs/file.go
generated
vendored
|
@ -19,17 +19,18 @@ func (vfsOS) FullPathname(path string) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fi, err := os.Lstat(path)
|
||||
return path, testSymlinks(filepath.Dir(path))
|
||||
}
|
||||
|
||||
func testSymlinks(path string) error {
|
||||
p, err := filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return path, nil
|
||||
}
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
if fi.Mode()&fs.ModeSymlink != 0 {
|
||||
err = _OK_SYMLINK
|
||||
if p != path {
|
||||
return _OK_SYMLINK
|
||||
}
|
||||
return path, err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vfsOS) Delete(path string, syncDir bool) error {
|
||||
|
@ -74,7 +75,7 @@ func (vfsOS) Open(name string, flags OpenFlag) (File, OpenFlag, error) {
|
|||
}
|
||||
|
||||
func (vfsOS) OpenFilename(name *Filename, flags OpenFlag) (File, OpenFlag, error) {
|
||||
var oflags int
|
||||
oflags := _O_NOFOLLOW
|
||||
if flags&OPEN_EXCLUSIVE != 0 {
|
||||
oflags |= os.O_EXCL
|
||||
}
|
||||
|
|
3
vendor/github.com/ncruces/go-sqlite3/vfs/memdb/api.go
generated
vendored
3
vendor/github.com/ncruces/go-sqlite3/vfs/memdb/api.go
generated
vendored
|
@ -43,7 +43,8 @@ func Create(name string, data []byte) {
|
|||
}
|
||||
|
||||
// Convert data from WAL/2 to rollback journal.
|
||||
if len(data) >= 20 && (data[18] == 2 && data[19] == 2 ||
|
||||
if len(data) >= 20 && (false ||
|
||||
data[18] == 2 && data[19] == 2 ||
|
||||
data[18] == 3 && data[19] == 3) {
|
||||
data[18] = 1
|
||||
data[19] = 1
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
"os"
|
||||
)
|
||||
|
||||
const _O_NOFOLLOW = 0
|
||||
|
||||
func osAccess(path string, flags AccessFlag) error {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
|
@ -34,3 +36,12 @@ func osAccess(path string, flags AccessFlag) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func osSetMode(file *os.File, modeof string) error {
|
||||
fi, err := os.Stat(modeof)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Chmod(fi.Mode())
|
||||
return nil
|
||||
}
|
14
vendor/github.com/ncruces/go-sqlite3/vfs/os_std_mode.go
generated
vendored
14
vendor/github.com/ncruces/go-sqlite3/vfs/os_std_mode.go
generated
vendored
|
@ -1,14 +0,0 @@
|
|||
//go:build !unix || sqlite3_nosys
|
||||
|
||||
package vfs
|
||||
|
||||
import "os"
|
||||
|
||||
func osSetMode(file *os.File, modeof string) error {
|
||||
fi, err := os.Stat(modeof)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Chmod(fi.Mode())
|
||||
return nil
|
||||
}
|
2
vendor/github.com/ncruces/go-sqlite3/vfs/os_unix.go
generated
vendored
2
vendor/github.com/ncruces/go-sqlite3/vfs/os_unix.go
generated
vendored
|
@ -9,6 +9,8 @@
|
|||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const _O_NOFOLLOW = unix.O_NOFOLLOW
|
||||
|
||||
func osAccess(path string, flags AccessFlag) error {
|
||||
var access uint32 // unix.F_OK
|
||||
switch flags {
|
||||
|
|
13
vendor/github.com/ncruces/go-sqlite3/vtab.go
generated
vendored
13
vendor/github.com/ncruces/go-sqlite3/vtab.go
generated
vendored
|
@ -57,9 +57,12 @@ func CreateModule[T VTab](db *Conn, name string, create, connect VTabConstructor
|
|||
flags |= VTAB_SHADOWTABS
|
||||
}
|
||||
|
||||
var modulePtr uint32
|
||||
defer db.arena.mark()()
|
||||
namePtr := db.arena.string(name)
|
||||
modulePtr := util.AddHandle(db.ctx, module[T]{create, connect})
|
||||
if connect != nil {
|
||||
modulePtr = util.AddHandle(db.ctx, module[T]{create, connect})
|
||||
}
|
||||
r := db.call("sqlite3_create_module_go", uint64(db.handle),
|
||||
uint64(namePtr), uint64(flags), uint64(modulePtr))
|
||||
return db.error(r)
|
||||
|
@ -352,8 +355,9 @@ func (idx *IndexInfo) load() {
|
|||
idx.OrderBy = make([]IndexOrderBy, util.ReadUint32(mod, ptr+8))
|
||||
|
||||
constraintPtr := util.ReadUint32(mod, ptr+4)
|
||||
constraint := idx.Constraint
|
||||
for i := range idx.Constraint {
|
||||
idx.Constraint[i] = IndexConstraint{
|
||||
constraint[i] = IndexConstraint{
|
||||
Column: int(int32(util.ReadUint32(mod, constraintPtr+0))),
|
||||
Op: IndexConstraintOp(util.ReadUint8(mod, constraintPtr+4)),
|
||||
Usable: util.ReadUint8(mod, constraintPtr+5) != 0,
|
||||
|
@ -362,8 +366,9 @@ func (idx *IndexInfo) load() {
|
|||
}
|
||||
|
||||
orderByPtr := util.ReadUint32(mod, ptr+12)
|
||||
for i := range idx.OrderBy {
|
||||
idx.OrderBy[i] = IndexOrderBy{
|
||||
orderBy := idx.OrderBy
|
||||
for i := range orderBy {
|
||||
orderBy[i] = IndexOrderBy{
|
||||
Column: int(int32(util.ReadUint32(mod, orderByPtr+0))),
|
||||
Desc: util.ReadUint8(mod, orderByPtr+4) != 0,
|
||||
}
|
||||
|
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
|
@ -518,7 +518,7 @@ github.com/modern-go/reflect2
|
|||
# github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
|
||||
## explicit
|
||||
github.com/munnerz/goautoneg
|
||||
# github.com/ncruces/go-sqlite3 v0.18.4
|
||||
# github.com/ncruces/go-sqlite3 v0.19.0
|
||||
## explicit; go 1.21
|
||||
github.com/ncruces/go-sqlite3
|
||||
github.com/ncruces/go-sqlite3/driver
|
||||
|
|
|
@ -41,6 +41,12 @@ main {
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
.boosted {
|
||||
padding: 0 0.75rem 0.75rem;
|
||||
color: var(--fg-reduced);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-header > address {
|
||||
/*
|
||||
Avoid stretching so wide that user
|
||||
|
@ -59,17 +65,27 @@ main {
|
|||
"avatar author-strap author-strap";
|
||||
gap: 0 0.5rem;
|
||||
font-style: normal;
|
||||
|
||||
|
||||
.avatar {
|
||||
grid-area: avatar;
|
||||
height: 3.5rem;
|
||||
width: 3.5rem;
|
||||
object-fit: cover;
|
||||
|
||||
position: relative;
|
||||
|
||||
border: 0.15rem solid $avatar-border;
|
||||
border-radius: $br;
|
||||
overflow: hidden; /* hides corners from img overflowing */
|
||||
|
||||
|
||||
.boosted-avatar {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
@ -77,7 +93,7 @@ main {
|
|||
background: $bg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.author-strap {
|
||||
grid-area: author-strap;
|
||||
display: grid;
|
||||
|
@ -87,7 +103,7 @@ main {
|
|||
"display display"
|
||||
"user user";
|
||||
gap: 0 0.5rem;
|
||||
|
||||
|
||||
.displayname, .username {
|
||||
justify-self: start;
|
||||
align-self: start;
|
||||
|
@ -95,12 +111,12 @@ main {
|
|||
font-size: 1rem;
|
||||
line-height: 1.3rem;
|
||||
}
|
||||
|
||||
|
||||
.displayname {
|
||||
grid-area: display;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.username {
|
||||
grid-area: user;
|
||||
color: $link-fg;
|
||||
|
@ -200,34 +216,34 @@ main {
|
|||
.poll {
|
||||
background-color: $gray2;
|
||||
z-index: 2;
|
||||
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: $br;
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
gap: 1rem;
|
||||
|
||||
|
||||
.poll-options {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
|
||||
.poll-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
|
||||
|
||||
label {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
|
||||
meter {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.poll-vote-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -236,7 +252,7 @@ main {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.poll-info {
|
||||
background-color: $gray4;
|
||||
display: flex;
|
||||
|
@ -245,7 +261,7 @@ main {
|
|||
border-radius: $br-inner;
|
||||
padding: 0.25rem;
|
||||
gap: 0.25rem;
|
||||
|
||||
|
||||
span {
|
||||
justify-self: center;
|
||||
white-space: nowrap;
|
||||
|
@ -301,12 +317,12 @@ main {
|
|||
width: 100%;
|
||||
z-index: 3;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
display: grid;
|
||||
padding: 1rem;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
grid-template-areas:
|
||||
grid-template-areas:
|
||||
"eye sensitive ."
|
||||
". sensitive .";
|
||||
|
||||
|
@ -369,7 +385,7 @@ main {
|
|||
height: 100%;
|
||||
padding: 0.8rem;
|
||||
border: 0.2rem dashed $white2;
|
||||
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@ -518,4 +534,4 @@ main {
|
|||
.plyr {
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ function UserProfileForm({ data: profile }) {
|
|||
maxPinnedFields: instance?.configuration?.accounts?.max_profile_fields ?? 6
|
||||
};
|
||||
}, [instance]);
|
||||
|
||||
|
||||
// Parse out available theme options into nice format.
|
||||
const { data: themes } = useAccountThemesQuery();
|
||||
const themeOptions = useMemo(() => {
|
||||
|
@ -114,6 +114,7 @@ function UserProfileForm({ data: profile }) {
|
|||
locked: useBoolInput("locked", { source: profile }),
|
||||
discoverable: useBoolInput("discoverable", { source: profile}),
|
||||
enableRSS: useBoolInput("enable_rss", { source: profile }),
|
||||
hideBoosts: useBoolInput("hide_boosts", { source: profile }),
|
||||
hideCollections: useBoolInput("hide_collections", { source: profile }),
|
||||
webVisibility: useTextInput("web_visibility", { source: profile, valueSelector: (p) => p.source?.web_visibility }),
|
||||
fields: useFieldArrayInput("fields_attributes", {
|
||||
|
@ -158,7 +159,7 @@ function UserProfileForm({ data: profile }) {
|
|||
autoCapitalize="sentences"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="file-input-with-image-description">
|
||||
<FileInput
|
||||
label="Avatar (1:1 images look best)"
|
||||
|
@ -257,6 +258,10 @@ function UserProfileForm({ data: profile }) {
|
|||
field={form.enableRSS}
|
||||
label="Enable RSS feed of posts."
|
||||
/>
|
||||
<Checkbox
|
||||
field={form.hideBoosts}
|
||||
label="Hide boosts from your public page"
|
||||
/>
|
||||
<Checkbox
|
||||
field={form.hideCollections}
|
||||
label="Hide who you follow / are followed by."
|
||||
|
|
|
@ -247,6 +247,16 @@
|
|||
class="status expanded"
|
||||
{{- includeAttr "status_attributes.tmpl" . | indentAttr 6 }}
|
||||
>
|
||||
{{- if .ReblogAccount }}
|
||||
<div class="boosted text-cutoff">
|
||||
<i class="fa fa-retweet" aria-hidden="true"></i> 
|
||||
{{- if $.account.DisplayName }}
|
||||
{{- emojify $.account.Emojis (escape $.account.DisplayName) }} boosted
|
||||
{{- else }}
|
||||
{{- $.account.Username }} boosted
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- end }}
|
||||
{{- include "status.tmpl" . | indent 6 }}
|
||||
</article>
|
||||
{{- end }}
|
||||
|
@ -264,4 +274,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
@ -48,6 +48,16 @@
|
|||
alt="Avatar for {{ .Username -}}"
|
||||
title="Avatar for {{ .Username -}}"
|
||||
>
|
||||
{{ if $.ReblogAccount }}
|
||||
<img
|
||||
class="boosted-avatar"
|
||||
src="{{ $.ReblogAccount.Avatar }}"
|
||||
alt="Avatar for {{ $.ReblogAccount.Username -}}"
|
||||
title="Avatar for {{ $.ReblogAccount.Username -}}"
|
||||
>
|
||||
{{ end }}
|
||||
|
||||
|
||||
</picture>
|
||||
<div class="author-strap">
|
||||
<span class="displayname text-cutoff">
|
||||
|
@ -63,4 +73,4 @@
|
|||
<span class="sr-only">(open profile)</span>
|
||||
</a>
|
||||
</address>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
Loading…
Reference in a new issue