<?php namespace Phlaym\Roastmonday; use APnutI\APnutI; use Phlaym\Roastmonday\DB\DB; use APnutI\Exceptions\NotFoundException; class Roastmonday extends APnutI { protected static $new_avatar_keywords = ['avatar', 'image', 'picture']; protected DB $db; protected string $pics_root; protected string $temp_pics_root; public function __construct($config, $pics_root = "", $app_name = 'Roastmonday') { parent::__construct( $config['client_secret'], $config['client_id'], $config['scope'], $app_name, $config['redirect_url'], __DIR__ . '/../logs/roastmonday.log', 'DEBUG' ); $this->logger->info("App: {$this->app_name}"); $this->pics_root = $pics_root . 'pics/'; $this->temp_pics_root = '../temp_avatars/'; if (!is_dir($this->pics_root . $this->temp_pics_root)) { mkdir($this->pics_root . $this->temp_pics_root); } $this->logger->info('Pics root ', ['path' => $this->pics_root]); $this->logger->info('Pics root realpath', ['path' => realpath($this->pics_root)]); $this->logger->info('Temp pics root', ['path' => $this->pics_root . $this->temp_pics_root]); $this->logger->info('Temp pics root realpath', ['path' => realpath($this->pics_root . $this->temp_pics_root)]); $db_logger = null; if ($this->logger instanceof \Monolog\Logger) { $db_logger = ($this->logger)->withName('RM_Database'); $this->logger->info("Setting up DB Logger"); } else { $this->logger->info("Logger is not a Monolog logger, can not clone for DB Logger"); } $this->db = new DB( $config['db_servername'], $config['db_database'], $config['db_username'], $config['db_password'], logger: $db_logger ); } protected function getExtension($ct) { $ext = 'jpg'; switch ($ct) { case 'image/png': $ext = 'png'; break; case 'image/bmp': $ext = 'bmp'; break; case 'image/gif': $ext = 'gif'; break; case 'image/jpeg': default: $ext = 'jpg'; break; } return $ext; } protected function downloadPicture($link, $target_folder = '') { $this->logger->info("Downloading picture", ['url' => $link]); $img_header = get_headers($link, 1); if (strpos($img_header[0], "200") === false && strpos($img_header[0], "OK")) { $this->logger->error("Error fetching avatar header", ['header' => $img_header]); return null; } $ext = $this->getExtension($img_header['Content-Type']); $exp = explode('/', explode('?', $link)[0]); $img_name = array_pop($exp) . '.' . $ext; $this->savePicture($link, $target_folder, $img_name); return $img_name; } protected function savePicture($url, $target_folder, $filename) { $pics_folder = $this->pics_root . $target_folder; $this->logger->debug("Checking pics directory", ['path' => $pics_folder]); if (!is_dir($pics_folder)) { mkdir($pics_folder); } $d = $pics_folder; if (!(substr($d, -1) === '/')) { $d .= '/'; } $d .= $filename; if (!file_exists($d)) { $this->logger->info('Saving avatar to ', ['path' => $d]); $av = file_get_contents($url); $fp = fopen($d, "w"); fwrite($fp, $av); fclose($fp); } else { $this->logger->info('already exists. Skipping.', ['path' => $d]); } } protected function getThemeMondaysFromWiki() { $m = []; $html = file_get_contents('https://wiki.pnut.io/ThemeMonday'); if ($html === false) { $this->logger->error('Error connecting to pnut wiki'); return []; } $doc = new \DOMDocument(); $doc->loadHTML($html); if ($doc === false) { $this->logger->error('Error loading HTML from pnut wiki'); return []; } $past_themes_element = $doc->getElementById('Past_Themes_on_Pnut'); $past_themes_list = []; try { $past_themes_list = $past_themes_element->parentNode->nextSibling->nextSibling; } catch (\Exception $e) { $this->logger->error('Error parsing wiki', ['exception' => $e->getMessage()]); return []; } foreach ($past_themes_list->childNodes as $child) { if ($child->nodeName === 'li') { $tag = null; $date = null; foreach ($child->childNodes as $list_entry) { if ($list_entry->nodeName === 'a' && $list_entry->nodeValue !== null) { $date = \DateTime::createFromFormat('Y F d', $list_entry->nodeValue); if ($date === false) { $date = null; } } elseif ($list_entry->nodeName === '#text' && $list_entry->nodeValue !== null) { $arr = explode('#', str_replace(': ', '', $list_entry->nodeValue)); if (count($arr) > 1) { $tag = '#' . $arr[1]; } } if ($tag !== null && $date !== null) { $tag = trim($tag); $this->logger->info( 'Found ThemeMonday in wiki', ['tag' => $tag, 'date' => $date] ); $m[] = [ 'date' => new \DateTime($date->format('Y-m-d')), 'tag' => $tag ]; } } } } return $m; } public function getThemeMonday($id) { $t = $this->db->getTheme($id); return $t; } protected function getThemeMondaysFromAccountPost() { $this->logger->info('Parsing @ThemeMonday polls'); $m = []; $p = []; try { $p = $this->getPollsFromUser(616); } catch (\Exception $e) { $this->logger->error('Error reading @ThemeMonday polls', ['exception' => $e->getMessage()]); return []; } $p = array_filter($p, function ($e) { return stripos($e->prompt, '#thememonday') !== false; }); $this->logger->info('Found polls count', ['count' => count($p)]); if (count($p) === 0) { return []; } foreach ($p as $poll) { $tag = $poll->getMostVotedOption(); if (count($tag) !== 1) { $this->logger->info('Skipping tag, because it was a tie', ['tag' => $tag]); continue; } $date = Roastmonday::getMondayAfterPoll($poll); $tagtext = $tag[0]->text; $this->logger->info( 'Found ThemeMonday', ['tag' => $tagtext, 'date' => $date] ); $m[] = [ 'date' => new \DateTime($date->format('Y-m-d')), 'tag' => $tagtext ]; } return $m; } protected static function getMondayAfterPoll($poll) { $d = $poll->closed_at; $days_to_add = (7 - ($d->format('w') - 1)) % 7; return $d->modify('+' . $days_to_add . ' days'); } protected function savePictureForPost($post, $theme_id) { $this->logger->info('Checking post for theme', ['post' => $post->id, 'theme' => $theme_id]); if (!empty($post->user) && !empty($post->user->avatar_image)) { $this->logger->info( 'Found new avatar on post', ['post' => $post->id, 'user' => $post->user->username] ); $filename = $this->downloadPicture($post->user->avatar_image->link, $theme_id); if ($filename !== null) { $realname = null; if (isset($post->user, $post->user->name)) { $realname = $post->user->name; } $text = ""; if (isset($post->content)) { if (isset($post->content->html)) { $text = $post->content->html; } elseif (isset($post->content->text)) { $text = $post->content->text; } } $this->logger->info( 'Saving avatar to database', ['theme' => $theme_id, 'user' => $post->user->username] ); try { $status = $this->db->saveAvatar( $theme_id, $post->user->username, $realname, $post->id, $text, $post->created_at->getTimestamp(), $filename ); switch ($status) { case DB::$TRIED_INSERT_DUPLICATE: $this->logger->info('Error, tried to insert a duplicate avatar'); break; case DB::$SKIPPED_DUPLICATE: $this->logger->info('Skipped, duplicate avatar'); break; case DB::$SUCCESS: $this->logger->info('Successfully inserted avatar'); break; default: $this->logger->info('Unknown return code', ['status' => $status]); break; } } catch (\Exception $e) { $this->logger->error( 'Error inserting avatar into database', ['exception' => $e->getMessage()] ); } return $filename; } $this->logger->warning( "Cannot save picture for post. Doesn't have a user or user doesn't have an avatar", ['post' => $post->id] ); } } public function getPost($post_id, $args = []) { $post = parent::getPost($post_id, $args); if (array_key_exists('avatarWidth', $args)) { $a = parent::getAvatar($post->user->id, ['w' => $args['avatarWidth']]); $this->logger->debug('Resized avatar', ['avatar' => $a]); $post->user->avatar_image->link = $a; } return $post; } protected function removeDuplicateThemes($arr) { $tmp = []; foreach ($arr as $e) { if (!in_array($e, $tmp)) { $f = false; foreach ($tmp as $t) { if (strtolower($t['tag']) === strtolower($e['tag'])) { $f = true; break; } } if (!$f) { $tmp[] = $e; } } } return $tmp; } public function getThemeMondays() { $themes = $this->db->listThemes(); $this->logger->debug('Themes', ['themes' => $themes]); return $themes; } public function getPicturesForTheme($id) { $this->logger->debug('Avatars for theme', ['theme' => $id]); $pics_folder = $this->pics_root . $id . '/'; $pics = []; if (!is_dir($pics_folder)) { $this->logger->warning('Pics folder ist not a directory', ['path' => $pics_folder]); return []; } $avatars = $this->db->getAvatarsForTheme($id); foreach ($avatars as $avatar) { $avatar['file'] = $pics_folder . $avatar['file']; $pics[] = $avatar; } return $pics; } public function findThemeMondays() { $this->logger->info('Searching for theme mondays'); $m = $this->getThemeMondaysFromAccountPost(); $m = array_filter($m, function ($e) { return !empty($e); }); $m = $this->removeDuplicateThemes( array_merge($m, $this->getThemeMondaysFromWiki()) ); usort($m, function ($a, $b) { if ($a['date'] == $b['date']) { return 0; } return $a['date'] > $b['date'] ? -1 : 1; }); $this->logger->info('Found theme mondays', ['mondays' => $m]); return $m; } public function savePicturesForTheme($theme) { $tag = preg_replace('/^#/', '', $theme['tag']); $tag = preg_replace('/ .*$/', '', $tag); $posts = []; $this->logger->info('Searching pictures for', ['tag' => $tag]); foreach (Roastmonday::$new_avatar_keywords as $keyword) { $query = [ 'tags' => $tag, 'q' => $keyword, 'include_deleted' => false, 'include_client' => false, 'include_counts' => false, 'include_html' => false, ]; $p = $this->searchPosts($query); $this->logger->info('Found posts', ['count' => count($p)]); foreach ($p as $post) { $this->savePictureForPost($post, $theme['id']); if (!in_array($post, $posts)) { $posts[] = $post; } } } } public function addTheme($tag, $date) { $tag = preg_replace('/^#/', '', $tag); $tag = preg_replace('/ .*$/', '', $tag); $this->logger->info('Adding: tag to database', ['tag' => $tag]); $id = $this->db->addTheme($tag, $date->format('Y-m-d')); if ($id !== -1) { $pics_folder = $this->pics_root . $id . '/'; if (!is_dir($pics_folder)) { mkdir($pics_folder); } } return $id; } public function addAvatar( $theme, $should_post, $avatar, $avatar_duration, $posttext = null, $overwrite_if_exist = false ) { $this->logger->info('Adding temporary avatar to theme', ['theme' => $theme, 'duration' => $avatar_duration]); switch ($avatar['error']) { case UPLOAD_ERR_OK: # ok break; case UPLOAD_ERR_NO_FILE: return ['error' => ['message' => 'No avatar has been uploaded']]; case UPLOAD_ERR_FORM_SIZE: return ['error' => ['message' => 'The uploaded avatar\'s file size is too big']]; default: return ['error' => ['message' => 'An unknown error has occured while uploading the avatar']]; } #1. Save current user avatar $a = self::getAvatar('me'); $this->logger->info('Current avatar', ['avatar' => $a]); $original_avatar = $this->downloadPicture($a, $this->temp_pics_root); if (empty($original_avatar)) { return ['error' => ['message' => 'Could not download original avatar']]; } #2. Save current avatar filename + enddate to DB $current_user = null; try { $current_user = $this->getAuthorizedUser(); if (empty($current_user)) { return ['error' => ['message' => 'Could not fetch the authorized user']]; } $this->logger->info('Current user', ['username' => $current_user->username, 'id' => $current_user->id]); $avatar_duration = min($avatar_duration, 1); $d = new \DateTime(); $d->add(new \DateInterval("P{$avatar_duration}D")); $this->db->addTempAvatar($current_user->id, $d->getTimestamp(), $original_avatar, $this->access_token); } catch (\Exception $e) { return ['error' => ['message' => 'Could not save original avatar: ' . $e->getMessage()]]; } #3. Upload new avatar $astr = print_r($avatar, true); $this->logger->info("Avatar: {$astr}"); try { $current_user = $this->updateAvatarFromUploaded($avatar); } catch (\Exception $e) { return ['error' => ['message' => 'Could not update to the theme avatar: ' . $e->getMessage()]]; } #4. Write post? $this->logger->info('Post about theme avatar? ' . ($should_post ? 'Yes' : 'No') . ': ' . $should_post); if (!$should_post) { try { $this->logger->info('Adding post-less avatar to gallery'); $this->authenticateServerToken(); $response = $this->manuallyAddAvatar( $theme, 0, $avatar, $current_user, overwrite_if_exist: $overwrite_if_exist ); if (!empty($response['error'])) { $this->logger->warning('Error adding to gallery'); $rs = print_r($response, true); $this->logger->warning($rs); return [ 'success' => true, 'warn' => 'Your avatar has been updated, but an error occured adding it to the gallery: ' . $response['error']['message'] ]; } else { $this->logger->info('Successfully added to gallery'); } } catch (\Exception $e) { $this->logger->error($e->getMessage()); $this->logger->error($e->getTraceAsString()); return [ 'success' => true, 'warn' => 'Your avatar has been updated, but an error occured adding it to the gallery: ' . $e->getMessage() ]; } return ['success' => true]; } if (empty($posttext)) { return [ 'success' => true, 'warn' => 'You selected to post about new avatar,' . '<br />but your posttext is empty.<br />' . 'Your avatar has been updated, but no post has been created' ]; } $this->logger->info('Posting about theme avatar'); $post = null; try { $post = $this->createPost($posttext, is_nsfw: false, auto_crop: true); } catch (\Exception $e) { $this->logger->error('Could not create post', ['exception' => $e]); return [ 'success' => true, 'warn' => 'Your avatar has been updated, but an error occured creating the post:<br />' . $e->getMessage() ]; } $this->logger->info('Post created successfully'); #5. Add to theme DB try { $this->authenticateServerToken(); $response = $this->manuallyAddAvatar($theme, $post->id, null, overwrite_if_exist: $overwrite_if_exist); $this->logger->info('Successfully added to gallery'); } catch (\Exception $e) { $this->logger->error('Could not authenticateServerToken', ['exception' => $e]); return [ 'success' => true, 'postID' => $post->id, 'warn' => 'Your avatar has been updated, and your post created,' . '<br />but an error occured adding it to the gallery: ' . $e->getMessage() ]; } return ['success' => true, 'postID' => $post->id, 'newGalleryAvatar' => $response]; } public function manuallyAddAvatar($theme, $post_id, $avatar, $user = null, $overwrite_if_exist = false) { $this->logger->info( "Manually adding avatar to theme from post", ['theme' => $theme, 'post_id' => $post_id] ); $resp = []; $date = new \DateTime(); $post = null; try { if (empty($user)) { $post = $this->getPost($post_id); $user = $post->user; $date = $post->created_at; if (empty($user)) { $this->logger->error( 'Error fetching avatar from post. Post has no creator.', ['post_id' => $post_id] ); $resp['error'] = [ 'code' => static::$ERROR_FETCHING_CREATOR_FIELD, 'message' => 'Error fetching avatar. Post has no creator.' ]; return $resp; } } if (empty($user->avatar_image) || empty($user->avatar_image->link)) { $this->logger->error( 'Error fetching avatar from post. Post creator has no avatar.', ['post_id' => $post_id] ); $resp['error'] = [ 'code' => static::$ERROR_FETCHING_CREATOR_FIELD, 'message' => 'Error fetching avatar. Post creator has no avatar.' ]; return $resp; } $astr = print_r($avatar, true); $this->logger->debug('Manually adding avatar', ['avatar' => $astr]); if (!empty($avatar) && (empty($avatar['error']) || $avatar['error'] === 0)) { $this->logger->info('Manually added post has an avatar attached', ['post_id' => $post_id]); $ext = $this->getExtension($avatar['type']); $target_file_name_without_theme = time() . '_' . $post_id . '.' . $ext; $target_file_name = $theme . '/' . $target_file_name_without_theme; $target_file = $this->pics_root . $target_file_name; if (move_uploaded_file($avatar["tmp_name"], $target_file)) { $resp['img'] = 'pics/' . $target_file_name; } else { $this->logger->error( 'Error saving uploaded avatar from post. Post has no creator', ['post_id' => $post_id] ); $resp['error'] = [ 'code' => static::$ERROR_UNKNOWN_SERVER_ERROR, 'message' => 'Uploaded file could not be moved' ]; return $resp; } $this->logger->info('Saved manually uploaded file. Adding to database', ['path' => $target_file]); $realname = null; $username = ''; if (isset($user)) { if (isset($user->name)) { $realname = $user->name; } $username = $user->username; $resp['presence'] = $user->getPresenceInt(); } $posttext = empty($post) ? '' : $post->getText(); $status = $this->db->saveAvatar( $theme, $username, $realname, $post_id, $posttext, $date->getTimestamp(), $target_file_name_without_theme, $overwrite_if_exist ); $this->logger->info( "Saved manually uploaded file and added to database", ['path' => $target_file, 'db_status_code' => $status, 'db_status' => DB::getResultName($status)] ); } else { $this->logger->info( "Manually added post doesn't have an avatar attached. fetching from user object", ['post_id' => $post_id] ); $filename = $this->savePictureForPost($post, $theme); if (empty($filename)) { $this->logger->error("Error fetching avatar from post #{$post_id}."); $resp['error'] = [ 'code' => static::$ERROR_UNKNOWN_SERVER_ERROR, 'message' => "Error fetching avatar from post #{$post_id}." ]; return $resp; } $resp['img'] = 'pics/' . $theme . '/' . $filename; $this->logger->info("Saved downloaded avatar to {$resp['img']}"); } $resp['presence'] = -1; $realname = null; $username = ''; if (isset($user)) { if (isset($user->name)) { $realname = $user->name; } $username = $user->username; $resp['presence'] = $user->getPresenceInt(); } $resp['user'] = '@' . $username; $text = ""; $text = empty($post) ? '' : $post->getText(); $resp['success'] = true; $resp['realname'] = $realname; $resp['posttext'] = $text; $resp['postid'] = $post_id; $resp['timestamp'] = $date->getTimestamp(); $this->logger->info( 'Successfully added manually uploaded avatar for user to theme', ['user' => $resp['user'], 'theme' => $theme] ); return $resp; } catch (NotFoundException $nfe) { $this->logger->error( 'Error adding manually uploaded avatar for theme. Post could not be found', ['post_id' => $post_id, 'theme' => $theme, 'exception' => $nfe] ); $resp['error'] = ['code' => static::$ERROR_NOT_FOUND, 'message' => 'Post could not be found']; return $resp; } catch (\Exception $e) { $this->logger->error( 'Error adding manually uploaded avatar for theme', ['theme' => $theme, 'exception' => $e] ); $resp['error'] = ['code' => static::$ERROR_UNKNOWN_SERVER_ERROR, 'message' => $e->getMessage()]; return $resp; } } public function resetOriginalAvatars() { $this->logger->info('Get outdated avatars'); $to_reset = $this->db->getOutdatedThemeAvatars(); $this->logger->info('Found outdated avatars', ['count' => count($to_reset)]); foreach ($to_reset as $value) { $user_id = $value['user_id']; $p = $this->pics_root . $this->temp_pics_root . $value['original_avatar']; $avatar_path = realpath($p); if ($avatar_path === false) { $this->logger->error( 'Avatar path for user does not exist!', ['path' => $p, 'user_id' => $user_id] ); } $this->logger->info( 'Resetting avatar for user to original', ['path' => $avatar_path, 'user_id' => $user_id] ); $this->access_token = $value['auth_token']; try { $this->updateAvatar($avatar_path); } catch (\Exception $e) { $this->logger->error('Error resetting user avatar', ['exception' => $e]); return; } $this->logger->info( 'Resetted avatar for user to original', ['path' => $avatar_path, 'user_id' => $user_id] ); $this->logger->info( 'Deleting original avatar for user', ['path' => $avatar_path, 'user_id' => $user_id] ); $res = unlink($avatar_path); if (!$res) { $this->logger->error('Failed to delete avatar at', ['path' => $avatar_path]); return; } $this->logger->info( 'Deleted original avatar for user', ['path' => $avatar_path, 'user_id' => $user_id] ); try { $this->db->removeOutdatedThemeAvatars($user_id); } catch (\Exception $e) { $this->logger->error('Error resetting user avatar', ['exception' => $e]); return; } $this->logger->info('Avatar for user is back to its original value!', ['user_id' => $user_id]); } } }