Skip to content

Instantly share code, notes, and snippets.

@JakubAndrysek
Last active August 13, 2025 21:39
Show Gist options
  • Select an option

  • Save JakubAndrysek/5fd7a5013edf5c10b51e0d21c6972610 to your computer and use it in GitHub Desktop.

Select an option

Save JakubAndrysek/5fd7a5013edf5c10b51e0d21c6972610 to your computer and use it in GitHub Desktop.
MapyCzCom Python API.md

FastRPC-binary-protocol-specification

Other links

# mapy_frpc_client.py
# Simple working FRPC client for Mapy.cz API

import requests
from typing import Dict, Optional
# from config_pro import MAPY_COOKIES, DEFAULT_FOLDER_ID
from config import MAPY_COOKIES, DEFAULT_FOLDER_ID


class MapyFRPCClient:
    def __init__(
        self,
        cookies: Dict[str, str],
        base_url: str = "https://pro.mapy.com/mapybox-ng/",
    ):
        self.base_url = base_url
        self.cookies = cookies
        self.headers = {
            "accept": "application/x-frpc",
            "content-type": "application/x-frpc",
            "origin": "https://mapy.com",
            "referer": "https://mapy.com/",
            "user-agent": "Mozilla/5.0",
            "x-correlation-id": "python-client-123",
        }

    def send_frpc_payload(self, payload: bytes) -> Optional[Dict[str, str]]:
        """Send FRPC payload and parse response."""
        try:
            response = requests.post(
                self.base_url, headers=self.headers, cookies=self.cookies, data=payload
            )
            if response.ok:
                return self._parse_response(response.content)
            else:
                return {
                    "error": response.status_code,
                    "raw": response.content.hex(),
                }
        except Exception as e:
            return {"exception": str(e)}

    def _parse_response(self, content: bytes) -> Dict[str, str]:
        """Parse FRPC response to extract status, message, and IDs."""
        result = {}

        try:
            # Extract status if present
            if b"status" in content:
                status_idx = content.index(b"status") + len("status")
                if status_idx < len(content):
                    result["status"] = content[status_idx]

            # Extract statusMessage if present
            if b"statusMessage" in content:
                start = content.index(b"statusMessage") + len("statusMessage")
                # Skip control bytes
                while start < len(content) and content[start] < 32:
                    start += 1

                # Find end of message
                end = start
                while (
                    end < len(content)
                    and content[end] >= 32
                    and content[end] != ord("%")
                ):
                    end += 1

                if end > start:
                    msg = content[start:end].decode("utf-8", errors="ignore")
                    result["message"] = msg.strip()

            # Look for folder/point ID in response
            if b"id" in content:
                # Find potential ID locations
                id_idx = content.find(b"id")
                while id_idx != -1:
                    # Try to extract ID after this position
                    search_start = id_idx + 2
                    # Look for 24-character hex strings (typical IDs)
                    end_search = min(search_start + 50, len(content))
                    for i in range(search_start, end_search):
                        candidate = content[i : i + 24]
                        if len(candidate) == 24 and candidate.isalnum():
                            result["id"] = candidate.decode("ascii")
                            break
                    id_idx = content.find(b"id", id_idx + 1)

        except Exception as e:
            result["parse_error"] = str(e)

        return result if result else {"raw": content.hex()}

    def create_point(
        self,
        title: str,
        usertitle: str,
        lat: float,
        lon: float,
        subtitle: str = "",
        parent_id: str = "",
        comment: str = "",
    ) -> Optional[Dict[str, str]]:
        """Create a point using the exact working binary payload format."""
        import struct

        def float64_bytes(f):
            return struct.pack("<d", f)

        # This replicates the exact working curl payload format
        # Based on: $'Ê\u0011\u0002\u0001h\u000blike.createP\u0007\u0004type \u0005point\u0005title \u000fPozice na mapÄ\u009b\u000bdescription \u0000\u0004markP\u0002\u0003lat\u0018*±åk¶\u0087H@\u0003lon\u0018 z\u001dx\u0005Å0@\u0009usertitle \u0009SomePoint\u0007visited\u0010\u0009parent_id \uA4fdsfsdfdsf....'

        payload = (
            b"\xca\x11\x02\x01h\x0blike.createP\x07"  # Header + method + 7 params
            + b"\x04type "  # Field name "type" (4 chars) + space
            + b"\x05point"  # Value "point" (5 chars)
            + b"\x05title "  # Field name "title" (5 chars) + space
            + bytes([len(title)])
            + title.encode("utf-8")  # Title
            + b"\x0bdescription "  # Field name "description" (11 chars) + space
            + (
                bytes([len(comment)]) + comment.encode("utf-8")
                if comment
                else b"\x00"
            )  # Comment or empty
            + b"\x04markP\x02"  # Field name "mark" + struct with 2 members
            + b"\x03lat"  # Field name "lat" (3 chars)
            + b"\x18"
            + float64_bytes(lat)  # Double type + lat value
            + b"\x03lon"  # Field name "lon" (3 chars)
            + b"\x18"
            + float64_bytes(lon)  # Double type + lon value
            + b"\tusertitle "  # Field name "usertitle" (9 chars = \t) + space
            + bytes([len(usertitle)])
            + usertitle.encode("utf-8")  # User title
            + b"\x07visited"  # Field name "visited" (7 chars)
            + b"\x10"  # Some visited value
            + b"\tparent_id "  # Field name "parent_id" (9 chars = \t) + space
            + bytes([len(parent_id)])
            + parent_id.encode("utf-8")  # Parent ID
        )
        return self.send_frpc_payload(payload)


# Example usage:
if __name__ == "__main__":
    client = MapyFRPCClient(cookies=MAPY_COOKIES)

    # Use existing folder ID
    existing_folder_id = DEFAULT_FOLDER_ID

    print("Testing point creation in existing folder...")
    point_result = client.create_point(
        title="Python Test Point",
        usertitle="TestPoint",
        lat=49.1951,
        lon=16.6068,
        parent_id=existing_folder_id,
        comment="This is a test comment",
    )
    print("Point result:", point_result)

    # Test with different coordinates
    print("\nCreating another point...")
    point_result2 = client.create_point(
        title="Second Python Point",
        usertitle="SecondPoint",
        lat=50.0755,
        lon=14.4378,  # Prague coordinates
        parent_id=existing_folder_id,
        comment="Prague location",
    )
    print("Point result2:", point_result2)

config.py

# config.py

MAPY_COOKIES = {
    "ds": "123fdsfnjk32...",
}

# Default folder for operations - your existing folder
DEFAULT_FOLDER_ID = "123fdsfnjk32..."

# Parent folder ID from your curl command (seems to be a different folder)
PARENT_FOLDER_ID = "123fdsfnjk32..."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment