Implemented delete note. Closes #38.

This commit is contained in:
Nick Gerakines 2020-03-19 05:14:05 -04:00
parent 5eb7483da5
commit a641fe2cf8
No known key found for this signature in database
GPG Key ID: 33D43D854F96B2E4
12 changed files with 330 additions and 125 deletions

View File

@ -2,11 +2,13 @@ package common
import (
"fmt"
"github.com/gofrs/uuid"
)
func ActivityURL(domain string, activityID uuid.UUID) string {
func ActorURL(domain string, name interface{}) string {
return fmt.Sprintf("https://%s/users/%s", domain, name)
}
func ActivityURL(domain string, activityID interface{}) string {
return fmt.Sprintf("https://%s/activity/%s", domain, activityID)
}
@ -14,10 +16,10 @@ func ObjectURL(domain string, objectID interface{}) string {
return fmt.Sprintf("https://%s/object/%s", domain, objectID)
}
func ObjectRepliesURL(domain string, objectID uuid.UUID) string {
func ObjectRepliesURL(domain string, objectID interface{}) string {
return fmt.Sprintf("https://%s/object/%s/replies", domain, objectID)
}
func ObjectRepliesPageURL(domain string, objectID uuid.UUID, page int) string {
func ObjectRepliesPageURL(domain string, objectID interface{}, page int) string {
return fmt.Sprintf("https://%s/object/%s/replies?page=%d", domain, objectID, page)
}

View File

@ -55,6 +55,8 @@ type ObjectStorage interface {
ListObjectPayloadsInObjectReplies(ctx context.Context, objectID uuid.UUID, limit int, offset int) ([]Payload, error)
RecordObjectReply(ctx context.Context, objectID, parentObjectID uuid.UUID) (uuid.UUID, error)
RecordObjectReplyAll(ctx context.Context, rowID uuid.UUID, createdAt, updatedAt time.Time, objectID, parentObjectID uuid.UUID) (uuid.UUID, error)
UpdateObjectPayload(ctx context.Context, objectRowID uuid.UUID, payload Payload) error
}
func (s pgStorage) ListObjectPayloadsByObjectIDs(ctx context.Context, objectIDs []string) ([]Payload, error) {
@ -127,7 +129,7 @@ func (s pgStorage) CountObjectPayloadsInUserConversation(ctx context.Context, us
}
func (s pgStorage) ListObjectPayloadsInUserConversation(ctx context.Context, userID uuid.UUID, conversation string) ([]Payload, error) {
query := `SELECT o.payload FROM objects o INNER JOIN user_conversations uc ON uc.object_id = o.id WHERE uc.user_id = $1 AND uc.conversation = $2`
query := `SELECT o.payload FROM objects o INNER JOIN user_conversations uc ON uc.object_id = o.id WHERE uc.user_id = $1 AND uc.conversation = $2 ORDER BY uc.created_at ASC`
return s.objectPayloads(ctx, query, userID, conversation)
}
@ -319,3 +321,9 @@ func (s pgStorage) RecordObjectReplyAll(ctx context.Context, rowID uuid.UUID, cr
err := s.db.QueryRowContext(ctx, query, rowID, createdAt, updatedAt, objectID, parentObjectID).Scan(&id)
return id, errors.WrapInsertQueryFailedError(err)
}
func (s pgStorage) UpdateObjectPayload(ctx context.Context, objectRowID uuid.UUID, payload Payload) error {
now := s.now()
_, err := s.db.ExecContext(ctx, "UPDATE objects SET payload = $3, updated_at = $2 WHERE id = $1", objectRowID, now, payload)
return errors.WrapUpdateQueryFailedError(err)
}

View File

@ -16,6 +16,21 @@
newForm.hide().appendTo("body").submit();
});
{{ if and .feed_view (eq .feed_view "mine") }}
$('body').on('click', 'a.owner_delete', function () {
let newForm = jQuery('<form>', {
'action': '/notes/delete',
'method': 'post',
'target': '_top'
}).append(jQuery('<input>', {
'name': 'object_id',
'value': $(this).data('object'),
'type': 'hidden'
}));
newForm.hide().appendTo("body").submit();
});
{{ end }}
{{ if .latest }}
var latest = "{{ .latest }}";

View File

@ -32,6 +32,10 @@
View Conversation
</a>
{{ end }}
{{- if $note.can_delete -}}
<a href="#" class="dropdown-item owner_delete"
data-object="{{ $note.object_id }}">Delete</a>
{{- end -}}
</div>
</div>
<p>

View File

@ -5,13 +5,16 @@
{{ if .feed }}
<ul class="list-unstyled activity-feed">
{{ range $item := .feed }}
{{ if or $item.create_note $item.announce_note }}
{{ if or $item.create_note $item.announce_note $item.tombstone }}
{{ if $item.create_note }}
{{template "activity_create_note" (dict "item" $item "actors" $actors "announcements" $announcements "media" $media) }}
{{ end }}
{{ if $item.announce_note }}
{{template "activity_announce_note" (dict "item" $item "actors" $actors "announcements" $announcements "media" $media) }}
{{ end }}
{{ if $item.tombstone }}
{{template "activity_tombstone" (dict "item" $item) }}
{{ end }}
{{ else }}
<p class="text-danger">Unexpected item</p>
{{ end }}

View File

@ -0,0 +1,22 @@
{{ define "activity_tombstone" }}
{{ with $item := .item }}
{{ with $tombstone := $item.tombstone }}
<li class="media mb-3 border-top border-secondary pt-2">
<div class="media-body">
<h3 class="text-danger">Object Removed</h3>
<ul class="text-muted">
{{ with $tombstone.object_id }}
<li>{{ . }}</li>
{{ end }}
{{ with $tombstone.deleted_at}}
<li>Deleted {{ date . }}</li>
{{ end }}
{{ with $tombstone.former_type }}
<li>Former Type "{{ . }}"</li>
{{ end }}
</ul>
</div>
</li>
{{ end }}
{{ end }}
{{ end }}

View File

@ -157,6 +157,7 @@ func serverCommandAction(cliCtx *cli.Context) error {
"partials/activity_feed",
"partials/activity_create_note",
"partials/activity_announce_note",
"partials/activity_tombstone",
},
Funcs: template.FuncMap{
"date": tmplDate,
@ -320,6 +321,7 @@ func serverCommandAction(cliCtx *cli.Context) error {
authenticated.GET("/compose", h.compose)
authenticated.POST("/compose/create/note", h.createNote)
authenticated.POST("/dashboard/notes/announce/note", h.announceNote)
authenticated.POST("/notes/delete", h.deleteNote)
authenticated.GET("/configure", h.configure)
authenticated.POST("/configure/user", h.saveUserSettings)

View File

@ -86,6 +86,8 @@ func (h handler) actorInbox(c *gin.Context) {
h.actorInboxCreate(c, user, payload)
case "Announce":
h.actorInboxAnnounce(c, user, payload)
case "Delete":
h.actorInboxDelete(c, user, payload)
default:
h.logger.Warn("User received unexpected payload type", zap.String("type", payloadType), zap.String("user", name))
c.Status(http.StatusOK)
@ -534,6 +536,73 @@ func (h handler) actorInboxAnnounce(c *gin.Context, user *storage.User, payload
c.Status(http.StatusOK)
}
func (h handler) actorInboxDelete(c *gin.Context, user *storage.User, payload storage.Payload) {
err := h.verifySignature(c)
if err != nil {
h.unauthorizedJSON(c, err)
return
}
ctx := c.Request.Context()
var objectID string
var objectRowID uuid.UUID
foundObjectID := false
objectIDs := storage.CollectJSONDeepStrings(payload, []string{"object"}, []string{"object", "id"})
for _, objectID = range objectIDs {
objectRowID, err = h.storage.ObjectRowIDForObjectID(ctx, objectID)
if err == nil {
foundObjectID = true
break
}
}
if !foundObjectID {
h.logger.Warn("unable to process delete request",
zap.String("user", user.Name),
)
c.Status(http.StatusOK)
return
}
activityURL, _ := storage.JSONString(payload, "id")
var tombstone storage.Payload
if object, hasObject := storage.JSONMap(payload, "object"); hasObject {
tombstone = object
}
if tombstone == nil {
// TODO: Replace this with a fetch of the object to get a proper tombstone from the remote instance.
tombstone = storage.EmptyPayload()
tombstone["type"] = "Tombstone"
// TODO: Ensure that the object is, in fact, a note.
tombstone["formerType"] = "Note"
tombstone["id"] = objectID
now := time.Now()
deletedAt := now.Format("2006-01-02T15:04:05Z")
tombstone["deleted"] = deletedAt
}
txErr := storage.TransactionalStorage(ctx, h.storage, func(storage storage.Storage) error {
err = storage.UpdateObjectPayload(ctx, objectRowID, tombstone)
if err != nil {
return err
}
_, err = storage.RecordObjectEvent(ctx, activityURL, objectRowID, payload)
if err != nil {
return err
}
return nil
})
if txErr != nil {
h.internalServerErrorJSON(c, txErr)
return
}
c.Status(http.StatusOK)
}
func skipActorInbox(j storage.Payload) bool {
t, _ := storage.JSONString(j, "type")
a, _ := storage.JSONString(j, "actor")

View File

@ -251,7 +251,7 @@ func (h handler) createNote(c *gin.Context) {
}
note["cc"] = cc
note["type"] = "Note"
note["url"] = activityURL
note["url"] = noteURL
replies := storage.EmptyPayload()
replies["id"] = common.ObjectRepliesURL(h.domain, createNoteID)
@ -452,7 +452,7 @@ func (h handler) createNote(c *gin.Context) {
}
err = nc.SendToInbox(ctx, localActor, foundActor, payload)
if err != nil {
h.logger.Error("failed sending to mentioned actor", zap.String("target", foundActor.GetID()), zap.String("activity", activityURL))
h.logger.Error("failed sending to actor", zap.String("target", foundActor.GetID()), zap.String("activity", activityURL))
}
}
}
@ -466,7 +466,7 @@ func (h handler) createNote(c *gin.Context) {
}
err = nc.SendToInbox(ctx, localActor, foundActor, payload)
if err != nil {
h.logger.Error("failed sending to mentioned actor", zap.String("target", foundActor.GetID()), zap.String("activity", activityURL))
h.logger.Error("failed sending to actor", zap.String("target", foundActor.GetID()), zap.String("activity", activityURL))
}
}
}
@ -691,6 +691,116 @@ func (h handler) uploadHash(ctx context.Context, file *multipart.FileHeader) (st
return h.storage.CreateImage(ctx, fullLocation, checksum, blurHash, int(written), contentType, bounds.Max.Y, bounds.Max.X, []string{})
}
func (h handler) deleteNote(c *gin.Context) {
_, user, _, cont := h.loggedIn(c)
if !cont {
return
}
ctx := c.Request.Context()
userActor, err := h.storage.GetActor(ctx, user.ActorID)
if err != nil {
h.hardFail(c, err)
return
}
objectID := c.PostForm("object_id")
now := time.Now()
deletedAt := now.Format("2006-01-02T15:04:05Z")
tombstone := storage.EmptyPayload()
tombstone["type"] = "Tombstone"
// TODO: Ensure that the object is, in fact, a note.
tombstone["formerType"] = "Note"
tombstone["id"] = objectID
tombstone["deleted"] = deletedAt
activityID := storage.NewV4()
activityURL := common.ActivityURL(h.domain, activityID)
deleteActivity := storage.EmptyPayload()
deleteActivity["@context"] = "https://www.w3.org/ns/activitystreams"
deleteActivity["id"] = activityURL
deleteActivity["type"] = "Delete"
deleteActivity["actor"] = common.ActorURL(h.domain, user.Name)
deleteActivity["to"] = []string{
"https://www.w3.org/ns/activitystreams#Public",
}
deleteActivity["object"] = tombstone
payload := deleteActivity.Bytes()
txErr := storage.TransactionalStorage(ctx, h.storage, func(storage storage.Storage) error {
objectRowID, err := storage.ObjectRowIDForObjectID(ctx, objectID)
if err != nil {
return err
}
// TODO: Pass in the `Create` activity ID and verify the user, activity, and object all match.
count, err := storage.RowCount(ctx, `SELECT COUNT(*) FROM user_object_events WHERE user_id = $1 AND object_id = $2`, user.ID, objectRowID)
if err != nil {
return err
}
if count == 0 {
return errors.NewNotFoundError(nil)
}
if count > 1 {
return fmt.Errorf("more than one activity found for object")
}
err = storage.UpdateObjectPayload(ctx, objectRowID, tombstone)
if err != nil {
return err
}
_, err = storage.RecordObjectEvent(ctx, activityURL, objectRowID, deleteActivity)
if err != nil {
return err
}
return nil
})
if txErr != nil {
h.internalServerErrorJSON(c, txErr)
return
}
followerTotal, err := h.storage.RowCount(ctx, `SELECT COUNT(*) FROM network_graph WHERE user_id = $1`, user.ID)
if err != nil {
h.internalServerErrorJSON(c, err)
return
}
destinations, err := h.storage.ListAcceptedFollowers(ctx, user.ID, followerTotal, 0)
if err != nil {
h.internalServerErrorJSON(c, err)
return
}
destinations = uniqueTo(destinations, common.ActivityURL(h.domain, user.Name))
nc := fed.ActorClient{
HTTPClient: h.httpClient,
Logger: h.logger,
}
localActor := h.userActor(user, userActor)
for _, dest := range destinations {
foundActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, h.httpClient, dest)
if err != nil {
h.logger.Error("unable to get or fetch actor", zap.Error(err), zap.String("actor", dest))
continue
}
err = nc.SendToInbox(ctx, localActor, foundActor, payload)
if err != nil {
h.logger.Error("failed sending to actor", zap.String("target", foundActor.GetID()), zap.String("activity", activityURL))
}
}
c.Redirect(http.StatusFound, h.url("feed_mine"))
}
func imageBounds(tempFileLocation string) (image.Rectangle, string, string, error) {
file, err := os.Open(tempFileLocation)
if err != nil {

View File

@ -265,7 +265,7 @@ func (h handler) viewMyFeed(c *gin.Context) {
}
}
vf := &viewFeed{storage: h.storage, logger: h.logger}
vf := &viewFeed{storage: h.storage, logger: h.logger, loggedInActor: common.ActorURL(h.domain, user.Name)}
err = vf.populateObjectEventPayloads(objects, objectsByObjectID)
if err != nil {
h.hardFail(c, err)
@ -317,100 +317,6 @@ func (h handler) viewMyFeed(c *gin.Context) {
data["feed_view"] = "mine"
data["feed_link"] = "feed_mine"
//
// userActorID := storage.NewActorID(user.Name, h.domain)
//
// page := intParam(c, "page", 1)
// limit := 20
//
// total, err := h.storage.RowCount(ctx, `SELECT COUNT(*) FROM user_activities WHERE user_id = $1`, user.ID)
// if err != nil {
// h.hardFail(c, err)
// return
// }
//
// uf, err := h.storage.PaginateUserActivity(ctx, user.ID, limit, (page-1)*limit)
// if err != nil {
// h.hardFail(c, err)
// return
// }
//
// pageCount := math.Ceil(float64(total) / float64(limit))
// data["page_count"] = int(pageCount)
// data["page"] = page
// data["limit"] = limit
//
// var objectEventIDs []uuid.UUID
// for _, ufi := range uf {
// objectEventIDs = append(objectEventIDs, ufi.ObjectEventID)
// }
//
// pairs, err := h.storage.ObjectPairsByObjectEventRowIDs(ctx, objectEventIDs)
// if err != nil {
// h.hardFail(c, err)
// return
// }
//
// pairsMap := make(map[uuid.UUID]storage.ObjectEventPair)
// actorIDs := make([]string, 0)
// objectIDs := make([]string, 0)
// for _, pair := range pairs {
// pairsMap[pair.ObjectEventRowID] = pair
// actorIDs = append(actorIDs, pair.ActorID)
// objectIDs = append(objectIDs, pair.ObjectID)
// }
//
// announcements, err := h.storage.UserAnnouncementsByObject(ctx, string(userActorID), objectIDs)
// if err != nil {
// h.hardFail(c, err)
// return
// }
// data["announcements"] = announcements
//
// vf := &viewFeed{storage: h.storage}
// err = vf.populate(uf, pairsMap)
// if err != nil {
// h.hardFail(c, err)
// return
// }
//
// data["feed"] = vf.feed
// if len(uf) > 0 {
// data["latest"] = uf[0].CreatedAt.UTC().Unix()
// }
//
// actors, err := h.storage.ActorsByActorID(ctx, append(actorIDs, vf.actorIDs...))
// if err != nil {
// h.hardFail(c, err)
// return
// }
// var actorRowIDs []uuid.UUID
// for _, actor := range actors {
// actorRowIDs = append(actorRowIDs, actor.ID)
// }
// actorSubjects, err := h.storage.ActorSubjects(ctx, actorRowIDs)
// if err != nil {
// h.hardFail(c, err)
// return
// }
// allActors := h.gatherActors(actors, actorSubjects)
//
// var pages []int
// for i := page - 3; i <= page+3; i++ {
// if i > 0 && i <= int(pageCount) {
// pages = append(pages, i)
// }
// }
// data["pages"] = pages
// data["actors"] = actorLookup{h.domain, allActors}
//
// ml := &mediaLookup{h.domain, make(map[string]string)}
// err = ml.load(ctx, h.storage, vf.mediaURLs)
// if err != nil {
// h.hardFail(c, err)
// }
//
// data["media"] = ml
if cont = h.saveSession(c, session); !cont {
return
@ -438,7 +344,7 @@ func (h handler) viewConversation(c *gin.Context) {
conversations := make([]string, 0)
var found []string
for _, object := range objects {
found = storage.CollectJSONDeepStrings(object, []string{"object"}, []string{"object", "id"}, []string{"object", "inReplyTo"})
found = storage.CollectJSONDeepStrings(object, []string{"id"}, []string{"object"}, []string{"object", "id"}, []string{"object", "inReplyTo"})
objectIDs = append(objectIDs, found...)
found = storage.CollectJSONDeepStrings(object, []string{"actor"}, []string{"object", "attributedTo"})

View File

@ -33,6 +33,12 @@ func (h handler) getObject(c *gin.Context) {
return
}
objectType, hasObjectType := storage.JSONString(objectPayload, "type")
if hasObjectType && objectType == "Tombstone" {
h.writeJSONLD(c, http.StatusOK, objectPayload)
return
}
var destinations []string
to, hasTo := storage.JSONStrings(objectPayload, "to")
if hasTo {

View File

@ -12,12 +12,13 @@ import (
)
type viewFeed struct {
logger *zap.Logger
feed []map[string]interface{}
actorIDs []string
objectIDs []string
mediaURLs []viewFeedMedia
storage storage.Storage
loggedInActor string
logger *zap.Logger
feed []map[string]interface{}
actorIDs []string
objectIDs []string
mediaURLs []viewFeedMedia
storage storage.Storage
}
type viewFeedMedia struct {
@ -35,31 +36,41 @@ func (v *viewFeed) populateObjectEventPayloads(objectEvents []storage.Payload, r
switch objectEventType {
case "Create":
object, _ := storage.JSONMap(objectEvent, "object")
view := v.createNoteViewFromPayload(object, refs)
name, view := v.createNoteViewFromPayload(object, refs)
if view == nil {
v.logger.Warn("Could not create view from object event", zap.Reflect("object_event", objectEvent))
continue
}
v.feed = append(v.feed, map[string]interface{}{
"create_note": view,
name: view,
})
case "Note":
view := v.createNoteViewFromPayload(objectEvent, refs)
name, view := v.createNoteViewFromPayload(objectEvent, refs)
if view == nil {
v.logger.Warn("Could not create view from object event", zap.Reflect("object_event", objectEvent))
continue
}
v.feed = append(v.feed, map[string]interface{}{
"create_note": view,
name: view,
})
case "Announce":
view := v.announceNoteView(objectEvent, refs)
name, view := v.announceNoteView(objectEvent, refs)
if view == nil {
v.logger.Warn("Could not create view from object event", zap.Reflect("object_event", objectEvent))
continue
}
v.feed = append(v.feed, map[string]interface{}{
"announce_note": view,
name: view,
})
case "Tombstone":
name, view := v.tombstoneView(objectEvent, refs)
if view == nil {
v.logger.Warn("Could not create view from object event", zap.Reflect("object_event", objectEvent))
continue
}
v.feed = append(v.feed, map[string]interface{}{
name: view,
})
default:
@ -70,7 +81,36 @@ func (v *viewFeed) populateObjectEventPayloads(objectEvents []storage.Payload, r
return nil
}
func (v *viewFeed) announceNoteView(p storage.Payload, refs map[string]storage.Payload) map[string]interface{} {
func (v *viewFeed) tombstoneView(orig storage.Payload, refs map[string]storage.Payload) (string, map[string]interface{}) {
view := make(map[string]interface{})
objectID, _ := storage.JSONString(orig, "id")
p, hasP := refs[objectID]
if !hasP {
v.logger.Warn("refs does not contain object id", zap.String("object_id", objectID))
return "", nil
}
view["object_id"] = objectID
view["tombstone"] = true
view["former_type"], _ = storage.JSONString(p, "formerType")
if deleted, hasDeleted := storage.JSONString(p, "deleted"); hasDeleted {
var deletedAt time.Time
deletedAt, err := time.Parse("2006-01-02T15:04:05Z", deleted)
if err != nil {
deletedAt = time.Now()
}
view["deleted_at"] = deletedAt
}
return "tombstone", view
}
func (v *viewFeed) announceNoteView(p storage.Payload, refs map[string]storage.Payload) (string, map[string]interface{}) {
announceNote := make(map[string]interface{})
objectID, _ := storage.JSONString(p, "object")
@ -85,7 +125,7 @@ func (v *viewFeed) announceNoteView(p storage.Payload, refs map[string]storage.P
object, hasObject := refs[objectID]
if hasObject {
noteView := v.createNoteViewFromPayload(object, refs)
_, noteView := v.createNoteViewFromPayload(object, refs)
if noteView != nil {
announceNote["note"] = noteView
} else {
@ -95,13 +135,26 @@ func (v *viewFeed) announceNoteView(p storage.Payload, refs map[string]storage.P
v.logger.Warn("announced object not found in feed", zap.String("object_id", objectID), zap.Reflect("object_event", p))
}
return announceNote
return "announce_note", announceNote
}
func (v *viewFeed) createNoteViewFromPayload(p storage.Payload, refs map[string]storage.Payload) map[string]interface{} {
func (v *viewFeed) createNoteViewFromPayload(orig storage.Payload, refs map[string]storage.Payload) (string, map[string]interface{}) {
createNote := make(map[string]interface{})
objectID, _ := storage.JSONString(p, "id")
objectID, _ := storage.JSONString(orig, "id")
p, hasP := refs[objectID]
if !hasP {
v.logger.Warn("refs does not contain object id", zap.String("object_id", objectID))
return "", nil
}
createNote["object_id"] = objectID
objectType, _ := storage.JSONDeepString(p, "type")
if objectType == "Tombstone" {
return v.tombstoneView(orig, refs)
}
createNote["object_id"] = objectID
v.objectIDs = append(v.objectIDs, objectID)
@ -109,7 +162,12 @@ func (v *viewFeed) createNoteViewFromPayload(p storage.Payload, refs map[string]
content, hasContent := storage.JSONString(p, "content")
if !hasContent || !hasAuthor {
return nil
return "", nil
}
if author == v.loggedInActor {
fmt.Println(author, v.loggedInActor, author == v.loggedInActor)
createNote["can_delete"] = true
}
v.actorIDs = append(v.actorIDs, author)
@ -212,5 +270,5 @@ func (v *viewFeed) createNoteViewFromPayload(p storage.Payload, refs map[string]
createNote["media"] = media
}
return createNote
return "create_note", createNote
}