diff --git a/chatlib.py b/chatlib.py deleted file mode 100644 index 0471b44..0000000 --- a/chatlib.py +++ /dev/null @@ -1,68 +0,0 @@ -import aiohttp as ah -import time -import asyncio -import datetime -import traceback -from dataclasses import dataclass -from asyncio import iscoroutinefunction - -func=type(lambda:1) # Gross, but I can't actually find it. -# And yes, lamdas are too. - -tracing=False -def trace(*msg): - if tracing: print(datetime.datetime.now(),*msg) - -event_queue=asyncio.Queue() #Contains Events. Everything in here must be an Event. -listeners=[] # These respond to events. They're executed by workers processing the event queue. They should all be Listeners -timers=[] # These just execute on a schedule. That schedule can be "as soon as it exits". Should all be Timers, which include the property of whether to wait for exit before starting the next call or not (rarely relevant, so defaults to "no"). - -def addlistener(name=None,match=None): - if isinstance(name,func) and match==None: match=name; name=None - def __wrap__(funky): - self.listeners.append(Listener(name,match,funky)) - return funky - -def loop(timer=0): - def _loop(funky): - @functools.wraps(funky) - async def _wrapper(*args,**kwargs): - while True: - try: await funky(*args,**kwargs) - except Exception: open('error '+str(datetime.datetime.now()),'w').write(traceback.format_exc()) - await asyncio.sleep(timer) - timers.append(_wrapper) - return _wrapper - return _loop - -async def process_queue(): - item=await event_queue.get(): - for listener in listeners: - if listener==item: - await listener(item) - -@dataclass -class Event(): - event_type:str - data:Object=None - raw_data:dict=None - outbound:bool=False - -class Listener(): - def __init__(self,name,match,function): - self.name=name or function.__name__ - self.match=match - self._matchstr=isinstance(match,str) - self.function=function - self._async=iscoroutinefunction(function) - - async def __call__(self,*args,**kwargs): - if not self._async: return self.function(*args,**kwargs) - return await self.function(*args,**kwargs) - - def __eq__(self,other): - if isinstance(other,Event): - if self._matchstr: return self.match==other.event_type - return self.match(other) # If it's not a string, assume it's a callable. - else: return super.__eq__(self,other) - def __str__(self): return self.name \ No newline at end of file diff --git a/chatlib/chatlib.py b/chatlib/chatlib.py new file mode 100644 index 0000000..e111c42 --- /dev/null +++ b/chatlib/chatlib.py @@ -0,0 +1,53 @@ +import asyncio +from traceback import format_exc +from .listener import Listener +from .timer import Timer +from . import utils +from .event import Event + +event_queue=asyncio.Queue() #Contains Events. Everything in here must be an Event. +listeners=[] # These respond to events. They're executed by workers processing the event queue. They should all be Listeners +timers=[] # These just execute on a schedule. That schedule can be "as soon as it exits". Should all be Timers, which include the property of whether to wait for exit before starting the next call or not (rarely relevant, so defaults to "no" unless schedule is 0). + + +# Queue management +async def process_queue(): + while True: + item=await event_queue.get() + asyncio.create_task(process_item(item)) + +async def process_item(item): + for listener in listeners: + terminate=False + if listener==item: + try: terminate=await listener(item) + except: utils.trace('explosions in',listener); format_exc() + if terminate: break + +# Convenience functions for initialising. +def addlistener(match): + def __wrap__(funky): + listeners.append(Listener(match,funky)) + return funky + return __wrap__ + +def addtimer(interval,start=None,block=None): + def __wrap__(funky): + a=Timer(funky,interval,block,start) + timers.append(a) + try: # Timers added after start will get run here, but timers added before start won't + asyncio.create_task(a.run()) + except RuntimeError: pass + return funky + return __wrap__ + +def addevent(*args,**kwargs): + asyncio.create_task(event_queue.put(Event(*args,**kwargs))) + +async def main(): + # Any time that didn't get run before occurs here. + for timer in timers: asyncio.create_task(timer.run()) + await process_queue() + +def run(): + asyncio.run(main()) \ No newline at end of file diff --git a/chatlib/event.py b/chatlib/event.py new file mode 100644 index 0000000..2865e43 --- /dev/null +++ b/chatlib/event.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + +@dataclass +class Event(): + event_type:str='' + data:object=None + raw_data:dict=None + outbound:bool=False # This should be remapped as a data piece so it could instead be a direction/destination \ No newline at end of file diff --git a/chatlib/listener.py b/chatlib/listener.py new file mode 100644 index 0000000..d00e890 --- /dev/null +++ b/chatlib/listener.py @@ -0,0 +1,26 @@ +from asyncio import iscoroutinefunction +from .event import Event + +class Listener(): + def __init__(self,match,function): + self.match=match + self.function=function + + @property + def matchstr(self): + return isinstance(self.match,str) + @property + def isasync(self): + return iscoroutinefunction(self.function) + + async def __call__(self,*args,**kwargs): + if not self.isasync: return self.function(*args,**kwargs) + return await self.function(*args,**kwargs) + + def __eq__(self,other): + if isinstance(other,Event): + if self.matchstr: return self.match==other.event_type + return self.match(other) # If it's not a string, assume it's a callable. + else: return super.__eq__(self,other) + + def __str__(self): return self.function.__name_ \ No newline at end of file diff --git a/chatlib/timer.py b/chatlib/timer.py new file mode 100644 index 0000000..15893ab --- /dev/null +++ b/chatlib/timer.py @@ -0,0 +1,54 @@ +from traceback import format_exc +from asyncio import sleep,create_task +from datetime import datetime +from asyncio import iscoroutinefunction,get_event_loop +from . import utils + +class Timer(): + def __init__(self,function,interval,block=None,start=None,*args,**kwargs): + self.function=function + self.interval=interval + self.args=args + self.kwargs=kwargs + self.block=block + if block is None: self.block=(interval == 0) + self.start=start + self.scheduled=None + + @property + def isasync(self): + return iscoroutinefunction(self.function) + + """ + This mess of functions... + Operate and enqueue are just wrappers that do some useful things that I couldn't fit elsewhere. Iterate is the core that runs them in the right order according to blocking. + """ + async def enqueue(self,interval=None): + self.scheduled=create_task(self.iterate(interval)) + + async def operate(self,*args,**kwargs): + if not args: args=self.args + if not kwargs: kwargs=self.kwargs + try: + if not self.isasync: return self.function(*args,**kwargs) + return await self.function(*args,**kwargs) + except: utils.trace('explosions in',self); format_exc() + + async def iterate(self,interval=None): + if interval is None: interval=self.interval + await sleep(interval) + await (self.operate,self.enqueue)[not self.block]() + await (self.operate,self.enqueue)[self.block]() + + async def run(self): + slop=0 + if self.start is not None: + ima=datetime.utcnow() + slop=(self.start-ima).total_seconds() # It'll be very slightly wrong but whatever + await self.enqueue(slop) + + def __str__(self): + return self.function.__name__ + + def __del__(self): + self.scheduled.cancel() \ No newline at end of file diff --git a/chatlib/utils.py b/chatlib/utils.py new file mode 100644 index 0000000..c8c61cb --- /dev/null +++ b/chatlib/utils.py @@ -0,0 +1,5 @@ +from datetime import datetime + +tracing=False +def trace(*msg): + if tracing: print(datetime.now(),*msg) \ No newline at end of file diff --git a/listeners.py b/matrix/inbound.py similarity index 70% rename from listeners.py rename to matrix/inbound.py index 3cd6227..7b300c3 100644 --- a/listeners.py +++ b/matrix/inbound.py @@ -10,10 +10,11 @@ from .chatlib import Listener as _l, Event as _e def sync_f(client,event): blob=event.raw_data # This event never really gets any processed data in it. - rooms=blob['rooms']['join'] - for rid,room in rooms.items(): - client.event_queue.add(_e('m.room',{'id':rid},room)) -sync=_l('sync','m.sync',sync_f) + for status in ['join','leave','invite']: + roomlist=blob.get('rooms',{status:{}}).get(status,{}) + for rid,room in roomlist.items(): + client.event_queue.add(_e('m.room',{'id':rid,'state':status},room)) +sync=_l('m.sync',sync_f) def room_f(client,event): blob=event.raw_data @@ -21,4 +22,4 @@ def room_f(client,event): room=goc(lambda x:x.id==rid,client.account.rooms,models.Room(rid)) event.data=room room.name= -room=_l('room','m.room',room_f) \ No newline at end of file +room=_l('m.room',room_f) \ No newline at end of file diff --git a/matrixapi.py b/matrix/matrixapi.py similarity index 100% rename from matrixapi.py rename to matrix/matrixapi.py diff --git a/matrix/models.py b/matrix/models.py new file mode 100644 index 0000000..fb3bf29 --- /dev/null +++ b/matrix/models.py @@ -0,0 +1,54 @@ +from dataclasses import dataclass, field +from datetime import datetime + +@dataclass +class User(): + @property + def mxid(self): return f'@{self.username}:{self.homeserver}' + @mxid.setter + def set_mxid(self,value): + a=value.lstrip('@').split(':') + self.username=a[0]; self.homeserver=a[1] + username:str + homeserver:str + nick:str='' + avatar:str='' + +@dataclass +class Account(User): + rooms:List[Room]=field(default_factory=list) + +@dataclass +class Room(): + room_id:str + name:str='' + viewer:Account=None + members:List[Member]=field(default_factory=list) + @property + def ismember(self): return self.viewer in self.members + parents:List[Space]=field(default_factory=list) + messages:List[Message]=field(default_factory=list) + +@dataclass +class Space(Room): + children:List[Room]=field(default_factory=list) + +@dataclass +class Event(): + event_id:str + author:Member + timestamp:datetime + contents:str='' + +@dataclass +class Message(Event): + format:str='' + +@dataclass +class State(Event): + key:str + +@dataclass +class Member(User): + room_av:str='' + room_nick:str='' \ No newline at end of file diff --git a/matrix/outbound.py b/matrix/outbound.py new file mode 100644 index 0000000..e69de29 diff --git a/timers.py b/matrix/timers.py similarity index 90% rename from timers.py rename to matrix/timers.py index f4f112d..61780ca 100644 --- a/timers.py +++ b/matrix/timers.py @@ -1,6 +1,9 @@ +from chatlib import addtimer # This function just dumps /sync blobs in the event queue as a raw event. # ALL handling is deferred to handlers. # ... Except updating the since token. That's important to do here to guarantee that it never calls with the same token twice. + +@addtimer(0) async def sync(self): blob=await self.request(params={'timeout':30000,'since':self.since}) self.since=blob['next_batch'] diff --git a/utils.py b/matrix/utils.py similarity index 100% rename from utils.py rename to matrix/utils.py diff --git a/models.py b/models.py deleted file mode 100644 index 0659d07..0000000 --- a/models.py +++ /dev/null @@ -1,39 +0,0 @@ -from dataclasses import dataclass, field - -@dataclass -class Account(): - mxid:str - username:str - homeserver:str - avatar_url:str='' - nickname:str='' - rooms:List[Room]=field(default_factory=list) - -@dataclass -class Room(): - id:str - name:str='' - members:List[Member]=field(default_factory=list) - parents:List[Space]=field(default_factory=list) - messages:List[Message]=field(default_factory=list) - -@dataclass -class Space(Room): - children:List[Room]=field(default_factory=list) - -@dataclass -class Message(): - id:str - contents:str - author:Member - -@dataclass -class User(): - mxid:str - nick:str - avatar:str - -@dataclass -class Member(User): - room_av:str - room_nick:str \ No newline at end of file