Compare commits
95 Commits
forgejo
...
forgejo-fe
Author | SHA1 | Date |
---|---|---|
Anthony Wang | a8be3ece4b | |
Anthony Wang | 439f6754ac | |
Anthony Wang | c64d3fa195 | |
Anthony Wang | f5a50ce457 | |
Anthony Wang | 3e690fbae2 | |
Anthony Wang | 77896f1a50 | |
Anthony Wang | 1066cfe785 | |
Anthony Wang | 3f5f626264 | |
Anthony Wang | a666eefe8f | |
Anthony Wang | c982b67626 | |
Anthony Wang | 2d74e4f555 | |
Anthony Wang | fd4d0e730e | |
Anthony Wang | 41e9a10763 | |
Anthony Wang | 447650f21c | |
Anthony Wang | d22dab748f | |
Anthony Wang | 19af0c9267 | |
Anthony Wang | 20e8c64317 | |
Anthony Wang | ca502244a0 | |
Anthony Wang | f75ab80b5c | |
Anthony Wang | 0cacdc37fb | |
Anthony Wang | 69c1bdddc7 | |
Anthony Wang | 5612130bcf | |
Anthony Wang | f133e9ca11 | |
Anthony Wang | e78dd699de | |
Anthony Wang | cbc2a970be | |
Anthony Wang | f9d9019720 | |
Anthony Wang | 379b9a7dce | |
Anthony Wang | 26f57be49c | |
Anthony Wang | 117463ba78 | |
Anthony Wang | f7dbbf73f6 | |
Anthony Wang | 47229ea208 | |
Anthony Wang | 42b1bac7a6 | |
Anthony Wang | f0269889c0 | |
Anthony Wang | ec1ffd66e3 | |
Gusted | 2e957e7ebb | |
Gusted | 45324e169f | |
Gusted | 2373b4177a | |
Gusted | 5ad0387fbd | |
Anthony Wang | 73284dbf0b | |
Anthony Wang | c0efdedaa9 | |
Anthony Wang | ee85f7d957 | |
Gusted | f1e61af242 | |
Gusted | 1bc8e67e9c | |
Gusted | 819e495dc0 | |
Gusted | b9dd4a2f5f | |
Gusted | 18809f811d | |
Anthony Wang | b3c065ce80 | |
Anthony Wang | 27cda2fcd4 | |
Anthony Wang | 6a6c6b3481 | |
Anthony Wang | 6b73c097ed | |
Anthony Wang | d945e6ac72 | |
Anthony Wang | 0b97c6aa69 | |
Anthony Wang | ecefb6a2d0 | |
Anthony Wang | fe8ef28bc2 | |
Anthony Wang | 71b2b4d815 | |
Anthony Wang | c94a891aad | |
Anthony Wang | 8e5621c9c3 | |
Anthony Wang | d909c97da9 | |
Anthony Wang | f0cded88bf | |
Anthony Wang | 38a687c60e | |
Anthony Wang | 30b431da49 | |
Anthony Wang | bffb682117 | |
Anthony Wang | ab540d07be | |
Anthony Wang | 5da6b4fd84 | |
Anthony Wang | 763f98b517 | |
Anthony Wang | 85abd9cfe0 | |
Anthony Wang | 5196dcd9a5 | |
Anthony Wang | c8a8e1ec91 | |
Anthony Wang | 6e100301cf | |
Anthony Wang | 0925235a96 | |
Anthony Wang | c100b8e1e0 | |
Anthony Wang | 08cb2d6d34 | |
Anthony Wang | 48deb8e1f5 | |
Anthony Wang | f1577c2f62 | |
Anthony Wang | 705706bc00 | |
Anthony Wang | b491a2ec34 | |
Anthony Wang | 1b4cd987b2 | |
Anthony Wang | 0609d7175c | |
Anthony Wang | 56717396fd | |
Anthony Wang | a63b2be21b | |
Anthony Wang | 63aa270a2e | |
Anthony Wang | 1b39e39fc1 | |
Anthony Wang | 79a59bd75b | |
Anthony Wang | d016dbbe70 | |
Anthony Wang | 8b354febf5 | |
Anthony Wang | 18b4cd32f3 | |
Anthony Wang | fa72294f64 | |
Anthony Wang | 721b734049 | |
Anthony Wang | 786ee03f57 | |
Anthony Wang | e348477c59 | |
dachary | 30a703c8ef | |
Anthony Wang | 24a462a95d | |
Anthony Wang | e090c95c17 | |
Anthony Wang | a7f32d3382 | |
Anthony Wang | d12fd434ba |
|
@ -0,0 +1,47 @@
|
|||
# Federation
|
||||
|
||||
*This describes Gitea's future federation capabilities, not what it can do currently.*
|
||||
|
||||
Gitea is federated using [ActivityPub](https://www.w3.org/TR/activitypub/) and the [ForgeFed extension](https://forgefed.org/) so you can interact with users and repositories from other instances as if they were on your own instance. By using the standardized ActivityPub protocol, users on any fediverse software such as [Mastodon](https://joinmastodon.org/) can follow Gitea users, star repositories, receive activity updates, and comment on issues.
|
||||
|
||||
C2S ActivityPub is not supported because Gitea already has an existing API.
|
||||
|
||||
## Following
|
||||
|
||||
You can use any fediverse software to follow a Gitea user. Gitea will automatically accept follow requests. The usernames of remote users are displayed as `username@instance.com`. To follow a remote user, click follow on their profile page, and a pop-up box will appear for you to type in your instance. You are redirected to your own instance, where the remote user is fetched and rendered, and you can now follow them.
|
||||
|
||||
When following a Gitea user, you will receive updates when they star a repo, create, fork, or make a private repo public, or follow a user. If you are using Mastodon or Pleroma, these will show up in your feed.
|
||||
|
||||
## Starring
|
||||
|
||||
You can star repositories on another instance. The full name of a remote repository is `username@instance.com/reponame`. Similar to following, a pop-up box appears for you to type in your instance, and you are redirected to your own instance, where the remote repository is fetched and rendered.
|
||||
|
||||
## Organizations
|
||||
|
||||
You can add users from other instances to organizations. An organization has a name and an instance, so its full name would look like `orgname@instance.com`. This indicates that the organization data resides on `instance.com`. To prevent synchronization errors, this data is only synchronized one-way to other instances.
|
||||
|
||||
## Collaborators
|
||||
|
||||
You can add users from other instances as collaborators. As mentioned previously, a repository has full name `username@instance.com/reponame`, which indicates that the repository data resides on `instance.com`. Each collaborator's instance has a copy of the repository, but to prevent synchronization errors, the copy at `instance.com` is the main copy and it is synchronized one-way to all other instances. When a collaborator tries to modify their copy of the repository, the modification is first sent to the main copy at `instance.com` and then synchronized back to their instance.
|
||||
|
||||
## Issues
|
||||
|
||||
You can create an issue on a remote repository. Your instance can also render a remote issue that you created so you can edit it or comment on it.
|
||||
|
||||
## Forks
|
||||
|
||||
When forking a remote repository, the fork is created on your instance, not the remote instance.
|
||||
|
||||
## Pull requests
|
||||
|
||||
When opening a pull request to a remote repository, the pull request can be rendered on your instance. Federated pull requests use the AGit-flow.
|
||||
|
||||
## Comments
|
||||
|
||||
You can comment on an issue or pull request using any fediverse software. The issue and existing comments are rendered on your instance.
|
||||
|
||||
## Migrations
|
||||
|
||||
If you change your username or the name of a repository, Gitea handles this similarly to how Mastodon does. Gitea will send a `Move` activity to your followers and update your actor to point to the new actor and the new actor to point to the old actor.
|
||||
|
||||
Changing your instance or a repository's instance is handled in a similar way, but additionally, the data to be migrated between instances.
|
|
@ -2308,7 +2308,7 @@ ROUTER = console
|
|||
;SHARE_USER_STATISTICS = true
|
||||
;;
|
||||
;; Maximum federation request and response size (MB)
|
||||
;MAX_SIZE = 4
|
||||
;MAX_SIZE = 8
|
||||
;;
|
||||
;; WARNING: Changing the settings below can break federation.
|
||||
;;
|
||||
|
|
8
go.mod
8
go.mod
|
@ -32,7 +32,7 @@ require (
|
|||
github.com/fsnotify/fsnotify v1.5.4
|
||||
github.com/gliderlabs/ssh v0.3.5
|
||||
github.com/go-ap/activitypub v0.0.0-20220917143152-e4e7018838c0
|
||||
github.com/go-ap/jsonld v0.0.0-20220917142617-76bf51585778
|
||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-enry/go-enry/v2 v2.8.3
|
||||
|
@ -87,6 +87,7 @@ require (
|
|||
github.com/tstranex/u2f v1.0.0
|
||||
github.com/unrolled/render v1.5.0
|
||||
github.com/urfave/cli v1.22.10
|
||||
github.com/valyala/fastjson v1.6.3
|
||||
github.com/xanzy/go-gitlab v0.73.1
|
||||
github.com/yohcop/openid-go v1.0.0
|
||||
github.com/yuin/goldmark v1.5.2
|
||||
|
@ -168,7 +169,7 @@ require (
|
|||
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
|
||||
github.com/fullstorydev/grpcurl v1.8.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||
github.com/go-ap/errors v0.0.0-20220917143055-4283ea5dae18 // indirect
|
||||
github.com/go-ap/errors v0.0.0-20221115052505-8aaa26f930b4 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/go-enry/go-oniguruma v1.2.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
|
@ -264,7 +265,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.2 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
|
@ -302,6 +302,8 @@ replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142
|
|||
|
||||
replace github.com/satori/go.uuid v1.2.0 => github.com/gofrs/uuid v4.2.0+incompatible
|
||||
|
||||
replace github.com/go-ap/activitypub => gitea.com/xy/activitypub v0.0.0-20221126171442-81405e14ea3b
|
||||
|
||||
exclude github.com/gofrs/uuid v3.2.0+incompatible
|
||||
|
||||
exclude github.com/gofrs/uuid v4.0.0+incompatible
|
||||
|
|
12
go.sum
12
go.sum
|
@ -96,6 +96,8 @@ gitea.com/lunny/levelqueue v0.4.2-0.20220729054728-f020868cc2f7 h1:Zc3RQWC2xOVgl
|
|||
gitea.com/lunny/levelqueue v0.4.2-0.20220729054728-f020868cc2f7/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||
gitea.com/xy/activitypub v0.0.0-20221126171442-81405e14ea3b h1:z5zmwZVoKEu2c3+lGiLlTDxQZpcKlZoWz4wjCtcyfxU=
|
||||
gitea.com/xy/activitypub v0.0.0-20221126171442-81405e14ea3b/go.mod h1:1jG7QyKCGx/FO63p/xWO0h9ytVSJmkjcQSYPj6zWpGs=
|
||||
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
|
||||
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 h1:r3qt8PCHnfjOv9PN3H+XXKmDA1dfFMIN1AislhlA/ps=
|
||||
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121/go.mod h1:Ock8XgA7pvULhIaHGAk/cDnRfNrF9Jey81nPcc403iU=
|
||||
|
@ -471,12 +473,10 @@ github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
|||
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||
github.com/go-ap/activitypub v0.0.0-20220917143152-e4e7018838c0 h1:EUMB0x7u3de/ikGBtXiLxaJbmxgiqiAcM4yjW4whApM=
|
||||
github.com/go-ap/activitypub v0.0.0-20220917143152-e4e7018838c0/go.mod h1:OX9ajs2vU4UauC/DlghS/8M468Kn79r+y9kB6j7LuGM=
|
||||
github.com/go-ap/errors v0.0.0-20220917143055-4283ea5dae18 h1:A48SbkWKEciiJMbbcPzaRj9aizPUABzXFvCM3LtGGf8=
|
||||
github.com/go-ap/errors v0.0.0-20220917143055-4283ea5dae18/go.mod h1:dd3ZgjjloBsKPDpqA2kf2VWhF0A1eKUItOBh0/QcDWI=
|
||||
github.com/go-ap/jsonld v0.0.0-20220917142617-76bf51585778 h1:0tV3i8tE1NghMC4rXZXfD39KUbkKgIyLTsvOEmMOPCQ=
|
||||
github.com/go-ap/jsonld v0.0.0-20220917142617-76bf51585778/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA=
|
||||
github.com/go-ap/errors v0.0.0-20221115052505-8aaa26f930b4 h1:oySiT87Q2cd0o5O8er2zyjiRcTQA0KuOgw1N9+RQqG0=
|
||||
github.com/go-ap/errors v0.0.0-20221115052505-8aaa26f930b4/go.mod h1:SaTNjEEkp0q+w3pUS1ccyEL/lUrHteORlDq/e21mCc8=
|
||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw=
|
||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
|
|
|
@ -329,14 +329,15 @@ func (a *Action) GetIssueContent() string {
|
|||
// GetFeedsOptions options for retrieving feeds
|
||||
type GetFeedsOptions struct {
|
||||
db.ListOptions
|
||||
RequestedUser *user_model.User // the user we want activity for
|
||||
RequestedTeam *organization.Team // the team we want activity for
|
||||
RequestedRepo *repo_model.Repository // the repo we want activity for
|
||||
Actor *user_model.User // the user viewing the activity
|
||||
IncludePrivate bool // include private actions
|
||||
OnlyPerformedBy bool // only actions performed by requested user
|
||||
IncludeDeleted bool // include deleted actions
|
||||
Date string // the day we want activity for: YYYY-MM-DD
|
||||
RequestedUser *user_model.User // the user we want activity for
|
||||
RequestedTeam *organization.Team // the team we want activity for
|
||||
RequestedRepo *repo_model.Repository // the repo we want activity for
|
||||
RequestedActionType ActionType // the type of activity we want
|
||||
Actor *user_model.User // the user viewing the activity
|
||||
IncludePrivate bool // include private actions
|
||||
OnlyPerformedBy bool // only actions performed by requested user
|
||||
IncludeDeleted bool // include deleted actions
|
||||
Date string // the day we want activity for: YYYY-MM-DD
|
||||
}
|
||||
|
||||
// GetFeeds returns actions according to the provided options
|
||||
|
@ -448,6 +449,10 @@ func activityQueryCondition(opts GetFeedsOptions) (builder.Cond, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if opts.RequestedActionType != 0 {
|
||||
cond = cond.And(builder.Eq{"`action`.op_type": opts.RequestedActionType})
|
||||
}
|
||||
|
||||
return cond, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issue_model "code.gitea.io/gitea/models/issues"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
@ -31,7 +31,7 @@ func TestAction_GetRepoLink(t *testing.T) {
|
|||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issue_model.Comment{ID: 2})
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
|
||||
action := &activities_model.Action{RepoID: repo.ID, CommentID: comment.ID}
|
||||
setting.AppSubURL = "/suburl"
|
||||
expected := path.Join(setting.AppSubURL, owner.Name, repo.Name)
|
||||
|
|
|
@ -23,14 +23,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
|
||||
|
@ -179,6 +180,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)
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
@ -1549,3 +1550,18 @@ func FixCommentTypeLabelWithOutsideLabels() (int64, error) {
|
|||
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
func (c *Comment) GetIRI() string {
|
||||
err := c.LoadIssue()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
err = c.Issue.LoadRepo(db.DefaultContext)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if strings.Contains(c.Issue.Repo.OwnerName, "@") {
|
||||
return c.OldTitle
|
||||
}
|
||||
return setting.AppURL + "api/v1/activitypub/note/" + c.Issue.Repo.OwnerName + "/" + c.Issue.Repo.Name + "/" + strconv.FormatInt(c.Issue.Index, 10) + "/" + strconv.FormatInt(c.ID, 10)
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
@ -2471,3 +2472,14 @@ func DeleteOrphanedIssues() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issue *Issue) GetIRI() string {
|
||||
err := issue.LoadRepo(db.DefaultContext)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("loadRepo: %v", err))
|
||||
}
|
||||
if strings.Contains(issue.Repo.OwnerName, "@") {
|
||||
return issue.OriginalAuthor
|
||||
}
|
||||
return setting.AppURL + "api/v1/activitypub/ticket/" + issue.Repo.OwnerName + "/" + issue.Repo.Name + "/" + strconv.FormatInt(issue.Index, 10)
|
||||
}
|
||||
|
|
|
@ -802,3 +802,10 @@ func FixNullArchivedRepository() (int64, error) {
|
|||
IsArchived: false,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) GetIRI() string {
|
||||
if strings.Contains(r.OwnerName, "@") {
|
||||
return r.OriginalURL
|
||||
}
|
||||
return setting.AppURL + "api/v1/activitypub/repo/" + r.OwnerName + "/" + r.Name
|
||||
}
|
||||
|
|
|
@ -105,6 +105,15 @@ func (u *User) AvatarLink() string {
|
|||
return link
|
||||
}
|
||||
|
||||
// AvatarFullLinkWithSize returns the full avatar link with size and http host
|
||||
func (u *User) AvatarFullLinkWithSize(size int) string {
|
||||
link := u.AvatarLinkWithSize(size)
|
||||
if !strings.HasPrefix(link, "//") && !strings.Contains(link, "://") {
|
||||
return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL+"/")
|
||||
}
|
||||
return link
|
||||
}
|
||||
|
||||
// IsUploadAvatarChanged returns true if the current user's avatar would be changed with the provided data
|
||||
func (u *User) IsUploadAvatarChanged(data []byte) bool {
|
||||
if !u.UseCustomAvatar || len(u.Avatar) == 0 {
|
||||
|
|
|
@ -1339,3 +1339,10 @@ func GetOrderByName() string {
|
|||
}
|
||||
return "name"
|
||||
}
|
||||
|
||||
func (u *User) GetIRI() string {
|
||||
if u.LoginType == auth.Federated {
|
||||
return u.LoginName
|
||||
}
|
||||
return setting.AppURL + "api/v1/activitypub/user/" + u.Name
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
// 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 (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
const (
|
||||
BranchType ap.ActivityVocabularyType = "Branch"
|
||||
)
|
||||
|
||||
type Branch struct {
|
||||
ap.Object
|
||||
// Ref the unique identifier of the branch within the repo
|
||||
Ref ap.Item `jsonld:"ref,omitempty"`
|
||||
}
|
||||
|
||||
// BranchNew initializes a Branch type Object
|
||||
func BranchNew() *Branch {
|
||||
a := ap.ObjectNew(BranchType)
|
||||
o := Branch{Object: *a}
|
||||
return &o
|
||||
}
|
||||
|
||||
func (b Branch) MarshalJSON() ([]byte, error) {
|
||||
bin, err := b.Object.MarshalJSON()
|
||||
if len(bin) == 0 || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bin = bin[:len(bin)-1]
|
||||
if b.Ref != nil {
|
||||
ap.JSONWriteItemJSONProp(&bin, "ref", b.Ref)
|
||||
}
|
||||
ap.JSONWrite(&bin, '}')
|
||||
return bin, nil
|
||||
}
|
||||
|
||||
func JSONLoadBranch(val *fastjson.Value, b *Branch) error {
|
||||
if err := ap.OnObject(&b.Object, func(o *ap.Object) error {
|
||||
return ap.JSONLoadObject(val, o)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Ref = ap.JSONGetItem(val, "ref")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Branch) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadBranch(val, b)
|
||||
}
|
||||
|
||||
// ToBranch tries to convert the it Item to a Branch object.
|
||||
func ToBranch(it ap.Item) (*Branch, error) {
|
||||
switch i := it.(type) {
|
||||
case *Branch:
|
||||
return i, nil
|
||||
case Branch:
|
||||
return &i, nil
|
||||
case *ap.Object:
|
||||
return (*Branch)(unsafe.Pointer(i)), nil
|
||||
case ap.Object:
|
||||
return (*Branch)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||
typ := reflect.TypeOf(new(Branch))
|
||||
if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Branch); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
return nil, ap.ErrorInvalidType[ap.Object](it)
|
||||
}
|
||||
|
||||
type withBranchFn func(*Branch) error
|
||||
|
||||
// OnBranch calls function fn on it Item if it can be asserted to type *Branch
|
||||
func OnBranch(it ap.Item, fn withBranchFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
ob, err := ToBranch(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
// 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 (
|
||||
"reflect"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
const (
|
||||
CommitType ap.ActivityVocabularyType = "Commit"
|
||||
)
|
||||
|
||||
type Commit struct {
|
||||
ap.Object
|
||||
// Created time at which the commit was written by its author
|
||||
Created time.Time `jsonld:"created,omitempty"`
|
||||
// Committed time at which the commit was committed by its committer
|
||||
Committed time.Time `jsonld:"committed,omitempty"`
|
||||
}
|
||||
|
||||
// CommitNew initializes a Commit type Object
|
||||
func CommitNew() *Commit {
|
||||
a := ap.ObjectNew(CommitType)
|
||||
o := Commit{Object: *a}
|
||||
return &o
|
||||
}
|
||||
|
||||
func (c Commit) MarshalJSON() ([]byte, error) {
|
||||
b, err := c.Object.MarshalJSON()
|
||||
if len(b) == 0 || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b = b[:len(b)-1]
|
||||
if !c.Created.IsZero() {
|
||||
ap.JSONWriteTimeJSONProp(&b, "created", c.Created)
|
||||
}
|
||||
if !c.Committed.IsZero() {
|
||||
ap.JSONWriteTimeJSONProp(&b, "committed", c.Committed)
|
||||
}
|
||||
ap.JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func JSONLoadCommit(val *fastjson.Value, c *Commit) error {
|
||||
if err := ap.OnObject(&c.Object, func(o *ap.Object) error {
|
||||
return ap.JSONLoadObject(val, o)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Created = ap.JSONGetTime(val, "created")
|
||||
c.Committed = ap.JSONGetTime(val, "committed")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commit) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadCommit(val, c)
|
||||
}
|
||||
|
||||
// ToCommit tries to convert the it Item to a Commit object.
|
||||
func ToCommit(it ap.Item) (*Commit, error) {
|
||||
switch i := it.(type) {
|
||||
case *Commit:
|
||||
return i, nil
|
||||
case Commit:
|
||||
return &i, nil
|
||||
case *ap.Object:
|
||||
return (*Commit)(unsafe.Pointer(i)), nil
|
||||
case ap.Object:
|
||||
return (*Commit)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||
typ := reflect.TypeOf(new(Commit))
|
||||
if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Commit); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
return nil, ap.ErrorInvalidType[ap.Object](it)
|
||||
}
|
||||
|
||||
type withCommitFn func(*Commit) error
|
||||
|
||||
// OnCommit calls function fn on it Item if it can be asserted to type *Commit
|
||||
func OnCommit(it ap.Item, fn withCommitFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
ob, err := ToCommit(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
// 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 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) {
|
||||
switch typ {
|
||||
case CommitType:
|
||||
return CommitNew(), nil
|
||||
case BranchType:
|
||||
return BranchNew(), nil
|
||||
case RepositoryType:
|
||||
return RepositoryNew(""), nil
|
||||
case PushType:
|
||||
return PushNew(), nil
|
||||
case TicketType:
|
||||
return TicketNew(), nil
|
||||
}
|
||||
return ap.GetItemByType(typ)
|
||||
}
|
||||
|
||||
// JSONUnmarshalerFn is the function that will load the data from a fastjson.Value into an Item
|
||||
// that the go-ap/activitypub package doesn't know about.
|
||||
func JSONUnmarshalerFn(typ ap.ActivityVocabularyType, val *fastjson.Value, i ap.Item) error {
|
||||
switch typ {
|
||||
case CommitType:
|
||||
return OnCommit(i, func(c *Commit) error {
|
||||
return JSONLoadCommit(val, c)
|
||||
})
|
||||
case BranchType:
|
||||
return OnBranch(i, func(b *Branch) error {
|
||||
return JSONLoadBranch(val, b)
|
||||
})
|
||||
case RepositoryType:
|
||||
return OnRepository(i, func(r *Repository) error {
|
||||
return JSONLoadRepository(val, r)
|
||||
})
|
||||
case PushType:
|
||||
return OnPush(i, func(p *Push) error {
|
||||
return JSONLoadPush(val, p)
|
||||
})
|
||||
case TicketType:
|
||||
return OnTicket(i, func(t *Ticket) error {
|
||||
return JSONLoadTicket(val, t)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NotEmpty is the function that checks if an object is empty
|
||||
func NotEmpty(i ap.Item) bool {
|
||||
if ap.IsNil(i) {
|
||||
return false
|
||||
}
|
||||
switch i.GetType() {
|
||||
case CommitType:
|
||||
c, err := ToCommit(i)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ap.NotEmpty(c.Object)
|
||||
case BranchType:
|
||||
b, err := ToBranch(i)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ap.NotEmpty(b.Object)
|
||||
case RepositoryType:
|
||||
r, err := ToRepository(i)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ap.NotEmpty(r.Actor)
|
||||
case PushType:
|
||||
p, err := ToPush(i)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ap.NotEmpty(p.Object)
|
||||
case TicketType:
|
||||
t, err := ToTicket(i)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ap.NotEmpty(t.Object)
|
||||
}
|
||||
return ap.NotEmpty(i)
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
// 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 (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
const (
|
||||
PushType ap.ActivityVocabularyType = "Push"
|
||||
)
|
||||
|
||||
type Push struct {
|
||||
ap.Object
|
||||
// Target the specific repo history tip onto which the commits were added
|
||||
Target ap.Item `jsonld:"target,omitempty"`
|
||||
// HashBefore hash before adding the new commits
|
||||
HashBefore ap.Item `jsonld:"hashBefore,omitempty"`
|
||||
// HashAfter hash before adding the new commits
|
||||
HashAfter ap.Item `jsonld:"hashAfter,omitempty"`
|
||||
}
|
||||
|
||||
// PushNew initializes a Push type Object
|
||||
func PushNew() *Push {
|
||||
a := ap.ObjectNew(PushType)
|
||||
o := Push{Object: *a}
|
||||
return &o
|
||||
}
|
||||
|
||||
func (p Push) MarshalJSON() ([]byte, error) {
|
||||
b, err := p.Object.MarshalJSON()
|
||||
if len(b) == 0 || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b = b[:len(b)-1]
|
||||
if p.Target != nil {
|
||||
ap.JSONWriteItemJSONProp(&b, "target", p.Target)
|
||||
}
|
||||
if p.HashBefore != nil {
|
||||
ap.JSONWriteItemJSONProp(&b, "hashBefore", p.HashBefore)
|
||||
}
|
||||
if p.HashAfter != nil {
|
||||
ap.JSONWriteItemJSONProp(&b, "hashAfter", p.HashAfter)
|
||||
}
|
||||
ap.JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func JSONLoadPush(val *fastjson.Value, p *Push) error {
|
||||
if err := ap.OnObject(&p.Object, func(o *ap.Object) error {
|
||||
return ap.JSONLoadObject(val, o)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Target = ap.JSONGetItem(val, "target")
|
||||
p.HashBefore = ap.JSONGetItem(val, "hashBefore")
|
||||
p.HashAfter = ap.JSONGetItem(val, "hashAfter")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Push) UnmarshalJSON(data []byte) error {
|
||||
par := fastjson.Parser{}
|
||||
val, err := par.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadPush(val, p)
|
||||
}
|
||||
|
||||
// ToPush tries to convert the it Item to a Push object.
|
||||
func ToPush(it ap.Item) (*Push, error) {
|
||||
switch i := it.(type) {
|
||||
case *Push:
|
||||
return i, nil
|
||||
case Push:
|
||||
return &i, nil
|
||||
case *ap.Object:
|
||||
return (*Push)(unsafe.Pointer(i)), nil
|
||||
case ap.Object:
|
||||
return (*Push)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||
typ := reflect.TypeOf(new(Push))
|
||||
if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Push); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
return nil, ap.ErrorInvalidType[ap.Object](it)
|
||||
}
|
||||
|
||||
type withPushFn func(*Push) error
|
||||
|
||||
// OnPush calls function fn on it Item if it can be asserted to type *Push
|
||||
func OnPush(it ap.Item, fn withPushFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
ob, err := ToPush(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
// 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 (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
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"`
|
||||
// ForkedFrom Identifies the repository which this repository was created as a fork
|
||||
ForkedFrom ap.Item `jsonld:"forkedFrom,omitempty"`
|
||||
}
|
||||
|
||||
// RepositoryNew initializes a Repository type actor
|
||||
func RepositoryNew(id ap.ID) *Repository {
|
||||
a := ap.ActorNew(id, RepositoryType)
|
||||
a.Type = RepositoryType
|
||||
o := Repository{Actor: *a}
|
||||
return &o
|
||||
}
|
||||
|
||||
func (r Repository) MarshalJSON() ([]byte, error) {
|
||||
b, err := r.Actor.MarshalJSON()
|
||||
if len(b) == 0 || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b = b[:len(b)-1]
|
||||
if r.Team != nil {
|
||||
ap.JSONWriteItemJSONProp(&b, "team", r.Team)
|
||||
}
|
||||
if r.Forks != nil {
|
||||
ap.JSONWriteItemJSONProp(&b, "forks", r.Forks)
|
||||
}
|
||||
if r.ForkedFrom != nil {
|
||||
ap.JSONWriteItemJSONProp(&b, "forkedFrom", r.ForkedFrom)
|
||||
}
|
||||
ap.JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func JSONLoadRepository(val *fastjson.Value, r *Repository) error {
|
||||
if err := ap.OnActor(&r.Actor, func(a *ap.Actor) error {
|
||||
return ap.JSONLoadActor(val, a)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Team = ap.JSONGetItem(val, "team")
|
||||
r.Forks = ap.JSONGetItem(val, "forks")
|
||||
r.ForkedFrom = ap.JSONGetItem(val, "forkedFrom")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadRepository(val, r)
|
||||
}
|
||||
|
||||
// ToRepository tries to convert the it Item to a Repository Actor.
|
||||
func ToRepository(it ap.Item) (*Repository, error) {
|
||||
switch i := it.(type) {
|
||||
case *Repository:
|
||||
return i, nil
|
||||
case Repository:
|
||||
return &i, nil
|
||||
case *ap.Actor:
|
||||
return (*Repository)(unsafe.Pointer(i)), nil
|
||||
case ap.Actor:
|
||||
return (*Repository)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||
typ := reflect.TypeOf(new(Repository))
|
||||
if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Repository); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
return nil, ap.ErrorInvalidType[ap.Actor](it)
|
||||
}
|
||||
|
||||
type withRepositoryFn func(*Repository) error
|
||||
|
||||
// OnRepository calls function fn on it Item if it can be asserted to type *Repository
|
||||
func OnRepository(it ap.Item, fn withRepositoryFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
ob, err := ToRepository(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
|
||||
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"),
|
||||
Actor: ap.Actor{
|
||||
ID: "https://example.com/1",
|
||||
},
|
||||
},
|
||||
want: []byte(`{"id":"https://example.com/1","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"),
|
||||
},
|
||||
Actor: ap.Actor{
|
||||
ID: "https://example.com/1",
|
||||
},
|
||||
},
|
||||
want: []byte(`{"id":"https://example.com/1","team":["https://example.com/1","https://example.com/2"]}`),
|
||||
},
|
||||
"with Team as Object": {
|
||||
item: Repository{
|
||||
Team: ap.Object{ID: "https://example.com/1"},
|
||||
Actor: ap.Actor{
|
||||
ID: "https://example.com/1",
|
||||
},
|
||||
},
|
||||
want: []byte(`{"id":"https://example.com/1","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"},
|
||||
},
|
||||
Actor: ap.Actor{
|
||||
ID: "https://example.com/1",
|
||||
},
|
||||
},
|
||||
want: []byte(`{"id":"https://example.com/1","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,134 @@
|
|||
// 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 (
|
||||
"reflect"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
const (
|
||||
TicketType ap.ActivityVocabularyType = "Ticket"
|
||||
)
|
||||
|
||||
type Ticket struct {
|
||||
ap.Object
|
||||
// Dependants Collection of Tickets which depend on this ticket
|
||||
Dependants ap.ItemCollection `jsonld:"dependants,omitempty"`
|
||||
// Dependencies Collection of Tickets on which this ticket depends
|
||||
Dependencies ap.ItemCollection `jsonld:"dependencies,omitempty"`
|
||||
// IsResolved Whether the work on this ticket is done
|
||||
IsResolved bool `jsonld:"isResolved,omitempty"`
|
||||
// ResolvedBy If the work on this ticket is done, who marked the ticket as resolved, or which activity did so
|
||||
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
|
||||
func TicketNew() *Ticket {
|
||||
a := ap.ObjectNew(TicketType)
|
||||
o := Ticket{Object: *a}
|
||||
return &o
|
||||
}
|
||||
|
||||
func (t Ticket) MarshalJSON() ([]byte, error) {
|
||||
b, err := t.Object.MarshalJSON()
|
||||
if len(b) == 0 || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b = b[:len(b)-1]
|
||||
if t.Dependants != nil {
|
||||
ap.JSONWriteItemCollectionJSONProp(&b, "dependants", t.Dependants)
|
||||
}
|
||||
if t.Dependencies != nil {
|
||||
ap.JSONWriteItemCollectionJSONProp(&b, "dependencies", t.Dependencies)
|
||||
}
|
||||
ap.JSONWriteBoolJSONProp(&b, "isResolved", t.IsResolved)
|
||||
if t.ResolvedBy != nil {
|
||||
ap.JSONWriteItemJSONProp(&b, "resolvedBy", t.ResolvedBy)
|
||||
}
|
||||
if !t.Resolved.IsZero() {
|
||||
ap.JSONWriteTimeJSONProp(&b, "resolved", t.Resolved)
|
||||
}
|
||||
if t.Origin != nil {
|
||||
ap.JSONWriteItemJSONProp(&b, "origin", t.Origin)
|
||||
}
|
||||
if t.Target != nil {
|
||||
ap.JSONWriteItemJSONProp(&b, "target", t.Target)
|
||||
}
|
||||
ap.JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func JSONLoadTicket(val *fastjson.Value, t *Ticket) error {
|
||||
if err := ap.OnObject(&t.Object, func(o *ap.Object) error {
|
||||
return ap.JSONLoadObject(val, o)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Dependants = ap.JSONGetItems(val, "dependants")
|
||||
t.Dependencies = ap.JSONGetItems(val, "dependencies")
|
||||
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 nil
|
||||
}
|
||||
|
||||
func (t *Ticket) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadTicket(val, t)
|
||||
}
|
||||
|
||||
// ToTicket tries to convert the it Item to a Ticket object.
|
||||
func ToTicket(it ap.Item) (*Ticket, error) {
|
||||
switch i := it.(type) {
|
||||
case *Ticket:
|
||||
return i, nil
|
||||
case Ticket:
|
||||
return &i, nil
|
||||
case *ap.Object:
|
||||
return (*Ticket)(unsafe.Pointer(i)), nil
|
||||
case ap.Object:
|
||||
return (*Ticket)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||
typ := reflect.TypeOf(new(Ticket))
|
||||
if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Ticket); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
return nil, ap.ErrorInvalidType[ap.Object](it)
|
||||
}
|
||||
|
||||
type withTicketFn func(*Ticket) error
|
||||
|
||||
// OnTicket calls function fn on it Item if it can be asserted to type *Ticket
|
||||
func OnTicket(it ap.Item, fn withTicketFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
ob, err := ToTicket(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
|
@ -23,7 +23,7 @@ var (
|
|||
}{
|
||||
Enabled: false,
|
||||
ShareUserStatistics: true,
|
||||
MaxSize: 4,
|
||||
MaxSize: 8,
|
||||
Algorithms: []string{"rsa-sha256", "rsa-sha512", "ed25519"},
|
||||
DigestAlgorithm: "SHA-256",
|
||||
GetHeaders: []string{"(request-target)", "Date"},
|
||||
|
|
|
@ -93,7 +93,7 @@ func IsValidExternalTrackerURLFormat(uri string) bool {
|
|||
}
|
||||
|
||||
var (
|
||||
validUsernamePattern = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`)
|
||||
validUsernamePattern = regexp.MustCompile(`^[\da-zA-Z][-.\w@]*$`)
|
||||
invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`) // No consecutive or trailing non-alphanumeric chars
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
// 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"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/forgefed"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/activitypub"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
// Fetch and load a remote object
|
||||
func AuthorizeInteraction(ctx *context.Context) {
|
||||
uri, err := url.Parse(ctx.Req.URL.Query().Get("uri"))
|
||||
if err != nil {
|
||||
ctx.ServerError("Parse URI", err)
|
||||
return
|
||||
}
|
||||
resp, err := activitypub.Fetch(uri)
|
||||
if err != nil {
|
||||
ctx.ServerError("Fetch", err)
|
||||
return
|
||||
}
|
||||
|
||||
ap.ItemTyperFunc = forgefed.GetItemByType
|
||||
ap.JSONItemUnmarshal = forgefed.JSONUnmarshalerFn
|
||||
ap.NotEmptyChecker = forgefed.NotEmpty
|
||||
object, err := ap.UnmarshalJSON(resp)
|
||||
if err != nil {
|
||||
ctx.ServerError("UnmarshalJSON", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch object.GetType() {
|
||||
case ap.PersonType:
|
||||
// Federated user
|
||||
person, err := ap.ToActor(object)
|
||||
if err != nil {
|
||||
ctx.ServerError("ToActor", err)
|
||||
return
|
||||
}
|
||||
err = createPerson(ctx, person)
|
||||
if err != nil {
|
||||
ctx.ServerError("FederatedUserNew", err)
|
||||
return
|
||||
}
|
||||
name, err := activitypub.PersonIRIToName(object.GetLink())
|
||||
if err != nil {
|
||||
ctx.ServerError("PersonIRIToName", err)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(setting.AppURL + name)
|
||||
case forgefed.RepositoryType:
|
||||
// Federated repository
|
||||
err = forgefed.OnRepository(object, func(r *forgefed.Repository) error {
|
||||
return createRepository(ctx, r)
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FederatedRepoNew", err)
|
||||
return
|
||||
}
|
||||
username, reponame, err := activitypub.RepositoryIRIToName(object.GetLink())
|
||||
if err != nil {
|
||||
ctx.ServerError("RepositoryIRIToName", err)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(setting.AppURL + username + "/" + reponame)
|
||||
case forgefed.TicketType:
|
||||
// Federated issue or pull request
|
||||
err = forgefed.OnTicket(object, func(t *forgefed.Ticket) error {
|
||||
return createTicket(ctx, t)
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("ReceiveIssue", err)
|
||||
return
|
||||
}
|
||||
username, reponame, idx, err := activitypub.TicketIRIToName(object.GetLink())
|
||||
if err != nil {
|
||||
ctx.ServerError("TicketIRIToName", err)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(setting.AppURL + username + "/" + reponame + "/issues/" + strconv.FormatInt(idx, 10))
|
||||
default:
|
||||
ctx.ServerError("Not implemented", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
|
@ -0,0 +1,319 @@
|
|||
// 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"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
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/forgefed"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/activitypub"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
// Create a new federated user from a Person object
|
||||
func createPerson(ctx context.Context, person *ap.Person) error {
|
||||
name, err := activitypub.PersonIRIToName(person.GetLink())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exists, err := user_model.IsUserExist(ctx, 0, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
var email string
|
||||
if person.Location != nil {
|
||||
email = person.Location.GetLink().String()
|
||||
} else {
|
||||
// This might not even work
|
||||
email = strings.ReplaceAll(name, "@", "+") + "@" + setting.Service.NoReplyAddress
|
||||
}
|
||||
|
||||
if person.PublicKey.PublicKeyPem == "" {
|
||||
return errors.New("person public key not found")
|
||||
}
|
||||
|
||||
user := &user_model.User{
|
||||
Name: name,
|
||||
FullName: person.Name.String(), // May not exist!!
|
||||
Email: email,
|
||||
LoginType: auth.Federated,
|
||||
LoginName: person.GetLink().String(),
|
||||
EmailNotificationsPreference: user_model.EmailNotificationsDisabled,
|
||||
}
|
||||
err = user_model.CreateUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if person.Icon != nil {
|
||||
icon, err := ap.ToObject(person.Icon)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iconURL, err := icon.URL.GetLink().URL()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := activitypub.Fetch(iconURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = user_service.UploadAvatar(user, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = user_model.SetUserSetting(user.ID, user_model.UserActivityPubPrivPem, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return user_model.SetUserSetting(user.ID, user_model.UserActivityPubPubPem, person.PublicKey.PublicKeyPem)
|
||||
}
|
||||
|
||||
func createPersonFromIRI(ctx context.Context, personIRI ap.IRI) error {
|
||||
ownerURL, err := url.Parse(personIRI.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Fetch person object
|
||||
resp, err := activitypub.Fetch(ownerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse person object
|
||||
ap.ItemTyperFunc = forgefed.GetItemByType
|
||||
ap.JSONItemUnmarshal = forgefed.JSONUnmarshalerFn
|
||||
ap.NotEmptyChecker = forgefed.NotEmpty
|
||||
object, err := ap.UnmarshalJSON(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create federated user
|
||||
person, err := ap.ToActor(object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return createPerson(ctx, person)
|
||||
}
|
||||
|
||||
// Create a new federated repo from a Repository object
|
||||
func createRepository(ctx context.Context, repository *forgefed.Repository) error {
|
||||
err := createPersonFromIRI(ctx, repository.AttributedTo.GetLink())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := activitypub.PersonIRIToUser(ctx, repository.AttributedTo.GetLink())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if repo exists
|
||||
_, err = repo_model.GetRepositoryByOwnerAndNameCtx(ctx, user.Name, repository.Name.String())
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
repo, err := repo_service.CreateRepository(user, user, repo_module.CreateRepoOptions{
|
||||
Name: repository.Name.String(),
|
||||
OriginalURL: repository.GetLink().String(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if repository.ForkedFrom != nil {
|
||||
repo.IsFork = true
|
||||
forkedFrom, err := activitypub.RepositoryIRIToRepository(ctx, repository.ForkedFrom.GetLink())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo.ForkID = forkedFrom.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createRepositoryFromIRI(ctx context.Context, repoIRI ap.IRI) error {
|
||||
repoURL, err := url.Parse(repoIRI.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Fetch repository object
|
||||
resp, err := activitypub.Fetch(repoURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse repository object
|
||||
ap.ItemTyperFunc = forgefed.GetItemByType
|
||||
ap.JSONItemUnmarshal = forgefed.JSONUnmarshalerFn
|
||||
ap.NotEmptyChecker = forgefed.NotEmpty
|
||||
object, err := ap.UnmarshalJSON(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create federated repo
|
||||
return forgefed.OnRepository(object, func(r *forgefed.Repository) error {
|
||||
return createRepository(ctx, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Create a ticket
|
||||
func createTicket(ctx context.Context, ticket *forgefed.Ticket) error {
|
||||
if ticket.Origin != nil && ticket.Target != nil {
|
||||
return createPullRequest(ctx, ticket)
|
||||
}
|
||||
return createIssue(ctx, ticket)
|
||||
}
|
||||
|
||||
// Create an issue
|
||||
func createIssue(ctx context.Context, ticket *forgefed.Ticket) error {
|
||||
err := createRepositoryFromIRI(ctx, ticket.Context.GetLink())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Construct issue
|
||||
user, err := activitypub.PersonIRIToUser(ctx, ap.IRI(ticket.AttributedTo.GetLink().String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo, err := activitypub.RepositoryIRIToRepository(ctx, ap.IRI(ticket.Context.GetLink().String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idx, err := strconv.ParseInt(ticket.Name.String()[1:], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
issue := &issues_model.Issue{
|
||||
Index: idx, // This doesn't seem to work?
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
Title: ticket.Summary.String(),
|
||||
PosterID: user.ID,
|
||||
Poster: user,
|
||||
Content: ticket.Content.String(),
|
||||
OriginalAuthor: ticket.GetLink().String(), // Create new database field to store IRI?
|
||||
IsClosed: ticket.IsResolved,
|
||||
}
|
||||
return issue_service.NewIssue(repo, issue, nil, nil, nil)
|
||||
}
|
||||
|
||||
// Create a pull request
|
||||
func createPullRequest(ctx context.Context, ticket *forgefed.Ticket) error {
|
||||
err := createRepositoryFromIRI(ctx, ticket.Context.GetLink())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := activitypub.PersonIRIToUser(ctx, ticket.AttributedTo.GetLink())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Extract origin and target repos
|
||||
originUsername, originReponame, originBranch, err := activitypub.BranchIRIToName(ticket.Origin.GetLink())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
originRepo, err := repo_model.GetRepositoryByOwnerAndName(originUsername, originReponame)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetUsername, targetReponame, targetBranch, err := activitypub.BranchIRIToName(ticket.Target.GetLink())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetRepo, err := repo_model.GetRepositoryByOwnerAndName(targetUsername, targetReponame)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
idx, err := strconv.ParseInt(ticket.Name.String()[1:], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prIssue := &issues_model.Issue{
|
||||
Index: idx,
|
||||
RepoID: targetRepo.ID,
|
||||
Title: ticket.Summary.String(),
|
||||
PosterID: user.ID,
|
||||
Poster: user,
|
||||
IsPull: true,
|
||||
Content: ticket.Content.String(),
|
||||
IsClosed: ticket.IsResolved,
|
||||
}
|
||||
pr := &issues_model.PullRequest{
|
||||
HeadRepoID: originRepo.ID,
|
||||
BaseRepoID: targetRepo.ID,
|
||||
HeadBranch: originBranch,
|
||||
BaseBranch: targetBranch,
|
||||
HeadRepo: originRepo,
|
||||
BaseRepo: targetRepo,
|
||||
MergeBase: "",
|
||||
Type: issues_model.PullRequestGitea,
|
||||
}
|
||||
return pull_service.NewPullRequest(ctx, targetRepo, prIssue, []int64{}, []string{}, pr, []int64{})
|
||||
}
|
||||
|
||||
// Create a comment
|
||||
func createComment(ctx context.Context, note *ap.Note) error {
|
||||
err := createPersonFromIRI(ctx, note.AttributedTo.GetLink())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := activitypub.PersonIRIToUser(ctx, note.AttributedTo.GetLink())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
username, reponame, idx, err := activitypub.TicketIRIToName(note.Context.GetLink())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndNameCtx(ctx, username, reponame)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
issue, err := issues_model.GetIssueByIndex(repo.ID, idx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = issues_model.CreateCommentCtx(ctx, &issues_model.CreateCommentOptions{
|
||||
Doer: user,
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
OldTitle: note.GetLink().String(),
|
||||
Content: note.Content.String(),
|
||||
})
|
||||
return err
|
||||
}
|
|
@ -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 activitypub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/services/activitypub"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
// Process an incoming Follow activity
|
||||
func follow(ctx context.Context, follow ap.Follow) error {
|
||||
// Actor is the user performing the follow
|
||||
actorIRI := follow.Actor.GetLink()
|
||||
actorUser, err := activitypub.PersonIRIToUser(ctx, actorIRI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Object is the user being followed
|
||||
objectIRI := follow.Object.GetLink()
|
||||
objectUser, err := activitypub.PersonIRIToUser(ctx, objectIRI)
|
||||
// Must be a local user
|
||||
if err != nil || strings.Contains(objectUser.Name, "@") {
|
||||
return err
|
||||
}
|
||||
|
||||
err = user_model.FollowUser(actorUser.ID, objectUser.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 = follow
|
||||
return activitypub.Send(objectUser, accept)
|
||||
}
|
||||
|
||||
// Process an incoming Undo follow activity
|
||||
func unfollow(ctx context.Context, unfollow ap.Undo) error {
|
||||
// Object contains the follow
|
||||
follow, err := ap.To[ap.Follow](unfollow.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Actor is the user performing the undo follow
|
||||
actorIRI := follow.Actor.GetLink()
|
||||
actorUser, err := activitypub.PersonIRIToUser(ctx, actorIRI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Object is the user being unfollowed
|
||||
objectIRI := follow.Object.GetLink()
|
||||
objectUser, err := activitypub.PersonIRIToUser(ctx, objectIRI)
|
||||
// Must be a local user
|
||||
if err != nil || strings.Contains(objectUser.Name, "@") {
|
||||
return err
|
||||
}
|
||||
|
||||
return user_model.UnfollowUser(actorUser.ID, objectUser.ID)
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
// 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 (
|
||||
"strconv"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/services/activitypub"
|
||||
)
|
||||
|
||||
// Note function returns the Note object for a comment to an issue or PR
|
||||
func Note(ctx *context.APIContext) {
|
||||
// swagger:operation GET /activitypub/note/{username}/{reponame}/{id}/{noteid} activitypub activitypubNote
|
||||
// ---
|
||||
// summary: Returns the Note object for a comment to an issue or PR
|
||||
// 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 repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: ID number of the issue or PR
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: noteid
|
||||
// in: path
|
||||
// description: ID number of the comment
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ActivityPub"
|
||||
|
||||
index, err := strconv.ParseInt(ctx.Params("noteid"), 10, 64)
|
||||
if err != nil {
|
||||
ctx.ServerError("ParseInt", err)
|
||||
return
|
||||
}
|
||||
// TODO: index can be spoofed!!!
|
||||
comment, err := issues_model.GetCommentByID(ctx, index)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCommentByID", err)
|
||||
return
|
||||
}
|
||||
note, err := activitypub.Note(comment)
|
||||
if err != nil {
|
||||
ctx.ServerError("Note", err)
|
||||
return
|
||||
}
|
||||
response(ctx, note)
|
||||
}
|
|
@ -5,16 +5,24 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/activitypub"
|
||||
"code.gitea.io/gitea/models/activities"
|
||||
"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/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
"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"
|
||||
"code.gitea.io/gitea/services/activitypub"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
"github.com/go-ap/jsonld"
|
||||
)
|
||||
|
||||
// Person function returns the Person actor for a user
|
||||
|
@ -23,7 +31,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
|
||||
|
@ -34,8 +42,8 @@ func Person(ctx *context.APIContext) {
|
|||
// "200":
|
||||
// "$ref": "#/responses/ActivityPub"
|
||||
|
||||
link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name
|
||||
person := ap.PersonNew(ap.IRI(link))
|
||||
iri := ctx.ContextUser.GetIRI()
|
||||
person := ap.PersonNew(ap.IRI(iri))
|
||||
|
||||
person.Name = ap.NaturalLanguageValuesNew()
|
||||
err := person.Name.Set("en", ap.Content(ctx.ContextUser.FullName))
|
||||
|
@ -52,19 +60,22 @@ func Person(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
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.AvatarLink()),
|
||||
URL: ap.IRI(ctx.ContextUser.AvatarFullLinkWithSize(2048)),
|
||||
}
|
||||
|
||||
person.Inbox = ap.IRI(link + "/inbox")
|
||||
person.Outbox = ap.IRI(link + "/outbox")
|
||||
|
||||
person.PublicKey.ID = ap.IRI(link + "#main-key")
|
||||
person.PublicKey.Owner = ap.IRI(link)
|
||||
person.Inbox = ap.IRI(iri + "/inbox")
|
||||
person.Outbox = ap.IRI(iri + "/outbox")
|
||||
person.Following = ap.IRI(iri + "/following")
|
||||
person.Followers = ap.IRI(iri + "/followers")
|
||||
person.Liked = ap.IRI(iri + "/liked")
|
||||
|
||||
person.PublicKey.ID = ap.IRI(iri + "#main-key")
|
||||
person.PublicKey.Owner = ap.IRI(iri)
|
||||
publicKeyPem, err := activitypub.GetPublicKey(ctx.ContextUser)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetPublicKey", err)
|
||||
|
@ -72,16 +83,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 +92,7 @@ func PersonInbox(ctx *context.APIContext) {
|
|||
// ---
|
||||
// summary: Send to the inbox
|
||||
// produces:
|
||||
// - application/json
|
||||
// - application/activity+json
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
|
@ -101,5 +103,263 @@ func PersonInbox(ctx *context.APIContext) {
|
|||
// "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 = follow(ctx, activity)
|
||||
case ap.UndoType:
|
||||
err = unfollow(ctx, activity)
|
||||
case ap.CreateType:
|
||||
// TODO: this is kinda a hack
|
||||
err = ap.OnObject(activity.Object, func(n *ap.Note) error {
|
||||
noteIRI := n.InReplyTo.GetLink().String()
|
||||
noteIRISplit := strings.Split(noteIRI, "/")
|
||||
n.Context = ap.IRI(strings.TrimSuffix(noteIRI, "/"+noteIRISplit[len(noteIRISplit)-1]))
|
||||
return createComment(ctx, n)
|
||||
})
|
||||
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"
|
||||
|
||||
iri := ctx.ContextUser.GetIRI()
|
||||
|
||||
orderedCollection := ap.OrderedCollectionNew(ap.IRI(iri + "/outbox"))
|
||||
orderedCollection.First = ap.IRI(iri + "/outbox?page=1")
|
||||
|
||||
outbox := ap.OrderedCollectionPageNew(orderedCollection)
|
||||
outbox.First = ap.IRI(iri + "/outbox?page=1")
|
||||
|
||||
feed, err := activities.GetFeeds(ctx, activities.GetFeedsOptions{
|
||||
RequestedUser: ctx.ContextUser,
|
||||
RequestedActionType: activities.ActionCreateRepo,
|
||||
Actor: ctx.Doer,
|
||||
IncludePrivate: false,
|
||||
IncludeDeleted: false,
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
})
|
||||
|
||||
// Only specify next if this amount of feed corresponds to the calculated limit.
|
||||
if len(feed) == convert.ToCorrectPageSize(ctx.FormInt("limit")) {
|
||||
outbox.Next = ap.IRI(fmt.Sprintf("%s/outbox?page=%d", iri, ctx.FormInt("page")+1))
|
||||
}
|
||||
|
||||
// Only specify previous page when there is one.
|
||||
if ctx.FormInt("page") > 1 {
|
||||
outbox.Prev = ap.IRI(fmt.Sprintf("%s/outbox?page=%d", iri, ctx.FormInt("page")-1))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ctx.ServerError("Couldn't fetch feed", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, action := range feed {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove this code and implement an ActionStarRepo type, so `GetFeeds`
|
||||
// can handle this with correct pagination and ordering.
|
||||
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"
|
||||
|
||||
iri := ctx.ContextUser.GetIRI()
|
||||
|
||||
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(iri + "/following"))
|
||||
following.TotalItems = uint(len(users))
|
||||
|
||||
for _, user := range users {
|
||||
person := ap.PersonNew(ap.IRI(user.GetIRI()))
|
||||
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"
|
||||
|
||||
iri := ctx.ContextUser.GetIRI()
|
||||
|
||||
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(iri + "/followers"))
|
||||
followers.TotalItems = uint(len(users))
|
||||
|
||||
for _, user := range users {
|
||||
person := ap.PersonNew(ap.IRI(user.GetIRI()))
|
||||
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"
|
||||
|
||||
iri := ctx.ContextUser.GetIRI()
|
||||
|
||||
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(iri + "/liked"))
|
||||
liked.TotalItems = uint(count)
|
||||
|
||||
for _, repo := range repos {
|
||||
repo := forgefed.RepositoryNew(ap.IRI(repo.GetIRI()))
|
||||
err := liked.OrderedItems.Append(repo)
|
||||
if err != nil {
|
||||
ctx.ServerError("OrderedItems.Append", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response(ctx, liked)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
// 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/modules/context"
|
||||
"code.gitea.io/gitea/modules/forgefed"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
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"
|
||||
|
||||
iri := ctx.Repo.Repository.GetIRI()
|
||||
repo := forgefed.RepositoryNew(ap.IRI(iri))
|
||||
|
||||
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(ctx.Repo.Owner.GetIRI())
|
||||
|
||||
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(iri + "/inbox")
|
||||
repo.Outbox = ap.IRI(iri + "/outbox")
|
||||
repo.Followers = ap.IRI(iri + "/followers")
|
||||
repo.Team = ap.IRI(iri + "/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
|
||||
}
|
||||
|
||||
ap.ItemTyperFunc = forgefed.GetItemByType
|
||||
ap.JSONItemUnmarshal = forgefed.JSONUnmarshalerFn
|
||||
ap.NotEmptyChecker = forgefed.NotEmpty
|
||||
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
|
||||
}
|
||||
|
||||
if activity.Object == nil {
|
||||
ctx.ServerError("Activity does not contain object", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Process activity
|
||||
switch activity.Type {
|
||||
case ap.CreateType:
|
||||
switch activity.Object.GetType() {
|
||||
case forgefed.RepositoryType:
|
||||
// Fork created by remote instance
|
||||
err = forgefed.OnRepository(activity.Object, func(r *forgefed.Repository) error {
|
||||
return createRepository(ctx, r)
|
||||
})
|
||||
case forgefed.TicketType:
|
||||
// New issue or pull request
|
||||
err = forgefed.OnTicket(activity.Object, func(t *forgefed.Ticket) error {
|
||||
return createTicket(ctx, t)
|
||||
})
|
||||
case ap.NoteType:
|
||||
// New comment
|
||||
err = ap.On(activity.Object, func(n *ap.Note) error {
|
||||
return createComment(ctx, n)
|
||||
})
|
||||
default:
|
||||
log.Info("Incoming unsupported ActivityStreams object type: %s", activity.Object.GetType())
|
||||
ctx.PlainText(http.StatusNotImplemented, "ActivityStreams object type not supported")
|
||||
return
|
||||
}
|
||||
case ap.LikeType:
|
||||
err = star(ctx, activity)
|
||||
default:
|
||||
ctx.PlainText(http.StatusNotImplemented, "ActivityStreams type not supported")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
ctx.ServerError("Error when processing", err)
|
||||
}
|
||||
|
||||
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 activitypubRepoOutbox
|
||||
// ---
|
||||
// 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)
|
||||
}
|
|
@ -9,14 +9,12 @@ 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"
|
||||
"code.gitea.io/gitea/services/activitypub"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
"github.com/go-fed/httpsig"
|
||||
|
@ -44,39 +42,28 @@ func getPublicKeyFromResponse(b []byte, keyID *url.URL) (p crypto.PublicKey, err
|
|||
return p, err
|
||||
}
|
||||
|
||||
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()
|
||||
func getKeyID(r *http.Request) (httpsig.Verifier, string, error) {
|
||||
v, err := httpsig.NewVerifier(r)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, "", err
|
||||
}
|
||||
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 b, err
|
||||
return v, v.KeyId(), nil
|
||||
}
|
||||
|
||||
func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, err error) {
|
||||
r := ctx.Req
|
||||
|
||||
// 1. Figure out what key we need to verify
|
||||
v, err := httpsig.NewVerifier(r)
|
||||
v, ID, err := getKeyID(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ID := v.KeyId()
|
||||
idIRI, err := url.Parse(ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 2. Fetch the public key of the other actor
|
||||
b, err := fetch(idIRI)
|
||||
b, err := activitypub.Fetch(idIRI)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -87,6 +74,19 @@ func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, er
|
|||
// 3. Verify the other actor's key
|
||||
algo := httpsig.Algorithm(setting.Federation.Algorithms[0])
|
||||
authenticated = v.Verify(pubKey, algo) == nil
|
||||
if !authenticated {
|
||||
return
|
||||
}
|
||||
// 4. Create a federated user for the actor
|
||||
// TODO: This is a very bad place for creating federated users
|
||||
// We end up creating way more users than necessary!
|
||||
var person ap.Person
|
||||
err = person.UnmarshalJSON(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = createPerson(ctx, &person)
|
||||
return authenticated, err
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
// 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/context"
|
||||
"code.gitea.io/gitea/modules/forgefed"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/services/activitypub"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
"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),
|
||||
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 {
|
||||
log.Error("write to resp err: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// 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"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/services/activitypub"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
// Process a Like activity to star a repository
|
||||
func star(ctx context.Context, like ap.Like) (err error) {
|
||||
user, err := activitypub.PersonIRIToUser(ctx, like.Actor.GetLink())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
repo, err := activitypub.RepositoryIRIToRepository(ctx, like.Object.GetLink())
|
||||
if err != nil || strings.Contains(repo.Name, "@") || repo.IsPrivate {
|
||||
return
|
||||
}
|
||||
return repo_model.StarRepo(user.ID, repo.ID, true)
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
// 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 (
|
||||
"strconv"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/services/activitypub"
|
||||
)
|
||||
|
||||
// Ticket function returns the Ticket object for an issue or PR
|
||||
func Ticket(ctx *context.APIContext) {
|
||||
// swagger:operation GET /activitypub/ticket/{username}/{reponame}/{id} activitypub forgefedTicket
|
||||
// ---
|
||||
// summary: Returns the Ticket object for an issue or PR
|
||||
// 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 repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: ID number of the issue or PR
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ActivityPub"
|
||||
|
||||
index, err := strconv.ParseInt(ctx.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
ctx.ServerError("ParseInt", err)
|
||||
return
|
||||
}
|
||||
issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, index)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetIssueByIndex", err)
|
||||
return
|
||||
}
|
||||
ticket, err := activitypub.Ticket(issue)
|
||||
if err != nil {
|
||||
ctx.ServerError("Ticket", err)
|
||||
return
|
||||
}
|
||||
response(ctx, ticket)
|
||||
}
|
|
@ -644,12 +644,25 @@ func Routes(ctx gocontext.Context) *web.Route {
|
|||
}
|
||||
m.Get("/version", misc.Version)
|
||||
if setting.Federation.Enabled {
|
||||
m.Get("/authorize_interaction", activitypub.AuthorizeInteraction)
|
||||
m.Get("/nodeinfo", misc.NodeInfo)
|
||||
m.Group("/activitypub", func() {
|
||||
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("/ticket/{username}/{reponame}/{id}", repoAssignment(), activitypub.Ticket)
|
||||
m.Get("/note/{username}/{reponame}/{id}/{noteid}", repoAssignment(), activitypub.Note)
|
||||
})
|
||||
}
|
||||
m.Get("/signing-key.gpg", misc.SigningKey)
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/convert"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
func responseAPIUsers(ctx *context.APIContext, users []*user_model.User) {
|
||||
|
@ -219,7 +220,7 @@ func Follow(ctx *context.APIContext) {
|
|||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
|
||||
if err := user_model.FollowUser(ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
||||
if err := user_service.FollowUser(ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "FollowUser", err)
|
||||
return
|
||||
}
|
||||
|
@ -241,7 +242,7 @@ func Unfollow(ctx *context.APIContext) {
|
|||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
|
||||
if err := user_model.UnfollowUser(ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
||||
if err := user_service.UnfollowUser(ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UnfollowUser", err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/web/feed"
|
||||
"code.gitea.io/gitea/routers/web/org"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
// Profile render user's profile page
|
||||
|
@ -302,9 +303,9 @@ func Action(ctx *context.Context) {
|
|||
var err error
|
||||
switch ctx.FormString("action") {
|
||||
case "follow":
|
||||
err = user_model.FollowUser(ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
err = user_service.FollowUser(ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
case "unfollow":
|
||||
err = user_model.UnfollowUser(ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
err = user_service.UnfollowUser(ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -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", "*")
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
// 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 (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
// Create and send Follow activity
|
||||
func Follow(actorUser, followUser *user_model.User) *ap.Follow {
|
||||
object := ap.PersonNew(ap.IRI(followUser.LoginName))
|
||||
follow := ap.FollowNew("", object)
|
||||
follow.Type = ap.FollowType
|
||||
follow.Actor = ap.PersonNew(ap.IRI(actorUser.GetIRI()))
|
||||
follow.To = ap.ItemCollection{ap.Item(ap.IRI(followUser.LoginName + "/inbox"))}
|
||||
return follow
|
||||
}
|
||||
|
||||
// Create and send Undo Follow activity
|
||||
func Unfollow(actorUser, followUser *user_model.User) *ap.Undo {
|
||||
object := ap.PersonNew(ap.IRI(followUser.LoginName))
|
||||
follow := ap.FollowNew("", object)
|
||||
follow.Actor = ap.PersonNew(ap.IRI(actorUser.GetIRI()))
|
||||
unfollow := ap.UndoNew("", follow)
|
||||
unfollow.Type = ap.UndoType
|
||||
unfollow.To = ap.ItemCollection{ap.Item(ap.IRI(followUser.LoginName + "/inbox"))}
|
||||
return unfollow
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// 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 (
|
||||
ap "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
func Create(to string, object ap.ObjectOrLink) *ap.Create {
|
||||
return &ap.Create{
|
||||
Type: ap.CreateType,
|
||||
Object: object,
|
||||
To: ap.ItemCollection{ap.Item(ap.IRI(to))},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
// 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"
|
||||
"strconv"
|
||||
"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) < 4 {
|
||||
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
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
// TODO: create remote repo if not exists
|
||||
return repo_model.GetRepositoryByOwnerAndName(username, reponame)
|
||||
}
|
||||
|
||||
// Returns the owner, repo name, and idx of a Ticket object IRI
|
||||
func TicketIRIToName(ticketIRI ap.IRI) (string, string, int64, error) {
|
||||
ticketIRISplit := strings.Split(ticketIRI.String(), "/")
|
||||
if len(ticketIRISplit) < 5 {
|
||||
return "", "", 0, errors.New("not a Ticket object IRI")
|
||||
}
|
||||
|
||||
instance := ticketIRISplit[2]
|
||||
username := ticketIRISplit[len(ticketIRISplit)-3]
|
||||
reponame := ticketIRISplit[len(ticketIRISplit)-2]
|
||||
idx, err := strconv.ParseInt(ticketIRISplit[len(ticketIRISplit)-1], 10, 64)
|
||||
if err != nil {
|
||||
return "", "", 0, err
|
||||
}
|
||||
if instance == setting.Domain {
|
||||
// Local repo
|
||||
return username, reponame, idx, nil
|
||||
}
|
||||
// Remote repo
|
||||
return username + "@" + instance, reponame, idx, nil
|
||||
}
|
||||
|
||||
// Returns the owner, repo name, and idx of a Branch object IRI
|
||||
func BranchIRIToName(ticketIRI ap.IRI) (string, string, string, error) {
|
||||
ticketIRISplit := strings.Split(ticketIRI.String(), "/")
|
||||
if len(ticketIRISplit) < 5 {
|
||||
return "", "", "", errors.New("not a Branch object IRI")
|
||||
}
|
||||
|
||||
instance := ticketIRISplit[2]
|
||||
username := ticketIRISplit[len(ticketIRISplit)-3]
|
||||
reponame := ticketIRISplit[len(ticketIRISplit)-2]
|
||||
branch := ticketIRISplit[len(ticketIRISplit)-1]
|
||||
if instance == setting.Domain {
|
||||
// Local repo
|
||||
return username, reponame, branch, nil
|
||||
}
|
||||
// Remote repo
|
||||
return username + "@" + instance, reponame, branch, nil
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
// 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 (
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/forgefed"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
// Construct a Note object from a comment
|
||||
func Note(comment *issues_model.Comment) (*ap.Note, error) {
|
||||
err := comment.LoadPoster()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = comment.LoadIssue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
note := ap.Note{
|
||||
Type: ap.NoteType,
|
||||
ID: ap.IRI(comment.GetIRI()),
|
||||
AttributedTo: ap.IRI(comment.Poster.GetIRI()),
|
||||
Context: ap.IRI(comment.Issue.GetIRI()),
|
||||
To: ap.ItemCollection{ap.IRI("https://www.w3.org/ns/activitystreams#Public")},
|
||||
}
|
||||
note.Content = ap.NaturalLanguageValuesNew()
|
||||
err = note.Content.Set("en", ap.Content(comment.Content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ¬e, nil
|
||||
}
|
||||
|
||||
// Construct a Ticket object from an issue
|
||||
func Ticket(issue *issues_model.Issue) (*forgefed.Ticket, error) {
|
||||
iri := issue.GetIRI()
|
||||
ticket := forgefed.TicketNew()
|
||||
ticket.Type = forgefed.TicketType
|
||||
ticket.ID = ap.IRI(iri)
|
||||
|
||||
// Setting a NaturalLanguageValue to a number causes go-ap's JSON parsing to do weird things
|
||||
// Workaround: set it to #1 instead of 1
|
||||
ticket.Name = ap.NaturalLanguageValuesNew()
|
||||
err := ticket.Name.Set("en", ap.Content("#"+strconv.FormatInt(issue.Index, 10)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = issue.LoadRepo(db.DefaultContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ticket.Context = ap.IRI(issue.Repo.GetIRI())
|
||||
|
||||
err = issue.LoadPoster()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ticket.AttributedTo = ap.IRI(issue.Poster.GetIRI())
|
||||
|
||||
ticket.Summary = ap.NaturalLanguageValuesNew()
|
||||
err = ticket.Summary.Set("en", ap.Content(issue.Title))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ticket.Content = ap.NaturalLanguageValuesNew()
|
||||
err = ticket.Content.Set("en", ap.Content(issue.Content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if issue.IsClosed {
|
||||
ticket.IsResolved = true
|
||||
}
|
||||
return ticket, nil
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
// 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/forgefed"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"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)
|
||||
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 b, err
|
||||
}
|
||||
|
||||
// Send an activity
|
||||
func Send(user *user_model.User, activity *ap.Activity) error {
|
||||
binary, err := jsonld.WithContext(
|
||||
jsonld.IRI(ap.ActivityBaseURI),
|
||||
jsonld.IRI(ap.SecurityContextURI),
|
||||
jsonld.IRI(forgefed.ForgeFedNamespaceURI),
|
||||
).Marshal(activity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, to := range activity.To {
|
||||
client, _ := NewClient(user, user.GetIRI()+"#main-key")
|
||||
resp, _ := client.Post(binary, to.GetLink().String())
|
||||
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, setting.Federation.MaxSize))
|
||||
log.Trace("Response from sending activity", string(respBody))
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -5,12 +5,15 @@
|
|||
package comments
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
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/notification"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/services/activitypub"
|
||||
)
|
||||
|
||||
// CreateIssueComment creates a plain issue comment.
|
||||
|
@ -27,6 +30,19 @@ func CreateIssueComment(doer *user_model.User, repo *repo_model.Repository, issu
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if strings.Contains(repo.OwnerName, "@") {
|
||||
// Federated comment
|
||||
note, err := activitypub.Note(comment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
create := activitypub.Create(repo.OriginalURL+"/inbox", note)
|
||||
err = activitypub.Send(doer, create)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
mentions, err := issues_model.FindAndUpdateIssueMentions(db.DefaultContext, issue, doer, comment.Content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -6,6 +6,7 @@ package issue
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -19,6 +20,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/notification"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/activitypub"
|
||||
)
|
||||
|
||||
// NewIssue creates new issue with labels for repository.
|
||||
|
@ -27,6 +29,19 @@ func NewIssue(repo *repo_model.Repository, issue *issues_model.Issue, labelIDs [
|
|||
return err
|
||||
}
|
||||
|
||||
if strings.Contains(repo.OwnerName, "@") {
|
||||
// Federated issue
|
||||
ticket, err := activitypub.Ticket(issue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
create := activitypub.Create(repo.OriginalURL+"/inbox", ticket)
|
||||
err = activitypub.Send(issue.Poster, create)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, assigneeID := range assigneeIDs {
|
||||
if err := AddAssigneeIfNotAssigned(issue, issue.Poster, assigneeID); err != nil {
|
||||
return err
|
||||
|
|
|
@ -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 repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/activitypub"
|
||||
"code.gitea.io/gitea/services/migrations"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
func CreateFork(ctx context.Context, instance, username, reponame, destUsername string) error {
|
||||
// TODO: Clean this up
|
||||
|
||||
// Migrate repository code
|
||||
user, err := user_model.GetUserByName(ctx, destUsername)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = migrations.MigrateRepository(ctx, user, destUsername, migrations.MigrateOptions{
|
||||
CloneAddr: "https://" + instance + "/" + username + "/" + reponame + ".git",
|
||||
RepoName: reponame,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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(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
|
||||
|
||||
return activitypub.Send(user, &create)
|
||||
}
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/models"
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
|
@ -26,6 +27,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/activitypub"
|
||||
"code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
|
@ -280,3 +282,53 @@ func DeleteAvatar(u *user_model.User) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FollowUser marks someone be another's follower.
|
||||
func FollowUser(userID, followID int64) (err error) {
|
||||
if userID == followID || user_model.IsFollowing(userID, followID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
followUser, err := user_model.GetUserByID(followID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if followUser.LoginType == auth.Federated {
|
||||
// Following remote user
|
||||
actorUser, err := user_model.GetUserByID(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = activitypub.Send(actorUser, activitypub.Follow(actorUser, followUser))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return user_model.FollowUser(userID, followID)
|
||||
}
|
||||
|
||||
// UnfollowUser unmarks someone as another's follower.
|
||||
func UnfollowUser(userID, followID int64) (err error) {
|
||||
if userID == followID || !user_model.IsFollowing(userID, followID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
followUser, err := user_model.GetUserByID(followID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if followUser.LoginType == auth.Federated {
|
||||
// Unfollowing remote user
|
||||
actorUser, err := user_model.GetUserByID(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = activitypub.Send(actorUser, activitypub.Unfollow(actorUser, followUser))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return user_model.UnfollowUser(userID, followID)
|
||||
}
|
||||
|
|
|
@ -23,10 +23,182 @@
|
|||
},
|
||||
"basePath": "{{AppSubUrl | JSEscape | Safe}}/api/v1",
|
||||
"paths": {
|
||||
"/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/repo/{username}/{reponame}/outbox": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/activity+json"
|
||||
],
|
||||
"tags": [
|
||||
"activitypub"
|
||||
],
|
||||
"summary": "Returns the outbox",
|
||||
"operationId": "activitypubRepoOutbox",
|
||||
"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/ticket/{username}/{reponame}/{id}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/activity+json"
|
||||
],
|
||||
"tags": [
|
||||
"activitypub"
|
||||
],
|
||||
"summary": "Returns the Ticket object for an issue or PR",
|
||||
"operationId": "forgefedTicket",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "reponame",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "ID number of the issue or PR",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/ActivityPub"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/activitypub/user/{username}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
"application/activity+json"
|
||||
],
|
||||
"tags": [
|
||||
"activitypub"
|
||||
|
@ -49,10 +221,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 +299,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": [
|
||||
|
|
|
@ -13,9 +13,9 @@ import (
|
|||
"testing"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/activitypub"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/routers"
|
||||
"code.gitea.io/gitea/services/activitypub"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -42,10 +42,10 @@ func TestActivityPubPerson(t *testing.T) {
|
|||
|
||||
assert.Equal(t, ap.PersonType, person.Type)
|
||||
assert.Equal(t, username, person.PreferredUsername.String())
|
||||
keyID := person.GetID().String()
|
||||
keyID := person.GetLink().String()
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user/%s$", username), keyID)
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/outbox$", username), person.Outbox.GetID().String())
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/inbox$", username), person.Inbox.GetID().String())
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/outbox$", username), person.Outbox.GetLink().String())
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/inbox$", username), person.Inbox.GetLink().String())
|
||||
|
||||
pubKey := person.PublicKey
|
||||
assert.NotNil(t, pubKey)
|
||||
|
|
Loading…
Reference in New Issue