-
-
Save yock/9e27493bb6ba90284fe6 to your computer and use it in GitHub Desktop.
Decronym: A simple Reddit bot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Dirty, dirty Reddit bot: Decronym | |
*/ | |
class Reddit { | |
const USERNAME = '***'; | |
const PASSWORD = '***'; | |
const CLIENTID = '***'; | |
const SECRET = '***'; | |
const ACCESS_TOKEN_URL = 'https://www.reddit.com/api/v1/access_token'; | |
const API_BASE_URL = 'https://oauth.reddit.com'; | |
const THING_COMMENT = 't1'; | |
const THING_THREAD = 't3'; | |
const THING_SUBREDDIT = 't5'; | |
private static $token; | |
public static function me() { | |
return self::_send(self::API_BASE_URL.'/api/v1/me'); | |
} | |
public static function fetch_comments($subreddit) { | |
return self::_send( | |
self::API_BASE_URL.'/r/'.$subreddit.'/comments/?'.http_build_query(array( | |
'cb' => time(), | |
'sort' => 'new', | |
'limit' => '100' | |
)) | |
); | |
} | |
public static function fetch_threads($threadids) { | |
$names = array(); | |
foreach ($threadids as $threadid) { | |
$names[] = self::_to_thingid(self::THING_THREAD, $threadid); | |
} | |
return self::_send( | |
self::API_BASE_URL.'/api/info?'.http_build_query(array( | |
'id' => join(',', $names), | |
)) | |
); | |
} | |
public static function comment($threadid, $body) { | |
return self::_send( | |
self::API_BASE_URL.'/api/comment', | |
array( | |
'api_type' => 'json', | |
'thing_id' => self::_to_thingid(self::THING_THREAD, $threadid), | |
'text' => $body | |
) | |
); | |
} | |
public static function edit($commentid, $body) { | |
return self::_send( | |
self::API_BASE_URL.'/api/editusertext', | |
array( | |
'api_type' => 'json', | |
'thing_id' => self::_to_thingid(self::THING_COMMENT, $commentid), | |
'text' => $body | |
) | |
); | |
} | |
public static function _to_thingid($type, $id) { | |
return $type.'_'.base_convert($id, 10, 36); | |
} | |
public static function _from_thingid($thingid) { | |
list($type, $id) = explode('_', $thingid); | |
return array( | |
'type' => $type, | |
'id' => base_convert($id, 36, 10) | |
); | |
} | |
private static function _send($url, $data = array()) { | |
self::_ensure_loggedin(); | |
$result = self::_curl($url, $data, null, array( | |
'Authorization' => 'bearer '.self::$token | |
)); | |
if (isset($result['body']['error']) && $result['body']['error']) { | |
if (in_array($result['body']['error'], array(401, 'invalid_grant'))) { | |
self::$token = null; | |
self::_ensure_loggedin(); | |
return self::_send($url, $data); | |
} else { | |
throw new Exception('Server error: '.$result['body']['error']); | |
} | |
} else if (isset($result['body']['json'], $result['body']['json']['errors'])) { | |
if (count($result['body']['json']['errors'])) { | |
throw new Exception(json_encode($result['body']['json']['errors'])); | |
} | |
} | |
return $result; | |
} | |
private static function _ensure_loggedin() { | |
if (!isset(self::$token)) { | |
$login = self::_curl( | |
self::ACCESS_TOKEN_URL, | |
array( | |
'grant_type' => 'password', | |
'username' => self::USERNAME, | |
'password' => self::PASSWORD | |
), | |
array( | |
'username' => self::CLIENTID, | |
'password' => self::SECRET | |
) | |
); | |
if (isset($login['body']['error']) && $login['body']['error']) { | |
throw new Exception('Authentication failed: '.$login['body']['error']); | |
} | |
self::$token = $login['body']['access_token']; | |
} | |
} | |
private static function _curl($url, $data = null, $auth = null, $headers = array()) { | |
$headers['Expect'] = ''; | |
$curlheaders = array(); | |
foreach ($headers as $k => $v) { | |
$curlheaders[] = "{$k}: {$v}"; | |
} | |
$c = curl_init(); | |
$params = array( | |
CURLOPT_URL => $url, | |
CURLOPT_HEADER => true, | |
CURLOPT_VERBOSE => false, | |
CURLOPT_RETURNTRANSFER => true, | |
CURLOPT_FOLLOWLOCATION => true, | |
CURLOPT_USERAGENT => 'Decronym/0.01', | |
CURLOPT_SSLVERSION => 4, | |
CURLOPT_SSL_VERIFYHOST => false, | |
CURLOPT_SSL_VERIFYPEER => false, | |
CURLOPT_HTTPHEADER => $curlheaders | |
); | |
if ($data) { | |
$params += array( | |
CURLOPT_POST => true, | |
CURLOPT_POSTFIELDS => http_build_query($data), | |
CURLOPT_CUSTOMREQUEST => 'POST' | |
); | |
} | |
if ($auth) { | |
$params += array( | |
CURLOPT_HTTPAUTH => CURLAUTH_BASIC, | |
CURLOPT_USERPWD => "{$auth['username']}:{$auth['password']}" | |
); | |
} | |
curl_setopt_array($c, $params); | |
$r = curl_exec($c); | |
$headersize = curl_getinfo($c, CURLINFO_HEADER_SIZE); | |
curl_close($c); | |
$headers = array(); | |
$header = substr($r, 0, $headersize); | |
$body = substr($r, $headersize); | |
foreach (explode("\r\n", $header) as $i => $line) { | |
if ($i === 0) { | |
$headers['_code'] = $line; | |
} else { | |
if (strlen(trim($line))) { | |
list($k, $v) = explode(':', $line); | |
$headers[trim($k)] = trim($v); | |
} | |
} | |
} | |
return array( | |
'headers' => $headers, | |
'body' => json_decode(trim($body), true) | |
); | |
} | |
} | |
class Decronym { | |
const DB_HOST = 'localhost'; | |
const DB_NAME = 'decronym'; | |
const DB_USER = '***'; | |
const DB_PASS = '***'; | |
private static $dbc; | |
private static $subreddit; | |
public static function read_comments_since_last_check() { | |
self::_ensure_unchecked_subreddit(); | |
$ts = self::$subreddit['last_check_ts']; | |
$comments_return = Reddit::fetch_comments(self::$subreddit['subreddit_name']); | |
$comments = $comments_return['body']['data']['children']; | |
$ret = array(); | |
foreach ($comments as $comment) { | |
if ( | |
($comment['data']['edited'] && $comment['data']['edited'] >= $ts) || | |
$comment['data']['created_utc'] >= $ts | |
) { | |
// We don't want comments written by the bot, that just gets silly | |
if ($comment['data']['author'] != Reddit::USERNAME) { | |
$ret[] = array( | |
'body' => $comment['data']['body'], | |
'thread' => Reddit::_from_thingid($comment['data']['link_id']) | |
); | |
} | |
} | |
} | |
return $ret; | |
} | |
public static function read_thread_info($thread_ids) { | |
$data = Reddit::fetch_threads($thread_ids); | |
$counts = array(); | |
if (isset($data['body']['data'], $data['body']['data']['children'])) { | |
foreach ($data['body']['data']['children'] as $child) { | |
$threadid = Reddit::_from_thingid($child['data']['name']); | |
$counts[$threadid['id']] = array( | |
'num_comments' => $child['data']['num_comments'], | |
'created_utc' => $child['data']['created_utc'] | |
); | |
} | |
} | |
return $counts; | |
} | |
public static function parse_comments($comments) { | |
self::_ensure_unchecked_subreddit(); | |
$acronyms = self::_fetch_acronyms(); | |
$keys = array(); | |
$threads = array(); | |
$regex = '#\b('.join('|', array_keys($acronyms)).')\b#i'; | |
foreach ($comments as $comment) { | |
if (preg_match_all($regex, $comment['body'], $matches)) { | |
foreach ($matches[1] as $match) { | |
self::_add_acronym_to_thread( | |
$comment['thread']['id'], | |
$acronyms[$match]['acronym_id'] | |
); | |
} | |
if (!isset($threads[$comment['thread']['id']])) { | |
$threads[$comment['thread']['id']] = true; | |
} | |
} | |
} | |
return array_keys($threads); | |
} | |
public static function post_or_edit($thread_id, $thread_info) { | |
if ( | |
($thread_info['num_comments'] > self::$subreddit['min_comments']) || | |
(time() > $thread_info['created_utc'] + self::$subreddit['max_thread_wait']) | |
) { | |
$thread = self::_fetch_thread($thread_id); | |
$body = self::_build_body_for_thread($thread_id); | |
if ($thread) { | |
Reddit::edit($thread['bot_comment_id'], $body); | |
} else { | |
$ret = Reddit::comment($thread_id, $body); | |
if (isset( | |
$ret['body']['json']['data'], | |
$ret['body']['json']['data']['things'], | |
$ret['body']['json']['data']['things'][0], | |
$ret['body']['json']['data']['things'][0]['data'], | |
$ret['body']['json']['data']['things'][0]['data']['name'] | |
)) { | |
$comment = Reddit::_from_thingid($ret['body']['json']['data']['things'][0]['data']['name']); | |
self::_add_thread($thread_id, $comment['id']); | |
} | |
} | |
} | |
} | |
private static function _fetch_acronyms() { | |
self::_ensure_unchecked_subreddit(); | |
$st = self::$dbc->prepare('SELECT acronym_id, acronym_key, acronym_value FROM acronyms WHERE subreddit_id = :id'); | |
$st->bindValue(':id', self::$subreddit['subreddit_id']); | |
$st->execute(); | |
$acronyms = array(); | |
foreach ($st->fetchAll() as $row) { | |
$acronyms[$row['acronym_key']] = $row; | |
} | |
return $acronyms; | |
} | |
private static function _fetch_thread($thread_id) { | |
self::_ensure_unchecked_subreddit(); | |
$st = self::$dbc->prepare('SELECT bot_comment_id, first_check_ts FROM threads WHERE subreddit_id = :subreddit AND thread_id = :thread'); | |
$st->bindValue(':subreddit', self::$subreddit['subreddit_id']); | |
$st->bindValue(':thread', $thread_id); | |
$st->execute(); | |
$rows = $st->fetchAll(); | |
if (count($rows)) { | |
return $rows[0]; | |
} | |
return false; | |
} | |
private static function _fetch_acronyms_for_thread($thread_id) { | |
$st = self::$dbc->prepare('SELECT a.acronym_id, a.acronym_key, a.acronym_value FROM thread_acronyms ta LEFT JOIN acronyms a ON ta.acronym_id = a.acronym_id WHERE ta.thread_id = :id ORDER BY a.acronym_key ASC'); | |
$st->bindValue(':id', $thread_id); | |
$st->execute(); | |
$acronyms = array(); | |
foreach ($st->fetchAll() as $row) { | |
$acronyms[$row['acronym_key']] = $row; | |
} | |
return $acronyms; | |
} | |
private static function _build_body_for_thread($thread_id) { | |
$thread = self::_fetch_thread($thread_id); | |
$acronyms = self::_fetch_acronyms_for_thread($thread_id); | |
$text = array(); | |
$first_check = $thread ? $thread['first_check_ts'] : time(); | |
$text[] = "###	"; | |
$text[] = "######	"; | |
$text[] = "####	"; | |
$text[] = ''; | |
$text[] = "Acronyms I've seen in this thread since I first looked:"; | |
$text[] = ''; | |
$text[] = '|Acronym|Expansion|'; | |
$text[] = '|-------|---------|'; | |
foreach ($acronyms as $key => $row) { | |
$text[] = sprintf('|%s|%s|', $key, $row['acronym_value']); | |
} | |
$text[] = ''; | |
$text[] = '----------------'; | |
$text[] = "^(I'm a bot; I've only been checking comments posted in this thread since ".date('H:i \U\T\C \o\n Y-m-d', $first_check).'.) '; | |
$text[] = "^(If I'm acting up, message )^[OrangeredStilton](https://www.reddit.com/message/compose?to=OrangeredStilton&subject=Hey,+your+acronym+bot+sucks)."; | |
return join("\n", $text); | |
} | |
private static function _add_acronym_to_thread($thread_id, $acronym_id) { | |
$st = self::$dbc->prepare('REPLACE INTO thread_acronyms(thread_id, acronym_id) VALUES(:thread, :acronym)'); | |
$st->bindValue(':thread', $thread_id); | |
$st->bindValue(':acronym', $acronym_id); | |
$st->execute(); | |
} | |
private static function _add_thread($thread_id, $comment_id) { | |
$st = self::$dbc->prepare('INSERT INTO threads(thread_id, subreddit_id, bot_comment_id, first_check_ts) VALUES(:thread, :subreddit, :comment, :ts)'); | |
$st->bindValue(':thread', $thread_id); | |
$st->bindValue(':subreddit', self::$subreddit['subreddit_id']); | |
$st->bindValue(':comment', $comment_id); | |
$st->bindValue(':ts', time()); | |
$st->execute(); | |
} | |
private static function _ensure_unchecked_subreddit() { | |
self::_ensure_connection(); | |
if (!self::$subreddit) { | |
$time = time(); | |
$st = self::$dbc->prepare('SELECT subreddit_id, subreddit_name, last_check_ts, min_comments, max_thread_wait FROM subreddits WHERE last_check_ts < :ts ORDER BY last_check_ts LIMIT 1'); | |
$st->bindValue(':ts', $time); | |
$st->execute(); | |
$row = $st->fetch(); | |
$update_st = self::$dbc->prepare('UPDATE subreddits SET last_check_ts = :ts WHERE subreddit_id = :id'); | |
$update_st->bindValue(':ts', $time); | |
$update_st->bindValue(':id', $row['subreddit_id']); | |
$update_st->execute(); | |
self::$subreddit = $row; | |
} | |
} | |
private static function _ensure_connection() { | |
if (!self::$dbc) { | |
self::$dbc = new PDO( | |
sprintf('mysql:host=%s;dbname=%s', self::DB_HOST, self::DB_NAME), | |
self::DB_USER, | |
self::DB_PASS | |
); | |
self::$dbc->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); | |
self::$dbc->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); | |
} | |
} | |
} | |
$threads = Decronym::parse_comments(Decronym::read_comments_since_last_check()); | |
$comment_counts = Decronym::read_thread_info($threads); | |
foreach ($threads as $thread_id) { | |
Decronym::post_or_edit($thread_id, $comment_counts[$thread_id]); | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment