#include "../include/matrixclient.h" #include #include #include <3ds.h> #include #include #include #include #include "util.h" #include "memorystore.h" #include #include #include #define SOC_ALIGN 0x1000 #define SOC_BUFFERSIZE 0x100000 #define SYNC_TIMEOUT 10000 #define DEBUG 1 #if DEBUG #define D #else #define D for(;0;) #endif PrintConsole* topScreenConsole = NULL; #define printf_top(f_, ...) do {consoleSelect(topScreenConsole);printf((f_), ##__VA_ARGS__);} while(0) namespace Matrix { #define POST_BUFFERSIZE 0x100000 static u32 *SOC_buffer = NULL; bool HTTPC_inited = false; Client::Client(std::string homeserverUrl, std::string matrixToken, Store* clientStore) { hsUrl = homeserverUrl; token = matrixToken; if (!clientStore) { clientStore = new MemoryStore(); } store = clientStore; if (!topScreenConsole) { topScreenConsole = new PrintConsole; consoleInit(GFX_TOP, topScreenConsole); } } std::string Client::getToken() { return token; } bool Client::login(std::string username, std::string password) { json_t* request = json_object(); json_object_set_new(request, "type", json_string("m.login.password")); json_t* identifier = json_object(); json_object_set_new(identifier, "type", json_string("m.id.user")); json_object_set_new(identifier, "user", json_string(username.c_str())); json_object_set_new(request, "identifier", identifier); json_object_set_new(request, "password", json_string(password.c_str())); json_object_set_new(request, "initial_device_display_name", json_string("Nintendo 3DS")); json_t* ret = doRequest("POST", "/_matrix/client/r0/login", request); json_decref(request); if (!ret) { return false; } json_t* accessToken = json_object_get(ret, "access_token"); if (!accessToken) { json_decref(ret); return false; } token = json_string_value(accessToken); json_decref(ret); return true; } std::string Client::getUserId() { if (userIdCache != "") { return userIdCache; } json_t* ret = doRequest("GET", "/_matrix/client/r0/account/whoami"); if (!ret) { return ""; } json_t* userId = json_object_get(ret, "user_id"); if (!userId) { json_decref(ret); return ""; } std::string userIdStr = json_string_value(userId); json_decref(ret); userIdCache = std::string(userIdStr); return userIdCache; } std::string Client::resolveRoom(std::string alias) { if (alias[0] == '!') { return alias; // this is already a room ID, nothing to do } json_t* ret = doRequest("GET", "/_matrix/client/r0/directory/room/" + urlencode(alias)); if (!ret) { return ""; } json_t* roomId = json_object_get(ret, "room_id"); if (!roomId) { json_decref(ret); return ""; } std::string roomIdStr = json_string_value(roomId); D printf_top("Room ID: %s\n", roomIdStr.c_str()); json_decref(ret); return roomIdStr; } std::vector Client::getJoinedRooms() { std::vector rooms; json_t* ret = doRequest("GET", "/_matrix/client/r0/joined_rooms"); json_t* roomsArr = json_object_get(ret, "joined_rooms"); if (!roomsArr) { json_decref(ret); return rooms; } size_t index; json_t* value; json_array_foreach(roomsArr, index, value) { rooms.push_back(json_string_value(value)); } json_decref(ret); return rooms; } RoomInfo Client::getRoomInfo(std::string roomId) { // if we resolve the roomId here it only resolves once roomId = resolveRoom(roomId); RoomInfo info = { name: getRoomName(roomId), topic: getRoomTopic(roomId), avatarUrl: getRoomAvatar(roomId), }; return info; } UserInfo Client::getUserInfo(std::string userId, std::string roomId) { std::string displayname = ""; std::string avatarUrl = ""; if (roomId != "") { // first try fetching fro the room json_t* ret = getStateEvent(roomId, "m.room.member", userId); if (ret) { json_t* val; val = json_object_get(ret, "displayname"); if (val) { displayname = json_string_value(val); } val = json_object_get(ret, "avatar_url"); if (val) { avatarUrl = json_string_value(val); } json_decref(ret); } } if (displayname == "") { // then attempt the account std::string path = "/_matrix/client/r0/profile/" + urlencode(userId); json_t* ret = doRequest("GET", path); if (ret) { json_t* val; val = json_object_get(ret, "displayname"); if (val) { displayname = json_string_value(val); } val = json_object_get(ret, "avatar_url"); if (val) { avatarUrl = json_string_value(val); } json_decref(ret); } } UserInfo info = { displayname: displayname, avatarUrl: avatarUrl, }; return info; } std::string Client::getRoomName(std::string roomId) { json_t* ret = getStateEvent(roomId, "m.room.name", ""); if (!ret) { return ""; } json_t* name = json_object_get(ret, "name"); if (!name) { json_decref(ret); return ""; } std::string nameStr = json_string_value(name); json_decref(ret); return nameStr; } std::string Client::getRoomTopic(std::string roomId) { json_t* ret = getStateEvent(roomId, "m.room.topic", ""); if (!ret) { return ""; } json_t* topic = json_object_get(ret, "topic"); if (!topic) { json_decref(ret); return ""; } std::string topicStr = json_string_value(topic); json_decref(ret); return topicStr; } std::string Client::getRoomAvatar(std::string roomId) { json_t* ret = getStateEvent(roomId, "m.room.avatar", ""); if (!ret) { return ""; } json_t* url = json_object_get(ret, "url"); if (!url) { json_decref(ret); return ""; } std::string urlStr = json_string_value(url); json_decref(ret); return urlStr; } std::string Client::sendEmote(std::string roomId, std::string text) { json_t* request = json_object(); json_object_set_new(request, "msgtype", json_string("m.emote")); json_object_set_new(request, "body", json_string(text.c_str())); std::string eventId = sendMessage(roomId, request); json_decref(request); return eventId; } std::string Client::sendNotice(std::string roomId, std::string text) { json_t* request = json_object(); json_object_set_new(request, "msgtype", json_string("m.notice")); json_object_set_new(request, "body", json_string(text.c_str())); std::string eventId = sendMessage(roomId, request); json_decref(request); return eventId; } std::string Client::sendText(std::string roomId, std::string text) { json_t* request = json_object(); json_object_set_new(request, "msgtype", json_string("m.text")); json_object_set_new(request, "body", json_string(text.c_str())); std::string eventId = sendMessage(roomId, request); json_decref(request); return eventId; } std::string Client::sendMessage(std::string roomId, json_t* content) { return sendEvent(roomId, "m.room.message", content); } std::string Client::sendEvent(std::string roomId, std::string eventType, json_t* content) { roomId = resolveRoom(roomId); std::string txid = std::to_string(time(NULL)) + "_REQ_" + std::to_string(requestId); std::string path = "/_matrix/client/r0/rooms/" + urlencode(roomId) + "/send/" + urlencode(eventType) + "/" + urlencode(txid); json_t* ret = doRequest("PUT", path, content); if (!ret) { return ""; } json_t* eventId = json_object_get(ret, "event_id"); if (!eventId) { json_decref(ret); return ""; } std::string eventIdStr = json_string_value(eventId); json_decref(ret); return eventIdStr; } json_t* Client::getStateEvent(std::string roomId, std::string type, std::string stateKey) { roomId = resolveRoom(roomId); std::string path = "/_matrix/client/r0/rooms/" + urlencode(roomId) + "/state/" + urlencode(type) + "/" + urlencode(stateKey); return doRequest("GET", path); } std::string Client::sendStateEvent(std::string roomId, std::string type, std::string stateKey, json_t* content) { roomId = resolveRoom(roomId); std::string path = "/_matrix/client/r0/rooms/" + urlencode(roomId) + "/state/" + urlencode(type) + "/" + urlencode(stateKey); json_t* ret = doRequest("PUT", path, content); if (!ret) { return ""; } json_t* eventId = json_object_get(ret, "event_id"); if (!eventId) { json_decref(ret); return ""; } std::string eventIdStr = json_string_value(eventId); json_decref(ret); return eventIdStr; } std::string Client::redactEvent(std::string roomId, std::string eventId, std::string reason) { roomId = resolveRoom(roomId); std::string txid = std::to_string(time(NULL)) + "_REQ_" + std::to_string(requestId); json_t* content = json_object(); if (reason != "") { json_object_set_new(content, "reason", json_string(reason.c_str())); } std::string path = "/_matrix/client/r0/rooms/" + urlencode(roomId) + "/redact/" + urlencode(eventId) + "/" + txid; json_t* ret = doRequest("PUT", path, content); json_decref(content); if (!ret) { return ""; } json_t* retEventId = json_object_get(ret, "event_id"); if (!retEventId) { json_decref(ret); return ""; } const char* eventIdStr = json_string_value(retEventId); json_decref(ret); return eventIdStr; } void startSyncLoopWithoutClass(void* arg) { ((Client*)arg)->startSync(); } void Client::startSyncLoop() { stopSyncLoop(); // first we stop an already running sync loop isSyncing = true; stopSyncing = false; s32 prio = 0; svcGetThreadPriority(&prio, CUR_THREAD_HANDLE); syncThread = threadCreate(startSyncLoopWithoutClass, this, 8*1024, prio-1, -2, true); } void Client::stopSyncLoop() { stopSyncing = true; if (isSyncing) { threadJoin(syncThread, U64_MAX); threadFree(syncThread); } isSyncing = false; } void Client::setSyncEventCallback(void (*cb)(std::string roomId, json_t* event)) { sync_event_callback = cb; } void Client::processSync(json_t* sync) { json_t* rooms = json_object_get(sync, "rooms"); if (!rooms) { return; // nothing to do } json_t* leftRooms = json_object_get(rooms, "leave"); json_t* invitedRooms = json_object_get(rooms, "invite"); json_t* joinedRooms = json_object_get(rooms, "join"); const char* roomId; json_t* room; size_t index; json_t* event; if (leftRooms) { json_object_foreach(leftRooms, roomId, room) { // rooms that we left // emit leave event with roomId } } if (invitedRooms) { json_object_foreach(invitedRooms, roomId, room) { // rooms that we were invited to } } if (joinedRooms) { json_object_foreach(joinedRooms, roomId, room) { // rooms that we are joined D printf_top("%s:\n", roomId); json_t* timeline = json_object_get(room, "timeline"); if (!timeline) { D printf_top("no timeline\n"); continue; } json_t* events = json_object_get(timeline, "events"); if (!events) { D printf_top("no events\n"); continue; } json_array_foreach(events, index, event) { json_t* eventType = json_object_get(event, "type"); D printf_top("%s\n", json_string_value(eventType)); if (sync_event_callback) { sync_event_callback(roomId, event); } } } } } void Client::startSync() { while (true) { std::string token = store->getSyncToken(); if (stopSyncing) { return; } json_t* ret = doSync(token); if (ret) { // set the token for the next batch json_t* token = json_object_get(ret, "next_batch"); if (token) { store->setSyncToken(json_string_value(token)); } else { store->setSyncToken(""); } processSync(ret); json_decref(ret); } svcSleepThread((u64)1000000ULL * (u64)200); } } json_t* Client::doSync(std::string token) { // D printf_top("Doing sync with token %s\n", token.c_str()); std::string query = "?full_state=false&timeout=" + std::to_string(SYNC_TIMEOUT); if (token != "") { query += "&since=" + token; } return doRequest("GET", "/_matrix/client/r0/sync" + query, NULL); } size_t DoRequestWriteCallback(char *contents, size_t size, size_t nmemb, void *userp) { ((std::string*)userp)->append((char*)contents, size * nmemb); return size * nmemb; } bool doingCurlRequest = false; json_t* Client::doRequest(const char* method, std::string path, json_t* body) { std::string url = hsUrl + path; requestId++; if (!doingCurlRequest) { doingCurlRequest = true; json_t* ret = doRequestCurl(method, url, body); doingCurlRequest = false; return ret; } else { return doRequestHttpc(method, url, body); } } json_t* Client::doRequestCurl(const char* method, std::string url, json_t* body) { D printf_top("Opening Request %d with CURL\n%s\n", requestId, url.c_str()); if (!SOC_buffer) { SOC_buffer = (u32*)memalign(0x1000, POST_BUFFERSIZE); if (!SOC_buffer) { return NULL; } if (socInit(SOC_buffer, POST_BUFFERSIZE) != 0) { return NULL; } } CURL* curl = curl_easy_init(); CURLcode res; if (!curl) { D printf_top("curl init failed\n"); return NULL; } std::string readBuffer; struct curl_slist* headers = NULL; if (token != "") { headers = curl_slist_append(headers, ("Authorization: Bearer " + token).c_str()); } if (body) { headers = curl_slist_append(headers, "Content-Type: application/json"); const char* bodyStr = json_dumps(body, JSON_ENSURE_ASCII | JSON_ESCAPE_SLASH); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, bodyStr); } curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 102400L); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "3ds"); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 50L); curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, DoRequestWriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); // curl_easy_setopt(curl, CURLOPT_STDERR, stdout); res = curl_easy_perform(curl); curl_easy_cleanup(curl); if (res != CURLE_OK) { D printf_top("curl res not ok %d\n", res); return NULL; } // D printf_top("%s\n", readBuffer.c_str()); json_error_t error; json_t* content = json_loads(readBuffer.c_str(), 0, &error); if (!content) { D printf_top("Failed to parse json\n"); return NULL; } return content; } json_t* Client::doRequestHttpc(const char* method, std::string url, json_t* body) { D printf_top("Opening Request %d with HTTPC\n%s\n", requestId, url.c_str()); if (!HTTPC_inited) { if (httpcInit(POST_BUFFERSIZE) != 0) { return NULL; } HTTPC_inited = true; } Result ret = 0; httpcContext context; HTTPC_RequestMethod methodReal = HTTPC_METHOD_GET; u32 statusCode = 0; u32 contentSize = 0, readsize = 0, size = 0; u8* buf, *lastbuf; if (strcmp(method, "GET") == 0) { D printf_top("method GET\n"); methodReal = HTTPC_METHOD_GET; } else if (strcmp(method, "PUT") == 0) { D printf_top("method PUT\n"); methodReal = HTTPC_METHOD_PUT; } else if (strcmp(method, "POST") == 0) { D printf_top("method POST\n"); methodReal = HTTPC_METHOD_POST; } else if (strcmp(method, "DELETE") == 0) { D printf_top("method DELETE\n"); methodReal = HTTPC_METHOD_DELETE; } do { httpcOpenContext(&context, methodReal, url.c_str(), 1); httpcSetSSLOpt(&context, SSLCOPT_DisableVerify); httpcSetKeepAlive(&context, HTTPC_KEEPALIVE_ENABLED); httpcAddRequestHeaderField(&context, "User-Agent", "3ds"); if (token != "") { httpcAddRequestHeaderField(&context, "Authorization", ("Bearer " + token).c_str()); } httpcAddRequestHeaderField(&context, "Connection", "Keep-Alive"); if (body) { httpcAddRequestHeaderField(&context, "Content-Type", "application/json"); const char* bodyStr = json_dumps(body, JSON_ENSURE_ASCII | JSON_ESCAPE_SLASH); httpcAddPostDataRaw(&context, (u32*)bodyStr, strlen(bodyStr)); } ret = httpcBeginRequest(&context); if (ret) { D printf_top("Failed to perform request\n"); httpcCloseContext(&context); return NULL; } httpcGetResponseStatusCode(&context, &statusCode); if ((statusCode >= 301 && statusCode <= 303) || (statusCode >= 307 && statusCode <= 308)) { char newUrl[0x100]; ret = httpcGetResponseHeader(&context, "Location", newUrl, 0x100); url = std::string(newUrl); } } while ((statusCode >= 301 && statusCode <= 303) || (statusCode >= 307 && statusCode <= 308)); ret = httpcGetDownloadSizeState(&context, NULL, &contentSize); if (ret != 0) { httpcCloseContext(&context); return NULL; } // Start with a single page buffer buf = (u8*)malloc(0x1000); if (!buf) { httpcCloseContext(&context); return NULL; } do { // This download loop resizes the buffer as data is read. ret = httpcDownloadData(&context, buf+size, 0x1000, &readsize); size += readsize; if (ret == (s32)HTTPC_RESULTCODE_DOWNLOADPENDING) { lastbuf = buf; // Save the old pointer, in case realloc() fails. buf = (u8*)realloc(buf, size + 0x1000); if (!buf) { httpcCloseContext(&context); free(lastbuf); return NULL; } } } while (ret == (s32)HTTPC_RESULTCODE_DOWNLOADPENDING); if (ret) { httpcCloseContext(&context); free(buf); return NULL; } // Resize the buffer back down to our actual final size lastbuf = buf; buf = (u8*)realloc(buf, size + 1); // +1 for zero-termination if (!buf) { // realloc() failed. httpcCloseContext(&context); free(lastbuf); return NULL; } buf[size] = '\0'; // zero-terminate httpcCloseContext(&context); // D printf_top("%s\n", buf); json_error_t error; json_t* content = json_loads((char*)buf, 0, &error); free(buf); if (!content) { D printf_top("Failed to parse json\n"); return NULL; } return content; } }; // namespace Matrix