// 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/forgefed" "code.gitea.io/gitea/modules/activitypub" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ap "github.com/go-ap/activitypub" ) // Repo function returns the Repository actor of a repo 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 := setting.AppURL + "api/v1/activitypub/repo/" + ctx.ContextUser.Name + "/" + ctx.Repo.Repository.Name 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(setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.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 handles the incoming data for a repo inbox 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) return } var activity map[string]interface{} err = json.Unmarshal(body, activity) if err != nil { ctx.ServerError("Unmarshal", err) return } // Make sure keyID matches the user doing the activity _, keyID, _ := getKeyID(ctx.Req) actor, ok := activity["actor"] if ok && !strings.HasPrefix(keyID, actor.(string)) { ctx.ServerError("Actor does not match HTTP signature keyID", nil) return } attributedTo, ok := activity["attributedTo"] if ok && !strings.HasPrefix(keyID, attributedTo.(string)) { ctx.ServerError("AttributedTo does not match HTTP signature keyID", nil) return } // Process activity switch activity["type"].(ap.ActivityVocabularyType) { case ap.CreateType: // Create activity, extract the object object, ok := activity["object"].(map[string]interface{}) if ok { ctx.ServerError("Create activity does not contain object", err) return } objectBinary, err := json.Marshal(object) if err != nil { ctx.ServerError("Marshal", err) return } switch object["type"].(ap.ActivityVocabularyType) { case forgefed.RepositoryType: // Fork created by remote instance var repository forgefed.Repository repository.UnmarshalJSON(objectBinary) activitypub.ForkFromCreate(ctx, repository) case forgefed.TicketType: // New issue or pull request var ticket forgefed.Ticket ticket.UnmarshalJSON(objectBinary) if ticket.Origin != nil { // New pull request activitypub.PullRequest(ctx, ticket) } else { // New issue activitypub.Issue(ctx, ticket) } case ap.NoteType: // New comment var note ap.Note note.UnmarshalJSON(objectBinary) activitypub.Comment(ctx, note) } default: log.Info("Incoming unsupported ActivityStreams type: %s", activity["type"]) ctx.PlainText(http.StatusNotImplemented, "ActivityStreams type not supported") return } ctx.Status(http.StatusNoContent) } // RepoOutbox function returns the repo's Outbox OrderedCollection func RepoOutbox(ctx *context.APIContext) { // swagger:operation GET /activitypub/repo/{username}/{reponame}/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" // TODO ctx.Status(http.StatusNotImplemented) } // RepoFollowers function returns the repo's Followers OrderedCollection 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" // TODO ctx.Status(http.StatusNotImplemented) }