on_behalf_of seems to work now

This commit is contained in:
or-else
2018-09-27 18:06:07 +03:00
parent 8510788c5b
commit ea7f89e6e2
8 changed files with 228 additions and 145 deletions

View File

@ -137,15 +137,7 @@ rm -f $GOPATH/bin/init-db
# Build chatbot release
echo "Building python code..."
pushd ./py_grpc > /dev/null
# Generate version file from git tags
python3 version.py
# Generate tinode-grpc package
python3 setup.py -q sdist bdist_wheel
popd > /dev/null
./build-py-grpc.py
# Release chatbot
echo "Packaging chatbot.py..."

View File

@ -1,6 +1,6 @@
# Tinode Chatbot Example
# Tinode Chatbot
This is a rudimentary chatbot for Tinode using [gRPC API](../../pbx/). It's written in Python as a demonstration
This is a simple chatbot for Tinode using [gRPC API](../../pbx/). It's written in Python as a demonstration
that the API is language-independent.
The chat bot subscribes to events stream using Plugin API and logs in as a regular user. The event stream API is used to listen for creation of new accounts. When a new account is created, the bot initiates a p2p topic with the new user. Then it listens for messages sent to the topic and responds to each with a random quote from `quotes.txt` file.

View File

@ -1,21 +1,23 @@
# Protocol Buffer and gRPC definitions
Definitions for [gRPC](https://grpc.io/) client and plugins.
Definitions for Tinode [gRPC](https://grpc.io/) client and plugins.
gRPC clients must implement rpc service `Node`, plugins `Plugin`.
Tinode gRPC clients must implement rpc service `Node`, Tinode plugins `Plugin`.
Generated `Go` and `Python` code is included. For a sample `Python` implementation of a command line client see [tn-cli](../tn-cli/).
Generated `Go` and `Python` code is included. For a sample `Python` implementation of a command line client see [tn-cli](../tn-cli/).
For a partial plugin implementation see [chatbot](../chatbot/).
If you want to make changes, you have to install protobuffers tool chain and gRPC. To generate `Go` bindings add the following comment to your code and run `go generate` (your actual path to `/pbx` may be different):
If you want to make changes, you have to install protobuffers tool chain and gRPC:
```
$ python -m pip install grpcio grpcio-tools googleapis-common-protos
```
To generate `Go` bindings add the following comment to your code and run `go generate` (your actual path to `/pbx` may be different):
```
//go:generate protoc --proto_path=../pbx --go_out=plugins=grpc:../pbx ../pbx/model.proto
```
To generate `Python` bindings:
```
python -m grpc_tools.protoc -I../pbx --python_out=. --grpc_python_out=. ../pbx/model.proto
```

View File

@ -1,4 +1,6 @@
#!/bin/bash
# This generates
# This generates python gRPC bindings for Tinode.
python -m grpc_tools.protoc -I../pbx --python_out=../py_grpc/tinode_grpc --grpc_python_out=../py_grpc/tinode_grpc ../pbx/model.proto
# Bindings are incompatible with Python packaging system. This is a fix.
python py_fix.py

File diff suppressed because one or more lines are too long

View File

@ -623,9 +623,10 @@ func (t *Topic) run(hub *Hub) {
func (t *Topic) handleSubscription(h *Hub, sreg *sessionJoin) error {
asUid := types.ParseUserId(sreg.pkt.from)
msgsub := sreg.pkt.Sub
getWhat := 0
if sreg.pkt.Get != nil {
getWhat = parseMsgClientMeta(sreg.pkt.Get.What)
if msgsub.Get != nil {
getWhat = parseMsgClientMeta(msgsub.Get.What)
}
if err := t.subCommonReply(h, sreg, (getWhat&constMsgMetaDesc != 0)); err != nil {
@ -705,7 +706,7 @@ func (t *Topic) handleSubscription(h *Hub, sreg *sessionJoin) error {
if getWhat&constMsgMetaSub != 0 {
// Send get.sub response as a separate {meta} packet
if err := t.replyGetSub(sreg.sess, asUid, sreg.pkt.id, sreg.pkt.Get.Sub); err != nil {
if err := t.replyGetSub(sreg.sess, asUid, sreg.pkt.id, msgsub.Get.Sub); err != nil {
log.Printf("topic[%s] handleSubscription Get.Sub failed: %v", t.name, err)
}
}
@ -719,14 +720,14 @@ func (t *Topic) handleSubscription(h *Hub, sreg *sessionJoin) error {
if getWhat&constMsgMetaData != 0 {
// Send get.data response as {data} packets
if err := t.replyGetData(sreg.sess, asUid, sreg.pkt.id, sreg.pkt.Get.Data); err != nil {
if err := t.replyGetData(sreg.sess, asUid, sreg.pkt.id, msgsub.Get.Data); err != nil {
log.Printf("topic[%s] handleSubscription Get.Data failed: %v", t.name, err)
}
}
if getWhat&constMsgMetaDel != 0 {
// Send get.del response as a separate {meta} packet
if err := t.replyGetDel(sreg.sess, asUid, sreg.pkt.id, sreg.pkt.Get.Del); err != nil {
if err := t.replyGetDel(sreg.sess, asUid, sreg.pkt.id, msgsub.Get.Del); err != nil {
log.Printf("topic[%s] handleSubscription Get.Del failed: %v", t.name, err)
}
}

View File

@ -25,3 +25,7 @@ The client takes optional parameters:
* `--login-cookie` direct the client to read the token from the cookie file generated during an earlier login.
If multiple `login-XYZ` are provided, `login-cookie` is considered first, then `login-token` then `login-basic`. Authentication with token (and cookie) is much faster than with the username-password pair.
## Crash on shutdown
Python 3 sometimes crashes on shutdown with a message `Fatal Python error: PyImport_GetModuleDict: no module dictionary!`. That happens because it's buggy: https://bugs.python.org/issue26153

View File

@ -35,9 +35,14 @@ onCompletion = {}
# Saved topic: default topic name to make keyboard input easier
SavedTopic = None
# IO queues for asynchronous input/output
# IO queues and thread for asynchronous input/output
input_queue = queue.Queue()
output_queue = queue.Queue()
input_thread = None
# Default values for user and topic
default_user = None
default_topic = None
# Pack user's name and avatar into a vcard represented as json.
def make_vcard(fn, photofile):
@ -77,7 +82,9 @@ def stdout(*args):
text = ""
for a in args:
text = text + str(a) + " "
output_queue.put(text)
text = text.strip(" ")
if text != "":
output_queue.put(text)
def stdoutln(*args):
args = args + ("\n",)
@ -114,7 +121,7 @@ def accMsg(id, user, scheme, secret, uname, password, do_login, fn, photo, priva
return pb.ClientMsg(acc=pb.ClientAcc(id=str(id), user_id=user,
scheme=scheme, secret=secret, login=do_login, tags=tags.split(",") if tags else None,
desc=pb.SetDesc(default_acs=pb.DefaultAcsMode(auth=auth, anon=anon),
public=public, private=private, cred=parse_cred(cred))))
public=public, private=private, cred=parse_cred(cred))), on_behalf_of=default_user)
def loginMsg(id, scheme, secret, cred, uname, password):
if secret == None and uname != None:
@ -129,6 +136,8 @@ def loginMsg(id, scheme, secret, cred, uname, password):
secret=secret, cred=parse_cred(cred)))
def subMsg(id, topic, fn, photo, private, auth, anon, mode, tags, get_query):
if not topic:
topic = default_topic
if get_query:
get_query = pb.GetQuery(what=get_query.split(",").join(" "))
public = encode_to_bytes(make_vcard(fn, photo))
@ -137,9 +146,23 @@ def subMsg(id, topic, fn, photo, private, auth, anon, mode, tags, get_query):
set_query=pb.SetQuery(
desc=pb.SetDesc(public=public, private=private, default_acs=pb.DefaultAcsMode(auth=auth, anon=anon)),
sub=pb.SetSub(mode=mode),
tags=tags.split(",") if tags else None), get_query=get_query))
tags=tags.split(",") if tags else None), get_query=get_query), on_behalf_of=default_user)
def leaveMsg(id, topic, unsub):
if not topic:
topic = default_topic
return pb.ClientMsg(leave=pb.ClientLeave(id=str(id), topic=topic, unsub=unsub), on_behalf_of=default_user)
def pubMsg(id, topic, content):
if not topic:
topic = default_topic
return pb.ClientMsg(pub=pb.ClientPub(id=str(id), topic=topic, no_echo=True,
content=encode_to_bytes(content)), on_behalf_of=default_user)
def getMsg(id, topic, desc, sub, tags, data):
if not topic:
topic = default_topic
what = []
if desc:
what.append("desc")
@ -150,9 +173,13 @@ def getMsg(id, topic, desc, sub, tags, data):
if data:
what.append("data")
return pb.ClientMsg(get=pb.ClientGet(id=str(id), topic=topic,
query=pb.GetQuery(what=" ".join(what))))
query=pb.GetQuery(what=" ".join(what))), on_behalf_of=default_user)
def setMsg(id, topic, user, fn, photo, public, private, auth, anon, mode, tags):
if not topic:
topic = default_topic
if public == None:
public = encode_to_bytes(make_vcard(fn, photo))
else:
@ -163,13 +190,17 @@ def setMsg(id, topic, user, fn, photo, public, private, auth, anon, mode, tags):
desc=pb.SetDesc(default_acs=pb.DefaultAcsMode(auth=auth, anon=anon),
public=public, private=private),
sub=pb.SetSub(user_id=user, mode=mode),
tags=tags)))
tags=tags)), on_behalf_of=default_user)
def delMsg(id, topic, what, param, hard):
if topic == None and param != None:
topic = param
param = None
if not topic:
topic = default_topic
stdoutln(id, topic, what, param, hard)
enum_what = None
before = None
@ -190,7 +221,7 @@ def delMsg(id, topic, what, param, hard):
enum_what = pb.ClientDel.TOPIC
# Field named 'del' conflicts with the keyword 'del. This is a work around.
msg = pb.ClientMsg()
msg = pb.ClientMsg(on_behalf_of=default_user)
xdel = getattr(msg, 'del')
"""
setattr(msg, 'del', pb.ClientDel(id=str(id), topic=topic, what=enum_what, hard=hard,
@ -208,6 +239,9 @@ def delMsg(id, topic, what, param, hard):
return msg
def noteMsg(id, topic, what, seq):
if not topic:
topic = default_topic
enum_what = None
if what == 'kp':
enum_what = pb.KP
@ -218,7 +252,7 @@ def noteMsg(id, topic, what, seq):
elif what == 'recv':
enum_what = pb.READ
seq = int(seq)
return pb.ClientMsg(note=pb.ClientNote(topic=topic, what=enum_what, seq_id=seq))
return pb.ClientMsg(note=pb.ClientNote(topic=topic, what=enum_what, seq_id=seq), on_behalf_of=default_user)
def parse_cmd(cmd):
"""Parses command line input into a dictionary"""
@ -227,7 +261,11 @@ def parse_cmd(cmd):
return None
parser = None
if parts[0] == "acc":
if parts[0] == ".use":
parser = argparse.ArgumentParser(prog=parts[0], description='Set default user or topic')
parser.add_argument('--user', default="unchanged", help='ID of the default user')
parser.add_argument('--topic', default="unchanged", help='Name of default topic')
elif parts[0] == "acc":
parser = argparse.ArgumentParser(prog=parts[0], description='Create or alter an account')
parser.add_argument('--user', default='new', help='ID of the account to update')
parser.add_argument('--scheme', default='basic', help='authentication scheme, default=basic')
@ -312,6 +350,7 @@ def parse_cmd(cmd):
else:
print("Unrecognized:", parts[0])
print("Possible commands:")
print("\t.use\t- set default user or topic")
print("\tacc\t- create account")
print("\tlogin\t- authenticate")
print("\tsub\t- subscribe to topic")
@ -340,7 +379,17 @@ def serialize_cmd(string, id):
return None
# Process dictionary
if cmd.cmd == "acc":
if cmd.cmd == ".use":
if cmd.user != "unchanged":
global default_user
default_user = cmd.user
stdoutln("Default user is '" + default_user + "'")
if cmd.topic != "unchanged":
global default_topic
default_topic = cmd.topic
stdoutln("Default topic is '" + default_topic + "'")
return None
elif cmd.cmd == "acc":
return accMsg(id, cmd.user, cmd.scheme, cmd.secret, cmd.uname, cmd.password,
cmd.do_login, cmd.fn, cmd.photo, cmd.private, cmd.auth, cmd.anon, cmd.tags, cmd.cred)
elif cmd.cmd == "login":
@ -349,10 +398,9 @@ def serialize_cmd(string, id):
return subMsg(id, cmd.topic, cmd.fn, cmd.photo, cmd.private, cmd.auth, cmd.anon,
cmd.mode, cmd.tags, cmd.get_query)
elif cmd.cmd == "leave":
return pb.ClientMsg(leave=pb.ClientLeave(id=str(id), topic=cmd.topic))
return leaveMsg(id, cmd.topic, cmd.unsub)
elif cmd.cmd == "pub":
return pb.ClientMsg(pub=pb.ClientPub(id=str(id), topic=cmd.topic, no_echo=True,
content=encode_to_bytes(cmd.content)))
return pubMsg(id, cmd.topic, cmd.content)
elif cmd.cmd == "get":
return getMsg(id, cmd.topic, cmd.desc, cmd.sub, cmd.tags, cmd.data)
elif cmd.cmd == "set":
@ -369,21 +417,22 @@ def serialize_cmd(string, id):
def gen_message(schema, secret):
"""Client message generator: reads user input as string,
converts to pb.ClientMsg, and yields"""
global input_thread
random.seed()
id = random.randint(10000,60000)
# Asynchronous input-output
input_thread = threading.Thread(target=stdin, args=(input_queue,))
input_thread.daemon = True
input_thread.start()
yield hiMsg(id)
if schema != None:
id += 1
yield loginMsg(id, schema, secret, None, None, None)
# Asynchronous input-output
input_thread = threading.Thread(target=stdin, args=(input_queue,))
input_thread.daemon = True
input_thread.start()
print_prompt = True
while True:
@ -410,11 +459,12 @@ def gen_message(schema, secret):
time.sleep(0.1)
def run(addr, schema, secret):
channel = grpc.insecure_channel(addr)
stub = pbx.NodeStub(channel)
# Call the server
stream = stub.MessageLoop(gen_message(schema, secret))
try:
channel = grpc.insecure_channel(addr)
stub = pbx.NodeStub(channel)
# Call the server
stream = stub.MessageLoop(gen_message(schema, secret))
# Read server responses
for msg in stream:
if msg.HasField("ctrl"):
@ -424,29 +474,34 @@ def run(addr, schema, secret):
del onCompletion[msg.ctrl.id]
if msg.ctrl.code >= 200 and msg.ctrl.code < 400:
func(msg.ctrl.params)
stdoutln(str(msg.ctrl.code) + " " + msg.ctrl.text)
stdoutln("\r" + str(msg.ctrl.code) + " " + msg.ctrl.text)
elif msg.HasField("data"):
stdoutln("\nFrom: " + msg.data.from_user_id + ":\n")
stdoutln(json.loads(msg.data.content) + "\n")
stdoutln("\rFrom: " + msg.data.from_user_id + ":\n")
stdoutln(json.loads(msg.data.content))
elif msg.HasField("pres"):
pass
elif msg.HasField("info"):
user = getattr(msg.info, 'from')
stdoutln("\rMessage #" + str(msg.info.seq) + " " + msg.info.what +
" by " + user + "; topic=" + msg.info.topic + "(" + msg.topic + ")")
else:
stdoutln("Message type not handled", msg)
stdoutln("\rMessage type not handled", msg)
except grpc._channel._Rendezvous as err:
stdoutln(err)
print(err)
channel.close()
if input_thread != None:
input_thread.join(0.3)
def read_cookie():
try:
cookie = open('.tn-cli-cookie', 'r')
params = json.load(cookie)
cookie.close()
if params.get("token") == None:
return None
return params
return params.get("token")
except Exception as err:
stdoutln("Missing or invalid cookie file '.tn-cli-cookie'", err)
println("Missing or invalid cookie file '.tn-cli-cookie'", err)
return None
def save_cookie(params):
@ -461,7 +516,7 @@ def save_cookie(params):
stdoutln("Authenticated as", nice.get('user'))
try:
cookie = open('.tn-cookie', 'w')
cookie = open('.tn-cli-cookie', 'w')
json.dump(nice, cookie)
cookie.close()
except Exception as err:
@ -496,20 +551,21 @@ if __name__ == '__main__':
"""Use token to login"""
schema = 'token'
secret = args.login_token.encode('acsii')
stdoutln("Logging in with token", args.login_token)
print("Logging in with token", args.login_token)
elif args.login_basic:
"""Use username:password"""
schema = 'basic'
secret = args.login_basic.encode('utf-8')
stdoutln("Logging in with login:password", args.login_basic)
print("Logging in with login:password", args.login_basic)
else:
"""Try reading the cookie file"""
try:
schema, secret = read_auth_cookie(args.login_cookie)
stdoutln("Logging in with cookie file", args.login_cookie)
schema = 'token'
secret = read_cookie()
print("Logging in with cookie file")
except Exception as err:
stdoutln("Failed to read authentication cookie", err)
print("Failed to read authentication cookie", err)
run(args.host, schema, secret)