Skip to content

Commit

Permalink
Merge pull request #13 from Local-Connectivity-Lab/ticket-509
Browse files Browse the repository at this point in the history
Ticket 509 - Fixing timeout exception and refactoring redmine HTTP session handling
  • Loading branch information
philion authored Mar 6, 2024
2 parents 04ef60b + a7f5cab commit 23eb724
Show file tree
Hide file tree
Showing 18 changed files with 1,684 additions and 1,018 deletions.
68 changes: 32 additions & 36 deletions cli.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
#!/usr/bin/env python3

import io
import sys
import logging
import datetime as dt
import math
import random
import time


import hashlib
import click
Expand All @@ -28,7 +25,7 @@
# redmine client
# load creds into env, and init the redmine client
load_dotenv()
redmine_client = redmine.Client()
redmine_client = redmine.Client.fromenv()

# figure out what the term refers to
# better way?
Expand Down Expand Up @@ -94,13 +91,13 @@ def print_tickets_md(tickets, fields=["link","status","priority","age","assigned
table.add_row(*row)

console.print(table)

buffer = io.StringIO(console.file.getvalue())

for line in buffer:
print(f"{line.strip()}")


def print_team(team):
console = Console()
table = Table(show_header=True, box=box.SIMPLE_HEAD, collapse_padding=True, header_style="bold magenta")
Expand Down Expand Up @@ -142,9 +139,9 @@ def hash_color(value):
# consistently-hash the value into a color
# hash_val = hash(value) <-- this does it inconsistantly (for security reasons)
hash_val = int(hashlib.md5(value.encode('utf-8')).hexdigest(), 16)
r = (hash_val & 0xFF0000) >> 16;
g = (hash_val & 0x00FF00) >> 8;
b = hash_val & 0x0000FF;
r = (hash_val & 0xFF0000) >> 16
g = (hash_val & 0x00FF00) >> 8
b = hash_val & 0x0000FF
return f"rgb({r},{g},{b})"

def get_formatted_field(ticket, field):
Expand Down Expand Up @@ -280,8 +277,8 @@ def format_ticket(ticket, fields=["link","priority","updated","assigned", "subje

def format_ticket_details(ticket):
print(ticket)


@click.group()
def cli():
"""This script showcases different terminal UI helpers in Click."""
Expand All @@ -296,38 +293,38 @@ def tickets(query):
print_tickets(resolve_query_term(query))
else:
print_tickets(redmine_client.my_tickets())


@cli.command()
@click.argument("id", type=int)
def resolve(id:int):
"""Reslove ticket"""
@click.argument("id", type=int)
def resolve(id:int):
"""Reslove ticket"""
# case "resolve":
redmine_client.resolve_ticket(id)
print_ticket(redmine_client.get_ticket(id))


@cli.command()
@click.argument("id", type=int)
def progress(id:int):
"""Mark ticket in-progress"""
@click.argument("id", type=int)
def progress(id:int):
"""Mark ticket in-progress"""
#case "progress":
redmine_client.progress_ticket(id)
print_ticket(redmine_client.get_ticket(id))


@cli.command()
@click.argument("id", type=int)
@click.argument("asignee", type=str)
@click.argument("id", type=int)
@click.argument("asignee", type=str)
def assign(id:int, asignee:str):
"""Assign ticket to user"""
# case assign
redmine_client.assign_ticket(id, asignee)
print_ticket(redmine_client.get_ticket(id))


@cli.command()
@click.argument("id", type=int)
@click.argument("id", type=int)
def unassign(id:int):
"""Unassign ticket"""
# case "unassign":
Expand All @@ -342,24 +339,24 @@ def teams():


@cli.command()
@click.argument("team", type=str)
@click.argument("team", type=str)
def team(team:str):
"""List team members"""
print_team(redmine_client.get_team(team))


@cli.command()
@click.argument("user", type=str)
@click.argument("team", type=str)
@click.argument("user", type=str)
@click.argument("team", type=str)
def join(user:str, team:str):
"""Join a team"""
redmine_client.join_team(user, team)
print_team(redmine_client.get_team(team))


@cli.command()
@click.argument("user", type=str)
@click.argument("team", type=str)
@click.argument("user", type=str)
@click.argument("team", type=str)
def leave(user:str, team:str):
"""Leave a team"""
redmine_client.leave_team(user, team)
Expand All @@ -368,4 +365,3 @@ def leave(user:str, team:str):

if __name__ == '__main__':
cli()

26 changes: 14 additions & 12 deletions cog_scn.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@ async def add(self, ctx:discord.ApplicationContext, redmine_login:str, member:di
log.info(f"Overriding current user={ctx.user.name} with member={member.name}")
discord_name = member.name

user = self.redmine.find_discord_user(discord_name)
user = self.redmine.user_mgr.find(discord_name)

if user:
await ctx.respond(f"Discord user: {discord_name} is already configured as redmine user: {user.login}")
else:
user = self.redmine.find_user(redmine_login)
user = self.redmine.user_mgr.find(redmine_login)
if user:
self.redmine.create_discord_mapping(redmine_login, discord_name)
await ctx.respond(f"Discord user: {discord_name} has been paired with redmine user: {redmine_login}")
Expand All @@ -95,15 +95,17 @@ async def add(self, ctx:discord.ApplicationContext, redmine_login:str, member:di
async def sync_thread(self, thread:discord.Thread):
"""syncronize an existing ticket thread with redmine"""
# get the ticket id from the thread name
# FIXME: notice the series of calls to "self.bot": could be better encapsulated
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

Expand Down Expand Up @@ -150,7 +152,7 @@ async def sync(self, ctx:discord.ApplicationContext):
@scn.command()
async def reindex(self, ctx:discord.ApplicationContext):
"""reindex the user and team information"""
self.redmine.reindex()
self.redmine.user_mgr.reindex()
await ctx.respond("Rebuilt redmine indices.")


Expand All @@ -161,13 +163,13 @@ async def join(self, ctx:discord.ApplicationContext, teamname:str , member: disc
log.info(f"Overriding current user={ctx.user.name} with member={member.name}")
discord_name = member.name

user = self.redmine.find_discord_user(discord_name)
user = self.redmine.user_mgr.find(discord_name)
if user is None:
await ctx.respond(f"Unknown user, no Discord mapping: {discord_name}")
elif self.redmine.find_team(teamname) is None:
elif self.redmine.user_mgr.get_team_by_name(teamname) is None:
await ctx.respond(f"Unknown team name: {teamname}")
else:
self.redmine.join_team(user.login, teamname)
self.redmine.user_mgr.join_team(user, teamname)
await ctx.respond(f"**{discord_name}** has joined *{teamname}*")


Expand All @@ -177,10 +179,10 @@ async def leave(self, ctx:discord.ApplicationContext, teamname:str, member: disc
if member:
log.info(f"Overriding current user={ctx.user.name} with member={member.name}")
discord_name = member.name
user = self.redmine.find_discord_user(discord_name)
user = self.redmine.user_mgr.find(discord_name)

if user:
self.redmine.leave_team(user.login, teamname)
self.redmine.user_mgr.leave_team(user, teamname)
await ctx.respond(f"**{discord_name}** has left *{teamname}*")
else:
await ctx.respond(f"Unknown Discord user: {discord_name}.")
Expand Down Expand Up @@ -214,10 +216,10 @@ async def teams(self, ctx:discord.ApplicationContext, teamname:str=None):
async def block(self, ctx:discord.ApplicationContext, username:str):
log.debug(f"blocking {username}")
#user = self.redmine.lookup_user(username)
user = self.redmine.find_user(username)
user = self.redmine.user_mgr.find(username)
if user:
# add the user to the blocked list
self.redmine.block_user(user)
self.redmine.user_mgr.block(user)
# search and reject all tickets from that user
for ticket in self.redmine.get_tickets_by(user):
self.redmine.reject_ticket(ticket.id)
Expand All @@ -230,7 +232,7 @@ async def block(self, ctx:discord.ApplicationContext, username:str):
@scn.command(description="unblock specific a email address")
async def unblock(self, ctx:discord.ApplicationContext, username:str):
log.debug(f"unblocking {username}")
user = self.redmine.find_user(username)
user = self.redmine.user_mgr.find(username)
if user:
self.redmine.unblock_user(user)
await ctx.respond(f"Unblocked user: {user.login}")
Expand Down
19 changes: 9 additions & 10 deletions cog_tickets.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def resolve_query_term(self, term):
return [ticket]
except ValueError:
# not a numeric id, check team
if self.redmine.is_user_or_group(term):
if self.redmine.user_mgr.is_user_or_group(term):
return self.redmine.tickets_for_team(term)
else:
# assume a search term
Expand All @@ -68,9 +68,7 @@ async def tickets(self, ctx: discord.ApplicationContext, params: str = ""):
# add groups to users.

# lookup the user
log.debug(f"looking for user mapping for {ctx}")

user = self.redmine.find_discord_user(ctx.user.name)
user = self.redmine.user_mgr.find(ctx.user.name)
log.debug(f"found user mapping for {ctx.user.name}: {user}")

args = params.split()
Expand All @@ -88,7 +86,7 @@ async def ticket(self, ctx: discord.ApplicationContext, ticket_id:int, action:st
"""Update status on a ticket, using: unassign, resolve, progress"""
try:
# lookup the user
user = self.redmine.find_discord_user(ctx.user.name)
user = self.redmine.user_mgr.find(ctx.user.name)
log.debug(f"found user mapping for {ctx.user.name}: {user}")

match action:
Expand Down Expand Up @@ -131,7 +129,7 @@ async def ticket(self, ctx: discord.ApplicationContext, ticket_id:int, action:st
@option("title", description="Title of the new SCN ticket")
@option("add_thread", description="Create a Discord thread for the new ticket", default=False)
async def create_new_ticket(self, ctx: discord.ApplicationContext, title:str):
user = self.redmine.find_discord_user(ctx.user.name)
user = self.redmine.user_mgr.find(ctx.user.name)
if user is None:
await ctx.respond(f"Unknown user: {ctx.user.name}")
return
Expand Down Expand Up @@ -166,7 +164,8 @@ async def thread_ticket(self, ctx: discord.ApplicationContext, ticket_id:int):
# update the discord flag on tickets, add a note with url of thread; thread.jump_url
# TODO message templates
note = f"Created Discord thread: {thread.name}: {thread.jump_url}"
user = self.redmine.find_discord_user(ctx.user.name)
user = self.redmine.user_mgr.find_discord_user(ctx.user.name)
log.debug(f">>> found {user} for {ctx.user.name}")
self.redmine.enable_discord_sync(ticket.id, user, note)

await ctx.respond(f"Created new thread for {ticket.id}: {thread}") # todo add some fancy formatting
Expand Down Expand Up @@ -199,7 +198,7 @@ def format_tickets(self, title, tickets, fields=None, max_len=2000):
return "No tickets found."

if fields is None:
fields = ["link","priority","updated","assigned","subject"]
fields = ["link","priority","updated_on","assigned_to","subject"]

section = "**" + title + "**\n"
for ticket in tickets:
Expand All @@ -216,8 +215,8 @@ def format_tickets(self, title, tickets, fields=None, max_len=2000):
def format_ticket(self, ticket, fields=None):
section = ""
if fields is None:
fields = ["link","priority","updated","assigned","subject"]
fields = ["link","priority","updated_on","assigned_to","subject"]

for field in fields:
section += self.redmine.get_field(ticket, field) + " " # spacer, one space
section += str(self.redmine.get_field(ticket, field)) + " " # spacer, one space
return section.strip() # remove trailing whitespace
19 changes: 12 additions & 7 deletions imap.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,20 @@ def __init__(self, name:str, content_type:str, payload):
self.payload = payload
self.token = None

def upload(self, client, user_id):
self.token = client.upload_file(user_id, self.payload, self.name, self.content_type)
def upload(self, client, user):
self.token = client.upload_file(user, self.payload, self.name, self.content_type)

def set_token(self, token):
self.token = token


class Message():
"""email message"""
from_address: str
subject:str
attachments: list[Attachment]
note: str

def __init__(self, from_addr:str, subject:str):
self.from_address = from_addr
self.subject = subject
Expand Down Expand Up @@ -90,7 +95,7 @@ def __init__(self):
self.host = os.getenv('IMAP_HOST')
self.user = os.getenv('IMAP_USER')
self.passwd = os.getenv('IMAP_PASSWORD')
self.redmine = redmine.Client()
self.redmine:redmine.Client = redmine.Client.fromenv()

# note: not happy with this method of dealing with complex email address
# but I don't see a better way. open to suggestions
Expand Down Expand Up @@ -232,11 +237,11 @@ def handle_message(self, msg_id:str, message:Message):
ticket = self.redmine.find_ticket_from_str(subject)

# get user id from from_address
user = self.redmine.find_user(addr)
user = self.redmine.user_mgr.get_by_name(addr)
if user is None:
log.debug(f"Unknown email address, no user found: {addr}, {message.from_address}")
# create new user
user = self.redmine.create_user(addr, first, last)
user = self.redmine.user_mgr.create(addr, first, last)
log.info(f"Unknow user: {addr}, created new account.")

# upload any attachments
Expand All @@ -251,8 +256,8 @@ def handle_message(self, msg_id:str, message:Message):
log.info(f"Updated ticket #{ticket.id} with message from {user.login} and {len(message.attachments)} attachments")
else:
# no open tickets, create new ticket for the email message
self.redmine.create_ticket(user, subject, message.note, message.attachments)
log.info(f"Created new ticket for: {user.login}, {subject}, with {len(message.attachments)} attachments")
ticket = self.redmine.create_ticket(user, subject, message.note, message.attachments)
log.info(f"Created new ticket for: {ticket}, with {len(message.attachments)} attachments")


def synchronize(self):
Expand Down
Loading

0 comments on commit 23eb724

Please sign in to comment.