Compare commits

..

5 Commits

Author SHA1 Message Date
64f5cf2c30 lol forgot to update this for ages 2023-12-02 16:57:04 +11:00
667fb2bd4f I don't need that anymore 2022-08-18 03:46:07 +10:00
9dd3b82bf8 Still splitting matrix out from corelib 2022-08-10 00:00:55 +10:00
80856ef17d lol 2022-08-09 20:36:27 +10:00
e3d3311ddb I don't know what I'm doing here 2022-08-09 20:36:16 +10:00
16 changed files with 250 additions and 170 deletions

View File

@@ -1,8 +0,0 @@
from .matrix import *
import .listeners
real_listeners=dict(filter(lambda x:isinstance(x,Listener),listeners.__dict__.items()))
class MatrixClient(MatrixClient):
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
self.listeners=real_listeners

53
chatlib/chatlib.py Normal file
View File

@@ -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())

8
chatlib/event.py Normal file
View File

@@ -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

26
chatlib/listener.py Normal file
View File

@@ -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_

54
chatlib/timer.py Normal file
View File

@@ -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()

5
chatlib/utils.py Normal file
View File

@@ -0,0 +1,5 @@
from datetime import datetime
tracing=False
def trace(*msg):
if tracing: print(datetime.now(),*msg)

View File

@@ -7,4 +7,7 @@ The whole bot will be synchronous. However, all network/disk/DB/etc., basically
Raw events will not be exposed, only processed events. This is not a "you have full control" framework. This is a clean, unified, easy-to-use framework.
https://spec.matrix.org
https://www.matrix.org/docs/develop/
https://www.matrix.org/docs/develop/
Main chat lib defines API - is NOT a class, the module itself is a singleton
Specific proto libs are classes, instansiate with the module + account info, it'll hook itself into the module. For convenience, if the module isn't provided they'll do it themselves, and provide a .run() wrapper. If that's used with a provided module (set a flag), they emit a warning.

View File

@@ -1,54 +0,0 @@
import aiohttp as ah
import time
import asyncio
import datetime
import traceback
from .utils import Event,Listener
tracing=False
def trace(*msg):
if tracing: print(datetime.datetime.now(),*msg)
class MatrixClient():
def __init__(self,homeserver,token):
self.event_queue=asyncio.Queue() #Contains Events. Everything in here must be an Event.
self.homeserver=homeserver
self.token=token # Deal with login stuff later
self.baseurl=f'https://{self.homeserver}/_matrix/client'
self.session=ah.ClientSession()
self.since=None
self.listeners=[]
def __del__(self):
asyncio.get_event_loop().create_task(self.session.close()) # lol
while not self.session.closed: time.sleep(0.01)
async def request(self,endpoint='sync',method='GET', ver=0,params={} *args,**kwargs):
async with self.session.request(method, f'{self.baseurl}/r{ver}/{endpoint}', params=params|{'access_token':self.token}|({'since':self.since} if self.since else {}), *args,**kwargs) as fetched:
if fetched.status_code!=200: raise Exception('fix ur shit')
try: return await fetched.json()
except JSONDecodeError: pass # TODO: Figure out what this is called
# 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.
async def sync(self):
blob=await self.request(params={'timeout':30000})
self.since=blob['next_batch']
self.event_queue.put(Event('m.sync',None,blob))
def addlistener(self,name=None,match=None):
func=type(lambda:1) # Gross, but I can't actually find it.
# And yes, lamdas are <class 'function'> too.
if isinstance(name,func) and match==None: match=name; name=None
def __wrap__(funky):
self.listeners.append(Listener(name,match,funky))
return funky
async def process_queue(self):
while True:
item=await self.event_queue.get():
for listener in self.listeners:
if listener==item.event_type:
listener(item)
#traceback.format_exc()

View File

@@ -1,5 +1,6 @@
from .utils import Listener as _l, Event as _e, get_or_create as goc
from .utils import get_or_create as goc
import .models
from .chatlib import Listener as _l, Event as _e
# Default handlers will be given two parts each:
# 1) Mould the raw event into a parsed event (update internal state)
@@ -9,14 +10,16 @@ import .models
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
rid=event.data['id']
room=goc(lambda x:x.id==rid,client.account.rooms,models.Room(rid))
event.data=room
room=_l('room','m.room',room_f)
room.name=
room=_l('m.room',room_f)

18
matrix/matrixapi.py Normal file
View File

@@ -0,0 +1,18 @@
class MatrixLibHttp():
def __init__(self,instance,token=None,username=None,password=None,chatlib=None):
self.instance=instance
self.https=True
self.token=token
if chatlib is None:
import chatlib
chatlib.addlistener() # TODO: probably should make that a decorator actually. Lol, it already is.
@property
def baseurl(self):
return 'http'+['','s'][self.https]+'://'+self.instance+'/_matrix/client'
async def request(self,endpoint='sync',method='GET', ver=0,headers={}, *args,**kwargs):
async with self.session.request(method, f'{self.baseurl}/r{ver}/{endpoint}', headers=headers|{'Authorization':f'Bearer {self.token}'}, *args,**kwargs) as fetched:
if fetched.status_code!=200: raise Exception('fix ur shit')
try: return await fetched.json()
except JSONDecodeError: pass # TODO: Figure out what this is called

54
matrix/models.py Normal file
View File

@@ -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=''

0
matrix/outbound.py Normal file
View File

10
matrix/timers.py Normal file
View File

@@ -0,0 +1,10 @@
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']
self.event_queue.put(Event('m.sync',None,blob))

9
matrix/utils.py Normal file
View File

@@ -0,0 +1,9 @@
def get_or_create(needle,haystack,default):
"""
This is a wrapper for filter that can add stuff. Nothing special. Needle is a function, default isn't. Haystack is a list. I might fix that later.
"""
f=filter(needle,haystack)
try: return next(f)
except StopIteration: pass
haystack.append(default)
return default

View File

@@ -1,39 +0,0 @@
from .utils import redc
@redc
class Account():
mxid:str
username:str
homeserver:str
avatar_url:str=''
nickname:str=''
rooms:List[Room]=field(default_factory=list)
@redc
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)
@redc
class Space(Room):
children:List[Room]=field(default_factory=list)
@redc
class Message():
id:str
contents:str
author:Member
@redc
class User():
mxid:str
nick:str
avatar:str
@redc
class Member(User):
room_av:str
room_nick:str

View File

@@ -1,62 +0,0 @@
from dataclasses import dataclass, field, _MISSING_TYPE as mt
class Event():
def __init__(self,event_type,data,raw_data,outbound=False):
self.event_type=event_type
self.data=data
self.raw_data=raw_data
self.outbound=outbound
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
def __call__(self,*args,**kwargs):
self.function(*args,**kwargs)
def __eq__(self,other):
if isinstance(other,str):
if self._matchstr: return self.match==other
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
# I HATE THE TYPEHINTS! I HATE THE TYPEHINTS!
# I REALLY fucking hate the typehints. This is the messiest fucking shit.
# WHY IS THIS NOT JUST A DEFAULT THING IN DATACLASS
# Please tell me as soon as this egregious oversight is corrected.
# I hate this function so much, I want to delete it ASAP.
def redc(classy):
wrapped=dataclass(classy)
def from_dict(somedict):
# Sure would be nice if we could just ask for a list of required args.
count=len(dict(filter(lambda x:isinstance(x[1].default,mt) and isinstance(x[1].default_factory,mt),wrapped.__dataclass_fields__.items())))
p=wrapped(*[None]*count)
for k,v in somedict.items():
if k not in p.__dict__: continue
t=p.__dataclass_fields__[k].type
try: parsed=t(v)
except TypeError:
for n in t.__args__:
try: parsed=n(v)
except: continue
else: break
# Everything failed so just leave it as default. 🤷
# Watch that somehow generate an error one day anyway.
else: parsed=p.__dict__[k]
p.__dict__[k]=parsed
wrapped.from_dict=from_dict
return wrapped
def get_or_create(needle,haystack,default):
"""
This is a wrapper for filter that can add stuff. Nothing special. Needle is a function, default isn't. Haystack is a list. I might fix that later.
"""
f=filter(needle,haystack)
try: return next(f)
except StopIteration: pass
haystack.append(default)
return default