async def children(self, ctx:utils.Context, user:utils.converters.UserID=None): """ Tells you who a user's children are. """ # Get the user's info user_id = user or ctx.author.id user_name = await localutils.DiscordNameManager.fetch_name_by_id(self.bot, user_id) user_info = localutils.FamilyTreeMember.get(user_id, localutils.get_family_guild_id(ctx)) # Get user's children if len(user_info._children) == 0: output = f"**{localutils.escape_markdown(user_name)}** has no children right now." if user_id == ctx.author.id: output = "You have no children right now." else: children_plural = 'child' if len(user_info._children) == 1 else 'children' output = f"**{localutils.escape_markdown(user_name)}** has {len(user_info._children)} {children_plural}:\n" if user_id == ctx.author.id: ouptut = f"You have {len(user_info._children)} {children_plural}:\n" children = [(await localutils.DiscordNameManager.fetch_name_by_id(self.bot, i), i) for i in user_info._children] output += "\n".join([f"* **{localutils.escape_markdown(i[0])}** (`{i[1]}`)" for i in children]) # Return all output await ctx.send(output, allowed_mentions=discord.AllowedMentions.none())
async def forceemancipate(self, ctx: utils.Context, child: utils.converters.UserID): """ Force emancipates a child. """ # Run checks family_guild_id = localutils.get_family_guild_id(ctx) child_tree = localutils.FamilyTreeMember.get(child, family_guild_id) child_name = await localutils.DiscordNameManager.fetch_name_by_id( self.bot, child) if not child_tree._parent: return await ctx.send( f"**{child_name}** doesn't even have a parent .-.") # Update database async with self.bot.database() as db: await db( """DELETE FROM parents WHERE child_id=$1 AND guild_id=$2""", child, family_guild_id, ) # Update cache try: child_tree.parent._children.remove(child) except ValueError: pass parent = child_tree.parent child_tree._parent = None async with self.bot.redis() as re: await re.publish('TreeMemberUpdate', child_tree.to_json()) await re.publish('TreeMemberUpdate', parent.to_json()) await ctx.send("Consider it done.")
async def parent(self, ctx: utils.Context, user: utils.converters.UserID = None): """ Tells you who someone's parent is. """ # Get the user's info user_id = user or ctx.author.id user_name = await localutils.DiscordNameManager.fetch_name_by_id( self.bot, user_id) user_info = localutils.FamilyTreeMember.get( user_id, localutils.get_family_guild_id(ctx)) # Make sure they have a parent if user_info._parent is None: if user_id == ctx.author.id: return await ctx.send("You have no parent.") return await ctx.send( f"**{localutils.escape_markdown(user_name)}** has no parent.", allowed_mentions=discord.AllowedMentions.none()) parent_name = await localutils.DiscordNameManager.fetch_name_by_id( self.bot, user_info._parent) # Output parent output = f"**{localutils.escape_markdown(user_name)}**'s parent is **{localutils.escape_markdown(parent_name)}** (`{user_info._parent}`)." if user_id == ctx.author.id: output = f"Your parent is **{localutils.escape_markdown(parent_name)}** (`{user_info._parent}`)." return await ctx.send(output, allowed_mentions=discord.AllowedMentions.none())
async def convert(cls, ctx: vbu.Context, value: str): """ A more gentle converter that accepts child names as well as pings and IDs. """ # See if it's a ping or a mention try: return await super().convert(ctx, value) # See if it's a name except Exception as e: user_tree = utils.FamilyTreeMember.get( ctx.author.id, utils.get_family_guild_id(ctx)) for child in user_tree.children: child_name = await utils.DiscordNameManager.fetch_name_by_id( ctx.bot, child.id) child_name_length = len(child_name) if len(value) == child_name_length or len( value) == len(child_name_length) - 4: pass else: raise if child_name.startswith(value): return child.id raise e
async def familysize(self, ctx: vbu.Context, user: vbu.converters.UserID = None): """ Gives you the size of your family tree. """ # Get the user's info user_id = user or ctx.author.id user_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, user_id) user_info = utils.FamilyTreeMember.get(user_id, utils.get_family_guild_id(ctx)) # Get size size = user_info.family_member_count # Output output = ( f"There {'are' if size > 1 else 'is'} {size} {'people' if size > 1 else 'person'} " f"in **{utils.escape_markdown(user_name)}**'s family tree.") if user_id == ctx.author.id: output = f"There {'are' if size > 1 else 'is'} {size} {'people' if size > 1 else 'person'} in your family tree." return await ctx.send(output, allowed_mentions=discord.AllowedMentions.none(), wait=False)
async def forcedivorce(self, ctx: utils.Context, usera: utils.converters.UserID): """ Divorces a user from their spouse. """ # Get user family_guild_id = localutils.get_family_guild_id(ctx) usera_tree = localutils.FamilyTreeMember.get(usera, guild_id=family_guild_id) usera_name = await localutils.DiscordNameManager.fetch_name_by_id( self.bot, usera) if not usera_tree.partner: return await ctx.send( f"**{usera_name}** isn't even married .-.", allowed_mentions=discord.AllowedMentions.none()) # Update database async with self.bot.database() as db: await db( """DELETE FROM marriages WHERE (user_id=$1 OR partner_id=$1) AND guild_id=$2""", usera, family_guild_id, ) # Update cache usera_tree.partner._partner = None userb_tree = usera_tree.partner usera_tree._partner = None async with self.bot.redis() as re: await re.publish('TreeMemberUpdate', usera_tree.to_json()) await re.publish('TreeMemberUpdate', userb_tree.to_json()) await ctx.send("Consider it done.")
async def emancipate(self, ctx: vbu.Context): """ Removes your parent. """ # Get the family tree member objects family_guild_id = utils.get_family_guild_id(ctx) user_tree = utils.FamilyTreeMember.get(ctx.author.id, guild_id=family_guild_id) # Make sure they're the child of the instigator parent_tree = user_tree.parent if not parent_tree: return await ctx.send("You don't have a parent right now :<", wait=False) # See if they're sure try: result = await utils.send_proposal_message( ctx, ctx.author, f"Are you sure you want to leave your parent, {ctx.author.mention}?", timeout_message= f"Timed out making sure you want to emancipate, {ctx.author.mention} :<", cancel_message="Alright, I've cancelled your emancipation!", ) except Exception: result = None if result is None: return # Remove family caching user_tree._parent = None try: parent_tree._children.remove(ctx.author.id) except ValueError: pass # Ping them off over reids async with self.bot.redis() as re: await re.publish('TreeMemberUpdate', user_tree.to_json()) await re.publish('TreeMemberUpdate', parent_tree.to_json()) # Remove their relationship from the database async with self.bot.database() as db: await db( """DELETE FROM parents WHERE parent_id=$1 AND child_id=$2 AND guild_id=$3""", parent_tree.id, ctx.author.id, family_guild_id, ) # And we're done parent_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, parent_tree.id) return await result.ctx.send( f"You no longer have **{utils.escape_markdown(parent_name)}** as a parent :c", wait=False)
async def partner(self, ctx: vbu.Context, user: vbu.converters.UserID = None): """ Tells you who a user is married to. """ # Get the user's info user_id = user or ctx.author.id user_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, user_id) user_info = utils.FamilyTreeMember.get(user_id, utils.get_family_guild_id(ctx)) # Check they have a partner if user_info._partner is None: if user_id == ctx.author.id: return await ctx.send( "You're not currently married.", allowed_mentions=discord.AllowedMentions.none(), wait=False, ) return await ctx.send( f"**{utils.escape_markdown(user_name)}** is not currently married.", allowed_mentions=discord.AllowedMentions.none(), wait=False, ) partner_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, user_info._partner) # Get timestamp async with self.bot.database() as db: if self.bot.config.get('is_server_specific', False): data = await db( "SELECT * FROM marriages WHERE user_id=$1 AND guild_id=$2", user_id, user_info._guild_id) else: data = await db( "SELECT * FROM marriages WHERE user_id=$1 AND guild_id=0", user_id) try: timestamp = data[0]['timestamp'] except Exception: timestamp = None # Output text = f"**{utils.escape_markdown(user_name)}** is currently married to **{utils.escape_markdown(partner_name)}** (`{user_info._partner}`). " if user_id == ctx.author.id: text = f"You're currently married to **{utils.escape_markdown(partner_name)}** (`{user_info._partner}`). " if timestamp: duration = vbu.TimeFormatter(timestamp) text += f"{'You' if user_id == ctx.author.id else 'They'} got married {duration.relative_time}." await ctx.send(text, allowed_mentions=discord.AllowedMentions.none(), wait=False)
async def disownall(self, ctx: vbu.Context): """ Disowns all of your children. """ # Get the family tree member objects family_guild_id = utils.get_family_guild_id(ctx) user_tree = utils.FamilyTreeMember.get(ctx.author.id, guild_id=family_guild_id) child_trees = list(user_tree.children) if not child_trees: return await ctx.send("You don't have any children to disown .-.", wait=False) # See if they're sure try: result = await utils.send_proposal_message( ctx, ctx.author, f"Are you sure you want to disown all your children, {ctx.author.mention}?", timeout_message= f"Timed out making sure you want to disownall, {ctx.author.mention} :<", cancel_message="Alright, I've cancelled your disownall!", ) except Exception: result = None if result is None: return # Disown em for child in child_trees: child._parent = None user_tree._children = [] # Save em async with self.bot.database() as db: await db( """DELETE FROM parents WHERE parent_id=$1 AND guild_id=$2 AND child_id=ANY($3::BIGINT[])""", ctx.author.id, family_guild_id, [child.id for child in child_trees], ) # Redis em async with self.bot.redis() as re: for person in child_trees + [user_tree]: await re.publish('TreeMemberUpdate', person.to_json()) # Output to user await result.ctx.send( "You've sucessfully disowned all of your children :c", wait=False)
async def forceadopt(self, ctx: vbu.Context, parent: vbu.converters.UserID, child: vbu.converters.UserID = None): """ Adds the child to the specified parent. """ # Correct params if child is None: parent_id, child_id = ctx.author.id, parent else: parent_id, child_id = parent, child # Check users family_guild_id = utils.get_family_guild_id(ctx) parent_tree, child_tree = utils.FamilyTreeMember.get_multiple( parent_id, child_id, guild_id=family_guild_id) child_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, child_id) if child_tree.parent: return await ctx.send( f"**{child_name}** already has a parent.", allowed_mentions=discord.AllowedMentions.none(), wait=False) parent_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, parent_tree.id) # Update database async with self.bot.database() as db: try: await db( """INSERT INTO parents (parent_id, child_id, guild_id, timestamp) VALUES ($1, $2, $3, $4)""", parent_id, child_id, family_guild_id, dt.utcnow(), ) except asyncpg.UniqueViolationError: return await ctx.send( "I ran into an error saving your family data.", wait=False) # Update cache parent_tree._children.append(child_id) child_tree._parent = parent_id async with self.bot.redis() as re: await re.publish('TreeMemberUpdate', parent_tree.to_json()) await re.publish('TreeMemberUpdate', child_tree.to_json()) await ctx.send( f"Added **{child_name}** to **{parent_name}**'s children list.", wait=False)
async def divorce(self, ctx: utils.Context): """ Divorces you from your current partner. """ # Get the family tree member objects family_guild_id = localutils.get_family_guild_id(ctx) author_tree = localutils.FamilyTreeMember.get(ctx.author.id, guild_id=family_guild_id) # See if they're married target_tree = author_tree.partner if not target_tree: return await ctx.send("It doesn't look like you're married yet!") # See if they're sure try: result = await localutils.send_proposal_message( ctx, ctx.author, f"Are you sure you want to divorce your partner, {ctx.author.mention}?", timeout_message= f"Timed out making sure you want to divorce, {ctx.author.mention} :<", cancel_message="Alright, I've cancelled your divorce!", ) except Exception: result = None if result is None: return # Remove them from the database async with self.bot.database() as db: await db( """DELETE FROM marriages WHERE (user_id=$1 OR user_id=$2) AND guild_id=$3""", ctx.author.id, target_tree.id, family_guild_id, ) partner_name = await localutils.DiscordNameManager.fetch_name_by_id( self.bot, author_tree._partner) await result.ctx.send( f"You've successfully divorced **{localutils.escape_markdown(partner_name)}** :c", allowed_mentions=discord.AllowedMentions.none(), ) # Ping over redis author_tree._partner = None target_tree._partner = None async with self.bot.redis() as re: await re.publish('TreeMemberUpdate', author_tree.to_json()) await re.publish('TreeMemberUpdate', target_tree.to_json())
async def children(self, ctx: vbu.Context, user: vbu.converters.UserID = None): """ Tells you who a user's children are. """ # Get the user's info user_id = user or ctx.author.id user_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, user_id) user_info = utils.FamilyTreeMember.get(user_id, utils.get_family_guild_id(ctx)) # See if the user has no children if len(user_info._children) == 0: if user_id == ctx.author.id: return await ctx.send( "You have no children right now.", allowed_mentions=discord.AllowedMentions.none(), wait=False, ) return await ctx.send( f"**{utils.escape_markdown(user_name)}** has no children right now.", allowed_mentions=discord.AllowedMentions.none(), wait=False, ) # They do have children! children_plural = 'child' if len( user_info._children) == 1 else 'children' output = f"**{utils.escape_markdown(user_name)}** has {len(user_info._children)} {children_plural}:\n" if user_id == ctx.author.id: output = f"You have {len(user_info._children)} {children_plural}:\n" children = [( await utils.DiscordNameManager.fetch_name_by_id(self.bot, i), i, ) for i in user_info._children] output += "\n".join([ f"\N{BULLET} **{utils.escape_markdown(i[0])}** (`{i[1]}`)" for i in children ]) # Return all output if len(output) > 2_000: return await ctx.send( f"<@{user_id}>'s children list goes over 2,000 characters. Amazing.", wait=False) await ctx.send(output, allowed_mentions=discord.AllowedMentions.none(), wait=False)
async def relationship(self, ctx: vbu.Context, user: vbu.converters.UserID, other: vbu.converters.UserID = None): """ Gets the relationship between the two specified users. """ # Fix up the arguments if other is None: user_id, other_id = ctx.author.id, user else: user_id, other_id = user, other # See if they're the same person if user_id == other_id: if user_id == ctx.author.id: return await ctx.send( "Unsurprisingly, you're pretty closely related to yourself.", wait=False) return await ctx.send( "Unsurprisingly, they're pretty closely related to themselves.", wait=False) # Get their relation user_info, other_info = utils.FamilyTreeMember.get_multiple( user_id, other_id, guild_id=utils.get_family_guild_id(ctx)) async with ctx.typing(): relation = user_info.get_relation(other_info) # Get names user_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, user_id) other_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, other_id) # Output if relation is None: output = f"**{utils.escape_markdown(user_name)}** is not related to **{utils.escape_markdown(other_name)}**." if user_id == ctx.author.id: output = f"You're not related to **{utils.escape_markdown(other_name)}**." else: output = f"**{utils.escape_markdown(other_name)}** is **{utils.escape_markdown(user_name)}**'s {relation}." if user_id == ctx.author.id: output = f"**{utils.escape_markdown(other_name)}** is your {relation}." return await ctx.send(output, allowed_mentions=discord.AllowedMentions.none(), wait=False)
async def copulate(self, ctx: vbu.Context, target: discord.Member): """ Lets you... um... heck someone. """ # Variables we're gonna need for later family_guild_id = utils.get_family_guild_id(ctx) author_tree, target_tree = utils.FamilyTreeMember.get_multiple( ctx.author.id, target.id, guild_id=family_guild_id) # Check they're not a bot if target.id == self.bot.user.id: return await ctx.send("Ew. No. Thanks.", wait=False) if target.id == ctx.author.id: return # See if they're already related async with ctx.typing(): relation = author_tree.get_relation(target_tree) if relation and relation != "partner" and utils.guild_allows_incest( ctx) is False: return await ctx.send( f"Woah woah woah, it looks like you guys are related! {target.mention} is your {relation}!", allowed_mentions=utils.only_mention(ctx.author), wait=False, ) # Set up the proposal if target.id != ctx.author.id: try: result = await utils.send_proposal_message( ctx, target, f"Hey, {target.mention}, {ctx.author.mention} do you wanna... smash? \N{SMIRKING FACE}", allow_bots=True, ) except Exception: result = None if result is None: return # Respond await result.ctx.send( random.choice(utils.random_text.Copulate.VALID).format( author=ctx.author, target=target), wait=False, )
async def familysize(self, ctx:utils.Context, user:utils.converters.UserID=None): """ Gives you the size of your family tree. """ # Get the user's info user_id = user or ctx.author.id user_name = await localutils.DiscordNameManager.fetch_name_by_id(self.bot, user_id) user_info = localutils.FamilyTreeMember.get(user_id, localutils.get_family_guild_id(ctx)) # Get size async with ctx.channel.typing(): size = user_info.family_member_count # Output output = f"There are {size} people in **{localutils.escape_markdown(user_name)}**'s family tree." if user_id == ctx.author.id: output = f"There are {size} people in your family tree." return await ctx.send(output, allowed_mentions=discord.AllowedMentions.none())
async def forcemarry(self, ctx: utils.Context, usera: utils.converters.UserID, userb: utils.converters.UserID = None): """ Marries the two specified users. """ # Correct params if userb is None: usera, userb = ctx.author.id, usera if usera == userb: return await ctx.send("You can't marry yourself.") # Get users family_guild_id = localutils.get_family_guild_id(ctx) usera_tree, userb_tree = localutils.FamilyTreeMember.get_multiple( usera, userb, guild_id=family_guild_id) # See if they have partners if usera_tree._partner is not None: user_name = await localutils.DiscordNameManager.fetch_name_by_id( self.bot, usera) return await ctx.send( f"**{user_name}** already has a partner.", allowed_mentions=discord.AllowedMentions.none()) if userb_tree._partner is not None: user_name = await localutils.DiscordNameManager.fetch_name_by_id( self.bot, userb) return await ctx.send( f"**{user_name}** already has a partner.", allowed_mentions=discord.AllowedMentions.none()) # Update database async with self.bot.database() as db: try: await db.start_transaction() await db( "INSERT INTO marriages (user_id, partner_id, guild_id, timestamp) VALUES ($1, $2, $3, $4), ($2, $1, $3, $4)", usera_tree.id, userb_tree.id, family_guild_id, dt.utcnow(), ) await db.commit_transaction() except asyncpg.UniqueViolationError: return await ctx.send( "I ran into an error saving your family data.") usera_name = await localutils.DiscordNameManager.fetch_name_by_id( self.bot, usera_tree.id) userb_name = await localutils.DiscordNameManager.fetch_name_by_id( self.bot, userb_tree.id) await ctx.send(f"Married **{usera_name}** and **{userb_name}**.", allowed_mentions=discord.AllowedMentions.none()) # Update cache usera_tree._partner = userb userb_tree._partner = usera async with self.bot.redis() as re: await re.publish('TreeMemberUpdate', usera_tree.to_json()) await re.publish('TreeMemberUpdate', userb_tree.to_json())
async def abandon(self, ctx: vbu.Context): """ Completely removes you from the tree. """ # Set up some variables family_guild_id = utils.get_family_guild_id(ctx) user_tree = utils.FamilyTreeMember.get(ctx.author.id, guild_id=family_guild_id) # See if they're sure try: result = await utils.send_proposal_message( ctx, ctx.author, f"Are you sure you want to completely abandon your family, {ctx.author.mention}? This will disown all your kids, emancipate, and divorce you", timeout_message= f"Timed out making sure you want to abandon your family, {ctx.author.mention} :<", cancel_message="Alright, I've cancelled your abandonment!", ) except Exception: result = None if result is None: return # Grab the users from the cache parent_tree = user_tree.parent child_trees = list(user_tree.children) partner_tree = user_tree.partner # Remove children from cache for child in child_trees: child._parent = None user_tree._children = [] # Remove parent from cache if parent_tree: user_tree._parent = None try: parent_tree._children.remove(ctx.author.id) except ValueError: pass # Remove partner from cache if partner_tree: user_tree._partner = None partner_tree._partner = None # Remove from database async with self.bot.database() as db: await db( """DELETE FROM parents WHERE parent_id=$1 AND guild_id=$2 AND child_id=ANY($3::BIGINT[])""", ctx.author.id, family_guild_id, [child.id for child in child_trees], ) if parent_tree: await db( """DELETE FROM parents WHERE parent_id=$1 AND child_id=$2 AND guild_id=$3""", parent_tree.id, ctx.author.id, family_guild_id, ) if partner_tree: await db( """DELETE FROM marriages WHERE (user_id=$1 OR user_id=$2) AND guild_id=$3""", ctx.author.id, partner_tree.id, family_guild_id, ) # Remove from redis async with self.bot.redis() as re: for person in child_trees: await re.publish('TreeMemberUpdate', person.to_json()) if parent_tree: await re.publish('TreeMemberUpdate', parent_tree.to_json()) if partner_tree: await re.publish('TreeMemberUpdate', partner_tree.to_json()) await re.publish('TreeMemberUpdate', user_tree.to_json()) # And we're done await result.ctx.send( f"You've successfully left your family, {ctx.author.mention} :c", allowed_mentions=discord.AllowedMentions.none(), wait=False, )
async def disown(self, ctx: vbu.Context, *, target: utils.ChildIDConverter = None): """ Lets you remove a user from being your child. """ # Get the user family tree member family_guild_id = utils.get_family_guild_id(ctx) user_tree = utils.FamilyTreeMember.get(ctx.author.id, guild_id=family_guild_id) # If they didn't give a child, give them a dropdown if target is None: # Make a list of options child_options = [] for index, child_tree in enumerate(user_tree.children): child_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, child_tree.id) child_options.append( vbu.SelectOption(label=child_name, value=f"DISOWN {child_tree.id}")) if index >= 25: return await ctx.send( ("I couldn't work out which of your children you wanted to disown. " "You can ping or use their ID to disown them."), wait=False, ) # See if they don't have any children if not child_options: return await ctx.send("You don't have any children!", wait=False) # Wait for them to pick one components = vbu.MessageComponents( vbu.ActionRow( vbu.SelectMenu(custom_id="DISOWN_USER", options=child_options), )) m = await ctx.send( "Which of your children would you like to disown?", components=components, wait=True, ) # Make our check def check(payload: vbu.ComponentInteractionPayload): if payload.message.id != m.id: return False if payload.user.id != ctx.author.id: self.bot.loop.create_task( payload.respond("You can't respond to this message!", wait=False, ephemeral=True)) return False return True try: payload = await self.bot.wait_for("component_interaction", check=check, timeout=60) await payload.defer_update() await payload.message.delete() except asyncio.TimeoutError: return await ctx.send( "Timed out asking for which child you want to disown :<", wait=False) # Get the child's ID that they selected target = int(payload.values[0][len("DISOWN "):]) # Get the family tree member objects child_tree = utils.FamilyTreeMember.get(target, guild_id=family_guild_id) child_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, child_tree.id) # Make sure they're actually children if child_tree.id not in user_tree._children: return await ctx.send( f"It doesn't look like **{utils.escape_markdown(child_name)}** is one of your children!", allowed_mentions=discord.AllowedMentions.none(), wait=False, ) # See if they're sure try: result = await utils.send_proposal_message( ctx, ctx.author, f"Are you sure you want to disown **{utils.escape_markdown(child_name)}**, {ctx.author.mention}?", timeout_message= f"Timed out making sure you want to disown, {ctx.author.mention} :<", cancel_message="Alright, I've cancelled your disown!", ) except Exception: result = None if result is None: return # Remove from cache try: user_tree._children.remove(child_tree.id) except ValueError: pass child_tree._parent = None # Remove from redis async with self.bot.redis() as re: await re.publish('TreeMemberUpdate', user_tree.to_json()) await re.publish('TreeMemberUpdate', child_tree.to_json()) # Remove from database async with self.bot.database() as db: await db( """DELETE FROM parents WHERE child_id=$1 AND parent_id=$2 AND guild_id=$3""", child_tree.id, ctx.author.id, family_guild_id, ) # And we're done await result.ctx.send( f"You've successfully disowned **{utils.escape_markdown(child_name)}** :c", allowed_mentions=discord.AllowedMentions.none(), wait=False, )
async def disown(self, ctx: utils.Context, *, target: utils.converters.UserID): """ Lets you remove a user from being your child. """ # Get the family tree member objects family_guild_id = localutils.get_family_guild_id(ctx) user_tree, child_tree = localutils.FamilyTreeMember.get_multiple( ctx.author.id, target, guild_id=family_guild_id) child_name = await localutils.DiscordNameManager.fetch_name_by_id( self.bot, child_tree.id) # Make sure they're actually children if child_tree.id not in user_tree._children: return await ctx.send( f"It doesn't look like **{localutils.escape_markdown(child_name)}** is one of your children!", allowed_mentions=discord.AllowedMentions.none(), ) # See if they're sure try: result = await localutils.send_proposal_message( ctx, ctx.author, f"Are you sure you want to disown your child, {ctx.author.mention}?", timeout_message= f"Timed out making sure you want to disown, {ctx.author.mention} :<", cancel_message="Alright, I've cancelled your disown!", ) except Exception: result = None if result is None: return # Remove from cache try: user_tree._children.remove(child_tree.id) except ValueError: pass child_tree._parent = None # Remove from redis async with self.bot.redis() as re: await re.publish('TreeMemberUpdate', user_tree.to_json()) await re.publish('TreeMemberUpdate', child_tree.to_json()) # Remove from database async with self.bot.database() as db: await db( """DELETE FROM parents WHERE child_id=$1 AND parent_id=$2 AND guild_id=$3""", child_tree.id, ctx.author.id, family_guild_id, ) # And we're done await ctx.send( f"You've successfully disowned **{localutils.escape_markdown(child_name)}** :c", allowed_mentions=discord.AllowedMentions.none(), )
async def treemaker(self, ctx: vbu.Context, user_id: int, stupid_tree: bool = False): """ Handles the generation and sending of the tree to the user. """ # Get their family tree user_info = utils.FamilyTreeMember.get(user_id, utils.get_family_guild_id(ctx)) user_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, user_id) # Make sure they have one if user_info.is_empty: if user_id == ctx.author.id: return await ctx.send( "You have no family to put into a tree .-.") return await ctx.send( f"**{utils.escape_markdown(user_name)}** has no family to put into a tree .-.", allowed_mentions=discord.AllowedMentions.none(), ) # Get their customisations async with self.bot.database() as db: ctu = await utils.CustomisedTreeUser.fetch_by_id(db, ctx.author.id) # Get their dot script async with ctx.typing(): if stupid_tree: dot_code = await user_info.to_full_dot_script(self.bot, ctu) else: dot_code = await user_info.to_dot_script(self.bot, ctu) # Write the dot to a file dot_filename = f'{self.bot.config["tree_file_location"]}/{ctx.author.id}.gz' try: with open(dot_filename, 'w', encoding='utf-8') as a: a.write(dot_code) except Exception as e: self.logger.error(f"Could not write to {dot_filename}") raise e # Convert to an image image_filename = f'{self.bot.config["tree_file_location"].rstrip("/")}/{ctx.author.id}.png' # http://www.graphviz.org/doc/info/output.html#d:png perks = await utils.get_marriagebot_perks(ctx.bot, ctx.author.id) # highest quality colour, and antialiasing # not using this because not much point # todo: add extra level for better colour, stroke etc, basically like the one in the readme (in addition to antialiasing) # if False: # format_rendering_option = '-Tpng:cairo' # -T:png does the same thing but this is clearer # normal colour, and antialising if perks.tree_render_quality >= 1: format_rendering_option = '-Tpng:cairo' # normal colour, no antialising else: format_rendering_option = '-Tpng:gd' dot = await asyncio.create_subprocess_exec('dot', format_rendering_option, dot_filename, '-o', image_filename, '-Gcharset=UTF-8') await asyncio.wait_for(dot.wait(), 10.0, loop=self.bot.loop) # Kill subprocess try: dot.kill() except ProcessLookupError: pass # It already died except Exception: raise # Send file try: file = discord.File(image_filename) except FileNotFoundError: return await ctx.send( "I was unable to send your family tree image - please try again later." ) text = "[Click here](https://marriagebot.xyz/) to customise your tree." if not stupid_tree: text += f" Use `{ctx.prefix}bloodtree` for your _entire_ family, including non-blood relatives." tree_message = await ctx.send(text, file=file) await self.bot.add_delete_reaction(tree_message) # Delete the files self.bot.loop.create_task( asyncio.create_subprocess_exec('rm', dot_filename)) self.bot.loop.create_task( asyncio.create_subprocess_exec('rm', image_filename))
async def makeparent(self, ctx: utils.Context, *, target: localutils.converters.UnblockedMember): """ Picks a user that you want to be your parent. """ # Variables we're gonna need for later family_guild_id = localutils.get_family_guild_id(ctx) author_tree, target_tree = localutils.FamilyTreeMember.get_multiple( ctx.author.id, target.id, guild_id=family_guild_id) # Check they're not themselves if target.id == ctx.author.id: return await ctx.send( "That's you. You can't make yourself your parent.") # Check they're not a bot if target.id == self.bot.user.id: return await ctx.send( "I think I could do better actually, but thank you!") # Lock those users re = await self.bot.redis.get_connection() try: lock = await localutils.ProposalLock.lock(re, ctx.author.id, target.id) except localutils.ProposalInProgress: return await ctx.send( "Aren't you popular! One of you is already waiting on a proposal - please try again later." ) # See if the *target* is already married if author_tree.parent: await lock.unlock() return await ctx.send( f"Hey! {ctx.author.mention}, you already have a parent \N{ANGRY FACE}", allowed_mentions=localutils.only_mention(ctx.author), ) # See if we're already married if ctx.author.id in target_tree._children: await lock.unlock() return await ctx.send( f"Hey isn't {target.mention} already your child? \N{FACE WITH ROLLING EYES}", allowed_mentions=localutils.only_mention(ctx.author), ) # See if they're already related async with ctx.channel.typing(): relation = author_tree.get_relation(target_tree) if relation and localutils.guild_allows_incest(ctx) is False: await lock.unlock() return await ctx.send( f"Woah woah woah, it looks like you guys are already related! {target.mention} is your {relation}!", allowed_mentions=localutils.only_mention(ctx.author), ) # Manage children children_amount = await self.get_max_children_for_member( ctx.guild, target) if len(author_tree._children) >= children_amount: return await ctx.send( f"You're currently at the maximum amount of children you can have - see `{ctx.prefix}perks` for more information." ) # Check the size of their trees # TODO I can make this a util because I'm going to use it a couple times max_family_members = localutils.get_max_family_members(ctx) async with ctx.channel.typing(): family_member_count = 0 for i in author_tree.span(add_parent=True, expand_upwards=True): if family_member_count >= max_family_members: break family_member_count += 1 for i in target_tree.span(add_parent=True, expand_upwards=True): if family_member_count >= max_family_members: break family_member_count += 1 if family_member_count >= max_family_members: await lock.unlock() return await ctx.send( f"If you added {target.mention} to your family, you'd have over {max_family_members} in your family. Sorry!", allowed_mentions=localutils.only_mention(ctx.author), ) # Set up the proposal try: result = await localutils.send_proposal_message( ctx, target, f"Hey, {target.mention}, {ctx.author.mention} wants to be your child! What do you think?", allow_bots=True, ) except Exception: result = None if result is None: return await lock.unlock() # Database it up async with self.bot.database() as db: try: await db( """INSERT INTO parents (parent_id, child_id, guild_id, timestamp) VALUES ($1, $2, $3, $4)""", target.id, ctx.author.id, family_guild_id, dt.utcnow(), ) except asyncpg.UniqueViolationError: await lock.unlock() return await ctx.send( "I ran into an error saving your family data - please try again later." ) await ctx.send( f"I'm happy to introduce {ctx.author.mention} as your child, {target.mention}!", ignore_error=True) # And we're done target_tree._children.append(author_tree.id) author_tree._parent = target.id await re.publish('TreeMemberUpdate', author_tree.to_json()) await re.publish('TreeMemberUpdate', target_tree.to_json()) await re.disconnect() await lock.unlock()
async def propose(self, ctx:utils.Context, *, target:localutils.converters.UnblockedMember): """ Lets you propose to another Discord user. """ # Get the family tree member objects family_guild_id = localutils.get_family_guild_id(ctx) author_tree, target_tree = localutils.FamilyTreeMember.get_multiple(ctx.author.id, target.id, guild_id=family_guild_id) # Check they're not themselves if target.id == ctx.author.id: return await ctx.send("That's you. You can't marry yourself.") # Check they're not a bot if target.bot: if target.id == self.bot.user.id: return await ctx.send("I think I could do better actually, but thank you!") return await ctx.send("That is a robot. Robots cannot consent to marriage.") # Lock those users re = await self.bot.redis.get_connection() try: lock = await localutils.ProposalLock.lock(re, ctx.author.id, target.id) except localutils.ProposalInProgress: return await ctx.send("Aren't you popular! One of you is already waiting on a proposal - please try again later.") # See if we're already married if author_tree._partner: await lock.unlock() return await ctx.send( f"Hey, {ctx.author.mention}, you're already married! Try divorcing your partner first \N{FACE WITH ROLLING EYES}", allowed_mentions=localutils.only_mention(ctx.author), ) # See if the *target* is already married if target_tree._partner: await lock.unlock() return await ctx.send( f"Sorry, {ctx.author.mention}, it looks like {target.mention} is already married \N{PENSIVE FACE}", allowed_mentions=localutils.only_mention(ctx.author), ) # See if they're already related async with ctx.channel.typing(): relation = author_tree.get_relation(target_tree) if relation and localutils.guild_allows_incest(ctx) is False: await lock.unlock() return await ctx.send( f"Woah woah woah, it looks like you guys are already related! {target.mention} is your {relation}!", allowed_mentions=localutils.only_mention(ctx.author), ) # Check the size of their trees # TODO I can make this a util because I'm going to use it a couple times max_family_members = localutils.get_max_family_members(ctx) async with ctx.channel.typing(): family_member_count = 0 for i in author_tree.span(add_parent=True, expand_upwards=True): if family_member_count >= max_family_members: break family_member_count += 1 for i in target_tree.span(add_parent=True, expand_upwards=True): if family_member_count >= max_family_members: break family_member_count += 1 if family_member_count >= max_family_members: await lock.unlock() return await ctx.send( f"If you added {target.mention} to your family, you'd have over {max_family_members} in your family. Sorry!", allowed_mentions=localutils.only_mention(ctx.author), ) # Set up the proposal try: result = await localutils.send_proposal_message( ctx, target, f"Hey, {target.mention}, it would make {ctx.author.mention} really happy if you would marry them. What do you say?", ) except Exception: result = None if result is None: return await lock.unlock() # They said yes! async with self.bot.database() as db: try: await db.start_transaction() await db( "INSERT INTO marriages (user_id, partner_id, guild_id, timestamp) VALUES ($1, $2, $3, $4), ($2, $1, $3, $4)", ctx.author.id, target.id, family_guild_id, dt.utcnow(), ) await db.commit_transaction() except asyncpg.UniqueViolationError: await lock.unlock() return await ctx.send("I ran into an error saving your family data.") await ctx.send(f"I'm happy to introduce {target.mention} into the family of {ctx.author.mention}!") # Ping over redis author_tree._partner = target.id target_tree._partner = ctx.author.id await re.publish('TreeMemberUpdate', author_tree.to_json()) await re.publish('TreeMemberUpdate', target_tree.to_json()) await re.disconnect() await lock.unlock()
async def treemaker(self, ctx: utils.Context, user_id: int, stupid_tree: bool = False): """ Handles the generation and sending of the tree to the user. """ # Get their family tree user_info = localutils.FamilyTreeMember.get( user_id, localutils.get_family_guild_id(ctx)) user_name = await localutils.DiscordNameManager.fetch_name_by_id( self.bot, user_id) # Make sure they have one if user_info.is_empty: if user_id == ctx.author.id: return await ctx.send( f"You have no family to put into a tree .-.") return await ctx.send( f"**{localutils.escape_markdown(user_name)}** has no family to put into a tree .-.", allowed_mentions=discord.AllowedMentions.none(), ) # Get their customisations async with self.bot.database() as db: ctu = await localutils.CustomisedTreeUser.fetch_by_id( db, ctx.author.id) # Get their dot script async with ctx.channel.typing(): if stupid_tree: dot_code = await user_info.to_full_dot_script(self.bot, ctu) else: dot_code = await user_info.to_dot_script(self.bot, ctu) # Write the dot to a file dot_filename = f'{self.bot.config["tree_file_location"]}/{ctx.author.id}.gz' try: with open(dot_filename, 'w', encoding='utf-8') as a: a.write(dot_code) except Exception as e: self.logger.error(f"Could not write to {dot_filename}") raise e # Convert to an image image_filename = f'{self.bot.config["tree_file_location"].rstrip("/")}/{ctx.author.id}.png' dot = await asyncio.create_subprocess_exec('dot', '-Tpng:gd', dot_filename, '-o', image_filename, '-Gcharset=UTF-8') await asyncio.wait_for(dot.wait(), 10.0, loop=self.bot.loop) # Kill subprocess try: dot.kill() except ProcessLookupError: pass # It already died except Exception: raise # Send file try: file = discord.File(image_filename) except FileNotFoundError: return await ctx.send( "I was unable to send your family tree image - please try again later." ) text = f"[Click here](https://marriagebot.xyz/) to customise your tree." if not stupid_tree: text += f" Use `{ctx.prefix}bloodtree` for your _entire_ family, including non-blood relatives." await ctx.send(text, file=file) # Delete the files self.bot.loop.create_task( asyncio.create_subprocess_exec('rm', dot_filename)) self.bot.loop.create_task( asyncio.create_subprocess_exec('rm', image_filename))
async def siblings(self, ctx: vbu.Context, user: vbu.converters.UserID = None): """ Tells you who a user's siblings are. """ # Get the user's info user_id = user or ctx.author.id user_name = await utils.DiscordNameManager.fetch_name_by_id( self.bot, user_id) user_info = utils.FamilyTreeMember.get(user_id, utils.get_family_guild_id(ctx)) # Make sure they have a parent parent_id = user_info._parent if not parent_id: if user_id == ctx.author.id: return await ctx.send( "You have no siblings.", allowed_mentions=discord.AllowedMentions.none(), wait=False, ) return await ctx.send( f"**{utils.escape_markdown(user_name)}** has no siblings.", allowed_mentions=discord.AllowedMentions.none(), wait=False, ) # Get parent's children parent_info = user_info.parent sibling_list = parent_info._children # Remove the user from the sibling list sibling_list = [ sibling for sibling in sibling_list if sibling != user_id ] # If the user has no siblings if not sibling_list: if user_id == ctx.author.id: return await ctx.send( "You have no siblings right now.", allowed_mentions=discord.AllowedMentions.none(), wait=False, ) return await ctx.send( f"**{utils.escape_markdown(user_name)}** has no siblings right now.", allowed_mentions=discord.AllowedMentions.none(), wait=False, ) # They do have siblings! sibling_plural = 'sibling' if len(sibling_list) == 1 else 'siblings' # Count the siblings output = f"**{utils.escape_markdown(user_name)}** has {len(sibling_list)} {sibling_plural}:\n" if user_id == ctx.author.id: output = f"You have {len(sibling_list)} {sibling_plural}:\n" # Get the name of the siblings sibling_list = [( await utils.DiscordNameManager.fetch_name_by_id(self.bot, sibling), sibling, ) for sibling in sibling_list] output += "\n".join([ f"\N{BULLET} **{utils.escape_markdown(username)}** (`{uid}`)" for username, uid in sibling_list ]) # Return all output if len(output) > 2_000: return await ctx.send( f"**{utils.escape_markdown(user_name)}**'s sibling list goes over 2,000 characters. Amazing.", wait=False) await ctx.send(output, allowed_mentions=discord.AllowedMentions.none(), wait=False)