Compare commits

...

10 Commits

Author SHA1 Message Date
Renovate Bot fd6265a07d Update ghcr.io/visualon/renovate Docker tag to v37.349.2 2024-05-07 12:03:12 +00:00
Earl Warren 99d1ae52fc Merge pull request 'Update module github.com/PuerkitoBio/goquery to v1.9.2' (#3634) from renovate/github.com-puerkitobio-goquery-1.x into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3634
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2024-05-07 11:28:32 +00:00
Earl Warren cae4a5456f
chore(dependency): automerge goquery when the CI passes 2024-05-07 11:57:25 +01:00
Earl Warren 3f6e4af5a9 Merge pull request 'UI: Hide hidden email from own profile, again' (#3636) from n0toose/profile/hide-unhidden-hidden-email into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3636
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Reviewed-by: Otto <otto@codeberg.org>
2024-05-07 10:39:01 +00:00
Earl Warren 420f017bce Merge pull request 'Update module github.com/cosmtrek/air to v1.52.0' (#3650) from renovate/github.com-cosmtrek-air-1.x into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3650
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2024-05-07 10:10:41 +00:00
Twenty Panda 44895011dc chore(dependencies): switch to air@v1
there are no tests but since Gitea uses @v1 since last month and Gitea
maintainers rely on make watch, it is safe to assume that upgrading is
not broken. Switching to v1 would require less scrutiny on the
upgrades. Even if there is breakage, it can be fixed with minimal
impact on the developer workflow.
2024-05-07 10:11:21 +01:00
Michael Jerger 2177d38e9c feat(federation): validate like activities (#3494)
First step on the way to #1680

The PR will

* accept like request on the api
* validate activity in a first level

You can find

* architecture at: https://codeberg.org/meissa/forgejo/src/branch/forgejo-federated-star/docs/unsure-where-to-put/federation-architecture.md

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3494
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: Michael Jerger <michael.jerger@meissa-gmbh.de>
Co-committed-by: Michael Jerger <michael.jerger@meissa-gmbh.de>
2024-05-07 07:59:49 +00:00
Renovate Bot 62c3540467 Update module github.com/cosmtrek/air to v1.52.0 2024-05-06 02:05:34 +00:00
Panagiotis "Ivory" Vasilopoulos d03be77665 UI: Hide hidden email from own profile, again
This is a follow-up for 5e1bd8af5f, which
was my first commit to Gitea. It is also a follow up for the
Gitea PR #29300 (https://github.com/go-gitea/gitea/pull/23900) created
by myself, which turned stale.

This change partially restores the behavior of Gitea PR #23747
(https://github.com/go-gitea/gitea/pull/23747) by wxiaoguang, but
maintains the lock.

The original idea was to differentiate things from GitHub and GitLab a
little bit, and show the email address on the profile. The profile is
not only a place where the user chooses to show how they present
themselves on an instance, it is also a place where they can assess
their relationship *with* the instance, as it provides features such
as the Public Activity feed that can be only shown to the user, in
private.

It's, in some way, a dashboard. The email was shown there to remind
the user that this is the primary email that will be used by a supposed
administrator to contact them. There were other motivations behind that
change as well, but, long story short, the idea did not work very well,
as some people (e.g. people livestreaming on the Internet, or 'normal'
users sharing their screens) do not want to put their email address
out there when showing their screen to other people.

Other alternatives, such as blurring the text or only showing the real
email address, were explored, but were rejected because of
browser compatibility and simplicity reasons. The padlock icon that
is shown when showing the email address to other people has been kept.
One viable alternative could be displaying the placeholder email
instead, but that requires some more thought.

Fixes https://codeberg.org/forgejo/forgejo/issues/1950.
2024-05-05 13:08:31 +02:00
Renovate Bot aa8a757fe2 Update module github.com/PuerkitoBio/goquery to v1.9.2 2024-05-05 00:05:34 +00:00
25 changed files with 1118 additions and 45 deletions

View File

@ -168,6 +168,14 @@ package "code.gitea.io/gitea/modules/emoji"
package "code.gitea.io/gitea/modules/eventsource"
func (*Event).String
package "code.gitea.io/gitea/modules/forgefed"
func NewForgeLike
func GetItemByType
func JSONUnmarshalerFn
func NotEmpty
func ToRepository
func OnRepository
package "code.gitea.io/gitea/modules/git"
func AllowLFSFiltersArgs
func AddChanges
@ -302,6 +310,9 @@ package "code.gitea.io/gitea/modules/translation"
package "code.gitea.io/gitea/modules/util/filebuffer"
func CreateFromReader
package "code.gitea.io/gitea/modules/validation"
func ValidateMaxLen
package "code.gitea.io/gitea/modules/web"
func RouteMock
func RouteMockReset

View File

@ -22,7 +22,7 @@ jobs:
runs-on: docker
container:
image: ghcr.io/visualon/renovate:37.340.9
image: ghcr.io/visualon/renovate:37.349.2
steps:
- name: Load renovate repo cache

View File

@ -26,7 +26,7 @@ DIFF ?= diff --unified
XGO_VERSION := go-1.21.x
AIR_PACKAGE ?= github.com/cosmtrek/air@v1.49.0 # renovate: datasource=go
AIR_PACKAGE ?= github.com/cosmtrek/air@v1 # renovate: datasource=go
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v2/cmd/editorconfig-checker@2.8.0 # renovate: datasource=go
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2 # renovate: datasource=go

10
go.mod
View File

@ -16,7 +16,7 @@ require (
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
github.com/ProtonMail/go-crypto v1.0.0
github.com/PuerkitoBio/goquery v1.8.1
github.com/PuerkitoBio/goquery v1.9.2
github.com/alecthomas/chroma/v2 v2.13.0
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.3.10
@ -94,16 +94,17 @@ require (
github.com/syndtr/goleveldb v1.0.0
github.com/ulikunitz/xz v0.5.11
github.com/urfave/cli/v2 v2.27.2
github.com/valyala/fastjson v1.6.4
github.com/xanzy/go-gitlab v0.96.0
github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.7.0
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
github.com/yuin/goldmark-meta v1.1.0
golang.org/x/crypto v0.21.0
golang.org/x/crypto v0.22.0
golang.org/x/image v0.15.0
golang.org/x/net v0.23.0
golang.org/x/net v0.24.0
golang.org/x/oauth2 v0.16.0
golang.org/x/sys v0.18.0
golang.org/x/sys v0.19.0
golang.org/x/text v0.14.0
golang.org/x/tools v0.17.0
google.golang.org/grpc v1.60.1
@ -265,7 +266,6 @@ require (
github.com/unknwon/com v1.0.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect

22
go.sum
View File

@ -93,8 +93,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68=
github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
@ -111,7 +111,6 @@ github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@ -885,8 +884,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -956,7 +955,6 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@ -966,8 +964,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1045,8 +1043,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -1057,8 +1055,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -0,0 +1,65 @@
// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgefed
import (
"time"
"code.gitea.io/gitea/modules/validation"
ap "github.com/go-ap/activitypub"
)
// ForgeLike activity data type
// swagger:model
type ForgeLike struct {
// swagger:ignore
ap.Activity
}
func NewForgeLike(actorIRI, objectIRI string, startTime time.Time) (ForgeLike, error) {
result := ForgeLike{}
result.Type = ap.LikeType
result.Actor = ap.IRI(actorIRI) // Thats us, a User
result.Object = ap.IRI(objectIRI) // Thats them, a Repository
result.StartTime = startTime
if valid, err := validation.IsValid(result); !valid {
return ForgeLike{}, err
}
return result, nil
}
func (like ForgeLike) MarshalJSON() ([]byte, error) {
return like.Activity.MarshalJSON()
}
func (like *ForgeLike) UnmarshalJSON(data []byte) error {
return like.Activity.UnmarshalJSON(data)
}
func (like ForgeLike) IsNewer(compareTo time.Time) bool {
return like.StartTime.After(compareTo)
}
func (like ForgeLike) Validate() []string {
var result []string
result = append(result, validation.ValidateNotEmpty(string(like.Type), "type")...)
result = append(result, validation.ValidateOneOf(string(like.Type), []any{"Like"}, "type")...)
if like.Actor == nil {
result = append(result, "Actor should not be nil.")
} else {
result = append(result, validation.ValidateNotEmpty(like.Actor.GetID().String(), "actor")...)
}
if like.Object == nil {
result = append(result, "Object should not be nil.")
} else {
result = append(result, validation.ValidateNotEmpty(like.Object.GetID().String(), "object")...)
}
result = append(result, validation.ValidateNotEmpty(like.StartTime.String(), "startTime")...)
if like.StartTime.IsZero() {
result = append(result, "StartTime was invalid.")
}
return result
}

View File

@ -0,0 +1,171 @@
// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgefed
import (
"fmt"
"reflect"
"strings"
"testing"
"time"
"code.gitea.io/gitea/modules/validation"
ap "github.com/go-ap/activitypub"
)
func Test_NewForgeLike(t *testing.T) {
actorIRI := "https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"
objectIRI := "https://codeberg.org/api/v1/activitypub/repository-id/1"
want := []byte(`{"type":"Like","startTime":"2024-03-27T00:00:00Z","actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1","object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}`)
startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-27")
sut, err := NewForgeLike(actorIRI, objectIRI, startTime)
if err != nil {
t.Errorf("unexpected error: %v\n", err)
}
if valid, _ := validation.IsValid(sut); !valid {
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
}
got, err := sut.MarshalJSON()
if err != nil {
t.Errorf("MarshalJSON() error = \"%v\"", err)
return
}
if !reflect.DeepEqual(got, want) {
t.Errorf("MarshalJSON() got = %q, want %q", got, want)
}
}
func Test_LikeMarshalJSON(t *testing.T) {
type testPair struct {
item ForgeLike
want []byte
wantErr error
}
tests := map[string]testPair{
"empty": {
item: ForgeLike{},
want: nil,
},
"with ID": {
item: ForgeLike{
Activity: ap.Activity{
Actor: ap.IRI("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"),
Type: "Like",
Object: ap.IRI("https://codeberg.org/api/v1/activitypub/repository-id/1"),
},
},
want: []byte(`{"type":"Like","actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1","object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}`),
},
}
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_LikeUnmarshalJSON(t *testing.T) {
type testPair struct {
item []byte
want *ForgeLike
wantErr error
}
//revive:disable
tests := map[string]testPair{
"with ID": {
item: []byte(`{"type":"Like","actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1","object":"https://codeberg.org/api/activitypub/repository-id/1"}`),
want: &ForgeLike{
Activity: ap.Activity{
Actor: ap.IRI("https://repo.prod.meissa.de/api/activitypub/user-id/1"),
Type: "Like",
Object: ap.IRI("https://codeberg.org/api/activitypub/repository-id/1"),
},
},
wantErr: nil,
},
"invalid": {
item: []byte(`{"type":"Invalid","actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1","object":"https://codeberg.org/api/activitypub/repository-id/1"`),
want: &ForgeLike{},
wantErr: fmt.Errorf("cannot parse JSON:"),
},
}
//revive:enable
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got := new(ForgeLike)
err := got.UnmarshalJSON(test.item)
if (err != nil || test.wantErr != nil) && !strings.Contains(err.Error(), test.wantErr.Error()) {
t.Errorf("UnmarshalJSON() error = \"%v\", wantErr \"%v\"", err, test.wantErr)
return
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("UnmarshalJSON() got = %q, want %q, err %q", got, test.want, err.Error())
}
})
}
}
func TestActivityValidation(t *testing.T) {
sut := new(ForgeLike)
sut.UnmarshalJSON([]byte(`{"type":"Like",
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
"object":"https://codeberg.org/api/activitypub/repository-id/1",
"startTime": "2014-12-31T23:00:00-08:00"}`))
if res, _ := validation.IsValid(sut); !res {
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
}
sut.UnmarshalJSON([]byte(`{"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
"object":"https://codeberg.org/api/activitypub/repository-id/1",
"startTime": "2014-12-31T23:00:00-08:00"}`))
if sut.Validate()[0] != "type should not be empty" {
t.Errorf("validation error expected but was: %v\n", sut.Validate()[0])
}
sut.UnmarshalJSON([]byte(`{"type":"bad-type",
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
"object":"https://codeberg.org/api/activitypub/repository-id/1",
"startTime": "2014-12-31T23:00:00-08:00"}`))
if sut.Validate()[0] != "Value bad-type is not contained in allowed values [Like]" {
t.Errorf("validation error expected but was: %v\n", sut.Validate()[0])
}
sut.UnmarshalJSON([]byte(`{"type":"Like",
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
"object":"https://codeberg.org/api/activitypub/repository-id/1",
"startTime": "not a date"}`))
if sut.Validate()[0] != "StartTime was invalid." {
t.Errorf("validation error expected but was: %v\n", sut.Validate())
}
sut.UnmarshalJSON([]byte(`{"type":"Wrong",
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
"object":"https://codeberg.org/api/activitypub/repository-id/1",
"startTime": "2014-12-31T23:00:00-08:00"}`))
if sut.Validate()[0] != "Value Wrong is not contained in allowed values [Like]" {
t.Errorf("validation error expected but was: %v\n", sut.Validate())
}
}
func TestActivityValidation_Attack(t *testing.T) {
sut := new(ForgeLike)
sut.UnmarshalJSON([]byte(`{rubbish}`))
if len(sut.Validate()) != 5 {
t.Errorf("5 validateion errors expected but was: %v\n", len(sut.Validate()))
}
}

View File

@ -0,0 +1,49 @@
// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
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 RepositoryType:
return RepositoryNew(""), 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 RepositoryType:
return OnRepository(i, func(r *Repository) error {
return JSONLoadRepository(val, r)
})
}
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 RepositoryType:
r, err := ToRepository(i)
if err != nil {
return false
}
return ap.NotEmpty(r.Actor)
}
return ap.NotEmpty(i)
}

View File

@ -0,0 +1,111 @@
// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
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.JSONWriteItemProp(&b, "team", r.Team)
}
if r.Forks != nil {
ap.JSONWriteItemProp(&b, "forks", r.Forks)
}
if r.ForkedFrom != nil {
ap.JSONWriteItemProp(&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)
}

View File

@ -0,0 +1,145 @@
// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgefed
import (
"fmt"
"reflect"
"testing"
"code.gitea.io/gitea/modules/json"
ap "github.com/go-ap/activitypub"
)
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)
}
})
}
}

View File

@ -0,0 +1,67 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package validation
import (
"fmt"
"strings"
"unicode/utf8"
"code.gitea.io/gitea/modules/timeutil"
)
type Validateable interface {
Validate() []string
}
func IsValid(v Validateable) (bool, error) {
if err := v.Validate(); len(err) > 0 {
errString := strings.Join(err, "\n")
return false, fmt.Errorf(errString)
}
return true, nil
}
func ValidateNotEmpty(value any, name string) []string {
isValid := true
switch v := value.(type) {
case string:
if v == "" {
isValid = false
}
case timeutil.TimeStamp:
if v.IsZero() {
isValid = false
}
case int64:
if v == 0 {
isValid = false
}
default:
isValid = false
}
if isValid {
return []string{}
}
return []string{fmt.Sprintf("%v should not be empty", name)}
}
func ValidateMaxLen(value string, maxLen int, name string) []string {
if utf8.RuneCountInString(value) > maxLen {
return []string{fmt.Sprintf("Value %v was longer than %v", name, maxLen)}
}
return []string{}
}
func ValidateOneOf(value any, allowed []any, name string) []string {
for _, allowedElem := range allowed {
if value == allowedElem {
return []string{}
}
}
return []string{fmt.Sprintf("Value %v is not contained in allowed values %v", value, allowed)}
}

