Skip to content

Commit 1b553e7

Browse files
committed
🚀 updated for python 3.8.6 and zenpy changes
1 parent 4f7fc6a commit 1b553e7

File tree

4 files changed

+93
-41
lines changed

4 files changed

+93
-41
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__pycache__/
2+
lza.p

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
# zendesk-discord-webhook-bot
22
Discord Webhook Bot for Zendesk
33

4+
## Usage
5+
6+
Tested with Python 3.8.2:
7+
```bash
8+
pip3 install -r requirements.txt
9+
10+
export ZDWB_DISCORD_WEBHOOK="https://discordapp.com/api/webhooks/...."
11+
export ZDWB_ZENDESK_EMAIL="richard.hendricks@piedpiper.com"
12+
export ZDWB_ZENDESK_TOKEN="abcdefghijklmnopqrstuvwxyz1234567890"
13+
export ZDWB_ZENDESK_SUBDOMAIN="piedpiper"
14+
15+
python3 bot.py
16+
```
17+
18+
`ZDWB_HISTORY_MINUTES` may also be set as an environment variable to signal the application on first run to collect the number of minutes you specify of historical data from Zendesk to post to the webhook channel.
19+
20+
## Screenshots
21+
422
![](https://i.cwlf.uk/evKB)
523

624
![](https://i.cwlf.uk/AZPl)

bot.py

Lines changed: 72 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,70 @@
11
import sys
22
import os
3-
import time, datetime
3+
import time, datetime, pytz
44
import urllib, hashlib
55
import pickle
66
import traceback
7+
import logging
78
from dateutil import parser
89
from zenpy import Zenpy
910
sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir))
1011
from discordWebhooks import Webhook, Attachment, Field
1112

12-
# CONFIGS
13+
logging.basicConfig()
14+
logger = logging.getLogger('ZDWB')
15+
logger.setLevel(logging.INFO)
1316

14-
url = "<discord webhook url>"
17+
if os.environ['ZDWB_HISTORY_MINUTES']:
18+
history_minutes = int(os.environ['ZDWB_HISTORY_MINUTES'])
19+
else:
20+
history_minutes = 0
21+
22+
url = os.environ['ZDWB_DISCORD_WEBHOOK']
1523

1624
creds = {
17-
'email' : 'user@example.org',
18-
'token' : '<zendesk token>',
19-
'subdomain' : '<zendesk subdomain>'
25+
'email' : os.environ['ZDWB_ZENDESK_EMAIL'],
26+
'token' : os.environ['ZDWB_ZENDESK_TOKEN'],
27+
'subdomain' : os.environ['ZDWB_ZENDESK_SUBDOMAIN']
2028
}
2129

22-
# END CONFIGS
23-
2430
status_color = {
2531
'new' : '#F5CA00',
2632
'open' : '#E82A2A',
2733
'pending' : '#59BBE0',
28-
'hold' : '#000',
34+
'hold' : '#000000',
2935
'solved' : '#828282',
30-
'closed' : '#ddd'
36+
'closed' : '#DDDDDD'
3137
}
3238

39+
default_icon = "https://d1eipm3vz40hy0.cloudfront.net/images/logos/favicons/favicon.ico"
40+
3341
zenpy = Zenpy(**creds)
3442

3543
tickets = {}
3644

45+
# Check if we know the last timestamp Zendesk was audited from
3746
if os.path.isfile('lza.p') is True: # lza = Last Zendesk Audit
3847
lza = pickle.load(open('lza.p','rb'))
48+
first_run = False
3949
else:
50+
lza = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC)
4051
first_run = True
41-
lza = datetime.datetime.utcnow()
4252

43-
print(lza)
53+
logger.info('Last Zendesk Audit: {}'.format(lza))
4454

4555
pickle.dump(lza,open('lza.p','wb'))
4656

4757
def get_gravatar(email):
48-
default = "https://{}.zendesk.com/images/favicon_2.ico".format(creds['subdomain'])
49-
avatar = "https://www.gravatar.com/avatar/" + hashlib.md5(email.encode("utf8").lower()).hexdigest() + "?"
50-
avatar += urllib.parse.urlencode({'d':default})
58+
# This will display the default Gravatar icon if the user has no Gravatar
59+
avatar = "https://www.gravatar.com/avatar/" + hashlib.md5(email.encode("utf8").lower()).hexdigest()
5160
return avatar
5261

5362
def post_webhook(event):
5463
try:
5564
ticket = zenpy.tickets(id=event.ticket_id)
5665
requester = zenpy.users(id=ticket.requester_id)
66+
67+
# Updater ID 0 is generally for Zendesk automation/non-user actions
5768
if event.updater_id > 0:
5869
updater = zenpy.users(id=event.updater_id)
5970
updater_name = updater.name
@@ -62,44 +73,58 @@ def post_webhook(event):
6273
updater_name = "Zendesk System"
6374
updater_email = "support@zendesk.com"
6475

76+
# If the user has no Zendesk profile photo, use Gravatar
6577
if requester.photo is not None:
66-
avatar = requester.photo.content_url
78+
avatar = requester.photo['content_url']
6779
else:
6880
avatar = get_gravatar(requester.email)
6981

82+
# Initialize an empty Discord Webhook object with the specified Webhook URL
7083
wh = Webhook(url, "", "", "")
7184

85+
# Prepare the base ticket info embed (attachment)
7286
at = Attachment(
7387
author_name = '{} ({})'.format(requester.name,requester.email),
7488
author_icon = avatar,
7589
color = status_color[ticket.status],
7690
title = '[Ticket #{}] {}'.format(ticket.id,ticket.raw_subject),
7791
title_link = "https://{}.zendesk.com/agent/#/tickets/{}".format(creds['subdomain'],ticket.id),
7892
footer = ticket.status.title(),
79-
ts = int(parser.parse(ticket.created_at).strftime('%s')))
93+
ts = int(parser.parse(ticket.created_at).strftime('%s'))) # TODO: always UTC, config timezone
8094

81-
for child in event._child_events:
95+
# If this is a new ticket, post it, ignore the rest.
96+
# This will only handle the first 'Create' child event
97+
# I have yet to see any more than one child event for new tickets
98+
for child in event.child_events:
8299
if child['event_type'] == 'Create':
83100
if first_run is True:
84101
wh = Webhook(url, "", "", "")
85102
else:
86103
wh = Webhook(url, "@here, New Ticket!", "", "")
104+
105+
87106
description = ticket.description
107+
108+
# Strip any double newlines from the description
88109
while "\n\n" in description:
89110
description = description.replace("\n\n", "\n")
111+
90112
field = Field("Description", ticket.description, False)
91113
at.addField(field)
114+
92115
wh.addAttachment(at)
93116
wh.post()
117+
94118
return
95119

96120
wh.addAttachment(at)
97121

122+
# Updater ID 0 is either Zendesk automation or non-user actions
98123
if int(event.updater_id) < 0:
99124
at = Attachment(
100125
color = status_color[ticket.status],
101126
footer = "Zendesk System",
102-
footer_icon = "https://{}.zendesk.com/images/favicon_2.ico".format(creds['subdomain']),
127+
footer_icon = default_icon,
103128
ts = int(parser.parse(event.created_at).strftime('%s')))
104129
else:
105130
at = Attachment(
@@ -108,15 +133,18 @@ def post_webhook(event):
108133
footer_icon = get_gravatar(updater_email),
109134
ts = int(parser.parse(event.created_at).strftime('%s')))
110135

111-
for child in event._child_events:
136+
for child in event.child_events:
112137
if child['event_type'] == 'Comment':
113-
for comment in zenpy.tickets.comments(ticket.id).values:
114-
if comment['id'] == child['id']:
115-
comment_body = comment['plain_body']
138+
for comment in zenpy.tickets.comments(ticket.id):
139+
if comment.id == child['id']:
140+
comment_body = comment.body
141+
116142
while "\n\n" in comment_body:
117143
comment_body = comment_body.replace("\n\n","\n")
118-
field = Field("New Comment", comment['plain_body'], False)
144+
145+
field = Field("Comment", comment_body, False)
119146
at.addField(field)
147+
120148
elif child['event_type'] == 'Change':
121149
if 'status' not in child.keys():
122150
if 'tags' in child.keys():
@@ -139,43 +167,46 @@ def post_webhook(event):
139167
field=Field("Type Change", '`{}`'.format(child['type']), True)
140168
at.addField(field)
141169
else:
142-
print(child)
170+
logger.debug(child)
143171
else:
144-
field = Field("Status Change", "{} from {}".format(child['status'].title(),child['previous_value'].title()), True)
172+
field = Field("Status Change", "{} to {}".format(child['previous_value'].title(), child['status'].title()), True)
145173
at.addField(field)
146174
else:
147-
print("Event not handled")
175+
logger.error("Event not handled")
148176

149177
wh.addAttachment(at)
178+
150179
i = 0
151180
while i < 4:
181+
logger.debug('Posting to Discord')
152182
r = wh.post()
153183
i += 1
154-
if r.text is not 'ok':
155-
if r.headers['X-RateLimit-Remaining'] == 0:
156-
now = int(time.time())
157-
then = int(r.headers['X-RateLimit-Reset'])
158-
ttw = then - now # ttw = Time To Wait
159-
if ttw > 0:
160-
print("Hit Rate Limit, sleeping for {}".format(str(ttw)))
161-
time.sleep(ttw)
162-
else:
163-
break
184+
if r.text != 'ok':
185+
logger.error(r)
186+
logger.info('Discord webhook retry {}/3'.format(i))
187+
else:
188+
break
164189
time.sleep(1)
165190

166191
except Exception as e:
167192
if "RecordNotFound" in str(e):
168193
pass
169194
else:
170-
traceback.print_exc()
195+
logger.error(traceback.print_exc())
171196

172197
if first_run is True:
173-
for event in zenpy.tickets.events("1970-01-01T00:00:00Z"):
198+
today = datetime.datetime.utcnow() - datetime.timedelta(minutes=history_minutes)
199+
for event in zenpy.tickets.events(today.replace(tzinfo=pytz.UTC)):
200+
logger.debug('Incoming Zendesk Event')
201+
logger.debug(event.event_type)
202+
for child in event.child_events:
203+
logger.debug('Child Event')
204+
logger.debug(child['event_type'])
174205
post_webhook(event)
175206

176207
while True:
177208
for event in zenpy.tickets.events(lza):
178209
post_webhook(event)
179-
lza = datetime.datetime.utcnow()
210+
lza = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC)
180211
pickle.dump(lza,open('lza.p','wb'))
181-
time.sleep(5)
212+
time.sleep(15)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
zenpy==2.0.19

0 commit comments

Comments
 (0)