iOS: - PhotosPicker with maxSelectionCount: 5 - Horizontal scroll preview strip with individual remove buttons - Sends "images" array instead of single "image" Server: - Gateway accepts both "image" (single, backwards compat) and "images" (array) fields - Uploads each as separate Gitea issue attachment Also closed Gitea issues #11, #12, #13, #14. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
145 lines
5.2 KiB
Python
145 lines
5.2 KiB
Python
"""
|
|
Platform Gateway — Feedback handler.
|
|
Creates Gitea issues from user feedback with auto-labeling.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import urllib.request
|
|
import urllib.error
|
|
|
|
GITEA_URL = "http://gitea:3000"
|
|
GITEA_TOKEN = "7abae0245e49e130e1b6752b7fa381a57607d8c7"
|
|
REPO = "yusiboyz/platform"
|
|
|
|
# Label IDs from Gitea
|
|
LABELS = {
|
|
"bug": 11,
|
|
"feature": 12,
|
|
"ios": 13,
|
|
"web": 14,
|
|
"fitness": 15,
|
|
"brain": 16,
|
|
"reader": 17,
|
|
"podcasts": 18,
|
|
"enhancement": 19,
|
|
}
|
|
|
|
|
|
def auto_label(text: str, source: str = "ios") -> list[int]:
|
|
"""Detect labels from feedback text."""
|
|
text_lower = text.lower()
|
|
label_ids = []
|
|
|
|
# Source platform
|
|
if source == "ios":
|
|
label_ids.append(LABELS["ios"])
|
|
elif source == "web":
|
|
label_ids.append(LABELS["web"])
|
|
|
|
# Bug or feature
|
|
bug_words = ["bug", "broken", "crash", "error", "wrong", "fix", "issue", "doesn't work", "not working", "can't", "cant", "won't", "wont", "stuck", "freeze", "blank", "missing", "fail", "weird", "off", "messed", "problem", "glitch", "after you edit", "after i edit", "when i click", "when you click", "should be", "supposed to"]
|
|
feature_words = ["want", "add", "wish", "would be nice", "can we", "can you", "feature", "idea", "suggest", "could you", "how about", "it would be", "please add", "need a", "would love"]
|
|
|
|
if any(w in text_lower for w in bug_words):
|
|
label_ids.append(LABELS["bug"])
|
|
elif any(w in text_lower for w in feature_words):
|
|
label_ids.append(LABELS["feature"])
|
|
else:
|
|
label_ids.append(LABELS["enhancement"])
|
|
|
|
# App detection
|
|
if any(w in text_lower for w in ["fitness", "calorie", "food", "meal", "macro", "protein"]):
|
|
label_ids.append(LABELS["fitness"])
|
|
elif any(w in text_lower for w in ["brain", "note", "save", "bookmark"]):
|
|
label_ids.append(LABELS["brain"])
|
|
elif any(w in text_lower for w in ["reader", "rss", "feed", "article"]):
|
|
label_ids.append(LABELS["reader"])
|
|
elif any(w in text_lower for w in ["podcast", "episode", "listen", "player"]):
|
|
label_ids.append(LABELS["podcasts"])
|
|
|
|
return label_ids
|
|
|
|
|
|
def handle_feedback(handler, body, user):
|
|
"""Create a Gitea issue from user feedback."""
|
|
try:
|
|
data = json.loads(body)
|
|
except Exception:
|
|
handler._send_json({"error": "Invalid JSON"}, 400)
|
|
return
|
|
|
|
text = (data.get("text") or "").strip()
|
|
source = data.get("source", "ios")
|
|
|
|
if not text:
|
|
handler._send_json({"error": "Feedback text is required"}, 400)
|
|
return
|
|
|
|
user_name = user.get("display_name") or user.get("username", "Unknown")
|
|
label_ids = auto_label(text, source)
|
|
|
|
# Create Gitea issue
|
|
issue_body = {
|
|
"title": text[:80] + ("..." if len(text) > 80 else ""),
|
|
"body": f"**From:** {user_name} ({source})\n\n{text}",
|
|
"labels": label_ids,
|
|
}
|
|
|
|
try:
|
|
req = urllib.request.Request(
|
|
f"{GITEA_URL}/api/v1/repos/{REPO}/issues",
|
|
data=json.dumps(issue_body).encode(),
|
|
headers={
|
|
"Authorization": f"token {GITEA_TOKEN}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
method="POST",
|
|
)
|
|
resp = urllib.request.urlopen(req, timeout=10)
|
|
result = json.loads(resp.read())
|
|
issue_number = result.get("number")
|
|
|
|
# Upload image attachments if provided
|
|
if issue_number:
|
|
import base64
|
|
# Support single "image" or multiple "images" array
|
|
images_b64 = data.get("images", [])
|
|
single_image = data.get("image")
|
|
if single_image:
|
|
images_b64.insert(0, single_image)
|
|
|
|
for idx, img_b64 in enumerate(images_b64):
|
|
try:
|
|
img_bytes = base64.b64decode(img_b64)
|
|
filename = f"screenshot{'_' + str(idx + 1) if idx > 0 else ''}.png"
|
|
boundary = f"----FeedbackBoundary{idx}"
|
|
body_parts = []
|
|
body_parts.append(f"--{boundary}\r\n".encode())
|
|
body_parts.append(f'Content-Disposition: form-data; name="attachment"; filename="{filename}"\r\n'.encode())
|
|
body_parts.append(b"Content-Type: image/png\r\n\r\n")
|
|
body_parts.append(img_bytes)
|
|
body_parts.append(f"\r\n--{boundary}--\r\n".encode())
|
|
multipart_body = b"".join(body_parts)
|
|
|
|
img_req = urllib.request.Request(
|
|
f"{GITEA_URL}/api/v1/repos/{REPO}/issues/{issue_number}/assets",
|
|
data=multipart_body,
|
|
headers={
|
|
"Authorization": f"token {GITEA_TOKEN}",
|
|
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
|
},
|
|
method="POST",
|
|
)
|
|
urllib.request.urlopen(img_req, timeout=15)
|
|
except Exception:
|
|
pass # Image upload failed but issue was created
|
|
|
|
handler._send_json({
|
|
"ok": True,
|
|
"issue_number": issue_number,
|
|
"url": result.get("html_url"),
|
|
})
|
|
except Exception as e:
|
|
handler._send_json({"error": f"Failed to create issue: {e}"}, 500)
|