View File

@ -0,0 +1,65 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package validation
import (
"testing"
"code.gitea.io/gitea/modules/timeutil"
)
type Sut struct {
valid bool
}
func (sut Sut) Validate() []string {
if sut.valid {
return []string{}
}
return []string{"invalid"}
}
func Test_IsValid(t *testing.T) {
sut := Sut{valid: true}
if res, _ := IsValid(sut); !res {
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
}
sut = Sut{valid: false}
if res, _ := IsValid(sut); res {
t.Errorf("sut expected to be invalid: %v\n", sut.Validate())
}
}
func Test_ValidateNotEmpty_ForString(t *testing.T) {
sut := ""
if len(ValidateNotEmpty(sut, "dummyField")) == 0 {
t.Errorf("sut should be invalid")
}
sut = "not empty"
if res := ValidateNotEmpty(sut, "dummyField"); len(res) > 0 {
t.Errorf("sut should be valid but was %q", res)
}
}
func Test_ValidateNotEmpty_ForTimestamp(t *testing.T) {
sut := timeutil.TimeStamp(0)
if res := ValidateNotEmpty(sut, "dummyField"); len(res) == 0 {
t.Errorf("sut should be invalid")
}
sut = timeutil.TimeStampNow()
if res := ValidateNotEmpty(sut, "dummyField"); len(res) > 0 {
t.Errorf("sut should be valid but was %q", res)
}
}
func Test_ValidateMaxLen(t *testing.T) {
sut := "0123456789"
if len(ValidateMaxLen(sut, 9, "dummyField")) == 0 {
t.Errorf("sut should be invalid")
}
sut = "0123456789"
if res := ValidateMaxLen(sut, 11, "dummyField"); len(res) > 0 {
t.Errorf("sut should be valid but was %q", res)
}
}

View File

@ -676,7 +676,6 @@ unblock = Unblock
user_bio = Biography
disabled_public_activity = This user has disabled the public visibility of the activity.
email_visibility.limited = Your email address is visible to all authenticated users
email_visibility.private = Your email address is only visible to you and administrators
show_on_map = Show this place on a map
settings = User settings

View File

@ -97,7 +97,7 @@
{
"description": "Automerge some packages when ci succeeds",
"extends": ["packages:linters"],
"matchDepNames": ["vitest", "vite-string-plugin"],
"matchDepNames": ["github.com/PuerkitoBio/goquery", "vitest", "vite-string-plugin"],
"automerge": true
},
{

View File

@ -0,0 +1,83 @@
// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package activitypub
import (
"fmt"
"net/http"
"strings"
"code.gitea.io/gitea/modules/forgefed"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/federation"
ap "github.com/go-ap/activitypub"
)
// Repository function returns the Repository actor for a repo
func Repository(ctx *context.APIContext) {
// swagger:operation GET /activitypub/repository-id/{repository-id} activitypub activitypubRepository
// ---
// summary: Returns the Repository actor for a repo
// produces:
// - application/json
// parameters:
// - name: repository-id
// in: path
// description: repository ID of the repo
// type: integer
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActivityPub"
link := fmt.Sprintf("%s/api/v1/activitypub/repository-id/%d", strings.TrimSuffix(setting.AppURL, "/"), ctx.Repo.Repository.ID)
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.Error(http.StatusInternalServerError, "Set Name", err)
return
}
response(ctx, repo)
}
// PersonInbox function handles the incoming data for a repository inbox
func RepositoryInbox(ctx *context.APIContext) {
// swagger:operation POST /activitypub/repository-id/{repository-id}/inbox activitypub activitypubRepositoryInbox
// ---
// summary: Send to the inbox
// produces:
// - application/json
// parameters:
// - name: repository-id
// in: path
// description: repository ID of the repo
// type: integer
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/ForgeLike"
// responses:
// "204":
// "$ref": "#/responses/empty"
repository := ctx.Repo.Repository
log.Info("RepositoryInbox: repo: %v", repository)
form := web.GetForm(ctx)
httpStatus, title, err := federation.ProcessLikeActivity(ctx, form, repository.ID)
if err != nil {
log.Error("Status: %v", httpStatus)
log.Error("Title: %v", title)
log.Error("Error: %v", err)
ctx.Error(httpStatus, title, err)
}
ctx.Status(http.StatusNoContent)
}

View File

@ -0,0 +1,27 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package activitypub
import (
"testing"
"code.gitea.io/gitea/models/user"
)
func Test_UserEmailValidate(t *testing.T) {
sut := "ab@cd.ef"
if err := user.ValidateEmail(sut); err != nil {
t.Errorf("sut should be valid, %v, %v", sut, err)
}
sut = "83ce13c8-af0b-4112-8327-55a54e54e664@code.cartoon-aa.xyz"
if err := user.ValidateEmail(sut); err != nil {
t.Errorf("sut should be valid, %v, %v", sut, err)
}
sut = "1"
if err := user.ValidateEmail(sut); err == nil {
t.Errorf("sut should not be valid, %v", sut)
}
}

View File

@ -0,0 +1,35 @@
// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package activitypub
import (
"net/http"
"code.gitea.io/gitea/modules/activitypub"
"code.gitea.io/gitea/modules/forgefed"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/services/context"
ap "github.com/go-ap/activitypub"
"github.com/go-ap/jsonld"
)
// Respond with an ActivityStreams object
func response(ctx *context.APIContext, v any) {
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)
}
}

View File

@ -1,5 +1,6 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2016 The Gitea Authors. All rights reserved.
// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package v1 Gitea API
@ -79,6 +80,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/forgefed"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
@ -802,6 +804,13 @@ func Routes() *web.Route {
m.Get("", activitypub.Person)
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
}, context.UserIDAssignmentAPI())
m.Group("/repository-id/{repository-id}", func() {
m.Get("", activitypub.Repository)
m.Post("/inbox",
bind(forgefed.ForgeLike{}),
// TODO: activitypub.ReqHTTPSignature(),
activitypub.RepositoryInbox)
}, context.RepositoryIDAssignmentAPI())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
}

View File

@ -1,9 +1,11 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import (
ffed "code.gitea.io/gitea/modules/forgefed"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/forms"
)
@ -14,6 +16,9 @@ import (
// parameterBodies
// swagger:response parameterBodies
type swaggerParameterBodies struct {
// in:body
ForgeLike ffed.ForgeLike
// in:body
AddCollaboratorOption api.AddCollaboratorOption

View File

@ -0,0 +1,25 @@
// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package context
import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
)
// RepositoryIDAssignmentAPI returns a middleware to handle context-repo assignment for api routes
func RepositoryIDAssignmentAPI() func(ctx *APIContext) {
return func(ctx *APIContext) {
repositoryID := ctx.ParamsInt64(":repository-id")
var err error
repository := new(Repository)
repository.Repository, err = repo_model.GetRepositoryByID(ctx, repositoryID)
if err != nil {
ctx.Error(http.StatusNotFound, "GetRepositoryByID", err)
}
ctx.Repo = repository
}
}

View File

@ -0,0 +1,30 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package federation
import (
"context"
"net/http"
fm "code.gitea.io/gitea/modules/forgefed"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/validation"
)
// ProcessLikeActivity receives a ForgeLike activity and does the following:
// Validation of the activity
// Creation of a (remote) federationHost if not existing
// Creation of a forgefed Person if not existing
// Validation of incoming RepositoryID against Local RepositoryID
// Star the repo if it wasn't already stared
// Do some mitigation against out of order attacks
func ProcessLikeActivity(ctx context.Context, form any, repositoryID int64) (int, string, error) {
activity := form.(*fm.ForgeLike)
if res, err := validation.IsValid(activity); !res {
return http.StatusNotAcceptable, "Invalid activity", err
}
log.Info("Activity validated:%v", activity)
return 0, "", nil
}

View File

@ -38,29 +38,18 @@
{{end}}
</li>
{{end}}
{{if (eq .SignedUserID .ContextUser.ID)}}
<li>
{{svg "octicon-mail"}}
<a class="tw-flex-1" href="mailto:{{.ContextUser.Email}}" rel="nofollow">{{.ContextUser.Email}}</a>
<a href="{{AppSubUrl}}/user/settings#privacy-user-settings">
{{if .ShowUserEmail}}
<i data-tooltip-content="{{ctx.Locale.Tr "user.email_visibility.limited"}}">
{{svg "octicon-unlock"}}
</i>
{{else}}
<i data-tooltip-content="{{ctx.Locale.Tr "user.email_visibility.private"}}">
{{svg "octicon-lock"}}
</i>
{{end}}
</a>
</li>
{{else}}
{{if .ShowUserEmail}}
{{if .ShowUserEmail}}
<li>
{{svg "octicon-mail"}}
<a href="mailto:{{.ContextUser.Email}}" rel="nofollow">{{.ContextUser.Email}}</a>
<a class="tw-flex-1" href="mailto:{{.ContextUser.Email}}" rel="nofollow">{{.ContextUser.Email}}</a>
{{if (eq .SignedUserID .ContextUser.ID)}}
<a href="{{AppSubUrl}}/user/settings#privacy-user-settings">
<i data-tooltip-content="{{ctx.Locale.Tr "user.email_visibility.limited"}}">
{{svg "octicon-unlock"}}
</i>
</a>
{{end}}
</li>
{{end}}
{{end}}
{{if .ContextUser.Website}}
<li>

View File

@ -23,6 +23,65 @@
},
"basePath": "{{AppSubUrl | JSEscape}}/api/v1",
"paths": {
"/activitypub/repository-id/{repository-id}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"activitypub"
],
"summary": "Returns the Repository actor for a repo",
"operationId": "activitypubRepository",
"parameters": [
{
"type": "integer",
"description": "repository ID of the repo",
"name": "repository-id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/ActivityPub"
}
}
}
},
"/activitypub/repository-id/{repository-id}/inbox": {
"post": {
"produces": [
"application/json"
],
"tags": [
"activitypub"
],
"summary": "Send to the inbox",
"operationId": "activitypubRepositoryInbox",
"parameters": [
{
"type": "integer",
"description": "repository ID of the repo",
"name": "repository-id",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/ForgeLike"
}
}
],
"responses": {
"204": {
"$ref": "#/responses/empty"
}
}
}
},
"/activitypub/user-id/{user-id}": {
"get": {
"produces": [
@ -21373,6 +21432,11 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"ForgeLike": {
"description": "ForgeLike activity data type",
"type": "object",
"x-go-package": "code.gitea.io/gitea/modules/forgefed"
},
"GPGKey": {
"description": "GPGKey a user GPG key to sign commit and tag in repository",
"type": "object",

View File

@ -0,0 +1,125 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/activitypub"
forgefed_modules "code.gitea.io/gitea/modules/forgefed"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers"
"github.com/stretchr/testify/assert"
)
func TestActivityPubRepository(t *testing.T) {
setting.Federation.Enabled = true
testWebRoutes = routers.NormalRoutes()
defer func() {
setting.Federation.Enabled = false
testWebRoutes = routers.NormalRoutes()
}()
onGiteaRun(t, func(*testing.T, *url.URL) {
repositoryID := 2
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/activitypub/repository-id/%v", repositoryID))
resp := MakeRequest(t, req, http.StatusOK)
body := resp.Body.Bytes()
assert.Contains(t, string(body), "@context")
var repository forgefed_modules.Repository
err := repository.UnmarshalJSON(body)
assert.NoError(t, err)
assert.Regexp(t, fmt.Sprintf("activitypub/repository-id/%v$", repositoryID), repository.GetID().String())
})
}
func TestActivityPubMissingRepository(t *testing.T) {
setting.Federation.Enabled = true
testWebRoutes = routers.NormalRoutes()
defer func() {
setting.Federation.Enabled = false
testWebRoutes = routers.NormalRoutes()
}()
onGiteaRun(t, func(*testing.T, *url.URL) {
repositoryID := 9999999
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/activitypub/repository-id/%v", repositoryID))
resp := MakeRequest(t, req, http.StatusNotFound)
assert.Contains(t, resp.Body.String(), "repository does not exist")
})
}
func TestActivityPubRepositoryInboxValid(t *testing.T) {
setting.Federation.Enabled = true
testWebRoutes = routers.NormalRoutes()
defer func() {
setting.Federation.Enabled = false
testWebRoutes = routers.NormalRoutes()
}()
srv := httptest.NewServer(testWebRoutes)
defer srv.Close()
onGiteaRun(t, func(*testing.T, *url.URL) {
appURL := setting.AppURL
setting.AppURL = srv.URL + "/"
defer func() {
setting.Database.LogSQL = false
setting.AppURL = appURL
}()
actionsUser := user.NewActionsUser()
repositoryID := 2
c, err := activitypub.NewClient(db.DefaultContext, actionsUser, "not used")
assert.NoError(t, err)
repoInboxURL := fmt.Sprintf("%s/api/v1/activitypub/repository-id/%v/inbox",
srv.URL, repositoryID)
activity := []byte(fmt.Sprintf(`{"type":"Like","startTime":"2024-03-27T00:00:00Z","actor":"%s/api/v1/activitypub/user-id/2","object":"%s/api/v1/activitypub/repository-id/%v"}`,
srv.URL, srv.URL, repositoryID))
resp, err := c.Post(activity, repoInboxURL)
assert.NoError(t, err)
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
})
}
func TestActivityPubRepositoryInboxInvalid(t *testing.T) {
setting.Federation.Enabled = true
testWebRoutes = routers.NormalRoutes()
defer func() {
setting.Federation.Enabled = false
testWebRoutes = routers.NormalRoutes()
}()
srv := httptest.NewServer(testWebRoutes)
defer srv.Close()
onGiteaRun(t, func(*testing.T, *url.URL) {
appURL := setting.AppURL
setting.AppURL = srv.URL + "/"
defer func() {
setting.Database.LogSQL = false
setting.AppURL = appURL
}()
actionsUser := user.NewActionsUser()
repositoryID := 2
c, err := activitypub.NewClient(db.DefaultContext, actionsUser, "not used")
assert.NoError(t, err)
repoInboxURL := fmt.Sprintf("%s/api/v1/activitypub/repository-id/%v/inbox",
srv.URL, repositoryID)
activity := []byte(`{"type":"Wrong"}`)
resp, err := c.Post(activity, repoInboxURL)
assert.NoError(t, err)
assert.Equal(t, http.StatusNotAcceptable, resp.StatusCode)
})
}

View File

@ -75,21 +75,21 @@ func TestSettingShowUserEmailProfile(t *testing.T) {
htmlDoc = NewHTMLParser(t, resp.Body)
assert.Contains(t, htmlDoc.doc.Find(".user.profile").Text(), "user1@example.com")
// user2 can see own hidden email
// user2 cannot see own hidden email
session = loginUser(t, "user2")
req = NewRequest(t, "GET", "/user2")
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
assert.Contains(t, htmlDoc.doc.Find(".user.profile").Text(), "user2@example.com")
assert.NotContains(t, htmlDoc.doc.Find(".user.profile").Text(), "user2@example.com")
setting.UI.ShowUserEmail = false
// user1 can see own (now hidden) email
// user1 cannot see own (now hidden) email
session = loginUser(t, "user1")
req = NewRequest(t, "GET", "/user1")
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
assert.Contains(t, htmlDoc.doc.Find(".user.profile").Text(), "user1@example.com")
assert.NotContains(t, htmlDoc.doc.Find(".user.profile").Text(), "user1@example.com")
setting.UI.ShowUserEmail = showUserEmail
}