#!/usr/bin/env python3 ################################### # Library Inclusions ################################### import requests import json import os import time import sys import base64 import urllib.parse from datetime import datetime from dateutil.relativedelta import relativedelta import itertools import threading ################################### # Function Definitions ################################### # Makes API call to target using either GET or POST and returns the JSON response ## request.post() usage (url, headers = headers, data= payload, files= files) or (url, headers= headers, json= payload) def makeCall (target, verb, key, payload= None, files= None): if verb.lower() == 'get': try: response = requests.get(target, auth=(key, 'X')) if response.status_code != 429: response.raise_for_status() responseData = response.json() if (checkRate(response)): return makeCall(target, verb, key) return responseData except requests.exceptions.HTTPError as http_error: print(f'{RED}HTTP Error:{RESET}\n{http_error}') print(f'Reponse Content: {response.content}') raise except Exception as error: print(f'{RED}Error:{RESET}\n{error}') raise elif verb.lower() == 'post': try: if files == None: response = requests.post(target, auth=(key, 'X'), json= payload) else: response = requests.post(target, data=payload, auth=(key, 'X'), files= files) if response.status_code != 429: response.raise_for_status() responseData = response.json() if (checkRate(response)): if files == None: return makeCall(target, verb, key, payload) else: return makeCall(target, verb, key, payload, files) return responseData except requests.exceptions.HTTPError as http_error: print(f'{RED}HTTP Error:{RESET}\n{http_error}') print(f'Reponse Content: {response.content}') raise except Exception as error: print(f'{RED}Error:{RESET}\n{error}') raise if verb.lower() == 'put': try: response = requests.put(target, auth=(key, 'X'), json= payload) if response.status_code != 429: response.raise_for_status() responseData = response.json() if (checkRate(response)): return makeCall(target, verb, key, payload) return responseData except requests.exceptions.HTTPError as http_error: print(f'{RED}HTTP Error:{RESET}\n{http_error}') print(f'Reponse Content: {response.content}') raise except Exception as error: print(f'{RED}Error:{RESET}\n{error}') raise # Clears out the specified number of console lines def clear(number): for num in range(number): sys.stdout.write('\x1b[1A') sys.stdout.write('\x1b[2K') def checkRate(response): if 'Retry-After' in response.headers: timer = int(response.headers.get('Retry-After')) + 5 while timer > 0: print(f'{YELLOW}*RATE LIMITED*{RESET}...Pausing for {timer} seconds') time.sleep(1) timer -= 1 clear(1) return True else: return False def buildTicketArray(domain, groupId, key): retArray = [] page = 1 moreTickets = True dateFormat = "%Y-%m-%d" currentDate = datetime.utcnow() ticketBatch = 30 maxPages = 10 while moreTickets: page = 1 morePages = True endDate = currentDate.strftime(dateFormat) startDate = (currentDate - relativedelta(months=1)).strftime(dateFormat) query = f'"(group_id:{groupId}) AND (created_at:>\'{startDate}\' AND created_at:<\'{endDate}\')"' encodeQuery = urllib.parse.quote(query, safe=":<>() AND") while morePages and page <= maxPages: url = f'{domain}/api/v2/search/tickets?query={encodeQuery}&page={page}' response = makeCall(url, 'get', key) if 'results' in response and len(response['results']) > 0: retArray.extend(response['results']) else: morePages = False page += 1 if len(response['results']) < ticketBatch: moreTickets = False currentDate -= relativedelta(months=1) return retArray def spinner(): anim = itertools.cycle(['◑', '◒', '◐', '◓']) while not stopSpin.is_set(): sys.stdout.write(f'\r{message}{next(anim)}\n') sys.stdout.flush() time.sleep(0.1) clear(1) def start_spinner(msg): global stopSpin, message stopSpin.clear() message = msg spinner_thread = threading.Thread(target=spinner) spinner_thread.start() return spinner_thread def stop_spinner(spinner_thread): stopSpin.set() spinner_thread.join() ################################### # Globals Variables ################################### ticketArray = [] groupIdDefs_source = [] sourceDom = '' destDom = '' sourceKey = '' destKey = '' targetGroup = '' targetGroupId = '' RED = '\033[31m' GREEN = '\033[32m' YELLOW = '\033[33m' RESET = '\033[0m' stopSpin = threading.Event() spinnerThread = threading.Thread(target=spinner) ################################### # Main Loop ################################### if os.name == 'nt': os.system('cls') else: os.system('clear') print(' ______ ___ __ __ ') print(' / ____/________ _____ ___ / | / / ____ / /_ ') print(' / / / ___/ __ `/ __ `__ \______/ /| |______/ / / __ \/ __/ ') print('/ /___/ / / /_/ / / / / / /_____/ ___ /_____/ /___/ /_/ / /_ ') print('\____/_/_ \__,_/_/ /_/ /_/ /_/__|_| /_____/\____/\__/___ ') print(' /_ __(_)____/ /_____ / /_ /_ __/________ _____ _____/ __/__ _____') print(' / / / / ___/ //_/ _ \/ __/ / / / ___/ __ `/ __ \/ ___/ /_/ _ \/ ___/') print(' / / / / /__/ ,< / __/ /_ / / / / / /_/ / / / (__ ) __/ __/ / ') print('/_/ /_/\___/_/|_|\___/\__/ /_/ /_/ \__,_/_/ /_/____/_/ \___/_/ \n') print('=========================================================================') ## Getting transfer settings from user sourceDom = input('Please enter the source domain:\n(ex. "https://your_domain.freshdesk.com")\n> ') if sourceDom == '': print(f'{RED}Invalid Domain Provided{RESET}') sys.exit(1) else: clear(3) sourceDom = "".join(sourceDom.split()) print(f'SETTINGS:\nSource Domain: {GREEN}{sourceDom}{RESET}\n=========================================================================') sourceKey = input('Please provide the API Key for the source account:\n(Hint: You can find this from your profile settings menu in Freshdesk)\n> ') if sourceKey == '': print(f'{RED}Invalid Key Provided{RESET}') sys.exit(1) else: clear(3) sourceKey = "".join(sourceKey.split()) destDom = input('Please enter the destination domain:\n> ') if destDom == '': print(f'{RED}Invalid Domain Provided{RESET}') sys.exit(1) else: clear(4) destDom = "".join(destDom.split()) print(f'Source Domain: {GREEN}{sourceDom}{RESET}\nDestination Domain: {GREEN}{destDom}{RESET}\n=========================================================================') destKey = input('Please provide the API key for the destination account:\n> ') if destKey == '': print(f'{RED}Invalid Key Provided{RESET}') sys.exit(1) else: clear(2) destKey = "".join(destKey.split()) targetGroup = input('What is the name of the Freshdesk group you want to transfer from?\n> ') if targetGroup == '': print(f'{RED}Invalid Group Name Provided{RESET}') sys.exit(1) else: clear(3) print(f'Group: {GREEN}{targetGroup}{RESET}') print(f'=========================================================================') ## Building Definition Arrays spinner_thread = start_spinner('Obtaining account information...') groupIdDefs_source = makeCall(sourceDom + '/api/v2/groups', 'get', sourceKey) groupIdDefs_dest = makeCall(destDom + '/api/v2/groups', 'get', destKey) for group in groupIdDefs_source: if group["name"] == targetGroup: targetGroupId = group["id"] break stop_spinner(spinner_thread) print(f'Obtaining account information...{GREEN}COMPLETE{RESET}') ## Building Ticket Array from Source spinner_thread = start_spinner('Gathering list of tickets from source...') ticketArray = buildTicketArray(sourceDom, targetGroupId, sourceKey) stop_spinner(spinner_thread) print(f'Gathering list of tickets from source...{GREEN}COMPLETE{RESET}') ## Transferring tickets to Destination print(f'{YELLOW}*This may take a while*{RESET}') spinner_thread = start_spinner(f'Transferring tickets to destination account...') for ticket in ticketArray: attachments = {} ticketData = makeCall(sourceDom + f'/api/v2/tickets/{ticket["id"]}?include=conversations,requester', 'get', sourceKey) if 'attachments' in ticketData: for attachment in ticketData['attachments']: if attachment.get('attachment_url'): fileRes = requests.get(attachment['attachment_url']) if fileRes.status_code == 200: attachments['attachments[]'] = (attachment['name'], fileRes.content) else: print(f'{RED}File Error:{RESET}\nFailed to download {attachment["name"]}') payload = { 'name': ticketData["requester"]["name"], 'email': ticketData["requester"]["email"], 'phone': ticketData["requester"]["phone"], 'subject': ticketData["subject"], 'status': ticketData["status"], 'priority': ticketData["priority"], 'description': ticketData["description"], 'source': ticketData["source"], } newTicket = makeCall(destDom + '/api/v2/tickets', 'post', destKey, payload, attachments if attachments else None) ticket['new_ticket'] = newTicket['id'] ## Gaterhing conversation history from ticketData, and then updating the ticket to include them in a private note if len(ticketData['conversations']) > 0: convos = ''' =====================
Conversation History
=====================
''' for convo in ticketData['conversations']: if convo['incoming'] == False: agent = (makeCall(sourceDom + f'/api/v2/agents/{convo["user_id"]}', 'get', sourceKey)) username = agent["contact"]["name"] if convo['incoming'] == True: user = (makeCall(sourceDom + f'/api/v2/contacts/{convo["user_id"]}', 'get', sourceKey)) username = user["name"] if convo['private'] == True: convos += f'Private Note from {username}:
{convo["body"]}
---------------------------
' elif convo['private'] == False and convo['incoming'] == True: convos += f'Incoming Reply from {username}:
{convo["body"]}
---------------------------
' elif convo['private'] == False and convo['incoming'] == False: convos += f'Outgoing Reply from {username}:
{convo["body"]}
---------------------------
' makeCall(destDom + f'/api/v2/tickets/{newTicket["id"]}/notes', 'post', destKey,{'body': convos}) stop_spinner(spinner_thread) clear(1) print(f'Transferring tickets to destination account...{GREEN}COMPLETE{RESET}') /* spinner_thread = start_spinner('Closing transferred tickets on source account...') for ticket in ticketArray: noteBody = f''' Ticket has been transferred to {destDom}/a/tickets/{ticket['new_ticket']}
Ticket ID: {ticket['new_ticket']}
''' makeCall(sourceDom + f'/api/v2/tickets/{ticket["id"]}/notes', 'post', sourceKey, {'body': noteBody}) makeCall(sourceDom + f'/api/v2/tickets/{ticket["id"]}', 'put', sourceKey, {'status': 5}) stop_spinner(spinner_thread) print(f'Closing transferred tickets on source account...{GREEN}COMPLETE{RESET}') */ print(f'{GREEN}*TICKET TRANSFER COMPLETED SUCCESSFULLY*{RESET}') for i in range(5, 0, -1): print(f'Exiting in {i} seconds...') time.sleep(1) clear(1) sys.exit(0)