├── .gitignore
├── README.md
├── action.php
├── actions
├── PostCleanupAction.php
├── aggregator.php
├── delayedmarkovpostaction.php
└── delayedmarkovpostaction
│ ├── markovfirstorder.php
│ └── markovsecondorder.php
├── config.php
├── install.php
├── phirehose
├── OauthPhirehose.php
├── Phirehose.php
└── UserstreamPhirehose.php
├── phpunit
├── data
│ └── testDelayedMarkovPostActionData.xml
└── tests
│ ├── DatabaseTest.php
│ └── MarkovTest.php
├── run.php
├── storage.php
├── twitterbot.php
├── twitteroauth
├── OAuth.php
└── twitteroauth.php
└── util
├── TwitterAPI.php
└── translate.php
/.gitignore:
--------------------------------------------------------------------------------
1 | config.php
2 | config_orig.php
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | PHP Twitterbot
2 | ==============
3 |
4 | **DEPRECATION NOTICE**: As of the release of [PyBot](https://github.com/magsol/pybot), this Twitter bot implementation has been deprecated. No further updates will be made to this repository. Apologies for any inconvenience; PHP just wasn't made for this sort of thing.
5 |
6 |
7 | This is designed to be a relatively lightweight, flexible framework for
8 | deploying your own customized and automated twittering bot that more or
9 | less does whatever you program it to do.
10 |
11 | - Author: Shannon Quinn
12 | - Project Wiki: http://www.magsol.me/wiki/index.php5?title=SpamBot
13 | - Git repository: https://github.com/magsol/Twitterbot
14 | - Version: 2.0 (alpha)
15 |
16 | Overview
17 | --------
18 |
19 | The general idea here is to provide a framework by which you can implement
20 | as simple or complex a twitterbot as you like without having to worry about
21 | anything but the specific behavior you want to implement. If you want a bot
22 | that does nothing but sample the public timeline, you can practically use
23 | this framework out of the box. If you want a box that reads posts, makes
24 | posts, adds friends, changes its profile, and develops a cure for cancer you
25 | can absolutely do that too (though it may take a bit of work). The point
26 | is you'll only have to focus on implementing that specific behavior; everything
27 | else (in theory :P ) has been taken care of.
28 |
29 | The core of this framework is the concept of an "Action": ideally, it
30 | encapsulates a single concrete activity the twitterbot performs (posting,
31 | or direct messaging, or changing background images, etc). In order to create
32 | a new action, you begin by subclassing the Action class.
33 |
34 | Say we want our bot to post the current time and a "Hello!" message every hour.
35 | We begin by defining this class as follows:
36 |
37 | require_once(BOTROOT . 'action.php');
38 | class ClockAction extends Action {
39 |
40 | We are then required to implement at least two other methods: a
41 | constructor and a `run()` method:
42 |
43 | public function __construct($name, $active, $params) {
44 | $this->name = $name;
45 | $this->isActive = $active;
46 | foreach ($params as $k => $v) {
47 | $this->$k = $v;
48 | }
49 | parent::__construct($name, $active, array()); // recommended!
50 | }
51 |
52 | public function run() {
53 | // do stuff here that will post hourly update
54 | // hint: make use of util/TwitterAPI.php
55 |
56 | // if the action completes without any errors,
57 | // return parent::SUCCESS. otherwise, return parent::FAILURE
58 | return parent::SUCCESS;
59 | }
60 |
61 | With this basic framework, you can extend it to do just about anything
62 | you'd like. Set `$this->frequency` in the `__construct()` method you wrote
63 | to provide a different frequency of your Action firing; or, for even
64 | greater control, override the `setNextAttempt()` method to completely
65 | redefine the frequency with which your Action fires. You can override
66 | the `post_run()` method to perform any custom post-Action clean-up or
67 | logging.
68 |
69 | Put your `ClockAction.php` file into the actions/ folder (along with
70 | any additional dependencies it may require), and point this Twitterbot
71 | to it by setting up `config.php` to point to it (details below in the
72 | SETUP section, step 1). Once this is complete, fire up the bot's daemon,
73 | sit back, and let the Twitter trolling begin :)
74 |
75 | Notes
76 | -----
77 |
78 | *THIS IS STILL VERY MUCH EXPERIMENTAL AND NOT PARTICULARLY ROBUST.* I implemented
79 | the process control without much previous experience in it (most of my experience
80 | is in multithreading with C...not very applicable to this), so it is still very
81 | rough around the edges, particularly with the database connection management.
82 |
83 | If you encounter any bugs, please don't hesitate to report them, either to the
84 | github page, or you can email me: magsol at gmail.
85 |
86 | Requirements
87 | ------------
88 |
89 | In order to run this bot, you need:
90 |
91 | - PHP 5.x (with pcntl)
92 | - PEAR and its basic libraries
93 | - MySQL 5.x (though will probably work with 4.x)
94 | - A brave soul
95 |
96 | Installation
97 | -----------
98 |
99 | 1. Fill out the necessary fields in config.php (there are several).
100 | - `BOT_ACCOUNT`: The display name for your bot.
101 | - `BOT_PASSWORD`: The password to log into your bot (will be removed once Twitter's
102 | Streaming API is integrated into OAuth...for now, a necessary evil).
103 | - `CONSUMER_KEY`: OAuth Consumer Key, obtained by creating an app for this bot.
104 | - `CONSUMER_SECRET`: Same as above.
105 | - `OAUTH_TOKEN`: Same as above.
106 | - `OAUTH_TOKEN_SECRET`: Same as above.
107 | - `DB_NAME`: Name of the database this application's data can be stored in.
108 | - `DB_HOST`: Host on which the database resides (usually "localhost").
109 | - `DB_USER`: Username of the account that has admin access to the DB_NAME database.
110 | - `DB_PASS`: Password for the above user.
111 |
112 | Within the $actions array you have optional definitions (required if you want
113 | the bot to do anything interesting) to customize how your bot behaves. If you
114 | leave everything blank, it will simply aggregate posts from Twitter's public
115 | timeline (you'll see them grow quite fast within your database).
116 |
117 | If you choose to add some fields, you'll need to implement your own Action
118 | subclass that performs whatever action of interest you want. Obeying the usual
119 | object-oriented programming guidelines generally makes for better-behaved bots,
120 | but in general you can have your action do whatever you want. Just be sure
121 | to fill in the required fields for any defined Action:
122 | - name (can be anything, used mainly for debugging)
123 | - class (case-sensitive name of the class you created)
124 | - file (case-sensitive name of the PHP file in which your class resides)
125 | - active (boolean indicating whether or not this action should be fired)
126 | - args (optional array of arguments, specific to your action)
127 |
128 | 2. Run the bot's install script.
129 |
130 | `php install.php`
131 |
132 | This will use the database values defined in your `config.php` to set up your
133 | database's schema. For optimal behavior, please only fire this script once.
134 | It shouldn't cause any unintended behavior if you execute it multiple times
135 | (it will simply quit if it detects the tables exist already), but why would
136 | you need to run the install script multiple times anyway?
137 |
138 | 3. Test the bot's settings.
139 |
140 | `php run.php --tests-only`
141 |
142 | This will have the bot run a battery of tests against the settings you've
143 | indicated. If there are any failures, it will halt immediately and let you
144 | know (wrong database username/password, a missing required field for a custom
145 | Action, etc).
146 |
147 | 4. Run the bot!
148 |
149 | `php run.php`
150 |
151 | You can include a `-h` flag to display all the available options. By default
152 | (run with no arguments), the script will execute the battery of tests like in
153 | step 3 but will, pending all tests passing, start up the bot itself. The bot
154 | will behave as a daemon, detaching itself from the terminal and spawning a child
155 | process for each custom Action defined in `config.php`. In order to kill the
156 | daemon and its child processes, run the command:
157 |
158 | `php run.php --stop`
159 |
160 | Acknowledgements
161 | ----------------
162 |
163 | There are several people who made this project possible.
164 |
165 | - [Rob Hall](http://cs.cmu.edu/~rjhall) : the original inspiration, with his
166 | hilarious [postmaster9001 bot](http://twitter.com/postmaster9001), whose awful
167 | Perl hack-job implementation inspired me to make something more flexible
168 | and robust.
169 |
170 | - [Abraham of twitteroauth](https://github.com/abraham/twitteroauth) : His library
171 | makes the implementation of OAuth authentication in this project possible.
172 | That's one huge black box that is reduced to one or two lines of code on my
173 | part to worry about. Awesome, awesome work.
174 |
175 | - [Fennb of Phirehose](https://github.com/fennb/phirehose) : Again, a huge, rich,
176 | robust API that makes my life really freaking easy. This makes data aggregation
177 | possible in this project, enabling me to spend my time doing interesting things
178 | with the data rather than finding ways of crawling Twitter. Keep up the good
179 | work.
180 |
181 | - [Sebastian Bergmann of PHPUnit](https://github.com/sebastianbergmann/phpunit/) :
182 | Author of a phenomenal unit-testing framework for PHP, it's still on my to-do
183 | list to incporporate it into this bot's testing. It will definitely happen,
184 | and this guy has made it a lot easier.
185 |
186 | - George Schlossnagle of "Advanced PHP Programming", from which I learned just
187 | about every trick in this project for process management, particularly in
188 | setting up the structure for the Twitterbot and Action classes. It was a
189 | phenomenal resource and made the daemon aspect of this project feasible.
190 |
--------------------------------------------------------------------------------
/action.php:
--------------------------------------------------------------------------------
1 | $v) {
40 | $this->$k = $v;
41 | }
42 | $this->name = $name;
43 | $this->isActive = $active;
44 | }
45 |
46 | /**
47 | * This method also needs to be implemented by the subclasses. Dictates
48 | * how the action runs.
49 | * NOTE: If you need access to a database connection, do it in here!
50 | *
51 | * @return Action::SUCCESS if the method runs successfully,
52 | * Action::FAILURE otherwise.
53 | */
54 | public abstract function run();
55 |
56 | /**
57 | * Calculates the time for the next firing of this action.
58 | */
59 | public function setNextAttempt() {
60 | $this->nextAttempt = time() + ($this->frequency * 60);
61 | }
62 |
63 | /**
64 | * Accessor for the nextAttempt field.
65 | * @return The unix timestamp of when this action should fire.
66 | */
67 | public function getNextAttempt() {
68 | return $this->nextAttempt;
69 | }
70 |
71 | /**
72 | * Accessor for the timeout count.
73 | * @return The number of seconds until this action should be timed out if
74 | * it has not completed.
75 | */
76 | public function getTimeout() {
77 | return $this->timeout * 60;
78 | }
79 |
80 | /**
81 | * This method can be called after the run() method to perform
82 | * post-processing.
83 | *
84 | * In this case, it logs any failures (if the run() return value is
85 | * Action::FAILURE) and saves the necessary values to the database.
86 | * @param int $status The return code from the child process exiting.
87 | */
88 | public function post_run($status) {
89 | $this->db = Storage::getDatabase();
90 |
91 | // log the status of this action
92 | if ($status !== $this->currentStatus) {
93 | $this->previousStatus = $this->currentStatus;
94 | }
95 | if ($status === self::FAILURE) {
96 | if ($this->currentStatus === self::FAILURE) {
97 | // failed consecutive times
98 | $this->db->log($this->name, "Still have not recovered from previous" .
99 | "error!");
100 | } else {
101 | // this is the first time the action has failed
102 | $this->db->log($this->name, "Error has occurred!");
103 | }
104 | } else {
105 | // Action::SUCCESS. Log this only if the previous status
106 | // was Action::FAILURE, so we know we've recovered from something.
107 | if ($this->previousStatus === Action::FAILURE) {
108 | $this->db->log($this->name, "Recovered from previous failure.");
109 | } else {
110 | $this->db->log($this->name, "Successful run!");
111 | }
112 | }
113 | // set the current status
114 | $this->currentStatus = $status;
115 |
116 | // destroy the database connection
117 | unset($this->db);
118 | }
119 |
120 | /**
121 | * Simple accessor for the state of this action.
122 | * @return True if this Action is active, false otherwise.
123 | */
124 | public function isActive() {
125 | return $this->isActive;
126 | }
127 |
128 | /**
129 | * Change the active state of this action. This is changed through
130 | * signaling.
131 | * @param boolean $state The new active state of this action (true or false).
132 | */
133 | public function setActive($state) {
134 | $this->isActive = $state;
135 | }
136 |
137 | /**
138 | * Accessor for this action's name.
139 | * @return The name (unique identifier) of this action.
140 | */
141 | public function getName() {
142 | return $this->name;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/actions/PostCleanupAction.php:
--------------------------------------------------------------------------------
1 | 0.0, this is the percentage of the number of posts to
14 | * delete (from least recent to most recent).
15 | * -numberDelete: If > 0, this is the raw number of posts to delete (from least
16 | * recent to most recent).
17 | * -maxAllowed: if > 0, this is the maximum number of tweets allowed in the
18 | * database, all others will be deleted (from least recent to most recent).
19 | * -delay: Minutes in between firings.
20 | *
21 | * NOTE: If more than one of these values are set, the order of precedence is:
22 | * 1) maxAllowed
23 | * 2) percent
24 | * 3) number
25 | */
26 | class PostCleanupAction extends Action {
27 |
28 | private $percentDelete = -1.0;
29 | private $numberDelete = -1;
30 | private $maxAllowed = 1000000;
31 | private $delay = 25;
32 |
33 | /**
34 | * Constructor.
35 | * @param string $name
36 | * @param bool $active
37 | * @param array $args
38 | */
39 | function __construct($name, $active, $args = array()) {
40 | parent::__construct($name, $active, array());
41 | foreach ($args as $k => $v) {
42 | $this->$k = $v;
43 | }
44 |
45 | if ((isset($args['maxAllowed']) && isset($args['numberDelete'])) ||
46 | (isset($args['maxAllowed']) && isset($args['percentDelete']))) {
47 | $this->numberDelete = -1;
48 | $this->percentDelete = -0.1;
49 | } else if (isset($args['percentDelete']) && isset($args['numberDelete'])) {
50 | $this->numberDelete = -0.1;
51 | }
52 | $this->frequency = $this->delay;
53 | $this->setNextAttempt();
54 | }
55 |
56 | /**
57 | * @see Action::run()
58 | */
59 | public function run() {
60 | $this->db = Storage::getDatabase();
61 |
62 | // First, figure out how many tweets there are in the whole database.
63 | $query = 'SELECT COUNT(*) FROM `' . DB_NAME . '`.`' . POST_TABLE . '`';
64 | $this->db->setQuery($query);
65 | $result = $this->db->query();
66 | $numPosts = $result[0]['COUNT(*)'];
67 | $toDelete = 0;
68 | if ($this->maxAllowed > 0) {
69 | if ($this->maxAllowed > $numPosts) {
70 | return parent::SUCCESS;
71 | }
72 | $toDelete = $numPosts - $this->maxAllowed;
73 | } else if ($this->number > 0) {
74 | if ($this->number > $numPosts) {
75 | return parent::SUCCESS;
76 | }
77 | // Delete this number of posts.
78 | $toDelete = $this->number;
79 | } else {
80 | $toDelete = intval($this->percent * (double)$numPosts);
81 | }
82 | $query = 'DELETE FROM `' . DB_NAME . '`.`' . POST_TABLE . '` ORDER BY ' .
83 | '`date_saved` ASC LIMIT ' . $toDelete;
84 | $this->db->setQuery($query);
85 | $this->db->query();
86 | $this->db->log($this->getName(), $toDelete . ' tweet' .
87 | ($toDelete != 1 ? 's' : '') . ' deleted!');
88 | return parent::SUCCESS;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/actions/aggregator.php:
--------------------------------------------------------------------------------
1 | db)) { $this->db = Storage::getDatabase(); }
29 |
30 | // save the status
31 | $data = json_decode($status, true);
32 | if (is_array($data) && isset($data['user']['screen_name'])) {
33 | $this->db->savePost($data['text'], $data['user']['screen_name']);
34 | }
35 | }
36 |
37 | /**
38 | * (non-PHPdoc)
39 | * @see util/Phirehose::log()
40 | */
41 | protected function log($message) {
42 | if (!isset($this->db)) { $this->db = Storage::getDatabase(); }
43 | $this->db->log('Phirehose', $message);
44 | }
45 | }
46 |
47 | ?>
48 |
--------------------------------------------------------------------------------
/actions/delayedmarkovpostaction.php:
--------------------------------------------------------------------------------
1 | $v) {
47 | $this->$k = $v;
48 | }
49 | $this->setNextAttempt();
50 | }
51 |
52 | /**
53 | * @see Action::run()
54 | */
55 | public function run() {
56 | $this->db = Storage::getDatabase();
57 |
58 | /*** PART 1: Read from saved posts and construct a markov chain ***/
59 | $posts = $this->db->getPosts($this->useOnlyUnmodeledPosts,
60 | $this->postLimit);
61 | $markov = $this->buildMarkovChain($posts);
62 | if ($this->useOnlyUnmodeledPosts && count($posts) > 0) {
63 | $recent_date = $posts[0]['date_saved'];
64 | $old_date = $posts[count($posts) - 1]['date_saved'];
65 | $this->db->markPostsModeled($old_date, $recent_date);
66 | }
67 |
68 | /*** PART 2: now that we have our markov chain built, sample from it to
69 | * build the post ***/
70 | $thepost = $this->constructPostFromMarkovChain($markov);
71 |
72 | /*** PART 3: Translate the post and send it, if the API key was set ***/
73 | $API = TwitterAPI::getTwitterAPI();
74 | /*
75 | ** GOOGLE TRANSLATE API DEPRECATED **
76 | if (isset($this->googleKey)) {
77 | $thepost = Translate::translate($thepost, 'en', $this->googleKey);
78 | }
79 | */
80 | $API->update($thepost);
81 |
82 | // destroy the database connection
83 | unset($this->db);
84 |
85 | // all done
86 | return parent::SUCCESS;
87 | }
88 |
89 | /**
90 | * Overridden from parent class' declaration. Since this action relies
91 | * on a probability distribution to determine its next firing, it is not
92 | * a simple addition of terms. Thus, for this class, the "frequency" field
93 | * is ignored and replaced with "delayMean" and "delayVar".
94 | *
95 | * @see Action::setNextAttempt()
96 | */
97 | public function setNextAttempt() {
98 | $mean = floatval($this->delayMean);
99 | $var = floatval($this->delayVar);
100 | $rand1 = floatval(mt_rand()) / floatval(mt_getrandmax());
101 | $rand2 = floatval(mt_rand()) / floatval(mt_getrandmax());
102 |
103 | // sample from a normal (gaussian) distribution
104 | $delay = intval((sqrt(-2 * log($rand1)) * cos(2 * pi() * $rand2) * $var) +
105 | $mean);
106 | if ($delay <= 0) { $delay = 1; } // sanity check
107 | $this->nextAttempt = time() + intval($delay * 60);
108 |
109 | // log the next attempt
110 | $this->db = Storage::getDatabase();
111 | $this->db->log($this->getName(), 'Next action firing set for ' .
112 | date('Y-m-d H:i:s', $this->nextAttempt));
113 | unset($this->db);
114 | }
115 |
116 | /**
117 | * This helper method appends the raw posts from the Twitter public timeline
118 | * to the provided second-order markov chain. It extracts each individual
119 | * post, explodes it into its constituent words, performs any needed
120 | * preprocessing, builds the words in the markov chain, and returns
121 | * the updated markov chain.
122 | *
123 | * @param array $posts JSON-decoded twitter posts from the public timeline.
124 | * @return object A markov chain, updated with the new posts.
125 | */
126 | private function buildMarkovChain($posts) {
127 | $numposts = count($posts);
128 | $markov = new MarkovSecondOrder();
129 | for ($j = 0; $j < $numposts; $j++) {
130 | $words = explode(' ', trim($posts[$j]['text']));
131 | array_unshift($words, '_START1_', '_START2_');
132 | $words[] = '_STOP_';
133 | $numwords = count($words);
134 | for ($k = 2; $k < $numwords; $k++) {
135 | $markov->add($words[$k - 2], $words[$k - 1], $words[$k]);
136 | }
137 | }
138 | return $markov;
139 | }
140 |
141 | /**
142 | * This helper method iterates over the second-order markov chain, sampling
143 | * from it appropriately and constructing a post from it.
144 | *
145 | * @param object $markov The second-order markov chain.
146 | * @return string The assembled post.
147 | */
148 | private function constructPostFromMarkovChain($markov) {
149 | $word1 = '_START1_';
150 | $word2 = '_START2_';
151 | $next = '';
152 | $thepost = '';
153 | while (($next = $markov->get($word1, $word2)) != '_STOP_') {
154 | $temp = $thepost . $next;
155 | if (strlen($temp) > 140) {
156 | $temp = trim($temp);
157 | break;
158 | }
159 | $thepost = $temp . ' ';
160 | $word1 = $word2;
161 | $word2 = $next;
162 | }
163 | return $thepost;
164 | }
165 | }
166 |
167 | ?>
168 |
--------------------------------------------------------------------------------
/actions/delayedmarkovpostaction/markovfirstorder.php:
--------------------------------------------------------------------------------
1 | hash = array();
16 | }
17 |
18 | /**
19 | * Adds a new word and the word following it to the model (first order)
20 | * @param word
21 | * @param next
22 | */
23 | public function add($word, $next) {
24 | // first, does the word already exist?
25 | if (isset($this->hash[$word])) {
26 | $this->hash[$word][$next] = (isset($this->hash[$word][$next]) ?
27 | $this->hash[$word][$next] + 1 : 1);
28 | } else {
29 | $this->hash[$word] = array();
30 | $this->hash[$word][$next] = 1;
31 | }
32 | }
33 |
34 | public function debug() {
35 | print_r($this->hash);
36 | }
37 |
38 | /**
39 | * Returns the next word from the distribution, given the current word.
40 | * @param word
41 | * @return
42 | */
43 | public function get($word) {
44 | // first, does the word even exist?
45 | if (!isset($this->hash[$word])) {
46 | return '';
47 | }
48 | $subarr = $this->hash[$word];
49 | // calculate the sum of the counts of the next possibilities
50 | $sum = array_sum($subarr);
51 |
52 | // generate a random number in this range
53 | $rand = mt_rand(1, $sum);
54 |
55 | // loop again, this time stopping once the counts have
56 | // reached the random number we generated, then return
57 | // that word
58 | $sum = 0;
59 | foreach ($subarr as $w => $count) {
60 | $sum += $count;
61 | if ($sum >= $rand) {
62 | return $w;
63 | }
64 | }
65 | }
66 | }
67 |
68 | ?>
69 |
--------------------------------------------------------------------------------
/actions/delayedmarkovpostaction/markovsecondorder.php:
--------------------------------------------------------------------------------
1 | hash = array();
17 | }
18 |
19 | /**
20 | * Adds a new word to the model, given the two previous words
21 | * @param string $word1
22 | * @param string $word2
23 | * @param string $next
24 | */
25 | public function add($word1, $word2, $next) {
26 | if (isset($this->hash[$word1])) {
27 | // the first level exists, how about the second?
28 | if (isset($this->hash[$word1][$word2])) {
29 | $this->hash[$word1][$word2][$next] = (isset($this->hash[$word1][$word2][$next]) ?
30 | $this->hash[$word1][$word2][$next] + 1 : 1);
31 | } else {
32 | $this->hash[$word1][$word2] = array();
33 | $this->hash[$word1][$word2][$next] = 1;
34 | }
35 | } else {
36 | $this->hash[$word1] = array();
37 | $this->hash[$word1][$word2] = array();
38 | $this->hash[$word1][$word2][$next] = 1;
39 | }
40 | }
41 |
42 | /**
43 | * Returns the next word in the distribution, given the current word.
44 | * @param string $firstword
45 | * @param string $secondword
46 | * @return string A word sampled from the distribution P(word | $firstword, $secondword)
47 | */
48 | public function get($firstword, $secondword) {
49 | // does this even exist?
50 | if (!isset($this->hash[$firstword]) || !isset($this->hash[$firstword][$secondword])) {
51 | return '';
52 | }
53 |
54 | // how many words are there in this sequence of initial tokens?
55 | $wordarr = $this->hash[$firstword][$secondword];
56 | $totalwords = array_sum($wordarr);
57 |
58 | // generate a random number in this range
59 | $sample = mt_rand(1, $totalwords);
60 |
61 | // loop over the model, stopping once we've exceeded our random number,
62 | // which corresponds to the token we want to sample
63 | $sum = 0;
64 | foreach ($wordarr as $word => $count) {
65 | $sum += $count;
66 | if ($sum >= $sample) {
67 | return $word;
68 | }
69 | }
70 | // should NEVER reach this point
71 | }
72 | }
--------------------------------------------------------------------------------
/config.php:
--------------------------------------------------------------------------------
1 | 'Name of your custom action', // Can be anything you want!
17 |
18 | 'class' => 'CaseSensitiveNameOfClass', // The case-sensitive name
19 | // of the action class you
20 | // wrote.
21 |
22 | 'file' => 'name_of_php_file.php', // The name of the PHP file
23 | // (with the .php extension)
24 | // of the file containing
25 | // your custom action class.
26 |
27 | 'active' => true | false, // Determines whether this
28 | // action will run or not.
29 | // If true, this action will
30 | // execute. If false, it
31 | // will be ignored.
32 |
33 | 'args' => array('Any additional arguments'),// An array of any
34 | // additional arguments
35 | // specific to your custom
36 | // action.
37 | ),
38 | */
39 | );
40 |
41 | // change the following values to connect to your database
42 | define('DB_NAME', 'your db name here'); // name of the database itself
43 | define('DB_HOST', 'your db host here'); // usually 'localhost'
44 | define('DB_USER', 'your db user here'); // username to connect
45 | define('DB_PASS', 'your db pass here'); // password for the username
46 |
47 | // ----------------------------------------------------------- //
48 | // -- That's it! Don't change anything else below this line -- //
49 | // ----------------------------------------------------------- //
50 |
51 | define('TWITTERBOT_LOCKFILE', '/tmp/.bot_lockfile');
52 | define('BOTROOT', __DIR__);
53 | define('ACTIONS', BOTROOT . DIRECTORY_SEPARATOR . 'actions' .
54 | DIRECTORY_SEPARATOR);
55 | define('UTIL', BOTROOT . DIRECTORY_SEPARATOR . 'util' . DIRECTORY_SEPARATOR);
56 | define('OAUTH', BOTROOT . DIRECTORY_SEPARATOR . 'twitteroauth' .
57 | DIRECTORY_SEPARATOR);
58 | define('PHIREHOSE', BOTROOT . DIRECTORY_SEPARATOR . 'phirehose' .
59 | DIRECTORY_SEPARATOR);
60 | define('POST_TABLE', 'posts');
61 | define('LOG_TABLE', 'logs');
62 |
63 | ?>
64 |
--------------------------------------------------------------------------------
/install.php:
--------------------------------------------------------------------------------
1 | setQuery($sql);
20 | $db->query();
21 | $sql = 'CREATE TABLE IF NOT EXISTS `' . DB_NAME . '`.`' . LOG_TABLE . '` (' .
22 | '`eventid` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,' .
23 | '`eventtime` DATETIME NOT NULL ,' .
24 | '`message` TEXT NOT NULL)';
25 | $db->setQuery($sql);
26 | $db->query();
27 | ?>
--------------------------------------------------------------------------------
/phirehose/OauthPhirehose.php:
--------------------------------------------------------------------------------
1 | consumerKey?$this->consumerKey:TWITTER_CONSUMER_KEY;
37 | $oauth['oauth_nonce'] = md5(uniqid(rand(), true));
38 | $oauth['oauth_signature_method'] = 'HMAC-SHA1';
39 | $oauth['oauth_timestamp'] = time();
40 | $oauth['oauth_version'] = '1.0A';
41 | $oauth['oauth_token'] = $this->username;
42 | if (isset($params['oauth_verifier']))
43 | {
44 | $oauth['oauth_verifier'] = $params['oauth_verifier'];
45 | unset($params['oauth_verifier']);
46 | }
47 | // encode all oauth values
48 | foreach ($oauth as $k => $v)
49 | $oauth[$k] = $this->encode_rfc3986($v);
50 |
51 | // encode all non '@' params
52 | // keep sigParams for signature generation (exclude '@' params)
53 | // rename '@key' to 'key'
54 | $sigParams = array();
55 | $hasFile = false;
56 | if (is_array($params))
57 | {
58 | foreach ($params as $k => $v)
59 | {
60 | if (strncmp('@', $k, 1) !== 0)
61 | {
62 | $sigParams[$k] = $this->encode_rfc3986($v);
63 | $params[$k] = $this->encode_rfc3986($v);
64 | }
65 | else
66 | {
67 | $params[substr($k, 1)] = $v;
68 | unset($params[$k]);
69 | $hasFile = true;
70 | }
71 | }
72 |
73 | if ($hasFile === true)
74 | $sigParams = array();
75 | }
76 |
77 | $sigParams = array_merge($oauth, (array) $sigParams);
78 |
79 | // sorting
80 | ksort($sigParams);
81 |
82 | // signing
83 | $oauth['oauth_signature'] = $this->encode_rfc3986($this->generateSignature($method, $url, $sigParams));
84 | return array('request' => $params, 'oauth' => $oauth);
85 | }
86 |
87 | protected function encode_rfc3986($string)
88 | {
89 | return str_replace('+', ' ', str_replace('%7E', '~', rawurlencode(($string))));
90 | }
91 |
92 | protected function generateSignature($method = null, $url = null,
93 | $params = null)
94 | {
95 | if (empty($method) || empty($url))
96 | return false;
97 |
98 | // concatenating and encode
99 | $concat = '';
100 | foreach ((array) $params as $key => $value)
101 | $concat .= "{$key}={$value}&";
102 | $concat = substr($concat, 0, -1);
103 | $concatenatedParams = $this->encode_rfc3986($concat);
104 |
105 | // normalize url
106 | $urlParts = parse_url($url);
107 | $scheme = strtolower($urlParts['scheme']);
108 | $host = strtolower($urlParts['host']);
109 | $port = isset($urlParts['port']) ? intval($urlParts['port']) : 0;
110 | $retval = strtolower($scheme) . '://' . strtolower($host);
111 | if (!empty($port) && (($scheme === 'http' && $port != 80) || ($scheme === 'https' && $port != 443)))
112 | $retval .= ":{$port}";
113 |
114 | $retval .= $urlParts['path'];
115 | if (!empty($urlParts['query']))
116 | $retval .= "?{$urlParts['query']}";
117 |
118 | $normalizedUrl = $this->encode_rfc3986($retval);
119 | $method = $this->encode_rfc3986($method); // don't need this but why not?
120 |
121 | $signatureBaseString = "{$method}&{$normalizedUrl}&{$concatenatedParams}";
122 |
123 | # sign the signature string
124 | $key = $this->encode_rfc3986($this->consumerSecret?$this->consumerSecret:TWITTER_CONSUMER_SECRET) . '&' . $this->encode_rfc3986($this->password);
125 | return base64_encode(hash_hmac('sha1', $signatureBaseString, $key, true));
126 | }
127 |
128 | protected function getOAuthHeader($method, $url, $params = array())
129 | {
130 | $params = $this->prepareParameters($method, $url, $params);
131 | $oauthHeaders = $params['oauth'];
132 |
133 | $urlParts = parse_url($url);
134 | $oauth = 'OAuth realm="",';
135 | foreach ($oauthHeaders as $name => $value)
136 | {
137 | $oauth .= "{$name}=\"{$value}\",";
138 | }
139 | $oauth = substr($oauth, 0, -1);
140 |
141 | return $oauth;
142 | }
143 |
144 | /** Overrides base class function */
145 | protected function getAuthorizationHeader($url,$requestParams)
146 | {
147 | return $this->getOAuthHeader('POST', $url, $requestParams);
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/phirehose/Phirehose.php:
--------------------------------------------------------------------------------
1 |
9 | * @version 1.0RC
10 | */
11 | abstract class Phirehose
12 | {
13 |
14 | /**
15 | * Class constants
16 | */
17 | const FORMAT_JSON = 'json';
18 | const FORMAT_XML = 'xml';
19 | const METHOD_FILTER = 'filter';
20 | const METHOD_SAMPLE = 'sample';
21 | const METHOD_RETWEET = 'retweet';
22 | const METHOD_FIREHOSE = 'firehose';
23 | const METHOD_LINKS = 'links';
24 | const METHOD_USER = 'user'; //See UserstreamPhirehose.php
25 | const METHOD_SITE = 'site'; //See UserstreamPhirehose.php
26 |
27 | const EARTH_RADIUS_KM = 6371;
28 |
29 | /**
30 | * @internal Moved from being a const to a variable, because some methods (user and site) need to change it.
31 | */
32 | protected $URL_BASE = 'https://stream.twitter.com/1.1/statuses/';
33 |
34 |
35 | /**
36 | * Member Attribs
37 | */
38 | protected $username;
39 | protected $password;
40 | protected $method;
41 | protected $format;
42 | protected $count; //Can be -150,000 to 150,000. @see http://dev.twitter.com/pages/streaming_api_methods#count
43 | protected $followIds;
44 | protected $trackWords;
45 | protected $locationBoxes;
46 | protected $conn;
47 | protected $fdrPool;
48 | protected $buff;
49 | // State vars
50 | protected $filterChanged;
51 | protected $reconnect;
52 |
53 | /**
54 | * The number of tweets received per second in previous minute; calculated fresh
55 | * just before each call to statusUpdate()
56 | * I.e. if fewer than 30 tweets in last minute then this will be zero; if 30 to 90 then it
57 | * will be 1, if 90 to 150 then 2, etc.
58 | *
59 | * @var integer
60 | */
61 | protected $statusRate;
62 |
63 | protected $lastErrorNo;
64 | protected $lastErrorMsg;
65 |
66 | /**
67 | * Number of tweets received.
68 | *
69 | * Note: by default this is the sum for last 60 seconds, and is therefore
70 | * reset every 60 seconds.
71 | * To change this behaviour write a custom statusUpdate() function.
72 | *
73 | * @var integer
74 | */
75 | protected $statusCount=0;
76 |
77 | /**
78 | * The number of calls to $this->checkFilterPredicates().
79 | *
80 | * By default it is called every 5 seconds, so if doing statusUpdates every
81 | * 60 seconds and then resetting it, this will usually be 12.
82 | *
83 | * @var integer
84 | */
85 | protected $filterCheckCount=0;
86 |
87 | /**
88 | * Total number of seconds (fractional) spent in the enqueueStatus() calls (i.e. the customized
89 | * function that handles each received tweet).
90 | *
91 | * @var float
92 | */
93 | protected $enqueueSpent=0;
94 |
95 | /**
96 | * Total number of seconds (fractional) spent in the checkFilterPredicates() calls
97 | *
98 | * @var float
99 | */
100 | protected $filterCheckSpent=0;
101 |
102 | /**
103 | * Number of seconds since the last tweet arrived (or the keep-alive newline)
104 | *
105 | * @var integer
106 | */
107 | protected $idlePeriod=0;
108 |
109 | /**
110 | * The maximum value $this->idlePeriod has reached.
111 | *
112 | * @var integer
113 | */
114 | protected $maxIdlePeriod=0;
115 |
116 | /**
117 | * Time spent on each call to enqueueStatus() (i.e. average time spent, in milliseconds,
118 | * spent processing received tweet).
119 | *
120 | * Simply: enqueueSpent divided by statusCount
121 | * Note: by default, calculated fresh for past 60 seconds, every 60 seconds.
122 | *
123 | * @var float
124 | */
125 | protected $enqueueTimeMS=0;
126 |
127 | /**
128 | * Like $enqueueTimeMS but for the checkFilterPredicates() function.
129 | * @var float
130 | */
131 | protected $filterCheckTimeMS=0;
132 |
133 | /**
134 | * Seconds since the last call to statusUpdate()
135 | *
136 | * Reset to zero after each call to statusUpdate()
137 | * Highest value it should ever reach is $this->avgPeriod
138 | *
139 | * @var integer
140 | */
141 | protected $avgElapsed=0;
142 |
143 | // Config type vars - override in subclass if desired
144 | protected $connectFailuresMax = 20;
145 | protected $connectTimeout = 5;
146 | protected $readTimeout = 5;
147 | protected $idleReconnectTimeout = 90;
148 | protected $avgPeriod = 60;
149 | protected $status_length_base = 10;
150 | protected $userAgent = 'Phirehose/1.0RC +https://github.com/fennb/phirehose';
151 | protected $filterCheckMin = 5;
152 | protected $filterUpdMin = 120;
153 | protected $tcpBackoff = 1;
154 | protected $tcpBackoffMax = 16;
155 | protected $httpBackoff = 10;
156 | protected $httpBackoffMax = 240;
157 |
158 | /**
159 | * Create a new Phirehose object attached to the appropriate twitter stream method.
160 | * Methods are: METHOD_FIREHOSE, METHOD_RETWEET, METHOD_SAMPLE, METHOD_FILTER, METHOD_LINKS, METHOD_USER, METHOD_SITE. Note: the method might cause the use of a different endpoint URL.
161 | * Formats are: FORMAT_JSON, FORMAT_XML
162 | * @see Phirehose::METHOD_SAMPLE
163 | * @see Phirehose::FORMAT_JSON
164 | *
165 | * @param string $username Any twitter username. When using oAuth, this is the 'oauth_token'.
166 | * @param string $password Any twitter password. When using oAuth this is you oAuth secret.
167 | * @param string $method
168 | * @param string $format
169 | *
170 | * @todo I've kept the "/2/" at the end of the URL for user streams, as that is what
171 | * was there before AND it works for me! But the official docs say to use /1.1/
172 | * so that is what I have used for site.
173 | * https://dev.twitter.com/docs/api/1.1/get/user
174 | *
175 | * @todo Shouldn't really hard-code URL strings in this function.
176 | */
177 | public function __construct($username, $password, $method = Phirehose::METHOD_SAMPLE, $format = self::FORMAT_JSON, $lang = FALSE)
178 | {
179 | $this->username = $username;
180 | $this->password = $password;
181 | $this->method = $method;
182 | $this->format = $format;
183 | $this->lang = $lang;
184 | switch($method){
185 | case self::METHOD_USER:$this->URL_BASE = 'https://userstream.twitter.com/2/';break;
186 | case self::METHOD_SITE:$this->URL_BASE = 'https://sitestream.twitter.com/1.1/';break;
187 | default:break; //Stick to the default
188 | }
189 | }
190 |
191 | /**
192 | * Returns public statuses from or in reply to a set of users. Mentions ("Hello @user!") and implicit replies
193 | * ("@user Hello!" created without pressing the reply button) are not matched. It is up to you to find the integer
194 | * IDs of each twitter user.
195 | * Applies to: METHOD_FILTER
196 | *
197 | * @param array $userIds Array of Twitter integer userIDs
198 | */
199 | public function setFollow($userIds)
200 | {
201 | $userIds = ($userIds === NULL) ? array() : $userIds;
202 | sort($userIds); // Non-optimal but necessary
203 | if ($this->followIds != $userIds) {
204 | $this->filterChanged = TRUE;
205 | }
206 | $this->followIds = $userIds;
207 | }
208 |
209 | /**
210 | * Returns an array of followed Twitter userIds (integers)
211 | *
212 | * @return array
213 | */
214 | public function getFollow()
215 | {
216 | return $this->followIds;
217 | }
218 |
219 | /**
220 | * Specifies keywords to track. Track keywords are case-insensitive logical ORs. Terms are exact-matched, ignoring
221 | * punctuation. Phrases, keywords with spaces, are not supported. Queries are subject to Track Limitations.
222 | * Applies to: METHOD_FILTER
223 | *
224 | * See: http://apiwiki.twitter.com/Streaming-API-Documentation#TrackLimiting
225 | *
226 | * @param array $trackWords
227 | */
228 | public function setTrack(array $trackWords)
229 | {
230 | $trackWords = ($trackWords === NULL) ? array() : $trackWords;
231 | sort($trackWords); // Non-optimal, but necessary
232 | if ($this->trackWords != $trackWords) {
233 | $this->filterChanged = TRUE;
234 | }
235 | $this->trackWords = $trackWords;
236 | }
237 |
238 | /**
239 | * Returns an array of keywords being tracked
240 | *
241 | * @return array
242 | */
243 | public function getTrack()
244 | {
245 | return $this->trackWords;
246 | }
247 |
248 | /**
249 | * Specifies a set of bounding boxes to track as an array of 4 element lon/lat pairs denoting ,
250 | * . Only tweets that are both created using the Geotagging API and are placed from within a tracked
251 | * bounding box will be included in the stream. The user's location field is not used to filter tweets. Bounding boxes
252 | * are logical ORs and must be less than or equal to 1 degree per side. A locations parameter may be combined with
253 | * track parameters, but note that all terms are logically ORd.
254 | *
255 | * NOTE: The argument order is Longitude/Latitude (to match the Twitter API and GeoJSON specifications).
256 | *
257 | * Applies to: METHOD_FILTER
258 | *
259 | * See: http://apiwiki.twitter.com/Streaming-API-Documentation#locations
260 | *
261 | * Eg:
262 | * setLocations(array(
263 | * array(-122.75, 36.8, -121.75, 37.8), // San Francisco
264 | * array(-74, 40, -73, 41), // New York
265 | * ));
266 | *
267 | * @param array $boundingBoxes
268 | */
269 | public function setLocations($boundingBoxes)
270 | {
271 | $boundingBoxes = ($boundingBoxes === NULL) ? array() : $boundingBoxes;
272 | sort($boundingBoxes); // Non-optimal, but necessary
273 | // Flatten to single dimensional array
274 | $locationBoxes = array();
275 | foreach ($boundingBoxes as $boundingBox) {
276 | // Sanity check
277 | if (count($boundingBox) != 4) {
278 | // Invalid - Not much we can do here but log error
279 | $this->log('Invalid location bounding box: [' . implode(', ', $boundingBox) . ']','error');
280 | return FALSE;
281 | }
282 | // Append this lat/lon pairs to flattened array
283 | $locationBoxes = array_merge($locationBoxes, $boundingBox);
284 | }
285 | // If it's changed, make note
286 | if ($this->locationBoxes != $locationBoxes) {
287 | $this->filterChanged = TRUE;
288 | }
289 | // Set flattened value
290 | $this->locationBoxes = $locationBoxes;
291 | }
292 |
293 | /**
294 | * Returns an array of 4 element arrays that denote the monitored location bounding boxes for tweets using the
295 | * Geotagging API.
296 | *
297 | * @see setLocations()
298 | * @return array
299 | */
300 | public function getLocations() {
301 | if ($this->locationBoxes == NULL) {
302 | return NULL;
303 | }
304 | $locationBoxes = $this->locationBoxes; // Copy array
305 | $ret = array();
306 | while (count($locationBoxes) >= 4) {
307 | $ret[] = array_splice($locationBoxes, 0, 4); // Append to ret array in blocks of 4
308 | }
309 | return $ret;
310 | }
311 |
312 | /**
313 | * Convenience method that sets location bounding boxes by an array of lon/lat/radius sets, rather than manually
314 | * specified bounding boxes. Each array element should contain 3 element subarray containing a latitude, longitude and
315 | * radius. Radius is specified in kilometers and is approximate (as boxes are square).
316 | *
317 | * NOTE: The argument order is Longitude/Latitude (to match the Twitter API and GeoJSON specifications).
318 | *
319 | * Eg:
320 | * setLocationsByCircle(array(
321 | * array(144.9631, -37.8142, 30), // Melbourne, 3km radius
322 | * array(-0.1262, 51.5001, 25), // London 10km radius
323 | * ));
324 | *
325 | *
326 | * @see setLocations()
327 | * @param array
328 | */
329 | public function setLocationsByCircle($locations) {
330 | $boundingBoxes = array();
331 | foreach ($locations as $locTriplet) {
332 | // Sanity check
333 | if (count($locTriplet) != 3) {
334 | // Invalid - Not much we can do here but log error
335 | $this->log('Invalid location triplet for ' . __METHOD__ . ': [' . implode(', ', $locTriplet) . ']','error');
336 | return FALSE;
337 | }
338 | list($lon, $lat, $radius) = $locTriplet;
339 |
340 | // Calc bounding boxes
341 | $maxLat = round($lat + rad2deg($radius / self::EARTH_RADIUS_KM), 2);
342 | $minLat = round($lat - rad2deg($radius / self::EARTH_RADIUS_KM), 2);
343 | // Compensate for degrees longitude getting smaller with increasing latitude
344 | $maxLon = round($lon + rad2deg($radius / self::EARTH_RADIUS_KM / cos(deg2rad($lat))), 2);
345 | $minLon = round($lon - rad2deg($radius / self::EARTH_RADIUS_KM / cos(deg2rad($lat))), 2);
346 | // Add to bounding box array
347 | $boundingBoxes[] = array($minLon, $minLat, $maxLon, $maxLat);
348 | // Debugging is handy
349 | $this->log('Resolved location circle [' . $lon . ', ' . $lat . ', r: ' . $radius . '] -> bbox: [' . $minLon .
350 | ', ' . $minLat . ', ' . $maxLon . ', ' . $maxLat . ']');
351 | }
352 | // Set by bounding boxes
353 | $this->setLocations($boundingBoxes);
354 | }
355 |
356 | /**
357 | * Sets the number of previous statuses to stream before transitioning to the live stream. Applies only to firehose
358 | * and filter + track methods. This is generally used internally and should not be needed by client applications.
359 | * Applies to: METHOD_FILTER, METHOD_FIREHOSE, METHOD_LINKS
360 | *
361 | * @param integer $count
362 | */
363 | public function setCount($count)
364 | {
365 | $this->count = $count;
366 | }
367 |
368 | /**
369 | * Restricts tweets to the given language, given by an ISO 639-1 code (http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
370 | *
371 | * @param string $lang
372 | */
373 | public function setLang($lang)
374 | {
375 | $this->lang = $lang;
376 | }
377 |
378 | /**
379 | * Returns the ISO 639-1 code formatted language string of the current setting. (http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
380 | *
381 | * @param string $lang
382 | */
383 | public function getLang()
384 | {
385 | return $this->lang;
386 | }
387 |
388 | /**
389 | * Connects to the stream API and consumes the stream. Each status update in the stream will cause a call to the
390 | * handleStatus() method.
391 | *
392 | * Note: in normal use this function does not return.
393 | * If you pass $reconnect as false, it will still not return in normal use: it will only return
394 | * if the remote side (Twitter) close the socket. (Or the socket dies for some other external reason.)
395 | *
396 | * @see handleStatus()
397 | * @param boolean $reconnect Reconnects as per recommended
398 | * @throws ErrorException
399 | */
400 | public function consume($reconnect = TRUE)
401 | {
402 | // Persist connection?
403 | $this->reconnect = $reconnect;
404 |
405 | // Loop indefinitely based on reconnect
406 | do {
407 |
408 | // (Re)connect
409 | $this->reconnect();
410 |
411 | // Init state
412 | $lastAverage = $lastFilterCheck = $lastFilterUpd = $lastStreamActivity = time();
413 | $fdw = $fde = NULL; // Placeholder write/error file descriptors for stream_select
414 |
415 | // We use a blocking-select with timeout, to allow us to continue processing on idle streams
416 | //TODO: there is a bug lurking here. If $this->conn is fine, but $numChanged returns zero, because readTimeout was
417 | // reached, then we should consider we still need to call statusUpdate() every 60 seconds, etc.
418 | // ($this->readTimeout is 5 seconds.) This can be quite annoying. E.g. Been getting data regularly for 55 seconds,
419 | // then it goes quiet for just 10 or so seconds. It is now 65 seconds since last call to statusUpdate() has been
420 | // called, which might mean a monitoring system kills the script assuming it has died.
421 | while ($this->conn !== NULL && !feof($this->conn) &&
422 | ($numChanged = stream_select($this->fdrPool, $fdw, $fde, $this->readTimeout)) !== FALSE) {
423 | /* Unfortunately, we need to do a safety check for dead twitter streams - This seems to be able to happen where
424 | * you end up with a valid connection, but NO tweets coming along the wire (or keep alives). The below guards
425 | * against this.
426 | */
427 | if ((time() - $lastStreamActivity) > $this->idleReconnectTimeout) {
428 | $this->log('Idle timeout: No stream activity for > ' . $this->idleReconnectTimeout . ' seconds. ' .
429 | ' Reconnecting.','info');
430 | $this->reconnect();
431 | $lastStreamActivity = time();
432 | continue;
433 | }
434 | // Process stream/buffer
435 | $this->fdrPool = array($this->conn); // Must reassign for stream_select()
436 |
437 | //Get a full HTTP chunk.
438 | //NB. This is a tight loop, not using stream_select.
439 | //NB. If that causes problems, then perhaps put something to give up after say trying for 10 seconds? (but
440 | // the stream will be all messed up, so will need to do a reconnect).
441 | $chunk_info=trim(fgets($this->conn)); //First line is hex digits giving us the length
442 | if($chunk_info=='')continue; //Usually indicates a time-out. If we wanted to be sure,
443 | //then stream_get_meta_data($this->conn)['timed_out']==1. (We could instead
444 | // look at the 'eof' member, which appears to be boolean false if just a time-out.)
445 | //TODO: need to consider calling statusUpdate() every 60 seconds, etc.
446 |
447 | // Track maximum idle period
448 | // (We got start of an HTTP chunk, this is stream activity)
449 | $this->idlePeriod = (time() - $lastStreamActivity);
450 | $this->maxIdlePeriod = ($this->idlePeriod > $this->maxIdlePeriod) ? $this->idlePeriod : $this->maxIdlePeriod;
451 | $lastStreamActivity = time();
452 |
453 | //Append one HTTP chunk to $this->buff
454 | $len=hexdec($chunk_info); //$len includes the \r\n at the end of the chunk (despite what wikipedia says)
455 | //TODO: could do a check for data corruption here. E.g. if($len>100000){...}
456 | $s='';
457 | $len+=2; //For the \r\n at the end of the chunk
458 | while(!feof($this->conn)){
459 | $s.=fread($this->conn,$len-strlen($s));
460 | if(strlen($s)>=$len)break; //TODO: Can never be >$len, only ==$len??
461 | }
462 | $this->buff.=substr($s,0,-2); //This is our HTTP chunk
463 |
464 | //Process each full tweet inside $this->buff
465 | while(1){
466 | $eol = strpos($this->buff,"\r\n"); //Find next line ending
467 | if($eol===0) { // if 0, then buffer starts with "\r\n", so trim it and loop again
468 | $this->buff = substr($this->buff,$eol+2); // remove the "\r\n" from line start
469 | continue; // loop again
470 | }
471 | if($eol===false)break; //Time to get more data
472 | $enqueueStart = microtime(TRUE);
473 | $this->enqueueStatus(substr($this->buff,0,$eol));
474 | $this->enqueueSpent += (microtime(TRUE) - $enqueueStart);
475 | $this->statusCount++;
476 | $this->buff = substr($this->buff,$eol+2); //+2 to allow for the \r\n
477 | }
478 |
479 | //NOTE: if $this->buff is not empty, it is tempting to go round and get the next HTTP chunk, as
480 | // we know there is data on the incoming stream. However, this could mean the below functions (heartbeat
481 | // and statusUpdate) *never* get called, which would be bad.
482 |
483 | // Calc counter averages
484 | $this->avgElapsed = time() - $lastAverage;
485 | if ($this->avgElapsed >= $this->avgPeriod) {
486 | $this->statusRate = round($this->statusCount / $this->avgElapsed, 0); // Calc tweets-per-second
487 | // Calc time spent per enqueue in ms
488 | $this->enqueueTimeMS = ($this->statusCount > 0) ?
489 | round($this->enqueueSpent / $this->statusCount * 1000, 2) : 0;
490 | // Calc time spent total in filter predicate checking
491 | $this->filterCheckTimeMS = ($this->filterCheckCount > 0) ?
492 | round($this->filterCheckSpent / $this->filterCheckCount * 1000, 2) : 0;
493 |
494 | $this->heartbeat();
495 | $this->statusUpdate();
496 | $lastAverage = time();
497 | }
498 | // Check if we're ready to check filter predicates
499 | if ($this->method == self::METHOD_FILTER && (time() - $lastFilterCheck) >= $this->filterCheckMin) {
500 | $this->filterCheckCount++;
501 | $lastFilterCheck = time();
502 | $filterCheckStart = microtime(TRUE);
503 | $this->checkFilterPredicates(); // This should be implemented in subclass if required
504 | $this->filterCheckSpent += (microtime(TRUE) - $filterCheckStart);
505 | }
506 | // Check if filter is ready + allowed to be updated (reconnect)
507 | if ($this->filterChanged == TRUE && (time() - $lastFilterUpd) >= $this->filterUpdMin) {
508 | $this->log('Reconnecting due to changed filter predicates.','info');
509 | $this->reconnect();
510 | $lastFilterUpd = time();
511 | }
512 |
513 | } // End while-stream-activity
514 |
515 | if (function_exists('pcntl_signal_dispatch')) {
516 | pcntl_signal_dispatch();
517 | }
518 |
519 | // Some sort of socket error has occured
520 | $this->lastErrorNo = is_resource($this->conn) ? @socket_last_error($this->conn) : NULL;
521 | $this->lastErrorMsg = ($this->lastErrorNo > 0) ? @socket_strerror($this->lastErrorNo) : 'Socket disconnected';
522 | $this->log('Phirehose connection error occured: ' . $this->lastErrorMsg,'error');
523 |
524 | // Reconnect
525 | } while ($this->reconnect);
526 |
527 | // Exit
528 | $this->log('Exiting.');
529 |
530 | }
531 |
532 |
533 | /**
534 | * Called every $this->avgPeriod (default=60) seconds, and this default implementation
535 | * calculates some rates, logs them, and resets the counters.
536 | */
537 | protected function statusUpdate()
538 | {
539 | $this->log('Consume rate: ' . $this->statusRate . ' status/sec (' . $this->statusCount . ' total), avg ' .
540 | 'enqueueStatus(): ' . $this->enqueueTimeMS . 'ms, avg checkFilterPredicates(): ' . $this->filterCheckTimeMS . 'ms (' .
541 | $this->filterCheckCount . ' total) over ' . $this->avgElapsed . ' seconds, max stream idle period: ' .
542 | $this->maxIdlePeriod . ' seconds.');
543 | // Reset
544 | $this->statusCount = $this->filterCheckCount = $this->enqueueSpent = 0;
545 | $this->filterCheckSpent = $this->idlePeriod = $this->maxIdlePeriod = 0;
546 | }
547 |
548 | /**
549 | * Returns the last error message (TCP or HTTP) that occured with the streaming API or client. State is cleared upon
550 | * successful reconnect
551 | * @return string
552 | */
553 | public function getLastErrorMsg()
554 | {
555 | return $this->lastErrorMsg;
556 | }
557 |
558 | /**
559 | * Returns the last error number that occured with the streaming API or client. Numbers correspond to either the
560 | * fsockopen() error states (in the case of TCP errors) or HTTP error codes from Twitter (in the case of HTTP errors).
561 | *
562 | * State is cleared upon successful reconnect.
563 | *
564 | * @return string
565 | */
566 | public function getLastErrorNo()
567 | {
568 | return $this->lastErrorNo;
569 | }
570 |
571 |
572 | /**
573 | * Connects to the stream URL using the configured method.
574 | * @throws ErrorException
575 | */
576 | protected function connect()
577 | {
578 |
579 | // Init state
580 | $connectFailures = 0;
581 | $tcpRetry = $this->tcpBackoff / 2;
582 | $httpRetry = $this->httpBackoff / 2;
583 |
584 | // Keep trying until connected (or max connect failures exceeded)
585 | do {
586 |
587 | // Check filter predicates for every connect (for filter method)
588 | if ($this->method == self::METHOD_FILTER) {
589 | $this->checkFilterPredicates();
590 | }
591 |
592 | // Construct URL/HTTP bits
593 | $url = $this->URL_BASE . $this->method . '.' . $this->format;
594 | $urlParts = parse_url($url);
595 |
596 | // Setup params appropriately
597 | $requestParams=array();
598 |
599 | //$requestParams['delimited'] = 'length'; //No, we don't want this any more
600 |
601 | // Setup the language of the stream
602 | if($this->lang) {
603 | $requestParams['language'] = $this->lang;
604 | }
605 |
606 | // Filter takes additional parameters
607 | if ($this->method == self::METHOD_FILTER && count($this->trackWords) > 0) {
608 | $requestParams['track'] = implode(',', $this->trackWords);
609 | }
610 | if ( ($this->method == self::METHOD_FILTER || $this->method == self::METHOD_SITE)
611 | && count($this->followIds) > 0) {
612 | $requestParams['follow'] = implode(',', $this->followIds);
613 | }
614 | if ($this->method == self::METHOD_FILTER && count($this->locationBoxes) > 0) {
615 | $requestParams['locations'] = implode(',', $this->locationBoxes);
616 | }
617 | if ($this->count <> 0) {
618 | $requestParams['count'] = $this->count;
619 | }
620 |
621 | // Debugging is useful
622 | $this->log('Connecting to twitter stream: ' . $url . ' with params: ' . str_replace("\n", '',
623 | var_export($requestParams, TRUE)));
624 |
625 | /**
626 | * Open socket connection to make POST request. It'd be nice to use stream_context_create with the native
627 | * HTTP transport but it hides/abstracts too many required bits (like HTTP error responses).
628 | */
629 | $errNo = $errStr = NULL;
630 | $scheme = ($urlParts['scheme'] == 'https') ? 'ssl://' : 'tcp://';
631 | $port = ($urlParts['scheme'] == 'https') ? 443 : 80;
632 |
633 | /**
634 | * We must perform manual host resolution here as Twitter's IP regularly rotates (ie: DNS TTL of 60 seconds) and
635 | * PHP appears to cache it the result if in a long running process (as per Phirehose).
636 | */
637 | $streamIPs = gethostbynamel($urlParts['host']);
638 | if(empty($streamIPs)) {
639 | throw new PhirehoseNetworkException("Unable to resolve hostname: '" . $urlParts['host'] . '"');
640 | }
641 |
642 | // Choose one randomly (if more than one)
643 | $this->log('Resolved host ' . $urlParts['host'] . ' to ' . implode(', ', $streamIPs));
644 | $streamIP = $streamIPs[rand(0, (count($streamIPs) - 1))];
645 | $this->log("Connecting to {$scheme}{$streamIP}, port={$port}, connectTimeout={$this->connectTimeout}");
646 |
647 | @$this->conn = fsockopen($scheme . $streamIP, $port, $errNo, $errStr, $this->connectTimeout);
648 |
649 | // No go - handle errors/backoff
650 | if (!$this->conn || !is_resource($this->conn)) {
651 | $this->lastErrorMsg = $errStr;
652 | $this->lastErrorNo = $errNo;
653 | $connectFailures++;
654 | if ($connectFailures > $this->connectFailuresMax) {
655 | $msg = 'TCP failure limit exceeded with ' . $connectFailures . ' failures. Last error: ' . $errStr;
656 | $this->log($msg,'error');
657 | throw new PhirehoseConnectLimitExceeded($msg, $errNo); // Throw an exception for other code to handle
658 | }
659 | // Increase retry/backoff up to max
660 | $tcpRetry = ($tcpRetry < $this->tcpBackoffMax) ? $tcpRetry * 2 : $this->tcpBackoffMax;
661 | $this->log('TCP failure ' . $connectFailures . ' of ' . $this->connectFailuresMax . ' connecting to stream: ' .
662 | $errStr . ' (' . $errNo . '). Sleeping for ' . $tcpRetry . ' seconds.','info');
663 | sleep($tcpRetry);
664 | continue;
665 | }
666 |
667 | // TCP connect OK, clear last error (if present)
668 | $this->log('Connection established to ' . $streamIP);
669 | $this->lastErrorMsg = NULL;
670 | $this->lastErrorNo = NULL;
671 |
672 | // If we have a socket connection, we can attempt a HTTP request - Ensure blocking read for the moment
673 | stream_set_blocking($this->conn, 1);
674 |
675 | // Encode request data
676 | $postData = http_build_query($requestParams, NULL, '&');
677 | $postData = str_replace('+','%20',$postData); //Change it from RFC1738 to RFC3986 (see
678 | //enc_type parameter in http://php.net/http_build_query and note that enc_type is
679 | //not available as of php 5.3)
680 | $authCredentials = $this->getAuthorizationHeader($url,$requestParams);
681 |
682 | // Do it
683 | $s = "POST " . $urlParts['path'] . " HTTP/1.1\r\n";
684 | $s.= "Host: " . $urlParts['host'] . ':' . $port . "\r\n";
685 | $s .= "Connection: Close\r\n";
686 | $s.= "Content-type: application/x-www-form-urlencoded\r\n";
687 | $s.= "Content-length: " . strlen($postData) . "\r\n";
688 | $s.= "Accept: */*\r\n";
689 | $s.= 'Authorization: ' . $authCredentials . "\r\n";
690 | $s.= 'User-Agent: ' . $this->userAgent . "\r\n";
691 | $s.= "\r\n";
692 | $s.= $postData . "\r\n";
693 | $s.= "\r\n";
694 |
695 | fwrite($this->conn, $s);
696 | $this->log($s);
697 |
698 | // First line is response
699 | list($httpVer, $httpCode, $httpMessage) = preg_split('/\s+/', trim(fgets($this->conn, 1024)), 3);
700 |
701 | // Response buffers
702 | $respHeaders = $respBody = '';
703 | $isChunking = false;
704 |
705 | // Consume each header response line until we get to body
706 | while ($hLine = trim(fgets($this->conn, 4096))) {
707 | $respHeaders .= $hLine."\n";
708 | if($hLine=='Transfer-Encoding: chunked')$isChunking=true;
709 | }
710 |
711 | // If we got a non-200 response, we need to backoff and retry
712 | if ($httpCode != 200) {
713 | $connectFailures++;
714 |
715 | // Twitter will disconnect on error, but we want to consume the rest of the response body (which is useful)
716 | //TODO: this might be chunked too? In which case this contains some bad characters??
717 | while ($bLine = trim(fgets($this->conn, 4096))) {
718 | $respBody .= $bLine;
719 | }
720 |
721 | // Construct error
722 | $errStr = 'HTTP ERROR ' . $httpCode . ': ' . $httpMessage . ' (' . $respBody . ')';
723 |
724 | // Set last error state
725 | $this->lastErrorMsg = $errStr;
726 | $this->lastErrorNo = $httpCode;
727 |
728 | // Have we exceeded maximum failures?
729 | if ($connectFailures > $this->connectFailuresMax) {
730 | $msg = 'Connection failure limit exceeded with ' . $connectFailures . ' failures. Last error: ' . $errStr;
731 | $this->log($msg,'error');
732 | throw new PhirehoseConnectLimitExceeded($msg, $httpCode); // We eventually throw an exception for other code to handle
733 | }
734 | // Increase retry/backoff up to max
735 | $httpRetry = ($httpRetry < $this->httpBackoffMax) ? $httpRetry * 2 : $this->httpBackoffMax;
736 | $this->log('HTTP failure ' . $connectFailures . ' of ' . $this->connectFailuresMax . ' connecting to stream: ' .
737 | $errStr . '. Sleeping for ' . $httpRetry . ' seconds.','info');
738 | sleep($httpRetry);
739 | continue;
740 |
741 | } // End if not http 200
742 | else{
743 | if(!$isChunking)throw new Exception("Twitter did not send a chunking header. Is this really HTTP/1.1? Here are headers:\n$respHeaders"); //TODO: rather crude!
744 | }
745 |
746 | // Loop until connected OK
747 | } while (!is_resource($this->conn) || $httpCode != 200);
748 |
749 | // Connected OK, reset connect failures
750 | $connectFailures = 0;
751 | $this->lastErrorMsg = NULL;
752 | $this->lastErrorNo = NULL;
753 |
754 | // Switch to non-blocking to consume the stream (important)
755 | stream_set_blocking($this->conn, 0);
756 |
757 | // Connect always causes the filterChanged status to be cleared
758 | $this->filterChanged = FALSE;
759 |
760 | // Flush stream buffer & (re)assign fdrPool (for reconnect)
761 | $this->fdrPool = array($this->conn);
762 | $this->buff = '';
763 |
764 | }
765 |
766 | protected function getAuthorizationHeader($url,$requestParams)
767 | {
768 | throw new Exception("Basic auth no longer works with Twitter. You must derive from OauthPhirehose, not directly from the Phirehose class.");
769 | $authCredentials = base64_encode($this->username . ':' . $this->password);
770 | return "Basic: ".$authCredentials;
771 | }
772 |
773 | /**
774 | * Method called as frequently as practical (every 5+ seconds) that is responsible for checking if filter predicates
775 | * (ie: track words or follow IDs) have changed. If they have, they should be set using the setTrack() and setFollow()
776 | * methods respectively within the overridden implementation.
777 | *
778 | * Note that even if predicates are changed every 5 seconds, an actual reconnect will not happen more frequently than
779 | * every 2 minutes (as per Twitter Streaming API documentation).
780 | *
781 | * Note also that this method is called upon every connect attempt, so if your predicates are causing connection
782 | * errors, they should be checked here and corrected.
783 | *
784 | * This should be implemented/overridden in any subclass implementing the FILTER method.
785 | *
786 | * @see setTrack()
787 | * @see setFollow()
788 | * @see Phirehose::METHOD_FILTER
789 | */
790 | protected function checkFilterPredicates()
791 | {
792 | // Override in subclass
793 | }
794 |
795 | /**
796 | * Basic log function that outputs logging to the standard error_log() handler. This should generally be overridden
797 | * to suit the application environment.
798 | *
799 | * @see error_log()
800 | * @param string $messages
801 | * @param String $level 'error', 'info', 'notice'. Defaults to 'notice', so you should set this
802 | * parameter on the more important error messages.
803 | * 'info' is used for problems that the class should be able to recover from automatically.
804 | * 'error' is for exceptional conditions that may need human intervention. (For instance, emailing
805 | * them to a system administrator may make sense.)
806 | */
807 | protected function log($message,$level='notice')
808 | {
809 | @error_log('Phirehose: ' . $message, 0);
810 | }
811 |
812 | /**
813 | * Performs forcible disconnect from stream (if connected) and cleanup.
814 | */
815 | protected function disconnect()
816 | {
817 | if (is_resource($this->conn)) {
818 | $this->log('Closing Phirehose connection.');
819 | fclose($this->conn);
820 | }
821 | $this->conn = NULL;
822 | $this->reconnect = FALSE;
823 | }
824 |
825 | /**
826 | * Reconnects as quickly as possible. Should be called whenever a reconnect is required rather that connect/disconnect
827 | * to preserve streams reconnect state
828 | */
829 | private function reconnect()
830 | {
831 | $reconnect = $this->reconnect;
832 | $this->disconnect(); // Implicitly sets reconnect to FALSE
833 | $this->reconnect = $reconnect; // Restore state to prev
834 | $this->connect();
835 | }
836 |
837 | /**
838 | * This is the one and only method that must be implemented additionally. As per the streaming API documentation,
839 | * statuses should NOT be processed within the same process that is performing collection
840 | *
841 | * @param string $status
842 | */
843 | abstract public function enqueueStatus($status);
844 |
845 | /**
846 | * Reports a periodic heartbeat. Keep execution time minimal.
847 | *
848 | * @return NULL
849 | */
850 | public function heartbeat() {}
851 |
852 | } // End of class
853 |
854 | class PhirehoseException extends Exception {}
855 | class PhirehoseNetworkException extends PhirehoseException {}
856 | class PhirehoseConnectLimitExceeded extends PhirehoseException {}
857 |
--------------------------------------------------------------------------------
/phirehose/UserstreamPhirehose.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | name
5 | active
6 | last_run
7 | next_run
8 | total_run_count
9 |
10 | MyAction
11 | 1
12 | 2011-04-22 12:30:00
13 | 2011-04-22 13:31:00
14 | 45
15 |
16 |
17 | OtherAction
18 | 0
19 | 2011-04-01 00:00:01
20 | 2011-04-02 12:34:56
21 | 1
22 |
23 |
24 |
25 | postid
26 | text
27 | user
28 | date_saved
29 | modeled
30 |
31 | 0
32 | Modeled post
33 | some1
34 | 2011-04-21 10:31:54
35 | 1
36 |
37 |
38 | 1
39 | This is a random-ass twitter post
40 | derpderp
41 | 2011-04-22 12:30:31
42 | 0
43 |
44 |
45 | 2
46 | @derpderp: sup mr random-ass bs
47 | orly
48 | 2011-04-22 12:45:12
49 | 0
50 |
51 |
52 | 3
53 | RT @orly this guy is a dumbass
54 | derpderp
55 | 2011-04-22 13:03:34
56 | 0
57 |
58 |
59 | 4
60 | @derpderp your bs posts are dumbass haha :)
61 | orly
62 | 2011-04-22 16:46:23
63 | 0
64 |
65 |
66 | 5
67 | @orly don't :) at me
68 | derpderp
69 | 2011-04-22 19:16:36
70 | 0
71 |
72 |
73 |
74 | eventid
75 | eventtime
76 | message
77 |
78 | 0
79 | 2011-04-22 13:51:36
80 | This is a test message.
81 |
82 |
83 |
--------------------------------------------------------------------------------
/phpunit/tests/DatabaseTest.php:
--------------------------------------------------------------------------------
1 | createXMLDataSet('..' . DIRECTORY_SEPARATOR . 'data' .
15 | DIRECTORY_SEPARATOR . 'testDelayedMarkovPostAction.xml');
16 | }
17 |
18 | }
19 |
20 | ?>
--------------------------------------------------------------------------------
/phpunit/tests/MarkovTest.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/run.php:
--------------------------------------------------------------------------------
1 | 2 || in_array('-h', $argv) || in_array('--help', $argv)) {
12 | die(printHelp());
13 | }
14 |
15 | if (in_array('--stop', $argv)) {
16 | // stop has been set! try to kill it
17 | echo "\n" . '==SHUTTING DOWN TWITTERBOT==' . "\n";
18 | halt();
19 | exit;
20 | }
21 |
22 | if (in_array('--tests-only', $argv)) {
23 | test();
24 | exit;
25 | }
26 |
27 | if (!in_array('--tests-skip', $argv)) {
28 | test();
29 | }
30 |
31 | echo "\n" . '==POWERING UP TWITTERBOT==' . "\n";
32 |
33 | // gain the lockfile
34 | $fp = @fopen(TWITTERBOT_LOCKFILE, "a");
35 | if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
36 | die("Failed to acquire lock. Twitterbot may already be running.\n");
37 | }
38 |
39 | // this bit guarantees that the process is 1) detached, and 2) independent
40 | echo 'Forking process into a daemon. Goodbye :)' . "\n";
41 | if (pcntl_fork()) {
42 | exit;
43 | }
44 | posix_setsid();
45 | if (pcntl_fork()) {
46 | exit;
47 | }
48 |
49 | // write the PIDs
50 | fwrite($fp, getmypid() . "\n");
51 | fflush($fp);
52 |
53 | // start the loop
54 | $engine = new Twitterbot();
55 | $engine->loop();
56 |
57 | // reaching this point means an exit command has been issued
58 | flock($fp, LOCK_UN);
59 | fclose($fp);
60 | @unlink(TWITTERBOT_LOCKFILE);
61 |
62 | /**
63 | * Runs tests against the configuration of this bot, to make sure everything
64 | * is in working order.
65 | */
66 | function test() {
67 | // test the database connection parameters
68 | echo "\n" . '==TESTING TWITTERBOT==' . "\n";
69 | echo 'Testing MySQL connection...';
70 | $db = @mysql_connect(DB_HOST, DB_USER, DB_PASS);
71 | if (!$db) {
72 | die('ERROR: Invalid MySQL connection parameters: ' . mysql_error() . "\n");
73 | }
74 | echo 'passed!' . "\n";
75 | mysql_close($db);
76 |
77 | // test the the curl libraries are installed
78 | echo 'Testing that you have curl installed...';
79 | if (!function_exists('curl_init')) {
80 | die('ERROR: You do not seem to have a curl library for PHP. Please '.
81 | 'check your PHP configuration.');
82 | }
83 | echo 'passed!' . "\n";
84 |
85 | // test Twitter OAuth settings
86 | echo 'Testing OAuth credentials...';
87 | $obj = new TwitterOAuth(TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, OAUTH_TOKEN,
88 | OAUTH_TOKEN_SECRET);
89 | $retval = $obj->get('account/verify_credentials');
90 | if (!is_object($retval) || !isset($retval->name)) {
91 | die('ERROR: Unable to successfully establish an OAuth connection with' .
92 | ' Twitter.' . "\n");
93 | }
94 | if ($retval->screen_name != BOT_ACCOUNT) {
95 | die('ERROR: The BOT_ACCOUNT indicated in configuration differs from' .
96 | ' what the Twitter API said.' . "\n");
97 | } else {
98 | echo 'passed!' . "\n";
99 | }
100 |
101 | // test the actions listed in the configuration file
102 | global $actions;
103 | foreach ($actions as $action) {
104 | echo 'Found action "' . $action['name'] . '", checking required fields...';
105 | if (isset($action['name']) && isset($action['file']) &&
106 | isset($action['class']) && isset($action['active'])) {
107 | echo "passed!\n";
108 | } else {
109 | die('ERROR: One or more required fields are missing in your ' .
110 | 'config.php for an action.' . "\n");
111 | }
112 | echo 'Checking instantiability of ' . $action['class'] . '...';
113 | if (!file_exists(ACTIONS . $action['file'])) {
114 | die('ERROR: Unable to find class file for the custom Action.' . "\n");
115 | }
116 | include_once(ACTIONS . $action['file']);
117 | $class = new ReflectionClass($action['class']);
118 | if (!$class->isInstantiable()) {
119 | die('ERROR: Unable to instantiate class ' . $action['class'] . ".\n");
120 | }
121 | echo 'passed!' . "\n";
122 | }
123 |
124 | // finally, test a few PHP dependencies
125 | echo 'Looking for pcntl_fork()...';
126 | if (!function_exists('pcntl_fork')) {
127 | die('ERROR: pcntl_fork() is undefined. Please check your PHP ' .
128 | 'configuration.' . "\n");
129 | }
130 | echo 'passed!' . "\n";
131 | }
132 |
133 | /**
134 | * Constructs a string with the help information for running this program.
135 | * @return The string
136 | */
137 | function printHelp() {
138 | $retval = "Twitterbot, v2.0\n\n" .
139 | "php run.php [--start | --stop | --tests-only | --tests-skip " .
140 | "| --help ]\n\n" .
141 | "--start\t\t\tStart the twitterbot daemon\n" .
142 | "--stop\t\t\tStop the twitterbot daemon\n" .
143 | "--tests-only\t\tExecute the pre-process tests and exit\n" .
144 | "--tests-skip\t\tDon't run any tests before launching the daemon\n" .
145 | "--help or -h\t\tPrints this help\n";
146 | return $retval;
147 | }
148 |
149 | /**
150 | * This function attempts to gracefully shut down the program.
151 | */
152 | function halt() {
153 | // first, read the PID from the lockfile
154 | echo 'Reading the lockfile...' . "\n";
155 | $contents = @file(TWITTERBOT_LOCKFILE);
156 | if (!$contents) {
157 | die('ERROR: Failed to acquire lock. Twitterbot may not be running.' . "\n");
158 | }
159 | $pid = intval($contents[0]);
160 | echo 'Found process with id ' . $pid . ', killing...' . "\n";
161 | // next, send a kill process; the handlers will take care of it from there
162 | posix_kill($pid, SIGTERM);
163 | echo 'Shutdown complete!' . "\n";
164 | }
165 |
166 | ?>
167 |
--------------------------------------------------------------------------------
/storage.php:
--------------------------------------------------------------------------------
1 | db = @mysql_connect($host, $user, $pass);
38 | if (!$this->db) {
39 | die('ERROR: Unable to access the database. ' . mysql_error() . "\n");
40 | }
41 | mysql_select_db($name);
42 | $this->query = 'SHOW TABLES';
43 | }
44 |
45 | /**
46 | * Destructor
47 | */
48 | public function __destruct() {
49 | mysql_close($this->db);
50 | }
51 |
52 | /**
53 | * Sets the current query string.
54 | * @param string $query
55 | */
56 | public function setQuery($query) {
57 | $this->query = $query;
58 | }
59 |
60 | /**
61 | * Executes the set query string and returns any pertinant results.
62 | * @return An array of the results, or number of updated tables.
63 | */
64 | public function query() {
65 | // first, perform the query
66 | $result = mysql_query($this->query);
67 |
68 | // what was the return value?
69 | if ($result === false) {
70 | // error!
71 | die('ERROR: MySQL query failed. ' . mysql_error() . "\n---\n" .
72 | $this->query . "\n");
73 | } else if ($result === true) {
74 | // update, insert, delete, etc statement, return the number of
75 | // affected rows
76 | return mysql_affected_rows();
77 | } else {
78 | // select statement, return the results
79 | $retval = array();
80 | while ($row = mysql_fetch_array($result)) {
81 | $retval[] = $row;
82 | }
83 | return $retval;
84 | }
85 | }
86 |
87 | /**
88 | * Retrieves a specific number of twitter posts stored in the table. A nice
89 | * utility function, given this will probably make up most of the database
90 | * accesses.
91 | *
92 | * @param boolean $unmodeled If true, this returns the $number most recent
93 | * posts marked as unmodeled. If false, this returns the $number most
94 | * recent posts (regardless of their modeled/unmodeled state).
95 | * @param int $number The number of posts to retrieve. If this is not
96 | * specified (or set to be <= 0), all posts are retrieved (with respect
97 | * to the $unmodeled parameter).
98 | * @return array List of twitter posts.
99 | */
100 | public function getPosts($unmodeled, $number = 0) {
101 | $query = 'SELECT `text`, `date_saved` FROM `' . DB_NAME . '`.`' .
102 | POST_TABLE . '`' . (isset($unmodeled) && $unmodeled ? ' WHERE ' .
103 | '`modeled` = 0' : '') . ' ORDER BY `date_saved` DESC' . ($number > 0 ?
104 | ' LIMIT ' . $number : '');
105 | $this->setQuery($query);
106 | return $this->query();
107 | }
108 |
109 | /**
110 | * Another utility method. This saves a post to the database.
111 | * @param string $status
112 | * @param string $user
113 | */
114 | public function savePost($status, $user) {
115 | $sql = 'INSERT INTO `' . DB_NAME . '`.`' . POST_TABLE .
116 | '` (text, user, date_saved, modeled) ' . 'VALUES ("' .
117 | mysql_real_escape_string(urldecode($status)) . '", "' .
118 | mysql_real_escape_string($user) . '", "' . date('Y-m-d H:i:s') . '", 0)';
119 | $this->setQuery($sql);
120 | $this->query();
121 | }
122 |
123 | /**
124 | * Utility method for marking a large range of saved posts as "modeled".
125 | * This works inclusively: the range of posts marked as "modeled" includes
126 | * those with the timestamps.
127 | *
128 | * @param string $old_date The oldest date post to mark as modeled.
129 | * @param string $recent_date The most recent date post.
130 | */
131 | public function markPostsModeled($old_date, $recent_date) {
132 | $sql = 'UPDATE `' . DB_NAME . '`.`' . POST_TABLE .
133 | '` SET modeled = 1 WHERE date_saved BETWEEN "' . $old_date .
134 | '" AND "' . $recent_date . '"';
135 | $this->setQuery($sql);
136 | $this->query();
137 | }
138 |
139 | /**
140 | * Utility method for saving a log entry to the database.
141 | * @param string $sender Author of the message.
142 | * @param string $message Message itself.
143 | */
144 | public function log($sender, $message) {
145 | $sql = 'INSERT INTO `' . DB_NAME . '`.`' . LOG_TABLE .
146 | '` (eventtime, message) ' . 'VALUES ("' . date('Y-m-d H:i:s') .
147 | '", "' . mysql_real_escape_string($sender . ': ' . $message) . '")';
148 | $this->setQuery($sql);
149 | $this->query();
150 | }
151 | }
152 |
153 | ?>
154 |
--------------------------------------------------------------------------------
/twitterbot.php:
--------------------------------------------------------------------------------
1 | isInstantiable()) {
35 | $item = $class->newInstance($action['name'], $action['active'],
36 | (isset($action['args']) ? $action['args'] : array()));
37 | if ($item->isActive()) {
38 | $this->actions[] = $item;
39 | }
40 | } else {
41 | die('Twitterbot: ERROR: ' . $action['name'] . ' is not instantiable!');
42 | }
43 | }
44 | $this->aggregator = new DataAggregator(OAUTH_TOKEN, OAUTH_TOKEN_SECRET,
45 | Phirehose::METHOD_SAMPLE);
46 | $this->aggregator->setLang('en'); // Set language to filter specific tweets.
47 | }
48 |
49 | /**
50 | * Sorts the actions based on which is to execute next.
51 | * @param Action $a
52 | * @param Action $b
53 | * @return -1 if $a < $b, 1 if $a > $b, 0 if $a == $b
54 | */
55 | private function nextAttemptSort($a, $b) {
56 | if ($a->getNextAttempt() == $b->getNextAttempt()) {
57 | return 0;
58 | }
59 | return ($a->getNextAttempt() < $b->getNextAttempt() ? -1 : 1);
60 | }
61 |
62 | /**
63 | * Returns the next action to fire.
64 | * @return Action The next action to fire.
65 | */
66 | private function next() {
67 | usort($this->actions, array($this, 'nextAttemptSort'));
68 | return (count($this->actions) > 0 ? $this->actions[0] : null);
69 | }
70 |
71 | /**
72 | * This is the main function of this class. This registers any needed
73 | * signal handlers, starts an infinite loop, and fires any events
74 | * as they need to be fired.
75 | */
76 | public function loop() {
77 | declare(ticks = 1);
78 |
79 | // spin off the aggregator
80 | if (($pid = pcntl_fork())) {
81 | $this->current[$pid] = $this->aggregator;
82 | } else {
83 | exit($this->aggregator->consume());
84 | }
85 |
86 | // set up signal handlers
87 | pcntl_signal(SIGCHLD, array($this, "sig_child"));
88 | pcntl_signal(SIGTERM, array($this, "sig_kill"));
89 |
90 | // now start all the other actions
91 | while (1) {
92 | // do we exit?
93 | if ($this->exit) {
94 | return;
95 | }
96 |
97 | // determine the next action that should fire
98 | $now = time();
99 | $action = $this->next();
100 | if ($action == null) {
101 | // in this case, just the aggregator is running,
102 | // so we can in fact safely quit!
103 | $this->exit = true;
104 | continue;
105 | }
106 | if ($now < $action->getNextAttempt()) {
107 | // sleep until the next action has to fire
108 | sleep($action->getNextAttempt() - $now);
109 | continue;
110 | }
111 | $action->setNextAttempt();
112 | if ($pid = pcntl_fork()) {
113 | // parent process
114 | $this->current[$pid] = $action;
115 | } else {
116 | // child process
117 | pcntl_alarm($action->getTimeout());
118 | exit($action->run());
119 | }
120 | }
121 | }
122 |
123 | /**
124 | * Signal handler for child processes that have exited via SIGCHLD.
125 | * @param int $signal
126 | */
127 | private function sig_child($signal) {
128 | $status = Action::FAILURE;
129 | while (($pid = pcntl_wait($status, WNOHANG)) > 0) {
130 | $action = $this->current[$pid];
131 | unset($this->current[$pid]);
132 | if (pcntl_wifexited($status) &&
133 | pcntl_wexitstatus($status) == Action::SUCCESS) {
134 | $status = Action::SUCCESS;
135 | }
136 | if ($action != $this->aggregator) {
137 | $action->post_run($status);
138 | } else {
139 | // the aggregator failed! this is a problem
140 | $db = Storage::getDatabase();
141 | $db->log('Twitterbot', 'Aggregator crashed! Exiting.');
142 | unset($db);
143 | exit;
144 | }
145 | }
146 | }
147 |
148 | /**
149 | * Signal handler for SIGTERM and SIGINT, conducts a graceful shutdown if
150 | * the user forcefully quits the parent process for the twitterbot.
151 | * @param int $signal
152 | */
153 | private function sig_kill($signal) {
154 | // send a kill signal to all the processes still running
155 | foreach ($this->current as $pid => $action) {
156 | // send the SIGTERM signal to the child
157 | posix_kill($pid, SIGTERM);
158 | }
159 | // set the flag to kill the parent process
160 | $this->exit = true;
161 | }
162 | }
163 |
164 | ?>
165 |
--------------------------------------------------------------------------------
/twitteroauth/OAuth.php:
--------------------------------------------------------------------------------
1 | key = $key;
18 | $this->secret = $secret;
19 | $this->callback_url = $callback_url;
20 | }
21 |
22 | function __toString() {
23 | return "OAuthConsumer[key=$this->key,secret=$this->secret]";
24 | }
25 | }
26 |
27 | class OAuthToken {
28 | // access tokens and request tokens
29 | public $key;
30 | public $secret;
31 |
32 | /**
33 | * key = the token
34 | * secret = the token secret
35 | */
36 | function __construct($key, $secret) {
37 | $this->key = $key;
38 | $this->secret = $secret;
39 | }
40 |
41 | /**
42 | * generates the basic string serialization of a token that a server
43 | * would respond to request_token and access_token calls with
44 | */
45 | function to_string() {
46 | return "oauth_token=" .
47 | OAuthUtil::urlencode_rfc3986($this->key) .
48 | "&oauth_token_secret=" .
49 | OAuthUtil::urlencode_rfc3986($this->secret);
50 | }
51 |
52 | function __toString() {
53 | return $this->to_string();
54 | }
55 | }
56 |
57 | /**
58 | * A class for implementing a Signature Method
59 | * See section 9 ("Signing Requests") in the spec
60 | */
61 | abstract class OAuthSignatureMethod {
62 | /**
63 | * Needs to return the name of the Signature Method (ie HMAC-SHA1)
64 | * @return string
65 | */
66 | abstract public function get_name();
67 |
68 | /**
69 | * Build up the signature
70 | * NOTE: The output of this function MUST NOT be urlencoded.
71 | * the encoding is handled in OAuthRequest when the final
72 | * request is serialized
73 | * @param OAuthRequest $request
74 | * @param OAuthConsumer $consumer
75 | * @param OAuthToken $token
76 | * @return string
77 | */
78 | abstract public function build_signature($request, $consumer, $token);
79 |
80 | /**
81 | * Verifies that a given signature is correct
82 | * @param OAuthRequest $request
83 | * @param OAuthConsumer $consumer
84 | * @param OAuthToken $token
85 | * @param string $signature
86 | * @return bool
87 | */
88 | public function check_signature($request, $consumer, $token, $signature) {
89 | $built = $this->build_signature($request, $consumer, $token);
90 | return $built == $signature;
91 | }
92 | }
93 |
94 | /**
95 | * The HMAC-SHA1 signature method uses the HMAC-SHA1 signature algorithm as defined in [RFC2104]
96 | * where the Signature Base String is the text and the key is the concatenated values (each first
97 | * encoded per Parameter Encoding) of the Consumer Secret and Token Secret, separated by an '&'
98 | * character (ASCII code 38) even if empty.
99 | * - Chapter 9.2 ("HMAC-SHA1")
100 | */
101 | class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {
102 | function get_name() {
103 | return "HMAC-SHA1";
104 | }
105 |
106 | public function build_signature($request, $consumer, $token) {
107 | $base_string = $request->get_signature_base_string();
108 | $request->base_string = $base_string;
109 |
110 | $key_parts = array(
111 | $consumer->secret,
112 | ($token) ? $token->secret : ""
113 | );
114 |
115 | $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
116 | $key = implode('&', $key_parts);
117 |
118 | return base64_encode(hash_hmac('sha1', $base_string, $key, true));
119 | }
120 | }
121 |
122 | /**
123 | * The PLAINTEXT method does not provide any security protection and SHOULD only be used
124 | * over a secure channel such as HTTPS. It does not use the Signature Base String.
125 | * - Chapter 9.4 ("PLAINTEXT")
126 | */
127 | class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {
128 | public function get_name() {
129 | return "PLAINTEXT";
130 | }
131 |
132 | /**
133 | * oauth_signature is set to the concatenated encoded values of the Consumer Secret and
134 | * Token Secret, separated by a '&' character (ASCII code 38), even if either secret is
135 | * empty. The result MUST be encoded again.
136 | * - Chapter 9.4.1 ("Generating Signatures")
137 | *
138 | * Please note that the second encoding MUST NOT happen in the SignatureMethod, as
139 | * OAuthRequest handles this!
140 | */
141 | public function build_signature($request, $consumer, $token) {
142 | $key_parts = array(
143 | $consumer->secret,
144 | ($token) ? $token->secret : ""
145 | );
146 |
147 | $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
148 | $key = implode('&', $key_parts);
149 | $request->base_string = $key;
150 |
151 | return $key;
152 | }
153 | }
154 |
155 | /**
156 | * The RSA-SHA1 signature method uses the RSASSA-PKCS1-v1_5 signature algorithm as defined in
157 | * [RFC3447] section 8.2 (more simply known as PKCS#1), using SHA-1 as the hash function for
158 | * EMSA-PKCS1-v1_5. It is assumed that the Consumer has provided its RSA public key in a
159 | * verified way to the Service Provider, in a manner which is beyond the scope of this
160 | * specification.
161 | * - Chapter 9.3 ("RSA-SHA1")
162 | */
163 | abstract class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {
164 | public function get_name() {
165 | return "RSA-SHA1";
166 | }
167 |
168 | // Up to the SP to implement this lookup of keys. Possible ideas are:
169 | // (1) do a lookup in a table of trusted certs keyed off of consumer
170 | // (2) fetch via http using a url provided by the requester
171 | // (3) some sort of specific discovery code based on request
172 | //
173 | // Either way should return a string representation of the certificate
174 | protected abstract function fetch_public_cert(&$request);
175 |
176 | // Up to the SP to implement this lookup of keys. Possible ideas are:
177 | // (1) do a lookup in a table of trusted certs keyed off of consumer
178 | //
179 | // Either way should return a string representation of the certificate
180 | protected abstract function fetch_private_cert(&$request);
181 |
182 | public function build_signature($request, $consumer, $token) {
183 | $base_string = $request->get_signature_base_string();
184 | $request->base_string = $base_string;
185 |
186 | // Fetch the private key cert based on the request
187 | $cert = $this->fetch_private_cert($request);
188 |
189 | // Pull the private key ID from the certificate
190 | $privatekeyid = openssl_get_privatekey($cert);
191 |
192 | // Sign using the key
193 | $ok = openssl_sign($base_string, $signature, $privatekeyid);
194 |
195 | // Release the key resource
196 | openssl_free_key($privatekeyid);
197 |
198 | return base64_encode($signature);
199 | }
200 |
201 | public function check_signature($request, $consumer, $token, $signature) {
202 | $decoded_sig = base64_decode($signature);
203 |
204 | $base_string = $request->get_signature_base_string();
205 |
206 | // Fetch the public key cert based on the request
207 | $cert = $this->fetch_public_cert($request);
208 |
209 | // Pull the public key ID from the certificate
210 | $publickeyid = openssl_get_publickey($cert);
211 |
212 | // Check the computed signature against the one passed in the query
213 | $ok = openssl_verify($base_string, $decoded_sig, $publickeyid);
214 |
215 | // Release the key resource
216 | openssl_free_key($publickeyid);
217 |
218 | return $ok == 1;
219 | }
220 | }
221 |
222 | class OAuthRequest {
223 | private $parameters;
224 | private $http_method;
225 | private $http_url;
226 | // for debug purposes
227 | public $base_string;
228 | public static $version = '1.0';
229 | public static $POST_INPUT = 'php://input';
230 |
231 | function __construct($http_method, $http_url, $parameters=NULL) {
232 | @$parameters or $parameters = array();
233 | $parameters = array_merge( OAuthUtil::parse_parameters(parse_url($http_url, PHP_URL_QUERY)), $parameters);
234 | $this->parameters = $parameters;
235 | $this->http_method = $http_method;
236 | $this->http_url = $http_url;
237 | }
238 |
239 |
240 | /**
241 | * attempt to build up a request from what was passed to the server
242 | */
243 | public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {
244 | $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on")
245 | ? 'http'
246 | : 'https';
247 | @$http_url or $http_url = $scheme .
248 | '://' . $_SERVER['HTTP_HOST'] .
249 | ':' .
250 | $_SERVER['SERVER_PORT'] .
251 | $_SERVER['REQUEST_URI'];
252 | @$http_method or $http_method = $_SERVER['REQUEST_METHOD'];
253 |
254 | // We weren't handed any parameters, so let's find the ones relevant to
255 | // this request.
256 | // If you run XML-RPC or similar you should use this to provide your own
257 | // parsed parameter-list
258 | if (!$parameters) {
259 | // Find request headers
260 | $request_headers = OAuthUtil::get_headers();
261 |
262 | // Parse the query-string to find GET parameters
263 | $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']);
264 |
265 | // It's a POST request of the proper content-type, so parse POST
266 | // parameters and add those overriding any duplicates from GET
267 | if ($http_method == "POST"
268 | && @strstr($request_headers["Content-Type"],
269 | "application/x-www-form-urlencoded")
270 | ) {
271 | $post_data = OAuthUtil::parse_parameters(
272 | file_get_contents(self::$POST_INPUT)
273 | );
274 | $parameters = array_merge($parameters, $post_data);
275 | }
276 |
277 | // We have a Authorization-header with OAuth data. Parse the header
278 | // and add those overriding any duplicates from GET or POST
279 | if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") {
280 | $header_parameters = OAuthUtil::split_header(
281 | $request_headers['Authorization']
282 | );
283 | $parameters = array_merge($parameters, $header_parameters);
284 | }
285 |
286 | }
287 |
288 | return new OAuthRequest($http_method, $http_url, $parameters);
289 | }
290 |
291 | /**
292 | * pretty much a helper function to set up the request
293 | */
294 | public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {
295 | @$parameters or $parameters = array();
296 | $defaults = array("oauth_version" => OAuthRequest::$version,
297 | "oauth_nonce" => OAuthRequest::generate_nonce(),
298 | "oauth_timestamp" => OAuthRequest::generate_timestamp(),
299 | "oauth_consumer_key" => $consumer->key);
300 | if ($token)
301 | $defaults['oauth_token'] = $token->key;
302 |
303 | $parameters = array_merge($defaults, $parameters);
304 |
305 | return new OAuthRequest($http_method, $http_url, $parameters);
306 | }
307 |
308 | public function set_parameter($name, $value, $allow_duplicates = true) {
309 | if ($allow_duplicates && isset($this->parameters[$name])) {
310 | // We have already added parameter(s) with this name, so add to the list
311 | if (is_scalar($this->parameters[$name])) {
312 | // This is the first duplicate, so transform scalar (string)
313 | // into an array so we can add the duplicates
314 | $this->parameters[$name] = array($this->parameters[$name]);
315 | }
316 |
317 | $this->parameters[$name][] = $value;
318 | } else {
319 | $this->parameters[$name] = $value;
320 | }
321 | }
322 |
323 | public function get_parameter($name) {
324 | return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
325 | }
326 |
327 | public function get_parameters() {
328 | return $this->parameters;
329 | }
330 |
331 | public function unset_parameter($name) {
332 | unset($this->parameters[$name]);
333 | }
334 |
335 | /**
336 | * The request parameters, sorted and concatenated into a normalized string.
337 | * @return string
338 | */
339 | public function get_signable_parameters() {
340 | // Grab all parameters
341 | $params = $this->parameters;
342 |
343 | // Remove oauth_signature if present
344 | // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
345 | if (isset($params['oauth_signature'])) {
346 | unset($params['oauth_signature']);
347 | }
348 |
349 | return OAuthUtil::build_http_query($params);
350 | }
351 |
352 | /**
353 | * Returns the base string of this request
354 | *
355 | * The base string defined as the method, the url
356 | * and the parameters (normalized), each urlencoded
357 | * and the concated with &.
358 | */
359 | public function get_signature_base_string() {
360 | $parts = array(
361 | $this->get_normalized_http_method(),
362 | $this->get_normalized_http_url(),
363 | $this->get_signable_parameters()
364 | );
365 |
366 | $parts = OAuthUtil::urlencode_rfc3986($parts);
367 |
368 | return implode('&', $parts);
369 | }
370 |
371 | /**
372 | * just uppercases the http method
373 | */
374 | public function get_normalized_http_method() {
375 | return strtoupper($this->http_method);
376 | }
377 |
378 | /**
379 | * parses the url and rebuilds it to be
380 | * scheme://host/path
381 | */
382 | public function get_normalized_http_url() {
383 | $parts = parse_url($this->http_url);
384 |
385 | $port = @$parts['port'];
386 | $scheme = $parts['scheme'];
387 | $host = $parts['host'];
388 | $path = @$parts['path'];
389 |
390 | $port or $port = ($scheme == 'https') ? '443' : '80';
391 |
392 | if (($scheme == 'https' && $port != '443')
393 | || ($scheme == 'http' && $port != '80')) {
394 | $host = "$host:$port";
395 | }
396 | return "$scheme://$host$path";
397 | }
398 |
399 | /**
400 | * builds a url usable for a GET request
401 | */
402 | public function to_url() {
403 | $post_data = $this->to_postdata();
404 | $out = $this->get_normalized_http_url();
405 | if ($post_data) {
406 | $out .= '?'.$post_data;
407 | }
408 | return $out;
409 | }
410 |
411 | /**
412 | * builds the data one would send in a POST request
413 | */
414 | public function to_postdata() {
415 | return OAuthUtil::build_http_query($this->parameters);
416 | }
417 |
418 | /**
419 | * builds the Authorization: header
420 | */
421 | public function to_header($realm=null) {
422 | $first = true;
423 | if($realm) {
424 | $out = 'Authorization: OAuth realm="' . OAuthUtil::urlencode_rfc3986($realm) . '"';
425 | $first = false;
426 | } else
427 | $out = 'Authorization: OAuth';
428 |
429 | $total = array();
430 | foreach ($this->parameters as $k => $v) {
431 | if (substr($k, 0, 5) != "oauth") continue;
432 | if (is_array($v)) {
433 | throw new OAuthException('Arrays not supported in headers');
434 | }
435 | $out .= ($first) ? ' ' : ',';
436 | $out .= OAuthUtil::urlencode_rfc3986($k) .
437 | '="' .
438 | OAuthUtil::urlencode_rfc3986($v) .
439 | '"';
440 | $first = false;
441 | }
442 | return $out;
443 | }
444 |
445 | public function __toString() {
446 | return $this->to_url();
447 | }
448 |
449 |
450 | public function sign_request($signature_method, $consumer, $token) {
451 | $this->set_parameter(
452 | "oauth_signature_method",
453 | $signature_method->get_name(),
454 | false
455 | );
456 | $signature = $this->build_signature($signature_method, $consumer, $token);
457 | $this->set_parameter("oauth_signature", $signature, false);
458 | }
459 |
460 | public function build_signature($signature_method, $consumer, $token) {
461 | $signature = $signature_method->build_signature($this, $consumer, $token);
462 | return $signature;
463 | }
464 |
465 | /**
466 | * util function: current timestamp
467 | */
468 | private static function generate_timestamp() {
469 | return time();
470 | }
471 |
472 | /**
473 | * util function: current nonce
474 | */
475 | private static function generate_nonce() {
476 | $mt = microtime();
477 | $rand = mt_rand();
478 |
479 | return md5($mt . $rand); // md5s look nicer than numbers
480 | }
481 | }
482 |
483 | class OAuthServer {
484 | protected $timestamp_threshold = 300; // in seconds, five minutes
485 | protected $version = '1.0'; // hi blaine
486 | protected $signature_methods = array();
487 |
488 | protected $data_store;
489 |
490 | function __construct($data_store) {
491 | $this->data_store = $data_store;
492 | }
493 |
494 | public function add_signature_method($signature_method) {
495 | $this->signature_methods[$signature_method->get_name()] =
496 | $signature_method;
497 | }
498 |
499 | // high level functions
500 |
501 | /**
502 | * process a request_token request
503 | * returns the request token on success
504 | */
505 | public function fetch_request_token(&$request) {
506 | $this->get_version($request);
507 |
508 | $consumer = $this->get_consumer($request);
509 |
510 | // no token required for the initial token request
511 | $token = NULL;
512 |
513 | $this->check_signature($request, $consumer, $token);
514 |
515 | // Rev A change
516 | $callback = $request->get_parameter('oauth_callback');
517 | $new_token = $this->data_store->new_request_token($consumer, $callback);
518 |
519 | return $new_token;
520 | }
521 |
522 | /**
523 | * process an access_token request
524 | * returns the access token on success
525 | */
526 | public function fetch_access_token(&$request) {
527 | $this->get_version($request);
528 |
529 | $consumer = $this->get_consumer($request);
530 |
531 | // requires authorized request token
532 | $token = $this->get_token($request, $consumer, "request");
533 |
534 | $this->check_signature($request, $consumer, $token);
535 |
536 | // Rev A change
537 | $verifier = $request->get_parameter('oauth_verifier');
538 | $new_token = $this->data_store->new_access_token($token, $consumer, $verifier);
539 |
540 | return $new_token;
541 | }
542 |
543 | /**
544 | * verify an api call, checks all the parameters
545 | */
546 | public function verify_request(&$request) {
547 | $this->get_version($request);
548 | $consumer = $this->get_consumer($request);
549 | $token = $this->get_token($request, $consumer, "access");
550 | $this->check_signature($request, $consumer, $token);
551 | return array($consumer, $token);
552 | }
553 |
554 | // Internals from here
555 | /**
556 | * version 1
557 | */
558 | private function get_version(&$request) {
559 | $version = $request->get_parameter("oauth_version");
560 | if (!$version) {
561 | // Service Providers MUST assume the protocol version to be 1.0 if this parameter is not present.
562 | // Chapter 7.0 ("Accessing Protected Ressources")
563 | $version = '1.0';
564 | }
565 | if ($version !== $this->version) {
566 | throw new OAuthException("OAuth version '$version' not supported");
567 | }
568 | return $version;
569 | }
570 |
571 | /**
572 | * figure out the signature with some defaults
573 | */
574 | private function get_signature_method(&$request) {
575 | $signature_method =
576 | @$request->get_parameter("oauth_signature_method");
577 |
578 | if (!$signature_method) {
579 | // According to chapter 7 ("Accessing Protected Ressources") the signature-method
580 | // parameter is required, and we can't just fallback to PLAINTEXT
581 | throw new OAuthException('No signature method parameter. This parameter is required');
582 | }
583 |
584 | if (!in_array($signature_method,
585 | array_keys($this->signature_methods))) {
586 | throw new OAuthException(
587 | "Signature method '$signature_method' not supported " .
588 | "try one of the following: " .
589 | implode(", ", array_keys($this->signature_methods))
590 | );
591 | }
592 | return $this->signature_methods[$signature_method];
593 | }
594 |
595 | /**
596 | * try to find the consumer for the provided request's consumer key
597 | */
598 | private function get_consumer(&$request) {
599 | $consumer_key = @$request->get_parameter("oauth_consumer_key");
600 | if (!$consumer_key) {
601 | throw new OAuthException("Invalid consumer key");
602 | }
603 |
604 | $consumer = $this->data_store->lookup_consumer($consumer_key);
605 | if (!$consumer) {
606 | throw new OAuthException("Invalid consumer");
607 | }
608 |
609 | return $consumer;
610 | }
611 |
612 | /**
613 | * try to find the token for the provided request's token key
614 | */
615 | private function get_token(&$request, $consumer, $token_type="access") {
616 | $token_field = @$request->get_parameter('oauth_token');
617 | $token = $this->data_store->lookup_token(
618 | $consumer, $token_type, $token_field
619 | );
620 | if (!$token) {
621 | throw new OAuthException("Invalid $token_type token: $token_field");
622 | }
623 | return $token;
624 | }
625 |
626 | /**
627 | * all-in-one function to check the signature on a request
628 | * should guess the signature method appropriately
629 | */
630 | private function check_signature(&$request, $consumer, $token) {
631 | // this should probably be in a different method
632 | $timestamp = @$request->get_parameter('oauth_timestamp');
633 | $nonce = @$request->get_parameter('oauth_nonce');
634 |
635 | $this->check_timestamp($timestamp);
636 | $this->check_nonce($consumer, $token, $nonce, $timestamp);
637 |
638 | $signature_method = $this->get_signature_method($request);
639 |
640 | $signature = $request->get_parameter('oauth_signature');
641 | $valid_sig = $signature_method->check_signature(
642 | $request,
643 | $consumer,
644 | $token,
645 | $signature
646 | );
647 |
648 | if (!$valid_sig) {
649 | throw new OAuthException("Invalid signature");
650 | }
651 | }
652 |
653 | /**
654 | * check that the timestamp is new enough
655 | */
656 | private function check_timestamp($timestamp) {
657 | if( ! $timestamp )
658 | throw new OAuthException(
659 | 'Missing timestamp parameter. The parameter is required'
660 | );
661 |
662 | // verify that timestamp is recentish
663 | $now = time();
664 | if (abs($now - $timestamp) > $this->timestamp_threshold) {
665 | throw new OAuthException(
666 | "Expired timestamp, yours $timestamp, ours $now"
667 | );
668 | }
669 | }
670 |
671 | /**
672 | * check that the nonce is not repeated
673 | */
674 | private function check_nonce($consumer, $token, $nonce, $timestamp) {
675 | if( ! $nonce )
676 | throw new OAuthException(
677 | 'Missing nonce parameter. The parameter is required'
678 | );
679 |
680 | // verify that the nonce is uniqueish
681 | $found = $this->data_store->lookup_nonce(
682 | $consumer,
683 | $token,
684 | $nonce,
685 | $timestamp
686 | );
687 | if ($found) {
688 | throw new OAuthException("Nonce already used: $nonce");
689 | }
690 | }
691 |
692 | }
693 |
694 | class OAuthDataStore {
695 | function lookup_consumer($consumer_key) {
696 | // implement me
697 | }
698 |
699 | function lookup_token($consumer, $token_type, $token) {
700 | // implement me
701 | }
702 |
703 | function lookup_nonce($consumer, $token, $nonce, $timestamp) {
704 | // implement me
705 | }
706 |
707 | function new_request_token($consumer, $callback = null) {
708 | // return a new token attached to this consumer
709 | }
710 |
711 | function new_access_token($token, $consumer, $verifier = null) {
712 | // return a new access token attached to this consumer
713 | // for the user associated with this token if the request token
714 | // is authorized
715 | // should also invalidate the request token
716 | }
717 |
718 | }
719 |
720 | class OAuthUtil {
721 | public static function urlencode_rfc3986($input) {
722 | if (is_array($input)) {
723 | return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input);
724 | } else if (is_scalar($input)) {
725 | return str_replace(
726 | '+',
727 | ' ',
728 | str_replace('%7E', '~', rawurlencode($input))
729 | );
730 | } else {
731 | return '';
732 | }
733 | }
734 |
735 |
736 | // This decode function isn't taking into consideration the above
737 | // modifications to the encoding process. However, this method doesn't
738 | // seem to be used anywhere so leaving it as is.
739 | public static function urldecode_rfc3986($string) {
740 | return urldecode($string);
741 | }
742 |
743 | // Utility function for turning the Authorization: header into
744 | // parameters, has to do some unescaping
745 | // Can filter out any non-oauth parameters if needed (default behaviour)
746 | public static function split_header($header, $only_allow_oauth_parameters = true) {
747 | $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/';
748 | $offset = 0;
749 | $params = array();
750 | while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) {
751 | $match = $matches[0];
752 | $header_name = $matches[2][0];
753 | $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0];
754 | if (preg_match('/^oauth_/', $header_name) || !$only_allow_oauth_parameters) {
755 | $params[$header_name] = OAuthUtil::urldecode_rfc3986($header_content);
756 | }
757 | $offset = $match[1] + strlen($match[0]);
758 | }
759 |
760 | if (isset($params['realm'])) {
761 | unset($params['realm']);
762 | }
763 |
764 | return $params;
765 | }
766 |
767 | // helper to try to sort out headers for people who aren't running apache
768 | public static function get_headers() {
769 | if (function_exists('apache_request_headers')) {
770 | // we need this to get the actual Authorization: header
771 | // because apache tends to tell us it doesn't exist
772 | $headers = apache_request_headers();
773 |
774 | // sanitize the output of apache_request_headers because
775 | // we always want the keys to be Cased-Like-This and arh()
776 | // returns the headers in the same case as they are in the
777 | // request
778 | $out = array();
779 | foreach( $headers AS $key => $value ) {
780 | $key = str_replace(
781 | " ",
782 | "-",
783 | ucwords(strtolower(str_replace("-", " ", $key)))
784 | );
785 | $out[$key] = $value;
786 | }
787 | } else {
788 | // otherwise we don't have apache and are just going to have to hope
789 | // that $_SERVER actually contains what we need
790 | $out = array();
791 | if( isset($_SERVER['CONTENT_TYPE']) )
792 | $out['Content-Type'] = $_SERVER['CONTENT_TYPE'];
793 | if( isset($_ENV['CONTENT_TYPE']) )
794 | $out['Content-Type'] = $_ENV['CONTENT_TYPE'];
795 |
796 | foreach ($_SERVER as $key => $value) {
797 | if (substr($key, 0, 5) == "HTTP_") {
798 | // this is chaos, basically it is just there to capitalize the first
799 | // letter of every word that is not an initial HTTP and strip HTTP
800 | // code from przemek
801 | $key = str_replace(
802 | " ",
803 | "-",
804 | ucwords(strtolower(str_replace("_", " ", substr($key, 5))))
805 | );
806 | $out[$key] = $value;
807 | }
808 | }
809 | }
810 | return $out;
811 | }
812 |
813 | // This function takes a input like a=b&a=c&d=e and returns the parsed
814 | // parameters like this
815 | // array('a' => array('b','c'), 'd' => 'e')
816 | public static function parse_parameters( $input ) {
817 | if (!isset($input) || !$input) return array();
818 |
819 | $pairs = explode('&', $input);
820 |
821 | $parsed_parameters = array();
822 | foreach ($pairs as $pair) {
823 | $split = explode('=', $pair, 2);
824 | $parameter = OAuthUtil::urldecode_rfc3986($split[0]);
825 | $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : '';
826 |
827 | if (isset($parsed_parameters[$parameter])) {
828 | // We have already recieved parameter(s) with this name, so add to the list
829 | // of parameters with this name
830 |
831 | if (is_scalar($parsed_parameters[$parameter])) {
832 | // This is the first duplicate, so transform scalar (string) into an array
833 | // so we can add the duplicates
834 | $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]);
835 | }
836 |
837 | $parsed_parameters[$parameter][] = $value;
838 | } else {
839 | $parsed_parameters[$parameter] = $value;
840 | }
841 | }
842 | return $parsed_parameters;
843 | }
844 |
845 | public static function build_http_query($params) {
846 | if (!$params) return '';
847 |
848 | // Urlencode both keys and values
849 | $keys = OAuthUtil::urlencode_rfc3986(array_keys($params));
850 | $values = OAuthUtil::urlencode_rfc3986(array_values($params));
851 | $params = array_combine($keys, $values);
852 |
853 | // Parameters are sorted by name, using lexicographical byte value ordering.
854 | // Ref: Spec: 9.1.1 (1)
855 | uksort($params, 'strcmp');
856 |
857 | $pairs = array();
858 | foreach ($params as $parameter => $value) {
859 | if (is_array($value)) {
860 | // If two or more parameters share the same name, they are sorted by their value
861 | // Ref: Spec: 9.1.1 (1)
862 | natsort($value);
863 | foreach ($value as $duplicate_value) {
864 | $pairs[] = $parameter . '=' . $duplicate_value;
865 | }
866 | } else {
867 | $pairs[] = $parameter . '=' . $value;
868 | }
869 | }
870 | // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61)
871 | // Each name-value pair is separated by an '&' character (ASCII code 38)
872 | return implode('&', $pairs);
873 | }
874 | }
875 |
--------------------------------------------------------------------------------
/twitteroauth/twitteroauth.php:
--------------------------------------------------------------------------------
1 | http_status; }
54 | function lastAPICall() { return $this->last_api_call; }
55 |
56 | /**
57 | * construct TwitterOAuth object
58 | */
59 | function __construct($consumer_key, $consumer_secret, $oauth_token = NULL, $oauth_token_secret = NULL) {
60 | $this->sha1_method = new OAuthSignatureMethod_HMAC_SHA1();
61 | $this->consumer = new OAuthConsumer($consumer_key, $consumer_secret);
62 | if (!empty($oauth_token) && !empty($oauth_token_secret)) {
63 | $this->token = new OAuthConsumer($oauth_token, $oauth_token_secret);
64 | } else {
65 | $this->token = NULL;
66 | }
67 | }
68 |
69 |
70 | /**
71 | * Get a request_token from Twitter
72 | *
73 | * @returns a key/value array containing oauth_token and oauth_token_secret
74 | */
75 | function getRequestToken($oauth_callback) {
76 | $parameters = array();
77 | $parameters['oauth_callback'] = $oauth_callback;
78 | $request = $this->oAuthRequest($this->requestTokenURL(), 'GET', $parameters);
79 | $token = OAuthUtil::parse_parameters($request);
80 | $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']);
81 | return $token;
82 | }
83 |
84 | /**
85 | * Get the authorize URL
86 | *
87 | * @returns a string
88 | */
89 | function getAuthorizeURL($token, $sign_in_with_twitter = TRUE) {
90 | if (is_array($token)) {
91 | $token = $token['oauth_token'];
92 | }
93 | if (empty($sign_in_with_twitter)) {
94 | return $this->authorizeURL() . "?oauth_token={$token}";
95 | } else {
96 | return $this->authenticateURL() . "?oauth_token={$token}";
97 | }
98 | }
99 |
100 | /**
101 | * Exchange request token and secret for an access token and
102 | * secret, to sign API calls.
103 | *
104 | * @returns array("oauth_token" => "the-access-token",
105 | * "oauth_token_secret" => "the-access-secret",
106 | * "user_id" => "9436992",
107 | * "screen_name" => "abraham")
108 | */
109 | function getAccessToken($oauth_verifier) {
110 | $parameters = array();
111 | $parameters['oauth_verifier'] = $oauth_verifier;
112 | $request = $this->oAuthRequest($this->accessTokenURL(), 'GET', $parameters);
113 | $token = OAuthUtil::parse_parameters($request);
114 | $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']);
115 | return $token;
116 | }
117 |
118 | /**
119 | * One time exchange of username and password for access token and secret.
120 | *
121 | * @returns array("oauth_token" => "the-access-token",
122 | * "oauth_token_secret" => "the-access-secret",
123 | * "user_id" => "9436992",
124 | * "screen_name" => "abraham",
125 | * "x_auth_expires" => "0")
126 | */
127 | function getXAuthToken($username, $password) {
128 | $parameters = array();
129 | $parameters['x_auth_username'] = $username;
130 | $parameters['x_auth_password'] = $password;
131 | $parameters['x_auth_mode'] = 'client_auth';
132 | $request = $this->oAuthRequest($this->accessTokenURL(), 'POST', $parameters);
133 | $token = OAuthUtil::parse_parameters($request);
134 | $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']);
135 | return $token;
136 | }
137 |
138 | /**
139 | * GET wrapper for oAuthRequest.
140 | */
141 | function get($url, $parameters = array()) {
142 | $response = $this->oAuthRequest($url, 'GET', $parameters);
143 | if ($this->format === 'json' && $this->decode_json) {
144 | return json_decode($response);
145 | }
146 | return $response;
147 | }
148 |
149 | /**
150 | * POST wrapper for oAuthRequest.
151 | */
152 | function post($url, $parameters = array()) {
153 | $response = $this->oAuthRequest($url, 'POST', $parameters);
154 | if ($this->format === 'json' && $this->decode_json) {
155 | return json_decode($response);
156 | }
157 | return $response;
158 | }
159 |
160 | /**
161 | * DELETE wrapper for oAuthReqeust.
162 | */
163 | function delete($url, $parameters = array()) {
164 | $response = $this->oAuthRequest($url, 'DELETE', $parameters);
165 | if ($this->format === 'json' && $this->decode_json) {
166 | return json_decode($response);
167 | }
168 | return $response;
169 | }
170 |
171 | /**
172 | * Format and sign an OAuth / API request
173 | */
174 | function oAuthRequest($url, $method, $parameters) {
175 | if (strrpos($url, 'https://') !== 0 && strrpos($url, 'http://') !== 0) {
176 | $url = "{$this->host}{$url}.{$this->format}";
177 | }
178 | $request = OAuthRequest::from_consumer_and_token($this->consumer, $this->token, $method, $url, $parameters);
179 | $request->sign_request($this->sha1_method, $this->consumer, $this->token);
180 | switch ($method) {
181 | case 'GET':
182 | return $this->http($request->to_url(), 'GET');
183 | default:
184 | return $this->http($request->get_normalized_http_url(), $method, $request->to_postdata());
185 | }
186 | }
187 |
188 | /**
189 | * Make an HTTP request
190 | *
191 | * @return API results
192 | */
193 | function http($url, $method, $postfields = NULL) {
194 | $this->http_info = array();
195 | $ci = curl_init();
196 | /* Curl settings */
197 | curl_setopt($ci, CURLOPT_USERAGENT, $this->useragent);
198 | curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, $this->connecttimeout);
199 | curl_setopt($ci, CURLOPT_TIMEOUT, $this->timeout);
200 | curl_setopt($ci, CURLOPT_RETURNTRANSFER, TRUE);
201 | curl_setopt($ci, CURLOPT_HTTPHEADER, array('Expect:'));
202 | curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, $this->ssl_verifypeer);
203 | curl_setopt($ci, CURLOPT_HEADERFUNCTION, array($this, 'getHeader'));
204 | curl_setopt($ci, CURLOPT_HEADER, FALSE);
205 |
206 | switch ($method) {
207 | case 'POST':
208 | curl_setopt($ci, CURLOPT_POST, TRUE);
209 | if (!empty($postfields)) {
210 | curl_setopt($ci, CURLOPT_POSTFIELDS, $postfields);
211 | }
212 | break;
213 | case 'DELETE':
214 | curl_setopt($ci, CURLOPT_CUSTOMREQUEST, 'DELETE');
215 | if (!empty($postfields)) {
216 | $url = "{$url}?{$postfields}";
217 | }
218 | }
219 |
220 | curl_setopt($ci, CURLOPT_URL, $url);
221 | $response = curl_exec($ci);
222 | $this->http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE);
223 | $this->http_info = array_merge($this->http_info, curl_getinfo($ci));
224 | $this->url = $url;
225 | curl_close ($ci);
226 | return $response;
227 | }
228 |
229 | /**
230 | * Get the header info to store.
231 | */
232 | function getHeader($ch, $header) {
233 | $i = strpos($header, ':');
234 | if (!empty($i)) {
235 | $key = str_replace('-', '_', strtolower(substr($header, 0, $i)));
236 | $value = trim(substr($header, $i + 2));
237 | $this->http_header[$key] = $value;
238 | }
239 | return strlen($header);
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/util/TwitterAPI.php:
--------------------------------------------------------------------------------
1 | user = BOT_ACCOUNT;
36 | $this->oauth = new TwitterOAuth(TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, OAUTH_TOKEN,
37 | OAUTH_TOKEN_SECRET);
38 | }
39 |
40 | /**
41 | * Private helper method for performing the given action with OAuth.
42 | * @param string $method The HTTP method.
43 | * @param string $call The Twitter API method call.
44 | * @param array $args Array of arguments for the API call.
45 | * @return Raw JSON-encoded response from the Twitter servers.
46 | */
47 | private function doAction($method, $call, $args) {
48 | switch (strtoupper($method)) {
49 | case 'GET':
50 | return $this->oauth->get($call, $args);
51 | case 'POST':
52 | return $this->oauth->post($call, $args);
53 | case 'DELETE':
54 | return $this->oauth->delete($call, $args);
55 | }
56 | }
57 |
58 | /**
59 | * Do the public_timeline action.
60 | * See http://dev.twitter.com/doc/get/statuses/public_timeline
61 | * @param array $args
62 | * @return JSON-decoded array.
63 | */
64 | public function public_timeline($args = array()) {
65 | return $this->doAction('get', 'statuses/public_timeline', $args);
66 | }
67 |
68 | /**
69 | * Do the update action. Posts a new tweet.
70 | * See http://dev.twitter.com/doc/post/statuses/update
71 | * @param string $post
72 | * @param array $args
73 | * @return JSON-decoded array
74 | */
75 | public function update($post, $args = array()) {
76 | return $this->doAction('post', 'statuses/update',
77 | array_merge(array('status' => $post),$args));
78 | }
79 |
80 | /**
81 | * Performs a search through Twitter's public archives for posts with
82 | * the given search terms.
83 | * See http://dev.twitter.com/doc/get/search
84 | *
85 | * NOTE: Due to current separations in Twitter's API, this method is
86 | * implemented separately from the rest of the methods, specifically
87 | * it does not use OAuth in order to perform its function.
88 | *
89 | * @param string $searchstring
90 | * @param array $args
91 | */
92 | public function search($searchstring, $args = array()) {
93 | $url = 'http://search.twitter.com/search.json?q=' .
94 | urlencode($searchstring);
95 | foreach ($args as $key => $value) {
96 | $url .= '&' . $key . '=' . urlencode($value);
97 | }
98 | // set up the curl session
99 | $ch = curl_init();
100 | $options = array(CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true);
101 | curl_setopt_array($ch, $options);
102 | $retval = json_decode(curl_exec($ch));
103 | curl_close($ch);
104 | return $retval;
105 | }
106 | }
107 |
108 | ?>
109 |
--------------------------------------------------------------------------------
/util/translate.php:
--------------------------------------------------------------------------------
1 |
33 | 'https://www.googleapis.com/language/translate/v2?key=' .
34 | $key . '&target=' . $targetlanguage . '&q=' . urlencode($text),
35 | CURLOPT_RETURNTRANSFER => true);
36 | curl_setopt_array($ch, $options);
37 | $retval = json_decode(curl_exec($ch));
38 | curl_close($ch);
39 | if (isset($retval->error) ||
40 | $retval->data->translations[0]->detectedSourceLanguage == 'en') {
41 | return $text;
42 | } else {
43 | // do a little clean-up on the @ tags
44 | $matches = preg_match_all('/@([A-Za-z0-9_]+)/', $text, $usernames);
45 | $translated = $retval->data->translations[0]->translatedText;
46 | if ($matches == 0 || $matches === false) {
47 | return $translated;
48 | }
49 | foreach ($usernames[1] as $index => $username) {
50 | $pos = strpos($translated, $username);
51 | $translated = substr_replace($translated, '@', $pos, 0);
52 | }
53 | return str_replace('@ ', '', $translated);
54 | }
55 | }
56 | }
57 |
58 | ?>
59 |
--------------------------------------------------------------------------------