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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +143 -149
app.py CHANGED
@@ -2,19 +2,30 @@ from flask import Flask, request, jsonify
2
  import requests
3
  import base64
4
  import logging
 
5
 
6
  app = Flask(__name__)
7
  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()
20
 
@@ -24,84 +35,54 @@ CW_HEADERS = {
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",
92
- headers=CW_HEADERS,
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(
@@ -110,77 +91,93 @@ def post_ticket(payload):
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(
179
  "https://slack.com/api/chat.postMessage",
180
- headers={
181
- "Authorization": f"Bearer {SLACK_TOKEN}",
182
- "Content-Type": "application/json",
183
- },
184
  json={"channel": channel, "text": text},
185
  timeout=10,
186
  )
@@ -196,31 +193,33 @@ def home():
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
 
@@ -229,36 +228,31 @@ def debug_cw():
229
  def slack_events():
230
  data = request.get_json()
231
 
232
- # URL verification handshake
233
  if data.get("type") == "url_verification":
234
  return jsonify({"challenge": data.get("challenge")})
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
 
242
  if event.get("type") == "message":
243
  text = event.get("text", "")
244
  channel = event.get("channel")
 
245
 
246
- text = text.replace("<@U0AL3KRRELE>", "").strip()
247
  if not text:
248
  return "", 200
249
 
250
  ticket_id, board_used = create_ticket(text)
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)
 
2
  import requests
3
  import base64
4
  import logging
5
+ import os
6
 
7
  app = Flask(__name__)
8
  logging.basicConfig(level=logging.INFO)
9
  logger = logging.getLogger(__name__)
10
 
11
  # ── Credentials ────────────────────────────────────────────────────────────────
12
+ SLACK_TOKEN = os.environ.get("SLACK_TOKEN", "xoxb-7025747032292-10683671864694-lhUNPKF1KSpYwRvrvZeP7CPy")
13
+ CW_COMPANY_ID = os.environ.get("CW_COMPANY_ID", "Intrinsic")
14
+ CW_PUBLIC_KEY = os.environ.get("CW_PUBLIC_KEY", "IkrljDywPCSE4s10")
15
+ CW_PRIVATE_KEY = os.environ.get("CW_PRIVATE_KEY", "kOwo89oUO6SVVSYi")
16
+ CW_CLIENT_ID = os.environ.get("CW_CLIENT_ID", "e45cf35f-24a8-4f5f-b47a-19376cafce64")
17
  BASE_URL = "https://api-na.myconnectwise.net/v4_6_release/apis/3.0"
18
 
19
+ # ── HARDCODED FALLBACKS ────────────────────────────────────────────────────────
20
+ # Set these as Hugging Face Secrets once you find the real values in ConnectWise.
21
+ # Log into ConnectWise manually and check:
22
+ # CW_BOARD_ID β†’ Service Desk β†’ Boards β†’ open your board β†’ ID is in the URL
23
+ # CW_COMPANY_NUM_ID→ Companies → your company → ID in URL
24
+ # CW_PRIORITY_ID β†’ Service Desk β†’ Priority List β†’ ID in URL
25
+ CW_BOARD_ID = os.environ.get("CW_BOARD_ID", "") # e.g. "1"
26
+ CW_COMPANY_NUM_ID = os.environ.get("CW_COMPANY_NUM_ID", "") # e.g. "250"
27
+ CW_PRIORITY_ID = os.environ.get("CW_PRIORITY_ID", "") # e.g. "8"
28
+
29
  auth = f"{CW_COMPANY_ID}+{CW_PUBLIC_KEY}:{CW_PRIVATE_KEY}"
30
  encoded_auth = base64.b64encode(auth.encode()).decode()
31
 
 
35
  "Content-Type": "application/json",
36
  }
37
 
 
 
 
38
 
39
+ # ── Discovery (works only once 403 is fixed) ───────────────────────────────────
40
 
41
+ def discover():
42
+ """Try to pull board/company/priority IDs from the API."""
43
+ board_id = company_id = priority_id = None
44
+ board_name = "default"
45
 
 
 
46
  try:
47
+ r = requests.get(f"{BASE_URL}/service/boards", headers=CW_HEADERS,
48
+ params={"pageSize": 100}, timeout=10)
49
+ logger.info("Boards %s: %s", r.status_code, r.text[:400])
50
+ if r.status_code == 200 and r.json():
51
+ b = r.json()[0]
52
+ board_id, board_name = b["id"], b["name"]
 
 
 
53
  except Exception as e:
54
+ logger.warning("Board discover error: %s", e)
 
 
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  try:
57
+ r = requests.get(f"{BASE_URL}/company/companies", headers=CW_HEADERS,
58
+ params={"pageSize": 50}, timeout=10)
59
+ logger.info("Companies %s: %s", r.status_code, r.text[:400])
 
 
 
60
  if r.status_code == 200 and r.json():
61
+ # Try to match our company name, else take first
62
+ all_companies = r.json()
63
+ match = next(
64
+ (c for c in all_companies
65
+ if CW_COMPANY_ID.lower() in (c.get("name","") + c.get("identifier","")).lower()),
66
+ all_companies[0]
67
+ )
68
+ company_id = match["id"]
69
+ logger.info("Matched company: %s id=%s", match.get("name"), company_id)
70
  except Exception as e:
71
+ logger.warning("Company discover error: %s", e)
 
 
72
 
 
 
73
  try:
74
+ r = requests.get(f"{BASE_URL}/service/priorities", headers=CW_HEADERS,
75
+ params={"pageSize": 50}, timeout=10)
 
 
 
 
76
  logger.info("Priorities %s: %s", r.status_code, r.text[:300])
77
  if r.status_code == 200 and r.json():
78
+ priority_id = r.json()[0]["id"]
79
  except Exception as e:
80
+ logger.warning("Priority discover error: %s", e)
81
+
82
+ return board_id, board_name, company_id, priority_id
83
 
84
 
85
+ # ── Ticket creation ────────────────────────────────────────────────────────────
86
 
87
  def post_ticket(payload):
88
  r = requests.post(
 
91
  json=payload,
92
  timeout=10,
93
  )
94
+ logger.info("CW POST %s: %s", r.status_code, r.text[:500])
95
  return r
96
 
97
 
98
  def create_ticket(issue):
99
+ summary = issue[:100]
100
+
101
+ # Use hardcoded env var IDs if set, otherwise try discovery
102
+ if CW_BOARD_ID and CW_COMPANY_NUM_ID:
103
+ board_id = int(CW_BOARD_ID)
104
+ company_id = int(CW_COMPANY_NUM_ID)
105
+ priority_id = int(CW_PRIORITY_ID) if CW_PRIORITY_ID else None
106
+ board_name = "configured"
107
+ logger.info("Using env var IDs: board=%s company=%s priority=%s",
108
+ board_id, company_id, priority_id)
109
+ else:
110
+ board_id, board_name, company_id, priority_id = discover()
111
+
112
+ attempts = []
113
+
114
+ # ── 1. Numeric IDs (most reliable) ───────────────────────────────────────
115
+ if board_id and company_id:
116
+ payload = {
117
+ "summary": summary, "initialDescription": issue,
118
+ "board": {"id": board_id},
119
+ "company": {"id": company_id},
120
+ }
121
+ if priority_id:
122
+ payload["priority"] = {"id": priority_id}
123
+ r = post_ticket(payload)
124
+ if r.status_code in (200, 201):
125
+ return r.json().get("id"), board_name
126
+ attempts.append(r.text)
127
+
128
+ # ── 2. Company numeric ID only (no board) ────────────────────────────────
129
  if company_id:
130
  payload = {
131
+ "summary": summary, "initialDescription": issue,
 
132
  "company": {"id": company_id},
133
  }
134
  r = post_ticket(payload)
135
  if r.status_code in (200, 201):
136
  return r.json().get("id"), "default"
137
+ attempts.append(r.text)
138
 
139
+ # ── 3. Member's own company as the company (common CW pattern) ───────────
140
+ # When company = the MSP itself, CW sometimes needs companyType or contactId
141
+ # Try with just the member company ID from login
142
+ try:
143
+ r2 = requests.get(
144
+ f"{BASE_URL}/system/members/me",
145
+ headers=CW_HEADERS, timeout=10
146
+ )
147
+ if r2.status_code == 200:
148
+ me = r2.json()
149
+ my_company_id = me.get("company", {}).get("id")
150
+ logger.info("Member company id: %s", my_company_id)
151
+ if my_company_id:
152
+ payload = {
153
+ "summary": summary, "initialDescription": issue,
154
+ "company": {"id": my_company_id},
155
+ }
156
+ if board_id:
157
+ payload["board"] = {"id": board_id}
158
+ r = post_ticket(payload)
159
+ if r.status_code in (200, 201):
160
+ return r.json().get("id"), board_name or "default"
161
+ attempts.append(r.text)
162
+ except Exception as e:
163
+ logger.warning("Member lookup failed: %s", e)
164
+
165
+ # ── 4. Absolute minimum ───────────────────────────────────────────────────
166
  r = post_ticket({"summary": summary, "initialDescription": issue})
167
  if r.status_code in (200, 201):
168
  return r.json().get("id"), "default"
169
 
170
+ logger.error("All attempts failed. Last errors: %s", attempts[-3:])
171
  return None, None
172
 
173
 
174
+ # ── Slack ──────────────────────────────────────────────────────────────────────
175
 
176
  def send_slack(channel, text):
177
  requests.post(
178
  "https://slack.com/api/chat.postMessage",
179
+ headers={"Authorization": f"Bearer {SLACK_TOKEN}",
180
+ "Content-Type": "application/json"},
 
 
181
  json={"channel": channel, "text": text},
182
  timeout=10,
183
  )
 
193
  @app.route("/debug/cw")
194
  def debug_cw():
195
  """
196
+ Visit /debug/cw to see what your API key CAN access.
197
+ If you see 403 errors here, your API key needs more permissions in ConnectWise.
198
+ Go to: System β†’ Members β†’ API Members β†’ Security Role β†’ enable Inquire on:
199
+ Companies > Company Maintenance
200
+ Service Desk > Service Boards
201
+ Service Desk > Service Tickets (Add + Inquire)
202
+ Service Desk > Priorities
203
  """
204
+ out = {
205
+ "env_vars": {
206
+ "CW_BOARD_ID": CW_BOARD_ID or "NOT SET",
207
+ "CW_COMPANY_NUM_ID": CW_COMPANY_NUM_ID or "NOT SET",
208
+ "CW_PRIORITY_ID": CW_PRIORITY_ID or "NOT SET",
209
+ }
210
+ }
211
+
212
+ for label, url, params in [
213
+ ("boards", f"{BASE_URL}/service/boards", {"pageSize": 50}),
214
+ ("companies", f"{BASE_URL}/company/companies", {"pageSize": 20}),
215
+ ("priorities", f"{BASE_URL}/service/priorities", {"pageSize": 20}),
216
+ ("member_me", f"{BASE_URL}/system/members/me", {}),
217
+ ]:
218
+ try:
219
+ r = requests.get(url, headers=CW_HEADERS, params=params, timeout=10)
220
+ out[label] = {"status": r.status_code, "data": r.json()}
221
+ except Exception as e:
222
+ out[label] = {"error": str(e)}
 
 
 
223
 
224
  return jsonify(out)
225
 
 
228
  def slack_events():
229
  data = request.get_json()
230
 
 
231
  if data.get("type") == "url_verification":
232
  return jsonify({"challenge": data.get("challenge")})
233
 
234
  event = data.get("event", {})
235
 
 
236
  if event.get("bot_id") or event.get("subtype") == "bot_message":
237
  return "", 200
238
 
239
  if event.get("type") == "message":
240
  text = event.get("text", "")
241
  channel = event.get("channel")
242
+ text = text.replace("<@U0AL3KRRELE>", "").strip()
243
 
 
244
  if not text:
245
  return "", 200
246
 
247
  ticket_id, board_used = create_ticket(text)
248
 
249
  if ticket_id:
250
+ reply = f"βœ… Ticket created!\n🎫 ID: `{ticket_id}`\nπŸ“‹ Board: {board_used}"
 
 
 
 
251
  else:
252
  reply = (
253
+ "⚠️ Could not create ticket. Your API key needs more permissions in ConnectWise.\n"
254
+ "Go to: System β†’ Members β†’ API Members β†’ Security Role\n"
255
+ "Enable Inquire on: Company Maintenance, Service Boards, Service Tickets, Priorities"
256
  )
257
 
258
  send_slack(channel, reply)