tavern/web/handler_compose.go

1028 lines
27 KiB
Go

package web
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"image"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/buckket/go-blurhash"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/microcosm-cc/bluemonday"
"github.com/russross/blackfriday/v2"
"go.uber.org/zap"
"github.com/ngerakines/tavern/common"
"github.com/ngerakines/tavern/errors"
"github.com/ngerakines/tavern/fed"
"github.com/ngerakines/tavern/storage"
)
func (h handler) compose(c *gin.Context) {
data, user, session, cont := h.loggedIn(c, true)
if !cont {
return
}
recipients := make([]string, 0)
inReplyTo, err := url.QueryUnescape(c.Query("inReplyTo"))
if err == nil && len(inReplyTo) > 0 {
inReplyToPayload, err := h.storage.ObjectPayloadByObjectID(c.Request.Context(), inReplyTo)
if err != nil {
h.flashErrorOrFail(c, h.url("compose"), err)
return
}
thread := fed.ObjectThread(inReplyToPayload)
attributedTo, ok := storage.JSONString(inReplyToPayload, "attributedTo")
if !ok {
h.flashErrorOrFail(c, h.url("compose"), fmt.Errorf("invalid object: missing attributedTo"))
return
}
data["thread_context"] = thread
data["in_reply_to"] = inReplyTo
recipients = append(recipients, attributedTo)
}
if len(recipients) > 0 {
data["recipients"] = recipients
}
view := "compose"
if ok, err := strconv.ParseBool(c.Query("advanced")); ok && err == nil {
view = "compose_advanced"
}
data["followers_collection"] = storage.NewActorID(user.Name, h.domain).Followers()
if err := session.Save(); err != nil {
h.hardFail(c, errors.NewCannotSaveSessionError(err))
return
}
c.HTML(http.StatusOK, view, data)
}
func (h handler) createNote(c *gin.Context) {
session := sessions.Default(c)
ctx := c.Request.Context()
user, err := h.storage.GetUserBySession(ctx, session)
if err != nil {
if errors.Is(err, errors.NewUserNotFoundError(nil)) {
h.flashErrorOrFail(c, h.url(), errors.NewAuthenticationRequiredError(err))
return
}
h.hardFail(c, err)
return
}
userActor, err := h.storage.GetActor(ctx, user.ActorID)
if err != nil {
h.hardFail(c, err)
return
}
userActorID := storage.NewActorID(user.Name, h.domain)
advanced := false
if ok, err := strconv.ParseBool(c.Query("advanced")); ok && err == nil {
advanced = true
}
if !advanced {
if ok, err := strconv.ParseBool(c.PostForm("advanced")); ok && err == nil {
advanced = true
}
}
errorPage := h.url("compose")
if advanced {
errorPage = errorPage + "?advanced=true"
}
now := time.Now().UTC()
activityID := storage.NewV4()
to := make([]string, 0)
cc := make([]string, 0)
broadcastTo := true
broadcastCC := true
broadcastPeers := true
parseTags := true
parseMentions := true
// mediaType := "text/html"
language := "en"
summary := c.PostForm("summary")
threadContext := c.PostForm("thread_context")
inReplyTo := c.PostForm("inReplyTo")
content := c.PostForm("content")
sensitive, _ := strconv.ParseBool(c.PostForm("sensitive"))
// localObjectPrefix := fmt.Sprintf("https://%s/object/", h.domain)
if advanced {
broadcastTo, _ = strconv.ParseBool(c.PostForm("broadcastTo"))
broadcastCC, _ = strconv.ParseBool(c.PostForm("broadcastCc"))
broadcastPeers, _ = strconv.ParseBool(c.PostForm("broadcastPeers"))
parseTags, _ = strconv.ParseBool(c.PostForm("parseTags"))
parseMentions, _ = strconv.ParseBool(c.PostForm("parseMentions"))
// mediaType = c.PostForm("mediaType")
language = c.PostForm("language")
}
if len(threadContext) == 0 {
threadContext = fmt.Sprintf("https://%s/context/%s/%s", h.domain, now.Format("2006-01-02"), storage.NewV4())
}
if len(content) == 0 {
h.flashErrorOrFail(c, errorPage, fmt.Errorf("note is empty"))
return
}
activityURL := common.ActivityURL(h.domain, activityID)
publishedAt := now.Format("2006-01-02T15:04:05Z")
if !advanced {
// The default behavior is to make the post public, send it to followers, and add any recipients to cc.
to = append(to, "https://www.w3.org/ns/activitystreams#Public")
cc = append(cc, userActorID.Followers())
for _, recipient := range c.PostFormArray("recipient") {
cc = append(cc, recipient)
}
} else {
for _, dest := range strings.Fields(c.PostForm("to")) {
to = append(to, dest)
}
for _, dest := range strings.Fields(c.PostForm("cc")) {
cc = append(cc, dest)
}
}
mentionedActors := make(map[string]*storage.Actor)
var mentionedActorNames []string
if parseMentions {
mentionedActorNames = storage.FindMentionedActors(content)
}
if formMentions := c.PostForm("mentions"); len(formMentions) > 0 {
moreMentionedActorNames := storage.FindMentionedActors(c.PostForm("mentions"))
mentionedActorNames = append(mentionedActorNames, moreMentionedActorNames...)
}
for _, mentionedActor := range mentionedActorNames {
if _, ok := mentionedActors[mentionedActor]; ok {
continue
}
foundActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, h.httpClient, mentionedActor)
if err == nil {
mentionedActors[mentionedActor] = foundActor
to = append(to, foundActor.ActorID)
}
}
to = uniqueTo(to)
cc = uniqueCc(to, cc)
var mentionedTags []string
if parseTags {
mentionedTags = storage.FindMentionedTags(content)
}
isPublic := (len(to) > 0 && to[0] == "https://www.w3.org/ns/activitystreams#Public") || (len(cc) > 0 && cc[0] == "https://www.w3.org/ns/activitystreams#Public")
firstTags := make([]string, 0)
createNoteID := storage.NewV4()
createNote := storage.EmptyPayload()
createNote["@context"] = []interface{}{
"https://www.w3.org/ns/activitystreams",
map[string]string{
"sensitive": "as:sensitive",
},
}
createNote["actor"] = string(userActorID)
createNote["id"] = activityURL
createNote["published"] = publishedAt
createNote["type"] = "Create"
createNote["to"] = to
createNote["cc"] = cc
note := storage.EmptyPayload()
note["attributedTo"] = string(userActorID)
unsafe := blackfriday.Run([]byte(content))
html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
note["content"] = string(html)
note["mediaType"] = "text/html"
note["sensitive"] = sensitive
source := storage.EmptyPayload()
source["content"] = content
source["mediaType"] = "markdown"
note["source"] = source
if len(language) > 0 {
note["contentMap"] = map[string]string{
language: string(html),
}
}
note["conversation"] = threadContext
note["context"] = threadContext
noteURL := common.ObjectURL(h.domain, createNoteID)
note["id"] = noteURL
note["published"] = publishedAt
note["summary"] = summary
note["to"] = to
if len(inReplyTo) > 0 {
note["inReplyTo"] = inReplyTo
}
note["cc"] = cc
note["type"] = "Note"
note["url"] = noteURL
replies := storage.EmptyPayload()
replies["id"] = common.ObjectRepliesURL(h.domain, createNoteID)
replies["id"] = "OrderedCollection"
replies["totalItems"] = 0
replies["published"] = publishedAt
replies["first"] = common.ObjectRepliesPageURL(h.domain, createNoteID, 1)
note["replies"] = replies
form, err := c.MultipartForm()
if err == nil {
files := form.File["upload[]"]
if len(files) > 0 {
parentTempDir := os.TempDir()
userUploadDir, err := ioutil.TempDir(parentTempDir, fmt.Sprintf("*-%s", user.ID))
if err != nil {
h.internalServerErrorJSON(c, err)
return
}
defer func() {
h.logger.Debug("deleting temp user upload directory", zap.String("directory", userUploadDir))
if rmErr := os.RemoveAll(userUploadDir); rmErr != nil {
h.logger.Warn("unable to delete temporary directory", zap.Error(rmErr))
}
}()
var images []storage.ImageAsset
for _, file := range files {
img, err := h.uploadHash(c.Request.Context(), file)
if err != nil {
h.internalServerErrorJSON(c, err)
return
}
images = append(images, img)
}
attachments := make([]map[string]interface{}, 0)
for _, img := range images {
attachments = append(attachments, map[string]interface{}{
"type": "Document",
"mediaType": img.GetContentType(),
"url": fmt.Sprintf("https://%s/asset/image/%s", h.domain, img.Checksum),
"name": "an image",
"blurhash": img.Blur,
"focalPoint": []int{0, 0},
})
}
if len(attachments) > 0 {
note["attachment"] = attachments
}
}
}
if len(mentionedActors)+len(mentionedTags) > 0 {
tag := make([]map[string]interface{}, 0)
for name, actor := range mentionedActors {
tag = append(tag, map[string]interface{}{
"type": "Mention",
"href": actor.GetID(),
"name": name,
})
}
for _, hashtag := range mentionedTags {
clean := strings.TrimPrefix(hashtag, "#")
tag = append(tag, map[string]interface{}{
"type": "Hashtag",
"href": fmt.Sprintf("https://%s/tags/%s", h.domain, clean),
"name": hashtag,
})
if len(firstTags) < 10 {
firstTags = append(firstTags, clean)
}
}
note["tag"] = tag
}
createNote["object"] = note
payload := createNote.Bytes()
if broadcastTo || broadcastCC || broadcastPeers {
h.logger.Debug("broadcasting")
}
if ok, err := strconv.ParseBool(c.PostForm("previewJSON")); ok && err == nil {
c.JSON(http.StatusOK, createNote)
return
}
followerTotal, err := h.storage.RowCount(ctx, `SELECT COUNT(*) FROM network_graph WHERE user_id = $1`, user.ID)
if err != nil {
h.flashErrorOrFail(c, errorPage, err)
return
}
followers, err := h.storage.ListAcceptedFollowers(ctx, user.ID, followerTotal, 0)
if err != nil {
h.flashErrorOrFail(c, errorPage, err)
return
}
var toDestinations []string
var ccDestinations []string
for _, dest := range to {
if dest == "https://www.w3.org/ns/activitystreams#Public" {
continue
}
if dest == userActorID.Followers() {
toDestinations = append(toDestinations, followers...)
continue
}
toDestinations = append(toDestinations, dest)
}
for _, dest := range cc {
if dest == "https://www.w3.org/ns/activitystreams#Public" {
continue
}
if dest == userActorID.Followers() {
ccDestinations = append(ccDestinations, followers...)
continue
}
ccDestinations = append(ccDestinations, dest)
}
toDestinations = uniqueTo(toDestinations, string(userActorID))
ccDestinations = uniqueCc(toDestinations, ccDestinations, string(userActorID))
if ok, err := strconv.ParseBool(c.PostForm("previewDestinations")); ok && err == nil {
c.JSON(http.StatusOK, gin.H{"to": toDestinations, "cc": ccDestinations})
return
}
err = storage.TransactionalStorage(ctx, h.storage, func(storage storage.Storage) error {
activityObjectRowID, err := storage.RecordObject(ctx, note, noteURL)
if err != nil {
return err
}
activityRowID, err := storage.RecordObjectEvent(ctx, activityURL, activityObjectRowID, createNote)
if err != nil {
return err
}
if len(inReplyTo) > 0 {
replyRowID, err := storage.ObjectRowIDForObjectID(ctx, inReplyTo)
if err != nil {
return err
}
_, err = storage.RecordObjectReply(ctx, activityObjectRowID, replyRowID)
if err != nil {
return err
}
}
_, err = storage.RecordUserObjectEvent(ctx, user.ID, activityRowID, activityObjectRowID, isPublic)
if err != nil {
return err
}
for _, tag := range firstTags {
_, err = storage.RecordObjectTag(ctx, activityObjectRowID, tag)
if err != nil {
return err
}
}
return nil
})
if err != nil {
h.internalServerErrorJSON(c, err)
return
}
if !broadcastTo && broadcastCC && broadcastPeers {
c.Redirect(http.StatusFound, h.url("feed_mine"))
}
nc := fed.ActorClient{
HTTPClient: h.httpClient,
Logger: h.logger,
}
localActor := h.userActor(user, userActor)
if broadcastTo {
for _, dest := range toDestinations {
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
}
if h.publisherClient != nil {
err = h.publisherClient.Send(context.Background(), foundActor.GetInbox(), userActor.GetKeyID(), user.PrivateKey, activityURL, string(payload))
if err != nil {
h.logger.Error("failed sending to actor", zap.String("target", foundActor.GetID()), zap.String("activity", activityURL), zap.Error(err))
}
} else {
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), zap.Error(err))
}
}
}
}
if broadcastCC {
for _, dest := range ccDestinations {
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
}
if h.publisherClient != nil {
err = h.publisherClient.Send(context.Background(), foundActor.GetInbox(), userActor.GetKeyID(), user.PrivateKey, activityURL, string(payload))
if err != nil {
h.logger.Error("failed sending to actor", zap.String("target", foundActor.GetID()), zap.String("activity", activityURL), zap.Error(err))
}
} else {
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), zap.Error(err))
}
}
}
}
c.Redirect(http.StatusFound, h.url("feed_mine"))
}
func (h handler) announceNote(c *gin.Context) {
session := sessions.Default(c)
ctx := c.Request.Context()
user, err := h.storage.GetUserBySession(ctx, session)
if err != nil {
if errors.Is(err, errors.UserSessionNotFoundError{}) {
if err = appendFlashError(session, "Not Authenticated"); err != nil {
h.hardFail(c, errors.NewCannotSaveSessionError(err))
return
}
c.Redirect(http.StatusFound, h.url())
return
}
h.hardFail(c, err)
return
}
userActor, err := h.storage.GetActor(ctx, user.ActorID)
if err != nil {
h.hardFail(c, err)
return
}
objectID := c.PostForm("object")
if len(objectID) == 0 {
h.flashErrorOrFail(c, h.url("feed_recent"), fmt.Errorf("invalid object id"))
return
}
actor := storage.NewActorID(user.Name, h.domain)
now := time.Now().UTC()
publishedAt := now.Format("2006-01-02T15:04:05Z")
to := []string{
"https://www.w3.org/ns/activitystreams#Public",
}
cc := []string{
actor.Followers(),
}
announceID := fmt.Sprintf("https://%s/activity/%s", h.domain, storage.NewV4())
announce := storage.EmptyPayload()
announce["@context"] = "https://www.w3.org/ns/activitystreams"
announce["id"] = announceID
announce["type"] = "Announce"
announce["actor"] = actor
announce["published"] = publishedAt
announce["to"] = to
announce["cc"] = cc
announce["object"] = objectID
announcePayload := announce.Bytes()
var destinations []string
err = storage.TransactionalStorage(ctx, h.storage, func(tx storage.Storage) error {
objectRowID, err := tx.ObjectRowIDForObjectID(ctx, objectID)
if err != nil {
return err
}
activityRowID, err := tx.RecordObjectEvent(ctx, announceID, objectRowID, announce)
if err != nil {
return err
}
_, err = tx.RecordUserObjectEvent(ctx, user.ID, activityRowID, objectRowID, true)
if err != nil {
return err
}
_, err = tx.RecordObjectAnnouncement(ctx, userActor.ID, activityRowID, objectRowID)
if err != nil {
return err
}
followerTotal, err := tx.CountFollowers(ctx, user.ID)
if err != nil {
return err
}
destinations, err = tx.ListAcceptedFollowers(ctx, user.ID, followerTotal, 0)
if err != nil {
return err
}
return nil
})
if err != nil {
h.internalServerErrorJSON(c, err)
return
}
destinations = uniqueTo(destinations, userActor.ActorID)
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
}
if h.publisherClient != nil {
err = h.publisherClient.Send(context.Background(), foundActor.GetInbox(), userActor.GetKeyID(), user.PrivateKey, announceID, string(announcePayload))
if err != nil {
h.logger.Error("failed sending to actor", zap.String("target", foundActor.GetID()), zap.String("activity", announceID), zap.Error(err))
}
} else {
err = nc.SendToInbox(ctx, localActor, foundActor, announcePayload)
if err != nil {
h.logger.Error("failed sending to actor", zap.String("target", foundActor.GetID()), zap.String("activity", announceID))
}
}
}
c.Redirect(http.StatusFound, h.url("feed_mine"))
}
func (h handler) likeNote(c *gin.Context) {
session := sessions.Default(c)
ctx := c.Request.Context()
user, err := h.storage.GetUserBySession(ctx, session)
if err != nil {
if errors.Is(err, errors.UserSessionNotFoundError{}) {
if err = appendFlashError(session, "Not Authenticated"); err != nil {
h.hardFail(c, errors.NewCannotSaveSessionError(err))
return
}
c.Redirect(http.StatusFound, h.url())
return
}
h.hardFail(c, err)
return
}
userActor, err := h.storage.GetActor(ctx, user.ActorID)
if err != nil {
h.hardFail(c, err)
return
}
userActorFollowers, _ := storage.JSONString(userActor.Payload, "followers")
objectID := c.PostForm("object")
if len(objectID) == 0 {
h.flashErrorOrFail(c, h.url("feed_recent"), fmt.Errorf("invalid object id"))
return
}
objectPayload, err := h.storage.ObjectPayloadByObjectID(ctx, objectID)
if err != nil {
h.hardFail(c, err)
return
}
attributedTo, hasAttributedTo := storage.JSONString(objectPayload, "attributedTo")
if !hasAttributedTo {
h.hardFail(c, fmt.Errorf("object has no attributedTo attribute"), zap.String("object_id", objectID))
return
}
if userActor.ActorID == attributedTo {
h.hardFail(c, fmt.Errorf("you cannot like your own stuff"), zap.String("object_id", objectID))
return
}
sourceActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, h.httpClient, attributedTo)
if err != nil {
h.hardFail(c, err, zap.String("actor", attributedTo))
return
}
sourceActorInbox, hasSourceActorInbox := storage.JSONString(sourceActor.Payload, "inbox")
if !hasSourceActorInbox {
h.hardFail(c, fmt.Errorf("actor has no inbox attribute"), zap.String("actor", attributedTo))
return
}
now := time.Now().UTC()
publishedAt := now.Format("2006-01-02T15:04:05Z")
to := []string{
"https://www.w3.org/ns/activitystreams#Public",
attributedTo,
}
cc := []string{
userActorFollowers,
}
likeURL := common.ActivityURL(h.domain, storage.NewV4())
like := storage.EmptyPayload()
like["@context"] = "https://www.w3.org/ns/activitystreams"
like["id"] = likeURL
like["type"] = "Like"
like["actor"] = userActor.ActorID
like["published"] = publishedAt
like["to"] = to
like["cc"] = cc
like["object"] = objectID
likePayload := like.Bytes()
var destinations []string
err = storage.TransactionalStorage(ctx, h.storage, func(tx storage.Storage) error {
objectRowID, err := tx.ObjectRowIDForObjectID(ctx, objectID)
if err != nil {
return err
}
activityRowID, err := tx.RecordObjectEvent(ctx, likeURL, objectRowID, like)
if err != nil {
return err
}
_, err = tx.RecordUserObjectEvent(ctx, user.ID, activityRowID, objectRowID, true)
if err != nil {
return err
}
_, err = tx.RecordLike(ctx, userActor.ID, objectRowID, like)
if err != nil {
return err
}
followerTotal, err := tx.CountFollowers(ctx, user.ID)
if err != nil {
return err
}
destinations, err = tx.ListAcceptedFollowers(ctx, user.ID, followerTotal, 0)
if err != nil {
return err
}
return nil
})
if err != nil {
h.internalServerErrorJSON(c, err)
return
}
destinations = append(destinations, sourceActorInbox)
destinations = uniqueTo(destinations, userActor.ActorID)
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
}
if h.publisherClient != nil {
err = h.publisherClient.Send(context.Background(), foundActor.GetInbox(), userActor.GetKeyID(), user.PrivateKey, likeURL, string(likePayload))
if err != nil {
h.logger.Error("failed sending to actor", zap.String("target", foundActor.GetID()), zap.String("activity", likeURL), zap.Error(err))
}
} else {
err = nc.SendToInbox(ctx, localActor, foundActor, likePayload)
if err != nil {
h.logger.Error("failed sending to actor", zap.String("target", foundActor.GetID()), zap.String("activity", likeURL))
}
}
}
c.Redirect(http.StatusFound, h.url("feed_mine"))
}
func uniqueTo(input []string, exclude ...string) []string {
results := make([]string, 0)
if len(input) == 0 {
return results
}
hasPublic := false
imap := make(map[string]bool)
for _, i := range input {
if i == "https://www.w3.org/ns/activitystreams#Public" {
hasPublic = true
}
imap[i] = true
}
for _, ex := range exclude {
delete(imap, ex)
}
if hasPublic {
results = append(results, "https://www.w3.org/ns/activitystreams#Public")
}
for dest := range imap {
if dest == "https://www.w3.org/ns/activitystreams#Public" {
continue
}
results = append(results, dest)
}
return results
}
func uniqueCc(to []string, cc []string, exclude ...string) []string {
results := make([]string, 0)
if len(cc) == 0 {
return results
}
hasPublic := false
toMap := make(map[string]bool)
ccMap := make(map[string]bool)
for _, i := range to {
toMap[i] = true
}
for _, i := range cc {
if i == "https://www.w3.org/ns/activitystreams#Public" {
hasPublic = true
}
ccMap[i] = true
}
for _, ex := range exclude {
delete(ccMap, ex)
}
if hasPublic {
results = append(results, "https://www.w3.org/ns/activitystreams#Public")
}
for dest := range ccMap {
if dest == "https://www.w3.org/ns/activitystreams#Public" {
continue
}
if _, ok := toMap[dest]; ok {
continue
}
results = append(results, dest)
}
return results
}
func (h handler) uploadHash(ctx context.Context, file *multipart.FileHeader) (storage.ImageAsset, error) {
src, err := file.Open()
if err != nil {
return storage.ImageAsset{}, err
}
defer src.Close()
tmpfile, err := ioutil.TempFile("", "image")
if err != nil {
return storage.ImageAsset{}, err
}
hasher := sha256.New()
w := io.MultiWriter(tmpfile, hasher)
lsr := io.LimitReader(src, 5000001) // 5 megs
written, err := io.Copy(w, lsr)
if err != nil {
return storage.ImageAsset{}, err
}
if written > 5000000 {
return storage.ImageAsset{}, fmt.Errorf("file size limit of 5 megabytes reached")
}
tmpFileName := tmpfile.Name()
checksum := hex.EncodeToString(hasher.Sum(nil))
err = tmpfile.Close()
bounds, ff, blurHash, err := imageBounds(tmpFileName)
if err != nil {
return storage.ImageAsset{}, err
}
contentType := storage.ContentTypeUnknown
switch ff {
case "png":
contentType = storage.ContentTypePNG
case "jpg", "jpeg":
contentType = storage.ContentTypeJPG
case "svg":
// This is put in here as a placeholder to do some basic parsing and detection of SVG images.
contentType = storage.ContentTypeSVG
}
fullLocation, err := h.assetStorage.Upload(context.Background(), checksum, tmpFileName)
if err != nil && !errors.Is(err, errors.NewAssetExistsError(nil)) {
return storage.ImageAsset{}, err
}
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, true)
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().UTC()
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.CountFollowers(ctx, 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 {
return image.Rectangle{}, "", "", err
}
defer file.Close()
img, ff, err := image.Decode(file)
if err != nil {
return image.Rectangle{}, "", "", err
}
blurHash, err := blurhash.Encode(4, 3, &img)
if err != nil {
return image.Rectangle{}, "", "", err
}
return img.Bounds(), ff, blurHash, nil
}