def vote_post(user_id, post_id, amount=1): """Handles voting on posts """ # Get the comment so we can check who the author is author_uid = get_post(post_id).get('user_id') if not has_voted(user_id, post_id): if author_uid != user_id: r.zadd(k.POST_VOTES.format(post_id), amount, user_id) # Increment the score by amount (can be negative) # Post score can go lower than 0 m.db.posts.update({'_id': post_id}, {'$inc': {'score': amount}}) if amount < 0: # Don't decrement the users score if it is already at 0 # we use a query to ONLY find user if the score if greater than # 0. This might seem stange but it is in the only way to keep # this atomic m.db.users.update({'_id': author_uid, 'score': {'$gt': 0}}, {'$inc': {'score': amount}}) else: # If its an increment it doesn't really matter m.db.users.update({'_id': author_uid}, {'$inc': {'score': amount}}) else: raise CantVoteOnOwn else: raise AlreadyVoted
def back_feed(who_id, whom_id): """Takes 5 lastest posts from user with ``who_id`` places them in user with ``whom_id`` feed. The reason behind this is that new users may follow someone but still have and empty feed, which makes them sad :( so we'll give them some. If the posts are to old for a non user they will be removed when the feed is trimmed, but they may make it in to the feed but not at the top. :param who_id: user who just followed ``who_id`` :type who_id: str :param whom_id: user who was just followed by ``whom_id`` :type whom_id: str :returns: None """ # Get followee's last 5 un-approved posts (doesn't matter if isn't any) # We only need the IDs and the created time posts = m.db.posts.find( {'user_id': whom_id, 'reply_to': None, 'permission': {'$lte': k.PERM_PJUU}}, {'_id': True, 'created': True}, ).sort('created', -1).limit(5) # Iterate the cursor and append the posts to the users feed for post in posts: timestamp = post.get('created') post_id = post.get('_id') # Place on the feed r.zadd(k.USER_FEED.format(who_id), timestamp, post_id) # Trim the feed to the 1000 max r.zremrangebyrank(k.USER_FEED.format(who_id), 0, -1000)
def alert(self, alert, user_ids): """Will attempt to alert the user with uid to the alert being managed. This will call the alerts before_alert() method, which allows you to change the alert per user. It's not needed though. """ # Check that the manager actually has an alert if not isinstance(alert, BaseAlert): raise ValueError('alert must be a BaseAlert object') # Ensure uids is iterable # Stopped strings being passed in if not isinstance(user_ids, Iterable) or isinstance(user_ids, str) or \ isinstance(user_ids, unicode): raise TypeError('user_ids must be iterable') # Create the alert object r.set(k.ALERT.format(alert.alert_id), jsonpickle.encode(alert)) # Set the 4WK timeout on it r.expire(k.ALERT.format(alert.alert_id), k.EXPIRE_4WKS) for user_id in user_ids: r.zadd(k.USER_ALERTS.format(user_id), alert.timestamp, alert.alert_id)
def vote_comment(uid, cid, amount=1): """Handles voting on posts """ # Ensure user has not voted before and ensure its a comment check if not has_voted(uid, cid, comment=True): author_uid = get_comment_author(cid) if author_uid != uid: r.zadd(K.COMMENT_VOTES.format(cid), amount, uid) # Post scores can be negative. # INCRBY with a minus value is the same a DECRBY r.hincrby(K.COMMENT.format(cid), 'score', amount=amount) # Get the score of the author cur_user_score = r.hget(K.USER.format(author_uid), 'score') # Stop users scores going lower than 0 cur_user_score = int(cur_user_score) if cur_user_score <= 0 and amount < 0: amount = 0 # Increment the users score r.hincrby(K.USER.format(author_uid), 'score', amount=amount) else: raise CantVoteOnOwn else: raise AlreadyVoted
def populate_approved_followers_feeds(user_id, post_id, timestamp): """Fan out a post_id to all the users approved followers.""" # Get a list of ALL users who are following a user followers = r.zrange(k.USER_APPROVED.format(user_id), 0, -1) # This is not transactional as to not hold Redis up. for follower_id in followers: # Add the pid to the list r.zadd(k.USER_FEED.format(follower_id), timestamp, post_id) # Stop followers feeds from growing to large, doesn't matter if it # doesn't exist r.zremrangebyrank(k.USER_FEED.format(follower_id), 0, -1000)
def approve_user(who_uid, whom_uid): """Allow a user to approve a follower""" # Check that the user is actually following. # Fail if not if r.zrank(k.USER_FOLLOWERS.format(who_uid), whom_uid) is None: return False # Add the user to the approved list # No alert is generated r.zadd(k.USER_APPROVED.format(who_uid), timestamp(), whom_uid) return True
def flag_post(user_id, post_id): """Flags a post for moderator review. :returns: True if flagged, false if removed. `CantFlagOwn` in case of error. """ # Get the comment so we can check who the author is post = get_post(post_id) if post.get('user_id') != user_id: if not has_flagged(user_id, post_id): # Increment the flag count by one and store the user name r.zadd(k.POST_FLAGS.format(post_id), timestamp(), user_id) m.db.posts.update({'_id': post_id}, {'$inc': {'flags': 1}}) else: raise AlreadyFlagged else: raise CantFlagOwn
def follow_user(who_uid, whom_uid): """Add whom to who's following zset and who to whom's followers zset. Generate an alert for this action. """ # Check that we are not already following the user if r.zrank(k.USER_FOLLOWING.format(who_uid), whom_uid) is not None: return False # Follow user # Score is based on UTC epoch time r.zadd(k.USER_FOLLOWING.format(who_uid), timestamp(), whom_uid) r.zadd(k.USER_FOLLOWERS.format(whom_uid), timestamp(), who_uid) # Create an alert and inform whom that who is now following them alert = FollowAlert(who_uid) AlertManager().alert(alert, [whom_uid]) # Back fill the who's feed with some posts from whom back_feed(who_uid, whom_uid) return True
def vote_post(user_id, post_id, amount=1, ts=None): """Handles voting on posts :param user_id: User who is voting :type user_id: str :param post_id: ID of the post the user is voting on :type post_id: int :param amount: The way to vote (-1 or 1) :type amount: int :param ts: Timestamp to use for vote (ONLY FOR TESTING) :type ts: int :returns: -1 if downvote, 0 if reverse vote and +1 if upvote """ if ts is None: ts = timestamp() # Get the comment so we can check who the author is author_uid = get_post(post_id).get('user_id') # Votes can ONLY ever be -1 or 1 and nothing else # we use the sign to store the time and score in one zset score amount = 1 if amount >= 0 else -1 voted = has_voted(user_id, post_id) if not voted: if author_uid != user_id: # Store the timestamp of the vote with the sign of the vote r.zadd(k.POST_VOTES.format(post_id), amount * timestamp(), user_id) # Update post score m.db.posts.update({'_id': post_id}, {'$inc': {'score': amount}}) # Update user score m.db.users.update({'_id': author_uid}, {'$inc': {'score': amount}}) return amount else: raise CantVoteOnOwn elif voted and abs(voted) + k.VOTE_TIMEOUT > ts: # No need to check if user is current user because it can't # happen in the first place # Remove the vote from Redis r.zrem(k.POST_VOTES.format(post_id), user_id) previous_vote = -1 if voted < 0 else 1 # Calculate how much to increment/decrement the scores by # Saves multiple trips to Mongo if amount == previous_vote: if previous_vote < 0: amount = 1 result = 0 else: amount = -1 result = 0 else: # We will only register the new vote if it is NOT a vote reversal. r.zadd(k.POST_VOTES.format(post_id), amount * timestamp(), user_id) if previous_vote < 0: amount = 2 result = 1 else: amount = -2 result = -1 # Update post score m.db.posts.update({'_id': post_id}, {'$inc': {'score': amount}}) # Update user score m.db.users.update({'_id': author_uid}, {'$inc': {'score': amount}}) return result else: raise AlreadyVoted
def create_post(user_id, username, body, reply_to=None, upload=None, permission=k.PERM_PUBLIC): """Creates a new post This handled both posts and what used to be called comments. If the reply_to field is not None then the post will be treat as a comment. You will need to make sure the reply_to post exists. :param user_id: The user id of the user posting the post :type user_id: str :param username: The user name of the user posting (saves a lookup) :type username: str :param body: The content of the post :type body: str :param reply_to: The post id of the post this is a reply to if any :type reply_to: str :param upload: :returns: The post id of the new post :param permission: Who can see/interact with the post you are posting :type permission: int :rtype: str or None """ # Get a new UUID for the post_id ("_id" in MongoDB) post_id = get_uuid() # Get the timestamp, we will use this to populate users feeds post_time = timestamp() post = { '_id': post_id, # Newly created post id 'user_id': user_id, # User id of the poster 'username': username, # Username of the poster 'body': body, # Body of the post 'created': post_time, # Unix timestamp for this moment in time 'score': 0, # Atomic score counter } if reply_to is not None: # If the is a reply it must have this property post['reply_to'] = reply_to else: # Replies don't need a comment count post['comment_count'] = 0 # Set the permission a user needs to view post['permission'] = permission # TODO: Make the upload process better at dealing with issues if upload: # If there is an upload along with this post it needs to go for # processing. # process_upload() can throw an Exception of UploadError. We will let # it fall through as a 500 is okay I think. # TODO: Turn this in to a Queue task at some point filename = process_upload(upload) if filename is not None: # If the upload process was okay attach the filename to the doc post['upload'] = filename else: # Stop the image upload process here if something went wrong. return None # Process everything thats needed in a post links, mentions, hashtags = parse_post(body) # Only add the fields if we need too. if links: post['links'] = links if mentions: post['mentions'] = mentions if hashtags: post['hashtags'] = hashtags # Add the post to the database # If the post isn't stored, result will be None result = m.db.posts.insert(post) # Only carry out the rest of the actions if the insert was successful if result: if reply_to is None: # Add post to authors feed r.zadd(k.USER_FEED.format(user_id), post_time, post_id) # Ensure the feed does not grow to large r.zremrangebyrank(k.USER_FEED.format(user_id), 0, -1000) # Subscribe the poster to there post subscribe(user_id, post_id, SubscriptionReasons.POSTER) # Alert everyone tagged in the post alert_tagees(mentions, user_id, post_id) # Append to all followers feeds or approved followers based # on the posts permission if permission < k.PERM_APPROVED: populate_followers_feeds(user_id, post_id, post_time) else: populate_approved_followers_feeds(user_id, post_id, post_time) else: # To reduce database look ups on the read path we will increment # the reply_to's comment count. m.db.posts.update({'_id': reply_to}, {'$inc': {'comment_count': 1}}) # Alert all subscribers to the post that a new comment has been # added. We do this before subscribing anyone new alert = CommentingAlert(user_id, reply_to) subscribers = [] # Iterate through subscribers and let them know about the comment for subscriber_id in get_subscribers(reply_to): # Ensure we don't get alerted for our own comments if subscriber_id != user_id: subscribers.append(subscriber_id) # Push the comment alert out to all subscribers AlertManager().alert(alert, subscribers) # Subscribe the user to the post, will not change anything if they # are already subscribed subscribe(user_id, reply_to, SubscriptionReasons.COMMENTER) # Alert everyone tagged in the post alert_tagees(mentions, user_id, reply_to) return post_id # If there was a problem putting the post in to Mongo we will return None return None # pragma: no cover