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);
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) {
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'])) {
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()) {
70 if ($aFile = $this->getFile()) {
73 if ($this->uploadPicture($aFile['filepath'])) {
75 //format and post message
76 if ($this->postMessage($aFile)) {
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.');
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
);
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
));
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
));
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);
127 //get list of all files
128 $aFileList = $this->recursiveScan($this->sPictureFolder
);
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
));
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;
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); });
190 $sFilename = array_rand($aTempIndex);
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); });
207 $sFilename = array_rand($aTempIndex);
210 $sFilePath = $this->sPictureFolder
. DS
. utf8_decode($sFilename);
211 $aImageInfo = getimagesize($sFilePath);
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)),
229 private function postMessage($aFile) {
231 echo "Posting tweet..\n";
234 $sTweet = $this->formatTweet($aFile);
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
. ')');
247 printf("- %s\n", utf8_decode($sTweet));
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.');
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);
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
. ')');
299 $this->sMediaId
= $oRet->media_id_string
;
300 printf("- uploaded %s to attach to next tweet\n", $sImage);
305 printf("- picture is too large!\n");
312 private function updatePostCount($aFile) {
314 $this->aPictureIndex
[$aFile['filename']]++
;
315 file_put_contents(MYPATH
. DS
. $this->sSettingsFile
, json_encode($this->aPictureIndex
));
320 private function halt($sMessage = '') {
321 echo $sMessage . "\n\nDone!\n\n";
325 private function logger($iLevel, $sMessage, $aExtra = array()) {
327 if ($iLevel > $this->iLogLevel
) {
331 $sLogLine = "%s [%s] %s\n";
332 $sTimestamp = date('Y-m-d H:i:s');
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!');