pubsub.lazy_guilds: add support for 'everyone'-type GMLs

if the everyone role can read the channel, then the member list
will be equivalent to any other list where the everyone role
can read the channel. with this in mind we can generate a "global"
member list directed only for that usecase.

 - permissions: add role_permissions
This commit is contained in:
Luna Mendes 2018-11-07 04:11:17 -03:00
parent 86923cc6e3
commit 55f8919689
3 changed files with 82 additions and 43 deletions

View File

@ -65,6 +65,20 @@ class Permissions(ctypes.Union):
ALL_PERMISSIONS = Permissions(0b01111111111101111111110111111111) ALL_PERMISSIONS = Permissions(0b01111111111101111111110111111111)
async def get_role_perms(guild_id, role_id, storage=None) -> Permissions:
"""Get the raw :class:`Permissions` object for a role."""
if not storage:
storage = app.storage
perms = await storage.db.fetchval("""
SELECT permissions
FROM roles
WHERE guild_id = $1 AND id = $2
""", guild_id, role_id)
return Permissions(perms)
async def base_permissions(member_id, guild_id, storage=None) -> Permissions: async def base_permissions(member_id, guild_id, storage=None) -> Permissions:
"""Compute the base permissions for a given user. """Compute the base permissions for a given user.
@ -89,13 +103,7 @@ async def base_permissions(member_id, guild_id, storage=None) -> Permissions:
return ALL_PERMISSIONS return ALL_PERMISSIONS
# get permissions for @everyone # get permissions for @everyone
everyone_perms = await storage.db.fetchval(""" permissions = await get_role_perms(guild_id, guild_id, storage)
SELECT permissions
FROM roles
WHERE guild_id = $1
""", guild_id)
permissions = Permissions(everyone_perms)
role_ids = await storage.db.fetch(""" role_ids = await storage.db.fetch("""
SELECT role_id SELECT role_id
@ -149,6 +157,26 @@ def overwrite_find_mix(perms: Permissions, overwrites: dict,
return perms return perms
async def role_permissions(guild_id: int, role_id: int,
channel_id: int, storage=None) -> Permissions:
"""Get the permissions for a role, in relation to a channel"""
if not storage:
storage = app.storage
perms = await get_role_perms(guild_id, role_id, storage)
overwrite = await storage.db.fetchrow("""
SELECT allow, deny
FROM channel_overwrites
WHERE channel_id = $1 AND target_type = $2 AND target_role = $3
""", channel_id, 1, role_id)
if overwrite:
perms = overwrite_mix(perms, overwrite)
return perms
async def compute_overwrites(base_perms, user_id, channel_id: int, async def compute_overwrites(base_perms, user_id, channel_id: int,
guild_id: int = None, storage=None): guild_id: int = None, storage=None):
"""Compute the permissions in the context of a channel.""" """Compute the permissions in the context of a channel."""

View File

@ -10,7 +10,7 @@ from logbook import Logger
from litecord.pubsub.dispatcher import Dispatcher from litecord.pubsub.dispatcher import Dispatcher
from litecord.permissions import ( from litecord.permissions import (
Permissions, overwrite_find_mix, get_permissions Permissions, overwrite_find_mix, get_permissions, role_permissions
) )
log = Logger(__name__) log = Logger(__name__)
@ -90,16 +90,15 @@ class GuildMemberList:
# a really long chain of classes to get # a really long chain of classes to get
# to the storage instance... # to the storage instance...
main = main_lg.main_dispatcher self.main = main_lg
self.storage = main.app.storage self.storage = self.main.app.storage
self.presence = main.app.presence self.presence = self.main.app.presence
self.state_man = main.app.state_manager self.state_man = self.main.app.state_manager
self.list = MemberList(None, None, None, None) self.list = MemberList(None, None, None, None)
#: holds the state of subscribed shards #: {session_id: set[list]}
# to this channels' member list self.state = defaultdict(set)
self.state = set()
def _set_empty_list(self): def _set_empty_list(self):
self.list = MemberList(None, None, None, None) self.list = MemberList(None, None, None, None)
@ -296,14 +295,16 @@ class GuildMemberList:
return res return res
async def sub(self, session_id: str): async def sub(self, _session_id: str):
"""Subscribe a shard to the member list.""" """Subscribe a shard to the member list."""
await self._init_check() await self._init_check()
self.state.add(session_id)
async def unsub(self, session_id: str): async def unsub(self, session_id: str):
"""Unsubscribe a shard from the member list""" """Unsubscribe a shard from the member list"""
self.state.discard(session_id) try:
self.state.pop(session_id)
except KeyError:
pass
# once we reach 0 subscribers, # once we reach 0 subscribers,
# we drop the current member list we have (for memory) # we drop the current member list we have (for memory)
@ -327,6 +328,29 @@ class GuildMemberList:
ranges of the list that we want. ranges of the list that we want.
""" """
# a guild list with a channel id of the guild
# represents the 'everyone' global list.
list_id = ('everyone'
if self.channel_id == self.guild_id
else str(self.channel_id))
# if everyone can read the channel,
# we direct the request to the 'everyone' gml instance
# instead of the current one.
everyone_perms = await role_permissions(
self.guild_id,
self.guild_id,
self.channel_id,
storage=self.storage
)
if everyone_perms.bits.read_messages and list_id != 'everyone':
everyone_gml = await self.main.get_gml(self.guild_id)
return await everyone_gml.shard_query(
session_id, ranges
)
await self._init_check() await self._init_check()
# make sure this is a sane state # make sure this is a sane state
@ -335,22 +359,9 @@ class GuildMemberList:
await self.unsub(session_id) await self.unsub(session_id)
return return
# since this is a sane state AND
# trying to query, we automatically
# subscribe the state to this list
await self.sub(session_id)
# TODO: subscribe shard to the 'everyone' member list
# and forward the query to that list
reply = { reply = {
'guild_id': str(self.guild_id), 'guild_id': str(self.guild_id),
'id': list_id,
# TODO: everyone for channels without overrides
# channel_id for channels WITH overrides.
'id': 'everyone',
# 'id': str(self.channel_id),
'groups': [ 'groups': [
{ {
@ -386,22 +397,17 @@ class GuildMemberList:
return list(self.state) return list(self.state)
async def dispatch(self, event: str, data: Any): async def dispatch(self, event: str, data: Any):
"""The dispatch() method here, instead of being """Modify the member list and dispatch the respective
about dispatching a single event to the subscribed events to subscribed shards.
users and forgetting about it, is about storing
the actual member list information so that we
can generate the respective events to the users.
GuildMemberList stores the current guilds' list GuildMemberList stores the current guilds' list
in its :attr:`GuildMemberList.member_list` attribute, in its :attr:`GuildMemberList.list` attribute,
with that attribute being modified via different with that attribute being modified via different
calls to :meth:`GuildMemberList.dispatch` calls to :meth:`GuildMemberList.dispatch`
""" """
if self.member_list is None: # if no subscribers, drop event
# if the list is currently uninitialized, if not self.list:
# no subscribers actually happened, so
# we can safely drop the incoming event.
return return
@ -436,6 +442,11 @@ class LazyGuildDispatcher(Dispatcher):
channel_id channel_id
) )
# if we don't find a guild, we just
# set it the same as the channel.
if not guild_id:
guild_id = channel_id
gml = GuildMemberList(guild_id, channel_id, self) gml = GuildMemberList(guild_id, channel_id, self)
self.state[channel_id] = gml self.state[channel_id] = gml
self.guild_map[guild_id].append(channel_id) self.guild_map[guild_id].append(channel_id)

View File

@ -331,7 +331,7 @@ CREATE TABLE IF NOT EXISTS channel_overwrites (
channel_id bigint REFERENCES channels (id) ON DELETE CASCADE, channel_id bigint REFERENCES channels (id) ON DELETE CASCADE,
-- target_type = 0 -> use target_user -- target_type = 0 -> use target_user
-- target_type = 1 -> user target_role -- target_type = 1 -> use target_role
-- discord already has overwrite.type = 'role' | 'member' -- discord already has overwrite.type = 'role' | 'member'
-- so this allows us to be more compliant with the API -- so this allows us to be more compliant with the API
target_type integer default null, target_type integer default null,