2023-11-16 14:49:05 +01:00
package activitypub
import (
"fmt"
"net/url"
2023-11-23 17:03:24 +01:00
"strconv"
2023-11-16 14:49:05 +01:00
"strings"
2023-11-24 12:48:14 +01:00
2023-12-06 13:06:30 +01:00
"code.gitea.io/gitea/modules/log"
2023-11-16 14:49:05 +01:00
)
2023-11-23 14:50:32 +01:00
type Validatable interface { // ToDo: What is the right package for this interface?
2023-11-24 11:37:29 +01:00
validate_is_not_nil ( ) error
2023-11-22 16:08:14 +01:00
validate_is_not_empty ( ) error
2023-11-22 15:25:43 +01:00
Validate ( ) error
2023-11-24 11:37:29 +01:00
IsValid ( ) ( bool , error )
PanicIfInvalid ( )
2023-11-22 15:25:43 +01:00
}
2023-11-22 13:28:13 +01:00
type ActorID struct {
2023-11-16 14:49:05 +01:00
userId string
2023-12-06 15:15:39 +01:00
source string
2023-11-24 12:49:36 +01:00
schema string
2023-11-16 14:49:05 +01:00
path string
host string
port string // optional
}
2023-12-07 11:24:27 +01:00
func validate_is_not_empty ( str string ) error {
2023-11-24 11:37:29 +01:00
if str == "" {
2023-12-07 11:24:27 +01:00
return fmt . Errorf ( "the given string was empty" )
2023-11-24 11:37:29 +01:00
}
return nil
}
/ *
2023-12-06 11:24:42 +01:00
Validate collects error strings in a slice and returns this
2023-11-24 11:37:29 +01:00
* /
func ( a ActorID ) Validate ( ) [ ] string {
2023-11-16 14:49:05 +01:00
2023-12-07 11:24:47 +01:00
var err = [ ] string { }
2023-11-24 11:37:29 +01:00
2023-12-07 11:24:47 +01:00
if res := validate_is_not_empty ( a . schema ) ; res != nil {
err = append ( err , strings . Join ( [ ] string { res . Error ( ) , "for schema field" } , " " ) )
2023-11-22 15:27:44 +01:00
}
2023-12-07 11:24:47 +01:00
if res := validate_is_not_empty ( a . host ) ; res != nil {
err = append ( err , strings . Join ( [ ] string { res . Error ( ) , "for host field" } , " " ) )
2023-11-16 14:49:05 +01:00
}
2023-11-24 12:48:14 +01:00
switch a . source {
case "forgejo" , "gitea" :
2023-12-06 15:16:01 +01:00
if ! strings . Contains ( a . path , "api/v1/activitypub/user-id" ) &&
! strings . Contains ( a . path , "api/v1/activitypub/repository-id" ) {
err = append ( err , fmt . Errorf ( "the Path to the API was invalid: ---%v---" , a . path ) . Error ( ) )
2023-11-24 12:48:14 +01:00
}
default :
err = append ( err , fmt . Errorf ( "currently only forgeo and gitea sources are allowed from actor id" ) . Error ( ) )
2023-11-16 14:49:05 +01:00
}
2023-11-24 11:37:29 +01:00
return err
2023-11-16 14:49:05 +01:00
}
2023-12-06 11:24:42 +01:00
/ *
IsValid concatenates the error messages with newlines and returns them if there are any
* /
2023-11-24 11:37:29 +01:00
func ( a ActorID ) IsValid ( ) ( bool , error ) {
if err := a . Validate ( ) ; len ( err ) > 0 {
errString := strings . Join ( err , "\n" )
return false , fmt . Errorf ( errString )
}
return true , nil
}
func ( a ActorID ) PanicIfInvalid ( ) {
if valid , err := a . IsValid ( ) ; ! valid {
panic ( err )
}
}
2023-11-24 12:49:36 +01:00
func ( a ActorID ) GetUserId ( ) int {
result , err := strconv . Atoi ( a . userId )
if err != nil {
panic ( err )
}
return result
}
2023-12-07 10:51:58 +01:00
func ( a ActorID ) GetNormalizedUri ( ) string {
2023-12-06 09:07:09 +01:00
result := fmt . Sprintf ( "%s://%s:%s/%s/%s" , a . schema , a . host , a . port , a . path , a . userId )
return result
}
2023-11-24 12:49:36 +01:00
// Returns the combination of host:port if port exists, host otherwise
func ( a ActorID ) GetHostAndPort ( ) string {
if a . port != "" {
return strings . Join ( [ ] string { a . host , a . port } , ":" )
}
return a . host
}
2023-12-06 13:06:30 +01:00
func containsEmptyString ( ar [ ] string ) bool {
for _ , elem := range ar {
if elem == "" {
return true
}
}
return false
}
func removeEmptyStrings ( ls [ ] string ) [ ] string {
var rs [ ] string
for _ , str := range ls {
if str != "" {
rs = append ( rs , str )
}
}
return rs
}
2023-12-08 11:54:07 +01:00
func ValidateAndParseIRI ( unvalidatedIRI string ) ( url . URL , error ) { // ToDo: Validate that it is not the same host as ours.
2023-12-07 11:44:59 +01:00
err := validate_is_not_empty ( unvalidatedIRI ) // url.Parse seems to accept empty strings?
if err != nil {
return url . URL { } , err
2023-12-06 16:14:39 +01:00
}
2023-12-07 11:44:59 +01:00
validatedURL , err := url . Parse ( unvalidatedIRI )
2023-11-16 14:49:05 +01:00
if err != nil {
2023-12-07 11:44:59 +01:00
return url . URL { } , err
2023-11-16 14:49:05 +01:00
}
2023-12-07 12:03:28 +01:00
if len ( validatedURL . Path ) <= 1 {
return url . URL { } , fmt . Errorf ( "path was empty" )
}
2023-12-07 11:44:59 +01:00
return * validatedURL , nil
}
// TODO: This parsing is very Person-Specific. We should adjust the name & move to a better location (maybe forgefed package?)
2023-12-08 11:54:07 +01:00
func ParseActorID ( validatedURL url . URL , source string ) ActorID { // ToDo: Turn this into a factory function and do not split parsing and validation rigurously
2023-12-07 11:44:59 +01:00
pathWithUserID := strings . Split ( validatedURL . Path , "/" )
2023-12-06 13:06:30 +01:00
if containsEmptyString ( pathWithUserID ) {
pathWithUserID = removeEmptyStrings ( pathWithUserID )
}
length := len ( pathWithUserID )
pathWithoutUserID := strings . Join ( pathWithUserID [ 0 : length - 1 ] , "/" )
userId := pathWithUserID [ length - 1 ]
log . Info ( "Actor: pathWithUserID: %s" , pathWithUserID )
log . Info ( "Actor: pathWithoutUserID: %s" , pathWithoutUserID )
log . Info ( "Actor: UserID: %s" , userId )
2023-11-16 14:49:05 +01:00
2023-11-24 09:55:47 +01:00
return ActorID { // ToDo: maybe keep original input to validate against (maybe extra method)
2023-11-16 14:49:05 +01:00
userId : userId ,
2023-12-06 15:15:39 +01:00
source : source ,
2023-12-07 11:44:59 +01:00
schema : validatedURL . Scheme ,
host : validatedURL . Hostname ( ) , // u.Host returns hostname:port
2023-12-06 13:06:30 +01:00
path : pathWithoutUserID ,
2023-12-07 11:44:59 +01:00
port : validatedURL . Port ( ) ,
}
2023-11-16 14:49:05 +01:00
}