Skip to content

Commit 7045039

Browse files
committed
Refactor, remove queue usage, and add debug mode
1 parent 11ef6b1 commit 7045039

File tree

3 files changed

+97
-89
lines changed

3 files changed

+97
-89
lines changed

rabbit/main.py

+62-82
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,27 @@
11
import sys
2-
if sys.version_info < (3,0,0):
3-
print("Please run me in Python 3.")
4-
sys.exit(0)
5-
6-
from .stackoverflowchatsession import StackOverflowChatSession
7-
import config
8-
9-
import asyncio
2+
import os
103
import json
114
import html
5+
import logging
126
import random
13-
from queue import Queue
7+
import threading
8+
import re
149
from .dbmodel import User, get_session
10+
from .sochat import StackOverflowChatSession, EventType
11+
12+
import config
1513

1614
PYTHON_ROOM_ID = 6
1715
PERSONAL_SANDBOX_ROOM_ID = 118024
1816
ROTATING_KNIVES_ROOM_ID = 71097
17+
AUTHORIZED_USERS = {
18+
953482, #Kevin
19+
6621329 #Terry
20+
}
1921

20-
#room that all of the bot's actions will occur in.
21-
#todo: make it possible for the bot to operate simultaneously in multiple rooms. The chat API supports this natively, I just didn't account for it in my design.
2222
PRIMARY_ROOM_ID = PYTHON_ROOM_ID
2323

24-
event_type_names = [
25-
"placeholder because ids are 1-indexed",
26-
"message posted",
27-
"message edited",
28-
"user entered",
29-
"user left",
30-
"room name changed",
31-
"message starred",
32-
"UNKNOWN",
33-
"user mentioned",
34-
"message flagged",
35-
"message deleted",
36-
"file added",
37-
"moderator flag",
38-
"user settings chagned",
39-
"global notification",
40-
"account level changed",
41-
"user notification",
42-
"invitation",
43-
"message reply",
44-
"message moved out",
45-
"message moved in",
46-
"time break",
47-
"feed ticker",
48-
"user suspended",
49-
"user merged",
50-
]
24+
logger = logging.getLogger('rabbit')
5125

5226
def abbreviate(msg, maxlen=25):
5327
if len(msg) < maxlen: return msg
@@ -67,13 +41,11 @@ class Rabbit(StackOverflowChatSession):
6741
- "kick [user id]" - kicks the user, if bot has RO rights
6842
- "move [message id,message id,message id]" - moves one or more messages to the Rotating Knives room, if bot has RO rights
6943
"""
70-
def __init__(self, email, password, admin_message_queue):
44+
def __init__(self, email, password, room, trash_room, authorized_users):
7145
StackOverflowChatSession.__init__(self, email, password)
72-
self.admin_message_queue = admin_message_queue
73-
self.authorized_users = {
74-
953482, #Kevin
75-
6621329 #Terry
76-
}
46+
self.room = room
47+
self.trash_room = trash_room
48+
self.authorized_users = authorized_users
7749

7850
def onConnect(self, response):
7951
print('Connected:', response.peer)
@@ -83,20 +55,18 @@ def onOpen(self):
8355

8456
def onMessage(self, payload):
8557
d = json.loads(payload.decode("utf-8"))
86-
for ROOM_ID, data in d.items():
58+
logger.debug("Payload: {}".format(d))
59+
for room_id, data in d.items():
8760
if "e" not in data: #some kind of keepalive message that we don't care about
8861
continue
8962
for event in data["e"]:
90-
event_type = event["event_type"]
91-
if event_type >= len(event_type_names):
92-
raise Exception("Unrecognized event type: {} \nWith event data:".format(event_type, event))
63+
try:
64+
event_type = event["event_type"]
65+
event_type = EventType(event_type)
66+
except ValueError:
67+
raise Exception("Unrecognized event type: {} \nWith event data: {}".format(event_type, event))
9368
if event_type == 1: #ordinary user message
94-
content = html.unescape(event["content"])
95-
print(abbreviate("{}: {}".format(event["user_name"], content), 119))
96-
if event["user_id"] in self.authorized_users: #possible administrator command
97-
if content == "!ping":
98-
print("Detected a command. Replying...")
99-
self.send_message(PRIMARY_ROOM_ID, "pong")
69+
self._on_regular_message(event)
10070
elif event_type in (3,4): #user entered/left
10171
action = {3:"entered", 4:"left"}[event_type]
10272
print("user {} {} room {}".format(repr(event["user_name"]), action, repr(event["room_name"])))
@@ -109,62 +79,57 @@ def onMessage(self, payload):
10979

11080
#now post a picture of a bunny.
11181
bunny_url = random.choice(config.kick_reply_images)
112-
self.send_message(PRIMARY_ROOM_ID, bunny_url)
82+
self.send_message(self.room, bunny_url)
11383
else:
11484
print("Info: Unknown event content {} in account level changed event.".format(repr(event["content"])))
11585
print(event)
11686
else:
117-
print(event_type_names[event_type])
87+
logger.info("Event: {}".format(event_type))
11888

119-
def onClose(self, was_clean, code, reason):
120-
print('Closed:', reason)
121-
import sys; sys.exit(0)
89+
def _on_regular_message(self, event):
90+
# TODO: never ever react to self messages
91+
content = html.unescape(event["content"])
92+
print(abbreviate("{}: {}".format(event["user_name"], content), 119))
93+
if event["user_id"] in self.authorized_users: #possible administrator command
94+
if content == "!ping":
95+
print("Detected a command. Replying...")
96+
self.send_message(self.room, "pong")
12297

123-
def onIdle(self):
124-
while not self.admin_message_queue.empty():
125-
msg = self.admin_message_queue.get()
126-
self.onAdminMessage(msg)
98+
def onClose(self, was_clean, code, reason):
99+
print('Closed:', reason)
100+
sys.exit(0)
127101

128102
def onAdminMessage(self, msg):
129103
print("Got admin message: {}".format(msg))
130104
if msg == "shutdown":
131105
print("Shutting down...")
132-
import sys; sys.exit(0)
106+
sys.exit(0)
133107
elif msg.startswith("say"):
134-
self.send_message(PRIMARY_ROOM_ID, msg.partition(" ")[2])
108+
self.send_message(self.room, msg.partition(" ")[2])
135109
elif msg.startswith("cancel"):
136110
messageId = msg.partition(" ")[2]
137111
self.cancel_stars(messageId)
138112
elif msg.startswith("kick"):
139113
userId = msg.partition(" ")[2]
140-
self.kick(PRIMARY_ROOM_ID, userId)
114+
self.kick(self.room, userId)
141115
elif msg.startswith("move"):
142116
messageIds = msg.partition(" ")[2].split()
143-
self.move_messages(PRIMARY_ROOM_ID, messageIds, ROTATING_KNIVES_ROOM_ID)
117+
self.move_messages(self.room, messageIds, self.trash_room)
144118
else:
145119
print("Sorry, didn't understand that command.")
146120

147121

148-
#connect to the SO chat server. This function never returns.
149-
def create_and_run_chat_session(admin_message_queue = None):
150-
if admin_message_queue is None:
151-
admin_message_queue = Queue()
152-
153-
session = Rabbit(config.email, config.password, admin_message_queue)
154-
session.join_and_run_forever(PRIMARY_ROOM_ID)
155-
156-
import threading
157122

158123
#create a GUI the user can use to send admin commands. This function never returns.
159124
#(hint: use threads if you want to run both this and `create_and_run_chat_session`)
160-
def create_admin_window(message_queue):
125+
def create_admin_window(bot):
161126
from tkinter import Tk, Entry, Button
162127

163128
def clicked():
164-
message_queue.put(box.get())
129+
bot.loop.call_soon_threadsafe(bot.onAdminMessage, box.get())
165130

166131
def on_closing():
167-
message_queue.put("shutdown")
132+
bot.loop.call_soon_threadsafe(bot.onAdminMessage, "shutdown")
168133
root.destroy()
169134

170135
root = Tk()
@@ -179,8 +144,23 @@ def on_closing():
179144

180145

181146
def main():
182-
message_queue = Queue()
183-
t = threading.Thread(target=create_admin_window, args=(message_queue,))
147+
session = Rabbit(config.email, config.password, PRIMARY_ROOM_ID, ROTATING_KNIVES_ROOM_ID, AUTHORIZED_USERS)
148+
t = threading.Thread(target=create_admin_window, args=(session,))
184149
t.start()
185150

186-
create_and_run_chat_session(message_queue)
151+
session.join_and_run_forever(PRIMARY_ROOM_ID)
152+
153+
154+
def debug():
155+
handler = logging.StreamHandler()
156+
handler.setLevel(logging.DEBUG)
157+
logger.addHandler(handler)
158+
logger.setLevel(logging.DEBUG)
159+
160+
session = Rabbit(config.email, config.password,
161+
os.environ['room'], os.environ['trash'], set(int(x) for x in os.environ['users'].split(':')))
162+
163+
if 'tk' in os.environ:
164+
t = threading.Thread(target=create_admin_window, args=(session,))
165+
t.start()
166+
session.join_and_run_forever(os.environ['room'])

rabbit/stackoverflowchatsession.py renamed to rabbit/sochat.py

+34-7
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,52 @@
11
import requests
22
import json
33
import asyncio
4+
import logging
5+
import enum
46
from urllib.parse import quote_plus
57
from bs4 import BeautifulSoup as BS
68
from autobahn.asyncio.websocket import WebSocketClientProtocol, WebSocketClientFactory
79

810

11+
logger = logging.getLogger('rabbit')
12+
13+
EventType = enum.IntEnum('EventType', [
14+
"message_posted",
15+
"message_edited",
16+
"user_entered",
17+
"user_left",
18+
"room_name_changed",
19+
"message_starred",
20+
"UNKNOWN",
21+
"user_mentioned",
22+
"message_flagged",
23+
"message_deleted",
24+
"file_added",
25+
"moderator_flag",
26+
"user_settings_chagned",
27+
"global_notification",
28+
"account_level_changed",
29+
"user_notification",
30+
"invitation",
31+
"message_reply",
32+
"message_moved_out",
33+
"message_moved_in",
34+
"time_break",
35+
"feed_ticker",
36+
"user_suspended",
37+
"user_merged",
38+
])
39+
940
class StackOverflowChatSession:
1041
def __init__(self, email, password):
1142
url = "https://stackoverflow.com/users/login"
1243
login_data = {"email": email, "password": password}
1344
session = requests.Session()
45+
logger.debug("Logging in")
1446
session.post(url,login_data)
1547
#TODO: perform some cursory checking to confirm that logging in actually worked
1648

49+
logger.debug("Getting cookie")
1750
x = session.get("http://chat.stackoverflow.com")
1851

1952
soup = BS(x.content, "html.parser")
@@ -71,8 +104,6 @@ def onMessage(self, payload):
71104
pass
72105
def onClose(self, was_clean, code, reason):
73106
pass
74-
def onIdle(self):
75-
pass
76107

77108
def join_and_run_forever(self, roomid):
78109
session = self
@@ -88,16 +119,11 @@ def onClose(self, was_clean, code, reason): session.onClose(was_clean, code, rea
88119
factory = WebSocketClientFactory(url, headers={"Origin":"http://chat.stackoverflow.com"})
89120
factory.protocol = SoClient
90121
self.loop = asyncio.get_event_loop()
91-
self.loop.call_later(1, self._onIdle)
92122
coro = self.loop.create_connection(factory, host, 80)
93123
self.loop.run_until_complete(coro)
94124
self.loop.run_forever()
95125
self.loop.close()
96126

97-
def _onIdle(self):
98-
self.onIdle()
99-
self.loop.call_later(1, self._onIdle)
100-
101127
def _get_webservice_url(self, roomid):
102128
x = self._post("http://chat.stackoverflow.com/ws-auth", {"roomid":roomid})
103129

@@ -111,6 +137,7 @@ def _post(self, url, params=None):
111137
Send a POST message using some predetermined headers and params that are always necessary when talking to SO.
112138
using this instead of requests.post will save you the effort of adding the fkey, cookie, etc yourself every time.
113139
"""
140+
logger.debug("Posting to {}".format(url))
114141
if params is None:
115142
params = {}
116143
params["fkey"] = self.fkey

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
console_scripts=[
2020
'rabbit_userscript=rabbit.userscript_server:main',
2121
'rabbit=rabbit.main:main',
22+
'bugs=rabbit.main:debug'
2223
]
2324
)
2425
)

0 commit comments

Comments
 (0)