mirror of https://gitlab.com/ngerakines/tavern.git
509 lines
15 KiB
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
|
|
}
|