Enable stricter linting with golangci-lint (#316)
* update golangci-lint * add golangci config file w/ more linters * correct issues flagged by stricter linters * add more generous timeout for golangci-lint * add some style + formatting guidelines * move timeout to config file * go fmt
This commit is contained in:
parent
38d73f0316
commit
f8630348b4
|
@ -13,7 +13,7 @@ steps:
|
||||||
# We use golangci-lint for linting.
|
# We use golangci-lint for linting.
|
||||||
# See: https://golangci-lint.run/
|
# See: https://golangci-lint.run/
|
||||||
- name: lint
|
- name: lint
|
||||||
image: golangci/golangci-lint:v1.42.1
|
image: golangci/golangci-lint:v1.43.0
|
||||||
volumes:
|
volumes:
|
||||||
- name: go-build-cache
|
- name: go-build-cache
|
||||||
path: /root/.cache/go-build
|
path: /root/.cache/go-build
|
||||||
|
@ -22,7 +22,7 @@ steps:
|
||||||
- name: go-src
|
- name: go-src
|
||||||
path: /go
|
path: /go
|
||||||
commands:
|
commands:
|
||||||
- golangci-lint run --timeout 5m0s --tests=false --verbose
|
- golangci-lint run
|
||||||
when:
|
when:
|
||||||
event:
|
event:
|
||||||
include:
|
include:
|
||||||
|
@ -115,6 +115,6 @@ trigger:
|
||||||
|
|
||||||
---
|
---
|
||||||
kind: signature
|
kind: signature
|
||||||
hmac: aa44c4655421fb0ed3141b8d7255a9a240dd4312244081d9e95929c4a196fd92
|
hmac: 07d6ed18510f9591c6b347d6768cbe8e613561b3759f1a8dda8721d1d231a522
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Configuration file for golangci-lint linter.
|
||||||
|
# This will be automatically picked up when golangci-lint is invoked.
|
||||||
|
# For all config options, see https://golangci-lint.run/usage/configuration/#config-file
|
||||||
|
#
|
||||||
|
# For GoToSocial we mostly take the default linters, but we add a few to catch style issues as well.
|
||||||
|
|
||||||
|
# options for analysis running
|
||||||
|
run:
|
||||||
|
# include test files or not, default is true
|
||||||
|
tests: false
|
||||||
|
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||||
|
timeout: 5m
|
||||||
|
|
||||||
|
linters:
|
||||||
|
# enable some extra linters, see here for the list: https://golangci-lint.run/usage/linters/
|
||||||
|
enable:
|
||||||
|
- contextcheck
|
||||||
|
- forcetypeassert
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- gofmt
|
||||||
|
- gosec
|
||||||
|
- ifshort
|
||||||
|
- nilerr
|
||||||
|
- revive
|
||||||
|
- wastedassign
|
|
@ -21,7 +21,9 @@ Check the [issues](https://github.com/superseriousbusiness/gotosocial/issues) to
|
||||||
- [SQLite](#sqlite)
|
- [SQLite](#sqlite)
|
||||||
- [Postgres](#postgres)
|
- [Postgres](#postgres)
|
||||||
- [Both](#both)
|
- [Both](#both)
|
||||||
- [Linting](#linting)
|
- [Project Structure](#project-structure)
|
||||||
|
- [Style](#style)
|
||||||
|
- [Linting and Formatting](#linting-and-formatting)
|
||||||
- [Updating Swagger docs](#updating-swagger-docs)
|
- [Updating Swagger docs](#updating-swagger-docs)
|
||||||
- [CI/CD configuration](#cicd-configuration)
|
- [CI/CD configuration](#cicd-configuration)
|
||||||
- [Building releases and Docker containers](#building-releases-and-docker-containers)
|
- [Building releases and Docker containers](#building-releases-and-docker-containers)
|
||||||
|
@ -179,33 +181,48 @@ Finally, to run tests against both database types one after the other, use:
|
||||||
./scripts/test.sh
|
./scripts/test.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Linting
|
## Project Structure
|
||||||
|
|
||||||
We use [golangci-lint](https://golangci-lint.run/) for linting. To run this locally, first install the linter following the instructions [here](https://golangci-lint.run/usage/install/#local-installation).
|
For project structure, GoToSocial follows a standard and widely-accepted project layout [defined here](https://github.com/golang-standards/project-layout). As the author writes:
|
||||||
|
|
||||||
|
> This is a basic layout for Go application projects. It's not an official standard defined by the core Go dev team; however, it is a set of common historical and emerging project layout patterns in the Go ecosystem.
|
||||||
|
|
||||||
|
Most of the crucial business logic of the application is found inside the various packages and subpackages of the `internal` directory.
|
||||||
|
|
||||||
|
Where possible, we prefer more files and packages of shorter length that very clearly pertain to definable chunks of application logic, rather than fewer but longer files: if one `.go` file is pushing 1,000 lines of code, it's probably too long.
|
||||||
|
|
||||||
|
## Style
|
||||||
|
|
||||||
|
It is a good idea to read the short official [Effective Go](https://golang.org/doc/effective_go) page before submitting code: this document is the foundation of many a style guide, for good reason, and GoToSocial more or less follows its advice.
|
||||||
|
|
||||||
|
Another useful style guide that we try to follow: [this one](https://github.com/bahlo/go-styleguide).
|
||||||
|
|
||||||
|
In addition, here are some specific highlights from Uber's Go style guide which agree with what we try to do in GtS:
|
||||||
|
|
||||||
|
- [Group Similar Declarations](https://github.com/uber-go/guide/blob/master/style.md#group-similar-declarations).
|
||||||
|
- [Reduce Nesting](https://github.com/uber-go/guide/blob/master/style.md#reduce-nesting).
|
||||||
|
- [Unnecessary Else](https://github.com/uber-go/guide/blob/master/style.md#unnecessary-else).
|
||||||
|
- [Local Variable Declarations](https://github.com/uber-go/guide/blob/master/style.md#local-variable-declarations).
|
||||||
|
- [Reduce Scope of Variables](https://github.com/uber-go/guide/blob/master/style.md#reduce-scope-of-variables).
|
||||||
|
- [Initializing Structs](https://github.com/uber-go/guide/blob/master/style.md#initializing-structs).
|
||||||
|
|
||||||
|
## Linting and Formatting
|
||||||
|
|
||||||
|
Before you submit any code, make sure to run `go fmt ./...` to update whitespace and other opinionated formatting.
|
||||||
|
|
||||||
|
We use [golangci-lint](https://golangci-lint.run/) for linting, which allows us to catch style inconsistencies and potential bugs or security issues, using static code analysis.
|
||||||
|
|
||||||
|
If you make a PR that doesn't pass the linter, it will be rejected. As such, it's good practice to run the linter locally before pushing or opening a PR.
|
||||||
|
|
||||||
|
To do this, first install the linter following the instructions [here](https://golangci-lint.run/usage/install/#local-installation).
|
||||||
|
|
||||||
Then, you can run the linter with:
|
Then, you can run the linter with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
golangci-lint run --tests=false
|
golangci-lint run
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that this linter also runs as a job on the Github repo, so if you make a PR that doesn't pass the linter, it will be rejected. As such, it's good practice to run the linter locally before pushing or opening a PR.
|
If there's no output, great! It passed :)
|
||||||
|
|
||||||
Another useful linter is [golint](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/goreporter/linters/golint), which catches some style issues that golangci-lint does not.
|
|
||||||
|
|
||||||
To install golint, run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go get -u github.com/golang/lint/golint
|
|
||||||
```
|
|
||||||
|
|
||||||
To run the linter, use:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
golint ./internal/...
|
|
||||||
```
|
|
||||||
|
|
||||||
Then make sure to run `go fmt ./...` to update whitespace and other opinionated formatting.
|
|
||||||
|
|
||||||
## Updating Swagger docs
|
## Updating Swagger docs
|
||||||
|
|
||||||
|
|
|
@ -49,8 +49,7 @@ func main() {
|
||||||
Commands: getCommands(),
|
Commands: getCommands(),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := app.Run(os.Args)
|
if err := app.Run(os.Args); err != nil {
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,6 @@ const (
|
||||||
ObjectRelationship = "Relationship" // ActivityStreamsRelationship https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship
|
ObjectRelationship = "Relationship" // ActivityStreamsRelationship https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship
|
||||||
ObjectTombstone = "Tombstone" // ActivityStreamsTombstone https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone
|
ObjectTombstone = "Tombstone" // ActivityStreamsTombstone https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone
|
||||||
ObjectVideo = "Video" // ActivityStreamsVideo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video
|
ObjectVideo = "Video" // ActivityStreamsVideo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video
|
||||||
ObjectCollection = "Collection" //ActivityStreamsCollection https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collection
|
ObjectCollection = "Collection" // ActivityStreamsCollection https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collection
|
||||||
ObjectCollectionPage = "CollectionPage" // ActivityStreamsCollectionPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage
|
ObjectCollectionPage = "CollectionPage" // ActivityStreamsCollectionPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,14 +20,22 @@ package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// permitted length for most fields
|
||||||
|
formFieldLen = 64
|
||||||
|
// redirect can be a bit bigger because we probably need to encode data in the redirect uri
|
||||||
|
formRedirectLen = 512
|
||||||
|
)
|
||||||
|
|
||||||
// AppsPOSTHandler swagger:operation POST /api/v1/apps appCreate
|
// AppsPOSTHandler swagger:operation POST /api/v1/apps appCreate
|
||||||
//
|
//
|
||||||
// Register a new application on this instance.
|
// Register a new application on this instance.
|
||||||
|
@ -79,11 +87,6 @@ func (m *Module) AppsPOSTHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// permitted length for most fields
|
|
||||||
formFieldLen := 64
|
|
||||||
// redirect can be a bit bigger because we probably need to encode data in the redirect uri
|
|
||||||
formRedirectLen := 512
|
|
||||||
|
|
||||||
// check lengths of fields before proceeding so the user can't spam huge entries into the database
|
// check lengths of fields before proceeding so the user can't spam huge entries into the database
|
||||||
if len(form.ClientName) > formFieldLen {
|
if len(form.ClientName) > formFieldLen {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("client_name must be less than %d bytes", formFieldLen)})
|
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("client_name must be less than %d bytes", formFieldLen)})
|
||||||
|
|
|
@ -29,6 +29,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/router"
|
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/* #nosec G101 */
|
||||||
const (
|
const (
|
||||||
// AuthSignInPath is the API path for users to sign in through
|
// AuthSignInPath is the API path for users to sign in through
|
||||||
AuthSignInPath = "/auth/sign_in"
|
AuthSignInPath = "/auth/sign_in"
|
||||||
|
|
|
@ -182,7 +182,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
|
||||||
//
|
//
|
||||||
// note that for the first iteration, iString is still "" when the check is made, so our first choice
|
// note that for the first iteration, iString is still "" when the check is made, so our first choice
|
||||||
// is still the raw username with no integer stuck on the end
|
// is still the raw username with no integer stuck on the end
|
||||||
for i := 1; !found; i = i + 1 {
|
for i := 1; !found; i++ {
|
||||||
usernameAvailable, err := m.db.IsUsernameAvailable(ctx, username+iString)
|
usernameAvailable, err := m.db.IsUsernameAvailable(ctx, username+iString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -190,7 +190,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
|
||||||
if usernameAvailable {
|
if usernameAvailable {
|
||||||
// no error so we've found a username that works
|
// no error so we've found a username that works
|
||||||
found = true
|
found = true
|
||||||
username = username + iString
|
username += iString
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
iString = strconv.Itoa(i)
|
iString = strconv.Itoa(i)
|
||||||
|
|
|
@ -19,10 +19,11 @@
|
||||||
package status
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
@ -110,13 +111,13 @@ func (m *Module) muxHandler(c *gin.Context) {
|
||||||
logrus.Debug("entering mux handler")
|
logrus.Debug("entering mux handler")
|
||||||
ru := c.Request.RequestURI
|
ru := c.Request.RequestURI
|
||||||
|
|
||||||
switch c.Request.Method {
|
if c.Request.Method == http.MethodGet {
|
||||||
case http.MethodGet:
|
switch {
|
||||||
if strings.HasPrefix(ru, ContextPath) {
|
case strings.HasPrefix(ru, ContextPath):
|
||||||
// TODO
|
// TODO
|
||||||
} else if strings.HasPrefix(ru, FavouritedPath) {
|
case strings.HasPrefix(ru, FavouritedPath):
|
||||||
m.StatusFavedByGETHandler(c)
|
m.StatusFavedByGETHandler(c)
|
||||||
} else {
|
default:
|
||||||
m.StatusGETHandler(c)
|
m.StatusGETHandler(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,9 +90,8 @@ func (m *Module) OutboxGETHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
page := false
|
var page bool
|
||||||
pageString := c.Query(PageKey)
|
if pageString := c.Query(PageKey); pageString != "" {
|
||||||
if pageString != "" {
|
|
||||||
i, err := strconv.ParseBool(pageString)
|
i, err := strconv.ParseBool(pageString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing page string: %s", err)
|
l.Debugf("error parsing page string: %s", err)
|
||||||
|
|
|
@ -102,9 +102,8 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
page := false
|
var page bool
|
||||||
pageString := c.Query(PageKey)
|
if pageString := c.Query(PageKey); pageString != "" {
|
||||||
if pageString != "" {
|
|
||||||
i, err := strconv.ParseBool(pageString)
|
i, err := strconv.ParseBool(pageString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing page string: %s", err)
|
l.Debugf("error parsing page string: %s", err)
|
||||||
|
|
|
@ -31,8 +31,7 @@ func (m *Module) UserAgentBlock(c *gin.Context) {
|
||||||
"func": "UserAgentBlock",
|
"func": "UserAgentBlock",
|
||||||
})
|
})
|
||||||
|
|
||||||
ua := c.Request.UserAgent()
|
if ua := c.Request.UserAgent(); ua == "" {
|
||||||
if ua == "" {
|
|
||||||
l.Debug("aborting request because there's no user-agent set")
|
l.Debug("aborting request because there's no user-agent set")
|
||||||
c.AbortWithStatus(http.StatusTeapot)
|
c.AbortWithStatus(http.StatusTeapot)
|
||||||
return
|
return
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/ReneKroon/ttlcache"
|
"github.com/ReneKroon/ttlcache"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,7 +27,10 @@ func NewAccountCache() *AccountCache {
|
||||||
|
|
||||||
// Set callback to purge lookup maps on expiration
|
// Set callback to purge lookup maps on expiration
|
||||||
c.cache.SetExpirationCallback(func(key string, value interface{}) {
|
c.cache.SetExpirationCallback(func(key string, value interface{}) {
|
||||||
account := value.(*gtsmodel.Account)
|
account, ok := value.(*gtsmodel.Account)
|
||||||
|
if !ok {
|
||||||
|
logrus.Panicf("AccountCache could not assert entry with key %s to *gtsmodel.Account", key)
|
||||||
|
}
|
||||||
|
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
delete(c.urls, account.URL)
|
delete(c.urls, account.URL)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/ReneKroon/ttlcache"
|
"github.com/ReneKroon/ttlcache"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,7 +27,10 @@ func NewStatusCache() *StatusCache {
|
||||||
|
|
||||||
// Set callback to purge lookup maps on expiration
|
// Set callback to purge lookup maps on expiration
|
||||||
c.cache.SetExpirationCallback(func(key string, value interface{}) {
|
c.cache.SetExpirationCallback(func(key string, value interface{}) {
|
||||||
status := value.(*gtsmodel.Status)
|
status, ok := value.(*gtsmodel.Status)
|
||||||
|
if !ok {
|
||||||
|
logrus.Panicf("StatusCache could not assert entry with key %s to *gtsmodel.Status", key)
|
||||||
|
}
|
||||||
|
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
delete(c.urls, status.URL)
|
delete(c.urls, status.URL)
|
||||||
|
|
|
@ -46,4 +46,4 @@ var DBTLSModeEnable DBTLSMode = "enable"
|
||||||
var DBTLSModeRequire DBTLSMode = "require"
|
var DBTLSModeRequire DBTLSMode = "require"
|
||||||
|
|
||||||
// DBTLSModeUnset means that the TLS mode has not been set.
|
// DBTLSModeUnset means that the TLS mode has not been set.
|
||||||
var DBTLSModeUnset DBTLSMode = ""
|
var DBTLSModeUnset DBTLSMode
|
||||||
|
|
|
@ -180,8 +180,8 @@ func GetDefaults() Defaults {
|
||||||
AccountsRequireApproval: true,
|
AccountsRequireApproval: true,
|
||||||
AccountsReasonRequired: true,
|
AccountsReasonRequired: true,
|
||||||
|
|
||||||
MediaMaxImageSize: 2097152, //2mb
|
MediaMaxImageSize: 2097152, // 2mb
|
||||||
MediaMaxVideoSize: 10485760, //10mb
|
MediaMaxVideoSize: 10485760, // 10mb
|
||||||
MediaMinDescriptionChars: 0,
|
MediaMinDescriptionChars: 0,
|
||||||
MediaMaxDescriptionChars: 500,
|
MediaMaxDescriptionChars: 500,
|
||||||
|
|
||||||
|
@ -244,8 +244,8 @@ func GetTestDefaults() Defaults {
|
||||||
AccountsRequireApproval: true,
|
AccountsRequireApproval: true,
|
||||||
AccountsReasonRequired: true,
|
AccountsReasonRequired: true,
|
||||||
|
|
||||||
MediaMaxImageSize: 1048576, //1mb
|
MediaMaxImageSize: 1048576, // 1mb
|
||||||
MediaMaxVideoSize: 5242880, //5mb
|
MediaMaxVideoSize: 5242880, // 5mb
|
||||||
MediaMinDescriptionChars: 0,
|
MediaMinDescriptionChars: 0,
|
||||||
MediaMaxDescriptionChars: 500,
|
MediaMaxDescriptionChars: 500,
|
||||||
|
|
||||||
|
|
|
@ -137,8 +137,7 @@ func (a *accountDB) GetInstanceAccount(ctx context.Context, domain string) (*gts
|
||||||
WhereGroup(" AND ", whereEmptyOrNull("domain"))
|
WhereGroup(" AND ", whereEmptyOrNull("domain"))
|
||||||
}
|
}
|
||||||
|
|
||||||
err := q.Scan(ctx)
|
if err := q.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, a.conn.ProcessError(err)
|
return nil, a.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
return account, nil
|
return account, nil
|
||||||
|
@ -155,8 +154,7 @@ func (a *accountDB) GetAccountLastPosted(ctx context.Context, accountID string)
|
||||||
Where("account_id = ?", accountID).
|
Where("account_id = ?", accountID).
|
||||||
Column("created_at")
|
Column("created_at")
|
||||||
|
|
||||||
err := q.Scan(ctx)
|
if err := q.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, a.conn.ProcessError(err)
|
return time.Time{}, a.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
return status.CreatedAt, nil
|
return status.CreatedAt, nil
|
||||||
|
@ -168,11 +166,12 @@ func (a *accountDB) SetAccountHeaderOrAvatar(ctx context.Context, mediaAttachmen
|
||||||
}
|
}
|
||||||
|
|
||||||
var headerOrAVI string
|
var headerOrAVI string
|
||||||
if mediaAttachment.Avatar {
|
switch {
|
||||||
|
case mediaAttachment.Avatar:
|
||||||
headerOrAVI = "avatar"
|
headerOrAVI = "avatar"
|
||||||
} else if mediaAttachment.Header {
|
case mediaAttachment.Header:
|
||||||
headerOrAVI = "header"
|
headerOrAVI = "header"
|
||||||
} else {
|
default:
|
||||||
return errors.New("given media attachment was neither a header nor an avatar")
|
return errors.New("given media attachment was neither a header nor an avatar")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,8 +201,7 @@ func (a *accountDB) GetLocalAccountByUsername(ctx context.Context, username stri
|
||||||
Where("username = ?", username).
|
Where("username = ?", username).
|
||||||
WhereGroup(" AND ", whereEmptyOrNull("domain"))
|
WhereGroup(" AND ", whereEmptyOrNull("domain"))
|
||||||
|
|
||||||
err := q.Scan(ctx)
|
if err := q.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, a.conn.ProcessError(err)
|
return nil, a.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
return account, nil
|
return account, nil
|
||||||
|
@ -308,8 +306,7 @@ func (a *accountDB) GetAccountBlocks(ctx context.Context, accountID string, maxI
|
||||||
fq = fq.Limit(limit)
|
fq = fq.Limit(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := fq.Scan(ctx)
|
if err := fq.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, "", "", a.conn.ProcessError(err)
|
return nil, "", "", a.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ const (
|
||||||
dbTypeSqlite = "sqlite"
|
dbTypeSqlite = "sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
var registerTables []interface{} = []interface{}{
|
var registerTables = []interface{}{
|
||||||
>smodel.StatusToEmoji{},
|
>smodel.StatusToEmoji{},
|
||||||
>smodel.StatusToTag{},
|
>smodel.StatusToTag{},
|
||||||
}
|
}
|
||||||
|
@ -305,6 +305,7 @@ func deriveBunDBPGOptions(c *config.Config) (*pgx.ConnConfig, error) {
|
||||||
case config.DBTLSModeDisable, config.DBTLSModeUnset:
|
case config.DBTLSModeDisable, config.DBTLSModeUnset:
|
||||||
break // nothing to do
|
break // nothing to do
|
||||||
case config.DBTLSModeEnable:
|
case config.DBTLSModeEnable:
|
||||||
|
/* #nosec G402 */
|
||||||
tlsConfig = &tls.Config{
|
tlsConfig = &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
}
|
}
|
||||||
|
@ -312,6 +313,7 @@ func deriveBunDBPGOptions(c *config.Config) (*pgx.ConnConfig, error) {
|
||||||
tlsConfig = &tls.Config{
|
tlsConfig = &tls.Config{
|
||||||
InsecureSkipVerify: false,
|
InsecureSkipVerify: false,
|
||||||
ServerName: c.DBConfig.Address,
|
ServerName: c.DBConfig.Address,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -116,8 +116,7 @@ func (i *instanceDB) GetInstanceAccounts(ctx context.Context, domain string, max
|
||||||
q = q.Limit(limit)
|
q = q.Limit(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := q.Scan(ctx)
|
if err := q.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, i.conn.ProcessError(err)
|
return nil, i.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
return accounts, nil
|
return accounts, nil
|
||||||
|
|
|
@ -45,8 +45,7 @@ func (m *mediaDB) GetAttachmentByID(ctx context.Context, id string) (*gtsmodel.M
|
||||||
q := m.newMediaQ(attachment).
|
q := m.newMediaQ(attachment).
|
||||||
Where("media_attachment.id = ?", id)
|
Where("media_attachment.id = ?", id)
|
||||||
|
|
||||||
err := q.Scan(ctx)
|
if err := q.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, m.conn.ProcessError(err)
|
return nil, m.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
return attachment, nil
|
return attachment, nil
|
||||||
|
|
|
@ -61,8 +61,7 @@ func (m *mentionDB) getMentionDB(ctx context.Context, id string) (*gtsmodel.Ment
|
||||||
q := m.newMentionQ(mention).
|
q := m.newMentionQ(mention).
|
||||||
Where("mention.id = ?", id)
|
Where("mention.id = ?", id)
|
||||||
|
|
||||||
err := q.Scan(ctx)
|
if err := q.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, m.conn.ProcessError(err)
|
return nil, m.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -125,8 +125,7 @@ func (n *notificationDB) getNotificationDB(ctx context.Context, id string, dst *
|
||||||
q := n.newNotificationQ(dst).
|
q := n.newNotificationQ(dst).
|
||||||
Where("notification.id = ?", id)
|
Where("notification.id = ?", id)
|
||||||
|
|
||||||
err := q.Scan(ctx)
|
if err := q.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return n.conn.ProcessError(err)
|
return n.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,8 +74,7 @@ func (r *relationshipDB) GetBlock(ctx context.Context, account1 string, account2
|
||||||
Where("block.account_id = ?", account1).
|
Where("block.account_id = ?", account1).
|
||||||
Where("block.target_account_id = ?", account2)
|
Where("block.target_account_id = ?", account2)
|
||||||
|
|
||||||
err := q.Scan(ctx)
|
if err := q.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, r.conn.ProcessError(err)
|
return nil, r.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
return block, nil
|
return block, nil
|
||||||
|
@ -286,8 +285,7 @@ func (r *relationshipDB) GetAccountFollowRequests(ctx context.Context, accountID
|
||||||
q := r.newFollowQ(&followRequests).
|
q := r.newFollowQ(&followRequests).
|
||||||
Where("target_account_id = ?", accountID)
|
Where("target_account_id = ?", accountID)
|
||||||
|
|
||||||
err := q.Scan(ctx)
|
if err := q.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, r.conn.ProcessError(err)
|
return nil, r.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
return followRequests, nil
|
return followRequests, nil
|
||||||
|
@ -299,8 +297,7 @@ func (r *relationshipDB) GetAccountFollows(ctx context.Context, accountID string
|
||||||
q := r.newFollowQ(&follows).
|
q := r.newFollowQ(&follows).
|
||||||
Where("account_id = ?", accountID)
|
Where("account_id = ?", accountID)
|
||||||
|
|
||||||
err := q.Scan(ctx)
|
if err := q.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, r.conn.ProcessError(err)
|
return nil, r.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
return follows, nil
|
return follows, nil
|
||||||
|
|
|
@ -47,7 +47,7 @@ func (s *sessionDB) GetSession(ctx context.Context) (*gtsmodel.RouterSession, db
|
||||||
return nil, s.conn.ProcessError(err)
|
return nil, s.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rss) <= 0 {
|
if len(rss) == 0 {
|
||||||
// no session created yet, so make one
|
// no session created yet, so make one
|
||||||
return s.createSession(ctx)
|
return s.createSession(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/cache"
|
"github.com/superseriousbusiness/gotosocial/internal/cache"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
@ -206,7 +207,11 @@ func (s *statusDB) GetStatusChildren(ctx context.Context, status *gtsmodel.Statu
|
||||||
children := []*gtsmodel.Status{}
|
children := []*gtsmodel.Status{}
|
||||||
for e := foundStatuses.Front(); e != nil; e = e.Next() {
|
for e := foundStatuses.Front(); e != nil; e = e.Next() {
|
||||||
// only append children, not the overall parent status
|
// only append children, not the overall parent status
|
||||||
entry := e.Value.(*gtsmodel.Status)
|
entry, ok := e.Value.(*gtsmodel.Status)
|
||||||
|
if !ok {
|
||||||
|
logrus.Panic("GetStatusChildren: found status could not be asserted to *gtsmodel.Status")
|
||||||
|
}
|
||||||
|
|
||||||
if entry.ID != status.ID {
|
if entry.ID != status.ID {
|
||||||
children = append(children, entry)
|
children = append(children, entry)
|
||||||
}
|
}
|
||||||
|
@ -233,7 +238,11 @@ func (s *statusDB) statusChildren(ctx context.Context, status *gtsmodel.Status,
|
||||||
for _, child := range immediateChildren {
|
for _, child := range immediateChildren {
|
||||||
insertLoop:
|
insertLoop:
|
||||||
for e := foundStatuses.Front(); e != nil; e = e.Next() {
|
for e := foundStatuses.Front(); e != nil; e = e.Next() {
|
||||||
entry := e.Value.(*gtsmodel.Status)
|
entry, ok := e.Value.(*gtsmodel.Status)
|
||||||
|
if !ok {
|
||||||
|
logrus.Panic("statusChildren: found status could not be asserted to *gtsmodel.Status")
|
||||||
|
}
|
||||||
|
|
||||||
if child.InReplyToAccountID != "" && entry.ID == child.InReplyToID {
|
if child.InReplyToAccountID != "" && entry.ID == child.InReplyToID {
|
||||||
foundStatuses.InsertAfter(child, e)
|
foundStatuses.InsertAfter(child, e)
|
||||||
break insertLoop
|
break insertLoop
|
||||||
|
@ -306,8 +315,7 @@ func (s *statusDB) GetStatusFaves(ctx context.Context, status *gtsmodel.Status)
|
||||||
q := s.newFaveQ(&faves).
|
q := s.newFaveQ(&faves).
|
||||||
Where("status_id = ?", status.ID)
|
Where("status_id = ?", status.ID)
|
||||||
|
|
||||||
err := q.Scan(ctx)
|
if err := q.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, s.conn.ProcessError(err)
|
return nil, s.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
return faves, nil
|
return faves, nil
|
||||||
|
@ -319,8 +327,7 @@ func (s *statusDB) GetStatusReblogs(ctx context.Context, status *gtsmodel.Status
|
||||||
q := s.newStatusQ(&reblogs).
|
q := s.newStatusQ(&reblogs).
|
||||||
Where("boost_of_id = ?", status.ID)
|
Where("boost_of_id = ?", status.ID)
|
||||||
|
|
||||||
err := q.Scan(ctx)
|
if err := q.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, s.conn.ProcessError(err)
|
return nil, s.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
return reblogs, nil
|
return reblogs, nil
|
||||||
|
|
|
@ -91,8 +91,7 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI
|
||||||
|
|
||||||
q = q.WhereGroup(" AND ", whereGroup)
|
q = q.WhereGroup(" AND ", whereGroup)
|
||||||
|
|
||||||
err := q.Scan(ctx)
|
if err := q.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, t.conn.ProcessError(err)
|
return nil, t.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
return statuses, nil
|
return statuses, nil
|
||||||
|
@ -136,8 +135,7 @@ func (t *timelineDB) GetPublicTimeline(ctx context.Context, accountID string, ma
|
||||||
q = q.Limit(limit)
|
q = q.Limit(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := q.Scan(ctx)
|
if err := q.Scan(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, t.conn.ProcessError(err)
|
return nil, t.conn.ProcessError(err)
|
||||||
}
|
}
|
||||||
return statuses, nil
|
return statuses, nil
|
||||||
|
|
|
@ -149,7 +149,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
|
||||||
requestingPublicKeyID, err := url.Parse(verifier.KeyId())
|
requestingPublicKeyID, err := url.Parse(verifier.KeyId())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debug("couldn't parse public key URL")
|
l.Debug("couldn't parse public key URL")
|
||||||
return nil, false, nil // couldn't parse the public key ID url
|
return nil, false, err // couldn't parse the public key ID url
|
||||||
}
|
}
|
||||||
|
|
||||||
requestingRemoteAccount := >smodel.Account{}
|
requestingRemoteAccount := >smodel.Account{}
|
||||||
|
|
|
@ -172,7 +172,7 @@ pageLoop:
|
||||||
l.Debugf("dereferencing page %s", currentPageIRI)
|
l.Debugf("dereferencing page %s", currentPageIRI)
|
||||||
nextPage, err := d.DereferenceCollectionPage(ctx, username, currentPageIRI)
|
nextPage, err := d.DereferenceCollectionPage(ctx, username, currentPageIRI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// next items could be either a list of URLs or a list of statuses
|
// next items could be either a list of URLs or a list of statuses
|
||||||
|
@ -188,19 +188,19 @@ pageLoop:
|
||||||
// We're looking for a url to feed to GetRemoteStatus.
|
// We're looking for a url to feed to GetRemoteStatus.
|
||||||
// Items can be either an IRI, or a Note.
|
// Items can be either an IRI, or a Note.
|
||||||
// If a note, we grab the ID from it and call it, rather than parsing the note.
|
// If a note, we grab the ID from it and call it, rather than parsing the note.
|
||||||
|
|
||||||
var itemURI *url.URL
|
var itemURI *url.URL
|
||||||
if iter.IsIRI() {
|
switch {
|
||||||
|
case iter.IsIRI():
|
||||||
// iri, easy
|
// iri, easy
|
||||||
itemURI = iter.GetIRI()
|
itemURI = iter.GetIRI()
|
||||||
} else if iter.IsActivityStreamsNote() {
|
case iter.IsActivityStreamsNote():
|
||||||
// note, get the id from it to use as iri
|
// note, get the id from it to use as iri
|
||||||
n := iter.GetActivityStreamsNote()
|
n := iter.GetActivityStreamsNote()
|
||||||
id := n.GetJSONLDId()
|
id := n.GetJSONLDId()
|
||||||
if id != nil && id.IsIRI() {
|
if id != nil && id.IsIRI() {
|
||||||
itemURI = id.GetIRI()
|
itemURI = id.GetIRI()
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
// if it's not an iri or a note, we don't know how to process it
|
// if it's not an iri or a note, we don't know how to process it
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -211,7 +211,7 @@ pageLoop:
|
||||||
}
|
}
|
||||||
|
|
||||||
// we can confidently say now that we found something
|
// we can confidently say now that we found something
|
||||||
foundReplies = foundReplies + 1
|
foundReplies++
|
||||||
|
|
||||||
// get the remote statusable and put it in the db
|
// get the remote statusable and put it in the db
|
||||||
_, statusable, new, err := d.GetRemoteStatus(ctx, username, itemURI, false, false)
|
_, statusable, new, err := d.GetRemoteStatus(ctx, username, itemURI, false, false)
|
||||||
|
|
|
@ -97,9 +97,7 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
|
||||||
if iter.GetType() == nil {
|
if iter.GetType() == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch iter.GetType().GetTypeName() {
|
if iter.GetType().GetTypeName() == ap.ActivityFollow {
|
||||||
// we have the whole object so we can figure out what we're accepting
|
|
||||||
case ap.ActivityFollow:
|
|
||||||
// ACCEPT FOLLOW
|
// ACCEPT FOLLOW
|
||||||
asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
|
asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
@ -24,6 +24,8 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Lock takes a lock for the object at the specified id. If an error
|
// Lock takes a lock for the object at the specified id. If an error
|
||||||
|
@ -54,7 +56,10 @@ func (f *federatingDB) Lock(c context.Context, id *url.URL) error {
|
||||||
// Get mutex, or create new
|
// Get mutex, or create new
|
||||||
mu, ok := f.locks[idStr]
|
mu, ok := f.locks[idStr]
|
||||||
if !ok {
|
if !ok {
|
||||||
mu = f.pool.Get().(*mutex)
|
mu, ok = f.pool.Get().(*mutex)
|
||||||
|
if !ok {
|
||||||
|
logrus.Panic("Lock: pool entry was not a *mutex")
|
||||||
|
}
|
||||||
f.locks[idStr] = mu
|
f.locks[idStr] = mu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,9 +90,8 @@ func (f *federatingDB) Reject(ctx context.Context, reject vocab.ActivityStreamsR
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch iter.GetType().GetTypeName() {
|
if iter.GetType().GetTypeName() == ap.ActivityFollow {
|
||||||
// we have the whole object so we can figure out what we're rejecting
|
// we have the whole object so we can figure out what we're rejecting
|
||||||
case ap.ActivityFollow:
|
|
||||||
// REJECT FOLLOW
|
// REJECT FOLLOW
|
||||||
asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
|
asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
@ -308,17 +308,29 @@ func (f *federatingDB) collectIRIs(ctx context.Context, iris []*url.URL) (vocab.
|
||||||
func extractFromCtx(ctx context.Context) (receivingAccount, requestingAccount *gtsmodel.Account, fromFederatorChan chan messages.FromFederator) {
|
func extractFromCtx(ctx context.Context) (receivingAccount, requestingAccount *gtsmodel.Account, fromFederatorChan chan messages.FromFederator) {
|
||||||
receivingAccountI := ctx.Value(util.APReceivingAccount)
|
receivingAccountI := ctx.Value(util.APReceivingAccount)
|
||||||
if receivingAccountI != nil {
|
if receivingAccountI != nil {
|
||||||
receivingAccount = receivingAccountI.(*gtsmodel.Account)
|
var ok bool
|
||||||
|
receivingAccount, ok = receivingAccountI.(*gtsmodel.Account)
|
||||||
|
if !ok {
|
||||||
|
logrus.Panicf("extractFromCtx: context entry with key %s could not be asserted to *gtsmodel.Account", util.APReceivingAccount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestingAcctI := ctx.Value(util.APRequestingAccount)
|
requestingAcctI := ctx.Value(util.APRequestingAccount)
|
||||||
if requestingAcctI != nil {
|
if requestingAcctI != nil {
|
||||||
requestingAccount = requestingAcctI.(*gtsmodel.Account)
|
var ok bool
|
||||||
|
requestingAccount, ok = requestingAcctI.(*gtsmodel.Account)
|
||||||
|
if !ok {
|
||||||
|
logrus.Panicf("extractFromCtx: context entry with key %s could not be asserted to *gtsmodel.Account", util.APRequestingAccount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey)
|
fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey)
|
||||||
if fromFederatorChanI != nil {
|
if fromFederatorChanI != nil {
|
||||||
fromFederatorChan = fromFederatorChanI.(chan messages.FromFederator)
|
var ok bool
|
||||||
|
fromFederatorChan, ok = fromFederatorChanI.(chan messages.FromFederator)
|
||||||
|
if !ok {
|
||||||
|
logrus.Panicf("extractFromCtx: context entry with key %s could not be asserted to chan messages.FromFederator", util.APFromFederatorChanKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
|
@ -54,17 +54,18 @@ func (f *federator) NewTransport(ctx context.Context, actorBoxIRI *url.URL, gofe
|
||||||
var username string
|
var username string
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if util.IsInboxPath(actorBoxIRI) {
|
switch {
|
||||||
|
case util.IsInboxPath(actorBoxIRI):
|
||||||
username, err = util.ParseInboxPath(actorBoxIRI)
|
username, err = util.ParseInboxPath(actorBoxIRI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("couldn't parse path %s as an inbox: %s", actorBoxIRI.String(), err)
|
return nil, fmt.Errorf("couldn't parse path %s as an inbox: %s", actorBoxIRI.String(), err)
|
||||||
}
|
}
|
||||||
} else if util.IsOutboxPath(actorBoxIRI) {
|
case util.IsOutboxPath(actorBoxIRI):
|
||||||
username, err = util.ParseOutboxPath(actorBoxIRI)
|
username, err = util.ParseOutboxPath(actorBoxIRI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("couldn't parse path %s as an outbox: %s", actorBoxIRI.String(), err)
|
return nil, fmt.Errorf("couldn't parse path %s as an outbox: %s", actorBoxIRI.String(), err)
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
return nil, fmt.Errorf("id %s was neither an inbox path nor an outbox path", actorBoxIRI.String())
|
return nil, fmt.Errorf("id %s was neither an inbox path nor an outbox path", actorBoxIRI.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,10 +82,9 @@ func (p *processor) GetStatusReplies(ctx context.Context, requestedUsername stri
|
||||||
// 1. we're asked for the whole collection and not a page -- we can just return the collection, with no items, but a link to 'first' page.
|
// 1. we're asked for the whole collection and not a page -- we can just return the collection, with no items, but a link to 'first' page.
|
||||||
// 2. we're asked for a page but only_other_accounts has not been set in the query -- so we should just return the first page of the collection, with no items.
|
// 2. we're asked for a page but only_other_accounts has not been set in the query -- so we should just return the first page of the collection, with no items.
|
||||||
// 3. we're asked for a page, and only_other_accounts has been set, and min_id has optionally been set -- so we need to return some actual items!
|
// 3. we're asked for a page, and only_other_accounts has been set, and min_id has optionally been set -- so we need to return some actual items!
|
||||||
|
switch {
|
||||||
if !page {
|
case !page:
|
||||||
// scenario 1
|
// scenario 1
|
||||||
|
|
||||||
// get the collection
|
// get the collection
|
||||||
collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts)
|
collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -96,9 +95,8 @@ func (p *processor) GetStatusReplies(ctx context.Context, requestedUsername stri
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
} else if page && requestURL.Query().Get("only_other_accounts") == "" {
|
case page && requestURL.Query().Get("only_other_accounts") == "":
|
||||||
// scenario 2
|
// scenario 2
|
||||||
|
|
||||||
// get the collection
|
// get the collection
|
||||||
collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts)
|
collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -109,7 +107,7 @@ func (p *processor) GetStatusReplies(ctx context.Context, requestedUsername stri
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
// scenario 3
|
// scenario 3
|
||||||
// get immediate children
|
// get immediate children
|
||||||
replies, err := p.db.GetStatusChildren(ctx, s, true, minID)
|
replies, err := p.db.GetStatusChildren(ctx, s, true, minID)
|
||||||
|
|
|
@ -38,13 +38,14 @@ func (p *processor) GetUser(ctx context.Context, requestedUsername string, reque
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestedPerson vocab.ActivityStreamsPerson
|
var requestedPerson vocab.ActivityStreamsPerson
|
||||||
if util.IsPublicKeyPath(requestURL) {
|
switch {
|
||||||
|
case util.IsPublicKeyPath(requestURL):
|
||||||
// if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key
|
// if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key
|
||||||
requestedPerson, err = p.tc.AccountToASMinimal(ctx, requestedAccount)
|
requestedPerson, err = p.tc.AccountToASMinimal(ctx, requestedAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
} else if util.IsUserPath(requestURL) {
|
case util.IsUserPath(requestURL):
|
||||||
// if it's a user path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile
|
// if it's a user path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile
|
||||||
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
|
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
|
||||||
if err != nil || !authenticated {
|
if err != nil || !authenticated {
|
||||||
|
@ -72,7 +73,7 @@ func (p *processor) GetUser(ctx context.Context, requestedUsername string, reque
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
return nil, gtserror.NewErrorBadRequest(fmt.Errorf("path was not public key path or user path"))
|
return nil, gtserror.NewErrorBadRequest(fmt.Errorf("path was not public key path or user path"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,15 +64,13 @@ func (p *processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages
|
||||||
}
|
}
|
||||||
case ap.ActivityAccept:
|
case ap.ActivityAccept:
|
||||||
// ACCEPT
|
// ACCEPT
|
||||||
switch clientMsg.APObjectType {
|
if clientMsg.APObjectType == ap.ActivityFollow {
|
||||||
case ap.ActivityFollow:
|
|
||||||
// ACCEPT FOLLOW
|
// ACCEPT FOLLOW
|
||||||
return p.processAcceptFollowFromClientAPI(ctx, clientMsg)
|
return p.processAcceptFollowFromClientAPI(ctx, clientMsg)
|
||||||
}
|
}
|
||||||
case ap.ActivityReject:
|
case ap.ActivityReject:
|
||||||
// REJECT
|
// REJECT
|
||||||
switch clientMsg.APObjectType {
|
if clientMsg.APObjectType == ap.ActivityFollow {
|
||||||
case ap.ActivityFollow:
|
|
||||||
// REJECT FOLLOW (request)
|
// REJECT FOLLOW (request)
|
||||||
return p.processRejectFollowFromClientAPI(ctx, clientMsg)
|
return p.processRejectFollowFromClientAPI(ctx, clientMsg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,8 +64,7 @@ func (p *processor) ProcessFromFederator(ctx context.Context, federatorMsg messa
|
||||||
}
|
}
|
||||||
case ap.ActivityUpdate:
|
case ap.ActivityUpdate:
|
||||||
// UPDATE SOMETHING
|
// UPDATE SOMETHING
|
||||||
switch federatorMsg.APObjectType {
|
if federatorMsg.APObjectType == ap.ObjectProfile {
|
||||||
case ap.ObjectProfile:
|
|
||||||
// UPDATE AN ACCOUNT
|
// UPDATE AN ACCOUNT
|
||||||
return p.processUpdateAccountFromFederator(ctx, federatorMsg)
|
return p.processUpdateAccountFromFederator(ctx, federatorMsg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,14 +38,15 @@ func (p *processor) ProcessVisibility(ctx context.Context, form *apimodel.Advanc
|
||||||
replyable := true
|
replyable := true
|
||||||
likeable := true
|
likeable := true
|
||||||
|
|
||||||
var vis gtsmodel.Visibility
|
|
||||||
// If visibility isn't set on the form, then just take the account default.
|
// If visibility isn't set on the form, then just take the account default.
|
||||||
// If that's also not set, take the default for the whole instance.
|
// If that's also not set, take the default for the whole instance.
|
||||||
if form.Visibility != "" {
|
var vis gtsmodel.Visibility
|
||||||
|
switch {
|
||||||
|
case form.Visibility != "":
|
||||||
vis = p.tc.APIVisToVis(form.Visibility)
|
vis = p.tc.APIVisToVis(form.Visibility)
|
||||||
} else if accountDefaultVis != "" {
|
case accountDefaultVis != "":
|
||||||
vis = accountDefaultVis
|
vis = accountDefaultVis
|
||||||
} else {
|
default:
|
||||||
vis = gtsmodel.VisibilityDefault
|
vis = gtsmodel.VisibilityDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ func oddOrEven(n int) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func noescape(str string) template.HTML {
|
func noescape(str string) template.HTML {
|
||||||
|
/* #nosec G203 */
|
||||||
return template.HTML(str)
|
return template.HTML(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,19 +68,21 @@ type iconWithLabel struct {
|
||||||
func visibilityIcon(visibility model.Visibility) template.HTML {
|
func visibilityIcon(visibility model.Visibility) template.HTML {
|
||||||
var icon iconWithLabel
|
var icon iconWithLabel
|
||||||
|
|
||||||
if visibility == model.VisibilityPublic {
|
switch visibility {
|
||||||
|
case model.VisibilityPublic:
|
||||||
icon = iconWithLabel{"globe", "public"}
|
icon = iconWithLabel{"globe", "public"}
|
||||||
} else if visibility == model.VisibilityUnlisted {
|
case model.VisibilityUnlisted:
|
||||||
icon = iconWithLabel{"unlock", "unlisted"}
|
icon = iconWithLabel{"unlock", "unlisted"}
|
||||||
} else if visibility == model.VisibilityPrivate {
|
case model.VisibilityPrivate:
|
||||||
icon = iconWithLabel{"lock", "private"}
|
icon = iconWithLabel{"lock", "private"}
|
||||||
} else if visibility == model.VisibilityMutualsOnly {
|
case model.VisibilityMutualsOnly:
|
||||||
icon = iconWithLabel{"handshake-o", "mutuals only"}
|
icon = iconWithLabel{"handshake-o", "mutuals only"}
|
||||||
} else if visibility == model.VisibilityDirect {
|
case model.VisibilityDirect:
|
||||||
icon = iconWithLabel{"envelope", "direct"}
|
icon = iconWithLabel{"envelope", "direct"}
|
||||||
}
|
}
|
||||||
|
|
||||||
return template.HTML(fmt.Sprintf(`<i aria-label="Visiblity: %v" class="fa fa-%v"></i>`, icon.label, icon.faIcon))
|
/* #nosec G203 */
|
||||||
|
return template.HTML(fmt.Sprintf(`<i aria-label="Visibility: %v" class="fa fa-%v"></i>`, icon.label, icon.faIcon))
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTemplateFunctions(engine *gin.Engine) {
|
func loadTemplateFunctions(engine *gin.Engine) {
|
||||||
|
|
|
@ -98,7 +98,7 @@ func (f *formatter) ReplaceLinks(ctx context.Context, in string) string {
|
||||||
shortString := thisURL.Hostname()
|
shortString := thisURL.Hostname()
|
||||||
|
|
||||||
if thisURL.Path != "" {
|
if thisURL.Path != "" {
|
||||||
shortString = shortString + thisURL.Path
|
shortString += thisURL.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
if thisURL.Fragment != "" {
|
if thisURL.Fragment != "" {
|
||||||
|
|
|
@ -127,7 +127,7 @@ func (t *timeline) GetXFromTop(ctx context.Context, amount int) ([]*apimodel.Sta
|
||||||
return nil, errors.New("GetXFromTop: could not parse e as a preparedPostsEntry")
|
return nil, errors.New("GetXFromTop: could not parse e as a preparedPostsEntry")
|
||||||
}
|
}
|
||||||
statuses = append(statuses, entry.prepared)
|
statuses = append(statuses, entry.prepared)
|
||||||
served = served + 1
|
served++
|
||||||
if served >= amount {
|
if served >= amount {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -145,7 +145,7 @@ func (t *timeline) GetXBehindID(ctx context.Context, amount int, behindID string
|
||||||
})
|
})
|
||||||
|
|
||||||
newAttempts := *attempts
|
newAttempts := *attempts
|
||||||
newAttempts = newAttempts + 1
|
newAttempts++
|
||||||
attempts = &newAttempts
|
attempts = &newAttempts
|
||||||
|
|
||||||
// make a slice of statuses with the length we need to return
|
// make a slice of statuses with the length we need to return
|
||||||
|
@ -161,7 +161,7 @@ func (t *timeline) GetXBehindID(ctx context.Context, amount int, behindID string
|
||||||
|
|
||||||
findMarkLoop:
|
findMarkLoop:
|
||||||
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
||||||
position = position + 1
|
position++
|
||||||
entry, ok := e.Value.(*preparedPostsEntry)
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("GetXBehindID: could not parse e as a preparedPostsEntry")
|
return nil, errors.New("GetXBehindID: could not parse e as a preparedPostsEntry")
|
||||||
|
@ -218,7 +218,7 @@ serveloop:
|
||||||
|
|
||||||
// serve up to the amount requested
|
// serve up to the amount requested
|
||||||
statuses = append(statuses, entry.prepared)
|
statuses = append(statuses, entry.prepared)
|
||||||
served = served + 1
|
served++
|
||||||
if served >= amount {
|
if served >= amount {
|
||||||
break serveloop
|
break serveloop
|
||||||
}
|
}
|
||||||
|
@ -272,7 +272,7 @@ findMarkLoop:
|
||||||
|
|
||||||
// serve up to the amount requested
|
// serve up to the amount requested
|
||||||
statuses = append(statuses, entry.prepared)
|
statuses = append(statuses, entry.prepared)
|
||||||
served = served + 1
|
served++
|
||||||
if served >= amount {
|
if served >= amount {
|
||||||
break serveloopFromTop
|
break serveloopFromTop
|
||||||
}
|
}
|
||||||
|
@ -288,7 +288,7 @@ findMarkLoop:
|
||||||
|
|
||||||
// serve up to the amount requested
|
// serve up to the amount requested
|
||||||
statuses = append(statuses, entry.prepared)
|
statuses = append(statuses, entry.prepared)
|
||||||
served = served + 1
|
served++
|
||||||
if served >= amount {
|
if served >= amount {
|
||||||
break serveloopFromBottom
|
break serveloopFromBottom
|
||||||
}
|
}
|
||||||
|
@ -311,7 +311,7 @@ func (t *timeline) GetXBetweenID(ctx context.Context, amount int, behindID strin
|
||||||
var behindIDMark *list.Element
|
var behindIDMark *list.Element
|
||||||
findMarkLoop:
|
findMarkLoop:
|
||||||
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
||||||
position = position + 1
|
position++
|
||||||
entry, ok := e.Value.(*preparedPostsEntry)
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("GetXBetweenID: could not parse e as a preparedPostsEntry")
|
return nil, errors.New("GetXBetweenID: could not parse e as a preparedPostsEntry")
|
||||||
|
@ -350,7 +350,7 @@ serveloop:
|
||||||
|
|
||||||
// serve up to the amount requested
|
// serve up to the amount requested
|
||||||
statuses = append(statuses, entry.prepared)
|
statuses = append(statuses, entry.prepared)
|
||||||
served = served + 1
|
served++
|
||||||
if served >= amount {
|
if served >= amount {
|
||||||
break serveloop
|
break serveloop
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ func (t *timeline) IndexBefore(ctx context.Context, statusID string, include boo
|
||||||
|
|
||||||
i := 0
|
i := 0
|
||||||
grabloop:
|
grabloop:
|
||||||
for ; len(filtered) < amount && i < 5; i = i + 1 { // try the grabloop 5 times only
|
for ; len(filtered) < amount && i < 5; i++ { // try the grabloop 5 times only
|
||||||
statuses, err := t.db.GetHomeTimeline(ctx, t.accountID, "", "", offsetStatus, amount, false)
|
statuses, err := t.db.GetHomeTimeline(ctx, t.accountID, "", "", offsetStatus, amount, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == db.ErrNoEntries {
|
if err == db.ErrNoEntries {
|
||||||
|
@ -129,7 +129,7 @@ positionLoop:
|
||||||
|
|
||||||
i := 0
|
i := 0
|
||||||
grabloop:
|
grabloop:
|
||||||
for ; len(filtered) < amount && i < 5; i = i + 1 { // try the grabloop 5 times only
|
for ; len(filtered) < amount && i < 5; i++ { // try the grabloop 5 times only
|
||||||
l.Tracef("entering grabloop; i is %d; len(filtered) is %d", i, len(filtered))
|
l.Tracef("entering grabloop; i is %d; len(filtered) is %d", i, len(filtered))
|
||||||
statuses, err := t.db.GetHomeTimeline(ctx, t.accountID, offsetStatus, "", "", amount, false)
|
statuses, err := t.db.GetHomeTimeline(ctx, t.accountID, offsetStatus, "", "", amount, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -50,7 +50,7 @@ func (p *postIndex) insertIndexed(i *postIndexEntry) (bool, error) {
|
||||||
// We need to iterate through the index to make sure we put this post in the appropriate place according to when it was created.
|
// We need to iterate through the index to make sure we put this post in the appropriate place according to when it was created.
|
||||||
// We also need to make sure we're not inserting a duplicate post -- this can happen sometimes and it's not nice UX (*shudder*).
|
// We also need to make sure we're not inserting a duplicate post -- this can happen sometimes and it's not nice UX (*shudder*).
|
||||||
for e := p.data.Front(); e != nil; e = e.Next() {
|
for e := p.data.Front(); e != nil; e = e.Next() {
|
||||||
position = position + 1
|
position++
|
||||||
|
|
||||||
entry, ok := e.Value.(*postIndexEntry)
|
entry, ok := e.Value.(*postIndexEntry)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
@ -107,7 +107,7 @@ prepareloop:
|
||||||
// we're done
|
// we're done
|
||||||
break prepareloop
|
break prepareloop
|
||||||
}
|
}
|
||||||
prepared = prepared + 1
|
prepared++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,7 +162,7 @@ prepareloop:
|
||||||
// we're done
|
// we're done
|
||||||
break prepareloop
|
break prepareloop
|
||||||
}
|
}
|
||||||
prepared = prepared + 1
|
prepared++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,7 +214,7 @@ prepareloop:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared = prepared + 1
|
prepared++
|
||||||
if prepared == amount {
|
if prepared == amount {
|
||||||
// we're done
|
// we're done
|
||||||
l.Trace("leaving prepareloop")
|
l.Trace("leaving prepareloop")
|
||||||
|
|
|
@ -53,7 +53,7 @@ func (p *preparedPosts) insertPrepared(i *preparedPostsEntry) error {
|
||||||
// We need to iterate through the index to make sure we put this post in the appropriate place according to when it was created.
|
// We need to iterate through the index to make sure we put this post in the appropriate place according to when it was created.
|
||||||
// We also need to make sure we're not inserting a duplicate post -- this can happen sometimes and it's not nice UX (*shudder*).
|
// We also need to make sure we're not inserting a duplicate post -- this can happen sometimes and it's not nice UX (*shudder*).
|
||||||
for e := p.data.Front(); e != nil; e = e.Next() {
|
for e := p.data.Front(); e != nil; e = e.Next() {
|
||||||
position = position + 1
|
position++
|
||||||
|
|
||||||
entry, ok := e.Value.(*preparedPostsEntry)
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
@ -52,7 +52,7 @@ func (t *timeline) Remove(ctx context.Context, statusID string) (int, error) {
|
||||||
}
|
}
|
||||||
for _, e := range removeIndexes {
|
for _, e := range removeIndexes {
|
||||||
t.postIndex.data.Remove(e)
|
t.postIndex.data.Remove(e)
|
||||||
removed = removed + 1
|
removed++
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove entr(ies) from prepared posts
|
// remove entr(ies) from prepared posts
|
||||||
|
@ -71,7 +71,7 @@ func (t *timeline) Remove(ctx context.Context, statusID string) (int, error) {
|
||||||
}
|
}
|
||||||
for _, e := range removePrepared {
|
for _, e := range removePrepared {
|
||||||
t.preparedPosts.data.Remove(e)
|
t.preparedPosts.data.Remove(e)
|
||||||
removed = removed + 1
|
removed++
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Debugf("removed %d entries", removed)
|
l.Debugf("removed %d entries", removed)
|
||||||
|
@ -104,7 +104,7 @@ func (t *timeline) RemoveAllBy(ctx context.Context, accountID string) (int, erro
|
||||||
}
|
}
|
||||||
for _, e := range removeIndexes {
|
for _, e := range removeIndexes {
|
||||||
t.postIndex.data.Remove(e)
|
t.postIndex.data.Remove(e)
|
||||||
removed = removed + 1
|
removed++
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove entr(ies) from prepared posts
|
// remove entr(ies) from prepared posts
|
||||||
|
@ -123,7 +123,7 @@ func (t *timeline) RemoveAllBy(ctx context.Context, accountID string) (int, erro
|
||||||
}
|
}
|
||||||
for _, e := range removePrepared {
|
for _, e := range removePrepared {
|
||||||
t.preparedPosts.data.Remove(e)
|
t.preparedPosts.data.Remove(e)
|
||||||
removed = removed + 1
|
removed++
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Debugf("removed %d entries", removed)
|
l.Debugf("removed %d entries", removed)
|
||||||
|
|
|
@ -29,7 +29,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testModels []interface{} = []interface{}{
|
var testModels = []interface{}{
|
||||||
>smodel.Account{},
|
>smodel.Account{},
|
||||||
>smodel.Application{},
|
>smodel.Application{},
|
||||||
>smodel.Block{},
|
>smodel.Block{},
|
||||||
|
|
Loading…
Reference in New Issue