Make bot library functional
This commit is contained in:
parent
3eade77cf1
commit
1bb38fc836
9 changed files with 167 additions and 498 deletions
21
ExampleBot.py
Normal file
21
ExampleBot.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import asyncio
|
||||||
|
import yaboli
|
||||||
|
|
||||||
|
class ExampleBot(yaboli.Bot):
|
||||||
|
async def send(self, room, message):
|
||||||
|
ping = "ExamplePong!"
|
||||||
|
short_help = "Example bot for the yaboli bot library"
|
||||||
|
long_help = "I'm an example bot for the yaboli bot library, which can be found at https://github.com/Garmelon/yaboli"
|
||||||
|
|
||||||
|
await self.botrulez_ping_general(room, message, ping_text=ping)
|
||||||
|
await self.botrulez_ping_specific(room, message, ping_text=ping)
|
||||||
|
await self.botrulez_help_general(room, message, help_text=short_help)
|
||||||
|
await self.botrulez_help_specific(room, message, help_text=long_help)
|
||||||
|
await self.botrulez_uptime(room, message)
|
||||||
|
await self.botrulez_kill(room, message)
|
||||||
|
await self.botrulez_restart(room, message)
|
||||||
|
|
||||||
|
forward = send # should work without modifications for most bots
|
||||||
|
|
||||||
|
bot = ExampleBot("ExampleBot", "examplebot_cookies", rooms=["test", "welcome"])
|
||||||
|
asyncio.get_event_loop().run_forever()
|
||||||
10
setup.py
10
setup.py
|
|
@ -1,10 +0,0 @@
|
||||||
from setuptools import setup
|
|
||||||
|
|
||||||
setup(name='yaboli',
|
|
||||||
version='1.0',
|
|
||||||
description='Yet Another BOt LIbrary for euphoria.io',
|
|
||||||
author='Garmelon',
|
|
||||||
url='https://github.com/Garmelon/yaboli',
|
|
||||||
packages=['yaboli'],
|
|
||||||
install_requires=['websockets']
|
|
||||||
)
|
|
||||||
|
|
@ -10,6 +10,7 @@ logging.getLogger("asyncio").setLevel(logging.DEBUG)
|
||||||
logging.getLogger(__name__).setLevel(logging.DEBUG)
|
logging.getLogger(__name__).setLevel(logging.DEBUG)
|
||||||
# ----------- END DEV SECTION -----------
|
# ----------- END DEV SECTION -----------
|
||||||
|
|
||||||
|
from .bot import *
|
||||||
from .cookiejar import *
|
from .cookiejar import *
|
||||||
from .connection import *
|
from .connection import *
|
||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
|
|
@ -17,6 +18,7 @@ from .room import *
|
||||||
from .utils import *
|
from .utils import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
bot.__all__ +
|
||||||
connection.__all__ +
|
connection.__all__ +
|
||||||
cookiejar.__all__ +
|
cookiejar.__all__ +
|
||||||
exceptions.__all__ +
|
exceptions.__all__ +
|
||||||
|
|
|
||||||
350
yaboli/bot.py
350
yaboli/bot.py
|
|
@ -1,166 +1,110 @@
|
||||||
import asyncio
|
|
||||||
from collections import namedtuple
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from .callbacks import *
|
|
||||||
from .controller import *
|
from .cookiejar import *
|
||||||
|
from .room import *
|
||||||
from .utils import *
|
from .utils import *
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
__all__ = ["Bot"]
|
__all__ = ["Bot"]
|
||||||
|
|
||||||
|
|
||||||
|
# Some command stuff
|
||||||
|
|
||||||
class Bot(Controller):
|
SPECIFIC_RE = re.compile(r"!(\S+)\s+@(\S+)\s*([\S\s]*)")
|
||||||
# ^ and $ not needed since we're doing a re.fullmatch
|
GENERAL_RE = re.compile(r"!(\S+)\s*([\S\s]*)")
|
||||||
SPECIFIC_RE = r"!(\S+)\s+@(\S+)([\S\s]*)"
|
|
||||||
GENERIC_RE = r"!(\S+)([\S\s]*)"
|
|
||||||
|
|
||||||
ParsedMessage = namedtuple("ParsedMessage", ["command", "argstr"])
|
def command(commandname, specific=True, noargs=False):
|
||||||
TopicHelp = namedtuple("TopicHelp", ["text", "visible"])
|
def decorator(func):
|
||||||
|
async def wrapper(self, room, message, *args, **kwargs):
|
||||||
def __init__(self, nick):
|
print(f"New message: {message.content!r}, current name: {room.session!r}")
|
||||||
super().__init__(nick)
|
if specific:
|
||||||
|
print(f"Trying specific: {message.content!r}")
|
||||||
self.restarting = False # whoever runs the bot can check if a restart is necessary
|
result = self._parse_command(message.content, specific=room.session.nick)
|
||||||
self.start_time = time.time()
|
|
||||||
|
|
||||||
self._commands = Callbacks()
|
|
||||||
self._triggers = Callbacks()
|
|
||||||
self.register_default_commands()
|
|
||||||
|
|
||||||
self._help_topics = {}
|
|
||||||
self.add_default_help_topics()
|
|
||||||
|
|
||||||
# settings (modify in your bot's __init__)
|
|
||||||
self.help_general = None # None -> does not respond to general help
|
|
||||||
self.help_specific = "No help available"
|
|
||||||
self.killable = True
|
|
||||||
self.kill_message = "/me *poof*" # how to respond to !kill, whether killable or not
|
|
||||||
self.restartable = True
|
|
||||||
self.restart_message = "/me temporary *poof*" # how to respond to !restart, whether restartable or not
|
|
||||||
self.ping_message = "Pong!" # as specified by the botrulez
|
|
||||||
|
|
||||||
def register_command(self, command, callback, specific=True):
|
|
||||||
self._commands.add((command, specific), callback)
|
|
||||||
|
|
||||||
def register_trigger(self, regex, callback):
|
|
||||||
self._triggers.add(re.compile(regex), callback)
|
|
||||||
|
|
||||||
def register_trigger_compiled(self, comp_regex, callback):
|
|
||||||
self._triggers.add(comp_regex, callback)
|
|
||||||
|
|
||||||
def add_help(self, topic, text, visible=True):
|
|
||||||
info = self.TopicHelp(text, visible)
|
|
||||||
self._help_topics[topic] = info
|
|
||||||
|
|
||||||
def get_help(self, topic):
|
|
||||||
info = self._help_topics.get(topic, None)
|
|
||||||
if info:
|
|
||||||
return self.format_help(info.text)
|
|
||||||
|
|
||||||
def format_help(self, helptext):
|
|
||||||
return helptext.format(
|
|
||||||
nick=mention(self.nick)
|
|
||||||
)
|
|
||||||
|
|
||||||
def list_help_topics(self, max_characters=100):
|
|
||||||
# Magic happens here to ensure that the resulting lines are always
|
|
||||||
# max_characters or less characters long.
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
curline = ""
|
|
||||||
wrapper = None
|
|
||||||
|
|
||||||
for topic, info in sorted(self._help_topics.items()):
|
|
||||||
if not info.visible:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if wrapper:
|
|
||||||
curline += ","
|
|
||||||
lines.append(curline)
|
|
||||||
curline = wrapper
|
|
||||||
wrapper = None
|
|
||||||
|
|
||||||
if not curline:
|
|
||||||
curline = topic
|
|
||||||
elif len(curline) + len(f", {topic},") <= max_characters:
|
|
||||||
curline += f", {topic}"
|
|
||||||
elif len(curline) + len(f", {topic}") <= max_characters:
|
|
||||||
wrapper = topic
|
|
||||||
else:
|
else:
|
||||||
curline += ","
|
print(f"Trying general: {message.content!r}")
|
||||||
lines.append(curline)
|
result = self._parse_command(message.content)
|
||||||
curline = topic
|
if result is None: return
|
||||||
|
cmd, argstr = result
|
||||||
if wrapper:
|
if cmd != commandname: return
|
||||||
curline += ","
|
if noargs:
|
||||||
lines.append(curline)
|
if argstr: return
|
||||||
lines.append(wrapper)
|
return await func(self, room, message, *args, **kwargs)
|
||||||
elif curline:
|
else:
|
||||||
lines.append(curline)
|
return await func(self, room, message, args*args, **kwargs)
|
||||||
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
async def restart(self):
|
|
||||||
# After calling this, the bot is stopped, not yet restarted.
|
|
||||||
self.restarting = True
|
|
||||||
await self.stop()
|
|
||||||
|
|
||||||
def noargs(func):
|
|
||||||
async def wrapper(self, message, argstr):
|
|
||||||
if not argstr:
|
|
||||||
return await func(self, message)
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
async def on_send(self, message):
|
|
||||||
wait = []
|
|
||||||
|
|
||||||
# get specific command to call (if any)
|
# And now comes the real bot...
|
||||||
specific = self.parse_message(message.content, specific=True)
|
|
||||||
if specific:
|
|
||||||
wait.append(self._commands.call(
|
|
||||||
(specific.command, True),
|
|
||||||
message, specific.argstr
|
|
||||||
))
|
|
||||||
|
|
||||||
# get generic command to call (if any)
|
class Bot(Inhabitant):
|
||||||
general = self.parse_message(message.content, specific=False)
|
def __init__(self, nick, cookiefile=None, rooms=["test"]):
|
||||||
if general:
|
self.target_nick = nick
|
||||||
wait.append(self._commands.call(
|
self.rooms = {}
|
||||||
(general.command, False),
|
self.cookiejar = CookieJar(cookiefile)
|
||||||
message, general.argstr
|
|
||||||
))
|
|
||||||
|
|
||||||
# find triggers to call (if any)
|
for roomname in rooms:
|
||||||
for trigger in self._triggers.list():
|
self.join_room(roomname)
|
||||||
match = trigger.fullmatch(message.content)
|
|
||||||
if match:
|
|
||||||
wait.append(self._triggers.call(trigger, message, match))
|
|
||||||
|
|
||||||
if wait:
|
# ROOM MANAGEMENT
|
||||||
await asyncio.wait(wait)
|
|
||||||
|
|
||||||
def parse_message(self, content, specific=True):
|
def join_room(self, roomname, password=None):
|
||||||
|
if roomname in self.rooms:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.rooms[roomname] = Room(self, roomname, self.target_nick, password=password, cookiejar=self.cookiejar)
|
||||||
|
|
||||||
|
async def part_room(self, roomname):
|
||||||
|
room = self.rooms.pop(roomname, None)
|
||||||
|
if room:
|
||||||
|
await room.exit()
|
||||||
|
|
||||||
|
# BOTRULEZ
|
||||||
|
|
||||||
|
@command("ping", specific=False, noargs=True)
|
||||||
|
async def botrulez_ping_general(self, room, message, ping_text="Pong!"):
|
||||||
|
await room.send(ping_text, message.mid)
|
||||||
|
|
||||||
|
@command("ping", specific=True, noargs=True)
|
||||||
|
async def botrulez_ping_specific(self, room, message, ping_text="Pong!"):
|
||||||
|
await room.send(ping_text, message.mid)
|
||||||
|
|
||||||
|
@command("help", specific=False, noargs=True)
|
||||||
|
async def botrulez_help_general(self, room, message, help_text="Placeholder help text"):
|
||||||
|
await room.send(help_text, message.mid)
|
||||||
|
|
||||||
|
@command("help", specific=True, noargs=True)
|
||||||
|
async def botrulez_help_specific(self, room, message, help_text="Placeholder help text"):
|
||||||
|
await room.send(help_text, message.mid)
|
||||||
|
|
||||||
|
@command("uptime", specific=True, noargs=True)
|
||||||
|
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", specific=True, noargs=True)
|
||||||
|
async def botrulez_kill(self, room, message, kill_text="/me dies"):
|
||||||
|
await room.send(kill_text, message.mid)
|
||||||
|
await self.part_room(room.roomname)
|
||||||
|
|
||||||
|
@command("restart", specific=True, noargs=True)
|
||||||
|
async def botrulez_restart(self, room, message, restart_text="/me restarts"):
|
||||||
|
await room.send(restart_text, message.mid)
|
||||||
|
await self.part_room(room.roomname)
|
||||||
|
self.join_room(room.roomname, password=room.password)
|
||||||
|
|
||||||
|
# COMMAND PARSING
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_args(text):
|
||||||
"""
|
"""
|
||||||
ParsedMessage = parse_message(content)
|
Use bash-style single- and double-quotes to include whitespace in arguments.
|
||||||
|
|
||||||
Returns None, not a (None, None) tuple, when message could not be parsed
|
|
||||||
"""
|
|
||||||
|
|
||||||
if specific:
|
|
||||||
match = re.fullmatch(self.SPECIFIC_RE, content)
|
|
||||||
if match and similar(match.group(2), self.nick):
|
|
||||||
return self.ParsedMessage(match.group(1), match.group(3))
|
|
||||||
else:
|
|
||||||
match = re.fullmatch(self.GENERIC_RE, content)
|
|
||||||
if match:
|
|
||||||
return self.ParsedMessage(match.group(1), match.group(2))
|
|
||||||
|
|
||||||
def parse_args(self, text):
|
|
||||||
"""
|
|
||||||
Use single- and double-quotes bash-style to include whitespace in arguments.
|
|
||||||
A backslash always escapes the next character.
|
A backslash always escapes the next character.
|
||||||
Any non-escaped whitespace separates arguments.
|
Any non-escaped whitespace separates arguments.
|
||||||
|
|
||||||
|
|
@ -201,7 +145,8 @@ class Bot(Controller):
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def parse_flags(self, arglist):
|
@staticmethod
|
||||||
|
def parse_flags(arglist):
|
||||||
flags = ""
|
flags = ""
|
||||||
args = []
|
args = []
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
@ -225,109 +170,16 @@ class Bot(Controller):
|
||||||
|
|
||||||
return flags, args, kwargs
|
return flags, args, kwargs
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_command(content, specific=None):
|
||||||
# BOTRULEZ AND YABOLI-SPECIFIC COMMANDS
|
print(repr(specific))
|
||||||
|
if specific is not None:
|
||||||
def register_default_commands(self):
|
print("SPECIFIC")
|
||||||
self.register_command("ping", self.command_ping)
|
match = SPECIFIC_RE.fullmatch(content)
|
||||||
self.register_command("ping", self.command_ping, specific=False)
|
if match and similar(match.group(2), specific):
|
||||||
self.register_command("help", self.command_help)
|
return match.group(1), match.group(3)
|
||||||
self.register_command("help", self.command_help_general, specific=False)
|
|
||||||
self.register_command("uptime", self.command_uptime)
|
|
||||||
self.register_command("kill", self.command_kill)
|
|
||||||
self.register_command("restart", self.command_restart)
|
|
||||||
|
|
||||||
def add_default_help_topics(self):
|
|
||||||
self.add_help("botrulez", (
|
|
||||||
"This bot complies with the botrulez at https://github.com/jedevc/botrulez.\n"
|
|
||||||
"It implements the standard commands, and additionally !kill and !restart.\n\n"
|
|
||||||
"Standard commands:\n"
|
|
||||||
" !ping, !ping @{nick} - reply with a short pong message\n"
|
|
||||||
" !help, !help @{nick} - reply with help about the bot\n"
|
|
||||||
" !uptime @{nick} - reply with the bot's uptime\n\n"
|
|
||||||
"Non-standard commands:\n"
|
|
||||||
" !kill @{nick} - terminate this bot instance\n"
|
|
||||||
" !restart @{nick} - restart this bot instance\n\n"
|
|
||||||
"Command extensions:\n"
|
|
||||||
" !help @{nick} <topic> [<topic> ...] - provide help on the topics listed"
|
|
||||||
))
|
|
||||||
|
|
||||||
self.add_help("yaboli", (
|
|
||||||
"Yaboli is \"Yet Another BOt LIbrary for euphoria\", written by @Garmy in Python.\n"
|
|
||||||
"It relies heavily on the asyncio module from the standard library and uses f-strings.\n"
|
|
||||||
"Because of this, Python version >= 3.6 is required.\n\n"
|
|
||||||
"Github: https://github.com/Garmelon/yaboli"
|
|
||||||
))
|
|
||||||
|
|
||||||
@noargs
|
|
||||||
async def command_ping(self, message):
|
|
||||||
if self.ping_message:
|
|
||||||
await self.room.send(self.ping_message, message.mid)
|
|
||||||
|
|
||||||
async def command_help(self, message, argstr):
|
|
||||||
args = self.parse_args(argstr.lower())
|
|
||||||
if not args:
|
|
||||||
if self.help_specific:
|
|
||||||
await self.room.send(
|
|
||||||
self.format_help(self.help_specific),
|
|
||||||
message.mid
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# collect all valid topics
|
print("GENERAL")
|
||||||
messages = []
|
match = GENERAL_RE.fullmatch(content)
|
||||||
for topic in sorted(set(args)):
|
if match:
|
||||||
text = self.get_help(topic)
|
return match.group(1), match.group(2)
|
||||||
if text:
|
|
||||||
messages.append(f"Topic: {topic}\n{text}")
|
|
||||||
|
|
||||||
# print result in separate messages
|
|
||||||
if messages:
|
|
||||||
for text in messages:
|
|
||||||
await self.room.send(text, message.mid)
|
|
||||||
else:
|
|
||||||
await self.room.send("None of those topics found.", message.mid)
|
|
||||||
|
|
||||||
@noargs
|
|
||||||
async def command_help_general(self, message):
|
|
||||||
if self.help_general is not None:
|
|
||||||
await self.room.send(self.help_general, message.mid)
|
|
||||||
|
|
||||||
@noargs
|
|
||||||
async def command_uptime(self, message):
|
|
||||||
now = time.time()
|
|
||||||
startformat = format_time(self.start_time)
|
|
||||||
deltaformat = format_time_delta(now - self.start_time)
|
|
||||||
text = f"/me has been up since {startformat} ({deltaformat})"
|
|
||||||
await self.room.send(text, message.mid)
|
|
||||||
|
|
||||||
async def command_kill(self, message, args):
|
|
||||||
logging.warn(f"Kill attempt by @{mention(message.sender.nick)} in &{self.room.roomname}: {message.content!r}")
|
|
||||||
|
|
||||||
if self.kill_message is not None:
|
|
||||||
await self.room.send(self.kill_message, message.mid)
|
|
||||||
|
|
||||||
if self.killable:
|
|
||||||
await self.stop()
|
|
||||||
|
|
||||||
async def command_restart(self, message, args):
|
|
||||||
logging.warn(f"Restart attempt by @{mention(message.sender.nick)} in &{self.room.roomname}: {message.content!r}")
|
|
||||||
|
|
||||||
if self.restart_message is not None:
|
|
||||||
await self.room.send(self.restart_message, message.mid)
|
|
||||||
|
|
||||||
if self.restartable:
|
|
||||||
await self.restart()
|
|
||||||
|
|
||||||
class Multibot(Bot):
|
|
||||||
def __init__(self, nick, keeper):
|
|
||||||
super().__init__(nick)
|
|
||||||
|
|
||||||
self.keeper = keeper
|
|
||||||
|
|
||||||
class MultibotKeeper():
|
|
||||||
def __init__(self, configfile):
|
|
||||||
# TODO: load configfile
|
|
||||||
|
|
||||||
# TODO: namedtuple botinfo (bot, task)
|
|
||||||
self._bots = {} # self._bots[roomname] = botinfo
|
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ class Connection:
|
||||||
if self.cookiejar:
|
if self.cookiejar:
|
||||||
for set_cookie in self._ws.response_headers.get_all("Set-Cookie"):
|
for set_cookie in self._ws.response_headers.get_all("Set-Cookie"):
|
||||||
self.cookiejar.bake(set_cookie)
|
self.cookiejar.bake(set_cookie)
|
||||||
|
self.cookiejar.save()
|
||||||
|
|
||||||
self._pingtask = asyncio.ensure_future(self._ping())
|
self._pingtask = asyncio.ensure_future(self._ping())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,213 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
from .room import Room
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
__all__ = ["Controller"]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Controller:
|
|
||||||
"""
|
|
||||||
Callback order:
|
|
||||||
|
|
||||||
on_start - self.room not available
|
|
||||||
while running:
|
|
||||||
<connects to room>
|
|
||||||
on_ping - always possible (until on_disconnected)
|
|
||||||
on_bounce - self.room only session
|
|
||||||
on_hello - self.room only session
|
|
||||||
<authenticated, access to room>
|
|
||||||
on_connected - self.room session and chat room (fully connected)
|
|
||||||
on_snapshot - self.room session and chat room
|
|
||||||
<other callbacks> - self.room session and chat room
|
|
||||||
<leaving room>
|
|
||||||
on_disconnected - self.room not connected to room any more
|
|
||||||
on_stop - self.room not available
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, nick, human=False, cookie=None, connect_timeout=10):
|
|
||||||
"""
|
|
||||||
roomname - name of room to connect to
|
|
||||||
human - whether the human flag should be set on connections
|
|
||||||
cookie - cookie to use in HTTP request, if any
|
|
||||||
connect_timeout - time for authentication to complete
|
|
||||||
"""
|
|
||||||
self.nick = nick
|
|
||||||
self.human = human
|
|
||||||
self.cookie = cookie
|
|
||||||
|
|
||||||
self.roomname = "test"
|
|
||||||
self.password = None
|
|
||||||
|
|
||||||
self.room = None
|
|
||||||
self.connect_timeout = connect_timeout # in seconds
|
|
||||||
self._connect_result = None
|
|
||||||
|
|
||||||
def _create_room(self, roomname):
|
|
||||||
return Room(roomname, self, human=self.human, cookie=self.cookie)
|
|
||||||
|
|
||||||
def _set_connect_result(self, result):
|
|
||||||
logger.debug(f"Attempting to set connect result to {result}")
|
|
||||||
if self._connect_result and not self._connect_result.done():
|
|
||||||
logger.debug(f"Setting connect result to {result}")
|
|
||||||
self._connect_result.set_result(result)
|
|
||||||
|
|
||||||
async def connect(self, roomname, password=None, timeout=10):
|
|
||||||
"""
|
|
||||||
task, reason = await connect(roomname, password=None, timeout=10)
|
|
||||||
|
|
||||||
Connect to a room and authenticate, if necessary.
|
|
||||||
|
|
||||||
roomname - name of the room to connect to
|
|
||||||
password - password for the room, if needed
|
|
||||||
timeout - wait this long for a reply from the server
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
task - the task running the bot, or None on failure
|
|
||||||
reason - the reason for failure
|
|
||||||
"no room" = could not establish connection, room doesn't exist
|
|
||||||
"auth option" = can't authenticate with a password
|
|
||||||
"no password" = password needed to connect to room
|
|
||||||
"wrong password" = password given does not work
|
|
||||||
"disconnected" = connection closed before client could access the room
|
|
||||||
"timeout" = timed out while waiting for server
|
|
||||||
"success" = no failure
|
|
||||||
"""
|
|
||||||
|
|
||||||
logger.info(f"Attempting to connect to &{roomname}")
|
|
||||||
|
|
||||||
# make sure nothing is running any more
|
|
||||||
try:
|
|
||||||
await self.stop()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
logger.error("Calling connect from the controller itself.")
|
|
||||||
raise
|
|
||||||
|
|
||||||
self.password = password
|
|
||||||
self.room = self._create_room(roomname)
|
|
||||||
|
|
||||||
# prepare for if connect() is successful
|
|
||||||
self._connect_result = asyncio.Future()
|
|
||||||
|
|
||||||
# attempt to connect to the room
|
|
||||||
task = await self.room.connect()
|
|
||||||
if not task:
|
|
||||||
logger.warn(f"Could not connect to &{roomname}.")
|
|
||||||
self.room = None
|
|
||||||
return None, "no room"
|
|
||||||
|
|
||||||
# connection succeeded, now we need to know whether we can log in
|
|
||||||
# wait for success/authentication/disconnect
|
|
||||||
try:
|
|
||||||
await asyncio.wait_for(self._connect_result, self.connect_timeout)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
result = "timeout"
|
|
||||||
else:
|
|
||||||
result = self._connect_result.result()
|
|
||||||
|
|
||||||
logger.debug(f"&{roomname}._connect_result: {result!r}")
|
|
||||||
|
|
||||||
# deal with result
|
|
||||||
if result == "success":
|
|
||||||
logger.info(f"Successfully connected to &{roomname}.")
|
|
||||||
return task, result
|
|
||||||
else: # not successful for some reason
|
|
||||||
logger.warn(f"Could not join &{roomname}: {result!r}")
|
|
||||||
await self.stop()
|
|
||||||
return None, result
|
|
||||||
|
|
||||||
async def stop(self):
|
|
||||||
if self.room:
|
|
||||||
logger.info(f"@{self.nick}: Stopping")
|
|
||||||
await self.room.stop()
|
|
||||||
logger.debug(f"@{self.nick}: Stopped. Deleting room")
|
|
||||||
self.room = None
|
|
||||||
|
|
||||||
async def set_nick(self, nick):
|
|
||||||
if nick != self.nick:
|
|
||||||
_, _, _, to_nick = await self.room.nick(nick)
|
|
||||||
self.nick = to_nick
|
|
||||||
|
|
||||||
if to_nick != nick:
|
|
||||||
logger.warn(f"&{self.room.roomname}: Could not set nick to {nick!r}, set to {to_nick!r} instead.")
|
|
||||||
|
|
||||||
async def on_connected(self):
|
|
||||||
"""
|
|
||||||
Client has successfully (re-)joined the room.
|
|
||||||
|
|
||||||
Use: Actions that are meant to happen upon (re-)connecting to a room,
|
|
||||||
such as resetting the message history.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._set_connect_result("success")
|
|
||||||
|
|
||||||
async def on_disconnected(self):
|
|
||||||
"""
|
|
||||||
Client has disconnected from the room.
|
|
||||||
|
|
||||||
This is the last time the old self.room can be accessed.
|
|
||||||
Use: Reconfigure self before next connection.
|
|
||||||
Need to store information from old room?
|
|
||||||
"""
|
|
||||||
|
|
||||||
logger.debug(f"on_disconnected: self.room is {self.room}")
|
|
||||||
self._set_connect_result("disconnected")
|
|
||||||
|
|
||||||
async def on_bounce(self, reason=None, auth_options=[], agent_id=None, ip=None):
|
|
||||||
if "passcode" not in auth_options:
|
|
||||||
self._set_connect_result("auth option")
|
|
||||||
elif self.password is None:
|
|
||||||
self._set_connect_result("no password")
|
|
||||||
else:
|
|
||||||
success, reason = await self.room.auth("passcode", passcode=self.password)
|
|
||||||
if not success:
|
|
||||||
self._set_connect_result("wrong password")
|
|
||||||
|
|
||||||
async def on_disconnect(self, reason):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_hello(self, user_id, session, room_is_private, version, account=None,
|
|
||||||
account_has_access=None, account_email_verified=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_join(self, session):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_login(self, account_id):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_logout(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_network(self, ntype, server_id, server_era):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_nick(self, session_id, user_id, from_nick, to_nick):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_edit_message(self, edit_id, message):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_part(self, session):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_ping(self, ptime, pnext):
|
|
||||||
"""
|
|
||||||
Default implementation, refer to api.euphoria.io
|
|
||||||
"""
|
|
||||||
|
|
||||||
logger.debug(f"&{self.room.roomname}: Pong!")
|
|
||||||
await self.room.ping_reply(ptime)
|
|
||||||
|
|
||||||
async def on_pm_initiate(self, from_id, from_nick, from_room, pm_id):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_send(self, message):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_snapshot(self, user_id, session_id, version, sessions, messages, nick=None,
|
|
||||||
pm_with_nick=None, pm_with_user_id=None):
|
|
||||||
if nick != self.nick:
|
|
||||||
await self.room.nick(self.nick)
|
|
||||||
|
|
@ -12,10 +12,14 @@ class CookieJar:
|
||||||
Keeps your cookies in a file.
|
Keeps your cookies in a file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, filename):
|
def __init__(self, filename=None):
|
||||||
self._filename = filename
|
self._filename = filename
|
||||||
self._cookies = cookies.SimpleCookie()
|
self._cookies = cookies.SimpleCookie()
|
||||||
|
|
||||||
|
if not self._filename:
|
||||||
|
logger.info("Could not load cookies, no filename given.")
|
||||||
|
return
|
||||||
|
|
||||||
with contextlib.suppress(FileNotFoundError):
|
with contextlib.suppress(FileNotFoundError):
|
||||||
with open(self._filename, "r") as f:
|
with open(self._filename, "r") as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
|
|
@ -45,6 +49,10 @@ class CookieJar:
|
||||||
Saves all current cookies to the cookie jar file.
|
Saves all current cookies to the cookie jar file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if not self._filename:
|
||||||
|
logger.info("Could not save cookies, no filename given.")
|
||||||
|
return
|
||||||
|
|
||||||
logger.debug(f"Saving cookies to {self._filename!r}")
|
logger.debug(f"Saving cookies to {self._filename!r}")
|
||||||
|
|
||||||
with open(self._filename, "w") as f:
|
with open(self._filename, "w") as f:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
from .connection import *
|
from .connection import *
|
||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
|
|
@ -19,12 +20,13 @@ class Room:
|
||||||
DISCONNECTED = 2
|
DISCONNECTED = 2
|
||||||
CLOSED = 3
|
CLOSED = 3
|
||||||
|
|
||||||
def __init__(self, roomname, inhabitant, password=None, human=False, cookiejar=None):
|
def __init__(self, inhabitant, roomname, nick, password=None, human=False, cookiejar=None):
|
||||||
# TODO: Connect to room etc.
|
# TODO: Connect to room etc.
|
||||||
# TODO: Deal with room/connection states of:
|
# TODO: Deal with room/connection states of:
|
||||||
# disconnected connecting, fast-forwarding, connected
|
# disconnected connecting, fast-forwarding, connected
|
||||||
|
|
||||||
# Room info (all fields readonly!)
|
# Room info (all fields readonly!)
|
||||||
|
self.target_nick = nick
|
||||||
self.roomname = roomname
|
self.roomname = roomname
|
||||||
self.password = password
|
self.password = password
|
||||||
self.human = human
|
self.human = human
|
||||||
|
|
@ -33,6 +35,8 @@ class Room:
|
||||||
self.account = None
|
self.account = None
|
||||||
self.listing = Listing()
|
self.listing = Listing()
|
||||||
|
|
||||||
|
self.start_time = time.time()
|
||||||
|
|
||||||
self.account_has_access = None
|
self.account_has_access = None
|
||||||
self.account_email_verified = None
|
self.account_email_verified = None
|
||||||
self.room_is_private = None
|
self.room_is_private = None
|
||||||
|
|
@ -103,6 +107,8 @@ class Room:
|
||||||
uid = data.get("id")
|
uid = data.get("id")
|
||||||
from_nick = data.get("from")
|
from_nick = data.get("from")
|
||||||
to_nick = data.get("to")
|
to_nick = data.get("to")
|
||||||
|
|
||||||
|
self.session.nick = to_nick
|
||||||
return sid, uid, from_nick, to_nick
|
return sid, uid, from_nick, to_nick
|
||||||
|
|
||||||
async def pm(self, uid):
|
async def pm(self, uid):
|
||||||
|
|
@ -260,15 +266,13 @@ class Room:
|
||||||
# Update room info
|
# Update room info
|
||||||
self.pm_with_nick = data.get("pm_with_nick", None),
|
self.pm_with_nick = data.get("pm_with_nick", None),
|
||||||
self.pm_with_user_id = data.get("pm_with_user_id", None)
|
self.pm_with_user_id = data.get("pm_with_user_id", None)
|
||||||
|
self.session.nick = data.get("nick", None)
|
||||||
|
|
||||||
# Remember old nick, because we're going to try to get it back
|
# Make sure a room is not CONNECTED without a nick
|
||||||
old_nick = self.session.nick if self.session else None
|
if self.target_nick and self.target_nick != self.session.nick:
|
||||||
new_nick = data.get("nick", None)
|
|
||||||
self.session.nick = new_nick
|
|
||||||
|
|
||||||
if old_nick and old_nick != new_nick:
|
|
||||||
try:
|
try:
|
||||||
await self._connection.send("nick", data={"name": old_nick})
|
_, nick_data, _, _ = await self._connection.send("nick", data={"name": self.target_nick})
|
||||||
|
self.session.nick = nick_data.get("to")
|
||||||
except ConnectionClosed:
|
except ConnectionClosed:
|
||||||
return # Aww, we've lost connection again
|
return # Aww, we've lost connection again
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,16 @@ import asyncio
|
||||||
import time
|
import time
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"parallel",
|
||||||
"mention", "mention_reduced", "similar",
|
"mention", "mention_reduced", "similar",
|
||||||
"format_time", "format_time_delta",
|
"format_time", "format_time_delta",
|
||||||
"Session", "Listing", "Message",
|
"Session", "Listing", "Message",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# alias for parallel message sending
|
||||||
|
parallel = asyncio.ensure_future
|
||||||
|
|
||||||
def mention(nick):
|
def mention(nick):
|
||||||
return "".join(c for c in nick if c not in ".!?;&<'\"" and not c.isspace())
|
return "".join(c for c in nick if c not in ".!?;&<'\"" and not c.isspace())
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue