Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#325] refactor post reactions #333

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
### Changed
- Refactor post reactions [#325](https://github.com/rokwire/groups-building-block/issues/325)

## [1.11.0] - 2023-01-13
### Added
Expand Down
8 changes: 8 additions & 0 deletions core/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ type Services interface {
CreatePost(clientID string, current *model.User, post *model.Post, group *model.Group) (*model.Post, error)
UpdatePost(clientID string, current *model.User, group *model.Group, post *model.Post) (*model.Post, error)
ReactToPost(clientID string, current *model.User, groupID string, postID string, reaction string) error
FindReactionsByPost(postID string) ([]model.PostReactions, error)

ReportPostAsAbuse(clientID string, current *model.User, group *model.Group, post *model.Post, comment string, sendToDean bool, sendToGroupAdmins bool) error
DeletePost(clientID string, current *model.User, groupID string, postID string, force bool) error

Expand Down Expand Up @@ -204,6 +206,10 @@ func (s *servicesImpl) ReactToPost(clientID string, current *model.User, groupID
return s.app.reactToPost(clientID, current, groupID, postID, reaction)
}

func (s *servicesImpl) FindReactionsByPost(postID string) ([]model.PostReactions, error) {
return s.app.findReactionsByPost(postID)
}

func (s *servicesImpl) ReportPostAsAbuse(clientID string, current *model.User, group *model.Group, post *model.Post, comment string, sendToDean bool, sendToGroupAdmins bool) error {
return s.app.reportPostAsAbuse(clientID, current, group, post, comment, sendToDean, sendToGroupAdmins)
}
Expand Down Expand Up @@ -359,6 +365,8 @@ type Storage interface {
ReportPostAsAbuse(clientID string, userID string, group *model.Group, post *model.Post) error
ReactToPost(context storage.TransactionContext, userID string, postID string, reaction string, on bool) error
DeletePost(ctx storage.TransactionContext, clientID string, userID string, groupID string, postID string, force bool) error
FindReactions(context storage.TransactionContext, postID string, userID string) (model.PostReactions, error)
FindReactionsByPost(postID string) ([]model.PostReactions, error)

FindAuthmanGroups(clientID string) ([]model.Group, error)
FindAuthmanGroupByKey(clientID string, authmanGroupKey string) (*model.Group, error)
Expand Down
5 changes: 3 additions & 2 deletions core/model/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ type Post struct {
Private bool `json:"private" bson:"private"`
UseAsNotification bool `json:"use_as_notification" bson:"use_as_notification"`
IsAbuse bool `json:"is_abuse" bson:"is_abuse"`
Replies []*Post `json:"replies,omitempty"` // This is constructed by the code (ParentID)
Reactions map[string][]string `json:"reactions,omitempty" bson:"reactions,omitempty"`
Replies []*Post `json:"replies,omitempty"` // This is constructed by the code (ParentID)
Reactions map[string][]string `json:"reactions,omitempty" bson:"reactions,omitempty"` //TODO deprecated delete when deployed to prod
ReactionStats map[string]int `json:"reaction_stats" bson:"reaction_stats"`
ImageURL *string `json:"image_url" bson:"image_url"`

ToMembersList []ToMember `json:"to_members" bson:"to_members"` // nil or empty means everyone; non-empty means visible to those user ids and admins
Expand Down
23 changes: 23 additions & 0 deletions core/model/post_reactions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2022 Board of Trustees of the University of Illinois.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package model

// PostReactions are the reations to a specific post
type PostReactions struct {
ID string `json:"id" bson:"_id"`
PostID string `json:"post_id" bson:"post_id"`
UserID string `json:"user_id" bson:"user_id"`
Reactions []string `json:"reactions" bson:"reactions"`
}
26 changes: 21 additions & 5 deletions core/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,17 +540,25 @@ func (app *Application) reactToPost(clientID string, current *model.User, groupI
return fmt.Errorf("missing post for id %s", postID)
}

for _, userID := range post.Reactions[reaction] {
if current.ID == userID {
err = app.storage.ReactToPost(context, current.ID, postID, reaction, false)
if err != nil {
return fmt.Errorf("error removing reaction: %v", err)
//get reactions bases on post
val, ok := post.ReactionStats[reaction]
if ok && val != 0 {
res, err := app.storage.FindReactions(context, postID, current.ID)
if err == nil {
for i := range res.Reactions {
if res.Reactions[i] == reaction {
err = app.storage.ReactToPost(context, current.ID, postID, reaction, false)
if err != nil {
return fmt.Errorf("error removing reaction: %v", err)
}
}
}

return nil
}
}

//New reaction for current user
err = app.storage.ReactToPost(context, current.ID, postID, reaction, true)
if err != nil {
return fmt.Errorf("error adding reaction: %v", err)
Expand All @@ -562,6 +570,14 @@ func (app *Application) reactToPost(clientID string, current *model.User, groupI
return app.storage.PerformTransaction(transaction)
}

func (app *Application) findReactionsByPost(postID string) ([]model.PostReactions, error) {
res, err := app.storage.FindReactionsByPost(postID)
if err != nil {
return nil, fmt.Errorf("error finding reaction stats: %v", err)
}
return res, err
}

func (app *Application) reportPostAsAbuse(clientID string, current *model.User, group *model.Group, post *model.Post, comment string, sendToDean bool, sendToGroupAdmins bool) error {

if !sendToDean && !sendToGroupAdmins {
Expand Down
22 changes: 22 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1991,6 +1991,28 @@ paths:
- APIKeyAuth: []
tags:
- Client-V1
/api/group/{groupId}/posts/{postId}/reactions:
get:
consumes:
- application/json
description: Gets the reaction stats to a post within the desired group.
operationId: FindReactionStats
parameters:
- description: APP
in: header
name: APP
required: true
type: string
responses:
"200":
description: OK
schema:
type: object
security:
- AppUserAuth: []
- APIKeyAuth: []
tags:
- Client-V1
/api/group/{groupId}/posts/{postId}/report/abuse:
put:
consumes:
Expand Down
159 changes: 150 additions & 9 deletions driven/storage/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,54 @@ func (sa *Adapter) Start() error {
return errors.New("error caching managed group configs")
}

//TODO delete after deployement
err = sa.PerformTransaction(func(context TransactionContext) error {

filter := bson.D{}
var list []*model.Post
err = sa.db.posts.Find(filter, &list, nil)
if err != nil {
return err
}

for i := 0; i < len(list); i++ {
//iterate through post reactions and create new Reaction Document
postID := list[i].ID
for _, userID := range list[i].Reactions["thumbs-up"] {
reactionArray := []string{"thumbs-up"}
reactions := model.PostReactions{
PostID: *postID,
UserID: userID,
Reactions: reactionArray,
}

_, err := sa.db.reactions.InsertOne(reactions)
if err != nil {
fmt.Printf("error migrating reactions %s", err)
return fmt.Errorf("error migrating reactions %s", err)
}
}

//take the count of the total thumbs up reactions
filter := bson.M{"_id": postID}
update := bson.D{
{"$set", bson.D{{"reaction_stats.thumbs-up", len(list[i].Reactions["thumbs-up"])}}},
{"$unset", bson.D{{"reactions", ""}}},
}

_, err := sa.db.posts.UpdateOne(filter, update, nil)
if err != nil {
fmt.Printf("error migrating reactions %s", err)
return fmt.Errorf("error migrating reactions %s", err)
}

}

return nil

})
//TODO end of deletion

return err
}

Expand Down Expand Up @@ -405,7 +453,14 @@ func (sa *Adapter) DeleteUser(clientID string, userID string) error {
}
}

// delete the user
//delete any reactions on posts
err = sa.DeleteUserPostReactions(sessionContext, clientID, userID)
if err != nil {
log.Printf("error deleting user reactions - %s", err.Error())
return err
}

//delete the user
filter := bson.D{
primitive.E{Key: "_id", Value: userID},
primitive.E{Key: "client_id", Value: clientID},
Expand All @@ -420,6 +475,36 @@ func (sa *Adapter) DeleteUser(clientID string, userID string) error {
})
}

// DeleteUserPostReactions updates and removes all user post reactions across all existing groups
func (sa *Adapter) DeleteUserPostReactions(context TransactionContext, clientID string, userID string) error {

filter := bson.M{"user_id": userID}

var res []model.PostReactions
err := sa.db.reactions.Find(filter, &res, nil)
if err != nil {
log.Printf("error deleting reactions for user %s - %s", userID, err.Error())
return err
}

_, err = sa.db.reactions.DeleteMany(filter, nil)
if err != nil {
log.Printf("error deleting user reactions to post - %s", err.Error())
return err
}

for i := 0; i < len(res); i++ {
for j := 0; j < len(res[i].Reactions); j++ {
err = sa.UpdateReactionStats(context, res[i].PostID, false, res[i].Reactions[j])
if err != nil {
return fmt.Errorf("error decrementing reaction stats for post %s with reaction %s for %s: %v", res[i].PostID, res[i].Reactions[j], userID, err)
}
}
}

return nil
}

// CreateGroup creates a group. Returns the id of the created group
func (sa *Adapter) CreateGroup(clientID string, current *model.User, group *model.Group, defaultMemberships []model.GroupMembership) (*string, *utils.GroupError) {
insertedID := uuid.NewString()
Expand Down Expand Up @@ -1432,29 +1517,85 @@ func (sa *Adapter) ReportPostAsAbuse(clientID string, userID string, group *mode

// ReactToPost React to a post
func (sa *Adapter) ReactToPost(context TransactionContext, userID string, postID string, reaction string, on bool) error {
filter := bson.D{primitive.E{Key: "_id", Value: postID}}
filter := bson.D{primitive.E{Key: "post_id", Value: postID}}

updateOperation := "$pull"
if on {
updateOperation = "$push"
}
update := bson.D{
primitive.E{Key: updateOperation, Value: bson.D{
primitive.E{Key: "reactions." + reaction, Value: userID},
}},

update := bson.M{
"$set": bson.M{
"post_id": postID,
"user_id": userID,
},
updateOperation: bson.M{
"reactions": reaction,
},
}
opts := options.Update().SetUpsert(true)
_, err := sa.db.reactions.UpdateOne(filter, update, opts)

res, err := sa.db.posts.UpdateOneWithContext(context, filter, update, nil)
if err != nil {
return fmt.Errorf("error updating post %s with reaction %s for %s: %v", postID, reaction, userID, err)
}
Comment on lines +1536 to 1541
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment related for transaction wrapping as above.

if res.ModifiedCount != 1 {
return fmt.Errorf("updated %d posts with reaction %s for %s, but expected 1", res.ModifiedCount, reaction, userID)

err = sa.UpdateReactionStats(context, postID, on, reaction)
if err != nil {
return fmt.Errorf("error updating reaction stats for post %s with reaction %s for %s: %v", postID, reaction, userID, err)
}
return nil
}

// UpdateReactionStats increments or decrements reaction counts
func (sa *Adapter) UpdateReactionStats(context TransactionContext, postID string, on bool, reaction string) error {
filter := bson.D{primitive.E{Key: "_id", Value: postID}}

incrementValue := -1
if on {
incrementValue = 1
}

update := bson.D{
primitive.E{Key: "$inc", Value: bson.D{
primitive.E{Key: "reaction_stats." + reaction, Value: incrementValue},
}},
Comment on lines +1560 to +1562
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one!

}

upsert := true
opts := options.UpdateOptions{Upsert: &upsert}

_, err := sa.db.posts.UpdateOneWithContext(context, filter, update, &opts)
if err != nil {
return fmt.Errorf("error updating reaction stats for post %s with reaction %s: %v", postID, reaction, err)

}
return nil
}

// FindsReactionsByPost finds reactions based on post id
func (sa *Adapter) FindReactionsByPost(postID string) ([]model.PostReactions, error) {
filter := bson.M{"post_id": postID}
var results []model.PostReactions
err := sa.db.reactions.Find(filter, &results, nil)
if err != nil {
return nil, fmt.Errorf("error storage.Adapter.FindReactionStats - %s", err)
}
return results, nil
}

// FindReactions gets reactions based on postID
func (sa *Adapter) FindReactions(context TransactionContext, postID string, userID string) (model.PostReactions, error) {
filter := bson.M{"post_id": postID, "user_id": userID}
var res model.PostReactions
err := sa.db.reactions.FindOneWithContext(context, filter, &res, nil)
if err != nil {
return res, fmt.Errorf("error finding post reactions %s: %v", postID, err)
}

return res, err
}

// DeletePost Deletes a post
func (sa *Adapter) DeletePost(ctx TransactionContext, clientID string, userID string, groupID string, postID string, force bool) error {

Expand Down
Loading