File size: 10,129 Bytes
2b66b9a
 
087a6a4
b6ec3ee
1c189d0
21b7a46
 
 
 
 
b6ec3ee
4586a84
b6ec3ee
2b66b9a
b6ec3ee
 
 
 
 
 
 
 
 
2b66b9a
 
 
 
 
 
 
21b7a46
1c189d0
dd8915e
1c189d0
21b7a46
1c189d0
 
4586a84
9faf9b1
21b7a46
f05e997
 
 
4586a84
b6ec3ee
 
 
 
 
 
 
 
 
 
4586a84
b6ec3ee
 
 
 
4586a84
b6ec3ee
 
 
 
2b66b9a
4586a84
21b7a46
2b66b9a
b6ec3ee
2b66b9a
 
4586a84
2b66b9a
 
1c189d0
 
 
 
2b66b9a
21b7a46
1c189d0
226db43
1c189d0
 
 
 
b6ec3ee
 
 
 
39374e4
 
 
 
 
 
 
 
 
 
 
 
4586a84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1c189d0
b6ec3ee
 
 
 
4586a84
b6ec3ee
31ac193
129de0c
b6ec3ee
4586a84
4f04102
 
 
debf758
4586a84
 
debf758
 
 
4586a84
 
 
 
 
debf758
 
4586a84
 
debf758
 
 
4586a84
 
 
 
 
debf758
4f04102
 
 
4586a84
9faf9b1
4586a84
3ab8f33
 
 
39374e4
 
4586a84
39374e4
 
 
 
 
4586a84
9faf9b1
 
 
 
226db43
 
 
 
4586a84
9faf9b1
 
 
3ab8f33
4586a84
087a6a4
4586a84
debf758
 
9faf9b1
 
 
debf758
9faf9b1
 
4f04102
9faf9b1
 
debf758
31ac193
 
 
 
 
39374e4
 
27dff6a
debf758
4586a84
 
debf758
 
 
 
 
 
 
 
 
 
 
 
9faf9b1
 
 
4586a84
9faf9b1
 
 
 
 
 
 
 
3ab8f33
9faf9b1
 
 
 
 
 
3ab8f33
 
 
1c189d0
9faf9b1
 
 
 
 
 
3ab8f33
 
 
 
 
 
1c189d0
3ab8f33
 
 
 
 
 
1c189d0
9faf9b1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
import os
import aiohttp
import hashlib
from fastapi import APIRouter, HTTPException, Header
from pydantic import BaseModel
from google.oauth2 import service_account
from google.auth.transport.requests import Request

router = APIRouter()

# 🔧 Supabase Config
SUPABASE_URL = "https://ussxqnifefkgkaumjann.supabase.co"
SUPABASE_KEY = os.getenv("SUPA_KEY")
SUPABASE_ROLE_KEY = os.getenv("SUPA_SERVICE_KEY")

if not SUPABASE_KEY or not SUPABASE_ROLE_KEY:
    raise ValueError("❌ SUPA_KEY or SUPA_SERVICE_KEY not set in environment!")

SUPABASE_HEADERS = {
    "apikey": SUPABASE_KEY,
    "Authorization": f"Bearer {SUPABASE_KEY}",
    "Content-Type": "application/json"
}

SUPABASE_ROLE_HEADERS = {
    "apikey": SUPABASE_ROLE_KEY,
    "Authorization": f"Bearer {SUPABASE_ROLE_KEY}",
    "Content-Type": "application/json",
    "Prefer": "return=representation"
}

# Firebase config
SERVICE_ACCOUNT_FILE = './closetcoach-2d50b-firebase-adminsdk-fbsvc-7fcccbacb1.json'
FCM_PROJECT_ID = "closetcoach-2d50b"

class NotificationRequest(BaseModel):
    keyword: str
    target_user_id: str = ""  # optional for 'newmessage'
    reference: str = ""

