-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbluesky.h
272 lines (255 loc) · 10 KB
/
bluesky.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/* Copyright (c) 2023 - John Mueller / MIT license */
#define BLUESKY_USERAGENT "Funky Banana/1.0"
#define BLUESKY_JSON_MAX 2048
// ============================================================
// == Lots of helper functions, the useful ones are on the bottom
// ============================================================
// Filter common unicodes, drop all non-ascii7
// non-latin text gets dropped, but we don't have full unicode fonts, shrug
const std::string _bluesky_filter_text(const std::string str) {
std::string out;
out = str;
// swap out common unicode characters to make it readable
std::vector<std::string> replaceable{"‘'", "’'", "“\"", "”\""};
for (auto &swapper : replaceable) {
std::replace( out.begin(), out.end(), swapper[0], swapper[1]);
}
// drop non-ascii7 characters
out.erase(std::remove_if(out.begin(), out.end(), [](char &ch){
return ((ch<32) || (ch>126));
}), out.end());
return out;
}
// just makes the XML RPC URL
const std::string _bluesky_xrpc_url(std::string service) {
std::string s = id(bs_server_host);
s += "xrpc/" + service; // com.atproto.server.createSession
return s;
}
// Call POST endpoints, supply data as JSON in body
const std::string _bluesky_post(const std::string service,
const std::map<std::string, std::string> &payload_map,
boolean include_auth) {
WiFiClientSecure wifiClient;
wifiClient.setInsecure();
HTTPClient http;
http.begin(wifiClient, _bluesky_xrpc_url(service).c_str());
http.setUserAgent(BLUESKY_USERAGENT);
http.addHeader("Content-Type", "application/json", false, true);
if (include_auth) {
http.addHeader("Authorization", id(bs_user_auth).c_str(), false, true);
}
DynamicJsonDocument doc(BLUESKY_JSON_MAX);
JsonObject root = doc.to<JsonObject>();
for (const auto& n : payload_map) {
root[n.first] = n.second;
}
std::string payload_str;
serializeJson(doc, payload_str);
int res = http.sendRequest("POST", payload_str.c_str());
ESP_LOGD("_bluesky_post:", "HTTP result: %i", res);
if (res==200) {
return std::string(http.getString().c_str());
} else {
return "";
}
}
// URL-Encode a string
const std::string _bluesky_urlencode(const std::string str) {
std::string output;
char const static hexes[]="0123456789abcdef";
for (const unsigned char &c : str) {
if ( (c>='a' && c<='z') || (c>='A' && c<='Z') || (c>='0' && c<='9') ) {
output += c;
} else if (c==' ') {
output += '+';
} else {
output += "%" + std::string(1, hexes[c/16]) + std::string(1, hexes[c%16]);
}
}
return output;
}
// copied from https://esphome.io/api/json__util_8cpp_source.html
// copy of json::parse_json, with more nesting
void _bluesky_parse_json(const std::string &data, const json::json_parse_t &f) {
// Here we are allocating 1.5 times the data size,
// with the heap size minus 2kb to be safe if less than that
// as we can not have a true dynamic sized document.
// The excess memory is freed below with `shrinkToFit()`
#ifdef USE_ESP8266
const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance)
#elif defined(USE_ESP32)
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
#elif defined(USE_RP2040)
const size_t free_heap = rp2040.getFreeHeap();
#endif
// NEW ==> for logging
const char *TAG = "_bluesky_parse_json";
bool pass = false;
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5));
do {
DynamicJsonDocument json_document(request_size);
if (json_document.capacity() == 0) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size,
free_heap);
return;
}
// NEW ==> change nesting limit from 10 to 20
DeserializationError err = deserializeJson(json_document, data,
DeserializationOption::NestingLimit(20));
json_document.shrinkToFit();
JsonObject root = json_document.as<JsonObject>();
if (err == DeserializationError::Ok) {
pass = true;
f(root);
} else if (err == DeserializationError::NoMemory) {
if (request_size * 2 >= free_heap) {
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
return;
}
ESP_LOGV(TAG, "Increasing memory allocation.");
request_size *= 2;
continue;
} else {
ESP_LOGE(TAG, "JSON parse error: %s", err.c_str());
return;
}
} while (!pass);
}
// split string into words
const std::vector<std::string> _bluesky_words(const std::string str) {
std::string word;
std::string sentence = str;
std::vector<std::string> out;
size_t pos = 0;
while ((pos = sentence.find(" ")) != std::string::npos) {
word = sentence.substr(0, pos);
if (!word.empty()) out.push_back(word);
sentence.erase(0, pos + 1);
}
if (!sentence.empty()) out.push_back(sentence);
return out;
}
// generic GET request for
const std::string _bluesky_get(const std::string service,
const std::map<std::string, std::string> &query_map,
boolean include_auth) {
WiFiClientSecure wifiClient;
wifiClient.setInsecure();
HTTPClient http;
std::string url;
for (const auto& n : query_map) {
if (url=="") url += "?"; else url += "&";
url += _bluesky_urlencode(n.first) + "=" + _bluesky_urlencode(n.second);
}
url = _bluesky_xrpc_url(service) + url;
ESP_LOGD("_bluesky_get:", "URL: %s", url.c_str());
http.begin(wifiClient, url.c_str());
http.setUserAgent(BLUESKY_USERAGENT);
if (include_auth) {
http.addHeader("Authorization", id(bs_user_auth).c_str(), false, true);
}
int res = http.sendRequest("GET");
ESP_LOGD("_bluesky_get:", "HTTP result: %i", res);
if (res==200) {
return std::string(http.getString().c_str());
} else {
return "";
}
}
// ============================================================
// == Stuff here is for calling
// ============================================================
// log in with your account
const boolean bluesky_login(std::string username, std::string password) {
id(bs_post_text) = "Logging in";
std::map<std::string, std::string> m;
m["identifier"] = username;
m["password"] = password;
id(bs_user_auth) = "";
std::string res = _bluesky_post("com.atproto.server.createSession", m, false);
if (res!="") {
json::parse_json(res, [](JsonObject root) {
const std::string my_did = root["did"];
const std::string my_handle = root["handle"];
const std::string my_auth = root["accessJwt"];
id(bs_user_did) = my_did; // store
id(bs_user_handle) = my_handle;
id(bs_user_auth) = "Bearer " + my_auth; // store auth with Bearer text
ESP_LOGD("bluesky_login:", "Received DID: '%s', handle: '%s'",
my_did.c_str(), my_handle.c_str());
});
id(bs_logged_in) = (id(bs_user_auth)!="");
} else { // login failed, clear settings
ESP_LOGD("bluesky_login:", "No data received.");
id(bs_user_did) = "";
id(bs_user_handle) = "";
id(bs_user_auth) = "";
id(bs_logged_in) = false;
}
if (id(bs_logged_in)) id(bs_post_text) = "Logged in.";
else id(bs_post_text) = "Login failed.";
return id(bs_logged_in);
}
// get notification count
const boolean bluesky_check_unread() {
if (!id(bs_logged_in)) return false;
std::map<std::string, std::string> q;
std::string res = _bluesky_get("app.bsky.notification.getUnreadCount", q, true);
if (res!="") {
_bluesky_parse_json(res, [](JsonObject root) {
const int count = root["count"];
id(bs_has_unread) = count;
ESP_LOGI("bluesky_check_unread:", "Count: %i", count);
});
return true;
} else {
ESP_LOGD("bluesky_check_unread:", "No data received.");
}
return false;
}
// get top popular posts
std::map<std::string, std::string> bluesky_get_pops(boolean filter_text) {
std::map<std::string, std::string> ret;
if (!id(bs_logged_in)) { ret["error"] = "login"; return ret; }
std::map<std::string, std::string> q;
q["limit"] = "1";
std::string res = _bluesky_get("app.bsky.unspecced.getPopular", q, true);
if (res!="") {
ESP_LOGD("bluesky_get_pops:", res.c_str());
ret["handle"] = ""; ret["name"] = ""; ret["date"] = ""; ret["text"] = "";
//json::parse_json(res, [&ret](JsonObject root) {
_bluesky_parse_json(res, [&ret](JsonObject root) {
const std::string shand = root["feed"][0]["post"]["author"]["handle"];
const std::string sname = root["feed"][0]["post"]["author"]["displayName"];
const std::string sdate = root["feed"][0]["post"]["record"]["createdAt"];
const std::string stext = root["feed"][0]["post"]["record"]["text"];
ret["handle"] = shand;
ret["name"] = sname;
ret["date"] = sdate;
ret["text"] = stext;
ESP_LOGI("bluesky_get_pops:", "Text: '%s'", stext.c_str());
});
if (filter_text) {
for (const auto& n : ret) ret[n.first] = _bluesky_filter_text(n.second);
}
std::vector<std::string> v = _bluesky_words(ret["text"]);
id(bs_disp_words) = v;
ret["error"] = "";
id(bs_post_handle) = ret["handle"];
id(bs_post_name) = ret["name"];
id(bs_post_date) = ret["date"];
id(bs_post_text) = ret["text"];
} else {
ret["error"] = "no data";
ESP_LOGD("bluesky_get_pops:", "No data received.");
}
return ret;
}
// call this once to set the server
void bluesky_set_server(std::string hostname) {
std::string host = hostname;
if (host.rfind("https://", 0) != 0) host = "https://" + host;
if (host.back()!='/') host = host + "/";
id(bs_server_host) = host;
}