diff --git a/ActivityPub/models.go b/ActivityPub/models.go index c95aa93..6b7e283 100644 --- a/ActivityPub/models.go +++ b/ActivityPub/models.go @@ -205,3 +205,79 @@ func (resource *WebfingerResource) GenerateFromActor(hostname *url.URL, actor *A }, } } + +// NodeinfoResources : Nodeinfo Resources. +type NodeinfoResources struct { + NodeinfoLinks NodeinfoLinks + Nodeinfo Nodeinfo +} + +// NodeinfoLinks : Nodeinfo Link Resource. +type NodeinfoLinks struct { + Links []NodeinfoLink `json:"links"` +} + +// NodeinfoLink : Nodeinfo Link Resource. +type NodeinfoLink struct { + Rel string `json:"rel"` + Href string `json:"href"` +} + +// Nodeinfo : Nodeinfo Resource. +type Nodeinfo struct { + Version string `json:"version"` + Software NodeinfoSoftware `json:"software"` + Protocols []string `json:"protocols"` + Services NodeinfoServices `json:"services"` + OpenRegistrations bool `json:"openRegistrations"` + Usage NodeinfoUsage `json:"usage"` + Metadata NodeinfoMetadata `json:"metadata"` +} + +// NodeinfoSoftware : NodeinfoSoftware Resource. +type NodeinfoSoftware struct { + Name string `json:"name"` + Version string `json:"version"` + Repository string `json:"repository,omitempty"` +} + +// NodeinfoServices : NodeinfoSoftware Resource. +type NodeinfoServices struct { + Inbound []string `json:"inbound"` + Outbound []string `json:"outbound"` +} + +// NodeinfoUsage : NodeinfoUsage Resource. +type NodeinfoUsage struct { + Users NodeinfoUsageUsers `json:"users"` +} + +// NodeinfoUsageUsers : NodeinfoUsageUsers Resource. +type NodeinfoUsageUsers struct { + Total int `json:"total"` + ActiveMonth int `json:"activeMonth"` + ActiveHalfyear int `json:"activeHalfyear"` +} + +// NodeinfoMetadata : NodeinfoMetadata Resource. +type NodeinfoMetadata struct { +} + +// GenerateFromActor : Generate Webfinger resource from Actor. +func (resource *NodeinfoResources) GenerateFromActor(hostname *url.URL, actor *Actor, serverVersion string) { + resource.NodeinfoLinks.Links = []NodeinfoLink{ + NodeinfoLink{ + "http://nodeinfo.diaspora.software/ns/schema/2.1", + "https://" + hostname.Host + "/nodeinfo/2.1", + }, + } + resource.Nodeinfo = Nodeinfo{ + "2.1", + NodeinfoSoftware{"activity-relay", serverVersion, "https://github.com/yukimochi/Activity-Relay"}, + []string{"activitypub"}, + NodeinfoServices{[]string{}, []string{}}, + true, + NodeinfoUsage{NodeinfoUsageUsers{0, 0, 0}}, + NodeinfoMetadata{}, + } +} diff --git a/cli/config_test.go b/cli/config_test.go index 13d587e..47daae6 100644 --- a/cli/config_test.go +++ b/cli/config_test.go @@ -69,7 +69,7 @@ func TestInvalidConfig(t *testing.T) { output := buffer.String() if strings.Split(output, "\n")[0] != "Invalid config given" { - t.Fatalf("Invalid Responce.") + t.Fatalf("Invalid Response.") } } @@ -86,15 +86,15 @@ func TestListConfig(t *testing.T) { switch strings.Split(row, ":")[0] { case "Blocking for service-type actor ": if strings.Split(row, ":")[1] == " true" { - t.Fatalf("Invalid Responce.") + t.Fatalf("Invalid Response.") } case "Manually accept follow-request ": if strings.Split(row, ":")[1] == " true" { - t.Fatalf("Invalid Responce.") + t.Fatalf("Invalid Response.") } case "Announce activity instead of relay create activity ": if strings.Split(row, ":")[1] == " true" { - t.Fatalf("Invalid Responce.") + t.Fatalf("Invalid Response.") } } } @@ -115,7 +115,7 @@ func TestExportConfig(t *testing.T) { jsonData, err := ioutil.ReadAll(file) output := buffer.String() if strings.Split(output, "\n")[0] != string(jsonData) { - t.Fatalf("Invalid Responce.") + t.Fatalf("Invalid Response.") } } @@ -138,7 +138,7 @@ func TestImportConfig(t *testing.T) { jsonData, err := ioutil.ReadAll(file) output := buffer.String() if strings.Split(output, "\n")[0] != string(jsonData) { - t.Fatalf("Invalid Responce.") + t.Fatalf("Invalid Response.") } relayState.RedisClient.FlushAll().Result() diff --git a/cli/domain_test.go b/cli/domain_test.go index a23d046..c432065 100644 --- a/cli/domain_test.go +++ b/cli/domain_test.go @@ -24,7 +24,7 @@ subscription.example.jp Total : 1 ` if output != valid { - t.Fatalf("Invalid Responce.") + t.Fatalf("Invalid Response.") } relayState.RedisClient.FlushAll().Result() @@ -49,7 +49,7 @@ limitedDomain.example.jp Total : 1 ` if output != valid { - t.Fatalf("Invalid Responce.") + t.Fatalf("Invalid Response.") } relayState.RedisClient.FlushAll().Result() @@ -74,7 +74,7 @@ blockedDomain.example.jp Total : 1 ` if output != valid { - t.Fatalf("Invalid Responce.") + t.Fatalf("Invalid Response.") } relayState.RedisClient.FlushAll().Result() @@ -185,7 +185,7 @@ func TestSetDomainInvalid(t *testing.T) { output := buffer.String() if strings.Split(output, "\n")[0] != "Invalid type given" { - t.Fatalf("Invalid Responce.") + t.Fatalf("Invalid Response.") } relayState.RedisClient.FlushAll().Result() @@ -230,7 +230,7 @@ func TestInvalidUnfollowDomain(t *testing.T) { output := buffer.String() if strings.Split(output, "\n")[0] != "Invalid domain [unknown.tld] given" { - t.Fatalf("Invalid Responce.") + t.Fatalf("Invalid Response.") } relayState.RedisClient.FlushAll().Result() diff --git a/cli/follow_test.go b/cli/follow_test.go index 46788a3..b30b1dd 100644 --- a/cli/follow_test.go +++ b/cli/follow_test.go @@ -29,7 +29,7 @@ example.com Total : 1 ` if output != valid { - t.Fatalf("Invalid Responce.") + t.Fatalf("Invalid Response.") } relayState.RedisClient.FlushAll().Result() @@ -103,7 +103,7 @@ func TestInvalidFollow(t *testing.T) { output := buffer.String() if strings.Split(output, "\n")[0] != "Invalid domain [unknown.tld] given" { - t.Fatalf("Invalid Responce.") + t.Fatalf("Invalid Response.") } relayState.RedisClient.FlushAll().Result() @@ -121,7 +121,7 @@ func TestInvalidRejectFollow(t *testing.T) { output := buffer.String() if strings.Split(output, "\n")[0] != "Invalid domain [unknown.tld] given" { - t.Fatalf("Invalid Responce.") + t.Fatalf("Invalid Response.") } relayState.RedisClient.FlushAll().Result() diff --git a/handle.go b/handle.go index 2a2558e..3a934b9 100644 --- a/handle.go +++ b/handle.go @@ -35,6 +35,40 @@ func handleWebfinger(writer http.ResponseWriter, request *http.Request) { } } +func handleNodeinfoLink(writer http.ResponseWriter, request *http.Request) { + if request.Method != "GET" { + writer.WriteHeader(400) + writer.Write(nil) + } else { + linksresource, err := json.Marshal(&Nodeinfo.NodeinfoLinks) + if err != nil { + panic(err) + } + writer.Header().Add("Content-Type", "application/json") + writer.WriteHeader(200) + writer.Write(linksresource) + } +} + +func handleNodeinfo(writer http.ResponseWriter, request *http.Request) { + if request.Method != "GET" { + writer.WriteHeader(400) + writer.Write(nil) + } else { + userCount := len(relayState.Subscriptions) + Nodeinfo.Nodeinfo.Usage.Users.Total = userCount + Nodeinfo.Nodeinfo.Usage.Users.ActiveMonth = userCount + Nodeinfo.Nodeinfo.Usage.Users.ActiveHalfyear = userCount + linksresource, err := json.Marshal(&Nodeinfo.Nodeinfo) + if err != nil { + panic(err) + } + writer.Header().Add("Content-Type", "application/json") + writer.WriteHeader(200) + writer.Write(linksresource) + } +} + func handleActor(writer http.ResponseWriter, request *http.Request) { if request.Method == "GET" { actor, err := json.Marshal(&Actor) diff --git a/handle_test.go b/handle_test.go index b34164f..6171309 100644 --- a/handle_test.go +++ b/handle_test.go @@ -46,7 +46,7 @@ func TestHandleWebfingerGet(t *testing.T) { var wfresource activitypub.WebfingerResource err = json.Unmarshal(data, &wfresource) if err != nil { - t.Fatalf("WebfingerResource responce is not valid.") + t.Fatalf("WebfingerResource response is not valid.") } domain, _ := url.Parse(wfresource.Links[0].Href) @@ -73,6 +73,88 @@ func TestHandleWebfingerGetBadResource(t *testing.T) { } } +func TestHandleNodeinfoLinkGet(t *testing.T) { + s := httptest.NewServer(http.HandlerFunc(handleNodeinfoLink)) + defer s.Close() + + req, _ := http.NewRequest("GET", s.URL, nil) + client := new(http.Client) + r, err := client.Do(req) + if err != nil { + t.Fatalf("Failed - " + err.Error()) + } + if r.Header.Get("Content-Type") != "application/json" { + t.Fatalf("Failed - Content-Type not match.") + } + if r.StatusCode != 200 { + t.Fatalf("Failed - StatusCode is not 200.") + } + defer r.Body.Close() + + data, _ := ioutil.ReadAll(r.Body) + var nodeinfoLinks activitypub.NodeinfoLinks + err = json.Unmarshal(data, &nodeinfoLinks) + if err != nil { + t.Fatalf("NodeinfoLinks response is not valid.") + } +} + +func TestHandleNodeinfoLinkInvalidMethod(t *testing.T) { + s := httptest.NewServer(http.HandlerFunc(handleNodeinfoLink)) + defer s.Close() + + req, _ := http.NewRequest("POST", s.URL, nil) + client := new(http.Client) + r, err := client.Do(req) + if err != nil { + t.Fatalf("Failed - " + err.Error()) + } + if r.StatusCode != 400 { + t.Fatalf("Failed - StatusCode is not 400.") + } +} + +func TestHandleNodeinfoGet(t *testing.T) { + s := httptest.NewServer(http.HandlerFunc(handleNodeinfo)) + defer s.Close() + + req, _ := http.NewRequest("GET", s.URL, nil) + client := new(http.Client) + r, err := client.Do(req) + if err != nil { + t.Fatalf("Failed - " + err.Error()) + } + if r.Header.Get("Content-Type") != "application/json" { + t.Fatalf("Failed - Content-Type not match.") + } + if r.StatusCode != 200 { + t.Fatalf("Failed - StatusCode is not 200.") + } + defer r.Body.Close() + + data, _ := ioutil.ReadAll(r.Body) + var nodeinfo activitypub.Nodeinfo + err = json.Unmarshal(data, &nodeinfo) + if err != nil { + t.Fatalf("Nodeinfo response is not valid.") + } +} + +func TestHandleNodeinfoInvalidMethod(t *testing.T) { + s := httptest.NewServer(http.HandlerFunc(handleNodeinfo)) + defer s.Close() + + req, _ := http.NewRequest("POST", s.URL, nil) + client := new(http.Client) + r, err := client.Do(req) + if err != nil { + t.Fatalf("Failed - " + err.Error()) + } + if r.StatusCode != 400 { + t.Fatalf("Failed - StatusCode is not 400.") + } +} + func TestHandleWebfingerInvalidMethod(t *testing.T) { s := httptest.NewServer(http.HandlerFunc(handleWebfinger)) defer s.Close() @@ -108,7 +190,7 @@ func TestHandleActorGet(t *testing.T) { var actor activitypub.Actor err = json.Unmarshal(data, &actor) if err != nil { - t.Fatalf("Actor responce is not valid.") + t.Fatalf("Actor response is not valid.") } domain, _ := url.Parse(actor.ID) diff --git a/main.go b/main.go index 1cff124..2310c0d 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,9 @@ var ( // WebfingerResource : Relay's Webfinger resource WebfingerResource activitypub.WebfingerResource + // Nodeinfo : Relay's Nodeinfo + Nodeinfo activitypub.NodeinfoResources + hostURL *url.URL hostPrivatekey *rsa.PrivateKey relayState state.RelayState @@ -74,6 +77,7 @@ func initConfig() { Actor.GenerateSelfKey(hostURL, &hostPrivatekey.PublicKey) actorCache = cache.New(5*time.Minute, 10*time.Minute) WebfingerResource.GenerateFromActor(hostURL, &Actor) + Nodeinfo.GenerateFromActor(hostURL, &Actor, version) fmt.Println("Welcome to YUKIMOCHI Activity-Relay [Server]", version) fmt.Println(" - Configurations") @@ -96,7 +100,9 @@ func main() { // Load Config initConfig() + http.HandleFunc("/.well-known/nodeinfo", handleNodeinfoLink) http.HandleFunc("/.well-known/webfinger", handleWebfinger) + http.HandleFunc("/nodeinfo/2.1", handleNodeinfo) http.HandleFunc("/actor", handleActor) http.HandleFunc("/inbox", func(w http.ResponseWriter, r *http.Request) { handleInbox(w, r, decodeActivity)