Update routes/sendnotifications.py
Browse files- routes/sendnotifications.py +71 -71
routes/sendnotifications.py
CHANGED
@@ -9,7 +9,7 @@ from google.auth.transport.requests import Request
|
|
9 |
router = APIRouter()
|
10 |
|
11 |
# 🔧 Supabase Config
|
12 |
-
SUPABASE_URL = "https://ussxqnifefkgkaumjann.supabase.co"
|
13 |
SUPABASE_KEY = os.getenv("SUPA_KEY")
|
14 |
SUPABASE_ROLE_KEY = os.getenv("SUPA_SERVICE_KEY")
|
15 |
|
@@ -35,13 +35,13 @@ FCM_PROJECT_ID = "closetcoach-2d50b"
|
|
35 |
|
36 |
class NotificationRequest(BaseModel):
|
37 |
keyword: str
|
38 |
-
target_user_id: str
|
39 |
reference: str = ""
|
40 |
|
41 |
def short_collapse_key(keyword: str, sender_id: str, receiver_id: str) -> str:
|
42 |
raw = f"{keyword}:{sender_id}:{receiver_id}"
|
43 |
return hashlib.sha1(raw.encode()).hexdigest()[:20]
|
44 |
-
|
45 |
async def verify_user_token(user_token: str) -> str:
|
46 |
headers = {
|
47 |
"Authorization": f"Bearer {user_token}",
|
@@ -52,47 +52,24 @@ async def verify_user_token(user_token: str) -> str:
|
|
52 |
async with aiohttp.ClientSession() as session:
|
53 |
async with session.get(f"{SUPABASE_URL}/auth/v1/user", headers=headers) as response:
|
54 |
if response.status != 200:
|
55 |
-
raise HTTPException(status_code=401, detail="
|
56 |
|
57 |
user_data = await response.json()
|
58 |
user_id = user_data.get("id")
|
59 |
if not user_id:
|
60 |
-
raise HTTPException(status_code=400, detail="ID
|
61 |
|
62 |
return user_id
|
63 |
|
64 |
-
async def get_post_info(feed_id: str):
|
65 |
-
# Busca o feed
|
66 |
-
feeds = await fetch_supabase("Feeds", "description,portfolios,user_id", {"id": feed_id})
|
67 |
-
if not feeds:
|
68 |
-
raise HTTPException(status_code=404, detail="Post not found")
|
69 |
-
|
70 |
-
feed = feeds[0]
|
71 |
-
description = feed.get("description", "")
|
72 |
-
portfolio_ids = feed.get("portfolios") or []
|
73 |
-
user_id = feed.get("user_id")
|
74 |
-
|
75 |
-
image_url = None
|
76 |
-
if portfolio_ids:
|
77 |
-
portfolio_data = await fetch_supabase("Portfolio", "image_url", {"id": portfolio_ids[0]})
|
78 |
-
if portfolio_data:
|
79 |
-
image_url = portfolio_data[0].get("image_url")
|
80 |
-
|
81 |
-
return {
|
82 |
-
"description": description,
|
83 |
-
"image_url": image_url,
|
84 |
-
"user_id": user_id # <-- importante
|
85 |
-
}
|
86 |
-
|
87 |
async def fetch_supabase(table: str, select: str, filters: dict, headers=SUPABASE_ROLE_HEADERS):
|
88 |
filter_query = '&'.join([f'{k}=eq.{v}' for k, v in filters.items()])
|
89 |
-
url = f"{SUPABASE_URL}/rest/v1/{table}?select={select}&{filter_query}"
|
90 |
|
91 |
async with aiohttp.ClientSession() as session:
|
92 |
async with session.get(url, headers=headers) as resp:
|
93 |
if resp.status != 200:
|
94 |
detail = await resp.text()
|
95 |
-
raise HTTPException(status_code=500, detail=f"
|
96 |
return await resp.json()
|
97 |
|
98 |
def format_name(full_name: str) -> str:
|
@@ -111,41 +88,6 @@ async def check_follow_exists(follower_id: str, following_id: str) -> bool:
|
|
111 |
result = await fetch_supabase("followers", "id", {"follower_id": follower_id, "following_id": following_id})
|
112 |
return len(result) > 0
|
113 |
|
114 |
-
def get_access_token():
|
115 |
-
credentials = service_account.Credentials.from_service_account_file(
|
116 |
-
SERVICE_ACCOUNT_FILE
|
117 |
-
)
|
118 |
-
scoped_credentials = credentials.with_scopes(
|
119 |
-
['https://www.googleapis.com/auth/firebase.messaging']
|
120 |
-
)
|
121 |
-
scoped_credentials.refresh(Request())
|
122 |
-
return scoped_credentials.token
|
123 |
-
|
124 |
-
async def send_fcm_notification(title: str, body: str, token: str):
|
125 |
-
payload = {
|
126 |
-
"message": {
|
127 |
-
"notification": {
|
128 |
-
"title": title,
|
129 |
-
"body": body,
|
130 |
-
},
|
131 |
-
"token": token
|
132 |
-
}
|
133 |
-
}
|
134 |
-
|
135 |
-
access_token = get_access_token()
|
136 |
-
headers = {
|
137 |
-
"Authorization": f"Bearer {access_token}",
|
138 |
-
"Content-Type": "application/json"
|
139 |
-
}
|
140 |
-
url = f"https://fcm.googleapis.com/v1/projects/{FCM_PROJECT_ID}/messages:send"
|
141 |
-
|
142 |
-
async with aiohttp.ClientSession() as session:
|
143 |
-
async with session.post(url, headers=headers, json=payload) as resp:
|
144 |
-
resp_text = await resp.text()
|
145 |
-
if resp.status != 200:
|
146 |
-
raise HTTPException(status_code=resp.status, detail=f"FCM error: {resp_text}")
|
147 |
-
return await resp.json()
|
148 |
-
|
149 |
async def check_subscription_exists(customer_id: str, stylist_id: str) -> bool:
|
150 |
result = await fetch_supabase(
|
151 |
"Subscriptions",
|
@@ -158,46 +100,99 @@ async def check_subscription_exists(customer_id: str, stylist_id: str) -> bool:
|
|
158 |
)
|
159 |
return len(result) > 0
|
160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
161 |
@router.post("/send-notification")
|
162 |
async def send_notification(
|
163 |
data: NotificationRequest,
|
164 |
user_token: str = Header(..., alias="User-key")
|
165 |
):
|
166 |
-
|
167 |
|
168 |
-
if data.keyword not in ("follow", "like", "subscriber"):
|
169 |
raise HTTPException(status_code=400, detail="Unsupported keyword")
|
170 |
|
|
|
171 |
if data.keyword == "like":
|
172 |
post_info = await get_post_info(data.reference)
|
173 |
target_user_id = post_info["user_id"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
174 |
else:
|
175 |
target_user_id = data.target_user_id
|
176 |
|
|
|
177 |
if data.keyword == "follow":
|
178 |
-
follow_exists = await check_follow_exists(
|
179 |
if not follow_exists:
|
180 |
raise HTTPException(status_code=403, detail="Follow relationship does not exist")
|
181 |
|
182 |
if data.keyword == "subscriber":
|
183 |
subscription_exists = await check_subscription_exists(
|
184 |
-
customer_id=
|
185 |
stylist_id=target_user_id
|
186 |
)
|
187 |
if not subscription_exists:
|
188 |
return {"detail": "No active subscription found, notification not sent"}
|
189 |
|
|
|
190 |
target_user = await get_user_info(target_user_id)
|
191 |
if not target_user or not target_user.get("token_fcm"):
|
192 |
raise HTTPException(status_code=404, detail="Target user or FCM token not found")
|
193 |
|
194 |
-
actor_info = await get_user_info(
|
195 |
if not actor_info or not actor_info.get("name"):
|
196 |
raise HTTPException(status_code=404, detail="User not found")
|
197 |
actor_name = format_name(actor_info["name"])
|
198 |
|
199 |
-
collapse_id = short_collapse_key(data.keyword,
|
200 |
|
|
|
201 |
if data.keyword == "follow":
|
202 |
title = "🎉 New Follower!"
|
203 |
body = f"{actor_name} started following you."
|
@@ -211,9 +206,14 @@ async def send_notification(
|
|
211 |
title = "💼 New Subscriber!"
|
212 |
body = f"{actor_name} just subscribed to your styling services."
|
213 |
image_url = None
|
|
|
|
|
|
|
|
|
214 |
else:
|
215 |
raise HTTPException(status_code=400, detail="Unsupported keyword")
|
216 |
|
|
|
217 |
message = {
|
218 |
"notification": {
|
219 |
"title": title,
|
|
|
9 |
router = APIRouter()
|
10 |
|
11 |
# 🔧 Supabase Config
|
12 |
+
SUPABASE_URL = "https://ussxqnifefkgkaumjann.supabase.co"
|
13 |
SUPABASE_KEY = os.getenv("SUPA_KEY")
|
14 |
SUPABASE_ROLE_KEY = os.getenv("SUPA_SERVICE_KEY")
|
15 |
|
|
|
35 |
|
36 |
class NotificationRequest(BaseModel):
|
37 |
keyword: str
|
38 |
+
target_user_id: str = "" # optional for 'newmessage'
|
39 |
reference: str = ""
|
40 |
|
41 |
def short_collapse_key(keyword: str, sender_id: str, receiver_id: str) -> str:
|
42 |
raw = f"{keyword}:{sender_id}:{receiver_id}"
|
43 |
return hashlib.sha1(raw.encode()).hexdigest()[:20]
|
44 |
+
|
45 |
async def verify_user_token(user_token: str) -> str:
|
46 |
headers = {
|
47 |
"Authorization": f"Bearer {user_token}",
|
|
|
52 |
async with aiohttp.ClientSession() as session:
|
53 |
async with session.get(f"{SUPABASE_URL}/auth/v1/user", headers=headers) as response:
|
54 |
if response.status != 200:
|
55 |
+
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
56 |
|
57 |
user_data = await response.json()
|
58 |
user_id = user_data.get("id")
|
59 |
if not user_id:
|
60 |
+
raise HTTPException(status_code=400, detail="User ID not found")
|
61 |
|
62 |
return user_id
|
63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
async def fetch_supabase(table: str, select: str, filters: dict, headers=SUPABASE_ROLE_HEADERS):
|
65 |
filter_query = '&'.join([f'{k}=eq.{v}' for k, v in filters.items()])
|
66 |
+
url = f"{SUPABASE_URL}/rest/v1/{table}?select={select}&{filter_query}&order=created_at.desc"
|
67 |
|
68 |
async with aiohttp.ClientSession() as session:
|
69 |
async with session.get(url, headers=headers) as resp:
|
70 |
if resp.status != 200:
|
71 |
detail = await resp.text()
|
72 |
+
raise HTTPException(status_code=500, detail=f"Supabase error: {detail}")
|
73 |
return await resp.json()
|
74 |
|
75 |
def format_name(full_name: str) -> str:
|
|
|
88 |
result = await fetch_supabase("followers", "id", {"follower_id": follower_id, "following_id": following_id})
|
89 |
return len(result) > 0
|
90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
async def check_subscription_exists(customer_id: str, stylist_id: str) -> bool:
|
92 |
result = await fetch_supabase(
|
93 |
"Subscriptions",
|
|
|
100 |
)
|
101 |
return len(result) > 0
|
102 |
|
103 |
+
async def get_post_info(feed_id: str):
|
104 |
+
feeds = await fetch_supabase("Feeds", "description,portfolios,user_id", {"id": feed_id})
|
105 |
+
if not feeds:
|
106 |
+
raise HTTPException(status_code=404, detail="Post not found")
|
107 |
+
|
108 |
+
feed = feeds[0]
|
109 |
+
description = feed.get("description", "")
|
110 |
+
portfolio_ids = feed.get("portfolios") or []
|
111 |
+
user_id = feed.get("user_id")
|
112 |
+
|
113 |
+
image_url = None
|
114 |
+
if portfolio_ids:
|
115 |
+
portfolio_data = await fetch_supabase("Portfolio", "image_url", {"id": portfolio_ids[0]})
|
116 |
+
if portfolio_data:
|
117 |
+
image_url = portfolio_data[0].get("image_url")
|
118 |
+
|
119 |
+
return {
|
120 |
+
"description": description,
|
121 |
+
"image_url": image_url,
|
122 |
+
"user_id": user_id
|
123 |
+
}
|
124 |
+
|
125 |
+
def get_access_token():
|
126 |
+
credentials = service_account.Credentials.from_service_account_file(
|
127 |
+
SERVICE_ACCOUNT_FILE
|
128 |
+
)
|
129 |
+
scoped_credentials = credentials.with_scopes(
|
130 |
+
['https://www.googleapis.com/auth/firebase.messaging']
|
131 |
+
)
|
132 |
+
scoped_credentials.refresh(Request())
|
133 |
+
return scoped_credentials.token
|
134 |
+
|
135 |
@router.post("/send-notification")
|
136 |
async def send_notification(
|
137 |
data: NotificationRequest,
|
138 |
user_token: str = Header(..., alias="User-key")
|
139 |
):
|
140 |
+
sender_id = await verify_user_token(user_token)
|
141 |
|
142 |
+
if data.keyword not in ("follow", "like", "subscriber", "newmessage"):
|
143 |
raise HTTPException(status_code=400, detail="Unsupported keyword")
|
144 |
|
145 |
+
# Determine target_user_id
|
146 |
if data.keyword == "like":
|
147 |
post_info = await get_post_info(data.reference)
|
148 |
target_user_id = post_info["user_id"]
|
149 |
+
elif data.keyword == "newmessage":
|
150 |
+
# Get last message in the chat
|
151 |
+
messages = await fetch_supabase("messages", "sender_id,content", {"chat_id": data.reference})
|
152 |
+
if not messages:
|
153 |
+
raise HTTPException(status_code=404, detail="No messages found in chat")
|
154 |
+
|
155 |
+
last_message = messages[0]
|
156 |
+
sender_id = last_message["sender_id"]
|
157 |
+
content = last_message["content"]
|
158 |
+
|
159 |
+
# Get participants
|
160 |
+
chats = await fetch_supabase("chats", "client_id,stylist_id", {"id": data.reference})
|
161 |
+
if not chats:
|
162 |
+
raise HTTPException(status_code=404, detail="Chat not found")
|
163 |
+
|
164 |
+
chat = chats[0]
|
165 |
+
target_user_id = chat["stylist_id"] if sender_id == chat["client_id"] else chat["client_id"]
|
166 |
else:
|
167 |
target_user_id = data.target_user_id
|
168 |
|
169 |
+
# Check conditions
|
170 |
if data.keyword == "follow":
|
171 |
+
follow_exists = await check_follow_exists(sender_id, target_user_id)
|
172 |
if not follow_exists:
|
173 |
raise HTTPException(status_code=403, detail="Follow relationship does not exist")
|
174 |
|
175 |
if data.keyword == "subscriber":
|
176 |
subscription_exists = await check_subscription_exists(
|
177 |
+
customer_id=sender_id,
|
178 |
stylist_id=target_user_id
|
179 |
)
|
180 |
if not subscription_exists:
|
181 |
return {"detail": "No active subscription found, notification not sent"}
|
182 |
|
183 |
+
# User info
|
184 |
target_user = await get_user_info(target_user_id)
|
185 |
if not target_user or not target_user.get("token_fcm"):
|
186 |
raise HTTPException(status_code=404, detail="Target user or FCM token not found")
|
187 |
|
188 |
+
actor_info = await get_user_info(sender_id)
|
189 |
if not actor_info or not actor_info.get("name"):
|
190 |
raise HTTPException(status_code=404, detail="User not found")
|
191 |
actor_name = format_name(actor_info["name"])
|
192 |
|
193 |
+
collapse_id = short_collapse_key(data.keyword, sender_id, target_user_id)
|
194 |
|
195 |
+
# Notification content
|
196 |
if data.keyword == "follow":
|
197 |
title = "🎉 New Follower!"
|
198 |
body = f"{actor_name} started following you."
|
|
|
206 |
title = "💼 New Subscriber!"
|
207 |
body = f"{actor_name} just subscribed to your styling services."
|
208 |
image_url = None
|
209 |
+
elif data.keyword == "newmessage":
|
210 |
+
title = "💬 New Message"
|
211 |
+
body = f"{actor_name}: {content}"
|
212 |
+
image_url = None
|
213 |
else:
|
214 |
raise HTTPException(status_code=400, detail="Unsupported keyword")
|
215 |
|
216 |
+
# Compose FCM message
|
217 |
message = {
|
218 |
"notification": {
|
219 |
"title": title,
|