init commit 2 (no token)
This commit is contained in:
commit
f9ed79ad62
8
__init__.py
Normal file
8
__init__.py
Normal 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
22
listeners.py
Normal 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
10
matrix.design
Normal 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
54
matrix.py
Normal 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
39
models.py
Normal 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
62
utils.py
Normal 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
|
Loading…
Reference in New Issue
Block a user