From 25829d2d0e379c323b8f2ae6e7c2aad7548f0a30 Mon Sep 17 00:00:00 2001 From: FChannel <> Date: Sat, 18 Jun 2022 13:57:30 -0700 Subject: sticky and lock implemented --- activitypub/activityPubStruct.go | 218 -------------------------------------- activitypub/actor.go | 89 +++++++++++++++- activitypub/object.go | 99 ++++++++++++++++++ activitypub/structs.go | 220 +++++++++++++++++++++++++++++++++++++++ databaseschema.psql | 12 ++- main.go | 2 + route/routes/boardmgmt.go | 96 +++++++++++++++++ route/util.go | 6 ++ static/locked.png | Bin 0 -> 717 bytes static/pin.png | Bin 0 -> 6118 bytes views/catalog.html | 2 +- views/partials/posts.html | 8 +- views/partials/top.html | 6 +- 13 files changed, 532 insertions(+), 226 deletions(-) delete mode 100644 activitypub/activityPubStruct.go create mode 100644 activitypub/structs.go create mode 100644 static/locked.png create mode 100644 static/pin.png diff --git a/activitypub/activityPubStruct.go b/activitypub/activityPubStruct.go deleted file mode 100644 index b8e3180..0000000 --- a/activitypub/activityPubStruct.go +++ /dev/null @@ -1,218 +0,0 @@ -package activitypub - -import ( - "time" - - "encoding/json" - "html/template" -) - -type AtContextRaw struct { - Context json.RawMessage `json:"@context,omitempty"` -} - -type ActivityRaw struct { - AtContextRaw - Type string `json:"type,omitempty"` - Id string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Summary string `json:"summary,omitempty"` - Auth string `json:"auth,omitempty"` - ToRaw json.RawMessage `json:"to,omitempty"` - BtoRaw json.RawMessage `json:"bto,omitempty"` - CcRaw json.RawMessage `json:"cc,omitempty"` - Published time.Time `json:"published,omitempty"` - ActorRaw json.RawMessage `json:"actor,omitempty"` - ObjectRaw json.RawMessage `json:"object,omitempty"` -} - -type AtContext struct { - Context string `json:"@context,omitempty"` -} - -type AtContextArray struct { - Context []interface{} `json:"@context,omitempty"` -} - -type AtContextString struct { - Context string `json:"@context,omitempty"` -} - -type ActorString struct { - Actor string `json:"actor,omitempty"` -} - -type ObjectArray struct { - Object []ObjectBase `json:"object,omitempty"` -} - -type Object struct { - Object *ObjectBase `json:"object,omitempty"` -} - -type ObjectString struct { - Object string `json:"object,omitempty"` -} - -type ToArray struct { - To []string `json:"to,omitempty"` -} - -type ToString struct { - To string `json:"to,omitempty"` -} - -type CcArray struct { - Cc []string `json:"cc,omitempty"` -} - -type CcOjectString struct { - Cc string `json:"cc,omitempty"` -} - -type Actor struct { - Type string `json:"type,omitempty"` - Id string `json:"id,omitempty"` - Inbox string `json:"inbox,omitempty"` - Outbox string `json:"outbox,omitempty"` - Following string `json:"following,omitempty"` - Followers string `json:"followers,omitempty"` - Name string `json:"name,omitempty"` - PreferredUsername string `json:"preferredUsername,omitempty"` - PublicKey PublicKeyPem `json:"publicKey,omitempty"` - Summary string `json:"summary,omitempty"` - AuthRequirement []string `json:"authrequirement,omitempty"` - Restricted bool `json:"restricted"` -} - -type PublicKeyPem struct { - Id string `json:"id,omitempty"` - Owner string `json:"owner,omitempty"` - PublicKeyPem string `json:"publicKeyPem,omitempty"` -} - -type Activity struct { - AtContext - Type string `json:"type,omitempty"` - Id string `json:"id,omitempty"` - Actor *Actor `json:"actor,omitempty"` - Name string `json:"name,omitempty"` - Summary string `json:"summary,omitempty"` - Auth string `json:"auth,omitempty"` - To []string `json:"to,omitempty"` - Bto []string `json:"bto,omitempty"` - Cc []string `json:"cc,omitempty"` - Published time.Time `json:"published,omitempty"` - Object ObjectBase `json:"object,omitempty"` -} - -type ObjectBase struct { - Type string `json:"type,omitempty"` - Id string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Option []string `json:"option,omitempty"` - Alias string `json:"alias,omitempty"` - AttributedTo string `json:"attributedTo,omitempty"` - TripCode string `json:"tripcode,omitempty"` - Actor string `json:"actor,omitempty"` - Audience string `json:"audience,omitempty"` - ContentHTML template.HTML `json:"contenthtml,omitempty"` - Content string `json:"content,omitempty"` - EndTime string `json:"endTime,omitempty"` - Generator string `json:"generator,omitempty"` - Icon string `json:"icon,omitempty"` - Image string `json:"image,omitempty"` - InReplyTo []ObjectBase `json:"inReplyTo,omitempty"` - Location string `json:"location,omitempty"` - Preview *NestedObjectBase `json:"preview,omitempty"` - Published time.Time `json:"published,omitempty"` - Updated time.Time `json:"updated,omitempty"` - Object *NestedObjectBase `json:"object,omitempty"` - Attachment []ObjectBase `json:"attachment,omitempty"` - Replies CollectionBase `json:"replies,omitempty"` - StartTime string `json:"startTime,omitempty"` - Summary string `json:"summary,omitempty"` - Tag []ObjectBase `json:"tag,omitempty"` - Wallet []CryptoCur `json:"wallet,omitempty"` - Deleted string `json:"deleted,omitempty"` - Url []ObjectBase `json:"url,omitempty"` - Href string `json:"href,omitempty"` - To []string `json:"to,omitempty"` - Bto []string `json:"bto,omitempty"` - Cc []string `json:"cc,omitempty"` - Bcc string `json:"Bcc,omitempty"` - MediaType string `json:"mediatype,omitempty"` - Duration string `json:"duration,omitempty"` - Size int64 `json:"size,omitempty"` - Sensitive bool `json:"sensitive,omitempty"` -} - -type CryptoCur struct { - Type string `json:"type,omitempty"` - Address string `json:"address,omitempty"` -} - -type NestedObjectBase struct { - AtContext - Type string `json:"type,omitempty"` - Id string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Alias string `json:"alias,omitempty"` - AttributedTo string `json:"attributedTo,omitempty"` - TripCode string `json:"tripcode,omitempty"` - Actor string `json:"actor,omitempty"` - Audience string `json:"audience,omitempty"` - ContentHTML template.HTML `json:"contenthtml,omitempty"` - Content string `json:"content,omitempty"` - EndTime string `json:"endTime,omitempty"` - Generator string `json:"generator,omitempty"` - Icon string `json:"icon,omitempty"` - Image string `json:"image,omitempty"` - InReplyTo []ObjectBase `json:"inReplyTo,omitempty"` - Location string `json:"location,omitempty"` - Preview ObjectBase `json:"preview,omitempty"` - Published time.Time `json:"published,omitempty"` - Attachment []ObjectBase `json:"attachment,omitempty"` - Replies *CollectionBase `json:"replies,omitempty"` - StartTime string `json:"startTime,omitempty"` - Summary string `json:"summary,omitempty"` - Tag []ObjectBase `json:"tag,omitempty"` - Updated time.Time `json:"updated,omitempty"` - Deleted string `json:"deleted,omitempty"` - Url []ObjectBase `json:"url,omitempty"` - Href string `json:"href,omitempty"` - To []string `json:"to,omitempty"` - Bto []string `json:"bto,omitempty"` - Cc []string `json:"cc,omitempty"` - Bcc string `json:"Bcc,omitempty"` - MediaType string `json:"mediatype,omitempty"` - Duration string `json:"duration,omitempty"` - Size int64 `json:"size,omitempty"` -} - -type CollectionBase struct { - Actor Actor `json:"actor,omitempty"` - Summary string `json:"summary,omitempty"` - Type string `json:"type,omitempty"` - TotalItems int `json:"totalItems,omitempty"` - TotalImgs int `json:"totalImgs,omitempty"` - OrderedItems []ObjectBase `json:"orderedItems,omitempty"` - Items []ObjectBase `json:"items,omitempty"` -} - -type Collection struct { - AtContext - CollectionBase -} - -type ObjectBaseSortDesc []ObjectBase - -func (a ObjectBaseSortDesc) Len() int { return len(a) } -func (a ObjectBaseSortDesc) Less(i, j int) bool { return a[i].Updated.After(a[j].Updated) } -func (a ObjectBaseSortDesc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } - -type ObjectBaseSortAsc []ObjectBase - -func (a ObjectBaseSortAsc) Len() int { return len(a) } -func (a ObjectBaseSortAsc) Less(i, j int) bool { return a[i].Published.Before(a[j].Published) } -func (a ObjectBaseSortAsc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } diff --git a/activitypub/actor.go b/activitypub/actor.go index 9996abd..4142685 100644 --- a/activitypub/actor.go +++ b/activitypub/actor.go @@ -214,11 +214,18 @@ func (actor Actor) GetCatalogCollection() (Collection, error) { var err error var rows *sql.Rows - 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` + 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' and id not in (select activity_id from sticky where actor_id=$1) 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' and id not in (select activity_id from sticky where actor_id=$1) 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' and id not in (select activity_id from sticky where actor_id=$1)) as x order by x.updated desc limit 165` + if rows, err = config.DB.Query(query, actor.Id); err != nil { return nColl, util.MakeError(err, "GetCatalogCollection") } + stickies, _ := actor.GetStickies() + + for _, e := range stickies.OrderedItems { + result = append(result, e) + } + defer rows.Close() for rows.Next() { var post ObjectBase @@ -236,6 +243,7 @@ func (actor Actor) GetCatalogCollection() (Collection, error) { return nColl, util.MakeError(err, "GetCatalogCollection") } + post.Locked, _ = post.IsLocked() post.Actor = actor.Id var replies CollectionBase @@ -277,9 +285,22 @@ func (actor Actor) GetCollectionPage(page int) (Collection, error) { var err error var rows *sql.Rows - 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` + 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' and id not in (select activity_id from sticky where actor_id=$1) 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' and id not in (select activity_id from sticky where actor_id=$1) union select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from cacheactivitystream where id not in (select activity_id from sticky where actor_id=$1) and 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 $2 offset $3` + + limit := 15 + + if page == 0 { + stickies, _ := actor.GetStickies() + limit = limit - stickies.TotalItems + + for _, e := range stickies.OrderedItems { + result = append(result, e) + } + } - if rows, err = config.DB.Query(query, actor.Id, page*15); err != nil { + offset := page * limit + + if rows, err = config.DB.Query(query, actor.Id, limit, offset); err != nil { return nColl, util.MakeError(err, "GetCollectionPage") } @@ -301,6 +322,7 @@ func (actor Actor) GetCollectionPage(page int) (Collection, error) { return nColl, util.MakeError(err, "GetCollectionPage") } + post.Locked, _ = post.IsLocked() post.Actor = actor.Id post.Replies, post.Replies.TotalItems, post.Replies.TotalImgs, err = post.GetRepliesLimit(5) @@ -359,6 +381,9 @@ func (actor Actor) GetCollection() (Collection, error) { return nColl, util.MakeError(err, "GetCollection") } + post.Sticky, _ = post.IsSticky() + post.Locked, _ = post.IsLocked() + post.Actor = actor.Id post.Replies, post.Replies.TotalItems, post.Replies.TotalImgs, err = post.GetReplies() @@ -1181,6 +1206,10 @@ func (actor Actor) ProcessInboxCreate(activity Activity) error { return util.MakeError(errors.New("Object does not exist"), "ActorInbox") } + if locked, _ := activity.Object.InReplyTo[0].IsLocked(); locked { + return util.MakeError(errors.New("Object locked"), "ActorInbox") + } + if wantToCache, err := activity.Object.WantToCache(actor); !wantToCache { return util.MakeError(err, "ActorInbox") } @@ -1197,3 +1226,57 @@ func (actor Actor) ProcessInboxCreate(activity Activity) error { return nil } + +func (actor Actor) GetStickies() (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' and id in (select activity_id from sticky where actor_id=$1) 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' and id in (select activity_id from sticky where actor_id=$1)) as x order by x.updated desc limit 15` + + rows, err := config.DB.Query(query, actor.Id) + + if err != nil { + return nColl, util.MakeError(err, "GetStickies") + } + + var count int + defer rows.Close() + for rows.Next() { + var post ObjectBase + var actor Actor + + var attch ObjectBase + post.Attachment = append(post.Attachment, attch) + + var prev NestedObjectBase + post.Preview = &prev + + err = rows.Scan(&count, &post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.Updated, &post.AttributedTo, &post.Attachment[0].Id, &post.Preview.Id, &actor.Id, &post.TripCode, &post.Sensitive) + + if err != nil { + return nColl, util.MakeError(err, "GetStickies") + } + + post.Sticky = true + post.Locked, _ = post.IsLocked() + post.Actor = actor.Id + + var postCnt int + var imgCnt int + post.Replies, postCnt, imgCnt, _ = post.GetRepliesLimit(5) + + post.Replies.TotalItems = postCnt + post.Replies.TotalImgs = imgCnt + + post.Attachment, _ = post.Attachment[0].GetAttachment() + + post.Preview, _ = post.Preview.GetPreview() + + result = append(result, post) + } + + nColl.TotalItems = count + nColl.OrderedItems = result + + return nColl, nil +} diff --git a/activitypub/object.go b/activitypub/object.go index 5eb8e67..acbe4a1 100644 --- a/activitypub/object.go +++ b/activitypub/object.go @@ -367,6 +367,9 @@ func (obj ObjectBase) GetCollectionLocal() (Collection, error) { return nColl, util.MakeError(err, "GetCollectionLocal") } + post.Sticky, _ = post.IsSticky() + post.Locked, _ = post.IsLocked() + post.Actor = actor.Id if post.InReplyTo, err = post.GetInReplyTo(); err != nil { @@ -462,6 +465,9 @@ func (obj ObjectBase) GetCollectionFromPath() (Collection, error) { return nColl, nil } + post.Sticky, _ = post.IsSticky() + post.Locked, _ = post.IsLocked() + post.Actor = actor.Id if post.InReplyTo, err = post.GetInReplyTo(); err != nil { @@ -1361,3 +1367,96 @@ func (obj ObjectBase) WriteWithAttachment(attachment ObjectBase) { panic(e) } } + +func (obj ObjectBase) MarkSticky(actorID string) error { + var count int + + var query = `select count(id) from replies where inreplyto='' and id=$1` + if err := config.DB.QueryRow(query, obj.Id).Scan(&count); err != nil { + return util.MakeError(err, "MarkSticky") + } + + if count == 1 { + var nCount int + query = `select count(activity_id) from sticky where activity_id=$1` + if err := config.DB.QueryRow(query, obj.Id).Scan(&nCount); err != nil { + return util.MakeError(err, "MarkSticky") + } + + if nCount > 0 { + query = `delete from sticky where activity_id=$1` + if _, err := config.DB.Exec(query, obj.Id); err != nil { + return util.MakeError(err, "MarkSticky") + } + } else { + query = `insert into sticky (actor_id, activity_id) values ($1, $2)` + if _, err := config.DB.Exec(query, actorID, obj.Id); err != nil { + return util.MakeError(err, "MarkSticky") + } + } + } + + return nil +} + +func (obj ObjectBase) MarkLocked(actorID string) error { + var count int + + var query = `select count(id) from replies where inreplyto='' and id=$1` + if err := config.DB.QueryRow(query, obj.Id).Scan(&count); err != nil { + return util.MakeError(err, "MarkLocked") + } + + if count == 1 { + var nCount int + + query = `select count(activity_id) from locked where activity_id=$1` + if err := config.DB.QueryRow(query, obj.Id).Scan(&nCount); err != nil { + return util.MakeError(err, "MarkLocked") + } + + if nCount > 0 { + query = `delete from locked where activity_id=$1` + if _, err := config.DB.Exec(query, obj.Id); err != nil { + return util.MakeError(err, "MarkLocked") + } + } else { + query = `insert into locked (actor_id, activity_id) values ($1, $2)` + if _, err := config.DB.Exec(query, actorID, obj.Id); err != nil { + return util.MakeError(err, "MarkLocked") + } + } + } + + return nil +} + +func (obj ObjectBase) IsSticky() (bool, error) { + var count int + + query := `select count(activity_id) from sticky where activity_id=$1 ` + if err := config.DB.QueryRow(query, obj.Id).Scan(&count); err != nil { + return false, util.MakeError(err, "IsSticky") + } + + if count != 0 { + return true, nil + } + + return false, nil +} + +func (obj ObjectBase) IsLocked() (bool, error) { + var count int + + query := `select count(activity_id) from locked where activity_id=$1 ` + if err := config.DB.QueryRow(query, obj.Id).Scan(&count); err != nil { + return false, util.MakeError(err, "IsSticky") + } + + if count != 0 { + return true, nil + } + + return false, nil +} diff --git a/activitypub/structs.go b/activitypub/structs.go new file mode 100644 index 0000000..c42d175 --- /dev/null +++ b/activitypub/structs.go @@ -0,0 +1,220 @@ +package activitypub + +import ( + "time" + + "encoding/json" + "html/template" +) + +type AtContextRaw struct { + Context json.RawMessage `json:"@context,omitempty"` +} + +type ActivityRaw struct { + AtContextRaw + Type string `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Summary string `json:"summary,omitempty"` + Auth string `json:"auth,omitempty"` + ToRaw json.RawMessage `json:"to,omitempty"` + BtoRaw json.RawMessage `json:"bto,omitempty"` + CcRaw json.RawMessage `json:"cc,omitempty"` + Published time.Time `json:"published,omitempty"` + ActorRaw json.RawMessage `json:"actor,omitempty"` + ObjectRaw json.RawMessage `json:"object,omitempty"` +} + +type AtContext struct { + Context string `json:"@context,omitempty"` +} + +type AtContextArray struct { + Context []interface{} `json:"@context,omitempty"` +} + +type AtContextString struct { + Context string `json:"@context,omitempty"` +} + +type ActorString struct { + Actor string `json:"actor,omitempty"` +} + +type ObjectArray struct { + Object []ObjectBase `json:"object,omitempty"` +} + +type Object struct { + Object *ObjectBase `json:"object,omitempty"` +} + +type ObjectString struct { + Object string `json:"object,omitempty"` +} + +type ToArray struct { + To []string `json:"to,omitempty"` +} + +type ToString struct { + To string `json:"to,omitempty"` +} + +type CcArray struct { + Cc []string `json:"cc,omitempty"` +} + +type CcOjectString struct { + Cc string `json:"cc,omitempty"` +} + +type Actor struct { + Type string `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Inbox string `json:"inbox,omitempty"` + Outbox string `json:"outbox,omitempty"` + Following string `json:"following,omitempty"` + Followers string `json:"followers,omitempty"` + Name string `json:"name,omitempty"` + PreferredUsername string `json:"preferredUsername,omitempty"` + PublicKey PublicKeyPem `json:"publicKey,omitempty"` + Summary string `json:"summary,omitempty"` + AuthRequirement []string `json:"authrequirement,omitempty"` + Restricted bool `json:"restricted"` +} + +type PublicKeyPem struct { + Id string `json:"id,omitempty"` + Owner string `json:"owner,omitempty"` + PublicKeyPem string `json:"publicKeyPem,omitempty"` +} + +type Activity struct { + AtContext + Type string `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Actor *Actor `json:"actor,omitempty"` + Name string `json:"name,omitempty"` + Summary string `json:"summary,omitempty"` + Auth string `json:"auth,omitempty"` + To []string `json:"to,omitempty"` + Bto []string `json:"bto,omitempty"` + Cc []string `json:"cc,omitempty"` + Published time.Time `json:"published,omitempty"` + Object ObjectBase `json:"object,omitempty"` +} + +type ObjectBase struct { + Type string `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Option []string `json:"option,omitempty"` + Alias string `json:"alias,omitempty"` + AttributedTo string `json:"attributedTo,omitempty"` + TripCode string `json:"tripcode,omitempty"` + Actor string `json:"actor,omitempty"` + Audience string `json:"audience,omitempty"` + ContentHTML template.HTML `json:"contenthtml,omitempty"` + Content string `json:"content,omitempty"` + EndTime string `json:"endTime,omitempty"` + Generator string `json:"generator,omitempty"` + Icon string `json:"icon,omitempty"` + Image string `json:"image,omitempty"` + InReplyTo []ObjectBase `json:"inReplyTo,omitempty"` + Location string `json:"location,omitempty"` + Preview *NestedObjectBase `json:"preview,omitempty"` + Published time.Time `json:"published,omitempty"` + Updated time.Time `json:"updated,omitempty"` + Object *NestedObjectBase `json:"object,omitempty"` + Attachment []ObjectBase `json:"attachment,omitempty"` + Replies CollectionBase `json:"replies,omitempty"` + StartTime string `json:"startTime,omitempty"` + Summary string `json:"summary,omitempty"` + Tag []ObjectBase `json:"tag,omitempty"` + Wallet []CryptoCur `json:"wallet,omitempty"` + Deleted string `json:"deleted,omitempty"` + Url []ObjectBase `json:"url,omitempty"` + Href string `json:"href,omitempty"` + To []string `json:"to,omitempty"` + Bto []string `json:"bto,omitempty"` + Cc []string `json:"cc,omitempty"` + Bcc string `json:"Bcc,omitempty"` + MediaType string `json:"mediatype,omitempty"` + Duration string `json:"duration,omitempty"` + Size int64 `json:"size,omitempty"` + Sensitive bool `json:"sensitive,omitempty"` + Sticky bool `json:"sticky,omitempty"` + Locked bool `json:"locked,omitempty"` +} + +type CryptoCur struct { + Type string `json:"type,omitempty"` + Address string `json:"address,omitempty"` +} + +type NestedObjectBase struct { + AtContext + Type string `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Alias string `json:"alias,omitempty"` + AttributedTo string `json:"attributedTo,omitempty"` + TripCode string `json:"tripcode,omitempty"` + Actor string `json:"actor,omitempty"` + Audience string `json:"audience,omitempty"` + ContentHTML template.HTML `json:"contenthtml,omitempty"` + Content string `json:"content,omitempty"` + EndTime string `json:"endTime,omitempty"` + Generator string `json:"generator,omitempty"` + Icon string `json:"icon,omitempty"` + Image string `json:"image,omitempty"` + InReplyTo []ObjectBase `json:"inReplyTo,omitempty"` + Location string `json:"location,omitempty"` + Preview ObjectBase `json:"preview,omitempty"` + Published time.Time `json:"published,omitempty"` + Attachment []ObjectBase `json:"attachment,omitempty"` + Replies *CollectionBase `json:"replies,omitempty"` + StartTime string `json:"startTime,omitempty"` + Summary string `json:"summary,omitempty"` + Tag []ObjectBase `json:"tag,omitempty"` + Updated time.Time `json:"updated,omitempty"` + Deleted string `json:"deleted,omitempty"` + Url []ObjectBase `json:"url,omitempty"` + Href string `json:"href,omitempty"` + To []string `json:"to,omitempty"` + Bto []string `json:"bto,omitempty"` + Cc []string `json:"cc,omitempty"` + Bcc string `json:"Bcc,omitempty"` + MediaType string `json:"mediatype,omitempty"` + Duration string `json:"duration,omitempty"` + Size int64 `json:"size,omitempty"` +} + +type CollectionBase struct { + Actor Actor `json:"actor,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + TotalItems int `json:"totalItems,omitempty"` + TotalImgs int `json:"totalImgs,omitempty"` + OrderedItems []ObjectBase `json:"orderedItems,omitempty"` + Items []ObjectBase `json:"items,omitempty"` +} + +type Collection struct { + AtContext + CollectionBase +} + +type ObjectBaseSortDesc []ObjectBase + +func (a ObjectBaseSortDesc) Len() int { return len(a) } +func (a ObjectBaseSortDesc) Less(i, j int) bool { return a[i].Updated.After(a[j].Updated) } +func (a ObjectBaseSortDesc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type ObjectBaseSortAsc []ObjectBase + +func (a ObjectBaseSortAsc) Len() int { return len(a) } +func (a ObjectBaseSortAsc) Less(i, j int) bool { return a[i].Published.Before(a[j].Published) } +func (a ObjectBaseSortAsc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } diff --git a/databaseschema.psql b/databaseschema.psql index f5671c2..d4b2616 100644 --- a/databaseschema.psql +++ b/databaseschema.psql @@ -244,4 +244,14 @@ instance varchar(100) primary key, timestamp TIMESTAMP default NOW() ); -ALTER TABLE boardaccess ADD COLUMN IF NOT EXISTS label varchar(50) default 'Anon'; \ No newline at end of file +ALTER TABLE boardaccess ADD COLUMN IF NOT EXISTS label varchar(50) default 'Anon'; + +CREATE TABLE IF NOT EXISTS sticky( +actor_id varchar(100), +activity_id varchar(100) +); + +CREATE TABLE IF NOT EXISTS locked( +actor_id varchar(100), +activity_id varchar(100) +); \ No newline at end of file diff --git a/main.go b/main.go index 837fec5..24d9753 100644 --- a/main.go +++ b/main.go @@ -89,6 +89,8 @@ func main() { app.All("/blacklist", routes.BoardBlacklist) app.All("/report", routes.ReportPost) app.Get("/make-report", routes.ReportGet) + app.Get("/sticky", routes.Sticky) + app.Get("/lock", routes.Lock) // Webfinger routes app.Get("/.well-known/webfinger", routes.Webfinger) diff --git a/route/routes/boardmgmt.go b/route/routes/boardmgmt.go index 5f24cdd..7ecc885 100644 --- a/route/routes/boardmgmt.go +++ b/route/routes/boardmgmt.go @@ -563,3 +563,99 @@ func ReportGet(ctx *fiber.Ctx) error { return ctx.Render("report", fiber.Map{"page": data}, "layouts/main") } + +func Sticky(ctx *fiber.Ctx) error { + id := ctx.Query("id") + board := ctx.Query("board") + + actor, _ := activitypub.GetActorByNameFromDB(board) + + _, auth := util.GetPasswordFromSession(ctx) + + if id == "" || auth == "" { + return util.MakeError(errors.New("no auth"), "Sticky") + } + + var obj = activitypub.ObjectBase{Id: id} + col, _ := obj.GetCollectionFromPath() + + if len(col.OrderedItems) < 1 { + if has, _ := util.HasAuth(auth, actor.Id); !has { + return util.MakeError(errors.New("no auth"), "Sticky") + } + + obj.MarkSticky(actor.Id) + + return ctx.Redirect("/"+board, http.StatusSeeOther) + } + + actor.Id = col.OrderedItems[0].Actor + + var OP string + if len(col.OrderedItems[0].InReplyTo) > 0 && col.OrderedItems[0].InReplyTo[0].Id != "" { + OP = col.OrderedItems[0].InReplyTo[0].Id + } else { + OP = id + } + + if has, _ := util.HasAuth(auth, actor.Id); !has { + return util.MakeError(errors.New("no auth"), "Sticky") + } + + obj.MarkSticky(actor.Id) + + var op = activitypub.ObjectBase{Id: OP} + if local, _ := op.IsLocal(); !local { + return ctx.Redirect("/"+board+"/"+util.RemoteShort(OP), http.StatusSeeOther) + } else { + return ctx.Redirect(OP, http.StatusSeeOther) + } +} + +func Lock(ctx *fiber.Ctx) error { + id := ctx.Query("id") + board := ctx.Query("board") + + actor, _ := activitypub.GetActorByNameFromDB(board) + + _, auth := util.GetPasswordFromSession(ctx) + + if id == "" || auth == "" { + return util.MakeError(errors.New("no auth"), "Lock") + } + + var obj = activitypub.ObjectBase{Id: id} + col, _ := obj.GetCollectionFromPath() + + if len(col.OrderedItems) < 1 { + if has, _ := util.HasAuth(auth, actor.Id); !has { + return util.MakeError(errors.New("no auth"), "Lock") + } + + obj.MarkLocked(actor.Id) + + return ctx.Redirect("/"+board, http.StatusSeeOther) + } + + actor.Id = col.OrderedItems[0].Actor + + var OP string + if len(col.OrderedItems[0].InReplyTo) > 0 && col.OrderedItems[0].InReplyTo[0].Id != "" { + OP = col.OrderedItems[0].InReplyTo[0].Id + } else { + OP = id + } + + if has, _ := util.HasAuth(auth, actor.Id); !has { + return util.MakeError(errors.New("no auth"), "Lock") + } + + obj.MarkLocked(actor.Id) + + var op = activitypub.ObjectBase{Id: OP} + if local, _ := op.IsLocal(); !local { + return ctx.Redirect("/"+board+"/"+util.RemoteShort(OP), http.StatusSeeOther) + } else { + return ctx.Redirect(OP, http.StatusSeeOther) + } +} diff --git a/route/util.go b/route/util.go index 09c5429..5a7d57c 100644 --- a/route/util.go +++ b/route/util.go @@ -139,6 +139,12 @@ func ParseOutboxRequest(ctx *fiber.Ctx, actor activitypub.Actor) error { nObj.Actor = config.Domain + "/" + actor.Name + if locked, _ := nObj.InReplyTo[0].IsLocked(); locked { + ctx.Response().Header.SetStatusCode(403) + _, err := ctx.Write([]byte("thread is locked")) + return util.MakeError(err, "ParseOutboxRequest") + } + nObj, err = nObj.Write() if err != nil { return util.MakeError(err, "ParseOutboxRequest") diff --git a/static/locked.png b/static/locked.png new file mode 100644 index 0000000..7792d16 Binary files /dev/null and b/static/locked.png differ diff --git a/static/pin.png b/static/pin.png new file mode 100644 index 0000000..6952601 Binary files /dev/null and b/static/pin.png differ diff --git a/views/catalog.html b/views/catalog.html index f19c489..4bd48aa 100644 --- a/views/catalog.html +++ b/views/catalog.html @@ -30,7 +30,7 @@ - {{ parseAttachment . true }} + {{ if .Sticky }}{{ end }}{{ if .Locked }}{{ end }}{{ parseAttachment . true }}