diff --git a/Bot/Cogs/dev_tools.py b/Bot/Cogs/dev_tools.py index 2205d193..6a1b071d 100644 --- a/Bot/Cogs/dev_tools.py +++ b/Bot/Cogs/dev_tools.py @@ -83,7 +83,7 @@ async def dispatch_event(self, ctx: commands.Context, event: str) -> None: Args: ctx (commands.Context): _description_ """ - self.bot.dispatch(event, ctx.guild) + self.bot.dispatch(event, ctx.guild, ctx.author) await ctx.send("Dispatched event") @commands.check_any(commands.is_owner(), is_nat()) diff --git a/Bot/Cogs/dictionary.py b/Bot/Cogs/dictionary.py index 62db1d80..d976158f 100644 --- a/Bot/Cogs/dictionary.py +++ b/Bot/Cogs/dictionary.py @@ -1,58 +1,58 @@ -import orjson -from discord import PartialEmoji, app_commands -from discord.ext import commands -from kumikocore import KumikoCore -from Libs.ui.dictionary import DictPages, JapaneseDictPages -from typing_extensions import Annotated -from yarl import URL - - -class Dictionary(commands.Cog): - """Commands to search definitions of words""" - - def __init__(self, bot: KumikoCore) -> None: - self.bot = bot - self.session = self.bot.session - - @property - def display_emoji(self) -> PartialEmoji: - return PartialEmoji(name="\U0001f4d6") - - @commands.hybrid_group(name="define", fallback="english") - @app_commands.describe(query="The word to define") - async def define( - self, ctx: commands.Context, *, query: Annotated[str, commands.clean_content] - ) -> None: - """Define a word from the English dictionary""" - url = URL("https://api.dictionaryapi.dev/api/v2/entries/en") / query - async with self.session.get(url) as r: - data = await r.json(loads=orjson.loads) - if len(data) == 0: - await ctx.send("No results found.") - return - pages = DictPages(data, ctx=ctx) - await pages.start() - - @define.command(name="japanese", aliases=["ja", "jp"]) - @app_commands.describe( - query="The word to define. This can be both in English or Japanese (romaji works)" - ) - async def japanese( - self, ctx: commands.Context, *, query: Annotated[str, commands.clean_content] - ) -> None: - """Get the definition of a word from the Japanese dictionary""" - params = {"keyword": query} - - async with self.session.get( - "https://jisho.org/api/v1/search/words", params=params - ) as r: - data = await r.json(loads=orjson.loads) - if len(data["data"]) == 0: - await ctx.send("No results found.") - return - pages = JapaneseDictPages(data["data"], ctx=ctx) - await pages.start() - - -async def setup(bot: KumikoCore) -> None: - await bot.add_cog(Dictionary(bot)) +import orjson +from discord import PartialEmoji, app_commands +from discord.ext import commands +from kumikocore import KumikoCore +from Libs.ui.dictionary import DictPages, JapaneseDictPages +from typing_extensions import Annotated +from yarl import URL + + +class Dictionary(commands.Cog): + """Commands to search definitions of words""" + + def __init__(self, bot: KumikoCore) -> None: + self.bot = bot + self.session = self.bot.session + + @property + def display_emoji(self) -> PartialEmoji: + return PartialEmoji(name="\U0001f4d6") + + @commands.hybrid_group(name="define", fallback="english") + @app_commands.describe(query="The word to define") + async def define( + self, ctx: commands.Context, *, query: Annotated[str, commands.clean_content] + ) -> None: + """Define a word from the English dictionary""" + url = URL("https://api.dictionaryapi.dev/api/v2/entries/en") / query + async with self.session.get(url) as r: + data = await r.json(loads=orjson.loads) + if "message" in data: + await ctx.send("No results found") + return + pages = DictPages(data, ctx=ctx) + await pages.start() + + @define.command(name="japanese", aliases=["ja", "jp"]) + @app_commands.describe( + query="The word to define. This can be both in English or Japanese (romaji works)" + ) + async def japanese( + self, ctx: commands.Context, *, query: Annotated[str, commands.clean_content] + ) -> None: + """Get the definition of a word from the Japanese dictionary""" + params = {"keyword": query} + + async with self.session.get( + "https://jisho.org/api/v1/search/words", params=params + ) as r: + data = await r.json(loads=orjson.loads) + if len(data["data"]) == 0: + await ctx.send("No results found.") + return + pages = JapaneseDictPages(data["data"], ctx=ctx) + await pages.start() + + +async def setup(bot: KumikoCore) -> None: + await bot.add_cog(Dictionary(bot)) diff --git a/Bot/Cogs/jobs.py b/Bot/Cogs/jobs.py index 41292367..c4303730 100644 --- a/Bot/Cogs/jobs.py +++ b/Bot/Cogs/jobs.py @@ -1,501 +1,504 @@ -import asyncio -from typing import Dict - -import discord -from discord import app_commands -from discord.ext import commands -from kumikocore import KumikoCore -from Libs.cog_utils.economy import is_economy_enabled -from Libs.cog_utils.jobs import ( - JobListFlags, - JobOutputFlags, - create_job, - create_job_link, - create_job_output_item, - format_job_options, - get_job, - submit_job_app, - update_job, -) -from Libs.ui.jobs import ( - CreateJob, - CreateJobOutputItemModal, - DeleteJobViaIDView, - DeleteJobView, - JobPages, - PurgeJobsView, - UpdateJobModal, -) -from Libs.utils import ConfirmEmbed, Embed, JobName, MessageConstants -from Libs.utils.pages import EmbedListSource, KumikoPages -from typing_extensions import Annotated - - -class Jobs(commands.Cog): - """Module for handling jobs for Kumiko's economy module""" - - def __init__(self, bot: KumikoCore) -> None: - self.bot = bot - self.pool = self.bot.pool - self._reserved_jobs_being_made: Dict[int, set[str]] = {} - - def is_job_being_made(self, guild_id: int, name: str) -> bool: - try: - being_made = self._reserved_jobs_being_made[guild_id] - except KeyError: - return False - else: - return name.lower() in being_made - - def add_in_progress_job(self, guild_id: int, name: str) -> None: - tags = self._reserved_jobs_being_made.setdefault(guild_id, set()) - tags.add(name.lower()) - - def remove_in_progress_job(self, guild_id: int, name: str) -> None: - try: - being_made = self._reserved_jobs_being_made[guild_id] - except KeyError: - return - - being_made.discard(name.lower()) - if len(being_made) == 0: - del self._reserved_jobs_being_made[guild_id] - - @property - def display_emoji(self) -> discord.PartialEmoji: - return discord.PartialEmoji(name="\U0001f4bc") - - @is_economy_enabled() - @commands.hybrid_group(name="jobs", fallback="list") - async def jobs(self, ctx: commands.Context, flags: JobListFlags) -> None: - """Lists all available jobs in your server""" - sql = """ - SELECT job.id, job.name, job.description, job.required_rank, job.pay_amount - FROM job_lookup - INNER JOIN job ON job.id = job_lookup.job_id - WHERE job_lookup.guild_id = $1 AND job_lookup.listed = $2; - """ - results = await self.pool.fetch(sql, ctx.guild.id, True) # type: ignore - - if len(results) == 0: - await ctx.send( - "There are no listed jobs in this server! Create one to get started!" - ) - return - if flags.compact is True: - pages = JobPages(entries=results, ctx=ctx, per_page=10) - await pages.start() - else: - data_list = [ - { - "title": row["name"], - "description": row["description"], - "fields": [ - {"name": "ID", "value": row["id"], "inline": True}, - { - "name": "Required Rank", - "value": row["required_rank"], - "inline": True, - }, - { - "name": "Pay Amount", - "value": row["pay_amount"], - "inline": True, - }, - ], - } - for row in results - ] - pages = KumikoPages(EmbedListSource(data_list, per_page=1), ctx=ctx) - await pages.start() - - @is_economy_enabled() - @jobs.command(name="create") - @app_commands.describe( - required_rank="The required rank or higher to obtain the job", - pay="The base pay required for the job", - ) - async def create( - self, ctx: commands.Context, required_rank: int = 0, pay: int = 15 - ) -> None: - """Create a job for your server""" - if ctx.interaction is not None: - create_job_modal = CreateJob(self.pool, required_rank, pay) - await ctx.interaction.response.send_modal(create_job_modal) - return - - await ctx.send("What would you like the job's name to be?") - - converter = JobName() - original = ctx.message - - def check(msg): - return msg.author == ctx.author and ctx.channel == msg.channel - - try: - name = await self.bot.wait_for("message", timeout=30.0, check=check) - except asyncio.TimeoutError: - await ctx.send("You took long. Goodbye.") - return - - try: - ctx.message = name - name = await converter.convert(ctx, name.content) - except commands.BadArgument as e: - await ctx.send(f'{e}. Redo the command "{ctx.prefix}jobs make" to retry.') - return - finally: - ctx.message = original - - if self.is_job_being_made(ctx.guild.id, name): # type: ignore - await ctx.send( - "Sorry. This job is currently being made by someone. " - f'Redo the command "{ctx.prefix}jobs make" to retry.' - ) - return - - query = """SELECT 1 FROM job WHERE guild_id=$1 AND LOWER(name)=$2;""" - async with self.pool.acquire() as conn: - row = await conn.fetchrow(query, ctx.guild.id, name.lower()) # type: ignore - if row is not None: - await ctx.send( - "Sorry. A job with that name already exists. " - f'Redo the command "{ctx.prefix}jobs make" to retry.' - ) - return None - - self.add_in_progress_job(ctx.guild.id, name) # type: ignore - await ctx.send( - f"Neat. So the name is {name}. What about the job's description? " - f"**You can type `abort` to abort the pin make process.**" - ) - - try: - msg = await self.bot.wait_for("message", check=check, timeout=350.0) - except asyncio.TimeoutError: - self.remove_in_progress_job(ctx.guild.id, name) # type: ignore - await ctx.send(MessageConstants.TIMEOUT.value) - return - - if msg.content == "abort": - self.remove_in_progress_job(ctx.guild.id, name) # type: ignore - await ctx.send("Aborting.") - return - elif msg.content: - clean_content = await commands.clean_content().convert(ctx, msg.content) - else: - # fast path I guess? - clean_content = msg.content - - if msg.attachments: - clean_content = f"{clean_content}\n{msg.attachments[0].url}" - - if len(clean_content) > 2000: - await ctx.send("Job description is a maximum of 2000 characters.") - return - - try: - status = await create_job(ctx.author.id, ctx.guild.id, self.pool, name, clean_content, required_rank, pay) # type: ignore - await ctx.send(status) - finally: - self.remove_in_progress_job(ctx.guild.id, name) # type: ignore - - @is_economy_enabled() - @jobs.command(name="update") - @app_commands.describe( - name="The name of the job to update", - required_rank="The mew required rank or higher to obtain the job", - pay="The new base pay required for the job", - ) - async def update( - self, - ctx: commands.Context, - name: Annotated[str, commands.clean_content], - required_rank: int, - pay: int, - ) -> None: - """Updates an owned job with new information""" - if ctx.interaction is not None: - update_job_modal = UpdateJobModal(self.pool, name, required_rank, pay) - await ctx.interaction.response.send_modal(update_job_modal) - return - - def check(msg): - return msg.author == ctx.author and ctx.channel == msg.channel - - await ctx.send( - "What's the description for your job going to be?" - "Note that this new description replaces the old one." - ) - try: - msg = await self.bot.wait_for("message", check=check, timeout=350.0) - except asyncio.TimeoutError: - self.remove_in_progress_job(ctx.guild.id, name) # type: ignore - await ctx.send(MessageConstants.TIMEOUT.value) - return - - if msg.content: - clean_content = await commands.clean_content().convert(ctx, msg.content) - else: - clean_content = msg.content - - if msg.attachments: - clean_content = f"{clean_content}\n{msg.attachments[0].url}" - - if len(clean_content) > 2000: - await ctx.send("Job description is a maximum of 2000 characters.") - return - - status = await update_job(ctx.author.id, ctx.guild.id, self.pool, name, clean_content, required_rank, pay) # type: ignore - if status[-1] == 0: - await ctx.send(MessageConstants.NO_JOB.value) - return - await ctx.send( - f"Successfully updated the job `{name}` (RR: {required_rank}, Pay: {pay})" - ) - return - - @is_economy_enabled() - @jobs.command(name="delete") - @app_commands.describe(name="The name of the job to delete") - async def delete( - self, ctx: commands.Context, name: Annotated[str, commands.clean_content] - ) -> None: - """Deletes a job by name. You can only delete your own jobs.""" - view = DeleteJobView(self.pool, name) - embed = ConfirmEmbed() - embed.description = f"Are you sure you want to delete the job `{name}`?" - await ctx.send(embed=embed, view=view) - - @is_economy_enabled() - @jobs.command(name="delete-id") - @app_commands.describe(id="The ID of the job to delete") - async def delete_via_id(self, ctx: commands.Context, id: int) -> None: - """Deletes the job via the job ID""" - view = DeleteJobViaIDView(self.pool, id) - embed = ConfirmEmbed() - embed.description = f"Are you sure you want to delete the job? (ID: `{id}`)?" - await ctx.send(embed=embed, view=view) - - @is_economy_enabled() - @jobs.command(name="purge") - async def purge(self, ctx: commands.Context) -> None: - """Purges all jobs that you own""" - view = PurgeJobsView(self.pool) - embed = ConfirmEmbed() - embed.description = "Are you sure you want to delete all jobs that you own?" - await ctx.send(embed=embed, view=view) - - @is_economy_enabled() - @jobs.command(name="file") - @app_commands.describe(name="The name of the job to file") - async def file( - self, ctx: commands.Context, *, name: Annotated[str, commands.clean_content] - ) -> None: - """Files (publicly lists) a job for general availability. This must be one that you own""" - query = """ - UPDATE job_lookup - SET listed = $4 - WHERE guild_id=$1 AND creator_id=$2 AND LOWER(name)=$3; - """ - status = await self.pool.execute(query, ctx.guild.id, ctx.author.id, name.lower(), True) # type: ignore - if status[-1] == 0: - await ctx.send(MessageConstants.NO_JOB.value) - else: - await ctx.send(f"Successfully filed job `{name}` for general availability.") - - @is_economy_enabled() - @jobs.command(name="unfile") - @app_commands.describe(name="The name of the job to un-file") - async def unfile( - self, ctx: commands.Context, *, name: Annotated[str, commands.clean_content] - ) -> None: - """Un-files a job for general availability. This must be one that you own""" - query = """ - UPDATE job_lookup - SET listed = $4 - WHERE guild_id=$1 AND creator_id=$2 AND LOWER(name)=$3; - """ - status = await self.pool.execute(query, ctx.guild.id, ctx.author.id, name.lower(), False) # type: ignore - if status[-1] == 0: - await ctx.send(MessageConstants.NO_JOB.value) - else: - await ctx.send( - f"Successfully un-filed job `{name}` for general availability." - ) - - # Probably should make a custom converter for this - @is_economy_enabled() - @jobs.command(name="apply") - @app_commands.describe(name="The name of the job to apply") - async def apply( - self, ctx: commands.Context, *, name: Annotated[str, commands.clean_content] - ) -> None: - """Apply for a job""" - query = """ - SELECT COUNT(*) FROM job WHERE guild_id = $1 AND worker_id = $2; - """ - async with self.pool.acquire() as conn: - job_count = await conn.fetchval(query, ctx.guild.id, ctx.author.id) # type: ignore - rows = await conn.fetchrow("SELECT creator_id, worker_id FROM job WHERE guild_id = $1 AND name = $2;", ctx.guild.id, name.lower()) # type: ignore - # customizable? - if job_count > 3: - await ctx.send("You can't have more than 3 jobs at a time!") - return - - if dict(rows)["creator_id"] == ctx.author.id: - await ctx.send("You can't apply for your own job!") - return - - if dict(rows)["worker_id"] is not None: - await ctx.send("This job is already taken!") - return - - status = await submit_job_app(ctx.author.id, ctx.guild.id, name.lower(), False, conn) # type: ignore - await ctx.send(status) - return - - @is_economy_enabled() - @jobs.command(name="quit") - @app_commands.describe(name="The name of the job to quit") - async def quit( - self, ctx: commands.Context, *, name: Annotated[str, commands.clean_content] - ) -> None: - """Quit a current job that you have""" - async with self.pool.acquire() as conn: - rows = await conn.fetchrow("SELECT creator_id, worker_id FROM job WHERE guild_id = $1 AND name = $2;", ctx.guild.id, name.lower()) # type: ignore - if dict(rows)["creator_id"] == ctx.author.id: - await ctx.send("You can't apply for your own job!") - return - - if dict(rows)["worker_id"] is None: - await ctx.send("This job is available! Apply for it first!") - return - else: - status = await submit_job_app(None, ctx.guild.id, name.lower(), True, conn) # type: ignore - await ctx.send(status) - return - - @is_economy_enabled() - @jobs.command(name="info") - @app_commands.describe(name="The name of the job to get") - async def info( - self, ctx: commands.Context, *, name: Annotated[str, commands.clean_content] - ) -> None: - """Get info about a job""" - job_results = await get_job(ctx.guild.id, name.lower(), self.pool) # type: ignore - if isinstance(job_results, list): - await ctx.send(format_job_options(job_results) or "No jobs were found") - return - embed = Embed(title=job_results["name"], description=job_results["description"]) # type: ignore - embed.add_field(name="Required Rank", value=job_results["required_rank"]) # type: ignore - embed.add_field(name="Pay Amount", value=job_results["pay_amount"]) # type: ignore - embed.set_footer(text=f"ID: {job_results['id']}") # type: ignore - await ctx.send(embed=embed) - - @is_economy_enabled() - @jobs.command(name="search") - @app_commands.describe(query="The name of the job to look for") - async def search( - self, ctx: commands.Context, *, query: Annotated[str, commands.clean_content] - ) -> None: - """Search for jobs that are available. These must be listed in order to show up""" - if len(query) < 3: - await ctx.send("The query must be at least 3 characters") - return - sql = """SELECT job.id, job.name, job.description, job.required_rank, job.pay_amount - FROM job_lookup - WHERE guild_id=$1 AND name % $2 AND listed = $3 - ORDER BY similarity(name, $2) DESC - LIMIT 100; - """ - rows = await self.pool.fetch(sql, ctx.guild.id, query, True) # type: ignore - if rows: - pages = JobPages(entries=rows, ctx=ctx, per_page=10) - await pages.start() - else: - await ctx.send("No jobs were found") - return - - @is_economy_enabled() - @jobs.command(name="output", usage=" price: int amount_per_hour: int") - @app_commands.describe(name="The name of the item that the job outputs") - async def associate_item( - self, - ctx: commands.Context, - name: Annotated[str, commands.clean_content], - *, - flags: JobOutputFlags, - ) -> None: - """Associate an item with the job's output. A job can only produce one item.""" - if ctx.interaction is not None: - output_modal = CreateJobOutputItemModal( - self.pool, name, flags.price, flags.amount_per_hour - ) - await ctx.interaction.response.send_modal(output_modal) - return - - def check(msg): - return msg.author == ctx.author and ctx.channel == msg.channel - - await ctx.send("What's the description for your item going to be?") - try: - msg = await self.bot.wait_for("message", check=check, timeout=350.0) - except asyncio.TimeoutError: - self.remove_in_progress_job(ctx.guild.id, name) # type: ignore - await ctx.send(MessageConstants.TIMEOUT.value) - return - - if msg.content: - clean_content = await commands.clean_content().convert(ctx, msg.content) - else: - clean_content = msg.content - - if msg.attachments: - clean_content = f"{clean_content}\n{msg.attachments[0].url}" - - if len(clean_content) > 2000: - await ctx.send("Item description is a maximum of 2000 characters.") - return - - query = """ - SELECT eco_item_lookup.item_id, job_lookup.job_id - FROM eco_item_lookup - INNER JOIN job_lookup ON eco_item_lookup.producer_id = job_lookup.creator_id - WHERE eco_item_lookup.guild_id=$1 AND LOWER(eco_item_lookup.name)=$2 AND eco_item_lookup.producer_id=$3; - """ - status = await create_job_output_item( - name=name, - description=clean_content, - price=flags.price, - amount=flags.amount_per_hour, - guild_id=ctx.guild.id, # type: ignore - worker_id=ctx.author.id, - pool=self.pool, - ) - async with self.pool.acquire() as conn: - if status[-1] != "0": - rows = await conn.fetchrow(query, ctx.guild.id, name, ctx.author.id) # type: ignore - if rows is None: - # this is bugged for some odd reason - await ctx.send("You aren't the producer of the item!") - return - record = dict(rows) - job_link_status = await create_job_link( - worker_id=ctx.author.id, - item_id=record["item_id"], - job_id=record["job_id"], - conn=conn, - ) - if job_link_status[-1] != "0": - await ctx.send( - f"Successfully created the output item `{name}` (Price: {flags.price}, Amount Per Hour: {flags.amount_per_hour})" - ) - return - else: - await ctx.send("There was an error making it. Please try again") - return - - -async def setup(bot: KumikoCore) -> None: - await bot.add_cog(Jobs(bot)) +import asyncio +from typing import Dict + +import discord +from discord import app_commands +from discord.ext import commands +from kumikocore import KumikoCore +from Libs.cog_utils.economy import is_economy_enabled +from Libs.cog_utils.jobs import ( + JobListFlags, + JobOutputFlags, + create_job, + create_job_output_item, + format_job_options, + get_job, + submit_job_app, + update_job, +) +from Libs.ui.jobs import ( + CreateJob, + CreateJobOutputItemModal, + DeleteJobViaIDView, + DeleteJobView, + JobPages, + PurgeJobsView, + UpdateJobModal, +) +from Libs.utils import ConfirmEmbed, Embed, JobName, MessageConstants +from Libs.utils.pages import EmbedListSource, KumikoPages +from typing_extensions import Annotated + + +class Jobs(commands.Cog): + """Module for handling jobs for Kumiko's economy module""" + + def __init__(self, bot: KumikoCore) -> None: + self.bot = bot + self.pool = self.bot.pool + self._reserved_jobs_being_made: Dict[int, set[str]] = {} + + def is_job_being_made(self, guild_id: int, name: str) -> bool: + try: + being_made = self._reserved_jobs_being_made[guild_id] + except KeyError: + return False + else: + return name.lower() in being_made + + def add_in_progress_job(self, guild_id: int, name: str) -> None: + tags = self._reserved_jobs_being_made.setdefault(guild_id, set()) + tags.add(name.lower()) + + def remove_in_progress_job(self, guild_id: int, name: str) -> None: + try: + being_made = self._reserved_jobs_being_made[guild_id] + except KeyError: + return + + being_made.discard(name.lower()) + if len(being_made) == 0: + del self._reserved_jobs_being_made[guild_id] + + @property + def display_emoji(self) -> discord.PartialEmoji: + return discord.PartialEmoji(name="\U0001f4bc") + + @is_economy_enabled() + @commands.hybrid_group(name="jobs", fallback="list") + async def jobs(self, ctx: commands.Context, flags: JobListFlags) -> None: + """Lists all available jobs in your server""" + sql = """ + SELECT job.id, job.name, job.description, job.required_rank, job.pay_amount + FROM job_lookup + INNER JOIN job ON job.id = job_lookup.job_id + WHERE job_lookup.guild_id = $1 AND job_lookup.listed = $2; + """ + results = await self.pool.fetch(sql, ctx.guild.id, True) # type: ignore + + if len(results) == 0: + await ctx.send( + "There are no listed jobs in this server! Create one to get started!" + ) + return + if flags.compact is True: + pages = JobPages(entries=results, ctx=ctx, per_page=10) + await pages.start() + else: + data_list = [ + { + "title": row["name"], + "description": row["description"], + "fields": [ + {"name": "ID", "value": row["id"], "inline": True}, + { + "name": "Required Rank", + "value": row["required_rank"], + "inline": True, + }, + { + "name": "Pay Amount", + "value": row["pay_amount"], + "inline": True, + }, + ], + } + for row in results + ] + pages = KumikoPages(EmbedListSource(data_list, per_page=1), ctx=ctx) + await pages.start() + + @is_economy_enabled() + @jobs.command(name="create") + @app_commands.describe( + required_rank="The required rank or higher to obtain the job", + pay="The base pay required for the job", + ) + async def create( + self, ctx: commands.Context, required_rank: int = 0, pay: int = 15 + ) -> None: + """Create a job for your server""" + if ctx.interaction is not None: + create_job_modal = CreateJob(self.pool, required_rank, pay) + await ctx.interaction.response.send_modal(create_job_modal) + return + + await ctx.send("What would you like the job's name to be?") + + converter = JobName() + original = ctx.message + + def check(msg): + return msg.author == ctx.author and ctx.channel == msg.channel + + try: + name = await self.bot.wait_for("message", timeout=30.0, check=check) + except asyncio.TimeoutError: + await ctx.send("You took long. Goodbye.") + return + + try: + ctx.message = name + name = await converter.convert(ctx, name.content) + except commands.BadArgument as e: + await ctx.send(f'{e}. Redo the command "{ctx.prefix}jobs make" to retry.') + return + finally: + ctx.message = original + + if self.is_job_being_made(ctx.guild.id, name): # type: ignore + await ctx.send( + "Sorry. This job is currently being made by someone. " + f'Redo the command "{ctx.prefix}jobs make" to retry.' + ) + return + + query = """SELECT 1 FROM job WHERE guild_id=$1 AND LOWER(name)=$2;""" + async with self.pool.acquire() as conn: + row = await conn.fetchrow(query, ctx.guild.id, name.lower()) # type: ignore + if row is not None: + await ctx.send( + "Sorry. A job with that name already exists. " + f'Redo the command "{ctx.prefix}jobs make" to retry.' + ) + return None + + self.add_in_progress_job(ctx.guild.id, name) # type: ignore + await ctx.send( + f"Neat. So the name is {name}. What about the job's description? " + f"**You can type `abort` to abort the pin make process.**" + ) + + try: + msg = await self.bot.wait_for("message", check=check, timeout=350.0) + except asyncio.TimeoutError: + self.remove_in_progress_job(ctx.guild.id, name) # type: ignore + await ctx.send(MessageConstants.TIMEOUT.value) + return + + if msg.content == "abort": + self.remove_in_progress_job(ctx.guild.id, name) # type: ignore + await ctx.send("Aborting.") + return + elif msg.content: + clean_content = await commands.clean_content().convert(ctx, msg.content) + else: + # fast path I guess? + clean_content = msg.content + + if msg.attachments: + clean_content = f"{clean_content}\n{msg.attachments[0].url}" + + if len(clean_content) > 2000: + await ctx.send("Job description is a maximum of 2000 characters.") + return + + try: + status = await create_job(ctx.author.id, ctx.guild.id, self.pool, name, clean_content, required_rank, pay) # type: ignore + await ctx.send(status) + finally: + self.remove_in_progress_job(ctx.guild.id, name) # type: ignore + + @is_economy_enabled() + @jobs.command(name="update") + @app_commands.describe( + name="The name of the job to update", + required_rank="The mew required rank or higher to obtain the job", + pay="The new base pay required for the job", + ) + async def update( + self, + ctx: commands.Context, + name: Annotated[str, commands.clean_content], + required_rank: int, + pay: int, + ) -> None: + """Updates an owned job with new information""" + if ctx.interaction is not None: + update_job_modal = UpdateJobModal(self.pool, name, required_rank, pay) + await ctx.interaction.response.send_modal(update_job_modal) + return + + def check(msg): + return msg.author == ctx.author and ctx.channel == msg.channel + + await ctx.send( + "What's the description for your job going to be?" + "Note that this new description replaces the old one." + ) + try: + msg = await self.bot.wait_for("message", check=check, timeout=350.0) + except asyncio.TimeoutError: + self.remove_in_progress_job(ctx.guild.id, name) # type: ignore + await ctx.send(MessageConstants.TIMEOUT.value) + return + + if msg.content: + clean_content = await commands.clean_content().convert(ctx, msg.content) + else: + clean_content = msg.content + + if msg.attachments: + clean_content = f"{clean_content}\n{msg.attachments[0].url}" + + if len(clean_content) > 2000: + await ctx.send("Job description is a maximum of 2000 characters.") + return + + status = await update_job(ctx.author.id, ctx.guild.id, self.pool, name, clean_content, required_rank, pay) # type: ignore + if status[-1] == 0: + await ctx.send(MessageConstants.NO_JOB.value) + return + await ctx.send( + f"Successfully updated the job `{name}` (RR: {required_rank}, Pay: {pay})" + ) + return + + @is_economy_enabled() + @jobs.command(name="delete") + @app_commands.describe(name="The name of the job to delete") + async def delete( + self, ctx: commands.Context, name: Annotated[str, commands.clean_content] + ) -> None: + """Deletes a job by name. You can only delete your own jobs.""" + view = DeleteJobView(self.pool, name) + embed = ConfirmEmbed() + embed.description = f"Are you sure you want to delete the job `{name}`?" + await ctx.send(embed=embed, view=view) + + @is_economy_enabled() + @jobs.command(name="delete-id") + @app_commands.describe(id="The ID of the job to delete") + async def delete_via_id(self, ctx: commands.Context, id: int) -> None: + """Deletes the job via the job ID""" + view = DeleteJobViaIDView(self.pool, id) + embed = ConfirmEmbed() + embed.description = f"Are you sure you want to delete the job? (ID: `{id}`)?" + await ctx.send(embed=embed, view=view) + + @is_economy_enabled() + @jobs.command(name="purge") + async def purge(self, ctx: commands.Context) -> None: + """Purges all jobs that you own""" + view = PurgeJobsView(self.pool) + embed = ConfirmEmbed() + embed.description = "Are you sure you want to delete all jobs that you own?" + await ctx.send(embed=embed, view=view) + + @is_economy_enabled() + @jobs.command(name="file") + @app_commands.describe(name="The name of the job to file") + async def file( + self, ctx: commands.Context, *, name: Annotated[str, commands.clean_content] + ) -> None: + """Files (publicly lists) a job for general availability. This must be one that you own""" + query = """ + UPDATE job_lookup + SET listed = $4 + WHERE guild_id=$1 AND creator_id=$2 AND LOWER(name)=$3; + """ + status = await self.pool.execute(query, ctx.guild.id, ctx.author.id, name.lower(), True) # type: ignore + if status[-1] == 0: + await ctx.send(MessageConstants.NO_JOB.value) + else: + await ctx.send(f"Successfully filed job `{name}` for general availability.") + + @is_economy_enabled() + @jobs.command(name="unfile") + @app_commands.describe(name="The name of the job to un-file") + async def unfile( + self, ctx: commands.Context, *, name: Annotated[str, commands.clean_content] + ) -> None: + """Un-files a job for general availability. This must be one that you own""" + query = """ + UPDATE job_lookup + SET listed = $4 + WHERE guild_id=$1 AND creator_id=$2 AND LOWER(name)=$3; + """ + status = await self.pool.execute(query, ctx.guild.id, ctx.author.id, name.lower(), False) # type: ignore + if status[-1] == 0: + await ctx.send(MessageConstants.NO_JOB.value) + else: + await ctx.send( + f"Successfully un-filed job `{name}` for general availability." + ) + + # Probably should make a custom converter for this + @is_economy_enabled() + @jobs.command(name="apply") + @app_commands.describe(name="The name of the job to apply") + async def apply( + self, ctx: commands.Context, *, name: Annotated[str, commands.clean_content] + ) -> None: + """Apply for a job""" + query = """ + SELECT COUNT(*) FROM job WHERE guild_id = $1 AND worker_id = $2; + """ + async with self.pool.acquire() as conn: + job_count = await conn.fetchval(query, ctx.guild.id, ctx.author.id) # type: ignore + rows = await conn.fetchrow("SELECT creator_id, worker_id FROM job WHERE guild_id = $1 AND name = $2;", ctx.guild.id, name.lower()) # type: ignore + # customizable? + if job_count > 3: + await ctx.send("You can't have more than 3 jobs at a time!") + return + + if dict(rows)["creator_id"] == ctx.author.id: + await ctx.send("You can't apply for your own job!") + return + + if dict(rows)["worker_id"] is not None: + await ctx.send("This job is already taken!") + return + + status = await submit_job_app(ctx.author.id, ctx.guild.id, name.lower(), False, conn) # type: ignore + await ctx.send(status) + return + + @is_economy_enabled() + @jobs.command(name="quit") + @app_commands.describe(name="The name of the job to quit") + async def quit( + self, ctx: commands.Context, *, name: Annotated[str, commands.clean_content] + ) -> None: + """Quit a current job that you have""" + async with self.pool.acquire() as conn: + rows = await conn.fetchrow("SELECT creator_id, worker_id FROM job WHERE guild_id = $1 AND name = $2;", ctx.guild.id, name.lower()) # type: ignore + if dict(rows)["creator_id"] == ctx.author.id: + await ctx.send("You can't apply for your own job!") + return + + if dict(rows)["worker_id"] is None: + await ctx.send("This job is available! Apply for it first!") + return + else: + status = await submit_job_app(None, ctx.guild.id, name.lower(), True, conn) # type: ignore + await ctx.send(status) + return + + @is_economy_enabled() + @jobs.command(name="info") + @app_commands.describe(name="The name of the job to get") + async def info( + self, ctx: commands.Context, *, name: Annotated[str, commands.clean_content] + ) -> None: + """Get info about a job""" + job_results = await get_job(ctx.guild.id, name.lower(), self.pool) # type: ignore + if isinstance(job_results, list): + await ctx.send(format_job_options(job_results) or "No jobs were found") + return + embed = Embed(title=job_results["name"], description=job_results["description"]) # type: ignore + embed.add_field(name="Required Rank", value=job_results["required_rank"]) # type: ignore + embed.add_field(name="Pay Amount", value=job_results["pay_amount"]) # type: ignore + embed.set_footer(text=f"ID: {job_results['id']}") # type: ignore + await ctx.send(embed=embed) + + @is_economy_enabled() + @jobs.command(name="search") + @app_commands.describe(query="The name of the job to look for") + async def search( + self, ctx: commands.Context, *, query: Annotated[str, commands.clean_content] + ) -> None: + """Search for jobs that are available. These must be listed in order to show up""" + if len(query) < 3: + await ctx.send("The query must be at least 3 characters") + return + sql = """SELECT job.id, job.name, job.description, job.required_rank, job.pay_amount + FROM job_lookup + WHERE guild_id=$1 AND name % $2 AND listed = $3 + ORDER BY similarity(name, $2) DESC + LIMIT 100; + """ + rows = await self.pool.fetch(sql, ctx.guild.id, query, True) # type: ignore + if rows: + pages = JobPages(entries=rows, ctx=ctx, per_page=10) + await pages.start() + else: + await ctx.send("No jobs were found") + return + + @is_economy_enabled() + @jobs.command(name="output", usage=" price: int amount_per_hour: int") + @app_commands.describe(name="The name of the item that the job outputs") + async def associate_item( + self, + ctx: commands.Context, + name: Annotated[str, commands.clean_content], + *, + flags: JobOutputFlags, + ) -> None: + """Associate an item with the job's output. A job can only produce one item.""" + if ctx.interaction is not None: + output_modal = CreateJobOutputItemModal( + self.pool, name, flags.price, flags.amount_per_hour + ) + await ctx.interaction.response.send_modal(output_modal) + return + + def check(msg): + return msg.author == ctx.author and ctx.channel == msg.channel + + await ctx.send("What's the description for your item going to be?") + try: + msg = await self.bot.wait_for("message", check=check, timeout=350.0) + except asyncio.TimeoutError: + self.remove_in_progress_job(ctx.guild.id, name) # type: ignore + await ctx.send(MessageConstants.TIMEOUT.value) + return + + if msg.content: + clean_content = await commands.clean_content().convert(ctx, msg.content) + else: + clean_content = msg.content + + if msg.attachments: + clean_content = f"{clean_content}\n{msg.attachments[0].url}" + + if len(clean_content) > 2000: + await ctx.send("Item description is a maximum of 2000 characters.") + return + + # query = """ + # SELECT eco_item_lookup.item_id, job_lookup.job_id + # FROM eco_item_lookup + # INNER JOIN job_lookup ON eco_item_lookup.producer_id = job_lookup.creator_id + # WHERE eco_item_lookup.guild_id=$1 AND LOWER(eco_item_lookup.name)=$2 AND eco_item_lookup.producer_id=$3; + # """ + status = await create_job_output_item( + name=name, + description=clean_content, + price=flags.price, + amount=flags.amount_per_hour, + guild_id=ctx.guild.id, # type: ignore + worker_id=ctx.author.id, + pool=self.pool, + ) + # async with self.pool.acquire() as conn: + if status[-1] != "0": + await ctx.send( + f"Successfully created the output item `{name}` (Price: {flags.price}, Amount Per Hour: {flags.amount_per_hour})" + ) + return + # rows = await conn.fetchrow(query, ctx.guild.id, name, ctx.author.id) # type: ignore + # if rows is None: + # # this is bugged for some odd reason + # await ctx.send("You aren't the producer of the item!") + # return + # record = dict(rows) + # job_link_status = await create_job_link( + # worker_id=ctx.author.id, + # item_id=record["item_id"], + # job_id=record["job_id"], + # conn=conn, + # ) + # if job_link_status[-1] != "0": + # await ctx.send( + # f"Successfully created the output item `{name}` (Price: {flags.price}, Amount Per Hour: {flags.amount_per_hour})" + # ) + # return + else: + await ctx.send("There was an error making it. Please try again") + return + + +async def setup(bot: KumikoCore) -> None: + await bot.add_cog(Jobs(bot)) diff --git a/Bot/Cogs/pronouns.py b/Bot/Cogs/pronouns.py index 80057a11..920efd8c 100644 --- a/Bot/Cogs/pronouns.py +++ b/Bot/Cogs/pronouns.py @@ -1,278 +1,279 @@ -from typing import Optional - -import discord -import orjson -from discord import app_commands -from discord.ext import commands -from kumikocore import KumikoCore -from Libs.cog_utils.pronouns import parse_pronouns -from Libs.ui.pronouns import ( - PronounsInclusiveEntry, - PronounsInclusivePages, - PronounsNounsEntry, - PronounsNounsPages, - PronounsProfileCircleEntry, - PronounsProfileEntry, - PronounsProfilePages, - PronounsTermsEntry, - PronounsTermsPages, - PronounsValuesEntry, - PronounsWordsEntry, -) -from Libs.utils import Embed -from typing_extensions import Annotated -from yarl import URL - - -class Pronouns(commands.Cog): - """Your to-go module for pronouns! - - This module provides a way to view pronouns for others to see. And this is used seriously as a resource for LGBTQ+ folks. - """ - - def __init__(self, bot: KumikoCore) -> None: - self.bot = bot - self.session = self.bot.session - - @property - def display_emoji(self) -> discord.PartialEmoji: - return discord.PartialEmoji.from_str( - "<:ProgressPrideheart:1053776316438167632>" - ) - - @commands.hybrid_group(name="pronouns", fallback="get") - @app_commands.describe(id="The ID of the user") - async def pronouns(self, ctx: commands.Context, id: str) -> None: - """Obtains the pronouns of a Discord user from PronounDB - - This is not directly from Discord but a third party extension - """ - member = self.bot.get_user(int(id)) - if member is None: - await ctx.send("Could not find member") - return - params = {"platform": "discord", "ids": member.id} - async with self.session.get( - "https://pronoundb.org/api/v2/lookup", params=params - ) as r: - data = await r.json(loads=orjson.loads) - if len(data) == 0: - await ctx.send("No pronouns found for these user(s).") - return - embed = Embed() - embed.set_author( - name=f"{member.global_name}'s pronouns", - icon_url=member.display_avatar.url, - ) - embed.description = "\n".join( - [ - f"{k}: {parse_pronouns(v)}" - for k, v in data[f"{member.id}"]["sets"].items() - ] - ) - await ctx.send(embed=embed) - - @pronouns.command(name="profile") - @app_commands.describe( - username="The username of the user. These are not Discord usernames, but pronouns.page usernames" - ) - async def profile( - self, ctx: commands.Context, *, username: Annotated[str, commands.clean_content] - ) -> None: - """Obtains the profile of an Pronouns.page user""" - await ctx.defer() - url = URL("https://en.pronouns.page/api/profile/get/") / username - params = {"version": 2} - async with self.session.get(url, params=params) as r: - data = await r.json(loads=orjson.loads) - if len(data) == 0: - await ctx.send("The pronouns were not found") - return - curr_username = data["username"] - avatar = data["avatar"] - converted = { - k: PronounsProfileEntry( - username=curr_username, - avatar=avatar, - locale=k, - names=[ - PronounsValuesEntry( - value=name["value"], opinion=name["opinion"] - ) - for name in v["names"] - ], - pronouns=[ - PronounsValuesEntry( - value=pronoun["value"], opinion=pronoun["opinion"] - ) - for pronoun in v["pronouns"] - ], - description=v["description"], - age=v["age"], - links=v["links"], - flags=v["flags"], - words=[ - PronounsWordsEntry( - header=words["header"], - values=[ - PronounsValuesEntry( - value=value["value"], opinion=value["opinion"] - ) - for value in words["values"] - ], - ) - for words in v["words"] - ], - timezone=v["timezone"]["tz"], - circle=[ - PronounsProfileCircleEntry( - username=member["username"], - avatar=member["avatar"], - mutual=member["circleMutual"], - relationship=member["relationship"], - ) - for member in v["circle"] - ] - if len(v["circle"]) != 0 - else None, - ) - for k, v in data["profiles"].items() - } - pages = PronounsProfilePages(entries=converted, ctx=ctx) - await pages.start() - - @pronouns.command(name="terms") - @app_commands.describe(query="The term to look for") - async def terms( - self, ctx: commands.Context, *, query: Optional[str] = None - ) -> None: - """Looks up terms from Pronouns.page""" - url = URL("https://en.pronouns.page/api/terms") - if query: - url = url / "search" / query - async with self.session.get(url) as r: - data = await r.json(loads=orjson.loads) - if len(data) == 0: - await ctx.send("No terms were found") - return - converted = [ - PronounsTermsEntry( - term=term["term"], - original=term["original"] if len(term["original"]) > 0 else None, - definition=term["definition"], - locale=term["locale"], - flags=term["flags"], - category=term["category"], - ) - for term in data - ] - pages = PronounsTermsPages(entries=converted, ctx=ctx) - await pages.start() - - @pronouns.command(name="nouns") - @app_commands.describe(query="The noun to look for") - async def nouns( - self, ctx: commands.Context, *, query: Optional[str] = None - ) -> None: - """Looks up nouns on Pronouns.page""" - url = URL("https://en.pronouns.page/api/nouns") - if query: - url = url / "search" / query - async with self.session.get(url) as r: - data = await r.json(loads=orjson.loads) - if len(data) == 0: - await ctx.send("No nouns were found") - return - converted = [ - PronounsNounsEntry( - masc=entry["masc"], - fem=entry["fem"], - neutr=entry["neutr"], - masc_plural=entry["mascPl"], - fem_plural=entry["femPl"], - neutr_plural=entry["neutrPl"], - ) - for entry in data - ] - pages = PronounsNounsPages(entries=converted, ctx=ctx) - await pages.start() - - @pronouns.command(name="inclusive") - @app_commands.describe(term="The inclusive term to look for") - async def inclusive( - self, ctx: commands.Context, *, term: Optional[str] = None - ) -> None: - """Provides inclusive terms for users""" - url = URL("https://en.pronouns.page/api/inclusive") - if term: - url = url / "search" / term - async with self.session.get(url) as r: - data = await r.json(loads=orjson.loads) - if len(data) == 0: - await ctx.send("No nouns were found") - return - converted = [ - PronounsInclusiveEntry( - instead_of=entry["insteadOf"], - say=entry["say"], - because=entry["because"], - categories=entry["categories"], - clarification=entry["clarification"], - ) - for entry in data - ] - pages = PronounsInclusivePages(entries=converted, ctx=ctx) - await pages.start() - - @pronouns.command(name="lookup") - @app_commands.describe( - pronouns="The pronouns to look up. These are actual pronouns, such as she/her, and they/them. " - ) - async def lookup(self, ctx: commands.Context, *, pronouns: str) -> None: - """Lookup info about the given pronouns - - Pronouns include she/her, they/them and many others. You don't have to use the binary forms (eg they/them), but search them up like 'they' or 'she' - """ - url = URL("https://en.pronouns.page/api/pronouns/") - banner_url = URL("https://en.pronouns.page/api/banner/") - full_url = url / pronouns - full_banner_url = banner_url / f"{pronouns}.png" - async with self.session.get(full_url) as r: - data = await r.json(loads=orjson.loads) - if data is None: - await ctx.send("The pronouns requested were not found") - return - desc = f"{data['description']}\n\n" - - desc += "**Info**\n" - desc += ( - f"Aliases: {data['aliases']}\nPronounceable: {data['pronounceable']}\n" - ) - desc += f"Normative: {data['normative']}\n" - if len(data["morphemes"]) != 0: - desc += "\n**Morphemes**\n" - for k, v in data["morphemes"].items(): - desc += f"{k.replace('_', ' ').title()}: {v}\n" - - if len(data["pronunciations"]) != 0: - desc += "\n**Pronunciations**\n" - for k, v in data["pronunciations"].items(): - desc += f"{k.replace('_', ' ').title()}: {v}\n" - embed = Embed() - embed.title = data["name"] - embed.description = desc - embed.add_field(name="Examples", value="\n".join(data["examples"])) - embed.add_field( - name="Forms", - value=f"Third Form: {data['thirdForm']}\nSmall Form: {data['smallForm']}", - ) - embed.add_field( - name="Plural?", - value=f"Plural: {data['plural']}\nHonorific: {data['pluralHonorific']}", - ) - embed.set_image(url=str(full_banner_url)) - await ctx.send(embed=embed) - - -async def setup(bot: KumikoCore) -> None: - await bot.add_cog(Pronouns(bot)) +from typing import Optional + +import discord +import orjson +from discord import app_commands +from discord.ext import commands +from kumikocore import KumikoCore +from Libs.cog_utils.pronouns import parse_pronouns +from Libs.ui.pronouns import ( + PronounsInclusiveEntry, + PronounsInclusivePages, + PronounsNounsEntry, + PronounsNounsPages, + PronounsProfileCircleEntry, + PronounsProfileEntry, + PronounsProfilePages, + PronounsTermsEntry, + PronounsTermsPages, + PronounsValuesEntry, + PronounsWordsEntry, +) +from Libs.utils import Embed +from typing_extensions import Annotated +from yarl import URL + + +class Pronouns(commands.Cog): + """Your to-go module for pronouns! + + This module provides a way to view pronouns for others to see. And this is used seriously as a resource for LGBTQ+ folks. + """ + + def __init__(self, bot: KumikoCore) -> None: + self.bot = bot + self.session = self.bot.session + + @property + def display_emoji(self) -> discord.PartialEmoji: + return discord.PartialEmoji.from_str( + "<:ProgressPrideheart:1053776316438167632>" + ) + + @commands.hybrid_group(name="pronouns", fallback="get") + @app_commands.describe(member="The member to lookup") + async def pronouns(self, ctx: commands.Context, member: discord.Member) -> None: + """Obtains the pronouns of a Discord user from PronounDB + + This is not directly from Discord but a third party extension + """ + params = {"platform": "discord", "ids": member.id} + async with self.session.get( + "https://pronoundb.org/api/v2/lookup", params=params + ) as r: + data = await r.json(loads=orjson.loads) + if len(data) == 0: + await ctx.send("No pronouns found for these user(s).") + return + embed = Embed() + embed.set_author( + name=f"{member.global_name}'s pronouns", + icon_url=member.display_avatar.url, + ) + embed.description = "\n".join( + [ + f"{k}: {parse_pronouns(v)}" + for k, v in data[f"{member.id}"]["sets"].items() + ] + ) + await ctx.send(embed=embed) + + @pronouns.command(name="profile") + @app_commands.describe( + username="The username of the user. These are not Discord usernames, but pronouns.page usernames" + ) + async def profile( + self, ctx: commands.Context, *, username: Annotated[str, commands.clean_content] + ) -> None: + """Obtains the profile of an Pronouns.page user""" + await ctx.defer() + url = URL("https://en.pronouns.page/api/profile/get/") / username + params = {"version": 2} + async with self.session.get(url, params=params) as r: + data = await r.json(loads=orjson.loads) + if len(data["profiles"]) == 0: + await ctx.send("The profile was not found") + return + curr_username = data["username"] + avatar = data["avatar"] + converted = { + k: PronounsProfileEntry( + username=curr_username, + avatar=avatar, + locale=k, + names=[ + PronounsValuesEntry( + value=name["value"], opinion=name["opinion"] + ) + for name in v["names"] + ], + pronouns=[ + PronounsValuesEntry( + value=pronoun["value"], opinion=pronoun["opinion"] + ) + for pronoun in v["pronouns"] + ], + description=v["description"], + age=v["age"], + links=v["links"], + flags=v["flags"], + words=[ + PronounsWordsEntry( + header=words["header"], + values=[ + PronounsValuesEntry( + value=value["value"], opinion=value["opinion"] + ) + for value in words["values"] + ], + ) + for words in v["words"] + ], + timezone=v["timezone"]["tz"], + circle=[ + PronounsProfileCircleEntry( + username=member["username"], + avatar=member["avatar"], + mutual=member["circleMutual"], + relationship=member["relationship"], + ) + for member in v["circle"] + ] + if len(v["circle"]) != 0 + else None, + ) + for k, v in data["profiles"].items() + } + pages = PronounsProfilePages(entries=converted, ctx=ctx) + await pages.start() + + @pronouns.command(name="terms") + @app_commands.describe(query="The term to look for") + async def terms( + self, ctx: commands.Context, *, query: Optional[str] = None + ) -> None: + """Looks up terms from Pronouns.page""" + url = URL("https://en.pronouns.page/api/terms") + if query: + url = url / "search" / query + async with self.session.get(url) as r: + data = await r.json(loads=orjson.loads) + if len(data) == 0: + await ctx.send("No terms were found") + return + converted = [ + PronounsTermsEntry( + term=term["term"], + original=term["original"] if len(term["original"]) > 0 else None, + definition=term["definition"], + locale=term["locale"], + flags=term["flags"], + category=term["category"], + ) + for term in data + ] + pages = PronounsTermsPages(entries=converted, ctx=ctx) + await pages.start() + + @pronouns.command(name="nouns") + @app_commands.describe(query="The noun to look for") + async def nouns( + self, ctx: commands.Context, *, query: Optional[str] = None + ) -> None: + """Looks up nouns on Pronouns.page""" + url = URL("https://en.pronouns.page/api/nouns") + if query: + url = url / "search" / query + async with self.session.get(url) as r: + # If people start using this for pronouns, then a generator shows up + # so that's in case this happens + if r.content_type == "text/html": + await ctx.send("Uhhhhhhhhhhhh what mate") + return + data = await r.json(loads=orjson.loads) + if len(data) == 0: + await ctx.send("No nouns were found") + return + converted = [ + PronounsNounsEntry( + masc=entry["masc"], + fem=entry["fem"], + neutr=entry["neutr"], + masc_plural=entry["mascPl"], + fem_plural=entry["femPl"], + neutr_plural=entry["neutrPl"], + ) + for entry in data + ] + pages = PronounsNounsPages(entries=converted, ctx=ctx) + await pages.start() + + @pronouns.command(name="inclusive") + @app_commands.describe(term="The inclusive term to look for") + async def inclusive( + self, ctx: commands.Context, *, term: Optional[str] = None + ) -> None: + """Provides inclusive terms for users""" + url = URL("https://en.pronouns.page/api/inclusive") + if term: + url = url / "search" / term + async with self.session.get(url) as r: + data = await r.json(loads=orjson.loads) + if len(data) == 0: + await ctx.send("No nouns were found") + return + converted = [ + PronounsInclusiveEntry( + instead_of=entry["insteadOf"], + say=entry["say"], + because=entry["because"], + categories=entry["categories"], + clarification=entry["clarification"], + ) + for entry in data + ] + pages = PronounsInclusivePages(entries=converted, ctx=ctx) + await pages.start() + + @pronouns.command(name="lookup") + @app_commands.describe( + pronouns="The pronouns to look up. These are actual pronouns, such as she/her, and they/them. " + ) + async def lookup(self, ctx: commands.Context, *, pronouns: str) -> None: + """Lookup info about the given pronouns + + Pronouns include she/her, they/them and many others. You don't have to use the binary forms (eg they/them), but search them up like 'they' or 'she' + """ + url = URL("https://en.pronouns.page/api/pronouns/") + banner_url = URL("https://en.pronouns.page/api/banner/") + full_url = url / pronouns + full_banner_url = banner_url / f"{pronouns}.png" + async with self.session.get(full_url) as r: + data = await r.json(loads=orjson.loads) + if data is None: + await ctx.send("The pronouns requested were not found") + return + desc = f"{data['description']}\n\n" + + desc += "**Info**\n" + desc += ( + f"Aliases: {data['aliases']}\nPronounceable: {data['pronounceable']}\n" + ) + desc += f"Normative: {data['normative']}\n" + if len(data["morphemes"]) != 0: + desc += "\n**Morphemes**\n" + for k, v in data["morphemes"].items(): + desc += f"{k.replace('_', ' ').title()}: {v}\n" + + if len(data["pronunciations"]) != 0: + desc += "\n**Pronunciations**\n" + for k, v in data["pronunciations"].items(): + desc += f"{k.replace('_', ' ').title()}: {v}\n" + embed = Embed() + embed.title = data["name"] + embed.description = desc + embed.add_field(name="Examples", value="\n".join(data["examples"])) + embed.add_field( + name="Forms", + value=f"Third Form: {data['thirdForm']}\nSmall Form: {data['smallForm']}", + ) + embed.add_field( + name="Plural?", + value=f"Plural: {data['plural']}\nHonorific: {data['pluralHonorific']}", + ) + embed.set_image(url=str(full_banner_url)) + await ctx.send(embed=embed) + + +async def setup(bot: KumikoCore) -> None: + await bot.add_cog(Pronouns(bot)) diff --git a/Bot/Libs/ui/jobs/modals.py b/Bot/Libs/ui/jobs/modals.py index 42c58477..b7180e38 100644 --- a/Bot/Libs/ui/jobs/modals.py +++ b/Bot/Libs/ui/jobs/modals.py @@ -1,157 +1,167 @@ -import asyncpg -import discord -from Libs.cog_utils.jobs import create_job_link, create_job_output_item, update_job - - -class CreateJob(discord.ui.Modal, title="Create Job"): - def __init__(self, pool: asyncpg.pool.Pool, required_rank: int, pay: int) -> None: - super().__init__() - self.pool: asyncpg.Pool = pool - self.required_rank = required_rank - self.pay = pay - self.name = discord.ui.TextInput( - label="Name", - placeholder="Name of the job", - min_length=1, - max_length=255, - row=0, - ) - self.description = discord.ui.TextInput( - label="Description", - style=discord.TextStyle.long, - placeholder="Description of the job", - min_length=1, - max_length=2000, - row=1, - ) - self.add_item(self.name) - self.add_item(self.description) - - async def on_submit(self, interaction: discord.Interaction) -> None: - # Ripped the whole thing from RDanny again... - query = """ - WITH job_insert AS ( - INSERT INTO job (name, description, guild_id, creator_id, required_rank, pay_amount) - VALUES ($3, $4, $1, $2, $5, $6) - RETURNING id - ) - INSERT into job_lookup (name, guild_id, creator_id, job_id) - VALUES ($3, $1, $2, (SELECT id FROM job_insert)); - """ - async with self.pool.acquire() as conn: - tr = conn.transaction() - await tr.start() - - try: - await conn.execute( - query, - interaction.guild.id, # type: ignore - interaction.user.id, - self.name.value, - self.description.value, - self.required_rank, - self.pay, - ) - except asyncpg.UniqueViolationError: - await tr.rollback() - await interaction.response.send_message("This job already exists.") - except Exception: - await tr.rollback() - await interaction.response.send_message("Could not create job.") - else: - await tr.commit() - await interaction.response.send_message( - f"Job {self.name} successfully created." - ) - - -class UpdateJobModal(discord.ui.Modal, title="Update Job"): - def __init__( - self, pool: asyncpg.pool.Pool, name: str, required_rank: int, pay: int - ) -> None: - super().__init__() - self.pool: asyncpg.Pool = pool - self.name = name - self.required_rank = required_rank - self.pay = pay - self.description = discord.ui.TextInput( - label="Description", - style=discord.TextStyle.long, - placeholder="Description of the job", - min_length=1, - max_length=2000, - row=1, - ) - self.add_item(self.description) - - async def on_submit(self, interaction: discord.Interaction) -> None: - status = await update_job(interaction.user.id, interaction.guild.id, self.pool, self.name, self.description.value, self.required_rank, self.pay) # type: ignore - if status[-1] == 0: - await interaction.response.send_message( - "You either don't own this job or the job doesn't exist. Try again." - ) - return - await interaction.response.send_message( - f"Successfully updated the job `{self.name}` (RR: {self.required_rank}, Pay: {self.pay})" - ) - return - - -class CreateJobOutputItemModal(discord.ui.Modal, title="Create Output Item"): - def __init__(self, pool: asyncpg.Pool, name: str, price: int, amount: int) -> None: - super().__init__() - self.pool = pool - self.name = name - self.price = price - self.amount = amount - self.description = discord.ui.TextInput( - label="Description", - style=discord.TextStyle.long, - placeholder="Description of the item", - min_length=1, - max_length=2000, - row=0, - ) - self.add_item(self.description) - - async def on_submit(self, interaction: discord.Interaction) -> None: - query = """ - SELECT eco_item_lookup.item_id, job_lookup.job_id - FROM eco_item_lookup - INNER JOIN job_lookup ON eco_item_lookup.producer_id = job_lookup.worker_id - WHERE eco_item_lookup.guild_id=$1 AND LOWER(eco_item_lookup.name)=$2 AND eco_item_lookup.producer_id=$3; - """ - status = await create_job_output_item( - name=self.name, - description=self.description.value, - price=self.price, - amount=self.amount, - guild_id=interaction.guild.id, # type: ignore - worker_id=interaction.user.id, - pool=self.pool, - ) - async with self.pool.acquire() as conn: - if status[-1] != "0": - rows = await conn.fetchrow(query, interaction.guild.id, self.name, interaction.user.id) # type: ignore - if rows is None: - await interaction.response.send_message( - "You aren't the producer of the item!" - ) - return - record = dict(rows) - job_link_status = await create_job_link( - worker_id=interaction.user.id, - item_id=record["item_id"], - job_id=record["job_id"], - conn=conn, - ) - if job_link_status[-1] != "0": - await interaction.response.send_message( - f"Successfully created the output item `{self.name}` (Price: {self.price}, Amount Per Hour: {self.amount})" - ) - return - else: - await interaction.response.send_message( - "There was an error making it. Please try again" - ) - return +import asyncpg +import discord +from Libs.cog_utils.jobs import create_job_output_item, update_job + + +class CreateJob(discord.ui.Modal, title="Create Job"): + def __init__(self, pool: asyncpg.pool.Pool, required_rank: int, pay: int) -> None: + super().__init__() + self.pool: asyncpg.Pool = pool + self.required_rank = required_rank + self.pay = pay + self.name = discord.ui.TextInput( + label="Name", + placeholder="Name of the job", + min_length=1, + max_length=255, + row=0, + ) + self.description = discord.ui.TextInput( + label="Description", + style=discord.TextStyle.long, + placeholder="Description of the job", + min_length=1, + max_length=2000, + row=1, + ) + self.add_item(self.name) + self.add_item(self.description) + + async def on_submit(self, interaction: discord.Interaction) -> None: + # Ripped the whole thing from RDanny again... + query = """ + WITH job_insert AS ( + INSERT INTO job (name, description, guild_id, creator_id, required_rank, pay_amount) + VALUES ($3, $4, $1, $2, $5, $6) + RETURNING id + ) + INSERT into job_lookup (name, guild_id, creator_id, job_id) + VALUES ($3, $1, $2, (SELECT id FROM job_insert)); + """ + async with self.pool.acquire() as conn: + tr = conn.transaction() + await tr.start() + + try: + await conn.execute( + query, + interaction.guild.id, # type: ignore + interaction.user.id, + self.name.value, + self.description.value, + self.required_rank, + self.pay, + ) + except asyncpg.UniqueViolationError: + await tr.rollback() + await interaction.response.send_message("This job already exists.") + except Exception: + await tr.rollback() + await interaction.response.send_message("Could not create job.") + else: + await tr.commit() + await interaction.response.send_message( + f"Job {self.name} successfully created." + ) + + +class UpdateJobModal(discord.ui.Modal, title="Update Job"): + def __init__( + self, pool: asyncpg.pool.Pool, name: str, required_rank: int, pay: int + ) -> None: + super().__init__() + self.pool: asyncpg.Pool = pool + self.name = name + self.required_rank = required_rank + self.pay = pay + self.description = discord.ui.TextInput( + label="Description", + style=discord.TextStyle.long, + placeholder="Description of the job", + min_length=1, + max_length=2000, + row=1, + ) + self.add_item(self.description) + + async def on_submit(self, interaction: discord.Interaction) -> None: + status = await update_job(interaction.user.id, interaction.guild.id, self.pool, self.name, self.description.value, self.required_rank, self.pay) # type: ignore + if status[-1] == 0: + await interaction.response.send_message( + "You either don't own this job or the job doesn't exist. Try again." + ) + return + await interaction.response.send_message( + f"Successfully updated the job `{self.name}` (RR: {self.required_rank}, Pay: {self.pay})" + ) + return + + +class CreateJobOutputItemModal(discord.ui.Modal, title="Create Output Item"): + def __init__(self, pool: asyncpg.Pool, name: str, price: int, amount: int) -> None: + super().__init__() + self.pool = pool + self.name = name + self.price = price + self.amount = amount + self.description = discord.ui.TextInput( + label="Description", + style=discord.TextStyle.long, + placeholder="Description of the item", + min_length=1, + max_length=2000, + row=0, + ) + self.add_item(self.description) + + async def on_submit(self, interaction: discord.Interaction) -> None: + query = """ + SELECT eco_item_lookup.item_id, job_lookup.job_id + FROM eco_item_lookup + INNER JOIN job_lookup ON eco_item_lookup.producer_id = job_lookup.worker_id + WHERE eco_item_lookup.guild_id=$1 AND LOWER(eco_item_lookup.name)=$2 AND eco_item_lookup.producer_id=$3; + """ + status = await create_job_output_item( + name=self.name, + description=self.description.value, + price=self.price, + amount=self.amount, + guild_id=interaction.guild.id, # type: ignore + worker_id=interaction.user.id, + pool=self.pool, + ) + if status[-1] != "0": + await interaction.response.send_message( + f"Successfully created the output item `{self.name}` (Price: {self.price}, Amount Per Hour: {self.amount})" + ) + return + else: + await interaction.response.send_message( + "There was an error making it. Please try again" + ) + return + # async with self.pool.acquire() as conn: + # if status[-1] != "0": + # rows = await conn.fetchrow(query, interaction.guild.id, self.name, interaction.user.id) # type: ignore + # if rows is None: + # await interaction.response.send_message( + # "You aren't the producer of the item!" + # ) + # return + # record = dict(rows) + # job_link_status = await create_job_link( + # worker_id=interaction.user.id, + # item_id=record["item_id"], + # job_id=record["job_id"], + # conn=conn, + # ) + # if job_link_status[-1] != "0": + # await interaction.response.send_message( + # f"Successfully created the output item `{self.name}` (Price: {self.price}, Amount Per Hour: {self.amount})" + # ) + # return + # else: + # await interaction.response.send_message( + # "There was an error making it. Please try again" + # ) + # return