Compare commits

...
Sign in to create a new pull request.

17 commits

Author SHA1 Message Date
Joscha
23329238c6 Ignore notes 2017-05-23 06:03:34 +00:00
Joscha
966034bdde Move utilities to seperate file 2017-04-03 20:36:27 +00:00
Joscha
2529c2d238 Rewrite session and connection
A session and connection now have a room assigned to them for their lifetime.
You can't connect a session to another room.
The launch() function must only be called once.
2017-04-02 20:10:59 +00:00
Joscha
c4fdb2942e Change switching rooms 2017-04-02 14:26:03 +00:00
Joscha
75b2108b47 Remove old files 2017-03-29 20:56:22 +00:00
Joscha
f56af13ede Add function to update sessions from a listing
This logic is used multiple times in the session.
2017-03-29 20:35:45 +00:00
Joscha
eb2b459216 Change all event and reply handling functions to hidden 2017-03-29 20:30:52 +00:00
Joscha
e9354194cf Deal with almost all other (useful) events and commands 2017-03-29 20:25:43 +00:00
Joscha
14bae17104 Handle connecting to rooms 2017-03-29 17:24:29 +00:00
Joscha
04f7c9c781 Change "add_callback" functions to "subscribe" functions 2017-03-28 20:52:39 +00:00
Joscha
f366a02758 Add Session with a few events already implemented 2017-03-28 20:27:45 +00:00
Joscha
1b9d12d253 Revise Connection room switching logic 2017-03-28 20:27:21 +00:00
Joscha
f1314c7ec1 Add context handlers to Connection and use system ca_cert file 2017-03-28 16:24:32 +00:00
Joscha
aee8e5c118 Clean up Connection and add logging 2017-03-28 13:41:20 +00:00
Joscha
3b3ce99625 Add logging and log formats 2017-03-28 13:40:54 +00:00
Joscha
4e37154737 Clean up basic_types 2017-03-28 07:38:09 +00:00
Joscha
dd4b5144a9 Reorganize Message and SessionView into basic_types 2017-03-27 21:26:15 +00:00
11 changed files with 682 additions and 609 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
yaboli/__pycache__/ yaboli/__pycache__/
*.txt

View file

@ -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
View 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

View file

@ -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

View file

@ -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

View file

@ -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
View file

@ -0,0 +1,4 @@
import sqlite3
class MessageDB():
pass

View file

@ -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

View file

@ -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.staff = is_staff
self.manager = is_manager
@classmethod self._connection = Connection(room)
def from_data(self, data): self._connection.subscribe("disconnect", self._reset_variables)
""" # and now the packet types
Creates and returns a session created from the data. 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)
data - a euphoria SessionView: http://api.euphoria.io/#sessionview self._callbacks = Callbacks()
""" self.subscribe("enter", self._on_enter)
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
#self.my_session = SessionView(None, None, None, None, None)
#self.sessions = {} # sessions in the room
#self.room_is_private = None
#self.server_version = None
return self( self._reset_variables()
data["id"],
data["name"],
data["server_id"],
data["server_era"],
data["session_id"],
is_staff,
is_manager
)
def session_type(self): def _reset_variables(self):
""" logger.debug("Resetting room-related variables")
session_type() -> str self._room_accessible = False
The session's type (bot, account, agent). self.my_session = SessionView(None, None, None, None, None)
""" self.sessions = {}
return self.id.split(":")[0] self._hello_event_completed = False
self._snapshot_event_completed = False
self._ready = False
def is_staff(self): self.room_is_private = None
""" self.server_version = None
is_staff() -> bool
Is a user staff? def _set_name(self, new_name):
""" with self._connection as conn:
logger.debug("Setting name to {!r}".format(new_name))
conn.subscribe_to_next(self._handle_nick_reply)
conn.send_packet("nick", name=new_name)
return self.staff and True or False def _on_enter(self):
logger.info("Connected and authenticated.")
self._room_accessible_timeout.cancel()
self._room_accessible = True
self._room_accessible_event.set()
self._room_accessible_event.clear()
def is_manager(self): if self.real_name:
""" self._set_name(self.real_name)
is_manager() -> bool
Is a user manager? def launch(self, timeout=10):
""" logger.info("Launching session &{}.".format(room))
return self.staff and True or False self._room_accessible_timeout.start()
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"))

View file

@ -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
View 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