예제 #1
0
    def test_update_returning_str(self):
        q = PostgreSQLQuery.update(self.table_abc).where(
            self.table_abc.foo == 0).set('foo', 'bar').returning('id')

        self.assertEqual(
            'UPDATE "abc" SET "foo"=\'bar\' WHERE "foo"=0 RETURNING id',
            str(q))
예제 #2
0
    def update_game(self,
                    chat_id: int,
                    cur_player: int,
                    cur_color: str,
                    player_passed: List[bool],
                    board: List[List[GridPosition]],
                    last_placed_stone: Tuple[int, int],
                    last_capt_stone: Optional[Tuple[int, int]]) -> None:
        if not self.use_db:
            return

        clean_state: Dict[str, str] = {}
        for x in range(len(board)):
            for y in range(len(board[x])):
                if not board[x][y].free:
                    clean_state[f"{x},{y}"] = board[x][y].color

        last_capt_stone_str = ""
        if last_capt_stone is not None:
            last_capt_stone_str = f"{last_capt_stone[0]},{last_capt_stone[1]}"

        games = Table('games')
        update_game_query = PostgreSQLQuery.update(games) \
            .set(games.state, json.dumps(clean_state)) \
            .set(games.turn_color, cur_color) \
            .set(games.turn_player, cur_player) \
            .set(games.last_stone, f'{last_placed_stone[0]},{last_placed_stone[1]}') \
            .set(games.last_capt_stone, last_capt_stone_str) \
            .set(games.player_passed, f'{player_passed[0]},{player_passed[1]}') \
            .where(games.chat_id == chat_id)
        self._cur.execute(update_game_query.get_sql())
        self._conn.commit()
예제 #3
0
    def test_update_returning_str(self):
        q = (PostgreSQLQuery.update(self.table_abc).where(
            self.table_abc.foo == 0).set("foo", "bar").returning("id"))

        self.assertEqual(
            'UPDATE "abc" SET "foo"=\'bar\' WHERE "foo"=0 RETURNING "abc"."id"',
            str(q))
예제 #4
0
 def test_non_insert_on_conflict_do_update(self):
     with self.assertRaises(QueryException):
         query = (
             PostgreSQLQuery.update(self.table_abc)
             .set("foo", "bar")
             .on_conflict("id")
             .do_update(["name"], ["m"])
         )
예제 #5
0
    def test_update_returning_from_different_tables(self):
        table_bcd = Table('bcd')

        q = (PostgreSQLQuery.update(self.table_abc).from_(table_bcd).set(
            self.table_abc.lname,
            table_bcd.long_name).returning(self.table_abc.id, table_bcd.fname))
        self.assertEqual(
            'UPDATE "abc" SET "lname"="bcd"."long_name" FROM "bcd" RETURNING "abc"."id","bcd"."fname"',
            str(q))
예제 #6
0
def update_response(name: str,
                    change: models.ResponseEditArgs,
                    authorization: str = Header(None)):
    if len(change.edit_reason) < 5:
        return JSONResponse(status_code=422,
                            content={
                                'detail': {
                                    'loc': ['body', 'edit_reason']
                                },
                                'msg': 'minimum 5 characters',
                                'type': 'too_short'
                            })

    with LazyItgs(no_read_only=True) as itgs:
        authed, user_id = users_helper.check_permissions_from_header(
            itgs, authorization, 'responses')
        if not authed:
            return Response(status_code=403)
        users = Table('users')
        itgs.write_cursor.execute(
            Query.from_(users).select(
                users.id).where(users.id == Parameter('%s')).get_sql() +
            ' FOR SHARE', (user_id, ))
        row = itgs.write_cursor.fetchone()
        if row is None:
            itgs.write_conn.rollback()
            return Response(status_code=403)
        responses = Table('responses')
        itgs.write_cursor.execute(
            Query.from_(responses).select(
                responses.id, responses.response_body,
                responses.description).where(
                    responses.name == Parameter('%s')).get_sql() +
            ' FOR UPDATE', (name, ))
        row = itgs.write_cursor.fetchone()
        if row is None:
            itgs.write_conn.rollback()
            return Response(status_code=404)
        (resp_id, old_body, old_desc) = row
        resp_hists = Table('response_histories')
        itgs.write_cursor.execute(
            Query.into(resp_hists).columns(
                resp_hists.response_id, resp_hists.user_id, resp_hists.old_raw,
                resp_hists.new_raw, resp_hists.reason, resp_hists.old_desc,
                resp_hists.new_desc).insert(
                    *[Parameter('%s') for _ in range(7)]).get_sql(),
            (resp_id, user_id, old_body, change.body, change.edit_reason,
             old_desc, change.desc))
        itgs.write_cursor.execute(
            Query.update(responses).set(
                responses.response_body, Parameter('%s')).set(
                    responses.description, Parameter('%s')).set(
                        responses.updated_at, ppfns.Now()).where(
                            responses.id == Parameter('%s')).get_sql(),
            (change.body, change.desc, resp_id))
        itgs.write_conn.commit()
        return Response(status_code=200)
예제 #7
0
 async def update_chat(self, request, context, metadata):
     query = PostgreSQLQuery.update(self.chats_table)
     for field in request.DESCRIPTOR.fields:
         if field.containing_oneof and request.HasField(field.name):
             field_value = getattr(request, field.name)
             query = query.set(field.name, field_value)
     query = query.where(self.chats_table.chat_id ==
                         request.chat_id).returning('*').get_sql()
     async with self.pool_holder.pool.acquire() as session:
         result = await session.execute(query)
         chat = await result.fetchone()
     return self.enrich_chat(ChatPb(**chat))
예제 #8
0
    async def refreshSession(self, session: Session) -> Optional[Session]:
        sql = Query.update(sessions).set(sessions.updated_at, datetime.now(timezone.utc)) \
            .where(Criterion.all([
                sessions.session_id == session.sessionId,
                sessions.user_id == session.userId,
                sessions.updated_at == session.updatedAt
            ])) \
            .returning(sessions.session_id, sessions.user_id, sessions.updated_at)

        result = await databaseClient.query(sql.get_sql())
        if len(result) == 0: return None
        row = result[0]
        return Session(sessionId=row[0], userId=row[1], updatedAt=row[2])
예제 #9
0
 def generate_update_sql(
     self,
     scimag_pb: ScimagPb,
     fields: Optional[Set[str]] = None,
 ) -> str:
     query = (PostgreSQLQuery.update(self.scimag_table))
     fields = fields or self.db_fields
     for field_name in fields:
         if self.is_field_set(scimag_pb, field_name):
             field_value = getattr(scimag_pb, field_name)
             field_name, field_value = self.cast_field_value(
                 field_name, field_value)
             query = query.set(field_name, field_value)
     return query.where(self.scimag_table.id == scimag_pb.id).get_sql()
예제 #10
0
    def _get_query_update_sql(self, updatequery):
        values = []
        table = self.pika_table
        query = PostgreSQLQuery.update(table)
        i = 0

        criterion, where_values = self._expressions_to_criterion(
            updatequery.expressions, i)
        query = query.where(criterion)
        values.extend(where_values)
        i += len(where_values)

        for key, value in updatequery.update_kwargs.items():
            query = query.set(table[key], self.parameter(i))
            values.append(value)
            i += 1
        sql = str(query.get_sql())
        return sql, values
예제 #11
0
    def _get_update_sql(self,
                        instance,
                        update_fields,
                        condition_fields={}) -> str:
        """
        Generates the SQL for updating a model depending on provided update_fields.
        Result is cached for performance.
        """
        key, sql, values = self._get_update_cached(instance, update_fields,
                                                   condition_fields)
        if sql:
            return sql, values

        values = []
        table = self.pika_table
        query = PostgreSQLQuery.update(table)
        count = 0
        for field_name in update_fields or self.meta.fields_db_projection.keys(
        ):
            db_field = self.meta.fields_db_projection[field_name]
            field_object = self.meta.fields_map[field_name]
            if not field_object.pk:
                query = query.set(table[db_field], self.parameter(count))
                values.append(
                    field_object.to_db_value(getattr(instance, field_name)))
                count += 1

        query = query.where(
            table[self.meta.db_pk_field] == self.parameter(count))
        values.append(self.meta.pk.to_db_value(instance.pk))
        count += 1
        for k, v in condition_fields:
            query = query.where(table[k] == self.parameter(count))
            values.append(v)
            count += 1

        sql = self.update_cache[key] = str(query.get_sql())
        return sql, values
예제 #12
0
async def update_user(**data):
    userid = data['id']
    if data.get('password'):
        data['password'] = hash_password(data['password'], algorithm='argon2')

    if data.get('email'):
        data['email'] = data['email'].lower()

    db = await get_db()
    query = Query.update(users_table).where(users_table.id == userid)
    for key, value in data.items():
        if key in ('id', '@id'):
            continue
        if key in ('data', 'allowed_scopes'):
            value = json.dumps(value)
        try:
            query = query.set(getattr(users_table, key), value)
        except AttributeError:
            pass
    async with db.acquire() as conn:
        await conn.execute(str(query))

    await notify(UserModifiedEvent(userid, data))
    return data