def short_collapse_key(keyword: str, sender_id: str, receiver_id: str) -> str:
    raw = f"{keyword}:{sender_id}:{receiver_id}"
    return hashlib.sha1(raw.encode()).hexdigest()[:20]

async def verify_user_token(user_token: str) -> str:
    headers = {
        "Authorization": f"Bearer {user_token}",
        "apikey": SUPABASE_KEY,
        "Content-Type": "application/json"
    }

    async with aiohttp.ClientSession() as session:
        async with session.get(f"{SUPABASE_URL}/auth/v1/user", headers=headers) as response:
            if response.status != 200:
                raise HTTPException(status_code=401, detail="Invalid or expired token")

            user_data = await response.json()
            user_id = user_data.get("id")
            if not user_id:
                raise HTTPException(status_code=400, detail="User ID not found")

    return user_id

async def fetch_supabase(table: str, select: str, filters: dict, headers=SUPABASE_ROLE_HEADERS):
    filter_query = '&'.join([f'{k}=eq.{v}' for k, v in filters.items()])
    url = f"{SUPABASE_URL}/rest/v1/{table}?select={select}&{filter_query}&order=created_at.desc"

    async with aiohttp.ClientSession() as session:
        async with session.get(url, headers=headers) as resp:
            if resp.status != 200:
                detail = await resp.text()
                raise HTTPException(status_code=500, detail=f"Supabase error: {detail}")
            return await resp.json()

def format_name(full_name: str) -> str:
    parts = full_name.strip().split()
    if len(parts) == 1:
        return parts[0]
    return f"{parts[0]} {parts[1][0].upper()}."

async def get_user_info(user_id: str):
    users = await fetch_supabase("User", "name,token_fcm,notifications", {"id": user_id})
    if not users:
        return None
    return users[0]

async def check_follow_exists(follower_id: str, following_id: str) -> bool:
    result = await fetch_supabase("followers", "id", {"follower_id": follower_id, "following_id": following_id})
    return len(result) > 0

async def check_subscription_exists(customer_id: str, stylist_id: str) -> bool:
    result = await fetch_supabase(
        "Subscriptions",
        "id",
        {
            "customer_id": customer_id,
            "stylist_id": stylist_id,
            "active": "true"
        }
    )
    return len(result) > 0

async def get_post_info(feed_id: str):
    feeds = await fetch_supabase("Feeds", "description,portfolios,user_id", {"id": feed_id})
    if not feeds:
        raise HTTPException(status_code=404, detail="Post not found")

    feed = feeds[0]
    description = feed.get("description", "")
    portfolio_ids = feed.get("portfolios") or []
    user_id = feed.get("user_id")

    image_url = None
    if portfolio_ids:
        portfolio_data = await fetch_supabase("Portfolio", "image_url", {"id": portfolio_ids[0]})
        if portfolio_data:
            image_url = portfolio_data[0].get("image_url")

    return {
        "description": description,
        "image_url": image_url,
        "user_id": user_id
    }

def get_access_token():
    credentials = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_FILE
    )
    scoped_credentials = credentials.with_scopes(
        ['https://www.googleapis.com/auth/firebase.messaging']
    )
    scoped_credentials.refresh(Request())
    return scoped_credentials.token

@router.post("/send-notification")
async def send_notification(
    data: NotificationRequest,
    user_token: str = Header(..., alias="User-key")
):
    sender_id = await verify_user_token(user_token)

    if data.keyword not in ("follow", "like", "subscriber", "newmessage", "changeprice"):
        raise HTTPException(status_code=400, detail="Unsupported keyword")

    # Determine target_user_id
    if data.keyword == "like":
        post_info = await get_post_info(data.reference)
        target_user_id = post_info["user_id"]

    elif data.keyword == "newmessage":
        # Get last message in the chat
        messages = await fetch_supabase("messages", "sender_id,content,file", {
            "chat_id": data.reference
        })
        if not messages:
            raise HTTPException(status_code=404, detail="No messages found in chat")

        last_message = messages[0]
        sender_id = last_message["sender_id"]
        content = (last_message.get("content") or "").strip()
        file_url = last_message.get("file")

        # Get participants
        chats = await fetch_supabase("chats", "client_id,stylist_id", {
            "id": data.reference
        })
        if not chats:
            raise HTTPException(status_code=404, detail="Chat not found")

        chat = chats[0]
        target_user_id = chat["stylist_id"] if sender_id == chat["client_id"] else chat["client_id"]

    else:
        target_user_id = data.target_user_id

    # Check conditions
    if data.keyword == "follow":
        follow_exists = await check_follow_exists(sender_id, target_user_id)
        if not follow_exists:
            raise HTTPException(status_code=403, detail="Follow relationship does not exist")

    if data.keyword == "subscriber":
        subscription_exists = await check_subscription_exists(
            customer_id=sender_id,
            stylist_id=target_user_id
        )
        if not subscription_exists:
            return {"detail": "No active subscription found, notification not sent"}

    # User info
    target_user = await get_user_info(target_user_id)
    if not target_user or not target_user.get("token_fcm"):
        raise HTTPException(status_code=404, detail="Target user or FCM token not found")

    # 🔔 Check if target user has notifications enabled
    if not target_user.get("notifications", True):  # Default to True if field doesn't exist
        return {"detail": "Target user has notifications disabled, notification not sent"}

    actor_info = await get_user_info(sender_id)
    if not actor_info or not actor_info.get("name"):
        raise HTTPException(status_code=404, detail="User not found")
    actor_name = format_name(actor_info["name"])

    collapse_id = short_collapse_key(data.keyword, sender_id, target_user_id)

    # Notification content
    title = image_url = None

    if data.keyword == "follow":
        title = "🎉 New Follower!"
        body = f"{actor_name} started following you."

    elif data.keyword == "like":
        desc = post_info["description"]
        title = "❤️ New Like!"
        body = f"{actor_name} liked your post" + (f": \"{desc}\"" if desc else ".")
        image_url = post_info["image_url"]

    elif data.keyword == "changeprice":
        title = "⚠️ Subscription Price Changed"
        # u.name está no actor_name (abreviado)
        body = f"{actor_name} changed your subscription price. Your subscription was automatically canceled. Please check the chat with {actor_name} for reactivation options and more info."

    elif data.keyword == "subscriber":
        title = "💼 New Subscriber!"
        body = f"{actor_name} just subscribed to your styling services."

    elif data.keyword == "newmessage":
        title = "💬 New Message"
        if content:
            body = f"{actor_name}: {content}"
        elif file_url:
            is_image = file_url.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp"))
            if is_image:
                body = f"{actor_name} sent you a photo"
                image_url = file_url
            else:
                body = f"{actor_name} sent you a file"
        else:
            body = f"{actor_name} sent you a message"

    else:
        raise HTTPException(status_code=400, detail="Unsupported keyword")

    # Compose FCM message
    message = {
        "notification": {
            "title": title,
            "body": body,
        },
        "token": target_user["token_fcm"],
        "android": {
            "collapse_key": collapse_id,
            "notification": {
                "tag": collapse_id
            }
        },
        "apns": {
            "headers": {
                "apns-collapse-id": collapse_id
            }
        }
    }

    if image_url:
        message["notification"]["image"] = image_url
        message["android"]["notification"]["image"] = image_url

    payload = {"message": message}

    access_token = get_access_token()
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }
    url = f"https://fcm.googleapis.com/v1/projects/{FCM_PROJECT_ID}/messages:send"

    async with aiohttp.ClientSession() as session:
        async with session.post(url, headers=headers, json=payload) as resp:
            resp_text = await resp.text()
            if resp.status != 200:
                raise HTTPException(status_code=resp.status, detail=f"FCM error: {resp_text}")
            fcm_response = await resp.json()

    return {"detail": "Notification sent successfully", "fcm_response": fcm_response}