Skip to content

Commit 5c9b8b0

Browse files
committed
initial bot api
allows subscribing to new message events
1 parent 6c5e652 commit 5c9b8b0

File tree

3 files changed

+176
-0
lines changed

3 files changed

+176
-0
lines changed

sogs/base_config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,6 @@
6060
# If true, show recent messages for public rooms when accessed via a web browser. If false only
6161
# show the QR code and URL but no recent messages.
6262
HTTP_SHOW_RECENT = True
63+
64+
# sogs bot api listener addresses
65+
API_ADDRS = ['ipc://bot-api.sock']

sogs/events.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from collections import defaultdict
2+
3+
from oxenmq import OxenMQ, AuthLevel
4+
5+
from . import config
6+
from . import model
7+
8+
import json
9+
10+
# module level oxenmq
11+
_mq = None
12+
13+
# pools for event propagation
14+
_pools = defaultdict(set)
15+
16+
status_OK = 'OK'
17+
status_ERR = 'ERROR'
18+
19+
# the events we are able to subscribe to
20+
EVENTS = ('message', 'joined', 'parted', 'banned', 'unbanned', 'deleted', 'uploaded')
21+
22+
23+
def event_name_valid(eventname):
24+
""" return True if this event name is something well formed """
25+
return eventname.lower() in EVENTS
26+
27+
28+
def _handle_subscribe_request(msg):
29+
""" handle a request to subscribe to events as a bot"""
30+
parts = msg.dataviews()
31+
name = None
32+
if len(parts) == 1:
33+
name = parts[0].decode('ascii').lower()
34+
if name is None:
35+
raise Exception('no event name provided')
36+
if event_name_valid(name):
37+
raise Exception('invalid event name: {}'.format(name))
38+
pool = _pools.get(name)
39+
if msg.conn in pool:
40+
raise Exception('already subscribed to {}'.format(name))
41+
pool.append(msg.conn)
42+
43+
44+
def _handle_find_room(metric, query, encode=json.dumps):
45+
""" find a room """
46+
if metric == 'id':
47+
room = model.Room(id=query)
48+
elif metric == 'token':
49+
room = model.Room(token=query)
50+
else:
51+
raise Exception("invalid query metric: {}".format(metric))
52+
reply = dict()
53+
reply['mods'] = room.get_mods()
54+
reply['active_users'] = room.active_users()
55+
for attr in model.Room.ALL_PROPS:
56+
reply[attr] = getattr(room, attr)
57+
return encode(reply)
58+
59+
60+
def _handle_find_user(metric, query, encode=json.dumps):
61+
""" find a user """
62+
if metric == 'id':
63+
user = model.User(id=query)
64+
elif metric == 'pubkey':
65+
user = model.User(session_id=query)
66+
else:
67+
raise Exception("invalid query metric: {}".format(metric))
68+
reply = dict()
69+
for attr in model.User.ALL_PROPS:
70+
reply[attr] = getattr(user, attr)
71+
return encode(reply)
72+
73+
74+
def _handle_find_request(msg):
75+
""" finds a user / room by id / token / pubkey """
76+
parts = msg.dataviews()
77+
if len(parts) != 3:
78+
raise Exception('3 arguments required: entity-kind, query-metric, query-value')
79+
80+
decode = lambda x: x.decode('utf-8').lower()
81+
82+
kind = decode(parts[0])
83+
metric = decode(parts[1])
84+
query = decode(parts[2])
85+
86+
_kinds = {'room': _handle_find_room, 'user': _handle_find_user}
87+
88+
if kind in _kinds:
89+
return _kinds[kind](metric, query)
90+
raise Exception("cannot find a '{}' we dont have those".format(kind))
91+
92+
93+
def _propagate_event(eventname, *args):
94+
""" propagate an event to everyone who cares about it """
95+
assert event_name_valid(eventname)
96+
global _mq
97+
for conn in _pool.get(eventname):
98+
_mq.command('sogs.event', eventname, *args)
99+
100+
101+
def _handle_request(handler, msg):
102+
""" safely handle a request handler and catch any exceptions that happen and propagate them to the remote connection """
103+
try:
104+
retval = handler(msg)
105+
if retval is None:
106+
msg.reply(status_OK)
107+
else:
108+
msg.reply(status_OK, retval)
109+
except Exception as ex:
110+
msg.reply(status_ERROR, '{}'.format(ex))
111+
112+
113+
@postfork
114+
def start():
115+
""" start event api """
116+
global _mq
117+
_mq = OxenMQ()
118+
_bot_category = _mq.add_category('sogs', AuthLevel.none)
119+
_bot_category.add_request_handler(
120+
'sub', lambda msg: _handle_request(_handle_subscribe_request, msg)
121+
)
122+
_bot_category.add_request_handler(
123+
'find', lambda msg: _handle_request(_handle_find_request, msg)
124+
)
125+
126+
for addr in config.API_ADDRS:
127+
# TODO: implement curve?
128+
_mq.listen(addr, False)
129+
_mq.start()
130+
131+
132+
# set up event notifiers
133+
for ev in EVENTS:
134+
setattr(__module__, ev, lambda *args: _propagate_event(ev, *args))

sogs/model.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from . import config
44
from . import db
55
from . import utils
6+
from . import events
67

78
import time
89

@@ -53,6 +54,20 @@ class Room:
5354
default_upload - True if default user permissions includes file upload permission
5455
"""
5556

57+
""" set of all proprerties on a room """
58+
ALL_PROPS = (
59+
'id',
60+
'token',
61+
'name',
62+
'description',
63+
'created',
64+
'updates',
65+
'info_updates',
66+
'default_read',
67+
'default_write',
68+
'default_upload',
69+
)
70+
5671
def __init__(self, row=None, *, id=None, token=None):
5772
"""
5873
Constructs a room from a pre-retrieved row *or* via lookup of a room token or id. When
@@ -320,6 +335,18 @@ class User:
320335
visible_mod - True if the user's admin/moderator status should be visible in rooms
321336
"""
322337

338+
""" all accessable properties on a user """
339+
ALL_PROPS = (
340+
"id",
341+
"session_id",
342+
"created",
343+
"last_active",
344+
"banned",
345+
"moderator",
346+
"visible_mod",
347+
"rooms",
348+
)
349+
323350
def __init__(self, row=None, *, id=None, session_id=None, autovivify=True, touch=False):
324351
"""
325352
Constructs a user from a pre-retrieved row *or* a session id or user primary key value.
@@ -366,6 +393,16 @@ def __init__(self, row=None, *, id=None, session_id=None, autovivify=True, touch
366393
if touch:
367394
self._touch()
368395

396+
@property
397+
def rooms(self):
398+
result = list()
399+
""" return the rooms this user is in by their id """
400+
with db.conn as conn:
401+
rows = conn.execute("SELECT room FROM room_users WHERE user = ?", (self.id,))
402+
for row in rows:
403+
result.append(row[0])
404+
return result
405+
369406
def _touch(self):
370407
db.conn.execute(
371408
"""
@@ -530,6 +567,8 @@ def add_post_to_room(user_id, room_id, data, sig, rate_limit_size=5, rate_limit_
530567
result = conn.execute("SELECT posted, id FROM messages WHERE id = ?", [lastid])
531568
row = result.fetchone()
532569
msg = {'timestamp': utils.convert_time(row['posted']), 'server_id': row['id']}
570+
# inform bot api of new post
571+
events.message(user_id, room_id, utils.message_body(data))
533572
return msg
534573

535574

0 commit comments

Comments
 (0)