Initial commit
This commit is contained in:
commit
5cfec13d6f
7 changed files with 635 additions and 0 deletions
3
__init__.py
Normal file
3
__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .connection import Connection
|
||||
from .session import Session
|
||||
from .message import Message
|
||||
235
connection.py
Normal file
235
connection.py
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
import json
|
||||
import time
|
||||
import threading
|
||||
import websocket
|
||||
from websocket import WebSocketException as WSException
|
||||
|
||||
|
||||
ROOM_FORMAT = "wss://euphoria.io/room/{}/ws"
|
||||
|
||||
|
||||
class Connection():
|
||||
"""
|
||||
Stays connected to a room in its own thread.
|
||||
Callback functions are called when a packet is received.
|
||||
"""
|
||||
|
||||
def __init__(self, room):
|
||||
"""
|
||||
room - name of the room to connect to
|
||||
"""
|
||||
|
||||
self.room = room
|
||||
|
||||
self.stopping = False
|
||||
|
||||
self.ws = None
|
||||
self.send_id = 0
|
||||
self.callbacks = {}
|
||||
self.id_callbacks = {}
|
||||
|
||||
def connect(self, tries=-1, delay=10):
|
||||
"""
|
||||
_connect(tries, delay) -> bool
|
||||
|
||||
tries - maximum number of retries
|
||||
-1 -> retry indefinitely
|
||||
|
||||
Returns True on success, False on failure.
|
||||
|
||||
Connect to the room.
|
||||
"""
|
||||
|
||||
while tries != 0:
|
||||
try:
|
||||
self.ws = websocket.create_connection(
|
||||
ROOM_FORMAT.format(self.room),
|
||||
enable_multithread=True
|
||||
)
|
||||
return True
|
||||
except WSException:
|
||||
if tries > 0:
|
||||
tries -= 1
|
||||
if tries != 0:
|
||||
time.sleep(delay)
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
disconnect() -> None
|
||||
|
||||
Reconnect to the room.
|
||||
WARNING: To completely disconnect, use stop().
|
||||
"""
|
||||
|
||||
if self.ws:
|
||||
self.ws.close()
|
||||
self.ws = None
|
||||
|
||||
def launch(self):
|
||||
"""
|
||||
launch() -> Thread
|
||||
|
||||
Connect to the room and spawn a new thread running run.
|
||||
"""
|
||||
|
||||
if self.connect(tries=1):
|
||||
self.thread = threading.Thread(target=self.run, name=self.room)
|
||||
self.thread.start()
|
||||
return self.thread
|
||||
else:
|
||||
self.stop()
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
run() -> None
|
||||
|
||||
Receive messages.
|
||||
"""
|
||||
|
||||
while not self.stopping:
|
||||
try:
|
||||
self.handle_json(self.ws.recv())
|
||||
except (WSException, OSError, ValueError):
|
||||
if not self.stopping:
|
||||
self.disconnect()
|
||||
self.connect()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
stop() -> None
|
||||
|
||||
Close the connection to the room.
|
||||
"""
|
||||
|
||||
self.stopping = True
|
||||
self.disconnect()
|
||||
|
||||
def join(self):
|
||||
"""
|
||||
join() -> None
|
||||
|
||||
Join the thread spawned by launch.
|
||||
"""
|
||||
|
||||
if self.thread:
|
||||
self.thread.join()
|
||||
|
||||
def add_callback(self, ptype, callback, *args, **kwargs):
|
||||
"""
|
||||
add_callback(ptype, callback) -> None
|
||||
|
||||
Add a function to be called when a packet of type ptype is received.
|
||||
"""
|
||||
|
||||
if not ptype in self.callbacks:
|
||||
self.callbacks[ptype] = []
|
||||
|
||||
callback_info = {
|
||||
"callback": callback,
|
||||
"args": args,
|
||||
"kwargs": kwargs
|
||||
}
|
||||
|
||||
self.callbacks[ptype].append(callback_info)
|
||||
|
||||
def add_id_callback(self, pid, callback, *args, **kwargs):
|
||||
"""
|
||||
add_id_callback(pid, callback) -> None
|
||||
|
||||
Add a function to be called when a packet with id pid is received.
|
||||
"""
|
||||
|
||||
if not pid in self.id_callbacks:
|
||||
self.id_callbacks[pid] = []
|
||||
|
||||
callback_info = {
|
||||
"callback": callback,
|
||||
"args": args,
|
||||
"kwargs": kwargs
|
||||
}
|
||||
|
||||
self.id_callbacks[pid].append(callback_info)
|
||||
|
||||
def call_callback(self, event, *args):
|
||||
"""
|
||||
call_callback(event) -> None
|
||||
|
||||
Call all callbacks subscribed to the event with *args.
|
||||
"""
|
||||
|
||||
if event in self.callbacks:
|
||||
for c_info in self.callbacks[event]:
|
||||
c = c_info["callback"]
|
||||
args = c_info["args"] + args
|
||||
kwargs = c_info["kwargs"]
|
||||
c(*args, **kwargs)
|
||||
|
||||
def call_id_callback(self, pid, *args):
|
||||
"""
|
||||
call_callback(pid) -> None
|
||||
|
||||
Call all callbacks subscribed to the pid with *args.
|
||||
"""
|
||||
|
||||
if pid in self.id_callbacks:
|
||||
for c_info in self.id_callbacks.pop(pid):
|
||||
c = c_info["callback"]
|
||||
args = c_info["args"] + args
|
||||
kwargs = c_info["kwargs"]
|
||||
c(*args, **kwargs)
|
||||
|
||||
def handle_json(self, data):
|
||||
"""
|
||||
handle_json(data) -> None
|
||||
|
||||
Handle incoming 'raw' data.
|
||||
"""
|
||||
|
||||
packet = json.loads(data)
|
||||
self.handle_packet(packet)
|
||||
|
||||
def handle_packet(self, packet):
|
||||
"""
|
||||
handle_packet(ptype, data) -> None
|
||||
|
||||
Handle incoming packets
|
||||
"""
|
||||
|
||||
if "data" in packet:
|
||||
data = packet["data"]
|
||||
else:
|
||||
data = None
|
||||
|
||||
self.call_callback(packet["type"], data)
|
||||
|
||||
if "id" in packet:
|
||||
self.call_id_callback(packet["id"], data)
|
||||
|
||||
def send_json(self, data):
|
||||
"""
|
||||
send_json(data) -> None
|
||||
|
||||
Send 'raw' json.
|
||||
"""
|
||||
|
||||
if self.ws:
|
||||
try:
|
||||
self.ws.send(json.dumps(data))
|
||||
except WSException:
|
||||
self.disconnect()
|
||||
|
||||
def send_packet(self, ptype, **kwargs):
|
||||
"""
|
||||
send_packet(ptype, **kwargs) -> None
|
||||
|
||||
Send a formatted packet.
|
||||
"""
|
||||
|
||||
packet = {
|
||||
"type": ptype,
|
||||
"data": kwargs or None,
|
||||
"id": str(self.send_id)
|
||||
}
|
||||
self.send_id += 1
|
||||
self.send_json(packet)
|
||||
87
message.py
Normal file
87
message.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import time
|
||||
|
||||
from . import session
|
||||
|
||||
class Message():
|
||||
"""
|
||||
This class keeps track of message details.
|
||||
"""
|
||||
|
||||
def __init__(self, message):
|
||||
"""
|
||||
message - A euphoria message: http://api.euphoria.io/#message
|
||||
"""
|
||||
|
||||
self.message = message
|
||||
self.session = session.Session(message["sender"])
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
id() -> str
|
||||
|
||||
The message's unique id.
|
||||
"""
|
||||
|
||||
return self.message["id"]
|
||||
|
||||
def parent(self):
|
||||
"""
|
||||
parent() -> str
|
||||
|
||||
The message's parent's unique id.
|
||||
"""
|
||||
|
||||
if "parent" in self.message:
|
||||
return self.message["parent"]
|
||||
|
||||
def content(self):
|
||||
"""
|
||||
content() -> str
|
||||
|
||||
The message's content.
|
||||
"""
|
||||
|
||||
return self.message["content"]
|
||||
|
||||
def sender(self):
|
||||
"""
|
||||
sender() -> Session
|
||||
|
||||
The sender of the message.
|
||||
"""
|
||||
|
||||
return self.session
|
||||
|
||||
def time(self):
|
||||
"""
|
||||
time() -> int
|
||||
|
||||
Unix epoch timestamp of when the message was posted.
|
||||
"""
|
||||
|
||||
return self.message["time"]
|
||||
|
||||
def time_formatted(self, date=False):
|
||||
"""
|
||||
time_formatted(date=False) -> str
|
||||
|
||||
date - include date in format
|
||||
|
||||
Time in a readable format:
|
||||
With date: YYYY-MM-DD HH:MM:SS
|
||||
Without date: HH:MM:SS
|
||||
"""
|
||||
|
||||
if date:
|
||||
return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(self.time()))
|
||||
else:
|
||||
return time.strftime("%H:%M:%S", time.gmtime(self.time()))
|
||||
|
||||
def deleted(self):
|
||||
"""
|
||||
deleted() -> bool
|
||||
|
||||
Is this message deleted?
|
||||
"""
|
||||
|
||||
return True if "deleted" in self.message and self.message["deleted"] else False
|
||||
79
messages.py
Normal file
79
messages.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
from . import message
|
||||
|
||||
class Messages():
|
||||
"""
|
||||
Message storage class which preserves thread hierarchy.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.by_id = {}
|
||||
self.by_parent = {}
|
||||
|
||||
def add_raw(self, raw_message):
|
||||
"""
|
||||
add_raw(raw_message) -> None
|
||||
|
||||
Create a message from raw data and add it.
|
||||
"""
|
||||
|
||||
mes = message.Message(raw_message)
|
||||
|
||||
self.add(mes)
|
||||
|
||||
def add(self, mes):
|
||||
"""
|
||||
add(message) -> None
|
||||
|
||||
Add a message to the structure.
|
||||
"""
|
||||
|
||||
self.remove(mes.id())
|
||||
|
||||
self.by_id[mes.id()] = mes
|
||||
|
||||
if mes.parent():
|
||||
if not mes.parent() in self.by_parent:
|
||||
self.by_parent[parent] = []
|
||||
self.by_parent[mes.parent()].append(mes)
|
||||
|
||||
def remove(self, mes):
|
||||
"""
|
||||
remove(message) -> None
|
||||
|
||||
Remove a message from the structure.
|
||||
"""
|
||||
|
||||
if mes.id() in self.by_id:
|
||||
self.by_id.pop(mes.id())
|
||||
|
||||
if mes.parent() and mes in self.get_children(mes.parent()):
|
||||
self.by_parent[mes.parent()].remove(mes)
|
||||
|
||||
def get(self, mid):
|
||||
"""
|
||||
get(message_id) -> Message
|
||||
|
||||
Returns the message with the given id, if found.
|
||||
"""
|
||||
|
||||
if mid in self.by_id:
|
||||
return self.by_id[mid]
|
||||
|
||||
def get_parent(self, mes):
|
||||
"""
|
||||
get_parent(message) -> str
|
||||
|
||||
Returns the message's parent.
|
||||
Returns None if no parent was found.
|
||||
"""
|
||||
|
||||
return self.get(mes.parent())
|
||||
|
||||
def get_children(self, mes):
|
||||
"""
|
||||
get_children(message) -> list
|
||||
|
||||
Returns a list of children of the given message.
|
||||
"""
|
||||
|
||||
return self.by_parent[mes.id()]
|
||||
9
room.py
Normal file
9
room.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import connection
|
||||
|
||||
class Room():
|
||||
"""
|
||||
TODO
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.messages = []
|
||||
118
session.py
Normal file
118
session.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
class Session():
|
||||
"""
|
||||
This class keeps track of session details.
|
||||
"""
|
||||
|
||||
def __init__(self, session):
|
||||
"""
|
||||
session - a euphoria SessionView: http://api.euphoria.io/#sessionview
|
||||
"""
|
||||
|
||||
self.session = session
|
||||
|
||||
def session_type(self):
|
||||
"""
|
||||
session_type() -> str
|
||||
|
||||
The session's type (bot, account, agent).
|
||||
"""
|
||||
|
||||
return self.user_id().split(":")[0]
|
||||
|
||||
def user_id(self):
|
||||
"""
|
||||
user_id() -> str
|
||||
|
||||
The user's id.
|
||||
"""
|
||||
|
||||
return self.session["id"]
|
||||
|
||||
def session_id(self):
|
||||
"""
|
||||
session_id() -> str
|
||||
|
||||
Returns the session's id.
|
||||
"""
|
||||
|
||||
return self.session["session_id"]
|
||||
|
||||
def name(self):
|
||||
"""
|
||||
name() -> str
|
||||
|
||||
The user's name.
|
||||
"""
|
||||
|
||||
return self.session["name"]
|
||||
|
||||
def mentionable(self):
|
||||
"""
|
||||
mentionable() -> str
|
||||
|
||||
Converts the name to a mentionable format.
|
||||
"""
|
||||
|
||||
return "".join(c for c in self.name() if not c in ",.!?;&<'\"" and not c.isspace())
|
||||
|
||||
def listable(self, width):
|
||||
"""
|
||||
listable(width): -> prefixes, name
|
||||
|
||||
Prefixes and name which together are <width> characters long or shorter.
|
||||
"""
|
||||
|
||||
prefixes = ""
|
||||
if self.session_type() == "account":
|
||||
prefixes += "*"
|
||||
if self.is_manager():
|
||||
prefixes += "m"
|
||||
if self.is_staff():
|
||||
prefixes += "s"
|
||||
|
||||
name = self.name()
|
||||
if len(prefixes + name) > width:
|
||||
name = name[:width - len(prefixes) - 1] + "…"
|
||||
|
||||
return prefixes, name
|
||||
|
||||
def server_id(self):
|
||||
"""
|
||||
server_id() -> server_id
|
||||
|
||||
The session's server id.
|
||||
"""
|
||||
|
||||
return self.session["server_id"]
|
||||
|
||||
def server_era(self):
|
||||
"""
|
||||
server_era() -> server_era
|
||||
|
||||
The session's server era.
|
||||
"""
|
||||
|
||||
return self.session["server_era"]
|
||||
|
||||
def is_staff(self):
|
||||
"""
|
||||
is_staff() -> staff
|
||||
|
||||
Is a user staff?
|
||||
"""
|
||||
if "is_staff" in self.session:
|
||||
return self.session["is_staff"]
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_manager(self):
|
||||
"""
|
||||
is_manager() -> manager
|
||||
|
||||
Is a user manager?
|
||||
"""
|
||||
if "is_manager" in self.session:
|
||||
return self.session["is_manager"]
|
||||
else:
|
||||
return False
|
||||
|
||||
104
sessions.py
Normal file
104
sessions.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
from . import session
|
||||
|
||||
class Sessions():
|
||||
"""
|
||||
Keeps track of sessions.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
TODO
|
||||
"""
|
||||
self.sessions = {}
|
||||
|
||||
def add_raw(self, raw_session):
|
||||
"""
|
||||
add_raw(raw_session) -> None
|
||||
|
||||
Create a session from raw data and add it.
|
||||
"""
|
||||
|
||||
ses = session.Session(raw_session)
|
||||
|
||||
self.sessions[ses.session_id()] = ses
|
||||
|
||||
def add(self, ses):
|
||||
"""
|
||||
add(session) -> None
|
||||
|
||||
Add a session.
|
||||
"""
|
||||
|
||||
self.sessions[ses.session_id()] = ses
|
||||
|
||||
def remove(self, ses):
|
||||
"""
|
||||
remove(session_id) -> None
|
||||
|
||||
Remove a session.
|
||||
"""
|
||||
|
||||
if ses.session_id() in self.sessions:
|
||||
self.sessions.pop(ses.session_id())
|
||||
|
||||
def remove_on_network_partition(self, server_id, server_era):
|
||||
"""
|
||||
remove_on_network_partition(server_id, server_era) -> None
|
||||
|
||||
Removes all sessions matching the server_id/server_era combo.
|
||||
http://api.euphoria.io/#network-event
|
||||
"""
|
||||
|
||||
for ses in self.sessions:
|
||||
if ses.server_id() == server_id and ses.server_era() == server_era:
|
||||
self.remove(ses)
|
||||
|
||||
def get_people(self):
|
||||
"""
|
||||
get_people() -> list
|
||||
|
||||
Returns a list of all non-bot and non-lurker sessions.
|
||||
"""
|
||||
|
||||
# not a list comprehension because that would span several lines too
|
||||
people = []
|
||||
for ses in self.sessions:
|
||||
if ses.session_type() in ["agent", "account"] and ses.name():
|
||||
people.append(ses)
|
||||
return people
|
||||
|
||||
def get_accounts(self):
|
||||
"""
|
||||
get_accounts() -> list
|
||||
|
||||
Returns a list of all logged-in sessions.
|
||||
"""
|
||||
|
||||
return [ses for ses in self.sessions if ses.session_type() == "account" and ses.name()]
|
||||
|
||||
def get_agents(self):
|
||||
"""
|
||||
get_agents() -> list
|
||||
|
||||
Returns a list of all sessions who are not signed into an account and not bots or lurkers.
|
||||
"""
|
||||
|
||||
return [ses for ses in self.sessions if ses.session_type() == "agent" and ses.name()]
|
||||
|
||||
def get_bots(self):
|
||||
"""
|
||||
get_bots() -> list
|
||||
|
||||
Returns a list of all bot sessions.
|
||||
"""
|
||||
|
||||
return [ses for ses in self.sessions if ses.session_type() == "bot" and ses.name()]
|
||||
|
||||
def get_lurkers(self):
|
||||
"""
|
||||
get_lurkers() -> list
|
||||
|
||||
Returns a list of all lurker sessions.
|
||||
"""
|
||||
|
||||
return [ses for ses in self.sessions if not ses.name()]
|
||||
Loading…
Add table
Add a link
Reference in a new issue