-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathirc.py
More file actions
206 lines (160 loc) · 7.19 KB
/
irc.py
File metadata and controls
206 lines (160 loc) · 7.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import admin, common, ircparser, logger, statekeeper
import socket
import random
import string
from ssl import wrap_socket, SSLError
from time import sleep, time
# run() should *never* for *any* reason throw any exceptions.
# the absolute worst cases should be caught in here and
# then 'reconnect' returned back to main
# This means that every single line should be covered
# by a general "except Exception as e"!!
def run(settings, state, log=logger.log, sock=None):
try:
if sock:
irc = sock
else:
irc = Socket(
settings['irc']['server'],
settings['irc']['port'],
settings['irc']['ssl'],
)
state['last_message'] = time()
state['pinged'] = False
state['nick'] = settings['irc']['nick']
state['joined_channel'] = None
irc.send('NICK {}'.format(state['nick']))
irc.send('USER {0} 0 * :IRC Bot {0}'.format(settings['irc']['nick']))
sleep(1)
except (socket.error, socket.herror, socket.gaierror):
log('error', 'Connection failed. Reconnecting in {} seconds...'.format(settings['irc']['reconnect_delay']))
return 'reconnect'
except Exception as e:
try:
log('error', '{}. Reconnecting in {} seconds...'.format(e, settings['irc']['reconnect_delay']))
except:
log('error', 'Bad config.')
return 'reconnect'
while True:
try:
line = irc.read()
if line:
state['last_message'] = time()
for response in handle(line, settings, state):
irc.send(response)
else:
if not state['pinged'] and time() - state['last_message'] > settings['irc']['grace_period']:
irc.send('PING :arst')
state['pinged'] = True
elif state['pinged'] and time() - state['last_message'] > settings['irc']['grace_period']:
log('error', 'Connection timed out. Reconnecting in {} seconds...'.format(settings['irc']['reconnect_delay']))
return 'reconnect'
# TODO: These exceptions should be handled in the socket class. They are overridden
# earlier in this function in the handshake.
except BrokenPipeError:
log('error', 'Broken pipe. Reconnecting in {} seconds...'.format(settings['irc']['reconnect_delay']))
return 'reconnect'
except ConnectionResetError:
log('error', 'Connection reset. Reconnecting in {} seconds...'.format(settings['irc']['reconnect_delay']))
return 'reconnect'
except (ConnectionAbortedError, ConnectionRefusedError):
log('error', 'Connection refused or aborted. Closing...')
return 'quit'
except Exception as e:
log('error', e)
return 'reconnect'
# TODO: A lot of the stuff in here are candidates for
# admin.py or behaviour.py of course. I'm just putting
# it here for safekeeping
def handle(line, settings, state, log=logger.log):
# TODO: Write docstring about how this yields responses
user, command, arguments = ircparser.split(line)
nick = ircparser.get_nick(user)
if command == 'PING':
yield 'PONG :' + arguments[0]
if command == 'PONG':
state['pinged'] = False
if command == '433':
def new_nick(nick):
nick = nick[:min(len(nick), 6)] # determine how much to shave off to make room for random chars
return '{}_{}'.format(nick, ''.join(random.choice(string.ascii_lowercase + string.digits) for x in range(2)))
log('warning', '[statekeeping] Nick {} already in use, trying another one.'.format(state['nick']))
state['nick'] = new_nick(settings['irc']['nick'])
yield 'NICK {}'.format(state['nick'])
if command == 'JOIN' and nick == state['nick']:
# TODO: Fancy logging
print('--> joined {}'.format(arguments[0]))
if state['joined_channel']:
# TODO: raise some sort of illegal state exception. remember to test for it
# then what? have we joined two channels? what the shit are we supposed to do?
pass
state['joined_channel'] = arguments[0]
if command == 'KICK' and arguments[1] == state['nick']:
log('warning', '[statekeeping] Kicked from channel {} because {}'.format(arguments[0], ' '.join(arguments[1:])))
if arguments[0] != state['joined_channel']:
# TODO: raise some sort of illegal state exception
pass
state['joined_channel'] = None
settings['irc']['channel'] = None
if command == 'PRIVMSG':
# Make the author the target for replies if it is a private message
channel = nick if arguments[0] == state['nick'] else arguments[0]
message = ' '.join(arguments[1:])
# TODO: Turn this into some sort of cooler logging
print('{}> {}'.format(channel, message))
# TODO: Better admin shit, this is just poc/temporary
if admin.is_admin(user):
admin_result = admin.parse_admin_command(message, state['nick'])
if admin_result:
yield ircparser.make_privmsg(channel, admin_result)
if message == 'hello, world':
yield ircparser.make_privmsg(channel, 'why, hello!')
else:
log('raw', line)
# TODO: Fix the state object so this isn't needed
if 'joined_channel' not in state:
state['joined_channel'] = None
if not state['joined_channel'] and settings['irc']['channel']:
yield 'JOIN {}'.format(settings['irc']['channel'])
class Socket:
""" A line buffered IRC socket interface. send(text) sends
text as UTF-8 and appends a newline, read() reads text
and returns a list of strings which are the read lines
without a line separator."""
def __init__(self, server, port, ssl_enabled,
sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM),
ssl_wrap=wrap_socket):
# try to connect
try: sock.connect((server, port))
# try really, really hard
# TODO: except what!?
except: sock.send(bytes('', 'utf-8'))
if ssl_enabled:
sock = ssl_wrap(sock)
sock.settimeout(1)
self.ssl_enabled = ssl_enabled
self.sock = sock
# initialise an empty buffer
self.buffer = b''
def send(self, text):
# TODO: sanity checking
self.sock.send(bytes(text + '\n', 'utf-8'))
def read(self):
try:
if b'\r\n' not in self.buffer:
self.buffer += self.sock.read(4096) if self.ssl_enabled else self.sock.recv(4096)
except socket.timeout:
return None
try:
[byteline, self.buffer] = self.buffer.split(b'\r\n', 1)
except ValueError:
return None
def decode(text, encs):
for enc in encs:
try: return text.decode(enc)
# TODO: except what?
except: continue
# fallback is iso-8859-1
# TODO: why is it, though? why not utf-8?
return text.decode('latin-1', 'replace')
return decode(byteline, ['utf-8', 'latin-1', 'cp1252'])