Start rewrite

This commit is contained in:
Joscha 2018-07-25 16:02:38 +00:00
parent 04f4c3a8b6
commit 6b65bef5e0
5 changed files with 354 additions and 776 deletions

View file

@ -1,30 +1,21 @@
# ---------- BEGIN DEV SECTION ----------
import asyncio import asyncio
#asyncio.get_event_loop().set_debug(True) # uncomment for asycio debugging mode
import logging import logging
# general (asyncio) logging level # asyncio debugging
#logging.basicConfig(level=logging.DEBUG) asyncio.get_event_loop().set_debug(True) # uncomment for asycio debugging mode
#logging.basicConfig(level=logging.INFO) logging.getLogger("asyncio").setLevel(logging.DEBUG)
logging.basicConfig(level=logging.WARNING)
# yaboli logger level # yaboli logger level
logger = logging.getLogger(__name__) logging.getLogger(__name__).setLevel(logging.DEBUG)
#logger.setLevel(logging.DEBUG) # ----------- END DEV SECTION -----------
logger.setLevel(logging.INFO)
from .bot import * from .cookiejar import *
from .connection import * from .connection import *
from .controller import * from .exceptions import *
from .database import *
from .room import *
from .utils import *
__all__ = ( __all__ = (
bot.__all__ +
connection.__all__ + connection.__all__ +
controller.__all__ + cookiejar.__all__ +
database.__all__ + exceptions.__all__
room.__all__ +
utils.__all__
) )

View file

@ -4,120 +4,38 @@ import logging
import socket import socket
import websockets import websockets
from .exceptions import ConnectionClosed
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
__all__ = ["Connection"] __all__ = ["Connection"]
class Connection: class Connection:
def __init__(self, url, packet_hook, cookie=None, ping_timeout=10, ping_delay=30): def __init__(self, url, packet_callback, disconnect_callback, cookiejar=None, ping_timeout=10, ping_delay=30, reconnect_attempts=10):
self.url = url self.url = url
self.packet_hook = packet_hook self.packet_callback = packet_callback
self.cookie = cookie self.disconnect_callback = disconnect_callback
self.cookiejar = cookiejar
self.ping_timeout = ping_timeout # how long to wait for websocket ping reply self.ping_timeout = ping_timeout # how long to wait for websocket ping reply
self.ping_delay = ping_delay # how long to wait between pings self.ping_delay = ping_delay # how long to wait between pings
self.reconnect_attempts = reconnect_attempts
self._ws = None self._ws = None
self._pid = 0 # successive packet ids self._pid = 0 # successive packet ids
self._spawned_tasks = set() #self._spawned_tasks = set()
self._pending_responses = {} self._pending_responses = {}
self._runtask = None
self._pingtask = None # pings
async def connect(self, max_tries=10, delay=60):
"""
success = await connect(max_tries=10, delay=60)
Attempt to connect to a room.
Returns the task listening for packets, or None if the attempt failed.
max_tries - maximum number of reconnect attempts before stopping
delay - time (in seconds) between reconnect attempts
"""
logger.debug(f"Attempting to connect, max_tries={max_tries}")
await self.stop()
logger.debug(f"Stopped previously running things.")
for tries_left in reversed(range(max_tries)):
logger.info(f"Attempting to connect, {tries_left} tries left.")
try: self._stopped = False
self._ws = await websockets.connect(self.url, max_size=None) self._pingtask = None
except (websockets.InvalidURI, websockets.InvalidHandshake, socket.gaierror): self._runtask = asyncio.create_task(self._run())
self._ws = None # ... aaand the connection is started.
if tries_left > 0:
await asyncio.sleep(delay)
else:
self._runtask = asyncio.ensure_future(self._run())
self._pingtask = asyncio.ensure_future(self._ping())
logger.debug(f"Started run and ping tasks")
return self._runtask
async def _run(self):
"""
Listen for packets and deal with them accordingly.
"""
try:
while True:
await self._handle_next_message()
except websockets.ConnectionClosed:
pass
finally:
self._clean_up_futures()
self._clean_up_tasks()
try:
await self._ws.close() # just to make sure
except:
pass # errors are not useful here
self._pingtask.cancel()
await self._pingtask # should stop now that the ws is closed
self._ws = None
async def _ping(self):
"""
Periodically ping the server to detect a timeout.
"""
while True:
try:
logger.debug("Pinging...")
wait_for_reply = await self._ws.ping()
await asyncio.wait_for(wait_for_reply, self.ping_timeout)
logger.debug("Pinged!")
await asyncio.sleep(self.ping_delay)
except asyncio.TimeoutError:
logger.warning("Ping timed out.")
await self._ws.close()
break
except (websockets.ConnectionClosed, ConnectionResetError, asyncio.CancelledError):
return
async def stop(self):
"""
Close websocket connection and wait for running task to stop.
"""
if self._ws:
try:
await self._ws.close()
except:
pass # errors not useful here
if self._runtask:
await self._runtask
async def send(self, ptype, data=None, await_response=True): async def send(self, ptype, data=None, await_response=True):
if not self._ws: if not self._ws:
raise asyncio.CancelledError raise exceptions.ConnectionClosed
#raise asyncio.CancelledError
pid = str(self._new_pid()) pid = str(self._new_pid())
packet = { packet = {
"type": ptype, "type": ptype,
@ -125,62 +43,157 @@ class Connection:
} }
if data: if data:
packet["data"] = data packet["data"] = data
if await_response: if await_response:
wait_for = self._wait_for_response(pid) wait_for = self._wait_for_response(pid)
logging.debug(f"Currently used websocket at self._ws: {self._ws}") logging.debug(f"Currently used websocket at self._ws: {self._ws}")
await self._ws.send(json.dumps(packet, separators=(',', ':'))) # minimum size await self._ws.send(json.dumps(packet, separators=(',', ':'))) # minimum size
if await_response: if await_response:
await wait_for await wait_for
return wait_for.result() return wait_for.result()
async def stop(self):
"""
Close websocket connection and wait for running task to stop.
No connection function are to be called after calling stop().
This means that stop() can only be called once.
"""
self._stopped = True
if self._ws:
await self._ws.close() # _run() does the cleaning up now.
await self._runtask
async def _connect(self, tries):
"""
Attempt to connect to a room.
If the Connection is already connected, it attempts to reconnect.
Returns True on success, False on failure.
If tries is None, connect retries infinitely.
The delay between connection attempts doubles every attempt (starts with 1s).
"""
# Assumes _disconnect() has already been called in _run()
delay = 1 # seconds
while True:
try:
if self._cookiejar:
cookies = [("Cookie", cookie) for cookie in self._cookiejar.sniff()]
self._ws = await websockets.connect(self.url, max_size=None, extra_headers=cookies)
else:
self._ws = await websockets.connect(self.url, max_size=None)
except (websockets.InvalidHandshake, socket.gaierror): # not websockets.InvalidURI
self._ws = None
if tries is not None:
tries -= 1
if tries <= 0:
return False
await asyncio.sleep(delay)
delay *= 2
else:
if self._cookiejar:
for set_cookie in self._ws.response_headers.get_all("Set-Cookie"):
self._cookiejar.bake(set_cookie)
self._pingtask = asyncio.create_task(self._ping())
return True
async def _disconnect(self):
"""
Disconnect and clean up all "residue", such as:
- close existing websocket connection
- cancel all pending response futures with a ConnectionClosed exception
- reset package ID counter
- make sure the ping task has finished
"""
# stop ping task
if self._pingtask:
self._pingtask.cancel()
await self._pingtask
self._pingtask = None
if self._ws:
await self._ws.close()
self._ws = None
self._pid = 0
# clean up pending response futures
for _, future in self._pending_responses.items():
logger.debug(f"Cancelling future with ConnectionClosed: {future}")
future.set_exception(exceptions.ConnectionClosed("No server response"))
self._pending_responses = {}
async def _run(self):
"""
Listen for packets and deal with them accordingly.
"""
while not self._stopped:
self._connect(self.reconnect_attempts)
try:
while True:
await self._handle_next_message()
except websockets.ConnectionClosed:
pass
finally:
await self._disconnect() # disconnect and clean up
async def _ping(self):
"""
Periodically ping the server to detect a timeout.
"""
try:
while True:
logger.debug("Pinging...")
wait_for_reply = await self._ws.ping()
await asyncio.wait_for(wait_for_reply, self.ping_timeout)
logger.debug("Pinged!")
await asyncio.sleep(self.ping_delay)
except asyncio.TimeoutError:
logger.warning("Ping timed out.")
await self._ws.close() # trigger a reconnect attempt
except (websockets.ConnectionClosed, ConnectionResetError, asyncio.CancelledError):
pass
def _new_pid(self): def _new_pid(self):
self._pid += 1 self._pid += 1
return self._pid return self._pid
async def _handle_next_message(self): async def _handle_next_message(self):
response = await self._ws.recv() response = await self._ws.recv()
task = asyncio.ensure_future(self._handle_json(response))
self._track_task(task) # will be cancelled when the connection is closed
def _clean_up_futures(self):
for pid, future in self._pending_responses.items():
logger.debug(f"Cancelling future: {future}")
future.cancel()
self._pending_responses = {}
def _clean_up_tasks(self):
for task in self._spawned_tasks:
if not task.done():
logger.debug(f"Cancelling task: {task}")
task.cancel()
else:
logger.debug(f"Task already done: {task}")
logger.debug(f"Exception: {task.exception()}")
self._spawned_tasks = set()
async def _handle_json(self, text):
packet = json.loads(text) packet = json.loads(text)
# Deal with pending responses # Deal with pending responses
pid = packet.get("id", None) pid = packet.get("id", None)
future = self._pending_responses.pop(pid, None) future = self._pending_responses.pop(pid, None)
if future: if future:
future.set_result(packet) future.set_result(packet)
ptype = packet.get("type")
data = packet.get("data", None)
error = packet.get("error", None)
if packet.get("throttled", False):
throttled = packet.get("throttled_reason")
else:
throttled = None
# Pass packet onto room # Pass packet onto room
await self.packet_hook(packet) asyncio.create_task(self.packet_callback(ptype, data, error, throttled))
def _track_task(self, task):
self._spawned_tasks.add(task)
# only keep running tasks
self._spawned_tasks = {task for task in self._spawned_tasks if not task.done()}
def _wait_for_response(self, pid): def _wait_for_response(self, pid):
future = asyncio.Future() future = asyncio.Future()
self._pending_responses[pid] = future self._pending_responses[pid] = future
return future return future

65
yaboli/cookiejar.py Normal file
View file

@ -0,0 +1,65 @@
import contextlib
import http.cookies as cookies
import logging
logger = logging.getLogger(__name__)
__all__ = ["CookieJar"]
class CookieJar:
"""
Keeps your cookies in a file.
"""
def __init__(self, filename):
self._filename = filename
self._cookies = cookies.SimpleCookie()
with contextlib.suppress(FileNotFoundError):
with open(self._filename, "r") as f:
for line in f:
self._cookies.load(line)
def sniff(self):
"""
Returns a list of Cookie headers containing all current cookies.
"""
return [morsel.OutputString(attrs=[]) for morsel in self._cookies.values()]
def bake(self, cookie_string):
"""
Parse cookie and add it to the jar.
Does not automatically save to the cookie file.
Example cookie: "a=bcd; Path=/; Expires=Wed, 24 Jul 2019 14:57:52 GMT; HttpOnly; Secure"
"""
logger.debug(f"Baking cookie: {cookie_string!r}")
self._cookies.load(cookie_string)
def save(self):
"""
Saves all current cookies to the cookie jar file.
"""
logger.debug(f"Saving cookies to {self._filename!r}")
with open(self._filename, "w") as f:
for morsel in self._cookies.values():
cookie_string = morsel.OutputString()
#f.write(f"{cookie_string}\n")
f.write(cookie_string)
f.write("\n")
def monster(self):
"""
Removes all cookies from the cookie jar.
Does not automatically save to the cookie file.
"""
logger.debug("OMNOMNOM, cookies are all gone!")
self._cookies = cookies.SimpleCookie()

4
yaboli/exceptions.py Normal file
View file

@ -0,0 +1,4 @@
__all__ = ["ConnectionClosed"]
class ConnectionClosed(Exception):
pass

View file

@ -1,637 +1,142 @@
import asyncio __all__ == ["Room", "Inhabitant"]
import logging
from .callbacks import *
from .connection import *
from .utils import *
logger = logging.getLogger(__name__)
__all__ = ["Room"]
class Room: class Room:
""" """
This class represents a connection to a room. This basically means that one TODO
room instance means one nick on the nick list.
It's purpose is to provide a higher-level way of interacting with a room to
a controller. This includes converting packets received from the server to
utility classes where possible, or keeping track of current room state like
the client's nick.
It does not keep track of the room's messages, as keeping (or not keeping)
messages is highly application-dependent. If needed, messages can be kept
using the utils.Log class.
Room implements all commands necessary for creating bots. For now, the
human flag should always be False, and the cookie None.
It also "attaches" to a controller and calls the corresponding functions
when it receives events from the server
When connection is lost while the room is running, it will attempt to
reconnect a few times. Loss of connection is determined by self._conn.
""" """
ROOM_FORMAT = "wss://euphoria.io/room/{}/ws" def __init__(self, roomname, inhabitant, password=None, human=False, cookiejar=None):
HUMAN_FORMAT = f"{ROOM_FORMAT}?h=1" # TODO: Connect to room etc.
# TODO: Deal with room/connection states of:
def __init__(self, roomname, controller, human=False, cookie=None): # disconnected connecting, fast-forwarding, connected
"""
Create a room. To connect to the room and start a run task that listens self._inhabitant = inhabitant
to packets on the connection, use connect().
# Room info (all fields readonly!)
roomname - name of the room to connect to, without a "&" in front
controller - the controller which should be notified of events
human - currently not implemented, should be False
cookie - currently not implemented, should be None
"""
self.roomname = roomname self.roomname = roomname
self.controller = controller
self.human = human
self.cookie = cookie
# Keeps track of sessions, but not messages, since they might be dealt
# with differently by different controllers.
# If you need to keep track of messages, use utils.Log.
self.session = None self.session = None
self.account = None self.account = None
self.listing = Listing() self.listing = None # TODO
# Various room information
self.account_has_access = None self.account_has_access = None
self.account_email_verified = None self.account_email_verified = None
self.room_is_private = None self.room_is_private = None
self.version = None # the version of the code being run and served by the server self.version = None # the version of the code being run and served by the server
self.pm_with_nick = None self.pm_with_nick = None
self.pm_with_user_id = None self.pm_with_user_id = None
self._callbacks = Callbacks() #asyncio.create_task(self._run())
self._add_callbacks()
async def exit(self):
self._stopping = False pass
self._runtask = None
# ROOM COMMANDS
if human: # These always return a response from the server.
url = self.HUMAN_FORMAT.format(self.roomname) # If the connection is lost while one of these commands is called,
# the command will retry once the bot has reconnected.
async def get_message(self, mid):
pass
async def log(self, n, before_mid=None):
pass
async def nick(self, nick):
pass
async def pm(self, uid):
pass
async def send(self, content, parent_mid=None):
"""
Send a message to the room.
See http://api.euphoria.io/#send
"""
if parent_mid:
data = await self._send_while_connected(
"send",
content=content,
parent=parent_mid
)
else: else:
url = self.ROOM_FORMAT.format(self.roomname) data = await self._send_while_connected(
self._conn = Connection(url, self._handle_packet, self.cookie) "send",
content=content
async def connect(self, max_tries=10, delay=60): )
"""
runtask = await connect(max_tries, delay) return Message.from_dict(data)
Attempts to connect to the room once and returns a task running
self._run, if successful, otherwise None. This can be used to detect if
a room exists.
The max_tries and delay parameters are passed on to self._run:
max_tries - maximum number of reconnect attempts before stopping
delay - time (in seconds) between reconnect attempts
"""
task = await self._conn.connect(max_tries=1)
if task:
self._runtask = asyncio.ensure_future(self._run(task, max_tries=max_tries, delay=delay))
return self._runtask
async def _run(self, task, max_tries=10, delay=60):
"""
await _run(max_tries, delay)
Run and reconnect when the connection is lost or closed, unless
self._stopping is set to True.
For an explanation of the parameters, see self.connect.
"""
while not self._stopping:
if task.done():
task = await self._conn.connect(max_tries=max_tries, delay=delay)
if not task:
return
await task
await self.controller.on_disconnected()
self.stopping = False
async def stop(self):
"""
await stop()
Close the connection to the room without reconnecting.
"""
self._stopping = True
await self._conn.stop()
if self._runtask:
await self._runtask
# CATEGORY: SESSION COMMANDS
async def auth(self, atype, passcode=None):
"""
success, reason=None = await auth(atype, passcode=None)
From api.euphoria.io:
The auth command attempts to join a private room. It should be sent in
response to a bounce-event at the beginning of a session.
The auth-reply packet reports whether the auth command succeeded.
"""
data = {"type": atype}
if passcode:
data["passcode"] = passcode
response = await self._send_packet("auth", data)
rdata = response.get("data")
success = rdata.get("success")
reason = rdata.get("reason", None)
return success, reason
async def ping_reply(self, time):
"""
await ping_reply(time)
From api.euphoria.io:
The ping command initiates a client-to-server ping. The server will
send back a ping-reply with the same timestamp as soon as possible.
ping-reply is a response to a ping command or ping-event.
"""
data = {"time": time}
await self._conn.send("ping-reply", data, await_response=False)
# CATEGORY: CHAT ROOM COMMANDS
async def get_message(self, message_id):
"""
message = await get_message(message_id)
From api.euphoria.io:
The get-message command retrieves the full content of a single message
in the room.
get-message-reply returns the message retrieved by get-message.
"""
data = {"id": message_id}
response = await self._send_packet("get-message", data)
rdata = response.get("data")
message = Message.from_dict(rdata)
return message
async def log(self, n, before=None):
"""
log, before=None = await log(n, before=None)
From api.euphoria.io:
The log command requests messages from the rooms message log. This can
be used to supplement the log provided by snapshot-event (for example,
when scrolling back further in history).
The log-reply packet returns a list of messages from the rooms message
"""
data = {"n": n}
if before:
data["before"] = before
response = await self._send_packet("log", data)
rdata = response.get("data")
messages = [Message.from_dict(d) for d in rdata.get("log")]
before = rdata.get("before", None)
return messages, before
async def nick(self, name):
"""
session_id, user_id, from_nick, to_nick = await nick(name)
From api.euphoria.io:
The nick command sets the name you present to the room. This name
applies to all messages sent during this session, until the nick
command is called again.
nick-reply confirms the nick command. It returns the sessions former
and new names (the server may modify the requested nick).
"""
data = {"name": name}
response = await self._send_packet("nick", data)
rdata = response.get("data")
session_id = rdata.get("session_id")
user_id = rdata.get("id")
from_nick = rdata.get("from")
to_nick = rdata.get("to")
# update self.session
self.session.nick = to_nick
return session_id, user_id, from_nick, to_nick
async def pm_initiate(self, user_id):
"""
pm_id, to_nick = await pm_initiate(user_id)
From api.euphoria.io:
The pm-initiate command constructs a virtual room for private messaging
between the client and the given UserID.
The pm-initiate-reply provides the PMID for the requested private
messaging room.
"""
data = {"user_id": user_id}
response = await self._send_packet("pm-initiate", data)
rdata = response.get("data")
pm_id = rdata.get("pm_id")
to_nick = rdata.get("to_nick")
return pm_id, to_nick
async def send(self, content, parent=None):
"""
message = await send(content, parent=None)
From api.euphoria.io:
The send command sends a message to a room. The session must be
successfully joined with the room. This message will be broadcast to
all sessions joined with the room.
If the room is private, then the message content will be encrypted
before it is stored and broadcast to the rest of the room.
The caller of this command will not receive the corresponding
send-event, but will receive the same information in the send-reply.
"""
data = {"content": content}
if parent:
data["parent"] = parent
response = await self._send_packet("send", data)
rdata = response.get("data")
message = Message.from_dict(rdata)
return message
async def who(self): async def who(self):
""" pass
sessions = await who()
# COMMUNICATION WITH CONNECTION
From api.euphoria.io:
The who command requests a list of sessions currently joined in the async def _receive_packet(self, ptype, data, error, throttled):
room. pass # TODO
The who-reply packet lists the sessions currently joined in the room. async def _disconnected(self):
""" pass # TODO
response = await self._send_packet("who") # SOME USEFUL PUBLIC METHODS
rdata = response.get("data")
@staticmethod
sessions = [Session.from_dict(d) for d in rdata.get("listing")] def format_room_url(roomname, private=False, human=False):
if private:
# update self.listing roomname = f"pm:{roomname}"
self.listing = Listing()
for session in sessions: url = f"wss://euphoria.io/room/{roomname}/ws"
if not session.sid == self.session.sid:
self.listing.add(session) if human:
url = f"{url}?h=1"
return sessions
return url
# CATEGORY: ACCOUNT COMMANDS
# NYI, and probably never will async def connected(self):
pass
# CATEGORY: ROOM HOST COMMANDS
# NYI, and probably never will # REST OF THE IMPLEMENTATION
# CATEGORY: STAFF COMMANDS async def _run(self):
# NYI, and probably never will pass
async def _send_while_connected(*args, **kwargs):
while True:
# All the private functions for dealing with stuff try:
await self.connected()
def _add_callbacks(self): if not self._status != Room._CONNECTED: continue # TODO: Figure out a good solution
""" return await self._connection.send(*args, **kwargs)
_add_callbacks() except RoomDisconnected:
pass # Just try again
Adds the functions that handle server events to the callbacks for that
event.
""" class Inhabitant:
"""
self._callbacks.add("bounce-event", self._handle_bounce) TODO
self._callbacks.add("disconnect-event", self._handle_disconnect) """
self._callbacks.add("hello-event", self._handle_hello)
self._callbacks.add("join-event", self._handle_join) # ROOM EVENTS
self._callbacks.add("login-event", self._handle_login) # These functions are called by the room when something happens.
self._callbacks.add("logout-event", self._handle_logout) # They're launched via asyncio.create_task(), so they don't block execution of the room.
self._callbacks.add("network-event", self._handle_network) # Just overwrite the events you need (make sure to keep the arguments the same though).
self._callbacks.add("nick-event", self._handle_nick)
self._callbacks.add("edit-message-event", self._handle_edit_message) async def disconnected(self, room):
self._callbacks.add("part-event", self._handle_part) pass
self._callbacks.add("ping-event", self._handle_ping)
self._callbacks.add("pm-initiate-event", self._handle_pm_initiate) async def connected(self, room, log):
self._callbacks.add("send-event", self._handle_send) pass
self._callbacks.add("snapshot-event", self._handle_snapshot)
async def join(self, room, session):
async def _send_packet(self, *args, **kwargs): pass
"""
reply_packet = await _send_packet(*args, **kwargs) async def part(self, room, session):
pass
Like self._conn.send, but checks for an error on the packet and raises
the corresponding exception. async def nick(self, room, sid, uid, from_nick, to_nick):
""" pass
response = await self._conn.send(*args, **kwargs) async def send(self, room, message):
self._check_for_errors(response) pass
return response async def pm(self, room, from_uid, from_nick, from_room, pm_id):
pass
async def _handle_packet(self, packet):
"""
await _handle_packet(packet)
Call the correct callbacks to deal with packet.
This function catches CancelledErrors and instead displays an info so
the console doesn't show stack traces when a bot loses connection.
"""
self._check_for_errors(packet)
ptype = packet.get("type")
try:
await self._callbacks.call(ptype, packet)
except asyncio.CancelledError as e:
logger.info(f"&{self.roomname}: Callback of type {ptype!r} cancelled.")
#raise # not necessary?
def _check_for_errors(self, packet):
"""
_check_for_errors(packet)
Checks for an error on the packet and raises the corresponding
exception.
"""
if packet.get("throttled", False):
logger.warn(f"&{self.roomname}: Throttled for reason: {packet.get('throttled_reason', 'no reason')!r}")
if "error" in packet:
raise ResponseError(packet.get("error"))
async def _handle_bounce(self, packet):
"""
From api.euphoria.io:
A bounce-event indicates that access to a room is denied.
"""
data = packet.get("data")
await self.controller.on_bounce(
reason=data.get("reason", None),
auth_options=data.get("auth_options", None),
agent_id=data.get("agent_id", None),
ip=data.get("ip", None)
)
async def _handle_disconnect(self, packet):
"""
From api.euphoria.io:
A disconnect-event indicates that the session is being closed. The
client will subsequently be disconnected.
If the disconnect reason is authentication changed, the client should
immediately reconnect.
"""
data = packet.get("data")
await self.controller.on_disconnect(data.get("reason"))
async def _handle_hello(self, packet):
"""
From api.euphoria.io:
A hello-event is sent by the server to the client when a session is
started. It includes information about the clients authentication and
associated identity.
"""
data = packet.get("data")
self.session = Session.from_dict(data.get("session"))
self.room_is_private = data.get("room_is_private")
self.version = data.get("version")
self.account = data.get("account", None)
self.account_has_access = data.get("account_has_access", None)
self.account_email_verified = data.get("account_email_verified", None)
await self.controller.on_hello(
data.get("id"),
self.session,
self.room_is_private,
self.version,
account=self.account,
account_has_access=self.account_has_access,
account_email_verified=self.account_email_verified
)
async def _handle_join(self, packet):
"""
From api.euphoria.io:
A join-event indicates a session just joined the room.
"""
data = packet.get("data")
session = Session.from_dict(data)
# update self.listing
self.listing.add(session)
await self.controller.on_join(session)
async def _handle_login(self, packet):
"""
From api.euphoria.io:
The login-event packet is sent to all sessions of an agent when that
agent is logged in (except for the session that issued the login
command).
"""
data = packet.get("data")
await self.controller.on_login(data.get("account_id"))
async def _handle_logout(self, packet):
"""
From api.euphoria.io:
The logout-event packet is sent to all sessions of an agent when that
agent is logged out (except for the session that issued the logout
command).
"""
await self.controller.on_logout()
async def _handle_network(self, packet):
"""
From api.euphoria.io:
A network-event indicates some server-side event that impacts the
presence of sessions in a room.
If the network event type is partition, then this should be treated as
a part-event for all sessions connected to the same server id/era
combo.
"""
data = packet.get("data")
server_id = data.get("server_id")
server_era = data.get("server_era")
# update self.listing
self.listing.remove_combo(server_id, server_era)
await self.controller.on_network(server_id, server_era)
async def _handle_nick(self, packet):
"""
From api.euphoria.io:
nick-event announces a nick change by another session in the room.
"""
data = packet.get("data")
session_id = data.get("session_id")
to_nick = data.get("to")
# update self.listing
session = self.listing.by_sid(session_id)
if session:
session.nick = to_nick
await self.controller.on_nick(
session_id,
data.get("id"),
data.get("from"),
to_nick
)
async def _handle_edit_message(self, packet):
"""
From api.euphoria.io:
An edit-message-event indicates that a message in the room has been
modified or deleted. If the client offers a user interface and the
indicated message is currently displayed, it should update its display
accordingly.
The event packet includes a snapshot of the message post-edit.
"""
data = packet.get("data")
message = Message.from_dict(data)
await self.controller.on_edit_message(
data.get("edit_id"),
message
)
async def _handle_part(self, packet):
"""
From api.euphoria.io:
A part-event indicates a session just disconnected from the room.
"""
data = packet.get("data")
session = Session.from_dict(data)
# update self.listing
self.listing.remove(session.session_id)
await self.controller.on_part(session)
async def _handle_ping(self, packet):
"""
From api.euphoria.io:
A ping-event represents a server-to-client ping. The client should send
back a ping-reply with the same value for the time field as soon as
possible (or risk disconnection).
"""
data = packet.get("data")
await self.controller.on_ping(
data.get("time"),
data.get("next")
)
async def _handle_pm_initiate(self, packet):
"""
From api.euphoria.io:
The pm-initiate-event informs the client that another user wants to
chat with them privately.
"""
data = packet.get("data")
await self.controller.on_pm_initiate(
data.get("from"),
data.get("from_nick"),
data.get("from_room"),
data.get("pm_id")
)
async def _handle_send(self, packet):
"""
From api.euphoria.io:
A send-event indicates a message received by the room from another
session.
"""
data = packet.get("data")
message = Message.from_dict(data)
await self.controller.on_send(message)
async def _handle_snapshot(self, packet):
"""
From api.euphoria.io:
A snapshot-event indicates that a session has successfully joined a
room. It also offers a snapshot of the rooms state and recent history.
"""
data = packet.get("data")
sessions = [Session.from_dict(d) for d in data.get("listing")]
messages = [Message.from_dict(d) for d in data.get("log")]
# update self.listing
for session in sessions:
self.listing.add(session)
self.session.nick = data.get("nick", None)
self.pm_with_nick = data.get("pm_with_nick", None),
self.pm_with_user_id = data.get("pm_with_user_id", None)
await self.controller.on_connected()
await self.controller.on_snapshot(
data.get("identity"),
data.get("session_id"),
self.version,
sessions, # listing
messages, # log
nick=self.session.nick,
pm_with_nick=self.pm_with_nick,
pm_with_user_id=self.pm_with_user_id
)