mirror of
https://github.com/NohamR/RMHook.git
synced 2026-04-08 07:59:58 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03b2b4c794 | ||
|
|
0db8a14ef7 |
@@ -114,24 +114,13 @@ endif()
|
|||||||
if(BUILD_MODE_QMLREBUILD)
|
if(BUILD_MODE_QMLREBUILD)
|
||||||
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_QMLREBUILD=1)
|
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_QMLREBUILD=1)
|
||||||
|
|
||||||
# Enable Qt MOC for MessageBroker (HttpServer is pure Obj-C/Foundation, no MOC needed)
|
# Enable Qt MOC for MessageBroker
|
||||||
set_target_properties(reMarkable PROPERTIES AUTOMOC ON)
|
set_target_properties(reMarkable PROPERTIES AUTOMOC ON)
|
||||||
|
|
||||||
# Add MessageBroker (needs MOC) and HttpServer (native macOS)
|
# Add MessageBroker source (needs MOC processing)
|
||||||
target_sources(reMarkable PRIVATE
|
target_sources(reMarkable PRIVATE
|
||||||
${PROJECT_ROOT_DIR}/src/utils/MessageBroker.mm
|
${PROJECT_ROOT_DIR}/src/utils/MessageBroker.mm
|
||||||
${PROJECT_ROOT_DIR}/src/utils/HttpServer.mm
|
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package(Qt6 COMPONENTS Qml QUIET)
|
|
||||||
if(Qt6Qml_FOUND)
|
|
||||||
target_link_libraries(reMarkable PRIVATE Qt6::Qml)
|
|
||||||
else()
|
|
||||||
find_package(Qt5 COMPONENTS Qml QUIET)
|
|
||||||
if(Qt5Qml_FOUND)
|
|
||||||
target_link_libraries(reMarkable PRIVATE Qt5::Qml)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
message(STATUS "Build mode: qmlrebuild (resource hooking)")
|
message(STATUS "Build mode: qmlrebuild (resource hooking)")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ RMHook hooks into the reMarkable Desktop app's network layer to redirect API cal
|
|||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
**Tested and working on:**
|
**Tested and working on:**
|
||||||
- reMarkable Desktop v3.24.0 (released 2025-12-03)
|
- reMarkable Desktop v3.26.0 (released 2026-23-03)
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/latest.png" width="40%" />
|
<img src="docs/latest.png" width="40%" />
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
// Add this MessageBroker to a QML component that has access to PlatformHelpers
|
|
||||||
// This listens for documentAccepted signals from the HTTP server
|
|
||||||
// and calls PlatformHelpers.documentAccepted()
|
|
||||||
|
|
||||||
import net.noham.MessageBroker
|
|
||||||
|
|
||||||
MessageBroker {
|
|
||||||
id: documentAcceptedBroker
|
|
||||||
listeningFor: ["documentAccepted"]
|
|
||||||
|
|
||||||
onSignalReceived: (signal, message) => {
|
|
||||||
console.log("[DocumentAccepted.MessageBroker] Received signal:", signal);
|
|
||||||
console.log("[DocumentAccepted.MessageBroker] Message data:", message);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Parse JSON message from HTTP server
|
|
||||||
const data = JSON.parse(message);
|
|
||||||
console.log("[DocumentAccepted.MessageBroker] Parsed request:", JSON.stringify(data));
|
|
||||||
|
|
||||||
// Extract parameters with defaults
|
|
||||||
const url = data.url || "";
|
|
||||||
const password = data.password || "";
|
|
||||||
const directoryId = data.directoryId || "";
|
|
||||||
const flag1 = data.flag1 !== undefined ? data.flag1 : false;
|
|
||||||
const flag2 = data.flag2 !== undefined ? data.flag2 : false;
|
|
||||||
|
|
||||||
console.log("[DocumentAccepted.MessageBroker] Parameters:");
|
|
||||||
console.log("[DocumentAccepted.MessageBroker] url:", url);
|
|
||||||
console.log("[DocumentAccepted.MessageBroker] password:", password ? "(set)" : "(empty)");
|
|
||||||
console.log("[DocumentAccepted.MessageBroker] directoryId:", directoryId);
|
|
||||||
console.log("[DocumentAccepted.MessageBroker] flag1:", flag1);
|
|
||||||
console.log("[DocumentAccepted.MessageBroker] flag2:", flag2);
|
|
||||||
|
|
||||||
// Validate required parameters
|
|
||||||
if (!url) {
|
|
||||||
console.error("[DocumentAccepted.MessageBroker] ERROR: Missing 'url' parameter");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!directoryId) {
|
|
||||||
console.error("[DocumentAccepted.MessageBroker] ERROR: Missing 'directoryId' parameter");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call PlatformHelpers.documentAccepted
|
|
||||||
console.log("[DocumentAccepted.MessageBroker] Calling PlatformHelpers.documentAccepted...");
|
|
||||||
PlatformHelpers.documentAccepted(url, password, directoryId, flag1, flag2);
|
|
||||||
console.log("[DocumentAccepted.MessageBroker] Document accepted successfully");
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[DocumentAccepted.MessageBroker] ERROR parsing request:", error);
|
|
||||||
console.error("[DocumentAccepted.MessageBroker] Message was:", message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
// Add this MessageBroker to ExportDialog.qml after the PopupDialog definition
|
|
||||||
// This should be added near the top of the component, after property definitions
|
|
||||||
|
|
||||||
import net.noham.MessageBroker
|
|
||||||
|
|
||||||
// ... existing properties ...
|
|
||||||
|
|
||||||
// MessageBroker for HTTP server export requests
|
|
||||||
MessageBroker {
|
|
||||||
id: exportBroker
|
|
||||||
listeningFor: ["exportFile"]
|
|
||||||
|
|
||||||
onSignalReceived: (signal, message) => {
|
|
||||||
console.log("[ExportDialog.MessageBroker] Received signal:", signal);
|
|
||||||
console.log("[ExportDialog.MessageBroker] Message data:", message);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Parse JSON message from HTTP server
|
|
||||||
const data = JSON.parse(message);
|
|
||||||
console.log("[ExportDialog.MessageBroker] Parsed export request:", JSON.stringify(data));
|
|
||||||
|
|
||||||
// Extract parameters
|
|
||||||
const target = data.target || "";
|
|
||||||
const documentId = data.id || data.documentId || "";
|
|
||||||
const format = data.format !== undefined ? data.format : PlatformHelpers.ExportPdf;
|
|
||||||
const password = data.password || "";
|
|
||||||
const keepPassword = data.keepPassword !== undefined ? data.keepPassword : true;
|
|
||||||
const grayscale = data.grayscale !== undefined ? data.grayscale : false;
|
|
||||||
const pageSelection = data.pageSelection || [];
|
|
||||||
|
|
||||||
console.log("[ExportDialog.MessageBroker] Export parameters:");
|
|
||||||
console.log("[ExportDialog.MessageBroker] target:", target);
|
|
||||||
console.log("[ExportDialog.MessageBroker] documentId:", documentId);
|
|
||||||
console.log("[ExportDialog.MessageBroker] format:", format);
|
|
||||||
console.log("[ExportDialog.MessageBroker] keepPassword:", keepPassword);
|
|
||||||
console.log("[ExportDialog.MessageBroker] grayscale:", grayscale);
|
|
||||||
console.log("[ExportDialog.MessageBroker] pageSelection:", JSON.stringify(pageSelection));
|
|
||||||
|
|
||||||
// Validate required parameters
|
|
||||||
if (!target) {
|
|
||||||
console.error("[ExportDialog.MessageBroker] ERROR: Missing 'target' parameter");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!documentId) {
|
|
||||||
console.error("[ExportDialog.MessageBroker] ERROR: Missing 'id' or 'documentId' parameter");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call PlatformHelpers.exportFile
|
|
||||||
console.log("[ExportDialog.MessageBroker] Calling PlatformHelpers.exportFile...");
|
|
||||||
|
|
||||||
if (pageSelection && pageSelection.length > 0) {
|
|
||||||
console.log("[ExportDialog.MessageBroker] Exporting with page selection");
|
|
||||||
PlatformHelpers.exportFile(target, documentId, format, password, keepPassword, grayscale, pageSelection);
|
|
||||||
} else {
|
|
||||||
console.log("[ExportDialog.MessageBroker] Exporting full document");
|
|
||||||
PlatformHelpers.exportFile(target, documentId, format, password, keepPassword, grayscale);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[ExportDialog.MessageBroker] Export completed successfully");
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[ExportDialog.MessageBroker] ERROR parsing export request:", error);
|
|
||||||
console.error("[ExportDialog.MessageBroker] Message was:", message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
# HTTP Server for Export Requests
|
|
||||||
|
|
||||||
The RMHook dylib includes an HTTP server that accepts export requests and forwards them to the reMarkable application via MessageBroker.
|
|
||||||
|
|
||||||
## Server Details
|
|
||||||
|
|
||||||
- **Host**: `localhost`
|
|
||||||
- **Port**: `8080`
|
|
||||||
- **Base URL**: `http://localhost:8080`
|
|
||||||
|
|
||||||
## Endpoints
|
|
||||||
|
|
||||||
### `POST /exportFile`
|
|
||||||
|
|
||||||
Trigger a document export from the reMarkable application.
|
|
||||||
|
|
||||||
**Request Body** (JSON):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"target": "file:///Users/username/Desktop/output.pdf",
|
|
||||||
"id": "document-uuid-here",
|
|
||||||
"format": 0,
|
|
||||||
"password": "",
|
|
||||||
"keepPassword": true,
|
|
||||||
"grayscale": false,
|
|
||||||
"pageSelection": []
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parameters**:
|
|
||||||
- `target` (string, required): File path or folder URL for the export. Use `file://` prefix for local paths.
|
|
||||||
- `id` or `documentId` (string, required): The UUID of the document to export.
|
|
||||||
- `format` (integer, optional): Export format. Default: `0` (PDF)
|
|
||||||
- `0`: PDF
|
|
||||||
- `1`: PNG
|
|
||||||
- `2`: SVG
|
|
||||||
- `3`: RmBundle
|
|
||||||
- `4`: RmHtml
|
|
||||||
- `password` (string, optional): Password for password-protected documents. Default: `""`
|
|
||||||
- `keepPassword` (boolean, optional): Whether to keep password protection on PDF exports. Default: `true`
|
|
||||||
- `grayscale` (boolean, optional): Export with grayscale pens. Default: `false`
|
|
||||||
- `pageSelection` (array, optional): Array of page indices to export. If empty or omitted, exports all pages. Example: `[0, 1, 2]`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"message": "Export request sent to application"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Error Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error": "Error description"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `POST /documentAccepted`
|
|
||||||
|
|
||||||
Import/accept a document into the reMarkable application.
|
|
||||||
|
|
||||||
**Request Body** (JSON):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"url": "file:///Users/username/Desktop/test.pdf",
|
|
||||||
"password": "",
|
|
||||||
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
|
|
||||||
"flag1": false,
|
|
||||||
"flag2": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parameters**:
|
|
||||||
- `url` (string, required): File URL to import. Use `file://` prefix for local paths.
|
|
||||||
- `password` (string, optional): Password for password-protected documents. Default: `""`
|
|
||||||
- `directoryId` (string, required): The UUID of the target directory/folder where the document should be imported.
|
|
||||||
- `flag1` (boolean, optional): Purpose unclear. Default: `false`
|
|
||||||
- `flag2` (boolean, optional): Purpose unclear. Default: `false`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"message": "Document accepted request sent to application"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Error Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error": "Error description"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `GET /health`
|
|
||||||
|
|
||||||
Health check endpoint.
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"service": "RMHook HTTP Server"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example Requests
|
|
||||||
|
|
||||||
### Export a document to PDF
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8080/exportFile \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"target": "file:///Users/noham/Desktop/export.pdf",
|
|
||||||
"id": "12345678-1234-1234-1234-123456789abc",
|
|
||||||
"format": 0,
|
|
||||||
"grayscale": false,
|
|
||||||
"keepPassword": true
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Export specific pages as PNG
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8080/exportFile \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"target": "file:///Users/noham/Desktop/pages",
|
|
||||||
"id": "12345678-1234-1234-1234-123456789abc",
|
|
||||||
"format": 1,
|
|
||||||
"pageSelection": [0, 1, 2],
|
|
||||||
"grayscale": true
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Export to RmBundle format
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8080/exportFile \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"target": "file:///Users/noham/Desktop/MyDocument",
|
|
||||||
"id": "12345678-1234-1234-1234-123456789abc",
|
|
||||||
"format": 3
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Import/Accept a document
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8080/documentAccepted \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"url": "file:///Users/noham/Desktop/test.pdf",
|
|
||||||
"password": "",
|
|
||||||
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
|
|
||||||
"flag1": false,
|
|
||||||
"flag2": false
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Python Example - Export
|
|
||||||
|
|
||||||
```python
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Export configuration
|
|
||||||
export_data = {
|
|
||||||
"target": "file:///Users/noham/Desktop/output.pdf",
|
|
||||||
"id": "12345678-1234-1234-1234-123456789abc",
|
|
||||||
"format": 0, # PDF
|
|
||||||
"grayscale": False,
|
|
||||||
"keepPassword": True
|
|
||||||
}
|
|
||||||
|
|
||||||
# Send request
|
|
||||||
response = requests.post(
|
|
||||||
"http://localhost:8080/exportFile",
|
|
||||||
json=export_data
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"Status: {response.status_code}")
|
|
||||||
print(f"Response: {response.json()}")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Python Example - Import Document
|
|
||||||
|
|
||||||
```python
|
|
||||||
import requests
|
|
||||||
|
|
||||||
# Import configuration
|
|
||||||
import_data = {
|
|
||||||
"url": "file:///Users/noham/Desktop/test.pdf",
|
|
||||||
"password": "",
|
|
||||||
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
|
|
||||||
"flag1": False,
|
|
||||||
"flag2": False
|
|
||||||
}
|
|
||||||
|
|
||||||
# Send request
|
|
||||||
response = requests.post(
|
|
||||||
"http://localhost:8080/documentAccepted",
|
|
||||||
json=import_data
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"Status: {response.status_code}")
|
|
||||||
print(f"Response: {response.json()}")
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript Example - Export
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Export configuration
|
|
||||||
const exportData = {
|
|
||||||
target: "file:///Users/noham/Desktop/output.pdf",
|
|
||||||
id: "12345678-1234-1234-1234-123456789abc",
|
|
||||||
format: 0, // PDF
|
|
||||||
grayscale: false,
|
|
||||||
keepPassword: true
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send request
|
|
||||||
fetch("http://localhost:8080/exportFile", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify(exportData)
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => console.log("Success:", data))
|
|
||||||
.catch(error => console.error("Error:", error));
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript Example - Import Document
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Import configuration
|
|
||||||
const importData = {
|
|
||||||
url: "file:///Users/noham/Desktop/test.pdf",
|
|
||||||
password: "",
|
|
||||||
directoryId: "2166c19d-d2cc-456c-9f0e-49482031092a",
|
|
||||||
flag1: false,
|
|
||||||
flag2: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send request
|
|
||||||
fetch("http://localhost:8080/documentAccepted", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify(importData)
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => console.log("Success:", data))
|
|
||||||
.catch(error => console.error("Error:", error));
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration with QML
|
|
||||||
|
|
||||||
### Export Dialog Integration
|
|
||||||
|
|
||||||
Add the MessageBroker snippet from `docs/ExportDialog_MessageBroker_snippet.qml` to your ExportDialog.qml replacement file. This will enable the QML side to receive export requests from the HTTP server.
|
|
||||||
|
|
||||||
The MessageBroker listens for "exportFile" signals and automatically calls `PlatformHelpers.exportFile()` with the provided parameters.
|
|
||||||
|
|
||||||
### Document Import Integration
|
|
||||||
|
|
||||||
Add the MessageBroker snippet from `docs/DocumentAccepted_MessageBroker_snippet.qml` to a QML component (such as GeneralSettings.qml) that has access to PlatformHelpers. This will enable the QML side to receive document import requests from the HTTP server.
|
|
||||||
|
|
||||||
The MessageBroker listens for "documentAccepted" signals and automatically calls `PlatformHelpers.documentAccepted()` with the provided parameters.
|
|
||||||
|
|
||||||
## Document ID and Directory ID Discovery
|
|
||||||
|
|
||||||
To find document and directory IDs, you can:
|
|
||||||
|
|
||||||
1. Check the reMarkable application logs when opening documents or folders
|
|
||||||
2. Use the reMarkable Cloud API
|
|
||||||
3. Access the local database at `~/Library/Application Support/remarkable/desktop-app/`
|
|
||||||
4. For the root directory ID, check the logs when navigating to "My Files"
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
- Ensure the HTTP server started successfully by checking the logs: `2025-12-08 17:32:22.288 reMarkable[19574:1316287] [HttpServer] HTTP server started successfully on http://localhost:8080`
|
|
||||||
- Test the health endpoint: `curl http://localhost:8080/health`
|
|
||||||
- Check the Console.app for detailed logging from the MessageBroker and HttpServer
|
|
||||||
- Verify the document ID and directory ID are correct UUIDs
|
|
||||||
- Ensure the target/url path is accessible and uses the `file://` prefix
|
|
||||||
- For imports, verify the target directory exists and is accessible
|
|
||||||
BIN
docs/latest.png
BIN
docs/latest.png
Binary file not shown.
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 230 KiB |
@@ -1,200 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
BASE_URL = "http://localhost:8080"
|
|
||||||
|
|
||||||
def export_document(document_id, target_path, format_type=0, grayscale=False,
|
|
||||||
keep_password=True, password="", page_selection=None):
|
|
||||||
"""
|
|
||||||
Export a document via HTTP API
|
|
||||||
|
|
||||||
Args:
|
|
||||||
document_id: UUID of the document to export
|
|
||||||
target_path: Target path for the export (use file:// prefix)
|
|
||||||
format_type: Export format (0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml)
|
|
||||||
grayscale: Export with grayscale pens
|
|
||||||
keep_password: Keep password protection (for PDFs)
|
|
||||||
password: Password for protected documents
|
|
||||||
page_selection: List of page indices to export (None = all pages)
|
|
||||||
"""
|
|
||||||
print(f"\nExporting document {document_id}...")
|
|
||||||
print(f"Target: {target_path}")
|
|
||||||
print(f"Format: {format_type}")
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"target": target_path,
|
|
||||||
"id": document_id,
|
|
||||||
"format": format_type,
|
|
||||||
"grayscale": grayscale,
|
|
||||||
"keepPassword": keep_password,
|
|
||||||
"password": password
|
|
||||||
}
|
|
||||||
|
|
||||||
if page_selection:
|
|
||||||
data["pageSelection"] = page_selection
|
|
||||||
print(f"Pages: {page_selection}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
f"{BASE_URL}/exportFile",
|
|
||||||
json=data,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
print(f"Status: {response.status_code}")
|
|
||||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
|
||||||
return response.status_code == 200
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def import_document(file_url, directory_id, password="", flag1=False, flag2=False):
|
|
||||||
"""
|
|
||||||
Import a document via HTTP API
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_url: File URL to import (use file:// prefix)
|
|
||||||
directory_id: UUID of the target directory
|
|
||||||
password: Password for protected documents (optional)
|
|
||||||
flag1: Additional flag parameter
|
|
||||||
flag2: Additional flag parameter
|
|
||||||
"""
|
|
||||||
print(f"\nImporting document from {file_url}...")
|
|
||||||
print(f"Target directory: {directory_id}")
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"url": file_url,
|
|
||||||
"password": password,
|
|
||||||
"directoryId": directory_id,
|
|
||||||
"flag1": flag1,
|
|
||||||
"flag2": flag2
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
f"{BASE_URL}/documentAccepted",
|
|
||||||
json=data,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
print(f"Status: {response.status_code}")
|
|
||||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
|
||||||
return response.status_code == 200
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description='reMarkable HTTP Server API Client',
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
epilog='''
|
|
||||||
Examples:
|
|
||||||
Export a document as PDF:
|
|
||||||
%(prog)s export 12345678-1234-1234-1234-123456789abc file:///Users/noham/Desktop/test.pdf
|
|
||||||
|
|
||||||
Export as PNG with grayscale:
|
|
||||||
%(prog)s export <doc-id> <target> --format 1 --grayscale
|
|
||||||
|
|
||||||
Export specific pages:
|
|
||||||
%(prog)s export <doc-id> <target> --pages 0 1 2
|
|
||||||
|
|
||||||
Import a document:
|
|
||||||
%(prog)s import file:///Users/noham/Desktop/test.pdf 2166c19d-d2cc-456c-9f0e-49482031092a
|
|
||||||
|
|
||||||
Import with password:
|
|
||||||
%(prog)s import <file-url> <directory-id> --password mypassword
|
|
||||||
'''
|
|
||||||
)
|
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
|
||||||
|
|
||||||
# Export command
|
|
||||||
export_parser = subparsers.add_parser('export', help='Export a document')
|
|
||||||
export_parser.add_argument('document_id', help='UUID of the document to export')
|
|
||||||
export_parser.add_argument('target_path', help='Target path for export (use file:// prefix)')
|
|
||||||
export_parser.add_argument(
|
|
||||||
'--format', '-f',
|
|
||||||
type=int,
|
|
||||||
default=0,
|
|
||||||
choices=[0, 1, 2, 3, 4],
|
|
||||||
help='Export format: 0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml (default: 0)'
|
|
||||||
)
|
|
||||||
export_parser.add_argument(
|
|
||||||
'--grayscale', '-g',
|
|
||||||
action='store_true',
|
|
||||||
help='Export with grayscale pens'
|
|
||||||
)
|
|
||||||
export_parser.add_argument(
|
|
||||||
'--no-keep-password',
|
|
||||||
action='store_true',
|
|
||||||
help='Do not keep password protection (for PDFs)'
|
|
||||||
)
|
|
||||||
export_parser.add_argument(
|
|
||||||
'--password', '-p',
|
|
||||||
default='',
|
|
||||||
help='Password for protected documents'
|
|
||||||
)
|
|
||||||
export_parser.add_argument(
|
|
||||||
'--pages',
|
|
||||||
type=int,
|
|
||||||
nargs='+',
|
|
||||||
help='List of page indices to export (default: all pages)'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Import command
|
|
||||||
import_parser = subparsers.add_parser('import', help='Import a document')
|
|
||||||
import_parser.add_argument('file_url', help='File URL to import (use file:// prefix)')
|
|
||||||
import_parser.add_argument('directory_id', help='UUID of the target directory')
|
|
||||||
import_parser.add_argument(
|
|
||||||
'--password', '-p',
|
|
||||||
default='',
|
|
||||||
help='Password for protected documents'
|
|
||||||
)
|
|
||||||
import_parser.add_argument(
|
|
||||||
'--flag1',
|
|
||||||
action='store_true',
|
|
||||||
help='Additional flag parameter'
|
|
||||||
)
|
|
||||||
import_parser.add_argument(
|
|
||||||
'--flag2',
|
|
||||||
action='store_true',
|
|
||||||
help='Additional flag parameter'
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.command == 'export':
|
|
||||||
success = export_document(
|
|
||||||
document_id=args.document_id,
|
|
||||||
target_path=args.target_path,
|
|
||||||
format_type=args.format,
|
|
||||||
grayscale=args.grayscale,
|
|
||||||
keep_password=not args.no_keep_password,
|
|
||||||
password=args.password,
|
|
||||||
page_selection=args.pages
|
|
||||||
)
|
|
||||||
if success:
|
|
||||||
print("\n✅ Export request sent successfully!")
|
|
||||||
else:
|
|
||||||
print("\n❌ Export request failed!")
|
|
||||||
sys.exit(1)
|
|
||||||
elif args.command == 'import':
|
|
||||||
success = import_document(
|
|
||||||
file_url=args.file_url,
|
|
||||||
directory_id=args.directory_id,
|
|
||||||
password=args.password,
|
|
||||||
flag1=args.flag1,
|
|
||||||
flag2=args.flag2
|
|
||||||
)
|
|
||||||
if success:
|
|
||||||
print("\n✅ Import request sent successfully!")
|
|
||||||
else:
|
|
||||||
print("\n❌ Import request failed!")
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
parser.print_help()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for RMHook HTTP Server
|
|
||||||
Demonstrates how to trigger exports and imports via HTTP API
|
|
||||||
"""
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
BASE_URL = "http://localhost:8080"
|
|
||||||
|
|
||||||
def test_health():
|
|
||||||
"""Test the health endpoint"""
|
|
||||||
print("Testing /health endpoint...")
|
|
||||||
try:
|
|
||||||
response = requests.get(f"{BASE_URL}/health", timeout=5)
|
|
||||||
print(f"Status: {response.status_code}")
|
|
||||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
|
||||||
return response.status_code == 200
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def export_document(document_id, target_path, format_type=0, grayscale=False,
|
|
||||||
keep_password=True, password="", page_selection=None):
|
|
||||||
"""
|
|
||||||
Export a document via HTTP API
|
|
||||||
|
|
||||||
Args:
|
|
||||||
document_id: UUID of the document to export
|
|
||||||
target_path: Target path for the export (use file:// prefix)
|
|
||||||
format_type: Export format (0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml)
|
|
||||||
grayscale: Export with grayscale pens
|
|
||||||
keep_password: Keep password protection (for PDFs)
|
|
||||||
password: Password for protected documents
|
|
||||||
page_selection: List of page indices to export (None = all pages)
|
|
||||||
"""
|
|
||||||
print(f"\nExporting document {document_id}...")
|
|
||||||
print(f"Target: {target_path}")
|
|
||||||
print(f"Format: {format_type}")
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"target": target_path,
|
|
||||||
"id": document_id,
|
|
||||||
"format": format_type,
|
|
||||||
"grayscale": grayscale,
|
|
||||||
"keepPassword": keep_password,
|
|
||||||
"password": password
|
|
||||||
}
|
|
||||||
|
|
||||||
if page_selection:
|
|
||||||
data["pageSelection"] = page_selection
|
|
||||||
print(f"Pages: {page_selection}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
f"{BASE_URL}/exportFile",
|
|
||||||
json=data,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
print(f"Status: {response.status_code}")
|
|
||||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
|
||||||
return response.status_code == 200
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def import_document(file_url, directory_id, password=""):
|
|
||||||
"""
|
|
||||||
Import a document via HTTP API
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_url: File URL to import (use file:// prefix)
|
|
||||||
directory_id: UUID of the target directory
|
|
||||||
password: Password for protected documents (optional)
|
|
||||||
"""
|
|
||||||
print(f"\nImporting document from {file_url}...")
|
|
||||||
print(f"Target directory: {directory_id}")
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"url": file_url,
|
|
||||||
"password": password,
|
|
||||||
"directoryId": directory_id,
|
|
||||||
"flag1": False,
|
|
||||||
"flag2": False
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
f"{BASE_URL}/documentAccepted",
|
|
||||||
json=data,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
print(f"Status: {response.status_code}")
|
|
||||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
|
||||||
return response.status_code == 200
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("=" * 60)
|
|
||||||
print("RMHook HTTP Server Test Script")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Test health endpoint
|
|
||||||
if not test_health():
|
|
||||||
print("\n❌ Health check failed. Is the server running?")
|
|
||||||
print("Make sure reMarkable app is running with the dylib injected.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print("\n✅ Server is running!")
|
|
||||||
|
|
||||||
# Command line interface
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("Usage Examples")
|
|
||||||
print("=" * 60)
|
|
||||||
print("\n1. Export a document:")
|
|
||||||
print(' python3 test_http_server.py export <doc-id> <target-path> [format] [grayscale]')
|
|
||||||
print("\n Example:")
|
|
||||||
print(' python3 test_http_server.py export "abc-123" "file:///Users/noham/Desktop/test.pdf" 0 false')
|
|
||||||
|
|
||||||
print("\n2. Import a document:")
|
|
||||||
print(' python3 test_http_server.py import <file-url> <directory-id>')
|
|
||||||
print("\n Example:")
|
|
||||||
print(' python3 test_http_server.py import "file:///Users/noham/Desktop/test.pdf" "2166c19d-d2cc-456c-9f0e-49482031092a"')
|
|
||||||
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
command = sys.argv[1].lower()
|
|
||||||
|
|
||||||
if command == "export" and len(sys.argv) >= 4:
|
|
||||||
doc_id = sys.argv[2]
|
|
||||||
target = sys.argv[3]
|
|
||||||
format_type = int(sys.argv[4]) if len(sys.argv) > 4 else 0
|
|
||||||
grayscale = sys.argv[5].lower() == "true" if len(sys.argv) > 5 else False
|
|
||||||
|
|
||||||
success = export_document(doc_id, target, format_type, grayscale)
|
|
||||||
if success:
|
|
||||||
print("\n✅ Export request sent successfully!")
|
|
||||||
else:
|
|
||||||
print("\n❌ Export request failed!")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
elif command == "import" and len(sys.argv) >= 4:
|
|
||||||
file_url = sys.argv[2]
|
|
||||||
directory_id = sys.argv[3]
|
|
||||||
password = sys.argv[4] if len(sys.argv) > 4 else ""
|
|
||||||
|
|
||||||
success = import_document(file_url, directory_id, password)
|
|
||||||
if success:
|
|
||||||
print("\n✅ Import request sent successfully!")
|
|
||||||
else:
|
|
||||||
print("\n❌ Import request failed!")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
else:
|
|
||||||
print(f"\n❌ Invalid command or missing arguments: {' '.join(sys.argv[1:])}")
|
|
||||||
print("Run without arguments to see usage examples.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
#endif
|
#endif
|
||||||
#ifdef BUILD_MODE_QMLREBUILD
|
#ifdef BUILD_MODE_QMLREBUILD
|
||||||
#import "MessageBroker.h"
|
#import "MessageBroker.h"
|
||||||
#import "HttpServer.h"
|
|
||||||
#endif
|
#endif
|
||||||
#import <objc/runtime.h>
|
#import <objc/runtime.h>
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
@@ -315,13 +314,6 @@ static inline bool shouldPatchURL(const QString &host) {
|
|||||||
NSLogger(@"[reMarkable] Native callback received signal '%s' with value '%s'", signal, value);
|
NSLogger(@"[reMarkable] Native callback received signal '%s' with value '%s'", signal, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start HTTP server for export requests
|
|
||||||
if (httpserver::start(8080)) {
|
|
||||||
NSLogger(@"[reMarkable] HTTP server started on http://localhost:8080");
|
|
||||||
} else {
|
|
||||||
NSLogger(@"[reMarkable] Failed to start HTTP server");
|
|
||||||
}
|
|
||||||
|
|
||||||
[MemoryUtils hookSymbol:@"QtCore"
|
[MemoryUtils hookSymbol:@"QtCore"
|
||||||
symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_"
|
symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_"
|
||||||
hookFunction:(void *)hooked_qRegisterResourceData
|
hookFunction:(void *)hooked_qRegisterResourceData
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
// HTTP Server for RMHook - native macOS implementation
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace httpserver {
|
|
||||||
// Start HTTP server on specified port
|
|
||||||
bool start(uint16_t port = 8080);
|
|
||||||
|
|
||||||
// Stop HTTP server
|
|
||||||
void stop();
|
|
||||||
|
|
||||||
// Check if server is running
|
|
||||||
bool isRunning();
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
// HTTP Server for RMHook - native macOS implementation using CFSocket
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <CoreFoundation/CoreFoundation.h>
|
|
||||||
#include <sys/socket.h>
|
|
||||||
#include <netinet/in.h>
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include "HttpServer.h"
|
|
||||||
#include "MessageBroker.h"
|
|
||||||
#include "Logger.h"
|
|
||||||
|
|
||||||
static CFSocketRef g_serverSocket = NULL;
|
|
||||||
static uint16_t g_serverPort = 0;
|
|
||||||
static bool g_isRunning = false;
|
|
||||||
|
|
||||||
// Forward declarations
|
|
||||||
static void handleClientConnection(int clientSocket);
|
|
||||||
static void sendResponse(int clientSocket, int statusCode, NSString *body, NSString *contentType);
|
|
||||||
static void handleExportFileRequest(int clientSocket, NSDictionary *jsonData);
|
|
||||||
static void handleDocumentAcceptedRequest(int clientSocket, NSDictionary *jsonData);
|
|
||||||
|
|
||||||
// Socket callback
|
|
||||||
static void socketCallback(CFSocketRef socket, CFSocketCallBackType type,
|
|
||||||
CFDataRef address, const void *data, void *info)
|
|
||||||
{
|
|
||||||
if (type == kCFSocketAcceptCallBack) {
|
|
||||||
CFSocketNativeHandle clientSocket = *(CFSocketNativeHandle *)data;
|
|
||||||
NSLogger(@"[HttpServer] New connection accepted, socket: %d", clientSocket);
|
|
||||||
|
|
||||||
// Handle client in background
|
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
||||||
handleClientConnection(clientSocket);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void handleClientConnection(int clientSocket)
|
|
||||||
{
|
|
||||||
@autoreleasepool {
|
|
||||||
// Read request
|
|
||||||
char buffer[4096];
|
|
||||||
ssize_t bytesRead = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
|
|
||||||
|
|
||||||
if (bytesRead <= 0) {
|
|
||||||
close(clientSocket);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer[bytesRead] = '\0';
|
|
||||||
NSString *request = [NSString stringWithUTF8String:buffer];
|
|
||||||
|
|
||||||
NSLogger(@"[HttpServer] Received request (%ld bytes)", (long)bytesRead);
|
|
||||||
|
|
||||||
// Parse request line
|
|
||||||
NSArray *lines = [request componentsSeparatedByString:@"\r\n"];
|
|
||||||
if (lines.count == 0) {
|
|
||||||
sendResponse(clientSocket, 400, @"{\"error\": \"Invalid request\"}", @"application/json");
|
|
||||||
close(clientSocket);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSArray *requestLine = [lines[0] componentsSeparatedByString:@" "];
|
|
||||||
if (requestLine.count < 3) {
|
|
||||||
sendResponse(clientSocket, 400, @"{\"error\": \"Invalid request line\"}", @"application/json");
|
|
||||||
close(clientSocket);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *method = requestLine[0];
|
|
||||||
NSString *path = requestLine[1];
|
|
||||||
|
|
||||||
NSLogger(@"[HttpServer] %@ %@", method, path);
|
|
||||||
|
|
||||||
// Find body (after \r\n\r\n)
|
|
||||||
NSRange bodyRange = [request rangeOfString:@"\r\n\r\n"];
|
|
||||||
NSString *body = nil;
|
|
||||||
if (bodyRange.location != NSNotFound) {
|
|
||||||
body = [request substringFromIndex:bodyRange.location + 4];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Route requests
|
|
||||||
if ([path isEqualToString:@"/exportFile"] && [method isEqualToString:@"POST"]) {
|
|
||||||
if (!body || body.length == 0) {
|
|
||||||
sendResponse(clientSocket, 400, @"{\"error\": \"Missing request body\"}", @"application/json");
|
|
||||||
close(clientSocket);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSData *jsonData = [body dataUsingEncoding:NSUTF8StringEncoding];
|
|
||||||
NSError *error = nil;
|
|
||||||
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
|
|
||||||
|
|
||||||
if (error || ![json isKindOfClass:[NSDictionary class]]) {
|
|
||||||
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Invalid JSON: %@\"}",
|
|
||||||
error ? error.localizedDescription : @"Not an object"];
|
|
||||||
sendResponse(clientSocket, 400, errorMsg, @"application/json");
|
|
||||||
close(clientSocket);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleExportFileRequest(clientSocket, (NSDictionary *)json);
|
|
||||||
|
|
||||||
} else if ([path isEqualToString:@"/documentAccepted"] && [method isEqualToString:@"POST"]) {
|
|
||||||
if (!body || body.length == 0) {
|
|
||||||
sendResponse(clientSocket, 400, @"{\"error\": \"Missing request body\"}", @"application/json");
|
|
||||||
close(clientSocket);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSData *jsonData = [body dataUsingEncoding:NSUTF8StringEncoding];
|
|
||||||
NSError *error = nil;
|
|
||||||
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
|
|
||||||
|
|
||||||
if (error || ![json isKindOfClass:[NSDictionary class]]) {
|
|
||||||
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Invalid JSON: %@\"}",
|
|
||||||
error ? error.localizedDescription : @"Not an object"];
|
|
||||||
sendResponse(clientSocket, 400, errorMsg, @"application/json");
|
|
||||||
close(clientSocket);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDocumentAcceptedRequest(clientSocket, (NSDictionary *)json);
|
|
||||||
|
|
||||||
} else if ([path isEqualToString:@"/health"] || [path isEqualToString:@"/"]) {
|
|
||||||
sendResponse(clientSocket, 200, @"{\"status\": \"ok\", \"service\": \"RMHook HTTP Server\"}", @"application/json");
|
|
||||||
|
|
||||||
} else {
|
|
||||||
sendResponse(clientSocket, 404, @"{\"error\": \"Endpoint not found\"}", @"application/json");
|
|
||||||
}
|
|
||||||
|
|
||||||
close(clientSocket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void handleExportFileRequest(int clientSocket, NSDictionary *jsonData)
|
|
||||||
{
|
|
||||||
NSLogger(@"[HttpServer] Processing /exportFile request");
|
|
||||||
|
|
||||||
// Convert to JSON string for MessageBroker
|
|
||||||
NSError *error = nil;
|
|
||||||
NSData *jsonDataEncoded = [NSJSONSerialization dataWithJSONObject:jsonData
|
|
||||||
options:0
|
|
||||||
error:&error];
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Failed to encode JSON: %@\"}",
|
|
||||||
error.localizedDescription];
|
|
||||||
sendResponse(clientSocket, 500, errorMsg, @"application/json");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *jsonStr = [[NSString alloc] initWithData:jsonDataEncoded encoding:NSUTF8StringEncoding];
|
|
||||||
NSLogger(@"[HttpServer] Broadcasting exportFile signal with data: %@", jsonStr);
|
|
||||||
|
|
||||||
// Broadcast to MessageBroker
|
|
||||||
messagebroker::broadcast("exportFile", [jsonStr UTF8String]);
|
|
||||||
|
|
||||||
// Send success response
|
|
||||||
sendResponse(clientSocket, 200,
|
|
||||||
@"{\"status\": \"success\", \"message\": \"Export request sent to application\"}",
|
|
||||||
@"application/json");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void handleDocumentAcceptedRequest(int clientSocket, NSDictionary *jsonData)
|
|
||||||
{
|
|
||||||
NSLogger(@"[HttpServer] Processing /documentAccepted request");
|
|
||||||
|
|
||||||
// Convert to JSON string for MessageBroker
|
|
||||||
NSError *error = nil;
|
|
||||||
NSData *jsonDataEncoded = [NSJSONSerialization dataWithJSONObject:jsonData
|
|
||||||
options:0
|
|
||||||
error:&error];
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Failed to encode JSON: %@\"}",
|
|
||||||
error.localizedDescription];
|
|
||||||
sendResponse(clientSocket, 500, errorMsg, @"application/json");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *jsonStr = [[NSString alloc] initWithData:jsonDataEncoded encoding:NSUTF8StringEncoding];
|
|
||||||
NSLogger(@"[HttpServer] Broadcasting documentAccepted signal with data: %@", jsonStr);
|
|
||||||
|
|
||||||
// Broadcast to MessageBroker
|
|
||||||
messagebroker::broadcast("documentAccepted", [jsonStr UTF8String]);
|
|
||||||
|
|
||||||
// Send success response
|
|
||||||
sendResponse(clientSocket, 200,
|
|
||||||
@"{\"status\": \"success\", \"message\": \"Document accepted request sent to application\"}",
|
|
||||||
@"application/json");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void sendResponse(int clientSocket, int statusCode, NSString *body, NSString *contentType)
|
|
||||||
{
|
|
||||||
NSString *statusText;
|
|
||||||
switch (statusCode) {
|
|
||||||
case 200: statusText = @"OK"; break;
|
|
||||||
case 400: statusText = @"Bad Request"; break;
|
|
||||||
case 404: statusText = @"Not Found"; break;
|
|
||||||
case 500: statusText = @"Internal Server Error"; break;
|
|
||||||
default: statusText = @"Unknown"; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSData *bodyData = [body dataUsingEncoding:NSUTF8StringEncoding];
|
|
||||||
|
|
||||||
NSString *response = [NSString stringWithFormat:
|
|
||||||
@"HTTP/1.1 %d %@\r\n"
|
|
||||||
@"Content-Type: %@; charset=utf-8\r\n"
|
|
||||||
@"Content-Length: %lu\r\n"
|
|
||||||
@"Access-Control-Allow-Origin: *\r\n"
|
|
||||||
@"Connection: close\r\n"
|
|
||||||
@"\r\n"
|
|
||||||
@"%@",
|
|
||||||
statusCode, statusText, contentType, (unsigned long)bodyData.length, body
|
|
||||||
];
|
|
||||||
|
|
||||||
NSData *responseData = [response dataUsingEncoding:NSUTF8StringEncoding];
|
|
||||||
send(clientSocket, responseData.bytes, responseData.length, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace httpserver {
|
|
||||||
|
|
||||||
bool start(uint16_t port)
|
|
||||||
{
|
|
||||||
if (g_isRunning) {
|
|
||||||
NSLogger(@"[HttpServer] Server already running on port %d", g_serverPort);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create socket
|
|
||||||
CFSocketContext context = {0, NULL, NULL, NULL, NULL};
|
|
||||||
g_serverSocket = CFSocketCreate(kCFAllocatorDefault,
|
|
||||||
PF_INET,
|
|
||||||
SOCK_STREAM,
|
|
||||||
IPPROTO_TCP,
|
|
||||||
kCFSocketAcceptCallBack,
|
|
||||||
socketCallback,
|
|
||||||
&context);
|
|
||||||
|
|
||||||
if (!g_serverSocket) {
|
|
||||||
NSLogger(@"[HttpServer] Failed to create socket");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set socket options
|
|
||||||
int yes = 1;
|
|
||||||
setsockopt(CFSocketGetNative(g_serverSocket), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
|
||||||
|
|
||||||
// Bind to address
|
|
||||||
struct sockaddr_in addr;
|
|
||||||
memset(&addr, 0, sizeof(addr));
|
|
||||||
addr.sin_len = sizeof(addr);
|
|
||||||
addr.sin_family = AF_INET;
|
|
||||||
addr.sin_port = htons(port);
|
|
||||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // localhost only
|
|
||||||
|
|
||||||
CFDataRef addressData = CFDataCreate(kCFAllocatorDefault,
|
|
||||||
(const UInt8 *)&addr,
|
|
||||||
sizeof(addr));
|
|
||||||
|
|
||||||
CFSocketError error = CFSocketSetAddress(g_serverSocket, addressData);
|
|
||||||
CFRelease(addressData);
|
|
||||||
|
|
||||||
if (error != kCFSocketSuccess) {
|
|
||||||
NSLogger(@"[HttpServer] Failed to bind to port %d (error: %ld)", port, (long)error);
|
|
||||||
CFRelease(g_serverSocket);
|
|
||||||
g_serverSocket = NULL;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to run loop
|
|
||||||
CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, g_serverSocket, 0);
|
|
||||||
CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopCommonModes);
|
|
||||||
CFRelease(source);
|
|
||||||
|
|
||||||
g_serverPort = port;
|
|
||||||
g_isRunning = true;
|
|
||||||
|
|
||||||
NSLogger(@"[HttpServer] HTTP server started successfully on http://localhost:%d", port);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void stop()
|
|
||||||
{
|
|
||||||
if (!g_isRunning) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (g_serverSocket) {
|
|
||||||
CFSocketInvalidate(g_serverSocket);
|
|
||||||
CFRelease(g_serverSocket);
|
|
||||||
g_serverSocket = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_isRunning = false;
|
|
||||||
NSLogger(@"[HttpServer] HTTP server stopped");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isRunning()
|
|
||||||
{
|
|
||||||
return g_isRunning;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace httpserver
|
|
||||||
@@ -348,8 +348,6 @@ void ReMarkableDumpResourceFile(struct ResourceRoot *root, int node, const char
|
|||||||
static const char *kFilesToReplace[] = {
|
static const char *kFilesToReplace[] = {
|
||||||
"/qml/client/dialogs/ExportDialog.qml",
|
"/qml/client/dialogs/ExportDialog.qml",
|
||||||
"/qml/client/settings/GeneralSettings.qml",
|
"/qml/client/settings/GeneralSettings.qml",
|
||||||
"/qml/client/dialogs/ExportUtils.js",
|
|
||||||
"/qml/client/desktop/FileImportDialog.qml",
|
|
||||||
NULL // Sentinel to mark end of list
|
NULL // Sentinel to mark end of list
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
// Example usage of MessageBroker from C++/Objective-C
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QProcess>
|
|
||||||
#include <QString>
|
|
||||||
#include <QQmlApplicationEngine>
|
|
||||||
#include "MessageBroker.h"
|
|
||||||
|
|
||||||
// Example: Register MessageBroker QML type (called from dylib init)
|
|
||||||
void initMessageBroker() {
|
|
||||||
messagebroker::registerQmlType();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example: Send a signal from C++ to QML
|
|
||||||
void sendSignal() {
|
|
||||||
messagebroker::broadcast("demoSignal", "Hello from C!");
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import net.noham.MessageBroker
|
|
||||||
|
|
||||||
MessageBroker {
|
|
||||||
id: demoBroker
|
|
||||||
listeningFor: ["demoSignal"]
|
|
||||||
|
|
||||||
onSignalReceived: (signal, message) => {
|
|
||||||
console.log("Got message", signal, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
onClicked: () => {
|
|
||||||
demoBroker.sendSignal("mySignalName", "Hello from QML!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user