mirror of https://gitlab.com/litecord/litecord.git
litecord.blueprints: split users bp into user sub-bp
- blueprints.user: add biling bp
- blueprints.user: add settings bp
- schema.sql: add user_payment_sources, user_subscriptions,
user_payments
This commit is contained in:
parent
49aa6cd495
commit
b9ef4c6d8c
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .billing import bp as user_billing
|
||||||
|
from .settings import bp as user_settings
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
import pprint
|
||||||
|
import json
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from quart import Blueprint, jsonify, request, current_app as app
|
||||||
|
|
||||||
|
from litecord.auth import token_check
|
||||||
|
from litecord.schemas import validate
|
||||||
|
from litecord.blueprints.checks import guild_check
|
||||||
|
from litecord.storage import timestamp_
|
||||||
|
from litecord.snowflake import snowflake_datetime, get_snowflake
|
||||||
|
|
||||||
|
bp = Blueprint('users_billing', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentSource(Enum):
|
||||||
|
CREDIT = 1
|
||||||
|
PAYPAL = 2
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionStatus:
|
||||||
|
ACTIVE = 1
|
||||||
|
CANCELLED = 3
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionType:
|
||||||
|
# unknown
|
||||||
|
PURCHASE = 1
|
||||||
|
UPGRADE = 2
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionPlan:
|
||||||
|
CLASSIC = 1
|
||||||
|
NITRO = 2
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentGateway:
|
||||||
|
STRIPE = 1
|
||||||
|
BRAINTREE = 2
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentStatus:
|
||||||
|
SUCCESS = 1
|
||||||
|
FAILED = 2
|
||||||
|
|
||||||
|
|
||||||
|
CREATE_SUBSCRIPTION = {
|
||||||
|
'payment_gateway_plan_id': {'type': 'string'},
|
||||||
|
'payment_source_id': {'coerce': int}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PAYMENT_SOURCE = {
|
||||||
|
'billing_address': {
|
||||||
|
'type': 'dict',
|
||||||
|
'schema': {
|
||||||
|
'country': {'type': 'string', 'required': True},
|
||||||
|
'city': {'type': 'string', 'required': True},
|
||||||
|
'name': {'type': 'string', 'required': True},
|
||||||
|
'line_1': {'type': 'string', 'required': False},
|
||||||
|
'line_2': {'type': 'string', 'required': False},
|
||||||
|
'postal_code': {'type': 'string', 'required': True},
|
||||||
|
'state': {'type': 'string', 'required': True},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'payment_gateway': {'type': 'number', 'required': True},
|
||||||
|
'token': {'type': 'string', 'required': True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_payment_source_ids(user_id: int) -> list:
|
||||||
|
rows = await app.db.fetch("""
|
||||||
|
SELECT id
|
||||||
|
FROM user_payment_sources
|
||||||
|
WHERE user_id = $1
|
||||||
|
""", user_id)
|
||||||
|
|
||||||
|
return [r['id'] for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_payment_ids(user_id: int) -> list:
|
||||||
|
rows = await app.db.fetch("""
|
||||||
|
SELECT id
|
||||||
|
FROM user_payments
|
||||||
|
WHERE user_id = $1
|
||||||
|
""", user_id)
|
||||||
|
|
||||||
|
return [r['id'] for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_subscription_ids(user_id: int) -> list:
|
||||||
|
rows = await app.db.fetch("""
|
||||||
|
SELECT id
|
||||||
|
FROM user_subscriptions
|
||||||
|
WHERE user_id = $1
|
||||||
|
""", user_id)
|
||||||
|
|
||||||
|
return [r['id'] for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_payment_source(user_id: int, source_id: int) -> dict:
|
||||||
|
"""Get a payment source's information."""
|
||||||
|
source = {}
|
||||||
|
|
||||||
|
source_type = await app.db.fetchval("""
|
||||||
|
SELECT source_type
|
||||||
|
FROM user_payment_sources
|
||||||
|
WHERE id = $1 AND user_id = $2
|
||||||
|
""", source_id, user_id)
|
||||||
|
|
||||||
|
source_type = PaymentSource(source_type)
|
||||||
|
|
||||||
|
specific_fields = {
|
||||||
|
PaymentSource.PAYPAL: ['paypal_email'],
|
||||||
|
PaymentSource.CREDIT: ['expires_month', 'expires_year',
|
||||||
|
'brand', 'cc_full']
|
||||||
|
}[source_type]
|
||||||
|
|
||||||
|
fields = ','.join(specific_fields)
|
||||||
|
|
||||||
|
extras_row = await app.db.fetchrow(f"""
|
||||||
|
SELECT {fields}, billing_address, default_, id::text
|
||||||
|
FROM user_payment_sources
|
||||||
|
WHERE id = $1
|
||||||
|
""", source_id)
|
||||||
|
|
||||||
|
derow = dict(extras_row)
|
||||||
|
|
||||||
|
if source_type == PaymentSource.CREDIT:
|
||||||
|
derow['last_4'] = derow['cc_full'][-4:]
|
||||||
|
derow.pop('cc_full')
|
||||||
|
|
||||||
|
derow['default'] = derow['default_']
|
||||||
|
derow.pop('default_')
|
||||||
|
|
||||||
|
source = {
|
||||||
|
'id': str(source_id),
|
||||||
|
'type': source_type.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {**source, **derow}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_subscription(subscription_id: int):
|
||||||
|
row = await app.db.execute("""
|
||||||
|
SELECT id::text, source_id::text AS payment_source_id,
|
||||||
|
payment_gateway, payment_gateway_plan_id,
|
||||||
|
period_start AS current_period_start,
|
||||||
|
period_end AS current_period_end,
|
||||||
|
canceled_at, s_type, status
|
||||||
|
FROM user_subscriptions
|
||||||
|
WHERE id = $1
|
||||||
|
""", subscription_id)
|
||||||
|
|
||||||
|
drow = dict(row)
|
||||||
|
|
||||||
|
drow['type'] = drow['s_type']
|
||||||
|
drow.pop('s_type')
|
||||||
|
|
||||||
|
to_tstamp = ['current_period_start', 'current_period_end', 'canceled_at']
|
||||||
|
|
||||||
|
for field in to_tstamp:
|
||||||
|
drow[field] = timestamp_(drow[field])
|
||||||
|
|
||||||
|
return drow
|
||||||
|
|
||||||
|
|
||||||
|
async def get_payment(payment_id: int):
|
||||||
|
row = await app.db.execute("""
|
||||||
|
SELECT id::text, source_id, subscription_id,
|
||||||
|
amount, amount_refunded, currency,
|
||||||
|
description, status, tax, tax_inclusive
|
||||||
|
FROM user_payments
|
||||||
|
WHERE id = $1
|
||||||
|
""", payment_id)
|
||||||
|
|
||||||
|
drow = dict(row)
|
||||||
|
drow['created_at'] = snowflake_datetime(int(drow['id']))
|
||||||
|
return drow
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/@me/billing/payment-sources', methods=['GET'])
|
||||||
|
async def _get_billing_sources():
|
||||||
|
user_id = await token_check()
|
||||||
|
source_ids = await get_payment_source_ids(user_id)
|
||||||
|
|
||||||
|
res = []
|
||||||
|
|
||||||
|
for source_id in source_ids:
|
||||||
|
source = await get_payment_source(user_id, source_id)
|
||||||
|
res.append(source)
|
||||||
|
|
||||||
|
return jsonify(res)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/@me/billing/subscriptions', methods=['GET'])
|
||||||
|
async def _get_billing_subscriptions():
|
||||||
|
user_id = await token_check()
|
||||||
|
sub_ids = await get_subscription_ids(user_id)
|
||||||
|
res = []
|
||||||
|
|
||||||
|
for sub_id in sub_ids:
|
||||||
|
res.append(await get_subscription(sub_id))
|
||||||
|
|
||||||
|
return jsonify(res)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/@me/billing/payments', methods=['GET'])
|
||||||
|
async def _get_billing_payments():
|
||||||
|
user_id = await token_check()
|
||||||
|
payment_ids = await get_payment_ids(user_id)
|
||||||
|
res = []
|
||||||
|
|
||||||
|
for payment_id in payment_ids:
|
||||||
|
res.append(await get_payment(payment_id))
|
||||||
|
|
||||||
|
return jsonify(res)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/@me/billing/payment-sources', methods=['POST'])
|
||||||
|
async def _create_payment_source():
|
||||||
|
user_id = await token_check()
|
||||||
|
j = validate(await request.get_json(), PAYMENT_SOURCE)
|
||||||
|
|
||||||
|
new_source_id = get_snowflake()
|
||||||
|
|
||||||
|
await app.db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO user_payment_sources (id, user_id, source_type,
|
||||||
|
default_, expires_month, expires_year, brand, cc_full,
|
||||||
|
billing_address)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
""", new_source_id, user_id, PaymentSource.CREDIT.value,
|
||||||
|
True, 12, 6969, 'Visa', '4242424242424242',
|
||||||
|
json.dumps(j['billing_address']))
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
await get_payment_source(user_id, new_source_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/@me/billing/subscriptions', methods=['POST'])
|
||||||
|
async def _create_subscription():
|
||||||
|
user_id = await token_check()
|
||||||
|
j = validate(await request.get_json(), CREATE_SUBSCRIPTION)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/@me/billing/subscriptions/<int:subscription_id>',
|
||||||
|
methods=['DELETE'])
|
||||||
|
async def _delete_subscription(subscription_id):
|
||||||
|
user_id = await token_check()
|
||||||
|
return '', 204
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/@me/billing/subscriptions/<int:subscription_id>',
|
||||||
|
methods=['PATCH'])
|
||||||
|
async def _patch_subscription(subscription_id):
|
||||||
|
"""change a subscription's payment source"""
|
||||||
|
user_id = await token_check()
|
||||||
|
j = validate(await request.get_json(), PATCH_SUBSCRIPTION)
|
||||||
|
# returns subscription object
|
||||||
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
from quart import Blueprint, jsonify, request, current_app as app
|
||||||
|
|
||||||
|
from litecord.auth import token_check
|
||||||
|
from litecord.schemas import validate, USER_SETTINGS, GUILD_SETTINGS
|
||||||
|
from litecord.blueprints.checks import guild_check
|
||||||
|
|
||||||
|
bp = Blueprint('users_settings', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/@me/settings', methods=['GET'])
|
||||||
|
async def get_user_settings():
|
||||||
|
"""Get the current user's settings."""
|
||||||
|
user_id = await token_check()
|
||||||
|
settings = await app.storage.get_user_settings(user_id)
|
||||||
|
return jsonify(settings)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/@me/settings', methods=['PATCH'])
|
||||||
|
async def patch_current_settings():
|
||||||
|
"""Patch the users' current settings.
|
||||||
|
|
||||||
|
More information on what settings exist
|
||||||
|
is at Storage.get_user_settings and the schema.sql file.
|
||||||
|
"""
|
||||||
|
user_id = await token_check()
|
||||||
|
j = validate(await request.get_json(), USER_SETTINGS)
|
||||||
|
|
||||||
|
for key in j:
|
||||||
|
await app.db.execute(f"""
|
||||||
|
UPDATE user_settings
|
||||||
|
SET {key}=$1
|
||||||
|
WHERE id = $2
|
||||||
|
""", j[key], user_id)
|
||||||
|
|
||||||
|
settings = await app.storage.get_user_settings(user_id)
|
||||||
|
await app.dispatcher.dispatch_user(
|
||||||
|
user_id, 'USER_SETTINGS_UPDATE', settings)
|
||||||
|
return jsonify(settings)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/@me/guilds/<int:guild_id>/settings', methods=['PATCH'])
|
||||||
|
async def patch_guild_settings(guild_id: int):
|
||||||
|
"""Update the users' guild settings for a given guild.
|
||||||
|
|
||||||
|
Guild settings are usually related to notifications.
|
||||||
|
"""
|
||||||
|
user_id = await token_check()
|
||||||
|
await guild_check(user_id, guild_id)
|
||||||
|
|
||||||
|
j = validate(await request.get_json(), GUILD_SETTINGS)
|
||||||
|
|
||||||
|
# querying the guild settings information before modifying
|
||||||
|
# will make sure they exist in the table.
|
||||||
|
await app.storage.get_guild_settings_one(user_id, guild_id)
|
||||||
|
|
||||||
|
for field in (k for k in j.keys() if k != 'channel_overrides'):
|
||||||
|
await app.db.execute(f"""
|
||||||
|
UPDATE guild_settings
|
||||||
|
SET {field} = $1
|
||||||
|
WHERE user_id = $2 AND guild_id = $3
|
||||||
|
""", j[field], user_id, guild_id)
|
||||||
|
|
||||||
|
chan_ids = await app.storage.get_channel_ids(guild_id)
|
||||||
|
|
||||||
|
for chandata in j.get('channel_overrides', {}).items():
|
||||||
|
chan_id, chan_overrides = chandata
|
||||||
|
chan_id = int(chan_id)
|
||||||
|
|
||||||
|
# ignore channels that aren't in the guild.
|
||||||
|
if chan_id not in chan_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for field in chan_overrides:
|
||||||
|
await app.db.execute(f"""
|
||||||
|
INSERT INTO guild_settings_channel_overrides
|
||||||
|
(user_id, guild_id, channel_id, {field})
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3, $4)
|
||||||
|
ON CONFLICT
|
||||||
|
ON CONSTRAINT guild_settings_channel_overrides_pkey
|
||||||
|
DO
|
||||||
|
UPDATE
|
||||||
|
SET {field} = $4
|
||||||
|
WHERE guild_settings_channel_overrides.user_id = $1
|
||||||
|
AND guild_settings_channel_overrides.guild_id = $2
|
||||||
|
AND guild_settings_channel_overrides.channel_id = $3
|
||||||
|
""", user_id, guild_id, chan_id, chan_overrides[field])
|
||||||
|
|
||||||
|
settings = await app.storage.get_guild_settings_one(user_id, guild_id)
|
||||||
|
|
||||||
|
await app.dispatcher.dispatch_user(
|
||||||
|
user_id, 'USER_GUILD_SETTINGS_UPDATE', settings)
|
||||||
|
|
||||||
|
return jsonify(settings)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/@me/notes/<int:target_id>', methods=['PUT'])
|
||||||
|
async def put_note(target_id: int):
|
||||||
|
"""Put a note to a user.
|
||||||
|
|
||||||
|
This route is in this blueprint because I consider
|
||||||
|
notes to be personalized settings, so.
|
||||||
|
"""
|
||||||
|
user_id = await token_check()
|
||||||
|
|
||||||
|
j = await request.get_json()
|
||||||
|
note = str(j['note'])
|
||||||
|
|
||||||
|
# UPSERTs are beautiful
|
||||||
|
await app.db.execute("""
|
||||||
|
INSERT INTO notes (user_id, target_id, note)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
|
||||||
|
ON CONFLICT ON CONSTRAINT notes_pkey
|
||||||
|
DO UPDATE SET
|
||||||
|
note = $3
|
||||||
|
WHERE notes.user_id = $1
|
||||||
|
AND notes.target_id = $2
|
||||||
|
""", user_id, target_id, note)
|
||||||
|
|
||||||
|
await app.dispatcher.dispatch_user(user_id, 'USER_NOTE_UPDATE', {
|
||||||
|
'id': str(target_id),
|
||||||
|
'note': note,
|
||||||
|
})
|
||||||
|
|
||||||
|
return '', 204
|
||||||
|
|
||||||
|
|
@ -242,6 +242,7 @@ async def get_me_guilds():
|
||||||
|
|
||||||
@bp.route('/@me/guilds/<int:guild_id>', methods=['DELETE'])
|
@bp.route('/@me/guilds/<int:guild_id>', methods=['DELETE'])
|
||||||
async def leave_guild(guild_id: int):
|
async def leave_guild(guild_id: int):
|
||||||
|
"""Leave a guild."""
|
||||||
user_id = await token_check()
|
user_id = await token_check()
|
||||||
await guild_check(user_id, guild_id)
|
await guild_check(user_id, guild_id)
|
||||||
|
|
||||||
|
|
@ -275,65 +276,6 @@ async def get_connections():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/@me/notes/<int:target_id>', methods=['PUT'])
|
|
||||||
async def put_note(target_id: int):
|
|
||||||
"""Put a note to a user."""
|
|
||||||
user_id = await token_check()
|
|
||||||
|
|
||||||
j = await request.get_json()
|
|
||||||
note = str(j['note'])
|
|
||||||
|
|
||||||
# UPSERTs are beautiful
|
|
||||||
await app.db.execute("""
|
|
||||||
INSERT INTO notes (user_id, target_id, note)
|
|
||||||
VALUES ($1, $2, $3)
|
|
||||||
|
|
||||||
ON CONFLICT ON CONSTRAINT notes_pkey
|
|
||||||
DO UPDATE SET
|
|
||||||
note = $3
|
|
||||||
WHERE notes.user_id = $1
|
|
||||||
AND notes.target_id = $2
|
|
||||||
""", user_id, target_id, note)
|
|
||||||
|
|
||||||
await app.dispatcher.dispatch_user(user_id, 'USER_NOTE_UPDATE', {
|
|
||||||
'id': str(target_id),
|
|
||||||
'note': note,
|
|
||||||
})
|
|
||||||
|
|
||||||
return '', 204
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/@me/settings', methods=['GET'])
|
|
||||||
async def get_user_settings():
|
|
||||||
"""Get the current user's settings."""
|
|
||||||
user_id = await token_check()
|
|
||||||
settings = await app.storage.get_user_settings(user_id)
|
|
||||||
return jsonify(settings)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/@me/settings', methods=['PATCH'])
|
|
||||||
async def patch_current_settings():
|
|
||||||
"""Patch the users' current settings.
|
|
||||||
|
|
||||||
More information on what settings exist
|
|
||||||
is at Storage.get_user_settings and the schema.sql file.
|
|
||||||
"""
|
|
||||||
user_id = await token_check()
|
|
||||||
j = validate(await request.get_json(), USER_SETTINGS)
|
|
||||||
|
|
||||||
for key in j:
|
|
||||||
await app.db.execute(f"""
|
|
||||||
UPDATE user_settings
|
|
||||||
SET {key}=$1
|
|
||||||
WHERE id = $2
|
|
||||||
""", j[key], user_id)
|
|
||||||
|
|
||||||
settings = await app.storage.get_user_settings(user_id)
|
|
||||||
await app.dispatcher.dispatch_user(
|
|
||||||
user_id, 'USER_SETTINGS_UPDATE', settings)
|
|
||||||
return jsonify(settings)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/@me/consent', methods=['GET', 'POST'])
|
@bp.route('/@me/consent', methods=['GET', 'POST'])
|
||||||
async def get_consent():
|
async def get_consent():
|
||||||
"""Always disable data collection.
|
"""Always disable data collection.
|
||||||
|
|
@ -420,59 +362,3 @@ async def get_profile(peer_id: int):
|
||||||
'premium_since': peer_premium,
|
'premium_since': peer_premium,
|
||||||
'mutual_guilds': mutual_res,
|
'mutual_guilds': mutual_res,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/@me/guilds/<int:guild_id>/settings', methods=['PATCH'])
|
|
||||||
async def patch_guild_settings(guild_id: int):
|
|
||||||
"""Update the users' guild settings for a given guild.
|
|
||||||
|
|
||||||
Guild settings are usually related to notifications.
|
|
||||||
"""
|
|
||||||
user_id = await token_check()
|
|
||||||
await guild_check(user_id, guild_id)
|
|
||||||
|
|
||||||
j = validate(await request.get_json(), GUILD_SETTINGS)
|
|
||||||
|
|
||||||
# querying the guild settings information before modifying
|
|
||||||
# will make sure they exist in the table.
|
|
||||||
await app.storage.get_guild_settings_one(user_id, guild_id)
|
|
||||||
|
|
||||||
for field in (k for k in j.keys() if k != 'channel_overrides'):
|
|
||||||
await app.db.execute(f"""
|
|
||||||
UPDATE guild_settings
|
|
||||||
SET {field} = $1
|
|
||||||
WHERE user_id = $2 AND guild_id = $3
|
|
||||||
""", j[field], user_id, guild_id)
|
|
||||||
|
|
||||||
chan_ids = await app.storage.get_channel_ids(guild_id)
|
|
||||||
|
|
||||||
for chandata in j.get('channel_overrides', {}).items():
|
|
||||||
chan_id, chan_overrides = chandata
|
|
||||||
chan_id = int(chan_id)
|
|
||||||
|
|
||||||
# ignore channels that aren't in the guild.
|
|
||||||
if chan_id not in chan_ids:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for field in chan_overrides:
|
|
||||||
await app.db.execute(f"""
|
|
||||||
INSERT INTO guild_settings_channel_overrides
|
|
||||||
(user_id, guild_id, channel_id, {field})
|
|
||||||
VALUES
|
|
||||||
($1, $2, $3, $4)
|
|
||||||
ON CONFLICT
|
|
||||||
ON CONSTRAINT guild_settings_channel_overrides_pkey
|
|
||||||
DO
|
|
||||||
UPDATE
|
|
||||||
SET {field} = $4
|
|
||||||
WHERE guild_settings_channel_overrides.user_id = $1
|
|
||||||
AND guild_settings_channel_overrides.guild_id = $2
|
|
||||||
AND guild_settings_channel_overrides.channel_id = $3
|
|
||||||
""", user_id, guild_id, chan_id, chan_overrides[field])
|
|
||||||
|
|
||||||
settings = await app.storage.get_guild_settings_one(user_id, guild_id)
|
|
||||||
|
|
||||||
await app.dispatcher.dispatch_user(
|
|
||||||
user_id, 'USER_GUILD_SETTINGS_UPDATE', settings)
|
|
||||||
|
|
||||||
return jsonify(settings)
|
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,8 @@ class LitecordValidator(Validator):
|
||||||
|
|
||||||
def _validate_type_voice_region(self, value: str) -> bool:
|
def _validate_type_voice_region(self, value: str) -> bool:
|
||||||
# TODO: complete this list
|
# TODO: complete this list
|
||||||
return value.lower() in ('brazil', 'us-east', 'us-west', 'us-south', 'russia')
|
return value.lower() in ('brazil', 'us-east', 'us-west',
|
||||||
|
'us-south', 'russia')
|
||||||
|
|
||||||
def _validate_type_verification_level(self, value: int) -> bool:
|
def _validate_type_verification_level(self, value: int) -> bool:
|
||||||
return _in_enum(VerificationLevel, value)
|
return _in_enum(VerificationLevel, value)
|
||||||
|
|
|
||||||
7
run.py
7
run.py
|
|
@ -28,6 +28,10 @@ from litecord.blueprints.channel import (
|
||||||
channel_messages, channel_reactions, channel_pins
|
channel_messages, channel_reactions, channel_pins
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from litecord.blueprints.user import (
|
||||||
|
user_settings, user_billing
|
||||||
|
)
|
||||||
|
|
||||||
from litecord.ratelimits.handler import ratelimit_handler
|
from litecord.ratelimits.handler import ratelimit_handler
|
||||||
from litecord.ratelimits.main import RatelimitManager
|
from litecord.ratelimits.main import RatelimitManager
|
||||||
|
|
||||||
|
|
@ -68,7 +72,10 @@ def set_blueprints(app_):
|
||||||
bps = {
|
bps = {
|
||||||
gateway: None,
|
gateway: None,
|
||||||
auth: '/auth',
|
auth: '/auth',
|
||||||
|
|
||||||
users: '/users',
|
users: '/users',
|
||||||
|
user_settings: '/users',
|
||||||
|
user_billing: '/users',
|
||||||
relationships: '/users',
|
relationships: '/users',
|
||||||
|
|
||||||
guilds: '/guilds',
|
guilds: '/guilds',
|
||||||
|
|
|
||||||
77
schema.sql
77
schema.sql
|
|
@ -156,6 +156,83 @@ CREATE TABLE IF NOT EXISTS user_settings (
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- main user billing tables
|
||||||
|
CREATE TABLE IF NOT EXISTS user_payment_sources (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
user_id bigint REFERENCES users (id) NOT NULL,
|
||||||
|
|
||||||
|
-- type=1: credit card fields
|
||||||
|
-- type=2: paypal fields
|
||||||
|
source_type int,
|
||||||
|
|
||||||
|
-- idk lol
|
||||||
|
invalid bool DEFAULT false,
|
||||||
|
default_ bool DEFAULT false,
|
||||||
|
|
||||||
|
-- credit card info (type 1 only)
|
||||||
|
expires_month int DEFAULT 12,
|
||||||
|
expires_year int DEFAULT 3000,
|
||||||
|
brand text,
|
||||||
|
cc_full text NOT NULL,
|
||||||
|
|
||||||
|
-- paypal info (type 2 only)
|
||||||
|
paypal_email text DEFAULT 'a@a.com',
|
||||||
|
|
||||||
|
-- applies to both
|
||||||
|
billing_address jsonb DEFAULT '{}'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- actual subscription statuses
|
||||||
|
CREATE TABLE IF NOT EXISTS user_subscriptions (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
source_id bigint REFERENCES user_payment_sources (id) NOT NULL,
|
||||||
|
user_id bigint REFERENCES users (id) NOT NULL,
|
||||||
|
|
||||||
|
-- s_type = 1: purchase
|
||||||
|
-- s_type = 2: upgrade
|
||||||
|
s_type int DEFAULT 1,
|
||||||
|
|
||||||
|
-- gateway = 1: stripe
|
||||||
|
-- gateway = 2: braintree
|
||||||
|
payment_gateway int DEFAULT 0,
|
||||||
|
payment_gateway_plan_id text,
|
||||||
|
|
||||||
|
-- status = 1: active
|
||||||
|
-- status = 3: cancelled
|
||||||
|
status int DEFAULT 1,
|
||||||
|
|
||||||
|
canceled_at timestamp without time zone default NULL,
|
||||||
|
|
||||||
|
-- set by us
|
||||||
|
period_start timestamp without time zone default (now() at time zone 'utc'),
|
||||||
|
period_end timestamp without time zone default NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- payment logs
|
||||||
|
CREATE TABLE IF NOT EXISTS user_payments (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
source_id bigint REFERENCES user_payment_sources (id),
|
||||||
|
subscription_id bigint REFERENCES user_subscriptions (id),
|
||||||
|
user_id bigint REFERENCES users (id),
|
||||||
|
|
||||||
|
currency text DEFAULT 'usd',
|
||||||
|
|
||||||
|
-- status = 1: success
|
||||||
|
-- status = 2: failed
|
||||||
|
status int DEFAULT 1,
|
||||||
|
|
||||||
|
-- 499 = 4 dollars 99 cents
|
||||||
|
amount bigint,
|
||||||
|
|
||||||
|
tax int DEFAULT 0,
|
||||||
|
tax_inclusive BOOL default true,
|
||||||
|
|
||||||
|
description text,
|
||||||
|
|
||||||
|
amount_refunded int DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
-- main user relationships
|
-- main user relationships
|
||||||
CREATE TABLE IF NOT EXISTS relationships (
|
CREATE TABLE IF NOT EXISTS relationships (
|
||||||
-- the id of who made the relationship
|
-- the id of who made the relationship
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue