tavern/web/view_feed.go

288 lines
7.5 KiB
Go

package web
import (
"fmt"
"strings"
"time"
"github.com/gofrs/uuid"
"github.com/microcosm-cc/bluemonday"
"go.uber.org/zap"
"github.com/ngerakines/tavern/storage"
)
type viewFeed struct {
loggedInActor string
viewedThread uuid.UUID
logger *zap.Logger
feed []map[string]interface{}
foundActorIDs []string
foundObjectIDs []string
mediaURLs []viewFeedMedia
storage storage.Storage
refs map[string]storage.Payload
objectThreads map[string]uuid.UUID
}
type viewFeedMedia struct {
URL string
Blurhash string
}
func (v *viewFeed) populateObjectEventPayloads(objectEvents []storage.Payload) error {
if v.feed == nil {
v.feed = make([]map[string]interface{}, 0)
}
for _, objectEvent := range objectEvents {
objectEventType, _ := storage.JSONString(objectEvent, "type")
switch objectEventType {
case "Create":
object, _ := storage.JSONMap(objectEvent, "object")
name, view := v.createNoteViewFromPayload(object)
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,
})
case "Note":
name, view := v.createNoteViewFromPayload(objectEvent)
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,
})
case "Announce":
name, view := v.announceNoteView(objectEvent)
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,
})
case "Tombstone":
name, view := v.tombstoneView(objectEvent)
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:
v.logger.Debug("unexpected view feed object event type", zap.String("type", objectEventType))
}
}
return nil
}
func (v *viewFeed) tombstoneView(orig storage.Payload) (string, map[string]interface{}) {
view := make(map[string]interface{})
objectID, _ := storage.JSONString(orig, "id")
p, hasP := v.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.ParseInLocation("2006-01-02T15:04:05Z", deleted, time.UTC)
if err != nil {
deletedAt = time.Now()
}
view["deleted_at"] = deletedAt
}
return "tombstone", view
}
func (v *viewFeed) announceNoteView(p storage.Payload) (string, map[string]interface{}) {
announceNote := make(map[string]interface{})
objectID, _ := storage.JSONString(p, "object")
announceNote["object_id"] = objectID
v.foundObjectIDs = append(v.foundObjectIDs, objectID)
announcer, hasAnnouncer := storage.JSONString(p, "actor")
if hasAnnouncer {
announceNote["announcer"] = announcer
v.foundActorIDs = append(v.foundActorIDs, announcer)
}
object, hasObject := v.refs[objectID]
if hasObject {
_, noteView := v.createNoteViewFromPayload(object)
if noteView != nil {
announceNote["note"] = noteView
} else {
v.logger.Warn("could not create note for announced object", zap.String("object_id", objectID), zap.Reflect("object_event", p))
}
} else {
v.logger.Warn("announced object not found in feed", zap.String("object_id", objectID), zap.Reflect("object_event", p))
}
return "announce_note", announceNote
}
func (v *viewFeed) createNoteViewFromPayload(orig storage.Payload) (string, map[string]interface{}) {
createNote := make(map[string]interface{})
objectID, _ := storage.JSONString(orig, "id")
p, hasP := v.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.JSONString(p, "type")
if objectType == "Tombstone" {
return v.tombstoneView(orig)
}
createNote["object_id"] = objectID
v.foundObjectIDs = append(v.foundObjectIDs, objectID)
author, hasAuthor := storage.JSONString(p, "attributedTo")
content, hasContent := storage.JSONString(p, "content")
if !hasContent || !hasAuthor {
return "", nil
}
if author == v.loggedInActor {
createNote["can_delete"] = true
}
v.foundActorIDs = append(v.foundActorIDs, author)
policy := bluemonday.UGCPolicy()
createNote["author"] = author
createNote["content"] = policy.Sanitize(content)
createNote["link_reply"] = true
if inReplyTo, hasInReplyTo := storage.JSONString(p, "inReplyTo"); hasInReplyTo {
createNote["in_reply_to"] = inReplyTo
replyObj, hasReplyObj := v.refs[inReplyTo]
if hasReplyObj {
inReplyToAuthor, hasInReplyToAuthor := storage.JSONString(replyObj, "attributedTo")
if hasInReplyToAuthor {
createNote["in_reply_to_author"] = inReplyToAuthor
}
}
}
//
// if threadID, hasThreadID := v.objectThreads[objectID]; hasThreadID {
// createNote["thread_id"] = threadID
// if v.viewedThread != threadID {
// createNote["link_thread"] = true
// }
// }
if published, hasPublished := storage.JSONString(p, "published"); hasPublished {
var publishedAt time.Time
publishedAt, err := time.ParseInLocation("2006-01-02T15:04:05Z", published, time.UTC)
if err != nil {
publishedAt = time.Now()
}
createNote["published_at"] = publishedAt
}
destinations := storage.ActivityDestinations(p)
for _, destination := range destinations {
if destination == "https://www.w3.org/ns/activitystreams#Public" {
createNote["is_public"] = true
break
}
}
tags := make(map[string]string)
mentions := make(map[string]string)
objectTag, ok := storage.JSONMapList(p, "tag")
if ok {
for _, tag := range objectTag {
tagType, hasType := storage.JSONString(tag, "type")
href, hasHref := storage.JSONString(tag, "href")
name, hasName := storage.JSONString(tag, "name")
// Friendica doesn't include "name" attribute in user mentions.
if !hasType || !hasHref {
continue
}
switch tagType {
case "Hashtag":
if hasName {
tags[name] = href
}
case "Mention":
if hasName {
mentions[name] = href
}
v.foundActorIDs = append(v.foundActorIDs, href)
}
}
}
var media []string
attachments, ok := storage.JSONMapList(p, "attachment")
if ok {
for _, attachment := range attachments {
url, hasUrl := storage.JSONString(attachment, "url")
if hasUrl && strings.HasPrefix(url, "https://") {
blurHash, _ := storage.JSONString(attachment, "blurhash")
mediaType, _ := storage.JSONString(attachment, "mediaType")
switch mediaType {
case "image/png":
v.mediaURLs = append(v.mediaURLs, viewFeedMedia{
URL: url,
Blurhash: blurHash,
})
media = append(media, url)
case "image/jpeg":
v.mediaURLs = append(v.mediaURLs, viewFeedMedia{
URL: url,
Blurhash: blurHash,
})
media = append(media, url)
default:
fmt.Println("unsupported attachment", mediaType)
}
}
}
}
if len(tags) > 0 {
createNote["tags"] = tags
}
if len(mentions) > 0 {
createNote["mentions"] = mentions
}
if len(media) > 0 {
createNote["media"] = media
}
return "create_note", createNote
}