From 49db112ccf957e03ac9b95a03e9bed5a7a15e675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Dachary?= Date: Tue, 26 Oct 2021 15:59:14 -1000 Subject: [PATCH] activitypub: implement /api/v1/activitypub/user/{username} (#14186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Return informations regarding a Person (as defined in ActivityStreams https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person). Refs: https://github.com/go-gitea/gitea/issues/14186 Signed-off-by: Loïc Dachary --- integrations/api_activitypub_person_test.go | 63 +++++++++++++++++++++ modules/structs/activitypub.go | 9 +++ routers/api/v1/activitypub/person.go | 62 ++++++++++++++++++++ routers/api/v1/api.go | 6 ++ routers/api/v1/swagger/activitypub.go | 16 ++++++ templates/swagger/v1_json.tmpl | 39 +++++++++++++ 6 files changed, 195 insertions(+) create mode 100644 integrations/api_activitypub_person_test.go create mode 100644 modules/structs/activitypub.go create mode 100644 routers/api/v1/activitypub/person.go create mode 100644 routers/api/v1/swagger/activitypub.go diff --git a/integrations/api_activitypub_person_test.go b/integrations/api_activitypub_person_test.go new file mode 100644 index 000000000..2efa82f02 --- /dev/null +++ b/integrations/api_activitypub_person_test.go @@ -0,0 +1,63 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + + "code.gitea.io/gitea/modules/setting" + "github.com/go-fed/activity/streams" + "github.com/go-fed/activity/streams/vocab" + + "github.com/stretchr/testify/assert" +) + +func TestActivityPubPerson(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + setting.Federation.Enabled = true + defer func() { + setting.Federation.Enabled = false + }() + + username := "user2" + req := NewRequestf(t, "GET", fmt.Sprintf("/api/v1/activitypub/user/%s", username)) + resp := MakeRequest(t, req, http.StatusOK) + assert.Contains(t, string(resp.Body.Bytes()), "@context") + var m map[string]interface{} + _ = json.Unmarshal(resp.Body.Bytes(), &m) + + var person vocab.ActivityStreamsPerson + resolver, _ := streams.NewJSONResolver(func(c context.Context, p vocab.ActivityStreamsPerson) error { + person = p + return nil + }) + ctx := context.Background() + err := resolver.Resolve(ctx, m) + assert.Equal(t, err, nil) + assert.Equal(t, person.GetTypeName(), "Person") + assert.Equal(t, person.GetActivityStreamsName().Begin().GetXMLSchemaString(), username) + assert.Regexp(t, fmt.Sprintf("activitypub/user/%s$", username), person.GetJSONLDId().GetIRI().String()) + assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/outbox$", username), person.GetActivityStreamsOutbox().GetIRI().String()) + assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/inbox$", username), person.GetActivityStreamsInbox().GetIRI().String()) + }) +} + +func TestActivityPubMissingPerson(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + setting.Federation.Enabled = true + defer func() { + setting.Federation.Enabled = false + }() + + req := NewRequestf(t, "GET", "/api/v1/activitypub/user/nonexistentuser") + resp := MakeRequest(t, req, http.StatusNotFound) + assert.Contains(t, string(resp.Body.Bytes()), "GetUserByName") + }) +} diff --git a/modules/structs/activitypub.go b/modules/structs/activitypub.go new file mode 100644 index 000000000..e1e2ec46a --- /dev/null +++ b/modules/structs/activitypub.go @@ -0,0 +1,9 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package structs + +type ActivityPub struct { + Context string `json:"@context"` +} diff --git a/routers/api/v1/activitypub/person.go b/routers/api/v1/activitypub/person.go new file mode 100644 index 000000000..ae1a8a7bb --- /dev/null +++ b/routers/api/v1/activitypub/person.go @@ -0,0 +1,62 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package activitypub + +import ( + "net/http" + "net/url" + "strings" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/routers/api/v1/user" + "github.com/go-fed/activity/streams" +) + +func Person(ctx *context.APIContext) { + // swagger:operation GET /activitypub/user/{username} information + // --- + // summary: Returns the person + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of the user + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/ActivityPub" + + user.GetUserByParamsName(ctx, "username") + username := ctx.Params("username") + + person := streams.NewActivityStreamsPerson() + + id := streams.NewJSONLDIdProperty() + link := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ctx.Req.URL.EscapedPath(), "/") + url_object, _ := url.Parse(link) + id.SetIRI(url_object) + person.SetJSONLDId(id) + + name := streams.NewActivityStreamsNameProperty() + name.AppendXMLSchemaString(username) + person.SetActivityStreamsName(name) + + ibox := streams.NewActivityStreamsInboxProperty() + url_object, _ = url.Parse(link + "/inbox") + ibox.SetIRI(url_object) + person.SetActivityStreamsInbox(ibox) + + obox := streams.NewActivityStreamsOutboxProperty() + url_object, _ = url.Parse(link + "/outbox") + obox.SetIRI(url_object) + person.SetActivityStreamsOutbox(obox) + + var jsonmap map[string]interface{} + jsonmap, _ = streams.Serialize(person) + ctx.JSON(http.StatusOK, jsonmap) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index aec2a6d7b..4ce71191b 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -81,6 +81,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/activitypub" "code.gitea.io/gitea/routers/api/v1/admin" "code.gitea.io/gitea/routers/api/v1/misc" "code.gitea.io/gitea/routers/api/v1/notify" @@ -633,6 +634,11 @@ func Routes() *web.Route { m.Get("/version", misc.Version) if setting.Federation.Enabled { m.Get("/nodeinfo", misc.NodeInfo) + m.Group("/activitypub", func() { + m.Group("/user/{username}", func() { + m.Get("", activitypub.Person) + }) + }) } m.Get("/signing-key.gpg", misc.SigningKey) m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown) diff --git a/routers/api/v1/swagger/activitypub.go b/routers/api/v1/swagger/activitypub.go new file mode 100644 index 000000000..3576439f4 --- /dev/null +++ b/routers/api/v1/swagger/activitypub.go @@ -0,0 +1,16 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package swagger + +import ( + api "code.gitea.io/gitea/modules/structs" +) + +// ActivityPub +// swagger:response ActivityPub +type swaggerResponseActivityPub struct { + // in:body + Body api.ActivityPub `json:"body"` +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 0374a53a6..228ea23e4 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -23,6 +23,29 @@ }, "basePath": "{{AppSubUrl | JSEscape | Safe}}/api/v1", "paths": { + "/activitypub/user/{username}": { + "get": { + "produces": [ + "application/json" + ], + "summary": "Returns the person", + "operationId": "information", + "parameters": [ + { + "type": "string", + "description": "username of the user", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActivityPub" + } + } + } + }, "/admin/cron": { "get": { "produces": [ @@ -12922,6 +12945,16 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "ActivityPub": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "x-go-name": "Context" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "AddCollaboratorOption": { "description": "AddCollaboratorOption options when adding a user as a collaborator of a repository", "type": "object", @@ -18569,6 +18602,12 @@ } } }, + "ActivityPub": { + "description": "ActivityPub", + "schema": { + "$ref": "#/definitions/ActivityPub" + } + }, "AnnotatedTag": { "description": "AnnotatedTag", "schema": {