|
| 1 | +""" |
| 2 | +Canvas service functions for ResCanvas. |
| 3 | +""" |
| 4 | + |
| 5 | +import logging |
| 6 | +from .db import strokes_coll, redis_client |
| 7 | +from .canvas_counter import get_canvas_draw_count |
| 8 | +from .graphql_service import commit_transaction_via_graphql |
| 9 | +from config import SIGNER_PUBLIC_KEY, SIGNER_PRIVATE_KEY, RECIPIENT_PUBLIC_KEY |
| 10 | +import time |
| 11 | +import json |
| 12 | + |
| 13 | +logger = logging.getLogger(__name__) |
| 14 | + |
| 15 | + |
| 16 | +def _now_ms(): |
| 17 | + """Get current timestamp in milliseconds.""" |
| 18 | + return int(time.time() * 1000) |
| 19 | + |
| 20 | + |
| 21 | +def _persist_marker(id_value: str, value_field: str, value): |
| 22 | + """Persist a small marker object (id + value) into ResDB so it survives Redis flush.""" |
| 23 | + payload = { |
| 24 | + "operation": "CREATE", |
| 25 | + "amount": 1, |
| 26 | + "signerPublicKey": SIGNER_PUBLIC_KEY, |
| 27 | + "signerPrivateKey": SIGNER_PRIVATE_KEY, |
| 28 | + "recipientPublicKey": RECIPIENT_PUBLIC_KEY, |
| 29 | + "asset": { |
| 30 | + "data": { |
| 31 | + "id": id_value, |
| 32 | + value_field: value |
| 33 | + } |
| 34 | + } |
| 35 | + } |
| 36 | + try: |
| 37 | + commit_transaction_via_graphql(payload) |
| 38 | + except Exception: |
| 39 | + logger.exception("Failed to persist marker %s", id_value) |
| 40 | + |
| 41 | + |
| 42 | +def clear_canvas(room_id: str) -> dict: |
| 43 | + """ |
| 44 | + Clear all strokes from a canvas/room. |
| 45 | + |
| 46 | + This function: |
| 47 | + 1. Deletes all strokes from MongoDB for the room |
| 48 | + 2. Sets a clear timestamp marker in Redis and ResDB |
| 49 | + 3. Clears undo/redo stacks for the room |
| 50 | + |
| 51 | + Args: |
| 52 | + room_id: The room ID to clear |
| 53 | + |
| 54 | + Returns: |
| 55 | + dict: Status dictionary with success flag and metadata |
| 56 | + """ |
| 57 | + try: |
| 58 | + # Get current timestamp and draw count |
| 59 | + ts = _now_ms() |
| 60 | + try: |
| 61 | + res_draw_count = int(get_canvas_draw_count()) |
| 62 | + except Exception: |
| 63 | + logger.exception("Failed reading canvas draw count; defaulting to 0") |
| 64 | + res_draw_count = 0 |
| 65 | + |
| 66 | + # Delete all strokes from MongoDB for this room |
| 67 | + delete_result = strokes_coll.delete_many({"roomId": room_id}) |
| 68 | + deleted_count = delete_result.deleted_count |
| 69 | + logger.info(f"Deleted {deleted_count} strokes from room {room_id}") |
| 70 | + |
| 71 | + # Set clear timestamp markers in Redis |
| 72 | + redis_ts_cache_key = f"last-clear-ts:{room_id}" |
| 73 | + redis_ts_legacy = f"clear-canvas-timestamp:{room_id}" |
| 74 | + resdb_ts_id = f"clear-canvas-timestamp:{room_id}" |
| 75 | + redis_count_key = f"res-canvas-draw-count:{room_id}" |
| 76 | + redis_count_legacy = f"draw_count_clear_canvas:{room_id}" |
| 77 | + resdb_count_id = f"res-canvas-draw-count:{room_id}" |
| 78 | + |
| 79 | + try: |
| 80 | + redis_client.set(redis_ts_cache_key, ts) |
| 81 | + redis_client.set(redis_ts_legacy, ts) |
| 82 | + redis_client.set(redis_count_key, res_draw_count) |
| 83 | + redis_client.set(redis_count_legacy, res_draw_count) |
| 84 | + except Exception: |
| 85 | + logger.exception("Failed setting Redis keys for clear markers") |
| 86 | + |
| 87 | + # Persist markers to ResDB |
| 88 | + try: |
| 89 | + _persist_marker(resdb_count_id, "value", res_draw_count) |
| 90 | + _persist_marker(resdb_ts_id, "ts", ts) |
| 91 | + _persist_marker(redis_count_legacy, "value", res_draw_count) |
| 92 | + except Exception: |
| 93 | + logger.exception("Failed persisting clear markers to ResDB") |
| 94 | + |
| 95 | + # Clear undo/redo stacks for this room |
| 96 | + try: |
| 97 | + # Clear room-specific undo/redo keys |
| 98 | + for pattern in (f"{room_id}:*:undo", f"{room_id}:*:redo"): |
| 99 | + for key in redis_client.scan_iter(pattern): |
| 100 | + try: |
| 101 | + redis_client.delete(key) |
| 102 | + except Exception: |
| 103 | + pass |
| 104 | + |
| 105 | + # Clear generic undo/redo keys for this room |
| 106 | + for key in redis_client.scan_iter("undo-*"): |
| 107 | + try: |
| 108 | + data = redis_client.get(key) |
| 109 | + if not data: |
| 110 | + continue |
| 111 | + rec = json.loads(data) |
| 112 | + if rec.get("roomId") == room_id: |
| 113 | + redis_client.delete(key) |
| 114 | + except Exception: |
| 115 | + pass |
| 116 | + |
| 117 | + for key in redis_client.scan_iter("redo-*"): |
| 118 | + try: |
| 119 | + data = redis_client.get(key) |
| 120 | + if not data: |
| 121 | + continue |
| 122 | + rec = json.loads(data) |
| 123 | + if rec.get("roomId") == room_id: |
| 124 | + redis_client.delete(key) |
| 125 | + except Exception: |
| 126 | + pass |
| 127 | + except Exception: |
| 128 | + logger.exception("Failed clearing undo/redo stacks for room %s", room_id) |
| 129 | + |
| 130 | + return { |
| 131 | + "success": True, |
| 132 | + "room_id": room_id, |
| 133 | + "deleted_count": deleted_count, |
| 134 | + "timestamp": ts, |
| 135 | + "draw_count": res_draw_count |
| 136 | + } |
| 137 | + |
| 138 | + except Exception as e: |
| 139 | + logger.exception("Failed to clear canvas for room %s", room_id) |
| 140 | + return { |
| 141 | + "success": False, |
| 142 | + "error": str(e), |
| 143 | + "room_id": room_id |
| 144 | + } |
0 commit comments