Erebot  latest
A modular IRC bot for PHP 5.3+
Base.php
1 <?php
2 /*
3  This file is part of Erebot, a modular IRC bot written in PHP.
4 
5  Copyright © 2010 François Poirotte
6 
7  Erebot is free software: you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation, either version 3 of the License, or
10  (at your option) any later version.
11 
12  Erebot is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with Erebot. If not, see <http://www.gnu.org/licenses/>.
19 */
20 
21 namespace Erebot\Module;
22 
28 abstract class Base
29 {
31  protected $connection;
32 
34  protected $channel;
35 
37  protected $translator;
38 
40  protected $factories;
41 
43  protected $logger;
44 
45 
47  const RELOAD_INIT = 0x01;
48 
50  const RELOAD_TESTING = 0x02;
51 
53  const RELOAD_MEMBERS = 0x10;
54 
56  const RELOAD_HANDLERS = 0x20;
57 
59  const RELOAD_ALL = 0xF0;
60 
61 
63  const MSG_TYPE_PRIVMSG = 'PRIVMSG';
64 
66  const MSG_TYPE_NOTICE = 'NOTICE';
67 
69  const MSG_TYPE_CTCP = 'CTCP';
70 
72  const MSG_TYPE_CTCPREPLY = 'CTCPREPLY';
73 
75  const MSG_TYPE_ACTION = 'ACTION';
76 
89  abstract protected function reload($flags);
90 
99  protected function unload()
100  {
101  }
102 
111  final public function __construct($channel)
112  {
113  $this->connection =
114  $this->translator =
115  $this->mainCfg = null;
116  $this->channel = $channel;
117  $this->factories = array();
118 
119  $ifaces = array(
120  '!EventHandler' => '\\Erebot\\EventHandler',
121 
122  '!Identity' => '\\Erebot\\Identity',
123 
124  '!NumericHandler' => '\\Erebot\\NumericHandler',
125 
126  '!NumericReference' => '\\Erebot\\NumericReference',
127 
128  '!Styling' => '\\Erebot\\Styling',
129 
130  '\\Erebot\\Styling\\Variables\\CurrencyInterface' =>
131  '\\Erebot\\Styling\\Variables\\CurrencyVariable',
132 
133  '\\Erebot\\Styling\\Variables\\DateTimeInterface' =>
134  '\\Erebot\\Styling\\Variables\\DateTimeVariable',
135 
136  '\\Erebot\\Styling\\Variables\\DurationInterface' =>
137  '\\Erebot\\Styling\\Variables\\DurationVariable',
138 
139  '!TextWrapper' => '\\Erebot\\TextWrapper',
140 
141  '!Timer' => '\\Erebot\\Timer',
142  );
143  foreach ($ifaces as $iface => $cls) {
144  try {
145  $this->setFactory($iface, $cls);
146  } catch (\Erebot\InvalidValueException $e) {
147  // Ignore silently as the only time the default classes
148  // won't exist is when we run the tests for some module.
149  }
150  }
151 
153  $this->logger = null;
154  if (class_exists('\\Plop\\Plop')) {
155  $this->logger =& \Plop\Plop::getInstance();
156  }
157  }
158 
160  final public function __destruct()
161  {
162  unset(
163  $this->connection,
164  $this->translator,
165  $this->channel,
166  $this->mainCfg
167  );
168  }
169 
186  final public function reloadModule(
187  \Erebot\Interfaces\Connection $connection,
188  $flags
189  ) {
190  if ($this->connection === null) {
191  $flags |= self::RELOAD_INIT;
192  } else {
193  $flags &= ~self::RELOAD_INIT;
194  }
195 
196  $this->connection = $connection;
197  $serverCfg = $this->connection->getConfig(null);
198  $this->mainCfg = $serverCfg->getMainCfg();
199 
200  $this->translator = $this->mainCfg->getTranslator(get_called_class());
201  $this->reload($flags);
202 
203  if ($this instanceof \Erebot\Interfaces\HelpEnabled) {
204  $this->registerHelpMethod(\Erebot\CallableWrapper::wrap(array($this, 'getHelp')));
205  }
206  }
207 
212  final public function unloadModule()
213  {
214  return $this->unload();
215  }
216 
234  public function setFactory($iface, $cls)
235  {
236  if (!is_string($iface)) {
237  throw new \Erebot\InvalidValueException('Not an interface name');
238  }
239 
240  $ifaceName = str_replace('!', '\\Erebot\\Interfaces\\', $iface);
241  if (!interface_exists($ifaceName, true)) {
242  $ifaceName = str_replace('!', '\\Erebot\\', $iface) . 'Interface';
243  if (!interface_exists($ifaceName, true)) {
244  throw new \Erebot\InvalidValueException(
245  'No such interface ('.$iface.')'
246  );
247  }
248  }
249 
250  if (!class_exists($cls, true)) {
251  throw new \Erebot\InvalidValueException('No such class ('.$cls.')');
252  }
253 
254  $reflector = new \ReflectionClass($cls);
255  if (!$reflector->isSubclassOf($ifaceName)) {
256  throw new \Erebot\InvalidValueException(
257  'A class that implements the interface was expected'
258  );
259  }
260  $iface = strtolower($ifaceName);
261  $this->factories[$iface] = $cls;
262  }
263 
283  public function getFactory($iface)
284  {
285  if (!is_string($iface)) {
286  throw new \Erebot\InvalidValueException('Not an interface name');
287  }
288 
289  $ifaceKey = strtolower(str_replace('!', '\\Erebot\\Interfaces\\', $iface));
290  if (!isset($this->factories[$ifaceKey])) {
291  $ifaceKey = strtolower(str_replace('!', '\\Erebot\\', $iface) . 'Interface');
292  if (!isset($this->factories[$ifaceKey])) {
293  throw new \Erebot\InvalidValueException(
294  'No such interface ('.$iface.')'
295  );
296  }
297  }
298  return $this->factories[$ifaceKey];
299  }
300 
320  protected function sendMessage(
321  $targets,
322  $message,
323  $type = self::MSG_TYPE_PRIVMSG
324  ) {
325  $types = array('PRIVMSG', 'NOTICE', 'CTCP', 'CTCPREPLY', 'ACTION');
326  $type = strtoupper($type);
327  if (!in_array($type, $types)) {
328  throw new \Exception('Not a valid type');
329  }
330 
331  if (is_array($targets)) {
332  $targets = implode(',', $targets);
333  } elseif ($targets instanceof \Erebot\Identity) {
334  $targets = (string) $targets;
335  } elseif (!is_string($targets)) {
336  throw new \Exception('Not a valid target (expected a string)');
337  }
338 
339  if (!\Erebot\Utils::stringifiable($message)) {
340  throw new \Exception('Not a valid message (expected a string)');
341  }
342 
343  $message = (string) $message;
344  $parts = array_map('trim', explode("\n", trim($message)));
345  $message = implode(' ', $parts);
346  $marker = '';
347  $ctcpType = '';
348 
349  if ($type == 'ACTION') {
350  $type = 'PRIVMSG';
351  $marker = "\001";
352  $ctcpType = 'ACTION';
353  }
354 
355  if ($type == 'CTCP' || $type == 'CTCPREPLY') {
356  $type = ($type == 'CTCP' ? 'PRIVMSG' : 'NOTICE');
357  $marker = "\001";
358  $parts = explode(' ', $message);
359  $ctcpType = array_shift($parts);
360  $message = implode(' ', $parts);
361  }
362 
363  if ($ctcpType != "" && $message != "") {
364  $ctcpType .= " ";
365  $message = self::ctcpQuote($message);
366  }
367 
368  $prefix = $type.' '.$targets.' :'.$marker.$ctcpType;
369  // 400 is a rough estimation of how big
370  // a message we may send.
371  $messages = explode(
372  "\n",
373  wordwrap(
374  $message,
375  400 - strlen($prefix) - 2,
376  "\n",
377  true
378  )
379  );
380  $io = $this->connection->getIO();
381  foreach ($messages as $msg) {
382  $io->push($prefix.$msg.$marker);
383  }
384  }
385 
399  protected static function ctcpQuote($message)
400  {
401  // First comes low-level quoting.
402  $quoting = array(
403  "\000" => "\0200",
404  "\n" => "\020n",
405  "\r" => "\020r",
406  "\020" => "\020\020",
407  );
408  $message = strtr($message, $quoting);
409 
410  // Next some CTCP-level quoting and we're done.
411  $quoting = array(
412  "\001" => "\\a",
413  "\\" => "\\\\",
414  );
415  $message = strtr($message, $quoting);
416  return $message;
417  }
418 
425  protected function sendCommand($command)
426  {
427  if (!\Erebot\Utils::stringifiable($command)) {
428  throw new \Exception('Invalid command (not a string)');
429  }
430  $this->connection->getIO()->push((string) $command);
431  }
432 
443  protected function addTimer(\Erebot\TimerInterface $timer)
444  {
445  $bot = $this->connection->getBot();
446  return $bot->addTimer($timer);
447  }
448 
459  protected function removeTimer(\Erebot\TimerInterface $timer)
460  {
461  $bot = $this->connection->getBot();
462  return $bot->removeTimer($timer);
463  }
464 
489  private function parseSomething($something, $param, $default)
490  {
491  $function = 'parse'.$something;
492  $bot = $this->connection->getBot();
493  if ($this->channel !== null) {
494  try {
495  $config = $this->connection->getConfig($this->channel);
496  return $config->$function('\\' . get_called_class(), $param);
497  } catch (\Erebot\Exception $e) {
498  unset($config);
499  }
500  }
501  $config = $this->connection->getConfig(null);
502  return $config->$function('\\' . get_called_class(), $param, $default);
503  }
504 
521  protected function parseBool($param, $default = null)
522  {
523  return $this->parseSomething('Bool', $param, $default);
524  }
525 
542  protected function parseString($param, $default = null)
543  {
544  return $this->parseSomething('String', $param, $default);
545  }
546 
563  protected function parseInt($param, $default = null)
564  {
565  return $this->parseSomething('Int', $param, $default);
566  }
567 
584  protected function parseReal($param, $default = null)
585  {
586  return $this->parseSomething('Real', $param, $default);
587  }
588 
612  protected function registerHelpMethod(\Erebot\CallableInterface $callback)
613  {
614  try {
615  $helper = $this->connection->getModule(
616  '\\Erebot\\Module\\Helper',
617  $this->channel
618  );
619  return $helper->realRegisterHelpMethod($this, $callback);
620  } catch (\Exception $e) {
621  return false;
622  }
623  }
624 
638  protected function getFormatter($chan)
639  {
640  $cls = $this->getFactory('!Styling');
641  if ($chan === false) {
642  return new $cls($this->translator);
643  } elseif ($chan !== null) {
644  $config = $this->connection->getConfig($chan);
645  try {
646  return new $cls($config->getTranslator(get_called_class()));
647  } catch (\Erebot\Exception $e) {
648  // The channel lacked a specific config. Use the cascade.
649  }
650  unset($config);
651  }
652 
653  $config = $this->connection->getConfig($this->channel);
654  try {
655  return new $cls($config->getTranslator(get_called_class()));
656  } catch (\Erebot\Exception $e) {
657  // The channel lacked a specific config. Use the cascade.
658  }
659  unset($config);
660 
661  $config = $this->connection->getConfig(null);
662  return new $cls($config->getTranslator(get_called_class()));
663  }
664 
676  public function getNumRef($name)
677  {
678  $cls = $this->getFactory('!NumericReference');
679  return new $cls($this->connection, $name);
680  }
681 }
sendMessage($targets, $message, $type=self::MSG_TYPE_PRIVMSG)
Definition: Base.php:320
setFactory($iface, $cls)
Definition: Base.php:234
getNumRef($name)
Definition: Base.php:676
getFormatter($chan)
Definition: Base.php:638
const MSG_TYPE_NOTICE
A notice.
Definition: Base.php:66
const MSG_TYPE_CTCP
A CTCP request.
Definition: Base.php:69
Base class for other (Erebot-related) exceptions.
Definition: Exception.php:27
Definition: CLI.php:21
parseSomething($something, $param, $default)
Definition: Base.php:489
sendCommand($command)
Definition: Base.php:425
$translator
The translator to use for messages coming from this instance.
Definition: Base.php:37
$connection
The connection associated with this instance.
Definition: Base.php:31
Interface for something that can be called.
static stringifiable($item)
Definition: Utils.php:257
static wrap($callable)
removeTimer(\Erebot\TimerInterface $timer)
Definition: Base.php:459
const MSG_TYPE_PRIVMSG
A regular message.
Definition: Base.php:63
__construct($channel)
Definition: Base.php:111
getFactory($iface)
Definition: Base.php:283
parseReal($param, $default=null)
Definition: Base.php:584
const RELOAD_HANDLERS
The module should (re)load its handlers.
Definition: Base.php:56
reloadModule(\Erebot\Interfaces\Connection $connection, $flags)
Definition: Base.php:186
An exception thrown when an invalid value has been passed to a function or method.
registerHelpMethod(\Erebot\CallableInterface $callback)
Definition: Base.php:612
const RELOAD_ALL
The module should (re)load all of its contents.
Definition: Base.php:59
const RELOAD_MEMBERS
The module should (re)load its members.
Definition: Base.php:53
$factories
Factories to use for this module.
Definition: Base.php:40
parseString($param, $default=null)
Definition: Base.php:542
Interface for a timer implementation.
$logger
A logger for this module&#39;s messages.
Definition: Base.php:43
parseInt($param, $default=null)
Definition: Base.php:563
Represents the identity of an IRC user.
Definition: Identity.php:27
$channel
The channel associated with this instance, if any.
Definition: Base.php:34
const RELOAD_INIT
Passed when the module is loaded (instead of reloaded).
Definition: Base.php:47
const MSG_TYPE_CTCPREPLY
A reply to a CTCP request.
Definition: Base.php:72
addTimer(\Erebot\TimerInterface $timer)
Definition: Base.php:443
An abstract class which serves as the base to build additional modules for Erebot.
Definition: Base.php:28
const RELOAD_TESTING
Passed during unittests (currently unused...).
Definition: Base.php:50
parseBool($param, $default=null)
Definition: Base.php:521
static ctcpQuote($message)
Definition: Base.php:399
const MSG_TYPE_ACTION
An action.
Definition: Base.php:75