def validate_auth_header(*, optional: bool = False, fetch_email: bool = False):
    """ Examine the current request headers for an Authorization: Token <uuid>
        header that identifies a LB user and then load the corresponding user
        object from the database and return it, if succesful. Otherwise raise
        APIUnauthorized() exception.

    Args:
        optional: If the optional flag is given, do not raise an exception
            if the Authorization header is not set.
        fetch_email: if True, include email in the returned dict
    """

    auth_token = request.headers.get('Authorization')
    if not auth_token:
        if optional:
            return None
        raise APIUnauthorized("You need to provide an Authorization header.")
    try:
        auth_token = auth_token.split(" ")[1]
    except IndexError:
        raise APIUnauthorized("Provided Authorization header is invalid.")

    user = db_user.get_by_token(auth_token, fetch_email=fetch_email)
    if user is None:
        raise APIUnauthorized("Invalid authorization token.")

    return user
Example #2
0
def _validate_auth_header():
    auth_token = request.headers.get('Authorization')
    if not auth_token:
        raise APIUnauthorized("You need to provide an Authorization header.")
    try:
        auth_token = auth_token.split(" ")[1]
    except IndexError:
        raise APIUnauthorized("Provided Authorization header is invalid.")

    user = db_user.get_by_token(auth_token)
    if user is None:
        raise APIUnauthorized("Invalid authorization token.")

    return user
def delete_feed_events(user_name):
    '''
    Delete those events from user's feed that belong to them.
    Supports deletion of recommendation and notification.
    Along with the authorization token, post the event type and event id.
    For example:

    .. code-block:: json

        {
            "event_type": "recording_recommendation",
            "id": "<integer id of the event>"
        }

    .. code-block:: json

        {
            "event_type": "notification",
            "id": "<integer id of the event>"
        }

    :param user_name: The MusicBrainz ID of the user from whose timeline events are being deleted
    :type user_name: ``str``
    :statuscode 200: Successful deletion
    :statuscode 400: Bad request, check ``response['error']`` for more details.
    :statuscode 401: Unauthorized
    :statuscode 404: User not found
    :statuscode 500: API Internal Server Error
    :resheader Content-Type: *application/json*
    '''
    user = validate_auth_header()
    if user_name != user['musicbrainz_id']:
        raise APIUnauthorized(
            "You don't have permissions to delete from this user's timeline.")

    try:
        event = ujson.loads(request.get_data())

        if event["event_type"] in [
                UserTimelineEventType.RECORDING_RECOMMENDATION.value,
                UserTimelineEventType.NOTIFICATION.value
        ]:
            try:
                event_deleted = db_user_timeline_event.delete_user_timeline_event(
                    event["id"], user["id"])
            except Exception as e:
                raise APIInternalServerError(
                    "Something went wrong. Please try again")
            if not event_deleted:
                raise APINotFound(
                    "Cannot find '%s' event with id '%s' for user '%s'" %
                    (event["event_type"], event["id"], user["id"]))
            return jsonify({"status": "ok"})

        raise APIBadRequest(
            "This event type is not supported for deletion via this method")

    except (ValueError, KeyError) as e:
        raise APIBadRequest(f"Invalid JSON: {str(e)}")
Example #4
0
def create_user_recording_recommendation_event(user_name):
    """ Make the user recommend a recording to their followers.

    The request should post the following data about the recording being recommended:

    .. code-block:: json

        {
            "metadata": {
                "artist_name": "<The name of the artist, required>",
                "track_name": "<The name of the track, required>",
                "artist_msid": "<The MessyBrainz ID of the artist, required>",
                "recording_msid": "<The MessyBrainz ID of the recording, required>",
                "release_name": "<The name of the release, optional>",
                "recording_mbid": "<The MusicBrainz ID of the recording, optional>"
            }
        }


    :param user_name: The MusicBrainz ID of the user who is recommending the recording.
    :type user_name: ``str``
    :statuscode 200: Successful query, recording has been recommended!
    :statuscode 400: Bad request, check ``response['error']`` for more details.
    :statuscode 401: Unauthorized, you do not have permissions to recommend recordings on the behalf of this user
    :statuscode 404: User not found
    :resheader Content-Type: *application/json*
    """
    user = validate_auth_header()
    if user_name != user['musicbrainz_id']:
        raise APIUnauthorized(
            "You don't have permissions to post to this user's timeline.")

    try:
        data = ujson.loads(request.get_data())
    except ValueError as e:
        raise APIBadRequest(f"Invalid JSON: {str(e)}")

    try:
        metadata = RecordingRecommendationMetadata(**data['metadata'])
    except pydantic.ValidationError as e:
        raise APIBadRequest(f"Invalid metadata: {str(e)}")

    try:
        event = db_user_timeline_event.create_user_track_recommendation_event(
            user['id'], metadata)
    except DatabaseException:
        raise APIInternalServerError("Something went wrong, please try again.")

    event_data = event.dict()
    event_data['created'] = event_data['created'].timestamp()
    event_data['event_type'] = event_data['event_type'].value
    return jsonify(event_data)
Example #5
0
def save_list():
    creator = _validate_auth_header()
    raw_data = request.get_data()
    try:
        data = ujson.loads(raw_data.decode("utf-8"))
    except ValueError as e:
        log_raise_400("Cannot parse JSON document: %s" % str(e), raw_data)

    try:
        list_name = data['name']
        list_id = data['id']
        members = data['users']
    except KeyError as e:
        log_raise_400("JSON missing key: %s" % str(e))

    members = db_user.validate_usernames(members)
    if list_id is None:
        # create a new list
        try:
            list_id = db_follow_list.save(
                name=list_name,
                creator=creator['id'],
                members=[member['id'] for member in members],
            )
        except DatabaseException as e:
            raise APIForbidden("List with same name already exists.")
    else:

        # do some validation
        current_list = db_follow_list.get(list_id)
        if current_list is None:
            raise APINotFound("List not found: %d" % list_id)
        if current_list['creator'] != creator['id']:
            raise APIUnauthorized("You can only edit your own lists.")

        # update the old list
        db_follow_list.update(
            list_id=list_id,
            name=list_name,
            members=[member['id'] for member in members],
        )

    return jsonify({
        "code": 200,
        "message": "it worked!",
        "list_id": list_id,
    })
Example #6
0
def user_feed(user_name: str):
    """ Get feed events for a user's timeline.

    :param user_name: The MusicBrainz ID of the user whose timeline is being requested.
    :type user_name: ``str``
    :param max_ts: If you specify a ``max_ts`` timestamp, events with timestamps less than the value will be returned
    :param min_ts: If you specify a ``min_ts`` timestamp, events with timestamps greater than the value will be returned
    :param count: Optional, number of events to return. Default: :data:`~webserver.views.api.DEFAULT_ITEMS_PER_GET` . Max: :data:`~webserver.views.api.MAX_ITEMS_PER_GET`
    :statuscode 200: Successful query, you have feed events!
    :statuscode 400: Bad request, check ``response['error']`` for more details.
    :statuscode 401: Unauthorized, you do not have permission to view this user's feed.
    :statuscode 404: User not found
    :resheader Content-Type: *application/json*
    """

    user = validate_auth_header()
    if user_name != user['musicbrainz_id']:
        raise APIUnauthorized(
            "You don't have permissions to view this user's timeline.")

    db_conn = webserver.create_timescale(current_app)
    min_ts, max_ts, count, time_range = _validate_get_endpoint_params(
        db_conn, user_name)
    if min_ts is None and max_ts is None:
        max_ts = int(time.time())

    users_following = db_user_relationship.get_following_for_user(user['id'])

    # get all listen events
    musicbrainz_ids = [user['musicbrainz_id'] for user in users_following]
    if len(users_following) == 0:
        listen_events = []
    else:
        listen_events = get_listen_events(db_conn, musicbrainz_ids, min_ts,
                                          max_ts, count, time_range)

    # for events like "follow" and "recording recommendations", we want to show the user
    # their own events as well
    users_for_feed_events = users_following + [user]
    follow_events = get_follow_events(
        user_ids=tuple(user['id'] for user in users_for_feed_events),
        min_ts=min_ts or 0,
        max_ts=max_ts or int(time.time()),
        count=count,
    )

    recording_recommendation_events = get_recording_recommendation_events(
        users_for_events=users_for_feed_events,
        min_ts=min_ts or 0,
        max_ts=max_ts or int(time.time()),
        count=count,
    )

    notification_events = get_notification_events(user, count)

    # TODO: add playlist event and like event
    all_events = sorted(listen_events + follow_events +
                        recording_recommendation_events + notification_events,
                        key=lambda event: -event.created)

    # sadly, we need to serialize the event_type ourselves, otherwise, jsonify converts it badly
    for index, event in enumerate(all_events):
        all_events[index].event_type = event.event_type.value

    all_events = all_events[:count]

    return jsonify({
        'payload': {
            'count': len(all_events),
            'user_id': user_name,
            'events': [event.dict() for event in all_events],
        }
    })
Example #7
0
def submit_listen():
    """
    Submit listens to the server. A user token (found on  https://listenbrainz.org/profile/ ) must
    be provided in the Authorization header! Each request should also contain at least one listen
    in the payload.

    Listens should be submitted for tracks when the user has listened to half the track or 4 minutes of
    the track, whichever is lower. If the user hasn't listened to 4 minutes or half the track, it doesn't
    fully count as a listen and should not be submitted.

    For complete details on the format of the JSON to be POSTed to this endpoint, see :ref:`json-doc`.

    :reqheader Authorization: Token <user token>
    :reqheader Content-Type: *application/json*
    :statuscode 200: listen(s) accepted.
    :statuscode 400: invalid JSON sent, see error message for details.
    :statuscode 401: invalid authorization. See error message for details.
    :resheader Content-Type: *application/json*
    """
    user = validate_auth_header(fetch_email=True)
    if mb_engine and current_app.config[
            "REJECT_LISTENS_WITHOUT_USER_EMAIL"] and not user["email"]:
        raise APIUnauthorized(REJECT_LISTENS_WITHOUT_EMAIL_ERROR)

    raw_data = request.get_data()
    try:
        data = ujson.loads(raw_data.decode("utf-8"))
    except ValueError as e:
        log_raise_400("Cannot parse JSON document: %s" % e, raw_data)

    try:
        payload = data['payload']

        if not isinstance(payload, list):
            raise APIBadRequest(
                "The payload in the JSON document should be a list of listens.",
                payload)

        if len(payload) == 0:
            log_raise_400("JSON document does not contain any listens",
                          payload)

        if len(raw_data) > len(payload) * MAX_LISTEN_SIZE:
            log_raise_400(
                "JSON document is too large. In aggregate, listens may not "
                "be larger than %d characters." % MAX_LISTEN_SIZE, payload)

        if data['listen_type'] not in ('playing_now', 'single', 'import'):
            log_raise_400("JSON document requires a valid listen_type key.",
                          payload)

        listen_type = _get_listen_type(data['listen_type'])
        if (listen_type == LISTEN_TYPE_SINGLE or listen_type
                == LISTEN_TYPE_PLAYING_NOW) and len(payload) > 1:
            log_raise_400(
                "JSON document contains more than listen for a single/playing_now. "
                "It should contain only one.", payload)
    except KeyError:
        log_raise_400("Invalid JSON document submitted.", raw_data)

    try:
        # validate listens to make sure json is okay
        validated_payload = [
            validate_listen(listen, listen_type) for listen in payload
        ]
    except ListenValidationError as err:
        raise APIBadRequest(err.message, err.payload)

    user_metadata = SubmitListenUserMetadata(
        user_id=user['id'], musicbrainz_id=user['musicbrainz_id'])
    insert_payload(validated_payload, user_metadata, listen_type)

    return jsonify({'status': 'ok'})
Example #8
0
 def decorated(*args, **kwargs):
     if not current_user.is_authenticated:
         raise APIUnauthorized(
             "You must be logged in to access this endpoint")
     return f(*args, **kwargs)