'; file_put_contents( get_log_file(), '[' . $level . '] ' . date('H:i:s') . ' ' . $string . PHP_EOL, FILE_APPEND | LOCK_EX ); } function get_log_file() { return realpath(__DIR__ . '/../pca_logs') . '/pca.log'; //return realpath(__DIR__ . '/../pca_logs') . '/' . date('Y-m-d') . '.log'; } class User { public $user_name; public $name; public $number_posts; public $following; public $follows_bot; public $last_club_notification = ''; private static function clean_api_response($api_reponse) { $data = []; if (array_key_exists('data', $api_reponse)) { $data = $api_reponse['data']; } else { $data = $api_reponse; } return $data; } public function __construct($api_reponse) { $data = self::clean_api_response($api_reponse); $this->user_name = '@' . $data['username']; $this->number_posts = preg_replace('/\s+/', '', $data['counts']['posts']); $this->follows_bot = $data['follows_you']; $this->name = array_key_exists('name', $data) ? $data['name'] : ''; } public static function get_users_from_api_reponse($api_reponse) { $data = self::clean_api_response($api_reponse); $users = []; // Single user if (count(array_filter(array_keys($data), 'is_string')) > 0) { $users[] = new User($data); } else { // Multiple users foreach ($data as $key => $value) { $users[] = new User($value); } } return $users; } public function get_highest_pca($clubs) { $key = $this->get_next_pca_key($clubs) - 1; if ($key == -1) { return []; } return $clubs[$key]; } public function get_next_pca($clubs) { return $clubs[$this->get_next_pca_key($clubs)]; } private function get_next_pca_key($clubs) { if (preg_replace('/\s+/', '', $clubs[0]['post_count']) > $this->number_posts ) { return 0; } foreach ($clubs as $key => $club) { if (preg_replace('/\s+/', '', $club['post_count']) > $this->number_posts ) { return $key; } } } } class API { public $access_token = ''; private static $api_endpoint = 'https://api.pnut.io/v1'; public $max_posttext_length = 256; private static $settings_file_location = __DIR__ . '/..' . '/pca_settings/user_settings.json'; private static $settings = []; private static $default_notification_text = 'Congratulations {user.username}, you are now a member of #{pca.name} {pca.emoji} ({pca.postcount} posts)! Next: {nextpca.emoji} at {nextpca.postcount} posts'; private static $notification_tokens = [ '{user.username}', '{user.name}', '{pca.name}', '{pca.emoji}', '{pca.postcount}', '{nextpca.name}', '{nextpca.emoji}', '{nextpca.postcount}', '{posts_to_pca}', ]; public $me; public function init() { write_pca_log(''); write_pca_log('================================='); write_pca_log(''); $this->max_posttext_length = $this->get_data( 'https://api.pnut.io/v1/sys/config' )['data']['post']['max_length']; $this->me = User::get_users_from_api_reponse( $this->get_data('https://api.pnut.io/v1/users/me') )[0]; write_pca_log('Hi, my name is ' . $this->me->user_name, 'DEBUG'); $sapi_type = php_sapi_name(); $mode = !empty($_SERVER['DOCUMENT_ROOT']) && (substr($sapi_type, 0, 3) == 'cli' || empty($_SERVER['REMOTE_ADDR'])) ? 'Debug' : 'Production'; write_pca_log('I am currently in ' . $mode . ' mode'); if (!file_exists(self::$settings_file_location)) { file_put_contents(self::$settings_file_location, json_encode([])); write_pca_log( 'No user settings file found. Creating empty one at ' . self::$settings_file_location ); } self::$settings = json_decode( file_get_contents(self::$settings_file_location), true ); write_pca_log('User settings file loaded'); } private function get_data( $endpoint, $parameters = [], $method = 'GET', $contenttype = 'application/x-www-form-urlencoded', $force = false ) { write_pca_log('Making request to pnut API at ' . $endpoint); $postdata = $this->build_query_string($parameters); if ($contenttype == 'application/json') { $postdata = json_encode($parameters); } $context_options = [ 'http' => [ 'method' => $method, 'header' => 'Content-type: ' . $contenttype . "\r\n" . 'Authorization: Bearer ' . $this->access_token, 'content' => $postdata, ], ]; $sapi_type = php_sapi_name(); if (!$force && !empty($_SERVER['DOCUMENT_ROOT']) && (substr($sapi_type, 0, 3) == 'cli' || empty($_SERVER['REMOTE_ADDR'])) && $method == 'POST' ) { write_pca_log( 'Running from shell instead of server. Debug mode assumed. Not submitting POST requests to server!', 'DEBUG' ); write_pca_log( 'Would have posted if run on a server: ' . $postdata, 'DEBUG' ); return; } $context = stream_context_create($context_options); $response = @file_get_contents($endpoint, false, $context); if ($response === false) { write_pca_log('Received no or invalid response from server', 'ERROR'); return [ 'meta' => ['code' => '401', 'error_message' => 'Invalid request'], ]; } $resp_dict = json_decode($response, true); write_pca_log( 'Got server response. Meta: ' . json_encode($resp_dict['meta']) ); $response_code = $resp_dict['meta']['code']; // Success if ($response_code >= 200 && $response_code <= 208) { return $resp_dict; } else { write_pca_log( 'Received error-response from server. ' . json_encode($resp_dict['meta']), 'ERROR' ); // die(); } } public function get_messages($clubs) { $num_unread_endpoint = self::$api_endpoint . '/users/me/channels/num_unread/pm'; $num_unread_pms = $this->get_data($num_unread_endpoint)['data']; $messages = []; //Keys: username, values: message-array of messages by the user write_pca_log($num_unread_pms . ' unread PMs'); if ($num_unread_pms > 0) { $channels_endpoint = self::$api_endpoint . '/users/me/channels/subscribed?include_read=0&channel_types=io.pnut.core.pm'; $channels = $this->get_data($channels_endpoint); foreach ($channels['data'] as $channel) { write_pca_log('Channel: ' . $channel['id'], 'DEBUG'); $messages_endpoint = self::$api_endpoint . '/channels/' . $channel['id'] . '/messages?include_deleted=0&include_html=0&include_client=0&include_marker=1'; $unread_messages = $this->get_data($messages_endpoint); $msg = []; $sender = ''; $last_read = $unread_messages['meta']['marker']['last_read_id']; foreach ($unread_messages['data'] as $message) { if ($message['id'] <= $last_read) { //Stop when reaching already read messages write_pca_log( 'Message ' . $message['id'] . 'is <= last read message ' . $last_read . '. Skipping rest of the messages.', 'DEBUG' ); break; } $sender_tmp = $message['user']['username']; if (substr($this->me->user_name, 1) == $sender_tmp) { //Ignore messages sent by the bot write_pca_log( 'Ignoring message ' . $message['id'] . ', because it was sent by myself', 'DEBUG' ); continue; } $sender = $sender_tmp; $message_text = $message['content']['text']; $msg[] = $message['content']['text']; write_pca_log( 'Message from ' . $sender . ': ' . $message_text, 'DEBUG' ); } if ($sender != '') { $messages[$sender] = $msg; } } } return $messages; } public function send_message($user, $message) { $send_message_endpoint = self::$api_endpoint . '/channels/pm/messages?update_marker=1'; $parameters = ['text' => $message, 'destinations' => ['@' . $user]]; $this->get_data( $send_message_endpoint, $parameters, 'POST', 'application/json', $user == 'hutattedonmyarm' ); } public function set_notification_text_template($user_id, $text) { // echo json_encode(self::$settings); self::$settings[$user_id] = $text; $this->save_settings(); } public function get_notification_text_template($user_id) { $notification_text = API::$default_notification_text; if (array_key_exists($user_id, self::$settings)) { write_pca_log( $user_id . ' has custom settings: ' . json_encode(self::$settings[$user_id]) ); $notification_text = self::$settings[$user_id]; } else { write_pca_log($user_id . ' does not have custom settings!'); } return $notification_text; } private function build_notification_text( $user, $clubs, $current_pca, $next_pca ) { $notification_text = $this->get_notification_text_template( $user->user_name ); $token_values = [ $user->user_name, $user->name, $current_pca['pca'], $current_pca['emoji'], $current_pca['post_count'], $next_pca['pca'], $next_pca['emoji'], $next_pca['post_count'], $next_pca['post_count'] - $user->number_posts, ]; if (array_key_exists($user->user_name, self::$settings)) { write_pca_log( $user->user_name . ' has custom settings: ' . json_encode(self::$settings[$user->username]) ); $notification_text = self::$settings[$user->username]; } else { write_pca_log($user->user_name . ' does not have custom settings!'); } foreach (self::$notification_tokens as $index => $token) { $notification_text = str_replace( $token, $token_values[$index], $notification_text ); } return $notification_text; } public function reset_notification_text_for_user($user_id) { if (array_key_exists($user_id, self::$settings)) { unset(self::$settings[$user_id]); $this->save_settings(); } } private function save_settings() { file_put_contents( self::$settings_file_location, json_encode(self::$settings) ); } public function write_post($posttext, $reply_to = -1) { $post_endpoint = self::$api_endpoint . '/posts'; $txt = mb_strimwidth($posttext, 0, $this->max_posttext_length, ''); // $txt = $posttext; $parameters = ['text' => $txt]; if ($reply_to != -1) { $parameters['reply_to'] = $reply_to; } write_pca_log( "Writing post: '" . $txt . "' => " . json_encode($parameters) ); $this->get_data($post_endpoint, $parameters, 'POST'); } public function get_user($user_id) { if ($user_id[0] != '@') { $user_id = '@' . $user_id; } $user_endpoint = self::$api_endpoint . '/users/' . $user_id; return new User($this->get_data($user_endpoint)); } public function get_bot_followers() { $before_id = null; $users = []; do { write_pca_log('Getting followers before id: ' . $before_id); $followers_endpoint = self::$api_endpoint . '/users/me/followers'; if ($before_id != null) { $followers_endpoint .= '?before_id=' . $before_id; } $followers_data = $this->get_data( $followers_endpoint, [ 'before_id' => $before_id, ] ); $before_id = $followers_data['meta']['min_id']; $users = array_merge( $users, User::get_users_from_api_reponse($followers_data) ); } while ($followers_data['meta']['more'] == '1'); return $users; } function build_query_string($array) { foreach ($array as $k => &$v) { if ($v === true) { $v = '1'; } elseif ($v === false) { $v = '0'; } unset($v); } return http_build_query($array); } public function notify_user($user, $clubs) { // Get current PCA $current_pca_dict = $user->get_highest_pca($clubs); $current_pca = ''; if (array_key_exists('pca', $current_pca_dict)) { $current_pca = $current_pca_dict['pca']; } else { // If key doesn't exist, user hasn't reached a club yet return null; } $last_notification = $user->last_club_notification; $next_pca = $user->get_next_pca($clubs); $next = [ 'club' => $next_pca, 'posts_until' => $next_pca['post_count'] - $user->number_posts ]; if ($current_pca != $last_notification) { // Haven't notified about the current club yet // Stitch together the post itself $this->build_notification_text( $user, $clubs, $current_pca_dict, $next_pca ); $text_components = []; $posttext = 'Congratulations ' . $user->user_name . ', you are now a member of #' . preg_replace('/\s+/', '', $current_pca) . ' ' . $current_pca_dict['emoji']; $text_components[] = ' (' . $current_pca_dict['post_count'] . '+ posts)!'; $text_components[] = ' Next: ' . $next_pca['emoji'] . ' at ' . $next_pca['post_count'] . ' posts'; foreach ($text_components as $component) { $available_length = $this->max_posttext_length - strlen($component); if (strlen($posttext) < $available_length) { $posttext .= $component; } } /* * Steps to write post as a reply to the post which made a user enter a new club: * 1) Get number of user posts (we already have that info) * 2) Subtract pca post count value from number posts. This will give us an offset * 3) Get their posts with ?count=OFFSET&include_html=0&include_counts=0&include_client=0 * 4) Check if counts match. If not, get more of their posts with before_id set to the relevant ID * 5) Repeat 4 until we have the correct post * 6) Reply to it */ $pca_offset = $user->number_posts - $current_pca_dict['post_count'] + 1; $before_id = -1; $loop_counts = 10; do { $ep = self::$api_endpoint . '/users/' . $user->user_name . '/posts?count=' . $pca_offset . '&include_html=0&include_counts=0&include_client=0&include_deleted=1'; // write_pca_log($ep, "DEBUG"); $response = $this->get_data($ep, []); $pca_offset -= count($response['data']); $before_id = $response['meta']['min_id']; write_pca_log( 'Received ' . count($response['data']) . ' posts', 'DEBUG' ); $loop_counts--; // write_pca_log("Remaining offset: ".$pca_offset); } while ($pca_offset > 0 && $loop_counts > 0); $reply_to = $response['data'][count($response['data']) - 1]['id']; // reached mac loop iterations if (!isset($reply_to) || $reply_to == 0 || $loop_counts <= 0) { $reply_to = -1; } $del_ar = $response['data'][count($response['data']) - 1]; $deleted = array_key_exists('is_deleted', $del_ar) && $del_ar['is_deleted'] == 'true'; $log_string = 'Post that made them reach ' . preg_replace('/\s+/', '', $current_pca) . ': ' . $reply_to . '. Deleted?: '; $log_string .= $deleted ? 'yes' : 'no'; write_pca_log($log_string); $this->write_post($posttext, $reply_to); $user->last_club_notification = $current_pca; write_pca_log($posttext); write_pca_log($log_string); $now = \DateTime::createFromFormat('U.u', microtime(true)); $recent_changes_dict = [ 'date' => $response['data'][count($response['data']) - 1]['created_at'], 'user' => $user->user_name, 'pca' => $current_pca, 'post_id' => $reply_to, ]; $history_file = 'history.json'; $inp = file_get_contents($history_file); $tempArray = json_decode($inp, true); if ($tempArray == null) { $tempArray = []; } array_push($tempArray, $recent_changes_dict); file_put_contents($history_file, json_encode($tempArray)); } else { // Already notified, nothing to do write_pca_log( $user->user_name . ' has already been notified for reaching ' . $current_pca ); } return $next; } } // Get all clubs write_pca_log(''); write_pca_log(''); write_pca_log(''); write_pca_log(''); write_pca_log('Log file: ' . get_log_file()); $clubs = json_decode(file_get_contents('https://pca.phlaym.net/pca.php'), true); write_pca_log('Found clubs: ' . implode(', ', array_column($clubs, 'pca'))); // $last_notification_file = '../last_notification.json'; $last_notification_file_dir = realpath(__DIR__ . '/../pca_settings'); $last_notification_file_name = '/last_notification.json'; $last_notification_file = $last_notification_file_dir . $last_notification_file_name; write_pca_log('Last notification file: ' . $last_notification_file); $last_notification_dict = []; if (file_exists($last_notification_file)) { $last_notification_dict = json_decode( file_get_contents($last_notification_file), true ); write_pca_log('Loaded last notification info from file'); } else { write_pca_log('Last notification info file could not be found'); } // Get users $access_token = '-1'; $token_file = realpath(__DIR__ . '/../pca_settings/access_token'); if ($token_file !== false) { $access_token = file_get_contents($token_file); write_pca_log('Using saved authentication'); if ($access_token == false) { write_pca_log('Re-authenticating'); header('Location: http://pca.phlaym.net/pnutauth.php'); } } else { write_pca_log('Re-authenticating'); header('Location: http://pca.phlaym.net/pnutauth.php'); } $api = new API(); $api->access_token = $access_token; $api->init(); $messages = $api->get_messages($clubs); /* * Message command syntax: * "Help" or "?" => prints help * "Get notification text" => replies with notification text * "Reset notification text" => resets to default * "Set notification text" followed by a space and then the text. Replies with sample text. Available tokens see class var */ foreach ($messages as $sender => $message) { $sender_obj = $api->get_user($sender); foreach ($message as $msg) { if (mb_strtolower($msg) == '?' || mb_strtolower($msg) == 'help') { $helptext = 'Welcome to ' . $api->me->user_name . "!\n"; $helptext .= "The following commands are available: \n"; $helptext .= "- 'Get notification text', which prints the current notification text for you\n"; $helptext .= "- 'Set notification text', followed by a space and a custom notification text\n"; $helptext .= "- 'Reset notification text', resets the notification text to the default\n"; $helptext .= "\n"; $helptext .= " Available tokens for the custom notification text\n"; $helptext .= " - '{user.username}', your username (including the @)\n"; $helptext .= " - '{user.name}', your real name\n"; $helptext .= " - '{pca.name}', the name of the PCA you achieved, excluding the #\n"; $helptext .= " - '{pca.emoji}', the emoji of the PCA you achieved\n"; $helptext .= " - '{pca.postcount}', the number of posts you need to achieve the PCA\n"; $helptext .= " - '{nextpca.name}', the name of the next PCA\n"; $helptext .= " - '{nextpca.emoji}', the emoji of the next PCA\n"; $helptext .= " - '{nextpca.postcount}', the total number of posts needed for the next PCA\n"; $helptext .= " - '{posts_to_pca}', the number of posts missing to the next PCA\n"; $api->send_message($sender, $helptext); //Print help text } elseif (substr(mb_strtolower($msg), 0, 21) === 'get notification text') { //Print current notification text for user //TODO: build example notification text. Create user object and do 'get_highest_pca($clubs)' and 'get_next_pca($clubs)' $api->send_message( $sender, "You current notification text is: \n" . $api->get_notification_text_template($sender) ); } elseif (substr(mb_strtolower($msg), 0, 23) === 'reset notification text' ) { //reset notification text for user to default $api->reset_notification_text_for_user($sender); $api->send_message( $sender, "Your notification text has been reset back to the default: \n" . $api->get_notification_text_template($sender) ); } elseif (substr(mb_strtolower($msg), 0, 21) === 'set notification text') { $notification_text = substr($msg, 22); $api->set_notification_text_template($sender, $notification_text); $api->send_message( $sender, "Your notification text has been set to:\n" . $api->get_notification_text_template($sender) ); } else { write_pca_log( "Unknown command: '" . $msg . "' from " . $sender, 'DEBUG' ); $api->send_message( $sender, "I'm sorry, I don't recognize that. Try PMing me 'help'" ); } } } $followers = $api->get_bot_followers(); // Check and notify. Also build notification dict $next_club_infos = []; foreach ($followers as $user) { if ($last_notification_dict != null && array_key_exists($user->user_name, $last_notification_dict) ) { write_pca_log( $user->user_name . ' has been notified in the past. Last time when reaching: ' . $last_notification_dict[$user->user_name] ); $user->last_club_notification = $last_notification_dict[$user->user_name]; } else { write_pca_log($user->user_name . ' has never been notified in the past'); } $next_club_info = $api->notify_user($user, $clubs); if ($next_club_info !== null) { $next_club_info['user'] = $user->user_name; $next_club_infos[] = $next_club_info; } if ($user->last_club_notification != '') { write_pca_log( 'Saving last notification ' . $user->last_club_notification . ' for user ' . $user->user_name ); $last_notification_dict[$user->user_name] = $user->last_club_notification; } else { write_pca_log( $user->user_name . ' will not be saved, as they have not reached a club yet' ); } } usort( $next_club_infos, fn($a, $b) => intval($a['posts_until']) - intval($b['posts_until']) ); foreach ($next_club_infos as $idx => $value) { if ($idx >= 10) { break; } write_pca_log( 'Next club will be: ' . $value['club']['pca'] . ' for user ' . $value['user'] . ' in ' . $value['posts_until'] . ' posts' ); } // Save changes write_pca_log('Saving updated notification info'); $f = fopen($last_notification_file, 'w'); fwrite($f, json_encode($last_notification_dict)); fclose($f); ?>