-
Notifications
You must be signed in to change notification settings - Fork 7
/
JustIRC.py
303 lines (228 loc) · 8.3 KB
/
JustIRC.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
import socket
from collections import defaultdict
from collections import namedtuple
_IRCPacket = namedtuple("IRCPacket", "prefix command arguments")
def _parse_irc_packet(packet):
prefix = ""
command = ""
arguments = []
if packet.startswith(":"):
prefix = packet[1:].split(" ")[0]
packet = packet.split(" ", 1)[1]
if " " in packet:
if " :" in packet:
last_argument = packet.split(" :")[1]
packet = packet.split(" :")[0]
for splitted in packet.split(" "):
if not command:
command = splitted
else:
arguments.append(splitted)
arguments.append(last_argument)
else:
for splitted in packet.split(" "):
if not command:
command = splitted
else:
arguments.append(splitted)
else:
command = packet
return _IRCPacket(prefix, command, arguments)
_IRCPacket.parse = _parse_irc_packet
class EventEmitter:
def __init__(self):
self.handlers = defaultdict(lambda: [])
def add_listener(self, name, handler):
self.handlers[name].append(handler)
def remove_listener(self, name, handler):
self.handlers[name].remove(handler)
def emit(self, name, data=None):
"""Emit an event
This function emits an event to all listeners registered to it.
Parameters
----------
name : str
Event name. Case sensitive.
data
Event data. Can be any type and passed directly to the event
handlers.
"""
for handler in list(self.handlers[name]):
handler(data)
def on(self, name):
"""
Decorate a function as an event handler.
Parameters
----------
name : str
The event name to handle
"""
def inner(func):
self.add_listener(name, func)
return func
return inner
# Event data types
_IRCEvent = namedtuple("IRCEvent", "bot")
_PacketEvent = namedtuple("PacketEvent", "bot packet")
_MessageEvent = namedtuple("MessageEvent", "bot channel sender message")
_JoinEvent = namedtuple("JoinEvent", "bot channel nick")
_PartEvent = namedtuple("PartEvent", "bot channel nick")
class IRCConnection(EventEmitter):
def __init__(self):
"""Create an IRC connection
After creating the object and adding all the event handlers, you need to
call .connect on it to actually connect to a server.
"""
super().__init__()
self.socket = None
self.nick = ""
def run_once(self):
"""Run one iteration of the IRC client.
This function is called in a loop by the run_loop function. It can be
called separately, but most of the time there is no need to do this.
"""
line = next(self.lines)
packet = _IRCPacket.parse(line)
sender = packet.prefix.split("!")[0]
ev = _PacketEvent(self, packet)
self.emit("packet", ev)
self.emit(f"packet_{packet.command}", ev)
if packet.command == "PRIVMSG":
channel = packet.arguments[0]
message = packet.arguments[1]
ev = _MessageEvent(self, channel, sender, message)
self.emit("message", ev)
self.emit(f"message_{channel}", ev)
self.emit(f"message_{sender}", ev)
if channel[0] == "#":
self.emit("message#", ev)
else:
self.emit("pm", ev)
elif packet.command == "PING":
# Handle a PING message
self.send_line("PONG :{}".format(packet.arguments[0]))
self.emit("ping", _IRCEvent(self))
elif packet.command == "433" or packet.command == "437":
# Command 433 is "Nick in use"
# Add underscore to the nick
self.set_nick("{}_".format(self.nick))
elif packet.command == "001":
self.emit("welcome", _IRCEvent(self))
elif packet.command == "JOIN":
ev = _JoinEvent(self, packet.arguments[0], sender)
self.emit("join", ev)
elif packet.command == "PART":
ev = _PartEvent(self, packet.arguments[0], sender)
self.emit("part", ev)
def run_loop(self):
"""Runs the main loop of the client
This function is usually called after you add all the callbacks and
connect to the server. It will block until the connection to the server
is broken.
"""
while True:
self.run_once()
def _read_lines(self):
buff = ""
while True:
buff += self.socket.recv(1024).decode("utf-8", "replace")
while "\n" in buff:
line, buff = buff.split("\n", 1)
line = line.replace("\r", "")
yield line
def connect(self, server, port=6667, tls=False):
"""Connects to the IRC server
Parameters
----------
server : str
The server IP or domain to connect to
port : int
The server port to connect to
tls : bool
Enable the use of TLS
"""
self.socket = socket.create_connection((server, port))
if tls:
import ssl
context = ssl.SSLContext()
self.socket = context.wrap_socket(self.socket, server)
self.lines = self._read_lines()
self.emit("connect", _IRCEvent(self))
def send_line(self, line):
"""Sends a line directly to the server.
This is a low-level function that can be used to implement functionality
that's not covered by this library. Almost all of the time, you should
have no need to use this function.
Parameters
----------
line : str
The line to send to the server
"""
self.socket.send(f"{line}\r\n".encode("utf-8"))
def send_message(self, to, message):
"""Sends a message to a user or a channel
This is the main method of interaction as an IRC bot or client. This
function results in a PRIVMSG packet to the server.
Parameters
----------
to : str
The target of the message
message : str
The message content
"""
self.send_line(f"PRIVMSG {to} :{message}")
def send_notice(self, to, message):
"""Send a notice message
Notice messages usually have special formatting on clients.
Parameters
----------
to : str
The target of the message
message : str
The message content
"""
self.send_line(f"NOTICE {to} :{message}")
def send_action_message(self, to, action):
"""Send an action message to a channel or user.
Action messages can have special formatting on clients and are usually
send like /me is happy
Parameters
----------
to : str
The target of the message. Can be a channel or a user.
action : str
The message content
"""
self.send_message(to, f"\x01ACTION {action}\x01")
def join_channel(self, channel):
"""Join a channel
This function joins a given channel. After the channel is joined, the
"join" event is emitted with your nick.
Parameters
----------
channel : str
The channel to join
"""
self.send_line(f"JOIN {channel}")
def set_nick(self, nick):
"""Sets or changes your nick
This should be called before joining channels, but can be called at any
time afterwards. If the requested nick is not available, the library
will keep adding underscores until an available nick is found.
Parameters
----------
nick : str
The nickname to use
"""
self.nick = nick
self.send_line(f"NICK {nick}")
def send_user_packet(self, username):
"""Send a user packet
This should be sent after your nickname. It is displayed on the clients
when they view your details and look at "Real Name".
Parameters
----------
username : str
The name to set
"""
self.send_line(f"USER {username} 0 * :{username}")