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].
|
||||
|
||||
[litecord-reference]: https://gitlab.com/lnmds/litecord-reference
|
||||
[litecord-reference]: https://gitlab.com/luna/litecord-reference
|
||||
|
||||
## Install
|
||||
|
||||
|
|
@ -19,6 +19,8 @@ This project is a rewrite of [litecord-reference].
|
|||
$ git clone https://gitlab.com/luna/litecord.git && cd litecord
|
||||
|
||||
# Setup the database:
|
||||
# don't forget that you can create a specific
|
||||
# postgres user just for the litecord database
|
||||
$ createdb litecord
|
||||
$ psql -f schema.sql litecord
|
||||
|
||||
|
|
|
|||
|
|
@ -87,3 +87,10 @@ async def login():
|
|||
return jsonify({
|
||||
'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']
|
||||
|
||||
guild_json = await app.storage.get_guild(guild_id, user_id)
|
||||
guild_extra = await app.storage.get_guild_extra(guild_id, user_id, 250)
|
||||
|
||||
guild_total = {**guild_json, **guild_extra}
|
||||
guild_total = await app.storage.get_guild_full(guild_id, user_id, 250)
|
||||
|
||||
app.dispatcher.sub_guild(guild_id, user_id)
|
||||
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 ..schemas import validate, INVITE
|
||||
from ..enums import ChannelType
|
||||
from ..errors import BadRequest
|
||||
from ..errors import BadRequest, Forbidden
|
||||
from .channels import channel_check
|
||||
from .guilds import guild_check
|
||||
from ..utils import async_map
|
||||
|
||||
log = Logger(__name__)
|
||||
bp = Blueprint('invites', __name__)
|
||||
|
|
@ -28,7 +30,7 @@ async def create_invite(channel_id):
|
|||
ChannelType.GUILD_VOICE.value):
|
||||
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(
|
||||
"""
|
||||
|
|
@ -54,3 +56,149 @@ async def get_invite(invite_code: str):
|
|||
inv.update(extra)
|
||||
|
||||
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',
|
||||
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
|
||||
|
||||
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],
|
||||
guild_id: int) -> List[GatewayState]:
|
||||
"""Fetch all possible states about members in a guild."""
|
||||
states = []
|
||||
|
||||
for member_id in member_ids:
|
||||
member_states = self.fetch_states(member_id, guild_id)
|
||||
|
||||
# for now, just get the first state
|
||||
try:
|
||||
state = next(iter(member_states))
|
||||
states.append(state)
|
||||
except StopIteration:
|
||||
pass
|
||||
# member_states is empty if the user never logged in
|
||||
# since server start, so we need to add a dummy state
|
||||
if not member_states:
|
||||
dummy_state = GatewayState(
|
||||
session_id='',
|
||||
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
|
||||
|
|
|
|||
|
|
@ -599,12 +599,11 @@ class GatewayWebsocket:
|
|||
if guild_id not in gids:
|
||||
return
|
||||
|
||||
members = await self.storage.get_member_data(guild_id)
|
||||
member_ids = [int(m['user']['id']) for m in members]
|
||||
member_ids = await self.storage.get_member_ids(guild_id)
|
||||
|
||||
# the current implementation is rudimentary and only
|
||||
# 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -52,10 +52,10 @@ class LitecordValidator(Validator):
|
|||
|
||||
def validate(reqjson, schema, raise_err: bool = True):
|
||||
validator = LitecordValidator(schema)
|
||||
log.debug('Validating {}', reqjson)
|
||||
|
||||
if not validator.validate(reqjson):
|
||||
errs = validator.errors
|
||||
log.warning('Error validating doc: {!r}', errs)
|
||||
|
||||
if raise_err:
|
||||
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]:
|
||||
rows = await self.db.fetch("""
|
||||
SELECT user_id
|
||||
|
|
@ -421,13 +428,16 @@ class Storage:
|
|||
|
||||
# fetch some guild info
|
||||
guild = await self.db.fetchrow("""
|
||||
SELECT id::text, name, splash, icon
|
||||
SELECT id::text, name, splash, icon, verification_level
|
||||
FROM guilds
|
||||
WHERE id = $1
|
||||
""", invite['guild_id'])
|
||||
|
||||
dinv['guild'] = dict(guild)
|
||||
|
||||
# TODO: query actual guild features
|
||||
dinv['guild']['features'] = []
|
||||
|
||||
chan = await self.get_channel(invite['channel_id'])
|
||||
dinv['channel'] = {
|
||||
'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 logging
|
||||
import websockets
|
||||
from quart import Quart, g, jsonify
|
||||
from quart import Quart, g, jsonify, request
|
||||
from logbook import StreamHandler, Logger
|
||||
from logbook.compat import redirect_logging
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ for bp, suffix in bps.items():
|
|||
|
||||
@app.after_request
|
||||
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, '
|
||||
'X-Fingerprint, '
|
||||
'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