init commit 2 (no token)

This commit is contained in:
Zergling_man 2022-02-24 16:27:55 +11:00
commit f9ed79ad62
6 changed files with 195 additions and 0 deletions

8
__init__.py Normal file
View File

@ -0,0 +1,8 @@
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

22
listeners.py Normal file
View File

@ -0,0 +1,22 @@
from .utils import Listener as _l, Event as _e, get_or_create as goc
import .models
# Default handlers will be given two parts each:
# 1) Mould the raw event into a parsed event (update internal state)
# 2) Identify event chains and dump them into the queue.
# I really don't recommend removing them. (But I won't stop you.)
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)
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)

10
matrix.design Normal file
View File

@ -0,0 +1,10 @@
Bot core receives stream of events; steps through them in turn
Bot has lots of model objects, each one registers itself with a type identifier (eg. m.room.message), corresponding to event types.
Bot core simply palms the event to all handlers of the relevant kind. They can then return a "processed event". Bot core will add that to the event queue.
Separate loop of the bot will pull events out of the queue and fire them. By default they'll do nothing, the outer layer can register handlers (and request a list of handlers, to make it easier to manage removing).
Can also add filters/splitters, a special kind of handler that will return new events to be fired; can be used for making up internal event types, eg. a splitter that looks at the first word in the message and fires an event of that type, to make a command handler.
The whole bot will be synchronous. However, all network/disk/DB/etc., basically all external, will just create a task and push it into a separate work queue which will fire off at maximum possible rate. Try to avoid making get calls.
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/

54
matrix.py Normal file
View File

@ -0,0 +1,54 @@
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()

39
models.py Normal file
View File

@ -0,0 +1,39 @@
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

62
utils.py Normal file
View File

@ -0,0 +1,62 @@
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