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 logging
|
||||
|
||||
from yaboli import Connection
|
||||
from yaboli import Room
|
||||
|
||||
FORMAT = "{asctime} [{levelname:<7}] <{name}> {funcName}(): {message}"
|
||||
DATE_FORMAT = "%F %T"
|
||||
|
|
@ -19,29 +19,17 @@ logger = logging.getLogger('yaboli')
|
|||
logger.setLevel(logging.DEBUG)
|
||||
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():
|
||||
conn = Connection("wss://euphoria.io/room/test/ws")
|
||||
#conn = Connection("wss://euphoria.io/room/cabal/ws") # password protected
|
||||
|
||||
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()
|
||||
tc = TestClient()
|
||||
await tc.run()
|
||||
|
||||
asyncio.run(main())
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from .events import *
|
|||
from .exceptions import *
|
||||
from .message import *
|
||||
from .room import *
|
||||
from .user import *
|
||||
from .session import *
|
||||
from .util import *
|
||||
|
||||
__all__: List[str] = []
|
||||
|
|
@ -16,4 +16,5 @@ __all__ += events.__all__
|
|||
__all__ += exceptions.__all__
|
||||
__all__ += message.__all__
|
||||
__all__ += room.__all__
|
||||
__all__ += user.__all__
|
||||
__all__ += session.__all__
|
||||
__all__ += util.__all__
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from .message import Message
|
||||
from .room import Room
|
||||
from .user import User
|
||||
|
||||
__all__ = ["Client"]
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
__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:
|
||||
"""
|
||||
The Connection handles the lower-level stuff required when connecting to
|
||||
|
|
@ -447,7 +450,8 @@ class Connection:
|
|||
|
||||
# Finally, reset the ping check
|
||||
logger.debug("Resetting ping check")
|
||||
self._ping_check.cancel()
|
||||
if self._ping_check is not None:
|
||||
self._ping_check.cancel()
|
||||
self._ping_check = asyncio.create_task(
|
||||
self._disconnect_in(self.PING_TIMEOUT))
|
||||
|
||||
|
|
@ -473,7 +477,7 @@ class Connection:
|
|||
except IncorrectStateException:
|
||||
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"
|
||||
callback.
|
||||
|
|
@ -481,7 +485,7 @@ class Connection:
|
|||
logger.debug("Pong!")
|
||||
await self._do_if_possible(self.send(
|
||||
"ping-reply",
|
||||
{"time": packet["data"]["time"]}
|
||||
{"time": packet["data"]["time"]},
|
||||
await_reply=False
|
||||
))
|
||||
|
||||
|
|
@ -489,7 +493,7 @@ class Connection:
|
|||
packet_type: str,
|
||||
data: Any,
|
||||
await_reply: bool = True
|
||||
) -> Optional[Any]:
|
||||
) -> Any:
|
||||
"""
|
||||
Send a packet of type packet_type to the server.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ __all__ = [
|
|||
"CouldNotConnectException",
|
||||
"CouldNotAuthenticateException",
|
||||
# Doing stuff in a room
|
||||
"RoomNotConnectedException",
|
||||
"EuphError",
|
||||
"RoomClosedException",
|
||||
]
|
||||
|
||||
|
|
@ -52,6 +54,21 @@ class CouldNotAuthenticateException(JoinException):
|
|||
|
||||
# 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):
|
||||
"""
|
||||
The room has been closed already.
|
||||
|
|
|
|||
|
|
@ -1,108 +1,30 @@
|
|||
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:
|
||||
from .client import Client
|
||||
from .room import Room
|
||||
|
||||
__all__ = ["Message", "LiveMessage"]
|
||||
|
||||
# "Offline" message
|
||||
class Message:
|
||||
def __init__(self,
|
||||
room_name: str,
|
||||
id_: str,
|
||||
parent_id: Optional[str],
|
||||
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
|
||||
pass
|
||||
# @property
|
||||
# def room_name(self) -> str:
|
||||
# return self._room_name
|
||||
#
|
||||
# @property
|
||||
# def time(self) -> datetime.datetime:
|
||||
# return datetime.datetime.fromtimestamp(self.timestamp)
|
||||
#
|
||||
# @property
|
||||
# def timestamp(self) -> int:
|
||||
# return self._timestamp
|
||||
|
||||
@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
|
||||
# duplicate around, if possible?
|
||||
self._livesender = sender
|
||||
pass
|
||||
|
||||
@property
|
||||
def room(self) -> 'Room':
|
||||
return self._room
|
||||
|
||||
@property
|
||||
def sender(self) -> LiveUser:
|
||||
return self._livesender
|
||||
|
||||
async def reply(self, text: str) -> None:
|
||||
pass
|
||||
|
||||
# TODO add some sort of permission guard that checks the room
|
||||
# UnauthorizedException
|
||||
async def delete(self,
|
||||
deleted: bool = True
|
||||
) -> None:
|
||||
@classmethod
|
||||
def from_data(cls, room: "Room", data: Any) -> "LiveMessage":
|
||||
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 .message import LiveMessage
|
||||
from .user import LiveUser
|
||||
from .session import Account, LiveSession, LiveSessionListing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ["Room"]
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Room:
|
||||
"""
|
||||
A Room represents one connection to a room on euphoria, i. e. what other
|
||||
implementations might consider a "client". This means that each Room has
|
||||
its own session (User) and nick.
|
||||
Events and parameters:
|
||||
|
||||
A Room can only be used once in the sense that after it has been closed,
|
||||
any further actions will result in a RoomClosedException. If you need to
|
||||
manually reconnect, instead just create a new Room object.
|
||||
"snapshot" - snapshot of the room's messages at the time of joining
|
||||
messages: List[LiveMessage]
|
||||
|
||||
"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
|
||||
2. await join()
|
||||
3. do room-related stuff
|
||||
4. await part()
|
||||
"nick" - another room member has changed their nick
|
||||
user: LiveUser
|
||||
from: str
|
||||
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
|
||||
current nick are used when first connecting to the room, or when
|
||||
reconnecting to the room after connection was lost.
|
||||
|
||||
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.
|
||||
"disconect" - corresponds to http://api.euphoria.io/#disconnect-event (if
|
||||
the reason is "authentication changed", the room automatically reconnects)
|
||||
reason: str - the reason for disconnection
|
||||
"""
|
||||
|
||||
# Phase 1
|
||||
URL_FORMAT = "wss://euphoria.io/room/{}/ws"
|
||||
|
||||
def __init__(self,
|
||||
room_name: str,
|
||||
nick: str = "",
|
||||
password: Optional[str] = None) -> None:
|
||||
pass
|
||||
name: str,
|
||||
password: Optional[str] = None,
|
||||
target_nick: str = "",
|
||||
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:
|
||||
if self.closed:
|
||||
raise RoomClosedException()
|
||||
self._connection.register_event("reconnecting", self._on_reconnecting)
|
||||
self._connection.register_event("hello-event", self._on_hello_event)
|
||||
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:
|
||||
pass
|
||||
self._connection.register_event("disconnect-event", self._on_disconnect_event)
|
||||
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:
|
||||
self._ensure_open()
|
||||
await self._ensure_joined()
|
||||
def register_event(self,
|
||||
event: str,
|
||||
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
|
||||
def name(self) -> str:
|
||||
pass
|
||||
|
||||
async def say(self,
|
||||
text: str,
|
||||
parent_id: Optional[str] = None
|
||||
) -> LiveMessage:
|
||||
pass
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def users(self) -> List[LiveUser]:
|
||||
pass
|
||||
def password(self) -> Optional[str]:
|
||||
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