Wajahat698 commited on
Commit
4d6bc19
Β·
verified Β·
1 Parent(s): 4620064

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -125
app.py CHANGED
@@ -8,12 +8,12 @@ logging.basicConfig(level=logging.INFO)
8
  logger = logging.getLogger(__name__)
9
 
10
  # ── Credentials ────────────────────────────────────────────────────────────────
11
- SLACK_TOKEN = "xoxb-7025747032292-10683671864694-lhUNPKF1KSpYwRvrvZeP7CPy"
12
- CW_COMPANY_ID = "Intrinsic"
13
- CW_PUBLIC_KEY = "IkrljDywPCSE4s10"
14
- CW_PRIVATE_KEY = "kOwo89oUO6SVVSYi"
15
- CW_CLIENT_ID = "e45cf35f-24a8-4f5f-b47a-19376cafce64"
16
- BASE_URL = "https://api-na.myconnectwise.net/v4_6_release/apis/3.0"
17
 
18
  auth = f"{CW_COMPANY_ID}+{CW_PUBLIC_KEY}:{CW_PRIVATE_KEY}"
19
  encoded_auth = base64.b64encode(auth.encode()).decode()
@@ -24,55 +24,68 @@ CW_HEADERS = {
24
  "Content-Type": "application/json",
25
  }
26
 
27
- # ── Fallback name lists (tried in order) ───────────────────────────────────────
28
- BOARD_FALLBACKS = ["Service Desk", "Help Desk", "Support", "Technical Support",
29
- "IT Support", "Service Board", "Default", "General"]
30
- COMPANY_FALLBACKS = ["Intrinsic", "intrinsic", "INTRINSIC", "IntrinsicIT",
31
- "intrinsicit", "Intrinsic IT"]
32
- PRIORITY_FALLBACKS = ["Priority 1", "P1", "High", "Critical", "Urgent",
33
- "Medium", "Normal", "Low", "Priority 2", "Priority 3"]
34
 
35
 
36
  # ── Discovery helpers ──────────────────────────────────────────────────────────
37
 
38
- def discover_boards():
39
- """Return list of active service board names from ConnectWise."""
40
  try:
41
  r = requests.get(
42
  f"{BASE_URL}/service/boards",
43
  headers=CW_HEADERS,
44
- params={"conditions": "inactive=false", "pageSize": 50},
45
  timeout=10,
46
  )
 
47
  if r.status_code == 200:
48
- names = [b.get("name") for b in r.json() if b.get("name")]
49
- logger.info("Discovered boards: %s", names)
50
- return names
51
  except Exception as e:
52
- logger.warning("Board discovery failed: %s", e)
53
  return []
54
 
55
 
56
- def discover_companies():
57
- """Return list of company identifiers from ConnectWise."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  try:
59
  r = requests.get(
60
  f"{BASE_URL}/company/companies",
61
  headers=CW_HEADERS,
62
- params={"pageSize": 50},
63
  timeout=10,
64
  )
65
- if r.status_code == 200:
66
- ids = [c.get("identifier") for c in r.json() if c.get("identifier")]
67
- logger.info("Discovered companies: %s", ids)
68
- return ids
69
  except Exception as e:
70
- logger.warning("Company discovery failed: %s", e)
71
- return []
72
 
73
 
74
- def discover_priorities():
75
- """Return list of priority names from ConnectWise."""
76
  try:
77
  r = requests.get(
78
  f"{BASE_URL}/service/priorities",
@@ -80,106 +93,86 @@ def discover_priorities():
80
  params={"pageSize": 50},
81
  timeout=10,
82
  )
83
- if r.status_code == 200:
84
- names = [p.get("name") for p in r.json() if p.get("name")]
85
- logger.info("Discovered priorities: %s", names)
86
- return names
87
  except Exception as e:
88
- logger.warning("Priority discovery failed: %s", e)
89
- return []
90
 
91
 
92
- def best_match(discovered, fallbacks):
93
- """
94
- Return the first discovered value that appears in fallbacks (case-insensitive),
95
- then the first discovered value, then the first fallback.
96
- """
97
- lower_disc = {d.lower(): d for d in discovered}
98
- for f in fallbacks:
99
- if f.lower() in lower_disc:
100
- return lower_disc[f.lower()]
101
- if discovered:
102
- return discovered[0]
103
- return fallbacks[0] # last resort: just try the first hardcoded name
104
-
105
 
106
- # ── Ticket creation with progressive fallbacks ─────────────────────────────────
107
-
108
- def try_create(payload):
109
  r = requests.post(
110
  f"{BASE_URL}/service/tickets",
111
  headers=CW_HEADERS,
112
  json=payload,
113
  timeout=10,
114
  )
115
- logger.info("CW status=%s body=%s", r.status_code, r.text[:300])
116
  return r
117
 
118
 
119
  def create_ticket(issue):
120
- # ── Step 1: discover real values ─────────────────────────────────────────
121
- boards = discover_boards()
122
- companies = discover_companies()
123
- priorities = discover_priorities()
124
-
125
- board_name = best_match(boards, BOARD_FALLBACKS)
126
- company_id = best_match(companies, COMPANY_FALLBACKS)
127
- priority_name = best_match(priorities, PRIORITY_FALLBACKS)
128
-
129
- logger.info("Using board=%s company=%s priority=%s",
130
- board_name, company_id, priority_name)
131
-
132
- # ── Step 2: full payload ──────────────────────────────────────────────────
133
- full_payload = {
134
- "summary": issue[:100],
135
- "initialDescription": issue,
136
- "board": {"name": board_name},
137
- "company": {"identifier": company_id},
138
- "priority": {"name": priority_name},
139
- }
140
- r = try_create(full_payload)
141
- if r.status_code in (200, 201):
142
- return r.json().get("id"), board_name
143
-
144
- # ── Step 3: try every discovered board one-by-one ────────────────────────
145
- for b in boards:
146
- if b == board_name:
147
- continue
148
- payload = {**full_payload, "board": {"name": b}}
149
- r = try_create(payload)
 
150
  if r.status_code in (200, 201):
151
- return r.json().get("id"), b
152
-
153
- # ── Step 4: drop priority (might be wrong) ────────────────────────────────
154
- logger.warning("Retrying without priority field")
155
- no_priority = {k: v for k, v in full_payload.items() if k != "priority"}
156
- r = try_create(no_priority)
157
- if r.status_code in (200, 201):
158
- return r.json().get("id"), board_name
159
-
160
- # ── Step 5: drop company too ─────────────────────────��────────────────────
161
- logger.warning("Retrying without company or priority")
162
- minimal_board = {
163
- "summary": issue[:100],
164
- "initialDescription": issue,
165
- "board": {"name": board_name},
166
- }
167
- r = try_create(minimal_board)
168
- if r.status_code in (200, 201):
169
- return r.json().get("id"), board_name
170
-
171
- # ── Step 6: bare minimum β€” just summary ───────────────────────────────────
172
- logger.warning("Retrying with bare minimum payload")
173
- bare = {"summary": issue[:100], "initialDescription": issue}
174
- r = try_create(bare)
175
  if r.status_code in (200, 201):
176
  return r.json().get("id"), "default"
177
 
178
- logger.error("All ticket creation attempts failed")
179
  return None, None
180
 
181
 
182
- # ── Slack helpers ──────────────────────────────────────────────────────────────
183
 
184
  def send_slack(channel, text):
185
  requests.post(
@@ -200,23 +193,41 @@ def home():
200
  return "Slack β†’ ConnectWise Bot Running βœ…"
201
 
202
 
203
- @app.route("/debug/cw", methods=["GET"])
204
  def debug_cw():
205
  """
