Hydra-Bolt commited on
Commit
31eedc3
·
1 Parent(s): 290ba42
Files changed (9) hide show
  1. Dockerfile +2 -7
  2. __pycache__/main.cpython-313.pyc +0 -0
  3. database.sql +183 -0
  4. index.html +0 -19
  5. main.py +275 -79
  6. requirements.txt +10 -0
  7. send_notifications.log +164 -0
  8. server.js +0 -43
  9. style.css +0 -28
Dockerfile CHANGED
@@ -18,11 +18,6 @@ RUN pip install --no-cache-dir -r requirements.txt
18
  # Copy application code
19
  COPY . .
20
 
21
- # Expose port (Hugging Face Spaces expects 7860, but FastAPI default is 3000)
22
- EXPOSE 7860
23
 
24
- # Set environment variable for Hugging Face Spaces
25
- ENV PORT=7860
26
-
27
- # Start FastAPI server on the expected port
28
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
18
  # Copy application code
19
  COPY . .
20
 
 
 
21
 
22
+ # Run the automation script
23
+ CMD ["python", "main.py"]
 
 
 
__pycache__/main.cpython-313.pyc CHANGED
Binary files a/__pycache__/main.cpython-313.pyc and b/__pycache__/main.cpython-313.pyc differ
 
database.sql ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- WARNING: This schema is for context only and is not meant to be run.
2
+ -- Table order and constraints may not be valid for execution.
3
+
4
+ CREATE TABLE public.attendee_preferences (
5
+ pref_id bigint GENERATED ALWAYS AS IDENTITY NOT NULL UNIQUE,
6
+ pref_name character varying NOT NULL DEFAULT ''::character varying,
7
+ CONSTRAINT attendee_preferences_pkey PRIMARY KEY (pref_id)
8
+ );
9
+ CREATE TABLE public.attendee_profiles (
10
+ user_id uuid NOT NULL UNIQUE,
11
+ first_name text NOT NULL,
12
+ last_name text NOT NULL,
13
+ phone_number text,
14
+ age_bracket text,
15
+ gender text,
16
+ marital_status text,
17
+ bio text,
18
+ social_links jsonb,
19
+ profile_image_url text,
20
+ preferences jsonb,
21
+ created_at timestamp with time zone DEFAULT now(),
22
+ updated_at timestamp with time zone DEFAULT now(),
23
+ email text,
24
+ CONSTRAINT attendee_profiles_pkey PRIMARY KEY (user_id),
25
+ CONSTRAINT attendee_profiles_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id)
26
+ );
27
+ CREATE TABLE public.event_attendance (
28
+ id bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
29
+ event_id bigint NOT NULL,
30
+ user_id uuid NOT NULL,
31
+ status USER-DEFINED NOT NULL,
32
+ created_at timestamp with time zone DEFAULT now(),
33
+ CONSTRAINT event_attendance_pkey PRIMARY KEY (id),
34
+ CONSTRAINT event_attendance_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id),
35
+ CONSTRAINT event_attendance_event_id_fkey FOREIGN KEY (event_id) REFERENCES public.events(id)
36
+ );
37
+ CREATE TABLE public.event_comments (
38
+ id bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
39
+ created_at timestamp with time zone NOT NULL DEFAULT now(),
40
+ commentor_id uuid DEFAULT auth.uid(),
41
+ event_id bigint,
42
+ comment_text character varying DEFAULT ''::character varying,
43
+ commentor_name character varying,
44
+ CONSTRAINT event_comments_pkey PRIMARY KEY (id),
45
+ CONSTRAINT event_comments_event_id_fkey FOREIGN KEY (event_id) REFERENCES public.events(id)
46
+ );
47
+ CREATE TABLE public.event_types (
48
+ type_id bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
49
+ type_name character varying NOT NULL DEFAULT ''::character varying,
50
+ type_image_url character varying NOT NULL DEFAULT ''::character varying,
51
+ CONSTRAINT event_types_pkey PRIMARY KEY (type_id)
52
+ );
53
+ CREATE TABLE public.events (
54
+ id bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
55
+ created_at timestamp with time zone NOT NULL DEFAULT now(),
56
+ name character varying DEFAULT ''::character varying,
57
+ description character varying DEFAULT ''::character varying,
58
+ type_id bigint,
59
+ organizer_email character varying DEFAULT ''::character varying,
60
+ organizer_contact_num character varying DEFAULT ''::character varying,
61
+ location USER-DEFINED,
62
+ starting_timestamp timestamp without time zone,
63
+ street_address character varying DEFAULT ''::character varying,
64
+ media jsonb DEFAULT '"[]"'::jsonb,
65
+ whos_allowed character varying DEFAULT '"Everyone"'::character varying,
66
+ organizer_id uuid DEFAULT auth.uid(),
67
+ type_name character varying DEFAULT ''::character varying,
68
+ ticket_price bigint,
69
+ ticket_link character varying,
70
+ organizer_name character varying DEFAULT ''::character varying,
71
+ CONSTRAINT events_pkey PRIMARY KEY (id),
72
+ CONSTRAINT events_type_id_fkey FOREIGN KEY (type_id) REFERENCES public.event_types(type_id)
73
+ );
74
+ CREATE TABLE public.follows (
75
+ id bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
76
+ followed_at timestamp with time zone NOT NULL DEFAULT now(),
77
+ follower_id uuid NOT NULL DEFAULT auth.uid(),
78
+ followee_id character varying DEFAULT ''::character varying,
79
+ CONSTRAINT follows_pkey PRIMARY KEY (id),
80
+ CONSTRAINT follows_follower_id_fkey FOREIGN KEY (follower_id) REFERENCES public.attendee_profiles(user_id)
81
+ );
82
+ CREATE TABLE public.notifications (
83
+ id integer NOT NULL DEFAULT nextval('notifications_id_seq'::regclass),
84
+ created_at timestamp with time zone DEFAULT now(),
85
+ title character varying NOT NULL,
86
+ message text NOT NULL,
87
+ audience character varying NOT NULL,
88
+ event_id integer,
89
+ user_ids ARRAY,
90
+ action_url text,
91
+ status character varying DEFAULT 'pending'::character varying,
92
+ sent_at timestamp with time zone,
93
+ read_count integer DEFAULT 0,
94
+ sender_id uuid,
95
+ metadata jsonb,
96
+ CONSTRAINT notifications_pkey PRIMARY KEY (id),
97
+ CONSTRAINT notifications_event_id_fkey FOREIGN KEY (event_id) REFERENCES public.events(id),
98
+ CONSTRAINT notifications_sender_id_fkey FOREIGN KEY (sender_id) REFERENCES auth.users(id)
99
+ );
100
+ CREATE TABLE public.organizer_company_profiles (
101
+ user_id uuid NOT NULL,
102
+ company_name text NOT NULL,
103
+ company_email text NOT NULL UNIQUE,
104
+ phone_number text NOT NULL,
105
+ contact_person_first_name text NOT NULL,
106
+ contact_person_last_name text NOT NULL,
107
+ contact_person_email text NOT NULL,
108
+ contact_person_phone_number text NOT NULL,
109
+ contact_person_designation text NOT NULL,
110
+ company_state text NOT NULL,
111
+ company_city text NOT NULL,
112
+ company_address text NOT NULL,
113
+ company_logo_url text,
114
+ company_bio text,
115
+ event_types jsonb DEFAULT '[]'::jsonb,
116
+ created_at timestamp with time zone DEFAULT now(),
117
+ updated_at timestamp with time zone DEFAULT now(),
118
+ social_links jsonb DEFAULT '[]'::jsonb,
119
+ CONSTRAINT organizer_company_profiles_pkey PRIMARY KEY (user_id),
120
+ CONSTRAINT organizer_company_profiles_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id)
121
+ );
122
+ CREATE TABLE public.organizer_individual_profiles (
123
+ user_id uuid NOT NULL,
124
+ first_name text NOT NULL,
125
+ last_name text NOT NULL,
126
+ email text NOT NULL UNIQUE,
127
+ phone_number text NOT NULL,
128
+ age_bracket text,
129
+ event_types jsonb DEFAULT '[]'::jsonb,
130
+ bio text,
131
+ profile_image_url text,
132
+ created_at timestamp with time zone DEFAULT now(),
133
+ updated_at timestamp with time zone DEFAULT now(),
134
+ social_links jsonb DEFAULT '[]'::jsonb,
135
+ CONSTRAINT organizer_individual_profiles_pkey PRIMARY KEY (user_id),
136
+ CONSTRAINT organizer_individual_profiles_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id)
137
+ );
138
+ CREATE TABLE public.spatial_ref_sys (
139
+ srid integer NOT NULL CHECK (srid > 0 AND srid <= 998999),
140
+ auth_name character varying,
141
+ auth_srid integer,
142
+ srtext character varying,
143
+ proj4text character varying,
144
+ CONSTRAINT spatial_ref_sys_pkey PRIMARY KEY (srid)
145
+ );
146
+ CREATE TABLE public.user_fcm_tokens (
147
+ user_id uuid NOT NULL,
148
+ fcm_token text NOT NULL,
149
+ updated_at timestamp without time zone DEFAULT now(),
150
+ CONSTRAINT user_fcm_tokens_pkey PRIMARY KEY (user_id),
151
+ CONSTRAINT user_fcm_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id)
152
+ );
153
+ CREATE TABLE public.user_notification_preferences (
154
+ user_id uuid NOT NULL,
155
+ new_events boolean DEFAULT true,
156
+ event_reminders boolean DEFAULT true,
157
+ event_updates boolean DEFAULT true,
158
+ event_cancellations boolean DEFAULT true,
159
+ organizer_updates boolean DEFAULT false,
160
+ updated_at timestamp without time zone DEFAULT now(),
161
+ CONSTRAINT user_notification_preferences_pkey PRIMARY KEY (user_id),
162
+ CONSTRAINT user_notification_preferences_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id)
163
+ );
164
+ CREATE TABLE public.user_notifications (
165
+ id uuid NOT NULL DEFAULT gen_random_uuid(),
166
+ user_id uuid,
167
+ title text NOT NULL,
168
+ body text NOT NULL,
169
+ type text NOT NULL,
170
+ priority text DEFAULT 'normal'::text,
171
+ event_id bigint,
172
+ organizer_id text,
173
+ image_url text,
174
+ data jsonb,
175
+ is_read boolean DEFAULT false,
176
+ is_actionable boolean DEFAULT false,
177
+ action_url text,
178
+ action_text text,
179
+ created_at timestamp without time zone DEFAULT now(),
180
+ CONSTRAINT user_notifications_pkey PRIMARY KEY (id),
181
+ CONSTRAINT user_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id),
182
+ CONSTRAINT user_notifications_event_id_fkey FOREIGN KEY (event_id) REFERENCES public.events(id)
183
+ );
index.html DELETED
@@ -1,19 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
main.py CHANGED
@@ -1,94 +1,290 @@
1
- from fastapi import FastAPI, HTTPException
2
- from pydantic import BaseModel
 
 
 
 
3
  from typing import List, Optional, Dict, Any
 
 
4
  import firebase_admin
5
  from firebase_admin import credentials, messaging
6
- import json
7
- import os
8
- from dotenv import load_dotenv
 
9
 
10
  # Load environment variables from .env file
11
  load_dotenv()
12
- # Load Firebase service account key
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  service_account_json = os.environ.get("FIREBASE_SERVICE_ACCOUNT_JSON")
14
  if not service_account_json:
 
15
  raise EnvironmentError("FIREBASE_SERVICE_ACCOUNT_JSON environment variable not set")
16
-
17
  service_account = json.loads(service_account_json)
18
-
19
- # Initialize Firebase Admin SDK
20
  cred = credentials.Certificate(service_account)
21
- firebase_admin.initialize_app(cred)
22
-
23
- # Create FastAPI app
24
- app = FastAPI(
25
- title="FCM Notification Server",
26
- description="A FastAPI server for sending Firebase Cloud Messaging notifications",
27
- version="1.0.0"
28
- )
29
-
30
- # Pydantic models for request/response
31
- class NotificationRequest(BaseModel):
32
- tokens: List[str]
33
- title: str
34
- body: str
35
- data: Optional[Dict[str, Any]] = {}
36
-
37
- class NotificationResponse(BaseModel):
38
- success: bool
39
- response: Optional[Dict[str, Any]] = None
40
- error: Optional[str] = None
41
-
42
- @app.get("/")
43
- async def root():
44
- """Root endpoint to check if the server is running"""
45
- return {"message": "🚀 FCM FastAPI Server is running!"}
46
-
47
- @app.post("/send", response_model=NotificationResponse)
48
- async def send_notification(request: NotificationRequest):
49
- """Send notification to multiple tokens"""
50
- try:
51
- if not request.tokens or len(request.tokens) == 0:
52
- raise HTTPException(status_code=400, detail="No tokens provided")
53
-
54
- # Create the message
55
- message = messaging.MulticastMessage(
56
- notification=messaging.Notification(
57
- title=request.title,
58
- body=request.body
59
- ),
60
- data=request.data or {},
61
- tokens=request.tokens
62
- )
63
-
64
- # Send the message
65
- response = messaging.send_each_for_multicast(message)
66
-
67
- return NotificationResponse(
68
- success=True,
69
- response={
70
- "success_count": response.success_count,
71
- "failure_count": response.failure_count,
72
- "responses": [
73
- {
74
- "success": resp.success,
75
- "message_id": resp.message_id if resp.success else None,
76
- "exception": str(resp.exception) if resp.exception else None
77
- }
78
- for resp in response.responses
79
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  }
81
- )
82
-
83
- except Exception as error:
84
- print(f"Error sending notification: {error}")
85
- raise HTTPException(status_code=500, detail=str(error))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
- @app.get("/health")
88
- async def health_check():
89
- """Health check endpoint"""
90
- return {"status": "healthy", "service": "FCM Notification Server"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
 
92
  if __name__ == "__main__":
93
- import uvicorn
94
- uvicorn.run(app, host="0.0.0.0", port=3000)
 
1
+
2
+ import os
3
+ import json
4
+ import time
5
+ import logging
6
+ from logging.handlers import RotatingFileHandler
7
  from typing import List, Optional, Dict, Any
8
+ from dotenv import load_dotenv
9
+ from supabase import create_client
10
  import firebase_admin
11
  from firebase_admin import credentials, messaging
12
+ import requests
13
+ from fastapi import FastAPI, HTTPException
14
+ from pydantic import BaseModel
15
+
16
 
17
  # Load environment variables from .env file
18
  load_dotenv()
19
+
20
+ # --- Logging configuration -------------------------------------------------
21
+ LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").upper()
22
+ LOG_FILE = os.environ.get("LOG_FILE", "send_notifications.log")
23
+ LOG_MAX_BYTES = int(os.environ.get("LOG_MAX_BYTES", 10 * 1024 * 1024)) # 10MB
24
+ LOG_BACKUP_COUNT = int(os.environ.get("LOG_BACKUP_COUNT", 5))
25
+
26
+ logger = logging.getLogger("send_notifications")
27
+ if not logger.handlers:
28
+ logger.setLevel(LOG_LEVEL)
29
+ # Console handler
30
+ ch = logging.StreamHandler()
31
+ ch.setLevel(LOG_LEVEL)
32
+ ch_formatter = logging.Formatter(
33
+ "%(asctime)s.%(msecs)03d %(levelname)s [%(name)s] %(message)s",
34
+ datefmt="%Y-%m-%d %H:%M:%S",
35
+ )
36
+ ch.setFormatter(ch_formatter)
37
+ logger.addHandler(ch)
38
+
39
+ # Rotating file handler
40
+ try:
41
+ fh = RotatingFileHandler(LOG_FILE, maxBytes=LOG_MAX_BYTES, backupCount=LOG_BACKUP_COUNT)
42
+ fh.setLevel(LOG_LEVEL)
43
+ fh.setFormatter(ch_formatter)
44
+ logger.addHandler(fh)
45
+ except Exception as e:
46
+ logger.warning("Failed to create file handler for logging: %s", e)
47
+
48
+ # Convenience alias for root logger
49
+ log = logger
50
+
51
+ # Supabase config
52
+ SUPABASE_URL = os.environ.get("SUPABASE_URL")
53
+ SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
54
+ if not SUPABASE_URL or not SUPABASE_KEY:
55
+ log.critical("SUPABASE_URL and SUPABASE_KEY must be set in environment variables")
56
+ raise EnvironmentError("SUPABASE_URL and SUPABASE_KEY must be set in environment variables")
57
+
58
+ # Initialize supabase client
59
+ supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
60
+
61
+ # Firebase config
62
  service_account_json = os.environ.get("FIREBASE_SERVICE_ACCOUNT_JSON")
63
  if not service_account_json:
64
+ log.critical("FIREBASE_SERVICE_ACCOUNT_JSON environment variable not set")
65
  raise EnvironmentError("FIREBASE_SERVICE_ACCOUNT_JSON environment variable not set")
 
66
  service_account = json.loads(service_account_json)
 
 
67
  cred = credentials.Certificate(service_account)
68
+ try:
69
+ firebase_admin.initialize_app(cred)
70
+ log.info("Initialized Firebase app with provided service account")
71
+ except Exception as e:
72
+ log.exception("Failed to initialize Firebase admin: %s", e)
73
+ raise
74
+
75
+
76
+ # --- Efficient Notification Service ---
77
+ def fetch_user_notifications():
78
+ """Fetch all pending user_notifications efficiently via Supabase client."""
79
+ log.debug("Fetching user_notifications from Supabase")
80
+ start = time.time()
81
+ resp = supabase.table("user_notifications").select("*").execute()
82
+ data = getattr(resp, "data", None) or (resp.get("data") if isinstance(resp, dict) else None)
83
+ error = getattr(resp, "error", None) or (resp.get("error") if isinstance(resp, dict) else None)
84
+ if error:
85
+ log.error("Failed to fetch user_notifications: %s", error)
86
+ raise RuntimeError(f"Failed to fetch user_notifications: {error}")
87
+ duration = time.time() - start
88
+ log.info("Fetched %d user_notifications in %.3fs", len(data or []), duration)
89
+ return data or []
90
+
91
+ def fetch_event_attendance_for_events(event_ids: List[int]) -> List[Dict[str, Any]]:
92
+ """Fetch attendance rows for multiple event_ids in one query."""
93
+ if not event_ids:
94
+ return []
95
+ log.debug("Fetching event_attendance for event_ids: %s", event_ids)
96
+ start = time.time()
97
+ resp = supabase.table("event_attendance").select("*").eq("event_id", 201).execute()
98
+ print(resp.data)
99
+
100
+ print(resp)
101
+ data = getattr(resp, "data", None) or (resp.get("data") if isinstance(resp, dict) else None)
102
+ error = getattr(resp, "error", None) or (resp.get("error") if isinstance(resp, dict) else None)
103
+ if error:
104
+ log.error("Failed to fetch event_attendance: %s", error)
105
+ raise RuntimeError(f"Failed to fetch event_attendance: {error}")
106
+ duration = time.time() - start
107
+ log.info("Fetched %d attendance rows for %d events in %.3fs", len(data or []), len(event_ids), duration)
108
+ return data or []
109
+
110
+ def fetch_fcm_tokens_for_users(user_ids: List[int]) -> List[Dict[str, Any]]:
111
+ """Fetch fcm tokens for multiple users in one query."""
112
+ if not user_ids:
113
+ return []
114
+ log.debug("Fetching FCM tokens for user_ids (count=%d)", len(user_ids))
115
+ start = time.time()
116
+ resp = (
117
+ supabase.table("user_fcm_tokens")
118
+ .select("user_id,fcm_token")
119
+ .in_("user_id", user_ids)
120
+ .execute()
121
+ )
122
+ data = getattr(resp, "data", None) or (resp.get("data") if isinstance(resp, dict) else None)
123
+ error = getattr(resp, "error", None) or (resp.get("error") if isinstance(resp, dict) else None)
124
+ if error:
125
+ log.error("Failed to fetch user_fcm_tokens: %s", error)
126
+ raise RuntimeError(f"Failed to fetch user_fcm_tokens: {error}")
127
+ duration = time.time() - start
128
+ log.info("Fetched %d FCM token rows in %.3fs", len(data or []), duration)
129
+ return data or []
130
+
131
+ def upsert_notifications(rows: List[Dict[str, Any]]):
132
+ if not rows:
133
+ return None
134
+ log.debug("Upserting %d notification rows", len(rows))
135
+ start = time.time()
136
+ resp = supabase.table("notifications").upsert(rows).execute()
137
+ data = getattr(resp, "data", None) or (resp.get("data") if isinstance(resp, dict) else None)
138
+ error = getattr(resp, "error", None) or (resp.get("error") if isinstance(resp, dict) else None)
139
+ if error:
140
+ log.error("Failed to upsert notifications: %s", error)
141
+ raise RuntimeError(f"Failed to upsert notifications: {error}")
142
+ duration = time.time() - start
143
+ log.info("Upserted %d notifications in %.3fs", len(data or []), duration)
144
+ return data
145
+
146
+ def delete_user_notifications_by_ids(ids: List[int]):
147
+ if not ids:
148
+ return None
149
+ log.debug("Deleting %d user_notifications", len(ids))
150
+ start = time.time()
151
+ resp = supabase.table("user_notifications").delete().in_("id", ids).execute()
152
+ data = getattr(resp, "data", None) or (resp.get("data") if isinstance(resp, dict) else None)
153
+ error = getattr(resp, "error", None) or (resp.get("error") if isinstance(resp, dict) else None)
154
+ if error:
155
+ log.error("Failed to delete user_notifications: %s", error)
156
+ raise RuntimeError(f"Failed to delete user_notifications: {error}")
157
+ duration = time.time() - start
158
+ log.info("Deleted %d user_notifications in %.3fs", len(data or []), duration)
159
+ return data
160
+
161
+ def aggregate_notifications(notifications):
162
+ agg = {}
163
+ for notif in notifications:
164
+ event_id = notif.get("event_id")
165
+ if not event_id:
166
+ continue
167
+ if event_id not in agg:
168
+ agg[event_id] = {
169
+ "title": notif["title"],
170
+ "body": notif["body"],
171
+ "type": notif["type"],
172
+ "priority": notif.get("priority", "normal"),
173
+ "event_id": event_id,
174
+ "user_ids": set(),
175
+ "ids": [],
176
+ "data": []
177
  }
178
+ agg[event_id]["user_ids"].add(notif["user_id"])
179
+ agg[event_id]["ids"].append(notif["id"])
180
+ agg[event_id]["data"].append(notif.get("data", {}))
181
+ log.debug("Aggregated notifications into %d events", len(agg))
182
+ return agg
183
+
184
+ def send_fcm(tokens, title, body, data):
185
+ if not tokens:
186
+ log.debug("No tokens provided to send_fcm")
187
+ return None
188
+ # Firebase FCM requires all values in the `data` dict to be strings.
189
+ # Ensure we coerce non-string values to strings and handle None.
190
+ def _sanitize_data(d):
191
+ if not d:
192
+ return {}
193
+ out = {}
194
+ for k, v in d.items():
195
+ # keys must be strings as well
196
+ key = str(k)
197
+ if v is None:
198
+ out[key] = ""
199
+ elif isinstance(v, str):
200
+ out[key] = v
201
+ else:
202
+ try:
203
+ out[key] = json.dumps(v, separators=(',', ':')) if isinstance(v, (dict, list)) else str(v)
204
+ except Exception:
205
+ out[key] = str(v)
206
+ return out
207
+
208
+ safe_data = _sanitize_data(data)
209
+
210
+ message = messaging.MulticastMessage(
211
+ notification=messaging.Notification(title=title, body=body),
212
+ data=safe_data,
213
+ tokens=tokens,
214
+ )
215
+ try:
216
+ log.info("Sending FCM multicast message to %d tokens", len(tokens))
217
+ res = messaging.send_each_for_multicast(message)
218
+ log.info("FCM multicast send result: %s", res)
219
+ return res
220
+ except Exception as e:
221
+ log.exception("Failed to send FCM multicast message: %s", e)
222
+ raise
223
+
224
+ def notification_service():
225
+ while True:
226
+ try:
227
+ # 1. Query user_notifications
228
+ notifications = fetch_user_notifications()
229
+ if not notifications:
230
+ log.debug("No notifications to process. Sleeping for 10 minutes.")
231
+ time.sleep(600)
232
+ continue
233
+
234
+ # 2. Aggregate by event_id
235
+ agg = aggregate_notifications(notifications)
236
+ # Batch fetch attendance for all events we need
237
+ event_ids = list(agg.keys())
238
+ attendance_rows = fetch_event_attendance_for_events(event_ids)
239
+ # build attendance_map: event_id -> [user_id]
240
+ attendance_map: Dict[int, List[int]] = {}
241
+ for r in attendance_rows:
242
+ eid = r.get("event_id")
243
+ uid = r.get("user_id")
244
+ if eid is None or uid is None:
245
+ continue
246
+ attendance_map.setdefault(eid, []).append(uid)
247
+
248
+ # Collect distinct user ids across all events and also include user_ids from notifications
249
+ all_user_ids = set()
250
+ for v in agg.values():
251
+ all_user_ids.update(v["user_ids"])
252
+ for ulist in attendance_map.values():
253
+ all_user_ids.update(ulist)
254
+ all_user_ids = list(all_user_ids)
255
+
256
+ # Batch fetch tokens
257
+ token_rows = fetch_fcm_tokens_for_users(all_user_ids)
258
+ token_map: Dict[int, str] = {r["user_id"]: r["fcm_token"] for r in token_rows}
259
 
260
+ for event_id, notif in agg.items():
261
+ # 3. Get attendees for event (from pre-fetched attendance_map)
262
+ user_ids = attendance_map.get(event_id, [])
263
+ # 4. Get FCM tokens for users (from pre-fetched token_map)
264
+ tokens = [token_map.get(uid) for uid in user_ids]
265
+ tokens = [t for t in tokens if t]
266
+ # 5. Send notification
267
+ data = {"event_id": event_id, "type": notif["type"], "priority": notif["priority"]}
268
+ response = send_fcm(tokens, notif["title"], notif["body"], data)
269
+ log.info("Sent notification for event %s to %d users. fcm_response=%s", event_id, len(tokens), getattr(response, 'success_count', response))
270
+ # 6. Upsert notification record
271
+ upsert_payload = [{
272
+ "title": notif["title"],
273
+ "message": notif["body"],
274
+ "audience": "attendees",
275
+ "event_id": event_id,
276
+ "user_ids": user_ids,
277
+ "status": "sent",
278
+ "metadata": data
279
+ }]
280
+ upsert_notifications(upsert_payload)
281
+ # 7. Delete processed notifications in bulk
282
+ delete_user_notifications_by_ids(notif["ids"])
283
+ log.debug("Deleted processed notifications for event %s.", event_id)
284
+ except Exception as e:
285
+ log.exception("Error in notification service main loop: %s", e)
286
+ time.sleep(600) # 10 minutes
287
 
288
+ # --- Run service if main ---
289
  if __name__ == "__main__":
290
+ notification_service()
 
requirements.txt CHANGED
@@ -7,6 +7,7 @@ cffi==2.0.0
7
  charset-normalizer==3.4.3
8
  click==8.3.0
9
  cryptography==46.0.1
 
10
  fastapi==0.117.1
11
  firebase_admin==7.1.0
12
  google-api-core==2.25.1
@@ -27,6 +28,8 @@ httpx==0.28.1
27
  hyperframe==6.1.0
28
  idna==3.10
29
  msgpack==1.1.1
 
 
30
  proto-plus==1.26.1
31
  protobuf==6.32.1
32
  pyasn1==0.6.1
@@ -36,11 +39,18 @@ pydantic==2.11.9
36
  pydantic_core==2.33.2
37
  PyJWT==2.10.1
38
  python-dotenv==1.1.1
 
39
  requests==2.32.5
40
  rsa==4.9.1
41
  sniffio==1.3.1
42
  starlette==0.48.0
 
 
 
 
 
43
  typing-inspection==0.4.1
44
  typing_extensions==4.15.0
45
  urllib3==2.5.0
46
  uvicorn==0.36.0
 
 
7
  charset-normalizer==3.4.3
8
  click==8.3.0
9
  cryptography==46.0.1
10
+ deprecation==2.1.0
11
  fastapi==0.117.1
12
  firebase_admin==7.1.0
13
  google-api-core==2.25.1
 
28
  hyperframe==6.1.0
29
  idna==3.10
30
  msgpack==1.1.1
31
+ packaging==25.0
32
+ postgrest==2.20.0
33
  proto-plus==1.26.1
34
  protobuf==6.32.1
35
  pyasn1==0.6.1
 
39
  pydantic_core==2.33.2
40
  PyJWT==2.10.1
41
  python-dotenv==1.1.1
42
+ realtime==2.20.0
43
  requests==2.32.5
44
  rsa==4.9.1
45
  sniffio==1.3.1
46
  starlette==0.48.0
47
+ storage3==2.20.0
48
+ StrEnum==0.4.15
49
+ supabase==2.20.0
50
+ supabase-auth==2.20.0
51
+ supabase-functions==2.20.0
52
  typing-inspection==0.4.1
53
  typing_extensions==4.15.0
54
  urllib3==2.5.0
55
  uvicorn==0.36.0
56
+ websockets==15.0.1
send_notifications.log ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 2025-09-29 20:55:32.648 INFO [send_notifications] Initialized Firebase app with provided service account
2
+ 2025-09-29 20:55:37.477 INFO [send_notifications] Fetched 0 user_notifications in 4.829s
3
+ 2025-09-29 20:56:31.327 INFO [send_notifications] Initialized Firebase app with provided service account
4
+ 2025-09-29 20:56:31.949 INFO [send_notifications] Fetched 0 user_notifications in 0.622s
5
+ 2025-09-29 20:57:02.298 INFO [send_notifications] Initialized Firebase app with provided service account
6
+ 2025-09-29 20:57:02.923 INFO [send_notifications] Fetched 3 user_notifications in 0.626s
7
+ 2025-09-29 20:57:03.203 INFO [send_notifications] Fetched 0 attendance rows for 1 events in 0.280s
8
+ 2025-09-29 20:57:03.465 INFO [send_notifications] Fetched 2 FCM token rows in 0.262s
9
+ 2025-09-29 20:57:03.465 INFO [send_notifications] Sent notification for event 201 to 0 users. fcm_response=None
10
+ 2025-09-29 20:57:03.723 INFO [send_notifications] Upserted 1 notifications in 0.258s
11
+ 2025-09-29 20:57:03.972 INFO [send_notifications] Deleted 3 user_notifications in 0.249s
12
+ 2025-09-29 20:58:56.714 INFO [send_notifications] Initialized Firebase app with provided service account
13
+ 2025-09-29 20:58:57.363 INFO [send_notifications] Fetched 0 user_notifications in 0.648s
14
+ 2025-09-29 20:59:12.770 INFO [send_notifications] Initialized Firebase app with provided service account
15
+ 2025-09-29 20:59:13.273 INFO [send_notifications] Fetched 3 user_notifications in 0.503s
16
+ 2025-09-29 20:59:13.519 INFO [send_notifications] Fetched 0 attendance rows for 1 events in 0.246s
17
+ 2025-09-29 20:59:13.775 INFO [send_notifications] Fetched 2 FCM token rows in 0.255s
18
+ 2025-09-29 20:59:13.775 INFO [send_notifications] Sent notification for event 201 to 0 users. fcm_response=None
19
+ 2025-09-29 20:59:14.015 INFO [send_notifications] Upserted 1 notifications in 0.240s
20
+ 2025-09-29 20:59:14.247 INFO [send_notifications] Deleted 3 user_notifications in 0.232s
21
+ 2025-09-29 21:00:16.562 INFO [send_notifications] Initialized Firebase app with provided service account
22
+ 2025-09-29 21:00:16.563 DEBUG [send_notifications] Fetching user_notifications from Supabase
23
+ 2025-09-29 21:00:17.159 INFO [send_notifications] Fetched 0 user_notifications in 0.596s
24
+ 2025-09-29 21:00:17.159 DEBUG [send_notifications] No notifications to process. Sleeping for 10 minutes.
25
+ 2025-09-29 21:00:34.273 INFO [send_notifications] Initialized Firebase app with provided service account
26
+ 2025-09-29 21:00:34.273 DEBUG [send_notifications] Fetching user_notifications from Supabase
27
+ 2025-09-29 21:00:34.804 INFO [send_notifications] Fetched 3 user_notifications in 0.531s
28
+ 2025-09-29 21:00:34.804 DEBUG [send_notifications] Aggregated notifications into 1 events
29
+ 2025-09-29 21:00:34.804 DEBUG [send_notifications] Fetching event_attendance for event_ids: [201]
30
+ 2025-09-29 21:00:35.048 INFO [send_notifications] Fetched 0 attendance rows for 1 events in 0.243s
31
+ 2025-09-29 21:00:35.048 DEBUG [send_notifications] Fetching FCM tokens for user_ids (count=3)
32
+ 2025-09-29 21:00:35.279 INFO [send_notifications] Fetched 2 FCM token rows in 0.231s
33
+ 2025-09-29 21:00:35.279 DEBUG [send_notifications] No tokens provided to send_fcm
34
+ 2025-09-29 21:00:35.279 INFO [send_notifications] Sent notification for event 201 to 0 users. fcm_response=None
35
+ 2025-09-29 21:00:35.279 DEBUG [send_notifications] Upserting 1 notification rows
36
+ 2025-09-29 21:00:35.529 INFO [send_notifications] Upserted 1 notifications in 0.250s
37
+ 2025-09-29 21:00:35.529 DEBUG [send_notifications] Deleting 3 user_notifications
38
+ 2025-09-29 21:00:35.790 INFO [send_notifications] Deleted 3 user_notifications in 0.261s
39
+ 2025-09-29 21:00:35.790 DEBUG [send_notifications] Deleted processed notifications for event 201.
40
+ 2025-09-29 21:03:09.066 INFO [send_notifications] Initialized Firebase app with provided service account
41
+ 2025-09-29 21:03:09.067 DEBUG [send_notifications] Fetching user_notifications from Supabase
42
+ 2025-09-29 21:03:10.465 INFO [send_notifications] Fetched 0 user_notifications in 1.398s
43
+ 2025-09-29 21:03:10.465 DEBUG [send_notifications] No notifications to process. Sleeping for 10 minutes.
44
+ 2025-09-29 21:03:27.018 INFO [send_notifications] Initialized Firebase app with provided service account
45
+ 2025-09-29 21:03:27.019 DEBUG [send_notifications] Fetching user_notifications from Supabase
46
+ 2025-09-29 21:03:27.528 INFO [send_notifications] Fetched 3 user_notifications in 0.509s
47
+ 2025-09-29 21:03:27.528 DEBUG [send_notifications] Aggregated notifications into 1 events
48
+ 2025-09-29 21:03:27.528 DEBUG [send_notifications] Fetching event_attendance for event_ids: [201]
49
+ 2025-09-29 21:03:27.783 INFO [send_notifications] Fetched 0 attendance rows for 1 events in 0.255s
50
+ 2025-09-29 21:03:27.783 DEBUG [send_notifications] Fetching FCM tokens for user_ids (count=3)
51
+ 2025-09-29 21:03:28.036 INFO [send_notifications] Fetched 2 FCM token rows in 0.253s
52
+ 2025-09-29 21:03:28.036 DEBUG [send_notifications] No tokens provided to send_fcm
53
+ 2025-09-29 21:03:28.036 INFO [send_notifications] Sent notification for event 201 to 0 users. fcm_response=None
54
+ 2025-09-29 21:03:28.036 DEBUG [send_notifications] Upserting 1 notification rows
55
+ 2025-09-29 21:03:28.281 INFO [send_notifications] Upserted 1 notifications in 0.245s
56
+ 2025-09-29 21:03:28.281 DEBUG [send_notifications] Deleting 3 user_notifications
57
+ 2025-09-29 21:03:28.533 INFO [send_notifications] Deleted 3 user_notifications in 0.252s
58
+ 2025-09-29 21:03:28.533 DEBUG [send_notifications] Deleted processed notifications for event 201.
59
+ 2025-09-29 21:07:57.944 INFO [send_notifications] Initialized Firebase app with provided service account
60
+ 2025-09-29 21:07:57.945 DEBUG [send_notifications] Fetching user_notifications from Supabase
61
+ 2025-09-29 21:07:58.647 INFO [send_notifications] Fetched 3 user_notifications in 0.702s
62
+ 2025-09-29 21:07:58.647 DEBUG [send_notifications] Aggregated notifications into 1 events
63
+ 2025-09-29 21:07:58.647 DEBUG [send_notifications] Fetching event_attendance for event_ids: [201]
64
+ 2025-09-29 21:07:58.903 INFO [send_notifications] Fetched 0 attendance rows for 1 events in 0.256s
65
+ 2025-09-29 21:07:58.903 DEBUG [send_notifications] Fetching FCM tokens for user_ids (count=3)
66
+ 2025-09-29 21:07:59.175 INFO [send_notifications] Fetched 2 FCM token rows in 0.272s
67
+ 2025-09-29 21:07:59.175 DEBUG [send_notifications] No tokens provided to send_fcm
68
+ 2025-09-29 21:07:59.175 INFO [send_notifications] Sent notification for event 201 to 0 users. fcm_response=None
69
+ 2025-09-29 21:07:59.175 DEBUG [send_notifications] Upserting 1 notification rows
70
+ 2025-09-29 21:07:59.416 INFO [send_notifications] Upserted 1 notifications in 0.241s
71
+ 2025-09-29 21:07:59.416 DEBUG [send_notifications] Deleting 3 user_notifications
72
+ 2025-09-29 21:07:59.657 INFO [send_notifications] Deleted 3 user_notifications in 0.241s
73
+ 2025-09-29 21:07:59.657 DEBUG [send_notifications] Deleted processed notifications for event 201.
74
+ 2025-09-29 21:09:07.318 INFO [send_notifications] Initialized Firebase app with provided service account
75
+ 2025-09-29 21:09:07.319 DEBUG [send_notifications] Fetching user_notifications from Supabase
76
+ 2025-09-29 21:09:07.895 INFO [send_notifications] Fetched 0 user_notifications in 0.576s
77
+ 2025-09-29 21:09:07.895 DEBUG [send_notifications] No notifications to process. Sleeping for 10 minutes.
78
+ 2025-09-29 21:09:21.071 INFO [send_notifications] Initialized Firebase app with provided service account
79
+ 2025-09-29 21:09:21.071 DEBUG [send_notifications] Fetching user_notifications from Supabase
80
+ 2025-09-29 21:09:21.591 INFO [send_notifications] Fetched 3 user_notifications in 0.520s
81
+ 2025-09-29 21:09:21.591 DEBUG [send_notifications] Aggregated notifications into 1 events
82
+ 2025-09-29 21:09:21.591 DEBUG [send_notifications] Fetching event_attendance for event_ids: [201]
83
+ 2025-09-29 21:09:21.834 INFO [send_notifications] Fetched 3 attendance rows for 1 events in 0.243s
84
+ 2025-09-29 21:09:21.834 DEBUG [send_notifications] Fetching FCM tokens for user_ids (count=3)
85
+ 2025-09-29 21:09:22.087 INFO [send_notifications] Fetched 2 FCM token rows in 0.253s
86
+ 2025-09-29 21:09:22.087 INFO [send_notifications] Sending FCM multicast message to 2 tokens
87
+ 2025-09-29 21:09:22.100 ERROR [send_notifications] Failed to send FCM multicast message: Message.data must not contain non-string values.
88
+ Traceback (most recent call last):
89
+ File "/home/muneeb/Projects/SendNotifications/main.py", line 195, in send_fcm
90
+ res = messaging.send_each_for_multicast(message)
91
+ File "/home/muneeb/Projects/SendNotifications/.venv/lib64/python3.13/site-packages/firebase_admin/messaging.py", line 240, in send_each_for_multicast
92
+ return _get_messaging_service(app).send_each(messages, dry_run)
93
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
94
+ File "/home/muneeb/Projects/SendNotifications/.venv/lib64/python3.13/site-packages/firebase_admin/messaging.py", line 449, in send_each
95
+ message_data = [self._message_data(message, dry_run) for message in messages]
96
+ ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
97
+ File "/home/muneeb/Projects/SendNotifications/.venv/lib64/python3.13/site-packages/firebase_admin/messaging.py", line 520, in _message_data
98
+ data = {'message': _MessagingService.encode_message(message)}
99
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
100
+ File "/home/muneeb/Projects/SendNotifications/.venv/lib64/python3.13/site-packages/firebase_admin/messaging.py", line 415, in encode_message
101
+ return cls.JSON_ENCODER.default(message)
102
+ ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
103
+ File "/home/muneeb/Projects/SendNotifications/.venv/lib64/python3.13/site-packages/firebase_admin/_messaging_encoder.py", line 692, in default
104
+ 'data': _Validators.check_string_dict('Message.data', o.data),
105
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
106
+ File "/home/muneeb/Projects/SendNotifications/.venv/lib64/python3.13/site-packages/firebase_admin/_messaging_encoder.py", line 128, in check_string_dict
107
+ raise ValueError(f'{label} must not contain non-string values.')
108
+ ValueError: Message.data must not contain non-string values.
109
+ 2025-09-29 21:09:22.105 ERROR [send_notifications] Error in notification service main loop: Message.data must not contain non-string values.
110
+ Traceback (most recent call last):
111
+ File "/home/muneeb/Projects/SendNotifications/main.py", line 246, in notification_service
112
+ response = send_fcm(tokens, notif["title"], notif["body"], data)
113
+ File "/home/muneeb/Projects/SendNotifications/main.py", line 195, in send_fcm
114
+ res = messaging.send_each_for_multicast(message)
115
+ File "/home/muneeb/Projects/SendNotifications/.venv/lib64/python3.13/site-packages/firebase_admin/messaging.py", line 240, in send_each_for_multicast
116
+ return _get_messaging_service(app).send_each(messages, dry_run)
117
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
118
+ File "/home/muneeb/Projects/SendNotifications/.venv/lib64/python3.13/site-packages/firebase_admin/messaging.py", line 449, in send_each
119
+ message_data = [self._message_data(message, dry_run) for message in messages]
120
+ ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
121
+ File "/home/muneeb/Projects/SendNotifications/.venv/lib64/python3.13/site-packages/firebase_admin/messaging.py", line 520, in _message_data
122
+ data = {'message': _MessagingService.encode_message(message)}
123
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
124
+ File "/home/muneeb/Projects/SendNotifications/.venv/lib64/python3.13/site-packages/firebase_admin/messaging.py", line 415, in encode_message
125
+ return cls.JSON_ENCODER.default(message)
126
+ ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
127
+ File "/home/muneeb/Projects/SendNotifications/.venv/lib64/python3.13/site-packages/firebase_admin/_messaging_encoder.py", line 692, in default
128
+ 'data': _Validators.check_string_dict('Message.data', o.data),
129
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
130
+ File "/home/muneeb/Projects/SendNotifications/.venv/lib64/python3.13/site-packages/firebase_admin/_messaging_encoder.py", line 128, in check_string_dict
131
+ raise ValueError(f'{label} must not contain non-string values.')
132
+ ValueError: Message.data must not contain non-string values.
133
+ 2025-09-29 21:10:25.942 INFO [send_notifications] Initialized Firebase app with provided service account
134
+ 2025-09-29 21:10:25.942 DEBUG [send_notifications] Fetching user_notifications from Supabase
135
+ 2025-09-29 21:10:26.563 INFO [send_notifications] Fetched 3 user_notifications in 0.621s
136
+ 2025-09-29 21:10:26.563 DEBUG [send_notifications] Aggregated notifications into 1 events
137
+ 2025-09-29 21:10:26.563 DEBUG [send_notifications] Fetching event_attendance for event_ids: [201]
138
+ 2025-09-29 21:10:26.840 INFO [send_notifications] Fetched 3 attendance rows for 1 events in 0.277s
139
+ 2025-09-29 21:10:26.841 DEBUG [send_notifications] Fetching FCM tokens for user_ids (count=3)
140
+ 2025-09-29 21:10:27.111 INFO [send_notifications] Fetched 2 FCM token rows in 0.271s
141
+ 2025-09-29 21:10:27.111 INFO [send_notifications] Sending FCM multicast message to 2 tokens
142
+ 2025-09-29 21:10:28.517 INFO [send_notifications] FCM multicast send result: <firebase_admin.messaging.BatchResponse object at 0x7f8f28d99400>
143
+ 2025-09-29 21:10:28.517 INFO [send_notifications] Sent notification for event 201 to 2 users. fcm_response=1
144
+ 2025-09-29 21:10:28.517 DEBUG [send_notifications] Upserting 1 notification rows
145
+ 2025-09-29 21:10:28.769 INFO [send_notifications] Upserted 1 notifications in 0.252s
146
+ 2025-09-29 21:10:28.769 DEBUG [send_notifications] Deleting 3 user_notifications
147
+ 2025-09-29 21:10:29.042 INFO [send_notifications] Deleted 3 user_notifications in 0.273s
148
+ 2025-09-29 21:10:29.042 DEBUG [send_notifications] Deleted processed notifications for event 201.
149
+ 2025-09-29 21:10:47.458 INFO [send_notifications] Initialized Firebase app with provided service account
150
+ 2025-09-29 21:10:47.459 DEBUG [send_notifications] Fetching user_notifications from Supabase
151
+ 2025-09-29 21:10:48.002 INFO [send_notifications] Fetched 3 user_notifications in 0.543s
152
+ 2025-09-29 21:10:48.002 DEBUG [send_notifications] Aggregated notifications into 1 events
153
+ 2025-09-29 21:10:48.002 DEBUG [send_notifications] Fetching event_attendance for event_ids: [201]
154
+ 2025-09-29 21:10:48.244 INFO [send_notifications] Fetched 3 attendance rows for 1 events in 0.242s
155
+ 2025-09-29 21:10:48.244 DEBUG [send_notifications] Fetching FCM tokens for user_ids (count=3)
156
+ 2025-09-29 21:10:48.504 INFO [send_notifications] Fetched 2 FCM token rows in 0.260s
157
+ 2025-09-29 21:10:48.504 INFO [send_notifications] Sending FCM multicast message to 2 tokens
158
+ 2025-09-29 21:10:49.836 INFO [send_notifications] FCM multicast send result: <firebase_admin.messaging.BatchResponse object at 0x7f3faf69d400>
159
+ 2025-09-29 21:10:49.836 INFO [send_notifications] Sent notification for event 201 to 2 users. fcm_response=1
160
+ 2025-09-29 21:10:49.836 DEBUG [send_notifications] Upserting 1 notification rows
161
+ 2025-09-29 21:10:50.094 INFO [send_notifications] Upserted 1 notifications in 0.257s
162
+ 2025-09-29 21:10:50.094 DEBUG [send_notifications] Deleting 3 user_notifications
163
+ 2025-09-29 21:10:50.331 INFO [send_notifications] Deleted 3 user_notifications in 0.238s
164
+ 2025-09-29 21:10:50.332 DEBUG [send_notifications] Deleted processed notifications for event 201.
server.js DELETED
@@ -1,43 +0,0 @@
1
- import express from "express";
2
- import bodyParser from "body-parser";
3
- import admin from "firebase-admin";
4
- import fs from "fs";
5
-
6
- // Load Firebase service account key
7
- const serviceAccount = JSON.parse(
8
- fs.readFileSync("./serviceAccountKey.json", "utf-8")
9
- );
10
-
11
- admin.initializeApp({
12
- credential: admin.credential.cert(serviceAccount),
13
- });
14
-
15
- const app = express();
16
- app.use(bodyParser.json());
17
-
18
- // Send notification to multiple tokens
19
- app.post("/send", async (req, res) => {
20
- try {
21
- const { tokens, title, body, data } = req.body;
22
-
23
- if (!tokens || tokens.length === 0) {
24
- return res.status(400).json({ error: "No tokens provided" });
25
- }
26
-
27
- const message = {
28
- notification: { title, body },
29
- data: data || {}, // custom key-value data
30
- tokens,
31
- };
32
-
33
- const response = await admin.messaging().sendEachForMulticast(message);
34
- res.json({ success: true, response });
35
- } catch (error) {
36
- console.error("Error sending notification:", error);
37
- res.status(500).json({ error: error.message });
38
- }
39
- });
40
-
41
- app.listen(3000, () => {
42
- console.log("🚀 FCM Server running on http://localhost:3000");
43
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
style.css DELETED
@@ -1,28 +0,0 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }