mirror of
https://github.com/tinode/chat.git
synced 2025-03-14 10:05:07 +00:00
on_behalf_of seems to work now
This commit is contained in:
10
build-all.sh
10
build-all.sh
@ -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..."
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
|
@ -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
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
130
tn-cli/tn-cli.py
130
tn-cli/tn-cli.py
@ -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)
|
||||
|
Reference in New Issue
Block a user