blueprints.guilds: add auto-role and auto-channel creation

also simplify a lot of repeated code on the blueprint.

 - litecord: add permissions module
    for future role code

 - schemas: add channel_type, guild_name, channel_name types
 - schemas: add GUILD_CREATE schema
This commit is contained in:
Luna Mendes 2018-10-26 02:34:17 -03:00
parent 75a8e77a21
commit aaa11be258
3 changed files with 269 additions and 91 deletions

View File

@ -9,6 +9,7 @@ from .channels import channel_ack
from .checks import guild_check from .checks import guild_check
bp = Blueprint('guilds', __name__) bp = Blueprint('guilds', __name__)
DEFAULT_EVERYONE_PERMS = 104324161
async def guild_owner_check(user_id: int, guild_id: int): async def guild_owner_check(user_id: int, guild_id: int):
@ -48,8 +49,116 @@ async def create_guild_settings(guild_id: int, user_id: int):
""", m_notifs, user_id, guild_id) """, m_notifs, user_id, guild_id)
async def add_member(guild_id: int, user_id: int):
"""Add a user to a 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)
async def guild_create_roles_prep(guild_id: int, roles: list):
"""Create roles in preparation in guild create."""
# by reaching this point in the code that means
# roles is not nullable, which means
# roles has at least one element, so we can access safely.
# the first member in the roles array
# are patches to the @everyone role
everyone_patches = roles[0]
for field in everyone_patches:
await app.db.execute(f"""
UPDATE roles
SET {field}={everyone_patches[field]}
WHERE roles.id = $1
""", guild_id)
default_perms = (everyone_patches.get('permissions')
or DEFAULT_EVERYONE_PERMS)
# from the 2nd and forward,
# should be treated as new roles
for role in roles[1:]:
new_role_id = get_snowflake()
await app.db.execute(
"""
INSERT INTO roles (id, guild_id, name, color,
hoist, position, permissions, managed, mentionable)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
""",
new_role_id,
guild_id,
role['name'],
role.get('color', 0),
role.get('hoist', False),
role.get('permissions', default_perms),
False,
role.get('mentionable', False)
)
async def _specific_chan_create(channel_id, ctype, **kwargs):
if ctype == ChannelType.GUILD_TEXT:
await app.db.execute("""
INSERT INTO guild_text_channels (id, topic)
VALUES ($1)
""", channel_id, kwargs.get('topic', ''))
elif ctype == ChannelType.GUILD_VOICE:
await app.db.execute(
"""
INSERT INTO guild_voice_channels (id, bitrate, user_limit)
VALUES ($1, $2, $3)
""",
channel_id,
kwargs.get('bitrate', 64),
kwargs.get('user_limit', 0)
)
async def create_guild_channel(guild_id: int, channel_id: int,
ctype: ChannelType, **kwargs):
"""Create a channel in a guild."""
await app.db.execute("""
INSERT INTO channels (id, channel_type)
VALUES ($1, $2)
""", channel_id, ctype.value)
# calc new pos
max_pos = await app.db.fetchval("""
SELECT MAX(position)
FROM guild_channels
WHERE guild_id = $1
""", guild_id)
# all channels go to guild_channels
await app.db.execute("""
INSERT INTO guild_channels (id, guild_id, name, position)
VALUES ($1, $2, $3, $4)
""", channel_id, guild_id, kwargs['name'], max_pos + 1)
# the rest of sql magic is dependant on the channel
# we're creating (a text or voice or category),
# so we use this function.
await _specific_chan_create(channel_id, ctype, **kwargs)
async def guild_create_channels_prep(guild_id: int, channels: list):
"""Create channels pre-guild create"""
for channel_raw in channels:
channel_id = get_snowflake()
ctype = ChannelType(channel_raw['type'])
await create_guild_channel(guild_id, channel_id, ctype)
@bp.route('', methods=['POST']) @bp.route('', methods=['POST'])
async def create_guild(): async def create_guild():
"""Create a new guild, assigning
the user creating it as the owner and
making them join."""
user_id = await token_check() user_id = await token_check()
j = await request.get_json() j = await request.get_json()
@ -66,36 +175,27 @@ async def create_guild():
j.get('default_message_notifications', 0), j.get('default_message_notifications', 0),
j.get('explicit_content_filter', 0)) j.get('explicit_content_filter', 0))
await app.db.execute(""" await add_member(guild_id, user_id)
INSERT INTO members (user_id, guild_id)
VALUES ($1, $2)
""", user_id, guild_id)
await create_guild_settings(guild_id, user_id)
# create the default @everyone role (everyone has it by default,
# so we don't insert that in the table)
await app.db.execute(""" await app.db.execute("""
INSERT INTO roles (id, guild_id, name, position, permissions) INSERT INTO roles (id, guild_id, name, position, permissions)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4, $5)
""", guild_id, guild_id, '@everyone', 0, 104324161) """, guild_id, guild_id, '@everyone', 0, DEFAULT_EVERYONE_PERMS)
# create a single #general channel.
general_id = get_snowflake() general_id = get_snowflake()
await app.db.execute(""" await create_guild_channel(
INSERT INTO channels (id, channel_type) guild_id, general_id, ChannelType.GUILD_TEXT,
VALUES ($1, $2) name='general')
""", general_id, ChannelType.GUILD_TEXT.value)
await app.db.execute(""" if j.get('roles'):
INSERT INTO guild_channels (id, guild_id, name, position) await guild_create_roles_prep(guild_id, j['roles'])
VALUES ($1, $2, $3, $4)
""", general_id, guild_id, 'general', 0)
await app.db.execute(""" if j.get('channels'):
INSERT INTO guild_text_channels (id) await guild_create_channels_prep(guild_id, j['channels'])
VALUES ($1)
""", general_id)
# TODO: j['roles'] and j['channels']
guild_total = await app.storage.get_guild_full(guild_id, user_id, 250) guild_total = await app.storage.get_guild_full(guild_id, user_id, 250)
@ -106,12 +206,13 @@ async def create_guild():
@bp.route('/<int:guild_id>', methods=['GET']) @bp.route('/<int:guild_id>', methods=['GET'])
async def get_guild(guild_id): async def get_guild(guild_id):
"""Get a single guilds' information."""
user_id = await token_check() user_id = await token_check()
await guild_check(user_id, guild_id)
gj = await app.storage.get_guild(guild_id, user_id) return jsonify(
gj_extra = await app.storage.get_guild_extra(guild_id, user_id, 250) await app.storage.get_guild_full(guild_id, user_id, 250)
)
return jsonify({**gj, **gj_extra})
@bp.route('/<int:guild_id>', methods=['UPDATE']) @bp.route('/<int:guild_id>', methods=['UPDATE'])
@ -139,8 +240,6 @@ async def update_guild(guild_id):
""", j['name'], guild_id) """, j['name'], guild_id)
if 'region' in j: if 'region' in j:
# TODO: check region value
await app.db.execute(""" await app.db.execute("""
UPDATE guilds UPDATE guilds
SET region = $1 SET region = $1
@ -167,15 +266,14 @@ async def update_guild(guild_id):
WHERE guild_id = $2 WHERE guild_id = $2
""", j[field], guild_id) """, j[field], guild_id)
# return guild object guild = await app.storage.get_guild_full(
gj = await app.storage.get_guild(guild_id, user_id) guild_id, user_id
gj_extra = await app.storage.get_guild_extra(guild_id, user_id, 250) )
gj_total = {**gj, **gj_extra} await app.dispatcher.dispatch_guild(
guild_id, 'GUILD_UPDATE', guild)
await app.dispatcher.dispatch_guild(guild_id, 'GUILD_UPDATE', gj_total) return jsonify(guild)
return jsonify({**gj, **gj_extra})
@bp.route('/<int:guild_id>', methods=['DELETE']) @bp.route('/<int:guild_id>', methods=['DELETE'])
@ -185,7 +283,7 @@ async def delete_guild(guild_id):
await guild_owner_check(user_id, guild_id) await guild_owner_check(user_id, guild_id)
await app.db.execute(""" await app.db.execute("""
DELETE FROM guild DELETE FROM guilds
WHERE guilds.id = $1 WHERE guilds.id = $1
""", guild_id) """, guild_id)
@ -219,42 +317,19 @@ async def create_channel(guild_id):
# TODO: check permissions for MANAGE_CHANNELS # TODO: check permissions for MANAGE_CHANNELS
await guild_check(user_id, guild_id) await guild_check(user_id, guild_id)
new_channel_id = get_snowflake()
channel_type = j.get('type', ChannelType.GUILD_TEXT) channel_type = j.get('type', ChannelType.GUILD_TEXT)
channel_type = ChannelType(channel_type) channel_type = ChannelType(channel_type)
if channel_type not in (ChannelType.GUILD_TEXT, if channel_type not in (ChannelType.GUILD_TEXT,
ChannelType.GUILD_VOICE): ChannelType.GUILD_VOICE):
raise BadRequest('Invalid channel type') raise BadRequest('Invalid channel type')
await app.db.execute(""" new_channel_id = get_snowflake()
INSERT INTO channels (id, channel_type) await create_guild_channel(guild_id, new_channel_id, channel_type,)
VALUES ($1, $2)
""", new_channel_id, channel_type.value)
max_pos = await app.db.fetchval("""
SELECT MAX(position)
FROM guild_channels
WHERE guild_id = $1
""", guild_id)
if channel_type == ChannelType.GUILD_TEXT:
await app.db.execute("""
INSERT INTO guild_channels (id, guild_id, name, position)
VALUES ($1, $2, $3, $4)
""", new_channel_id, guild_id, j['name'], max_pos + 1)
await app.db.execute("""
INSERT INTO guild_text_channels (id)
VALUES ($1)
""", new_channel_id)
elif channel_type == ChannelType.GUILD_VOICE:
raise NotImplementedError()
chan = await app.storage.get_channel(new_channel_id) chan = await app.storage.get_channel(new_channel_id)
await app.dispatcher.dispatch_guild(guild_id, 'CHANNEL_CREATE', chan) await app.dispatcher.dispatch_guild(
guild_id, 'CHANNEL_CREATE', chan)
return jsonify(chan) return jsonify(chan)
@ -271,15 +346,16 @@ async def modify_channel_pos(guild_id):
@bp.route('/<int:guild_id>/members/<int:member_id>', methods=['GET']) @bp.route('/<int:guild_id>/members/<int:member_id>', methods=['GET'])
async def get_guild_member(guild_id, member_id): async def get_guild_member(guild_id, member_id):
"""Get a member's information in 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)
member = await app.storage.get_single_member(guild_id, member_id) member = await app.storage.get_single_member(guild_id, member_id)
return jsonify(member) return jsonify(member)
@bp.route('/<int:guild_id>/members', methods=['GET']) @bp.route('/<int:guild_id>/members', methods=['GET'])
async def get_members(guild_id): async def get_members(guild_id):
"""Get members inside 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)
@ -304,6 +380,7 @@ async def get_members(guild_id):
@bp.route('/<int:guild_id>/members/<int:member_id>', methods=['PATCH']) @bp.route('/<int:guild_id>/members/<int:member_id>', methods=['PATCH'])
async def modify_guild_member(guild_id, member_id): async def modify_guild_member(guild_id, member_id):
"""Modify a members' information in a guild."""
j = await request.get_json() j = await request.get_json()
if 'nick' in j: if 'nick' in j:
@ -350,6 +427,7 @@ async def modify_guild_member(guild_id, member_id):
@bp.route('/<int:guild_id>/members/@me/nick', methods=['PATCH']) @bp.route('/<int:guild_id>/members/@me/nick', methods=['PATCH'])
async def update_nickname(guild_id): async def update_nickname(guild_id):
"""Update a member's nickname in 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)
@ -371,28 +449,36 @@ async def update_nickname(guild_id):
return j['nick'] return j['nick']
@bp.route('/<int:guild_id>/members/<int:member_id>', methods=['DELETE']) async def remove_member(guild_id: int, member_id: int):
async def kick_member(guild_id, member_id): """Do common tasks related to deleting a member from the guild,
user_id = await token_check() such as dispatching GUILD_DELETE and GUILD_MEMBER_REMOVE."""
# TODO: check KICK_MEMBERS permission
await guild_owner_check(user_id, guild_id)
await app.db.execute(""" await app.db.execute("""
DELETE FROM members DELETE FROM members
WHERE guild_id = $1 AND user_id = $2 WHERE guild_id = $1 AND user_id = $2
""", guild_id, member_id) """, guild_id, member_id)
await app.dispatcher.dispatch_user(user_id, 'GUILD_DELETE', { await app.dispatcher.dispatch_user(member_id, 'GUILD_DELETE', {
'guild_id': guild_id, 'guild_id': guild_id,
'unavailable': False, 'unavailable': False,
}) })
await app.dispatcher.unsub('guild', guild_id, member_id)
await app.dispatcher.dispatch_guild(guild_id, 'GUILD_MEMBER_REMOVE', { await app.dispatcher.dispatch_guild(guild_id, 'GUILD_MEMBER_REMOVE', {
'guild': guild_id, 'guild': guild_id,
'user': await app.storage.get_user(member_id), 'user': await app.storage.get_user(member_id),
}) })
@bp.route('/<int:guild_id>/members/<int:member_id>', methods=['DELETE'])
async def kick_member(guild_id, member_id):
"""Remove a member from a guild."""
user_id = await token_check()
# TODO: check KICK_MEMBERS permission
await guild_owner_check(user_id, guild_id)
await remove_member(guild_id, member_id)
return '', 204 return '', 204
@ -434,22 +520,7 @@ async def create_ban(guild_id, member_id):
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
""", guild_id, member_id, j.get('reason', '')) """, guild_id, member_id, j.get('reason', ''))
await app.db.execute(""" await remove_member(guild_id, member_id)
DELETE FROM members
WHERE guild_id = $1 AND user_id = $2
""", guild_id, user_id)
await app.dispatcher.dispatch_user(member_id, 'GUILD_DELETE', {
'guild_id': guild_id,
'unavailable': False,
})
await app.dispatcher.unsub('guild', guild_id, member_id)
await app.dispatcher.dispatch_guild(guild_id, 'GUILD_MEMBER_REMOVE', {
'guild': guild_id,
'user': await app.storage.get_user(member_id),
})
await app.dispatcher.dispatch_guild(guild_id, 'GUILD_BAN_ADD', {**{ await app.dispatcher.dispatch_guild(guild_id, 'GUILD_BAN_ADD', {**{
'guild': guild_id, 'guild': guild_id,
@ -460,6 +531,10 @@ async def create_ban(guild_id, member_id):
@bp.route('/<int:guild_id>/messages/search') @bp.route('/<int:guild_id>/messages/search')
async def search_messages(guild_id): async def search_messages(guild_id):
"""Search messages in a guild.
This is an undocumented route.
"""
user_id = await token_check() user_id = await token_check()
await guild_check(user_id, guild_id) await guild_check(user_id, guild_id)
@ -474,6 +549,7 @@ async def search_messages(guild_id):
@bp.route('/<int:guild_id>/ack', methods=['POST']) @bp.route('/<int:guild_id>/ack', methods=['POST'])
async def ack_guild(guild_id): async def ack_guild(guild_id):
"""ACKnowledge all messages in the 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)

54
litecord/permissions.py Normal file
View File

@ -0,0 +1,54 @@
import ctypes
# so we don't keep repeating the same
# type for all the fields
_i = ctypes.c_uint8
class _RawPermsBits(ctypes.LittleEndianStructure):
"""raw bitfield for discord's permission number."""
_fields_ = [
('create_invites', _i, 1),
('kick_members', _i, 1),
('ban_members', _i, 1),
('administrator', _i, 1),
('manage_channels', _i, 1),
('manage_guild', _i, 1),
('add_reactions', _i, 1),
('view_audit_log', _i, 1),
('priority_speaker', _i, 1),
('_unused1', _i, 1),
('read_messages', _i, 1),
('send_messages', _i, 1),
('send_tts', _i, 1),
('manage_messages', _i, 1),
('embed_links', _i, 1),
('attach_files', _i, 1),
('read_history', _i, 1),
('mention_everyone', _i, 1),
('external_emojis', _i, 1),
('_unused2', _i, 1),
('connect', _i, 1),
('speak', _i, 1),
('mute_members', _i, 1),
('deafen_members', _i, 1),
('move_members', _i, 1),
('use_voice_activation', _i, 1),
('change_nickname', _i, 1),
('manage_nicknames', _i, 1),
('manage_roles', _i, 1),
('manage_webhooks', _i, 1),
('manage_emojis', _i, 1),
]
class Permissions(ctypes.Union):
_fields_ = [
('bits', _RawPermsBits),
('binary', ctypes.c_uint64),
]
def __init__(self, val: int):
self.binary = val
def numby(self):
return self.binary

View File

@ -1,11 +1,13 @@
import re import re
from typing import Union, Dict, List, Any
from cerberus import Validator from cerberus import Validator
from logbook import Logger from logbook import Logger
from .errors import BadRequest from .errors import BadRequest
from .permissions import Permissions
from .enums import ActivityType, StatusType, ExplicitFilter, \ from .enums import ActivityType, StatusType, ExplicitFilter, \
RelationshipType, MessageNotifications RelationshipType, MessageNotifications, ChannelType
log = Logger(__name__) log = Logger(__name__)
@ -61,6 +63,9 @@ class LitecordValidator(Validator):
def _validate_type_activity_type(self, value: int) -> bool: def _validate_type_activity_type(self, value: int) -> bool:
return value in ActivityType.values() return value in ActivityType.values()
def _validate_type_channel_type(self, value: int) -> bool:
return value in ChannelType.values()
def _validate_type_status_external(self, value: str) -> bool: def _validate_type_status_external(self, value: str) -> bool:
statuses = StatusType.values() statuses = StatusType.values()
@ -94,8 +99,19 @@ class LitecordValidator(Validator):
return val in MessageNotifications.values() return val in MessageNotifications.values()
def _validate_type_guild_name(self, value: str) -> bool:
return 2 <= len(value) <= 100
def validate(reqjson, schema, raise_err: bool = True): def _validate_type_channel_name(self, value: str) -> bool:
# for now, we'll use the same validation for guild_name
return self._validate_type_guild_name(value)
def validate(reqjson: Union[Dict, List], schema: Dict,
raise_err: bool = True) -> Union[Dict, List]:
"""Validate a given document (user-input) and give
the correct document as a result.
"""
validator = LitecordValidator(schema) validator = LitecordValidator(schema)
if not validator.validate(reqjson): if not validator.validate(reqjson):
@ -146,12 +162,44 @@ USER_UPDATE = {
} }
PARTIAL_ROLE_GUILD_CREATE = {
'name': {'type': 'role_name'},
'color': {'type': 'number', 'default': 0},
'hoist': {'type': 'boolean', 'default': False},
# NOTE: no position on partial role (on guild create)
'permissions': {'coerce': Permissions, 'required': False},
'mentionable': {'type': 'boolean', 'default': False},
}
PARTIAL_CHANNEL_GUILD_CREATE = {
'name': {'type': 'channel_name'},
'type': {'type': 'channel_type'}
}
GUILD_CREATE = {
'name': {'type': 'guild_name'},
'region': {'type': 'voice_region'},
'icon': {'type': 'icon', 'required': False, 'nullable': True},
'verification_level': {
'type': 'verification_level', 'default': 0},
'default_message_notifications': {
'type': 'msg_notifications', 'default': 0},
'explicit_content_filter': {
'type': 'explicit', 'default': 0},
'roles': {
'type': 'list', 'required': False,
'schema': PARTIAL_ROLE_GUILD_CREATE},
'channels': {
'type': 'list', 'default': [], 'schema': PARTIAL_CHANNEL_GUILD_CREATE},
}
GUILD_UPDATE = { GUILD_UPDATE = {
'name': { 'name': {
'type': 'string', 'type': 'guild_name',
'minlength': 2,
'maxlength': 100,
'required': False 'required': False
}, },
'region': {'type': 'voice_region', 'required': False}, 'region': {'type': 'voice_region', 'required': False},