diff --git a/.gitignore b/.gitignore index 7d10ca4..eb03c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ data *.swp *.save testout +audio +video diff --git a/TODO b/TODO index 65c4f2e..f9a223f 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,4 @@ use libmagic to determine file types? -//create a raii interface for libav* which allows creation of thumbnails add av_freep cleanup to raii interface for libav* (probably replacing unique_ptr in that case) +raii-ify the muxing function +scale video thumbnails to same size as image thumbnails (500 in either dimension, same aspect ratio) diff --git a/include/matrix.hpp b/include/matrix.hpp index 763d1c0..15dfb93 100644 --- a/include/matrix.hpp +++ b/include/matrix.hpp @@ -26,6 +26,9 @@ #include +#define HAS_FREEIMAGE +#define HAS_FFMPEG + namespace matrix{ struct auth_data{ raii::rjp_string bot_name; @@ -125,9 +128,11 @@ namespace matrix{ //upload media file_info upload_file(const raii::string_base& filename); - image_info upload_image(const raii::string_base& filename, const raii::string_base& alias); + file_info upload_file(const raii::string_base& filename, const raii::string_base& alias); image_info upload_image(const raii::string_base& filename); + image_info upload_image(const raii::string_base& filename, const raii::string_base& alias); video_info upload_video(const raii::string_base& filename); + video_info upload_video(const raii::string_base& filename, const raii::string_base& alias); //send messages raii::rjp_string send_image(const raii::string_base& room, const image_info& image); @@ -140,7 +145,7 @@ namespace matrix{ protected: raii::rjp_string _upload_file(raii::filerd& fp, const raii::curl_llist& header); - raii::rjp_string _send_message(const raii::string_base& room, const raii::string_base& msg, const raii::curl_llist& header); + raii::rjp_string _send_message(const raii::string_base& room, const raii::string_base& msg); static size_t _post_reply_curl_callback(char* ptr, size_t size, size_t nmemb, void* userdata); raii::string _get_curl(const raii::string_base& url); raii::string _post_curl(const raii::string_base& postdata, const raii::string_base& url, const raii::curl_llist& header); diff --git a/include/raii/video_man.hpp b/include/raii/video_man.hpp index c989884..9e123db 100644 --- a/include/raii/video_man.hpp +++ b/include/raii/video_man.hpp @@ -20,9 +20,7 @@ #define RAII_VIDEO_MAN_HPP #include //unique_ptr -#include //exchange #include "raii/string.hpp" -#include "raii/static_string.hpp" //needs to be extern C because quality library writing extern "C"{ @@ -36,23 +34,10 @@ namespace raii{ class video_man { - private: - static inline auto _context_deleter = [](AVCodecContext* ptr){ - avcodec_close(ptr); - avcodec_free_context(&ptr); - }; - static inline auto _frame_deleter = [](AVFrame* ptr){ - av_frame_unref(ptr); - av_frame_free(&ptr); - }; - static inline auto _packet_deleter = [](AVPacket* ptr){ - av_packet_unref(ptr); - av_packet_free(&ptr); - }; public: - using context_type = std::unique_ptr; - using frame_type = std::unique_ptr; - using packet_type = std::unique_ptr; + using context_type = std::unique_ptr; + using frame_type = std::unique_ptr; + using packet_type = std::unique_ptr; private: AVFormatContext* m_vid; @@ -62,145 +47,30 @@ namespace raii{ public: - video_man(const raii::string_base& filename){ - av_register_all(); - _init(filename); - } + video_man(const raii::string_base& filename); video_man(const video_man& v) = delete; - video_man(video_man&& v): - m_vid(std::exchange(v.m_vid, nullptr)){} - ~video_man(void){ - reset(); - } + video_man(video_man&& v); + ~video_man(void); - int height(void)const{ - return m_context->height; - } - int width(void)const{ - return m_context->width; - } - raii::string get_mimetype(void)const{ - const char* first = strstr(m_vid->iformat->name, ","); - return raii::string(raii::static_string(m_vid->iformat->name, first - m_vid->iformat->name)); - } - packet_type create_jpg_thumbnail(void){ - context_type jpg_context = _get_jpeg_context(m_context); - frame_type frame(_init_frame(m_context->width, m_context->height, m_context->pix_fmt), _frame_deleter); - frame_type jpg_frame(_init_frame(m_context->width, m_context->height, jpg_context->pix_fmt), _frame_deleter); - packet_type packet(av_packet_alloc(), _packet_deleter); - SwsContext* sws_ctx = sws_getContext(m_context->width, m_context->height, m_context->pix_fmt, - m_context->width, m_context->height, AV_PIX_FMT_YUV420P, - SWS_BILINEAR, NULL, NULL, NULL); - _change_color_range(sws_ctx); + int height(void)const; + int width(void)const; + raii::string get_mimetype(void)const; + packet_type create_jpg_thumbnail(void); - av_image_alloc(jpg_frame->data, jpg_frame->linesize, m_context->width, m_context->height, jpg_context->pix_fmt, 32); - while(av_read_frame(m_vid, packet.get()) >= 0){ - if(packet->stream_index == m_stream_index){ - avcodec_send_packet(m_context, packet.get()); - if(avcodec_receive_frame(m_context, frame.get()) >= 0) - break; - } - av_packet_unref(packet.get()); - } - sws_scale(sws_ctx, frame->data, frame->linesize, 0, m_context->height, jpg_frame->data, jpg_frame->linesize); + AVFormatContext* release(void); + void reset(const raii::string_base& filename); + void reset(void); - sws_freeContext(sws_ctx); - - packet_type jpg_packet(av_packet_alloc(), _packet_deleter); - if(avcodec_send_frame(jpg_context.get(), jpg_frame.get()) < 0){ - av_freep(&jpg_frame->data[0]); - return packet_type(nullptr, _packet_deleter); - } - if(avcodec_receive_packet(jpg_context.get(), jpg_packet.get()) < 0){ - return packet_type(nullptr, _packet_deleter); - } - - av_freep(&jpg_frame->data[0]); - return jpg_packet; - } - - AVFormatContext* release(void){ - avcodec_close(m_context); - avcodec_free_context(&m_context); - m_context = nullptr; - m_codec = nullptr; - m_stream_index = -1; - return std::exchange(m_vid, nullptr); - } - void reset(const raii::string_base& filename){ - reset(); - _init(filename); - } - void reset(void){ - avcodec_close(m_context); - avcodec_free_context(&m_context); - avformat_close_input(&m_vid); - m_context = nullptr; - m_vid = nullptr; - m_codec = nullptr; - m_stream_index = -1; - } + operator bool(void)const; private: - void _init(const raii::string_base& filename){ - m_vid = _open_video_file(filename); - if(!m_vid) - return; - - AVCodecParameters* codec_parms = m_vid->streams[m_stream_index]->codecpar; - m_codec = avcodec_find_decoder(codec_parms->codec_id); - m_context = avcodec_alloc_context3(m_codec); - avcodec_parameters_to_context(m_context, codec_parms); - avcodec_open2(m_context, m_codec, NULL); - } - AVFormatContext* _open_video_file(const raii::string_base& filename){ - AVFormatContext* context = NULL; - avformat_open_input(&context, filename.get(), NULL, NULL); - avformat_find_stream_info(context, NULL); - for(size_t i = 0;i < context->nb_streams;++i){ - if(context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){ - m_stream_index = i; - break; - } - } - if(m_stream_index != -1) - return context; - avformat_close_input(&context); - return nullptr; - } - static AVFrame* _init_frame(int w, int h, int format){ - AVFrame* f = av_frame_alloc(); - f->data[0] = NULL; - f->width = w; - f->height = h; - f->format = format; - return f; - } - static void _change_color_range(SwsContext* sws_ctx){ - //we want to convert from YUVJ420P to YUV420P for sws_scale to be happy. but we actually want to output as YUVJ420P. - //so we change the color range to use the full range like YUVJ420P. dummy is used to ignore unneeded returns. - int dummy[4]; - int src_range, dest_range; - int br, con, sat; - sws_getColorspaceDetails(sws_ctx, (int**)&dummy, &src_range, (int**)&dummy, &dest_range, &br, &con, &sat); - const int* coefs = sws_getCoefficients(SWS_CS_DEFAULT); - src_range = 2; - sws_setColorspaceDetails(sws_ctx, coefs, src_range, coefs, dest_range, br, con, sat); - } - static context_type _get_jpeg_context(const AVCodecContext* decoder){ - AVCodec* jpg_codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG); - context_type jpg_context(avcodec_alloc_context3(jpg_codec), _context_deleter); - jpg_context->pix_fmt = AV_PIX_FMT_YUVJ420P; - jpg_context->width = decoder->width; - jpg_context->height = decoder->height; - jpg_context->codec_id = AV_CODEC_ID_MJPEG; - jpg_context->codec_type = AVMEDIA_TYPE_VIDEO; - if(decoder->time_base.num == 0 || decoder->time_base.den == 0) - jpg_context->time_base = AVRational{1, 30}; - else - jpg_context->time_base = decoder->time_base; - avcodec_open2(jpg_context.get(), jpg_codec, NULL); - return jpg_context; - } + void _init(const raii::string_base& filename); + AVFormatContext* _open_video_file(const raii::string_base& filename); + static AVFrame* _init_frame(int w, int h, int format); + static void _change_color_range(SwsContext* sws_ctx); + static context_type _get_jpeg_context(const AVCodecContext* decoder); + static void _context_deleter(AVCodecContext* ptr); + static void _frame_deleter(AVFrame* ptr); + static void _packet_deleter(AVPacket* ptr); }; diff --git a/include/reddit.hpp b/include/reddit.hpp index a641091..7c90ed6 100644 --- a/include/reddit.hpp +++ b/include/reddit.hpp @@ -65,6 +65,7 @@ namespace reddit{ private: raii::string m_post; raii::rjp_string m_media_url; + raii::string m_hosted_video_audio; raii::rjp_string m_author; raii::rjp_string m_post_hint; raii::rjp_string m_title; @@ -88,6 +89,7 @@ namespace reddit{ const raii::string& raw(void)const; const raii::rjp_string& mediaurl(void)const; + const raii::string& hosted_video_audio(void)const; const raii::rjp_string& posturl(void)const; const raii::rjp_string& author(void)const; const raii::rjp_string& post_hint(void)const; diff --git a/src/fat_strings.cpp b/src/fat_strings.cpp index 0f56b75..f58f980 100644 --- a/src/fat_strings.cpp +++ b/src/fat_strings.cpp @@ -1,3 +1,21 @@ +/** + This file is a part of rexy's matrix bot + Copyright (C) 2019 rexy712 + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + #include "fat_strings.hpp" #include "matrix.hpp" #include "raii/static_string.hpp" diff --git a/src/matrix.cpp b/src/matrix.cpp index 824b4e2..dc0f314 100644 --- a/src/matrix.cpp +++ b/src/matrix.cpp @@ -1,3 +1,21 @@ +/** + This file is a part of rexy's matrix bot + Copyright (C) 2019 rexy712 + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + #include "matrix.hpp" #include @@ -111,14 +129,18 @@ namespace matrix{ return _post_curl(postdata, m_urls.create_room, raii::curl_llist()); } file_info bot::upload_file(const raii::string_base& filename){ + return upload_file(filename, raii::static_string()); + } + file_info bot::upload_file(const raii::string_base& filename, const raii::string_base& alias){ raii::filerd fd(filename); if(!fd) return {}; - return file_info{_upload_file(fd, raii::curl_llist{}), filename, {}, fd.length()}; + return file_info{_upload_file(fd, raii::curl_llist{}), alias ? alias : filename, {}, fd.length()}; } image_info bot::upload_image(const raii::string_base& filename){ return upload_image(filename, raii::static_string()); } +#ifdef HAS_FREEIMAGE image_info bot::upload_image(const raii::string_base& filename, const raii::string_base& alias){ image_info ret; @@ -133,24 +155,25 @@ namespace matrix{ }; return nullptr; }; + + //determine type of image. used to create thumbnail of the same type and verify this is an image. auto type = fipImage::identifyFIF(filename.get()); + if(type == FIF_UNKNOWN){ + return {}; + } + 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 + raii::curl_llist header(raii::string("Content-Type: image/" + ret.filetype)); + + //create and upload thumbnail image.makeThumbnail(500); + FreeImageIO fileout; std::vector buffer; fileout.write_proc = [](void* ptr, unsigned int size, unsigned int nmemb, void* fp) -> unsigned int{ @@ -184,45 +207,75 @@ namespace matrix{ ret.thumbsize = buffer.size(); } + raii::filerd fd(filename, "rb"); + if(!fd) + return ret; + + ret.filesize = fd.length(); + ret.fileurl = _upload_file(fd, header); + fd.reset(); + return ret; } +#else //HAS_FREEIMAGE + image_info bot::upload_image(const raii::string_base& filename, const raii::string_base& alias){ + image_info ret = {}; + ret = upload_file(filename, alias); + return ret; + } +#endif //HAS_FREEIMAGE + video_info bot::upload_video(const raii::string_base& filename){ + return upload_video(filename, raii::static_string()); + } +#ifdef HAS_FFMPEG + video_info bot::upload_video(const raii::string_base& filename, const raii::string_base& alias){ video_info ret = {}; raii::video_man context(filename); + if(!context) + return {}; 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.width = context.width(); + ret.height = context.height(); + raii::string mimetype = context.get_mimetype(); + context.reset(); //early cleanup to save memory 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(); - + packet.reset(); 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; + ret.filename = alias ? alias : filename; return ret; } +#else //HAS_FFMPEG + video_info bot::upload_video(const raii::string_base& filename, const raii::string_base& alias){ + video_info ret = {}; + ret = upload_file(filename, alias); + return ret; + } +#endif //HAS_FFMPEG raii::rjp_string bot::send_file(const raii::string_base& room, const file_info& file){ - return _send_message(room, detail::_file_body(file), raii::curl_llist{}); + return _send_message(room, detail::_file_body(file)); } raii::rjp_string bot::send_image(const raii::string_base& room, const image_info& image){ - return _send_message(room, detail::_image_body(image), raii::curl_llist{}); + return _send_message(room, detail::_image_body(image)); } raii::rjp_string bot::send_video(const raii::string_base& room, const video_info& video){ - return _send_message(room, detail::_video_body(video), raii::curl_llist()); + return _send_message(room, detail::_video_body(video)); } raii::rjp_string bot::send_message(const raii::string_base& room, const raii::string_base& text){ - return _send_message(room, detail::_message_body(text), raii::curl_llist()); + return _send_message(room, detail::_message_body(text)); } void bot::logout(void){ _get_curl(raii::string("https://" + m_homeserver + "/_matrix/client/r0/logout?access_token=" + m_access_token)); @@ -261,11 +314,11 @@ namespace matrix{ 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 bot::_send_message(const raii::string_base& room, const raii::string_base& msg){ 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, + raii::curl_llist(), "event_id"_ss); return reply; } diff --git a/src/raii/video_man.cpp b/src/raii/video_man.cpp new file mode 100644 index 0000000..352f8e9 --- /dev/null +++ b/src/raii/video_man.cpp @@ -0,0 +1,178 @@ +/** + This file is a part of rexy's matrix bot + Copyright (C) 2019 rexy712 + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +#include "raii/video_man.hpp" +#include "raii/static_string.hpp" + +#include //exchange + +namespace raii{ + + video_man::video_man(const raii::string_base& filename){ + av_register_all(); + _init(filename); + } + video_man::video_man(video_man&& v): + m_vid(std::exchange(v.m_vid, nullptr)){} + video_man::~video_man(void){ + reset(); + } + + int video_man::height(void)const{ + return m_context->height; + } + int video_man::width(void)const{ + return m_context->width; + } + raii::string video_man::get_mimetype(void)const{ + const char* first = strstr(m_vid->iformat->name, ","); + return raii::string(raii::static_string(m_vid->iformat->name, first - m_vid->iformat->name)); + } + auto video_man::create_jpg_thumbnail(void) -> packet_type{ + context_type jpg_context = _get_jpeg_context(m_context); + frame_type frame(_init_frame(m_context->width, m_context->height, m_context->pix_fmt), _frame_deleter); + frame_type jpg_frame(_init_frame(m_context->width, m_context->height, jpg_context->pix_fmt), _frame_deleter); + packet_type packet(av_packet_alloc(), _packet_deleter); + SwsContext* sws_ctx = sws_getContext(m_context->width, m_context->height, m_context->pix_fmt, + m_context->width, m_context->height, AV_PIX_FMT_YUV420P, + SWS_BILINEAR, NULL, NULL, NULL); + _change_color_range(sws_ctx); + + av_image_alloc(jpg_frame->data, jpg_frame->linesize, m_context->width, m_context->height, jpg_context->pix_fmt, 32); + while(av_read_frame(m_vid, packet.get()) >= 0){ + if(packet->stream_index == m_stream_index){ + avcodec_send_packet(m_context, packet.get()); + if(avcodec_receive_frame(m_context, frame.get()) >= 0) + break; + } + av_packet_unref(packet.get()); + } + sws_scale(sws_ctx, frame->data, frame->linesize, 0, m_context->height, jpg_frame->data, jpg_frame->linesize); + + sws_freeContext(sws_ctx); + + packet_type jpg_packet(av_packet_alloc(), _packet_deleter); + if(avcodec_send_frame(jpg_context.get(), jpg_frame.get()) < 0){ + av_freep(&jpg_frame->data[0]); + return packet_type(nullptr, _packet_deleter); + } + if(avcodec_receive_packet(jpg_context.get(), jpg_packet.get()) < 0){ + return packet_type(nullptr, _packet_deleter); + } + + av_freep(&jpg_frame->data[0]); + return jpg_packet; + } + + AVFormatContext* video_man::release(void){ + avcodec_close(m_context); + avcodec_free_context(&m_context); + m_context = nullptr; + m_codec = nullptr; + m_stream_index = -1; + return std::exchange(m_vid, nullptr); + } + void video_man::reset(const raii::string_base& filename){ + reset(); + _init(filename); + } + void video_man::reset(void){ + avcodec_close(m_context); + avcodec_free_context(&m_context); + avformat_close_input(&m_vid); + m_context = nullptr; + m_vid = nullptr; + m_codec = nullptr; + m_stream_index = -1; + } + video_man::operator bool(void)const{ + return m_vid; + } + void video_man::_init(const raii::string_base& filename){ + m_vid = _open_video_file(filename); + if(!m_vid) + return; + + AVCodecParameters* codec_parms = m_vid->streams[m_stream_index]->codecpar; + m_codec = avcodec_find_decoder(codec_parms->codec_id); + m_context = avcodec_alloc_context3(m_codec); + avcodec_parameters_to_context(m_context, codec_parms); + avcodec_open2(m_context, m_codec, NULL); + } + AVFormatContext* video_man::_open_video_file(const raii::string_base& filename){ + AVFormatContext* context = NULL; + avformat_open_input(&context, filename.get(), NULL, NULL); + avformat_find_stream_info(context, NULL); + for(size_t i = 0;i < context->nb_streams;++i){ + if(context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){ + m_stream_index = i; + break; + } + } + if(m_stream_index != -1) + return context; + avformat_close_input(&context); + return nullptr; + } + AVFrame* video_man::_init_frame(int w, int h, int format){ + AVFrame* f = av_frame_alloc(); + f->data[0] = NULL; + f->width = w; + f->height = h; + f->format = format; + return f; + } + void video_man::_change_color_range(SwsContext* sws_ctx){ + //we want to convert from YUVJ420P to YUV420P for sws_scale to be happy. but we actually want to output as YUVJ420P. + //so we change the color range to use the full range like YUVJ420P. dummy is used to ignore unneeded returns. + int dummy[4]; + int src_range, dest_range; + int br, con, sat; + sws_getColorspaceDetails(sws_ctx, (int**)&dummy, &src_range, (int**)&dummy, &dest_range, &br, &con, &sat); + const int* coefs = sws_getCoefficients(SWS_CS_DEFAULT); + src_range = 2; + sws_setColorspaceDetails(sws_ctx, coefs, src_range, coefs, dest_range, br, con, sat); + } + auto video_man::_get_jpeg_context(const AVCodecContext* decoder) -> context_type{ + AVCodec* jpg_codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG); + context_type jpg_context(avcodec_alloc_context3(jpg_codec), _context_deleter); + jpg_context->pix_fmt = AV_PIX_FMT_YUVJ420P; + jpg_context->width = decoder->width; + jpg_context->height = decoder->height; + jpg_context->codec_id = AV_CODEC_ID_MJPEG; + jpg_context->codec_type = AVMEDIA_TYPE_VIDEO; + if(decoder->time_base.num == 0 || decoder->time_base.den == 0) + jpg_context->time_base = AVRational{1, 30}; + else + jpg_context->time_base = decoder->time_base; + avcodec_open2(jpg_context.get(), jpg_codec, NULL); + return jpg_context; + } + void video_man::_context_deleter(AVCodecContext* ptr){ + avcodec_close(ptr); + avcodec_free_context(&ptr); + } + void video_man::_frame_deleter(AVFrame* ptr){ + av_frame_unref(ptr); + av_frame_free(&ptr); + } + void video_man::_packet_deleter(AVPacket* ptr){ + av_packet_unref(ptr); + av_packet_free(&ptr); + } +} diff --git a/src/reddit.cpp b/src/reddit.cpp index c8bcc9f..367c5b5 100644 --- a/src/reddit.cpp +++ b/src/reddit.cpp @@ -1,3 +1,21 @@ +/** + This file is a part of rexy's matrix bot + Copyright (C) 2019 rexy712 + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + #include "reddit.hpp" #include "raii/rjp_string.hpp" #include "raii/string.hpp" @@ -6,6 +24,13 @@ #include "raii/static_string.hpp" #include //search +#include + +//no idea if this covers everything reddit might dish out at me +//there is no consistency in their content tagging. there are gifs marked as images, others as videos +//they separate audio and video streams for their hosted videos, there is no true way to tell +//what kind of content a post contains since the post_hint field might be completely nonexistent. +//it's just a game of hacking together solutions each time reddit throws me a new type of unexpected complication. namespace reddit{ @@ -39,17 +64,16 @@ namespace reddit{ } - static raii::rjp_string media_search(RJP_value* root){ - RJP_search_res media = rjp_search_member(root, "media", 0); - if(!media.value) + static raii::rjp_string media_search(RJP_value* media){ + if(!media) + return {}; + RJP_search_res res = rjp_search_member(media, "reddit_video", 0); + if(!res.value) return raii::rjp_string{}; - media = rjp_search_member(media.value, "reddit_video", 0); - if(!media.value) + res = rjp_search_member(res.value, "fallback_url", 0); + if(!res.value) return raii::rjp_string{}; - media = rjp_search_member(media.value, "fallback_url", 0); - if(!media.value) - return raii::rjp_string{}; - return raii::rjp_string{media.value}; + return raii::rjp_string(res.value); } static raii::rjp_string preview_search(RJP_value* root){ RJP_search_res media = rjp_search_member(root, "preview", 0); @@ -69,8 +93,10 @@ namespace reddit{ } static raii::rjp_string find_video_url(RJP_value* root){ - if(raii::rjp_string res = media_search(root)) + RJP_search_res media = rjp_search_member(root, "media", 0); + if(raii::rjp_string res = media_search(media.value)){ return res; + } raii::rjp_string res = preview_search(root); return res; } @@ -142,6 +168,13 @@ namespace reddit{ data = rjp_search_member(data.value, "data", 0); if(!data.value) return; + RJP_search_res crosspost = rjp_search_member(data.value, "crosspost_parent_list", 0); + if(crosspost.value){ + crosspost.value = rjp_get_element(crosspost.value); + if(crosspost.value) + data = crosspost; + } + rjp_search_members(data.value, num_searches, search_items, results, 0); m_media_url = results[0].value; m_author = results[1].value; @@ -178,8 +211,41 @@ namespace reddit{ } } //handle hosted video - else if(!strcmp(m_post_hint, "hosted:video") || !strcmp(m_post_hint, "rich:video")){ - raii::rjp_string res = media_search(data.value); + else if(!strcmp(m_post_hint, "hosted:video")){ + RJP_search_res media = rjp_search_member(data.value, "media", 0); + RJP_search_res gif = rjp_search_member(media.value, "reddit_video", 0); + if(gif.value) + gif = rjp_search_member(media.value, "is_gif", 0); + if(gif.value && rjp_value_boolean(gif.value)){ + m_type = post_type::image; + }else{ + raii::rjp_string res = media_search(media.value); + if(!res){ + res = preview_search(data.value); + if(!res){ + m_type = post_type::link; + return; + } + } + m_type = post_type::video; + m_media_url = std::move(res); + } + + //reddit hosts audio and video separately. Meaning I have to find a way to manually recombine them + static constexpr char url_base[] = "https://v.redd.it/"; + static constexpr size_t url_base_len = sizeof(url_base)-1; + char* end = strstr(m_media_url.get()+url_base_len, "/"); + if(!end) + end = m_media_url.get()+m_media_url.length(); + size_t len = end - m_media_url.get(); + m_hosted_video_audio = raii::string(len + 6); + memcpy(m_hosted_video_audio.get(), m_media_url.get(), len); + memcpy(m_hosted_video_audio.get()+len, "/audio", 6); + m_hosted_video_audio[len+6] = 0; + } + else if(!strcmp(m_post_hint, "rich:video")){ + RJP_search_res media = rjp_search_member(data.value, "media", 0); + raii::rjp_string res = media_search(media.value); if(res){ m_type = post_type::video; m_media_url = std::move(res); @@ -230,6 +296,9 @@ namespace reddit{ const raii::rjp_string& post::mediaurl(void)const{ return m_media_url; } + const raii::string& post::hosted_video_audio(void)const{ + return m_hosted_video_audio; + } const raii::rjp_string& post::posturl(void)const{ return m_post_url; } diff --git a/src/test.cpp b/src/test.cpp index 7508c4c..5475b2f 100644 --- a/src/test.cpp +++ b/src/test.cpp @@ -1,3 +1,21 @@ +/** + This file is a part of rexy's matrix bot + Copyright (C) 2019 rexy712 + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + #include #include #include //move @@ -20,18 +38,191 @@ #include "reddit.hpp" #include "matrix.hpp" +extern "C"{ +# include //AVCodecContext, AVCodec +# include //AVFormatContext +# include //sws_scale +# include //av_image_alloc +} + +//a lot copied from a github repo, but with all deprecation warnings fixed. +//no idea how the one guy managed to figure out all this with the minimal and conflicting documentation for ffmpeg and libav +bool mux_audio_video(const raii::string_base& audio_file, const raii::string_base& video_file, const raii::string_base& output_file){ + av_register_all(); + av_log_set_level(AV_LOG_FATAL); + + AVOutputFormat* out_format = NULL; + AVFormatContext* audio_context = NULL, *video_context = NULL, *output_context = NULL; + int video_index_in = -1, audio_index_in = -1; + int video_index_out = -1, audio_index_out = -1; + + + if(avformat_open_input(&audio_context, audio_file.get(), 0, 0) < 0) + return false; + if(avformat_find_stream_info(audio_context, 0) < 0){ + avformat_close_input(&audio_context); + return false; + } + if(avformat_open_input(&video_context, video_file.get(), 0, 0) < 0){ + avformat_close_input(&audio_context); + return false; + } + if(avformat_find_stream_info(video_context, 0) < 0){ + avformat_close_input(&audio_context); + avformat_close_input(&video_context); + return false; + } + + if(avformat_alloc_output_context2(&output_context, av_guess_format("mp4", NULL, NULL), NULL, output_file.get()) < 0){ + avformat_close_input(&audio_context); + avformat_close_input(&video_context); + return false; + } + out_format = output_context->oformat; + + for(size_t i = 0;i < audio_context->nb_streams;++i){ + if(audio_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){ + audio_index_in = i; + + AVStream* in_stream = audio_context->streams[i]; + AVCodec* codec = avcodec_find_encoder(in_stream->codecpar->codec_id); + AVCodecContext* tmp = avcodec_alloc_context3(codec); + avcodec_parameters_to_context(tmp, in_stream->codecpar); + AVStream* out_stream = avformat_new_stream(output_context, codec); + audio_index_out = out_stream->index; + if(output_context->oformat->flags & AVFMT_GLOBALHEADER){ + tmp->flags |= CODEC_FLAG_GLOBAL_HEADER; + } + tmp->codec_tag = 0; + avcodec_parameters_from_context(out_stream->codecpar, tmp); + avcodec_free_context(&tmp); + + break; + } + } + for(size_t i = 0;i < video_context->nb_streams;++i){ + if(video_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){ + video_index_in = i; + + AVStream* in_stream = video_context->streams[i]; + AVCodec* codec = avcodec_find_encoder(in_stream->codecpar->codec_id); + AVCodecContext* tmp = avcodec_alloc_context3(codec); + avcodec_parameters_to_context(tmp, in_stream->codecpar); + AVStream* out_stream = avformat_new_stream(output_context, codec); + video_index_out = out_stream->index; + if(output_context->oformat->flags & AVFMT_GLOBALHEADER){ + tmp->flags |= CODEC_FLAG_GLOBAL_HEADER; + } + tmp->codec_tag = 0; + avcodec_parameters_from_context(out_stream->codecpar, tmp); + avcodec_free_context(&tmp); + + break; + } + } + + if(!(out_format->flags & AVFMT_NOFILE)){ + if(avio_open(&output_context->pb, output_file.get(), AVIO_FLAG_WRITE) < 0){ + avformat_free_context(output_context); + avformat_close_input(&audio_context); + avformat_close_input(&video_context); + return false; + } + } + + if(avformat_write_header(output_context, NULL) < 0){ + if(!(out_format->flags & AVFMT_NOFILE)){ + avio_close(output_context->pb); + } + avformat_free_context(output_context); + avformat_close_input(&audio_context); + avformat_close_input(&video_context); + return false; + } + + int64_t video_pts = 0, audio_pts = 0; + AVPacket packet; + + while(true){ + AVFormatContext* in_context; + int stream_index = 0; + AVStream* in_stream, *out_stream; + + if(av_compare_ts(video_pts, video_context->streams[video_index_in]->time_base, + audio_pts, audio_context->streams[audio_index_in]->time_base) <= 0) + { + //video + in_context = video_context; + stream_index = video_index_out; + + if(av_read_frame(in_context, &packet) >= 0){ + do{ + if(packet.stream_index == video_index_in){ + video_pts = packet.pts; + break; + } + av_packet_unref(&packet); + }while(av_read_frame(in_context, &packet) >= 0); + }else{ + break; + } + }else{ + //audio + in_context = audio_context; + stream_index = audio_index_out; + + if(av_read_frame(in_context, &packet) >= 0){ + do{ + if(packet.stream_index == audio_index_in){ + audio_pts = packet.pts; + break; + } + av_packet_unref(&packet); + }while(av_read_frame(in_context, &packet) >= 0); + }else{ + break; + } + } + in_stream = in_context->streams[packet.stream_index]; + out_stream = output_context->streams[stream_index]; + + packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)); + packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)); + packet.duration = av_rescale_q(packet.duration, in_stream->time_base, out_stream->time_base); + packet.pos = -1; + packet.stream_index = stream_index; + + if(av_interleaved_write_frame(output_context, &packet) < 0){ + break; + } + av_packet_unref(&packet); + + } + + av_write_trailer(output_context); + + if(!(out_format->flags & AVFMT_NOFILE)){ + avio_close(output_context->pb); + } + avformat_free_context(output_context); + avformat_close_input(&audio_context); + avformat_close_input(&video_context); + return true; +} + //Get username/password for reddit account and bot. Plus a useragent string -std::tuple parse_data_file(const raii::rjp_ptr& root){ +std::tuple parse_data_file(const raii::rjp_ptr& root){ RJP_search_res res = rjp_search_member(root.get(), "reddit", 0); reddit::auth_data red_ret = reddit::parse_auth_data(res.value); res = rjp_search_member(root.get(), "matrix", 0); matrix::auth_data mat_ret = matrix::parse_auth_data(res.value); + raii::rjp_string roomid(rjp_search_member(res.value, "roomid", 0).value); res = rjp_search_member(root.get(), "useragent", 0); - return std::tuple(std::move(red_ret), std::move(mat_ret), raii::rjp_string(res.value)); + return std::tuple(std::move(red_ret), std::move(mat_ret), raii::rjp_string(res.value), std::move(roomid)); } //Read in file containing username/password details raii::rjp_ptr read_data_file(const char* file){ @@ -53,9 +244,9 @@ size_t filewrite_response(char* ptr, size_t size, size_t nmemb, void* userdata){ return nmemb*size; } -void file_output_curl(raii::curler& curl, const raii::string_base& url){ +bool file_output_curl(raii::curler& curl, const raii::string_base& filename, const raii::string_base& url){ //Download the post's image - raii::filerd fp("testout", "w"); + raii::filerd fp(filename, "w"); if(!fp) fprintf(stderr, "unable to open file for writing\n"); curl.seturl(url); @@ -63,10 +254,16 @@ void file_output_curl(raii::curler& curl, const raii::string_base& url){ curl.setopt(CURLOPT_NOPROGRESS, 1L); curl.setopt(CURLOPT_FOLLOWLOCATION, 1L); curl.setopt(CURLOPT_MAXREDIRS, 50L); + curl.setopt(CURLOPT_FAILONERROR, 1L); curl.forcessl(CURL_SSLVERSION_TLSv1_2); curl.setopt(CURLOPT_WRITEFUNCTION, filewrite_response); curl.setopt(CURLOPT_WRITEDATA, fp.get()); - curl.perform(); + int ret = curl.perform(); + if(ret != CURLE_OK) + return false; + if(!fp.length()) + return false; + return true; } @@ -86,7 +283,7 @@ int main(){ } //Parse data file - auto [reddit_auth,matrix_auth,useragent] = parse_data_file(root); + auto [reddit_auth,matrix_auth,useragent,roomid] = parse_data_file(root); if(!(reddit_auth && matrix_auth && useragent)){ fprintf(stderr, "Missing data field\n"); return 2; @@ -103,8 +300,8 @@ int main(){ { int retries = 5; do{ - reply = mybot.get_top_post("ProgrammerHumor"_ss, reply.name()); - if(reply.type() != reddit::post_type::text) + reply = mybot.get_top_post("ProgrammerHumor"_ss, reply.name(), reddit::time::hour); + if(reply.type() != reddit::post_type::text && reply.type() != reddit::post_type::link) break; --retries; DEBUG_PRINT("Not an image.\nTODO, search for another\n"); @@ -125,26 +322,44 @@ int main(){ DEBUG_PRINT("mediaurl: %s\n", reply.mediaurl().get()); DEBUG_PRINT("posturl: %s\n", reply.posturl().get()); - raii::curler curl; if(reply.type() == reddit::post_type::image) { DEBUG_PRINT("Got an image\n"); - file_output_curl(curl, reply.mediaurl()); + file_output_curl(curl, "testout"_ss, reply.mediaurl()); auto img_data = matbot.upload_image("testout"_ss, reply.name()); - auto val = matbot.send_image("!QeYfNDCRodtNohhnaI:matrix.org"_ss, img_data); + auto val = matbot.send_image(roomid, img_data); DEBUG_PRINT("image event: %s\n", val.get()); - val = matbot.send_message("!QeYfNDCRodtNohhnaI:matrix.org"_ss, raii::string(reply.title() + "\n" + reply.posturl())); + val = matbot.send_message(roomid, raii::string(reply.title() + "\n" + reply.posturl())); DEBUG_PRINT("text event: %s\n", val.get()); } else if(reply.type() == reddit::post_type::video) { DEBUG_PRINT("Got a video\n"); - file_output_curl(curl, reply.mediaurl()); - auto vid_data = matbot.upload_video("testout"_ss); - auto val = matbot.send_video("!QeYfNDCRodtNohhnaI:matrix.org"_ss, vid_data); + + raii::static_string target = "testout"; + + if(reply.hosted_video_audio()){ + DEBUG_PRINT("fuck reddit\n"); + file_output_curl(curl, "video"_ss, reply.mediaurl()); + if(!file_output_curl(curl, "audio"_ss, reply.hosted_video_audio())){ + target = "video"; + remove("audio"); + }else{ + DEBUG_PRINT("Remuxing audio and video\n"); + mux_audio_video("audio"_ss, "video"_ss, "testout"_ss); + remove("video"); + remove("audio"); + } + }else{ + file_output_curl(curl, target, reply.mediaurl()); + } + + DEBUG_PRINT("Uploading video\n"); + auto vid_data = matbot.upload_video(target); + auto val = matbot.send_video(roomid, vid_data); DEBUG_PRINT("video event: %s\n", val.get()); - val = matbot.send_message("!QeYfNDCRodtNohhnaI:matrix.org"_ss, raii::string(reply.title() + "\n" + reply.posturl())); + val = matbot.send_message(roomid, raii::string(reply.title() + "\n" + reply.posturl())); DEBUG_PRINT("text event: %s\n", val.get()); } else if(reply.type() == reddit::post_type::link)