refactor package structure & remove v1 API

This commit is contained in:
Lukas Schulte Pelkum 2022-08-29 15:52:21 +02:00
parent 1a574add49
commit e93b292daf
No known key found for this signature in database
GPG Key ID: 408DA7CA81DB885C
15 changed files with 71 additions and 385 deletions

View File

@ -5,7 +5,6 @@ import (
"os"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
"github.com/lus/pasty/internal/storage"
)
@ -20,7 +19,7 @@ func main() {
config.Load()
// Create and initialize the first (from) driver
from, err := storage.GetDriver(shared.StorageType(os.Args[1]))
from, err := storage.GetDriver(storage.Type(os.Args[1]))
if err != nil {
panic(err)
}
@ -30,7 +29,7 @@ func main() {
}
// Create and initialize the second (to) driver
to, err := storage.GetDriver(shared.StorageType(os.Args[2]))
to, err := storage.GetDriver(storage.Type(os.Args[2]))
if err != nil {
panic(err)
}

View File

@ -5,13 +5,12 @@ import (
"time"
"github.com/lus/pasty/internal/env"
"github.com/lus/pasty/internal/shared"
)
// Config represents the general application configuration structure
type Config struct {
WebAddress string
StorageType shared.StorageType
StorageType string
HastebinSupport bool
IDLength int
IDCharacters string
@ -80,7 +79,7 @@ func Load() {
Current = &Config{
WebAddress: env.MustString("WEB_ADDRESS", ":8080"),
StorageType: shared.StorageType(strings.ToLower(env.MustString("STORAGE_TYPE", "file"))),
StorageType: strings.ToLower(env.MustString("STORAGE_TYPE", "file")),
HastebinSupport: env.MustBool("HASTEBIN_SUPPORT", false),
IDLength: env.MustInt("ID_LENGTH", 6),
IDCharacters: env.MustString("ID_CHARACTERS", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"),

30
internal/paste/paste.go Normal file
View File

@ -0,0 +1,30 @@
package paste
import (
"github.com/alexedwards/argon2id"
)
// Paste represents a paste
type Paste struct {
ID string `json:"id"`
Content string `json:"content"`
ModificationToken string `json:"modificationToken,omitempty"`
Created int64 `json:"created"`
Metadata map[string]interface{} `json:"metadata"`
}
// HashModificationToken hashes the current modification token of a paste
func (paste *Paste) HashModificationToken() error {
hash, err := argon2id.CreateHash(paste.ModificationToken, argon2id.DefaultParams)
if err != nil {
return err
}
paste.ModificationToken = hash
return nil
}
// CheckModificationToken checks whether or not the given modification token is correct
func (paste *Paste) CheckModificationToken(modificationToken string) bool {
match, err := argon2id.ComparePasswordAndHash(modificationToken, paste.ModificationToken)
return err == nil && match
}

View File

@ -1,42 +0,0 @@
package shared
import (
"log"
"github.com/alexedwards/argon2id"
)
// Paste represents a saved paste
type Paste struct {
ID string `json:"id" bson:"_id"`
Content string `json:"content" bson:"content"`
DeletionToken string `json:"deletionToken,omitempty" bson:"deletionToken"` // Required for legacy paste storage support
ModificationToken string `json:"modificationToken,omitempty" bson:"modificationToken"`
Created int64 `json:"created" bson:"created"`
Metadata map[string]interface{} `json:"metadata" bson:"metadata"`
}
// HashModificationToken hashes the current modification token of a paste
func (paste *Paste) HashModificationToken() error {
hash, err := argon2id.CreateHash(paste.ModificationToken, argon2id.DefaultParams)
if err != nil {
return err
}
paste.ModificationToken = hash
return nil
}
// CheckModificationToken checks whether or not the given modification token is correct
func (paste *Paste) CheckModificationToken(modificationToken string) bool {
// The modification token may be stored in the deletion token field in old pastes
usedToken := paste.ModificationToken
if usedToken == "" {
usedToken = paste.DeletionToken
if usedToken != "" {
log.Println("WARNING: You seem to have pastes with the old 'deletionToken' field stored in your storage driver. Though this does not cause any issues right now, it may in the future. Consider some kind of migration.")
}
}
match, err := argon2id.ComparePasswordAndHash(modificationToken, usedToken)
return err == nil && match
}

View File

@ -1,11 +0,0 @@
package shared
// StorageType represents a type of storage a paste can be stored with
type StorageType string
const (
StorageTypeFile = StorageType("file")
StorageTypePostgres = StorageType("postgres")
StorageTypeMongoDB = StorageType("mongodb")
StorageTypeS3 = StorageType("s3")
)

View File

@ -2,9 +2,10 @@ package storage
import (
"fmt"
"strings"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
"github.com/lus/pasty/internal/paste"
"github.com/lus/pasty/internal/storage/file"
"github.com/lus/pasty/internal/storage/mongodb"
"github.com/lus/pasty/internal/storage/postgres"
@ -19,8 +20,8 @@ type Driver interface {
Initialize() error
Terminate() error
ListIDs() ([]string, error)
Get(id string) (*shared.Paste, error)
Save(paste *shared.Paste) error
Get(id string) (*paste.Paste, error)
Save(paste *paste.Paste) error
Delete(id string) error
Cleanup() (int, error)
}
@ -43,15 +44,15 @@ func Load() error {
}
// GetDriver returns the driver with the given type if it exists
func GetDriver(storageType shared.StorageType) (Driver, error) {
switch storageType {
case shared.StorageTypeFile:
func GetDriver(storageType string) (Driver, error) {
switch strings.TrimSpace(strings.ToLower(storageType)) {
case "file":
return new(file.FileDriver), nil
case shared.StorageTypePostgres:
case "postgres":
return new(postgres.PostgresDriver), nil
case shared.StorageTypeMongoDB:
case "mongodb":
return new(mongodb.MongoDBDriver), nil
case shared.StorageTypeS3:
case "s3":
return new(s3.S3Driver), nil
default:
return nil, fmt.Errorf("invalid storage type '%s'", storageType)

View File

@ -10,7 +10,7 @@ import (
"time"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
"github.com/lus/pasty/internal/paste"
)
// FileDriver represents the file storage driver
@ -35,7 +35,7 @@ func (driver *FileDriver) ListIDs() ([]string, error) {
var ids []string
// Fill the IDs slice
err := filepath.Walk(driver.filePath, func(path string, info os.FileInfo, err error) error {
err := filepath.Walk(driver.filePath, func(_ string, info os.FileInfo, err error) error {
// Check if a walking error occurred
if err != nil {
return err
@ -65,7 +65,7 @@ func (driver *FileDriver) ListIDs() ([]string, error) {
}
// Get loads a paste
func (driver *FileDriver) Get(id string) (*shared.Paste, error) {
func (driver *FileDriver) Get(id string) (*paste.Paste, error) {
// Read the file
id = base64.StdEncoding.EncodeToString([]byte(id))
data, err := ioutil.ReadFile(filepath.Join(driver.filePath, id+".json"))
@ -77,7 +77,7 @@ func (driver *FileDriver) Get(id string) (*shared.Paste, error) {
}
// Unmarshal the file into a paste
paste := new(shared.Paste)
paste := new(paste.Paste)
err = json.Unmarshal(data, &paste)
if err != nil {
return nil, err
@ -86,7 +86,7 @@ func (driver *FileDriver) Get(id string) (*shared.Paste, error) {
}
// Save saves a paste
func (driver *FileDriver) Save(paste *shared.Paste) error {
func (driver *FileDriver) Save(paste *paste.Paste) error {
// Marshal the paste
jsonBytes, err := json.Marshal(paste)
if err != nil {

View File

@ -5,7 +5,7 @@ import (
"time"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
"github.com/lus/pasty/internal/paste"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
@ -65,7 +65,7 @@ func (driver *MongoDBDriver) ListIDs() ([]string, error) {
}
// Decode all paste documents
var pasteSlice []shared.Paste
var pasteSlice []paste.Paste
err = result.All(ctx, &pasteSlice)
if err != nil {
return nil, err
@ -80,7 +80,7 @@ func (driver *MongoDBDriver) ListIDs() ([]string, error) {
}
// Get loads a paste
func (driver *MongoDBDriver) Get(id string) (*shared.Paste, error) {
func (driver *MongoDBDriver) Get(id string) (*paste.Paste, error) {
// Define the collection to use for this database operation
collection := driver.client.Database(driver.database).Collection(driver.collection)
@ -100,7 +100,7 @@ func (driver *MongoDBDriver) Get(id string) (*shared.Paste, error) {
}
// Return the retrieved paste object
paste := new(shared.Paste)
paste := new(paste.Paste)
err = result.Decode(paste)
if err != nil {
return nil, err
@ -109,7 +109,7 @@ func (driver *MongoDBDriver) Get(id string) (*shared.Paste, error) {
}
// Save saves a paste
func (driver *MongoDBDriver) Save(paste *shared.Paste) error {
func (driver *MongoDBDriver) Save(paste *paste.Paste) error {
// Define the collection to use for this database operation
collection := driver.client.Database(driver.database).Collection(driver.collection)

View File

@ -12,7 +12,7 @@ import (
"github.com/jackc/pgx/v4/pgxpool"
"github.com/johejo/golang-migrate-extra/source/iofs"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
"github.com/lus/pasty/internal/paste"
)
//go:embed migrations/*.sql
@ -76,12 +76,12 @@ func (driver *PostgresDriver) ListIDs() ([]string, error) {
}
// Get loads a paste
func (driver *PostgresDriver) Get(id string) (*shared.Paste, error) {
func (driver *PostgresDriver) Get(id string) (*paste.Paste, error) {
query := "SELECT * FROM pastes WHERE id = $1"
row := driver.pool.QueryRow(context.Background(), query, id)
paste := new(shared.Paste)
paste := new(paste.Paste)
if err := row.Scan(&paste.ID, &paste.Content, &paste.ModificationToken, &paste.Created, &paste.Metadata); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
@ -92,7 +92,7 @@ func (driver *PostgresDriver) Get(id string) (*shared.Paste, error) {
}
// Save saves a paste
func (driver *PostgresDriver) Save(paste *shared.Paste) error {
func (driver *PostgresDriver) Save(paste *paste.Paste) error {
query := `
INSERT INTO pastes (id, content, "modificationToken", created, metadata)
VALUES ($1, $2, $3, $4, $5)

View File

@ -9,7 +9,7 @@ import (
"time"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
"github.com/lus/pasty/internal/paste"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
@ -59,7 +59,7 @@ func (driver *S3Driver) ListIDs() ([]string, error) {
}
// Get loads a paste
func (driver *S3Driver) Get(id string) (*shared.Paste, error) {
func (driver *S3Driver) Get(id string) (*paste.Paste, error) {
// Read the object
object, err := driver.client.GetObject(context.Background(), driver.bucket, id+".json", minio.GetObjectOptions{})
if err != nil {
@ -74,7 +74,7 @@ func (driver *S3Driver) Get(id string) (*shared.Paste, error) {
}
// Unmarshal the object into a paste
paste := new(shared.Paste)
paste := new(paste.Paste)
err = json.Unmarshal(data, &paste)
if err != nil {
return nil, err
@ -83,7 +83,7 @@ func (driver *S3Driver) Get(id string) (*shared.Paste, error) {
}
// Save saves a paste
func (driver *S3Driver) Save(paste *shared.Paste) error {
func (driver *S3Driver) Save(paste *paste.Paste) error {
// Marshal the paste
jsonBytes, err := json.Marshal(paste)
if err != nil {

View File

@ -1,72 +0,0 @@
package v1
import (
"encoding/json"
"time"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
"github.com/lus/pasty/internal/storage"
"github.com/lus/pasty/internal/utils"
"github.com/valyala/fasthttp"
)
// HastebinSupportHandler handles the legacy hastebin requests
func HastebinSupportHandler(ctx *fasthttp.RequestCtx) {
// Check content length before reading body into memory
if config.Current.LengthCap > 0 &&
ctx.Request.Header.ContentLength() > config.Current.LengthCap {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("request body length overflow")
return
}
// Define the paste content
var content string
if string(ctx.Request.Header.ContentType()) == "multipart/form-data" {
content = string(ctx.FormValue("data"))
} else {
content = string(ctx.PostBody())
}
// Acquire the paste ID
id, err := storage.AcquireID()
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
// Create the paste object
paste := &shared.Paste{
ID: id,
Content: content,
Created: time.Now().Unix(),
}
// Set a modification token
if config.Current.ModificationTokens {
paste.ModificationToken = utils.RandomString(config.Current.ModificationTokenCharacters, config.Current.ModificationTokenLength)
err = paste.HashModificationToken()
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
}
// Save the paste
err = storage.Current.Save(paste)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
// Respond with the paste key
jsonData, _ := json.Marshal(map[string]string{
"key": paste.ID,
})
ctx.SetBody(jsonData)
}

View File

@ -1,24 +0,0 @@
package v1
import "github.com/lus/pasty/internal/shared"
type legacyPaste struct {
ID string `json:"id"`
Content string `json:"content"`
DeletionToken string `json:"deletionToken,omitempty"`
Created int64 `json:"created"`
}
func legacyFromModern(paste *shared.Paste) *legacyPaste {
deletionToken := paste.ModificationToken
if deletionToken == "" {
deletionToken = paste.DeletionToken
}
return &legacyPaste{
ID: paste.ID,
Content: paste.Content,
DeletionToken: deletionToken,
Created: paste.Created,
}
}

View File

@ -1,180 +0,0 @@
package v1
import (
"encoding/json"
"time"
"github.com/fasthttp/router"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
"github.com/lus/pasty/internal/storage"
"github.com/lus/pasty/internal/utils"
limitFasthttp "github.com/ulule/limiter/v3/drivers/middleware/fasthttp"
"github.com/valyala/fasthttp"
)
// InitializePastesController initializes the '/v1/pastes/*' controller
func InitializePastesController(group *router.Group, rateLimiterMiddleware *limitFasthttp.Middleware) {
group.GET("/{id}", rateLimiterMiddleware.Handle(v1GetPaste))
group.POST("", rateLimiterMiddleware.Handle(v1PostPaste))
group.DELETE("/{id}", rateLimiterMiddleware.Handle(v1DeletePaste))
}
// v1GetPaste handles the 'GET /v1/pastes/{id}' endpoint
func v1GetPaste(ctx *fasthttp.RequestCtx) {
// Read the ID
id := ctx.UserValue("id").(string)
// Retrieve the paste
paste, err := storage.Current.Get(id)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
if paste == nil {
ctx.SetStatusCode(fasthttp.StatusNotFound)
ctx.SetBodyString("paste not found")
return
}
legacyPaste := legacyFromModern(paste)
legacyPaste.DeletionToken = ""
// Respond with the paste
jsonData, err := json.Marshal(legacyPaste)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
ctx.SetBody(jsonData)
}
// v1PostPaste handles the 'POST /v1/pastes' endpoint
func v1PostPaste(ctx *fasthttp.RequestCtx) {
// Check content length before reading body into memory
if config.Current.LengthCap > 0 &&
ctx.Request.Header.ContentLength() > config.Current.LengthCap {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("request body length overflow")
return
}
// Unmarshal the body
values := make(map[string]string)
err := json.Unmarshal(ctx.PostBody(), &values)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("invalid request body")
return
}
// Validate the content of the paste
if values["content"] == "" {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("missing 'content' field")
return
}
// Acquire the paste ID
id, err := storage.AcquireID()
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
// Create the paste object
paste := &shared.Paste{
ID: id,
Content: values["content"],
Created: time.Now().Unix(),
}
// Set a modification token
modificationToken := ""
if config.Current.ModificationTokens {
modificationToken = utils.RandomString(config.Current.ModificationTokenCharacters, config.Current.ModificationTokenLength)
paste.ModificationToken = modificationToken
err = paste.HashModificationToken()
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
}
// Save the paste
err = storage.Current.Save(paste)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
// Respond with the paste
pasteCopy := legacyFromModern(paste)
pasteCopy.DeletionToken = modificationToken
jsonData, err := json.Marshal(pasteCopy)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
ctx.SetBody(jsonData)
}
// v1DeletePaste handles the 'DELETE /v1/pastes/{id}'
func v1DeletePaste(ctx *fasthttp.RequestCtx) {
// Read the ID
id := ctx.UserValue("id").(string)
// Unmarshal the body
values := make(map[string]string)
err := json.Unmarshal(ctx.PostBody(), &values)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("invalid request body")
return
}
// Validate the modification token of the paste
modificationToken := values["deletionToken"]
if modificationToken == "" {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("missing 'deletionToken' field")
return
}
// Retrieve the paste
paste, err := storage.Current.Get(id)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
if paste == nil {
ctx.SetStatusCode(fasthttp.StatusNotFound)
ctx.SetBodyString("paste not found")
return
}
// Check if the modification token is correct
if (config.Current.ModificationTokenMaster == "" || modificationToken != config.Current.ModificationTokenMaster) && !paste.CheckModificationToken(modificationToken) {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetBodyString("invalid deletion token")
return
}
// Delete the paste
err = storage.Current.Delete(paste.ID)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
// Respond with 'ok'
ctx.SetBodyString("ok")
}

View File

@ -9,8 +9,7 @@ import (
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/static"
"github.com/lus/pasty/internal/storage"
v1 "github.com/lus/pasty/internal/web/controllers/v1"
v2 "github.com/lus/pasty/internal/web/controllers/v2"
v2 "github.com/lus/pasty/internal/web/v2"
"github.com/ulule/limiter/v3"
limitFasthttp "github.com/ulule/limiter/v3/drivers/middleware/fasthttp"
"github.com/ulule/limiter/v3/drivers/store/memory"
@ -57,18 +56,6 @@ func Serve() error {
// Route the API endpoints
apiRoute := router.Group("/api")
{
v1Route := apiRoute.Group("/v1")
{
v1Route.GET("/info", func(ctx *fasthttp.RequestCtx) {
jsonData, _ := json.Marshal(map[string]interface{}{
"version": static.Version,
"deletionTokens": config.Current.ModificationTokens,
})
ctx.SetBody(jsonData)
})
v1.InitializePastesController(v1Route.Group("/pastes"), rateLimiterMiddleware)
}
v2Route := apiRoute.Group("/v2")
{
pasteLifetime := int64(-1)
@ -90,7 +77,7 @@ func Serve() error {
// Route the hastebin documents route if hastebin support is enabled
if config.Current.HastebinSupport {
router.POST("/documents", rateLimiterMiddleware.Handle(v1.HastebinSupportHandler))
// TODO: Reimplement hastebin support
}
// Serve the web resources

View File

@ -7,8 +7,8 @@ import (
"github.com/fasthttp/router"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/paste"
"github.com/lus/pasty/internal/report"
"github.com/lus/pasty/internal/shared"
"github.com/lus/pasty/internal/storage"
"github.com/lus/pasty/internal/utils"
limitFasthttp "github.com/ulule/limiter/v3/drivers/middleware/fasthttp"
@ -58,7 +58,7 @@ func middlewareInjectPaste(next fasthttp.RequestHandler) fasthttp.RequestHandler
// middlewareValidateModificationToken extracts and validates a given modification token for an injected paste
func middlewareValidateModificationToken(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
paste := ctx.UserValue("_paste").(*shared.Paste)
paste := ctx.UserValue("_paste").(*paste.Paste)
authHeaderSplit := strings.SplitN(string(ctx.Request.Header.Peek("Authorization")), " ", 2)
if len(authHeaderSplit) < 2 || authHeaderSplit[0] != "Bearer" {
@ -85,8 +85,7 @@ func middlewareValidateModificationToken(next fasthttp.RequestHandler) fasthttp.
// endpointGetPaste handles the 'GET /v2/pastes/{id}' endpoint
func endpointGetPaste(ctx *fasthttp.RequestCtx) {
paste := ctx.UserValue("_paste").(*shared.Paste)
paste.DeletionToken = ""
paste := ctx.UserValue("_paste").(*paste.Paste)
paste.ModificationToken = ""
jsonData, err := json.Marshal(paste)
@ -135,7 +134,7 @@ func endpointCreatePaste(ctx *fasthttp.RequestCtx) {
if payload.Metadata == nil {
payload.Metadata = map[string]interface{}{}
}
paste := &shared.Paste{
paste := &paste.Paste{
ID: id,
Content: payload.Content,
Created: time.Now().Unix(),
@ -203,7 +202,7 @@ func endpointModifyPaste(ctx *fasthttp.RequestCtx) {
}
// Modify the paste itself
paste := ctx.UserValue("_paste").(*shared.Paste)
paste := ctx.UserValue("_paste").(*paste.Paste)
if payload.Content != nil {
paste.Content = *payload.Content
}
@ -227,7 +226,7 @@ func endpointModifyPaste(ctx *fasthttp.RequestCtx) {
// endpointDeletePaste handles the 'DELETE /v2/pastes/{id}' endpoint
func endpointDeletePaste(ctx *fasthttp.RequestCtx) {
paste := ctx.UserValue("_paste").(*shared.Paste)
paste := ctx.UserValue("_paste").(*paste.Paste)
if err := storage.Current.Delete(paste.ID); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
@ -254,7 +253,7 @@ func endpointReportPaste(ctx *fasthttp.RequestCtx) {
}
request := &report.ReportRequest{
Paste: ctx.UserValue("_paste").(*shared.Paste).ID,
Paste: ctx.UserValue("_paste").(*paste.Paste).ID,
Reason: payload.Reason,
}
response, err := report.SendReport(request)