Start rewrite (yet again)
This will hopefully be the final rewrite.
This commit is contained in:
parent
5e108fd31b
commit
a5af01f669
19 changed files with 455 additions and 1344 deletions
14
.gitignore
vendored
14
.gitignore
vendored
|
|
@ -1,2 +1,12 @@
|
||||||
**/__pycache__/
|
# python stuff
|
||||||
*.cookie
|
*/__pycache__/
|
||||||
|
|
||||||
|
# venv stuff
|
||||||
|
bin/
|
||||||
|
include/
|
||||||
|
lib/
|
||||||
|
lib64
|
||||||
|
pyvenv.cfg
|
||||||
|
|
||||||
|
# mypy stuff
|
||||||
|
.mypy_cache/
|
||||||
|
|
|
||||||
26
example.py
Normal file
26
example.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import yyb
|
||||||
|
|
||||||
|
class MyClient(yyb.Client):
|
||||||
|
async def on_join(self, room):
|
||||||
|
await room.say("Hello!")
|
||||||
|
|
||||||
|
async def on_message(self, message):
|
||||||
|
if message.content == "reply to me"):
|
||||||
|
reply = await message.reply("reply")
|
||||||
|
await reply.reply("reply to the reply")
|
||||||
|
await message.room.say("stuff going on")
|
||||||
|
|
||||||
|
elif message.content == "hey, join &test!":
|
||||||
|
# returns room in phase 3, or throws JoinException
|
||||||
|
room = await self.join("test")
|
||||||
|
if room:
|
||||||
|
room.say("hey, I joined!")
|
||||||
|
else:
|
||||||
|
message.reply("didn't work :(")
|
||||||
|
|
||||||
|
async def before_part(self, room):
|
||||||
|
await room.say("Goodbye!")
|
||||||
|
|
||||||
|
# Something like this, I guess. It's still missing password fields though.
|
||||||
|
c = MyClient("my:bot:")
|
||||||
|
c.run("test", "bots")
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
[general]
|
|
||||||
nick = ExampleBot
|
|
||||||
cookiefile = examplebot.cookie
|
|
||||||
|
|
||||||
[rooms]
|
|
||||||
# Format:
|
|
||||||
# room
|
|
||||||
# room=password
|
|
||||||
test
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import configparser
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import yaboli
|
|
||||||
from yaboli.utils import *
|
|
||||||
|
|
||||||
|
|
||||||
class ExampleBot(yaboli.Bot):
|
|
||||||
async def on_command_specific(self, room, message, command, nick, argstr):
|
|
||||||
long_help = (
|
|
||||||
"I'm an example bot for the yaboli bot library,"
|
|
||||||
" which can be found at https://github.com/Garmelon/yaboli"
|
|
||||||
)
|
|
||||||
|
|
||||||
if similar(nick, room.session.nick) and not argstr:
|
|
||||||
await self.botrulez_ping(room, message, command, text="ExamplePong!")
|
|
||||||
await self.botrulez_help(room, message, command, text=long_help)
|
|
||||||
await self.botrulez_uptime(room, message, command)
|
|
||||||
await self.botrulez_kill(room, message, command)
|
|
||||||
await self.botrulez_restart(room, message, command)
|
|
||||||
|
|
||||||
async def on_command_general(self, room, message, command, argstr):
|
|
||||||
short_help = "Example bot for the yaboli bot library"
|
|
||||||
|
|
||||||
if not argstr:
|
|
||||||
await self.botrulez_ping(room, message, command, text="ExamplePong!")
|
|
||||||
await self.botrulez_help(room, message, command, text=short_help)
|
|
||||||
|
|
||||||
def main(configfile):
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
|
|
||||||
config = configparser.ConfigParser(allow_no_value=True)
|
|
||||||
config.read(configfile)
|
|
||||||
|
|
||||||
nick = config.get("general", "nick")
|
|
||||||
cookiefile = config.get("general", "cookiefile", fallback=None)
|
|
||||||
bot = ExampleBot(nick, cookiefile=cookiefile)
|
|
||||||
|
|
||||||
for room, password in config.items("rooms"):
|
|
||||||
if not password:
|
|
||||||
password = None
|
|
||||||
bot.join_room(room, password=password)
|
|
||||||
|
|
||||||
asyncio.get_event_loop().run_forever()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main("examplebot.conf")
|
|
||||||
22
info.txt
Normal file
22
info.txt
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
Signature of a normal function:
|
||||||
|
|
||||||
|
def a(b: int, c: str) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
a # type: Callable[[int, str], bool]
|
||||||
|
|
||||||
|
Signature of an async function:
|
||||||
|
|
||||||
|
async def a(b: int, c: str) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
a # type: Callable[[int, str], Awaitable[bool]]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Enable logging (from the websockets docs):
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger('websockets')
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
logger.addHandler(logging.StreamHandler())
|
||||||
3
mypy.ini
Normal file
3
mypy.ini
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[mypy]
|
||||||
|
disallow_untyped_defs = True
|
||||||
|
disallow_incomplete_defs = True
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
websockets==7.0
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
from .bot import *
|
from typing import List
|
||||||
from .cookiejar import *
|
|
||||||
from .connection import *
|
|
||||||
from .database import *
|
|
||||||
from .exceptions import *
|
|
||||||
from .room import *
|
|
||||||
from .utils import *
|
|
||||||
|
|
||||||
__all__ = (
|
__all__: List[str] = []
|
||||||
bot.__all__ +
|
|
||||||
connection.__all__ +
|
from .client import *
|
||||||
cookiejar.__all__ +
|
__all__ += client.__all__
|
||||||
database.__all__ +
|
|
||||||
exceptions.__all__ +
|
from .exceptions import *
|
||||||
room.__all__ +
|
__all__ += client.__all__
|
||||||
utils.__all__
|
|
||||||
)
|
from .message import *
|
||||||
|
__all__ += exceptions.__all__
|
||||||
|
|
||||||
|
from .room import *
|
||||||
|
__all__ += message.__all__
|
||||||
|
|
||||||
|
__all__ += room.__all__
|
||||||
|
from .user import *
|
||||||
|
|
||||||
|
__all__ += user.__all__
|
||||||
|
from .util import *
|
||||||
|
|
|
||||||
271
yaboli/bot.py
271
yaboli/bot.py
|
|
@ -1,271 +0,0 @@
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
|
|
||||||
from .cookiejar import *
|
|
||||||
from .room import *
|
|
||||||
from .utils import *
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
__all__ = ["Bot", "command", "trigger", "Module", "ModuleBot"]
|
|
||||||
|
|
||||||
|
|
||||||
# Some command stuff
|
|
||||||
|
|
||||||
SPECIFIC_RE = re.compile(r"!(\S+)\s+@(\S+)\s*([\S\s]*)")
|
|
||||||
GENERAL_RE = re.compile(r"!(\S+)\s*([\S\s]*)")
|
|
||||||
|
|
||||||
# Decorator magic for commands and triggers.
|
|
||||||
# I think commands could probably be implemented as some kind of triggers,
|
|
||||||
# but I'm not gonna do that now because commands are working fine this way.
|
|
||||||
def command(*commands):
|
|
||||||
def decorator(func):
|
|
||||||
async def wrapper(self, room, message, command, *args, **kwargs):
|
|
||||||
if command in commands:
|
|
||||||
await func(self, room, message, *args, **kwargs)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
return wrapper
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def trigger(regex, fullmatch=True, flags=0):
|
|
||||||
def decorator(func):
|
|
||||||
compiled_regex = re.compile(regex, flags=flags)
|
|
||||||
async def wrapper(self, room, message, *args, **kwargs):
|
|
||||||
if fullmatch:
|
|
||||||
match = compiled_regex.fullmatch(message.content)
|
|
||||||
else:
|
|
||||||
match = compiled_regex.match(message.content)
|
|
||||||
if match is not None:
|
|
||||||
await func(self, room, message, match, *args, **kwargs)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
return wrapper
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
# And now comes the real bot...
|
|
||||||
|
|
||||||
class Bot(Inhabitant):
|
|
||||||
def __init__(self, nick, cookiefile=None):
|
|
||||||
self.target_nick = nick
|
|
||||||
self.rooms = {}
|
|
||||||
self.cookiejar = CookieJar(cookiefile)
|
|
||||||
|
|
||||||
# ROOM MANAGEMENT
|
|
||||||
|
|
||||||
def join_room(self, roomname, **kwargs):
|
|
||||||
if roomname in self.rooms:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.rooms[roomname] = Room(self, roomname, self.target_nick, cookiejar=self.cookiejar, **kwargs)
|
|
||||||
|
|
||||||
async def part_room(self, roomname):
|
|
||||||
room = self.rooms.pop(roomname, None)
|
|
||||||
if room:
|
|
||||||
await room.exit()
|
|
||||||
|
|
||||||
# COMMANDS
|
|
||||||
|
|
||||||
async def on_command_specific(self, room, message, command, nick, argstr):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_command_general(self, room, message, command, argstr):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# INHABITED FUNCTIONS
|
|
||||||
|
|
||||||
async def on_send(self, room, message):
|
|
||||||
match = SPECIFIC_RE.fullmatch(message.content)
|
|
||||||
if match:
|
|
||||||
command, nick, argstr = match.groups()
|
|
||||||
await self.on_command_specific(room, message, command, nick, argstr)
|
|
||||||
|
|
||||||
match = GENERAL_RE.fullmatch(message.content)
|
|
||||||
if match:
|
|
||||||
command, argstr = match.groups()
|
|
||||||
await self.on_command_general(room, message, command, argstr)
|
|
||||||
|
|
||||||
async def on_stopped(self, room):
|
|
||||||
await self.part_room(room.roomname)
|
|
||||||
|
|
||||||
# BOTRULEZ
|
|
||||||
|
|
||||||
@command("ping")
|
|
||||||
async def botrulez_ping(self, room, message, text="Pong!"):
|
|
||||||
await room.send(text, message.mid)
|
|
||||||
|
|
||||||
@command("help")
|
|
||||||
async def botrulez_help(self, room, message, text="Placeholder help text"):
|
|
||||||
await room.send(text, message.mid)
|
|
||||||
|
|
||||||
@command("uptime")
|
|
||||||
async def botrulez_uptime(self, room, message):
|
|
||||||
now = time.time()
|
|
||||||
startformat = format_time(room.start_time)
|
|
||||||
deltaformat = format_time_delta(now - room.start_time)
|
|
||||||
text = f"/me has been up since {startformat} ({deltaformat})"
|
|
||||||
await room.send(text, message.mid)
|
|
||||||
|
|
||||||
@command("kill")
|
|
||||||
async def botrulez_kill(self, room, message, text="/me dies"):
|
|
||||||
await room.send(text, message.mid)
|
|
||||||
await self.part_room(room.roomname)
|
|
||||||
|
|
||||||
@command("restart")
|
|
||||||
async def botrulez_restart(self, room, message, text="/me restarts"):
|
|
||||||
await room.send(text, message.mid)
|
|
||||||
await self.part_room(room.roomname)
|
|
||||||
self.join_room(room.roomname, password=room.password)
|
|
||||||
|
|
||||||
# COMMAND PARSING
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_args(text):
|
|
||||||
"""
|
|
||||||
Use bash-style single- and double-quotes to include whitespace in arguments.
|
|
||||||
A backslash always escapes the next character.
|
|
||||||
Any non-escaped whitespace separates arguments.
|
|
||||||
|
|
||||||
Returns a list of arguments.
|
|
||||||
Deals with unclosed quotes and backslashes without crashing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
escape = False
|
|
||||||
quote = None
|
|
||||||
args = []
|
|
||||||
arg = ""
|
|
||||||
|
|
||||||
for character in text:
|
|
||||||
if escape:
|
|
||||||
arg += character
|
|
||||||
escape = False
|
|
||||||
elif character == "\\":
|
|
||||||
escape = True
|
|
||||||
elif quote:
|
|
||||||
if character == quote:
|
|
||||||
quote = None
|
|
||||||
else:
|
|
||||||
arg += character
|
|
||||||
elif character in "'\"":
|
|
||||||
quote = character
|
|
||||||
elif character.isspace():
|
|
||||||
if len(arg) > 0:
|
|
||||||
args.append(arg)
|
|
||||||
arg = ""
|
|
||||||
else:
|
|
||||||
arg += character
|
|
||||||
|
|
||||||
#if escape or quote:
|
|
||||||
#return None # syntax error
|
|
||||||
|
|
||||||
if escape:
|
|
||||||
arg += "\\"
|
|
||||||
|
|
||||||
if len(arg) > 0:
|
|
||||||
args.append(arg)
|
|
||||||
|
|
||||||
return args
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_flags(arglist):
|
|
||||||
flags = ""
|
|
||||||
args = []
|
|
||||||
kwargs = {}
|
|
||||||
|
|
||||||
for arg in arglist:
|
|
||||||
# kwargs (--abc, --foo=bar)
|
|
||||||
if arg[:2] == "--":
|
|
||||||
arg = arg[2:]
|
|
||||||
if "=" in arg:
|
|
||||||
s = arg.split("=", maxsplit=1)
|
|
||||||
kwargs[s[0]] = s[1]
|
|
||||||
else:
|
|
||||||
kwargs[arg] = None
|
|
||||||
# flags (-x, -rw)
|
|
||||||
elif arg[:1] == "-":
|
|
||||||
arg = arg[1:]
|
|
||||||
flags += arg
|
|
||||||
# args (normal arguments)
|
|
||||||
else:
|
|
||||||
args.append(arg)
|
|
||||||
|
|
||||||
return flags, args, kwargs
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_command(content, specific=None):
|
|
||||||
if specific:
|
|
||||||
match = SPECIFIC_RE.fullmatch(content)
|
|
||||||
if match:
|
|
||||||
return match.group(1), match.group(3)
|
|
||||||
else:
|
|
||||||
match = GENERAL_RE.fullmatch(content)
|
|
||||||
if match:
|
|
||||||
return match.group(1), match.group(2)
|
|
||||||
|
|
||||||
class Module(Inhabitant):
|
|
||||||
SHORT_DESCRIPTION = "short module description"
|
|
||||||
LONG_DESCRIPTION = "long module description"
|
|
||||||
SHORT_HELP = "short !help"
|
|
||||||
LONG_HELP = "long !help"
|
|
||||||
|
|
||||||
async def on_command_specific(self, room, message, command, nick, argstr, mentioned):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_command_general(self, room, message, command, argstr):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ModuleBot(Bot):
|
|
||||||
def __init__(self, module, nick, *args, cookiefile=None, **kwargs):
|
|
||||||
super().__init__(nick, cookiefile=cookiefile)
|
|
||||||
self.module = module
|
|
||||||
|
|
||||||
async def on_created(self, room):
|
|
||||||
await self.module.on_created(room)
|
|
||||||
|
|
||||||
async def on_connected(self, room, log):
|
|
||||||
await self.module.on_connected(room, log)
|
|
||||||
|
|
||||||
async def on_disconnected(self, room):
|
|
||||||
await self.module.on_disconnected(room)
|
|
||||||
|
|
||||||
async def on_stopped(self, room):
|
|
||||||
await self.module.on_stopped(room)
|
|
||||||
|
|
||||||
async def on_join(self, room, session):
|
|
||||||
await self.module.on_join(room, session)
|
|
||||||
|
|
||||||
async def on_part(self, room, session):
|
|
||||||
await self.module.on_part(room, session)
|
|
||||||
|
|
||||||
async def on_nick(self, room, sid, uid, from_nick, to_nick):
|
|
||||||
await self.module.on_nick(room, sid, uid, from_nick, to_nick)
|
|
||||||
|
|
||||||
async def on_send(self, room, message):
|
|
||||||
await super().on_send(room, message)
|
|
||||||
|
|
||||||
await self.module.on_send(room, message)
|
|
||||||
|
|
||||||
async def on_command_specific(self, room, message, command, nick, argstr):
|
|
||||||
if similar(nick, room.session.nick):
|
|
||||||
await self.module.on_command_specific(room, message, command, nick, argstr, True)
|
|
||||||
|
|
||||||
if not argstr:
|
|
||||||
await self.botrulez_ping(room, message, command)
|
|
||||||
await self.botrulez_help(room, message, command, text=self.module.LONG_HELP)
|
|
||||||
await self.botrulez_uptime(room, message, command)
|
|
||||||
await self.botrulez_kill(room, message, command)
|
|
||||||
await self.botrulez_restart(room, message, command)
|
|
||||||
|
|
||||||
else:
|
|
||||||
await self.module.on_command_specific(room, message, command, nick, argstr, False)
|
|
||||||
|
|
||||||
async def on_command_general(self, room, message, command, argstr):
|
|
||||||
await self.module.on_command_general(room, message, command, argstr)
|
|
||||||
|
|
||||||
if not argstr:
|
|
||||||
await self.botrulez_ping(room, message, command)
|
|
||||||
await self.botrulez_help(room, message, command, text=self.module.SHORT_HELP)
|
|
||||||
23
yaboli/client.py
Normal file
23
yaboli/client.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
from .message import Message
|
||||||
|
from .room import Room
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
__all__ = ["Client"]
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
|
||||||
|
# Joining and leaving rooms
|
||||||
|
|
||||||
|
async def join(self,
|
||||||
|
room_name: str,
|
||||||
|
password: str = None,
|
||||||
|
nick: str = None) -> Room:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get(self, room_name: str) -> Optional[Room]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_all(self, room_name: str) -> List[Room]:
|
||||||
|
pass
|
||||||
|
|
@ -1,229 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import socket
|
|
||||||
import websockets
|
|
||||||
|
|
||||||
from .exceptions import *
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
__all__ = ["Connection"]
|
|
||||||
|
|
||||||
|
|
||||||
class Connection:
|
|
||||||
def __init__(self, url, packet_callback, disconnect_callback, stop_callback, cookiejar=None, ping_timeout=10, ping_delay=30, reconnect_attempts=10):
|
|
||||||
self.url = url
|
|
||||||
self.packet_callback = packet_callback
|
|
||||||
self.disconnect_callback = disconnect_callback
|
|
||||||
self.stop_callback = stop_callback # is called when the connection stops on its own
|
|
||||||
self.cookiejar = cookiejar
|
|
||||||
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.reconnect_attempts = reconnect_attempts
|
|
||||||
|
|
||||||
self._ws = None
|
|
||||||
self._pid = 0 # successive packet ids
|
|
||||||
#self._spawned_tasks = set()
|
|
||||||
self._pending_responses = {}
|
|
||||||
|
|
||||||
self._stopped = False
|
|
||||||
self._pingtask = None
|
|
||||||
self._runtask = asyncio.ensure_future(self._run())
|
|
||||||
# ... aaand the connection is started.
|
|
||||||
|
|
||||||
async def send(self, ptype, data=None, await_response=True):
|
|
||||||
if not self._ws:
|
|
||||||
raise ConnectionClosed
|
|
||||||
#raise asyncio.CancelledError
|
|
||||||
|
|
||||||
pid = str(self._new_pid())
|
|
||||||
packet = {
|
|
||||||
"type": ptype,
|
|
||||||
"id": pid
|
|
||||||
}
|
|
||||||
if data:
|
|
||||||
packet["data"] = data
|
|
||||||
|
|
||||||
if await_response:
|
|
||||||
wait_for = self._wait_for_response(pid)
|
|
||||||
|
|
||||||
logging.debug(f"Currently used websocket at self._ws: {self._ws}")
|
|
||||||
try:
|
|
||||||
await self._ws.send(json.dumps(packet, separators=(',', ':'))) # minimum size
|
|
||||||
except websockets.ConnectionClosed:
|
|
||||||
raise ConnectionClosed()
|
|
||||||
|
|
||||||
if await_response:
|
|
||||||
await wait_for
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not self._stopped:
|
|
||||||
self._stopped = True
|
|
||||||
await self.reconnect() # _run() does the cleaning up now.
|
|
||||||
await self._runtask
|
|
||||||
|
|
||||||
async def reconnect(self):
|
|
||||||
"""
|
|
||||||
Reconnect to the url.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._ws:
|
|
||||||
await self._ws.close()
|
|
||||||
|
|
||||||
async def _connect(self, tries, timeout=10):
|
|
||||||
"""
|
|
||||||
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()]
|
|
||||||
ws = asyncio.ensure_future(
|
|
||||||
websockets.connect(self.url, max_size=None, extra_headers=cookies)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
ws = asyncio.ensure_future(
|
|
||||||
websockets.connect(self.url, max_size=None)
|
|
||||||
)
|
|
||||||
self._ws = await asyncio.wait_for(ws, timeout)
|
|
||||||
except (websockets.InvalidHandshake, socket.gaierror, asyncio.TimeoutError): # not websockets.InvalidURI
|
|
||||||
logger.warn(f"Connection attempt failed, {tries} tries left.")
|
|
||||||
self._ws = None
|
|
||||||
|
|
||||||
if tries is not None:
|
|
||||||
tries -= 1
|
|
||||||
if tries <= 0:
|
|
||||||
logger.warn(f"{self.url}:Ran out of tries")
|
|
||||||
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.cookiejar.save()
|
|
||||||
|
|
||||||
self._pingtask = asyncio.ensure_future(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
|
|
||||||
"""
|
|
||||||
|
|
||||||
asyncio.ensure_future(self.disconnect_callback())
|
|
||||||
|
|
||||||
# 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(ConnectionClosed("No server response"))
|
|
||||||
self._pending_responses = {}
|
|
||||||
|
|
||||||
async def _run(self):
|
|
||||||
"""
|
|
||||||
Listen for packets and deal with them accordingly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
while not self._stopped:
|
|
||||||
logger.debug(f"{self.url}:Connecting...")
|
|
||||||
connected = await self._connect(self.reconnect_attempts)
|
|
||||||
if connected:
|
|
||||||
logger.debug(f"{self.url}:Connected")
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
await self._handle_next_message()
|
|
||||||
except websockets.ConnectionClosed:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
await self._disconnect() # disconnect and clean up
|
|
||||||
else:
|
|
||||||
logger.debug(f"{self.url}:Stopping")
|
|
||||||
asyncio.ensure_future(self.stop_callback)
|
|
||||||
self._stopped = True
|
|
||||||
await self._disconnect()
|
|
||||||
|
|
||||||
|
|
||||||
async def _ping(self):
|
|
||||||
"""
|
|
||||||
Periodically ping the server to detect a timeout.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
logger.debug(f"{self.url}:Pinging...")
|
|
||||||
wait_for_reply = await self._ws.ping()
|
|
||||||
await asyncio.wait_for(wait_for_reply, self.ping_timeout)
|
|
||||||
logger.debug(f"{self.url}:Pinged!")
|
|
||||||
await asyncio.sleep(self.ping_delay)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
logger.warning(f"{self.url}:Ping timed out")
|
|
||||||
await self.reconnect()
|
|
||||||
except (websockets.ConnectionClosed, ConnectionResetError, asyncio.CancelledError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _new_pid(self):
|
|
||||||
self._pid += 1
|
|
||||||
return self._pid
|
|
||||||
|
|
||||||
async def _handle_next_message(self):
|
|
||||||
response = await self._ws.recv()
|
|
||||||
packet = json.loads(response)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Deal with pending responses
|
|
||||||
pid = packet.get("id", None)
|
|
||||||
future = self._pending_responses.pop(pid, None)
|
|
||||||
if future:
|
|
||||||
future.set_result((ptype, data, error, throttled))
|
|
||||||
|
|
||||||
# Pass packet onto room
|
|
||||||
asyncio.ensure_future(self.packet_callback(ptype, data, error, throttled))
|
|
||||||
|
|
||||||
def _wait_for_response(self, pid):
|
|
||||||
future = asyncio.Future()
|
|
||||||
self._pending_responses[pid] = future
|
|
||||||
return future
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
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=None):
|
|
||||||
self._filename = filename
|
|
||||||
self._cookies = cookies.SimpleCookie()
|
|
||||||
|
|
||||||
if not self._filename:
|
|
||||||
logger.warning("Could not load cookies, no filename given.")
|
|
||||||
return
|
|
||||||
|
|
||||||
with contextlib.suppress(FileNotFoundError):
|
|
||||||
logger.info(f"Loading cookies from {self._filename!r}")
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not self._filename:
|
|
||||||
logger.warning("Could not save cookies, no filename given.")
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(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()
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
from .utils import *
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
__all__ = ["Database", "operation"]
|
|
||||||
|
|
||||||
|
|
||||||
def operation(func):
|
|
||||||
async def wrapper(self, *args, **kwargs):
|
|
||||||
async with self as db:
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
return await asyncify(func, self, db, *args, **kwargs)
|
|
||||||
except sqlite3.OperationalError as e:
|
|
||||||
logger.warn(f"Operational error encountered: {e}")
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
class Database:
|
|
||||||
def __init__(self, database):
|
|
||||||
self._connection = sqlite3.connect(database, check_same_thread=False)
|
|
||||||
self._lock = asyncio.Lock()
|
|
||||||
|
|
||||||
self.initialize(self._connection)
|
|
||||||
|
|
||||||
def initialize(self, db):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def __aenter__(self, *args, **kwargs):
|
|
||||||
await self._lock.__aenter__(*args, **kwargs)
|
|
||||||
return self._connection
|
|
||||||
|
|
||||||
async def __aexit__(self, *args, **kwargs):
|
|
||||||
return await self._lock.__aexit__(*args, **kwargs)
|
|
||||||
|
|
@ -1,13 +1,51 @@
|
||||||
__all__ = ["ConnectionClosed"]
|
__all__ = ["EuphException", "JoinException", "CouldNotConnectException",
|
||||||
|
"CouldNotAuthenticateException", "RoomClosedException",
|
||||||
|
"RateLimitException", "NotLoggedInException", "UnauthorizedException"]
|
||||||
|
|
||||||
class ConnectionClosed(Exception):
|
class EuphException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class RoomException(Exception):
|
# Joining a room
|
||||||
pass
|
|
||||||
|
|
||||||
class AuthenticationRequired(RoomException):
|
class JoinException(EuphException):
|
||||||
pass
|
"""
|
||||||
|
An exception that happened while joining a room.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
class RoomClosed(RoomException):
|
class CouldNotConnectException(JoinException):
|
||||||
pass
|
"""
|
||||||
|
Could not establish a websocket connection to euphoria.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CouldNotAuthenticateException(JoinException):
|
||||||
|
"""
|
||||||
|
The password is either incorrect or not set, even though authentication is
|
||||||
|
required.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Doing stuff in a room
|
||||||
|
|
||||||
|
class RoomClosedException(EuphException):
|
||||||
|
"""
|
||||||
|
The room has been closed already.
|
||||||
|
|
||||||
|
This means that phase 4 (see the docstring of Room) has been initiated or
|
||||||
|
completed.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# exception for having no username?
|
||||||
|
|
||||||
|
# Maybe these will become real exceptions one day?
|
||||||
|
|
||||||
|
class RateLimitException(EuphException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NotLoggedInException(EuphException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class UnauthorizedException(EuphException):
|
||||||
|
pass
|
||||||
|
|
|
||||||
108
yaboli/message.py
Normal file
108
yaboli/message.py
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
from .user import User, LiveUser
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
@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:
|
||||||
|
pass
|
||||||
510
yaboli/room.py
510
yaboli/room.py
|
|
@ -1,443 +1,107 @@
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
|
|
||||||
from .connection import *
|
|
||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
from .utils import *
|
from .message import LiveMessage
|
||||||
|
from .user import LiveUser
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
__all__ = ["Room"]
|
||||||
__all__ = ["Room", "Inhabitant"]
|
|
||||||
|
|
||||||
|
|
||||||
class Room:
|
class Room:
|
||||||
"""
|
"""
|
||||||
TODO
|
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.
|
||||||
CONNECTED = 1
|
|
||||||
DISCONNECTED = 2
|
|
||||||
CLOSED = 3
|
|
||||||
FORWARDING = 4
|
|
||||||
|
|
||||||
def __init__(self, inhabitant, roomname, nick, password=None, human=False, cookiejar=None, **kwargs):
|
|
||||||
# TODO: Connect to room etc.
|
|
||||||
# TODO: Deal with room/connection states of:
|
|
||||||
# disconnected connecting, fast-forwarding, connected
|
|
||||||
|
|
||||||
# Room info (all fields readonly!)
|
|
||||||
self.target_nick = nick
|
|
||||||
self.roomname = roomname
|
|
||||||
self.password = password
|
|
||||||
self.human = human
|
|
||||||
|
|
||||||
self.session = None
|
|
||||||
self.account = None
|
|
||||||
self.listing = Listing()
|
|
||||||
|
|
||||||
self.start_time = time.time()
|
|
||||||
|
|
||||||
self.account_has_access = None
|
|
||||||
self.account_email_verified = None
|
|
||||||
self.room_is_private = None
|
|
||||||
self.version = None # the version of the code being run and served by the server
|
|
||||||
self.pm_with_nick = None
|
|
||||||
self.pm_with_user_id = None
|
|
||||||
|
|
||||||
self._inhabitant = inhabitant
|
|
||||||
self._status = Room.DISCONNECTED
|
|
||||||
self._connected_future = asyncio.Future()
|
|
||||||
|
|
||||||
self._last_known_mid = None
|
|
||||||
self._forwarding = None # task that downloads messages and fowards
|
|
||||||
self._forward_new = [] # new messages received while downloading old messages
|
|
||||||
|
|
||||||
# TODO: Allow for all parameters of Connection() to be specified in Room().
|
|
||||||
self._connection = Connection(
|
|
||||||
self.format_room_url(self.roomname, human=self.human),
|
|
||||||
self._receive_packet,
|
|
||||||
self._disconnected,
|
|
||||||
self._stopped,
|
|
||||||
cookiejar,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
asyncio.ensure_future(self._inhabitant.on_created(self))
|
|
||||||
|
|
||||||
async def exit(self):
|
|
||||||
self._status = Room.CLOSED
|
|
||||||
await self._connection.stop()
|
|
||||||
|
|
||||||
# ROOM COMMANDS
|
|
||||||
# These always return a response from the server.
|
|
||||||
# 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):
|
|
||||||
if self._status == Room.CLOSED:
|
|
||||||
raise RoomClosed()
|
|
||||||
|
|
||||||
ptype, data, error, throttled = await self._send_while_connected(
|
|
||||||
"get-message",
|
|
||||||
id=mid
|
|
||||||
)
|
|
||||||
|
|
||||||
if data:
|
|
||||||
return Message.from_dict(data)
|
|
||||||
# else: message does not exist
|
|
||||||
|
|
||||||
# The log returned is sorted from old to new
|
|
||||||
async def log(self, n, before=None):
|
|
||||||
if self._status == Room.CLOSED:
|
|
||||||
raise RoomClosed()
|
|
||||||
|
|
||||||
if before:
|
|
||||||
ptype, data, error, throttled = await self._send_while_connected(
|
|
||||||
"log",
|
|
||||||
n=n,
|
|
||||||
before=before
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
ptype, data, error, throttled = await self._send_while_connected(
|
|
||||||
"log",
|
|
||||||
n=n
|
|
||||||
)
|
|
||||||
|
|
||||||
return [Message.from_dict(d) for d in data.get("log")]
|
|
||||||
|
|
||||||
async def nick(self, nick):
|
|
||||||
if self._status == Room.CLOSED:
|
|
||||||
raise RoomClosed()
|
|
||||||
|
|
||||||
self.target_nick = nick
|
|
||||||
ptype, data, error, throttled = await self._send_while_connected(
|
|
||||||
"nick",
|
|
||||||
name=nick
|
|
||||||
)
|
|
||||||
|
|
||||||
sid = data.get("session_id")
|
|
||||||
uid = data.get("id")
|
|
||||||
from_nick = data.get("from")
|
|
||||||
to_nick = data.get("to")
|
|
||||||
|
|
||||||
self.session.nick = to_nick
|
|
||||||
return sid, uid, from_nick, to_nick
|
|
||||||
|
|
||||||
async def pm(self, uid):
|
|
||||||
if self._status == Room.CLOSED:
|
|
||||||
raise RoomClosed()
|
|
||||||
|
|
||||||
ptype, data, error, throttled = await self._send_while_connected(
|
|
||||||
"pm-initiate",
|
|
||||||
user_id=uid
|
|
||||||
)
|
|
||||||
|
|
||||||
# Just ignoring non-authenticated errors
|
|
||||||
pm_id = data.get("pm_id")
|
|
||||||
to_nick = data.get("to_nick")
|
|
||||||
return pm_id, to_nick
|
|
||||||
|
|
||||||
async def send(self, content, parent=None):
|
|
||||||
if parent:
|
|
||||||
ptype, data, error, throttled = await self._send_while_connected(
|
|
||||||
"send",
|
|
||||||
content=content,
|
|
||||||
parent=parent
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
ptype, data, error, throttled = await self._send_while_connected(
|
|
||||||
"send",
|
|
||||||
content=content
|
|
||||||
)
|
|
||||||
|
|
||||||
message = Message.from_dict(data)
|
|
||||||
self._last_known_mid = message.mid
|
|
||||||
return message
|
|
||||||
|
|
||||||
async def who(self):
|
|
||||||
ptype, data, error, throttled = await self._send_while_connected("who")
|
|
||||||
self.listing = Listing.from_dict(data.get("listing"))
|
|
||||||
self.listing.add(self.session)
|
|
||||||
|
|
||||||
# COMMUNICATION WITH CONNECTION
|
|
||||||
|
|
||||||
async def _disconnected(self):
|
|
||||||
# While disconnected, keep the last known session info, listing etc.
|
|
||||||
# All of this is instead reset when the hello/snapshot events are received.
|
|
||||||
logger.warn(f"&{self.roomname}:Lost connection.")
|
|
||||||
self.status = Room.DISCONNECTED
|
|
||||||
self._connected_future = asyncio.Future()
|
|
||||||
|
|
||||||
if self._forwarding is not None:
|
|
||||||
self._forwarding.cancel()
|
|
||||||
|
|
||||||
await self._inhabitant.on_disconnected(self)
|
|
||||||
|
|
||||||
async def _stopped(self):
|
|
||||||
await self._inhabitant.on_stopped(self)
|
|
||||||
|
|
||||||
async def _receive_packet(self, ptype, data, error, throttled):
|
|
||||||
# Ignoring errors and throttling for now
|
|
||||||
functions = {
|
|
||||||
"bounce-event": self._event_bounce,
|
|
||||||
#"disconnect-event": self._event_disconnect, # Not important, can ignore
|
|
||||||
"hello-event": self._event_hello,
|
|
||||||
"join-event": self._event_join,
|
|
||||||
#"login-event": self._event_login,
|
|
||||||
#"logout-event": self._event_logout,
|
|
||||||
"network-event": self._event_network,
|
|
||||||
"nick-event": self._event_nick,
|
|
||||||
"edit-message-event": self._event_edit_message,
|
|
||||||
"part-event": self._event_part,
|
|
||||||
"ping-event": self._event_ping,
|
|
||||||
"pm-initiate-event": self._event_pm_initiate,
|
|
||||||
"send-event": self._event_send,
|
|
||||||
"snapshot-event": self._event_snapshot,
|
|
||||||
}
|
|
||||||
|
|
||||||
function = functions.get(ptype)
|
|
||||||
if function:
|
|
||||||
await function(data)
|
|
||||||
|
|
||||||
async def _event_bounce(self, data):
|
|
||||||
logger.info(f"&{self.roomname}:Received bounce-event")
|
|
||||||
if self.password is not None:
|
|
||||||
try:
|
|
||||||
data = {"type": "passcode", "passcode": self.password}
|
|
||||||
ptype, rdata, error, throttled = await self._connection.send("auth", data=data)
|
|
||||||
success = rdata.get("success")
|
|
||||||
if not success:
|
|
||||||
reason = rdata.get("reason")
|
|
||||||
logger.warn(f"&{self.roomname}:Authentication failed: {reason}")
|
|
||||||
raise AuthenticationRequired(f"Could not join &{self.roomname}:{reason}")
|
|
||||||
else:
|
|
||||||
logger.info(f"&{self.roomname}:Authentication successful")
|
|
||||||
except ConnectionClosed:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
logger.warn(f"&{self.roomname}:Could not authenticate: Password unknown")
|
|
||||||
raise AuthenticationRequired(f"&{self.roomname} is password locked but no password was given")
|
|
||||||
|
|
||||||
async def _event_hello(self, 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)
|
|
||||||
|
|
||||||
self.listing.add(self.session)
|
|
||||||
|
|
||||||
async def _event_join(self, data):
|
|
||||||
session = Session.from_dict(data)
|
|
||||||
self.listing.add(session)
|
|
||||||
await self._inhabitant.on_join(self, session)
|
|
||||||
|
|
||||||
async def _event_network(self, data):
|
|
||||||
server_id = data.get("server_id")
|
|
||||||
server_era = data.get("server_era")
|
|
||||||
logger.debug(f"&{self.roomname}:Received network-event: server_id: {server_id!r}, server_era: {server_era!r}")
|
|
||||||
|
|
||||||
sessions = self.listing.remove_combo(server_id, server_era)
|
|
||||||
for session in sessions:
|
|
||||||
asyncio.ensure_future(self._inhabitant.on_part(self, session))
|
|
||||||
|
|
||||||
async def _event_nick(self, data):
|
|
||||||
sid = data.get("session_id")
|
|
||||||
uid = data.get("user_id")
|
|
||||||
from_nick = data.get("from")
|
|
||||||
to_nick = data.get("to")
|
|
||||||
|
|
||||||
session = self.listing.by_sid(sid)
|
|
||||||
if session:
|
|
||||||
session.nick = to_nick
|
|
||||||
|
|
||||||
await self._inhabitant.on_nick(self, sid, uid, from_nick, to_nick)
|
|
||||||
|
|
||||||
async def _event_edit_message(self, data):
|
|
||||||
message = Message.from_dict(data)
|
|
||||||
await self._inhabitant.on_edit(self, message)
|
|
||||||
|
|
||||||
async def _event_part(self, data):
|
|
||||||
session = Session.from_dict(data)
|
|
||||||
self.listing.remove(session.sid)
|
|
||||||
await self._inhabitant.on_part(self, session)
|
|
||||||
|
|
||||||
async def _event_ping(self, data):
|
|
||||||
try:
|
|
||||||
new_data = {"time": data.get("time")}
|
|
||||||
await self._connection.send( "ping-reply", data=new_data, await_response=False)
|
|
||||||
except ConnectionClosed:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def _event_pm_initiate(self, data):
|
|
||||||
from_uid = data.get("from")
|
|
||||||
from_nick = data.get("from_nick")
|
|
||||||
from_room = data.get("from_room")
|
|
||||||
pm_id = data.get("pm_id")
|
|
||||||
|
|
||||||
await self._inhabitant.on_pm(self, from_uid, from_nick, from_room, pm_id)
|
|
||||||
|
|
||||||
async def _event_send(self, data):
|
|
||||||
message = Message.from_dict(data)
|
|
||||||
|
|
||||||
if self._status == Room.FORWARDING:
|
|
||||||
logger.info(f"&{self.roomname}:Received new message while forwarding, adding to queue")
|
|
||||||
self._forward_new.append(message)
|
|
||||||
else:
|
|
||||||
self._last_known_mid = message.mid
|
|
||||||
await self._inhabitant.on_send(self, message)
|
|
||||||
|
|
||||||
# TODO: Figure out a way to bring fast-forwarding into this
|
|
||||||
|
|
||||||
async def _event_snapshot(self, data):
|
|
||||||
logger.debug(f"&{self.roomname}:Received snapshot-event, gained access to the room")
|
|
||||||
log = [Message.from_dict(m) for m in data.get("log")]
|
|
||||||
sessions = [Session.from_dict(d) for d in data.get("listing")]
|
|
||||||
|
|
||||||
# Update listing
|
|
||||||
self.listing = Listing()
|
|
||||||
for session in sessions:
|
|
||||||
self.listing.add(session)
|
|
||||||
self.listing.add(self.session)
|
|
||||||
|
|
||||||
# Update room info
|
|
||||||
self.pm_with_nick = data.get("pm_with_nick", None),
|
|
||||||
self.pm_with_user_id = data.get("pm_with_user_id", None)
|
|
||||||
self.session.nick = data.get("nick", None)
|
|
||||||
|
|
||||||
# Make sure a room is not CONNECTED without a nick
|
|
||||||
if self.target_nick and self.target_nick != self.session.nick:
|
|
||||||
logger.info(f"&{self.roomname}:Current nick doesn't match target nick {self.target_nick!r}, changing nick")
|
|
||||||
try:
|
|
||||||
_, nick_data, _, _ = await self._connection.send("nick", data={"name": self.target_nick})
|
|
||||||
self.session.nick = nick_data.get("to")
|
|
||||||
except ConnectionClosed:
|
|
||||||
return # Aww, we've lost connection again
|
|
||||||
|
|
||||||
# Now, we're finally connected again!
|
|
||||||
if self._last_known_mid is None:
|
|
||||||
logger.info(f"&{self.roomname}:Fully connected")
|
|
||||||
self._status = Room.CONNECTED
|
|
||||||
if log: # log goes from old to new
|
|
||||||
self._last_known_mid = log[-1].mid
|
|
||||||
else:
|
|
||||||
logger.info(f"&{self.roomname}:Not fully connected yet, starting message rewinding")
|
|
||||||
self._status = Room.FORWARDING
|
|
||||||
self._forward_new = []
|
|
||||||
|
|
||||||
if self._forwarding is not None:
|
|
||||||
self._forwarding.cancel()
|
|
||||||
self._forwarding = asyncio.ensure_future(self._forward(log))
|
|
||||||
|
|
||||||
if not self._connected_future.done(): # Should never be done already, I think
|
|
||||||
self._connected_future.set_result(None)
|
|
||||||
|
|
||||||
# Let's let the inhabitant know.
|
|
||||||
await self._inhabitant.on_connected(self, log)
|
|
||||||
|
|
||||||
# TODO: Figure out a way to bring fast-forwarding into this
|
|
||||||
# Should probably happen where this comment is
|
|
||||||
|
|
||||||
# SOME USEFUL PUBLIC METHODS
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_room_url(roomname, private=False, human=False):
|
|
||||||
if private:
|
|
||||||
roomname = f"pm:{roomname}"
|
|
||||||
|
|
||||||
url = f"wss://euphoria.io/room/{roomname}/ws"
|
|
||||||
|
|
||||||
if human:
|
|
||||||
url = f"{url}?h=1"
|
|
||||||
|
|
||||||
return url
|
|
||||||
|
|
||||||
async def connected(self):
|
|
||||||
await self._connected_future
|
|
||||||
|
|
||||||
# REST OF THE IMPLEMENTATION
|
|
||||||
|
|
||||||
async def _forward(self, log):
|
|
||||||
old_messages = []
|
|
||||||
while True:
|
|
||||||
found_last_known = True
|
|
||||||
for message in reversed(log):
|
|
||||||
if message.mid <= self._last_known_mid:
|
|
||||||
break
|
|
||||||
old_messages.append(message)
|
|
||||||
else:
|
|
||||||
found_last_known = False
|
|
||||||
|
|
||||||
if found_last_known:
|
|
||||||
break
|
|
||||||
|
|
||||||
log = await self.log(100, before=log[0].mid)
|
|
||||||
|
|
||||||
logger.info(f"&{self.roomname}:Reached last known message, forwarding through messages")
|
A Room can only be used once in the sense that after it has been closed,
|
||||||
for message in reversed(old_messages):
|
any further actions will result in a RoomClosedException. If you need to
|
||||||
self._last_known_mid = message.mid
|
manually reconnect, instead just create a new Room object.
|
||||||
asyncio.ensure_future(self._inhabitant.on_forward(self, message))
|
|
||||||
for message in self._forward_new:
|
|
||||||
self._last_known_mid = message.mid
|
|
||||||
asyncio.ensure_future(self._inhabitant.on_forward(self, message))
|
|
||||||
|
|
||||||
logger.info(f"&{self.roomname}:Forwarding complete, fully connected")
|
|
||||||
self._forward_new = []
|
|
||||||
self._status = Room.CONNECTED
|
|
||||||
|
|
||||||
async def _send_while_connected(self, *args, **kwargs):
|
|
||||||
while True:
|
|
||||||
if self._status == Room.CLOSED:
|
|
||||||
raise RoomClosed()
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.connected()
|
|
||||||
return await self._connection.send(*args, data=kwargs)
|
|
||||||
except ConnectionClosed:
|
|
||||||
pass # just try again
|
|
||||||
|
|
||||||
|
|
||||||
class Inhabitant:
|
|
||||||
"""
|
|
||||||
TODO
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ROOM EVENTS
|
Life cycle of a Room
|
||||||
# These functions are called by the room when something happens.
|
|
||||||
# They're launched via asyncio.ensure_future(), so they don't block execution of the room.
|
|
||||||
# Just overwrite the events you need (make sure to keep the arguments the same though).
|
|
||||||
|
|
||||||
async def on_created(self, room):
|
1. create a new Room and register callbacks
|
||||||
pass
|
2. await join()
|
||||||
|
3. do room-related stuff
|
||||||
|
4. await part()
|
||||||
|
|
||||||
async def on_connected(self, room, log):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_disconnected(self, room):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_stopped(self, room):
|
IN PHASE 1, a password and a starting nick can be set. The password and
|
||||||
pass
|
current nick are used when first connecting to the room, or when
|
||||||
|
reconnecting to the room after connection was lost.
|
||||||
|
|
||||||
async def on_join(self, room, session):
|
Usually, event callbacks are also registered during this phase.
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_part(self, room, session):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_nick(self, room, sid, uid, from_nick, to_nick):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_send(self, room, message):
|
IN PHASE 2, the Room creates the initial connection to euphoria and
|
||||||
pass
|
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:
|
||||||
|
|
||||||
async def on_forward(self, room, message):
|
1. the room is now in phase 3, in which case join() returns None
|
||||||
await self.on_send(room, message)
|
2. the room could not be joined, in which case one of the JoinExceptions is
|
||||||
|
returned
|
||||||
|
|
||||||
async def on_edit(self, room, message):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_pm(self, room, from_uid, from_nick, from_room, pm_id):
|
|
||||||
pass
|
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
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
room_name: str,
|
||||||
|
nick: str = None,
|
||||||
|
password: str = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.closed = False
|
||||||
|
|
||||||
|
# Phase 2
|
||||||
|
|
||||||
|
# Phase 3
|
||||||
|
|
||||||
|
def _ensure_open(self) -> None:
|
||||||
|
if self.closed:
|
||||||
|
raise RoomClosedException()
|
||||||
|
|
||||||
|
async def _ensure_joined(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _ensure(self) -> None:
|
||||||
|
self._ensure_open()
|
||||||
|
await self._ensure_joined()
|
||||||
|
|
||||||
|
# Phase 4
|
||||||
|
|
||||||
|
# Other stuff
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def say(self,
|
||||||
|
text: str,
|
||||||
|
parent_id: Optional[str] = None
|
||||||
|
) -> LiveMessage:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def users(self) -> List[LiveUser]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# retrieving messages
|
||||||
|
|
|
||||||
91
yaboli/user.py
Normal file
91
yaboli/user.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
from .util import mention, atmention
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
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
|
||||||
15
yaboli/util.py
Normal file
15
yaboli/util.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
__all__ = ["mention", "atmention", "normalize", "compare"]
|
||||||
|
|
||||||
|
# Name/nick related functions
|
||||||
|
|
||||||
|
def mention(name: str) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def atmention(name: str) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def normalize(name: str) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def compare(name_a: str, name_b: str) -> bool:
|
||||||
|
pass
|
||||||
225
yaboli/utils.py
225
yaboli/utils.py
|
|
@ -1,225 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import functools
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
__all__ = [
|
|
||||||
"parallel", "asyncify",
|
|
||||||
"mention", "normalize", "similar",
|
|
||||||
"format_time", "format_time_delta",
|
|
||||||
"Session", "PersonalAccountView", "Listing", "Message",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# alias for parallel message sending
|
|
||||||
parallel = asyncio.ensure_future
|
|
||||||
|
|
||||||
async def asyncify(func, *args, **kwargs):
|
|
||||||
func_with_args = functools.partial(func, *args, **kwargs)
|
|
||||||
return await asyncio.get_event_loop().run_in_executor(None, func_with_args)
|
|
||||||
|
|
||||||
def mention(nick, ping=True):
|
|
||||||
nick = re.sub(r"""[,.!?;&<'"\s]""", "", nick)
|
|
||||||
return "@" + nick if ping else nick
|
|
||||||
|
|
||||||
def normalize(nick):
|
|
||||||
return mention(nick, ping=False).lower()
|
|
||||||
|
|
||||||
def similar(nick1, nick2):
|
|
||||||
return normalize(nick1) == normalize(nick2)
|
|
||||||
|
|
||||||
def format_time(timestamp):
|
|
||||||
return time.strftime(
|
|
||||||
"%Y-%m-%d %H:%M:%S UTC",
|
|
||||||
time.gmtime(timestamp)
|
|
||||||
)
|
|
||||||
|
|
||||||
def format_time_delta(delta):
|
|
||||||
if delta < 0:
|
|
||||||
result = "-"
|
|
||||||
else:
|
|
||||||
result = ""
|
|
||||||
|
|
||||||
delta = int(delta)
|
|
||||||
|
|
||||||
second = 1
|
|
||||||
minute = second*60
|
|
||||||
hour = minute*60
|
|
||||||
day = hour*24
|
|
||||||
|
|
||||||
if delta >= day:
|
|
||||||
result += f"{delta//day}d "
|
|
||||||
delta = delta%day
|
|
||||||
|
|
||||||
if delta >= hour:
|
|
||||||
result += f"{delta//hour}h "
|
|
||||||
delta = delta%hour
|
|
||||||
|
|
||||||
if delta >= minute:
|
|
||||||
result += f"{delta//minute}m "
|
|
||||||
delta = delta%minute
|
|
||||||
|
|
||||||
result += f"{delta}s"
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
class Session:
|
|
||||||
def __init__(self, user_id, nick, server_id, server_era, session_id, is_staff=None,
|
|
||||||
is_manager=None, client_address=None, real_client_address=None):
|
|
||||||
self.user_id = user_id
|
|
||||||
self.nick = nick
|
|
||||||
self.server_id = server_id
|
|
||||||
self.server_era = server_era
|
|
||||||
self.session_id = session_id
|
|
||||||
self.is_staff = is_staff
|
|
||||||
self.is_manager = is_manager
|
|
||||||
self.client_address = client_address
|
|
||||||
self.real_client_address = real_client_address
|
|
||||||
|
|
||||||
@property
|
|
||||||
def uid(self):
|
|
||||||
return self.user_id
|
|
||||||
|
|
||||||
@uid.setter
|
|
||||||
def uid(self, new_uid):
|
|
||||||
self.user_id = new_uid
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sid(self):
|
|
||||||
return self.session_id
|
|
||||||
|
|
||||||
@sid.setter
|
|
||||||
def sid(self, new_sid):
|
|
||||||
self.session_id = new_sid
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, d):
|
|
||||||
return cls(
|
|
||||||
d.get("id"),
|
|
||||||
d.get("name"),
|
|
||||||
d.get("server_id"),
|
|
||||||
d.get("server_era"),
|
|
||||||
d.get("session_id"),
|
|
||||||
d.get("is_staff", None),
|
|
||||||
d.get("is_manager", None),
|
|
||||||
d.get("client_address", None),
|
|
||||||
d.get("real_client_address", None)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def client_type(self):
|
|
||||||
# account, agent or bot
|
|
||||||
return self.user_id.split(":")[0]
|
|
||||||
|
|
||||||
class PersonalAccountView:
|
|
||||||
def __init__(self, account_id, name, email):
|
|
||||||
self.account_id = account_id
|
|
||||||
self.name = name
|
|
||||||
self.email = email
|
|
||||||
|
|
||||||
@property
|
|
||||||
def aid(self):
|
|
||||||
return self.account_id
|
|
||||||
|
|
||||||
@aid.setter
|
|
||||||
def aid(self, new_aid):
|
|
||||||
self.account_id = new_aid
|
|
||||||
|
|
||||||
class Listing:
|
|
||||||
def __init__(self):
|
|
||||||
self._sessions = {}
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self._sessions)
|
|
||||||
|
|
||||||
def add(self, session):
|
|
||||||
self._sessions[session.session_id] = session
|
|
||||||
|
|
||||||
def remove(self, session_id):
|
|
||||||
self._sessions.pop(session_id)
|
|
||||||
|
|
||||||
def remove_combo(self, server_id, server_era):
|
|
||||||
removed = [ses for ses in self._sessions.items()
|
|
||||||
if ses.server_id == server_id and ses.server_era == server_era]
|
|
||||||
|
|
||||||
self._sessions = {i: ses for i, ses in self._sessions.items()
|
|
||||||
if ses.server_id != server_id and ses.server_era != server_era}
|
|
||||||
|
|
||||||
return removed
|
|
||||||
|
|
||||||
def by_sid(self, session_id):
|
|
||||||
return self._sessions.get(session_id);
|
|
||||||
|
|
||||||
def by_uid(self, user_id):
|
|
||||||
return [ses for ses in self._sessions if ses.user_id == user_id]
|
|
||||||
|
|
||||||
def get(self, types=["agent", "account", "bot"], lurker=None):
|
|
||||||
sessions = []
|
|
||||||
for uid, ses in self._sessions.items():
|
|
||||||
if ses.client_type not in types:
|
|
||||||
continue
|
|
||||||
|
|
||||||
is_lurker = not ses.nick # "" or None
|
|
||||||
if lurker is None or lurker == is_lurker:
|
|
||||||
sessions.append(ses)
|
|
||||||
|
|
||||||
return sessions
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, d):
|
|
||||||
listing = cls()
|
|
||||||
for session in d:
|
|
||||||
listing.add(Session.from_dict(session))
|
|
||||||
return listing
|
|
||||||
|
|
||||||
#def get_people(self):
|
|
||||||
#return self.get(types=["agent", "account"])
|
|
||||||
|
|
||||||
#def get_accounts(self):
|
|
||||||
#return self.get(types=["account"])
|
|
||||||
|
|
||||||
#def get_agents(self):
|
|
||||||
#return self.get(types=["agent"])
|
|
||||||
|
|
||||||
#def get_bots(self):
|
|
||||||
#return self.get(types=["bot"])
|
|
||||||
|
|
||||||
class Message():
|
|
||||||
def __init__(self, message_id, time, sender, content, parent=None, previous_edit_id=None,
|
|
||||||
encryption_key_id=None, edited=None, deleted=None, truncated=None):
|
|
||||||
self.message_id = message_id
|
|
||||||
self.time = time
|
|
||||||
self.sender = sender
|
|
||||||
self.content = content
|
|
||||||
self.parent = parent
|
|
||||||
self.previous_edit_id = previous_edit_id
|
|
||||||
self.encryption_key_id = encryption_key_id
|
|
||||||
self.edited = edited
|
|
||||||
self.deleted = deleted
|
|
||||||
self.truncated = truncated
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mid(self):
|
|
||||||
return self.message_id
|
|
||||||
|
|
||||||
@mid.setter
|
|
||||||
def mid(self, new_mid):
|
|
||||||
self.message_id = new_mid
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, d):
|
|
||||||
return cls(
|
|
||||||
d.get("id"),
|
|
||||||
d.get("time"),
|
|
||||||
Session.from_dict(d.get("sender")),
|
|
||||||
d.get("content"),
|
|
||||||
d.get("parent", None),
|
|
||||||
d.get("previous_edit_id", None),
|
|
||||||
d.get("encryption_key_id", None),
|
|
||||||
d.get("edited", None),
|
|
||||||
d.get("deleted", None),
|
|
||||||
d.get("truncated", None)
|
|
||||||
)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue