From 803d303420b313a04a08de0d751b337c29d78aa6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 19 Nov 2019 13:51:54 +0900 Subject: [PATCH] Migrate to new follows storage --- arn/AnimeListItemAPI.go | 4 +- arn/User.go | 19 +- arn/User.init.go | 21 ++ arn/UserFollows.go | 137 ------------- arn/UserFollows2.go | 180 ++++++++++++++++++ arn/UserFollowsAPI.go | 34 ---- arn/UserJoins.go | 39 ---- pages/activity/activity.go | 8 +- pages/anime/anime.go | 2 +- pages/anime/episodes.go | 2 +- pages/character/character.go | 2 +- pages/profile/profile.go | 2 +- pages/profile/profile.pixy | 6 +- patches/user-copy-follows/user-copy-follow.go | 22 +++ 14 files changed, 237 insertions(+), 241 deletions(-) create mode 100644 arn/User.init.go create mode 100644 arn/UserFollows2.go delete mode 100644 arn/UserFollowsAPI.go create mode 100644 patches/user-copy-follows/user-copy-follow.go diff --git a/arn/AnimeListItemAPI.go b/arn/AnimeListItemAPI.go index 3a9b0776..35d2eb6b 100644 --- a/arn/AnimeListItemAPI.go +++ b/arn/AnimeListItemAPI.go @@ -46,11 +46,9 @@ func (item *AnimeListItem) Edit(ctx aero.Context, key string, value reflect.Valu // Broadcast event to all users so they can reload the activity page if needed. for receiver := range StreamUsers() { - receiverIsFollowing := Contains(receiver.Follows().Items, user.ID) - receiver.BroadcastEvent(&aero.Event{ Name: "activity", - Data: receiverIsFollowing, + Data: receiver.IsFollowing(user.ID), }) } } diff --git a/arn/User.go b/arn/User.go index d1add695..e5f9ff2e 100644 --- a/arn/User.go +++ b/arn/User.go @@ -20,17 +20,10 @@ import ( gravatar "github.com/ungerik/go-gravatar" ) -var setNickMutex sync.Mutex -var setEmailMutex sync.Mutex - -// Register data lists. -func init() { - DataLists["genders"] = []*Option{ - // &Option{"", "Prefer not to say"}, - {"male", "Male"}, - {"female", "Female"}, - } -} +var ( + setNickMutex sync.Mutex + setEmailMutex sync.Mutex +) // UserID represents a user ID. type UserID = ID @@ -62,6 +55,7 @@ type User struct { Browser UserBrowser `json:"browser" private:"true"` OS UserOS `json:"os" private:"true"` Location *Location `json:"location" private:"true"` + FollowIDs []UserID `json:"follows"` hasPosts @@ -124,9 +118,6 @@ func RegisterUser(user *User) { Items: []*PushSubscription{}, }) - // Add empty follow list - NewUserFollows(user.ID).Save() - // Add empty notifications list NewUserNotifications(user.ID).Save() diff --git a/arn/User.init.go b/arn/User.init.go new file mode 100644 index 00000000..fab596aa --- /dev/null +++ b/arn/User.init.go @@ -0,0 +1,21 @@ +package arn + +import "github.com/aerogo/api" + +// Register data lists. +func init() { + DataLists["genders"] = []*Option{ + // &Option{"", "Prefer not to say"}, + {"male", "Male"}, + {"female", "Female"}, + } + + // Actions + API.RegisterActions("User", []*api.Action{ + // Add follow + FollowAction(), + + // Remove follow + UnfollowAction(), + }) +} diff --git a/arn/UserFollows.go b/arn/UserFollows.go index 6d6a4a20..da71677c 100644 --- a/arn/UserFollows.go +++ b/arn/UserFollows.go @@ -1,8 +1,6 @@ package arn import ( - "errors" - "github.com/aerogo/nano" ) @@ -12,130 +10,6 @@ type UserFollows struct { Items []string `json:"items"` } -// NewUserFollows creates a new UserFollows list. -func NewUserFollows(userID UserID) *UserFollows { - return &UserFollows{ - UserID: userID, - Items: []string{}, - } -} - -// Add adds an user to the list if it hasn't been added yet. -func (list *UserFollows) Add(userID UserID) error { - if userID == list.UserID { - return errors.New("You can't follow yourself") - } - - if list.Contains(userID) { - return errors.New("User " + userID + " has already been added") - } - - list.Items = append(list.Items, userID) - - // Send notification - user, err := GetUser(userID) - - if err == nil { - if !user.Settings().Notification.NewFollowers { - return nil - } - - follower, err := GetUser(list.UserID) - - if err == nil { - user.SendNotification(&PushNotification{ - Title: "You have a new follower!", - Message: follower.Nick + " started following you.", - Icon: "https:" + follower.AvatarLink("large"), - Link: "https://notify.moe" + follower.Link(), - Type: NotificationTypeFollow, - }) - } - } - - return nil -} - -// Remove removes the user ID from the list. -func (list *UserFollows) Remove(userID UserID) bool { - for index, item := range list.Items { - if item == userID { - list.Items = append(list.Items[:index], list.Items[index+1:]...) - return true - } - } - - return false -} - -// Contains checks if the list contains the user ID already. -func (list *UserFollows) Contains(userID UserID) bool { - for _, item := range list.Items { - if item == userID { - return true - } - } - - return false -} - -// Users returns a slice of all the users you are following. -func (list *UserFollows) Users() []*User { - followsObj := DB.GetMany("User", list.Items) - follows := make([]*User, len(followsObj)) - - for i, obj := range followsObj { - follows[i] = obj.(*User) - } - - return follows -} - -// UsersWhoFollowBack returns a slice of all the users you are following that also follow you. -func (list *UserFollows) UsersWhoFollowBack() []*User { - followsObj := DB.GetMany("User", list.Items) - friends := make([]*User, 0, len(followsObj)) - - for _, obj := range followsObj { - friend := obj.(*User) - - if Contains(friend.Follows().Items, list.UserID) { - friends = append(friends, friend) - } - } - - return friends -} - -// GetID returns the ID. -func (list *UserFollows) GetID() string { - return list.UserID -} - -// UserFollowerCountMap returns a map of user ID keys and their corresping number of followers as the value. -func UserFollowerCountMap() map[string]int { - followCount := map[string]int{} - - for list := range StreamUserFollows() { - for _, followUserID := range list.Items { - followCount[followUserID]++ - } - } - - return followCount -} - -// GetUserFollows ... -func GetUserFollows(id UserID) (*UserFollows, error) { - obj, err := DB.Get("UserFollows", id) - - if err != nil { - return nil, err - } - - return obj.(*UserFollows), nil -} - // StreamUserFollows returns a stream of all user follows. func StreamUserFollows() <-chan *UserFollows { channel := make(chan *UserFollows, nano.ChannelBufferSize) @@ -150,14 +24,3 @@ func StreamUserFollows() <-chan *UserFollows { return channel } - -// AllUserFollows returns a slice of all user follows. -func AllUserFollows() ([]*UserFollows, error) { - all := make([]*UserFollows, 0, DB.Collection("UserFollows").Count()) - - for obj := range StreamUserFollows() { - all = append(all, obj) - } - - return all, nil -} diff --git a/arn/UserFollows2.go b/arn/UserFollows2.go new file mode 100644 index 00000000..58a95b89 --- /dev/null +++ b/arn/UserFollows2.go @@ -0,0 +1,180 @@ +package arn + +import ( + "errors" + + "github.com/aerogo/aero" + "github.com/aerogo/api" +) + +// Add adds an user to the user if it hasn't been added yet. +func (user *User) Follow(followUserID UserID) error { + if followUserID == user.ID { + return errors.New("You can't follow yourself") + } + + if user.IsFollowing(followUserID) { + return errors.New("User " + followUserID + " has already been added") + } + + user.FollowIDs = append(user.FollowIDs, followUserID) + + // Send notification + user, err := GetUser(followUserID) + + if err == nil { + if !user.Settings().Notification.NewFollowers { + return nil + } + + follower, err := GetUser(user.ID) + + if err == nil { + user.SendNotification(&PushNotification{ + Title: "You have a new follower!", + Message: follower.Nick + " started following you.", + Icon: "https:" + follower.AvatarLink("large"), + Link: "https://notify.moe" + follower.Link(), + Type: NotificationTypeFollow, + }) + } + } + + return nil +} + +// Unfollow removes the user ID from the follow list. +func (user *User) Unfollow(userID UserID) bool { + for index, item := range user.FollowIDs { + if item == userID { + user.FollowIDs = append(user.FollowIDs[:index], user.FollowIDs[index+1:]...) + return true + } + } + + return false +} + +// IsFollowing checks if the object follows the user ID. +func (user *User) IsFollowing(userID UserID) bool { + for _, item := range user.FollowIDs { + if item == userID { + return true + } + } + + return false +} + +// Follows returns a slice of all the users you are following. +func (user *User) Follows() []*User { + followsObj := DB.GetMany("User", user.FollowIDs) + follows := make([]*User, len(followsObj)) + + for i, user := range followsObj { + follows[i] = user.(*User) + } + + return follows +} + +// Friends returns a slice of all the users you are following that also follow you. +func (user *User) Friends() []*User { + followsObj := DB.GetMany("User", user.FollowIDs) + friends := make([]*User, 0, len(followsObj)) + + for _, friendObj := range followsObj { + friend := friendObj.(*User) + + if friend.IsFollowing(user.ID) { + friends = append(friends, friend) + } + } + + return friends +} + +// Followers returns the users who follow the user. +func (user *User) Followers() []*User { + var followerIDs []string + + for follower := range StreamUsers() { + if follower.IsFollowing(user.ID) { + followerIDs = append(followerIDs, follower.ID) + } + } + + usersObj := DB.GetMany("User", followerIDs) + users := make([]*User, len(usersObj)) + + for i, obj := range usersObj { + users[i] = obj.(*User) + } + + return users +} + +// FollowersCount returns how many followers the user has. +func (user *User) FollowersCount() int { + count := 0 + + for follower := range StreamUsers() { + if follower.IsFollowing(user.ID) { + count++ + } + } + + return count +} + +// UserFollowerCountMap returns a map of user ID keys and their corresping number of followers as the value. +func UserFollowerCountMap() map[string]int { + followCount := map[string]int{} + + for user := range StreamUsers() { + for _, followUserID := range user.FollowIDs { + followCount[followUserID]++ + } + } + + return followCount +} + +// FollowAction returns an API action that adds a user ID to the follow list. +func FollowAction() *api.Action { + return &api.Action{ + Name: "follow", + Route: "/follow/:follow-id", + Run: func(obj interface{}, ctx aero.Context) error { + user := obj.(*User) + followID := ctx.Get("follow-id") + err := user.Follow(followID) + + if err != nil { + return err + } + + user.Save() + return nil + }, + } +} + +// UnfollowAction returns an API action that removes a user ID from the follow list. +func UnfollowAction() *api.Action { + return &api.Action{ + Name: "unfollow", + Route: "/unfollow/:unfollow-id", + Run: func(obj interface{}, ctx aero.Context) error { + user := obj.(*User) + unfollowID := ctx.Get("unfollow-id") + + if !user.Unfollow(unfollowID) { + return errors.New("This item does not exist in the list") + } + + user.Save() + return nil + }, + } +} diff --git a/arn/UserFollowsAPI.go b/arn/UserFollowsAPI.go deleted file mode 100644 index d9de3f93..00000000 --- a/arn/UserFollowsAPI.go +++ /dev/null @@ -1,34 +0,0 @@ -package arn - -import ( - "github.com/aerogo/aero" - "github.com/aerogo/api" -) - -// Force interface implementations -var ( - _ Identifiable = (*UserFollows)(nil) - _ IDCollection = (*UserFollows)(nil) - _ api.Editable = (*UserFollows)(nil) -) - -// Actions -func init() { - API.RegisterActions("UserFollows", []*api.Action{ - // Add follow - AddAction(), - - // Remove follow - RemoveAction(), - }) -} - -// Authorize returns an error if the given API request is not authorized. -func (list *UserFollows) Authorize(ctx aero.Context, action string) error { - return AuthorizeIfLoggedInAndOwnData(ctx, "id") -} - -// Save saves the follow list in the database. -func (list *UserFollows) Save() { - DB.Set("UserFollows", list.UserID, list) -} diff --git a/arn/UserJoins.go b/arn/UserJoins.go index 1d8504c0..621a00fe 100644 --- a/arn/UserJoins.go +++ b/arn/UserJoins.go @@ -42,51 +42,12 @@ func (user *User) Inventory() *Inventory { return inventory } -// Follows returns the list of user follows. -func (user *User) Follows() *UserFollows { - follows, _ := GetUserFollows(user.ID) - return follows -} - // Notifications returns the list of user notifications. func (user *User) Notifications() *UserNotifications { notifications, _ := GetUserNotifications(user.ID) return notifications } -// Followers ... -func (user *User) Followers() []*User { - var followerIDs []string - - for list := range StreamUserFollows() { - if list.Contains(user.ID) { - followerIDs = append(followerIDs, list.UserID) - } - } - - usersObj := DB.GetMany("User", followerIDs) - users := make([]*User, len(usersObj)) - - for i, obj := range usersObj { - users[i] = obj.(*User) - } - - return users -} - -// FollowersCount ... -func (user *User) FollowersCount() int { - count := 0 - - for list := range StreamUserFollows() { - if list.Contains(user.ID) { - count++ - } - } - - return count -} - // DraftIndex ... func (user *User) DraftIndex() *DraftIndex { draftIndex, _ := GetDraftIndex(user.ID) diff --git a/pages/activity/activity.go b/pages/activity/activity.go index c87e4911..44a282b4 100644 --- a/pages/activity/activity.go +++ b/pages/activity/activity.go @@ -21,14 +21,8 @@ func Followed(ctx aero.Context) error { // fetchActivities filters the activities by the given filters. func fetchActivities(user *arn.User, followedOnly bool) []arn.Activity { - var followedUserIDs []string - - if followedOnly && user != nil { - followedUserIDs = user.Follows().Items - } - activities := arn.FilterActivityCreates(func(activity arn.Activity) bool { - if followedOnly && !arn.Contains(followedUserIDs, activity.GetCreatedBy()) { + if followedOnly && user != nil && !user.IsFollowing(activity.GetCreatedBy()) { return false } diff --git a/pages/anime/anime.go b/pages/anime/anime.go index 9bb87927..0f332cc3 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -47,7 +47,7 @@ func Get(ctx aero.Context) error { episodeToFriends := map[int][]*arn.User{} if user != nil { - friends = user.Follows().Users() + friends = user.Follows() deleted := 0 if animeListItem != nil { diff --git a/pages/anime/episodes.go b/pages/anime/episodes.go index 656d84ab..1d82f25b 100644 --- a/pages/anime/episodes.go +++ b/pages/anime/episodes.go @@ -23,7 +23,7 @@ func Episodes(ctx aero.Context) error { episodeToFriends[ownListItem.Episodes] = append(episodeToFriends[ownListItem.Episodes], user) } - for _, friend := range user.Follows().Users() { + for _, friend := range user.Follows() { friendAnimeList := friend.AnimeList() friendAnimeListItem := friendAnimeList.Find(anime.ID) diff --git a/pages/character/character.go b/pages/character/character.go index c8a940dd..f9e8ebf9 100644 --- a/pages/character/character.go +++ b/pages/character/character.go @@ -126,7 +126,7 @@ func Get(ctx aero.Context) error { var friends []*arn.User if user != nil { - friendIDs := utils.Intersection(character.Likes, user.Follows().Items) + friendIDs := utils.Intersection(character.Likes, user.FollowIDs) friendObjects := arn.DB.GetMany("User", friendIDs) for _, obj := range friendObjects { diff --git a/pages/profile/profile.go b/pages/profile/profile.go index 99a14cf8..d6f100ce 100644 --- a/pages/profile/profile.go +++ b/pages/profile/profile.go @@ -120,7 +120,7 @@ func Profile(ctx aero.Context, viewUser *arn.User) error { } // Friends - friends := viewUser.Follows().UsersWhoFollowBack() + friends := viewUser.Friends() arn.SortUsersFollowers(friends) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index e61036df..39b96f88 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -141,12 +141,12 @@ component ProfileHead(viewUser *arn.User, animeList *arn.AnimeList, user *arn.Us .profile-actions if user != nil && user.ID != viewUser.ID - if !user.Follows().Contains(viewUser.ID) - button.profile-action.action.mountable.never-unmount(data-action="followUser", data-trigger="click", data-api="/api/userfollows/" + user.ID + "/add/" + viewUser.ID) + if !user.IsFollowing(viewUser.ID) + button.profile-action.action.mountable.never-unmount(data-action="followUser", data-trigger="click", data-api="/api/user/" + user.ID + "/follow/" + viewUser.ID) Icon("user-plus") span Follow else - button.profile-action.action.mountable.never-unmount(data-action="unfollowUser", data-trigger="click", data-api="/api/userfollows/" + user.ID + "/remove/" + viewUser.ID) + button.profile-action.action.mountable.never-unmount(data-action="unfollowUser", data-trigger="click", data-api="/api/user/" + user.ID + "/unfollow/" + viewUser.ID) Icon("user-times") span Unfollow diff --git a/patches/user-copy-follows/user-copy-follow.go b/patches/user-copy-follows/user-copy-follow.go new file mode 100644 index 00000000..a28f018c --- /dev/null +++ b/patches/user-copy-follows/user-copy-follow.go @@ -0,0 +1,22 @@ +package main + +import ( + "github.com/akyoto/color" + "github.com/animenotifier/notify.moe/arn" +) + +func main() { + defer arn.Node.Close() + + for follows := range arn.StreamUserFollows() { + user, err := arn.GetUser(follows.UserID) + + if err != nil { + color.Red(err.Error()) + continue + } + + user.FollowIDs = follows.Items + user.Save() + } +}