206
- Hit /debug/cw in your browser to see exactly what boards / companies /
207
- priorities exist in your ConnectWise instance.
208
  """
209
- return jsonify({
210
- "boards": discover_boards(),
211
- "companies": discover_companies(),
212
- "priorities": discover_priorities(),
213
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
 
216
  @app.route("/slack/events", methods=["POST"])
217
  def slack_events():
218
  data = request.get_json()
219
- logger.info("Slack payload type=%s", data.get("type"))
220
 
221
  # URL verification handshake
222
  if data.get("type") == "url_verification":
@@ -224,7 +235,7 @@ def slack_events():
224
 
225
  event = data.get("event", {})
226
 
227
- # Ignore bot messages to prevent loops
228
  if event.get("bot_id") or event.get("subtype") == "bot_message":
229
  return "", 200
230
 
@@ -232,7 +243,6 @@ def slack_events():
232
  text = event.get("text", "")
233
  channel = event.get("channel")
234
 
235
- # Strip bot mention
236
  text = text.replace("<@U0AL3KRRELE>", "").strip()
237
  if not text:
238
  return "", 200
@@ -241,14 +251,14 @@ def slack_events():
241
 
242
  if ticket_id:
243
  reply = (
244
- f"βœ… Support ticket created!\n"
245
- f"🎫 Ticket ID: `{ticket_id}`\n"
246
  f"πŸ“‹ Board: {board_used}"
247
  )
248
  else:
249
  reply = (
250
- "⚠️ Could not create a ConnectWise ticket right now.\n"
251
- "Please contact support directly or try again shortly."
252
  )
253
 
254
  send_slack(channel, reply)
 
8
  logger = logging.getLogger(__name__)
9
 
10
  # ── Credentials ────────────────────────────────────────────────────────────────
11
+ SLACK_TOKEN = "xoxb-7025747032292-10683671864694-lhUNPKF1KSpYwRvrvZeP7CPy"
12
+ CW_COMPANY_ID = "Intrinsic"
13
+ CW_PUBLIC_KEY = "IkrljDywPCSE4s10"
14
+ CW_PRIVATE_KEY = "kOwo89oUO6SVVSYi"
15
+ CW_CLIENT_ID = "e45cf35f-24a8-4f5f-b47a-19376cafce64"
16
+ BASE_URL = "https://api-na.myconnectwise.net/v4_6_release/apis/3.0"
17
 
18
  auth = f"{CW_COMPANY_ID}+{CW_PUBLIC_KEY}:{CW_PRIVATE_KEY}"
19
  encoded_auth = base64.b64encode(auth.encode()).decode()
 
24
  "Content-Type": "application/json",
25
  }
26
 
27
+ COMPANY_NAME_VARIANTS = [
28
+ "Intrinsic", "intrinsic", "INTRINSIC", "IntrinsicIT", "Intrinsic IT"
29
+ ]
 
 
 
 
30
 
31
 
32
  # ── Discovery helpers ──────────────────────────────────────────────────────────
33
 
34
+ def get_all_boards():
35
+ """Return list of (id, name) for all service boards (active + inactive)."""
36
  try:
37
  r = requests.get(
38
  f"{BASE_URL}/service/boards",
39
  headers=CW_HEADERS,
40
+ params={"pageSize": 100},
41
  timeout=10,
42
  )
43
+ logger.info("Boards %s: %s", r.status_code, r.text[:600])
44
  if r.status_code == 200:
45
+ return [(b.get("id"), b.get("name")) for b in r.json() if b.get("id")]
 
 
46
  except Exception as e:
47
+ logger.warning("Board fetch error: %s", e)
48
  return []
49
 
50
 
51
+ def get_company_id():
52
+ """Try multiple name variants; fall back to first company in system."""
53
+ for variant in COMPANY_NAME_VARIANTS:
54
+ try:
55
+ r = requests.get(
56
+ f"{BASE_URL}/company/companies",
57
+ headers=CW_HEADERS,
58
+ params={
59
+ "conditions": f'name contains "{variant}"',
60
+ "pageSize": 10,
61
+ },
62
+ timeout=10,
63
+ )
64
+ logger.info("Company search '%s' β†’ %s: %s", variant, r.status_code, r.text[:300])
65
+ if r.status_code == 200 and r.json():
66
+ return r.json()[0].get("id")
67
+ except Exception as e:
68
+ logger.warning("Company search error '%s': %s", variant, e)
69
+
70
+ # Fall back to first company in the system
71
  try:
72
  r = requests.get(
73
  f"{BASE_URL}/company/companies",
74
  headers=CW_HEADERS,
75
+ params={"pageSize": 1},
76
  timeout=10,
77
  )
78
+ if r.status_code == 200 and r.json():
79
+ c = r.json()[0]
80
+ logger.warning("Using first company fallback: %s id=%s", c.get("name"), c.get("id"))
81
+ return c.get("id")
82
  except Exception as e:
83
+ logger.warning("Fallback company error: %s", e)
84
+ return None
85
 
86
 
87
+ def get_priority_id():
88
+ """Return numeric id of the first available priority."""
89
  try:
90
  r = requests.get(
91
  f"{BASE_URL}/service/priorities",
 
93
  params={"pageSize": 50},
94
  timeout=10,
95
  )
96
+ logger.info("Priorities %s: %s", r.status_code, r.text[:300])
97
+ if r.status_code == 200 and r.json():
98
+ return r.json()[0].get("id")
 
99
  except Exception as e:
100
+ logger.warning("Priority fetch error: %s", e)
101
+ return None
102
 
103
 
104
+ # ── Ticket creation with 4-level fallback ──────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
+ def post_ticket(payload):
 
 
107
  r = requests.post(
108
  f"{BASE_URL}/service/tickets",
109
  headers=CW_HEADERS,
110
  json=payload,
111
  timeout=10,
112
  )
113
+ logger.info("CW POST status=%s body=%s", r.status_code, r.text[:500])
114
  return r
115
 
116
 
117
  def create_ticket(issue):
118
+ summary = issue[:100]
119
+ boards = get_all_boards() # [(id, name), ...]
120
+ company_id = get_company_id() # numeric int
121
+ priority_id = get_priority_id() # numeric int
122
+
123
+ logger.info("Resolved β†’ boards=%s company_id=%s priority_id=%s",
124
+ boards, company_id, priority_id)
125
+
126
+ # ── Attempt 1: each board + company numeric ID ────────────────────────────
127
+ if company_id and boards:
128
+ for bid, bname in boards:
129
+ payload = {
130
+ "summary": summary,
131
+ "initialDescription": issue,
132
+ "board": {"id": bid},
133
+ "company": {"id": company_id},
134
+ }
135
+ if priority_id:
136
+ payload["priority"] = {"id": priority_id}
137
+ r = post_ticket(payload)
138
+ if r.status_code in (200, 201):
139
+ return r.json().get("id"), bname
140
+
141
+ # ── Attempt 2: company only (CW picks default board) ─────────────────────
142
+ if company_id:
143
+ payload = {
144
+ "summary": summary,
145
+ "initialDescription": issue,
146
+ "company": {"id": company_id},
147
+ }
148
+ r = post_ticket(payload)
149
  if r.status_code in (200, 201):
150
+ return r.json().get("id"), "default"
151
+
152
+ # ── Attempt 3: string identifier variants ─────────────────────────────────
153
+ for variant in COMPANY_NAME_VARIANTS:
154
+ for bid, bname in (boards or [(None, None)]):
155
+ payload = {
156
+ "summary": summary,
157
+ "initialDescription": issue,
158
+ "company": {"identifier": variant},
159
+ }
160
+ if bid:
161
+ payload["board"] = {"id": bid}
162
+ r = post_ticket(payload)
163
+ if r.status_code in (200, 201):
164
+ return r.json().get("id"), bname or "default"
165
+
166
+ # ── Attempt 4: absolute minimum (summary only) ────────────────────────────
167
+ r = post_ticket({"summary": summary, "initialDescription": issue})
 
 
 
 
 
 
168
  if r.status_code in (200, 201):
169
  return r.json().get("id"), "default"
170
 
171
+ logger.error("All ticket attempts failed")
172
  return None, None
173
 
174
 
175
+ # ── Slack helper ───────────────────────────────────────────────────────────────
176
 
177
  def send_slack(channel, text):
178
  requests.post(
 
193
  return "Slack β†’ ConnectWise Bot Running βœ…"
194
 
195
 
196
+ @app.route("/debug/cw")
197
  def debug_cw():
198
  """
