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