Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ticket 509 - Fixing timeout exception and refactoring redmine HTTP session handling #13

Merged
merged 15 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading