Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blocklist import #77

Merged
merged 2 commits into from
Jul 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@
* [ ] Blocks
* [ ] /api/v1/blocks GET (See list of blocked accounts)
* [ ] Domain Blocks
* [ ] /api/v1/domain_blocks GET (See list of domain blocks)
* [ ] /api/v1/domain_blocks POST (Create a domain block)
* [ ] /api/v1/domain_blocks DELETE (Remove a domain block)
* [x] /api/v1/domain_blocks GET (See list of domain blocks)
* [x] /api/v1/domain_blocks POST (Create a domain block)
* [x] /api/v1/domain_blocks DELETE (Remove a domain block)
* [ ] Filters
* [ ] /api/v1/filters GET (Get list of filters)
* [ ] /api/v1/filters/:id GET (View a filter)
Expand Down Expand Up @@ -134,7 +134,7 @@
* [x] /api/v2/search GET (Get search query results)
* [ ] Instance
* [x] /api/v1/instance GET (Get instance information)
* [ ] /api/v1/instance PATCH (Update instance information)
* [x] /api/v1/instance PATCH (Update instance information)
* [ ] /api/v1/instance/peers GET (Get list of federated servers)
* [ ] /api/v1/instance/activity GET (Instance activity over the last 3 months, binned weekly.)
* [ ] Trends
Expand Down Expand Up @@ -198,10 +198,10 @@
* [ ] App creation guide
* [ ] Tooling
* [ ] Database migration tool
* [ ] Admin CLI tool
* [x] Admin CLI tool
* [ ] Build
* [ ] Docker containerization
* [ ] Dockerfile
* [x] Docker containerization
* [x] Dockerfile
* [ ] docker-compose.yml
* [ ] Tests
* [ ] Unit/integration
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,15 @@ A grab-bag of things that are already included or will be included in the projec

## Implementation Status

Things are moving on the project! As of June 2021 you can now:
Things are moving on the project! As of July 2021 you can now:

### Admin

* Build and deploy GoToSocial as a binary, with automatic LetsEncrypt certificate support built-in.
* Create, confirm, and promote users using self-documented CLI tool.

### User

* Connect to the running instance via Tusky or Pinafore, using email address and password (stored encrypted).
* Post/delete posts.
* Reply/delete replies.
Expand All @@ -44,7 +50,12 @@ Things are moving on the project! As of June 2021 you can now:
* View local timeline.
* View and scroll home timeline (with ~10ms latency hell yeah).
* Stream new posts, notifications and deletes through a websockets connection via Pinafore.

### Federation

* Federation support and interoperability with Mastodon and others.
* Domain blocking: create, update, delete, and export domain blocks.
* Domain blocking: import lists of domain blocks -- no more blocking domains one-by-one.

In other words, a deployed GoToSocial instance is already pretty useable!

Expand Down
10 changes: 6 additions & 4 deletions internal/api/client/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ const (
EmojiPath = BasePath + "/custom_emojis"
// DomainBlocksPath is used for posting domain blocks.
DomainBlocksPath = BasePath + "/domain_blocks"
// DomainBlockPath is used for interacting with a single domain block.
DomainBlockPath = DomainBlocksPath + "/:" + IDKey
// DomainBlocksPathWithID is used for interacting with a single domain block.
DomainBlocksPathWithID = DomainBlocksPath + "/:" + IDKey

// ExportQueryKey is for requesting a public export of some data.
ExportQueryKey = "export"
// ImportQueryKey is for submitting an import of some data.
ImportQueryKey = "import"
// IDKey specifies the ID of a single item being interacted with.
IDKey = "id"
)
Expand All @@ -65,7 +67,7 @@ func (m *Module) Route(r router.Router) error {
r.AttachHandler(http.MethodPost, EmojiPath, m.emojiCreatePOSTHandler)
r.AttachHandler(http.MethodPost, DomainBlocksPath, m.DomainBlocksPOSTHandler)
r.AttachHandler(http.MethodGet, DomainBlocksPath, m.DomainBlocksGETHandler)
r.AttachHandler(http.MethodGet, DomainBlockPath, m.DomainBlockGETHandler)
r.AttachHandler(http.MethodDelete, DomainBlockPath, m.DomainBlockDELETEHandler)
r.AttachHandler(http.MethodGet, DomainBlocksPathWithID, m.DomainBlockGETHandler)
r.AttachHandler(http.MethodDelete, DomainBlocksPathWithID, m.DomainBlockDELETEHandler)
return nil
}
54 changes: 42 additions & 12 deletions internal/api/client/admin/domainblockcreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
"strconv"

"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -33,6 +34,18 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) {
return
}

imp := false
importString := c.Query(ImportQueryKey)
if importString != "" {
i, err := strconv.ParseBool(importString)
if err != nil {
l.Debugf("error parsing import string: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse import query param"})
return
}
imp = i
}

// extract the media create form from the request context
l.Tracef("parsing request form: %+v", c.Request.Form)
form := &model.DomainBlockCreateRequest{}
Expand All @@ -44,26 +57,43 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) {

// Give the fields on the request form a first pass to make sure the request is superficially valid.
l.Tracef("validating form %+v", form)
if err := validateCreateDomainBlock(form); err != nil {
if err := validateCreateDomainBlock(form, imp); err != nil {
l.Debugf("error validating form: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

domainBlock, err := m.processor.AdminDomainBlockCreate(authed, form)
if err != nil {
l.Debugf("error creating domain block: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if imp {
// we're importing multiple blocks
domainBlocks, err := m.processor.AdminDomainBlocksImport(authed, form)
if err != nil {
l.Debugf("error importing domain blocks: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, domainBlocks)
} else {
// we're just creating one block
domainBlock, err := m.processor.AdminDomainBlockCreate(authed, form)
if err != nil {
l.Debugf("error creating domain block: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, domainBlock)
}

c.JSON(http.StatusOK, domainBlock)
}

func validateCreateDomainBlock(form *model.DomainBlockCreateRequest) error {
// add some more validation here later if necessary
if form.Domain == "" {
return errors.New("empty domain provided")
func validateCreateDomainBlock(form *model.DomainBlockCreateRequest, imp bool) error {
if imp {
if form.Domains.Size == 0 {
return errors.New("import was specified but list of domains is empty")
}
} else {
// add some more validation here later if necessary
if form.Domain == "" {
return errors.New("empty domain provided")
}
}

return nil
Expand Down
10 changes: 7 additions & 3 deletions internal/api/model/domainblock.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,26 @@

package model

import "mime/multipart"

// DomainBlock represents a block on one domain
type DomainBlock struct {
ID string `json:"id,omitempty"`
Domain string `json:"domain"`
Domain string `form:"domain" json:"domain" validation:"required"`
Obfuscate bool `json:"obfuscate,omitempty"`
PrivateComment string `json:"private_comment,omitempty"`
PublicComment string `json:"public_comment,omitempty"`
PublicComment string `form:"public_comment" json:"public_comment,omitempty"`
SubscriptionID string `json:"subscription_id,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
}

// DomainBlockCreateRequest is the form submitted as a POST to /api/v1/admin/domain_blocks to create a new block.
type DomainBlockCreateRequest struct {
// A list of domains to block. Only used if import=true is specified.
Domains *multipart.FileHeader `form:"domains" json:"domains" xml:"domains"`
// hostname/domain to block
Domain string `form:"domain" json:"domain" xml:"domain" validation:"required"`
Domain string `form:"domain" json:"domain" xml:"domain"`
// whether the domain should be obfuscated when being displayed publicly
Obfuscate bool `form:"obfuscate" json:"obfuscate" xml:"obfuscate"`
// private comment for other admins on why the domain was blocked
Expand Down
3 changes: 3 additions & 0 deletions internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ type DB interface {
// UpdateOneByID updates interface i with database the given database id. It will update one field of key key and value value.
UpdateOneByID(id string, key string, value interface{}, i interface{}) error

// UpdateWhere updates column key of interface i with the given value, where the given parameters apply.
UpdateWhere(where []Where, key string, value interface{}, i interface{}) error

// DeleteByID removes i with id id.
// If i didn't exist anyway, then no error should be returned.
DeleteByID(id string, i interface{}) error
Expand Down
57 changes: 57 additions & 0 deletions internal/db/pg/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
GoToSocial
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org

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 pg

import (
"errors"

"github.com/go-pg/pg/v10"
"github.com/superseriousbusiness/gotosocial/internal/db"
)

func (ps *postgresService) DeleteByID(id string, i interface{}) error {
if _, err := ps.conn.Model(i).Where("id = ?", id).Delete(); err != nil {
// if there are no rows *anyway* then that's fine
// just return err if there's an actual error
if err != pg.ErrNoRows {
return err
}
}
return nil
}

func (ps *postgresService) DeleteWhere(where []db.Where, i interface{}) error {
if len(where) == 0 {
return errors.New("no queries provided")
}

q := ps.conn.Model(i)
for _, w := range where {
q = q.Where("? = ?", pg.Safe(w.Key), w.Value)
}

if _, err := q.Delete(); err != nil {
// if there are no rows *anyway* then that's fine
// just return err if there's an actual error
if err != pg.ErrNoRows {
return err
}
}
return nil
}
75 changes: 75 additions & 0 deletions internal/db/pg/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
GoToSocial
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org

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 pg

import (
"errors"

"github.com/go-pg/pg/v10"
"github.com/superseriousbusiness/gotosocial/internal/db"
)

func (ps *postgresService) GetByID(id string, i interface{}) error {
if err := ps.conn.Model(i).Where("id = ?", id).Select(); err != nil {
if err == pg.ErrNoRows {
return db.ErrNoEntries{}
}
return err

}
return nil
}

func (ps *postgresService) GetWhere(where []db.Where, i interface{}) error {
if len(where) == 0 {
return errors.New("no queries provided")
}

q := ps.conn.Model(i)
for _, w := range where {

if w.Value == nil {
q = q.Where("? IS NULL", pg.Ident(w.Key))
} else {
if w.CaseInsensitive {
q = q.Where("LOWER(?) = LOWER(?)", pg.Safe(w.Key), w.Value)
} else {
q = q.Where("? = ?", pg.Safe(w.Key), w.Value)
}
}
}

if err := q.Select(); err != nil {
if err == pg.ErrNoRows {
return db.ErrNoEntries{}
}
return err
}
return nil
}

func (ps *postgresService) GetAll(i interface{}) error {
if err := ps.conn.Model(i).Select(); err != nil {
if err == pg.ErrNoRows {
return db.ErrNoEntries{}
}
return err
}
return nil
}
22 changes: 20 additions & 2 deletions internal/db/pg/instance.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
/*
GoToSocial
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org

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 pg

import (
Expand Down Expand Up @@ -42,8 +60,8 @@ func (ps *postgresService) GetDomainCountForInstance(domain string) (int, error)

if domain == ps.config.Host {
// if the domain is *this* domain, just count other instances it knows about
// TODO: exclude domains that are blocked or silenced
q = q.Where("domain != ?", domain)
// exclude domains that are blocked
q = q.Where("domain != ?", domain).Where("? IS NULL", pg.Ident("suspended_at"))
} else {
// TODO: implement federated domain counting properly for remote domains
return 0, nil
Expand Down
Loading