From 6f078c8495eed4b93ff476760028fc0f216a6f20 Mon Sep 17 00:00:00 2001 From: rexy712 Date: Sat, 2 Mar 2019 12:33:31 -0800 Subject: [PATCH] initial commit to allow me to stash --- .gitignore | 7 + TODO | 11 + include/matrix.hpp | 138 +++++++++++ include/raii/curl_llist.hpp | 49 ++++ include/raii/curl_string.hpp | 41 +++ include/raii/curler.hpp | 55 +++++ include/raii/filerd.hpp | 47 ++++ include/raii/rjp_ptr.hpp | 23 ++ include/raii/rjp_string.hpp | 55 +++++ include/raii/static_string.hpp | 50 ++++ include/raii/string.hpp | 76 ++++++ include/raii/string_base.hpp | 303 +++++++++++++++++++++++ include/reddit.hpp | 137 +++++++++++ makefile | 86 +++++++ matrix_curls.txt | 39 +++ reddit_oauth_curls.txt | 2 + src/matrix.cpp | 434 ++++++++++++++++++++++++++++++++ src/raii/curl_llist.cpp | 41 +++ src/raii/curler.cpp | 89 +++++++ src/raii/filerd.cpp | 78 ++++++ src/raii/string_base.cpp | 140 +++++++++++ src/reddit.cpp | 438 +++++++++++++++++++++++++++++++++ src/test.cpp | 169 +++++++++++++ upload_bot_image.sh | 37 +++ youtube_curl.txt | 2 + 25 files changed, 2547 insertions(+) create mode 100644 .gitignore create mode 100644 TODO create mode 100644 include/matrix.hpp create mode 100644 include/raii/curl_llist.hpp create mode 100644 include/raii/curl_string.hpp create mode 100644 include/raii/curler.hpp create mode 100644 include/raii/filerd.hpp create mode 100644 include/raii/rjp_ptr.hpp create mode 100644 include/raii/rjp_string.hpp create mode 100644 include/raii/static_string.hpp create mode 100644 include/raii/string.hpp create mode 100644 include/raii/string_base.hpp create mode 100644 include/reddit.hpp create mode 100644 makefile create mode 100644 matrix_curls.txt create mode 100644 reddit_oauth_curls.txt create mode 100644 src/matrix.cpp create mode 100644 src/raii/curl_llist.cpp create mode 100644 src/raii/curler.cpp create mode 100644 src/raii/filerd.cpp create mode 100644 src/raii/string_base.cpp create mode 100644 src/reddit.cpp create mode 100644 src/test.cpp create mode 100755 upload_bot_image.sh create mode 100644 youtube_curl.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d10ca4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +obj +tester +*.log +data +*.swp +*.save +testout diff --git a/TODO b/TODO new file mode 100644 index 0000000..b80e2c8 --- /dev/null +++ b/TODO @@ -0,0 +1,11 @@ +file info struct as return from upload_file so as to know some metadata about the file + uploaded url + file type if able to be figured out + file size + image + dimensions + thumbnail + + +use libmagic to determine file types +create thumbnail images somehow diff --git a/include/matrix.hpp b/include/matrix.hpp new file mode 100644 index 0000000..2f39bbf --- /dev/null +++ b/include/matrix.hpp @@ -0,0 +1,138 @@ +#ifndef MATRIX_HPP +#define MATRIX_HPP + +#include "raii/curler.hpp" +#include "raii/string.hpp" +#include "raii/rjp_string.hpp" +#include "raii/rjp_ptr.hpp" +#include "raii/filerd.hpp" + +#include + +namespace matrix{ + struct auth_data{ + raii::rjp_string bot_name; + raii::rjp_string bot_pass; + raii::rjp_string homeserver; + raii::rjp_string bot_alias; + raii::rjp_string access_token; + + operator bool(void)const{ + return (bot_name && bot_pass && homeserver); + } + }; + + struct file_info{ + raii::rjp_string fileurl; + raii::string filename; + raii::string filetype; + size_t filesize; + }; + struct image_info : file_info{ + size_t width; + size_t height; + + raii::rjp_string thumburl; + size_t thumb_width; + size_t thumb_height; + size_t thumbsize; + }; + + class bot + { + private: + struct mat_url_list{ + mat_url_list(void) = default; + mat_url_list(const raii::string_base& homeserver, const raii::string_base& access_token); + mat_url_list(const mat_url_list&) = default; + mat_url_list(mat_url_list&&) = default; + mat_url_list& operator=(const mat_url_list&) = default; + mat_url_list& operator=(mat_url_list&&) = default; + + void repopulate_accesstoken(const raii::string_base& homeserver, const raii::string_base& access_token); + void repopulate(const raii::string_base& homeserver, const raii::string_base& access_token); + void invalidate_accesstoken(void); + + raii::string create_room; + raii::string file_upload; + raii::string room_list; + raii::string login; + raii::string alias_lookup; + raii::string whoami; + + private: + static constexpr const char* s_proto = "https://"; + }; + private: + raii::curler m_curl; //https access + raii::string m_useragent; //useragent to identify our application + raii::string m_homeserver; //name of our homeserver + raii::rjp_string m_access_token; //authentication + raii::rjp_string m_userid; //userid including homeserver + mat_url_list m_urls; + + raii::rjp_string m_next_batch; //string which tracks where we are in the server history + size_t m_sync_timeout; //max time to wait during sync in milliseconds + + public: + bot(const auth_data& a, const raii::string_base& useragent); + bot(const auth_data& a, raii::string&& useragent); + bot(const bot& b) = default; + bot(bot&& b) = default; + ~bot(void) = default; + + bot& operator=(const bot&) = default; + bot& operator=(bot&&) = default; + + //local getter + const raii::rjp_string& access_token(void)const; + const raii::rjp_string& userid(void)const; + const raii::string& useragent(void)const; + + //local setter + void set_useragent(const raii::string_base&); + void set_useragent(raii::string&&); + + //networked setter + void set_display_name(const raii::string_base&); + void set_profile_picture(const raii::string_base&); + + + //networked getter + raii::rjp_string room_alias_to_id(const raii::string_base& alias); + std::vector list_rooms(void); + + //other networked operations + raii::string create_room(const raii::string_base& name, const raii::string_base& alias); + + raii::rjp_string upload_file(const raii::string_base& filename, const raii::curl_llist& header); + image_info upload_image(const raii::string_base& filename, const raii::string_base& alias); + image_info upload_image(const raii::string_base& filename); + raii::rjp_string upload_video(const raii::string_base& filename); + + raii::rjp_string send_image(const raii::string_base& room, const image_info& image); + raii::rjp_string send_video(const raii::string_base& room, const raii::string_base& file_url, const raii::string_base& filetype, const raii::string_base& filename); + raii::rjp_string send_message(const raii::string_base& room, const raii::string_base& text); + + bool send_file(const raii::string_base& room, const raii::string_base& file_url); + void sync(void); + void logout(void); + + protected: + raii::rjp_string _upload_file(raii::filerd& fp, const raii::curl_llist& header); + 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); + raii::rjp_string _post_and_find(const raii::string_base& data, const raii::string_base& url, const raii::curl_llist& header, const raii::string_base& target); + raii::rjp_string _get_and_find(const raii::string_base& url, const raii::string_base& search); + raii::rjp_string _curl_reply_search(const raii::string_base& reply, const raii::string_base& search); + void _set_curl_defaults(void); + raii::string _request_access_token(const auth_data& a); + void _acquire_access_token(const auth_data& a); + }; + + + auth_data parse_auth_data(RJP_value* root); +} + +#endif diff --git a/include/raii/curl_llist.hpp b/include/raii/curl_llist.hpp new file mode 100644 index 0000000..5f941dc --- /dev/null +++ b/include/raii/curl_llist.hpp @@ -0,0 +1,49 @@ +#ifndef RAII_CURL_LLIST_HPP +#define RAII_CURL_LLIST_HPP + +#include +#include //forward + +namespace raii{ + //RAII wrapper for curl's slist (singly linked list?) + class curl_llist + { + private: + curl_slist* m_data = nullptr; + + public: + curl_llist(void) = default; + template + curl_llist(Args&&... args){ + assign(std::forward(args)...); + } + curl_llist(const curl_llist&) = delete; + curl_llist(curl_llist&& l)noexcept; + ~curl_llist(void); + + curl_llist& operator=(const curl_llist&) = delete; + curl_llist& operator=(curl_llist&& l)noexcept; + + curl_llist& operator+=(const char* data); + + operator curl_slist*(void); + operator const curl_slist*(void)const; + curl_slist* get(void); + const curl_slist* get(void)const; + + void reset(curl_slist* nd = nullptr); + + private: + template + void assign(T&& t, Args&&... args){ + (*this) += std::forward(t); + if constexpr(sizeof...(args) > 0){ + assign(std::forward(args)...); + } + } + + }; + +} + +#endif diff --git a/include/raii/curl_string.hpp b/include/raii/curl_string.hpp new file mode 100644 index 0000000..ae48697 --- /dev/null +++ b/include/raii/curl_string.hpp @@ -0,0 +1,41 @@ +#ifndef RAII_CURL_STRING_HPP +#define RAII_CURL_STRING_HPP + +#include "raii/string_base.hpp" + +#include +#include //size_t + + +namespace raii{ + + namespace detail{ + + class curl_allocator + { + public: + static void* allocate(size_t){return nullptr;} + static void* copy(const void*, size_t){return nullptr;} + static void free(void* data){ + curl_free(data); + } + }; + + } + + //curl allocated string + struct curl_string : public string_intermediary + { + curl_string(const curl_string&) = default; + curl_string(curl_string&&) = default; + curl_string& operator=(const curl_string&) = default; + curl_string& operator=(curl_string&&) = default; + + using string_intermediary::string_intermediary; + using string_intermediary::operator=; + }; + + +} + +#endif diff --git a/include/raii/curler.hpp b/include/raii/curler.hpp new file mode 100644 index 0000000..8923b39 --- /dev/null +++ b/include/raii/curler.hpp @@ -0,0 +1,55 @@ +#ifndef RAII_CURLER_HPP +#define RAII_CURLER_HPP + +#include + +#include "raii/curl_llist.hpp" +#include "raii/curl_string.hpp" + +namespace raii{ + + //RAII wrapper for CURL* with some convenience functions added + class curler + { + private: + CURL* m_curl; + + public: + curler(void); + curler(const curler& c); + curler(curler&& c)noexcept; + ~curler(void); + + template + curler& setopt(CURLoption option, T&& t){ + curl_easy_setopt(m_curl, option, t); + return *this; + } + + curler& postreq(void); + curler& getreq(void); + curler& setheader(const curl_llist& h); + curler& seturl(const char* s); + curler& seturl(const string_base& s); + curler& setuseragent(const char* s); + curler& setuseragent(const string_base& s); + curler& setuserpwd(const char* s); + curler& setuserpwd(const string_base& s); + curler& setpostdata(const char* s, curl_off_t len = -1); + curler& setpostdata(const string_base& s); + curler& forcessl(long version = CURL_SSLVERSION_DEFAULT); + void reset(void); + decltype(curl_easy_perform(m_curl)) perform(void); + + curl_string encode(const char* data, int len = 0); + curl_string decode(const char* data, int* outlen = nullptr, int len = 0); + + CURL* get(void); + const CURL* get(void)const; + operator CURL*(void); + operator const CURL*(void)const; + }; + +} + +#endif diff --git a/include/raii/filerd.hpp b/include/raii/filerd.hpp new file mode 100644 index 0000000..92606f4 --- /dev/null +++ b/include/raii/filerd.hpp @@ -0,0 +1,47 @@ +#ifndef RAII_FILERD_HPP +#define RAII_FILERD_HPP + +#include //FILE +#include //size_t + +#include "raii/string.hpp" + +namespace raii{ + + //RAII wrapper for FILE* + class filerd + { + private: + FILE* m_fp = nullptr; + + public: + filerd(void) = default; + filerd(const char* f, const char* mode = "r"); + filerd(const filerd&) = delete; + filerd(filerd&& f); + ~filerd(void); + filerd& operator=(const filerd&) = delete; + filerd& operator=(filerd&& f); + + void reset(FILE* fp = nullptr); + FILE* release(void); + size_t length(void); + size_t position(void)const; + void rewind(size_t pos = 0); + + operator FILE*(void); + operator const FILE*(void)const; + FILE* get(void); + const FILE* get(void)const; + operator bool(void)const; + + size_t read(char* dest, size_t bytes); + raii::string read(size_t bytes); + + size_t write(const char* c, size_t bytes); + size_t write(const raii::string_base&); + }; + +} + +#endif diff --git a/include/raii/rjp_ptr.hpp b/include/raii/rjp_ptr.hpp new file mode 100644 index 0000000..60cc023 --- /dev/null +++ b/include/raii/rjp_ptr.hpp @@ -0,0 +1,23 @@ +#ifndef RAII_RJP_PTR_HPP +#define RAII_RJP_PTR_HPP + +#include +#include //unique_ptr + +namespace raii{ + + namespace detail{ + struct rjp_tree_deleter + { + template + void operator()(T* ptr){ + rjp_free_value(ptr); + } + }; + } + + using rjp_ptr = std::unique_ptr; + +} + +#endif diff --git a/include/raii/rjp_string.hpp b/include/raii/rjp_string.hpp new file mode 100644 index 0000000..3d6cc3c --- /dev/null +++ b/include/raii/rjp_string.hpp @@ -0,0 +1,55 @@ +#ifndef RAII_RJP_STRING_HPP +#define RAII_RJP_STRING_HPP + +#include +#include "raii/string_base.hpp" +#include //exchange +#include //memcpy + +namespace raii{ + + namespace detail{ + + class rjp_allocator + { + public: + static void free(void* data){ + rjp_free(data); + } + static void* allocate(size_t size){ + return rjp_alloc(size); + } + static void* copy(const void* data, size_t len){ + void* tmp = allocate(len); + memcpy(tmp, data, len); + return tmp; + } + }; + + } + + //rjp allocated string + struct rjp_string : public string_intermediary + { + rjp_string(const rjp_string&) = default; + rjp_string(rjp_string&&) = default; + rjp_string& operator=(const rjp_string&) = default; + rjp_string& operator=(rjp_string&&) = default; + + using string_intermediary::string_intermediary; + rjp_string(RJP_value* r): + string_intermediary(r ? std::exchange(r->string.value, nullptr) : nullptr, r ? r->string.length : 0){} + using string_intermediary::operator=; + rjp_string& operator=(RJP_value* r){ + if(!r) + return *this; + reset(); + m_data = std::exchange(r->string.value, nullptr); + m_length = r->string.length; + return *this; + } + }; + +} + +#endif diff --git a/include/raii/static_string.hpp b/include/raii/static_string.hpp new file mode 100644 index 0000000..598e97e --- /dev/null +++ b/include/raii/static_string.hpp @@ -0,0 +1,50 @@ +#ifndef RAII_STATIC_STRING_HPP +#define RAII_STATIC_STRING_HPP + +#include "raii/string_base.hpp" + +namespace raii{ + + class static_string : public string_base + { + public: + constexpr static_string(void) = default; + template + constexpr static_string(const char(&str)[N]): + string_base(const_cast(str), N){} + constexpr static_string(const char* str, size_t len): + string_base(const_cast(str), len){} + static_string(const char* c): + string_base(const_cast(c), strlen(c)){} + constexpr static_string(const static_string& s): + string_base(s.m_data, s.m_length){} + constexpr static_string(static_string&& s): + string_base(s.m_data, s.m_length){} + ~static_string(void) = default; + + static_string& operator=(const char* c){ + m_data = const_cast(c); + m_length = strlen(c); + return *this; + } + static_string& operator=(const static_string& s){ + m_data = s.m_data; + m_length = s.m_length; + return *this; + } + static_string& operator=(static_string&&) = delete; + + private: + char* _allocate(size_t)const override final{return nullptr;} + void _free(char*)const override final{} + char* _copy(const char*,size_t)const override final{return nullptr;} + }; +} + +namespace{ + inline raii::static_string operator"" _ss(const char* str, size_t len){ + return raii::static_string(str, len); + } +} + +#endif diff --git a/include/raii/string.hpp b/include/raii/string.hpp new file mode 100644 index 0000000..fb2047f --- /dev/null +++ b/include/raii/string.hpp @@ -0,0 +1,76 @@ +#ifndef RAII_STRING_HPP +#define RAII_STRING_HPP + +#include "raii/string_base.hpp" + +#include //operator new/delete +#include //memcpy + +namespace raii{ + + namespace detail{ + + class default_allocator + { + public: + static void free(void* data){ + ::operator delete(data); + } + static void* allocate(size_t size){ + return ::operator new(size); + } + static void* copy(const void* c, size_t size){ + void* tmp = allocate(size); + memcpy(tmp, c, size); + return tmp; + } + }; + + } + + //new allocated string + struct string : public string_intermediary + { + string(const string&) = default; + string(string&&) = default; + string& operator=(const string&) = default; + string& operator=(string&&) = default; + + using string_intermediary::string_intermediary; + using string_intermediary::operator=; + }; + + namespace detail{ + size_t _calc_escaped_len(const char* str); + char _escape_to_letter(char escape); + size_t _sanitize_json_copy(char* dest, const char* in); + + template + void _js_assign(char* dest, Tup&& t, size_t offset){ + size_t written = _sanitize_json_copy(dest+offset, std::get(t)); + if constexpr(I+2 < std::tuple_size>::value){ + _js_assign(dest, std::forward(t), offset+written); + } + } + template + size_t _calc_escaped_len_all(T&& t){ + size_t len = _calc_escaped_len(std::get(t)); + if constexpr(I+2 < std::tuple_size>::value){ + len += _calc_escaped_len_all(std::forward(t)); + } + return len; + } + } + template::value && !detail::is_concrete_string::value,void>::type* = nullptr> + string json_escape(T&& t){ + auto tup = t.get(); + size_t len = detail::_calc_escaped_len_all(tup); + char* tmp = reinterpret_cast(string::allocator_type::allocate(len+1)); + detail::_js_assign(tmp, tup, 0); + tmp[len] = 0; + return string(tmp, len); + } + string json_escape(const string_base& str); +} + +#endif diff --git a/include/raii/string_base.hpp b/include/raii/string_base.hpp new file mode 100644 index 0000000..32358b3 --- /dev/null +++ b/include/raii/string_base.hpp @@ -0,0 +1,303 @@ +#ifndef RAII_STRING_BASE_HPP +#define RAII_STRING_BASE_HPP + +#include //size_t +#include //strlen, strcpy +#include //memcpy +#include +#include +#include + +namespace raii{ + + class string_expr{}; + class string_base; + + namespace detail{ + std::true_type is_string_helper(string_expr); + std::false_type is_string_helper(...); + template + struct is_string{ + static constexpr bool value = std::is_same()))>::value; + }; + std::true_type is_string_base(string_base*); + std::false_type is_string_base(...); + template + struct is_concrete_string{ + static constexpr bool value = std::is_same::type*>()))>::value; + }; + template + std::true_type is_tuple_helper(std::tuple); + std::false_type is_tuple_helper(...); + template + struct is_tuple{ + static constexpr bool value = std::is_same()))>::value; + }; + } + + //Base of all RAII strings. Its use is allowing passing of raii strings to functions without knowing the exact type + class string_base : public string_expr + { + protected: + size_t m_length = 0; + char* m_data = nullptr; + + protected: + constexpr string_base(void) = default; + //Initialize without copying + constexpr string_base(char* data, size_t len): + m_length(len), m_data(data){} + //Allocate without assigning + string_base(size_t len); + //Copy ctor (do nothing) + string_base(const string_base&){} + + public: + virtual ~string_base(void) = default; + + public: + //Copy from c string + string_base& operator=(const char* c); + //Copy from other string_base + string_base& operator=(const string_base& s); + //Move from other string base + template::value && !detail::is_concrete_string::value,void>::type* = nullptr> + string_base& operator=(T&& t){ + size_t len = t.length(); + char* tmp; + if(len > m_length){ + tmp = _allocate(len+1); + _assign(tmp, t.get(), 0); + _free(m_data); + }else{ + tmp = m_data; + _assign(tmp, t.get(), 0); + } + m_data = tmp; + m_data[len] = 0; + m_length = len; + return *this; + } + //Replace managed pointer. Frees existing value + void reset(char* val = nullptr); + //Stop managing stored pointer. Does not free. + char* release(void); + + //Length of string not including null terminator + size_t length(void)const; + //direct access to managed pointer + char* get(void); + const char* get(void)const; + operator char*(void); + operator const char*(void)const; + //true if m_data is not null + operator bool(void)const; + + char& operator[](size_t i); + const char& operator[](size_t i)const; + + + protected: + template + static void _assign(char* dest, Tup&& t, size_t offset){ + memcpy(dest+offset, std::get(t), std::get(t)); + if constexpr(I+2 < std::tuple_size::value){ + _assign(dest, std::forward(t), offset+std::get(t)); + } + } + private: + virtual char* _allocate(size_t)const = 0; + virtual void _free(char*)const = 0; + virtual char* _copy(const char*, size_t)const = 0; + }; + + //Supplies all functions that string_base can't implement + template + class string_intermediary : public string_base + { + public: + using allocator_type = Allocator; + + public: + string_intermediary(void) = default; + string_intermediary(char* data, size_t len): + string_base(data, len){} + string_intermediary(const char* data): + string_base(strlen(data)) + { + m_data = reinterpret_cast(Allocator::copy(data, m_length+1)); + } + string_intermediary(size_t len): + string_base(reinterpret_cast(Allocator::allocate(len+1)), len){} + + //normal copy and move ctors + string_intermediary(const string_intermediary& b): + string_base(reinterpret_cast(Allocator::copy(b.m_data, b.m_length+1)), b.m_length){} + string_intermediary(string_intermediary&& s): + string_base(std::exchange(s.m_data, nullptr), s.m_length){} + + string_intermediary(const string_base& b): + string_base(reinterpret_cast(Allocator::copy(b.get(), b.length()+1)), b.length()){} + + //copy from string expression + template::value && !detail::is_concrete_string::value,void>::type* = nullptr> + string_intermediary(T&& t){ + size_t len = t.length(); + + char* tmp = reinterpret_cast(Allocator::allocate(len+1)); + _assign(tmp, t.get(), 0); + m_data = tmp; + m_data[len] = 0; + m_length = len; + } + + //dtor + ~string_intermediary(void){ + Allocator::free(m_data); + } + + string_intermediary& operator=(const string_intermediary&) = default; + string_intermediary& operator=(string_intermediary&& s){ + std::swap(m_data, s.m_data); + m_length = s.m_length; + return *this; + } + using string_base::operator=; + + + string_intermediary operator+(const string_base& s)const{ + string_intermediary tmp(reinterpret_cast(Allocator::allocate(m_length + s.length() + 1)), m_length+s.length()); + memcpy(tmp.get(), m_data, m_length); + strcpy(tmp.get()+m_length, s.get()); + return tmp; + } + + string_intermediary operator+(const char* c)const{ + size_t len = strlen(c); + string_intermediary tmp(reinterpret_cast(Allocator::allocate(m_length + len + 1)), m_length+len); + memcpy(tmp.get(), m_data, m_length); + strcpy(tmp.get()+m_length, c); + return tmp; + } + + private: + char* _allocate(size_t len)const override final{ + return reinterpret_cast(Allocator::allocate(len)); + } + void _free(char* ptr)const override final{ + Allocator::free(ptr); + } + char* _copy(const char* ptr, size_t len)const override final{ + return reinterpret_cast(Allocator::copy(ptr, len)); + } + }; + + //check for member function 'length' + namespace detail{ + template + struct has_len{ + template + struct check; + + template + static std::true_type test(check*); + template + static std::false_type test(...); + + static constexpr bool value = std::is_same(0))>::value; + }; + + } + + //Like an expression template but not really + template + class string_cat_expr : public string_expr + { + private: + Left m_l; + Right m_r; + + public: + template + string_cat_expr(T&& l, U&& r): + m_l(std::forward(l)), + m_r(std::forward(r)){} + string_cat_expr(const string_cat_expr& s): + m_l(s.m_l), + m_r(s.m_r){} + string_cat_expr(string_cat_expr&& s): + m_l(s.m_l), + m_r(s.m_r){} + + size_t length(void)const{ + return _llen() + _rlen(); + } + auto get(void){ + return std::tuple_cat(_lget(), _rget()); + } + private: + auto _lget(void){ + if constexpr(detail::is_string::value){ + if constexpr(detail::is_tuple::value){ + //string_cat_expr + return m_l.get(); + }else{ + //string_base + return std::make_tuple(m_l.get(), m_l.length()); + } + }else{ + //c string + return std::make_tuple(m_l, strlen(m_l)); + } + } + auto _rget(void){ + if constexpr(detail::is_string::value){ + if constexpr(detail::is_tuple::value){ + return m_r.get(); + }else{ + return std::make_tuple(m_r.get(), m_r.length()); + } + }else{ + return std::make_tuple(m_r, strlen(m_r)); + } + } + size_t _llen(void)const{ + if constexpr(detail::has_len::type>::value){ + return m_l.length(); + }else{ + return strlen(m_l); + } + } + size_t _rlen(void)const{ + if constexpr(detail::has_len::type>::value){ + return m_r.length(); + }else{ + return strlen(m_r); + } + } + }; + +} + +template::value,void>::type* = nullptr> +auto operator+(const char* left, Right&& right){ + return raii::string_cat_expr(right))>(left, std::forward(right)); +} +template::value,void>::type* = nullptr> +auto operator+(Left&& left, const char* right){ + return raii::string_cat_expr(left)),const char*>(std::forward(left), right); +} +template::value&&raii::detail::is_string::value,void>::type* = nullptr> +auto operator+(Left&& l, Right&& r){ + return raii::string_cat_expr(l)),decltype(std::forward(r))>(std::forward(l), std::forward(r)); +} +template::value&&raii::detail::is_string::value,void>::type* = nullptr> +decltype(auto) operator+=(Left& l, Right&& r){ + return l = (l + std::forward(r)); +} +template::value,void>::type* = nullptr> +decltype(auto) operator+=(Left& l, const char* r){ + return l = (l + r); +} + +#endif diff --git a/include/reddit.hpp b/include/reddit.hpp new file mode 100644 index 0000000..e98e7af --- /dev/null +++ b/include/reddit.hpp @@ -0,0 +1,137 @@ +#include "raii/rjp_string.hpp" +#include "raii/curler.hpp" +#include "raii/string_base.hpp" +#include "raii/string.hpp" +#include "raii/curler.hpp" + +namespace reddit{ + struct auth_data{ + raii::rjp_string bot_name; + raii::rjp_string bot_pass; + raii::rjp_string acc_name; + raii::rjp_string acc_pass; + + operator bool(void)const{ + return (bot_name && bot_pass && acc_name && acc_pass); + } + }; + + + namespace time{ + namespace detail{ + class time_period{ + protected: + const char* data; + public: + constexpr time_period(const char* d): + data(d){} + constexpr const char* get(void)const{ + return data; + } + }; + } + extern detail::time_period hour; + extern detail::time_period day; + extern detail::time_period week; + extern detail::time_period month; + extern detail::time_period year; + extern detail::time_period all; + } + + enum class post_type{ + image, link, text, video, unrecognized + }; + + class post + { + private: + raii::string m_post; + raii::rjp_string m_media_url; + raii::rjp_string m_author; + raii::rjp_string m_post_hint; + raii::rjp_string m_title; + raii::rjp_string m_name; + raii::rjp_string m_post_url; + post_type m_type = post_type::unrecognized; + + public: + post(void) = default; + post(const raii::string_base& p); + post(raii::string_base&& p); + post(const post& p) = default; + post(post&& p) = default; + ~post(void) = default; + + post& operator=(const raii::string_base& p); + post& operator=(const post& p) = default; + post& operator=(post&& p) = default; + + operator bool(void)const; + + const raii::string& raw(void)const; + const raii::rjp_string& mediaurl(void)const; + const raii::rjp_string& posturl(void)const; + const raii::rjp_string& author(void)const; + const raii::rjp_string& post_hint(void)const; + const raii::rjp_string& title(void)const; + const raii::rjp_string& name(void)const; + post_type type(void)const; + private: + void _parse_post(void); + }; + + class bot + { + private: + raii::curler m_curl; + raii::string m_useragent; + raii::rjp_string m_access_token; + + public: + bot(const auth_data& a, const raii::string_base& useragent); + bot(const auth_data& a, raii::string_base&& useragent); + bot(const bot& b); + bot(bot&& b); + ~bot(void) = default; + + bot& operator=(const bot& b); + bot& operator=(bot&& b); + + const raii::rjp_string& access_token(void)const; + const raii::string& useragent(void)const; + void set_useragent(const raii::string_base&); + void set_useragent(raii::string_base&&); + + + post get_new_post(const raii::string_base& subreddit); + post get_new_post(const raii::string_base& subreddit, const raii::string_base& after); + post get_hot_post(const raii::string_base& subreddit); + post get_hot_post(const raii::string_base& subreddit, const raii::string_base& after); + post get_rising_post(const raii::string_base& subreddit); + post get_rising_post(const raii::string_base& subreddit, const raii::string_base& after); + post get_best_post(const raii::string_base& subreddit); + post get_best_post(const raii::string_base& subreddit, const raii::string_base& after); + post get_top_post(const raii::string_base& subreddit, time::detail::time_period period = time::day); + post get_top_post(const raii::string_base& subreddit, const raii::string_base& after, time::detail::time_period period = time::day); + post get_controversial_post(const raii::string_base& subreddit, time::detail::time_period period = time::day); + post get_controversial_post(const raii::string_base& subreddit, const raii::string_base& after, time::detail::time_period period = time::day); + + protected: + static size_t _get_response_curl_callback(char* ptr, size_t size, size_t nmemb, void* userdata); + static raii::curl_llist _create_auth_header(const raii::string_base& access_token); + post _get_post(const raii::string_base& subreddit, const raii::string_base& category, const raii::string_base& extradata); + void _setup_subreddit_get_curl(const raii::curl_llist& header, const raii::string_base& url, const raii::string_base& reply); + + static size_t _post_reply_curl_callback(char* ptr, size_t size, size_t nmemb, void* userdata); + static raii::string _create_request_post_data(const raii::string_base& acc_name, const raii::string_base& acc_pass); + static raii::string _create_request_userpwd(const raii::string_base& bot_name, const raii::string_base& bot_pass); + void _setup_token_request_curl(const raii::string_base& userpwd, const raii::string_base& postdata, void* result); + + raii::string _request_access_token(const auth_data& a); + raii::rjp_string _acquire_access_token(const auth_data& a); + }; + + + auth_data parse_auth_data(RJP_value* root); + +} diff --git a/makefile b/makefile new file mode 100644 index 0000000..4dfd987 --- /dev/null +++ b/makefile @@ -0,0 +1,86 @@ +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU 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 General Public License for more details. + +#You should have received a copy of the GNU General Public License +#along with this program. If not, see . + +#Copyright 2018-2019 rexy712 + + +SOURCE_DIRS:=src src/raii +OBJDIR:=obj +DEPDIR:=$(OBJDIR)/dep +INCLUDE_DIRS:=include +EXT:=cpp +MAIN_EXECUTABLE:=tester + +CXX:=g++ +CXXFLAGS:=-g -std=c++17 -Wall -pedantic -Wextra +all: CXXFLAGS+=-O0 +release: CXXFLAGS+=-O2 +LDFLAGS= +LDLIBS:=-lcurl -lrjp -lavformat -lavcodec -lavutil -lswresample -lswscale -lfreeimageplus +STRIP:=strip + +memchk:LDFLAGS+=-fsanitize=address -fno-omit-frame-pointer -fno-optimize-sibling-calls +memchk:CXXFLAGS+=-O0 -fsanitize=address -fno-omit-frame-pointer -fno-optimize-sibling-calls + +ifeq ($(OS),Windows_NT) + mkdir=mkdir $(subst /,\,$(1)) > NUL 2>&1 + rm=del /F $(1) > NUL 2>&1 + rmdir=rd /s /q $(1) > NUL 2>&1 + move=move /y $(subst /,\,$(1)) $(subst /,\,$(2)) > NUL 2>&1 + MAIN_EXECUTABLE:=$(MAIN_EXECUTABLE).exe + LDLIBS:=-lglfw3 -lSOIL -lgl3w -lm -lopengl32 -lglu32 -lgdi32 -lkernel32 +else + mkdir=mkdir -p $(1) + rm=rm -f $(1) + rmdir=rm -rf $(1) + move=mv $(1) $(2) +endif + +INTERNAL_CXXFLAGS=-c $(foreach dir,$(INCLUDE_DIRS),-I"$(dir)") -MMD -MP -MF"$(DEPDIR)/$(notdir $(patsubst %.o,%.d,$@))" +SOURCES:=$(foreach source,$(SOURCE_DIRS),$(foreach ext,$(EXT),$(wildcard $(source)/*.$(ext)))) +OBJECTS:=$(addprefix $(OBJDIR)/,$(subst \,.,$(subst /,.,$(addsuffix .o,$(SOURCES))))) + +all: $(MAIN_EXECUTABLE) +memchk: $(MAIN_EXECUTABLE) + +$(MAIN_EXECUTABLE): $(OBJECTS) + $(CXX) $(LDFLAGS) $^ -o "$(basename $@)" $(LDLIBS) + +.PHONY: release +release: $(OBJECTS) + $(CXX) $(LDFLAGS) $^ -o "$(basename $(MAIN_EXECUTABLE))" $(LDLIBS) + $(STRIP) --strip-all "$(MAIN_EXECUTABLE)" + +define GENERATE_OBJECTS + +$$(OBJDIR)/$(subst \,.,$(subst /,.,$(1))).%.o: $(1)/% + $$(CXX) $$(CXXFLAGS) $$(INTERNAL_CXXFLAGS) "$$<" -o "$$@" + +endef + +$(foreach dir,$(SOURCE_DIRS),$(eval $(call GENERATE_OBJECTS,$(dir)))) +$(OBJECTS): | $(OBJDIR) $(DEPDIR) + +$(OBJDIR): + $(call mkdir,"$@") +$(DEPDIR): + $(call mkdir,"$@") + +.PHONY: clean +clean: + $(call rmdir,"$(DEPDIR)") + $(call rmdir,"$(OBJDIR)") + $(call rm,"$(MAIN_EXECUTABLE)") + +-include $(wildcard $(DEPDIR)/*.d) + diff --git a/matrix_curls.txt b/matrix_curls.txt new file mode 100644 index 0000000..1b1e910 --- /dev/null +++ b/matrix_curls.txt @@ -0,0 +1,39 @@ +#register user +curl -X POST -d '{"username":"", "password":"", "auth": {"type":"m.login.dummy"}}' "https:///_matrix/client/r0/register" + +#set display name +curl -X PUT -d '{"displayname": ""}' "https:///_matrix/client/r0/profile/@:/displayname?access_token=<>" + +#set profile picture +curl -X PUT -d '{"avatar_url": "mxc:///"}' "https:///_matrix/client/unstable/profile/@:/avatar_url?access_token=<>" + +#get server login methods +curl -X GET "https:///_matrix/client/r0/login" + +#get access token +curl -X POST -d '{"type":"m.login.password", "user":"", "password":""}' "https:///_matrix/client/r0/login" + +#list joined rooms +curl -X GET "https:///_matrix/client/r0/joined_rooms?access_token=<>" + +#create room +curl -X POST -d '{"name": "", "room_alias_name":""}' "https:///_matrix/client/r0/createRoom?access_token=<>" + +#get room events +curl -X GET "https:///_matrix/client/r0/rooms/"'!'":/state?access_token=<>" + +#upload file (image) +curl -X POST -H "Content-Type: image/" --data-binary @ "https:///_matrix/media/r0/upload?access_token=<>&filename=" + +#send image to room +#there is no way to caption an image at the moment, so you have to either send a separate message +#or set the filename as the caption +curl -X POST -d '{"body": "","info": {"mimetype": "image/"},"msgtype": "m.image","url": "mxc:///"}' "https:///_matrix/client/r0/rooms/"'!'":/send/m.room.message?access_token=<>" + +#send text to room +curl -X POST -d '{"body": "text", "msgtype": "m.text"}' "https:///_matrix/client/r0/rooms"'!'":/send/m.room.message?access_token=<>" + +#use the sync api to get message updates + +#get room name if it exists +curl -XGET 'https:///_matrix/client/r0/rooms//state/m.room.name?access_token=<>' diff --git a/reddit_oauth_curls.txt b/reddit_oauth_curls.txt new file mode 100644 index 0000000..519fa96 --- /dev/null +++ b/reddit_oauth_curls.txt @@ -0,0 +1,2 @@ +curl -X POST -A "UserAgent: puller_bot" -d 'grant_type=password&username=rexy712&password=Mynameisrexy712' --user '1Bs38m2_jL9bow:dCQLZVFypHQcqqxBPAAe_kw609A' https://www.reddit.com/api/v1/access_token +curl -H "Authorization: bearer 81077812340-8u0foxY-a6mQ9B_W5wrhA1ZDZRQ" -A "UserAgent: puller_bot" "https://oauth.reddit.com/r/ProgrammerHumor/new?limit=1" | python -mjson.tool diff --git a/src/matrix.cpp b/src/matrix.cpp new file mode 100644 index 0000000..36af6f4 --- /dev/null +++ b/src/matrix.cpp @@ -0,0 +1,434 @@ +#include "matrix.hpp" + +#include "raii/curl_llist.hpp" +#include "raii/static_string.hpp" +#include "raii/rjp_ptr.hpp" +#include "raii/filerd.hpp" +#include + +extern "C"{ +#include +#include +#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()); + } + raii::rjp_string bot::upload_file(const raii::string_base& filename, const raii::curl_llist& header){ + raii::filerd fd(filename); + if(!fd) return {}; + return _upload_file(fd, header); + } + 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 {}; + + 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(fopen("anewNewimage", "w+")); + +//TODO remove temporary file + image.makeThumbnail(500); + FreeImageIO fileout; + fileout.read_proc = [](void* ptr, unsigned int size, unsigned int nmemb, void* fp) -> unsigned int{ + return fread(ptr, size, nmemb, (FILE*)fp); + }; + fileout.write_proc = [](void* ptr, unsigned int size, unsigned int nmemb, void* fp) -> unsigned int{ + return fwrite(ptr, size, nmemb, (FILE*)fp); + }; + fileout.seek_proc = [](void* fp, long int off, int whence) -> int{ + return fseek((FILE*)fp, off, whence); + }; + fileout.tell_proc = [](void* fp) -> long int{ + return ftell((FILE*)fp); + }; + bool b; + switch(type){ + case FIF_JPEG: + b = image.saveToHandle(type, &fileout, (fi_handle)(fd.get()), JPEG_QUALITYGOOD | JPEG_SUBSAMPLING_411); + break; + case FIF_PNG: + b = image.saveToHandle(type, &fileout, (fi_handle)(fd.get()), PNG_Z_BEST_COMPRESSION); + break; + case FIF_GIF: + b = image.saveToHandle(type, &fileout, (fi_handle)(fd.get())); + break; + default: + ; + }; + ret.thumb_width = image.getWidth(); + ret.thumb_height = image.getHeight(); + + fd.rewind(); + ret.thumburl = _upload_file(fd, header); + ret.thumbsize = fd.length(); + + return ret; + } + raii::rjp_string bot::upload_video(const raii::string_base& filename){ + image_info ret; + + av_register_all(); + AVFormatContext* context = NULL; + avformat_open_input(&context, filename.get(), NULL, NULL); + avformat_find_stream_info(context, NULL); + av_dump_format(context, 0, filename.get(), false); + avformat_close_input(&context); + + raii::filerd fd(filename); + if(!fd) return {}; + + raii::curl_llist header(raii::string("Content-Type: video/mp4")); + return _upload_file(fd, header); + } + + 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; + + 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 + "\"" + "}"; + raii::rjp_string reply = _post_and_find( + body, + raii::string("https://" + m_homeserver + "/_matrix/client/r0/rooms/" + m_curl.encode(room) + "/send/m.room.message?access_token=" + m_access_token), + raii::curl_llist(), + "event_id"_ss); + return reply; + } + raii::rjp_string bot::send_video(const raii::string_base& room, const raii::string_base& file_url, const raii::string_base& filetype, const raii::string_base& filename){ + raii::rjp_string reply = _post_and_find(raii::string("{\"body\":\"" + raii::json_escape(filename) + "\",\"info\":{\"w\":854,\"mimetype\":\"video/mp4\"},\"msgtype\":\"m.video\",\"url\":\"" + raii::json_escape(file_url) + "\"}"), + raii::string("https://" + m_homeserver + "/_matrix/client/r0/rooms/" + m_curl.encode(room) + "/send/m.room.message?access_token=" + m_access_token), + raii::curl_llist(), + "event_id"_ss); + return reply; + } + raii::rjp_string bot::send_message(const raii::string_base& room, const raii::string_base& text){ + return _post_and_find(raii::string("{\"body\":\""_ss + raii::json_escape(text) + "\",\"msgtype\":\"m.text\"}"_ss), + raii::string("https://" + m_homeserver + "/_matrix/client/r0/rooms/" + m_curl.encode(room) + "/send/m.room.message?access_token=" + m_access_token), + raii::curl_llist(), + "event_id"_ss); + } + void bot::logout(void){ + _get_curl(raii::string("https://" + m_homeserver + "/_matrix/client/r0/logout?access_token=" + m_access_token)); + m_urls.invalidate_accesstoken(); + } + + + raii::rjp_string bot::_upload_file(raii::filerd& fp, const raii::curl_llist& header){ + raii::string fileurl; + m_curl.postreq(); + 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; + } + 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}; + } + } + +} + diff --git a/src/raii/curl_llist.cpp b/src/raii/curl_llist.cpp new file mode 100644 index 0000000..bffc7bb --- /dev/null +++ b/src/raii/curl_llist.cpp @@ -0,0 +1,41 @@ +#include "raii/curl_llist.hpp" + +#include //exchange + +namespace raii{ + + curl_llist::curl_llist(curl_llist&& l)noexcept: + m_data(std::exchange(l.m_data, nullptr)){} + curl_llist::~curl_llist(void){ + curl_slist_free_all(m_data); + } + + curl_llist& curl_llist::operator=(curl_llist&& l)noexcept{ + std::swap(m_data, l.m_data); + return *this; + } + + curl_llist& curl_llist::operator+=(const char* data){ + m_data = curl_slist_append(m_data, data); + return *this; + } + + curl_llist::operator curl_slist*(void){ + return m_data; + } + curl_llist::operator const curl_slist*(void)const{ + return m_data; + } + curl_slist* curl_llist::get(void){ + return m_data; + } + const curl_slist* curl_llist::get(void)const{ + return m_data; + } + + void curl_llist::reset(curl_slist* nd){ + curl_slist_free_all(m_data); + m_data = nd; + } + +} diff --git a/src/raii/curler.cpp b/src/raii/curler.cpp new file mode 100644 index 0000000..e44e4c0 --- /dev/null +++ b/src/raii/curler.cpp @@ -0,0 +1,89 @@ +#include "raii/curler.hpp" + +#include //exchange + +namespace raii{ + + //RAII wrapper for CURL* with some convenience functions added + curler::curler(void): + m_curl(curl_easy_init()){} + curler::curler(const curler& c): + m_curl(curl_easy_duphandle(c.m_curl)){} + curler::curler(curler&& c)noexcept: + m_curl(std::exchange(c.m_curl, nullptr)){} + curler::~curler(void){ + curl_easy_cleanup(m_curl); + } + + curler& curler::postreq(void){ + return setopt(CURLOPT_POST, 1L); + } + curler& curler::getreq(void){ + return setopt(CURLOPT_HTTPGET, 1L); + } + curler& curler::setheader(const curl_llist& h){ + return setopt(CURLOPT_HTTPHEADER, h.get()); + } + curler& curler::seturl(const char* s){ + return setopt(CURLOPT_URL, s); + } + curler& curler::seturl(const string_base& s){ + return seturl(s.get()); + } + curler& curler::setuseragent(const char* s){ + return setopt(CURLOPT_USERAGENT, s); + } + curler& curler::setuseragent(const string_base& s){ + return setuseragent(s.get()); + } + curler& curler::setuserpwd(const char* s){ + return setopt(CURLOPT_USERPWD, s); + } + curler& curler::setuserpwd(const string_base& s){ + return setuserpwd(s.get()); + } + curler& curler::setpostdata(const char* s, curl_off_t len){ + setopt(CURLOPT_POSTFIELDS, s); + setopt(CURLOPT_POSTFIELDSIZE_LARGE, len); + return *this; + } + curler& curler::setpostdata(const string_base& s){ + setpostdata(s.get(), s.length()); + return *this; + } + curler& curler::forcessl(long version){ + setopt(CURLOPT_SSL_VERIFYPEER, 1); + setopt(CURLOPT_SSL_VERIFYHOST, 1); + setopt(CURLOPT_SSLVERSION, version); + return *this; + } + + void curler::reset(void){ + curl_easy_reset(m_curl); + } + auto curler::perform(void) -> decltype(curl_easy_perform(m_curl)){ + return curl_easy_perform(m_curl); + } + + curl_string curler::encode(const char* data, int len){ + char* tmp = curl_easy_escape(m_curl, data, len); + return curl_string(tmp, strlen(tmp)); + } + curl_string curler::decode(const char* data, int* outlen, int len){ + return curl_string(curl_easy_unescape(m_curl, data, len, outlen), *outlen); + } + + CURL* curler::get(void){ + return m_curl; + } + const CURL* curler::get(void)const{ + return m_curl; + } + curler::operator CURL*(void){ + return m_curl; + } + curler::operator const CURL*(void)const{ + return m_curl; + } + +} diff --git a/src/raii/filerd.cpp b/src/raii/filerd.cpp new file mode 100644 index 0000000..b2a6966 --- /dev/null +++ b/src/raii/filerd.cpp @@ -0,0 +1,78 @@ +#include "raii/filerd.hpp" + +#include //fopen, fclose +#include //exchange, swap + +namespace raii{ + + filerd::filerd(const char* f, const char* mode): + m_fp(fopen(f, mode)){} + filerd::filerd(filerd&& f): + m_fp(std::exchange(f.m_fp, nullptr)){} + filerd::~filerd(void){ + if(m_fp) + fclose(m_fp); + } + filerd& filerd::operator=(filerd&& f){ + std::swap(m_fp, f.m_fp); + return *this; + } + + void filerd::reset(FILE* fp){ + if(m_fp) + fclose(m_fp); + m_fp = fp; + } + FILE* filerd::release(void){ + return std::exchange(m_fp, nullptr); + } + size_t filerd::length(void){ + if(!m_fp) + return 0; + size_t tmp, ret; + tmp = ftell(m_fp); + fseek(m_fp, 0, SEEK_END); + ret = ftell(m_fp); + fseek(m_fp, tmp, SEEK_SET); + return ret; + } + size_t filerd::position(void)const{ + return ftell(m_fp); + } + void filerd::rewind(size_t pos){ + fseek(m_fp, pos, SEEK_SET); + } + + filerd::operator FILE*(void){ + return m_fp; + } + filerd::operator const FILE*(void)const{ + return m_fp; + } + FILE* filerd::get(void){ + return m_fp; + } + const FILE* filerd::get(void)const{ + return m_fp; + } + filerd::operator bool(void)const{ + return m_fp; + } + + size_t filerd::read(char* dest, size_t bytes){ + return fread(dest, 1, bytes, m_fp); + } + raii::string filerd::read(size_t bytes){ + char* tmp = reinterpret_cast(raii::string::allocator_type::allocate(bytes)); + size_t written = read(tmp, bytes); + return raii::string(tmp, written); + } + size_t filerd::write(const char* c, size_t bytes){ + return fwrite(c, 1, bytes, m_fp); + } + size_t filerd::write(const raii::string_base& c){ + return write(c.get(), c.length()); + } + +} + diff --git a/src/raii/string_base.cpp b/src/raii/string_base.cpp new file mode 100644 index 0000000..6e72aa3 --- /dev/null +++ b/src/raii/string_base.cpp @@ -0,0 +1,140 @@ +#include "raii/string_base.hpp" +#include "raii/string.hpp" + +#include //exchange, swap +#include //memcpy +#include //strcpy, strlen +#include //bad_alloc + +namespace raii{ + string_base::string_base(size_t len): + m_length(len), + m_data(nullptr){} + string_base& string_base::operator=(const char* c){ + size_t len = strlen(c); + if(len <= m_length){ + strcpy(m_data, c); + }else{ + _free(m_data); + m_data = _copy(c, len+1); + if(!m_data) + throw std::bad_alloc{}; + } + m_length = len; + return *this; + } + string_base& string_base::operator=(const string_base& s){ + if(s.m_length <= m_length){ + strcpy(m_data, s.m_data); + }else{ + _free(m_data); + m_data = _copy(s.m_data, s.m_length+1); + if(!m_data) + throw std::bad_alloc{}; + } + m_length = s.m_length; + return *this; + } + void string_base::reset(char* val){ + _free(m_data); + m_data = val; + } + + size_t string_base::length(void)const{ + return m_length; + } + char* string_base::get(void){ + return m_data; + } + const char* string_base::get(void)const{ + return m_data; + } + string_base::operator bool(void)const{ + return m_data; + } + string_base::operator char*(void){ + return m_data; + } + string_base::operator const char*(void)const{ + return m_data; + } + char* string_base::release(void){ + return std::exchange(m_data, nullptr); + } + char& string_base::operator[](size_t i){ + return m_data[i]; + } + const char& string_base::operator[](size_t i)const{ + return m_data[i]; + } + + namespace detail{ + + size_t _calc_escaped_len(const char* str){ + size_t ret = 0; + for(const char* c = str;*c;++c){ + switch(*c){ + case '\\': + case '"': + case '/': + case '\n': + case '\r': + case '\b': + case '\f': + case '\t': + ++ret; + break; + }; + ++ret; + } + return ret; + } + char _escape_to_letter(char escape){ + switch(escape){ + case '\n': + return 'n'; + case '\r': + return 'r'; + case '\b': + return 'b'; + case '\f': + return 'f'; + case '\t': + return 't'; + }; + return '\\'; + } + size_t _sanitize_json_copy(char* dest, const char* in){ + size_t pos = 0; + for(const char* c = in;*c;++c){ + switch(*c){ + case '"': + case '\\': + case '/': + dest[pos++] = '\\'; + dest[pos++] = *c; + break; + case '\n': + case '\r': + case '\b': + case '\f': + case '\t': + dest[pos++] = '\\'; + dest[pos++] = _escape_to_letter(*c); + break; + default: + dest[pos++] = *c; + }; + } + dest[pos] = 0; + return pos; + } + } + string json_escape(const string_base& str){ + size_t len = detail::_calc_escaped_len(str.get()); + char* tmp = reinterpret_cast(string::allocator_type::allocate(len+1)); + detail::_sanitize_json_copy(tmp, str); + tmp[len] = 0; + return string(tmp, len); + } +} diff --git a/src/reddit.cpp b/src/reddit.cpp new file mode 100644 index 0000000..34bc710 --- /dev/null +++ b/src/reddit.cpp @@ -0,0 +1,438 @@ +#include "reddit.hpp" +#include "raii/rjp_string.hpp" +#include "raii/string.hpp" +#include "raii/curler.hpp" +#include "raii/rjp_ptr.hpp" +#include "raii/static_string.hpp" + +#include //search + +namespace reddit{ + + namespace time{ + detail::time_period hour = "hour"; + detail::time_period day = "day"; + detail::time_period week = "week"; + detail::time_period month = "month"; + detail::time_period year = "year"; + detail::time_period all = "all"; + } + + auth_data parse_auth_data(RJP_value* root){ + static const char* account_names[2] = {"bot", "account"}; + static const char* account_fields[2] = {"username", "password"}; + + auth_data ret; + RJP_search_res accounts[2]; + RJP_search_res details[2]; + rjp_search_members(root, 2, account_names, accounts, 0); + + rjp_search_members(accounts[0].value, 2, account_fields, details, 0); + ret.bot_name = details[0].value; + ret.bot_pass = details[1].value; + + rjp_search_members(accounts[1].value, 2, account_fields, details, 0); + ret.acc_name = details[0].value; + ret.acc_pass = details[1].value; + + return ret; + } + + + static raii::rjp_string media_search(RJP_value* root){ + RJP_search_res media = rjp_search_member(root, "media", 0); + if(!media.value) + return raii::rjp_string{}; + media = rjp_search_member(media.value, "reddit_video", 0); + if(!media.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}; + } + static raii::rjp_string preview_search(RJP_value* root){ + RJP_search_res media = rjp_search_member(root, "preview", 0); + if(!media.value) + return raii::rjp_string{}; + media = rjp_search_member(media.value, "reddit_video_preview", 0); + if(!media.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); + } + static bool check_reddit_media_domain(RJP_value* root){ + RJP_search_res res = rjp_search_member(root, "is_reddit_media_domain", 0); + return (res.value && rjp_value_boolean(res.value)); + } + + static raii::rjp_string find_video_url(RJP_value* root){ + if(raii::rjp_string res = media_search(root)) + return res; + raii::rjp_string res = preview_search(root); + return res; + } + static bool is_gifv(const raii::string_base& str){ + const char* s = str.get(); + size_t len = str.length(); + if(len > 5 && + *(s+len-1) == 'v' && + *(s+len-2) == 'f' && + *(s+len-3) == 'i' && + *(s+len-4) == 'g' && + *(s+len-5) == '.') + { + return true; + } + return false; + } + static bool has_extension(const raii::string_base& str){ + size_t i = 0; + for(const char* p = str.get() + str.length() - 1;*p && i < 6;--p,++i){ + if(*p == '/') + return false; + else if(*p == '.') + return true; + } + return false; + } + static bool is_gfycat_link(const raii::string_base& str){ + static const char gfycat[] = "gfycat.com"; + return *std::search(str.get(), str.get()+str.length(), gfycat, gfycat+sizeof(gfycat)-1) != 0; + } + static bool is_imgur_link(const raii::string_base& str){ + static const char imgur[] = "i.imgur.com"; + return *std::search(str.get(), str.get()+str.length(), imgur, imgur+sizeof(imgur)-1) != 0; + } + static bool is_direct_imgur_link(const raii::string_base& str){ + return is_imgur_link(str) && has_extension(str); + } + + + + post::post(const raii::string_base& p): + m_post(p) + { + _parse_post(); + } + post::post(raii::string_base&& p): + m_post(std::move(p)), + m_type(post_type::unrecognized) + { + _parse_post(); + } + void post::_parse_post(void){ + raii::rjp_ptr root(rjp_parse(m_post)); + if(!root) + return; + + static const char* search_items[] = {"url", "author", "post_hint", "title", "id"}; + static constexpr size_t num_searches = sizeof(search_items)/sizeof(search_items[0]); + RJP_search_res results[num_searches]; + RJP_search_res data = rjp_search_member(root.get(), "data", 0); + if(!data.value) return; + data = rjp_search_member(data.value, "children", 0); + if(!data.value) return; + data.value = rjp_get_element(data.value); + if(!data.value) return; + RJP_search_res kind = rjp_search_member(data.value, "kind", 0); + if(!kind.value) return; + data = rjp_search_member(data.value, "data", 0); + if(!data.value) return; + + rjp_search_members(data.value, num_searches, search_items, results, 0); + m_media_url = results[0].value; + m_author = results[1].value; + m_post_hint = results[2].value; + m_title = results[3].value; + m_name = raii::rjp_string(kind.value) + "_" + rjp_value_string(results[4].value); + m_post_url = "https://redd.it/" + raii::rjp_string(results[4].value); + + if(m_post_hint){ + //handle simple image + if(!strcmp(m_post_hint, "image")){ + m_type = post_type::image; + } + //handle link + else if(!strcmp(m_post_hint, "link")){ + m_type = post_type::link; + + //imgur support + if(is_imgur_link(m_media_url)){ + if(is_gifv(m_media_url)){ + if(raii::rjp_string tmp = preview_search(data.value)){ + m_media_url = std::move(tmp); + m_type = post_type::video; + } + }else{ + m_media_url += ".jpg"; //imgur is dumb + m_type = post_type::image; + } + }else if(is_gfycat_link(m_media_url)){ + if(raii::rjp_string tmp = find_video_url(data.value)){ + m_media_url = std::move(tmp); + m_type = post_type::video; + } + } + } + //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); + if(res){ + m_type = post_type::video; + m_media_url = std::move(res); + return; + } + res = preview_search(data.value); + if(res){ + m_type = post_type::video; + m_media_url = std::move(res); + return; + } + m_type = post_type::link; + } + //assume text post for other + else{ + m_type = post_type::text; + } + }else if(is_direct_imgur_link(m_media_url)){ + m_type = post_type::image; + return; + }else if(check_reddit_media_domain(data.value)){ + RJP_value* media = rjp_search_member(data.value, "media", 0).value; + if(media && (rjp_value_type(media) != json_null)) + m_type = post_type::video; + else + m_type = post_type::image; + }else{ + m_media_url.reset(); + m_type = post_type::text; + } + } + post& post::operator=(const raii::string_base& p){ + post tmp(p); + if(!tmp) + return *this; + return (*this = std::move(tmp)); + } + post::operator bool(void)const{ + if(m_type == post_type::text) + return (m_post_url && m_author && m_title && m_name); + else + return (m_post_url && m_media_url && m_author && m_title && m_name); + } + + const raii::string& post::raw(void)const{ + return m_post; + } + const raii::rjp_string& post::mediaurl(void)const{ + return m_media_url; + } + const raii::rjp_string& post::posturl(void)const{ + return m_post_url; + } + const raii::rjp_string& post::author(void)const{ + return m_author; + } + const raii::rjp_string& post::post_hint(void)const{ + return m_post_hint; + } + const raii::rjp_string& post::title(void)const{ + return m_title; + } + const raii::rjp_string& post::name(void)const{ + return m_name; + } + post_type post::type(void)const{ + return m_type; + } + + + + bot::bot(const auth_data& a, const raii::string_base& useragent): + m_curl(), + m_useragent(useragent), + m_access_token(_acquire_access_token(a)){} + bot::bot(const auth_data& a, raii::string_base&& useragent): + m_curl(), + m_useragent(std::move(useragent)), + m_access_token(_acquire_access_token(a)){} + bot::bot(const bot& b): + m_curl(b.m_curl), + m_useragent(b.m_useragent), + m_access_token(b.m_access_token){} + bot::bot(bot&& b): + m_curl(std::move(b.m_curl)), + m_useragent(std::move(b.m_useragent)), + m_access_token(std::move(b.m_access_token)){} + + bot& bot::operator=(bot&& b){ + m_useragent = std::move(b.m_useragent); + m_access_token = std::move(b.m_access_token); + return *this; + } + bot& bot::operator=(const bot& b){ + bot tmp(b); + return *this = std::move(tmp); + } + + const raii::rjp_string& bot::access_token(void)const{ + return m_access_token; + } + const raii::string& bot::useragent(void)const{ + return m_useragent; + } + void bot::set_useragent(const raii::string_base& s){ + m_useragent = s; + } + void bot::set_useragent(raii::string_base&& s){ + m_useragent = std::move(s); + } + + post bot::get_new_post(const raii::string_base& subreddit){ + return _get_post(subreddit, "new"_ss, "limit=1"_ss); + } + post bot::get_new_post(const raii::string_base& subreddit, const raii::string_base& after){ + return _get_post(subreddit, "new"_ss, raii::string("limit=1&after=" + after)); + } + post bot::get_hot_post(const raii::string_base& subreddit){ + return _get_post(subreddit, "hot"_ss, "limit=1"_ss); + } + post bot::get_hot_post(const raii::string_base& subreddit, const raii::string_base& after){ + return _get_post(subreddit, "hot"_ss, raii::string("limit=1&after=" + after)); + } + post bot::get_rising_post(const raii::string_base& subreddit){ + return _get_post(subreddit, "rising"_ss, "limit=1"_ss); + } + post bot::get_rising_post(const raii::string_base& subreddit, const raii::string_base& after){ + return _get_post(subreddit, "rising"_ss, raii::string("limit=1&after=" + after)); + } + post bot::get_best_post(const raii::string_base& subreddit){ + return _get_post(subreddit, "best"_ss, "limit=1"_ss); + } + post bot::get_best_post(const raii::string_base& subreddit, const raii::string_base& after){ + return _get_post(subreddit, "best"_ss, raii::string("limit=1&after=" + after)); + } + post bot::get_top_post(const raii::string_base& subreddit, time::detail::time_period period){ + raii::static_string pstr = period.get(); + return _get_post(subreddit, "top"_ss, raii::string("limit=1&t=" + pstr)); + } + post bot::get_top_post(const raii::string_base& subreddit, const raii::string_base& after, time::detail::time_period period){ + raii::static_string pstr = period.get(); + return _get_post(subreddit, "top"_ss, raii::string("limit=1&t=" + pstr + "&after=" + after)); + } + post bot::get_controversial_post(const raii::string_base& subreddit, time::detail::time_period period){ + raii::static_string pstr = period.get(); + return _get_post(subreddit, "controversial"_ss, raii::string("limit=1&t=" + pstr)); + } + post bot::get_controversial_post(const raii::string_base& subreddit, const raii::string_base& after, time::detail::time_period period){ + raii::static_string pstr = period.get(); + return _get_post(subreddit, "controversial"_ss, raii::string("limit=1&t=" + pstr + "&after=" + after)); + } + + + post bot::_get_post(const raii::string_base& subreddit, const raii::string_base& category, const raii::string_base& extra){ + raii::string rep; + static constexpr char url_base[] = "https://oauth.reddit.com/r/"; + raii::string url; + if(extra) + url = (url_base + subreddit) + "/" + category + "?" + extra; + else + url = (url_base + subreddit) + "/" + category; + raii::curl_llist header(_create_auth_header(m_access_token)); + m_curl.reset(); + _setup_subreddit_get_curl(header, url, rep); + m_curl.perform(); + return post(rep); + } + size_t bot::_get_response_curl_callback(char* ptr, size_t size, size_t nmemb, void* userdata){ + raii::rjp_string* reply = reinterpret_cast(userdata); + (*reply) += ptr; + return size*nmemb; + } + raii::curl_llist bot::_create_auth_header(const raii::string_base& access_token){ + return raii::curl_llist(raii::string("Authorization: bearer " + access_token)); + } + void bot::_setup_subreddit_get_curl(const raii::curl_llist& header, const raii::string_base& url, const raii::string_base& reply){ + m_curl.seturl(url); + m_curl.setopt(CURLOPT_BUFFERSIZE, 102400L); + m_curl.setopt(CURLOPT_NOPROGRESS, 1L); + 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); + m_curl.setheader(header); + m_curl.setuseragent(m_useragent); + m_curl.setopt(CURLOPT_WRITEFUNCTION, _get_response_curl_callback); + m_curl.setopt(CURLOPT_WRITEDATA, &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; + } + //Create reddit login data + raii::string bot::_create_request_post_data(const raii::string_base& account_name, const raii::string_base& account_pass){ + return raii::string("grant_type=password&username=" + account_name + "&password=" + account_pass); + } + //Setup login data for reddit bot + raii::string bot::_create_request_userpwd(const raii::string_base& bot_name, const raii::string_base& bot_pass){ + return raii::string(bot_name + ":" + bot_pass); + } + void bot::_setup_token_request_curl(const raii::string_base& userpwd, const raii::string_base& postdata, void* result){ + static constexpr char reddit_token_address[] = "https://www.reddit.com/api/v1/access_token"; + m_curl.setopt(CURLOPT_BUFFERSIZE, 102400L); + m_curl.seturl(reddit_token_address); + m_curl.setopt(CURLOPT_NOPROGRESS, 1L); + m_curl.setuserpwd(userpwd); + m_curl.setpostdata(postdata); + 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_CUSTOMREQUEST, "POST"); + m_curl.setopt(CURLOPT_TCP_KEEPALIVE, 1L); + m_curl.setopt(CURLOPT_WRITEFUNCTION, _post_reply_curl_callback); + m_curl.setopt(CURLOPT_WRITEDATA, result); + } + + raii::string bot::_request_access_token(const auth_data& auth){ + CURLcode result; + + //URL encode the POST data + raii::curl_string acc_name = m_curl.encode(auth.acc_name, auth.acc_name.length()); + raii::curl_string acc_pass = m_curl.encode(auth.acc_pass, auth.acc_pass.length()); + + //unify the post data, clean up remnants + raii::string postdata = _create_request_post_data(acc_name, acc_pass); + acc_name.reset(); + acc_pass.reset(); + + //Unify the username/password + raii::string userpwd = _create_request_userpwd(auth.bot_name, auth.bot_pass); + + //Load curl with data then run POST operation + raii::string reply; + _setup_token_request_curl(userpwd, postdata, &reply); + result = m_curl.perform(); + + if(result != CURLE_OK) + return {}; + return reply; + } + + raii::rjp_string bot::_acquire_access_token(const auth_data& a){ + raii::string reply = _request_access_token(a); + if(!reply) + return raii::rjp_string{}; + + raii::rjp_ptr root(rjp_parse(reply)); + if(!root) + return raii::rjp_string{}; + RJP_search_res token = rjp_search_member(root.get(), "access_token", 0); + return raii::rjp_string{token.value}; + } +} diff --git a/src/test.cpp b/src/test.cpp new file mode 100644 index 0000000..9ffc195 --- /dev/null +++ b/src/test.cpp @@ -0,0 +1,169 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#define DEBUG + +#ifdef DEBUG +# define DEBUG_PRINT(...) do{printf(__VA_ARGS__);}while(0) +#else +# define DEBUG_PRINT(...) do{}while(0) +#endif + +#include "raii/curler.hpp" +#include "raii/filerd.hpp" +#include "raii/rjp_string.hpp" +#include "raii/string.hpp" +#include "raii/rjp_ptr.hpp" +#include "raii/static_string.hpp" +#include "reddit.hpp" +#include "matrix.hpp" + + +//Get username/password for reddit account and bot. Plus a useragent string +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); + + res = rjp_search_member(root.get(), "useragent", 0); + + return std::tuple(std::move(red_ret), std::move(mat_ret), raii::rjp_string(res.value)); +} +//Read in file containing username/password details +raii::rjp_ptr read_data_file(const char* file){ + raii::filerd fp(file); + size_t blen; + if(!fp){ + return nullptr; + } + blen = fp.length(); + raii::string buff(blen); + fread(buff, blen, 1, fp); + buff[blen] = 0; + + return raii::rjp_ptr(rjp_parse(buff)); +} + +size_t filewrite_response(char* ptr, size_t size, size_t nmemb, void* userdata){ + fwrite(ptr, size, nmemb, reinterpret_cast(userdata)); + return nmemb*size; +} + +void file_output_curl(raii::curler& curl, const raii::string_base& url){ + //Download the post's image + raii::filerd fp("testout", "w"); + if(!fp) + fprintf(stderr, "unable to open file for writing\n"); + curl.seturl(url); + curl.setopt(CURLOPT_BUFFERSIZE, 102400L); + curl.setopt(CURLOPT_NOPROGRESS, 1L); + curl.setopt(CURLOPT_FOLLOWLOCATION, 1L); + curl.setopt(CURLOPT_MAXREDIRS, 50L); + curl.forcessl(CURL_SSLVERSION_TLSv1_2); + curl.setopt(CURLOPT_WRITEFUNCTION, filewrite_response); + curl.setopt(CURLOPT_WRITEDATA, fp.get()); + curl.perform(); +} + + +void write_to_file(const char* file, const raii::string_base& data){ + raii::filerd out(file, "a"); + out.write(data); + out.write("\n"_ss); +} + +int main(){ + //Read data file + DEBUG_PRINT("reading data file \"data\"\n"); + raii::rjp_ptr root = read_data_file("data"); + if(!root){ + fprintf(stderr, "Could not open data file\n"); + return 1; + } + + //Parse data file + auto [reddit_auth,matrix_auth,useragent] = parse_data_file(root); + if(!(reddit_auth && matrix_auth && useragent)){ + fprintf(stderr, "Missing data field\n"); + return 2; + } + + //Get reddit post + reddit::bot mybot(reddit_auth, useragent); + DEBUG_PRINT("reddit bot initialized\n"); + matrix::bot matbot(matrix_auth, useragent); + DEBUG_PRINT("matrix bot initialized\n"); + + reddit::post reply; + + { + int retries = 5; + do{ + reply = mybot.get_new_post("ProgrammerHumor"_ss, reply.name()); + if(reply.type() != reddit::post_type::text) + break; + --retries; + DEBUG_PRINT("Not an image.\nTODO, search for another\n"); + if(reply.post_hint()) + DEBUG_PRINT("post_hint: %s\n", reply.post_hint().get()); + }while(retries); + } + + if(!reply){ + fprintf(stderr, "Did not recieve a reply!\n"); + return 3; + } + write_to_file("post.log", reply.raw()); + + DEBUG_PRINT("name: %s\n", reply.name().get()); + DEBUG_PRINT("title: %s\n", reply.title().get()); + DEBUG_PRINT("author: %s\n", reply.author().get()); + 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()); + auto img_data = matbot.upload_image("testout"_ss, reply.name()); + auto val = matbot.send_image("!QeYfNDCRodtNohhnaI:matrix.org"_ss, 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())); + 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_url = matbot.upload_video("testout"_ss); + //rudimentary video sending + auto val = matbot.send_video("!QeYfNDCRodtNohhnaI:matrix.org"_ss, vid_url, "mp4"_ss, raii::static_string("testout")); + val = matbot.send_message("!QeYfNDCRodtNohhnaI:matrix.org"_ss, raii::string(reply.title() + "\n" + reply.posturl())); + } + else if(reply.type() == reddit::post_type::link) + { + DEBUG_PRINT("Got a link\n"); + return 0; + } + else + { + DEBUG_PRINT("post_hint: %s\n", reply.post_hint().get()); + DEBUG_PRINT("TODO HANDLE THIS TYPE OF POST\n"); + return 0; + } + + +} diff --git a/upload_bot_image.sh b/upload_bot_image.sh new file mode 100755 index 0000000..de548e7 --- /dev/null +++ b/upload_bot_image.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +#A script to easily upload an image to a matrix homeserver for use as a profile picture of the bot. +#Will print out the URI of the uploaded image if it succeeds. If it fails, I have no guess as to what might happen tbh + +image_file="$1" +homeserver="$2" +username="$3" +password="$4" + +if [ -z "$image_file" ];then + echo "need image file to upload" + exit 1 +fi +if [ -z "$homeserver" ];then + echo "need homeserver to upload to" + exit 2 +fi +if [ -z "$username" ];then + echo "need username" + exit 3 +fi + +if [ -z "$password" ];then + stty -echo + printf "Password: " + read password + stty echo + echo +fi + +image_type="$(file "$image_file" | grep -o "[^ \t]* image data" | tr '[:upper:]' '[:lower:]' | cut -d' ' -f1)" + +access_token="$(curl -X POST -d '{"type":"m.login.password", "user":"'"$username"'", "password":"'"${password}"'"}' "https://${homeserver}/_matrix/client/r0/login" | grep "access_token" | sed -e 's/^[ \t]*//' -e 's/[\"\,]//g' | cut -d' ' -f2)" + +media_id="$(curl -X POST -H "Content-Type: image/${image_type}" --data-binary @"$image_file" "https://${homeserver}/_matrix/media/r0/upload?access_token=${access_token}&filename=$(basename $image_file)" | cut -d':' -f2- | sed -e 's/[\"\}]//g')" +echo "$media_id" diff --git a/youtube_curl.txt b/youtube_curl.txt new file mode 100644 index 0000000..fbe6c24 --- /dev/null +++ b/youtube_curl.txt @@ -0,0 +1,2 @@ +curl -XGET http://www.youtube.com/get_video_info?video_id=<> +