Compare commits
	
		
			No commits in common. "1d9802530d4b2255a05c7c348a6c2d212e53a672" and "57deaa3670acfe3e1b063747e23a49a19932c00e" have entirely different histories.
		
	
	
		
			1d9802530d
			...
			57deaa3670
		
	
		|  | @ -1 +0,0 @@ | |||
| /target | ||||
|  | @ -1,45 +0,0 @@ | |||
| { | ||||
| 	// Use IntelliSense to learn about possible attributes. | ||||
| 	// Hover to view descriptions of existing attributes. | ||||
| 	// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||||
| 	"version": "0.2.0", | ||||
| 	"configurations": [ | ||||
| 		{ | ||||
| 			"type": "lldb", | ||||
| 			"request": "launch", | ||||
| 			"name": "Debug executable 'flabk'", | ||||
| 			"cargo": { | ||||
| 				"args": [ | ||||
| 					"build", | ||||
| 					"--bin=flabk", | ||||
| 					"--package=flabk" | ||||
| 				], | ||||
| 				"filter": { | ||||
| 					"name": "flabk", | ||||
| 					"kind": "bin" | ||||
| 				} | ||||
| 			}, | ||||
| 			"args": [], | ||||
| 			"cwd": "${workspaceFolder}" | ||||
| 		}, | ||||
| 		// { | ||||
| 		// 	"type": "lldb", | ||||
| 		// 	"request": "launch", | ||||
| 		// 	"name": "Debug unit tests in executable 'flabk'", | ||||
| 		// 	"cargo": { | ||||
| 		// 		"args": [ | ||||
| 		// 			"test", | ||||
| 		// 			"--no-run", | ||||
| 		// 			"--bin=flabk", | ||||
| 		// 			"--package=flabk" | ||||
| 		// 		], | ||||
| 		// 		"filter": { | ||||
| 		// 			"name": "flabk", | ||||
| 		// 			"kind": "bin" | ||||
| 		// 		} | ||||
| 		// 	}, | ||||
| 		// 	"args": [], | ||||
| 		// 	"cwd": "${workspaceFolder}" | ||||
| 		// } | ||||
| 	] | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										20
									
								
								Cargo.toml
								
								
								
								
							
							
						
						
									
										20
									
								
								Cargo.toml
								
								
								
								
							|  | @ -1,20 +0,0 @@ | |||
| [package] | ||||
| name = "flabk" | ||||
| version = "0.0.1" | ||||
| edition = "2021" | ||||
| 
 | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
| 
 | ||||
| [dependencies] | ||||
| anyhow = "1.0.64" | ||||
| base-62 = "0.1.1" | ||||
| handlebars = "4.3.3" | ||||
| mime_guess = "2.0.4" | ||||
| rand = "0.8.5" | ||||
| rust-embed = "6.4.0" | ||||
| serde = { version = "1.0.144", features = ["derive", "std", "serde_derive"]} | ||||
| serde_json = "1.0.85" | ||||
| tokio = { version = "1", features = ["full"] } | ||||
| tokio-postgres = { version = "0.7.7", features = ["with-serde_json-1"] } | ||||
| warp = { version = "0.3.2" } | ||||
| warp-embed = "0.4.0" | ||||
|  | @ -0,0 +1,44 @@ | |||
| package main | ||||
| 
 | ||||
| // MUST (client and server)  application/ld+json; profile="https://www.w3.org/ns/activitystreams
 | ||||
| // SHOULD application/activity+json
 | ||||
| 
 | ||||
| // public actor https://www.w3.org/ns/activitystreams#Public
 | ||||
| 
 | ||||
| func main() { | ||||
| 	panic(asfhttp.New("my.site").Run()) | ||||
| } | ||||
| 
 | ||||
| // {
 | ||||
| // 	"@context": ["https://www.w3.org/ns/activitystreams",
 | ||||
| // 				 {"@language": "en"}],
 | ||||
| // 	"type": "Like",
 | ||||
| // 	"actor": "https://dustycloud.org/chris/",
 | ||||
| // 	"name": "Chris liked 'Minimal ActivityPub update client'",
 | ||||
| // 	"object": "https://rhiaro.co.uk/2016/05/minimal-activitypub",
 | ||||
| // 	"to": ["https://rhiaro.co.uk/#amy",
 | ||||
| // 		   "https://dustycloud.org/followers",
 | ||||
| // 		   "https://rhiaro.co.uk/followers/"],
 | ||||
| // 	"cc": "https://e14n.com/evan"
 | ||||
| //   }
 | ||||
| 
 | ||||
| // {
 | ||||
| // 	"@context": [
 | ||||
| // 		"https://www.w3.org/ns/activitystreams",
 | ||||
| // 		{
 | ||||
| // 			"@language": "en"
 | ||||
| // 		}
 | ||||
| // 	],
 | ||||
| // 	"type": "Like",
 | ||||
| // 	"to": [
 | ||||
| // 		"https://rhiaro.co.uk/#amy",
 | ||||
| // 		"https://dustycloud.org/followers",
 | ||||
| // 		"https://rhiaro.co.uk/followers/"
 | ||||
| // 	],
 | ||||
| // 	"cc": [
 | ||||
| // 		"https://e14n.com/evan"
 | ||||
| // 	],
 | ||||
| // 	"object": "https://rhiaro.co.uk/2016/05/minimal-activitypub",
 | ||||
| // 	"actor": "https://dustycloud.org/chris/",
 | ||||
| // 	"name": "Chris liked 'Minimal ActivityPub update client'"
 | ||||
| // }
 | ||||
|  | @ -0,0 +1,36 @@ | |||
| package ap | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/go-ap/jsonld" | ||||
| ) | ||||
| 
 | ||||
| type Object struct { | ||||
| 	Context jsonld.Context `jsonld:"@context,omitempty,collapsible"` | ||||
| 	Type    string         `jsonld:"type"` | ||||
| 	Type2   string         `jsonld:"@type"` | ||||
| 	ID      jsonld.IRI     `jsonld:"id,omitempty"` | ||||
| 	Name    string         `jsonld:"name,omitempty"` | ||||
| 	Source  *Source        `jsonld:"source,omitempty"` | ||||
| 	To      []Object       `jsonld:"to,omitempty,collapsible"` | ||||
| 	CC      []Object       `jsonld:"cc,omitempty,collapsible"` | ||||
| 	Object  *Object        `jsonld:"object,omitempty,collapsible"` | ||||
| 	Actor   *Actor         `jsonld:"actor,omitempty,collapsible"` | ||||
| } | ||||
| 
 | ||||
| func (o Object) Collapse() interface{} { | ||||
| 	return o.ID | ||||
| } | ||||
| 
 | ||||
| type Actor struct { | ||||
| 	ID   jsonld.IRI `jsonld:"id,omitempty"` | ||||
| 	Name string     `jsonld:"name,omitempty"` | ||||
| } | ||||
| 
 | ||||
| func (a Actor) Collapse() interface{} { | ||||
| 	return a.ID | ||||
| } | ||||
| 
 | ||||
| type Source struct { | ||||
| 	Content   string `jsonld:"content,omitempty"` | ||||
| 	MediaType string `jsonld:"mediaType,omitempty"` | ||||
| } | ||||
|  | @ -0,0 +1,52 @@ | |||
| package ap_test | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/go-ap/jsonld" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 	"sectorinf.com/emilis/flabk/flabk/ap" | ||||
| ) | ||||
| 
 | ||||
| func GetExample() ap.Object { | ||||
| 	return ap.Object{ | ||||
| 		Context: jsonld.Context{ | ||||
| 			{ | ||||
| 				IRI: jsonld.IRI("https://www.w3.org/ns/activitystreams"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				Term: jsonld.LanguageKw, | ||||
| 				IRI:  "en", | ||||
| 			}, | ||||
| 		}, | ||||
| 		Type: "Like", | ||||
| 		Actor: &ap.Actor{ | ||||
| 			ID: "https://dustycloud.org/chris/", | ||||
| 		}, | ||||
| 		Name: "Chris liked 'Minimal ActivityPub update client'", | ||||
| 		Object: &ap.Object{ | ||||
| 			ID: "https://rhiaro.co.uk/2016/05/minimal-activitypub", | ||||
| 		}, | ||||
| 		To: []ap.Object{ | ||||
| 			{ID: "https://rhiaro.co.uk/#amy"}, | ||||
| 			{ID: "https://dustycloud.org/followers"}, | ||||
| 			{ID: "https://rhiaro.co.uk/followers/"}, | ||||
| 		}, | ||||
| 		CC: []ap.Object{ | ||||
| 			{ID: "https://e14n.com/evan"}, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestObject(t *testing.T) { | ||||
| 	that := require.New(t) | ||||
| 	result, err := jsonld.Marshal(GetExample()) | ||||
| 	that.NoError(err) | ||||
| 	fmt.Println(string(result)) | ||||
| 	fmt.Println() | ||||
| 	another := ap.Object{} | ||||
| 	err = jsonld.Unmarshal(result, &another) | ||||
| 	that.NoError(err) | ||||
| 	fmt.Printf("%+v\n", another) | ||||
| } | ||||
|  | @ -0,0 +1,46 @@ | |||
| package flabweb | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"sectorinf.com/emilis/flabk/util/uriutil" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	EndpointOutbox = "outbox" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	tempOutboxIDCauseImNotStoringThis = 0 | ||||
| ) | ||||
| 
 | ||||
| type Server struct { | ||||
| 	router   *gin.Engine | ||||
| 	v1       *gin.RouterGroup | ||||
| 	hostname string | ||||
| } | ||||
| 
 | ||||
| func New(hostname string) Server { | ||||
| 	router := gin.Default() | ||||
| 	v1 := router.Group("v1") | ||||
| 	server := Server{ | ||||
| 		hostname: hostname, | ||||
| 		router:   router, | ||||
| 		v1:       v1, | ||||
| 	} | ||||
| 	v1.POST(EndpointOutbox, server.Outbox) | ||||
| 	return server | ||||
| } | ||||
| 
 | ||||
| func (s Server) Run() error { | ||||
| 	return s.router.Run(":8081") | ||||
| } | ||||
| 
 | ||||
| func (s Server) Outbox(ctx *gin.Context) { | ||||
| 	id := tempOutboxIDCauseImNotStoringThis | ||||
| 	tempOutboxIDCauseImNotStoringThis++ | ||||
| 	ctx.Header("Location", uriutil.JoinURIs(s.hostname, EndpointOutbox, strconv.Itoa(id))) | ||||
| 	ctx.Status(http.StatusCreated) | ||||
| } | ||||
|  | @ -0,0 +1,35 @@ | |||
| module sectorinf.com/emilis/flabk | ||||
| 
 | ||||
| go 1.18 | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/gin-gonic/gin v1.8.1 | ||||
| 	github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d | ||||
| 	github.com/piprate/json-gold v0.4.1 | ||||
| 	github.com/stretchr/testify v1.7.2 | ||||
| ) | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||
| 	github.com/gin-contrib/sse v0.1.0 // indirect | ||||
| 	github.com/go-playground/locales v0.14.0 // indirect | ||||
| 	github.com/go-playground/universal-translator v0.18.0 // indirect | ||||
| 	github.com/go-playground/validator/v10 v10.11.0 // indirect | ||||
| 	github.com/goccy/go-json v0.9.8 // indirect | ||||
| 	github.com/json-iterator/go v1.1.12 // indirect | ||||
| 	github.com/leodido/go-urn v1.2.1 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.14 // indirect | ||||
| 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||||
| 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||
| 	github.com/pelletier/go-toml/v2 v2.0.2 // indirect | ||||
| 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||
| 	github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect | ||||
| 	github.com/ugorji/go/codec v1.2.7 // indirect | ||||
| 	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect | ||||
| 	golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect | ||||
| 	golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e // indirect | ||||
| 	golang.org/x/text v0.3.7 // indirect | ||||
| 	google.golang.org/protobuf v1.28.0 // indirect | ||||
| 	gopkg.in/yaml.v2 v2.4.0 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| ) | ||||
|  | @ -0,0 +1,100 @@ | |||
| github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= | ||||
| github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= | ||||
| github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= | ||||
| github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= | ||||
| github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d h1:Z/oRXMlZHjvjIqDma1FrIGL3iE5YL7MUI0bwYEZ6qbA= | ||||
| github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA= | ||||
| github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= | ||||
| github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= | ||||
| github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= | ||||
| github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= | ||||
| github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= | ||||
| github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= | ||||
| github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= | ||||
| github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= | ||||
| github.com/goccy/go-json v0.9.8 h1:DxXB6MLd6yyel7CLph8EwNIonUtVZd3Ue5iRcL4DQCE= | ||||
| github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= | ||||
| github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= | ||||
| github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= | ||||
| github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||
| github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | ||||
| github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | ||||
| github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||||
| github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= | ||||
| github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= | ||||
| github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= | ||||
| github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||||
| github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||
| github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||||
| github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= | ||||
| github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= | ||||
| github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= | ||||
| github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= | ||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= | ||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | ||||
| github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | ||||
| github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw= | ||||
| github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI= | ||||
| github.com/piprate/json-gold v0.4.1 h1:JYbYN36n6YcAYipKy3ttv3X2HDQPeqWqmwta35NPj04= | ||||
| github.com/piprate/json-gold v0.4.1/go.mod h1:OK1z7UgtBZk06n2cDE2OSq1kffmjFFp5/2yhLLCz9UM= | ||||
| github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= | ||||
| github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= | ||||
| github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= | ||||
| github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= | ||||
| github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= | ||||
| github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= | ||||
| github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= | ||||
| github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= | ||||
| github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= | ||||
| golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= | ||||
| golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= | ||||
| golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= | ||||
| golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||
| golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw= | ||||
| golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e h1:CsOuNlbOuf0mzxJIefr6Q4uAUetRUwZE4qt7VfzP+xo= | ||||
| golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= | ||||
| golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= | ||||
| google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= | ||||
| google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= | ||||
| gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= | ||||
| gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= | ||||
| gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
|  | @ -1,10 +0,0 @@ | |||
| CREATE TABLE users ( | ||||
| 	id CHAR(22) NOT NULL PRIMARY KEY, | ||||
| 	username TEXT NOT NULL, | ||||
| 	host TEXT, | ||||
| 	display_name TEXT | ||||
| ); | ||||
| 
 | ||||
| CREATE UNIQUE INDEX u_username_host ON users (username, host); | ||||
| CREATE UNIQUE INDEX u_username_local ON users (username) WHERE host IS NULL; | ||||
| 
 | ||||
|  | @ -0,0 +1,69 @@ | |||
| // Package coll provides generic collection types and functions
 | ||||
| package coll | ||||
| 
 | ||||
| func Map[T, V any](c Vector[T], f func(T) V) Vector[V] { | ||||
| 	out := make([]V, len(c)) | ||||
| 	for index := 0; index < len(c); index++ { | ||||
| 		out[index] = f(c[index]) | ||||
| 	} | ||||
| 
 | ||||
| 	return From(out) | ||||
| } | ||||
| 
 | ||||
| func Filter[T any](v Vector[T], f func(T) bool) Vector[T] { | ||||
| 	out := WithCap[T](len(v)) | ||||
| 	for _, t := range v { | ||||
| 		if f(t) { | ||||
| 			out = out.Push(t) | ||||
| 		} | ||||
| 	} | ||||
| 	return out | ||||
| } | ||||
| 
 | ||||
| func Take[T any](v Vector[T], howMany int) Vector[T] { | ||||
| 	if len(v) == 0 { | ||||
| 		return New[T]() | ||||
| 	} | ||||
| 	if len(v) < howMany { | ||||
| 		howMany = len(v) | ||||
| 	} | ||||
| 	return From(v[:howMany-1]) | ||||
| } | ||||
| 
 | ||||
| func Any[T any](v Vector[T], f func(T) bool) bool { | ||||
| 	for _, t := range v { | ||||
| 		if f(t) { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| type numeric interface { | ||||
| 	~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64 | ||||
| } | ||||
| 
 | ||||
| func Min[T numeric](v Vector[T]) T { | ||||
| 	var min T | ||||
| 	if len(v) == 0 { | ||||
| 		return min | ||||
| 	} | ||||
| 	min = v[0] | ||||
| 	for _, t := range v { | ||||
| 		if min > t { | ||||
| 			min = t | ||||
| 		} | ||||
| 	} | ||||
| 	return min | ||||
| } | ||||
| 
 | ||||
| func Max[T numeric](v Vector[T]) T { | ||||
| 	var max T | ||||
| 	for _, t := range v { | ||||
| 		if max < t { | ||||
| 			max = t | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return max | ||||
| } | ||||
|  | @ -0,0 +1,138 @@ | |||
| package coll | ||||
| 
 | ||||
| import "fmt" | ||||
| 
 | ||||
| const ( | ||||
| 	DefaultSize = 4 | ||||
| ) | ||||
| 
 | ||||
| type Vector[T any] []T | ||||
| 
 | ||||
| func New[T any]() Vector[T] { | ||||
| 	return WithCap[T](DefaultSize) | ||||
| } | ||||
| 
 | ||||
| func WithCap[T any](capacity int) Vector[T] { | ||||
| 	return make(Vector[T], 0, capacity) | ||||
| } | ||||
| 
 | ||||
| func From[T any](v []T) Vector[T] { | ||||
| 	return Vector[T](v) | ||||
| } | ||||
| 
 | ||||
| func (v Vector[T]) Push(t T) Vector[T] { | ||||
| 	if len(v) == cap(v) { | ||||
| 		v = v.upsize() | ||||
| 	} | ||||
| 
 | ||||
| 	v = v[:len(v)+1] | ||||
| 	v[len(v)-1] = t | ||||
| 	return v | ||||
| } | ||||
| 
 | ||||
| func (v Vector[T]) Append(t ...T) Vector[T] { | ||||
| 	if cap(v)-len(v) < len(t) { | ||||
| 		v = v.upsizeSpecific(len(t)) | ||||
| 	} | ||||
| 	startLen := len(v) | ||||
| 	v = v[:len(v)+len(t)] | ||||
| 	var tIndex int | ||||
| 	for index := startLen; index < cap(v); index++ { | ||||
| 		v[index] = t[tIndex] | ||||
| 		tIndex++ | ||||
| 	} | ||||
| 	return v | ||||
| } | ||||
| 
 | ||||
| func (v Vector[T]) Remove(index int) Vector[T] { | ||||
| 	return From(append(v[:index], v[index+1:]...)) | ||||
| } | ||||
| 
 | ||||
| // Faster remove function that can be used on vectors where ordering doesn't matter
 | ||||
| func (v Vector[T]) RemoveUnordered(index int) Vector[T] { | ||||
| 	v[index] = v[len(v)-1] | ||||
| 	return v[:len(v)-1] | ||||
| } | ||||
| 
 | ||||
| func (v Vector[T]) upsize() Vector[T] { | ||||
| 	return v.upsizeSpecific(cap(v)) | ||||
| } | ||||
| 
 | ||||
| func (v Vector[T]) upsizeSpecific(extraSize int) Vector[T] { | ||||
| 	resized := make([]T, len(v), cap(v)+extraSize) | ||||
| 	copy(resized, v) | ||||
| 	return resized | ||||
| } | ||||
| 
 | ||||
| func (v Vector[T]) panicIndex(index int) { | ||||
| 	min := -1 | ||||
| 	if len(v) != 0 { | ||||
| 		min = 0 | ||||
| 	} | ||||
| 	panic(fmt.Sprintf("index %d out of range [%d;%d)", index, min, len(v))) | ||||
| } | ||||
| 
 | ||||
| // GetSoft tries to get the requested index, or returns a default value if
 | ||||
| // it's out of range
 | ||||
| func (v Vector[T]) GetSoft(index int) T { | ||||
| 	var result T | ||||
| 	if index < 0 || index >= len(v) { | ||||
| 		return result | ||||
| 	} | ||||
| 	return v[index] | ||||
| } | ||||
| 
 | ||||
| func (v Vector[T]) Get(index int) T { | ||||
| 	if index < 0 || index >= len(v) { | ||||
| 		v.panicIndex(index) | ||||
| 	} | ||||
| 	return v[index] | ||||
| } | ||||
| 
 | ||||
| func (v Vector[T]) Set(index int, value T) Vector[T] { | ||||
| 	if index < 0 || index >= len(v) { | ||||
| 		v.panicIndex(index) | ||||
| 	} | ||||
| 	v[index] = value | ||||
| 	return v | ||||
| } | ||||
| 
 | ||||
| func (v Vector[T]) Pop() (T, Vector[T]) { | ||||
| 	t := v[len(v)-1] | ||||
| 	return t, v[:len(v)-1] | ||||
| } | ||||
| 
 | ||||
| func (v Vector[T]) Sub(start, end int) Vector[T] { | ||||
| 	if start >= len(v) { | ||||
| 		v.panicIndex(start) | ||||
| 	} | ||||
| 	if end >= len(v) { | ||||
| 		v.panicIndex(end) | ||||
| 	} | ||||
| 	if start < 0 { | ||||
| 		return From(v[:end]) | ||||
| 	} | ||||
| 	if end < 0 { | ||||
| 		return From(v[start:]) | ||||
| 	} | ||||
| 	return From(v[start:end]) | ||||
| } | ||||
| 
 | ||||
| func (v Vector[T]) Filter(f func(T) bool) Vector[T] { | ||||
| 	return Filter(v, f) | ||||
| } | ||||
| 
 | ||||
| func (v Vector[T]) Any(f func(T) bool) bool { | ||||
| 	return Any(v, f) | ||||
| } | ||||
| 
 | ||||
| func (v Vector[T]) Take(howMany int) Vector[T] { | ||||
| 	return Take(v, howMany) | ||||
| } | ||||
| 
 | ||||
| func (v Vector[T]) Clone() Vector[T] { | ||||
| 	clone := make([]T, len(v)) | ||||
| 	copy(clone, v) | ||||
| 	v = clone | ||||
| 	return v | ||||
| } | ||||
|  | @ -0,0 +1,160 @@ | |||
| package coll_test | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 	"sectorinf.com/emilis/flabk/pkg/coll" | ||||
| ) | ||||
| 
 | ||||
| func TestVector(t *testing.T) { | ||||
| 	tests := map[string]func(that *require.Assertions){ | ||||
| 		"push": func(that *require.Assertions) { | ||||
| 			v := coll.New[int]() | ||||
| 			v = v.Push(0).Push(1).Push(2).Push(3).Push(4).Push(5) | ||||
| 			that.Len(v, 6) | ||||
| 			that.EqualValues([]int{0, 1, 2, 3, 4, 5}, v) | ||||
| 		}, | ||||
| 		"len": func(that *require.Assertions) { | ||||
| 			v := coll.From([]int{1, 2, 3}) | ||||
| 			that.Len(v, len(v)) | ||||
| 		}, | ||||
| 		"from": func(that *require.Assertions) { | ||||
| 			v := coll.From([]int{1, 2, 3}) | ||||
| 			that.Len(v, 3) | ||||
| 		}, | ||||
| 		"pop": func(that *require.Assertions) { | ||||
| 			v := coll.From([]int{1, 2, 3}) | ||||
| 			t, v := v.Pop() | ||||
| 			that.Equal(3, t) | ||||
| 			that.EqualValues([]int{1, 2}, v) | ||||
| 		}, | ||||
| 		"withcap": func(that *require.Assertions) { | ||||
| 			v := coll.WithCap[int](3) | ||||
| 			that.Zero(len(v)) | ||||
| 			that.Len(v, 0) | ||||
| 		}, | ||||
| 		"remove": func(that *require.Assertions) { | ||||
| 			v := coll.New[int]().Push(0).Push(1).Push(2) | ||||
| 			r1 := v.Remove(1) | ||||
| 			that.Equal(2, len(r1)) | ||||
| 			that.EqualValues([]int{0, 2}, r1) | ||||
| 			that.Panics(func() { | ||||
| 				v.Remove(-1) | ||||
| 			}) | ||||
| 			that.Panics(func() { | ||||
| 				v.Remove(100000) | ||||
| 			}) | ||||
| 		}, | ||||
| 		"remove unordered": func(that *require.Assertions) { | ||||
| 			v := coll.New[int]().Push(0).Push(1).Push(2) | ||||
| 			v = v.RemoveUnordered(1) | ||||
| 			that.Equal(2, len(v)) | ||||
| 			vSlice := v | ||||
| 			if vSlice[0] == 0 { | ||||
| 				that.Equal(2, vSlice[1]) | ||||
| 			} else { | ||||
| 				that.Equal(2, vSlice[0]) | ||||
| 				that.Zero(vSlice[1]) | ||||
| 			} | ||||
| 		}, | ||||
| 		"append": func(that *require.Assertions) { | ||||
| 			v := coll.From([]int{1, 2, 3}).Append(4, 5, 6) | ||||
| 			that.Equal(6, len(v)) | ||||
| 			that.EqualValues([]int{1, 2, 3, 4, 5, 6}, v) | ||||
| 		}, | ||||
| 		"getsoft": func(that *require.Assertions) { | ||||
| 			v := coll.From([]int{1, 2, 3}) | ||||
| 			that.Equal(2, v.GetSoft(1)) | ||||
| 			that.Zero(v.GetSoft(-1000)) | ||||
| 			that.Zero(v.GetSoft(1000)) | ||||
| 		}, | ||||
| 		"get": func(that *require.Assertions) { | ||||
| 			v := coll.From([]int{1, 2, 3}) | ||||
| 			that.Equal(2, v.Get(1)) | ||||
| 			that.Panics(func() { | ||||
| 				v.Get(-1000) | ||||
| 			}) | ||||
| 			that.Panics(func() { | ||||
| 				v.Get(1000) | ||||
| 			}) | ||||
| 		}, | ||||
| 		"clone": func(that *require.Assertions) { | ||||
| 			v := coll.From([]int{1, 2, 3}) | ||||
| 
 | ||||
| 			v1 := v.Clone().Set(1, 1) | ||||
| 			that.Equal(3, len(v1)) | ||||
| 			that.EqualValues([]int{1, 1, 3}, v1) | ||||
| 			that.EqualValues([]int{1, 2, 3}, v) | ||||
| 		}, | ||||
| 		"set": func(that *require.Assertions) { | ||||
| 			v := coll.From([]int{1, 2, 3}) | ||||
| 
 | ||||
| 			v1 := v.Clone().Set(1, 1) | ||||
| 			that.Equal(3, len(v1)) | ||||
| 			that.EqualValues([]int{1, 1, 3}, v1) | ||||
| 
 | ||||
| 			v2 := v.Clone().Set(2, 1) | ||||
| 			that.Equal(3, len(v2)) | ||||
| 			that.EqualValues([]int{1, 2, 1}, v2) | ||||
| 
 | ||||
| 			v3 := v.Clone().Set(0, 16) | ||||
| 			that.Equal(3, len(v3)) | ||||
| 			that.EqualValues([]int{16, 2, 3}, v3) | ||||
| 
 | ||||
| 			that.Panics(func() { | ||||
| 				v.Set(-1000, 1) | ||||
| 			}) | ||||
| 			that.Panics(func() { | ||||
| 				v.Set(3, 1) | ||||
| 			}) | ||||
| 		}, | ||||
| 		"sub": func(that *require.Assertions) { | ||||
| 			v := coll.From([]int{0, 1, 2, 3, 4, 5, 6}) | ||||
| 
 | ||||
| 			v1 := v.Clone().Sub(2, 4) | ||||
| 			that.Equal(2, len(v1)) | ||||
| 			that.EqualValues([]int{2, 3}, v1) | ||||
| 
 | ||||
| 			v2 := v.Clone().Sub(2, 5) | ||||
| 			that.Equal(3, len(v2)) | ||||
| 			that.EqualValues([]int{2, 3, 4}, v2) | ||||
| 
 | ||||
| 			v3 := v.Clone().Sub(5, -1) | ||||
| 			that.Equal(2, len(v3)) | ||||
| 			that.EqualValues([]int{5, 6}, v3) | ||||
| 
 | ||||
| 			v4 := v.Clone().Sub(-1, 2) | ||||
| 			that.Equal(2, len(v4)) | ||||
| 			that.EqualValues([]int{0, 1}, v4) | ||||
| 
 | ||||
| 			that.Panics(func() { | ||||
| 				v.Sub(0, 1000) | ||||
| 			}) | ||||
| 			that.Panics(func() { | ||||
| 				v.Sub(4000, 4002) | ||||
| 			}) | ||||
| 			that.Panics(func() { | ||||
| 				v.Sub(2, 1) | ||||
| 			}) | ||||
| 		}, | ||||
| 		"filter": func(that *require.Assertions) { | ||||
| 			v := coll.From([]int{1, 2, 3, 4, 5, 6}) | ||||
| 			v = v.Filter(func(i int) bool { return i%2 == 0 }) | ||||
| 			that.Len(v, 3) | ||||
| 		}, | ||||
| 		"any": func(that *require.Assertions) { | ||||
| 			that.True(coll.From([]int{1, 2, 3, 4, 5, 6}).Any(func(i int) bool { return i == 3 })) | ||||
| 			that.False(coll.From([]int{1, 2, 3, 4, 5, 6}).Any(func(i int) bool { return i == 666 })) | ||||
| 		}, | ||||
| 		"take": func(that *require.Assertions) { | ||||
| 			that.EqualValues([]int{1, 2, 3}, coll.From([]int{1, 2, 3, 4, 5, 6}).Take(3)) | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for name, test := range tests { | ||||
| 		t.Run(name, func(tt *testing.T) { | ||||
| 			test(require.New(tt)) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,34 @@ | |||
| package epk | ||||
| 
 | ||||
| import "github.com/stretchr/testify/require" | ||||
| 
 | ||||
| type ErrorTest func(*require.Assertions, error) | ||||
| 
 | ||||
| var ( | ||||
| 	NoError ErrorTest = func(a *require.Assertions, err error) { | ||||
| 		a.NoError(err) | ||||
| 	} | ||||
| 	Error ErrorTest = func(a *require.Assertions, err error) { | ||||
| 		a.Error(err) | ||||
| 	} | ||||
| ) | ||||
| 
 | ||||
| func ErrorIs(target error, contains ...string) ErrorTest { | ||||
| 	return func(a *require.Assertions, err error) { | ||||
| 		a.Error(err) | ||||
| 		a.ErrorIs(err, target) | ||||
| 		for _, elem := range contains { | ||||
| 			a.Contains(err.Error(), elem) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func NotErrorIs(target error, contains ...string) ErrorTest { | ||||
| 	return func(a *require.Assertions, err error) { | ||||
| 		a.Error(err) | ||||
| 		a.NotErrorIs(err, target) | ||||
| 		for _, elem := range contains { | ||||
| 			a.Contains(err.Error(), elem) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,4 @@ | |||
| // // Package asld handles JSON-LD for asflab
 | ||||
| // //
 | ||||
| // // This will not go well
 | ||||
| package asld | ||||
|  | @ -0,0 +1 @@ | |||
| package asld | ||||
|  | @ -0,0 +1,239 @@ | |||
| package asld_test | ||||
| 
 | ||||
| // import (
 | ||||
| // 	"encoding/json"
 | ||||
| // 	"fmt"
 | ||||
| // 	"math"
 | ||||
| // 	"testing"
 | ||||
| // 	"time"
 | ||||
| 
 | ||||
| // 	"sectorinf.com/emilis/flabk/pkg/asld"
 | ||||
| // 	"sectorinf.com/emilis/flabk/pkg/epk"
 | ||||
| // 	"github.com/stretchr/testify/require"
 | ||||
| // )
 | ||||
| 
 | ||||
| // type testObj struct {
 | ||||
| // 	String  string `asld:"string" json:"string"`
 | ||||
| // 	NoTag   string
 | ||||
| // 	Int     int
 | ||||
| // 	Int8    int8
 | ||||
| // 	Int16   int16
 | ||||
| // 	Int32   int32
 | ||||
| // 	Int64   int64
 | ||||
| // 	Uint    uint
 | ||||
| // 	Uint8   uint8
 | ||||
| // 	Uint16  uint16
 | ||||
| // 	Uint32  uint32
 | ||||
| // 	Uint64  uint64
 | ||||
| // 	Float32 float32
 | ||||
| // 	Float64 float64
 | ||||
| // 	// Complex64     complex64
 | ||||
| // 	// Complex128    complex128
 | ||||
| // 	IntPtr     *int
 | ||||
| // 	Int8Ptr    *int8
 | ||||
| // 	Int16Ptr   *int16
 | ||||
| // 	Int32Ptr   *int32
 | ||||
| // 	Int64Ptr   *int64
 | ||||
| // 	UintPtr    *uint
 | ||||
| // 	Uint8Ptr   *uint8
 | ||||
| // 	Uint16Ptr  *uint16
 | ||||
| // 	Uint32Ptr  *uint32
 | ||||
| // 	Uint64Ptr  *uint64
 | ||||
| // 	Float32Ptr *float32
 | ||||
| // 	Float64Ptr *float64
 | ||||
| // 	// Complex64Ptr  *complex64
 | ||||
| // 	// Complex128Ptr *complex128
 | ||||
| // 	TestPtr   *testObj
 | ||||
| // 	TestArray []testObj
 | ||||
| // }
 | ||||
| 
 | ||||
| // func TestUnmarshal(t *testing.T) {
 | ||||
| // 	tests := map[string]struct {
 | ||||
| // 		obj      testObj
 | ||||
| // 		errCheck epk.ErrorTest
 | ||||
| // 	}{
 | ||||
| // 		"string children": {
 | ||||
| // 			obj: testObj{
 | ||||
| // 				String:  "hello",
 | ||||
| // 				NoTag:   "no_tag",
 | ||||
| // 				Int:     math.MaxInt,
 | ||||
| // 				Int8:    math.MaxInt8,
 | ||||
| // 				Int16:   math.MaxInt16,
 | ||||
| // 				Int32:   math.MaxInt32,
 | ||||
| // 				Int64:   math.MaxInt64,
 | ||||
| // 				Uint:    math.MaxUint,
 | ||||
| // 				Uint8:   math.MaxUint8,
 | ||||
| // 				Uint16:  math.MaxUint16,
 | ||||
| // 				Uint32:  math.MaxUint32,
 | ||||
| // 				Uint64:  math.MaxUint64,
 | ||||
| // 				Float32: math.MaxFloat32,
 | ||||
| // 				Float64: math.MaxFloat64,
 | ||||
| // 				TestPtr: &testObj{
 | ||||
| // 					String: "hello2",
 | ||||
| // 				},
 | ||||
| // 				TestArray: []testObj{
 | ||||
| // 					{
 | ||||
| // 						String: "hello3",
 | ||||
| // 					},
 | ||||
| // 					{
 | ||||
| // 						String: "hello4",
 | ||||
| // 						TestPtr: &testObj{
 | ||||
| // 							TestArray: []testObj{
 | ||||
| // 								{
 | ||||
| // 									String: "hello5",
 | ||||
| // 								},
 | ||||
| // 							},
 | ||||
| // 						},
 | ||||
| // 					},
 | ||||
| // 				},
 | ||||
| // 				// Complex64:  complex(math.MaxFloat32, math.MaxFloat32),
 | ||||
| // 				// Complex128: complex(math.MaxFloat64, math.MaxFloat64),
 | ||||
| // 			},
 | ||||
| // 		},
 | ||||
| // 	}
 | ||||
| 
 | ||||
| // 	for name, test := range tests {
 | ||||
| // 		test := test
 | ||||
| // 		t.Run(name, func(tt *testing.T) {
 | ||||
| // 			that := require.New(tt)
 | ||||
| // 			objJSON, err := json.MarshalIndent(test.obj, "", "  ")
 | ||||
| // 			that.NoError(err)
 | ||||
| // 			result, err := asld.Unmarshal[testObj](objJSON)
 | ||||
| // 			that.NoError(err)
 | ||||
| // 			that.Equal(test.obj, result)
 | ||||
| // 		})
 | ||||
| // 	}
 | ||||
| // }
 | ||||
| 
 | ||||
| // func TestBench(t *testing.T) {
 | ||||
| // 	obj := testObj{
 | ||||
| // 		String:  "hello",
 | ||||
| // 		NoTag:   "no_tag",
 | ||||
| // 		Int:     math.MaxInt,
 | ||||
| // 		Int8:    math.MaxInt8,
 | ||||
| // 		Int16:   math.MaxInt16,
 | ||||
| // 		Int32:   math.MaxInt32,
 | ||||
| // 		Int64:   math.MaxInt64,
 | ||||
| // 		Uint:    math.MaxUint,
 | ||||
| // 		Uint8:   math.MaxUint8,
 | ||||
| // 		Uint16:  math.MaxUint16,
 | ||||
| // 		Uint32:  math.MaxUint32,
 | ||||
| // 		Uint64:  math.MaxUint64,
 | ||||
| // 		Float32: math.MaxFloat32,
 | ||||
| // 		Float64: math.MaxFloat64,
 | ||||
| // 		// Complex64:  complex(math.MaxFloat32, math.MaxFloat32),
 | ||||
| // 		// Complex128: complex(math.MaxFloat64, math.MaxFloat64),
 | ||||
| // 	}
 | ||||
| // 	that := require.New(t)
 | ||||
| // 	asldTotal := int64(0)
 | ||||
| // 	jsonTotal := int64(0)
 | ||||
| 
 | ||||
| // 	jsonMax := int64(0)
 | ||||
| // 	jsonMin := math.MaxInt64
 | ||||
| 
 | ||||
| // 	asldMax := int64(0)
 | ||||
| // 	asldMin := math.MaxInt64
 | ||||
| 
 | ||||
| // 	count := int64(1 << 20)
 | ||||
| // 	for index := int64(0); index < count; index++ {
 | ||||
| // 		objJSON, err := json.Marshal(obj)
 | ||||
| // 		that.NoError(err)
 | ||||
| 
 | ||||
| // 		asldStart := time.Now()
 | ||||
| // 		_, err = asld.Unmarshal[testObj](objJSON)
 | ||||
| // 		asldDur := time.Since(asldStart)
 | ||||
| // 		asldTotal += int64(asldDur)
 | ||||
| // 		if asldDur < time.Duration(asldMin) {
 | ||||
| // 			asldMin = int(asldDur)
 | ||||
| // 		}
 | ||||
| // 		if asldDur > time.Duration(asldMax) {
 | ||||
| // 			asldMax = int64(asldDur)
 | ||||
| // 		}
 | ||||
| 
 | ||||
| // 		that.NoError(err)
 | ||||
| // 		a := testObj{}
 | ||||
| 
 | ||||
| // 		jsonStart := time.Now()
 | ||||
| // 		err = json.Unmarshal(objJSON, &a)
 | ||||
| // 		jsonDur := time.Since(jsonStart)
 | ||||
| // 		jsonTotal += int64(jsonDur)
 | ||||
| 
 | ||||
| // 		if jsonDur < time.Duration(jsonMin) {
 | ||||
| // 			jsonMin = int(jsonDur)
 | ||||
| // 		}
 | ||||
| // 		if jsonDur > time.Duration(jsonMax) {
 | ||||
| // 			jsonMax = int64(jsonDur)
 | ||||
| // 		}
 | ||||
| 
 | ||||
| // 		that.NoError(err)
 | ||||
| // 	}
 | ||||
| // 	fmt.Println(count, "runs")
 | ||||
| // 	fmt.Printf("json avg (%s), min (%s), max (%s)\n", time.Duration(jsonTotal/count), time.Duration(jsonMin), time.Duration(jsonMax))
 | ||||
| // 	fmt.Printf("asld avg (%s), min (%s), max (%s)\n", time.Duration(asldTotal/count), time.Duration(asldMin), time.Duration(asldMax))
 | ||||
| // }
 | ||||
| // func jsonElemList[T any](that *require.Assertions, a any) [][]byte {
 | ||||
| // 	that.NotNil(a)
 | ||||
| // 	v, ok := a.([]T)
 | ||||
| // 	that.True(ok)
 | ||||
| // 	fields := make([][]byte, len(v))
 | ||||
| // 	for index, field := range v {
 | ||||
| // 		jsonField, err := json.Marshal(field)
 | ||||
| // 		that.NoError(err)
 | ||||
| // 		fields[index] = jsonField
 | ||||
| // 	}
 | ||||
| // 	return fields
 | ||||
| // }
 | ||||
| 
 | ||||
| // type hello struct {
 | ||||
| // 	Hello string
 | ||||
| // }
 | ||||
| 
 | ||||
| // func TestArrayMembers(t *testing.T) {
 | ||||
| // 	tests := map[string]struct {
 | ||||
| // 		object            any
 | ||||
| // 		errCheck          epk.ErrorTest
 | ||||
| // 		toJsonElementList func(*require.Assertions, any) [][]byte
 | ||||
| // 	}{
 | ||||
| // 		"strings": {
 | ||||
| // 			object:            []string{"a", "b", "c", "d"},
 | ||||
| // 			toJsonElementList: jsonElemList[string],
 | ||||
| // 		},
 | ||||
| // 		"ints": {
 | ||||
| // 			object:            []int{1, 2, 3, 4, 5, 6},
 | ||||
| // 			toJsonElementList: jsonElemList[int],
 | ||||
| // 		},
 | ||||
| // 		"floats": {
 | ||||
| // 			object:            []float64{123.456, 789.0123, 456.789},
 | ||||
| // 			toJsonElementList: jsonElemList[float64],
 | ||||
| // 		},
 | ||||
| // 		"strings with commas": {
 | ||||
| // 			object:            []string{"this, is what I do", "what, don't like it?"},
 | ||||
| // 			toJsonElementList: jsonElemList[string],
 | ||||
| // 		},
 | ||||
| // 		"objects": {
 | ||||
| // 			object:            []hello{{"world"}, {"mom, dad"}},
 | ||||
| // 			toJsonElementList: jsonElemList[hello],
 | ||||
| // 		},
 | ||||
| // 	}
 | ||||
| 
 | ||||
| // 	for name, test := range tests {
 | ||||
| // 		test := test
 | ||||
| // 		t.Run(name, func(tt *testing.T) {
 | ||||
| // 			that := require.New(tt)
 | ||||
| // 			objJson, err := json.Marshal(test.object)
 | ||||
| // 			that.NoError(err)
 | ||||
| // 			w := newWalker(objJson)
 | ||||
| // 			members, err := arrayMembers(w)
 | ||||
| // 			if test.errCheck == nil {
 | ||||
| // 				test.errCheck = epk.NoError
 | ||||
| // 			}
 | ||||
| // 			test.errCheck(that, err)
 | ||||
| // 			expected := test.toJsonElementList(that, test.object)
 | ||||
| // 			that.Len(members, len(expected))
 | ||||
| // 			for index, elem := range expected {
 | ||||
| // 				equivMember := members[index]
 | ||||
| // 				that.Equal(elem, equivMember.content)
 | ||||
| // 			}
 | ||||
| // 		})
 | ||||
| // 	}
 | ||||
| // }
 | ||||
|  | @ -1,47 +0,0 @@ | |||
| use std::sync::Arc; | ||||
| 
 | ||||
| use super::users::Users; | ||||
| use tokio::sync::Mutex; | ||||
| use tokio_postgres::{tls::NoTlsStream, Client, Connection, NoTls, Socket}; | ||||
| 
 | ||||
| const DBERR_UNIQUE: &str = "23505"; | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct DB { | ||||
|     client: Arc<Mutex<Client>>, | ||||
| } | ||||
| 
 | ||||
| impl DB { | ||||
|     pub async fn new(host: String, user: String, database: String) -> Result<Self, anyhow::Error> { | ||||
|         let (cl, conn) = tokio_postgres::connect( | ||||
|             format!("host={host} user={user} dbname={database}").as_str(), | ||||
|             NoTls, | ||||
|         ) | ||||
|         .await?; | ||||
|         tokio::spawn(async move { | ||||
|             if let Err(e) = conn.await { | ||||
|                 eprintln!("connection error: {}", e); | ||||
|             } | ||||
|         }); | ||||
|         let client = Arc::new(Mutex::new(cl)); | ||||
|         Ok(Self { client }) | ||||
|     } | ||||
| 
 | ||||
|     pub fn users(&self) -> Users { | ||||
|         Users::new(self.client.clone()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub enum DBError { | ||||
|     Duplicate, | ||||
|     Other(tokio_postgres::Error), | ||||
| } | ||||
| 
 | ||||
| impl From<tokio_postgres::Error> for DBError { | ||||
|     fn from(err: tokio_postgres::Error) -> Self { | ||||
|         if let Some(code) = err.code() && code.code() == DBERR_UNIQUE { | ||||
| 			return DBError::Duplicate; | ||||
| 		} | ||||
|         DBError::Other(err) | ||||
|     } | ||||
| } | ||||
|  | @ -1,2 +0,0 @@ | |||
| pub mod db; | ||||
| pub mod users; | ||||
|  | @ -1,95 +0,0 @@ | |||
| use std::sync::Arc; | ||||
| 
 | ||||
| use rand::Rng; | ||||
| use tokio::sync::Mutex; | ||||
| use tokio_postgres::{Client, Row}; | ||||
| 
 | ||||
| use super::db; | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct Users(Arc<Mutex<Client>>); | ||||
| 
 | ||||
| impl Users { | ||||
|     pub fn new(client: Arc<Mutex<Client>>) -> Self { | ||||
|         Self(client) | ||||
|     } | ||||
| 
 | ||||
|     fn new_id() -> String { | ||||
|         let bytes = rand::thread_rng().gen::<[u8; 16]>(); | ||||
|         base_62::encode(&bytes) | ||||
|     } | ||||
| 
 | ||||
|     pub async fn create_user(&self, u: User) -> Result<User, db::DBError> { | ||||
|         let row = self.0.lock().await.query_one( | ||||
|             "insert into users (id, username, host, display_name) values ($1, $2, $3, $4) returning id", | ||||
|             &[&Self::new_id(), &u.username, &u.host, &u.display_name], | ||||
|         ).await?; | ||||
|         Ok(User { | ||||
|             id: row.get("id"), | ||||
|             username: u.username, | ||||
|             host: u.host, | ||||
|             display_name: u.display_name, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     pub async fn user(&self, by: UserSelect) -> Result<Option<User>, anyhow::Error> { | ||||
|         let where_param: String; | ||||
|         let where_clause = match by { | ||||
|             UserSelect::ID(id) => { | ||||
|                 where_param = id; | ||||
|                 "id = $1" | ||||
|             } | ||||
|             UserSelect::Username(username) => { | ||||
|                 where_param = username; | ||||
|                 "username = $1" | ||||
|             } | ||||
|             UserSelect::FullUsername(full) => { | ||||
|                 where_param = full; | ||||
|                 "(username || '@' || host) = $1" | ||||
|             } | ||||
|         }; | ||||
|         let rows = self | ||||
|             .0 | ||||
|             .lock() | ||||
|             .await | ||||
|             .query( | ||||
|                 format!( | ||||
|                     "select id, username, host, display_name from users where {}", | ||||
|                     where_clause | ||||
|                 ) | ||||
|                 .as_str(), | ||||
|                 &[&where_param], | ||||
|             ) | ||||
|             .await?; | ||||
| 
 | ||||
|         if let Some(row) = rows.first() && rows.len() == 1 { | ||||
| 			Ok(Some(User::from(row))) | ||||
| 		} else { | ||||
| 			Ok(None) | ||||
| 		} | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub struct User { | ||||
|     pub id: String, | ||||
|     pub username: String, | ||||
|     pub host: Option<String>, | ||||
|     pub display_name: Option<String>, | ||||
| } | ||||
| 
 | ||||
| impl From<&Row> for User { | ||||
|     fn from(row: &Row) -> Self { | ||||
|         Self { | ||||
|             id: row.get("id"), | ||||
|             username: row.get("username"), | ||||
|             host: row.get("host"), | ||||
|             display_name: row.get("display_name"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub enum UserSelect { | ||||
|     ID(String), | ||||
|     Username(String), | ||||
|     FullUsername(String), | ||||
| } | ||||
							
								
								
									
										48
									
								
								src/main.rs
								
								
								
								
							
							
						
						
									
										48
									
								
								src/main.rs
								
								
								
								
							|  | @ -1,48 +0,0 @@ | |||
| mod database; | ||||
| mod model; | ||||
| mod servek; | ||||
| mod svc; | ||||
| 
 | ||||
| use database::db::DB; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use servek::servek::Server; | ||||
| use svc::profiles::Profiler; | ||||
| 
 | ||||
| #[derive(Clone, Copy, Serialize, Deserialize)] | ||||
| pub enum ActivityKind { | ||||
|     Create, | ||||
|     Like, | ||||
|     Note, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Serialize, Deserialize)] | ||||
| pub struct ActivityLD { | ||||
|     pub context_uri: String, | ||||
|     pub kind: ActivityKind, | ||||
|     pub actor_uri: String, | ||||
|     pub to_uris: Vec<String>, | ||||
|     pub object: Option<ObjectLD>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Serialize, Deserialize)] | ||||
| pub struct ObjectLD { | ||||
|     pub context_uri: String, | ||||
|     pub id: Option<String>, | ||||
|     pub kind: Option<ActivityKind>, | ||||
|     pub attributed_to: Option<String>, | ||||
|     pub published: Option<String>, | ||||
|     pub content: Option<String>, | ||||
| } | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() -> Result<(), anyhow::Error> { | ||||
|     let db = DB::new( | ||||
|         "localhost".to_owned(), | ||||
|         "flabk".to_owned(), | ||||
|         "flabk".to_owned(), | ||||
|     ) | ||||
|     .await?; | ||||
|     let profiler = Profiler::new(db.users()); | ||||
|     Server::new(profiler).listen_and_serve(8008).await; | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										12
									
								
								src/model.rs
								
								
								
								
							
							
						
						
									
										12
									
								
								src/model.rs
								
								
								
								
							|  | @ -1,12 +0,0 @@ | |||
| use serde::Serialize; | ||||
| 
 | ||||
| #[derive(Clone, Serialize)] | ||||
| pub struct Error<'a> { | ||||
|     pub error: &'a str, | ||||
| } | ||||
| 
 | ||||
| impl<'a> Error<'a> { | ||||
|     pub fn error(error: &'a str) -> Self { | ||||
|         Self { error } | ||||
|     } | ||||
| } | ||||
|  | @ -1,91 +0,0 @@ | |||
| use std::str::FromStr; | ||||
| 
 | ||||
| use warp::{http::HeaderValue, hyper::Uri, path::Tail, reply::Response, Filter, Rejection, Reply}; | ||||
| 
 | ||||
| use super::servek::{Server, ServerError}; | ||||
| use rust_embed::RustEmbed; | ||||
| 
 | ||||
| #[derive(RustEmbed)] | ||||
| #[folder = "static"] | ||||
| struct StaticData; | ||||
| 
 | ||||
| impl Server { | ||||
|     pub(super) async fn html( | ||||
|         &self, | ||||
|     ) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { | ||||
|         Self::index() | ||||
|             .or(self.profile().await) | ||||
|             .or(self.create_profile().await.or(Server::static_files())) | ||||
|     } | ||||
| 
 | ||||
|     fn index() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { | ||||
|         warp::get().and(warp::path::end().map(move || { | ||||
|             warp::reply::html(include_str!("../../templates/html/index.html").to_owned()) | ||||
|         })) | ||||
|     } | ||||
| 
 | ||||
|     fn with_server( | ||||
|         srv: Server, | ||||
|     ) -> impl Filter<Extract = (Server,), Error = std::convert::Infallible> + Clone { | ||||
|         warp::any().map(move || srv.clone()) | ||||
|     } | ||||
| 
 | ||||
|     async fn profile(&self) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | ||||
|         warp::get().and( | ||||
|             warp::path!("@" / String) | ||||
|                 .and(Self::with_server(self.clone())) | ||||
|                 .and_then(|username: String, srv: Server| async move { | ||||
|                     srv.hb | ||||
|                         .render( | ||||
|                             "profile", | ||||
|                             &serde_json::json!(srv | ||||
|                                 .profiler | ||||
|                                 .profile(username) | ||||
|                                 .await | ||||
|                                 .map_err(|e| ServerError::from(e))?), | ||||
|                         ) | ||||
|                         .map(|html| warp::reply::html(html)) | ||||
|                         .map_err(|e| ServerError::from(e).reject_self()) | ||||
|                 }), | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     async fn create_profile(&self) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | ||||
|         warp::post().and( | ||||
|             warp::path!("@" / String) | ||||
|                 .and(Self::with_server(self.clone())) | ||||
|                 .and_then(|username: String, srv: Server| async move { | ||||
|                     let user = srv | ||||
|                         .profiler | ||||
|                         .create_user(username, None) | ||||
|                         .await | ||||
|                         .map_err(|e| ServerError::from(e)); | ||||
|                     match user { | ||||
|                         Ok(u) => Ok(warp::redirect( | ||||
|                             Uri::from_str(format!("/@/{}", u.username).as_str()).unwrap(), | ||||
|                         )), | ||||
|                         Err(e) => Err(e.reject_self()), | ||||
|                     } | ||||
|                 }), | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     fn static_files() -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | ||||
|         warp::get().and(warp::path("static").and(warp::path::tail()).and_then( | ||||
|             |path: Tail| async move { | ||||
|                 let asset = match StaticData::get(path.as_str()) { | ||||
|                     Some(a) => a, | ||||
|                     None => return Err(ServerError::NotFound.reject_self()), | ||||
|                 }; | ||||
|                 let mime = mime_guess::from_path(path.as_str()).first_or_octet_stream(); | ||||
| 
 | ||||
|                 let mut res = Response::new(asset.data.into()); | ||||
|                 res.headers_mut().insert( | ||||
|                     "Content-Type", | ||||
|                     HeaderValue::from_str(mime.as_ref()).unwrap(), | ||||
|                 ); | ||||
|                 Ok(res) | ||||
|             }, | ||||
|         )) | ||||
|     } | ||||
| } | ||||
|  | @ -1,2 +0,0 @@ | |||
| mod html; | ||||
| pub mod servek; | ||||
|  | @ -1,116 +0,0 @@ | |||
| use core::panic; | ||||
| use std::{convert::Infallible, fmt::Display}; | ||||
| 
 | ||||
| use handlebars::{Handlebars, RenderError}; | ||||
| use warp::{ | ||||
|     hyper::StatusCode, | ||||
|     reject::{MethodNotAllowed, Reject}, | ||||
|     Filter, Rejection, Reply, | ||||
| }; | ||||
| 
 | ||||
| use crate::{ | ||||
|     database::users::User, | ||||
|     model, | ||||
|     svc::profiles::{Profiler, UserError}, | ||||
| }; | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct Server { | ||||
|     pub(super) hb: Handlebars<'static>, | ||||
|     pub(super) profiler: Profiler, | ||||
| } | ||||
| 
 | ||||
| impl Server { | ||||
|     pub fn new(profiler: Profiler) -> Self { | ||||
|         let mut hb = Handlebars::new(); | ||||
|         hb.register_template_string("profile", include_str!("../../templates/html/profile.html")) | ||||
|             .expect("profile template"); | ||||
| 
 | ||||
|         Self { hb, profiler } | ||||
|     } | ||||
| 
 | ||||
|     pub async fn listen_and_serve(self, port: u16) -> ! { | ||||
|         println!("starting server on port {}", port); | ||||
|         warp::serve(self.html().await.recover(Self::handle_rejection)) | ||||
|             .run(([127, 0, 0, 1], port)) | ||||
|             .await; | ||||
|         panic!("server stopped prematurely") | ||||
|     } | ||||
| 
 | ||||
|     async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> { | ||||
|         let code; | ||||
|         let message; | ||||
| 
 | ||||
|         if err.is_not_found() { | ||||
|             code = StatusCode::NOT_FOUND; | ||||
|             message = "not found"; | ||||
|         } else if let Some(err) = err.find::<ServerError>() { | ||||
|             match err { | ||||
|                 ServerError::Internal(err) => { | ||||
|                     println!("internal server error: {}", err); | ||||
|                     code = StatusCode::INTERNAL_SERVER_ERROR; | ||||
|                     message = "internal server error"; | ||||
|                 } | ||||
|                 ServerError::NotFound => { | ||||
|                     code = StatusCode::NOT_FOUND; | ||||
|                     message = "not found"; | ||||
|                 } | ||||
|                 ServerError::Duplicate => { | ||||
|                     code = StatusCode::BAD_REQUEST; | ||||
|                     message = "duplicate entry exists"; | ||||
|                 } | ||||
|             } | ||||
|         } else if let Some(err) = err.find::<MethodNotAllowed>() { | ||||
|             println!("MethodNotAllowed: {:#?}", err); | ||||
|             code = StatusCode::NOT_FOUND; | ||||
|             message = "not found"; | ||||
|         } else { | ||||
|             // We should have expected this... Just log and say its a 500
 | ||||
|             println!("FIXME: unhandled rejection: {:?}", err); | ||||
|             code = StatusCode::INTERNAL_SERVER_ERROR; | ||||
|             message = "internal server error" | ||||
|         } | ||||
| 
 | ||||
|         Ok(warp::reply::with_status( | ||||
|             warp::reply::json(&model::Error::error(message)), | ||||
|             code, | ||||
|         )) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Debug)] | ||||
| pub(super) enum ServerError { | ||||
|     Internal(String), | ||||
|     NotFound, | ||||
|     Duplicate, | ||||
| } | ||||
| 
 | ||||
| impl ServerError { | ||||
|     pub(super) fn reject_self(self) -> Rejection { | ||||
|         warp::reject::custom(self) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Reject for ServerError {} | ||||
| 
 | ||||
| impl From<RenderError> for ServerError { | ||||
|     fn from(r: RenderError) -> Self { | ||||
|         Self::Internal(r.to_string()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<UserError> for ServerError { | ||||
|     fn from(u: UserError) -> Self { | ||||
|         match u { | ||||
|             UserError::Duplicate => Self::Duplicate, | ||||
|             UserError::NotFound => Self::NotFound, | ||||
|             UserError::Other(o) => Self::Internal(format!("UserError: {}", o)), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Display for ServerError { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "{}", self) | ||||
|     } | ||||
| } | ||||
|  | @ -1 +0,0 @@ | |||
| pub mod profiles; | ||||
|  | @ -1,115 +0,0 @@ | |||
| use serde::Serialize; | ||||
| use warp::{reject::Reject, Rejection}; | ||||
| 
 | ||||
| use crate::database::{ | ||||
|     db, | ||||
|     users::{self, UserSelect, Users}, | ||||
| }; | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct Profiler { | ||||
|     db: Users, | ||||
| } | ||||
| 
 | ||||
| impl Profiler { | ||||
|     pub fn new(db: Users) -> Self { | ||||
|         Self { db } | ||||
|     } | ||||
| 
 | ||||
|     pub async fn profile(&self, username: String) -> Result<User, UserError> { | ||||
|         let select = if username.contains("@") { | ||||
|             UserSelect::FullUsername(username) | ||||
|         } else { | ||||
|             UserSelect::Username(username) | ||||
|         }; | ||||
|         match self.db.user(select).await? { | ||||
|             Some(user) => Ok(User::from(user)), | ||||
|             None => Err(UserError::NotFound), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub async fn create_user( | ||||
|         &self, | ||||
|         username: String, | ||||
|         display_name: Option<String>, | ||||
|     ) -> Result<User, UserError> { | ||||
|         let result = self | ||||
|             .db | ||||
|             .create_user( | ||||
|                 User { | ||||
|                     id: String::new(), | ||||
|                     username, | ||||
|                     display_name, | ||||
|                     host: None, | ||||
|                 } | ||||
|                 .into(), | ||||
|             ) | ||||
|             .await?; | ||||
| 
 | ||||
|         Ok(User::from(result)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Debug, Serialize)] | ||||
| pub struct User { | ||||
|     pub id: String, | ||||
|     pub username: String, | ||||
|     pub display_name: Option<String>, | ||||
|     pub host: Option<String>, | ||||
| } | ||||
| 
 | ||||
| impl From<users::User> for User { | ||||
|     fn from(u: users::User) -> Self { | ||||
|         Self { | ||||
|             id: u.id, | ||||
|             username: u.username, | ||||
|             display_name: u.display_name, | ||||
|             host: u.host, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Into<users::User> for User { | ||||
|     fn into(self) -> users::User { | ||||
|         users::User { | ||||
|             id: self.id, | ||||
|             username: self.username, | ||||
|             display_name: self.display_name, | ||||
|             host: self.host, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub enum UserError { | ||||
|     Duplicate, | ||||
|     NotFound, | ||||
|     Other(String), | ||||
| } | ||||
| 
 | ||||
| impl From<anyhow::Error> for UserError { | ||||
|     fn from(err: anyhow::Error) -> Self { | ||||
|         Self::Other(format!("UserError: {}", err)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<db::DBError> for UserError { | ||||
|     fn from(err: db::DBError) -> Self { | ||||
|         match err { | ||||
|             db::DBError::Duplicate => Self::Duplicate, | ||||
|             db::DBError::Other(e) => Self::Other(e.to_string()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl ToString for UserError { | ||||
|     fn to_string(&self) -> String { | ||||
|         match self { | ||||
|             Self::Duplicate => String::from("duplicate insert"), | ||||
|             Self::NotFound => String::from("not found"), | ||||
|             Self::Other(err) => err.clone(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Reject for UserError {} | ||||
|  | @ -1,8 +0,0 @@ | |||
| body { | ||||
| 	background-color: black; | ||||
| 	color: rebeccapurple; | ||||
| } | ||||
| 
 | ||||
| h1 { | ||||
| 	text-align: center; | ||||
| } | ||||
|  | @ -1,12 +0,0 @@ | |||
| <!DOCTYPE html> | ||||
| <html> | ||||
| 
 | ||||
| <head> | ||||
| 	<title>flabk - not found</title> | ||||
| </head> | ||||
| 
 | ||||
| <body> | ||||
| 	<h1>not found</h1> | ||||
| </body> | ||||
| 
 | ||||
| </html> | ||||
|  | @ -1,13 +0,0 @@ | |||
| <!DOCTYPE html> | ||||
| <html> | ||||
| 
 | ||||
| <head> | ||||
| 	<title>flabk</title> | ||||
| 	<link rel="stylesheet" href="/static/style/main.css"> | ||||
| </head> | ||||
| 
 | ||||
| <body> | ||||
| 	<h1>hi</h1> | ||||
| </body> | ||||
| 
 | ||||
| </html> | ||||
|  | @ -1,13 +0,0 @@ | |||
| <!DOCTYPE html> | ||||
| <html> | ||||
| 
 | ||||
| <head> | ||||
| 	<title>@{{username}}</title> | ||||
| 	<link rel="stylesheet" href="/static/style/main.css"> | ||||
| </head> | ||||
| 
 | ||||
| <body> | ||||
| 	<h1>hi {{username}}, your id is {{id}}</h1> | ||||
| </body> | ||||
| 
 | ||||
| </html> | ||||
|  | @ -0,0 +1,13 @@ | |||
| package uriutil | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| func JoinURIs(base string, parts ...string) string { | ||||
| 	if len(base) < 8 || len(base) > 8 && base[:8] != "https://" { | ||||
| 		base = "https://" + base | ||||
| 	} | ||||
| 	return fmt.Sprintf("%s/%s", strings.TrimRight(base, "/"), strings.Join(parts, "/")) | ||||
| } | ||||
		Loading…
	
		Reference in New Issue