Merge branch 'master' into 'master'

new invite handling and unconfirmed account support

See merge request litecord/litecord!8
This commit is contained in:
Luna Mendes 2018-12-04 07:30:51 +00:00
commit 00346555d1
7 changed files with 125 additions and 89 deletions

View File

@ -7,6 +7,7 @@ from quart import Blueprint, jsonify, request, current_app as app
from litecord.auth import token_check, create_user from litecord.auth import token_check, create_user
from litecord.schemas import validate, REGISTER, REGISTER_WITH_INVITE from litecord.schemas import validate, REGISTER, REGISTER_WITH_INVITE
from litecord.errors import BadRequest from litecord.errors import BadRequest
from .invites import use_invite
bp = Blueprint('auth', __name__) bp = Blueprint('auth', __name__)
@ -35,15 +36,29 @@ async def register():
"""Register a single user.""" """Register a single user."""
enabled = app.config.get('REGISTRATIONS') enabled = app.config.get('REGISTRATIONS')
if not enabled: if not enabled:
return 'Registrations disabled', 405 raise BadRequest('Registrations disabled', {
'email': 'Registrations are disabled.'
})
j = validate(await request.get_json(), REGISTER) j = await request.get_json()
email, password, username = j['email'], j['password'], j['username']
if not 'password' in j:
j['password'] = 'default_password' # we need some password to make a token
j = validate(j, REGISTER)
email, password, username, invite = j['email'] if 'email' in j else None, j['password'], j['username'], j['invite']
new_id, pwd_hash = await create_user( new_id, pwd_hash = await create_user(
username, email, password, app.db username, email, password, app.db
) )
if invite:
try:
await use_invite(new_id, invite)
except Exception as e:
print(e)
pass # do nothing
return jsonify({ return jsonify({
'token': make_token(new_id, pwd_hash) 'token': make_token(new_id, pwd_hash)
}) })

View File

@ -1,5 +1,6 @@
import datetime import datetime
import hashlib import hashlib
import base64
import os import os
from quart import Blueprint, request, current_app as app, jsonify from quart import Blueprint, request, current_app as app, jsonify
@ -19,6 +20,82 @@ from litecord.blueprints.checks import (
log = Logger(__name__) log = Logger(__name__)
bp = Blueprint('invites', __name__) bp = Blueprint('invites', __name__)
# TODO: Ban handling
async def use_invite(user_id, invite_code):
"""Try using an invite"""
inv = await app.db.fetchrow("""
SELECT guild_id, created_at, max_age, uses, max_uses
FROM invites
WHERE code = $1
""", invite_code)
if inv is None:
raise BadRequest('Unknown invite')
if inv['max_age'] is not 0:
now = datetime.datetime.utcnow()
delta_sec = (now - inv['created_at']).total_seconds()
if delta_sec > inv['max_age']:
await delete_invite(invite_code)
raise BadRequest('Unknown invite (expiried)')
if inv['max_uses'] is not -1 and inv['uses'] > inv['max_uses']:
await delete_invite(invite_code)
raise BadRequest('Unknown invite (too many uses)')
guild_id = inv['guild_id']
joined = await app.db.fetchval("""
SELECT joined_at
FROM members
WHERE user_id = $1 AND guild_id = $2
""", user_id, guild_id)
if joined is not None:
raise BadRequest('You are already in the guild')
await app.db.execute("""
INSERT INTO members (user_id, guild_id)
VALUES ($1, $2)
""", user_id, guild_id)
await create_guild_settings(guild_id, user_id)
# add the @everyone role to the invited member
await app.db.execute("""
INSERT INTO member_roles (user_id, guild_id, role_id)
VALUES ($1, $2, $3)
""", user_id, guild_id, guild_id)
await app.db.execute("""
UPDATE invites
SET uses = uses + 1
WHERE code = $1
""", invite_code)
# tell current members a new member came up
member = await app.storage.get_member_data_one(guild_id, user_id)
await app.dispatcher.dispatch_guild(guild_id, 'GUILD_MEMBER_ADD', {
**member,
**{
'guild_id': str(guild_id),
},
})
# update member lists for the new member
await app.dispatcher.dispatch(
'lazy_guild', guild_id, 'new_member', user_id)
# subscribe new member to guild, so they get events n stuff
await app.dispatcher.sub('guild', guild_id, user_id)
# tell the new member that theres the guild it just joined.
# we use dispatch_user_guild so that we send the GUILD_CREATE
# just to the shards that are actually tied to it.
guild = await app.storage.get_guild_full(guild_id, user_id, 250)
await app.dispatcher.dispatch_user_guild(
user_id, guild_id, 'GUILD_CREATE', guild)
@bp.route('/channels/<int:channel_id>/invites', methods=['POST']) @bp.route('/channels/<int:channel_id>/invites', methods=['POST'])
async def create_invite(channel_id): async def create_invite(channel_id):
@ -34,7 +111,7 @@ async def create_invite(channel_id):
ChannelType.GUILD_VOICE.value): ChannelType.GUILD_VOICE.value):
raise BadRequest('Invalid channel type') raise BadRequest('Invalid channel type')
invite_code = hashlib.md5(os.urandom(64)).hexdigest()[:6] invite_code = base64.b64encode(hashlib.md5(os.urandom(64)).digest()).decode("utf-8").replace("/", "").replace("+", "")[:7]
await app.db.execute( await app.db.execute(
""" """
@ -148,82 +225,11 @@ async def get_channel_invites(channel_id: int):
@bp.route('/invite/<invite_code>', methods=['POST']) @bp.route('/invite/<invite_code>', methods=['POST'])
async def use_invite(invite_code): async def _use_invite(invite_code):
"""Use an invite.""" """Use an invite."""
user_id = await token_check() user_id = await token_check()
inv = await app.db.fetchrow(""" await use_invite(user_id, invite_code)
SELECT guild_id, created_at, max_age, uses, max_uses
FROM invites
WHERE code = $1
""", invite_code)
if inv is None:
raise BadRequest('Invite not found')
now = datetime.datetime.utcnow()
delta_sec = (now - inv['created_at']).total_seconds()
if delta_sec > inv['max_age']:
await delete_invite(invite_code)
raise BadRequest('Invite has expired (age).')
if inv['uses'] > inv['max_uses']:
await delete_invite(invite_code)
raise BadRequest('Invite has expired (uses).')
guild_id = inv['guild_id']
joined = await app.db.fetchval("""
SELECT joined_at
FROM members
WHERE user_id = $1 AND guild_id = $2
""", user_id, guild_id)
if joined is not None:
raise BadRequest('You are already in the guild')
await app.db.execute("""
INSERT INTO members (user_id, guild_id)
VALUES ($1, $2)
""", user_id, guild_id)
await create_guild_settings(guild_id, user_id)
# add the @everyone role to the invited member
await app.db.execute("""
INSERT INTO member_roles (user_id, guild_id, role_id)
VALUES ($1, $2, $3)
""", user_id, guild_id, guild_id)
await app.db.execute("""
UPDATE invites
SET uses = uses + 1
WHERE code = $1
""", invite_code)
# tell current members a new member came up
member = await app.storage.get_member_data_one(guild_id, user_id)
await app.dispatcher.dispatch_guild(guild_id, 'GUILD_MEMBER_ADD', {
**member,
**{
'guild_id': str(guild_id),
},
})
# update member lists for the new member
await app.dispatcher.dispatch(
'lazy_guild', guild_id, 'new_member', user_id)
# subscribe new member to guild, so they get events n stuff
await app.dispatcher.sub('guild', guild_id, user_id)
# tell the new member that theres the guild it just joined.
# we use dispatch_user_guild so that we send the GUILD_CREATE
# just to the shards that are actually tied to it.
guild = await app.storage.get_guild_full(guild_id, user_id, 250)
await app.dispatcher.dispatch_user_guild(
user_id, guild_id, 'GUILD_CREATE', guild)
# the reply is an invite object for some reason. # the reply is an invite object for some reason.
inv = await app.storage.get_invite(invite_code) inv = await app.storage.get_invite(invite_code)

View File

@ -171,6 +171,10 @@ def to_update(j: dict, user: dict, field: str):
async def _check_pass(j, user): async def _check_pass(j, user):
# Do not do password checks on unclaimed accounts
if user['email'] is None:
return
if not j['password']: if not j['password']:
raise BadRequest('password required', { raise BadRequest('password required', {
'password': 'password required' 'password': 'password required'
@ -248,6 +252,11 @@ async def patch_me():
WHERE id = $2 WHERE id = $2
""", new_icon.icon_hash, user_id) """, new_icon.icon_hash, user_id)
if user['email'] is None and not 'new_password' in j:
raise BadRequest('missing password', {
'password': 'Please set a password.'
})
if 'new_password' in j and j['new_password']: if 'new_password' in j and j['new_password']:
await _check_pass(j, user) await _check_pass(j, user)

View File

@ -224,7 +224,7 @@ class GatewayWebsocket:
if self.state.bot: if self.state.bot:
return [{ return [{
'id': row[0], 'id': row,
'unavailable': True, 'unavailable': True,
} for row in guild_ids] } for row in guild_ids]
@ -373,12 +373,13 @@ class GatewayWebsocket:
log.info('subscribing to {} dms', len(dm_ids)) log.info('subscribing to {} dms', len(dm_ids))
await self.ext.dispatcher.sub_many('channel', user_id, dm_ids) await self.ext.dispatcher.sub_many('channel', user_id, dm_ids)
# subscribe to all friends if not self.state.bot:
# (their friends will also subscribe back # subscribe to all friends
# when they come online) # (their friends will also subscribe back
friend_ids = await self.user_storage.get_friend_ids(user_id) # when they come online)
log.info('subscribing to {} friends', len(friend_ids)) friend_ids = await self.user_storage.get_friend_ids(user_id)
await self.ext.dispatcher.sub_many('friend', user_id, friend_ids) log.info('subscribing to {} friends', len(friend_ids))
await self.ext.dispatcher.sub_many('friend', user_id, friend_ids)
async def update_status(self, status: dict): async def update_status(self, status: dict):
"""Update the status of the current websocket connection.""" """Update the status of the current websocket connection."""
@ -891,7 +892,6 @@ class GatewayWebsocket:
log.warning('conn close, state={}, err={}', self.state, err) log.warning('conn close, state={}, err={}', self.state, err)
except WebsocketClose as err: except WebsocketClose as err:
log.warning('ws close, state={} err={}', self.state, err) log.warning('ws close, state={} err={}', self.state, err)
await self.ws.close(code=err.code, reason=err.reason) await self.ws.close(code=err.code, reason=err.reason)
except Exception as err: except Exception as err:
log.exception('An exception has occoured. state={}', self.state) log.exception('An exception has occoured. state={}', self.state)

View File

@ -157,9 +157,13 @@ def validate(reqjson: Union[Dict, List], schema: Dict,
REGISTER = { REGISTER = {
'email': {'type': 'email', 'required': True},
'username': {'type': 'username', 'required': True}, 'username': {'type': 'username', 'required': True},
'password': {'type': 'string', 'minlength': 5, 'required': True} 'email': {'type': 'email', 'required': False},
'password': {'type': 'string', 'minlength': 5, 'required': False},
'invite': {'type': 'string', 'required': False, 'nullable': True}, # optional
'fingerprint': {'type': 'string', 'required': False, 'nullable': True}, # these are sent by official client
'captcha_key': {'type': 'string', 'required': False, 'nullable': True},
'consent': {'type': 'boolean'}
} }
# only used by us, not discord, hence 'invcode' (to separate from discord) # only used by us, not discord, hence 'invcode' (to separate from discord)
@ -470,7 +474,7 @@ INVITE = {
'temporary': {'type': 'boolean', 'required': False, 'default': False}, 'temporary': {'type': 'boolean', 'required': False, 'default': False},
'unique': {'type': 'boolean', 'required': False, 'default': True}, 'unique': {'type': 'boolean', 'required': False, 'default': True},
'validate': {'type': 'boolean', 'required': False, 'nullable': True} 'validate': {'type': 'string', 'required': False, 'nullable': True} # discord client sends invite code there
} }
USER_SETTINGS = { USER_SETTINGS = {

View File

@ -0,0 +1,2 @@
ALTER TABLE users ALTER COLUMN email DROP NOT NULL;
ALTER TABLE users ALTER COLUMN email SET DEFAULT NULL;

View File

@ -74,7 +74,7 @@ CREATE TABLE IF NOT EXISTS users (
id bigint UNIQUE NOT NULL, id bigint UNIQUE NOT NULL,
username text NOT NULL, username text NOT NULL,
discriminator varchar(4) NOT NULL, discriminator varchar(4) NOT NULL,
email varchar(255) NOT NULL UNIQUE, email varchar(255) DEFAULT NULL,
-- user properties -- user properties
bot boolean DEFAULT FALSE, bot boolean DEFAULT FALSE,