internal/repo/activity_common/follow.go (209 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 activity_common import ( "context" "time" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/service/activity_common" "github.com/apache/answer/internal/service/unique" "github.com/apache/answer/pkg/obj" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "xorm.io/builder" "xorm.io/xorm" ) // FollowRepo follow repository type FollowRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo activityRepo activity_common.ActivityRepo } // NewFollowRepo new repository func NewFollowRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, activityRepo activity_common.ActivityRepo, ) activity_common.FollowRepo { return &FollowRepo{ data: data, uniqueIDRepo: uniqueIDRepo, activityRepo: activityRepo, } } // GetFollowAmount get object id's follows func (ar *FollowRepo) GetFollowAmount(ctx context.Context, objectID string) (follows int, err error) { objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return 0, err } switch objectType { case "question": model := &entity.Question{} _, err = ar.data.DB.Context(ctx).Where("id = ?", objectID).Cols("`follow_count`").Get(model) if err == nil { follows = int(model.FollowCount) } case "user": model := &entity.User{} _, err = ar.data.DB.Context(ctx).Where("id = ?", objectID).Cols("`follow_count`").Get(model) if err == nil { follows = int(model.FollowCount) } case "tag": model := &entity.Tag{} _, err = ar.data.DB.Context(ctx).Where("id = ?", objectID).Cols("`follow_count`").Get(model) if err == nil { follows = int(model.FollowCount) } default: err = errors.InternalServer(reason.DisallowFollow).WithMsg("this object can't be followed") } if err != nil { return 0, err } return follows, nil } // GetFollowUserIDs get follow userID by objectID func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (userIDs []string, err error) { objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return nil, err } activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow") if err != nil { log.Errorf("can't get activity type by object key: %s", objectTypeStr) return nil, err } userIDs = make([]string, 0) session := ar.data.DB.Context(ctx).Select("user_id") session.Table(entity.Activity{}.TableName()) session.Where("object_id = ?", objectID) session.Where("activity_type = ?", activityType) session.Where("cancelled = 0") err = session.Find(&userIDs) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return userIDs, nil } // GetFollowIDs get all follow id list func (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string) (followIDs []string, err error) { followIDs = make([]string, 0) activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow") if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } session := ar.data.DB.Context(ctx).Select("object_id") session.Table(entity.Activity{}.TableName()) session.Where("user_id = ? AND activity_type = ?", userID, activityType) session.Where("cancelled = 0") err = session.Find(&followIDs) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return followIDs, nil } // IsFollowed check user if follow object or not func (ar *FollowRepo) IsFollowed(ctx context.Context, userID, objectID string) (followed bool, err error) { objectKey, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return false, err } activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow") if err != nil { return false, err } at := &entity.Activity{} has, err := ar.data.DB.Context(ctx).Where("user_id = ? AND object_id = ? AND activity_type = ?", userID, objectID, activityType).Get(at) if err != nil { return false, err } if !has { return false, nil } if at.Cancelled == entity.ActivityCancelled { return false, nil } else { return true, nil } } // MigrateFollowers migrate followers from source object to target object func (ar *FollowRepo) MigrateFollowers(ctx context.Context, sourceObjectID, targetObjectID, action string) error { // if source object id and target object id are same type sourceObjectTypeStr, err := obj.GetObjectTypeStrByObjectID(sourceObjectID) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } targetObjectTypeStr, err := obj.GetObjectTypeStrByObjectID(targetObjectID) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if sourceObjectTypeStr != targetObjectTypeStr { return errors.InternalServer(reason.DisallowFollow).WithMsg("not same object type") } activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, sourceObjectTypeStr, action) if err != nil { return err } // 1. get all user ids who follow the source object userIDs, err := ar.GetFollowUserIDs(ctx, sourceObjectID) if err != nil { log.Errorf("MigrateFollowers: failed to get user ids who follow %s: %v", sourceObjectID, err) return err } _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) // 1. delete all follows of the source object _, err = session.Table(entity.Activity{}.TableName()). Where(builder.Eq{ "object_id": sourceObjectID, "activity_type": activityType, }). Delete(&entity.Activity{}) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // 2. update cancel status to active for target tag if source tag followers is active _, err = session.Table(entity.Activity{}.TableName()). Where(builder.Eq{ "object_id": targetObjectID, "activity_type": activityType, }). And(builder.In("user_id", userIDs)). Cols("cancelled"). Update(&entity.Activity{ Cancelled: entity.ActivityAvailable, }) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // 3. get existing follows of the target object targetFollowers := make([]string, 0) err = session.Table(entity.Activity{}.TableName()). Where(builder.Eq{ "object_id": targetObjectID, "activity_type": activityType, "cancelled": entity.ActivityAvailable, }). Cols("user_id"). Find(&targetFollowers) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // 4. filter out user ids that already follow the target object and create new activity // Create a map for faster lookup of existing followers existingFollowers := make(map[string]bool) for _, uid := range targetFollowers { existingFollowers[uid] = true } // Filter out users who already follow the target newFollowers := make([]string, 0) for _, uid := range userIDs { if !existingFollowers[uid] { newFollowers = append(newFollowers, uid) } } // Create new activities for the filtered users for _, uid := range newFollowers { activity := &entity.Activity{ UserID: uid, ObjectID: targetObjectID, OriginalObjectID: targetObjectID, ActivityType: activityType, CreatedAt: time.Now(), UpdatedAt: time.Now(), Cancelled: entity.ActivityAvailable, } if _, err = session.Insert(activity); err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } } return nil, nil }) return err }