mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-26 05:36:38 +00:00
Compare commits
9 commits
e07f0ec928
...
600969a215
Author | SHA1 | Date | |
---|---|---|---|
600969a215 | |||
095663f5cc | |||
18b7e00fef | |||
504b3a9162 | |||
40c33ccc49 | |||
90b773ae2a | |||
4b7d7f9b8b | |||
af5a766f62 | |||
d9e59820ed |
|
@ -16,3 +16,4 @@ We consider these topics advanced because applying them incorrectly does have th
|
|||
* [Tracing](tracing.md)
|
||||
* [Metrics](metrics.md)
|
||||
* [Replicating SQLite](replicating-sqlite.md)
|
||||
* [SQLite on networked storage](sqlite-networked-storage.md)
|
||||
|
|
35
docs/advanced/sqlite-networked-storage.md
Normal file
35
docs/advanced/sqlite-networked-storage.md
Normal file
|
@ -0,0 +1,35 @@
|
|||
# SQLite on networked storage
|
||||
|
||||
SQLite's operating model assumes the database and the processes or applications using it are colocated on the same host. When running the database in WAL-mode, which is GoToSocial's default, it relies on shared memory between processes to ensure the integrity of your database.
|
||||
|
||||
!!! quote
|
||||
All processes using a database must be on the same host computer; WAL does not work over a network filesystem. This is because WAL requires all processes to share a small amount of memory and processes on separate host machines obviously cannot share memory with each other.
|
||||
|
||||
— SQLite.org [Write-Ahead Logging](https://www.sqlite.org/wal.html)
|
||||
|
||||
This also means that any other processes accessing the database need to run in the same namespace or container context.
|
||||
|
||||
It is in theory possible to run SQLite over Samba, NFS, iSCSI or other forms of filesystems accessed over the network. But it is neither recommended nor supported by the SQLite maintainers, irrespective of whether you're running with write-ahead logging or not. Doing so puts you at risk of database corruption. There is a long history of networked storage having synchronisation issues in their locking primitives, or implementing them with weaker guarantees than what a local filesystem can provide.
|
||||
|
||||
Your cloud provider's external volumes, like Hetzner Cloud Volumes, AWS EBS, GCP Persistent Disk etc. may also cause problems, and add variable latency. This has a tendency to severely degrade SQLite's performance.
|
||||
|
||||
If you're going to access your database over the network, it's better to use a database with a client-server architecture. GoToSocial supports Postgres for such use-cases.
|
||||
|
||||
For the purpose of having a copy of the SQLite database on durable long-term storage, refer to [SQLite streaming replication](replicating-sqlite.md) instead. Remember that neither replication nor using a networked filesystem are a substitute [for having backups](../admin/backup_and_restore.md).
|
||||
|
||||
## Settings
|
||||
|
||||
!!! danger "Corrupted database"
|
||||
We do not support running GoToSocial with SQLite on a networked filesystem and we will not be able to help you if you damage your database this way.
|
||||
|
||||
Should you really want to take this risk, you'll need to adjust the SQLite [synchronous][sqlite-sync] mode and [journal][sqlite-journal] mode to match the limitations of the filesystem.
|
||||
|
||||
[sqlite-sync]: https://www.sqlite.org/pragma.html#pragma_synchronous
|
||||
[sqlite-journal]: https://www.sqlite.org/pragma.html#pragma_journal_mode
|
||||
|
||||
You'll need to update the following settings:
|
||||
|
||||
* `db-sqlite-journal-mode`
|
||||
* `db-sqlite-synchronous`
|
||||
|
||||
We don't provide any recommendations as this will vary based on the solution you're using. See [this issue](https://github.com/superseriousbusiness/gotosocial/issues/3360#issuecomment-2380332027) for what you could potentially set those values to.
|
|
@ -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.
|
||||
|
@ -2284,6 +2290,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.
|
||||
|
|
|
@ -65,14 +65,16 @@ If you decide to use a VPS instead, you can spin yourself up something cheap wit
|
|||
|
||||
[Greenhost](https://greenhost.net) is also great: it has zero CO2 emissions, but is a bit more costly. Their 1GB, 1-cpu VPS works great for a single-user or small instance.
|
||||
|
||||
!!! warning "Cloud storage volumes"
|
||||
Not all cloud VPS storage offerings are equal, and just because something claims to be backed by an SSD doesn't mean that it will necessarily be suitable to run a GoToSocial instance.
|
||||
|
||||
The [performance of Hetzner Cloud Volumes](https://github.com/superseriousbusiness/gotosocial/issues/2471#issuecomment-1891098323) is not guaranteed and seems to have very volatile latency. This will result in your GoToSocial instance performing poorly.
|
||||
|
||||
!!! danger "Oracle Free Tier"
|
||||
[Oracle Cloud Free Tier](https://www.oracle.com/cloud/free/) servers are not suitable for a GoToSocial deployment if you intend to federate with more than a handful of other instances and users.
|
||||
|
||||
GoToSocial admins running on Oracle Cloud Free Tier have reported that their instances become extremely slow or unresponsive during periods of moderate load. This is most likely due to memory or storage latency, which causes even simple database queries to take a long time to run.
|
||||
|
||||
!!! danger "Hetzner Cloud Volume"
|
||||
The [performance of Hetzner Cloud Volumes](https://github.com/superseriousbusiness/gotosocial/issues/2471#issuecomment-1891098323) is not guaranteed and seems to have very volatile latency. You're going to have a bad time running your database on those, with extremely poor query performance for even the most basic operations. Before filing performance issues against GoToSocial, make sure the problems reproduce with local storage.
|
||||
|
||||
### Distribution system requirements
|
||||
|
||||
Please make sure to check on your distribution system requirments, especially memory. Many distributions have baseline requirements and running them on a system that doesn't meet them will cause problems without further tuning and tweaking on your part.
|
||||
|
@ -99,13 +101,15 @@ GoToSocial supports both SQLite and Postgres as database drivers. Though it is p
|
|||
|
||||
SQLite is the default driver and it's been shown to work brilliantly for instances in the range of 1-30 users (or maybe more).
|
||||
|
||||
!!! danger "SQLite on networked storage"
|
||||
Don't put your SQLite database on remote storage, whether that's NFS/Samba, iSCSI volumes, things like Ceph/Gluster or your cloud provider's network volume storage solution.
|
||||
|
||||
See [SQLite on networked storage](../advanced/sqlite-networked-storage.md) for further information.
|
||||
|
||||
If you're planning on hosting more people than this on an instance, you may wish to use Postgres instead, as it offers the possibility of database clustering and redundancy, at the cost of some complexity.
|
||||
|
||||
Regardless of which database driver you choose, for proper performance they should be run on fast storage that operates with low and stable latency. It is possible to run databases on network attached storage, but this adds variable latency and network congestion to the mix, as well as potential I/O contention on the origin storage.
|
||||
|
||||
!!! danger "Cloud Storage Volumes"
|
||||
Not all cloud VPS storage offerings are equal, and just because something claims to be backed by an SSD doesn't mean that it will necessarily be suitable to run a GoToSocial instance on. Please see the [Server/VPS section](#vps) section below.
|
||||
|
||||
!!! tip
|
||||
Please [backup your database](../admin/backup_and_restore.md). The database contains encryption keys for the instance and any user accounts. You won't be able to federate again from the same domain if you lose these keys!
|
||||
|
||||
|
|
|
@ -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
|
@ -22,7 +22,7 @@ require (
|
|||
codeberg.org/gruf/go-runners v1.6.3
|
||||
codeberg.org/gruf/go-sched v1.2.4
|
||||
codeberg.org/gruf/go-storage v0.2.0
|
||||
codeberg.org/gruf/go-structr v0.8.9
|
||||
codeberg.org/gruf/go-structr v0.8.10
|
||||
codeberg.org/superseriousbusiness/exif-terminator v0.9.0
|
||||
github.com/DmitriyVTitov/size v1.5.0
|
||||
github.com/KimMachineGun/automemlimit v0.6.1
|
||||
|
|
4
go.sum
4
go.sum
|
@ -72,8 +72,8 @@ codeberg.org/gruf/go-sched v1.2.4 h1:ddBB9o0D/2oU8NbQ0ldN5aWxogpXPRBATWi58+p++Hw
|
|||
codeberg.org/gruf/go-sched v1.2.4/go.mod h1:wad6l+OcYGWMA2TzNLMmLObsrbBDxdJfEy5WvTgBjNk=
|
||||
codeberg.org/gruf/go-storage v0.2.0 h1:mKj3Lx6AavEkuXXtxqPhdq+akW9YwrnP16yQBF7K5ZI=
|
||||
codeberg.org/gruf/go-storage v0.2.0/go.mod h1:o3GzMDE5QNUaRnm/daUzFqvuAaC4utlgXDXYO79sWKU=
|
||||
codeberg.org/gruf/go-structr v0.8.9 h1:OyiSspWYCeJOm356fFPd+bDRumPrard2VAUXAPqZiJ0=
|
||||
codeberg.org/gruf/go-structr v0.8.9/go.mod h1:zkoXVrAnKosh8VFAsbP/Hhs8FmLBjbVVy5w/Ngm8ApM=
|
||||
codeberg.org/gruf/go-structr v0.8.10 h1:uSapW97/StRnYEhCtycaM0isCsEMYC+tx/knYr6SiVo=
|
||||
codeberg.org/gruf/go-structr v0.8.10/go.mod h1:zkoXVrAnKosh8VFAsbP/Hhs8FmLBjbVVy5w/Ngm8ApM=
|
||||
codeberg.org/superseriousbusiness/exif-terminator v0.9.0 h1:/EfyGI6HIrbkhFwgXGSjZ9o1kr/+k8v4mKdfXTH02Go=
|
||||
codeberg.org/superseriousbusiness/exif-terminator v0.9.0/go.mod h1:gCWKduudUWFzsnixoMzu0FYVdxHWG+AbXnZ50DqxsUE=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -73,7 +73,7 @@ func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnly() {
|
|||
suite.Equal(apimodel.VisibilityPublic, s.Visibility)
|
||||
}
|
||||
|
||||
suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned=false&only_media=false&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01G36SF3V6Y6V5BF9P4R7PQG7G&exclude_replies=false&exclude_reblogs=false&pinned=false&only_media=false&only_public=true>; rel="prev"`, result.Header.Get("link"))
|
||||
suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned=false&only_media=false&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01J5QVB9VC76NPPRQ207GG4DRZ&exclude_replies=false&exclude_reblogs=false&pinned=false&only_media=false&only_public=true>; rel="prev"`, result.Header.Get("link"))
|
||||
}
|
||||
|
||||
func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnlyMediaOnly() {
|
||||
|
|
|
@ -591,7 +591,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostImplicitAccept() {
|
|||
"text": "Hi @1happyturtle, can I reply?",
|
||||
"uri": "http://localhost:8080/some/determinate/url",
|
||||
"url": "http://localhost:8080/some/determinate/url",
|
||||
"visibility": "unlisted"
|
||||
"visibility": "public"
|
||||
},
|
||||
"reblogged": true,
|
||||
"reblogs_count": 0,
|
||||
|
@ -601,7 +601,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostImplicitAccept() {
|
|||
"tags": [],
|
||||
"uri": "http://localhost:8080/some/determinate/url",
|
||||
"url": "http://localhost:8080/some/determinate/url",
|
||||
"visibility": "unlisted"
|
||||
"visibility": "public"
|
||||
}`, out)
|
||||
|
||||
// Target status should no
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
|
@ -185,13 +186,24 @@ func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
|
|||
// Fave a status that's pending approval by us.
|
||||
func (suite *StatusFaveTestSuite) TestPostFaveImplicitAccept() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
targetStatus = suite.testStatuses["admin_account_status_5"]
|
||||
app = suite.testApplications["application_1"]
|
||||
token = suite.testTokens["local_account_2"]
|
||||
user = suite.testUsers["local_account_2"]
|
||||
account = suite.testAccounts["local_account_2"]
|
||||
visFilter = visibility.NewFilter(&suite.state)
|
||||
)
|
||||
|
||||
// Check visibility of status to public before posting fave.
|
||||
visible, err := visFilter.StatusVisible(ctx, nil, targetStatus)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if visible {
|
||||
suite.FailNow("status should not be visible yet")
|
||||
}
|
||||
|
||||
out, recorder := suite.postStatusFave(
|
||||
targetStatus.ID,
|
||||
app,
|
||||
|
@ -268,30 +280,40 @@ func (suite *StatusFaveTestSuite) TestPostFaveImplicitAccept() {
|
|||
"text": "Hi @1happyturtle, can I reply?",
|
||||
"uri": "http://localhost:8080/some/determinate/url",
|
||||
"url": "http://localhost:8080/some/determinate/url",
|
||||
"visibility": "unlisted"
|
||||
"visibility": "public"
|
||||
}`, out)
|
||||
|
||||
// Target status should no
|
||||
// longer be pending approval.
|
||||
dbStatus, err := suite.state.DB.GetStatusByID(
|
||||
context.Background(),
|
||||
ctx,
|
||||
targetStatus.ID,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.False(*dbStatus.PendingApproval)
|
||||
suite.NotEmpty(dbStatus.ApprovedByURI)
|
||||
|
||||
// There should be an Accept
|
||||
// stored for the target status.
|
||||
intReq, err := suite.state.DB.GetInteractionRequestByInteractionURI(
|
||||
context.Background(), targetStatus.URI,
|
||||
ctx, targetStatus.URI,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.NotZero(intReq.AcceptedAt)
|
||||
suite.NotEmpty(intReq.URI)
|
||||
|
||||
// Check visibility of status to public after posting fave.
|
||||
visible, err = visFilter.StatusVisible(ctx, nil, dbStatus)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if !visible {
|
||||
suite.FailNow("status should be visible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusFaveTestSuite(t *testing.T) {
|
||||
|
|
|
@ -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"`
|
||||
|
|
2
internal/cache/util.go
vendored
2
internal/cache/util.go
vendored
|
@ -18,7 +18,6 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
|
@ -42,7 +41,6 @@ func ignoreErrors(err error) bool {
|
|||
// (until invalidation).
|
||||
db.ErrNoEntries,
|
||||
db.ErrAlreadyExists,
|
||||
sql.ErrNoRows,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
12
internal/cache/visibility.go
vendored
12
internal/cache/visibility.go
vendored
|
@ -48,9 +48,15 @@ func (c *Caches) initVisibility() {
|
|||
{Fields: "RequesterID", Multiple: true},
|
||||
{Fields: "Type,RequesterID,ItemID"},
|
||||
},
|
||||
MaxSize: cap,
|
||||
IgnoreErr: ignoreErrors,
|
||||
Copy: copyF,
|
||||
MaxSize: cap,
|
||||
IgnoreErr: func(err error) bool {
|
||||
// don't cache any errors,
|
||||
// it gets a little too tricky
|
||||
// otherwise with ensuring
|
||||
// errors are cleared out
|
||||
return true
|
||||
},
|
||||
Copy: copyF,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -26,8 +26,10 @@
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
@ -84,6 +86,53 @@ func(request *gtsmodel.InteractionRequest) error {
|
|||
)
|
||||
}
|
||||
|
||||
func (i *interactionDB) GetInteractionRequestsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.InteractionRequest, error) {
|
||||
// Load all interaction request IDs via cache loader callbacks.
|
||||
requests, err := i.state.Caches.DB.InteractionRequest.LoadIDs("ID",
|
||||
ids,
|
||||
func(uncached []string) ([]*gtsmodel.InteractionRequest, error) {
|
||||
// Preallocate expected length of uncached interaction requests.
|
||||
requests := make([]*gtsmodel.InteractionRequest, 0, len(uncached))
|
||||
|
||||
// Perform database query scanning
|
||||
// the remaining (uncached) IDs.
|
||||
if err := i.db.NewSelect().
|
||||
Model(&requests).
|
||||
Where("? IN (?)", bun.Ident("id"), bun.In(uncached)).
|
||||
Scan(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return requests, nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reorder the requests by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(r *gtsmodel.InteractionRequest) string { return r.ID }
|
||||
util.OrderBy(requests, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
return requests, nil
|
||||
}
|
||||
|
||||
// Populate all loaded interaction requests, removing those we
|
||||
// fail to populate (removes needing so many nil checks everywhere).
|
||||
requests = slices.DeleteFunc(requests, func(request *gtsmodel.InteractionRequest) bool {
|
||||
if err := i.PopulateInteractionRequest(ctx, request); err != nil {
|
||||
log.Errorf(ctx, "error populating %s: %v", request.ID, err)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return requests, nil
|
||||
}
|
||||
|
||||
func (i *interactionDB) getInteractionRequest(
|
||||
ctx context.Context,
|
||||
lookup string,
|
||||
|
@ -205,13 +254,18 @@ func (i *interactionDB) UpdateInteractionRequest(ctx context.Context, request *g
|
|||
}
|
||||
|
||||
func (i *interactionDB) DeleteInteractionRequestByID(ctx context.Context, id string) error {
|
||||
defer i.state.Caches.DB.InteractionRequest.Invalidate("ID", id)
|
||||
// Delete interaction request by ID.
|
||||
if _, err := i.db.NewDelete().
|
||||
Table("interaction_requests").
|
||||
Where("? = ?", bun.Ident("id"), id).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := i.db.NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("interaction_requests"), bun.Ident("interaction_request")).
|
||||
Where("? = ?", bun.Ident("interaction_request.id"), id).
|
||||
Exec(ctx)
|
||||
return err
|
||||
// Invalidate cached interaction request with ID.
|
||||
i.state.Caches.DB.InteractionRequest.Invalidate("ID", id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *interactionDB) GetInteractionsRequestsForAcct(
|
||||
|
@ -317,19 +371,8 @@ func (i *interactionDB) GetInteractionsRequestsForAcct(
|
|||
slices.Reverse(reqIDs)
|
||||
}
|
||||
|
||||
// For each interaction request ID,
|
||||
// select the interaction request.
|
||||
reqs := make([]*gtsmodel.InteractionRequest, 0, len(reqIDs))
|
||||
for _, id := range reqIDs {
|
||||
req, err := i.GetInteractionRequestByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqs = append(reqs, req)
|
||||
}
|
||||
|
||||
return reqs, nil
|
||||
// Load all interaction requests by their IDs.
|
||||
return i.GetInteractionRequestsByIDs(ctx, reqIDs)
|
||||
}
|
||||
|
||||
func (i *interactionDB) IsInteractionRejected(ctx context.Context, interactionURI string) (bool, error) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -74,7 +74,8 @@ func (suite *TimelineTestSuite) publicCount() int {
|
|||
var publicCount int
|
||||
for _, status := range suite.testStatuses {
|
||||
if status.Visibility == gtsmodel.VisibilityPublic &&
|
||||
status.BoostOfID == "" {
|
||||
status.BoostOfID == "" &&
|
||||
!util.PtrOrZero(status.PendingApproval) {
|
||||
publicCount++
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,6 +168,9 @@ func (suite *StatusVisibleTestSuite) TestVisiblePending() {
|
|||
testStatus := new(gtsmodel.Status)
|
||||
*testStatus = *suite.testStatuses["admin_account_status_3"]
|
||||
testStatus.PendingApproval = util.Ptr(true)
|
||||
if err := suite.state.DB.UpdateStatus(ctx, testStatus); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
for _, testCase := range []struct {
|
||||
acct *gtsmodel.Account
|
||||
|
@ -198,6 +201,43 @@ func (suite *StatusVisibleTestSuite) TestVisiblePending() {
|
|||
suite.NoError(err)
|
||||
suite.Equal(testCase.visible, visible)
|
||||
}
|
||||
|
||||
// Update the status to mark it as approved.
|
||||
testStatus.PendingApproval = util.Ptr(false)
|
||||
testStatus.ApprovedByURI = "http://localhost:8080/some/accept/uri"
|
||||
if err := suite.state.DB.UpdateStatus(ctx, testStatus); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
for _, testCase := range []struct {
|
||||
acct *gtsmodel.Account
|
||||
visible bool
|
||||
}{
|
||||
{
|
||||
acct: suite.testAccounts["admin_account"],
|
||||
visible: true, // Own status, always visible.
|
||||
},
|
||||
{
|
||||
acct: suite.testAccounts["local_account_1"],
|
||||
visible: true, // Reply to zork, always visible.
|
||||
},
|
||||
{
|
||||
acct: suite.testAccounts["local_account_2"],
|
||||
visible: true, // Should be visible now.
|
||||
},
|
||||
{
|
||||
acct: suite.testAccounts["remote_account_1"],
|
||||
visible: true, // Should be visible now.
|
||||
},
|
||||
{
|
||||
acct: nil, // Unauthed request.
|
||||
visible: true, // Should be visible now (public status).
|
||||
},
|
||||
} {
|
||||
visible, err := suite.filter.StatusVisible(ctx, testCase.acct, testStatus)
|
||||
suite.NoError(err)
|
||||
suite.Equal(testCase.visible, visible)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatusVisibleTestSuite) TestVisibleLocalOnly() {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
|
@ -32,6 +33,7 @@
|
|||
// Request wraps an HTTP request
|
||||
// to add our own retry / backoff.
|
||||
type Request struct {
|
||||
|
||||
// Current backoff dur.
|
||||
backoff time.Duration
|
||||
|
||||
|
@ -57,8 +59,7 @@ func WrapRequest(r *http.Request) *Request {
|
|||
// Only add content-type header if a request body exists.
|
||||
entry = entry.WithField("contentType", r.Header.Get("Content-Type"))
|
||||
}
|
||||
// note our formatting library follows ptr values
|
||||
entry = entry.WithField("attempt", &rr.attempts)
|
||||
entry = entry.WithField("attempt", uintPtr{&rr.attempts})
|
||||
rr.Entry = entry
|
||||
return rr
|
||||
}
|
||||
|
@ -73,3 +74,12 @@ func (r *Request) BackOff() time.Duration {
|
|||
}
|
||||
return r.backoff
|
||||
}
|
||||
|
||||
type uintPtr struct{ u *uint }
|
||||
|
||||
func (f uintPtr) String() string {
|
||||
if f.u == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return strconv.FormatUint(uint64(*f.u), 10)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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",
|
||||
|
@ -1744,7 +1745,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToAPIStatusPendingApproval()
|
|||
"in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"visibility": "unlisted",
|
||||
"visibility": "public",
|
||||
"language": null,
|
||||
"uri": "http://localhost:8080/users/admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||
"url": "http://localhost:8080/@admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||
|
@ -3177,7 +3178,7 @@ func (suite *InternalToFrontendTestSuite) TestIntReqToAPI() {
|
|||
"in_reply_to_account_id": null,
|
||||
"sensitive": true,
|
||||
"spoiler_text": "you won't be able to reply to this without my approval",
|
||||
"visibility": "unlisted",
|
||||
"visibility": "public",
|
||||
"language": "en",
|
||||
"uri": "http://localhost:8080/users/1happyturtle/statuses/01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||
"url": "http://localhost:8080/@1happyturtle/statuses/01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||
|
@ -3269,7 +3270,7 @@ func (suite *InternalToFrontendTestSuite) TestIntReqToAPI() {
|
|||
"in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"visibility": "unlisted",
|
||||
"visibility": "public",
|
||||
"language": null,
|
||||
"uri": "http://localhost:8080/users/admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||
"url": "http://localhost:8080/@admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -116,6 +116,7 @@ nav:
|
|||
- "advanced/tracing.md"
|
||||
- "advanced/metrics.md"
|
||||
- "advanced/replicating-sqlite.md"
|
||||
- "advanced/sqlite-networked-storage.md"
|
||||
|
||||
- "Admin":
|
||||
- "admin/settings.md"
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
@ -1531,7 +1535,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
BoostOfID: "",
|
||||
BoostOfAccountID: "",
|
||||
ThreadID: "01HCWE4P0EW9HBA5WHW97D5YV0",
|
||||
Visibility: gtsmodel.VisibilityUnlocked,
|
||||
Visibility: gtsmodel.VisibilityPublic,
|
||||
Sensitive: util.Ptr(false),
|
||||
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||
Federated: util.Ptr(true),
|
||||
|
@ -1811,7 +1815,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
BoostOfID: "",
|
||||
ThreadID: "01HCWE4P0EW9HBA5WHW97D5YV0",
|
||||
ContentWarning: "you won't be able to reply to this without my approval",
|
||||
Visibility: gtsmodel.VisibilityUnlocked,
|
||||
Visibility: gtsmodel.VisibilityPublic,
|
||||
Sensitive: util.Ptr(true),
|
||||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||
|
|
14
vendor/codeberg.org/gruf/go-structr/cache.go
generated
vendored
14
vendor/codeberg.org/gruf/go-structr/cache.go
generated
vendored
|
@ -575,8 +575,9 @@ func (c *Cache[T]) store_value(index *Index, key string, value T) {
|
|||
item.data = value
|
||||
|
||||
if index != nil {
|
||||
// Append item to index.
|
||||
index.append(key, item)
|
||||
// Append item to index a key
|
||||
// was already generated for.
|
||||
index.append(&c.lru, key, item)
|
||||
}
|
||||
|
||||
// Get ptr to value data.
|
||||
|
@ -607,8 +608,8 @@ func (c *Cache[T]) store_value(index *Index, key string, value T) {
|
|||
continue
|
||||
}
|
||||
|
||||
// Append item to index.
|
||||
idx.append(key, item)
|
||||
// Append item to this index.
|
||||
idx.append(&c.lru, key, item)
|
||||
}
|
||||
|
||||
// Add item to main lru list.
|
||||
|
@ -645,8 +646,9 @@ func (c *Cache[T]) store_error(index *Index, key string, err error) {
|
|||
// Set error val.
|
||||
item.data = err
|
||||
|
||||
// Append item to index.
|
||||
index.append(key, item)
|
||||
// Append item to index a key
|
||||
// was already generated for.
|
||||
index.append(&c.lru, key, item)
|
||||
|
||||
// Add item to main lru list.
|
||||
c.lru.push_front(&item.elem)
|
||||
|
|
37
vendor/codeberg.org/gruf/go-structr/index.go
generated
vendored
37
vendor/codeberg.org/gruf/go-structr/index.go
generated
vendored
|
@ -174,7 +174,7 @@ func (i *Index) init(t reflect.Type, cfg IndexConfig, cap int) {
|
|||
// get_one will fetch one indexed item under key.
|
||||
func (i *Index) get_one(key Key) *indexed_item {
|
||||
// Get list at hash.
|
||||
l, _ := i.data.Get(key.key)
|
||||
l := i.data.Get(key.key)
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -192,7 +192,7 @@ func (i *Index) get(key string, hook func(*indexed_item)) {
|
|||
}
|
||||
|
||||
// Get list at hash.
|
||||
l, _ := i.data.Get(key)
|
||||
l := i.data.Get(key)
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
|
@ -237,11 +237,12 @@ func (i *Index) key(buf *byteutil.Buffer, parts []unsafe.Pointer) string {
|
|||
}
|
||||
|
||||
// append will append the given index entry to appropriate
|
||||
// doubly-linked-list in index hashmap. this handles case
|
||||
// of key collisions and overwriting 'unique' entries.
|
||||
func (i *Index) append(key string, item *indexed_item) {
|
||||
// doubly-linked-list in index hashmap. this handles case of
|
||||
// overwriting "unique" index entries, and removes from given
|
||||
// outer linked-list in the case that it is no longer indexed.
|
||||
func (i *Index) append(ll *list, key string, item *indexed_item) {
|
||||
// Look for existing.
|
||||
l, _ := i.data.Get(key)
|
||||
l := i.data.Get(key)
|
||||
|
||||
if l == nil {
|
||||
|
||||
|
@ -255,12 +256,21 @@ func (i *Index) append(key string, item *indexed_item) {
|
|||
elem := l.head
|
||||
l.remove(elem)
|
||||
|
||||
// Drop index from inner item.
|
||||
// Drop index from inner item,
|
||||
// catching the evicted item.
|
||||
e := (*index_entry)(elem.data)
|
||||
e.item.drop_index(e)
|
||||
evicted := e.item
|
||||
evicted.drop_index(e)
|
||||
|
||||
// Free unused entry.
|
||||
free_index_entry(e)
|
||||
|
||||
if len(evicted.indexed) == 0 {
|
||||
// Evicted item is not indexed,
|
||||
// remove from outer linked list.
|
||||
ll.remove(&evicted.elem)
|
||||
free_indexed_item(evicted)
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare new index entry.
|
||||
|
@ -283,7 +293,7 @@ func (i *Index) delete(key string, hook func(*indexed_item)) {
|
|||
}
|
||||
|
||||
// Get list at hash.
|
||||
l, _ := i.data.Get(key)
|
||||
l := i.data.Get(key)
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
|
@ -292,10 +302,9 @@ func (i *Index) delete(key string, hook func(*indexed_item)) {
|
|||
i.data.Delete(key)
|
||||
|
||||
// Iterate entries in list.
|
||||
for x := 0; x < l.len; x++ {
|
||||
l.rangefn(func(elem *list_elem) {
|
||||
|
||||
// Pop list head.
|
||||
elem := l.head
|
||||
// Remove elem.
|
||||
l.remove(elem)
|
||||
|
||||
// Extract element entry + item.
|
||||
|
@ -310,7 +319,7 @@ func (i *Index) delete(key string, hook func(*indexed_item)) {
|
|||
|
||||
// Pass to hook.
|
||||
hook(item)
|
||||
}
|
||||
})
|
||||
|
||||
// Release list.
|
||||
free_list(l)
|
||||
|
@ -319,7 +328,7 @@ func (i *Index) delete(key string, hook func(*indexed_item)) {
|
|||
// delete_entry deletes the given index entry.
|
||||
func (i *Index) delete_entry(entry *index_entry) {
|
||||
// Get list at hash sum.
|
||||
l, _ := i.data.Get(entry.key)
|
||||
l := i.data.Get(entry.key)
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
|
|
5
vendor/codeberg.org/gruf/go-structr/item.go
generated
vendored
5
vendor/codeberg.org/gruf/go-structr/item.go
generated
vendored
|
@ -50,12 +50,9 @@ func (i *indexed_item) drop_index(entry *index_entry) {
|
|||
continue
|
||||
}
|
||||
|
||||
// Unset tptr value to
|
||||
// ensure GC can take it.
|
||||
i.indexed[x] = nil
|
||||
|
||||
// Move all index entries down + reslice.
|
||||
_ = copy(i.indexed[x:], i.indexed[x+1:])
|
||||
i.indexed[len(i.indexed)-1] = nil
|
||||
i.indexed = i.indexed[:len(i.indexed)-1]
|
||||
break
|
||||
}
|
||||
|
|
120
vendor/codeberg.org/gruf/go-structr/list.go
generated
vendored
120
vendor/codeberg.org/gruf/go-structr/list.go
generated
vendored
|
@ -48,27 +48,17 @@ func free_list(list *list) {
|
|||
|
||||
// push_front will push the given elem to front (head) of list.
|
||||
func (l *list) push_front(elem *list_elem) {
|
||||
if l.len == 0 {
|
||||
// Set new tail + head
|
||||
l.head = elem
|
||||
l.tail = elem
|
||||
|
||||
// Link elem to itself
|
||||
elem.next = elem
|
||||
elem.prev = elem
|
||||
} else {
|
||||
oldHead := l.head
|
||||
// Set new head.
|
||||
oldHead := l.head
|
||||
l.head = elem
|
||||
|
||||
if oldHead != nil {
|
||||
// Link to old head
|
||||
elem.next = oldHead
|
||||
oldHead.prev = elem
|
||||
|
||||
// Link up to tail
|
||||
elem.prev = l.tail
|
||||
l.tail.next = elem
|
||||
|
||||
// Set new head
|
||||
l.head = elem
|
||||
} else {
|
||||
// First in list.
|
||||
l.tail = elem
|
||||
}
|
||||
|
||||
// Incr count
|
||||
|
@ -77,27 +67,17 @@ func (l *list) push_front(elem *list_elem) {
|
|||
|
||||
// push_back will push the given elem to back (tail) of list.
|
||||
func (l *list) push_back(elem *list_elem) {
|
||||
if l.len == 0 {
|
||||
// Set new tail + head
|
||||
l.head = elem
|
||||
l.tail = elem
|
||||
|
||||
// Link elem to itself
|
||||
elem.next = elem
|
||||
elem.prev = elem
|
||||
} else {
|
||||
oldTail := l.tail
|
||||
// Set new tail.
|
||||
oldTail := l.tail
|
||||
l.tail = elem
|
||||
|
||||
if oldTail != nil {
|
||||
// Link to old tail
|
||||
elem.prev = oldTail
|
||||
oldTail.next = elem
|
||||
|
||||
// Link up to head
|
||||
elem.next = l.head
|
||||
l.head.prev = elem
|
||||
|
||||
// Set new tail
|
||||
l.tail = elem
|
||||
} else {
|
||||
// First in list.
|
||||
l.head = elem
|
||||
}
|
||||
|
||||
// Incr count
|
||||
|
@ -105,53 +85,57 @@ func (l *list) push_back(elem *list_elem) {
|
|||
}
|
||||
|
||||
// move_front will move given elem to front (head) of list.
|
||||
// if it is already at front this call is a no-op.
|
||||
func (l *list) move_front(elem *list_elem) {
|
||||
if elem == l.head {
|
||||
return
|
||||
}
|
||||
l.remove(elem)
|
||||
l.push_front(elem)
|
||||
}
|
||||
|
||||
// move_back will move given elem to back (tail) of list.
|
||||
// move_back will move given elem to back (tail) of list,
|
||||
// if it is already at back this call is a no-op.
|
||||
func (l *list) move_back(elem *list_elem) {
|
||||
if elem == l.tail {
|
||||
return
|
||||
}
|
||||
l.remove(elem)
|
||||
l.push_back(elem)
|
||||
}
|
||||
|
||||
// remove will remove given elem from list.
|
||||
func (l *list) remove(elem *list_elem) {
|
||||
if l.len <= 1 {
|
||||
// Drop elem's links
|
||||
elem.next = nil
|
||||
elem.prev = nil
|
||||
|
||||
// Only elem in list
|
||||
l.head = nil
|
||||
l.tail = nil
|
||||
l.len = 0
|
||||
return
|
||||
}
|
||||
|
||||
// Get surrounding elems
|
||||
// Get linked elems.
|
||||
next := elem.next
|
||||
prev := elem.prev
|
||||
|
||||
// Relink chain
|
||||
next.prev = prev
|
||||
prev.next = next
|
||||
|
||||
switch elem {
|
||||
// Set new head
|
||||
case l.head:
|
||||
l.head = next
|
||||
|
||||
// Set new tail
|
||||
case l.tail:
|
||||
l.tail = prev
|
||||
}
|
||||
|
||||
// Drop elem's links
|
||||
// Unset elem.
|
||||
elem.next = nil
|
||||
elem.prev = nil
|
||||
|
||||
switch {
|
||||
// elem is ONLY one in list.
|
||||
case next == nil && prev == nil:
|
||||
l.head = nil
|
||||
l.tail = nil
|
||||
|
||||
// elem is front in list.
|
||||
case next != nil && prev == nil:
|
||||
l.head = next
|
||||
next.prev = nil
|
||||
|
||||
// elem is last in list.
|
||||
case prev != nil && next == nil:
|
||||
l.tail = prev
|
||||
prev.next = nil
|
||||
|
||||
// elem in middle of list.
|
||||
default:
|
||||
next.prev = prev
|
||||
prev.next = next
|
||||
}
|
||||
|
||||
// Decr count
|
||||
l.len--
|
||||
}
|
||||
|
@ -161,9 +145,11 @@ func (l *list) rangefn(fn func(*list_elem)) {
|
|||
if fn == nil {
|
||||
panic("nil fn")
|
||||
}
|
||||
elem := l.head
|
||||
for i := 0; i < l.len; i++ {
|
||||
fn(elem)
|
||||
elem = elem.next
|
||||
for e := l.head; //
|
||||
e != nil; //
|
||||
{
|
||||
n := e.next
|
||||
fn(e)
|
||||
e = n
|
||||
}
|
||||
}
|
||||
|
|
5
vendor/codeberg.org/gruf/go-structr/map.go
generated
vendored
5
vendor/codeberg.org/gruf/go-structr/map.go
generated
vendored
|
@ -10,9 +10,8 @@ func (m *hashmap) init(cap int) {
|
|||
m.n = cap
|
||||
}
|
||||
|
||||
func (m *hashmap) Get(key string) (*list, bool) {
|
||||
list, ok := m.m[key]
|
||||
return list, ok
|
||||
func (m *hashmap) Get(key string) *list {
|
||||
return m.m[key]
|
||||
}
|
||||
|
||||
func (m *hashmap) Put(key string, list *list) {
|
||||
|
|
4
vendor/codeberg.org/gruf/go-structr/queue.go
generated
vendored
4
vendor/codeberg.org/gruf/go-structr/queue.go
generated
vendored
|
@ -308,8 +308,8 @@ func (q *Queue[T]) index(value T) *indexed_item {
|
|||
continue
|
||||
}
|
||||
|
||||
// Append item to index.
|
||||
idx.append(key, item)
|
||||
// Append item to this index.
|
||||
idx.append(&q.queue, key, item)
|
||||
}
|
||||
|
||||
// Done with buf.
|
||||
|
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
|
@ -66,7 +66,7 @@ codeberg.org/gruf/go-storage/disk
|
|||
codeberg.org/gruf/go-storage/internal
|
||||
codeberg.org/gruf/go-storage/memory
|
||||
codeberg.org/gruf/go-storage/s3
|
||||
# codeberg.org/gruf/go-structr v0.8.9
|
||||
# codeberg.org/gruf/go-structr v0.8.10
|
||||
## explicit; go 1.21
|
||||
codeberg.org/gruf/go-structr
|
||||
# codeberg.org/superseriousbusiness/exif-terminator v0.9.0
|
||||
|
|
|
@ -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