Advertisement
Guest User

Untitled

a guest
Mar 28th, 2017
113
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 14.79 KB | None | 0 0
  1. from _ssl import PROTOCOL_SSLv23
  2. import asyncio
  3. import re
  4. import ssl
  5. import logging
  6. from ssl import SSLContext
  7.  
  8. from cloudbot.client import Client
  9. from cloudbot.event import Event, EventType
  10.  
  11. logger = logging.getLogger("cloudbot")
  12.  
  13. irc_prefix_re = re.compile(r":([^ ]*) ([^ ]*) (.*)")
  14. irc_noprefix_re = re.compile(r"([^ ]*) (.*)")
  15. irc_netmask_re = re.compile(r"([^!@]*)!([^@]*)@(.*)")
  16. irc_param_re = re.compile(r"(?:^|(?<= ))(:.*|[^ ]+)")
  17.  
  18. irc_bad_chars = ''.join([chr(x) for x in list(range(0, 1)) + list(range(4, 32)) + list(range(127, 160))])
  19. irc_clean_re = re.compile('[{}]'.format(re.escape(irc_bad_chars)))
  20.  
  21. def irc_clean(dirty):
  22. return irc_clean_re.sub('',dirty)
  23.  
  24. irc_bad_chars = ''.join([chr(x) for x in list(range(0, 32)) + list(range(127, 160))])
  25. irc_clean_re = re.compile('[{}]'.format(re.escape(irc_bad_chars)))
  26.  
  27. irc_command_to_event_type = {
  28. "PRIVMSG": EventType.message,
  29. "JOIN": EventType.join,
  30. "PART": EventType.part,
  31. "KICK": EventType.kick,
  32. "NOTICE": EventType.notice
  33. }
  34.  
  35.  
  36. def decode(bytestring):
  37. """
  38. Tries to decode a bytestring using multiple encoding formats
  39. """
  40. for codec in ('utf-8', 'iso-8859-1', 'shift_jis', 'cp1252'):
  41. try:
  42. return bytestring.decode(codec)
  43. except UnicodeDecodeError:
  44. continue
  45. return bytestring.decode('utf-8', errors='ignore')
  46.  
  47.  
  48. class IrcClient(Client):
  49. """
  50. An implementation of Client for IRC.
  51. :type use_ssl: bool
  52. :type server: str
  53. :type port: int
  54. :type _connected: bool
  55. :type _ignore_cert_errors: bool
  56. """
  57.  
  58. def __init__(self, bot, name, nick, *, channels=None, config=None,
  59. server, port=6667, use_ssl=False, ignore_cert_errors=True, timeout=300, local_bind=False):
  60. """
  61. :type bot: cloudbot.bot.CloudBot
  62. :type name: str
  63. :type nick: str
  64. :type channels: list[str]
  65. :type config: dict[str, unknown]
  66. :type server: str
  67. :type port: int
  68. :type use_ssl: bool
  69. :type ignore_cert_errors: bool
  70. :type timeout: int
  71. """
  72. super().__init__(bot, name, nick, channels=channels, config=config)
  73.  
  74. self.use_ssl = use_ssl
  75. self._ignore_cert_errors = ignore_cert_errors
  76. self._timeout = timeout
  77. self.server = server
  78. self.port = port
  79. self.local_bind = local_bind
  80. # create SSL context
  81. if self.use_ssl:
  82. self.ssl_context = SSLContext(PROTOCOL_SSLv23)
  83. if self._ignore_cert_errors:
  84. self.ssl_context.verify_mode = ssl.CERT_NONE
  85. else:
  86. self.ssl_context.verify_mode = ssl.CERT_REQUIRED
  87. else:
  88. self.ssl_context = None
  89.  
  90. # if we're connected
  91. self._connected = False
  92. # if we've quit
  93. self._quit = False
  94.  
  95. # transport and protocol
  96. self._transport = None
  97. self._protocol = None
  98.  
  99. def describe_server(self):
  100. if self.use_ssl:
  101. return "+{}:{}".format(self.server, self.port)
  102. else:
  103. return "{}:{}".format(self.server, self.port)
  104.  
  105. @asyncio.coroutine
  106. def connect(self):
  107. """
  108. Connects to the IRC server, or reconnects if already connected.
  109. """
  110. # connect to the clients server
  111. if self._quit:
  112. # we've quit, so close instead (because this has probably been called because of EOF received)
  113. self.close()
  114. return
  115.  
  116. if self._connected:
  117. logger.info("[{}] Reconnecting".format(self.name))
  118. self._transport.close()
  119. else:
  120. self._connected = True
  121. logger.info("[{}] Connecting".format(self.name))
  122. optional_params = {}
  123. if self.local_bind:
  124. optional_params["local_addr"] = self.local_bind
  125. self._transport, self._protocol = yield from self.loop.create_connection(
  126. lambda: _IrcProtocol(self), host=self.server, port=self.port, ssl=self.ssl_context, **optional_params)
  127.  
  128. # send the password, nick, and user
  129. self.set_pass(self.config["connection"].get("password"))
  130. self.set_nick(self.nick)
  131. self.cmd("USER", self.config.get('user', 'cloudbot'), "3", "*",
  132. self.config.get('realname', 'CloudBot - https://git.io/CloudBot'))
  133.  
  134. def quit(self, reason=None):
  135. if self._quit:
  136. return
  137. self._quit = True
  138. if reason:
  139. self.cmd("QUIT", reason)
  140. else:
  141. self.cmd("QUIT")
  142.  
  143. def close(self):
  144. if not self._quit:
  145. self.quit()
  146. if not self._connected:
  147. return
  148.  
  149. self._transport.close()
  150. self._connected = False
  151.  
  152. def message(self, target, *messages, sanatize=True):
  153. for text in messages:
  154. <<<<<<< HEAD
  155. text = "".join(text.splitlines())
  156. self.cmd("PRIVMSG", target, text)
  157.  
  158. def action(self, target, text):
  159. text = "".join(text.splitlines())
  160. self.ctcp(target, "ACTION", text)
  161.  
  162. def notice(self, target, text):
  163. text = "".join(text.splitlines())
  164. =======
  165. if sanatize == True:
  166. text = "".join(text.splitlines())
  167. self.cmd("PRIVMSG", target, text)
  168.  
  169. def action(self, target, text, sanatize=True):
  170. if sanatize == True:
  171. text = "".join(text.splitlines())
  172. self.ctcp(target, "ACTION", text)
  173.  
  174. def notice(self, target, text, sanatize=True):
  175. if sanatize == True:
  176. text = "".join(text.splitlines())
  177. >>>>>>> upstream/master
  178. self.cmd("NOTICE", target, text)
  179.  
  180. def set_nick(self, nick):
  181. self.cmd("NICK", nick)
  182.  
  183. def join(self, channel):
  184. self.send("JOIN {}".format(channel))
  185. if channel not in self.channels:
  186. self.channels.append(channel)
  187.  
  188. def part(self, channel):
  189. self.cmd("PART", channel)
  190. if channel in self.channels:
  191. self.channels.remove(channel)
  192.  
  193. def set_pass(self, password):
  194. if not password:
  195. return
  196. self.cmd("PASS", password)
  197.  
  198. def ctcp(self, target, ctcp_type, text):
  199. """
  200. Makes the bot send a PRIVMSG CTCP of type <ctcp_type> to the target
  201. :type ctcp_type: str
  202. :type text: str
  203. :type target: str
  204. """
  205. out = "\x01{} {}\x01".format(ctcp_type, text)
  206. self.cmd("PRIVMSG", target, out)
  207.  
  208. def cmd(self, command, *params):
  209. """
  210. Sends a raw IRC command of type <command> with params <params>
  211. :param command: The IRC command to send
  212. :param params: The params to the IRC command
  213. :type command: str
  214. :type params: (str)
  215. """
  216. params = list(params) # turn the tuple of parameters into a list
  217. if params:
  218. params[-1] = ':' + params[-1]
  219. self.send("{} {}".format(command, ' '.join(params)))
  220. else:
  221. self.send(command)
  222.  
  223. def send(self, line):
  224. """
  225. Sends a raw IRC line
  226. :type line: str
  227. """
  228. if not self._connected:
  229. raise ValueError("Client must be connected to irc server to use send")
  230. self.loop.call_soon_threadsafe(self._send, line)
  231.  
  232. def _send(self, line):
  233. """
  234. Sends a raw IRC line unchecked. Doesn't do connected check, and is *not* threadsafe
  235. :type line: str
  236. """
  237. logger.info("[{}] >> {}".format(self.name, line))
  238. asyncio.async(self._protocol.send(line), loop=self.loop)
  239.  
  240.  
  241. @property
  242. def connected(self):
  243. return self._connected
  244.  
  245.  
  246. class _IrcProtocol(asyncio.Protocol):
  247. """
  248. :type loop: asyncio.events.AbstractEventLoop
  249. :type conn: IrcClient
  250. :type bot: cloudbot.bot.CloudBot
  251. :type _input_buffer: bytes
  252. :type _connected: bool
  253. :type _transport: asyncio.transports.Transport
  254. :type _connected_future: asyncio.Future
  255. """
  256.  
  257. def __init__(self, conn):
  258. """
  259. :type conn: IrcClient
  260. """
  261. self.loop = conn.loop
  262. self.bot = conn.bot
  263. self.conn = conn
  264.  
  265. # input buffer
  266. self._input_buffer = b""
  267.  
  268. # connected
  269. self._connected = False
  270.  
  271. # transport
  272. self._transport = None
  273.  
  274. # Future that waits until we are connected
  275. self._connected_future = asyncio.Future(loop=self.loop)
  276.  
  277. def connection_made(self, transport):
  278. self._transport = transport
  279. self._connected = True
  280. self._connected_future.set_result(None)
  281. # we don't need the _connected_future, everything uses it will check _connected first.
  282. del self._connected_future
  283.  
  284. def connection_lost(self, exc):
  285. self._connected = False
  286. # create a new connected_future for when we are connected.
  287. self._connected_future = asyncio.Future(loop=self.loop)
  288. if exc is None:
  289. # we've been closed intentionally, so don't reconnect
  290. return
  291. logger.error("[{}] Connection lost: {}".format(self.conn.name, exc))
  292. asyncio.async(self.conn.connect(), loop=self.loop)
  293.  
  294. def eof_received(self):
  295. self._connected = False
  296. # create a new connected_future for when we are connected.
  297. self._connected_future = asyncio.Future(loop=self.loop)
  298. logger.info("[{}] EOF received.".format(self.conn.name))
  299. asyncio.async(self.conn.connect(), loop=self.loop)
  300. return True
  301.  
  302. @asyncio.coroutine
  303. def send(self, line):
  304. # make sure we are connected before sending
  305. if not self._connected:
  306. yield from self._connected_future
  307. line = line[:510] + "\r\n"
  308. data = line.encode("utf-8", "replace")
  309. self._transport.write(data)
  310.  
  311. def data_received(self, data):
  312. self._input_buffer += data
  313.  
  314. while b"\r\n" in self._input_buffer:
  315. line_data, self._input_buffer = self._input_buffer.split(b"\r\n", 1)
  316. line = decode(line_data)
  317.  
  318. # parse the line into a message
  319. if line.startswith(":"):
  320. prefix_line_match = irc_prefix_re.match(line)
  321. if prefix_line_match is None:
  322. logger.critical("[{}] Received invalid IRC line '{}' from {}".format(
  323. self.conn.name, line, self.conn.describe_server()))
  324. continue
  325.  
  326. netmask_prefix, command, params = prefix_line_match.groups()
  327. prefix = ":" + netmask_prefix # TODO: Do we need to know this?
  328. netmask_match = irc_netmask_re.match(netmask_prefix)
  329. if netmask_match is None:
  330. # This isn't in the format of a netmask
  331. nick = netmask_prefix
  332. user = None
  333. host = None
  334. mask = netmask_prefix
  335. else:
  336. nick = netmask_match.group(1)
  337. user = netmask_match.group(2)
  338. host = netmask_match.group(3)
  339. mask = netmask_prefix
  340. else:
  341. prefix = None
  342. noprefix_line_match = irc_noprefix_re.match(line)
  343. if noprefix_line_match is None:
  344. logger.critical("[{}] Received invalid IRC line '{}' from {}".format(
  345. self.conn.name, line, self.conn.describe_server()))
  346. continue
  347. command = noprefix_line_match.group(1)
  348. params = noprefix_line_match.group(2)
  349. nick = None
  350. user = None
  351. host = None
  352. mask = None
  353.  
  354. command_params = irc_param_re.findall(params)
  355.  
  356. # Reply to pings immediately
  357.  
  358. if command == "PING":
  359. asyncio.async(self.send("PONG " + command_params[-1]), loop=self.loop)
  360.  
  361. # Parse the command and params
  362.  
  363. # Content
  364. if command_params and command_params[-1].startswith(":"):
  365. # If the last param is in the format of `:content` remove the `:` from it, and set content from it
  366. content_raw = command_params[-1][1:]
  367. content = irc_clean(content_raw)
  368. else:
  369. content_raw = None
  370. content = None
  371.  
  372. # Event type
  373. if command in irc_command_to_event_type:
  374. event_type = irc_command_to_event_type[command]
  375. else:
  376. event_type = EventType.other
  377.  
  378. # Target (for KICK, INVITE)
  379. if event_type is EventType.kick:
  380. target = command_params[1]
  381. elif command == "INVITE":
  382. target = command_params[0]
  383. else:
  384. # TODO: Find more commands which give a target
  385. target = None
  386.  
  387. # Parse for CTCP
  388. if event_type is EventType.message and content_raw.count("\x01") >= 2 and content_raw.startswith("\x01"):
  389. # Remove the first \x01, then rsplit to remove the last one, and ignore text after the last \x01
  390. ctcp_text = content_raw[1:].rsplit("\x01", 1)[0]
  391. ctcp_text_split = ctcp_text.split(None, 1)
  392. if ctcp_text_split[0] == "ACTION":
  393. # this is a CTCP ACTION, set event_type and content accordingly
  394. event_type = EventType.action
  395. content = ctcp_text_split[1]
  396. else:
  397. # this shouldn't be considered a regular message
  398. event_type = EventType.other
  399. else:
  400. ctcp_text = None
  401.  
  402. # Channel
  403. # TODO: Migrate plugins using chan for storage to use chan.lower() instead so we can pass the original case
  404. if command_params and (len(command_params) > 2 or not command_params[0].startswith(":")):
  405.  
  406. if command_params[0].lower() == self.conn.nick.lower():
  407. # this is a private message - set the channel to the sender's nick
  408. channel = nick.lower()
  409. else:
  410. channel = command_params[0].lower()
  411. else:
  412. channel = None
  413.  
  414. # Set up parsed message
  415. # TODO: Do we really want to send the raw `prefix` and `command_params` here?
  416. event = Event(bot=self.bot, conn=self.conn, event_type=event_type, content=content, target=target,
  417. channel=channel, nick=nick, user=user, host=host, mask=mask, irc_raw=line, irc_prefix=prefix,
  418. irc_command=command, irc_paramlist=command_params, irc_ctcp_text=ctcp_text)
  419.  
  420. # handle the message, async
  421. asyncio.async(self.bot.process(event), loop=self.loop)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement