A fork of https://github.com/crosspoint-reader/crosspoint-reader
1#include "KOReaderSyncClient.h"
2
3#include <ArduinoJson.h>
4#include <HTTPClient.h>
5#include <Logging.h>
6#include <WiFi.h>
7#include <WiFiClientSecure.h>
8
9#include <ctime>
10
11#include "KOReaderCredentialStore.h"
12
13namespace {
14// Device identifier for CrossPoint reader
15constexpr char DEVICE_NAME[] = "CrossPoint";
16constexpr char DEVICE_ID[] = "crosspoint-reader";
17
18void addAuthHeaders(HTTPClient& http) {
19 http.addHeader("Accept", "application/vnd.koreader.v1+json");
20 http.addHeader("x-auth-user", KOREADER_STORE.getUsername().c_str());
21 http.addHeader("x-auth-key", KOREADER_STORE.getMd5Password().c_str());
22
23 // HTTP Basic Auth (RFC 7617) header. This is needed to support koreader sync server embedded in Calibre Web Automated
24 // (https://github.com/crocodilestick/Calibre-Web-Automated/blob/main/cps/progress_syncing/protocols/kosync.py)
25 http.setAuthorization(KOREADER_STORE.getUsername().c_str(), KOREADER_STORE.getPassword().c_str());
26}
27
28bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0; }
29} // namespace
30
31KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
32 if (!KOREADER_STORE.hasCredentials()) {
33 LOG_DBG("KOSync", "No credentials configured");
34 return NO_CREDENTIALS;
35 }
36
37 std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth";
38 LOG_DBG("KOSync", "Authenticating: %s", url.c_str());
39
40 HTTPClient http;
41 std::unique_ptr<WiFiClientSecure> secureClient;
42 WiFiClient plainClient;
43
44 if (isHttpsUrl(url)) {
45 secureClient.reset(new WiFiClientSecure);
46 secureClient->setInsecure();
47 http.begin(*secureClient, url.c_str());
48 } else {
49 http.begin(plainClient, url.c_str());
50 }
51 addAuthHeaders(http);
52
53 const int httpCode = http.GET();
54 http.end();
55
56 LOG_DBG("KOSync", "Auth response: %d", httpCode);
57
58 if (httpCode == 200) {
59 return OK;
60 } else if (httpCode == 401) {
61 return AUTH_FAILED;
62 } else if (httpCode < 0) {
63 return NETWORK_ERROR;
64 }
65 return SERVER_ERROR;
66}
67
68KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash,
69 KOReaderProgress& outProgress) {
70 if (!KOREADER_STORE.hasCredentials()) {
71 LOG_DBG("KOSync", "No credentials configured");
72 return NO_CREDENTIALS;
73 }
74
75 std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash;
76 LOG_DBG("KOSync", "Getting progress: %s", url.c_str());
77
78 HTTPClient http;
79 std::unique_ptr<WiFiClientSecure> secureClient;
80 WiFiClient plainClient;
81
82 if (isHttpsUrl(url)) {
83 secureClient.reset(new WiFiClientSecure);
84 secureClient->setInsecure();
85 http.begin(*secureClient, url.c_str());
86 } else {
87 http.begin(plainClient, url.c_str());
88 }
89 addAuthHeaders(http);
90
91 const int httpCode = http.GET();
92
93 if (httpCode == 200) {
94 // Parse JSON response from response string
95 String responseBody = http.getString();
96 http.end();
97
98 JsonDocument doc;
99 const DeserializationError error = deserializeJson(doc, responseBody);
100
101 if (error) {
102 LOG_ERR("KOSync", "JSON parse failed: %s", error.c_str());
103 return JSON_ERROR;
104 }
105
106 outProgress.document = documentHash;
107 outProgress.progress = doc["progress"].as<std::string>();
108 outProgress.percentage = doc["percentage"].as<float>();
109 outProgress.device = doc["device"].as<std::string>();
110 outProgress.deviceId = doc["device_id"].as<std::string>();
111 outProgress.timestamp = doc["timestamp"].as<int64_t>();
112
113 LOG_DBG("KOSync", "Got progress: %.2f%% at %s", outProgress.percentage * 100, outProgress.progress.c_str());
114 return OK;
115 }
116
117 http.end();
118
119 LOG_DBG("KOSync", "Get progress response: %d", httpCode);
120
121 if (httpCode == 401) {
122 return AUTH_FAILED;
123 } else if (httpCode == 404) {
124 return NOT_FOUND;
125 } else if (httpCode < 0) {
126 return NETWORK_ERROR;
127 }
128 return SERVER_ERROR;
129}
130
131KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgress& progress) {
132 if (!KOREADER_STORE.hasCredentials()) {
133 LOG_DBG("KOSync", "No credentials configured");
134 return NO_CREDENTIALS;
135 }
136
137 std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress";
138 LOG_DBG("KOSync", "Updating progress: %s", url.c_str());
139
140 HTTPClient http;
141 std::unique_ptr<WiFiClientSecure> secureClient;
142 WiFiClient plainClient;
143
144 if (isHttpsUrl(url)) {
145 secureClient.reset(new WiFiClientSecure);
146 secureClient->setInsecure();
147 http.begin(*secureClient, url.c_str());
148 } else {
149 http.begin(plainClient, url.c_str());
150 }
151 addAuthHeaders(http);
152 http.addHeader("Content-Type", "application/json");
153
154 // Build JSON body (timestamp not required per API spec)
155 JsonDocument doc;
156 doc["document"] = progress.document;
157 doc["progress"] = progress.progress;
158 doc["percentage"] = progress.percentage;
159 doc["device"] = DEVICE_NAME;
160 doc["device_id"] = DEVICE_ID;
161
162 std::string body;
163 serializeJson(doc, body);
164
165 LOG_DBG("KOSync", "Request body: %s", body.c_str());
166
167 const int httpCode = http.PUT(body.c_str());
168 http.end();
169
170 LOG_DBG("KOSync", "Update progress response: %d", httpCode);
171
172 if (httpCode == 200 || httpCode == 202) {
173 return OK;
174 } else if (httpCode == 401) {
175 return AUTH_FAILED;
176 } else if (httpCode < 0) {
177 return NETWORK_ERROR;
178 }
179 return SERVER_ERROR;
180}
181
182const char* KOReaderSyncClient::errorString(Error error) {
183 switch (error) {
184 case OK:
185 return "Success";
186 case NO_CREDENTIALS:
187 return "No credentials configured";
188 case NETWORK_ERROR:
189 return "Network error";
190 case AUTH_FAILED:
191 return "Authentication failed";
192 case SERVER_ERROR:
193 return "Server error (try again later)";
194 case JSON_ERROR:
195 return "JSON parse error";
196 case NOT_FOUND:
197 return "No progress found";
198 default:
199 return "Unknown error";
200 }
201}