예제 #13
0
def delete(id: int, reason_wrapped: models.Reason, authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    request_cost = 5
    with LazyItgs(no_read_only=True) as itgs:
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, authorization,
            (helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM,
             helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM,
             helper.CAN_MODIFY_OTHERS_AUTHENTICATION_METHODS_PERM,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429,
                            headers={'x-request-cost': str(request_cost)})

        if user_id is None:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        can_view_others_auth_methods = helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM in perms
        can_view_deleted_auth_methods = helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM in perms
        can_modify_others_auth_methods = (
            helper.CAN_MODIFY_OTHERS_AUTHENTICATION_METHODS_PERM in perms)

        auth_methods = Table('password_authentications')
        itgs.read_cursor.execute(
            Query.from_(auth_methods).select(
                auth_methods.human, auth_methods.deleted,
                auth_methods.user_id).where(
                    auth_methods.id == Parameter('%s')).get_sql(), (id, ))

        row = itgs.read_cursor.fetchone()
        if row is None:
            return Response(status_code=404,
                            headers={'x-request-cost': str(request_cost)})

        (human, deleted, auth_method_user_id) = row
        if deleted and not can_view_deleted_auth_methods:
            return Response(status_code=404,
                            headers={'x-request-cost': str(request_cost)})

        if auth_method_user_id != user_id and not can_view_others_auth_methods:
            return Response(status_code=404,
                            headers={'x-request-cost': str(request_cost)})

        if human:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        if auth_method_user_id != user_id and not can_modify_others_auth_methods:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        if deleted:
            return Response(status_code=409,
                            headers={'x-request-cost': str(request_cost)})

        itgs.write_cursor.execute(
            Query.update(auth_methods).set(
                auth_methods.deleted,
                True).where(auth_methods.id == Parameter('%s')).get_sql(),
            (id, ))

        auth_events = Table('password_authentication_events')
        itgs.write_cursor.execute(
            Query.into(auth_events).columns(
                auth_events.password_authentication_id, auth_events.type,
                auth_events.reason, auth_events.user_id,
                auth_events.permission_id).insert(
                    *[Parameter('%s') for _ in range(5)]).get_sql(),
            (id, 'deleted', reason_wrapped.reason, user_id, None))

        authtokens = Table('authtokens')
        itgs.write_cursor.execute(
            Query.from_(authtokens).delete().where(
                authtokens.source_type == Parameter('%s')).where(
                    authtokens.source_id == Parameter('%s')).get_sql(),
            ('password_authentication', id))
        itgs.write_conn.commit()
        return Response(status_code=200,
                        headers={'x-request-cost': str(request_cost)})
예제 #14
0
def try_handle_deprecated_call(
        itgs: LazyItgs,
        request: Request,
        endpoint_slug: str,
        user_id: int = None) -> Response:
    """Attempts to fully handle the deprecated call. If the underlying
    functionality on this call should not be provided then this will
    return the Response that should be given instead.

    Arguments:
    - `itgs (LazyItgs)`: The lazy integrations to use for connecting to
      networked components.
    - `request (Request)`: The underlying starlette request, which we will
      use for checking for shared query parameters like `deprecated`.
    - `endpoint_slug (str)`: The internal name we have for this
      endpoint, which will let us find the description, reason for deprecation,
      and list of alternatives in the database (main table: `endpoints`).
    - `user_id (int, None)`: If the request was authenticated in any way that
      could be recognized with `find_bearer_token`, and the token was valid,
      this should be the id of the authenticated user.

    Returns:
        - `resp (Response, None)`: If the response for the endpoint should be
          overriden, this is the response that should be used. Otherwise this
          is None.
    """
    endpoints = Table('endpoints')
    itgs.read_cursor.execute(
        Query.from_(endpoints).select(
            endpoints.id,
            endpoints.deprecated_on,
            endpoints.sunsets_on
        ).where(endpoints.slug == Parameter('%s'))
        .get_sql(),
        (endpoint_slug,)
    )
    row = itgs.read_cursor.fetchone()
    if row is None:
        return None

    host = URL(
        scope={
            'scheme': 'https',
            'server': (request.headers['x-real-host'], 443),
            'root_path': '/api',
            'path': request.url.path,
            'query_string': request.url.query.encode('utf-8'),
            'headers': {}
        }
    )
    ip_address = request.headers.get('x-real-ip', '')
    user_agent = request.headers.get('user-agent', '')

    (
        endpoint_id,
        deprecated_on,
        sunsets_on
    ) = row

    if deprecated_on is None:
        return None

    if sunsets_on is None:
        itgs.logger.print(
            Level.WARN,
            'The endpoint slug {} is deprecated but does not have a sunset '
            'date set! This should not happen; the maximum sunsetting time '
            'of 36 months will be assigned',
            endpoint_slug
        )
        itgs.write_cursor.execute(
            Query.update(endpoints)
            .set(
                endpoints.sunsets_on,
                Coalesce(endpoints.sunsets_on, Now() + Interval(months=36))
            )
            .where(endpoints.slug == Parameter('%s'))
            .returning(endpoints.sunsets_on)
            .get_sql(),
            (endpoint_slug,)
        )
        (sunsets_on,) = itgs.write_cursor.fetchone()
        itgs.write_conn.commit()

    curtime = datetime.utcnow()

    if curtime.date() < deprecated_on:
        return None

    # 2pm UTC = 10am est = 7am pst
    sunset_time = datetime(
        sunsets_on.year, sunsets_on.month, sunsets_on.day,
        14, tzinfo=curtime.tzinfo
    )

    if curtime >= sunset_time + timedelta(days=31):
        # No logging, can't be suppressed, provides no info
        if request.method not in ('GET', 'HEAD'):
            return Response(
                status_code=405,
                headers={
                    'Allow': 'GET, HEAD'
                }
            )

        return Response(
            status_code=404,
            headers=SUNSETTED_HEADERS
        )

    if curtime >= sunset_time:
        # No logging, can't be suppressed, provides info
        if request.method == 'HEAD':
            return Response(status_code=400)

        return JSONResponse(
            status_code=400,
            content={
                'deprecated': True,
                'sunsetted': True,
                'retryable': False,
                'error': (
                    'This endpoint has been deprecated since {} and was sunsetted on {}, '
                    'meaning that it can no longer be used. For the reason for deprecation '
                    'and how to migrate off, visit {}://{}/endpoints.html?slug={}'
                ).format(
                    deprecated_on.strftime('%B %d, %Y'),
                    sunsets_on.strftime('%B %d, %Y'),
                    host.scheme,
                    host.netloc,
                    endpoint_slug
                )
            },
            headers=SUNSETTED_HEADERS if request.method == 'GET' else None
        )

    if request.query_params.get('deprecated') == 'true':
        # This flag suppresses all behavior before sunset, including logging
        return None

    if user_id is not None:
        ip_address = None
        user_agent = None

    if curtime >= sunset_time - timedelta(days=14):
        store_response(itgs, user_id, ip_address, user_agent, endpoint_id, 'error')
        return JSONResponse(
            status_code=400,
            content={
                'deprecated': True,
                'sunsetted': False,
                'retryable': False,
                'error': (
                    'This endpoint has been deprecated since {deprecated_on} and will sunset '
                    'on {sunsets_on}. For the reason for deprecation and how to migrate off, '
                    'visit {scheme}://{netloc}/endpoints.html?slug={slug}. To continue using '
                    'this endpoint until {sunsets_on} you must acknowledge this warning by '
                    'setting the query parameter "deprecated" with the value "true". For '
                    'example: {scheme}://{netloc}{path}?{query_params}{opt_ambersand}'
                    'deprecated=true{opt_hashtag}{fragment}'
                ).format(
                    deprecated_on=deprecated_on.strftime('%B %d, %Y'),
                    sunsets_on=sunsets_on.strftime('%B %d, %Y'),
                    scheme=host.scheme,
                    netloc=host.netloc,
                    path=host.path,
                    query_params=host.query_params,
                    opt_ambersand='' if host.query_params == '' else '&',
                    opt_hashtag='' if host.fragment == '' else '#',
                    fragment=host.fragment,
                    slug=endpoint_slug
                )
            },
            headers={
                'Cache-Control': 'no-store'
            }
        )

    if user_id is None:
        # We will error them if they have <5 errors this month or it's
        # within 30 days of sunsetting and they have received <5 errors
        # this week
        endpoint_users = Table('endpoint_users')
        std_query = (
            Query.from_(endpoint_users)
            .select(Count(Star()))
            .where(endpoint_users.ip_address == Parameter('%s'))
            .where(endpoint_users.user_agent == Parameter('%s'))
            .where(endpoint_users.response_type == Parameter('%s'))
            # notnull ensure postgres uses matching index
            .where(endpoint_users.ip_address.notnull())
            .where(endpoint_users.user_agent.notnull())
        )
        std_args = [
            ip_address,
            user_agent,
            'error'
        ]
        itgs.read_cursor.execute(
            std_query
            .where(endpoint_users.created_at > DateTrunc('month', Now()))
            .get_sql(),
            std_args
        )
        (errors_this_month,) = itgs.read_cursor.fetchone()

        should_error = errors_this_month < 5
        if not should_error and curtime >= sunset_time - timedelta(days=30):
            itgs.read_cursor.execute(
                std_query
                .where(endpoint_users.created_at > Now() - Interval(days=7))
                .get_sql(),
                std_args
            )
            (errors_this_week,) = itgs.read_cursor.fetchone()
            should_error = errors_this_week < 5

        if should_error:
            store_response(itgs, None, ip_address, user_agent, endpoint_id, 'error')
            return JSONResponse(
                status_code=400,
                content={
                    'deprecated': True,
                    'sunsetted': False,
                    'retryable': True,
                    'error': (
                        'This endpoint has been deprecated since {deprecated_on} and will '
                        'sunset on {sunsets_on}. Since your request is not authenticated '
                        'the only means to alert you of the sunset date is to fail some of '
                        'your requests. You may pass the query parameter `deprecated=true` '
                        'to suppress this behavior. We will only fail 5 requests per month '
                        'until it gets closer to the sunset date.\n\n'
                        'Check {scheme}://{netloc}/endpoints.html?slug={slug} for information '
                        'about why this endpoint was deprecated and how to migrate.'
                    ).format(
                        deprecated_on=deprecated_on.strftime('%B %d, %Y'),
                        sunsets_on=sunsets_on.strftime('%B %d, %Y'),
                        scheme=host.scheme,
                        netloc=host.netloc,
                        slug=endpoint_slug
                    )
                },
                headers={
                    'Cache-Control': 'no-store'
                }
            )

    store_response(itgs, user_id, ip_address, user_agent, endpoint_id, 'passthrough')
    return None
예제 #15
0
 def test_non_insert_on_conflict_do_update(self):
     with self.assertRaises(QueryException):
         query = PostgreSQLQuery.update(self.table_abc).set(
             'foo', 'bar').on_conflict('id').do_update(['name'], ["m"])
예제 #16
0
 def test_non_insert_on_conflict_do_nothing(self):
     with self.assertRaises(QueryException):
         (PostgreSQLQuery.update(self.table_abc).set(
             "foo", "bar").on_conflict("id").do_nothing())
예제 #17
0
    def test_update_returning(self):
        q = PostgreSQLQuery.update(self.table_abc).where(
            self.table_abc.foo == 0
        ).set('foo', 'bar').returning(self.table_abc.id)

        self.assertEqual('UPDATE "abc" SET "foo"=\'bar\' WHERE "foo"=0 RETURNING id', str(q))
예제 #18
0
def update(loan_id: int,
           loan: edit_models.LoanBasicFields,
           dry_run: bool = False,
           dry_run_text: bool = False,
           if_match: str = Header(None),
           authorization: str = Header(None)):
    """Allows modifying the standard fields on a loan. Must provide an If-Match
    header which is the etag of the loan being modified.
    """
    if if_match is None:
        return Response(status_code=428)

    with LazyItgs(no_read_only=True) as itgs:
        has_perm, user_id = users.helper.check_permissions_from_header(
            itgs, authorization, (helper.EDIT_LOANS_PERMISSION, ))

        if not has_perm:
            return Response(status_code=403)

        etag = helper.calculate_etag(itgs, loan_id)
        if etag is None:
            return Response(status_code=410)

        if etag != if_match:
            return Response(status_code=412)

        loans = Table('loans')
        moneys = Table('moneys')
        principals = moneys.as_('principals')
        principal_repayments = moneys.as_('principal_repayments')
        currencies = Table('currencies')

        itgs.read_cursor.execute(
            Query.from_(loans).select(
                currencies.id, currencies.code).join(principals).on(
                    principals.id == loans.principal_id).join(currencies).on(
                        currencies.id == principals.currency_id).where(
                            loans.id == Parameter('%s')).get_sql(),
            (loan_id, ))
        (currency_id, currency_code) = itgs.read_cursor.fetchone()

        is_repaid = None
        if (loan.principal_minor is None) != (loan.principal_repayment_minor is
                                              None):
            itgs.read_cursor.execute(
                Query.from_(loans).select(
                    principals.amount,
                    principal_repayments.amount).join(principals).on(
                        principals.id == loans.principal_id).join(
                            principal_repayments).on(
                                principal_repayments.id ==
                                loans.principal_repayment_id).where(
                                    loans.id == Parameter('%s')).get_sql(),
                (loan_id, ))
            princ_amt, princ_repay_amt = itgs.read_cursor.fetchone()
            new_princ_amt = loan.principal_minor or princ_amt
            new_princ_repay_amt = loan.principal_repayment_minor or princ_repay_amt
            if new_princ_amt < new_princ_repay_amt:
                return JSONResponse(
                    status_code=422,
                    content={
                        'detail': {
                            'loc': [
                                'loan',
                                (loan.principal_minor is None
                                 and 'principal_minor'
                                 or 'principal_repayment_minor')
                            ],
                            'msg':
                            'Cannot have principal repayment higher than principal',
                            'type':
                            'value_error'
                        }
                    })
            is_repaid = new_princ_amt == new_princ_repay_amt
        elif loan.principal_minor is not None:
            is_repaid = loan.principal_minor == loan.principal_repayment_minor

        admin_events = Table('loan_admin_events')
        query = (Query.into(admin_events).columns(
            admin_events.loan_id, admin_events.admin_id, admin_events.reason,
            admin_events.old_principal_id, admin_events.new_principal_id,
            admin_events.old_principal_repayment_id,
            admin_events.new_principal_repayment_id,
            admin_events.old_created_at, admin_events.new_created_at,
            admin_events.old_repaid_at, admin_events.new_repaid_at,
            admin_events.old_unpaid_at, admin_events.new_unpaid_at,
            admin_events.old_deleted_at, admin_events.new_deleted_at))

        query = (query.from_(loans).select(
            Parameter('$1'), Parameter('$2'),
            Parameter('$3')).where(loans.id == Parameter('$1')))
        query_params = [loan_id, user_id, loan.reason]

        update_query = (Query.update(loans).where(loans.id == Parameter('$1')))
        update_params = [loan_id]

        # Principal
        query = query.select(loans.principal_id)
        if loan.principal_minor is None:
            query = query.select(loans.principal_id)
        else:
            usd_amount = (
                loan.principal_minor *
                (1 / lbshared.convert.convert(itgs, 'USD', currency_code)))
            itgs.write_cursor.execute(
                Query.into(moneys).columns(
                    moneys.currency_id, moneys.amount,
                    moneys.amount_usd_cents).insert(
                        *[Parameter('%s')
                          for _ in range(3)]).returning(moneys.id).get_sql(),
                (currency_id, loan.principal_minor, usd_amount))
            (new_principal_id, ) = itgs.write_cursor.fetchone()
            query = query.select(Parameter(f'${len(query_params) + 1}'))
            query_params.append(new_principal_id)

            update_query = update_query.set(
                loans.principal_id, Parameter(f'${len(update_params) + 1}'))
            update_params.append(new_principal_id)

        # Principal Repayment
        query = query.select(loans.principal_repayment_id)
        if loan.principal_repayment_minor is None:
            query = query.select(loans.principal_repayment_id)
        else:
            usd_amount = (
                loan.principal_repayment_minor *
                (1 / lbshared.convert.convert(itgs, 'USD', currency_code)))
            itgs.write_cursor.execute(
                Query.into(moneys).columns(
                    moneys.currency_id, moneys.amount,
                    moneys.amount_usd_cents).insert(
                        *[Parameter('%s')
                          for _ in range(3)]).returning(moneys.id).get_sql(),
                (currency_id, loan.principal_repayment_minor, usd_amount))
            (new_principal_repayment_id, ) = itgs.write_cursor.fetchone()
            query = query.select(Parameter(f'${len(query_params) + 1}'))
            query_params.append(new_principal_repayment_id)

            update_query = update_query.set(
                loans.principal_repayment_id,
                Parameter(f'${len(update_params) + 1}'))
            update_params.append(new_principal_repayment_id)

        # Created At
        query = query.select(loans.created_at)
        if loan.created_at is None:
            query = query.select(loans.created_at)
        else:
            new_created_at = datetime.fromtimestamp(loan.created_at)
            query = query.select(Parameter(f'${len(query_params) + 1}'))
            query_params.append(new_created_at)

            update_query = update_query.set(
                loans.created_at, Parameter(f'${len(update_params) + 1}'))
            update_params.append(new_created_at)

        # Repaid
        query = query.select(loans.repaid_at)
        if is_repaid is None:
            query = query.select(loans.repaid_at)
        else:
            query = query.select(Parameter(f'${len(query_params) + 1}'))
            update_query = update_query.set(
                loans.repaid_at, Parameter(f'${len(update_params) + 1}'))
            if is_repaid:
                val = datetime.now()
                query_params.append(val)
                update_params.append(val)
            else:
                query_params.append(None)
                update_params.append(None)

        # Unpaid
        query = query.select(loans.unpaid_at)
        if is_repaid:
            query = query.select(Parameter(f'${len(query_params) + 1}'))
            update_query = update_query.set(
                loans.unpaid_at, Parameter(f'${len(update_params) + 1}'))
            query_params.append(None)
            update_params.append(None)
        elif loan.unpaid is None:
            query = query.select(loans.unpaid_at)
        else:
            query = query.select(Parameter(f'${len(query_params) + 1}'))
            update_query = update_query.set(
                loans.unpaid_at, Parameter(f'${len(update_params) + 1}'))
            if loan.unpaid:
                val = datetime.now()
                query_params.append(val)
                update_params.append(val)
            else:
                query_params.append(None)
                update_params.append(None)

        # Deleted
        query = query.select(loans.deleted_at)
        if loan.deleted is None:
            query = query.select(loans.deleted_at)
        else:
            query = query.select(Parameter(f'${len(query_params) + 1}'))
            update_query = update_query.set(
                loans.deleted_at, Parameter(f'${len(update_params) + 1}'))
            if loan.deleted:
                val = datetime.now()
                query_params.append(val)
                update_params.append(val)
            else:
                query_params.append(None)
                update_params.append(None)

        admin_event_insert_sql, admin_event_insert_params = (
            lbshared.queries.convert_numbered_args(query.get_sql(),
                                                   query_params))
        update_loan_sql, update_loan_params = (
            lbshared.queries.convert_numbered_args(update_query.get_sql(),
                                                   update_params))
        itgs.write_cursor.execute(admin_event_insert_sql,
                                  admin_event_insert_params)

        if update_loan_sql.strip():
            itgs.write_cursor.execute(update_loan_sql, update_loan_params)

        if not dry_run:
            itgs.write_conn.commit()
            itgs.logger.print(Level.INFO,
                              'Admin user {} just modified loan {}', user_id,
                              loan_id)
            return Response(status_code=200)
        else:
            itgs.write_conn.rollback()

            fmtted_admin_event_insert_sql = sqlparse.format(
                admin_event_insert_sql, keyword_case='upper', reindent=True)
            fmtted_update_loan_sql = sqlparse.format(update_loan_sql,
                                                     keyword_case='upper',
                                                     reindent=True)
            if not dry_run_text:
                return JSONResponse(
                    status_code=200,
                    content={
                        'loan_id':
                        loan_id,
                        'loan':
                        loan.dict(),
                        'dry_run':
                        dry_run,
                        'dry_run_text':
                        dry_run_text,
                        'admin_event_insert_sql':
                        fmtted_admin_event_insert_sql,
                        'admin_event_insert_params':
                        jsonable_encoder(admin_event_insert_params),
                        'update_loan_sql':
                        fmtted_update_loan_sql,
                        'update_loan_params':
                        jsonable_encoder(update_loan_params)
                    })

            spaces = ' ' * 20
            return Response(status_code=200,
                            headers={'Content-Type': 'plain/text'},
                            content=("\n".join(
                                (line[20:] if line[:20] == spaces else line)
                                for line in f"""
                    loan_id: {loan_id},
                    loan:
                        principal_minor: {loan.principal_minor},
                        principal_repayment_minor: {loan.principal_repayment_minor},
                        unpaid: {loan.unpaid},
                        created_at: {loan.created_at}
                        deleted: {loan.deleted}
                        reason: {repr(loan.reason)}
                    admin_event_insert_sql:

                    {fmtted_admin_event_insert_sql}

                    admin_event_insert_params: {admin_event_insert_params}

                    update_loan_sql:

                    {fmtted_update_loan_sql}

                    update_loan_params:

                    {update_loan_params}
                    """.splitlines())))
예제 #19
0
def update_users(loan_id: int,
                 new_users: edit_models.ChangeLoanUsers,
                 if_match: str = Header(None),
                 authorization: str = Header(None)):
    """Allows modifying the users on a loan. Must provide an If-Match header
    which is the etag of the loan being modified.
    """
    if if_match is None:
        return Response(status_code=428)

    with LazyItgs(no_read_only=True) as itgs:
        has_perm, user_id = users.helper.check_permissions_from_header(
            itgs, authorization, (helper.EDIT_LOANS_PERMISSION, ))

        if not has_perm:
            return Response(status_code=403)

        etag = helper.calculate_etag(itgs, loan_id)
        if etag is None:
            return Response(status_code=410)

        if etag != if_match:
            return Response(status_code=412)

        usrs = Table('users')
        created_lender = False
        try:
            itgs.write_cursor.execute(
                Query.into(usrs).columns(usrs.username).insert(
                    Parameter('%s')).returning(usrs.id).get_sql(),
                (new_users.lender_name.lower(), ))
            created_lender = True
        except UniqueViolation:
            itgs.write_conn.rollback()
            itgs.write_cursor.execute(
                Query.from_(usrs).select(
                    usrs.id).where(usrs.username == Parameter('%s')).get_sql(),
                (new_users.lender_name.lower(), ))

        (lender_id, ) = itgs.write_cursor.fetchone()

        try:
            itgs.write_cursor.execute(
                Query.into(usrs).columns(usrs.username).insert(
                    Parameter('%s')).returning(usrs.id).get_sql(),
                (new_users.borrower_name.lower(), ))
        except UniqueViolation:
            itgs.write_conn.rollback()
            if created_lender:
                itgs.write_cursor.execute(
                    Query.into(usrs).columns(usrs.username).insert(
                        Parameter('%s')).returning(usrs.id).get_sql(),
                    (new_users.lender_name.lower(), ))
            else:
                # Slight race condition
                itgs.write_cursor.execute(
                    Query.from_(usrs).select(usrs.id).where(
                        usrs.username == Parameter('%s')).get_sql(),
                    (new_users.lender_name.lower(), ))
            (lender_id, ) = itgs.write_cursor.fetchone()
            itgs.write_cursor.execute(
                Query.from_(usrs).select(
                    usrs.id).where(usrs.username == Parameter('%s')).get_sql(),
                (new_users.borrower_name.lower(), ))

        (borrower_id, ) = itgs.write_cursor.fetchone()

        loans = Table('loans')
        itgs.write_cursor.execute(
            Query.into(loans).columns(
                loans.lender_id, loans.borrower_id, loans.principal_id,
                loans.principal_repayment_id, loans.created_at,
                loans.repaid_at, loans.unpaid_at,
                loans.deleted_at).from_(loans).select(
                    Parameter('%s'), Parameter('%s'), loans.principal_id,
                    loans.principal_repayment_id, loans.created_at,
                    loans.repaid_at, loans.unpaid_at, loans.deleted_at).where(
                        loans.id == Parameter('%s')).returning(
                            loans.id).get_sql(),
            (lender_id, borrower_id, loan_id))
        (new_loan_id, ) = itgs.write_cursor.fetchone()

        admin_events = Table('loan_admin_events')
        base_query = (Query.into(admin_events).columns(
            admin_events.loan_id, admin_events.admin_id, admin_events.reason,
            admin_events.old_principal_id, admin_events.new_principal_id,
            admin_events.old_principal_repayment_id,
            admin_events.new_principal_repayment_id,
            admin_events.old_created_at, admin_events.new_created_at,
            admin_events.old_repaid_at, admin_events.new_repaid_at,
            admin_events.old_unpaid_at, admin_events.new_unpaid_at,
            admin_events.old_deleted_at,
            admin_events.new_deleted_at).from_(loans).where(
                loans.id == Parameter('$1')))
        base_select_params = [
            loans.id,
            Parameter('$2'),
            Parameter('$3'), loans.principal_id, loans.principal_id,
            loans.principal_repayment_id, loans.principal_repayment_id,
            loans.created_at, loans.created_at, loans.repaid_at,
            loans.repaid_at, loans.unpaid_at, loans.unpaid_at,
            loans.deleted_at, loans.deleted_at
        ]
        base_args = [None, user_id, None]

        new_deleted_at = datetime.now()
        update_old_select_params = base_select_params.copy()
        update_old_select_params[-1] = new_deleted_at
        update_old_args = base_args.copy()
        update_old_args[0] = loan_id
        update_old_args[2] = ('This loan had the users changed. ' +
                              f'The new loan id is {new_loan_id}. ' +
                              'Do not modify this loan further.')
        itgs.write_cursor.execute(*lbshared.queries.convert_numbered_args(
            base_query.select(
                *update_old_select_params).get_sql(), update_old_args))

        itgs.write_cursor.execute(
            Query.update(loans).set(
                loans.deleted_at,
                new_deleted_at).where(loans.id == Parameter('%s')).get_sql(),
            (loan_id, ))

        creation_infos = Table('loan_creation_infos')
        itgs.write_cursor.execute(
            Query.into(creation_infos).columns(
                creation_infos.loan_id, creation_infos.type,
                creation_infos.mod_user_id).insert(Parameter('%s'),
                                                   Parameter('%s'),
                                                   Parameter('%s')).get_sql(),
            (new_loan_id, 1, user_id))

        base_args[0] = new_loan_id
        base_args[2] = (
            f'This loan was copied from loan {loan_id}. ' +
            'The users were changed during this operation. Reason: ' +
            new_users.reason)
        itgs.write_cursor.execute(*lbshared.queries.convert_numbered_args(
            base_query.select(*base_select_params).get_sql(), base_args))
        itgs.write_conn.commit()
        return JSONResponse(
            status_code=200,
            content=edit_models.SingleLoanResponse(loan_id=new_loan_id).dict())
예제 #20
0
    def handle_comment(self, itgs, comment, rpiden, rpversion):
        token_vals = PARSER.parse(comment['body'])
        lender_username = comment['author']
        borrower_username = token_vals[0]

        comment_permalink = 'https://www.reddit.com/comments/{}/redditloans/{}'.format(
            comment['link_fullname'][3:], comment['fullname'][3:])

        loans = Table('loans')
        lenders = Table('lenders')
        borrowers = Table('borrowers')
        itgs.write_cursor.execute(
            loan_format_helper.create_loans_query().where(
                lenders.username == Parameter('%s')).where(
                    borrowers.username == Parameter('%s')).where(
                        loans.unpaid_at.isnull()).where(
                            loans.repaid_at.isnull()).get_sql(),
            (lender_username.lower(), borrower_username.lower()))
        row = itgs.write_cursor.fetchone()

        affected_pre = []
        while row is not None:
            affected_pre.append(loan_format_helper.fetch_loan(row))
            row = itgs.write_cursor.fetchone()

        if affected_pre:
            itgs.write_cursor.execute(
                Query.update(loans).set(loans.unpaid_at, Now()).where(
                    loans.id.isin([Parameter('%s')
                                   for _ in affected_pre])).get_sql(),
                tuple(loan.id for loan in affected_pre))

            loan_unpaid_events = Table('loan_unpaid_events')
            itgs.write_cursor.execute(
                Query.into(loan_unpaid_events).columns(
                    loan_unpaid_events.loan_id,
                    loan_unpaid_events.unpaid).insert(
                        *[(Parameter('%s'), True)
                          for _ in affected_pre]).returning(
                              loan_unpaid_events.id).get_sql(),
                tuple(loan.id for loan in affected_pre))
            itgs.channel.exchange_declare('events', 'topic')
            row = itgs.write_cursor.fetchone()
            while row is not None:
                itgs.channel.basic_publish(
                    'events', 'loans.unpaid',
                    json.dumps({"loan_unpaid_event_id": row[0]}))
                row = itgs.write_cursor.fetchone()

            itgs.write_cursor.execute(
                loan_format_helper.create_loans_query().where(
                    loans.id.isin([Parameter('%s')
                                   for _ in affected_pre])).get_sql(),
                tuple(loan.id for loan in affected_pre))
            row = itgs.write_cursor.fetchone()
            affected_post = []
            while row is not None:
                affected_post.append(loan_format_helper.fetch_loan(row))
                row = itgs.write_cursor.fetchone()
        else:
            affected_post = []

        itgs.logger.print(Level.INFO,
                          '/u/{} marked {} loan{} sent to /u/{} unpaid at {}',
                          lender_username, len(affected_pre),
                          's' if len(affected_pre) != 1 else '',
                          borrower_username, comment_permalink)

        borrower_summary = loan_format_helper.get_and_format_all_or_summary(
            itgs, borrower_username)

        formatted_response = get_response(
            itgs,
            'unpaid',
            lender_username=lender_username,
            borrower_username=borrower_username,
            loans_before=loan_format_helper.format_loan_table(affected_pre),
            loans_after=loan_format_helper.format_loan_table(affected_post),
            borrower_summary=borrower_summary)

        utils.reddit_proxy.send_request(itgs, rpiden, rpversion,
                                        'post_comment', {
                                            'parent': comment['fullname'],
                                            'text': formatted_response
                                        })
예제 #21
0
def update_currency(loan_id: int,
                    new_currency: edit_models.ChangeLoanCurrency,
                    if_match: str = Header(None),
                    authorization: str = Header(None)):
    """Allows modifying the currency on a loan. Must provide an If-Match header
    which is the etag of the loan being modified.
    """
    if if_match is None:
        return Response(status_code=428)

    with LazyItgs(no_read_only=True) as itgs:
        has_perm, user_id = users.helper.check_permissions_from_header(
            itgs, authorization, (helper.EDIT_LOANS_PERMISSION, ))

        if not has_perm:
            return Response(status_code=403)

        etag = helper.calculate_etag(itgs, loan_id)
        if etag is None:
            return Response(status_code=410)

        if etag != if_match:
            return Response(status_code=412)

        loans = Table('loans')
        moneys = Table('moneys')
        currencies = Table('currencies')

        itgs.write_cursor.execute(
            Query.from_(currencies).select(currencies.id).where(
                currencies.code == Parameter('%s')).get_sql(),
            (new_currency.currency_code.upper(), ))
        row = itgs.write_cursor.fetchone()
        if row is None:
            return JSONResponse(status_code=422,
                                content={
                                    'detail': [{
                                        'loc': ['currency_code'],
                                        'msg':
                                        'Must be a recognized currency code',
                                        'type': 'value_error'
                                    }]
                                })
        (currency_id, ) = row

        rate_usd_to_currency = lbshared.convert.convert(
            itgs, 'USD', new_currency.currency_code.upper())

        principal_usd = round(new_currency.principal_minor /
                              rate_usd_to_currency)
        principal_repayment_usd = round(
            new_currency.principal_repayment_minor / rate_usd_to_currency)

        itgs.write_cursor.execute(
            Query.into(moneys).columns(
                moneys.currency_id, moneys.amount,
                moneys.amount_usd_cents).insert(
                    *[Parameter('%s')
                      for _ in range(3)]).returning(moneys.id).get_sql(),
            (currency_id, new_currency.principal_minor, principal_usd))
        (new_principal_id, ) = itgs.write_cursor.fetchone()

        itgs.write_cursor.execute(
            Query.into(moneys).columns(
                moneys.currency_id, moneys.amount,
                moneys.amount_usd_cents).insert(
                    *[Parameter('%s')
                      for _ in range(3)]).returning(moneys.id).get_sql(),
            (currency_id, new_currency.principal_repayment_minor,
             principal_repayment_usd))
        (new_principal_repayment_id, ) = itgs.write_cursor.fetchone()

        itgs.write_cursor.execute(
            Query.into(loans).columns(
                loans.lender_id, loans.borrower_id, loans.principal_id,
                loans.principal_repayment_id, loans.created_at,
                loans.repaid_at,
                loans.unpaid_at, loans.deleted_at).from_(loans).select(
                    loans.lender_id, loans.borrower_id, Parameter('%s'),
                    Parameter('%s'), loans.created_at, loans.repaid_at,
                    loans.unpaid_at, loans.deleted_at).where(
                        loans.id == Parameter('%s')).returning(
                            loans.id).get_sql(),
            (new_principal_id, new_principal_repayment_id, loan_id))
        (new_loan_id, ) = itgs.write_cursor.fetchone()

        admin_events = Table('loan_admin_events')
        query = (Query.into(admin_events).columns(
            admin_events.loan_id, admin_events.admin_id, admin_events.reason,
            admin_events.old_principal_id, admin_events.new_principal_id,
            admin_events.old_principal_repayment_id,
            admin_events.new_principal_repayment_id,
            admin_events.old_created_at, admin_events.new_created_at,
            admin_events.old_repaid_at, admin_events.new_repaid_at,
            admin_events.old_unpaid_at, admin_events.new_unpaid_at,
            admin_events.old_deleted_at,
            admin_events.new_deleted_at).from_(loans).select(
                loans.id, user_id, Parameter('%s'), loans.principal_id,
                loans.principal_id, loans.principal_repayment_id,
                loans.principal_repayment_id, loans.created_at,
                loans.created_at, loans.repaid_at, loans.repaid_at,
                loans.unpaid_at, loans.unpaid_at,
                loans.deleted_at).where(loans.id == Parameter('%s')))

        new_deleted_at = datetime.now()
        itgs.write_cursor.execute(
            query.select(Parameter('%s')).get_sql(),
            (f'This loan was copied to loan {new_loan_id} then deleted in ' +
             f'order to change the currency to {new_currency.currency_code.upper()}. '
             + 'Do not modify this loan further.', new_deleted_at, loan_id))
        itgs.write_cursor.execute(
            Query.update(loans).set(
                loans.deleted_at,
                Parameter('%s')).where(loans.id == Parameter('%s')).get_sql(),
            (new_deleted_at, loan_id))

        creation_infos = Table('loan_creation_infos')
        itgs.write_cursor.execute(
            Query.into(creation_infos).columns(
                creation_infos.loan_id, creation_infos.type,
                creation_infos.mod_user_id).insert(
                    *[Parameter('%s') for _ in range(3)]).get_sql(),
            (new_loan_id, 1, user_id))

        itgs.write_cursor.execute(
            query.select(loans.deleted_at).get_sql(),
            (f'This loan was copied from {loan_id} with the currency changed to '
             +
             f'{new_currency.currency_code.upper()}. Reason: {new_currency.reason}',
             new_loan_id))
        itgs.write_conn.commit()
        return JSONResponse(
            status_code=200,
            content=edit_models.SingleLoanResponse(loan_id=new_loan_id).dict())
예제 #22
0
    async def do(
            self,
            document_operation_pb: DocumentOperationPb) -> DocumentOperationPb:
        update_document_pb = document_operation_pb.update_document
        scitech_pb = update_document_pb.typed_document.scitech
        fields = update_document_pb.fields or self.db_fields

        conditions = []
        if scitech_pb.id:
            conditions.append(self.scitech_table.id == scitech_pb.id)
        if scitech_pb.libgen_id:
            conditions.append(
                self.scitech_table.libgen_id == scitech_pb.libgen_id)
        if scitech_pb.fiction_id:
            conditions.append(
                self.scitech_table.fiction_id == scitech_pb.fiction_id)
        if scitech_pb.doi:
            conditions.append(self.scitech_table.doi == scitech_pb.doi)
        # if scitech_pb.md5:
        # conditions.append(self.scitech_table.md5 == UuidFunction(scitech_pb.md5))

        if conditions:
            casted_conditions = conditions[0]
            for condition in conditions[1:]:
                casted_conditions = casted_conditions | condition
            sql = (PostgreSQLQuery.from_(self.scitech_table).select(
                functions.Count('*')).where(casted_conditions).get_sql())
            result = await self.pool_holder.execute(sql, fetch=True)
            count = result[0][0]

            if count > 1:
                raise ConflictError(scitech_pb, duplicates=[])

            if count == 1:
                query = PostgreSQLQuery.update(self.scitech_table)
                for field_name in fields:
                    if self.is_field_set(scitech_pb, field_name):
                        field_value = getattr(scitech_pb, field_name)
                        field_name, field_value = self.cast_field_value(
                            field_name, field_value)
                        query = query.set(field_name, field_value)
                sql = query.where(casted_conditions).returning(
                    'id', 'original_id').get_sql()
            else:
                columns = []
                inserts = []
                for field_name in fields:
                    if self.is_field_set(scitech_pb, field_name):
                        field_value = getattr(scitech_pb, field_name)
                        field_name, field_value = self.cast_field_value(
                            field_name, field_value)
                        columns.append(field_name)
                        inserts.append(field_value)
                query = (PostgreSQLQuery.into(self.scitech_table).columns(
                    *columns).insert(*inserts).on_conflict('libgen_id', 'doi'))
                for col, val in zip(columns, inserts):
                    query = query.do_update(col, val)
                sql = query.returning('id', 'original_id').get_sql()

            result = await self.pool_holder.execute(sql, fetch=True)
            scitech_pb.id, scitech_pb.original_id = result[0][
                0], result[0][1] or 0
            return document_operation_pb
예제 #23
0
 def test_non_insert_on_conflict_do_update(self):
     with self.assertRaises(QueryException):
         query = PostgreSQLQuery.update(self.table_abc).set('foo', 'bar').on_conflict(
               'id').do_update(['name'], ["m"])
예제 #24
0
def apply_repayment(itgs: LazyItgs, loan_id: int, amount: Money):
    """Applies up to the given amount of money to the given loan. This will
    convert the amount to the loans currency if necessary. This will return the
    primary key of the loan_repayment_events row that was created, the amount
    of money that was applied to the loan (which will be in the loan currency),
    and the amount of money which exceeded the remaining principal to be repaid
    on this loan (which will be in the provided currency).

    This does not commit anything and expects to be running with explicit
    commits, i.e., where this is running in a transaction which it does not
    itself commit.

    For consistency this will use the same conversion rate to USD for the loan
    as when the loan was initially created.

    Example:
        (repayment_event_id, amount_applied, amount_remaining) = apply_repayment(
            itgs, loan_id, amount
        )

    Raises:
        ValueError: If the loan does not exist, is already repaid or, the
          amount is 0.

    Arguments:
        itgs (LazyIntegrations): The lazy loaded networked services connector
        loan_id (int): The primary key of the loan to apply the repayment to
        amount (Money): The amount of money to apply toward this loan. This may
            be in any currency, although it will be converted to the loans
            currency.

    Returns:
        repayment_event_id (int): The primary key of of the loan repayment
            event that this created.
        amount_applied (Money): The amount of money that was applied toward
            the loan, in the loans currency.
        amount_remaining (Money): The amount of money that is remaining, in
            the same currency that the amount was given in.
    """
    tus.check(itgs=(itgs, LazyItgs),
              loan_id=(loan_id, int),
              amount=(amount, Money))
    if amount.minor <= 0:
        raise ValueError(
            f'Cannot apply {amount} toward a loan (only positive amounts can be applied)'
        )

    loans = Table('loans')
    moneys = Table('moneys')
    usrs = Table('users')
    principals = moneys.as_('principals')
    principal_repayments = moneys.as_('principal_repayments')
    currencies = Table('currencies')
    principal_currencies = currencies.as_('principal_currencies')
    lenders = usrs.as_('lenders')
    borrowers = usrs.as_('borrowers')

    itgs.write_cursor.execute(
        Query.from_(loans).select(
            lenders.id, lenders.username, borrowers.id, borrowers.username,
            principal_currencies.id, principal_currencies.code,
            principal_currencies.exponent, principal_currencies.symbol,
            principal_currencies.symbol_on_left, principals.amount,
            principals.amount_usd_cents, principal_repayments.id,
            principal_repayments.amount, loans.unpaid_at).join(principals).
        on(principals.id == loans.principal_id).join(principal_currencies).on(
            principal_currencies.id == principals.currency_id).
        join(principal_repayments).on(
            principal_repayments.id == loans.principal_repayment_id).join(
                lenders).on(lenders.id == loans.lender_id).join(borrowers).on(
                    borrowers.id == loans.borrower_id).where(
                        loans.id == Parameter('%s')).get_sql(), (loan_id, ))
    row = itgs.write_cursor.fetchone()
    if row is None:
        raise ValueError(f'Loan {loan_id} does not exist')

    (lender_user_id, lender_username, borrower_user_id, borrower_username,
     loan_currency_id, loan_currency, loan_currency_exp, loan_currency_symbol,
     loan_currency_symbol_on_left, principal_amount, principal_usd_cents,
     principal_repayment_id, principal_repayment_amount, unpaid_at) = row

    rate_loan_to_usd = (principal_amount / float(principal_usd_cents))

    if principal_amount == principal_repayment_amount:
        raise ValueError(f'Loan {loan_id} is already repaid')

    if loan_currency == amount.currency:
        loan_currency_amount = amount
    else:
        rate_given_to_loan = convert(itgs, amount.currency, loan_currency)
        loan_currency_amount = Money(
            int(math.ceil(amount.minor * rate_given_to_loan)),
            loan_currency,
            exp=loan_currency_exp,
            symbol=loan_currency_symbol,
            symbol_on_left=loan_currency_symbol_on_left)

    applied = Money(min(principal_amount - principal_repayment_amount,
                        loan_currency_amount.minor),
                    loan_currency,
                    exp=loan_currency_exp,
                    symbol=loan_currency_symbol,
                    symbol_on_left=loan_currency_symbol_on_left)
    applied_usd_cents = int(math.ceil(applied.minor / rate_loan_to_usd))

    if loan_currency == amount.currency:
        remaining = Money(amount.minor - applied.minor,
                          loan_currency,
                          exp=loan_currency_exp,
                          symbol=loan_currency_symbol,
                          symbol_on_left=loan_currency_symbol_on_left)
    else:
        applied_in_given_currency = int(
            math.ceil(applied.minor / rate_given_to_loan))
        remaining = Money(max(0, amount.minor - applied_in_given_currency),
                          amount.currency,
                          exp=amount.exp,
                          symbol=amount.symbol,
                          symbol_on_left=amount.symbol_on_left)

    repayment_event_money_id = utils.money_utils.find_or_create_money(
        itgs, applied, applied_usd_cents)

    loan_repayment_events = Table('loan_repayment_events')
    itgs.write_cursor.execute(
        Query.into(loan_repayment_events).columns(
            loan_repayment_events.loan_id,
            loan_repayment_events.repayment_id).insert(
                *[Parameter('%s') for _ in range(2)]).returning(
                    loan_repayment_events.id).get_sql(),
        (loan_id, repayment_event_money_id))
    (repayment_event_id, ) = itgs.write_cursor.fetchone()

    new_princ_repayment_amount = principal_repayment_amount + applied.minor
    new_princ_repayment_usd_cents = int(
        math.ceil(new_princ_repayment_amount / rate_loan_to_usd))
    new_princ_repayment_id = utils.money_utils.find_or_create_money(
        itgs,
        Money(new_princ_repayment_amount,
              loan_currency,
              exp=loan_currency_exp,
              symbol=loan_currency_symbol,
              symbol_on_left=loan_currency_symbol_on_left),
        new_princ_repayment_usd_cents)
    itgs.write_cursor.execute(
        Query.update(loans).set(
            loans.principal_repayment_id,
            Parameter('%s')).where(loans.id == Parameter('%s')).get_sql(),
        (new_princ_repayment_id, loan_id))

    if new_princ_repayment_amount == principal_amount:
        itgs.write_cursor.execute(
            Query.update(loans).set(loans.repaid_at, Now()).set(
                loans.unpaid_at,
                None).where(loans.id == Parameter('%s')).get_sql(),
            (loan_id, ))

        if unpaid_at is not None:
            loan_unpaid_events = Table('loan_unpaid_events')
            itgs.write_cursor.execute(
                Query.into(loan_unpaid_events).columns(
                    loan_unpaid_events.loan_id,
                    loan_unpaid_events.unpaid).insert(
                        *[Parameter('%s') for _ in range(2)]).get_sql(),
                (loan_id, False))

        itgs.channel.exchange_declare('events', 'topic')
        itgs.channel.basic_publish(
            'events', 'loans.paid',
            json.dumps({
                'loan_id': loan_id,
                'lender': {
                    'id': lender_user_id,
                    'username': lender_username
                },
                'borrower': {
                    'id': borrower_user_id,
                    'username': borrower_username
                },
                'amount': {
                    'minor': amount.minor,
                    'currency': amount.currency,
                    'exp': amount.exp,
                    'symbol': amount.symbol,
                    'symbol_on_left': amount.symbol_on_left
                },
                'was_unpaid': unpaid_at is not None
            }))

    return (repayment_event_id, applied, remaining)
예제 #25
0
def send_messages(version):
    with LazyIntegrations(logger_iden=LOGGER_IDEN) as itgs:
        itgs.logger.print(Level.TRACE,
                          'Sending moderator onboarding messages...')

        mod_onboarding_messages = Table('mod_onboarding_messages')
        itgs.read_cursor.execute(
            Query.from_(mod_onboarding_messages).select(
                Max(mod_onboarding_messages.msg_order)).get_sql())
        (max_msg_order, ) = itgs.read_cursor.fetchone()
        if max_msg_order is None:
            itgs.logger.print(Level.DEBUG,
                              'There are no moderator onboarding messages.')
            return

        mod_onboarding_progress = Table('mod_onboarding_progress')
        moderators = Table('moderators')
        users = Table('users')
        itgs.read_cursor.execute(
            Query.from_(moderators).join(users).on(
                users.id == moderators.user_id).
            left_join(mod_onboarding_progress).on(
                mod_onboarding_progress.moderator_id == moderators.id).select(
                    users.id, moderators.id, users.username,
                    mod_onboarding_progress.msg_order).where(
                        mod_onboarding_progress.msg_order.isnull()
                        | (mod_onboarding_progress.msg_order < Parameter('%s'))
                    ).get_sql(), (max_msg_order, ))
        rows = itgs.read_cursor.fetchall()

        responses = Table('responses')
        titles = responses.as_('titles')
        bodies = responses.as_('bodies')
        for (user_id, mod_id, username, cur_msg_order) in rows:
            itgs.read_cursor.execute(
                Query.from_(mod_onboarding_messages).join(titles).on(
                    titles.id == mod_onboarding_messages.title_id).join(bodies)
                .on(bodies.id == mod_onboarding_messages.body_id).select(
                    mod_onboarding_messages.msg_order, titles.id, titles.name,
                    bodies.id, bodies.name).where(
                        Parameter('%s').isnull()
                        | (mod_onboarding_messages.msg_order > Parameter('%s'))
                    ).orderby(mod_onboarding_messages.msg_order,
                              order=Order.asc).limit(1).get_sql(), (
                                  cur_msg_order,
                                  cur_msg_order,
                              ))
            (new_msg_order, title_id, title_name, body_id,
             body_name) = itgs.read_cursor.fetchone()
            title_formatted = get_response(itgs, title_name, username=username)
            body_formatted = get_response(itgs, body_name, username=username)
            utils.reddit_proxy.send_request(
                itgs, 'mod_onboarding_messages', version, 'compose', {
                    'recipient': username,
                    'subject': title_formatted,
                    'body': body_formatted
                })
            utils.mod_onboarding_utils.store_letter_message_with_id_and_names(
                itgs, user_id, title_id, title_name, body_id, body_name)
            if cur_msg_order is None:
                itgs.write_cursor.execute(
                    Query.into(mod_onboarding_progress).columns(
                        mod_onboarding_progress.moderator_id,
                        mod_onboarding_progress.msg_order).insert(
                            *(Parameter('%s') for _ in range(2))).get_sql(),
                    (mod_id, new_msg_order))
            else:
                itgs.write_cursor.execute(
                    Query.update(mod_onboarding_progress).set(
                        mod_onboarding_progress.msg_order,
                        Parameter('%s')).set(
                            mod_onboarding_progress.updated_at,
                            CurTimestamp()).where(
                                mod_onboarding_progress.moderator_id ==
                                Parameter('%s')).get_sql(),
                    (new_msg_order, mod_id))
            itgs.write_conn.commit()
            itgs.logger.print(
                Level.INFO,
                'Successfully sent moderator onboarding message (msg_order={}) to /u/{}',
                new_msg_order, username)
def destroy(req_user_id: int, captcha: str, authorization=Header(None)):
    """Purges our accessible stores of the given users demographics. This
    operation is not reversible and will destroy our history of who knew what
    about this user. We will not allow the user to submit further information
    once they do this.

    This should only be done in extreme circumstances, or by the users request.
    We would much prefer users update their demographic information to all
    blanks, which will _also_ prevent anyone from using the website to access
    the information, but preserves the history in the database.

    If the users information is already purged this returns 451. If the
    user does not have demographic information, we create them an all blank
    record and mark it as purged to ensure they cannot submit new information,
    for consistency.
    """
    if authorization is None:
        return Response(status_code=401)

    attempt_request_cost = 5
    success_request_cost = 95
    with LazyItgs(no_read_only=True) as itgs:
        auth = demographics_helper.get_failure_response_or_user_id_and_perms_for_authorization(
            itgs, authorization, attempt_request_cost, req_user_id,
            demographics_helper.PURGE_SELF_DEMOGRAPHICS_PERMISSION,
            demographics_helper.PURGE_OTHERS_DEMOGRAPHICS_PERMISSION, [])

        if isinstance(auth, Response):
            return auth

        (user_id, perms) = auth

        headers = {'x-request-cost': str(attempt_request_cost)}
        if not security.verify_captcha(itgs, captcha):
            return Response(status_code=403, headers=headers)

        headers['x-request-cost'] = str(attempt_request_cost +
                                        success_request_cost)
        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                success_request_cost):
            return Response(status_code=429, headers=headers)

        demos = Table('user_demographics')
        itgs.read_cursor.execute(
            Query.from_(demos).select(demos.id, demos.deleted).where(
                demos.user_id == Parameter('%s')).get_sql(), (req_user_id, ))
        row = itgs.read_cursor.fetchone()
        if row is not None and row[1]:
            return Response(status_code=451, headers=headers)

        if row is None:
            users = Table('users')
            itgs.read_cursor.execute(
                Query.from_(users).select(1).where(
                    users.id == Parameter('%s')).limit(1).get_sql(),
                (req_user_id, ))
            if itgs.read_cursor.fetchone() is None:
                return Response(status_code=404, headers=headers)

            itgs.write_cursor.execute(
                Query.into(demos).columns(
                    demos.user_id,
                    demos.deleted).insert(*[Parameter('%s')
                                            for _ in range(2)]).get_sql(),
                (req_user_id, True))
            itgs.write_conn.commit()
            return Response(status_code=200, headers=headers)

        demo_id = row[0]

        demo_history = Table('user_demographic_history')
        itgs.write_cursor.execute(
            Query.update(demo_history).set(demo_history.old_email, None).set(
                demo_history.new_email,
                None).set(demo_history.old_name, None).set(
                    demo_history.new_name,
                    None).set(demo_history.old_street_address, None).set(
                        demo_history.new_street_address,
                        None).set(demo_history.old_city, None).set(
                            demo_history.new_city,
                            None).set(demo_history.old_state, None).set(
                                demo_history.new_state,
                                None).set(demo_history.old_zip, None).set(
                                    demo_history.new_zip, None).set(
                                        demo_history.old_country, None).set(
                                            demo_history.new_country,
                                            None).set(demo_history.purged_at,
                                                      Now()).
            where(
                demo_history.user_demographic_id == Parameter('%s')).get_sql(),
            (demo_id, ))
        itgs.write_cursor.execute(
            Query.into(demo_history).columns(
                demo_history.user_demographic_id,
                demo_history.changed_by_user_id, demo_history.old_deleted,
                demo_history.new_deleted, demo_history.purged_at).insert(
                    *[Parameter('%s') for _ in range(4)], Now()).get_sql(),
            (demo_id, user_id, False, True))
        itgs.write_cursor.execute(
            Query.update(demos).set(demos.email, None).set(
                demos.name, None).set(demos.street_address, None).set(
                    demos.city, None).set(demos.state, None).set(
                        demos.zip, None).set(demos.country, None).set(
                            demos.deleted,
                            True).where(demos.id == Parameter('%s')).get_sql(),
            (demo_id, ))
        itgs.write_conn.commit()
        return Response(status_code=200, headers=headers)
예제 #27
0
def change_password(id: int,
                    args: models.ChangePasswordParams,
                    authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    # In reality this is probably closer to ~10000, but we want to make sure
    # everyone could actually save enough tokens to do this. To avoid user error,
    # we will split the ratelimit cost into a pre- and post- part
    check_request_cost = 5
    perform_request_cost = 295
    with LazyItgs(no_read_only=True) as itgs:
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, authorization,
            (helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM,
             helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                check_request_cost):
            return Response(
                status_code=429,
                headers={'x-request-cost': str(check_request_cost)})

        if user_id is None:
            return Response(
                status_code=403,
                headers={'x-request-cost': str(check_request_cost)})

        can_view_others_auth_methods = helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM in perms
        can_view_deleted_auth_methods = helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM in perms

        auth_methods = Table('password_authentications')
        itgs.read_cursor.execute(
            Query.from_(auth_methods).select(
                auth_methods.user_id, auth_methods.deleted).where(
                    auth_methods.id == Parameter('%s')).get_sql(), (id, ))
        row = itgs.read_cursor.fetchone()

        if row is None:
            return Response(
                status_code=404,
                headers={'x-request-cost': str(check_request_cost)})

        (auth_method_user_id, deleted) = row
        if auth_method_user_id != user_id:
            if not can_view_others_auth_methods:
                return Response(
                    status_code=404,
                    headers={'x-request-cost': str(check_request_cost)})

            return Response(
                status_code=403,
                headers={'x-request-cost': str(check_request_cost)})

        if deleted:
            if not can_view_deleted_auth_methods:
                return Response(
                    status_code=404,
                    headers={'x-request-cost': str(check_request_cost)})

            return Response(
                status_code=403,
                headers={'x-request-cost': str(check_request_cost)})

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                perform_request_cost):
            return Response(status_code=429,
                            headers={
                                'x-request-cost':
                                str(check_request_cost + perform_request_cost)
                            })

        salt = secrets.token_urlsafe(23)  # 31 chars
        block_size = int(os.environ.get('NONHUMAN_PASSWORD_BLOCK_SIZE', '8'))
        dklen = int(os.environ.get('NONHUMAN_PASSWORD_DKLEN', '64'))

        # final number is MiB of RAM for the default
        iterations = int(
            os.environ.get('NONHUMAN_PASSWORD_ITERATIONS', str(
                (1024 * 8) * 64)))
        hash_name = f'scrypt-{block_size}-{dklen}'

        password_digest = b64encode(
            scrypt(
                args.password.encode('utf-8'),
                salt=salt.encode('utf-8'),
                n=iterations,
                r=block_size,
                p=1,
                maxmem=128 * iterations * block_size +
                1024 * 64,  # padding not necessary?
                dklen=dklen)).decode('ascii')

        itgs.write_cursor.execute(
            Query.update(auth_methods).set(
                auth_methods.hash_name,
                Parameter('%s')).set(auth_methods.hash, Parameter('%s')).set(
                    auth_methods.salt, Parameter('%s')).set(
                        auth_methods.iterations, Parameter('%s')).where(
                            auth_methods.id == Parameter('%s')).get_sql(),
            (hash_name, password_digest, salt, iterations, id))

        auth_events = Table('password_authentication_events')
        itgs.write_cursor.execute(
            Query.into(auth_events).columns(
                auth_events.password_authentication_id, auth_events.type,
                auth_events.reason, auth_events.permission_id,
                auth_events.user_id).insert(
                    *[Parameter('%s') for _ in range(5)]).get_sql(),
            (id, 'password-changed', args.reason, None, user_id))
        itgs.write_conn.commit()
        return Response(status_code=200,
                        headers={
                            'x-request-cost':
                            str(check_request_cost + perform_request_cost)
                        })
def update(req_user_id: int,
           captcha: str,
           args: demographics_models.UserDemographics,
           authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    if args.user_id != req_user_id:
        return JSONResponse(status_code=422,
                            content={
                                'detail': {
                                    'loc': ['body', 'user_id'],
                                    'msg':
                                    'must match path parameter req_user_id',
                                    'type': 'mismatch_error'
                                }
                            })

    attempt_request_cost = 5
    success_request_cost = 95
    with LazyItgs(no_read_only=True) as itgs:
        auth = demographics_helper.get_failure_response_or_user_id_and_perms_for_authorization(
            itgs, authorization, attempt_request_cost, req_user_id,
            demographics_helper.EDIT_SELF_DEMOGRAPHICS_PERMISSION,
            demographics_helper.EDIT_OTHERS_DEMOGRAPHICS_PERMISSION, [])

        if isinstance(auth, Response):
            return auth

        (user_id, perms) = auth

        headers = {'x-request-cost': str(attempt_request_cost)}
        if not security.verify_captcha(itgs, captcha):
            return Response(status_code=403, headers=headers)

        headers['x-request-cost'] = str(attempt_request_cost +
                                        success_request_cost)
        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                success_request_cost):
            return Response(status_code=429, headers=headers)

        demos = Table('user_demographics')
        itgs.read_cursor.execute(
            Query.from_(demos).select(demos.id, demos.deleted).where(
                demos.user_id == Parameter('%s')).limit(1).get_sql(),
            (req_user_id, ))
        row = itgs.read_cursor.fetchone()
        if row is None:
            users = Table('users')
            itgs.read_cursor.execute(
                Query.from_(users).select(1).where(
                    users.id == Parameter('%s')).limit(1).get_sql(),
                (req_user_id, ))
            if itgs.read_cursor.fetchone() is None:
                return Response(status_code=404, headers=headers)

            itgs.write_cursor.execute(
                Query.into(demos).columns(demos.user_id).insert(
                    Parameter('%s')).returning(demos.id).get_sql(),
                (req_user_id, ))
            row = itgs.write_cursor.fetchone()

        demo_id = row[0]
        demo_history = Table('user_demographic_history')
        itgs.write_cursor.execute(
            Query.into(demo_history).columns(
                demo_history.user_demographic_id,
                demo_history.changed_by_user_id,
                demo_history.old_email,
                demo_history.new_email,
                demo_history.old_name,
                demo_history.new_name,
                demo_history.old_street_address,
                demo_history.new_street_address,
                demo_history.old_city,
                demo_history.new_city,
                demo_history.old_state,
                demo_history.new_state,
                demo_history.old_zip,
                demo_history.new_zip,
                demo_history.old_country,
                demo_history.new_country,
                demo_history.old_deleted,
                demo_history.new_deleted,
            ).from_(demos).select(
                demos.id,
                Parameter('%s'),
                demos.email,
                Parameter('%s'),
                demos.name,
                Parameter('%s'),
                demos.street_address,
                Parameter('%s'),
                demos.city,
                Parameter('%s'),
                demos.state,
                Parameter('%s'),
                demos.zip,
                Parameter('%s'),
                demos.country,
                Parameter('%s'),
                demos.deleted,  # This should be false (we just checked), but
                demos.deleted  # we store the value here just in case
            ).where(demos.id == Parameter('%s')).get_sql(),
            (user_id, args.email, args.name, args.street_address, args.city,
             args.state, args.zip, args.country, demo_id))
        itgs.write_cursor.execute(
            Query.update(demos).set(demos.email, Parameter('%s')).set(
                demos.name, Parameter('%s')).set(
                    demos.street_address,
                    Parameter('%s')).set(demos.city, Parameter('%s')).set(
                        demos.state,
                        Parameter('%s')).set(demos.zip, Parameter('%s')).set(
                            demos.country, Parameter('%s')).where(
                                demos.id == Parameter('%s')).get_sql(),
            (args.email, args.name, args.street_address, args.city, args.state,
             args.zip, args.country, demo_id))
        itgs.write_conn.commit()
        return Response(status_code=200, headers=headers)