Compare commits
5 Commits
43c7460100
...
master
Author | SHA1 | Date | |
---|---|---|---|
64f5cf2c30 | |||
667fb2bd4f | |||
9dd3b82bf8 | |||
80856ef17d | |||
e3d3311ddb |
@@ -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
53
chatlib/chatlib.py
Normal 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
8
chatlib/event.py
Normal 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
26
chatlib/listener.py
Normal 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
54
chatlib/timer.py
Normal 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
5
chatlib/utils.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from datetime import datetime
|
||||
|
||||
tracing=False
|
||||
def trace(*msg):
|
||||
if tracing: print(datetime.now(),*msg)
|
@@ -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.
|
54
matrix.py
54
matrix.py
@@ -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()
|
@@ -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
18
matrix/matrixapi.py
Normal 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
54
matrix/models.py
Normal 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
0
matrix/outbound.py
Normal file
10
matrix/timers.py
Normal file
10
matrix/timers.py
Normal 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
9
matrix/utils.py
Normal 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
|
39
models.py
39
models.py
@@ -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
|
62
utils.py
62
utils.py
@@ -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
|
Reference in New Issue
Block a user