199
+ Visit this URL in your browser to see the REAL board names, company IDs,
200
+ and priority IDs in your ConnectWise instance.
201
  """
202
+ out = {}
203
+
204
+ try:
205
+ r = requests.get(f"{BASE_URL}/service/boards", headers=CW_HEADERS,
206
+ params={"pageSize": 100}, timeout=10)
207
+ out["boards"] = r.json() if r.status_code == 200 else {"error": r.text}
208
+ except Exception as e:
209
+ out["boards"] = {"error": str(e)}
210
+
211
+ try:
212
+ r = requests.get(f"{BASE_URL}/company/companies", headers=CW_HEADERS,
213
+ params={"pageSize": 20}, timeout=10)
214
+ out["companies"] = r.json() if r.status_code == 200 else {"error": r.text}
215
+ except Exception as e:
216
+ out["companies"] = {"error": str(e)}
217
+
218
+ try:
219
+ r = requests.get(f"{BASE_URL}/service/priorities", headers=CW_HEADERS,
220
+ params={"pageSize": 20}, timeout=10)
221
+ out["priorities"] = r.json() if r.status_code == 200 else {"error": r.text}
222
+ except Exception as e:
223
+ out["priorities"] = {"error": str(e)}
224
+
225
+ return jsonify(out)
226
 
227
 
228
  @app.route("/slack/events", methods=["POST"])
229
  def slack_events():
230
  data = request.get_json()
 
231
 
232
  # URL verification handshake
233
  if data.get("type") == "url_verification":
 
235
 
236
  event = data.get("event", {})
237
 
238
+ # Block bot messages to prevent infinite loops
239
  if event.get("bot_id") or event.get("subtype") == "bot_message":
240
  return "", 200
241
 
 
243
  text = event.get("text", "")
244
  channel = event.get("channel")
245
 
 
246
  text = text.replace("<@U0AL3KRRELE>", "").strip()
247
  if not text:
248
  return "", 200
 
251
 
252
  if ticket_id:
253
  reply = (
254
+ f"βœ… Ticket created!\n"
255
+ f"🎫 ID: `{ticket_id}`\n"
256
  f"πŸ“‹ Board: {board_used}"
257
  )
258
  else:
259
  reply = (
260
+ "⚠️ Could not create a ConnectWise ticket.\n"
261
+ "Visit /debug/cw to see available boards & companies."
262
  )
263
 
264
  send_slack(channel, reply)