diff options
author | FChannel <> | 2022-04-30 11:00:55 -0700 |
---|---|---|
committer | FChannel <> | 2022-06-19 12:53:29 -0700 |
commit | 1892327cee2c3fa1d3bea729bd08eb63c2189a96 (patch) | |
tree | 7b846f7d9caf46fba6c9d15ff81b9d89dcca9476 /activitypub | |
parent | 5b52d269faa2ce2014d0feba603a2122361cf4eb (diff) |
restructured code base to prevent circular dependicies
Diffstat (limited to 'activitypub')
-rw-r--r-- | activitypub/activity.go | 524 | ||||
-rw-r--r-- | activitypub/actor.go | 853 | ||||
-rw-r--r-- | activitypub/object.go | 1802 | ||||
-rw-r--r-- | activitypub/pem.go | 241 |
4 files changed, 3420 insertions, 0 deletions
diff --git a/activitypub/activity.go b/activitypub/activity.go new file mode 100644 index 0000000..aef060b --- /dev/null +++ b/activitypub/activity.go @@ -0,0 +1,524 @@ +package activitypub + +import ( + "crypto" + crand "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "os" + "regexp" + "strings" + + "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/util" + "github.com/gofiber/fiber/v2" +) + +// False positive for application/ld+ld, application/activity+ld, application/json+json +var activityRegexp = regexp.MustCompile("application\\/(ld|json|activity)((\\+(ld|json))|$)") + +func AcceptActivity(header string) bool { + accept := false + if strings.Contains(header, ";") { + split := strings.Split(header, ";") + accept = accept || activityRegexp.MatchString(split[0]) + accept = accept || strings.Contains(split[len(split)-1], "profile=\"https://www.w3.org/ns/activitystreams\"") + } else { + accept = accept || activityRegexp.MatchString(header) + } + return accept +} + +func ActivitySign(actor Actor, signature string) (string, error) { + query := `select file from publicKeyPem where id=$1 ` + + rows, err := config.DB.Query(query, actor.PublicKey.Id) + if err != nil { + return "", err + } + + defer rows.Close() + + var file string + rows.Next() + rows.Scan(&file) + + file = strings.ReplaceAll(file, "public.pem", "private.pem") + _, err = os.Stat(file) + if err != nil { + fmt.Println(`\n Unable to locate private key. Now, +this means that you are now missing the proof that you are the +owner of the "` + actor.Name + `" board. If you are the developer, +then your job is just as easy as generating a new keypair, but +if this board is live, then you'll also have to convince the other +owners to switch their public keys for you so that they will start +accepting your posts from your board from this site. Good luck ;)`) + return "", errors.New("unable to locate private key") + } + + publickey, err := ioutil.ReadFile(file) + if err != nil { + return "", err + } + + block, _ := pem.Decode(publickey) + + pub, _ := x509.ParsePKCS1PrivateKey(block.Bytes) + rng := crand.Reader + hashed := sha256.New() + hashed.Write([]byte(signature)) + cipher, _ := rsa.SignPKCS1v15(rng, pub, crypto.SHA256, hashed.Sum(nil)) + + return base64.StdEncoding.EncodeToString(cipher), nil +} + +func DeleteReportActivity(id string) error { + query := `delete from reported where id=$1` + + _, err := config.DB.Exec(query, id) + return err +} + +func GetActivityFromDB(id string) (Collection, error) { + var nColl Collection + var nActor Actor + var result []ObjectBase + + nColl.Actor = &nActor + + query := `select actor, id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where id=$1 order by updated asc` + + rows, err := config.DB.Query(query, id) + if err != nil { + return nColl, err + } + + defer rows.Close() + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + var previewID string + + if err := rows.Scan(&nColl.Actor.Id, &post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.Updated, &post.AttributedTo, &attachID, &previewID, &actor.Id, &post.TripCode, &post.Sensitive); err != nil { + return nColl, err + } + + post.Actor = actor.Id + + var postCnt int + var imgCnt int + var err error + post.Replies, postCnt, imgCnt, err = GetObjectRepliesDB(post) + if err != nil { + return nColl, err + } + + post.Replies.TotalItems = postCnt + post.Replies.TotalImgs = imgCnt + + post.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return nColl, err + } + + post.Preview, err = GetObjectPreview(previewID) + if err != nil { + return nColl, err + } + + result = append(result, post) + } + + nColl.OrderedItems = result + + return nColl, nil +} + +func GetActivityFromJson(ctx *fiber.Ctx) (Activity, error) { + + var respActivity ActivityRaw + var nActivity Activity + var nType string + + if err := json.Unmarshal(ctx.Body(), &respActivity); err != nil { + return nActivity, err + } + + if res, err := HasContextFromJson(respActivity.AtContextRaw.Context); err == nil && res { + var jObj ObjectBase + + if respActivity.Type == "Note" { + jObj, err = GetObjectFromJson(ctx.Body()) + if err != nil { + return nActivity, err + } + + nType = "Create" + } else { + jObj, err = GetObjectFromJson(respActivity.ObjectRaw) + if err != nil { + return nActivity, err + } + + nType = respActivity.Type + } + + actor, err := GetActorFromJson(respActivity.ActorRaw) + if err != nil { + return nActivity, err + } + + to, err := GetToFromJson(respActivity.ToRaw) + if err != nil { + return nActivity, err + } + + cc, err := GetToFromJson(respActivity.CcRaw) + if err != nil { + return nActivity, err + } + + nActivity.AtContext.Context = "https://www.w3.org/ns/activitystreams" + nActivity.Type = nType + nActivity.Actor = &actor + nActivity.Published = respActivity.Published + nActivity.Auth = respActivity.Auth + + if len(to) > 0 { + nActivity.To = to + } + + if len(cc) > 0 { + nActivity.Cc = cc + } + + nActivity.Name = respActivity.Name + nActivity.Object = &jObj + } else if err != nil { + return nActivity, err + } + + return nActivity, nil +} + +func HasContextFromJson(context []byte) (bool, error) { + var generic interface{} + + err := json.Unmarshal(context, &generic) + if err != nil { + return false, err + } + + hasContext := false + + switch generic.(type) { + case []interface{}: + var arrContext AtContextArray + err = json.Unmarshal(context, &arrContext.Context) + if len(arrContext.Context) > 0 { + if arrContext.Context[0] == "https://www.w3.org/ns/activitystreams" { + hasContext = true + } + } + break + + case string: + var arrContext AtContextString + err = json.Unmarshal(context, &arrContext.Context) + if arrContext.Context == "https://www.w3.org/ns/activitystreams" { + hasContext = true + } + break + } + + return hasContext, err +} + +func IsActivityLocal(activity Activity) (bool, error) { + for _, e := range activity.To { + if res, err := GetActorFromDB(e); err == nil && res.Id != "" { + return true, nil + } else if err != nil { + return false, err + } + } + + for _, e := range activity.Cc { + if res, err := GetActorFromDB(e); err == nil && res.Id != "" { + return true, nil + } else if err != nil { + return false, err + } + } + + if res, err := GetActorFromDB(activity.Actor.Id); err == nil && activity.Actor != nil && res.Id != "" { + return true, nil + } else if err != nil { + return false, err + } + + return false, nil +} + +func ProcessActivity(activity Activity) error { + activityType := activity.Type + + if activityType == "Create" { + for _, e := range activity.To { + if res, err := GetActorFromDB(e); err == nil && res.Id != "" { + fmt.Println("actor is in the database") + } else if err != nil { + return err + } else { + fmt.Println("actor is NOT in the database") + } + } + } else if activityType == "Follow" { + // TODO: okay? + return errors.New("not implemented") + } else if activityType == "Delete" { + return errors.New("not implemented") + } + + return nil +} + +func RejectActivity(activity Activity) Activity { + var accept Activity + accept.AtContext.Context = activity.AtContext.Context + accept.Type = "Reject" + var nObj ObjectBase + accept.Object = &nObj + var nActor Actor + accept.Actor = &nActor + accept.Actor.Id = activity.Object.Actor + accept.Object.Actor = activity.Actor.Id + var nNested NestedObjectBase + accept.Object.Object = &nNested + accept.Object.Object.Actor = activity.Object.Actor + accept.Object.Object.Type = "Follow" + accept.To = append(accept.To, activity.Actor.Id) + + return accept +} + +func ReportActivity(id string, reason string) (bool, error) { + if res, err := IsIDLocal(id); err == nil && !res { + // TODO: not local error + return false, nil + } else if err != nil { + return false, err + } + + actor, err := GetActivityFromDB(id) + if err != nil { + return false, err + } + + query := `select count from reported where id=$1` + + rows, err := config.DB.Query(query, id) + if err != nil { + return false, err + } + defer rows.Close() + + var count int + for rows.Next() { + if err := rows.Scan(&count); err != nil { + return false, err + } + } + + if count < 1 { + query = `insert into reported (id, count, board, reason) values ($1, $2, $3, $4)` + + _, err := config.DB.Exec(query, id, 1, actor.Actor.Id, reason) + if err != nil { + return false, err + } + } else { + count = count + 1 + query = `update reported set count=$1 where id=$2` + + _, err := config.DB.Exec(query, count, id) + if err != nil { + return false, err + } + } + + return true, nil +} + +func WriteActivitytoCache(obj ObjectBase) error { + obj.Name = util.EscapeString(obj.Name) + obj.Content = util.EscapeString(obj.Content) + obj.AttributedTo = util.EscapeString(obj.AttributedTo) + + query := `select id from cacheactivitystream where id=$1` + + rows, err := config.DB.Query(query, obj.Id) + if err != nil { + return err + } + defer rows.Close() + + var id string + rows.Next() + err = rows.Scan(&id) + if err != nil { + return err + } else if id != "" { + return nil // TODO: error? + } + + if obj.Updated.IsZero() { + obj.Updated = obj.Published + } + + query = `insert into cacheactivitystream (id, type, name, content, published, updated, attributedto, actor, tripcode, sensitive) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` + + _, err = config.DB.Exec(query, obj.Id, obj.Type, obj.Name, obj.Content, obj.Published, obj.Updated, obj.AttributedTo, obj.Actor, obj.TripCode, obj.Sensitive) + return err +} + +func WriteActivitytoCacheWithAttachment(obj ObjectBase, attachment ObjectBase, preview NestedObjectBase) error { + obj.Name = util.EscapeString(obj.Name) + obj.Content = util.EscapeString(obj.Content) + obj.AttributedTo = util.EscapeString(obj.AttributedTo) + + query := `select id from cacheactivitystream where id=$1` + + rows, err := config.DB.Query(query, obj.Id) + if err != nil { + return err + } + defer rows.Close() + + var id string + rows.Next() + err = rows.Scan(&id) + if err != nil { + return err + } else if id != "" { + return nil // TODO: error? + } + + if obj.Updated.IsZero() { + obj.Updated = obj.Published + } + + query = `insert into cacheactivitystream (id, type, name, content, attachment, preview, published, updated, attributedto, actor, tripcode, sensitive) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)` + + _, err = config.DB.Exec(query, obj.Id, obj.Type, obj.Name, obj.Content, attachment.Id, preview.Id, obj.Published, obj.Updated, obj.AttributedTo, obj.Actor, obj.TripCode, obj.Sensitive) + return err +} + +func WriteActivitytoDB(obj ObjectBase) error { + obj.Name = util.EscapeString(obj.Name) + obj.Content = util.EscapeString(obj.Content) + obj.AttributedTo = util.EscapeString(obj.AttributedTo) + + query := `insert into activitystream (id, type, name, content, published, updated, attributedto, actor, tripcode, sensitive) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` + + _, err := config.DB.Exec(query, obj.Id, obj.Type, obj.Name, obj.Content, obj.Published, obj.Updated, obj.AttributedTo, obj.Actor, obj.TripCode, obj.Sensitive) + return err +} + +func WriteActivitytoDBWithAttachment(obj ObjectBase, attachment ObjectBase, preview NestedObjectBase) { + + obj.Name = util.EscapeString(obj.Name) + obj.Content = util.EscapeString(obj.Content) + obj.AttributedTo = util.EscapeString(obj.AttributedTo) + + query := `insert into activitystream (id, type, name, content, attachment, preview, published, updated, attributedto, actor, tripcode, sensitive) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)` + + _, e := config.DB.Exec(query, obj.Id, obj.Type, obj.Name, obj.Content, attachment.Id, preview.Id, obj.Published, obj.Updated, obj.AttributedTo, obj.Actor, obj.TripCode, obj.Sensitive) + + if e != nil { + fmt.Println("error inserting new activity with attachment") + panic(e) + } +} + +func WriteAttachmentToCache(obj ObjectBase) error { + query := `select id from cacheactivitystream where id=$1` + + rows, err := config.DB.Query(query, obj.Id) + if err != nil { + return err + } + defer rows.Close() + + var id string + rows.Next() + err = rows.Scan(&id) + if err != nil { + return err + } else if id != "" { + return nil // TODO: error? + } + + if obj.Updated.IsZero() { + obj.Updated = obj.Published + } + + query = `insert into cacheactivitystream (id, type, name, href, published, updated, attributedTo, mediatype, size) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)` + + _, err = config.DB.Exec(query, obj.Id, obj.Type, obj.Name, obj.Href, obj.Published, obj.Updated, obj.AttributedTo, obj.MediaType, obj.Size) + return err +} + +func WriteAttachmentToDB(obj ObjectBase) { + query := `insert into activitystream (id, type, name, href, published, updated, attributedTo, mediatype, size) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)` + + _, e := config.DB.Exec(query, obj.Id, obj.Type, obj.Name, obj.Href, obj.Published, obj.Updated, obj.AttributedTo, obj.MediaType, obj.Size) + + if e != nil { + fmt.Println("error inserting new attachment") + panic(e) + } +} + +func WritePreviewToCache(obj NestedObjectBase) error { + query := `select id from cacheactivitystream where id=$1` + + rows, err := config.DB.Query(query, obj.Id) + if err != nil { + return err + } + defer rows.Close() + + var id string + rows.Next() + err = rows.Scan(&id) + if err != nil { + return err + } else if id != "" { + return nil // TODO: error? + } + + if obj.Updated.IsZero() { + obj.Updated = obj.Published + } + + query = `insert into cacheactivitystream (id, type, name, href, published, updated, attributedTo, mediatype, size) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)` + + _, err = config.DB.Exec(query, obj.Id, obj.Type, obj.Name, obj.Href, obj.Published, obj.Updated, obj.AttributedTo, obj.MediaType, obj.Size) + return err +} + +func WritePreviewToDB(obj NestedObjectBase) error { + query := `insert into activitystream (id, type, name, href, published, updated, attributedTo, mediatype, size) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)` + + _, err := config.DB.Exec(query, obj.Id, obj.Type, obj.Name, obj.Href, obj.Published, obj.Updated, obj.AttributedTo, obj.MediaType, obj.Size) + return err +} diff --git a/activitypub/actor.go b/activitypub/actor.go new file mode 100644 index 0000000..d9399ab --- /dev/null +++ b/activitypub/actor.go @@ -0,0 +1,853 @@ +package activitypub + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "regexp" + "strings" + + "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/post" + "github.com/FChannel0/FChannel-Server/util" + "github.com/gofiber/fiber/v2" +) + +func CreateNewActor(board string, prefName string, summary string, authReq []string, restricted bool) *Actor { + actor := new(Actor) + + var path string + if board == "" { + path = config.Domain + actor.Name = "main" + } else { + path = config.Domain + "/" + board + actor.Name = board + } + + actor.Type = "Group" + actor.Id = fmt.Sprintf("%s", path) + actor.Following = fmt.Sprintf("%s/following", actor.Id) + actor.Followers = fmt.Sprintf("%s/followers", actor.Id) + actor.Inbox = fmt.Sprintf("%s/inbox", actor.Id) + actor.Outbox = fmt.Sprintf("%s/outbox", actor.Id) + actor.PreferredUsername = prefName + actor.Restricted = restricted + actor.Summary = summary + actor.AuthRequirement = authReq + + return actor +} + +func DeleteActorCache(actorID string) error { + query := `select id from cacheactivitystream where id in (select id from cacheactivitystream where actor=$1)` + + rows, err := config.DB.Query(query, actorID) + + if err != nil { + return err + } + + defer rows.Close() + + for rows.Next() { + var id string + if err := rows.Scan(&id); err != nil { + return err + } + + if err := DeleteObject(id); err != nil { + return err + } + } + + return nil +} + +func GetActorAuth(actor string) ([]string, error) { + var auth []string + + query := `select type from actorauth where board=$1` + + rows, err := config.DB.Query(query, actor) + if err != nil { + return auth, err + } + defer rows.Close() + + for rows.Next() { + var e string + if err := rows.Scan(&e); err != nil { + return auth, err + } + + auth = append(auth, e) + } + + return auth, nil +} + +func GetActorAutoSubscribeDB(id string) (bool, error) { + query := `select autosubscribe from actor where id=$1` + + rows, err := config.DB.Query(query, id) + if err != nil { + return false, err + } + + var subscribed bool + defer rows.Close() + rows.Next() + err = rows.Scan(&subscribed) + return subscribed, err +} + +func GetActorByNameFromDB(name string) (Actor, error) { + var nActor Actor + + query := `select type, id, name, preferedusername, inbox, outbox, following, followers, restricted, summary, publickeypem from actor where name=$1` + + rows, err := config.DB.Query(query, name) + if err != nil { + return nActor, err + } + + var publicKeyPem string + defer rows.Close() + for rows.Next() { + if err := rows.Scan(&nActor.Type, &nActor.Id, &nActor.Name, &nActor.PreferredUsername, &nActor.Inbox, &nActor.Outbox, &nActor.Following, &nActor.Followers, &nActor.Restricted, &nActor.Summary, &publicKeyPem); err != nil { + return nActor, err + } + } + + if nActor.Id != "" && nActor.PublicKey.PublicKeyPem == "" { + if err := CreatePublicKeyFromPrivate(&nActor, publicKeyPem); err != nil { + return nActor, err + } + } + + return nActor, nil +} + +func GetActorCollectionDBType(actorId string, nType string) (Collection, error) { + var nColl Collection + var result []ObjectBase + + query := `select x.id, x.name, x.content, x.type, x.published, x.updated, x.attributedto, x.attachment, x.preview, x.actor, x.tripcode, x.sensitive from (select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where actor=$1 and id in (select id from replies where inreplyto='') and type=$2 union select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where actor in (select following from following where id=$1) and id in (select id from replies where inreplyto='') and type=$2 union select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from cacheactivitystream where actor in (select following from following where id=$1) and id in (select id from replies where inreplyto='') and type=$2) as x order by x.updated desc` + + rows, err := config.DB.Query(query, actorId, nType) + if err != nil { + return nColl, err + } + + defer rows.Close() + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + var previewID string + + if err := rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.Updated, &post.AttributedTo, &attachID, &previewID, &actor.Id, &post.TripCode, &post.Sensitive); err != nil { + return nColl, err + } + + post.Actor = actor.Id + + var replies CollectionBase + + post.Replies = &replies + + var err error + post.Replies.TotalItems, post.Replies.TotalImgs, err = GetObjectRepliesCount(post) + if err != nil { + return nColl, err + } + + post.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return nColl, err + } + + post.Preview, err = GetObjectPreview(previewID) + if err != nil { + return nColl, err + } + + result = append(result, post) + } + + nColl.OrderedItems = result + + return nColl, nil +} + +func GetActorCollectionDBTypeLimit(actorId string, nType string, limit int) (Collection, error) { + var nColl Collection + var result []ObjectBase + + query := `select x.id, x.name, x.content, x.type, x.published, x.updated, x.attributedto, x.attachment, x.preview, x.actor, x.tripcode, x.sensitive from (select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where actor=$1 and id in (select id from replies where inreplyto='') and type=$2 union select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where actor in (select following from following where id=$1) and id in (select id from replies where inreplyto='') and type=$2 union select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from cacheactivitystream where actor in (select following from following where id=$1) and id in (select id from replies where inreplyto='') and type=$2) as x order by x.updated desc limit $3` + + rows, err := config.DB.Query(query, actorId, nType, limit) + if err != nil { + return nColl, err + } + + defer rows.Close() + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + var previewID string + + if err := rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.Updated, &post.AttributedTo, &attachID, &previewID, &actor.Id, &post.TripCode, &post.Sensitive); err != nil { + return nColl, err + } + + post.Actor = actor.Id + + var replies CollectionBase + + post.Replies = &replies + + var err error + post.Replies.TotalItems, post.Replies.TotalImgs, err = GetObjectRepliesCount(post) + if err != nil { + return nColl, err + } + + post.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return nColl, err + } + + post.Preview, err = GetObjectPreview(previewID) + if err != nil { + return nColl, err + } + + result = append(result, post) + } + + nColl.OrderedItems = result + + return nColl, nil +} + +func GetActorCollectionReq(collection string) (Collection, error) { + var nCollection Collection + + req, err := http.NewRequest("GET", collection, nil) + if err != nil { + return nCollection, err + } + + // TODO: rewrite this for fiber + pass := "FIXME" + //_, pass := GetPasswordFromSession(r) + + req.Header.Set("Accept", config.ActivityStreams) + + req.Header.Set("Authorization", "Basic "+pass) + + resp, err := util.RouteProxy(req) + if err != nil { + return nCollection, err + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + body, _ := ioutil.ReadAll(resp.Body) + + if err := json.Unmarshal(body, &nCollection); err != nil { + return nCollection, err + } + } + + return nCollection, nil +} + +func GetActorFollowDB(id string) ([]ObjectBase, error) { + var followerCollection []ObjectBase + + query := `select follower from follower where id=$1` + + rows, err := config.DB.Query(query, id) + if err != nil { + return followerCollection, err + } + defer rows.Close() + + for rows.Next() { + var obj ObjectBase + + if err := rows.Scan(&obj.Id); err != nil { + return followerCollection, err + } + + followerCollection = append(followerCollection, obj) + } + + return followerCollection, nil +} + +func GetActorFollowNameFromPath(path string) string { + var actor string + + re := regexp.MustCompile("f\\w+-") + + actor = re.FindString(path) + + actor = strings.Replace(actor, "f", "", 1) + actor = strings.Replace(actor, "-", "", 1) + + return actor +} + +func GetActorFollowTotal(id string) (int, int, error) { + var following int + var followers int + + query := `select count(following) from following where id=$1` + + rows, err := config.DB.Query(query, id) + if err != nil { + return 0, 0, err + } + defer rows.Close() + + for rows.Next() { + if err := rows.Scan(&following); err != nil { + return following, 0, err + } + } + + query = `select count(follower) from follower where id=$1` + + rows, err = config.DB.Query(query, id) + if err != nil { + return 0, 0, err + } + defer rows.Close() + + for rows.Next() { + if err := rows.Scan(&followers); err != nil { + return following, followers, err + } + + } + + return following, followers, nil +} + +func GetActorFollowers(w http.ResponseWriter, id string) error { + var following Collection + var err error + + following.AtContext.Context = "https://www.w3.org/ns/activitystreams" + following.Type = "Collection" + _, following.TotalItems, err = GetActorFollowTotal(id) + if err != nil { + return err + } + + following.Items, err = GetActorFollowDB(id) + if err != nil { + return err + } + + enc, _ := json.MarshalIndent(following, "", "\t") + w.Header().Set("Content-Type", config.ActivityStreams) + _, err = w.Write(enc) + return err +} + +func GetActorFollowing(w http.ResponseWriter, id string) error { + var following Collection + var err error + + following.AtContext.Context = "https://www.w3.org/ns/activitystreams" + following.Type = "Collection" + following.TotalItems, _, err = GetActorFollowTotal(id) + if err != nil { + return err + } + + following.Items, err = GetActorFollowingDB(id) + if err != nil { + return err + } + + enc, _ := json.MarshalIndent(following, "", "\t") + w.Header().Set("Content-Type", config.ActivityStreams) + _, err = w.Write(enc) + + return err +} + +func GetActorFollowingDB(id string) ([]ObjectBase, error) { + var followingCollection []ObjectBase + query := `select following from following where id=$1` + + rows, err := config.DB.Query(query, id) + if err != nil { + return followingCollection, err + } + defer rows.Close() + + for rows.Next() { + var obj ObjectBase + + if err := rows.Scan(&obj.Id); err != nil { + return followingCollection, err + } + + followingCollection = append(followingCollection, obj) + } + + return followingCollection, nil +} + +func GetActorFromDB(id string) (Actor, error) { + var nActor Actor + + query := `select type, id, name, preferedusername, inbox, outbox, following, followers, restricted, summary, publickeypem from actor where id=$1` + + rows, err := config.DB.Query(query, id) + if err != nil { + return nActor, err + } + + var publicKeyPem string + defer rows.Close() + for rows.Next() { + if err := rows.Scan(&nActor.Type, &nActor.Id, &nActor.Name, &nActor.PreferredUsername, &nActor.Inbox, &nActor.Outbox, &nActor.Following, &nActor.Followers, &nActor.Restricted, &nActor.Summary, &publicKeyPem); err != nil { + return nActor, err + } + } + + nActor.PublicKey, err = GetActorPemFromDB(publicKeyPem) + if err != nil { + return nActor, err + } + + if nActor.Id != "" && nActor.PublicKey.PublicKeyPem == "" { + if err := CreatePublicKeyFromPrivate(&nActor, publicKeyPem); err != nil { + return nActor, err + } + } + + return nActor, nil +} + +func GetActorFromJson(actor []byte) (Actor, error) { + var generic interface{} + var nActor Actor + err := json.Unmarshal(actor, &generic) + if err != nil { + return nActor, err + } + + if generic != nil { + switch generic.(type) { + case map[string]interface{}: + err = json.Unmarshal(actor, &nActor) + break + + case string: + var str string + err = json.Unmarshal(actor, &str) + nActor.Id = str + break + } + + return nActor, err + } + + return nActor, nil +} + +func GetActorInfo(ctx *fiber.Ctx, id string) error { + actor, err := GetActorFromDB(id) + if err != nil { + return err + } + + enc, _ := json.MarshalIndent(actor, "", "\t") + ctx.Response().Header.Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + + _, err = ctx.Write(enc) + + return err +} + +func GetActorInstance(path string) (string, string) { + re := regexp.MustCompile(`([@]?([\w\d.-_]+)[@](.+))`) + atFormat := re.MatchString(path) + + if atFormat { + match := re.FindStringSubmatch(path) + if len(match) > 2 { + return match[2], match[3] + } + } + + re = regexp.MustCompile(`(https?://)?(www)?([\w\d-_.:]+)(/|\s+|\r|\r\n)?$`) + mainActor := re.MatchString(path) + if mainActor { + match := re.FindStringSubmatch(path) + if len(match) > 2 { + return "main", match[3] + } + } + + re = regexp.MustCompile(`(https?://)?(www)?([\w\d-_.:]+)\/([\w\d-_.]+)(\/([\w\d-_.]+))?`) + httpFormat := re.MatchString(path) + + if httpFormat { + match := re.FindStringSubmatch(path) + if len(match) > 3 { + if match[4] == "users" { + return match[6], match[3] + } + + return match[4], match[3] + } + } + + return "", "" +} + +func GetActorObjectCollectionFromDB(actorId string) (Collection, error) { + var nColl Collection + var result []ObjectBase + + query := `select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where actor=$1 and id in (select id from replies where inreplyto='') and type='Note' order by updated desc` + + rows, err := config.DB.Query(query, actorId) + if err != nil { + return nColl, err + } + + defer rows.Close() + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + var previewID string + + if err := rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.Updated, &post.AttributedTo, &attachID, &previewID, &actor.Id, &post.TripCode, &post.Sensitive); err != nil { + return nColl, err + } + + post.Actor = actor.Id + + var postCnt int + var imgCnt int + post.Replies, postCnt, imgCnt, err = GetObjectRepliesDB(post) + if err != nil { + return nColl, err + } + + post.Replies.TotalItems = postCnt + post.Replies.TotalImgs = imgCnt + + post.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return nColl, err + } + + post.Preview, err = GetObjectPreview(previewID) + if err != nil { + return nColl, err + } + + result = append(result, post) + } + + nColl.OrderedItems = result + + return nColl, nil +} + +func GetActorReportedDB(id string) ([]ObjectBase, error) { + var nObj []ObjectBase + + query := `select id, count, reason from reported where board=$1` + + rows, err := config.DB.Query(query, id) + if err != nil { + return nObj, err + } + + defer rows.Close() + + for rows.Next() { + var obj ObjectBase + + rows.Scan(&obj.Id, &obj.Size, &obj.Content) + + nObj = append(nObj, obj) + } + + return nObj, nil +} + +func GetActorReportedTotal(id string) (int, error) { + query := `select count(id) from reported where board=$1` + + rows, err := config.DB.Query(query, id) + if err != nil { + return 0, err + } + + defer rows.Close() + + var count int + for rows.Next() { + rows.Scan(&count) + } + + return count, nil +} + +func GetActorsFollowPostFromId(actors []string, id string) (Collection, error) { + var collection Collection + + for _, e := range actors { + tempCol, err := GetObjectByIDFromDB(e + "/" + id) + if err != nil { + return collection, err + } + + if len(tempCol.OrderedItems) > 0 { + collection = tempCol + return collection, nil + } + } + + return collection, nil +} + +func GetAllActorArchiveDB(id string, offset int) (Collection, error) { + var nColl Collection + var result []ObjectBase + + query := `select x.id, x.updated from (select id, updated from activitystream where actor=$1 and id in (select id from replies where inreplyto='') and type='Note' union select id, updated from activitystream where actor in (select following from following where id=$1) and id in (select id from replies where inreplyto='') and type='Note' union select id, updated from cacheactivitystream where actor in (select following from following where id=$1) and id in (select id from replies where inreplyto='') and type='Note') as x order by x.updated desc offset $2` + + rows, err := config.DB.Query(query, id, offset) + if err != nil { + return nColl, err + } + defer rows.Close() + + for rows.Next() { + var post ObjectBase + + if err := rows.Scan(&post.Id, &post.Updated); err != nil { + return nColl, err + } + + post.Replies, _, _, err = GetObjectRepliesDB(post) + + result = append(result, post) + } + + nColl.OrderedItems = result + + return nColl, nil +} + +func GetBoards() ([]Actor, error) { + var boards []Actor + + query := `select type, id, name, preferedusername, inbox, outbox, following, followers FROM actor` + + rows, err := config.DB.Query(query) + if err != nil { + return boards, err + } + + defer rows.Close() + for rows.Next() { + var actor = new(Actor) + + if err := rows.Scan(&actor.Type, &actor.Id, &actor.Name, &actor.PreferredUsername, &actor.Inbox, &actor.Outbox, &actor.Following, &actor.Followers); err != nil { + return boards, err + } + + boards = append(boards, *actor) + } + + return boards, nil +} + +func IsActorLocal(id string) (bool, error) { + actor, err := GetActorFromDB(id) + return actor.Id != "", err +} + +func IsAlreadyFollowing(actor string, follow string) (bool, error) { + followers, err := GetActorFollowingDB(actor) + if err != nil { + return false, err + } + + for _, e := range followers { + if e.Id == follow { + return true, nil + } + } + + return false, nil +} + +func IsAlreadyFollower(actor string, follow string) (bool, error) { + followers, err := GetActorFollowDB(actor) + if err != nil { + return false, err + } + + for _, e := range followers { + if e.Id == follow { + return true, nil + } + } + + return false, nil +} + +func SetActorAutoSubscribeDB(id string) error { + current, err := GetActorAutoSubscribeDB(id) + if err != nil { + return err + } + + query := `update actor set autosubscribe=$1 where id=$2` + + _, err = config.DB.Exec(query, !current, id) + return err +} + +func SetActorFollowerDB(activity Activity) (Activity, error) { + var query string + alreadyFollow, err := IsAlreadyFollower(activity.Actor.Id, activity.Object.Actor) + if err != nil { + return activity, err + } + + activity.Type = "Reject" + if activity.Actor.Id == activity.Object.Actor { + return activity, nil + } + + if alreadyFollow { + query = `delete from follower where id=$1 and follower=$2` + activity.Summary = activity.Object.Actor + " Unfollow " + activity.Actor.Id + + if _, err := config.DB.Exec(query, activity.Actor.Id, activity.Object.Actor); err != nil { + return activity, err + } + + activity.Type = "Accept" + return activity, err + } + + query = `insert into follower (id, follower) values ($1, $2)` + activity.Summary = activity.Object.Actor + " Follow " + activity.Actor.Id + + if _, err := config.DB.Exec(query, activity.Actor.Id, activity.Object.Actor); err != nil { + return activity, err + } + + activity.Type = "Accept" + return activity, nil +} + +func WriteActorObjectReplyToDB(obj ObjectBase) error { + for _, e := range obj.InReplyTo { + query := `select id from replies where id=$1 and inreplyto=$2` + + rows, err := config.DB.Query(query, obj.Id, e.Id) + if err != nil { + return err + } + + defer rows.Close() + + var id string + rows.Next() + rows.Scan(&id) + + if id == "" { + query := `insert into replies (id, inreplyto) values ($1, $2)` + + if _, err := config.DB.Exec(query, obj.Id, e.Id); err != nil { + return err + } + } + } + + if len(obj.InReplyTo) < 1 { + query := `select id from replies where id=$1 and inreplyto=$2` + + rows, err := config.DB.Query(query, obj.Id, "") + if err != nil { + return err + } + defer rows.Close() + + var id string + rows.Next() + rows.Scan(&id) + + if id == "" { + query := `insert into replies (id, inreplyto) values ($1, $2)` + + if _, err := config.DB.Exec(query, obj.Id, ""); err != nil { + return err + } + } + } + + return nil +} + +func WriteActorObjectToCache(obj ObjectBase) (ObjectBase, error) { + if res, err := post.IsPostBlacklist(obj.Content); err == nil && res { + fmt.Println("\n\nBlacklist post blocked\n\n") + return obj, nil + } else if err != nil { + return obj, err + } + + if len(obj.Attachment) > 0 { + if res, err := IsIDLocal(obj.Id); err == nil && res { + return obj, err + } else if err != nil { + return obj, err + } + + if obj.Preview.Href != "" { + WritePreviewToCache(*obj.Preview) + } + + for i, _ := range obj.Attachment { + WriteAttachmentToCache(obj.Attachment[i]) + WriteActivitytoCacheWithAttachment(obj, obj.Attachment[i], *obj.Preview) + } + + } else { + WriteActivitytoCache(obj) + } + + WriteActorObjectReplyToDB(obj) + + if obj.Replies != nil { + for _, e := range obj.Replies.OrderedItems { + WriteActorObjectToCache(e) + } + } + + return obj, nil +} diff --git a/activitypub/object.go b/activitypub/object.go new file mode 100644 index 0000000..692fa5b --- /dev/null +++ b/activitypub/object.go @@ -0,0 +1,1802 @@ +package activitypub + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "mime/multipart" + "net/http" + "os" + "os/exec" + "regexp" + "sort" + "strings" + "time" + + "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/post" + "github.com/FChannel0/FChannel-Server/util" +) + +func CheckIfObjectOP(id string) (bool, error) { + var count int + + query := `select count(id) from replies where inreplyto='' and id=$1 ` + + rows, err := config.DB.Query(query, id) + if err != nil { + return false, err + } + defer rows.Close() + + rows.Next() + if err := rows.Scan(&count); err != nil { + return false, err + } + + if count > 0 { + return true, nil + } + + return false, nil +} + +func CreateAttachmentObject(file multipart.File, header *multipart.FileHeader) ([]ObjectBase, *os.File, error) { + contentType, err := post.GetFileContentType(file) + if err != nil { + return nil, nil, err + } + + filename := header.Filename + size := header.Size + + re := regexp.MustCompile(`.+/`) + + fileType := re.ReplaceAllString(contentType, "") + + tempFile, err := ioutil.TempFile("./public", "*."+fileType) + if err != nil { + return nil, nil, err + } + + var nAttachment []ObjectBase + var image ObjectBase + + image.Type = "Attachment" + image.Name = filename + image.Href = config.Domain + "/" + tempFile.Name() + image.MediaType = contentType + image.Size = size + image.Published = time.Now().UTC() + + nAttachment = append(nAttachment, image) + + return nAttachment, tempFile, nil +} + +func CreateObject(objType string) ObjectBase { + var nObj ObjectBase + + nObj.Type = objType + nObj.Published = time.Now().UTC() + nObj.Updated = time.Now().UTC() + + return nObj +} + +func CreatePreviewObject(obj ObjectBase) *NestedObjectBase { + re := regexp.MustCompile(`/.+$`) + + mimetype := re.ReplaceAllString(obj.MediaType, "") + + var nPreview NestedObjectBase + + if mimetype != "image" { + return &nPreview + } + + re = regexp.MustCompile(`.+/`) + + file := re.ReplaceAllString(obj.MediaType, "") + + href := util.GetUniqueFilename(file) + + nPreview.Type = "Preview" + nPreview.Name = obj.Name + nPreview.Href = config.Domain + "" + href + nPreview.MediaType = obj.MediaType + nPreview.Size = obj.Size + nPreview.Published = obj.Published + + re = regexp.MustCompile(`/public/.+`) + + objFile := re.FindString(obj.Href) + + cmd := exec.Command("convert", "."+objFile, "-resize", "250x250>", "-strip", "."+href) + + if err := cmd.Run(); err != nil { + // TODO: previously we would call CheckError here + var preview NestedObjectBase + return &preview + } + + return &nPreview +} + +func DeleteAttachmentFromDB(id string) error { + query := `delete from activitystream where id in (select attachment from activitystream where id=$1)` + + if _, err := config.DB.Exec(query, id); err != nil { + return err + } + + query = `delete from cacheactivitystream where id in (select attachment from cacheactivitystream where id=$1)` + + _, err := config.DB.Exec(query, id) + return err +} + +func DeleteAttachmentFromFile(id string) error { + query := `select href from activitystream where id in (select attachment from activitystream where id=$1)` + + rows, err := config.DB.Query(query, id) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var href string + + if err := rows.Scan(&href); err != nil { + return err + } + + href = strings.Replace(href, config.Domain+"/", "", 1) + + if href != "static/notfound.png" { + _, err = os.Stat(href) + if err == nil { + os.Remove(href) + } + return err + } + } + + return nil +} + +func DeletePreviewFromDB(id string) error { + query := `delete from activitystream where id=$1` + + if _, err := config.DB.Exec(query, id); err != nil { + return err + } + + query = `delete from cacheactivitystream where id in (select preview from cacheactivitystream where id=$1)` + + _, err := config.DB.Exec(query, id) + return err +} + +func DeletePreviewFromFile(id string) error { + query := `select href from activitystream where id in (select preview from activitystream where id=$1)` + + rows, err := config.DB.Query(query, id) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var href string + + if err := rows.Scan(&href); err != nil { + return err + } + + href = strings.Replace(href, config.Domain+"/", "", 1) + + if href != "static/notfound.png" { + _, err = os.Stat(href) + if err == nil { + return os.Remove(href) + } + return err + } + } + + return nil +} + +func DeleteObject(id string) error { + if err := DeleteReportActivity(id); err != nil { + return err + } + + if err := DeleteAttachmentFromFile(id); err != nil { + return err + } + + if err := DeleteAttachmentFromDB(id); err != nil { + return err + } + + if err := DeletePreviewFromFile(id); err != nil { + return err + } + + if err := DeletePreviewFromDB(id); err != nil { + return err + } + + if err := DeleteObjectFromDB(id); err != nil { + return err + } + + return DeleteObjectRepliedTo(id) +} + +func DeleteObjectFromDB(id string) error { + var query = `delete from activitystream where id=$1` + + if _, err := config.DB.Exec(query, id); err != nil { + return err + } + + query = `delete from cacheactivitystream where id=$1` + + _, err := config.DB.Exec(query, id) + return err +} + +func DeleteObjectRepliedTo(id string) error { + query := `delete from replies where id=$1` + _, err := config.DB.Exec(query, id) + return err +} + +func DeleteObjectsInReplyTo(id string) error { + query := `delete from replies where id in (select id from replies where inreplyto=$1)` + _, err := config.DB.Exec(query, id) + return err +} + +func GetCollectionFromID(id string) (Collection, error) { + var nColl Collection + + req, err := http.NewRequest("GET", id, nil) + if err != nil { + return nColl, err + } + + req.Header.Set("Accept", config.ActivityStreams) + + resp, err := util.RouteProxy(req) + if err != nil { + return nColl, err + } + + if resp.StatusCode == 200 { + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + if len(body) > 0 { + if err := json.Unmarshal(body, &nColl); err != nil { + return nColl, err + } + } + } + + return nColl, nil +} + +func GetCollectionFromPath(path string) (Collection, error) { + var nColl Collection + var result []ObjectBase + + query := `select id, name, content, type, published, attributedto, attachment, preview, actor from activitystream where id=$1 order by published desc` + + rows, err := config.DB.Query(query, path) + if err != nil { + return nColl, err + } + defer rows.Close() + + for rows.Next() { + var actor Actor + var post ObjectBase + var attachID string + var previewID string + + if err := rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.AttributedTo, &attachID, &previewID, &actor.Id); err != nil { + return nColl, err + } + + post.Actor = actor.Id + + post.InReplyTo, err = GetInReplyToDB(post) + if err != nil { + return nColl, err + } + + var postCnt int + var imgCnt int + post.Replies, postCnt, imgCnt, err = GetObjectRepliesDB(post) + if err != nil { + return nColl, err + } + + post.Replies.TotalItems, post.Replies.TotalImgs, err = GetObjectRepliesCount(post) + if err != nil { + return nColl, err + } + + post.Replies.TotalItems = post.Replies.TotalItems + postCnt + post.Replies.TotalImgs = post.Replies.TotalImgs + imgCnt + + post.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return nColl, err + } + + post.Preview, err = GetObjectPreview(previewID) + if err != nil { + return nColl, err + } + + result = append(result, post) + } + + nColl.AtContext.Context = "https://www.w3.org/ns/activitystreams" + + nColl.OrderedItems = result + + return nColl, nil +} + +func GetInReplyToDB(parent ObjectBase) ([]ObjectBase, error) { + var result []ObjectBase + + query := `select inreplyto from replies where id =$1` + + rows, err := config.DB.Query(query, parent.Id) + if err != nil { + return result, err + } + + defer rows.Close() + for rows.Next() { + var post ObjectBase + + if err := rows.Scan(&post.Id); err != nil { + return result, err + } + + result = append(result, post) + } + + return result, nil +} + +func GetObjectAttachment(id string) ([]ObjectBase, error) { + var attachments []ObjectBase + + query := `select x.id, x.type, x.name, x.href, x.mediatype, x.size, x.published from (select id, type, name, href, mediatype, size, published from activitystream where id=$1 union select id, type, name, href, mediatype, size, published from cacheactivitystream where id=$1) as x` + + rows, err := config.DB.Query(query, id) + if err != nil { + return attachments, err + } + + defer rows.Close() + for rows.Next() { + var attachment = new(ObjectBase) + + if err := rows.Scan(&attachment.Id, &attachment.Type, &attachment.Name, &attachment.Href, &attachment.MediaType, &attachment.Size, &attachment.Published); err != nil { + return attachments, err + } + + attachments = append(attachments, *attachment) + } + + return attachments, nil +} + +func GetObjectByIDFromDB(postID string) (Collection, error) { + var nColl Collection + var result []ObjectBase + + query := `select x.id, x.name, x.content, x.type, x.published, x.updated, x.attributedto, x.attachment, x.preview, x.actor, x.tripcode, x.sensitive from (select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where id=$1 and (type='Note' or type='Archive') union select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from cacheactivitystream where id=$1 and (type='Note' or type='Archive')) as x` + + rows, err := config.DB.Query(query, postID) + if err != nil { + return nColl, err + } + defer rows.Close() + + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + var previewID string + + if err := rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.Updated, &post.AttributedTo, &attachID, &previewID, &actor.Id, &post.TripCode, &post.Sensitive); err != nil { + return nColl, err + } + + actor, err = GetActorFromDB(actor.Id) + if err != nil { + return nColl, err + } + + post.Actor = actor.Id + + nColl.Actor = &actor + + var postCnt int + var imgCnt int + post.Replies, postCnt, imgCnt, err = GetObjectRepliesDB(post) + if err != nil { + return nColl, err + } + + post.Replies.TotalItems = postCnt + post.Replies.TotalImgs = imgCnt + + post.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return nColl, err + } + + post.Preview, err = GetObjectPreview(previewID) + if err != nil { + return nColl, err + } + + result = append(result, post) + } + + nColl.OrderedItems = result + + return nColl, nil +} + +func GetObjectFromDB(id string) (Collection, error) { + var nColl Collection + var result []ObjectBase + + query := `select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where id=$1 union select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from cacheactivitystream where id=$1 order by updated desc` + + rows, err := config.DB.Query(query, id) + if err != nil { + return nColl, err + } + + defer rows.Close() + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + var previewID string + + if err := rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.Updated, &post.AttributedTo, &attachID, &previewID, &actor.Id, &post.TripCode, &post.Sensitive); err != nil { + return nColl, err + } + + post.Actor = actor.Id + + var postCnt int + var imgCnt int + post.Replies, postCnt, imgCnt, err = GetObjectRepliesDB(post) + if err != nil { + return nColl, err + } + + post.Replies.TotalItems = postCnt + post.Replies.TotalImgs = imgCnt + + post.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return nColl, err + } + + post.Preview, err = GetObjectPreview(previewID) + if err != nil { + return nColl, err + } + + result = append(result, post) + } + + nColl.OrderedItems = result + + return nColl, nil +} + +func GetObjectFromDBCatalog(id string) (Collection, error) { + var nColl Collection + var result []ObjectBase + + query := `select x.id, x.name, x.content, x.type, x.published, x.updated, x.attributedto, x.attachment, x.preview, x.actor, x.tripcode, x.sensitive from (select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where actor=$1 and id in (select id from replies where inreplyto='') and type='Note' union select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where actor in (select following from following where id=$1) and id in (select id from replies where inreplyto='') and type='Note' union select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from cacheactivitystream where actor in (select following from following where id=$1) and id in (select id from replies where inreplyto='') and type='Note') as x order by x.updated desc limit 165` + + rows, err := config.DB.Query(query, id) + + if err != nil { + return nColl, err + } + + defer rows.Close() + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + var previewID string + + if err := rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.Updated, &post.AttributedTo, &attachID, &previewID, &actor.Id, &post.TripCode, &post.Sensitive); err != nil { + return nColl, err + } + + post.Actor = actor.Id + + var replies CollectionBase + + post.Replies = &replies + + post.Replies.TotalItems, post.Replies.TotalImgs, err = GetObjectRepliesCount(post) + + if err != nil { + return nColl, err + } + + post.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return nColl, err + } + + post.Preview, err = GetObjectPreview(previewID) + if err != nil { + return nColl, err + } + + result = append(result, post) + } + + nColl.OrderedItems = result + + return nColl, nil +} + +func GetObjectFromDBFromID(id string) (Collection, error) { + var nColl Collection + var result []ObjectBase + + query := `select x.id, x.name, x.content, x.type, x.published, x.updated, x.attributedto, x.attachment, x.preview, x.actor, x.tripcode, x.sensitive from (select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where id like $1 and type='Note' union select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from cacheactivitystream where id like $1 and type='Note') as x order by x.updated` + + re := regexp.MustCompile(`f(\w+)\-`) + match := re.FindStringSubmatch(id) + + if len(match) > 0 { + re := regexp.MustCompile(`(.+)\-`) + id = re.ReplaceAllString(id, "") + id = "%" + match[1] + "/" + id + } + + rows, err := config.DB.Query(query, id) + if err != nil { + return nColl, err + } + + defer rows.Close() + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + var previewID string + + if err := rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.Updated, &post.AttributedTo, &attachID, &previewID, &actor.Id, &post.TripCode, &post.Sensitive); err != nil { + return nColl, err + } + + post.Actor = actor.Id + + var postCnt int + var imgCnt int + post.Replies, postCnt, imgCnt, err = GetObjectRepliesDB(post) + if err != nil { + return nColl, err + } + + post.Replies.TotalItems = postCnt + post.Replies.TotalImgs = imgCnt + + post.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return nColl, err + } + + post.Preview, err = GetObjectPreview(previewID) + if err != nil { + return nColl, err + } + + result = append(result, post) + } + + nColl.OrderedItems = result + + return nColl, nil +} + +func GetObjectFromDBPage(id string, page int) (Collection, error) { + var nColl Collection + var result []ObjectBase + + query := `select count (x.id) over(), x.id, x.name, x.content, x.type, x.published, x.updated, x.attributedto, x.attachment, x.preview, x.actor, x.tripcode, x.sensitive from (select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where actor=$1 and id in (select id from replies where inreplyto='') and type='Note' union select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where actor in (select following from following where id=$1) and id in (select id from replies where inreplyto='') and type='Note' union select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from cacheactivitystream where actor in (select following from following where id=$1) and id in (select id from replies where inreplyto='') and type='Note') as x order by x.updated desc limit 15 offset $2` + + rows, err := config.DB.Query(query, id, page*15) + if err != nil { + return nColl, err + } + + var count int + defer rows.Close() + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + var previewID string + + if err := rows.Scan(&count, &post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.Updated, &post.AttributedTo, &attachID, &previewID, &actor.Id, &post.TripCode, &post.Sensitive); err != nil { + return nColl, err + } + + post.Actor = actor.Id + + var postCnt int + var imgCnt int + var err error + post.Replies, postCnt, imgCnt, err = GetObjectRepliesDBLimit(post, 5) + if err != nil { + return nColl, err + } + + post.Replies.TotalItems = postCnt + post.Replies.TotalImgs = imgCnt + + post.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return nColl, err + } + + post.Preview, err = GetObjectPreview(previewID) + if err != nil { + return nColl, err + } + + result = append(result, post) + } + + nColl.TotalItems = count + nColl.OrderedItems = result + + return nColl, nil +} + +func GetObjectFromJson(obj []byte) (ObjectBase, error) { + var generic interface{} + var nObj ObjectBase + + if err := json.Unmarshal(obj, &generic); err != nil { + return ObjectBase{}, err + } + + if generic != nil { + switch generic.(type) { + case []interface{}: + var lObj ObjectBase + var arrContext ObjectArray + + if err := json.Unmarshal(obj, &arrContext.Object); err != nil { + return nObj, err + } + + if len(arrContext.Object) > 0 { + lObj = arrContext.Object[0] + } + nObj = lObj + break + + case map[string]interface{}: + var arrContext Object + + if err := json.Unmarshal(obj, &arrContext.Object); err != nil { + return nObj, err + } + + nObj = *arrContext.Object + break + + case string: + var lObj ObjectBase + var arrContext ObjectString + + if err := json.Unmarshal(obj, &arrContext.Object); err != nil { + return nObj, err + } + + lObj.Id = arrContext.Object + nObj = lObj + break + } + } + + return nObj, nil +} + +func GetObjectFromPath(path string) (ObjectBase, error) { + var nObj ObjectBase + + query := `select id, name, content, type, published, attributedto, attachment, preview, actor from activitystream where id=$1 order by published desc` + + rows, err := config.DB.Query(query, path) + if err != nil { + return nObj, err + } + + defer rows.Close() + rows.Next() + var attachID string + var previewID string + + var nActor Actor + nObj.Actor = nActor.Id + + if err := rows.Scan(&nObj.Id, &nObj.Name, &nObj.Content, &nObj.Type, &nObj.Published, &nObj.AttributedTo, &attachID, &previewID, &nObj.Actor); err != nil { + return nObj, err + } + + var postCnt int + var imgCnt int + + nObj.Replies, postCnt, imgCnt, err = GetObjectRepliesDB(nObj) + if err != nil { + return nObj, err + } + + nObj.Replies.TotalItems, nObj.Replies.TotalImgs, err = GetObjectRepliesCount(nObj) + if err != nil { + return nObj, err + } + + nObj.Replies.TotalItems = nObj.Replies.TotalItems + postCnt + nObj.Replies.TotalImgs = nObj.Replies.TotalImgs + imgCnt + + nObj.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return nObj, err + } + + nObj.Preview, err = GetObjectPreview(previewID) + return nObj, err +} + +func GetObjectImgsTotalDB(actor Actor) (int, error) { + count := 0 + query := `select count(attachment) from activitystream where actor=$1 and id in (select id from replies where inreplyto='' and type='Note' )` + + rows, err := config.DB.Query(query, actor.Id) + if err != nil { + return count, err + } + defer rows.Close() + + for rows.Next() { + if err := rows.Scan(&count); err != nil { + return count, err + } + } + + return count, nil +} + +func GetObjectPostsTotalDB(actor Actor) (int, error) { + count := 0 + query := `select count(id) from activitystream where actor=$1 and id in (select id from replies where inreplyto='' and type='Note')` + + rows, err := config.DB.Query(query, actor.Id) + if err != nil { + return count, err + } + + defer rows.Close() + for rows.Next() { + if err := rows.Scan(&count); err != nil { + return count, err + } + } + + return count, nil +} + +func GetObjectPreview(id string) (*NestedObjectBase, error) { + var preview NestedObjectBase + + query := `select x.id, x.type, x.name, x.href, x.mediatype, x.size, x.published from (select id, type, name, href, mediatype, size, published from activitystream where id=$1 union select id, type, name, href, mediatype, size, published from cacheactivitystream where id=$1) as x` + + rows, err := config.DB.Query(query, id) + if err != nil { + return nil, err + } + + defer rows.Close() + for rows.Next() { + if err := rows.Scan(&preview.Id, &preview.Type, &preview.Name, &preview.Href, &preview.MediaType, &preview.Size, &preview.Published); err != nil { + return nil, err + } + } + + return &preview, nil +} + +func GetObjectRepliesCount(parent ObjectBase) (int, int, error) { + var countId int + var countImg int + + query := `select count(x.id) over(), sum(case when RTRIM(x.attachment) = '' then 0 else 1 end) over() from (select id, attachment from activitystream where id in (select id from replies where inreplyto=$1) and type='Note' union select id, attachment from cacheactivitystream where id in (select id from replies where inreplyto=$1) and type='Note') as x` + + rows, err := config.DB.Query(query, parent.Id) + if err != nil { + return 0, 0, err + } + + defer rows.Close() + + for rows.Next() { + err = rows.Scan(&countId, &countImg) + } + + return countId, countImg, err +} + +func GetObjectRepliesDB(parent ObjectBase) (*CollectionBase, int, int, error) { + var nColl CollectionBase + var result []ObjectBase + + query := `select count(x.id) over(), sum(case when RTRIM(x.attachment) = '' then 0 else 1 end) over(), x.id, x.name, x.content, x.type, x.published, x.attributedto, x.attachment, x.preview, x.actor, x.tripcode, x.sensitive from (select * from activitystream where id in (select id from replies where inreplyto=$1) and (type='Note' or type='Archive') union select * from cacheactivitystream where id in (select id from replies where inreplyto=$1) and (type='Note' or type='Archive')) as x order by x.published asc` + + rows, err := config.DB.Query(query, parent.Id) + if err != nil { + return nil, 0, 0, err + } + + var postCount int + var attachCount int + + defer rows.Close() + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + var previewID string + + post.InReplyTo = append(post.InReplyTo, parent) + + if err := rows.Scan(&postCount, &attachCount, &post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.AttributedTo, &attachID, &previewID, &actor.Id, &post.TripCode, &post.Sensitive); err != nil { + return &nColl, postCount, attachCount, err + } + + post.Actor = actor.Id + + var postCnt int + var imgCnt int + + post.Replies, postCnt, imgCnt, err = GetObjectRepliesRepliesDB(post) + if err != nil { + return &nColl, postCount, attachCount, err + } + + post.Replies.TotalItems = postCnt + post.Replies.TotalImgs = imgCnt + + post.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return &nColl, postCount, attachCount, err + } + + post.Preview, err = GetObjectPreview(previewID) + if err != nil { + return &nColl, postCount, attachCount, err + } + + result = append(result, post) + } + + nColl.OrderedItems = result + + return &nColl, postCount, attachCount, nil +} + +func GetObjectRepliesDBLimit(parent ObjectBase, limit int) (*CollectionBase, int, int, error) { + var nColl CollectionBase + var result []ObjectBase + + query := `select count(x.id) over(), sum(case when RTRIM(x.attachment) = '' then 0 else 1 end) over(), x.id, x.name, x.content, x.type, x.published, x.attributedto, x.attachment, x.preview, x.actor, x.tripcode, x.sensitive from (select * from activitystream where id in (select id from replies where inreplyto=$1) and type='Note' union select * from cacheactivitystream where id in (select id from replies where inreplyto=$1) and type='Note') as x order by x.published desc limit $2` + + rows, err := config.DB.Query(query, parent.Id, limit) + if err != nil { + return nil, 0, 0, err + } + + var postCount int + var attachCount int + + defer rows.Close() + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + var previewID string + + post.InReplyTo = append(post.InReplyTo, parent) + + if err := rows.Scan(&postCount, &attachCount, &post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.AttributedTo, &attachID, &previewID, &actor.Id, &post.TripCode, &post.Sensitive); err != nil { + return &nColl, postCount, attachCount, err + } + + post.Actor = actor.Id + + var postCnt int + var imgCnt int + + post.Replies, postCnt, imgCnt, err = GetObjectRepliesRepliesDB(post) + if err != nil { + return &nColl, postCount, attachCount, err + } + + post.Replies.TotalItems = postCnt + post.Replies.TotalImgs = imgCnt + + post.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return &nColl, postCount, attachCount, err + } + + post.Preview, err = GetObjectPreview(previewID) + if err != nil { + return &nColl, postCount, attachCount, err + } + + result = append(result, post) + } + + nColl.OrderedItems = result + + sort.Sort(ObjectBaseSortAsc(nColl.OrderedItems)) + + return &nColl, postCount, attachCount, nil +} + +func GetObjectRepliesReplies(parent ObjectBase) (*CollectionBase, int, int, error) { + var nColl CollectionBase + var result []ObjectBase + + query := `select id, name, content, type, published, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where id in (select id from replies where inreplyto=$1) and (type='Note' or type='Archive') order by updated asc` + + rows, err := config.DB.Query(query, parent.Id) + if err != nil { + return &nColl, 0, 0, err + } + + defer rows.Close() + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + var previewID string + + post.InReplyTo = append(post.InReplyTo, parent) + + if err := rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.AttributedTo, &attachID, &previewID, &actor.Id, &post.TripCode, &post.Sensitive); err != nil { + return &nColl, 0, 0, err + } + + post.Actor = actor.Id + + post.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return &nColl, 0, 0, err + } + + post.Preview, err = GetObjectPreview(previewID) + if err != nil { + return &nColl, 0, 0, err + } + + result = append(result, post) + } + + nColl.OrderedItems = result + + return &nColl, 0, 0, nil +} + +func GetObjectRepliesRepliesDB(parent ObjectBase) (*CollectionBase, int, int, error) { + var nColl CollectionBase + var result []ObjectBase + + query := `select count(x.id) over(), sum(case when RTRIM(x.attachment) = '' then 0 else 1 end) over(), x.id, x.name, x.content, x.type, x.published, x.attributedto, x.attachment, x.preview, x.actor, x.tripcode, x.sensitive from (select * from activitystream where id in (select id from replies where inreplyto=$1) and type='Note' union select * from cacheactivitystream where id in (select id from replies where inreplyto=$1) and type='Note') as x order by x.published asc` + + rows, err := config.DB.Query(query, parent.Id) + if err != nil { + return &nColl, 0, 0, err + } + + var postCount int + var attachCount int + defer rows.Close() + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + var previewID string + + post.InReplyTo = append(post.InReplyTo, parent) + + if err := rows.Scan(&postCount, &attachCount, &post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.AttributedTo, &attachID, &previewID, &actor.Id, &post.TripCode, &post.Sensitive); err != nil { + return &nColl, postCount, attachCount, err + } + + post.Actor = actor.Id + + post.Attachment, err = GetObjectAttachment(attachID) + if err != nil { + return &nColl, postCount, attachCount, err + } + + post.Preview, err = GetObjectPreview(previewID) + if err != nil { + return &nColl, postCount, attachCount, err + } + + result = append(result, post) + } + + nColl.OrderedItems = result + + return &nColl, postCount, attachCount, nil +} + +func GetObjectTypeDB(id string) (string, error) { + query := `select type from activitystream where id=$1 union select type from cacheactivitystream where id=$1` + + rows, err := config.DB.Query(query, id) + if err != nil { + return "", err + } + defer rows.Close() + + var nType string + rows.Next() + rows.Scan(&nType) + + return nType, nil +} + +func GetObjectsWithoutPreviewsCallback(callback func(id string, href string, mediatype string, name string, size int, published time.Time) error) error { + query := `select id, href, mediatype, name, size, published from activitystream where id in (select attachment from activitystream where attachment!='' and preview='')` + + rows, err := config.DB.Query(query) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var id string + var href string + var mediatype string + var name string + var size int + var published time.Time + + if err := rows.Scan(&id, &href, &mediatype, &name, &size, &published); err != nil { + return err + } + + if err := callback(id, href, mediatype, name, size, published); err != nil { + return err + } + } + + return nil +} + +func GetToFromJson(to []byte) ([]string, error) { + var generic interface{} + + err := json.Unmarshal(to, &generic) + if err != nil { + return nil, err + } + + if generic != nil { + var nStr []string + switch generic.(type) { + case []interface{}: + err = json.Unmarshal(to, &nStr) + break + case string: + var str string + err = json.Unmarshal(to, &str) + nStr = append(nStr, str) + break + } + return nStr, err + } + + return nil, nil +} + +func IsObjectCached(id string) (bool, error) { + query := `select id from cacheactivitystream where id=$1` + rows, err := config.DB.Query(query, id) + if err != nil { + return false, err + } + + var nID string + defer rows.Close() + + rows.Next() + err = rows.Scan(&nID) + return nID != "", err +} + +func IsIDLocal(id string) (bool, error) { + activity, err := GetActivityFromDB(id) + return len(activity.OrderedItems) > 0, err +} + +func IsObjectLocal(id string) (bool, error) { + query := `select id from activitystream where id=$1` + + rows, err := config.DB.Query(query, id) + if err != nil { + return false, err + } + + var nID string + defer rows.Close() + + rows.Next() + err = rows.Scan(&nID) + return nID != "", err +} + +func MarkObjectSensitive(id string, sensitive bool) error { + var query = `update activitystream set sensitive=$1 where id=$2` + if _, err := config.DB.Exec(query, sensitive, id); err != nil { + return err + } + + query = `update cacheactivitystream set sensitive=$1 where id=$2` + _, err := config.DB.Exec(query, sensitive, id) + return err +} + +func ObjectFromJson(r *http.Request, obj ObjectBase) (ObjectBase, error) { + body, _ := ioutil.ReadAll(r.Body) + + var respActivity ActivityRaw + + err := json.Unmarshal(body, &respActivity) + if err != nil { + return obj, err + } + + res, err := HasContextFromJson(respActivity.AtContextRaw.Context) + + if err == nil && res { + var jObj ObjectBase + jObj, err = GetObjectFromJson(respActivity.ObjectRaw) + if err != nil { + return obj, err + } + + jObj.To, err = GetToFromJson(respActivity.ToRaw) + if err != nil { + return obj, err + } + + jObj.Cc, err = GetToFromJson(respActivity.CcRaw) + } + + return obj, err +} + +func SetAttachmentFromDB(id string, _type string) error { + datetime := time.Now().UTC().Format(time.RFC3339) + + query := `update activitystream set type=$1, deleted=$2 where id in (select attachment from activitystream where id=$3)` + + if _, err := config.DB.Exec(query, _type, datetime, id); err != nil { + return err + } + + query = `update cacheactivitystream set type=$1, deleted=$2 where id in (select attachment from cacheactivitystream where id=$3)` + + _, err := config.DB.Exec(query, _type, datetime, id) + return err +} + +func SetAttachmentRepliesFromDB(id string, _type string) error { + datetime := time.Now().UTC().Format(time.RFC3339) + + query := `update activitystream set type=$1, deleted=$2 where id in (select attachment from activitystream where id in (select id from replies where inreplyto=$3))` + + if _, err := config.DB.Exec(query, _type, datetime, id); err != nil { + return err + } + + query = `update cacheactivitystream set type=$1, deleted=$2 where id in (select attachment from cacheactivitystream where id in (select id from replies where inreplyto=$3))` + + _, err := config.DB.Exec(query, _type, datetime, id) + return err +} + +func SetPreviewFromDB(id string, _type string) error { + datetime := time.Now().UTC().Format(time.RFC3339) + + query := `update activitystream set type=$1, deleted=$2 where id in (select preview from activitystream where id=$3)` + + if _, err := config.DB.Exec(query, _type, datetime, id); err != nil { + return err + } + + query = `update cacheactivitystream set type=$1, deleted=$2 where id in (select preview from cacheactivitystream where id=$3)` + + _, err := config.DB.Exec(query, _type, datetime, id) + return err +} + +func SetPreviewRepliesFromDB(id string, _type string) error { + datetime := time.Now().UTC().Format(time.RFC3339) + + query := `update activitystream set type=$1, deleted=$2 where id in (select preview from activitystream where id in (select id from replies where inreplyto=$3))` + + if _, err := config.DB.Exec(query, _type, datetime, id); err != nil { + return err + } + + query = `update cacheactivitystream set type=$1, deleted=$2 where id in (select preview from cacheactivitystream where id in (select id from replies where inreplyto=$3))` + + _, err := config.DB.Exec(query, _type, datetime, id) + return err +} + +func SetObject(id string, _type string) error { + if err := SetAttachmentFromDB(id, _type); err != nil { + return err + } + + if err := SetPreviewFromDB(id, _type); err != nil { + return err + } + + return SetObjectFromDB(id, _type) +} + +func SetObjectAndReplies(id string, _type string) error { + if err := SetAttachmentFromDB(id, _type); err != nil { + return err + } + + if err := SetPreviewFromDB(id, _type); err != nil { + return err + } + + if err := SetObjectRepliesFromDB(id, _type); err != nil { + return err + } + + if err := SetAttachmentRepliesFromDB(id, _type); err != nil { + return err + } + + if err := SetPreviewRepliesFromDB(id, _type); err != nil { + return err + } + + return SetObjectFromDB(id, _type) +} + +func SetObjectFromDB(id string, _type string) error { + datetime := time.Now().UTC().Format(time.RFC3339) + + query := `update activitystream set type=$1, deleted=$2 where id=$3` + + if _, err := config.DB.Exec(query, _type, datetime, id); err != nil { + return err + } + + query = `update cacheactivitystream set type=$1, deleted=$2 where id=$3` + + _, err := config.DB.Exec(query, _type, datetime, id) + return err +} + +func SetObjectRepliesFromDB(id string, _type string) error { + datetime := time.Now().UTC().Format(time.RFC3339) + + var query = `update activitystream set type=$1, deleted=$2 where id in (select id from replies where inreplyto=$3)` + if _, err := config.DB.Exec(query, _type, datetime, id); err != nil { + return err + } + + query = `update cacheactivitystream set type=$1, deleted=$2 where id in (select id from replies where inreplyto=$3)` + _, err := config.DB.Exec(query, _type, datetime, id) + return err +} + +func SetObjectType(id string, nType string) error { + col, err := GetObjectFromDB(id) + if err != nil { + return err + } + + for _, e := range col.OrderedItems { + for _, k := range e.Replies.OrderedItems { + if err := UpdateObjectTypeDB(k.Id, nType); err != nil { + return err + } + } + + if err := UpdateObjectTypeDB(e.Id, nType); err != nil { + return err + } + } + + return nil +} + +func TombstoneAttachmentFromDB(id string) error { + datetime := time.Now().UTC().Format(time.RFC3339) + + query := `update activitystream set type='Tombstone', mediatype='image/png', href=$1, name='', content='', attributedto='deleted', deleted=$2 where id in (select attachment from activitystream where id=$3)` + + if _, err := config.DB.Exec(query, config.Domain+"/static/notfound.png", datetime, id); err != nil { + return err + } + + query = `update cacheactivitystream set type='Tombstone', mediatype='image/png', href=$1, name='', content='', attributedto='deleted', deleted=$2 where id in (select attachment from cacheactivitystream where id=$3)` + + _, err := config.DB.Exec(query, config.Domain+"/static/notfound.png", datetime, id) + return err +} + +func TombstoneAttachmentRepliesFromDB(id string) error { + query := `select id from activitystream where id in (select id from replies where inreplyto=$1)` + + rows, err := config.DB.Query(query, id) + if err != nil { + return err + } + + defer rows.Close() + for rows.Next() { + var attachment string + + if err := rows.Scan(&attachment); err != nil { + return err + } + + if err := DeleteAttachmentFromFile(attachment); err != nil { + return err + } + + if err := TombstoneAttachmentFromDB(attachment); err != nil { + return err + } + } + + return nil +} + +func TombstonePreviewFromDB(id string) error { + datetime := time.Now().UTC().Format(time.RFC3339) + + query := `update activitystream set type='Tombstone', mediatype='image/png', href=$1, name='', content='', attributedto='deleted', deleted=$2 where id in (select preview from activitystream where id=$3)` + + if _, err := config.DB.Exec(query, config.Domain+"/static/notfound.png", datetime, id); err != nil { + return err + } + + query = `update cacheactivitystream set type='Tombstone', mediatype='image/png', href=$1, name='', content='', attributedto='deleted', deleted=$2 where id in (select preview from cacheactivitystream where id=$3)` + + _, err := config.DB.Exec(query, config.Domain+"/static/notfound.png", datetime, id) + return err +} + +func TombstonePreviewRepliesFromDB(id string) error { + query := `select id from activitystream where id in (select id from replies where inreplyto=$1)` + + rows, err := config.DB.Query(query, id) + if err != nil { + return err + } + + defer rows.Close() + for rows.Next() { + var attachment string + + if err := rows.Scan(&attachment); err != nil { + return err + } + + if err := DeletePreviewFromFile(attachment); err != nil { + return err + } + + if err := TombstonePreviewFromDB(attachment); err != nil { + return err + } + } + + return nil +} + +func TombstoneObject(id string) error { + if err := DeleteReportActivity(id); err != nil { + return err + } + + if err := DeleteAttachmentFromFile(id); err != nil { + return err + } + + if err := TombstoneAttachmentFromDB(id); err != nil { + return err + } + + if err := DeletePreviewFromFile(id); err != nil { + return err + } + + if err := TombstonePreviewFromDB(id); err != nil { + return err + } + + return TombstoneObjectFromDB(id) +} + +func TombstoneObjectAndReplies(id string) error { + if err := DeleteReportActivity(id); err != nil { + return err + } + + if err := DeleteAttachmentFromFile(id); err != nil { + return err + } + + if err := TombstoneAttachmentFromDB(id); err != nil { + return err + } + + if err := DeletePreviewFromFile(id); err != nil { + return err + } + + if err := TombstonePreviewFromDB(id); err != nil { + return err + } + + if err := TombstoneObjectRepliesFromDB(id); err != nil { + return err + } + + if err := TombstoneAttachmentRepliesFromDB(id); err != nil { + return err + } + + if err := TombstonePreviewRepliesFromDB(id); err != nil { + return err + } + + return TombstoneObjectFromDB(id) +} + +func TombstoneObjectFromDB(id string) error { + datetime := time.Now().UTC().Format(time.RFC3339) + query := `update activitystream set type='Tombstone', name='', content='', attributedto='deleted', tripcode='', deleted=$1 where id=$2` + + if _, err := config.DB.Exec(query, datetime, id); err != nil { + return err + } + + query = `update cacheactivitystream set type='Tombstone', name='', content='', attributedto='deleted', tripcode='', deleted=$1 where id=$2` + + _, err := config.DB.Exec(query, datetime, id) + return err +} + +func TombstoneObjectRepliesFromDB(id string) error { + datetime := time.Now().UTC().Format(time.RFC3339) + + query := `update activitystream set type='Tombstone', name='', content='', attributedto='deleted', tripcode='', deleted=$1 where id in (select id from replies where inreplyto=$2)` + + if _, err := config.DB.Exec(query, datetime, id); err != nil { + return err + } + + query = `update cacheactivitystream set type='Tombstone', name='', content='', attributedto='deleted', tripcode='', deleted=$1 where id in (select id from replies where inreplyto=$2)` + + _, err := config.DB.Exec(query, datetime, id) + return err +} + +func UpdateObjectTypeDB(id string, nType string) error { + query := `update activitystream set type=$2 where id=$1 and type !='Tombstone'` + if _, err := config.DB.Exec(query, id, nType); err != nil { + return err + } + + query = `update cacheactivitystream set type=$2 where id=$1 and type !='Tombstone'` + _, err := config.DB.Exec(query, id, nType) + return err +} + +func UpdateObjectWithPreview(id string, preview string) error { + query := `update activitystream set preview=$1 where attachment=$2` + + _, err := config.DB.Exec(query, preview, id) + return err +} + +func AddFollower(id string, follower string) error { + query := `insert into follower (id, follower) values ($1, $2)` + + _, err := config.DB.Exec(query, id, follower) + return err +} + +func WriteObjectReplyToDB(obj ObjectBase) error { + for i, e := range obj.InReplyTo { + + if res, err := CheckIfObjectOP(obj.Id); err == nil && !res && i == 0 { + nType, err := GetObjectTypeDB(e.Id) + if err != nil { + return err + } + + if nType == "Archive" { + if err := UpdateObjectTypeDB(obj.Id, "Archive"); err != nil { + return err + } + } + } else if err != nil { + return err + } + + query := `select id from replies where id=$1 and inreplyto=$2` + + rows, err := config.DB.Query(query, obj.Id, e.Id) + if err != nil { + return err + } + defer rows.Close() + + var id string + rows.Next() + if err := rows.Scan(&id); err != nil { + return err + } + + if id == "" { + query := `insert into replies (id, inreplyto) values ($1, $2)` + + _, err := config.DB.Exec(query, obj.Id, e.Id) + if err != nil { + return err + } + } + + update := true + for _, e := range obj.Option { + if e == "sage" || e == "nokosage" { + update = false + break + } + } + + if update { + if err := WriteObjectUpdatesToDB(e); err != nil { + return err + } + } + } + + if len(obj.InReplyTo) < 1 { + query := `select id from replies where id=$1 and inreplyto=$2` + + rows, err := config.DB.Query(query, obj.Id, "") + if err != nil { + return err + } + defer rows.Close() + + var id string + rows.Next() + rows.Scan(&id) + + if id == "" { + query := `insert into replies (id, inreplyto) values ($1, $2)` + + if _, err := config.DB.Exec(query, obj.Id, ""); err != nil { + return err + } + } + } + + return nil +} + +func WriteObjectReplyToLocalDB(id string, replyto string) error { + query := `select id from replies where id=$1 and inreplyto=$2` + + rows, err := config.DB.Query(query, id, replyto) + if err != nil { + return err + } + defer rows.Close() + + var nID string + rows.Next() + rows.Scan(&nID) + + if nID == "" { + query := `insert into replies (id, inreplyto) values ($1, $2)` + + if _, err := config.DB.Exec(query, id, replyto); err != nil { + return err + } + } + + query = `select inreplyto from replies where id=$1` + + rows, err = config.DB.Query(query, replyto) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var val string + rows.Scan(&val) + if val == "" { + updated := time.Now().UTC().Format(time.RFC3339) + query := `update activitystream set updated=$1 where id=$2` + + if _, err := config.DB.Exec(query, updated, replyto); err != nil { + return err + } + } + } + + return nil +} + +func WriteObjectToCache(obj ObjectBase) (ObjectBase, error) { + if res, err := post.IsPostBlacklist(obj.Content); err == nil && res { + fmt.Println("\n\nBlacklist post blocked\n\n") + return obj, nil + } else { + return obj, err + } + + if len(obj.Attachment) > 0 { + if obj.Preview.Href != "" { + WritePreviewToCache(*obj.Preview) + } + + for i, _ := range obj.Attachment { + WriteAttachmentToCache(obj.Attachment[i]) + WriteActivitytoCacheWithAttachment(obj, obj.Attachment[i], *obj.Preview) + } + + } else { + WriteActivitytoCache(obj) + } + + WriteObjectReplyToDB(obj) + + if obj.Replies != nil { + for _, e := range obj.Replies.OrderedItems { + WriteObjectToCache(e) + } + } + + return obj, nil +} + +func WriteObjectToDB(obj ObjectBase) (ObjectBase, error) { + id, err := util.CreateUniqueID(obj.Actor) + if err != nil { + return obj, err + } + + obj.Id = fmt.Sprintf("%s/%s", obj.Actor, id) + if len(obj.Attachment) > 0 { + if obj.Preview.Href != "" { + id, err := util.CreateUniqueID(obj.Actor) + if err != nil { + return obj, err + } + + obj.Preview.Id = fmt.Sprintf("%s/%s", obj.Actor, id) + obj.Preview.Published = time.Now().UTC() + obj.Preview.Updated = time.Now().UTC() + obj.Preview.AttributedTo = obj.Id + if err := WritePreviewToDB(*obj.Preview); err != nil { + return obj, err + } + } + + for i := range obj.Attachment { + id, err := util.CreateUniqueID(obj.Actor) + if err != nil { + return obj, err + } + + obj.Attachment[i].Id = fmt.Sprintf("%s/%s", obj.Actor, id) + obj.Attachment[i].Published = time.Now().UTC() + obj.Attachment[i].Updated = time.Now().UTC() + obj.Attachment[i].AttributedTo = obj.Id + WriteAttachmentToDB(obj.Attachment[i]) + WriteActivitytoDBWithAttachment(obj, obj.Attachment[i], *obj.Preview) + } + + } else { + if err := WriteActivitytoDB(obj); err != nil { + return obj, err + } + } + + if err := WriteObjectReplyToDB(obj); err != nil { + return obj, err + } + + err = WriteWalletToDB(obj) + return obj, err +} + +func WriteObjectUpdatesToDB(obj ObjectBase) error { + query := `update activitystream set updated=$1 where id=$2` + + if _, err := config.DB.Exec(query, time.Now().UTC().Format(time.RFC3339), obj.Id); err != nil { + return err + } + + query = `update cacheactivitystream set updated=$1 where id=$2` + + _, err := config.DB.Exec(query, time.Now().UTC().Format(time.RFC3339), obj.Id) + return err +} + +func WriteWalletToDB(obj ObjectBase) error { + for _, e := range obj.Option { + if e == "wallet" { + for _, e := range obj.Wallet { + query := `insert into wallet (id, type, address) values ($1, $2, $3)` + + if _, err := config.DB.Exec(query, obj.Id, e.Type, e.Address); err != nil { + return err + } + } + + return nil + } + } + return nil +} diff --git a/activitypub/pem.go b/activitypub/pem.go new file mode 100644 index 0000000..ca6c068 --- /dev/null +++ b/activitypub/pem.go @@ -0,0 +1,241 @@ +package activitypub + +import ( + crand "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "os" + "regexp" + "strings" + + "github.com/FChannel0/FChannel-Server/config" +) + +type Signature struct { + KeyId string + Headers []string + Signature string + Algorithm string +} + +func CreatePem(actor Actor) error { + privatekey, err := rsa.GenerateKey(crand.Reader, 2048) + if err != nil { + return err + } + + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privatekey) + + privateKeyBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + } + + privatePem, err := os.Create("./pem/board/" + actor.Name + "-private.pem") + if err != nil { + return err + } + + if err := pem.Encode(privatePem, privateKeyBlock); err != nil { + return err + } + + publickey := &privatekey.PublicKey + publicKeyBytes, err := x509.MarshalPKIXPublicKey(publickey) + if err != nil { + return err + } + + publicKeyBlock := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: publicKeyBytes, + } + + publicPem, err := os.Create("./pem/board/" + actor.Name + "-public.pem") + if err != nil { + return err + } + + if err := pem.Encode(publicPem, publicKeyBlock); err != nil { + return err + } + + _, err = os.Stat("./pem/board/" + actor.Name + "-public.pem") + if os.IsNotExist(err) { + return err + } else { + return StorePemToDB(actor) + } + + fmt.Println(`Created PEM keypair for the "` + actor.Name + `" board. Please keep in mind that +the PEM key is crucial in identifying yourself as the legitimate owner of the board, +so DO NOT LOSE IT!!! If you lose it, YOU WILL LOSE ACCESS TO YOUR BOARD!`) + + return nil +} + +func CreatePublicKeyFromPrivate(actor *Actor, publicKeyPem string) error { + publicFilename, err := GetActorPemFileFromDB(publicKeyPem) + if err != nil { + return err + } + + privateFilename := strings.ReplaceAll(publicFilename, "public.pem", "private.pem") + if _, err := os.Stat(privateFilename); err == nil { + // Not a lost cause + priv, err := ioutil.ReadFile(privateFilename) + if err != nil { + return err + } + + block, _ := pem.Decode([]byte(priv)) + if block == nil || block.Type != "RSA PRIVATE KEY" { + return errors.New("failed to decode PEM block containing public key") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return err + } + + publicKeyDer, err := x509.MarshalPKIXPublicKey(&key.PublicKey) + if err != nil { + return err + } + + pubKeyBlock := pem.Block{ + Type: "PUBLIC KEY", + Headers: nil, + Bytes: publicKeyDer, + } + + publicFileWriter, err := os.Create(publicFilename) + if err != nil { + return err + } + + if err := pem.Encode(publicFileWriter, &pubKeyBlock); err != nil { + return err + } + } else { + fmt.Println(`\nUnable to locate private key from public key generation. Now, +this means that you are now missing the proof that you are the +owner of the "` + actor.Name + `" board. If you are the developer, +then your job is just as easy as generating a new keypair, but +if this board is live, then you'll also have to convince the other +owners to switch their public keys for you so that they will start +accepting your posts from your board from this site. Good luck ;)`) + return errors.New("unable to locate private key") + } + return nil +} + +func GetActorPemFromDB(pemID string) (PublicKeyPem, error) { + var pem PublicKeyPem + + query := `select id, owner, file from publickeypem where id=$1` + + rows, err := config.DB.Query(query, pemID) + if err != nil { + return pem, err + } + + defer rows.Close() + + rows.Next() + rows.Scan(&pem.Id, &pem.Owner, &pem.PublicKeyPem) + f, err := os.ReadFile(pem.PublicKeyPem) + if err != nil { + return pem, err + } + + pem.PublicKeyPem = strings.ReplaceAll(string(f), "\r\n", `\n`) + + return pem, nil +} + +func GetActorPemFileFromDB(pemID string) (string, error) { + query := `select file from publickeypem where id=$1` + rows, err := config.DB.Query(query, pemID) + if err != nil { + return "", err + } + + defer rows.Close() + + var file string + rows.Next() + rows.Scan(&file) + + return file, nil +} + +func StorePemToDB(actor Actor) error { + query := "select publicKeyPem from actor where id=$1" + rows, err := config.DB.Query(query, actor.Id) + if err != nil { + return err + } + + defer rows.Close() + + var result string + rows.Next() + rows.Scan(&result) + + if result != "" { + return errors.New("already storing public key for actor") + } + + publicKeyPem := actor.Id + "#main-key" + query = "update actor set publicKeyPem=$1 where id=$2" + if _, err := config.DB.Exec(query, publicKeyPem, actor.Id); err != nil { + return err + } + + file := "./pem/board/" + actor.Name + "-public.pem" + query = "insert into publicKeyPem (id, owner, file) values($1, $2, $3)" + _, err = config.DB.Exec(query, publicKeyPem, actor.Id, file) + return err +} + +func ParseHeaderSignature(signature string) Signature { + var nsig Signature + + keyId := regexp.MustCompile(`keyId=`) + headers := regexp.MustCompile(`headers=`) + sig := regexp.MustCompile(`signature=`) + algo := regexp.MustCompile(`algorithm=`) + + signature = strings.ReplaceAll(signature, "\"", "") + parts := strings.Split(signature, ",") + + for _, e := range parts { + if keyId.MatchString(e) { + nsig.KeyId = keyId.ReplaceAllString(e, "") + continue + } + + if headers.MatchString(e) { + header := headers.ReplaceAllString(e, "") + nsig.Headers = strings.Split(header, " ") + continue + } + + if sig.MatchString(e) { + nsig.Signature = sig.ReplaceAllString(e, "") + continue + } + + if algo.MatchString(e) { + nsig.Algorithm = algo.ReplaceAllString(e, "") + continue + } + } + + return nsig +} |