tavern/web/handler_feed.go

509 lines
15 KiB
Go

package web
import (
"encoding/hex"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
"go.uber.org/zap"
"github.com/ngerakines/tavern/storage"
)
func (h handler) saveSession(c *gin.Context, session sessions.Session) bool {
if err := session.Save(); err != nil {
h.hardFail(c, err)
return false
}
return true
}
type feedObjectSource func(user *storage.User) (int, []storage.Payload, error)
func (h handler) displayObjectFeed(c *gin.Context, requireUser bool, vars map[string]interface{}, feedObjectSource feedObjectSource) {
data, user, session, cont := h.loggedIn(c, requireUser)
if !cont {
return
}
ctx := c.Request.Context()
for k, v := range vars {
data[k] = v
}
var err error
var localUserActor *storage.Actor
if user != nil {
localUserActor, err = h.storage.GetActor(c.Request.Context(), user.ActorID)
if err != nil {
h.hardFail(c, err)
return
}
}
page := intParam(c, "page", 1)
limit := 20
total, feedPayloads, err := feedObjectSource(user)
if err != nil {
h.hardFail(c, err)
return
}
if total == 0 {
if cont = h.saveSession(c, session); !cont {
return
}
c.HTML(http.StatusOK, "objects", data)
return
}
data["total"] = total
data["objects"] = feedPayloads
foundPayloadIDs := make([]string, 0)
foundActorIDs := make([]string, 0)
for _, feedPayload := range feedPayloads {
foundPayloadIDs = append(foundPayloadIDs,
storage.CollectJSONDeepStrings(feedPayload,
[]string{"id"},
[]string{"inReplyTo"},
[]string{"object"},
[]string{"object", "id"},
[]string{"object", "inReplyTo"})...)
foundActorIDs = append(foundActorIDs,
storage.CollectJSONDeepStrings(feedPayload, []string{"actor"}, []string{"object", "attributedTo"}, []string{"attributedTo"})...)
}
data["found_payload_ids"] = foundPayloadIDs
data["found_actor_ids"] = foundActorIDs
actorNames := make(map[string]string)
actorIcons := make(map[string]string)
// objectIDToObjectRowID := make(map[string]uuid.UUID)
objectRowIDs, err := h.storage.ObjectRowIDsForObjectIDs(ctx, foundPayloadIDs)
if err != nil {
h.hardFail(c, err)
return
}
data["collected_object_row_ids"] = objectRowIDs
collectedRowIDs := make([]uuid.UUID, 0, len(objectRowIDs))
for _, id := range objectRowIDs {
collectedRowIDs = append(collectedRowIDs, id)
}
refs, err := h.storage.ObjectPayloadByUUID(ctx, collectedRowIDs)
if err != nil {
h.hardFail(c, err)
return
}
h.logger.Debug("queried object refs", zap.Int("count", len(refs)))
boosts, err := h.storage.CountObjectBoostsByObjectIDs(ctx, collectedRowIDs)
if err != nil {
h.hardFail(c, err)
return
}
mappedBoosts := make(map[string]int)
for _, boost := range boosts {
mappedBoosts[boost.Key] = boost.Count
}
mappedUserBoosts := make(map[string]int)
if localUserActor != nil {
userBoosts, err := h.storage.CountObjectBoostsByActorObjectIDs(ctx, localUserActor.ID, collectedRowIDs)
if err != nil {
h.hardFail(c, err)
return
}
for _, userBoost := range userBoosts {
mappedUserBoosts[userBoost.Key] = userBoost.Count
}
}
replies, err := h.storage.CountObjectRepliesByObjectIDs(ctx, collectedRowIDs)
if err != nil {
h.hardFail(c, err)
return
}
mappedReplies := make(map[string]int)
for _, reply := range replies {
mappedReplies[reply.Key] = reply.Count
}
for _, ref := range refs {
foundActorIDs = append(foundActorIDs,
storage.CollectJSONDeepStrings(ref, []string{"actor"}, []string{"attributedTo"}, []string{"object", "attributedTo"})...)
}
actors, err := h.storage.ActorsByActorID(ctx, foundActorIDs)
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)
for actorID, actorSummary := range allActors {
actorNames[actorID] = actorSummary["at"]
actorIcons[actorID] = actorSummary["icon"]
}
mediaPrefix := fmt.Sprintf("https://%s/asset/image/", h.domain)
feed := make([]*viewObject, 0)
for _, feedPayload := range feedPayloads {
payloadID, hasPayloadID := storage.JSONString(feedPayload, "id")
if !hasPayloadID {
h.logger.Warn("payload missing id", zap.Reflect("payload", feedPayload))
continue
}
payloadType, hasPayloadType := storage.JSONString(feedPayload, "type")
if !hasPayloadType {
h.logger.Warn("payload missing type", zap.Reflect("payload", feedPayload))
continue
}
objectID := storage.FirstJSONDeepStrings(feedPayload, []string{"object"}, []string{"object", "id"}, []string{"id"})
if len(objectID) == 0 {
h.logger.Warn("payload missing object id", zap.Reflect("payload", feedPayload))
continue
}
objectRowID, hasObjectRowID := objectRowIDs[objectID]
if !hasObjectRowID {
h.logger.Warn("no object row id for object id", zap.String("object_id", objectID))
continue
}
objectPayload, hasObjectPayload := refs[objectRowID]
if !hasObjectPayload {
h.logger.Warn("no ref for object row id", zap.String("object_row_id", objectRowID.String()))
continue
}
objectType, hasObjectType := storage.JSONString(objectPayload, "type")
if !hasObjectType {
h.logger.Warn("payload missing type", zap.Reflect("object_payload", objectPayload))
continue
}
element := createViewObject(objectRowID, objectPayload, nil)
element.applyView(func(viewObjectRowID uuid.UUID, payload storage.Payload, viewContext *viewObjectContext) *viewObjectContext {
if payloadType == "Announce" && objectType == "Note" {
viewContext.HasAnnouncer = true
viewContext.AnnounceActivityID = payloadID
announcer, hasAnnouncer := storage.JSONString(feedPayload, "actor")
if hasAnnouncer {
viewContext.Announcer = announcer
viewContext.AnnouncerLink = announcer
if actorName, foundActorName := actorNames[announcer]; foundActorName {
viewContext.Announcer = actorName
}
}
}
objectID, hasObjectID := storage.JSONString(objectPayload, "id")
if !hasObjectID {
return &viewObjectContext{Error: "Object has no id."}
}
viewContext.ObjectID = objectID
if objectType == "Tombstone" {
viewContext.Tombstone = true
viewContext.TombstoneFormerType, _ = storage.JSONString(payload, "formerType")
if deleted, hasDeleted := storage.JSONString(payload, "deleted"); hasDeleted {
var deletedAt time.Time
deletedAt, err := time.ParseInLocation("2006-01-02T15:04:05Z", deleted, time.UTC)
if err == nil {
viewContext.TombstoneDeletedAt = deletedAt
}
}
return viewContext
}
attributedTo, hasAttributedTo := storage.JSONString(objectPayload, "attributedTo")
content, hasContent := storage.JSONString(objectPayload, "content")
if !hasAttributedTo {
return &viewObjectContext{Error: "Object has no attribution."}
}
if !hasContent {
return &viewObjectContext{Error: "Object has no content."}
}
viewContext.Icon = fmt.Sprintf("https://%s/avatar/png/unknown/unknown?size=60", h.domain)
if actorIcon, hasActorIcon := actorIcons[attributedTo]; hasActorIcon {
viewContext.Icon = actorIcon
}
destinations := storage.ActivityDestinations(objectPayload)
for _, destination := range destinations {
if destination == "https://www.w3.org/ns/activitystreams#Public" {
viewContext.Public = true
break
}
}
viewContext.Content = content
viewContext.AttributedTo = attributedTo
viewContext.AttributedToLink = attributedTo
if actorName, foundActorName := actorNames[attributedTo]; foundActorName {
viewContext.AttributedTo = actorName
}
if objectURL, hasObjectURL := storage.JSONString(objectPayload, "url"); hasObjectURL {
viewContext.LinkView = true
viewContext.LinkViewURL = objectURL
}
viewContext.LinkReply = true
if viewContext.LinkReply {
viewContext.LinkReplyURL = fmt.Sprintf("%s?inReplyTo=%s", h.url("compose"), url.QueryEscape(objectID))
}
viewContext.LinkAnnounce = true
if localUserActor != nil && attributedTo == localUserActor.ActorID {
viewContext.LinkDelete = true
}
if published, hasPublished := storage.JSONString(objectPayload, "published"); hasPublished {
var publishedAt time.Time
publishedAt, err := time.ParseInLocation("2006-01-02T15:04:05Z", published, time.UTC)
if err == nil {
viewContext.HasPublishedAt = true
viewContext.PublishedAt = publishedAt
}
}
if inReplyTo, hasInReplyTo := storage.JSONString(objectPayload, "inReplyTo"); hasInReplyTo {
viewContext.HasParent = true
viewContext.ParentObjectLink = inReplyTo
if parentObjectRowID, hasParentObjectRowID := objectRowIDs[inReplyTo]; hasParentObjectRowID {
if parentRef, hasParentRef := refs[parentObjectRowID]; hasParentRef {
inReplyToAuthor, hasInReplyToAuthor := storage.JSONString(parentRef, "attributedTo")
if hasInReplyToAuthor {
viewContext.ParentAttributedToLink = inReplyToAuthor
viewContext.ParentAttributedTo = inReplyToAuthor
if actorName, foundActorName := actorNames[inReplyToAuthor]; foundActorName {
viewContext.ParentAttributedTo = actorName
}
}
}
}
}
objectTag, ok := storage.JSONMapList(objectPayload, "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")
if hasType && tagType == "Hashtag" && hasName {
viewContext.Tags = append(viewContext.Tags, name)
}
if hasType && tagType == "Mention" && hasHref {
viewContext.Mentions = append(viewContext.Mentions, href)
}
}
}
attachments, ok := storage.JSONMapList(objectPayload, "attachment")
if ok {
for _, attachment := range attachments {
mediaURL, hasMediaURL := storage.JSONString(attachment, "url")
if hasMediaURL && strings.HasPrefix(mediaURL, "https://") {
blurHash, hasBlurHash := storage.JSONString(attachment, "blurhash")
mediaType, _ := storage.JSONString(attachment, "mediaType")
switch mediaType {
case "image/png", "image/jpeg":
if strings.HasPrefix(mediaURL, mediaPrefix) {
viewContext.Media = append(viewContext.Media, viewContentAttachment{
URL: mediaURL,
Thumbnail: fmt.Sprintf("https://%s/asset/blurhash/%s", h.domain, hex.EncodeToString([]byte(blurHash))),
})
} else if hasBlurHash {
viewContext.Media = append(viewContext.Media, viewContentAttachment{
URL: mediaURL,
Thumbnail: fmt.Sprintf("https://%s/asset/blurhash/%s", h.domain, hex.EncodeToString([]byte(blurHash))),
})
} else {
viewContext.Media = append(viewContext.Media, viewContentAttachment{
URL: mediaURL,
Thumbnail: mediaURL,
})
}
}
}
}
}
if count, hasCount := mappedUserBoosts[viewObjectRowID.String()]; hasCount && count > 0 {
viewContext.ViewerAnnounced = true
}
if count, hasCount := mappedBoosts[viewObjectRowID.String()]; hasCount {
viewContext.TotalAnnounces = count
}
if count, hasCount := mappedReplies[viewObjectRowID.String()]; hasCount {
viewContext.TotalReplies = count
}
return viewContext
})
feed = append(feed, element)
}
data["feed"] = feed
selfURL, hasSelfURL := storage.JSONString(vars, "self_url")
if hasSelfURL {
paged, err := createPaged(limit, page, total, selfURL)
if err == nil {
data["paged"] = paged
} else {
h.logger.Error("error creating page", zap.Error(err), zap.String("url", selfURL))
}
}
feedTemplate, hasFeedTemplate := storage.JSONString(vars, "feed_template")
if !hasFeedTemplate {
feedTemplate = "objects"
}
if cont = h.saveSession(c, session); !cont {
return
}
c.HTML(http.StatusOK, feedTemplate, data)
}
func (h handler) viewFeed(c *gin.Context) {
meta := map[string]interface{}{
"feed_view": "recent",
"self_url": h.url("feed_recent"),
}
page := intParam(c, "page", 1)
limit := 20
h.displayObjectFeed(c, true, meta, func(user *storage.User) (i int, payloads []storage.Payload, err error) {
total, err := h.storage.CountObjectEventPayloadsInFeed(c.Request.Context(), user.ID)
if err != nil {
return 0, nil, err
}
h.logger.Debug("counted objects in feed", zap.Int("total", total))
objects, err := h.storage.ListObjectEventPayloadsInFeed(c.Request.Context(), user.ID, limit, (page-1)*limit)
if err != nil {
return 0, nil, err
}
h.logger.Debug("queried objects", zap.Int("count", len(objects)))
return total, objects, nil
})
}
func (h handler) viewMyFeed(c *gin.Context) {
meta := map[string]interface{}{
"feed_view": "mine",
"self_url": h.url("feed_mine"),
}
page := intParam(c, "page", 1)
limit := 20
h.displayObjectFeed(c, true, meta, func(user *storage.User) (i int, payloads []storage.Payload, err error) {
total, err := h.storage.CountObjectEventPayloadsInUserFeed(c.Request.Context(), user.ID)
if err != nil {
return 0, nil, err
}
h.logger.Debug("counted objects in feed", zap.Int("total", total))
objects, err := h.storage.ListObjectEventPayloadsInUserFeed(c.Request.Context(), user.ID, limit, (page-1)*limit)
if err != nil {
return 0, nil, err
}
h.logger.Debug("queried objects", zap.Int("count", len(objects)))
return total, objects, nil
})
}
func (h handler) viewLocalFeed(c *gin.Context) {
meta := map[string]interface{}{
"feed_view": "local",
"self_url": h.url("feed_local"),
}
page := intParam(c, "page", 1)
limit := 20
h.displayObjectFeed(c, true, meta, func(user *storage.User) (i int, payloads []storage.Payload, err error) {
total, err := h.storage.CountObjectPayloadsInLocalFeed(c.Request.Context())
if err != nil {
return 0, nil, err
}
h.logger.Debug("counted objects in feed", zap.Int("total", total))
objects, err := h.storage.ListObjectPayloadsInLocalFeed(c.Request.Context(), limit, (page-1)*limit)
if err != nil {
return 0, nil, err
}
h.logger.Debug("queried objects", zap.Int("count", len(objects)))
return total, objects, nil
})
}
func (h handler) gatherActors(actors []*storage.Actor, actorSubjects []storage.ActorAlias) map[string]map[string]string {
results := make(map[string]map[string]string)
subjects := storage.CollectActorSubjectsActorToSubject(actorSubjects)
for _, actor := range actors {
summary := make(map[string]string)
subject, hasSubject := subjects[actor.ID]
if hasSubject {
trimmed := strings.TrimPrefix(subject, "acct:")
subjectParts := strings.Split(trimmed, "@")
if len(subjectParts) == 2 {
summary["at"] = trimmed
summary["icon"] = fmt.Sprintf("https://%s/avatar/png/%s/%s?size=60", h.domain, subjectParts[1], subjectParts[0])
results[actor.ActorID] = summary
continue
}
}
actorID := actor.ActorID
u, err := url.Parse(actorID)
if err != nil {
continue
}
domain := u.Hostname()
summary["at"] = fmt.Sprintf("%s@%s", actor.Name, domain)
summary["icon"] = fmt.Sprintf("https://%s/avatar/png/%s/%s?size=60", h.domain, domain, actor.Name)
results[actorID] = summary
}
return results
}