多租户更改
This commit is contained in:
@@ -0,0 +1 @@
|
||||
.tn-cookie
|
||||
@@ -0,0 +1,119 @@
|
||||
# Tinode Chatbot
|
||||
|
||||
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 to Tinode server as a regular user over gRPC interface (see `grpc_listen` in [tinode.conf](../../server/tinode.conf) file). 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.
|
||||
|
||||
Generated files are provided for convenience in a [separate folder](../../py_grpc/tinode_grpc). You may re-generate them if needed:
|
||||
```
|
||||
python -m pip install grpcio-tools
|
||||
python -m grpc_tools.protoc -../../pbx --python_out=. --grpc_python_out=. ../../pbx/model.proto
|
||||
```
|
||||
|
||||
Chatbot expects gRPC binding to be provided as `tinode-grpc`. If you want to use them locally, first copy `model_pb2.py` and `model_pb2_grpc.py` to the same folder as `chatbot.py` then find the lines
|
||||
```
|
||||
from tinode_grpc import pb
|
||||
from tinode_grpc import pbx
|
||||
```
|
||||
in `chatbot.py` and replace them with
|
||||
```
|
||||
import model_pb2 as pb
|
||||
import model_pb2_grpc as pbx
|
||||
```
|
||||
|
||||
## Installing and running
|
||||
|
||||
### Using PIP
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
[gRPC](https://grpc.io/) requires [python](https://www.python.org/) 2.7 or 3.4 or higher.
|
||||
Make sure [pip](https://pip.pypa.io/en/stable/installing/) 9.0.1 or higher is installed.
|
||||
```
|
||||
$ python -m pip install --upgrade pip
|
||||
```
|
||||
If you cannot upgrade pip due to a system-owned installation, you can run install it in a `virtualenv`:
|
||||
```
|
||||
$ python -m pip install virtualenv
|
||||
$ virtualenv venv
|
||||
$ source venv/bin/activate
|
||||
$ python -m pip install --upgrade pip
|
||||
```
|
||||
|
||||
#### Install dependencies:
|
||||
```
|
||||
$ python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
On El Capitan OSX, you may get the following error:
|
||||
```
|
||||
$ OSError: [Errno 1] Operation not permitted: '/tmp/pip-qwTLbI-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/six-1.4.1-py2.7.egg-info'
|
||||
```
|
||||
You can work around this using:
|
||||
```
|
||||
$ python -m pip install tinode_grpc --ignore-installed
|
||||
```
|
||||
|
||||
### Run the chatbot
|
||||
|
||||
Start the [tinode server](../../INSTALL.md) first. Then start the chatbot with credentials of the user you want to be your bot, `alice` in this example:
|
||||
```
|
||||
python chatbot.py --login-basic=alice:alice123
|
||||
```
|
||||
If you want to run the bot in the background, start it as
|
||||
```
|
||||
nohup python chatbot.py --login-basic=alice:alice123 &
|
||||
```
|
||||
Run `python chatbot.py -h` for more options.
|
||||
|
||||
If you are using python 2, keep in mind that `condition.wait()` [is forever buggy](https://bugs.python.org/issue8844). As a consequence of this bug the bot cannot be terminated with a SIGINT. It has to be stopped with a SIGKILL.
|
||||
|
||||
You can use cookie file to store credentials. Sample cookie files are provided as `basic-cookie.sample` and `token-cookie.sample`. Once authenticated the bot will store the token in the cookie file, `.tn-cookie` by default. If you have a cookie file with the desired credentials, you can run the bot with no parameters:
|
||||
```
|
||||
python chatbot.py
|
||||
```
|
||||
|
||||
If the server is configured to use TLS, i.e. running as `httpS://my-server.example.com/`, the gRPC endpoint also uses the same SSL certificate. In that case add the `--ssl` option when starting the chatbot. If you want the chatbot to connect to the secure server over a local network or under a different name rather than the `my-server.example.com`, for instance as `localhost`, you must specify the SSL domain name to use, otherwise the server will not be able to find the right SSL certificate:
|
||||
```
|
||||
python chatbot.py --host=localhost:16060 --ssl --ssl-host=my-server.example.com
|
||||
```
|
||||
|
||||
Quotes are read from `quotes.txt` by default. The file is plain text with one quote per line.
|
||||
|
||||
|
||||
### Using Docker
|
||||
|
||||
**Warning!** Although the chatbot itself is less than 11KB, the chatbot Docker image is 175MB: the `:slim` Python 3 image is about 140MB, gRPC adds another ~30MB.
|
||||
|
||||
1. Follow [instructions](../../docker/README.md) to build and run dockerized Tinode chat server up to and including _step 3_.
|
||||
|
||||
2. In _step 4_ run the server adding `--env PLUGIN_PYTHON_CHAT_BOT_ENABLED=true` and `--volume botdata:/botdata` to the command line:
|
||||
1. **RethinkDB**:
|
||||
```
|
||||
$ docker run -p 6060:18080 -d --name tinode-srv --env PLUGIN_PYTHON_CHAT_BOT_ENABLED=true --volume botdata:/botdata --network tinode-net tinode/tinode-rethink:latest
|
||||
```
|
||||
2. **MySQL**:
|
||||
```
|
||||
$ docker run -p 6060:18080 -d --name tinode-srv --env PLUGIN_PYTHON_CHAT_BOT_ENABLED=true --volume botdata:/botdata --network tinode-net tinode/tinode-mysql:latest
|
||||
```
|
||||
3. **MongoDB**:
|
||||
```
|
||||
$ docker run -p 6060:18080 -d --name tinode-srv --env PLUGIN_PYTHON_CHAT_BOT_ENABLED=true --volume botdata:/botdata --network tinode-net tinode/tinode-mongodb:latest
|
||||
```
|
||||
|
||||
3. Run the chatbot
|
||||
```
|
||||
$ docker run -d --name tino-chatbot --network tinode-net --volume botdata:/botdata tinode/chatbot:latest
|
||||
```
|
||||
|
||||
4. Test that the bot is functional by pointing your browser to http://localhost:6060/, login and talk to user `Tino`. The user should respond to every message with a random quote.
|
||||
|
||||
|
||||
You may replace the `:latest` with a different tag. See all available tags here:
|
||||
* [Tinode-MySQL tags](https://hub.docker.com/r/tinode/tinode-mysql/tags/)
|
||||
* [Tinode-RethinkDB tags](https://hub.docker.com/r/tinode/tinode-rethink/tags/)
|
||||
* [Tinode-MongoDB tags](https://hub.docker.com/r/tinode/tinode-mongodb/tags/)
|
||||
* [Chatbot tags](https://hub.docker.com/r/tinode/chatbot/tags/)
|
||||
|
||||
In general try to use docker images all with the same tag.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"schema": "basic", "secret": "alice:alice123"}
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
"""Python implementation of a Tinode chatbot."""
|
||||
|
||||
# For compatibility between python 2 and 3
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
from concurrent import futures
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
try:
|
||||
from importlib.metadata import version
|
||||
except ImportError:
|
||||
# Fallback for Python < 3.8
|
||||
from importlib_metadata import version
|
||||
import platform
|
||||
try:
|
||||
import Queue as queue
|
||||
except ImportError:
|
||||
import queue
|
||||
import random
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
import grpc
|
||||
from google.protobuf.json_format import MessageToDict
|
||||
|
||||
# Import generated grpc modules
|
||||
from tinode_grpc import pb
|
||||
from tinode_grpc import pbx
|
||||
|
||||
# For compatibility with python2
|
||||
if sys.version_info[0] >= 3:
|
||||
unicode = str
|
||||
|
||||
APP_NAME = "Tino-chatbot"
|
||||
APP_VERSION = "1.2.3"
|
||||
LIB_VERSION = version("tinode_grpc")
|
||||
|
||||
# Maximum length of string to log. Shorten longer strings.
|
||||
MAX_LOG_LEN = 64
|
||||
|
||||
# User ID of the current user
|
||||
botUID = None
|
||||
|
||||
# Dictionary wich contains lambdas to be executed when server response is received
|
||||
onCompletion = {}
|
||||
|
||||
# This is needed for gRPC ssl to work correctly.
|
||||
os.environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA"
|
||||
|
||||
def log(*args):
|
||||
print(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3], *args)
|
||||
|
||||
# Add bundle for future execution
|
||||
def add_future(tid, bundle):
|
||||
onCompletion[tid] = bundle
|
||||
|
||||
# Shorten long strings for logging.
|
||||
def clip_long_string(obj):
|
||||
if isinstance(obj, unicode) or isinstance(obj, str):
|
||||
if len(obj) > MAX_LOG_LEN:
|
||||
return '<' + str(len(obj)) + ' bytes: ' + obj[:12] + '...' + obj[-12:] + '>'
|
||||
return obj
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
return [clip_long_string(item) for item in obj]
|
||||
elif isinstance(obj, dict):
|
||||
return dict((key, clip_long_string(val)) for key, val in obj.items())
|
||||
else:
|
||||
return obj
|
||||
|
||||
def to_json(msg):
|
||||
return json.dumps(clip_long_string(MessageToDict(msg)))
|
||||
|
||||
# Resolve or reject the future
|
||||
def exec_future(tid, code, text, params):
|
||||
bundle = onCompletion.get(tid)
|
||||
if bundle != None:
|
||||
del onCompletion[tid]
|
||||
try:
|
||||
if code >= 200 and code < 400:
|
||||
arg = bundle.get('arg')
|
||||
bundle.get('onsuccess')(arg, params)
|
||||
else:
|
||||
log("Error: {} {} ({})".format(code, text, tid))
|
||||
onerror = bundle.get('onerror')
|
||||
if onerror:
|
||||
onerror(bundle.get('arg'), {'code': code, 'text': text})
|
||||
except Exception as err:
|
||||
log("Error handling server response", err)
|
||||
|
||||
# List of active subscriptions
|
||||
subscriptions = {}
|
||||
def add_subscription(topic):
|
||||
subscriptions[topic] = True
|
||||
|
||||
def del_subscription(topic):
|
||||
subscriptions.pop(topic, None)
|
||||
|
||||
def subscription_failed(topic, errcode):
|
||||
if topic == 'me':
|
||||
# Failed 'me' subscription means the bot is disfunctional.
|
||||
if errcode.get('code') == 502:
|
||||
# Cluster unreachable. Break the loop and retry in a few seconds.
|
||||
client_post(None)
|
||||
else:
|
||||
exit(1)
|
||||
|
||||
def login_error(unused, errcode):
|
||||
# Check for 409 "already authenticated".
|
||||
if errcode.get('code') != 409:
|
||||
exit(1)
|
||||
|
||||
def server_version(params):
|
||||
if params == None:
|
||||
return
|
||||
log("Server:", params['build'].decode('ascii'), params['ver'].decode('ascii'))
|
||||
|
||||
def next_id():
|
||||
next_id.tid += 1
|
||||
return str(next_id.tid)
|
||||
next_id.tid = 100
|
||||
|
||||
# Quotes from the fortune cookie file
|
||||
quotes = []
|
||||
|
||||
def next_quote():
|
||||
idx = random.randrange(0, len(quotes))
|
||||
# Make sure quotes are not repeated
|
||||
while idx == next_quote.idx:
|
||||
idx = random.randrange(0, len(quotes))
|
||||
next_quote.idx = idx
|
||||
return quotes[idx]
|
||||
next_quote.idx = 0
|
||||
|
||||
# This is the class for the server-side gRPC endpoints
|
||||
class Plugin(pbx.PluginServicer):
|
||||
def Account(self, acc_event, context):
|
||||
action = None
|
||||
if acc_event.action == pb.CREATE:
|
||||
action = "created"
|
||||
# TODO: subscribe to the new user.
|
||||
|
||||
elif acc_event.action == pb.UPDATE:
|
||||
action = "updated"
|
||||
elif acc_event.action == pb.DELETE:
|
||||
action = "deleted"
|
||||
else:
|
||||
action = "unknown"
|
||||
|
||||
log("Account", action, ":", acc_event.user_id, acc_event.public)
|
||||
|
||||
return pb.Unused()
|
||||
|
||||
queue_out = queue.Queue()
|
||||
|
||||
def client_generate():
|
||||
while True:
|
||||
msg = queue_out.get()
|
||||
if msg == None:
|
||||
return
|
||||
log("out:", to_json(msg))
|
||||
yield msg
|
||||
|
||||
def client_post(msg):
|
||||
queue_out.put(msg)
|
||||
|
||||
def client_reset():
|
||||
# Drain the queue
|
||||
try:
|
||||
while queue_out.get(False) != None:
|
||||
pass
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
def hello():
|
||||
tid = next_id()
|
||||
add_future(tid, {
|
||||
'onsuccess': lambda unused, params: server_version(params),
|
||||
})
|
||||
return pb.ClientMsg(hi=pb.ClientHi(id=tid, user_agent=APP_NAME + "/" + APP_VERSION + " (" +
|
||||
platform.system() + "/" + platform.release() + "); gRPC-python/" + LIB_VERSION,
|
||||
ver=LIB_VERSION, lang="EN"))
|
||||
|
||||
def login(cookie_file_name, scheme, secret):
|
||||
tid = next_id()
|
||||
add_future(tid, {
|
||||
'arg': cookie_file_name,
|
||||
'onsuccess': lambda fname, params: on_login(fname, params),
|
||||
'onerror': lambda unused, errcode: login_error(unused, errcode),
|
||||
})
|
||||
return pb.ClientMsg(login=pb.ClientLogin(id=tid, scheme=scheme, secret=secret))
|
||||
|
||||
def subscribe(topic):
|
||||
tid = next_id()
|
||||
add_future(tid, {
|
||||
'arg': topic,
|
||||
'onsuccess': lambda topicName, unused: add_subscription(topicName),
|
||||
'onerror': lambda topicName, errcode: subscription_failed(topicName, errcode),
|
||||
})
|
||||
return pb.ClientMsg(sub=pb.ClientSub(id=tid, topic=topic))
|
||||
|
||||
def leave(topic):
|
||||
tid = next_id()
|
||||
add_future(tid, {
|
||||
'arg': topic,
|
||||
'onsuccess': lambda topicName, unused: del_subscription(topicName)
|
||||
})
|
||||
return pb.ClientMsg(leave=pb.ClientLeave(id=tid, topic=topic))
|
||||
|
||||
def publish(topic, text):
|
||||
tid = next_id()
|
||||
return pb.ClientMsg(pub=pb.ClientPub(id=tid, topic=topic, no_echo=True,
|
||||
head={"auto": json.dumps(True).encode('utf-8')}, content=json.dumps(text).encode('utf-8')))
|
||||
|
||||
def note_read(topic, seq):
|
||||
return pb.ClientMsg(note=pb.ClientNote(topic=topic, what=pb.READ, seq_id=seq))
|
||||
|
||||
def init_server(listen):
|
||||
# Launch plugin server: accept connection(s) from the Tinode server.
|
||||
server = grpc.server(futures.ThreadPoolExecutor(max_workers=16))
|
||||
pbx.add_PluginServicer_to_server(Plugin(), server)
|
||||
server.add_insecure_port(listen)
|
||||
server.start()
|
||||
|
||||
log("Plugin server running at '"+listen+"'")
|
||||
|
||||
return server
|
||||
|
||||
def init_client(addr, schema, secret, cookie_file_name, secure, ssl_host):
|
||||
log("Connecting to", "secure" if secure else "", "server at", addr,
|
||||
"SNI="+ssl_host if ssl_host else "")
|
||||
|
||||
channel = None
|
||||
if secure:
|
||||
opts = (('grpc.ssl_target_name_override', ssl_host),) if ssl_host else None
|
||||
channel = grpc.secure_channel(addr, grpc.ssl_channel_credentials(), opts)
|
||||
else:
|
||||
channel = grpc.insecure_channel(addr)
|
||||
|
||||
# Call the server
|
||||
stream = pbx.NodeStub(channel).MessageLoop(client_generate())
|
||||
|
||||
# Session initialization sequence: {hi}, {login}, {sub topic='me'}
|
||||
client_post(hello())
|
||||
client_post(login(cookie_file_name, schema, secret))
|
||||
|
||||
return stream
|
||||
|
||||
def client_message_loop(stream):
|
||||
try:
|
||||
# Read server responses
|
||||
for msg in stream:
|
||||
log(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3], "in:", to_json(msg))
|
||||
|
||||
if msg.HasField("ctrl"):
|
||||
# Run code on command completion
|
||||
exec_future(msg.ctrl.id, msg.ctrl.code, msg.ctrl.text, msg.ctrl.params)
|
||||
|
||||
elif msg.HasField("data"):
|
||||
# log("message from:", msg.data.from_user_id)
|
||||
|
||||
# Protection against the bot talking to self from another session.
|
||||
if msg.data.from_user_id != botUID:
|
||||
# Respond to message.
|
||||
# Mark received message as read
|
||||
client_post(note_read(msg.data.topic, msg.data.seq_id))
|
||||
# Insert a small delay to prevent accidental DoS self-attack.
|
||||
time.sleep(0.1)
|
||||
# Respond with a witty quote
|
||||
client_post(publish(msg.data.topic, next_quote()))
|
||||
|
||||
elif msg.HasField("pres"):
|
||||
# log("presence:", msg.pres.topic, msg.pres.what)
|
||||
# Wait for peers to appear online and subscribe to their topics
|
||||
if msg.pres.topic == 'me':
|
||||
if (msg.pres.what == pb.ServerPres.ON or msg.pres.what == pb.ServerPres.MSG) \
|
||||
and subscriptions.get(msg.pres.src) == None:
|
||||
client_post(subscribe(msg.pres.src))
|
||||
elif msg.pres.what == pb.ServerPres.OFF and subscriptions.get(msg.pres.src) != None:
|
||||
client_post(leave(msg.pres.src))
|
||||
|
||||
else:
|
||||
# Ignore everything else
|
||||
pass
|
||||
|
||||
except grpc._channel._Rendezvous as err:
|
||||
log("Disconnected:", err)
|
||||
|
||||
def read_auth_cookie(cookie_file_name):
|
||||
"""Read authentication token from a file"""
|
||||
cookie = open(cookie_file_name, 'r')
|
||||
params = json.load(cookie)
|
||||
cookie.close()
|
||||
schema = params.get("schema")
|
||||
secret = None
|
||||
if schema == None:
|
||||
return None, None
|
||||
if schema == 'token':
|
||||
secret = base64.b64decode(params.get('secret').encode('utf-8'))
|
||||
else:
|
||||
secret = params.get('secret').encode('utf-8')
|
||||
return schema, secret
|
||||
|
||||
def on_login(cookie_file_name, params):
|
||||
global botUID
|
||||
client_post(subscribe('me'))
|
||||
|
||||
"""Save authentication token to file"""
|
||||
if params == None or cookie_file_name == None:
|
||||
return
|
||||
|
||||
if 'user' in params:
|
||||
botUID = params['user'].decode("ascii")[1:-1]
|
||||
|
||||
# Protobuf map 'params' is not a python object or dictionary. Convert it.
|
||||
nice = {'schema': 'token'}
|
||||
for key_in in params:
|
||||
if key_in == 'token':
|
||||
key_out = 'secret'
|
||||
else:
|
||||
key_out = key_in
|
||||
nice[key_out] = json.loads(params[key_in].decode('utf-8'))
|
||||
|
||||
try:
|
||||
cookie = open(cookie_file_name, 'w')
|
||||
json.dump(nice, cookie)
|
||||
cookie.close()
|
||||
except Exception as err:
|
||||
log("Failed to save authentication cookie", err)
|
||||
|
||||
def load_quotes(file_name):
|
||||
with open(file_name) as f:
|
||||
for line in f:
|
||||
quotes.append(line.strip())
|
||||
|
||||
return len(quotes)
|
||||
|
||||
def run(args):
|
||||
schema = None
|
||||
secret = None
|
||||
|
||||
if args.login_token:
|
||||
"""Use token to login"""
|
||||
schema = 'token'
|
||||
secret = args.login_token.encode('ascii')
|
||||
log("Logging in with token", args.login_token)
|
||||
|
||||
elif args.login_basic:
|
||||
"""Use username:password"""
|
||||
schema = 'basic'
|
||||
secret = args.login_basic.encode('utf-8')
|
||||
log("Logging in with login:password", args.login_basic)
|
||||
|
||||
else:
|
||||
"""Try reading the cookie file"""
|
||||
try:
|
||||
schema, secret = read_auth_cookie(args.login_cookie)
|
||||
log("Logging in with cookie file", args.login_cookie)
|
||||
except Exception as err:
|
||||
log("Failed to read authentication cookie", err)
|
||||
|
||||
if schema:
|
||||
# Load random quotes from file
|
||||
log("Loaded {} quotes".format(load_quotes(args.quotes)))
|
||||
|
||||
# Start Plugin server
|
||||
server = init_server(args.listen)
|
||||
|
||||
# Initialize and launch client
|
||||
client = init_client(args.host, schema, secret, args.login_cookie, args.ssl, args.ssl_host)
|
||||
|
||||
# Setup closure for graceful termination
|
||||
def exit_gracefully(signo, stack_frame):
|
||||
log("Terminated with signal", signo)
|
||||
server.stop(0)
|
||||
client.cancel()
|
||||
sys.exit(0)
|
||||
|
||||
# Add signal handlers
|
||||
signal.signal(signal.SIGINT, exit_gracefully)
|
||||
signal.signal(signal.SIGTERM, exit_gracefully)
|
||||
|
||||
# Run blocking message loop in a cycle to handle
|
||||
# server being down.
|
||||
while True:
|
||||
client_message_loop(client)
|
||||
time.sleep(3)
|
||||
client_reset()
|
||||
client = init_client(args.host, schema, secret, args.login_cookie, args.ssl, args.ssl_host)
|
||||
|
||||
# Close connections gracefully before exiting
|
||||
server.stop(None)
|
||||
client.cancel()
|
||||
|
||||
else:
|
||||
log("Error: authentication scheme not defined")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
"""Parse command-line arguments. Extract server host name, listen address, authentication scheme"""
|
||||
random.seed()
|
||||
|
||||
purpose = "Tino, Tinode's chatbot."
|
||||
log(purpose)
|
||||
parser = argparse.ArgumentParser(description=purpose)
|
||||
parser.add_argument('--host', default='localhost:16060', help='address of Tinode server gRPC endpoint')
|
||||
parser.add_argument('--ssl', action='store_true', help='use SSL to connect to the server')
|
||||
parser.add_argument('--ssl-host', help='SSL host name to use instead of default (useful for connecting to localhost)')
|
||||
parser.add_argument('--listen', default='0.0.0.0:40051', help='address to listen on for incoming Plugin API calls')
|
||||
parser.add_argument('--login-basic', help='login using basic authentication username:password')
|
||||
parser.add_argument('--login-token', help='login using token authentication')
|
||||
parser.add_argument('--login-cookie', default='.tn-cookie', help='read credentials from the provided cookie file')
|
||||
parser.add_argument('--quotes', default='quotes.txt', help='file with messages for the chatbot to use, one message per line')
|
||||
args = parser.parse_args()
|
||||
|
||||
run(args)
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
login:
|
||||
$3,000,000
|
||||
1 bulls, 3 cows
|
||||
A Cray is the best machine for simulating the performance of a Cray.
|
||||
A consistent indentation style is the hobgoblin of little minds.
|
||||
A gentleman is one who is never rude unintentionally. -Noel Coward
|
||||
A man must destroy himself before others can destroy him. -Mong Tse
|
||||
A philosopher does not need a torch to gather glow-worms by at mid-day. --Earnest Bramah
|
||||
A song in time is worth a dime.
|
||||
A woman is only a woman, but a good cigar is a smoke. -Rudyard Kipling
|
||||
Admiration is our polite recognition of another's resemblance to ourselves.
|
||||
All the good ones are taken.
|
||||
An atheist is a man with no invisible means of support.
|
||||
Any country with "democratic" in the title isn't.
|
||||
Are we not men?
|
||||
Attend winter sheep meetings. Learning never ends!
|
||||
Be the sea, and see me be.
|
||||
Beware: the light at the end of the tunnel may be New Jersey.
|
||||
Bubble bubble, toil and trouble; cast that float into a double.
|
||||
C'est dommage, mais c'est vrai.
|
||||
Caution: Do not view laser light with remaining eye.
|
||||
Cogito cogito ergo cogito sum.
|
||||
Crazee Edeee, his prices are INSANE!!!
|
||||
Death to all fanatics!
|
||||
Disk crisis, please clean up!
|
||||
Do not meddle in the mouth.
|
||||
Don't be overly suspicious where it's not warranted.
|
||||
Don't let your thoughts get in a rut. The knife which spreads may also cut.
|
||||
Don't worry if it doesn't work right; if everything did, you'd be out of a job.
|
||||
E Pluribus Unix.
|
||||
Either we are alone or we are not. Either way is mind-boggling.
|
||||
Even these days, it's not as easy to go crazy as you think.
|
||||
Everybody should believe in something -- I believe I'll have another drink.
|
||||
Exercise is the Yuppie version of bulemia.
|
||||
Far too noisy, my dear Mozart. Far too many notes. -Emperor Ferdinand.
|
||||
First things first. Why not send for the Nazis right now.
|
||||
Fortran est; non potest legi.
|
||||
Generalizations are useful. The work contained in them can be reckoned as labor
|
||||
God does not play dice.
|
||||
Good day to avoid cops. Crawl to work.
|
||||
Great shot, kid. That was one in a million.
|
||||
Have you done your Christmas chopping yet? -anon. White House Advisor 12/24/81
|
||||
He was al coltissh, ful of ragerye,/And ful of jargon as a flekke pye. -Chaucer
|
||||
He who listens last is the last one listening.
|
||||
History is a race between education and catastrophe. -H. G. Wells
|
||||
How do I love thee? Hand me my calculator...
|
||||
I am a high-pressure guy, and I didn't take this job to conduct a going-out-of-business sale. - A.A. Penzias
|
||||
I don't even know what street Canada is on. - Al Capone
|
||||
I have the most perfect confidence in your indiscretion.
|
||||
I must have slipped a disk; my pack hurts.
|
||||
I think that I shall never see a billboard as lovely as a tree. -Ogden Nash
|
||||
I'd rather have my mail delivered by Lockheed than ride in a plane built by the Post Office.
|
||||
IOT trap -- core dumped
|
||||
If I had to choose between System V and 4.2, I'd resign. - Peter Honeyman
|
||||
If butterflies had teeth like tigers they would never make it out of the hangar.
|
||||
If it's not broken, don't fix it.
|
||||
If the shoe fits, buy the other one, too.
|
||||
If you don't care where you are, then you ain't lost.
|
||||
If you take the last cup, make a new pot.
|
||||
If your experiment needs statistics, you ought to have done a better experiment. - E. Rutherford
|
||||
In challenging a kzin, a simple scream of rage is sufficient.
|
||||
In this world, truth can wait; she's used to it.
|
||||
It is better to have loved and lost than just to have lost.
|
||||
It is useless to put on your brakes when you're upside down. -Paul Newman
|
||||
It's a small world, but I'd hate to have to paint it.
|
||||
It's hard to love someone who looks down on you because your hands get bloody protecting him.
|
||||
It's not the time between the takes that takes the time - it's changing your mind between the takes that takes the time. -Stage Dept.
|
||||
Join me and I will complete your training.
|
||||
Lando's not a system, he's a man. He's a gambler, scoundrel. You'd like him.
|
||||
Let sleeping wraiths lie.
|
||||
Like winter snow on summer lawn, time past is time gone.
|
||||
Lose a few, lose a few.
|
||||
Macro context switch under way, please do not log out!
|
||||
Man is in doubt to deem himself a god or beast. -Alexander Pope
|
||||
May you live all the days of your life.
|
||||
Mind your own business, Spock. I'm sick of your halfbreed interference.
|
||||
Multilevel standards are like onions. They're smelly and make you cry a lot. -Ron Natalie
|
||||
Never attribute to malice what can be found in scientific american, under computer recreations.
|
||||
New career ideas are worth pursuing.
|
||||
No man's life, liberty or property are safe while the legislature is in session.
|
||||
Non serviam.
|
||||
Nothing of interest ever happened on this day.
|
||||
Oh, so there you are!
|
||||
One scythe fits all.
|
||||
Otto's too so-so to toss soot, too sot to toot SOSs. So?
|
||||
People get it into their heads that this is a democracy. Well it isn't. -gwl
|
||||
Personality is a flimsy thing on which to build an art. -John Cage
|
||||
Populus vult decipi.
|
||||
Promptness is its own reward, if one lives by the clock instead of the sword.
|
||||
Real programmers can't say `lint' without adding `hbaxcu' -Wm Leler
|
||||
Remember the Unknown Buffalo.
|
||||
Rog-O-Matic callidus est.
|
||||
Say "no" to long-sleeved shirts!! Support your right to bare arms!
|
||||
Señor, if you hurry from here, you will wait longer there. -Mexico taxi driver
|
||||
Smoked carp is terrible unless you're out of smoked salmon.
|
||||
Some men are discovered; others are found out.
|
||||
Specialization is for insects. -Robt. A. Heinlein
|
||||
Stop searching. Happiness is right next to you.
|
||||
System going down for 5 minutes -- back up in barrels.
|
||||
Technological unemployment is total today -- 300 years ago we were all farmers.
|
||||
That's the nice thing about standards -- there's so many to choose from. -trb
|
||||
The Luddites always lose. Always.
|
||||
The average legislator is somewhere nearly all the time. -Herb Nore
|
||||
The dawning of the Information Age is bringing about dramatic changes in the fundamental fabric of our civilization. - AA Penzias
|
||||
The first piece of luggage out of the chute doesn't belong to anyone, ever.
|
||||
The hippo has no sting, but the wise man would rather be sat upon by the bee.
|
||||
The meek shall inherit the earth -- they are pronounced "o".
|
||||
The one interesting fact about the Diplodocus is that the accent is on the second syllable.
|
||||
The plural of spouse is spice.
|
||||
The skeletons in the cupboard will all come out in the wash.
|
||||
The universe is laughing behind your back.
|
||||
Them that dishes it out need not fall over every time someone blows hard.
|
||||
There is no fear in love; but perfect love casteth out fear.
|
||||
There'll always be an England - if not it would be necessary to invent one.
|
||||
These widows, sir, are the most perverse creatures in the world. - Joseph Addison.
|
||||
Things will be bright in P.M. A cop will shine a light in your face.
|
||||
This space available. Call 686-7600 for details.
|
||||
Those who in quarrels interpose must often wipe a bloody nose.
|
||||
To be is to be related.
|
||||
To play billiards well is a sign of an ill-spent youth.
|
||||
Toto, I've a feeling we're not in any immediate danger of having just committed suicide!
|
||||
Try a new system or a different approach.
|
||||
Uneasy lies the head that wears a crown.
|
||||
Veni, vidi, maeni, mo, cacha tigrem baedas to, iffi hollers, ledem go, veni, vidi, maeni, mo.
|
||||
Warning: this fortune may change your life.
|
||||
We don't know half of what we know.
|
||||
We retard what we cannot repel, we palliate what we cannot cure. -Johnson
|
||||
We're too close to System Test.
|
||||
What garlic is to salad, insanity is to art.
|
||||
When all else fails, read the instructions.
|
||||
When in trouble or in doubt, run in circles; scream and shout.
|
||||
Whenever I see his fingernails, I thank God I don't have to look at his feet.
|
||||
Whom the gods must destroy they first must drive insane.
|
||||
Without alkaloids, life itself would be impossible.
|
||||
Yes, the red switch.
|
||||
You can tell a man by the company that keeps him.
|
||||
You cannot buy beer; you can only rent it.
|
||||
You have bills.
|
||||
You look strong enough to pull the ears off a Gundark!
|
||||
You should go home.
|
||||
You will never find a more wretched hive of scum and villainy.
|
||||
Your computer account is overdrawn. Please reauthorize.
|
||||
Zero is greater than minus zero, but don't ask by how much. -6600 ref. manual
|
||||
`is false when preceded by its quotation' is false when preceded by its quotation.
|
||||
fortune: not found
|
||||
pic: 5 X 58008 picture shrunk to 0.000603365 X 7
|
||||
uuxqt cmd (rnews ) status (ucsfcgl!uucp 256)
|
||||
In days of yore, the crab and the crayfish lived in the forest.
|
||||
* Method As described above, see details below.
|
||||
Whatever happens, happens because it must.
|
||||
Boost, don't knock
|
||||
pass 2 error:(file ) more than 100 args?
|
||||
Nepal premier won't resign.
|
||||
init: /dev/console: getty failing, sleeping
|
||||
Sentence without verb.
|
||||
That's the way I got promoted, by eating everything. -pjw
|
||||
Pittsburgh has become a kind of knowledge aircraft carrier, its "top-guns" scattered regularly around the planet.
|
||||
When the music stops, the house of cards collapses and the emperor is found to be wearing no clothes.
|
||||
Never put snow on a frostbitten part.
|
||||
Put a smoke detector in your vacation cottage.
|
||||
Draw up a family fire-escape plan.
|
||||
A mathematician is a machine for turning coffee into theorems. -Paul Erdös
|
||||
I'm TRYING to be a back end! - A Hume
|
||||
There are only 26 calls and most of them are trivial.
|
||||
162 is unimplemented
|
||||
Incest more common than thought in United States
|
||||
The product classroom is marked pass-fail.
|
||||
ISDN is real and implementable.
|
||||
To dissimulate is to feign not to have what one has. - J Baudrillard
|
||||
Vacuums are nothings. We only mention them to let them know we know they're there.
|
||||
Two wrongs don't make a right; three lefts do.
|
||||
?12 Machine check during machine check.
|
||||
The downside of having an architecture is wart-for-wart compatibility. - Bob Willard, DEC
|
||||
It doesn't matter if you don't know how your program works, so long as it's parallel -R. O'Keefe
|
||||
sendmail[94] AA00493: SYSERR: net hang reading from coma: Connection timed out during greeting wait with coma
|
||||
Performance doesn't matter if your product is sufficiently feature-rich. --SF system engineer
|
||||
PLEASE LOG IN TO 3B20'S AT 4800 BAUD.
|
||||
SQUASH, do not crush (seen on a vegetable crate)
|
||||
If you get to meet sufficently important people, it's ok to debase yourself. -pjw
|
||||
The system is ready.
|
||||
A watermelon will not ripen in your armpit.
|
||||
Contrary to English and other similar languages, Turkish can be hyphenated with a simple 4 state finite-state machine.
|
||||
nop...session...attach...clone...walk...open...
|
||||
Spare me your sorrow's tears.
|
||||
He is one inch good, one foot evil.
|
||||
Heaven cannot use two suns or a house two masters.
|
||||
To give ground is sometimes the best victory.
|
||||
No word can cut kindness.
|
||||
Let wisdom and virtue be the two wheels of your cart.
|
||||
Willow branches never snap under the weight of snow.
|
||||
Only a monkey tries to catch the full moon in the pond.
|
||||
Don't lug dirt to a hilltop.
|
||||
Don't paint on water or carve on ice.
|
||||
The nail that raises its head is hammered down.
|
||||
Who can tell the he-crow from his mate?
|
||||
He is wise who knows what is enough.
|
||||
His hand was bitten by his own dog.
|
||||
To kill a general first shoot his horse.
|
||||
What is left unsaid is rich as flowers.
|
||||
If you are in a hurry go round-about.
|
||||
Better be ignorant than mistaught.
|
||||
You can't judge widows or horses without handling them.
|
||||
Don't use the ox-cleaver to kill a hen.
|
||||
Two hearts: and only one body.
|
||||
Bread is better than blossoms.
|
||||
Good medicine has often a bitter smack.
|
||||
First among blossoms the cherry: among men the warrior.
|
||||
You can't wrap up the wind or tie down the shadow.
|
||||
You cannot live in the same world with your father's murderer.
|
||||
Even a starving hawk won't lower himself to eat corn.
|
||||
Keep your mouth shut, your eyes open.
|
||||
Some ride in palanquins, some bear palanquins: some weave sandals for palanquin-bearers.
|
||||
Tuning filesystem for rot 0...
|
||||
Two things will make you lose your earrings, and one of them's dancing. -Bonnie Raitt.
|
||||
When the humans are away, the monkeys enter the hut, eat up the maize, and rearrange the furniture.
|
||||
The problem is not getting ksh to execute any particular command, the problem is recognizing that there might be a problem.
|
||||
diff: usage diff [whatever] etc.
|
||||
Intense opportunities for reorganization
|
||||
Stringent ideas in the upper echelon mind
|
||||
Leveraged brainpower at the labs
|
||||
>>>>>>> REMOVE ALL YOUR FILES AND DIRECTORIES NOW! <<<<<<<
|
||||
UX:lp: ERROR: Can't establish contact with the LP print service.
|
||||
Awk is one of the world's greatest collections of surprises. -Doug McIlroy
|
||||
If you think awk is the perfect programming language for the problem, you don't understand the problem yet. -Rob Pike
|
||||
``Workers of the World, forgive us!'' (a banner in a Moscow counter-rally, Oct 8, 1989)
|
||||
The first step is to determine what the remaining steps are. -Mark Horton
|
||||
If you do something stupid on UNIX you generally get strange behavior. -Doug Gwyn
|
||||
I'd still like you to explain that worm to me - Judge Munson to Robert T. Morris
|
||||
Like raisins in a bread pudding, the moments lie within the body of Henry.
|
||||
A real gentleman never takes bases unless he really has to.
|
||||
There are two proteins involved in DNA synthesis, they are called DNAsynthase 1 and DNAsynthase 3.
|
||||
Drawing on my fine command of the English language, I said nothing. -Robert Benchley
|
||||
Hay, be seedy! He-effigy, hate-shy jaky yellow man, oh peek, you are rusty, you've edible, you ex-wise he!
|
||||
The FSF is not overly concerned about security. - FSF
|
||||
Make: Don't know how your program works
|
||||
OSIfy, v.: To make code impenetrable.
|
||||
Rule 3: If the character is comprised of a container without another radical, then
|
||||
An economic reality of our time: computerized job deskilling. - a book review in Science
|
||||
Estne ebriamen de furfure avenaceo factum?
|
||||
The isomer with the higher dipole moment has the higher physical constants, regardless of the heat content. Van Arkel Rule
|
||||
Other factors being equal, the metal which is most susceptible to failure is that with the lowest boiling point. Mogro-Campero Rule
|
||||
The solid particle erosion rate of annealed face-centered cubic metals is inversely related to their hardness. Finnie-Wolak-Kabil Rule
|
||||
In dichroic crystals, the faster ray is less absorbed. Babinet Rule
|
||||
If you can't stand the heat, get a pool.
|
||||
A rolling stone is a singing rock group.
|
||||
UX:mail: INFO: No mail.
|
||||
Real software has its own 800 support line. - Stu Feldman
|
||||
Marine math: 2 beers times 39 Marines is 49 cases.
|
||||
When times are bad, people feel compelled to overeat.
|
||||
Of the physical pages in use, 3436738592 pages are permanently allocated to VMS.
|
||||
If you carry on, head-to-head, on `solution' you don't get anywhere
|
||||
This is like ignoring both the speed limit and the odometer in your car. It won't get you far. -Kenneth P. Birman
|
||||
/bin/ls: exec header invalid
|
||||
My pile of equipment is bigger than your pile of equipment -- philw
|
||||
Connected to 192.65.218.43.
|
||||
as1: Error: ../bpvvv.c, line 1324: Too many float literals--compile with "-Wb,-nopool"
|
||||
The helpful thought for which you look/Is written somewhere in a book.
|
||||
Is the tool broadly supported or maintained?
|
||||
I'm pulling *something* here. - Dom Marotta
|
||||
I'm drawing a line under the sand. - John Major
|
||||
They bit the wrong chicken's head off with their own teeth and got blood all over their shirt - nls
|
||||
I just want a bare-boned, straight EMACS. - Rae McLellan
|
||||
*** Message content is not printable: delete, write or save it to a file ***
|
||||
1181258 SAVECORE SEGMENTATION FAULT WHILE TRYING TO SAVE CORE
|
||||
Everything that can ever be invented has been invented -Charles H. Duell,
|
||||
Dan Quayle addressing the United Negro College Fund
|
||||
Here in Nuremberg, information hiding is much more popular in the organization than it is in the software...
|
||||
License Error : The license for this product(SPARCompiler C) has expired
|
||||
My favorite thing about the Internet is that you get to go into the private world of real creeps without having to smell them. - Penn Jillette
|
||||
Music is the pleasure the human soul experiences from counting without being aware that it is counting. -Leibnitz
|
||||
Millenium parties/with loud music, lights and joy;/then, how cold!
|
||||
warning: Hit heuristic-fence-post without finding enclosing function for address 0xfa2e470
|
||||
War in the Clinton era is just P.R. by other means. - Michael Hirschorn
|
||||
Application Developer's Architecture Guide
|
||||
! TeX capacity exceeded, sorry [main memory size=263001].
|
||||
His hand was bitten by his own side in an offhand way. - Mark V. Shaney
|
||||
Sperm bank sued for tossing samples - Headline in Star-Ledger
|
||||
boot: nop...cfs...session...no physical memory
|
||||
Computers come in putty-colored boxes and have AUTOEXEC.BAT files and run screen-savers with flying toasters, and brains do not. - Steven Pinker
|
||||
We have discovered a pervasive nonstationarity.
|
||||
verb = a > b ? 'a': c > d ? 'd': 'c';
|
||||
It's not only stupid it's wrong!
|
||||
Lucky Numbers 12, 14, 19, 24, 36, 43
|
||||
The goal of the experimental trials with the artificial heart is to "double the life span of these patients" to 60 days, Lederman said.
|
||||
canlock: corrupted 0xcafebabe
|
||||
Devil Duckie, when you float, it's like I'm bathing in a flaming moat!
|
||||
He lost both Ali-pilaf and Vali-pilaf.
|
||||
He is a man who gives bread.
|
||||
We cut salt and bread together.
|
||||
You can't make stew with cheap meat.
|
||||
Absquotilate it in style, you old skunk,..and show the gentlemen what you can do.
|
||||
Cookie import from '/usr/dhog/lib/cookies' failed: fil
|
||||
Fortunately, Mac OS X supports *that* feature! - Brendan Connell
|
||||
The full documentation for cat is maintained as a Texinfo manual. If the info and cat programs are properly installed at your site, the command "info cat" should give you access to the complete manual.
|
||||
That's nothing.. RMS sent me a .doc file the other day...
|
||||
Notegroups! They kill you!
|
||||
Welcome, rqzgc_mfuv8
|
||||
Isn't it funny how people say they'll never grow up to be their parents, then one day they look in the mirror and they're moving aircraft carriers into the Gulf region? - the Onion
|
||||
Where would Christianity be if Jesus got eight to fifteen years, with time off for good behavior? -- New York Senator James H. Donovan on capital punishment.
|
||||
Few, few the bird make her nest.
|
||||
To become of bishop miller.
|
||||
The dress don't make the monk.
|
||||
He is mad to bind.
|
||||
He turns as a weath turcocl.
|
||||
After the paunch comes the dance.
|
||||
Linux programmer's mind: don't invalidate the D-cache unless it wasn't enabled
|
||||
Practically noiseless and impossible to explode. - ad for the 1897 Oldsmobile
|
||||
The essence of XML is this: the problem it solves is not hard, and it does not solve the problem well. - Phil Wadler, POPL 2003
|
||||
If you are idle for more than 1000 hours, the system will log you out. Please save reviews frequently.
|
||||
Setting up your SIP account will allow you to call both other SIP users as well as pstn phones. - Wim Sweldens
|
||||
What's grey? A melted penguin.
|
||||
Linux: the world's best text adventure game.
|
||||
i know what jmk did. he added reentrancy for threads. - boyd, about uintptr
|
||||
I recently visited plan9.bell-labs.com/wiki/plan9 and I would like to offer my web design services. I can help you with your Plan 9 website. - spam
|
||||
gcc is the holy cow of compilers, not the holy grail. - forsyth
|
||||
we live in a world of dogmas, probably trying to fight it with a stronger dogma is a bad idea, but trying to fight it with preumpted objectivism clearly doesn't work. - uriel
|
||||
Lucent is the best place 4 me to work. (Please keep confidential)
|
||||
Subject: There's More to Nevada Than You Think
|
||||
/* keep the code below somewhat more readonable; not used elsewhere */
|
||||
swapon: /dev/disk/by-uuid/928aaabd-5744-4f0a-b9ef-aa101f286514: Invalid argument - Linux
|
||||
I guess the idea is that ... even though the structures are all different.
|
||||
rusty: kernel pseudo files are not the place for chit-chat
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
futures>=3.2.0; python_version<'3'
|
||||
grpcio>=1.40.0
|
||||
tinode-grpc>=0.20.0b3
|
||||
importlib-metadata>=1.0; python_version<'3.8'
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import setuptools
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
with open('README.md', 'r') as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setuptools.setup(
|
||||
name="tinode-chatbot",
|
||||
version=git_version(),
|
||||
author="Tinode Authors",
|
||||
author_email="info@tinode.co",
|
||||
description="Tinode demo chatbot.",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/tinode/chat",
|
||||
packages=setuptools.find_packages(),
|
||||
install_requires=['grpcio>=1.40.0'],
|
||||
classifiers=(
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: Apache 2.0",
|
||||
"Operating System :: OS Independent",
|
||||
),
|
||||
)
|
||||
|
||||
def git_version():
|
||||
try:
|
||||
p = Popen(['git', 'describe', '--tags'],
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
p.stderr.close()
|
||||
line = p.stdout.readlines()[0]
|
||||
return line.strip()
|
||||
|
||||
except:
|
||||
return None
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"schema": "token", "secret": "mtXWlt9ERZCKsw9aFAABAFGGCnxinE8ruLE21t6SQfck4uBKCIy44kerjmOh4h1+", "expires": "2017-11-18T04:14:02Z"}
|
||||
|
||||
Reference in New Issue
Block a user