Compare commits
No commits in common. "federation" and "tasks-598" have entirely different histories.
federation
...
tasks-598
12 changed files with 65 additions and 729 deletions
32
CODEOWNERS
Normal file
32
CODEOWNERS
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# This file describes the expected reviewers for a PR based on the changed
|
||||||
|
# files. Unlike what the name of the file suggests they don't own the code, but
|
||||||
|
# merely have a good understanding of that area of the codebase and therefore
|
||||||
|
# are usually suited as a reviewer.
|
||||||
|
|
||||||
|
|
||||||
|
# Please mind the alphabetic order of reviewers.
|
||||||
|
|
||||||
|
# Files related to the CI of the Forgejo project.
|
||||||
|
.forgejo/.* @dachary @earl-warren
|
||||||
|
|
||||||
|
# Files related to frontend development.
|
||||||
|
|
||||||
|
# Javascript and CSS code.
|
||||||
|
web_src/.* @caesar @crystal @gusted
|
||||||
|
|
||||||
|
# HTML templates used by the backend.
|
||||||
|
templates/.* @caesar @crystal @gusted
|
||||||
|
|
||||||
|
# Files related to Go development.
|
||||||
|
|
||||||
|
# The modules usually don't require much knowledge about Forgejo and could
|
||||||
|
# be reviewed by Go developers.
|
||||||
|
modules/.* @dachary @earl-warren @gusted
|
||||||
|
|
||||||
|
# Models has code related to SQL queries, general database knowledge and XORM.
|
||||||
|
models/.* @dachary @earl-warren @gusted
|
||||||
|
|
||||||
|
# The routers directory contains the most amount code that requires a good grasp
|
||||||
|
# of how Forgejo comes together. It's tedious to write good integration testing
|
||||||
|
# for code that lives in here.
|
||||||
|
routers/.* @dachary @earl-warren @gusted
|
|
@ -1,259 +0,0 @@
|
||||||
package federation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
|
||||||
"code.gitea.io/gitea/models/user"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
"xorm.io/builder"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HookTask represents a hook task.
|
|
||||||
// exact copy of models/webhook/hooktask.go when this migration was created
|
|
||||||
// - xorm:"-" fields deleted
|
|
||||||
type FederatedHost struct {
|
|
||||||
ID int64 `xorm:"pk autoincr"`
|
|
||||||
isBlocked bool
|
|
||||||
HostFqdn string `xorm:"UNIQUE(s) INDEX"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetFederatdHost(ctx context.Context, hostFqdn string) (*FederatedHost, error) {
|
|
||||||
rec := new(FederatedHost)
|
|
||||||
_, err := db.GetEngine(ctx).
|
|
||||||
Table("federated_host").Where("host_fqdn = ?", hostFqdn).Get(rec)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return rec, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func FederatedHostExists(ctx context.Context, hostFqdn string) (bool, error) {
|
|
||||||
rec := new(FederatedHost)
|
|
||||||
exists, err := db.GetEngine(ctx).
|
|
||||||
Table("federated_host").Where("host_fqdn = ?", hostFqdn).Get(rec)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return exists, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (host *FederatedHost) Save(ctx context.Context) error {
|
|
||||||
_, err := db.GetEngine(ctx).
|
|
||||||
Insert(host)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type FederatedUser struct {
|
|
||||||
ID int64 `xorm:"pk autoincr"`
|
|
||||||
UserID int64 `xorm:"INDEX"`
|
|
||||||
ExternalID string `xorm:"UNIQUE(s) INDEX"`
|
|
||||||
FederationHostID int64 `xorm:"INDEX"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateFederatedUser(ctx context.Context, alias string, website string, hostname string) (*user.User, error) {
|
|
||||||
engine := db.GetEngine(ctx)
|
|
||||||
|
|
||||||
// create FederatedHost
|
|
||||||
exists, err := FederatedHostExists(ctx, hostname)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var federatedHost FederatedHost
|
|
||||||
if exists {
|
|
||||||
x, err := GetFederatdHost(ctx, hostname)
|
|
||||||
federatedHost = *x
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
federatedHost := new(FederatedHost)
|
|
||||||
federatedHost.HostFqdn = hostname
|
|
||||||
if err = federatedHost.Save(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create user.User
|
|
||||||
u := new(user.User)
|
|
||||||
u.Name = "@" + alias + "@" + hostname
|
|
||||||
//panic(u.Name)
|
|
||||||
u.Email = alias + "@" + hostname
|
|
||||||
u.Website = website
|
|
||||||
u.KeepEmailPrivate = true
|
|
||||||
|
|
||||||
exist, err := user.GetUser(ctx, u)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if exist {
|
|
||||||
return u, nil // TODO: must also check for federatedUser existence
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = createUser(ctx, u); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
federatedUser := new(FederatedUser)
|
|
||||||
federatedUser.ExternalID = u.Name
|
|
||||||
federatedUser.UserID = u.ID
|
|
||||||
federatedUser.FederationHostID = federatedHost.ID
|
|
||||||
exist, err = engine.Get(federatedUser)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exist {
|
|
||||||
_, err = engine.Insert(federatedUser)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createUser(ctx context.Context, u *user.User) error {
|
|
||||||
// set system defaults
|
|
||||||
u.Visibility = setting.Service.DefaultUserVisibilityMode
|
|
||||||
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation
|
|
||||||
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification
|
|
||||||
u.MaxRepoCreation = -1
|
|
||||||
u.Theme = setting.UI.DefaultTheme
|
|
||||||
u.IsRestricted = setting.Service.DefaultUserIsRestricted
|
|
||||||
u.IsActive = !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm)
|
|
||||||
|
|
||||||
// Ensure consistency of the dates.
|
|
||||||
if u.UpdatedUnix < u.CreatedUnix {
|
|
||||||
u.UpdatedUnix = u.CreatedUnix
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate data
|
|
||||||
if err := user.ValidateUser(u); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.ValidateEmail(u.Email); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer committer.Close()
|
|
||||||
|
|
||||||
isExist, err := user.IsUserExist(ctx, 0, u.Name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if isExist {
|
|
||||||
return user.ErrUserAlreadyExist{u.Name}
|
|
||||||
}
|
|
||||||
|
|
||||||
isExist, err = user.IsEmailUsed(ctx, u.Email)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if isExist {
|
|
||||||
return user.ErrEmailAlreadyUsed{
|
|
||||||
Email: u.Email,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare for database
|
|
||||||
|
|
||||||
u.LowerName = strings.ToLower(u.Name)
|
|
||||||
u.AvatarEmail = u.Email
|
|
||||||
if u.Rands, err = user.GetUserSalt(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if u.Passwd != "" {
|
|
||||||
if err = u.SetPassword(u.Passwd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
u.Salt = ""
|
|
||||||
u.PasswdHashAlgo = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// save changes to database
|
|
||||||
|
|
||||||
if err = user.DeleteUserRedirect(ctx, u.Name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.CreatedUnix == 0 {
|
|
||||||
// Caller expects auto-time for creation & update timestamps.
|
|
||||||
err = db.Insert(ctx, u)
|
|
||||||
} else {
|
|
||||||
// Caller sets the timestamps themselves. They are responsible for ensuring
|
|
||||||
// both `CreatedUnix` and `UpdatedUnix` are set appropriately.
|
|
||||||
_, err = db.GetEngine(ctx).NoAutoTime().Insert(u)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert email address
|
|
||||||
if err := db.Insert(ctx, &user.EmailAddress{
|
|
||||||
UID: u.ID,
|
|
||||||
Email: u.Email,
|
|
||||||
LowerEmail: strings.ToLower(u.Email),
|
|
||||||
IsActivated: u.IsActive,
|
|
||||||
IsPrimary: true,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return committer.Commit()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRemoteUsersWithNoLocalFollowers(ctx context.Context, olderThan time.Duration, page int) ([]user.User, error) {
|
|
||||||
limit := 40
|
|
||||||
offset := page * limit
|
|
||||||
var users []user.User
|
|
||||||
|
|
||||||
err := db.GetEngine(ctx).
|
|
||||||
Table("user").
|
|
||||||
Where("num_followers = 0").
|
|
||||||
And(builder.Lt{"user.created_unix": time.Now().Add(-olderThan).Unix()}).
|
|
||||||
Join("inner", "federated_user", "federated_user.user_id = user.id").
|
|
||||||
Limit(limit, offset).
|
|
||||||
Find(&users)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Trace("Error: GetRemoteUserWithNoLocalFollowers: %w", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return users, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRemotePersons(ctx context.Context, page int) ([]FederatedUser, error) {
|
|
||||||
limit := 1
|
|
||||||
offset := page * limit
|
|
||||||
var federatedUsers []FederatedUser
|
|
||||||
|
|
||||||
err := db.GetEngine(ctx).
|
|
||||||
Table("federated_user").
|
|
||||||
Limit(limit, offset).
|
|
||||||
Find(&federatedUsers)
|
|
||||||
|
|
||||||
// TODO: this doesn't work, so fetching federated_user and then getting user. How to make this work?
|
|
||||||
// var users []user.User
|
|
||||||
// err := db.GetEngine(ctx).
|
|
||||||
// Table("user").
|
|
||||||
// Join("inner", "federated_user", "federated_user.user_id = user.id").
|
|
||||||
// Limit(limit, offset).
|
|
||||||
// Find(&users)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Trace("Error: GetRemotePersons: %w", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return federatedUsers, nil
|
|
||||||
}
|
|
|
@ -50,10 +50,6 @@ var migrations = []*Migration{
|
||||||
NewMigration("create the forgejo_repo_flag table", forgejo_v1_22.CreateRepoFlagTable),
|
NewMigration("create the forgejo_repo_flag table", forgejo_v1_22.CreateRepoFlagTable),
|
||||||
// v5 -> v6
|
// v5 -> v6
|
||||||
NewMigration("Add wiki_branch to repository", forgejo_v1_22.AddWikiBranchToRepository),
|
NewMigration("Add wiki_branch to repository", forgejo_v1_22.AddWikiBranchToRepository),
|
||||||
// v6 -> v7
|
|
||||||
NewMigration("create federated_host table", forgejo_v1_22.AddFederatedHost),
|
|
||||||
// v7 -> v8
|
|
||||||
NewMigration("create federated_user table", forgejo_v1_22.AddFederatedUser),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||||
|
@ -122,7 +118,6 @@ func Migrate(x *xorm.Engine) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
v := currentVersion.Version
|
v := currentVersion.Version
|
||||||
log.Info("Current version: %d", v)
|
|
||||||
|
|
||||||
// Downgrading Forgejo's database version not supported
|
// Downgrading Forgejo's database version not supported
|
||||||
if v > ExpectedVersion() {
|
if v > ExpectedVersion() {
|
||||||
|
@ -161,6 +156,5 @@ func Migrate(x *xorm.Engine) error {
|
||||||
return fmt.Errorf("sync: %w", err)
|
return fmt.Errorf("sync: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// panic("fn end")
|
|
||||||
return semver.SetVersionStringWithEngine(x, setting.ForgejoVersion)
|
return semver.SetVersionStringWithEngine(x, setting.ForgejoVersion)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
// Copyright 2024 The Forgejo Authors
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
package v1_22 //nolint
|
|
||||||
|
|
||||||
import (
|
|
||||||
"code.gitea.io/gitea/models/federation"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"xorm.io/xorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
//// HookTask represents a hook task.
|
|
||||||
//// exact copy of models/webhook/hooktask.go when this migration was created
|
|
||||||
//// - xorm:"-" fields deleted
|
|
||||||
//type FederatedHost struct {
|
|
||||||
// ID int64 `xorm:"pk autoincr"`
|
|
||||||
// isBlocked bool
|
|
||||||
// HostFqdn string `xorm:"UNIQUE(s) INDEX"`
|
|
||||||
//}
|
|
||||||
|
|
||||||
func AddFederatedHost(x *xorm.Engine) error {
|
|
||||||
|
|
||||||
// panic("add host")
|
|
||||||
log.Info("Running Add host migration")
|
|
||||||
return x.Sync(new(federation.FederatedHost))
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
// Copyright 2024 The Forgejo Authors
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
package v1_22 //nolint
|
|
||||||
|
|
||||||
import (
|
|
||||||
"code.gitea.io/gitea/models/federation"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"xorm.io/xorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HookTask represents a hook task.
|
|
||||||
// exact copy of models/webhook/hooktask.go when this migration was created
|
|
||||||
// - xorm:"-" fields deleted
|
|
||||||
//type FederatedUser struct {
|
|
||||||
// ID int64 `xorm:"pk autoincr"`
|
|
||||||
// UserID int64 `xorm:"INDEX"`
|
|
||||||
// ExternalID string `xorm:"UNIQUE(s) INDEX"`
|
|
||||||
// FederationHostID int64 `xorm:"INDEX"`
|
|
||||||
//}
|
|
||||||
|
|
||||||
func AddFederatedUser(x *xorm.Engine) error {
|
|
||||||
log.Info("Running Add user migration")
|
|
||||||
return x.Sync(new(federation.FederatedUser))
|
|
||||||
}
|
|
|
@ -2921,8 +2921,6 @@ dashboard.start_schedule_tasks = Start schedule tasks
|
||||||
dashboard.sync_branch.started = Branches Sync started
|
dashboard.sync_branch.started = Branches Sync started
|
||||||
dashboard.sync_tag.started = Tags Sync started
|
dashboard.sync_tag.started = Tags Sync started
|
||||||
dashboard.rebuild_issue_indexer = Rebuild issue indexer
|
dashboard.rebuild_issue_indexer = Rebuild issue indexer
|
||||||
dashboard.remote_actor_cleanup = Clean remote actors with no local followers
|
|
||||||
dashboard.remote_actor_update = Update remote actors' data
|
|
||||||
|
|
||||||
users.user_manage_panel = Manage user accounts
|
users.user_manage_panel = Manage user accounts
|
||||||
users.new_account = Create User Account
|
users.new_account = Create User Account
|
||||||
|
|
|
@ -17,7 +17,6 @@ import (
|
||||||
"code.gitea.io/gitea/modules/sitemap"
|
"code.gitea.io/gitea/modules/sitemap"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/forgefed"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -93,31 +92,6 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions,
|
||||||
|
|
||||||
opts.Keyword = ctx.FormTrim("q")
|
opts.Keyword = ctx.FormTrim("q")
|
||||||
opts.OrderBy = orderBy
|
opts.OrderBy = orderBy
|
||||||
|
|
||||||
if len(opts.Keyword) > 0 && forgefed.IsFingerable(opts.Keyword) {
|
|
||||||
webfingerRes, err := forgefed.WebFingerLookup(opts.Keyword)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("SearchUsers", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
person, err := forgefed.GetActor(webfingerRes.GetActorLink().Href)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("SearchUsers", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = forgefed.SavePerson(ctx, person)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("SearchUsers", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// users, count, err = user_model.SearchUsers(ctx, opts)
|
|
||||||
// if err != nil {
|
|
||||||
// ctx.ServerError("SearchUsers", err)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
|
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
|
||||||
users, count, err = user_model.SearchUsers(ctx, opts)
|
users, count, err = user_model.SearchUsers(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -125,7 +99,6 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSitemap {
|
if isSitemap {
|
||||||
m := sitemap.NewSitemap()
|
m := sitemap.NewSitemap()
|
||||||
for _, item := range users {
|
for _, item := range users {
|
||||||
|
|
|
@ -13,11 +13,25 @@ import (
|
||||||
"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/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/forgefed"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-webfinger-14#section-4.4
|
// https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-webfinger-14#section-4.4
|
||||||
|
|
||||||
|
type webfingerJRD struct {
|
||||||
|
Subject string `json:"subject,omitempty"`
|
||||||
|
Aliases []string `json:"aliases,omitempty"`
|
||||||
|
Properties map[string]any `json:"properties,omitempty"`
|
||||||
|
Links []*webfingerLink `json:"links,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type webfingerLink struct {
|
||||||
|
Rel string `json:"rel,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Href string `json:"href,omitempty"`
|
||||||
|
Titles map[string]string `json:"titles,omitempty"`
|
||||||
|
Properties map[string]any `json:"properties,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// WebfingerQuery returns information about a resource
|
// WebfingerQuery returns information about a resource
|
||||||
// https://datatracker.ietf.org/doc/html/rfc7565
|
// https://datatracker.ietf.org/doc/html/rfc7565
|
||||||
func WebfingerQuery(ctx *context.Context) {
|
func WebfingerQuery(ctx *context.Context) {
|
||||||
|
@ -90,7 +104,7 @@ func WebfingerQuery(ctx *context.Context) {
|
||||||
aliases = append(aliases, fmt.Sprintf("mailto:%s", u.Email))
|
aliases = append(aliases, fmt.Sprintf("mailto:%s", u.Email))
|
||||||
}
|
}
|
||||||
|
|
||||||
links := []*forgefed.WebfingerLink{
|
links := []*webfingerLink{
|
||||||
{
|
{
|
||||||
Rel: "http://webfinger.net/rel/profile-page",
|
Rel: "http://webfinger.net/rel/profile-page",
|
||||||
Type: "text/html",
|
Type: "text/html",
|
||||||
|
@ -113,7 +127,7 @@ func WebfingerQuery(ctx *context.Context) {
|
||||||
|
|
||||||
ctx.Resp.Header().Add("Content-Type", "application/jrd+json")
|
ctx.Resp.Header().Add("Content-Type", "application/jrd+json")
|
||||||
ctx.Resp.Header().Add("Access-Control-Allow-Origin", "*")
|
ctx.Resp.Header().Add("Access-Control-Allow-Origin", "*")
|
||||||
ctx.JSON(http.StatusOK, &forgefed.WebfingerJRD{
|
ctx.JSON(http.StatusOK, &webfingerJRD{
|
||||||
Subject: fmt.Sprintf("acct:%s@%s", url.QueryEscape(u.Name), appURL.Host),
|
Subject: fmt.Sprintf("acct:%s@%s", url.QueryEscape(u.Name), appURL.Host),
|
||||||
Aliases: aliases,
|
Aliases: aliases,
|
||||||
Links: links,
|
Links: links,
|
||||||
|
|
|
@ -15,7 +15,6 @@ import (
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/services/actions"
|
"code.gitea.io/gitea/services/actions"
|
||||||
"code.gitea.io/gitea/services/auth"
|
"code.gitea.io/gitea/services/auth"
|
||||||
forgefed_service "code.gitea.io/gitea/services/forgefed"
|
|
||||||
"code.gitea.io/gitea/services/migrations"
|
"code.gitea.io/gitea/services/migrations"
|
||||||
mirror_service "code.gitea.io/gitea/services/mirror"
|
mirror_service "code.gitea.io/gitea/services/mirror"
|
||||||
packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup"
|
packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup"
|
||||||
|
@ -188,40 +187,7 @@ func initBasicTasks() {
|
||||||
if setting.Packages.Enabled {
|
if setting.Packages.Enabled {
|
||||||
registerCleanupPackages()
|
registerCleanupPackages()
|
||||||
}
|
}
|
||||||
|
|
||||||
if setting.Actions.Enabled {
|
if setting.Actions.Enabled {
|
||||||
registerActionsCleanup()
|
registerActionsCleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
if setting.Federation.Enabled {
|
|
||||||
registerCleanupRemotePersonsWithNoFollowers()
|
|
||||||
registerUpdateRemotePersons()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerCleanupRemotePersonsWithNoFollowers() {
|
|
||||||
RegisterTaskFatal("remote_actor_cleanup", &OlderThanConfig{
|
|
||||||
BaseConfig: BaseConfig{
|
|
||||||
Enabled: true,
|
|
||||||
RunAtStart: true,
|
|
||||||
Schedule: "@midnight",
|
|
||||||
},
|
|
||||||
OlderThan: 24 * time.Hour,
|
|
||||||
}, func(ctx context.Context, _ *user_model.User, config Config) error {
|
|
||||||
acConfig := config.(*OlderThanConfig)
|
|
||||||
return forgefed_service.CleanUpRemotePersons(ctx, acConfig.OlderThan)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerUpdateRemotePersons() {
|
|
||||||
RegisterTaskFatal("remote_actor_update", &OlderThanConfig{
|
|
||||||
BaseConfig: BaseConfig{
|
|
||||||
Enabled: true,
|
|
||||||
RunAtStart: true,
|
|
||||||
Schedule: "@every 6h",
|
|
||||||
},
|
|
||||||
}, func(ctx context.Context, _ *user_model.User, config Config) error {
|
|
||||||
return forgefed_service.UpdatePersonActor(ctx)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,178 +0,0 @@
|
||||||
package forgefed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/federation"
|
|
||||||
"code.gitea.io/gitea/models/user"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
user_service "code.gitea.io/gitea/services/user"
|
|
||||||
|
|
||||||
ap "github.com/go-ap/activitypub"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetActor(id string) (*ap.Actor, error) {
|
|
||||||
client := http.Client{}
|
|
||||||
req, err := http.NewRequest("GET", id, nil)
|
|
||||||
if err != nil {
|
|
||||||
//Handle Error
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header = http.Header{
|
|
||||||
"Content-Type": {"application/activity+json"},
|
|
||||||
}
|
|
||||||
r, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer r.Body.Close()
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
actorObj := new(ap.Actor)
|
|
||||||
err = json.Unmarshal(body, &actorObj)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return actorObj, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetPersonAvatar(ctx context.Context, person *ap.Person) ([]byte, error) {
|
|
||||||
|
|
||||||
avatarObj := new(ap.Image)
|
|
||||||
ap.CopyItemProperties(avatarObj, person.Icon)
|
|
||||||
log.Info("Getting avatar from link : %s", avatarObj.URL.GetLink().String())
|
|
||||||
|
|
||||||
r, err := http.Get(avatarObj.URL.GetLink().String())
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Got error while fetching avatar fn: %w", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer r.Body.Close()
|
|
||||||
return io.ReadAll(r.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SavePerson(ctx context.Context, person *ap.Person) (*user.User, error) {
|
|
||||||
|
|
||||||
fmt.Println(person.ID.String())
|
|
||||||
hostname, err := GetHostnameFromResource(person.ID.String())
|
|
||||||
|
|
||||||
u, err := federation.CreateFederatedUser(ctx, person.PreferredUsername.String(), person.URL.GetID().String(), hostname)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Got error while saving person: %w", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
avatar, err := GetPersonAvatar(ctx, person)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Got error while fetching avatar: %w", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.IsUploadAvatarChanged(avatar) {
|
|
||||||
_ = user_service.UploadAvatar(ctx, u, avatar)
|
|
||||||
}
|
|
||||||
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetActorFromUser(ctx context.Context, u *user.User) (*ap.Actor, error) {
|
|
||||||
|
|
||||||
alias := u.Name
|
|
||||||
|
|
||||||
webfingerRes, err := WebFingerLookup(alias)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
actorID := webfingerRes.GetActorLink().Href
|
|
||||||
|
|
||||||
return GetActor(actorID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up remote actors (persons) without any followers in local instance
|
|
||||||
func CleanUpRemotePersons(ctx context.Context, olderThan time.Duration) error {
|
|
||||||
page := 0
|
|
||||||
for {
|
|
||||||
users, err := federation.GetRemoteUsersWithNoLocalFollowers(ctx, olderThan, page)
|
|
||||||
if len(users) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Trace("Error: CleanUpRemotePersons: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, u := range users {
|
|
||||||
err = user_service.DeleteUser(ctx, &u, false)
|
|
||||||
if err != nil {
|
|
||||||
log.Trace("Error: CleanUpRemotePersons: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
page += 1
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdatePersonActor(ctx context.Context) error {
|
|
||||||
// NOTE: change of any of these don't matter at this point since we are
|
|
||||||
// ignoring actor's PreferredUsername and using their address to generate
|
|
||||||
// username and email. Ask suggestions from other devs.
|
|
||||||
//
|
|
||||||
// fmt.Println(person.ID.String())
|
|
||||||
// hostname, err := GetHostnameFromResource(person.ID.String())
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// u := new(user.User)
|
|
||||||
// u.Name = "@" + person.PreferredUsername.String() + "@" + hostname
|
|
||||||
// //panic(u.Name)
|
|
||||||
// u.Email = person.PreferredUsername.String() + "@" + hostname
|
|
||||||
// u.Website = person.URL.GetID().String()
|
|
||||||
// u.KeepEmailPrivate = true
|
|
||||||
|
|
||||||
page := 0
|
|
||||||
for {
|
|
||||||
federatedUsers, err := federation.GetRemotePersons(ctx, page)
|
|
||||||
if len(federatedUsers) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Trace("Error: UpdatePersonActor: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range federatedUsers {
|
|
||||||
log.Info("Updating users, got %s", f.ExternalID)
|
|
||||||
u, err := user.GetUserByName(ctx, f.ExternalID)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Got error while getting user: %w", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
person, err := GetActorFromUser(ctx, u)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Got error while fetching actor: %w", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
avatar, err := GetPersonAvatar(ctx, person)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Got error while fetching avatar: %w", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.IsUploadAvatarChanged(avatar) {
|
|
||||||
_ = user_service.UploadAvatar(ctx, u, avatar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
page += 1
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,167 +0,0 @@
|
||||||
package forgefed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-webfinger-14#section-4.4
|
|
||||||
|
|
||||||
type WebfingerJRD struct {
|
|
||||||
Subject string `json:"subject,omitempty"`
|
|
||||||
Aliases []string `json:"aliases,omitempty"`
|
|
||||||
Properties map[string]any `json:"properties,omitempty"`
|
|
||||||
Links []*WebfingerLink `json:"links,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w WebfingerJRD) GetAvatar() *WebfingerLink {
|
|
||||||
for _, link := range w.Links {
|
|
||||||
if link.Rel == "http://webfinger.net/rel/avatar" {
|
|
||||||
return link
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w WebfingerJRD) GetProfilePage() *WebfingerLink {
|
|
||||||
for _, link := range w.Links {
|
|
||||||
if link.Rel == "http://webfinger.net/rel/profile-page" && link.Type == "text/html" {
|
|
||||||
return link
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w WebfingerJRD) GetActorLink() *WebfingerLink {
|
|
||||||
for _, link := range w.Links {
|
|
||||||
if link.Rel == "self" && link.Type == "application/activity+json" {
|
|
||||||
return link
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebfingerLink struct {
|
|
||||||
Rel string `json:"rel,omitempty"`
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
Href string `json:"href,omitempty"`
|
|
||||||
Titles map[string]string `json:"titles,omitempty"`
|
|
||||||
Properties map[string]any `json:"properties,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetHostnameFromResource(resource string) (string, error) {
|
|
||||||
r := resource
|
|
||||||
if strings.HasPrefix(resource, "@") {
|
|
||||||
resource, _ = strings.CutPrefix(resource, "@")
|
|
||||||
}
|
|
||||||
actor, err := url.Parse(resource)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostname string
|
|
||||||
switch actor.Scheme {
|
|
||||||
case "":
|
|
||||||
i := strings.Split(resource, "@")
|
|
||||||
if len(i) != 2 {
|
|
||||||
log.Error("Invalid webfinger query " + r)
|
|
||||||
return "", errors.New("Invalid webfinger query " + r)
|
|
||||||
}
|
|
||||||
hostname = i[1]
|
|
||||||
case "mailto":
|
|
||||||
i := strings.Split(resource, "@")
|
|
||||||
if len(i) != 2 {
|
|
||||||
log.Error("Invalid webfinger query " + r)
|
|
||||||
return "", errors.New("Invalid webfinger query " + r)
|
|
||||||
}
|
|
||||||
hostname = i[1]
|
|
||||||
case "https":
|
|
||||||
hostname = actor.Host
|
|
||||||
default:
|
|
||||||
log.Error("Invalid webfinger query " + r)
|
|
||||||
return "", errors.New("Invalid webfinger query" + r)
|
|
||||||
|
|
||||||
}
|
|
||||||
return hostname, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Actor object by performing webfinger lookup
|
|
||||||
func WebFingerLookup(q string) (*WebfingerJRD, error) {
|
|
||||||
if strings.HasPrefix(q, "@") {
|
|
||||||
q, _ = strings.CutPrefix(q, "@")
|
|
||||||
}
|
|
||||||
actor, err := url.Parse(q)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res string
|
|
||||||
switch actor.Scheme {
|
|
||||||
case "":
|
|
||||||
res = fmt.Sprintf("acct:%s", q)
|
|
||||||
case "mailto":
|
|
||||||
res = q
|
|
||||||
case "https":
|
|
||||||
res = q
|
|
||||||
default:
|
|
||||||
return nil, errors.New("Invalid webfinger query")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname, err := GetHostnameFromResource(q)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
link := fmt.Sprintf("https://%s/.well-known/webfinger?resource=%s", hostname, res)
|
|
||||||
|
|
||||||
r, err := http.Get(link)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
webfingerResponse := new(WebfingerJRD)
|
|
||||||
err = json.NewDecoder(r.Body).Decode(webfingerResponse)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return webfingerResponse, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsFingerable(resource string) bool {
|
|
||||||
if strings.HasPrefix(resource, "@") {
|
|
||||||
resource, _ = strings.CutPrefix(resource, "@")
|
|
||||||
}
|
|
||||||
actor, err := url.Parse(resource)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch actor.Scheme {
|
|
||||||
case "":
|
|
||||||
i := strings.Split(resource, "@")
|
|
||||||
if len(i) == 2 {
|
|
||||||
_ = i[1] // TODO: do len check before referencing element #2
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
case "mailto":
|
|
||||||
i := strings.Split(resource, "@")
|
|
||||||
if len(i) == 2 {
|
|
||||||
_ = i[1]
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
case "https":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/services/forgefed"
|
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -30,12 +29,27 @@ func TestWebfinger(t *testing.T) {
|
||||||
|
|
||||||
appURL, _ := url.Parse(setting.AppURL)
|
appURL, _ := url.Parse(setting.AppURL)
|
||||||
|
|
||||||
|
type webfingerLink struct {
|
||||||
|
Rel string `json:"rel,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Href string `json:"href,omitempty"`
|
||||||
|
Titles map[string]string `json:"titles,omitempty"`
|
||||||
|
Properties map[string]any `json:"properties,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type webfingerJRD struct {
|
||||||
|
Subject string `json:"subject,omitempty"`
|
||||||
|
Aliases []string `json:"aliases,omitempty"`
|
||||||
|
Properties map[string]any `json:"properties,omitempty"`
|
||||||
|
Links []*webfingerLink `json:"links,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
session := loginUser(t, "user1")
|
session := loginUser(t, "user1")
|
||||||
|
|
||||||
req := NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:%s@%s", user.LowerName, appURL.Host))
|
req := NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:%s@%s", user.LowerName, appURL.Host))
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
var jrd forgefed.WebfingerJRD
|
var jrd webfingerJRD
|
||||||
DecodeJSON(t, resp, &jrd)
|
DecodeJSON(t, resp, &jrd)
|
||||||
assert.Equal(t, "acct:user2@"+appURL.Host, jrd.Subject)
|
assert.Equal(t, "acct:user2@"+appURL.Host, jrd.Subject)
|
||||||
assert.ElementsMatch(t, []string{user.HTMLURL(), appURL.String() + "api/v1/activitypub/user-id/" + fmt.Sprint(user.ID)}, jrd.Aliases)
|
assert.ElementsMatch(t, []string{user.HTMLURL(), appURL.String() + "api/v1/activitypub/user-id/" + fmt.Sprint(user.ID)}, jrd.Aliases)
|
||||||
|
|
Loading…
Reference in a new issue