Пример #1
0
    def test_uploads(self):
        """Simply tests the backend functions in `lib.uploads`.

        Also tests `posts.get_upload` since this is only a simple wrapper
        around the backend function.

        """
        test_upload_dir = 'tests/upload_test_files/'
        test_upload_files = [
            join(test_upload_dir, f) for f in listdir(test_upload_dir)
            if isfile(join(test_upload_dir, f))
        ]

        # Create a GridFS object to test image deletion
        grid = gridfs.GridFS(m.db, collection='uploads')

        # Test each file in the upload directory
        for f in test_upload_files:
            uuid = get_uuid()
            image = io.BytesIO(
                open(f).read()
            )
            filename = process_upload(uuid, image)

            self.assertEqual(filename, '{}.png'.format(uuid))

            # Get the upload these are designed for being served directly by
            # Flask. This is a Flask/Werkzeug response object
            image = get_upload(filename)
            self.assertTrue(grid.exists({'filename': filename}))
            self.assertEqual(image.headers['Content-Type'], 'image/png')

            # Test deletion
            # Ensure file is present (it will be)
            self.assertTrue(grid.exists({'filename': filename}))
            # Delete the file and ensure it is not there through GridFS
            delete_upload(filename)
            # Ensure the file has gone
            self.assertFalse(grid.exists({'filename': filename}))

        # Ensure that if we load a non-image file a None value is returned
        uuid = get_uuid()
        image = io.BytesIO()
        self.assertIsNone(process_upload(uuid, image))
Пример #2
0
def create_account(username, email, password):
    """Creates a new user account.

    :param username: The new users user name
    :type username: str
    :param email: The new users e-mail address
    :type email: str
    :param password: The new users password un-hashed
    :type password: str
    :returns: The UID of the new user
    :rtype: str or None

    """
    username = username.lower()
    email = email.lower()
    try:
        if check_username(username) and check_username_pattern(username) and \
                check_email(email) and check_email_pattern(email):
            # Get a new UUID for the user
            uid = get_uuid()

            user = {
                '_id': uid,
                'username': username.lower(),
                'email': email.lower(),
                'password': generate_password(password,
                                              method='pbkdf2:sha256:2000',
                                              salt_length=20),
                'created': timestamp(),
                'last_login': -1,
                'active': False,
                'banned': False,
                'op': False,
                'muted': False,
                'about': "",
                'score': 0,
                'alerts_last_checked': -1,
                # Set the TTL for a newly created user, this has to be Datetime
                # object for MongoDB to recognise it. This is removed on
                # activation.
                'ttl': datetime.utcnow()
            }

            # Set all the tips for new users
            for tip_name in k.VALID_TIP_NAMES:
                user['tip_{}'.format(tip_name)] = True

            # Insert the new user in to Mongo. If this fails a None will be
            # returned
            result = m.db.users.insert(user)
            return uid if result else None
    except DuplicateKeyError:  # pragma: no cover
        # Oh no something went wrong. Pass over it. A None will be returned.
        pass

    return None
Пример #3
0
def create_account(username, email, password):
    """Creates a new user account.

    :param username: The new users user name
    :type username: str
    :param email: The new users e-mail address
    :type email: str
    :param password: The new users password un-hashed
    :type password: str
    :returns: The UID of the new user
    :rtype: str or None

    """
    username = username.lower()
    email = email.lower()
    try:
        if (
            check_username(username)
            and check_username_pattern(username)
            and check_email(email)
            and check_email_pattern(email)
        ):
            # Get a new UUID for the user
            uid = get_uuid()

            user = {
                "_id": uid,
                "username": username.lower(),
                "email": email.lower(),
                "password": generate_password(password, method="pbkdf2:sha256:2000", salt_length=20),
                "created": timestamp(),
                "last_login": -1,
                "active": False,
                "banned": False,
                "op": False,
                "muted": False,
                "about": "",
                "score": 0,
                "alerts_last_checked": -1,
                # Set the TTL for a newly created user, this has to be Datetime
                # object for MongoDB to recognise it. This is removed on
                # activation.
                "ttl": datetime.utcnow(),
            }

            # Insert the new user in to Mongo. If this fails a None will be
            # returned
            result = m.db.users.insert(user)
            return uid if result else None
    except DuplicateKeyError:  # pragma: no cover
        # Oh no something went wrong. Pass over it. A None will be returned.
        pass

    return None
Пример #4
0
def create_post(uid, body):
    """Creates a new post

    """
    # Get a new UUID for the pid
    pid = get_uuid()

    # Hash form for posts
    # TODO this needs expanding to include some form of image upload hook
    post = {
        'pid': pid,
        'uid': uid,
        'body': body,
        'created': timestamp(),
        'score': 0
    }

    # Add post
    r.hmset(K.POST.format(pid), post)
    # Add post to users post list
    r.lpush(K.USER_POSTS.format(uid), pid)
    # Add post to authors feed
    r.lpush(K.USER_FEED.format(uid), pid)
    # Ensure the feed does not grow to large
    r.ltrim(K.USER_FEED.format(uid), 0, 999)

    # Append to all followers feeds
    populate_feeds(uid, pid)

    # Subscribe the poster to there post
    subscribe(uid, pid, SubscriptionReasons.POSTER)

    # TAGGING

    # Create alert manager and alert
    alert = TaggingAlert(uid, pid)
    # Alert tagees
    tagees = parse_tags(body)
    # Store a list of uids which need to alerted to the tagging
    tagees_to_alert = []
    for tagee in tagees:
        # Don't allow tagging yourself
        if tagee[0] != uid:
            # Subscribe the tagee to the alert
            subscribe(tagee[0], pid, SubscriptionReasons.TAGEE)
            # Add the tagee's uid to the list to alert them
            tagees_to_alert.append(tagee[0])

    # Alert the required tagees
    AlertManager().alert(alert, tagees_to_alert)

    return pid
Пример #5
0
def process_upload(upload, collection='uploads', image_size=(1280, 720),
                   thumbnail=True):
    """Processes the uploaded images in the posts and also the users avatars.
    This should be extensible in future to support the uploading of videos,
    audio, etc...

    :param _id: The ID for the post, user or anything you want as the filename
    :type _id: str
    :param upload: The uploaded Werkzeug FileStorage object
    :type upload: ``Werkzeug.datastructures.FileStorage``
    :param collection: The GridFS collection to upload the file too.
    :type collection: str
    :param image_size: The max height and width for the upload
    :type image_size: Tuple length 2 of int
    :param thumbnail: Is the image to have it's aspect ration kept?
    :type thumbnail: bool
    """
    try:
        # StringIO to take the uploaded image to transport to GridFS
        output = io.BytesIO()

        # All images are passed through PIL and turned in to PNG files.
        # Will change if they need thumbnailing or resizing.
        img = PILImage.open(upload)
        if thumbnail:
            img.thumbnail(image_size, PILImage.ANTIALIAS)
        else:
            # Pillow `resize` returns an image unlike thumbnail
            img = img.resize(image_size, PILImage.ANTIALIAS)

        img.save(output, format='PNG', quality=100)

        # Return the file pointer to the start
        output.seek(0)

        # Create a new file name <uuid>.<upload_extension>
        filename = '{0}.{1}'.format(get_uuid(), 'png')

        # Place file inside GridFS
        m.save_file(filename, output, base=collection)

        return filename
    except (IOError):
        # File will not have been uploaded
        return None
Пример #6
0
def create_user(username, email, password):
    """Creates a user account

    """
    username = username.lower()
    email = email.lower()
    if check_username(username) and check_email(email) and \
       check_username_pattern(username) and check_email_pattern(email):
        # Create the user lookup keys. This LUA script ensures
        # that the name can not be taken at the same time causing a race
        # condition. This is also passed a UUID and will only return it if
        # successful
        uid = L.create_user(keys=[K.UID_USERNAME.format(username),
                                  K.UID_EMAIL.format(email)],
                            args=[get_uuid()])
        # Create user dictionary ready for HMSET only if uid is not None
        # This will only be None in the event of a race condition which we cant
        # really test for.
        if uid is not None:  # pragma: no branch
            user = {
                'uid': uid,
                'username': username,
                'email': email,
                'password': generate_password(password),
                'created': timestamp(),
                'last_login': -1,
                'active': 0,
                'banned': 0,
                'op': 0,
                'muted': 0,
                'about': "",
                'score': 0,
                'alerts_last_checked': 0
            }
            r.hmset(K.USER.format(uid), user)
            # Set the TTL for the user account
            r.expire(K.USER.format(uid), K.EXPIRE_24HRS)
            return uid

    # If none of this worked return nothing
    return None
Пример #7
0
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
Пример #8
0
def process_upload(upload, collection='uploads', image_size=(1280, 720),
                   thumbnail=True):
    """Processes the uploaded images in the posts and also the users avatars.
    This should be extensible in future to support the uploading of videos,
    audio, etc...

    :param _id: The ID for the post, user or anything you want as the filename
    :type _id: str
    :param upload: The uploaded Werkzeug FileStorage object
    :type upload: ``Werkzeug.datastructures.FileStorage``
    :param collection: The GridFS collection to upload the file too.
    :type collection: str
    :param image_size: The max height and width for the upload
    :type image_size: Tuple length 2 of int
    :param thumbnail: Is the image to have it's aspect ration kept?
    :type thumbnail: bool
    """
    try:
        # StringIO to take the uploaded image to transport to GridFS
        output = io.BytesIO()

        # All images are passed through Wand and turned in to PNG files.
        # Unless the image is a GIF and its format is kept
        # Will change if they need thumbnailing or resizing.
        img = Image(file=upload)

        # If the input file if a GIF then we need to know
        gif = True if img.format == 'GIF' else False
        animated_gif = True if gif and len(img.sequence) > 1 else False

        # Check the exif data.
        # If there is an orientation then transform the image so that
        # it is always looking up.
        try:
            exif_data = {
                k[5:]: v
                for k, v in img.metadata.items()
                if k.startswith('exif:')
            }

            orientation = exif_data.get('Orientation')

            orientation = int(orientation)

            if orientation:  # pragma: no branch
                if orientation == 2:
                    img.flop()
                elif orientation == 3:
                    img.rotate(180)
                elif orientation == 4:
                    img.flip()
                elif orientation == 5:
                    img.flip()
                    img.rotate(90)
                elif orientation == 6:
                    img.rotate(90)
                elif orientation == 7:
                    img.flip()
                    img.rotate(270)
                elif orientation == 8:
                    img.rotate(270)

        except (AttributeError, TypeError, AttributeError):
            pass

        if thumbnail:
            # If the GIF was known to be animated then save the animated
            # then cycle through the frames, transforming them and save the
            # output
            if animated_gif:
                animated_image = Image()
                for frame in img.sequence:
                    frame.transform(resize='{0}x{1}>'.format(*image_size))
                    # Directly append the frame to the output image
                    animated_image.sequence.append(frame)

                animated_output = io.BytesIO()
                animated_output.format = 'GIF'
                animated_image.save(file=animated_output)
                animated_output.seek(0)

            img.transform(resize='{0}x{1}>'.format(*image_size))
        else:
            # Just sample the image to the correct size
            img.sample(*image_size)
            # Turn off animated GIF
            animated_gif = False

        img.format = 'PNG'
        uuid = get_uuid()
        filename = '{0}.{1}'.format(uuid, 'png')

        # Return the file pointer to the start
        img.save(file=output)
        output.seek(0)

        # Place file inside GridFS
        m.save_file(filename, output, base=collection)

        animated_filename = ''
        if animated_gif:
            animated_filename = '{0}.{1}'.format(uuid, 'gif')
            m.save_file(animated_filename, animated_output, base=collection)

        return filename, animated_filename
    except (IOError, MissingDelegateError):
        # File will not have been uploaded
        return None, None
Пример #9
0
def create_account(username, email, password):
    """Creates a new user account.

    :param username: The new users user name
    :type username: str
    :param email: The new users e-mail address
    :type email: str
    :param password: The new users password un-hashed
    :type password: str
    :returns: The UID of the new user
    :rtype: str or None

    """
    username = username.lower()
    email = email.lower()
    try:
        if check_username(username) and check_username_pattern(username) and \
                check_email(email) and check_email_pattern(email):
            # Get a new UUID for the user
            uid = get_uuid()

            user = {
                '_id':
                uid,
                'username':
                username.lower(),
                'email':
                email.lower(),
                'password':
                generate_password(password,
                                  method='pbkdf2:sha256:2000',
                                  salt_length=20),
                'created':
                timestamp(),
                'last_login':
                -1,
                'active':
                False,
                'banned':
                False,
                'op':
                False,
                'muted':
                False,
                'about':
                "",
                'score':
                0,
                'alerts_last_checked':
                -1,
                # Set the TTL for a newly created user, this has to be Datetime
                # object for MongoDB to recognise it. This is removed on
                # activation.
                'ttl':
                datetime.utcnow()
            }

            # Set all the tips for new users
            for tip_name in k.VALID_TIP_NAMES:
                user['tip_{}'.format(tip_name)] = True

            # Insert the new user in to Mongo. If this fails a None will be
            # returned
            result = m.db.users.insert(user)
            return uid if result else None
    except DuplicateKeyError:  # pragma: no cover
        # Oh no something went wrong. Pass over it. A None will be returned.
        pass

    return None
Пример #10
0
 def __init__(self, user_id):
     self.alert_id = get_uuid()
     self.timestamp = timestamp()
     self.user_id = user_id
Пример #11
0
def create_comment(uid, pid, body):
    """Create a new comment

    """
    # Get a new UUID for the cid
    cid = get_uuid()

    # Form for comment hash
    comment = {
        'cid': cid,
        'uid': uid,
        'pid': pid,
        'body': body,
        'created': timestamp(),
        'score': 0
    }

    # Add comment
    r.hmset(K.COMMENT.format(cid), comment)
    # Add comment to posts comment list
    r.lpush(K.POST_COMMENTS.format(pid), cid)
    # Add comment to users comment list
    # This may seem redundant but it allows for perfect account deletion
    # Please see Issue #3 on Github
    r.lpush(K.USER_COMMENTS.format(uid), cid)

    # COMMENT ALERTING

    # Alert all subscribers to the post that a new comment has been added.
    # We do this before subscribing anyone new
    # Create alert manager and alert
    alert = CommentingAlert(uid, pid)

    subscribers = []
    # Iterate through subscribers and let them know about the comment
    for subscriber in get_subscribers(pid):
        # Ensure we don't get alerted for our own comments
        if subscriber != uid:
            subscribers.append(subscriber)

    # 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(uid, pid, SubscriptionReasons.COMMENTER)

    # TAGGING

    # Create alert
    alert = TaggingAlert(uid, pid)

    # Subscribe tagees
    tagees = parse_tags(body)
    tagees_to_alert = []
    for tagee in tagees:
        # Don't allow tagging yourself
        if tagee[0] != uid:
            subscribe(tagee[0], pid, SubscriptionReasons.TAGEE)
            tagees_to_alert.append(tagee[0])

    # Get an alert manager to notify all tagees
    AlertManager().alert(alert, tagees_to_alert)

    return cid
Пример #12
0
def process_upload(upload,
                   collection='uploads',
                   image_size=(1280, 720),
                   thumbnail=True):
    """Processes the uploaded images in the posts and also the users avatars.
    This should be extensible in future to support the uploading of videos,
    audio, etc...

    :param _id: The ID for the post, user or anything you want as the filename
    :type _id: str
    :param upload: The uploaded Werkzeug FileStorage object
    :type upload: ``Werkzeug.datastructures.FileStorage``
    :param collection: The GridFS collection to upload the file too.
    :type collection: str
    :param image_size: The max height and width for the upload
    :type image_size: Tuple length 2 of int
    :param thumbnail: Is the image to have it's aspect ration kept?
    :type thumbnail: bool
    """
    try:
        # StringIO to take the uploaded image to transport to GridFS
        output = io.BytesIO()

        # All images are passed through PIL and turned in to PNG files.
        # Will change if they need thumbnailing or resizing.
        img = PILImage.open(upload)

        # Check the exif data.
        # If there is an orientation then transform the image so that
        # it is always looking up.
        try:
            exif_data = {
                ExifTags.TAGS[k]: v
                for k, v in img._getexif().items() if k in ExifTags.TAGS
            }

            orientation = exif_data.get('Orientation')

            if orientation:  # pragma: no branch
                if orientation == 2:
                    img = img.transpose(PILImage.FLIP_LEFT_RIGHT)
                if orientation == 3:
                    img = img.transpose(PILImage.ROTATE_180)
                elif orientation == 4:
                    img = img.transpose(PILImage.FLIP_TOP_BOTTOM)
                elif orientation == 5:
                    img = img.transpose(PILImage.FLIP_TOP_BOTTOM)
                    img = img.transpose(PILImage.ROTATE_270)
                elif orientation == 6:
                    img = img.transpose(PILImage.ROTATE_270)
                elif orientation == 7:
                    img = img.transpose(PILImage.FLIP_TOP_BOTTOM)
                    img = img.transpose(PILImage.ROTATE_90)
                elif orientation == 8:
                    img = img.transpose(PILImage.ROTATE_90)

        except AttributeError:
            pass

        if thumbnail:
            img.thumbnail(image_size, PILImage.ANTIALIAS)
        else:
            # Pillow `resize` returns an image unlike thumbnail
            img = img.resize(image_size, PILImage.ANTIALIAS)

        img.save(output, format='PNG', quality=100)

        # Return the file pointer to the start
        output.seek(0)

        # Create a new file name <uuid>.<upload_extension>
        filename = '{0}.{1}'.format(get_uuid(), 'png')

        # Place file inside GridFS
        m.save_file(filename, output, base=collection)

        return filename
    except (IOError):
        # File will not have been uploaded
        return None
Пример #13
0
 def __init__(self, user_id):
     self.alert_id = get_uuid()
     self.timestamp = timestamp()
     self.user_id = user_id