480 lines
15 KiB
C++
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};
|
|
}
|
|
}
|
|
|
|
}
|
|
|