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
|
# This module is based on https://github.com/gkbrk/hackchat (MIT License)
import json
import threading
import time
import websocket
import sys
import traceback
import signal
class HackChat:
"""A library to connect to https://hack.chat.
<on_message> is <list> of callback functions to receive data from
https://hack.chat. Add your callback functions to this attribute.
e.g., on_message += [my_callback]
The callback function should have 3 parameters, the first for the
<HackChat> object, the second for the message someone sent and the
third for the nickname of the sender of the message.
"""
def __init__(self, nick, wsurl="wss://hack.chat/chat-ws", channel="programming"):
"""Connects to a channel on https://hack.chat.
Keyword arguments:
nick -- <str>; the nickname to use upon joining the channel
channel -- <str>; the channel to connect to on https://hack.chat
"""
self.nick = nick
self.channel = channel
self.wsurl = wsurl
self.online_users = []
self.on_message = []
self.on_join = []
self.on_leave = []
self.on_emote = []
self.on_invite = []
self.stopped = False
self._stop = threading.Event()
# Receiver thread
self._recv_thread = threading.Thread(target = self._receive)
self._recv_thread.daemon = True
# Keepalive thread
self._ka_thread = threading.Thread(target = self._ping)
self._ka_thread.daemon = True
def send_message(self, msg):
"""Sends a message on the channel."""
self._send_packet({"cmd": "chat", "text": msg})
def _send_packet(self, packet):
"""Sends <packet> (<dict>) to https://hack.chat."""
encoded = json.dumps(packet)
self.ws.send(encoded)
def run(self):
"""Starts the bot asynchronously."""
if self.stopped:
raise ValueError("Can't run a stopped bot.")
self.ws = websocket.create_connection(self.wsurl)
self._send_packet({"cmd": "join", "channel": self.channel, "nick": self.nick})
self._recv_thread.start()
self._ka_thread.start()
def _receive(self):
"""Waits for data and then sends it to the callback functions.
Will send a SIGALRM to its own process upon connection loss or crash."""
try:
while not self._stop.wait(timeout=0):
self.ws.settimeout(1)
try:
result_raw = self.ws.recv()
#print(result_raw)
result = json.loads(result_raw)
#print(result)
self._handleCommand(result)
except websocket._exceptions.WebSocketTimeoutException:
# Ignore timeouts
pass
except (json.decoder.JSONDecodeError,
websocket._exceptions.WebSocketConnectionClosedException) as e:
print("Connection lost!")
# Signal main thread that bot crashed
signal.alarm(1)
except:
print("Receiver thread crashed!")
traceback.print_exc()
# Signal main thread that bot crashed
signal.alarm(1)
print("Receiver thread shut down.")
def _handleCommand(self, result):
"""Will demultiplex incoming packets to their respective callback
functions."""
if result["cmd"] == "chat" and not result["nick"] == self.nick:
for handler in list(self.on_message):
handler(self, result)
elif result["cmd"] == "onlineAdd":
self.online_users.append(result["nick"])
for handler in list(self.on_join):
handler(self, result)
elif result["cmd"] == "onlineRemove":
self.online_users.remove(result["nick"])
for handler in list(self.on_leave):
handler(self, result)
elif result["cmd"] == "onlineSet":
for nick in result["nicks"]:
self.online_users.append(nick)
elif result["cmd"] == "info":
if result["type"] == "emote":
for handler in list(self.on_emote):
handler(self, result)
elif result["type"] == "invite":
for handler in list(self.on_invite):
handler(self, result)
def stop(self):
"""Gracefully stops all bot threads and closes WebSocket connection."""
if self.stopped:
return
self._stop.set()
self._recv_thread.join()
self._ka_thread.join()
self.ws.close()
self.stopped = True
def _ping(self):
"""Retains the websocket connection."""
while self.ws.connected \
and not self._stop.wait(timeout=60):
self._send_packet({"cmd": "ping"})
#print("PING")
print("Keepalive thread shut down.")
|