313 lines
12 KiB
Python
313 lines
12 KiB
Python
#!/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 = '''
|
|
=====================<br>
|
|
Conversation History<br>
|
|
=====================<br>
|
|
'''
|
|
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'<b>Private Note from {username}:</b><br>{convo["body"]}<br>---------------------------<br>'
|
|
elif convo['private'] == False and convo['incoming'] == True:
|
|
convos += f'<b>Incoming Reply from {username}:</b><br>{convo["body"]}<br>---------------------------<br>'
|
|
elif convo['private'] == False and convo['incoming'] == False:
|
|
convos += f'<b>Outgoing Reply from {username}:</b><br>{convo["body"]}<br>---------------------------<br>'
|
|
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']}<br>
|
|
Ticket ID: {ticket['new_ticket']}<br>
|
|
'''
|
|
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) |