#include "raii/video_man.hpp" #include "matrix.hpp" #include "raii/curl_llist.hpp" #include "raii/static_string.hpp" #include "raii/rjp_ptr.hpp" #include "raii/filerd.hpp" #include namespace matrix{ auth_data parse_auth_data(RJP_value* root){ static const char* fields[] = {"username", "password", "homeserver", "alias", "access_token"}; RJP_search_res details[5]; rjp_search_members(root, 5, fields, details, 0); return auth_data{details[0].value, details[1].value, details[2].value, details[3].value, details[4].value}; } //shamelessly stolen from stackoverflow (of all the things to need to steal) constexpr static size_t intlen(int i){ if(i >= 100000) { if(i >= 10000000) { if(i >= 1000000000) return 10; if(i >= 100000000) return 9; return 8; } if(i >= 1000000) return 7; return 6; } else { if(i >= 1000) { if(i >= 10000) return 5; return 4; } else { if(i >= 100) return 3; if(i >= 10) return 2; return 1; } } } static raii::string itostr(int i){ if(i == 0) return raii::string("0"); int place = intlen(i); raii::string ret(place); char* buf = ret.get(); buf[place] = 0; while(i != 0){ int rem = i % 10; buf[--place] = rem + '0'; i /= 10; } return ret; } bot::mat_url_list::mat_url_list(const raii::string_base& homeserver, const raii::string_base& access_token){ repopulate(homeserver, access_token); } void bot::mat_url_list::repopulate_accesstoken(const raii::string_base& homeserver, const raii::string_base& access_token){ create_room = s_proto + homeserver + "/_matrix/client/r0/createRoom?access_token=" + access_token; file_upload = s_proto + homeserver + "/_matrix/media/r0/upload?access_token=" + access_token; room_list = s_proto + homeserver + "/_matrix/client/r0/joined_rooms?access_token=" + access_token; whoami = s_proto + homeserver + "/_matrix/client/r0/account/whoami?access_token=" + access_token; } void bot::mat_url_list::repopulate(const raii::string_base& homeserver, const raii::string_base& access_token){ repopulate_accesstoken(homeserver, access_token); alias_lookup = s_proto + homeserver + "/_matrix/client/r0/directory/room/"; login = s_proto + homeserver + "/_matrix/client/r0/login"; } void bot::mat_url_list::invalidate_accesstoken(void){ create_room.reset(); file_upload.reset(); room_list.reset(); whoami.reset(); } bot::bot(const auth_data& a, const raii::string_base& useragent): m_curl(), m_useragent(useragent), m_homeserver(a.homeserver) { _acquire_access_token(a); } bot::bot(const auth_data& a, raii::string&& useragent): m_curl(), m_useragent(std::move(useragent)), m_homeserver(a.homeserver) { _acquire_access_token(a); } const raii::rjp_string& bot::access_token(void)const{ return m_access_token; } const raii::rjp_string& bot::userid(void)const{ return m_userid; } const raii::string& bot::useragent(void)const{ return m_useragent; } void bot::set_useragent(const raii::string_base& useragent){ m_useragent = useragent; } void bot::set_useragent(raii::string&& useragent){ m_useragent = std::move(useragent); } raii::rjp_string bot::room_alias_to_id(const raii::string_base& alias){ auto tmp = m_curl.encode(alias, alias.length()); return _get_and_find(raii::string(m_urls.alias_lookup + tmp), "room_id"_ss); } std::vector bot::list_rooms(void){ std::vector ret; raii::string reply = _get_curl(m_urls.room_list); if(!reply) return ret; raii::rjp_ptr root(rjp_parse(reply)); if(!root) return ret; RJP_search_res res = rjp_search_member(root.get(), "joined_rooms", 0); if(!res.value) return ret; for(RJP_value* v = rjp_get_element(res.value);v;v = rjp_next_element(v)){ ret.emplace_back(v); } return ret; } raii::string bot::create_room(const raii::string_base& name, const raii::string_base& alias){ raii::string postdata; if(alias) postdata = "{\"name\": \"" + raii::json_escape(name) + "\",\"room_alias_name\": \"" + raii::json_escape(alias) + "\"}"; else postdata = "{\"name\": \"" + raii::json_escape(name) + "\"}"; return _post_curl(postdata, m_urls.create_room, raii::curl_llist()); } file_info bot::upload_file(const raii::string_base& filename){ raii::filerd fd(filename); if(!fd) return {}; return file_info{_upload_file(fd, raii::curl_llist{}), filename, {}, fd.length()}; } image_info bot::upload_image(const raii::string_base& filename){ return upload_image(filename, raii::static_string()); } image_info bot::upload_image(const raii::string_base& filename, const raii::string_base& alias){ image_info ret; fipImage image; auto formattotype = [](auto type) -> const char*{ switch(type){ case FIF_JPEG: return "jpeg"; case FIF_PNG: return "png"; case FIF_GIF: return "gif"; default: ; }; return nullptr; }; auto type = fipImage::identifyFIF(filename.get()); image.load(filename.get()); raii::filerd fd(filename, "rb"); if(!fd) return {}; //save fullsize image info ret.width = image.getWidth(); ret.height = image.getHeight(); ret.filetype = formattotype(type); ret.filename = alias ? alias : filename; ret.filesize = fd.length(); raii::curl_llist header(raii::string("Content-Type: image/" + ret.filetype)); ret.fileurl = _upload_file(fd, header); fd.reset(); //create thumbnail image.makeThumbnail(500); FreeImageIO fileout; std::vector buffer; fileout.write_proc = [](void* ptr, unsigned int size, unsigned int nmemb, void* fp) -> unsigned int{ std::vector& buffer = *reinterpret_cast*>(fp); buffer.insert(buffer.end(), (char*)ptr, ((char*)ptr)+size*nmemb); return size*nmemb; }; bool b = false; switch(type){ case FIF_JPEG: b = image.saveToHandle(type, &fileout, (fi_handle)&buffer, JPEG_QUALITYGOOD | JPEG_SUBSAMPLING_411); break; case FIF_PNG: b = image.saveToHandle(type, &fileout, (fi_handle)&buffer, PNG_Z_BEST_COMPRESSION); break; case FIF_GIF: b = image.saveToHandle(type, &fileout, (fi_handle)&buffer); break; default: ; }; if(!b){ fprintf(stderr, "Unable to create image thumbnail"); ret.thumb_width = ret.width; ret.thumb_height = ret.height; ret.thumbsize = ret.filesize; }else{ ret.thumb_width = image.getWidth(); ret.thumb_height = image.getHeight(); ret.thumburl = _post_and_find(raii::static_string(buffer.data(), buffer.size()), m_urls.file_upload, header, "content_uri"_ss); ret.thumbsize = buffer.size(); } return ret; } video_info bot::upload_video(const raii::string_base& filename){ video_info ret = {}; raii::video_man context(filename); auto packet = context.create_jpg_thumbnail(); raii::curl_llist header("Content-Type: image/jpeg"); ret.thumb_width = context.width(); ret.thumb_height = context.height(); ret.thumbsize = packet->size; ret.thumburl = _post_and_find(raii::static_string((char*)packet->data, packet->size), m_urls.file_upload, header, "content_uri"_ss); raii::string mimetype = context.get_mimetype(); header = raii::curl_llist(raii::string("Content-Type: video/" + mimetype)); raii::filerd fd(filename); if(!fd) return {}; ret.filesize = fd.length(); ret.width = context.width(); ret.height = context.height(); ret.filetype = std::move(mimetype); ret.fileurl = _upload_file(fd, header); ret.filename = filename; return ret; } raii::rjp_string bot::send_file(const raii::string_base& room, const file_info& file){ raii::string url = raii::json_escape(file.fileurl); raii::string body = "{" "\"body\":\"" + raii::json_escape(file.filename) + "\"," "\"info\":{" "\"size\":" + itostr(file.filesize) + "}," "\"msgtype\":\"m.file\"," "\"body\":\"" + file.filename + "\"," "\"url\":\"" + url + "\"" "}"; return _send_message(room, body, raii::curl_llist{}); } raii::rjp_string bot::send_image(const raii::string_base& room, const image_info& image){ raii::string mimetype = "\"mimetype\":\"image/" + raii::json_escape(image.filetype) + "\""; raii::string url = raii::json_escape(image.fileurl); const raii::string_base* thumburl; if(image.thumburl) thumburl = &image.thumburl; else thumburl = &image.fileurl; //compiler intensive raii::string body = "{" "\"body\":\"" + raii::json_escape(image.filename) + "\"," "\"info\":{" "\"h\":" + itostr(image.height) + "," + mimetype + "," "\"size\":" + itostr(image.filesize) + "," "\"thumnail_info\":{" "\"h\":" + itostr(image.thumb_height) + "," + mimetype + "," "\"size\":" + itostr(image.thumbsize) + "," "\"w\":" + itostr(image.thumb_width) + "}," "\"thumbnail_url\":\"" + (*thumburl) + "\"," "\"w\":" + itostr(image.width) + "}," "\"msgtype\":\"m.image\"," "\"url\":\"" + url + "\"" "}"; return _send_message(room, body, raii::curl_llist{}); } raii::rjp_string bot::send_video(const raii::string_base& room, const video_info& video){ raii::string body = "{" "\"body\":\"" + raii::json_escape(video.filename) + "\"," "\"info\":{" "\"h\":" + itostr(video.height) + "," "\"mimetype\":\"video/" + raii::json_escape(video.filetype) + "\"," "\"size\":" + itostr(video.filesize) + "," "\"thumnail_info\":{" "\"h\":" + itostr(video.thumb_height) + "," "\"mimetype\":\"image/jpeg\"," "\"size\":" + itostr(video.thumbsize) + "," "\"w\":" + itostr(video.thumb_width) + "}," "\"thumbnail_url\":\"" + video.thumburl + "\"," "\"w\":" + itostr(video.width) + "}," "\"msgtype\":\"m.video\"," "\"url\":\"" + raii::json_escape(video.fileurl) + "\"" "}"; return _send_message(room, body, raii::curl_llist()); } raii::rjp_string bot::send_message(const raii::string_base& room, const raii::string_base& text){ return _send_message(room, raii::string("{\"body\":\""_ss + raii::json_escape(text) + "\",\"msgtype\":\"m.text\"}"_ss), raii::curl_llist()); } void bot::logout(void){ _get_curl(raii::string("https://" + m_homeserver + "/_matrix/client/r0/logout?access_token=" + m_access_token)); m_urls.invalidate_accesstoken(); } /******************************* Internal functions ********************************/ raii::rjp_string bot::_upload_file(raii::filerd& fp, const raii::curl_llist& header){ raii::string fileurl; m_curl.postreq(); m_curl.setopt(CURLOPT_POSTFIELDS, NULL); m_curl.setopt(CURLOPT_READDATA, (void*)fp.get()); m_curl.setopt(CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)fp.length()); m_curl.setopt(CURLOPT_INFILESIZE_LARGE, (curl_off_t)fp.length()); m_curl.seturl(m_urls.file_upload); m_curl.setheader(header); m_curl.setopt(CURLOPT_WRITEFUNCTION, _post_reply_curl_callback); m_curl.setopt(CURLOPT_WRITEDATA, &fileurl); CURLcode cres = m_curl.perform(); m_curl.setopt(CURLOPT_READDATA, NULL); if(cres != CURLE_OK) return {}; if(!fileurl) return {}; raii::rjp_ptr root(rjp_parse(fileurl)); if(!root) return {}; RJP_search_res res = rjp_search_member(root.get(), "content_uri", 0); return res.value; } raii::rjp_string bot::_send_message(const raii::string_base& room, const raii::string_base& msg, const raii::curl_llist& header){ raii::rjp_string reply = _post_and_find( msg, raii::string("https://" + m_homeserver + "/_matrix/client/r0/rooms/" + m_curl.encode(room) + "/send/m.room.message?access_token=" + m_access_token), header, "event_id"_ss); return reply; } size_t bot::_post_reply_curl_callback(char* ptr, size_t size, size_t nmemb, void* userdata){ raii::string* data = reinterpret_cast(userdata); (*data) += ptr; return size*nmemb; } raii::string bot::_get_curl(const raii::string_base& url){ raii::string reply; m_curl.getreq(); m_curl.seturl(url); m_curl.setheader(raii::curl_llist{}); m_curl.setopt(CURLOPT_WRITEFUNCTION, _post_reply_curl_callback); m_curl.setopt(CURLOPT_WRITEDATA, &reply); CURLcode res = m_curl.perform(); if(res != CURLE_OK) return {}; return reply; } raii::string bot::_post_curl(const raii::string_base& postdata, const raii::string_base& url, const raii::curl_llist& header){ raii::string reply; m_curl.postreq(); m_curl.setopt(CURLOPT_POSTFIELDS, postdata.get()); m_curl.setopt(CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)postdata.length()); m_curl.seturl(url); m_curl.setheader(header); m_curl.setopt(CURLOPT_WRITEFUNCTION, _post_reply_curl_callback); m_curl.setopt(CURLOPT_WRITEDATA, &reply); CURLcode res = m_curl.perform(); if(res != CURLE_OK) return {}; return reply; } raii::rjp_string bot::_post_and_find(const raii::string_base& data, const raii::string_base& url, const raii::curl_llist& header, const raii::string_base& target) { raii::string reply = _post_curl(data, url, header); if(!reply) return {}; return _curl_reply_search(reply, target); } raii::rjp_string bot::_get_and_find(const raii::string_base& url, const raii::string_base& target){ raii::string reply = _get_curl(url); if(!reply) return {}; return _curl_reply_search(reply, target); } raii::rjp_string bot::_curl_reply_search(const raii::string_base& reply, const raii::string_base& target){ raii::rjp_ptr root(rjp_parse(reply)); if(!root) return {}; RJP_search_res res = rjp_search_member(root.get(), target.get(), 0); if(rjp_value_type(res.value) != json_string) return {}; return raii::rjp_string(res.value); } void bot::_set_curl_defaults(void){ m_curl.setopt(CURLOPT_BUFFERSIZE, 102400L); m_curl.setopt(CURLOPT_NOPROGRESS, 1L); m_curl.setuseragent(m_useragent); m_curl.setopt(CURLOPT_MAXREDIRS, 50L); m_curl.setopt(CURLOPT_FOLLOWLOCATION, 1L); m_curl.forcessl(CURL_SSLVERSION_TLSv1_2); m_curl.setopt(CURLOPT_TCP_KEEPALIVE, 1L); } raii::string bot::_request_access_token(const auth_data& a){ CURLcode result; raii::string postdata("{\"type\":\"m.login.password\", \"user\":\"" + raii::json_escape(a.bot_name) + "\", \"password\":\"" + raii::json_escape(a.bot_pass) + "\"}"); raii::string reply; m_curl.seturl(m_urls.login); m_curl.setpostdata(postdata); m_curl.postreq(); m_curl.setopt(CURLOPT_WRITEFUNCTION, _post_reply_curl_callback); m_curl.setopt(CURLOPT_WRITEDATA, &reply); result = m_curl.perform(); if(result != CURLE_OK) return {}; return reply; } void bot::_acquire_access_token(const auth_data& a){ _set_curl_defaults(); if(a.access_token){ m_access_token = a.access_token; m_urls = mat_url_list(m_homeserver, m_access_token); raii::string reply = _get_curl(m_urls.whoami); if(!reply) return; raii::rjp_ptr root(rjp_parse(reply)); if(!root) return; RJP_search_res id = rjp_search_member(root.get(), "user_id", 0); m_userid = raii::rjp_string(id.value); }else{ raii::string reply = _request_access_token(a); if(!reply) return; raii::rjp_ptr root(rjp_parse(reply)); if(!root) return; RJP_search_res token = rjp_search_member(root.get(), "access_token", 0); m_access_token = raii::rjp_string{token.value}; m_urls = mat_url_list(m_homeserver, m_access_token); token = rjp_search_member(root.get(), "user_id", 0); m_userid = raii::rjp_string{token.value}; } } }