Skip to content

Commit

Permalink
Merge pull request #18 from Local-Connectivity-Lab/ticket-669
Browse files Browse the repository at this point in the history
Ticket 669 - Implement sync for Discord threads without tickets
  • Loading branch information
philion authored Apr 5, 2024
2 parents 78c450b + b09d15b commit d71821b
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 42 deletions.
60 changes: 37 additions & 23 deletions cog_scn.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import discord

from discord.commands import SlashCommandGroup
from discord.ext import commands, tasks
from discord.ext import commands

from netbot import NetbotException
from model import Message
from redmine import Client


Expand Down Expand Up @@ -91,33 +91,47 @@ async def add(self, ctx:discord.ApplicationContext, redmine_login:str, member:di
self.redmine.user_mgr.reindex_users()


async def sync_thread(self, thread:discord.Thread):
"""syncronize an existing ticket thread with redmine"""
# get the ticket id from the thread name
ticket_id = self.bot.parse_thread_title(thread.name)

ticket = self.redmine.get_ticket(ticket_id, include_journals=True)
if ticket:
completed = await self.bot.synchronize_ticket(ticket, thread)
if completed:
return ticket
else:
raise NetbotException(f"Ticket {ticket.id} is locked for syncronization.")
else:
log.debug(f"no ticket found for {thread.name}")

return None


@scn.command()
async def sync(self, ctx:discord.ApplicationContext):
"""syncronize an existing ticket thread with redmine"""
if isinstance(ctx.channel, discord.Thread):
ticket = await self.sync_thread(ctx.channel)
thread = ctx.channel
ticket = await self.bot.sync_thread(thread)
if ticket:
await ctx.respond(f"SYNC ticket {ticket.id} to thread: {ctx.channel.name} complete")
await ctx.respond(f"SYNC ticket {ticket.id} to thread: {thread.name} complete")
else:
await ctx.respond(f"Cannot find ticket# in thread name: {ctx.channel.name}") # error
# double-check thread name
ticket_id = self.bot.parse_thread_title(thread.name)
if ticket_id:
await ctx.respond(f"No ticket (#{ticket_id}) found for thread named: {thread.name}")
else:
# create new ticket
subject = thread.name
user = self.redmine.user_mgr.find(ctx.user.name)
message = Message(user.login, subject) # user.mail?
message.note = subject + "\n\nCreated by netbot by syncing Discord thread with same name."
ticket = self.redmine.ticket_mgr.create(user, message)
# set tracker
# TODO: search up all parents in hierarchy?
tracker = self.bot.lookup_tracker(thread.parent.name)
if tracker:
log.debug(f"found {thread.parent.name} => {tracker}")
params = {
"tracker_id": str(tracker.id),
"notes": f"Setting tracker based on channel name: {thread.parent.name}"
}
self.redmine.ticket_mgr.update(ticket.id, params, user.login)
else:
log.debug(f"not tracker for {thread.parent.name}")

# rename thread
await thread.edit(name=f"Ticket #{ticket.id}: {ticket.subject}")

# sync the thread
ticket = await self.bot.sync_thread(thread) # refesh the ticket
await ctx.respond(self.bot.formatter.format_ticket(ticket))

#OLD await ctx.respond(f"Cannot find ticket# in thread name: {ctx.channel.name}") # error
else:
await ctx.respond("Not a thread.") # error

Expand Down
43 changes: 30 additions & 13 deletions cog_tickets.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import discord

from discord.commands import option
from discord.ext import commands, tasks
from discord.ext import commands

from model import Message, Ticket
from redmine import Client
Expand Down Expand Up @@ -112,6 +112,16 @@ async def ticket(self, ctx: discord.ApplicationContext, ticket_id:int, action:st
await ctx.respond(f"Error {action} {ticket_id}: {e}")


async def create_thread(self, ticket:Ticket, ctx:discord.ApplicationContext):
log.info(f"creating a new thread for ticket #{ticket.id} in channel: {ctx.channel}")
thread_name = f"Ticket #{ticket.id}: {ticket.subject}"
# added public_thread type param
thread = await ctx.channel.create_thread(name=thread_name, type=discord.ChannelType.public_thread)
# ticket-614: Creating new thread should post the ticket details to the new thread
await thread.send(self.bot.formatter.format_ticket_details(ticket))
return thread


@commands.slash_command(name="new", description="Create a new ticket")
@option("title", description="Title of the new SCN ticket")
@option("add_thread", description="Create a Discord thread for the new ticket", default=False)
Expand All @@ -120,25 +130,32 @@ async def create_new_ticket(self, ctx: discord.ApplicationContext, title:str):
if user is None:
await ctx.respond(f"Unknown user: {ctx.user.name}")
return

channel_name = ctx.channel.name
# text templating
text = f"ticket created by Discord user {ctx.user.name} -> {user.login}, with the text: {title}"
message = Message(from_addr=user.mail, subject=title, to=ctx.channel.name)
message.set_note(text)
## TODO cleanup with cogscn.sync: both have complex ticket creation
ticket = self.redmine.create_ticket(user, message)
if ticket:
await self.bot.formatter.print_ticket(ticket, ctx)
# ticket created, set tracker
# set tracker
# TODO: search up all parents in hierarchy?
tracker = self.bot.lookup_tracker(channel_name)
if tracker:
log.debug(f"found {channel_name} => {tracker}")
params = {
"tracker_id": str(tracker.id),
"notes": f"Setting tracker based on channel name: {channel_name}"
}
self.redmine.ticket_mgr.update(ticket.id, params, user.login)
else:
log.debug(f"not tracker for {channel_name}")
# create related discord thread
await self.thread_ticket(ctx, ticket.id)
#await self.bot.formatter.print_ticket(ticket, ctx)
else:
await ctx.respond(f"error creating ticket with title={title}")


async def create_thread(self, ticket:Ticket, ctx:discord.ApplicationContext):
log.info(f"creating a new thread for ticket #{ticket.id} in channel: {ctx.channel}")
thread_name = f"Ticket #{ticket.id}: {ticket.subject}"
thread = await ctx.channel.create_thread(name=thread_name)
# ticket-614: Creating new thread should post the ticket details to the new thread
await thread.send(self.bot.formatter.format_ticket_details(ticket))
return thread
await ctx.respond(f"Error creating ticket with title={title}")


@commands.slash_command(name="thread", description="Create a Discord thread for the specified ticket")
Expand Down
8 changes: 8 additions & 0 deletions formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ def format_discord_note(self, note) -> str:
return f"> **{note.user}** *{age} ago*\n> {note.notes}"[:MAX_MESSAGE_LEN]


def format_ticket(self, ticket:Ticket) -> str:
link = self.format_link(ticket)
status = f"{EMOJI[ticket.status.name]} {ticket.status.name}"
priority = f"{EMOJI[ticket.priority.name]} {ticket.priority.name}"
assigned = ticket.assigned_to.name if ticket.assigned_to else ""
return " ".join([link, priority, status, ticket.tracker.name, assigned, ticket.subject])


def format_ticket_details(self, ticket:Ticket) -> str:
link = self.format_link(ticket)
# link is mostly hidden, so we can't use the length to format.
Expand Down
42 changes: 39 additions & 3 deletions netbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from discord.ext import commands, tasks

from formatting import DiscordFormatter
from model import TicketNote, Ticket
from model import TicketNote, Ticket, NamedId
import synctime
import redmine

Expand Down Expand Up @@ -60,9 +60,23 @@ def __init__(self, client: redmine.Client):
)


def initialize_tracker_mapping(self):
# load the trackers, indexed by tracker name
self.trackers = self.redmine.ticket_mgr.load_trackers()
# update to include each mapping in
for tracker_name, channel_name in _TRACKER_MAPPING.items():
if tracker_name in self.trackers:
self.trackers[channel_name] = self.trackers[tracker_name]
else:
log.debug(f"unmapped tracker: {tracker_name}")


def run_bot(self):
"""start netbot"""
log.info(f"starting {self}")

self.initialize_tracker_mapping()

super().run(os.getenv('DISCORD_TOKEN'))


Expand All @@ -84,7 +98,7 @@ async def on_message(self, message:discord.Message):
if isinstance(message.channel, discord.Thread):
# IS a thread, check the name
ticket_id = self.parse_thread_title(message.channel.name)
if ticket_id > 0:
if ticket_id:
user = self.redmine.user_mgr.find(message.author.name)
if user:
log.debug(f"known user commenting on ticket #{ticket_id}: redmine={user.login}, discord={message.author.name}")
Expand Down Expand Up @@ -222,6 +236,24 @@ async def on_application_command_error(self, context: discord.ApplicationContext
await context.respond(f"Error processing due to: {exception.__cause__}")


async def sync_thread(self, thread:discord.Thread):
"""syncronize an existing ticket thread with redmine"""
# get the ticket id from the thread name
ticket_id = self.parse_thread_title(thread.name)

ticket = self.redmine.get_ticket(ticket_id, include_journals=True)
if ticket:
completed = await self.synchronize_ticket(ticket, thread)
if completed:
return ticket
else:
raise NetbotException(f"Ticket {ticket.id} is locked for syncronization.")
else:
log.debug(f"no ticket found for {thread.name}")

return None


@tasks.loop(minutes=1.0) # FIXME to 5.0 minutes. set to 1 min for testing
async def sync_all_threads(self):
"""
Expand Down Expand Up @@ -317,6 +349,10 @@ async def check_expired_tickets(self):
self.expire_expired_tickets()


def lookup_tracker(self, tracker:str) -> NamedId:
return self.trackers.get(tracker, None)


def main():
"""netbot main function"""
log.info(f"loading .env for {__name__}")
Expand All @@ -335,7 +371,7 @@ def main():

def setup_logging():
"""set up logging for netbot"""
logging.basicConfig(level=logging.DEBUG,
logging.basicConfig(level=logging.INFO,
format="{asctime} {levelname:<8s} {name:<16} {message}", style='{')
logging.getLogger("discord.gateway").setLevel(logging.WARNING)
logging.getLogger("discord.http").setLevel(logging.WARNING)
Expand Down
2 changes: 1 addition & 1 deletion test_cog_scn.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ async def test_locked_during_sync_ticket(self):

try:
# synchronize thread
await self.cog.sync_thread(thread)
await self.bot.sync_thread(thread)
self.fail("No exception when one was expected")
except Exception as ex:
self.assertIn(f"Ticket {ticket.id}", str(ex))
Expand Down
17 changes: 15 additions & 2 deletions tickets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@



from model import SYNC_FIELD_NAME, TO_CC_FIELD_NAME, User, Message, NamedId, Team, Ticket, TicketNote, TicketsResult
from model import TO_CC_FIELD_NAME, User, Message, NamedId, Team, Ticket, TicketNote, TicketsResult
from session import RedmineSession, RedmineException
import synctime

Expand Down Expand Up @@ -48,6 +48,19 @@ def load_custom_fields(self) -> dict[str,NamedId]:
log.warning("No custom fields to load")


def load_trackers(self) -> dict[str,NamedId]:
# call redmine to get the ticket trackers
response = self.session.get("/trackers.json")
if response:
trackers = {}
for item in response['trackers']:
#print(f"##### {item}")
trackers[item['name']] = NamedId(id=item['id'], name=item['name'])
return trackers
else:
log.warning("No custom fields to load")


def get_field_id(self, name:str) -> int | None:
if name in self.custom_fields:
return self.custom_fields[name].id
Expand Down Expand Up @@ -486,7 +499,7 @@ def main():
#print(ticket_mgr.get(105, include_children=True).json_str())
#print(json.dumps(ticket_mgr.load_custom_fields(), indent=4, default=vars))

#print(ticket_mgr.due())
print(ticket_mgr.due())

# for testing the redmine
if __name__ == '__main__':
Expand Down

0 comments on commit d71821b

Please sign in to comment.