mirror of https://gitlab.com/ngerakines/tavern.git
Added support for liking notes in the ui. Also implements undo/like activity in user inbox.
This commit is contained in:
parent
4657c9415b
commit
c808cf236b
|
@ -53,7 +53,7 @@ type ObjectStorage interface {
|
|||
CountObjectBoostsByObjectIDs(ctx context.Context, objectIDs []uuid.UUID) ([]Count, error)
|
||||
CountObjectBoostsByActorObjectIDs(ctx context.Context, actorRowID uuid.UUID, objectRowIDs []uuid.UUID) ([]Count, error)
|
||||
CountObjectRepliesByObjectIDs(ctx context.Context, objectIDs []uuid.UUID) ([]Count, error)
|
||||
|
||||
CountObjectLikesByObjectIDs(ctx context.Context, objectIDs []uuid.UUID) ([]Count, error)
|
||||
ExistsObjectInUserFeedByObjectID(ctx context.Context, objectID string) (bool, error)
|
||||
ObjectParentsByObjectID(ctx context.Context, objectRowID uuid.UUID) (map[uuid.UUID][]uuid.UUID, error)
|
||||
ObjectChildrenByObjectID(ctx context.Context, objectRowID uuid.UUID) (map[uuid.UUID][]uuid.UUID, error)
|
||||
|
@ -306,6 +306,15 @@ func (s pgStorage) CountObjectBoostsByObjectIDs(ctx context.Context, objectIDs [
|
|||
return s.keyedCount(errors.WrapObjectBoostQueryFailedError, ctx, query, common.UUIDsToInterfaces(objectIDs)...)
|
||||
}
|
||||
|
||||
func (s pgStorage) CountObjectLikesByObjectIDs(ctx context.Context, objectIDs []uuid.UUID) ([]Count, error) {
|
||||
if len(objectIDs) == 0 {
|
||||
return []Count{}, nil
|
||||
}
|
||||
placeholders := common.DollarForEach(len(objectIDs))
|
||||
query := fmt.Sprintf(`SELECT object_id, COUNT(*) FROM object_likes WHERE object_id IN (%s) GROUP BY object_id`, strings.Join(placeholders, ","))
|
||||
return s.keyedCount(errors.WrapObjectBoostQueryFailedError, ctx, query, common.UUIDsToInterfaces(objectIDs)...)
|
||||
}
|
||||
|
||||
func (s pgStorage) CountObjectRepliesByObjectIDs(ctx context.Context, objectIDs []uuid.UUID) ([]Count, error) {
|
||||
if len(objectIDs) == 0 {
|
||||
return []Count{}, nil
|
||||
|
|
|
@ -14,6 +14,19 @@
|
|||
}));
|
||||
newForm.hide().appendTo("body").submit();
|
||||
});
|
||||
|
||||
$('body').on('click', 'a.like', function () {
|
||||
let newForm = jQuery('<form>', {
|
||||
'action': '/dashboard/notes/like/note',
|
||||
'method': 'post',
|
||||
'target': '_top'
|
||||
}).append(jQuery('<input>', {
|
||||
'name': 'object',
|
||||
'value': $(this).data('object'),
|
||||
'type': 'hidden'
|
||||
}));
|
||||
newForm.hide().appendTo("body").submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -35,6 +35,13 @@
|
|||
Announce
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ if .ViewContext.LinkLike }}
|
||||
<a href="#" class="dropdown-item like"
|
||||
data-object="{{ .ViewContext.ObjectID }}">
|
||||
<i class="far fa-thumbs-up"></i>
|
||||
Like
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ if and (or .ViewContext.LinkView .ViewContext.LinkReply) .ViewContext.LinkDelete }}
|
||||
{{ end }}
|
||||
{{ if .ViewContext.LinkDelete }}
|
||||
|
@ -104,7 +111,7 @@
|
|||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
{{ if or .ViewContext.TotalAnnounces .ViewContext.TotalReplies .ViewContext.ViewerAnnounced }}
|
||||
{{ if or .ViewContext.TotalAnnounces .ViewContext.TotalReplies .ViewContext.TotalLikes .ViewContext.ViewerAnnounced }}
|
||||
<ul class="list-inline text-muted">
|
||||
{{ if .ViewContext.TotalAnnounces }}
|
||||
<li class="list-inline-item">
|
||||
|
@ -117,6 +124,9 @@
|
|||
{{ if .ViewContext.TotalReplies }}
|
||||
<li class="list-inline-item">{{ .ViewContext.TotalReplies }} replies</li>
|
||||
{{ end }}
|
||||
{{ if .ViewContext.TotalLikes }}
|
||||
<li class="list-inline-item">{{ .ViewContext.TotalLikes }} likes</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
|
||||
|
|
|
@ -406,6 +406,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("/dashboard/notes/like/note", h.likeNote)
|
||||
authenticated.POST("/notes/delete", h.deleteNote)
|
||||
|
||||
authenticated.GET("/configure", h.configure)
|
||||
|
|
|
@ -613,6 +613,163 @@ func (h handler) announceNote(c *gin.Context) {
|
|||
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)
|
||||
|
||||
|
|
|
@ -152,6 +152,16 @@ func (h handler) displayObjectFeed(c *gin.Context, requireUser bool, vars map[st
|
|||
mappedBoosts[boost.Key] = boost.Count
|
||||
}
|
||||
|
||||
likes, err := h.storage.CountObjectLikesByObjectIDs(ctx, collectedRowIDs)
|
||||
if err != nil {
|
||||
h.hardFail(c, err)
|
||||
return
|
||||
}
|
||||
mappedLikes := make(map[string]int)
|
||||
for _, like := range likes {
|
||||
mappedLikes[like.Key] = like.Count
|
||||
}
|
||||
|
||||
mappedUserBoosts := make(map[string]int)
|
||||
if localUserActor != nil {
|
||||
userBoosts, err := h.storage.CountObjectBoostsByActorObjectIDs(ctx, localUserActor.ID, collectedRowIDs)
|
||||
|
@ -308,7 +318,10 @@ func (h handler) displayObjectFeed(c *gin.Context, requireUser bool, vars map[st
|
|||
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.LinkAnnounce = true
|
||||
viewContext.LinkLike = true
|
||||
}
|
||||
|
||||
if localUserActor != nil && attributedTo == localUserActor.ActorID {
|
||||
viewContext.LinkDelete = true
|
||||
|
@ -397,6 +410,9 @@ func (h handler) displayObjectFeed(c *gin.Context, requireUser bool, vars map[st
|
|||
if count, hasCount := mappedBoosts[viewObjectRowID.String()]; hasCount {
|
||||
viewContext.TotalAnnounces = count
|
||||
}
|
||||
if count, hasCount := mappedLikes[viewObjectRowID.String()]; hasCount {
|
||||
viewContext.TotalLikes = count
|
||||
}
|
||||
if count, hasCount := mappedReplies[viewObjectRowID.String()]; hasCount {
|
||||
viewContext.TotalReplies = count
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ func (h handler) actorInboxLike(c *gin.Context, user *storage.User, payload stor
|
|||
}
|
||||
|
||||
actor, hasActor := storage.JSONString(payload, "actor")
|
||||
object, hasObject := storage.JSONDeepString(payload, "object", "object")
|
||||
object, hasObject := storage.JSONString(payload, "object")
|
||||
|
||||
if !hasActor || !hasObject {
|
||||
h.logger.Warn("unable to process like activity",
|
||||
|
@ -593,6 +593,8 @@ func (h handler) actorInboxUndo(c *gin.Context, user *storage.User, payload stor
|
|||
switch innerType {
|
||||
case "Follow":
|
||||
h.actorInboxUndoFollow(c, user, payload)
|
||||
case "Like":
|
||||
h.actorInboxUndoLike(c, user, payload)
|
||||
default:
|
||||
h.logger.Warn("User received unexpected undo payload type", zap.String("type", innerType), zap.String("user", user.Name))
|
||||
c.Status(http.StatusOK)
|
||||
|
|
|
@ -50,6 +50,7 @@ type viewObjectContext struct {
|
|||
LinkReplyURL string
|
||||
|
||||
LinkAnnounce bool
|
||||
LinkLike bool
|
||||
LinkDelete bool
|
||||
|
||||
AttributedTo string
|
||||
|
@ -81,6 +82,7 @@ type viewObjectContext struct {
|
|||
ViewerAnnounced bool
|
||||
TotalAnnounces int
|
||||
TotalReplies int
|
||||
TotalLikes int
|
||||
|
||||
Error string
|
||||
|
||||
|
|
Loading…
Reference in New Issue