-
Notifications
You must be signed in to change notification settings - Fork 1
/
multigateway.py
340 lines (241 loc) · 8.52 KB
/
multigateway.py
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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# Requires Py 3.4+
import collections
import configparser
import contextlib
import email
import io
import json
import os
import select
import socket
import sys
import requests
# readers = {
# 'irc': irc_read,
# 'rc': rc_read,
# }
# writers = {
# 'irc': irc_write,
# 'rc': rc_write,
# }
conf = {}
class dotdict(dict):
"""Allows accessing a dict like an object.
Source: http://stackoverflow.com/a/23689767/
"""
def __getattr__(self, attr):
# ConfigParser lowerize all params' name but not sections' name
try:
return self[attr]
except KeyError:
return self[attr.lower()]
__setattr__ = dict.__setattr__
__delattr__ = dict.__delattr__
def parse_headers(raw_headers:str) -> dict:
"""Parses HTTP headers."""
# Source: http://stackoverflow.com/a/40481308/
return dict(email.message_from_file(io.StringIO(raw_headers)).items())
def load_config(filename):
conf = configparser.ConfigParser()
conf.read(filename)
conf = dotdict({
key: dotdict(val) for key, val in conf.items()
})
return conf
def init_rc_hook(host=None, port=None):
if host is None:
host = conf.RC.HOST
if port is None:
port = conf.RC.PORT
rc_hook = socket.socket()
rc_hook.setblocking(0)
# Allows quicker reuse of the address after the server is being resetted
rc_hook.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
rc_hook.bind((host, int(port)))
rc_hook.listen(-1)
return rc_hook
def init_irc_conn(**kwargs):
# Get the params from kwargs
# Then from conf.IRC if absent in kwargs
c = dotdict(
collections.ChainMap(
kwargs,
conf.IRC
)
)
irc = socket.socket()
irc.connect((c.host, int(c.port)))
# /!\ setblocking AFTER connect:
# connect rely on a DNS server that can't always be non blocking
# thus raising an exception.
irc.setblocking(0)
if c.password:
sendcmd(irc, 'PASS {}'.format(c.password))
sendcmd(irc, 'NICK {}'.format(c.bot_name))
sendcmd(irc, 'USER {} {} bla :{}'.format(
c.I,
c.HOST,
c.DESCRIPT
))
sendcmd(irc, 'JOIN {}'.format(c.ROOM))
if c.welcome_msg:
sendcmd(irc, 'PRIVMSG {} :{}'.format(
c.ROOM, c.welcome_msg
))
return irc
def sendcmd(s, msg):
s.sendall((msg+'\r\n').encode('utf-8'))
def sendmsg(s, msg, to=None):
if to is None:
to = conf.IRC.ROOM
sendcmd(s, 'PRIVMSG {} :{}'.format(to, msg))
def recv_data(s):
return s.recv(4096).decode('utf-8')
def http_recv_all(s):
# Reinvented the wheel :)
# To be able to work with non-blocking sockets and select module.
r = ''
headers = {}
while 'content-length' not in headers:
# Note: Blocking but shouldn't be an issue (it's an HTTP request)
r += recv_data(s)
# Separate headers and content
headers = r.split('\r\n\r\n', 1)[0]
# Separate request line and headers (eg "GET / HTTP/1.1")
headers = headers.split('\r\n', 1)[1]
headers = parse_headers(headers)
body = r.split('\r\n\r\n', 1)[1]
# Still not complete? Get moar and retry!
while len(body.encode('utf-8')) < int(headers['content-length'])-1:
r += recv_data(s)
body = r.split('\r\n\r\n', 1)[1]
# Properly close the connection or RC will keep spamming until we do.
s.sendall(b'HTTP/1.0 200 OK\r\n\r\n')
s.close()
return (headers, body)
def handle_irc(irc, readbuffer, room=None, rc=None):
if room is None:
room = conf.IRC.ROOM
if rc is None:
rc = {}
rc = dotdict(collections.ChainMap(rc, conf.RC))
new = recv_data(irc)
if new:
print(new)
# new finishes with "\r\n" IF we received all
# readbuffer may contain uncomplete commands
readbuffer = readbuffer + new
# Last entry is empty if we received all
commands = str.split(readbuffer, '\n')
# Contains nothing if we received all
# Or, ALTERNATIVELY, some uncomplete command
readbuffer = commands.pop()
# Process all BUT the (potentially) uncomplete command lines (in readbuffer)
for cmd in commands:
cmd = str.rstrip(cmd)
print(cmd)
cmd = str.split(cmd)
if cmd[0] == 'PING':
sendcmd(irc, 'PONG {}'.format(cmd[1]))
print('PONG!')
elif cmd[1] == 'PRIVMSG' and cmd[2] == room:
# Ex: :username!idthing PRIVMSG #roomName :My message
# DEV note: Should I use REGEX instead?
sender = cmd[0].split('!')[0][1:]
msg = ' '.join(cmd[3:])[1:]
print('{sender}: {msg}'.format(sender=sender, msg=msg))
r = requests.post(
rc.HOOK_ADDR,
json={
"icon_url": rc.AVATAR_URL.format(sender=sender),
"text": rc.msgtemplate.format(
sender=sender,
msg=msg
),
}
)
return readbuffer
def handle_rc_hook(rc_hook, rc_hook_addr=None, msgtemplate=None, bot_name=None, admin_username=None):
if rc_hook_addr is None:
rc_hook_addr = conf.RC.HOOK_ADDR
if msgtemplate is None:
msgtemplate = conf.IRC.msgtemplate
if bot_name is None:
bot_name = conf.IRC.BOT_NAME
if admin_username is None:
admin_username = conf.APP.admin_username
c, _ = rc_hook.accept()
headers, body = http_recv_all(c)
try:
data = json.loads(body)
print(data)
except ValueError:
print('INVALID JSON ({}): {} {}'.format(len(body), headers, body))
sendmsg(irc, '@{}: Invalid JSON! Go check the logs!'.format(
admin_username
))
requests.post(
rc_hook_addr,
json={
"text": '@{}: Invalid JSON! Go check the logs!'.format(
admin_username
),
}
)
return
# Not our bot nor any others'
# PREVENT infinite backfeed loop
if data['user_name'] != bot_name and not data['bot']:
msg = msgtemplate.format(
sender=data['user_name'],
msg=data['text']
)
print(msg)
sendmsg(irc, msg)
if __name__ == '__main__':
# If you don't load a config then most functions requires to be given each
# individual parameter they may need in order to function.
#
# Everytime you provide a parameter already given by the config, it will be
# selected over the config param. Meaning you can overwrite default
# behaviour on each function call
#
conf = load_config('config.INI')
_print = print
if conf.APP.LOGGING_FILE:
print('Logging is enabled')
with contextlib.suppress(FileNotFoundError):
# Back it up if it exists
os.replace(conf.APP.LOGGING_FILE, conf.APP.LOGGING_FILE + '.BCK')
# Reset on start
os.remove(conf.APP.LOGGING_FILE)
# Dev NOTE: Should probably wrap around while 42 instead of
# around each print call
def print(*args, **kwargs):
with open(conf.APP.LOGGING_FILE, 'a') as f:
with contextlib.redirect_stdout(f):
_print(*args, **kwargs)
else:
def print(*args, **kwargs):
"""Allows for printing in the Windows terminal without crashing."""
try:
_print(*args, **kwargs)
except UnicodeEncodeError:
_print('Can\'t print that!')
with contextlib.suppress(KeyboardInterrupt):
rc_hook = init_rc_hook()
irc = init_irc_conn()
readbuffer = ''
while 42:
rdy2read_sockets, __, __ = select.select([irc, rc_hook], (), ())
for read_s in rdy2read_sockets:
if read_s is irc:
# Incoming IRC commands
readbuffer = handle_irc(irc, readbuffer)
else:
# Incoming http POST request
handle_rc_hook(rc_hook)
with contextlib.suppress(NameError):
irc.close()
with contextlib.suppress(NameError):
rc_hook.close()