mirror of https://gitlab.com/litecord/litecord.git
add invite usage
users can now join guilds!
invites:
- add GET /api/v6/invites/<invite_code>
- add DELETE /api/v6/invites/<invite_code>
- add GET /api/v6/guilds/<guild_id>/invites
- add GET /api/v6/channels/<channel_id>/invites
- add POST /api/v6/invite/<invite_code>
storage:
- add verification_level to invites
- add empty invite.guild.features
gateway.state_manager:
- add StateManager.user_states
- give a dummy offline state on guild_states
- this makes it possible for people to see offline members even when
those members never logged in (since the would have no state being
reffered to them)
- gateway.websocket: use get_member_ids on lazy guild handler
- auth: add GET /api/v6/auth/consent-required
- dispatcher: add dispatch_user_guild and dispatch_user
- run: use Origin header on Access-Control-Allow-Origin
This commit is contained in:
parent
f346a991c5
commit
92f6e3cf75
|
|
@ -5,7 +5,7 @@ Python.
|
||||||
|
|
||||||
This project is a rewrite of [litecord-reference].
|
This project is a rewrite of [litecord-reference].
|
||||||
|
|
||||||
[litecord-reference]: https://gitlab.com/lnmds/litecord-reference
|
[litecord-reference]: https://gitlab.com/luna/litecord-reference
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|
@ -19,6 +19,8 @@ This project is a rewrite of [litecord-reference].
|
||||||
$ git clone https://gitlab.com/luna/litecord.git && cd litecord
|
$ git clone https://gitlab.com/luna/litecord.git && cd litecord
|
||||||
|
|
||||||
# Setup the database:
|
# Setup the database:
|
||||||
|
# don't forget that you can create a specific
|
||||||
|
# postgres user just for the litecord database
|
||||||
$ createdb litecord
|
$ createdb litecord
|
||||||
$ psql -f schema.sql litecord
|
$ psql -f schema.sql litecord
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,3 +87,10 @@ async def login():
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'token': make_token(user_id, pwd_hash)
|
'token': make_token(user_id, pwd_hash)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/consent-required', methods=['GET'])
|
||||||
|
async def consent_required():
|
||||||
|
return jsonify({
|
||||||
|
'required': True,
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -83,10 +83,7 @@ async def create_guild():
|
||||||
|
|
||||||
# TODO: j['roles'] and j['channels']
|
# TODO: j['roles'] and j['channels']
|
||||||
|
|
||||||
guild_json = await app.storage.get_guild(guild_id, user_id)
|
guild_total = await app.storage.get_guild_full(guild_id, user_id, 250)
|
||||||
guild_extra = await app.storage.get_guild_extra(guild_id, user_id, 250)
|
|
||||||
|
|
||||||
guild_total = {**guild_json, **guild_extra}
|
|
||||||
|
|
||||||
app.dispatcher.sub_guild(guild_id, user_id)
|
app.dispatcher.sub_guild(guild_id, user_id)
|
||||||
await app.dispatcher.dispatch_guild(guild_id, 'GUILD_CREATE', guild_total)
|
await app.dispatcher.dispatch_guild(guild_id, 'GUILD_CREATE', guild_total)
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@ from logbook import Logger
|
||||||
from ..auth import token_check
|
from ..auth import token_check
|
||||||
from ..schemas import validate, INVITE
|
from ..schemas import validate, INVITE
|
||||||
from ..enums import ChannelType
|
from ..enums import ChannelType
|
||||||
from ..errors import BadRequest
|
from ..errors import BadRequest, Forbidden
|
||||||
from .channels import channel_check
|
from .channels import channel_check
|
||||||
|
from .guilds import guild_check
|
||||||
|
from ..utils import async_map
|
||||||
|
|
||||||
log = Logger(__name__)
|
log = Logger(__name__)
|
||||||
bp = Blueprint('invites', __name__)
|
bp = Blueprint('invites', __name__)
|
||||||
|
|
@ -28,7 +30,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()[:16]
|
invite_code = hashlib.md5(os.urandom(64)).hexdigest()[:6]
|
||||||
|
|
||||||
await app.db.execute(
|
await app.db.execute(
|
||||||
"""
|
"""
|
||||||
|
|
@ -54,3 +56,149 @@ async def get_invite(invite_code: str):
|
||||||
inv.update(extra)
|
inv.update(extra)
|
||||||
|
|
||||||
return jsonify(inv)
|
return jsonify(inv)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/invite/<invite_code>', methods=['GET'])
|
||||||
|
async def get_invite_2(invite_code: str):
|
||||||
|
return await get_invite(invite_code)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/invites/<invite_code>', methods=['DELETE'])
|
||||||
|
async def delete_invite(invite_code: str):
|
||||||
|
user_id = await token_check()
|
||||||
|
|
||||||
|
gid = await app.db.fetchval("""
|
||||||
|
SELECT guild_id
|
||||||
|
FROM invites
|
||||||
|
WHERE code = $1
|
||||||
|
""", invite_code)
|
||||||
|
|
||||||
|
if gid is None:
|
||||||
|
raise BadRequest('Unknown invite')
|
||||||
|
|
||||||
|
# TODO: check MANAGE_CHANNELS permission
|
||||||
|
# for now we'll go with checking owner
|
||||||
|
|
||||||
|
owner_id = await app.db.fetchval("""
|
||||||
|
SELECT owner_id
|
||||||
|
FROM guilds
|
||||||
|
WHERE id = $1
|
||||||
|
""". gid)
|
||||||
|
|
||||||
|
if owner_id != user_id:
|
||||||
|
raise Forbidden('Not guild owner')
|
||||||
|
|
||||||
|
inv = await app.storage.get_invite(invite_code)
|
||||||
|
|
||||||
|
await app.db.fetchval("""
|
||||||
|
DELETE FROM invites
|
||||||
|
WHERE code = $1
|
||||||
|
""", invite_code)
|
||||||
|
|
||||||
|
return jsonify(inv)
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_inv(code):
|
||||||
|
inv = await app.storage.get_invite(code)
|
||||||
|
meta = await app.storage.get_invite_metadata(code)
|
||||||
|
return {**inv, **meta}
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/guilds/<int:guild_id>/invites', methods=['GET'])
|
||||||
|
async def get_guild_invites(guild_id: int):
|
||||||
|
user_id = await token_check()
|
||||||
|
await guild_check(user_id, guild_id)
|
||||||
|
|
||||||
|
inv_codes = await app.db.fetch("""
|
||||||
|
SELECT code
|
||||||
|
FROM invites
|
||||||
|
WHERE guild_id = $1
|
||||||
|
""", guild_id)
|
||||||
|
|
||||||
|
# TODO: MANAGE_GUILD permission
|
||||||
|
|
||||||
|
inv_codes = [r['code'] for r in inv_codes]
|
||||||
|
invs = await async_map(_get_inv, inv_codes)
|
||||||
|
return jsonify(invs)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/channels/<int:channel_id>/invites', methods=['GET'])
|
||||||
|
async def get_channel_invites(channel_id: int):
|
||||||
|
user_id = await token_check
|
||||||
|
guild_id = await channel_check(user_id, channel_id)
|
||||||
|
|
||||||
|
inv_codes = await app.db.fetch("""
|
||||||
|
SELECT code
|
||||||
|
FROM invites
|
||||||
|
WHERE guild_id = $1 AND channel_id = $2
|
||||||
|
""", guild_id, channel_id)
|
||||||
|
|
||||||
|
# TODO: check MANAGE_CHANNELS permission
|
||||||
|
|
||||||
|
inv_codes = [r['code'] for r in inv_codes]
|
||||||
|
invs = await async_map(_get_inv, inv_codes)
|
||||||
|
return jsonify(invs)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/invite/<invite_code>', methods=['POST'])
|
||||||
|
async def use_invite(invite_code):
|
||||||
|
user_id = await token_check()
|
||||||
|
|
||||||
|
guild_id = await app.db.fetchval("""
|
||||||
|
SELECT guild_id
|
||||||
|
FROM invites
|
||||||
|
WHERE code = $1
|
||||||
|
""", invite_code)
|
||||||
|
|
||||||
|
if not guild_id:
|
||||||
|
raise BadRequest('Guild not Found')
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
# subscribe new member to guild, so they get events n stuff
|
||||||
|
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.
|
||||||
|
inv = await app.storage.get_invite(invite_code)
|
||||||
|
inv_meta = await app.storage.get_invite_metadata(invite_code)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
**inv,
|
||||||
|
**{
|
||||||
|
'inviter': inv_meta['inviter']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -58,3 +58,26 @@ class EventDispatcher:
|
||||||
|
|
||||||
log.info('Dispatched {} {!r} to {} states',
|
log.info('Dispatched {} {!r} to {} states',
|
||||||
guild_id, event_name, dispatched)
|
guild_id, event_name, dispatched)
|
||||||
|
|
||||||
|
async def _dispatch_states(self, states: list, event: str, data: Any):
|
||||||
|
for state in states:
|
||||||
|
await state.ws.dispatch(event, data)
|
||||||
|
|
||||||
|
async def dispatch_user_guild(self, user_id: int, guild_id: int,
|
||||||
|
event: str, data: Any):
|
||||||
|
"""Dispatch a single event to a user inside a guild.
|
||||||
|
|
||||||
|
The difference between dispatch_user and dispatch_user_guild
|
||||||
|
is sharding management happening here, via StateManager.fetch_states
|
||||||
|
"""
|
||||||
|
states = self.state_manager.fetch_states(user_id, guild_id)
|
||||||
|
|
||||||
|
if not states:
|
||||||
|
self.unsub_guild(guild_id, user_id)
|
||||||
|
|
||||||
|
await self._dispatch_states(states, event, data)
|
||||||
|
|
||||||
|
async def dispatch_user(self, user_id: int, event: str, data: Any):
|
||||||
|
"""Dispatch an event to a single user."""
|
||||||
|
states = self.state_manager.user_states(user_id)
|
||||||
|
await self._dispatch_states(states, event, data)
|
||||||
|
|
|
||||||
|
|
@ -52,18 +52,37 @@ class StateManager:
|
||||||
|
|
||||||
return states
|
return states
|
||||||
|
|
||||||
|
def user_states(self, user_id: int) -> List[GatewayState]:
|
||||||
|
"""Fetch all states tied to a single user."""
|
||||||
|
return list(self.states[user_id].values())
|
||||||
|
|
||||||
def guild_states(self, member_ids: List[int],
|
def guild_states(self, member_ids: List[int],
|
||||||
guild_id: int) -> List[GatewayState]:
|
guild_id: int) -> List[GatewayState]:
|
||||||
|
"""Fetch all possible states about members in a guild."""
|
||||||
states = []
|
states = []
|
||||||
|
|
||||||
for member_id in member_ids:
|
for member_id in member_ids:
|
||||||
member_states = self.fetch_states(member_id, guild_id)
|
member_states = self.fetch_states(member_id, guild_id)
|
||||||
|
|
||||||
# for now, just get the first state
|
# member_states is empty if the user never logged in
|
||||||
try:
|
# since server start, so we need to add a dummy state
|
||||||
state = next(iter(member_states))
|
if not member_states:
|
||||||
states.append(state)
|
dummy_state = GatewayState(
|
||||||
except StopIteration:
|
session_id='',
|
||||||
pass
|
user_id=member_id,
|
||||||
|
presence={
|
||||||
|
'afk': False,
|
||||||
|
'status': 'offline',
|
||||||
|
'game': None,
|
||||||
|
'since': 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
states.append(dummy_state)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# push all available member states to the result
|
||||||
|
# array
|
||||||
|
states.extend(member_states)
|
||||||
|
|
||||||
return states
|
return states
|
||||||
|
|
|
||||||
|
|
@ -599,12 +599,11 @@ class GatewayWebsocket:
|
||||||
if guild_id not in gids:
|
if guild_id not in gids:
|
||||||
return
|
return
|
||||||
|
|
||||||
members = await self.storage.get_member_data(guild_id)
|
member_ids = await self.storage.get_member_ids(guild_id)
|
||||||
member_ids = [int(m['user']['id']) for m in members]
|
|
||||||
|
|
||||||
# the current implementation is rudimentary and only
|
# the current implementation is rudimentary and only
|
||||||
# generates two groups: online and offline, using
|
# generates two groups: online and offline, using
|
||||||
# guild_presences for list_data.
|
# PresenceManager.guild_presences to fill list_data.
|
||||||
|
|
||||||
# this also doesn't take account the channels in lazy_request.
|
# this also doesn't take account the channels in lazy_request.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,10 @@ class LitecordValidator(Validator):
|
||||||
|
|
||||||
def validate(reqjson, schema, raise_err: bool = True):
|
def validate(reqjson, schema, raise_err: bool = True):
|
||||||
validator = LitecordValidator(schema)
|
validator = LitecordValidator(schema)
|
||||||
log.debug('Validating {}', reqjson)
|
|
||||||
|
|
||||||
if not validator.validate(reqjson):
|
if not validator.validate(reqjson):
|
||||||
errs = validator.errors
|
errs = validator.errors
|
||||||
|
log.warning('Error validating doc: {!r}', errs)
|
||||||
|
|
||||||
if raise_err:
|
if raise_err:
|
||||||
raise BadRequest('bad payload', errs)
|
raise BadRequest('bad payload', errs)
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,13 @@ class Storage:
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
async def get_guild_full(self, guild_id: int,
|
||||||
|
user_id: int, large_count: int = 250) -> Dict:
|
||||||
|
guild = await self.get_guild(guild_id, user_id)
|
||||||
|
extra = await self.get_guild_extra(guild_id, user_id, large_count)
|
||||||
|
|
||||||
|
return {**guild, **extra}
|
||||||
|
|
||||||
async def get_member_ids(self, guild_id: int) -> List[int]:
|
async def get_member_ids(self, guild_id: int) -> List[int]:
|
||||||
rows = await self.db.fetch("""
|
rows = await self.db.fetch("""
|
||||||
SELECT user_id
|
SELECT user_id
|
||||||
|
|
@ -421,13 +428,16 @@ class Storage:
|
||||||
|
|
||||||
# fetch some guild info
|
# fetch some guild info
|
||||||
guild = await self.db.fetchrow("""
|
guild = await self.db.fetchrow("""
|
||||||
SELECT id::text, name, splash, icon
|
SELECT id::text, name, splash, icon, verification_level
|
||||||
FROM guilds
|
FROM guilds
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
""", invite['guild_id'])
|
""", invite['guild_id'])
|
||||||
|
|
||||||
dinv['guild'] = dict(guild)
|
dinv['guild'] = dict(guild)
|
||||||
|
|
||||||
|
# TODO: query actual guild features
|
||||||
|
dinv['guild']['features'] = []
|
||||||
|
|
||||||
chan = await self.get_channel(invite['channel_id'])
|
chan = await self.get_channel(invite['channel_id'])
|
||||||
dinv['channel'] = {
|
dinv['channel'] = {
|
||||||
'id': chan['id'],
|
'id': chan['id'],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
async def async_map(function, iterable) -> list:
|
||||||
|
"""Map a coroutine to an iterable."""
|
||||||
|
res = []
|
||||||
|
|
||||||
|
for element in iterable:
|
||||||
|
result = await function(element)
|
||||||
|
res.append(result)
|
||||||
|
|
||||||
|
return res
|
||||||
4
run.py
4
run.py
|
|
@ -5,7 +5,7 @@ import asyncpg
|
||||||
import logbook
|
import logbook
|
||||||
import logging
|
import logging
|
||||||
import websockets
|
import websockets
|
||||||
from quart import Quart, g, jsonify
|
from quart import Quart, g, jsonify, request
|
||||||
from logbook import StreamHandler, Logger
|
from logbook import StreamHandler, Logger
|
||||||
from logbook.compat import redirect_logging
|
from logbook.compat import redirect_logging
|
||||||
|
|
||||||
|
|
@ -64,7 +64,7 @@ for bp, suffix in bps.items():
|
||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
async def app_after_request(resp):
|
async def app_after_request(resp):
|
||||||
resp.headers['Access-Control-Allow-Origin'] = 'https://ptb.discordapp.com'
|
resp.headers['Access-Control-Allow-Origin'] = request.headers['Origin']
|
||||||
resp.headers['Access-Control-Allow-Headers'] = ('*, X-Super-Properties, '
|
resp.headers['Access-Control-Allow-Headers'] = ('*, X-Super-Properties, '
|
||||||
'X-Fingerprint, '
|
'X-Fingerprint, '
|
||||||
'X-Context-Properties, '
|
'X-Context-Properties, '
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argv = sys.argv
|
||||||
|
inst_url = 'https://discordapp.io'
|
||||||
|
|
||||||
|
if len(argv) < 4:
|
||||||
|
print('useradd.py <email> <username> <password>')
|
||||||
|
return
|
||||||
|
|
||||||
|
email, username, password = sys.argv[1:4]
|
||||||
|
print('email', repr(email))
|
||||||
|
print('username', repr(username))
|
||||||
|
print('password', repr(password))
|
||||||
|
resp = requests.post(f'{inst_url}/api/v6/auth/register', json={
|
||||||
|
'email': email,
|
||||||
|
'password': password,
|
||||||
|
'username': username,
|
||||||
|
}, headers={
|
||||||
|
'Origin': inst_url
|
||||||
|
})
|
||||||
|
print(resp.status_code)
|
||||||
|
print(resp.text)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue