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)
|
CountObjectBoostsByObjectIDs(ctx context.Context, objectIDs []uuid.UUID) ([]Count, error)
|
||||||
CountObjectBoostsByActorObjectIDs(ctx context.Context, actorRowID uuid.UUID, objectRowIDs []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)
|
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)
|
ExistsObjectInUserFeedByObjectID(ctx context.Context, objectID string) (bool, error)
|
||||||
ObjectParentsByObjectID(ctx context.Context, objectRowID uuid.UUID) (map[uuid.UUID][]uuid.UUID, 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)
|
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)...)
|
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) {
|
func (s pgStorage) CountObjectRepliesByObjectIDs(ctx context.Context, objectIDs []uuid.UUID) ([]Count, error) {
|
||||||
if len(objectIDs) == 0 {
|
if len(objectIDs) == 0 {
|
||||||
return []Count{}, nil
|
return []Count{}, nil
|
||||||
|
|
|
@ -14,6 +14,19 @@
|
||||||
}));
|
}));
|
||||||
newForm.hide().appendTo("body").submit();
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,13 @@
|
||||||
Announce
|
Announce
|
||||||
</a>
|
</a>
|
||||||
{{ end }}
|
{{ 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 }}
|
{{ if and (or .ViewContext.LinkView .ViewContext.LinkReply) .ViewContext.LinkDelete }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .ViewContext.LinkDelete }}
|
{{ if .ViewContext.LinkDelete }}
|
||||||
|
@ -104,7 +111,7 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
{{ end }}
|
{{ 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">
|
<ul class="list-inline text-muted">
|
||||||
{{ if .ViewContext.TotalAnnounces }}
|
{{ if .ViewContext.TotalAnnounces }}
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">
|
||||||
|
@ -117,6 +124,9 @@
|
||||||
{{ if .ViewContext.TotalReplies }}
|
{{ if .ViewContext.TotalReplies }}
|
||||||
<li class="list-inline-item">{{ .ViewContext.TotalReplies }} replies</li>
|
<li class="list-inline-item">{{ .ViewContext.TotalReplies }} replies</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ if .ViewContext.TotalLikes }}
|
||||||
|
<li class="list-inline-item">{{ .ViewContext.TotalLikes }} likes</li>
|
||||||
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
|
|
@ -406,6 +406,7 @@ func serverCommandAction(cliCtx *cli.Context) error {
|
||||||
authenticated.GET("/compose", h.compose)
|
authenticated.GET("/compose", h.compose)
|
||||||
authenticated.POST("/compose/create/note", h.createNote)
|
authenticated.POST("/compose/create/note", h.createNote)
|
||||||
authenticated.POST("/dashboard/notes/announce/note", h.announceNote)
|
authenticated.POST("/dashboard/notes/announce/note", h.announceNote)
|
||||||
|
authenticated.POST("/dashboard/notes/like/note", h.likeNote)
|
||||||
authenticated.POST("/notes/delete", h.deleteNote)
|
authenticated.POST("/notes/delete", h.deleteNote)
|
||||||
|
|
||||||
authenticated.GET("/configure", h.configure)
|
authenticated.GET("/configure", h.configure)
|
||||||
|
|
|
@ -613,6 +613,163 @@ func (h handler) announceNote(c *gin.Context) {
|
||||||
c.Redirect(http.StatusFound, h.url("feed_mine"))
|
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 {
|
func uniqueTo(input []string, exclude ...string) []string {
|
||||||
results := make([]string, 0)
|
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
|
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)
|
mappedUserBoosts := make(map[string]int)
|
||||||
if localUserActor != nil {
|
if localUserActor != nil {
|
||||||
userBoosts, err := h.storage.CountObjectBoostsByActorObjectIDs(ctx, localUserActor.ID, collectedRowIDs)
|
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 {
|
if viewContext.LinkReply {
|
||||||
viewContext.LinkReplyURL = fmt.Sprintf("%s?inReplyTo=%s", h.url("compose"), url.QueryEscape(objectID))
|
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 {
|
if localUserActor != nil && attributedTo == localUserActor.ActorID {
|
||||||
viewContext.LinkDelete = true
|
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 {
|
if count, hasCount := mappedBoosts[viewObjectRowID.String()]; hasCount {
|
||||||
viewContext.TotalAnnounces = count
|
viewContext.TotalAnnounces = count
|
||||||
}
|
}
|
||||||
|
if count, hasCount := mappedLikes[viewObjectRowID.String()]; hasCount {
|
||||||
|
viewContext.TotalLikes = count
|
||||||
|
}
|
||||||
if count, hasCount := mappedReplies[viewObjectRowID.String()]; hasCount {
|
if count, hasCount := mappedReplies[viewObjectRowID.String()]; hasCount {
|
||||||
viewContext.TotalReplies = count
|
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")
|
actor, hasActor := storage.JSONString(payload, "actor")
|
||||||
object, hasObject := storage.JSONDeepString(payload, "object", "object")
|
object, hasObject := storage.JSONString(payload, "object")
|
||||||
|
|
||||||
if !hasActor || !hasObject {
|
if !hasActor || !hasObject {
|
||||||
h.logger.Warn("unable to process like activity",
|
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 {
|
switch innerType {
|
||||||
case "Follow":
|
case "Follow":
|
||||||
h.actorInboxUndoFollow(c, user, payload)
|
h.actorInboxUndoFollow(c, user, payload)
|
||||||
|
case "Like":
|
||||||
|
h.actorInboxUndoLike(c, user, payload)
|
||||||
default:
|
default:
|
||||||
h.logger.Warn("User received unexpected undo payload type", zap.String("type", innerType), zap.String("user", user.Name))
|
h.logger.Warn("User received unexpected undo payload type", zap.String("type", innerType), zap.String("user", user.Name))
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
|
|
|
@ -50,6 +50,7 @@ type viewObjectContext struct {
|
||||||
LinkReplyURL string
|
LinkReplyURL string
|
||||||
|
|
||||||
LinkAnnounce bool
|
LinkAnnounce bool
|
||||||
|
LinkLike bool
|
||||||
LinkDelete bool
|
LinkDelete bool
|
||||||
|
|
||||||
AttributedTo string
|
AttributedTo string
|
||||||
|
@ -81,6 +82,7 @@ type viewObjectContext struct {
|
||||||
ViewerAnnounced bool
|
ViewerAnnounced bool
|
||||||
TotalAnnounces int
|
TotalAnnounces int
|
||||||
TotalReplies int
|
TotalReplies int
|
||||||
|
TotalLikes int
|
||||||
|
|
||||||
Error string
|
Error string
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue