diff options
Diffstat (limited to 'father.cpp')
| -rw-r--r-- | father.cpp | 190 | 
1 files changed, 144 insertions, 46 deletions
| diff --git a/father.cpp b/father.cpp index d95f506..1bef9e3 100644 --- a/father.cpp +++ b/father.cpp | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | #include <twitter.h> | 1 | #include <mastodonpp/mastodonpp.hpp> | 
| 2 | #include <random> | 2 | #include <random> | 
| 3 | #include <yaml-cpp/yaml.h> | 3 | #include <yaml-cpp/yaml.h> | 
| 4 | #include <iostream> | 4 | #include <iostream> | 
| @@ -11,6 +11,8 @@ | |||
| 11 | #include <iterator> | 11 | #include <iterator> | 
| 12 | #include <verbly.h> | 12 | #include <verbly.h> | 
| 13 | #include <hkutil/string.h> | 13 | #include <hkutil/string.h> | 
| 14 | #include <json.hpp> | ||
| 15 | #include "timeline.h" | ||
| 14 | 16 | ||
| 15 | // Sync followers every 4 hours. | 17 | // Sync followers every 4 hours. | 
| 16 | const int CHECK_FOLLOWERS_EVERY = 4 * 60 / 5; | 18 | const int CHECK_FOLLOWERS_EVERY = 4 * 60 / 5; | 
| @@ -32,6 +34,46 @@ verbly::word findWordOfType( | |||
| 32 | } | 34 | } | 
| 33 | } | 35 | } | 
| 34 | 36 | ||
| 37 | std::set<std::string> getPaginatedList( | ||
| 38 | mastodonpp::Connection& connection, | ||
| 39 | mastodonpp::API::endpoint_type endpoint, | ||
| 40 | std::string account_id) | ||
| 41 | { | ||
| 42 | std::set<std::string> result; | ||
| 43 | |||
| 44 | mastodonpp::parametermap parameters; | ||
| 45 | for (;;) | ||
| 46 | { | ||
| 47 | parameters["id"] = account_id; | ||
| 48 | |||
| 49 | auto answer = connection.get(endpoint, parameters); | ||
| 50 | if (!answer) | ||
| 51 | { | ||
| 52 | if (answer.curl_error_code == 0) | ||
| 53 | { | ||
| 54 | std::cout << "HTTP status: " << answer.http_status << std::endl; | ||
| 55 | } | ||
| 56 | else | ||
| 57 | { | ||
| 58 | std::cout << "libcurl error " << std::to_string(answer.curl_error_code) | ||
| 59 | << ": " << answer.error_message << std::endl; | ||
| 60 | } | ||
| 61 | return {}; | ||
| 62 | } | ||
| 63 | |||
| 64 | parameters = answer.next(); | ||
| 65 | if (parameters.empty()) break; | ||
| 66 | |||
| 67 | nlohmann::json body = nlohmann::json::parse(answer.body); | ||
| 68 | for (const auto& item : body) | ||
| 69 | { | ||
| 70 | result.insert(item["id"].get<std::string>()); | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | return result; | ||
| 75 | } | ||
| 76 | |||
| 35 | int main(int argc, char** argv) | 77 | int main(int argc, char** argv) | 
| 36 | { | 78 | { | 
| 37 | std::random_device randomDevice; | 79 | std::random_device randomDevice; | 
| @@ -48,17 +90,38 @@ int main(int argc, char** argv) | |||
| 48 | 90 | ||
| 49 | verbly::database database(config["verbly_datafile"].as<std::string>()); | 91 | verbly::database database(config["verbly_datafile"].as<std::string>()); | 
| 50 | 92 | ||
| 51 | twitter::auth auth( | 93 | mastodonpp::Instance instance{ | 
| 52 | config["consumer_key"].as<std::string>(), | 94 | config["mastodon_instance"].as<std::string>(), | 
| 53 | config["consumer_secret"].as<std::string>(), | 95 | config["mastodon_token"].as<std::string>()}; | 
| 54 | config["access_key"].as<std::string>(), | 96 | mastodonpp::Connection connection{instance}; | 
| 55 | config["access_secret"].as<std::string>()); | ||
| 56 | 97 | ||
| 57 | auto startedTime = std::chrono::system_clock::now(); | 98 | nlohmann::json account_details; | 
| 99 | { | ||
| 100 | const mastodonpp::parametermap parameters {}; | ||
| 101 | auto answer = connection.get(mastodonpp::API::v1::accounts_verify_credentials, parameters); | ||
| 102 | if (!answer) | ||
| 103 | { | ||
| 104 | if (answer.curl_error_code == 0) | ||
| 105 | { | ||
| 106 | std::cout << "HTTP status: " << answer.http_status << std::endl; | ||
| 107 | } | ||
| 108 | else | ||
| 109 | { | ||
| 110 | std::cout << "libcurl error " << std::to_string(answer.curl_error_code) | ||
| 111 | << ": " << answer.error_message << std::endl; | ||
| 112 | } | ||
| 113 | return 1; | ||
| 114 | } | ||
| 115 | std::cout << answer.body << std::endl; | ||
| 116 | account_details = nlohmann::json::parse(answer.body); | ||
| 117 | } | ||
| 118 | |||
| 119 | timeline home_timeline(mastodonpp::API::v1::timelines_home); | ||
| 120 | home_timeline.poll(connection); // just ignore the results | ||
| 58 | 121 | ||
| 59 | twitter::client client(auth); | 122 | auto startedTime = std::chrono::system_clock::now(); | 
| 60 | 123 | ||
| 61 | std::set<twitter::user_id> friends; | 124 | std::set<std::string> friends; | 
| 62 | int followerTimeout = 0; | 125 | int followerTimeout = 0; | 
| 63 | 126 | ||
| 64 | for (;;) | 127 | for (;;) | 
| @@ -68,11 +131,17 @@ int main(int argc, char** argv) | |||
| 68 | // Sync friends with followers. | 131 | // Sync friends with followers. | 
| 69 | try | 132 | try | 
| 70 | { | 133 | { | 
| 71 | friends = client.getFriends(); | 134 | friends = getPaginatedList( | 
| 135 | connection, | ||
| 136 | mastodonpp::API::v1::accounts_id_following, | ||
| 137 | account_details["id"].get<std::string>()); | ||
| 72 | 138 | ||
| 73 | std::set<twitter::user_id> followers = client.getFollowers(); | 139 | std::set<std::string> followers = getPaginatedList( | 
| 140 | connection, | ||
| 141 | mastodonpp::API::v1::accounts_id_followers, | ||
| 142 | account_details["id"].get<std::string>()); | ||
| 74 | 143 | ||
| 75 | std::list<twitter::user_id> oldFriends; | 144 | std::list<std::string> oldFriends; | 
| 76 | std::set_difference( | 145 | std::set_difference( | 
| 77 | std::begin(friends), | 146 | std::begin(friends), | 
| 78 | std::end(friends), | 147 | std::end(friends), | 
| @@ -80,7 +149,7 @@ int main(int argc, char** argv) | |||
| 80 | std::end(followers), | 149 | std::end(followers), | 
| 81 | std::back_inserter(oldFriends)); | 150 | std::back_inserter(oldFriends)); | 
| 82 | 151 | ||
| 83 | std::set<twitter::user_id> newFollowers; | 152 | std::set<std::string> newFollowers; | 
| 84 | std::set_difference( | 153 | std::set_difference( | 
| 85 | std::begin(followers), | 154 | std::begin(followers), | 
| 86 | std::end(followers), | 155 | std::end(followers), | 
| @@ -88,24 +157,44 @@ int main(int argc, char** argv) | |||
| 88 | std::end(friends), | 157 | std::end(friends), | 
| 89 | std::inserter(newFollowers, std::begin(newFollowers))); | 158 | std::inserter(newFollowers, std::begin(newFollowers))); | 
| 90 | 159 | ||
| 91 | for (twitter::user_id f : oldFriends) | 160 | for (const std::string& f : oldFriends) | 
| 92 | { | 161 | { | 
| 93 | client.unfollow(f); | 162 | const mastodonpp::parametermap parameters {{"id", f}}; | 
| 163 | auto answer = connection.post(mastodonpp::API::v1::accounts_id_unfollow, parameters); | ||
| 164 | if (!answer) | ||
| 165 | { | ||
| 166 | if (answer.curl_error_code == 0) | ||
| 167 | { | ||
| 168 | std::cout << "HTTP status: " << answer.http_status << std::endl; | ||
| 169 | } | ||
| 170 | else | ||
| 171 | { | ||
| 172 | std::cout << "libcurl error " << std::to_string(answer.curl_error_code) | ||
| 173 | << ": " << answer.error_message << std::endl; | ||
| 174 | } | ||
| 175 | } | ||
| 94 | } | 176 | } | 
| 95 | 177 | ||
| 96 | std::list<twitter::user> newFollowerObjs = | 178 | for (const std::string& f : newFollowers) | 
| 97 | client.hydrateUsers(std::move(newFollowers)); | ||
| 98 | |||
| 99 | for (twitter::user f : newFollowerObjs) | ||
| 100 | { | 179 | { | 
| 101 | if (!f.isProtected()) | 180 | const mastodonpp::parametermap parameters {{"id", f}, {"reblogs", "false"}}; | 
| 181 | auto answer = connection.post(mastodonpp::API::v1::accounts_id_follow, parameters); | ||
| 182 | if (!answer) | ||
| 102 | { | 183 | { | 
| 103 | client.follow(f); | 184 | if (answer.curl_error_code == 0) | 
| 185 | { | ||
| 186 | std::cout << "HTTP status: " << answer.http_status << std::endl; | ||
| 187 | } | ||
| 188 | else | ||
| 189 | { | ||
| 190 | std::cout << "libcurl error " << std::to_string(answer.curl_error_code) | ||
| 191 | << ": " << answer.error_message << std::endl; | ||
| 192 | } | ||
| 104 | } | 193 | } | 
| 105 | } | 194 | } | 
| 106 | } catch (const twitter::twitter_error& error) | 195 | } catch (const std::exception& error) | 
| 107 | { | 196 | { | 
| 108 | std::cout << "Twitter error while syncing followers: " << error.what() | 197 | std::cout << "Error while syncing followers: " << error.what() | 
| 109 | << std::endl; | 198 | << std::endl; | 
| 110 | } | 199 | } | 
| 111 | 200 | ||
| @@ -117,25 +206,32 @@ int main(int argc, char** argv) | |||
| 117 | try | 206 | try | 
| 118 | { | 207 | { | 
| 119 | // Poll the timeline. | 208 | // Poll the timeline. | 
| 120 | std::list<twitter::tweet> tweets = client.getHomeTimeline().poll(); | 209 | std::list<nlohmann::json> posts = home_timeline.poll(connection); | 
| 121 | 210 | ||
| 122 | for (twitter::tweet& tweet : tweets) | 211 | for (const nlohmann::json& post : posts) | 
| 123 | { | 212 | { | 
| 124 | auto createdTime = | ||
| 125 | std::chrono::system_clock::from_time_t(tweet.getCreatedAt()); | ||
| 126 | |||
| 127 | if ( | 213 | if ( | 
| 128 | // Only monitor people you are following | 214 | // Only monitor people you are following | 
| 129 | friends.count(tweet.getAuthor().getID()) | 215 | friends.count(post["account"]["id"].get<std::string>()) | 
| 130 | // Ignore tweets from before the bot started up | ||
| 131 | && createdTime > startedTime | ||
| 132 | // Ignore retweets | 216 | // Ignore retweets | 
| 133 | && !tweet.isRetweet() | 217 | && post["reblog"].is_null()) | 
| 134 | // Ignore messages | ||
| 135 | && tweet.getText().front() != '@') | ||
| 136 | { | 218 | { | 
| 219 | std::string post_content = post["content"].get<std::string>(); | ||
| 220 | std::string::size_type pos; | ||
| 221 | while ((pos = post_content.find("<")) != std::string::npos) | ||
| 222 | { | ||
| 223 | std::string prefix = post_content.substr(0, pos); | ||
| 224 | std::string rest = post_content.substr(pos); | ||
| 225 | std::string::size_type right_pos = rest.find(">"); | ||
| 226 | if (right_pos == std::string::npos) { | ||
| 227 | post_content = prefix; | ||
| 228 | } else { | ||
| 229 | post_content = prefix + rest.substr(right_pos); | ||
| 230 | } | ||
| 231 | } | ||
| 232 | |||
| 137 | std::vector<std::string> tokens = | 233 | std::vector<std::string> tokens = | 
| 138 | hatkirby::split<std::vector<std::string>>(tweet.getText(), " "); | 234 | hatkirby::split<std::vector<std::string>>(post_content, " "); | 
| 139 | 235 | ||
| 140 | std::vector<std::string> canonical; | 236 | std::vector<std::string> canonical; | 
| 141 | for (std::string token : tokens) | 237 | for (std::string token : tokens) | 
| @@ -212,21 +308,23 @@ int main(int argc, char** argv) | |||
| 212 | name)), | 308 | name)), | 
| 213 | "I'm Dad."}; | 309 | "I'm Dad."}; | 
| 214 | 310 | ||
| 215 | std::string result = | 311 | std::string result = "@" + post["account"]["acct"].get<std::string>() + " " + action.compile(); | 
| 216 | tweet.generateReplyPrefill(client.getUser()) | ||
| 217 | + action.compile(); | ||
| 218 | 312 | ||
| 219 | std::cout << result << std::endl; | 313 | mastodonpp::parametermap parameters{ | 
| 314 | {"status", result}, | ||
| 315 | {"in_reply_to_id", post["id"].get<std::string>()}}; | ||
| 220 | 316 | ||
| 221 | if (result.length() <= 140) | 317 | auto answer{connection.post(mastodonpp::API::v1::statuses, parameters)}; | 
| 318 | if (!answer) | ||
| 222 | { | 319 | { | 
| 223 | try | 320 | if (answer.curl_error_code == 0) | 
| 224 | { | 321 | { | 
| 225 | client.replyToTweet(result, tweet); | 322 | std::cout << "HTTP status: " << answer.http_status << std::endl; | 
| 226 | } catch (const twitter::twitter_error& e) | 323 | } | 
| 324 | else | ||
| 227 | { | 325 | { | 
| 228 | std::cout << "Twitter error while tweeting: " << e.what() | 326 | std::cout << "libcurl error " << std::to_string(answer.curl_error_code) | 
| 229 | << std::endl; | 327 | << ": " << answer.error_message << std::endl; | 
| 230 | } | 328 | } | 
| 231 | } | 329 | } | 
| 232 | } | 330 | } | 
| @@ -234,7 +332,7 @@ int main(int argc, char** argv) | |||
| 234 | } | 332 | } | 
| 235 | } | 333 | } | 
| 236 | } | 334 | } | 
| 237 | } catch (const twitter::rate_limit_exceeded&) | 335 | } catch (const std::exception&) | 
| 238 | { | 336 | { | 
| 239 | // Wait out the rate limit (10 minutes here and 5 below = 15). | 337 | // Wait out the rate limit (10 minutes here and 5 below = 15). | 
| 240 | std::this_thread::sleep_for(std::chrono::minutes(10)); | 338 | std::this_thread::sleep_for(std::chrono::minutes(10)); | 
