From eb8c42d0c017a968ff60a7590404ba3d5a4f80f7 Mon Sep 17 00:00:00 2001 From: Tykayn Date: Fri, 26 Sep 2025 11:57:54 +0200 Subject: [PATCH] add live page --- LANCER_SERVEUR.md | 62 ++ Makefile | 11 +- backend.py | 6 + extractors/osm_cal.py | 294 ++++--- oedb/resources/demo.py | 122 ++- oedb/resources/demo/demo_main.py | 312 ++++++-- oedb/resources/demo/static/demo_styles.css | 102 +++ oedb/resources/demo/static/pouet.mp3 | 1 + oedb/resources/demo/static/social.js | 750 ++++++++++++++++++ oedb/resources/demo/static/traffic.js | 48 +- .../demo/templates/partials/demo_nav.html | 30 +- oedb/resources/demo/websocket.py | 337 ++++++++ oedb/resources/event_form.py | 2 + oedb/resources/live.py | 406 ++++++++++ oedb/resources/rss.py | 86 ++ test_osm_cal_api.py | 48 ++ test_osm_cal_ical.py | 128 +++ uwsgi.ini | 19 + wsgi_websocket.py | 194 +++++ 19 files changed, 2759 insertions(+), 199 deletions(-) create mode 100644 LANCER_SERVEUR.md create mode 100644 oedb/resources/demo/static/pouet.mp3 create mode 100644 oedb/resources/demo/static/social.js create mode 100644 oedb/resources/demo/websocket.py create mode 100644 oedb/resources/live.py create mode 100644 oedb/resources/rss.py create mode 100644 test_osm_cal_api.py create mode 100644 test_osm_cal_ical.py create mode 100644 uwsgi.ini create mode 100644 wsgi_websocket.py diff --git a/LANCER_SERVEUR.md b/LANCER_SERVEUR.md new file mode 100644 index 0000000..4142e93 --- /dev/null +++ b/LANCER_SERVEUR.md @@ -0,0 +1,62 @@ +# Lancement du serveur OEDB avec WebSockets + +Ce document explique comment lancer le serveur OEDB avec le support des WebSockets intégré via uWSGI. + +## Prérequis + +Assurez-vous d'avoir installé les dépendances nécessaires : + +```bash +pip install uwsgi +``` + +## Lancement du serveur + +Pour lancer le serveur avec le support WebSocket, vous avez plusieurs options : + +### Utiliser la commande make + +```bash +make websocket +``` + +Ou pour lancer en arrière-plan (mode démon) : + +```bash +make websocket-daemon +``` + +### Lancer manuellement avec uWSGI + +```bash +uwsgi --ini uwsgi.ini +``` + +Ces commandes démarreront le serveur sur le port 8080 avec le support WebSocket activé sur la route `/ws`. + +## Configuration en production + +En production, vous pouvez utiliser uWSGI avec Nginx. Voici un exemple de configuration Nginx : + +```nginx +server { + listen 80; + server_name votre-domaine.com; + + location / { + include uwsgi_params; + uwsgi_pass 127.0.0.1:8080; + } + + location /ws { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +Cette configuration permet d'utiliser Nginx comme proxy inverse pour les requêtes HTTP normales et les connexions WebSocket. diff --git a/Makefile b/Makefile index 3ccb394..5c7feb5 100644 --- a/Makefile +++ b/Makefile @@ -7,4 +7,13 @@ LOGFILE ?= uwsgi.log start: python3 -m venv venv - . venv/bin/activate && pip install -r requirements.txt && uwsgi --http :$(PORT) --wsgi-file backend.py --callable app \ No newline at end of file + . venv/bin/activate && pip install -r requirements.txt && uwsgi --http :$(PORT) --wsgi-file backend.py --callable app + +websocket: + python3 -m venv venv + . venv/bin/activate && pip install -r requirements.txt && pip install websockets && uwsgi --ini uwsgi.ini + +# Version en arrière-plan (démon) +websocket-daemon: + python3 -m venv venv + . venv/bin/activate && pip install -r requirements.txt && pip install websockets && uwsgi --ini uwsgi.ini --daemonize $(LOGFILE) \ No newline at end of file diff --git a/backend.py b/backend.py index 6b8d276..9e30b8e 100644 --- a/backend.py +++ b/backend.py @@ -25,6 +25,8 @@ from oedb.resources.stats import StatsResource from oedb.resources.search import EventSearch from oedb.resources.root import root from oedb.resources.demo import demo, demo_stats +from oedb.resources.live import live +from oedb.resources.rss import rss_latest, rss_by_family from oedb.resources.event_form import event_form def create_app(): @@ -86,9 +88,13 @@ def create_app(): app.add_route('/demo/by-what', demo, suffix='by_what') # Handle events by type page app.add_route('/demo/map-by-what', demo, suffix='map_by_what') # Handle map by event type page app.add_route('/demo/edit/{id}', demo, suffix='edit') # Handle event editing page + app.add_route('/demo/by_id/{id}', demo, suffix='by_id') # Handle view single event by id app.add_route('/demo/traffic', demo, suffix='traffic') # Handle traffic jam reporting page app.add_route('/demo/view-events', demo, suffix='view_events') # Handle view saved events page app.add_route('/demo/stats', demo_stats) # Handle stats by what page + app.add_route('/demo/live', live) # Live page + app.add_route('/rss', rss_latest) # RSS latest 200 + app.add_route('/rss/by/{family}', rss_by_family) # RSS by family logger.success("Application initialized successfully") return app diff --git a/extractors/osm_cal.py b/extractors/osm_cal.py index 2bd0d3a..663d3b2 100755 --- a/extractors/osm_cal.py +++ b/extractors/osm_cal.py @@ -3,18 +3,35 @@ OSM Calendar Extractor for the OpenEventDatabase. This script fetches events from the OpenStreetMap Calendar RSS feed -and adds them to the OpenEventDatabase if they don't already exist. +and adds them to the OpenEventDatabase via the API. + +For events that don't have geographic coordinates in the RSS feed but have a link +to an OSM Calendar event (https://osmcal.org/event/...), the script will fetch +the iCal version of the event and extract the coordinates and location from there. RSS Feed URL: https://osmcal.org/events.rss +API Endpoint: https://api.openeventdatabase.org/event + +Usage: + python osm_cal.py [--max-events MAX_EVENTS] [--offset OFFSET] + +Arguments: + --max-events MAX_EVENTS Maximum number of events to insert (default: 1) + --offset OFFSET Number of events to skip from the beginning of the RSS feed (default: 0) + +Examples: + # Insert the first event from the RSS feed + python osm_cal.py + + # Insert up to 5 events from the RSS feed + python osm_cal.py --max-events 5 + + # Skip the first 3 events and insert the next 2 + python osm_cal.py --offset 3 --max-events 2 Environment Variables: - DB_NAME: The name of the database (default: "oedb") - DB_HOST: The hostname of the database server (default: "localhost") - DB_USER: The username to connect to the database (default: "") - POSTGRES_PASSWORD: The password to connect to the database (default: None) - -These environment variables can be set in the system environment or in a .env file -in the project root directory. + These environment variables can be set in the system environment or in a .env file + in the project root directory. """ import json @@ -34,6 +51,8 @@ from oedb.utils.logging import logger # RSS Feed URL for OSM Calendar RSS_URL = "https://osmcal.org/events.rss" +# Base URL for OSM Calendar events +OSMCAL_EVENT_BASE_URL = "https://osmcal.org/event/" def fetch_osm_calendar_data(): """ @@ -179,6 +198,73 @@ def parse_event_dates(description): now = datetime.now() return (now.isoformat(), (now + timedelta(days=1)).isoformat()) +def fetch_ical_data(event_url): + """ + Fetch and parse iCal data for an OSM Calendar event. + + Args: + event_url (str): The URL of the OSM Calendar event. + + Returns: + tuple: A tuple containing (location_name, coordinates). + """ + try: + # Check if the URL is an OSM Calendar event URL + if not event_url.startswith(OSMCAL_EVENT_BASE_URL): + logger.warning(f"Not an OSM Calendar event URL: {event_url}") + return ("Unknown Location", [0, 0]) + + # Extract the event ID from the URL + event_id_match = re.search(r'event/(\d+)', event_url) + if not event_id_match: + logger.warning(f"Could not extract event ID from URL: {event_url}") + return ("Unknown Location", [0, 0]) + + event_id = event_id_match.group(1) + + # Construct the iCal URL + ical_url = f"{OSMCAL_EVENT_BASE_URL}{event_id}.ics" + + # Fetch the iCal content + logger.info(f"Fetching iCal data from: {ical_url}") + response = requests.get(ical_url) + + if not response.ok: + logger.warning(f"Failed to fetch iCal data: {response.status_code}") + return ("Unknown Location", [0, 0]) + + # Parse the iCal content + ical_content = response.text + + # Extract GEO information + geo_match = re.search(r'GEO:([-+]?\d+\.\d+);([-+]?\d+\.\d+)', ical_content) + if geo_match: + # GEO format is latitude;longitude + latitude = float(geo_match.group(2)) + longitude = float(geo_match.group(1)) + coordinates = [longitude, latitude] # GeoJSON uses [longitude, latitude] + logger.info(f"Extracted coordinates from iCal: {coordinates}") + else: + logger.warning(f"No GEO information found in iCal data for event: {event_id}") + coordinates = [0, 0] + + # Extract LOCATION information + location_match = re.search(r'LOCATION:(.+?)(?:\r\n|\n|\r)', ical_content) + if location_match: + location_name = location_match.group(1).strip() + # Unescape backslash-escaped characters (e.g., \, becomes ,) + location_name = re.sub(r'\\(.)', r'\1', location_name) + logger.info(f"Extracted location from iCal: {location_name}") + else: + logger.warning(f"No LOCATION information found in iCal data for event: {event_id}") + location_name = "Unknown Location" + + return (location_name, coordinates) + + except Exception as e: + logger.error(f"Error fetching or parsing iCal data: {e}") + return ("Unknown Location", [0, 0]) + def extract_location(description): """ Extract location information from the event description. @@ -239,9 +325,25 @@ def create_event(item): # Parse dates from the description start_date, end_date = parse_event_dates(description) - # Extract location information + # Extract location information from the description location_name, coordinates = extract_location(description) + # If we don't have coordinates and the link is to an OSM Calendar event, + # try to get coordinates and location from the iCal file + if coordinates == [0, 0] and link and link.startswith(OSMCAL_EVENT_BASE_URL): + logger.info(f"No coordinates found in description, trying to get from iCal: {link}") + ical_location_name, ical_coordinates = fetch_ical_data(link) + + # Use iCal coordinates if available + if ical_coordinates != [0, 0]: + coordinates = ical_coordinates + logger.info(f"Using coordinates from iCal: {coordinates}") + + # Use iCal location name if available and better than what we have + if ical_location_name != "Unknown Location": + location_name = ical_location_name + logger.info(f"Using location name from iCal: {location_name}") + # Create a descriptive label label = title @@ -325,7 +427,7 @@ def event_exists(db, properties): def submit_event(event): """ - Submit an event to the OpenEventDatabase. + Submit an event to the OpenEventDatabase using the API. Args: event: A GeoJSON Feature representing the event. @@ -334,129 +436,53 @@ def submit_event(event): bool: True if the event was successfully submitted, False otherwise. """ try: - # Connect to the database - db = db_connect() - - # Extract event properties + # Extract event properties for logging properties = event['properties'] - - # Check if the event already exists - if event_exists(db, properties): - logger.info(f"Skipping event '{properties.get('label')}' as it already exists") - db.close() - return False - - cur = db.cursor() - geometry = json.dumps(event['geometry']) - - print('event: ', event) - # Insert the geometry into the geo table - cur.execute(""" - INSERT INTO geo - SELECT geom, md5(st_astext(geom)) as hash, st_centroid(geom) as geom_center FROM - (SELECT st_setsrid(st_geomfromgeojson(%s),4326) as geom) as g - WHERE ST_IsValid(geom) - ON CONFLICT DO NOTHING RETURNING hash; - """, (geometry,)) - - # Get the geometry hash - hash_result = cur.fetchone() - - if hash_result is None: - # If the hash is None, check if the geometry already exists in the database - cur.execute(""" - SELECT hash FROM geo - WHERE hash = md5(st_astext(st_setsrid(st_geomfromgeojson(%s),4326))); - """, (geometry,)) - existing_hash = cur.fetchone() + + # API endpoint for OpenEventDatabase + api_url = "https://api.openeventdatabase.org/event" + + # Make the API request + logger.info(f"Submitting event '{properties.get('label')}' to API") + response = requests.post( + api_url, + headers={"Content-Type": "application/json"}, + data=json.dumps(event) + ) + + # Check if the request was successful + if response.status_code == 200 or response.status_code == 201: + # Parse the response to get the event ID + response_data = response.json() + event_id = response_data.get('id') - if existing_hash: - # Geometry already exists in the database, use its hash - geo_hash = existing_hash[0] - logger.info(f"Using existing geometry with hash: {geo_hash}") + if event_id: + logger.success(f"Event created with ID: {event_id}") + return True else: - # Geometry doesn't exist, try to insert it directly - cur.execute(""" - SELECT md5(st_astext(geom)) as hash, - ST_IsValid(geom), - ST_IsValidReason(geom) from (SELECT st_setsrid(st_geomfromgeojson(%s),4326) as geom) as g; - """, (geometry,)) - hash_result = cur.fetchone() - - if hash_result is None or not hash_result[1]: - logger.error(f"Invalid geometry for event: {properties.get('label')}") - if hash_result and len(hash_result) > 2: - logger.error(f"Reason: {hash_result[2]}") - db.close() - return False - - geo_hash = hash_result[0] - - # Now insert the geometry explicitly - cur.execute(""" - INSERT INTO geo (geom, hash, geom_center) - VALUES ( - st_setsrid(st_geomfromgeojson(%s),4326), - %s, - st_centroid(st_setsrid(st_geomfromgeojson(%s),4326)) - ) - ON CONFLICT (hash) DO NOTHING; - """, (geometry, geo_hash, geometry)) - - # Verify the geometry was inserted - cur.execute("SELECT 1 FROM geo WHERE hash = %s", (geo_hash,)) - if cur.fetchone() is None: - logger.error(f"Failed to insert geometry with hash: {geo_hash}") - db.close() - return False - - logger.info(f"Inserted new geometry with hash: {geo_hash}") + logger.warning(f"Event created but no ID returned in response") + return True else: - geo_hash = hash_result[0] - - # Determine the bounds for the time range - bounds = '[]' if properties['start'] == properties['stop'] else '[)' - - # Insert the event into the database - cur.execute(""" - INSERT INTO events (events_type, events_what, events_when, events_tags, events_geo) - VALUES (%s, %s, tstzrange(%s, %s, %s), %s, %s) - ON CONFLICT DO NOTHING RETURNING events_id; - """, ( - properties['type'], - properties['what'], - properties['start'], - properties['stop'], - bounds, - json.dumps(properties), - geo_hash - )) - - # Get the event ID - event_id = cur.fetchone() - - if event_id: - logger.success(f"Event created with ID: {event_id[0]}") - db.commit() - db.close() - return True - else: - logger.warning(f"Failed to create event: {properties.get('label')}") - db.close() + logger.warning(f"Failed to create event: {properties.get('label')}. Status code: {response.status_code}") + logger.warning(f"Response: {response.text}") return False except Exception as e: logger.error(f"Error submitting event: {e}") return False -def main(): +def main(max_events=1, offset=0): """ - Main function to fetch OSM Calendar events and add them to the database. + Main function to fetch OSM Calendar events and add them to the OpenEventDatabase API. + + Args: + max_events (int): Maximum number of events to insert (default: 1) + offset (int): Number of events to skip from the beginning of the RSS feed (default: 0) The function will exit if the .env file doesn't exist, as it's required - for database connection parameters. + for environment variables. """ - logger.info("Starting OSM Calendar extractor") + logger.info(f"Starting OSM Calendar extractor (max_events={max_events}, offset={offset})") # Load environment variables from .env file and check if it exists if not load_env_from_file(): @@ -472,20 +498,42 @@ def main(): logger.warning("No events found, exiting") return + # Apply offset and limit + if offset >= len(items): + logger.warning(f"Offset {offset} is greater than or equal to the number of events {len(items)}, no events to process") + return + + # Slice the items list according to offset and max_events + items_to_process = items[offset:offset + max_events] + logger.info(f"Processing {len(items_to_process)} events (offset={offset}, max_events={max_events})") + # Process each item success_count = 0 - for item in items: + for item in items_to_process: # Create an event from the item event = create_event(item) if not event: continue - # Submit the event to the database + # Submit the event to the API if submit_event(event): success_count += 1 - logger.success(f"Successfully added {success_count} out of {len(items)} events to the database") + logger.success(f"Successfully added {success_count} out of {len(items_to_process)} events to the OpenEventDatabase") if __name__ == "__main__": - main() \ No newline at end of file + import argparse + + # Set up command line argument parsing + parser = argparse.ArgumentParser(description='OSM Calendar Extractor for the OpenEventDatabase') + parser.add_argument('--max-events', type=int, default=1, + help='Maximum number of events to insert (default: 1)') + parser.add_argument('--offset', type=int, default=0, + help='Number of events to skip from the beginning of the RSS feed (default: 0)') + + # Parse arguments + args = parser.parse_args() + + # Run the main function with the provided arguments + main(max_events=args.max_events, offset=args.offset) \ No newline at end of file diff --git a/oedb/resources/demo.py b/oedb/resources/demo.py index cf35dfa..09730c2 100644 --- a/oedb/resources/demo.py +++ b/oedb/resources/demo.py @@ -59,6 +59,8 @@ class DemoResource: Edit Event - OpenEventDatabase + + + + +
+ +

Évènement {id}

+
+ + + + {''.join([f'' for k,v in sorted((feature.get('properties') or {{}}).items())])} + +
CléValeur
{k}{(v if not isinstance(v, dict) else str(v))}
+
+ + + + """ + resp.text = html + resp.status = falcon.HTTP_200 + logger.success(f"Successfully processed GET request to /demo/by_id/{id}") + except Exception as e: + logger.error(f"Error processing GET request to /demo/by_id/{id}: {e}") + resp.status = falcon.HTTP_500 + resp.text = f"Error: {str(e)}" + # Create a global instance of DemoResource demo = DemoResource() \ No newline at end of file diff --git a/oedb/resources/demo/demo_main.py b/oedb/resources/demo/demo_main.py index 3ea7702..a4012b3 100644 --- a/oedb/resources/demo/demo_main.py +++ b/oedb/resources/demo/demo_main.py @@ -50,6 +50,8 @@ class DemoMainResource: + + + + +
+

+ OEDB Live +

+
+ Période: 7 jours (rafraîchit chaque minute) + + +
+
+
+ +
+
+

Filtrer par type

+
+ + +
+
+ +
+
+
+
+
+ +
+
+

Arbre des familles d'évènements

+
+
+
+

Derniers évènements

+
+ + + + + + + + + + + + + + +
IDWhatLabelStartStopLonLat
+
+
+
+ + + + """ + resp.text = html + resp.status = falcon.HTTP_200 + + +live = LiveResource() + + diff --git a/oedb/resources/rss.py b/oedb/resources/rss.py new file mode 100644 index 0000000..3c903bd --- /dev/null +++ b/oedb/resources/rss.py @@ -0,0 +1,86 @@ +""" +RSS feeds for recent events and by family. +""" + +import falcon +import html +from datetime import datetime +from oedb.utils.logging import logger +from oedb.utils.db import db_connect + + +def _rss_header(title: str, link: str, desc: str) -> str: + return f""" + + + {html.escape(title)} + {html.escape(link)} + {html.escape(desc)} +""" + + +def _rss_footer() -> str: + return """ + + +""" + + +def _row_to_item(row) -> str: + events_id, events_tags, createdate = row + title = html.escape(str(events_tags.get('label') or events_tags.get('what') or f"event {events_id}")) + link = f"https://api.openeventdatabase.org/event/{events_id}" + guid = link + pubdate = datetime.fromisoformat(str(createdate)).strftime('%a, %d %b %Y %H:%M:%S %z') if createdate else '' + description = html.escape(str(events_tags)) + return f""" + + {title} + {link} + {guid} + {pubdate} + {description} + +""" + + +class RSSLatestResource: + def on_get(self, req, resp): + logger.info("Processing GET /rss") + resp.content_type = 'application/rss+xml; charset=utf-8' + db = db_connect() + cur = db.cursor() + cur.execute(""" + SELECT events_id, events_tags, createdate + FROM events + ORDER BY createdate DESC + LIMIT 200 + """) + items = ''.join(_row_to_item(r) for r in cur.fetchall()) + xml = _rss_header('OEDB - Latest events', 'https://api.openeventdatabase.org/event', 'Latest 200 events') + items + _rss_footer() + resp.text = xml + + +class RSSByFamilyResource: + def on_get(self, req, resp, family: str): + logger.info(f"Processing GET /rss/by/{family}") + resp.content_type = 'application/rss+xml; charset=utf-8' + db = db_connect() + cur = db.cursor() + like = family + '%' + cur.execute(""" + SELECT events_id, events_tags, createdate + FROM events + WHERE events_what LIKE %s + ORDER BY createdate DESC + LIMIT 200 + """, (like,)) + items = ''.join(_row_to_item(r) for r in cur.fetchall()) + xml = _rss_header(f'OEDB - {family}', f'https://api.openeventdatabase.org/event?what={family}', f'Latest 200 events in {family}') + items + _rss_footer() + resp.text = xml + + +rss_latest = RSSLatestResource() +rss_by_family = RSSByFamilyResource() + + diff --git a/test_osm_cal_api.py b/test_osm_cal_api.py new file mode 100644 index 0000000..3c8a220 --- /dev/null +++ b/test_osm_cal_api.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +""" +Test script for the OSM Calendar extractor with different parameter combinations. +This script tests the functionality of the osm_cal.py script with various +combinations of max_events and offset parameters. +""" + +import sys +import os +from extractors.osm_cal import main as osm_cal_main + +def run_test(max_events, offset): + """ + Run the OSM Calendar extractor with the specified parameters. + + Args: + max_events (int): Maximum number of events to insert + offset (int): Number of events to skip from the beginning of the RSS feed + """ + print(f"\n=== Testing with max_events={max_events}, offset={offset} ===") + osm_cal_main(max_events=max_events, offset=offset) + print("=== Test completed ===\n") + +def main(): + """ + Run tests with different parameter combinations. + """ + print("Starting OSM Calendar API tests...") + + # Test 1: Default parameters (max_events=1, offset=0) + run_test(1, 0) + + # Test 2: Multiple events (max_events=3, offset=0) + run_test(3, 0) + + # Test 3: With offset (max_events=2, offset=2) + run_test(2, 2) + + # Test 4: Large offset (max_events=1, offset=10) + run_test(1, 10) + + # Test 5: Large max_events (max_events=10, offset=0) + run_test(10, 0) + + print("All tests completed.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_osm_cal_ical.py b/test_osm_cal_ical.py new file mode 100644 index 0000000..f231704 --- /dev/null +++ b/test_osm_cal_ical.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +""" +Test script for the OSM Calendar extractor's iCal functionality. + +This script tests the ability of the osm_cal.py script to extract +geographic coordinates and location information from iCal files +for events that don't have this information in the RSS feed. +""" + +import sys +import os +from extractors.osm_cal import fetch_ical_data, OSMCAL_EVENT_BASE_URL +from oedb.utils.logging import logger + +def test_fetch_ical_data(): + """ + Test the fetch_ical_data function with a real OSM Calendar event. + """ + print("\n=== Testing fetch_ical_data function ===") + + # Test with a real OSM Calendar event URL + # Note: This event ID might not exist in the future, so the test might fail + event_url = f"{OSMCAL_EVENT_BASE_URL}3973" + + print(f"Fetching iCal data for event: {event_url}") + location_name, coordinates = fetch_ical_data(event_url) + + print(f"Location name: {location_name}") + print(f"Coordinates: {coordinates}") + + if coordinates != [0, 0]: + print("✅ Successfully extracted coordinates from iCal") + else: + print("❌ Failed to extract coordinates from iCal") + + if location_name != "Unknown Location": + print("✅ Successfully extracted location name from iCal") + else: + print("❌ Failed to extract location name from iCal") + + print("=== Test completed ===\n") + + return coordinates != [0, 0] and location_name != "Unknown Location" + +def test_with_invalid_url(): + """ + Test the fetch_ical_data function with an invalid URL. + """ + print("\n=== Testing fetch_ical_data with invalid URL ===") + + # Test with an invalid event URL + event_url = f"{OSMCAL_EVENT_BASE_URL}999999999" + + print(f"Fetching iCal data for invalid event: {event_url}") + location_name, coordinates = fetch_ical_data(event_url) + + print(f"Location name: {location_name}") + print(f"Coordinates: {coordinates}") + + if coordinates == [0, 0]: + print("✅ Correctly returned default coordinates for invalid URL") + else: + print("❌ Unexpectedly returned coordinates for invalid URL") + + if location_name == "Unknown Location": + print("✅ Correctly returned default location name for invalid URL") + else: + print("❌ Unexpectedly returned location name for invalid URL") + + print("=== Test completed ===\n") + + return coordinates == [0, 0] and location_name == "Unknown Location" + +def test_with_non_osmcal_url(): + """ + Test the fetch_ical_data function with a non-OSM Calendar URL. + """ + print("\n=== Testing fetch_ical_data with non-OSM Calendar URL ===") + + # Test with a non-OSM Calendar URL + event_url = "https://example.com/event/123" + + print(f"Fetching iCal data for non-OSM Calendar URL: {event_url}") + location_name, coordinates = fetch_ical_data(event_url) + + print(f"Location name: {location_name}") + print(f"Coordinates: {coordinates}") + + if coordinates == [0, 0]: + print("✅ Correctly returned default coordinates for non-OSM Calendar URL") + else: + print("❌ Unexpectedly returned coordinates for non-OSM Calendar URL") + + if location_name == "Unknown Location": + print("✅ Correctly returned default location name for non-OSM Calendar URL") + else: + print("❌ Unexpectedly returned location name for non-OSM Calendar URL") + + print("=== Test completed ===\n") + + return coordinates == [0, 0] and location_name == "Unknown Location" + +def main(): + """ + Run all tests. + """ + print("Starting OSM Calendar iCal tests...") + + # Run tests + test1 = test_fetch_ical_data() + test2 = test_with_invalid_url() + test3 = test_with_non_osmcal_url() + + # Print summary + print("\n=== Test Summary ===") + print(f"Test 1 (fetch_ical_data): {'PASSED' if test1 else 'FAILED'}") + print(f"Test 2 (invalid URL): {'PASSED' if test2 else 'FAILED'}") + print(f"Test 3 (non-OSM Calendar URL): {'PASSED' if test3 else 'FAILED'}") + + if test1 and test2 and test3: + print("\n✅ All tests PASSED") + else: + print("\n❌ Some tests FAILED") + + print("All tests completed.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/uwsgi.ini b/uwsgi.ini new file mode 100644 index 0000000..4eaf823 --- /dev/null +++ b/uwsgi.ini @@ -0,0 +1,19 @@ +[uwsgi] +http = :8080 +http-websockets = true +wsgi-file = wsgi_websocket.py +chdir = . +virtualenv = venv +async = 100 +ugreen = true +processes = 2 +threads = 4 +master = true +vacuum = true +die-on-term = true +pythonpath = . +buffer-size = 65535 +harakiri = 30 +max-requests = 1000 +socket-timeout = 120 +log-date = true diff --git a/wsgi_websocket.py b/wsgi_websocket.py new file mode 100644 index 0000000..56b1827 --- /dev/null +++ b/wsgi_websocket.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +""" +Configuration uWSGI pour l'intégration des WebSockets avec OEDB +""" + +import os +import sys +import json +import time +import asyncio +from oedb.utils.logging import logger + +# Import l'application Falcon principale +sys.path.insert(0, os.path.dirname(__file__)) +from backend import app + +# Import le gestionnaire WebSocket +from oedb.resources.demo.websocket import WebSocketManager + +# Créer une instance du gestionnaire WebSocket +ws_manager = WebSocketManager() + +# Gestionnaire de WebSocket pour uWSGI +def websocket_handler(environ, start_response): + """Gestionnaire pour les connexions WebSocket.""" + path = environ.get('PATH_INFO', '') + + # Vérifier si c'est une requête WebSocket et qu'elle est sur le bon chemin + if path == '/ws' and environ.get('HTTP_UPGRADE', '').lower() == 'websocket': + try: + # Importer uwsgi ici car il n'est disponible qu'à l'exécution dans l'environnement uWSGI + import uwsgi + + # Passer la requête au gestionnaire de WebSocket uWSGI + uwsgi.websocket_handshake() + logger.info(f"Nouvelle connexion WebSocket établie") + + client_id = id(environ) + # Simuler la fonction handle_connection pour ce client + try: + # Ajouter le client à la liste + with ws_manager.lock: + ws_manager.clients[client_id] = { + 'websocket': environ, # Utiliser environ comme proxy + 'username': None, + 'position': None, + 'last_seen': time.time(), + 'show_only_to_friends': False, + } + + # Envoyer la liste des utilisateurs connectés + users_list = [] + with ws_manager.lock: + for client_info in ws_manager.clients.values(): + if client_info.get('username'): + users_list.append({ + 'username': client_info['username'], + 'timestamp': time.time() + }) + + # Envoyer la liste des utilisateurs + uwsgi.websocket_send(json.dumps({ + 'type': 'users', + 'users': users_list + }).encode('utf-8')) + + # Boucle de traitement des messages + while True: + msg = uwsgi.websocket_recv() + data = json.loads(msg.decode('utf-8')) + message_type = data.get('type') + + if message_type == 'position': + # Traiter la mise à jour de position + username = data.get('username') + position = data.get('position') + timestamp = data.get('timestamp') + show_only_to_friends = data.get('showOnlyToFriends', False) + + # Mettre à jour les informations du client + with ws_manager.lock: + if client_id in ws_manager.clients: + ws_manager.clients[client_id]['username'] = username + ws_manager.clients[client_id]['position'] = position + ws_manager.clients[client_id]['last_seen'] = time.time() + ws_manager.clients[client_id]['show_only_to_friends'] = show_only_to_friends + + # Mettre à jour dans le dictionnaire des positions + ws_manager.positions[username] = { + 'position': position, + 'timestamp': timestamp, + 'show_only_to_friends': show_only_to_friends + } + + # Diffuser à tous les autres clients + broadcast_position(username, position, timestamp, show_only_to_friends) + + elif message_type in ['pouet', 'friendRequest']: + # Rediriger ces messages vers les destinataires + handle_direct_message(data, message_type) + + except Exception as e: + logger.error(f"Erreur WebSocket: {e}") + finally: + # Supprimer le client de la liste + with ws_manager.lock: + if client_id in ws_manager.clients: + username = ws_manager.clients[client_id].get('username') + if username and username in ws_manager.positions: + del ws_manager.positions[username] + del ws_manager.clients[client_id] + + except Exception as e: + logger.error(f"Erreur de connexion WebSocket: {e}") + return ['500 Internal Server Error', [], [b'WebSocket Error']] + + # Si ce n'est pas une requête WebSocket, passer à l'application WSGI + return app(environ, start_response) + +# Fonctions utilitaires pour les WebSockets uWSGI +def broadcast_position(username, position, timestamp, show_only_to_friends): + """Diffuse la position d'un utilisateur à tous les autres utilisateurs.""" + import uwsgi + + message = json.dumps({ + 'type': 'position', + 'username': username, + 'position': position, + 'timestamp': timestamp, + 'showOnlyToFriends': show_only_to_friends + }).encode('utf-8') + + with ws_manager.lock: + for client_id, client_info in ws_manager.clients.items(): + # Ne pas envoyer à l'utilisateur lui-même + if client_info.get('username') == username: + continue + + try: + # Dans uWSGI, nous devons utiliser environ pour identifier le socket + if 'websocket' in client_info: + uwsgi.websocket_send(message) + except Exception as e: + logger.error(f"Erreur d'envoi de broadcast de position: {e}") + +def handle_direct_message(data, message_type): + """Traite un message direct (pouet ou demande d'ami).""" + import uwsgi + + from_user = data.get('from') + to_user = data.get('to') + + if not from_user or not to_user: + return + + # Trouver le client destinataire + recipient_client_id = None + with ws_manager.lock: + for client_id, client_info in ws_manager.clients.items(): + if client_info.get('username') == to_user: + recipient_client_id = client_id + break + + if recipient_client_id and recipient_client_id in ws_manager.clients: + # Préparer le message selon le type + if message_type == 'pouet': + msg = json.dumps({ + 'type': 'pouet', + 'from': from_user, + 'timestamp': data.get('timestamp') + }).encode('utf-8') + logger.info(f"Pouet pouet envoyé de {from_user} à {to_user}") + elif message_type == 'friendRequest': + msg = json.dumps({ + 'type': 'friendRequest', + 'from': from_user, + 'timestamp': data.get('timestamp') + }).encode('utf-8') + logger.info(f"Demande d'ami envoyée de {from_user} à {to_user}") + + # Envoyer le message + try: + uwsgi.websocket_send(msg) + except Exception as e: + logger.error(f"Erreur d'envoi de message direct: {e}") + +# Appliquer le patch au démarrage +patch_websocket_manager() + +# Application WSGI pour uWSGI +application = websocket_handler + +# Remarque: Pour utiliser ce fichier, démarrez uWSGI avec: +# uwsgi --http :8080 --http-websockets --wsgi-file wsgi_websocket.py --async 100 --ugreen