def list_flags(uid):
    """Receives a list of ids as JSON in the message body. Returns a list of flags (True/False) for each id."""
    try:
        ids = request.get_json()

        if not isinstance(ids, list):
            raise ValueError()
    except ValueError:
        abort_json(400)

    if len(ids) == 0:
        return {'flags': []}

    with engine().begin() as con:
        uflags = select([flags_.c.id, flags_.c.lead_id])\
            .where(flags_.c.user_id == uid).alias('uflags')

        query = select([leads.c.id, text('not isnull(uflags.id) as flagged')])\
            .select_from(leads.outerjoin(uflags))\
            .where(leads.c.id.in_(ids))

        res = con.execute(query)

        result = {row['id']: row['flagged'] for row in res}

        return {'flags': [result[id] if id in result else False for id in ids]}
Beispiel #2
0
def create_alert(uid):
    try:
        data = request.get_json()
        assert EMAIL_REGEX.fullmatch(data['recipient']) is not None
    except AssertionError:
        return abort_json(400, 'Unable to read or validate alert data')

    with engine().begin() as con:
        # before beginning, check if this email belongs to another existing users
        is_taken = email_taken(uid, data['recipient'], con)

        if is_taken:
            return is_taken

        # we're going to abuse the DB to do validation & type checking. the only exception is the email, which is validated above
        try:
            query = alerts_.insert().values(  # pylint: disable=no-value-for-parameter
                filter=data['filter'],
                recipient=data['recipient'],
                federal_source=data['sources'].get('federal', None),
                regional_source=data['sources'].get('regional', None),
                local_source=data['sources'].get('local', None),
                frequency=data['frequency'],
                user_id=uid
            )

            res = con.execute(query)

            assert len(res.inserted_primary_key) > 0

            alert_id = res.inserted_primary_key[0]
        except Exception as e:
            print(e)
            return abort_json(400, 'Unable to create alert in database')

        base = {'id': alert_id}
        try:
            conf_sent = send_confirmation(uid, data['recipient'], con)
        except ConfirmationPendingError:
            return {'notes': ['A confirmation for this recipient is already pending.'], **base}
        else:
            if conf_sent:
                return {'status': 'ok', 'notes': [CONFIRMATION_NOTE], **base}
            else:
                return {'status': 'ok', **base}
Beispiel #3
0
def email_taken(uid, email, con):
    query = select([confirmed_emails]).where(and_(
        confirmed_emails.c.email == email, not_(confirmed_emails.c.user_id == uid)))
    res = con.execute(query)

    if res.rowcount > 0:
        return abort_json(400, 'Email address is already claimed by another user.')

    return False
Beispiel #4
0
def resend_confirmation(alert_id, uid):
    with engine().begin() as con:
        query = select([alerts_]).where(
            and_(alerts_.c.id == alert_id, alerts_.c.user_id == uid))
        res = con.execute(query)

        if res.rowcount == 0:
            return abort_json(400, 'This email address is already confirmed.')

        alert = res.fetchone()

        try:
            send_confirmation(
                uid, alert['recipient'], con, min_delay=timedelta(minutes=5))
        except ConfirmationPendingError as err:
            return abort_json(400, err.message)
        else:
            return {'status': 'ok'}
Beispiel #5
0
def delete_alert(uid, alert_id):
    with engine().begin() as con:
        query = alerts_.delete().where(  # pylint: disable=no-value-for-parameter
            and_(alerts_.c.id == alert_id, alerts_.c.user_id == uid))
        res = con.execute(query)
        if res.rowcount == 0:
            return abort_json(404)

        return {'status': 'ok'}
def put_flag(uid, lead_id):
    with engine().begin() as con:
        query = flags_.insert().values(  # pylint: disable=no-value-for-parameter
            lead_id=lead_id, user_id=uid)

        try:
            res = con.execute(query)
            return {'status': 'ok', 'rows': res.rowcount}
        except IntegrityError:
            # invalid lead_id
            return abort_json(404)
Beispiel #7
0
def lookup_alert(uid, alert_id):
    with engine().begin() as con:
        query = select([alerts_])\
            .where(and_(alerts_.c.id == alert_id, alerts_.c.user_id == uid))
        res = con.execute(query)
        if res.rowcount == 0:
            # does not exist or no access
            return abort_json(404)

        response = format_alert(res.fetchone())
        response['confirmed'] = is_confirmed(uid, response['recipient'], con)
        return flask.jsonify(response)
Beispiel #8
0
def delete_alert_via_link():
    token = request.args.get('token', None)
    if token is None:
        return abort_json(400, 'Missing token')

    try:
        contents = read_private_alert_token(token)
        with engine().begin() as con:
            send_query = sent_alerts.select().where(and_(sent_alerts.c.id == contents['send'], sent_alerts.c.user_id == contents['user']))
            res = con.execute(send_query)
            sent = res.fetchone()
            # test (sqlite) database doesn't support CTEs on DELETEs so rather
            # than have this untested we're just going to use 2 queries. this
            # endpoint shouldn't be hit often so this cost should be low
            query = alerts_.delete().where(alerts_.c.id == sent['alert_id'])
            res = con.execute(query)

            if res.rowcount == 0:
                return abort_json(404, 'No such alert')
    except BadSignature:
        return abort_json(400, 'Invalid token')
    return {'status': 'ok'}
Beispiel #9
0
def confirm_email():
    token = request.args.get('token')
    serializer = URLSafeTimedSerializer(current_app.secret_key)
    try:
        # tokens only work for 24hr
        confirmation_id = serializer.loads(token,
                                           max_age=24 * 60 * 60,
                                           salt='confirm')
        with engine().begin() as con:
            res = con.execute(
                select([
                    pending_confirmations
                ]).where(pending_confirmations.c.id == confirmation_id))

            if res.rowcount == 0:
                raise NoSuchConfirmation(
                    'No pending confirmation for that address')

            confirmation = res.fetchone()

            con.execute(confirmed_emails.insert().values(  # pylint: disable=no-value-for-parameter
                user_id=confirmation['user_id'],
                email=confirmation['email']))

            con.execute(pending_confirmations.delete().where(  # pylint: disable=no-value-for-parameter
                and_(
                    pending_confirmations.c.user_id == confirmation['user_id'],
                    pending_confirmations.c.email == confirmation['email'])))

        return {'status': 'ok'}
    except NoSuchConfirmation as e:
        return abort_json(400, e.message)
    except BadSignature as e:
        print(e)
        return abort_json(
            400,
            'That token is invalid or has expired. You can request another confirmation email on the Alerts page.'
        )
Beispiel #10
0
def unsubscribe_all_alerts():
    token = request.args.get('token', None)
    if token is None:
        return abort_json(400, 'Missing token')

    try:
        contents = read_private_alert_token(token)
        with engine().begin() as con:
            send_query = sent_alerts.select().where(and_(sent_alerts.c.id == contents['send'], sent_alerts.c.user_id == contents['user']))
            res = con.execute(send_query)
            sent = res.fetchone()

            # see note in delete_alert_via_link
            query = confirmed_emails.delete().where(confirmed_emails.c.email == sent['recipient'])
            res = con.execute(query)
            query = alerts_.delete().where(alerts_.c.recipient == sent['recipient'])
            res = con.execute(query)

            if res.rowcount == 0:
                return abort_json(404, 'No such alert')
    except BadSignature:
        return abort_json(400, 'Invalid token')
    return {'status': 'ok'}
Beispiel #11
0
def update_alert(uid, alert_id):
    try:
        data = request.get_json()
        assert EMAIL_REGEX.fullmatch(data['recipient']) is not None
    except AssertionError:
        return abort_json(400, 'Unable to read or validate alert data')

    with engine().begin() as con:
        is_taken = email_taken(uid, data['recipient'], con)
        if is_taken:
            return is_taken

        query = alerts_.update().values(  # pylint: disable=no-value-for-parameter
            filter=data['filter'],
            recipient=data['recipient'],
            federal_source=data['sources'].get('federal', None),
            regional_source=data['sources'].get('regional', None),
            local_source=data['sources'].get('local', None),
            frequency=data['frequency']
        ).where(and_(alerts_.c.id == alert_id, alerts_.c.user_id == uid))

        res = con.execute(query)

        if res.rowcount == 0:
            return abort_json(404)

        try:
            conf_sent = send_confirmation(uid, data['recipient'], con)
        except ConfirmationPendingError:
            # confirmation email already pending
            return {'status': 'ok', 'notes': ['A confirmation email is pending.']}

        if conf_sent:
            return {'status': 'ok', 'notes': [CONFIRMATION_NOTE]}
        else:
            return {'status': 'ok'}
def get_lead(uid, lead_id):
    with engine().begin() as con:
        query = build_lead_selection(uid, where=[leads.c.id == lead_id])

        resultset = con.execute(query)
        result = resultset.fetchone()
        if result is not None:
            result = dict(result)

            # now we load comments for it
            ratings_query = select([crowd_ratings])\
                .where(crowd_ratings.c.lead_id == lead_id)
            ratings = con.execute(ratings_query)
            result['ratings'] = [dict(row) for row in ratings.fetchall()]
            return flask.jsonify(result)
        else:
            return abort_json(404, 'no such id')
Beispiel #13
0
def trigger_alerts():
    from api.api import build_filtered_lead_selection

    if request.remote_addr not in current_app.config['ALERT_TRIGGER_WHITELIST']:
        return abort_json(401, 'Unauthorized')

    freq = request.args.get('frequency', None)

    if freq is not None and freq not in FREQS:
        return abort_json(400, 'Invalid frequency specified.')

    with engine().connect() as con:
        # select all alerts where:
        # 1. the recipient email is confirmed
        # 2. the alert hasn't been sent in the current time period
        confirmed = select(
            [confirmed_emails.c.user_id, confirmed_emails.c.email]).cte()

        where = tuple_(alerts_.c.user_id, alerts_.c.recipient).in_(confirmed)
        if freq is not None:
            where = and_(where, alerts_.c.frequency == FREQS[freq])

        query = select([alerts_, func.max(sent_alerts.c.send_date).label('last_sent')])\
            .select_from(alerts_.outerjoin(sent_alerts, sent_alerts.c.alert_id == alerts_.c.id))\
            .where(where)\
            .group_by(alerts_.c.id)

        results = con.execute(query)

        # these results satisfy #1, but not #2 yet
        # however, we need to go row by row anyway because MySQL cannot match
        # on column values (only plaintext)
        for result in results:
            row = dict(result)
            if row['last_sent'] is not None and row['last_sent'] >= min_date_threshold(row['frequency']):
                # has been sent more recently than we allow
                print(
                    f"Last trigger for {row['id']} is too recent ({row['last_sent']}, {min_date_threshold(row['frequency'])})")
                continue
            with con.begin():
                query = build_filtered_lead_selection(
                    filter_=row['filter'],
                    from_=None,
                    to=None,
                    sources={
                        key: row[f"{key}_source"]
                        for key in ['federal', 'regional', 'local']
                    },
                    page=None,
                    fields=[leads.c.id, annotated_leads.c.name],
                    where=[
                        annotated_leads.c.published_dt >= min_date_threshold(
                            row['frequency'], fudge=timedelta(0))
                    ]
                )

                lead_results = con.execute(query)
                lead_results = list(dict(row) for row in lead_results)

                if len(lead_results) == 0:
                    print(f"Skipping alert {row['id']}. No new results.")
                    continue

                # record alert sending
                sent_alert = {
                    k: v
                    for k, v in row.items()
                    if k not in ['id', 'last_sent']
                }

                sent_alert['alert_id'] = row['id']
                sent_alert['send_date'] = datetime.now()
                sent_alert['db_link'] = build_db_url(sent_alert)

                query = sent_alerts.insert().values(  # pylint: disable=no-value-for-parameter
                    **sent_alert)

                res = con.execute(query)

                send_id = res.inserted_primary_key[0]

                sent_alert['send_id'] = send_id

                templates = render_alert(sent_alert, [{
                    'name': lead['name'],
                    'link': f'{BASE_URL}/lead/{lead["id"]}'
                } for lead in islice(lead_results, None)])

                sent_contents = [
                    {'send_id': send_id,
                     'lead_id': lead['id']}
                    for lead in lead_results
                ]

                con.execute(sent_alert_contents.insert(  # pylint: disable=no-value-for-parameter
                ), *sent_contents)

                send_alert(sent_alert, *templates)

    return {'status': 'ok'}