diff --git a/netbot/cog_tickets.py b/netbot/cog_tickets.py index 8dc52a2..d531d50 100644 --- a/netbot/cog_tickets.py +++ b/netbot/cog_tickets.py @@ -3,16 +3,20 @@ """encapsulate Discord ticket functions""" import logging +import datetime as dt import discord - +from discord import ScheduledEvent from discord.commands import option, SlashCommandGroup - from discord.ext import commands from discord.enums import InputTextStyle from discord.ui.item import Item, V from discord.utils import basic_autocomplete + +import dateparser + from redmine.model import Message, Ticket +from redmine import synctime from redmine.redmine import Client from netbot.netbot import NetBot, TEAM_MAPPING, CHANNEL_MAPPING, default_ticket @@ -216,6 +220,7 @@ def __init__(self, bot:NetBot): self.bot:NetBot = bot self.redmine: Client = bot.redmine + # see https://github.com/Pycord-Development/pycord/blob/master/examples/app_commands/slash_cog_groups.py ticket = SlashCommandGroup("ticket", "ticket commands") @@ -629,6 +634,63 @@ async def subject(self, ctx: discord.ApplicationContext, ticket_id:int, subject: embed=self.bot.formatter.ticket_embed(ctx, updated)) + async def find_event_for_ticket(self, ctx: discord.ApplicationContext, ticket_id:int) -> ScheduledEvent: + title_prefix = f"Ticket #{ticket_id}" + for event in await ctx.guild.fetch_scheduled_events(): + if event.name.startswith(title_prefix): + return event + + + @ticket.command(name="due", description="Set a due date for the ticket") + @option("date", description="New ticket due date") + async def due(self, ctx: discord.ApplicationContext, date_str:str): + # automatiuc date conversion? + # get the ticket ID from the thread: + ticket_id = ctx.bot.parse_thread_title(ctx.channel.name) + if ticket_id: + # got a valid ticket, update it + # standard date string, date format, etc. + due_date = dateparser.parse(date_str, settings={ + 'RETURN_AS_TIMEZONE_AWARE': True, + 'PREFER_DATES_FROM': 'future', + 'REQUIRE_PARTS': ['day', 'month', 'year']}) + if due_date: + due_str = synctime.date_str(due_date) + ticket = self.redmine.ticket_mgr.update(ticket_id, {"due_date": due_str}) + if ticket: + # valid ticket, create an event + # check for time, default to 9am if 0 + if due_date.hour == 0: + due_date.hour = 9 # DEFAULT "meeting" time, 9am local time (for the bot) + + event_name = f"Ticket #{ticket.id} Due" + + # check for existing event + existing = await self.find_event_for_ticket(ctx, ticket.id) + if existing: + await existing.edit( + start_time = due_date, + end_time = due_date + dt.timedelta(hours=1)) # DEFAULT "meeting" length, one hour) + log.info(f"Updated existing DUE event: {existing.name}") + await ctx.respond(f"Updated due date on {ticket_id} to {due_date.strftime(synctime.DATETIME_FORMAT)}") + else: + event = await ctx.guild.create_scheduled_event( + name = event_name, + description = ticket.subject, + start_time = due_date, + end_time = due_date + dt.timedelta(hours=1), # DEFAULT "meeting" length, one hour + location = ctx.channel.name) + log.info(f"created event {event} for ticket={ticket.id}") + await ctx.send(f"Updated due date on ticket #{ticket_id} to {due_date}.") + else: + await ctx.respond(f"Problem updating ticket {ticket_id}, unknown ticket ID.") + else: + await ctx.respond(f"Invalid date value entered. Unable to parse `{date_str}`") + else: + # no ticket available. + await ctx.respond("Command only valid in ticket thread. No ticket info found in this thread.") + + @ticket.command(name="help", description="Display hepl about ticket management") async def help(self, ctx: discord.ApplicationContext): await ctx.respond(embed=self.bot.formatter.help_embed(ctx)) diff --git a/redmine/session.py b/redmine/session.py index 21ab892..d7885f9 100644 --- a/redmine/session.py +++ b/redmine/session.py @@ -95,6 +95,7 @@ def put(self, resource: str, data:str, impersonate_id:str|None=None) -> None: if r.ok: log.debug(f"PUT {resource}: {data}") else: + log.warning(f"Request: {data}, impersonate_id={impersonate_id}") raise RedmineException(f"PUT {resource} by {impersonate_id} failed, status=[{r.status_code}] {r.reason}", r.headers['X-Request-Id']) diff --git a/redmine/synctime.py b/redmine/synctime.py index f683035..cca2f96 100644 --- a/redmine/synctime.py +++ b/redmine/synctime.py @@ -10,8 +10,12 @@ log = logging.getLogger(__name__) +# 2014-01-02 +DATE_FORMAT = "%Y-%m-%d" +DATETIME_FORMAT = DATE_FORMAT + " %H:%M" + # 2014-01-02T08:12:32Z -ZULU_FORMAT = "%Y-%m-%dT%H:%M:%SZ" +ZULU_FORMAT = DATE_FORMAT + "T%H:%M:%SZ" def now() -> dt.datetime: @@ -47,6 +51,11 @@ def age_str(time:dt.datetime) -> str: return humanize.naturaldelta(age(time)) +def date_str(timestamp:dt.datetime) -> str: + """convert a datetime to the UTC Zulu string redmine expects""" + return timestamp.strftime(DATE_FORMAT) + + def zulu(timestamp:dt.datetime) -> str: """convert a datetime to the UTC Zulu string redmine expects""" return timestamp.strftime(ZULU_FORMAT) diff --git a/requirements.txt b/requirements.txt index f54f71e..ed1c9fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ charset-normalizer==3.2.0 click==8.1.7 coverage==7.4.0 cryptography==42.0.0 +dateparser==1.2.0 dill==0.3.8 frozenlist==1.4.0 humanize==4.8.0 @@ -28,11 +29,16 @@ Pygments==2.16.1 pylint==3.0.3 pynetbox==7.0.1 pyOpenSSL==24.0.0 +python-dateutil==2.9.0.post0 python-dotenv==1.0.0 python-gnupg==0.5.2 +pytz==2024.2 +regex==2024.9.11 requests==2.31.0 rich==13.6.0 ruff==0.3.0 +six==1.16.0 tomlkit==0.12.3 +tzlocal==5.2 urllib3==2.0.4 yarl==1.9.2