Compare commits
17 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23329238c6 | ||
|
|
966034bdde | ||
|
|
2529c2d238 | ||
|
|
c4fdb2942e | ||
|
|
75b2108b47 | ||
|
|
f56af13ede | ||
|
|
eb2b459216 | ||
|
|
e9354194cf | ||
|
|
14bae17104 | ||
|
|
04f7c9c781 | ||
|
|
f366a02758 | ||
|
|
1b9d12d253 | ||
|
|
f1314c7ec1 | ||
|
|
aee8e5c118 | ||
|
|
3b3ce99625 | ||
|
|
4e37154737 | ||
|
|
dd4b5144a9 |
11 changed files with 682 additions and 609 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
yaboli/__pycache__/
|
yaboli/__pycache__/
|
||||||
|
*.txt
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
from .bot import Bot
|
import logging
|
||||||
from .botmanager import BotManager
|
logging.basicConfig(
|
||||||
|
level=logging.DEBUG,
|
||||||
|
format="[{levelname: <7}] in {threadName: <17} <{name}>: {message}",
|
||||||
|
style="{"
|
||||||
|
)
|
||||||
|
|
||||||
|
from .basic_types import Message, SessionView
|
||||||
from .callbacks import Callbacks
|
from .callbacks import Callbacks
|
||||||
from .connection import Connection
|
from .connection import Connection
|
||||||
from .exceptions import *
|
|
||||||
from .session import Session
|
from .session import Session
|
||||||
from .message import Message
|
|
||||||
from .sessions import Sessions
|
|
||||||
from .messages import Messages
|
|
||||||
from .room import Room
|
|
||||||
|
|
|
||||||
163
yaboli/basic_types.py
Normal file
163
yaboli/basic_types.py
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
import time
|
||||||
|
|
||||||
|
class SessionView():
|
||||||
|
"""
|
||||||
|
This class keeps track of session details.
|
||||||
|
http://api.euphoria.io/#sessionview
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, id, name, server_id, server_era, session_id, is_staff=None, is_manager=None):
|
||||||
|
"""
|
||||||
|
id - agent/account id
|
||||||
|
name - name of the client when the SessionView was captured
|
||||||
|
server_id - id of the server
|
||||||
|
server_era - era of the server
|
||||||
|
session_id - session id (unique across euphoria)
|
||||||
|
is_staff - client is staff
|
||||||
|
is_manager - client is manager
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.server_id = server_id
|
||||||
|
self.server_era = server_era
|
||||||
|
self.session_id = session_id
|
||||||
|
self.staff = is_staff
|
||||||
|
self.manager = is_manager
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls, data):
|
||||||
|
"""
|
||||||
|
Creates and returns a session created from the data.
|
||||||
|
|
||||||
|
data - a euphoria SessionView
|
||||||
|
"""
|
||||||
|
|
||||||
|
view = cls(None, None, None, None, None)
|
||||||
|
view.read_data(data)
|
||||||
|
return view
|
||||||
|
|
||||||
|
def read_data(self, data):
|
||||||
|
if "id" in data: self.id = data.get("id")
|
||||||
|
if "name" in data: self.name = data.get("name")
|
||||||
|
if "server_id" in data: self.server_id = data.get("server_id")
|
||||||
|
if "server_era" in data: self.server_era = data.get("server_era")
|
||||||
|
if "session_id" in data: self.session_id = data.get("session_id")
|
||||||
|
if "is_staff" in data: self.is_staff = data.get("is_staff")
|
||||||
|
if "is_manager" in data: self.is_manager = data.get("is_manager")
|
||||||
|
|
||||||
|
def session_type(self):
|
||||||
|
"""
|
||||||
|
session_type() -> str
|
||||||
|
|
||||||
|
The session's type (bot, account, agent).
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.id.split(":")[0] if ":" in self.id else None
|
||||||
|
|
||||||
|
class Message():
|
||||||
|
"""
|
||||||
|
This class represents a single euphoria message.
|
||||||
|
http://api.euphoria.io/#message
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, id, time, sender, content, parent=None, edited=None, previous_edit_id=None,
|
||||||
|
deleted=None, truncated=None, encryption_key_id=None):
|
||||||
|
"""
|
||||||
|
id - message id
|
||||||
|
time - time the message was sent (epoch)
|
||||||
|
sender - SessionView of the sender
|
||||||
|
content - content of the message
|
||||||
|
parent - id of the parent message, or None
|
||||||
|
edited - time of last edit (epoch)
|
||||||
|
previous_edit_id - edit id of the most recent edit of this message
|
||||||
|
deleted - time of deletion (epoch)
|
||||||
|
truncated - message was truncated
|
||||||
|
encryption_key_id - id of the key that encrypts the message in storage
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.id = id
|
||||||
|
self.time = time
|
||||||
|
self.sender = sender
|
||||||
|
self.content = content
|
||||||
|
self.parent = parent
|
||||||
|
self.edited = edited
|
||||||
|
self.previous_edit_id = previous_edit_id
|
||||||
|
self.deleted = deleted
|
||||||
|
self.truncated = truncated
|
||||||
|
self.encryption_key_id = encryption_key_id
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(self, data):
|
||||||
|
"""
|
||||||
|
Creates and returns a message created from the data.
|
||||||
|
NOTE: This also creates a session object using the data in "sender".
|
||||||
|
|
||||||
|
data - a euphoria message: http://api.euphoria.io/#message
|
||||||
|
"""
|
||||||
|
|
||||||
|
sender = SessionView.from_data(data.get("sender"))
|
||||||
|
|
||||||
|
return self(
|
||||||
|
data.get("id"),
|
||||||
|
data.get("time"),
|
||||||
|
sender,
|
||||||
|
data.get("content"),
|
||||||
|
parent=data.get("parent"),
|
||||||
|
edited=data.get("edited"),
|
||||||
|
deleted=data.get("deleted"),
|
||||||
|
truncated=data.get("truncated"),
|
||||||
|
previous_edit_id=data.get("previous_edit_id"),
|
||||||
|
encryption_key_id=data.get("encryption_key_id")
|
||||||
|
)
|
||||||
|
|
||||||
|
def time_formatted(self, date=True):
|
||||||
|
"""
|
||||||
|
time_formatted(date=True) -> 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 formatted(self, show_time=False, date=True, insert_string=None, repeat_insert_string=True):
|
||||||
|
"""
|
||||||
|
formatted() -> strftime
|
||||||
|
|
||||||
|
The message contents in the following format (does not end on a newline):
|
||||||
|
<time><insert_string>[<sender name>] message content
|
||||||
|
<insert_string> more message on a new line
|
||||||
|
|
||||||
|
If repeat_insert_string is False, the insert_string will only appear
|
||||||
|
on the first line.
|
||||||
|
|
||||||
|
If show_time is False, the time will not appear in the first line of
|
||||||
|
the formatted message.
|
||||||
|
The date option works like it does in Message.time_formatted().
|
||||||
|
"""
|
||||||
|
|
||||||
|
msgtime = self.time_formatted(date) if show_time else ""
|
||||||
|
if insert_string is None:
|
||||||
|
insert_string = " " if show_time else ""
|
||||||
|
lines = self.content.split("\n")
|
||||||
|
|
||||||
|
# first line
|
||||||
|
msg = "{}{}[{}] {}\n".format(msgtime, insert_string, self.sender.name, lines[0])
|
||||||
|
|
||||||
|
# all other lines
|
||||||
|
for line in lines[1:]:
|
||||||
|
msg += "{}{} {} {}\n".format(
|
||||||
|
" "*len(msgtime),
|
||||||
|
insert_string if repeat_insert_string else " "*len(insert_string),
|
||||||
|
" "*len(self.sender.name),
|
||||||
|
line
|
||||||
|
)
|
||||||
|
|
||||||
|
return msg[:-1] # remove trailing newline
|
||||||
|
|
@ -1,10 +1,20 @@
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import websocket
|
import websocket
|
||||||
from websocket import WebSocketException as WSException
|
from websocket import WebSocketException as WSException
|
||||||
|
|
||||||
from . import callbacks
|
from .callbacks import Callbacks
|
||||||
|
|
||||||
|
SSLOPT = {"ca_certs": ssl.get_default_verify_paths().cafile}
|
||||||
|
#SSLOPT = {"cert_reqs": ssl.CERT_NONE}
|
||||||
|
ROOM_FORMAT = "wss://euphoria.io/room/{}/ws"
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
#logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
class Connection():
|
class Connection():
|
||||||
"""
|
"""
|
||||||
|
|
@ -13,41 +23,50 @@ class Connection():
|
||||||
|
|
||||||
Callbacks:
|
Callbacks:
|
||||||
- all the message types from api.euphoria.io
|
- all the message types from api.euphoria.io
|
||||||
These pass the packet data as argument to the called functions.
|
These pass the packet data and errors (if any) as arguments to the called functions.
|
||||||
The other callbacks don't pass any special arguments.
|
The other callbacks don't pass any special arguments.
|
||||||
- "connect"
|
- "connect"
|
||||||
- "disconnect"
|
- "disconnect"
|
||||||
- "stop"
|
- "stop"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ROOM_FORMAT = "wss://euphoria.io/room/{}/ws"
|
def __init__(self, room, url_format=ROOM_FORMAT, tries=10, delay=30):
|
||||||
|
|
||||||
def __init__(self, room, url_format=None):
|
|
||||||
"""
|
"""
|
||||||
room - name of the room to connect to
|
url_format - url the bot will connect to, where the room name is represented by {}
|
||||||
|
tries - how often to try to reconnect when connection is lost (-1 - try forever)
|
||||||
|
delay - time (in seconds) to wait between tries
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.room = room
|
self.room = room
|
||||||
|
self.tries = tries
|
||||||
|
self.delay = delay
|
||||||
|
self.url_format = url_format
|
||||||
|
|
||||||
if not url_format:
|
self.start_time = None
|
||||||
url_format = self.ROOM_FORMAT
|
|
||||||
self._url = url_format.format(self.room)
|
|
||||||
|
|
||||||
self._stopping = False
|
self._stopping = True
|
||||||
|
|
||||||
self._ws = None
|
self._ws = None
|
||||||
self._thread = None
|
self._thread = None
|
||||||
self._send_id = 0
|
self._send_id = 0
|
||||||
self._callbacks = callbacks.Callbacks()
|
self._callbacks = Callbacks()
|
||||||
self._id_callbacks = callbacks.Callbacks()
|
self._id_callbacks = Callbacks()
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
|
||||||
def _connect(self, tries=-1, delay=10):
|
def __enter__(self):
|
||||||
|
self._lock.acquire()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
self._lock.release()
|
||||||
|
|
||||||
|
def _connect(self, tries=10, delay=30):
|
||||||
"""
|
"""
|
||||||
_connect(tries, delay) -> bool
|
_connect(tries, delay) -> bool
|
||||||
|
|
||||||
tries - maximum number of retries
|
delay - delay between retries (in seconds)
|
||||||
-1 -> retry indefinitely
|
tries - maximum number of retries
|
||||||
|
-1 -> retry indefinitely
|
||||||
|
|
||||||
Returns True on success, False on failure.
|
Returns True on success, False on failure.
|
||||||
|
|
||||||
|
|
@ -56,50 +75,35 @@ class Connection():
|
||||||
|
|
||||||
while tries != 0:
|
while tries != 0:
|
||||||
try:
|
try:
|
||||||
|
url = self.url_format.format(self.room)
|
||||||
|
logger.info("Connecting to url: {!r} ({} {} left)".format(
|
||||||
|
url,
|
||||||
|
tries-1 if tries > 0 else "infinite",
|
||||||
|
"tries" if (tries-1) != 1 else "try" # proper english :D
|
||||||
|
))
|
||||||
self._ws = websocket.create_connection(
|
self._ws = websocket.create_connection(
|
||||||
self._url,
|
url,
|
||||||
enable_multithread=True
|
enable_multithread=True,
|
||||||
|
sslopt=SSLOPT
|
||||||
)
|
)
|
||||||
|
|
||||||
self._callbacks.call("connect")
|
except (WSException, socket.gaierror, TimeoutError):
|
||||||
|
|
||||||
return True
|
|
||||||
except WSException:
|
|
||||||
if tries > 0:
|
if tries > 0:
|
||||||
tries -= 1
|
tries -= 1
|
||||||
if tries != 0:
|
if tries != 0:
|
||||||
|
logger.info("Connection failed. Retrying in {} seconds.".format(delay))
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
|
else:
|
||||||
|
logger.info("No more tries, stopping.")
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug("Connected")
|
||||||
|
self._callbacks.call("connect")
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
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
|
|
||||||
|
|
||||||
self._callbacks.call("disconnect")
|
|
||||||
|
|
||||||
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="{}-{}".format(self.room, int(time.time())))
|
|
||||||
self._thread.start()
|
|
||||||
return self._thread
|
|
||||||
else:
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
"""
|
"""
|
||||||
_run() -> None
|
_run() -> None
|
||||||
|
|
@ -107,13 +111,104 @@ class Connection():
|
||||||
Receive messages.
|
Receive messages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
logger.debug("Running")
|
||||||
|
|
||||||
while not self._stopping:
|
while not self._stopping:
|
||||||
try:
|
try:
|
||||||
self._handle_json(self._ws.recv())
|
j = self._ws.recv()
|
||||||
|
self._handle_json(j)
|
||||||
except (WSException, ConnectionResetError):
|
except (WSException, ConnectionResetError):
|
||||||
if not self._stopping:
|
if not self._stopping:
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
self._connect()
|
self._connect(self.tries, self.delay)
|
||||||
|
|
||||||
|
logger.debug("Finished running")
|
||||||
|
self._thread = None
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
ptype = packet.get("type")
|
||||||
|
logger.debug("Handling packet of type {}.".format(ptype))
|
||||||
|
|
||||||
|
data = packet.get("data")
|
||||||
|
if "error" in packet:
|
||||||
|
logger.debug("Error in packet: {!r}".format(error))
|
||||||
|
|
||||||
|
if "id" in packet:
|
||||||
|
self._id_callbacks.call(packet["id"], data, packet)
|
||||||
|
self._id_callbacks.remove(packet["id"])
|
||||||
|
|
||||||
|
self._callbacks.call(packet["type"], data, packet)
|
||||||
|
|
||||||
|
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 launch(self):
|
||||||
|
"""
|
||||||
|
launch() -> bool
|
||||||
|
|
||||||
|
Connect to the room and spawn a new thread.
|
||||||
|
Returns True if connecting was successful and a new thread was spawned.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._connect(1):
|
||||||
|
self.start_time = time.time()
|
||||||
|
self._stopping = False
|
||||||
|
|
||||||
|
self._thread = threading.Thread(
|
||||||
|
target=self._run,
|
||||||
|
name="{}-{}".format(int(self.start_time), self.room)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug("Launching new thread: {}".format(self._thread.name))
|
||||||
|
self._thread.start()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""
|
||||||
|
disconnect() -> None
|
||||||
|
|
||||||
|
Disconnect from the room.
|
||||||
|
This will cause the connection to reconnect.
|
||||||
|
To completely disconnect, use stop().
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._ws:
|
||||||
|
logger.debug("Closing connection!")
|
||||||
|
self._ws.abort()
|
||||||
|
self._ws.close()
|
||||||
|
self._ws = None
|
||||||
|
|
||||||
|
logger.debug("Disconnected")
|
||||||
|
self._id_callbacks = Callbacks() # we don't need the old id callbacks any more
|
||||||
|
self._callbacks.call("disconnect")
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -123,6 +218,7 @@ class Connection():
|
||||||
Joins the thread launched by self.launch().
|
Joins the thread launched by self.launch().
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
logger.debug("Stopping")
|
||||||
self._stopping = True
|
self._stopping = True
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
|
|
||||||
|
|
@ -140,79 +236,33 @@ class Connection():
|
||||||
|
|
||||||
return str(self._send_id)
|
return str(self._send_id)
|
||||||
|
|
||||||
def add_callback(self, ptype, callback, *args, **kwargs):
|
def subscribe(self, ptype, callback, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
add_callback(ptype, callback, *args, **kwargs) -> None
|
subscribe(ptype, callback, *args, **kwargs) -> None
|
||||||
|
|
||||||
Add a function to be called when a packet of type ptype is received.
|
Add a function to be called when a packet of type ptype is received.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self._callbacks.add(ptype, callback, *args, **kwargs)
|
self._callbacks.add(ptype, callback, *args, **kwargs)
|
||||||
|
|
||||||
def add_id_callback(self, pid, callback, *args, **kwargs):
|
def subscribe_to_id(self, pid, callback, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
add_id_callback(pid, callback, *args, **kwargs) -> None
|
subscribe_to_id(pid, callback, *args, **kwargs) -> None
|
||||||
|
|
||||||
Add a function to be called when a packet with id pid is received.
|
Add a function to be called when a packet with id pid is received.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self._id_callbacks.add(pid, callback, *args, **kwargs)
|
self._id_callbacks.add(pid, callback, *args, **kwargs)
|
||||||
|
|
||||||
def add_next_callback(self, callback, *args, **kwargs):
|
def subscribe_to_next(self, callback, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
add_next_callback(callback, *args, **kwargs) -> None
|
subscribe_to_next(callback, *args, **kwargs) -> None
|
||||||
|
|
||||||
Add a function to be called when the answer to the next message sent is received.
|
Add a function to be called when the answer to the next message sent is received.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self._id_callbacks.add(self.next_id(), callback, *args, **kwargs)
|
self._id_callbacks.add(self.next_id(), callback, *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
|
|
||||||
|
|
||||||
if "error" in packet:
|
|
||||||
error = packet["error"]
|
|
||||||
else:
|
|
||||||
error = None
|
|
||||||
|
|
||||||
self._callbacks.call(packet["type"], data, error)
|
|
||||||
|
|
||||||
if "id" in packet:
|
|
||||||
self._id_callbacks.call(packet["id"], data, error)
|
|
||||||
self._id_callbacks.remove(packet["id"])
|
|
||||||
|
|
||||||
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):
|
def send_packet(self, ptype, **kwargs):
|
||||||
"""
|
"""
|
||||||
send_packet(ptype, **kwargs) -> None
|
send_packet(ptype, **kwargs) -> None
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
class YaboliException(Exception):
|
|
||||||
"""
|
|
||||||
Generic yaboli exception class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
class BotManagerException(YaboliException):
|
|
||||||
"""
|
|
||||||
Generic BotManager exception class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
class CreateBotException(BotManagerException):
|
|
||||||
"""
|
|
||||||
This exception will be raised when BotManager could not create a bot.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
class BotNotFoundException(BotManagerException):
|
|
||||||
"""
|
|
||||||
This exception will be raised when BotManager could not find a bot.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
class BotException(YaboliException):
|
|
||||||
"""
|
|
||||||
Generic Bot exception class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ParseMessageException(BotException):
|
|
||||||
"""
|
|
||||||
This exception will be raised when a failure parsing a message occurs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
import time
|
|
||||||
|
|
||||||
from . import session
|
|
||||||
|
|
||||||
class Message():
|
|
||||||
"""
|
|
||||||
This class keeps track of message details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, id, time, sender, content, parent=None, edited=None, deleted=None,
|
|
||||||
truncated=None):
|
|
||||||
"""
|
|
||||||
id - message id
|
|
||||||
time - time the message was sent (epoch)
|
|
||||||
sender - session of the sender
|
|
||||||
content - content of the message
|
|
||||||
parent - id of the parent message, or None
|
|
||||||
edited - time of last edit (epoch)
|
|
||||||
deleted - time of deletion (epoch)
|
|
||||||
truncated - message was truncated
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.id = id
|
|
||||||
self.time = time
|
|
||||||
self.sender = sender
|
|
||||||
self.content = content
|
|
||||||
self.parent = parent
|
|
||||||
self.edited = edited
|
|
||||||
self.deleted = deleted
|
|
||||||
self.truncated = truncated
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_data(self, data):
|
|
||||||
"""
|
|
||||||
Creates and returns a message created from the data.
|
|
||||||
NOTE: This also creates a session object using the data in "sender".
|
|
||||||
|
|
||||||
data - a euphoria message: http://api.euphoria.io/#message
|
|
||||||
"""
|
|
||||||
|
|
||||||
sender = session.Session.from_data(data["sender"])
|
|
||||||
parent = data["parent"] if "parent" in data else None
|
|
||||||
edited = data["edited"] if "edited" in data else None
|
|
||||||
deleted = data["deleted"] if "deleted" in data else None
|
|
||||||
truncated = data["truncated"] if "truncated" in data else None
|
|
||||||
|
|
||||||
return self(
|
|
||||||
data["id"],
|
|
||||||
data["time"],
|
|
||||||
sender,
|
|
||||||
data["content"],
|
|
||||||
parent=parent,
|
|
||||||
edited=edited,
|
|
||||||
deleted=deleted,
|
|
||||||
truncated=truncated
|
|
||||||
)
|
|
||||||
|
|
||||||
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 is_edited(self):
|
|
||||||
"""
|
|
||||||
is_edited() -> bool
|
|
||||||
|
|
||||||
Has this message been edited?
|
|
||||||
"""
|
|
||||||
|
|
||||||
return True if self.edited is not None else False
|
|
||||||
|
|
||||||
def is_deleted(self):
|
|
||||||
"""
|
|
||||||
is_deleted() -> bool
|
|
||||||
|
|
||||||
Has this message been deleted?
|
|
||||||
"""
|
|
||||||
|
|
||||||
return True if self.deleted is not None else False
|
|
||||||
|
|
||||||
def is_truncated(self):
|
|
||||||
"""
|
|
||||||
is_truncated() -> bool
|
|
||||||
|
|
||||||
Has this message been truncated?
|
|
||||||
"""
|
|
||||||
|
|
||||||
return True if self.truncated is not None else False
|
|
||||||
4
yaboli/messagedb.py
Normal file
4
yaboli/messagedb.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
class MessageDB():
|
||||||
|
pass
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
import operator
|
|
||||||
|
|
||||||
from . import message
|
|
||||||
|
|
||||||
class Messages():
|
|
||||||
"""
|
|
||||||
Message storage class which preserves thread hierarchy.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, message_limit=500):
|
|
||||||
"""
|
|
||||||
message_limit - maximum amount of messages that will be stored at a time
|
|
||||||
None - no limit
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.message_limit = message_limit
|
|
||||||
|
|
||||||
self._by_id = {}
|
|
||||||
self._by_parent = {}
|
|
||||||
|
|
||||||
def _sort(self, msgs):
|
|
||||||
"""
|
|
||||||
_sort(messages) -> None
|
|
||||||
|
|
||||||
Sorts a list of messages by their id, in place.
|
|
||||||
"""
|
|
||||||
|
|
||||||
msgs.sort(key=operator.attrgetter("id"))
|
|
||||||
|
|
||||||
def add_from_data(self, data):
|
|
||||||
"""
|
|
||||||
add_from_data(data) -> None
|
|
||||||
|
|
||||||
Create a message from "raw" data and add it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
mes = message.Message.from_data(data)
|
|
||||||
|
|
||||||
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[mes.parent] = []
|
|
||||||
self._by_parent[mes.parent].append(mes)
|
|
||||||
|
|
||||||
if self.message_limit and len(self._by_id) > self.message_limit:
|
|
||||||
self.remove(self.get_oldest().id)
|
|
||||||
|
|
||||||
def remove(self, mid):
|
|
||||||
"""
|
|
||||||
remove(message_id) -> None
|
|
||||||
|
|
||||||
Remove a message from the structure.
|
|
||||||
"""
|
|
||||||
|
|
||||||
mes = self.get(mid)
|
|
||||||
if mes:
|
|
||||||
if mes.id in self._by_id:
|
|
||||||
self._by_id.pop(mes.id)
|
|
||||||
|
|
||||||
parent = self.get_parent(mes.id)
|
|
||||||
if parent and mes in self.get_children(parent.id):
|
|
||||||
self._by_parent[mes.parent].remove(mes)
|
|
||||||
|
|
||||||
def remove_all(self):
|
|
||||||
"""
|
|
||||||
remove_all() -> None
|
|
||||||
|
|
||||||
Removes all messages.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._by_id = {}
|
|
||||||
self._by_parent = {}
|
|
||||||
|
|
||||||
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_oldest(self):
|
|
||||||
"""
|
|
||||||
get_oldest() -> Message
|
|
||||||
|
|
||||||
Returns the oldest message, if found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
oldest = None
|
|
||||||
for mid in self._by_id:
|
|
||||||
if oldest is None or mid < oldest:
|
|
||||||
oldest = mid
|
|
||||||
return self.get(oldest)
|
|
||||||
|
|
||||||
def get_youngest(self):
|
|
||||||
"""
|
|
||||||
get_youngest() -> Message
|
|
||||||
|
|
||||||
Returns the youngest message, if found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
youngest = None
|
|
||||||
for mid in self._by_id:
|
|
||||||
if youngest is None or mid > youngest:
|
|
||||||
youngest = mid
|
|
||||||
return self.get(youngest)
|
|
||||||
|
|
||||||
def get_parent(self, mid):
|
|
||||||
"""
|
|
||||||
get_parent(message_id) -> str
|
|
||||||
|
|
||||||
Returns the message's parent, if found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
mes = self.get(mid)
|
|
||||||
if mes:
|
|
||||||
return self.get(mes.parent)
|
|
||||||
|
|
||||||
def get_children(self, mid):
|
|
||||||
"""
|
|
||||||
get_children(message_id) -> list
|
|
||||||
|
|
||||||
Returns a sorted list of children of the given message, if found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if mid in self._by_parent:
|
|
||||||
children = self._by_parent[mid][:]
|
|
||||||
self._sort(children)
|
|
||||||
return children
|
|
||||||
|
|
||||||
def get_top_level(self):
|
|
||||||
"""
|
|
||||||
get_top_level() -> list
|
|
||||||
|
|
||||||
Returns a sorted list of top-level messages.
|
|
||||||
"""
|
|
||||||
|
|
||||||
msgs = [self.get(mid) for mid in self._by_id if not self.get(mid).parent]
|
|
||||||
self._sort(msgs)
|
|
||||||
return msgs
|
|
||||||
|
|
@ -1,71 +1,344 @@
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from .callbacks import Callbacks
|
||||||
|
from .connection import Connection
|
||||||
|
from .basic_types import Message, SessionView, mention
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Session():
|
class Session():
|
||||||
"""
|
"""
|
||||||
This class keeps track of session details.
|
Deals with the things arising from being connected to a room, such as:
|
||||||
|
- playing ping pong
|
||||||
|
- having a name (usually)
|
||||||
|
- seeing other clients
|
||||||
|
- sending and receiving messages
|
||||||
|
|
||||||
|
event (args) | meaning
|
||||||
|
--------------------|-------------------------------------------------
|
||||||
|
join (bool) | joining the room was successful/not successful
|
||||||
|
| Callbacks for this event are cleared whenever it is called.
|
||||||
|
enter | can view the room
|
||||||
|
ready | can view the room and post messages (has a nick)
|
||||||
|
sessions-update | self.sessions has changed
|
||||||
|
own-session-update | your own message has changed
|
||||||
|
message (msg) | a message has been received (no own messages)
|
||||||
|
own-message (msg) | a message that you have sent
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, id, name, server_id, server_era, session_id, is_staff=None, is_manager=None):
|
def __init__(self, room, password=None, name=None, timeout=10):
|
||||||
"""
|
self.password = password
|
||||||
id - agent/account id
|
self.real_name = name
|
||||||
name - name of the client when the SessionView was captured
|
|
||||||
server_id - id of the server
|
|
||||||
server_era - era of the server
|
|
||||||
session_id - session id (unique across euphoria)
|
|
||||||
is_staff - client is staff
|
|
||||||
is_manager - client is manager
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.id = id
|
self._room_accessible = False
|
||||||
self.name = name
|
self._room_accessible_event = threading.Event()
|
||||||
self.server_id = server_id
|
self._room_accessible_timeout = threading.Timer(timeout, self.stop)
|
||||||
self.server_era = server_era
|
|
||||||
self.session_id = session_id
|
self._connection = Connection(room)
|
||||||
self.staff = is_staff
|
self._connection.subscribe("disconnect", self._reset_variables)
|
||||||
self.manager = is_manager
|
# and now the packet types
|
||||||
|
self._connection.subscribe("bounce-event", self._handle_bounce_event)
|
||||||
|
self._connection.subscribe("disconnect-event", self._handle_disconnect_event)
|
||||||
|
self._connection.subscribe("hello-event", self._handle_hello_event)
|
||||||
|
self._connection.subscribe("join-event", self._handle_join_event)
|
||||||
|
self._connection.subscribe("logout-event", self._handle_logout_event)
|
||||||
|
self._connection.subscribe("network-event", self._handle_network_event)
|
||||||
|
self._connection.subscribe("nick-event", self._handle_nick_event)
|
||||||
|
self._connection.subscribe("edit-message-event", self._handle_edit_message_event)
|
||||||
|
self._connection.subscribe("part-event", self._handle_part_event)
|
||||||
|
self._connection.subscribe("ping-event", self._handle_ping_event)
|
||||||
|
self._connection.subscribe("pm-initiate-event", self._handle_pm_initiate_event)
|
||||||
|
self._connection.subscribe("send-event", self._handle_send_event)
|
||||||
|
self._connection.subscribe("snapshot-event", self._handle_snapshot_event)
|
||||||
|
|
||||||
|
self._callbacks = Callbacks()
|
||||||
|
self.subscribe("enter", self._on_enter)
|
||||||
|
|
||||||
|
#self._hello_event_completed = False
|
||||||
|
#self._snapshot_event_completed = False
|
||||||
|
#self._ready = False
|
||||||
|
#self.my_session = SessionView(None, None, None, None, None)
|
||||||
|
#self.sessions = {} # sessions in the room
|
||||||
|
#self.room_is_private = None
|
||||||
|
#self.server_version = None
|
||||||
|
|
||||||
|
self._reset_variables()
|
||||||
|
|
||||||
@classmethod
|
def _reset_variables(self):
|
||||||
def from_data(self, data):
|
logger.debug("Resetting room-related variables")
|
||||||
"""
|
self._room_accessible = False
|
||||||
Creates and returns a session created from the data.
|
|
||||||
|
|
||||||
data - a euphoria SessionView: http://api.euphoria.io/#sessionview
|
self.my_session = SessionView(None, None, None, None, None)
|
||||||
"""
|
self.sessions = {}
|
||||||
|
|
||||||
is_staff = data["is_staff"] if "is_staff" in data else None
|
self._hello_event_completed = False
|
||||||
is_manager = data["is_manager"] if "is_manager" in data else None
|
self._snapshot_event_completed = False
|
||||||
|
self._ready = False
|
||||||
|
|
||||||
return self(
|
self.room_is_private = None
|
||||||
data["id"],
|
self.server_version = None
|
||||||
data["name"],
|
|
||||||
data["server_id"],
|
|
||||||
data["server_era"],
|
|
||||||
data["session_id"],
|
|
||||||
is_staff,
|
|
||||||
is_manager
|
|
||||||
)
|
|
||||||
|
|
||||||
def session_type(self):
|
def _set_name(self, new_name):
|
||||||
"""
|
with self._connection as conn:
|
||||||
session_type() -> str
|
logger.debug("Setting name to {!r}".format(new_name))
|
||||||
|
conn.subscribe_to_next(self._handle_nick_reply)
|
||||||
The session's type (bot, account, agent).
|
conn.send_packet("nick", name=new_name)
|
||||||
"""
|
|
||||||
|
|
||||||
return self.id.split(":")[0]
|
|
||||||
|
|
||||||
def is_staff(self):
|
def _on_enter(self):
|
||||||
"""
|
logger.info("Connected and authenticated.")
|
||||||
is_staff() -> bool
|
self._room_accessible_timeout.cancel()
|
||||||
|
self._room_accessible = True
|
||||||
|
self._room_accessible_event.set()
|
||||||
|
self._room_accessible_event.clear()
|
||||||
|
|
||||||
Is a user staff?
|
if self.real_name:
|
||||||
"""
|
self._set_name(self.real_name)
|
||||||
|
|
||||||
return self.staff and True or False
|
|
||||||
|
|
||||||
def is_manager(self):
|
def launch(self, timeout=10):
|
||||||
"""
|
logger.info("Launching session &{}.".format(room))
|
||||||
is_manager() -> bool
|
|
||||||
|
|
||||||
Is a user manager?
|
self._room_accessible_timeout.start()
|
||||||
"""
|
|
||||||
|
|
||||||
return self.staff and True or False
|
if self._connection.launch(room):
|
||||||
|
logger.debug("Connection established. Waiting for correct events")
|
||||||
|
self._room_accessible_event.wait()
|
||||||
|
return self._room_accessible
|
||||||
|
else:
|
||||||
|
logger.warn("Could not connect to room url.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def launch(self):
|
||||||
|
return self._connection.launch()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
logger.info("Stopping")
|
||||||
|
self._room_accessible_timeout.cancel()
|
||||||
|
self._room_accessible = False
|
||||||
|
self._room_accessible_event.set()
|
||||||
|
self._room_accessible_event.clear()
|
||||||
|
|
||||||
|
with self._connection as conn:
|
||||||
|
conn.stop()
|
||||||
|
|
||||||
|
def subscribe(self, event, callback, *args, **kwargs):
|
||||||
|
logger.debug("Adding callback {} to {}".format(callback, event))
|
||||||
|
self._callbacks.add(event, callback, *args, **kwargs)
|
||||||
|
|
||||||
|
def send(self, content, parent=None):
|
||||||
|
if self._ready:
|
||||||
|
self._connection.send_packet("send", content=content, parent=parent)
|
||||||
|
logger.debug("Message sent.")
|
||||||
|
else:
|
||||||
|
logger.warn("Attempted to send message while not ready.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.my_session.name
|
||||||
|
|
||||||
|
@name.setter
|
||||||
|
def name(self, new_name):
|
||||||
|
self.real_name = new_name
|
||||||
|
|
||||||
|
if not self._ready:
|
||||||
|
self._set_name(new_name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def room(self):
|
||||||
|
return self._connection.room
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start_time(self):
|
||||||
|
return self._connection.start_time
|
||||||
|
|
||||||
|
def refresh_sessions(self):
|
||||||
|
logger.debug("Refreshing sessions")
|
||||||
|
self._connection.send_packet("who")
|
||||||
|
|
||||||
|
def _set_sessions_from_listing(self, listing):
|
||||||
|
self.sessions = {}
|
||||||
|
|
||||||
|
for item in listing:
|
||||||
|
view = SessionView.from_data(item)
|
||||||
|
self.sessions[view.session_id] = view
|
||||||
|
|
||||||
|
self._callbacks.call("sessions-update")
|
||||||
|
|
||||||
|
def _revert_to_revious_room(self):
|
||||||
|
self._callbacks.call("join", False)
|
||||||
|
|
||||||
|
if self._prev_room:
|
||||||
|
self.password = self._prev_password
|
||||||
|
self.room = self._prev_room # shouldn't do this
|
||||||
|
|
||||||
|
self._prev_room = None
|
||||||
|
self._prev_password = None
|
||||||
|
else:
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def _handle_bounce_event(self, data, packet):
|
||||||
|
if data.get("reason") == "authentication required":
|
||||||
|
if self.password:
|
||||||
|
with self._connection as conn:
|
||||||
|
conn.subscribe_to_next(self._handle_auth_reply)
|
||||||
|
conn.send_packet("auth", type="passcode", passcode=self.password)
|
||||||
|
else:
|
||||||
|
logger.warn("Could not access &{}: No password.".format(self._connection.room))
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def _handle_disconnect_event(self, data, packet):
|
||||||
|
self._connection.disconnect() # should reconnect
|
||||||
|
|
||||||
|
def _handle_hello_event(self, data, packet):
|
||||||
|
self.my_session.read_data(data.get("session"))
|
||||||
|
self._callbacks.call("own-session-update")
|
||||||
|
|
||||||
|
self.room_is_private = data.get("room_is_private")
|
||||||
|
self.server_version = data.get("version")
|
||||||
|
|
||||||
|
self._hello_event_completed = True
|
||||||
|
if self._snapshot_event_completed:
|
||||||
|
self._callbacks.call("enter")
|
||||||
|
|
||||||
|
def _handle_join_event(self, data, packet):
|
||||||
|
view = SessionView.from_data(data)
|
||||||
|
self.sessions[view.session_id] = view
|
||||||
|
|
||||||
|
if view.name:
|
||||||
|
logger.debug("@{} joined the room.".format(mention(view.name)))
|
||||||
|
else:
|
||||||
|
logger.debug("Someone joined the room.")
|
||||||
|
|
||||||
|
self._callbacks.call("sessions-update")
|
||||||
|
|
||||||
|
def _handle_logout_event(self, data, packet):
|
||||||
|
# no idea why this should happen to the bot
|
||||||
|
# just reconnect, in case it does happen
|
||||||
|
self._connection.disconnect()
|
||||||
|
|
||||||
|
def _handle_network_event(self, data, packet):
|
||||||
|
if data.get("type") == "partition":
|
||||||
|
prev_len = len(self.sessions)
|
||||||
|
|
||||||
|
# only remove views matching the server_id/server_era combo
|
||||||
|
self.sessions = {
|
||||||
|
sid: view for sid, view in self.sessions.items()
|
||||||
|
if view.server_id != data.get("server_id")
|
||||||
|
or view.server_era != data.get("server_era")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sessions) != prev_len:
|
||||||
|
logger.info("Some people left after a network event.")
|
||||||
|
else:
|
||||||
|
logger.info("No people left after a network event.")
|
||||||
|
|
||||||
|
self._callbacks.call("sessions-update")
|
||||||
|
|
||||||
|
def _handle_nick_event(self, data, packet):
|
||||||
|
session_id = data.get("session_id")
|
||||||
|
|
||||||
|
if session_id not in self.sessions:
|
||||||
|
logger.warn("SessionView not found: Refreshing sessions.")
|
||||||
|
self.refresh_sessions()
|
||||||
|
else:
|
||||||
|
self.sessions[session_id].name = data.get("to")
|
||||||
|
|
||||||
|
if data.get("from"):
|
||||||
|
logger.debug("@{} changed their name to @{}.".format(
|
||||||
|
mention(data.get("from")),
|
||||||
|
mention(data.get("to"))
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
logger.debug("Someone changed their name to @{}.".format(
|
||||||
|
mention(data.get("to"))
|
||||||
|
))
|
||||||
|
|
||||||
|
self._callbacks.call("sessions-update")
|
||||||
|
|
||||||
|
def _handle_edit_message_event(self, data, packet):
|
||||||
|
# TODO: implement
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _handle_part_event(self, data, packet):
|
||||||
|
view = SessionView.from_data(data)
|
||||||
|
if view.session_id not in self.sessions:
|
||||||
|
logger.warn("SessionView not found: Refreshing sessions.")
|
||||||
|
self.refresh_sessions()
|
||||||
|
else:
|
||||||
|
del self.sessions[view.session_id]
|
||||||
|
|
||||||
|
if view.name:
|
||||||
|
logger.debug("@{} left the room.".format(mention(view.name)))
|
||||||
|
else:
|
||||||
|
logger.debug("Someone left the room.")
|
||||||
|
|
||||||
|
self._callbacks.call("sessions-update")
|
||||||
|
|
||||||
|
def _handle_ping_event(self, data, packet):
|
||||||
|
with self._connection as conn:
|
||||||
|
conn.send_packet("ping-reply", time=data.get("time"))
|
||||||
|
|
||||||
|
def _handle_pm_initiate_event(self, data, error):
|
||||||
|
pass # placeholder, maybe implemented in the future
|
||||||
|
|
||||||
|
def _handle_send_event(self, data, error):
|
||||||
|
# TODO: implement
|
||||||
|
msg = Message.from_data(data)
|
||||||
|
self._callbacks.call("message", msg)
|
||||||
|
|
||||||
|
def _handle_snapshot_event(self, data, packet):
|
||||||
|
# deal with connected sessions
|
||||||
|
self._set_sessions_from_listing(data.get("listing"))
|
||||||
|
|
||||||
|
# deal with messages
|
||||||
|
# TODO: implement
|
||||||
|
|
||||||
|
# deal with other info
|
||||||
|
self.server_version = data.get("version")
|
||||||
|
if "nick" in data:
|
||||||
|
self.my_session.name = data.get("nick")
|
||||||
|
self._callbacks.call("own-session-update")
|
||||||
|
|
||||||
|
self._snapshot_event_completed = True
|
||||||
|
if self._hello_event_completed:
|
||||||
|
self._callbacks.call("enter")
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_auth_reply(self, data, packet):
|
||||||
|
if not data.get("success"):
|
||||||
|
logger.warn("Could not authenticate, reason: {!r}".format(data.get("reason")))
|
||||||
|
self.stop()
|
||||||
|
else:
|
||||||
|
logger.debug("Authetication complete, password was correct.")
|
||||||
|
|
||||||
|
def _handle_get_message_reply(self, data, packet):
|
||||||
|
# TODO: implement
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _handle_log_event(self, data, packet):
|
||||||
|
# TODO: implement
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _handle_nick_reply(self, data, packet):
|
||||||
|
first_name = not self.name
|
||||||
|
|
||||||
|
if data.get("from"):
|
||||||
|
logger.info("Changed name from {!r} to {!r}.".format(data.get("from"), data.get("to")))
|
||||||
|
else:
|
||||||
|
logger.info("Changed name to {!r}.".format(data.get("to")))
|
||||||
|
|
||||||
|
self.my_session.name = data.get("to")
|
||||||
|
self._callbacks.call("own-session-update")
|
||||||
|
|
||||||
|
if first_name:
|
||||||
|
self._ready = True
|
||||||
|
self._callbacks.call("ready")
|
||||||
|
|
||||||
|
def _handle_send_reply(self, data, packet):
|
||||||
|
# TODO: implement
|
||||||
|
msg = Message.from_data(data)
|
||||||
|
self._callbacks.call("own-message", msg)
|
||||||
|
|
||||||
|
def _handle_who_reply(self, data, packet):
|
||||||
|
self._set_sessions_from_listing(data.get("listing"))
|
||||||
|
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
from . import session
|
|
||||||
|
|
||||||
class Sessions():
|
|
||||||
"""
|
|
||||||
Keeps track of sessions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
TODO
|
|
||||||
"""
|
|
||||||
self._sessions = {}
|
|
||||||
|
|
||||||
def add_from_data(self, data):
|
|
||||||
"""
|
|
||||||
add_raw(data) -> None
|
|
||||||
|
|
||||||
Create a session from "raw" data and add it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
ses = session.Session.from_data(data)
|
|
||||||
|
|
||||||
self._sessions[ses.session_id] = ses
|
|
||||||
|
|
||||||
def add(self, ses):
|
|
||||||
"""
|
|
||||||
add(session) -> None
|
|
||||||
|
|
||||||
Add a session.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._sessions[ses.session_id] = ses
|
|
||||||
|
|
||||||
def get(self, sid):
|
|
||||||
"""
|
|
||||||
get(session_id) -> Session
|
|
||||||
|
|
||||||
Returns the session with that id.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if sid in self._sessions:
|
|
||||||
return self._sessions[sid]
|
|
||||||
|
|
||||||
def remove(self, sid):
|
|
||||||
"""
|
|
||||||
remove(session) -> None
|
|
||||||
|
|
||||||
Remove a session.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if sid in self._sessions:
|
|
||||||
self._sessions.pop(sid)
|
|
||||||
|
|
||||||
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
|
|
||||||
"""
|
|
||||||
|
|
||||||
sesnew = {}
|
|
||||||
for sid in self._sessions:
|
|
||||||
ses = self.get(sid)
|
|
||||||
if not (ses.server_id == server_id and ses.server_era == server_era):
|
|
||||||
sesnew[sid] = ses
|
|
||||||
self._sessions = sesnew
|
|
||||||
|
|
||||||
def remove_all(self):
|
|
||||||
"""
|
|
||||||
remove_all() -> None
|
|
||||||
|
|
||||||
Removes all sessions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._sessions = {}
|
|
||||||
|
|
||||||
def get_all(self):
|
|
||||||
"""
|
|
||||||
get_all() -> list
|
|
||||||
|
|
||||||
Returns the full list of sessions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return [ses for sid, ses in self._sessions.items()]
|
|
||||||
|
|
||||||
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 sid in self._sessions:
|
|
||||||
ses = self.get(sid)
|
|
||||||
if ses.session_type() in ["agent", "account"] and ses.name:
|
|
||||||
people.append(ses)
|
|
||||||
return people
|
|
||||||
|
|
||||||
def _get_by_type(self, tp):
|
|
||||||
"""
|
|
||||||
_get_by_type(session_type) -> list
|
|
||||||
|
|
||||||
Returns a list of all non-lurker sessions with that type.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return [ses for sid, ses in self._sessions.items()
|
|
||||||
if ses.session_type() == tp and ses.name]
|
|
||||||
|
|
||||||
def get_accounts(self):
|
|
||||||
"""
|
|
||||||
get_accounts() -> list
|
|
||||||
|
|
||||||
Returns a list of all logged-in sessions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._get_by_type("account")
|
|
||||||
|
|
||||||
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 self._get_by_type("agent")
|
|
||||||
|
|
||||||
def get_bots(self):
|
|
||||||
"""
|
|
||||||
get_bots() -> list
|
|
||||||
|
|
||||||
Returns a list of all bot sessions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._get_by_type("bot")
|
|
||||||
|
|
||||||
def get_lurkers(self):
|
|
||||||
"""
|
|
||||||
get_lurkers() -> list
|
|
||||||
|
|
||||||
Returns a list of all lurker sessions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return [ses for sid, ses in self._sessions.items() if not ses.name]
|
|
||||||
21
yaboli/utils.py
Normal file
21
yaboli/utils.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
def mention(name):
|
||||||
|
"""
|
||||||
|
mention(name) -> name
|
||||||
|
|
||||||
|
Removes all whitespace and some special characters from the name,
|
||||||
|
such that the resulting name, if prepended with a "@", will mention the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return "".join(c for c in name if not c in ".!?;&<'\"" and not c.isspace())
|
||||||
|
|
||||||
|
def reduce_name(name):
|
||||||
|
"""
|
||||||
|
reduce_name(name) -> name
|
||||||
|
|
||||||
|
Reduces a name to a form which can be compared with other such forms.
|
||||||
|
If two such forms are equal, they are both mentioned by the same @mentions,
|
||||||
|
and should be considered identical when used to identify users.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#TODO: implement
|
||||||
|
pass
|
||||||
Loading…
Add table
Add a link
Reference in a new issue