root/trunk/Phergie/Plugin/Logging.php @ 126

Revision 126, 18.3 KB (checked in by tobias382, 5 years ago)

* Fixed a bug in the Logging plugin where the seen command response used the event source instead of the event nick
* Added to the willsee command response in the Logging plugin to address the person making the request

Line 
1<?php
2
3/**
4* @see Phergie_Plugin_Abstract_Command
5*/
6require_once 'Phergie/Plugin/Abstract/Command.php';
7
8/**
9* @see Phergie_Plugin_Users
10*/
11require_once 'Phergie/Plugin/Users.php';
12
13/**
14* Logs channel events and handles requests for logged data pertaining to the
15* last actions taken or messages sent by a given user or posts sent
16* containing a given search phrase.
17*/
18class Phergie_Plugin_Logging extends Phergie_Plugin_Abstract_Command
19{
20    /**
21    * Indicates that a local directory is required for this plugin
22    *
23    * @var bool
24    */
25    protected $needsDir = true;
26
27    /**
28    * Indicates a JOIN event in the type column of the logs table
29    *
30    * @const int
31    */
32    const JOIN = 1;
33
34    /**
35    * Indicates a PART event in the type column of the logs table
36    *
37    * @const int
38    */
39    const PART = 2;
40
41    /**
42    * Indicates a QUIT event in the type column of the logs table
43    *
44    * @const int
45    */
46    const QUIT = 3;
47
48    /**
49    * Indicates a PRIVMSG event in the type column of the logs table
50    *
51    * @const int
52    */
53    const PRIVMSG = 4;
54
55    /**
56    * Indicates a CTCP ACTION event in the type column of the logs table
57    *
58    * @const int
59    */
60    const ACTION = 5;
61
62    /**
63    * Indicates a NICK event in the type column of the logs table
64    *
65    * @const int
66    */
67    const NICK = 6;
68
69    /**
70    * Indicates a KICK event in the type column of the logs table
71    *
72    * @const int
73    */
74    const KICK = 7;
75
76    /**
77    * Indicates a MODE event in the type column of the logs table
78    *
79    * @const int
80    */
81    const MODE = 8;
82
83    /**
84    * Indicates a TOPIC event in the type column of the logs table
85    *
86    * @const int
87    */
88    const TOPIC = 9;
89
90    /**
91    * PDO instance for the database
92    *
93    * @var PDO
94    */
95    protected $db = null;
96
97    /**
98    * Prepared statement for searching for the last logged action in which a
99    * particular user was mentioned
100    *
101    * @var PDOStatement
102    */
103    protected $search;
104
105    /**
106    * Prepared statement for searching for the last logged action originating
107    * from a particular user
108    *
109    * @var PDOStatement
110    */
111    protected $seen;
112
113    /**
114    * Prepared statement for searching for the last logged PRIVMSG action or
115    * CTCP ACTION command originating from a particular user
116    *
117    * @var PDOStatement
118    */
119    protected $heard;
120
121    /**
122    * PRepared statement for searching for one or more time ranges during
123    * which a particular user is most likely to be present in a particular
124    * channel
125    *
126    * @var PDOStatement
127    */
128    protected $willsee;
129
130    /**
131    * Prepared statement for inserting new log entries
132    *
133    * @var PDOStatement
134    */
135    protected $insert;
136
137    /**
138    * Answers for seen when the user is present
139    *
140    * @var array
141    */
142    protected $present = array
143    (
144        'Open your eyes already!',
145        'Are you blind?',
146        'Behind you!',
147        'Over there!',
148    );
149
150    /**
151    * Answers for when record of a user cannot be found
152    *
153    * @var array
154    */
155    protected $missing = array
156    (
157        'must be mute, or one of your imaginary friends?',
158        'was never heard from... a first time.',
159        'was never heard from... to begin with.',
160        'must have eluded my ubiquity.',
161    );
162
163    /**
164    * Action descriptions corresponding to event constants
165    *
166    * @var array
167    */
168    protected $actions = array
169    (
170        self::JOIN => 'joining this channel',
171        self::PART => 'leaving this channel because',
172        self::QUIT => 'quitting',
173        self::PRIVMSG => 'saying',
174        self::ACTION => 'saying',
175        self::NICK => 'changing nick to',
176        self::KICK => 'being kicked off because',
177    );
178
179    /**
180    * Initializes the database.
181    *
182    * @return void
183    */
184    public function init()
185    {
186        try {
187            // Initialize the database connection
188            $db = $this->dir . 'logging.db';
189            $create = !file_exists($db);
190            $this->db = new PDO('sqlite:' . $db);
191
192            // Create database tables if necessary
193            if ($create) {
194                $result = $this->db->exec('
195                    CREATE TABLE logs (
196                        tstamp VARCHAR(19),
197                        type SHORTINT,
198                        chan VARCHAR(45),
199                        nick VARCHAR(25),
200                        message VARCHAR(255)
201                    );
202                    CREATE INDEX channicktype ON logs (tstamp, type, chan, nick);
203                    CREATE INDEX channick ON logs (tstamp, chan, nick);
204                ');
205            }
206
207            // Initialize prepared statements for common operations
208            $this->search = $this->db->prepare('
209                SELECT tstamp, type, chan, nick, message
210                FROM logs
211                WHERE nick LIKE :phrase
212                OR message LIKE :phrase
213                ORDER BY tstamp DESC
214                LIMIT :limit
215            ');
216
217            $this->seen = $this->db->prepare('
218                SELECT tstamp, type, message
219                FROM logs
220                WHERE nick = :name
221                AND chan = :chan
222                ORDER BY tstamp DESC
223                LIMIT 1
224            ');
225
226            $this->heard = $this->db->prepare('
227                SELECT tstamp, type, message
228                FROM logs
229                WHERE type IN (' . self::PRIVMSG . ', ' . self::ACTION . ')
230                AND nick = :nick
231                AND chan = :chan
232                ORDER BY tstamp DESC
233                LIMIT 1
234            ');
235
236            $this->willsee = $this->db->prepare('
237                SELECT strftime("%H", tstamp) post_hour, COUNT(*) post_count
238                FROM logs
239                WHERE type IN (' . self::PRIVMSG . ', ' . self::ACTION . ')
240                AND nick = :nick
241                AND chan = :chan
242                GROUP BY strftime("%H", tstamp)
243                ORDER BY 2 DESC, 1
244                LIMIT 1
245            ');
246
247            $this->insert = $this->db->prepare('
248                INSERT INTO logs (
249                    tstamp,
250                    type,
251                    chan,
252                    nick,
253                    message
254                )
255                VALUES (
256                    :tstamp,
257                    :type,
258                    :chan,
259                    :nick,
260                    :message
261                )
262            ');
263        } catch (PDOException $e) { }
264    }
265
266    /**
267    * Returns whether or not the plugin's dependencies are met.
268    *
269    * @param Phergie_Driver_Abstract $client Client instance
270    * @param array $plugins List of short names for plugins that the
271    *                       bootstrap file intends to instantiate
272    * @see Phergie_Plugin_Abstract_Base::checkDependencies()
273    * @return bool TRUE if dependencies are met, FALSE otherwise
274    */
275    public static function checkDependencies(Phergie_Driver_Abstract $client, array $plugins)
276    {
277        if (! in_array('Users', $plugins)
278            || ! Phergie_Plugin_Users::checkDependencies($client, $plugins)) {
279            return false;
280        }
281
282        if (!extension_loaded('PDO')
283            || !extension_loaded('pdo_sqlite')) {
284            return false;
285        }
286
287        return true;
288    }
289
290    /**
291    * Formats a timestamp for display purposes.
292    *
293    * @param string $timestamp Timestamp to format
294    * @return string Formatted timestamp
295    */
296    protected function formatTimestamp($timestamp)
297    {
298        return preg_replace(
299            '#^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$#',
300            '$1-$2-$3 @ $4:$5:$6',
301            $timestamp
302        );
303    }
304
305    /**
306    * Returns a random message used when a user is currently present.
307    *
308    * @return string
309    */
310    protected function randomPresent()
311    {
312        return $this->present[array_rand($this->present)];
313    }
314
315    /**
316    * Returns a random message used when no logged record of a user exists.
317    *
318    * @return string
319    */
320    protected function randomMissing()
321    {
322        return $this->missing[array_rand($this->missing)];
323    }
324
325    /**
326    * Inserts a new entry in the log database.
327    *
328    * @param int $type Class constant representing the event type
329    * @param string $chan Name of the channel in which the event occurs
330    * @param string $nick Nick of the user from which the event originates
331    * @param string $message Message associated with the event if applicable
332    *                        (optional)
333    * @return void
334    */
335    protected function insertEvent($type, $chan, $nick, $message = null)
336    {
337        if (!$this->db) {
338            return;
339        }
340
341        $params = array(
342            ':tstamp' => date('Y-m-d H:i:s'),
343            ':type' => $type,
344            ':chan' => $chan,
345            ':nick' => $nick,
346            ':message' => trim($message)
347        );
348
349        $result = $this->insert->execute($params);
350    }
351
352    /**
353    * Logs incoming messages.
354    *
355    * @return void
356    */
357    public function onPrivmsg()
358    {
359        // Allow the Command plugin to process command calls
360        parent::onPrivmsg();
361
362        if ($this->event->isInChannel()) {
363            $this->insertEvent(
364                self::PRIVMSG,
365                $this->event->getSource(),
366                $this->event->getNick(),
367                $this->event->getArgument(1)
368            );
369        }
370    }
371
372    /**
373    * Logics incoming actions.
374    *
375    * @return void
376    */
377    public function onAction()
378    {
379        if ($this->event->isInChannel()) {
380            $this->insertEvent(
381                self::ACTION,
382                $this->event->getSource(),
383                $this->event->getNick(),
384                $this->event->getArgument(1)
385            );
386        }
387    }
388
389    /**
390    * Tracks users joining.
391    *
392    * @return void
393    */
394    public function onJoin()
395    {
396        $this->insertEvent(
397            self::JOIN,
398            $this->event->getSource(),
399            $this->event->getNick()
400        );
401    }
402
403    /**
404    * Tracks users parting.
405    *
406    * @return void
407    */
408    public function onPart()
409    {
410        $this->insertEvent(
411            self::PART,
412            $this->event->getSource(),
413            $this->event->getNick(),
414            $this->event->getArgument(1)
415        );
416    }
417
418    /**
419    * Tracks users being kicked.
420    *
421    * @return void
422    */
423    public function onKick()
424    {
425        $this->insertEvent(
426            self::KICK,
427            $this->event->getSource(),
428            $this->event->getNick(),
429            $this->event->getArgument(1)
430        );
431    }
432
433    /**
434    * Tracks users changing modes.
435    *
436    * @return void
437    */
438    public function onMode()
439    {
440        $this->insertEvent(
441            self::MODE,
442            $this->event->getSource(),
443            $this->event->getNick(),
444            implode(' ', array_slice($this->event->getArguments(), 1))
445        );
446    }
447
448    /**
449    * Tracks channel topic changes.
450    *
451    * @return void
452    */
453    public function onTopic()
454    {
455        $this->insertEvent(
456            self::TOPIC,
457            $this->event->getSource(),
458            $this->event->getNick(),
459            $this->event->getArgument(1)
460        );
461    }
462
463    /**
464    * Tracks users quitting.
465    *
466    * @return void
467    */
468    public function onQuit()
469    {
470        $nick = $this->event->getNick();
471
472        foreach (Phergie_Plugin_Users::getChannels($nick) as $chan) {
473            $this->insertEvent(
474                self::QUIT,
475                $chan,
476                $this->event->getNick(),
477                $this->event->getArgument(0)
478            );
479        }
480    }
481
482    /**
483    * Tracks users changing nicks.
484    *
485    * @return void
486    */
487    public function onNick()
488    {
489        $nick = $this->event->getNick();
490
491        foreach (Phergie_Plugin_Users::getChannels($nick) as $chan) {
492            $this->insertEvent(
493                self::NICK,
494                $chan,
495                $this->event->getNick(),
496                $this->event->getArgument(0)
497            );
498        }
499    }
500
501    /**
502    * Responds to requests for logged messages containing a particular search
503    * phrase.
504    *
505    * @param string $phrase Phrase to search for
506    * @return void
507    */
508    public function onDoSearch($phrase)
509    {
510        if (!$this->db) {
511            return;
512        }
513
514        $source = $this->event->getSource();
515
516        $params = array(
517            ':phrase' => '%' . $phrase . '%',
518            ':limit' => ($source[0] == '#' ? 1 : 6)
519        );
520
521        $this->search->execute($params);
522
523        foreach($this->search as $row) {
524            $this->doPrivmsg(
525                $source,
526                sprintf(
527                    '%s was seen %s: %s on %s (on %s)',
528                    $row['nick'],
529                    $this->actions[$row['type']],
530                    $row['message'],
531                    $row['chan'],
532                    $this->formatTimestamp($row['tstamp'])
533                )
534            );
535        }
536    }
537
538    /**
539    * Responds to requests for the last logged action originating from a
540    * particular user.
541    *
542    * @param string $user Nick of the user to search for
543    * @return void
544    */
545    public function onDoSeen($user)
546    {
547        if (!$this->db) {
548            return;
549        }
550
551        // Don't match if user has a space (obviously it's not a nick)
552        if (strpos($user, ' ') !== false) {
553            return;
554        }
555
556        $source = $this->event->getSource();
557        $target = $this->event->getNick();
558
559        // Handle 'me' alias
560        if ($user == 'me') {
561            $user = $target;
562        }
563
564        // Person is online, send a random prank answer
565        if (Phergie_Plugin_Users::isIn($user, $source)) {
566            $this->doPrivmsg(
567                $source,
568                sprintf(
569                    '%s: %s',
570                    $target,
571                    $this->randomPresent()
572                )
573            );
574
575        // Person is offline
576        } else {
577            $params = array(
578                ':name' => $user,
579                ':chan' => $source
580            );
581
582            $this->seen->execute($params);
583            $row = $this->seen->fetch(PDO::FETCH_ASSOC);
584
585            // Send the last action if available
586            if ($row) {
587                $this->doPrivmsg(
588                    $source,
589                    sprintf(
590                        '%s: %s was last seen %s: %s (on %s)',
591                        $target,
592                        $user,
593                        $this->actions[$row['type']],
594                        $row['message'],
595                        $this->formatTimestamp($row['tstamp'])
596                    )
597                );
598
599            // Send a prank answer if no last action is found
600            } else {
601                $this->doPrivmsg(
602                    $source,
603                    sprintf(
604                        '%s: %s %s',
605                        $target,
606                        $user,
607                        $this->randomMissing()
608                    )
609                );
610            }
611        }
612    }
613
614    /**
615    * Responds to requests for the last logged PRIVMSG action or CTCP ACTION
616    * command originating from a particular user.
617    *
618    * @param string $user Nick of the user to search for
619    * @return void
620    */
621    public function onDoHeard($user)
622    {
623        if (!$this->db) {
624            return;
625        }
626
627        // Don't match if user has a space (obviously it's not a nick)
628        if (strpos($user, ' ') !== false) {
629            return;
630        }
631
632        $source = $this->event->getSource();
633        $target = $this->event->getNick();
634
635        // Handle 'me' alias
636        if ($user == 'me') {
637            $user = $target;
638        }
639
640        // Search for the last recorded action
641        $params = array(
642            ':nick' => $user,
643            ':chan' => $source
644        );
645
646        $this->heard->execute($params);
647        $row = $this->heard->fetch(PDO::FETCH_ASSOC);
648
649        // Send the last action if available
650        if($row) {
651            $this->doPrivmsg(
652                $source,
653                sprintf(
654                    '%s: %s\'s last words were: %s (on %s)',
655                    $target,
656                    $user,
657                    $row['message'],
658                    $this->formatTimestamp($row['tstamp'])
659                )
660            );
661
662        // Send a prank answer if no last action is found
663        } else {
664            $this->doPrivmsg(
665                $source,
666                sprintf(
667                    '%s: %s %s',
668                    $target,
669                    $user,
670                    $this->randomMissing()
671                )
672            );
673        }
674    }
675
676    /**
677    * Responds to requests for a time range during which a particular user
678    * is most likely to be present in the channel from which the request
679    * originates.
680    *
681    * @param string $user Nick of the user to search for
682    * @return void
683    */
684    public function onDoWillsee($user)
685    {
686        if (!$this->db) {
687            return;
688        }
689       
690        // Check to make sure the request came from a channel
691        $source = $this->event->getSource();
692        if ($source[0] != '#') {
693            return;
694        }
695
696        // Handle cases where the bot is the subject
697        if ($user == $this->getIni('nick')) {
698            $this->doPrivmsg($source, 'What are you talking about? I\'m always here!');
699            return;
700        }
701
702        // Handle 'me' alias
703        if ($user == 'me') {
704            $user = $this->event->getNick();
705        }
706
707        // Perform the search
708        $params = array(
709            ':nick' => $user,
710            ':chan' => $source
711        );
712
713        $this->willsee->execute($params);
714        $prediction = $this->willsee->fetchColumn();
715
716        // Return if no results are found
717        if ($prediction === false) {
718            $this->doPrivmsg($source, $user . ' ' . $this->randomMissing());
719            return;
720        }
721
722        // Calculate a predicted time of arrival
723        $hour = date('H');
724        if ($hour > $prediction) {
725            $prediction = 24 - ($hour - $prediction);
726        } else {
727            $prediction = $prediction - $hour;
728        }
729
730        // Return with a message including the prediction
731        $message = $target . ': ' . $user . ' is most likely to be online ';
732        if ($prediction == 0) {
733            $message .= 'now!';
734        } elseif ($prediction == 1) {
735            $message .= 'in 1 hour.';
736        } else {
737            $message .= 'in ' . $prediction . ' hours.';
738        }
739        $this->doPrivmsg($source, $message);
740    }
741}
Note: See TracBrowser for help on using the browser.