class CoreSettings(Model): name = fields.CharField(primary_key=True, max_length=64) prefixes = fields.SeparatedValuesField(max_length=256, default=['!']) description = fields.TextField(max_length=512, blank=True, null=True) status = fields.CharField(max_length=128, blank=True, null=True) lang = fields.LanguageField() home = fields.GuildField(blank=True, null=True, on_delete=fields.SET_NULL)
class Guild(DiscordModel): # TODO normalize Guild register_time = fields.DatetimeField(auto_now_add=True) invite_code = fields.CharField(max_length=64, db_index=True) url = fields.CharField(max_length=256, unique=True) is_deleted = fields.BooleanField(default=False) prefix = fields.CharField(max_length=64) lang = fields.LanguageField(default=Languages.default.value) members = fields.ManyUsersField('hero.User', through='hero.Member', forward_key='user', backward_key='guild', related_name='guilds') @property def invite_url(self): return f'https://discord.gg/{self.invite_code}' @invite_url.setter def invite_url(self, value: str): if not isinstance(value, str): raise TypeError("invite_url must be a str") try: self.invite_code = value.split('://discord.gg/')[1] except IndexError: try: self.invite_code = value.split('://discordapp.com/invite/')[1] except IndexError: raise ValueError("Not a valid invite URL.")
class Guild(DiscordModel): id = fields.BigIntegerField(primary_key=True) home = fields.BooleanField(default=False) # shard_id = fields.SmallIntegerField(db_index=True) register_time = fields.DateTimeField(auto_now_add=True) invite_code = fields.CharField(null=True, blank=True, max_length=64, db_index=True) prefix = fields.CharField(null=True, blank=True, max_length=64) notifications_channel = fields.OneToOneField( 'TextChannel', related_name='notifying_guild', null=True, blank=True, on_delete=fields.SET_NULL) language = fields.LanguageField() members = fields.ManyToManyField('User', through='Member') _discord_cls = discord.Guild @property def invite_url(self): if self.invite_code is None: return None return f'https://discord.gg/{self.invite_code}' @invite_url.setter def invite_url(self, value: str): if not isinstance(value, str): raise TypeError("invite_url must be a str") try: self.invite_code = value.split('://discord.gg/')[1] except IndexError: try: self.invite_code = value.split('://discordapp.com/invite/')[1] except IndexError: try: self.invite_code = value.split('://discord.com/invite/')[1] except IndexError: raise ValueError("Not a valid invite URL.") async def notify(self, content: str = None, **send_kwargs): notifications_channel = await self.notifications_channel if notifications_channel is None: raise ValueError("notifications_channel needs to be set first") dest = await notifications_channel.fetch() msg = await dest.send(content, **send_kwargs) return msg async def fetch(self) -> discord.Guild: discord_guild = self._core.get_guild(self.id) if discord_guild is None: discord_guild = await self._core.fetch_guild(self.id) self._discord_obj = discord_guild return discord_guild
class ScheduledTask(models.Model): extension_name = fields.CharField(max_length=64) method_name = fields.CharField(max_length=128) kwargs = JSONField(null=True, blank=True) when = fields.DateTimeField(db_index=True) time_tolerance = fields.IntegerField( default=300, null=True) # seconds | None means infinite time tolerance context = fields.ForeignKey(Context, null=True, blank=True, on_delete=fields.SET_NULL)
class CoreSettings(AbstractSettings): name = fields.CharField(pk=True, max_length=64) token = fields.CharField(max_length=64) lang = fields.LanguageField(default=Languages.default.value) @property def logging_level(self): if hero.TEST: return logging.DEBUG else: return logging.WARNING
class Player(models.Model): user = fields.OneToOneField(models.User, primary_key=True, on_delete=fields.CASCADE) challonge_username = fields.CharField(null=True, blank=True, unique=True, max_length=64) challonge_user_id = fields.BigIntegerField(null=True, blank=True, unique=True) region = RegionField(null=True, blank=True, db_index=True) rating = fields.IntegerField(db_index=True, default=1500) deviation = fields.IntegerField(default=350) volatility = fields.FloatField(default=0.06) @async_using_db def get_last_ranked_match(self): from ..models import Match user = self.user qs = (Match.objects.filter( guild__guildsetup__verified=True, ranked=True, player_1=user) | Match.objects.filter(guild__guildsetup__verified=True, ranked=True, player_2=user)) return qs.latest()
class Ruleset(models.Model): class Meta: unique_together = (('name', 'guild', 'version'), ) get_latest_by = 'version' name = fields.CharField(max_length=128) guild = fields.GuildField(db_index=True, on_delete=fields.CASCADE) version = fields.IntegerField(default=1) starter_stages = fields.SeparatedValuesField( default=Stage.get_default_starters, max_length=64, converter=Stage.parse, serializer=Stage.serialize) counterpick_stages = fields.SeparatedValuesField( default=Stage.get_default_counterpicks, max_length=64, converter=Stage.parse, serializer=Stage.serialize) counterpick_bans = fields.SmallIntegerField(default=2) dsr = DSRField(default=DSR('on')) @classmethod async def convert(cls, ctx, argument): try: argument = int(argument) except ValueError: raise BadArgument( f"{argument} is not a valid identifier for a ruleset") return await cls.async_get(pk=argument) def __str__(self): return self.name
class Guild(DiscordModel): id = fields.BigIntegerField(primary_key=True) home = fields.BooleanField(default=False) shard_id = fields.SmallIntegerField(db_index=True) register_time = fields.DateTimeField(auto_now_add=True) invite_code = fields.CharField(null=True, blank=True, max_length=64, db_index=True) prefix = fields.CharField(null=True, blank=True, max_length=64) language = fields.LanguageField() members = fields.ManyToManyField(to='User', through='Member') moderating_guild = fields.GuildField(null=True, blank=True, on_delete=fields.SET_NULL) _discord_cls = discord.Guild @property def invite_url(self): if self.invite_code is None: return None return f'https://discord.gg/{self.invite_code}' @invite_url.setter def invite_url(self, value: str): if not isinstance(value, str): raise TypeError("invite_url must be a str") try: self.invite_code = value.split('://discord.gg/')[1] except IndexError: try: self.invite_code = value.split('://discordapp.com/invite/')[1] except IndexError: try: self.invite_code = value.split('://discord.com/invite/')[1] except IndexError: raise ValueError("Not a valid invite URL.") async def fetch(self) -> discord.Guild: discord_guild = self._core.get_guild(self.id) if discord_guild is None: discord_guild = await self._core.fetch_guild(self.id) self._discord_obj = discord_guild return discord_guild
class Emoji(DiscordModel): id = fields.BigAutoField(primary_key=True) name = fields.CharField(max_length=64) animated = fields.BooleanField(default=False) is_custom = fields.BooleanField() _discord_cls = discord.PartialEmoji _discord_converter_cls = converter.PartialEmojiConverter @classmethod def sync_from_discord_obj(cls, discord_obj, create_if_new=True): """Create a Hero object from a Discord object""" if not isinstance(discord_obj, (cls._discord_cls, discord.Emoji)): raise TypeError( f"discord_obj has to be a discord.{cls._discord_cls.__name__} " f"or discord.Emoji" f"but a {type(discord_obj).__name__} was passed") if isinstance(discord_obj, discord.Emoji): discord_obj = discord.PartialEmoji(name=discord_obj.name, animated=discord_obj.animated, id=discord_obj.id) if discord_obj.is_custom_emoji(): obj, created = cls.objects.get_or_create( id=discord_obj.id, name=discord_obj.name, animated=discord_obj.animated, is_custom=True) else: obj, created = cls.objects.get_or_create(name=discord_obj.name, animated=False, is_custom=False) obj._discord_obj = discord_obj return obj, not created async def fetch(self) -> discord.PartialEmoji: if self.is_custom: # if not self.guild.is_fetched: # await self.guild.fetch() guild = await self.guild await guild.fetch() emoji = await guild.fetch_emoji(self.id) discord_emoji = discord.PartialEmoji(name=emoji.name, animated=emoji.animated, id=emoji.id) if self.name != emoji.name: self.name = emoji.name await self.async_save() else: discord_emoji = discord.PartialEmoji(name=self.name) self._discord_obj = discord_emoji return discord_emoji
class MatchmakingSetup(models.Model): channel = fields.OneToOneField(models.TextChannel, primary_key=True, on_delete=fields.CASCADE) name = fields.CharField(max_length=64) matchmaking_message = fields.MessageField(on_delete=fields.CASCADE) ruleset = fields.ForeignKey(Ruleset, null=True, blank=True, on_delete=fields.SET_NULL) looking_role = fields.RoleField(on_delete=fields.CASCADE) available_role = fields.RoleField(on_delete=fields.CASCADE) ranked = fields.BooleanField(default=False)
class Emoji(DiscordModel): id = fields.BigIntegerField(primary_key=True, auto_created=True) name = fields.CharField(max_length=64) animated = fields.BooleanField(default=False) is_custom = fields.BooleanField() _discord_cls = discord.PartialEmoji _discord_converter_cls = converter.PartialEmojiConverter @classmethod def sync_from_discord_obj(cls, discord_obj): """Create a Hero object from a Discord object""" if not isinstance(discord_obj, (cls._discord_cls, discord.Emoji)): raise TypeError( f"discord_obj has to be a discord.{cls._discord_cls.__name__} " f"or discord.Emoji" f"but a {type(discord_obj).__name__} was passed") if isinstance(discord_obj, discord.Emoji): discord_obj = discord.PartialEmoji(name=discord_obj.name, animated=discord_obj.animated, id=discord_obj.id) obj = cls(id=discord_obj.id, name=discord_obj.name, animated=discord_obj.animated) obj._discord_obj = discord_obj try: obj.load() existed_already = True except cls.DoesNotExist: existed_already = False return obj, existed_already async def fetch(self) -> discord.PartialEmoji: if self.is_custom: if not self.guild.is_fetched: await self.guild.fetch() self.guild: discord.Guild emoji = await self.guild.fetch_emoji(self.id) discord_emoji = discord.PartialEmoji(name=emoji.name, animated=emoji.animated, id=emoji.id) if self.name != emoji.name: self.name = emoji.name await self.async_save() else: discord_emoji = discord.PartialEmoji(name=self.name) self._discord_obj = discord_emoji return discord_emoji
class Team(models.Model): class Meta: unique_together = (('member_1', 'member_2'), ) # member_1 is always the user with the lower user ID # to avoid duplicate teams member_1 = fields.ForeignKey(Player, db_index=True, on_delete=fields.CASCADE) member_2 = fields.ForeignKey(Player, db_index=True, on_delete=fields.CASCADE) custom_name = fields.CharField(null=True, max_length=64) current_tournament = fields.ForeignKey(Tournament, null=True, blank=True, on_delete=fields.SET_NULL) current_participant_team = fields.ForeignKey(ParticipantTeam, null=True, blank=True, on_delete=fields.SET_NULL) elo = fields.IntegerField(db_index=True, default=1000)
class SsbuSettings(models.Settings): challonge_username = fields.CharField(max_length=64) challonge_api_key = fields.CharField(max_length=128)
class Tournament(models.Model): id = fields.BigIntegerField(primary_key=True) # Challonge ID key = fields.CharField(max_length=128, unique=True) name = fields.CharField(max_length=128) series = fields.ForeignKey(TournamentSeries, null=True, blank=True, db_index=True, on_delete=fields.SET_NULL) ranked = fields.BooleanField() signup_message = fields.MessageField(unique=True, db_index=True, null=True, on_delete=fields.SET_NULL) checkin_message = fields.MessageField(unique=True, db_index=True, null=True, blank=True, on_delete=fields.SET_NULL) # signup_emoji = fields.EmojiField(default=get_default_emoji, on_delete=fields.SET_DEFAULT) # checkin_emoji = fields.EmojiField(default=get_default_emoji, on_delete=fields.SET_DEFAULT) guild = fields.GuildField(db_index=True, on_delete=fields.CASCADE) announcements_channel = fields.TextChannelField(null=True, on_delete=fields.SET_NULL) talk_channel = fields.TextChannelField(null=True, blank=True, on_delete=fields.SET_NULL) participant_role = fields.RoleField(null=True, unique=True, on_delete=fields.SET_NULL) organizer_role = fields.RoleField(null=True, on_delete=fields.SET_NULL) streamer_role = fields.RoleField(null=True, blank=True, on_delete=fields.SET_NULL) doubles = fields.BooleanField(db_index=True) format = FormatField(default=Formats.double_elimination) allow_matches_in_dms = fields.BooleanField() # don't hard delete rulesets that have already been used # instead, the ruleset should be swapped out with the updated version # and only for upcoming and ongoing tournaments ruleset = fields.ForeignKey(Ruleset, on_delete=fields.PROTECT) start_time = fields.DateTimeField(db_index=True) delay_start = fields.SmallIntegerField(null=True, blank=True) # minutes start_task = fields.ForeignKey(ScheduledTask, null=True, on_delete=fields.SET_NULL) start_checkin_task = fields.ForeignKey(ScheduledTask, null=True, on_delete=fields.SET_NULL) check_reactions_task = fields.ForeignKey(ScheduledTask, null=True, on_delete=fields.SET_NULL) ended = fields.BooleanField(db_index=True, default=False) @property def full_challonge_url(self): return f"https://challonge.com/{self.key}" async def get_challonge_tournament(self): core = self._core extension_name = self._meta.app_label ssbu = core.get_controller(extension_name) return await ssbu.get_challonge_tournament(self.id) @classmethod async def convert(cls, ctx, argument): try: argument = int(argument) # argument is Challonge ID tournament = await cls.async_get(pk=argument) except ValueError: # argument is URL key tournament = await cls.async_get(key=argument) return tournament
class Context(models.DiscordModel): message = fields.MessageField(null=True, blank=True, on_delete=fields.SET_NULL) prefix = fields.CharField(max_length=64, null=True, blank=True) command_name = fields.CharField(max_length=256, null=True, blank=True) args_and_kwargs = JSONField(default={}) _discord_cls = Context @property def args(self): if self.is_fetched: return self._discord_obj.args return self.args_and_kwargs['args'] @property def kwargs(self): if self.is_fetched: return self._discord_obj.kwargs kwargs = self.args_and_kwargs.copy() if 'args' in kwargs: kwargs.pop('args') return kwargs @property def _state(self): if hasattr(self, '_discord_obj'): return self._discord_obj._state else: return self.message._state @classmethod @async_using_db def from_discord_obj(cls, discord_obj): message = async_to_sync( models.Message.from_discord_obj(discord_obj.message)) prefix = discord_obj.prefix command_name = discord_obj.command_name args_and_kwargs = discord_obj.kwargs or {} if discord_obj.args: args_and_kwargs['args'] = discord_obj.args if not args_and_kwargs: args_and_kwargs = None return cls(message=message, prefix=prefix, command_name=command_name, args_and_kwargs=args_and_kwargs) async def fetch(self): if self.message is not None: message = await self.message.fetch() args_and_kwargs = self.args_and_kwargs.copy() self._core: hero.Core command = self._core.get_command(self.command_name) ctx = hero.Context(message=message, bot=self._core, args=args_and_kwargs.pop('args', []), kwargs=args_and_kwargs, prefix=self.prefix, command=command) self._discord_obj = ctx return ctx else: return False
class Emoji(DiscordModel): guild = fields.GuildField(on_delete=fields.CASCADE) name = fields.CharField(max_length=64)
class UserGroup(Model): name = fields.CharField(max_length=64, unique=True)
class TournamentSeries(models.Model): class Meta: unique_together = (('name', 'guild'), ) key_prefix = fields.CharField(primary_key=True, max_length=128) guild = fields.GuildField(db_index=True, on_delete=fields.CASCADE) name = fields.CharField(max_length=128) next_iteration = fields.IntegerField(default=1) ranked = fields.BooleanField() admins = fields.ManyToManyField(Player, null=True, blank=True) participant_role = fields.RoleField( null=True, unique=True, on_delete=fields.SET_NULL ) # cancel tournament creation if not found on Discord organizer_role = fields.RoleField( null=True, on_delete=fields.SET_NULL ) # cancel tournament creation if not found on Discord streamer_role = fields.RoleField( null=True, blank=True, on_delete=fields.SET_NULL) # delete if not found on Discord # signup_emoji = fields.EmojiField(default=get_default_emoji, on_delete=fields.SET_DEFAULT) # checkin_emoji = fields.EmojiField(default=get_default_emoji, on_delete=fields.SET_DEFAULT) announcements_channel = fields.TextChannelField( null=True, on_delete=fields.SET_NULL) # if None, cancel tournament creation talk_channel = fields.TextChannelField(null=True, blank=True, on_delete=fields.SET_NULL) introduction = fields.TextField(max_length=2048) default_participants_limit = fields.IntegerField(default=512) last_start_time = fields.DateTimeField(null=True, blank=True) delay_start = fields.SmallIntegerField(null=True, blank=True) # minutes interval = IntervalField(null=True, blank=True) doubles = fields.BooleanField(db_index=True) format = FormatField(default=Formats.double_elimination) affects_elo = fields.BooleanField(default=True) allow_matches_in_dms = fields.BooleanField() ruleset = fields.ForeignKey(Ruleset, null=True, blank=True, on_delete=fields.SET_NULL) cancelled = fields.BooleanField(default=False) @property def next_start_time(self): self.last_start_time: datetime.datetime if self.last_start_time is None or self.interval is None: return None if self.interval == Intervals.MONTHLY: weekday = self.last_start_time.weekday() day_number = self.last_start_time.day @classmethod async def convert(cls, ctx, argument): try: tournament_series = await cls.async_get(pk=argument) except ObjectDoesNotExist: try: guild = await models.Guild.from_discord_obj(ctx.guild) tournament_series = await cls.async_get(guild=guild, name=argument) except ObjectDoesNotExist: raise BadArgument( f"{argument} does not seem to be a valid tournament series." ) return tournament_series