├── .editorconfig ├── .env ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── api ├── api.go ├── entities.go ├── filters.go └── render.go ├── bors.toml ├── cmd ├── create.go ├── create_client.go ├── create_user.go ├── gen.go ├── gen_entity.go ├── gen_entity_test.go ├── ingest.go ├── migrate.go ├── root.go ├── serve.go └── util.go ├── config ├── config.go └── secrets │ ├── init.go │ ├── secrets.go │ └── subkey.go ├── docker-compose.yml ├── fixtures ├── localhost-jane.json └── localhost.json ├── go.mod ├── go.sum ├── ingress ├── ingest.go ├── ingest_test.go └── rules │ ├── activities.go │ ├── c2s.go │ ├── c2s_test.go │ ├── rule.go │ ├── util.go │ └── util_test.go ├── ld ├── cast.go ├── contexts │ ├── security-v1.json │ └── security-v2.json ├── entity.go ├── helpers.go ├── helpers_test.go ├── json.go ├── json_test.go ├── namespace.go ├── ns │ ├── as │ │ ├── as.ld.json │ │ ├── as.ttl │ │ ├── classes.gen.go │ │ ├── datatypes.gen.go │ │ ├── ns.gen.go │ │ └── properties.gen.go │ ├── index.gen.go │ ├── index.go │ ├── ldp │ │ ├── classes.gen.go │ │ ├── datatypes.gen.go │ │ ├── ldp.ld.json │ │ ├── ldp.ttl │ │ ├── ns.gen.go │ │ └── properties.gen.go │ ├── meta │ │ └── type.go │ ├── owl │ │ ├── classes.gen.go │ │ ├── datatypes.gen.go │ │ ├── ns.gen.go │ │ ├── owl.ld.json │ │ ├── owl.ttl │ │ └── properties.gen.go │ ├── rdf │ │ ├── classes.gen.go │ │ ├── datatypes.gen.go │ │ ├── ns.gen.go │ │ ├── properties.gen.go │ │ ├── rdf.ld.json │ │ ├── rdf.ttl │ │ ├── rdfs.ld.json │ │ └── rdfs.ttl │ └── sec │ │ ├── classes.gen.go │ │ ├── datatypes.gen.go │ │ ├── ns.gen.go │ │ ├── properties.gen.go │ │ ├── sec.ld.json │ │ └── sec.ttl ├── object.go ├── object_test.go └── source.go ├── lib ├── clog │ └── clog.go ├── context.go ├── context_test.go ├── errors.go ├── http.go ├── rediscache │ └── redis.go ├── snowflakes.go ├── snowflakes.mock.go ├── tokens.go ├── url.go └── xrd │ └── xrd.go ├── main.go ├── migrations ├── 1524325015_entities.down.sql ├── 1524325015_entities.up.sql ├── 1524569269_oauth2.down.sql ├── 1524569269_oauth2.up.sql ├── 1524739570_entity_2_entity_harder.down.sql ├── 1524739570_entity_2_entity_harder.up.sql ├── 1525550010_users.down.sql ├── 1525550010_users.up.sql ├── 1525551737_entity_kind.down.sql ├── 1525551737_entity_kind.up.sql ├── 1525606933_client_owners.down.sql ├── 1525606933_client_owners.up.sql ├── 1526417398_entity_id_validation.down.sql ├── 1526417398_entity_id_validation.up.sql ├── 1528746007_stream_items.down.sql └── 1528746007_stream_items.up.sql ├── models ├── accesstoken.go ├── accesstoken.mock.go ├── accesstoken_test.go ├── authorization.go ├── authorization.mock.go ├── authorization_test.go ├── client.go ├── client.mock.go ├── client_test.go ├── context.go ├── entity.go ├── entity.mock.go ├── entity_test.go ├── errors.go ├── errors_test.go ├── refreshtoken.go ├── refreshtoken.mock.go ├── refreshtoken_test.go ├── setup_test.go ├── stores.go ├── stores_mock.go ├── streamitem.go ├── streamitem.mock.go ├── streamitem_test.go ├── types.go ├── user.go ├── user.mock.go ├── user_test.go ├── util.go └── util_test.go ├── templates ├── login.html └── oauth │ └── authorize.html └── tools └── nsgen ├── .gitignore ├── README.md ├── declaration.go ├── main.go ├── namespaces.go ├── patches ├── patch.go └── sec.go ├── templates.go └── templates_test.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [*.go] 9 | indent_style = tab 10 | indent_size = 4 11 | 12 | [*.html] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | MEOW_DB=postgres://meow:meow@localhost:35432/meow?sslmode=disable 2 | MEOW_TEST_DB=postgres://meow:meow@localhost:35432/postgres?sslmode=disable 3 | MEOW_REDIS=redis://localhost:36379/0 4 | MEOW_SECRET=YWNjb3JkaW5nIHRvIGFsbCBrbm93biBsYXdzIG9mIGF2aWF0aW9uLCB0aGVyZSdzIG5vIHdheSBhIGJlZSBzaA== 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /meow 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | except: 3 | # Exclude bors staging branches 4 | - /\.tmp$/ 5 | 6 | language: go 7 | 8 | go: 9 | - "1.x" 10 | - "1.12.x" 11 | - "1.13.x" 12 | 13 | addons: 14 | postgresql: "9.6" # Use an up-to-date version of Postgres; default is 9.2. 15 | 16 | services: 17 | - postgresql # Needed for model tests. 18 | 19 | env: 20 | - MEOW_TEST_DB: postgres://localhost/meow_test?sslmode=disable 21 | 22 | before_script: 23 | - psql -U postgres -c 'create database meow_test;' 24 | 25 | 26 | 27 | # Why aren't test dependencies installed by default e_e 28 | # Passing -u so that we can use a cache and pull updates when needed, let's be nice to the hosts. 29 | install: go get -t -u -v ./... 30 | 31 | # Run with the race detector, it makes it slower, but races in something like this are bad. 32 | script: go test -race ./... 33 | 34 | 35 | 36 | # Cache $GOPATH/src to avoid having to re-fetch everything. 37 | cache: 38 | directories: 39 | - $GOPATH/src 40 | 41 | # Do not cache our own sources, that would be silly. 42 | before_cache: 43 | - ( shopt -s dotglob && rm -rf $GOPATH/src/github.com/meowpub/meow/* ) 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 the meow project contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![godoc reference](https://godoc.org/github.com/meowpub/meow?status.svg)](https://godoc.org/github.com/meowpub/meow) 2 | [![Build Status](https://travis-ci.org/meowpub/meow.svg?branch=master)](https://travis-ci.org/meowpub/meow) 3 | 4 | **:3c** 5 | 6 | Setting up a development environment 7 | ------------------------------------ 8 | 9 | You will need `docker` and `docker-compose` installed. 10 | 11 | 1. Run `docker-compose up` to bring up Postgres and Redis containers. 12 | 1. Run migrations: `go run . migrate up`. 13 | 1. (Optional) Load sample data for development - note that `meow` requires every node in a path. 14 | 1. `go run . ingest < fixtures/localhost.json` - create `https://localhost`. 15 | 1. `go run . ingest < fixtures/localhost-jane.json` - create `https://localhost/~jane`. 16 | 1. Run the server, accept requests to `localhost`: `go run . serve --domain localhost`. 17 | 1. Make some test requests: 18 | 1. `curl -v localhost:8000` 19 | 1. `curl -v localhost:8000/~jane` 20 | 21 | Make some changes, then just restart `go run . serve --domain localhost` to have them reflected. 22 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | 7 | "github.com/monzo/typhon" 8 | "github.com/pkg/errors" 9 | 10 | "github.com/meowpub/meow/lib" 11 | "github.com/meowpub/meow/models" 12 | ) 13 | 14 | var Service = typhon.Service(Handler) 15 | 16 | func Handler(req typhon.Request) typhon.Response { 17 | u := *req.URL 18 | u.Host, _, _ = net.SplitHostPort(req.Host) 19 | if u.Host == "" { 20 | u.Host = req.Host 21 | } 22 | entity, err := Lookup(req, u) 23 | if err != nil { 24 | return typhon.Response{Error: err} 25 | } 26 | if entity == nil { 27 | return typhon.Response{Error: lib.Error(http.StatusNotFound, "Not Found")} 28 | } 29 | switch req.Method { 30 | case http.MethodGet: 31 | return entity.GET(req) 32 | default: 33 | return typhon.Response{Error: lib.Error(http.StatusMethodNotAllowed, "Method Not Allowed")} 34 | } 35 | } 36 | 37 | // Returns the HTTP status code for an error. 38 | func ErrorCode(err error) int { 39 | if code := errorCode(err); code != 0 { 40 | return code 41 | } 42 | return http.StatusInternalServerError 43 | } 44 | 45 | func errorCode(err error) int { 46 | if err, ok := err.(lib.Err); ok { 47 | return err.StatusCode 48 | } 49 | if models.IsNotFound(err) { 50 | return http.StatusNotFound 51 | } 52 | if cause := errors.Cause(err); cause != err { 53 | return errorCode(cause) 54 | } 55 | return 0 56 | } 57 | -------------------------------------------------------------------------------- /api/entities.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "strings" 7 | 8 | "github.com/monzo/typhon" 9 | "github.com/pkg/errors" 10 | "go.uber.org/zap" 11 | 12 | "github.com/meowpub/meow/ld" 13 | "github.com/meowpub/meow/lib" 14 | "github.com/meowpub/meow/models" 15 | ) 16 | 17 | type Node interface { 18 | GET(typhon.Request) typhon.Response 19 | } 20 | 21 | type Object struct{ *ld.Object } 22 | 23 | func (o Object) GET(req typhon.Request) typhon.Response { 24 | return EntityResponse(req, o.Object) 25 | } 26 | 27 | func Instantiate(obj *ld.Object) Node { 28 | return Object{obj} 29 | } 30 | 31 | func Lookup(ctx context.Context, reqURL url.URL) (Node, error) { 32 | // Normalise the URL, forcing it to https:// and removing any authentication or queries. 33 | reqURL = url.URL{Scheme: "https", Host: reqURL.Host, Path: reqURL.Path} 34 | lib.GetLogger(ctx).Debug("Looking up entity...", zap.String("id", reqURL.String())) 35 | 36 | entities := models.GetStores(ctx).Entities() 37 | e, err := entities.GetByID(reqURL.String()) 38 | if err != nil { 39 | if models.IsNotFound(err) && strings.HasSuffix(reqURL.Path, "/") { 40 | newURL := reqURL 41 | newURL.Path = strings.TrimSuffix(newURL.Path, "/") 42 | return Lookup(ctx, newURL) 43 | } 44 | return nil, errors.Wrap(err, reqURL.String()) 45 | } 46 | return Instantiate(e.Obj), nil 47 | } 48 | -------------------------------------------------------------------------------- /api/filters.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | 10 | "github.com/go-redis/redis" 11 | "github.com/jinzhu/gorm" 12 | "github.com/meowpub/meow/config" 13 | "github.com/meowpub/meow/lib" 14 | "github.com/meowpub/meow/models" 15 | "github.com/monzo/typhon" 16 | "github.com/pkg/errors" 17 | "go.uber.org/zap" 18 | ) 19 | 20 | func StoresFilter(db *gorm.DB, r *redis.Client) typhon.Filter { 21 | return func(req typhon.Request, next typhon.Service) typhon.Response { 22 | req.Context = lib.WithDB(req, db) 23 | req.Context = lib.WithRedis(req, r) 24 | req.Context = models.WithStores(req, models.NewStores(db, r, config.RedisKeyspace())) 25 | req.Request = *req.WithContext(req) 26 | return next(req) 27 | } 28 | } 29 | 30 | func ErrorFilter(req typhon.Request, next typhon.Service) typhon.Response { 31 | var rsp typhon.Response 32 | if err := req.Err(); err != nil { 33 | rsp = typhon.NewResponse(req) 34 | rsp.Error = err 35 | } else { 36 | rsp = next(req) 37 | } 38 | 39 | if rsp.Response == nil { 40 | rsp.Response = &http.Response{} 41 | } 42 | if rsp.Request == nil { 43 | rsp.Request = &req 44 | } 45 | 46 | if rsp.Error != nil && rsp.Error.Error() != "" { 47 | // TODO: Grab stack traces out of the error, if possible. 48 | errV := map[string]interface{}{ 49 | "error": rsp.Error.Error(), 50 | } 51 | 52 | var buf bytes.Buffer 53 | enc := json.NewEncoder(&buf) 54 | enc.SetIndent("", " ") 55 | if err := enc.Encode(errV); err != nil { 56 | lib.GetLogger(req).DPanic("Failed to marshal error response", zap.Error(err)) 57 | } 58 | 59 | if rsp.Body != nil { 60 | rsp.Body.Close() 61 | } 62 | rsp.Body = ioutil.NopCloser(&buf) 63 | 64 | if rsp.StatusCode == 0 || rsp.StatusCode == http.StatusOK { 65 | rsp.StatusCode = ErrorCode(rsp.Error) 66 | } 67 | } 68 | return rsp 69 | } 70 | 71 | func PanicFilter(req typhon.Request, next typhon.Service) (rsp typhon.Response) { 72 | defer func() { 73 | if v := recover(); v != nil { 74 | lib.GetLogger(req).Error("panic: " + fmt.Sprint(v)) 75 | err, ok := v.(error) 76 | if !ok { 77 | err = errors.Errorf("panic: %v", v) 78 | } 79 | rsp = typhon.Response{Error: err} 80 | } 81 | }() 82 | return next(req) 83 | } 84 | -------------------------------------------------------------------------------- /api/render.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/monzo/typhon" 5 | 6 | "github.com/meowpub/meow/ld" 7 | ) 8 | 9 | // TODO: Do something with the Accept header here. 10 | func EntityResponse(req typhon.Request, obj *ld.Object) typhon.Response { 11 | v, err := ld.CompactObject(req, obj) 12 | if err != nil { 13 | return typhon.Response{Error: err} 14 | } 15 | return req.Response(v) 16 | } 17 | -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | required_approvals = 1 2 | delete_merged_branches = true 3 | pr_status = ["continuous-integration/travis-ci/pr"] 4 | status = ["continuous-integration/travis-ci/push"] 5 | -------------------------------------------------------------------------------- /cmd/create.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | // createCmd represents the create command 8 | var createCmd = &cobra.Command{ 9 | Use: "create", 10 | Short: "Create entities", 11 | Long: `Create entities from the commandline. See subcommands.`, 12 | } 13 | 14 | func init() { 15 | rootCmd.AddCommand(createCmd) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/create_client.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/bwmarrin/snowflake" 5 | "github.com/jinzhu/gorm" 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | 9 | "github.com/meowpub/meow/config" 10 | "github.com/meowpub/meow/models" 11 | ) 12 | 13 | // createClientCmd represents the createClient command 14 | var createClientCmd = &cobra.Command{ 15 | Use: "client", 16 | Short: "Creates an OAuth client application", 17 | Long: `Creates an OAuth client application.`, 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | // Collect flags upfront. 20 | name := viper.GetString("create.client.name") 21 | desc := viper.GetString("create.client.description") 22 | ownerID := snowflake.ID(viper.GetInt64("create.client.owner-id")) 23 | redirect := viper.GetString("create.client.redirect-uri") 24 | 25 | // Create a client object. 26 | clData := models.ClientUserData{ 27 | Name: name, 28 | Description: desc, 29 | } 30 | if ownerID != 0 { 31 | clData.OwnerID = &ownerID 32 | } 33 | cl, err := models.NewClient(clData, redirect) 34 | if err != nil { 35 | return err 36 | } 37 | dump(cl) 38 | 39 | // Connect to the database. 40 | db, err := gorm.Open("postgres", config.DB()) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | // Insert the objects inside a transaction. 46 | tx := db.Begin() 47 | if err := models.NewClientStore(db).Create(cl); err != nil { 48 | return err 49 | } 50 | return tx.Commit().Error 51 | }, 52 | } 53 | 54 | func init() { 55 | createCmd.AddCommand(createClientCmd) 56 | 57 | createClientCmd.Flags().StringP("name", "n", "", "human-readable name (required)") 58 | createClientCmd.Flags().StringP("description", "d", "", "human-readable description") 59 | createClientCmd.Flags().Int64P("owner-id", "O", 0, "ID of the owning user") 60 | createClientCmd.Flags().StringP("redirect-uri", "r", "", "redirect URI (required)") 61 | bindPFlags("create.client", createClientCmd.Flags()) 62 | } 63 | -------------------------------------------------------------------------------- /cmd/create_user.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | "go.uber.org/zap" 11 | "golang.org/x/crypto/ssh/terminal" 12 | 13 | "github.com/meowpub/meow/config" 14 | "github.com/meowpub/meow/models" 15 | ) 16 | 17 | var createUserCmd = &cobra.Command{ 18 | Use: "user id email", 19 | Short: "Create a user", 20 | Long: "Create a user.", 21 | Args: cobra.MinimumNArgs(2), 22 | RunE: func(cmd *cobra.Command, args []string) error { 23 | L := zap.L().Named("create.user") 24 | 25 | db, err := openDB(L) 26 | if err != nil { 27 | return err 28 | } 29 | stores := models.NewStores(db, nil, config.RedisKeyspace()) 30 | 31 | id := args[0] 32 | email := args[1] 33 | noPasswd := viper.GetBool("create.user.no-passwd") 34 | 35 | // Look up the user's profile; it should've been created with `meow gen entity | meow ingest' already. 36 | profile, err := stores.Entities().GetByID(id) 37 | if err != nil { 38 | if models.IsNotFound(err) { 39 | return errors.Wrap(err, "please create the user's profile with 'meow gen entity as:Person' and 'meow ingest' first") 40 | } 41 | return errors.Wrap(err, "couldn't look up profile") 42 | } 43 | 44 | // Read the password from stdin. 45 | var password string 46 | if !noPasswd { 47 | fmt.Fprint(os.Stderr, "Password: ") 48 | pass, err := terminal.ReadPassword(int(os.Stdin.Fd())) 49 | if err != nil { 50 | return errors.Wrap(err, "couldn't read password from stdin") 51 | } 52 | password = string(pass) 53 | } 54 | 55 | // Create a user! 56 | user, err := models.NewUser(profile.ID, email, password) 57 | if err != nil { 58 | return err 59 | } 60 | return stores.Users().Save(user) 61 | }, 62 | } 63 | 64 | func init() { 65 | createCmd.AddCommand(createUserCmd) 66 | 67 | createUserCmd.Flags().BoolP("no-passwd", "N", false, "don't set a password; disallows login") 68 | bindPFlags("create.user", createUserCmd.Flags()) 69 | } 70 | -------------------------------------------------------------------------------- /cmd/gen.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | // genCmd represents the gen command 8 | var genCmd = &cobra.Command{ 9 | Use: "gen", 10 | Short: "Generate things", 11 | Long: `Generate things from the commandline. See subcommands.`, 12 | } 13 | 14 | func init() { 15 | rootCmd.AddCommand(genCmd) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/gen_entity.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/meowpub/meow/ld" 9 | "github.com/meowpub/meow/ld/ns" 10 | "github.com/meowpub/meow/lib" 11 | "github.com/pkg/errors" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | // genEntityCmd represents the generate entity command 17 | var genEntityCmd = &cobra.Command{ 18 | Use: "entity id ns:Type [ns:Type...]", 19 | Short: "Generate JSON-LD entities", 20 | Long: `Generate JSON-LD entities from the commandline.`, 21 | Args: cobra.MinimumNArgs(2), 22 | RunE: func(cmd *cobra.Command, args []string) error { 23 | id := args[0] 24 | shortTypes := args[1:] 25 | 26 | // URLs must be normalised before going in the database. 27 | u, err := url.Parse(id) 28 | if err != nil { 29 | return err 30 | } 31 | nu := lib.NormalizeURL(*u) 32 | id = nu.String() 33 | 34 | // Types are passed as "ns:Type", eg. "as:Note". 35 | longTypes := make([]string, len(shortTypes)) 36 | for i, short := range shortTypes { 37 | long, err := resolveShortType(short) 38 | if err != nil { 39 | return err 40 | } 41 | longTypes[i] = long 42 | } 43 | 44 | obj := &ld.Object{V: map[string]interface{}{ 45 | "@id": id, 46 | "@type": longTypes, 47 | }} 48 | 49 | // All attributes we're aware of are exposed as flags; as:content -> --as-content=... 50 | // These default to being strings, but can be explicitly typed with "type=value". 51 | for _, n := range ns.Namespaces { 52 | for _, p := range n.Props { 53 | raw := viper.GetStringSlice("create.entity." + n.Short + "-" + p.Short) 54 | if len(raw) == 0 { 55 | continue 56 | } 57 | vs := make([]interface{}, len(raw)) 58 | for i, s := range raw { 59 | v, err := parseTypedValue(s) 60 | if err != nil { 61 | return errors.Wrap(err, n.ID) 62 | } 63 | vs[i] = v 64 | } 65 | obj.V[p.ID] = vs 66 | } 67 | } 68 | 69 | dump(obj) 70 | 71 | return nil 72 | }, 73 | } 74 | 75 | func parseTypedValue(s string) (interface{}, error) { 76 | parts := strings.SplitN(s, "=", 2) 77 | typ := "" 78 | val := parts[0] 79 | if len(parts) > 1 { 80 | typ = parts[0] 81 | val = parts[1] 82 | } 83 | 84 | switch typ { 85 | case "", "s", "str": 86 | return ld.Value(val), nil 87 | case "i", "int": 88 | i, err := strconv.ParseInt(val, 10, 64) 89 | return ld.Value(i), err 90 | case "id": 91 | return ld.ID(val), nil 92 | default: 93 | return nil, errors.Errorf("unknown type: %s", typ) 94 | } 95 | } 96 | 97 | func resolveShortType(s string) (string, error) { 98 | parts := strings.SplitN(s, ":", 2) 99 | if len(parts) != 2 { 100 | return "", errors.Errorf("type must be in ns:Type form (eg. as:Note): %s", s) 101 | } 102 | nName := parts[0] 103 | tName := parts[1] 104 | for _, n := range ns.Namespaces { 105 | if n.Short != nName { 106 | continue 107 | } 108 | for _, t := range n.Types { 109 | if t.Short == tName { 110 | return t.ID, nil 111 | } 112 | } 113 | return "", errors.Errorf("unknown type: %s%s", n.ID, tName) 114 | } 115 | return "", errors.Errorf("unknown namespace: %s", nName) 116 | } 117 | 118 | func init() { 119 | genCmd.AddCommand(genEntityCmd) 120 | 121 | // Every dang attribute we know about is a commandline flag. 122 | // This is dumb, but I can't think of anything better that doesn't involve throwing 123 | // multityping under a bus or typing full attribute names, which is just cursed. 124 | for _, n := range ns.Namespaces { 125 | for _, p := range n.Props { 126 | genEntityCmd.Flags().StringSlice(n.Short+"-"+p.Short, nil, p.Comment) 127 | } 128 | } 129 | bindPFlags("create.entity", genEntityCmd.Flags()) 130 | } 131 | -------------------------------------------------------------------------------- /cmd/gen_entity_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_resolveShortType(t *testing.T) { 11 | t.Run("as.Note", func(t *testing.T) { 12 | full, err := resolveShortType("as:Note") 13 | assert.NoError(t, err) 14 | assert.Equal(t, "http://www.w3.org/ns/activitystreams#Note", full) 15 | }) 16 | t.Run("as2.Note", func(t *testing.T) { 17 | _, err := resolveShortType("as2:Note") 18 | assert.EqualError(t, err, "unknown namespace: as2") 19 | }) 20 | t.Run("as.Notee", func(t *testing.T) { 21 | _, err := resolveShortType("as:Notee") 22 | assert.EqualError(t, err, "unknown type: http://www.w3.org/ns/activitystreams#Notee") 23 | }) 24 | } 25 | 26 | func Test_parseTypedValue(t *testing.T) { 27 | testdata := map[string]map[string]interface{}{ 28 | "a string": {"@value": "a string"}, 29 | } 30 | for s, out := range testdata { 31 | t.Run(s, func(t *testing.T) { 32 | v, err := parseTypedValue(s) 33 | require.NoError(t, err) 34 | assert.Equal(t, out, v) 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cmd/ingest.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io/ioutil" 7 | "os" 8 | 9 | "github.com/meowpub/meow/ld" 10 | "github.com/meowpub/meow/lib" 11 | "github.com/meowpub/meow/models" 12 | "github.com/spf13/cobra" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | var ingestCmd = &cobra.Command{ 17 | Use: "ingest", 18 | Short: "Ingest JSON from stdin", 19 | Long: "Ingest JSON from stdin", 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | L := zap.L().Named("ingest") 22 | 23 | // Read object from stdin. 24 | indata, err := ioutil.ReadAll(os.Stdin) 25 | if err != nil { 26 | return err 27 | } 28 | var kv map[string]interface{} 29 | if err := json.Unmarshal(indata, &kv); err != nil { 30 | return err 31 | } 32 | kv, err = ld.Expand(context.Background(), kv, "") 33 | if err != nil { 34 | return err 35 | } 36 | obj := ld.Object{V: kv} 37 | dump(obj) 38 | 39 | // Entities ingested this way are always ObjectEntities. 40 | // (Add a flag to say otherwise if that ever becomes useful.) 41 | e := &models.Entity{ 42 | ID: lib.GenSnowflake(), 43 | Kind: models.ObjectEntity, 44 | Obj: &obj, 45 | } 46 | 47 | // Connect to the database. 48 | db, err := openDB(L) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | // Insert the objects inside a transaction. 54 | tx := db.Begin() 55 | if err := models.NewEntityStore(db).Save(e); err != nil { 56 | return err 57 | } 58 | return tx.Commit().Error 59 | }, 60 | } 61 | 62 | func init() { 63 | rootCmd.AddCommand(ingestCmd) 64 | } 65 | -------------------------------------------------------------------------------- /cmd/migrate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/mattes/migrate" 12 | "github.com/pkg/errors" 13 | "github.com/spf13/cobra" 14 | "go.uber.org/multierr" 15 | 16 | "github.com/meowpub/meow/config" 17 | ) 18 | 19 | func migrationsDir() string { 20 | wd, _ := os.Getwd() 21 | return path.Join(wd, "migrations") 22 | } 23 | 24 | func newMigrate() (*migrate.Migrate, error) { 25 | return migrate.New("file://"+migrationsDir(), config.DB()) 26 | } 27 | 28 | // Helper to make error handling from migrate.Migrate.Close() cleaner. 29 | func closeAfterMigrate(m *migrate.Migrate, err error) error { 30 | srcerr, dberr := m.Close() 31 | return multierr.Combine(err, srcerr, dberr) 32 | } 33 | 34 | // migrateCmd represents the migrate command 35 | var migrateCmd = &cobra.Command{ 36 | Use: "migrate", 37 | Short: "Run database migrations", 38 | Long: `Run database migrations.`, 39 | } 40 | 41 | // migrateCreateCmd represents the migrate create command 42 | var migrateCreateCmd = &cobra.Command{ 43 | Use: "create NAME", 44 | Short: "Create a new migration", 45 | Long: `Create a new migration.`, 46 | Args: cobra.ExactArgs(1), 47 | RunE: func(cmd *cobra.Command, args []string) error { 48 | dir := migrationsDir() 49 | version := strconv.FormatInt(time.Now().Unix(), 10) 50 | name := args[0] 51 | upPath := path.Join(dir, fmt.Sprintf("%s_%s.up.sql", version, name)) 52 | downPath := path.Join(dir, fmt.Sprintf("%s_%s.down.sql", version, name)) 53 | 54 | text := fmt.Sprintf("--\n-- version: %s\n--\n\nBEGIN;\n\n--\n-- TODO: Add your SQL!\n--\n\nCOMMIT;\n", version) 55 | if err := ioutil.WriteFile(upPath, []byte(text), 0644); err != nil { 56 | return err 57 | } 58 | if err := ioutil.WriteFile(downPath, []byte(text), 0644); err != nil { 59 | return err 60 | } 61 | return nil 62 | }, 63 | } 64 | 65 | // migrateUpCmd represents the migrate up command 66 | var migrateUpCmd = &cobra.Command{ 67 | Use: "up [steps]", 68 | Short: "Apply migrations", 69 | Long: `Apply migrations.`, 70 | Args: cobra.MaximumNArgs(1), 71 | RunE: func(cmd *cobra.Command, args []string) error { 72 | m, err := newMigrate() 73 | if err != nil { 74 | return err 75 | } 76 | if len(args) > 0 { 77 | steps, err := strconv.Atoi(args[0]) 78 | if err != nil { 79 | return err 80 | } 81 | return closeAfterMigrate(m, m.Steps(steps)) 82 | } 83 | return closeAfterMigrate(m, m.Up()) 84 | }, 85 | } 86 | 87 | // migrateDownCmd represents the migrate down command 88 | var reallyMigrateDown = false 89 | var migrateDownCmd = &cobra.Command{ 90 | Use: "down [steps]", 91 | Short: "Revert migrations", 92 | Long: `Revert migrations.`, 93 | Args: cobra.MaximumNArgs(1), 94 | RunE: func(cmd *cobra.Command, args []string) error { 95 | m, err := newMigrate() 96 | if err != nil { 97 | return err 98 | } 99 | if len(args) > 0 { 100 | steps, err := strconv.Atoi(args[0]) 101 | if err != nil { 102 | return err 103 | } 104 | return closeAfterMigrate(m, m.Steps(-steps)) 105 | } 106 | if !reallyMigrateDown { 107 | return errors.New("this will revert every migration, pass a number of migrations (eg. `migrate down 1`) to revert only a few steps, or --please-delete-my-data if you really meant to do that") 108 | } 109 | return closeAfterMigrate(m, m.Down()) 110 | }, 111 | } 112 | 113 | // migrateForceCommand represents the migrate force command 114 | var migrateForceCommand = &cobra.Command{ 115 | Use: "force VERSION", 116 | Short: "Force set version, eg. after a failed migration", 117 | Long: `Force set version, eg. after a failed migration.`, 118 | Args: cobra.ExactArgs(1), 119 | RunE: func(cmd *cobra.Command, args []string) error { 120 | m, err := newMigrate() 121 | if err != nil { 122 | return err 123 | } 124 | ver, err := strconv.Atoi(args[0]) 125 | if err != nil { 126 | return err 127 | } 128 | return closeAfterMigrate(m, m.Force(ver)) 129 | }, 130 | } 131 | 132 | // migrateVersionCommand represents the migrate version command 133 | var migrateVersionCommand = &cobra.Command{ 134 | Use: "version", 135 | Short: "Print the current database version", 136 | Long: `Print the current database version.`, 137 | Args: cobra.NoArgs, 138 | RunE: func(cmd *cobra.Command, args []string) error { 139 | m, err := newMigrate() 140 | if err != nil { 141 | return err 142 | } 143 | ver, dirty, err := m.Version() 144 | fmt.Printf("%d (dirty: %v)\n", ver, dirty) 145 | return closeAfterMigrate(m, err) 146 | }, 147 | } 148 | 149 | // migrateDropCmd represents the migrate drop command 150 | var reallyMigrateDrop = false 151 | var migrateDropCmd = &cobra.Command{ 152 | Use: "drop", 153 | Short: "Drop all tables in the database", 154 | Long: `Drop all tables in the database.`, 155 | Args: cobra.NoArgs, 156 | RunE: func(cmd *cobra.Command, args []string) error { 157 | if !reallyMigrateDrop { 158 | return errors.New("this will drop all tables in your entire database, pass --please-delete-my-data if you really meant to do that") 159 | } 160 | m, err := newMigrate() 161 | if err != nil { 162 | return err 163 | } 164 | return closeAfterMigrate(m, m.Drop()) 165 | }, 166 | } 167 | 168 | func init() { 169 | rootCmd.AddCommand(migrateCmd) 170 | 171 | migrateCmd.AddCommand(migrateCreateCmd) 172 | 173 | migrateCmd.AddCommand(migrateUpCmd) 174 | 175 | migrateCmd.AddCommand(migrateDownCmd) 176 | migrateDownCmd.Flags().BoolVar(&reallyMigrateDown, "please-delete-my-data", false, "yes, I really meant to undo everything") 177 | 178 | migrateCmd.AddCommand(migrateForceCommand) 179 | 180 | migrateCmd.AddCommand(migrateVersionCommand) 181 | 182 | migrateCmd.AddCommand(migrateDropCmd) 183 | migrateDropCmd.Flags().BoolVar(&reallyMigrateDrop, "please-delete-my-data", false, "yes, I really want to do this") 184 | } 185 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "github.com/fatih/color" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | "go.uber.org/zap" 13 | "go.uber.org/zap/zapcore" 14 | 15 | "github.com/meowpub/meow/config" 16 | ) 17 | 18 | var rootCmd = &cobra.Command{ 19 | Use: "meow", 20 | Short: "A cuter ActivityPub server", 21 | Long: `A cuter ActivityPub server.`, 22 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 23 | // Set up the global logger. 24 | lconf := zap.NewDevelopmentConfig() 25 | if config.IsProd() { 26 | lconf = zap.NewProductionConfig() 27 | } else { 28 | lconf.EncoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { 29 | enc.AppendString(fmt.Sprintf("%02d:%02d:%02d", t.Hour(), t.Minute(), t.Second())) 30 | } 31 | lconf.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 32 | } 33 | l, err := lconf.Build() 34 | if err != nil { 35 | return err 36 | } 37 | zap.RedirectStdLog(l) 38 | zap.ReplaceGlobals(l) 39 | 40 | // Disable colours globally if asked. 41 | if config.NoColour() { 42 | color.NoColor = true 43 | } 44 | 45 | return nil 46 | }, 47 | } 48 | 49 | // Execute adds all child commands to the root command and sets flags appropriately. 50 | // This is called by main.main(). It only needs to happen once to the rootCmd. 51 | func Execute() { 52 | if err := rootCmd.Execute(); err != nil { 53 | fmt.Println(err) 54 | os.Exit(1) 55 | } 56 | } 57 | 58 | func init() { 59 | cobra.OnInitialize(initConfig) 60 | 61 | rootCmd.PersistentFlags().BoolP("prod", "P", false, "run in production mode") 62 | rootCmd.PersistentFlags().StringP("secret", "S", "", "secret key (base64, min 64 bytes)") 63 | rootCmd.PersistentFlags().Int64("node-id", 0, "node id used for snowflake generation") 64 | rootCmd.PersistentFlags().String("db", "postgres:///meow?sslmode=disable", "database connection uri") 65 | rootCmd.PersistentFlags().String("redis", "redis://localhost:6379/0", "redis connection uri") 66 | rootCmd.PersistentFlags().String("redis-keyspace", "meow", "prepended (+ a ':') to all redis keys") 67 | 68 | rootCmd.PersistentFlags().String("highlight-style", "vim", "style used for syntax highlighting") 69 | rootCmd.PersistentFlags().Bool("no-colour", false, "disable all colours in the output") 70 | 71 | viper.BindPFlags(rootCmd.PersistentFlags()) 72 | } 73 | 74 | // initConfig reads in config file and ENV variables if set. 75 | func initConfig() { 76 | viper.SetEnvPrefix("meow") 77 | viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) 78 | viper.AutomaticEnv() 79 | } 80 | -------------------------------------------------------------------------------- /cmd/serve.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/monzo/typhon" 11 | "github.com/pkg/errors" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | "go.uber.org/zap" 15 | 16 | "github.com/meowpub/meow/api" 17 | "github.com/meowpub/meow/config" 18 | "github.com/meowpub/meow/config/secrets" 19 | "github.com/meowpub/meow/lib" 20 | ) 21 | 22 | // serveCmd represents the api command 23 | var serveCmd = &cobra.Command{ 24 | Use: "serve", 25 | Aliases: []string{"s"}, 26 | Short: "Runs a web worker", 27 | Long: `Runs a web worker.`, 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | L := zap.L().Named("serve") 30 | 31 | addr := viper.GetString("api.addr") 32 | domains := viper.GetStringSlice("api.domain") 33 | 34 | // We need to know what domains are local, because it changes some semantics. 35 | if len(domains) == 0 { 36 | return errors.Errorf("no domains configured: specify -d/--domain, eg. `meow s -d localhost`") 37 | } 38 | 39 | // Derive subkeys from the master secret; this has to be done before use. 40 | if err := secrets.Init(L.Named("secrets"), config.Secret()); err != nil { 41 | return errors.Wrap(err, "secrets.Init") 42 | } 43 | 44 | // Create a context that is cancelled on Ctrl+C. 45 | ctx, cancel := context.WithCancel(context.Background()) 46 | defer cancel() 47 | 48 | go func() { 49 | sigC := make(chan os.Signal, 1) 50 | signal.Notify(sigC, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 51 | <-sigC 52 | signal.Stop(sigC) 53 | cancel() 54 | }() 55 | 56 | // Connect to the database + redis, disconnect when the context exits. 57 | db, err := openDB(L.Named("db")) 58 | if err != nil { 59 | return err 60 | } 61 | defer func() { lib.Report(ctx, db.Close(), "couldn't cleanly close database connection") }() 62 | 63 | r, err := openRedis(L.Named("redis")) 64 | if err != nil { 65 | return err 66 | } 67 | defer func() { lib.Report(ctx, r.Close(), "couldn't cleanly close redis connection") }() 68 | 69 | // Listen! 70 | svc := api.Service. 71 | Filter(api.StoresFilter(db, r)). 72 | Filter(typhon.ExpirationFilter). 73 | Filter(api.PanicFilter). 74 | Filter(api.ErrorFilter). 75 | Filter(typhon.H2cFilter) 76 | srv, err := typhon.Listen(svc, addr) 77 | if err != nil { 78 | return err 79 | } 80 | L.Info("Listening!", zap.String("addr", addr), zap.Strings("domains", domains)) 81 | 82 | // Stop that when the context exits, eg. on Ctrl+C. 83 | <-ctx.Done() 84 | cancelCtx, cancelCancel := context.WithTimeout(context.Background(), 10*time.Second) 85 | defer cancelCancel() 86 | srv.Stop(cancelCtx) 87 | return nil 88 | }, 89 | } 90 | 91 | func init() { 92 | rootCmd.AddCommand(serveCmd) 93 | 94 | serveCmd.Flags().StringSliceP("domain", "d", nil, "domain(s) to serve on (eg. \"example.com\")") 95 | serveCmd.Flags().StringP("addr", "a", "localhost:8000", "address to listen on") 96 | bindPFlags("api", serveCmd.Flags()) 97 | } 98 | -------------------------------------------------------------------------------- /cmd/util.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/alecthomas/chroma/quick" 10 | "github.com/fatih/color" 11 | "github.com/go-redis/redis" 12 | "github.com/jinzhu/gorm" 13 | "github.com/spf13/pflag" 14 | "github.com/spf13/viper" 15 | "go.uber.org/multierr" 16 | "go.uber.org/zap" 17 | 18 | "github.com/meowpub/meow/config" 19 | ) 20 | 21 | func openDB(L *zap.Logger) (*gorm.DB, error) { 22 | addr := config.DB() 23 | L.Info("Connecting to Postgres...", zap.String("addr", addr)) 24 | 25 | return gorm.Open("postgres", addr) 26 | } 27 | 28 | func openRedis(L *zap.Logger) (*redis.Client, error) { 29 | addr := config.Redis() 30 | L.Info("Connecting to Redis...", zap.String("addr", addr)) 31 | 32 | opts, err := redis.ParseURL(addr) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return redis.NewClient(opts), nil 37 | } 38 | 39 | func bindPFlags(pfx string, flags *pflag.FlagSet) error { 40 | var errs []error 41 | flags.VisitAll(func(flag *pflag.Flag) { 42 | key := flag.Name 43 | if pfx != "" { 44 | key = pfx + "." + key 45 | } 46 | if err := viper.BindPFlag(key, flag); err != nil { 47 | errs = append(errs, err) 48 | } 49 | }) 50 | return multierr.Combine(errs...) 51 | } 52 | 53 | func dump(v interface{}) { 54 | data, err := json.MarshalIndent(v, "", " ") 55 | if err != nil { 56 | _, _ = fmt.Fprintf(os.Stderr, "ERROR: %v", err) 57 | return 58 | } 59 | if !color.NoColor { 60 | var buf bytes.Buffer 61 | if err := quick.Highlight(&buf, string(data), "json", "terminal", config.HighlightStyle()); err != nil { 62 | _, _ = fmt.Fprintf(os.Stderr, "ERROR: %v", err) 63 | } 64 | data = buf.Bytes() 65 | } 66 | _, _ = fmt.Println(string(data)) 67 | } 68 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | ) 6 | 7 | // IsProd returns whether we're running in production mode. 8 | func IsProd() bool { 9 | return viper.GetBool("prod") 10 | } 11 | 12 | // Secret returns the master secret, as a hex string. 13 | // Do not use this key directly; use config/secrets to generate per-task derivative keys. 14 | func Secret() string { 15 | return viper.GetString("secret") 16 | } 17 | 18 | // DB returns the database connection string. 19 | func DB() string { 20 | return viper.GetString("db") 21 | } 22 | 23 | // Redis returns the redis connection string. 24 | func Redis() string { 25 | return viper.GetString("redis") 26 | } 27 | 28 | // RedisKeyspace returns the keyspace for redis, eg. "meow" will prefix all redis keys with "meow:". 29 | func RedisKeyspace() string { 30 | return viper.GetString("redis-keyspace") 31 | } 32 | 33 | // NodeID returns the node ID used for distributed snowflake generation. 34 | func NodeID() int64 { 35 | return viper.GetInt64("node-id") 36 | } 37 | 38 | // Syntax style for JSON output. 39 | func HighlightStyle() string { 40 | return viper.GetString("highlight-style") 41 | } 42 | 43 | // Disable colour in the CLI. 44 | func NoColour() bool { 45 | return viper.GetBool("no-colour") 46 | } 47 | -------------------------------------------------------------------------------- /config/secrets/init.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "crypto/sha512" 5 | "encoding/base64" 6 | "strings" 7 | "time" 8 | 9 | "github.com/pkg/errors" 10 | "go.uber.org/zap" 11 | "golang.org/x/sync/errgroup" 12 | ) 13 | 14 | const minMasterKeyLength = 64 15 | 16 | var ( 17 | // Populated from other files by init functions. 18 | subkeys []*subkey 19 | 20 | // Changing this will invalidate all old keys, pls don't without a good reason. 21 | hsh = sha512.New 22 | ) 23 | 24 | // Returns a registered subkey. We're breaking the usual register() pattern here just to be 25 | // absolutely certain we're not forgetting to register a key for initialization, and to make sure 26 | // that if new fields are added for whatever reason, they're actually updated everywhere. 27 | func declare(name string, length int) *subkey { 28 | k := &subkey{Name: name, Length: length} 29 | subkeys = append(subkeys, k) 30 | return k 31 | } 32 | 33 | // Derives secrets from a master key. 34 | func Init(L *zap.Logger, masterKeyStr string) error { 35 | // Strip whitespace, make sure the master key is non-blank. 36 | masterKeyStr = strings.TrimSpace(masterKeyStr) 37 | if masterKeyStr == "" { 38 | return errors.New("you must provide a secret key; you can generate one with `openssl rand -base64 64`") 39 | } 40 | 41 | // Decode and validate the master key. 42 | masterKey, err := base64.StdEncoding.DecodeString(masterKeyStr) 43 | if err != nil { 44 | return errors.Wrapf(err, "secret must be a base64-encoded string with no newlines, try `openssl rand -base64 64`") 45 | } 46 | if len(masterKey) < minMasterKeyLength { 47 | return errors.Errorf("provided secret is too short (%d, at least %d required), try `openssl rand -base64 64`", len(masterKey), minMasterKeyLength) 48 | } 49 | if isZero(masterKey) { 50 | return errors.New("secret can't be all zeroes, try `openssl rand -base64 64`") 51 | } 52 | 53 | // Use an error group to derive subkeys in parallel, making sure we track time taken. 54 | L.Debug("Deriving subkeys from master key, this may take a moment...", 55 | zap.Int("len", len(masterKey)), 56 | ) 57 | 58 | startTime := time.Now() 59 | var g errgroup.Group 60 | for _, k := range subkeys { 61 | // Derive each key in a goroutine, tracking time taken per key as well. 62 | k := k 63 | g.Go(func() error { 64 | l := L.With( 65 | zap.String("key", k.Name), 66 | zap.Int("len", k.Length), 67 | ) 68 | 69 | startTime := time.Now() 70 | if err := k.init(hsh, masterKey); err != nil { 71 | l.Error("Subkey derivation failed", 72 | zap.Error(err), 73 | zap.Duration("t", time.Since(startTime)), 74 | ) 75 | return errors.Wrap(err, k.Name) 76 | } 77 | 78 | l.Debug("Subkey derived", 79 | zap.Duration("t", time.Since(startTime)), 80 | // zap.Binary("key", k.value), 81 | ) 82 | return nil 83 | }) 84 | } 85 | if err := g.Wait(); err != nil { 86 | L.Error("One or more subkeys couldn't be derived", 87 | zap.Error(err), 88 | zap.Duration("t", time.Since(startTime)), 89 | ) 90 | } 91 | return nil 92 | } 93 | 94 | // Returns whether a byte slice is nil, zero-length or contains nothing but zero bytes. 95 | func isZero(buf []byte) bool { 96 | for _, b := range buf { 97 | if b != 0 { 98 | return false 99 | } 100 | } 101 | return true 102 | } 103 | -------------------------------------------------------------------------------- /config/secrets/secrets.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | var sessionKey = declare("session", 64) 4 | 5 | func SessionKey() []byte { return sessionKey.access() } 6 | 7 | var csrfKey = declare("csrf", 64) 8 | 9 | func CSRFKey() []byte { return csrfKey.access() } 10 | 11 | var webClientSecret = declare("web", 64) 12 | 13 | func WebClientSecret() []byte { return webClientSecret.access() } 14 | -------------------------------------------------------------------------------- /config/secrets/subkey.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "hash" 5 | "io" 6 | 7 | "github.com/pkg/errors" 8 | "golang.org/x/crypto/hkdf" 9 | ) 10 | 11 | type subkey struct { 12 | Name string 13 | Length int 14 | 15 | value []byte 16 | } 17 | 18 | // Derives a value for this subkey from the master key. 19 | func (k *subkey) init(hsh func() hash.Hash, masterKey []byte) error { 20 | // Use HKDF to derive a subkey from the master key. 21 | r := hkdf.New(hsh, masterKey, nil, []byte(k.Name)) 22 | key := make([]byte, k.Length) 23 | if _, err := io.ReadFull(r, key); err != nil { 24 | return err 25 | } 26 | 27 | // Sanity check the heck out of it to be safe. 28 | if len(key) != k.Length { 29 | return errors.Errorf("derived key has the wrong length: %d != %d", len(key), k.Length) 30 | } 31 | if isZero(key) { 32 | return errors.New("derived key is all zeroes?????") 33 | } 34 | 35 | // Only now export the key. 36 | k.value = key 37 | return nil 38 | } 39 | 40 | // Returns the value, or panics if it's uninitialized or invalid in some way. 41 | func (k *subkey) access() []byte { 42 | if isZero(k.value) { 43 | panic(k.Name + ": key uninitialized; did you call secrets.Init() properly?") 44 | } 45 | if len(k.value) != k.Length { 46 | panic(k.Name + ": key has an invalid length, what the heck did you do???") 47 | } 48 | return k.value 49 | } 50 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | redis: 4 | image: redis:5 5 | ports: 6 | - "36379:6379" 7 | postgres: 8 | image: postgres:12 9 | environment: 10 | POSTGRES_USER: meow 11 | POSTGRES_PASSWORD: meow 12 | ports: 13 | - "35432:5432" 14 | -------------------------------------------------------------------------------- /fixtures/localhost-jane.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "@type": "Person", 4 | "@id": "https://localhost/~jane", 5 | "inbox": "https://localhost/~jane/-inbox", 6 | "outbox": "https://localhost/~jane/-outbox", 7 | "liked": "https://localhost/~jane/-liked", 8 | "following": "https://localhost/~jane/-following", 9 | "followers": "https://localhost/~jane/-followers", 10 | "preferredUsername": "jane", 11 | "name": "Jane Smith", 12 | "summary": "meowpub test user" 13 | } 14 | -------------------------------------------------------------------------------- /fixtures/localhost.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "@type": "Service", 4 | "@id": "https://localhost", 5 | "name": "Test Domain" 6 | } 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/meowpub/meow 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/GeertJohan/go.rice v1.0.0 7 | github.com/RangelReale/osin v1.0.1 8 | github.com/alecthomas/chroma v0.7.0 9 | github.com/bwmarrin/snowflake v0.3.0 10 | github.com/fatih/color v1.8.0 11 | github.com/go-playground/form v3.1.4+incompatible 12 | github.com/go-redis/redis v6.15.6+incompatible 13 | github.com/golang/mock v1.3.1 14 | github.com/gorilla/sessions v1.2.0 15 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 16 | github.com/jinzhu/gorm v1.9.11 17 | github.com/joho/godotenv v1.3.0 18 | github.com/keybase/saltpack v0.0.0-20190828020936-3f47e8e2e6ec 19 | github.com/mattes/migrate v3.0.1+incompatible 20 | github.com/pborman/uuid v1.2.0 // indirect 21 | github.com/piprate/json-gold v0.2.0 22 | github.com/pkg/errors v0.8.1 23 | github.com/spf13/cobra v0.0.5 24 | github.com/spf13/pflag v1.0.5 25 | github.com/spf13/viper v1.6.1 26 | github.com/unrolled/render v1.0.1 27 | go.uber.org/multierr v1.4.0 28 | go.uber.org/zap v1.13.0 29 | golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 30 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e 31 | ) 32 | -------------------------------------------------------------------------------- /ingress/ingest.go: -------------------------------------------------------------------------------- 1 | package ingress 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/meowpub/meow/ingress/rules" 7 | "github.com/meowpub/meow/ld" 8 | "github.com/meowpub/meow/ld/ns/as" 9 | "github.com/meowpub/meow/models" 10 | ) 11 | 12 | // Ingests an Entity posted to an Inbox (S2S) or Outbox (C2S), as directed by `src`. 13 | // The caller is responsible for validating the actor's identity by whatever means. 14 | func Ingest(ctx context.Context, stores models.Stores, src ld.Source, actor, stream *models.Entity, e ld.Entity) error { 15 | activity := as.AsActivity(e) 16 | 17 | // Rules may reject or modify submitted activities, and broadly have two roles: 18 | // - S2S: Filtering out unwanted content, eg. federation black/whitelists. 19 | // - C2S: Validation, generating IDs, wrapping plain objects in Create objects. 20 | if err := rules.Run(ctx, rules.Rules, src, actor, stream, &activity); err != nil { 21 | return err 22 | } 23 | 24 | // TODO: Rule-like system for "side effects", which is what the spec calls things like 25 | // a Create activity creating the contained object, or a Delete activity deleting it. 26 | 27 | entity := models.NewEntityFrom(models.ObjectEntity, activity) 28 | return stores.Entities().Save(entity) 29 | } 30 | -------------------------------------------------------------------------------- /ingress/ingest_test.go: -------------------------------------------------------------------------------- 1 | package ingress 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golang/mock/gomock" 8 | "github.com/meowpub/meow/ld" 9 | "github.com/meowpub/meow/ld/ns/as" 10 | "github.com/meowpub/meow/models" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestIngest(t *testing.T) { 16 | t.Run("Create", func(t *testing.T) { 17 | actor := models.NewEntityFrom(models.ObjectEntity, 18 | as.NewPerson("https://remote.com/~jdoe")) 19 | stream := models.NewEntityFrom(models.ObjectEntity, 20 | as.NewPerson("https://example.com/~jsmith/inbox")) 21 | 22 | note := as.NewNote("https://example.com/lorem-ipsum") 23 | note.SetContent(ld.Value("lorem ipsum dolor sit amet")) 24 | create := as.NewCreate("https://example.com/lorem-ipsum/outbox/create") 25 | create.SetObject(note) 26 | 27 | for _, src := range []ld.Source{ld.ClientToServer, ld.ServerToServer} { 28 | t.Run(src.String(), func(t *testing.T) { 29 | ctrl := gomock.NewController(t) 30 | defer ctrl.Finish() 31 | 32 | stores := models.NewMockStores(ctrl) 33 | stores.EntityStore = models.NewMockEntityStore(ctrl) 34 | stores.EntityStore.EXPECT().Save(gomock.Any()).Do(func(e *models.Entity) { 35 | expected := models.NewEntityFrom(models.ObjectEntity, create) 36 | expected.ID = e.ID 37 | assert.Equal(t, expected, e) 38 | }) 39 | 40 | require.NoError(t, Ingest(context.Background(), stores, src, actor, stream, create)) 41 | }) 42 | } 43 | }) 44 | 45 | t.Run("Object", func(t *testing.T) { 46 | actor := models.NewEntityFrom(models.ObjectEntity, 47 | as.NewPerson("https://example.com/~jsmith")) 48 | stream := models.NewEntityFrom(models.ObjectEntity, 49 | as.NewPerson("https://example.com/~jsmith/inbox")) 50 | 51 | note := as.NewNote("https://example.com/lorem-ipsum") 52 | note.SetContent(ld.Value("lorem ipsum dolor sit amet")) 53 | 54 | t.Run("ServerToServer", func(t *testing.T) { 55 | require.EqualError(t, Ingest(context.Background(), nil, ld.ServerToServer, actor, stream, note), "@type=[[http://www.w3.org/ns/activitystreams#Note]] does not include required type (or a known subtype thereof): http://www.w3.org/ns/activitystreams#Activity") 56 | }) 57 | 58 | t.Run("ClientToServer", func(t *testing.T) { 59 | ctrl := gomock.NewController(t) 60 | defer ctrl.Finish() 61 | 62 | stores := models.NewMockStores(ctrl) 63 | stores.EntityStore = models.NewMockEntityStore(ctrl) 64 | stores.EntityStore.EXPECT().Save(gomock.Any()).Do(func(e *models.Entity) { 65 | ecreate := as.AsCreate(e.Obj) 66 | assert.Equal(t, []string{as.Class_Create.ID}, ecreate.Type()) 67 | assert.Equal(t, note.Obj().V, ecreate.Object.Object()) 68 | }) 69 | 70 | require.NoError(t, Ingest(context.Background(), stores, ld.ClientToServer, actor, stream, note)) 71 | }) 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /ingress/rules/activities.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/meowpub/meow/ld" 7 | "github.com/meowpub/meow/ld/ns/as" 8 | "github.com/meowpub/meow/models" 9 | ) 10 | 11 | func RequireActivity(ctx context.Context, src ld.Source, actor, stream *models.Entity, activity *as.Activity) (err error) { 12 | return ValidateQuacksLike(activity, as.Class_Activity) 13 | } 14 | -------------------------------------------------------------------------------- /ingress/rules/c2s.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | "path" 8 | "strings" 9 | 10 | "go.uber.org/multierr" 11 | "go.uber.org/zap" 12 | 13 | "github.com/meowpub/meow/ld" 14 | "github.com/meowpub/meow/ld/ns" 15 | "github.com/meowpub/meow/ld/ns/as" 16 | "github.com/meowpub/meow/lib" 17 | "github.com/meowpub/meow/models" 18 | ) 19 | 20 | // This rule ensures that the processed activity is indeed an activity, and wraps it as 21 | // the as:Object in an as:Create event if it's not. 22 | // 23 | // NOTE: This rule is ran before C2SGenerateIDs, do not depend on things having IDs. 24 | // 25 | // ActivityPub §6: Client To Server Interactions 26 | // The body of the POST request MUST contain a single Activity (which MAY contain embedded 27 | // objects), or a single non-Activity object which will be wrapped in a Create activity 28 | // by the server. 29 | // 30 | // ActivityPub §6.2.1: Object creation without a Create Activity 31 | // For client to server posting, it is possible to submit an object for creation without a 32 | // surrounding activity. The server MUST accept a valid [ActivityStreams] object that isn't 33 | // a subtype of Activity in the POST request to the outbox. The server then MUST attach this 34 | // object as the object of a Create Activity. 35 | // ... 36 | // Any to, bto, cc, bcc, and audience properties specified on the object MUST be copied over 37 | // to the new Create activity by the server. 38 | func C2SAutoCreateActivity(ctx context.Context, src ld.Source, actor, stream *models.Entity, activity *as.Activity) error { 39 | if src != ld.ClientToServer { 40 | return nil 41 | } 42 | 43 | // Just ignore if this is already an Activity. 44 | if ns.QuacksLike(activity, as.Class_Activity) { 45 | return nil 46 | } 47 | obj := as.AsObject(activity) 48 | 49 | // Create a new as:Create activity. 50 | create := as.NewCreate("") 51 | create.SetObject(obj) 52 | 53 | // Copy addressing properties off the object. 54 | Copy(obj, create, as.Prop_To) 55 | Copy(obj, create, as.Prop_Bto) 56 | Copy(obj, create, as.Prop_Cc) 57 | Copy(obj, create, as.Prop_Bcc) 58 | Copy(obj, create, as.Prop_Audience) 59 | 60 | *activity = as.AsActivity(create) 61 | return nil 62 | } 63 | 64 | // Ensure that activities have required attributes for their type as dictated by the spec. 65 | // 66 | // TODO: Should this be enforced for federated activities as well as client-submitted ones? 67 | // 68 | // NOTE: This rule is ran before C2SGenerateIDs, do not depend on things having IDs. 69 | // 70 | // ActivityPub §6.1 Client Addressing: 71 | // Clients submitting the following activities to an outbox MUST provide the object property 72 | // in the activity: Create, Update, Delete, Follow, Add, Remove, Like, Block, Undo. 73 | // Additionally, clients submitting the following activities to an outbox MUST also provide 74 | // the target property: Add, Remove. 75 | func C2SRequiredActivityProperties(ctx context.Context, src ld.Source, actor, stream *models.Entity, activity *as.Activity) (err error) { 76 | // Ignore if this isn't a client-to-server request. 77 | if src != ld.ClientToServer { 78 | return nil 79 | } 80 | 81 | // Activity types that must have an object property 82 | if ns.QuacksLikeAny(activity, 83 | as.Class_Create, as.Class_Update, as.Class_Delete, 84 | as.Class_Follow, as.Class_Add, as.Class_Remove, 85 | as.Class_Like, as.Class_Block, as.Class_Undo, 86 | ) && activity.Object.Object() == nil { 87 | err = multierr.Append(err, lib.Errorf(http.StatusBadRequest, 88 | "activity of type %v is missing required property: %s", 89 | activity.Type(), as.Prop_Object.ID)) 90 | } 91 | 92 | // Activity types that must have a target property 93 | if ns.QuacksLikeAny(activity, as.Class_Add, as.Class_Remove) && activity.Target() == nil { 94 | err = multierr.Append(err, lib.Errorf(http.StatusBadRequest, 95 | "activity of type %v is missing required property: %s", 96 | activity.Type(), as.Prop_Target.ID)) 97 | } 98 | 99 | return err 100 | } 101 | 102 | // This rule generates local IDs for client-to-server submissions. 103 | // 104 | // ActivityPub §6: Client To Server Interactions 105 | // If an Activity is submitted with a value in the id property, servers MUST ignore this 106 | // and generate a new id for the Activity. 107 | // 108 | // ActivityPub §6.2.1: Object creation without a Create activity 109 | // For non-transient objects, the server MUST attach an id to both the wrapping Create and 110 | // its wrapped Object. 111 | // 112 | // ^ I'm choosing to interpret this as having been placed in the wrong section of the spec 113 | // (§6.2.1 rather than §6.2: Create Activity), because it makes no sense for it only to 114 | // apply to objects created without Create activities. 115 | func C2SGenerateIDs(ctx context.Context, src ld.Source, actor, stream *models.Entity, activity *as.Activity) error { 116 | if src != ld.ClientToServer { 117 | return nil 118 | } 119 | 120 | // Activities go under the outbox, eg. https://example.com/@jsmith/outbox/123456789. 121 | streamURL, err := url.Parse(stream.Obj.ID()) 122 | if err != nil { 123 | return lib.Wrap(err, http.StatusInternalServerError, "stream ID is invalid") 124 | } 125 | activityURL := streamURL.ResolveReference(&url.URL{ 126 | Path: path.Join(streamURL.Path, lib.GenSnowflake().String()), 127 | }) 128 | activity.Obj().SetID(activityURL.String()) 129 | 130 | // If this is a Create activity, the object gets an ID as well. 131 | if ns.QuacksLike(activity, as.Class_Create) { 132 | create := as.AsCreate(activity) 133 | object := as.AsObject(ld.ToObject(create.Object.Object())) 134 | 135 | // Allow an as:name property to override the last part of the URL. 136 | // This is gonna need some handling for collissions at some point... 137 | objectID := "" 138 | if name := ld.ToString(object.Name()); name != "" { 139 | slug, err := Slugify(name) 140 | if err != nil { 141 | lib.GetLogger(ctx).Warn("Couldn't slugify object name", 142 | zap.String("objectID", objectID), 143 | zap.String("name", name), 144 | zap.String("slug", slug), 145 | zap.Error(err), 146 | ) 147 | } else { 148 | // Take the first (at most) 50 characters, make sure there isn't a trailing -. 149 | if len(slug) > 50 { 150 | slug = strings.Trim(slug[:50], "-") 151 | } 152 | objectID += slug 153 | } 154 | } 155 | if objectID == "" { 156 | objectID = lib.GenSnowflake().String() 157 | } 158 | 159 | // Objects are placed under the actor, eg. https://example.com/@jsmith/123456789. 160 | actorURL, err := url.Parse(actor.Obj.ID()) 161 | if err != nil { 162 | return lib.Wrap(err, http.StatusInternalServerError, "actor ID is invalid") 163 | } 164 | objectURL := actorURL.ResolveReference(&url.URL{ 165 | Path: path.Join(actorURL.Path, objectID), 166 | }) 167 | object.Obj().SetID(objectURL.String()) 168 | } 169 | 170 | return nil 171 | } 172 | -------------------------------------------------------------------------------- /ingress/rules/rule.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/meowpub/meow/ld" 7 | "github.com/meowpub/meow/ld/ns/as" 8 | "github.com/meowpub/meow/models" 9 | ) 10 | 11 | var Rules = []Rule{ 12 | // Client to Server rules are ran first, and only for C2S sources. 13 | C2SAutoCreateActivity, // Handle as:Objects posted w/o an as:Create activity. 14 | C2SRequiredActivityProperties, // Check required properties. 15 | C2SGenerateIDs, // Generate activity/object IDs, discarding old ones. 16 | 17 | RequireActivity, // Require input to be a proper Activity. 18 | } 19 | 20 | // A rule can process or reject an activity before it's ingested. 21 | type Rule func(ctx context.Context, src ld.Source, actor, stream *models.Entity, activity *as.Activity) error 22 | 23 | // Runs a series of rules on the input. 24 | func Run(ctx context.Context, rules []Rule, src ld.Source, actor, stream *models.Entity, activity *as.Activity) error { 25 | for _, rule := range rules { 26 | if err := rule(ctx, src, actor, stream, activity); err != nil { 27 | return err 28 | } 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /ingress/rules/util.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "unicode" 7 | 8 | "golang.org/x/text/runes" 9 | "golang.org/x/text/transform" 10 | "golang.org/x/text/unicode/norm" 11 | 12 | "github.com/meowpub/meow/ld" 13 | "github.com/meowpub/meow/ld/ns" 14 | "github.com/meowpub/meow/ld/ns/meta" 15 | "github.com/meowpub/meow/lib" 16 | ) 17 | 18 | // Returns whether two IDs are from the same origin. 19 | func IsSameOrigin(a, b string) bool { 20 | ua, _ := url.Parse(a) 21 | ub, _ := url.Parse(b) 22 | return ua != nil && ub != nil && ua.Hostname() == ub.Hostname() 23 | } 24 | 25 | func Slugify(input string) (string, error) { 26 | s, _, err := transform.String( 27 | transform.Chain( 28 | // Normalise to NFD form, splitting things out into base forms and modifiers. 29 | norm.NFD, 30 | 31 | // Make all of it lowercase. 32 | runes.Map(unicode.ToLower), 33 | 34 | // Filter out characters to clean up the output. 35 | runes.Remove(runes.In(unicode.M)), // Marks, eg. the ´ in é. 36 | runes.Remove(runes.In(unicode.P)), // Punctuation. 37 | 38 | // Replace non-URL-safe characters (including whitespace) with '-'. 39 | runes.Map(func(r rune) rune { 40 | if (r >= 'a' && r <= 'z') || 41 | (r >= 'A' && r <= 'Z') || 42 | (r >= '0' && r <= '9') { 43 | return r 44 | } 45 | return '-' 46 | }), 47 | ), 48 | input) 49 | return s, err 50 | } 51 | 52 | func Copy(from, to ld.Entity, prop *meta.Prop) { 53 | if v := from.Get(prop.ID); v != nil { 54 | to.Set(prop.ID, v) 55 | } 56 | } 57 | 58 | func ValidateQuacksLike(e ld.Entity, t *meta.Type) error { 59 | if !ns.QuacksLike(e, t) { 60 | return lib.Errorf(http.StatusBadRequest, "@type=[%s] does not include required type (or a known subtype thereof): %s", e.Type(), t.ID) 61 | } 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /ingress/rules/util_test.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIsSameOrigin(t *testing.T) { 10 | assert.True(t, IsSameOrigin("https://example.com/~a", "https://example.com/~b")) 11 | assert.True(t, IsSameOrigin("https://example.com/~a", "http://example.com/~b")) 12 | assert.False(t, IsSameOrigin("https://example.com/~a", "http://example.co.uk/~b")) 13 | } 14 | 15 | func TestSlugify(t *testing.T) { 16 | testdata := map[string]string{ 17 | "i'm gay": "im-gay", 18 | "lovely cafés": "lovely-cafes", 19 | "According to all known Laws of Aviation,": "according-to-all-known-laws-of-aviation", 20 | } 21 | for input, expected := range testdata { 22 | t.Run(input, func(t *testing.T) { 23 | output, err := Slugify(input) 24 | assert.NoError(t, err) 25 | assert.Equal(t, expected, output) 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ld/cast.go: -------------------------------------------------------------------------------- 1 | package ld 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func ToString(raw interface{}) string { 8 | switch v := raw.(type) { 9 | case nil: 10 | return "" 11 | case string: 12 | return v 13 | case []interface{}: 14 | out := "" 15 | for i, vv := range v { 16 | s := ToString(vv) 17 | if i == 0 { 18 | out = s 19 | } else { 20 | out = out + "," + s 21 | } 22 | } 23 | return out 24 | case map[string]interface{}: 25 | if val, ok := v["@value"]; ok { 26 | return ToString(val) 27 | } 28 | return fmt.Sprint(v) 29 | case fmt.Stringer: 30 | return v.String() 31 | default: 32 | return fmt.Sprint(v) 33 | } 34 | } 35 | 36 | // Casts to a slice. 37 | // If the value is a slice, it's returned verbatim. 38 | // If not, it's returned wrapped in a slice with one item. 39 | func ToSlice(raw interface{}) []interface{} { 40 | switch v := raw.(type) { 41 | case nil: 42 | return nil 43 | case []interface{}: 44 | return v 45 | case []string: 46 | vs := make([]interface{}, len(v)) 47 | for i, vv := range v { 48 | vs[i] = vv 49 | } 50 | return vs 51 | default: 52 | return []interface{}{raw} 53 | } 54 | } 55 | 56 | func ToStringSlice(raw interface{}) []string { 57 | switch v := raw.(type) { 58 | case []string: 59 | return v 60 | default: 61 | arr := ToSlice(raw) 62 | if arr == nil { 63 | return nil 64 | } 65 | strs := make([]string, len(arr)) 66 | for i, v := range arr { 67 | strs[i] = ToString(v) 68 | } 69 | return strs 70 | } 71 | } 72 | 73 | func ToObject(raw interface{}) *Object { 74 | switch v := raw.(type) { 75 | case nil: 76 | return nil 77 | case map[string]interface{}: 78 | return &Object{V: v} 79 | case []interface{}: 80 | for _, item := range v { 81 | if obj := ToObject(item); obj != nil { 82 | return obj 83 | } 84 | } 85 | return nil 86 | default: 87 | return nil 88 | } 89 | } 90 | 91 | func ToObjects(raw interface{}) []*Object { 92 | switch v := raw.(type) { 93 | case nil: 94 | return nil 95 | case map[string]interface{}: 96 | return []*Object{&Object{V: v}} 97 | case []interface{}: 98 | var objs []*Object 99 | for _, item := range v { 100 | if obj := ToObject(item); obj != nil { 101 | objs = append(objs, obj) 102 | } 103 | } 104 | return objs 105 | default: 106 | return nil 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /ld/contexts/security-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": { 3 | "id": "@id", 4 | "type": "@type", 5 | 6 | "dc": "http://purl.org/dc/terms/", 7 | "sec": "https://w3id.org/security#", 8 | "xsd": "http://www.w3.org/2001/XMLSchema#", 9 | 10 | "EcdsaKoblitzSignature2016": "sec:EcdsaKoblitzSignature2016", 11 | "Ed25519Signature2018": "sec:Ed25519Signature2018", 12 | "EncryptedMessage": "sec:EncryptedMessage", 13 | "GraphSignature2012": "sec:GraphSignature2012", 14 | "LinkedDataSignature2015": "sec:LinkedDataSignature2015", 15 | "LinkedDataSignature2016": "sec:LinkedDataSignature2016", 16 | "CryptographicKey": "sec:Key", 17 | 18 | "authenticationTag": "sec:authenticationTag", 19 | "canonicalizationAlgorithm": "sec:canonicalizationAlgorithm", 20 | "cipherAlgorithm": "sec:cipherAlgorithm", 21 | "cipherData": "sec:cipherData", 22 | "cipherKey": "sec:cipherKey", 23 | "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, 24 | "creator": {"@id": "dc:creator", "@type": "@id"}, 25 | "digestAlgorithm": "sec:digestAlgorithm", 26 | "digestValue": "sec:digestValue", 27 | "domain": "sec:domain", 28 | "encryptionKey": "sec:encryptionKey", 29 | "expiration": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, 30 | "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, 31 | "initializationVector": "sec:initializationVector", 32 | "iterationCount": "sec:iterationCount", 33 | "nonce": "sec:nonce", 34 | "normalizationAlgorithm": "sec:normalizationAlgorithm", 35 | "owner": {"@id": "sec:owner", "@type": "@id"}, 36 | "password": "sec:password", 37 | "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, 38 | "privateKeyPem": "sec:privateKeyPem", 39 | "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, 40 | "publicKeyBase58": "sec:publicKeyBase58", 41 | "publicKeyPem": "sec:publicKeyPem", 42 | "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"}, 43 | "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, 44 | "salt": "sec:salt", 45 | "signature": "sec:signature", 46 | "signatureAlgorithm": "sec:signingAlgorithm", 47 | "signatureValue": "sec:signatureValue" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ld/contexts/security-v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [{ 3 | "@version": 1.1 4 | }, "https://w3id.org/security/v1", { 5 | "Ed25519Signature2018": "sec:Ed25519Signature2018", 6 | "Ed25519VerificationKey2018": "sec:Ed25519VerificationKey2018", 7 | "EquihashProof2018": "sec:EquihashProof2018", 8 | "RsaSignature2018": "sec:RsaSignature2018", 9 | "RsaVerificationKey2018": "sec:RsaVerificationKey2018", 10 | "sha256": "http://www.w3.org/2001/04/xmlenc#sha256", 11 | 12 | "digestAlgorithm": {"@id": "sec:digestAlgorithm", "@type": "@vocab"}, 13 | "digestValue": {"@id": "sec:digestValue", "@type": "@vocab"}, 14 | "equihashParameterK": {"@id": "sec:equihashParameterK", "@type": "xsd:integer"}, 15 | "equihashParameterN": {"@id": "sec:equihashParameterN", "@type": "xsd:integer"}, 16 | "jws": "sec:jws", 17 | "proof": {"@id": "sec:proof", "@type": "@id", "@container": "@graph"}, 18 | "proofPurpose": {"@id": "sec:proofPurpose", "@type": "@vocab"}, 19 | "proofValue": "sec:proofValue" 20 | }] 21 | } 22 | 23 | -------------------------------------------------------------------------------- /ld/entity.go: -------------------------------------------------------------------------------- 1 | package ld 2 | 3 | // An interface for any kind of entity which embeds Object. 4 | type Entity interface { 5 | Obj() *Object 6 | IsNull() bool 7 | ID() string 8 | Value() string 9 | Type() []string 10 | Get(key string) interface{} 11 | Set(key string, v interface{}) 12 | Apply(other Entity, mergeArrays bool) error 13 | } 14 | -------------------------------------------------------------------------------- /ld/helpers.go: -------------------------------------------------------------------------------- 1 | package ld 2 | 3 | func Is(e Entity, typ string) bool { 4 | for _, typ2 := range e.Type() { 5 | if typ == typ2 { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | 12 | func ObjectIDs(objs []*Object) []string { 13 | ids := make([]string, len(objs)) 14 | for i, obj := range objs { 15 | ids[i] = obj.ID() 16 | } 17 | return ids 18 | } 19 | 20 | // Shorthand to make {"@value": v} structures. 21 | func Value(v interface{}) map[string]interface{} { 22 | return map[string]interface{}{"@value": v} 23 | } 24 | 25 | // Shorthand to make {"@id": v} structures. 26 | func ID(id string) map[string]interface{} { 27 | return map[string]interface{}{"@id": id} 28 | } 29 | -------------------------------------------------------------------------------- /ld/helpers_test.go: -------------------------------------------------------------------------------- 1 | package ld 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIs(t *testing.T) { 10 | obj := NewObject("https://example.com", "http://www.w3.org/ns/activitystreams#Note") 11 | assert.True(t, Is(obj, "http://www.w3.org/ns/activitystreams#Note")) 12 | assert.False(t, Is(obj, "http://www.w3.org/ns/activitystreams#Image")) 13 | } 14 | -------------------------------------------------------------------------------- /ld/json.go: -------------------------------------------------------------------------------- 1 | package ld 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "strings" 10 | 11 | rice "github.com/GeertJohan/go.rice" 12 | "github.com/piprate/json-gold/ld" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | const ( 17 | // Recommended Accept from the AS2 spec; json-gold defaults to the more inclusive: 18 | // "application/ld+json, application/json;q=0.9, application/javascript;q=0.5, 19 | // text/javascript;q=0.5, text/plain;q=0.2, */*;q=0.1" 20 | acceptHeader = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` 21 | ) 22 | 23 | type documentLoader struct { 24 | ctx context.Context 25 | } 26 | 27 | // Note: 28 | // * Do not mark changing documents as known (e.g. the as2 context) 29 | // * If a document has good caching, it is not important to include here 30 | var knownDocuments = map[string]string{ 31 | "https://w3id.org/security/v1": "security-v1.json", 32 | "https://w3id.org/security/v2": "security-v2.json", 33 | } 34 | 35 | // Returns a DocumentLoader for the json-gold library. It's basically a simplified version of 36 | // the default DocumentLoader, which can't be tricked into touching the local filesystem, and 37 | // with the addition of proper request contexts. 38 | func NewDocumentLoader(ctx context.Context) ld.DocumentLoader { 39 | return documentLoader{ctx: ctx} 40 | } 41 | 42 | func (l documentLoader) LoadDocument(u string) (*ld.RemoteDocument, error) { 43 | box := rice.MustFindBox("contexts") 44 | if name, ok := knownDocuments[u]; ok { 45 | var obj interface{} 46 | 47 | buf := box.MustBytes(name) 48 | if err := json.Unmarshal(buf, &obj); err != nil { 49 | return nil, err 50 | } 51 | doc := &ld.RemoteDocument{ 52 | DocumentURL: u, 53 | Document: obj, 54 | ContextURL: "", 55 | } 56 | return doc, nil 57 | } 58 | 59 | req, err := http.NewRequest("GET", u, nil) 60 | if err != nil { 61 | return nil, err 62 | } 63 | req = req.WithContext(l.ctx) 64 | req.Header.Set("Accept", acceptHeader) 65 | 66 | resp, err := http.DefaultClient.Do(req) 67 | if err != nil { 68 | return nil, err 69 | } 70 | defer func() { 71 | // Remember to drain the response body, or H/2 and connection pooling break horribly! 72 | io.Copy(ioutil.Discard, resp.Body) 73 | resp.Body.Close() 74 | }() 75 | 76 | if resp.StatusCode != http.StatusOK { 77 | return nil, ld.NewJsonLdError(ld.LoadingDocumentFailed, 78 | errors.Errorf("fetching remote document returned HTTP %d %s: %s", 79 | resp.StatusCode, resp.Status, req.URL.String())) 80 | } 81 | 82 | doc, err := ld.DocumentFromReader(resp.Body) 83 | if err != nil { 84 | return nil, ld.NewJsonLdError(ld.LoadingDocumentFailed, err) 85 | } 86 | return &ld.RemoteDocument{ 87 | DocumentURL: resp.Request.URL.String(), // Final URL after redirections 88 | Document: doc, 89 | }, nil 90 | } 91 | 92 | func Options(ctx context.Context, uri string) *ld.JsonLdOptions { 93 | opts := ld.NewJsonLdOptions(uri) 94 | opts.DocumentLoader = NewDocumentLoader(ctx) 95 | return opts 96 | } 97 | 98 | func Expand(ctx context.Context, doc map[string]interface{}, uri string) (map[string]interface{}, error) { 99 | proc := ld.NewJsonLdProcessor() 100 | opts := Options(ctx, uri) 101 | 102 | res, err := proc.Expand(doc, opts) 103 | if err != nil { 104 | return nil, errors.Wrapf(err, "expanding '%s'", uri) 105 | } 106 | 107 | if len(res) == 1 { 108 | return res[0].(map[string]interface{}), nil 109 | } else { 110 | return nil, errors.Errorf("Expansion of '%s' returned an array of length %d (need 1)", uri, len(res)) 111 | } 112 | } 113 | 114 | func Compact(ctx context.Context, doc map[string]interface{}, uri string, context interface{}) (map[string]interface{}, error) { 115 | proc := ld.NewJsonLdProcessor() 116 | opts := Options(ctx, uri) 117 | 118 | arr := []interface{}{doc} 119 | res, err := proc.Compact(arr, context, opts) 120 | if err != nil { 121 | return nil, errors.Wrapf(err, "compacting '%s'", uri) 122 | } 123 | 124 | res["@context"] = context 125 | 126 | if err != nil { 127 | return nil, errors.Wrapf(err, "marshalling '%s' after compaction", uri) 128 | } else { 129 | return res, nil 130 | } 131 | } 132 | 133 | func CompactObject(ctx context.Context, obj *Object) (map[string]interface{}, error) { 134 | return Compact(ctx, obj.V, "", contextForObject(obj)) 135 | } 136 | 137 | func contextForObject(obj *Object) interface{} { 138 | embeds := map[string]struct{}{} 139 | aliases := map[string]interface{}{} 140 | walkValueForContext(embeds, aliases, obj.V) 141 | if num := len(embeds); num > 0 { 142 | ldctx := make([]interface{}, 0, len(embeds)) 143 | for embed := range embeds { 144 | if num == 1 { 145 | return embed 146 | } 147 | ldctx = append(ldctx, embed) 148 | } 149 | return ldctx 150 | } 151 | return aliases 152 | } 153 | 154 | func walkValueForContext(embeds map[string]struct{}, aliases map[string]interface{}, vv interface{}) { 155 | switch v := vv.(type) { 156 | case map[string]interface{}: 157 | for key, value := range v { 158 | if attrNS := attrNamespace(key); attrNS != "" { 159 | nsAlias, embed := namespaceAlias(attrNS) 160 | if embed { 161 | embeds[nsAlias] = struct{}{} 162 | } else if nsAlias != "" { 163 | aliases[nsAlias] = attrNS 164 | } 165 | } 166 | walkValueForContext(embeds, aliases, value) 167 | } 168 | } 169 | } 170 | 171 | func attrNamespace(attr string) string { 172 | if idx := strings.IndexRune(attr, '#'); idx != -1 { 173 | return attr[:idx+1] 174 | } 175 | return "" 176 | } 177 | 178 | func namespaceAlias(ns string) (string, bool) { 179 | if idx := strings.Index(ns, "://"); idx != -1 { 180 | ns = ns[idx+3:] 181 | } 182 | switch ns { 183 | case "www.w3.org/1999/02/22-rdf-syntax-ns#": 184 | return "rdf", false 185 | case "www.w3.org/2000/01/rdf-schema#": 186 | return "rdfs", false 187 | case "www.w3.org/2002/07/owl#": 188 | return "owl", false 189 | case "www.w3.org/ns/activitystreams#": 190 | return "https://www.w3.org/ns/activitystreams", true 191 | case "www.w3.org/ns/ldp#": 192 | return "ldp", false 193 | case "w3id.org/security#": 194 | return "https://w3id.org/security", true 195 | } 196 | return "", false 197 | } 198 | -------------------------------------------------------------------------------- /ld/json_test.go: -------------------------------------------------------------------------------- 1 | package ld 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | const expanded = ` 17 | { 18 | "@id": "https://example.com/something", 19 | "http://purl.org/dc/terms/creator": [ 20 | {"@id": "https://example.com/someBODY"} 21 | ], 22 | "https://w3id.org/security#Ed25519Signature2018": [ 23 | {"@value": "foo"} 24 | ] 25 | } 26 | ` 27 | 28 | const compacted = ` 29 | { 30 | "@context":"https://w3id.org/security/v1", 31 | "Ed25519Signature2018":"foo", 32 | "creator":"someBODY", 33 | "id":"something" 34 | } 35 | ` 36 | 37 | func TestDocumentLoader(t *testing.T) { 38 | srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 39 | assert.Equal(t, "GET", req.Method, "Wrong Method") 40 | assert.Equal(t, "/something", req.RequestURI, "Wrong RequestURI") 41 | assert.Equal(t, acceptHeader, req.Header.Get("Accept"), "Wrong Accept header") 42 | fmt.Fprintln(rw, expanded) 43 | })) 44 | defer srv.Close() 45 | 46 | ctx, cancel := context.WithCancel(context.Background()) 47 | defer cancel() 48 | 49 | loader := NewDocumentLoader(ctx) 50 | rdoc, err := loader.LoadDocument(srv.URL + "/something") 51 | require.NoError(t, err) 52 | assert.Equal(t, srv.URL+"/something", rdoc.DocumentURL, "Wrong DocumentURL") 53 | assert.Equal(t, map[string]interface{}{ 54 | "@id": "https://example.com/something", 55 | "http://purl.org/dc/terms/creator": []interface{}{ 56 | map[string]interface{}{"@id": "https://example.com/someBODY"}, 57 | }, 58 | "https://w3id.org/security#Ed25519Signature2018": []interface{}{ 59 | map[string]interface{}{"@value": "foo"}, 60 | }, 61 | }, rdoc.Document, "Wrong Document") 62 | 63 | cancel() 64 | 65 | _, err = loader.LoadDocument(srv.URL + "/something") 66 | assert.EqualError(t, err, "Get "+srv.URL+"/something: context canceled", "Wrong error from cancelled context") 67 | } 68 | 69 | func TestCompact(t *testing.T) { 70 | var compBuf bytes.Buffer 71 | require.NoError(t, json.Compact(&compBuf, []byte(compacted)), "reference must json compact") 72 | 73 | var v map[string]interface{} 74 | require.NoError(t, json.Unmarshal([]byte(expanded), &v), "json.Unmarshal") 75 | 76 | // We can use a nil http.Client as no fetches should actually be done 77 | res, err := Compact(nil, v, "https://example.com/something", "https://w3id.org/security/v1") 78 | require.NoError(t, err, "Compact should succeed") 79 | 80 | buf, err := json.Marshal(res) 81 | require.NoError(t, err, "json.Marshal") 82 | 83 | require.Equal(t, compBuf.String(), string(buf), "should produce expected result") 84 | } 85 | 86 | func TestExpand(t *testing.T) { 87 | var expBuf bytes.Buffer 88 | require.NoError(t, json.Compact(&expBuf, []byte(expanded)), "reference must compact") 89 | 90 | var v map[string]interface{} 91 | require.NoError(t, json.Unmarshal([]byte(compacted), &v), "json.Unmarshal") 92 | 93 | res, err := Expand(nil, v, "https://example.com/something") 94 | require.NoError(t, err, "Expand should succeed") 95 | 96 | buf, err := json.Marshal(res) 97 | require.NoError(t, err, "json.Marshal") 98 | 99 | require.Equal(t, expBuf.String(), string(buf), "should produce expected result") 100 | } 101 | 102 | func Test_attrNamespace_namespaceAlias(t *testing.T) { 103 | testdata := map[string]struct { 104 | Alias string 105 | Embed bool 106 | }{ 107 | "http://www.w3.org/ns/activitystreams#Note": {"https://www.w3.org/ns/activitystreams", true}, 108 | "https://www.w3.org/ns/activitystreams#Note": {"https://www.w3.org/ns/activitystreams", true}, 109 | } 110 | for attr, final := range testdata { 111 | t.Run(attr, func(t *testing.T) { 112 | ns := attrNamespace(attr) 113 | alias, embed := namespaceAlias(ns) 114 | assert.Equal(t, final.Alias, alias) 115 | assert.Equal(t, final.Embed, embed) 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /ld/namespace.go: -------------------------------------------------------------------------------- 1 | package ld 2 | -------------------------------------------------------------------------------- /ld/ns/as/datatypes.gen.go: -------------------------------------------------------------------------------- 1 | // GENERATED FILE, DO NOT EDIT. 2 | // Please refer to: tools/nsgen/templates.go 3 | package as 4 | -------------------------------------------------------------------------------- /ld/ns/index.go: -------------------------------------------------------------------------------- 1 | package ns 2 | 3 | import ( 4 | "github.com/meowpub/meow/ld" 5 | "github.com/meowpub/meow/ld/ns/meta" 6 | ) 7 | 8 | // Returns whether the entity quacks like a certain type. 9 | func QuacksLike(e ld.Entity, t *meta.Type) bool { 10 | for _, tID := range e.Type() { 11 | if t.ID == tID { 12 | return true 13 | } 14 | typ, ok := Classes[tID] 15 | if !ok { 16 | continue 17 | } 18 | if typ.IsSubClassOf(t) { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | 25 | // Returns whether the entity quacks like any of the given types. 26 | func QuacksLikeAny(e ld.Entity, ts ...*meta.Type) bool { 27 | for _, t := range ts { 28 | if QuacksLike(e, t) { 29 | return true 30 | } 31 | } 32 | return false 33 | } 34 | 35 | // Manifests concrete, typed Entities from an Object based on its Type() entries. 36 | // It's worth noting that the Object is not copied; all returned Entities refer to the same 37 | // underlying Object, and motifications to one will reflect on all of them. 38 | func Manifest(obj *ld.Object) (entities []ld.Entity) { 39 | for _, typ := range obj.Type() { 40 | if typ, ok := Classes[typ]; ok { 41 | entities = append(entities, typ.Cast(obj)) 42 | } 43 | } 44 | return 45 | } 46 | -------------------------------------------------------------------------------- /ld/ns/ldp/datatypes.gen.go: -------------------------------------------------------------------------------- 1 | // GENERATED FILE, DO NOT EDIT. 2 | // Please refer to: tools/nsgen/templates.go 3 | package ldp 4 | -------------------------------------------------------------------------------- /ld/ns/ldp/properties.gen.go: -------------------------------------------------------------------------------- 1 | // GENERATED FILE, DO NOT EDIT. 2 | // Please refer to: tools/nsgen/templates.go 3 | package ldp 4 | 5 | import ( 6 | "github.com/meowpub/meow/ld" 7 | ) 8 | 9 | // Links a resource with constraints that the server requires requests like creation and update to conform to. 10 | func GetConstrainedBy(e ld.Entity) interface{} { return e.Get(Prop_ConstrainedBy.ID) } 11 | 12 | func SetConstrainedBy(e ld.Entity, v interface{}) { e.Set(Prop_ConstrainedBy.ID, v) } 13 | 14 | // Links a container with resources created through the container. 15 | func GetContains(e ld.Entity) interface{} { return e.Get(Prop_Contains.ID) } 16 | 17 | func SetContains(e ld.Entity, v interface{}) { e.Set(Prop_Contains.ID, v) } 18 | 19 | // Indicates which predicate is used in membership triples, and that the membership triple pattern is < membership-constant-URI , object-of-hasMemberRelation, member-URI >. 20 | func GetHasMemberRelation(e ld.Entity) interface{} { return e.Get(Prop_HasMemberRelation.ID) } 21 | 22 | func SetHasMemberRelation(e ld.Entity, v interface{}) { e.Set(Prop_HasMemberRelation.ID, v) } 23 | 24 | // Links a resource to a container where notifications for the resource can be created and discovered. 25 | func GetInbox(e ld.Entity) interface{} { return e.Get(Prop_Inbox.ID) } 26 | 27 | func SetInbox(e ld.Entity, v interface{}) { e.Set(Prop_Inbox.ID, v) } 28 | 29 | // Indicates which triple in a creation request should be used as the member-URI value in the membership triple added when the creation request is successful. 30 | func GetInsertedContentRelation(e ld.Entity) interface{} { 31 | return e.Get(Prop_InsertedContentRelation.ID) 32 | } 33 | 34 | func SetInsertedContentRelation(e ld.Entity, v interface{}) { e.Set(Prop_InsertedContentRelation.ID, v) } 35 | 36 | // Indicates which predicate is used in membership triples, and that the membership triple pattern is < member-URI , object-of-isMemberOfRelation, membership-constant-URI >. 37 | func GetIsMemberOfRelation(e ld.Entity) interface{} { return e.Get(Prop_IsMemberOfRelation.ID) } 38 | 39 | func SetIsMemberOfRelation(e ld.Entity, v interface{}) { e.Set(Prop_IsMemberOfRelation.ID, v) } 40 | 41 | // LDP servers should use this predicate as the membership predicate if there is no obvious predicate from an application vocabulary to use. 42 | func GetMember(e ld.Entity) interface{} { return e.Get(Prop_Member.ID) } 43 | 44 | func SetMember(e ld.Entity, v interface{}) { e.Set(Prop_Member.ID, v) } 45 | 46 | // Indicates the membership-constant-URI in a membership triple. Depending upon the membership triple pattern a container uses, as indicated by the presence of ldp:hasMemberRelation or ldp:isMemberOfRelation, the membership-constant-URI might occupy either the subject or object position in membership triples. 47 | func GetMembershipResource(e ld.Entity) interface{} { return e.Get(Prop_MembershipResource.ID) } 48 | 49 | func SetMembershipResource(e ld.Entity, v interface{}) { e.Set(Prop_MembershipResource.ID, v) } 50 | 51 | // Link to a page sequence resource, as defined by LDP Paging. Typically used to communicate the sorting criteria used to allocate LDPC members to pages. 52 | func GetPageSequence(e ld.Entity) interface{} { return e.Get(Prop_PageSequence.ID) } 53 | 54 | func SetPageSequence(e ld.Entity, v interface{}) { e.Set(Prop_PageSequence.ID, v) } 55 | 56 | // The collation used to order the members across pages in a page sequence when comparing strings. 57 | func GetPageSortCollation(e ld.Entity) interface{} { return e.Get(Prop_PageSortCollation.ID) } 58 | 59 | func SetPageSortCollation(e ld.Entity, v interface{}) { e.Set(Prop_PageSortCollation.ID, v) } 60 | 61 | // Link to the list of sorting criteria used by the server in a representation. Typically used on Link response headers as an extension link relation URI in the rel= parameter. 62 | func GetPageSortCriteria(e ld.Entity) interface{} { return e.Get(Prop_PageSortCriteria.ID) } 63 | 64 | func SetPageSortCriteria(e ld.Entity, v interface{}) { e.Set(Prop_PageSortCriteria.ID, v) } 65 | 66 | // The ascending/descending/etc order used to order the members across pages in a page sequence. 67 | func GetPageSortOrder(e ld.Entity) interface{} { return e.Get(Prop_PageSortOrder.ID) } 68 | 69 | func SetPageSortOrder(e ld.Entity, v interface{}) { e.Set(Prop_PageSortOrder.ID, v) } 70 | 71 | // Predicate used to specify the order of the members across a page sequence's in-sequence page resources; it asserts nothing about the order of members in the representation of a single page. 72 | func GetPageSortPredicate(e ld.Entity) interface{} { return e.Get(Prop_PageSortPredicate.ID) } 73 | 74 | func SetPageSortPredicate(e ld.Entity, v interface{}) { e.Set(Prop_PageSortPredicate.ID, v) } 75 | -------------------------------------------------------------------------------- /ld/ns/meta/type.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "github.com/meowpub/meow/ld" 5 | ) 6 | 7 | // Describes a namespace. 8 | type Namespace struct { 9 | ID string // "http://www.w3.org/ns/activitystreams#" 10 | Short string // "as" 11 | Props []*Prop // All properties in this namespace. 12 | Types map[string]*Type // All Types in this namespace. 13 | } 14 | 15 | // Describes a property. 16 | type Prop struct { 17 | ID string 18 | Short string 19 | Comment string 20 | } 21 | 22 | // Describes a type. 23 | type Type struct { 24 | ID string // "http://www.w3.org/ns/activitystreams#Note" 25 | Short string // "Note" 26 | SubClassOf []*Type // Supertypes, if any. 27 | Props []*Prop // Property names. 28 | 29 | // Casts an arbitrary Entity to this type. 30 | // Underlying Objects are not copied - see the note on Manifest! 31 | Cast func(ld.Entity) ld.Entity 32 | } 33 | 34 | // Returns whether the type inherits from another type, somewhere up the inheritance chain. 35 | func (t *Type) IsSubClassOf(other *Type) bool { 36 | for _, sup := range t.SubClassOf { 37 | if sup.ID == other.ID { 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | -------------------------------------------------------------------------------- /ld/ns/owl/datatypes.gen.go: -------------------------------------------------------------------------------- 1 | // GENERATED FILE, DO NOT EDIT. 2 | // Please refer to: tools/nsgen/templates.go 3 | package owl 4 | -------------------------------------------------------------------------------- /ld/ns/rdf/datatypes.gen.go: -------------------------------------------------------------------------------- 1 | // GENERATED FILE, DO NOT EDIT. 2 | // Please refer to: tools/nsgen/templates.go 3 | package rdf 4 | 5 | // The datatype of RDF literals storing fragments of HTML content 6 | type HTML interface{} 7 | 8 | // The class of plain (i.e. untyped) literal values, as used in RIF and OWL 2 9 | type PlainLiteral interface{} 10 | 11 | // The datatype of XML literal values. 12 | type XMLLiteral interface{} 13 | 14 | // The datatype of language-tagged string values 15 | type LangString interface{} 16 | -------------------------------------------------------------------------------- /ld/ns/rdf/properties.gen.go: -------------------------------------------------------------------------------- 1 | // GENERATED FILE, DO NOT EDIT. 2 | // Please refer to: tools/nsgen/templates.go 3 | package rdf 4 | 5 | import ( 6 | "github.com/meowpub/meow/ld" 7 | ) 8 | 9 | // The first item in the subject RDF list. 10 | func GetFirst(e ld.Entity) interface{} { return e.Get(Prop_First.ID) } 11 | 12 | func SetFirst(e ld.Entity, v interface{}) { e.Set(Prop_First.ID, v) } 13 | 14 | // The object of the subject RDF statement. 15 | func GetObject(e ld.Entity) interface{} { return e.Get(Prop_Object.ID) } 16 | 17 | func SetObject(e ld.Entity, v interface{}) { e.Set(Prop_Object.ID, v) } 18 | 19 | // The predicate of the subject RDF statement. 20 | func GetPredicate(e ld.Entity) interface{} { return e.Get(Prop_Predicate.ID) } 21 | 22 | func SetPredicate(e ld.Entity, v interface{}) { e.Set(Prop_Predicate.ID, v) } 23 | 24 | // The rest of the subject RDF list after the first item. 25 | func GetRest(e ld.Entity) interface{} { return e.Get(Prop_Rest.ID) } 26 | 27 | func SetRest(e ld.Entity, v interface{}) { e.Set(Prop_Rest.ID, v) } 28 | 29 | // The subject of the subject RDF statement. 30 | func GetSubject(e ld.Entity) interface{} { return e.Get(Prop_Subject.ID) } 31 | 32 | func SetSubject(e ld.Entity, v interface{}) { e.Set(Prop_Subject.ID, v) } 33 | 34 | // The subject is an instance of a class. 35 | func GetType(e ld.Entity) interface{} { return e.Get(Prop_Type.ID) } 36 | 37 | func SetType(e ld.Entity, v interface{}) { e.Set(Prop_Type.ID, v) } 38 | 39 | // Idiomatic property used for structured values. 40 | func GetValue(e ld.Entity) interface{} { return e.Get(Prop_Value.ID) } 41 | 42 | func SetValue(e ld.Entity, v interface{}) { e.Set(Prop_Value.ID, v) } 43 | 44 | // A description of the subject resource. 45 | func GetComment(e ld.Entity) interface{} { return e.Get(Prop_Comment.ID) } 46 | 47 | func SetComment(e ld.Entity, v interface{}) { e.Set(Prop_Comment.ID, v) } 48 | 49 | // A domain of the subject property. 50 | func GetDomain(e ld.Entity) interface{} { return e.Get(Prop_Domain.ID) } 51 | 52 | func SetDomain(e ld.Entity, v interface{}) { e.Set(Prop_Domain.ID, v) } 53 | 54 | // The defininition of the subject resource. 55 | func GetIsDefinedBy(e ld.Entity) interface{} { return e.Get(Prop_IsDefinedBy.ID) } 56 | 57 | func SetIsDefinedBy(e ld.Entity, v interface{}) { e.Set(Prop_IsDefinedBy.ID, v) } 58 | 59 | // A human-readable name for the subject. 60 | func GetLabel(e ld.Entity) interface{} { return e.Get(Prop_Label.ID) } 61 | 62 | func SetLabel(e ld.Entity, v interface{}) { e.Set(Prop_Label.ID, v) } 63 | 64 | // A member of the subject resource. 65 | func GetMember(e ld.Entity) interface{} { return e.Get(Prop_Member.ID) } 66 | 67 | func SetMember(e ld.Entity, v interface{}) { e.Set(Prop_Member.ID, v) } 68 | 69 | // A range of the subject property. 70 | func GetRange(e ld.Entity) interface{} { return e.Get(Prop_Range.ID) } 71 | 72 | func SetRange(e ld.Entity, v interface{}) { e.Set(Prop_Range.ID, v) } 73 | 74 | // Further information about the subject resource. 75 | func GetSeeAlso(e ld.Entity) interface{} { return e.Get(Prop_SeeAlso.ID) } 76 | 77 | func SetSeeAlso(e ld.Entity, v interface{}) { e.Set(Prop_SeeAlso.ID, v) } 78 | 79 | // The subject is a subclass of a class. 80 | func GetSubClassOf(e ld.Entity) interface{} { return e.Get(Prop_SubClassOf.ID) } 81 | 82 | func SetSubClassOf(e ld.Entity, v interface{}) { e.Set(Prop_SubClassOf.ID, v) } 83 | 84 | // The subject is a subproperty of a property. 85 | func GetSubPropertyOf(e ld.Entity) interface{} { return e.Get(Prop_SubPropertyOf.ID) } 86 | 87 | func SetSubPropertyOf(e ld.Entity, v interface{}) { e.Set(Prop_SubPropertyOf.ID, v) } 88 | -------------------------------------------------------------------------------- /ld/ns/rdf/rdf.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdf: . 2 | @prefix rdfs: . 3 | @prefix owl: . 4 | @prefix dc: . 5 | 6 | a owl:Ontology ; 7 | dc:title "The RDF Concepts Vocabulary (RDF)" ; 8 | dc:description "This is the RDF Schema for the RDF vocabulary terms in the RDF Namespace, defined in RDF 1.1 Concepts." . 9 | 10 | rdf:HTML a rdfs:Datatype ; 11 | rdfs:subClassOf rdfs:Literal ; 12 | rdfs:isDefinedBy ; 13 | rdfs:seeAlso ; 14 | rdfs:label "HTML" ; 15 | rdfs:comment "The datatype of RDF literals storing fragments of HTML content" . 16 | 17 | rdf:langString a rdfs:Datatype ; 18 | rdfs:subClassOf rdfs:Literal ; 19 | rdfs:isDefinedBy ; 20 | rdfs:seeAlso ; 21 | rdfs:label "langString" ; 22 | rdfs:comment "The datatype of language-tagged string values" . 23 | 24 | rdf:PlainLiteral a rdfs:Datatype ; 25 | rdfs:isDefinedBy ; 26 | rdfs:subClassOf rdfs:Literal ; 27 | rdfs:seeAlso ; 28 | rdfs:label "PlainLiteral" ; 29 | rdfs:comment "The class of plain (i.e. untyped) literal values, as used in RIF and OWL 2" . 30 | 31 | rdf:type a rdf:Property ; 32 | rdfs:isDefinedBy ; 33 | rdfs:label "type" ; 34 | rdfs:comment "The subject is an instance of a class." ; 35 | rdfs:range rdfs:Class ; 36 | rdfs:domain rdfs:Resource . 37 | 38 | rdf:Property a rdfs:Class ; 39 | rdfs:isDefinedBy ; 40 | rdfs:label "Property" ; 41 | rdfs:comment "The class of RDF properties." ; 42 | rdfs:subClassOf rdfs:Resource . 43 | 44 | rdf:Statement a rdfs:Class ; 45 | rdfs:isDefinedBy ; 46 | rdfs:label "Statement" ; 47 | rdfs:subClassOf rdfs:Resource ; 48 | rdfs:comment "The class of RDF statements." . 49 | 50 | rdf:subject a rdf:Property ; 51 | rdfs:isDefinedBy ; 52 | rdfs:label "subject" ; 53 | rdfs:comment "The subject of the subject RDF statement." ; 54 | rdfs:domain rdf:Statement ; 55 | rdfs:range rdfs:Resource . 56 | 57 | rdf:predicate a rdf:Property ; 58 | rdfs:isDefinedBy ; 59 | rdfs:label "predicate" ; 60 | rdfs:comment "The predicate of the subject RDF statement." ; 61 | rdfs:domain rdf:Statement ; 62 | rdfs:range rdfs:Resource . 63 | 64 | rdf:object a rdf:Property ; 65 | rdfs:isDefinedBy ; 66 | rdfs:label "object" ; 67 | rdfs:comment "The object of the subject RDF statement." ; 68 | rdfs:domain rdf:Statement ; 69 | rdfs:range rdfs:Resource . 70 | 71 | rdf:Bag a rdfs:Class ; 72 | rdfs:isDefinedBy ; 73 | rdfs:label "Bag" ; 74 | rdfs:comment "The class of unordered containers." ; 75 | rdfs:subClassOf rdfs:Container . 76 | 77 | rdf:Seq a rdfs:Class ; 78 | rdfs:isDefinedBy ; 79 | rdfs:label "Seq" ; 80 | rdfs:comment "The class of ordered containers." ; 81 | rdfs:subClassOf rdfs:Container . 82 | 83 | rdf:Alt a rdfs:Class ; 84 | rdfs:isDefinedBy ; 85 | rdfs:label "Alt" ; 86 | rdfs:comment "The class of containers of alternatives." ; 87 | rdfs:subClassOf rdfs:Container . 88 | 89 | rdf:value a rdf:Property ; 90 | rdfs:isDefinedBy ; 91 | rdfs:label "value" ; 92 | rdfs:comment "Idiomatic property used for structured values." ; 93 | rdfs:domain rdfs:Resource ; 94 | rdfs:range rdfs:Resource . 95 | 96 | rdf:List a rdfs:Class ; 97 | rdfs:isDefinedBy ; 98 | rdfs:label "List" ; 99 | rdfs:comment "The class of RDF Lists." ; 100 | rdfs:subClassOf rdfs:Resource . 101 | 102 | rdf:nil a rdf:List ; 103 | rdfs:isDefinedBy ; 104 | rdfs:label "nil" ; 105 | rdfs:comment "The empty list, with no items in it. If the rest of a list is nil then the list has no more items in it." . 106 | 107 | rdf:first a rdf:Property ; 108 | rdfs:isDefinedBy ; 109 | rdfs:label "first" ; 110 | rdfs:comment "The first item in the subject RDF list." ; 111 | rdfs:domain rdf:List ; 112 | rdfs:range rdfs:Resource . 113 | 114 | rdf:rest a rdf:Property ; 115 | rdfs:isDefinedBy ; 116 | rdfs:label "rest" ; 117 | rdfs:comment "The rest of the subject RDF list after the first item." ; 118 | rdfs:domain rdf:List ; 119 | rdfs:range rdf:List . 120 | 121 | rdf:XMLLiteral a rdfs:Datatype ; 122 | rdfs:subClassOf rdfs:Literal ; 123 | rdfs:isDefinedBy ; 124 | rdfs:label "XMLLiteral" ; 125 | rdfs:comment "The datatype of XML literal values." . 126 | -------------------------------------------------------------------------------- /ld/ns/rdf/rdfs.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdf: . 2 | @prefix rdfs: . 3 | @prefix owl: . 4 | @prefix dc: . 5 | 6 | a owl:Ontology ; 7 | dc:title "The RDF Schema vocabulary (RDFS)" . 8 | 9 | rdfs:Resource a rdfs:Class ; 10 | rdfs:isDefinedBy ; 11 | rdfs:label "Resource" ; 12 | rdfs:comment "The class resource, everything." . 13 | 14 | rdfs:Class a rdfs:Class ; 15 | rdfs:isDefinedBy ; 16 | rdfs:label "Class" ; 17 | rdfs:comment "The class of classes." ; 18 | rdfs:subClassOf rdfs:Resource . 19 | 20 | rdfs:subClassOf a rdf:Property ; 21 | rdfs:isDefinedBy ; 22 | rdfs:label "subClassOf" ; 23 | rdfs:comment "The subject is a subclass of a class." ; 24 | rdfs:range rdfs:Class ; 25 | rdfs:domain rdfs:Class . 26 | 27 | rdfs:subPropertyOf a rdf:Property ; 28 | rdfs:isDefinedBy ; 29 | rdfs:label "subPropertyOf" ; 30 | rdfs:comment "The subject is a subproperty of a property." ; 31 | rdfs:range rdf:Property ; 32 | rdfs:domain rdf:Property . 33 | 34 | rdfs:comment a rdf:Property ; 35 | rdfs:isDefinedBy ; 36 | rdfs:label "comment" ; 37 | rdfs:comment "A description of the subject resource." ; 38 | rdfs:domain rdfs:Resource ; 39 | rdfs:range rdfs:Literal . 40 | 41 | rdfs:label a rdf:Property ; 42 | rdfs:isDefinedBy ; 43 | rdfs:label "label" ; 44 | rdfs:comment "A human-readable name for the subject." ; 45 | rdfs:domain rdfs:Resource ; 46 | rdfs:range rdfs:Literal . 47 | 48 | rdfs:domain a rdf:Property ; 49 | rdfs:isDefinedBy ; 50 | rdfs:label "domain" ; 51 | rdfs:comment "A domain of the subject property." ; 52 | rdfs:range rdfs:Class ; 53 | rdfs:domain rdf:Property . 54 | 55 | rdfs:range a rdf:Property ; 56 | rdfs:isDefinedBy ; 57 | rdfs:label "range" ; 58 | rdfs:comment "A range of the subject property." ; 59 | rdfs:range rdfs:Class ; 60 | rdfs:domain rdf:Property . 61 | 62 | rdfs:seeAlso a rdf:Property ; 63 | rdfs:isDefinedBy ; 64 | rdfs:label "seeAlso" ; 65 | rdfs:comment "Further information about the subject resource." ; 66 | rdfs:range rdfs:Resource ; 67 | rdfs:domain rdfs:Resource . 68 | 69 | rdfs:isDefinedBy a rdf:Property ; 70 | rdfs:isDefinedBy ; 71 | rdfs:subPropertyOf rdfs:seeAlso ; 72 | rdfs:label "isDefinedBy" ; 73 | rdfs:comment "The defininition of the subject resource." ; 74 | rdfs:range rdfs:Resource ; 75 | rdfs:domain rdfs:Resource . 76 | 77 | rdfs:Literal a rdfs:Class ; 78 | rdfs:isDefinedBy ; 79 | rdfs:label "Literal" ; 80 | rdfs:comment "The class of literal values, eg. textual strings and integers." ; 81 | rdfs:subClassOf rdfs:Resource . 82 | 83 | rdfs:Container a rdfs:Class ; 84 | rdfs:isDefinedBy ; 85 | rdfs:label "Container" ; 86 | rdfs:subClassOf rdfs:Resource ; 87 | rdfs:comment "The class of RDF containers." . 88 | 89 | rdfs:ContainerMembershipProperty a rdfs:Class ; 90 | rdfs:isDefinedBy ; 91 | rdfs:label "ContainerMembershipProperty" ; 92 | rdfs:comment """The class of container membership properties, rdf:_1, rdf:_2, ..., 93 | all of which are sub-properties of 'member'.""" ; 94 | rdfs:subClassOf rdf:Property . 95 | 96 | rdfs:member a rdf:Property ; 97 | rdfs:isDefinedBy ; 98 | rdfs:label "member" ; 99 | rdfs:comment "A member of the subject resource." ; 100 | rdfs:domain rdfs:Resource ; 101 | rdfs:range rdfs:Resource . 102 | 103 | rdfs:Datatype a rdfs:Class ; 104 | rdfs:isDefinedBy ; 105 | rdfs:label "Datatype" ; 106 | rdfs:comment "The class of RDF datatypes." ; 107 | rdfs:subClassOf rdfs:Class . 108 | 109 | rdfs:seeAlso . 110 | -------------------------------------------------------------------------------- /ld/ns/sec/datatypes.gen.go: -------------------------------------------------------------------------------- 1 | // GENERATED FILE, DO NOT EDIT. 2 | // Please refer to: tools/nsgen/templates.go 3 | package sec 4 | -------------------------------------------------------------------------------- /ld/ns/sec/sec.ttl: -------------------------------------------------------------------------------- 1 | @prefix owl: . 2 | @prefix rdf: . 3 | @prefix rdfs: . 4 | @prefix sec: . 5 | @prefix vs: . 6 | @prefix xml: . 7 | @prefix xsd: . 8 | 9 | sec:Digest a rdfs:Class ; 10 | vs:term_status "stable" . 11 | 12 | sec:EncryptedMessage a rdfs:Class ; 13 | vs:term_status "stable" . 14 | 15 | sec:GraphSignature2012 a rdfs:Class ; 16 | vs:term_status "stable" . 17 | 18 | sec:Key a rdfs:Class ; 19 | vs:term_status "stable" . 20 | 21 | sec:LinkedDataSignature2015 a rdfs:Class ; 22 | vs:term_status "stable" . 23 | 24 | sec:Signature a rdfs:Class ; 25 | vs:term_status "stable" . 26 | 27 | sec:canonicalizationAlgorithm a rdf:Property ; 28 | vs:term_status "stable" . 29 | 30 | sec:cipherAlgorithm a rdf:Property ; 31 | vs:term_status "stable" . 32 | 33 | sec:cipherData a rdf:Property ; 34 | vs:term_status "stable" . 35 | 36 | sec:cipherKey a rdf:Property ; 37 | vs:term_status "stable" . 38 | 39 | sec:digestAlgorithm a rdf:Property ; 40 | vs:term_status "stable" . 41 | 42 | sec:digestValue a rdf:Property ; 43 | vs:term_status "stable" . 44 | 45 | sec:expires a rdf:Property ; 46 | vs:term_status "stable" . 47 | 48 | sec:initializationVector a rdf:Property ; 49 | vs:term_status "stable" . 50 | 51 | sec:nonce a rdf:Property ; 52 | vs:term_status "stable" . 53 | 54 | sec:owner a rdf:Property ; 55 | vs:term_status "stable" . 56 | 57 | sec:password a rdf:Property ; 58 | vs:term_status "stable" . 59 | 60 | sec:privateKeyPem a rdf:Property ; 61 | vs:term_status "stable" . 62 | 63 | sec:publicKey a rdf:Property ; 64 | vs:term_status "stable" . 65 | 66 | sec:publicKeyPem a rdf:Property ; 67 | vs:term_status "stable" . 68 | 69 | sec:publicKeyService a rdf:Property ; 70 | vs:term_status "unstable" . 71 | 72 | sec:revoked a rdf:Property ; 73 | vs:term_status "stable" . 74 | 75 | sec:signature a rdf:Property ; 76 | vs:term_status "stable" . 77 | 78 | sec:signatureAlgorithm a rdf:Property ; 79 | vs:term_status "unstable" . 80 | 81 | sec:signatureValue a rdf:Property ; 82 | vs:term_status "stable" . 83 | 84 | vs:term_status "stable" . 85 | 86 | 87 | -------------------------------------------------------------------------------- /ld/object.go: -------------------------------------------------------------------------------- 1 | package ld 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | 8 | "github.com/meowpub/meow/lib" 9 | ) 10 | 11 | var _ Entity = &Object{} 12 | 13 | // The base type of all JSON-LD objects. 14 | type Object struct { 15 | V map[string]interface{} 16 | 17 | // Cached attributes. 18 | id string 19 | value string 20 | typ []string 21 | } 22 | 23 | func NewObject(id string, types ...string) *Object { 24 | obj := &Object{} 25 | if id != "" { 26 | obj.SetID(id) 27 | } 28 | if types != nil { 29 | obj.SetType(types...) 30 | } 31 | return obj 32 | } 33 | 34 | func FetchObject(ctx context.Context, id string) (*Object, error) { 35 | rdoc, err := NewDocumentLoader(ctx).LoadDocument(id) 36 | if err != nil { 37 | return nil, lib.Code(err, http.StatusUnprocessableEntity) 38 | } 39 | v, ok := rdoc.Document.(map[string]interface{}) 40 | if !ok { 41 | return nil, lib.Errorf(http.StatusInternalServerError, "can't cast %T to map[string]interface{}", v) 42 | } 43 | return &Object{V: v}, nil 44 | } 45 | 46 | // Creates a new object from a JSON object. 47 | func ParseObject(data []byte) (*Object, error) { 48 | var obj Object 49 | return &obj, json.Unmarshal(data, &obj.V) 50 | } 51 | 52 | // Creates a new list of objects from a JSON array. 53 | func ParseObjects(data []byte) ([]*Object, error) { 54 | var vs []map[string]interface{} 55 | if err := json.Unmarshal(data, &vs); err != nil { 56 | return nil, err 57 | } 58 | objs := make([]*Object, len(vs)) 59 | for i, v := range vs { 60 | objs[i] = &Object{V: v} 61 | } 62 | return objs, nil 63 | } 64 | 65 | func (obj Object) MarshalJSON() ([]byte, error) { 66 | return json.Marshal(obj.V) 67 | } 68 | 69 | func (obj *Object) UnmarshalJSON(data []byte) error { 70 | var v map[string]interface{} 71 | if err := json.Unmarshal(data, &v); err != nil { 72 | return err 73 | } 74 | obj.V = v 75 | return nil 76 | } 77 | 78 | // Revolver Ocelot. 79 | func (obj *Object) Obj() *Object { 80 | return obj 81 | } 82 | 83 | func (obj *Object) IsNull() bool { 84 | return obj == nil 85 | } 86 | 87 | // Returns the object's @id, or "". 88 | func (obj *Object) ID() string { 89 | if obj == nil { 90 | return "" 91 | } 92 | if obj.id == "" { 93 | obj.id = ToString(obj.Get("@id")) 94 | } 95 | return obj.id 96 | } 97 | 98 | // Sets the object's @id. 99 | func (obj *Object) SetID(id string) { 100 | obj.Set("@id", id) 101 | } 102 | 103 | // Returns the object's @value, or "". 104 | func (obj *Object) Value() string { 105 | if obj == nil { 106 | return "" 107 | } 108 | if obj.value == "" { 109 | obj.value = ToString(obj.Get("@value")) 110 | } 111 | return obj.value 112 | } 113 | 114 | // Sets the object's @value. 115 | func (obj *Object) SetValue(v string) { 116 | obj.Set("@value", v) 117 | } 118 | 119 | // Returns the object's @type, or nil. 120 | func (obj *Object) Type() []string { 121 | if obj == nil { 122 | return nil 123 | } 124 | if obj.typ == nil { 125 | obj.typ = ToStringSlice(obj.Get("@type")) 126 | } 127 | return obj.typ 128 | } 129 | 130 | // Sets the object's @type. 131 | func (obj *Object) SetType(ts ...string) { 132 | obj.Set("@type", ts) 133 | } 134 | 135 | // Nil-safe getter for attributes. 136 | func (obj *Object) Get(key string) interface{} { 137 | if obj != nil && obj.V != nil { 138 | return obj.V[key] 139 | } 140 | return nil 141 | } 142 | 143 | // Nil-safe setter for attributes. 144 | func (obj *Object) Set(key string, value interface{}) { 145 | if obj == nil { 146 | return 147 | } 148 | if obj.V == nil { 149 | obj.V = map[string]interface{}{key: value} 150 | return 151 | } 152 | switch v := value.(type) { 153 | case Entity: 154 | value = v.Obj().V 155 | case *Object: 156 | value = v.V 157 | } 158 | obj.V[key] = value 159 | switch key { 160 | case "@id": 161 | obj.id = "" 162 | case "@value": 163 | obj.value = "" 164 | case "@type": 165 | obj.typ = nil 166 | } 167 | } 168 | 169 | // Applies another object as a patch to this object. The mergeArrays argument specifies whether 170 | // values for existing keys should replace the existing value, or be added as an additional value. 171 | func (obj *Object) Apply(patch Entity, mergeArrays bool) error { 172 | patchV := patch.Obj().V 173 | for k, v := range patchV { 174 | if k == "@id" || !mergeArrays { 175 | obj.V[k] = v 176 | } else if arr, ok := v.([]interface{}); ok { 177 | obj.V[k] = append(ToSlice(obj.Get(k)), arr...) 178 | } else { 179 | obj.V[k] = append(ToSlice(obj.Get(k)), v) 180 | } 181 | } 182 | return nil 183 | } 184 | -------------------------------------------------------------------------------- /ld/object_test.go: -------------------------------------------------------------------------------- 1 | package ld 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestObjectSet(t *testing.T) { 11 | t.Run("Simple", func(t *testing.T) { 12 | obj := NewObject("https://example.com") 13 | obj.Set("key", "value") 14 | assert.Equal(t, "value", obj.Get("key")) 15 | }) 16 | t.Run("Object", func(t *testing.T) { 17 | obj := NewObject("https://example.com") 18 | obj.Set("key", NewObject("https://example.com/other")) 19 | assert.Equal(t, map[string]interface{}{ 20 | "@id": "https://example.com/other", 21 | }, obj.Get("key")) 22 | }) 23 | t.Run("Entity", func(t *testing.T) { 24 | obj := NewObject("https://example.com") 25 | obj.Set("key", Entity(NewObject("https://example.com/other"))) 26 | assert.Equal(t, map[string]interface{}{ 27 | "@id": "https://example.com/other", 28 | }, obj.Get("key")) 29 | }) 30 | } 31 | 32 | func TestObjectID(t *testing.T) { 33 | t.Run("None", func(t *testing.T) { 34 | obj, err := ParseObject([]byte(`{}`)) 35 | require.NoError(t, err) 36 | assert.Equal(t, "", obj.ID()) 37 | }) 38 | 39 | t.Run("Null", func(t *testing.T) { 40 | obj, err := ParseObject([]byte(`{ 41 | "@id": null 42 | }`)) 43 | require.NoError(t, err) 44 | assert.Equal(t, "", obj.ID()) 45 | }) 46 | 47 | t.Run("Number", func(t *testing.T) { 48 | obj, err := ParseObject([]byte(`{ 49 | "@id": 12345 50 | }`)) 51 | require.NoError(t, err) 52 | assert.Equal(t, "12345", obj.ID()) 53 | }) 54 | 55 | t.Run("String", func(t *testing.T) { 56 | obj, err := ParseObject([]byte(`{ 57 | "@id": "https://example.com" 58 | }`)) 59 | require.NoError(t, err) 60 | assert.Equal(t, "https://example.com", obj.ID()) 61 | }) 62 | 63 | t.Run("Array", func(t *testing.T) { 64 | obj, err := ParseObject([]byte(`{ 65 | "@id": ["https://example.com/1", "https://example.com/2"] 66 | }`)) 67 | require.NoError(t, err) 68 | assert.Equal(t, `https://example.com/1,https://example.com/2`, obj.ID()) 69 | }) 70 | } 71 | 72 | func TestObjectSetID(t *testing.T) { 73 | obj := NewObject("https://example.com") 74 | assert.Equal(t, "https://example.com", obj.ID()) 75 | obj.SetID("https://example.com/boop") 76 | assert.Equal(t, "https://example.com/boop", obj.ID()) 77 | } 78 | 79 | func TestObjectType(t *testing.T) { 80 | t.Run("None", func(t *testing.T) { 81 | obj, err := ParseObject([]byte(`{}`)) 82 | require.NoError(t, err) 83 | assert.Empty(t, obj.Type()) 84 | }) 85 | 86 | t.Run("Null", func(t *testing.T) { 87 | obj, err := ParseObject([]byte(`{"@type": null}`)) 88 | require.NoError(t, err) 89 | assert.Empty(t, obj.Type()) 90 | }) 91 | 92 | t.Run("Number", func(t *testing.T) { 93 | obj, err := ParseObject([]byte(`{ 94 | "@type": 12345 95 | }`)) 96 | require.NoError(t, err) 97 | assert.Equal(t, []string{ 98 | "12345", 99 | }, obj.Type()) 100 | }) 101 | 102 | t.Run("String", func(t *testing.T) { 103 | obj, err := ParseObject([]byte(`{ 104 | "@type": "http://www.w3.org/ns/activitystreams#Note" 105 | }`)) 106 | require.NoError(t, err) 107 | assert.Equal(t, []string{ 108 | "http://www.w3.org/ns/activitystreams#Note", 109 | }, obj.Type()) 110 | }) 111 | 112 | t.Run("Array", func(t *testing.T) { 113 | obj, err := ParseObject([]byte(`{ 114 | "@type": [ 115 | "http://www.w3.org/ns/activitystreams#Note", 116 | "http://www.w3.org/ns/activitystreams#Image" 117 | ] 118 | }`)) 119 | require.NoError(t, err) 120 | assert.Equal(t, []string{ 121 | "http://www.w3.org/ns/activitystreams#Note", 122 | "http://www.w3.org/ns/activitystreams#Image", 123 | }, obj.Type()) 124 | }) 125 | } 126 | 127 | func TestObjectSetType(t *testing.T) { 128 | obj := NewObject("https://example.com", "http://www.w3.org/ns/activitystreams#Note") 129 | assert.Equal(t, []string{"http://www.w3.org/ns/activitystreams#Note"}, obj.Type()) 130 | obj.SetType("http://www.w3.org/ns/activitystreams#Image") 131 | assert.Equal(t, []string{"http://www.w3.org/ns/activitystreams#Image"}, obj.Type()) 132 | } 133 | 134 | func TestObjectMerge(t *testing.T) { 135 | t.Run("New", func(t *testing.T) { 136 | obj, err := ParseObject([]byte(`{ 137 | "@id": "https://example.com" 138 | }`)) 139 | require.NoError(t, err) 140 | patch, err := ParseObject([]byte(`{ 141 | "@id": "https://example.com", 142 | "http://www.w3.org/ns/activitystreams#content": "hi" 143 | }`)) 144 | require.NoError(t, err) 145 | assert.NoError(t, obj.Apply(patch, false)) 146 | assert.Equal(t, obj.V, map[string]interface{}{ 147 | "@id": "https://example.com", 148 | "http://www.w3.org/ns/activitystreams#content": "hi", 149 | }) 150 | }) 151 | 152 | t.Run("Replace", func(t *testing.T) { 153 | obj, err := ParseObject([]byte(`{ 154 | "@id": "https://example.com", 155 | "http://www.w3.org/ns/activitystreams#content": "hi" 156 | }`)) 157 | require.NoError(t, err) 158 | patch, err := ParseObject([]byte(`{ 159 | "@id": "https://example.com", 160 | "http://www.w3.org/ns/activitystreams#content": "bye" 161 | }`)) 162 | require.NoError(t, err) 163 | assert.NoError(t, obj.Apply(patch, false)) 164 | assert.Equal(t, obj.V, map[string]interface{}{ 165 | "@id": "https://example.com", 166 | "http://www.w3.org/ns/activitystreams#content": "bye", 167 | }) 168 | }) 169 | 170 | t.Run("Merge", func(t *testing.T) { 171 | obj, err := ParseObject([]byte(`{ 172 | "@id": "https://example.com", 173 | "http://www.w3.org/ns/activitystreams#content": "hi" 174 | }`)) 175 | require.NoError(t, err) 176 | patch, err := ParseObject([]byte(`{ 177 | "@id": "https://example.com", 178 | "http://www.w3.org/ns/activitystreams#content": "bye" 179 | }`)) 180 | require.NoError(t, err) 181 | assert.NoError(t, obj.Apply(patch, true)) 182 | assert.Equal(t, obj.V, map[string]interface{}{ 183 | "@id": "https://example.com", 184 | "http://www.w3.org/ns/activitystreams#content": []interface{}{ 185 | "hi", 186 | "bye", 187 | }, 188 | }) 189 | }) 190 | } 191 | -------------------------------------------------------------------------------- /ld/source.go: -------------------------------------------------------------------------------- 1 | package ld 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Source int 8 | 9 | const ( 10 | ClientToServer Source = iota 11 | ServerToServer 12 | ) 13 | 14 | func (s Source) String() string { 15 | switch s { 16 | case ClientToServer: 17 | return "ClientToServer" 18 | case ServerToServer: 19 | return "ServerToServer" 20 | default: 21 | return fmt.Sprintf("[%d]", s) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/clog/clog.go: -------------------------------------------------------------------------------- 1 | package clog 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/meowpub/meow/lib" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | func DPanic(ctx context.Context, msg string, fields ...zap.Field) { 11 | lib.GetLogger(ctx).DPanic(msg, fields...) 12 | } 13 | 14 | func Debug(ctx context.Context, msg string, fields ...zap.Field) { 15 | lib.GetLogger(ctx).Debug(msg, fields...) 16 | } 17 | 18 | func Error(ctx context.Context, msg string, fields ...zap.Field) { 19 | lib.GetLogger(ctx).Error(msg, fields...) 20 | } 21 | 22 | func Fatal(ctx context.Context, msg string, fields ...zap.Field) { 23 | lib.GetLogger(ctx).Fatal(msg, fields...) 24 | } 25 | 26 | func Info(ctx context.Context, msg string, fields ...zap.Field) { 27 | lib.GetLogger(ctx).Info(msg, fields...) 28 | } 29 | 30 | func Panic(ctx context.Context, msg string, fields ...zap.Field) { 31 | lib.GetLogger(ctx).Panic(msg, fields...) 32 | } 33 | 34 | func Warn(ctx context.Context, msg string, fields ...zap.Field) { 35 | lib.GetLogger(ctx).Warn(msg, fields...) 36 | } 37 | -------------------------------------------------------------------------------- /lib/context.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/go-redis/redis" 8 | "github.com/jinzhu/gorm" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | type ctxKey string 13 | 14 | const ( 15 | ctxKeyLogger ctxKey = "logger" 16 | ctxKeyDB ctxKey = "db" 17 | ctxKeyRedis ctxKey = "redis" 18 | ctxKeyRender ctxKey = "render" 19 | ctxKeyHttpClient ctxKey = "httpclient" 20 | ) 21 | 22 | // GetLogger returns the logger associated with the context, or the global logger if none is set. 23 | func GetLogger(ctx context.Context) *zap.Logger { 24 | if ctx == nil { 25 | ctx = context.Background() 26 | } 27 | if l, ok := ctx.Value(ctxKeyLogger).(*zap.Logger); ok { 28 | return l 29 | } 30 | return zap.L() 31 | } 32 | 33 | // WithLogger associates a logger with a context. 34 | func WithLogger(ctx context.Context, l *zap.Logger) context.Context { 35 | return context.WithValue(ctx, ctxKeyLogger, l) 36 | } 37 | 38 | // WithLogger is a shorthand for associating a named sub-logger with a context. 39 | func WithNamedLogger(ctx context.Context, name string) context.Context { 40 | return context.WithValue(ctx, ctxKeyLogger, GetLogger(ctx).Named(name)) 41 | } 42 | 43 | func WithLoggerFields(ctx context.Context, f ...zap.Field) context.Context { 44 | return context.WithValue(ctx, ctxKeyLogger, GetLogger(ctx).With(f...)) 45 | } 46 | 47 | // GetDB returns the DB associated with the context, or nil. 48 | func GetDB(ctx context.Context) *gorm.DB { 49 | db, _ := ctx.Value(ctxKeyDB).(*gorm.DB) 50 | return db 51 | } 52 | 53 | // WithDB associates a DB with a context. 54 | func WithDB(ctx context.Context, db *gorm.DB) context.Context { 55 | return context.WithValue(ctx, ctxKeyDB, db) 56 | } 57 | 58 | // GetRedis returns the Redis client associated with the context, or nil. 59 | func GetRedis(ctx context.Context) *redis.Client { 60 | r, _ := ctx.Value(ctxKeyRedis).(*redis.Client) 61 | return r 62 | } 63 | 64 | // WithRedis associates a Redis connection with a context. 65 | func WithRedis(ctx context.Context, r *redis.Client) context.Context { 66 | return context.WithValue(ctx, ctxKeyRedis, r) 67 | } 68 | 69 | // GetHttpClient returns the Http client associated with the context, or nil 70 | func GetHttpClient(ctx context.Context) *http.Client { 71 | r, _ := ctx.Value(ctxKeyHttpClient).(*http.Client) 72 | return r 73 | } 74 | 75 | // WithHttpClient associates the HttpClient with a context 76 | func WithHttpClient(ctx context.Context, cli *http.Client) context.Context { 77 | return context.WithValue(ctx, ctxKeyHttpClient, cli) 78 | } 79 | -------------------------------------------------------------------------------- /lib/context_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/go-redis/redis" 8 | "github.com/jinzhu/gorm" 9 | "go.uber.org/zap" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | 14 | _ "github.com/jinzhu/gorm/dialects/sqlite" 15 | ) 16 | 17 | func TestContextLogger(t *testing.T) { 18 | l, err := zap.NewDevelopment() 19 | require.NoError(t, err) 20 | 21 | t.Run("Global", func(t *testing.T) { 22 | defer zap.ReplaceGlobals(l)() 23 | assert.Equal(t, l, GetLogger(context.Background())) 24 | 25 | t.Run("nil", func(t *testing.T) { 26 | assert.Equal(t, l, GetLogger(nil)) 27 | }) 28 | }) 29 | 30 | t.Run("Named", func(t *testing.T) { 31 | l2 := GetLogger(WithNamedLogger(context.Background(), "sub")) 32 | assert.NotNil(t, l2) 33 | assert.NotEqual(t, l, l2) 34 | }) 35 | 36 | t.Run("Local", func(t *testing.T) { 37 | assert.Equal(t, l, GetLogger(WithLogger(context.Background(), l))) 38 | }) 39 | } 40 | 41 | func TestContextDB(t *testing.T) { 42 | db, err := gorm.Open("sqlite3", ":memory:") 43 | require.NoError(t, err) 44 | assert.Equal(t, db, GetDB(WithDB(context.Background(), db))) 45 | } 46 | 47 | func TestContextRedis(t *testing.T) { 48 | r := redis.NewClient(&redis.Options{}) 49 | assert.Equal(t, r, GetRedis(WithRedis(context.Background(), r))) 50 | } 51 | -------------------------------------------------------------------------------- /lib/errors.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | // Error attaches a status code to an error. 11 | type Err struct { 12 | Err error 13 | StatusCode int 14 | } 15 | 16 | // Wraps an error with an error code. 17 | func Code(err error, code int) error { 18 | if err != nil { 19 | return Err{err, code} 20 | } 21 | return nil 22 | } 23 | 24 | // Wraps an error with an error code and message. 25 | func Wrap(err error, code int, s string) error { 26 | return errors.Wrap(Code(err, code), s) 27 | } 28 | 29 | // Wraps an error with an error code and message. 30 | func Wrapf(err error, code int, s string, args ...interface{}) error { 31 | return errors.Wrapf(Code(err, code), s, args...) 32 | } 33 | 34 | // Returns an error with the given message and status code. 35 | func Error(code int, s string) error { 36 | return Code(errors.New(s), code) 37 | } 38 | 39 | // Returns an error with the given message format and status code. 40 | func Errorf(code int, s string, args ...interface{}) error { 41 | return Code(errors.Errorf(s, args...), code) 42 | } 43 | 44 | // Satisfies error. 45 | func (err Err) Error() string { return err.Err.Error() } 46 | 47 | // Satisfies errors.causer. 48 | func (err Err) Cause() error { return err.Err } 49 | 50 | // Report logs an error to the global logger. Useful for `defer conn.Close()`-type constructs, 51 | // where there's not really anything useful to do with the error, but you still want to log it. 52 | func Report(ctx context.Context, err error, msg string) { 53 | if err != nil { 54 | GetLogger(ctx).Error(msg, zap.Error(err)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/http.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-redis/redis" 7 | "github.com/gregjones/httpcache" 8 | 9 | "github.com/meowpub/meow/config" 10 | "github.com/meowpub/meow/lib/rediscache" 11 | ) 12 | 13 | // CreateHttpClient creates a http.Client instance which 14 | // will cache responses inside our Redis database 15 | func CreateHttpClient(conn redis.Conn) *http.Client { 16 | // Build a cache 17 | cache := rediscache.NewWithClient(conn, config.RedisKeyspace()+"httpcache:") 18 | cacheTransport := httpcache.NewTransport(cache) 19 | 20 | // Mark cached responses for debugging purposes 21 | cacheTransport.MarkCachedResponses = true 22 | 23 | // Return the resulting client 24 | return cacheTransport.Client() 25 | } 26 | -------------------------------------------------------------------------------- /lib/rediscache/redis.go: -------------------------------------------------------------------------------- 1 | // Package redis provides a redis interface for http caching. 2 | package rediscache 3 | 4 | import ( 5 | "github.com/go-redis/redis" 6 | "github.com/gregjones/httpcache" 7 | "time" 8 | ) 9 | 10 | // cache is an implementation of httpcache.Cache that caches responses in a 11 | // redis server. 12 | type cache struct { 13 | conn redis.Conn 14 | prefix string 15 | } 16 | 17 | func (c cache) key(k string) string { 18 | return c.prefix + k 19 | } 20 | 21 | // Get returns the response corresponding to key if present. 22 | func (c cache) Get(key string) (resp []byte, ok bool) { 23 | return c.Get(c.key(key)) 24 | } 25 | 26 | // Set saves a response to the cache as key. 27 | func (c cache) Set(key string, resp []byte) { 28 | c.conn.Set(c.key(key), resp, time.Duration(0)) 29 | } 30 | 31 | // Delete removes the response with key from the cache. 32 | func (c cache) Delete(key string) { 33 | c.Delete(c.key(key)) 34 | } 35 | 36 | // NewWithClient returns a new Cache with the given redis connection. 37 | func NewWithClient(client redis.Conn, prefix string) httpcache.Cache { 38 | return cache{ 39 | conn: client, 40 | prefix: prefix, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/snowflakes.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "github.com/bwmarrin/snowflake" 5 | ) 6 | 7 | //go:generate mockgen -package=lib -source=snowflakes.go -destination=snowflakes.mock.go 8 | 9 | var DefaultSnowflakeGenerator SnowflakeGenerator 10 | 11 | func init() { 12 | node, err := snowflake.NewNode(0) 13 | if err != nil { 14 | panic(err) 15 | } 16 | DefaultSnowflakeGenerator = node 17 | } 18 | 19 | type SnowflakeGenerator interface { 20 | Generate() snowflake.ID 21 | } 22 | 23 | // GenSnowflake generates a snowflake ID. 24 | func GenSnowflake() snowflake.ID { 25 | return DefaultSnowflakeGenerator.Generate() 26 | } 27 | -------------------------------------------------------------------------------- /lib/snowflakes.mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: snowflakes.go 3 | 4 | // Package lib is a generated GoMock package. 5 | package lib 6 | 7 | import ( 8 | snowflake "github.com/bwmarrin/snowflake" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockSnowflakeGenerator is a mock of SnowflakeGenerator interface 14 | type MockSnowflakeGenerator struct { 15 | ctrl *gomock.Controller 16 | recorder *MockSnowflakeGeneratorMockRecorder 17 | } 18 | 19 | // MockSnowflakeGeneratorMockRecorder is the mock recorder for MockSnowflakeGenerator 20 | type MockSnowflakeGeneratorMockRecorder struct { 21 | mock *MockSnowflakeGenerator 22 | } 23 | 24 | // NewMockSnowflakeGenerator creates a new mock instance 25 | func NewMockSnowflakeGenerator(ctrl *gomock.Controller) *MockSnowflakeGenerator { 26 | mock := &MockSnowflakeGenerator{ctrl: ctrl} 27 | mock.recorder = &MockSnowflakeGeneratorMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockSnowflakeGenerator) EXPECT() *MockSnowflakeGeneratorMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // Generate mocks base method 37 | func (m *MockSnowflakeGenerator) Generate() snowflake.ID { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "Generate") 40 | ret0, _ := ret[0].(snowflake.ID) 41 | return ret0 42 | } 43 | 44 | // Generate indicates an expected call of Generate 45 | func (mr *MockSnowflakeGeneratorMockRecorder) Generate() *gomock.Call { 46 | mr.mock.ctrl.T.Helper() 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Generate", reflect.TypeOf((*MockSnowflakeGenerator)(nil).Generate)) 48 | } 49 | -------------------------------------------------------------------------------- /lib/tokens.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "crypto/rand" 5 | "io" 6 | 7 | "github.com/keybase/saltpack/encoding/basex" 8 | ) 9 | 10 | // Generates a random, 64-bit token, encoded as base62. 11 | func GenToken() (string, error) { 12 | var data [64]byte 13 | if _, err := io.ReadFull(rand.Reader, data[:]); err != nil { 14 | return "", err 15 | } 16 | return basex.Base62StdEncoding.EncodeToString(data[:]), nil 17 | } 18 | -------------------------------------------------------------------------------- /lib/url.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | func NormalizeURL(u url.URL) url.URL { 9 | // Remove query/fragment portions as we cannot route on them 10 | u.RawQuery = "" 11 | u.ForceQuery = false 12 | u.Fragment = "" 13 | 14 | // Normalize path to remove any trailing slashes 15 | u.Path = strings.TrimSuffix(u.Path, "/") 16 | u.RawPath = u.EscapedPath() 17 | 18 | return u 19 | } 20 | 21 | func RootURL(url url.URL) url.URL { 22 | // Build the Url of the root object for traversal 23 | rootURL := url 24 | rootURL.Path = "" 25 | rootURL.RawPath = rootURL.EscapedPath() 26 | return rootURL 27 | } 28 | -------------------------------------------------------------------------------- /lib/xrd/xrd.go: -------------------------------------------------------------------------------- 1 | package xrd 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "time" 7 | ) 8 | 9 | type XRD struct { 10 | ID string `json:"-"` 11 | Expires *time.Time `json:"expires,omitempty"` 12 | Subject string `json:"subject,omitempty"` 13 | Aliases []string `json:"aliases,omitempty"` 14 | Properties map[string]string `json:"properties,omitempty"` 15 | Links []Link `json:"links,omitempty"` 16 | } 17 | 18 | func (x *XRD) MarshalXML(e *xml.Encoder, start xml.StartElement) error { 19 | xm := xmlXRD{ 20 | ID: x.ID, 21 | Expires: x.Expires, 22 | Subject: x.Subject, 23 | Aliases: x.Aliases, 24 | Properties: make([]xmlProperty, 0, len(x.Properties)), 25 | Links: x.Links, 26 | } 27 | 28 | for k, v := range x.Properties { 29 | xm.Properties = append(xm.Properties, xmlProperty{ 30 | Type: k, 31 | Value: v, 32 | }) 33 | } 34 | 35 | return e.EncodeElement(xm, start) 36 | } 37 | 38 | type xmlXRD struct { 39 | XMLName xml.Name `xml:"XRD"` 40 | ID string `xml:"http://www.w3.org/XML/1998/namespace id,attr,omitempty"` 41 | Expires *time.Time `xml:"Expires,omitempty"` 42 | Subject string `xml:"Subject,omitempty"` 43 | Aliases []string `xml:"Alias"` 44 | Properties []xmlProperty `xml:"Property"` 45 | Links []Link `xml:"Link"` 46 | } 47 | 48 | type xmlProperty struct { 49 | XMLName xml.Name `xml:"Property" json:"-"` 50 | 51 | Type string `xml:"type,attr"` 52 | Value string `xml:",chardata"` 53 | } 54 | 55 | type Link struct { 56 | Rel string `json:"rel,omitempty"` 57 | Type string `json:"type,omitempty"` 58 | HRef string `json:"href,omitempty"` 59 | Template string `json:"template,omitempty"` 60 | Properties map[string]string `json:"properties,omitempty"` 61 | Titles map[string]string `json:"title,omitempty"` 62 | } 63 | 64 | func (l *Link) MarshalXML(e *xml.Encoder, start xml.StartElement) error { 65 | xl := xmlLink{ 66 | Rel: l.Rel, 67 | Type: l.Type, 68 | HRef: l.HRef, 69 | Template: l.Template, 70 | Properties: make([]xmlProperty, 0, len(l.Properties)), 71 | Titles: make([]xmlTitle, 0, len(l.Titles)), 72 | } 73 | 74 | for k, v := range l.Properties { 75 | xl.Properties = append(xl.Properties, xmlProperty{ 76 | Type: k, 77 | Value: v, 78 | }) 79 | } 80 | 81 | for k, v := range l.Titles { 82 | if k == "und" { 83 | k = "" 84 | } 85 | 86 | xl.Titles = append(xl.Titles, xmlTitle{ 87 | Lang: k, 88 | Value: v, 89 | }) 90 | } 91 | 92 | return e.EncodeElement(xl, start) 93 | } 94 | 95 | type xmlLink struct { 96 | XMLName xml.Name `xml:"Link" json:"-"` 97 | Rel string `xml:"rel,attr,omitempty"` 98 | Type string `xml:"type,attr,omitempty"` 99 | HRef string `xml:"href,attr,omitempty"` 100 | Template string `xml:"template,attr,omitempty"` 101 | Properties []xmlProperty `xml:"Property,omitempty"` 102 | Titles []xmlTitle `xml:"Title,omitempty"` 103 | } 104 | 105 | type xmlTitle struct { 106 | XMLName xml.Name `xml:"Title" json:"-"` 107 | Lang string `xml:"http://www.w3.org/XML/1998/namespace lang,attr,omitempty"` 108 | Value string `xml:",chardata"` 109 | } 110 | 111 | func UnmarshalXRD(buf []byte) (*XRD, error) { 112 | x := &XRD{} 113 | if err := xml.Unmarshal(buf, x); err != nil { 114 | return nil, err 115 | } 116 | return x, nil 117 | } 118 | 119 | func (x *XRD) MarshalXRD() ([]byte, error) { 120 | return xml.Marshal(x) 121 | } 122 | 123 | func (x *XRD) MarshalIndentXRD() ([]byte, error) { 124 | return xml.MarshalIndent(x, "", "\t") 125 | } 126 | 127 | func (x *XRD) MarshalJRD() ([]byte, error) { 128 | return json.Marshal(x) 129 | } 130 | 131 | func (x *XRD) MarshalIndentJRD() ([]byte, error) { 132 | return json.MarshalIndent(x, "", "\t") 133 | } 134 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/meowpub/meow/cmd" 5 | 6 | // Autoload a .env file as environment variables. 7 | _ "github.com/joho/godotenv/autoload" 8 | 9 | // Load database drivers. 10 | _ "github.com/jinzhu/gorm/dialects/postgres" 11 | _ "github.com/mattes/migrate/database/postgres" 12 | 13 | // Why do I have to load support for local files for migrations. 14 | _ "github.com/mattes/migrate/source/file" 15 | ) 16 | 17 | //go:generate go generate ./models 18 | 19 | func main() { 20 | cmd.Execute() 21 | } 22 | -------------------------------------------------------------------------------- /migrations/1524325015_entities.down.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1524325015 3 | -- 4 | 5 | BEGIN; 6 | 7 | DROP TABLE entities; 8 | 9 | COMMIT; 10 | -------------------------------------------------------------------------------- /migrations/1524325015_entities.up.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1524325015 3 | -- 4 | 5 | BEGIN; 6 | 7 | -- The entities table is really simple - just JSON blobs and indices. 8 | -- Turns out Postgres is better at being MongoDB than MongoDB is. Not webscale tho. 9 | CREATE TABLE entities ( data JSONB NOT NULL ); 10 | 11 | -- Ensure that no entities lack an '@id', not just because that's incorrect, but because 12 | -- that caused trouble with the UNIQUE constraint (NULL != NULL for some reason). 13 | ALTER TABLE entities ADD CONSTRAINT entities_data_id_not_null_check CHECK (data ? '@id'); 14 | 15 | -- The indexing here is done on "->>", which coerces to string in case it's a different type. 16 | CREATE UNIQUE INDEX entities_data_id_idx ON entities ((data ->> '@id')); 17 | 18 | COMMIT; 19 | -------------------------------------------------------------------------------- /migrations/1524569269_oauth2.down.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1524569269 3 | -- 4 | 5 | BEGIN; 6 | 7 | DROP TABLE clients; 8 | 9 | COMMIT; 10 | -------------------------------------------------------------------------------- /migrations/1524569269_oauth2.up.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1524569269 3 | -- 4 | 5 | BEGIN; 6 | 7 | CREATE TABLE clients ( 8 | id BIGINT PRIMARY KEY, 9 | name VARCHAR(255) NOT NULL CHECK (name != ''), 10 | description VARCHAR(255) NOT NULL CHECK (description != ''), 11 | redirect_uri VARCHAR(255) NOT NULL CHECK (redirect_uri != ''), 12 | secret VARCHAR(255) NOT NULL CHECK (secret != '') 13 | ); 14 | 15 | COMMIT; 16 | -------------------------------------------------------------------------------- /migrations/1524739570_entity_2_entity_harder.down.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1524739570 3 | -- 4 | 5 | BEGIN; 6 | 7 | ALTER TABLE entities DROP COLUMN id; 8 | 9 | COMMIT; 10 | -------------------------------------------------------------------------------- /migrations/1524739570_entity_2_entity_harder.up.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1524739570 3 | -- 4 | 5 | BEGIN; 6 | 7 | ALTER TABLE entities ADD COLUMN id BIGINT PRIMARY KEY; 8 | ALTER TABLE entities ADD CHECK (id != 0); 9 | 10 | COMMIT; 11 | -------------------------------------------------------------------------------- /migrations/1525550010_users.down.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1525550010 3 | -- 4 | 5 | BEGIN; 6 | 7 | DROP TABLE users; 8 | 9 | COMMIT; 10 | -------------------------------------------------------------------------------- /migrations/1525550010_users.up.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1525550010 3 | -- 4 | 5 | BEGIN; 6 | 7 | CREATE TABLE users ( 8 | id BIGINT PRIMARY KEY, 9 | entity_id BIGINT NOT NULL UNIQUE REFERENCES entities (id) ON DELETE CASCADE, 10 | email VARCHAR(255) NOT NULL UNIQUE CHECK (email != ''), 11 | password_hash VARCHAR(255) NOT NULL CHECK (password_hash != '') 12 | ); 13 | 14 | COMMIT; 15 | -------------------------------------------------------------------------------- /migrations/1525551737_entity_kind.down.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1525551737 3 | -- 4 | 5 | BEGIN; 6 | 7 | ALTER TABLE entities DROP COLUMN kind; 8 | 9 | COMMIT; 10 | -------------------------------------------------------------------------------- /migrations/1525551737_entity_kind.up.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1525551737 3 | -- 4 | 5 | BEGIN; 6 | 7 | ALTER TABLE entities ADD COLUMN kind VARCHAR(16) NOT NULL; 8 | ALTER TABLE entities ADD CHECK (kind != ''); 9 | 10 | COMMIT; 11 | -------------------------------------------------------------------------------- /migrations/1525606933_client_owners.down.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1525606933 3 | -- 4 | 5 | BEGIN; 6 | 7 | ALTER TABLE clients DROP COLUMN owner_id; 8 | 9 | COMMIT; 10 | -------------------------------------------------------------------------------- /migrations/1525606933_client_owners.up.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1525606933 3 | -- 4 | 5 | BEGIN; 6 | 7 | ALTER TABLE clients ADD COLUMN owner_id BIGINT REFERENCES users (id); 8 | ALTER TABLE clients ADD CHECK (owner_id != 0); 9 | 10 | COMMIT; 11 | -------------------------------------------------------------------------------- /migrations/1526417398_entity_id_validation.down.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1526417398 3 | -- 4 | 5 | BEGIN; 6 | 7 | ALTER TABLE entities DROP CONSTRAINT entities_data_id_not_empty_check; 8 | 9 | COMMIT; 10 | -------------------------------------------------------------------------------- /migrations/1526417398_entity_id_validation.up.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1526417398 3 | -- 4 | 5 | BEGIN; 6 | 7 | -- Require that the ID is non-empty. 8 | ALTER TABLE entities ADD CONSTRAINT entities_data_id_not_empty_check CHECK ( 9 | trim(both from (data ->> '@id')) != '' 10 | ); 11 | 12 | -- Require that the ID has a http:// or https:// protocol. 13 | ALTER TABLE entities ADD CONSTRAINT entities_data_id_protocol_check CHECK ( 14 | ((data ->> '@id') LIKE 'http://%') OR ((data ->> '@id') LIKE 'https://%') 15 | ); 16 | 17 | COMMIT; 18 | -------------------------------------------------------------------------------- /migrations/1528746007_stream_items.down.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1528746007 3 | -- 4 | 5 | BEGIN; 6 | 7 | DROP TABLE stream_items; 8 | 9 | COMMIT; 10 | -------------------------------------------------------------------------------- /migrations/1528746007_stream_items.up.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- version: 1528746007 3 | -- 4 | 5 | BEGIN; 6 | 7 | CREATE TABLE stream_items ( 8 | item_id BIGINT PRIMARY KEY, 9 | stream_id BIGINT NOT NULL, 10 | entity_id BIGINT NOT NULL 11 | ); 12 | 13 | CREATE UNIQUE INDEX ON stream_items (stream_id, entity_id); 14 | CREATE INDEX ON stream_items (stream_id, item_id); 15 | 16 | COMMIT; 17 | -------------------------------------------------------------------------------- /models/accesstoken.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/go-redis/redis" 8 | ) 9 | 10 | //go:generate mockgen -package=models -source=accesstoken.go -destination=accesstoken.mock.go 11 | 12 | // AccessToken stores an active access token in Redis. See also: RefreshToken. 13 | type AccessToken struct { 14 | Token string `json:"token"` 15 | ClientID string `json:"client_id"` 16 | Scope string `json:"scope"` 17 | AuthorizationUserData 18 | } 19 | 20 | type AccessTokenStore interface { 21 | Set(token *AccessToken, ttl time.Duration) error 22 | Get(token string) (*AccessToken, error) 23 | Delete(token string) error 24 | } 25 | 26 | type accessTokenStore struct { 27 | K string 28 | R *redis.Client 29 | } 30 | 31 | func NewAccessTokenStore(keyspace string, r *redis.Client) AccessTokenStore { 32 | return &accessTokenStore{keyspace, r} 33 | } 34 | 35 | func (s *accessTokenStore) key(code string) string { 36 | return s.K + ":oauth2:access_tokens:" + code 37 | } 38 | 39 | func (s *accessTokenStore) Set(token *AccessToken, ttl time.Duration) error { 40 | if ttl == 0 { 41 | return ErrNoTTL 42 | } 43 | data, err := json.Marshal(token) 44 | if err != nil { 45 | return err 46 | } 47 | return s.R.Set(s.key(token.Token), data, ttl).Err() 48 | } 49 | 50 | func (s *accessTokenStore) Get(token string) (*AccessToken, error) { 51 | cmd := s.R.Get(s.key(token)) 52 | if err := cmd.Err(); err != nil { 53 | if err == redis.Nil { 54 | return nil, NotFoundError("token is invalid or expired") 55 | } 56 | return nil, err 57 | } 58 | data, err := cmd.Bytes() 59 | if err != nil { 60 | return nil, err 61 | } 62 | var tok AccessToken 63 | return &tok, json.Unmarshal(data, &tok) 64 | } 65 | 66 | func (s *accessTokenStore) Delete(token string) error { 67 | return s.R.Del(s.key(token)).Err() 68 | } 69 | -------------------------------------------------------------------------------- /models/accesstoken.mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: accesstoken.go 3 | 4 | // Package models is a generated GoMock package. 5 | package models 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | reflect "reflect" 10 | time "time" 11 | ) 12 | 13 | // MockAccessTokenStore is a mock of AccessTokenStore interface 14 | type MockAccessTokenStore struct { 15 | ctrl *gomock.Controller 16 | recorder *MockAccessTokenStoreMockRecorder 17 | } 18 | 19 | // MockAccessTokenStoreMockRecorder is the mock recorder for MockAccessTokenStore 20 | type MockAccessTokenStoreMockRecorder struct { 21 | mock *MockAccessTokenStore 22 | } 23 | 24 | // NewMockAccessTokenStore creates a new mock instance 25 | func NewMockAccessTokenStore(ctrl *gomock.Controller) *MockAccessTokenStore { 26 | mock := &MockAccessTokenStore{ctrl: ctrl} 27 | mock.recorder = &MockAccessTokenStoreMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockAccessTokenStore) EXPECT() *MockAccessTokenStoreMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // Set mocks base method 37 | func (m *MockAccessTokenStore) Set(token *AccessToken, ttl time.Duration) error { 38 | ret := m.ctrl.Call(m, "Set", token, ttl) 39 | ret0, _ := ret[0].(error) 40 | return ret0 41 | } 42 | 43 | // Set indicates an expected call of Set 44 | func (mr *MockAccessTokenStoreMockRecorder) Set(token, ttl interface{}) *gomock.Call { 45 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockAccessTokenStore)(nil).Set), token, ttl) 46 | } 47 | 48 | // Get mocks base method 49 | func (m *MockAccessTokenStore) Get(token string) (*AccessToken, error) { 50 | ret := m.ctrl.Call(m, "Get", token) 51 | ret0, _ := ret[0].(*AccessToken) 52 | ret1, _ := ret[1].(error) 53 | return ret0, ret1 54 | } 55 | 56 | // Get indicates an expected call of Get 57 | func (mr *MockAccessTokenStoreMockRecorder) Get(token interface{}) *gomock.Call { 58 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockAccessTokenStore)(nil).Get), token) 59 | } 60 | 61 | // Delete mocks base method 62 | func (m *MockAccessTokenStore) Delete(token string) error { 63 | ret := m.ctrl.Call(m, "Delete", token) 64 | ret0, _ := ret[0].(error) 65 | return ret0 66 | } 67 | 68 | // Delete indicates an expected call of Delete 69 | func (mr *MockAccessTokenStoreMockRecorder) Delete(token interface{}) *gomock.Call { 70 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAccessTokenStore)(nil).Delete), token) 71 | } 72 | -------------------------------------------------------------------------------- /models/accesstoken_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/alicebob/miniredis" 8 | "github.com/go-redis/redis" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestAccessTokenStore(t *testing.T) { 14 | minir, err := miniredis.Run() 15 | require.NoError(t, err) 16 | 17 | r := redis.NewClient(&redis.Options{Addr: minir.Addr()}) 18 | store := NewAccessTokenStore("meow", r) 19 | token := &AccessToken{ 20 | Token: "8f9f21e6-d7d9-44f4-a018-295916935375", 21 | ClientID: "f41d0dea-7a3b-4ca6-8b16-2eefe07b28f0", 22 | Scope: "myscope", 23 | AuthorizationUserData: AuthorizationUserData{ 24 | UserID: 12345, 25 | }, 26 | } 27 | 28 | require.Len(t, r.Keys("*").Val(), 0) 29 | 30 | t.Run("Not Found", func(t *testing.T) { 31 | _, err := store.Get(token.Token) 32 | assert.EqualError(t, err, "token is invalid or expired") 33 | assert.True(t, IsNotFound(err)) 34 | }) 35 | 36 | t.Run("Set", func(t *testing.T) { 37 | t.Run("NoTTL", func(t *testing.T) { 38 | assert.EqualError(t, store.Set(token, 0), "a TTL is required, but not given") 39 | }) 40 | 41 | assert.NoError(t, store.Set(token, 15*time.Minute)) 42 | assert.Equal(t, r.Keys("*").Val(), []string{"meow:oauth2:access_tokens:" + token.Token}) 43 | }) 44 | 45 | t.Run("Get", func(t *testing.T) { 46 | token2, err := store.Get(token.Token) 47 | require.NoError(t, err) 48 | assert.Equal(t, token, token2) 49 | }) 50 | 51 | t.Run("Delete", func(t *testing.T) { 52 | assert.NoError(t, store.Delete(token.Token)) 53 | require.Len(t, r.Keys("*").Val(), 0) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /models/authorization.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/bwmarrin/snowflake" 8 | "github.com/go-redis/redis" 9 | ) 10 | 11 | //go:generate mockgen -package=models -source=authorization.go -destination=authorization.mock.go 12 | 13 | // Authorization stores server-side state for an authorization. It's stored temporarily, before 14 | // being traded for an AccessToken and a RefreshToken and subsequently deleted. 15 | type Authorization struct { 16 | Code string `json:"code"` 17 | ClientID string `json:"client_id"` 18 | Scope string `json:"scope"` 19 | RedirectURI string `json:"redirect_uri"` 20 | State string `json:"state"` 21 | AuthorizationUserData 22 | } 23 | 24 | type AuthorizationUserData struct { 25 | UserID snowflake.ID `json:"user_id"` 26 | } 27 | 28 | // AuthorizationStore stores Authorizations (in Redis). 29 | type AuthorizationStore interface { 30 | Set(auth *Authorization, ttl time.Duration) error 31 | Get(code string) (*Authorization, error) 32 | Delete(code string) error 33 | } 34 | 35 | type authorizationStore struct { 36 | K string 37 | R *redis.Client 38 | } 39 | 40 | func NewAuthorizationStore(keyspace string, r *redis.Client) AuthorizationStore { 41 | return &authorizationStore{keyspace, r} 42 | } 43 | 44 | func (s authorizationStore) key(code string) string { 45 | return s.K + ":oauth2:authorizations:" + code 46 | } 47 | 48 | func (s authorizationStore) Set(auth *Authorization, ttl time.Duration) error { 49 | if ttl == 0 { 50 | return ErrNoTTL 51 | } 52 | data, err := json.Marshal(auth) 53 | if err != nil { 54 | return err 55 | } 56 | return s.R.Set(s.key(auth.Code), data, ttl).Err() 57 | } 58 | 59 | func (s authorizationStore) Get(code string) (*Authorization, error) { 60 | cmd := s.R.Get(s.key(code)) 61 | if err := cmd.Err(); err != nil { 62 | if err == redis.Nil { 63 | return nil, NotFoundError("invalid authorization code") 64 | } 65 | return nil, err 66 | } 67 | data, err := cmd.Bytes() 68 | if err != nil { 69 | return nil, err 70 | } 71 | var auth Authorization 72 | return &auth, json.Unmarshal(data, &auth) 73 | } 74 | 75 | func (s authorizationStore) Delete(code string) error { 76 | return s.R.Del(s.key(code)).Err() 77 | } 78 | -------------------------------------------------------------------------------- /models/authorization.mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: authorization.go 3 | 4 | // Package models is a generated GoMock package. 5 | package models 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | reflect "reflect" 10 | time "time" 11 | ) 12 | 13 | // MockAuthorizationStore is a mock of AuthorizationStore interface 14 | type MockAuthorizationStore struct { 15 | ctrl *gomock.Controller 16 | recorder *MockAuthorizationStoreMockRecorder 17 | } 18 | 19 | // MockAuthorizationStoreMockRecorder is the mock recorder for MockAuthorizationStore 20 | type MockAuthorizationStoreMockRecorder struct { 21 | mock *MockAuthorizationStore 22 | } 23 | 24 | // NewMockAuthorizationStore creates a new mock instance 25 | func NewMockAuthorizationStore(ctrl *gomock.Controller) *MockAuthorizationStore { 26 | mock := &MockAuthorizationStore{ctrl: ctrl} 27 | mock.recorder = &MockAuthorizationStoreMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockAuthorizationStore) EXPECT() *MockAuthorizationStoreMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // Set mocks base method 37 | func (m *MockAuthorizationStore) Set(auth *Authorization, ttl time.Duration) error { 38 | ret := m.ctrl.Call(m, "Set", auth, ttl) 39 | ret0, _ := ret[0].(error) 40 | return ret0 41 | } 42 | 43 | // Set indicates an expected call of Set 44 | func (mr *MockAuthorizationStoreMockRecorder) Set(auth, ttl interface{}) *gomock.Call { 45 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockAuthorizationStore)(nil).Set), auth, ttl) 46 | } 47 | 48 | // Get mocks base method 49 | func (m *MockAuthorizationStore) Get(code string) (*Authorization, error) { 50 | ret := m.ctrl.Call(m, "Get", code) 51 | ret0, _ := ret[0].(*Authorization) 52 | ret1, _ := ret[1].(error) 53 | return ret0, ret1 54 | } 55 | 56 | // Get indicates an expected call of Get 57 | func (mr *MockAuthorizationStoreMockRecorder) Get(code interface{}) *gomock.Call { 58 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockAuthorizationStore)(nil).Get), code) 59 | } 60 | 61 | // Delete mocks base method 62 | func (m *MockAuthorizationStore) Delete(code string) error { 63 | ret := m.ctrl.Call(m, "Delete", code) 64 | ret0, _ := ret[0].(error) 65 | return ret0 66 | } 67 | 68 | // Delete indicates an expected call of Delete 69 | func (mr *MockAuthorizationStoreMockRecorder) Delete(code interface{}) *gomock.Call { 70 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAuthorizationStore)(nil).Delete), code) 71 | } 72 | -------------------------------------------------------------------------------- /models/authorization_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/alicebob/miniredis" 8 | "github.com/go-redis/redis" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestAuthorizationStore(t *testing.T) { 14 | minir, err := miniredis.Run() 15 | require.NoError(t, err) 16 | 17 | r := redis.NewClient(&redis.Options{Addr: minir.Addr()}) 18 | store := NewAuthorizationStore("meow", r) 19 | auth := &Authorization{ 20 | Code: "c049a780-8e07-44aa-9b2c-7bdae5af728d", 21 | ClientID: "f41d0dea-7a3b-4ca6-8b16-2eefe07b28f0", 22 | Scope: "myscope", 23 | RedirectURI: "https://google.com/", 24 | State: "weh", 25 | AuthorizationUserData: AuthorizationUserData{ 26 | UserID: 12345, 27 | }, 28 | } 29 | 30 | require.Len(t, r.Keys("*").Val(), 0) 31 | 32 | t.Run("Not Found", func(t *testing.T) { 33 | _, err := store.Get(auth.Code) 34 | assert.EqualError(t, err, "invalid authorization code") 35 | assert.True(t, IsNotFound(err)) 36 | }) 37 | 38 | t.Run("Set", func(t *testing.T) { 39 | t.Run("NoTTL", func(t *testing.T) { 40 | assert.EqualError(t, store.Set(auth, 0), "a TTL is required, but not given") 41 | }) 42 | 43 | assert.NoError(t, store.Set(auth, 15*time.Minute)) 44 | assert.Equal(t, r.Keys("*").Val(), []string{"meow:oauth2:authorizations:" + auth.Code}) 45 | }) 46 | 47 | t.Run("Get", func(t *testing.T) { 48 | auth2, err := store.Get(auth.Code) 49 | require.NoError(t, err) 50 | assert.Equal(t, auth, auth2) 51 | }) 52 | 53 | t.Run("Delete", func(t *testing.T) { 54 | assert.NoError(t, store.Delete(auth.Code)) 55 | require.Len(t, r.Keys("*").Val(), 0) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /models/client.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "strconv" 7 | 8 | "github.com/bwmarrin/snowflake" 9 | "github.com/jinzhu/gorm" 10 | 11 | "github.com/meowpub/meow/lib" 12 | ) 13 | 14 | //go:generate mockgen -package=models -source=client.go -destination=client.mock.go 15 | 16 | // Client is an OAuth2 client application. 17 | // Implements the osin.Client interface for the oauth package. 18 | type Client struct { 19 | ID snowflake.ID `json:"id"` 20 | RedirectURI string `json:"redirect_uri"` 21 | Secret string `json:"-"` 22 | ClientUserData 23 | } 24 | 25 | // ClientUserData contains additional fields for a client to be included as UserData. 26 | type ClientUserData struct { 27 | Name string `json:"name"` 28 | Description string `json:"description"` 29 | OwnerID *snowflake.ID `json:"owner_id"` 30 | } 31 | 32 | func NewClient(ud ClientUserData, redirectURI string) (*Client, error) { 33 | // Generate a snowflake. 34 | id := lib.GenSnowflake() 35 | 36 | // Generate a Secret. 37 | secretData := make([]byte, 64) 38 | if _, err := rand.Read(secretData); err != nil { 39 | return nil, err 40 | } 41 | secret := base64.RawURLEncoding.EncodeToString(secretData) 42 | 43 | return &Client{ 44 | ID: id, 45 | RedirectURI: redirectURI, 46 | Secret: secret, 47 | ClientUserData: ud, 48 | }, nil 49 | } 50 | 51 | func (c Client) GetId() string { return strconv.FormatInt(int64(c.ID), 10) } 52 | func (c Client) GetSecret() string { return c.Secret } 53 | func (c Client) GetRedirectUri() string { return c.RedirectURI } 54 | func (c Client) GetUserData() interface{} { return c.ClientUserData } 55 | 56 | // ClientStore abstracts data access to Clients. 57 | type ClientStore interface { 58 | // Create creates a new client. 59 | Create(cl *Client) error 60 | 61 | // Get returns a client by ID, or an error if it doesn't exist. 62 | Get(id snowflake.ID) (*Client, error) 63 | } 64 | 65 | type clientStore struct { 66 | DB *gorm.DB 67 | } 68 | 69 | // NewClientStore returns a ClientStore that accesses the given DB. 70 | func NewClientStore(db *gorm.DB) ClientStore { 71 | return &clientStore{db} 72 | } 73 | 74 | func (s clientStore) Create(cl *Client) error { 75 | return s.DB.Create(cl).Error 76 | } 77 | 78 | func (s clientStore) Get(id snowflake.ID) (*Client, error) { 79 | var cl Client 80 | return &cl, s.DB.First(&cl, Client{ID: id}).Error 81 | } 82 | -------------------------------------------------------------------------------- /models/client.mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: client.go 3 | 4 | // Package models is a generated GoMock package. 5 | package models 6 | 7 | import ( 8 | snowflake "github.com/bwmarrin/snowflake" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockClientStore is a mock of ClientStore interface 14 | type MockClientStore struct { 15 | ctrl *gomock.Controller 16 | recorder *MockClientStoreMockRecorder 17 | } 18 | 19 | // MockClientStoreMockRecorder is the mock recorder for MockClientStore 20 | type MockClientStoreMockRecorder struct { 21 | mock *MockClientStore 22 | } 23 | 24 | // NewMockClientStore creates a new mock instance 25 | func NewMockClientStore(ctrl *gomock.Controller) *MockClientStore { 26 | mock := &MockClientStore{ctrl: ctrl} 27 | mock.recorder = &MockClientStoreMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockClientStore) EXPECT() *MockClientStoreMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // Create mocks base method 37 | func (m *MockClientStore) Create(cl *Client) error { 38 | ret := m.ctrl.Call(m, "Create", cl) 39 | ret0, _ := ret[0].(error) 40 | return ret0 41 | } 42 | 43 | // Create indicates an expected call of Create 44 | func (mr *MockClientStoreMockRecorder) Create(cl interface{}) *gomock.Call { 45 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockClientStore)(nil).Create), cl) 46 | } 47 | 48 | // Get mocks base method 49 | func (m *MockClientStore) Get(id snowflake.ID) (*Client, error) { 50 | ret := m.ctrl.Call(m, "Get", id) 51 | ret0, _ := ret[0].(*Client) 52 | ret1, _ := ret[1].(error) 53 | return ret0, ret1 54 | } 55 | 56 | // Get indicates an expected call of Get 57 | func (mr *MockClientStoreMockRecorder) Get(id interface{}) *gomock.Call { 58 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClientStore)(nil).Get), id) 59 | } 60 | -------------------------------------------------------------------------------- /models/client_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestClientAccessors(t *testing.T) { 13 | cl, err := NewClient(ClientUserData{ 14 | Name: "test client", 15 | Description: "lorem ipsum dolor sit amet", 16 | }, "https://google.com/") 17 | require.NoError(t, err) 18 | 19 | t.Run("GetId", func(t *testing.T) { 20 | idStr := cl.GetId() 21 | assert.Equal(t, fmt.Sprintf("%d", cl.ID), idStr) 22 | id, err := strconv.ParseInt(idStr, 10, 64) 23 | assert.NoError(t, err) 24 | assert.Equal(t, int64(cl.ID), id) 25 | }) 26 | 27 | t.Run("GetSecret", func(t *testing.T) { 28 | assert.Equal(t, cl.Secret, cl.GetSecret()) 29 | }) 30 | 31 | t.Run("GetRedirectUri", func(t *testing.T) { 32 | assert.Equal(t, cl.RedirectURI, cl.GetRedirectUri()) 33 | }) 34 | 35 | t.Run("GetUserData", func(t *testing.T) { 36 | assert.Equal(t, cl.ClientUserData, cl.GetUserData()) 37 | }) 38 | } 39 | 40 | func TestClientStore(t *testing.T) { 41 | estore := NewEntityStore(TestDB) 42 | ustore := NewUserStore(TestDB) 43 | 44 | profile, err := NewEntity("client", []byte(`{"@id": "https://example.com/@client-dev"}`)) 45 | require.NoError(t, err) 46 | require.NoError(t, estore.Save(profile)) 47 | 48 | user, err := NewUser(profile.ID, "client-dev@example.com", "password") 49 | require.NoError(t, err) 50 | require.NoError(t, ustore.Save(user)) 51 | 52 | store := NewClientStore(TestDB) 53 | cl, err := NewClient(ClientUserData{ 54 | Name: "test client", 55 | Description: "lorem ipsum dolor sit amet", 56 | OwnerID: &user.ID, 57 | }, "https://google.com/") 58 | 59 | t.Run("NotFound", func(t *testing.T) { 60 | _, err := store.Get(cl.ID) 61 | require.EqualError(t, err, "record not found") 62 | }) 63 | 64 | t.Run("Create", func(t *testing.T) { 65 | require.NoError(t, err) 66 | require.NoError(t, store.Create(cl)) 67 | }) 68 | 69 | t.Run("Get", func(t *testing.T) { 70 | cl2, err := store.Get(cl.ID) 71 | require.NoError(t, err) 72 | assert.Equal(t, cl, cl2) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /models/context.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type ctxKey string 8 | 9 | const ctxKeyStores ctxKey = "stores" 10 | 11 | // GetStores returns the Stores object associated with the context. 12 | func GetStores(ctx context.Context) Stores { 13 | s, _ := ctx.Value(ctxKeyStores).(Stores) 14 | return s 15 | } 16 | 17 | // WithStores associates a Stores object with a context. 18 | func WithStores(ctx context.Context, stores Stores) context.Context { 19 | return context.WithValue(ctx, ctxKeyStores, stores) 20 | } 21 | -------------------------------------------------------------------------------- /models/entity.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/bwmarrin/snowflake" 7 | "github.com/jinzhu/gorm" 8 | 9 | "github.com/meowpub/meow/ld" 10 | "github.com/meowpub/meow/lib" 11 | ) 12 | 13 | //go:generate mockgen -package=models -source=entity.go -destination=entity.mock.go 14 | 15 | var entityOnConflict = genOnConflict(Entity{}, "(data->>'@id')", "id") 16 | 17 | // Kind of an Entity. 18 | type EntityKind string 19 | 20 | const ( 21 | ObjectEntity = "object" // Default Kind of an Entity. 22 | ) 23 | 24 | // An Entity represents a raw, database-form Entity. 25 | // This is a lower-level API and you probably actually want the higher-level entities package. 26 | type Entity struct { 27 | // Snowflake ID, separate from the public one, used for foreign key consistency. 28 | ID snowflake.ID `json:"_id"` 29 | 30 | // Raw JSON data. You probably want to use Obj instead. 31 | Data JSONB `json:"_data"` 32 | 33 | // "Kind" of an entity, normally ObjectEntity. 34 | // Kinds determine what special server side behavior applies. 35 | Kind EntityKind `json:"_kind"` 36 | 37 | // Concrete Object representation of this Entity. 38 | // Modifications to this Object will be written back (and synched to Data) by EntityStore.Save(). 39 | Obj *ld.Object `json:"_obj" gorm:"-"` 40 | } 41 | 42 | func NewEntity(kind EntityKind, data []byte) (*Entity, error) { 43 | e := &Entity{ID: lib.GenSnowflake(), Data: data, Kind: kind} 44 | return e, e.SyncDataToObject() 45 | } 46 | 47 | func NewEntityFrom(kind EntityKind, e ld.Entity) *Entity { 48 | return &Entity{ID: lib.GenSnowflake(), Obj: e.Obj(), Kind: kind} 49 | } 50 | 51 | // Overwrites Object with Data. Called automatically by EntityStore.Get*() and NewEntity(). 52 | func (e *Entity) SyncDataToObject() error { 53 | if len(e.Data) > 0 { 54 | obj, err := ld.ParseObject(e.Data) 55 | if err != nil { 56 | return err 57 | } 58 | e.Obj = obj 59 | } else { 60 | e.Obj = nil 61 | } 62 | return nil 63 | } 64 | 65 | // Overwrites Data with Object. Called automatically by EntityStore.Save(). 66 | func (e *Entity) SyncObjectToData() error { 67 | if e.Obj != nil { 68 | data, err := json.Marshal(e.Obj) 69 | if err != nil { 70 | return err 71 | } 72 | e.Data = data 73 | } 74 | return nil 75 | } 76 | 77 | // EntityStore stores Entities in their raw database form. 78 | type EntityStore interface { 79 | // GetBySnowflake returns an Entity by its snowflake, eg. "353894652568535040". 80 | GetBySnowflake(id snowflake.ID) (*Entity, error) 81 | 82 | // GetByID returns an Entity by its ID, eg. "https://example.com/@johnsmith. 83 | GetByID(id string) (*Entity, error) 84 | 85 | // Save stores an Entity using an upsert. Updates Data if the object has been modified. 86 | Save(e *Entity) error 87 | } 88 | 89 | type entityStore struct { 90 | DB *gorm.DB 91 | } 92 | 93 | func NewEntityStore(db *gorm.DB) EntityStore { 94 | return &entityStore{db} 95 | } 96 | 97 | func (s entityStore) GetBySnowflake(id snowflake.ID) (*Entity, error) { 98 | var e Entity 99 | if err := s.DB.First(&e, Entity{ID: id}).Error; err != nil { 100 | return &e, err 101 | } 102 | return &e, e.SyncDataToObject() 103 | } 104 | 105 | func (s entityStore) GetByID(id string) (*Entity, error) { 106 | var e Entity 107 | if err := s.DB.First(&e, `data->>'@id' = ?`, id).Error; err != nil { 108 | return &e, err 109 | } 110 | return &e, e.SyncDataToObject() 111 | } 112 | 113 | func (s entityStore) Save(e *Entity) error { 114 | if err := e.SyncObjectToData(); err != nil { 115 | return err 116 | } 117 | return s.DB.Set(gormInsertOption, entityOnConflict).Create(e).Error 118 | } 119 | -------------------------------------------------------------------------------- /models/entity.mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: entity.go 3 | 4 | // Package models is a generated GoMock package. 5 | package models 6 | 7 | import ( 8 | snowflake "github.com/bwmarrin/snowflake" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockEntityStore is a mock of EntityStore interface 14 | type MockEntityStore struct { 15 | ctrl *gomock.Controller 16 | recorder *MockEntityStoreMockRecorder 17 | } 18 | 19 | // MockEntityStoreMockRecorder is the mock recorder for MockEntityStore 20 | type MockEntityStoreMockRecorder struct { 21 | mock *MockEntityStore 22 | } 23 | 24 | // NewMockEntityStore creates a new mock instance 25 | func NewMockEntityStore(ctrl *gomock.Controller) *MockEntityStore { 26 | mock := &MockEntityStore{ctrl: ctrl} 27 | mock.recorder = &MockEntityStoreMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockEntityStore) EXPECT() *MockEntityStoreMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // GetBySnowflake mocks base method 37 | func (m *MockEntityStore) GetBySnowflake(id snowflake.ID) (*Entity, error) { 38 | ret := m.ctrl.Call(m, "GetBySnowflake", id) 39 | ret0, _ := ret[0].(*Entity) 40 | ret1, _ := ret[1].(error) 41 | return ret0, ret1 42 | } 43 | 44 | // GetBySnowflake indicates an expected call of GetBySnowflake 45 | func (mr *MockEntityStoreMockRecorder) GetBySnowflake(id interface{}) *gomock.Call { 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBySnowflake", reflect.TypeOf((*MockEntityStore)(nil).GetBySnowflake), id) 47 | } 48 | 49 | // GetByID mocks base method 50 | func (m *MockEntityStore) GetByID(id string) (*Entity, error) { 51 | ret := m.ctrl.Call(m, "GetByID", id) 52 | ret0, _ := ret[0].(*Entity) 53 | ret1, _ := ret[1].(error) 54 | return ret0, ret1 55 | } 56 | 57 | // GetByID indicates an expected call of GetByID 58 | func (mr *MockEntityStoreMockRecorder) GetByID(id interface{}) *gomock.Call { 59 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByID", reflect.TypeOf((*MockEntityStore)(nil).GetByID), id) 60 | } 61 | 62 | // Save mocks base method 63 | func (m *MockEntityStore) Save(e *Entity) error { 64 | ret := m.ctrl.Call(m, "Save", e) 65 | ret0, _ := ret[0].(error) 66 | return ret0 67 | } 68 | 69 | // Save indicates an expected call of Save 70 | func (mr *MockEntityStoreMockRecorder) Save(e interface{}) *gomock.Call { 71 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockEntityStore)(nil).Save), e) 72 | } 73 | -------------------------------------------------------------------------------- /models/entity_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/meowpub/meow/lib" 11 | ) 12 | 13 | func TestEntityConflictClause(t *testing.T) { 14 | assert.Equal(t, `ON CONFLICT ((data->>'@id')) DO UPDATE SET data=EXCLUDED.data, kind=EXCLUDED.kind`, entityOnConflict) 15 | } 16 | 17 | func TestEntityStore(t *testing.T) { 18 | tx := TestDB.Begin() 19 | defer tx.Rollback() 20 | 21 | store := NewEntityStore(TestDB) 22 | 23 | t.Run("Not Found", func(t *testing.T) { 24 | t.Run("Snowflake", func(t *testing.T) { 25 | _, err := store.GetBySnowflake(12345) 26 | require.EqualError(t, err, "record not found") 27 | assert.True(t, IsNotFound(err)) 28 | }) 29 | t.Run("ID", func(t *testing.T) { 30 | _, err := store.GetByID("https://example.com/@jsmith") 31 | require.EqualError(t, err, "record not found") 32 | assert.True(t, IsNotFound(err)) 33 | }) 34 | }) 35 | 36 | t.Run("Create", func(t *testing.T) { 37 | t.Run("No Snowflake", func(t *testing.T) { 38 | require.EqualError(t, store.Save(&Entity{Kind: ObjectEntity, Data: JSONB(`{ 39 | "@id": "https://example.com/@jsmith", 40 | "@type": ["http://schema.org/Person"], 41 | "http://schema.org/name": [{"@value": "John Smith"}] 42 | }`)}), "pq: null value in column \"id\" violates not-null constraint") 43 | }) 44 | 45 | id := lib.GenSnowflake() 46 | 47 | t.Run("Empty", func(t *testing.T) { 48 | require.EqualError(t, store.Save(&Entity{ID: id, Kind: ObjectEntity, Data: JSONB(`{}`)}), "pq: new row for relation \"entities\" violates check constraint \"entities_data_id_not_null_check\"") 49 | }) 50 | 51 | t.Run("Empty ID", func(t *testing.T) { 52 | require.EqualError(t, store.Save(&Entity{ID: id, Kind: ObjectEntity, Data: JSONB(`{"@id":""}`)}), "pq: new row for relation \"entities\" violates check constraint \"entities_data_id_not_empty_check\"") 53 | }) 54 | 55 | t.Run("Invalid ID", func(t *testing.T) { 56 | require.EqualError(t, store.Save(&Entity{ID: id, Kind: ObjectEntity, Data: JSONB(`{"@id":"test"}`)}), "pq: new row for relation \"entities\" violates check constraint \"entities_data_id_protocol_check\"") 57 | }) 58 | 59 | t.Run("Invalid ID Protocol", func(t *testing.T) { 60 | require.EqualError(t, store.Save(&Entity{ID: id, Kind: ObjectEntity, Data: JSONB(`{"@id":"ftp://example.com/~jsmith"}`)}), "pq: new row for relation \"entities\" violates check constraint \"entities_data_id_protocol_check\"") 61 | }) 62 | 63 | require.NoError(t, store.Save(&Entity{ID: id, Data: JSONB(`{ 64 | "@id": "https://example.com/@jsmith", 65 | "@type": ["http://schema.org/Person"], 66 | "http://schema.org/name": [{"@value": "John Smith"}] 67 | }`), Kind: "weird"})) 68 | 69 | t.Run("Get", func(t *testing.T) { 70 | se, err := store.GetBySnowflake(id) 71 | require.NoError(t, err) 72 | ie, err := store.GetByID("https://example.com/@jsmith") 73 | require.NoError(t, err) 74 | 75 | assert.Equal(t, ie, se) 76 | assert.Equal(t, id, se.ID) 77 | 78 | var data map[string]interface{} 79 | require.NoError(t, json.Unmarshal(se.Data, &data)) 80 | assert.Equal(t, map[string]interface{}{ 81 | "@id": "https://example.com/@jsmith", 82 | "@type": []interface{}{"http://schema.org/Person"}, 83 | "http://schema.org/name": []interface{}{ 84 | map[string]interface{}{"@value": "John Smith"}, 85 | }, 86 | }, data) 87 | 88 | t.Run("Object", func(t *testing.T) { 89 | assert.Equal(t, "https://example.com/@jsmith", se.Obj.ID()) 90 | assert.Equal(t, se.Obj.ID(), ie.Obj.ID()) 91 | 92 | assert.Equal(t, []string{"http://schema.org/Person"}, se.Obj.Type()) 93 | assert.Equal(t, se.Obj.Type(), ie.Obj.Type()) 94 | 95 | assert.Equal(t, map[string]interface{}{ 96 | "@id": "https://example.com/@jsmith", 97 | "@type": []interface{}{"http://schema.org/Person"}, 98 | "http://schema.org/name": []interface{}{ 99 | map[string]interface{}{"@value": "John Smith"}, 100 | }, 101 | }, se.Obj.V) 102 | assert.Equal(t, se.Obj.V, ie.Obj.V) 103 | 104 | t.Run("Update", func(t *testing.T) { 105 | se.Obj.V["http://schema.org/name"] = []interface{}{ 106 | map[string]interface{}{"@value": "Jane Smith"}, 107 | } 108 | require.NoError(t, store.Save(se)) 109 | 110 | t.Run("Get", func(t *testing.T) { 111 | se2, err := store.GetBySnowflake(id) 112 | require.NoError(t, err) 113 | ie2, err := store.GetByID("https://example.com/@jsmith") 114 | require.NoError(t, err) 115 | 116 | assert.Equal(t, []interface{}{ 117 | map[string]interface{}{"@value": "Jane Smith"}, 118 | }, se2.Obj.Get("http://schema.org/name")) 119 | assert.Equal(t, se2.Obj.V, ie2.Obj.V) 120 | }) 121 | }) 122 | }) 123 | }) 124 | 125 | t.Run("Update", func(t *testing.T) { 126 | newID := lib.GenSnowflake() 127 | 128 | require.NoError(t, store.Save(&Entity{ID: newID, Data: JSONB(`{ 129 | "@id": "https://example.com/@jsmith", 130 | "@type": ["http://schema.org/Person"], 131 | "http://schema.org/name": [{"@value": "Jane Smith"}] 132 | }`), Kind: "Success!"})) 133 | 134 | t.Run("Get", func(t *testing.T) { 135 | se, err := store.GetBySnowflake(id) 136 | require.NoError(t, err) 137 | ie, err := store.GetByID("https://example.com/@jsmith") 138 | require.NoError(t, err) 139 | 140 | assert.Equal(t, ie, se) 141 | assert.Equal(t, id, se.ID) 142 | 143 | var data map[string]interface{} 144 | require.NoError(t, json.Unmarshal(se.Data, &data)) 145 | assert.Equal(t, map[string]interface{}{ 146 | "@id": "https://example.com/@jsmith", 147 | "@type": []interface{}{"http://schema.org/Person"}, 148 | "http://schema.org/name": []interface{}{ 149 | map[string]interface{}{"@value": "Jane Smith"}, 150 | }, 151 | }, data) 152 | }) 153 | }) 154 | }) 155 | } 156 | -------------------------------------------------------------------------------- /models/errors.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | var ErrNoTTL = errors.New("a TTL is required, but not given") 9 | 10 | type NotFoundError string 11 | 12 | func (err NotFoundError) Error() string { 13 | return string(err) 14 | } 15 | 16 | func IsNotFound(err error) bool { 17 | err = errors.Cause(err) 18 | 19 | if _, ok := err.(NotFoundError); ok { 20 | return true 21 | } else if gorm.IsRecordNotFoundError(err) { 22 | return true 23 | } else { 24 | return false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /models/errors_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestIsNotFound(t *testing.T) { 11 | nf := NotFoundError("Not found!") 12 | 13 | require.True(t, IsNotFound(nf), "NotFound error should be IsNotFound") 14 | 15 | wrapped := errors.Wrap(nf, "Foo") 16 | require.True(t, IsNotFound(wrapped), "Wrapped NotFound error should be IsNotFound") 17 | } 18 | -------------------------------------------------------------------------------- /models/refreshtoken.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/go-redis/redis" 7 | ) 8 | 9 | //go:generate mockgen -package=models -source=refreshtoken.go -destination=refreshtoken.mock.go 10 | 11 | // RefreshToken stores a persistent refresh token in Redis. See also: AccessToken. 12 | type RefreshToken struct { 13 | Token string `json:"token"` 14 | ClientID string `json:"client_id"` 15 | Scope string `json:"scope"` 16 | AuthorizationUserData 17 | } 18 | 19 | type RefreshTokenStore interface { 20 | Set(token *RefreshToken) error 21 | Get(token string) (*RefreshToken, error) 22 | Delete(token string) error 23 | } 24 | 25 | type refreshTokenStore struct { 26 | K string 27 | R *redis.Client 28 | } 29 | 30 | func NewRefreshTokenStore(keyspace string, r *redis.Client) RefreshTokenStore { 31 | return &refreshTokenStore{keyspace, r} 32 | } 33 | 34 | func (s *refreshTokenStore) key(code string) string { 35 | return s.K + ":oauth2:refresh_tokens:" + code 36 | } 37 | 38 | func (s *refreshTokenStore) Set(token *RefreshToken) error { 39 | data, err := json.Marshal(token) 40 | if err != nil { 41 | return err 42 | } 43 | return s.R.Set(s.key(token.Token), data, 0).Err() 44 | } 45 | 46 | func (s *refreshTokenStore) Get(token string) (*RefreshToken, error) { 47 | cmd := s.R.Get(s.key(token)) 48 | if err := cmd.Err(); err != nil { 49 | if err == redis.Nil { 50 | return nil, NotFoundError("token is invalid or expired") 51 | } 52 | return nil, err 53 | } 54 | data, err := cmd.Bytes() 55 | if err != nil { 56 | return nil, err 57 | } 58 | var tok RefreshToken 59 | return &tok, json.Unmarshal(data, &tok) 60 | } 61 | 62 | func (s *refreshTokenStore) Delete(token string) error { 63 | return s.R.Del(s.key(token)).Err() 64 | } 65 | -------------------------------------------------------------------------------- /models/refreshtoken.mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: refreshtoken.go 3 | 4 | // Package models is a generated GoMock package. 5 | package models 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | reflect "reflect" 10 | ) 11 | 12 | // MockRefreshTokenStore is a mock of RefreshTokenStore interface 13 | type MockRefreshTokenStore struct { 14 | ctrl *gomock.Controller 15 | recorder *MockRefreshTokenStoreMockRecorder 16 | } 17 | 18 | // MockRefreshTokenStoreMockRecorder is the mock recorder for MockRefreshTokenStore 19 | type MockRefreshTokenStoreMockRecorder struct { 20 | mock *MockRefreshTokenStore 21 | } 22 | 23 | // NewMockRefreshTokenStore creates a new mock instance 24 | func NewMockRefreshTokenStore(ctrl *gomock.Controller) *MockRefreshTokenStore { 25 | mock := &MockRefreshTokenStore{ctrl: ctrl} 26 | mock.recorder = &MockRefreshTokenStoreMockRecorder{mock} 27 | return mock 28 | } 29 | 30 | // EXPECT returns an object that allows the caller to indicate expected use 31 | func (m *MockRefreshTokenStore) EXPECT() *MockRefreshTokenStoreMockRecorder { 32 | return m.recorder 33 | } 34 | 35 | // Set mocks base method 36 | func (m *MockRefreshTokenStore) Set(token *RefreshToken) error { 37 | ret := m.ctrl.Call(m, "Set", token) 38 | ret0, _ := ret[0].(error) 39 | return ret0 40 | } 41 | 42 | // Set indicates an expected call of Set 43 | func (mr *MockRefreshTokenStoreMockRecorder) Set(token interface{}) *gomock.Call { 44 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockRefreshTokenStore)(nil).Set), token) 45 | } 46 | 47 | // Get mocks base method 48 | func (m *MockRefreshTokenStore) Get(token string) (*RefreshToken, error) { 49 | ret := m.ctrl.Call(m, "Get", token) 50 | ret0, _ := ret[0].(*RefreshToken) 51 | ret1, _ := ret[1].(error) 52 | return ret0, ret1 53 | } 54 | 55 | // Get indicates an expected call of Get 56 | func (mr *MockRefreshTokenStoreMockRecorder) Get(token interface{}) *gomock.Call { 57 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRefreshTokenStore)(nil).Get), token) 58 | } 59 | 60 | // Delete mocks base method 61 | func (m *MockRefreshTokenStore) Delete(token string) error { 62 | ret := m.ctrl.Call(m, "Delete", token) 63 | ret0, _ := ret[0].(error) 64 | return ret0 65 | } 66 | 67 | // Delete indicates an expected call of Delete 68 | func (mr *MockRefreshTokenStoreMockRecorder) Delete(token interface{}) *gomock.Call { 69 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRefreshTokenStore)(nil).Delete), token) 70 | } 71 | -------------------------------------------------------------------------------- /models/refreshtoken_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alicebob/miniredis" 7 | "github.com/go-redis/redis" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestRefreshTokenStore(t *testing.T) { 13 | minir, err := miniredis.Run() 14 | require.NoError(t, err) 15 | 16 | r := redis.NewClient(&redis.Options{Addr: minir.Addr()}) 17 | store := NewRefreshTokenStore("meow", r) 18 | token := &RefreshToken{ 19 | Token: "8f9f21e6-d7d9-44f4-a018-295916935375", 20 | ClientID: "f41d0dea-7a3b-4ca6-8b16-2eefe07b28f0", 21 | Scope: "myscope", 22 | AuthorizationUserData: AuthorizationUserData{ 23 | UserID: 12345, 24 | }, 25 | } 26 | 27 | require.Len(t, r.Keys("*").Val(), 0) 28 | 29 | t.Run("Not Found", func(t *testing.T) { 30 | _, err := store.Get(token.Token) 31 | assert.EqualError(t, err, "token is invalid or expired") 32 | assert.True(t, IsNotFound(err)) 33 | }) 34 | 35 | t.Run("Set", func(t *testing.T) { 36 | assert.NoError(t, store.Set(token)) 37 | assert.Equal(t, r.Keys("*").Val(), []string{"meow:oauth2:refresh_tokens:" + token.Token}) 38 | }) 39 | 40 | t.Run("Get", func(t *testing.T) { 41 | token2, err := store.Get(token.Token) 42 | require.NoError(t, err) 43 | assert.Equal(t, token, token2) 44 | }) 45 | 46 | t.Run("Delete", func(t *testing.T) { 47 | assert.NoError(t, store.Delete(token.Token)) 48 | require.Len(t, r.Keys("*").Val(), 0) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /models/setup_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "os" 5 | "os/user" 6 | "path" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/jinzhu/gorm" 11 | "github.com/joho/godotenv" 12 | "github.com/mattes/migrate" 13 | 14 | _ "github.com/jinzhu/gorm/dialects/postgres" 15 | _ "github.com/mattes/migrate/database/postgres" 16 | _ "github.com/mattes/migrate/source/file" 17 | ) 18 | 19 | var ( 20 | // TestDB is the database used for testing. 21 | TestDB *gorm.DB 22 | 23 | GOPATH string // Defaults to ~/go if not set 24 | MeowPath string // Path to $GOPATH/src/github.com/meowpub/meow 25 | ) 26 | 27 | func init() { 28 | GOPATH := os.Getenv("GOPATH") 29 | if GOPATH == "" { 30 | u, err := user.Current() 31 | if err != nil { 32 | panic(err) 33 | } 34 | GOPATH = filepath.Join(u.HomeDir, "go") 35 | } 36 | MeowPath = filepath.Join(GOPATH, "src", "github.com", "meowpub", "meow") 37 | } 38 | 39 | func must(err error) { 40 | if err != nil { 41 | panic(err) 42 | } 43 | } 44 | 45 | func TestMain(m *testing.M) { 46 | if err := godotenv.Load(filepath.Join(MeowPath, ".env")); err != nil { 47 | if !os.IsNotExist(err) { 48 | panic(err) 49 | } 50 | } 51 | 52 | // Override the test DB URI with TEST_DB_URI! 53 | // -- DO NOT USE A PRODUCTION DATABASE; IT WILL BE WIPED -- 54 | uri := os.Getenv("MEOW_TEST_DB") 55 | if uri == "" { 56 | uri = "postgres:///meow_test?sslmode=disable" 57 | } 58 | 59 | // Connect to the database! 60 | db, err := gorm.Open("postgres", uri) 61 | must(err) 62 | TestDB = db.LogMode(true).Debug() 63 | 64 | // Nuke the entire default schema. 65 | must(db.Exec(`DROP SCHEMA IF EXISTS public CASCADE`).Error) 66 | must(db.Exec(`CREATE SCHEMA public`).Error) 67 | 68 | // Read migrations... 69 | wd, err := os.Getwd() 70 | must(err) 71 | migr, err := migrate.New("file://"+path.Join(wd, "..", "migrations"), uri) 72 | must(err) 73 | 74 | // Migrate up, down, then back up to verify that both ways are working. 75 | must(migr.Up()) 76 | must(migr.Down()) 77 | must(migr.Up()) 78 | 79 | // Finish! 80 | srcerr, dberr := migr.Close() 81 | must(srcerr) 82 | must(dberr) 83 | 84 | // Now we can run some tests. 85 | os.Exit(m.Run()) 86 | } 87 | -------------------------------------------------------------------------------- /models/stores.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/go-redis/redis" 5 | "github.com/jinzhu/gorm" 6 | ) 7 | 8 | // Stores is an interface that returns other Stores. There are too many of them at this point to 9 | // keep them all around individually. I wasn't allowed to name this the StoreStore :( 10 | type Stores interface { 11 | Entities() EntityStore 12 | Users() UserStore 13 | Clients() ClientStore 14 | Authorizations() AuthorizationStore 15 | AccessTokens() AccessTokenStore 16 | RefreshTokens() RefreshTokenStore 17 | StreamItems() StreamItemStore 18 | } 19 | 20 | type stores struct { 21 | DB *gorm.DB 22 | R *redis.Client 23 | K string 24 | 25 | entityStore EntityStore 26 | userStore UserStore 27 | clientStore ClientStore 28 | authorizationStore AuthorizationStore 29 | accessTokenStore AccessTokenStore 30 | refreshTokenStore RefreshTokenStore 31 | streamItemStore StreamItemStore 32 | } 33 | 34 | func NewStores(db *gorm.DB, r *redis.Client, keyspace string) Stores { 35 | return &stores{ 36 | DB: db, 37 | R: r, 38 | K: keyspace, 39 | } 40 | } 41 | 42 | func (s *stores) Entities() EntityStore { 43 | if s.entityStore == nil { 44 | s.entityStore = NewEntityStore(s.DB) 45 | } 46 | return s.entityStore 47 | } 48 | 49 | func (s *stores) Users() UserStore { 50 | if s.userStore == nil { 51 | s.userStore = NewUserStore(s.DB) 52 | } 53 | return s.userStore 54 | } 55 | 56 | func (s *stores) Clients() ClientStore { 57 | if s.clientStore == nil { 58 | s.clientStore = NewClientStore(s.DB) 59 | } 60 | return s.clientStore 61 | } 62 | 63 | func (s *stores) Authorizations() AuthorizationStore { 64 | if s.authorizationStore == nil { 65 | s.authorizationStore = NewAuthorizationStore(s.K, s.R) 66 | } 67 | return s.authorizationStore 68 | } 69 | 70 | func (s *stores) AccessTokens() AccessTokenStore { 71 | if s.accessTokenStore == nil { 72 | s.accessTokenStore = NewAccessTokenStore(s.K, s.R) 73 | } 74 | return s.accessTokenStore 75 | } 76 | 77 | func (s *stores) RefreshTokens() RefreshTokenStore { 78 | if s.refreshTokenStore == nil { 79 | s.refreshTokenStore = NewRefreshTokenStore(s.K, s.R) 80 | } 81 | return s.refreshTokenStore 82 | } 83 | 84 | func (s *stores) StreamItems() StreamItemStore { 85 | if s.streamItemStore == nil { 86 | s.streamItemStore = NewStreamItemStore(s.DB) 87 | } 88 | return s.streamItemStore 89 | } 90 | -------------------------------------------------------------------------------- /models/stores_mock.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/golang/mock/gomock" 5 | ) 6 | 7 | // Unlike the other stores, we're not autogenerating the mock for Stores, because it's more useful 8 | // to have a Store that creates Mock<*>Store instances that we can run tests on than to have to go 9 | // mockStore.EXPECT().Clients().Returns(myExistingMockClientStore) etc. before every real EXPECT(). 10 | // That would also break in obnoxious ways if tested functions change the way they reuse stores, 11 | // which should not be part of the API contract in the slightest. 12 | type MockStores struct { 13 | ctrl *gomock.Controller 14 | 15 | EntityStore *MockEntityStore 16 | UserStore *MockUserStore 17 | ClientStore *MockClientStore 18 | AuthorizationStore *MockAuthorizationStore 19 | AccessTokenStore *MockAccessTokenStore 20 | RefreshTokenStore *MockRefreshTokenStore 21 | StreamItemStore *MockStreamItemStore 22 | } 23 | 24 | func NewMockStores(ctrl *gomock.Controller) *MockStores { 25 | return &MockStores{ctrl: ctrl} 26 | } 27 | 28 | func (s *MockStores) Entities() EntityStore { 29 | if s.EntityStore == nil { 30 | s.EntityStore = NewMockEntityStore(s.ctrl) 31 | } 32 | return s.EntityStore 33 | } 34 | 35 | func (s *MockStores) Users() UserStore { 36 | if s.UserStore == nil { 37 | s.UserStore = NewMockUserStore(s.ctrl) 38 | } 39 | return s.UserStore 40 | } 41 | 42 | func (s *MockStores) Clients() ClientStore { 43 | if s.ClientStore == nil { 44 | s.ClientStore = NewMockClientStore(s.ctrl) 45 | } 46 | return s.ClientStore 47 | } 48 | 49 | func (s *MockStores) Authorizations() AuthorizationStore { 50 | if s.AuthorizationStore == nil { 51 | s.AuthorizationStore = NewMockAuthorizationStore(s.ctrl) 52 | } 53 | return s.AuthorizationStore 54 | } 55 | 56 | func (s *MockStores) AccessTokens() AccessTokenStore { 57 | if s.AccessTokenStore == nil { 58 | s.AccessTokenStore = NewMockAccessTokenStore(s.ctrl) 59 | } 60 | return s.AccessTokenStore 61 | } 62 | 63 | func (s *MockStores) RefreshTokens() RefreshTokenStore { 64 | if s.RefreshTokenStore == nil { 65 | s.RefreshTokenStore = NewMockRefreshTokenStore(s.ctrl) 66 | } 67 | return s.RefreshTokenStore 68 | } 69 | 70 | func (s *MockStores) StreamItems() StreamItemStore { 71 | if s.StreamItemStore == nil { 72 | s.StreamItemStore = NewMockStreamItemStore(s.ctrl) 73 | } 74 | return s.StreamItemStore 75 | } 76 | -------------------------------------------------------------------------------- /models/streamitem.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | 7 | "github.com/bwmarrin/snowflake" 8 | "github.com/jinzhu/gorm" 9 | "github.com/meowpub/meow/lib" 10 | ) 11 | 12 | //go:generate mockgen -package=models -source=streamitem.go -destination=streamitem.mock.go 13 | 14 | // StreamItem represents a single item in a string 15 | type StreamItem struct { 16 | ItemID snowflake.ID `json:"item_id"` 17 | StreamID snowflake.ID `json:"stream_id"` 18 | EntityID snowflake.ID `json:"entity_id"` 19 | } 20 | 21 | type Direction int 22 | 23 | const ( 24 | Before Direction = -1 25 | After Direction = 1 26 | 27 | Beginning snowflake.ID = 0 28 | End snowflake.ID = math.MaxInt64 29 | ) 30 | 31 | type StreamItemStore interface { 32 | // GetItems returns count items from the specified stream, starting 33 | // after/before the specified item, in the specified direction. Pass "Beginning"/"End" to 34 | // start at the beginning or end of the stream 35 | GetItems(streamID snowflake.ID, startID snowflake.ID, direction Direction, count uint) ([]StreamItem, error) 36 | 37 | // GetItem returns a specific stream item 38 | GetItem(itemID snowflake.ID) (*StreamItem, error) 39 | 40 | // GetItemByEntityId returns a specific stream item by stream and entry ID 41 | GetItemByEntityID(streamID snowflake.ID, entityID snowflake.ID) (*StreamItem, error) 42 | 43 | // TryInsertItem attempts to insert the specified entity ID into the stream. 44 | // Returns: 45 | // * If the entry was inserted 46 | // * The stream item (regardless) 47 | TryInsertItem(stream snowflake.ID, entityId snowflake.ID) (*StreamItem, bool, error) 48 | } 49 | 50 | type streamItemStore struct { 51 | DB *gorm.DB 52 | } 53 | 54 | func NewStreamItemStore(db *gorm.DB) StreamItemStore { 55 | return &streamItemStore{db} 56 | } 57 | 58 | func (s *streamItemStore) GetItems(streamID snowflake.ID, startID snowflake.ID, direction Direction, count uint) ([]StreamItem, error) { 59 | var items []StreamItem 60 | 61 | q := s.DB.Where("stream_id = ?", streamID) 62 | 63 | if direction == Before { 64 | q = q.Where("item_id < ?", startID).Order("item_id DESC") 65 | } else if direction == After { 66 | q = q.Where("item_id > ?", startID).Order("item_id ASC") 67 | } else { 68 | return nil, errors.New("Bad direction") 69 | } 70 | 71 | if err := q.Limit(count).Find(&items).Error; err != nil { 72 | return nil, err 73 | } 74 | return items, nil 75 | 76 | } 77 | 78 | func (s *streamItemStore) GetItem(itemID snowflake.ID) (*StreamItem, error) { 79 | var item StreamItem 80 | return &item, s.DB.First(&item, StreamItem{ItemID: itemID}).Error 81 | } 82 | 83 | func (s *streamItemStore) GetItemByEntityID(streamID snowflake.ID, entityID snowflake.ID) (*StreamItem, error) { 84 | var item StreamItem 85 | return &item, s.DB.First(&item, StreamItem{StreamID: streamID, EntityID: entityID}).Error 86 | } 87 | 88 | func (s *streamItemStore) TryInsertItem(streamID snowflake.ID, entityID snowflake.ID) (*StreamItem, bool, error) { 89 | item, err := s.GetItemByEntityID(streamID, entityID) 90 | 91 | if err != nil { 92 | // Not found - insert our own entry 93 | if gorm.IsRecordNotFoundError(err) { 94 | item := &StreamItem{ 95 | StreamID: streamID, 96 | EntityID: entityID, 97 | ItemID: lib.GenSnowflake(), 98 | } 99 | err = s.DB.Create(item).Error 100 | 101 | return item, true, err 102 | } 103 | 104 | // Other error 105 | return nil, false, err 106 | } 107 | 108 | // Found existing item 109 | return item, false, nil 110 | } 111 | -------------------------------------------------------------------------------- /models/streamitem.mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: streamitem.go 3 | 4 | // Package models is a generated GoMock package. 5 | package models 6 | 7 | import ( 8 | snowflake "github.com/bwmarrin/snowflake" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockStreamItemStore is a mock of StreamItemStore interface 14 | type MockStreamItemStore struct { 15 | ctrl *gomock.Controller 16 | recorder *MockStreamItemStoreMockRecorder 17 | } 18 | 19 | // MockStreamItemStoreMockRecorder is the mock recorder for MockStreamItemStore 20 | type MockStreamItemStoreMockRecorder struct { 21 | mock *MockStreamItemStore 22 | } 23 | 24 | // NewMockStreamItemStore creates a new mock instance 25 | func NewMockStreamItemStore(ctrl *gomock.Controller) *MockStreamItemStore { 26 | mock := &MockStreamItemStore{ctrl: ctrl} 27 | mock.recorder = &MockStreamItemStoreMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockStreamItemStore) EXPECT() *MockStreamItemStoreMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // GetItems mocks base method 37 | func (m *MockStreamItemStore) GetItems(streamID, startID snowflake.ID, direction Direction, count uint) ([]StreamItem, error) { 38 | ret := m.ctrl.Call(m, "GetItems", streamID, startID, direction, count) 39 | ret0, _ := ret[0].([]StreamItem) 40 | ret1, _ := ret[1].(error) 41 | return ret0, ret1 42 | } 43 | 44 | // GetItems indicates an expected call of GetItems 45 | func (mr *MockStreamItemStoreMockRecorder) GetItems(streamID, startID, direction, count interface{}) *gomock.Call { 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItems", reflect.TypeOf((*MockStreamItemStore)(nil).GetItems), streamID, startID, direction, count) 47 | } 48 | 49 | // GetItem mocks base method 50 | func (m *MockStreamItemStore) GetItem(itemID snowflake.ID) (*StreamItem, error) { 51 | ret := m.ctrl.Call(m, "GetItem", itemID) 52 | ret0, _ := ret[0].(*StreamItem) 53 | ret1, _ := ret[1].(error) 54 | return ret0, ret1 55 | } 56 | 57 | // GetItem indicates an expected call of GetItem 58 | func (mr *MockStreamItemStoreMockRecorder) GetItem(itemID interface{}) *gomock.Call { 59 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItem", reflect.TypeOf((*MockStreamItemStore)(nil).GetItem), itemID) 60 | } 61 | 62 | // GetItemByEntityID mocks base method 63 | func (m *MockStreamItemStore) GetItemByEntityID(streamID, entityID snowflake.ID) (*StreamItem, error) { 64 | ret := m.ctrl.Call(m, "GetItemByEntityID", streamID, entityID) 65 | ret0, _ := ret[0].(*StreamItem) 66 | ret1, _ := ret[1].(error) 67 | return ret0, ret1 68 | } 69 | 70 | // GetItemByEntityID indicates an expected call of GetItemByEntityID 71 | func (mr *MockStreamItemStoreMockRecorder) GetItemByEntityID(streamID, entityID interface{}) *gomock.Call { 72 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItemByEntityID", reflect.TypeOf((*MockStreamItemStore)(nil).GetItemByEntityID), streamID, entityID) 73 | } 74 | 75 | // TryInsertItem mocks base method 76 | func (m *MockStreamItemStore) TryInsertItem(stream, entityId snowflake.ID) (*StreamItem, bool, error) { 77 | ret := m.ctrl.Call(m, "TryInsertItem", stream, entityId) 78 | ret0, _ := ret[0].(*StreamItem) 79 | ret1, _ := ret[1].(bool) 80 | ret2, _ := ret[2].(error) 81 | return ret0, ret1, ret2 82 | } 83 | 84 | // TryInsertItem indicates an expected call of TryInsertItem 85 | func (mr *MockStreamItemStoreMockRecorder) TryInsertItem(stream, entityId interface{}) *gomock.Call { 86 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TryInsertItem", reflect.TypeOf((*MockStreamItemStore)(nil).TryInsertItem), stream, entityId) 87 | } 88 | -------------------------------------------------------------------------------- /models/streamitem_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bwmarrin/snowflake" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestStreamItemStore(t *testing.T) { 11 | tx := TestDB.Begin() 12 | defer tx.Rollback() 13 | 14 | store := NewStreamItemStore(tx) 15 | 16 | // Empty DB - everything should be not found 17 | 18 | t.Run("Not Found", func(t *testing.T) { 19 | _, err := store.GetItem(snowflake.ID(353894652568535040)) 20 | require.EqualError(t, err, "record not found") 21 | }) 22 | 23 | t.Run("Empty", func(t *testing.T) { 24 | l, err := store.GetItems(snowflake.ID(353894652568535040), Beginning, After, 20) 25 | require.NoError(t, err, "GetItems") 26 | require.Equal(t, 0, len(l)) 27 | }) 28 | 29 | t.Run("Not Found by Entity ID", func(t *testing.T) { 30 | _, err := store.GetItemByEntityID(snowflake.ID(353894652568535040), snowflake.ID(384408932061417472)) 31 | require.EqualError(t, err, "record not found") 32 | }) 33 | 34 | // Now we should insert some items into a stream 35 | t.Run("Insert", func(t *testing.T) { 36 | t.Run("Candles", func(t *testing.T) { 37 | item, inserted, err := store.TryInsertItem(snowflake.ID(353894652568535040), snowflake.ID(384408932061417472)) 38 | require.NoError(t, err, "TryInsertItem") 39 | require.Equal(t, true, inserted, "Should have inserted") 40 | require.Equal(t, snowflake.ID(353894652568535040), item.StreamID, "stream ID should match") 41 | require.Equal(t, snowflake.ID(384408932061417472), item.EntityID, "entity ID should match") 42 | }) 43 | 44 | t.Run("No", func(t *testing.T) { 45 | item, inserted, err := store.TryInsertItem(snowflake.ID(353894652568535040), snowflake.ID(384411458794057728)) 46 | require.NoError(t, err, "TryInsertItem") 47 | require.Equal(t, true, inserted, "Should have inserted") 48 | require.Equal(t, snowflake.ID(353894652568535040), item.StreamID, "stream ID should match") 49 | require.Equal(t, snowflake.ID(384411458794057728), item.EntityID, "entity ID should match") 50 | }) 51 | }) 52 | 53 | // Check we don't insert duplicates 54 | t.Run("Duplicates", func(t *testing.T) { 55 | t.Run("Candles", func(t *testing.T) { 56 | item, inserted, err := store.TryInsertItem(snowflake.ID(353894652568535040), snowflake.ID(384408932061417472)) 57 | require.NoError(t, err, "TryInsertItem") 58 | require.Equal(t, false, inserted, "Should not have inserted") 59 | require.Equal(t, snowflake.ID(353894652568535040), item.StreamID, "stream ID should match") 60 | require.Equal(t, snowflake.ID(384408932061417472), item.EntityID, "entity ID should match") 61 | }) 62 | 63 | t.Run("No", func(t *testing.T) { 64 | item, inserted, err := store.TryInsertItem(snowflake.ID(353894652568535040), snowflake.ID(384411458794057728)) 65 | require.NoError(t, err, "TryInsertItem") 66 | require.Equal(t, false, inserted, "Should not have inserted") 67 | require.Equal(t, snowflake.ID(353894652568535040), item.StreamID, "stream ID should match") 68 | require.Equal(t, snowflake.ID(384411458794057728), item.EntityID, "entity ID should match") 69 | }) 70 | }) 71 | 72 | // Fetch items back out of stream 73 | t.Run("Fetch", func(t *testing.T) { 74 | t.Run("Forwards", func(t *testing.T) { 75 | l, err := store.GetItems(snowflake.ID(353894652568535040), Beginning, After, 20) 76 | require.NoError(t, err, "GetItems") 77 | require.Equal(t, 2, len(l), "2 items") 78 | require.Equal(t, snowflake.ID(384408932061417472), l[0].EntityID, "id 1") 79 | require.Equal(t, snowflake.ID(384411458794057728), l[1].EntityID, "id 2") 80 | }) 81 | 82 | t.Run("Forwards from End", func(t *testing.T) { 83 | l, err := store.GetItems(snowflake.ID(353894652568535040), End, After, 20) 84 | require.NoError(t, err, "GetItems") 85 | require.Equal(t, 0, len(l), "0 items") 86 | }) 87 | 88 | t.Run("Backwards", func(t *testing.T) { 89 | l, err := store.GetItems(snowflake.ID(353894652568535040), End, Before, 20) 90 | require.NoError(t, err, "GetItems") 91 | require.Equal(t, 2, len(l), "2 items") 92 | require.Equal(t, snowflake.ID(384411458794057728), l[0].EntityID, "id 1") 93 | require.Equal(t, snowflake.ID(384408932061417472), l[1].EntityID, "id 2") 94 | }) 95 | 96 | t.Run("Backwards from Beginning", func(t *testing.T) { 97 | l, err := store.GetItems(snowflake.ID(353894652568535040), Beginning, Before, 20) 98 | require.NoError(t, err, "GetItems") 99 | require.Equal(t, 0, len(l), "0 items") 100 | }) 101 | 102 | t.Run("After First", func(t *testing.T) { 103 | i, err := store.GetItemByEntityID(snowflake.ID(353894652568535040), snowflake.ID(384408932061417472)) 104 | require.NoError(t, err, "first record should be found") 105 | 106 | l, err := store.GetItems(snowflake.ID(353894652568535040), i.ItemID, After, 20) 107 | require.NoError(t, err, "GetItems") 108 | require.Equal(t, 1, len(l), "1 items") 109 | require.Equal(t, snowflake.ID(384411458794057728), l[0].EntityID, "id 1") 110 | }) 111 | 112 | t.Run("After Second", func(t *testing.T) { 113 | i, err := store.GetItemByEntityID(snowflake.ID(353894652568535040), snowflake.ID(384411458794057728)) 114 | require.NoError(t, err, "second record should be found") 115 | 116 | l, err := store.GetItems(snowflake.ID(353894652568535040), i.ItemID, After, 20) 117 | require.NoError(t, err, "GetItems") 118 | require.Equal(t, 0, len(l), "0 items") 119 | }) 120 | 121 | t.Run("Before Second", func(t *testing.T) { 122 | i, err := store.GetItemByEntityID(snowflake.ID(353894652568535040), snowflake.ID(384411458794057728)) 123 | require.NoError(t, err, "second record should be found") 124 | 125 | l, err := store.GetItems(snowflake.ID(353894652568535040), i.ItemID, Before, 20) 126 | require.NoError(t, err, "GetItems") 127 | require.Equal(t, 1, len(l), "1 items") 128 | require.Equal(t, snowflake.ID(384408932061417472), l[0].EntityID, "id 1") 129 | }) 130 | 131 | t.Run("Before First", func(t *testing.T) { 132 | i, err := store.GetItemByEntityID(snowflake.ID(353894652568535040), snowflake.ID(384408932061417472)) 133 | require.NoError(t, err, "first record should be found") 134 | 135 | l, err := store.GetItems(snowflake.ID(353894652568535040), i.ItemID, Before, 20) 136 | require.NoError(t, err, "GetItems") 137 | require.Equal(t, 0, len(l), "0 items") 138 | }) 139 | 140 | t.Run("Empty Stream", func(t *testing.T) { 141 | l, err := store.GetItems(snowflake.ID(384408932061417472), Beginning, After, 20) 142 | require.NoError(t, err, "GetItems") 143 | require.Equal(t, 0, len(l), "0 items") 144 | }) 145 | }) 146 | } 147 | -------------------------------------------------------------------------------- /models/types.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "bytes" 5 | "database/sql/driver" 6 | "encoding/json" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // JSONB type based on the jinzhu/gorm one, which serialises JSON *as strings*, mostly to produce 12 | // more useful logging than the nonsense byte arrays produce. 13 | type JSONB []byte 14 | 15 | func (j JSONB) String() string { 16 | return string(j) 17 | } 18 | 19 | func (j *JSONB) UnmarshalJSON(data []byte) error { 20 | if bytes.Equal(data, []byte(`null`)) { 21 | *j = JSONB{} 22 | } 23 | *j = make(JSONB, len(data)) 24 | copy(*j, data) 25 | return nil 26 | } 27 | 28 | func (j JSONB) MarshalJSON() ([]byte, error) { 29 | return []byte(j), nil 30 | } 31 | 32 | func (j JSONB) Value() (driver.Value, error) { 33 | if len(j) == 0 { 34 | return nil, nil 35 | } 36 | data, err := json.Marshal(j) 37 | return string(data), err 38 | } 39 | 40 | func (j *JSONB) Scan(v interface{}) error { 41 | data, ok := v.([]byte) 42 | if !ok { 43 | return errors.Errorf("scanned value for jsonb has invalid type: %T", v) 44 | } 45 | return json.Unmarshal(data, j) 46 | } 47 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/bwmarrin/snowflake" 5 | "github.com/jinzhu/gorm" 6 | "golang.org/x/crypto/bcrypt" 7 | 8 | "github.com/meowpub/meow/lib" 9 | ) 10 | 11 | //go:generate mockgen -package=models -source=user.go -destination=user.mock.go 12 | 13 | var userOnConflict = genOnConflict(User{}, "id") 14 | 15 | // A User stores authentication data for a local user. This is separate from profile information, 16 | // which is stored as an Entity. This should never be sent to clients, and only used internally. 17 | // To get the public ID, etc., please look up the related Entity. 18 | type User struct { 19 | ID snowflake.ID `json:"id"` 20 | EntityID snowflake.ID `json:"entity_id"` 21 | Email string `json:"email"` 22 | PasswordHash string `json:"-"` 23 | } 24 | 25 | func NewUser(profileID snowflake.ID, email, password string) (*User, error) { 26 | user := User{ 27 | ID: lib.GenSnowflake(), 28 | EntityID: profileID, 29 | Email: email, 30 | } 31 | return &user, user.SetPassword(password) 32 | } 33 | 34 | // SetPassword updates the user's PasswordHash by bcrypt'ing the password. 35 | func (u *User) SetPassword(password string) error { 36 | hash, err := bcrypt.GenerateFromPassword([]byte(password), 0) 37 | u.PasswordHash = string(hash) 38 | return err 39 | } 40 | 41 | // CheckPassword compares the given password to the stored hash. Returns whether the comparison 42 | // succeeded, and any errors produced. Please note that if an error is produced here, you should 43 | // NOT RELAY IT VERBATIM TO THE USER; log it, check the bool and return a generic "unauthorized" 44 | // response if it's false. 45 | func (u *User) CheckPassword(password string) (bool, error) { 46 | err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) 47 | if err == bcrypt.ErrMismatchedHashAndPassword { 48 | return false, nil 49 | } 50 | return err == nil, err 51 | } 52 | 53 | type UserStore interface { 54 | GetBySnowflake(id snowflake.ID) (*User, error) 55 | GetByEntityID(id snowflake.ID) (*User, error) 56 | GetByEmail(email string) (*User, error) 57 | Save(u *User) error 58 | } 59 | 60 | type userStore struct { 61 | DB *gorm.DB 62 | } 63 | 64 | func NewUserStore(db *gorm.DB) UserStore { 65 | return &userStore{db} 66 | } 67 | 68 | func (s *userStore) GetBySnowflake(id snowflake.ID) (*User, error) { 69 | var u User 70 | return &u, s.DB.First(&u, User{ID: id}).Error 71 | } 72 | 73 | func (s *userStore) GetByEntityID(eid snowflake.ID) (*User, error) { 74 | var u User 75 | return &u, s.DB.First(&u, User{EntityID: eid}).Error 76 | } 77 | 78 | func (s *userStore) GetByEmail(email string) (*User, error) { 79 | var u User 80 | return &u, s.DB.First(&u, User{Email: email}).Error 81 | } 82 | 83 | func (s *userStore) Save(u *User) error { 84 | return s.DB.Set(gormInsertOption, userOnConflict).Create(u).Error 85 | } 86 | -------------------------------------------------------------------------------- /models/user.mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: user.go 3 | 4 | // Package models is a generated GoMock package. 5 | package models 6 | 7 | import ( 8 | snowflake "github.com/bwmarrin/snowflake" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockUserStore is a mock of UserStore interface 14 | type MockUserStore struct { 15 | ctrl *gomock.Controller 16 | recorder *MockUserStoreMockRecorder 17 | } 18 | 19 | // MockUserStoreMockRecorder is the mock recorder for MockUserStore 20 | type MockUserStoreMockRecorder struct { 21 | mock *MockUserStore 22 | } 23 | 24 | // NewMockUserStore creates a new mock instance 25 | func NewMockUserStore(ctrl *gomock.Controller) *MockUserStore { 26 | mock := &MockUserStore{ctrl: ctrl} 27 | mock.recorder = &MockUserStoreMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockUserStore) EXPECT() *MockUserStoreMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // GetBySnowflake mocks base method 37 | func (m *MockUserStore) GetBySnowflake(id snowflake.ID) (*User, error) { 38 | ret := m.ctrl.Call(m, "GetBySnowflake", id) 39 | ret0, _ := ret[0].(*User) 40 | ret1, _ := ret[1].(error) 41 | return ret0, ret1 42 | } 43 | 44 | // GetBySnowflake indicates an expected call of GetBySnowflake 45 | func (mr *MockUserStoreMockRecorder) GetBySnowflake(id interface{}) *gomock.Call { 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBySnowflake", reflect.TypeOf((*MockUserStore)(nil).GetBySnowflake), id) 47 | } 48 | 49 | // GetByEntityID mocks base method 50 | func (m *MockUserStore) GetByEntityID(id snowflake.ID) (*User, error) { 51 | ret := m.ctrl.Call(m, "GetByEntityID", id) 52 | ret0, _ := ret[0].(*User) 53 | ret1, _ := ret[1].(error) 54 | return ret0, ret1 55 | } 56 | 57 | // GetByEntityID indicates an expected call of GetByEntityID 58 | func (mr *MockUserStoreMockRecorder) GetByEntityID(id interface{}) *gomock.Call { 59 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByEntityID", reflect.TypeOf((*MockUserStore)(nil).GetByEntityID), id) 60 | } 61 | 62 | // GetByEmail mocks base method 63 | func (m *MockUserStore) GetByEmail(email string) (*User, error) { 64 | ret := m.ctrl.Call(m, "GetByEmail", email) 65 | ret0, _ := ret[0].(*User) 66 | ret1, _ := ret[1].(error) 67 | return ret0, ret1 68 | } 69 | 70 | // GetByEmail indicates an expected call of GetByEmail 71 | func (mr *MockUserStoreMockRecorder) GetByEmail(email interface{}) *gomock.Call { 72 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByEmail", reflect.TypeOf((*MockUserStore)(nil).GetByEmail), email) 73 | } 74 | 75 | // Save mocks base method 76 | func (m *MockUserStore) Save(u *User) error { 77 | ret := m.ctrl.Call(m, "Save", u) 78 | ret0, _ := ret[0].(error) 79 | return ret0 80 | } 81 | 82 | // Save indicates an expected call of Save 83 | func (mr *MockUserStoreMockRecorder) Save(u interface{}) *gomock.Call { 84 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockUserStore)(nil).Save), u) 85 | } 86 | -------------------------------------------------------------------------------- /models/user_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/meowpub/meow/lib" 10 | ) 11 | 12 | func TestUserConflictClause(t *testing.T) { 13 | assert.Equal(t, "ON CONFLICT (id) DO UPDATE SET entity_id=EXCLUDED.entity_id, email=EXCLUDED.email, password_hash=EXCLUDED.password_hash", userOnConflict) 14 | } 15 | 16 | func TestUserPasswords(t *testing.T) { 17 | user := &User{} 18 | require.NoError(t, user.SetPassword("password")) 19 | 20 | ok, err := user.CheckPassword("abc123") 21 | assert.NoError(t, err) 22 | assert.False(t, ok, "incorrect password accepted!!") 23 | 24 | ok, err = user.CheckPassword("") 25 | assert.NoError(t, err) 26 | assert.False(t, ok, "blank password accepted!!") 27 | 28 | ok, err = user.CheckPassword("password") 29 | assert.NoError(t, err) 30 | assert.True(t, ok, "correct password rejected") 31 | } 32 | 33 | func TestUserStore(t *testing.T) { 34 | tx := TestDB.Begin() 35 | defer tx.Rollback() 36 | 37 | store := NewUserStore(tx) 38 | estore := NewEntityStore(tx) 39 | 40 | t.Run("Not Found", func(t *testing.T) { 41 | t.Run("Snowflake", func(t *testing.T) { 42 | _, err := store.GetBySnowflake(12345) 43 | require.EqualError(t, err, "record not found") 44 | }) 45 | t.Run("EntityID", func(t *testing.T) { 46 | _, err := store.GetByEntityID(12345) 47 | require.EqualError(t, err, "record not found") 48 | }) 49 | t.Run("Email", func(t *testing.T) { 50 | _, err := store.GetByEmail("test@example.com") 51 | require.EqualError(t, err, "record not found") 52 | }) 53 | }) 54 | 55 | eid := lib.GenSnowflake() 56 | entity := &Entity{ID: eid, Data: JSONB(`{ 57 | "@id": "https://example.com/~jsmith", 58 | "@type": ["http://schema.org/Person"], 59 | "http://schema.org/name": [{"@value": "John Smith"}] 60 | }`), Kind: "user"} 61 | require.NoError(t, estore.Save(entity)) 62 | 63 | id := lib.GenSnowflake() 64 | user := &User{ 65 | ID: id, 66 | EntityID: eid, 67 | Email: "jsmith@example.com", 68 | } 69 | assert.NoError(t, user.SetPassword("password")) 70 | 71 | t.Run("Password", func(t *testing.T) { 72 | ok, err := user.CheckPassword("password") 73 | assert.NoError(t, err) 74 | assert.True(t, ok, "password not accepted") 75 | }) 76 | 77 | t.Run("Create", func(t *testing.T) { 78 | require.NoError(t, store.Save(user)) 79 | }) 80 | 81 | t.Run("Get", func(t *testing.T) { 82 | t.Run("Snowflake", func(t *testing.T) { 83 | u2, err := store.GetBySnowflake(id) 84 | require.NoError(t, err) 85 | assert.Equal(t, user, u2) 86 | }) 87 | t.Run("EntityID", func(t *testing.T) { 88 | u2, err := store.GetByEntityID(entity.ID) 89 | require.NoError(t, err) 90 | assert.Equal(t, user, u2) 91 | }) 92 | t.Run("Email", func(t *testing.T) { 93 | u2, err := store.GetByEmail("jsmith@example.com") 94 | require.NoError(t, err) 95 | assert.Equal(t, user, u2) 96 | }) 97 | }) 98 | 99 | t.Run("Change Email", func(t *testing.T) { 100 | user.Email = "jsmith2@example.com" 101 | require.NoError(t, store.Save(user)) 102 | 103 | u2, err := store.GetBySnowflake(id) 104 | require.NoError(t, err) 105 | assert.Equal(t, user, u2) 106 | }) 107 | 108 | t.Run("Change Password", func(t *testing.T) { 109 | require.NoError(t, user.SetPassword("new_password")) 110 | require.NoError(t, store.Save(user)) 111 | 112 | u2, err := store.GetBySnowflake(id) 113 | require.NoError(t, err) 114 | assert.Equal(t, user, u2) 115 | 116 | t.Run("Password", func(t *testing.T) { 117 | ok, err := u2.CheckPassword("password") 118 | assert.NoError(t, err) 119 | assert.False(t, ok, "old password accepted!!") 120 | 121 | ok, err = u2.CheckPassword("new_password") 122 | assert.NoError(t, err) 123 | assert.True(t, ok, "new password not accepted") 124 | }) 125 | }) 126 | } 127 | -------------------------------------------------------------------------------- /models/util.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/jinzhu/gorm" 8 | ) 9 | 10 | const gormInsertOption = "gorm:insert_option" 11 | 12 | // genOnConflict build an ON CONFLICT clause from a model type. 13 | func genOnConflict(t interface{}, conflictOn string, exclude ...string) string { 14 | rT := reflect.TypeOf(t) 15 | seenExcludes := make(map[string]struct{}) 16 | 17 | var updates []string 18 | fieldLoop: 19 | for i := 0; i < rT.NumField(); i++ { 20 | field := rT.Field(i) 21 | if field.PkgPath != "" { 22 | continue // Field is unexported 23 | } 24 | dbName := gorm.ToDBName(field.Name) 25 | if tag := field.Tag.Get("gorm"); tag != "" { 26 | if tag == "-" { 27 | continue 28 | } 29 | parts := strings.Split(tag, ",") 30 | for _, part := range parts { 31 | if strings.HasPrefix(part, "column:") { 32 | dbName = strings.TrimPrefix(tag, "column:") 33 | } 34 | } 35 | } 36 | if dbName == "" || dbName == conflictOn { 37 | continue 38 | } 39 | for _, excl := range exclude { 40 | if dbName == excl { 41 | seenExcludes[dbName] = struct{}{} 42 | continue fieldLoop 43 | } 44 | } 45 | updates = append(updates, dbName+"=EXCLUDED."+dbName) 46 | } 47 | 48 | for _, excl := range exclude { 49 | if _, ok := seenExcludes[excl]; !ok { 50 | panic("genOnConflict exclusion on nonexistent column: " + excl) 51 | } 52 | } 53 | 54 | return "ON CONFLICT (" + conflictOn + ") DO UPDATE SET " + strings.Join(updates, ", ") 55 | } 56 | -------------------------------------------------------------------------------- /models/util_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bwmarrin/snowflake" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_genOnConflict(t *testing.T) { 11 | var v struct { 12 | ID snowflake.ID 13 | FirstName string 14 | LastName string 15 | Email string `gorm:"column:e_mail"` 16 | Priv string `gorm:"-"` 17 | priv string 18 | } 19 | assert.Equal(t, "ON CONFLICT (id) DO UPDATE SET first_name=EXCLUDED.first_name, last_name=EXCLUDED.last_name, e_mail=EXCLUDED.e_mail", genOnConflict(v, "id")) 20 | } 21 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | 8 |

9 | logging in uses cookies. 10 |

11 | -------------------------------------------------------------------------------- /templates/oauth/authorize.html: -------------------------------------------------------------------------------- 1 | hi -------------------------------------------------------------------------------- /tools/nsgen/.gitignore: -------------------------------------------------------------------------------- 1 | /nsgen 2 | /declarations.ld.json 3 | -------------------------------------------------------------------------------- /tools/nsgen/README.md: -------------------------------------------------------------------------------- 1 | nsgen 2 | ===== 3 | 4 | This program takes Turtle RDF declarations and turn them into code generated entities, by converting them into JSON-LD and using our own LD machinery. 5 | 6 | If you're hacking on this, be prepared to do a lot of `git checkout ../../ld/ns` whenever you generate a blurb of invalid code and the code generator no longer compiles. 7 | 8 | You'll find that some RDF out on the internet is written using horrible things like XML Schema and RDFa (Turtle embedded inside HTML, the horrific hybrid nobody asked for). For XML Schema you're basically fucked, but for RDFa, try this: https://www.w3.org/2012/pyRdfa 9 | -------------------------------------------------------------------------------- /tools/nsgen/declaration.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/meowpub/meow/ld" 7 | "github.com/meowpub/meow/ld/ns/rdf" 8 | ) 9 | 10 | var _ ld.Entity = Declaration{} 11 | 12 | type Declaration struct { 13 | *ld.Object 14 | NS *Namespace 15 | } 16 | 17 | func (t Declaration) Obj() *ld.Object { 18 | return t.Object 19 | } 20 | 21 | func (t Declaration) Short() string { 22 | if parts := strings.SplitN(t.ID(), "#", 2); len(parts) > 1 { 23 | return parts[1] 24 | } 25 | return t.ID() 26 | } 27 | 28 | func (t Declaration) Package() string { 29 | if t.NS != nil { 30 | return t.NS.Package 31 | } 32 | return "" 33 | } 34 | 35 | func (t Declaration) TypeName() string { 36 | if s := t.Short(); s != "" { 37 | return strings.ToUpper(s[0:1]) + s[1:] 38 | } 39 | return "" 40 | } 41 | 42 | func (t Declaration) FuncName() string { 43 | tname := t.TypeName() 44 | switch tname { 45 | case "Value": 46 | return "Value_" 47 | case "Type": 48 | return "Type_" 49 | default: 50 | return tname 51 | } 52 | } 53 | 54 | func (t Declaration) RDFTypes() []string { 55 | return ld.ObjectIDs(ld.ToObjects(t.V[rdf.Prop_Type.ID])) 56 | } 57 | 58 | func (t Declaration) Domain() string { 59 | return ld.ToObject(t.V[rdf.Prop_Domain.ID]).ID() 60 | } 61 | 62 | func (t Declaration) SubClassOf() []string { 63 | return ld.ObjectIDs(ld.ToObjects(t.V[rdf.Prop_SubClassOf.ID])) 64 | } 65 | -------------------------------------------------------------------------------- /tools/nsgen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "sort" 13 | "strings" 14 | "text/template" 15 | 16 | "github.com/deiu/rdf2go" 17 | "github.com/pkg/errors" 18 | "go.uber.org/multierr" 19 | "golang.org/x/tools/imports" 20 | 21 | "github.com/meowpub/meow/ld" 22 | ) 23 | 24 | // Base path to the github.com/meowpub/meow package. 25 | var MeowBasePath = filepath.Join(os.Getenv("GOPATH"), "src", "github.com", "meowpub", "meow") 26 | 27 | func LoadWithCache(uri, cachePath string) ([]byte, error) { 28 | if cacheData, err := ioutil.ReadFile(cachePath); err == nil { 29 | return cacheData, nil 30 | } else if !os.IsNotExist(err) { 31 | return nil, err 32 | } 33 | 34 | resp, err := http.Get(uri) 35 | if err != nil { 36 | return nil, err 37 | } 38 | defer resp.Body.Close() 39 | data, err := ioutil.ReadAll(resp.Body) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return data, ioutil.WriteFile(cachePath, data, 0644) 44 | } 45 | 46 | func DumpJSON(path string, v interface{}) error { 47 | data, err := json.MarshalIndent(v, "", " ") 48 | if err != nil { 49 | return err 50 | } 51 | return ioutil.WriteFile(path, data, 0644) 52 | } 53 | 54 | func Render(path string, tmpl *template.Template, rctx interface{}) error { 55 | var buf bytes.Buffer 56 | if err := tmpl.Execute(&buf, rctx); err != nil { 57 | return errors.Wrap(err, "render") 58 | } 59 | data := buf.Bytes() 60 | 61 | data2, err := imports.Process(path, data, nil) 62 | if err == nil { 63 | data = data2 64 | } 65 | 66 | return multierr.Append( 67 | errors.Wrap(err, "goimports"), 68 | ioutil.WriteFile(path, data, 0644), 69 | ) 70 | } 71 | 72 | func TurtleToJSONLD(namespace string, data []byte) ([]byte, error) { 73 | as2 := rdf2go.NewGraph(namespace) 74 | if err := as2.Parse(bytes.NewReader(data), "text/turtle"); err != nil { 75 | return nil, err 76 | } 77 | var buf bytes.Buffer 78 | if err := as2.Serialize(&buf, "application/ld+json"); err != nil { 79 | return nil, err 80 | } 81 | return buf.Bytes(), nil 82 | } 83 | 84 | func GetAndCreatePackagePath(pkg string) (string, error) { 85 | outdir := filepath.Join(MeowBasePath, "ld", "ns", pkg) 86 | return outdir, os.MkdirAll(outdir, 0755) 87 | } 88 | 89 | func NamespaceFragments(ns *Namespace) ([]*ld.Object, error) { 90 | pkgdir, err := GetAndCreatePackagePath(ns.Package) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | // Load and store the turtle data. 96 | turtlePath := filepath.Join(pkgdir, ns.Short+".ttl") 97 | turtleData, err := LoadWithCache(ns.Source, turtlePath) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | // Turn it into JSON-LD fragments. 103 | ldData, err := TurtleToJSONLD(ns.Long, turtleData) 104 | fragments, err := ld.ParseObjects(ldData) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | // Apply patches. 110 | fragments = append(fragments, ns.Patches...) 111 | 112 | // Sort this mess. 113 | sort.SliceStable(fragments, func(i, j int) bool { 114 | idata, err := json.Marshal(fragments[i]) 115 | if err != nil { 116 | panic(err) 117 | } 118 | jdata, err := json.Marshal(fragments[j]) 119 | if err != nil { 120 | panic(err) 121 | } 122 | return string(idata) < string(jdata) 123 | }) 124 | 125 | return fragments, DumpJSON(filepath.Join(pkgdir, ns.Short+".ld.json"), fragments) 126 | } 127 | 128 | func Main() error { 129 | var fragments []*ld.Object 130 | for _, ns := range Namespaces { 131 | if ns.Source == "" { 132 | continue 133 | } 134 | log.Printf("Loading: %s: %s", ns.Short, ns.Long) 135 | nsFragments, err := NamespaceFragments(ns) 136 | if err != nil { 137 | return errors.Wrap(err, ns.Short) 138 | } 139 | fragments = append(fragments, nsFragments...) 140 | } 141 | 142 | // Reassemble the fragments into usable Declarations. 143 | log.Printf("Reassembling fragments...") 144 | declMap := make(map[string]*Declaration) 145 | orderedKeys := []string{} 146 | for _, fragment := range fragments { 147 | key := fragment.ID() 148 | if key == "_:n0" { 149 | continue 150 | } 151 | if obj, ok := declMap[key]; ok { 152 | if err := obj.Apply(fragment, true); err != nil { 153 | return err 154 | } 155 | } else { 156 | declMap[key] = &Declaration{Object: fragment} 157 | orderedKeys = append(orderedKeys, key) 158 | } 159 | } 160 | declarations := make([]*Declaration, 0, len(declMap)) 161 | for _, key := range orderedKeys { 162 | declarations = append(declarations, declMap[key]) 163 | } 164 | // if err := DumpJSON("declarations.ld.json", declarations); err != nil { 165 | // return err 166 | // } 167 | 168 | // Generate packages! 169 | pkgs := make(map[string][]*Declaration) 170 | var namespaces []*Namespace 171 | for _, ns := range Namespaces { 172 | namespaces = append(namespaces, ns) 173 | for _, decl := range declarations { 174 | if strings.HasPrefix(decl.ID(), ns.Long) { 175 | decl.NS = ns 176 | pkgs[ns.Package] = append(pkgs[ns.Package], decl) 177 | } 178 | } 179 | } 180 | var errs []error 181 | for pkg, pkgdecls := range pkgs { 182 | outdir, err := GetAndCreatePackagePath(pkg) 183 | if err != nil { 184 | return errors.Wrap(err, pkg) 185 | } 186 | log.Printf("Generating package: %s: %s", pkg, outdir) 187 | rctx := &RenderContext{ 188 | Package: pkg, 189 | Declarations: pkgdecls, 190 | Packages: pkgs, 191 | Namespaces: namespaces, 192 | } 193 | errs = append(errs, 194 | errors.Wrap(Render(filepath.Join(outdir, "ns.gen.go"), NSTemplate, rctx), "ns.gen.go"), 195 | errors.Wrap(Render(filepath.Join(outdir, "properties.gen.go"), PropertiesTemplate, rctx), "properties.gen.go"), 196 | errors.Wrap(Render(filepath.Join(outdir, "classes.gen.go"), ClassesTemplate, rctx), "classes.gen.go"), 197 | errors.Wrap(Render(filepath.Join(outdir, "datatypes.gen.go"), DataTypesTemplate, rctx), "datatypes.gen.go"), 198 | ) 199 | } 200 | grctx := &RenderContext{ 201 | Declarations: declarations, 202 | Packages: pkgs, 203 | Namespaces: namespaces, 204 | } 205 | 206 | log.Printf("Generating index files...") 207 | errs = append(errs, 208 | errors.Wrap(Render(filepath.Join(MeowBasePath, "ld", "ns", "index.gen.go"), IndexTemplate, grctx), "index.gen.go"), 209 | ) 210 | // if err := multierr.Combine(errs...); err != nil { 211 | // return err 212 | // } 213 | 214 | // log.Printf("Generating index...") 215 | // indexPath := filepath.Join(MeowBasePath, "ld", "ns", "index.gen.go") 216 | // return Render(indexPath, IndexTemplate, Namespaces) 217 | return multierr.Combine(errs...) 218 | } 219 | 220 | func main() { 221 | if err := Main(); err != nil { 222 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /tools/nsgen/namespaces.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/meowpub/meow/ld" 5 | "github.com/meowpub/meow/tools/nsgen/patches" 6 | ) 7 | 8 | type Namespace struct { 9 | Package string 10 | Short string 11 | Long string 12 | Source string 13 | Patches []*ld.Object 14 | } 15 | 16 | var Namespaces = []*Namespace{ 17 | { 18 | Package: "rdf", 19 | Short: "rdf", 20 | Long: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 21 | Source: "https://www.w3.org/1999/02/22-rdf-syntax-ns", 22 | }, 23 | { 24 | Package: "rdf", 25 | Short: "rdfs", 26 | Long: "http://www.w3.org/2000/01/rdf-schema#", 27 | Source: "https://www.w3.org/2000/01/rdf-schema", 28 | }, 29 | { 30 | Package: "owl", 31 | Short: "owl", 32 | Long: "http://www.w3.org/2002/07/owl#", 33 | Source: "http://www.w3.org/2002/07/owl", 34 | }, 35 | { 36 | Package: "as", 37 | Short: "as", 38 | Long: "http://www.w3.org/ns/activitystreams#", 39 | Source: "https://raw.githubusercontent.com/w3c/activitystreams/master/vocabulary/activitystreams2.owl", 40 | }, 41 | { 42 | Package: "ldp", 43 | Short: "ldp", 44 | Long: "http://www.w3.org/ns/ldp#", 45 | Source: "http://www.w3.org/ns/ldp.ttl", 46 | }, 47 | { 48 | Package: "sec", 49 | Short: "sec", 50 | Long: "https://w3id.org/security#", 51 | Source: "https://www.w3.org/2012/pyRdfa/extract?uri=https://web-payments.org/vocabs/security", 52 | Patches: patches.SecPatches, 53 | }, 54 | 55 | // These aren't available in Turtle anywhere, so we have to implement them manually. 56 | // Well, XMLSchema is available in XML schema and... no. Just, no. Not parsing that. 57 | // {Package: "xsd", Short: "xsd", Long: "http://www.w3.org/2001/XMLSchema#"}, 58 | } 59 | -------------------------------------------------------------------------------- /tools/nsgen/patches/patch.go: -------------------------------------------------------------------------------- 1 | package patches 2 | 3 | import ( 4 | "github.com/meowpub/meow/ld" 5 | "github.com/meowpub/meow/ld/ns/rdf" 6 | ) 7 | 8 | func patch(id, key, subkey, value string) *ld.Object { 9 | return &ld.Object{V: map[string]interface{}{ 10 | "@id": id, 11 | key: []interface{}{ 12 | map[string]interface{}{subkey: value}, 13 | }, 14 | }} 15 | } 16 | 17 | func comment(typ, comment string) *ld.Object { 18 | return patch(typ, rdf.Prop_Comment.ID, "@value", comment) 19 | } 20 | -------------------------------------------------------------------------------- /tools/nsgen/templates_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_varsafe(t *testing.T) { 10 | assert.Equal(t, varsafe("http://example.com#Type"), "http_example_com_Type") 11 | } 12 | --------------------------------------------------------------------------------