This repository has been archived on 2024-01-11. You can view files and clone it, but cannot push or open issues or pull requests.
gitea/routers/api/v1/activitypub/person.go

343 lines
9.6 KiB
Go

// 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/db"
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/forgefed"
"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"
)
// Person function returns the Person actor for a user
func Person(ctx *context.APIContext) {
// swagger:operation GET /activitypub/user/{username} activitypub activitypubPerson
// ---
// summary: Returns the Person actor for a user
// produces:
// - application/activity+json
// parameters:
// - name: username
// in: path
// description: username of the user
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActivityPub"
link := setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.Name
person := ap.PersonNew(ap.IRI(link))
person.Name = ap.NaturalLanguageValuesNew()
err := person.Name.Set("en", ap.Content(ctx.ContextUser.FullName))
if err != nil {
ctx.ServerError("Set Name", err)
return
}
person.PreferredUsername = ap.NaturalLanguageValuesNew()
err = person.PreferredUsername.Set("en", ap.Content(ctx.ContextUser.Name))
if err != nil {
ctx.ServerError("Set PreferredUsername", err)
return
}
person.URL = ap.IRI(ctx.ContextUser.HTMLURL())
person.Location = ap.IRI(ctx.ContextUser.GetEmail())
person.Icon = ap.Image{
Type: ap.ImageType,
MediaType: "image/png",
URL: ap.IRI(ctx.ContextUser.AvatarFullLinkWithSize(2048)),
}
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)
return
}
person.PublicKey.PublicKeyPem = publicKeyPem
response(ctx, person)
}
// PersonInbox function handles the incoming data for a user inbox
func PersonInbox(ctx *context.APIContext) {
// swagger:operation POST /activitypub/user/{username}/inbox activitypub activitypubPersonInbox
// ---
// summary: Send to the inbox
// produces:
// - application/activity+json
// parameters:
// - name: username
// in: path
// description: username of the user
// type: string
// required: true
// 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)
return
}
var activity ap.Activity
err = activity.UnmarshalJSON(body)
if err != nil {
ctx.ServerError("UnmarshalJSON", err)
return
}
// Make sure keyID matches the user doing the activity
_, keyID, _ := getKeyID(ctx.Req)
if activity.Actor != nil && !strings.HasPrefix(keyID, activity.Actor.GetLink().String()) {
ctx.ServerError("Actor does not match HTTP signature keyID", nil)
return
}
if activity.AttributedTo != nil && !strings.HasPrefix(keyID, activity.AttributedTo.GetLink().String()) {
ctx.ServerError("AttributedTo does not match HTTP signature keyID", nil)
return
}
// TODO: Check activity.Object actor and attributedTo
// Process activity
switch activity.Type {
case ap.FollowType:
err = activitypub.Follow(ctx, activity)
case ap.UndoType:
err = activitypub.Unfollow(ctx, activity)
default:
log.Info("Incoming unsupported ActivityStreams type: %s", activity.GetType())
ctx.PlainText(http.StatusNotImplemented, "ActivityStreams type not supported")
return
}
if err != nil {
ctx.ServerError("Could not process activity", err)
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 := setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.Name
outbox := ap.OrderedCollectionNew(ap.IRI(link + "/outbox"))
feed, err := models.GetFeeds(ctx, models.GetFeedsOptions{
RequestedUser: ctx.ContextUser,
Actor: ctx.ContextUser,
IncludePrivate: false,
IncludeDeleted: false,
ListOptions: db.ListOptions{Page: 1, PageSize: 1000000},
})
if err != nil {
ctx.ServerError("Couldn't fetch feed", err)
return
}
for _, action := range feed {
if action.OpType == models.ActionCreateRepo {
// Created a repo
object := ap.Note{Type: ap.NoteType, Content: ap.NaturalLanguageValuesNew()}
object.Content.Set("en", ap.Content(action.GetRepoName()))
create := ap.Create{Type: ap.CreateType, Object: object}
err := outbox.OrderedItems.Append(create)
if err != nil {
ctx.ServerError("OrderedItems.Append", err)
return
}
}
}
stars, err := repo_model.GetStarredRepos(ctx.ContextUser.ID, false, db.ListOptions{Page: 1, PageSize: 1000000})
if err != nil {
ctx.ServerError("Couldn't fetch stars", err)
return
}
for _, star := range stars {
object := ap.Note{Type: ap.NoteType, Content: ap.NaturalLanguageValuesNew()}
object.Content.Set("en", ap.Content("Starred "+star.Name))
create := ap.Create{Type: ap.CreateType, Object: object}
err := outbox.OrderedItems.Append(create)
if err != nil {
ctx.ServerError("OrderedItems.Append", err)
return
}
}
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 := 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 {
ctx.ServerError("GetUserFollowing", err)
return
}
following := ap.OrderedCollectionNew(ap.IRI(link + "/following"))
following.TotalItems = uint(len(users))
for _, user := range users {
// TODO: handle non-Federated users
person := ap.PersonNew(ap.IRI(user.Website))
err := following.OrderedItems.Append(person)
if err != nil {
ctx.ServerError("OrderedItems.Append", err)
return
}
}
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 := 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 {
ctx.ServerError("GetUserFollowers", err)
return
}
followers := ap.OrderedCollectionNew(ap.IRI(link + "/followers"))
followers.TotalItems = uint(len(users))
for _, user := range users {
// TODO: handle non-Federated users
person := ap.PersonNew(ap.IRI(user.Website))
err := followers.OrderedItems.Append(person)
if err != nil {
ctx.ServerError("OrderedItems.Append", err)
return
}
}
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 := setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.Name
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"))
liked.TotalItems = uint(count)
for _, repo := range repos {
// TODO: Handle remote starred repos
repo := forgefed.RepositoryNew(ap.IRI(setting.AppURL + "api/v1/activitypub/repo/" + repo.OwnerName + "/" + repo.Name))
err := liked.OrderedItems.Append(repo)
if err != nil {
ctx.ServerError("OrderedItems.Append", err)
return
}
}
response(ctx, liked)
}