Source code for the PTG event scheduling bot
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

169 lines
5.9KB

  1. #! /usr/bin/env python
  2. # Copyright (c) 2017, Thierry Carrez
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import argparse
  16. import configparser
  17. import daemon
  18. import irc.bot
  19. import logging.config
  20. import os
  21. import time
  22. import ssl
  23. import ptgbot.db
  24. try:
  25. import daemon.pidlockfile as pid_file_module
  26. except ImportError:
  27. # as of python-daemon 1.6 it doesn't bundle pidlockfile anymore
  28. # instead it depends on lockfile-0.9.1
  29. import daemon.pidfile as pid_file_module
  30. # https://bitbucket.org/jaraco/irc/issue/34/
  31. # irc-client-should-not-crash-on-failed
  32. # ^ This is why pep8 is a bad idea.
  33. irc.client.ServerConnection.buffer_class.errors = 'replace'
  34. ANTI_FLOOD_SLEEP = 2
  35. class PTGBot(irc.bot.SingleServerIRCBot):
  36. log = logging.getLogger("ptgbot.bot")
  37. def __init__(self, nickname, password, server, port, channels, dbfile):
  38. if port == 6697:
  39. factory = irc.connection.Factory(wrapper=ssl.wrap_socket)
  40. irc.bot.SingleServerIRCBot.__init__(self,
  41. [(server, port)],
  42. nickname, nickname,
  43. connect_factory=factory)
  44. else:
  45. irc.bot.SingleServerIRCBot.__init__(self,
  46. [(server, port)],
  47. nickname, nickname)
  48. self.nickname = nickname
  49. self.password = password
  50. self.channel_list = channels
  51. self.identify_msg_cap = False
  52. self.data = ptgbot.db.PTGDataBase(dbfile)
  53. def on_nicknameinuse(self, c, e):
  54. self.log.debug("Nickname in use, releasing")
  55. c.nick(c.get_nickname() + "_")
  56. c.privmsg("nickserv", "identify %s " % self.password)
  57. c.privmsg("nickserv", "ghost %s %s" % (self.nickname, self.password))
  58. c.privmsg("nickserv", "release %s %s" % (self.nickname, self.password))
  59. time.sleep(ANTI_FLOOD_SLEEP)
  60. c.nick(self.nickname)
  61. def on_welcome(self, c, e):
  62. self.identify_msg_cap = False
  63. self.log.debug("Requesting identify-msg capability")
  64. c.cap('REQ', 'identify-msg')
  65. c.cap('END')
  66. if (self.password):
  67. self.log.debug("Identifying to nickserv")
  68. c.privmsg("nickserv", "identify %s " % self.password)
  69. for channel in self.channel_list:
  70. self.log.info("Joining %s" % channel)
  71. c.join(channel)
  72. time.sleep(ANTI_FLOOD_SLEEP)
  73. def on_cap(self, c, e):
  74. self.log.debug("Received cap response %s" % repr(e.arguments))
  75. if e.arguments[0] == 'ACK' and 'identify-msg' in e.arguments[1]:
  76. self.log.debug("identify-msg cap acked")
  77. self.identify_msg_cap = True
  78. def usage(self, channel):
  79. self.send(channel, "Format is '@ROOM [now|next] SESSION'")
  80. def on_pubmsg(self, c, e):
  81. if not self.identify_msg_cap:
  82. self.log.debug("Ignoring message because identify-msg "
  83. "cap not enabled")
  84. return
  85. nick = e.source.split('!')[0]
  86. auth = e.arguments[0][0] == '+'
  87. msg = e.arguments[0][1:]
  88. chan = e.target
  89. if msg.startswith('#') and auth:
  90. words = msg.split()
  91. if len(words) < 3:
  92. self.send(chan, "%s: Incorrect number of arguments" % (nick,))
  93. self.usage(chan)
  94. return
  95. room = words[0][1:].lower()
  96. # TODO: Add test for room/day/person match
  97. adverb = words[1].lower()
  98. session = str.join(' ', words[2:])
  99. if adverb == 'now':
  100. self.data.add_now(room, session)
  101. elif adverb == 'next':
  102. self.data.add_next(room, session)
  103. else:
  104. self.send(chan, "%s: unknown directive '%s'" % (nick, adverb))
  105. self.usage(chan)
  106. def send(self, channel, msg):
  107. self.connection.privmsg(channel, msg)
  108. time.sleep(ANTI_FLOOD_SLEEP)
  109. def start(configpath):
  110. config = configparser.RawConfigParser()
  111. config.read(configpath)
  112. if config.has_option('ircbot', 'log_config'):
  113. log_config = config.get('ircbot', 'log_config')
  114. fp = os.path.expanduser(log_config)
  115. if not os.path.exists(fp):
  116. raise Exception("Unable to read logging config file at %s" % fp)
  117. logging.config.fileConfig(fp)
  118. else:
  119. logging.basicConfig(level=logging.DEBUG)
  120. channels = ['#' + name.strip() for name in
  121. config.get('ircbot', 'channels').split(',')]
  122. bot = PTGBot(config.get('ircbot', 'nick'),
  123. config.get('ircbot', 'pass'),
  124. config.get('ircbot', 'server'),
  125. config.getint('ircbot', 'port'),
  126. channels,
  127. config.get('ircbot', 'db'))
  128. bot.start()
  129. def main():
  130. parser = argparse.ArgumentParser(description='PTG bot.')
  131. parser.add_argument('configfile', nargs=1,
  132. help='specify the config file')
  133. parser.add_argument('-d', dest='nodaemon', action='store_true',
  134. help='do not run as a daemon')
  135. args = parser.parse_args()
  136. if not args.nodaemon:
  137. pid = pid_file_module.TimeoutPIDLockFile(
  138. "/var/run/ptgbot/ptgbot.pid", 10)
  139. with daemon.DaemonContext(pidfile=pid):
  140. start(args.configfile)
  141. start(args.configfile)
  142. if __name__ == "__main__":
  143. main()