Condensed methods
[EroTweet.git] / lib / picturebot.php
blobf19c9880fd94315e4c36c69b56311b127b7252da
1 <?php
2 require_once('twitteroauth.php');
3 require_once('logger.php');
5 //runs every 15 minutes, mirroring & attaching images might take a while
6 set_time_limit(15 * 60);
8 class PictureBot {
10 private $sUsername; //username we will be tweeting from
11 private $sSettingsFile; //settings file to cache file list and store postcount
12 private $sPictureFolder; //folder with images
13 private $aPictureIndex; //cached index of pictures
14 private $sMediaId; //media id of uploaded picture
15 private $iMaxIndexAge; //max age of picture index
17 private $sLogFile; //where to log stuff
18 private $iLogLevel = 3; //increase for debugging
20 private $aTweetSettings; //tweet format settings
22 public function __construct($aArgs) {
24 //connect to twitter
25 $this->oTwitter = new TwitterOAuth(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET);
26 $this->oTwitter->host = "https://api.twitter.com/1.1/";
28 //make output visible in browser
29 if (!empty($_SERVER['HTTP_HOST'])) {
30 echo '<pre>';
33 //load args
34 $this->parseArgs($aArgs);
36 define('DS', DIRECTORY_SEPARATOR);
39 private function parseArgs($aArgs) {
41 $this->sUsername = (!empty($aArgs['sUsername']) ? $aArgs['sUsername'] : '');
42 $this->bReplyToCmds = (!empty($aArgs['bReplyToCmds']) ? $aArgs['bReplyToCmds'] : FALSE);
43 $this->sSettingsFile = (!empty($aArgs['sSettingsFile']) ? $aArgs['sSettingsFile'] : strtolower($this->sUsername) . '.json');
45 $this->sPictureFolder = (!empty($aArgs['sPictureFolder']) ? $aArgs['sPictureFolder'] : '.');
46 $this->iMaxIndexAge = (!empty($aArgs['iMaxIndexAge']) ? $aArgs['iMaxIndexAge'] : 3600 * 24);
48 //stuff to determine what we're tweeting
49 $this->aTweetSettings = array(
50 'sFormat' => (isset($aArgs['sTweetFormat']) ? $aArgs['sTweetFormat'] : ''),
51 'bPostOnlyOnce' => (isset($aArgs['bPostOnlyOnce']) ? $aArgs['bPostOnlyOnce'] : FALSE),
54 $this->sLogFile = (!empty($aArgs['sLogFile']) ? $aArgs['sLogFile'] : strtolower($this->sUsername) . '.log');
56 if ($this->sLogFile == '.log') {
57 $this->sLogFile = pathinfo($_SERVER['SCRIPT_FILENAME'], PATHINFO_FILENAME) . '.log';
61 public function run() {
63 //verify current twitter user is correct
64 if ($this->getIdentity()) {
66 //build picture index
67 $this->getIndex();
69 //fetch random file
70 if ($aFile = $this->getFile()) {
72 //upload picture
73 if ($this->uploadPicture($aFile['filepath'])) {
75 //format and post message
76 if ($this->postMessage($aFile)) {
78 $this->halt('Done.');
81 $this->updatePostCount($aFile);
87 private function getIdentity() {
89 echo "Fetching identify..\n";
91 if (!$this->sUsername) {
92 $this->logger(2, 'No username');
93 $this->halt('- No username! Set username when calling constructor.');
94 return FALSE;
97 $oUser = $this->oTwitter->get('account/verify_credentials', array('include_entities' => FALSE, 'skip_status' => TRUE));
99 if (is_object($oUser) && !empty($oUser->screen_name)) {
100 if ($oUser->screen_name == $this->sUsername) {
101 printf("- Allowed: @%s, continuing.\n\n", $oUser->screen_name);
102 } else {
103 $this->logger(2, sprintf('Authenticated username was unexpected: %s (expected: %s)', $oUser->screen_name, $this->sUsername));
104 $this->halt(sprintf('- Not alowed: @%s (expected: %s), halting.', $oUser->screen_name, $this->sUsername));
105 return FALSE;
107 } else {
108 $this->logger(2, sprintf('Twitter API call failed: GET account/verify_credentials (%s)', $oUser->errors[0]->message));
109 $this->halt(sprintf('- Call failed, halting. (%s)', $oUser->errors[0]->message));
110 return FALSE;
113 return TRUE;
116 private function getIndex() {
118 //read file from disk if present from prev run
119 if (is_file(MYPATH . DS . $this->sSettingsFile) && filesize(MYPATH . DS . $this->sSettingsFile) > 0 && filemtime(MYPATH . DS . $this->sSettingsFile) + $this->iMaxIndexAge < time()) {
121 $this->aPictureIndex = json_decode(file_get_contents(MYPATH . DS . $this->sSettingsFile), TRUE);
123 return TRUE;
125 } else {
127 //get list of all files
128 $aFileList = $this->recursiveScan($this->sPictureFolder);
129 if ($aFileList) {
130 natcasesort($aFileList);
132 //convert list into keys of array with postcount
133 $this->aPictureIndex = array();
134 foreach ($aFileList as $sFile) {
135 $this->aPictureIndex[utf8_encode($sFile)] = 0;
137 unset($this->aPictureIndex['.']);
138 unset($this->aPictureIndex['..']);
140 //merge with existing index
141 if (is_file(MYPATH . DS . $this->sSettingsFile) && filesize(MYPATH . DS . $this->sSettingsFile) > 0) {
143 $aOldPictureIndex = json_decode(file_get_contents(MYPATH . DS . $this->sSettingsFile), TRUE);
144 foreach ($aOldPictureIndex as $sFile => $iPostcount) {
146 //carry over postcount from existing files
147 if (isset($this->aPictureIndex[$sFile])) {
148 $this->aPictureIndex[$sFile] = $iPostcount;
152 file_put_contents(MYPATH . DS . $this->sSettingsFile, json_encode($this->aPictureIndex));
154 return TRUE;
157 return FALSE;
161 private function recursiveScan($sFolder) {
163 $aFiles = scandir($sFolder);
165 foreach ($aFiles as $key => $sFile) {
167 if (is_dir($sFolder . DS . $sFile) && !in_array($sFile, array('.', '..'))) {
168 unset($aFiles[$key]);
169 $aSubFiles = $this->recursiveScan($sFolder . DS . $sFile);
170 foreach ($aSubFiles as $sSubFile) {
171 if (!in_array($sSubFile, array('.', '..'))) {
172 $aFiles[] = $sFile . DS . $sSubFile;
178 return $aFiles;
181 private function getFile() {
183 if ($this->aTweetSettings['bPostOnlyOnce'] == TRUE) {
184 echo "Getting random unposted file from folder..\n";
186 //create temp array of all files that have postcount = 0
187 $aTempIndex = array_filter($this->aPictureIndex, function($i) { return ($i == 0 ? TRUE : FALSE); });
189 //pick random file
190 $sFilename = array_rand($aTempIndex);
191 } else {
192 echo "Getting random file with lowest postcount from folder..\n";
194 //get lowest postcount in index
195 global $iLowestCount;
196 $iLowestCount = FALSE;
197 foreach ($this->aPictureIndex as $sFilename => $iCount) {
198 if ($iLowestCount === FALSE || $iCount < $iLowestCount) {
199 $iLowestCount = $iCount;
203 //create temp array of files with lowest postcount
204 $aTempIndex = array_filter($this->aPictureIndex, function($i) { global $iLowestCount; return ($i == $iLowestCount ? TRUE : FALSE); });
206 //pick random file
207 $sFilename = array_rand($aTempIndex);
210 $sFilePath = $this->sPictureFolder . DS . utf8_decode($sFilename);
211 $aImageInfo = getimagesize($sFilePath);
213 $aFile = array(
214 'filepath' => $sFilePath,
215 'dirname' => pathinfo($sFilename, PATHINFO_DIRNAME),
216 'filename' => $sFilename,
217 'basename' => pathinfo($sFilePath, PATHINFO_FILENAME),
218 'extension' => pathinfo($sFilePath, PATHINFO_EXTENSION),
219 'size' => number_format(filesize($sFilePath) / 1024, 0) . 'k',
220 'width' => $aImageInfo[0],
221 'height' => $aImageInfo[1],
222 'created' => date('Y-m-d', filectime($sFilePath)),
223 'modified' => date('Y-m-d', filemtime($sFilePath)),
226 return $aFile;
229 private function postMessage($aFile) {
231 echo "Posting tweet..\n";
233 //construct tweet
234 $sTweet = $this->formatTweet($aFile);
235 if (!$sTweet) {
236 return FALSE;
239 //tweet
240 if ($this->sMediaId) {
241 $oRet = $this->oTwitter->post('statuses/update', array('status' => $sTweet, 'trim_users' => TRUE, 'media_ids' => $this->sMediaId));
242 if (isset($oRet->errors)) {
243 $this->logger(2, sprintf('Twitter API call failed: statuses/update (%s)', $oRet->errors[0]->message), array('tweet' => $sTweet, 'file' => $aFile, 'media' => $this->sMediaId));
244 $this->halt('- Error: ' . $oRet->errors[0]->message . ' (code ' . $oRet->errors[0]->code . ')');
245 return FALSE;
246 } else {
247 printf("- %s\n", utf8_decode($sTweet));
250 return TRUE;
252 } else {
253 $this->logger(2, sprintf('Skipping tweet because picture was not uploaded: %s', $aFile['filename']));
257 private function formatTweet($aFile) {
259 //should get this by API (GET /help/configuration ->short_url_length) but it rarely changes
260 $iMaxTweetLength = 280;
261 $iShortUrlLength = 23;
263 if (empty($this->aTweetSettings['sFormat'])) {
264 $this->logger(2, 'Tweet format settings missing.');
265 $this->halt('- The tweet format settings are missing, halting.');
266 return FALSE;
269 //construct tweet
270 $sTweet = $this->aTweetSettings['sFormat'];
272 //replace all non-truncated fields
273 $aFile['filename'] = $aFile['filename'];
274 foreach ($aFile as $sVar => $sValue) {
275 $sTweet = str_replace(':' . $sVar, $sValue, $sTweet);
278 //determine maximum length left over for truncated field (links are shortened to t.co format of max 22 chars)
279 $sTempTweet = preg_replace('/http:\/\/\S+/', str_repeat('x', $iShortUrlLength), $sTweet);
280 $sTempTweet = preg_replace('/https:\/\/\S+/', str_repeat('x', $iShortUrlLength + 1), $sTempTweet);
281 $iTruncateLimit = $iMaxTweetLength - strlen($sTweet);
283 return $sTweet;
286 private function uploadPicture($sImage) {
288 //upload image and save media id to attach to tweet
289 printf("Uploading to Twitter: %s\n", $sImage);
290 $sImageBinary = base64_encode(file_get_contents($sImage));
291 if ($sImageBinary && strlen($sImageBinary) < 15 * pow(1024, 2)) { //max size is 15MB
293 $oRet = $this->oTwitter->upload('media/upload', array('media' => $sImageBinary));
294 if (isset($oRet->errors)) {
295 $this->logger(2, sprintf('Twitter API call failed: media/upload (%s)', $oRet->errors[0]->message), array('file' => $sImage, 'length' => strlen($sImageBinary)));
296 $this->halt('- Error: ' . $oRet->errors[0]->message . ' (code ' . $oRet->errors[0]->code . ')');
297 return FALSE;
298 } else {
299 $this->sMediaId = $oRet->media_id_string;
300 printf("- uploaded %s to attach to next tweet\n", $sImage);
303 return TRUE;
304 } else {
305 printf("- picture is too large!\n");
308 return FALSE;
312 private function updatePostCount($aFile) {
314 $this->aPictureIndex[$aFile['filename']]++;
315 file_put_contents(MYPATH . DS . $this->sSettingsFile, json_encode($this->aPictureIndex));
317 return TRUE;
320 private function halt($sMessage = '') {
321 echo $sMessage . "\n\nDone!\n\n";
322 return FALSE;
325 private function logger($iLevel, $sMessage, $aExtra = array()) {
327 if ($iLevel > $this->iLogLevel) {
328 return FALSE;
331 $sLogLine = "%s [%s] %s\n";
332 $sTimestamp = date('Y-m-d H:i:s');
334 switch($iLevel) {
335 case 1:
336 $sLevel = 'FATAL';
337 break;
338 case 2:
339 $sLevel = 'ERROR';
340 break;
341 case 3:
342 $sLevel = 'WARN';
343 break;
344 case 4:
345 default:
346 $sLevel = 'INFO';
347 break;
348 case 5:
349 $sLevel = 'DEBUG';
350 break;
351 case 6:
352 $sLevel = 'TRACE';
353 break;
356 $aBacktrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
357 TwitterLogger::write($this->sUsername, $sLevel, $sMessage, pathinfo($aBacktrace[0]['file'], PATHINFO_BASENAME), $aBacktrace[0]['line'], $aExtra);
359 $iRet = file_put_contents(MYPATH . '/' . $this->sLogFile, sprintf($sLogLine, $sTimestamp, $sLevel, $sMessage), FILE_APPEND);
361 if ($iRet === FALSE) {
362 die($sTimestamp . ' [FATAL] Unable to write to logfile!');