diff --git a/tn-cli/README.md b/tn-cli/README.md index 3358ac6e..6e0ad895 100644 --- a/tn-cli/README.md +++ b/tn-cli/README.md @@ -50,6 +50,7 @@ python tn-cli.py < sample-script.txt ### Local (non-networking) * `.await` - issue a gRPC call and wait for completion, optionally assign result to a variable. +* `.delmark` - use custom delete marker instead of default `DEL!`; needed when some value is to be removed rather than set to blank. * `.exit` - terminate execution and exit the CLI; also `.quit`. * `.log` - write a value of a variable to `stdout`. * `.must` - issue a gRPC call and wait for completion, optionally assign result to a variable; raise an exception if result is not a success. @@ -85,7 +86,7 @@ Macros are high-level wrappers for series of gRPC calls. Currently, the followin * `useradd` - create a new user account * `userdel` - delete user account (requires root privileges) * `usermod` - modify user account (requires root privileges) -* `vcard` - print user's public and private info (requires root privileges) +* `thecard` - print user's public and private info (requires root privileges) You can define your own macros in [macros.py](macros.py) or create a separate python module (you can load it via `--load-macros`). Refer to [macros.py](macros.py) for examples. diff --git a/tn-cli/macros.py b/tn-cli/macros.py index dc8435f7..3a0f1c1a 100644 --- a/tn-cli/macros.py +++ b/tn-cli/macros.py @@ -15,7 +15,7 @@ class Macro: self.parser = argparse.ArgumentParser(prog=self.name(), description=self.description()) self.add_parser_args() # Explain argument. - self.parser.add_argument('--explain', action='store_true', help='Only print out expanded macro') + self.parser.add_argument('--explain', action='store_true', help='Only print out expanded macro') def name(self): """Macro name.""" pass @@ -65,7 +65,9 @@ class Usermod(Macro): self.parser.add_argument('-U', '--unsuspend', action='store_true', help='Unsuspend account') self.parser.add_argument('--name', help='Public name') self.parser.add_argument('--avatar', help='Avatar file name') - self.parser.add_argument('--comment', help='Private comment on account') + self.parser.add_argument('--comment', help='Private comment on account') + self.parser.add_argument('--note', help='Account description') + self.parser.add_argument('--trusted', help='Add/remove trusted marker: verified, staff, danger') def expand(self, id, cmd, args): if not cmd.userid: @@ -91,6 +93,10 @@ class Usermod(Macro): set_cmd += ' --photo="%s"' % cmd.avatar if cmd.comment is not None: set_cmd += ' --private="%s"' % cmd.comment + if cmd.note is not None: + set_cmd += ' --note="%s"' % cmd.note + if cmd.trusted is not None: + set_cmd += ' --trusted="%s"' % cmd.trusted old_user = tn_globals.DefaultUser if tn_globals.DefaultUser else '' return ['.use --user %s' % cmd.userid, '.must sub me', @@ -162,6 +168,8 @@ class Useradd(Macro): self.parser.add_argument('--cred', help='List of comma-separated credentials in format "(email|tel):value1,(email|tel):value2,..."') self.parser.add_argument('--name', help='Public name of the user') self.parser.add_argument('--comment', help='Private comment') + self.parser.add_argument('--note', help='Public description') + self.parser.add_argument('--trusted', help='Add/remove trusted marker: verified, staff, danger') self.parser.add_argument('--tags', help='Comma-separated list of tags') self.parser.add_argument('--avatar', help='Path to avatar file') self.parser.add_argument('--auth', help='Default auth acs') @@ -182,6 +190,10 @@ class Useradd(Macro): new_cmd += ' --fn="%s"' % cmd.name if cmd.comment: new_cmd += ' --private="%s"' % cmd.comment + if cmd.note is not None: + set_cmd += ' --note="%s"' % cmd.note + if cmd.trusted is not None: + set_cmd += ' --trusted="%s"' % cmd.trusted if cmd.tags: new_cmd += ' --tags="%s"' % cmd.tags if cmd.avatar: @@ -287,8 +299,8 @@ class Chcred(Macro): '.use --user "%s"' % old_user] -class VCard(Macro): - """Prints user's VCard.""" +class Thecard(Macro): + """Prints user's theCard.""" def name(self): return "vcard" @@ -324,4 +336,4 @@ def parse_macro(parts): return macro.parser -Macros = {x.name(): x for x in [Usermod(), Resolve(), Passwd(), Useradd(), Chacs(), Userdel(), Chcred(), VCard()]} +Macros = {x.name(): x for x in [Usermod(), Resolve(), Passwd(), Useradd(), Chacs(), Userdel(), Chcred(), Thecard()]} diff --git a/tn-cli/tn-cli.py b/tn-cli/tn-cli.py index c0c2297a..be3ec628 100644 --- a/tn-cli/tn-cli.py +++ b/tn-cli/tn-cli.py @@ -78,6 +78,12 @@ RE_INDEX = re.compile(r"(\w+)\[(\w+)\]") # Macros module (may be None). macros = None +# String used as a delete marker. I.e. when a value needs to be deleted, use this string +DELETE_MARKER = 'DEL!' + +# Unicode DEL character used internally by Tinode when a value needs to be deleted. +TINODE_DEL = '␡' + # Python is retarded. class dotdict(dict): """dot.notation access to dictionary attributes""" @@ -86,20 +92,25 @@ class dotdict(dict): __delattr__ = dict.__delitem__ -# Pack user's name and avatar into a theCard. -def makeTheCard(fn, photofile): +# Pack name, description, and avatar into a theCard. +def makeTheCard(fn, note, photofile): card = None - if (fn != None and fn.strip() != "") or photofile != None: + if (fn != None and fn.strip() != "") or photofile != None or note != None: card = {} if fn != None: - card['fn'] = fn.strip() + fn = fn.strip() + card['fn'] = TINODE_DEL if fn == DELETE_MARKER or fn == '' else fn + + if note != None: + note = note.strip() + card['note'] = TINODE_DEL if note == DELETE_MARKER or note == '' else note if photofile != None: - if photofile == '': + if photofile == '' or photofile == DELETE_MARKER: # Delete the avatar. card['photo'] = { - 'data': '␡' + 'data': TINODE_DEL } else: try: @@ -203,6 +214,20 @@ def parse_cred(cred): return result +# Parse trusted values: [staff,rm-verified]. +def parse_trusted(trusted): + result = None + if trusted != None: + result = {} + for t in trusted.split(","): + t = t.strip() + if t.startswith("rm-"): + result[t[3:]] = TINODE_DEL + else: + result[t] = True + + return result + # Read a value in the server response using dot notation, i.e. # $user.params.token or $meta.sub[1].user def getVar(path): @@ -326,12 +351,12 @@ def accMsg(id, cmd, ignored): elif cmd.suspend == 'false': state = 'ok' - cmd.public = encode_to_bytes(makeTheCard(cmd.fn, cmd.photo)) + cmd.public = encode_to_bytes(makeTheCard(cmd.fn, cmd.note, cmd.photo)) cmd.private = encode_to_bytes(cmd.private) return pb.ClientMsg(acc=pb.ClientAcc(id=str(id), user_id=cmd.user, state=state, scheme=cmd.scheme, secret=cmd.secret, login=cmd.do_login, tags=cmd.tags.split(",") if cmd.tags else None, desc=pb.SetDesc(default_acs=pb.DefaultAcsMode(auth=cmd.auth, anon=cmd.anon), - public=cmd.public, private=cmd.private), + public=cmd.public, private=cmd.private, trusted=parse_trusted(cmd.trusted)), cred=parse_cred(cmd.cred)), extra=pb.ClientExtra(on_behalf_of=tn_globals.DefaultUser)) @@ -367,11 +392,11 @@ def subMsg(id, cmd, ignored): cmd.topic = tn_globals.DefaultTopic if cmd.get_query: cmd.get_query = pb.GetQuery(what=" ".join(cmd.get_query.split(","))) - cmd.public = encode_to_bytes(makeTheCard(cmd.fn, cmd.photo)) - cmd.private = encode_to_bytes(cmd.private) + cmd.public = encode_to_bytes(makeTheCard(cmd.fn, cmd.note, cmd.photo)) + cmd.private = TINODE_DEL if cmd.private == DELETE_MARKER else encode_to_bytes(cmd.private) return pb.ClientMsg(sub=pb.ClientSub(id=str(id), topic=cmd.topic, set_query=pb.SetQuery( - desc=pb.SetDesc(public=cmd.public, private=cmd.private, + desc=pb.SetDesc(public=cmd.public, private=cmd.private, trusted=parse_trusted(cmd.trusted), default_acs=pb.DefaultAcsMode(auth=cmd.auth, anon=cmd.anon)), sub=pb.SetSub(mode=cmd.mode), tags=cmd.tags.split(",") if cmd.tags else None), @@ -438,10 +463,10 @@ def setMsg(id, cmd, ignored): cmd.topic = tn_globals.DefaultTopic if cmd.public == None: - cmd.public = encode_to_bytes(makeTheCard(cmd.fn, cmd.photo)) + cmd.public = encode_to_bytes(makeTheCard(cmd.fn, cmd.note, cmd.photo)) else: - cmd.public = encode_to_bytes(cmd.public) - cmd.private = encode_to_bytes(cmd.private) + cmd.public = TINODE_DEL if cmd.public == DELETE_MARKER else encode_to_bytes(cmd.public) + cmd.private = TINODE_DEL if cmd.private == DELETE_MARKER else encode_to_bytes(cmd.private) cred = parse_cred(cmd.cred) if cred: if len(cred) > 1: @@ -451,7 +476,7 @@ def setMsg(id, cmd, ignored): 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), + public=cmd.public, private=cmd.private, trusted=parse_trusted(cmd.trusted)), sub=pb.SetSub(user_id=cmd.user, mode=cmd.mode), tags=cmd.tags.split(",") if cmd.tags else None, cred=cred)), @@ -603,7 +628,6 @@ def upload(id, cmd, args): return None - # Given an array of parts, parse commands and arguments def parse_cmd(parts): parser = None @@ -619,6 +643,8 @@ def parse_cmd(parts): parser.add_argument('--fn', default=None, help='user\'s human name') parser.add_argument('--photo', default=None, help='avatar file name') parser.add_argument('--private', default=None, help='user\'s private info') + parser.add_argument('--note', default=None, help='user\'s description') + parser.add_argument('--trusted', default=None, help='trusted markers: verified, staff, danger, prepend with rm- to remove, e.g. rm-verified') parser.add_argument('--auth', default=None, help='default access mode for authenticated users') parser.add_argument('--anon', default=None, help='default access mode for anonymous users') parser.add_argument('--cred', default=None, help='credentials, comma separated list in method:value format, e.g. email:test@example.com,tel:12345') @@ -646,6 +672,8 @@ def parse_cmd(parts): parser.add_argument('--fn', default=None, help='topic\'s user-visible name') parser.add_argument('--photo', default=None, help='avatar file name') parser.add_argument('--private', default=None, help='topic\'s private info') + parser.add_argument('--note', default=None, help='topic\'s description') + parser.add_argument('--trusted', default=None, help='trusted markers: verified, staff, danger') parser.add_argument('--auth', default=None, help='default access mode for authenticated users') parser.add_argument('--anon', default=None, help='default access mode for anonymous users') parser.add_argument('--mode', default=None, help='new value of access mode') @@ -680,8 +708,10 @@ def parse_cmd(parts): parser.add_argument('topic', help='topic to update') parser.add_argument('--fn', help='topic\'s title') parser.add_argument('--photo', help='avatar file name') - parser.add_argument('--public', help='topic\'s public info, alternative to fn+photo') + parser.add_argument('--public', help='topic\'s public info, alternative to fn+photo+note') parser.add_argument('--private', help='topic\'s private info') + parser.add_argument('--note', default=None, help='topic\'s description') + parser.add_argument('--trusted', default=None, help='trusted markers: verified, staff, danger') parser.add_argument('--auth', help='default access mode for authenticated users') parser.add_argument('--anon', help='default access mode for anonymous users') parser.add_argument('--user', help='ID of the account to update') @@ -748,6 +778,10 @@ def parse_input(cmd): elif parts[0] == ".verbose": parser = argparse.ArgumentParser(prog=parts[0], description='Toggle logging verbosity') + elif parts[0] == ".delmark": + parser = argparse.ArgumentParser(prog=parts[0], description='Use custom delete maker instead of default DEL!') + parser.add_argument('delmark', help='marker to use') + else: parser = parse_cmd(parts) @@ -755,6 +789,7 @@ def parse_input(cmd): printout("Unrecognized:", parts[0]) printout("Possible commands:") printout("\t.await\t\t- wait for completion of an operation") + printout("\t.delmark\t\t- custom delete marker to use instead of default DEL!") printout("\t.exit\t\t- exit the program (also .quit)") printout("\t.log\t\t- write value of a variable to stdout") printout("\t.must\t\t- wait for completion of an operation, terminate on failure") @@ -852,6 +887,11 @@ def serialize_cmd(string, id, args): stdoutln("Logging is {}".format("verbose" if tn_globals.Verbose else "normal")) return None, None + elif cmd.cmd == ".delmark": + DELETE_MARKER = cmd.delmark + stdoutln("Using {} as delete marker".format(DELETE_MARKER)) + 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)