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
bp = Blueprint('guilds', __name__)
DEFAULT_EVERYONE_PERMS = 104324161
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)
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'])
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()
j = await request.get_json()
@ -66,36 +175,27 @@ async def create_guild():
j.get('default_message_notifications', 0),
j.get('explicit_content_filter', 0))
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)
await add_member(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("""
INSERT INTO roles (id, guild_id, name, position, permissions)
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()
await app.db.execute("""
INSERT INTO channels (id, channel_type)
VALUES ($1, $2)
""", general_id, ChannelType.GUILD_TEXT.value)
await create_guild_channel(
guild_id, general_id, ChannelType.GUILD_TEXT,
name='general')
await app.db.execute("""
INSERT INTO guild_channels (id, guild_id, name, position)
VALUES ($1, $2, $3, $4)
""", general_id, guild_id, 'general', 0)
if j.get('roles'):
await guild_create_roles_prep(guild_id, j['roles'])
await app.db.execute("""
INSERT INTO guild_text_channels (id)
VALUES ($1)
""", general_id)
# TODO: j['roles'] and j['channels']
if j.get('channels'):
await guild_create_channels_prep(guild_id, j['channels'])
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'])
async def get_guild(guild_id):
"""Get a single guilds' information."""
user_id = await token_check()
await guild_check(user_id, guild_id)
gj = await app.storage.get_guild(guild_id, user_id)
gj_extra = await app.storage.get_guild_extra(guild_id, user_id, 250)
return jsonify({**gj, **gj_extra})
return jsonify(
await app.storage.get_guild_full(guild_id, user_id, 250)
)
@bp.route('/<int:guild_id>', methods=['UPDATE'])
@ -139,8 +240,6 @@ async def update_guild(guild_id):
""", j['name'], guild_id)
if 'region' in j:
# TODO: check region value
await app.db.execute("""
UPDATE guilds
SET region = $1
@ -167,15 +266,14 @@ async def update_guild(guild_id):
WHERE guild_id = $2
""", j[field], guild_id)
# return guild object
gj = await app.storage.get_guild(guild_id, user_id)
gj_extra = await app.storage.get_guild_extra(guild_id, user_id, 250)
guild = await app.storage.get_guild_full(
guild_id, user_id
)
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({**gj, **gj_extra})
return jsonify(guild)
@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 app.db.execute("""
DELETE FROM guild
DELETE FROM guilds
WHERE guilds.id = $1
""", guild_id)
@ -219,42 +317,19 @@ async def create_channel(guild_id):
# TODO: check permissions for MANAGE_CHANNELS
await guild_check(user_id, guild_id)
new_channel_id = get_snowflake()
channel_type = j.get('type', ChannelType.GUILD_TEXT)
channel_type = ChannelType(channel_type)
if channel_type not in (ChannelType.GUILD_TEXT,
ChannelType.GUILD_VOICE):
raise BadRequest('Invalid channel type')
await app.db.execute("""
INSERT INTO channels (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()
new_channel_id = get_snowflake()
await create_guild_channel(guild_id, new_channel_id, channel_type,)
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)
@ -271,15 +346,16 @@ async def modify_channel_pos(guild_id):
@bp.route('/<int:guild_id>/members/<int:member_id>', methods=['GET'])
async def get_guild_member(guild_id, member_id):
"""Get a member's information in a guild."""
user_id = await token_check()
await guild_check(user_id, guild_id)
member = await app.storage.get_single_member(guild_id, member_id)
return jsonify(member)
@bp.route('/<int:guild_id>/members', methods=['GET'])
async def get_members(guild_id):
"""Get members inside a guild."""
user_id = await token_check()
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'])
async def modify_guild_member(guild_id, member_id):
"""Modify a members' information in a guild."""
j = await request.get_json()
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'])
async def update_nickname(guild_id):
"""Update a member's nickname in a guild."""
user_id = await token_check()
await guild_check(user_id, guild_id)
@ -371,28 +449,36 @@ async def update_nickname(guild_id):
return j['nick']
@bp.route('/<int:guild_id>/members/<int:member_id>', methods=['DELETE'])
async def kick_member(guild_id, member_id):
user_id = await token_check()
# TODO: check KICK_MEMBERS permission
await guild_owner_check(user_id, guild_id)
async def remove_member(guild_id: int, member_id: int):
"""Do common tasks related to deleting a member from the guild,
such as dispatching GUILD_DELETE and GUILD_MEMBER_REMOVE."""
await app.db.execute("""
DELETE FROM members
WHERE guild_id = $1 AND user_id = $2
""", 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,
'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),
})
@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
@ -434,22 +520,7 @@ async def create_ban(guild_id, member_id):
VALUES ($1, $2, $3)
""", guild_id, member_id, j.get('reason', ''))
await app.db.execute("""
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 remove_member(guild_id, member_id)
await app.dispatcher.dispatch_guild(guild_id, 'GUILD_BAN_ADD', {**{
'guild': guild_id,
@ -460,6 +531,10 @@ async def create_ban(guild_id, member_id):
@bp.route('/<int:guild_id>/messages/search')
async def search_messages(guild_id):
"""Search messages in a guild.
This is an undocumented route.
"""
user_id = await token_check()
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'])
async def ack_guild(guild_id):
"""ACKnowledge all messages in the guild."""
user_id = await token_check()
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
from typing import Union, Dict, List, Any
from cerberus import Validator
from logbook import Logger
from .errors import BadRequest
from .permissions import Permissions
from .enums import ActivityType, StatusType, ExplicitFilter, \
RelationshipType, MessageNotifications
RelationshipType, MessageNotifications, ChannelType
log = Logger(__name__)
@ -61,6 +63,9 @@ class LitecordValidator(Validator):
def _validate_type_activity_type(self, value: int) -> bool:
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:
statuses = StatusType.values()
@ -94,8 +99,19 @@ class LitecordValidator(Validator):
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)
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 = {
'name': {
'type': 'string',
'minlength': 2,
'maxlength': 100,
'type': 'guild_name',
'required': False
},
'region': {'type': 'voice_region', 'required': False},