def make_vcard(fn, photofile): card = None if (fn != None and fn.strip() != "") or photofile != None: card = {} if fn != None: card['fn'] = fn.strip() if photofile != None: if photofile == '': # Delete the avatar. card['photo'] = {'data': '␡'} else: try: f = open(photofile, 'rb') # File extension is used as a file type mimetype = mimetypes.guess_type(photofile) if mimetype[0]: mimetype = mimetype[0].split("/")[1] else: mimetype = 'jpeg' data = base64.b64encode(f.read()) # python3 fix. if type(data) is not str: data = data.decode() card['photo'] = {'data': data, 'type': mimetype} f.close() except IOError as err: stdoutln("Error opening '" + photofile + "':", err) return card
def expand(self, id, cmd, args): if not cmd.login: return None if not cmd.password: stdoutln("Password --password must be specified") return None if not cmd.cred: stdoutln("Must specify at least one credential: --cred.") return None varname = cmd.varname if hasattr( cmd, 'varname') and cmd.varname else '$temp' new_cmd = '.must ' + varname + ' acc --scheme basic --secret="%s:%s" --cred="%s"' % ( cmd.login, cmd.password, cmd.cred) if cmd.name: new_cmd += ' --fn="%s"' % cmd.name if cmd.comment: new_cmd += ' --private="%s"' % cmd.comment if cmd.tags: new_cmd += ' --tags="%s"' % cmd.tags if cmd.avatar: new_cmd += ' --photo="%s"' % cmd.avatar if cmd.auth: new_cmd += ' --auth="%s"' % cmd.auth if cmd.anon: new_cmd += ' --anon="%s"' % cmd.anon return [new_cmd]
def setMsg(id, cmd, ignored): if not cmd.topic: cmd.topic = tn_globals.DefaultTopic if cmd.public == None: cmd.public = encode_to_bytes(make_vcard(cmd.fn, cmd.photo)) else: cmd.public = encode_to_bytes(cmd.public) cmd.private = encode_to_bytes(cmd.private) cred = parse_cred(cmd.cred) if cred: if len(cred) > 1: stdoutln( 'Warning: multiple credentials specified. Will use only the first one.' ) cred = cred[0] return pb.ClientMsg(set=pb.ClientSet( id=str(id), topic=cmd.topic, query=pb.SetQuery(desc=pb.SetDesc(default_acs=pb.DefaultAcsMode( auth=cmd.auth, anon=cmd.anon), public=cmd.public, private=cmd.private), sub=pb.SetSub(user_id=cmd.user, mode=cmd.mode), tags=cmd.tags.split(",") if cmd.tags else None, cred=cred)), on_behalf_of=tn_globals.DefaultUser)
def attachment(filename): try: f = open(filename, 'rb') # Try to guess the mime type. mimetype = mimetypes.guess_type(filename) data = base64.b64encode(f.read()) # python3 fix. if type(data) is not str: data = data.decode() result = { 'fmt': [{ 'at': -1 }], 'ent': [{ 'tp': 'EX', 'data': { 'val': data, 'mime': mimetype, 'name': os.path.basename(filename) } }] } f.close() return result except IOError as err: stdoutln("Error processing attachment '" + filename + "':", err) return None
def expand(self, id, cmd, args): if not cmd.userid: return None # Suspend/unsuspend user. if cmd.suspend or cmd.unsuspend: if cmd.suspend and cmd.unsuspend: stdoutln("Cannot both suspend and unsuspend account") return None new_cmd = 'acc --user %s' % cmd.userid if cmd.suspend: new_cmd += ' --suspend true' if cmd.unsuspend: new_cmd += ' --suspend false' return [new_cmd] # Change VCard. varname = cmd.varname if hasattr( cmd, 'varname') and cmd.varname else '$temp' set_cmd = '.must ' + varname + ' set me' if cmd.name is not None: set_cmd += ' --fn="%s"' % cmd.name if cmd.avatar is not None: set_cmd += ' --photo="%s"' % cmd.avatar if cmd.comment is not None: set_cmd += ' --private="%s"' % cmd.comment old_user = tn_globals.DefaultUser if tn_globals.DefaultUser else '' return [ '.use --user %s' % cmd.userid, '.must sub me', set_cmd, '.must leave me', '.use --user "%s"' % old_user ]
def expand(self, id, cmd, args): if not cmd.userid: return None if not cmd.password: stdoutln("Password (-P) not specified") return None return ['acc --user %s --scheme basic --secret :%s' % (cmd.userid, cmd.password)]
def save_cookie(params): if params == None: return try: cookie = open('.tn-cli-cookie', 'w') json.dump(handle_login(params), cookie) cookie.close() except Exception as err: stdoutln("Failed to save authentication cookie", err)
def run(self, id, cmd, args): """Expands the macro and returns the list of commands to actually execute to the caller depending on the presence of the --explain argument. """ cmds = self.expand(id, cmd, args) if cmd.explain: if cmds is None: return None for item in cmds: stdoutln(item) return [] return cmds
def handle_login(params): if params == None: return None # Protobuf map 'params' is a map which is not a python object or a dictionary. Convert it. nice = {} for p in params: nice[p] = json.loads(params[p]) stdoutln("Authenticated as", nice.get('user')) tn_globals.AuthToken = nice.get('token') return nice
def inline_image(filename): try: im = Image.open(filename, 'r') width = im.width height = im.height format = im.format if im.format else "JPEG" if width > MAX_IMAGE_DIM or height > MAX_IMAGE_DIM: # Scale the image scale = min( min(width, MAX_IMAGE_DIM) / width, min(height, MAX_IMAGE_DIM) / height) width = int(width * scale) height = int(height * scale) resized = im.resize((width, height)) im.close() im = resized mimetype = 'image/' + format.lower() bitbuffer = memory_io() im.save(bitbuffer, format=format) data = base64.b64encode(bitbuffer.getvalue()) # python3 fix. if type(data) is not str: data = data.decode() result = { 'txt': ' ', 'fmt': [{ 'len': 1 }], 'ent': [{ 'tp': 'IM', 'data': { 'val': data, 'mime': mimetype, 'width': width, 'height': height, 'name': os.path.basename(filename) } }] } im.close() return result except IOError as err: stdoutln("Failed processing image '" + filename + "':", err) return None
def expand(self, id, cmd, args): if not cmd.userid: return None if not cmd.auth and not cmd.anon: stdoutln('Must specify at least either of --auth, --anon') return None set_cmd = '.must set me' if cmd.auth: set_cmd += ' --auth=%s' % cmd.auth if cmd.anon: set_cmd += ' --anon=%s' % cmd.anon old_user = tn_globals.DefaultUser if tn_globals.DefaultUser else '' return [ '.use --user %s' % cmd.userid, '.must sub me', set_cmd, '.must leave me', '.use --user "%s"' % old_user ]
def handle_ctrl(ctrl): # Run code on command completion func = tn_globals.OnCompletion.get(ctrl.id) if func: del tn_globals.OnCompletion[ctrl.id] if ctrl.code >= 200 and ctrl.code < 400: func(ctrl.params) if tn_globals.WaitingFor and tn_globals.WaitingFor.await_id == ctrl.id: if 'varname' in tn_globals.WaitingFor: tn_globals.Variables[tn_globals.WaitingFor.varname] = ctrl if tn_globals.WaitingFor.failOnError and ctrl.code >= 400: raise Exception(str(ctrl.code) + " " + ctrl.text) tn_globals.WaitingFor = None topic = " (" + str(ctrl.topic) + ")" if ctrl.topic else "" stdoutln("\r<= " + str(ctrl.code) + " " + ctrl.text + topic)
def upload(id, cmd, args): try: scheme = 'https' if args.ssl else 'http' result = requests.post( scheme + '://' + args.web_host + '/v' + PROTOCOL_VERSION + '/file/u/', headers = { 'X-Tinode-APIKey': args.api_key, 'X-Tinode-Auth': 'Token ' + tn_globals.AuthToken, 'User-Agent': APP_NAME + " " + APP_VERSION + "/" + LIB_VERSION }, data = {'id': id}, files = {'file': (cmd.filename, open(cmd.filename, 'rb'))}) handle_ctrl(dotdict(json.loads(result.text)['ctrl'])) except Exception as ex: stdoutln("Failed to upload '{0}'".format(cmd.filename), ex) return None
def expand(self, id, cmd, args): if not cmd.userid: return None if not cmd.cred: stdoutln('Must specify cred') return None num_actions = (1 if cmd.add else 0) + (1 if cmd.rm else 0) + (1 if cmd.validate else 0) if num_actions == 0 or num_actions > 1: stdoutln('Must specify exactly one action: --add, --rm, --validate') return None if cmd.add: cred_cmd = '.must set me --cred %s' % cmd.cred if cmd.rm: cred_cmd = '.must del --topic me --cred %s cred' % cmd.cred old_user = tn_globals.DefaultUser if tn_globals.DefaultUser else '' return ['.use --user %s' % cmd.userid, '.must sub me', cred_cmd, '.must leave me', '.use --user "%s"' % old_user]
def delMsg(id, cmd, ignored): if not cmd.what: stdoutln("Must specify what to delete") return None cmd.topic = cmd.topic if cmd.topic else tn_globals.DefaultTopic cmd.user = cmd.user if cmd.user else tn_globals.DefaultUser enum_what = None before = None seq_list = None cred = None if cmd.what == 'msg': if not cmd.topic: stdoutln("Must specify topic to delete messages") return None enum_what = pb.ClientDel.MSG cmd.user = None if cmd.msglist == 'all': seq_list = [pb.DelQuery(range=pb.SeqRange(low=1, hi=0x8FFFFFF))] elif cmd.msglist != None: seq_list = [ pb.DelQuery(seq_id=int(x.strip())) for x in cmd.msglist.split(',') ] elif cmd.what == 'sub': if not cmd.user or not cmd.topic: stdoutln("Must specify topic and user to delete subscription") return None enum_what = pb.ClientDel.SUB elif cmd.what == 'topic': if not cmd.topic: stdoutln("Must specify topic to delete") return None enum_what = pb.ClientDel.TOPIC cmd.user = None elif cmd.what == 'user': enum_what = pb.ClientDel.USER cmd.topic = None elif cmd.what == 'cred': if cmd.topic != 'me': stdoutln("Topic must be 'me'") return None cred = parse_cred(cmd.cred) if cred is None: stdoutln("Topic must be 'me'") return None cred = cred[0] enum_what = pb.ClientDel.CRED else: stdoutln("Unrecognized delete option '", cmd.what, "'") return None msg = pb.ClientMsg(on_behalf_of=tn_globals.DefaultUser) # Field named 'del' conflicts with the keyword 'del. This is a work around. xdel = getattr(msg, 'del') """ setattr(msg, 'del', pb.ClientDel(id=str(id), topic=topic, what=enum_what, hard=hard, del_seq=seq_list, user_id=user)) """ xdel.id = str(id) xdel.what = enum_what if cmd.hard != None: xdel.hard = cmd.hard if seq_list != None: xdel.del_seq.extend(seq_list) if cmd.user != None: xdel.user_id = cmd.user if cmd.topic != None: xdel.topic = cmd.topic if cred != None: xdel.cred.MergeFrom(cred) return msg
def run(args, schema, secret): try: if tn_globals.IsInteractive: tn_globals.Prompt = PromptSession() # Create secure channel with default credentials. channel = None if args.ssl: opts = (('grpc.ssl_target_name_override', args.ssl_host),) if args.ssl_host else None channel = grpc.secure_channel(args.host, grpc.ssl_channel_credentials(), opts) else: channel = grpc.insecure_channel(args.host) # Call the server stream = pbx.NodeStub(channel).MessageLoop(gen_message(schema, secret, args)) # Read server responses for msg in stream: if tn_globals.Verbose: stdoutln("\r<= " + to_json(msg)) if msg.HasField("ctrl"): handle_ctrl(msg.ctrl) elif msg.HasField("meta"): what = [] if len(msg.meta.sub) > 0: what.append("sub") if msg.meta.HasField("desc"): what.append("desc") if msg.meta.HasField("del"): what.append("del") if len(msg.meta.tags) > 0: what.append("tags") stdoutln("\r<= meta " + ",".join(what) + " " + msg.meta.topic) if tn_globals.WaitingFor and tn_globals.WaitingFor.await_id == msg.meta.id: if 'varname' in tn_globals.WaitingFor: tn_globals.Variables[tn_globals.WaitingFor.varname] = msg.meta tn_globals.WaitingFor = None elif msg.HasField("data"): stdoutln("\n\rFrom: " + msg.data.from_user_id) stdoutln("Topic: " + msg.data.topic) stdoutln("Seq: " + str(msg.data.seq_id)) if msg.data.head: stdoutln("Headers:") for key in msg.data.head: stdoutln("\t" + key + ": "+str(msg.data.head[key])) stdoutln(json.loads(msg.data.content)) elif msg.HasField("pres"): # 'ON', 'OFF', 'UA', 'UPD', 'GONE', 'ACS', 'TERM', 'MSG', 'READ', 'RECV', 'DEL', 'TAGS' what = pb.ServerPres.What.Name(msg.pres.what) stdoutln("\r<= pres " + what + " " + msg.pres.topic) elif msg.HasField("info"): switcher = { pb.READ: 'READ', pb.RECV: 'RECV', pb.KP: 'KP' } stdoutln("\rMessage #" + str(msg.info.seq_id) + " " + switcher.get(msg.info.what, "unknown") + " by " + msg.info.from_user_id + "; topic=" + msg.info.topic + " (" + msg.topic + ")") else: stdoutln("\rMessage type not handled" + str(msg)) except grpc.RpcError as err: # print(err) printerr("gRPC failed with {0}: {1}".format(err.code(), err.details())) except Exception as ex: printerr("Request failed: {0}".format(ex)) # print(traceback.format_exc()) finally: printout('Shutting down...') channel.close() if tn_globals.InputThread != None: tn_globals.InputThread.join(0.3)
def gen_message(scheme, secret, args): """Client message generator: reads user input as string, converts to pb.ClientMsg, and yields""" random.seed() id = random.randint(10000,60000) # Asynchronous input-output tn_globals.InputThread = threading.Thread(target=stdin, args=(tn_globals.InputQueue,)) tn_globals.InputThread.daemon = True tn_globals.InputThread.start() msg = hiMsg(id, args.background) if tn_globals.Verbose: stdoutln("\r=> " + to_json(msg)) yield msg if scheme != None: id += 1 login = lambda:None setattr(login, 'scheme', scheme) setattr(login, 'secret', secret) setattr(login, 'cred', None) msg = loginMsg(id, login, args) if tn_globals.Verbose: stdoutln("\r=> " + to_json(msg)) yield msg print_prompt = True while True: try: if not tn_globals.WaitingFor and tn_globals.InputQueue: id += 1 inp = tn_globals.InputQueue.popleft() if inp == 'exit' or inp == 'quit' or inp == '.exit' or inp == '.quit': # Drain the output queue. while pop_from_output_queue(): pass return pbMsg, cmd = serialize_cmd(inp, id, args) print_prompt = tn_globals.IsInteractive if isinstance(cmd, list): # Push the expanded macro back on the command queue. tn_globals.InputQueue.extendleft(reversed(cmd)) continue if pbMsg != None: if not tn_globals.IsInteractive: sys.stdout.write("=> " + inp + "\n") sys.stdout.flush() if cmd.synchronous: cmd.await_ts = time.time() cmd.await_id = str(id) tn_globals.WaitingFor = cmd if not hasattr(cmd, 'no_yield'): if tn_globals.Verbose: stdoutln("\r=> " + to_json(pbMsg)) yield pbMsg elif not tn_globals.OutputQueue.empty(): pop_from_output_queue() print_prompt = tn_globals.IsInteractive else: if print_prompt: sys.stdout.write("tn> ") sys.stdout.flush() print_prompt = False if tn_globals.WaitingFor: if time.time() - tn_globals.WaitingFor.await_ts > AWAIT_TIMEOUT: stdoutln("Timeout while waiting for '{0}' response".format(tn_globals.WaitingFor.cmd)) tn_globals.WaitingFor = None time.sleep(0.1) except Exception as err: stdoutln("Exception in generator: {0}".format(err))
def serialize_cmd(string, id, args): """Take string read from the command line, convert in into a protobuf message""" messages = { "acc": accMsg, "login": loginMsg, "sub": subMsg, "leave": leaveMsg, "pub": pubMsg, "get": getMsg, "set": setMsg, "del": delMsg, "note": noteMsg, } try: # Convert string into a dictionary cmd = parse_input(string) if cmd == None: return None, None # Process dictionary if cmd.cmd == ".log": stdoutln(getVar(cmd.varname)) return None, None elif cmd.cmd == ".use": if cmd.user != "unchanged": if cmd.user: if len(cmd.user) > 3 and cmd.user.startswith("usr"): tn_globals.DefaultUser = cmd.user else: stdoutln("Error: user ID '{}' is invalid".format(cmd.user)) else: tn_globals.DefaultUser = None stdoutln("Default user='******'".format(tn_globals.DefaultUser)) if cmd.topic != "unchanged": if cmd.topic: if cmd.topic[:3] in ['me', 'fnd', 'sys', 'usr', 'grp', 'chn']: tn_globals.DefaultTopic = cmd.topic else: stdoutln("Error: topic '{}' is invalid".format(cmd.topic)) else: tn_globals.DefaultTopic = None stdoutln("Default topic='{}'".format(tn_globals.DefaultTopic)) return None, None elif cmd.cmd == ".sleep": stdoutln("Pausing for {}ms...".format(cmd.millis)) time.sleep(cmd.millis/1000.) return None, None elif cmd.cmd == ".verbose": tn_globals.Verbose = not tn_globals.Verbose stdoutln("Logging is {}".format("verbose" if tn_globals.Verbose else "normal")) return None, None elif cmd.cmd == "upload": # Start async upload upload_thread = threading.Thread(target=upload, args=(id, derefVals(cmd), args), name="Uploader_"+cmd.filename) upload_thread.start() cmd.no_yield = True return True, cmd elif cmd.cmd in messages: return messages[cmd.cmd](id, derefVals(cmd), args), cmd elif macros and cmd.cmd in macros.Macros: return True, macros.Macros[cmd.cmd].run(id, derefVals(cmd), args) else: stdoutln("Error: unrecognized: '{}'".format(cmd.cmd)) return None, None except Exception as err: stdoutln("Error in '{0}': {1}".format(cmd.cmd, err)) return None, None
def delMsg(id, cmd, ignored): if not cmd.what: stdoutln("Must specify what to delete") return None enum_what = None before = None seq_list = None cred = None if cmd.what == 'msg': enum_what = pb.ClientDel.MSG cmd.topic = cmd.topic if cmd.topic else tn_globals.DefaultTopic if not cmd.topic: stdoutln("Must specify topic to delete messages") return None if cmd.user: stdoutln("Unexpected '--user' parameter") return None if not cmd.seq: stdoutln("Must specify message IDs to delete") return None if cmd.seq == 'all': seq_list = [pb.SeqRange(low=1, hi=0x8FFFFFF)] else: # Split a list like '1,2,3,10-22' into ranges. try: seq_list = [] for item in cmd.seq.split(','): if '-' in item: low, hi = [int(x.strip()) for x in item.split('-')] if low>=hi or low<=0: stdoutln("Invalid message ID range {0}-{1}".format(low, hi)) return None seq_list.append(pb.SeqRange(low=low, hi=hi)) else: seq_list.append(pb.SeqRange(low=int(item.strip()))) except ValueError as err: stdoutln("Invalid message IDs: {0}".format(err)) return None elif cmd.what == 'sub': cmd.topic = cmd.topic if cmd.topic else tn_globals.DefaultTopic cmd.user = cmd.user if cmd.user else tn_globals.DefaultUser if not cmd.user or not cmd.topic: stdoutln("Must specify topic and user to delete subscription") return None enum_what = pb.ClientDel.SUB elif cmd.what == 'topic': cmd.topic = cmd.topic if cmd.topic else tn_globals.DefaultTopic if cmd.user: stdoutln("Unexpected '--user' parameter") return None if not cmd.topic: stdoutln("Must specify topic to delete") return None enum_what = pb.ClientDel.TOPIC elif cmd.what == 'user': cmd.user = cmd.user if cmd.user else tn_globals.DefaultUser if cmd.topic: stdoutln("Unexpected '--topic' parameter") return None enum_what = pb.ClientDel.USER elif cmd.what == 'cred': if cmd.user: stdoutln("Unexpected '--user' parameter") return None if cmd.topic != 'me': stdoutln("Topic must be 'me'") return None cred = parse_cred(cmd.cred) if cred is None: stdoutln("Failed to parse credential '{0}'".format(cmd.cred)) return None cred = cred[0] enum_what = pb.ClientDel.CRED else: stdoutln("Unrecognized delete option '", cmd.what, "'") return None msg = pb.ClientMsg(on_behalf_of=tn_globals.DefaultUser) # Field named 'del' conflicts with the keyword 'del. This is a work around. xdel = getattr(msg, 'del') """ setattr(msg, 'del', pb.ClientDel(id=str(id), topic=topic, what=enum_what, hard=hard, del_seq=seq_list, user_id=user)) """ xdel.id = str(id) xdel.what = enum_what if cmd.hard != None: xdel.hard = cmd.hard if seq_list != None: xdel.del_seq.extend(seq_list) if cmd.user != None: xdel.user_id = cmd.user if cmd.topic != None: xdel.topic = cmd.topic if cred != None: xdel.cred.MergeFrom(cred) return msg
def print_server_params(params): servParams = [] for p in params: servParams.append(p + ": " + str(json.loads(params[p]))) stdoutln("\r<= Connected to server: " + "; ".join(servParams))
def serialize_cmd(string, id, args): """Take string read from the command line, convert in into a protobuf message""" messages = { "acc": accMsg, "login": loginMsg, "sub": subMsg, "leave": leaveMsg, "pub": pubMsg, "get": getMsg, "set": setMsg, "del": delMsg, "note": noteMsg, } try: # Convert string into a dictionary cmd = parse_input(string) if cmd == None: return None, None # Process dictionary if cmd.cmd == ".log": stdoutln(getVar(cmd.varname)) return None, None elif cmd.cmd == ".use": if cmd.user != "unchanged": if cmd.user: tn_globals.DefaultUser = cmd.user else: tn_globals.DefaultUser = None stdoutln("Default user='******'") if cmd.topic != "unchanged": if cmd.topic: tn_globals.DefaultTopic = cmd.topic else: tn_globals.DefaultTopic = None stdoutln("Default topic='" + cmd.topic + "'") return None, None elif cmd.cmd == ".sleep": stdoutln("Pausing for " + str(cmd.millis) + "ms...") time.sleep(cmd.millis / 1000.) return None, None elif cmd.cmd == "upload": # Start async upload upload_thread = threading.Thread(target=upload, args=(id, derefVals(cmd), args), name="Uploader_" + cmd.filename) upload_thread.start() cmd.no_yield = True return True, cmd elif cmd.cmd in messages: return messages[cmd.cmd](id, derefVals(cmd), args), cmd elif macros and cmd.cmd in macros.Macros: return True, macros.Macros[cmd.cmd].run(id, derefVals(cmd), args) else: stdoutln("Error: unrecognized: '{0}'".format(cmd.cmd)) return None, None except Exception as err: stdoutln("Error in '{0}': {1}".format(cmd.cmd, err)) return None, None