Skip to content

Instantly share code, notes, and snippets.

@yock
Forked from Two9A/decronym.php
Created October 3, 2015 19:13
Show Gist options
  • Save yock/9e27493bb6ba90284fe6 to your computer and use it in GitHub Desktop.
Save yock/9e27493bb6ba90284fe6 to your computer and use it in GitHub Desktop.
Decronym: A simple Reddit bot
<?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[] = "###&#009;";
$text[] = "######&#009;";
$text[] = "####&#009;";
$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