diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index b79a79fb2..125b7ee3b 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/sitemap" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forgefed" ) const ( @@ -99,6 +100,31 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions, return } } + + 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 isSitemap { m := sitemap.NewSitemap() for _, item := range users { diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go index a30844360..8a6a1acc4 100644 --- a/routers/web/webfinger.go +++ b/routers/web/webfinger.go @@ -13,25 +13,11 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "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 -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 // https://datatracker.ietf.org/doc/html/rfc7565 func WebfingerQuery(ctx *context.Context) { @@ -104,7 +90,7 @@ func WebfingerQuery(ctx *context.Context) { aliases = append(aliases, fmt.Sprintf("mailto:%s", u.Email)) } - links := []*webfingerLink{ + links := []*forgefed.WebfingerLink{ { Rel: "http://webfinger.net/rel/profile-page", Type: "text/html", @@ -127,7 +113,7 @@ func WebfingerQuery(ctx *context.Context) { ctx.Resp.Header().Add("Content-Type", "application/jrd+json") ctx.Resp.Header().Add("Access-Control-Allow-Origin", "*") - ctx.JSON(http.StatusOK, &webfingerJRD{ + ctx.JSON(http.StatusOK, &forgefed.WebfingerJRD{ Subject: fmt.Sprintf("acct:%s@%s", url.QueryEscape(u.Name), appURL.Host), Aliases: aliases, Links: links, diff --git a/services/forgefed/webfinger.go b/services/forgefed/webfinger.go new file mode 100644 index 000000000..2b7a64111 --- /dev/null +++ b/services/forgefed/webfinger.go @@ -0,0 +1,167 @@ +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 + } +}