Advertisement
Guest User

Untitled

a guest
Jun 27th, 2016
68
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 24.82 KB | None | 0 0
  1. A guide to creating a socket based IRC bot in Python. This guide will teach you to create a class that connects to and interacts with IRC data.
  2.  
  3. <!--more-->
  4. <h2><strong>Disclaimer:</strong></h2>
  5. I do not discourage the use of a 3rd party library such as Twisted for IRC, If you do wish to create one with sockets this guide will help.
  6.  
  7. &nbsp;
  8. <h2><strong>Introduction:</strong></h2>
  9. IRC is a well-known simple socket based chat system. Lots of people use it day-to-day for numerous reasons. Most languages have socket wrappers / libraries which makes it very easy to create programs that interact with IRC. This guide will be creating a class, You will need to know how to create these before reading this guide. I will be littering the examples with comments so that you can get a good grasp of what is going on (hopefully). I will also be obeying PEP8's indentation and line length standard, Python is all about the readability ;)
  10.  
  11. Here is a very useful <a href="https://tools.ietf.org/html/rfc1459#section-4.1" target="_blank">link to the RFC</a> if you want to find out more about IRC.
  12. <h2></h2>
  13. <h2>A little about the IRC syntax:</h2>
  14. Generally all IRC servers today follow a standard syntax, with the exception of a few different commands. The data you get from IRC will be newline (\r\n) separated messages. Each message type will have different data. For example, PRIVMSG will look something like this:
  15. <pre class="lang:python highlight:0 decode:true">:nickname!~host@ipaddress PRIVMSG #channel :message here\r\n</pre>
  16. JOIN will look like:
  17. <pre class="lang:python highlight:0 decode:true">:nickname!~host@ipaddress JOIN #channel\r\n</pre>
  18. Im sure you can see a pattern forming there, generally the data is made up of things like nickname and host, message type, channel name etc. There are exceptions to this style so don't rely on data always looking like that.
  19.  
  20. Things like PING or ERROR have a different syntax. PING will look like <span class="lang:python highlight:0 decode:true crayon-inline">PING :server.name\r\n</span> and ERROR will look something like <span class="lang:python highlight:0 decode:true crayon-inline">ERROR :Closing Link:\r\n</span>
  21.  
  22. The data you send to IRC is a little simpler. Ill cover the standard ones. Each one should end with \r\n. You can also chain them together like this. eg:
  23. <pre class="lang:python highlight:0 decode:true ">PRIVMSG #channel :bye!\r\nPART #channel\r\n</pre>
  24. <ul>
  25. <li>PRIVMSG - Sending a channel message - <span class="lang:python highlight:0 decode:true crayon-inline">PRIVMSG #channel :My message here\r\n</span>
  26. <ul>
  27. <li>If channel is a nickname it will be sent in PM (Private Message)</li>
  28. </ul>
  29. </li>
  30. <li>JOIN - Join a channel - <span class="lang:python highlight:0 decode:true crayon-inline ">JOIN #channel\r\n</span></li>
  31. <li>PART - Leave a channel - <span class="lang:python highlight:0 decode:true crayon-inline">PART #channel\r\n</span></li>
  32. <li>NICK - How you specify your nickname. This should be sent once when you connect to specify what nick you want and again at any time you want to change your nickname - <span class="lang:python highlight:0 decode:true crayon-inline">NICK nickname\r\n</span></li>
  33. <li>QUIT - Totally leaves IRC - <span class="lang:python highlight:0 decode:true crayon-inline">QUIT :reason why you quit\r\n</span></li>
  34. </ul>
  35. There are a lot more messages you can send, the RFC will cover all of them. I also suggest you print the data the bot receives so you can see what is happening, this makes it easy to get familiar with what IRC will send you.
  36. <h2></h2>
  37. <h2><strong>Creating the class:</strong></h2>
  38. Okay as I've said we will be using a class. We will call this class Connection.
  39.  
  40. &nbsp;
  41. <h3><strong>__init__()</strong></h3>
  42. The __init__ constructor will need to take a few params. Lets cover these.
  43. <ul>
  44. <li><strong>server - </strong>The server the bot will connect to. (We will use irc.freenode.net for this guide)</li>
  45. <li><strong>port - </strong>The port it will connect on. (usually 6667, It might be different on other servers)</li>
  46. <li><strong>nickname - </strong>The nickname for the bot.</li>
  47. <li><strong>realname - </strong>This shows up when someone uses /whois nickname. This can be anything you want. More than one word.</li>
  48. <li><strong>password - </strong>The password for the server. (Defaults to None)</li>
  49. </ul>
  50. We only need to set these variables and __init__() is done. You can do anything you want after these vars are set, such as creating log files or whatever you want.
  51. <pre class="lang:python decode:true">#!/usr/bin/env python3
  52.  
  53.  
  54. import time
  55. import socket
  56. import random
  57.  
  58.  
  59. class Connection:
  60.  
  61. def __init__(self, server, port, nickname, realname, password=None):
  62. self.server = server
  63. self.port = port
  64. self.nickname = nickname
  65. self.realname = realname
  66. self.password = password</pre>
  67. &nbsp;
  68. <h3><strong>send()</strong></h3>
  69. By default the socket will have a .send() method but I wrap around this to make things easier to use. The params are
  70. <ul>
  71. <li><strong>data - </strong>The data to be sent.</li>
  72. <li><strong>end - </strong>The thing to append to the end of data. (defaults to \r\n)</li>
  73. </ul>
  74. <pre class="lang:python decode:true"> def send(self, data, end='\r\n'):
  75. # print('[out] {}'.format(data), end=end)
  76. # Encode and send the data.
  77. self.socket.send('{}{}'.format(data, end).encode('utf-8'))
  78. return None</pre>
  79. <h3><strong>connect()</strong></h3>
  80. After we get the variables set we should connect. This method will create the socket instance, we will use the default values for the socket. This method returns True if the connection was a success and False otherwise. After we connect we need to identify the bot by sending NICK / USER messages. params:
  81. <ul>
  82. <li><strong>attempts - </strong>The number of times it will try to connect if it fails. (Defaults to 10)</li>
  83. <li><strong>delay - </strong>The delay between each attempt. This will be multiplied by the attempt number + 1. (Defaults to 10)</li>
  84. </ul>
  85. <pre class="lang:python decode:true"> def connect(self, attempts=10, delay=10):
  86. self.socket = socket.socket()
  87. for i in range(attempts):
  88. # Wrap the connect in a try/except so it can try again if it
  89. # fails.
  90. try:
  91. self.socket.connect((self.server, self.port))
  92. except OSError:
  93. # If there was an error connecting sleep.
  94. # This delay will increase each time it fails.
  95. time.sleep((i+1)*delay)
  96. else:
  97. # Identify the bot with the server. PASS / NICK / USER
  98. # PASS password
  99. # NICK nickname
  100. # USER nickname user host :realname
  101. # For user and host we will just use the nickname.
  102. # If you have set a password send it first.
  103. if self.password is not None:
  104. self.send('PASS {}'.format(password))
  105. self.send('NICK {}'.format(self.nickname))
  106. self.send('USER {0} {0} {0} :{1}'.format(self.nickname,
  107. self.realname))
  108. return True
  109. return False</pre>
  110. &nbsp;
  111. <h3><strong>receive_loop()</strong></h3>
  112. This is the loop that does all the heavy lifting. Receiving and parsing the data. It will then call a function according to the data it receives. I explain this system in the code.
  113. <pre class="scroll:true lang:python decode:true"> def receive_loop(self):
  114. # Create a variable to append data to.
  115. data = b''
  116. # Set the timeout to 300 seconds. If the bot hasn't received data
  117. # in 300 seconds it is disconnected. This is used as a fallback
  118. # if the recv == b'': line fails to work.
  119. self.socket.settimeout(300)
  120. while True:
  121. try:
  122. # Append the received data to the recv variable.
  123. # I found 1024 works well for the amount of bytes received.
  124. recv = self.socket.recv(1024)
  125. # Check if the data is blank, this will happen if the bot
  126. # has been disconnected.
  127. if recv == b'':
  128. raise ConnectionResetError
  129. # Append the data that was just received to the data variable.
  130. data += recv
  131. except (OSError, ConnectionResetError, TimeoutError):
  132. # The bot has been disconnected for some reason.
  133. # Wait for 30 seconds then reconnect.
  134. time.sleep(30)
  135. self.connect()
  136. continue
  137. else:
  138. # If no error has been encountered loop through each line
  139. # of the data. If some of it doesn't end with \r\n
  140. # it will stay in the data variable until more data comes in.
  141. # Somtimes IRC sends half a line then the next .recv()
  142. # it will send the rest. This handles that.
  143. while b'\r\n' in data:
  144. line, data = data.split(b'\r\n', 1)
  145. print( '[in] {}'.format(line) )
  146. # This is where we decode and handle the data.
  147. # I prefer to split the data by space and handle each
  148. # segment. You will see why I do this once we start
  149. # receiving data.
  150. split = line.decode('utf-8').split(' ')
  151. # IRC sends certain message 'types' like PRIVMSG JOIN PART
  152. # etc. All of these apart for PING and ERROR are in the
  153. # second index of the split list if we split the line
  154. # by a space. PING and ERROR are always in the first.
  155. # We check the first index to see if it matches any
  156. # of the functions this class has. This creates a
  157. # system where each time the bot sees a certain message
  158. # type it will call the related function.
  159. # (I prefix it with r_ since some of the IRC messages
  160. # are numbers)
  161. first = getattr(self, 'r_{}'.format(split[0]), None)
  162. if first is not None:
  163. # The class either has r_PING or r_ERROR defined.
  164. # Call it with the rest of the data.
  165. # A line such as "PING :server" will end up calling
  166. # r_PING(':server')
  167. first(*split[1:])
  168. # Check for the second index. (PRIVMSG JOIN PART etc.)
  169. second = getattr(self, 'r_{}'.format(split[1]), None)
  170. if second is not None:
  171. # We send all but the second index in the params.
  172. # A line such as
  173. # :nickname!~host@ipaddress PRIVMSG #channel :message
  174. # will end up calling
  175. # r_PRIVMSG(':nickname!~host@ipaddress', '#channel', ':message')
  176. # Yes the message and nickname are prefixed with :
  177. # but it is very easy to remove that in the function
  178. # that gets called.
  179. second(*[split[0]] + split[2:])
  180. return None</pre>
  181. &nbsp;
  182. <h3><strong>privmsg()</strong></h3>
  183. A wrapper of .send() that makes sending messages to channels easier.
  184. <pre class="lang:python decode:true"> def privmsg(self, channel, message):
  185. # Sends a message to a channel.
  186. self.send('PRIVMSG {} :{}'.format(channel, message))
  187. return None</pre>
  188. &nbsp;
  189. <h3><strong>r_PING()</strong></h3>
  190. This function gets called every time the bot finds a PING message. Sending PONG keeps it alive.
  191. <pre class="lang:python decode:true"> def r_PING(self, server):
  192. # Sends PONG as a response. This keeps the bot connected.
  193. self.send('PONG {}'.format(server))
  194. return None</pre>
  195. &nbsp;
  196. <h3><strong>r_433()</strong></h3>
  197. Gets called when the nickname is currently in use. I just add _ to the end.
  198. <pre class="lang:python decode:true"> def r_433(self, *_):
  199. # Append _ to the nickname if the current nickname is in use.
  200. self.nickname = '{}_'.format(self.nickname)
  201. self.send('NICK {}'.format(self.nickname))
  202. return None</pre>
  203. &nbsp;
  204.  
  205. Now that we have this code created we have 2 choices of how we want to continue. You can leave it as that so you have a reusable simple base for any IRC program you want or build the bot into this class. I recommend the first, leaving this class as it currently is.
  206.  
  207. &nbsp;
  208. <h2><strong>Creating another class - Extending classes - The bot class:</strong></h2>
  209. &nbsp;
  210. <pre class="lang:python decode:true">class Bot(Connection):
  211.  
  212. def __init__(self, server, port, nickname, password=None):
  213. Connection.__init__(self, server, port, nickname, password)
  214. # Attempt to connect.
  215. connected = self.connect()
  216. # If the bot is connected start the receive loop.
  217. if connected:
  218. self.receive_loop()</pre>
  219. &nbsp;
  220. <h3><strong>r_001()</strong></h3>
  221. This is the first message the server sends. Perfect for sending JOIN (joining a channel). Usually I would send JOIN after the end of MOTD message, however, some servers do not send this. All servers send 001.
  222. <pre class="lang:python decode:true"> def r_001(self, *_):
  223. # 001 is the perfect time to join a channel. All servers send 001
  224. # so this will always happen.
  225. self.send('JOIN #Sjc_Bot')
  226. return None</pre>
  227. &nbsp;
  228. <h3></h3>
  229. This will get called when the bot receives a PRIVMSG, which is any channel message. params:
  230. <ul>
  231. <li><strong>host - </strong>The full nickname and host for the person who just spoke.</li>
  232. <li><strong>channel - </strong>The channel they spoke in.</li>
  233. <li><strong>*message - </strong>A tuple of the space separated message.</li>
  234. </ul>
  235. &nbsp;
  236. <pre class="lang:python decode:true"> def r_PRIVMSG(self, host, channel, *message):
  237. # Get the nickname and host.
  238. nickname, host = host[1:].split('!')
  239. # If the channel is the bots nickname change the channel
  240. # to whoever called it. This enables the bot to respond in PM
  241. # if the user talks to it in PM (Private Message).
  242. if channel == self.nickname:
  243. channel = nickname
  244. # Join the message by space, then remove the :
  245. # then split it into command and params
  246. command, *params = ' '.join(message)[1:].split(' ')
  247. # Feel free to use any command system here. I will just show you a
  248. # simple one.
  249. if command == '!hello':
  250. # Responds to !hello with Hello nickname
  251. self.privmsg(channel, 'Hello {}!'.format(nickname))
  252.  
  253. if command == '!random':
  254. # Randomizes between params
  255. if len(params) == 1:
  256. try:
  257. number = int(params[0])
  258. except ValueError:
  259. number = 10
  260. output = random.randint(number)
  261. if len(params) == 2:
  262. try:
  263. start = int(params[0])
  264. end = int(params[1])
  265. except ValueError:
  266. start = 0
  267. end = 10
  268. output = random.randrange(start, end)
  269. self.privmsg(channel, str(output))
  270. return None</pre>
  271. &nbsp;
  272.  
  273. &nbsp;
  274. <h2><strong>Bringing it together</strong></h2>
  275. <pre class="lang:python decode:true">#!/usr/bin/env python3
  276.  
  277.  
  278. import time
  279. import socket
  280. import random
  281.  
  282.  
  283. class Connection:
  284.  
  285. def __init__(self, server, port, nickname, realname, password=None):
  286. self.server = server
  287. self.port = port
  288. self.nickname = nickname
  289. self.realname = realname
  290. self.password = password
  291.  
  292. def send(self, data, end='\r\n'):
  293. # print('[out] {}'.format(data), end=end)
  294. # Encode and send the data.
  295. self.socket.send('{}{}'.format(data, end).encode('utf-8'))
  296. return None
  297.  
  298. def connect(self, attempts=10, delay=10):
  299. self.socket = socket.socket()
  300. for i in range(attempts):
  301. # Wrap the connect in a try/except so it can try again if it
  302. # fails.
  303. try:
  304. self.socket.connect((self.server, self.port))
  305. except OSError:
  306. # If there was an error connecting sleep.
  307. # This delay will increase each time it fails.
  308. time.sleep((i+1)*delay)
  309. else:
  310. # Identify the bot with the server. PASS / NICK / USER
  311. # PASS password
  312. # NICK nickname
  313. # USER nickname user host :realname
  314. # For user and host we will just use the nickname.
  315. # If you have set a password send it first.
  316. if self.password is not None:
  317. self.send('PASS {}'.format(password))
  318. self.send('NICK {}'.format(self.nickname))
  319. self.send('USER {0} {0} {0} :{1}'.format(self.nickname,
  320. self.realname))
  321. return True
  322. return False
  323.  
  324. def receive_loop(self):
  325. # Create a variable to append data to.
  326. data = b''
  327. # Set the timeout to 300 seconds. If the bot hasn't received data
  328. # in 300 seconds it is disconnected. This is used as a fallback
  329. # if the recv == b'': line fails to work.
  330. self.socket.settimeout(300)
  331. while True:
  332. try:
  333. # Append the received data to the recv variable.
  334. # I found 1024 works well for the amount of bytes received.
  335. recv = self.socket.recv(1024)
  336. # Check if the data is blank, this will happen if the bot
  337. # has been disconnected.
  338. if recv == b'':
  339. raise ConnectionResetError
  340. # Append the data that was just received to the data variable.
  341. data += recv
  342. except (OSError, ConnectionResetError, TimeoutError):
  343. # The bot has been disconnected for some reason.
  344. # Wait for 30 seconds then reconnect.
  345. time.sleep(30)
  346. self.connect()
  347. continue
  348. else:
  349. # If no error has been encountered loop through each line
  350. # of the data. If some of it doesn't end with \r\n
  351. # it will stay in the data variable until more data comes in.
  352. # Somtimes IRC sends half a line then the next .recv()
  353. # it will send the rest. This handles that.
  354. while b'\r\n' in data:
  355. line, data = data.split(b'\r\n', 1)
  356. print( '[in] {}'.format(line) )
  357. # This is where we decode and handle the data.
  358. # I prefer to split the data by space and handle each
  359. # segment. You will see why I do this once we start
  360. # receiving data.
  361. split = line.decode('utf-8').split(' ')
  362. # IRC sends certain message 'types' like PRIVMSG JOIN PART
  363. # etc. All of these apart for PING and ERROR are in the
  364. # second index of the split list if we split the line
  365. # by a space. PING and ERROR are always in the first.
  366. # We check the first index to see if it matches any
  367. # of the functions this class has. This creates a
  368. # system where each time the bot sees a certain message
  369. # type it will call the related function.
  370. # (I prefix it with r_ since some of the IRC messages
  371. # are numbers)
  372. first = getattr(self, 'r_{}'.format(split[0]), None)
  373. if first is not None:
  374. # The class either has r_PING or r_ERROR defined.
  375. # Call it with the rest of the data.
  376. # A line such as "PING :server" will end up calling
  377. # r_PING(':server')
  378. first(*split[1:])
  379. # Check for the second index. (PRIVMSG JOIN PART etc.)
  380. second = getattr(self, 'r_{}'.format(split[1]), None)
  381. if second is not None:
  382. # We send all but the second index in the params.
  383. # A line such as
  384. # :nickname!~host@ipaddress PRIVMSG #channel :message
  385. # will end up calling
  386. # r_PRIVMSG(':nickname!~host@ipaddress', '#channel', ':message')
  387. # Yes the message and nickname are prefixed with :
  388. # but it is very easy to remove that in the function
  389. # that gets called.
  390. second(*[split[0]] + split[2:])
  391. return None
  392.  
  393. def privmsg(self, channel, message):
  394. # Sends a message to a channel.
  395. self.send('PRIVMSG {} :{}'.format(channel, message))
  396. return None
  397.  
  398. def r_PING(self, server):
  399. # Sends PONG as a response. This keeps the bot connected.
  400. self.send('PONG {}'.format(server))
  401. return None
  402.  
  403. def r_433(self, *_):
  404. # Append _ to the nickname if the current nickname is in use.
  405. self.nickname = '{}_'.format(self.nickname)
  406. self.send('NICK {}'.format(self.nickname))
  407. return None
  408.  
  409.  
  410. class Bot(Connection):
  411.  
  412. def __init__(self, server, port, nickname, password=None):
  413. Connection.__init__(self, server, port, nickname, password)
  414. # Attempt to connect.
  415. connected = self.connect()
  416. # If the bot is connected start the receive loop.
  417. if connected:
  418. self.receive_loop()
  419.  
  420. def r_001(self, *_):
  421. # 001 is the perfect time to join a channel. All servers send 001
  422. # so this will always happen.
  423. self.send('JOIN #Sjc_Bot')
  424. return None
  425.  
  426. def r_PRIVMSG(self, host, channel, *message):
  427. # Get the nickname and host.
  428. nickname, host = host[1:].split('!')
  429. # If the channel is the bots nickname change the channel
  430. # to whoever called it. This enables the bot to respond in PM
  431. # if the user talks to it in PM (Private Message).
  432. if channel == self.nickname:
  433. channel = nickname
  434. # Join the message by space, then remove the :
  435. # then split it into command and params
  436. command, *params = ' '.join(message)[1:].split(' ')
  437. # Feel free to use any command system here. I will just show you a
  438. # simple one.
  439. if command == '!hello':
  440. # Responds to !hello with Hello nickname
  441. self.privmsg(channel, 'Hello {}!'.format(nickname))
  442.  
  443. if command == '!random':
  444. # Randomizes between params
  445. if len(params) == 1:
  446. try:
  447. number = int(params[0])
  448. except ValueError:
  449. number = 10
  450. output = random.randint(number)
  451. if len(params) == 2:
  452. try:
  453. start = int(params[0])
  454. end = int(params[1])
  455. except ValueError:
  456. start = 0
  457. end = 10
  458. output = random.randrange(start, end)
  459. self.privmsg(channel, str(output))
  460. return None
  461.  
  462.  
  463. def main():
  464. Bot('irc.freenode.net', 6667, 'MyBot123532')
  465. return None
  466.  
  467. if __name__ == '__main__':
  468. main()
  469. </pre>
  470. &nbsp;
  471. <h3>Threading:</h3>
  472. The bot that you just made is purely single threaded. This means if any commands take a long time to run it will not receive more data until the command is done. This can be easily fixed with this little gem - A threading decorator.
  473. <pre class="lang:python decode:true ">import threading
  474.  
  475.  
  476. class asthread(object):
  477. def __init__(self, daemon=False):
  478. self.daemon = daemon
  479.  
  480. def __call__(self, function):
  481. def inner(*args, **kwargs):
  482. thread = threading.Thread(target=function, args=args,
  483. kwargs=kwargs)
  484. thread.daemon = self.daemon
  485. thread.start()
  486. return None
  487. return inner</pre>
  488. Then you can add @asthread() to the top of any function and it will be called in a new thread.
  489.  
  490. Example:
  491. <pre class="lang:python decode:true "> @asthread()
  492. def r_001(self, *_):
  493. # 001 is the perfect time to join a channel. All servers send 001
  494. # so this will always happen.
  495. self.send('JOIN #Sjc_Bot')
  496. return None</pre>
  497. The only downside is the return value will not get passed to where it was called from.
  498.  
  499. I hope this was helpful :)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement