class ZendeskAction(Action): def __init__(self, config): super(ZendeskAction, self).__init__(config=config) self.email = self.config['email'] self.token = self.config['api_token'] self.subdomain = self.config['subdomain'] self.credentials = { 'email': self.email, 'token': self.token, 'subdomain': self.subdomain } self.api = Zenpy(**self.credentials) def clean_response(self, text): return text.replace('\n', ' ').replace(' ', ' ').strip() def url_for_ticket(self, ticket): return 'https://{}.zendesk.com/agent/tickets/{}'.format(self.subdomain, ticket) def api_search(self, query, search_type): return self.api.search(query, type=search_type, sort_by='created_at', sort_order='desc') def create_ticket(self, subject, description): ticket = Ticket(subject=subject, description=description) try: created_ticket_audit = self.api.tickets.create(ticket) return { 'ticket_id': created_ticket_audit.ticket.id, 'ticket_url': self.url_for_ticket(created_ticket_audit.ticket.id), 'subject': self.clean_response(subject), 'description': self.clean_response(description) } except APIException: return {'error': 'Could not create ticket with provided parameters'} except Exception as e: self.logger.error(e) return {'error': 'Could not make API request'} def search_tickets(self, query, search_type='ticket', limit=10): try: query_results = self.api_search(query, search_type) results_clean = map(lambda t: { 'ticket_id': t.id, 'ticket_url': self.url_for_ticket(t.id), 'ticket_status': t.status, 'subject': self.clean_response(t.subject), 'description': self.clean_response(t.description)}, list(query_results)[:limit] ) return {'search_results': results_clean} except APIException: return {'error': 'Could not execute search for query: {}'.format(query)} except Exception as e: self.logger.error(e) return {'error': 'There was an error executing your search'} def update_ticket(self, ticket_id, comment_text, public): try: ticket = self.api.tickets(id=ticket_id) ticket.comment = Comment(body=comment_text, public=public) self.api.tickets.update(ticket) return { 'ticket_id': ticket_id, 'ticket_url': self.url_for_ticket(ticket_id), 'body': self.clean_response(comment_text), 'public': public } except RecordNotFoundException: return {'error': 'Could not find ticket #{}'.format(ticket_id)} except Exception as e: self.logger.error(e) return {'error': 'Could not update ticket'} def update_ticket_status(self, ticket_id, status): valid_statuses = ['new', 'open', 'pending', 'solved', 'closed'] if status in valid_statuses: try: ticket = self.api.tickets(id=ticket_id) ticket.status = status self.api.tickets.update(ticket) return { 'ticket_id': ticket_id, 'ticket_url': self.url_for_ticket(ticket_id), 'status': status } except RecordNotFoundException: return {'error': 'Could not find ticket #{}'.format(ticket_id)} except Exception as e: self.logger.error(e) return {'error': 'Could not update ticket status'} else: return {'error': 'Invalid status given for ticket'}
def close_ticket(prescription): if not isinstance(prescription, MedicalPrescription): return False mticket = ZendeskTicket.objects.filter(prescription=prescription).first() zenpy_client = Zenpy(**settings.ZENDESK) if not mticket: return False ticket = zenpy_client.tickets(id=mticket.external_id) ticket.status = 'solved' if prescription.status in domain_models.MedicalPrescription.STATUS_CHOICES: new_cf = [] for cf in ticket.custom_fields: if cf.get('id', None) == 114103846891: new_cf.append({ 'id': 114103846891, 'value': prescription.status.lower() }) new_cf.append(cf) ticket.custom_fields = new_cf ticket_audit = zenpy_client.tickets.update(ticket) if ticket_audit: return True return False
def index(): try: # We use try method to keep running if there is no API available. # Default zenpy_client = Zenpy(**creds) ticket_generator = zenpy_client.tickets() return render_template('index.html', tickets=ticket_generator) except: print("Sorry, API isn't currently available")
def get_tickets(client: Zenpy) -> List: """ Gets all tickets from the Zendesk API for the workspace. N.B.: will be inefficient at large size, should paginate requests in future. :param client: the Zenpy client connection object :return: a list of all tickets for the client workspace """ return list(client.tickets())
def get_description(ticket_id): global zenpy_client try: zenpy_client = Zenpy(**credentials) ticket = zenpy_client.tickets(id=ticket_id) return ticket.description except Exception as e: logger.exception("get_description {}".format(e)) raise
def get_unclosed_tickets(email: str, token: str, subdomain: str) -> Iterable[Ticket]: zenpy = Zenpy(subdomain, email, token) ticket: ZendeskTicket for ticket in zenpy.tickets(): requester: ZendeskUser = ticket.requester waiting_on = None staled_at = None yield Ticket(ticket.id, ticket.subject, requester.email, requester.email, ticket.status, waiting_on, staled_at, ticket.created_at)
def update_ticket(ticket_id, message): global zenpy_client comment = "```\n{}\n```".format(message) try: zenpy_client = Zenpy(**credentials) ticket = zenpy_client.tickets(id=ticket_id) ticket.comment = Comment(body=comment, public=False) zenpy_client.tickets.update(ticket) except Exception as e: logger.exception("update_ticket {}".format(e)) raise
def close_adf_ticket(ticket): if not isinstance(ticket, ZendeskASFTicket): return False zenpy_client = Zenpy(**settings.ZENDESK) ticket = zenpy_client.tickets(id=ticket.external_id) ticket.status = 'solved' ticket_audit = zenpy_client.tickets.update(ticket) if ticket_audit: return True return False
def update_zendesk_ticket(ticket_id, comment=None, status=None): client = Zenpy( subdomain=settings.ZENDESK_SUBDOMAIN, email=settings.ZENDESK_EMAIL, token=settings.ZENDESK_TOKEN, ) ticket = client.tickets(id=ticket_id) if comment: ticket.comment = Comment(body=comment, public=False) if status: ticket.status = status client.tickets.update(ticket) return ticket
def display_ticket(number: str, client: Zenpy) -> None: """ Displays detailed view of a single ticket :param number: the ID number of the ticket to display :param client: the Zenpy client connection object """ try: ticket = client.tickets(id=number) except ZenpyException: print("Error accessing ticket with that ID. Are you sure it exists?") return print( f"{'*' * 80}\n" f"{pad('Ticket ID: ', 15)}{pad(str(ticket.id), 65)}\n" f"{pad('Subject: ', 15)}{pad(ticket.subject, 65)}\n" f"{pad('Description: ', 15)}{pad(sanitise_description(ticket.description), 65)}\n" f"{pad('Created at: ', 15)}{pad(ticket.created_at, 65)}\n" f"{pad('Submitted by: ', 15)}{pad(ticket.submitter.name, 65)}\n" f"{pad('Assigned to: ', 15)}{pad(ticket.assignee.name, 65)}\n" f"{pad('Status: ', 15)}{pad(ticket.status.capitalize(), 65)}\n" f"{'*' * 80}\n")
def zenpy_update_ticket(output, text_detected_body, images_detected_body): credentials = { 'email': os.environ['ZENDESK_EMAIL'], 'token': os.environ['ZENDESK_TOKEN'], 'subdomain': os.environ['ZENDESK_SUBDOMAIN'] } zenpy_client = Zenpy(**credentials) ticket = zenpy_client.tickets(id=output['attachment_data']['ticket_id']) ticket.comment = Comment( body=text_detected_body, html_body= '<h4>Attachment processed by Amazon Textract & Amazon Rekognition</h4><p>{}<p><p>{}</p>' .format(images_detected_body, text_detected_body), public=False) zenpy_client.tickets.update(ticket) return
def resend_notifications(): start = datetime.datetime.strptime('2018-03-29 00:00:00', '%Y-%m-%d %H:%M:%S') zenpy_client = Zenpy(**settings.ZENDESK) presc = MedicalPrescription.objects.filter( status=MedicalPrescription.PATIENT_REQUESTED, created__gte=start) for p in presc: try: print(p) ticket = zenpy_client.tickets(id=p.zendeskticket.external_id) if ticket.status == 'solved': p.status = [ i.get('value') for i in ticket.custom_fields if i.get('id') == 114103586991 ][0].upper() if p.status == 'PATIENT_REQUESTED': p.status = MedicalPrescription.UNREADABLE_PICTURES p.save() print('{}:{} - {}'.format(p.patient.user.email, p.patient.full_name, p.status)) except Exception: print(p)
class ZendeskAction(Action): def __init__(self, config): super(ZendeskAction, self).__init__(config=config) self.email = self.config['email'] self.token = self.config['api_token'] self.subdomain = self.config['subdomain'] self.credentials = { 'email': self.email, 'token': self.token, 'subdomain': self.subdomain } self.api = Zenpy(**self.credentials) def clean_response(self, text): return text.replace('\n', ' ').replace(' ', ' ').strip() def url_for_ticket(self, ticket): return 'https://{}.zendesk.com/agent/tickets/{}'.format( self.subdomain, ticket) def api_search(self, query, search_type): return self.api.search(query, type=search_type, sort_by='created_at', sort_order='desc') def create_ticket(self, subject, description): ticket = Ticket(subject=subject, description=description) try: created_ticket_audit = self.api.tickets.create(ticket) return { 'ticket_id': created_ticket_audit.ticket.id, 'ticket_url': self.url_for_ticket(created_ticket_audit.ticket.id), 'subject': self.clean_response(subject), 'description': self.clean_response(description) } except APIException: return { 'error': 'Could not create ticket with provided parameters' } except Exception as e: self.logger.error(e) return {'error': 'Could not make API request'} def search_tickets(self, query, search_type='ticket', limit=10): try: query_results = self.api_search(query, search_type) results_clean = map( lambda t: { 'ticket_id': t.id, 'ticket_url': self.url_for_ticket(t.id), 'ticket_status': t.status, 'subject': self.clean_response(t.subject), 'description': self.clean_response(t.description) }, list(query_results)[:limit]) return {'search_results': results_clean} except APIException: return { 'error': 'Could not execute search for query: {}'.format(query) } except Exception as e: self.logger.error(e) return {'error': 'There was an error executing your search'} def update_ticket(self, ticket_id, comment_text, public): try: ticket = self.api.tickets(id=ticket_id) ticket.comment = Comment(body=comment_text, public=public) self.api.tickets.update(ticket) return { 'ticket_id': ticket_id, 'ticket_url': self.url_for_ticket(ticket_id), 'body': self.clean_response(comment_text), 'public': public } except RecordNotFoundException: return {'error': 'Could not find ticket #{}'.format(ticket_id)} except Exception as e: self.logger.error(e) return {'error': 'Could not update ticket'} def update_ticket_status(self, ticket_id, status): valid_statuses = ['new', 'open', 'pending', 'solved', 'closed'] if status in valid_statuses: try: ticket = self.api.tickets(id=ticket_id) ticket.status = status self.api.tickets.update(ticket) return { 'ticket_id': ticket_id, 'ticket_url': self.url_for_ticket(ticket_id), 'status': status } except RecordNotFoundException: return {'error': 'Could not find ticket #{}'.format(ticket_id)} except Exception as e: self.logger.error(e) return {'error': 'Could not update ticket status'} else: return {'error': 'Invalid status given for ticket'}
class ZengoService(object): """Encapsulate behaviour allowing easy customisation.""" def __init__(self, *args, **kwargs): creds = { "email": settings.ZENDESK_EMAIL, "token": settings.ZENDESK_TOKEN, "subdomain": settings.ZENDESK_SUBDOMAIN, } self.client = Zenpy(**creds) # extraction of data from local users for injection into Zendesk def get_local_user_name(self, local_user): return local_user.first_name def get_local_user_external_id(self, local_user): return local_user.id def get_local_user_profile_image(self, local_user): return None def get_local_user_for_external_id(self, external_id): return get_user_model().objects.filter(id=external_id).first() def get_remote_zd_user_for_local_user(self, local_user): """ Attempt to resolve the provided user to an extant Zendesk User. Returns a Zendesk API User instance, and a boolean that indicates how definite the resolution is, based on how they've been found. """ result = self.client.search( type="user", external_id=self.get_local_user_external_id(local_user) ) if result.count: # strong match by external_id return result.next(), True # else check by associated emails, if allauth is installed and being used if "allauth.account" in settings.INSTALLED_APPS: emails = local_user.emailaddress_set.all() for e in emails: result = self.client.search(type="user", email=e.email) if result.count: # match strength based on email verification state return result.next(), e.verified # check for a weak match match using the email field on user instance if local_user.email: result = self.client.search(type="user", email=local_user.email) if result.count: return result.next(), False # no match at all, buh-bow return None, False def create_remote_zd_user_for_local_user(self, local_user): """Create a remote zendesk user based on the given local user's details.""" try: remote_zd_user = self.client.users.create( RemoteZendeskUser( name=self.get_local_user_name(local_user), external_id=self.get_local_user_external_id(local_user), email=local_user.email, remote_photo_url=self.get_local_user_profile_image(local_user), ) ) except APIException as a: # if this is a duplicate error try one last time to get the user print(a.response.json()) details = a.response.json()["details"] if any([d[0]["error"] == "DuplicateValue" for d in details.values()]): ( remote_zd_user, is_definite_match, ) = self.get_remote_zd_user_for_local_user(local_user) else: raise return remote_zd_user def get_or_create_remote_zd_user_for_local_user(self, local_user): user, is_definite_match = self.get_remote_zd_user_for_local_user(local_user) if user: return user, is_definite_match # we create a remote user in Zendesk for this local user return self.create_remote_zd_user_for_local_user(local_user), True def get_special_zendesk_user(self): """ Return a ZendeskUser instance representing the special Zendesk user that automations can add use to add comments. """ instance, created = models.ZendeskUser.objects.get_or_create( zendesk_id=-1, defaults=dict( name="Zendesk", active=True, role=models.ZendeskUser.roles.admin, created_at=timezone.now(), ), ) return instance def update_remote_zd_user_for_local_user(self, local_user, remote_zd_user): """ Compare the User and ZendeskUser instances and determine whether we need to update the data in Zendesk. This method is suitable to be called when a local user changes their email address or other user data. """ changed = False email_changed = False if self.get_local_user_name(local_user) != remote_zd_user.name: remote_zd_user.name = self.get_local_user_name(local_user) changed = True if self.get_local_user_external_id(local_user) != remote_zd_user.external_id: remote_zd_user.external_id = self.get_local_user_external_id(local_user) changed = True if local_user.email and local_user.email != remote_zd_user.email: remote_zd_user.email = local_user.email changed = True email_changed = True if changed: remote_zd_user = self.client.users.update(remote_zd_user) if email_changed: # then in addition to the above, we have to mark the newly # created identity as verified and promote it to be primary results = self.client.users.identities(id=remote_zd_user.id) for identity in results: if identity.value == local_user.email: self.client.users.identities.make_primary( user=remote_zd_user, identity=identity ) break def update_or_create_remote_zd_user(self, local_user): remote_zd_user, is_definite_match = self.get_remote_zd_user_for_local_user( local_user ) if remote_zd_user and is_definite_match: # check if we need to do any updates self.update_remote_zd_user_for_local_user(local_user, remote_zd_user) else: remote_zd_user = self.create_remote_zd_user_for_local_user(local_user) return remote_zd_user def sync_user(self, remote_zd_user): """ Given a RemoteZendeskUser instance, persist it as a local ZendeskUser instance. """ instance, created = models.ZendeskUser.objects.update_or_create( zendesk_id=remote_zd_user.id, defaults=dict( # attempt to resolve the local user if possible user=self.get_local_user_for_external_id(remote_zd_user.external_id), alias=remote_zd_user.alias, email=remote_zd_user.email, created_at=remote_zd_user.created_at, name=remote_zd_user.name, active=remote_zd_user.active, role=remote_zd_user.role, # store their latest photo JSON data photos_json=json.dumps(remote_zd_user.photo), ), ) return instance def sync_ticket_id(self, ticket_id): return self.sync_ticket(self.client.tickets(id=ticket_id)) def sync_ticket(self, remote_zd_ticket): """ Create or update local representations of a Zendesk ticket, its comments and all associated Zendesk users. This uses `update_or_create` to avoid integrity errors, demanding that comments and users be sync'd in a consistent order to avoid deadlock. Todo: only pull comments beyond those we've already got in the database """ kwargs = dict(include_inline_images=True) remote_comments = [ c for c in self.client.tickets.comments(remote_zd_ticket.id, **kwargs) ] remote_comments.sort(key=lambda c: (c.created_at, c.id)) # establish a distinct, ordered list of Zendesk users users = set( [remote_zd_ticket.requester] + [c.author for c in remote_comments if c.author_id != -1] # noqa ) users = list(users) users.sort(key=lambda u: u.id) # sync the users and establish a mapping to local records user_map = {u: self.sync_user(u) for u in users} defaults = dict( requester=user_map[remote_zd_ticket.requester], subject=remote_zd_ticket.subject, url=remote_zd_ticket.url, status=models.Ticket.states.by_id.get(remote_zd_ticket.status.lower()), custom_fields=json.dumps(remote_zd_ticket.custom_fields), tags=json.dumps(remote_zd_ticket.tags), created_at=remote_zd_ticket.created_at, updated_at=remote_zd_ticket.updated_at, ) # In some API responses we don't get a priority, but it could be an existing ticket with # priority already initialised so we don't want to overwrite the priority to the Ticket # object. if remote_zd_ticket.priority is not None: defaults["priority"] = models.Ticket.priorities.by_id.get( remote_zd_ticket.priority.lower() ) # update or create the ticket local_ticket, created = models.Ticket.objects.update_or_create( zendesk_id=remote_zd_ticket.id, defaults=defaults, ) # and now update or create the comments - baring in mind some might be type `VoiceComment` # https://developer.zendesk.com/rest_api/docs/support/ticket_audits#voice-comment-event for remote_comment in remote_comments: # if we know Zendesk created this comment as part of an automation or # merge, link it to the Zendesk user (skipping any zenpy/network hits) if remote_comment.author_id == -1: author = self.get_special_zendesk_user() else: author = user_map[remote_comment.author] local_comment, _created = models.Comment.objects.update_or_create( zendesk_id=remote_comment.id, ticket=local_ticket, defaults=dict( author=author, body=remote_comment.body, html_body=remote_comment.html_body, # VoiceComments have no `plain_body` content plain_body=getattr(remote_comment, "plain_body", None), public=remote_comment.public, created_at=remote_comment.created_at, ), ) for attachment in remote_comment.attachments: local_attachment, _created = models.Attachment.objects.update_or_create( zendesk_id=attachment.id, comment=local_comment, defaults=dict( file_name=attachment.file_name, content_url=attachment.content_url, content_type=attachment.content_type, size=attachment.size, width=attachment.width, height=attachment.height, inline=attachment.inline, ), ) for photo in attachment.thumbnails: local_photo, _created = models.Photo.objects.update_or_create( zendesk_id=photo.id, attachment=local_attachment, defaults=dict( file_name=photo.file_name, content_url=photo.content_url, content_type=photo.content_type, size=photo.size, width=photo.width, height=photo.height, ), ) return local_ticket, created
def view(ticketId): # Default zenpy_client = Zenpy(**creds) ticket = zenpy_client.tickets(id=ticketId) return render_template('view.html', ticket=ticket)