matrix_thing/src/matrix.cpp
2019-03-05 14:19:32 -08:00

480 lines
15 KiB
C++

#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 <FreeImagePlus.h>
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<raii::rjp_string> bot::list_rooms(void){
std::vector<raii::rjp_string> 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<char> buffer;
fileout.write_proc = [](void* ptr, unsigned int size, unsigned int nmemb, void* fp) -> unsigned int{
std::vector<char>& buffer = *reinterpret_cast<std::vector<char>*>(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<raii::string*>(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};
}
}
}