#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "timeline.h" // Sync followers every 4 hours. const int CHECK_FOLLOWERS_EVERY = 4 * 60 / 5; verbly::word findWordOfType( verbly::database& database, std::string form, verbly::part_of_speech partOfSpeech) { std::vector isThing = database.words( (verbly::notion::partOfSpeech == partOfSpeech) && (verbly::form::text == form)).all(); if (isThing.empty()) { return {}; } else { return isThing.front(); } } std::set getPaginatedList( mastodonpp::Connection& connection, mastodonpp::API::endpoint_type endpoint, std::string account_id) { std::set result; mastodonpp::parametermap parameters; for (;;) { parameters["id"] = account_id; auto answer = connection.get(endpoint, parameters); if (!answer) { if (answer.curl_error_code == 0) { std::cout << "HTTP status: " << answer.http_status << std::endl; } else { std::cout << "libcurl error " << std::to_string(answer.curl_error_code) << ": " << answer.error_message << std::endl; } return {}; } parameters = answer.next(); if (parameters.empty()) break; nlohmann::json body = nlohmann::json::parse(answer.body); for (const auto& item : body) { result.insert(item["id"].get()); } } return result; } int main(int argc, char** argv) { std::random_device randomDevice; std::mt19937 rng{randomDevice()}; if (argc != 2) { std::cout << "usage: father [configfile]" << std::endl; return -1; } std::string configfile(argv[1]); YAML::Node config = YAML::LoadFile(configfile); verbly::database database(config["verbly_datafile"].as()); mastodonpp::Instance instance{ config["mastodon_instance"].as(), config["mastodon_token"].as()}; mastodonpp::Connection connection{instance}; nlohmann::json account_details; { const mastodonpp::parametermap parameters {}; auto answer = connection.get(mastodonpp::API::v1::accounts_verify_credentials, parameters); if (!answer) { if (answer.curl_error_code == 0) { std::cout << "HTTP status: " << answer.http_status << std::endl; } else { std::cout << "libcurl error " << std::to_string(answer.curl_error_code) << ": " << answer.error_message << std::endl; } return 1; } std::cout << answer.body << std::endl; account_details = nlohmann::json::parse(answer.body); } timeline home_timeline(mastodonpp::API::v1::timelines_home); home_timeline.poll(connection); // just ignore the results auto startedTime = std::chrono::system_clock::now(); std::set friends; int followerTimeout = 0; for (;;) { if (followerTimeout == 0) { // Sync friends with followers. try { friends = getPaginatedList( connection, mastodonpp::API::v1::accounts_id_following, account_details["id"].get()); std::set followers = getPaginatedList( connection, mastodonpp::API::v1::accounts_id_followers, account_details["id"].get()); std::list oldFriends; std::set_difference( std::begin(friends), std::end(friends), std::begin(followers), std::end(followers), std::back_inserter(oldFriends)); std::set newFollowers; std::set_difference( std::begin(followers), std::end(followers), std::begin(friends), std::end(friends), std::inserter(newFollowers, std::begin(newFollowers))); for (const std::string& f : oldFriends) { const mastodonpp::parametermap parameters {{"id", f}}; auto answer = connection.post(mastodonpp::API::v1::accounts_id_unfollow, parameters); if (!answer) { if (answer.curl_error_code == 0) { std::cout << "HTTP status: " << answer.http_status << std::endl; } else { std::cout << "libcurl error " << std::to_string(answer.curl_error_code) << ": " << answer.error_message << std::endl; } } } for (const std::string& f : newFollowers) { const mastodonpp::parametermap parameters {{"id", f}, {"reblogs", "false"}}; auto answer = connection.post(mastodonpp::API::v1::accounts_id_follow, parameters); if (!answer) { if (answer.curl_error_code == 0) { std::cout << "HTTP status: " << answer.http_status << std::endl; } else { std::cout << "libcurl error " << std::to_string(answer.curl_error_code) << ": " << answer.error_message << std::endl; } } } } catch (const std::exception& error) { std::cout << "Error while syncing followers: " << error.what() << std::endl; } followerTimeout = CHECK_FOLLOWERS_EVERY; } followerTimeout--; try { // Poll the timeline. std::list posts = home_timeline.poll(connection); for (const nlohmann::json& post : posts) { if ( // Only monitor people you are following friends.count(post["account"]["id"].get()) // Ignore retweets && post["reblog"].is_null()) { std::string post_content = post["content"].get(); std::string::size_type pos; while ((pos = post_content.find("<")) != std::string::npos) { std::string prefix = post_content.substr(0, pos); std::string rest = post_content.substr(pos); std::string::size_type right_pos = rest.find(">"); if (right_pos == std::string::npos) { post_content = prefix; } else { post_content = prefix + rest.substr(right_pos); } } std::vector tokens = hatkirby::split>(post_content, " "); std::vector canonical; for (std::string token : tokens) { std::string canonStr; for (char ch : token) { if (std::isalpha(ch)) { canonStr += std::tolower(ch); } } canonical.push_back(canonStr); } std::vector::iterator imIt = std::find(std::begin(canonical), std::end(canonical), "im"); if (imIt != std::end(canonical)) { imIt++; if (imIt != std::end(canonical)) { verbly::token name; verbly::word firstAdverb = findWordOfType( database, *imIt, verbly::part_of_speech::adverb); if (firstAdverb.isValid()) { std::vector::iterator adjIt = imIt; adjIt++; if (adjIt != std::end(canonical)) { verbly::word secondAdjective = findWordOfType( database, *adjIt, verbly::part_of_speech::adjective); if (secondAdjective.isValid()) { name << firstAdverb; name << secondAdjective; } } } if (name.isEmpty()) { verbly::word firstAdjective = findWordOfType( database, *imIt, verbly::part_of_speech::adjective); if (firstAdjective.isValid()) { name = firstAdjective; } } if ((!name.isEmpty()) && (std::bernoulli_distribution(1.0/10.0)(rng))) { verbly::token action = { "Hi", verbly::token::punctuation(",", verbly::token::capitalize( verbly::token::casing::title_case, name)), "I'm Dad."}; std::string result = "@" + post["account"]["acct"].get() + " " + action.compile(); mastodonpp::parametermap parameters{ {"status", result}, {"in_reply_to_id", post["id"].get()}}; auto answer{connection.post(mastodonpp::API::v1::statuses, parameters)}; if (!answer) { if (answer.curl_error_code == 0) { std::cout << "HTTP status: " << answer.http_status << std::endl; } else { std::cout << "libcurl error " << std::to_string(answer.curl_error_code) << ": " << answer.error_message << std::endl; } } } } } } } } catch (const std::exception&) { // Wait out the rate limit (10 minutes here and 5 below = 15). std::this_thread::sleep_for(std::chrono::minutes(10)); } // We can poll the timeline at most once every five minutes. std::this_thread::sleep_for(std::chrono::minutes(5)); } }