mirror of https://gitlab.com/ngerakines/tavern.git
initial commit
This commit is contained in:
parent
d7c0ed1ac0
commit
50cfa8e395
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 Nick Gerakines
|
||||
Copyright (c) 2020 Nick Gerakines
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
107
README.md
107
README.md
|
@ -1 +1,106 @@
|
|||
# tavern
|
||||
# tavern
|
||||
|
||||
A minimalistic Activity Pub server. Think [mastodon](https://joinmastodon.org/), but smaller and with fewer features.
|
||||
|
||||
**_This project is in active development and should not be used in production._**
|
||||
|
||||
# Build
|
||||
|
||||
$ go build -o tavern main.go
|
||||
|
||||
# Commands
|
||||
|
||||
```
|
||||
NAME:
|
||||
tavern - The tavern application.
|
||||
|
||||
USAGE:
|
||||
main [global options] command [command options] [arguments...]
|
||||
|
||||
COMMANDS:
|
||||
init Initialize the server
|
||||
server Run the web server.
|
||||
help, h Shows a list of commands or help for one command
|
||||
|
||||
GLOBAL OPTIONS:
|
||||
--help, -h show help (default: false)
|
||||
--version, -v print the version (default: false)
|
||||
|
||||
COPYRIGHT:
|
||||
(c) 2020 Nick Gerakines
|
||||
```
|
||||
|
||||
## Init
|
||||
|
||||
```
|
||||
NAME:
|
||||
main init - Initialize the server
|
||||
|
||||
USAGE:
|
||||
main init [command options] [arguments...]
|
||||
|
||||
OPTIONS:
|
||||
--environment value Set the environment the application is running in. (default: "development") [$ENVIRONMENT]
|
||||
--listen value Configure the server to listen to this interface. (default: "0.0.0.0:8000") [$LISTEN]
|
||||
--domain value Set the website domain. (default: "tavern.ngrok.io") [$DOMAIN]
|
||||
--database value Select the database to connect to. (default: "host=localhost port=5432 user=postgres dbname=tavern password=password sslmode=disable") [$DATABASE]
|
||||
--translations value The path translations are located (default: "translations") [$TRANSLATIONS]
|
||||
--admin-email value The email address of the admin user
|
||||
--admin-password value The password of the admin user
|
||||
--admin-locale value The locale of the admin user (default: "en")
|
||||
--admin-name value The name of the admin user
|
||||
--admin-displayname value The display name of the admin user
|
||||
--admin-about value The 'about me' of the admin user
|
||||
--help, -h show help (default: false)
|
||||
```
|
||||
|
||||
## Server
|
||||
|
||||
```
|
||||
NAME:
|
||||
main server - Run the web server.
|
||||
|
||||
USAGE:
|
||||
main server [command options] [arguments...]
|
||||
|
||||
OPTIONS:
|
||||
--environment value Set the environment the application is running in. (default: "development") [$ENVIRONMENT]
|
||||
--listen value Configure the server to listen to this interface. (default: "0.0.0.0:8000") [$LISTEN]
|
||||
--domain value Set the website domain. (default: "tavern.ngrok.io") [$DOMAIN]
|
||||
--database value Select the database to connect to. (default: "host=localhost port=5432 user=postgres dbname=tavern password=password sslmode=disable") [$DATABASE]
|
||||
--translations value The path translations are located (default: "translations") [$TRANSLATIONS]
|
||||
--secret value Set the server secret [$SECRET]
|
||||
--admin-name value The name of the admin user (default: "nick") [$ADMIN_NAME]
|
||||
--help, -h show help (default: false)
|
||||
```
|
||||
|
||||
# Roadmap
|
||||
|
||||
* [x] User authentication
|
||||
* [x] Follow remote user
|
||||
* [x] User inbox - Follower "Accept" activities
|
||||
* [ ] Display feed - Note
|
||||
* [ ] Display feed - Announce
|
||||
* [ ] Announce from feed
|
||||
* [ ] Reply to note with conversation and context
|
||||
* [ ] Activity endpoint
|
||||
* [ ] User inbox - "Undo Follow" activities
|
||||
* [ ] Delete activities
|
||||
* [ ] Server inbox
|
||||
* [ ] Create note non-english
|
||||
* [ ] Server stats
|
||||
* [ ] Markdown support in notes
|
||||
* [ ] User thumbnails/avatars
|
||||
* [ ] Tags support in notes
|
||||
* [ ] Mentions support in notes
|
||||
* [ ] Actor outbox
|
||||
* [ ] Public timeline API
|
||||
* [ ] 2FA
|
||||
* [ ] Unfollow
|
||||
* [ ] Activity de-duplication
|
||||
* [ ] Sentry integration
|
||||
* [ ] Email notifications
|
||||
* [ ] Pushover notifications
|
||||
* [ ] HTML User profile pages
|
||||
* [ ] HTML activity pages
|
||||
* [ ] Direct message support
|
|
@ -0,0 +1,56 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var ProductionEnvironment = "production"
|
||||
|
||||
var SecretFlag = cli.StringFlag{
|
||||
Name: "secret",
|
||||
Usage: "Set the server secret",
|
||||
EnvVars: []string{"SECRET"},
|
||||
}
|
||||
|
||||
func Logger(c *cli.Context) (*zap.Logger, error) {
|
||||
if c.String("environment") == ProductionEnvironment {
|
||||
return zap.NewProduction()
|
||||
}
|
||||
return zap.NewDevelopment()
|
||||
}
|
||||
|
||||
func ListenAddress(cliCtx *cli.Context) string {
|
||||
if listen := cliCtx.String("listen"); len(listen) > 0 {
|
||||
return listen
|
||||
}
|
||||
return ":8080"
|
||||
}
|
||||
|
||||
func Secret(cliCtx *cli.Context) ([]byte, error) {
|
||||
secret := cliCtx.String("secret")
|
||||
secret = strings.TrimSpace(secret)
|
||||
if len(secret) > 0 {
|
||||
decoded, err := hex.DecodeString(secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return first32Bytes(decoded), nil
|
||||
}
|
||||
id, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return first32Bytes(id.Bytes()), nil
|
||||
}
|
||||
|
||||
func first32Bytes(in []byte) []byte {
|
||||
if len(in) > 32 {
|
||||
return in[0:32]
|
||||
}
|
||||
return in
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var EnvironmentFlag = cli.StringFlag{Name: "environment", Usage: "Set the environment the application is running in.", EnvVars: []string{"ENVIRONMENT",}, Value: "development",}
|
||||
|
||||
var ListenFlag = cli.StringFlag{Name: "listen", Usage: "Configure the server to listen to this interface.", EnvVars: []string{"LISTEN",}, Value: "0.0.0.0:8000",}
|
||||
|
||||
var DomainFlag = cli.StringFlag{Name: "domain", Usage: "Set the website domain.", Value: "tavern.ngrok.io", EnvVars: []string{"DOMAIN",},}
|
|
@ -0,0 +1,31 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/go-playground/locales/en"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var TranslationsFlag = cli.StringFlag{
|
||||
Name: "translations",
|
||||
Usage: "The path translations are located",
|
||||
EnvVars: []string{"TRANSLATIONS"},
|
||||
Value: "translations",
|
||||
}
|
||||
|
||||
func Trans(cliCtx *cli.Context) (*ut.UniversalTranslator, error) {
|
||||
english := en.New()
|
||||
utrans := ut.New(english, english)
|
||||
|
||||
err := utrans.Import(ut.FormatJSON, cliCtx.String("translations"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = utrans.VerifyTranslations()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return utrans, nil
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type SentryConfig struct {
|
||||
Enabled bool
|
||||
Key string
|
||||
}
|
||||
|
||||
var EnableSentryFlag = cli.BoolFlag{
|
||||
Name: "enable-sentry",
|
||||
Usage: "Enable sentry integration",
|
||||
EnvVars: []string{"ENABLE_SENTRY"},
|
||||
Value: false,
|
||||
}
|
||||
|
||||
var SentryFlag = cli.StringFlag{
|
||||
Name: "sentry",
|
||||
Usage: "Configure the sentry key to use.",
|
||||
EnvVars: []string{"SENTRY_KEY",},
|
||||
Value: "",
|
||||
}
|
||||
|
||||
func NewSentryConfig(cliCtx *cli.Context) (SentryConfig, error) {
|
||||
cfg := SentryConfig{
|
||||
Enabled: cliCtx.Bool("enable-sentry"),
|
||||
Key: cliCtx.String("sentry"),
|
||||
}
|
||||
if cfg.Enabled && len(cfg.Key) == 0 {
|
||||
return cfg, fmt.Errorf("invalid sentry config: sentry enabled with empty key")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var DatabaseFlag = cli.StringFlag{
|
||||
Name: "database",
|
||||
Usage: "Select the database to connect to.",
|
||||
Value: "host=localhost port=5432 user=postgres dbname=tavern password=password sslmode=disable",
|
||||
EnvVars: []string{"DATABASE",},
|
||||
}
|
||||
|
||||
func DB(c *cli.Context, logger *zap.Logger) (*sql.DB, func(), error) {
|
||||
dbConnInfo := c.String("database")
|
||||
db, err := sql.Open("postgres", dbConnInfo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
dbClose := func() {
|
||||
if closeErr := db.Close(); closeErr != nil {
|
||||
logger.Error("error closing database connection", zap.Error(closeErr))
|
||||
}
|
||||
}
|
||||
if err := db.Ping(); err != nil {
|
||||
dbClose()
|
||||
return nil, nil, err
|
||||
}
|
||||
return db, dbClose, nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,840 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by herr at 2020-02-25 12:24:47.40365038 -0500 EST m=+0.012451131
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"errors"
|
||||
)
|
||||
|
||||
|
||||
func TestNotFound (t *testing.T) {
|
||||
err1 := NewNotFoundError(nil)
|
||||
{
|
||||
err1, ok := err1.(NotFoundError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on NotFound: %T is not NotFoundError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAV" {
|
||||
t.Errorf("Assertion failed on NotFound: %s != TAV", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 1 {
|
||||
t.Errorf("Assertion failed on NotFound: %d != 1", err1.Code())
|
||||
}
|
||||
if err1.Description() != "Not found." {
|
||||
t.Errorf("Assertion failed on NotFound: %s != Not found.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewNotFoundError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(NotFoundError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on NotFound: %T is not NotFoundError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 1 {
|
||||
t.Errorf("Assertion failed on NotFound: %d != 1", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on NotFound: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on NotFound: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, NotFoundError{}) {
|
||||
t.Errorf("Assertion failed on NotFound: NotFoundError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, NotFoundError{}) {
|
||||
t.Errorf("Assertion failed on NotFound: NotFoundError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptFailed (t *testing.T) {
|
||||
err1 := NewEncryptFailedError(nil)
|
||||
{
|
||||
err1, ok := err1.(EncryptFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on EncryptFailed: %T is not EncryptFailedError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAV" {
|
||||
t.Errorf("Assertion failed on EncryptFailed: %s != TAV", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 2 {
|
||||
t.Errorf("Assertion failed on EncryptFailed: %d != 2", err1.Code())
|
||||
}
|
||||
if err1.Description() != "Encrypting data failed." {
|
||||
t.Errorf("Assertion failed on EncryptFailed: %s != Encrypting data failed.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewEncryptFailedError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(EncryptFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on EncryptFailed: %T is not EncryptFailedError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 2 {
|
||||
t.Errorf("Assertion failed on EncryptFailed: %d != 2", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on EncryptFailed: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on EncryptFailed: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, EncryptFailedError{}) {
|
||||
t.Errorf("Assertion failed on EncryptFailed: EncryptFailedError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, EncryptFailedError{}) {
|
||||
t.Errorf("Assertion failed on EncryptFailed: EncryptFailedError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptFailed (t *testing.T) {
|
||||
err1 := NewDecryptFailedError(nil)
|
||||
{
|
||||
err1, ok := err1.(DecryptFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on DecryptFailed: %T is not DecryptFailedError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAV" {
|
||||
t.Errorf("Assertion failed on DecryptFailed: %s != TAV", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 3 {
|
||||
t.Errorf("Assertion failed on DecryptFailed: %d != 3", err1.Code())
|
||||
}
|
||||
if err1.Description() != "Decrypting data failed." {
|
||||
t.Errorf("Assertion failed on DecryptFailed: %s != Decrypting data failed.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewDecryptFailedError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(DecryptFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on DecryptFailed: %T is not DecryptFailedError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 3 {
|
||||
t.Errorf("Assertion failed on DecryptFailed: %d != 3", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on DecryptFailed: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on DecryptFailed: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, DecryptFailedError{}) {
|
||||
t.Errorf("Assertion failed on DecryptFailed: DecryptFailedError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, DecryptFailedError{}) {
|
||||
t.Errorf("Assertion failed on DecryptFailed: DecryptFailedError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryFailed (t *testing.T) {
|
||||
err1 := NewQueryFailedError(nil)
|
||||
{
|
||||
err1, ok := err1.(QueryFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on QueryFailed: %T is not QueryFailedError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVDAT" {
|
||||
t.Errorf("Assertion failed on QueryFailed: %s != TAVDAT", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 1 {
|
||||
t.Errorf("Assertion failed on QueryFailed: %d != 1", err1.Code())
|
||||
}
|
||||
if err1.Description() != "The query operation failed." {
|
||||
t.Errorf("Assertion failed on QueryFailed: %s != The query operation failed.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewQueryFailedError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(QueryFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on QueryFailed: %T is not QueryFailedError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 1 {
|
||||
t.Errorf("Assertion failed on QueryFailed: %d != 1", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on QueryFailed: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on QueryFailed: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, QueryFailedError{}) {
|
||||
t.Errorf("Assertion failed on QueryFailed: QueryFailedError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, QueryFailedError{}) {
|
||||
t.Errorf("Assertion failed on QueryFailed: QueryFailedError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseTransactionFailed (t *testing.T) {
|
||||
err1 := NewDatabaseTransactionFailedError(nil)
|
||||
{
|
||||
err1, ok := err1.(DatabaseTransactionFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on DatabaseTransactionFailed: %T is not DatabaseTransactionFailedError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVDAT" {
|
||||
t.Errorf("Assertion failed on DatabaseTransactionFailed: %s != TAVDAT", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 2 {
|
||||
t.Errorf("Assertion failed on DatabaseTransactionFailed: %d != 2", err1.Code())
|
||||
}
|
||||
if err1.Description() != "The database transaction failed." {
|
||||
t.Errorf("Assertion failed on DatabaseTransactionFailed: %s != The database transaction failed.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewDatabaseTransactionFailedError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(DatabaseTransactionFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on DatabaseTransactionFailed: %T is not DatabaseTransactionFailedError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 2 {
|
||||
t.Errorf("Assertion failed on DatabaseTransactionFailed: %d != 2", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on DatabaseTransactionFailed: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on DatabaseTransactionFailed: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, DatabaseTransactionFailedError{}) {
|
||||
t.Errorf("Assertion failed on DatabaseTransactionFailed: DatabaseTransactionFailedError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, DatabaseTransactionFailedError{}) {
|
||||
t.Errorf("Assertion failed on DatabaseTransactionFailed: DatabaseTransactionFailedError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertQueryFailed (t *testing.T) {
|
||||
err1 := NewInsertQueryFailedError(nil)
|
||||
{
|
||||
err1, ok := err1.(InsertQueryFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on InsertQueryFailed: %T is not InsertQueryFailedError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVDAT" {
|
||||
t.Errorf("Assertion failed on InsertQueryFailed: %s != TAVDAT", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 3 {
|
||||
t.Errorf("Assertion failed on InsertQueryFailed: %d != 3", err1.Code())
|
||||
}
|
||||
if err1.Description() != "The insert query failed" {
|
||||
t.Errorf("Assertion failed on InsertQueryFailed: %s != The insert query failed", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewInsertQueryFailedError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(InsertQueryFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on InsertQueryFailed: %T is not InsertQueryFailedError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 3 {
|
||||
t.Errorf("Assertion failed on InsertQueryFailed: %d != 3", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on InsertQueryFailed: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on InsertQueryFailed: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, InsertQueryFailedError{}) {
|
||||
t.Errorf("Assertion failed on InsertQueryFailed: InsertQueryFailedError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, InsertQueryFailedError{}) {
|
||||
t.Errorf("Assertion failed on InsertQueryFailed: InsertQueryFailedError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectQueryFailed (t *testing.T) {
|
||||
err1 := NewSelectQueryFailedError(nil)
|
||||
{
|
||||
err1, ok := err1.(SelectQueryFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on SelectQueryFailed: %T is not SelectQueryFailedError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVDAT" {
|
||||
t.Errorf("Assertion failed on SelectQueryFailed: %s != TAVDAT", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 4 {
|
||||
t.Errorf("Assertion failed on SelectQueryFailed: %d != 4", err1.Code())
|
||||
}
|
||||
if err1.Description() != "The select query failed" {
|
||||
t.Errorf("Assertion failed on SelectQueryFailed: %s != The select query failed", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewSelectQueryFailedError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(SelectQueryFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on SelectQueryFailed: %T is not SelectQueryFailedError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 4 {
|
||||
t.Errorf("Assertion failed on SelectQueryFailed: %d != 4", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on SelectQueryFailed: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on SelectQueryFailed: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, SelectQueryFailedError{}) {
|
||||
t.Errorf("Assertion failed on SelectQueryFailed: SelectQueryFailedError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, SelectQueryFailedError{}) {
|
||||
t.Errorf("Assertion failed on SelectQueryFailed: SelectQueryFailedError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateQueryFailed (t *testing.T) {
|
||||
err1 := NewUpdateQueryFailedError(nil)
|
||||
{
|
||||
err1, ok := err1.(UpdateQueryFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on UpdateQueryFailed: %T is not UpdateQueryFailedError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVDAT" {
|
||||
t.Errorf("Assertion failed on UpdateQueryFailed: %s != TAVDAT", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 5 {
|
||||
t.Errorf("Assertion failed on UpdateQueryFailed: %d != 5", err1.Code())
|
||||
}
|
||||
if err1.Description() != "The update query failed" {
|
||||
t.Errorf("Assertion failed on UpdateQueryFailed: %s != The update query failed", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewUpdateQueryFailedError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(UpdateQueryFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on UpdateQueryFailed: %T is not UpdateQueryFailedError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 5 {
|
||||
t.Errorf("Assertion failed on UpdateQueryFailed: %d != 5", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on UpdateQueryFailed: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on UpdateQueryFailed: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, UpdateQueryFailedError{}) {
|
||||
t.Errorf("Assertion failed on UpdateQueryFailed: UpdateQueryFailedError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, UpdateQueryFailedError{}) {
|
||||
t.Errorf("Assertion failed on UpdateQueryFailed: UpdateQueryFailedError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidUserID (t *testing.T) {
|
||||
err1 := NewInvalidUserIDError(nil)
|
||||
{
|
||||
err1, ok := err1.(InvalidUserIDError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on InvalidUserID: %T is not InvalidUserIDError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVDAT" {
|
||||
t.Errorf("Assertion failed on InvalidUserID: %s != TAVDAT", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 6 {
|
||||
t.Errorf("Assertion failed on InvalidUserID: %d != 6", err1.Code())
|
||||
}
|
||||
if err1.Description() != "The user ID is invalid." {
|
||||
t.Errorf("Assertion failed on InvalidUserID: %s != The user ID is invalid.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewInvalidUserIDError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(InvalidUserIDError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on InvalidUserID: %T is not InvalidUserIDError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 6 {
|
||||
t.Errorf("Assertion failed on InvalidUserID: %d != 6", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on InvalidUserID: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on InvalidUserID: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, InvalidUserIDError{}) {
|
||||
t.Errorf("Assertion failed on InvalidUserID: InvalidUserIDError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, InvalidUserIDError{}) {
|
||||
t.Errorf("Assertion failed on InvalidUserID: InvalidUserIDError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserNotFound (t *testing.T) {
|
||||
err1 := NewUserNotFoundError(nil)
|
||||
{
|
||||
err1, ok := err1.(UserNotFoundError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on UserNotFound: %T is not UserNotFoundError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVDAT" {
|
||||
t.Errorf("Assertion failed on UserNotFound: %s != TAVDAT", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 7 {
|
||||
t.Errorf("Assertion failed on UserNotFound: %d != 7", err1.Code())
|
||||
}
|
||||
if err1.Description() != "The user was not found." {
|
||||
t.Errorf("Assertion failed on UserNotFound: %s != The user was not found.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewUserNotFoundError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(UserNotFoundError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on UserNotFound: %T is not UserNotFoundError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 7 {
|
||||
t.Errorf("Assertion failed on UserNotFound: %d != 7", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on UserNotFound: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on UserNotFound: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, UserNotFoundError{}) {
|
||||
t.Errorf("Assertion failed on UserNotFound: UserNotFoundError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, UserNotFoundError{}) {
|
||||
t.Errorf("Assertion failed on UserNotFound: UserNotFoundError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateUserFailed (t *testing.T) {
|
||||
err1 := NewCreateUserFailedError(nil)
|
||||
{
|
||||
err1, ok := err1.(CreateUserFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on CreateUserFailed: %T is not CreateUserFailedError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVDAT" {
|
||||
t.Errorf("Assertion failed on CreateUserFailed: %s != TAVDAT", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 8 {
|
||||
t.Errorf("Assertion failed on CreateUserFailed: %d != 8", err1.Code())
|
||||
}
|
||||
if err1.Description() != "The user was not able to be created." {
|
||||
t.Errorf("Assertion failed on CreateUserFailed: %s != The user was not able to be created.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewCreateUserFailedError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(CreateUserFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on CreateUserFailed: %T is not CreateUserFailedError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 8 {
|
||||
t.Errorf("Assertion failed on CreateUserFailed: %d != 8", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on CreateUserFailed: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on CreateUserFailed: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, CreateUserFailedError{}) {
|
||||
t.Errorf("Assertion failed on CreateUserFailed: CreateUserFailedError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, CreateUserFailedError{}) {
|
||||
t.Errorf("Assertion failed on CreateUserFailed: CreateUserFailedError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserQueryFailed (t *testing.T) {
|
||||
err1 := NewUserQueryFailedError(nil)
|
||||
{
|
||||
err1, ok := err1.(UserQueryFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on UserQueryFailed: %T is not UserQueryFailedError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVDAT" {
|
||||
t.Errorf("Assertion failed on UserQueryFailed: %s != TAVDAT", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 9 {
|
||||
t.Errorf("Assertion failed on UserQueryFailed: %d != 9", err1.Code())
|
||||
}
|
||||
if err1.Description() != "The user query failed." {
|
||||
t.Errorf("Assertion failed on UserQueryFailed: %s != The user query failed.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewUserQueryFailedError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(UserQueryFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on UserQueryFailed: %T is not UserQueryFailedError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 9 {
|
||||
t.Errorf("Assertion failed on UserQueryFailed: %d != 9", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on UserQueryFailed: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on UserQueryFailed: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, UserQueryFailedError{}) {
|
||||
t.Errorf("Assertion failed on UserQueryFailed: UserQueryFailedError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, UserQueryFailedError{}) {
|
||||
t.Errorf("Assertion failed on UserQueryFailed: UserQueryFailedError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUserFailed (t *testing.T) {
|
||||
err1 := NewUpdateUserFailedError(nil)
|
||||
{
|
||||
err1, ok := err1.(UpdateUserFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on UpdateUserFailed: %T is not UpdateUserFailedError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVDAT" {
|
||||
t.Errorf("Assertion failed on UpdateUserFailed: %s != TAVDAT", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 10 {
|
||||
t.Errorf("Assertion failed on UpdateUserFailed: %d != 10", err1.Code())
|
||||
}
|
||||
if err1.Description() != "The user was not able to be updated." {
|
||||
t.Errorf("Assertion failed on UpdateUserFailed: %s != The user was not able to be updated.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewUpdateUserFailedError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(UpdateUserFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on UpdateUserFailed: %T is not UpdateUserFailedError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 10 {
|
||||
t.Errorf("Assertion failed on UpdateUserFailed: %d != 10", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on UpdateUserFailed: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on UpdateUserFailed: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, UpdateUserFailedError{}) {
|
||||
t.Errorf("Assertion failed on UpdateUserFailed: UpdateUserFailedError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, UpdateUserFailedError{}) {
|
||||
t.Errorf("Assertion failed on UpdateUserFailed: UpdateUserFailedError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserSessionNotFound (t *testing.T) {
|
||||
err1 := NewUserSessionNotFoundError(nil)
|
||||
{
|
||||
err1, ok := err1.(UserSessionNotFoundError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on UserSessionNotFound: %T is not UserSessionNotFoundError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVDAT" {
|
||||
t.Errorf("Assertion failed on UserSessionNotFound: %s != TAVDAT", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 11 {
|
||||
t.Errorf("Assertion failed on UserSessionNotFound: %d != 11", err1.Code())
|
||||
}
|
||||
if err1.Description() != "The user session was not found." {
|
||||
t.Errorf("Assertion failed on UserSessionNotFound: %s != The user session was not found.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewUserSessionNotFoundError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(UserSessionNotFoundError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on UserSessionNotFound: %T is not UserSessionNotFoundError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 11 {
|
||||
t.Errorf("Assertion failed on UserSessionNotFound: %d != 11", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on UserSessionNotFound: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on UserSessionNotFound: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, UserSessionNotFoundError{}) {
|
||||
t.Errorf("Assertion failed on UserSessionNotFound: UserSessionNotFoundError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, UserSessionNotFoundError{}) {
|
||||
t.Errorf("Assertion failed on UserSessionNotFound: UserSessionNotFoundError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidEmailVerification (t *testing.T) {
|
||||
err1 := NewInvalidEmailVerificationError(nil)
|
||||
{
|
||||
err1, ok := err1.(InvalidEmailVerificationError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on InvalidEmailVerification: %T is not InvalidEmailVerificationError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVWEB" {
|
||||
t.Errorf("Assertion failed on InvalidEmailVerification: %s != TAVWEB", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 1 {
|
||||
t.Errorf("Assertion failed on InvalidEmailVerification: %d != 1", err1.Code())
|
||||
}
|
||||
if err1.Description() != "Invalid verification." {
|
||||
t.Errorf("Assertion failed on InvalidEmailVerification: %s != Invalid verification.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewInvalidEmailVerificationError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(InvalidEmailVerificationError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on InvalidEmailVerification: %T is not InvalidEmailVerificationError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 1 {
|
||||
t.Errorf("Assertion failed on InvalidEmailVerification: %d != 1", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on InvalidEmailVerification: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on InvalidEmailVerification: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, InvalidEmailVerificationError{}) {
|
||||
t.Errorf("Assertion failed on InvalidEmailVerification: InvalidEmailVerificationError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, InvalidEmailVerificationError{}) {
|
||||
t.Errorf("Assertion failed on InvalidEmailVerification: InvalidEmailVerificationError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestCannotSaveSession (t *testing.T) {
|
||||
err1 := NewCannotSaveSessionError(nil)
|
||||
{
|
||||
err1, ok := err1.(CannotSaveSessionError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on CannotSaveSession: %T is not CannotSaveSessionError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVWEB" {
|
||||
t.Errorf("Assertion failed on CannotSaveSession: %s != TAVWEB", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 2 {
|
||||
t.Errorf("Assertion failed on CannotSaveSession: %d != 2", err1.Code())
|
||||
}
|
||||
if err1.Description() != "Cannot save session." {
|
||||
t.Errorf("Assertion failed on CannotSaveSession: %s != Cannot save session.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewCannotSaveSessionError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(CannotSaveSessionError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on CannotSaveSession: %T is not CannotSaveSessionError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 2 {
|
||||
t.Errorf("Assertion failed on CannotSaveSession: %d != 2", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on CannotSaveSession: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on CannotSaveSession: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, CannotSaveSessionError{}) {
|
||||
t.Errorf("Assertion failed on CannotSaveSession: CannotSaveSessionError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, CannotSaveSessionError{}) {
|
||||
t.Errorf("Assertion failed on CannotSaveSession: CannotSaveSessionError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidCaptcha (t *testing.T) {
|
||||
err1 := NewInvalidCaptchaError(nil)
|
||||
{
|
||||
err1, ok := err1.(InvalidCaptchaError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on InvalidCaptcha: %T is not InvalidCaptchaError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVWEB" {
|
||||
t.Errorf("Assertion failed on InvalidCaptcha: %s != TAVWEB", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 3 {
|
||||
t.Errorf("Assertion failed on InvalidCaptcha: %d != 3", err1.Code())
|
||||
}
|
||||
if err1.Description() != "Invalid captcha." {
|
||||
t.Errorf("Assertion failed on InvalidCaptcha: %s != Invalid captcha.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewInvalidCaptchaError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(InvalidCaptchaError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on InvalidCaptcha: %T is not InvalidCaptchaError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 3 {
|
||||
t.Errorf("Assertion failed on InvalidCaptcha: %d != 3", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on InvalidCaptcha: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on InvalidCaptcha: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, InvalidCaptchaError{}) {
|
||||
t.Errorf("Assertion failed on InvalidCaptcha: InvalidCaptchaError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, InvalidCaptchaError{}) {
|
||||
t.Errorf("Assertion failed on InvalidCaptcha: InvalidCaptchaError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailSendFailed (t *testing.T) {
|
||||
err1 := NewEmailSendFailedError(nil)
|
||||
{
|
||||
err1, ok := err1.(EmailSendFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on EmailSendFailed: %T is not EmailSendFailedError", err1)
|
||||
}
|
||||
if err1.Prefix() != "TAVWEB" {
|
||||
t.Errorf("Assertion failed on EmailSendFailed: %s != TAVWEB", err1.Prefix())
|
||||
}
|
||||
if err1.Code() != 4 {
|
||||
t.Errorf("Assertion failed on EmailSendFailed: %d != 4", err1.Code())
|
||||
}
|
||||
if err1.Description() != "Unable to send email." {
|
||||
t.Errorf("Assertion failed on EmailSendFailed: %s != Unable to send email.", err1.Description())
|
||||
}
|
||||
}
|
||||
|
||||
errNotFound := fmt.Errorf("not found")
|
||||
errThingNotFound := fmt.Errorf("thing: %w", errNotFound)
|
||||
err2 := NewEmailSendFailedError(errThingNotFound)
|
||||
{
|
||||
err2, ok := err2.(EmailSendFailedError)
|
||||
if !ok {
|
||||
t.Errorf("Assertion failed on EmailSendFailed: %T is not EmailSendFailedError", err2)
|
||||
}
|
||||
errNestErr2 := fmt.Errorf("oh snap: %w", err2)
|
||||
if err2.Code() != 4 {
|
||||
t.Errorf("Assertion failed on EmailSendFailed: %d != 4", err2.Code())
|
||||
}
|
||||
if !errors.Is(err2, errNotFound) {
|
||||
t.Errorf("Assertion failed on EmailSendFailed: errNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, errThingNotFound) {
|
||||
t.Errorf("Assertion failed on EmailSendFailed: errThingNotFound not unwrapped correctly")
|
||||
}
|
||||
if !errors.Is(err2, EmailSendFailedError{}) {
|
||||
t.Errorf("Assertion failed on EmailSendFailed: EmailSendFailedError{} not identified correctly")
|
||||
}
|
||||
if !errors.Is(errNestErr2, EmailSendFailedError{}) {
|
||||
t.Errorf("Assertion failed on EmailSendFailed: EmailSendFailedError{} not identified correctly")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package errors
|
||||
|
||||
//go:generate go run github.com/sslhound/herr -package errors -source tav.csv -source tavdat.csv -source tavweb.csv -out errors_generated.go -test-out errors_generated_test.go -locale-out ../translations/en/errors_generated_test.json
|
|
@ -0,0 +1,3 @@
|
|||
1,TAV ,NotFound ,Not found.
|
||||
2,TAV,EncryptFailed,Encrypting data failed.
|
||||
3,TAV ,DecryptFailed ,Decrypting data failed.
|
|
|
@ -0,0 +1,14 @@
|
|||
1 ,TAVDAT,QueryFailed ,The query operation failed.
|
||||
2 ,TAVDAT,DatabaseTransactionFailed ,The database transaction failed.
|
||||
|
||||
3, TAVDAT,InsertQueryFailed,The insert query failed
|
||||
4, TAVDAT,SelectQueryFailed,The select query failed
|
||||
5, TAVDAT,UpdateQueryFailed,The update query failed
|
||||
|
||||
6 ,TAVDAT,InvalidUserID ,The user ID is invalid.
|
||||
7 ,TAVDAT,UserNotFound ,The user was not found.
|
||||
8 ,TAVDAT,CreateUserFailed ,The user was not able to be created.
|
||||
9 ,TAVDAT,UserQueryFailed ,The user query failed.
|
||||
10 ,TAVDAT,UpdateUserFailed ,The user was not able to be updated.
|
||||
|
||||
11,TAVDAT,UserSessionNotFound ,The user session was not found.
|
|
|
@ -0,0 +1,4 @@
|
|||
1 ,TAVWEB,InvalidEmailVerification ,Invalid verification.
|
||||
2 ,TAVWEB,CannotSaveSession ,Cannot save session.
|
||||
3 ,TAVWEB,InvalidCaptcha ,Invalid captcha.
|
||||
4 ,TAVWEB,EmailSendFailed ,Unable to send email.
|
|
|
@ -0,0 +1,12 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
New = errors.New
|
||||
Is = errors.Is
|
||||
As = errors.As
|
||||
Unwrap = errors.Unwrap
|
||||
)
|
|
@ -0,0 +1,53 @@
|
|||
package fed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ngerakines/tavern/g"
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
type ActivityClient struct {
|
||||
HTTPClient HTTPClient
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
func (client ActivityClient) Get(location string) (string, storage.Payload, error) {
|
||||
client.Logger.Debug("Sending activity request", zap.String("url", location))
|
||||
return ldJsonGet(client.HTTPClient, location)
|
||||
}
|
||||
|
||||
func ldJsonGet(client HTTPClient, location string) (string, storage.Payload, error) {
|
||||
request, err := http.NewRequest("GET", location, nil)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
request.Header.Add("Accept", "application/ld+json")
|
||||
request.Header.Set("User-Agent", g.UserAgent())
|
||||
|
||||
resp, err := client.Do(request)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1*1024*1024))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
p, err := storage.PayloadFromBytes(body)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return string(body), p, nil
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
package fed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
type ActorClient struct {
|
||||
HTTPClient HTTPClient
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
func (client ActorClient) Get(location string) (string, storage.Payload, error) {
|
||||
client.Logger.Debug("Sending actor request", zap.String("url", location))
|
||||
request, err := http.NewRequest("GET", location, nil)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
request.Header.Add("Accept", "application/ld+json")
|
||||
|
||||
resp, err := client.HTTPClient.Do(request)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1*1024*1024))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
p, err := storage.PayloadFromBytes(body)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// return storage.PayloadFromReader(io.LimitReader(resp.Body, 1*1024*1024))
|
||||
return string(body), p, nil
|
||||
}
|
||||
|
||||
func GetOrFetchActor(ctx context.Context, store storage.Storage, logger *zap.Logger, httpClient HTTPClient, hint string) (storage.Actor, error) {
|
||||
var actorURL string
|
||||
if strings.HasPrefix(hint, "https://") {
|
||||
actorURL = hint
|
||||
}
|
||||
|
||||
count, err := store.RowCount(ctx, `SELECT COUNT(*) FROM actors WHERE aliases @> ARRAY[$1]::varchar[]`, hint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return store.GetActorByAlias(ctx, hint)
|
||||
}
|
||||
|
||||
if len(actorURL) == 0 {
|
||||
wfc := WebFingerClient{
|
||||
HTTPClient: httpClient,
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
wfp, err := wfc.Fetch(hint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actorURL, err = ActorIDFromWebFingerPayload(wfp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Debug("parsed actor id from webfinger payload", zap.String("actor", actorURL))
|
||||
}
|
||||
|
||||
ac := ActorClient{
|
||||
HTTPClient: httpClient,
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
actorBody, actorPayload, err := ac.Get(actorURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyID, keyPEM, err := storage.KeyFromActor(actorPayload)
|
||||
|
||||
actorRowID := storage.NewV4()
|
||||
keyRowID := storage.NewV4()
|
||||
|
||||
actorID, ok := storage.JSONString(actorPayload, "id")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no id found for actor")
|
||||
}
|
||||
|
||||
err = store.CreateActor(ctx, actorRowID, keyRowID, actorID, actorBody, keyID, keyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, a := range []string{actorID, actorURL, hint} {
|
||||
if err = store.RecordActorAlias(ctx, actorID, a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if endpoints, ok := storage.JSONMap(actorPayload, "endpoints"); ok {
|
||||
sharedInbox, ok := storage.JSONString(endpoints, "sharedInbox")
|
||||
if ok {
|
||||
peerID := storage.NewV4()
|
||||
err = store.CreatePeer(ctx, peerID, sharedInbox)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return store.GetActor(ctx, actorID)
|
||||
}
|
||||
|
||||
func ActorsFromActivity(activity storage.Payload) []string {
|
||||
var actors []string
|
||||
|
||||
actor, ok := storage.JSONString(activity, "actor")
|
||||
if ok {
|
||||
actors = append(actors, actor)
|
||||
}
|
||||
|
||||
obj, ok := storage.JSONMap(activity, "object")
|
||||
if ok {
|
||||
attributedTo, ok := storage.JSONString(obj, "attributedTo")
|
||||
if ok {
|
||||
actors = append(actors, attributedTo)
|
||||
}
|
||||
|
||||
tag, ok := storage.JSONMapList(obj, "tag")
|
||||
if ok {
|
||||
for _, i := range tag {
|
||||
tagType, hasTagType := storage.JSONString(i, "type")
|
||||
href, hasHref := storage.JSONString(i, "href")
|
||||
if hasTagType && hasHref && tagType == "Mention" {
|
||||
actors = append(actors, href)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return actors
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package fed
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/yukimochi/httpsig"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ngerakines/tavern/g"
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
type hasInbox interface {
|
||||
GetInbox() string
|
||||
}
|
||||
|
||||
type fakeInbox string
|
||||
|
||||
func (f fakeInbox) GetInbox() string {
|
||||
return string(f)
|
||||
}
|
||||
|
||||
func (client ActorClient) Broadcast(ctx context.Context, store storage.Storage, localActor storage.LocalActor, payload []byte) error {
|
||||
followers, err := store.FollowerInboxes(ctx, localActor.User.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, follower := range followers {
|
||||
client.Logger.Info("sending payload to follower", zap.String("inbox", follower))
|
||||
err = client.SendToInbox(ctx, localActor, fakeInbox(follower), payload)
|
||||
if err != nil {
|
||||
client.Logger.Error("unable to send payload to inbox", zap.String("inbox", follower), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client ActorClient) SendToInbox(ctx context.Context, localActor storage.LocalActor, actor hasInbox, payload []byte) error {
|
||||
sigConfig := []httpsig.Algorithm{httpsig.RSA_SHA256}
|
||||
headersToSign := []string{httpsig.RequestTarget, "date"}
|
||||
signer, _, err := httpsig.NewSigner(sigConfig, headersToSign, httpsig.Signature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, "POST", actor.GetInbox(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Add("content-type", `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`)
|
||||
request.Header.Add("date", time.Now().UTC().Format(http.TimeFormat))
|
||||
request.Header.Set("User-Agent", g.UserAgent())
|
||||
|
||||
privateKey, err := localActor.User.GetPrivateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = signer.SignRequest(privateKey, localActor.GetKeyID(), request); err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := client.HTTPClient.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lr := io.LimitReader(resp.Body, 500000)
|
||||
|
||||
data, err := ioutil.ReadAll(lr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("unexpected response %d: %s", resp.StatusCode, string(data))
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package fed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
type HTTPClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type WebFingerClient struct {
|
||||
HTTPClient HTTPClient
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
func (client WebFingerClient) Fetch(location string) (storage.Payload, error) {
|
||||
destination, err := BuildWebFingerURL(location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.Logger.Debug("Sending webfinger request", zap.String("url", destination))
|
||||
_, payload, err := ldJsonGet(client.HTTPClient, destination)
|
||||
return payload, err
|
||||
}
|
||||
|
||||
func BuildWebFingerURL(location string) (string, error) {
|
||||
if strings.HasPrefix(location, "@") {
|
||||
location = strings.TrimPrefix(location, "@")
|
||||
parts := strings.Split(location, "@")
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("invalid actor location: %s", location)
|
||||
}
|
||||
q := url.QueryEscape(location)
|
||||
return fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", parts[1], q), nil
|
||||
}
|
||||
|
||||
if strings.Index(location, "@") != -1 {
|
||||
parts := strings.Split(strings.TrimPrefix(location, "@"), "@")
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("invalid actor location: %s", location)
|
||||
}
|
||||
q := url.QueryEscape(location)
|
||||
return fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", parts[1], q), nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(location)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
q := url.QueryEscape(location)
|
||||
return fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", u.Host, q), nil
|
||||
}
|
||||
|
||||
func ActorIDFromWebFingerPayload(wfp storage.Payload) (string, error) {
|
||||
if wfp == nil {
|
||||
return "", fmt.Errorf("unable to get actor href from webfinger content: wfp nil")
|
||||
}
|
||||
links, ok := wfp["links"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unable to get actor href from webfinger content")
|
||||
}
|
||||
objLinks, ok := links.([]interface{})
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unable to get actor href from webfinger content: links not array")
|
||||
}
|
||||
if len(objLinks) < 1 {
|
||||
return "", fmt.Errorf("unable to get actor href from webfinger content: links empty")
|
||||
}
|
||||
first := objLinks[0]
|
||||
firstMap, ok := first.(map[string]interface{})
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unable to get actor href from webfinger content: first link not map")
|
||||
}
|
||||
href, ok := firstMap["href"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unable to get actor href from webfinger content: href present")
|
||||
}
|
||||
hrefStr, ok := href.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unable to get actor href from webfinger content: href not string")
|
||||
}
|
||||
return hrefStr, nil
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package g
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var ReleaseCode string
|
||||
var GitCommit string
|
||||
var BuildTime string
|
||||
|
||||
func Version() string {
|
||||
return fmt.Sprintf("%s-%s", ReleaseCode, GitCommit)
|
||||
}
|
||||
|
||||
func UserAgent() string {
|
||||
return fmt.Sprintf("tavern/%s-%s (+https://github.com/ngerakines/tavern)", ReleaseCode, GitCommit)
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
module github.com/ngerakines/tavern
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/foolin/goview v0.2.0
|
||||
github.com/getsentry/sentry-go v0.4.0
|
||||
github.com/gin-contrib/sessions v0.0.3
|
||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
|
||||
github.com/gin-contrib/zap v0.0.0-20191128031730-d12829f8f61b
|
||||
github.com/gin-gonic/gin v1.5.0
|
||||
github.com/go-playground/locales v0.12.1
|
||||
github.com/go-playground/universal-translator v0.16.0
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/kr/pretty v0.1.0
|
||||
github.com/lib/pq v1.3.0
|
||||
github.com/oklog/run v1.0.0
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/urfave/cli/v2 v2.1.1
|
||||
github.com/yukimochi/httpsig v0.1.3
|
||||
go.uber.org/zap v1.13.0
|
||||
golang.org/x/crypto v0.0.0-20200221170553-0f24fbd83dfb
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.7 // indirect
|
||||
)
|
|
@ -0,0 +1,292 @@
|
|||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw=
|
||||
github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w=
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||
github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM=
|
||||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA=
|
||||
github.com/foolin/goview v0.2.0 h1:ovIVJmi2Lui+0obJ+NUhf2TN9mmV4hx5FA99oX1yH6U=
|
||||
github.com/foolin/goview v0.2.0/go.mod h1:hmQ++dSxBx8Uod3ZIpH+g2l1NSg/1Yc3kfNuLfY0EoI=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/getsentry/sentry-go v0.4.0 h1:WqRI2/7EiALbdG9qGB47c0Aks1tdznG5DZd6GSQ1y/8=
|
||||
github.com/getsentry/sentry-go v0.4.0/go.mod h1:xkGcb82SipKQloDNa5b7hTV4VdEyc2bhwd1/UczP52k=
|
||||
github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI=
|
||||
github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 h1:xLG16iua01X7Gzms9045s2Y2niNpvSY/Zb1oBwgNYZY=
|
||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ=
|
||||
github.com/gin-contrib/zap v0.0.0-20191128031730-d12829f8f61b h1:RbSrAGE5n0H4cQwsxWLWN8EFMoY5rdJQ83hZnHZ5plA=
|
||||
github.com/gin-contrib/zap v0.0.0-20191128031730-d12829f8f61b/go.mod h1:vJJndZ8f44gsTHQrDPIB4YOZzwOwiEIdE0mMrZLOogk=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc=
|
||||
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
|
||||
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
|
||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
|
||||
github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI=
|
||||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
|
||||
github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
|
||||
github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk=
|
||||
github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U=
|
||||
github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw=
|
||||
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0=
|
||||
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
||||
github.com/labstack/echo/v4 v4.1.5/go.mod h1:3LbYC6VkwmUnmLPZ8WFdHdQHG77e9GQbjyhWdb1QvC4=
|
||||
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
|
||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg=
|
||||
github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ=
|
||||
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM=
|
||||
github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
|
||||
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/yukimochi/httpsig v0.1.3 h1:rosV0EcuZfmi7DvlW1QaxORYCEesm28Sa+lTyKh8+OA=
|
||||
github.com/yukimochi/httpsig v0.1.3/go.mod h1:JY9Gq5vuFbLhrPZjoc+j+UiYu9/ohq5Ou5b4oZHc8Cw=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200221170553-0f24fbd83dfb h1:Bg7BRk6M/6/zfhJrglNmi/oiI2jwKuETZ3vrdYr1qFk=
|
||||
golang.org/x/crypto v0.0.0-20200221170553-0f24fbd83dfb/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
|
@ -0,0 +1,12 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func DefaultHTTPClient() HTTPClient {
|
||||
return &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/oklog/run"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type HTTPClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type Job interface {
|
||||
Run(context.Context) error
|
||||
Shutdown(context.Context) error
|
||||
}
|
||||
|
||||
func RunWorker(group *run.Group, logger *zap.Logger, j Job, parent context.Context) {
|
||||
group.Add(func() error {
|
||||
logger.Info("starting job")
|
||||
return ignoreCanceled(j.Run(parent))
|
||||
}, func(error) {
|
||||
shutdownCtx, shutdownCtxCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer shutdownCtxCancel()
|
||||
logger.Info("stopping publisher")
|
||||
err := ignoreCanceled(j.Shutdown(shutdownCtx))
|
||||
if err != nil {
|
||||
logger.Error("error stopping job", zap.Error(err))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func ignoreCanceled(err error) error {
|
||||
if err == nil || err == context.Canceled {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ngerakines/tavern/fed"
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
type webfinger struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
logger *zap.Logger
|
||||
queue storage.StringQueue
|
||||
storage storage.Storage
|
||||
httpClient HTTPClient
|
||||
}
|
||||
|
||||
func NewWebFingerWorker(logger *zap.Logger, queue storage.StringQueue, storage storage.Storage) Job {
|
||||
return &webfinger{
|
||||
logger: logger,
|
||||
queue: queue,
|
||||
storage: storage,
|
||||
httpClient: DefaultHTTPClient(),
|
||||
}
|
||||
}
|
||||
|
||||
func (job *webfinger) Run(parent context.Context) error {
|
||||
job.ctx, job.cancel = context.WithCancel(parent)
|
||||
defer job.cancel()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
err := job.work()
|
||||
if err != nil {
|
||||
job.logger.Error("error processing work", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
case <-job.ctx.Done():
|
||||
return ignoreCanceled(job.ctx.Err())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (job *webfinger) Shutdown(parent context.Context) error {
|
||||
job.cancel()
|
||||
select {
|
||||
case <-parent.Done():
|
||||
return parent.Err()
|
||||
case <-job.ctx.Done():
|
||||
return job.ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (job *webfinger) work() error {
|
||||
work, err := job.queue.Take()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(work) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var actorID string
|
||||
|
||||
if strings.HasPrefix(work, "https://") {
|
||||
actorID = work
|
||||
}
|
||||
|
||||
if len(actorID) == 0 {
|
||||
wfc := fed.WebFingerClient{
|
||||
HTTPClient: job.httpClient,
|
||||
Logger: job.logger,
|
||||
}
|
||||
|
||||
wfp, err := wfc.Fetch(work)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actorID, err = fed.ActorIDFromWebFingerPayload(wfp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
job.logger.Debug("parsed actor id from webfinger payload", zap.String("actor", actorID))
|
||||
}
|
||||
|
||||
count, err := job.storage.RowCount(job.ctx, `SELECT COUNT(*) FROM actors WHERE actor_id = $1`, actorID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ac := fed.ActorClient{
|
||||
HTTPClient: job.httpClient,
|
||||
Logger: job.logger,
|
||||
}
|
||||
|
||||
actorBody, actorPayload, err := ac.Get(actorID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyID, keyPEM, err := storage.KeyFromActor(actorPayload)
|
||||
|
||||
actorRowID := storage.NewV4()
|
||||
keyRowID := storage.NewV4()
|
||||
|
||||
return job.storage.CreateActor(context.Background(), actorRowID, keyRowID, actorID, actorBody, keyID, keyPEM)
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
type mockStorage struct {
|
||||
GetActorFunc func(context.Context, string) (storage.Actor, error)
|
||||
CreateActorFunc func(context.Context, uuid.UUID, uuid.UUID, string, string, string, string) error
|
||||
}
|
||||
|
||||
func (m mockStorage) RowCount(ctx context.Context, query string, args ...interface{}) (int, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m mockStorage) AuthenticateUser(ctx context.Context, email string, password []byte) (uuid.UUID, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m mockStorage) CreateUser(ctx context.Context, userID uuid.UUID, email, locale, name, displayName, about, publicKey, privateKey string, password []byte) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m mockStorage) GetUser(ctx context.Context, userID uuid.UUID) (*storage.User, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m mockStorage) GetUserByName(ctx context.Context, name string) (*storage.User, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m mockStorage) GetUserBySession(ctx context.Context, session sessions.Session) (*storage.User, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m mockStorage) UpdateUserLastAuth(ctx context.Context, userID uuid.UUID) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m mockStorage) GetUserFollowers(ctx context.Context, userID uuid.UUID, limit, offset int) ([]string, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m mockStorage) GetUserFollowing(ctx context.Context, userID uuid.UUID, limit, offset int) ([]string, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m mockStorage) RecordActivity(ctx context.Context, activityID uuid.UUID, payload string) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m mockStorage) GetActor(ctx context.Context, id string) (storage.Actor, error) {
|
||||
if m.GetActorFunc != nil {
|
||||
return m.GetActorFunc(ctx, id)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m mockStorage) CreateActor(a context.Context, b uuid.UUID, c uuid.UUID, d string, e string, f string, g string) error {
|
||||
if m.CreateActorFunc != nil {
|
||||
return m.CreateActorFunc(a, b, c, d, e, f, g)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ storage.Storage = mockStorage{}
|
||||
|
||||
func TestWebfinger_Run(t *testing.T) {
|
||||
// if skip, _ := strconv.ParseBool(os.Getenv("TEST_INTEGRATION")); !skip {
|
||||
// t.Skip("skipping integration test")
|
||||
// return
|
||||
// }
|
||||
|
||||
logger, _ := zap.NewDevelopment()
|
||||
|
||||
q := storage.NewStringQueue()
|
||||
// assert.NoError(t, q.Add("https://mastodon.social/users/ngerakines"))
|
||||
// assert.NoError(t, q.Add("@ngerakines@mastodon.social"))
|
||||
assert.NoError(t, q.Add("ngerakines@mastodon.social"))
|
||||
|
||||
j := &webfinger{
|
||||
logger: logger,
|
||||
queue: q,
|
||||
storage: mockStorage{
|
||||
CreateActorFunc: func(ctx context.Context, u uuid.UUID, u2 uuid.UUID, actorID string, s2 string, keyID string, s4 string) error {
|
||||
assert.Equal(t, "https://mastodon.social/@ngerakines", actorID)
|
||||
assert.Equal(t, "https://mastodon.social/users/ngerakines#main-key", keyID)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
httpClient: DefaultHTTPClient(),
|
||||
}
|
||||
assert.NoError(t, j.work())
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/ngerakines/tavern/g"
|
||||
"github.com/ngerakines/tavern/start"
|
||||
"github.com/ngerakines/tavern/web"
|
||||
)
|
||||
|
||||
var ReleaseCode string
|
||||
var GitCommit string
|
||||
var BuildTime string
|
||||
|
||||
func main() {
|
||||
compiledAt, err := time.Parse(time.RFC822Z, BuildTime)
|
||||
if err != nil {
|
||||
compiledAt = time.Now()
|
||||
}
|
||||
if ReleaseCode == "" {
|
||||
ReleaseCode = "na"
|
||||
}
|
||||
if GitCommit == "" {
|
||||
GitCommit = "na"
|
||||
}
|
||||
|
||||
g.BuildTime = BuildTime
|
||||
g.ReleaseCode = ReleaseCode
|
||||
g.GitCommit = GitCommit
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "tavern"
|
||||
app.Usage = "The tavern application."
|
||||
app.Version = fmt.Sprintf("%s-%s", ReleaseCode, GitCommit)
|
||||
app.Compiled = compiledAt
|
||||
app.Copyright = "(c) 2020 Nick Gerakines"
|
||||
|
||||
app.Commands = []*cli.Command{
|
||||
&start.Command,
|
||||
&web.Command,
|
||||
}
|
||||
|
||||
sort.Sort(cli.FlagsByName(app.Flags))
|
||||
sort.Sort(cli.CommandsByName(app.Commands))
|
||||
|
||||
err = app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,8 @@
|
|||
/* TEAM */
|
||||
Author: Nick Gerakines
|
||||
Twitter: @ngerakines
|
||||
ActivityPub: ngerakines@mastodon.social
|
||||
Location: Dayton, Ohio, USA
|
||||
|
||||
/* SITE */
|
||||
Language: English
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /secrets
|
|
@ -0,0 +1,160 @@
|
|||
package start
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/ngerakines/tavern/config"
|
||||
"github.com/ngerakines/tavern/g"
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
var Command = cli.Command{
|
||||
Name: "init",
|
||||
Usage: "Initialize the server",
|
||||
Flags: []cli.Flag{
|
||||
&config.EnvironmentFlag,
|
||||
&config.ListenFlag,
|
||||
&config.DomainFlag,
|
||||
&config.DatabaseFlag,
|
||||
&config.TranslationsFlag,
|
||||
&cli.StringFlag{
|
||||
Name: "admin-email",
|
||||
Usage: "The email address of the admin user",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "admin-password",
|
||||
Usage: "The password of the admin user",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "admin-locale",
|
||||
Usage: "The locale of the admin user",
|
||||
Value: "en",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "admin-name",
|
||||
Usage: "The name of the admin user",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "admin-displayname",
|
||||
Usage: "The display name of the admin user",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "admin-about",
|
||||
Usage: "The 'about me' of the admin user",
|
||||
},
|
||||
},
|
||||
Action: serverCommandAction,
|
||||
}
|
||||
|
||||
func serverCommandAction(cliCtx *cli.Context) error {
|
||||
logger, err := config.Logger(cliCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
domain := cliCtx.String("domain")
|
||||
siteBase := fmt.Sprintf("https://%s", domain)
|
||||
|
||||
logger.Info("Starting",
|
||||
zap.String("command", cliCtx.Command.Name),
|
||||
zap.String("GOOS", runtime.GOOS),
|
||||
zap.String("site", siteBase),
|
||||
zap.String("env", cliCtx.String("environment")))
|
||||
|
||||
sentryConfig, err := config.NewSentryConfig(cliCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sentryConfig.Enabled {
|
||||
err = sentry.Init(sentry.ClientOptions{
|
||||
Dsn: sentryConfig.Key,
|
||||
Environment: cliCtx.String("environment"),
|
||||
Release: fmt.Sprintf("%s-%s", g.ReleaseCode, g.GitCommit),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetTags(map[string]string{"container": "server"})
|
||||
})
|
||||
defer sentry.Recover()
|
||||
}
|
||||
|
||||
db, dbClose, err := config.DB(cliCtx, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbClose()
|
||||
|
||||
s := storage.DefaultStorage(db)
|
||||
|
||||
userID := storage.NewV4()
|
||||
|
||||
name := cliCtx.String("admin-name")
|
||||
displayName := cliCtx.String("admin-displayname")
|
||||
if len(displayName) == 0 {
|
||||
displayName = name
|
||||
}
|
||||
about := cliCtx.String("admin-about")
|
||||
if len(about) == 0 {
|
||||
about = "Just a user"
|
||||
}
|
||||
|
||||
encPassword, err := bcrypt.GenerateFromPassword([]byte(cliCtx.String("admin-password")), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privateKeyBytes := x509.MarshalPKCS1PrivateKey(key)
|
||||
var privateKeyBuffer bytes.Buffer
|
||||
if err := pem.Encode(&privateKeyBuffer, &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: privateKeyBytes,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
privateKey := string(privateKeyBuffer.Bytes())
|
||||
|
||||
publicKeyBytes, err := x509.MarshalPKIXPublicKey(key.Public())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var publicKeyBuffer bytes.Buffer
|
||||
if err = pem.Encode(&publicKeyBuffer, &pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: publicKeyBytes,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
publicKey := string(publicKeyBuffer.Bytes())
|
||||
|
||||
err = s.CreateUser(context.Background(), userID, cliCtx.String("admin-email"), cliCtx.String("admin-locale"), name, displayName, about, publicKey, privateKey, encPassword)
|
||||
if err != nil {
|
||||
logger.Error("unable to create user", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/spaolacci/murmur3"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
)
|
||||
|
||||
type ActivityStorage interface {
|
||||
RecordActivity(ctx context.Context, rowID uuid.UUID, activityID, payload string) error
|
||||
GetActivitiesByIDs(ctx context.Context, activityIDs []uuid.UUID) ([]Activity, error)
|
||||
RecordUserActivity(ctx context.Context, rowID, userID, activityID uuid.UUID) error
|
||||
}
|
||||
|
||||
type Activity struct {
|
||||
ID uuid.UUID
|
||||
Payload string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (s pgStorage) RecordActivity(ctx context.Context, rowID uuid.UUID, activityID, payload string) error {
|
||||
checksum := ActivityChecksum([]byte(payload))
|
||||
_, err := s.db.ExecContext(ctx, "INSERT INTO activities (id, activity_id, checksum, payload, created_at) VALUES ($1, $2, $3, $4, $5)", rowID, activityID, checksum, payload, s.now())
|
||||
return errors.WrapInsertQueryFailedError(err)
|
||||
}
|
||||
|
||||
func (s pgStorage) RecordUserActivity(ctx context.Context, rowID, userID, activityID uuid.UUID) error {
|
||||
_, err := s.db.ExecContext(ctx, "INSERT INTO user_activities (id, user_id, activity_id, created_at) VALUES ($1, $2, $3, $4)", rowID, userID, activityID, s.now())
|
||||
return errors.WrapInsertQueryFailedError(err)
|
||||
}
|
||||
|
||||
func (s pgStorage) GetActivitiesByIDs(ctx context.Context, activityIDs []uuid.UUID) ([]Activity, error) {
|
||||
if len(activityIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
params := make([]string, len(activityIDs))
|
||||
args := make([]interface{}, len(activityIDs))
|
||||
for i, id := range activityIDs {
|
||||
params[i] = fmt.Sprintf("$%d", i+1)
|
||||
args[i] = id
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("SELECT id, payload, created_at FROM activities WHERE id IN (%s) ORDER BY created_at ASC", strings.Join(params, ", "))
|
||||
|
||||
return s.activitiesQuery(ctx, query, args...)
|
||||
}
|
||||
|
||||
func (s pgStorage) activitiesQuery(ctx context.Context, query string, args ...interface{}) ([]Activity, error) {
|
||||
var activities []Activity
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.NewSelectQueryFailedError(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var activity Activity
|
||||
if err := rows.Scan(&activity.ID, &activity.Payload, &activity.CreatedAt); err != nil {
|
||||
return nil, errors.NewSelectQueryFailedError(err)
|
||||
}
|
||||
activities = append(activities, activity)
|
||||
}
|
||||
|
||||
return activities, nil
|
||||
}
|
||||
|
||||
func ActivityChecksum(raw []byte) string {
|
||||
h := murmur3.New64()
|
||||
h.Write(raw)
|
||||
return strconv.FormatUint(h.Sum64(), 10)
|
||||
}
|
||||
|
||||
func ValidateActivity(activity Payload) (string, error) {
|
||||
id, ok := JSONString(activity, "id")
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing activity attribute: id")
|
||||
}
|
||||
return id, nil
|
||||
}
|
|
@ -0,0 +1,280 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
)
|
||||
|
||||
type ActorStorage interface {
|
||||
GetActor(ctx context.Context, id string) (Actor, error)
|
||||
GetActorByAlias(ctx context.Context, alias string) (Actor, error)
|
||||
CreateActor(context.Context, uuid.UUID, uuid.UUID, string, string, string, string) error
|
||||
GetKey(ctx context.Context, keyID string) (*Key, error)
|
||||
RecordActorAlias(ctx context.Context, actorID, alias string) error
|
||||
}
|
||||
|
||||
type Actor interface {
|
||||
GetID() string
|
||||
GetInbox() string
|
||||
GetPublicKey() string
|
||||
GetKeyID() string
|
||||
GetDecodedPublicKey() (*rsa.PublicKey, error)
|
||||
}
|
||||
|
||||
type ActorID string
|
||||
|
||||
type actor struct {
|
||||
ID uuid.UUID
|
||||
ActorID string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
||||
Inbox string
|
||||
PublicKey string
|
||||
KeyID string
|
||||
|
||||
payload string
|
||||
}
|
||||
|
||||
type LocalActor struct {
|
||||
User *User
|
||||
ActorID ActorID
|
||||
}
|
||||
|
||||
type Key struct {
|
||||
ID uuid.UUID
|
||||
KeyID string
|
||||
PublicKey string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
var _ Actor = &actor{}
|
||||
var _ Actor = &LocalActor{}
|
||||
|
||||
func (a *actor) GetDecodedPublicKey() (*rsa.PublicKey, error) {
|
||||
block, _ := pem.Decode([]byte(a.PublicKey))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("invalid RSA PEM")
|
||||
}
|
||||
|
||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid RSA PEM")
|
||||
}
|
||||
rsaPublicKey, ok := pub.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid RSA PEM")
|
||||
}
|
||||
return rsaPublicKey, nil
|
||||
}
|
||||
|
||||
func (a *actor) GetID() string {
|
||||
return a.ActorID
|
||||
}
|
||||
|
||||
func (a *actor) GetInbox() string {
|
||||
return a.Inbox
|
||||
}
|
||||
|
||||
func (a *actor) GetKeyID() string {
|
||||
return a.KeyID
|
||||
}
|
||||
|
||||
func (a *actor) GetPublicKey() string {
|
||||
return a.PublicKey
|
||||
}
|
||||
|
||||
func (a *actor) init() error {
|
||||
p, err := PayloadFromString(a.payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.Inbox, err = InboxFromActor(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.KeyID, a.PublicKey, err = KeyFromActor(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l LocalActor) GetID() string {
|
||||
return string(l.ActorID)
|
||||
}
|
||||
|
||||
func (l LocalActor) GetInbox() string {
|
||||
return l.ActorID.Inbox()
|
||||
}
|
||||
|
||||
func (l LocalActor) GetPublicKey() string {
|
||||
return l.User.PublicKey
|
||||
}
|
||||
|
||||
func (l LocalActor) GetKeyID() string {
|
||||
return l.ActorID.MainKey()
|
||||
}
|
||||
|
||||
func (l LocalActor) GetDecodedPublicKey() (*rsa.PublicKey, error) {
|
||||
return l.GetDecodedPublicKey()
|
||||
}
|
||||
|
||||
func (k *Key) GetDecodedPublicKey() (*rsa.PublicKey, error) {
|
||||
block, _ := pem.Decode([]byte(k.PublicKey))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("invalid RSA PEM")
|
||||
}
|
||||
|
||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid RSA PEM")
|
||||
}
|
||||
rsaPublicKey, ok := pub.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid RSA PEM")
|
||||
}
|
||||
return rsaPublicKey, nil
|
||||
}
|
||||
|
||||
func (s pgStorage) GetActor(ctx context.Context, id string) (Actor, error) {
|
||||
a := &actor{}
|
||||
err := s.db.
|
||||
QueryRowContext(ctx, `SELECT id, actor_id, payload, created_at, updated_at from actors WHERE actor_id = $1`, id).
|
||||
Scan(&a.ID,
|
||||
&a.ActorID,
|
||||
&a.payload,
|
||||
&a.CreatedAt,
|
||||
&a.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = a.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (s pgStorage) GetActorByAlias(ctx context.Context, alias string) (Actor, error) {
|
||||
a := &actor{}
|
||||
err := s.db.
|
||||
QueryRowContext(ctx, `SELECT id, actor_id, payload, created_at, updated_at from actors WHERE aliases @> ARRAY[$1]::varchar[]`, alias).
|
||||
Scan(&a.ID,
|
||||
&a.ActorID,
|
||||
&a.payload,
|
||||
&a.CreatedAt,
|
||||
&a.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = a.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (s pgStorage) GetKey(ctx context.Context, keyID string) (*Key, error) {
|
||||
key := &Key{}
|
||||
err := s.db.
|
||||
QueryRowContext(ctx, `SELECT id, key_id, public_key, created_at from keys WHERE key_id = $1`, keyID).
|
||||
Scan(&key.ID,
|
||||
&key.KeyID,
|
||||
&key.PublicKey,
|
||||
&key.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (s pgStorage) CreateActor(ctx context.Context, actorRowID, keyRowID uuid.UUID, actorID, payload, keyID, pem string) error {
|
||||
now := s.now()
|
||||
return runTransactionWithOptions(s.db, func(tx *sql.Tx) error {
|
||||
_, err := tx.ExecContext(ctx, "INSERT INTO actors (id, actor_id, payload, created_at) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING", actorRowID, actorID, payload, now)
|
||||
if err != nil {
|
||||
return errors.NewInsertQueryFailedError(err)
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, "INSERT INTO keys (id, key_id, public_key, created_at) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING", keyRowID, keyID, pem, now)
|
||||
return errors.WrapInsertQueryFailedError(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s pgStorage) RecordActorAlias(ctx context.Context, actorID, alias string) error {
|
||||
now := s.now()
|
||||
_, err := s.db.ExecContext(ctx, `UPDATE actors SET aliases = array_append(aliases, $2), updated_at = $3 WHERE actor_id = $1 AND NOT(aliases @> ARRAY[$2]::varchar[])`, actorID, alias, now)
|
||||
return errors.WrapUpdateQueryFailedError(err)
|
||||
}
|
||||
|
||||
func (ID ActorID) Followers() string {
|
||||
return fmt.Sprintf("%s/followers", ID)
|
||||
}
|
||||
|
||||
func (ID ActorID) FollowersPage(page int) string {
|
||||
return fmt.Sprintf("%s/followers?page=%d", ID, page)
|
||||
}
|
||||
|
||||
func (ID ActorID) Following() string {
|
||||
return fmt.Sprintf("%s/following", ID)
|
||||
}
|
||||
|
||||
func (ID ActorID) FollowingPage(page int) string {
|
||||
return fmt.Sprintf("%s/following?page=%d", ID, page)
|
||||
}
|
||||
|
||||
func (ID ActorID) Outbox() string {
|
||||
return fmt.Sprintf("%s/outbox", ID)
|
||||
}
|
||||
|
||||
func (ID ActorID) OutboxPage(page int) string {
|
||||
return fmt.Sprintf("%s/outbox?page=%d", ID, page)
|
||||
}
|
||||
|
||||
func (ID ActorID) Inbox() string {
|
||||
return fmt.Sprintf("%s/inbox", ID)
|
||||
}
|
||||
|
||||
func (ID ActorID) MainKey() string {
|
||||
return fmt.Sprintf("%s#main-key", ID)
|
||||
}
|
||||
|
||||
func NewActorID(name, domain string) ActorID {
|
||||
return ActorID(fmt.Sprintf("https://%s/users/%s", domain, name))
|
||||
}
|
||||
|
||||
func KeyFromActor(actor Payload) (string, string, error) {
|
||||
if actor == nil {
|
||||
return "", "", fmt.Errorf("unable to get key from actor: actor nil")
|
||||
}
|
||||
publicKey, ok := JSONMap(actor, "publicKey")
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("unable to get key from actor: public key object missing")
|
||||
}
|
||||
keyID, ok := JSONString(publicKey, "id")
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("unable to get key from actor: public key object id invalid")
|
||||
}
|
||||
pem, ok := JSONString(publicKey, "publicKeyPem")
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("unable to get key from actor: public key object id invalid")
|
||||
}
|
||||
return keyID, pem, nil
|
||||
}
|
||||
|
||||
func InboxFromActor(actor Payload) (string, error) {
|
||||
inbox, ok := JSONString(actor, "inbox")
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unable to get inbox from actor: inbox field")
|
||||
}
|
||||
return inbox, nil
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
)
|
||||
|
||||
type FollowerStorage interface {
|
||||
GetUserFollowers(ctx context.Context, userID uuid.UUID, limit, offset int) ([]Relationship, error)
|
||||
GetUserFollowing(ctx context.Context, userID uuid.UUID, limit, offset int) ([]Relationship, error)
|
||||
IsFollowing(ctx context.Context, userID uuid.UUID, actor string) (bool, error)
|
||||
IsFollower(ctx context.Context, userID uuid.UUID, actor string) (bool, error)
|
||||
FollowRequest(ctx context.Context, followID, userID, requestActivityID uuid.UUID, actor string) error
|
||||
FollowAccept(ctx context.Context, userID, acceptActivityID uuid.UUID, actor string) error
|
||||
FollowerRequest(ctx context.Context, followerID, userID, requestActivityID, acceptActivityID uuid.UUID, actor string) error
|
||||
FollowerInboxes(ctx context.Context, userID uuid.UUID) ([]string, error)
|
||||
}
|
||||
|
||||
type RelationshipDirection int
|
||||
|
||||
const (
|
||||
FollowerRelationshipDirection = 0
|
||||
FollowingRelationshipDirection = 1
|
||||
)
|
||||
|
||||
type Relationship struct {
|
||||
ID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
Actor string
|
||||
CreatedAt time.Time
|
||||
Direction RelationshipDirection
|
||||
}
|
||||
|
||||
func (s pgStorage) GetUserFollowers(ctx context.Context, userID uuid.UUID, limit, offset int) ([]Relationship, error) {
|
||||
query := `SELECT id, user_id, actor, created_at, 1 FROM followers WHERE user_id = $3 ORDER BY created_at ASC LIMIT $1 OFFSET $2`
|
||||
return s.relationshipQuery(ctx, query, limit, offset, userID)
|
||||
}
|
||||
|
||||
func (s pgStorage) IsFollowing(ctx context.Context, userID uuid.UUID, actor string) (bool, error) {
|
||||
count, err := s.RowCount(ctx, `SELECT COUNT(*) FROM following WHERE user_id = $1 AND actor = $2`, userID, actor)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count == 1, nil
|
||||
}
|
||||
|
||||
func (s pgStorage) IsFollower(ctx context.Context, userID uuid.UUID, actor string) (bool, error) {
|
||||
count, err := s.RowCount(ctx, `SELECT COUNT(*) FROM followers WHERE user_id = $1 AND actor = $2`, userID, actor)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count == 1, nil
|
||||
}
|
||||
|
||||
func (s pgStorage) GetUserFollowing(ctx context.Context, userID uuid.UUID, limit, offset int) ([]Relationship, error) {
|
||||
query := `SELECT id, user_id, actor, created_at, 1 FROM following WHERE user_id = $3 ORDER BY created_at ASC LIMIT $1 OFFSET $2`
|
||||
return s.relationshipQuery(ctx, query, limit, offset, userID)
|
||||
}
|
||||
|
||||
func (s pgStorage) relationshipQuery(ctx context.Context, query string, args ...interface{}) ([]Relationship, error) {
|
||||
var rels []Relationship
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.NewSelectQueryFailedError(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var rel Relationship
|
||||
if err := rows.Scan(&rel.ID, &rel.UserID, &rel.Actor, &rel.CreatedAt, &rel.Direction); err != nil {
|
||||
return nil, errors.NewSelectQueryFailedError(err)
|
||||
}
|
||||
rels = append(rels, rel)
|
||||
}
|
||||
|
||||
return rels, nil
|
||||
}
|
||||
|
||||
func (s pgStorage) FollowRequest(ctx context.Context, followID, userID, requestActivityID uuid.UUID, actor string) error {
|
||||
now := s.now()
|
||||
_, err := s.db.ExecContext(ctx, "INSERT INTO following (id, user_id, actor, request_activity_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $5)", followID, userID, actor, requestActivityID, now)
|
||||
return errors.WrapInsertQueryFailedError(err)
|
||||
}
|
||||
|
||||
func (s pgStorage) FollowAccept(ctx context.Context, userID, acceptActivityID uuid.UUID, actor string) error {
|
||||
now := s.now()
|
||||
_, err := s.db.ExecContext(ctx, `UPDATE following SET updated_at = $1, accept_activity_id = $2 WHERE user_id = $3 AND actor = $4`, now, acceptActivityID, userID, actor)
|
||||
return errors.WrapInsertQueryFailedError(err)
|
||||
}
|
||||
|
||||
func (s pgStorage) FollowerRequest(ctx context.Context, followerID, userID, requestActivityID, acceptActivityID uuid.UUID, actor string) error {
|
||||
now := s.now()
|
||||
_, err := s.db.ExecContext(ctx, "INSERT INTO followers (id, user_id, actor, request_activity_id, accept_activity_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $6)", followerID, userID, actor, requestActivityID, acceptActivityID, now)
|
||||
return errors.WrapInsertQueryFailedError(err)
|
||||
}
|
||||
|
||||
func (s pgStorage) FollowerInboxes(ctx context.Context, userID uuid.UUID) ([]string, error) {
|
||||
query := `SELECT payload FROM actors WHERE actor_id IN (SELECT actor FROM followers WHERE user_id = $1)`
|
||||
var results []string
|
||||
rows, err := s.db.QueryContext(ctx, query, userID)
|
||||
if err != nil {
|
||||
return nil, errors.NewSelectQueryFailedError(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var payload string
|
||||
if err := rows.Scan(&payload); err != nil {
|
||||
return nil, errors.NewSelectQueryFailedError(err)
|
||||
}
|
||||
p, err := PayloadFromString(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inbox, ok := JSONString(p, "inbox")
|
||||
if ok {
|
||||
results = append(results, inbox)
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package storage
|
||||
|
||||
func JSONDeepString(document map[string]interface{}, keys ...string) (string, bool) {
|
||||
if len(keys) == 1 {
|
||||
return JSONString(document, keys[0])
|
||||
}
|
||||
if len(keys) > 1 {
|
||||
inner, ok := JSONMap(document, keys[0])
|
||||
if ok {
|
||||
return JSONDeepString(inner, keys[1:]...)
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func JSONMap(document map[string]interface{}, key string) (map[string]interface{}, bool) {
|
||||
if value, ok := document[key]; ok {
|
||||
if mapVal, isMap := value.(map[string]interface{}); isMap {
|
||||
return mapVal, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func JSONString(document map[string]interface{}, key string) (string, bool) {
|
||||
if value, ok := document[key]; ok {
|
||||
if strValue, isString := value.(string); isString {
|
||||
return strValue, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func JSONStrings(document map[string]interface{}, key string) ([]string, bool) {
|
||||
var results []string
|
||||
value, ok := document[key]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
results = append(results, v)
|
||||
case []interface{}:
|
||||
for _, el := range v {
|
||||
if strValue, isString := el.(string); isString {
|
||||
results = append(results, strValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results, false
|
||||
}
|
||||
|
||||
func JSONMapList(document map[string]interface{}, key string) ([]map[string]interface{}, bool) {
|
||||
var results []map[string]interface{}
|
||||
value, ok := document[key]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case map[string]interface{}:
|
||||
results = append(results, v)
|
||||
case []interface{}:
|
||||
for _, el := range v {
|
||||
if mapValue, isMap := el.(map[string]interface{}); isMap {
|
||||
results = append(results, mapValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results, false
|
||||
}
|
||||
|
||||
func StringsContainsString(things []string, value string) bool {
|
||||
for _, thing := range things {
|
||||
if thing == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestJSONString(t *testing.T) {
|
||||
doc := map[string]interface{}{
|
||||
"foo": 1,
|
||||
"bar": "a",
|
||||
"baz": map[string]interface{}{
|
||||
"foo": "b",
|
||||
},
|
||||
}
|
||||
{
|
||||
value, ok := JSONString(doc, "foo")
|
||||
assert.Equal(t, "", value)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
{
|
||||
value, ok := JSONString(doc, "bar")
|
||||
assert.Equal(t, "a", value)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
{
|
||||
value, ok := JSONString(doc, "baz")
|
||||
assert.Equal(t, "", value)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONDeepString(t *testing.T) {
|
||||
doc := map[string]interface{}{
|
||||
"foo": 1,
|
||||
"bar": "a",
|
||||
"baz": map[string]interface{}{
|
||||
"foo": "b",
|
||||
},
|
||||
}
|
||||
{
|
||||
value, ok := JSONDeepString(doc, "foo")
|
||||
assert.Equal(t, "", value)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
{
|
||||
value, ok := JSONDeepString(doc, "bar")
|
||||
assert.Equal(t, "a", value)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
{
|
||||
value, ok := JSONDeepString(doc, "baz", "foo")
|
||||
assert.Equal(t, "b", value)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Payload map[string]interface{}
|
||||
|
||||
func (p Payload) Write(w io.Writer) error {
|
||||
e := json.NewEncoder(w)
|
||||
e.SetEscapeHTML(false)
|
||||
return e.Encode(p)
|
||||
}
|
||||
|
||||
func (p Payload) Bytes() []byte {
|
||||
var buf bytes.Buffer
|
||||
p.Write(&buf)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func EmptyPayload() Payload {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
|
||||
func PayloadFromReader(r io.Reader) (Payload, error) {
|
||||
lr := io.LimitReader(r, 1048576)
|
||||
decoder := json.NewDecoder(lr)
|
||||
var p Payload
|
||||
if err := decoder.Decode(&p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func PayloadFromBytes(b []byte) (Payload, error) {
|
||||
return PayloadFromReader(bytes.NewReader(b))
|
||||
}
|
||||
|
||||
func PayloadFromString(s string) (Payload, error) {
|
||||
return PayloadFromReader(strings.NewReader(s))
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
)
|
||||
|
||||
type PeerStorage interface {
|
||||
CreatePeer(ctx context.Context, peerID uuid.UUID, inbox string) error
|
||||
}
|
||||
|
||||
func (s pgStorage) CreatePeer(ctx context.Context, peerID uuid.UUID, inbox string) error {
|
||||
now := s.now()
|
||||
_, err := s.db.ExecContext(ctx, "INSERT INTO keys (id, inbox, created_at) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING", peerID, inbox, now)
|
||||
return errors.WrapInsertQueryFailedError(err)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
)
|
||||
|
||||
func userIDFromSession(session sessions.Session) (uuid.UUID, error) {
|
||||
return userIDFromSessionKey(session, gin.AuthUserKey)
|
||||
}
|
||||
|
||||
func userIDFromSessionKey(session sessions.Session, key string) (uuid.UUID, error) {
|
||||
sessionData := session.Get(key)
|
||||
if sessionData == nil {
|
||||
return uuid.Nil, errors.NewUserSessionNotFoundError(nil)
|
||||
}
|
||||
userIDStr, ok := sessionData.(string)
|
||||
if !ok {
|
||||
return uuid.Nil, errors.NewInvalidUserIDError(nil)
|
||||
}
|
||||
userID, err := uuid.FromString(userIDStr)
|
||||
if !ok {
|
||||
return uuid.Nil, errors.NewInvalidUserIDError(err)
|
||||
}
|
||||
return userID, nil
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
)
|
||||
|
||||
type Storage interface {
|
||||
RowCount(ctx context.Context, query string, args ...interface{}) (int, error)
|
||||
|
||||
UserStorage
|
||||
ActorStorage
|
||||
FollowerStorage
|
||||
ActivityStorage
|
||||
PeerStorage
|
||||
ThreadStorage
|
||||
}
|
||||
|
||||
func DefaultStorage(db *sql.DB) Storage {
|
||||
return pgStorage{
|
||||
db: db,
|
||||
now: defaultNowFunc,
|
||||
}
|
||||
}
|
||||
|
||||
type pgStorage struct {
|
||||
db *sql.DB
|
||||
now nowFunc
|
||||
}
|
||||
|
||||
type transactionScopedWork func(db *sql.Tx) error
|
||||
type nowFunc func() time.Time
|
||||
|
||||
func defaultNowFunc() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
func runTransactionWithOptions(db *sql.DB, txBody transactionScopedWork) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return errors.NewDatabaseTransactionFailedError(err)
|
||||
}
|
||||
|
||||
err = txBody(tx)
|
||||
if err != nil {
|
||||
if txErr := tx.Rollback(); txErr != nil {
|
||||
return errors.NewDatabaseTransactionFailedError(txErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return errors.WrapDatabaseTransactionFailedError(tx.Commit())
|
||||
}
|
||||
|
||||
func (s pgStorage) RowCount(ctx context.Context, query string, args ...interface{}) (int, error) {
|
||||
var total int
|
||||
|
||||
err := s.db.QueryRowContext(ctx, query, args...).Scan(&total)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return total, nil
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type stringQueue struct {
|
||||
values []string
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
type StringQueue interface {
|
||||
Add(value string) error
|
||||
Take() (string, error)
|
||||
}
|
||||
|
||||
func NewStringQueue() StringQueue {
|
||||
return &stringQueue{
|
||||
values: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (q *stringQueue) Add(value string) error {
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
|
||||
q.values = append(q.values, value)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *stringQueue) Take() (string, error) {
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
|
||||
if len(q.values) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var value string
|
||||
value, q.values = q.values[0], q.values[1:]
|
||||
|
||||
return value, nil
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStringQueue(t *testing.T) {
|
||||
q := NewStringQueue()
|
||||
assert.NoError(t, q.Add("a"))
|
||||
assert.NoError(t, q.Add("b"))
|
||||
assert.NoError(t, q.Add("c"))
|
||||
|
||||
a, e1 := q.Take()
|
||||
assert.Equal(t, "a", a)
|
||||
assert.NoError(t, e1)
|
||||
|
||||
b, e2 := q.Take()
|
||||
assert.Equal(t, "b", b)
|
||||
assert.NoError(t, e2)
|
||||
|
||||
c, e3 := q.Take()
|
||||
assert.Equal(t, "c", c)
|
||||
assert.NoError(t, e3)
|
||||
|
||||
d, e4 := q.Take()
|
||||
assert.Equal(t, "", d)
|
||||
assert.NoError(t, e4)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
)
|
||||
|
||||
type ThreadStorage interface {
|
||||
PartOfThread(ctx context.Context, userID uuid.UUID, conversation string) (bool, error)
|
||||
WatchThread(ctx context.Context, userID uuid.UUID, conversation string) error
|
||||
|
||||
AddToUserFeed(ctx context.Context, userID, activityID uuid.UUID) error
|
||||
PaginateUserFeed(ctx context.Context, userID uuid.UUID, limit, offset int) ([]UserFeed, error)
|
||||
}
|
||||
|
||||
type UserFeed struct {
|
||||
ID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
ActivityID uuid.UUID
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (s pgStorage) PartOfThread(ctx context.Context, userID uuid.UUID, conversation string) (bool, error) {
|
||||
c, err := s.RowCount(ctx, `SELECT COUNT(*) FROM user_threads WHERE user_id = $1 AND thread = $2`, userID, conversation)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return c > 0, nil
|
||||
}
|
||||
|
||||
func (s pgStorage) WatchThread(ctx context.Context, userID uuid.UUID, conversation string) error {
|
||||
rowID := NewV4()
|
||||
_, err := s.db.ExecContext(ctx, "INSERT INTO user_threads (id, user_id, conversation, created_at) VALUES ($1, $2, $3, $4)", rowID, userID, conversation, s.now())
|
||||
return errors.WrapInsertQueryFailedError(err)
|
||||
}
|
||||
|
||||
func (s pgStorage) AddToUserFeed(ctx context.Context, userID, activityID uuid.UUID) error {
|
||||
rowID := NewV4()
|
||||
_, err := s.db.ExecContext(ctx, "INSERT INTO user_feed (id, user_id, activity_id, created_at) VALUES ($1, $2, $3, $4)", rowID, userID, activityID, s.now())
|
||||
return errors.WrapInsertQueryFailedError(err)
|
||||
}
|
||||
|
||||
func (s pgStorage) PaginateUserFeed(ctx context.Context, userID uuid.UUID, limit, offset int) ([]UserFeed, error) {
|
||||
query := `SELECT id, user_id, activity_id, created_at FROM user_feed WHERE user_id = $3 ORDER BY created_at ASC LIMIT $1 OFFSET $2`
|
||||
var results []UserFeed
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, limit, offset, userID)
|
||||
if err != nil {
|
||||
return nil, errors.NewSelectQueryFailedError(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var uf UserFeed
|
||||
if err := rows.Scan(&uf.ID, &uf.UserID, &uf.ActivityID, &uf.CreatedAt); err != nil {
|
||||
return nil, errors.NewSelectQueryFailedError(err)
|
||||
}
|
||||
results = append(results, uf)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gofrs/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
)
|
||||
|
||||
type UserStorage interface {
|
||||
AuthenticateUser(ctx context.Context, email string, password []byte) (uuid.UUID, error)
|
||||
CreateUser(ctx context.Context, userID uuid.UUID, email, locale, name, displayName, about, publicKey, privateKey string, password []byte) error
|
||||
GetUser(ctx context.Context, userID uuid.UUID) (*User, error)
|
||||
GetUserByName(ctx context.Context, name string) (*User, error)
|
||||
GetUserBySession(ctx context.Context, session sessions.Session) (*User, error)
|
||||
UpdateUserLastAuth(ctx context.Context, userID uuid.UUID) error
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uuid.UUID
|
||||
Email string
|
||||
Password []byte
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
LastAuthAt time.Time
|
||||
Location string
|
||||
MuteEmail bool
|
||||
Locale string
|
||||
PrivateKey string
|
||||
PublicKey string
|
||||
Name string
|
||||
DisplayName string
|
||||
About string
|
||||
AcceptFollowers bool
|
||||
}
|
||||
|
||||
func (u *User) GetID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (u *User) GetInbox() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (u *User) GetKeyID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (u *User) GetPrivateKey() (*rsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode([]byte(u.PrivateKey))
|
||||
if block == nil {
|
||||
return nil, errors.New("invalid RSA PEM")
|
||||
}
|
||||
|
||||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (u *User) GetDecodedPublicKey() (*rsa.PublicKey, error) {
|
||||
block, _ := pem.Decode([]byte(u.PublicKey))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("invalid RSA PEM")
|
||||
}
|
||||
|
||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid RSA PEM")
|
||||
}
|
||||
rsaPublicKey, ok := pub.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid RSA PEM")
|
||||
}
|
||||
return rsaPublicKey, nil
|
||||
}
|
||||
|
||||
var userFields = []string{
|
||||
"id",
|
||||
"email",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"last_auth_at",
|
||||
"location",
|
||||
"mute_email",
|
||||
"locale",
|
||||
"public_key",
|
||||
"private_key",
|
||||
"name",
|
||||
"display_name",
|
||||
"about",
|
||||
"accept_followers",
|
||||
}
|
||||
|
||||
func (s pgStorage) GetUserBySession(ctx context.Context, session sessions.Session) (*User, error) {
|
||||
userID, err := userIDFromSession(session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.GetUser(ctx, userID)
|
||||
}
|
||||
|
||||
func (s pgStorage) GetUserByName(ctx context.Context, name string) (*User, error) {
|
||||
query := fmt.Sprintf("SELECT %s FROM users WHERE name = $1", strings.Join(userFields, ", "))
|
||||
return s.GetUserWithQuery(ctx, query, name)
|
||||
}
|
||||
|
||||
func (s pgStorage) GetUser(ctx context.Context, userID uuid.UUID) (*User, error) {
|
||||
query := fmt.Sprintf("SELECT %s FROM users WHERE id = $1", strings.Join(userFields, ", "))
|
||||
return s.GetUserWithQuery(ctx, query, userID)
|
||||
}
|
||||
|
||||
func (s pgStorage) GetUserWithQuery(ctx context.Context, query string, args ...interface{}) (*User, error) {
|
||||
user := &User{}
|
||||
|
||||
err := s.db.
|
||||
QueryRowContext(ctx, query, args...).
|
||||
Scan(&user.ID,
|
||||
&user.Email,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
&user.LastAuthAt,
|
||||
&user.Location,
|
||||
&user.MuteEmail, &user.Locale,
|
||||
&user.PublicKey,
|
||||
&user.PrivateKey,
|
||||
&user.Name,
|
||||
&user.DisplayName,
|
||||
&user.About,
|
||||
&user.AcceptFollowers)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errors.NewUserNotFoundError(err)
|
||||
}
|
||||
return nil, errors.NewUserQueryFailedError(err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s pgStorage) CreateUser(ctx context.Context, userID uuid.UUID, email, locale, name, displayName, about, publicKey, privateKey string, password []byte) error {
|
||||
now := s.now()
|
||||
_, err := s.db.ExecContext(ctx, "INSERT INTO users (id, email, password, created_at, updated_at, last_auth_at, locale, public_key, private_key, name, display_name, about) VALUES ($1, $2, $3, $4, $4, $4, $5, $6, $7, $8, $9, $10)", userID, email, password, now, locale, publicKey, privateKey, name, displayName, about)
|
||||
if err != nil {
|
||||
return errors.NewCreateUserFailedError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s pgStorage) AuthenticateUser(ctx context.Context, email string, password []byte) (uuid.UUID, error) {
|
||||
var userID uuid.UUID
|
||||
var currentPassword []byte
|
||||
err := s.db.QueryRowContext(ctx, "SELECT id, password FROM users WHERE email = $1", email).Scan(&userID, ¤tPassword)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return uuid.Nil, errors.NewUserNotFoundError(err)
|
||||
}
|
||||
return uuid.Nil, errors.NewUserQueryFailedError(err)
|
||||
}
|
||||
err = bcrypt.CompareHashAndPassword(currentPassword, password)
|
||||
if err != nil {
|
||||
return uuid.Nil, errors.NewUserNotFoundError(err)
|
||||
}
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func (s pgStorage) UpdateUserLastAuth(ctx context.Context, userID uuid.UUID) error {
|
||||
now := s.now()
|
||||
_, err := s.db.ExecContext(ctx, "UPDATE users SET last_auth_at = $2, updated_at = $2 WHERE id = $1", userID, now)
|
||||
return errors.WrapUpdateUserFailedError(err)
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
func NewV4() uuid.UUID {
|
||||
u, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{{define "head"}}{{end}}
|
||||
{{define "footer_script"}}{{end}}
|
||||
{{define "content"}}
|
||||
{{ $t := .Trans }}
|
||||
{{ template "dashboard_menu" "configure" }}
|
||||
<div class="row pt-3">
|
||||
<div class="col">
|
||||
<h1>Configure</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
|
@ -0,0 +1,18 @@
|
|||
{{define "head"}}{{end}}
|
||||
{{define "footer_script"}}{{end}}
|
||||
{{define "content"}}
|
||||
{{ $t := .Trans }}
|
||||
{{ template "dashboard_menu" "dashboard" }}
|
||||
<div class="row pt-3">
|
||||
<div class="col">
|
||||
<h1>Your Dashboard</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{ range $a := .activities }}
|
||||
<div class="row pt-3">
|
||||
<div class="col">
|
||||
<pre>{{ $a.Payload }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{end}}
|
|
@ -0,0 +1,70 @@
|
|||
{{define "head"}}{{end}}
|
||||
{{define "footer_script"}}{{end}}
|
||||
{{define "content"}}
|
||||
{{ $t := .Trans }}
|
||||
{{ template "dashboard_menu" "network" }}
|
||||
<div class="row pt-3">
|
||||
<div class="col">
|
||||
<h1 class="pb-1">Follow</h1>
|
||||
<form method="POST" action="/dashboard/network/follow" id="follow">
|
||||
<div class="form-group">
|
||||
<label for="followUser">Actor</label>
|
||||
<input type="text" class="form-control" id="followUser" name="actor"
|
||||
placeholder="ngerakines@mastodon.social" required>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-dark" name="submit" value="Follow"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pt-3">
|
||||
<div class="col">
|
||||
<h1>Following</h1>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actor</th>
|
||||
<th>Since</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $f := .following }}
|
||||
<tr>
|
||||
<td>{{ $f.Actor }}</td>
|
||||
<td>{{ date $f.CreatedAt }}</td>
|
||||
<td>
|
||||
<a href="#">Unfollow</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pt-3">
|
||||
<div class="col">
|
||||
<h1>Followers</h1>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actor</th>
|
||||
<th>Since</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $f := .followers }}
|
||||
<tr>
|
||||
<td>{{ $f.Actor }}</td>
|
||||
<td>{{ date $f.CreatedAt }}</td>
|
||||
<td>
|
||||
<a href="#">Unfollow</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{end}}
|
|
@ -0,0 +1,18 @@
|
|||
{{define "head"}}{{end}}
|
||||
{{define "footer_script"}}{{end}}
|
||||
{{define "content"}}
|
||||
{{ $t := .Trans }}
|
||||
{{ template "dashboard_menu" "notes" }}
|
||||
<div class="row pt-3">
|
||||
<div class="col">
|
||||
<h1 class="pb-1">Create Note</h1>
|
||||
<form method="POST" action="/dashboard/notes/create/note" id="create_note">
|
||||
<div class="form-group">
|
||||
<label for="createNoteContent">Content</label>
|
||||
<input type="text" class="form-control" id="createNoteContent" name="content" required>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-dark" name="submit" value="Create Note"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
|
@ -0,0 +1,7 @@
|
|||
{{define "head"}}{{end}}
|
||||
{{define "footer_script"}}{{end}}
|
||||
{{define "content"}}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ .Trans.T .error }}
|
||||
</div>
|
||||
{{end}}
|
|
@ -0,0 +1,27 @@
|
|||
{{define "head"}}{{end}}
|
||||
{{define "footer_script"}}{{end}}
|
||||
{{define "content"}}
|
||||
<div class="row pt-3 pb-3">
|
||||
<div class="col">
|
||||
<h1>Please Authenticate</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pt-md-3 pt-3">
|
||||
<div class="col-sm-12 col-lg-6">
|
||||
<a id="login"></a>
|
||||
<h3>Sign-in</h3>
|
||||
<form method="POST" action="{{ url "signin" }}" id="login">
|
||||
<div class="form-group">
|
||||
<label for="loginEmail">Email</label>
|
||||
<input type="email" class="form-control" id="loginEmail" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword">Password</label>
|
||||
<input type="password" class="form-control" id="loginPassword" name="password" required>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-dark" name="submit" value="Submit"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{end}}
|
|
@ -0,0 +1,12 @@
|
|||
<div class="row border-top mt-4">
|
||||
<div class="col">
|
||||
<p class="lead">Copyright 2020 Nick Gerakines</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p class="lead text-center">
|
||||
<i class="fal fa-taco"></i>
|
||||
<i class="fal fa-taco"></i>
|
||||
<i class="fal fa-taco"></i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,38 @@
|
|||
<!doctype html>
|
||||
<html lang="{{ .Trans.Locale }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>{{.title}}</title>
|
||||
<link rel="stylesheet" href="/public/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/public/all.min.css">
|
||||
{{template "head" .}}
|
||||
</head>
|
||||
<body>
|
||||
<main role="main" class="container">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="{{ url }}">Tavern</a>
|
||||
<div class="collapse navbar-collapse" id="navbarItems">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link"
|
||||
href="{{ url "public_notes" }}">Public Notes</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
{{ template "flashes" . }}
|
||||
<div class="container">
|
||||
{{template "content" .}}
|
||||
</div>
|
||||
{{include "layouts/footer"}}
|
||||
</main>
|
||||
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
|
||||
integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="/public/bootstrap.min.js"></script>
|
||||
{{ template "footer_script" . }}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,18 @@
|
|||
{{ define "dashboard_menu" }}
|
||||
<div role="navigation" class="pt-3">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{ if eq "dashboard" . }} active{{ end }}" href="{{ url "dashboard" }}">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{ if eq "network" . }} active{{ end }}" href="{{ url "network" }}">Network</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{ if eq "notes" . }} active{{ end }}" href="{{ url "notes" }}">Notes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{ if eq "configure" . }} active{{ end }}" href="{{ url "configure" }}">Configure</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
|
@ -0,0 +1,22 @@
|
|||
{{ define "flashes" }}
|
||||
{{ $t := .Trans }}
|
||||
{{ if and .flashes .flashes.Show }}
|
||||
<div class="container pt-3 ">
|
||||
{{ range .flashes.Error }}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ range .flashes.Success }}
|
||||
<div class="alert alert-success" role="alert">
|
||||
{{ . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ range .flashes.Info }}
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
{{ . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
|
@ -0,0 +1,92 @@
|
|||
[
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVAAAAAAAB",
|
||||
"trans": "Not found."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVAAAAAAAC",
|
||||
"trans": "Encrypting data failed."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVAAAAAAAD",
|
||||
"trans": "Decrypting data failed."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVDATAAAAAAAB",
|
||||
"trans": "The query operation failed."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVDATAAAAAAAC",
|
||||
"trans": "The database transaction failed."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVDATAAAAAAAD",
|
||||
"trans": "The insert query failed"
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVDATAAAAAAAE",
|
||||
"trans": "The select query failed"
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVDATAAAAAAAF",
|
||||
"trans": "The update query failed"
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVDATAAAAAAAG",
|
||||
"trans": "The user ID is invalid."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVDATAAAAAAAH",
|
||||
"trans": "The user was not found."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVDATAAAAAAAI",
|
||||
"trans": "The user was not able to be created."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVDATAAAAAAAJ",
|
||||
"trans": "The user query failed."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVDATAAAAAAAK",
|
||||
"trans": "The user was not able to be updated."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVDATAAAAAAAL",
|
||||
"trans": "The user session was not found."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVWEBAAAAAAAB",
|
||||
"trans": "Invalid verification."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVWEBAAAAAAAC",
|
||||
"trans": "Cannot save session."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVWEBAAAAAAAD",
|
||||
"trans": "Invalid captcha."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "TAVWEBAAAAAAAE",
|
||||
"trans": "Unable to send email."
|
||||
}
|
||||
]
|
|
@ -0,0 +1,271 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/foolin/goview"
|
||||
"github.com/foolin/goview/supports/ginview"
|
||||
"github.com/getsentry/sentry-go"
|
||||
sentrygin "github.com/getsentry/sentry-go/gin"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-contrib/static"
|
||||
ginzap "github.com/gin-contrib/zap"
|
||||
"github.com/gin-gonic/gin"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"github.com/oklog/run"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ngerakines/tavern/config"
|
||||
"github.com/ngerakines/tavern/g"
|
||||
"github.com/ngerakines/tavern/job"
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
var Command = cli.Command{
|
||||
Name: "server",
|
||||
Usage: "Run the web server.",
|
||||
Flags: []cli.Flag{
|
||||
&config.EnvironmentFlag,
|
||||
&config.ListenFlag,
|
||||
&config.DomainFlag,
|
||||
&config.DatabaseFlag,
|
||||
&config.TranslationsFlag,
|
||||
&config.SecretFlag,
|
||||
&cli.StringFlag{
|
||||
Name: "admin-name",
|
||||
Usage: "The name of the admin user",
|
||||
EnvVars: []string{"ADMIN_NAME"},
|
||||
Value: "nick",
|
||||
},
|
||||
},
|
||||
Action: serverCommandAction,
|
||||
}
|
||||
|
||||
func serverCommandAction(cliCtx *cli.Context) error {
|
||||
logger, err := config.Logger(cliCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
domain := cliCtx.String("domain")
|
||||
siteBase := fmt.Sprintf("https://%s", domain)
|
||||
|
||||
logger.Info("Starting",
|
||||
zap.String("command", cliCtx.Command.Name),
|
||||
zap.String("GOOS", runtime.GOOS),
|
||||
zap.String("site", siteBase),
|
||||
zap.String("env", cliCtx.String("environment")))
|
||||
|
||||
ohioLocal, err = time.LoadLocation("America/New_York")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sentryConfig, err := config.NewSentryConfig(cliCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sentryConfig.Enabled {
|
||||
err = sentry.Init(sentry.ClientOptions{
|
||||
Dsn: sentryConfig.Key,
|
||||
Environment: cliCtx.String("environment"),
|
||||
Release: fmt.Sprintf("%s-%s", g.ReleaseCode, g.GitCommit),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetTags(map[string]string{"container": "server"})
|
||||
})
|
||||
defer sentry.Recover()
|
||||
}
|
||||
|
||||
db, dbClose, err := config.DB(cliCtx, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbClose()
|
||||
|
||||
s := storage.DefaultStorage(db)
|
||||
|
||||
utrans, err := config.Trans(cliCtx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
|
||||
r := gin.New()
|
||||
if sentryConfig.Enabled {
|
||||
r.Use(sentrygin.New(sentrygin.Options{
|
||||
Repanic: true,
|
||||
}))
|
||||
}
|
||||
|
||||
r.HTMLRender = ginview.New(goview.Config{
|
||||
Root: "templates",
|
||||
Extension: ".html",
|
||||
Master: filepath.Join("layouts", "master"),
|
||||
Partials: []string{"partials/flashes", "partials/dashboard_menu"},
|
||||
Funcs: template.FuncMap{
|
||||
"date": tmplDate,
|
||||
"odate": tmplOptionalDate,
|
||||
"time": tmplTime,
|
||||
"otime": tmplOptionalTime,
|
||||
"datetime": tmplDateTime,
|
||||
"odatetime": tmplOptionalDateTime,
|
||||
"shortUUID": tmplShortUUID,
|
||||
"short": tmplShort,
|
||||
"title": strings.Title,
|
||||
"a": tmplAnchor,
|
||||
"toHTML": tmplToHTML,
|
||||
"strong": tmplStrong,
|
||||
"url": tmplUrlGen(siteBase),
|
||||
},
|
||||
DisableCache: true,
|
||||
})
|
||||
|
||||
r.Use(func(i *gin.Context) {
|
||||
nonce := i.GetHeader("X-Request-ID")
|
||||
if nonce != "" {
|
||||
i.Set("nonce", nonce)
|
||||
}
|
||||
i.Next()
|
||||
})
|
||||
|
||||
r.Use(ginzap.Ginzap(logger, time.RFC3339, true))
|
||||
|
||||
r.GET("/health", func(i *gin.Context) {
|
||||
i.Data(200, "text/plain", []byte("OK"))
|
||||
})
|
||||
|
||||
r.Use(static.Serve("/public", static.LocalFile("./public", false)))
|
||||
|
||||
secret, err := config.Secret(cliCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
webFingerQueue := storage.NewStringQueue()
|
||||
|
||||
h := handler{
|
||||
storage: s,
|
||||
logger: logger,
|
||||
domain: domain,
|
||||
sentryConfig: sentryConfig,
|
||||
webFingerQueue: webFingerQueue,
|
||||
adminUser: cliCtx.String("admin-name"),
|
||||
}
|
||||
|
||||
configI18nMiddleware(sentryConfig, logger, utrans, domain, r)
|
||||
|
||||
root := r.Group("/")
|
||||
{
|
||||
root.GET("/.well-known/webfinger", h.webFinger)
|
||||
root.GET("/.well-known/nodeinfo", h.nodeInfo)
|
||||
root.GET("/nodeinfo/2.0", h.nodeInfoDetails)
|
||||
}
|
||||
|
||||
apiV1 := r.Group("/api/v1")
|
||||
{
|
||||
apiV1.GET("/instance", h.apiV1Instance)
|
||||
apiV1.GET("/instance/peers", h.apiV1InstancePeers)
|
||||
}
|
||||
|
||||
authenticated := r.Group("/")
|
||||
{
|
||||
configSessionMiddleware(secret, domain, authenticated)
|
||||
authenticated.GET("/", h.home)
|
||||
authenticated.POST("/signin", h.signin)
|
||||
authenticated.GET("/signout", h.signout)
|
||||
|
||||
authenticated.GET("/dashboard", h.dashboard)
|
||||
authenticated.GET("/dashboard/network", h.dashboardNetwork)
|
||||
authenticated.POST("/dashboard/network/follow", h.followActor)
|
||||
authenticated.GET("/dashboard/notes", h.dashboardNotes)
|
||||
authenticated.POST("/dashboard/notes/create/note", h.createNote)
|
||||
authenticated.GET("/configure", h.configure)
|
||||
}
|
||||
|
||||
server := r.Group("/server")
|
||||
{
|
||||
server.POST("/inbox", h.serverInbox)
|
||||
}
|
||||
|
||||
actor := r.Group("/users")
|
||||
{
|
||||
actor.GET("/:name", h.actorInfo)
|
||||
actor.POST("/:name/inbox", h.actorInbox)
|
||||
}
|
||||
|
||||
var group run.Group
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: config.ListenAddress(cliCtx),
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
parentCtx, parentCtxCancel := context.WithCancel(context.Background())
|
||||
defer parentCtxCancel()
|
||||
|
||||
group.Add(func() error {
|
||||
logger.Info("starting http service", zap.String("address", srv.Addr))
|
||||
return srv.ListenAndServe()
|
||||
}, func(error) {
|
||||
httpCancelCtx, httpCancelCtxCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer httpCancelCtxCancel()
|
||||
logger.Info("stopping http service")
|
||||
if err := srv.Shutdown(httpCancelCtx); err != nil {
|
||||
logger.Info("error stopping http service:", zap.Error(err))
|
||||
}
|
||||
})
|
||||
|
||||
webFingerJob := job.NewWebFingerWorker(logger, webFingerQueue, s)
|
||||
job.RunWorker(&group, logger, webFingerJob, parentCtx)
|
||||
|
||||
quit := make(chan os.Signal)
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
group.Add(func() error {
|
||||
logger.Info("starting signal listener")
|
||||
<-quit
|
||||
return nil
|
||||
}, func(error) {
|
||||
logger.Info("stopping signal listener")
|
||||
close(quit)
|
||||
})
|
||||
|
||||
return group.Run()
|
||||
}
|
||||
|
||||
func configI18nMiddleware(sentryCfg config.SentryConfig, logger *zap.Logger, utrans *ut.UniversalTranslator, domain string, r gin.IRouter) {
|
||||
im := i18nMiddleware{
|
||||
utrans: utrans,
|
||||
logger: logger,
|
||||
domain: domain,
|
||||
enableSentry: sentryCfg.Enabled,
|
||||
}
|
||||
r.Use(im.findLocale)
|
||||
}
|
||||
|
||||
func configSessionMiddleware(secret []byte, domain string, r gin.IRouter) {
|
||||
store := cookie.NewStore(secret)
|
||||
store.Options(sessions.Options{
|
||||
Path: "/",
|
||||
Domain: domain,
|
||||
MaxAge: 86400 * 30,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
})
|
||||
r.Use(sessions.Sessions("sess", store))
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/kr/pretty"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
flashError = "error"
|
||||
flashSuccess = "success"
|
||||
flashInfo = "_flash"
|
||||
)
|
||||
|
||||
type flashes struct {
|
||||
Success []interface{}
|
||||
Info []interface{}
|
||||
Error []interface{}
|
||||
}
|
||||
|
||||
func getFlashes(session sessions.Session) flashes {
|
||||
return flashes{
|
||||
Success: session.Flashes(flashSuccess),
|
||||
Info: session.Flashes(flashInfo),
|
||||
Error: session.Flashes(flashError),
|
||||
}
|
||||
}
|
||||
|
||||
func (f flashes) Show() bool {
|
||||
pretty.Println(f)
|
||||
return len(f.Success) > 0 || len(f.Info) > 0 || len(f.Error) > 0
|
||||
}
|
||||
|
||||
func appendFlashError(session sessions.Session, messages ...string) error {
|
||||
for _, message := range messages {
|
||||
session.AddFlash(message, flashError)
|
||||
}
|
||||
return errors.WrapCannotSaveSessionError(session.Save())
|
||||
}
|
||||
|
||||
func appendFlashSuccess(session sessions.Session, message string) error {
|
||||
session.AddFlash(message, flashSuccess)
|
||||
return errors.WrapCannotSaveSessionError(session.Save())
|
||||
}
|
||||
|
||||
func appendFlashInfo(session sessions.Session, message string) error {
|
||||
session.AddFlash(message, flashInfo)
|
||||
return errors.WrapCannotSaveSessionError(session.Save())
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ngerakines/tavern/config"
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
storage storage.Storage
|
||||
logger *zap.Logger
|
||||
domain string
|
||||
sentryConfig config.SentryConfig
|
||||
webFingerQueue storage.StringQueue
|
||||
adminUser string
|
||||
}
|
||||
|
||||
func (h handler) hardFail(ctx *gin.Context, err error, fields ...zap.Field) {
|
||||
if h.sentryConfig.Enabled {
|
||||
hub := sentry.CurrentHub().Clone()
|
||||
hub.Scope().SetRequest(sentry.Request{}.FromHTTPRequest(ctx.Request))
|
||||
hub.CaptureException(err)
|
||||
}
|
||||
|
||||
trans, transOK := ctx.Get("trans")
|
||||
if !transOK {
|
||||
panic("trans not found in context")
|
||||
}
|
||||
|
||||
fields = append(fields, zap.Error(err), zap.Strings("error_chain", errors.ErrorChain(err)))
|
||||
h.logger.Error("request hard failed", fields...)
|
||||
ctx.HTML(http.StatusInternalServerError, "error", gin.H{"error": err.Error(), "Trans": trans})
|
||||
ctx.Abort()
|
||||
}
|
||||
|
||||
func (h handler) hardFailJson(ctx *gin.Context, err error, fields ...zap.Field) {
|
||||
if h.sentryConfig.Enabled {
|
||||
hub := sentry.CurrentHub().Clone()
|
||||
hub.Scope().SetRequest(sentry.Request{}.FromHTTPRequest(ctx.Request))
|
||||
hub.CaptureException(err)
|
||||
}
|
||||
|
||||
fields = append(fields, zap.Error(err), zap.Strings("error_chain", errors.ErrorChain(err)))
|
||||
h.logger.Error("request hard failed", fields...)
|
||||
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ctx.Abort()
|
||||
}
|
||||
|
||||
func (h handler) WriteJRD(c *gin.Context, data map[string]interface{}) {
|
||||
c.Writer.Header().Set("Content-Type", "application/jrd+json")
|
||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||
c.Writer.Header().Set("Pragma", "no-cache")
|
||||
c.JSON(200, data)
|
||||
}
|
||||
|
||||
func (h handler) writeJSONLD(c *gin.Context, data map[string]interface{}) {
|
||||
c.Writer.Header().Set("Content-Type", "application/activity+json")
|
||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||
c.Writer.Header().Set("Pragma", "no-cache")
|
||||
c.JSON(200, data)
|
||||
}
|
||||
|
||||
func (h handler) writeActivityJsonWithProfile(c *gin.Context, data map[string]interface{}) {
|
||||
c.Writer.Header().Set("Content-Type", `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`)
|
||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||
c.Writer.Header().Set("Pragma", "no-cache")
|
||||
c.JSON(200, data)
|
||||
}
|
||||
|
||||
func (h handler) flashErrorOrFail(c *gin.Context, location string, err error) {
|
||||
session := sessions.Default(c)
|
||||
h.logger.Error("error", zap.Error(err))
|
||||
if err := appendFlashError(session, err.Error()); err != nil {
|
||||
h.hardFail(c, errors.NewCannotSaveSessionError(err))
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, location)
|
||||
}
|
||||
|
||||
func intParam(c *gin.Context, name string, defaultValue int) int {
|
||||
if input := c.Query(name); input != "" {
|
||||
value, err := strconv.Atoi(input)
|
||||
if err == nil && value >= 0 {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
func (h handler) actorInfo(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
|
||||
user, err := h.storage.GetUserByName(c.Request.Context(), name)
|
||||
if err != nil {
|
||||
h.logger.Error("unable to get user by name", zap.Error(err), zap.String("name", name))
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
actorID := storage.NewActorID(user.Name, h.domain)
|
||||
|
||||
h.logger.Debug("User info request received", zap.String("user", user.Name))
|
||||
|
||||
j := storage.EmptyPayload()
|
||||
j["@context"] = "https://www.w3.org/ns/activitystreams"
|
||||
j["id"] = actorID
|
||||
j["inbox"] = actorID.Inbox()
|
||||
j["outbox"] = actorID.Outbox()
|
||||
j["name"] = user.DisplayName
|
||||
j["preferredUsername"] = user.Name
|
||||
j["summary"] = ""
|
||||
j["type"] = "Person"
|
||||
j["url"] = actorID
|
||||
j["followers"] = actorID.Followers()
|
||||
j["following"] = actorID.Following()
|
||||
k := storage.EmptyPayload()
|
||||
k["id"] = actorID.MainKey()
|
||||
k["owner"] = actorID
|
||||
k["publicKeyPem"] = user.PublicKey
|
||||
j["publicKey"] = k
|
||||
|
||||
h.writeActivityJsonWithProfile(c, j)
|
||||
}
|
||||
|
||||
func (h handler) actorFollowers(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
|
||||
user, err := h.storage.GetUserByName(c.Request.Context(), name)
|
||||
if err != nil {
|
||||
h.logger.Error("unable to get user by name", zap.Error(err), zap.String("name", name))
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
total, err := h.storage.RowCount(c.Request.Context(), `SELECT COUNT(*) FROM followers WHERE user_id = $1`, user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("unable to count followers", zap.Error(err), zap.String("name", name))
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
j := storage.EmptyPayload()
|
||||
j["@context"] = "https://www.w3.org/ns/activitystreams"
|
||||
|
||||
actorID := storage.NewActorID(user.Name, h.domain)
|
||||
page := intParam(c, "page", 0)
|
||||
|
||||
if page == 0 {
|
||||
|
||||
j["id"] = actorID.Followers()
|
||||
j["type"] = "OrderedCollection"
|
||||
j["totalItems"] = total
|
||||
j["first"] = actorID.FollowersPage(1)
|
||||
|
||||
h.writeJSONLD(c, j)
|
||||
return
|
||||
}
|
||||
|
||||
offset := (page - 1) * 20
|
||||
|
||||
actors, err := h.storage.GetUserFollowers(c.Request.Context(), user.ID, 20, offset)
|
||||
if err != nil {
|
||||
h.logger.Error("unable to get followers", zap.Error(err), zap.String("name", name))
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
actorIDs := make([]string, len(actors))
|
||||
for i, actor := range actors {
|
||||
actorIDs[i] = actor.Actor
|
||||
}
|
||||
|
||||
j["id"] = actorID.FollowersPage(1)
|
||||
j["type"] = "OrderedCollectionPage"
|
||||
j["totalItems"] = total
|
||||
j["partOf"] = actorID.Followers()
|
||||
if offset < total {
|
||||
j["next"] = actorID.FollowersPage(page + 1)
|
||||
}
|
||||
if page > 1 {
|
||||
j["prev"] = actorID.FollowersPage(page - 1)
|
||||
}
|
||||
j["orderedItems"] = actorIDs
|
||||
|
||||
h.writeJSONLD(c, j)
|
||||
}
|
||||
|
||||
func (h handler) actorFollowing(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
|
||||
user, err := h.storage.GetUserByName(c.Request.Context(), name)
|
||||
if err != nil {
|
||||
h.logger.Error("unable to get user by name", zap.Error(err), zap.String("name", name))
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
total, err := h.storage.RowCount(c.Request.Context(), `SELECT COUNT(*) FROM following WHERE user_id = $1`, user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("unable to count following", zap.Error(err), zap.String("name", name))
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
j := storage.EmptyPayload()
|
||||
j["@context"] = "https://www.w3.org/ns/activitystreams"
|
||||
|
||||
actorID := storage.NewActorID(user.Name, h.domain)
|
||||
page := intParam(c, "page", 0)
|
||||
|
||||
if page == 0 {
|
||||
|
||||
j["id"] = actorID.Following()
|
||||
j["type"] = "OrderedCollection"
|
||||
j["totalItems"] = total
|
||||
j["first"] = actorID.FollowingPage(1)
|
||||
|
||||
h.writeJSONLD(c, j)
|
||||
return
|
||||
}
|
||||
|
||||
offset := (page - 1) * 20
|
||||
|
||||
actors, err := h.storage.GetUserFollowing(c.Request.Context(), user.ID, 20, offset)
|
||||
if err != nil {
|
||||
h.logger.Error("unable to get following", zap.Error(err), zap.String("name", name))
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
actorIDs := make([]string, len(actors))
|
||||
for i, actor := range actors {
|
||||
actorIDs[i] = actor.Actor
|
||||
}
|
||||
|
||||
j["id"] = actorID.FollowersPage(1)
|
||||
j["type"] = "OrderedCollectionPage"
|
||||
j["totalItems"] = total
|
||||
j["partOf"] = actorID.Following()
|
||||
if offset < total {
|
||||
j["next"] = actorID.FollowingPage(page + 1)
|
||||
}
|
||||
if page > 1 {
|
||||
j["prev"] = actorID.FollowingPage(page - 1)
|
||||
}
|
||||
j["orderedItems"] = actorIDs
|
||||
|
||||
h.writeJSONLD(c, j)
|
||||
}
|
|
@ -0,0 +1,500 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yukimochi/httpsig"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ngerakines/tavern/fed"
|
||||
"github.com/ngerakines/tavern/job"
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
func (h handler) actorInbox(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
|
||||
user, err := h.storage.GetUserByName(c.Request.Context(), name)
|
||||
if err != nil {
|
||||
h.logger.Error("unable to get user by name", zap.Error(err), zap.String("name", name))
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
defer c.Request.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(io.LimitReader(c.Request.Body, 1*1024*1024))
|
||||
if err != nil {
|
||||
h.logger.Error("unable to slurp request body into payload", zap.Error(err))
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
payload, err := storage.PayloadFromBytes(body)
|
||||
if err != nil {
|
||||
h.logger.Error("unable to marshall request body into payload", zap.Error(err))
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if skipActorInbox(payload) {
|
||||
h.logger.Debug("actor inbox can ignore message", zap.String("user", name))
|
||||
c.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(c.GetHeader("Signature"))
|
||||
fmt.Println(string(body))
|
||||
|
||||
payloadType, _ := storage.JSONString(payload, "type")
|
||||
|
||||
actor, _ := storage.JSONString(payload, "actor")
|
||||
if len(actor) > 0 {
|
||||
err = h.webFingerQueue.Add(actor)
|
||||
if err != nil {
|
||||
h.logger.Error("unable to add actor to web finger queue", zap.String("actor", actor))
|
||||
}
|
||||
}
|
||||
|
||||
switch payloadType {
|
||||
case "Ping":
|
||||
h.logger.Debug("User received ping payload", zap.String("user", name))
|
||||
h.actorInboxPing(c, user, payload)
|
||||
case "Pong":
|
||||
h.logger.Debug("User received pong payload", zap.String("user", name))
|
||||
h.actorInboxPong(c, user, payload)
|
||||
case "Follow":
|
||||
h.logger.Debug("User received follow payload", zap.String("user", name))
|
||||
h.actorInboxFollow(c, user, payload, body)
|
||||
case "Undo":
|
||||
h.logger.Debug("User received undo payload", zap.String("user", name))
|
||||
h.actorInboxUndo(c, user, payload)
|
||||
case "Update":
|
||||
h.logger.Debug("User received update payload", zap.String("user", name))
|
||||
h.actorInboxUpdate(c, user, payload)
|
||||
case "Accept":
|
||||
h.logger.Debug("User received accept payload", zap.String("user", name))
|
||||
h.actorInboxAccept(c, user, payload, body)
|
||||
case "Create":
|
||||
h.logger.Debug("User received accept payload", zap.String("user", name))
|
||||
h.actorInboxCreate(c, user, payload, body)
|
||||
case "Announce":
|
||||
h.logger.Debug("User received announce payload", zap.String("user", name))
|
||||
h.actorInboxAnnounce(c, user, payload, body)
|
||||
default:
|
||||
h.logger.Debug("User received unexpected payload type", zap.String("type", payloadType), zap.String("user", name))
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func (h handler) actorInboxPing(c *gin.Context, user *storage.User, payload storage.Payload) {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h handler) actorInboxPong(c *gin.Context, user *storage.User, payload storage.Payload) {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h handler) actorInboxAccept(c *gin.Context, user *storage.User, payload storage.Payload, body []byte) {
|
||||
if err := h.verifySignature(c); err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
payloadID, err := storage.ValidateActivity(payload)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
obj, ok := storage.JSONMap(payload, "object")
|
||||
if !ok {
|
||||
h.hardFail(c, fmt.Errorf("object key not present in payload"))
|
||||
return
|
||||
}
|
||||
requestID, ok := storage.JSONString(obj, "id")
|
||||
if !ok {
|
||||
h.hardFail(c, fmt.Errorf("id key not present in payload object"))
|
||||
return
|
||||
}
|
||||
target, ok := storage.JSONString(obj, "object")
|
||||
if !ok {
|
||||
h.hardFail(c, fmt.Errorf("object key not present in payload object"))
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("accept received", zap.String("request_activity_id", requestID))
|
||||
|
||||
requestActivityID := strings.TrimPrefix(requestID, fmt.Sprintf("https://%s/activity/", h.domain))
|
||||
|
||||
c1, err := h.storage.RowCount(c.Request.Context(), `SELECT COUNT(*) FROM following WHERE user_id = $1 AND actor = $2 AND request_activity_id = $3 AND accept_activity_id IS NULL`, user.ID, target, requestActivityID)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
if c1 == 1 {
|
||||
incomingActivityID := storage.NewV4()
|
||||
err = h.storage.RecordActivity(c.Request.Context(), incomingActivityID, payloadID, string(body))
|
||||
if err != nil {
|
||||
h.logger.Error("unable to record activity", zap.Error(err))
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.storage.FollowAccept(c.Request.Context(), user.ID, incomingActivityID, target)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h handler) actorInboxFollow(c *gin.Context, user *storage.User, payload storage.Payload, body []byte) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
payloadID, err := storage.ValidateActivity(payload)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
localActor, ok := storage.JSONString(payload, "object")
|
||||
if !ok {
|
||||
h.hardFail(c, fmt.Errorf("actor missing from payload"))
|
||||
return
|
||||
}
|
||||
if localActor != fmt.Sprintf("https://%s/users/%s", h.domain, user.Name) {
|
||||
h.logger.Warn("follow request for unexpected local actor", zap.String("object", localActor))
|
||||
c.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
remoteActorID, ok := storage.JSONString(payload, "actor")
|
||||
if !ok {
|
||||
h.hardFail(c, fmt.Errorf("actor missing from payload"))
|
||||
return
|
||||
}
|
||||
actor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, job.DefaultHTTPClient(), remoteActorID)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.verifySignature(c); err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
incomingActivityID := storage.NewV4()
|
||||
err = h.storage.RecordActivity(c.Request.Context(), incomingActivityID, payloadID, string(body))
|
||||
if err != nil {
|
||||
h.logger.Error("unable to record activity", zap.Error(err))
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := storage.EmptyPayload()
|
||||
acceptActivityID := storage.NewV4()
|
||||
response["@context"] = "https://www.w3.org/ns/activitystreams"
|
||||
response["actor"] = storage.NewActorID(user.Name, h.domain)
|
||||
response["id"] = fmt.Sprintf("https://%s/activity/%s", h.domain, acceptActivityID)
|
||||
response["object"] = payload
|
||||
response["to"] = actor.GetID()
|
||||
response["type"] = "Accept"
|
||||
response["published"] = time.Now().Format("2006-01-02T15:04:05Z")
|
||||
responsePayload := response.Bytes()
|
||||
|
||||
err = h.storage.RecordActivity(ctx, acceptActivityID, response["id"].(string), string(responsePayload))
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
nc := fed.ActorClient{
|
||||
HTTPClient: job.DefaultHTTPClient(),
|
||||
Logger: h.logger,
|
||||
}
|
||||
err = nc.SendToInbox(ctx, storage.LocalActor{User: user, ActorID: storage.NewActorID(user.Name, h.domain),}, actor, responsePayload)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
followerID := storage.NewV4()
|
||||
err = h.storage.FollowerRequest(ctx, followerID, user.ID, incomingActivityID, acceptActivityID, remoteActorID)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h handler) actorInboxUndo(c *gin.Context, user *storage.User, payload storage.Payload) {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h handler) actorInboxUpdate(c *gin.Context, user *storage.User, payload storage.Payload) {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h handler) actorInboxCreate(c *gin.Context, user *storage.User, payload storage.Payload, body []byte) {
|
||||
payloadID, err := storage.ValidateActivity(payload)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
isRelevant, err := h.isActivityRelevant(c.Request.Context(), payload, user)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
if !isRelevant {
|
||||
h.logger.Info("activity is not relevant to user")
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
if err = h.verifySignature(c); err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
incomingActivityID := storage.NewV4()
|
||||
err = h.storage.RecordActivity(c.Request.Context(), incomingActivityID, payloadID, string(body))
|
||||
if err != nil {
|
||||
h.logger.Error("unable to record activity", zap.Error(err))
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.storage.AddToUserFeed(c.Request.Context(), user.ID, incomingActivityID)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h handler) actorInboxAnnounce(c *gin.Context, user *storage.User, payload storage.Payload, body []byte) {
|
||||
isRelevant, err := h.isActivityRelevant(c.Request.Context(), payload, user)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
if !isRelevant {
|
||||
h.logger.Info("activity is not relevant to user")
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
if err = h.verifySignature(c); err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
object, ok := storage.JSONString(payload, "object")
|
||||
if !ok {
|
||||
h.hardFail(c, fmt.Errorf("object missing from announce payload"))
|
||||
return
|
||||
}
|
||||
payloadID, err := storage.ValidateActivity(payload)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
incomingActivityID := storage.NewV4()
|
||||
err = h.storage.RecordActivity(c.Request.Context(), incomingActivityID, payloadID, string(body))
|
||||
if err != nil {
|
||||
h.logger.Error("unable to record activity", zap.Error(err))
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.storage.AddToUserFeed(c.Request.Context(), user.ID, incomingActivityID)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
localActivityPrefix := fmt.Sprintf("https://%s/activity/", h.domain)
|
||||
if strings.HasPrefix(object, localActivityPrefix) {
|
||||
// activity is local, nothing else to do
|
||||
c.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
ac := fed.ActivityClient{
|
||||
HTTPClient: job.DefaultHTTPClient(),
|
||||
Logger: h.logger,
|
||||
}
|
||||
rawAnnounced, payloadAnnounced, err := ac.Get(object)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
announcementID, err := storage.ValidateActivity(payloadAnnounced)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
announcedActivityID := storage.NewV4()
|
||||
err = h.storage.RecordActivity(c.Request.Context(), announcedActivityID, announcementID, rawAnnounced)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
allActors := fed.ActorsFromActivity(payloadAnnounced)
|
||||
for _, activityActor := range allActors {
|
||||
_, err := fed.GetOrFetchActor(c.Request.Context(), h.storage, h.logger, job.DefaultHTTPClient(), activityActor)
|
||||
if err != nil {
|
||||
h.logger.Warn("unable to fetch activity actor", zap.String("actor", activityActor))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "https://mastodon.social/users/ngerakines/statuses/103731083662350899/activity",
|
||||
"type": "Announce",
|
||||
"actor": "https://mastodon.social/users/ngerakines",
|
||||
"published": "2020-02-27T13:38:44Z",
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"cc": [
|
||||
"https://tavern.ngrok.io/users/nick",
|
||||
"https://mastodon.social/users/ngerakines/followers"
|
||||
],
|
||||
"object": "https://tavern.ngrok.io/activity/0cec0e12-ff08-444b-90ec-d13c2e0449bc",
|
||||
"atomUri": "https://mastodon.social/users/ngerakines/statuses/103731083662350899/activity",
|
||||
"signature": {
|
||||
"type": "RsaSignature2017",
|
||||
"creator": "https://mastodon.social/users/ngerakines#main-key",
|
||||
"created": "2020-02-27T13:38:44Z",
|
||||
"signatureValue": "MVKS4SWKSj3swdmPlpAtrJOPJzHT6xblMASVAbKAyq01ErNcugB5RjjCLhrSKWvI80tYO3SmUtDYiZedbrOPdN947ZTqeECqXtUFNCWMtoBaHqtrkvsKTIeLiZ6zyx8bEKCA/P3RrZGoi6KxeLed7n4b0cZIKBpsstq7CALbP+/5DECs47gOW/cw6He4RbC9JpGRjTmmCF53rmQMgCEng0ixrTfjzqVlDMZpvPgY+BJXne9Y5Y5pzkWglXRTmT/BOQOGqmLtx3aFTB827FdhNE+AbxYMt3ot9dAso9YwdqeNC9ocvNsPwpevEjcedq7/1vVB175FRXluILxzs+J7EA=="
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "https://mastodon.social/users/ngerakines/statuses/103731105039051714/activity",
|
||||
"type": "Announce",
|
||||
"actor": "https://mastodon.social/users/ngerakines",
|
||||
"published": "2020-02-27T13:44:11Z",
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"cc": [
|
||||
"https://mastodon.social/users/aindri",
|
||||
"https://mastodon.social/users/ngerakines/followers"
|
||||
],
|
||||
"object": "https://mastodon.social/users/aindri/statuses/103730835135743634",
|
||||
"atomUri": "https://mastodon.social/users/ngerakines/statuses/103731105039051714/activity",
|
||||
"signature": {
|
||||
"type": "RsaSignature2017",
|
||||
"creator": "https://mastodon.social/users/ngerakines#main-key",
|
||||
"created": "2020-02-27T13:44:11Z",
|
||||
"signatureValue": "FXs2qNTs9FmRyIId/ywuOTOARhureFOA3K/1nqXsq7HGvcHaZ9mPxpBaXxFxdO/wAPoBvVJpidPFc5dOMoNTC4fhfAG3VmDZYlFTjJBqK5UqEHqu0xHzzbmeA65QBdwYryUxccgs6uJ0tcIBvgrNIHYOeEub1jpT153dgi5YcSpvQegafpuBAkEdP74RXXEsfyTH1WSQ/57FVaA4eZQhg5gm53HT/eEMuzk9PbyEwW4vbvlzrWC6gVnFaS1kQzQfJk1zZTDmu/Ee/SyQrS4ZIHY2SbFW/zzKpCyuX4Li3fqqzVCfBDe0nl2p5YY3E/5RxsZONIt6HOEk8sVTryJhIg=="
|
||||
}
|
||||
}
|
||||
*/
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func skipActorInbox(j storage.Payload) bool {
|
||||
t, _ := storage.JSONString(j, "type")
|
||||
a, _ := storage.JSONString(j, "actor")
|
||||
o, _ := storage.JSONString(j, "object")
|
||||
if t == "Delete" && a == o {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h handler) isActivityRelevant(ctx context.Context, activity storage.Payload, user *storage.User) (bool, error) {
|
||||
actor, ok := storage.JSONString(activity, "actor")
|
||||
if ok {
|
||||
isFollowing, err := h.storage.IsFollowing(ctx, user.ID, actor)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if isFollowing {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
actorID := storage.NewActorID(user.Name, h.domain)
|
||||
|
||||
obj, ok := storage.JSONMap(activity, "object")
|
||||
if ok {
|
||||
to, ok := storage.JSONStrings(obj, "to")
|
||||
if ok {
|
||||
for _, i := range to {
|
||||
if i == string(actorID) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
cc, ok := storage.JSONStrings(obj, "cc")
|
||||
if ok {
|
||||
for _, i := range cc {
|
||||
if i == string(actorID) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
conversation, ok := storage.JSONString(obj, "conversation")
|
||||
if ok {
|
||||
if isWatching, err := h.storage.PartOfThread(ctx, user.ID, conversation); err == nil && isWatching {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
tag, ok := storage.JSONMapList(obj, "tag")
|
||||
if ok {
|
||||
for _, i := range tag {
|
||||
tagType, hasTagType := storage.JSONString(i, "type")
|
||||
href, hasHref := storage.JSONString(i, "href")
|
||||
if hasTagType && hasHref && tagType == "Mention" && href == string(actorID) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (h handler) verifySignature(c *gin.Context) error {
|
||||
// Host header isn't set for some reason.
|
||||
c.Request.Header.Add("Host", c.Request.Host)
|
||||
verifier, err := httpsig.NewVerifier(c.Request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := h.storage.GetKey(c.Request.Context(), verifier.KeyId())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicKey, err := key.GetDecodedPublicKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = verifier.Verify(publicKey, httpsig.RSA_SHA256); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/ngerakines/tavern/g"
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
func (h handler) apiV1Instance(c *gin.Context) {
|
||||
user, err := h.storage.GetUserByName(c.Request.Context(), h.adminUser)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
followers, err := h.storage.RowCount(c.Request.Context(), `SELECT COUNT(*) FROM followers WHERE user_id = $1`, user.ID)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
following, err := h.storage.RowCount(c.Request.Context(), `SELECT COUNT(*) FROM following WHERE user_id = $1`, user.ID)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
users, err := h.storage.RowCount(c.Request.Context(), `SELECT COUNT(*) FROM users`)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"uri": h.domain,
|
||||
"title": "Tavern",
|
||||
"short_description": "An ActivityPub server.",
|
||||
"description": "An ActivityPub server.",
|
||||
"email": fmt.Sprintf("admin@%s", h.domain),
|
||||
"version": g.Version(),
|
||||
"urls": map[string]interface{}{},
|
||||
"stats": map[string]interface{}{
|
||||
"user_count": users,
|
||||
"status_count": 0,
|
||||
"domain_count": 0,
|
||||
},
|
||||
"thumbnail": fmt.Sprintf("https://%s/public/server_thumbnail.png", h.domain),
|
||||
"languages": []string{"en"},
|
||||
"registrations": false,
|
||||
"approval_required": true,
|
||||
"contact_account": map[string]interface{}{
|
||||
"id": user.ID.String(),
|
||||
"username": user.Name,
|
||||
"acct": user.Name,
|
||||
"display_name": user.DisplayName,
|
||||
"locked": false,
|
||||
"bot": false,
|
||||
"discoverable": true,
|
||||
"group": false,
|
||||
"created_at": user.CreatedAt,
|
||||
"note": user.About,
|
||||
"url": storage.NewActorID(user.Name, h.domain),
|
||||
"avatar": fmt.Sprintf("https://%s/avatars/%s", h.domain, user.Name),
|
||||
"avatar_static": fmt.Sprintf("https://%s/avatars/%s", h.domain, user.Name),
|
||||
"followers_count": followers,
|
||||
"following_count": following,
|
||||
"statuses_count": 0,
|
||||
"last_status_at": time.Now().Format("2006-01-02"),
|
||||
"fields": []string{},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (h handler) apiV1InstancePeers(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, []string{
|
||||
"mastodon.social",
|
||||
})
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
)
|
||||
|
||||
func (h handler) configure(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
ctx := c.Request.Context()
|
||||
trans, transOK := c.Get("trans")
|
||||
if !transOK {
|
||||
panic("trans not found in context")
|
||||
}
|
||||
data := gin.H{
|
||||
"flashes": getFlashes(session),
|
||||
"Trans": trans,
|
||||
}
|
||||
|
||||
user, err := h.storage.GetUserBySession(ctx, session)
|
||||
if err != nil {
|
||||
if errors.Is(err, errors.UserSessionNotFoundError{}) {
|
||||
|
||||
if err = appendFlashError(session, "Not Authenticated"); err != nil {
|
||||
h.hardFail(c, errors.NewCannotSaveSessionError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
return
|
||||
}
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
data["user"] = user
|
||||
|
||||
if err = session.Save(); err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "configure", data)
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
)
|
||||
|
||||
func (h handler) dashboard(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
ctx := c.Request.Context()
|
||||
trans, transOK := c.Get("trans")
|
||||
if !transOK {
|
||||
panic("trans not found in context")
|
||||
}
|
||||
data := gin.H{
|
||||
"flashes": getFlashes(session),
|
||||
"Trans": trans,
|
||||
}
|
||||
|
||||
user, err := h.storage.GetUserBySession(ctx, session)
|
||||
if err != nil {
|
||||
if errors.Is(err, errors.UserSessionNotFoundError{}) {
|
||||
|
||||
if err = appendFlashError(session, "Not Authenticated"); err != nil {
|
||||
h.hardFail(c, errors.NewCannotSaveSessionError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
return
|
||||
}
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
data["user"] = user
|
||||
|
||||
page := intParam(c, "page", 1)
|
||||
limit := intParam(c, "limit", 100)
|
||||
|
||||
total, err := h.storage.RowCount(ctx, `SELECT COUNT(*) FROM user_feed WHERE user_id = $1`, user.ID)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
uf, err := h.storage.PaginateUserFeed(ctx, user.ID, limit, (page-1)*limit)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
pageCount := math.Ceil(float64(total) / float64(limit))
|
||||
data["page_count"] = int(pageCount)
|
||||
data["page"] = page
|
||||
data["limit"] = limit
|
||||
|
||||
var activityIDs []uuid.UUID
|
||||
for _, ufi := range uf {
|
||||
activityIDs = append(activityIDs, ufi.ActivityID)
|
||||
}
|
||||
|
||||
activities, err := h.storage.GetActivitiesByIDs(ctx, activityIDs)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
data["activities"] = activities
|
||||
|
||||
var pages []int
|
||||
for i := page - 3; i <= page+3; i++ {
|
||||
if i > 0 && i <= int(pageCount) {
|
||||
pages = append(pages, i)
|
||||
}
|
||||
}
|
||||
data["pages"] = pages
|
||||
|
||||
if err = session.Save(); err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, "dashboard", data)
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
)
|
||||
|
||||
func (h handler) home(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
ctx := c.Request.Context()
|
||||
trans, transOK := c.Get("trans")
|
||||
if !transOK {
|
||||
panic("trans not found in context")
|
||||
}
|
||||
data := gin.H{
|
||||
"flashes": getFlashes(session),
|
||||
"authenticated": false,
|
||||
"Trans": trans,
|
||||
}
|
||||
|
||||
if c.Query("landing") == "true" {
|
||||
c.HTML(http.StatusOK, "index", data)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := h.storage.GetUserBySession(ctx, session)
|
||||
if err == nil {
|
||||
c.Redirect(http.StatusFound, "/dashboard")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, errors.UserSessionNotFoundError{}) {
|
||||
|
||||
if err := session.Save(); err != nil {
|
||||
h.hardFail(c, errors.NewCannotSaveSessionError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "index", data)
|
||||
return
|
||||
}
|
||||
h.hardFail(c, err)
|
||||
}
|
||||
|
||||
func (h handler) signin(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
ctx := c.Request.Context()
|
||||
|
||||
email := c.PostForm("email")
|
||||
password := c.PostForm("password")
|
||||
userID, err := h.storage.AuthenticateUser(ctx, email, []byte(password))
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.storage.GetUser(ctx, userID)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
err = h.storage.UpdateUserLastAuth(ctx, userID)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
c.SetCookie("lang", user.Locale, 0, "/", h.domain, true, true)
|
||||
session.Set(gin.AuthUserKey, userID.String())
|
||||
if err = session.Save(); err != nil {
|
||||
h.hardFail(c, errors.NewCannotSaveSessionError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
func (h handler) signout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
|
||||
session.Clear()
|
||||
|
||||
if err := session.Save(); err != nil {
|
||||
h.hardFail(c, errors.NewCannotSaveSessionError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
"github.com/ngerakines/tavern/fed"
|
||||
"github.com/ngerakines/tavern/job"
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
func (h handler) dashboardNetwork(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
ctx := c.Request.Context()
|
||||
trans, transOK := c.Get("trans")
|
||||
if !transOK {
|
||||
panic("trans not found in context")
|
||||
}
|
||||
data := gin.H{
|
||||
"flashes": getFlashes(session),
|
||||
"Trans": trans,
|
||||
}
|
||||
|
||||
user, err := h.storage.GetUserBySession(ctx, session)
|
||||
if err != nil {
|
||||
if errors.Is(err, errors.UserSessionNotFoundError{}) {
|
||||
|
||||
if err = appendFlashError(session, "Not Authenticated"); err != nil {
|
||||
h.hardFail(c, errors.NewCannotSaveSessionError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
return
|
||||
}
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
data["user"] = user
|
||||
|
||||
totalFollowers, err := h.storage.RowCount(ctx, `SELECT COUNT(*) FROM followers WHERE user_id = $1`, user.ID)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
followers, err := h.storage.GetUserFollowers(ctx, user.ID, totalFollowers+1, 0)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
data["followers"] = followers
|
||||
|
||||
totalFollowing, err := h.storage.RowCount(ctx, `SELECT COUNT(*) FROM following WHERE user_id = $1`, user.ID)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
following, err := h.storage.GetUserFollowing(ctx, user.ID, totalFollowing+1, 0)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
data["following"] = following
|
||||
|
||||
c.HTML(http.StatusOK, "dashboard_network", data)
|
||||
}
|
||||
|
||||
func (h handler) followActor(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
ctx := c.Request.Context()
|
||||
|
||||
user, err := h.storage.GetUserBySession(ctx, session)
|
||||
if err != nil {
|
||||
if errors.Is(err, errors.UserSessionNotFoundError{}) {
|
||||
|
||||
if err = appendFlashError(session, "Not Authenticated"); err != nil {
|
||||
h.hardFail(c, errors.NewCannotSaveSessionError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
return
|
||||
}
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
actor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, job.DefaultHTTPClient(), c.PostForm("actor"))
|
||||
if err != nil {
|
||||
h.flashErrorOrFail(c, "/dashboard/network", err)
|
||||
return
|
||||
}
|
||||
isFollowing, err := h.storage.IsFollowing(ctx, user.ID, actor.GetID())
|
||||
if err != nil {
|
||||
h.flashErrorOrFail(c, "/dashboard/network", err)
|
||||
return
|
||||
}
|
||||
if isFollowing {
|
||||
c.Redirect(http.StatusFound, "/dashboard/network")
|
||||
return
|
||||
}
|
||||
follow := storage.EmptyPayload()
|
||||
activityID := storage.NewV4()
|
||||
follow["@context"] = "https://www.w3.org/ns/activitystreams"
|
||||
follow["actor"] = storage.NewActorID(user.Name, h.domain)
|
||||
follow["id"] = fmt.Sprintf("https://%s/activity/%s", h.domain, activityID)
|
||||
follow["object"] = actor.GetID()
|
||||
follow["to"] = actor.GetID()
|
||||
follow["type"] = "Follow"
|
||||
follow["published"] = time.Now().Format("2006-01-02T15:04:05Z")
|
||||
|
||||
payload := follow.Bytes()
|
||||
fmt.Println(string(payload))
|
||||
|
||||
err = h.storage.RecordActivity(ctx, activityID, fmt.Sprintf("https://%s/activity/%s", h.domain, activityID), string(payload))
|
||||
if err != nil {
|
||||
h.flashErrorOrFail(c, "/dashboard/network", err)
|
||||
return
|
||||
}
|
||||
|
||||
nc := fed.ActorClient{
|
||||
HTTPClient: job.DefaultHTTPClient(),
|
||||
Logger: h.logger,
|
||||
}
|
||||
err = nc.SendToInbox(ctx, storage.LocalActor{User: user, ActorID: storage.NewActorID(user.Name, h.domain),}, actor, payload)
|
||||
if err != nil {
|
||||
h.flashErrorOrFail(c, "/dashboard/network", err)
|
||||
return
|
||||
}
|
||||
|
||||
followID := storage.NewV4()
|
||||
err = h.storage.FollowRequest(ctx, followID, user.ID, activityID, actor.GetID())
|
||||
if err != nil {
|
||||
h.flashErrorOrFail(c, "/dashboard/network", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = session.Save(); err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, "/dashboard/network")
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
"github.com/ngerakines/tavern/fed"
|
||||
"github.com/ngerakines/tavern/job"
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
func (h handler) dashboardNotes(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
ctx := c.Request.Context()
|
||||
trans, transOK := c.Get("trans")
|
||||
if !transOK {
|
||||
panic("trans not found in context")
|
||||
}
|
||||
data := gin.H{
|
||||
"flashes": getFlashes(session),
|
||||
"Trans": trans,
|
||||
}
|
||||
|
||||
user, err := h.storage.GetUserBySession(ctx, session)
|
||||
if err != nil {
|
||||
if errors.Is(err, errors.UserSessionNotFoundError{}) {
|
||||
|
||||
if err = appendFlashError(session, "Not Authenticated"); err != nil {
|
||||
h.hardFail(c, errors.NewCannotSaveSessionError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
return
|
||||
}
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
data["user"] = user
|
||||
|
||||
if err = session.Save(); err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, "dashboard_notes", data)
|
||||
}
|
||||
|
||||
func (h handler) createNote(c *gin.Context) {
|
||||
// "conversation":"tag:mastodon.social,2020-02-26:objectId=156298483:objectType=Conversation"
|
||||
|
||||
session := sessions.Default(c)
|
||||
ctx := c.Request.Context()
|
||||
|
||||
user, err := h.storage.GetUserBySession(ctx, session)
|
||||
if err != nil {
|
||||
if errors.Is(err, errors.UserSessionNotFoundError{}) {
|
||||
|
||||
if err = appendFlashError(session, "Not Authenticated"); err != nil {
|
||||
h.hardFail(c, errors.NewCannotSaveSessionError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
return
|
||||
}
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
content := c.PostForm("content")
|
||||
if len(content) == 0 {
|
||||
h.flashErrorOrFail(c, "/dashboard/network", fmt.Errorf("note is empty"))
|
||||
return
|
||||
}
|
||||
|
||||
actor := storage.NewActorID(user.Name, h.domain)
|
||||
activityID := storage.NewV4()
|
||||
activityURL := fmt.Sprintf("https://%s/activity/%s", h.domain, activityID)
|
||||
now := time.Now()
|
||||
publishedAt := now.Format("2006-01-02T15:04:05Z")
|
||||
conversation := fmt.Sprintf("tag:%s,%s:objectId=%s:objectType=Conversation", h.domain, now.Format("2006-01-02"), activityID)
|
||||
|
||||
createNote := storage.EmptyPayload()
|
||||
createNote["@context"] = "https://www.w3.org/ns/activitystreams"
|
||||
createNote["actor"] = actor
|
||||
createNote["id"] = activityURL
|
||||
createNote["published"] = publishedAt
|
||||
createNote["type"] = "Create"
|
||||
createNote["to"] = []string{
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
actor.Followers(),
|
||||
}
|
||||
note := storage.EmptyPayload()
|
||||
note["attributedTo"] = actor
|
||||
note["content"] = content
|
||||
note["context"] = conversation
|
||||
note["conversation"] = conversation
|
||||
note["id"] = activityURL
|
||||
note["published"] = publishedAt
|
||||
note["summary"] = ""
|
||||
note["to"] = "https://www.w3.org/ns/activitystreams#Public"
|
||||
note["type"] = "Note"
|
||||
note["url"] = activityURL
|
||||
createNote["object"] = note
|
||||
payload := createNote.Bytes()
|
||||
|
||||
err = h.storage.RecordActivity(ctx, activityID, activityURL, string(payload))
|
||||
if err != nil {
|
||||
h.flashErrorOrFail(c, "/dashboard/notes", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.storage.WatchThread(ctx, user.ID, conversation)
|
||||
if err != nil {
|
||||
h.flashErrorOrFail(c, "/dashboard/notes", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.storage.AddToUserFeed(ctx, user.ID, activityID)
|
||||
if err != nil {
|
||||
h.flashErrorOrFail(c, "/dashboard/notes", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.storage.RecordUserActivity(ctx, storage.NewV4(), user.ID, activityID)
|
||||
if err != nil {
|
||||
h.flashErrorOrFail(c, "/dashboard/notes", err)
|
||||
return
|
||||
}
|
||||
|
||||
nc := fed.ActorClient{
|
||||
HTTPClient: job.DefaultHTTPClient(),
|
||||
Logger: h.logger,
|
||||
}
|
||||
err = nc.Broadcast(ctx, h.storage, storage.LocalActor{User: user, ActorID: storage.NewActorID(user.Name, h.domain)}, payload)
|
||||
if err != nil {
|
||||
h.flashErrorOrFail(c, "/dashboard/notes", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/dashboard/notes")
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
func (h handler) serverInbox(c *gin.Context) {
|
||||
// TODO: verify signature
|
||||
defer c.Request.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(io.LimitReader(c.Request.Body, 1*1024*1024))
|
||||
if err != nil {
|
||||
h.logger.Error("unable to slurp request body into payload", zap.Error(err))
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
payload, err := storage.PayloadFromBytes(body)
|
||||
if err != nil {
|
||||
h.logger.Error("unable to marshall request body into payload", zap.Error(err))
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
payloadID, err := storage.ValidateActivity(payload)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
activityID := storage.NewV4()
|
||||
err = h.storage.RecordActivity(c.Request.Context(), activityID, payloadID, string(body))
|
||||
if err != nil {
|
||||
h.logger.Error("unable to record activity", zap.Error(err))
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
payloadType, _ := storage.JSONString(payload, "type")
|
||||
switch payloadType {
|
||||
case "Follow":
|
||||
h.logger.Debug("User received ping payload")
|
||||
h.serverInboxFollow(c, payload)
|
||||
case "Undo":
|
||||
h.logger.Debug("User received pong payload")
|
||||
h.serverInboxUndo(c, payload)
|
||||
default:
|
||||
h.logger.Debug("Server received unexpected payload type", zap.String("type", payloadType))
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func (h handler) serverInboxFollow(c *gin.Context, payload storage.Payload) {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h handler) serverInboxUndo(c *gin.Context, payload storage.Payload) {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
"github.com/ngerakines/tavern/g"
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
func (h handler) nodeInfo(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
"href": fmt.Sprintf("https://%s/nodeinfo/2.0", h.domain),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (h handler) nodeInfoDetails(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"version": "2.0",
|
||||
"software": map[string]interface{}{
|
||||
"name": "tavern",
|
||||
"version": g.Version(),
|
||||
},
|
||||
"protocols": []string{"activitypub"},
|
||||
"usage": map[string]interface{}{
|
||||
"users": map[string]interface{}{
|
||||
"total": 1,
|
||||
"activeMonth": 1,
|
||||
"activeHalfyear": 1,
|
||||
},
|
||||
"localPosts": 1,
|
||||
},
|
||||
"openRegistrations": false,
|
||||
})
|
||||
}
|
||||
|
||||
func (h handler) webFinger(c *gin.Context) {
|
||||
username, domain, err := fingerUserDomain(c.Query("resource"), h.domain)
|
||||
if err != nil {
|
||||
h.logger.Error("error parsing resource", zap.Error(err))
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("webfinger request", zap.String("resource", c.Query("resource")))
|
||||
|
||||
user, err := h.storage.GetUserByName(c.Request.Context(), username)
|
||||
if err != nil {
|
||||
// TODO: Handle not found
|
||||
h.logger.Error("error getting user", zap.Error(err))
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
j := storage.EmptyPayload()
|
||||
j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, domain)
|
||||
j["aliases"] = []string{
|
||||
fmt.Sprintf("https://%s/users/%s", domain, user.Name),
|
||||
}
|
||||
|
||||
l := storage.EmptyPayload()
|
||||
l["rel"] = "self"
|
||||
l["type"] = `application/activity+json`
|
||||
l["href"] = fmt.Sprintf("https://%s/users/%s", domain, user.Name)
|
||||
j["links"] = []storage.Payload{l}
|
||||
|
||||
h.WriteJRD(c, j)
|
||||
}
|
||||
|
||||
func fingerUserDomain(input, domain string) (string, string, error) {
|
||||
input = strings.TrimPrefix(input, "acct:")
|
||||
|
||||
domainPrefix := fmt.Sprintf("https://%s/users/", domain)
|
||||
if strings.HasPrefix(input, domainPrefix) {
|
||||
user := strings.TrimPrefix(input, domainPrefix)
|
||||
return user, domain, nil
|
||||
}
|
||||
if input == fmt.Sprintf("https://%s/users/%s") {
|
||||
|
||||
}
|
||||
|
||||
parts := strings.FieldsFunc(input, func(r rune) bool {
|
||||
return r == '@'
|
||||
})
|
||||
if len(parts) != 2 {
|
||||
return "", "", errors.New("malformed resource parameter")
|
||||
}
|
||||
if parts[1] != domain {
|
||||
return "", "", errors.New("malformed resource parameter")
|
||||
}
|
||||
user := strings.TrimSpace(parts[0])
|
||||
if len(user) == 0 {
|
||||
return "", "", errors.New("malformed resource parameter")
|
||||
}
|
||||
return user, domain, nil
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/locales"
|
||||
"github.com/go-playground/locales/currency"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Translator interface {
|
||||
locales.Translator
|
||||
T(key interface{}, params ...string) string
|
||||
THTML(key interface{}, params ...template.HTML) template.HTML
|
||||
C(key interface{}, num float64, digits uint64, param string) string
|
||||
O(key interface{}, num float64, digits uint64, param string) string
|
||||
R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) string
|
||||
Currency() currency.Type
|
||||
}
|
||||
|
||||
type translator struct {
|
||||
locales.Translator
|
||||
trans ut.Translator
|
||||
logger *zap.Logger
|
||||
enableSentry bool
|
||||
}
|
||||
|
||||
var _ Translator = (*translator)(nil)
|
||||
|
||||
func (t *translator) T(key interface{}, params ...string) string {
|
||||
keyValue := fmt.Sprintf("%s", key)
|
||||
s, err := t.trans.T(keyValue, params...)
|
||||
if err != nil {
|
||||
t.logger.Warn("issue translating key",
|
||||
zap.String("key", keyValue),
|
||||
zap.String("locale", t.Locale()),
|
||||
zap.Error(err))
|
||||
if t.enableSentry {
|
||||
hub := sentry.CurrentHub().Clone()
|
||||
hub.Scope().SetExtras(map[string]interface{}{
|
||||
"key": keyValue,
|
||||
"locale": t.Locale(),
|
||||
"param_count": len(params),
|
||||
})
|
||||
hub.CaptureException(err)
|
||||
}
|
||||
return keyValue
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *translator) THTML(key interface{}, params ...template.HTML) template.HTML {
|
||||
var stringParams []string
|
||||
for _, param := range params {
|
||||
stringParams = append(stringParams, string(param))
|
||||
}
|
||||
s, err := t.trans.T(key, stringParams...)
|
||||
if err != nil {
|
||||
t.logger.Warn("issue translating key", zap.String("key", fmt.Sprintf("%v", key)), zap.Error(err))
|
||||
return template.HTML(fmt.Sprintf("%s", key))
|
||||
}
|
||||
|
||||
return template.HTML(s)
|
||||
}
|
||||
|
||||
func (t *translator) C(key interface{}, num float64, digits uint64, param string) string {
|
||||
|
||||
s, err := t.trans.C(key, num, digits, param)
|
||||
if err != nil {
|
||||
t.logger.Warn("issue translating cardinal key", zap.String("key", fmt.Sprintf("%v", key)), zap.Error(err))
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *translator) O(key interface{}, num float64, digits uint64, param string) string {
|
||||
|
||||
s, err := t.trans.C(key, num, digits, param)
|
||||
if err != nil {
|
||||
t.logger.Warn("issue translating ordinal key", zap.String("key", fmt.Sprintf("%v", key)), zap.Error(err))
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *translator) R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) string {
|
||||
|
||||
s, err := t.trans.R(key, num1, digits1, num2, digits2, param1, param2)
|
||||
if err != nil {
|
||||
t.logger.Warn("issue translating range key", zap.String("key", fmt.Sprintf("%v", key)), zap.Error(err))
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *translator) Currency() currency.Type {
|
||||
switch t.Locale() {
|
||||
case "en":
|
||||
return currency.USD
|
||||
case "fr":
|
||||
return currency.EUR
|
||||
default:
|
||||
return currency.USD
|
||||
}
|
||||
}
|
||||
|
||||
type i18nMiddleware struct {
|
||||
utrans *ut.UniversalTranslator
|
||||
logger *zap.Logger
|
||||
domain string
|
||||
enableSentry bool
|
||||
}
|
||||
|
||||
func (h i18nMiddleware) findLocale(c *gin.Context) {
|
||||
var langs []string
|
||||
|
||||
setCookie := false
|
||||
if localeParam := c.Query("lang"); len(localeParam) > 0 {
|
||||
setCookie = true
|
||||
langs = append(langs, localeParam)
|
||||
}
|
||||
localeCookie, err := c.Cookie("lang")
|
||||
if err == nil && len(localeCookie) > 0 {
|
||||
langs = append(langs, localeCookie)
|
||||
}
|
||||
langs = append(langs, AcceptedLanguages(c)...)
|
||||
langs = append(langs, "en")
|
||||
|
||||
t, found := h.utrans.FindTranslator(langs...)
|
||||
if !found {
|
||||
h.logger.Warn("no translation found for language", zap.Strings("langs", langs))
|
||||
}
|
||||
if localeCookie == "" || t.Locale() != localeCookie {
|
||||
setCookie = true
|
||||
}
|
||||
if setCookie {
|
||||
c.SetCookie("lang", t.Locale(), 0, "/", h.domain, true, true)
|
||||
}
|
||||
|
||||
c.Set("locale", t.Locale())
|
||||
c.Set("trans", &translator{
|
||||
trans: t,
|
||||
Translator: t.(locales.Translator),
|
||||
logger: h.logger,
|
||||
enableSentry: h.enableSentry,
|
||||
})
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func localeOrDefault(c *gin.Context) string {
|
||||
ctxValue, ok := c.Get("locale")
|
||||
if ok {
|
||||
if locale, ok := ctxValue.(string); ok {
|
||||
return locale
|
||||
}
|
||||
}
|
||||
return "en"
|
||||
}
|
||||
|
||||
func AcceptedLanguages(c *gin.Context) (languages []string) {
|
||||
accepted := c.GetHeader("Accept-Language")
|
||||
if accepted == "" {
|
||||
return
|
||||
}
|
||||
options := strings.Split(accepted, ",")
|
||||
l := len(options)
|
||||
languages = make([]string, l)
|
||||
|
||||
for i := 0; i < l; i++ {
|
||||
locale := strings.SplitN(options[i], ";", 2)
|
||||
languages[i] = strings.Trim(locale[0], " ")
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
ohioLocal *time.Location
|
||||
)
|
||||
|
||||
func tmplOptionalDate(value sql.NullTime, defaultValue string) string {
|
||||
if value.Valid {
|
||||
return tmplDate(value.Time)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func tmplDate(value time.Time) string {
|
||||
return value.In(ohioLocal).Format("02 Jan 2006") // " MST"
|
||||
}
|
||||
|
||||
func tmplOptionalTime(value sql.NullTime, defaultValue string) string {
|
||||
if value.Valid {
|
||||
return tmplTime(value.Time)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func tmplTime(value time.Time) string {
|
||||
return value.In(ohioLocal).Format("3:04 PM") // " MST"
|
||||
}
|
||||
|
||||
func tmplOptionalDateTime(value sql.NullTime, defaultValue string) string {
|
||||
if value.Valid {
|
||||
return tmplDateTime(value.Time)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func tmplDateTime(value time.Time) string {
|
||||
return value.In(ohioLocal).Format("02 Jan 2006 3:04 PM") // " MST"
|
||||
}
|
||||
|
||||
func tmplShortUUID(id uuid.UUID) string {
|
||||
return hex.EncodeToString(id.Bytes())
|
||||
}
|
||||
|
||||
func tmplShort(input string, max int) string {
|
||||
halfMax := int(math.Ceil(float64(max) / 2))
|
||||
if l := len(input); l > max {
|
||||
return fmt.Sprintf("%s...%s", input[0:halfMax], input[l-halfMax:])
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
func tmplAnchor(url, label string) template.HTML {
|
||||
return template.HTML(fmt.Sprintf(`<a href="%s">%s</a>`, url, label))
|
||||
}
|
||||
|
||||
func tmplToHTML(input string) template.HTML {
|
||||
return template.HTML(input)
|
||||
}
|
||||
|
||||
func tmplStrong(args ...string) template.HTML {
|
||||
if len(args) == 1 {
|
||||
return template.HTML(fmt.Sprintf(`<strong>%s</strong>`, args[0]))
|
||||
}
|
||||
if len(args) == 2 {
|
||||
return template.HTML(fmt.Sprintf(`<strong class="%s">%s</strong>`, args[0], args[1]))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func tmplUrlGen(siteBase string) func(parts ...interface{}) string {
|
||||
return func(parts ...interface{}) string {
|
||||
if len(parts) >= 1 {
|
||||
if controller, ok := parts[0].(string); ok {
|
||||
switch controller {
|
||||
case "signin":
|
||||
return fmt.Sprintf("%s/signin", siteBase)
|
||||
case "signout":
|
||||
return fmt.Sprintf("%s/signout", siteBase)
|
||||
case "dashboard":
|
||||
return fmt.Sprintf("%s/dashboard", siteBase)
|
||||
case "network":
|
||||
return fmt.Sprintf("%s/dashboard/network", siteBase)
|
||||
case "notes":
|
||||
return fmt.Sprintf("%s/dashboard/notes", siteBase)
|
||||
case "configure":
|
||||
return fmt.Sprintf("%s/configure", siteBase)
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s/", siteBase)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue