Implement Room
First, the Room itself.
This commit is contained in:
parent
20f635a7ae
commit
40edcdc791
9 changed files with 554 additions and 291 deletions
36
test.py
36
test.py
|
|
@ -4,7 +4,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from yaboli import Connection
|
from yaboli import Room
|
||||||
|
|
||||||
FORMAT = "{asctime} [{levelname:<7}] <{name}> {funcName}(): {message}"
|
FORMAT = "{asctime} [{levelname:<7}] <{name}> {funcName}(): {message}"
|
||||||
DATE_FORMAT = "%F %T"
|
DATE_FORMAT = "%F %T"
|
||||||
|
|
@ -19,29 +19,17 @@ logger = logging.getLogger('yaboli')
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
class TestClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.room = Room("test", target_nick="testbot")
|
||||||
|
self.stop = asyncio.Event()
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
await self.room.connect()
|
||||||
|
await self.stop.wait()
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
conn = Connection("wss://euphoria.io/room/test/ws")
|
tc = TestClient()
|
||||||
#conn = Connection("wss://euphoria.io/room/cabal/ws") # password protected
|
await tc.run()
|
||||||
|
|
||||||
print()
|
|
||||||
print(" DISCONNECTING TWICE AT THE SAME TIME")
|
|
||||||
print("Connected successfully:", await conn.connect())
|
|
||||||
a = asyncio.create_task(conn.disconnect())
|
|
||||||
b = asyncio.create_task(conn.disconnect())
|
|
||||||
await a
|
|
||||||
await b
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" DISCONNECTING WHILE CONNECTING (test not working properly)")
|
|
||||||
asyncio.create_task(conn.disconnect())
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
print("Connected successfully:", await conn.connect())
|
|
||||||
await conn.disconnect()
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(" WAITING FOR PING TIMEOUT")
|
|
||||||
print("Connected successfully:", await conn.connect())
|
|
||||||
await asyncio.sleep(conn.PING_TIMEOUT + 10)
|
|
||||||
await conn.disconnect()
|
|
||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from .events import *
|
||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
from .message import *
|
from .message import *
|
||||||
from .room import *
|
from .room import *
|
||||||
from .user import *
|
from .session import *
|
||||||
from .util import *
|
from .util import *
|
||||||
|
|
||||||
__all__: List[str] = []
|
__all__: List[str] = []
|
||||||
|
|
@ -16,4 +16,5 @@ __all__ += events.__all__
|
||||||
__all__ += exceptions.__all__
|
__all__ += exceptions.__all__
|
||||||
__all__ += message.__all__
|
__all__ += message.__all__
|
||||||
__all__ += room.__all__
|
__all__ += room.__all__
|
||||||
__all__ += user.__all__
|
__all__ += session.__all__
|
||||||
|
__all__ += util.__all__
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from .message import Message
|
|
||||||
from .room import Room
|
from .room import Room
|
||||||
from .user import User
|
|
||||||
|
|
||||||
__all__ = ["Client"]
|
__all__ = ["Client"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
__all__ = ["Connection"]
|
__all__ = ["Connection"]
|
||||||
|
|
||||||
|
# This class could probably be cleaned up by introducing one or two well-placed
|
||||||
|
# Locks – something for the next rewrite :P
|
||||||
|
|
||||||
class Connection:
|
class Connection:
|
||||||
"""
|
"""
|
||||||
The Connection handles the lower-level stuff required when connecting to
|
The Connection handles the lower-level stuff required when connecting to
|
||||||
|
|
@ -447,6 +450,7 @@ class Connection:
|
||||||
|
|
||||||
# Finally, reset the ping check
|
# Finally, reset the ping check
|
||||||
logger.debug("Resetting ping check")
|
logger.debug("Resetting ping check")
|
||||||
|
if self._ping_check is not None:
|
||||||
self._ping_check.cancel()
|
self._ping_check.cancel()
|
||||||
self._ping_check = asyncio.create_task(
|
self._ping_check = asyncio.create_task(
|
||||||
self._disconnect_in(self.PING_TIMEOUT))
|
self._disconnect_in(self.PING_TIMEOUT))
|
||||||
|
|
@ -473,7 +477,7 @@ class Connection:
|
||||||
except IncorrectStateException:
|
except IncorrectStateException:
|
||||||
logger.debug("Could not send (disconnecting or already disconnected)")
|
logger.debug("Could not send (disconnecting or already disconnected)")
|
||||||
|
|
||||||
async def _ping_pong(self, packet):
|
async def _ping_pong(self, packet: Any) -> None:
|
||||||
"""
|
"""
|
||||||
Implements http://api.euphoria.io/#ping and is called as "ping-event"
|
Implements http://api.euphoria.io/#ping and is called as "ping-event"
|
||||||
callback.
|
callback.
|
||||||
|
|
@ -481,7 +485,7 @@ class Connection:
|
||||||
logger.debug("Pong!")
|
logger.debug("Pong!")
|
||||||
await self._do_if_possible(self.send(
|
await self._do_if_possible(self.send(
|
||||||
"ping-reply",
|
"ping-reply",
|
||||||
{"time": packet["data"]["time"]}
|
{"time": packet["data"]["time"]},
|
||||||
await_reply=False
|
await_reply=False
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
@ -489,7 +493,7 @@ class Connection:
|
||||||
packet_type: str,
|
packet_type: str,
|
||||||
data: Any,
|
data: Any,
|
||||||
await_reply: bool = True
|
await_reply: bool = True
|
||||||
) -> Optional[Any]:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
Send a packet of type packet_type to the server.
|
Send a packet of type packet_type to the server.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ __all__ = [
|
||||||
"CouldNotConnectException",
|
"CouldNotConnectException",
|
||||||
"CouldNotAuthenticateException",
|
"CouldNotAuthenticateException",
|
||||||
# Doing stuff in a room
|
# Doing stuff in a room
|
||||||
|
"RoomNotConnectedException",
|
||||||
|
"EuphError",
|
||||||
"RoomClosedException",
|
"RoomClosedException",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -52,6 +54,21 @@ class CouldNotAuthenticateException(JoinException):
|
||||||
|
|
||||||
# Doing stuff in a room
|
# Doing stuff in a room
|
||||||
|
|
||||||
|
class RoomNotConnectedException(EuphException):
|
||||||
|
"""
|
||||||
|
Either the Room's connect() function has not been called or it has not
|
||||||
|
completed successfully.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class EuphError(EuphException):
|
||||||
|
"""
|
||||||
|
The euphoria server has sent back an "error" field in its response.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# TODO This exception is not used currently, decide on whether to keep it or
|
||||||
|
# throw it away
|
||||||
class RoomClosedException(EuphException):
|
class RoomClosedException(EuphException):
|
||||||
"""
|
"""
|
||||||
The room has been closed already.
|
The room has been closed already.
|
||||||
|
|
|
||||||
|
|
@ -1,108 +1,30 @@
|
||||||
import datetime
|
import datetime
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Any, Optional
|
||||||
|
|
||||||
from .user import LiveUser, User
|
from .session import LiveSession, Session
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .client import Client
|
|
||||||
from .room import Room
|
from .room import Room
|
||||||
|
|
||||||
__all__ = ["Message", "LiveMessage"]
|
__all__ = ["Message", "LiveMessage"]
|
||||||
|
|
||||||
# "Offline" message
|
|
||||||
class Message:
|
class Message:
|
||||||
def __init__(self,
|
pass
|
||||||
room_name: str,
|
# @property
|
||||||
id_: str,
|
# def room_name(self) -> str:
|
||||||
parent_id: Optional[str],
|
# return self._room_name
|
||||||
timestamp: int,
|
|
||||||
sender: User,
|
|
||||||
content: str,
|
|
||||||
deleted: bool,
|
|
||||||
truncated: bool):
|
|
||||||
self._room_name = room_name
|
|
||||||
self._id = id_
|
|
||||||
self._parent_id = parent_id
|
|
||||||
self._timestamp = timestamp
|
|
||||||
self._sender = sender
|
|
||||||
self._content = content
|
|
||||||
self._deleted = deleted
|
|
||||||
self._truncated = truncated
|
|
||||||
|
|
||||||
@property
|
|
||||||
def room_name(self) -> str:
|
|
||||||
return self._room_name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def id(self) -> str:
|
|
||||||
return self._id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def parent_id(self) -> Optional[str]:
|
|
||||||
return self._parent_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def time(self) -> datetime.datetime:
|
|
||||||
return datetime.datetime.fromtimestamp(self.timestamp)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def timestamp(self) -> int:
|
|
||||||
return self._timestamp
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sender(self) -> User:
|
|
||||||
return self._sender
|
|
||||||
|
|
||||||
@property
|
|
||||||
def content(self) -> str:
|
|
||||||
return self._content
|
|
||||||
|
|
||||||
@property
|
|
||||||
def deleted(self) -> bool:
|
|
||||||
return self._deleted
|
|
||||||
|
|
||||||
@property
|
|
||||||
def truncated(self) -> bool:
|
|
||||||
return self._truncated
|
|
||||||
|
|
||||||
# "Online" message
|
|
||||||
# has a few nice functions
|
|
||||||
class LiveMessage(Message):
|
|
||||||
def __init__(self,
|
|
||||||
client: 'Client',
|
|
||||||
room: 'Room',
|
|
||||||
id_: str,
|
|
||||||
parent_id: Optional[str],
|
|
||||||
timestamp: int,
|
|
||||||
sender: LiveUser,
|
|
||||||
content: str,
|
|
||||||
deleted: bool,
|
|
||||||
truncated: bool):
|
|
||||||
self._client = client
|
|
||||||
super().__init__(room.name, id_, parent_id, timestamp, sender, content,
|
|
||||||
deleted, truncated)
|
|
||||||
self._room = room
|
|
||||||
# The typechecker can't use self._sender directly, because it has type
|
|
||||||
# User.
|
|
||||||
#
|
#
|
||||||
# TODO Find a way to satisfy the type checker without having this
|
# @property
|
||||||
# duplicate around, if possible?
|
# def time(self) -> datetime.datetime:
|
||||||
self._livesender = sender
|
# return datetime.datetime.fromtimestamp(self.timestamp)
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def timestamp(self) -> int:
|
||||||
|
# return self._timestamp
|
||||||
|
|
||||||
@property
|
class LiveMessage(Message):
|
||||||
def room(self) -> 'Room':
|
|
||||||
return self._room
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sender(self) -> LiveUser:
|
|
||||||
return self._livesender
|
|
||||||
|
|
||||||
async def reply(self, text: str) -> None:
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TODO add some sort of permission guard that checks the room
|
@classmethod
|
||||||
# UnauthorizedException
|
def from_data(cls, room: "Room", data: Any) -> "LiveMessage":
|
||||||
async def delete(self,
|
|
||||||
deleted: bool = True
|
|
||||||
) -> None:
|
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
499
yaboli/room.py
499
yaboli/room.py
|
|
@ -1,107 +1,460 @@
|
||||||
from typing import List, Optional
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Any, Awaitable, Callable, List, Optional, TypeVar
|
||||||
|
|
||||||
|
from .connection import Connection
|
||||||
|
from .events import Events
|
||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
from .message import LiveMessage
|
from .message import LiveMessage
|
||||||
from .user import LiveUser
|
from .session import Account, LiveSession, LiveSessionListing
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
__all__ = ["Room"]
|
__all__ = ["Room"]
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class Room:
|
class Room:
|
||||||
"""
|
"""
|
||||||
A Room represents one connection to a room on euphoria, i. e. what other
|
Events and parameters:
|
||||||
implementations might consider a "client". This means that each Room has
|
|
||||||
its own session (User) and nick.
|
|
||||||
|
|
||||||
A Room can only be used once in the sense that after it has been closed,
|
"snapshot" - snapshot of the room's messages at the time of joining
|
||||||
any further actions will result in a RoomClosedException. If you need to
|
messages: List[LiveMessage]
|
||||||
manually reconnect, instead just create a new Room object.
|
|
||||||
|
|
||||||
|
"send" - another room member has sent a message
|
||||||
|
message: LiveMessage
|
||||||
|
|
||||||
|
"join" - somebody has joined the room
|
||||||
|
user: LiveUser
|
||||||
|
|
||||||
Life cycle of a Room
|
"part" - somebody has left the room
|
||||||
|
user: LiveUser
|
||||||
|
|
||||||
1. create a new Room and register callbacks
|
"nick" - another room member has changed their nick
|
||||||
2. await join()
|
user: LiveUser
|
||||||
3. do room-related stuff
|
from: str
|
||||||
4. await part()
|
to: str
|
||||||
|
|
||||||
|
"edit" - a message in the room has been modified or deleted
|
||||||
|
message: LiveMessage
|
||||||
|
|
||||||
|
"pm" - another session initiated a pm with you
|
||||||
|
from: str - the id of the user inviting the client to chat privately
|
||||||
|
from_nick: str - the nick of the inviting user
|
||||||
|
from_room: str - the room where the invitation was sent from
|
||||||
|
pm_id: str - the private chat can be accessed at /room/pm:PMID
|
||||||
|
|
||||||
IN PHASE 1, a password and a starting nick can be set. The password and
|
"disconect" - corresponds to http://api.euphoria.io/#disconnect-event (if
|
||||||
current nick are used when first connecting to the room, or when
|
the reason is "authentication changed", the room automatically reconnects)
|
||||||
reconnecting to the room after connection was lost.
|
reason: str - the reason for disconnection
|
||||||
|
|
||||||
Usually, event callbacks are also registered during this phase.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
IN PHASE 2, the Room creates the initial connection to euphoria and
|
|
||||||
performs initialisations (i. e. authentication or setting the nick) where
|
|
||||||
necessary. It also starts the Room's main event loop. The join() function
|
|
||||||
returns once one of the following cases has occurred:
|
|
||||||
|
|
||||||
1. the room is now in phase 3, in which case join() returns None
|
|
||||||
2. the room could not be joined, in which case one of the JoinExceptions is
|
|
||||||
returned
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
IN PHASE 3, the usual room-related functions like say() or nick() are
|
|
||||||
available. The Room's event loop is running.
|
|
||||||
|
|
||||||
The room will automatically reconnect if it loses connection to euphoria.
|
|
||||||
The usual room-related functions will block until the room has successfully
|
|
||||||
reconnected.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
IN PHASE 4, the Room is disconnected and the event loop stopped. During and
|
|
||||||
after completion of this phase, the Room is considered closed. Any further
|
|
||||||
attempts to re-join or call room action functions will result in a
|
|
||||||
RoomClosedException.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Phase 1
|
URL_FORMAT = "wss://euphoria.io/room/{}/ws"
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
room_name: str,
|
name: str,
|
||||||
nick: str = "",
|
password: Optional[str] = None,
|
||||||
password: Optional[str] = None) -> None:
|
target_nick: str = "",
|
||||||
pass
|
url_format: str = URL_FORMAT
|
||||||
|
) -> None:
|
||||||
|
self._name = name
|
||||||
|
self._password = password
|
||||||
|
self._target_nick = target_nick
|
||||||
|
self._url_format = url_format
|
||||||
|
|
||||||
self.closed = False
|
self._session: Optional[LiveSession] = None
|
||||||
|
self._account: Optional[Account] = None
|
||||||
|
self._private: Optional[bool] = None
|
||||||
|
self._version: Optional[str] = None
|
||||||
|
self._users: Optional[LiveSessionListing] = None
|
||||||
|
self._pm_with_nick: Optional[str] = None
|
||||||
|
self._pm_with_user_id: Optional[str] = None
|
||||||
|
self._server_version: Optional[str] = None
|
||||||
|
|
||||||
# Phase 2
|
# Connected management
|
||||||
|
self._url = self._url_format.format(self._name)
|
||||||
|
self._connection = Connection(self._url)
|
||||||
|
self._events = Events()
|
||||||
|
|
||||||
# Phase 3
|
self._connected = asyncio.Event()
|
||||||
|
self._connected_successfully = False
|
||||||
|
self._hello_received = False
|
||||||
|
self._snapshot_received = False
|
||||||
|
|
||||||
def _ensure_open(self) -> None:
|
self._connection.register_event("reconnecting", self._on_reconnecting)
|
||||||
if self.closed:
|
self._connection.register_event("hello-event", self._on_hello_event)
|
||||||
raise RoomClosedException()
|
self._connection.register_event("snapshot-event", self._on_snapshot_event)
|
||||||
|
self._connection.register_event("bounce-event", self._on_bounce_event)
|
||||||
|
|
||||||
async def _ensure_joined(self) -> None:
|
self._connection.register_event("disconnect-event", self._on_disconnect_event)
|
||||||
pass
|
self._connection.register_event("join-event", self._on_join_event)
|
||||||
|
self._connection.register_event("login-event", self._on_login_event)
|
||||||
|
self._connection.register_event("logout-event", self._on_logout_event)
|
||||||
|
self._connection.register_event("network-event", self._on_network_event)
|
||||||
|
self._connection.register_event("nick-event", self._on_nick_event)
|
||||||
|
self._connection.register_event("edit-message-event", self._on_edit_message_event)
|
||||||
|
self._connection.register_event("part-event", self._on_part_event)
|
||||||
|
self._connection.register_event("pm-initiate-event", self._on_pm_initiate_event)
|
||||||
|
self._connection.register_event("send-event", self._on_send_event)
|
||||||
|
|
||||||
async def _ensure(self) -> None:
|
def register_event(self,
|
||||||
self._ensure_open()
|
event: str,
|
||||||
await self._ensure_joined()
|
callback: Callable[..., Awaitable[None]]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Register an event callback.
|
||||||
|
|
||||||
# Phase 4
|
For an overview of the possible events, see the Room docstring.
|
||||||
|
"""
|
||||||
|
|
||||||
# Other stuff
|
self._events.register(event, callback)
|
||||||
|
|
||||||
|
# Connecting, reconnecting and disconnecting
|
||||||
|
|
||||||
|
def _set_connected(self) -> None:
|
||||||
|
packets_received = self._hello_received and self._snapshot_received
|
||||||
|
if packets_received and not self._connected.is_set():
|
||||||
|
self._connected_successfully = True
|
||||||
|
self._connected.set()
|
||||||
|
|
||||||
|
def _set_connected_failed(self) -> None:
|
||||||
|
if not self._connected.is_set():
|
||||||
|
self._connected_successfully = False
|
||||||
|
self._connected.set()
|
||||||
|
|
||||||
|
def _set_connected_reset(self) -> None:
|
||||||
|
self._connected.clear()
|
||||||
|
self._connected_successfully = False
|
||||||
|
self._hello_received = False
|
||||||
|
self._snapshot_received = False
|
||||||
|
|
||||||
|
async def _on_reconnecting(self) -> None:
|
||||||
|
self._set_connected_reset()
|
||||||
|
|
||||||
|
async def _on_hello_event(self, packet: Any) -> None:
|
||||||
|
data = packet["data"]
|
||||||
|
|
||||||
|
self._session = LiveSession.from_data(self, data["session"])
|
||||||
|
self._account = Account.from_data(data)
|
||||||
|
self._private = data["room_is_private"]
|
||||||
|
self._version = data["version"]
|
||||||
|
|
||||||
|
self._hello_received = True
|
||||||
|
self._set_connected()
|
||||||
|
|
||||||
|
async def _on_snapshot_event(self, packet: Any) -> None:
|
||||||
|
data = packet["data"]
|
||||||
|
|
||||||
|
self._server_version = data["version"]
|
||||||
|
self._users = LiveSessionListing.from_data(self, data["listing"])
|
||||||
|
self._pm_with_nick = data.get("pm_with_nick")
|
||||||
|
self._pm_with_user_id = data.get("pm_with_user_id")
|
||||||
|
|
||||||
|
# Update session nick
|
||||||
|
nick = data.get("nick")
|
||||||
|
if nick is not None and self._session is not None:
|
||||||
|
self._session = self.session.with_nick(nick)
|
||||||
|
|
||||||
|
# Send "session" event
|
||||||
|
messages = [LiveMessage.from_data(self, msg_data)
|
||||||
|
for msg_data in data["log"]]
|
||||||
|
self._events.fire("session", messages)
|
||||||
|
|
||||||
|
self._snapshot_received = True
|
||||||
|
self._set_connected()
|
||||||
|
|
||||||
|
async def _on_bounce_event(self, packet: Any) -> None:
|
||||||
|
data = packet["data"]
|
||||||
|
|
||||||
|
# Can we even authenticate?
|
||||||
|
if not "passcode" in data.get("auth_options", []):
|
||||||
|
self._set_connected_failed()
|
||||||
|
return
|
||||||
|
|
||||||
|
# If so, do we have a password?
|
||||||
|
if self._password is None:
|
||||||
|
self._set_connected_failed()
|
||||||
|
return
|
||||||
|
|
||||||
|
reply = await self._connection.send(
|
||||||
|
"auth",
|
||||||
|
{"type": "passcode", "passcode": self._password}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not reply["data"]["success"]:
|
||||||
|
self._set_connected_failed()
|
||||||
|
|
||||||
|
async def connect(self) -> bool:
|
||||||
|
"""
|
||||||
|
Attempt to connect to the room and start handling events.
|
||||||
|
|
||||||
|
This function returns once the Room is fully connected, i. e.
|
||||||
|
authenticated, using the correct nick and able to post messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not await self._connection.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
await self._connected.wait()
|
||||||
|
if not self._connected_successfully:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._session is None or self._target_nick != self._session.nick:
|
||||||
|
await self._nick(self._target_nick)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def disconnect(self) -> None:
|
||||||
|
"""
|
||||||
|
Disconnect from the room and stop the Room.
|
||||||
|
|
||||||
|
This function has the potential to mess things up, and it has not yet
|
||||||
|
been tested thoroughly. Use at your own risk, especially if you want to
|
||||||
|
call connect() after calling disconnect().
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._set_connected_reset()
|
||||||
|
await self._connection.disconnect()
|
||||||
|
|
||||||
|
# Other events
|
||||||
|
|
||||||
|
async def _on_disconnect_event(self, packet: Any) -> None:
|
||||||
|
reason = packet["data"]["reason"]
|
||||||
|
|
||||||
|
if reason == "authentication changed":
|
||||||
|
await self._connection.reconnect()
|
||||||
|
|
||||||
|
self._events.fire("disconnect", reason)
|
||||||
|
|
||||||
|
async def _on_join_event(self, packet: Any) -> None:
|
||||||
|
data = packet["data"]
|
||||||
|
|
||||||
|
session = LiveSession.from_data(self, data)
|
||||||
|
self._users = self.users.with_join(session)
|
||||||
|
|
||||||
|
self._events.fire("join", session)
|
||||||
|
|
||||||
|
async def _on_login_event(self, packet: Any) -> None:
|
||||||
|
pass # TODO implement once cookie support is here
|
||||||
|
|
||||||
|
async def _on_logout_event(self, packet: Any) -> None:
|
||||||
|
pass # TODO implement once cookie support is here
|
||||||
|
|
||||||
|
async def _on_network_event(self, packet: Any) -> None:
|
||||||
|
data = packet["data"]
|
||||||
|
|
||||||
|
if data["type"] == "partition":
|
||||||
|
server_id = data["server_id"]
|
||||||
|
server_era = data["server_era"]
|
||||||
|
|
||||||
|
users = self.users
|
||||||
|
|
||||||
|
for user in self.users:
|
||||||
|
if user.server_id == server_id and user.server_era == server_era:
|
||||||
|
users = users.with_part(user)
|
||||||
|
self._events.fire("part", user)
|
||||||
|
|
||||||
|
self._users = users
|
||||||
|
|
||||||
|
async def _on_nick_event(self, packet: Any) -> None:
|
||||||
|
data = packet["data"]
|
||||||
|
session_id = data["session_id"]
|
||||||
|
nick_from = data["from"]
|
||||||
|
nick_to = data["to"]
|
||||||
|
|
||||||
|
session = self.users.get(session_id)
|
||||||
|
if session is not None:
|
||||||
|
self._users = self.users.with_nick(session, nick_to)
|
||||||
|
else:
|
||||||
|
await self.who() # recalibrating self._users
|
||||||
|
|
||||||
|
self._events.fire("nick", session, nick_from, nick_to)
|
||||||
|
|
||||||
|
async def _on_edit_message_event(self, packet: Any) -> None:
|
||||||
|
data = packet["data"]
|
||||||
|
|
||||||
|
message = LiveMessage.from_data(self, data)
|
||||||
|
|
||||||
|
self._events.fire("edit", message)
|
||||||
|
|
||||||
|
async def _on_part_event(self, packet: Any) -> None:
|
||||||
|
data = packet["data"]
|
||||||
|
|
||||||
|
session = LiveSession.from_data(self, data)
|
||||||
|
self._users = self.users.with_part(session)
|
||||||
|
|
||||||
|
self._events.fire("part", session)
|
||||||
|
|
||||||
|
async def _on_pm_initiate_event(self, packet: Any) -> None:
|
||||||
|
data = packet["data"]
|
||||||
|
from_id = data["from"]
|
||||||
|
from_nick = data["from_nick"]
|
||||||
|
from_room = data["from_room"]
|
||||||
|
pm_id = data["pm_id"]
|
||||||
|
|
||||||
|
self._events.fire("pm", from_id, from_nick, from_room, pm_id)
|
||||||
|
|
||||||
|
async def _on_send_event(self, packet: Any) -> None:
|
||||||
|
data = packet["data"]
|
||||||
|
|
||||||
|
message = LiveMessage.from_data(self, data)
|
||||||
|
|
||||||
|
self._events.fire("send", message)
|
||||||
|
|
||||||
|
# Attributes, ordered the same as in __init__
|
||||||
|
|
||||||
|
def _wrap_optional(self, x: Optional[T]) -> T:
|
||||||
|
if x is None:
|
||||||
|
raise RoomNotConnectedException()
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
pass
|
return self._name
|
||||||
|
|
||||||
async def say(self,
|
|
||||||
text: str,
|
|
||||||
parent_id: Optional[str] = None
|
|
||||||
) -> LiveMessage:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def users(self) -> List[LiveUser]:
|
def password(self) -> Optional[str]:
|
||||||
pass
|
return self._password
|
||||||
|
|
||||||
# retrieving messages
|
@property
|
||||||
|
def target_nick(self) -> str:
|
||||||
|
return self._target_nick
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_format(self) -> str:
|
||||||
|
return self._url_format
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session(self) -> LiveSession:
|
||||||
|
return self._wrap_optional(self._session)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account(self) -> Account:
|
||||||
|
return self._wrap_optional(self._account)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def private(self) -> bool:
|
||||||
|
return self._wrap_optional(self._private)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self) -> str:
|
||||||
|
return self._wrap_optional(self._version)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def users(self) -> LiveSessionListing:
|
||||||
|
return self._wrap_optional(self._users)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pm_with_nick(self) -> str:
|
||||||
|
return self._wrap_optional(self._pm_with_nick)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pm_with_user_id(self) -> str:
|
||||||
|
return self._wrap_optional(self._pm_with_user_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self) -> str:
|
||||||
|
return self._url
|
||||||
|
|
||||||
|
# Functionality
|
||||||
|
|
||||||
|
# These functions require cookie support and are thus not implemented yet:
|
||||||
|
#
|
||||||
|
# login, logout, pm
|
||||||
|
|
||||||
|
def _extract_data(self, packet: Any) -> Any:
|
||||||
|
error = packet.get("error")
|
||||||
|
if error is not None:
|
||||||
|
raise EuphError(error)
|
||||||
|
|
||||||
|
return packet["data"]
|
||||||
|
|
||||||
|
async def _ensure_connected(self) -> None:
|
||||||
|
await self._connected.wait()
|
||||||
|
|
||||||
|
if not self._connected_successfully:
|
||||||
|
raise RoomNotConnectedException()
|
||||||
|
|
||||||
|
async def send(self,
|
||||||
|
content: str,
|
||||||
|
parent_id: Optional[str] = None
|
||||||
|
) -> LiveMessage:
|
||||||
|
await self._ensure_connected()
|
||||||
|
|
||||||
|
data = {"content": content}
|
||||||
|
if parent_id is not None:
|
||||||
|
data["parent"] = parent_id
|
||||||
|
|
||||||
|
reply = await self._connection.send("send", data)
|
||||||
|
data = self._extract_data(reply)
|
||||||
|
|
||||||
|
return LiveMessage.from_data(self, data)
|
||||||
|
|
||||||
|
async def _nick(self, nick: str) -> str:
|
||||||
|
"""
|
||||||
|
This function implements all of the nick-setting logic except waiting
|
||||||
|
for the room to actually connect. This is because connect() actually
|
||||||
|
uses this function to set the desired nick before the room is
|
||||||
|
connected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.debug(f"Setting nick to {nick!r}")
|
||||||
|
|
||||||
|
self._target_nick = nick
|
||||||
|
|
||||||
|
reply = await self._connection.send("nick", {"name": nick})
|
||||||
|
data = self._extract_data(reply)
|
||||||
|
|
||||||
|
new_nick = data["to"]
|
||||||
|
self._target_nick = new_nick
|
||||||
|
|
||||||
|
logger.debug(f"Set nick to {new_nick!r}")
|
||||||
|
|
||||||
|
return new_nick
|
||||||
|
|
||||||
|
async def nick(self, nick: str) -> str:
|
||||||
|
await self._ensure_connected()
|
||||||
|
|
||||||
|
return await self._nick(nick)
|
||||||
|
|
||||||
|
async def get(self, message_id: str) -> LiveMessage:
|
||||||
|
await self._ensure_connected()
|
||||||
|
|
||||||
|
reply = await self._connection.send("get-message", {"id": message_id})
|
||||||
|
data = self._extract_data(reply)
|
||||||
|
|
||||||
|
return LiveMessage.from_data(self, data)
|
||||||
|
|
||||||
|
async def log(self,
|
||||||
|
amount: int,
|
||||||
|
before_id: Optional[str] = None
|
||||||
|
) -> List[LiveMessage]:
|
||||||
|
await self._ensure_connected()
|
||||||
|
|
||||||
|
data: Any = {"n": amount}
|
||||||
|
if before_id is not None:
|
||||||
|
data["before"] = before_id
|
||||||
|
|
||||||
|
reply = await self._connection.send("log", data)
|
||||||
|
data = self._extract_data(reply)
|
||||||
|
|
||||||
|
messages = [LiveMessage.from_data(self, msg_data)
|
||||||
|
for msg_data in data["log"]]
|
||||||
|
return messages
|
||||||
|
|
||||||
|
async def who(self) -> LiveSessionListing:
|
||||||
|
await self._ensure_connected()
|
||||||
|
|
||||||
|
reply = await self._connection.send("who", {})
|
||||||
|
data = self._extract_data(reply)
|
||||||
|
|
||||||
|
own_id = self._session.session_id if self._session is not None else None
|
||||||
|
self._users = LiveSessionListing.from_data(
|
||||||
|
self,
|
||||||
|
data["listing"],
|
||||||
|
exclude_id = own_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._users
|
||||||
|
|
|
||||||
71
yaboli/session.py
Normal file
71
yaboli/session.py
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
from typing import TYPE_CHECKING, Any, Iterator, Optional
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .room import Room
|
||||||
|
|
||||||
|
__all__ = ["Account", "Session", "LiveSession", "LiveSessionListing"]
|
||||||
|
|
||||||
|
class Account:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls, data: Any) -> "Account":
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Session:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nick(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session_id(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class LiveSession(Session):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls, room: "Room", data: Any) -> "LiveSession":
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_id(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_era(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def with_nick(self, nick: str) -> "LiveSession":
|
||||||
|
pass
|
||||||
|
|
||||||
|
class LiveSessionListing:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[LiveSession]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls,
|
||||||
|
room: "Room",
|
||||||
|
data: Any,
|
||||||
|
exclude_id: Optional[str] = None
|
||||||
|
) -> "LiveSessionListing":
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get(self, session_id: str) -> Optional[LiveSession]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def with_join(self, session: LiveSession) -> "LiveSessionListing":
|
||||||
|
pass
|
||||||
|
|
||||||
|
def with_part(self, session: LiveSession) -> "LiveSessionListing":
|
||||||
|
pass
|
||||||
|
|
||||||
|
def with_nick(self,
|
||||||
|
session: LiveSession,
|
||||||
|
new_nick: str
|
||||||
|
) -> "LiveSessionListing":
|
||||||
|
pass
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from .util import atmention, mention
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .client import Client
|
|
||||||
from .room import Room
|
|
||||||
|
|
||||||
__all__ = ["User", "LiveUser"]
|
|
||||||
|
|
||||||
class User:
|
|
||||||
def __init__(self,
|
|
||||||
room_name: str,
|
|
||||||
id_: str,
|
|
||||||
name: str,
|
|
||||||
is_staff: bool,
|
|
||||||
is_manager: bool):
|
|
||||||
self._room_name = room_name
|
|
||||||
self._id = id_
|
|
||||||
self._name = name
|
|
||||||
self._is_staff = is_staff
|
|
||||||
self._is_manager = is_manager
|
|
||||||
|
|
||||||
@property
|
|
||||||
def room_name(self) -> str:
|
|
||||||
return self._room_name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def id(self) -> str:
|
|
||||||
return self._id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
# no name = empty str
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_staff(self) -> bool:
|
|
||||||
return self._is_staff
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_manager(self) -> bool:
|
|
||||||
return self._is_manager
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_account(self) -> bool:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_agent(self) -> bool:
|
|
||||||
# TODO should catch all old ids too
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_bot(self) -> bool:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# TODO possibly add other fields
|
|
||||||
|
|
||||||
# Properties here? Yeah sure, why not?
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mention(self) -> str:
|
|
||||||
return mention(self.name)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def atmention(self) -> str:
|
|
||||||
return atmention(self.name)
|
|
||||||
|
|
||||||
class LiveUser(User):
|
|
||||||
def __init__(self,
|
|
||||||
client: 'Client',
|
|
||||||
room: 'Room',
|
|
||||||
id_: str,
|
|
||||||
name: str,
|
|
||||||
is_staff: bool,
|
|
||||||
is_manager: bool):
|
|
||||||
super().__init__(room.name, id_, name, is_staff, is_manager)
|
|
||||||
self._room = room
|
|
||||||
|
|
||||||
@property
|
|
||||||
def room(self) -> 'Room':
|
|
||||||
return self._room
|
|
||||||
|
|
||||||
# NotLoggedInException
|
|
||||||
async def pm(self) -> 'Room':
|
|
||||||
pass
|
|
||||||
|
|
||||||
# kick
|
|
||||||
# ban
|
|
||||||
# ip_ban
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue