Compare commits
10 Commits
7aea826c60
...
f12889ccdf
Author | SHA1 | Date | |
---|---|---|---|
|
f12889ccdf | ||
da1d9a23d6 | |||
5f49cd7f46 | |||
41b1579785 | |||
c9d8be1a24 | |||
f008ed2462 | |||
ee4a6865d8 | |||
ab311c54d8 | |||
57fc83567d | |||
502afe9827 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -206,3 +206,6 @@ fabric.properties
|
||||
|
||||
# Others
|
||||
*.db
|
||||
/v25pushx/
|
||||
/report.sql
|
||||
/vapidkeys.json
|
||||
|
@ -2,6 +2,7 @@
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/v25pushx" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
@ -2,5 +2,6 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/v25pushx" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
19
LICENSE
Normal file
19
LICENSE
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2021 PARRRATE T&V
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -1,2 +1,3 @@
|
||||
# v25
|
||||
|
||||
replace public keys in `dev-config.json` and `staging-config.json` with your own to allow messaging
|
||||
|
@ -1,7 +1,7 @@
|
||||
from flask import Flask
|
||||
from werkzeug.middleware.dispatcher import DispatcherMiddleware
|
||||
|
||||
import config
|
||||
from v25 import config
|
||||
from v25.storage.dbstorage import DBStorage
|
||||
from v25.web.server.api import API
|
||||
|
||||
@ -11,7 +11,7 @@ def simple(_env, resp):
|
||||
return []
|
||||
|
||||
|
||||
d = config.get_config()
|
||||
d = config.get_config('dev-config.json')
|
||||
config.from_config(d)
|
||||
|
||||
app = Flask(__name__)
|
||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='v25',
|
||||
version='0.0.1-a1',
|
||||
version='0.0.1-rc1',
|
||||
packages=['v25', 'v25.web', 'v25.web.client', 'v25.web.server', 'v25.storage', 'v25.messaging'],
|
||||
url='',
|
||||
license='',
|
||||
|
17
staging-config.json
Normal file
17
staging-config.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"db": "sqlite:///staging.db",
|
||||
"subjects": {
|
||||
"ro6ncuJxA_cGQ51hPKw11Q84of08j7GtOjL0Xr5GaFs=": {
|
||||
"allowed": null,
|
||||
"contacts": null
|
||||
},
|
||||
"oNNNAvX5nsJEQGf33xulhh27cpECgQtJT3jzu2VyNKY=": {
|
||||
"allowed": null,
|
||||
"contacts": null
|
||||
},
|
||||
"uLep1UFgMlYDaIM8MEgMYTDY6HWcUq6Y4VvkyglbGJ8=": {
|
||||
"allowed": null,
|
||||
"contacts": null
|
||||
}
|
||||
}
|
||||
}
|
22
staging-main.py
Normal file
22
staging-main.py
Normal file
@ -0,0 +1,22 @@
|
||||
from flask import Flask
|
||||
from werkzeug.middleware.dispatcher import DispatcherMiddleware
|
||||
|
||||
from v25 import config
|
||||
from v25.storage.dbstorage import DBStorage
|
||||
from v25.web.server.api import API
|
||||
|
||||
|
||||
def simple(_env, resp):
|
||||
resp('404 OK', [])
|
||||
return []
|
||||
|
||||
|
||||
d = config.get_config('staging-config.json')
|
||||
config.from_config(d)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.wsgi_app = DispatcherMiddleware(simple, {
|
||||
'/v25': API(__name__, DBStorage(d['db']).pushing())
|
||||
})
|
||||
app.config['ENV'] = 'staging'
|
||||
app.run(port=5013)
|
@ -4,11 +4,11 @@ from typing import Dict, Any, Union
|
||||
from v25.storage.dbstorage import DBStorage
|
||||
|
||||
|
||||
_d_type = Dict[Any, Union[str, Dict[str, Any]]]
|
||||
_d_type = Dict[str, Union[str, Dict[str, Any]]]
|
||||
|
||||
|
||||
def get_config() -> _d_type:
|
||||
with open('config.json') as f:
|
||||
def get_config(file: str) -> _d_type:
|
||||
with open(file) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
@ -17,11 +17,3 @@ def from_config(d: _d_type):
|
||||
subjects = d["subjects"]
|
||||
for subject in subjects:
|
||||
storage.ssssj(subject, json.dumps(subjects[subject]))
|
||||
|
||||
|
||||
def main():
|
||||
from_config(get_config())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,6 +1,6 @@
|
||||
__all__ = ('Flags',)
|
||||
import nacl.hash
|
||||
|
||||
Q_FLAG = '<?>'
|
||||
__all__ = ('Flags',)
|
||||
|
||||
|
||||
class Flags:
|
||||
@ -9,14 +9,17 @@ class Flags:
|
||||
def __init__(self, flags: str):
|
||||
self.flags: str = flags
|
||||
|
||||
def quable(self) -> bool:
|
||||
return Q_FLAG in self.flags
|
||||
def hash(self):
|
||||
return nacl.hash.sha256(self.flags.encode()).decode()
|
||||
|
||||
def deq(self) -> str:
|
||||
return Flags(self.flags.replace(Q_FLAG, '')).deq() if self.quable() else self.flags
|
||||
def joint(self, flags: str):
|
||||
return flags.startswith(self.hash())
|
||||
|
||||
def enq(self) -> str:
|
||||
return self.flags if self.quable() else self.flags + Q_FLAG
|
||||
def join(self, flags: str):
|
||||
h = self.hash()
|
||||
if flags.startswith(h):
|
||||
return flags
|
||||
return h + flags
|
||||
|
||||
|
||||
Flags.default = Flags('<unedited>').enq()
|
||||
Flags.default = '<unedited>'
|
||||
|
@ -95,7 +95,7 @@ class Message:
|
||||
def edit(self, pcontent: bytes) -> 'Message':
|
||||
return Message(self.sfrom, self.sto, self.idnonce, None,
|
||||
Encoding.nonce(), pcontent, None,
|
||||
Flags(self.flags.replace('<unedited>', '<edited>')).enq()).sealed()
|
||||
self.flags.replace('<unedited>', '<edited>')).sealed()
|
||||
|
||||
def edit_(self):
|
||||
return self.flags_(self.flags)
|
||||
|
@ -1,10 +1,11 @@
|
||||
import json
|
||||
from contextlib import closing
|
||||
from subprocess import run
|
||||
from sys import stderr
|
||||
from threading import Thread
|
||||
from time import time, sleep
|
||||
from typing import Tuple, Optional, Iterable, List
|
||||
|
||||
import requests
|
||||
from nacl.bindings import crypto_sign_PUBLICKEYBYTES
|
||||
from sqlalchemy import create_engine, LargeBinary, Column, REAL, BLOB, String, or_, and_, ForeignKeyConstraint, \
|
||||
Integer, UniqueConstraint, Index, Boolean
|
||||
@ -145,6 +146,43 @@ class PushState(Base):
|
||||
return True
|
||||
|
||||
|
||||
class TypingState(Base):
|
||||
__tablename__ = 'typingstate'
|
||||
|
||||
sf = Column(LargeBinary(crypto_sign_PUBLICKEYBYTES), primary_key=True)
|
||||
st = Column(LargeBinary(crypto_sign_PUBLICKEYBYTES), primary_key=True)
|
||||
last = Column(REAL)
|
||||
|
||||
@classmethod
|
||||
def _get(cls, sfrom: Subject, sto: Subject, session) -> Tuple['TypingState', 'TypingState']:
|
||||
sf = sfrom.vkey.encode()
|
||||
st = sto.vkey.encode()
|
||||
try:
|
||||
statef = session.query(TypingState).filter_by(sf=sf, st=st).one()
|
||||
except NoResultFound:
|
||||
statef = TypingState(sf=sf, st=st, last=0)
|
||||
session.add(statef)
|
||||
try:
|
||||
statet = session.query(TypingState).filter_by(sf=st, st=sf).one()
|
||||
except NoResultFound:
|
||||
statet = TypingState(sf=st, st=sf, last=0)
|
||||
session.add(statet)
|
||||
return statef, statet
|
||||
|
||||
@classmethod
|
||||
def typing(cls, sfrom: Subject, sto: Subject, last: float, session) -> float:
|
||||
statef, statet = cls._get(sfrom, sto, session)
|
||||
statef.last = max(statef.last, last)
|
||||
session.commit()
|
||||
return statet.last
|
||||
|
||||
@classmethod
|
||||
def reset(cls, m: Message, session):
|
||||
statef, _ = cls._get(m.sfrom, m.sto, session)
|
||||
statef.last = 0
|
||||
session.commit()
|
||||
|
||||
|
||||
class DBStorage(PushStorage):
|
||||
def __init__(self, *args):
|
||||
self.args = args
|
||||
@ -153,8 +191,7 @@ class DBStorage(PushStorage):
|
||||
self.Session = sessionmaker(bind=self.engine)
|
||||
|
||||
def check(self, subject: Subject) -> dict:
|
||||
session = self.Session()
|
||||
try:
|
||||
with closing(self.Session()) as session:
|
||||
sssj = session.query(SSSJ).filter_by(subject=subject.vkey.encode()).one_or_none()
|
||||
status = json.loads(sssj.status) if sssj else {}
|
||||
if 'contacts' in status:
|
||||
@ -169,29 +206,23 @@ class DBStorage(PushStorage):
|
||||
contacts.add(st)
|
||||
status['contacts'] = list(map(Encoding.encode, contacts))
|
||||
return status
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def push(self, m: Message) -> None:
|
||||
session = self.Session()
|
||||
try:
|
||||
with closing(self.Session()) as session:
|
||||
msg = Msg.from_message(m)
|
||||
session.add(msg)
|
||||
session.add(msg.sgn())
|
||||
self.event(session, m, EVENT_PUSH)
|
||||
session.commit()
|
||||
if m.sfrom == m.sto:
|
||||
return
|
||||
state = PushState.of(m.sto, session)
|
||||
state.pushmsg = True
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
if m.sfrom != m.sto:
|
||||
state = PushState.of(m.sto, session)
|
||||
state.pushmsg = True
|
||||
session.commit()
|
||||
TypingState.reset(m, session)
|
||||
|
||||
def edit(self, old: Message, new: Message):
|
||||
assert old.edited(new), 'edit misuse'
|
||||
session = self.Session()
|
||||
try:
|
||||
with closing(self.Session()) as session:
|
||||
msg = self.one_alike(session, old)
|
||||
assert msg.en == old.editnonce, 'edit misuse'
|
||||
msgn = Msg.from_message(new)
|
||||
@ -200,27 +231,22 @@ class DBStorage(PushStorage):
|
||||
msg.flags = msgn.flags
|
||||
self.event(session, new, EVENT_EDIT)
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
TypingState.reset(new, session)
|
||||
|
||||
@staticmethod
|
||||
def one_alike(session, m: Message) -> Msg:
|
||||
return session.query(Msg).filter_by(sf=m.sfrom.vkey.encode(), st=m.sto.vkey.encode(), idn=m.idnonce).one()
|
||||
|
||||
def delete(self, m: Message):
|
||||
session = self.Session()
|
||||
try:
|
||||
with closing(self.Session()) as session:
|
||||
session.delete(self.one_alike(session, m))
|
||||
self.event(session, m, EVENT_DELETE)
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def pull(self, pair: Tuple[Subject, Subject], params: Optional[dict] = None) -> Iterable[Message]:
|
||||
if params is None:
|
||||
params = {}
|
||||
session = self.Session()
|
||||
try:
|
||||
with closing(self.Session()) as session:
|
||||
cquery: Query = session.query(Msg).filter(or_(
|
||||
and_(
|
||||
Msg.sf == pair[0].vkey.encode(),
|
||||
@ -232,56 +258,51 @@ class DBStorage(PushStorage):
|
||||
),
|
||||
))
|
||||
query: Query = cquery
|
||||
if 'ts' in params:
|
||||
if '>' in params:
|
||||
query = query.filter(Msg.ts > params['ts']['>'])
|
||||
if '<' in params:
|
||||
query = query.filter(Msg.ts < params['ts']['<'])
|
||||
if params.get('before'):
|
||||
query = query.filter(Msg.oid < cquery.filter(Msg.idn == Encoding.decode(params['before'])).one().oid)
|
||||
if params.get('after'):
|
||||
query = query.filter(Msg.oid > cquery.filter(Msg.idn == Encoding.decode(params['after'])).one().oid)
|
||||
if params.get('exact'):
|
||||
query = query.filter_by(idn=Encoding.decode(params['exact']))
|
||||
query = query.filter_by(idn=Encoding.decode(params['exact']), sf=pair[0].vkey.encode())
|
||||
for flag in params.get('flags', ()):
|
||||
query = query.filter(Msg.flags.contains(flag))
|
||||
query = query.order_by(Msg.oid.desc())
|
||||
if 'limit' in params:
|
||||
query = query.limit(params['limit'])
|
||||
return map(Msg.to_message, list(query.from_self().order_by(Msg.oid)))
|
||||
finally:
|
||||
session.close()
|
||||
res = map(Msg.to_message, list(query.from_self().order_by(Msg.oid)))
|
||||
if 'edit_' in params:
|
||||
res = (m.edit_() for m in res)
|
||||
return res
|
||||
|
||||
def exact(self, sfrom: Subject, sto: Subject, idnonce: bytes, editnonce: Optional[bytes]) -> Optional[Message]:
|
||||
with closing(self.Session()) as session:
|
||||
query: Query = session.query(Msg)
|
||||
query = query.filter_by(sf=sfrom, st=sto, idn=idnonce)
|
||||
if editnonce:
|
||||
query = query.filter_by(en=editnonce)
|
||||
msg: Optional[Msg] = query.one_or_none()
|
||||
return msg or msg.to_message()
|
||||
|
||||
def flags(self, m: Message, flags: str):
|
||||
assert not Flags(flags).quable(), 'flags misuse'
|
||||
session = self.Session()
|
||||
try:
|
||||
with closing(self.Session()) as session:
|
||||
msg: Msg = self.one_alike(session, m)
|
||||
assert msg.en == m.editnonce, 'flags misuse'
|
||||
assert Flags(msg.flags).quable(), 'flags misuse'
|
||||
assert Flags(msg.flags).joint(flags)
|
||||
msg.flags = flags
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def clearsssj(self):
|
||||
session = self.Session()
|
||||
try:
|
||||
with closing(self.Session()) as session:
|
||||
session.query(SSSJ).delete()
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def ssssj(self, subject: str, status: str):
|
||||
"""set SSSJ"""
|
||||
session = self.Session()
|
||||
try:
|
||||
with closing(self.Session()) as session:
|
||||
subject = Subject(Encoding.decode(subject)).vkey.encode()
|
||||
session.query(SSSJ).filter_by(subject=subject).delete()
|
||||
session.add(SSSJ(subject=subject, status=status))
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@staticmethod
|
||||
def event(session, m: Message, code: int):
|
||||
@ -294,8 +315,7 @@ class DBStorage(PushStorage):
|
||||
return query.filter_by(en=en).one()
|
||||
|
||||
def events(self, sfrom: Subject, sto: Subject, after: Optional[bytes]) -> Iterable[Tuple[bytes, bytes]]:
|
||||
session = self.Session()
|
||||
try:
|
||||
with closing(self.Session()) as session:
|
||||
PushState.of(sto, session).online()
|
||||
session.commit()
|
||||
query: Query = session.query(MsgEvent)
|
||||
@ -308,67 +328,51 @@ class DBStorage(PushStorage):
|
||||
query = query.order_by(MsgEvent.oid)
|
||||
ev: MsgEvent
|
||||
return [(ev.en, ev.idn) for ev in query.all()]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def cleanevents(self):
|
||||
session = self.Session()
|
||||
try:
|
||||
with closing(self.Session()) as session:
|
||||
session.query(MsgEvent).delete(MsgEvent.ts < time() - 604800)
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def subscribe(self, subject: Subject, subscription: dict):
|
||||
session = self.Session()
|
||||
try:
|
||||
with closing(self.Session()) as session:
|
||||
subject = subject.vkey.encode()
|
||||
session.query(PushSub).filter_by(subject=subject).delete()
|
||||
session.add(PushSub(subject=subject, subscription=json.dumps(subscription)))
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def subscription(self, subject: Subject) -> Optional[dict]:
|
||||
session = self.Session()
|
||||
try:
|
||||
with closing(self.Session()) as session:
|
||||
subscription: Optional[PushSub] = session.query(PushSub).filter_by(
|
||||
subject=subject.vkey.encode()).one_or_none()
|
||||
if subscription is None:
|
||||
return None
|
||||
return json.loads(subscription.subscription)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@staticmethod
|
||||
def pushsubscription(subscription: dict):
|
||||
assert requests.post('http://localhost:5025/api/push', json={
|
||||
assert not run(["node", "v25pushx/push.js"], input=json.dumps({
|
||||
"subscription": subscription,
|
||||
"notification": {
|
||||
"notification": {
|
||||
"title": "New Message (V25PUSH)"
|
||||
"title": "New Message (V25PUSH)",
|
||||
"vibrate": [100, 50, 100],
|
||||
}
|
||||
}
|
||||
}).status_code == 200, "push failed"
|
||||
}), encoding='utf-8').returncode, "push subprocess failed"
|
||||
|
||||
def pushpush(self, subject: Subject):
|
||||
session = self.Session()
|
||||
try:
|
||||
with closing(self.Session()) as session:
|
||||
notify = PushState.of(subject, session).notify()
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
if notify:
|
||||
subscription = self.subscription(subject)
|
||||
if subscription:
|
||||
self.pushsubscription(subscription)
|
||||
|
||||
def push_once(self):
|
||||
session = self.Session()
|
||||
try:
|
||||
with closing(self.Session()) as session:
|
||||
subs: List[PushSub] = session.query(PushSub).all()
|
||||
finally:
|
||||
session.close()
|
||||
for sub in subs:
|
||||
try:
|
||||
self.pushpush(Subject(sub.subject))
|
||||
@ -390,3 +394,7 @@ class DBStorage(PushStorage):
|
||||
def pushing(self):
|
||||
self.push_forever()
|
||||
return self
|
||||
|
||||
def typing(self, sfrom: Subject, sto: Subject, last: float) -> float:
|
||||
with closing(self.Session()) as session:
|
||||
return TypingState.typing(sfrom, sto, last, session)
|
||||
|
@ -33,6 +33,10 @@ class SecureStorage(PushStorage):
|
||||
assert self.subject in pair, self.asrt()
|
||||
return self.storage.pull(pair, params)
|
||||
|
||||
def exact(self, sfrom: Subject, sto: Subject, idnonce: bytes, editnonce: Optional[bytes]) -> Optional[Message]:
|
||||
assert self.subject in (sfrom, sto)
|
||||
return self.storage.exact(sfrom, sto, idnonce, editnonce)
|
||||
|
||||
def flags(self, m: Message, flags: str):
|
||||
assert self.subject in m.pair, self.asrt()
|
||||
return self.storage.flags(m, flags)
|
||||
@ -44,3 +48,7 @@ class SecureStorage(PushStorage):
|
||||
def subscribe(self, subject: Subject, subscription: dict):
|
||||
assert self.subject == subject, self.asrt()
|
||||
return self.storage.subscribe(subject, subscription)
|
||||
|
||||
def typing(self, sfrom: Subject, sto: Subject, last: float) -> float:
|
||||
assert self.subject == sfrom, self.asrt()
|
||||
return self.storage.typing(sfrom, sto, last)
|
||||
|
@ -21,6 +21,9 @@ class AbstractStorage(ABC):
|
||||
def pull(self, pair: Tuple[Subject, Subject], params: Optional[dict] = None) -> Iterable[Message]:
|
||||
raise NotImplementedError
|
||||
|
||||
def exact(self, sfrom: Subject, sto: Subject, idnonce: bytes, editnonce: Optional[bytes]) -> Optional[Message]:
|
||||
raise NotImplementedError
|
||||
|
||||
def flags(self, m: Message, flags: str):
|
||||
raise NotImplementedError
|
||||
|
||||
@ -29,6 +32,9 @@ class EventStorage(AbstractStorage, ABC):
|
||||
def events(self, sfrom: Subject, sto: Subject, after: Optional[bytes]) -> Iterable[Tuple[bytes, bytes]]:
|
||||
raise NotImplementedError
|
||||
|
||||
def typing(self, sfrom: Subject, sto: Subject, last: float) -> float:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
EVENT_PUSH = 0
|
||||
EVENT_EDIT = 1
|
||||
|
@ -42,6 +42,9 @@ class RemoteStorage(PushStorage):
|
||||
return map(Message.loads,
|
||||
self.req('pull', {'params': params, 'pair': [subject.dumps() for subject in pair]}))
|
||||
|
||||
def exact(self, sfrom: Subject, sto: Subject, idnonce: bytes, editnonce: Optional[bytes]) -> Optional[Message]:
|
||||
raise NotImplementedError
|
||||
|
||||
def flags(self, m: Message, flags: str):
|
||||
self.req('flags', {'m': m.dumps(), 'flags': flags})
|
||||
|
||||
@ -50,6 +53,13 @@ class RemoteStorage(PushStorage):
|
||||
self.req('events', {'sfrom': sfrom.dumps(), 'sto': sto.dumps(),
|
||||
'after': Encoding.encode(after)})]
|
||||
|
||||
def typing(self, sfrom: Subject, sto: Subject, last: float) -> float:
|
||||
return self.req('typing', {
|
||||
'sfrom': sfrom.dumps(),
|
||||
'sto': sto.dumps(),
|
||||
'last': last
|
||||
})
|
||||
|
||||
def subscribe(self, subject: Subject, subscription: dict):
|
||||
self.req('subscribe', {
|
||||
'subject': subject.dumps(),
|
||||
|
@ -80,3 +80,11 @@ class API(Flask):
|
||||
def subscribe():
|
||||
return self.nomessassertcall(lambda d, storage: storage.subscribe(Subject.loads(d['subject']),
|
||||
d['subscription']))
|
||||
|
||||
@app.route('/typing', methods=['POST'])
|
||||
def typing():
|
||||
return self.nomessassertcall(lambda d, storage: storage.typing(
|
||||
Subject.loads(d['sfrom']),
|
||||
Subject.loads(d['sto']),
|
||||
d['last']
|
||||
))
|
||||
|
Loading…
Reference in New Issue
Block a user