Changes
16 changed files (+1028/-47)
-
-
@@ -85,6 +85,7 @@ require (github.com/tstranex/u2f v1.0.0 github.com/unrolled/render v1.4.1 github.com/urfave/cli v1.22.9 github.com/valyala/fastjson v1.6.3 github.com/xanzy/go-gitlab v0.64.0 github.com/yohcop/openid-go v1.0.0 github.com/yuin/goldmark v1.4.12
-
@@ -256,7 +257,6 @@ require (github.com/toqueteos/webbrowser v1.2.0 // indirect github.com/ulikunitz/xz v0.5.10 // indirect github.com/unknwon/com v1.0.1 // indirect github.com/valyala/fastjson v1.6.3 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.1 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
-
-
-
@@ -22,14 +22,15 @@ type Type int// Note: new type must append to the end of list to maintain compatibility. const ( NoType Type = iota Plain // 1 LDAP // 2 SMTP // 3 PAM // 4 DLDAP // 5 OAuth2 // 6 SSPI // 7 NoType Type = iota Plain // 1 LDAP // 2 SMTP // 3 PAM // 4 DLDAP // 5 OAuth2 // 6 SSPI // 7 Federated // 8 ) // String returns the string name of the LoginType
-
@@ -178,6 +179,11 @@ func (source *Source) IsSSPI() bool {return source.Type == SSPI } // IsFederated returns true of this source is of the Federated type. func (source *Source) IsFederated() bool { return source.Type == Federated } // HasTLS returns true of this source supports TLS. func (source *Source) HasTLS() bool { hasTLSer, ok := source.Cfg.(HasTLSer)
-
-
-
@@ -17,7 +17,7 @@ var (ErrNameEmpty = errors.New("Name is empty") // AlphaDashDotPattern characters prohibited in a user name (anything except A-Za-z0-9_.-) AlphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) AlphaDashDotPattern = regexp.MustCompile(`[^\w-\.@]`) ) // ErrNameReserved represents a "reserved name" error.
-
-
-
@@ -0,0 +1,71 @@// Copyright 2022 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 forgefed import ( ap "github.com/go-ap/activitypub" "github.com/valyala/fastjson" ) const ( RepositoryType ap.ActivityVocabularyType = "Repository" ) type Repository struct { ap.Actor // Team Collection of actors who have management/push access to the repository Team ap.Item `jsonld:"team,omitempty"` // Forks OrderedCollection of repositories that are forks of this repository Forks ap.Item `jsonld:"forks,omitempty"` } // GetItemByType instantiates a new Repository object if the type matches // otherwise it defaults to existing activitypub package typer function. func GetItemByType(typ ap.ActivityVocabularyType) (ap.Item, error) { if typ == RepositoryType { return RepositoryNew(""), nil } return ap.GetItemByType(typ) } // RepositoryNew initializes a Repository type actor func RepositoryNew(id ap.ID) *Repository { a := ap.ActorNew(id, RepositoryType) o := Repository{Actor: *a} o.Type = RepositoryType return &o } func (r Repository) MarshalJSON() ([]byte, error) { b, err := r.Actor.MarshalJSON() if len(b) == 0 || err != nil { return make([]byte, 0), err } b = b[:len(b)-1] if r.Team != nil { ap.WriteItemJSONProp(&b, "team", r.Team) } if r.Forks != nil { ap.WriteItemJSONProp(&b, "forks", r.Forks) } ap.Write(&b, '}') return b, nil } func (r *Repository) UnmarshalJSON(data []byte) error { p := fastjson.Parser{} val, err := p.ParseBytes(data) if err != nil { return err } r.Team = ap.JSONGetItem(val, "team") r.Forks = ap.JSONGetItem(val, "forks") return ap.OnActor(&r.Actor, func(a *ap.Actor) error { return ap.LoadActor(val, a) }) }
-
-
-
@@ -0,0 +1,167 @@package forgefed import ( "encoding/json" "fmt" "reflect" "testing" ap "github.com/go-ap/activitypub" ) func Test_GetItemByType(t *testing.T) { type testtt struct { typ ap.ActivityVocabularyType want ap.Item wantErr error } tests := map[string]testtt{ "invalid type": { typ: ap.ActivityVocabularyType("invalidtype"), wantErr: fmt.Errorf("empty ActivityStreams type"), // TODO(marius): this error message needs to be improved in go-ap/activitypub }, "Repository": { typ: RepositoryType, want: new(Repository), }, "Person - fall back": { typ: ap.PersonType, want: new(ap.Person), }, "Question - fall back": { typ: ap.QuestionType, want: new(ap.Question), }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { maybeRepository, err := GetItemByType(tt.typ) if !reflect.DeepEqual(tt.wantErr, err) { t.Errorf("GetItemByType() error = \"%+v\", wantErr = \"%+v\" when getting Item for type %q", tt.wantErr, err, tt.typ) } if reflect.TypeOf(tt.want) != reflect.TypeOf(maybeRepository) { t.Errorf("Invalid type received %T, expected %T", maybeRepository, tt.want) } }) } } func Test_RepositoryMarshalJSON(t *testing.T) { type testPair struct { item Repository want []byte wantErr error } tests := map[string]testPair{ "empty": { item: Repository{}, want: nil, }, "with ID": { item: Repository{ Actor: ap.Actor{ ID: "https://example.com/1", }, Team: nil, }, want: []byte(`{"id":"https://example.com/1"}`), }, "with Team as IRI": { item: Repository{ Team: ap.IRI("https://example.com/1"), }, want: []byte(`{"team":"https://example.com/1"}`), }, "with Team as IRIs": { item: Repository{ Team: ap.ItemCollection{ ap.IRI("https://example.com/1"), ap.IRI("https://example.com/2"), }, }, want: []byte(`{"team":["https://example.com/1","https://example.com/2"]}`), }, "with Team as Object": { item: Repository{ Team: ap.Object{ID: "https://example.com/1"}, }, want: []byte(`{"team":{"id":"https://example.com/1"}}`), }, "with Team as slice of Objects": { item: Repository{ Team: ap.ItemCollection{ ap.Object{ID: "https://example.com/1"}, ap.Object{ID: "https://example.com/2"}, }, }, want: []byte(`{"team":[{"id":"https://example.com/1"},{"id":"https://example.com/2"}]}`), }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { got, err := tt.item.MarshalJSON() if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() { t.Errorf("MarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("MarshalJSON() got = %q, want %q", got, tt.want) } }) } } func Test_RepositoryUnmarshalJSON(t *testing.T) { type testPair struct { data []byte want *Repository wantErr error } tests := map[string]testPair{ "nil": { data: nil, wantErr: fmt.Errorf("cannot parse JSON: %w", fmt.Errorf("cannot parse empty string; unparsed tail: %q", "")), }, "empty": { data: []byte{}, wantErr: fmt.Errorf("cannot parse JSON: %w", fmt.Errorf("cannot parse empty string; unparsed tail: %q", "")), }, "with Type": { data: []byte(`{"type":"Repository"}`), want: &Repository{ Actor: ap.Actor{ Type: RepositoryType, }, }, }, "with Type and ID": { data: []byte(`{"id":"https://example.com/1","type":"Repository"}`), want: &Repository{ Actor: ap.Actor{ ID: "https://example.com/1", Type: RepositoryType, }, }, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { got := new(Repository) err := got.UnmarshalJSON(tt.data) if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() { t.Errorf("UnmarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr) return } if tt.want != nil && !reflect.DeepEqual(got, tt.want) { jGot, _ := json.Marshal(got) jWant, _ := json.Marshal(tt.want) t.Errorf("UnmarshalJSON() got = %s, want %s", jGot, jWant) } }) } }
-
-
-
@@ -0,0 +1,46 @@// Copyright 2022 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 ( "context" "strings" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" ap "github.com/go-ap/activitypub" ) func Follow(ctx context.Context, activity ap.Follow) { actorIRI := activity.Actor.GetID() objectIRI := activity.Object.GetID() actorIRISplit := strings.Split(actorIRI.String(), "/") objectIRISplit := strings.Split(objectIRI.String(), "/") actorName := actorIRISplit[len(actorIRISplit)-1] + "@" + actorIRISplit[2] objectName := objectIRISplit[len(objectIRISplit)-1] err := FederatedUserNew(actorName, actorIRI) if err != nil { log.Warn("Couldn't create new user", err) } actorUser, err := user_model.GetUserByName(ctx, actorName) if err != nil { log.Warn("Couldn't find actor", err) } objectUser, err := user_model.GetUserByName(ctx, objectName) if err != nil { log.Warn("Couldn't find object", err) } user_model.FollowUser(actorUser.ID, objectUser.ID) accept := ap.AcceptNew(objectIRI, activity) accept.Actor = ap.Person{ID: objectIRI} accept.To = ap.ItemCollection{ap.IRI(actorIRI.String() + "/inbox")} accept.Object = activity Send(objectUser, accept) }
-
-
-
@@ -0,0 +1,59 @@// Copyright 2022 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 ( "fmt" "io" "net/http" "net/url" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ap "github.com/go-ap/activitypub" ) func Fetch(iri *url.URL) (b []byte, err error) { req := httplib.NewRequest(iri.String(), http.MethodGet) req.Header("Accept", ActivityStreamsContentType) req.Header("User-Agent", "Gitea/"+setting.AppVer) resp, err := req.Response() if err != nil { return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { err = fmt.Errorf("url IRI fetch [%s] failed with status (%d): %s", iri, resp.StatusCode, resp.Status) return } b, err = io.ReadAll(io.LimitReader(resp.Body, setting.Federation.MaxSize)) return } func Send(user *user_model.User, activity *ap.Activity) { body, err := activity.MarshalJSON() if err != nil { return } var jsonmap map[string]interface{} err = json.Unmarshal(body, &jsonmap) if err != nil { return } jsonmap["@context"] = "https://www.w3.org/ns/activitystreams" body, _ = json.Marshal(jsonmap) for _, to := range activity.To { client, _ := NewClient(user, setting.AppURL+"api/v1/activitypub/user/"+user.Name+"#main-key") resp, _ := client.Post(body, to.GetID().String()) respBody, _ := io.ReadAll(io.LimitReader(resp.Body, setting.Federation.MaxSize)) log.Debug(string(respBody)) } }
-
-
-
@@ -0,0 +1,22 @@// Copyright 2022 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 ( "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" ap "github.com/go-ap/activitypub" ) func FederatedUserNew(name string, IRI ap.IRI) error { user := &user_model.User{ Name: name, Email: name, LoginType: auth.Federated, Website: IRI.String(), } return user_model.CreateUser(user) }
-
-
-
@@ -5,16 +5,21 @@package activitypub import ( "io" "net/http" "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/forgefed" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/activitypub" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers/api/v1/utils" ap "github.com/go-ap/activitypub" "github.com/go-ap/jsonld" ) // Person function returns the Person actor for a user
-
@@ -23,7 +28,7 @@ func Person(ctx *context.APIContext) {// --- // summary: Returns the Person actor for a user // produces: // - application/json // - application/activity+json // parameters: // - name: username // in: path
-
@@ -62,6 +67,11 @@ func Person(ctx *context.APIContext) {person.Inbox = ap.IRI(link + "/inbox") person.Outbox = ap.IRI(link + "/outbox") person.Following = ap.IRI(link + "/following") person.Followers = ap.IRI(link + "/followers") person.Liked = ap.IRI(link + "/liked") person.PublicKey.ID = ap.IRI(link + "#main-key") person.PublicKey.Owner = ap.IRI(link)
-
@@ -72,16 +82,7 @@ func Person(ctx *context.APIContext) {} person.PublicKey.PublicKeyPem = publicKeyPem binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(person) if err != nil { ctx.ServerError("MarshalJSON", err) return } ctx.Resp.Header().Add("Content-Type", activitypub.ActivityStreamsContentType) ctx.Resp.WriteHeader(http.StatusOK) if _, err = ctx.Resp.Write(binary); err != nil { log.Error("write to resp err: %v", err) } response(ctx, person) } // PersonInbox function handles the incoming data for a user inbox
-
@@ -90,7 +91,7 @@ func PersonInbox(ctx *context.APIContext) {// --- // summary: Send to the inbox // produces: // - application/json // - application/activity+json // parameters: // - name: username // in: path
-
@@ -98,9 +99,180 @@ func PersonInbox(ctx *context.APIContext) {// type: string // required: true // responses: // responses: // "204": // "$ref": "#/responses/empty" body, err := io.ReadAll(io.LimitReader(ctx.Req.Body, setting.Federation.MaxSize)) if err != nil { ctx.ServerError("Error reading request body", err) } var activity ap.Activity activity.UnmarshalJSON(body) if activity.Type == ap.FollowType { activitypub.Follow(ctx, activity) } else { log.Warn("ActivityStreams type not supported", activity) ctx.PlainText(http.StatusNotImplemented, "ActivityStreams type not supported") return } ctx.Status(http.StatusNoContent) } // PersonOutbox function returns the user's Outbox OrderedCollection func PersonOutbox(ctx *context.APIContext) { // swagger:operation GET /activitypub/user/{username}/outbox activitypub activitypubPersonOutbox // --- // summary: Returns the Outbox OrderedCollection // produces: // - application/activity+json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // responses: // "200": // "$ref": "#/responses/ActivityPub" link := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ctx.Req.URL.EscapedPath(), "/") feed, err := models.GetFeeds(ctx, models.GetFeedsOptions{ RequestedUser: ctx.ContextUser, Actor: ctx.ContextUser, IncludePrivate: false, OnlyPerformedBy: true, IncludeDeleted: false, Date: ctx.FormString("date"), }) if err != nil { ctx.ServerError("Couldn't fetch outbox", err) } outbox := ap.OrderedCollectionNew(ap.IRI(link)) for _, action := range feed { /*if action.OpType == ExampleType { activity := ap.ExampleNew() outbox.OrderedItems.Append(activity) }*/ log.Debug(action.Content) } outbox.TotalItems = uint(len(outbox.OrderedItems)) response(ctx, outbox) } // PersonFollowing function returns the user's Following Collection func PersonFollowing(ctx *context.APIContext) { // swagger:operation GET /activitypub/user/{username}/following activitypub activitypubPersonFollowing // --- // summary: Returns the Following Collection // produces: // - application/activity+json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // responses: // "200": // "$ref": "#/responses/ActivityPub" link := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ctx.Req.URL.EscapedPath(), "/") users, err := user_model.GetUserFollowing(ctx.ContextUser, utils.GetListOptions(ctx)) if err != nil { ctx.ServerError("GetUserFollowing", err) return } following := ap.OrderedCollectionNew(ap.IRI(link)) following.TotalItems = uint(len(users)) for _, user := range users { // TODO: handle non-Federated users person := ap.PersonNew(ap.IRI(user.Website)) following.OrderedItems.Append(person) } response(ctx, following) } // PersonFollowers function returns the user's Followers Collection func PersonFollowers(ctx *context.APIContext) { // swagger:operation GET /activitypub/user/{username}/followers activitypub activitypubPersonFollowers // --- // summary: Returns the Followers Collection // produces: // - application/activity+json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // responses: // "200": // "$ref": "#/responses/ActivityPub" link := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ctx.Req.URL.EscapedPath(), "/") users, err := user_model.GetUserFollowers(ctx.ContextUser, utils.GetListOptions(ctx)) if err != nil { ctx.ServerError("GetUserFollowers", err) return } followers := ap.OrderedCollectionNew(ap.IRI(link)) followers.TotalItems = uint(len(users)) for _, user := range users { person := ap.PersonNew(ap.IRI(user.Website)) followers.OrderedItems.Append(person) } response(ctx, followers) } // PersonLiked function returns the user's Liked Collection func PersonLiked(ctx *context.APIContext) { // swagger:operation GET /activitypub/user/{username}/followers activitypub activitypubPersonLiked // --- // summary: Returns the Liked Collection // produces: // - application/activity+json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // responses: // "200": // "$ref": "#/responses/ActivityPub" link := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ctx.Req.URL.EscapedPath(), "/") repos, count, err := repo_model.SearchRepository(&repo_model.SearchRepoOptions{ Actor: ctx.Doer, Private: ctx.IsSigned, StarredByID: ctx.ContextUser.ID, }) if err != nil { ctx.ServerError("GetUserStarred", err) return } liked := ap.OrderedCollectionNew(ap.IRI(link)) liked.TotalItems = uint(count) for _, repo := range repos { repo := forgefed.RepositoryNew(ap.IRI(strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/repo/" + repo.OwnerName + "/" + repo.Name)) liked.OrderedItems.Append(repo) } response(ctx, liked) }
-
-
-
@@ -0,0 +1,198 @@// Copyright 2022 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 ( "io" "net/http" "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/forgefed" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers/api/v1/utils" ap "github.com/go-ap/activitypub" ) // Repo function func Repo(ctx *context.APIContext) { // swagger:operation GET /activitypub/repo/{username}/{reponame} activitypub activitypubRepo // --- // summary: Returns the repository // produces: // - application/activity+json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // - name: reponame // in: path // description: name of the repository // type: string // required: true // responses: // "200": // "$ref": "#/responses/ActivityPub" link := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ctx.Req.URL.EscapedPath(), "/") repo := forgefed.RepositoryNew(ap.IRI(link)) repo.Name = ap.NaturalLanguageValuesNew() err := repo.Name.Set("en", ap.Content(ctx.Repo.Repository.Name)) if err != nil { ctx.ServerError("Set Name", err) return } repo.AttributedTo = ap.IRI(strings.TrimSuffix(link, "/"+ctx.Repo.Repository.Name)) repo.Summary = ap.NaturalLanguageValuesNew() err = repo.Summary.Set("en", ap.Content(ctx.Repo.Repository.Description)) if err != nil { ctx.ServerError("Set Description", err) return } repo.Inbox = ap.IRI(link + "/inbox") repo.Outbox = ap.IRI(link + "/outbox") repo.Followers = ap.IRI(link + "/followers") repo.Team = ap.IRI(link + "/team") response(ctx, repo) } // RepoInbox function func RepoInbox(ctx *context.APIContext) { // swagger:operation POST /activitypub/repo/{username}/{reponame}/inbox activitypub activitypubRepoInbox // --- // summary: Send to the inbox // produces: // - application/activity+json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // - name: reponame // in: path // description: name of the repository // type: string // required: true // responses: // "204": // "$ref": "#/responses/empty" body, err := io.ReadAll(ctx.Req.Body) if err != nil { ctx.ServerError("Error reading request body", err) } var activity ap.Activity activity.UnmarshalJSON(body) if activity.Type == ap.FollowType { // activitypub.Follow(ctx, activity) } else { log.Warn("ActivityStreams type not supported", activity) } ctx.Status(http.StatusNoContent) } // RepoOutbox function func RepoOutbox(ctx *context.APIContext) { // swagger:operation GET /activitypub/repo/{username}/outbox activitypub activitypubPersonOutbox // --- // summary: Returns the outbox // produces: // - application/activity+json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // - name: reponame // in: path // description: name of the repository // type: string // required: true // responses: // "200": // "$ref": "#/responses/ActivityPub" link := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ctx.Req.URL.EscapedPath(), "/") feed, err := models.GetFeeds(ctx, models.GetFeedsOptions{ RequestedUser: ctx.ContextUser, Actor: ctx.ContextUser, IncludePrivate: false, OnlyPerformedBy: true, IncludeDeleted: false, Date: ctx.FormString("date"), }) if err != nil { ctx.ServerError("Couldn't fetch outbox", err) } outbox := ap.OrderedCollectionNew(ap.IRI(link)) for _, action := range feed { /*if action.OpType == ExampleType { activity := ap.ExampleNew() outbox.OrderedItems.Append(activity) }*/ log.Debug(action.Content) } outbox.TotalItems = uint(len(outbox.OrderedItems)) response(ctx, outbox) } // RepoFollowers function func RepoFollowers(ctx *context.APIContext) { // swagger:operation GET /activitypub/repo/{username}/{reponame}/followers activitypub activitypubRepoFollowers // --- // summary: Returns the followers collection // produces: // - application/activity+json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // - name: reponame // in: path // description: name of the repository // type: string // required: true // responses: // "200": // "$ref": "#/responses/ActivityPub" link := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ctx.Req.URL.EscapedPath(), "/") users, err := user_model.GetUserFollowers(ctx.ContextUser, utils.GetListOptions(ctx)) if err != nil { ctx.ServerError("GetUserFollowers", err) return } followers := ap.OrderedCollectionNew(ap.IRI(link)) followers.TotalItems = uint(len(users)) for _, user := range users { person := ap.PersonNew(ap.IRI(user.Website)) followers.OrderedItems.Append(person) } response(ctx, followers) }
-
-
-
@@ -9,13 +9,11 @@ import ("crypto/x509" "encoding/pem" "fmt" "io" "net/http" "net/url" "code.gitea.io/gitea/modules/activitypub" gitea_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/setting" ap "github.com/go-ap/activitypub"
-
@@ -44,24 +42,6 @@ func getPublicKeyFromResponse(b []byte, keyID *url.URL) (p crypto.PublicKey, errreturn } func fetch(iri *url.URL) (b []byte, err error) { req := httplib.NewRequest(iri.String(), http.MethodGet) req.Header("Accept", activitypub.ActivityStreamsContentType) req.Header("User-Agent", "Gitea/"+setting.AppVer) resp, err := req.Response() if err != nil { return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { err = fmt.Errorf("url IRI fetch [%s] failed with status (%d): %s", iri, resp.StatusCode, resp.Status) return } b, err = io.ReadAll(io.LimitReader(resp.Body, setting.Federation.MaxSize)) return } func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, err error) { r := ctx.Req
-
@@ -76,7 +56,7 @@ func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, erreturn } // 2. Fetch the public key of the other actor b, err := fetch(idIRI) b, err := activitypub.Fetch(idIRI) if err != nil { return }
-
-
-
@@ -0,0 +1,29 @@// Copyright 2022 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" "code.gitea.io/gitea/modules/activitypub" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" ap "github.com/go-ap/activitypub" "github.com/go-ap/jsonld" ) func response(ctx *context.APIContext, v interface{}) { binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(v) if err != nil { ctx.ServerError("Marshal", err) return } ctx.Resp.Header().Add("Content-Type", activitypub.ActivityStreamsContentType) ctx.Resp.WriteHeader(http.StatusOK) if _, err = ctx.Resp.Write(binary); err != nil { log.Error("write to resp err: %v", err) } }
-
-
-
@@ -648,7 +648,17 @@ func Routes() *web.Route {m.Group("/user/{username}", func() { m.Get("", activitypub.Person) m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox) m.Get("/outbox", activitypub.PersonOutbox) m.Get("/following", activitypub.PersonFollowing) m.Get("/followers", activitypub.PersonFollowers) m.Get("/liked", activitypub.PersonLiked) }, context_service.UserAssignmentAPI()) m.Group("/repo/{username}/{reponame}", func() { m.Get("", activitypub.Repo) m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.RepoInbox) m.Get("/outbox", activitypub.RepoOutbox) m.Get("/followers", activitypub.RepoFollowers) }, repoAssignment()) }) } m.Get("/signing-key.gpg", misc.SigningKey)
-
-
-
@@ -36,6 +36,12 @@ func Profile(ctx *context.Context) {return } if strings.Contains(ctx.ContextUser.Name, "@") { ctx.Resp.Header().Add("Location", ctx.ContextUser.Website) ctx.Resp.WriteHeader(http.StatusTemporaryRedirect) return } if ctx.ContextUser.IsOrganization() { org.Home(ctx) return
-
-
-
@@ -29,6 +29,7 @@ type webfingerLink struct {Rel string `json:"rel,omitempty"` Type string `json:"type,omitempty"` Href string `json:"href,omitempty"` Template string `json:"template,omitempty"` Titles map[string]string `json:"titles,omitempty"` Properties map[string]interface{} `json:"properties,omitempty"` }
-
@@ -107,6 +108,10 @@ func WebfingerQuery(ctx *context.Context) {Type: "application/activity+json", Href: appURL.String() + "api/v1/activitypub/user/" + url.PathEscape(u.Name), }, { Rel: "http://ostatus.org/schema/1.0/subscribe", Template: appURL.String() + "api/v1/authorize_interaction?uri={uri}", }, } ctx.Resp.Header().Add("Access-Control-Allow-Origin", "*")
-
-
-
@@ -23,10 +23,142 @@}, "basePath": "{{AppSubUrl | JSEscape | Safe}}/api/v1", "paths": { "/activitypub/repo/{username}/outbox": { "get": { "produces": [ "application/activity+json" ], "tags": [ "activitypub" ], "summary": "Returns the outbox", "operationId": "activitypubPersonOutbox", "parameters": [ { "type": "string", "description": "username of the user", "name": "username", "in": "path", "required": true }, { "type": "string", "description": "name of the repository", "name": "reponame", "in": "path", "required": true } ], "responses": { "200": { "$ref": "#/responses/ActivityPub" } } } }, "/activitypub/repo/{username}/{reponame}": { "get": { "produces": [ "application/activity+json" ], "tags": [ "activitypub" ], "summary": "Returns the repository", "operationId": "activitypubRepo", "parameters": [ { "type": "string", "description": "username of the user", "name": "username", "in": "path", "required": true }, { "type": "string", "description": "name of the repository", "name": "reponame", "in": "path", "required": true } ], "responses": { "200": { "$ref": "#/responses/ActivityPub" } } } }, "/activitypub/repo/{username}/{reponame}/followers": { "get": { "produces": [ "application/activity+json" ], "tags": [ "activitypub" ], "summary": "Returns the followers collection", "operationId": "activitypubRepoFollowers", "parameters": [ { "type": "string", "description": "username of the user", "name": "username", "in": "path", "required": true }, { "type": "string", "description": "name of the repository", "name": "reponame", "in": "path", "required": true } ], "responses": { "200": { "$ref": "#/responses/ActivityPub" } } } }, "/activitypub/repo/{username}/{reponame}/inbox": { "post": { "produces": [ "application/activity+json" ], "tags": [ "activitypub" ], "summary": "Send to the inbox", "operationId": "activitypubRepoInbox", "parameters": [ { "type": "string", "description": "username of the user", "name": "username", "in": "path", "required": true }, { "type": "string", "description": "name of the repository", "name": "reponame", "in": "path", "required": true } ], "responses": { "204": { "$ref": "#/responses/empty" } } } }, "/activitypub/user/{username}": { "get": { "produces": [ "application/json" "application/activity+json" ], "tags": [ "activitypub"
-
@@ -49,10 +181,62 @@} } }, "/activitypub/user/{username}/followers": { "get": { "produces": [ "application/activity+json" ], "tags": [ "activitypub" ], "summary": "Returns the Liked Collection", "operationId": "activitypubPersonLiked", "parameters": [ { "type": "string", "description": "username of the user", "name": "username", "in": "path", "required": true } ], "responses": { "200": { "$ref": "#/responses/ActivityPub" } } } }, "/activitypub/user/{username}/following": { "get": { "produces": [ "application/activity+json" ], "tags": [ "activitypub" ], "summary": "Returns the Following Collection", "operationId": "activitypubPersonFollowing", "parameters": [ { "type": "string", "description": "username of the user", "name": "username", "in": "path", "required": true } ], "responses": { "200": { "$ref": "#/responses/ActivityPub" } } } }, "/activitypub/user/{username}/inbox": { "post": { "produces": [ "application/json" "application/activity+json" ], "tags": [ "activitypub"
-
@@ -75,6 +259,32 @@} } }, "/activitypub/user/{username}/outbox": { "get": { "produces": [ "application/activity+json" ], "tags": [ "activitypub" ], "summary": "Returns the Outbox OrderedCollection", "operationId": "activitypubPersonOutbox", "parameters": [ { "type": "string", "description": "username of the user", "name": "username", "in": "path", "required": true } ], "responses": { "200": { "$ref": "#/responses/ActivityPub" } } } }, "/admin/cron": { "get": { "produces": [
-