From d1d54a468a88323a8ef7798ff084a1371d5893ec Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 17 Oct 2016 18:25:23 -0400 Subject: Rename Server to Guild everywhere. --- discord/guild.py | 377 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 discord/guild.py (limited to 'discord/guild.py') diff --git a/discord/guild.py b/discord/guild.py new file mode 100644 index 00000000..ad713bb6 --- /dev/null +++ b/discord/guild.py @@ -0,0 +1,377 @@ +# -*- coding: utf-8 -*- + +""" +The MIT License (MIT) + +Copyright (c) 2015-2016 Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from . import utils +from .role import Role +from .member import Member, VoiceState +from .emoji import Emoji +from .game import Game +from .channel import * +from .enums import GuildRegion, Status, ChannelType, try_enum, VerificationLevel +from .mixins import Hashable + +import copy + +class Guild(Hashable): + """Represents a Discord guild. + + This is referred to as a "server" in the official Discord UI. + + Supported Operations: + + +-----------+--------------------------------------+ + | Operation | Description | + +===========+======================================+ + | x == y | Checks if two guilds are equal. | + +-----------+--------------------------------------+ + | x != y | Checks if two guilds are not equal. | + +-----------+--------------------------------------+ + | hash(x) | Returns the guild's hash. | + +-----------+--------------------------------------+ + | str(x) | Returns the guild's name. | + +-----------+--------------------------------------+ + + Attributes + ---------- + name: str + The guild name. + me: :class:`Member` + Similar to :attr:`Client.user` except an instance of :class:`Member`. + This is essentially used to get the member version of yourself. + roles + A list of :class:`Role` that the guild has available. + emojis + A list of :class:`Emoji` that the guild owns. + region: :class:`GuildRegion` + The region the guild belongs on. There is a chance that the region + will be a ``str`` if the value is not recognised by the enumerator. + afk_timeout: int + The timeout to get sent to the AFK channel. + afk_channel: :class:`Channel` + The channel that denotes the AFK channel. None if it doesn't exist. + members + An iterable of :class:`Member` that are currently on the guild. + channels + An iterable of :class:`Channel` that are currently on the guild. + icon: str + The guild's icon. + id: int + The guild's ID. + owner_id: int + The guild owner's ID. Use :attr:`Guild.owner` instead. + unavailable: bool + Indicates if the guild is unavailable. If this is ``True`` then the + reliability of other attributes outside of :meth:`Guild.id` is slim and they might + all be None. It is best to not do anything with the guild if it is unavailable. + + Check the :func:`on_guild_unavailable` and :func:`on_guild_available` events. + large: bool + Indicates if the guild is a 'large' guild. A large guild is defined as having + more than ``large_threshold`` count members, which for this library is set to + the maximum of 250. + voice_client: Optional[:class:`VoiceClient`] + The VoiceClient associated with this guild. A shortcut for the + :meth:`Client.voice_client_in` call. + mfa_level: int + Indicates the guild's two factor authorisation level. If this value is 0 then + the guild does not require 2FA for their administrative members. If the value is + 1 then they do. + verification_level: :class:`VerificationLevel` + The guild's verification level. + features: List[str] + A list of features that the guild has. They are currently as follows: + + - ``VIP_REGIONS``: Guild has VIP voice regions + - ``VANITY_URL``: Guild has a vanity invite URL (e.g. discord.gg/discord-api) + - ``INVITE_SPLASH``: Guild's invite page has a special splash. + + splash: str + The guild's invite splash. + """ + + __slots__ = ('afk_timeout', 'afk_channel', '_members', '_channels', 'icon', + 'name', 'id', 'unavailable', 'name', 'region', '_state', + '_default_role', '_default_channel', 'roles', '_member_count', + 'large', 'owner_id', 'mfa_level', 'emojis', 'features', + 'verification_level', 'splash', '_voice_states' ) + + def __init__(self, *, data, state): + self._channels = {} + self._members = {} + self._voice_states = {} + self._state = state + self._from_data(data) + + @property + def channels(self): + return self._channels.values() + + def get_channel(self, channel_id): + """Returns a :class:`Channel` with the given ID. If not found, returns None.""" + return self._channels.get(channel_id) + + def _add_channel(self, channel): + self._channels[channel.id] = channel + + def _remove_channel(self, channel): + self._channels.pop(channel.id, None) + + def _voice_state_for(self, user_id): + return self._voice_states.get(user_id) + + @property + def members(self): + return self._members.values() + + def get_member(self, user_id): + """Returns a :class:`Member` with the given ID. If not found, returns None.""" + return self._members.get(user_id) + + def _add_member(self, member): + self._members[member.id] = member + + def _remove_member(self, member): + self._members.pop(member.id, None) + + def __str__(self): + return self.name + + def _update_voice_state(self, data, channel_id): + user_id = int(data['user_id']) + channel = self.get_channel(channel_id) + try: + # check if we should remove the voice state from cache + if channel is None: + after = self._voice_states.pop(user_id) + else: + after = self._voice_states[user_id] + + before = copy.copy(after) + after._update(data, channel) + except KeyError: + # if we're here then we're getting added into the cache + after = VoiceState(data=data, channel=channel) + before = VoiceState(data=data, channel=None) + self._voice_states[user_id] = after + + member = self.get_member(user_id) + if member is not None: + old = before.channel + # update the references pointed to by the voice channels + if old is None and channel is not None: + # we joined a channel + channel.voice_members.append(member) + elif old is not None: + try: + # we either left a channel or switched channels + old.voice_members.remove(member) + except ValueError: + pass + finally: + # we switched channels + if channel is not None: + channel.voice_members.append(self) + + return member, before, after + + def _add_role(self, role): + # roles get added to the bottom (position 1, pos 0 is @everyone) + # so since self.roles has the @everyone role, we can't increment + # its position because it's stuck at position 0. Luckily x += False + # is equivalent to adding 0. So we cast the position to a bool and + # increment it. + for r in self.roles: + r.position += bool(r.position) + + self.roles.append(role) + + def _remove_role(self, role): + # this raises ValueError if it fails.. + self.roles.remove(role) + + # since it didn't, we can change the positions now + # basically the same as above except we only decrement + # the position if we're above the role we deleted. + for r in self.roles: + r.position -= r.position > role.position + + def _from_data(self, guild): + # according to Stan, this is always available even if the guild is unavailable + # I don't have this guarantee when someone updates the guild. + member_count = guild.get('member_count', None) + if member_count: + self._member_count = member_count + + self.name = guild.get('name') + self.region = try_enum(GuildRegion, guild.get('region')) + self.verification_level = try_enum(VerificationLevel, guild.get('verification_level')) + self.afk_timeout = guild.get('afk_timeout') + self.icon = guild.get('icon') + self.unavailable = guild.get('unavailable', False) + self.id = int(guild['id']) + self.roles = [Role(guild=self, data=r, state=self._state) for r in guild.get('roles', [])] + self.mfa_level = guild.get('mfa_level') + self.emojis = [Emoji(server=self, data=r, state=self._state) for r in guild.get('emojis', [])] + self.features = guild.get('features', []) + self.splash = guild.get('splash') + + for mdata in guild.get('members', []): + roles = [self.default_role] + for role_id in mdata['roles']: + role = utils.find(lambda r: r.id == role_id, self.roles) + if role is not None: + roles.append(role) + + mdata['roles'] = roles + member = Member(data=mdata, guild=self, state=self._state) + self._add_member(member) + + self._sync(guild) + self.large = None if member_count is None else self._member_count >= 250 + + self.owner_id = utils._get_as_snowflake(guild, 'owner_id') + self.afk_channel = self.get_channel(utils._get_as_snowflake(guild, 'afk_channel_id')) + + for obj in guild.get('voice_states', []): + self._update_voice_state(obj, int(obj['channel_id'])) + + def _sync(self, data): + try: + self.large = data['large'] + except KeyError: + pass + + for presence in data.get('presences', []): + user_id = int(presence['user']['id']) + member = self.get_member(user_id) + if member is not None: + member.status = try_enum(Status, presence['status']) + game = presence.get('game', {}) + member.game = Game(**game) if game else None + + if 'channels' in data: + channels = data['channels'] + for c in channels: + if c['type'] == ChannelType.text.value: + channel = TextChannel(guild=self, data=c, state=self._state) + else: + channel = VoiceChannel(guild=self, data=c, state=self._state) + + self._add_channel(channel) + + @utils.cached_slot_property('_default_role') + def default_role(self): + """Gets the @everyone role that all members have by default.""" + return utils.find(lambda r: r.is_everyone, self.roles) + + @utils.cached_slot_property('_default_channel') + def default_channel(self): + """Gets the default :class:`Channel` for the guild.""" + return utils.find(lambda c: c.is_default, self.channels) + + @property + def owner(self): + """:class:`Member`: The member that owns the guild.""" + return self.get_member(self.owner_id) + + @property + def icon_url(self): + """Returns the URL version of the guild's icon. Returns an empty string if it has no icon.""" + if self.icon is None: + return '' + return 'https://discordapp.com/api/guilds/{0.id}/icons/{0.icon}.jpg'.format(self) + + @property + def splash_url(self): + """Returns the URL version of the server's invite splash. Returns an empty string if it has no splash.""" + if self.splash is None: + return '' + return 'https://cdn.discordapp.com/splashes/{0.id}/{0.splash}.jpg?size=2048'.format(self) + + @property + def member_count(self): + """Returns the true member count regardless of it being loaded fully or not.""" + return self._member_count + + @property + def created_at(self): + """Returns the guild's creation time in UTC.""" + return utils.snowflake_time(self.id) + + @property + def role_hierarchy(self): + """Returns the guild's roles in the order of the hierarchy. + + The first element of this list will be the highest role in the + hierarchy. + """ + return sorted(self.roles, reverse=True) + + def get_member_named(self, name): + """Returns the first member found that matches the name provided. + + The name can have an optional discriminator argument, e.g. "Jake#0001" + or "Jake" will both do the lookup. However the former will give a more + precise result. Note that the discriminator must have all 4 digits + for this to work. + + If a nickname is passed, then it is looked up via the nickname. Note + however, that a nickname + discriminator combo will not lookup the nickname + but rather the username + discriminator combo due to nickname + discriminator + not being unique. + + If no member is found, ``None`` is returned. + + Parameters + ----------- + name: str + The name of the member to lookup with an optional discriminator. + + Returns + -------- + :class:`Member` + The member in this guild with the associated name. If not found + then ``None`` is returned. + """ + + result = None + members = self.members + if len(name) > 5 and name[-5] == '#': + # The 5 length is checking to see if #0000 is in the string, + # as a#0000 has a length of 6, the minimum for a potential + # discriminator lookup. + potential_discriminator = name[-4:] + + # do the actual lookup and return if found + # if it isn't found then we'll do a full name lookup below. + result = utils.get(members, name=name[:-5], discriminator=potential_discriminator) + if result is not None: + return result + + def pred(m): + return m.nick == name or m.name == name + + return utils.find(pred, members) -- cgit v1.2.3