Big refactor: Improve inbox handling logic, move some IRI stuff to iri.go

This commit is contained in:
Anthony Wang 2022-07-13 22:10:03 -05:00
parent a63b2be21b
commit 56717396fd
No known key found for this signature in database
GPG Key ID: BC96B00AEC5F2D76
14 changed files with 315 additions and 182 deletions

View File

@ -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-\.@]`) // Ugly hack to allow remote usernames to contain @
)
// ErrNameReserved represents a "reserved name" error.

View File

@ -8,6 +8,8 @@ import (
ap "github.com/go-ap/activitypub"
)
const ForgeFedNamespaceURI = "https://forgefed.org/ns"
// GetItemByType instantiates a new ForgeFed object if the type matches
// otherwise it defaults to existing activitypub package typer function.
func GetItemByType(typ ap.ActivityVocabularyType) (ap.Item, error) {

View File

@ -27,6 +27,10 @@ type Ticket struct {
ResolvedBy ap.Item `jsonld:"resolvedBy,omitempty"`
// Resolved When the ticket has been marked as resolved
Resolved time.Time `jsonld:"resolved,omitempty"`
// Origin The head branch if this ticket is a pull request
Origin ap.Item `jsonld:"origin,omitempty"`
// Target The base branch if this ticket is a pull request
Target ap.Item `jsonld:"target,omitempty"`
}
// TicketNew initializes a Ticket type Object
@ -56,6 +60,12 @@ func (t Ticket) MarshalJSON() ([]byte, error) {
if !t.Resolved.IsZero() {
ap.WriteTimeJSONProp(&b, "resolved", t.Resolved)
}
if t.Origin != nil {
ap.WriteItemJSONProp(&b, "origin", t.Origin)
}
if t.Target != nil {
ap.WriteItemJSONProp(&b, "target", t.Target)
}
ap.Write(&b, '}')
return b, nil
}
@ -72,6 +82,8 @@ func (t *Ticket) UnmarshalJSON(data []byte) error {
t.IsResolved = ap.JSONGetBoolean(val, "isResolved")
t.ResolvedBy = ap.JSONGetItem(val, "resolvedBy")
t.Resolved = ap.JSONGetTime(val, "resolved")
t.Origin = ap.JSONGetItem(val, "origin")
t.Target = ap.JSONGetItem(val, "target")
return ap.OnObject(&t.Object, func(a *ap.Object) error {
return ap.LoadObject(val, a)

View File

@ -0,0 +1,41 @@
// 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"
"strconv"
"strings"
"code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
ap "github.com/go-ap/activitypub"
)
// Create a comment
func Comment(ctx context.Context, note ap.Note) {
actorUser, err := personIRIToUser(ctx, note.AttributedTo.GetLink())
if err != nil {
log.Warn("Couldn't find actor", err)
}
// TODO: Move IRI processing stuff to iri.go
context := note.Context.GetLink()
contextSplit := strings.Split(context.String(), "/")
username := contextSplit[3]
reponame := contextSplit[4]
repo, _ := repo_model.GetRepositoryByOwnerAndName(username, reponame)
idx, _ := strconv.ParseInt(contextSplit[len(contextSplit)-1], 10, 64)
issue, _ := issues.GetIssueByIndex(repo.ID, idx)
issues.CreateCommentCtx(ctx, &issues.CreateCommentOptions{
Doer: actorUser,
Repo: repo,
Issue: issue,
Content: note.Content.String(),
})
}

View File

@ -14,33 +14,61 @@ import (
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]
// Process a Follow activity
func Follow(ctx context.Context, follow ap.Follow) {
// Actor is the user performing the follow
actorIRI := follow.Actor.GetID()
actorUser, err := personIRIToUser(ctx, actorIRI)
if err != nil {
log.Warn("Couldn't find actor user for follow", err)
return
}
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)
// Object is the user being followed
objectIRI := follow.Object.GetID()
objectUser, err := personIRIToUser(ctx, objectIRI)
// Must be a local user
if strings.Contains(objectUser.Name, "@") || err != nil {
log.Warn("Couldn't find object user for follow", err)
return
}
user_model.FollowUser(actorUser.ID, objectUser.ID)
accept := ap.AcceptNew(objectIRI, activity)
// Send back an Accept activity
accept := ap.AcceptNew(objectIRI, follow)
accept.Actor = ap.Person{ID: objectIRI}
accept.To = ap.ItemCollection{ap.IRI(actorIRI.String() + "/inbox")}
accept.Object = activity
accept.Object = follow
Send(objectUser, accept)
}
// Process a Undo follow activity
// I haven't tried this yet so hopefully it works
func Unfollow(ctx context.Context, unfollow ap.Undo) {
// Actor is the user performing the undo follow
actorIRI := unfollow.Actor.GetID()
actorUser, err := personIRIToUser(ctx, actorIRI)
if err != nil {
log.Warn("Couldn't find actor user for follow", err)
return
}
// Object is the user being unfollowed
objectIRI := unfollow.Object.GetID()
objectUser, err := personIRIToUser(ctx, objectIRI)
// Must be a local user
if strings.Contains(objectUser.Name, "@") || err != nil {
log.Warn("Couldn't find object user for follow", err)
return
}
user_model.UnfollowUser(actorUser.ID, objectUser.ID)
// Send back an Accept activity
accept := ap.AcceptNew(objectIRI, unfollow)
accept.Actor = ap.Person{ID: objectIRI}
accept.To = ap.ItemCollection{ap.IRI(actorIRI.String() + "/inbox")}
accept.Object = unfollow
Send(objectUser, accept)
}

View File

@ -8,7 +8,7 @@ import (
"context"
"strings"
//"code.gitea.io/gitea/models/forgefed"
"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/log"
@ -20,6 +20,8 @@ import (
)
func Fork(ctx context.Context, instance, username, reponame, destUsername string) {
// TODO: Clean this up
// Migrate repository code
user, _ := user_model.GetUserByName(ctx, destUsername)
_, err := migrations.MigrateRepository(ctx, user, destUsername, migrations.MigrateOptions{
@ -30,25 +32,27 @@ func Fork(ctx context.Context, instance, username, reponame, destUsername string
log.Warn("Couldn't create fork", err)
}
// Make the migrated repo a fork
// TODO: Make the migrated repo a fork
// Send a Create activity to the instance we are forking from
create := ap.Create{Type: ap.CreateType}
create.To = ap.ItemCollection{ap.IRI("https://" + instance + "/api/v1/activitypub/repo/" + username + "/" + reponame + "/inbox")}
repo := ap.IRI(strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/repo/" + destUsername + "/" + reponame)
// repo := forgefed.RepositoryNew(ap.IRI(strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/repo/" + destUsername + "/" + reponame))
repo := ap.IRI(setting.AppURL + "api/v1/activitypub/repo/" + destUsername + "/" + reponame)
// repo := forgefed.RepositoryNew(ap.IRI(setting.AppURL + "api/v1/activitypub/repo/" + destUsername + "/" + reponame))
// repo.ForkedFrom = forgefed.RepositoryNew(ap.IRI())
create.Object = repo
Send(user, &create)
}
func ForkFromCreate(ctx context.Context, activity ap.Create) {
func ForkFromCreate(ctx context.Context, repository forgefed.Repository) {
// TODO: Clean this up
// Don't create an actual copy of the remote repo!
// https://gitea.com/Ta180m/gitea/issues/7
// Create the fork
repoIRI := activity.Object.GetID()
repoIRI := repository.GetID()
repoIRISplit := strings.Split(repoIRI.String(), "/")
instance := repoIRISplit[2]
username := repoIRISplit[7]
@ -63,6 +67,4 @@ func ForkFromCreate(ctx context.Context, activity ap.Create) {
_, err := repo_service.ForkRepository(ctx, user, user, repo_service.ForkRepoOptions{BaseRepo: repo, Name: reponame, Description: "this is a remote fork"})
log.Warn("Couldn't create copy of remote fork", err)
// TODO: send back accept
}

View File

@ -0,0 +1,80 @@
// 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"
"errors"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
ap "github.com/go-ap/activitypub"
)
// Returns the username corresponding to a Person actor IRI
func personIRIToName(personIRI ap.IRI) (string, error) {
personIRISplit := strings.Split(personIRI.String(), "/")
if len(personIRISplit) < 3 {
return "", errors.New("Not a Person actor IRI")
}
instance := personIRISplit[2]
name := personIRISplit[len(personIRISplit)-1]
if instance == setting.Domain {
// Local user
return name, nil
} else {
// Remote user
// Get name in username@instance.com format
return name + "@" + instance, nil
}
}
// Returns the user corresponding to a Person actor IRI
func personIRIToUser(ctx context.Context, personIRI ap.IRI) (*user_model.User, error) {
name, err := personIRIToName(personIRI)
if err != nil {
return nil, err
}
user, err := user_model.GetUserByName(ctx, name)
if err != nil || !strings.Contains(name, "@") {
return user, err
}
FederatedUserNew(personIRI)
return user_model.GetUserByName(ctx, name)
}
// Returns the owner and name corresponding to a Repository actor IRI
func repositoryIRIToName(repoIRI ap.IRI) (string, string, error) {
repoIRISplit := strings.Split(repoIRI.String(), "/")
if len(repoIRISplit) < 5 {
return "", "", errors.New("Not a Repository actor IRI")
}
instance := repoIRISplit[2]
username := repoIRISplit[len(repoIRISplit)-2]
reponame := repoIRISplit[len(repoIRISplit)-1]
if instance == setting.Domain {
// Local repo
return username, reponame, nil
} else {
// Remote repo
return username + "@" + instance, reponame, nil
}
}
// Returns the repository corresponding to a Repository actor IRI
func repositoryIRIToRepository(ctx context.Context, repoIRI ap.IRI) (*repo_model.Repository, error) {
username, reponame, err := repositoryIRIToName(repoIRI)
if err != nil {
return nil, err
}
return repo_model.GetRepositoryByOwnerAndName(username, reponame)
}

View File

@ -6,41 +6,11 @@ package activitypub
import (
"context"
"strconv"
"strings"
"code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
ap "github.com/go-ap/activitypub"
"code.gitea.io/gitea/models/forgefed"
)
func Comment(ctx context.Context, activity ap.Note) {
actorIRI := activity.AttributedTo.GetLink()
actorIRISplit := strings.Split(actorIRI.String(), "/")
actorName := actorIRISplit[len(actorIRISplit)-1] + "@" + actorIRISplit[2]
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)
}
context := activity.Context.GetLink()
contextSplit := strings.Split(context.String(), "/")
username := contextSplit[3]
reponame := contextSplit[4]
repo, _ := repo_model.GetRepositoryByOwnerAndName(username, reponame)
idx, _ := strconv.ParseInt(contextSplit[len(contextSplit)-1], 10, 64)
issue, _ := issues.GetIssueByIndex(repo.ID, idx)
issues.CreateCommentCtx(ctx, &issues.CreateCommentOptions{
Doer: actorUser,
Repo: repo,
Issue: issue,
Content: activity.Content.String(),
})
// Create an issue
func Issue(ctx context.Context, ticket forgefed.Ticket) {
// TODO
}

View File

@ -9,31 +9,23 @@ import (
"fmt"
"strings"
"code.gitea.io/gitea/models/forgefed"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
pull_service "code.gitea.io/gitea/services/pull"
ap "github.com/go-ap/activitypub"
)
func PullRequest(ctx context.Context, activity ap.Move) {
actorIRI := activity.AttributedTo.GetLink()
actorIRISplit := strings.Split(actorIRI.String(), "/")
actorName := actorIRISplit[len(actorIRISplit)-1] + "@" + actorIRISplit[2]
err := FederatedUserNew(actorName, actorIRI)
func PullRequest(ctx context.Context, ticket forgefed.Ticket) {
// TODO: Clean this up
actorUser, err := personIRIToUser(ctx, ticket.AttributedTo.GetLink())
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)
log.Warn("Couldn't find ticket actor user", err)
}
// This code is really messy
// The IRI processing stuff should be in a separate function
originIRI := activity.Origin.GetLink()
// TODO: The IRI processing stuff should be moved to iri.go
originIRI := ticket.Origin.GetLink()
originIRISplit := strings.Split(originIRI.String(), "/")
originInstance := originIRISplit[2]
originUsername := originIRISplit[3]
@ -41,7 +33,7 @@ func PullRequest(ctx context.Context, activity ap.Move) {
originBranch := originIRISplit[len(originIRISplit)-1]
originRepo, _ := repo_model.GetRepositoryByOwnerAndName(originUsername+"@"+originInstance, originReponame)
targetIRI := activity.Target.GetLink()
targetIRI := ticket.Target.GetLink()
targetIRISplit := strings.Split(targetIRI.String(), "/")
// targetInstance := targetIRISplit[2]
targetUsername := targetIRISplit[3]
@ -56,19 +48,18 @@ func PullRequest(ctx context.Context, activity ap.Move) {
PosterID: actorUser.ID,
Poster: actorUser,
IsPull: true,
Content: "🎉",
Content: "🎉", // TODO: Get content from Ticket object
}
pr := &issues_model.PullRequest{
HeadRepoID: originRepo.ID,
BaseRepoID: targetRepo.ID,
HeadBranch: originBranch,
HeadCommitID: "73f228996f27fad2c7bb60435f912d943b66b0ee", // hardcoded for now
BaseBranch: targetBranch,
HeadRepo: originRepo,
BaseRepo: targetRepo,
MergeBase: "",
Type: issues_model.PullRequestGitea,
HeadRepoID: originRepo.ID,
BaseRepoID: targetRepo.ID,
HeadBranch: originBranch,
BaseBranch: targetBranch,
HeadRepo: originRepo,
BaseRepo: targetRepo,
MergeBase: "",
Type: issues_model.PullRequestGitea,
}
err = pull_service.NewPullRequest(ctx, targetRepo, prIssue, []int64{}, []string{}, pr, []int64{})

View File

@ -10,15 +10,17 @@ import (
"net/http"
"net/url"
"code.gitea.io/gitea/models/forgefed"
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"
"github.com/go-ap/jsonld"
)
// Fetch a remote ActivityStreams object
func Fetch(iri *url.URL) (b []byte, err error) {
req := httplib.NewRequest(iri.String(), http.MethodGet)
req.Header("Accept", ActivityStreamsContentType)
@ -37,22 +39,21 @@ func Fetch(iri *url.URL) (b []byte, err error) {
return b, err
}
// Send an activity
func Send(user *user_model.User, activity *ap.Activity) {
body, err := activity.MarshalJSON()
binary, err := jsonld.WithContext(
jsonld.IRI(ap.ActivityBaseURI),
jsonld.IRI(ap.SecurityContextURI),
jsonld.IRI(forgefed.ForgeFedNamespaceURI),
).Marshal(activity)
if err != nil {
log.Warn("Marshal", err)
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())
resp, _ := client.Post(binary, to.GetID().String())
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, setting.Federation.MaxSize))
log.Debug(string(respBody))
}

View File

@ -11,10 +11,15 @@ import (
ap "github.com/go-ap/activitypub"
)
func FederatedUserNew(name string, IRI ap.IRI) error {
func FederatedUserNew(IRI ap.IRI) error {
name, err := personIRIToName(IRI)
if err != nil {
return err
}
user := &user_model.User{
Name: name,
Email: name, // TODO: change this to something else to prevent collisions with normal users
Email: name, // TODO: change this to something else to prevent collisions with normal users, maybe fetch email using Gitea API
LoginType: auth.Federated,
Website: IRI.String(),
}

View File

@ -7,7 +7,6 @@ package activitypub
import (
"io"
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/forgefed"
@ -39,7 +38,7 @@ func Person(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/ActivityPub"
link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name
link := setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.Name
person := ap.PersonNew(ap.IRI(link))
person.Name = ap.NaturalLanguageValuesNew()
@ -66,15 +65,12 @@ 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)
publicKeyPem, err := activitypub.GetPublicKey(ctx.ContextUser)
if err != nil {
ctx.ServerError("GetPublicKey", err)
@ -109,10 +105,13 @@ func PersonInbox(ctx *context.APIContext) {
var activity ap.Activity
activity.UnmarshalJSON(body)
if activity.Type == ap.FollowType {
switch activity.Type {
case ap.FollowType:
activitypub.Follow(ctx, activity)
} else {
log.Warn("ActivityStreams type not supported", activity)
case ap.UndoType:
activitypub.Unfollow(ctx, activity)
default:
log.Debug("ActivityStreams type not supported", activity)
ctx.PlainText(http.StatusNotImplemented, "ActivityStreams type not supported")
return
}
@ -137,7 +136,7 @@ func PersonOutbox(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/ActivityPub"
link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name
link := setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.Name
feed, err := models.GetFeeds(ctx, models.GetFeedsOptions{
RequestedUser: ctx.ContextUser,
@ -186,7 +185,7 @@ func PersonFollowing(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/ActivityPub"
link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name
link := setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.Name
users, _, err := user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, utils.GetListOptions(ctx))
if err != nil {
@ -223,7 +222,7 @@ func PersonFollowers(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/ActivityPub"
link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name
link := setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.Name
users, _, err := user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, utils.GetListOptions(ctx))
if err != nil {
@ -235,6 +234,7 @@ func PersonFollowers(ctx *context.APIContext) {
followers.TotalItems = uint(len(users))
for _, user := range users {
// TODO: handle non-Federated users
person := ap.PersonNew(ap.IRI(user.Website))
followers.OrderedItems.Append(person)
}
@ -259,7 +259,7 @@ func PersonLiked(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/ActivityPub"
link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name
link := setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.Name
repos, count, err := repo_model.SearchRepository(&repo_model.SearchRepoOptions{
Actor: ctx.Doer,
@ -275,7 +275,8 @@ func PersonLiked(ctx *context.APIContext) {
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))
// TODO: Handle remote starred repos
repo := forgefed.RepositoryNew(ap.IRI(setting.AppURL + "api/v1/activitypub/repo/" + repo.OwnerName + "/" + repo.Name))
liked.OrderedItems.Append(repo)
}

View File

@ -7,21 +7,18 @@ 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/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"
"code.gitea.io/gitea/routers/api/v1/utils"
ap "github.com/go-ap/activitypub"
)
// Repo function
// Repo function returns the Repository actor of a repo
func Repo(ctx *context.APIContext) {
// swagger:operation GET /activitypub/repo/{username}/{reponame} activitypub activitypubRepo
// ---
@ -43,7 +40,7 @@ func Repo(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/ActivityPub"
link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/repo/" + ctx.ContextUser.Name + "/" + ctx.Repo.Repository.Name
link := setting.AppURL + "api/v1/activitypub/repo/" + ctx.ContextUser.Name + "/" + ctx.Repo.Repository.Name
repo := forgefed.RepositoryNew(ap.IRI(link))
repo.Name = ap.NaturalLanguageValuesNew()
@ -53,7 +50,7 @@ func Repo(ctx *context.APIContext) {
return
}
repo.AttributedTo = ap.IRI(strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name)
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))
@ -70,7 +67,7 @@ func Repo(ctx *context.APIContext) {
response(ctx, repo)
}
// RepoInbox function
// 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
// ---
@ -95,29 +92,63 @@ func RepoInbox(ctx *context.APIContext) {
body, err := io.ReadAll(ctx.Req.Body)
if err != nil {
ctx.ServerError("Error reading request body", err)
return
}
var activity ap.Activity
activity.UnmarshalJSON(body) // This function doesn't support ForgeFed types!!!
log.Warn("Debug", activity)
switch activity.Type {
case ap.NoteType:
// activitypub.Comment(ctx, activity)
var activity map[string]interface{}
err = json.Unmarshal(body, activity)
if err != nil {
ctx.ServerError("Unmarshal", err)
return
}
switch activity["type"].(ap.ActivityVocabularyType) {
case ap.CreateType:
// if activity.Object.GetType() == forgefed.RepositoryType {
// Fork created by remote instance
activitypub.ForkFromCreate(ctx, activity)
//}
case ap.MoveType:
// This should actually be forgefed.TicketType but that the UnmarshalJSON function above doesn't support ForgeFed!
activitypub.PullRequest(ctx, activity)
// Create activity, extract the object
object, ok := activity["object"].(map[string]interface{})
if ok {
ctx.ServerError("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.Warn("ActivityStreams type not supported", activity)
ctx.PlainText(http.StatusNotImplemented, "ActivityStreams type not supported")
return
}
ctx.Status(http.StatusNoContent)
}
// RepoOutbox function
// RepoOutbox function returns the repo's Outbox OrderedCollection
func RepoOutbox(ctx *context.APIContext) {
// swagger:operation GET /activitypub/repo/{username}/outbox activitypub activitypubPersonOutbox
// ---
@ -139,34 +170,11 @@ func RepoOutbox(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/ActivityPub"
link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/repo/" + ctx.ContextUser.Name + "/" + ctx.Repo.Repository.Name
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 + "/outbox"))
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)
// TODO
ctx.Status(http.StatusNotImplemented)
}
// RepoFollowers function
// RepoFollowers function returns the repo's Followers OrderedCollection
func RepoFollowers(ctx *context.APIContext) {
// swagger:operation GET /activitypub/repo/{username}/{reponame}/followers activitypub activitypubRepoFollowers
// ---
@ -188,21 +196,6 @@ func RepoFollowers(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/ActivityPub"
link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/repo/" + ctx.ContextUser.Name + "/" + ctx.Repo.Repository.Name
users, _, err := user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, utils.GetListOptions(ctx))
if err != nil {
ctx.ServerError("GetUserFollowers", err)
return
}
followers := ap.OrderedCollectionNew(ap.IRI(link + "/followers"))
followers.TotalItems = uint(len(users))
for _, user := range users {
person := ap.PersonNew(ap.IRI(user.Website))
followers.OrderedItems.Append(person)
}
response(ctx, followers)
// TODO
ctx.Status(http.StatusNotImplemented)
}

View File

@ -7,6 +7,7 @@ package activitypub
import (
"net/http"
"code.gitea.io/gitea/models/forgefed"
"code.gitea.io/gitea/modules/activitypub"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
@ -15,12 +16,18 @@ import (
"github.com/go-ap/jsonld"
)
// Respond with an ActivityStreams object
func response(ctx *context.APIContext, v interface{}) {
binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(v)
binary, err := jsonld.WithContext(
jsonld.IRI(ap.ActivityBaseURI),
jsonld.IRI(ap.SecurityContextURI),
jsonld.IRI(forgefed.ForgeFedNamespaceURI),
).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 {